summaryrefslogtreecommitdiff
path: root/docs/typ/scripting.typ
blob: 7bf04efe3be632a834ad7f383cf98817610f4b69 (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
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
// Scripting pardes over 9P: finding the session, the recipes, and the
// traps. Every file's full semantics are in the reference; this is the
// five-minute path to using them.
#import "style.typ": key, keys, btn, chord, word, tag, addr, file, cmd, doc, pairs

Every session serves its panes, columns and tags as files, as acme does:
`cat`, `echo >` and `ls` are the whole interface, and a program that opens
files is an extension. The reference (#doc("fs")) has every file; this
chapter is how to reach them and what to do with them.

= Finding the session <find-the-session>

Each session listens on a Unix socket,
`$XDG_RUNTIME_DIR/pardes-9p-<name>.sock` (else under
`~/.local/state/pardes`), `<name>` being the pid, the `--detach=NAME` or
`--9p=NAME`, and posts it in the 9P registry as
`$XDG_RUNTIME_DIR/9p/pardes/<name>`. Shells in its panes get `PARDES_PID`
(the editor's pid), `PARDES_9P` (the socket) and `PARDES_PANE` (their
pane's serial).

Pane shells and command panes alike can reach the session; one rule picks
the client:

- *With a mount* (`$NINE_MOUNT` set, as under cloud9's `9ns --mntgen`;
  `git.sr.ht/~gbrls/cloud9`, whose `zig build` installs `9ns` on Linux),
  the session is a directory and plain `cat` and `echo >` work:
  #cmd("s=${PARDES_9P##*/pardes-9p-}; m=$NINE_MOUNT/pardes/${s%.sock}\ncat \"$m/index\"")
  (Under `9ns --unix SOCK -- cmd` the session is `$NINE_MOUNT` itself.)
- *Without one*, plan9port's `9p` talks to the socket:
  #cmd("9p -a \"unix!$PARDES_9P\" read index\necho Save | 9p -a \"unix!$PARDES_9P\" write pane/3/ctl")

The recipes below use `$m`; with `9p`, `cat $m/x` is `9p ... read x` and
`echo y > $m/x` is `echo y | 9p ... write x`. A dead session's registry
entry stays listed and answers `Input/output error`: name the session,
never glob. A running session serves the binary that started it. The
reference has the other ways in: a #word("Tty9p") terminal's kernel mount,
and the Python client in the source tree.

A command run from a pane (#btn("B2") on a line in its tag or text) also
gets `$winid`, the serial of that pane, as acme's commands do; it is unset
for one run from a column's or the workspace's tag, and `PARDES_PANE`
stays the command pane's own.

A paging command run through #file("pty/run") does not hang: the
terminal's pager is `pardes -`, which puts the text in a `+Pager` pane and
returns. A command pane pages through `cat`; its output is a pane already.
Both hold only where your environment names no pager.

= Recipes

Each recipe starts after these lines, which find the session (with a
mount) and make a pane of your own. Work through that pane's #file("look")
and #file("exec"), not the root's (see the traps):
#cmd("s=${PARDES_9P##*/pardes-9p-}; m=$NINE_MOUNT/pardes/${s%.sock}\nn=$(cat $m/pane/new); p=$m/pane/$n # a scratch of your own")

#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")],
  [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: another client's command in the same directory gets a
pane of its own. Read its #file("body") and poll its tag before you close
the open; once you have, the directory's next command may reuse the pane.
In #file("index") a `term` is a shell to type at, a `cmd` a
command's pane.

`follow` without `new` replays the ring first.

== A script for a tag

A script can act on the pane it was clicked from through `$winid`. This
one saves a Zig file, formats it and loads the result: put it on your
`PATH` as `fmt-here`, type `fmt-here` into a pane's tag and click it with
#btn("B2"). It follows the one-client rule itself, since a script starts
with nothing set but its environment:

```sh
#!/bin/sh
# fmt-here: save the pane it was clicked from, zig fmt its file, reload it
n=$winid
if [ -n "$NINE_MOUNT" ]; then
    s=${PARDES_9P##*/pardes-9p-}; m=$NINE_MOUNT/pardes/${s%.sock}
    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)
echo Save | wr pane/$n/ctl && zig fmt "$f" && echo get | wr pane/$n/ctl
```

Its output, and `exit 0`, show in the command pane it runs in. #file("pty/run") answers
`busy: <program> is running` when a program holds the terminal: talk to it
through #file("pty/data") instead.

== Event helpers

Holding a pane's #file("event") open takes its #btn("B2") and #btn("B3")
clicks: they arrive as acme's records instead of acting, so the pane's tag
can carry a helper's own words, and writing a record back has pardes do
it. The keyboard is never taken, and the clicks come back when the helper
closes the file. One open at a time; the record format is in the reference
(#doc("fs", section: "event")).

This helper gives pane `$1` two tag words of its own: `Upper` upper-cases
the selection and `Done` ends it. Every other click is written back, so it
acts as ever, and when the helper ends, its pane's clicks are pardes's again:

```bash
#!/bin/bash
# upper PANE: own the tag words Upper and Done in pane PANE
s=${PARDES_9P##*/pardes-9p-}; p=$NINE_MOUNT/pardes/${s%.sock}/pane/$1
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); 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
```

`read` takes a record a line, which suits words; a helper that must take
text holding newlines (a sweep over several lines) reads the record's `n`
bytes of text itself, with `read -N`.

== Waiting for an edit

`pardes --wait FILE` in a pane's shell returns when the pane showing FILE
is closed (#doc("tags", section: "editor")): `EDITOR='pardes --wait'` makes
pardes the editor of every program run there.

= Traps <traps>

- A refused write says only `Invalid argument` or `Input/output error`;
  its reason is the log's last `err` record: `grep '^err' $m/log | tail -1`.
  Not the log's last line: after a refused Del, Exit or Restore that may be
  `new N .../+Unsaved`. A write that succeeds adds no record, so check the
  write's own status first. Only writes log: a refused open, truncation or
  `rmdir` has its errno alone.
- The root #file("look") and #file("exec") act at the active pane, which
  another client, an idle shell or an event helper may own: a line written
  there may be typed into a terminal or taken by an event reader. Use a
  pane's own #file("look") and #file("exec").
- `head` through a 9ns mount says `Illegal seek` on the files that stat 0
  (#file("index"), #file("layout"), #file("log"), #file("recent")): they
  are streams. Use `sed -n 1p` or `awk 'NR==1'`; `tail -n` works.
- #file("addr") belongs to the pane, not to you, and moves on: each
  `/re/` searches from the last address, a #file("data") write leaves it
  just past what it wrote, and a read moves it too. Write #file("addr")
  before each replacement, `0` to start at the top. A failed address
  leaves none, and #file("data") refuses until you write one.
- An Edit `x` that matches nothing succeeds: check the result
  (`grep -c foo body`), not the exit status.
- Each open of #file("pane/new") makes a pane. `ls`, `stat` and `find`
  never do.
- Through a mount, bash's `printf 'a\nb\n' > ctl` arrives one write per
  line, so a block that should be refused whole runs its good lines before
  the bad one fails. Send a block as one write (`cat block > $p/ctl`), and
  end every write whose result matters with a newline.
- plan9port's `9p write` opens with OTRUNC: `echo x | 9p write pane/3/body` replaces the whole body. Append with `>>` through a mount.
- Read #file("event") on a descriptor your shell owns
  (`exec 3<>$p/event` ... `exec 3<&-`), never `cat $p/event | while read`:
  the `cat` outlives the loop and holds #file("event") open, so the pane's
  next click goes to it and is lost.
- Read #file("look"), #file("exec") or #file("pager") on the open you
  wrote: a read answers the panes touched by this open's last write, and a
  fresh open reads the session's last answer, from whichever client wrote
  it (`exec 3<>$p/exec; echo cmd >&3; cat <&3; exec 3<&-`).
- `tail -f log` never sees anything new: write `follow` on the open you
  read. Through a FUSE mount bash's `read -t` cannot time out: wrap the
  loop in `timeout`.
- Settings and session words go to #file("/ctl"), pane words
  (#word("Undo"), #word("Save"), #word("Del")) to #file("pane/<n>/ctl");
  the wrong one is refused, naming the right one.
- #word("Exit"), #word("Restore"), #word("Del") and `get` refuse once over
  unsaved text; the same word again discards.
- A word no builtin knows runs as a shell command: a typo ends `exit 127`.
- `lock` needs a held file descriptor:
  `exec 3>$p/ctl; echo lock >&3; ...; exec 3>&-`.
- Names in #file("index") may hold blanks: split a row with
  `rsplit(maxsplit=1)` first, and read #file("index") again after anything
  that opens or closes panes.
- A #word("Restore") hangs up every connection: dial again and restart the
  mount.

= An isolated session

Never experiment on a session someone is using. Strip every `PARDES_*`
variable first, or a file argument goes to the session you are inside;
then `pardes --detach=NAME &` with its own `HOME` and `XDG_*` directories,
and mount it with `9ns --mntgen` (the session is `$NINE_MOUNT/pardes/NAME`)
or `9ns --unix $XDG_RUNTIME_DIR/pardes-9p-NAME.sock -- sh`. Kill it when
done. From the source tree, `test/fs.py`'s `session()` starts a private
session and cleans it up, and `test/agent_session.py` drives terminals.