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 1The reference (→ Reference) has every file.
Finding the session
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 (→ 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: reloadType 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
doneRecipes
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 | |
| open a file at a place | |
| fill, name, save, close | |
| say something | |
| replace everywhere | |
| insert after a match | |
| replace one match | |
| delete line 3 | |
| select line 3, read it | |
| run a command | |
| run at a prompt | |
| type, interrupt | |
| ask the language server | |
| follow what happens | |
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 againTraps
- A refused write says only
Invalid argumentorInput/output error; its reason isgrep '^err' $m/log | tail -1, not the log’s last line, which after a refusedDelmay be+Unsaved’snew. A write that succeeds adds no record, so check its status first. - The root
lookandexecact 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,execorpageron the open you wrote (exec 3<>…): a fresh open reads the session’s last answer, from whichever client wrote it. addris the pane’s and moves on: each/re/searches from the last address, and adatawrite leaves it past the text. Writeaddrbefore each replacement; a failed one leaves none.- An
Editxthat matches nothing succeeds: check the text. - Through a mount,
printf 'a\nb\n' > ctlarrives one write per line: send a block as one write, ending in a newline. - Read
eventon a descriptor your shell owns, nevercat $p/event | while read: thecatoutlives the loop, holdseventand swallows the next click. headthrough 9ns saysIllegal seekonindex,layout,logandrecent: usesed -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