summaryrefslogtreecommitdiff
path: root/docs/typ/scripting.typ
blob: 47eb8a24f295b58c9930420ab9bffded91471e3a (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
152
153
154
155
156
157
158
159
160
161
// 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 <find-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 <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 <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
```