// 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--