// 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, so a program that opens files is an extension: ``` $ ls $m README col commands ctl exec focus index layout listeners log look os pager pane recent screen status tag tagexec $ cat $m/index 1 term 0 /home/me/src 1 2 text 0 /home/me/src/+New 1 ``` The reference (#doc("fs")) has every file. = Finding the session #pairs( [`PARDES_9P`], [the session's socket, in every shell and command pane], [`PARDES_PANE`], [the serial of the pane the shell runs in], [`$winid`], [the serial of the pane a command was clicked in, as in acme (unset from a column's or the workspace's tag)], [`PARDES_MOUNT`], [the session as a directory, under `9ns --mntgen` (#doc("setup", section: "ninens"))], ) Every recipe here starts from this line: #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. This `Fmt` finds its pane through `$winid` (`PARDES_PANE` 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: reload ``` Type `Fmt` into a Go pane's tag and #word("Exec") it (middle-click: run the builtin a word names, or else run it as a shell line); 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 #word("Exec") and #word("Look") clicks (right-click: open what the text names, or else find it): 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` #word("Exec") or `L` #word("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 owns the tag words `Upper` and `Done` in pane `$1` and writes every other click back: ```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 #word("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 #word("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. Under `9ns --mntgen`, start one with an empty environment and home, and quit it when done: ``` env -i PATH="$PATH" HOME="$(mktemp -d)" XDG_RUNTIME_DIR="$XDG_RUNTIME_DIR" pardes --detach=try & m=$NINE_MOUNT/pardes/try cat $m/index echo Exit > $m/exec ```