summaryrefslogtreecommitdiff
path: root/docs/typ/scripting.typ
blob: d3e425731bd1cc0dcabd28de42f85954a0cc4a19 (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
// 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

The paths below are under `$m`; `p=$m/pane/$n` for a pane.

#pairs(
  [look around], [#cmd("cat $m/index\ncat $m/layout\ncat $m/focus\ncat $m/commands")],
  [open a file at a place], [#cmd("echo \"$PWD/main.zig:120\" > $m/look; cat $m/look # where it went\necho 'main.zig:0/fn main/' > $m/look # the first match")],
  [make, fill, name, save], [#cmd("n=$(cat $m/pane/new) # one pane per open\nprintf 'hello\\n' > $m/pane/$n/body # > replaces, >> appends\necho \"$PWD/notes.txt\" > $m/pane/$n/name\necho Save > $m/pane/$n/ctl\nrmdir $m/pane/$n # close it")],
  [say something], [#cmd("echo 'Msg hello' > $m/exec")],
  [replace everywhere], [#cmd("echo 'Edit ,x/foo/c/bar/' > $p/ctl && grep -c foo $p/body")],
  [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("echo 'make test' > $m/exec; c=$(cat $m/exec) # its pane\ngrep \"^exit $c \" $m/log | tail -1 # exit c N")],
  [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("printf 'q' > $m/pane/$t/pty/data # \\r Enter, \\x03 Ctrl-C\necho 'sig INT' > $m/pane/$t/pty/ctl")],
  [ask the language server], [#cmd("echo /myFunc/ > $p/addr; echo dot=addr > $p/ctl\necho Hover > $p/exec # also Rename new, Symbols")],
  [act on the pane a command came from], [#cmd("#!/bin/sh\n# fmt-here: put its name in a pane's tag, click it with B2\nn=$winid; f=$(cat $m/pane/$n/name)\necho Save > $m/pane/$n/ctl && zig fmt \"$f\" && echo get > $m/pane/$n/ctl")],
  [follow what happens], [#cmd("exec 3<>$m/log; echo 'follow new' >&3\ntimeout 30 cat <&3; exec 3<&-")],
)

`follow` without `new` replays the ring first. #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")).

== 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`; the
  reason is the `err` record in the log (`tail -1 $m/log`). Only writes
  log: a refused open, truncation or `rmdir` has its errno alone.
- #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.
- `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.