// Scripting pardes over 9P: finding the session, a first tag word, the // recipes, and the traps. Every file's full semantics are in the // reference. #import "style.typ": key, keys, btn, chord, word, tag, addr, file, cmd, doc, pairs Every session is a virtual filesystem, served over 9P, as acme's is: its panes, columns and tags are files. `cat`, `echo >` and `ls` are the whole interface, and a program that opens files is an extension. The reference (#doc("fs")) has every file. = Finding the session Shells and command panes in a session get `PARDES_9P` (its socket) and `PARDES_PANE` (their own pane's serial). A command run from a pane's tag or text also gets `$winid`, the serial of the pane it was clicked in, as acme's commands do (unset from a column's or the workspace's tag). Under `9ns --mntgen` (see setup) every pane also gets `PARDES_MOUNT`, the session as a directory. That is the session line every recipe here starts from: #cmd("m=${PARDES_MOUNT:-}\ncat \"$m/index\"") When it is empty (no mount), plan9port's `9p` talks to the socket instead: `cat $m/x` is `9p -a "unix!$PARDES_9P" read x`, and `echo y > $m/x` is `echo y | 9p -a "unix!$PARDES_9P" write x`. = Your first tag word A script on `PATH` is a word you can click: command panes inherit pardes's environment. This `Fmt` runs `gofmt` on the Go file whose tag it was clicked in and reloads the pane. It finds the pane through `$winid`, not `PARDES_PANE`, which is the command pane it runs in: ```sh #!/bin/sh # Fmt: gofmt the Go file whose tag it was clicked in, then reload it n=${winid:?Fmt: click me in a pane tag} m=${PARDES_MOUNT:-} if [ -n "$m" ]; then 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) # the file it shows case $f in *.go) ;; *) echo "Fmt: $f is not Go" >&2; exit 1 ;; esac echo Save | wr pane/$n/ctl && gofmt -w "$f" && echo get | wr pane/$n/ctl ``` `get` on a pane's ctl reloads its text from the file on disk. Type `Fmt` into a Go pane's tag and click it with #btn("B2"); its output shows in a command pane. To have `Fmt` in every Go file's tag, follow the log and add it to each new `.go` pane (with a mount): ```sh #!/bin/sh # fmt-tags: put Fmt in the tag of every Go file opened from now on m=${PARDES_MOUNT:?fmt-tags: needs 9ns --mntgen} exec 3<>$m/log; echo 'follow new' >&3 while read -r what serial name <&3; do case $what:$name in new:*.go) printf ' Fmt' >> $m/pane/$serial/tag ;; esac done ``` = Recipes Each recipe starts after the session line above and this one, which makes a pane of your own: reading #file("pane/new") makes a pane and answers its serial, and `rmdir $p` closes it. Work through that pane's #file("look") and #file("exec"), not the root's: #cmd("n=$(cat $m/pane/new); p=$m/pane/$n") #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: read its #file("body") before you close it. In #file("index") a `term` is a shell, a `cmd` a command's pane. == 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, and writing a record back has pardes do it. A record is a line, `MX31 36 1 5 Upper`: who (`M` mouse, `K` keyboard, `E` or `F` a write to a file), what (`X` execute or `L` look in the body, `x` `l` in the tag), the range, a flag, the text's length and the text (#doc("fs", section: "event")). This helper gives pane `$1` the tag words `Upper` (upper-case the selection) and `Done`; every other click is written back, and when it ends the pane's clicks are pardes's again: ```bash #!/bin/bash # upper PANE: own the tag words Upper and Done in pane PANE m=${PARDES_MOUNT:?upper: needs 9ns --mntgen}; p=$m/pane/$1 grep -q ' Upper Done' $p/tag || 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; echo x); sel=${sel%x}; 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 ``` = Traps - A refused write says only `Invalid argument` or `Input/output error`; its reason is `grep '^err' $m/log | tail -1`, not the log's last line, which after a refused Del may be `+Unsaved`'s `new`. A write that succeeds adds no record, so check its status first. - The root #file("look") and #file("exec") act at the pane with the keyboard, which another client, an idle shell or an event helper may own. Use a pane's own. - Read #file("look"), #file("exec") or #file("pager") on the open you wrote (`exec 3<>…`): a fresh open reads the session's last answer, from whichever client wrote it. - #file("addr") is the pane's and moves on: each `/re/` searches from the last address, and a #file("data") write leaves it past the text. Write #file("addr") before each replacement; a failed one leaves none. - An Edit `x` that matches nothing succeeds: check the text. - Through a mount, `printf 'a\nb\n' > ctl` arrives one write per line: send a block as one write, ending in a newline. - Read #file("event") on a descriptor your shell owns, never `cat $p/event | while read`: the `cat` outlives the loop, holds #file("event") and swallows the next click. - `head` through 9ns says `Illegal seek` on #file("index"), #file("layout"), #file("log") and #file("recent"): use `sed -n 1p`. = An isolated session Never experiment on a session someone is using. Strip every `PARDES_*` variable first, then `pardes --detach=NAME &` with its own `HOME` and `XDG_*` directories, and mount it with `9ns --mntgen` (it is `$NINE_MOUNT/pardes/NAME`). Kill it when done.