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
|
// 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).
Two clients do the rest. `9ns` comes from cloud9
(`git.sr.ht/~gbrls/cloud9`; its `zig build` installs `9ns` on Linux) and
mounts a 9P tree through FUSE in a private namespace, no root needed;
`9p` is plan9port's, and reads and writes without a mount.
#cmd("9ns --mntgen # every posted session, under $NINE_MOUNT")
#cmd("9ns --unix \"$PARDES_9P\" -- sh -c 'cat \"$NINE_MOUNT/index\"' # one session, for that command")
#cmd("9p -a \"unix!$PARDES_9P\" read index # no mount")
Under `9ns --mntgen` find this pane's session from its socket:
#cmd("[ -n \"$NINE_MOUNT\" ] || echo 'no 9P mount: use 9p or the Python client'")
#cmd("s=${PARDES_9P##*/pardes-9p-}; m=$NINE_MOUNT/pardes/${s%.sock}")
#cmd("cat \"$m/index\"")
Under `9ns --unix` the session is `$NINE_MOUNT` itself (`m=$NINE_MOUNT`),
and in a #word("Tty9p") terminal (#key("SPC n 9")) it is `$PARDES_MOUNT`.
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.
#word("Tty9p") opens a terminal with the session kernel-mounted (Linux
v9fs): it asks for your sudo password in the pane, mounts the socket in a
private mount namespace and starts your shell as you, with
`PARDES_MOUNT` set. Only that shell sees the mount, and each takes one of
the session's 16 connections. It needs the `9p` and `9pnet_fd` kernel
modules and the `pardes-v9fs` helper the build installs beside `pardes`.
= 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")],
[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.
= No mount: the Python client
`test/ninep.py` in the source tree speaks 9P itself, for when nothing is
mounted or a fid must stay open (#file("event"), #file("pty/data"), a
followed #file("log")). Its paths are the served root:
```python
import sys
from ninep import Client # PYTHONPATH=test
with Client(sys.argv[1]) as c: # a socket path, or (ip, port)
print(c.read('/index').decode(), end='')
n = int(c.read('/pane/new'))
c.write(f'/pane/{n}/body', b'hello\n', truncate=True)
fid = c.open(f'/pane/{n}/event', 0) # hold event: clicks come here
c.write(f'/pane/{n}/exec', b'Msg hi\n')
print(c.read_fid(fid, 0, 4096)) # b'FX0 0 1 6 Msg hi\n'
c.close(fid)
c.remove(f'/pane/{n}')
```
`client.screen()` returns the parsed #file("/screen").
= 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.
|