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.
231 lines
14 KiB
Markdown
231 lines
14 KiB
Markdown
# Nocal product and interaction specification
|
|
|
|
Nocal is a local-first, keyboard-driven month calendar for Linux terminals. Its
|
|
visual target is the information density of a desktop month view without
|
|
pretending that a terminal is a pixel canvas. The terminal emulator owns the
|
|
typeface and palette; Nocal owns hierarchy, spacing, and restrained emphasis.
|
|
|
|
## Product principles
|
|
|
|
1. **The month is the home screen.** Launching Nocal immediately shows six
|
|
complete weeks, Monday-first by default. `--week-start` can rotate the grid
|
|
to any weekday. There is no dashboard or splash screen.
|
|
2. **Useful at a glance.** Every visible day shows as many appointments as fit,
|
|
ordered as all-day first and then by start time. A final `+N more` line is
|
|
used instead of clipping silently.
|
|
3. **Keyboard-native.** Arrow keys and Vim keys move by day or week; month and
|
|
today jumps are single keystrokes. Every action remains discoverable in the
|
|
footer and help overlay.
|
|
4. **Terminal-native aesthetics.** Nocal uses default foreground/background and
|
|
the terminal's ANSI semantic colors. It never paints a fixed RGB theme over
|
|
the user's Kitty, foot, Alacritty, or WezTerm theme.
|
|
5. **Local-first, sync-ready.** The canonical domain model is independent of
|
|
iCalendar files and of any future remote API. Sync engines translate into
|
|
domain objects and never leak provider-specific types into the UI.
|
|
6. **Fast enough to feel instant.** Startup should remain below 50 ms for a
|
|
normal local calendar and redraw should be flicker-free at interactive
|
|
resize rates.
|
|
|
|
## Month view
|
|
|
|
The full terminal is treated as a responsive canvas:
|
|
|
|
```text
|
|
JULY 2026 Today Fri 17 Jul
|
|
Mon Tue Wed Thu Fri Sat Sun
|
|
┌────────────┬────────────┬────────────┬────────────┬────────────┬────────────┬────────────┐
|
|
│ 29 │ 30 │ 1 │ 2 │ 3 │ 4 │ 5 │
|
|
│ │ │ 09:30 Team │ │ All day … │ │ │
|
|
├────────────┼────────────┼────────────┼────────────┼────────────┼────────────┼────────────┤
|
|
│ … │
|
|
└─────────────────────────────────────────────────────────────────────────────────────────┘
|
|
←↓↑→ move PgUp/PgDn month t today ? help q quit 2 appointments
|
|
```
|
|
|
|
- At 100 columns and above, Nocal draws the full bordered grid.
|
|
- Narrow cells abbreviate times and ellipsize summaries by display width.
|
|
- When height is constrained, appointment lines are reduced before structural
|
|
chrome. At extremely small sizes a clear minimum-size message replaces a
|
|
broken grid.
|
|
- Days outside the focused month remain visible but dim.
|
|
- The selected date is reverse-video, today is bold/underlined, and collisions
|
|
use semantic ANSI accents. These attributes work on monochrome terminals.
|
|
- The footer reports the selected day's appointment count and the most useful
|
|
keys for the available width.
|
|
|
|
## Interaction map
|
|
|
|
| Action | Keys |
|
|
| --- | --- |
|
|
| Previous/next day | `Left` / `Right`, `h` / `l` |
|
|
| Previous/next week | `Up` / `Down`, `k` / `j` |
|
|
| Previous/next month | `PageUp` / `PageDown`, `p` / `n` |
|
|
| Jump to today | `t` |
|
|
| Next/previous appointment | `Tab` / `Shift-Tab` |
|
|
| Read focused appointment | `Enter` |
|
|
| Browse inside reader | arrows, `h j k l`, `Tab` / `Shift-Tab` |
|
|
| Open/close 42-day agenda | `g`; `Esc` also closes |
|
|
| Choose agenda occurrence | arrows, `j` / `k`, `Tab` / `Shift-Tab`, then `Enter` |
|
|
| Shift agenda by 42 days | `PageUp` / `PageDown`, `p` / `n` |
|
|
| Add appointment | `a` |
|
|
| Edit focused appointment | `e` |
|
|
| Delete focused appointment | `d`, then `y` to confirm |
|
|
| Undo/redo successful mutation | `u` / `Ctrl-R` |
|
|
| Move between editor fields | `Tab` / `Shift-Tab`, arrows |
|
|
| Save/cancel editor | `Ctrl-S` / `Esc` |
|
|
| Return to month/unfocus | `Esc` |
|
|
| Search appointments | `/`, then type and press `Enter` |
|
|
| Choose search result | arrows, `j` / `k`, then `Enter` |
|
|
| Toggle calendar visibility | `c`, arrows or `j` / `k`, then `Enter` |
|
|
| Help | `?` |
|
|
| Quit | `q` |
|
|
|
|
The first usable foundation supports day and appointment navigation, a
|
|
full-frame reader, a read-only 42-day agenda, validated add/edit/delete forms,
|
|
atomic `.ics` writes, and explicit non-interactive calendar import/export.
|
|
Dense days keep every appointment keyboard-reachable even when all lines do
|
|
not fit in the cell. Unknown or unsupported iCalendar content makes a source
|
|
read-only instead of being discarded. Saves reject external changes, retain a
|
|
last-known-good backup, and feed bounded session undo/redo history. Supported
|
|
recurring instances appear throughout the month, preserve civil time across
|
|
daylight-saving changes, and expose their source zone and whole-series mutation
|
|
semantics in the reader. A supported recurrence set combines `DTSTART`, rule
|
|
expansion, and compatible DATE or DATE-TIME `RDATE` starts, deduplicates starts,
|
|
and then removes `EXDATE` exclusions. `/` searches occurrence-aware title,
|
|
description, location, and calendar-ID matches in a finite ten-year window
|
|
centered on the selected date. `c` toggles session-only calendar visibility
|
|
consistently across the grid, appointment focus, and search without filtering
|
|
the model supplied to persistence. `g` opens an occurrence-aware agenda at the
|
|
selected month date; navigation is bounded to consecutive 42-day windows and
|
|
`Enter` returns to the exact occurrence in the month grid. `Esc` or `g` closes
|
|
the agenda without changing the month selection unless an occurrence was
|
|
chosen. Hidden calendars remain filtered there, while calendar selection and
|
|
search can overlay the agenda. Agenda navigation never persists state, and
|
|
add/edit/delete remain in the month workflow. Week-start selection rotates only
|
|
the month grid, header, and the visible range used by its month queries;
|
|
storage, agenda windows, and
|
|
recurrence rules are unchanged. The default remains Monday for backward
|
|
compatibility. The CLI accepts the exact lowercase values `monday`, `tuesday`,
|
|
`wednesday`, `thursday`, `friday`, `saturday`, and `sunday`. `RDATE` periods,
|
|
values incompatible with the event's `DTSTART` time basis or `TZID`, detached
|
|
`RECURRENCE-ID` overrides, and embedded `VTIMEZONE` definitions remain
|
|
read-only. Remaining local-data work includes detached overrides and rule
|
|
editing.
|
|
|
|
## Remote calendar foundation
|
|
|
|
Remote calendars must cross a durable local boundary before they are exposed to
|
|
the UI. That boundary is a private, provider-neutral SQLite cache in WAL mode,
|
|
with versioned migrations that either commit completely or leave the prior
|
|
schema intact. It keeps raw provider payloads, inactive calendars, and
|
|
tombstoned events so refreshes and older clients do not erase information. Each
|
|
opaque incremental cursor belongs to a particular calendar and time window and
|
|
is committed atomically with every pulled page. Normalized event instances feed
|
|
immutable snapshots rather than exposing provider response types to the month
|
|
view.
|
|
|
|
The cache also reserves transactional outbox and conflict records for future
|
|
offline writes, but remote mutation behavior is not part of this phase. OAuth
|
|
tokens and other credentials never belong in the calendar database; account
|
|
secrets will be held by the Freedesktop Secret Service.
|
|
|
|
The Microsoft sign-in design treats Nocal as a public desktop client: sign-in
|
|
will open the system browser, use the authorization-code flow with PKCE `S256`,
|
|
and return through a loopback redirect. Its application registration must be
|
|
multitenant for the intended organizational and personal Microsoft account
|
|
audience. Initial synchronization requests only delegated `Calendars.Read`, not
|
|
write access. Users can normally consent to that delegated permission, but
|
|
their organization's tenant policy may still require administrator consent or
|
|
deny the application. Nocal never embeds a client secret in its open-source
|
|
desktop binary.
|
|
|
|
The provider-neutral HTTP seam accepts an injected transport and can therefore
|
|
be exercised with deterministic fakes. It intentionally owns no generic retry
|
|
policy; provider operations must decide whether a retry is safe. The concrete
|
|
Linux adapter uses libcurl while preserving
|
|
TLS certificate and host verification, bounds responses and timeouts, and
|
|
follows neither redirects nor retries automatically. Cleartext HTTP exists only
|
|
for loopback tests. Browser opening invokes `xdg-open` without a shell.
|
|
|
|
The receiver listens only on `127.0.0.1`, chooses a dynamic port, advertises a
|
|
`localhost` URL with the fixed `/nocal/oauth/callback` path, and processes one
|
|
bounded callback. The Microsoft registration is a Mobile and desktop public
|
|
client with `http://localhost/nocal/oauth/callback`. Microsoft ignores the port
|
|
when matching localhost redirects. Literal HTTP `127.0.0.1` registration needs
|
|
a manifest edit, and IPv6 loopback redirects are currently unsupported.
|
|
|
|
The token and Secret Service boundary strictly parses
|
|
bounded token JSON and requires a refresh token at initial authorization. A
|
|
rotated refresh token replaces the stored value; if a refresh response omits
|
|
one, Nocal retains the prior refresh token. Access and refresh tokens are stored
|
|
only as a versioned libsecret secret keyed by an opaque local account ID. They
|
|
never appear in SQLite, logs, command lines, or environment variables.
|
|
|
|
Secret Service operations are synchronous but cancellable and run only on a
|
|
worker thread so a D-Bus or keyring wait cannot block rendering. The real
|
|
adapter is exercised against isolated D-Bus and gnome-keyring services. Missing
|
|
credentials leave an account disconnected. Locked-keyring, cancellation,
|
|
parsing, and storage failures are reported distinctly, with no plaintext
|
|
fallback and no partially connected account. Connection state is published
|
|
only after the complete secret store succeeds.
|
|
|
|
The Microsoft Graph read phase keeps Nocal a public desktop multitenant
|
|
application for organizational and personal
|
|
Microsoft accounts. It requests delegated `User.Read` for `/me` and delegated
|
|
`Calendars.Read` for calendars and events. Neither normally requires
|
|
administrator consent, although tenant policy may require approval or block
|
|
user consent. Access tokens are passed only to the operation that needs them;
|
|
Secret Service retains the persistent token set.
|
|
|
|
Calendar discovery reads every page and publishes an atomic replacement only
|
|
after the full listing succeeds. Stable provider/account identity gives each
|
|
calendar a deterministic local ID. Event reads request immutable Graph IDs and
|
|
use strict bounded parsing with credential-safe failures. Requests and opaque
|
|
next/delta links are restricted to HTTPS `graph.microsoft.com` URLs.
|
|
|
|
Event windows are half-open UTC intervals. Each event page, its deletions, and
|
|
its continuation cursor cross the cache boundary in one transaction. A partial
|
|
round can resume from its durable cursor, and deletion tombstones remain
|
|
explicit instead of silently dropping remote state.
|
|
|
|
The stable Graph v1.0 `calendarView` delta API is documented only for the
|
|
primary calendar. This slice discovers all calendars but delta-syncs only the
|
|
primary; it uses neither beta nor undocumented per-calendar routes. Secondary
|
|
calendar support requires a deliberate choice between v1 full-window
|
|
reconciliation with defined deletion semantics and beta-specific delta. Nocal
|
|
does not yet claim Outlook-equivalent coverage.
|
|
|
|
There is still no account UI/CLI orchestration or live Microsoft credential
|
|
integration, and remote writes remain out of scope. Account setup/orchestration
|
|
and the secondary-calendar reconciliation decision are the next product phase.
|
|
|
|
## Local file interchange
|
|
|
|
Import and export are explicit command-line workflows. They never contact the
|
|
network and do not enter the interactive TUI.
|
|
|
|
- `nocal --import SOURCE --calendar TARGET` atomically merges events into the
|
|
target. Exact same-UID, same-field events are duplicates and are skipped. A
|
|
differing same-UID event, an empty or duplicate UID or invalid event interval
|
|
in either file, or content in either file that cannot be safely round-tripped
|
|
rejects the complete import before the target is written. A successful
|
|
merge keeps the target's exact previous bytes as its adjacent `.bak`; a
|
|
duplicate-only import leaves both target and backup untouched.
|
|
- `nocal --export DESTINATION --calendar SOURCE` validates the source and
|
|
atomically creates a canonical calendar. It never overwrites an existing
|
|
destination and refuses any source whose events are invalid or that cannot
|
|
be safely round-tripped.
|
|
|
|
The two operations are mutually exclusive and cannot be combined with demo,
|
|
frame printing, or backup restoration. These restrictions keep destructive
|
|
ambiguity out of automation and make every file-writing mode deliberate.
|
|
|
|
## Accessibility
|
|
|
|
- Information is never encoded by color alone.
|
|
- `NO_COLOR` and terminals without color remain fully usable.
|
|
- Borders and icons have ASCII fallbacks; Unicode width is calculated rather
|
|
than assumed.
|
|
- Focus is always visible and keyboard operation is complete.
|
|
- Motion is limited to direct redraws; there are no decorative animations.
|