feat: initialize Nocal terminal calendar

This commit is contained in:
2026-07-17 21:24:06 +01:00
commit 22c6399056
37 changed files with 6146 additions and 0 deletions

89
docs/ARCHITECTURE.md Normal file
View File

@@ -0,0 +1,89 @@
# Architecture
Nocal is split into four layers. Dependencies point inward, keeping remote sync
and terminal rendering replaceable.
```text
src/main.cpp
├── tui/ ANSI rendering, input decoding, terminal lifecycle
├── storage/ iCalendar file adapter (later: SQLite cache)
└── domain/ Event, Calendar, civil dates, month layout and queries
future sync workers
├── caldav/ Nextcloud and generic CalDAV
├── google/ Google Calendar REST/OAuth
└── graph/ Microsoft Graph/OAuth
└──────────> domain/ through a SyncProvider interface
```
## Domain
The domain layer uses C++20 `<chrono>` civil dates and time points. It knows
nothing about escape sequences, files, OAuth, or HTTP. Month layout always
produces 42 cells, starting on Monday, which keeps redraw geometry stable.
Events retain a stable UID, title, half-open start/end interval, all-day flag,
optional descriptive fields, and calendar identity. Queries define overlap as
`event.start < range.end && event.end > range.start`; this matters for events
crossing midnight.
## Storage
Version 0.1 uses a user-owned iCalendar file, defaulting to
`$XDG_DATA_HOME/nocal/calendar.ics` or `~/.local/share/nocal/calendar.ics`.
The adapter unfolds content lines before parsing and escapes text on output.
On POSIX systems, writes are serialized with an advisory lock, staged in a
same-directory private temporary file, flushed, and atomically renamed over the
destination. A load retains an immutable snapshot of the exact source bytes.
Before saving, the writer compares that snapshot with the destination while it
holds the same lock used for replacement, then checks again after staging and
immediately before commit. This catches same-size and same-timestamp changes
and fully serializes cooperating Nocal writers. The lock is advisory: an
unrelated program can ignore it, so no portable filesystem API can provide a
true compare-and-swap against every possible external writer.
Before an existing destination is replaced, its exact bytes are atomically
written to the adjacent `.bak` file. Explicit backup restoration uses the same
lock, source-revision check, private staging, and atomic replacement path; it
does not consume the backup. The TUI mutates a copyable in-memory model and
rolls it back if persistence fails. Its bounded undo/redo history contains only
mutations that crossed the persistence boundary successfully.
The current writer deliberately refuses to mutate an existing file when the
loader encounters information it cannot round-trip, including recurrence,
alarms, attendees, time-zone identifiers, unknown properties, or malformed
components. Browsing remains available. This conservative boundary is more
important than partial editing because a successful-looking edit must not
erase unrelated calendar data.
Provider synchronization will not write directly into this UI file. It will use
a transaction-capable local cache and preserve remote ETags/sync tokens in a
provider metadata table.
The planned cache schema has `calendars`, `events`, `event_instances`, and
`sync_state`. Raw provider payloads are retained alongside normalized fields so
an older Nocal cannot destroy fields it does not understand.
## Terminal backend
The initial backend depends only on POSIX `termios`, `poll`, and ANSI/ECMA-48.
It uses the alternate screen, hides the cursor while rendering, restores all
terminal state through RAII, and repaints from a complete frame buffer. A resize
signal only sets a flag; terminal size and rendering are handled safely in the
main loop.
Colors are semantic ANSI slots and default background, intentionally delegating
actual RGB values to the terminal theme. This is both simpler and more native
than shipping an application theme that fights the desktop palette.
## Sync roadmap
All providers implement the same conceptual operations: authenticate, list
calendars, perform an incremental pull, push a local mutation, and resolve a
conflict. CalDAV uses sync tokens/ETags, Google uses page/sync tokens, and
Microsoft uses Graph delta links. Secrets belong in the Freedesktop Secret
Service, never in the calendar database or command line.
Conflict resolution is deterministic: unchanged side wins; otherwise retain
both versions and mark a conflict for the user. Network work happens outside the
render loop and publishes immutable snapshots to it.