Files
nocal/docs/ARCHITECTURE.md

5.6 KiB

Architecture

Nocal is split into four layers. Dependencies point inward, keeping remote sync and terminal rendering replaceable.

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.

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.