Read notes verbatim and insert the final newline only when it is missing,
instead of stripping the terminator on load and appending it unconditionally
on save. The editor's `rows = #\n + 1` model then renders a file's POSIX
terminator as a visible trailing blank line — what a writer expects: open a
note and see (and land the caret on) the empty line the newline stands for.
Supersedes the strip-on-load / unconditional-append handling that shipped
with the prefs work (c535864), which kept the buffer newline-free and hid the
terminator. Load + save are now an identity round-trip for any device-written
file (all end in '\n'); files stay git-clean (exactly one terminator); and a
trailing blank line the writer leaves is mirrored, never doubled.
- load_path: read verbatim (drop the strip)
- save_path: guarded final-newline (drop the unconditional append)
- Prefs::to_toml: ends in a newline again — the guarded save leaves exactly
one, so the prefs file is byte-identical to before and its device-verified
round-trip still holds
- sd_fat spike: payload ends in '\n' so its exact-equality round-trip holds
Git-tracked editor preferences read at boot and toggled live on-device:
- Prefs type (line-based TOML parse/serialize, no crate on xtensa) held on
Editor; firmware reads /sd/repo/.typoena.toml before the first render and
falls back to per-key defaults. Keys: save_on_idle, format_on_save,
line_numbers (bool) + auto_sync (string, schema/default only until v0.7).
- line_numbers applied live (gutter_cols -> 0 when off).
- Palette > command mode toggles the three bools; the list stays open so
several flip in one visit, and :settings opens it directly. Each toggle
applies live and queues Effect::SavePrefs (host atomic-writes the file,
which rides the next :sync).
- save_on_idle honoured host-side as a silent, unformatted idle auto-save.
- to_toml is newline-free; save_path now appends exactly one terminator
unconditionally so buffers round-trip byte-for-byte (trailing blanks kept).
- Firmware 0.4.0 -> 0.5.0; new docs/typoena-toml.md reference. Also refreshes
the slice-3 macroplan status (delete fix was confirmed on device).
Add the POSIX line terminator on save and strip it on load, so files
written by the editor no longer trip git's "No newline at end of file".
Done at the persistence choke point, not in :fmt: the editor buffer is
newline-free by design (rows = #\n + 1), so a trailing '\n' in the buffer
would render a phantom blank last line. save_path adds exactly one '\n'
(guarded against doubling); load_path strips one back off so the buffer
stays newline-free and round-trips byte-stable.
Update the sd_fat spike payload to be newline-free so its byte-identity
round-trip assertion holds under the new normalizing contract.
:enew <name> creates a new file (empty, dirty, added to the palette list);
:delete unlinks the current file via a new Effect::Delete and switches to a
parked buffer or scratch. Scope is read from the path (local/x, repo/x) rather
than a modal prompt, and the /sd prefix is now optional in resolve_path.
On-device testing showed deletions never reached the remote: add_all(["*"])
alone does not stage a removal on this libgit2, so the tree came back unchanged
and the push was a silent no-op. stage_and_commit now runs add_all then
update_all(["*"]) (git add -u) — together git add -A. The :delete snackbar now
confirms the scoped file and flags that a Tracked file is local until :sync.
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.
Graduate the proven Spike 3 storage stack into firmware::persistence so the
editor and the spike share one implementation. Storage::mount brings up the
dedicated SPI3 bus (ADR-012) and mounts FAT with format_if_mount_failed=false
(never reformat the user's card). save() does the FAT-safe atomic write
(tmp -> fsync -> unlink -> rename); recover() reconciles a leftover *.tmp at
boot, keeping the committed file when the crash point is ambiguous and only
promoting the tmp when the target was already unlinked. load() caps reads at
256 KiB.