# Filesystem Every native session serves 9P2000 on a Unix socket. Pane shells receive `PARDES_PID` (the editor's process id), `PARDES_9P` (socket path) and `PARDES_PANE` (pane serial). The socket is `$XDG_RUNTIME_DIR/pardes-9p-.sock`, or lives under `~/.local/state/pardes` when XDG_RUNTIME_DIR is unset. Detached sessions use their session name; `--9p=` overrides it. A `pardes ` launched from a pane forwards Look to that pane over 9P. `PARDES_PID` alone says the shell is inside pardes; `PARDES_9P` and `PARDES_PANE` say how to reach it, and a launch that has the first without the other two refuses rather than opening a second editor. `--nested` opens a separate editor and withholds `PARDES_PID` from its pane shells, so a pardes started in one of them runs a session of its own; its 9P service stays available. Look resolves the OS filesystem first, then the editor's virtual filesystem. Explicit paths bypass that search: | Editor path | Meaning | 9P server path | |---|---|---| | `/n/os/proc/self` | OS filesystem | `/os/proc/self` | | `/n/self/pane/2/body` | pane 2's text | `/pane/2/body` | | `/virtual/pane/2/body` | the same, in the editor's own spelling | `/pane/2/body` | | `/virtual/src/pardes.zig` | source embedded in this build | `/src/pardes.zig` | | `/n/peer/pane/2/body` | another session's text | peer's `/pane/2/body` | The mount name `self` is reserved and maps to the server root, so `/n/self/X` and `/virtual/X` both name the served `/X`. `--mount=peer=work` mounts the named session `work`; the dial can also be an absolute socket path, `unix!/path`, `tcp!IP!port`, or `quic!IP!port`. At runtime, use `Mount peer dial` and `Unmount peer`. There are eight named mounts; `os` and `self` are reserved. Unmount refuses mounts still used by a pane, its working directory, or a pending Save. Mounts are saved in dumps. Save uses the file's original mount. `pardes --9p-tcp='tcp!127.0.0.1!5640'` adds a TCP listener alongside the Unix socket. Build with `-Dquic=true` and system OpenSSL 3.6+ to enable QUIC; `--9p-quic='quic!127.0.0.1!5641'` adds its listener. Both accept numeric IPv4/IPv6 addresses, not DNS names. Listener port zero chooses a free port; `/listeners` reports all active dial addresses. All connections have session access, including `os`. TCP is unencrypted. QUIC uses an ephemeral TLS identity without peer verification or login. It carries 9P2000 on one bidirectional stream with ALPN `pardes-9p`. Unix and TCP connections share four slots served by cloud9's `std.Io` runner; QUIC has four of its own on the editor's poll loop. OpenSSL's internal buffers are separate, dynamically allocated memory. [Plan9port's client](https://9fans.github.io/plan9port/man/man1/9p.html) can drive Unix or TCP without a kernel mount, and `9ns` mounts the tree in a private namespace: ```sh 9p -n -a "unix!$PARDES_9P" read index 9p -n -a 'tcp!127.0.0.1!5640' ls pane/1 9ns --unix "$PARDES_9P" -- sh -c 'cat "$NINE_MOUNT/index"' ``` A `9ns --unix` mount lives in the private namespace of the command it runs, and nothing outside that command sees it. The mount everyone on the machine shares is the registry one, `9ns --mntgen` (default `/mnt/9p`): every running editor posts itself there, so `/mnt/9p/pardes//` is that editor's tree for any process. A new pane made through `pane/new` is a scratch named `/+New` until it is given a name, and closing a column's last pane leaves such a `+New` in its place (`Delcol` closes the column). For [Linux v9fs](https://www.kernel.org/doc/html/latest/filesystems/9p.html), use `version=9p2000,cache=none,access=any` and `trans=unix`, or `trans=tcp` with `port=5640`. Set `uname`, `dfltuid`, and `dfltgid` for the local user. Leave `aname` empty. The opt-in [Linux v9fs experiment](v9fs.md) tests a kernel mount in a separate subprocess namespace (`zig build v9fs-test`, requiring explicit mount authorization). Neither 9P2000.u nor 9P2000.L is implemented. Existing Plan9port/v9fs clients need a userspace bridge for QUIC. ## The served tree ``` /README this guide, also src/fs-help.txt /index one line per pane: serial, kind (text|term|pdf|image), dirty flag, name /status pid, version and pane count /look write a line: a right click on it at the active pane; read: the serials it touched /exec write a line: a middle click; read the same serials /log recent events, one a line: new|del|rename|save , msg ; write follow to that open to wait for more /screen rendered screen JSON; frozen per open handle /listeners the session's dial addresses /focus the serial of the pane with the keyboard; write a serial to give it the keyboard /ctl the settings, one a line as a write takes them; write a setting or a session builtin /commands every builtin: word, `arg` if it takes one, and `root` or `pane`, the ctl that takes it /pane/new open it to make a pane; the read answers that pane's serial /pane// name body tag ctl addr dot limit data xdata sel dirty mark scroll errors event look exec, plus pty/{ctl,status,data} on terminals /os/ the host filesystem /src/ the editor's embedded sources, only when built with -Dembed-sources=true ``` Control messages are split by what they act on, as acme keeps window verbs on a window's ctl and webfs and upas/fs keep session settings on a root ctl. Each builtin declares its scope in src/builtins.zig (`scope = .session`; every setting is one, the rest act on a pane). `/ctl` takes the session's builtins, one a line, at whichever pane has the keyboard as each runs -- `Newcol`, `Dump`, `Mount name dial`, `Theme ink`, `Verbose off`, and `Kill`, which **quits the editor** (acme's Kill only stops commands; pardes's is acme's Exit) -- and reads every setting there is, one a line, in the words a write of it takes (`Verbose on`, `WindowOpacity 70`, `PanelSlide off`, `DumpDir` bare for the default directory, `LocationsConfig ...`), so writing what it reads back changes nothing; platform and startup facts are `/status`'s and the Config window's, not settings. A pane's `ctl` takes the builtins that act on a pane (`Del`, `Save f`, `Collapse`, `Find pat`) beside acme's `get`, `lock` and `unlock`. The column words are pane words too, acting on the column that pane is in: `Delcol`, `Collapse`, `DelAbove`, `DelBelow` and the focus moves `Left`/`Right`/`Up`/`Down` from it. `Joincol` and `Newcol` are the root's: they act on the column of the pane with the keyboard. The words are case-sensitive and do not alias: acme's verbs are lowercase and the builtins keep their tag spelling, so `Get` is no word and `del` none either. A write is checked whole before any line of it runs, and a line is refused in Plan 9's words for a ctl (kernel/misc/parse.c:82-97), quoting the line: `unknown control message "X"`; `wrong #args in control message "X"` for an argument to a builtin that takes none, or none to one that needs it (`Msg`, `Mount`, `Find`, a setting's value but a switch's, which flips bare); `bad value in control message "X"` for a setting's value it does not take; and `not a session control message "X"` or `not a window control message "X"` for a word of the other ctl. 9ns maps them all to EINVAL, and a write refused here has done nothing. A line that then fails as it runs fails the write with the error the editor reports for it and the line, e.g. `Mount: AlreadyMounted "Mount peer /tmp/s"` (EIO); so does `control message needs its argument "Save"`, for a builtin that would have asked at a prompt (a `Save` on a scratch) rather than open one nobody is there to answer. The lines before a failing one have taken effect and those after it never run, which is what acme's ctl loop does (editors/acme/xfid.c:600-790). An error that only happens as the editor performs what a line asked for -- a `Save` whose disk write fails -- is reported in the editor and /log, not in the write's answer. Like any write, a ctl write answers once the editor has performed what it asked for (a save written, a shell started). A click on the same word, or the word written to `exec`, still opens its prompt. `/commands` lists every builtin the registry holds, in registry order, one a line: its word, `arg` when it takes one, and `root` or `pane` for the ctl that takes it, e.g. `Newcol root`, `Save arg pane`, `Verbose arg root`. It is generated from the registry, so it is always this build's own list. `/focus` reads the serial of the pane with the keyboard, and a serial written to it gives that pane the keyboard, off any column or workspace tag that had it -- rio's `current` written to a window's `wctl`, named once for the whole tree since there is one keyboard. While a column's or the workspace's tag has the keyboard no pane does: `/focus` reads empty and every pane's `ctl` says `notcurrent`. A write gives the keyboard and nothing else: a folded pane stays folded (unfold it with `Collapse` on its ctl), as rio keeps `current` apart from `unhide`. A serial no pane has fails with `no such window`; anything but a number, with `ill-formed control message`. A pane is made by **opening** `/pane/new`, and closed by Tremove on `/pane/` (`rmdir`), which is the only remove the tree serves; Tcreate is refused everywhere, as it is in acme. Reading the open fid answers the serial of the pane that open made, so `n=$(cat /pane/new)` makes one and names it in a line. Each open makes another pane, and two reads of one fid answer the same serial: the open acted, the read only observes. Closing the fid leaves the pane. This is `/net/tcp/clone`'s mechanism, not acme's `new`, and the difference is deliberate. acme allocates during the *walk* and lets the walk land inside the new window, so `/dev/new/body` works in one step (acme(4): "accessing any file in `new` creates a new window"). acme can also afford to list `new`, because a Plan 9 directory read carries the stat of every entry and nothing walks. A kernel or FUSE mount is not so lucky: it walks and stats each name a listing gave it, so an allocate-on-walk name would make a pane per `ls -l`. Allocating on open instead keeps `new` listed and `ls` honest — a stat is not an open — at the cost of acme's one-step `new/body`. Nothing in the tree is created by list, stat, walk or read; only that one open. Every other name in `/pane` is a serial. A session can open its own tree through a mount: a Look at `/mnt/9p/pardes//pane/2/body` from inside that very editor opens it, and a Save of that pane writes back through the mount into pane 2. Requests on the Unix and TCP listeners are answered on the 9P connection's own task, not by the editor's loop, so the realpath, the stat and the read the editor makes out through the mount come back while it waits for them. The one rule is whose turn it is with the core (`pardes.turn`): the editor has it, and gives it up while it waits for input and while a step of it is out in a syscall. A step of a connection task's own -- a Look written to `look` -- goes out the same way, and the editor waits for it to return before it takes a step of its own. While any step is out, a request that would change a pane (a write, a truncation, an rmdir) parks in the engine until none is; everything else, opening `pane/new` and `screen` included, is answered at once, which is why a Look at any path in the tree comes back. A write into the tree from the editor itself only ever happens between steps (a Save), so nothing it waits on out there is a request that has to park. QUIC is still served on the editor's loop, so through QUIC the old hang remains. `/n/self/...` names the same tree without leaving the process. `/look` and `/exec` are the editor's two clicks, one per line of a write: - a line written to `look` is a right click on it: a path opens a file, `file:12` jumps to a line, a directory opens a shell there, a URL opens in the browser. - a line written to `exec` is a middle click: a command word from `src/builtins.zig` (`Save`, `Del`, `New`, `Newcol`, `Mount NAME DIAL`, `Unmount NAME`, `Dump`, `Restore`, `Msg TEXT`, `Find`, `Grep`, `Tty`, ...), or anything else, which is typed into a terminal: the pane itself when it is a terminal at its prompt, otherwise a terminal in the pane's directory that is at its prompt, and failing both a new one made below the last column. That is not an error, whatever the shell makes of the line, and a misspelled builtin word ends up there too (whether it should is an open question, docs/open-questions.md). `echo Tty > pane//ctl` makes a terminal in that pane's directory outright. The root's pair clicks at the active pane and `/pane//look` and `/pane//exec` at that pane. Blank lines are skipped, and every other line is checked before any of them runs, so a control character fails the whole write with EINVAL; a command that fails inside the editor is reported on the message row, not as a write error. Reading any of these files answers the serials of the panes the last command created, or, when it created none, the pane a look focused or the pane an exec acted on (even one it closed), one per line. `/pane//name` reads the pane's file name (a terminal's directory) and writing it renames the buffer; a relative name resolves against the pane's directory. `body` appends on write and replaces on truncating open. `sel` reads the selected text and writing it replaces the selection. `errors` appends to the directory's `+Errors` pane. Holding `event` open redirects the pane's Look and Exec clicks to that client; writing a record back performs the action. `ctl` reads acme's window status line — serial, tag length, body length, a reserved zero, the dirty flag, the width in cells, the font and the tab width — followed by rio's `current` or `notcurrent` (rio(4), `wctl`): whether the pane has the keyboard. It takes the pane's builtins (below), `get`, which reloads the buffer from the name it carries, and acme's `lock` and `unlock` (editors/acme/xfid.c:603-611), for an edit of several writes to `addr` and `data` that another client must not land in the middle of. As in acme the lock binds only the clients that take it: a `lock` while another open holds it fails at once with `file in use` (EBUSY), to be tried again, until that open writes `unlock` or closes (or the pane does) -- where acme's blocks, because through a kernel or FUSE mount a blocked write would hold up the holder's own `unlock` and close on that file -- and nothing else is refused for it -- not a write to any other file, not the person at the keyboard. It belongs to the open that wrote it, so only that open's `unlock` is taken; a write on an open that cannot write (or the editor's own, on none) cannot lock. From a shell the lock needs an open held across the edit, since `echo lock > ctl` closes, and so unlocks, at once: `exec 3>ctl; echo lock >&3; ...edits...; exec 3>&-`. The three range files `addr`, `dot` and `limit` each read the pair of offsets they also accept, so copying one onto another is all that acme's `addr=dot`, `dot=addr` and `limit=addr` ever were. A write is either that pair or an address expression (`#0,#5`, `/pattern/`, `2+1`); `addr` selects what `data` and `xdata` read or replace, `dot` is the editor's own selection and moving it scrolls the pane into view, and `limit` bounds a search and reads empty until it is set. Truncating a range file empties it; truncating `limit` lifts it. Truncating `data` or `xdata` deletes the range `addr` names and nothing else, so a shell's `echo NEW > data` replaces that range, `: > data` deletes it, and `>>` inserts at it; only truncating `body` empties the whole buffer. `addr` belongs to the pane rather than to a client and keeps what was written until someone writes or truncates it, so writing an address and reading it back evaluates it, which is what acme(4) promises of its own `addr`. The regular expressions are mvzr's (sets, `\d`/`\w`/`\s`, `{m,n}` and lazy `*?` included), searched the way sam searches (editors/acme/regx.c): as lines, so `^` and `$` match at the start and end of any line, `.` and a negated class never match a newline, and `$` also matches at the end of a text with no final newline. A pattern that names a newline (`\n`) runs over the whole text instead, its `.` kept to one line. `/re/` searches forward from the end of the current range to `limit` if one is set, and otherwise wraps to the start of the text; `?re?` finds the last match ending before the range, wrapping to the text's last. The match is the leftmost, but of the alternatives at that place mvzr takes the first that matches where sam takes the longest (`/gam|gamma/` finds `gam`); in a search begun in the middle of a line, `^` inside an alternation can match there; and in a pattern that spans lines, `^`, `$` and `[^...]` keep mvzr's own meaning. pardes has no regex engine of its own on purpose; these are its limits. An address that does not evaluate fails the write with why: `bad address syntax`, `no match for regexp`, `address out of range` or `bad regular expression`. A failed write to `addr` leaves no address at all, where acme keeps the old one: until an address is written or `addr` is truncated, reading `addr`, and reading, writing or truncating `data` and `xdata`, fail with `no address: the last one written to addr failed`, so a script that missed its target cannot then write at the last one. The three flag files `dirty`, `mark` and `scroll` read `0` or `1` and take `0` or `1`: whether the buffer differs from its file, whether a write pushes an undo point (writing `1` pushes one now), and whether a write scrolls the pane. `tag` reads the whole tag as the pane shows it: the computed path, dirty marker or PDF page, then the text you may edit. A write appends to that text, newlines included, and a tag with more than one line takes a row per line on screen; truncating `tag` clears it, as acme's `cleartag` does. The clearing is an edit of the tag like a typed one and its undo history is kept: `u` in the tag brings back the text it cleared. Stats report real lengths for `index`, `status`, `look`, `exec`, `listeners`, `name`, `body`, `tag`, `sel`, `ctl`, the range files and the flag files, and for `event` and `pty/data` the length of the record a read would answer, which is zero when nothing is waiting; for `log`, the text an open would freeze now. Modes are 0644/0666 (0444 for read-only files, 0222 for write-only); mtime is the pane's last edit or the process start. The qid version of `body`, `data` and `xdata` is the pane's revision, so a stat sees an edit land without reading the text; every other file leaves it zero rather than promise a version it cannot keep. Directory entries carry no sizes, and neither does `/screen`, which has no length until an open renders its frame; stat the entry. `/log` is one ring (64 KiB) that records whether or not anyone reads it: `new`, `del`, `rename` (a terminal's too, as its shell changes directory, since a terminal is named by its directory) and `save `, and `msg ` for every line the editor says, repeats included (with `verbose` on, that includes each builtin announcing itself as it runs), and `err : ` for every write or truncation the tree refused or that failed -- through a mount a shell sees only the errno its kernel mapped the reply to, usually `Invalid argument`, and this is the reason (`err 3 addr: no match for regexp`). There is no per-pane error file to read instead: acme's `errors` only takes text, and one record stream is simpler to watch than a file per pane. A `msg` said while a pane is being made can precede that pane's `new`; panes present at boot are recorded before anything else. Control characters in a record become spaces, so a record is one line. An open freezes the ring's text the way `/screen` freezes a frame: reads walk it and end. Writing `follow` to that same open makes reads past it wait for the next record, one per read; a follower the ring outran reads `lost N` first. Closing the open is the only way back, as with rio's `consctl`. A read with nothing to give yet -- a following `log`, `event`, `pty/data`, a `pty/run` before its answer -- is held, the way factotum holds its log's reads (security/auth/factotum/log.c) and acme an event read: the core keeps it, and whoever next has news for it (a record, output, a run's answer, the pane closing, which answers acme's "window shut down") answers it on the connection it came on as the turn is given up. Nothing else parked is retried for it. The core keeps the ticket cloud9 gave the park (`Conn.hold`) and answers only while that very park waits, so a read the client flushed or whose fid it clunked meanwhile is dropped unanswered, no record is spent on it, and a tag the client reuses is never answered with what was meant for the old one. An open waits with one read at a time, as acme's window keeps one `eventx`; a second read on it meanwhile fails with "file in use". QUIC connections still retry their parked reads each tick. `pty/run` runs one line at a terminal's prompt and answers how it ended, on the same open (factotum's `rpc` shape): write the line, then read `exit N` once the command has ended and the shell is back at a prompt, followed by what it printed. The output is what the screen showed between the command's start and end marks: stderr interleaved, `\r` progress collapsed to its last state, no colour, tabs as the spaces they drew, trailing spaces trimmed and trailing blank lines dropped (`printf 'a\n\n\n'` answers `a`), leading whitespace kept; a program on the alternate screen (vim, less, htop) leaves none, and rows a program redrew above its start are missed. A bash job notice printed before its PROMPT_COMMAND lands in the next run's output. Only its last 64 KiB are kept, from a line start, and the header then reads `exit N cut M` (M bytes left out). `exit N cut`, with no count, says the output's start is not there to read: it scrolled out of the history, the command erased the screen (`clear`, `watch`, a full-screen program's redraw, a reset -- so such a command's output may read as cut), a start or end mark came on the alternate screen, or the command printed more than 8192 rows, of which only the last are read so that the answer costs the editor a bounded amount. `exit ?` is a command whose end mark carried no status, which is not a success; `error out of memory` is an answer that could not be made. The header is always the whole first line. It reads `busy` at once when a command is running or text is typed at the prompt, which is also when the third field of `pty/status` reads 1. A line written before a new terminal's shell has drawn its first prompt is not busy: it waits for that prompt (a respawn meanwhile keeps it waiting for the new shell's) and is sent then, so the first command a script gives a fresh terminal is not lost; `error not run` when the shell refused the line without running it (a fish syntax error; the line is taken back off the prompt); `error shell gone` when the pane closed or its shell was replaced; `error no prompt marks` for a shell pardes could not instrument. It relies on the OSC 133 marks pardes injects into bash and fish, tagged `aid=pardes` so fish's own marks and a nested shell's are ignored. A second line on an open whose command still runs fails the write. `exec zsh` or a continuation prompt never reports an end; cancel the read. A read of `run` waits in pardes, so through 9ns it needs 9ns's concurrent requests or it holds up the rest of the mount. A record longer than a read comes in pieces, so a shell's `read` loop works. `tail -f` never writes `follow`, so it sees nothing new: use the follow open instead. `/screen` returns JSON with `cols`, `rows`, `cursor`, a `styles` table, and row-major `cells` of `[grapheme, style_index]`. Each open freezes one frame until close. A terminal `body` freezes its history on the first read of each open handle; `pty/data` streams live output. An open that holds something between open and close -- a frozen screen, terminal body or log, a run, an `event` or `pty/data` open -- takes one of 64 records (lib9p's per-fid aux, acme's Fid), released on close or disconnect; past that such an open fails with `ENFILE`. Other opens hold nothing and are not counted. `-Dembed-sources=true` embeds the editor's sources and serves them under `/src` (and `/shaders` on GUI builds). `EffectCode ` lists the current backend's implementation files under `/virtual`, which Look opens; without the option the command reports the sources as unavailable. The esp32p4 build enables the option by default, so the device can serve its own source. This is a control filesystem, not a complete POSIX export. Native filenames may contain up to 255 bytes. Existing regular OS files support read, write, and truncation to zero; under `/os` protocol create, remove, rename and other metadata changes are refused, as is every create in the control tree and every remove in it but a pane directory's. Ownership and permissions under `/os` are synthetic. Zero-length truncation accepts the accompanying `mtime` hint sent by Linux v9fs; the hint is not stored. Standalone timestamp changes remain refused. The tree lives in `src/ninep/`: `tree.zig` (nodes, lookup, readdir, dispatch, and the editor's reply payload over cloud9's backend contract), `pane.zig` (pane files), `ctl.zig`, `addr.zig`, `pty.zig`, `events.zig` (event and log streams), `screen.zig` and `sources.zig`. The protocol engine is cloud9's `fs.Server`, configured in `src/9p.zig` (the editor's and the board's capacities); the transports are `src/9p_io.zig` (cloud9's `serve.Runner` for Unix and TCP, a poll loop for QUIC, and the 9P client for mounts); `src/fs.zig` keeps host access, mounts, resolution, find and grep. `zig build fs-test` drives real sessions using the independent Python client in `test/ninep.py`; `zig build fs-discovery-test` checks that browsing creates nothing, that an open of `/pane/new` and a remove work, and that `look`, `exec`, `name`, `sel` and `log` behave. `zig build 9p-test` checks the two engine configurations' budgets (the engine's own tests are cloud9's `zig build test`); `zig build fs-bench` measures filesystem transactions in the core. `zig build fs-test quic-test -Dquic=true` also exercises QUIC mounts and I/O.