--- 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//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 \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 ` \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).