nocal account connect microsoft runs the PKCE browser flow, reads /me, upserts the deterministic account row, and persists tokens to Secret Service as the final connected-state gate. Account IDs are hashed from the remote subject so reconnect is idempotent, and a secret-store failure leaves the same coherent state as a disconnect. Ships a placeholder client ID with NOCAL_MICROSOFT_CLIENT_ID override until a real Entra registration exists.
192 lines
11 KiB
Markdown
192 lines
11 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 paging/delta links must use HTTPS on
|
|
`graph.microsoft.com`. Event windows are half-open UTC ranges. Primary-calendar
|
|
delta pages commit their changes and cursor atomically, so an interrupted round
|
|
resumes without advancing past durable data.
|
|
|
|
Secondary-calendar reconciliation through stable Graph v1.0 reads the
|
|
documented per-calendar `calendarView`, finishes every page, and then atomically
|
|
replaces durable membership for that complete window. Events omitted from one
|
|
window lose cached instances only after no other complete window references
|
|
them; omission is not mislabeled as a remote deletion tombstone. This avoids
|
|
beta and undocumented delta routes, at the cost of refetching each secondary
|
|
window instead of incrementally updating it.
|
|
|
|
Account setup begins with `nocal account connect microsoft`: it runs the
|
|
PKCE browser flow, reads `/me`, upserts the deterministic account row, and
|
|
stores the tokens in the Secret Service keyring as the final connected-state
|
|
gate. This build ships a placeholder client ID; live connect requires a real
|
|
mobile/desktop Entra registration supplied via `NOCAL_MICROSOFT_CLIENT_ID`.
|
|
There is still no account TUI or sync orchestration, and remote writes remain
|
|
out of scope.
|