The customer-requirements (WHATs) Source column was plain text ("product
§UX", "README v0.8"). Replace with anchored Markdown links into the
product spec, README, and roadmap so each requirement's provenance is
one click away.
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:
- Tracked — lives in the git working copy, gets committed and pushed.
- Local — never leaves the device. Drafts, journal entries, scratch, things that aren't ready or aren't anyone else's business.
Same editor, same keymap; the difference is just whether Ctrl-G (commit &
push) 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 5.79" strip aspect: less screen than a 7.5" page-shaped panel, but the long-narrow shape biases toward "current line + recent context" — the writing posture we actually want. The smaller framebuffer is cheap on RAM, and SPI panels keep the GPIO budget open for SD + future peripherals. A larger panel (10.3" via IT8951) stays on the table for v1.x once UX is proven.
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, commit msg. |
| 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
sdkconfig.defaults
/hardware BOM, schematic, enclosure (later)
/docs ADRs, power measurements
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-packsmart-HTTP push working onesp-idf-rswith 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.