Add a bounded libcurl transport with verified TLS, no redirects or retries, sanitized failures, binary-safe requests, and resolved-address enforcement for cleartext loopback traffic. Add shell-free xdg-open launching and a one-shot IPv4 loopback callback receiver with strict HTTP/query parsing, deadlines, fixed callback path, and RAII cleanup. Token parsing, Secret Service, and Graph remain follow-up phases.
11 KiB
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
- The month is the home screen. Launching Nocal immediately shows six
complete weeks, Monday-first by default.
--week-startcan rotate the grid to any weekday. There is no dashboard or splash screen. - 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 moreline is used instead of clipping silently. - 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.
- 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.
- 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.
- 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:
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.
These adapters still do not make Office 365 usable. They include no token JSON parser, Secret Service backend, Graph call, or account UI. libsecret exposes synchronous operations that may block, so the next phase isolates its adapter off the render thread. Token parsing and secure persistence come before read-only Graph calendar discovery and delta synchronization. Remote writes remain gated on the durability and conflict paths being exercised end to end.
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 TARGETatomically 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 SOURCEvalidates 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_COLORand 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.