Julien Calixte bea3058004 feat(firmware): add libgit2 esp-idf component (CMake + mbedTLS shims)
Builds vendored libgit2 as an esp-idf component so it inherits the
lwip/mbedtls/pthread/vfs/newlib include+link graph -- the include cascade
that sank CFLAGS injection never appears. Configured for mbedTLS via a
hand-written git2_features.h. Small port surface, libgit2 sources
untouched: poll.h shim, lstat==stat (esp_port.h), p_mmap via malloc+read
(esp_map.c), and identity/symlink stubs (esp_stubs.c). Registers empty
when LIBGIT2_SRC is unset so the editor build is unaffected.
2026-07-05 21:28:47 +02:00
2026-05-16 14:21:32 +02:00

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

How each decision is weighted against the user-facing requirements — and the critical performance budget that falls out — lives in docs/qfd.md. The ontology layers those docs use (WHAT / Function / Characteristic / Metric / Target) are defined in GLOSSARY.md.


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: ~13 lines, ~79 cols at the editor's 10px font. 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. The ontology layers those docs use (WHAT / Function / Characteristic / Metric / Target) are defined in GLOSSARY.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 Typoena — 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.2.5 international input :v025, after v02, 2w
    v0.3 editing              :v03, after v025, 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.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 /, :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,
                rendering/UX spike log (spikes.md)
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 used across docs
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%