Files
typewriter/docs/v0.5-palette-and-multi-file.md
Julien Calixte 55a8d21e0f docs(macroplan): split each version into its own page
Move each version's scope checklist and status out of the monolithic
macroplan into a dedicated docs/vX.Y-<slug>.md page; macroplan keeps the
source block, the rollup status, and a one-line summary + link per
version. v0.1 reuses its existing product/technical pages.

Also drop the "Optional column ruler at 80" requirement from v0.6.
2026-07-12 02:10:29 +02:00

14 KiB
Raw Blame History

v0.5 — File palette + multi-file

Part of the Typoena macro plan. Requirements and targets: qfd.md. Load-bearing decisions: adr.md. Prefs reference: typoena-toml.md.

Status: buffer foundation landed in core 2026-07-11 (slice 1 of 4), host-tested; the palette + transient panel (Spike 11) and delete → git-staging (Spike 14) remain the on-device gates. The single-file Effect return became a drained effect queue (Save{path,contents} / Load{path} / Publish / Pull), so one action can ask the host for several steps in order — opening a non-resident file queues a Save of the outgoing dirty buffer then a Load of the target. The multi-buffer state deliberately avoids a rope-per-buffer rewrite: the active buffer keeps its fields inline on Editor, inactive buffers park in a small LRU Vec<Buffer> (≤ 3 resident = active + 2), and a switch marshals fields in/out so the ~3k-line editing engine is untouched. A dirty parked buffer is saved before it is evicted (nothing leaves RAM unsaved); :e <path> opens by prefix (/sd/repo → Tracked, /sd/local → Local); :sync is refused in-core in a Local buffer. Firmware drains the queue to empty each batch (a Load can cascade an eviction Save), and persistence::{load_path,save_path} generalise the atomic save off the hard-coded notes.md.

Slice 2 of 4 landed in core 2026-07-11, host-tested: the Cmd-P file palette — a modal transient panel over the writing column with a bare fuzzy-search input (no > prefix: > is reserved for the command palette, slice 4 — VS Code semantics), the ranked list, and the selected row in reverse video. A pure host-testable fuzzy matcher (fuzzy_score: subsequence match, boundary + consecutive-run bonuses, no penalties) ranks results; an in-core MRU floats recently-opened files to the top on an empty query and is shared with :e (both flow through open_path). The host feeds the file list once at boot (set_file_list, enumerating /sd/repo + /sd/local, dotfiles skipped); Ctrl-n/Ctrl-p (fzf-style; Ctrl-d/Ctrl-u too) move the selection — the 60 % board has no arrow keys — Enter opens via the same park/evict path as :e, Esc (or Cmd-P again) closes. Same slice: Ctrl-n/Ctrl-p also work as down/up line motions in Normal mode (vim CTRL-Nj, CTRL-Pk, count-aware), which is why the palette opener moved to Cmd-P alone. Scope shows as the inline repo/… vs local/… label rather than the planned [git]/[local] badge — it also disambiguates subpaths, not just scope. 111 editor tests + 28 keymap tests pass; the no-git firmware binary builds clean. The transient-panel refresh (Spike 11) is CONFIRMED ON DEVICE 2026-07-12 — no ghosting (user flashed it and eyeballed the full-area partial the palette forces); Cmd-P opens it on-device too. Remaining v0.5 slice: 4 prefs + palette command mode.

Slice 3 (:enew + delete) COMPLETE + CONFIRMED ON DEVICE 2026-07-12 (committed c9c0716). :enew <name> creates a new file: empty, active, marked dirty so eviction/:w persists it, and added to the in-core file list so the palette finds it without a disk re-enumeration — no card IO until it is saved. :delete unlinks the current file (a new Effect::Delete the host services), then switches to the most-recently-parked buffer or an empty scratch; the discarded buffer is never saved even when dirty. Scope for a new file is read from the path, not a modal promptlocal/x / repo/x (the palette label form) select the scope, a bare name uses the current buffer's scope. Same change made the /sd prefix optional everywhere in resolve_path: /sd/repo/x, /repo/x, and repo/x all name one file and nothing resolves outside /sd (the writer can't reach anything else). Spike 14 (delete → git-staging) DID need a firmware fix. The first on-device test found add_all(["*"]) alone does not stage a deletion on this libgit2 build (the tree came back unchanged, so the second push was a silent "up to date" no-op — the "delete didn't work" report). Fix: stage_and_commit now runs add_all then update_all(["*"]) (git add -u), which removes index entries whose working-tree file is gone — together they are git add -A. Also, :delete gave no clear feedback, so the snackbar now names the scoped file and, for a Tracked file, that it is local until :sync (deleted repo/notes.md - :sync to publish). Deferred to later: greying the Publish affordance for a Local buffer, and the multi-file publish count. 123 editor tests + 28 keymap tests pass; the no-git firmware binary builds clean. The update_all fix (behind --features git, unbuildable locally) was verified on device 2026-07-12:enew test.txt:sync:delete:sync removed test.txt from origin.

Slice 4 (.typoena.toml prefs + palette > command mode) COMPLETE in core 2026-07-12, HOST-TESTED not yet on-device. A Prefs type (host-testable line-based TOML parse/serialize — flat key = value bools + one string with # comments, no crate pulled onto xtensa) lives on Editor; the host reads /sd/repo/.typoena.toml at boot and applies it before the first render, and a missing/partial file falls back to per-key defaults. Keys: save_on_idle, format_on_save, line_numbers (all bool, default on) and auto_sync (string, default "10m", schema + default only — nothing reads it yet). line_numbers is live: gutter_cols() returns 0 when off, so the text reclaims the gutter's columns (the gutter - 1 field width made saturating to avoid the underflow). The palette > command mode (VS Code semantics — a leading > in the query switches file search to the command list) exposes the three booleans as live toggles; Enter flips the pref, applies it at once, queues a new Effect::SavePrefs (the editor serializes; the host does the atomic write to .typoena.toml, which rides the next :sync to other devices), and confirms the new state on the snackbar. The list stays open after a toggle so several prefs flip in one visit (Esc/Cmd-P closes); :settings opens the palette straight into > mode as a one-command shortcut (both requested by the user 2026-07-12, chosen over a separate settings modal — same surface, no duplicate machinery). Committed c535864. Three "decide before build" calls: (1) the idle auto-save is unformatted:fmt runs only on explicit :w/:sync, so tables/blank-lines are never reflowed mid-session; (2) the per-device auto_sync override (card-local typoena.conf) is deferred — auto_sync is inert in v0.5, so there is nothing yet to override; (3) > auto sync: <dur> as a palette command is deferred to v0.7 — a control that changes a value nothing reads would be a dead switch. save_on_idle is honoured host-side: a silent idle auto-save (no snackbar, no forced e-ink flash — a safety net, not an action) fires once per typing burst after a 1.5 s pause. 141 editor tests + 28 keymap tests pass; the no-git firmware binary builds clean. Firmware bumped 0.4.0 → 0.5.0 (the v0.5 feature set is met). Boot-read of the prefs file CONFIRMED ON DEVICE 2026-07-12 — a .typoena.toml in typoena-test with non-default values (save_on_idle=false, line_numbers=false, auto_sync="5m") logged back prefs: Prefs { save_on_idle: false, format_on_save: true, line_numbers: false, auto_sync: "5m" } at boot, a byte-exact parse (comments skipped, bools + quoted string read). Full gate CLOSED 2026-07-12: the palette > live-toggle round-trip is confirmed — origin's .typoena.toml went line_numbers false → true via a device-authored publish (3c79f38), proving toggle → SavePrefs → atomic write → git add -A → push — and the save_on_idle autosave works on device too. v0.5 slice 4 fully DONE + on-device confirmed.

  • Cmd-P opens fuzzy file palette over both /sd/repo/ and /sd/local/landed and CONFIRMED ON DEVICE 2026-07-12 (Spike 11: no ghosting on the transient panel); scope shows as the inline repo/… / local/… label instead of a [git]/[local] badge.
  • [~] Open, switch, close buffers (keep ≤ 3 in memory) — open + switch + the ≤ 3 LRU-resident model with dirty-aware save-before-evict done in core (host-tested); :e <path> and the palette drive it today. Explicit close still to come.
  • :e and palette share the same recent-files list — both open via open_path, which pushes to the in-core MRU that orders the palette.
  • :enew creates a new file — done in core (host-tested) 2026-07-12. Scope is read from the path (local/x / repo/x select it, the palette label form; a bare name uses the current scope) rather than a modal prompt — the resolved scope is echoed in the snackbar. The /sd prefix is optional throughout (/sd/repo/x = /repo/x = repo/x).
  • Delete a file — core done (host-tested) 2026-07-12; :delete unlinks the current file via Effect::Delete. For a Tracked file the removal reaches the next :sync Publish's staged set. Spike 14 (on-device) found the staging incomplete: add_all(["*"]) alone did not stage the deletion, so stage_and_commit now also runs update_all(["*"]) (git add -u) — the two together are git add -A. A Local file is just unlinked. The snackbar now confirms the delete and flags that a Tracked file needs :sync. Verified on device 2026-07-12 — the :enew:sync:delete:sync cycle removed test.txt from origin.
  • [~] Ctrl-G is disabled / hidden when the current buffer is local-scope — :sync / Publish is blocked in-core for a Local buffer (posts "Publish unavailable (Local)"); the side-panel affordance that hides/greys the gesture is the remaining half.
  • The side panel briefly shows file count on Ctrl-G when the publish bundles more than one dirty Tracked file (e.g. "publishing 3 files: abc1234"), so workspace-scoped behaviour stays visible to the user
  • Preferences file /sd/repo/.typoena.toml — a git-tracked, hand-editable TOML file for editor behaviour, deliberately distinct from the /sd/typoena.conf card secrets (Wi-Fi / PAT / remote / author, gitignored, never committed — see v0.1). Read at boot; a missing file or key falls back to the defaults below. Core done 2026-07-12 (a Prefs type on Editor, host-testable parse/serialize, applied via Editor::set_prefs before the first render); full reference: typoena-toml.md. Keys:
    • save_on_idle (bool, default true) — auto-save the current buffer on the idle typing-pause, so :w becomes optional rather than required. Honoured host-side as a silent save (no snackbar, no forced e-ink flash — a safety net, not an action), unformatted, once per typing burst after a 1.5 s pause.
    • format_on_save (bool, default true) — run :fmt (table alignment, blank-line collapse, trailing-whitespace strip) on the buffer before it is persisted, so :sync is fmt → save → commit → push and :w saves formatted. Implemented in-core 2026-07-11 (Editor), now driven by this key. Open question RESOLVED (2026-07-12): fmt runs only on an explicit :w/:sync; the save_on_idle auto-save is deliberately unformatted, so tables/blank lines are never reflowed mid-session (the caret would jump under you on every thinking pause).
    • line_numbers (bool, default true) — show the absolute line-number gutter (built always-on in v0.2). Off reclaims the gutter's columns for text (gutter_cols() → 0); the palette > line numbers: on/off command toggles it live. Done 2026-07-12.
    • auto_sync (duration string, default "10m"; "0" / omitted disables; min clamp ~"2m" so a palette typo can't drain the battery) — a max-staleness cap, not a wall-clock timer: opportunistic, rate-limited Publish. Push when already awake + dirty (coalesced into the idle-pause, ≤ once per auto_sync) and once on the way into sleep if dirty; never wake from deep sleep purely to sync. Wi-Fi energy is a 1/T curve whose knee sits at 510 min, and save_on_idle already owns local data safety — so 10 min halves the sync energy of a 5-min default for no real risk. Full derivation: tradeoff-curves/wifi-auto-sync.md. The schema + default ("10m") live here in v0.5 and round-trip through Prefs; nothing reads the value yet — the periodic side rides the better-git work (v0.7) and must interact with light / deep sleep (v0.8). Marked [~]: parsed and preserved, no behaviour.
    • Open question RESOLVED (2026-07-12): the per-device sync cadence override (a card-local typoena.conf layer over the committed prefs) is deferredauto_sync is inert in v0.5, so there is nothing yet to override; revisit when v0.7 makes the periodic push real.
  • Palette command mode — typing > at the Cmd-P palette switches it from file search to a command list (VS Code-style). Done in core 2026-07-12. The v0.5 commands toggle the three boolean .typoena.toml prefs — > save on idle, > format on save, > line numbers — each label carrying its live state; Enter flips the pref, applies it at once, queues Effect::SavePrefs (persist to the file), and confirms on the snackbar. The list stays open after a toggle (flip several, Esc/Cmd-P closes), and :settings opens it directly — both added 2026-07-12 as the "change config from the device" surface (chosen over a separate settings modal). This command list is the discoverable surface later actions (:fmt, theme, font) also register into. > auto sync: <dur> deferred to v0.7 — a value control that changes nothing readable would be a dead switch.