Add explicit noninteractive CLI transfer modes with full event validation, idempotent duplicate handling, collision refusal, atomic import backups, and no-overwrite export creation. Cover pure merge semantics and end-to-end rollback paths for unsupported content, invalid identities, conflicts, option combinations, and existing destinations.
144 lines
7.7 KiB
Markdown
144 lines
7.7 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.
|
|
|
|
Explicit CLI import and export reuse the storage parser, safety classification,
|
|
validation, and atomic persistence boundary; these workflows do not depend on
|
|
the TUI and perform no network access. Import loads and validates both source
|
|
and target before staging a target replacement. UIDs in both files must be
|
|
non-empty and unique, every interval must be valid, and every component in both
|
|
files must be safe to round-trip. Matching UID plus identical fields is an
|
|
idempotent duplicate; matching UID with different fields is a collision that
|
|
aborts the whole operation. A committed merge writes the exact old target to
|
|
`.bak` before atomic replacement. If every source event is already present
|
|
exactly, import performs no write and does not alter the backup.
|
|
|
|
Export accepts only a valid, safely round-trippable source, serializes a
|
|
canonical calendar through private staging, and atomically creates a destination
|
|
that must not already exist. Destination refusal is part of the
|
|
commit boundary, so export never replaces an existing path. Import, export,
|
|
demo, non-interactive frame printing, and backup restoration are distinct
|
|
top-level application modes; import and export cannot be selected together or
|
|
combined with any of the other modes.
|
|
|
|
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.
|