Add a read-only 42-day occurrence agenda with keyboard navigation, six-week paging, exact return-to-grid focus, search and calendar overlays, and bounded recurrence expansion. Keep agenda state outside persistence, document month-only mutations, and cover empty windows, filtering, overlays, navigation, rendering, and terminal restoration.
125 lines
6.5 KiB
Markdown
125 lines
6.5 KiB
Markdown
# 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.
|
|
|
|
Recurring events remain one domain object. Occurrence queries expand only the
|
|
requested time window and return values that point back to their source series,
|
|
preserving series-level storage and mutation semantics. The supported rule
|
|
surface is deliberately bounded to daily/weekly/monthly/yearly frequency,
|
|
interval, count or until, weekly `BYDAY`, monthly `BYMONTHDAY`, yearly
|
|
`BYMONTH`, and `EXDATE`. Invalid metadata retains at most the base event rather
|
|
than crashing a render.
|
|
|
|
Search matching is a pure domain predicate over normalized event text; the TUI
|
|
owns result presentation and occurrence identity. Recurring searches expand a
|
|
finite window of five years on either side of the selected date so an unbounded
|
|
rule cannot turn an interactive query into unbounded work.
|
|
|
|
Calendar visibility is session presentation state. The controller derives its
|
|
calendar list from event calendar identifiers and applies the same domain
|
|
predicate to month rendering, agenda queries, focus traversal, and search.
|
|
Hidden events remain in the canonical collection passed to persistence, and
|
|
focus identities are always derived from that complete collection so filtering
|
|
cannot renumber events with duplicate or missing UIDs.
|
|
|
|
The agenda is a TUI projection over domain occurrence queries, not a second
|
|
calendar model. It requests exactly one 42-day window at a time so recurrence
|
|
expansion remains finite, preserves occurrence identity when returning to the
|
|
month grid, and keeps its window and selection as non-persistent controller
|
|
state. Calendar and search overlays may be opened above it, but mutations stay
|
|
in the month workflow and continue to operate on the canonical event
|
|
collection.
|
|
|
|
Timed events retain their source basis: UTC, floating process-local time, or an
|
|
IANA `TZID`. C++20 time-zone conversion keeps zoned recurrences at their civil
|
|
wall time across daylight-saving transitions. The month grid displays instants
|
|
in the user's local zone; the reader also names an explicit source zone.
|
|
|
|
## 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
|
|
overrides, `RDATE`, custom `VTIMEZONE` definitions, alarms, attendees, unknown
|
|
properties, or malformed components. Supported recurrence rules, exclusions,
|
|
and system IANA time-zone identifiers round-trip semantically. Browsing remains
|
|
available for unsupported files. 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.
|