Owl
A word processor for the netbook in your closet.
That machine isn’t too slow to write on. Writing never got heavier; the software did.
Owl is what’s left when you take a text editor and remove everything that isn’t the page. Full screen. One centered column, seventy-two characters wide. The line you’re typing pinned to the middle of the display. No chrome asking you to decide anything.
It targets 32-bit hardware first.
A Perfectly Good Computer
A machine people wanted five years ago is now worth nothing. Not broken. Not incapable. Worth nothing, in the sense that you can’t sell it and you feel faintly embarrassed still using it.
Some of that is manufactured. It suits Apple, and everybody imitating Apple, for a four-year-old laptop to feel like a liability. But what bothers me is that most of us aren’t asking a computer to do much more than we did twenty years ago. I write. I answer mail. I look things up. The system requirements for those three things have climbed by orders of magnitude and I cannot point at what I got in return.
Not all of that is a fault you can fix from your side of the screen. The web got heavy, and the web is most of what a computer is now. That one is out of my hands.
Writing isn’t. Writing is exactly the size it has always been.
What tipped it was watching other people get there first. Writing decks built out of Raspberry Pis. The Freewrite. I’d had a web-based writing project back in 2019 that quietly stalled, and looking at those I understood why: the thing I wanted didn’t need the web at all. (A compatible web version someday, sure. Just not the foundation.)
And I have a low-grade climate anxiety that makes me want a project I can point at. Keeping working laptops out of the landfill is a small thing. It is also a thing I can do.
Appliances
Then I remembered that this used to be a category. The Canon Cat. The Wang machines. The AlphaSmart that half of us wrote papers on. Not computers that ran a word processor: machines that were a word processor, that turned on ready and had nothing else to offer you.
That’s the word I use for Owl. Every “no” in this piece traces back to it.
The one thing those machines got wrong was the ending. When the appliance died, or the company did, so did everything you’d written on it. So Owl’s format is a folder of Markdown, its sync is designed as plain git, and the plan for moving a project between two machines is a git bundle handed over by magic-wormhole. All of it borrowed, all of it outliving me. The appliance is disposable. Your writing is not.
The Machine
The target class is a Samsung N100: an Atom netbook, i686, tight RAM. The distro is AntiX, which won over Void. Debian stopped releasing i386 ISOs in Trixie and AntiX still ships them. It ships non-free firmware in the default install, so the wifi works. And it runs sysvinit rather than systemd, which means nothing has to be wrangled to make the machine boot straight into an editor.
The deployment plan reversed itself early. The original assumption was cross-compilation from the workstation, which meant a static-link yak-shave with SDL2. But AntiX is Debian-derived, so apt install cargo libsdl2-dev gives the same toolchain on both ends. You clone the source onto the netbook and build it there. The dependency chain for libsdl2-dev pulls in the exact libraries SDL2’s KMSDRM backend needs at runtime, so installing the compiler also installs the eventual video path.
That’s the rule the whole toolchain follows: it should install on the same kind of machine the software targets. The browser build obeys it too. Everything it needs (libstd-rust-dev-wasm32, binaryen, esbuild, the TypeScript compiler) is a Debian package. No npm tree at all.
Full Screen from the First Commit
Not as a mode you can turn on. The appliance takes the whole screen on the target hardware, and I didn’t want the development environment offering an escape from that.
What’s on the screen: your text, a caret, and a small clock at the top. Six color families, light and dark, on the function keys; three body fonts on three more. The fonts are compiled into the binary, so owl has no runtime file dependency except the document you’re editing.
The caret disappears while you have a selection. Two carets competing for attention reads as noise, and the selection already says where you are.
Markup You Never See
The rope holds plain text. Bold and italic ride alongside it in a separate overlay keyed to character ranges. Markdown is the save format, not the editing model, so ** and _ exist in the file and nowhere else.
The round-trip is asserted rather than described. The first save is allowed to normalize loose or foreign Markdown; every save after that has to be byte-identical, and the text and styles have to survive the reload unchanged. Unclosed **. A bare *. ***, which is bold plus a literal star. Italic nested outside bold, which canonicalizes the other way. A heading whose text happens to look like a list. Each of those is a test that fails if the fixed point drifts.
Anything Owl doesn’t model (lists, block quotes, fenced code) is carried through verbatim, so a file that’s been through another editor comes out the far side unmangled.
LEAP
Hold the right Alt key to jump toward the end of the document, the left one to jump toward the start. Type while you’re holding it and the search refines live. Let go and you land.
This is the Cat’s LEAP, and I have never found a better answer to “where was that paragraph.” The two thumb keys become the two Alt keys. Landing keeps the match selected, so typing replaces it, which is the Cat’s leap-then-overtype. And starting a leap with something already selected seeds the search from it, so pressing again walks you through occurrences without retyping. The seed is provisional: your first keystroke throws it away and starts fresh.
Two pieces got cut. The Cat’s “creep,” where a bare tap nudges the cursor one character, is the Cat’s substitute for arrow keys. Owl has arrow keys, so creep had nothing left to do but surprise me. And there used to be a floating » pattern label where the clock sits. It was distracting, and the highlight already shows you exactly where you’re about to land.
One embarrassing development caveat: a window manager that claims Alt for itself swallows the keypress before owl ever sees it. Not a problem on the appliance, which has no window manager.
The Terminal Version Isn’t a Lesser Version
owlet is the same editor in a terminal, Word Grinder in feel. Same buffer, same motions, expressed in cells instead of pixels.
The keys diverge, because the medium does. A terminal can’t report key release, and a bare console can’t tell the two Alt keys apart when they’re only being held, so hold-to-leap is impossible there. Owlet leaps modally instead, on f and j: left and right on the home row, mirroring owl’s left and right Alt.
Ctrl+I is byte-identical to Tab in a terminal, and Ctrl+digit is unreliable, so the whole formatting tier moved to Alt. That split turned out to be the right one for a different reason: Ctrl stays the universal tier (save, undo, quit, clipboard), and those are exactly the keys that have to work on a bare console with nothing helping.
Headings render in cyan, which is owlet’s only use of color, with the # characters hung out in the left margin so the heading text stays flush with the body.
My favorite owlet bug had nothing to do with text. Idle redraws four times a second were resetting the terminal’s hardware cursor blink phase, so the cursor looked like it was flashing constantly. The fix was splitting one dirty flag into two: one for “re-wrap the text,” one for “repaint.” An idle tick now does zero terminal IO and zero work on the rope, and the cursor blinks like a cursor.
Nesting in Somebody Else’s Eyrie
The shared crate is roost, because a roost is where owls gather. The browser half is eyrie, because large owls don’t build nests. They move into other raptors’ abandoned eyries, which is a fair description of a frontend that lives in the browser’s borrowed infrastructure instead of owning its hardware.
Why There’s No wasm-bindgen
Eyrie compiles the editing core to WebAssembly behind a boundary I wrote by hand. No wasm-bindgen, and not out of taste: it isn’t packaged in Debian, it needs a CLI matched to the exact patch version of the crate, and that CLI wants a newer compiler than the one Debian ships. So commands cross as scalars, strings cross through a scratch buffer, and after every call a fixed-size summary lands in one Uint32Array where JavaScript reads the cursor, the selection, and which lines went dirty without allocating anything.
Who Does the Wrapping
The browser does all the line wrapping. Owlet wraps text itself because a terminal is a grid of cells. A browser already has an incremental inline-layout engine that thousands of people have spent decades on, and anything I wrote to replace it would be slower and wrong in ways I wouldn’t find for months. A position map between text nodes and character ranges replaces the entire layout pass.
Never Ask the DOM Where It Is
Measuring the render path turned up one rule I wrote down because I knew I’d forget it: never read getSelection() on the keystroke path.
That was the “typing feels sluggish” bug, and it’s my favorite thing I’ve learned all year. Everybody knows that setting the selection is expensive. It turns out that reading anchorNode right after you’ve patched the DOM makes Blink re-canonicalize the selection against the new tree, and the cost scales with the size of the document. On a 3,000-line file it was 3.4 ms per keystroke: ninety-four percent of everything typing cost. I was paying full price to ask a question.
A keystroke on that document now costs 0.078 ms.
What It Weighs
The bundle is about 190 KB gzipped, which is nearly three times what it was before I turned project mode on. That’s not the tree screen; it’s the KDL parser, which drags in a diagnostics library and arbitrary-precision arithmetic. A word processor for a netbook is now shipping bignum support to read a config file. The budget is 300 KB and I’m watching it.
Content in Markdown, Structure in KDL
A project is a directory. There’s a KDL file at the root, project.kdl, describing the shape of the project, and one NN_slug.md per section, and a sibling NN_slug/ directory when a section has children. Structure and content never mix. Nothing is buried in frontmatter, and every file still reads as a document on its own.
Delete Owl tomorrow and your writing is still a folder of documents.
The tree screen borrows its shape from Gingko Writer: columns of cards, each column a set of sibling groups, the path from the root to wherever you are drawn as a tinted spine with everything below it in paper white. A card’s title is the first # heading line of its file, not a field in a sidecar. The Markdown file is the title and the body.
New cards land as NN_untitled.md, and the first time you name one, the file renames itself to match. Every rename after that leaves the filename alone, so a section keeps its identity and git history doesn’t churn every time you tweak a chapter title.
The Whole Project Is One Document
The full editor never shows you a single file. It concatenates every node in reading order into one buffer, keeping a table of character offsets that marks where each file starts. Saving splits it back apart and skips the files that didn’t change.
Undo, selection, and LEAP span the whole project for free, because it is one buffer.
The boundaries are fully editable, which is the part I had to think hardest about. Backspace at the top of a section joins that line up into the previous document, and the text redistributes between files when you save. What editing can’t do is create or destroy a boundary. Empty a section out completely and it survives at zero length as an empty file. Structural changes stay tree operations, deliberately. You shouldn’t be able to delete a chapter by leaning on the backspace key.
The divider you see between sections is chrome and nothing else. Owl pads the first row of each section and draws a hairline rule at half the column width. Owlet interleaves a dim · · · at render time and does its typewriter math in visual rows so your cursor still sits dead center. Motions never see it. There is no character in the rope.
Small Decisions I’m Fond Of
The clipboard is internal. Not the system clipboard. The appliance runs on KMSDRM or a bare console, where there is no clipboard backend, so an app-local register is the only thing that behaves the same in development as it does on the device.
Carriage returns get stripped at load. Files saved on Windows or pulled from Project Gutenberg arrive with \r\n, and a bare \r has no glyph in any font Owl ships, so it renders as the missing-glyph box. The rope only ever holds \n.
Destructive keys ask first. Deleting a card arms a confirm: y commits, n or Escape cancels, and any other key cancels quietly. Afterward focus moves to a sensible neighbor rather than vanishing.
Unfocused cards have transparent borders, not no borders. Identical geometry in both states, so focusing a card can never rewrap its text.
A vertical rhythm grid, tried and reverted. Rows snapped to multiples of a base unit, baselines aligned, the whole apparatus. Letting each font use its own natural line height reads better and costs a fraction of the code.
The Numbers the Netbook Exists to Check
There’s a corpus generator that writes a deterministic Markdown document of whatever size you ask for, shaped like prose: varied word and sentence lengths, paragraphs separated by blank lines, salted with the constructs Owl actually models. The point is to exercise the real load path against text that behaves like writing, instead of a block of one repeated character that no layout code finds difficult.
A second generator builds a whole multi-file project. A third binary times the hot paths and prints peak memory. All three are pure Rust with no graphics dependencies, so they compile once and copy onto the netbook.
On the workstation, a sixty-node project of about a megabyte opens its full editor in 9.4 ms, wraps the entire flow in 2.1 ms, costs 2.3 ms per keystroke, and saves in under 5. Peak memory is 14 MB. Push it to a five-megabyte single file and parsing takes 27 ms, a full wrap 10, at 68 MB.
Rule of thumb for an Atom, which runs somewhere between ten and twenty times slower: a wrap pass under about 30 ms feels instant, and 50–100 ms means owlet needs incremental layout before whole-project editing is comfortable there. Two milliseconds times twenty is forty. So the honest answer is “probably fine, barely,” which is not an answer. Owl is insulated, because its layout is already incremental. Owlet re-wraps the entire buffer on every keystroke, and making that buffer the whole project raised the stakes. The netbook is the only thing that can settle it.
It Took Three Tries
Owl is on its third implementation. The first two weren’t mistakes so much as arguments I had to win before I was allowed to keep going.
The first was Lua on Love2D, picked because it was fast to start and I wanted something on a screen. I could not believe how well it ran on old hardware. That was the moment the project stopped being a notion. The machine was fine. The software was the problem. Then documents got big, Lua’s habit of copying strings around caught up with me, and typing turned to glue.
So I rewrote it in C with a gap buffer I wrote myself, to settle one question. Could I load the entire text of War and Peace into memory and edit it smoothly? Three and a quarter megabytes, no paging, no tricks, no pretending the last chapter isn’t there. I could. The file is still in the repo. It’s the test corpus now.
Then I wanted headings and bold and italic, and I wanted the Cat’s LEAP, and it was clear the gap buffer wasn’t going to keep up with either. Ropes are what you use instead. But writing your own rope in C, with a UTF-8 hazard at every index, is a longer project than the word processor that sits on top of it. Nearly every bad bug in the C version was a byte where I’d meant a character.
So: Rust, and a rope somebody else already got right, addressed in characters instead of bytes so that particular family of bug can’t happen again.
War and Peace still opens. It parses in 17 milliseconds and sits in 20 MB, and I still open it sometimes for no reason at all.
Where It Is Now
The editor works, on real hardware. Owl and owlet are both validated on an Atom netbook running AntiX. The browser build has project mode working end to end.
Not done: sync between machines, which is designed as plain git mediated by a small service; backups; and the appliance image that boots straight into the editor.
So the netbook still boots into a desktop it doesn’t need, and Owl still opens as a program on it. The last mile is the one where it stops being a program, and closing the lid means you’re done writing.