// 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. A paging command run through #file("pty/run") does not hang: the terminal's pager is `pardes -`, which puts the text in a `+Pager` pane and returns. A command pane pages through `cat`; its output is a pane already. Both hold only where your environment names no pager. = Recipes Each recipe starts after these lines, which find the session (with a mount) and make a pane of your own. Work through that pane's #file("look") and #file("exec"), not the root's (see the traps): #cmd("s=${PARDES_9P##*/pardes-9p-}; m=$NINE_MOUNT/pardes/${s%.sock}\nn=$(cat $m/pane/new); p=$m/pane/$n # a scratch of your own") #pairs( [look around], [#cmd("cat $m/index\ncat $m/layout\ncat $m/focus\ncat $m/commands")], [open a file at a place], [#cmd("exec 3<>$p/look; echo \"$PWD/main.zig:120\" >&3; f=$(cat <&3); exec 3<&- # its pane\necho \"$PWD/main.zig:0/fn main/\" > $p/look # the first match")], [fill, name, save, close], [#cmd("printf 'hello\\n' > $p/body # > replaces, >> appends\necho \"$PWD/notes.txt\" > $p/name\necho Save > $p/ctl\nrmdir $p")], [say something], [#cmd("echo 'Msg hello' > $p/exec")], [replace everywhere], [#cmd("echo 'Edit ,x/foo/c/bar/' > $p/ctl\ngrep -c foo $p/body # 0: none left")], [insert after a match], [#cmd("echo 'Edit /old/a/ text/' > $p/ctl # from dot; one undo step")], [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("exec 3<>$p/exec; echo \"cd '$PWD' && make test\" >&3; c=$(cat <&3) # its pane\nuntil grep -q ') exit ' $m/pane/$c/tag; do sleep 0.2; done\ncat $m/pane/$c/body; exec 3<&- # read it before letting go")], [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("t=$(awk '$2==\"term\"{print $1; exit}' $m/index)\nprintf 'q' > $m/pane/$t/pty/data # \\r Enter, \\x03 Ctrl-C\necho 'sig INT' > $m/pane/$t/pty/ctl")], [ask the language server], [#cmd("exec 3<>$p/look; echo \"$PWD/main.zig\" >&3; q=$m/pane/$(cat <&3); exec 3<&-\necho /myFunc/ > $q/addr; echo dot=addr > $q/ctl\necho Hover > $q/exec # also Rename new, Symbols")], [follow what happens], [#cmd("exec 3<>$m/log; echo 'follow new' >&3\ntimeout 30 cat <&3; exec 3<&-")], ) A command pane your #file("exec") open was answered is yours while that open stays open: another client's command in the same directory gets a pane of its own. Read its #file("body") and poll its tag before you close the open; once you have, the directory's next command may reuse the pane. In #file("index") a `term` is a shell to type at, a `cmd` a command's pane. `follow` without `new` replays the ring first. == A script for a tag A script can act on the pane it was clicked from through `$winid`. This one saves a Zig file, formats it and loads the result: put it on your `PATH` as `fmt-here`, type `fmt-here` into a pane's tag and click it with #btn("B2"). It follows the one-client rule itself, since a script starts with nothing set but its environment: ```sh #!/bin/sh # fmt-here: save the pane it was clicked from, zig fmt its file, reload it n=$winid if [ -n "$NINE_MOUNT" ]; then s=${PARDES_9P##*/pardes-9p-}; m=$NINE_MOUNT/pardes/${s%.sock} rd() { cat "$m/$1"; } wr() { cat > "$m/$1"; } else rd() { 9p -a "unix!$PARDES_9P" read "$1"; } wr() { 9p -a "unix!$PARDES_9P" write "$1"; } fi f=$(rd pane/$n/name) echo Save | wr pane/$n/ctl && zig fmt "$f" && echo get | wr pane/$n/ctl ``` Its output, and `exit 0`, show in the command pane it runs in. #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")). This helper gives pane `$1` two tag words of its own: `Upper` upper-cases the selection and `Done` ends it. Every other click is written back, so it acts as ever, and when the helper ends, its pane's clicks are pardes's again: ```bash #!/bin/bash # upper PANE: own the tag words Upper and Done in pane PANE s=${PARDES_9P##*/pardes-9p-}; p=$NINE_MOUNT/pardes/${s%.sock}/pane/$1 printf ' Upper Done' >> $p/tag exec 3<>$p/event # hold event on fd 3, this shell's own while IFS= read -r rec <&3; do read -r head q1 flag n text <<< "$rec" # e.g. Mx31 36 1 5 Upper case $head$text in [EFKM][Xx]*Upper) sel=$(cat $p/sel); printf %s "${sel^^}" > $p/sel ;; [EFKM][Xx]*Done) break ;; [EFKM][XxLl]*) printf '%s\n' "$rec" >&3 ;; # not ours: do what it would esac # I D i d report edits: nothing to do done exec 3<&- # let event go: clicks act again ``` `read` takes a record a line, which suits words; a helper that must take text holding newlines (a sweep over several lines) reads the record's `n` bytes of text itself, with `read -N`. == 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`; its reason is the log's last `err` record: `grep '^err' $m/log | tail -1`. Not the log's last line: after a refused Del, Exit or Restore that may be `new N .../+Unsaved`. A write that succeeds adds no record, so check the write's own status first. Only writes log: a refused open, truncation or `rmdir` has its errno alone. - The root #file("look") and #file("exec") act at the active pane, which another client, an idle shell or an event helper may own: a line written there may be typed into a terminal or taken by an event reader. Use a pane's own #file("look") and #file("exec"). - `head` through a 9ns mount says `Illegal seek` on the files that stat 0 (#file("index"), #file("layout"), #file("log"), #file("recent")): they are streams. Use `sed -n 1p` or `awk 'NR==1'`; `tail -n` works. - #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. - Read #file("event") on a descriptor your shell owns (`exec 3<>$p/event` ... `exec 3<&-`), never `cat $p/event | while read`: the `cat` outlives the loop and holds #file("event") open, so the pane's next click goes to it and is lost. - Read #file("look"), #file("exec") or #file("pager") on the open you wrote: a read answers the panes touched by this open's last write, and a fresh open reads the session's last answer, from whichever client wrote it (`exec 3<>$p/exec; echo cmd >&3; cat <&3; exec 3<&-`). - `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.