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
|
---
name: pardes-9p
description: Inspect and drive a running Pardes editor through its 9P control filesystem, or exercise its panes, builtins, terminal input and rendered output in an isolated session. Use for Pardes interaction, plugin development and end-to-end debugging.
---
# Pardes over 9P
Pardes is files: `cat`, `echo >`, `ls` and shell scripts are the whole
interface. [docs/fs.md](../../../docs/fs.md) is the reference for every
file; this page is recipes. Paths are relative to the repository root.
## Find the session
```sh
[ -n "$NINE_MOUNT" ] || echo 'no 9P mount: use the Python client below'
s=${PARDES_9P##*/pardes-9p-}; m=$NINE_MOUNT/pardes/${s%.sock}
cat "$m/index"
```
`$NINE_MOUNT` is set by `9ns --mntgen` (usually `/mnt/9p`). `$PARDES_9P` is
the session socket of the pane you run in, `$PARDES_PANE` that pane's
serial. Under `9ns --unix SOCK -- cmd` the session is the mount itself:
`m=$NINE_MOUNT`. In a `Tty9p` shell: `m=$PARDES_MOUNT`. A dead session's
entry answers `Input/output error`: name the session, never glob. A running
session serves the binary that started it.
## Look around
```sh
cat "$m/index" # serial kind dirty name column, a pane a line
cat "$m/layout" # columns, then `active <serial>`
cat "$m/focus" # the pane with the keyboard
cat "$m/README" # the one-screen guide
cat "$m/commands" # every builtin and where it goes
```
Parse an index row with `rsplit(maxsplit=1)` first: names may hold blanks.
Re-read the index after anything that opens or closes panes.
## Open a file, go to a place
```sh
echo "$PWD/main.zig:120" > "$m/look"; cat "$m/look" # serial it landed in
echo "main.zig:0/fn main/" > "$m/look" # first match; without 0, the next after dot
```
A miss opens nothing, reads back empty and logs an `err <serial> look: ...`
line; the write itself succeeds. `L:C` columns are bytes.
## Make, fill, name and save a pane
```sh
n=$(cat "$m/pane/new") # each open makes one: read it once
printf 'hello\n' > "$m/pane/$n/body" # > replaces the text
printf 'more\n' >> "$m/pane/$n/body" # >> appends
echo "$PWD/notes.txt" > "$m/pane/$n/name"
echo Save > "$m/pane/$n/ctl"
rmdir "$m/pane/$n" # close it, saved or not
```
## Edit text
```sh
p=$m/pane/$n
echo 'Edit ,x/foo/c/bar/' > $p/ctl # sam edit, one undo step
echo '/old/' > $p/addr; printf 'new' > $p/data # replace the next match
echo 0 > $p/addr # back to the top
echo '3' > $p/addr; : > $p/data # delete line 3
cp $p/addr $p/dot; cat $p/sel # select it, read the selection
```
`addr` searches on from the last address (it is the pane's, shared by all
clients); after a `data` write it sits just past the text, so write `addr`
before each replacement. A failed `addr` leaves none and `data` refuses:
check the write's status. An Edit block of several lines goes in one open:
`printf 'Edit ,x/foo/{\ni/</\na/>/\n}\n' > $p/ctl`.
## Run a command
```sh
echo 'make test' > "$m/exec"; c=$(cat "$m/exec") # a command pane; its serial
grep "^exit $c " "$m/log" | tail -1 # `exit <serial> <N>` once done
```
At a terminal's prompt, run and get the status and output in one go:
```sh
t=$(awk '$2=="term"{print $1; exit}' "$m/index")
exec 3<>"$m/pane/$t/pty/run"; echo 'ls' >&3; cat <&3; exec 3<&- # `exit N`, then output
printf 'q' > "$m/pane/$t/pty/data" # raw keystrokes (\r Enter, \x03 Ctrl-C)
echo 'sig INT' > "$m/pane/$t/pty/ctl" # interrupt
```
`busy: <program> is running` means the prompt is not free: a program holds
the terminal, so talk to it through `pty/data`.
Ask the language server at a file pane's cursor (`addr` then `dot=addr`):
```sh
echo '/myFunc/' > "$m/pane/$n/addr"; echo 'dot=addr' > "$m/pane/$n/ctl"
echo Hover > "$m/pane/$n/exec" # +Hover; Rename new, Diagnostics, Symbols
echo Lspwhy > "$m/pane/$n/exec" # +Lsp: why the last query found what it did
echo Lspinfo > "$m/pane/$n/exec" # which server, and its state
```
## Follow what happens
```sh
exec 3<>"$m/log"; echo 'follow new' >&3
timeout 30 cat <&3 # one record a line, as they come
exec 3<&-
```
`follow` (without `new`) replays the ring first. `tail -f` sees nothing new.
Through FUSE `read -t` never times out: use `timeout`. After a `Restore`
every connection is cut: dial again and restart the mount.
## Traps
- A refused write says only `Invalid argument` or `Input/output error`; the
reason is the `err` line in `$m/log` (`tail -1 "$m/log"`).
- Only writes log: a refused open, truncation (`> data` after a failed
`addr`) or `rmdir` has its errno alone.
- Settings and session words go to `$m/ctl`; pane words (`Undo`, `Save`,
`Del`) to `$m/pane/<n>/ctl`. The wrong one is refused naming the right one.
- `Exit`, `Restore`, `Del`, `get` refuse once over unsaved text; the same word
again discards.
- A word no builtin knows is a shell command (`exit 127` if a typo).
- `lock` needs a held fd: `exec 3>$p/ctl; echo lock >&3; ...; exec 3>&-`.
- plan9port `9p write` truncates: it replaces a whole `body`.
## No mount: the Python client
Use [test/ninep.py](../../../test/ninep.py) when nothing is mounted, or when
a fid must stay open (`event`, `pty/data`, a followed `log`). Its paths are
the served root (`/index`, `/pane/2/body`).
```sh
PYTHONPATH=test python3 -B - "$PARDES_9P" <<'PY'
import sys
from ninep import Client
with Client(sys.argv[1]) as c:
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}')
PY
```
`Client` takes a Unix socket path or `(ip, port)`; `client.screen()` returns
the parsed `/screen`. For `event` records and writing them back, see
[fs.md#event](../../../docs/fs.md#event).
## An isolated session
Never experiment on a session someone is using. Build a binary with
`zig build install -Dplatform=tty` (a bare `zig build`, or `--prefix
zig-out`, installs over `~/.local/bin`). `test/fs.py` starts a private
session and cleans it up:
```sh
PYTHONPATH=test python3 -B - "$(realpath zig-out/bin/pardes)" <<'PY'
import sys, tempfile
from pathlib import Path
from fs import session, new_pane, execute
with tempfile.TemporaryDirectory(prefix='pardes-9p-') as d:
with session(sys.argv[1], Path(d), 'probe') as (client, address):
n = new_pane(client, b'fn main() void {}\n')
execute(client, n, 'Msg ready')
assert 'ready' in ''.join(c[0] for c in client.screen()['cells'])
PY
```
By hand: `pardes --detach=NAME &` with its own `HOME` and `XDG_*`
directories, then `9ns --mntgen` (the session is `$NINE_MOUNT/pardes/NAME`)
or `9ns --unix $XDG_RUNTIME_DIR/pardes-9p-NAME.sock -- sh`. Strip every
`PARDES_*` variable first, or a file argument goes to the session you are
inside. `session(..., tty=True)` and
[test/agent_session.py](../../../test/agent_session.py) drive terminals.
|