Rework the single Effect return into a drained effect queue
(Save{path,scope,contents} / Load / Publish / Pull) so one action can
ask the host for several ordered steps: opening a non-resident file
queues a Save of the outgoing dirty buffer then a Load of the target.
Keep the active buffer's fields inline on Editor and park inactive
buffers in a small LRU (<=3 resident = active + 2); switching back to a
resident buffer restores its caret/scroll/undo without touching the SD.
A dirty parked buffer is saved before eviction, so nothing leaves RAM
unsaved. `:e <path>` opens by prefix (/sd/repo -> Tracked, /sd/local ->
Local); `:sync` is refused in-core for a Local buffer.
Firmware drains the queue to empty each batch (a Load can cascade an
eviction Save) and persistence generalises the atomic save off the
hard-coded notes.md into load_path/save_path.
Also bump the side panel to FONT_9X15 and the `:` command line to
FONT_10X20 for legibility, word-wrapping the snackbar so a long notice
keeps its actionable tail.
Charwise `v` and linewise `V` selection with yank/delete/change on the
span; motions and counts extend it. Read-only View moves off v/V to
`gr` (go-read). Selection renders reverse-video on the 1-bit panel.
Normal motions factored into a shared move_by. Firmware -> 0.4.0.
A multi-line paste near the bottom of the screen left its later lines below
the viewport — adjust_scroll only kept the caret's (first) pasted line
visible. Add reveal(), which scrolls the end of the pasted block into view
while the caret stays on its first line (vim semantics unchanged).
Root cause, shipped safety net, and the specced eradication (a
persistent internal DMA scratch buffer in Epd so paints never allocate
mid-sync). Tracks hardware re-test and the eradication as follow-ups.
A screen refresh that ran while :sync had Wi-Fi + TLS up could fail to
allocate an internal DMA bounce buffer (ESP_ERR_NO_MEM); the error
propagated through ? out of main(), main_task returned from app_main(),
and the editor loop died while the USB and git threads kept running —
keys logged, panel frozen forever.
A paint is idempotent and retryable (the buffer is the source of truth),
so every editor-loop paint now logs and drops the frame instead of
propagating, leaves shown untouched so the next paint repaints the diff,
and forces a full refresh next to re-sync both RAM banks. Same contract
as save_note. The boot-time first render stays fatal on purpose.
Records v0.3 as delivered in core (host-tested, on-device smoke-test pending)
and notes the known dot-repeat limits. Also reorders the plan so the Status
block follows the macroplan source.
Yank/paste via one unnamed register: `y` operator (yy/yw/yiw/y$), `p`/`P`,
with `x`/`d`/`c` also filling it so `dd`…`p` moves a line. Undo/redo (`u`,
`Ctrl-r`) as bounded full-buffer snapshots — one Insert session is one undo
group. `.` replays the last change from its recorded keystrokes, so it
repeats insert sessions like `ciwfoo<Esc>`.
Spike 13 (line-number gutter) verified on the panel closes the last v0.2
gate. Record v0.2 as delivered, and give v0.2.5 a delivered date + learning
note to match v0.1/v0.2 (its source-block entry still read on-track).
Run :fmt in-core before :w/:sync emit their effect, so :sync is
fmt -> save -> commit -> push and :w saves formatted. Gated on the
format_on_save field (default on); the v0.5 .typoena.toml key will drive it.
Add Effect::Pull and the `:gl` command (fetch + fast-forward only, refuse on
divergence). The editor side is host-tested; the firmware arm is a stub posting
"pull: not wired yet (v0.7)" — the on-device fetch/fast-forward in git_sync is
v0.7 work (only push is wired today).
Handle DeleteWord (Ctrl-W) and DeleteLine (Cmd-Backspace) in Command mode:
Ctrl-W drops the previous word, Cmd-Backspace clears the line. Neither exits to
Normal on empty (unlike Backspace), so command editing stays on the line.
The file is the macroplan (plus per-version scope), so rename it to match and
retitle to "Macroplan"; update all inbound links and friendly labels across the
docs. Refresh the plan while here: v0.2 gutter built, :gl pull recorded (v0.7),
command-line editing (v0.4), and the format_on_save pref (v0.5).
Mark the v0.2 gutter built + host-tested (Spike 13 bench check still pending),
add the v0.5 .typoena.toml line_numbers toggle, and update CONTEXT.md screen
regions for the rebalanced 63-col writing region / 25-col side panel.
Reserve a left gutter for absolute line numbers, drawn on each logical line's
first display row and blank on wrapped continuation rows. Gutter width tracks
the buffer's line count (2 digits + separator, widening past 99 lines) and
steals its columns from the soft-wrap. Widen the writing region 60->63 cols so
the gutter doesn't narrow the text: a file up to 99 lines keeps a full 60-col
text column, at the cost of the side panel shrinking 30->25 cols.
Decode Ctrl+D/Ctrl+U as HalfPageDown/HalfPageUp intents (matching the existing
DeleteWord/DeleteLine chord pattern, so the editor stays ignorant of Ctrl).
They step display (soft-wrapped) rows, not logical lines, so half a page is
half the visible window regardless of how prose wraps: Normal moves the caret
and the viewport follows, View scrolls the viewport, Insert/Command are no-ops.
Relative numbering renumbers the whole gutter on every j/k, a tall partial
refresh per cursor move that eats the e-ink ghosting budget for no
proportionate gain. Gutter is now absolute-only; Spike 13 downgrades from a
genuine e-ink risk to a layout decision.
Update the House now that v0.1 is delivered and hardware-verified:
- Perception zone: Typoena column moves from v0.1 target to measured.
W6 3->4 (1 h soak attested), W1 4->2 (type latency measured ~630 ms,
a visible lag). Typoena total 52->51, lead over Pomera down to one.
- H1 target: v0.1 relaxed 200->400 ms and v1.0 150->300 ms; ~630 ms
still exceeds 400 ms, recorded as the open latency gap, not a pass.
- Fix pre-existing drift: TikZ W14 disagreed with the table/totals
(Pomera/Smart 2/5 vs 5/1); "thirteen rows" -> "fourteen".
- Hoist the House diagram above section 1 so the picture leads.
- Trim AI writing tells and thin em-dash density in the new prose.
Adds a ‡ footnote on H7 (mirroring the H4 boot-latency †): cold :sync ~16 s /
warm ~10 s measured 2026-07-11 — within the ≤30 s v0.1 target, marginal against
the ≤10 s v1.0 target (cold's one-time Wi-Fi+SNTP push it over). Links the
breakdown in notes/sync-latency.md.
quality-house.md mirrored qfd.md's §1/§2 catalogues in a separate file,
which silently drifted (H3 cadence read 20 there vs 64 in qfd). Fold the
House diagram, perception scores, and regen notes into qfd.md §3 with no
section renumbering, so inbound #3/#6/#7 anchors still resolve. Delete
quality-house.md; re-point quality-house-empty.md and the docs index.
New docs/notes/sync-latency.md breaks down the measured ~16 s cold :sync
(Wi-Fi + SNTP + one TLS push), explains the optimistic-retry handshake saving,
the reconcile/last-writer-wins semantics, and why the rest is near the protocol
floor. Linked from the notes index, the docs index, and the Git row of the root
README.
The pre-commit fetch cost ~6s on the measured cold sync (it did real work
absorbing a foreign push; ~3s + a full TLS handshake even when the remote is
unchanged). Drop it: push onto the current tip first, and only when the remote
rejects the push non-fast-forward do we fetch, mixed-reset onto origin, replay
the note, and retry. The happy path is now a single handshake.
- stage_and_commit extracted (used on the first attempt and the replay).
- reconcile_onto_origin replaces fast_forward_before_commit + fetch_and_integrate.
Mixed reset keeps the just-saved note; the replay lands it on origin's tip.
- Single-writer semantics: a foreign push resolves last-writer-wins instead of
bailing on divergence, so the device never gets stuck. A remote-only added file
it doesn't have would be dropped by the replay — needs the real merge path
(increment B), doesn't arise from this device's own use.
publish_cycle now times wifi assoc, SNTP, TLS setup and publish separately
and logs a one-line total. Cold sync shows all phases; a warm sync reads 0ms
for the first three. Makes the fetch added to publish measurable.
Two robustness fixes surfaced by commit 07d87772 (the first editor-driven
:sync), which shipped three macOS AppleDouble files to typoena-test:
- Staging filter: add_all now runs a per-path callback that skips ._* and
.DS_Store, so Finder/Spotlight cruft on the FAT card never lands in a
commit. Device-level, so it protects every repo without a per-repo
.gitignore.
- Pre-commit fast-forward: publish_once fetches origin and, if the local
branch is behind, fast-forwards to it via a MIXED reset before committing.
This lets the device absorb a foreign push (e.g. the sidecar cleanup) and
fast-forward cleanly instead of stacking a commit on a stale base and
diverging at push time. The mixed reset moves the ref+index but leaves the
working tree, so the just-saved unsynced note isn't lost.
Land the v0.1 editor integration: the git_sync module (libgit2 on the SD
/sd/repo, dedicated 96KB git thread, lazy Wi-Fi, :sync push with
synced/up-to-date/failed snackbars), the boot splash (Spike 9) plus its bin
and justfile recipes, and a power-on→cursor boot-timing log. Also re-syncs
the roadmap/spikes/v0.1-product status and adds the SD hardware reference
photo.
Ground the off-between-syncs assumption in the code: the shipped firmware
runs the stay-associated strategy the section argues against. Link
run_git_service so the claim is verifiable and mark teardown as v0.8 work.
Make explicit that the per-sync burst assumes a full radio de-init between
pushes, and that staying associated to skip handshakes loses on energy until
~150 syncs/hr — a regime a writing appliance never reaches.
Record the git-tracked .typoena.toml preferences file (save_on_idle,
auto_sync) and the palette `>` command mode that edits it in the v0.5
roadmap. Add a tradeoff-curves note deriving Wi-Fi sync energy as a 1/T
curve, which sets auto_sync's default to 10m (opportunistic, min-clamped)
rather than a 5m wall-clock timer.
Spike 3 (SD) is resolved (genuine 32 GB card mounts, verified on its own
SPI3 host per ADR-012) and the mount/save path is now wired into main.rs.
- README: move the SD/CMD59 open question to the retired-risks line; drop
the stale "blocked on SD (waiting on a compatible card)" from the status
blurb. Remaining v0.1 gate is the boot splash + wiring git publish.
- roadmap: flip the boot-load and save-to-SD v0.1 items to done, re-sync
the status blurb and macroplan note, re-point the config-at-boot TODO
from the (landed) SD wiring to the git-publish wiring.
The side-panel split (writing column + panel) is already built in the
editor crate; correct the stale "not built yet" note and add the
snackbar under v0.1.
Boot posts "loaded <name>" (the note's filename without suffix); :w and
:sync post "saved" / "save FAILED - retry :w" via Editor::set_notice, so
save state is visible on-device, where there is no serial console.
Two boot/panel UX changes that share the editor's constructor, so they
land in one commit:
- Power-on mode is now Normal (was Insert). with_text() opens a loaded
note in Normal with the caret on the last char (the resume point);
draw(false) now suppresses the caret in every mode, so the boot-error
screen renders as pure text rather than sprouting a block caret.
- notice/set_notice(): a transient side-panel message ("snackbar")
drawn under the word count and cleared on the next keystroke — no
timed repaint, which on e-ink would cost a ~630 ms flash to erase.
Make `just build`/`just flash` the nominal product build (--features git
+ LIBGIT2_SRC), and move the fast editor-only build to `build-light`/
`flash-light`. Verified the git default links libgit2 into the firmware
ELF (1.2 MB) cleanly.
Route :sync through publish(), whose git-push path is #[cfg(feature =
"git")]. Git code in the firmware binary is now only ever compiled with
--features git, so the default build stays a light editor build with no
git2 crate and (since the justfile only sets LIBGIT2_SRC for git recipes)
no libgit2 component. Feature is off by default; :sync saves locally.
Reconciles a split-brain: fd62fac was pushed to GitHub only (not apoena)
and is an empty commit. Merging lets both remotes fast-forward to a single
head without rewriting the already-pushed persistence-wiring commits.