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.
Typoena
A distraction-free, hackable, DIY writing machine. ESP32-S3 + e-ink + a real mechanical keyboard. You write Markdown, you commit, you push. Nothing else runs on it.
Status: pre-MVP, hardware on bench. Display, USB keyboard, live typing with partial refresh, Wi-Fi + TLS, SD storage, and on-device git push are all verified in spikes; SD mount and save are now wired into the app binary. No release has shipped yet — v0.1's remaining gate is the boot splash and wiring git publish (
Ctrl-G→ push) into the app binary. Live per-item status:docs/roadmap.md· failure write-ups:docs/postmortems/.
Vision
A single-purpose appliance that boots into a text editor with a Vim keymap, edits Markdown files, and (optionally) pushes them to a git remote (GitHub first) over Wi-Fi. No browser, no notifications, no apps. Open lid → write → push (or don't) → close lid.
Two file scopes coexist on the SD card — formal definitions in
CONTEXT.md:
- Tracked — lives in the git working copy, gets Published when the
user presses
Ctrl-G. - Local — never leaves the device. Permanently-private: journal entries, scratch, things that aren't anyone else's business. There is no "promote to Tracked" gesture — scope is fixed at file creation.
Same editor, same keymap; the difference is just whether Ctrl-G (publish to
the remote) is offered.
Hardware
ESP32-S3-N16R8 (16 MB flash, 8 MB PSRAM) · GDEY0579T93 5.79″ e-ink strip (792×272, ~2.9:1 — biases the UX toward "current line + recent context", the writing posture we want) · Nuphy wired USB keyboard with the S3 as USB host · microSD over SPI · USB-C wall power for the MVP, battery in v0.8.
Full part table, rationale, and bench status:
docs/hardware.md. Enclosure — a parametric,
3D-printable typewriter-body case (OpenSCAD): hardware/case/.
Software stack
Language: Rust on esp-idf-rs (std). Every stack decision — language, UI
strategy, display, git lib, auth, concurrency, storage, power, keyboard
transport — has an ADR in docs/adr.md, including the rejected
alternatives (Ratatui, Gleam + Shore on AtomVM, C/Arduino — ADR-001/002). How
each decision is weighted against the user-facing requirements lives in
docs/qfd.md; the ontology those docs use is defined in
GLOSSARY.md. Where a default traces to a cost curve rather than
a discrete pick — energy, latency, or memory bending against an interval or size
— the curve and its knee live in
docs/tradeoff-curves/README.md. A
memory-safety review of the Rust unsafe/FFI
surface (mostly usb_kbd.rs) is in MEMORY_AUDIT.md.
| Layer | Choice | Notes |
|---|---|---|
| HAL / runtime | esp-idf-svc, esp-idf-hal |
std build: heap, threads, VFS, mbedtls, Wi-Fi stack. |
| Display | Custom SSD1683 driver (src/epd.rs) + embedded-graphics |
Dual-controller 792×272 panel; dirty-rect partial refresh (~630 ms measured). |
| UI layer | Custom thin widget layer | Ratatui's API shape without its char-grid terminal model (ADR-002). |
| Editor core | Custom, in-tree (src/editor.rs) |
Modal (Normal / Insert / View / Command), motions, operators + text objects. Plain-ASCII buffer until the v0.2 UTF-8 work. |
| USB host | esp-idf TinyUSB bindings |
Boot-protocol HID; verified on hardware (Spike 4). |
| Git | libgit2 via git2, built as an esp-idf component with mbedTLS (firmware/components/libgit2/) |
gix was the original pick but can't push over HTTPS — the ADR-004 kill-switch fired (postmortem). On-device add → commit → push verified; ~16 s cold-:sync latency breakdown. |
| TLS | mbedtls via esp-idf |
GitHub HTTPS with the chain checked against embedded roots; ≈35 KB heap measured during handshake (Spike 6). |
| Auth | HTTPS + GitHub PAT | v0.1 bakes credentials in at build time via TW_* env vars; provisioning + at-rest protection is ADR-011 (open), on-device settings land in v0.9. |
| Filesystem | FAT on SD (esp_vfs_fat) |
Working copy lives here. Internal LittleFS holds config. |
UX boundaries set by the medium
E-ink is a brutal honesty filter on UI choices. Hard constraints we design around, not against:
- No cursor blink. Kills the panel and the battery.
- Typing latency target: ≤ 200 ms from keypress to glyph on screen, using partial refresh on the affected line only.
- Full refresh every ~20 partials to clear ghosting. User-visible flash — schedule it on pauses (>1 s of no input).
- No smooth scrolling. Page-style jumps only.
- No animations. Anywhere.
- Render only changed lines, not the viewport.
Roadmap
Releases are frequent, and every version is a usable artifact rather than a
checkpoint. Per-version scope, current [x]/[~] marks, and the Macroplan
source live in docs/roadmap.md.
| Version | Theme | One-liner |
|---|---|---|
| v0.1 | MVP | Boots, edits one file, Ctrl-G pushes. |
| v0.2 | Vim nav | Normal/Insert, motions, line numbers. |
| v0.2.5 | Intl input | US-Intl dead keys: à é ê ç, '+space = '. |
| v0.3 | Vim edit | dd yy p, undo/redo, counts. |
| v0.4 | Visual + ex | v V, :w :q :e command line. |
| v0.5 | Files | Ctrl-P over /repo + /local, buffers. |
| v0.6 | Markdown | Headings, list continuation, soft-wrap. |
| v0.7 | Search + git | / search, :Gpull. |
| v0.8 | Power | 18650 + sleep + lid switch. |
| v0.9 | Robustness | Crash-safe writes, reconnect, settings. |
| v1.0 | Polish | Boot ≤ 3 s, fonts, themes, enclosure, guide. |
| v1.x | Stretch | 10.3″ panel, multi-remote, stats, BLE. |
Repo layout
/firmware Rust crate, esp-idf-rs target
(SD card mounted at runtime contains /repo and /local)
/src
main.rs app binary — editor + display + USB (SD/git not wired yet)
editor.rs modal editor core: buffer, modes, keymap, :commands
epd.rs SSD1683 dual-controller e-ink driver
usb_kbd.rs TinyUSB host glue, HID → key events
/bin on-device spike binaries (sd_fat, wifi_tls, git_push,
git_sync, git_smoke)
/components/libgit2 libgit2 as an esp-idf CMake component (mbedTLS);
source vendored as a git submodule
build.rs bakes TW_* env vars (Wi-Fi, PAT, author) — v0.1 config path
/spikes desktop spikes (spike7 git push proof, pre-device)
/docs ADRs, QFD, hardware, roadmap, per-version specs,
spikes.md, postmortems/, notes/
/hardware enclosure — parametric OpenSCAD case (case/) + renders
CONTEXT.md project glossary — Tracked / Local / Save / Publish, and
the principles that fall out of them
GLOSSARY.md methodology glossary — the WHAT / Function /
Characteristic / Metric / Target ontology layers
package.json pnpm + oxfmt — formatting toolchain for docs/JSON
Open questions / risks (tracked, not yet resolved)
- Heap fragmentation over a long writing session with the PSRAM allocator.
- Real-world e-ink ghosting with the current partial-refresh cadence.
- [~] Use-after-free freeing the in-flight USB transfer on keyboard unplug —
fixed in code, pending an on-device hot-plug run to confirm
(
MEMORY_AUDIT.mdfinding #1).
Retired risks (gix push,
SD CMD59 rejection, TinyUSB HID
stability, TLS heap, libgit2-on-xtensa) and how they died:
docs/spikes.md and
docs/postmortems/.
These get resolved by writing code, not by deciding harder.