# 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` or `Modified`, EIO. 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`). An `Edit` whose `{` or `a`/`c`/`i` text is still open waits for the next write on that open, and fails at the close if it never ends (``unmatched `{'``). 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`. A mount (9ns) is one connection, so that is 128 followers through it, and the mount keeps answering everything else beside them. Through a FUSE mount bash's `read -t` cannot time out: wrap the loop in `timeout N`. **Stats.** Lengths are real (for `event` and `pty/data` the next record's, zero when none waits; for `log` what an open would freeze). 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: - 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. - `@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. - 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; `: no match for regexp` or `address out of range` when the address fails), opens nothing, and leaves `look` reading empty; the write succeeds. 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 everywhere and lists the places, `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. 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--