// The reference: every file the virtual filesystem serves over 9P, 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 is a virtual filesystem, served as 9P2000 (not .u, not .L), 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`; #doc("scripting", section: "find-the-session") 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 pane with the keyboard; read: the serials touched /pager write a directory: its one +Pager, made or emptied; read: its serial /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 (so they can have gaps). 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` (a shell), `cmd` (a command's pane, no shell to type into), `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 `\\`, and any other control byte, DEL, a C1 control or a byte that is not UTF-8 `\xNN`, so a name decodes to the bytes it is. = Rules for every file *Failure.* A write fails whenever what it asked for fails, and logs one `err : ` record, with no `msg`; a write that succeeds logs none. A refused write says why in the log's last `err` record, found with `grep '^err' log | tail -1`: after a refused Del, Exit or Restore the newest line may be `new N .../+Unsaved`. 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; a bad setting value is quoted, the value itself (`bad value in control message; takes on, off "maybe"`); a ctl refusal quotes the offending word alone (`wrong #args in control message "Newcol"`). Through 9ns the kernel sees an errno 9ns reads from those words (cloud9's `fs.enameErrno`): the first of these that the words hold, case aside, wins, and anything else is EIO (`Modified`, an `Edit` command pardes leaves out): #pairs( [`control message`], [EINVAL], [`interrupt`], [EINTR], [`shut down`], [EIO], [`not exist`, `not found`, `no such`], [ENOENT], [`exists`], [EEXIST], [`not empty`], [ENOTEMPTY], [`not a dir`], [ENOTDIR], [`is a dir`], [EISDIR], [`permission`, `denied`], [EACCES], [`read-only`, `read only`, `readonly`], [EROFS], [`no space`], [ENOSPC], [`not allowed`, `not permitted`, `cannot`], [EPERM], [`fid`], [EBADF], [`bad offset`, `invalid`, `bad `], [EINVAL], [`busy`, `in use`], [EBUSY], [`too long`], [ENAMETOOLONG], [`too many open files`], [EMFILE], [`not supported`, `unsupported`], [EOPNOTSUPP], [acme's address and argument words: `no match for regexp`, `no previous regular expression`, `address out of range`, `addresses out of order`, `past end of body`, `written to addr failed`, `not locked by this open`, `too small for the panes`, `owns the size`, `no question asked`, `answer takes`], [EINVAL], ) The `err` record has the words; a shell sees only the errno. 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 at its newline; the last one, unended, runs after its open is closed, the close answered first, so its failure is only its `err` in the log: end a write with a newline when its result matters (#doc("building", section: "writes-through-a-mount")). `> exec` (a truncating open) is fine through a mount. An `Edit` whose `{` or `a`/`c`/`i` text is still open waits for the next write on that open. A line or Edit block over 1 MiB is refused once (EINVAL), and the rest of it, through its newline, is dropped. *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")). A read answers the panes touched by this open's last write. An open that never wrote reads the session's last answer, from whichever client wrote it, so read on the open you wrote. 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/pane/$n/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. *Open records.* A session holds 64 open records, shared by every client. An open that keeps state takes one: such a snapshot, a #file("run"), #file("event") or #file("pty/data") open, a write open of #file("data"), #file("xdata"), #file("sel") or a text pane's #file("body"), and every write open of a command file (#file("look"), #file("exec"), #file("tagexec"), a #file("ctl")). A plain read of a pane's text takes none. Past 64 an open is refused `too many open files`, which a mount reports as EMFILE. Close what you open: a shell's `exec 3>$m/exec` holds its record until fd 3 is closed. *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. A stream and a view generated by each read stat 0, as acme's do: #file("log"), #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 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 #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. A bare `:N` or `:N:M` (or any `:addr`) addresses the pane looked from. `@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 fails the write and is named: `look: : permission denied`. A zero column is refused, `file:0:0` included: columns count from 1. - A whole line of a diff pane written to #file("look") is the look a right-click on its first column makes. The line is matched in the pane from its cursor row on, wrapping, and the first match wins. - On a PDF pane `:P:H` is hit H of the pane's search on page P, as `file.pdf:P:H` is; a hit not there, or any H with no search active, is a miss, `has no search hit H on page P`. Only a `+PdfSections` row's second number is a section, `file.pdf:PAGE:SECTION`, and only such rows are checked against the outline: a section not there, or not on that page, is a miss. - A plain word selects its next place after dot, wrapping. `LookWord list` on the root ctl lists a row per line holding it in a `+Search` pane instead; the next word looked at in that directory refills it. 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 `./x` or `../x` that is not there is such a miss too (`look: ./x: no such file`). A look whose address fails says so and leaves open no file it opened. A look of `file:12:` reads as `file:12`: a trailing colon is dropped, as a click leaves it off. A path too long to repeat whole gives up its middle to `…`. A line written to #file("exec") is a middle-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, and fails when there is nothing to rename. One that reaches other files applies nothing: it opens a `+Search` preview of the edits, which is the pane the write answers, and says how many it lists. So does one the server answers in this file alone while another open file of its language still holds the old name, a row per such file (zls renaming at a declaration; from a use it reaches every file); #word("Diagnostics") and #word("Symbols") list the file's in `+Search`; #word("Lspinfo") fills `+Lsp`, the pane its write answers, with which server serves the file and its state (`not started yet` until its first query starts it, ` failed recently; retry in Ns` while it backs off); #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 (a typo ends `exit 127`), 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, and while that open stays open the pane is its own: another client's command in the same directory gets a pane of its own. 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, leaving out its `exit N`: #cmd("awk '/^% /{out=\"\"; next} /^exit [0-9?]+/{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 pane with the keyboard 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 dial: `unix!/path`, `/path`, `tcp!!`, a session name, or with `-Dquic` `quic!…`; a missing socket is ENOENT, `no such socket`; `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 any other `x!y` `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 (in the directory of the pane with the keyboard, or the session's when that directory is not on disk). #word("Restore"), #word("Del"), #word("Delcol") and a pane's `get` refuse the same way with their own word, except that #word("Delcol") opens no `+Unsaved` pane: it names the panes only in its `unsaved` records and its notice. - #word("Dump") writes `pardes--