# 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"' ``` 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 one line per editor event: new|del|rename|save ; reads park /screen rendered screen JSON; frozen per open handle /listeners the session's dial addresses /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 ``` 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. `/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 runs in the pane's terminal. 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 — and takes one verb, `get`, which reloads the buffer from the name it carries. 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. `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 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. Truncating `tag` clears the part of the tag you may edit. Stats report real lengths for `index`, `status`, `look`, `exec`, `listeners`, `name`, `body`, `tag`, `sel`, `ctl`, the range files and the flag files, and for `log`, `event` and `pty/data` the length of the record a read would answer, which is zero when nothing is waiting. 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` records `new`, `del`, `rename` and `save` while at least one client holds it open; a read parks until a record arrives. `/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. Screen and terminal-body snapshots share 32 handle slots, released on close or disconnect. `-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 the walk to `/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.