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
|
---
name: pardes-9p
description: Inspect and drive a running Pardes editor over 9P, 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 through its control filesystem.
---
# Pardes over 9P
Use the existing [Python client](../../../test/ninep.py) for ad hoc interaction
and functional tests. Do not build another wire client. Project tooling stays
in Zig. Run the examples from the repository root; paths below are relative to
that root unless linked.
## Connect and identify panes
Every native session opens a Unix socket. Inside a pane, `PARDES_9P` names the
socket and `PARDES_PANE` is that pane's serial. Outside Pardes, find
`pardes-9p-*.sock` under `$XDG_RUNTIME_DIR`, or `~/.local/state/pardes` when
that variable is unset. Select the intended session explicitly; do not assume
the newest socket is the right one. Names come from `--9p=name`, the detached
session name, or the process ID.
```sh
PYTHONPATH=test python3 -B - "$PARDES_9P" <<'PY'
import sys
from ninep import Client
with Client(sys.argv[1]) as client:
print(client.read('/self/index').decode(), end='')
print(client.read('/self/listeners').decode(), end='')
PY
```
`Client` takes a raw Unix socket path, or `(numeric_ip, port)` for TCP; it
does not parse Pardes dial strings or implement QUIC. It negotiates 9P2000,
uses a five-second socket timeout, and closes on leaving `with`.
The first field of each index row is a stable pane serial, not a slot or row
number. The tag starts after five numeric fields; use `row.split(maxsplit=5)`
to preserve spaces. Inspect `/self/pane/<serial>/tag`, `body`, or directory
entries before choosing a target. Re-read the index after actions that might
open, reuse or close panes.
Wire paths start with `/self` or `/os`. `/n/self`, `/n/os`, `/n/peer` and
`/virtual` belong to editor Look paths, not the server root. For example,
Look `/virtual/src/pardes.zig` corresponds to reading `/self/src/pardes.zig`.
## Edit, Look and execute
For a file or scratch pane, with a connected `client` and a confirmed
`serial`, let `pane = f'/self/pane/{serial}'`. Terminal body writes instead
send child input; truncation does not erase terminal history.
| Operation | Client call |
|---|---|
| Read text | `client.read(pane + '/body')` |
| Append text | `client.write(pane + '/body', b'text\n')` |
| Replace all text | `client.write(pane + '/body', b'text\n', truncate=True)` |
| Select a byte range | `client.write(pane + '/addr', b'#0,#2')` |
| Replace that range | `client.write(pane + '/data', b'pub fn')` |
| Show the addressed selection | `client.write(pane + '/ctl', b'dot=addr\n')` |
| Look from this pane | `client.write(pane + '/ctl', b'look /virtual/src/pardes.zig:10\n')` |
| Save | `client.write(pane + '/ctl', b'put\n')` |
| Reload | `client.write(pane + '/ctl', b'get\n')` |
| Close, refusing dirty text | `client.write(pane + '/ctl', b'del\n')` |
`delete` force-closes, discarding unsaved text. `get` replaces edits with file
contents. Use those only when that loss is intended. Reading `/self/new/ctl`
creates a scratch pane and returns its serial as the first field; it is not
an observational read.
`ctl` is not a builtin interpreter. Execute builtins through a body Exec event
on an owned scratch pane:
```python
from fs import new_pane, execute
control = new_pane(client, b'')
execute(client, control, 'Msg 9p-ready')
```
`execute` overwrites that pane's body with the command, then writes
`MX0 <UTF-8-byte-length>\n` to its `event` file. Keep the control pane separate
from user text. The same helper can run `Mini path`, `Mount peer dial`, and
`Unmount peer`; commands resolve relative to the control pane's directory.
A successful event write acknowledges dispatch, not completion: inspect the
resulting pane, message or screen for success.
Opening `addr` resets its range. To inspect the current selection, open
`addr` first, write `addr=dot\n` to `ctl`, then use `read_fid` on that already
open handle. Do not replace that last step with `client.read`, which reopens
and resets it. Address state is shared by the pane, not private to a client.
## Terminal input and screen observations
Only terminal panes have `pty/`. Write keystroke bytes to `pty/data`, not
`body`: `client.write(pane + '/pty/data', b'printf hello\r')` submits a shell
command. For an interactive application, send its actual input bytes;
`b'\x03'` is Ctrl-C, and Ctrl-U is `b'\x15'` where that application supports it.
These inputs go to the child terminal, not Pardes editor key bindings.
`client.screen()` returns `cols`, `rows`, `cursor`, `styles`, and row-major
`cells` of `[grapheme, style_index]`. Reconstruct rows using `cols`; resolve
each cell's style through `styles` when checking highlighting. Compare colors
and attributes, not just style-table indices or flattened text.
Each screen open freezes one frame. A terminal `body` freezes history on its
first read. `client.read` and `client.screen` reopen each time; use repeated
calls for fresh observations. Do not poll a stale handle. Poll a specific
condition with a deadline and a short delay, rather than a fixed long sleep.
For large histories, measure whole-body reads separately from screen polling;
9P write-to-observation timing includes RPC, rendering and polling overhead.
For live terminal output or plugin events, use `open` / `read_fid` / `close`,
not the read-until-EOF helper. `pty/data` captures output while held open;
it is not a history replay. Both files are shared, consuming queues, not
per-client broadcasts; slow readers can lose older data. Holding `event` open
intercepts Look/Exec clicks, so it is not a passive logger. For Look/Exec
records whose offsets identify the intended file/tag text, forward the short
record `<origin><action><q0> <q1>\n`, not the entire report with flags and text.
Expansion/chord reports need explicit handling; terminal-body events can have
empty ranges with text carried only in the report, so short writeback cannot
reproduce them. Read the event implementation before building an interceptor.
Close handles in `finally`; disconnect after a socket timeout. The service
shares four connection slots and 32 screen/terminal-history snapshot handles.
## Exercise an isolated session
Reuse [test/fs.py](../../../test/fs.py), which starts a private session with
temporary configuration and cleans up its editor process. Pass an existing
native binary, not a benchmark executable:
```sh
PYTHONPATH=test python3 -B - /absolute/path/to/pardes <<'PY'
from pathlib import Path
import sys
import tempfile
from fs import session, new_pane, execute
binary = str(Path(sys.argv[1]).resolve())
with tempfile.TemporaryDirectory(prefix='pardes-9p-skill-') as directory:
with session(binary, Path(directory), 'skill') as (client, address):
serial = new_pane(client, b'fn main() void {}\n')
pane = f'/self/pane/{serial}'
client.write(pane + '/ctl', b'name probe.zig\n')
client.write(pane + '/addr', b'#0,#2')
client.write(pane + '/data', b'pub fn')
assert client.read(pane + '/body') == b'pub fn main() void {}\n'
control = new_pane(client, b'')
execute(client, control, 'Msg 9p-ready')
frame = client.screen()
assert '9p-ready' in ''.join(cell[0] for cell in frame['cells'])
print('9P edit, builtin and screen checks passed')
PY
```
For terminal tests, use `session(..., tty=True)` and read
[test/agent_session.py](../../../test/agent_session.py) for bounded interactive
driving. Its readiness text and history threshold are application-specific;
session cleanup alone does not guarantee arbitrary grandchildren have exited.
Use [test/snapshot.zig](../../../test/snapshot.zig) when editor key/mouse input
or an independent terminal-rendering comparison matters; 9P screen inspection
alone does not test physical input routing or the host renderer.
Read [docs/fs.md](../../../docs/fs.md) for runtime mounts, TCP/QUIC listeners,
Plan9port and Linux v9fs compatibility. Unix is always available; network
listeners are opt-in and grant full session/OS-file access. Use isolated
loopback listeners for tests. For less common control verbs or event details,
read their implementation and tests in [src/fs.zig](../../../src/fs.zig).
|