- Rust 81.9%
- Slint 14.5%
- Kotlin 2%
- Python 0.9%
- Shell 0.7%
Vier Motive, dieselben wie deutsch: Monat, Tag mit mehreren Terminen, Woche, Nextcloud verbinden. Aufgenommen aus der Release-APK der Fassung 0.6.0 auf dem AVD ngencal_laden, 1080x1920 -- das Telefon-AVD hat 1080x2400 und waere mit 2,22:1 ueber Plays Grenze von 2:1. Zwei Kleinigkeiten, die das Bild sonst verdorben haetten und die in ladeneintrag-en.md notiert sind: ohne vorher erteilte Benachrichtigungs- Erlaubnis steht unten eine rote Warnung, und das Kontoblatt gehoert ueber die Monatsansicht, weil das deutsche 05 es so zeigt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|---|---|---|
| .forgejo/workflows | ||
| .vscode | ||
| doc/iphone | ||
| ngen_nc_cal_android | ||
| ngen_nc_cal_core | ||
| ngen_nc_cal_desktop | ||
| pruefung | ||
| tests | ||
| .gitignore | ||
| ARCHITEKTUR.md | ||
| Cargo.lock | ||
| Cargo.toml | ||
| check.sh | ||
| CLAUDE.md | ||
| DATENSCHUTZ.md | ||
| FORTSCHRITT.md | ||
| GELERNT.md | ||
| LICENSE | ||
| PRUEFLAUF.md | ||
| PRUEFPLAN.md | ||
| README.md | ||
ngenNcCalendar
Lokaler Kalender in Rust mit CalDAV-Anbindung an Nextcloud, für Linux, Windows und Android. Die Daten liegen lokal; der Server ist ein Replikat, nicht die Voraussetzung zum Arbeiten.
Dieses Projekt ist KI-Programmiert und vom Menschen geleitet.
Stand: v0.1.0 veröffentlicht, Meilensteine 1 bis 5 abgeschlossen — Monatsansicht, lokaler SQLite-Speicher, CalDAV in beide Richtungen, Konto-Einrichtung, Wiederholungsregeln, Zeitzonen samt Windows-Namen, Anlegen/Ändern/Löschen, Konfliktauflösung, selbsttätiger Abgleich, Erinnerungen als Systembenachrichtigung und ICS-Export. Alles gegen eine echte Nextcloud durchgespielt. Offen: Android.
- FORTSCHRITT.md — Arbeitsstand und die nächsten Schritte
- ARCHITEKTUR.md — die Entwurfsentscheidungen und ihre Begründung
Wo das Projekt liegt
Zuhause auf einem selbst betriebenen Forgejo, gespiegelt nach GitLab:
| Quelle | https://git.bg-ak.de/leonhard.suckau/ngenNcCalendar — hier laufen CI und Releases |
| Spiegel | https://gitlab.com/audioprog/ngennccalendar — öffentlich erreichbar, trägt die Datenschutzerklärung für Google Play |
Gespiegelt wird serverseitig: Forgejo schiebt bei jedem Push nach GitLab (Repo-Einstellungen → Mirror-Einstellungen). Lokal bleibt es bei einem einzigen Remote — zwei Push-Adressen am selben Remote wären der andere Weg, aber dann hinge der Spiegel an diesem einen Rechner, und ein Push von woanders käme dort nie an.
Der Spiegel überträgt Commits und Tags, keine Releases: die Archive und die APK liegen weiterhin nur am Forgejo.
Aufbau
ngen_nc_cal_core Plattformunabhängiger Kern — kennt keine GUI und kein OS.
Domänenmodell, Kalenderrechnung, später iCalendar, SQLite,
CalDAV-Sync. Vollständig headless testbar.
ngen_nc_cal_desktop Desktop-GUI (Slint), Binary `ngennccalendar`.
Hält nur den Ansichtszustand und füllt die Slint-Modelle.
Diese Trennung ist der Kern des Entwurfs: Android bekommt später ein eigenes Frontend auf demselben Kern — kein zweiter Sync-Pfad, kein zweites Datenmodell.
ngen_nc_cal_core/src/model.rs– Kalender, Termine, die drei iCalendar-Zeitformenngen_nc_cal_core/src/month.rs– 6×7-Monatsraster, Kalenderwoche, deutsche Datumsformatengen_nc_cal_core/src/source.rs–EventSource: die Abfrage-Schnittstelle der Frontendsngen_nc_cal_core/src/sqlite.rs– lokale Datenbank: Schema, Migration, Bereichsabfragengen_nc_cal_core/src/store.rs– dieselbe Schnittstelle im Arbeitsspeicher (Tests, Beispieldaten)ngen_nc_cal_core/src/ical.rs– iCalendar lesen (RFC 5545)ngen_nc_cal_core/src/icalwrite.rs– iCalendar schreiben: neu anlegen, im Rohtext ändernngen_nc_cal_core/src/icalexport.rs– einen ganzen Kalender als eine.icsausgebenngen_nc_cal_core/src/projection.rs– daraus die Zeilen für die Anzeige, in der Anzeigezonengen_nc_cal_core/src/recurrence.rs– Wiederholungsregeln (RRULE) auswertenngen_nc_cal_core/src/search.rs– Titel und Ort aus dem Rohtext, für die Suche und die Ortslistengen_nc_cal_core/src/reminders.rs– wann eine Erinnerung (VALARM) fällig istngen_nc_cal_core/src/windowszones.rs– Windows-Zonennamen auf IANA-Zonen abbildenngen_nc_cal_core/src/series.rs– eine.icsfür einen Zeitraum ausrechnen, samt Ausnahmenngen_nc_cal_core/src/caldav/– CalDAV-Client;xml.rsist die serverfreie Hälftengen_nc_cal_core/src/sync.rs– der Abgleich: ctag → Delta → Vollabgleich als Rückfallngen_nc_cal_core/src/demo.rs– Beispieldaten, solange kein Konto eingerichtet istngen_nc_cal_desktop/ui/appwindow.slint– die GUIngen_nc_cal_desktop/src/main.rs– Verdrahtung Kern ↔ GUIngen_nc_cal_desktop/src/account.rs– Anmeldung und Abgleich im Hintergrund-Threadngen_nc_cal_desktop/src/form.rs– Termin-Formular: zwischen Textfeldern und Modellngen_nc_cal_desktop/src/secrets.rs– App-Passwort: Schlüsselbund oder eigene Dateingen_nc_cal_desktop/src/paths.rs– wo die Datenbank liegt (XDG bzw.%APPDATA%)ngen_nc_cal_desktop/src/notify.rs– fällige Erinnerung an den Benachrichtigungsdienst des Systemsngen_nc_cal_desktop/src/update.rs– Selbst-Aktualisierung über die Forgejo-Releasesngen_nc_cal_desktop/ui/icon.svg– Programmsymbol (Quelle);icon.icoist das daraus gebaute Windows-Symbol
Veröffentlichen
Ein Tag vX.Y.Z auf main löst .forgejo/workflows/release.yml aus: Linux und
Windows werden gebaut und als Release veröffentlicht. Die Version im Tag muss
zur Workspace-Version in Cargo.toml passen — der Workflow bricht sonst ab,
denn sonst böte die Selbst-Aktualisierung dasselbe Update endlos erneut an.
# Vor dem Tag: baut es überhaupt für Windows?
cargo check --target x86_64-pc-windows-gnu -p ngen_nc_cal_desktop
# Version in Cargo.toml anheben, committen, dann:
git tag v0.2.0 && git push origin v0.2.0
Der Windows-Check vor dem Tag ist kein Ritual. Der erste Release-Versuch
scheiterte daran, dass notify_rust::Notification::hint() nur unter Unix
existiert — hier fällt das nie auf, weil hier für Linux gebaut wird. Im
Workflow kommt der Windows-Bau nach dem Linux-Bau, ein solcher Fehler
kostet dort also erst eine Viertelstunde und dann einen verbrannten Tag.
Bewusst nicht ins Tor von test.yml genommen: die Prüfung für ein zweites Ziel
dauert rund zwölf Minuten und würde jeden Push ausbremsen.
Warum -gnu und nicht das CI-Ziel -msvc: für MSVC braucht cargo xwin noch
llvm-lib und lld-link, die hier nicht installiert sind. Für diese Art Fehler
genügt -gnu — beide Ziele sehen dieselben cfg(windows)-Zweige, sie
unterscheiden sich erst in C-Laufzeit und Linker.
Je Release entstehen drei Assets:
| Asset | wofür |
|---|---|
ngenNcCalendar-x86_64-unknown-linux-gnu.tar.gz |
Linux, mit Lizenz, Symbol und Desktop-Eintrag — das zieht der Updater |
ngennccalendar-x86_64-unknown-linux-gnu |
dieselbe Binärdatei nackt: herunterladen, chmod +x, starten |
ngenNcCalendar-x86_64-pc-windows-msvc.zip |
Windows |
Ein Tag baut zusätzlich das Android-Paket (.forgejo/workflows/android.yml)
und hängt zwei weitere Assets an:
| Asset | wofür |
|---|---|
ngenNcCalendar-android.aab |
zum Hochladen in die Google Play Console |
ngenNcCalendar-android.apk |
zum direkten Installieren, ohne Play |
Nicht beides auf demselben Gerät. Play signiert die verteilte App mit seinem eigenen Schlüssel neu; die APK hier trägt den Upload-Schlüssel. Android hält das für zwei verschiedene Apps und verweigert die Aktualisierung („App nicht installiert"). Der Wechsel ginge nur über Deinstallieren — und das löscht die lokale Datenbank samt allem, was noch nicht beim Server ist. Einen Weg wählen und dabei bleiben; siehe GELERNT.md.
Dafür braucht es vier Secrets — ANDROID_KEYSTORE_BASE64,
ANDROID_KEYSTORE_PASSWORD, ANDROID_KEY_ALIAS, ANDROID_KEY_PASSWORD —, sonst
bricht der Lauf gleich am Anfang ab statt nach zwanzig Minuten.
Die Namen sind Vertrag mit ngen_nc_cal_desktop/src/update.rs: gesucht wird per
Target-Triple nach einem Asset, dessen Name auch ngenNcCalendar enthält,
mit ngennccalendar im Wurzelverzeichnis des Archivs. Die nackte Binärdatei ist
deshalb klein geschrieben — so fällt sie aus der Auswahl des Updaters heraus
und kann dem Archiv nicht den Rang ablaufen. asset_for nimmt nämlich den
ersten Treffer, nicht den besten. Ein Test in update.rs liest die
Workflow-Datei und schlägt fehl, wenn jemand daran dreht. Voraussetzung ist ein Secret PACKAGES_TOKEN
mit Scope write:repository.
Die Datenbank liegt unter ~/.local/share/ngennccalendar/ngennccalendar.sqlite
(Windows: %APPDATA%\ngenNcCalendar\). Die Statuszeile im Fenster nennt den Pfad
und sagt, ob gerade echte Daten oder Beispieldaten zu sehen sind.
Distributionspakete
Derselbe Tag löst zusätzlich .forgejo/workflows/packages.yml aus: ein .deb
und ein .rpm, beide in der Paketregistry von Forgejo — nicht als
Release-Asset. Die Asset-Namen im Release sind Vertrag mit der
Selbst-Aktualisierung; zwei weitere Dateien dort, deren Namen zufällig
x86_64 enthalten, wären ein Risiko ohne Gegenwert.
Die Registry-Seite in Forgejo zeigt dieselben Zeilen mit $distribution und
$component — das sind Platzhalter, keine Shell-Variablen. Kopiert man sie
unverändert in eine Shell, werden sie zu nichts, und apt meldet „Missgestalteter
Eintrag … (Suite)": eine Meldung, die aufs Format zeigt und nicht auf die
Ursache. Hier stehen die Werte deshalb ausgeschrieben.
Die Quelle heißt nach dem Registry-Eigentümer, nicht nach diesem Projekt.
Unter leonhard.suckau/debian liegen alle Pakete dieses Kontos, ngenAudioRecord
eingeschlossen. Wer je Projekt eine eigene .list anlegt, hat zweimal dieselbe
Quelle mit womöglich verschiedenen Schlüsselpfaden — und apt verweigert dann den
Dienst komplett: „Widersprüchliche Werte gesetzt für Option Signed-By".
# Debian, Ubuntu und Verwandte
sudo install -d /etc/apt/keyrings
curl -fsSL https://git.bg-ak.de/api/packages/leonhard.suckau/debian/repository.key \
| sudo tee /etc/apt/keyrings/forgejo-leonhard.suckau.asc >/dev/null
echo "deb [signed-by=/etc/apt/keyrings/forgejo-leonhard.suckau.asc] https://git.bg-ak.de/api/packages/leonhard.suckau/debian bookworm main" \
| sudo tee /etc/apt/sources.list.d/forgejo-leonhard.suckau.list
sudo apt update && sudo apt install ngennccalendar
Der Schlüssel wird ASCII-armiert ausgeliefert; signed-by kommt mit .asc
unmittelbar zurecht, ein gpg --dearmor ist unnötig.
# openSUSE
# Der Schlüssel muss VOR dem Hinzufügen eingelesen werden: Forgejo signiert die
# repomd.xml, und zypper bricht sonst mit „Signature verification failed" ab und
# überspringt die Quelle stillschweigend — die Installation scheitert dann mit
# „No provider of 'ngennccalendar' found", was wie ein fehlendes Paket aussieht.
sudo rpm --import https://git.bg-ak.de/api/packages/leonhard.suckau/rpm/repository.key
# Auch hier der Eigentümer als Name der Quelle, nicht das Projekt:
sudo zypper ar https://git.bg-ak.de/api/packages/leonhard.suckau/rpm/leap16 forgejo-leonhard.suckau
sudo zypper in ngennccalendar
Jedes Paket wird in einem Container seiner Zieldistribution gebaut, nicht
einmal gebaut und zweimal verpackt. Das ist keine Sorgfaltsgeste: bookworm
bringt glibc 2.36, Leap 16.0 bringt 2.40 (nachgemessen). Ein auf Leap gebautes
Programm verlangt Symbole, die bookworm nicht kennt — das Paket ließe sich
installieren und stürbe beim ersten Start. Aus demselben Grund heißt die
zypper-Gruppe leap16 und nicht stable: ein späteres Tumbleweed-Paket gehört
in eine eigene, nicht in dieselbe.
Die Abhängigkeiten stehen zur Hälfte von Hand da. dpkg-shlibdeps und
find-requires lesen die SONAMEs aus dem ELF-Header, sehen also nur, was der
Linker eingetragen hat. Beim ersten Bau kamen genau zwei Einträge heraus:
libc6 und libfontconfig1. Alles Weitere — X11, Wayland, EGL, GL,
xkbcommon — holt sich Slint zur Laufzeit per dlopen, und davon steht nichts
im Header. Das Paket hätte sich installieren lassen und wäre beim Start
gescheitert. Die Liste in ngen_nc_cal_desktop/Cargo.toml stammt deshalb aus
dem Binary selbst:
strings -a target/release/ngennccalendar \
| grep -oE 'lib[A-Za-z0-9_+-]+\.so(\.[0-9]+)?' | sort -u
Beim .deb stehen dort Debian-Paketnamen, beim .rpm die SONAMEs als
Capability — openSUSE benennt seine Pakete anders, aber jedes RPM stellt den
SONAMEn bereit, den es mitbringt. Die automatische Hälfte bleibt trotzdem
eingeschaltet, deshalb sind dpkg-dev und rpm-build im Container Pflicht.
Beides ist am 31.08.2026 gegen die Registry durchgespielt worden, jeweils in
einem leeren Container: apt-get install ngennccalendar zieht unter bookworm
und unter Ubuntu 26.04 alle zwölf Bibliotheken mit, zypper in ngennccalendar löst die SONAMEs unter
Leap 16.0 auf openSUSE-eigene Paketnamen auf (Mesa-libEGL1, Mesa-libGL1 …)
und installiert 56 Pakete. Das ist der Grund für die SONAME-Schreibweise: eine
Liste geratener openSUSE-Paketnamen wäre hier falsch gewesen.
Aus dem Paket installiert heißt: kein Selbst-Ersetzen. Liegt die Binärdatei unter
/usr/bin, verweigert der Aktualisieren-Knopf die Arbeit und nennt stattdessenapt upgradebzw.zypper up. Erkannt wird das am Pfad, nicht am Schreibrecht — als root dürfte das Programm nach/usr/binschreiben und täte damit genau das Falsche:dpkgundrpmführen Buch über jede Datei, die sie angelegt haben.
Voraussetzung ist, dass PACKAGES_TOKEN zusätzlich den Scope
write:package trägt. Für release.yml genügt write:repository; die Registry
ist ein eigener Scope. Der erste Schritt jedes Paketjobs prüft das Recht, bevor
eine Viertelstunde Bauzeit verbrannt ist.
Entwicklung
cargo run # Desktop-GUI starten
cargo test -p ngen_nc_cal_core # Kern ohne Netz und ohne HTTP-Stack
cargo test -p ngen_nc_cal_core --features caldav # zusätzlich CalDAV und Abgleich
Der Netzzugriff steckt hinter dem Feature caldav. Ohne das Feature hat
ngen_nc_cal_core keinen HTTP-Stack — das hält die Kern-Tests schnell und den
späteren Android-Build klein. Die CI prüft beide Varianten.
Klemmt die Verbindung zum Server, zerlegt dieses Werkzeug die Kette in Namensauflösung, TCP zu jeder gefundenen Adresse und die echte Anfrage:
cargo run -p ngen_nc_cal_core --features caldav --example connection -- cloud.example.de
Klemmt es erst beim Abgleich, geht dieses Werkzeug mit dem eingerichteten Konto den ganzen CalDAV-Ablauf durch — nur lesend, und ohne das App-Passwort auszugeben:
cargo run -p ngen_nc_cal_core --features caldav --example sync
Gegen eine echte Nextcloud prüfen lässt sich der ganze Ablauf mit dem
Integrationstest. Er schreibt, deshalb läuft er nur mit ausdrücklich
gesetzter Kalender-Adresse und legt ausschließlich Dateien mit dem Präfix
ngencal-test- an, die er hinterher wieder löscht — trotzdem gehört dafür ein
eigens angelegter Testkalender:
export NGENNCCAL_TEST_CALENDAR=https://cloud.example.de/remote.php/dav/calendars/leo/test/
export NGENNCCAL_TEST_USER=leo
export NGENNCCAL_TEST_PASSWORD=<App-Passwort>
cargo test -p ngen_nc_cal_core --features caldav --test nextcloud -- --nocapture
Statt eines vorhandenen Kalenders geht auch nur die Server-Adresse — dann legt sich der Test seinen Kalender selbst an und entfernt ihn wieder:
export NGENNCCAL_TEST_SERVER=https://cloud.example.de
Ohne diese Variablen überspringt er sich selbst; die CI braucht also keinen
Server. Für einen Lauf ohne fremde Instanz liegt in tests/ eine
Container-Beschreibung bereit:
podman-compose -f tests/nextcloud-compose.yml up -d
./tests/nextcloud-wait.sh
export NGENNCCAL_TEST_SERVER=http://127.0.0.1:8081
export NGENNCCAL_TEST_USER=admin NGENNCCAL_TEST_PASSWORD=admin-passwort
cargo test -p ngen_nc_cal_core --features caldav --test nextcloud -- --nocapture
podman-compose -f tests/nextcloud-compose.yml down
Das läuft rootless und ohne Sonderrechte; warum Host-Netz statt Port-Weiterleitung benutzt wird, steht in der Compose-Datei.
Die .slint-Datei lässt sich ohne Rust-Code vorschauen (lädt bei jedem Speichern neu):
slint-viewer --auto-reload ngen_nc_cal_desktop/ui/appwindow.slint
Ohne Daten aus Rust bleibt das Raster dabei leer — die Vorschau dient dem Layout, nicht dem Inhalt.
Bedienung
-
‹ / › oder Pfeiltasten links/rechts — Monat blättern
-
Heute oder T — zurück zum aktuellen Monat
-
Mehrtägige Termine laufen als durchgehender Balken über die Tage, in jeder Zelle auf derselben Höhe.
-
Klick auf einen Tag — Termine des Tages in der rechten Spalte; ein Klick auf einen Randtag blättert in dessen Monat
-
Konto einrichten — Adresse der Nextcloud eintragen, im Browser bestätigen
-
Abgleichen — von Hand; von selbst passiert es kurz nach dem Start und danach alle 15 Minuten
-
+ Neuer Termin in der Tagesspalte, Klick auf einen Termin zum Ändern. Titel, Ort, Beschreibung, Zeitraum, Wiederholung und Erinnerung stehen im Dialog. Bringt die Datei eine ungewöhnliche Erinnerung mit, steht sie zusätzlich in der Liste; was sich nicht schreiben lässt — mehrere, per E-Mail, zu einem festen Zeitpunkt —, bleibt unangetastet.
-
Suchen — über Titel, Ort und Beschreibung. Passt nur die Beschreibung, steht der Ausschnitt um die Fundstelle unter dem Treffer; sonst wüsste man nicht, warum er dasteht.
-
Symbol im Benachrichtigungsfeld — sofern der Schreibtisch eines anzeigt. Dann schließt der ✕ das Fenster nur weg: das Programm läuft weiter und erinnert weiter, das Menü am Symbol öffnet es wieder, gleicht ab oder beendet. Der Kurzhinweis am Symbol nennt den nächsten Termin. Zeigt der Schreibtisch kein Benachrichtigungsfeld, gibt es auch kein Symbol — dann beendet der ✕ das Programm wie zuvor, statt es unsichtbar weiterlaufen zu lassen.
Unter XFCE fehlt das Feld oft nur in der Leiste: Rechtsklick auf die Leiste → Neue Elemente hinzufügen → Status Tray-Erweiterung. Datum und Uhrzeit werden getippt (
TT.MM.JJJJ,HH:MM);1.8.26und9:00gehen auch. Änderungen sind sofort im Kalender und gehen gleich zum Server — ohne Netz warten sie in der Warteschlange
Wenn zwei Geräte denselben Termin ändern
Dann gewinnt niemand. Die Fassung des Servers steht im Kalender, die eigene daneben — mit Warnzeichen, eigenem Rand und dem Hinweis, dass sie nur lokal existiert. Ein Klick darauf öffnet sie mit zwei Möglichkeiten:
- Meine übernehmen — sie geht gegen den aktuellen Stand erneut zum Server.
- Meine verwerfen — nur die lokale Kopie verschwindet.
Darunter steht, worin sich beide unterscheiden — je Feld der Wert vom Server und ein Knopf, der ihn übernimmt. Die Zeile verschwindet dabei, so sieht man, was noch offen ist. Am Ende gilt, was im Formular steht: den Titel von der einen Fassung, den Ort von der anderen, ein Klick je Feld.
Automatisch zusammengeführt wird nichts, denn bei zwei verschiedenen Uhrzeiten gäbe es keine richtige Wahl.
Konto verbinden
Es muss kein App-Passwort von Hand angelegt werden: die App fragt eines an, der Browser öffnet sich zur einmaligen Bestätigung, danach kommt das fertige App-Passwort zurück. Das Kennwort des Kontos selbst sieht das Programm nie, und das App-Passwort lässt sich in Nextcloud jederzeit einzeln zurückziehen.
Wo das App-Passwort liegt, entscheidest du im selben Panel:
- Schlüsselbund des Systems — Secret Service unter Linux, Credential Manager unter Windows.
- Selbst speichern — eine Datei im Datenverzeichnis, unter Unix mit
0600. Nötig ohne Schlüsselbund (headless Linux), sonst eine bewusste Wahl.
Die getroffene Wahl steht im Panel, damit niemand über die Ablage seines Passworts im Unklaren ist.
iPhone und iPad
Für iOS gibt es ngenNcCalendar nicht, und das bleibt vorerst so: iOS spricht CalDAV selbst, und ein Nextcloud-Kalender landet dort mit ein paar Handgriffen im System-Kalender — samt Siri, Uhr und allem, was daran hängt. Eine eigene App wäre dagegen ein Rückschritt.
Die Anleitung dafür, mit dem Konfigurationsprofil der offiziellen Nextcloud-App: doc/iphone/ — English.
Warum kein fertiger Client
Die Entscheidung, unter Android nicht den System-Kalender und DAVx⁵ zu benutzen, sondern eine eigenständige App zu bauen, ist bewusst getroffen — Begründung und Konsequenzen stehen in ARCHITEKTUR.md, Abschnitt „Android".