From 9070942b29bd10dddcdecdb0e88ba0fb40608467 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Mon, 21 Sep 2026 20:07:43 -0300 Subject: Plan 9 idiom for the control filesystem, and the regressions a624a56 left The 9P tree stops being a command language wearing a filesystem. /new created a pane as a side effect of a *read*; it is now Tcreate in /pane, with Tremove to close, which cloud9's engine has always supported and the editor never declared: tree.zig now says `features = .{ .create = true, .remove = true }`. Eleven pane ctl verbs become files that can be read as well as written -- dot, limit, dirty, mark, scroll, look, exec -- leaving ctl with `get`, the one verb no file would say better. Root /ctl splits into a read-only /status and the /look and /exec files whose write IS the click. stat carries real sizes where it used to answer 0, and qid versions track a pane's revision, so a client can poll for change without re-reading the body. Commit a624a56 moved raw-tty keys to an early-return branch that knew only Ctrl-B and bare Escape, and in the same edit deleted the paste branch below it. That cost Shift-Escape (the unconditional way out of tty mode) and both paste chords: Ctrl-V and Ctrl-Shift-V reached the child as keystrokes, so an agent CLI running in a pane took Ctrl-V for its image-paste binding and answered "No image found in clipboard". Both are restored, with tests. Nested detection was not subtly broken but deleted: 60367d8 removed nested.zig's process-ancestry walk and left "am I inside pardes" derived from PARDES_FORWARD_LOOK, which read "0" both for --nested and for "the listener did not come up". PARDES_PID now answers that question on its own, checked with kill(pid, 0); PARDES_9P and PARDES_PANE answer how to reach it; the flag is gone. The posted-9P registry also self-heals now -- a session that aborts cannot unlink its own socket, so posting sweeps entries whose target refuses a connection, symlinks only and on a definite ECONNREFUSED only. Elsewhere: tty scrolling is sticky-bottom, following new output only from the last row, with typing and entering raw mode snapping back to live; the boot layouts are a Boot enum instead of a chain of ifs, and the bare tty startup (Boot.tty, which main.zig names) opens an empty text pane under the shell while tests keep Boot.tty_shell; builtins announce themselves on the message row under a Verbose setting that is on by default; Config prints each setting the way you would type it back, so WindowOpacity 70 rather than "WindowOpacity: 70%"; LocationsConfig opens its window only when called bare; every tagline puts the word that closes the thing last, and a column now outlives its panes -- closing the last one leaves an empty pane, and only Delcol, newly on the column tagline, takes the column away. Co-Authored-By: Claude Opus 5 (1M context) --- docs/cloud9.md | 11 ++++++ docs/config.md | 18 +++++++-- docs/design.typ | 31 +++++++++------ docs/fs.md | 120 ++++++++++++++++++++++++++++++++++++-------------------- docs/v9fs.md | 2 +- 5 files changed, 122 insertions(+), 60 deletions(-) (limited to 'docs') diff --git a/docs/cloud9.md b/docs/cloud9.md index 79fda717..d030db1d 100644 --- a/docs/cloud9.md +++ b/docs/cloud9.md @@ -54,6 +54,17 @@ way a private `ZMX_DIR` does for zmx. Stopping unposts, and only while the entry is still ours, so a name another editor has since claimed is never unlinked. +An exit that cannot run any code of its own — an aborted test, a kill, a +crash — leaves its entry and its socket behind, so posting first sweeps the +group: every entry that is a symlink and whose socket answers a connect with +a definite ECONNREFUSED is unlinked, along with the socket it points at when +a `stat` agrees that is a socket of ours. Anything that is not a symlink is +somebody else's, and any other answer — connected, busy, refused permission, +a surprise — counts as live, because uncertainty belongs to the server that +owns the socket rather than to the sweeper. That is `cloud9.post.Probe`'s +classification, repeated in `src/9p_io.zig` only because `post.probe` is raw +Linux syscalls and pardes also builds for darwin. + **Consuming.** Nothing. `9ns --mntgen` mounts the whole registry at `/mnt/9p`, and an interactive fish already self-wraps in one, so a pardes started from a terminal sees every posted service as ordinary files — diff --git a/docs/config.md b/docs/config.md index a55aef1c..04b0fd82 100644 --- a/docs/config.md +++ b/docs/config.md @@ -111,6 +111,10 @@ Workspace, column and pane command text can be edited directly; see `ColumnTags` toggles the column command row in both GUI and TTY; it is on by default. Add `ColumnTags` to startup configuration to reclaim that row on a compact screen. Hiding it preserves your custom column commands. +`Verbose` toggles the message-row announcement every builtin makes of its own +name before it runs; it is on by default, and the builtins that own the message +row themselves (`Msg`) never announce. Turning it off leaves the row to the +messages a builtin chooses to write. `SyntaxBold` toggles bold syntax keywords; it is off by default. These settings are shared by GUI and TTY, and `Config` reports their current states. Like `Colors` and `Wrap`, these commands take no argument and invert @@ -334,10 +338,16 @@ The installed `pardes-v9fs` helper lives beside the editor; development builds can set `PARDES_V9FS_HELPER` to its absolute path. See [v9fs.md](v9fs.md). Ctrl-B switches between raw TTY and editor mode. Plain Escape at a detected -shell prompt hops back to the previous pane. Other keys, including Ctrl-O, Ctrl-W, -paste shortcuts, and modified Escape belong -to the child. Use `Mode` in the pane tag to return to editor -mode in place. Desktop paste events still feed the terminal. +shell prompt hops back to the previous pane. Other keys, including Ctrl-O, +Ctrl-W and modified Escape, belong to the child. Use `Mode` in the pane tag to +return to editor mode in place. Desktop paste events still feed the terminal. + +The paste chords are the exception the window keeps. Ctrl-V types the yank +register at the program, and Ctrl-Shift-V asks the desktop for its clipboard +and types that; both go through the program's bracketed paste when it has +asked for one. Neither reaches the child as a keystroke, so an application +that would otherwise answer Ctrl-V by reading the system clipboard itself +never gets the chance to read the wrong thing. `Font` and `FontSel` exist ONLY in the SDL GUI and native macOS builds — a terminal's font belongs to its emulator and a browser's to the page — so a diff --git a/docs/design.typ b/docs/design.typ index 4c8956ba..f8f0b05c 100644 --- a/docs/design.typ +++ b/docs/design.typ @@ -1492,15 +1492,20 @@ same thread as editing. == Nested Look -Pane shells inherit `PARDES_9P`, `PARDES_PANE` (the pane serial), and -`PARDES_FORWARD_LOOK`. A child launch resolves its OS-relative argument in -the child's working directory, then writes `look ` to the parent's -`self/pane//ctl`. Explicit `/virtual` and `/n` paths resolve in the -parent. The ordinary filesystem update performs layout and drains host effects. - -`--nested` starts a separate editor and disables forwarding from its direct -pane shells. Its 9P socket remains available for control and plugins. There is -no executable-name discovery or separate Look listener. +Pane shells inherit `PARDES_PID` (the editor's process id), `PARDES_9P` and +`PARDES_PANE` (the pane serial). The first answers whether the shell is inside +a pardes at all, the other two answer how to reach it, and they are separate +because a session whose listener never came up still owns its children. A child +launch that finds a live `PARDES_PID` and no way to reach it says so instead of +starting a second editor. Otherwise it resolves its OS-relative argument in the +child's working directory, then writes `look ` to the parent's +`pane//ctl`. Explicit `/virtual` and `/n` paths resolve in the parent. +The ordinary filesystem update performs layout and drains host effects. + +`--nested` starts a separate editor and withholds `PARDES_PID` from its direct +pane shells, which is the whole of the opt-out. Its 9P socket remains available +for control and plugins, so those shells still get `PARDES_9P` and +`PARDES_PANE`. There is no executable-name discovery or separate Look listener. Detached frontends use `pardes-detached-.sock` in the same runtime directory. The shared Unix socket conventions live in `src/9p_io.zig`. @@ -1928,9 +1933,11 @@ arrives bracketed, as one `paste` event. staged edits: Enter commits a new buffer save target, Escape cancels, and no disk rename or write happens until explicit Save. Terminal cwd and image/PDF status stay generated. Save leads the tail of every pane holding text of its own: -`Save Tty Del Collapse` for a file or an output buffer, -`Save Tty Del Togglettymode Filter Collapse` for a terminal, and `Tty Del Collapse` -for an image. PDFs use `Tty Del PdfSections PdfTint Collapse`, without tint status text. +`Save Tty Collapse Del` for a file or an output buffer, +`Tty Save Mode Filter Collapse Del` for a terminal, and `Tty Collapse Del` +for an image. PDFs use `Tty PdfSections PdfTint Collapse Del`, without tint status text. +The word that closes the thing is last on every tagline, so overshooting the +click before it cannot destroy anything. `Collapse` toggles a pane between its tagline alone and its expanded height; hidden body contents and running terminals are retained. An unsaved file has `*` after its name; an image tag diff --git a/docs/fs.md b/docs/fs.md index 55ffdc9c..0bcc0313 100644 --- a/docs/fs.md +++ b/docs/fs.md @@ -1,14 +1,19 @@ # Filesystem Every native session serves 9P2000 on a Unix socket. Pane shells receive -`PARDES_9P` (socket path) and `PARDES_PANE` (pane serial). The socket is +`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. -`--nested` opens a separate editor and disables forwarding from its pane shells; -its 9P service stays available. +`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: @@ -68,53 +73,79 @@ Existing Plan9port/v9fs clients need a userspace bridge for QUIC. ``` /README this guide, also src/fs-help.txt /index one line per pane: serial, kind (text|term|pdf|image), dirty flag, name -/ctl write: one command per line; read: the serials the last command made or touched -/new reading it creates one empty pane and answers "\n" (the /net/tcp/clone idiom) +/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// name body tag ctl addr data xdata sel errors event, plus pty/{ctl,status,data} +/pane/ create a directory here to open a pane; remove one to close it +/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 ``` -Nothing in the tree is created by list, stat, walk or read, except that -reading `/new` makes a pane; that is its whole purpose, so a recursive read -of the tree makes one pane per open of it. - -`/ctl` and `/pane//ctl` take the editor's own command language, two verbs: - -- `look TEXT` is a right click on `TEXT`: a path opens a file, `file:12` jumps - to a line, a directory opens a shell there, a URL opens in the browser. From - `/ctl` the look happens at the active pane; from a pane's `ctl` at that pane. -- `exec TEXT` 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. - -Both verbs are lowercase; the words after `exec` are the editor's capitalized -commands. A pane's `ctl` also keeps acme's addr verbs (`addr=dot`, `dot=addr`, -`limit=addr`, `clean`, `dirty`, `cleartag`, `get`, `mark`, `nomark`, -`noscroll`, `scroll`, `show`). Every line of a write is checked before any -line runs; a malformed line fails the write with EINVAL. A command that fails -inside the editor is reported on the message row, not as a write error. - -After a write to either `ctl`, reading `/ctl` answers the serials of the panes -the 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. Before any command it answers `pid`, `version` and `panes` lines. +Nothing in the tree is created by list, stat, walk or read. A pane is opened +by Tcreate in `/pane` (`mkdir`) and closed by Tremove on `/pane/` (`rmdir`), +which is the only create and the only remove the tree serves. The name a +create asks for is ignored, since a pane is named by the serial the editor +gives it: the Rcreate qid names the new directory, and because `/index` is +ordered by serial its last line is the pane just made. + +`/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. `sel` reads the editor selection and writing it replaces the -selection. `body` appends on write and replaces on truncating open. `addr` -selects a range and `data` or `xdata` read or replace it. `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. - -Stats report real lengths for `index`, `ctl`, `name`, `body`, `tag` and `sel`, -modes 0644/0666 (0444 for read-only files, 0222 for write-only), the pane's -last edit time or the process start as mtime, and stable qids. Directory -entries carry no sizes; stat the entry. +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. + +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 @@ -132,8 +163,10 @@ 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; protocol create, remove, rename, and other metadata -changes are refused. Ownership and permissions under `/os` are synthetic. +and truncation to zero; under `/os` protocol create, remove, rename and other +metadata changes are refused, as is every create and remove in the control +tree but the pane directories. 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. @@ -148,7 +181,8 @@ for Unix and TCP, a poll loop for QUIC, and the 9P client for mounts); `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 and that `new`, `ctl`, `name`, `sel` and `log` behave. `zig build +nothing, that a pane create and 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. diff --git a/docs/v9fs.md b/docs/v9fs.md index 6cb4e998..8cc97ff9 100644 --- a/docs/v9fs.md +++ b/docs/v9fs.md @@ -12,7 +12,7 @@ ls "$PARDES_MOUNT/pane" cat "$PARDES_MOUNT/index" cat "$PARDES_MOUNT/README" cat "$PARDES_MOUNT/pane/$PARDES_PANE/body" -echo 'exec Msg hello' > "$PARDES_MOUNT/ctl" +echo 'Msg hello' > "$PARDES_MOUNT/exec" ``` The mount belongs to that pane's subprocess tree. Other panes and the editor -- cgit v1.3