diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-30 23:04:26 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:17 -0300 |
| commit | 5e72bfc34cc97d12be5185930135b55c2eb001b4 (patch) | |
| tree | d81daad17e81789e5ce583f22280d6cc368d6ab6 /docs/typ | |
| parent | 074f113fa95a875a6bf17196f8e46e69806a247c (diff) | |
| download | pardes-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/typ')
| -rw-r--r-- | docs/typ/reference.typ | 721 |
1 files changed, 721 insertions, 0 deletions
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], +) |
