pardes Scripting

Scripting

Every session is a virtual filesystem, served over 9P and modelled on acme’s (→ Reference: Coming from acme), 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 (→ Reference) has every file.

Finding the session

PARDES_9Pthe session’s socket, in every shell and command pane
PARDES_PANEthe serial of the pane the shell runs in
$winidthe serial of the pane a command was clicked in, as in acme (unset from a column’s or the workspace’s tag)
PARDES_MOUNTthe session as a directory, under 9ns --mntgen (→ Setting up your environment: A 9P mount: 9ns)

Every recipe here starts from this line:

m=${PARDES_MOUNT:-}
cat "$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):

#!/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 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):

#!/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 pane/new makes a pane and answers its serial, and rmdir $p closes it. Work through that pane’s look and exec, not the root’s:

n=$(cat $m/pane/new); p=$m/pane/$n
look around
cat $m/index
cat $m/layout
cat $m/focus
cat $m/commands
open a file at a place
exec 3<>$p/look; echo "$PWD/main.zig:120" >&3; f=$(cat <&3); exec 3<&- # its pane
echo "$PWD/main.zig:0/fn main/" > $p/look # the first match
fill, name, save, close
printf 'hello\n' > $p/body # > replaces, >> appends
echo "$PWD/notes.txt" > $p/name
echo Save > $p/ctl
rmdir $p
say something
echo 'Msg hello' > $p/exec
replace everywhere
echo 'Edit ,x/foo/c/bar/' > $p/ctl
grep -c foo $p/body # 0: none left
insert after a match
echo 'Edit /old/a/ text/' > $p/ctl # from dot; one undo step
replace one match
echo /old/ > $p/addr && printf new > $p/data
delete line 3
echo 3 > $p/addr; : > $p/data
select line 3, read it
echo 3 > $p/addr; cp $p/addr $p/dot; cat $p/sel
run a command
exec 3<>$p/exec; echo "cd '$PWD' && make test" >&3; c=$(cat <&3) # its pane
until grep -q ') exit ' $m/pane/$c/tag; do sleep 0.2; done
cat $m/pane/$c/body; exec 3<&- # read it before letting go
run at a prompt
t=$(awk '$2=="term"{print $1; exit}' $m/index)
exec 3<>$m/pane/$t/pty/run; echo ls >&3; cat <&3; exec 3<&-
type, interrupt
t=$(awk '$2=="term"{print $1; exit}' $m/index)
printf 'q' > $m/pane/$t/pty/data # \r Enter, \x03 Ctrl-C
echo 'sig INT' > $m/pane/$t/pty/ctl
ask the language server
exec 3<>$p/look; echo "$PWD/main.zig" >&3; q=$m/pane/$(cat <&3); exec 3<&-
echo /myFunc/ > $q/addr; echo dot=addr > $q/ctl
echo Hover > $q/exec # also Rename new, Symbols
follow what happens
exec 3<>$m/log; echo 'follow new' >&3
timeout 30 cat <&3; exec 3<&-

A command pane your exec open was answered is yours while that open stays open: read its body before you close it. In index a term is a shell, a cmd a command’s pane.

Event helpers

Holding a pane’s event open takes its Exec and 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 Exec or L Look in the body, x l in the tag), the range, a flag, the text’s length and the text (→ Reference: event). This helper owns the tag words Upper and Done in pane $1 and writes every other click back:

#!/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 look and 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 look, exec or pager on the open you wrote (exec 3<>…): a fresh open reads the session’s last answer, from whichever client wrote it.
  • addr is the pane’s and moves on: each /re/ searches from the last address, and a data write leaves it past the text. Write 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 event on a descriptor your shell owns, never cat $p/event | while read: the cat outlives the loop, holds event and swallows the next click.
  • head through 9ns says Illegal seek on index, layout, log and 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