From 5e72bfc34cc97d12be5185930135b55c2eb001b4 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Wed, 30 Sep 2026 23:04:26 -0300 Subject: 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 --- docs/typ/reference.typ | 721 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 721 insertions(+) create mode 100644 docs/typ/reference.typ (limited to 'docs/typ/reference.typ') 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 + +``` +/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 +/layout a line per column, then `active ` +/tag /tagexec the workspace tag, and a word clicked in it +/col// tag ctl exec of column ; rmdir closes an empty one +/pane/new open it to make a pane; read answers the serial +/pane// 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/") or an empty #file("/col/") +removes. Tcreate is refused everywhere. + +#file("/index") rows are `serial kind dirty name column`, kind `text`, +`term`, `pdf` or `image`, the name `/+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 + +*Failure.* A write fails whenever what it asked for fails, and logs one +`err : ` 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 + +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:` 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:` 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 look: ...` (`no match for "zzq:#3"` quoting what +was written when nothing by that name exists; `:99 has no line 99` +for a line past a file's end, ` has no page 99` for a PDF's page; +`: no match for regexp` or `: 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 ` and `exit `. A reused pane's #file("body") keeps every earlier run + above a `% ` 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: : 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//look") and #file("exec") at pane n; +#file("/tagexec") and #file("/col//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//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 + +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 ` 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 + +Reading #file("/ctl") gives every setting, one a line, in the words a write +takes (`Theme orchard`, `Verbose on`, `Placement acme`, `DumpDir `, +`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 ` + record per pane, then the write fails `: 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--