typoena.dev / about
How it's built
Typoena began with one constraint: a machine that only lets you write. The whole build is a set of answers to that — which hardware can hold an editor and nothing else, how to save your words without a cloud account, and how to make it boot fast enough that you stop thinking about it. This page is how it went.
The parts
- Compute
- ESP32-S3 · 8 MB PSRAM
- Display
- 5.79″ e-ink · 792 × 272
- Input
- USB-host mechanical keyboard
- Storage
- SD card, on its own SPI bus
- Sync
- git over Wi-Fi · HTTPS + token
- Editor
- Vim keymap, written in Rust
One spike at a time
Almost nothing here was known to work in advance, so each hard part was built and proven on its own before it touched the editor: a blink test, then text on the panel, a USB keyboard, partial screen refresh, Wi-Fi with a validated TLS chain, and finally a git push over HTTPS. Only once a piece stood up alone did it join the rest. A few of those spikes nearly ended the project.
Problems worth telling
The hardest two got full write-ups, with the real numbers and the dead ends, over in stories.
The card that wouldn't mount
The SD card refused to mount for days. The culprit turned out to be the card itself: a 133 GB SDXC card was the entire fault, and a plain 32 GB SDHC card mounted on the first try. Then a second problem surfaced — the e-ink panel and the card were sharing one SPI bus and stepping on each other. The fix was to stop sharing. The card got its own bus, the panel kept its own, and the arbitration trouble went away.
Git on a microcontroller
The plan was always to save your work as commits and push them somewhere real. The first attempt used a pure-Rust git library and hit a wall we couldn't get past. So we switched to libgit2 — the same C library desktop git leans on — compiled as an ESP-IDF component against mbedTLS and given its own 96 KB thread. After that the device could clone and push over HTTPS with a token, checking GitHub's certificate chain against CAs baked into the firmware.
The memory war
Pushing a toy repo was easy. Pushing a real one — 63,000 objects and 560 MB of history — took nine attempts against 8 MB of RAM. libgit2 memory-maps its pack-index files, and on a repo that size those maps alone overran the budget. A double-free in libgit2's TLS error path crashed the device mid-push. The pack builder wanted to inflate ten objects at once. Each fix bought back a few hundred kilobytes and exposed the next ceiling. On the ninth try the numbers finally lined up — tuned memory-map windows, a capped object cache, a patched TLS layer — and a real repository pushed from the device for the first time.
The full story — the freeze, and nine tries to get past it →
Seconds to a cursor
A writing machine has to feel instant, so cold-boot time got its own campaign. It started at 4159 ms to a cursor, and most of what could be removed ran before the app even did — a PSRAM memory test and a 160 MHz clock, both pure overhead on a known-good board. Turning the test off and the clock up cut nearly a second. That same speed-up then sprang a trap: a background file-index walk began winning a disk lock it used to lose, starving the boot path, until it was moved to run after the first frame. Boot is about 3.2 seconds now — 239 ms short of the sub-three-second goal, which now runs into the hard floor of the e-ink refresh itself.
The wall behind every keystroke
That same e-ink floor sets how fast a letter appears when you type. The obvious fix — drive fewer rows of the panel for a one-line change — turned out to buy nothing: the refresh time lives in the waveform, not the number of rows, so a 20-row update runs almost as long as a full one. What was left to win was everything around the waveform: a faster display bus, and a settle delay that a FreeRTOS tick had quietly turned from 2 ms into as much as 10. Three rounds of that took a full-screen update from about 693 ms to 527, and a keystroke to roughly 495 — essentially the bare cost of ink forming on the panel. A fourth round then moved that floor itself: fed the panel maker's own partial waveform and re-authored to run a shorter schedule, a keystroke drops to about 265 ms — the first thing to beat the wall. It gets there by spending the factory's cold-weather safety margin, so for now it's an opt-in setting, off by default and still being soak-tested.
The full story — four kaizen rounds, and the floor that finally moved →
Where it is now
The editor grew up in versioned steps: cursor navigation, then a real Vim keymap with modes, registers, undo and redo, and dot-repeat; visual mode and ex commands; a fuzzy file palette across a whole notes folder; search with smartcase and accent folding, so /ete finds été; and git pull and push straight from the device.
The newest piece is a first-boot wizard, so you can set one up with no computer at all: pick your Wi-Fi on screen, sign in to GitHub by scanning a QR code, choose a repository, and start writing.
It runs today — hour-long writing sessions with no crash. Boot is still slower than we want and some edges are rough, but the loop it was built for works: open the lid, write, push, close the lid.
The whole thing is open source — read the code ↗