--- name: pardes-9p description: Inspect and drive a running Pardes editor through its 9P control filesystem, or exercise its panes, builtins, terminal input and rendered output in an isolated session. Use for Pardes interaction, plugin development and end-to-end debugging. --- # Pardes over 9P Pardes is files: `cat`, `echo >`, `ls` and shell scripts are the whole interface. [docs/fs.md](../../../docs/fs.md) is the reference for every file; this page is recipes. Paths are relative to the repository root. ## Find the session ```sh [ -n "$NINE_MOUNT" ] || echo 'no 9P mount: use the Python client below' s=${PARDES_9P##*/pardes-9p-}; m=$NINE_MOUNT/pardes/${s%.sock} cat "$m/index" ``` `$NINE_MOUNT` is set by `9ns --mntgen` (usually `/mnt/9p`). `$PARDES_9P` is the session socket of the pane you run in, `$PARDES_PANE` that pane's serial. Under `9ns --unix SOCK -- cmd` the session is the mount itself: `m=$NINE_MOUNT`. In a `Tty9p` shell: `m=$PARDES_MOUNT`. A dead session's entry answers `Input/output error`: name the session, never glob. A running session serves the binary that started it. ## Look around ```sh cat "$m/index" # serial kind dirty name column, a pane a line cat "$m/layout" # columns, then `active ` cat "$m/focus" # the pane with the keyboard cat "$m/README" # the one-screen guide cat "$m/commands" # every builtin and where it goes ``` Parse an index row with `rsplit(maxsplit=1)` first: names may hold blanks. Re-read the index after anything that opens or closes panes. ## Open a file, go to a place ```sh echo "$PWD/main.zig:120" > "$m/look"; cat "$m/look" # serial it landed in echo "main.zig:0/fn main/" > "$m/look" # first match; without 0, the next after dot ``` A miss opens nothing, reads back empty and logs an `err look: ...` line; the write itself succeeds. `L:C` columns are bytes. ## Make, fill, name and save a pane ```sh n=$(cat "$m/pane/new") # each open makes one: read it once printf 'hello\n' > "$m/pane/$n/body" # > replaces the text printf 'more\n' >> "$m/pane/$n/body" # >> appends echo "$PWD/notes.txt" > "$m/pane/$n/name" echo Save > "$m/pane/$n/ctl" rmdir "$m/pane/$n" # close it, saved or not ``` ## Edit text ```sh p=$m/pane/$n echo 'Edit ,x/foo/c/bar/' > $p/ctl # sam edit, one undo step echo '/old/' > $p/addr; printf 'new' > $p/data # replace the next match echo 0 > $p/addr # back to the top echo '3' > $p/addr; : > $p/data # delete line 3 cp $p/addr $p/dot; cat $p/sel # select it, read the selection ``` `addr` searches on from the last address (it is the pane's, shared by all clients); after a `data` write it sits just past the text, so write `addr` before each replacement. A failed `addr` leaves none and `data` refuses: check the write's status. An Edit block of several lines goes in one open: `printf 'Edit ,x/foo/{\ni//\n}\n' > $p/ctl`. ## Run a command ```sh echo 'make test' > "$m/exec"; c=$(cat "$m/exec") # a command pane; its serial grep "^exit $c " "$m/log" | tail -1 # `exit ` once done ``` At a terminal's prompt, run and get the status and output in one go: ```sh t=$(awk '$2=="term"{print $1; exit}' "$m/index") exec 3<>"$m/pane/$t/pty/run"; echo 'ls' >&3; cat <&3; exec 3<&- # `exit N`, then output printf 'q' > "$m/pane/$t/pty/data" # raw keystrokes (\r Enter, \x03 Ctrl-C) echo 'sig INT' > "$m/pane/$t/pty/ctl" # interrupt ``` `busy: is running` means the prompt is not free: a program holds the terminal, so talk to it through `pty/data`. ## Follow what happens ```sh exec 3<>"$m/log"; echo 'follow new' >&3 timeout 30 cat <&3 # one record a line, as they come exec 3<&- ``` `follow` (without `new`) replays the ring first. `tail -f` sees nothing new. Through FUSE `read -t` never times out: use `timeout`. After a `Restore` every connection is cut: dial again and restart the mount. ## Traps - A refused write says only `Invalid argument` or `Input/output error`; the reason is the `err` line in `$m/log` (`tail -1 "$m/log"`). - Only writes log: a refused open, truncation (`> data` after a failed `addr`) or `rmdir` has its errno alone. - Settings and session words go to `$m/ctl`; pane words (`Undo`, `Save`, `Del`) to `$m/pane//ctl`. The wrong one is refused naming the right one. - `Exit`, `Restore`, `Del`, `get` refuse once over unsaved text; the same word again discards. - A word no builtin knows is a shell command (`exit 127` if a typo). - `lock` needs a held fd: `exec 3>$p/ctl; echo lock >&3; ...; exec 3>&-`. - plan9port `9p write` truncates: it replaces a whole `body`. ## No mount: the Python client Use [test/ninep.py](../../../test/ninep.py) when nothing is mounted, or when a fid must stay open (`event`, `pty/data`, a followed `log`). Its paths are the served root (`/index`, `/pane/2/body`). ```sh PYTHONPATH=test python3 -B - "$PARDES_9P" <<'PY' import sys from ninep import Client with Client(sys.argv[1]) as c: print(c.read('/index').decode(), end='') n = int(c.read('/pane/new')) c.write(f'/pane/{n}/body', b'hello\n', truncate=True) fid = c.open(f'/pane/{n}/event', 0) # hold event: clicks come here c.write(f'/pane/{n}/exec', b'Msg hi\n') print(c.read_fid(fid, 0, 4096)) # b'FX0 0 1 6 Msg hi\n' c.close(fid) c.remove(f'/pane/{n}') PY ``` `Client` takes a Unix socket path or `(ip, port)`; `client.screen()` returns the parsed `/screen`. For `event` records and writing them back, see [fs.md#event](../../../docs/fs.md#event). ## An isolated session Never experiment on a session someone is using. Build a binary with `zig build install -Dplatform=tty` (a bare `zig build`, or `--prefix zig-out`, installs over `~/.local/bin`). `test/fs.py` starts a private session and cleans it up: ```sh PYTHONPATH=test python3 -B - "$(realpath zig-out/bin/pardes)" <<'PY' import sys, tempfile from pathlib import Path from fs import session, new_pane, execute with tempfile.TemporaryDirectory(prefix='pardes-9p-') as d: with session(sys.argv[1], Path(d), 'probe') as (client, address): n = new_pane(client, b'fn main() void {}\n') execute(client, n, 'Msg ready') assert 'ready' in ''.join(c[0] for c in client.screen()['cells']) PY ``` By hand: `pardes --detach=NAME &` with its own `HOME` and `XDG_*` directories, then `9ns --mntgen` (the session is `$NINE_MOUNT/pardes/NAME`) or `9ns --unix $XDG_RUNTIME_DIR/pardes-9p-NAME.sock -- sh`. Strip every `PARDES_*` variable first, or a file argument goes to the session you are inside. `session(..., tty=True)` and [test/agent_session.py](../../../test/agent_session.py) drive terminals.