diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-29 19:25:00 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:17 -0300 |
| commit | 57b30ba3e38153a4446626449b0fed5120da954c (patch) | |
| tree | 7b9381327a05791181a855bd8d4ba3cfb4df5301 /docs/fs.md | |
| parent | 0fd908eea63d04886b269438aa7529d3dd422256 (diff) | |
| download | pardes-57b30ba3e38153a4446626449b0fed5120da954c.tar.gz pardes-57b30ba3e38153a4446626449b0fed5120da954c.zip | |
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 <[email protected]>
Diffstat (limited to 'docs/fs.md')
| -rw-r--r-- | docs/fs.md | 1689 |
1 files changed, 603 insertions, 1086 deletions
@@ -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-<pid>.sock`, or lives under -`~/.local/state/pardes` when XDG_RUNTIME_DIR is unset. Detached sessions use -their session name; `--9p=<name>` overrides it. +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. -A `pardes <file>` 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. +## Connecting -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. +The socket is `$XDG_RUNTIME_DIR/pardes-9p-<name>.sock` (else under +`~/.local/state/pardes`), `<name>` being the pid, the `--detach=NAME`, or +`--9p=<name>`. A socket in the runtime directory is also posted in the 9P +registry as `$XDG_RUNTIME_DIR/9p/pardes/<name>` (a symlink to the socket; +[cloud9.md](cloud9.md#the-posted-9p-registry)). -Look resolves the OS filesystem first, then the editor's virtual filesystem. -Explicit paths bypass that search: +Pane shells get `PARDES_PID` (the editor's pid), `PARDES_9P` (the socket) +and `PARDES_PANE` (their pane's serial). -| 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` | +**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`. -The mount name `self` is reserved and maps to the server root, so `/n/self/X` -and `/virtual/X` both name the served `/X`. +`--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. -`--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. +**Clients.** -`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. +```sh +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 +``` -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. +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/<pid or NAME>/`; 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 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: +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. -```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`, `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: -(Under `9ns --unix`, `$NINE_MOUNT` is that session's root itself.) +| 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` | -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/<pid>/` 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-<pid or NAME>.sock`). A new pane made through `pane/new` is a scratch named -`<dir>/+New` until it is given a name, where `<dir>` 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). +`--mount=peer=work` or `Mount peer <dial>` 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. -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. +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 served tree +## 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 <serial> <name>, msg <serial|-> <text>, - dump|restore <path>, err <serial|-> <file>: <why>; - 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 <cols> <rows>` 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 <path>` or `closed <path>` (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 - <serial>: 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/<n>/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/<n>/tag the tag of the column with serial n, the same way -/col/<n>/ctl write Delcol, Joincol, New or Tty: each acts on that column, as from its tag -/col/<n>/exec write a word: a middle click on it in that column's tag; read as /exec; rmdir col/<n> 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/<n>/ 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 <path> +/layout a line per column, then `active <serial>` +/tag /tagexec the workspace tag, and a word clicked in it +/col/<n>/ tag ctl exec of column <n>; rmdir closes an empty one +/pane/new open it to make a pane; read answers the serial +/pane/<n>/ 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 <serial>` and `delcol <serial>` as columns come and go, and after a -Restore `restoredcol <old> <new>` 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/<serial>/ctl`. +Panes and columns are named by serials the editor gives: stable while they +live, never reused. Nothing is created by a listing, stat, walk or read: +only an open of `/pane/new` makes a pane (so `ls -l` and `find` are safe), +only `rmdir` of `/pane/<n>` or an empty `/col/<n>` removes. Tcreate is +refused everywhere. -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 <serial> <name>`, then the write fails with one line that is -never a list cut short, `<name>: 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, `<name>: 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-<date>-<time>.zon` (UTC) in -`DumpDir` (default `$XDG_DATA_HOME/pardes`, else `~/.local/share/pardes`) -and logs `dump <path>`, and `Restore` with no path takes the last one; a -Restore puts a new editor under every client, so the write of it is -answered and then every connection is hung up, their fids naming the old -editor's panes (a 9ns older than cloud9 2a7137c could fail the write with -ECONNRESET all the same, when another request's send met the hang-up -before the answer was read; the log is the authority): dial again -- a 9ns -mount is one such connection, so after a Restore stop it and start 9ns -again, or every file under it fails -- and the new -log names the restored panes and -`restore <path>`. Undo history does not survive a Restore: a restored pane -starts with none. Restored panes have new serials (`restored <old> -<new>` maps them) and so may columns (`restoredcol`; a fresh editor counts -column serials from 1 again, so they often come back the same). A command -pane comes back showing what it showed, its tag saying how it ended, -`exit 0` (`exit ?` for one still running when dumped, whose end nobody -saw): its command is not run again. REPL bindings are not dumped. The answer has 200 ms to leave before the cut, so a slow -client may see only the cut; the log's `restore <path>` is what says the -Restore happened. Keeping connections across it would mean carrying serials -and opens into the new editor, which acme, whose Load only adds windows, -never needed); and `Kill`, which -does not quit but stops commands, as acme's does: bare, every command pardes -started, and `Kill make ls`, those whose line begins with one of the words. A -command pardes started is a command pane's, until its child exits -- Kill -sends SIGTERM to its running job, to its shell and to every `&` job the -line started, so the whole command stops and leaves nothing running (of -`sleep 30; echo done` the `echo` never runs; an `&` job outlives only a -command that exits on its own) -- or a line it typed into -a terminal (a -word written to `exec`, a middle click on one, a `pty/run`), from its shell's -start mark (C) to its end mark (D), where Kill sends its foreground job SIGTERM -- acme posts the -"kill" note, which ends a process -- and never signals the shell itself; -in a shell running without job control (`set +m`) the job shares the -shell's group, so there is none to signal: Kill says `Kill: no job to -signal`, and a write of it to `ctl` fails with that; with nothing running -it says `Kill: nothing running`; and, of a typed line, only the -foreground job, after which what the rest of the line does is the shell's -affair: of `sleep 30; echo done` typed at a prompt, bash (saying -`Terminated`) and fish (saying `Job 1, 'sleep 30' terminated by signal -SIGTERM`) both go on and run the `echo`: SIGTERM, unlike an interrupt, -ends only the job, not the line -- -and reads every setting there is, one a line, in the words a write of it -takes (`Verbose on`, `WindowOpacity 70`, `PanelSlide off`, `DumpDir -<the directory in effect>`, `LocationsConfig ...`), so writing what it reads -back changes nothing; a setting the frontend cannot show (`Lift`, -`GripWidth` on a terminal) is refused as `Lift is GUI-only, invalid here` -(EINVAL: a request this build cannot take); acme's own words run as -pardes's where it has one -- `Put` is `Save`, `Delete` a `Del` that does -not ask -- and the rest (`Get`, `Putall`, `Snarf`, `Cut`, `Paste`, `Zerox`, -`Sort`, `Load`, `ID`, `Send`) are refused, EINVAL, `invalid: acme's Snarf -is not a pardes builtin`, never run as shell commands; and so is a builtin only the -GUI has (`Fonts`), written to an `exec` too, where it would otherwise run as -a shell command; a `Shell` or `Tty` naming no -shell says `shell "x" not found` (ENOENT); a `DumpDir` whose last -directory is missing has it made at the Dump, and one further up missing -says `Dump <path>: no such directory` (ENOENT), one that is no directory -(`/dev/null`) `Dump /dev/null/pardes-<time>.zon: /dev/null is not a -directory`, each naming the dump file it would have written; 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`, or `Del k`/`Del j` to give its rows to the pane above or -below, `Save f`, `Collapse`, which folds that pane, `Undo` and -`Redo`, which step its body through its last 256 edits -- with none left -they say `Undo: nothing to undo` and the write still succeeds, as acme's -Undo is silent --, `Find pat`) -beside acme's `get`, `lock` and `unlock`; acme's other ctl words are taken -too, done by the file or builtin that replaces each: `name x` (the `name` -file), `put` (`Save`), `clean` and `dirty` (`dirty`), `del` (`Del`), -`delete` (a `Del` that does not ask), `dot=addr`, `addr=dot`, -`limit=addr`, `mark`, `nomark` (`mark`), `show` and `cleartag`; `dump`, -`dumpdir`, `font`, `menu` and `nomenu` are refused with why, EINVAL. The column words are pane words -too, acting on the column that pane is in: `Delcol`, `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, and `Joincol` needs a column to its right (`Joincol: no column to -the right`). 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. +`/index` rows are `serial kind dirty name column`, kind `text`, `term`, +`pdf` or `image`, the name `<dir>/+New` for an unnamed scratch. Names may +hold blanks, so split `head, col = row.rsplit(maxsplit=1)`, then +`serial, kind, dirty, name = head.split(maxsplit=3)`. Names in `/index`, +the log and a terminal's tag are escaped: a newline `\n`, a backslash `\\`, a byte that +is not UTF-8 `\xNN`. -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 -(for `Theme`, whose names are any case, naming every theme that shares -the name's first letter when they fit the 128 bytes a 9P error carries, -else the nearest few, since all of them, `Themes`'s list, are too many); -and `not a session control message "X": write it to pane/<n>/ctl` or `not -a window control message "X": write it to /ctl` 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, in its own words, which -name what failed, the line not quoted after them (only a line refused -before it runs, as no message at all, is quoted), e.g. `Mount: already -mounted` (EIO), and `control message needs its -argument "Save"` (EINVAL), for a builtin that would have asked at a prompt -(a `Save` on a scratch) rather than open one nobody is there to answer. A -builtin that means nothing without its argument (`Mount`, `Msg`, `Find`), -written bare to an `exec` or `/tagexec`, is refused the same way, `wrong -#args in control message "Mount"` (EINVAL), before it runs. 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 -- fails the write too, once the editor has tried -(`Save /root/x.txt: access denied`, which 9ns reads as EACCES, a shell's -`Permission denied`), and changes nothing: a scratch -keeps its name and stays a scratch, a clean file stays clean, a dirty one -dirty. 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. +## Rules for every file -`/commands` lists every builtin the registry holds, in registry order, one -a line: its word, `arg` when it takes one, `root`, `pane` or `both` for the -ctl that takes it, for a setting that chooses among words those words, -comma-joined, then ` -- ` and one sentence of what it does (a builtin's doc -comment's first sentence, a setting's doc), e.g. +**Failure.** A write fails whenever what it asked for fails, and logs one +`err <serial|-> <file>: <why>` record, with no `msg`. Only writes log: a +refused open or truncation (an OTRUNC open such as `> data` after a failed +`addr`), create or remove answers its error alone, as do a write to +`pane/new` (`permission denied`) and a write on a read-only fid (`bad use +of fid`). Errors are words (Plan 9's where pardes has none of its own), +never a C library string. Through 9ns the kernel sees an errno 9ns reads +from those words (cloud9 `9ns/src/nine.zig`, `enameToErrno`): `control +message`, `invalid` or `bad ` is EINVAL (malformed input); `no such`, `not +found` ENOENT; `in use` EBUSY; `no space` ENOSPC; `denied` EACCES; anything +else, such as `no match for regexp`, `address out of range`, `Modified` or +`tag: over 4096 bytes`, EIO. The `err` record has the words; a shell sees +only the errno, most often `Invalid argument` or `Input/output error`. -``` -Newcol root -- An empty column right of the keyboard's, its tag taking the keyboard. -Save arg pane -- Write the pane's text to its file, or to the file its argument names. -Verbose arg root on,off -- A builtin says its own name on the message row as it runs, on or off. -Placement arg root acme,pardes -- Where a new pane goes: acme, as makenewwindow does, or pardes, the older rules. -``` +Not failures: a look that finds nothing (it answers nothing and logs one +`err`), an Edit `x` that matches nothing, `Undo` with nothing to undo (a +`msg`), and a command pane's command, which ends in its own time with an +`exit` record. + +**Command lines.** `look`, `exec`, `tagexec`, the three kinds of `ctl` and a +column's `exec` take one command a line. The whole write is checked first +(a control character other than a tab fails it all, EINVAL); then lines run +in order, and a failing line fails the write after the lines before it took +effect, as acme's ctl does. Blank lines are skipped. A mount cuts a big +write into pieces of at most one message (msize 8192, less the header), and +each line runs once it is whole. A write that does not fill its message is +whole, so its last line runs even without a newline (`printf Save > exec`). An `Edit` whose `{` or +`a`/`c`/`i` text is still open waits for the next write on that open, and +fails at the close if it never ends (``unmatched `{'``). A line held past +1 MiB is refused. + +**Answers.** Reading `look`, `exec` or `tagexec` answers the serials the +last command made, or else the pane it acted on or focused, one a line. An +open that wrote reads its own last answer; an open that never wrote reads +the session's last. A read is a stream: once read, the next read on that +fid is EOF until the next write (or seek to 0). With other clients about, +write and read on one open: +`exec 3<>$m/look; echo x >&3; cat <&3; exec 3<&-`. + +**Snapshots.** `/index`, `/layout`, `/recent`, `/commands`, `/status`, +`/listeners`, a read-only `ctl`, `/log`, `/screen` and a terminal's `body` +freeze at the open, so one read in several chunks never splices two moments; +open again for now. Such an open, or a `run`, `event` or `pty/data` open, +takes one of 64 open records; past that the open fails `too many open +files`. + +**Held reads.** A read with nothing to give yet (a followed `log`, `event`, +`pty/data`, `pty/run` before its answer) waits in the editor and is answered +when news comes. A second read on that open meanwhile fails `file in use`. +A read the client flushed is dropped. Through a FUSE mount bash's `read -t` +cannot time out: wrap the loop in `timeout N`. + +**Stats.** Lengths are real (for `event` and `pty/data` the next record's, +zero when none waits; for `log` what an open would freeze). The qid +version of `body`, `data` and `xdata` is the pane's revision, so a stat sees +an edit land. Modes are 0644/0666, 0444 read-only, 0222 write-only. + +## look and exec + +A line written to `look` is a right click on it: + +- a path opens the file (the pane already showing it, if any); `file:12` + selects line 12, newline included; `file:12:5` puts the caret at line 12, + byte column 5; `file:<addr>` takes any address (below), **evaluated from + the file's dot**: `file:/re/` finds the next match after the selection, + `file:0/re/` the first. `:addr` addresses the pane itself. +- `@p<serial>:<addr>` addresses a pane by serial, a terminal's logical lines + too. +- a directory types `ls` into a terminal idle there, else opens one there. +- a URL opens in the browser. +- a plain word selects its next place after dot, wrapping (`LookWord list` + on the root ctl lists every place in a `+Search` pane instead). In a + terminal a word is always listed, rows spelled `@p3:12:5-9`. + +A miss logs `err <serial> look: ...` (`no match for "zzq:#3"` quoting what +was written when nothing by that name exists; `<path>: no match for regexp` +or `address out of range` when the address fails), opens nothing, and leaves +`look` reading empty; the write succeeds. + +A line written to `exec` is a middle click: + +- a builtin word runs (`/commands` lists them): `Save`, `Del`, `New`, `Tty + [shell]`, `Msg text`, `Find`, `Grep`, `Edit ...`, `Mount`, ... + A builtin that needs its argument (`Msg`, `Mount`, `Find`) written bare is + `wrong #args in control message "Msg"`. +- acme's words run as pardes's where it has one (`Put` is `Save`, `Delete` + a `Del` that does not ask); the rest (`Get`, `Putall`, `Snarf`, `Cut`, + `Paste`, `Zerox`, `Sort`, `Load`, `ID`, `Send`) are refused, `invalid: + acme's Get is not a pardes builtin: ...`, never run as commands. So is a + GUI-only builtin (`Fonts`) on another frontend. +- anything else is a command line, at most 1024 bytes. At a terminal idle + at an empty prompt it is typed into that shell. Anywhere else it runs as + a **command pane**: a terminal running the root ctl's `Shell` (`$SHELL`, + else `/bin/sh`) with `-c` and the line, in the pane's directory, with job + control on. Its tag reads `<dir> (<line>) running`, then `exit N`; the log + says `run <serial> <line>` and `exit <serial> <N|?>`; `exec` reads back its + serial. A typo ends `exit 127`. The directory's next command reuses a + finished command pane, below what it showed; one still running gets a + second pane. From a pane whose directory is gone nothing runs: `exec: + <dir>: no such directory` (ENOENT). + +The root's `look` and `exec` act at the active pane and log as that pane's; +`/pane/<n>/look` and `exec` at pane n; `/tagexec` and `/col/<n>/exec` +click in the workspace's or that column's tag, run commands in the session's +directory, and log as `-`. A pane's word (`Undo`, `Msg`, `Save`) is refused +at `/tagexec` and a column's exec: `not a session control message "Undo": +write it to pane/<n>/ctl`. + +A background job (`&`) outlives a command that exits on its own; its output +goes on below `exit N` until it lets go of the pty. `Kill` (root ctl) +stops the commands pardes started: bare, all; `Kill make ls`, those whose +line starts with one of the words. For a command pane it signals the whole +line, `&` jobs included; for a line typed into a shell only the foreground +job (SIGTERM), and the shell decides the rest. With nothing running it says +`Kill: nothing running`. Kill does not reach a REPL's code: use `sig INT` on +its `pty/ctl`. + +## The root ctl + +Reading `/ctl` gives every setting, one a line, in the words a write takes +(`Theme orchard`, `Verbose on`, `Placement acme`, `DumpDir <dir>`, +`Shell /bin/bash`, ...), so writing back what it reads changes nothing. +Writes take settings and session builtins (`scope = .session` in +`src/builtins.zig`), acting at the pane with the keyboard: + +- A setting written bare steps to its next value (a switch flips; so do + `Placement`, `BootShell`, `Crt`). A value it does not take is `bad value in + control message; ...` naming what it takes; `/commands` lists them. A + setting the frontend cannot show is refused (`Lift is GUI-only, invalid + here`). +- `Newcol` makes an empty column right of the keyboard's, halving the active + column. `Joincol` folds the keyboard's column into the one on its right + (its panes go below that column's), `Joincol: no column to the right`. +- `Exit` quits. It refuses once while panes hold unsaved text: one `unsaved + <serial> <name>` record per pane, then the write fails `<name>: Modified + (Exit again to discard)` or `4 unsaved panes: Modified (Exit again to + discard)` (EIO), and the list stays in a `+Unsaved` pane. The same word + again with nothing edited since discards; after more editing it refuses + again, naming only the panes edited since. `Restore`, `Del`, `Delcol` and + a pane's `get` refuse the same way with their own word. A `+New` scratch + under 100 bytes, or a command's output, is never asked about. +- `Dump` writes `pardes-<date>-<time>.zon` (UTC) in `DumpDir` + ([config.md](config.md#dumps)) and logs `dump <path>`. `Restore [path]` + replaces every pane (bare: the last dump this session wrote). The Restore + write is answered, then **every connection is hung up**: dial again, and + restart a 9ns mount. The new log has a `new` per pane, `restore <path>`, + then `restored <old> <new>` per pane and `restoredcol <old> <new>` per + column. Undo history and REPL bindings are not restored; a command pane + comes back showing how it ended (`exit ?` if it was running) and does not + run again. +- `Kill [word...]` (above), `Mount name dial`, `Unmount name`, `Theme x`, + `size <cols> <rows>`. +- `size C R` sizes a `--detach` session no frontend is attached to (160x50 + until then): from 20x6 to 4096x4096, else `invalid size`; refused while a + frontend owns the size, and when a column would lose its panes' minimum + rows (`size: too small for the panes, each its tag and 2 rows`). + +A write is refused whole, before anything runs, in Plan 9's words: +`unknown control message "X"`, `wrong #args in control message "X"`, +`bad value in control message ...`, or a word of the other ctl: `not a +session control message "Undo": write it to pane/<n>/ctl`, `... "Delcol": +write it to col/<serial>/ctl`, `not a window control message "X": write it +to /ctl`. A builtin that would open a prompt (`Save` on a scratch) fails +`control message needs its argument "Save"`. A line that fails as it runs +fails with the editor's words (`Mount: already mounted`, `Save /root/x: +access denied`), changing nothing. + +## Columns and tags -Every builtin has its sentence; a test fails the build of one without. Such a setting written bare steps to its -next value, so a two-valued one flips (`Crt`, `Placement`, `BootShell`), as -its word clicked in a tag does; a value it does not take is refused with -`bad value in control message; takes ...` naming those it does. It is -generated from the registry, so it is always this build's own list. +`/layout` has a line per column, left to right: `serial index x width +current|notcurrent empty|full pane-serials...`, then `active <serial>` +(acme's activecol: where `pane/new` and a look put the next pane; `-` when +none). `current` is the column with the keyboard now, which may differ +from the active one. `/index`'s last field is each pane's column serial. -`/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 -pane`; anything but a number, with `ill-formed control message`. +A session holds 16 columns (`no space for a column: 16 max`, ENOSPC), each at +least 10 cells wide (`Newcol` from a column under 20 fails `this one is too +narrow to split`). Since root `Newcol` halves the active column, reach 16 by +writing `Newcol` to the widest column's `exec`. -A pane is made by **opening** `/pane/new`, and closed by Tremove on -`/pane/<n>` (`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. +`/col/<n>/ctl` takes `Delcol`, `Joincol`, `New` and `Tty` for that column; +`/col/<n>/exec` is a click in its tag; `rmdir /col/<n>` closes an empty column +(one with panes: ENOTEMPTY). A column may be empty, as in acme: `Newcol` +makes one, and closing its last pane leaves it (the keyboard goes to its +tag, `focus` reads empty, the log says only `del`). Closing the session's +last pane quits pardes. The log says `newcol <serial>` and `delcol <serial>`. -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. +`tag` files (`/tag`, `/col/<n>/tag`, `/pane/<n>/tag`) read the whole tag. +A pane's starts with its computed path (an image's with its mode words, a +PDF's with its page), then the editable text. `>` replaces the editable text +(the default words too: `echo Make > tag` leaves only `Make`) and drops the +one newline that ends it; `>>` appends, so `printf ' Make' >> tag` (the +blank matters; `echo` would start a second line). A pane tag may hold +several lines; a column or workspace tag is one, a newline written into it +becoming a space. Control characters other than tab, DEL, C1 controls and +non-UTF-8 bytes are refused (`invalid tag text`). The editable text is at +most 4096 bytes (`tag: over 4096 bytes`, EIO). A clear is an ordinary edit +and `u` in the tag undoes it. [tags.md](tags.md) covers tags on screen. -A session can open its own tree through a mount: a Look at -`/mnt/9p/pardes/<me>/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. +## Panes -`/look` and `/exec` are the editor's two clicks, one per line of a write: +**Making and closing.** Opening `/pane/new` makes a scratch `<dir>/+New` +(the session's directory), and reading that open answers its serial: +`n=$(cat $m/pane/new)`. Each open makes another pane; two reads of one open +answer the same serial. It goes where acme's makenewwindow puts a window +([tags.md](tags.md#where-new-panes-go)): in the active column, filling it +if empty, else the bottom half of its last pane. A session holds 64 panes +(`no space for a pane: 64 max`), and every pane keeps its tag and 2 rows +(`no space for a pane in that column: each keeps its tag and 2 rows`); both +are ENOSPC, for this open and for a look, exec, `New` or `Tty` alike. +`rmdir /pane/<n>` closes the pane, unsaved or not. -- a line written to `look` is a right click on it: a path opens a file, - `file:12` jumps to a line, a directory types `ls` into a terminal idle - at an empty prompt there, else opens a terminal there that runs - `ls` once (pardes has no directory listing pane of acme's kind; a shell - in it is where one goes on from a directory), a URL opens in the - browser, and a plain word, as acme's look3 does, selects its next - place in that pane after the dot, wrapping at the end, opening nothing - (`LookWord list` on the root ctl lists every place in a `+Search` pane - instead, as pardes did before; `LookWord search` is the default). In a - terminal, which has no dot to go on from, a plain word is always listed - in a `+Search` pane, its rows naming the terminal as - `@p<serial>:<line>:<cols>`, the columns a range, `@p3:12:5-9` (bytes 5 - through 9 of logical line 12, 1-based, both ends in), which a look of the - row selects, - and look reads that pane back. -- 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`, ...; - `Tty`'s argument is the shell it runs, `Tty fish`, and `Tty` on a pane's - ctl opens a new terminal pane, not in that one: in the active column, as - acme's makenewwindow puts a new window (util.c:456-467: the active column - first, then the pane's own), under the text with room or halving the - tallest, never shorter than its tag and 2 rows), - or anything else, a command line. Written at a terminal idle at an EMPTY - prompt it is typed into that shell -- any line that is no builtin, so - `Delcol x` at a terminal goes to the shell -- (and a terminal whose shell - exits, `exit` typed or run, closes its pane). Nothing is ever typed over - text someone typed at a prompt and did not send. From anywhere else -- a - file, a scratch, a tag, a terminal with a line typed at its prompt or - whose tty a program holds -- it runs as a command pane (from a pane whose - directory is not there it runs nothing and makes no pane, `exec: <dir>: - no such directory`, ENOENT, as `Tty` there does; one whose shell the host - cannot start ends at once, `exit <serial> 127` in the log, its tag no - longer running, nothing for Kill): a - terminal whose child is the root ctl's `Shell` ($SHELL, else /bin/sh, unless set) run - with `-c` and the line, in the pane's directory, with job control on - (bash, sh, dash, zsh, ksh `-m`; fish `status job-control full`), - which shows its output and then `exit N` (its tag reads `<dir> (<line>) - running`, then `exit N`), and stays. The command is over when its process - exits, as in acme, not when its terminal closes: a job it left in the - background prints on below `exit N` until it lets go of the pty, and a - command that lets go of its terminal early runs on to its own exit. A - background job outlives a command that exits on its own (Kill stops it too): job control gives it a process - group of its own, so the hangup the kernel sends the terminal's - foreground group when the shell exits misses it. It survives the pane - closing too, but its writes to the terminal then fail, so start one that - must keep writing with `nohup` or its output redirected. The directory's - next command runs in that pane once it is done and nothing holds its pty, below what it showed, after a `% <line>` line; - one still running gets a second pane. Before the next command the pane - leaves any alternate screen and turns off the modes a program left on - (mouse reports, bracketed paste, a hidden cursor); a command that clears - the screen and its scrollback (`clear`, ED3) erases the history above it. A command pane's own exec starts - the next command there too. The log says `run <serial> <line>` and `exit - <serial> <N|?>`; `exec` reads back the command pane's serial; Kill stops - its whole line, `&` jobs included; a command line is at most 1024 bytes, and a - longer one written to an exec fails the write (EINVAL, `invalid command - line: a command line is at most 1024 bytes`, and one with a control - character (DEL too) but a tab, `invalid command line: it holds a control - character (or DEL) other than a tab`; the root's exec logs either against the pane it would - have run at) before anything in it runs; a builtin's line (a long - `Msg`, an Edit block) may be longer. To run a command again, execute - its line again from its directory: `echo 'make test' > pane/<n>/exec` on - the command pane runs it there, below the last run. A misspelled word is a - command that says so and ends `exit 127`. `echo Tty > pane/<n>/ctl` makes - an interactive terminal in that pane's directory. +**`name`** reads the file name (a terminal's directory); a write renames the +buffer (relative to the pane's directory) and marks nothing dirty; `Save` +then writes under the new name. A name is one line; refused (EINVAL) are a +second line, a blank at either end, control bytes and non-UTF-8 (`bad +character in file name: a blank at its start`, ...). Up to 255 bytes a +component. -A terminal can be bound as a language's REPL: `Repl python` in its tag or -on its `ctl` (the language names are the syntax table's, or a code fence's -alias such as `py`, in any case; `Repl -` unbinds, -`Repl` bare says the binding, `Repl python` again changes nothing). Its tag -shows its id, `python-a`, `python-b` for the next, a freed letter reused, -and so does the end of its `ctl` line, after `current`/`notcurrent`. -A builtin's word still runs first, bound or not: `Del` clicked in the file -closes its pane. Then any other exec made by a gesture on the body of a file -in that language -- a -middle click, the execute key, on a selection or a single word, even `make` -in a comment -- or on the REPL's own body is typed into the REPL instead of -run: bracketed paste when its program asked for it (DECSET 2004), else line -by line, where a blank line inside a Python block ends the block (said -once), then Enter. The message row says `→ python-a` in the tag's name tint -and the log `send <from> <to> <id>`. With several REPLs bound for the -language the pane asks which, on its notice band, one key answering and Esc -sending nowhere; nothing is remembered. A REPL takes text only while its -program has the terminal: a command pane's until its command is done (the -binding goes with it, and a done one cannot be bound), an interactive -terminal's while a program other than its shell holds the tty -- after -Ctrl-D the shell would run the text, so nothing is sent and the pane says -so. Still commands, whatever is bound: a word in a tag (so -the tag is how to run `make` from that file), `Exec <text>` run by name -(typed, a 2-1 chord onto `Exec`, a `ctl` line) -- in a `.py` pane bound to -a REPL, `Exec print(1)` runs `print(1)` as a command, never in the REPL -- -and a command word @`cmd` in -the text, looked at or clicked (`# @`pytest -x`` in a script). A 9P `exec` -is no gesture and is never sent: a script writes to the REPL pane's -`pty/data`, multi-line code as a bracketed paste (`\e[200~<code>\e[201~`, -then `\r` in a write of its own once the REPL has echoed the paste -- -Python 3.13's REPL takes an Enter read with the paste as part of it, even a -one-line one -- and, for a paste of more than one line that does not end -in a newline, a second `\r`: one Enter leaves a multi-line input at `...`; a middle click does all of this -itself, holding the Enter until the REPL answers the paste), since line by line a blank line ends a Python block and Python -3.14's REPL auto-indents each line typed into it. The REPL gets the text -wherever it is -- at a `pdb` or `input()` prompt too. Over 9P, a range of a -`.py` pane goes to its bound REPL as a click would: write the event record -`MX<q0> <q1>` back to the `.py` pane's `event` (a range past its end is -refused, `range past end of body`). `Kill` does not stop -what a REPL runs, since pardes did not start it; `sig INT` on the REPL -pane's `pty/ctl` interrupts it as Ctrl-C would. Bindings are not dumped, so a Restore leaves none. +**`body`** reads the text; a write appends; `>` (OTRUNC) replaces it all. +A terminal's body is its history as plain text in logical lines (wrapped +rows joined), frozen per open; writing it sends input to the child. A PDF's +body is the text layer of the page shown; images and PDFs take no write +(`this pane has no text`). -The root's pair clicks at the active pane and `/pane/<n>/look` and -`/pane/<n>/exec` at that pane; `/tagexec` and `/col/<n>/exec` click in the -workspace's or that column's tag (never an event reader's, which hears -only its pane's; a command they run starts in the session's directory, -where pardes started, not the focused pane's), and what the word says is logged as the session's, -`msg -`. 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 builtin that fails there fails the write as well -(below: one rule). 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; a look that found text answers the pane the text is selected in, -not the hits buffer it opened. +**`sel`** reads the selected text; a write replaces it. **`errors`** is +write-only: text appended to the directory's `+Errors` pane (logged as +`msg` records when no column has room). -Reads are a stream: once an open has read the answer, a second read on it -gives EOF (on an open that wrote, until its next write); open it again, or -seek to 0, to read it again. The answer belongs to the open, as -/net/tcp/clone's does: an open that -wrote reads what its own last write touched, from the start after each -write, whatever offset the read comes at (a shell's `exec 3<>look` shares -one offset between its write and its read, as with `pty/run`); an open -that never wrote reads the session's last answer as it stood when it was -opened. So `echo x > look; cat look` works for one client, but two -clients doing it at once may read each other's; for that, write and read -on one open: `exec 3<>$m/look; echo x >&3; cat <&3; exec 3<&-`. +**`focus`** (root): a serial written gives that pane the keyboard and makes +its column active (a folded pane stays folded); `no such pane`, or +`ill-formed control message` for a non-number. It reads empty while a +column or workspace tag has the keyboard. -A write of command lines -- to `look`, `exec`, `tagexec`, a `ctl` of the -root, a pane or a column, or a column's `exec` -- runs each line once it is -whole: a mount cuts a big write at its message size (4 KiB through the -kernel's, 8 KiB from a client that asks), anywhere, and each piece comes as -a write of its own. A write that fills its piece may go on in the next, so -the open keeps its last line with no newline until then; a write shorter -than a piece is the whole of what was written, as acme takes each write, so -its last line runs with it even with no newline (`printf Save > exec`), and -a failure is that write's. Only what needs more is held: an Edit block -whose `{` or `a`/`c`/`i` text has not ended waits for its next write, and -what is left when the file closes runs at the close -- where an Edit block -whose `{` or `a`/`c`/`i` text never ended fails and changes nothing -(``unmatched `{'``, or `a, c or i text not ended by a . line`, logged as an -`err`): sam takes the end of input for a `.`, but a block that reaches Edit -unfinished was cut short. A fragment never runs on its -own. A line or Edit block held past 1 MiB is refused (EINVAL). +**Pane ctl.** Reading it gives acme's status line: serial, tag length, body +length, isdir (0), dirty, width in cells, font, tab width, undo available, +redo available, then `current`/`notcurrent` and a REPL's id if bound. It +takes: -`@p<serial>:<address>` addresses the pane with that serial as `file:<address>` -does a file, with any sam address (`/re/`, `?re?`, `#n`, `$`, `0/re/`, a range), -a terminal too: its body is then the text of its logical lines (the lines -a `+Search` of it lists), addressed from its cursor, and the match is -selected. A miss, and a serial no pane has (with a line, `@p77:3`, as -with any other address), each log an `err`, and look reads back empty. A look takes acme's addresses after a colon (editors/acme/look.c:450): a -line written to `look` as `file:/re/`, `file:?re?`, `file:#n`, `file:$` or any address -opens (or finds) the file and selects what the address names. **The -address is evaluated from the file's dot**, as acme's is: `file:/re/` -finds the next match after the current selection, not the first in the -file. For the first, start at the top: `file:0/re/` (or `file:#0/re/`). -A pattern that can match empty, such as `^`, passes over the empty match -at #0 as sam does (a search never answers where it started), so it finds -the next one: for the start itself write `file:0` or `file:#0`. -`:addr` does the same in the pane itself, and a pattern may hold blanks -(`calc.py:/return a/`). `file:12` selects line 12, its newline included, -as acme's does; `file:12:5` puts the caret at line 12, column 5 (columns count bytes from 1, -as `addr`'s `12:5` does and as the rows of Grep, +Search and the language -servers write them). A bare -`/re/` is a path, as in acme, and failing that a search for its text. A -look that finds nothing, an address that does not evaluate, or a line past -the file's end (`calc.py:99`) says so on the message row and in the log -(`err <serial> look: ...`), focuses and opens nothing, keeps the -selection, and leaves `look` reading back empty. +- the pane builtins: `Del` (`Del k`/`Del j`, or `DelAbove`/`DelBelow`, give + its rows to the pane above or below), `Save [path]`, `Collapse` (fold), + `Undo`/`Redo` (256 steps each), `Find pat`, `Edit ...`, `Tty [shell]` + (a new terminal pane in its directory), and the column words acting on + its column: `Delcol`, `Left`/`Right`/`Up`/`Down`. +- `get`: reload from the file (refused once over unsaved edits, `<name>: + Modified (get again to discard)`). +- `lock`/`unlock` (acme's). The lock binds only clients that take it and + belongs to the open that wrote it: `exec 3>$pane/ctl; echo lock >&3; ...; + exec 3>&-`. A `lock` another open holds fails at once, `file in use` + (EBUSY): retry. +- `answer <choice>` to the question the pane asks on its notice band, logged + `ask <serial> <what> <choices>` (`ask 4 del k j`, `ask 4 repl a b`, `ask 4 + save path`); `answer -` takes it back. +- acme's lowercase ctl words, done by what replaces each: `name x`, `put` + (Save), `clean`/`dirty`, `del`, `delete` (no asking), `dot=addr`, + `addr=dot`, `limit=addr`, `mark`/`nomark`, `show`, `cleartag`. `dump`, + `dumpdir`, `font`, `menu`, `nomenu` are refused, EINVAL. These lowercase + words are ctl-only: written to an `exec`, `del` is a command line. -`/pane/<n>/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. A name alone is no edit: the pane's `dirty` stays what its text made it -(a renamed clean file is still 0, and nothing asks about it at Exit, -Restore or Del, which ask only about text edited), and `Save` writes it -under the new name all the same. An open's writes are one name: held -until its newline, applied once however the writes cut it; a write with -no newline that is whole in its Twrite is the name then, so a bad one fails -that write, not the close. Nothing else is trimmed. Two lines are refused, EINVAL, -in one write or as a second line on the same open (bash's `printf -'a\nb\n' > name` writes a line at a time: the first names it). A blank inside a name is taken -(`two words.zig`); refused, EINVAL, in acme's words (xfid.c:650) and why, -are a blank at either end (not quietly cut off), `bad character in file -name: a blank at its start` (or `end`), a second line, `...: a newline (a name is one -line)`, a control byte, DEL or a C1 control (U+0080-U+009F), `...: a -control character`, and bytes that are not UTF-8, `...: not UTF-8`. `body` appends on write and replaces on truncating open. A -terminal's `body` is its history as plain text, frozen per open, in logical -lines: a row the terminal wrapped is joined back to the row before it (the -wrap is ghostty's, as `pty/run`'s output unwraps), and the last line ends -with a newline. `sel` -reads the selected text and writing it replaces the selection, leaving dot -just past the text written (a `data` write moves dot as its text moves it, -so a dot at the address it wrote at ends just past that text too). `errors` -appends to the directory's `+Errors` pane; with no room for that pane in -any column, what it would have shown is logged as `msg` records instead, a -line each, and the write still succeeds. Holding `event` open redirects the -pane's Look and Exec clicks to that client, and so does a line written to the -pane's own `look` or `exec`, or to the root's while that pane has the -keyboard (never its `tagexec`, which is a click in its tag and runs), a -click with no place in the text: an `F` record at `0 0` carrying -the line. So a client holding `event` that wants a command run gets its own -exec back as a record: it runs it through `ctl`, or writes the record back. -Writing a record back performs the action, as the click would have (acme's -xfideventwrite): a body `X` goes to a REPL bound for the text as a middle -click does, where an `F` record, a line written to `exec`, runs as the -command it was. acme takes back only `<origin> -<action><q0> <q1>`, the text of that range; pardes takes the record whole as -it was read too, and for an empty range acts on its text, which is how such -a line is done. A click in a file's body carries the offsets of the text it -took; one in a terminal's body cannot, since that body is a history -snapshot, and is also at `0 0` with its text. A click that takes no text -sends nothing, as in acme. `ctl` reads acme's window status line, field for -field as plan9port's — serial, tag length, body length, isdir (0), the dirty -flag, the width in cells, the font, the tab width, whether Undo has a step -(1) and whether Redo has one — followed by pardes's own: rio's `current` or -`notcurrent` (rio(4), `wctl`), whether the pane has the keyboard, and a -REPL's id. It takes the pane's builtins (below), -`get`, which reloads the buffer from the name it -carries (unsaved edits are refused once, listed in `+Unsaved` with a short -notice as Exit's are, the write failing `<name>: Modified (get again to -discard)`, as acme's get asks winclean, exec.c:513). A file that changes on -disk reloads by itself only into a buffer with no unsaved edits; one with -them keeps its text and stays dirty, says `<name> changed on disk (get -reloads it, Save overwrites it)` and logs `changed <serial>`, and then its -`Save` warns once, `<name> modified on disk since read (Save again to -overwrite)`, as acme's Put does (exec.c:577) -- acme reloads nothing by -itself. `answer <choice>` -for the question the pane asks on its notice band, which the log names as -`ask <serial> <what> <choices>` -- `ask 4 del k j` for Del's side from the -keyboard (`k` the pane above takes the rows, `j` the one below), `ask 4 -repl a b` for which bound REPL takes an exec (a REPL's letter) -- `answer -` -taking it back as Esc does (a choice the question does not offer is refused, -naming those it does, and the question stands; with no question standing, -`answer` is refused), -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>&-`. +A file changed on disk reloads by itself only when the buffer has no unsaved +edits; otherwise it stays dirty, says `<name> changed on disk (get reloads +it, Save overwrites it)`, logs `changed <serial>`, and its next `Save` +warns once. -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`, and pardes's own `12:5`, -below); `addr` selects where `data` reads from -- to the end of the text, -as acme's does, so `cat data` reads everything after the address -- and -the range `xdata` reads, just that; either replaces the range, `dot` is the editor's own selection and moving it -scrolls the pane into view, and `limit` bounds only the end of a forward -search, as acme's does, and reads empty until it is set. Truncating `dot` empties it, truncating `limit` lifts it -- -though a write after the truncation that fails puts the old limit back, so -`echo /bad/ > limit` changes nothing -- and truncating `addr` leaves it as -it is (below). +### Addresses and data -A rename everywhere, or any other sam edit, is one write to the pane's -`ctl`: `Edit ,x/foo/c/bar/` runs acme's Edit (docs/tags.md) on the body as -one undo step; a failure fails the write with acme's words and changes -nothing. A write is one message a line, except that an `Edit` line takes -the lines after it while its `{` group is open or its `a`, `c` or `i` text -block waits for its `.` line, on a pane's `ctl`, the root's (at the active -pane) and `exec` alike; an unclosed group is refused (``unmatched `{'``). -A block may come in several writes on one open (bash's builtin `printf` -writes a line at a time): the open holds it until it ends (below). +`addr`, `dot` and `limit` read the pair of byte offsets they take, so +copying one onto another (`cp $p/addr $p/dot`) is acme's `dot=addr`. A +write is a pair or an address expression. `addr` names where `data` reads +(to the end of text) and the range `xdata` reads; `dot` is the selection +(moving it scrolls the pane there); `limit` bounds the end of a forward +search and reads empty until set. Truncating `dot` empties it, truncating +`limit` lifts it. -A line number counts newlines as sam's lineaddr does (editors/sam/ -address.c:180), so the empty line just past a text's last newline is an -address: `1` of an empty text is `#0,#0`, `2` of `a\n` is `#2,#2`, and -`Edit 1i/header/` on an empty file inserts; a line past that is `address -out of range`. +- A write to `data` or `xdata` **replaces** the `addr` range (`>` and `>>` + alike); `: > data` deletes it. Truncating `data` is pardes's own (acme + ignores OTRUNC there); only truncating `body` empties the buffer. +- A write leaves `addr` just past what it wrote, so a second `echo x > data` + inserts after the first: write `addr` before each replacement. A read of + `data`/`xdata` moves `addr` past what it read. +- `addr` belongs to the pane, not the client, and neither an open nor a + truncation resets it (acme resets on first open): each `echo /re/ > addr` + searches on from the last, so a find loop advances. Write `0` to start + at the top; searches wrap, so stop a loop when the address comes back or + set `limit`. +- A failed address leaves **no address**: `addr` reads empty and `data`/ + `xdata` refuse (`no address: the last one written to addr failed`) until + a standalone address (`2`, `#0`, `/re/`) is written, so a missed target is + never written at the old one. -`line:col` is a pardes extension to sam's addresses, the spelling Look -takes in `file:12:5`: `12:5` is the point at line 12, column 5, and it -composes like any simple address (`12:5,14:1`, `12:5+#3`). The column is -in bytes from 1, as Look's is, clamped to the end of the line and snapped -back to the start of the rune it falls in; a line past the end, or -column 0, is `address out of range`. Since the column counts bytes, a -column a tool gives in characters (pytest's, a compiler's) is the same -only on an ASCII line; elsewhere address the line and search it -(`12/name/`) or use `#n`. sam would read `12:5` as a syntax -error. The rows of Recent, a +Search and the Jumplist spell a range -`12:5-14:2` (or `12:5-9` on one line), and `addr` takes that too, so a row -pastes in; the two spellings side by side: +Addresses are sam's: `#n`, a line number, `/re/`, `?re?`, `-/re/`, `$`, +`.`, `0`, ranges `a,b` and `a;b`, `+`/`-`. They are evaluated from the +current address (the last written to `addr`, or just past the last `data` +write), not from the selection. pardes adds `12:5`: line 12, **byte** +column 5 from 1, clamped to the line's end (`12:0` is `address out of range: +a column counts from 1`); it composes (`12:5,14:1`). A tool's character +column matches only on an ASCII line; elsewhere use `12/name/` or `#n`. A +row of Recent, `+Search` or the Jumplist spells a range `12:5-14:2` (through +14:2 inclusive) and `addr` takes it as such. - 12:5,14:2 sam's range: from line 12 column 5 up to the point at 14:2 - 12:5-14:2 a row's range: 12:5 through the character at 14:2, inclusive +Offsets and counts are bytes everywhere (acme counts runes), but every +address lands on a rune boundary: `#n` or `L:C` inside a rune snaps to its +start, a match covers the runes it touches, and a combining mark or a CRLF's +`\r` is addressable alone. -A `-` right after `L:C` and a digit is this range, not sam's "back N -lines" from that point. -A write to `data` or `xdata` replaces the range `addr` names, as acme's -does (editors/acme/xfid.c:491-523: it deletes the range, then inserts), so -`echo NEW > data` and `echo NEW >> data` both replace that range, and -truncating deletes it and nothing else, so `: > data` deletes it; only -truncating `body` empties the whole buffer. Truncating `data` is pardes's -own: acme ignores OTRUNC there (editors/acme/fsys.c:543). And as in acme a write -leaves `addr` just past what it wrote, so a second `echo x > data` deletes -the empty range there and inserts after the first rather than replacing it -again; write `addr` before each replacement. A read of `data` or `xdata` -moves `addr` past what it read, as acme's does. -`addr` belongs to the pane rather than to a client and keeps what was written -until someone writes another, so writing an address and reading it back -evaluates it, which is what acme(4) promises of its own `addr`. Unlike acme, -neither an open nor a truncation resets it: acme sets it to `#0` when the -first client opens `addr` (editors/acme/xfid.c:105-108), which suits a -client that holds the fid, but a shell opens the file anew for every -`echo /re/ > addr` and so would search from the top each time and never -advance. Here each such write searches on from the last address, as `>>` -does; write `0` to start again from the top. A search wraps at the end of -the text, so a find-all loop stops when the address comes back to where it -began, or bounds itself with `limit`. +Refusals: `bad address syntax`, `no match for regexp`, `address out of +range`, `addresses out of order` (`#100,#50`), `bad regular expression`. -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; there a leading `^` still -matches at every line start (`$` on a CRLF line sits before the `\r`, -as the line's end is its `\r\n`), and `$` may stand just before a `\n` (where it -changes nothing). Any other `^` or `$` in such a pattern, `(^|\n)def` or -`a\nb$`, is refused, EINVAL, with `bad regular expression: in a pattern with \n, ^ can only come first and $ -only just before a \n`, since mvzr would read it as the start or end of the -whole text: never a search that silently finds nothing. An alternation -whose every branch starts with `^` (`^def|^ `) finds a line that starts -either way (it is taken as `^(def| )`); one that mixes anchored and -unanchored branches (`^def|x`) is refused, `bad regular expression`, since -mvzr keeps `^` only first. A pattern may be up to 512 of mvzr's operations, -about 512 characters (mvzr's own is 64; pardes builds it with more); a -longer one is refused, `bad regular expression: longer than mvzr's 512 -operations`. A class may hold non-ASCII runes (`[éa-z]`, `[à-ÿ]`): it is -taken as an alternation of them (`(é|[a-z])`), a range of up to 256 runes -spelled out, and the 512 counts the pattern as rewritten. A wider range -(`a range of runes wider than 256 in [...] is not supported`) and a negated -class with a non-ASCII rune (`[^é]`) are refused, since mvzr's classes hold -bytes. An expression is -evaluated from the current address, the range last written to `addr` (or -left by the last `data` write, just past it), as acme evaluates it from -`w->addr` (xfid.c:446): `.` is that address, not the selection (`dot` is -the selection's own file), and `#9/re/` searches from `#9`; in `/a/;/b/` -the second search starts at the end of the first, as acme's `;` does, where -`/a/,/b/` starts both at the current address. `/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?` and `-/re/` find 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 a group can match there; and in a -pattern that spans lines, `^`, `$` and `[^...]` keep mvzr's own meaning. -mvzr backtracks without bound of its own (`a?` twenty times then twenty -`a`s is 2^20 steps from each place it tries), and a search holds the editor, so pardes patches a -step budget into mvzr's matcher (build.zig): a search that spends it, -about 300 ms, fails with `regular expression search took too much time, -gave up` rather -than answer a match it is not sure of. The budget is each search's: an -Edit `x` over 100k lines makes 100k searches, each with its own. Ordinary patterns spend a few -thousand steps; what runs out is exponential backtracking, and a quadratic -pattern over a very long line (`\s*(\w+)\s*=` over 20 KB of letters). -pardes has no regex engine of its own on purpose; these are its limits. -Normal mode's `s` and `S` search a selection the same way (src/regexp.zig -is the one place both call), so `^` there also means a line's start. +sam details kept: `$-1` is the last line when the text ends in a newline, +else the one before; with a final newline the empty place after it is a line +(`1` of an empty text is `#0,#0`, so `Edit 1i/x/` works on an empty file); +`2,1` is an empty range at line 2's start; `/^/` finds the empty place after +a final newline; a pattern that can match empty (`^`) passes over the match +where the search starts. -An address that does not evaluate fails the write with why: `bad address -syntax`, `no match for regexp`, `address out of range`, `bad regular -expression`, `regular expression search gave up, ...`, or sam's -`addresses out of order` for a range that ends before it starts -(`#100,#50`), which acme lets through. Each end is checked first, so in a -text shorter than 100 bytes `#100,#50` says `address out of range`. A failed write to `addr` leaves no -address at all, where acme -keeps the old one: until a good address is written, `addr` reads empty -(as an unset `limit` does), 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; so does an address -written to `addr` that goes from the current one (`.`, `+1`, `-/re/`), which -there is none of, until an address that stands alone (`2`, `#0`, `/re/`) is. +### Regular expressions -The three flag files `dirty`, `mark` and `scroll` read `0` or `1` and take -`0` or `1`: whether the buffer differs from its file (a file deleted on -disk counts, as in acme: its text is only here now, and Del, Exit and the -rest ask first), whether a write pushes -an undo point (writing `1` pushes one now), and whether a write scrolls the -pane. The writes of one open of `data`, `xdata` or `body` are one undo -step while `mark` is 1 (so `printf 'a\nb\n' > data` is one, though a -shell writes it a line at a time), and the next open starts another. The -pane has one history, so two opens writing at once take turns in it: each -open's first write after the other's starts a step of its own; to -make a loop's writes one step, write `1` (an undo point here), then `0`, -the writes, then `1` again. An open's writes in a row to one place -- an -append to `body`, inserts going on at `data`'s address -- are held and go -in as one edit when anything else comes (another request, the close, or -the editor's step once they pause 20 ms), so a 10 MB write is one copy, not -one per 8 KB piece; any read sees them. +Patterns are [mvzr](https://github.com/mnemnion/mvzr)'s (classes, `\d\w\s`, +`{m,n}`, lazy `*?`), searched as sam searches, line by line: `^` and `$` +match at any line's start and end, `.` and `[^...]` never match a newline. +The same code (`src/regexp.zig`) serves addresses, Edit, and normal mode's +`s` and `S`. The ceiling: -One rule for what fails: a write fails whenever what it asked for fails, -whether it came to a `ctl` (the root's, a pane's, a column's, a pane's -`pty/ctl` -- its `exec` included), `look`, `exec`, `tagexec` or a -column's `exec`, or to any other file -- with an -errno that fits, EINVAL for malformed input (an unknown word, a control -character, a command line over 1024 bytes, a `size` or `winsize` out of -range, a bad address or event record), else EIO or the errno the words -name (ENOENT for a pane, file or directory gone -- `look .` from a pane -whose directory is gone says `look: <dir>: no such directory`, and a -`./zz.txt` or `../x` that is not there `look: ./zz.txt: no such file`, while a -plain `zz.txt` is looked for as text, a miss logged as any look's -- and for a -Find or Grep that finds nothing, `Grep: pattern not found`; Grep walks -every pane's directory on this host, passing over panes of the served -tree (`/virtual/`, a peer's `/n/<name>/`) and directories not there, -so none of them spoils the rest; Find, Grep and a language server's lists -(Symbols, Diagnostics, Callers and the rest) share one `+Search` a -directory, each run replacing what the last showed, as acme reuses a -directory's `+Errors` (the exec reads that pane back; one that finds -nothing empties it rather than leave the last rows), while a plain -word's `LookWord list` search keeps a pane a pattern, ENOSPC for no room or -slot, EBUSY for a held lock) -- and logs its reason exactly once, as `err <serial|-> -<file>: <why>`, with no `msg` for it. A builtin a click runs (Save, get's -`Modified`, Tty with no room, Edit) is no exception. The rule is a write's: -a refused open or truncation -- an OTRUNC open, such as `data`'s after a -failed `addr` --, create or remove answers its error and -logs no `err`, and so do a write to `pane/new` (`permission denied`: it is -only read) and a write on a fid opened OREAD (`bad use of fid`). Every -refusal is said in words, Plan 9's where pardes has none of its own -(`permission denied`, `file does not exist`, `bad argument`), never a C -library string such as `Operation not permitted`. What is not a -failure: a look that finds nothing answers nothing and logs one `err` -(`look: no match for ...`, quoting what was written in every form -- -`no match for "zzq:#3"`, `no match for "zzq:2"` -- the same miss again counted, `(x2)`, as any -repeated `err` is), the write succeeding; and a command line run in -a command pane ends in its own time, told by its `exit` record. +- The leftmost match wins, but among alternatives the first that matches, + not sam's longest (`/gam|gamma/` finds `gam`). mvzr keeps no + submatches, so Edit's `s` has no `\1`-`\9`. +- A pattern holding `\n` runs over the whole text: there `^` may only come + first and `$` only just before a `\n`, else it is refused. +- An alternation must anchor every branch or none (`^def|^ ` works, + `^def|x` is refused). +- A class may hold non-ASCII runes (`[éa-z]`, a range up to 256 runes); a + wider range or a negated class with one (`[^é]`) is refused. +- At most 512 operations (about 512 characters, counted after that + rewriting): `bad regular expression: longer than mvzr's 512 operations + (about 512 pattern characters)`. +- Each search has a step budget (about 300 ms; each search of an Edit `x` + its own): `regular expression search took too much time, gave up`. What + runs out is exponential backtracking (`a?` twenty times then twenty `a`s) + or a quadratic pattern over a very long line. + +### Edit + +`Edit <sam commands>` on a pane's `ctl` or `exec` (or the root's, at the +active pane) runs acme's Edit on the body: addresses as above, commands `x y +g v c a i d s p = m t u` and `{ }`. All changes are one undo step, applied +only if every command succeeds; a failure changes nothing and fails the write +with acme's words (`Edit: no substitution`). An `x` that finds nothing +succeeds silently. `p` and `=` print to the directory's `+Errors`. Not +there: `b B D e r w f X Y`, `< | >`, `\1`-`\9`. In `s`, `&` is the match +(`\&` a literal); in `c`, `a`, `i` it is a literal. `y` yields the stretch +before the first match too. + +Braces take a command a line, so a block goes on one open, in one write or +several: + +```sh +printf 'Edit ,x/foo/{\ni/</\na/>/\n}\n' > $p/ctl +``` + +### Flags and undo + +`dirty`, `mark` and `scroll` read and take `0` or `1`: the buffer differs +from its file (a file deleted on disk counts); a write pushes an undo point +(writing `1` pushes one now); a write scrolls the pane. `/index`'s dirty +flag is `dirty`; a `+New` scratch reads 1 but holds up nothing under 100 +bytes. + +The writes of one open of `data`, `xdata` or `body` are one undo step (so a +multi-line `printf` is one). To make a loop of opens one step: `echo 1 > +mark` (a point now), `echo 0 > mark`, the writes, `echo 1 > mark`. + +### event + +Holding `event` open takes the pane's Look and Exec clicks: they come to the +reader as records instead of acting, as do lines written to the pane's own +`look`/`exec` (or the root's while it has the keyboard). A record is acme's +`<origin><action><q0> <q1> <flag> <n> <text>\n`; read `n` **bytes** of +text, which may hold newlines. + +- origin: `E` a 9P write to body or tag, `F` other files and the editor's + own lines, `K` keyboard, `M` mouse. +- action: `X`/`L` executed/looked in the body, `x`/`l` in the tag (offsets + into the whole tag, path included), `I`/`D` body text inserted/deleted, + `i`/`d` the tag's. +- flag: 1 the text's first word is a builtin, 4 (look) a file name or + address, 8 (exec) chorded: two records follow, the argument and where it + came from. pardes never sends flag 2. +- A written line, or a click in a terminal's body, has no place: `0 0` with + its text (`FX0 0 1 6 Msg hi`). + +Write a record back to have it done as the click would: `<o><a><q0> <q1>\n` +acts on that range; the whole record as read acts on its text (the only way +for `0 0`). A chorded record with its two follow-ups, in one write or +three, runs once with its argument. `I`, `D`, `i`, `d` are refused. A helper +holding `event` that writes its own pane's `exec` gets its command back as a +record: run it through `ctl` instead. + +### REPLs + +`Repl python` on a terminal's `ctl` (or in its tag) binds it as that +language's REPL (names as a code fence spells them: `py`, `sh`, ...); its +tag and `ctl` line show its id, `python-a`. `Repl -` unbinds, bare `Repl` +says the binding. A middle click or the execute key on a `.py` body then +types the text into the REPL instead of running it; builtin words, tag +words, `Exec <text>` and @`cmd` words still run. With several bound, the +pane asks (`ask <serial> repl a b`). Bindings are not dumped. + +A 9P `exec` is never sent to a REPL. A script either writes the event record +`MX<q0> <q1>` to the `.py` pane's `event` (sent as the click would be), or +writes the REPL's `pty/data` itself: multi-line code as a bracketed paste, +`\e[200~<code>\e[201~`, then `\r` in a separate write once the REPL has +echoed the paste; a paste of more than one line not ending in a newline needs +a second `\r`. Line by line, a blank line ends a Python block and Python +3.14 auto-indents each line. + +## Terminals + +Terminal panes also have `pty/`: + +- `pty/data`: write bytes as typed (`printf 'ls\r'`, `\x03` is Ctrl-C); + read the live output stream (a consuming queue shared by readers, not a + replay). +- `pty/status`: one line, `cols rows busy`; busy is 1 while a command runs or + text is typed at the prompt. +- `pty/ctl`: `winsize C R` (at least 2 rows), `sig INT|TERM|HUP|QUIT|KILL`, + `exec` (restart the shell in its directory: refused on a command pane, `a + command pane does not restart`; `exec: <dir>: no such directory` if it is + gone; a shell that cannot start fails and leaves the old one running). +- `pty/run`: one line at the shell's prompt, answered on the same open: + + ```sh + exec 3<>$m/pane/$n/pty/run; echo make >&3; cat <&3; exec 3<&- + ``` + + The answer's first line is the header: `exit N` then the command's output + (as the screen showed it: no colour, `\r` progress collapsed, trailing + blanks dropped; the last 64 KiB, `exit N cut M` when M bytes were left + out, bare `cut` when the start scrolled away or was cleared). Or: `busy: + <program> is running` (bare `busy` when text is typed at the prompt), + `exit ?` (no status reported, not a success), `error not run` (the shell + refused the line, e.g. a fish syntax error), `error shell gone`, `error no + prompt marks`, and on a command pane `error a command runs here, not a + shell` (`error command done; not a shell` once it ended). A line written + before a fresh terminal's first prompt waits for it. One line per run; + a second line before the answer is refused. + +`pty/run` relies on the OSC 133 marks pardes injects into bash and fish; +`exec zsh` or a continuation prompt never reports an end, so cancel the +read. A program on the alternate screen (vim, less) leaves no output. A +program holding the terminal (a REPL, `less`) takes no run: write to +`pty/data`. + +## /log + +One 64 KiB ring, one record a line, kept whether or not anyone reads it. An +open freezes it and reads to EOF. Write `follow` to that open to then wait +for each new record (`follow new` skips the history, as `tail -n0 -f`); +`tail -f` never writes `follow`, so it sees nothing new. + +```sh +exec 3<>$m/log; echo follow >&3; while read -r line <&3; do ...; done +``` -`tag` reads the whole tag as the pane shows it: the computed path or PDF -page (no mark for unsaved text: the grip shows that, and `dirty` says it), -then the text you may edit. An image's tag begins with its mode words, not -its path: `img petscii:off palette:commodore ascii:on <path>` by default, the modes -it is drawn in, then the file. The editable text is 4096 bytes at most: a -write that would pass that is refused whole, ENOSPC, `tag: over 4096 -bytes`. 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 default -words (`Save Tty Collapse Del` and the rest) with it, since they are that text until -you edit it, so `echo Make > tag` leaves only `Make` -- a truncating write -drops the one newline that ends what it wrote, which would draw an empty -row, and keeps any other; append with `>>` to -keep them, with `printf ' Make' >> tag`. The leading blank is needed: the -tag reads back with no blank after its last word, so `printf Make >> tag` -glues `Make` onto it; and `echo ' Make' >> tag` ends with a newline, which -starts a new line of the tag. 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, words -included. +A follower the ring outran reads `lost N` first. A Restore hangs the +follower up: dial again and read from `restore <path>`. -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. +| record | when | +|---|---| +| `new <serial> <name>`, `del`, `rename`, `save` | a pane made, closed, renamed (a terminal's too, as its shell changes directory), saved | +| `newcol <serial>`, `delcol <serial>` | a column made or closed | +| `msg <serial\|-> <text>` | the editor said something (not a builtin's own name under `Verbose`) | +| `err <serial\|-> <file>: <why>` | a write was refused or failed | +| `run <serial> <line>`, `exit <serial> <N\|?>` | a command pane's command started and ended; also a terminal whose shell exited, before its `del` | +| `send <from> <to> <repl-id>` | text went to a REPL | +| `ask <serial> <what> <choices>`, `answer <serial> <choice\|->` | a pane asked, and was answered (`-`: taken back, or the pane closed) | +| `changed <serial> [reloaded\|deleted]` | its file changed on disk (bare: under unsaved edits) | +| `unsaved <serial> <name>` | a pane an Exit, Restore, Del or Delcol refused over, before the `err` | +| `dump <path>`, `restore <path>` | a Dump written; a Restore, after its panes' `new`s | +| `restored <old> <new>`, `restoredcol <old> <new>` | serial maps after a Restore | -`/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 -- -a new terminal is `new N <dir>`, named by the directory it is started in, -and renamed only when its shell goes elsewhere (the boot's, started with -no directory given, is `new N /` until its shell says where it is); it -runs `ls` once, at its shell's first prompt only, never later over what -someone ran or typed first, a greeting that shows the directory it opened -in, since a terminal is named by its directory and nothing else on it says -where it is), `exit <serial> <N>` before -the `del` of a terminal whose shell exited by itself, `ask <serial> <what> -<choices>` when a pane asks a question (answered by `answer` on its ctl; -`ask <serial> save path` when a Save made over 9P, on a terminal or a -scratch, opens its prompt for a path, answered `answer <path>` or `answer --`; asked again, the one open stands, not a second), -`answer <serial> <choice|->` when it is answered, by key or ctl, `-` for -taken back or for its pane closing with the question standing, -`changed <serial>` when a pane's file changed on disk under its unsaved -edits (below), `changed <serial> reloaded` when a pane with none reloaded -it, and `changed <serial> deleted` when it was deleted or moved away, `unsaved <serial> <name>` for each pane an Exit, Restore, -Del or Delcol refuses over (before that write's `err`), and `save <serial> <name>`, -`dump <path>` when a Dump is written and `restore <path>` in a Restore's -new log after its panes' `new`s (a relative Restore path is looked for in -`DumpDir`, then in the directory pardes started in; a bare Restore takes the -last dump this session wrote), then `restored <old> <new>` for each pane, -mapping the serial it had to the one it has now, and `restoredcol <old> -<new>` for each column, and `msg <serial|-> <text>` -for every line the editor says (a builtin announcing its own name as it -runs, with `Verbose` on, is the message row's alone, never logged: a `msg` -is something said, `Kill: nothing running`). A builtin that -fails its write (`ctl`, `exec`, `/tagexec` or a column's `exec`, and an -open of `pane/new`) is logged by that write's `err` alone, no `msg`, so -the same failure again is the same record again; a line said again word for word is counted, `msg 3 Undo: nothing to -undo (x40)`, as `err` is (below); a text past 256 bytes is cut there, -between words, and ends in an ellipsis, `…`, as an `err`'s reason is past -200; its serial is the pane it ran at -- a line written to the root's -`exec` or `look` runs at the active pane, and is logged as that pane's, -even while the keyboard is on a column's or the workspace's tag -- and `-` -for a line to the root's `ctl`, `/tagexec`, or a column's `ctl` or `exec`, -which run in a tag no pane owns), and `err <serial|-> -<file>: <why>` (the serial is that of the pane whose file it is, and for -the root's `exec` and `look` the pane the line ran at, `err 3 exec: ...`; -`-` for another root file) 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`). A record said again word for word, straight after -itself, is counted rather than repeated (`err 3 addr: no match for regexp -(x4)`: four in all, counting the first), so a client retrying a failing -write does not push the rest out of the ring. A record a follower has -already read is never rewritten: the next repeat is a line of its own -carrying the running total, `(x5)`, and counting goes on from there, so a -follower sees each count as a new line: with a follower attached, a repeat -is a new line carrying its `(xN)` count, never an edit of the one read. Through a kernel mount a client sees only an errno, which 9ns reads from the -error's words (cloud9's 9ns/src/nine.zig, `enameToErrno`): a malformed write --- an unknown or ill-formed control message, `bad address syntax`, `bad -regular expression` -- is EINVAL; a lock another open holds, EBUSY; a pane -gone, ENOENT; a well-formed write that fails -- `no match for regexp`, -`address out of range`, `addresses out of order`, a search that gave up, -`<name>: Modified (Exit again to discard)` -- EIO. The err record has the -words. 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. -Every EINVAL a write gets says why, in its reply and in its `err` record -(`bad character in file name: an empty name`, `invalid write to log: it -takes \`follow\` or \`follow new\``, `invalid write: this pane has no -text`), never a bare `Invalid argument`. -Control characters, DEL and C1 controls (U+0080-U+009F) in a record become -spaces, and a byte that is not UTF-8 is written `\xNN`, so a record is one -line of UTF-8; a pane's name, in a record, in `/index` and in a terminal's -tag alike, shows a newline in it as `\n` rather than a space and its own -backslash as `\\`, so a directory named with a newline and one named -with a backslash and an `n` read back differently, as what they are. -(An `event` record is not: acme's `<origin><action><q0> <q1> <flag> <n> -<text>\n`, whose text may hold newlines. The origin is `E` (a 9P write to body or tag), `F` (other -files, the editor's own lines), `K` (the keyboard) or `M` (the mouse); the -action's case says where: `x`/`l` a click executed or looked at in the tag -(its offsets count the tag's whole text, path included, as `tag` reads), -`X`/`L` in the body, `I`/`D` text put in or taken out of the body, `i`/`d` -of the tag. The flag is acme's: 1 the text's first word is a builtin's, 2 -an expansion record follows (acme's look.c:42-43, exec.c:154-157; pardes -never sends one: a click's record already carries the word it took, its -range and text, so 2 is never set, whatever the origin), 4 (a look) the -text is a file name or address, 8 (an exec) chorded: two records -follow, the argument's text and where it came from, `<file>:#q0,#q1`, each -with no place of its own: offsets `0 0` and flag 0, `Mx0 0 0 5 hello` (as -acme's exec.c:182 writes them). -Written back, an `X`/`x` record executes and an `L`/`l` record looks, as -the click would have. A chorded one (flag 8) runs with its argument: the -record after it in the same write, else the one kept from the click; its -two follow-up records written back after it, together or in writes of -their own, are consumed as its, never run as commands (written back alone -with no argument kept, it waits for its argument record); the origin letter -written back is ignored but for `F` on `X`, which runs as the command it -was rather than going to a bound REPL; `I`, `D`, `i` and `d` are reports and -are refused. Read `n` bytes of the text, not up to a newline -- bytes here, where acme counts runes. Every offset and -count pardes serves is in bytes, `#n` and `q0`/`q1` too; the event count -follows them rather than switch alone, so an acme library reads pardes -correctly for ASCII text and not beyond it. Offsets are bytes, but every -address lands on a rune boundary, as sam's and acme's work in runes, never -inside a multibyte rune and never widened to a grapheme cluster: a `#n` -inside a rune snaps back to its start, a `line:col` likewise, a search's -match covers the runes it touches, and a copy of addr to dot keeps its -runes, so a lone combining mark or the `\r` of a CRLF is addressable on its -own (an Edit's `x`, `y` and `s` match the same way: `.` is one rune); `addr` reads back the snapped offset. (How the terminal draws such a -text, a cluster to a cell, is apart from this.) A click in a -tag gives offsets into the whole tag as `tag` reads it, the path first.) 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, after first reading all that the open froze (the -ring's whole history, up to 64 KiB); `follow new` skips that and waits for -what comes after, as `tail -n0 -f` does. A follower the ring outran reads -`lost N` first. -Closing the open is the only way back, as with rio's `consctl`. A follower -misses nothing within a session only: a Restore hangs its connection up -(and a 9ns mount with it, which must be restarted), so it dials again and -reads the new log from its `restore <path>`. +The serial is the pane the line ran at (the active pane for the root's +`look`/`exec`), `-` for the root `ctl`, `/tagexec` and column files. A +record said again straight after itself is counted, `err 3 addr: no match +for regexp (x4)`; a follower sees each count as a new line. A `msg` is cut +at 256 bytes and an `err` reason at 200, ending in `…`. Control characters +become spaces. -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 `no such pane: its window shut`, ENOENT as any -other file of a gone pane gives, where acme says "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. +## Other files -`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. One line a run: two lines in one write, or a next line on -the open before the last answer is read (bash writes `printf 'a\nb\n'` a -line at a time), are refused, EINVAL. 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: <program> is running` at once when a command is running (bare `busy` -where the host cannot name the program, and when text is typed at the prompt) --- a run is a line typed at the shell's prompt, so a program holding the -terminal (a REPL, `less`) takes none: write to `pty/data` for it -- -which is also when the third field of `pty/status` reads 1. `pty/status` is -one line, three right-aligned fields and a newline: the pty's columns and -rows, then busy (0 or 1). `pty/ctl` takes `winsize C R`, `sig -INT|TERM|HUP|QUIT|KILL` and `exec`, which starts the pane's shell again in -its directory (a command pane's child is its command, which does not -restart: `exec` there is refused, EINVAL, `a command pane does not -restart`): one that is gone is refused before anything runs, `exec: -<dir>: no such directory` (ENOENT), and a shell the host cannot start (not -there, not executable, a script whose interpreter is not there) fails the -write with why -- `shell: shell not found`, or `shell: interpreter -/no/such/interp not found` for a script whose `#!` names a program that is -not there, ENOENT -- keeping the terminal and its running shell: a shell -not there is refused before anything runs, and the host starts the new one -before the old goes, so one that cannot start leaves the old be; a `Tty` naming such a shell or -script is refused before anything runs (`Tty: interpreter ... not found`, -its `err` the only record), and one whose shell cannot start fails the -same way and leaves no pane. The host knows before it answers: the child reports a failed exec -through a close-on-exec pipe. A directory removed -under a running shell leaves the pane its name (never `... (deleted)`), so -an `exec` works there once the directory is back. The size, and `pty/ctl`'s `winsize` read back, is -what `winsize C R` last set (R of 1 is taken as 2: a one-row pty loses -its prompt's mark and would read busy for ever), until the pane itself -resizes and gives the pty its grid again. 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. A shell that never draws a tagged prompt (one pardes -could not instrument, or a startup that hangs) leaves such a line waiting -for ever: cancel the read (interrupt it, or close the open) to give up; -`error not run` when the shell refused -the line without running it (a fish syntax error; the line is taken back off -the prompt): the shell's marks say only that it did not run, so the answer -carries no reason or code, and the shell's own complaint is in the pane's -body (`tail body`); `exit N` and what it printed when the line ended the shell -itself (`exit 3`, or `echo bye; exit 3`): its terminal closes, and a read -of the run's open still answers after the pane is gone; `error shell gone` -when the pane closed or its shell was replaced, the shell went without -an exit status to tell, or it never started (a `Tty` in a directory that is -not there is refused before, `Tty: <dir>: no such directory`, and makes no -pane); `error no prompt marks` for a shell pardes could not instrument; -`error command done; not a shell` (or `error a command runs here, not a -shell`) on a command pane, whose child is its command. -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: `exec -3<>$m/log; echo follow >&3; while read -r line <&3; do ...; done`. bash's -`read` takes a chunk, keeps one line and seeks back to just past it; a -followed log, `event` and `pty/data` answer a read at an offset inside -their last answer from that answer again, so no record is lost. Through a -FUSE mount (9ns --mntgen) bash's `read -t` does not time out on a followed -file: the read waits in the mount, where its timer cannot cut it; wrap the -loop in `timeout N`, or read with `cat` in the background and poll what it -wrote. `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; -a PDF's `body` reads the text layer of the page it shows (MuPDF's -extraction; turn the page for another), and it, as an image's, takes no -write (`this pane has no text`, EINVAL); -`pty/data` streams live output. The listings -- `/index`, `/layout`, -`/recent`, `/commands`, `/status`, `/listeners`, and a `ctl` opened only to -read -- freeze at the open too, so one read in several chunks never splices -two moments; open again for what is there now. An open that holds something between open -and close -- a frozen screen, listing, 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. +- `/screen`: JSON `cols`, `rows`, `cursor`, `styles`, and row-major `cells` + of `[grapheme, style_index]`; one frame per open. Compare a cell's style + through `styles`, not the index. +- `/commands`: `Word [arg] root|pane|both [values] -- sentence`, one a line, + generated from the builtin registry (`both`: Edit, at the active pane from + the root). +- `/recent`: up to 200 files, most recent first, `open <path>` or `closed + <path>`; kept in `$XDG_STATE_HOME/pardes/recent`. `Recent` shows them in a + pane; a look at a row reopens the file at its last dot. +- `/status`: `pid`, `version`, `panes`. +- `/os/`: existing regular files take read, write and truncation to zero; + create, remove, rename and metadata changes are refused; ownership is + synthetic. Linux v9fs's truncation `mtime` hint is accepted and dropped. +- `/src/` (and `/shaders` on GUI builds) with `-Dembed-sources=true`; + `EffectCode <effect>` lists an effect's files under `/virtual`. -`-Dembed-sources=true` embeds the editor's sources and serves them under -`/src` (and `/shaders` on GUI builds). `EffectCode <effect>` lists the current -backend's implementation files under `/virtual`, which Look opens; without the -option the command reports the sources as unavailable. It is off by default -everywhere; the esp32p4 image in particular has no room for them (~1.8 MB of -source against a 1.5 MiB app partition). +## Limits -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. +| | | +|---|---| +| panes | 64 (16 on the board) | +| columns | 16 (6 on the board), each at least 10 cells wide | +| rows a pane keeps | its tag and 2 | +| msize | 8192 offered | +| connections | 16 Unix+TCP, 16 QUIC | +| opens holding state | 64 | +| named mounts | 8 | +| command line | 1024 bytes; a held line or Edit block 1 MiB | +| tag text | 4096 bytes | +| regular expression | 512 operations; a step budget per search (~300 ms) | +| undo | 256 steps | +| log | 64 KiB ring; `msg` 256 bytes, `err` reason 200 | +| `pty/run` output | 64 KiB | +| a dump | 6 columns (so `Dump` of more fails `bad dump columns`) | +| file name component | 255 bytes | -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. +## Source and tests -`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. +`src/ninep/`: `tree.zig` (nodes, dispatch), `pane.zig`, `ctl.zig`, +`cols.zig`, `addr.zig`, `pty.zig`, `events.zig` (event and log), `screen.zig`, +`sources.zig`. The engine is cloud9's `fs.Server` (`src/9p.zig`); transports +are in `src/9p_io.zig`; `src/fs.zig` holds host access, mounts and +resolution. `zig build fs-test` drives real sessions with `test/ninep.py`; +`zig build 9p-test` checks the engine budgets. |
