# v0.5 — File palette + multi-file > Part of the [Typoena macro plan](macroplan.md). Requirements and targets: > [qfd.md](qfd.md). Load-bearing decisions: [adr.md](adr.md). Prefs reference: > [typoena-toml.md](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` (≤ 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 ` 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-N`≡`j`, `CTRL-P`≡`k`, 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 ` 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 prompt** — `local/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: ` 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.** - [x] `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 ` **and the palette** drive it today. Explicit **close** still to come. - [x] `: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. - [x] `: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`). - [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 - [x] **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](v0.1-mvp-product.md)). 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`](typoena-toml.md). Keys: - [x] `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. - [x] `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). - [x] `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 5–10 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`](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. - [x] Open question RESOLVED (2026-07-12): the per-device sync cadence override (a card-local `typoena.conf` layer over the committed prefs) is **deferred** — `auto_sync` is inert in v0.5, so there is nothing yet to override; revisit when v0.7 makes the periodic push real. - [x] **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: ` deferred to v0.7** — a value control that changes nothing readable would be a dead switch.