summaryrefslogtreecommitdiff
path: root/docs/fs.md
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-29 19:25:00 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:17 -0300
commit57b30ba3e38153a4446626449b0fed5120da954c (patch)
tree7b9381327a05791181a855bd8d4ba3cfb4df5301 /docs/fs.md
parent0fd908eea63d04886b269438aa7529d3dd422256 (diff)
downloadpardes-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.md1689
1 files changed, 603 insertions, 1086 deletions
diff --git a/docs/fs.md b/docs/fs.md
index ca02082b..e4750690 100644
--- a/docs/fs.md
+++ b/docs/fs.md
@@ -1,1142 +1,659 @@
# Filesystem
-Every native session serves 9P2000 on a Unix socket. Pane shells receive
-`PARDES_PID` (the editor's process id), `PARDES_9P` (socket path) and
-`PARDES_PANE` (pane serial). The socket is
-`$XDG_RUNTIME_DIR/pardes-9p-<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.