# Filesystem Every native session serves 9P2000 (not .u, not .L) as a control filesystem, in acme's manner: panes, columns, tags and the session are files. This page is the reference for what each file does. The served [`/README`](../src/fs-help.txt) is its one-screen summary. ## Connecting The socket is `$XDG_RUNTIME_DIR/pardes-9p-.sock` (else under `~/.local/state/pardes`), `` being the pid, the `--detach=NAME`, or `--9p=`. A socket in the runtime directory is also posted in the 9P registry as `$XDG_RUNTIME_DIR/9p/pardes/` (a symlink to the socket; [cloud9.md](cloud9.md#the-posted-9p-registry)). Pane shells get `PARDES_PID` (the editor's pid), `PARDES_9P` (the socket) and `PARDES_PANE` (their pane's serial). **Forwarding.** `pardes FILE` run in a pane (a live `PARDES_PID`, with `PARDES_9P` and `PARDES_PANE`) writes FILE to that pane's `look` and returns at once, as acme's `B` does. A FILE not there yet (`pardes notes/new.txt`) opens a new pane named for it, empty, and its Save creates the file, making its directories first when they are not there either. A session that answers never gets a nested editor in its pane: what it refuses (a bad name, a pane it has not) is printed, `pardes: : `, and the launch exits 1, leaving no pane behind. A missing `PARDES_9P`/`PARDES_PANE`, or a session that does not answer, starts a separate editor instead. Bare `pardes` in a pane refuses and names `--nested`. `--wait` (`-w`) returns when the pane that shows FILE is deleted (exit 0) or the session goes away (exit 1), as acme's `E` does. Use `EDITOR='pardes --wait'` (`GIT_EDITOR` follows `EDITOR`), so fish's Ctrl-O, `git commit` and `crontab -e` read the file after you close its pane. `--nested` runs a separate session whose shells do not forward to it. **Clients.** ```sh 9p -a "unix!$PARDES_9P" read index # plan9port, no mount 9ns --unix "$PARDES_9P" -- sh -c 'cat "$NINE_MOUNT/index"' # private mount 9ns --mntgen # the whole registry at /mnt/9p ``` Under `9ns --unix` the session is `$NINE_MOUNT` itself and exists only inside that command. Under `9ns --mntgen` every posted session is `$NINE_MOUNT/pardes//`; take the name from `$PARDES_9P`. A dead session's entry stays listed and answers `Input/output error`, so name the session rather than globbing. `Tty9p` gives one pane's shell a kernel mount at `$PARDES_MOUNT` ([v9fs.md](v9fs.md)). plan9port's `9p write` always opens with OTRUNC, so `echo x | 9p write pane/3/body` replaces the whole body where acme would append. Append with `>>` through a mount. For [Linux v9fs](https://www.kernel.org/doc/html/latest/filesystems/9p.html) use `version=9p2000,cache=none,access=any`, `trans=unix` (or `trans=tcp` with `port=`), `uname`, `dfltuid` and `dfltgid` for the local user, and an empty `aname`. **Listeners.** `--9p-tcp='tcp!127.0.0.1!5640'` adds TCP; `--9p-quic='quic!127.0.0.1!5641'` adds QUIC (build with `-Dquic=true`, OpenSSL 3.6+; ALPN `pardes-9p`, an ephemeral TLS identity, no peer verification). Addresses are numeric IPv4/IPv6; port 0 picks one; `/listeners` reads them back. Every connection has full session access, `/os` included, and TCP is unencrypted: use loopback. Unix and TCP share 16 connection slots; a 17th client's Tversion gets `too many connections` (and the log `err - 9p: too many connections (N turned away)`). QUIC has 16 of its own. Plan9port and v9fs need a userspace bridge for QUIC. **Look paths and mounts.** Look resolves the OS filesystem first, then the editor's own tree. Explicit paths skip that search: | Look path | Meaning | Served path | |---|---|---| | `/n/os/proc/self` | the host filesystem | `/os/proc/self` | | `/n/self/pane/2/body`, `/virtual/pane/2/body` | this session's tree | `/pane/2/body` | | `/virtual/src/pardes.zig` | sources embedded with `-Dembed-sources=true` | `/src/pardes.zig` | | `/n/peer/pane/2/body` | a mounted session | the peer's `/pane/2/body` | `--mount=peer=work` or `Mount peer ` mounts a session name, an absolute socket path, `unix!/path`, `tcp!IP!port` or `quic!IP!port`; `Unmount peer` removes it. Mount dials at once and fails if nothing answers (`dial failed: no answer`, `timed out`, `hung up`). There are eight named mounts; `os` and `self` are reserved. Unmount refuses a mount a pane, a working directory or a pending Save still uses. Mounts are dumped. A session may open its own tree through a mount (a Look at `$m/pane/2/body` from the editor serving `$m`): requests are answered on the connection's task while the editor waits in its syscall. Through QUIC that still hangs. ## The tree ``` /README the one-screen guide (src/fs-help.txt) /index a line per pane: serial kind dirty name column /status pid, version, panes /look /exec write a line: a right / middle click at the active pane; read: the serials touched /log the event log; write `follow` to wait for more /screen the rendered screen as JSON /listeners dial addresses /focus the serial of the pane with the keyboard; write one to move it /ctl settings and session builtins /commands every builtin, one a line /recent files opened lately: open|closed /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 `/pane/new` makes a pane (so `ls -l` and `find` are safe), only `rmdir` of `/pane/` or an empty `/col/` removes. Tcreate is refused everywhere. `/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 `/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 `pane/new` (`permission denied`) and a write on a read-only fid (`bad use of fid`). Errors are words (Plan 9's where pardes has none of its own), never a C library string. Through 9ns the kernel sees an errno 9ns reads from those words (cloud9 `9ns/src/nine.zig`, `enameToErrno`): `control message`, `invalid` or `bad ` is EINVAL (malformed input); `no such`, `not found` ENOENT; `in use` EBUSY; `no space` ENOSPC; `denied` EACCES; anything else, such as `no match for regexp`, `address out of range`, `Modified` or an `Edit` command pardes leaves out (`w`, `e`, `r`, `|`: `w is a sam command pardes's Edit leaves out`), EIO; no refusal reads as EOPNOTSUPP. The `err` record has the words; a shell sees only the errno, most often `Invalid argument` or `Input/output error`. Not failures: a look that finds nothing (it answers nothing and logs one `err`), an Edit `x` that matches nothing, `Undo` with nothing to undo (a `msg`), and a command pane's command, which ends in its own time with an `exit` record. **Command lines.** `look`, `exec`, `tagexec`, the three kinds of `ctl` and a column's `exec` take one command a line. The whole write is checked first (a control character other than a tab fails it all, EINVAL); then lines run in order, and a failing line fails the write after the lines before it took effect, as acme's ctl does. Blank lines are skipped. A mount cuts a big write into pieces of at most one message (msize 64 KiB, 65536 negotiated, less the header), and each line runs once it is whole. A write that does not fill its message is whole, so its last line runs even without a newline (`printf Save > exec`), unless it is a multiple of 4096 bytes: that is where a writer's buffer (stdio, a mount's page cache) filled and cut a line, so its tail waits for the next write or the close. An `Edit` whose `{` or `a`/`c`/`i` text is still open waits for the next write on that open. A line held to the close (a 4096-multiple write's tail, an `Edit` block never ended) runs there, and its failure is only in the log, as its `err` record (``err ctl: unmatched `{'`` or `a, c or i text not ended by a . line`): the close itself reports no error, and the write that sent it had already succeeded. A script that needs a line's result ends the write with a newline, so the line runs, and fails, with its write. A line held past 1 MiB is refused. **Answers.** Reading `look`, `exec` or `tagexec` answers the serials the last command made, or else the pane it acted on or focused, one a line; nothing when it did none of these (`Newcol` at `/tagexec`). An open that wrote reads its own last answer; an open that never wrote reads the session's last. A read is a stream: once read, the next read on that fid is EOF until the next write (or seek to 0). With other clients about, write and read on one open: `exec 3<>$m/look; echo x >&3; cat <&3; exec 3<&-`. **Snapshots.** `/index`, `/layout`, `/recent`, `/commands`, `/status`, `/listeners`, a read-only `ctl`, `/log`, `/screen` and a terminal's `body` freeze at the open, so one read in several chunks never splices two moments; open again for now. Such an open, or a `run`, `event` or `pty/data` open, takes one of 64 open records; past that the open fails `too many open files`. **Held reads.** A read with nothing to give yet (a followed `log`, `event`, `pty/data`, `pty/run` before its answer) waits in the editor and is answered when news comes. A second read on that open meanwhile fails `file in use`. A read the client flushed is dropped. One connection holds at most 128 reads at once; the next is refused `too many reads waiting: 128`. Every held read is on an open that keeps state, and those are 64 in the session (above), so 64 is the real cap on reads held at once, through one connection or many. A mount (9ns) is one connection, and it keeps answering everything else beside its held reads. Through a FUSE mount bash's `read -t` cannot time out: wrap the loop in `timeout N`. **Stats.** A file whose text is kept has its real length: a pane's `body`, `tag`, `name`, `ctl`, `sel` and the range and flag files, the workspace's `tag`, the root `ctl`, `status`, `commands`, `README`, the `look`/`exec` answers, `focus`; `event` and `pty/data` the next record's, zero when none waits; `log` what an open would freeze. A view generated by each read stats 0, as acme's do: `screen`, `data`, `xdata`, `index`, `layout`, `recent`, `listeners`, `pane/new`. Read those to the end rather than trust a length (`cat` does). The qid version of `body`, `data` and `xdata` is the pane's revision, so a stat sees an edit land. Modes are 0644/0666, 0444 read-only, 0222 write-only. ## look and exec A line written to `look` is a right click on it, on the line as written: its leading blanks are its own (` return x` finds that indented line, and a diff's blank context line ` ` is a look too); only its newline and `\r` go. An `exec` line's blanks at either end are trimmed. - a path opens the file (the pane already showing it, if any); `file:12` selects line 12, newline included; `file:12:5` puts the caret at line 12, byte column 5; `file:` takes any address (below), **evaluated from the file's dot**: `file:/re/` finds the next match after the selection, `file:0/re/` the first. `:addr` addresses the pane itself. A leading `~` is the home directory ($HOME, else the passwd entry's; `~user` that user's) here and wherever a path is typed (`name`, `Save`, `ThemeFile`, `DumpDir`, `Restore`, `pardes '~/x'`), even beside a file named `~`: write `./~` for that. A path to no file is a miss, as a search that finds nothing is: said on the message row and logged as an `err`, the write still answered. A file that is there but will not open (no permission to read it, say) fails the write, with why. - `@p:` addresses a pane by serial, a terminal's logical lines too. - a directory types `ls` into a terminal idle there, else opens one there. - a URL opens in the browser. - in a diff pane, a whole line of the diff is a look at the `path:line` it names, as a right click on its first column is ([tags.md](tags.md#reviewing-diffs)). - a plain word selects its next place after dot, wrapping (`LookWord list` on the root ctl lists every place in a `+Search` pane instead). In a terminal a word is always listed, rows spelled `@p3:12:5-9`. A miss logs `err 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 `look` reading empty; the write succeeds. A path too long to repeat whole gives up its middle to `…`. A line written to `exec` is a middle click: - a builtin word runs (`/commands` lists them): `Save`, `Del`, `New`, `Tty [shell]`, `Msg text`, `Find`, `Grep`, `Edit ...`, `Mount`, ... A builtin that needs its argument (`Msg`, `Mount`, `Find`) written bare is `wrong #args in control message "Msg"`, and so is one that takes none written with one (`Config extra`). - a line starting with `#` is a comment, as in a shell: it runs as nothing, silently, here and in every ctl. - the language server's words ask about a file pane's text at its cursor (set it with `addr` and `dot=addr` first): `Hover` fills `+Hover`, `Rename new` renames the symbol in the file and says how many ranges it changed (on the message row, and so in /log) without listing them; one that finds nothing to rename fails; a rename the server spreads over other files lists them in `+Search`, `Diagnostics` and `Symbols` list the file's in `+Search`, `Lspinfo` says which server serves the file and its state, and `Lspwhy` narrates the last query step by step (in `+Lsp`), to tell why it found nothing. The write returns once the answer is in; a pane with no file is refused. - acme's words run as pardes's where it has one (`Put` is `Save`, `Delete` a `Del` that does not ask); the rest (`Get`, `Putall`, `Snarf`, `Cut`, `Paste`, `Zerox`, `Sort`, `Load`, `ID`, `Send`, `Tab`, `Indent`, `Local`, `Incl`, `Abort`) are refused, `invalid: acme's Get is not a pardes builtin: ...`, never run as commands. So is a GUI-only builtin (`Fonts`) on another frontend. - anything else is a command line, at most 1024 bytes. At a terminal idle at an empty prompt it is typed into that shell. Anywhere else it runs as a **command pane**: a terminal running the root ctl's `Shell` (`$SHELL`, else `/bin/sh`) with `-c` and the line, in the pane's directory, with job control on. Its tag reads ` () running`, then `exit N`; the log says `run ` and `exit `; `exec` reads back its serial. A typo ends `exit 127`. The directory's next command reuses a finished command pane, below what it showed; one still running gets a second pane. A reused pane's `body` keeps every earlier run above a `% ` row naming each command, so to take only the last run's output read from after the last `% ` row: `awk '/^% /{out=""; next} {out = out $0 "\n"} END {printf "%s", out}' body`. From a column's or the workspace's tag (or `/tagexec`, `col//exec`) a command always runs as a command pane, in the session's directory. From a pane whose directory is gone nothing runs: `exec: : no such directory` (ENOENT). The root's `look` and `exec` act at the active pane and log as that pane's (with no pane at all, in the session's directory: a look opens its file, making a column as `New` does, and an exec runs its command there); `/pane//look` and `exec` at pane n; `/tagexec` and `/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 (`Undo`, `Msg`, `Save`) is refused at `/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. `Kill` (root ctl) stops the commands pardes started: bare, all; `Kill make ls`, those whose line starts with one of the words. For a command pane it signals the whole line, `&` jobs included; for a line typed into a shell only the foreground job (SIGTERM), and the shell decides the rest. With nothing running, named or not, it says `Kill: nothing running` and the write succeeds; words that name none of what runs fail it (`Kill: no running command has that first word`). Kill does not reach a REPL's code: use `sig INT` on its `pty/ctl`. ## The root ctl Reading `/ctl` gives every setting, one a line, in the words a write takes (`Theme orchard`, `Verbose on`, `Placement acme`, `DumpDir `, `Shell /bin/bash`, ...), so writing back what it reads changes nothing. Writes take settings and session builtins (`scope = .session` in `src/builtins.zig`), acting at the pane with the keyboard: - A setting written bare steps to its next value (a switch flips; so do `Placement`, `BootShell`, `Crt`). A value it does not take is `bad value in control message; ...` naming what it takes; `/commands` lists them. A setting the frontend cannot show is refused (`Lift is GUI-only, invalid here`). - `Newcol` makes an empty column right of the keyboard's, halving the active column. `Joincol` folds the keyboard's column into the one on its right (its panes go below that column's), `Joincol: no column to the right`. - `Exit` quits. It refuses once while panes hold unsaved text: one `unsaved ` 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. The same word again with nothing edited since discards; after more editing it refuses again, naming only the panes edited since. `Restore`, `Del`, `Delcol` and a pane's `get` refuse the same way with their own word. A `+New` scratch under 100 bytes, or a command's output, is never asked about. - `Dump` writes `pardes--