summaryrefslogtreecommitdiff
path: root/docs/typ/scripting.typ
blob: b3f5ca26f933a7a407b54fcc3ebe6b20c6c43a76 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
// 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 <find-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 <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 middle-click it; 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 middle- and right-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 <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.