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
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
|
# Filesystem
Every native session serves 9P2000 (not .u, not .L) as a control
filesystem, in acme's manner: panes, columns, tags and the session are files.
This page is the reference for what each file does. The served
[`/README`](../src/fs-help.txt) is its one-screen summary.
## Connecting
The socket is `$XDG_RUNTIME_DIR/pardes-9p-<name>.sock` (else under
`~/.local/state/pardes`), `<name>` being the pid, the `--detach=NAME`, or
`--9p=<name>`. A socket in the runtime directory is also posted in the 9P
registry as `$XDG_RUNTIME_DIR/9p/pardes/<name>` (a symlink to the socket;
[cloud9.md](cloud9.md#the-posted-9p-registry)).
Pane shells get `PARDES_PID` (the editor's pid), `PARDES_9P` (the socket)
and `PARDES_PANE` (their pane's serial).
**Forwarding.** `pardes FILE` run in a pane (a live `PARDES_PID`, with
`PARDES_9P` and `PARDES_PANE`) writes FILE to that pane's `look` and returns
at once, as acme's `B` does. FILE must already exist; a name that does not
resolve, or a missing `PARDES_9P`/`PARDES_PANE`, starts a separate editor
instead. Bare `pardes` in a pane refuses and names `--nested`.
`--wait` (`-w`) returns when the pane that shows FILE is deleted (exit 0) or
the session goes away (exit 1), as acme's `E` does. Use
`EDITOR='pardes --wait'` (`GIT_EDITOR` follows `EDITOR`), so fish's Ctrl-O,
`git commit` and `crontab -e` read the file after you close its pane.
`--nested` runs a separate session whose shells do not forward to it.
**Clients.**
```sh
9p -a "unix!$PARDES_9P" read index # plan9port, no mount
9ns --unix "$PARDES_9P" -- sh -c 'cat "$NINE_MOUNT/index"' # private mount
9ns --mntgen # the whole registry at /mnt/9p
```
Under `9ns --unix` the session is `$NINE_MOUNT` itself and exists only inside
that command. Under `9ns --mntgen` every posted session is
`$NINE_MOUNT/pardes/<pid or NAME>/`; take the name from `$PARDES_9P`. A dead
session's entry stays listed and answers `Input/output error`, so name the
session rather than globbing. `Tty9p` gives one pane's shell a kernel mount
at `$PARDES_MOUNT` ([v9fs.md](v9fs.md)).
plan9port's `9p write` always opens with OTRUNC, so `echo x | 9p write
pane/3/body` replaces the whole body where acme would append. Append with
`>>` through a mount.
For [Linux v9fs](https://www.kernel.org/doc/html/latest/filesystems/9p.html)
use `version=9p2000,cache=none,access=any`, `trans=unix` (or `trans=tcp`
with `port=`), `uname`, `dfltuid` and `dfltgid` for the local user, and an
empty `aname`.
**Listeners.** `--9p-tcp='tcp!127.0.0.1!5640'` adds TCP;
`--9p-quic='quic!127.0.0.1!5641'` adds QUIC (build with `-Dquic=true`,
OpenSSL 3.6+; ALPN `pardes-9p`, an ephemeral TLS identity, no peer
verification). Addresses are numeric IPv4/IPv6; port 0 picks one; `/listeners`
reads them back. Every connection has full session access, `/os` included,
and TCP is unencrypted: use loopback. Unix and TCP share 16 connection slots;
a 17th client's Tversion gets `too many connections` (and the log
`err - 9p: too many connections (N turned away)`). QUIC has 16 of its own.
Plan9port and v9fs need a userspace bridge for QUIC.
**Look paths and mounts.** Look resolves the OS filesystem first, then the
editor's own tree. Explicit paths skip that search:
| Look path | Meaning | Served path |
|---|---|---|
| `/n/os/proc/self` | the host filesystem | `/os/proc/self` |
| `/n/self/pane/2/body`, `/virtual/pane/2/body` | this session's tree | `/pane/2/body` |
| `/virtual/src/pardes.zig` | sources embedded with `-Dembed-sources=true` | `/src/pardes.zig` |
| `/n/peer/pane/2/body` | a mounted session | the peer's `/pane/2/body` |
`--mount=peer=work` or `Mount peer <dial>` mounts a session name, an absolute
socket path, `unix!/path`, `tcp!IP!port` or `quic!IP!port`; `Unmount peer`
removes it. Mount dials at once and fails if nothing answers (`dial failed:
no answer`, `timed out`, `hung up`). There are eight named mounts; `os` and
`self` are reserved. Unmount refuses a mount a pane, a working directory or
a pending Save still uses. Mounts are dumped.
A session may open its own tree through a mount (a Look at
`$m/pane/2/body` from the editor serving `$m`): requests are answered on the
connection's task while the editor waits in its syscall. Through QUIC that
still hangs.
## The tree
```
/README the one-screen guide (src/fs-help.txt)
/index a line per pane: serial kind dirty name column
/status pid, version, panes
/look /exec write a line: a right / middle click at the active pane; read: the serials touched
/log the event log; write `follow` to wait for more
/screen the rendered screen as JSON
/listeners dial addresses
/focus the serial of the pane with the keyboard; write one to move it
/ctl settings and session builtins
/commands every builtin, one a line
/recent files opened lately: open|closed <path>
/layout a line per column, then `active <serial>`
/tag /tagexec the workspace tag, and a word clicked in it
/col/<n>/ tag ctl exec of column <n>; rmdir closes an empty one
/pane/new open it to make a pane; read answers the serial
/pane/<n>/ name body tag ctl addr dot limit data xdata sel dirty mark scroll
errors event look exec tagexec, and pty/{ctl,status,data,run} on terminals;
rmdir closes the pane
/os/ the host filesystem
/src/ the editor's sources (only with -Dembed-sources=true)
```
Panes and columns are named by serials the editor gives: stable while they
live, never reused. Nothing is created by a listing, stat, walk or read:
only an open of `/pane/new` makes a pane (so `ls -l` and `find` are safe),
only `rmdir` of `/pane/<n>` or an empty `/col/<n>` removes. Tcreate is
refused everywhere.
`/index` rows are `serial kind dirty name column`, kind `text`, `term`,
`pdf` or `image`, the name `<dir>/+New` for an unnamed scratch. Names may
hold blanks, so split `head, col = row.rsplit(maxsplit=1)`, then
`serial, kind, dirty, name = head.split(maxsplit=3)`. Names in `/index`,
the log and a terminal's tag are escaped: a newline `\n`, a backslash `\\`, a byte that
is not UTF-8 `\xNN`.
## Rules for every file
**Failure.** A write fails whenever what it asked for fails, and logs one
`err <serial|-> <file>: <why>` record, with no `msg`. Only writes log: a
refused open or truncation (an OTRUNC open such as `> data` after a failed
`addr`), create or remove answers its error alone, as do a write to
`pane/new` (`permission denied`) and a write on a read-only fid (`bad use
of fid`). Errors are words (Plan 9's where pardes has none of its own),
never a C library string. Through 9ns the kernel sees an errno 9ns reads
from those words (cloud9 `9ns/src/nine.zig`, `enameToErrno`): `control
message`, `invalid` or `bad ` is EINVAL (malformed input); `no such`, `not
found` ENOENT; `in use` EBUSY; `no space` ENOSPC; `denied` EACCES; anything
else, such as `no match for regexp`, `address out of range` or `Modified`,
EIO. The `err` record has the words; a shell sees
only the errno, most often `Invalid argument` or `Input/output error`.
Not failures: a look that finds nothing (it answers nothing and logs one
`err`), an Edit `x` that matches nothing, `Undo` with nothing to undo (a
`msg`), and a command pane's command, which ends in its own time with an
`exit` record.
**Command lines.** `look`, `exec`, `tagexec`, the three kinds of `ctl` and a
column's `exec` take one command a line. The whole write is checked first
(a control character other than a tab fails it all, EINVAL); then lines run
in order, and a failing line fails the write after the lines before it took
effect, as acme's ctl does. Blank lines are skipped. A mount cuts a big
write into pieces of at most one message (msize 8192, less the header), and
each line runs once it is whole. A write that does not fill its message is
whole, so its last line runs even without a newline (`printf Save > exec`). An `Edit` whose `{` or
`a`/`c`/`i` text is still open waits for the next write on that open, and
fails at the close if it never ends (``unmatched `{'``). A line held past
1 MiB is refused.
**Answers.** Reading `look`, `exec` or `tagexec` answers the serials the
last command made, or else the pane it acted on or focused, one a line. An
open that wrote reads its own last answer; an open that never wrote reads
the session's last. A read is a stream: once read, the next read on that
fid is EOF until the next write (or seek to 0). With other clients about,
write and read on one open:
`exec 3<>$m/look; echo x >&3; cat <&3; exec 3<&-`.
**Snapshots.** `/index`, `/layout`, `/recent`, `/commands`, `/status`,
`/listeners`, a read-only `ctl`, `/log`, `/screen` and a terminal's `body`
freeze at the open, so one read in several chunks never splices two moments;
open again for now. Such an open, or a `run`, `event` or `pty/data` open,
takes one of 64 open records; past that the open fails `too many open
files`.
**Held reads.** A read with nothing to give yet (a followed `log`, `event`,
`pty/data`, `pty/run` before its answer) waits in the editor and is answered
when news comes. A second read on that open meanwhile fails `file in use`.
A read the client flushed is dropped. Through a FUSE mount bash's `read -t`
cannot time out: wrap the loop in `timeout N`.
**Stats.** Lengths are real (for `event` and `pty/data` the next record's,
zero when none waits; for `log` what an open would freeze). The qid
version of `body`, `data` and `xdata` is the pane's revision, so a stat sees
an edit land. Modes are 0644/0666, 0444 read-only, 0222 write-only.
## look and exec
A line written to `look` is a right click on it:
- a path opens the file (the pane already showing it, if any); `file:12`
selects line 12, newline included; `file:12:5` puts the caret at line 12,
byte column 5; `file:<addr>` takes any address (below), **evaluated from
the file's dot**: `file:/re/` finds the next match after the selection,
`file:0/re/` the first. `:addr` addresses the pane itself.
- `@p<serial>:<addr>` addresses a pane by serial, a terminal's logical lines
too.
- a directory types `ls` into a terminal idle there, else opens one there.
- a URL opens in the browser.
- a plain word selects its next place after dot, wrapping (`LookWord list`
on the root ctl lists every place in a `+Search` pane instead). In a
terminal a word is always listed, rows spelled `@p3:12:5-9`.
A miss logs `err <serial> look: ...` (`no match for "zzq:#3"` quoting what
was written when nothing by that name exists; `<path>: no match for regexp`
or `address out of range` when the address fails), opens nothing, and leaves
`look` reading empty; the write succeeds.
A line written to `exec` is a middle click:
- a builtin word runs (`/commands` lists them): `Save`, `Del`, `New`, `Tty
[shell]`, `Msg text`, `Find`, `Grep`, `Edit ...`, `Mount`, ...
A builtin that needs its argument (`Msg`, `Mount`, `Find`) written bare is
`wrong #args in control message "Msg"`.
- acme's words run as pardes's where it has one (`Put` is `Save`, `Delete`
a `Del` that does not ask); the rest (`Get`, `Putall`, `Snarf`, `Cut`,
`Paste`, `Zerox`, `Sort`, `Load`, `ID`, `Send`) are refused, `invalid:
acme's Get is not a pardes builtin: ...`, never run as commands. So is a
GUI-only builtin (`Fonts`) on another frontend.
- anything else is a command line, at most 1024 bytes. At a terminal idle
at an empty prompt it is typed into that shell. Anywhere else it runs as
a **command pane**: a terminal running the root ctl's `Shell` (`$SHELL`,
else `/bin/sh`) with `-c` and the line, in the pane's directory, with job
control on. Its tag reads `<dir> (<line>) running`, then `exit N`; the log
says `run <serial> <line>` and `exit <serial> <N|?>`; `exec` reads back its
serial. A typo ends `exit 127`. The directory's next command reuses a
finished command pane, below what it showed; one still running gets a
second pane. From a pane whose directory is gone nothing runs: `exec:
<dir>: no such directory` (ENOENT).
The root's `look` and `exec` act at the active pane and log as that pane's;
`/pane/<n>/look` and `exec` at pane n; `/tagexec` and `/col/<n>/exec`
click in the workspace's or that column's tag, run commands in the session's
directory, and log as `-`. A pane's word (`Undo`, `Msg`, `Save`) is refused
at `/tagexec` and a column's exec: `not a session control message "Undo":
write it to pane/<n>/ctl`.
A background job (`&`) outlives a command that exits on its own; its output
goes on below `exit N` until it lets go of the pty. `Kill` (root ctl)
stops the commands pardes started: bare, all; `Kill make ls`, those whose
line starts with one of the words. For a command pane it signals the whole
line, `&` jobs included; for a line typed into a shell only the foreground
job (SIGTERM), and the shell decides the rest. With nothing running it says
`Kill: nothing running`. Kill does not reach a REPL's code: use `sig INT` on
its `pty/ctl`.
## The root ctl
Reading `/ctl` gives every setting, one a line, in the words a write takes
(`Theme orchard`, `Verbose on`, `Placement acme`, `DumpDir <dir>`,
`Shell /bin/bash`, ...), so writing back what it reads changes nothing.
Writes take settings and session builtins (`scope = .session` in
`src/builtins.zig`), acting at the pane with the keyboard:
- A setting written bare steps to its next value (a switch flips; so do
`Placement`, `BootShell`, `Crt`). A value it does not take is `bad value in
control message; ...` naming what it takes; `/commands` lists them. A
setting the frontend cannot show is refused (`Lift is GUI-only, invalid
here`).
- `Newcol` makes an empty column right of the keyboard's, halving the active
column. `Joincol` folds the keyboard's column into the one on its right
(its panes go below that column's), `Joincol: no column to the right`.
- `Exit` quits. It refuses once while panes hold unsaved text: one `unsaved
<serial> <name>` record per pane, then the write fails `<name>: Modified
(Exit again to discard)` or `4 unsaved panes: Modified (Exit again to
discard)` (EIO), and the list stays in a `+Unsaved` pane. The same word
again with nothing edited since discards; after more editing it refuses
again, naming only the panes edited since. `Restore`, `Del`, `Delcol` and
a pane's `get` refuse the same way with their own word. A `+New` scratch
under 100 bytes, or a command's output, is never asked about.
- `Dump` writes `pardes-<date>-<time>.zon` (UTC) in `DumpDir`
([config.md](config.md#dumps)) and logs `dump <path>`. `Restore [path]`
replaces every pane (bare: the last dump this session wrote). The Restore
write is answered, then **every connection is hung up**: dial again, and
restart a 9ns mount. The new log has a `new` per pane, `restore <path>`,
then `restored <old> <new>` per pane and `restoredcol <old> <new>` per
column. Undo history and REPL bindings are not restored; a command pane
comes back showing how it ended (`exit ?` if it was running) and does not
run again.
- `Kill [word...]` (above), `Mount name dial`, `Unmount name`, `Theme x`,
`size <cols> <rows>`.
- `size C R` sizes a `--detach` session no frontend is attached to (160x50
until then): from 20x6 to 4096x4096, else `invalid size`; refused while a
frontend owns the size, and when a column would lose its panes' minimum
rows (`size: too small for the panes, each its tag and 2 rows`).
A write is refused whole, before anything runs, in Plan 9's words:
`unknown control message "X"`, `wrong #args in control message "X"`,
`bad value in control message ...`, or a word of the other ctl: `not a
session control message "Undo": write it to pane/<n>/ctl`, `... "Delcol":
write it to col/<serial>/ctl`, `not a window control message "X": write it
to /ctl`. A builtin that would open a prompt (`Save` on a scratch) fails
`control message needs its argument "Save"`. A line that fails as it runs
fails with the editor's words (`Mount: already mounted`, `Save /root/x:
access denied`), changing nothing.
## Columns and tags
`/layout` has a line per column, left to right: `serial index x width
current|notcurrent empty|full pane-serials...`, then `active <serial>`
(acme's activecol: where `pane/new` and a look put the next pane; `-` when
none). `current` is the column with the keyboard now, which may differ
from the active one. `/index`'s last field is each pane's column serial.
A session holds 16 columns (`no space for a column: 16 max`, ENOSPC), each at
least 10 cells wide (`Newcol` from a column under 20 fails `this one is too
narrow to split`). Since root `Newcol` halves the active column, reach 16 by
writing `Newcol` to the widest column's `exec`.
`/col/<n>/ctl` takes `Delcol`, `Joincol`, `New` and `Tty` for that column;
`/col/<n>/exec` is a click in its tag; `rmdir /col/<n>` closes an empty column
(one with panes: ENOTEMPTY). A column may be empty, as in acme: `Newcol`
makes one, and closing its last pane leaves it (the keyboard goes to its
tag, `focus` reads empty, the log says only `del`). Closing the session's
last pane quits pardes. The log says `newcol <serial>` and `delcol <serial>`.
`tag` files (`/tag`, `/col/<n>/tag`, `/pane/<n>/tag`) read the whole tag.
A pane's starts with its computed path (an image's with its mode words, a
PDF's with its page), then the editable text. `>` replaces the editable text
(the default words too: `echo Make > tag` leaves only `Make`) and drops the
one newline that ends it; `>>` appends, so `printf ' Make' >> tag` (the
blank matters; `echo` would start a second line). A pane tag may hold
several lines; a column or workspace tag is one, a newline written into it
becoming a space. Control characters other than tab, DEL, C1 controls and
non-UTF-8 bytes are refused (`invalid tag text`). The editable text is at
most 4096 bytes (`tag: no space: over 4096 bytes`, ENOSPC). A clear is an ordinary edit
and `u` in the tag undoes it. [tags.md](tags.md) covers tags on screen.
## Panes
**Making and closing.** Opening `/pane/new` makes a scratch `<dir>/+New`
(the session's directory), and reading that open answers its serial:
`n=$(cat $m/pane/new)`. Each open makes another pane; two reads of one open
answer the same serial. It goes where acme's makenewwindow puts a window
([tags.md](tags.md#where-new-panes-go)): in the active column, filling it
if empty, else the bottom half of its last pane. A session holds 64 panes
(`no space for a pane: 64 max`), and every pane keeps its tag and 2 rows
(`no space for a pane in that column: each keeps its tag and 2 rows`); both
are ENOSPC, for this open and for a look, exec, `New` or `Tty` alike.
`rmdir /pane/<n>` closes the pane, unsaved or not.
**`name`** reads the file name (a terminal's directory); a write renames the
buffer (relative to the pane's directory) and marks nothing dirty; `Save`
then writes under the new name. A name is one line; refused (EINVAL) are a
second line, a blank at either end, control bytes and non-UTF-8 (`bad
character in file name: a blank at its start`, ...). Up to 255 bytes a
component.
**`body`** reads the text; a write appends; `>` (OTRUNC) replaces it all.
A terminal's body is its history as plain text in logical lines (wrapped
rows joined), frozen per open; writing it sends input to the child. A PDF's
body is the text layer of the page shown; images and PDFs take no write
(`this pane has no text`).
**`sel`** reads the selected text; a write replaces it. **`errors`** is
write-only: text appended to the directory's `+Errors` pane (logged as
`msg` records when no column has room).
**`focus`** (root): a serial written gives that pane the keyboard and makes
its column active (a folded pane stays folded); `no such pane`, or
`ill-formed control message` for a non-number. It reads empty while a
column or workspace tag has the keyboard.
**Pane ctl.** Reading it gives acme's status line: serial, tag length, body
length, isdir (0), dirty, width in cells, font, tab width, undo available,
redo available, then `current`/`notcurrent` and a REPL's id if bound. It
takes:
- the pane builtins: `Del` (`Del k`/`Del j`, or `DelAbove`/`DelBelow`, give
its rows to the pane above or below), `Save [path]`, `Collapse` (fold),
`Undo`/`Redo` (256 steps each), `Find pat`, `Edit ...`, `Tty [shell]`
(a new terminal pane in its directory), and the column words acting on
its column: `Delcol`, `Left`/`Right`/`Up`/`Down`.
- `get`: reload from the file (refused once over unsaved edits, `<name>:
Modified (get again to discard)`).
- `lock`/`unlock` (acme's). The lock binds only clients that take it and
belongs to the open that wrote it: `exec 3>$pane/ctl; echo lock >&3; ...;
exec 3>&-`. A `lock` another open holds fails at once, `file in use`
(EBUSY): retry.
- `answer <choice>` to the question the pane asks on its notice band, logged
`ask <serial> <what> <choices>` (`ask 4 del k j`, `ask 4 repl a b`, `ask 4
save path`); `answer -` takes it back.
- acme's lowercase ctl words, done by what replaces each: `name x`, `put`
(Save), `clean`/`dirty`, `del`, `delete` (no asking), `dot=addr`,
`addr=dot`, `limit=addr`, `mark`/`nomark`, `show`, `cleartag`. `dump`,
`dumpdir`, `font`, `menu`, `nomenu` are refused, EINVAL. These lowercase
words are ctl-only: written to an `exec`, `del` is a command line.
A file changed on disk reloads by itself only when the buffer has no unsaved
edits; otherwise it stays dirty, says `<name> changed on disk (get reloads
it, Save overwrites it)`, logs `changed <serial>`, and its next `Save`
warns once.
### Addresses and data
`addr`, `dot` and `limit` read the pair of byte offsets they take, so
copying one onto another (`cp $p/addr $p/dot`) is acme's `dot=addr`. A
write is a pair or an address expression. `addr` names where `data` reads
(to the end of text) and the range `xdata` reads; `dot` is the selection
(moving it scrolls the pane there); `limit` bounds the end of a forward
search and reads empty until set. Truncating `dot` empties it, truncating
`limit` lifts it.
- A write to `data` or `xdata` **replaces** the `addr` range (`>` and `>>`
alike); `: > data` deletes it. Truncating `data` is pardes's own (acme
ignores OTRUNC there); only truncating `body` empties the buffer.
- A write leaves `addr` just past what it wrote, so a second `echo x > data`
inserts after the first: write `addr` before each replacement. A read of
`data`/`xdata` moves `addr` past what it read.
- `addr` belongs to the pane, not the client, and neither an open nor a
truncation resets it (acme resets on first open): each `echo /re/ > addr`
searches on from the last, so a find loop advances. Write `0` to start
at the top; searches wrap, so stop a loop when the address comes back or
set `limit`.
- A failed address leaves **no address**: `addr` reads empty and `data`/
`xdata` refuse (`no address: the last one written to addr failed`) until
a standalone address (`2`, `#0`, `/re/`) is written, so a missed target is
never written at the old one.
Addresses are sam's: `#n`, a line number, `/re/`, `?re?`, `-/re/`, `$`,
`.`, `0`, ranges `a,b` and `a;b`, `+`/`-`. They are evaluated from the
current address (the last written to `addr`, or just past the last `data`
write), not from the selection. pardes adds `12:5`: line 12, **byte**
column 5 from 1, clamped to the line's end (`12:0` is `address out of range:
a column counts from 1`); it composes (`12:5,14:1`). A tool's character
column matches only on an ASCII line; elsewhere use `12/name/` or `#n`. A
row of Recent, `+Search` or the Jumplist spells a range `12:5-14:2` (through
14:2 inclusive) and `addr` takes it as such.
Offsets and counts are bytes everywhere (acme counts runes), but every
address lands on a rune boundary: `#n` or `L:C` inside a rune snaps to its
start, a match covers the runes it touches, and a combining mark or a CRLF's
`\r` is addressable alone.
Refusals: `bad address syntax`, `no match for regexp`, `address out of
range`, `addresses out of order` (`#100,#50`), `bad regular expression`.
sam details kept: `$-1` is the last line when the text ends in a newline,
else the one before; with a final newline the empty place after it is a line
(`1` of an empty text is `#0,#0`, so `Edit 1i/x/` works on an empty file);
`2,1` is an empty range at line 2's start; `/^/` finds the empty place after
a final newline; a pattern that can match empty (`^`) passes over the match
where the search starts.
### Regular expressions
Patterns are [mvzr](https://github.com/mnemnion/mvzr)'s (classes, `\d\w\s`,
`{m,n}`, lazy `*?`), searched as sam searches, line by line: `^` and `$`
match at any line's start and end, `.` and `[^...]` never match a newline.
The same code (`src/regexp.zig`) serves addresses, Edit, and normal mode's
`s` and `S`. The ceiling:
- The leftmost match wins, but among alternatives the first that matches,
not sam's longest (`/gam|gamma/` finds `gam`). mvzr keeps no
submatches, so Edit's `s` has no `\1`-`\9`.
- A pattern holding `\n` runs over the whole text: there `^` may only come
first and `$` only just before a `\n`, else it is refused.
- An alternation must anchor every branch or none (`^def|^ ` works,
`^def|x` is refused).
- A class may hold non-ASCII runes (`[éa-z]`, a range up to 256 runes); a
wider range or a negated class with one (`[^é]`) is refused.
- At most 512 operations (about 512 characters, counted after that
rewriting): `bad regular expression: longer than mvzr's 512 operations
(about 512 pattern characters)`.
- Each search has a step budget (about 300 ms; each search of an Edit `x`
its own): `regular expression search took too much time, gave up`. What
runs out is exponential backtracking (`a?` twenty times then twenty `a`s)
or a quadratic pattern over a very long line.
### Edit
`Edit <sam commands>` on a pane's `ctl` or `exec` (or the root's, at the
active pane) runs acme's Edit on the body: addresses as above, commands `x y
g v c a i d s p = m t u` and `{ }`. All changes are one undo step, applied
only if every command succeeds; a failure changes nothing and fails the write
with acme's words (`Edit: no substitution`). An `x` that finds nothing
succeeds silently. `p` and `=` print to the directory's `+Errors`. Not
there: `b B D e r w f X Y`, `< | >`, `\1`-`\9`. In `s`, `&` is the match
(`\&` a literal); in `c`, `a`, `i` it is a literal. `y` yields the stretch
before the first match too.
Braces take a command a line, so a block goes on one open, in one write or
several:
```sh
printf 'Edit ,x/foo/{\ni/</\na/>/\n}\n' > $p/ctl
```
### Flags and undo
`dirty`, `mark` and `scroll` read and take `0` or `1`: the buffer differs
from its file (a file deleted on disk counts); a write pushes an undo point
(writing `1` pushes one now); a write scrolls the pane. `/index`'s dirty
flag is `dirty`; a `+New` scratch reads 1 but holds up nothing under 100
bytes.
The writes of one open of `data`, `xdata` or `body` are one undo step (so a
multi-line `printf` is one). To make a loop of opens one step: `echo 1 >
mark` (a point now), `echo 0 > mark`, the writes, `echo 1 > mark`.
### event
Holding `event` open takes the pane's Look and Exec clicks: they come to the
reader as records instead of acting, as do lines written to the pane's own
`look`/`exec` (or the root's while it has the keyboard). A record is acme's
`<origin><action><q0> <q1> <flag> <n> <text>\n`; read `n` **bytes** of
text, which may hold newlines.
- origin: `E` a 9P write to body or tag, `F` other files and the editor's
own lines, `K` keyboard, `M` mouse.
- action: `X`/`L` executed/looked in the body, `x`/`l` in the tag (offsets
into the whole tag, path included), `I`/`D` body text inserted/deleted,
`i`/`d` the tag's.
- flag: 1 the text's first word is a builtin, 4 (look) a file name or
address, 8 (exec) chorded: two records follow, the argument and where it
came from. pardes never sends flag 2.
- A written line, or a click in a terminal's body, has no place: `0 0` with
its text (`FX0 0 1 6 Msg hi`).
Write a record back to have it done as the click would: `<o><a><q0> <q1>\n`
acts on that range; the whole record as read acts on its text (the only way
for `0 0`). A chorded record with its two follow-ups, in one write or
three, runs once with its argument. `I`, `D`, `i`, `d` are refused. A helper
holding `event` that writes its own pane's `exec` gets its command back as a
record: run it through `ctl` instead.
### REPLs
`Repl python` on a terminal's `ctl` (or in its tag) binds it as that
language's REPL (names as a code fence spells them: `py`, `sh`, ...); its
tag and `ctl` line show its id, `python-a`. `Repl -` unbinds, bare `Repl`
says the binding. A middle click or the execute key on a `.py` body then
types the text into the REPL instead of running it; builtin words, tag
words, `Exec <text>` and @`cmd` words still run. With several bound, the
pane asks (`ask <serial> repl a b`). Bindings are not dumped.
A 9P `exec` is never sent to a REPL. A script either writes the event record
`MX<q0> <q1>` to the `.py` pane's `event` (sent as the click would be), or
writes the REPL's `pty/data` itself: multi-line code as a bracketed paste,
`\e[200~<code>\e[201~`, then `\r` in a separate write once the REPL has
echoed the paste; a paste of more than one line not ending in a newline needs
a second `\r`. Line by line, a blank line ends a Python block and Python
3.14 auto-indents each line.
## Terminals
Terminal panes also have `pty/`:
- `pty/data`: write bytes as typed (`printf 'ls\r'`, `\x03` is Ctrl-C);
read the live output stream (a consuming queue shared by readers, not a
replay).
- `pty/status`: one line, `cols rows busy`; busy is 1 while a command runs or
text is typed at the prompt.
- `pty/ctl`: `winsize C R` (at least 2 rows), `sig INT|TERM|HUP|QUIT|KILL`,
`exec` (restart the shell in its directory: refused on a command pane, `a
command pane does not restart`; `exec: <dir>: no such directory` if it is
gone; a shell that cannot start fails and leaves the old one running).
- `pty/run`: one line at the shell's prompt, answered on the same open:
```sh
exec 3<>$m/pane/$n/pty/run; echo make >&3; cat <&3; exec 3<&-
```
The answer's first line is the header: `exit N` then the command's output
(as the screen showed it: no colour, `\r` progress collapsed, trailing
blanks dropped; the last 64 KiB, `exit N cut M` when M bytes were left
out, bare `cut` when the start scrolled away or was cleared). Or: `busy:
<program> is running` (bare `busy` when text is typed at the prompt),
`exit ?` (no status reported, not a success), `error not run` (the shell
refused the line, e.g. a fish syntax error), `error shell gone`, `error no
prompt marks`, and on a command pane `error a command runs here, not a
shell` (`error command done; not a shell` once it ended). A line written
before a fresh terminal's first prompt waits for it. One line per run;
a second line before the answer is refused.
`pty/run` relies on the OSC 133 marks pardes injects into bash and fish;
`exec zsh` or a continuation prompt never reports an end, so cancel the
read. A program on the alternate screen (vim, less) leaves no output. A
program holding the terminal (a REPL, `less`) takes no run: write to
`pty/data`.
## /log
One 64 KiB ring, one record a line, kept whether or not anyone reads it. An
open freezes it and reads to EOF. Write `follow` to that open to then wait
for each new record (`follow new` skips the history, as `tail -n0 -f`);
`tail -f` never writes `follow`, so it sees nothing new.
```sh
exec 3<>$m/log; echo follow >&3; while read -r line <&3; do ...; done
```
A follower the ring outran reads `lost N` first. A Restore hangs the
follower up: dial again and read from `restore <path>`.
| record | when |
|---|---|
| `new <serial> <name>`, `del`, `rename`, `save` | a pane made, closed, renamed (a terminal's too, as its shell changes directory), saved |
| `newcol <serial>`, `delcol <serial>` | a column made or closed |
| `msg <serial\|-> <text>` | the editor said something (not a builtin's own name under `Verbose`) |
| `err <serial\|-> <file>: <why>` | a write was refused or failed |
| `run <serial> <line>`, `exit <serial> <N\|?>` | a command pane's command started and ended; also a terminal whose shell exited, before its `del` |
| `send <from> <to> <repl-id>` | text went to a REPL |
| `ask <serial> <what> <choices>`, `answer <serial> <choice\|->` | a pane asked, and was answered (`-`: taken back, or the pane closed) |
| `changed <serial> [reloaded\|deleted]` | its file changed on disk (bare: under unsaved edits) |
| `unsaved <serial> <name>` | a pane an Exit, Restore, Del or Delcol refused over, before the `err` |
| `dump <path>`, `restore <path>` | a Dump written; a Restore, after its panes' `new`s |
| `restored <old> <new>`, `restoredcol <old> <new>` | serial maps after a Restore |
The serial is the pane the line ran at (the active pane for the root's
`look`/`exec`), `-` for the root `ctl`, `/tagexec` and column files. A
record said again straight after itself is counted, `err 3 addr: no match
for regexp (x4)`; a follower sees each count as a new line. A `msg` is cut
at 256 bytes and an `err` reason at 200, ending in `…`. Control characters
become spaces.
## Other files
- `/screen`: JSON `cols`, `rows`, `cursor`, `styles`, and row-major `cells`
of `[grapheme, style_index]`; one frame per open. Compare a cell's style
through `styles`, not the index.
- `/commands`: `Word [arg] root|pane|both [values] -- sentence`, one a line,
generated from the builtin registry (`both`: Edit, at the active pane from
the root).
- `/recent`: up to 200 files, most recent first, `open <path>` or `closed
<path>`; kept in `$XDG_STATE_HOME/pardes/recent`. `Recent` shows them in a
pane; a look at a row reopens the file at its last dot.
- `/status`: `pid`, `version`, `panes`.
- `/os/`: existing regular files take read, write and truncation to zero;
create, remove, rename and metadata changes are refused; ownership is
synthetic. Linux v9fs's truncation `mtime` hint is accepted and dropped.
- `/src/` (and `/shaders` on GUI builds) with `-Dembed-sources=true`;
`EffectCode <effect>` lists an effect's files under `/virtual`.
## Limits
| | |
|---|---|
| panes | 64 (16 on the board) |
| columns | 16 (6 on the board), each at least 10 cells wide |
| rows a pane keeps | its tag and 2 |
| msize | 8192 offered |
| connections | 16 Unix+TCP, 16 QUIC |
| opens holding state | 64 |
| named mounts | 8 |
| command line | 1024 bytes; a held line or Edit block 1 MiB |
| tag text | 4096 bytes |
| regular expression | 512 operations; a step budget per search (~300 ms) |
| undo | 256 steps |
| log | 64 KiB ring; `msg` 256 bytes, `err` reason 200 |
| `pty/run` output | 64 KiB |
| file name component | 255 bytes |
## Source and tests
`src/ninep/`: `tree.zig` (nodes, dispatch), `pane.zig`, `ctl.zig`,
`cols.zig`, `addr.zig`, `pty.zig`, `events.zig` (event and log), `screen.zig`,
`sources.zig`. The engine is cloud9's `fs.Server` (`src/9p.zig`); transports
are in `src/9p_io.zig`; `src/fs.zig` holds host access, mounts and
resolution. `zig build fs-test` drives real sessions with `test/ninep.py`;
`zig build 9p-test` checks the engine budgets.
|