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
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
|
# Filesystem
Every native session serves 9P2000 on a Unix socket. Pane shells receive
`PARDES_PID` (the editor's process id), `PARDES_9P` (socket path) and
`PARDES_PANE` (pane serial). The socket is
`$XDG_RUNTIME_DIR/pardes-9p-<pid>.sock`, or lives under
`~/.local/state/pardes` when XDG_RUNTIME_DIR is unset. Detached sessions use
their session name; `--9p=<name>` overrides it.
A `pardes <file>` launched from a pane forwards Look to that pane over 9P.
`PARDES_PID` alone says the shell is inside pardes; `PARDES_9P` and
`PARDES_PANE` say how to reach it, and a launch that has the first without the
other two refuses rather than opening a second editor. `--nested` opens a
separate editor and withholds `PARDES_PID` from its pane shells, so a pardes
started in one of them runs a session of its own; its 9P service stays
available.
Look resolves the OS filesystem first, then the editor's virtual filesystem.
Explicit paths bypass that search:
| Editor path | Meaning | 9P server path |
|---|---|---|
| `/n/os/proc/self` | OS filesystem | `/os/proc/self` |
| `/n/self/pane/2/body` | pane 2's text | `/pane/2/body` |
| `/virtual/pane/2/body` | the same, in the editor's own spelling | `/pane/2/body` |
| `/virtual/src/pardes.zig` | source embedded in this build | `/src/pardes.zig` |
| `/n/peer/pane/2/body` | another session's text | peer's `/pane/2/body` |
The mount name `self` is reserved and maps to the server root, so `/n/self/X`
and `/virtual/X` both name the served `/X`.
`--mount=peer=work` mounts the named session `work`; the dial can also be an
absolute socket path, `unix!/path`, `tcp!IP!port`, or `quic!IP!port`.
At runtime, use `Mount peer dial` and
`Unmount peer`. There are eight named mounts; `os` and `self` are reserved.
Unmount refuses mounts still used by a pane, its working directory, or a
pending Save. Mounts are saved in dumps. Save uses the file's original mount.
`pardes --9p-tcp='tcp!127.0.0.1!5640'` adds a TCP listener alongside the Unix
socket. Build with `-Dquic=true` and system OpenSSL 3.6+ to enable QUIC;
`--9p-quic='quic!127.0.0.1!5641'` adds its listener. Both accept numeric
IPv4/IPv6 addresses, not DNS names. Listener port zero chooses a free port;
`/listeners` reports all active dial addresses.
All connections have session access, including `os`. TCP is unencrypted.
QUIC uses an ephemeral TLS identity without peer verification or login.
It carries 9P2000 on one bidirectional stream with ALPN `pardes-9p`.
Unix and TCP connections share four slots served by cloud9's `std.Io`
runner; QUIC has four of its own on the editor's poll loop. OpenSSL's
internal buffers are separate, dynamically allocated memory.
[Plan9port's client](https://9fans.github.io/plan9port/man/man1/9p.html) can
drive Unix or TCP without a kernel mount, and `9ns` mounts the tree in a
private namespace:
```sh
9p -n -a "unix!$PARDES_9P" read index
9p -n -a 'tcp!127.0.0.1!5640' ls pane/1
9ns --unix "$PARDES_9P" -- sh -c 'cat "$NINE_MOUNT/index"'
```
For [Linux v9fs](https://www.kernel.org/doc/html/latest/filesystems/9p.html),
use `version=9p2000,cache=none,access=any` and `trans=unix`, or `trans=tcp`
with `port=5640`. Set `uname`, `dfltuid`, and `dfltgid` for the local user.
Leave `aname` empty. The opt-in
[Linux v9fs experiment](v9fs.md) tests a kernel mount in a separate subprocess
namespace (`zig build v9fs-test`, requiring explicit mount authorization).
Neither 9P2000.u nor 9P2000.L is implemented.
Existing Plan9port/v9fs clients need a userspace bridge for QUIC.
## The served tree
```
/README this guide, also src/fs-help.txt
/index one line per pane: serial, kind (text|term|pdf|image), dirty flag, name
/status pid, version and pane count
/look write a line: a right click on it at the active pane; read: the serials it touched
/exec write a line: a middle click; read the same serials
/log one line per editor event: new|del|rename|save <serial> <name>; reads park
/screen rendered screen JSON; frozen per open handle
/listeners the session's dial addresses
/pane/new open it to make a pane; the read answers that pane's serial
/pane/<n>/ name body tag ctl addr dot limit data xdata sel dirty mark scroll
errors event look exec, plus pty/{ctl,status,data} on terminals
/os/ the host filesystem
/src/ the editor's embedded sources, only when built with -Dembed-sources=true
```
A pane is made by **opening** `/pane/new`, and closed by Tremove on
`/pane/<n>` (`rmdir`), which is the only remove the tree serves; Tcreate is
refused everywhere, as it is in acme. Reading the open fid answers the serial
of the pane that open made, so `n=$(cat /pane/new)` makes one and names it in
a line. Each open makes another pane, and two reads of one fid answer the same
serial: the open acted, the read only observes. Closing the fid leaves the
pane.
This is `/net/tcp/clone`'s mechanism, not acme's `new`, and the difference is
deliberate. acme allocates during the *walk* and lets the walk land inside the
new window, so `/dev/new/body` works in one step (acme(4): "accessing any file
in `new` creates a new window"). acme can also afford to list `new`, because a
Plan 9 directory read carries the stat of every entry and nothing walks. A
kernel or FUSE mount is not so lucky: it walks and stats each name a listing
gave it, so an allocate-on-walk name would make a pane per `ls -l`. Allocating
on open instead keeps `new` listed and `ls` honest — a stat is not an open —
at the cost of acme's one-step `new/body`. Nothing in the tree is created by
list, stat, walk or read; only that one open. Every other name in `/pane` is a
serial.
`/look` and `/exec` are the editor's two clicks, one per line of a write:
- a line written to `look` is a right click on it: a path opens a file,
`file:12` jumps to a line, a directory opens a shell there, a URL opens in
the browser.
- a line written to `exec` is a middle click: a command word from
`src/builtins.zig` (`Save`, `Del`, `New`, `Newcol`, `Mount NAME DIAL`,
`Unmount NAME`, `Dump`, `Restore`, `Msg TEXT`, `Find`, `Grep`, `Tty`, ...),
or anything else, which runs in the pane's terminal.
The root's pair clicks at the active pane and `/pane/<n>/look` and
`/pane/<n>/exec` at that pane. Blank lines are skipped, and every other line
is checked before any of them runs, so a control character fails the whole
write with EINVAL; a command that fails inside the editor is reported on the
message row, not as a write error. Reading any of these files answers the
serials of the panes the last command created, or, when it created none, the
pane a look focused or the pane an exec acted on (even one it closed), one
per line.
`/pane/<n>/name` reads the pane's file name (a terminal's directory) and
writing it renames the buffer; a relative name resolves against the pane's
directory. `body` appends on write and replaces on truncating open. `sel`
reads the selected text and writing it replaces the selection. `errors`
appends to the directory's `+Errors` pane. Holding `event` open redirects the
pane's Look and Exec clicks to that client; writing a record back performs the
action. `ctl` reads acme's window status line — serial, tag length, body
length, a reserved zero, the dirty flag, the width in cells, the font and the
tab width — and takes one verb, `get`, which reloads the buffer from the name
it carries.
The three range files `addr`, `dot` and `limit` each read the pair of offsets
they also accept, so copying one onto another is all that acme's `addr=dot`,
`dot=addr` and `limit=addr` ever were. A write is either that pair or an
address expression (`#0,#5`, `/pattern/`, `2+1`); `addr` selects what `data`
and `xdata` read or replace, `dot` is the editor's own selection and moving it
scrolls the pane into view, and `limit` bounds a search and reads empty until
it is set. Truncating a range file empties it; truncating `limit` lifts it.
`addr` belongs to the pane rather than to a client and keeps what was written
until someone writes or truncates it, so writing an address and reading it
back evaluates it, which is what acme(4) promises of its own `addr`.
The three flag files `dirty`, `mark` and `scroll` read `0` or `1` and take
`0` or `1`: whether the buffer differs from its file, whether a write pushes
an undo point (writing `1` pushes one now), and whether a write scrolls the
pane. Truncating `tag` clears the part of the tag you may edit.
Stats report real lengths for `index`, `status`, `look`, `exec`, `listeners`,
`name`, `body`, `tag`, `sel`, `ctl`, the range files and the flag files, and
for `log`, `event` and `pty/data` the length of the record a read would
answer, which is zero when nothing is waiting. Modes are 0644/0666 (0444 for
read-only files, 0222 for write-only); mtime is the pane's last edit or the
process start. The qid version of `body`, `data` and `xdata` is the pane's
revision, so a stat sees an edit land without reading the text; every other
file leaves it zero rather than promise a version it cannot keep. Directory
entries carry no sizes, and neither does `/screen`, which has no length until
an open renders its frame; stat the entry.
`/log` records `new`, `del`, `rename` and `save` while at least one client
holds it open; a read parks until a record arrives. `/screen` returns JSON with
`cols`, `rows`, `cursor`, a `styles` table, and row-major `cells` of
`[grapheme, style_index]`. Each open freezes one frame until close. A
terminal `body` freezes its history on the first read of each open handle;
`pty/data` streams live output. Screen and terminal-body snapshots share 32
handle slots, released on close or disconnect.
`-Dembed-sources=true` embeds the editor's sources and serves them under
`/src` (and `/shaders` on GUI builds). `EffectCode <effect>` lists the current
backend's implementation files under `/virtual`, which Look opens; without the
option the command reports the sources as unavailable. The esp32p4 build
enables the option by default, so the device can serve its own source.
This is a control filesystem, not a complete POSIX export. Native filenames
may contain up to 255 bytes. Existing regular OS files support read, write,
and truncation to zero; under `/os` protocol create, remove, rename and other
metadata changes are refused, as is every create in the control tree and every
remove in it but a pane directory's. Ownership and permissions under `/os` are
synthetic.
Zero-length truncation accepts the accompanying `mtime` hint sent by Linux
v9fs; the hint is not stored. Standalone timestamp changes remain refused.
The tree lives in `src/ninep/`: `tree.zig` (nodes, lookup, readdir, dispatch,
and the editor's reply payload over cloud9's backend contract), `pane.zig`
(pane files), `ctl.zig`, `addr.zig`, `pty.zig`, `events.zig` (event and log
streams), `screen.zig` and `sources.zig`. The protocol engine is cloud9's
`fs.Server`, configured in `src/9p.zig` (the editor's and the board's
capacities); the transports are `src/9p_io.zig` (cloud9's `serve.Runner`
for Unix and TCP, a poll loop for QUIC, and the 9P client for mounts);
`src/fs.zig` keeps host access, mounts, resolution, find and grep.
`zig build fs-test` drives real sessions using the independent Python client
in `test/ninep.py`; `zig build fs-discovery-test` checks that browsing
creates nothing, that the walk to `/pane/new` and a remove work, and that
`look`, `exec`, `name`, `sel` and `log` behave. `zig build
9p-test` checks the two engine configurations' budgets (the engine's own
tests are cloud9's `zig build test`); `zig build fs-bench` measures
filesystem transactions in the core.
`zig build fs-test quic-test -Dquic=true` also exercises QUIC mounts and I/O.
|