// 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). Pane shells and command panes alike can reach the session; one rule picks the client: - *With a mount* (`$NINE_MOUNT` set, as under cloud9's `9ns --mntgen`; `git.sr.ht/~gbrls/cloud9`, whose `zig build` installs `9ns` on Linux), the session is a directory and plain `cat` and `echo >` work: #cmd("s=${PARDES_9P##*/pardes-9p-}; m=$NINE_MOUNT/pardes/${s%.sock}\ncat \"$m/index\"") (Under `9ns --unix SOCK -- cmd` the session is `$NINE_MOUNT` itself.) - *Without one*, plan9port's `9p` talks to the socket: #cmd("9p -a \"unix!$PARDES_9P\" read index\necho Save | 9p -a \"unix!$PARDES_9P\" write pane/3/ctl") The recipes below use `$m`; with `9p`, `cat $m/x` is `9p ... read x` and `echo y > $m/x` is `echo y | 9p ... write x`. 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. The reference has the other ways in: a #word("Tty9p") terminal's kernel mount, and the Python client in the source tree. A command run from a pane (#btn("B2") on a line in its tag or text) also gets `$winid`, the serial of that pane, as acme's commands do; it is unset for one run from a column's or the workspace's tag, and `PARDES_PANE` stays the command pane's own. Every pane's child, a terminal's shell or a command, gets `PAGER=cat` and `GIT_PAGER=cat` unless your environment sets them, so nothing waits in a pager. = 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")], [act on the pane a command came from], [#cmd("#!/bin/sh\n# fmt-here: put its name in a pane's tag, click it with B2\nn=$winid; f=$(cat $m/pane/$n/name)\necho Save > $m/pane/$n/ctl && zig fmt \"$f\" && echo get > $m/pane/$n/ctl")], [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. - #file("pty/run") runs in the terminal's own shell, so a command that pages waits in the pager when your environment names one (pardes sets `PAGER` and `GIT_PAGER` to `cat` only when it does not): run `git --no-pager ...` or `PAGER=cat ...`. - `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. = 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.