Files
nocal/README.md
Bernardo Magri 0582444c61 feat(sync): add Microsoft Graph read adapter
Add strict /me identity, atomic paged calendar discovery, and resumable primary-calendar v1 delta synchronization through the durable provider cache. Keep opaque continuations on trusted Graph URLs and retain credentials only per operation.

Document that stable Graph v1 delta covers only the primary calendar; secondary calendars still require a v1 reconciliation or beta dependency decision.

Verified: 18/18 normal, 18/18 -Werror, and 18/18 ASan/UBSan tests.
2026-07-18 12:34:21 +01:00

186 lines
10 KiB
Markdown

# Nocal
Nocal is a fast, local-first C++20 month calendar designed for the Linux
terminal and Hyprland. It presents appointments inside a six-week month grid and
inherits its colors from your terminal theme.
This repository currently contains the first local-calendar vertical slice:
date/domain logic, guarded atomic iCalendar persistence, add/edit/delete forms,
a responsive ANSI terminal UI with month and agenda views, explicit safe
calendar import/export, tests, and Linux launch integration. A provider-neutral
SQLite cache now provides the durable boundary required before
Office 365 or any other remote provider is connected. See
[the product specification](docs/PRODUCT.md),
[architecture](docs/ARCHITECTURE.md), and [roadmap](docs/ROADMAP.md).
## Controls
Use arrow keys or `h j k l` to move, `PageUp`/`PageDown` or `p`/`n` to change
month, and `t` for today. On a day with appointments, `Tab` and `Shift-Tab`
move focus through them and `Enter` opens the appointment reader. In the
reader, arrows/Vim keys or Tab variants browse the day's appointments and
`Esc` returns to the month. Press `g` to open a read-only 42-day agenda starting
on the selected date. Arrows, `j`/`k`, or Tab variants choose an occurrence;
`Enter` returns to the month at that exact occurrence. `PageUp`/`PageDown` or
`p`/`n` shift the agenda by 42 days, while `Esc` or `g` returns without changing
the month selection. Press `c` to open the calendar picker; arrows or
`j`/`k` select a calendar and `Enter` toggles its in-session visibility. Press
`a` to add an appointment. Focus an
appointment and press `e` to edit it or `d` to delete it. Forms use `Tab` and
`Shift-Tab` (or arrows) between fields, `Space` for the all-day toggle,
`Ctrl-S` to save, and `Esc` to cancel. Back in the calendar, `u` undoes the
last successful mutation and `Ctrl-R` redoes it. Press `/` to search titles,
descriptions, locations, and calendar IDs. Search results use arrows or
`j`/`k`; `Enter` jumps to the exact occurrence and `Esc` returns to the month.
The calendar picker and search are also available over the agenda with `c` and
`/`. Add, edit, and delete remain month-view workflows.
Press `?` for help and `q` to quit.
## Build and run
```sh
nix-shell --run 'meson setup build && meson compile -C build'
./build/nocal --demo
```
Run the tests with `meson test -C build --print-errorlogs`. Nocal reads
`$XDG_DATA_HOME/nocal/calendar.ics` by default (falling back to
`~/.local/share/nocal/calendar.ics`), or accepts another `.ics` path as its
positional argument. `--demo` adds a few unsaved sample appointments and
`--print` emits a non-interactive frame. Weeks start on Monday by default. Pass
`--week-start DAY`, where `DAY` is exactly one of `monday`, `tuesday`,
`wednesday`, `thursday`, `friday`, `saturday`, or `sunday`, to rotate the month
grid and its weekday header. This is display-only: it does not change stored
events, recurrence interpretation, agenda windows, or calendar interchange.
Calendar interchange is explicit, non-interactive, and network-free:
```sh
nocal --import SOURCE.ics --calendar TARGET.ics
nocal --export DESTINATION.ics --calendar SOURCE.ics
```
Import atomically merges source events into the target. An event whose UID and
all fields exactly match an existing event is skipped. The complete operation
is rejected before the target is written if either file is unsafe to
round-trip or contains empty or duplicate UIDs or invalid intervals, or if an
imported UID collides with a different target event. A successful merge that
adds events retains the target's exact previous bytes as `TARGET.ics.bak`; an
import containing only exact duplicates does not rewrite the target or its
backup.
Export validates the source and atomically creates a canonical calendar at the
destination. It refuses to overwrite any existing destination and rejects a
source that fails event validation or that Nocal cannot safely round-trip.
`--import` and `--export` are mutually exclusive and cannot be combined with
`--demo`, `--print`, or `--restore-backup`.
Nocal expands and safely round-trips a focused iCalendar recurrence subset:
daily, weekly, monthly, and yearly rules with interval, count or until, common
weekly/monthly/yearly selectors, compatible DATE or DATE-TIME `RDATE` additions,
`EXDATE`, floating times, UTC, and system IANA `TZID` names. Occurrences merge
`DTSTART`, rule expansion, and `RDATE`, remove duplicate starts, and then apply
`EXDATE`. Recurring instances are individually navigable; edit and delete
clearly operate on the entire series.
Search expands recurring matches within five years on either side of the
currently selected date. This finite window keeps unbounded recurrence rules
responsive while one-time and recurring results share the same chronological
result list. Agenda recurrence expansion is bounded to its visible 42-day
window. Hidden calendars are excluded consistently from the month grid,
agenda, appointment focus, and search. Visibility and agenda position are
session-only presentation state; saves always retain events from hidden
calendars.
Calendars containing features this version cannot preserve—such as `RDATE`
periods or values incompatible with the event's `DTSTART` time basis or `TZID`,
detached `RECURRENCE-ID` overrides, ordinal `BYDAY`, embedded `VTIMEZONE`
definitions, alarms, or attendees—remain browsable but read-only. This guard
prevents a local edit from silently discarding unknown data.
Every replacement checks that the source has not changed since it was loaded.
If another Nocal instance or an external editor changes the file, the mutation
is rejected and the in-memory change is rolled back. Successful replacements
retain the immediately previous bytes as `calendar.ics.bak`. Run
`nocal --restore-backup [CALENDAR.ics]` for an explicit, atomic recovery; the
backup itself is kept so recovery can be repeated.
For a popup calendar binding and version-specific Hyprland rules, see
[Hyprland integration](docs/HYPRLAND.md).
## Remote-provider foundation
The remote-provider foundation is a private local SQLite database in WAL mode,
separate from the interactive `.ics` file workflow. Its schema is migrated by
version in all-or-nothing transactions. It preserves raw provider payloads,
inactive calendars, and tombstoned events so synchronization cannot silently
discard data that the current application does not understand. Each opaque
provider cursor is scoped to a calendar and synchronization window and is
committed atomically with the pulled page it represents. Normalized event
instances provide immutable snapshot input, while transactional outbox and
conflict records are reserved for later write synchronization.
The provider boundary models Microsoft sign-in as a public desktop client
using the system browser, the OAuth authorization-code flow, PKCE with `S256`,
and a loopback redirect. Nocal's application registration must be multitenant,
with the intended organizational/personal Microsoft account audience. The
read-only adapter requests delegated `User.Read` for `/me` plus
`Calendars.Read` for calendar and event reads. Neither delegated permission
normally requires administrator consent, but an organization's tenant policy
can still require approval or block user consent. An
open-source desktop executable cannot keep a client secret, so none is embedded
or expected.
The generic HTTP boundary uses injected transports and deterministic fakes. It
does not retry requests: provider code must later make retry decisions with the
operation's idempotency and server response in view. The concrete Linux adapter
uses libcurl with TLS verification and bounded
responses and timeouts, and neither automatic redirects nor retries. Cleartext
HTTP is limited to loopback tests. Browser launch executes `xdg-open` directly,
never through a shell.
The loopback receiver binds only `127.0.0.1`, advertises a dynamic-port
`localhost` URL at the fixed `/nocal/oauth/callback` path, and accepts one
bounded callback. Register Nocal in Entra as a Mobile and desktop public client
with `http://localhost/nocal/oauth/callback`. Microsoft documents that the port
is ignored when matching a `localhost` redirect, allowing a fresh local port;
an HTTP `127.0.0.1` redirect requires manifest editing, while IPv6 loopback
redirects are currently unsupported.
The token and Secret Service boundary parses token responses with strict size
bounds. Initial authorization must return a refresh
token; when Microsoft rotates it, the replacement is retained, while a refresh
response that omits one keeps the previously stored refresh token. Tokens live
only in a versioned libsecret payload keyed by an opaque local account ID. They
never enter SQLite, logs, command-line arguments, or environment variables.
libsecret operations are synchronous but cancellable and must run on a worker
thread, never the render thread. Isolated D-Bus and gnome-keyring tests exercise
the real adapter. A missing secret means the account is disconnected; locked,
cancelled, and storage failures remain distinct errors and never fall back to
another storage channel. Nocal does not mark an account connected until the
complete secret has been stored successfully.
The Microsoft Graph read phase calls `/me` and discovers every calendar through
the complete paged listing, replacing the
cached calendar set atomically only after every page succeeds. Provider/account
identity produces stable deterministic local IDs. Event requests prefer
immutable Graph IDs, parse strictly within fixed bounds, and keep credentials
out of errors. Access tokens are supplied per operation; Secret Service remains
the only persistent token store.
All Graph requests and opaque next/delta links must use HTTPS on
`graph.microsoft.com`. Primary-calendar event windows are half-open UTC ranges.
Each page, its tombstones, and its next or delta cursor commit atomically, so an
interrupted round resumes without advancing past durable data.
Stable Graph v1.0 documents `calendarView` delta only for the primary calendar.
Nocal therefore discovers all calendars but delta-syncs only the primary one;
it deliberately avoids beta and undocumented per-calendar routes. Secondary
calendars require a product decision between v1 full-window reconciliation with
explicit deletion semantics and a beta-specific delta implementation before
Outlook-equivalent coverage can be claimed. There is still no account UI/CLI
orchestration or live Microsoft credential integration, and remote writes
remain out of scope. The next phase is account setup and orchestration plus that
secondary-calendar decision.