summaryrefslogtreecommitdiff
path: root/docs
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-30 23:04:26 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:17 -0300
commit5e72bfc34cc97d12be5185930135b55c2eb001b4 (patch)
treed81daad17e81789e5ce583f22280d6cc368d6ab6 /docs
parent074f113fa95a875a6bf17196f8e46e69806a247c (diff)
downloadpardes-5e72bfc34cc97d12be5185930135b55c2eb001b4.tar.gz
pardes-5e72bfc34cc97d12be5185930135b55c2eb001b4.zip
The reference is fs.md's per-file semantics, errors and limits in Typst, with the settings table, and says what the pane ctl's Left, Right, Up and Down do
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'docs')
-rw-r--r--docs/config.md202
-rw-r--r--docs/fs.md763
-rw-r--r--docs/macos.md4
-rw-r--r--docs/open-questions.md4
-rw-r--r--docs/typ/reference.typ721
5 files changed, 725 insertions, 969 deletions
diff --git a/docs/config.md b/docs/config.md
deleted file mode 100644
index 23355b06..00000000
--- a/docs/config.md
+++ /dev/null
@@ -1,202 +0,0 @@
-# Configuration
-
-## The startup file
-
-Native builds read one command file, `init`, from the per-user `pardes`
-configuration directory:
-
-- Unix: `$XDG_CONFIG_HOME/pardes/init` (only an absolute `XDG_CONFIG_HOME`
- counts), else `~/.config/pardes/init`.
-- macOS: `$XDG_CONFIG_HOME/pardes/init` when set, else
- `~/Library/Application Support/pardes/init`.
-- Windows: `%LOCALAPPDATA%\pardes\init`, else
- `%USERPROFILE%\AppData\Local\pardes\init`.
-
-The browser build has none. A file over 1 MiB or unreadable counts as absent.
-
-Each line is one builtin, spelled as it would be executed in pardes:
-
-```text
-Theme orchard
-Font DejaVuSansMono-Regular
-TaglineSize 82
-Shell zsh
-Wrap
-```
-
-A builtin that takes no argument matches only as the whole line (`Kill` runs,
-`Kill something` does not); one that takes an argument takes the rest of the
-line. Blank, unknown, malformed or failing lines are ignored silently and do
-not stop later ones. Text that is no builtin is not run as a shell command
-(`Exec ...` still is). Key bindings are compile-time choices in
-`src/config.zig`; `init` does not remap them.
-
-`Config` (`SPC f c`) opens `init` itself in a pane, or goes to its pane when
-it is open. With no file there yet, the pane is named for it, empty, and
-Save writes it, making its directory first. `DumpConfig` opens a
-`+DumpConfig` pane with every live setting, each line the word that sets
-it and its value (`Shell /bin/bash`, `PanelSlide off`, `LocationsConfig …`)
-and a `# Lift unsupported` comment for one the frontend cannot show. After a blank line
-come what is in effect but set by no word, such as the last shell spawned
-and the font in use, and the startup path (a right click opens it), each a
-`#` line. A line starting with `#` is a comment wherever a line runs (the
-init file, a ctl, an exec), so the report can be written back as is. The
-root `ctl` file reads the settings back in the words a write takes
-([fs.md](fs.md#the-root-ctl)).
-
-## Settings
-
-A setting that chooses among words (`on`/`off` switches, `Placement`,
-`BootShell`, `Crt`, `Bloom`, `Vignette`, `Grain`, `Lift`, `Motion`,
-`ShaderAnimation`) steps to its next value when given bare, as its word
-clicked in a tag does; a value it does not take is refused, naming those it
-does. `/commands` lists every builtin and its values.
-
-| setting | default | |
-|---|---|---|
-| `Theme <name>` | `orchard` | names as `Themes` (`SPC t t`) lists them ([themes.md](themes.md)); `NextColor` walks the ring |
-| `ThemeFile <path>` | | a `.zon` theme, relative to the config directory; reloads live when saved |
-| `FocusTint` | on | tint the focused pane's and column's tags |
-| `SyntaxBold` | off | bold syntax keywords |
-| `Verbose` | on | a builtin announces its name on the message row |
-| `MessageAnimation` | on | messages ease in and dissolve |
-| `MessageLinger`, `MessageFall`, `MessageDissolve` | 800, 180, 150 | milliseconds, at most 60000 |
-| `Placement acme\|pardes` | `acme` | where new panes go ([tags.md](tags.md#where-new-panes-go)) |
-| `BootShell keep\|replace` | `keep` | `replace` closes the untouched lone shell a dragged document lands beside |
-| `LookWord search\|list` | `search` | a looked-at word selects its next place, or lists all in `+Search` |
-| `Shell <name or path>` | `$SHELL`, else the login shell, else `/bin/sh` | the shell the next terminal runs; a bare name is looked for in the usual bin directories, not `$PATH`; bare `Shell` returns to the default |
-| `DumpDir <dir>` | `$XDG_DATA_HOME/pardes`, else `~/.local/share/pardes` | where `Dump` writes; `~/` is home; bare returns to the default |
-| `TreeContext` | off | sticky declaration headers in a source pane (per pane, dumped) |
-| `TreeContextTagStyle` | on | draw those headers in the tagline style |
-| `LocationsConfig ...` | | Search, Grep and LSP result layout (below) |
-| `Wrap`, `Colors`, `Tagbottom`, `Debug` | | toggles |
-| `Font <name>[:<size>]`, `Fonts` | | SDL and macOS only; size 8-72 (pixels in SDL, points on macOS) |
-| `TaglineSize <1-100>` | 82 | tagline face, percent; SDL and macOS |
-| `WindowOpacity <0-100>` | 100 | SDL only: everything but text and the cursor |
-| `Ligatures` | on | SDL only; macOS draws CoreText's own |
-| `Pet cat\|frog\|off` | off | SDL only: a sprite in the workspace tag's blank space |
-
-`Tty9p` (`SPC n 9`) is described in [v9fs.md](v9fs.md); the
-`PARDES_V9FS_HELPER` variable points development builds at the helper.
-
-### Terminals
-
-Ctrl-B switches a terminal between raw input and editor mode. Plain Escape
-at a detected shell prompt hops back to the previous pane; other keys,
-Ctrl-O, Ctrl-W and modified Escape included, go to the program. `Mode` in the
-tag returns to editor mode in place. Ctrl-V types the yank register and
-Ctrl-Shift-V the desktop clipboard, through bracketed paste when the program
-asked for it; neither reaches the program as a keystroke.
-
-`Filter` in a terminal's tag maps its ANSI colours through the theme,
-keeping each foreground at least `tty_filter_min_contrast` (WCAG 1.5, in
-`src/config.zig`) against its background.
-
-### Location results
-
-`LocationsConfig` with no argument prints the current settings as a line
-that can be run again; with fields it changes only those:
-
-```text
-LocationsConfig context:5 tscontext:on tslocations:off layout:stacked
-```
-
-- `context` (0): source lines shown above and below each match.
-- `tscontext` (off): include the enclosing tree-sitter declaration headers.
-- `tslocations` (on): show a location on each declaration header.
-- `layout` (`stacked`): the location on its own line; `inline` puts it
- beside the source, padded in groups of eight matches.
-
-An invalid field rejects the whole line; a repeated field's last value wins.
-The settings survive Dump and Restore. Source analysis is cached for 64
-files and 64 MiB.
-
-### Effects
-
-Panel transitions, one at a time; running the active one again turns it
-off: `PanelSlide`, `PanelZoom`, `PanelDissolve`, `PanelAscii`,
-`PanelVertical`, `PanelEdges`, `PanelFall`, `PanelWave`, `PanelCurtain`,
-`PanelScramble`, `PanelType`. All start off; the web shell has none.
-
-Scene passes (SDL GUI, and a GUI attached to a detached session), each at a
-level 0-3 (`on` is 2), all off by default: `Crt`, `Bloom`, `Vignette`,
-`Grain`. `Shader <file.glsl>` adds a Shadertoy file written for ghostty to
-the chain (`Shader off` removes it; it recompiles when saved);
-`ShaderAnimation off|on|always` says when the chain animates by itself.
-
-The focused pane can stand off the page: `Lift shadow|rim|auto|off`,
-`InactiveDim <percent>`, `Motion off|crisp|smooth|bouncy|playful`
-(default `smooth`), `SelectionGlow`, `HoverGlow`, `Occlusion`, `Parallax`,
-`JumpTrail`, `ChipShadow`, `ThumbFlash`, `CursorBlink`, `GripWidth <50-300>`.
-Most are GUI-only; `InactiveDim` works everywhere, and `JumpTrail`,
-`ChipShadow` and `ThumbFlash` are the terminal's. No effect may lower the
-contrast of text, the selection or a focus indicator
-([effects.md](effects.md)).
-
-`EffectCode <effect>` lists that effect's sources under `/virtual` when the
-build embeds them (`-Dembed-sources=true`). `zig build shaders` refreshes
-the committed SPIR-V with its GLSL.
-
-### Look preview
-
-Resting the pointer on text for about 32 ms (`look_preview_delay_frames`, 2
-frames) tints what a right click would look at, with no other effect. Set
-`look_preview_delay_frames` to `null` in `src/config.zig` to turn it off.
-
-## Themes from files
-
-`ThemeFile themes/mine.zon` loads a complete theme; a malformed save keeps
-the last good one, and `Theme <name>` stops the watch. `DumpThemes` writes
-every compiled theme to `<config dir>/themes/builtin/<name>.zon`: copy one,
-change its `.name`, and edit. The format is `pardes.Theme` as
-`std.zon.stringify` writes it; [themes.md](themes.md) describes each role.
-
-## Dumps
-
-`Dump` writes `pardes-<date>-<time>.zon` (UTC) in `DumpDir`, creating the
-last directory if it is missing (a missing parent fails `no such
-directory`); `$PARDES_DUMP` overrides the file. `Restore <path>` looks for a
-relative path in `DumpDir`, then in the directory pardes started in; bare
-`Restore` takes the last dump this session wrote. `pardes -l <dump.zon>`
-starts from one.
-
-A dump keeps panes, columns and their tags, each text pane's selection, the
-theme, and the settings that differ from a fresh session's; the font stays
-the frontend's. A terminal comes back with its last MiB of output as
-history, a `── restored history ──` line, and a new shell in its old
-directory; a command pane comes back finished (`exit ?` if it was running).
-Undo history and REPL bindings are not kept. A dump holds at most 6 columns.
-
-## Crash records
-
-A panic appends two lines to `crashes` beside `init` (build, time, platform,
-pid; then the panic message) before printing to stderr. There is no stack
-trace in it: collecting one from a panic handler can hang the process. The
-trace stays on stderr.
-
-## Build options
-
-`zig build --help` lists the options for the selected platform.
-
-| option | values | default |
-|---|---|---|
-| `-Dplatform` | `tty`, `gui`, `web`, `macos`, `esp32p4` | absent: the tty and SDL shells together, installed into `~/.local` |
-| `-Dstatic` | bool | `false` |
-| `-Dquic` | bool; 9P over QUIC with system OpenSSL 3.6+ | `false` |
-| `-Dmupdf` | bool | on natively, off for web and esp32p4 |
-| `-Djpx` | bool; JPEG 2000, and with it scanned PDFs | `true` |
-| `-Dtree-sitter` | `disabled`, `zig`, `minimal`, `full` | `full` natively, `zig` for web, `disabled` for esp32p4 |
-| `-Dembed-sources` | bool; serve the sources under `/src` | `false` |
-| `-Dstamp-commit` | bool; the git commit in `--version` and crash records | on for release builds and the `~/.local` install |
-| `-Dtheme-animation` | bool | on except for esp32p4 |
-| `-Dworkspace-tag` | bool; draw the workspace tag row | on except for macOS, whose menu bar carries it |
-| `-Dprebuilt-shaders` | bool; embed the committed SPIR-V | on for a bare `zig build`, off with `-Dplatform` |
-| `-Dtracy` | path to a Tracy checkout | off |
-| `-Dmacos-identity` | codesigning identity for `pardes.app` | `-` (ad-hoc) |
-| `-Ddump` | a `dump.zon` to embed in the web shell | none |
-| `-Dtest-filter` | run only tests whose name contains it | none |
-| `-Dtest-rebuild` | bool; fresh Zig test compilation | `false` |
-| `-Dhelix-harness` | reference executable for live differential tests | `HX_HARNESS`, else `hx-harness` on PATH |
-| `-Desp32p4-cols`, `-Desp32p4-rows` | the board's grid | 56, 14 |
-
-The version comes from `build.zig.zon`'s `.version`.
diff --git a/docs/fs.md b/docs/fs.md
deleted file mode 100644
index 85be803d..00000000
--- a/docs/fs.md
+++ /dev/null
@@ -1,763 +0,0 @@
-# Filesystem
-
-Every native session serves 9P2000 (not .u, not .L) as a control
-filesystem, in acme's manner: panes, columns, tags and the session are files.
-This page is the reference for what each file does. The served
-[`/README`](../src/fs-help.txt) is its one-screen summary.
-
-## Connecting
-
-The socket is `$XDG_RUNTIME_DIR/pardes-9p-<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)).
-
-Pane shells get `PARDES_PID` (the editor's pid), `PARDES_9P` (the socket)
-and `PARDES_PANE` (their pane's serial).
-
-**Forwarding.** `pardes FILE` run in a pane (a live `PARDES_PID`, with
-`PARDES_9P` and `PARDES_PANE`) writes FILE to that pane's `look` and returns
-at once, as acme's `B` does. A FILE not there yet (`pardes notes/new.txt`)
-opens a new pane named for it, empty, and its Save creates the file, making
-its directories first when they are not there either. A session that
-answers never gets a nested editor in its pane: what it refuses (a bad
-name, a pane it has not) is printed, `pardes: <file>: <why>`, and the
-launch exits 1, leaving no pane behind. A missing `PARDES_9P`/`PARDES_PANE`,
-or a session that does not answer, starts a separate editor instead. Bare
-`pardes` in a pane refuses and names `--nested`.
-
-`--wait` (`-w`) returns when the pane that shows FILE is deleted (exit 0) or
-the session goes away (exit 1), as acme's `E` does. Use
-`EDITOR='pardes --wait'` (`GIT_EDITOR` follows `EDITOR`), so fish's Ctrl-O,
-`git commit` and `crontab -e` read the file after you close its pane.
-`--nested` runs a separate session whose shells do not forward to it.
-
-**Clients.**
-
-```sh
-9p -a "unix!$PARDES_9P" read index # plan9port, no mount
-9ns --unix "$PARDES_9P" -- sh -c 'cat "$NINE_MOUNT/index"' # private mount
-9ns --mntgen # the whole registry at /mnt/9p
-```
-
-Under `9ns --unix` 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 `9p write` always opens with OTRUNC, so `echo x | 9p write
-pane/3/body` replaces the whole body where acme would append. Append with
-`>>` through a mount.
-
-For [Linux v9fs](https://www.kernel.org/doc/html/latest/filesystems/9p.html)
-use `version=9p2000,cache=none,access=any`, `trans=unix` (or `trans=tcp`
-with `port=`), `uname`, `dfltuid` and `dfltgid` for the local user, and an
-empty `aname`.
-
-**Listeners.** `--9p-tcp='tcp!127.0.0.1!5640'` adds TCP;
-`--9p-quic='quic!127.0.0.1!5641'` adds QUIC (build with `-Dquic=true`,
-OpenSSL 3.6+; ALPN `pardes-9p`, an ephemeral TLS identity, no peer
-verification). Addresses are numeric IPv4/IPv6; port 0 picks one; `/listeners`
-reads them back. Every connection has full session access, `/os` included,
-and TCP is unencrypted: use loopback. Unix and TCP share 16 connection slots;
-a 17th client's Tversion gets `too many connections` (and the log
-`err - 9p: too many connections (N turned away)`). QUIC has 16 of its own.
-Plan9port and v9fs need a userspace bridge for QUIC.
-
-**Look paths and mounts.** Look resolves the OS filesystem first, then the
-editor's own tree. Explicit paths skip that search:
-
-| Look path | Meaning | Served path |
-|---|---|---|
-| `/n/os/proc/self` | the host filesystem | `/os/proc/self` |
-| `/n/self/pane/2/body`, `/virtual/pane/2/body` | this session's tree | `/pane/2/body` |
-| `/virtual/src/pardes.zig` | sources embedded with `-Dembed-sources=true` | `/src/pardes.zig` |
-| `/n/peer/pane/2/body` | a mounted session | the peer's `/pane/2/body` |
-
-`--mount=peer=work` or `Mount peer <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.
-
-A session may open its own tree through a mount (a Look at
-`$m/pane/2/body` from the editor serving `$m`): requests are answered on the
-connection's task while the editor waits in its syscall. Through QUIC that
-still hangs.
-
-## The tree
-
-```
-/README 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)
-```
-
-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.
-
-`/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`.
-
-## Rules for every file
-
-**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
-an `Edit` command pardes leaves out (`w`, `e`, `r`, `|`: `w is a sam command
-pardes's Edit leaves out`), EIO; no refusal reads as EOPNOTSUPP. The `err` record has the words; a shell sees
-only the errno, most often `Invalid argument` or `Input/output error`.
-
-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 64 KiB, 65536 negotiated,
-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`),
-unless it is a multiple of 4096 bytes: that is where a writer's buffer (stdio,
-a mount's page cache) filled and cut a line, so its tail waits for the next
-write or the close. An `Edit` whose `{` or
-`a`/`c`/`i` text is still open waits for the next write on that open. A
-line held to the close (a 4096-multiple write's tail, an `Edit` block never
-ended) runs there, and its failure is only in the log, as its `err` record
-(``err <serial> ctl: unmatched `{'`` or `a, c or i text not ended by a .
-line`): the close itself reports no error, and the write that sent it had
-already succeeded. A script that needs a line's result ends the write with
-a newline, so the line runs, and fails, with its write. 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;
-nothing when it did none of these (`Newcol` at `/tagexec`). 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. One connection holds at most 128
-reads at once; the next is refused `too many reads waiting: 128`. Every
-held read is on an open that keeps state, and those are 64 in the session
-(above), so 64 is the real cap on reads held at once, through one
-connection or many. A mount (9ns) is one connection, and it keeps
-answering everything else beside its held reads. Through a FUSE mount bash's
-`read -t` cannot time out: wrap the loop in `timeout N`.
-
-**Stats.** A file whose text is kept has its real length: a pane's
-`body`, `tag`, `name`, `ctl`, `sel` and the range and flag files, the
-workspace's `tag`, the root `ctl`, `status`, `commands`, `README`, the
-`look`/`exec` answers, `focus`; `event` and `pty/data` the next record's,
-zero when none waits; `log` what an open would freeze. A view generated by
-each read stats 0, as acme's do: `screen`, `data`, `xdata`, `index`,
-`layout`, `recent`, `listeners`, `pane/new`. Read those to the end rather
-than trust a length (`cat` does). 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, on the line as written:
-its leading blanks are its own (` return x` finds that indented line, and
-a diff's blank context line ` ` is a look too); only its newline and `\r`
-go. An `exec` line's blanks at either end are trimmed.
-
-- 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. A leading `~`
- is the home directory ($HOME, else the passwd entry's; `~user` that
- user's) here and wherever a path is typed (`name`, `Save`, `ThemeFile`,
- `DumpDir`, `Restore`, `pardes '~/x'`), even beside a file named `~`: write
- `./~` for that. A path to no file is a miss, as a search that finds
- nothing is: said on the message row and logged as an `err`, the write
- still answered. A file that is there but will not open (no permission to
- read it, say) fails the write, with why.
-- a relative path is resolved where the click was, then where you have
- been: first against the looking pane's own directory, then against the
- directory of each pane in the jump list, most recent first, each
- directory tried once; the first that names a file (or a pane open on
- that path) wins. A pane never visited (not in the jump list) is not
- searched. `./x` and `../x` are the looking pane's directory's alone.
-- `@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.
-- in a diff pane, a whole line of the diff is a look at the `path:line` it
- names, as a right click on its first column is
- ([tags.md](tags.md#reviewing-diffs)).
-- 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>:99 has no line 99`
-for a line past a file's end, `<path> has no page 99` for a PDF's page;
-`<path>: no match for regexp` or `<path>: address out of range` when an
-address fails), opens nothing, and leaves `look` reading empty; the write
-succeeds. A path too long to repeat whole gives up its middle to `…`.
-
-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"`, and so is one that takes none
- written with one (`Config extra`).
-- a line starting with `#` is a comment, as in a shell: it runs as
- nothing, silently, here and in every ctl.
-- the language server's words ask about a file pane's text at its cursor
- (set it with `addr` and `dot=addr` first): `Hover` fills `+Hover`,
- `Rename new` renames the symbol in the file and says how many ranges it
- changed (on the message row, and so in /log) without listing them; one
- that finds nothing to rename fails; a rename the server spreads over
- other files lists them in `+Search`,
- `Diagnostics` and `Symbols` list the file's in `+Search`, `Lspinfo` says
- which server serves the file and its state, and `Lspwhy` narrates the
- last query step by step (in `+Lsp`), to tell why it found nothing. The
- write returns once the answer is in; a pane with no file is refused.
-- 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`, `Tab`, `Indent`, `Local`,
- `Incl`, `Abort`) 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. A reused pane's `body` keeps every earlier run above a
- `% <line>` row naming each command, so to take only the last run's
- output read from after the last `% ` row:
- `awk '/^% /{out=""; next} {out = out $0 "\n"} END {printf "%s", out}' body`.
- From a column's or the workspace's tag (or `/tagexec`, `col/<n>/exec`)
- a command always runs as a command pane, in the session's directory. 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
-(with no pane at all, in the session's directory: a look opens its file,
-making a column as `New` does, and an exec runs its command there);
-`/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, named
-or not, it says `Kill: nothing running` and the write succeeds; words that
-name none of what runs fail it (`Kill: no running command has that first
-word`). 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)), logs `dump <path>`, and adds a `Restore
- <path>` word naming it to the workspace tag (the last dump's, replacing
- an earlier one's). `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, the jump list 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
-
-`/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.
-
-A session holds 16 columns (`no space for a column: 16 max`, ENOSPC), each at
-least 10 cells wide, so 16 wants a window 160 cells wide or more. `Newcol`
-halves the column it is run from, and one under 20 cells does not split
-(`this one is too narrow to split`): reach 16 by writing `Newcol` to the
-widest column's `exec` each time, not to the root's, which keeps halving the
-one just made. `Newcol` is refused as well when a narrower column would
-wrap its panes' tags onto more rows and leave one under its tag and two
-rows (`Newcol: no space for a column: the panes' tags would not fit`); a
-refused `Newcol` logs its `err` alone and uses up no column serial.
-
-`/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>`.
-
-`tag` files (`/tag`, `/col/<n>/tag`, `/pane/<n>/tag`) read the whole tag,
-with no newline after it.
-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 (`no space: over 4096 bytes`, ENOSPC; the log says `err <serial> tag: no space: ...`). All three kinds
-take the same checks, whole or not at all, and a `>` whose write is refused
-changes nothing: its truncation is done with the write that fits, or at the
-close (or a read) when none came. Each write stands on its own: in a `>`
-cut into several writes, those taken before a refused one stay in the
-tag; only the refused write changes nothing. A clear is an ordinary edit
-and `u` in the tag undoes it. [tags.md](tags.md) covers tags on screen.
-
-## Panes
-
-**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. A
-`pane/new` whose column is full takes its rows from a pane in another column
-that has them, last column first, as acme does, and is refused only when
-no pane anywhere can give them.
-`rmdir /pane/<n>` closes the pane, unsaved or not.
-
-**`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 takes any byte a file name can
-hold, blanks, control bytes and bytes not UTF-8 included, so what `name`
-reads writes back as it was; refused (EINVAL) are only a second line and a
-NUL (`bad character in file name: a NUL`). Up to 255 bytes a component.
-The ctl word `name x` takes all after its one blank; a second blank there
-is refused rather than read as the name's first byte. A directory (`/`, `~`, `foo/`) is no file name: refused, EISDIR
-(`name: /home/u is a directory, not a file`).
-
-**`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 as typed
-keys, never a paste: no bracketed-paste marks around it, even when the
-program asked for them, so a newline in it is Enter. A PDF's
-body is the text layer of the page shown, read-only: it is no text of the
-pane's, so `addr`, `data`, `dot` and a `file:<addr>` look do not address
-it (a PDF's `:<n>` is its page); images and PDFs take no write
-(`this pane has no text`).
-
-**`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).
-
-**`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.
-
-**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:
-
-- the pane builtins: `Del` (`Del k`/`Del j`, or `DelAbove`/`DelBelow`, give
- its rows to the pane above or below), `Save [path]` (making the
- directories the file goes in first, whichever name it writes), `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.
-
-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.
-
-### Addresses and data
-
-`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; a pair is checked as an address
-is, `addresses out of order` (`5 2`) or `address out of range` (past the
-text), never clamped. `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 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.
-
-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.
-
-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.
-
-Refusals: `bad address syntax`, `no match for regexp`, `address out of
-range`, `addresses out of order` (`#100,#50`), `bad regular expression`.
-
-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.
-
-### Regular expressions
-
-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:
-
-- 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 with `^` or none (`^def|^ `
- works, `^def|x` is refused: `an alternation anchors every branch with ^
- or none`). A `$` does not count: `foo$|bar` is fine.
-- 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), as long as no other client or keystroke edits
-the pane between them: such an edit ends the step, and the open's next
-write starts another. 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. One open reads a pane's `event` at a time,
-as acme's is one window's: a second open for reading fails `file in use`
-(EBUSY) until the first is closed.
-
-- 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 last record of a write needs no newline), and an
-empty one (`MX12 12`) on the word or file name a click there expands to;
-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, fewer refused `invalid winsize: at least 2 rows`; at most 4096 a side), `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
-```
-
-A follower the ring outran reads `lost N` first. A Restore hangs the
-follower up: dial again and read from `restore <path>`.
-
-| record | when |
-|---|---|
-| `new <serial> <name>`, `del`, `rename`, `save` | a pane made, closed, renamed (a terminal's too, as its shell changes directory), saved (`Save path` of a copy: `save <serial> <path>`, the path written) |
-| `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 |
-
-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.
-
-## Other files
-
-- `/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, PDFs and images, most recent first, `open
- <path>` or `closed <path>`; kept in `$XDG_STATE_HOME/pardes/recent`.
- `Recent` shows them in a pane, an open one at its dot now and a closed
- one at its last (a PDF's is its page); a look at a row reopens it there.
-- `/status`: `pid`, `version`, `panes`.
-- `/os/`: existing regular files take read, write and truncation to zero;
- create, remove, rename and mode changes are refused as not permitted
- (`permission denied`, EACCES through a mount); 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`.
-
-## Limits
-
-| | |
-|---|---|
-| 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 | 65536 (64 KiB) 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 |
-| file name component | 255 bytes |
-
-## Source and tests
-
-`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.
diff --git a/docs/macos.md b/docs/macos.md
index 5512edbd..3ee55d0a 100644
--- a/docs/macos.md
+++ b/docs/macos.md
@@ -462,7 +462,7 @@ after `cd` and after an idle tick.
Nested launches use the same inherited 9P address and pane serial as other
native hosts. They do not inspect ancestor processes or executable names.
-See [the control filesystem](fs.md) for paths and transports.
+See [the control filesystem](typ/reference.typ) for paths and transports.
Two more things this host cannot inherit from its launcher, because a `.app`
has none:
@@ -1140,7 +1140,7 @@ used for the table and is not folded into the direct-path claim.
`src/macos.zig` never calls `takeAttach`, so the word does nothing at all and
says nothing either. Wiring it up means a unix-socket frontend loop beside
the AppKit one, which is `src/detached/client.zig`'s job in the tty and SDL
- shells; see `docs/detached.md`.
+ shells; see `docs/typ/building.typ`.
- **IME and marked text.** Only finished characters reach `pardes_key`, so a
dead key composes nothing and Option is Alt rather than a compose modifier.
Real composition means implementing `NSTextInputClient` *and* giving the core
diff --git a/docs/open-questions.md b/docs/open-questions.md
index 3f365ce9..f1ffb544 100644
--- a/docs/open-questions.md
+++ b/docs/open-questions.md
@@ -8,10 +8,10 @@ picked up without redoing the research. None is open now.
- **Where an unknown command word runs** (2026-09-28): at a terminal idle at
its prompt it is typed into that shell; anywhere else it runs as a command
pane, a terminal whose child is `Shell -c <line>`, reporting `exit N` in
- its tag and the log ([fs.md](fs.md#look-and-exec)). Chosen over acme's
+ its tag and the log ([the reference](typ/reference.typ)). Chosen over acme's
process writing into `+Errors` because interactive programs keep working
without a streaming runner, and over checking `PATH` before typing into a
shell, which misjudges builtins, aliases and functions.
- **Where an exec from a code file goes** (2026-09-28): to a REPL bound for
- its language, when one is ([fs.md](fs.md#repls)). Chosen over a per-file
+ its language, when one is ([the reference](typ/reference.typ)). Chosen over a per-file
or per-directory binding, which would need a place to keep and show it.
diff --git a/docs/typ/reference.typ b/docs/typ/reference.typ
new file mode 100644
index 00000000..5fc2dd1d
--- /dev/null
+++ b/docs/typ/reference.typ
@@ -0,0 +1,721 @@
+// The reference: every file the 9P control filesystem serves, what it
+// does, how it fails, and the limits. How pardes behaves on screen is the
+// guide's; recipes and traps are scripting's; how a mount cuts writes and
+// the listeners are building's.
+#import "style.typ": key, keys, btn, chord, word, tag, addr, file, cmd, doc, pairs
+
+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. Paths here are the served ones (#file("/pane/2/body")); through a
+mount they sit under the session's directory (`$m`; the scripting chapter
+says how to find it). The served #file("/README") is a
+one-screen summary of this page.
+
+= The tree <tree>
+
+```
+/README the one-screen summary (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)
+```
+
+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 #file("/pane/new") makes a pane (so `ls -l` and `find` are
+safe), only `rmdir` of #file("/pane/<n>") or an empty #file("/col/<n>")
+removes. Tcreate is refused everywhere.
+
+#file("/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
+#file("/index"), the log and a terminal's tag are escaped: a newline `\n`, a
+backslash `\\`, a byte that is not UTF-8 `\xNN`.
+
+= Rules for every file <rules>
+
+*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
+#file("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's `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 an `Edit` command pardes leaves out (`w is a sam command pardes's Edit leaves out`), EIO; no refusal reads as EOPNOTSUPP.
+The `err` record has the words; a shell sees only the errno, most often
+`Invalid argument` or `Input/output error`.
+
+Not failures: a look that finds nothing (it answers nothing and logs one
+`err`), an Edit `x` that matches nothing, #word("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.* #file("look"), #file("exec"), #file("tagexec"), the three
+kinds of #file("ctl") and a column's #file("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 line runs once it is whole: a write's last line runs
+without its newline, except where a mount cut the write (the building
+chapter has the mechanics); end a write with a newline when its result
+matters. An `Edit` whose `{` or `a`/`c`/`i` text is still
+open waits for the next write on that open. A line held past 1 MiB is
+refused.
+
+*Answers.* Reading #file("look"), #file("exec") or #file("tagexec") answers
+the serials the last command made, or else the pane it acted on or
+focused, one a line; nothing when it did none of these (#word("Newcol") at
+#file("/tagexec")). 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:
+#cmd("exec 3<>$m/look; echo x >&3; cat <&3; exec 3<&-")
+
+*Snapshots.* #file("/index"), #file("/layout"), #file("/recent"),
+#file("/commands"), #file("/status"), #file("/listeners"), a read-only
+#file("ctl"), #file("/log"), #file("/screen") and a terminal's
+#file("body") freeze at the open, so one read in several chunks never
+splices two moments; open again for now. Such an open, or a #file("run"),
+#file("event") or #file("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 #file("log"),
+#file("event"), #file("pty/data"), #file("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.
+One connection holds at most 128 reads at once; the next is refused `too many reads waiting: 128`. Every held read is on an open that keeps state,
+and those are 64 in the session (above), so 64 is the real cap on reads
+held at once, through one connection or many. A mount (9ns) is one
+connection, and it keeps answering everything else beside its held reads.
+
+*Stats.* A file whose text is kept has its real length: a pane's
+#file("body"), #file("tag"), #file("name"), #file("ctl"), #file("sel") and
+the range and flag files, the workspace's #file("tag"), the root
+#file("ctl"), #file("status"), #file("commands"), #file("README"), the
+#file("look")/#file("exec") answers, #file("focus"); #file("event") and
+#file("pty/data") the next record's, zero when none waits; #file("log")
+what an open would freeze. A view generated by each read stats 0, as
+acme's do: #file("screen"), #file("data"), #file("xdata"), #file("index"),
+#file("layout"), #file("recent"), #file("listeners"), #file("pane/new").
+Read those to the end rather than trust a length (`cat` does). The qid
+version of #file("body"), #file("data") and #file("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 <look-and-exec>
+
+A line written to #file("look") is a #btn("B3") click on it, on the line as
+written: its leading blanks are its own (` return x` finds that
+indented line, and a diff's blank context line ` ` is a look too); only its
+newline and `\r` go. An #file("exec") line's blanks at either end are
+trimmed. The guide says what a look opens and where it looks for a relative
+path (#doc("tags", section: "looking")); here is what is particular to the
+files.
+
+- `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 leading `~` is the home directory (`$HOME`, else the passwd entry's;
+ `~user` that user's) here and wherever a path is typed (#file("name"),
+ #word("Save"), #word("ThemeFile"), #word("DumpDir"), #word("Restore"),
+ `pardes '~/x'`), even beside a file named `~`: write `./~` for that.
+- A path to no file is a miss, as a search that finds nothing is: said on
+ the message row and logged as an `err`, the write still answered. A file
+ that is there but will not open (no permission to read it, say) fails the
+ write, with why.
+- 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>:99 has no line 99`
+for a line past a file's end, `<path> has no page 99` for a PDF's page;
+`<path>: no match for regexp` or `<path>: address out of range` when an
+address fails), opens nothing, and leaves #file("look") reading empty; the
+write succeeds. A path too long to repeat whole gives up its middle to `…`.
+
+A line written to #file("exec") is a #btn("B2") click:
+
+- A builtin word runs (#file("/commands") lists them). A builtin that needs
+ its argument (#word("Msg"), #word("Mount"), #word("Find")) written bare is
+ `wrong #args in control message "Msg"`, and so is one that takes none
+ written with one (`Config extra`).
+- A line starting with `#` is a comment, as in a shell: it runs as nothing,
+ silently, here and in every ctl.
+- The language server's words ask about a file pane's text at its cursor
+ (set it with #file("addr") and `dot=addr` first): #word("Hover") fills
+ `+Hover`; `Rename new` renames the symbol in the file and says how many
+ ranges it changed (on the message row, and so in the log) without listing
+ them, fails when there is nothing to rename, and lists the files of a
+ rename the server spreads over several in `+Search`;
+ #word("Diagnostics") and #word("Symbols") list the file's in `+Search`;
+ #word("Lspinfo") says which server serves the file and its state;
+ #word("Lspwhy") narrates the last query step by step (in `+Lsp`). The
+ write returns once the answer is in; a pane with no file is refused.
+- acme's words run as pardes's where it has one (`Put` is #word("Save"),
+ `Delete` a #word("Del") that does not ask); the rest (`Get`, `Putall`,
+ `Snarf`, `Cut`, `Paste`, `Zerox`, `Sort`, `Load`, `ID`, `Send`, `Tab`,
+ `Indent`, `Local`, `Incl`, `Abort`) 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, run as the guide
+ says (#doc("tags", section: "command-panes")): typed into a terminal idle
+ at an empty prompt, else run in a command pane. #file("exec") reads back
+ the command pane's serial; the log says `run <serial> <line>` and `exit <serial> <N|?>`. A reused pane's #file("body") keeps every earlier run
+ above a `% <line>` row naming each command, so to take only the last
+ run's output read from after the last `% ` row:
+ #cmd("awk '/^% /{out=\"\"; next} {out = out $0 \"\\n\"} END {printf \"%s\", out}' body")
+ From a pane whose directory is gone nothing runs: `exec: <dir>: no such directory` (ENOENT).
+
+The root's #file("look") and #file("exec") act at the active pane and log as
+that pane's (with no pane at all, in the session's directory: a look opens
+its file, making a column as #word("New") does, and an exec runs its
+command there); #file("/pane/<n>/look") and #file("exec") at pane n;
+#file("/tagexec") and #file("/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 (#word("Undo"), #word("Msg"), #word("Save")) is refused
+at #file("/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. #word("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, named or not, it says `Kill: nothing running` and the
+write succeeds; words that name none of what runs fail it (`Kill: no running command has that first word`). #word("Kill") does not reach a
+REPL's code: use `sig INT` on its #file("pty/ctl").
+
+== Look paths and mounts <mounts>
+
+A look resolves the OS filesystem first, then the editor's own tree.
+Explicit paths skip that search:
+
+#pairs(
+ [`/n/os/proc/self`], [the host filesystem, served as #file("/os/proc/self")],
+ [`/n/self/pane/2/body`, `/virtual/pane/2/body`], [this session's tree, #file("/pane/2/body")],
+ [`/virtual/src/pardes.zig`], [sources embedded with `-Dembed-sources=true`, #file("/src/pardes.zig")],
+ [`/n/peer/pane/2/body`], [a mounted session: the peer's #file("/pane/2/body")],
+)
+
+`--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. #word("Mount") dials at once and fails if
+nothing answers (`dial failed: no answer`, `timed out`, `hung up`); with no
+dial it is `wrong #args`, and a dial that is no address `bad dial address`,
+both EINVAL. There are eight named mounts; `os` and `self` are reserved.
+#word("Unmount") refuses a mount a pane, a working directory or a pending
+Save still uses. Mounts are dumped.
+
+A session may open its own tree through a mount (a look at `$m/pane/2/body`
+from the editor serving `$m`): requests are answered on the connection's
+task while the editor waits in its syscall. Through QUIC that still hangs.
+
+= The root ctl <root-ctl>
+
+Reading #file("/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
+ #word("Placement"), #word("BootShell"), `Crt`). A value it does not
+ take is `bad value in control message; ...` naming what it takes;
+ #file("/commands") lists them. A setting the frontend cannot show is
+ refused (`Lift is GUI-only, invalid here`).
+- #word("Newcol") makes an empty column right of the keyboard's, halving
+ it. #word("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`.
+- #word("Exit") quits. While panes hold unsaved text it refuses once
+ (#doc("tags", section: "unsaved-panes")): 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. #word("Restore"), #word("Del"),
+ #word("Delcol") and a pane's `get` refuse the same way with their own
+ word.
+- #word("Dump") writes `pardes-<date>-<time>.zon` (UTC) in #word("DumpDir"),
+ logs `dump <path>`, and adds a `Restore <path>` word naming it to the
+ workspace tag (the last dump's, replacing an earlier one's).
+ `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.
+- `Kill [word...]` (above), `Mount name dial`, `Unmount name`, `Theme x`.
+- `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 (#word("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.
+
+== Settings <settings>
+
+A setting that chooses among words (`on`/`off` switches,
+#word("Placement"), #word("BootShell"), `Crt`, `Bloom`,
+`Vignette`, `Grain`, `Lift`, #word("Motion"),
+`ShaderAnimation`) steps to its next value when given bare, as its
+word clicked in a tag does. The same lines go in the startup file
+(#doc("config")).
+
+#pairs(
+ [`Theme <name>`], [`orchard`; names as #word("Themes") lists them, #word("NextColor") walks the ring (#doc("themes"))],
+ [`ThemeFile <path>`], [a `.zon` theme, relative to the config directory; reloads live when saved],
+ [`FocusTint`], [on: tint the focused pane's and column's tags],
+ [`SyntaxBold`], [off: bold syntax keywords],
+ [`Verbose`], [on: a builtin announces its name on the message row],
+ [`MessageAnimation`], [on: messages ease in and dissolve],
+ [`MessageLinger`, `MessageFall`, `MessageDissolve`], [800, 180, 150 milliseconds, at most 60000],
+ [`Placement acme|pardes`], [`acme`: where new panes go (#doc("tags", section: "new-panes"))],
+ [`BootShell keep|replace`], [`keep`; `replace` closes the untouched lone shell a dragged document lands beside],
+ [`LookWord search|list`], [`search`: a looked-at word selects its next place, or lists all in `+Search`],
+ [`Shell <name or path>`], [`$SHELL` when it is executable, else `/bin/sh`: the shell the next terminal and command pane run; a bare name is looked for in the usual bin directories, not `$PATH`; bare #word("Shell") returns to the default],
+ [`DumpDir <dir>`], [`$XDG_DATA_HOME/pardes`, else `~/.local/share/pardes`: where #word("Dump") writes; bare returns to the default],
+ [`TreeContext`], [off: sticky declaration headers in a source pane (per pane, dumped)],
+ [`TreeContextTagStyle`], [on: draw those headers in the tagline style],
+ [`LocationsConfig ...`], [the layout of #word("Grep"), search and language-server results (below)],
+ [`Wrap`, `Colors`, `Tagbottom`, `Debug`], [toggles],
+ [`Font <name>[:<size>]`, `Fonts`], [SDL and macOS only; size 8-72 (pixels in SDL, points on macOS)],
+ [`TaglineSize <1-100>`], [82: tagline face, percent; SDL and macOS],
+ [`WindowOpacity <0-100>`], [100; SDL only: everything but text and the cursor],
+ [`Ligatures`], [on; SDL only, macOS draws CoreText's own],
+ [`Pet cat|frog|off`], [off; SDL only: a sprite in the workspace tag's blank space],
+)
+
+`LocationsConfig` with no argument prints the current settings as a line
+that can be run again; with fields it changes only those:
+`LocationsConfig context:5 tscontext:on tslocations:off layout:stacked`.
+`context` (0) is the source lines shown above and below each match;
+`tscontext` (off) includes the enclosing tree-sitter declaration headers;
+`tslocations` (on) shows a location on each declaration header; `layout`
+(`stacked`) puts the location on its own line, `inline` beside the source,
+padded in groups of eight matches. An invalid field rejects the whole line;
+a repeated field's last value wins. The settings survive Dump and Restore.
+Source analysis is cached for 64 files and 64 MiB.
+
+*Effects.* Panel transitions, one at a time, running the active one again
+turning it off: #word("PanelSlide"), #word("PanelZoom"),
+#word("PanelDissolve"), #word("PanelAscii"), #word("PanelVertical"),
+#word("PanelEdges"), #word("PanelFall"), #word("PanelWave"),
+#word("PanelCurtain"), #word("PanelScramble"), #word("PanelType"); all
+start off, and the web shell has none. Scene passes (the SDL window, and
+one attached to a detached session), each at a level 0-3 (`on` is 2), all
+off: `Crt`, `Bloom`, `Vignette`, `Grain`.
+`Shader <file.glsl>` adds a Shadertoy file written for ghostty to the
+chain (`Shader off` removes it; it recompiles when saved);
+`ShaderAnimation off|on|always` says when the chain animates by itself. The
+focused pane can stand off the page: `Lift shadow|rim|auto|off`,
+`InactiveDim <percent>`, `Motion off|crisp|smooth|bouncy|playful` (default
+`smooth`), `SelectionGlow`, `HoverGlow`, `Occlusion`,
+`Parallax`, #word("JumpTrail"), #word("ChipShadow"),
+#word("ThumbFlash"), `CursorBlink`, `GripWidth <50-300>`. Most are
+the GUI's; #word("InactiveDim") works everywhere, and #word("JumpTrail"),
+#word("ChipShadow") and #word("ThumbFlash") are the terminal's. No effect
+may lower the contrast of text, the selection or a focus indicator.
+`EffectCode <effect>` lists that effect's sources under `/virtual` when the
+build embeds them.
+
+Resting the pointer on text for about 32 ms (`look_preview_delay_frames`,
+2 frames, in `src/config.zig`; `null` turns it off) tints what a
+#btn("B3") click would look at, with no other effect. A terminal's
+#word("Filter") keeps each foreground at least `tty_filter_min_contrast`
+(WCAG 1.5, `src/config.zig`) against its background.
+
+= Columns and tags <columns-and-tags>
+
+#file("/layout") has a line per column, left to right: `serial index x width current|notcurrent empty|full pane-serials...`, then `active <serial>` (the active column: where #file("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 (#doc("tags", section: "active-column")).
+#file("/index")'s last field is each pane's column serial.
+
+A session holds 16 columns (`no space for a column: 16 max`, ENOSPC), each
+at least 10 cells wide, so 16 wants a window 160 cells wide or more.
+#word("Newcol") halves the column it is run from, and one under 20 cells
+does not split (`this one is too narrow to split`): reach 16 by writing
+#word("Newcol") to the widest column's #file("exec") each time, not to the
+root's, which keeps halving the one just made. #word("Newcol") is refused
+as well when a narrower column would wrap its panes' tags onto more rows
+and leave one under its tag and two rows (`Newcol: no space for a column: the panes' tags would not fit`); a refused #word("Newcol") logs its `err`
+alone and uses up no column serial.
+
+#file("/col/<n>/ctl") takes #word("Delcol"), #word("Joincol"),
+#word("New") and #word("Tty") for that column; #file("/col/<n>/exec") is
+a click in its tag; `rmdir /col/<n>` closes an empty column (one with
+panes: ENOTEMPTY). Closing a column's last pane leaves it empty (the
+keyboard goes to its tag, #file("focus") reads empty, the log says only
+`del`); closing the session's last pane quits pardes. The log says
+`newcol <serial>` and `delcol <serial>`.
+
+#file("tag") files (#file("/tag"), #file("/col/<n>/tag"),
+#file("/pane/<n>/tag")) read the whole tag, with no newline after it. 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 (`no space: over 4096 bytes`, ENOSPC; the log says `err <serial> tag: no space: ...`). All
+three kinds take the same checks, whole or not at all, and a `>` whose
+write is refused changes nothing: its truncation is done with the write
+that fits, or at the close (or a read) when none came. Each write stands
+on its own: in a `>` cut into several writes, those taken before a refused
+one stay in the tag; only the refused write changes nothing. A clear is an
+ordinary edit and #key("u") in the tag undoes it.
+
+= Panes <panes>
+
+*Making and closing.* Opening #file("/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 a #word("New") from a
+tag would (#doc("tags", section: "new-panes")): 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,
+#word("New") or #word("Tty") alike. A #file("pane/new") whose column is
+full takes its rows from a pane in another column that has them, last
+column first, as acme does, and is refused only when no pane anywhere can
+give them. `rmdir /pane/<n>` closes the pane, unsaved or not.
+
+*#file("name")* reads the file name (a terminal's directory); a write
+renames the buffer (relative to the pane's directory) and marks nothing
+dirty; #word("Save") then writes under the new name. A name takes any byte
+a file name can hold, blanks, control bytes and bytes not UTF-8 included,
+so what #file("name") reads writes back as it was; refused (EINVAL) are
+only a second line and a NUL (`bad character in file name: a NUL`). Up to
+255 bytes a component. The ctl word `name x` takes all after its one
+blank; a second blank there is refused rather than read as the name's
+first byte. A directory (`/`, `~`, `foo/`) is no file name: refused,
+EISDIR (`name: /home/u is a directory, not a file`).
+
+*#file("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 as typed keys, never a paste: no bracketed-paste marks around it,
+even when the program asked for them, so a newline in it is Enter. A PDF's
+body is the text layer of the page shown, read-only: it is no text of the
+pane's, so #file("addr"), #file("data"), #file("dot") and a `file:<addr>`
+look do not address it (a PDF's `:<n>` is its page); images and PDFs take
+no write (`this pane has no text`).
+
+*#file("sel")* reads the selected text; a write replaces it.
+*#file("errors")* is write-only: text appended to the directory's
+`+Errors` pane (logged as `msg` records when no column has room).
+
+*#file("focus")* (root): a serial written gives that pane the keyboard and
+makes its column the active one (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.
+
+*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:
+
+- the pane builtins: #word("Del") (`Del k`/`Del j`, or
+ #word("DelAbove")/#word("DelBelow"), give its rows to the pane above or
+ below), `Save [path]` (making the directories the file goes in first,
+ whichever name it writes), #word("Collapse"), #word("Undo")/#word("Redo")
+ (256 steps each), `Find pat`, `Edit ...`, `Tty [shell]` (a new terminal
+ in its directory), #word("Delcol") for its column, and
+ #word("Left")/#word("Right")/#word("Up")/#word("Down"), which give the
+ keyboard to the pane beside it that way (#key("Ctrl-w") with
+ #keys("h", "l", "k", "j")), up from a column's top pane to its tag.
+- `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 #file("exec"), `del` is a command line.
+
+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
+#word("Save") warns once.
+
+== Addresses and data <addresses>
+
+#file("addr"), #file("dot") and #file("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; a pair is
+checked as an address is, `addresses out of order` (`5 2`) or `address out of range` (past the text), never clamped. #file("addr") names where
+#file("data") reads (to the end of text) and the range #file("xdata")
+reads; #file("dot") is the selection (moving it scrolls the pane there);
+#file("limit") bounds the end of a forward search and reads empty until
+set. Truncating #file("dot") empties it, truncating #file("limit") lifts
+it.
+
+- A write to #file("data") or #file("xdata") *replaces* the #file("addr")
+ range (`>` and `>>` alike); `: > data` deletes it. Truncating
+ #file("data") is pardes's own (acme ignores OTRUNC there); only
+ truncating #file("body") empties the buffer.
+- A write leaves #file("addr") just past what it wrote, so a second
+ `echo x > data` inserts after the first: write #file("addr") before each
+ replacement. A read of #file("data")/#file("xdata") moves #file("addr")
+ past what it read.
+- #file("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 #file("limit").
+- A failed address leaves *no address*: #file("addr") reads empty and
+ #file("data")/#file("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.
+
+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 #file("addr"), or just past the last
+#file("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 #word("Recent"), `+Search` or the #word("Jumplist")
+spells a range `12:5-14:2` (through 14:2 inclusive) and #file("addr")
+takes it as such.
+
+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.
+
+Refusals: `bad address syntax`, `no match for regexp`, `address out of range`, `addresses out of order` (`#100,#50`), `bad regular expression`.
+
+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.
+
+== Regular expressions <regexp>
+
+Patterns are 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 #key("s") and #key("S"). The
+ceiling:
+
+- 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 with `^` or none (`^def|^ `
+ works, `^def|x` is refused: `an alternation anchors every branch with ^ or none`). A `$` does not count: `foo$|bar` is fine.
+- 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>
+
+`Edit <sam commands>` on a pane's #file("ctl") or #file("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:
+#cmd("printf 'Edit ,x/foo/{\\ni/</\\na/>/\\n}\\n' > $p/ctl")
+
+== Flags and undo <flags>
+
+#file("dirty"), #file("mark") and #file("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. #file("/index")'s dirty flag is #file("dirty"); a `+New` scratch
+reads 1 but holds up nothing under 100 bytes.
+
+The writes of one open of #file("data"), #file("xdata") or #file("body")
+are one undo step (so a multi-line `printf` is one), as long as no other
+client or keystroke edits the pane between them: such an edit ends the
+step, and the open's next write starts another. To make a loop of opens
+one step: `echo 1 > mark` (a point now), `echo 0 > mark`, the writes,
+`echo 1 > mark`.
+
+== event <event>
+
+Holding #file("event") open takes the pane's look and execute clicks: they
+come to the reader as records instead of acting, as do lines written to
+the pane's own #file("look")/#file("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. One open
+reads a pane's #file("event") at a time, as acme's is one window's: a
+second open for reading fails `file in use` (EBUSY) until the first is
+closed.
+
+- 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 last record of a write needs no newline),
+and an empty one (`MX12 12`) on the word or file name a click there expands
+to; 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
+#file("event") that writes its own pane's #file("exec") gets its command
+back as a record: run it through #file("ctl") instead.
+
+== REPLs <repls>
+
+`Repl python` on a terminal's #file("ctl") (or in its tag) binds it as
+that language's REPL: a #btn("B2") click or #key("Tab") on a `.py` body
+then types the text into the REPL instead of running it; builtin words,
+tag words, `Exec <text>` and #raw("@`cmd`") words still run. #word("Repl")
+takes the languages a code fence names (ada, bash, c, c_sharp, clojure,
+cpp, css, elixir, erlang, fortran, go, haskell, html, java, javascript,
+json, kotlin, ocaml, markdown, pascal, php, powershell, python, ruby, rust,
+scala, typst, zig) and aliases such as `py` and `sh`. Its tag and
+#file("ctl") line show its id, `python-a`. `Repl -` unbinds, bare
+#word("Repl") says the binding. With several bound, the pane asks (`ask <serial> repl a b`). Bindings are not dumped.
+
+A 9P #file("exec") is never sent to a REPL. A script either writes the
+event record `MX<q0> <q1>` to the `.py` pane's #file("event") (sent as the
+click would be), or writes the REPL's #file("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 <pty>
+
+Terminal panes also have #file("pty/"):
+
+- #file("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).
+- #file("pty/status"): one line, `cols rows busy`; busy is 1 while a
+ command runs or text is typed at the prompt.
+- #file("pty/ctl"): `winsize C R` (at least 2 rows, fewer refused `invalid winsize: at least 2 rows`; at most 4096 a side), `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).
+- #file("pty/run"): one line at the shell's prompt, answered on the same
+ open: #cmd("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.
+
+#file("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
+#file("pty/data").
+
+= The log <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. A follower
+the ring outran reads `lost N` first. A Restore hangs the follower up: dial
+again and read from `restore <path>`.
+
+#pairs(
+ [`new <serial> <name>`, `del`, `rename`, `save`], [a pane made, closed, renamed (a terminal's too, as its shell changes directory), saved (`Save path` of a copy: `save <serial> <path>`, the path written)],
+ [`newcol <serial>`, `delcol <serial>`], [a column made or closed],
+ [`msg <serial|-> <text>`], [the editor said something (not a builtin's own name under #word("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],
+)
+
+The serial is the pane the line ran at (the active pane for the root's
+#file("look")/#file("exec")), `-` for the root #file("ctl"),
+#file("/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.
+
+= Other files <other-files>
+
+- #file("/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.
+- #file("/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).
+- #file("/recent"): up to 200 files, PDFs and images, most recent first,
+ `open <path>` or `closed <path>`; kept in `$XDG_STATE_HOME/pardes/recent`.
+ #word("Recent") shows them in a pane, an open one at its dot now and a
+ closed one at its last (a PDF's is its page); a look at a row reopens it
+ there.
+- #file("/status"): `pid`, `version`, `panes`.
+- #file("/listeners"): the session's dial addresses, a line each (the
+ building chapter has the listeners).
+- #file("/os/"): existing regular files take read, write and truncation to
+ zero; create, remove, rename and mode changes are refused as not
+ permitted (`permission denied`, EACCES through a mount); ownership is
+ synthetic. Linux v9fs's truncation `mtime` hint is accepted and dropped.
+- #file("/src/") (and #file("/shaders") on GUI builds) with
+ `-Dembed-sources=true`; `EffectCode <effect>` lists an effect's files
+ under `/virtual`.
+
+= Limits <limits>
+
+#pairs(
+ [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], [65536 (64 KiB) offered],
+ [connections], [16 Unix and TCP, 16 QUIC],
+ [opens holding state], [64],
+ [held reads a connection], [128],
+ [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 (about 300 ms)],
+ [undo], [256 steps],
+ [log], [64 KiB ring; `msg` 256 bytes, `err` reason 200],
+ [`pty/run` output], [64 KiB],
+ [file name component], [255 bytes],
+)