feat: initialize Nocal terminal calendar

This commit is contained in:
2026-07-17 21:24:06 +01:00
commit 22c6399056
37 changed files with 6146 additions and 0 deletions

89
docs/ARCHITECTURE.md Normal file
View File

@@ -0,0 +1,89 @@
# 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.
## 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.
The current writer deliberately refuses to mutate an existing file when the
loader encounters information it cannot round-trip, including recurrence,
alarms, attendees, time-zone identifiers, unknown properties, or malformed
components. Browsing remains available. 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.

43
docs/HYPRLAND.md Normal file
View File

@@ -0,0 +1,43 @@
# Hyprland integration
The installed `nocal-hyprland` launcher chooses Kitty, foot, WezTerm, or
Alacritty and gives its Wayland window the dedicated `nocal` class/app-id. Set
`NOCAL_TERMINAL` to an executable path to override that order.
Hyprland 0.55 moved its configuration from hyprlang to Lua. Add this to
`~/.config/hypr/hyprland.lua` for a large, centered calendar on `SUPER+C`:
```lua
hl.bind("SUPER + C", hl.dsp.exec_cmd("nocal-hyprland"), {
description = "Open calendar"
})
hl.window_rule({
name = "nocal-calendar",
match = { class = "nocal" },
float = true,
center = true,
size = { "monitor_w*0.82", "monitor_h*0.82" },
persistent_size = true,
no_dim = true,
})
```
For Hyprland 0.54 and older hyprlang configurations, the equivalent is:
```ini
bind = SUPER, C, exec, nocal-hyprland
windowrule = float, class:^(nocal)$
windowrule = center, class:^(nocal)$
windowrule = size 82% 82%, class:^(nocal)$
windowrule = persistentsize, class:^(nocal)$
windowrule = nodim, class:^(nocal)$
```
Nocal deliberately does not request a particular opacity or color. Configure
those on the terminal and Nocal will inherit them; the application uses default
foreground/background plus the ANSI semantic palette.
The syntax above follows the current official [Hyprland bind documentation]
(https://wiki.hypr.land/Configuring/Basics/Binds/) and [window-rule
documentation](https://wiki.hypr.land/Configuring/Basics/Window-Rules/).

92
docs/PRODUCT.md Normal file
View File

@@ -0,0 +1,92 @@
# 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 Monday-first weeks. 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` |
| 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` |
| Help | `?` |
| Quit | `q` |
The first usable foundation supports day and appointment navigation, a
full-frame reader, validated add/edit/delete forms, and atomic `.ics` writes.
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. The next
local-data work adds `/` search, calendar visibility toggles, and recurring-event
and time-zone expansion.
## 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.

50
docs/ROADMAP.md Normal file
View File

@@ -0,0 +1,50 @@
# Roadmap
## Foundation — current milestone
- C++20 domain model and deterministic six-week month layout
- Responsive, theme-native ANSI month view with keyboard navigation
- Appointment focus, dense-day traversal, and full-frame detail reader
- Validated add/edit/delete forms with confirmation and failure rollback
- Guarded atomic `.ics` persistence, advisory locking, and round-trip tests
- Bounded session undo/redo, exact external-change detection, and backup recovery
- Meson build, Nix development shell, desktop entry, and Hyprland launcher
- Seeded sample data when explicitly requested, never silent data mutation
## 0.1 — complete local calendar
- Recurrence rules, exclusions, time zones, multi-day presentation
- Search, agenda view, calendar toggles, import/export, configurable week start
- Screen-reader-friendly linear view and full Unicode display-width handling
## 0.2 — durable cache and provider boundary
- SQLite WAL cache and migration framework
- Background job queue, immutable UI snapshots, observable sync status
- Secret Service credentials and OAuth callback helper
- Generic `SyncProvider` contract plus a fake provider test suite
## 0.3 — CalDAV
- Discovery and incremental sync for Nextcloud and standards-compliant servers
- ETag-aware writes, offline mutation queue, conflict UI
- TLS and failure-path integration tests
## 0.4 — hosted providers
- Google Calendar adapter with incremental synchronization
- Microsoft 365 adapter over Microsoft Graph delta queries
- Account/calendar management UI and per-calendar ANSI identity
## 1.0 — distribution quality
- Stable configuration/data format and documented recovery procedures
- Nomarchy package, Hyprland defaults, man page, shell completions
- Performance budgets exercised against large and adversarial calendars
- Fuzzed iCalendar parser, accessibility review, translations
## Quality gates
Every milestone requires clean warning-enabled builds, unit and integration
tests, sanitizer runs, terminal-state recovery after signals/errors, no network
access without explicit account setup, and no loss of unknown calendar data.