Architecture

One headless core does the font work. Several front-ends put an interface on it. This is why, and what belongs where.

StatusCurrent, and the reason for most of the code's shape
StabilityThe split is settled; which front-end is default is not

The Shape

Runebender is one editing engine with interchangeable shells.

The pieces
runebender-coreEvery operation that changes a font. No window, no toolkit, no drawing.
runebender-gpuiThe current editor. Native on desktop and in the browser from one codebase.
runebender-xilemThe same editor on Xilem, the Linebender UI stack. More experimental.
runebender-webVello and Kurbo in WebAssembly with a Vue interface around them.
runebender-druidThe original, kept as project history.

Each front-end owns its window, input and drawing, and calls core for anything that changes a glyph.

The Rule

If an edit changes a font, it belongs in core.

That single rule explains most of the layout. Core can be tested without opening a window, which means the hard parts (segment surgery, overlap removal, interpolation) have tests that run in milliseconds. It also means a second front-end is a new interface rather than a second editor.

The test is not “is this about drawing” but “would another front-end need it”. Where a click lands is the shell’s business. What that click does to the outline is core’s.

Why More Than One Front-End

Because the interesting question is not settled.

GPUI is Zed’s toolkit. It is fast, it ships in a real application, and it compiles to WebAssembly, so one codebase serves desktop and browser. That is why it is the default today.

Xilem is the Linebender stack, which is where this project’s sympathies are and where its dependencies already live. It is younger. The port exists so the comparison is made on a real application rather than a demo. It also keeps the editor ready if the answer changes.

A shell that is hard to write on one stack and easy on another is evidence. Keeping two honest is the cost of collecting it.

What Sits in Core

Everything that changes a font or reads one to answer a question, plus the document model and the command line.

Modules are named for what they do: glyph_ops, cleanup, effects, measure, lib_keys, project, and so on. The full map, by concern, is on Code Layout.

The Format Is the Model

Runebender edits UFO through norad rather than reading a font into a private model and writing it back.

The gain is that nothing is lost in translation, and any other tool can read the sources mid-session. The cost is that the file’s shape is the editor’s shape, including the parts of UFO that are awkward.

This is also why an agent or a script can work alongside the editor at all. See Agents.

Two Versions of Kurbo

Core carries kurbo twice, as kurbo_09 and through its consumers.

Front-ends are pinned to their toolkit’s version, and a library can only declare one. Rather than hold every front-end to the oldest, core keeps the geometry types it needs at a pinned version and converts at the boundary.

It is a real cost. Every consumer is on kurbo 0.13 now, so the only conversion left is around the hyperbezier solver.

Inside a Front-End

runebender-gpui is one Workspace struct, split across files by concern: commands, canvas, panels, input, theme accessors, and the widgets the editor owns. Font logic is not among them. The editor imports what it needs from core by name, at the top of main.rs.

A command is the whole of one intent. The menu item, the shortcut and the context menu all land on the same method. There is one place to read, and one place to fix.

The file-by-file map is on Code Layout.

Decisions Worth Knowing

Small choices that look odd until you know why.

The ones that surprise people
One canvas, not one per cellOutlines in the grid are painted by a single canvas over the whole grid. gpui ends its render pass at every run of paths, so a canvas per cell meant a pass switch per cell.
Themes are compiled inOne token file, resolved at startup. It is what keeps the editors from drifting apart. See Themes.
The editor owns its widgetsText fields are built on parley. A dependency that themes itself is a second design system to keep in step.
Undo snapshots the glyphSimpler to reason about than a command log, and a glyph is small.
Files are watchedSources changed on disk reload. This is the whole integration surface for scripts and agents.

Files, Not a Runtime

Runebender’s automation lives outside the application.

There is no scripting runtime in the editor, no plugin API and no assistant. Instead the sources sit on disk as UFO, the editor reloads what changes, and the same operations run from a shell through runebender. Whatever you already use for scripting keeps working, because the interface is the file.

The other approach is to put the runtime inside: an environment, a language and an assistant that all ship with the editor. Counterpunch does that, with Python in the browser and an assistant that writes scripts for it.

What each choice buys
A runtime insideNothing to set up, and an assistant that knows the editor’s own API.
Files outsideYour git history, your build, your existing scripts and your own agent all keep working.

Neither approach sends your font anywhere. That is a separate question from where the automation runs, and both answers can be private.

How Unix-Shaped It Actually Is

Mostly, and it is worth being exact about where it is not.

Against the old rules
Do one thing wellHeld. Core does font work, the CLI exposes it, the front-end draws. None of the three needs the others to be tested.
Work togetherHeld. Files on disk are the interface, the editor reloads them, and exit codes let a script branch without reading prose.
Text as the universal interfacePartly. Output is JSON on request and UFO is XML on disk, but a font source is a directory, not a stream.

Three places it does not hold:

runebender takes paths, not stdin, and writes reports rather than fonts. You cannot pipe one invocation into the next. That is a real limit, not a stylistic one, and it comes from a font source being a directory of files.

Configuration is thin. There is a file, at ~/.config/runebender/config.toml, and it sets three things. Themes are still compiled in, so the file names one rather than defining one.

A window is not a filter. The editor is a normal desktop application, and no amount of architecture changes that.

The claim that holds is narrower than “it is a Unix tool”. The parts are separable, the interface is a file, and the shell can reach the operations the window can.

The Browser Build

The same code compiles to WebAssembly. What changes is what the platform allows, not the editing model.

Text fields cannot take focus, in-window menus do not activate, paste is unavailable, and export is not possible. Those are limits of the build, and the native application has none of them.

See Development for building it, and Troubleshooting for what to expect.