From 57b30ba3e38153a4446626449b0fed5120da954c Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Tue, 29 Sep 2026 19:25:00 -0300 Subject: The docs and the 9P skill say each fact once, in the file that owns it, and say only what a live session does Co-Authored-By: Claude Opus 5.5 --- docs/fs.md | 1773 ++++++++++++++++++++++-------------------------------------- 1 file changed, 645 insertions(+), 1128 deletions(-) (limited to 'docs/fs.md') diff --git a/docs/fs.md b/docs/fs.md index ca02082b..e4750690 100644 --- a/docs/fs.md +++ b/docs/fs.md @@ -1,1142 +1,659 @@ # 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. - -A forwarded launch returns at once, as acme's `B` does. `--wait` (`-w`) -returns only once the pane the file landed in -- the one already showing -it, if any -- is deleted (exit 0), or the session goes away (exit 1), as -acme's `E` does: what an `$EDITOR` must do, since fish's Ctrl-O -(`edit_command_buffer`), `git commit` and `crontab -e` read the file back -when the editor exits. Set `EDITOR='pardes --wait'`; `GIT_EDITOR` follows -`EDITOR` when unset. Outside pardes `--wait` changes nothing, a session -blocking anyway. - -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`. Mount dials the peer when it mounts it and fails its write -if nothing answers, `Mount peer /tmp/s: dial failed: no answer` (or `timed -out`, `hung up`), mounting nothing; a peer that goes away later is found out -by the next use, as `look: /n/peer/f: dial failed: no answer`. 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 sixteen slots served by cloud9's `std.Io` -runner; QUIC has sixteen of its own on the editor's poll loop. A client -that finds every Unix/TCP slot taken gets an Rerror `too many connections` -to its Tversion, and the log an `err - 9p: too many connections` record. 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: +Every native session serves 9P2000 (not .u, not .L) as a control +filesystem, in acme's manner: panes, columns, tags and the session are files. +This page is the reference for what each file does. The served +[`/README`](../src/fs-help.txt) is its one-screen summary. + +## Connecting + +The socket is `$XDG_RUNTIME_DIR/pardes-9p-.sock` (else under +`~/.local/state/pardes`), `` being the pid, the `--detach=NAME`, or +`--9p=`. A socket in the runtime directory is also posted in the 9P +registry as `$XDG_RUNTIME_DIR/9p/pardes/` (a symlink to the socket; +[cloud9.md](cloud9.md#the-posted-9p-registry)). + +Pane shells get `PARDES_PID` (the editor's pid), `PARDES_9P` (the socket) +and `PARDES_PANE` (their pane's serial). + +**Forwarding.** `pardes FILE` run in a pane (a live `PARDES_PID`, with +`PARDES_9P` and `PARDES_PANE`) writes FILE to that pane's `look` and returns +at once, as acme's `B` does. FILE must already exist; a name that does not +resolve, or a missing `PARDES_9P`/`PARDES_PANE`, starts a separate editor +instead. Bare `pardes` in a pane refuses and names `--nested`. + +`--wait` (`-w`) returns when the pane that shows FILE is deleted (exit 0) or +the session goes away (exit 1), as acme's `E` does. Use +`EDITOR='pardes --wait'` (`GIT_EDITOR` follows `EDITOR`), so fish's Ctrl-O, +`git commit` and `crontab -e` read the file after you close its pane. +`--nested` runs a separate session whose shells do not forward to it. + +**Clients.** ```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"' +9p -a "unix!$PARDES_9P" read index # plan9port, no mount +9ns --unix "$PARDES_9P" -- sh -c 'cat "$NINE_MOUNT/index"' # private mount +9ns --mntgen # the whole registry at /mnt/9p ``` -(Under `9ns --unix`, `$NINE_MOUNT` is that session's root itself.) - -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 `$NINE_MOUNT/pardes//` is that editor's tree -for any process, and `$NINE_MOUNT/pardes/NAME/` a `--detach=NAME` session's. -9ns exports `$NINE_MOUNT` to everything it starts, so a script checks that -variable to know the mount is there, and takes the name from `$PARDES_9P` -(`pardes-9p-.sock`). A new pane made through `pane/new` is a scratch named -`/+New` until it is given a name, where `` is the session's -directory (the one pardes started in), whichever pane last had the keyboard: -no pane asked for it, as acme's new window has acme's directory. So is a -`New` written to a column's `exec` or to `/tagexec`, a word in a tag no -pane owns. A column may hold no pane, as in acme: -`Newcol` makes one empty, and closing a column's last pane leaves it empty -with its tag holding the keyboard (`focus` reads empty) and logs only the -`del`. `pane/new` places its pane as acme's makenewwindow(nil) does: in the -active column, filling it when it is empty, else taking the bottom half of its -last pane ([where new panes go](tags.md#where-new-panes-go)). `Delcol` closes the column. Closing the -session's last pane quits pardes; see [tags](tags.md#empty-columns). - -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 +Under `9ns --unix` the session is `$NINE_MOUNT` itself and exists only inside +that command. Under `9ns --mntgen` every posted session is +`$NINE_MOUNT/pardes//`; take the name from `$PARDES_9P`. A dead +session's entry stays listed and answers `Input/output error`, so name the +session rather than globbing. `Tty9p` gives one pane's shell a kernel mount +at `$PARDES_MOUNT` ([v9fs.md](v9fs.md)). + +plan9port's `9p write` always opens with OTRUNC, so `echo x | 9p write +pane/3/body` replaces the whole body where acme would append. Append with +`>>` through a mount. + +For [Linux v9fs](https://www.kernel.org/doc/html/latest/filesystems/9p.html) +use `version=9p2000,cache=none,access=any`, `trans=unix` (or `trans=tcp` +with `port=`), `uname`, `dfltuid` and `dfltgid` for the local user, and an +empty `aname`. + +**Listeners.** `--9p-tcp='tcp!127.0.0.1!5640'` adds TCP; +`--9p-quic='quic!127.0.0.1!5641'` adds QUIC (build with `-Dquic=true`, +OpenSSL 3.6+; ALPN `pardes-9p`, an ephemeral TLS identity, no peer +verification). Addresses are numeric IPv4/IPv6; port 0 picks one; `/listeners` +reads them back. Every connection has full session access, `/os` included, +and TCP is unencrypted: use loopback. Unix and TCP share 16 connection slots; +a 17th client's Tversion gets `too many connections` (and the log +`err - 9p: too many connections (N turned away)`). QUIC has 16 of its own. +Plan9port and v9fs need a userspace bridge for QUIC. + +**Look paths and mounts.** Look resolves the OS filesystem first, then the +editor's own tree. Explicit paths skip that search: + +| Look path | Meaning | Served path | +|---|---|---| +| `/n/os/proc/self` | the host filesystem | `/os/proc/self` | +| `/n/self/pane/2/body`, `/virtual/pane/2/body` | this session's tree | `/pane/2/body` | +| `/virtual/src/pardes.zig` | sources embedded with `-Dembed-sources=true` | `/src/pardes.zig` | +| `/n/peer/pane/2/body` | a mounted session | the peer's `/pane/2/body` | + +`--mount=peer=work` or `Mount peer ` mounts a session name, an absolute +socket path, `unix!/path`, `tcp!IP!port` or `quic!IP!port`; `Unmount peer` +removes it. Mount dials at once and fails if nothing answers (`dial failed: +no answer`, `timed out`, `hung up`). There are eight named mounts; `os` and +`self` are reserved. Unmount refuses a mount a pane, a working directory or +a pending Save still uses. Mounts are dumped. + +A session may open its own tree through a mount (a Look at +`$m/pane/2/body` from the editor serving `$m`): requests are answered on the +connection's task while the editor waits in its syscall. Through QUIC that +still hangs. + +## The tree ``` -/README this guide, also src/fs-help.txt -/index one line per pane: serial, kind (text|term|pdf|image), dirty flag, name, column serial - (the name as the log shows it: one line of UTF-8, a newline in it `\n`, - a backslash `\\`, a byte not UTF-8 `\xNN`) -/status pid, version and pane count -/look write a line: a right click on it at the active pane; read: the serials the last - look, exec or ctl write touched (made, else acted at) -/exec write a line: a middle click; read the same serials -/log recent events, one a line: new|del|rename|save , msg , - dump|restore , err : ; - 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, - which makes its column the active one as a click there would -/ctl the settings, one a line as a write takes them; write a setting or a session builtin; - `size ` sets the screen of a session no frontend is attached to - (`--detach`, 160x50 until then; refused while a frontend owns the size), - from 20x6 to 4096x4096 (outside that, `invalid size`, EINVAL), and refused - when a column has not the rows for its panes' minima, each its tag and 2 - rows, the minimum placement keeps; so a size once taken is taken again, - and growing is never refused. A pane the resize took under its minimum - gets its rows back from its column's others; a terminal's pty follows its - pane on every resize -/commands every builtin: word, `arg` if it takes one, `root`, `pane` or `both` (the ctl that takes it: - Edit is both, at the active pane from the root; a pane's word such as Undo or Msg - is refused at the root), a setting's values, then ` -- ` and what it does -/recent the files opened lately, closed ones too, most recent first, a line each: - `open ` or `closed ` (read-only; `Recent` shows them in a pane, - a look at a row reopening the file at its last dot; kept across sessions - in $XDG_STATE_HOME/pardes/recent, 200 files, the oldest closed one dropped - first, never an open one; only files on disk, not a name never saved nor - /virtual/; a name escaped as /index's is; acme has none, its dump and - Load the nearest) -/layout one line per column (16 at most; the board 6; Newcol past that fails, - `no space for a column: 16 max`, ENOSPC; and each at least 10 cells - wide, so Newcol from a column under 20 fails `this one is too - narrow to split`, ENOSPC; the root's Newcol halves the active column, - so reaching 16 means running Newcol from the widest column's tag), - left to right: serial index x width current|notcurrent - (the column with the keyboard now) empty|full pane-serials...; then active - : acme's activecol, which the keyboard leaving for another column's - tag does not move, so the two can differ -- the active column, where - pane/new and a look place a pane next (- when there is none) -/tag the workspace tag; > replaces it, >> appends, one line; a control character - but a tab (a NUL too), DEL, a C1 control or bytes not UTF-8 are refused, `invalid - tag text: ...` (EINVAL), in any tag, a pane's or a column's too -/tagexec write a word: a middle click on it in the workspace tag; read as /exec. A - pane's word (Undo, Msg, Save, Edit too) is refused there and at a column's exec, - `not a session control message "Undo": write it to pane//ctl` (EINVAL), - never done at the pane with the keyboard; a tag's own words (New, Tty, - Find, Grep in a column's) run -/col//tag the tag of the column with serial n, the same way -/col//ctl write Delcol, Joincol, New or Tty: each acts on that column, as from its tag -/col//exec write a word: a middle click on it in that column's tag; read as /exec; rmdir col/ closes - an empty column (a column with panes is refused, ENOTEMPTY) -/pane/new open it to make a pane (the bottom half of the active column's last - pane, acme's coladd; with no room there, last all the same, taking half - the tallest pane's rows); the read answers that pane's serial. A session holds - 64 panes (16 on the board); at that, every route that would open one -- this - open, look, exec, New, Tty -- fails with `no space for a pane: 64 max` (ENOSPC through 9ns, which has - no word for ENFILE) and an err - record, and look reads back empty; a column with no room for one - (each pane keeps its tag and 2 rows) refuses it the same way, - `no space for a pane in that column` (docs/tags.md) -/pane// name body tag ctl addr dot limit data xdata sel dirty mark scroll - errors event look exec tagexec (a word as a click in its tag), plus - pty/{ctl,status,data} on terminals -/os/ the host filesystem -/src/ the editor's embedded sources, only when built with -Dembed-sources=true +/README the one-screen guide (src/fs-help.txt) +/index a line per pane: serial kind dirty name column +/status pid, version, panes +/look /exec write a line: a right / middle click at the active pane; read: the serials touched +/log the event log; write `follow` to wait for more +/screen the rendered screen as JSON +/listeners dial addresses +/focus the serial of the pane with the keyboard; write one to move it +/ctl settings and session builtins +/commands every builtin, one a line +/recent files opened lately: open|closed +/layout a line per column, then `active ` +/tag /tagexec the workspace tag, and a word clicked in it +/col// tag ctl exec of column ; rmdir closes an empty one +/pane/new open it to make a pane; read answers the serial +/pane// name body tag ctl addr dot limit data xdata sel dirty mark scroll + errors event look exec tagexec, and pty/{ctl,status,data,run} on terminals; + rmdir closes the pane +/os/ the host filesystem +/src/ the editor's sources (only with -Dembed-sources=true) ``` -`/layout`, `/tag` and `/col` go past acme, which serves no column files -- -its columns are only where a window sits. They are here so a script can see -where the panes are (`/index`'s last word is each one's column serial) and -edit the tags a person clicks in: a column tag is one line, a newline -written into it a space, and a truncating write (`echo Make > col/3/tag`) -clears it and drops the newline that ends it, as a pane tag's does. A -column is named by its serial, as a pane is: it stays while the column -lives, whatever opens or closes beside it, and is never reused; /layout -gives each column's serial and its index left to right, and the log says -`newcol ` and `delcol ` as columns come and go, and after a -Restore `restoredcol ` for each column as `restored` does for -panes. Joincol folds a column into the one on its right, which keeps its own -serial and tag; the joined column's panes go below that column's own, in -their order, and its serial is gone (`delcol`). The root ctl -takes no column word: its `Delcol` is refused, pointing at -`col//ctl`. - -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`; `Exit`, -which quits the editor as acme's does (it refuses once over the panes with -unsaved text, a `+New` scratch of 100 bytes or more too, whether it came -through a `ctl` write or a click: first one `/log` record per pane, -`unsaved `, then the write fails with one line that is -never a list cut short, `: Modified (Exit again to discard)` for one -pane, `4 unsaved panes: Modified (Exit again to discard)` for more, and -the `err` record says the same. On screen the notice is short, `3 unsaved -panes — Exit again to discard`, and the whole list stays in one `+Unsaved` -pane, filled again by each refusal: a row a pane, `: Modified` (a -`+New` scratch with its serial, as scratches share a name, `/dir/+New -(pane 12): Modified`), then `Exit again to discard`. Restore, -Del and Delcol refuse the same way, with their own word; an `Exit` after more editing refuses -again naming only the panes edited since the last refusal, as acme's does, -and an `Exit` with nothing edited since quits, throwing all of it away; a scratch or a -command's output under 100 bytes is not asked about, as acme's winclean -asks about no small unnamed window (so a scratch's `dirty` of 1 in `/index` -blocks nothing until it holds 100 bytes: it has no file to be out of step -with, and a few lines typed to try something are not work to lose); `Restore`, which replaces every pane, -asks the same first -- `Dump` writes `pardes--