Commit Graph

344 Commits

Author SHA1 Message Date
Julien Calixte
abbd9d2e6e docs(context): align the glossary with the shipped v0.7 surface
Save is :w (not Ctrl-S), Publish is :gp (not Ctrl-G), :e was retired in
v0.6, and Publish's internals are the journaled-dirty-path splice with a
fetch-and-replay reconcile — never a merge. Also fix the principles
section's own use of the retired "status line" term.
2026-07-14 12:49:17 +02:00
Julien Calixte
4c29488f8a fix(docs): repair oxfmt emphasis corruption in CONTEXT.md
The repo-wide format pass mis-paired the bare FONT_9X15 underscore with
the _Avoid_ span in the side-panel entry, producing FONT*9X15 and
\_Avoid*. Backtick the identifier (and TW_* in the images note) so the
paragraph is unambiguous and the formatter stays idempotent.
2026-07-14 12:46:40 +02:00
Julien Calixte
625cd91967 docs: backtick underscore identifiers that destabilize oxfmt
oxfmt's markdown emphasis pass mis-pairs a bare intraword underscore
(N_tree, esp_map.c, auto_sync) with a later _italic_ span in the same
paragraph, corrupting both on the next format run (auto_sync ->
auto*sync). Backticking the identifiers removes the ambiguity and makes
fmt idempotent; fmt:check now passes repo-wide.
2026-07-14 12:43:05 +02:00
Julien Calixte
43d80b518f style: run oxfmt repo-wide
First full pass of the pnpm fmt toolchain: markdown table alignment,
TOML array wrapping, CSS/HTML expansion in the enclosure concept page.
No content changes; mermaid blocks untouched.
2026-07-14 12:40:51 +02:00
Julien Calixte
1493ee246f chore(fmt): exclude the vendored libgit2 submodule from oxfmt 2026-07-14 12:40:50 +02:00
Julien Calixte
4c61319995 docs(readme): refresh stale statements, link the doc and kaizen indexes
The README still described the pre-v0.1 world: no release shipped,
Ctrl-G as the publish gesture, editor.rs inside firmware, an ASCII
buffer, and a LittleFS config store that was never built. Reality:
v0.1-v0.7 shipped (firmware 0.7.0), publish/pull are :gp/:gl, the
palette is Cmd-P, the editor/display/keymap crates are top-level and
host-tested, and prefs live in the git-tracked .typoena.toml.
2026-07-14 11:01:36 +02:00
Julien Calixte
1c4bdb78c9 docs(kaizen): add an index README, matching the sibling folders
postmortems/, notes/, and tradeoff-curves/ each have an index; kaizen/
was the odd one out, so the docs index had to deep-link a single file.
2026-07-14 11:01:07 +02:00
Julien Calixte
459d1a5eb4 refactor(editor): split lib.rs into domain modules
Move-only: tests to tests.rs, then the 6.8k-line lib.rs into 12
concept modules (buffers, editing, fuzzy, markdown, motions, palette,
prefs, render, search, snippets, undo, visual). impl Editor is split
across them; moved private items become pub(crate) and the public API
is preserved via root re-exports. No logic changes; 221 tests and the
firmware build pass unchanged.
2026-07-14 10:53:33 +02:00
Julien Calixte
091688d7c3 feat(editor): let a palette-query space match any separator
Filenames use dashes where the typist thinks "space", so `la conv`
now finds la-convergence.md (space matches / _ - . or space).
2026-07-14 10:39:23 +02:00
Julien Calixte
057d6a6e2e docs(prefs): document open_last_on_boot 2026-07-14 10:35:09 +02:00
Julien Calixte
d4c59c2929 feat(firmware): boot into the last-active file
Record the active buffer in a device-local marker (/sd/.typoena-last,
beside the dirty journal) on every switch; when open_last_on_boot is set,
boot resumes it. Prefs are now read before the boot note is chosen.
A missing/garbled/stale marker falls back to notes.md — only the default
note stays boot-fatal.
2026-07-14 10:35:00 +02:00
Julien Calixte
72b9615105 feat(editor): add open_last_on_boot pref with palette toggle
Default true. Only the choice lives in the tracked toml; the last-active
path itself is device state the host keeps out of the repo.
2026-07-14 10:34:49 +02:00
Julien Calixte
2a12f95165 feat(editor): open the Cmd-P palette from every mode
Insert/Visual/View/Command now accept Cmd-P by first bailing out as Esc
would (end insert session, drop selection, abandon the : line), then
opening the palette; closing always lands in Normal. Opening mid-change
aborts the `.` recording so a palette round-trip is never replayed.
2026-07-14 10:33:16 +02:00
Julien Calixte
6fe8c86708 docs(sync): revise wifi-auto-sync curve with v0.7 field numbers
The ~8s-burst placeholder is replaced by the measured warm :gp
(19.1s = 10.3s splice + 5.9s push leg): K roughly doubles to
~0.6 mAh/sync and its mix shifts from radio to SD/CPU, but the splice
reruns per sync so the 1/T shape, the 5-10 min knee, and the 10-min
default all hold. TLS session resumption (2.4s vs 4.0s connect) and
the run-8 keep-alive death strengthen teardown-between-syncs; clean
ticks are radio-free via the dirty journal; :gl noted as outbound-only.
ASCII curve replaced with a mermaid xychart.
2026-07-14 10:07:06 +02:00
Julien Calixte
866b7b722a docs(v0.8): re-home the auto_sync runtime to v0.8
v0.7 closed with manual :gl/:gp only, leaving the auto_sync timer
unscheduled. It lands in v0.8 alongside the sleep transitions it must
respect and a new explicit item: per-sync Wi-Fi teardown (the git_sync
modem-ownership refactor), prerequisite for both sleep modes. Forward
pointers in typoena-toml.md and the v0.5 spec updated to match; the
v0.5 macroplan learning is left as a historical record.
2026-07-14 10:06:56 +02:00
Julien Calixte
60c9aef9dc docs(sync): record the images-off-card decision — skip-media-in-apply
Repo stays untouched (remanso reads images as git blobs); the device
half landed as media-invisible apply_tree_diff. Documents the accepted
trade: stale card media, phantom git-status noise, never hand-commit
from the card.
2026-07-14 10:05:41 +02:00
Julien Calixte
026eaea69a feat(sync): skip media paths in the pull apply
A pulled image blob is materialized whole in RAM before landing on the
card — history holds 16MB PNGs and a 38MB mp3 against 8MB PSRAM, the
last OOM path in :gl. The device never renders media, and the splice's
explicit-path staging means a missing image can't be committed as a
deletion, so media become invisible to apply_tree_diff: never written,
deleted, or belt-hashed. Blobs still arrive in .git via fetch; the
card's working copy stays text-only and goes deliberately image-stale.
2026-07-14 10:05:41 +02:00
Julien Calixte
3da31de0f2 docs(v0.7): finish version 2026-07-14 09:58:22 +02:00
Julien Calixte
6d5179fa11 docs(v0.7): record the warm :gp measurement — 19.1s, mix inverted
Push leg fell to 5.9s (session-resumed TLS + repacked pack); the splice
grew to 10.3s because a depth-4 path means ~5 loose tree writes, each
paying the FAT dir-scan cost, plus the session's first 1.7MB .idx map.
The publish-gap residual is now splice depth x loose-write cost.
2026-07-14 09:55:25 +02:00
Julien Calixte
fe62b47eee docs(sync): convict the loose-write residual — FAT linear dir scans
sd_bench dir-entry scaling: stat-miss is ~0.1 ms/entry and the
loose-object composite hits p50 402.9 ms at 256 siblings, reproducing
the field ~360-400 ms/loose-write from primitives. The objects/
fan-out is ~256 dirs, so every path resolution under it pays the scan.
Bounded and accepted; levers (fewer resolutions, pack-not-loose) go to
a future perf pass.
2026-07-14 09:44:47 +02:00
Julien Calixte
7378d30450 fix(sd_bench): remove the scratch tree without trusting dirent types
std's remove_dir_all reads directories as char devices through the
prebuilt libc's generic-unix DT table, unlinks them, and FatFS refuses
with FR_DENIED — the 6b fan-out subdirs were the first to trigger it.
Recurse with the same both-tables decoding as the palette walk.
2026-07-14 09:44:36 +02:00
Julien Calixte
cdfeabc152 feat(editor): rename :sync to :gp
Pairs with :gl as the push half of the git pair. Same behavior
(fmt → save → commit → push); notices, logs, and current READMEs
follow, historical docs keep :sync as a record of their time.
2026-07-14 09:42:30 +02:00
Julien Calixte
e161b1eae7 docs(v0.7): close the version — run-3 fast-forward pull verified
Pulled shape passed on device: apply_tree_diff 1271ms for 1 file, no
crash, interning confirmed (0 KB internal on re-walk). Records run-3
timings, the reconcile 30s→5s session-resumption win, and the
1-byte-load/0-byte-save trailing-newline watch item.
2026-07-14 09:35:35 +02:00
Julien Calixte
761fd3a750 feat(editor): smartcase + accent-folded search
Supersedes the plain case-insensitive matcher from earlier today (user
decision): an all-lowercase pattern searches case-insensitively, one
capital makes it exact (vim smartcase), and diacritics always fold both
ways — /ete finds été, /été finds ete. fold() strips the Latin-1
accent set (ligatures œ/æ excluded: they fold to two chars); n/N
recompute the smartcase verdict from the remembered pattern so repeats
behave like the original search.
2026-07-14 09:29:31 +02:00
Julien Calixte
11545efb0a docs(v0.7): record run 2, the checkout rebuild, and the closing gate 2026-07-14 09:23:04 +02:00
Julien Calixte
44c65e9ea5 perf(sync): reuse the TLS session and skip the pull's needless fetch
Two halves of the connection-cost lever (first on-device pull: 9.7s
fetch to learn 'up to date'; a rejected-push :sync runs three full
handshakes):

- esp_mbedtls_stream.c caches the mbedTLS session at stream close and
  offers it on the next connect to the same host — the server then
  skips the certificate exchange and most of the key exchange. Second
  vendor delta, single git thread, best-effort (any refusal falls back
  to a full handshake).

- pull_once now ls-refs first: the ref advertisement alone answers
  'anything new?', so up-to-date and local-ahead return without ever
  entering pack negotiation, and a needed download rides the SAME open
  connection instead of a second handshake. Tracking-ref updates share
  one update_tracking helper with the reconcile fetch.
2026-07-14 09:21:58 +02:00
Julien Calixte
75166f74cf perf(palette): intern the file list into one PSRAM blob
1099 paths as individual Strings measured 182 KB of internal DRAM
resident (each alloc is under the 16 KB SPIRAM-malloc threshold), a
main contributor to the DMA-pool exhaustion behind the :gl crash. The
walk now appends into one newline-joined String seeded at 64 KB so it
and its growth land in PSRAM; the editor stores the blob plus sorted
byte spans (set_file_list_joined) and the palette reads slices. :enew
appends to the blob; :delete drops only the span. Also log the :gl
dirty-journal gate, which looked like a silent no-op on the serial log.
2026-07-14 09:17:36 +02:00
Julien Calixte
329995ab58 fix(sync): apply the pull fast-forward as a tree diff, not checkout_tree
First on-device ff attempt (2026-07-14) crashed: libgit2's SAFE
checkout iterates the whole working directory (readdir over SPI on all
~1100 files) to find dirty files — the same O(tree) wall the splice
commit avoids — and with fetch memory still resident it exhausted
internal DRAM; esp-idf's spi_master then NULL-derefs on its own
failed-DMA-alloc path instead of erroring.

apply_tree_diff mirrors the splice: diff_tree_to_tree skips identical
subtree OIDs in memory, then writes/deletes exactly the changed paths
(unlink+tmp+rename — FAT rename won't overwrite). Belt kept O(changed):
any to-be-clobbered file whose content no longer hashes to the old blob
aborts the pull before the first write. Half-applied + unmoved ref
self-heals on the next :gl (idempotent re-apply).

Also raise the internal DMA reserve 32→64 KB so the SPI driver never
sees a failed alloc (its error path is the crash), and log the fetch
completion so the fetch/apply phases are separable in the serial log.
2026-07-14 09:17:36 +02:00
Julien Calixte
0734c1f67e feat(editor): make / search case-insensitive
Unicode lowercase-fold comparison at char boundaries (find_ci/rfind_ci)
instead of str::find — no lowercased buffer copy, whose byte lengths
could shift the returned offsets. Prose search shouldn't care about
capitalization; deliberately not smartcase (a capital silently flipping
to exact-match is a surprise here, not a feature).
2026-07-14 09:09:01 +02:00
Julien Calixte
3009dc4d9c feat(boot): async splash refresh + background palette file walk
The splash's ~2.2s full-refresh waveform now runs while the SD mounts
and the note/prefs load (Epd::display_frame_async + wait_ready guard on
every public display call), and the 4.3s palette walk moves to a 16KB
background thread whose result lands in the idle branch — the first
editor frame no longer waits on non-mandatory work. The post-pull
re-walk rides the same channel, removing a 4.3s UI stall after :gl.

Also: rustfmt pass on sd_bench, drop the spike-9 log line in splash.
2026-07-14 01:34:28 +02:00
Julien Calixte
8c4866df1f feat(bench): attribute the loose-write residual — dir-scaling + splice I/O counters
sd_bench grows a directory-entry-scaling section (stat hit/miss and
the loose-object composite at 8/64/256 siblings): FAT resolves every
path component by linear scan, so cost climbing with N convicts
directory scans as the ~360ms residual; flat cost exonerates them.
stage_and_commit brackets the splice with the p_mmap counters and logs
'N mmaps / M KB read' per commit, splitting the residual into pack-read
I/O vs everything else on the next real :sync.
2026-07-14 01:12:35 +02:00
Julien Calixte
a941ae39b3 feat(sync): :gl pull — fetch + fast-forward only on the git thread
The git channel now carries GitRequest::{Publish,Pull} with a shared
ensure_online preamble (Wi-Fi/clock/TLS once per session). pull_once
fetches origin — refreshing the tracking ref so the radio-free
up-to-date check stays honest — and maps four shapes: up to date,
LocalAhead (stranded commit, :sync's job), clean fast-forward,
Diverged (refused; no merge on the device). The fast-forward is a SAFE
checkout then ref move: it refuses to overwrite content that differs
from HEAD, the belt under the UI gate that refuses :gl while the dirty
journal is non-empty. A RAM-dirty buffer doesn't gate — its edits win
(last-writer-wins, like the reconcile).

After a pull the UI drops clean parked buffers, re-reads the clean
active buffer in place (Editor::refresh_active), and re-walks the
palette list. Firmware 0.6.0 -> 0.7.0. On-device verification pending.
2026-07-14 01:10:00 +02:00
Julien Calixte
8d22f81804 feat(editor): / forward search with n/N repeat (v0.7)
Literal case-sensitive substring search sharing the : command line's
editing (cmd_prompt discriminates the prompt; Enter dispatches). Jumps
on Enter only — incremental caret-chasing loses to the e-ink refresh
cost, same call as the snippet no-popup decision. Wraps with a notice,
keeps the caret on a miss, bare / repeats the editor-global last
pattern. n/N live in move_by so counts and Visual extension come free;
they are not operator targets.
2026-07-13 22:15:19 +02:00
Julien Calixte
4f0d745e84 docs: sweep sync follow-ups into their owning version docs
Kaizen/postmortem next-steps kept accumulating with no owner. Each item
now lives in one version doc (v0.7 sync perf, v0.9 robustness, v1.0
boot) with the source docs linking there; the editor-freeze safety net
is marked hardware-verified and the v0.5 walk re-measure closed at 4.3s.
2026-07-13 22:07:34 +02:00
Julien Calixte
5d623151a8 chore(justfile): repack multi-pack source clones in _load-repo
Single-pack card prep is now enforced, not remembered: each pack's
whole .idx (plus any midx) is mmap'd outside the device's window
budget, and a multi-pack marking walk lost the HTTP keep-alive (run 8:
31s vs 3.5s repacked). Skipped when the clone is already one pack.
2026-07-13 22:07:26 +02:00
Julien Calixte
8d5bd1903d refactor(sync): retire the diagnostic push heartbeat
It served its purpose (localized the mmap plateau, runs 7-9); its 8KB
internal stack was live during the TLS send, where internal min-ever
hit 2099 bytes. The time-gated pack/transfer callbacks stay.
2026-07-13 22:07:16 +02:00
Julien Calixte
028ebc8f5f docs(kaizen): close the real-repo sync loop — first :sync at 24.1s
Step 6 evaluated: infinity (never completes) to 24.1s measured
snackbar-to-snackbar, with the measured after-bar charted in step 6 (the
step-1 chart stays potential-only, per the format). Documents the second
localization loop the verification runs forced on the push half: UI death
by shared heap fate, odb cache exonerated, telemetry axis bug, mmaps named
as the eater, the impossible fixed-maps budget, the keep-alive race, and
pack layout as a device-performance variable.
2026-07-13 21:45:47 +02:00
Julien Calixte
9e99c1767a fix(sync): budget mmaps at 64KB/1.5MB, trace the push heap live
Runs 7-9 on the real repo (2026-07-13): esp_map live-bytes wired into
log_push_heap plus a 5s sibling heartbeat thread proved the push OOM was
libgit2's mmaps — ~3.4MB of whole-file .idx/multi-pack-index maps living
OUTSIDE the mwindow budget, plus windows at the (working) 4MB soft limit,
an impossible budget on the 8MB pool. Shrinking to 64KB windows / 1.5MB
mapped limit left 4.5MB min-ever free and cut marking reads 4x; with the
card repacked to a single pack, the first-ever real-repo push landed
(6713ea6e, :sync 24.1s end-to-end).

The heartbeat stays for now (the marking phase fires no libgit2 callbacks,
so it is the only heap trace through it); its 8KB internal stack is part
of the min-ever-2099B-internal tightness flagged for follow-up.
2026-07-13 21:45:47 +02:00
Julien Calixte
f79d13453a fix(sync): survive push heap exhaustion and instrument the push path
Run 4 (2026-07-13): remote.push() consumed ~6 MB of PSRAM over 66 s and
the UI thread aborted on Frame::new_white's per-draw vec![0xFF; 26928].
Run 5 confirmed the UI now survives, but the push still exhausts both
pools (a ~7 KB inflateInit fails inside the pack build) and the heap
telemetry never fired — hence the telemetry rework.

- display/editor: Editor::draw_into() renders into a caller-owned
  Frame; main.rs keeps two boot-time frames (shown/back) and mem::swaps
  on success, so steady-state repaints never allocate.
- git_sync: odb cache capped at 1 MB via raw libgit2-sys opts (git2
  0.20 doesn't wrap the total cap); log_push_heap at pre-push,
  post-push and push-failure with largest-PSRAM-block and per-pool
  min-evers; pack_progress logging is now time-gated (2 s) — the
  count gate (+256 objects) never fired because AddingObjects reports
  total=0 and a small push inserts only dozens of objects.
- editor: empty/whitespace snippets file parses as 0 snippets instead
  of a JSON error at boot.
- build.rs: refuse a git-feature build with unset TW_* publish vars —
  a bare `cargo build --features git` baked empty creds and produced a
  firmware whose :sync could never work.
2026-07-13 21:04:18 +02:00
Julien Calixte
05706478a2 feat: wrap palette selection; fix push classing, file walk, card load
- Palette Ctrl-N/P (and half-page keys) wrap around the result list
  instead of clamping at the ends; tests updated to the new semantics.
- try_push maps libgit2's negotiation-time NotFastForward error to
  PushFailure::Rejected so the reconcile path runs instead of failing
  as a transport error.
- walk_files decodes esp-idf d_type values (DT_REG=1/DT_DIR=2) that
  libc 0.2.178's generic unix table misreads as fifo/chardev — the
  palette walk was silently dropping every file on the card.
- `just load` refuses to mirror over unpublished device edits listed
  in the card's .typoena-dirty journal (backup+discard via
  TW_DISCARD_UNPUBLISHED=1), gains --force for a from-scratch copy,
  and a flash-only recipe flashes the already-built ELF.
2026-07-13 20:15:14 +02:00
Julien Calixte
5f50cf1b45 chore: remove bar 2026-07-13 11:35:08 +02:00
Julien Calixte
5c97e019ec docs(kaizen): drop the after-kaizen bar until step 6 measures it
Step 1 is baseline + target only; the target line stays, the bar would
fake a measurement that doesn't exist yet.
2026-07-13 11:07:25 +02:00
Julien Calixte
7712de6153 docs(kaizen): size the xychart so its labels stop clipping
Explicit 900x500 canvas, tighter x-axis labels, unit-only y-axis title
(the long vertical title collided with the tick numbers in Remanso).
2026-07-13 11:06:44 +02:00
Julien Calixte
1df25f75d6 docs(kaizen): chart the improvement potential as a mermaid xychart
Before/after bars with the target as a line series; the caption keeps
the nuance the chart can't draw (true before value is ∞, not 611 s).
2026-07-13 11:03:43 +02:00
Julien Calixte
acbafa3d6b docs(kaizen): retell the real-repo sync fix as a six-step kaizen
The tradeoff-curves doc stays the chronological data layer; the kaizen
is the story layer — value model, 8-factor hypothesis table, ideas
ranked by verified cause, and an evaluation left PENDING until the
end-to-end on-device :sync lands.
2026-07-13 10:56:05 +02:00
Julien Calixte
e801d2d480 docs(palette): note the two on-device boot-log measurements pending
Walk time after the d_type fix, and the file-list internal-DRAM cost
with the decision rule for interning paths into one PSRAM buffer.
2026-07-13 10:26:25 +02:00
Julien Calixte
a771c3f6de chore(diag): log internal DRAM next to PSRAM-dominated heap totals
"free heap 5.8MB" in the crash log was PSRAM and masked the actual
internal-RAM exhaustion. git_sync's publish/commit/push lines now
carry the MALLOC_CAP_INTERNAL reading, and boot brackets the palette
file-list build with it — small Strings are forced internal by the
16 KB SPIRAM malloc threshold, so the 1098-path list is a suspected
~60-70 KB DRAM consumer competing with Wi-Fi/TLS; the log decides
whether interning into one PSRAM buffer is worth it.
2026-07-13 10:25:25 +02:00
Julien Calixte
4c92b0dddc fix(sync): move mbedTLS allocations to PSRAM
esp-idf's default is internal-RAM-only, and a TLS connection needs
~33 KB of it (two ~17 KB I/O buffers + contexts) at the exact moment
:sync pushes — with Wi-Fi, USB host, the editor and libgit2 resident,
mbedtls_ssl_setup failed there on the first real-repo push (and its
failure path then hit the stream double-free fixed in the previous
commit). TLS buffers are CPU-only data, so PSRAM is safe; the
handshake is network-bound.
2026-07-13 10:25:25 +02:00
Julien Calixte
af5b41a104 fix(sync): patch the mbedtls stream double-free that reset the chip
Upstream libgit2 v1.9.4 bug: mbedtls_stream_wrap's ssl_setup error
path closes and frees the caller's socket stream, then
git_mbedtls_stream_new frees it again — and wrap's other error paths
don't free it at all, so callers can't compensate either way. When
ssl_setup failed on the device (internal-RAM exhaustion during the
first real-repo push), the double git__free tripped tlsf ("block
already marked as free") and reset the chip mid-:sync.

Fixed the way esp_map.c replaces map.c: the vendored streams/mbedtls.c
is excluded from the component and esp_mbedtls_stream.c (verbatim copy
+ one-hunk fix: out_err leaves st->io to the caller, and frees the
leaked ssl struct) takes its place. Keep the copy in lockstep on
submodule bumps; worth reporting upstream.
2026-07-13 10:25:12 +02:00
Julien Calixte
2660a3e9dd perf(palette): trust dirent d_type instead of a per-entry stat
The per-entry metadata() call makes FatFS re-walk the directory by
path every time — ~32ms/file on the card, 35s for a 1098-file tree.
esp-idf's FAT VFS always fills d_type (DT_DIR/DT_REG from the FILINFO
readdir already holds, never unknown) and Rust std maps file_type()
onto it stat-free, so the walk is now one readdir pass per directory.
2026-07-13 01:05:10 +02:00