The Filesystem as Plugin API: What Plan 9's Acme Gets Right About Extensibility
Source: lobsters
The terminal is one of computing’s most persistent fictions. We treat it as a window into a machine, but it is really a simulation of a simulation: a graphical program emulating a physical device that was itself an abstraction over teletype hardware from the 1960s. Plan 9’s Acme, designed by Rob Pike in the early 1990s, starts from a different premise, and Daniel Moch’s recent exploration of it is a useful occasion to examine what that premise means in practice, particularly around the question of extensibility.
What Acme Is and Is Not
Acme is not a text editor in the conventional sense, though it does edit text. It is not a terminal emulator, though you can run shells inside it. Rob Pike’s original 1994 paper describes it as “a user interface for programmers,” which is deliberately broad. The more precise description is that Acme is a window system built entirely around text, where the boundary between editing text and operating a computing environment has been dissolved.
Acme windows have two parts: a tag and a body. The tag is a single horizontal strip at the top containing the window’s name (usually a file path or directory) alongside a set of words. The body is everything below. Both are editable text. You can type anything into the tag, execute any word in it by middle-clicking, and place your own commands alongside the built-in ones (New, Get, Put, Look, Del). The tag is not a toolbar or a metadata field; it is text, and executing it is just sending that text to the environment.
The Three-Button Model
Acme’s interaction model requires a three-button mouse, which is worth understanding before treating it as an antique design choice. Button 1 (left) places the cursor and selects text. Button 2 (middle) executes the word under the cursor or the current selection as a command. Button 3 (right) searches for selected text in the current window or, more interestingly, plumbs it.
“Plumbing” is Plan 9’s inter-application routing mechanism. When you right-click on a filename, a compiler error like main.go:42, or a URL, the plumber matches the text against a set of pattern rules and routes it to the appropriate handler. The rules live in a user-configurable file using a small predicate language. A rule for opening Go source locations might look like this:
type is text
data matches '([a-zA-Z_/][a-zA-Z_/0-9.]*\.go):([0-9]+)'
plumb to edit
plumb client $editor
Each rule is a series of predicates followed by a terminal action. Another rule might send URLs to a browser. The effect is that any text in any window is potentially actionable without typing a command, switching modes, or opening a menu.
Chord gestures extend this further. With Button 1 held to maintain a selection, clicking Button 2 executes a command with the selection as an argument; clicking Button 3 plumbs or searches for the selection. This three-way interaction between selection, execution, and navigation has no direct analog in terminal workflows, where the sequencing is always linear: type command, press enter, read output.
The Acme Filesystem
Plan 9’s “everything is a file” principle goes further than Unix’s version. Acme exposes its internal state through a synthetic filesystem served over the 9P protocol. Each window gets a directory, /mnt/acme/ID/, containing files named addr, body, ctl, data, event, and tag. External programs read and write these files to interact with windows from outside.
The event file is particularly significant. When you click or type in a window, Acme writes a structured record in a fixed format:
<origin><type> <q0> <q1> <flags> <n> <text>
Origin is one of E (external tool), F (filesystem/plumber), K (keyboard), or M (mouse). Type is x/X (execute in body/tag), l/L (look/search), i/I (insert), or d/D (delete). The fields q0 and q1 are character offsets bounding the event text, and flags encodes whether Acme expanded the selection automatically or already handled the event internally. A program reading from event can intercept events, modify them, or handle them entirely. Writing an event back passes it through to Acme for default handling.
This is how Acme “plugins” work: they are independent processes communicating over the filesystem, not shared libraries loaded into the editor’s process space. Writing an integration looks roughly like this in Go, using the acme package from Russ Cox’s plan9port:
w, err := acme.New()
if err != nil {
log.Fatal(err)
}
w.Name("/guide")
w.Write("body", []byte("Hello from an external tool\n"))
for e := range w.EventChan() {
switch e.C2 {
case 'x', 'X': // execute event
if string(e.Text) == "MyCmd" {
doSomething(w)
} else {
w.WriteEvent(e) // pass unhandled events back to Acme
}
}
}
This model means that extending Acme is just writing a program. There is no plugin API to learn, no event loop to hook into, no editor-internal object model to understand. Any language capable of opening files and reading bytes can integrate with Acme fully. The filesystem is the API, and it has been stable for over thirty years.
Comparison with Modern Extensibility Models
The Language Server Protocol addresses a superficially similar problem: decoupling language intelligence from the editor. But LSP is editor-centric by design. The language server exists to serve the editor’s model of the world, providing completions, diagnostics, and hover text on the editor’s request cycle. In Acme’s model, external tools attach to windows as peers; the editor does not orchestrate them.
Terminal multiplexers like tmux solve session persistence and window management but remain entirely within the terminal abstraction. They organize columns and panes of character streams on pseudoterminals. Acme’s columns and rows of windows look similar from a distance, but every window is an addressable filesystem object, not a character stream with escape codes.
Neovim’s extensibility is more sophisticated: an RPC API, a Lua runtime with access to editor internals, and a growing ecosystem of plugins. This is powerful, but it requires learning Neovim’s internal model: buffers, windows, tabpages, the async event loop, which API functions are safe to call from which contexts. Acme’s filesystem interface requires learning almost nothing Acme-specific. The primitives are files and processes, which developers already understand.
The closest modern analog might be VS Code’s language server integration, where features are provided by external processes communicating over a protocol. But VS Code still has a massive built-in feature set, a JavaScript extension API, and a packaging ecosystem. Acme has essentially none of that, and the sparseness is a design decision rather than an oversight.
Sam and the Historical Thread
Acme did not emerge in isolation. Rob Pike’s earlier editor, Sam, introduced structural regular expressions and a command language for addressing and manipulating text. Sam’s x/re/cmd construction means “for each non-overlapping match of re in the current selection, run cmd” — and cmd can itself be another x invocation, allowing arbitrarily nested decomposition. To delete all blank lines in a file:
,x/^$/d
To replace foo only inside C block comments:
,x/\/\*([^*]|\*[^\/])*\*\//x/foo/c/bar/
The outer x iterates over comment regions; the inner x iterates over matches within each region. This is meaningfully more expressive than sed’s line-oriented model because the addressable unit is a regex match, not a line, and operations compose freely. Acme absorbed Sam’s address language and makes it available interactively through the Edit command in any window’s tag. The addr file in Acme’s filesystem is Sam’s address model exposed as a writable file: write 10 to set the selection to line 10, write /foo/ to advance it to the next match, then write to data to modify exactly that region from an external program.
The lineage matters because it shows these ideas have been tested over decades. Acme is not a research prototype that never shipped; it was the daily working environment for a significant portion of the Plan 9 team at Bell Labs through the 1990s and into the 2000s.
Using Acme Today
Plan 9 is not where most people work, but plan9port, maintained by Russ Cox, brings Acme and the rest of Plan 9’s userland to Linux and macOS. The win command gives you a shell window inside Acme. The plumber can be configured to handle file paths, error messages, and URLs by routing them to appropriate programs on the host system.
The experience on a non-Plan-9 system is real but incomplete. On Plan 9, applications speak plumber natively. On Linux, almost none do. You end up configuring plumbing rules to bridge between Acme’s model and a system that was not designed with it in mind. Tools like Watch (rerun a command on file save) and acme-lsp (an LSP client that reads Acme events and calls language servers) are available and work well, but you are assembling a coherent workflow from parts rather than inheriting one.
There is also Edwood, a Go port of Acme that maintains the same design and filesystem interface while being easier to build and modify on modern systems. It is actively developed and a reasonable entry point for understanding the internals without wrestling with Plan 9 C.
The community around 9fans and the broader Plan 9 from User Space ecosystem maintains a collection of Acme helper programs. These demonstrate the filesystem-as-API model working cleanly but also reveal its friction on modern systems: each helper is a standalone process, which means you need to manage process lifecycles, and there is no package manager for Acme extensions in the way VS Code has a marketplace.
What the Design Demands
Acme’s design makes two specific demands. The first is accepting the mouse as a first-class input device. This is a reasonable objection for keyboard-centric developers, but the mouse-centricity follows from the model. If any text in any window can be executed or plumbed by pointing at it, you need a way to point that is more precise than cursor movement, and the mouse is better suited to that than keyboard navigation.
The second demand is accepting an environment with fewer built-in answers. Acme will not autocomplete your code, highlight your syntax, or manage your version control workflow. It provides a programmable surface and steps back. Whether that sparseness is productive depends on how you work and how much time you are willing to invest in building the surrounding tooling.
The original article frames Acme as a “text-based GUI” to distinguish it from TUIs, and that framing holds up. Acme is not a terminal application with a fancy rendering mode; it is a graphical application where text is the fundamental computational object. The distinction clarifies what Acme is doing: not making terminals more powerful, but replacing the terminal’s role in the workflow with something that takes text more seriously as a substrate, one that can be addressed, routed, executed, and observed by external processes through a stable, file-based interface.