Julien Calixte 852b7d8de0 fix(quality-house): give HOW labels and perception title room to breathe
Raise the header zone (qfdHdrH 3.2 → 4.2) so rotated HOW labels stop
crossing into the roof diamond grid, and wrap the perception title onto
two lines via text width so it stays inside its zone instead of bleeding
left over the H14/H15 column labels.
2026-05-16 00:32:35 +02:00

typewriter

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 not yet on bench. Bring-up in progress.


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

Part Choice Why
MCU ESP32-S3-N16R8 (16 MB flash, 8 MB octal PSRAM) USB OTG host (for the keyboard), Wi-Fi, BLE, dual core @ 240 MHz, plenty of PSRAM for git pack data and screen buffer. Best-supported Rust target in the ESP family.
Display GDEY0579T93 + DESPI-c579 breakout (5.79", 792×272, 1-bit) Good Display panel matched with its own FPC breakout. Strip aspect (~2.9:1) — Freewrite-coded: ~12 lines of edit area, ~95 cols. Tiny framebuffer (~27 KB) leaves PSRAM headroom. The DESPI-c579 is a passive level-shifter / FPC-to-header board, not an active controller — driven over plain SPI like any other epd.
Keyboard Nuphy Air60/Halo65 wired USB-C ESP32-S3 acts as USB host via TinyUSB. BLE-HID is a fallback but contends with Wi-Fi for radio time during push.
Storage microSD over SPI Holds both the git working copy (/sd/repo/) and the local-only scratch space (/sd/local/). Internal flash is for firmware + config only.
Power USB-C wall power for MVP, 18650 + IP5306 in Phase 3 Measure power profile on real hardware before sizing the battery. E-ink + sleep should give multi-day battery life but battery introduces charging, safety, and BMS complexity we don't need on day one.
Enclosure 3D-printed, hinged lid Phase 4 concern.

Why the strip aspect: the ~2.9:1 long-narrow shape biases the UX toward "current line + recent context" rather than "full page" — the writing posture we want. The renderer stays resolution-agnostic so a 10.3" e-ink upgrade (v1.x) is a swap, not a rewrite. Medium choice (e-ink over LCD / memory LCD / OLED) and panel rationale: ADR-003.


Software stack

Language: Rust on esp-idf-rs (std). Decision is load-bearing — see the rejected alternatives below, and docs/adr.md for the full decision log covering language, UI strategy, display, git lib, auth, concurrency, storage, power, and keyboard transport. How each decision is weighted against the user-facing requirements — and the critical performance budget that falls out — lives in docs/qfd.md.

Layer Crate / Component Notes
HAL / runtime esp-idf-svc, esp-idf-hal std build, gives us heap, threads, VFS, mbedtls, Wi-Fi stack.
Display embedded-graphics + epd-waveshare (or custom driver) Pixel framebuffer with partial-refresh regions. We track dirty rects ourselves. The GDEY0579T93 uses an SSD1683-class controller; if it's not already in epd-waveshare, we write a small driver against embedded-hal SPI — ~300 LoC, low risk.
Editor core Custom, in-tree Rope buffer (ropey), mode state machine, Vim keymap table.
TUI-style layout Custom thin layer (~500 LoC) API inspired by Ratatui (Layout, Block, Paragraph) but renders directly to embedded-graphics. See below.
USB host esp-idf TinyUSB bindings Boot-protocol HID is enough for the keyboard.
Git gitoxide (gix) Pure-Rust, modular. We only need add / commit / push (smart HTTP). No libgit2, no mbedtls glue beyond what esp-idf already gives us.
TLS mbedtls via esp-idf Used for GitHub HTTPS. ~120 KB heap during handshake — fits in PSRAM.
Auth GitHub Personal Access Token in encrypted NVS SSH on embedded is painful; HTTPS+PAT is the pragmatic path.
Filesystem FAT on SD (esp_vfs_fat) Working copy lives here. Internal LittleFS holds config.

Why not Ratatui

Ratatui assumes a character-grid terminal with an ANSI backend. E-ink is a pixel framebuffer with partial-refresh windows. The right primitive for e-ink is dirty-rectangle tracking aligned to the panel's refresh regions — Ratatui's per-cell diff model fights this. We can borrow its widget API shape (it's a good one) without dragging in the terminal abstraction. Net saving: probably 200 KB of binary and a lot of pretending the screen is a VT100.

Why not Gleam + Shore

BEAM doesn't run on ESP32. AtomVM does, but: memory budget is tight, Gleam-on- AtomVM is bleeding-edge, and there are no bindings for USB host / e-ink / SD / TLS / git in that ecosystem. Shore is also terminal-oriented, so the same impedance mismatch as Ratatui applies. Building this on Gleam would be a research project stacked on a research project. Revisit in 2-3 years.

Why not C / Arduino

Workable, well-trodden, fastest path to a blinking screen. But this is a project I want to keep evolving — Rust's refactoring leverage and type safety pay off the moment we start adding modes, palette, search, etc.


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

Frequent releases. Each version is a usable artifact, not a checkpoint. Macro-plan below; per-version scope lives in docs/roadmap.md.

gantt
    title Typewriter — macro plan
    dateFormat YYYY-MM-DD
    axisFormat %b %Y
    section MVP
    v0.1 it writes, it pushes :v01, 2026-06-01, 4w
    section Vim
    v0.2 navigation           :v02, after v01, 3w
    v0.3 editing              :v03, after v02, 3w
    v0.4 visual + ex          :v04, after v03, 2w
    section Files
    v0.5 palette + multi-file :v05, after v04, 3w
    v0.6 markdown             :v06, after v05, 2w
    v0.7 search + git         :v07, after v06, 3w
    section Hardware polish
    v0.8 battery + sleep      :v08, after v07, 4w
    v0.9 robustness           :v09, after v08, 4w
    v1.0 polish               :v10, after v09, 4w
Version Theme One-liner
v0.1 MVP Boots, edits one file, Ctrl-G pushes.
v0.2 Vim nav Normal/Insert, motions, line numbers.
v0.3 Vim edit dd yy p, undo/redo, counts.
v0.4 Visual + ex v V, :w :q :e, status line.
v0.5 Files Ctrl-P over /repo + /local, buffers.
v0.6 Markdown Headings, list continuation, soft-wrap.
v0.7 Search + git /, :Gpull, :Gbranch.
v0.8 Power 18650 + sleep + lid switch.
v0.9 Robustness Crash-safe writes, reconnect, settings.
v1.0 Polish Boot ≤ 3 s, fonts, enclosure, guide.
v1.x Stretch 10.3" panel, spell-check, themes, BLE.

Repo layout (planned)

/firmware       Rust crate, esp-idf-rs target
                (SD card mounted at runtime contains /repo and /local)
  /src
    editor/     rope buffer, modes, keymap
    render/     embedded-graphics + dirty rects
    git/        gitoxide wrapper, auth
    usb/        TinyUSB host glue
    fs/         SD + NVS
  build.rs      reads TW_* env vars (Wi-Fi, PAT, author) — v0.1 config path
  sdkconfig.defaults
/hardware       BOM, schematic, enclosure (later)
/docs           ADRs, QFD, roadmap, per-version product + technical specs
CONTEXT.md      project glossary — Tracked / Local / Save / Publish, and the
                principles that fall out of them
package.json    pnpm + oxfmt — formatting toolchain for docs/JSON
                (companions: pnpm-lock.yaml, .oxfmtrc.json, .node-version)

Open questions / risks (tracked, not yet resolved)

  • gix-clone + gix-pack smart-HTTP push working on esp-idf-rs with mbedtls — needs an early spike before locking the stack.
  • TinyUSB host stability with arbitrary HID descriptors (Nuphy reports consumer-control keys we may need to ignore).
  • Heap fragmentation over a long writing session with PSRAM allocator.
  • Real-world e-ink ghosting with current partial-refresh cadence.

These get resolved by writing code, not by deciding harder.

Description
No description provided
Readme 6.7 MiB
Languages
Rust 86.7%
Just 5%
OpenSCAD 3%
HTML 2.4%
C 2%
Other 0.9%