// Scripting pardes over 9P: finding the session, the recipes, and the // traps. Every file's full semantics are in the reference; this is the // five-minute path to using them. #import "style.typ": key, keys, btn, chord, word, tag, addr, file, cmd, doc, pairs Every session serves its panes, columns and tags as files, as acme does: `cat`, `echo >` and `ls` are the whole interface, and a program that opens files is an extension. The reference (#doc("fs")) has every file; this chapter is how to reach them and what to do with them. = Finding the session Each session listens on a Unix socket, `$XDG_RUNTIME_DIR/pardes-9p-.sock` (else under `~/.local/state/pardes`), `` being the pid, the `--detach=NAME` or `--9p=NAME`, and posts it in the 9P registry as `$XDG_RUNTIME_DIR/9p/pardes/`. Shells in its panes get `PARDES_PID` (the editor's pid), `PARDES_9P` (the socket) and `PARDES_PANE` (their pane's serial). Two clients do the rest. `9ns` comes from cloud9 (`git.sr.ht/~gbrls/cloud9`; its `zig build` installs `9ns` on Linux) and mounts a 9P tree through FUSE in a private namespace, no root needed; `9p` is plan9port's, and reads and writes without a mount. #cmd("9ns --mntgen # every posted session, under $NINE_MOUNT") #cmd("9ns --unix \"$PARDES_9P\" -- sh -c 'cat \"$NINE_MOUNT/index\"' # one session, for that command") #cmd("9p -a \"unix!$PARDES_9P\" read index # no mount") Under `9ns --mntgen` find this pane's session from its socket: #cmd("[ -n \"$NINE_MOUNT\" ] || echo 'no 9P mount: use 9p or the Python client'") #cmd("s=${PARDES_9P##*/pardes-9p-}; m=$NINE_MOUNT/pardes/${s%.sock}") #cmd("cat \"$m/index\"") Under `9ns --unix` the session is `$NINE_MOUNT` itself (`m=$NINE_MOUNT`), and in a #word("Tty9p") terminal (#key("SPC n 9")) it is `$PARDES_MOUNT`. A dead session's registry entry stays listed and answers `Input/output error`: name the session, never glob. A running session serves the binary that started it. #word("Tty9p") opens a terminal with the session kernel-mounted (Linux v9fs): it asks for your sudo password in the pane, mounts the socket in a private mount namespace and starts your shell as you, with `PARDES_MOUNT` set. Only that shell sees the mount, and each takes one of the session's 16 connections. It needs the `9p` and `9pnet_fd` kernel modules and the `pardes-v9fs` helper the build installs beside `pardes`. = Recipes The paths below are under `$m`; `p=$m/pane/$n` for a pane. #pairs( [look around], [#cmd("cat $m/index\ncat $m/layout\ncat $m/focus\ncat $m/commands")], [open a file at a place], [#cmd("echo \"$PWD/main.zig:120\" > $m/look; cat $m/look # where it went\necho 'main.zig:0/fn main/' > $m/look # the first match")], [make, fill, name, save], [#cmd("n=$(cat $m/pane/new) # one pane per open\nprintf 'hello\\n' > $m/pane/$n/body # > replaces, >> appends\necho \"$PWD/notes.txt\" > $m/pane/$n/name\necho Save > $m/pane/$n/ctl\nrmdir $m/pane/$n # close it")], [say something], [#cmd("echo 'Msg hello' > $m/exec")], [replace everywhere], [#cmd("echo 'Edit ,x/foo/c/bar/' > $p/ctl && grep -c foo $p/body")], [replace one match], [#cmd("echo /old/ > $p/addr && printf new > $p/data")], [delete line 3], [#cmd("echo 3 > $p/addr; : > $p/data")], [select line 3, read it], [#cmd("echo 3 > $p/addr; cp $p/addr $p/dot; cat $p/sel")], [run a command], [#cmd("echo 'make test' > $m/exec; c=$(cat $m/exec) # its pane\ngrep \"^exit $c \" $m/log | tail -1 # exit c N")], [run at a prompt], [#cmd("t=$(awk '$2==\"term\"{print $1; exit}' $m/index)\nexec 3<>$m/pane/$t/pty/run; echo ls >&3; cat <&3; exec 3<&-")], [type, interrupt], [#cmd("printf 'q' > $m/pane/$t/pty/data # \\r Enter, \\x03 Ctrl-C\necho 'sig INT' > $m/pane/$t/pty/ctl")], [ask the language server], [#cmd("echo /myFunc/ > $p/addr; echo dot=addr > $p/ctl\necho Hover > $p/exec # also Rename new, Symbols")], [follow what happens], [#cmd("exec 3<>$m/log; echo 'follow new' >&3\ntimeout 30 cat <&3; exec 3<&-")], ) `follow` without `new` replays the ring first. #file("pty/run") answers `busy: is running` when a program holds the terminal: talk to it through #file("pty/data") instead. == Event helpers Holding a pane's #file("event") open takes its #btn("B2") and #btn("B3") clicks: they arrive as acme's records instead of acting, so the pane's tag can carry a helper's own words, and writing a record back has pardes do it. The keyboard is never taken, and the clicks come back when the helper closes the file. One open at a time; the record format is in the reference (#doc("fs", section: "event")). == Waiting for an edit `pardes --wait FILE` in a pane's shell returns when the pane showing FILE is closed (#doc("tags", section: "editor")): `EDITOR='pardes --wait'` makes pardes the editor of every program run there. = Traps - A refused write says only `Invalid argument` or `Input/output error`; the reason is the `err` record in the log (`tail -1 $m/log`). Only writes log: a refused open, truncation or `rmdir` has its errno alone. - #file("addr") belongs to the pane, not to you, and moves on: each `/re/` searches from the last address, a #file("data") write leaves it just past what it wrote, and a read moves it too. Write #file("addr") before each replacement, `0` to start at the top. A failed address leaves none, and #file("data") refuses until you write one. - An Edit `x` that matches nothing succeeds: check the result (`grep -c foo body`), not the exit status. - Each open of #file("pane/new") makes a pane. `ls`, `stat` and `find` never do. - Through a mount, bash's `printf 'a\nb\n' > ctl` arrives one write per line, so a block that should be refused whole runs its good lines before the bad one fails. Send a block as one write (`cat block > $p/ctl`), and end every write whose result matters with a newline. - plan9port's `9p write` opens with OTRUNC: `echo x | 9p write pane/3/body` replaces the whole body. Append with `>>` through a mount. - `tail -f log` never sees anything new: write `follow` on the open you read. Through a FUSE mount bash's `read -t` cannot time out: wrap the loop in `timeout`. - Settings and session words go to #file("/ctl"), pane words (#word("Undo"), #word("Save"), #word("Del")) to #file("pane//ctl"); the wrong one is refused, naming the right one. - #word("Exit"), #word("Restore"), #word("Del") and `get` refuse once over unsaved text; the same word again discards. - A word no builtin knows runs as a shell command: a typo ends `exit 127`. - `lock` needs a held file descriptor: `exec 3>$p/ctl; echo lock >&3; ...; exec 3>&-`. - Names in #file("index") may hold blanks: split a row with `rsplit(maxsplit=1)` first, and read #file("index") again after anything that opens or closes panes. - A #word("Restore") hangs up every connection: dial again and restart the mount. = No mount: the Python client `test/ninep.py` in the source tree speaks 9P itself, for when nothing is mounted or a fid must stay open (#file("event"), #file("pty/data"), a followed #file("log")). Its paths are the served root: ```python import sys from ninep import Client # PYTHONPATH=test with Client(sys.argv[1]) as c: # a socket path, or (ip, port) 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}') ``` `client.screen()` returns the parsed #file("/screen"). = An isolated session Never experiment on a session someone is using. Strip every `PARDES_*` variable first, or a file argument goes to the session you are inside; then `pardes --detach=NAME &` with its own `HOME` and `XDG_*` directories, and mount it with `9ns --mntgen` (the session is `$NINE_MOUNT/pardes/NAME`) or `9ns --unix $XDG_RUNTIME_DIR/pardes-9p-NAME.sock -- sh`. Kill it when done. From the source tree, `test/fs.py`'s `session()` starts a private session and cleans it up, and `test/agent_session.py` drives terminals.