summaryrefslogtreecommitdiff
path: root/docs/fs.md
blob: 14b82d48a4ff30c5ba8acfbf9f13c85ba03a9598 (plain) (blame)
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
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
# 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 sixteen slots served by cloud9's `std.Io`
runner; QUIC has sixteen of its own on the editor's poll loop. A client
that finds every Unix/TCP slot taken gets an Rerror `too many connections`
to its Tversion, and the log an `err - 9p: too many connections` record. 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"'
```

(Under `9ns --unix`, `$NINE_MOUNT` is that session's root itself.)

A `9ns --unix` mount lives in the private namespace of the command it runs,
and nothing outside that command sees it. The mount everyone on the machine
shares is the registry one, `9ns --mntgen` (default `/mnt/9p`): every running
editor posts itself there, so `$NINE_MOUNT/pardes/<pid>/` is that editor's tree
for any process, and `$NINE_MOUNT/pardes/NAME/` a `--detach=NAME` session's.
9ns exports `$NINE_MOUNT` to everything it starts, so a script checks that
variable to know the mount is there, and takes the name from `$PARDES_9P`
(`pardes-9p-<pid or NAME>.sock`). A new pane made through `pane/new` is a scratch named
`<dir>/+New` until it is given a name. A column may hold no pane, as in acme:
`Newcol` makes one empty, and closing a column's last pane leaves it empty
with its tag holding the keyboard (`focus` reads empty) and logs only the
`del`. `pane/new` places its pane as acme's makenewwindow(nil) does: in the
active column, filling it when it is empty, else taking the bottom half of its
last pane ([where new panes go](tags.md#where-new-panes-go)). `Delcol` closes the column. Closing the
session's last pane quits pardes; see [tags](tags.md#empty-columns).

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, column serial
/status      pid, version and pane count
/look        write a line: a right click on it at the active pane; read: the serials the last
             look, exec or ctl write touched (made, else acted at)
/exec        write a line: a middle click; read the same serials
/log         recent events, one a line: new|del|rename|save <serial> <name>, msg <serial|-> <text>,
             dump|restore <path>, err <serial|-> <file>: <why>;
             write follow to that open to wait for more
/screen      rendered screen JSON; frozen per open handle
/listeners   the session's dial addresses
/focus       the serial of the pane with the keyboard; write a serial to give it the keyboard,
             which makes its column the active one as a click there would
/ctl         the settings, one a line as a write takes them; write a setting or a session builtin;
             `size <cols> <rows>` sets the screen of a session no frontend is attached to
             (`--detach`, 80x24 until then; refused while a frontend owns the size),
             from 20x6 to 4096x4096, and refused when the panes it has would not each
             keep their tag and 2 rows; a terminal's pty follows its pane on every resize
/commands    every builtin: word, `arg` if it takes one, `root`, `pane` or `both` (the ctl that takes it:
             Edit is both, at the active pane from the root; a pane's word such as Undo or Msg
             is refused at the root), a setting's values, then ` -- ` and what it does
/layout      one line per column (16 at most; the board 6; Newcol past that fails,
             `no space for a column: 16 max`, ENOSPC), left to right: serial index x width current|notcurrent
             (the column with the keyboard now) empty|full pane-serials...; then active
             <serial>: acme's activecol, which the keyboard leaving for another column's
             tag does not move, so the two can differ -- the active column, where
             pane/new and a look place a pane next (- when there is none)
/tag         the workspace tag; > replaces it, >> appends, one line
/tagexec     write a word: a middle click on it in the workspace tag; read as /exec
/col/<n>/tag the tag of the column with serial n, the same way
/col/<n>/ctl write Delcol, Joincol, New or Tty: each acts on that column, as from its tag
/col/<n>/exec write a word: a middle click on it in that column's tag; read as /exec; rmdir col/<n> closes
             an empty column (a column with panes is refused, ENOTEMPTY)
/pane/new    open it to make a pane; the read answers that pane's serial. A session holds
             64 panes (16 on the board); at that, every route that would open one -- this
             open, look, exec, New, Tty -- fails with `no space for a pane: 64 max` (ENOSPC through 9ns, which has
             no word for ENFILE) and an err
             record, and look reads back empty; a column with no room for one
             (each pane keeps its tag and 2 rows) refuses it the same way,
             `no space for a pane in that column` (docs/tags.md)
/pane/<n>/   name body tag ctl addr dot limit data xdata sel dirty mark scroll
             errors event look exec tagexec (a word as a click in its tag), plus
             pty/{ctl,status,data} on terminals
/os/         the host filesystem
/src/        the editor's embedded sources, only when built with -Dembed-sources=true
```

`/layout`, `/tag` and `/col` go past acme, which serves no column files --
its columns are only where a window sits. They are here so a script can see
where the panes are (`/index`'s last word is each one's column serial) and
edit the tags a person clicks in: a column tag is one line, a newline
written into it a space, and a truncating write (`echo Make > col/3/tag`)
clears it and drops the newline that ends it, as a pane tag's does. A
column is named by its serial, as a pane is: it stays while the column
lives, whatever opens or closes beside it, and is never reused; /layout
gives each column's serial and its index left to right, and the log says
`newcol <serial>` and `delcol <serial>` as columns come and go, and after a
Restore `restoredcol <old> <new>` for each column as `restored` does for
panes. Joincol folds a column into the one on its right, which keeps its own
serial and tag; the joined column's panes go below that column's own, in
their order, and its serial is gone (`delcol`). The root ctl
takes no column word: its `Delcol` is refused, pointing at
`col/<serial>/ctl`.

Control messages are split by what they act on, as acme keeps window verbs
on a window's ctl and webfs and upas/fs keep session settings on a root ctl.
Each builtin declares its scope in src/builtins.zig (`scope = .session`;
every setting is one, the rest act on a pane). `/ctl` takes the session's
builtins, one a line, at whichever pane has the keyboard as each runs --
`Newcol`, `Dump`, `Mount name dial`, `Theme ink`, `Verbose off`; `Exit`,
which quits the editor as acme's does (it refuses once, naming each pane with
unsaved text, a `+New` scratch of 100 bytes or more too, in one line,
`<name>, <name>: Modified (Exit again to discard)`, and a second `Exit`
with nothing edited since quits, throwing that text away; a scratch or a
command's output under 100 bytes is not asked about, as acme's winclean
asks about no small unnamed window (so a scratch's `dirty` of 1 in `/index`
blocks nothing until it holds 100 bytes: it has no file to be out of step
with, and a few lines typed to try something are not work to lose); `Restore`, which replaces every pane,
asks the same first -- `Dump` writes `pardes-<date>-<time>.zon` (UTC) in
`DumpDir` (default `$XDG_DATA_HOME/pardes`, else `~/.local/share/pardes`)
and logs `dump <path>`, and `Restore` with no path takes the last one; a
Restore puts a new editor under every client, so the write of it is
answered and then every connection is hung up, their fids naming the old
editor's panes (a 9ns older than cloud9 2a7137c could fail the write with
ECONNRESET all the same, when another request's send met the hang-up
before the answer was read; the log is the authority): dial again -- a 9ns
mount is one such connection, so after a Restore stop it and start 9ns
again, or every file under it fails -- and the new
log names the restored panes and
`restore <path>`. Restored panes have new serials (`restored <old>
<new>` maps them) and so may columns (`restoredcol`; a fresh editor counts
column serials from 1 again, so they often come back the same). A command
pane comes back showing what it showed, its tag `exit ?`: its command is
not run again. REPL bindings are not dumped. The answer has 200 ms to leave before the cut, so a slow
client may see only the cut; the log's `restore <path>` is what says the
Restore happened. Keeping connections across it would mean carrying serials
and opens into the new editor, which acme, whose Load only adds windows,
never needed); and `Kill`, which
does not quit but stops commands, as acme's does: bare, every command pardes
started, and `Kill make ls`, those whose line begins with one of the words. A
command pardes started is a command pane's, until its child exits -- Kill
sends SIGTERM to its running job, to its shell and to every `&` job the
line started, so the whole command stops and leaves nothing running (of
`sleep 30; echo done` the `echo` never runs; an `&` job outlives only a
command that exits on its own) -- or a line it typed into
a terminal (a
word written to `exec`, a middle click on one, a `pty/run`), from its shell's
start mark (C) to its end mark (D), where Kill sends its foreground job SIGTERM -- acme posts the
"kill" note, which ends a process -- and never signals the shell itself;
in a shell running without job control (`set +m`) the job shares the
shell's group, so there is none to signal: Kill says `Kill: no job to
signal`, and a write of it to `ctl` fails with that; with nothing running
it says `Kill: nothing running`; and, of a typed line, only the
foreground job, after which what the rest of the line does is the shell's
affair: of `sleep 30; echo done` typed at a prompt, fish goes on and runs
the `echo`, and bash abandons the line, as each does when a job it waits
on is killed --
and reads every setting there is, one a line, in the words a write of it
takes (`Verbose on`, `WindowOpacity 70`, `PanelSlide off`, `DumpDir
<the directory in effect>`, `LocationsConfig ...`), so writing what it reads
back changes nothing; a setting the frontend cannot show (`Lift`,
`GripWidth` on a terminal) is refused as `Lift is GUI-only`; platform and startup facts are `/status`'s and the
Config window's, not settings. A pane's `ctl` takes the builtins that act on
a pane (`Del`, or `Del k`/`Del j` to give its rows to the pane above or
below, `Save f`, `Collapse`, which folds that pane, `Undo` and
`Redo`, which step its body through its last 256 edits -- with none left
they say `Undo: nothing to undo` and the write still succeeds, as acme's
Undo is silent --, `Find pat`)
beside acme's `get`, `lock` and `unlock`. The column words are pane words
too, acting on the column that pane is in: `Delcol`, `DelAbove`, `DelBelow`
and the focus moves `Left`/`Right`/`Up`/`Down` from it. `Joincol` and
`Newcol` are the root's: they act on the column of the pane with the
keyboard, and `Joincol` needs a column to its right (`Joincol: no column to
the right`). The words are case-sensitive and do not alias: acme's verbs
are lowercase and the builtins keep their tag spelling, so `Get` is no word
and `del` none either.

A write is checked whole before any line of it runs, and a line is refused
in Plan 9's words for a ctl (kernel/misc/parse.c:82-97), quoting the line:
`unknown control message "X"`; `wrong #args in control message "X"` for an
argument to a builtin that takes none, or none to one that needs it (`Msg`,
`Mount`, `Find`, a setting's value but a switch's, which flips bare);
`bad value in control message "X"` for a setting's value it does not take
(for `Theme`, naming the themes that share the name's first letter, since
all of them, `ThemeSel`'s list, are too many for an error);
and `not a session control message "X": write it to pane/<n>/ctl` or `not
a window control message "X": write it to /ctl` for a word of the other ctl. 9ns maps them all to EINVAL, and a write
refused here has done nothing. A line that then fails as it runs fails the
write with the error the editor reports for it and the line, e.g. `Mount:
AlreadyMounted "Mount peer /tmp/s"` (EIO), and `control message needs its
argument "Save"` (EINVAL), for a builtin that would have asked at a prompt
(a `Save` on a scratch) rather than open one nobody is there to answer. The
lines before a failing one have taken effect and those after it never run,
which is what acme's ctl loop does (editors/acme/xfid.c:600-790). An error
that only happens as the editor performs what a line asked for -- a `Save`
whose disk write fails -- is reported in the editor and /log, not in the
write's answer. Like any write, a ctl write answers once the editor has
performed what it asked for (a save written, a shell started). A click on
the same word, or the word written to `exec`, still opens its prompt.

`/commands` lists every builtin the registry holds, in registry order, one
a line: its word, `arg` when it takes one, `root` or `pane` for the ctl
that takes it, and for a setting that chooses among words those words,
comma-joined, e.g. `Newcol root`, `Save arg pane`, `Verbose arg root on,off`,
`Placement arg root acme,pardes`. Such a setting written bare steps to its
next value, so a two-valued one flips (`Crt`, `Placement`, `BootShell`), as
its word clicked in a tag does; a value it does not take is refused with
`bad value in control message; takes ...` naming those it does. It is
generated from the registry, so it is always this build's own list.

`/focus` reads the serial of the pane with the keyboard, and a serial written
to it gives that pane the keyboard, off any column or workspace tag that had
it -- rio's `current` written to a window's `wctl`, named once for the whole
tree since there is one keyboard. While a column's or the workspace's tag has
the keyboard no pane does: `/focus` reads empty and every pane's `ctl` says
`notcurrent`. A write gives the keyboard and nothing else: a folded pane
stays folded (unfold it with `Collapse` on its ctl), as rio keeps `current`
apart from `unhide`. A serial no pane has fails with `no such
pane`; anything but a number, with `ill-formed control message`.

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.

A session can open its own tree through a mount: a Look at
`/mnt/9p/pardes/<me>/pane/2/body` from inside that very editor opens it, and
a Save of that pane writes back through the mount into pane 2. Requests on
the Unix and TCP listeners are answered on the 9P connection's own task, not
by the editor's loop, so the realpath, the stat and the read the editor makes
out through the mount come back while it waits for them. The one rule is
whose turn it is with the core (`pardes.turn`): the editor has it, and gives
it up while it waits for input and while a step of it is out in a syscall. A
step of a connection task's own -- a Look written to `look` -- goes out the
same way, and the editor waits for it to return before it takes a step of
its own. While any step is out, a request that would change a pane (a write,
a truncation, an rmdir) parks in the engine until none is; everything else,
opening `pane/new` and `screen` included, is answered at once, which is why a
Look at any path in the tree comes back. A write into the tree from the
editor itself only ever happens between steps (a Save), so nothing it waits
on out there is a request that has to park. QUIC is still served on the
editor's loop, so through QUIC the old hang remains. `/n/self/...` names the
same tree without leaving the process.

`/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, and a plain word, as acme's look3 does, selects its next
  place in that pane after the dot, wrapping at the end, opening nothing
  (`LookWord list` on the root ctl lists every place in a `+Search` pane
  instead, as pardes did before; `LookWord search` is the default).
- 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`, ...;
  `Tty`'s argument is the shell it runs, `Tty fish`, and `Tty` on a pane's
  ctl opens a new terminal pane beside that one, not in it),
  or anything else, a command line. Written at a terminal at its prompt it
  is typed into that shell (and a terminal whose shell exits, `exit` typed
  or run, closes its pane). From anywhere else -- a file, a scratch, a tag,
  a terminal whose tty a program holds -- it runs as a command pane: a
  terminal whose child is the root ctl's `Shell` ($SHELL, else /bin/sh, unless set) run
  with `-c` and the line, in the pane's directory, with job control on
  (bash, sh, dash, zsh, ksh `-m`; fish `status job-control full`),
  which shows its output and then `exit N` (its tag reads `<dir> (<line>)
  running`, then `exit N`), and stays. The command is over when its process
  exits, as in acme, not when its terminal closes: a job it left in the
  background prints on below `exit N` until it lets go of the pty, and a
  command that lets go of its terminal early runs on to its own exit. A
  background job outlives a command that exits on its own (Kill stops it too): job control gives it a process
  group of its own, so the hangup the kernel sends the terminal's
  foreground group when the shell exits misses it. It survives the pane
  closing too, but its writes to the terminal then fail, so start one that
  must keep writing with `nohup` or its output redirected. The directory's
  next command runs in that pane once it is done and nothing holds its pty, below what it showed, after a `% <line>` line;
  one still running gets a second pane. Before the next command the pane
  leaves any alternate screen and turns off the modes a program left on
  (mouse reports, bracketed paste, a hidden cursor); a command that clears
  the screen and its scrollback (`clear`, ED3) erases the history above it. A command pane's own exec starts
  the next command there too. The log says `run <serial> <line>` and `exit
  <serial> <N|?>`; `exec` reads back the command pane's serial; Kill stops
  its whole line, `&` jobs included; a command line is at most 1024 bytes, and a
  longer one written to an exec fails the write (EINVAL, `a command line is
  at most 1024 bytes`) before anything in it runs; a builtin's line (a long
  `Msg`, an Edit block) may be longer. To run a command again, execute
  its line again from its directory: `echo 'make test' > pane/<n>/exec` on
  the command pane runs it there, below the last run. A misspelled word is a
  command that says so and ends `exit 127`. `echo Tty > pane/<n>/ctl` makes
  an interactive terminal in that pane's directory.

A terminal can be bound as a language's REPL: `Repl python` in its tag or
on its `ctl` (the language names are the syntax table's, or a code fence's
alias such as `py`, in any case; `Repl -` unbinds,
`Repl` bare says the binding, `Repl python` again changes nothing). Its tag
shows its id, `python-a`, `python-b` for the next, a freed letter reused,
and so does the end of its `ctl` line, after `current`/`notcurrent`.
A builtin's word still runs first, bound or not: `Del` clicked in the file
closes its pane. Then any other exec made by a gesture on the body of a file
in that language -- a
middle click, the execute key, on a selection or a single word, even `make`
in a comment -- or on the REPL's own body is typed into the REPL instead of
run: bracketed paste when its program asked for it (DECSET 2004), else line
by line, where a blank line inside a Python block ends the block (said
once), then Enter. The message row says `→ python-a` in the tag's name tint
and the log `send <from> <to> <id>`. With several REPLs bound for the
language the pane asks which, on its notice band, one key answering and Esc
sending nowhere; nothing is remembered. A REPL takes text only while its
program has the terminal: a command pane's until its command is done (the
binding goes with it, and a done one cannot be bound), an interactive
terminal's while a program other than its shell holds the tty -- after
Ctrl-D the shell would run the text, so nothing is sent and the pane says
so. Still commands, whatever is bound: a word in a tag (so
the tag is how to run `make` from that file), `Exec <text>` run by name
(typed, a 2-1 chord onto `Exec`, a `ctl` line) -- in a `.py` pane bound to
a REPL, `Exec print(1)` runs `print(1)` as a command, never in the REPL --
and a command word @`cmd` in
the text, looked at or clicked (`# @`pytest -x`` in a script). A 9P `exec`
is no gesture and is never sent: a script writes to the REPL pane's
`pty/data`, multi-line code as a bracketed paste (`\e[200~<code>\e[201~`,
then `\r` in a write of its own once the REPL has echoed the paste --
Python 3.13's REPL takes an Enter read with the paste as part of it, even a
one-line one -- and, for a paste of more than one line that does not end
in a newline, a second `\r`: one Enter leaves a multi-line input at `...`; a middle click does all of this
itself, holding the Enter until the REPL answers the paste), since line by line a blank line ends a Python block and Python
3.14's REPL auto-indents each line typed into it. The REPL gets the text
wherever it is -- at a `pdb` or `input()` prompt too. Over 9P, a range of a
`.py` pane goes to its bound REPL as a click would: write the event record
`MX<q0> <q1>` back to the `.py` pane's `event` (a range past its end is
refused, `range past end of body`). `Kill` does not stop
what a REPL runs, since pardes did not start it; `sig INT` on the REPL
pane's `pty/ctl` interrupts it as Ctrl-C would. Bindings are not dumped, so a Restore leaves none.

The root's pair clicks at the active pane and `/pane/<n>/look` and
`/pane/<n>/exec` at that pane; `/tagexec` and `/col/<n>/exec` click in the
workspace's or that column's tag (never an event reader's, which hears
only its pane's; a command they run starts in the session's directory,
where pardes started, not the focused pane's), and what the word says is logged as the session's,
`msg -`. 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; a look that found text answers the pane the text is selected in,
not the hits buffer it opened.

Reads are a stream: once an open has read the answer, a second read on it
gives EOF (on an open that wrote, until its next write); open it again, or
seek to 0, to read it again. The answer belongs to the open, as
/net/tcp/clone's does: an open that
wrote reads what its own last write touched, from the start after each
write, whatever offset the read comes at (a shell's `exec 3<>look` shares
one offset between its write and its read, as with `pty/run`); an open
that never wrote reads the session's last answer as it stood when it was
opened. So `echo x > look; cat look` works for one client, but two
clients doing it at once may read each other's; for that, write and read
on one open: `exec 3<>$m/look; echo x >&3; cat <&3; exec 3<&-`.

A write of command lines -- to `look`, `exec`, `tagexec`, a `ctl` of the
root, a pane or a column, or a column's `exec` -- runs each line once it is
whole: a mount cuts a big write at its message size (4 KiB through the
kernel's, 8 KiB from a client that asks), anywhere, and each piece comes as
a write of its own, so the open keeps a last line with no newline yet, or
an Edit block whose text has not ended, until its next write, and runs it
then; what is left when it closes runs at the close, where an Edit block
whose `{` or `a`/`c`/`i` text never ended fails and changes nothing
(``unmatched `{'``, or `a, c or i text not ended by a . line`, logged as an
`err`): sam takes the end of input for a `.`, but a block that reaches Edit
unfinished was cut short. A fragment never runs on its
own. A line or Edit block held past 1 MiB is refused (EINVAL).

A look takes acme's addresses after a colon (editors/acme/look.c:450): a
line written to `look` as `file:/re/`, `file:#n`, `file:$` or any address
opens (or finds) the file and selects what the address names. **The
address is evaluated from the file's dot**, as acme's is: `file:/re/`
finds the next match after the current selection, not the first in the
file. For the first, start at the top: `file:0/re/` (or `file:#0/re/`).
`:addr` does the same in the pane itself, and a pattern may hold blanks
(`calc.py:/return a/`). `file:12` selects line 12, its newline included,
as acme's does; `file:12:5` puts the caret at line 12, column 5. A bare
`/re/` is a path, as in acme, and failing that a search for its text. A
look that finds nothing, an address that does not evaluate, or a line past
the file's end (`calc.py:99`) says so on the message row and in the log
(`err <serial> look: ...`), focuses and opens nothing, keeps the
selection, and leaves `look` reading back empty.

`/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. A
terminal's `body` is its history as plain text, frozen per open, in logical
lines: a row the terminal wrapped is joined back to the row before it (the
wrap is ghostty's, as `pty/run`'s output unwraps), and the last line ends
with a newline. `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, and so does a line written to the
pane's own `look` or `exec`, or to the root's while that pane has the
keyboard, a click with no place in the text: an `F` record at `0 0` carrying
the line. So a client holding `event` that wants a command run gets its own
exec back as a record: it runs it through `ctl`, or writes the record back.
Writing a record back performs the action, as the click would have (acme's
xfideventwrite): a body `X` goes to a REPL bound for the text as a middle
click does, where an `F` record, a line written to `exec`, runs as the
command it was. acme takes back only `<origin>
<action><q0> <q1>`, the text of that range; pardes takes the record whole as
it was read too, and for an empty range acts on its text, which is how such
a line is done. A click in a file's body carries the offsets of the text it
took; one in a terminal's body cannot, since that body is a history
snapshot, and is also at `0 0` with its text. A click that takes no text
sends nothing, as in acme. `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 — followed by rio's `current` or `notcurrent` (rio(4), `wctl`):
whether the pane has the keyboard. It takes the pane's builtins (below),
`get`, which reloads the buffer from the name it
carries (unsaved edits are refused once, `<name>: Modified (get again to
discard)`, as acme's get asks winclean, exec.c:513). A file that changes on
disk reloads by itself only into a buffer with no unsaved edits; one with
them keeps its text and stays dirty, says `<name> changed on disk (get
reloads it, Save overwrites it)` and logs `changed <serial>`, and then its
`Save` warns once, `<name> modified on disk since read (Save again to
overwrite)`, as acme's Put does (exec.c:577) -- acme reloads nothing by
itself. `answer <choice>`
for the question the pane asks on its notice band, which the log names as
`ask <serial> <what> <choices>` -- `ask 4 del k j` for Del's side from the
keyboard (`k` the pane above takes the rows, `j` the one below), `ask 4
repl a b` for which bound REPL takes an exec (a REPL's letter) -- `answer -`
taking it back as Esc does (a choice the question does not offer is refused,
naming those it does, and the question stands; with no question standing,
`answer` is refused),
and acme's `lock` and `unlock` (editors/acme/xfid.c:603-611), for an
edit of several writes to `addr` and `data` that another client must not
land in the middle of. As in acme the lock binds only the clients that take
it: a `lock` while another open holds it fails at once with `file in use`
(EBUSY), to be tried again, until that open writes `unlock` or closes (or
the pane does) -- where acme's blocks, because through a kernel or FUSE
mount a blocked write would hold up the holder's own `unlock` and close on
that file -- and nothing else is refused for it --
not a write to any other file, not the person at the keyboard. It belongs to
the open that wrote it, so only that open's `unlock` is taken; a write on an
open that cannot write (or the editor's own, on none) cannot lock. From a
shell the lock needs an open held across the edit, since `echo lock > ctl`
closes, and so unlocks, at once: `exec 3>ctl; echo lock >&3; ...edits...;
exec 3>&-`.

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`, and pardes's own `12:5`,
below); `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 only the end of a forward
search, as acme's does, and reads empty until it is set. Truncating `dot` empties it, truncating `limit` lifts it --
though a write after the truncation that fails puts the old limit back, so
`echo /bad/ > limit` changes nothing -- and truncating `addr` leaves it as
it is (below).

A rename everywhere, or any other sam edit, is one write to the pane's
`ctl`: `Edit ,x/foo/c/bar/` runs acme's Edit (docs/tags.md) on the body as
one undo step; a failure fails the write with acme's words and changes
nothing. A write is one message a line, except that an `Edit` line takes
the lines after it while its `{` group is open or its `a`, `c` or `i` text
block waits for its `.` line, on a pane's `ctl`, the root's (at the active
pane) and `exec` alike; an unclosed group is refused (``unmatched `{'``).
A block may come in several writes on one open (bash's builtin `printf`
writes a line at a time): the open holds it until it ends (below).

A line number counts newlines as sam's lineaddr does (editors/sam/
address.c:180), so the empty line just past a text's last newline is an
address: `1` of an empty text is `#0,#0`, `2` of `a\n` is `#2,#2`, and
`Edit 1i/header/` on an empty file inserts; a line past that is `address
out of range`.

`line:col` is a pardes extension to sam's addresses, the spelling Look
takes in `file:12:5`: `12:5` is the point at line 12, column 5, and it
composes like any simple address (`12:5,14:1`, `12:5+#3`). The column is
in bytes from 1, as Look's is, clamped to the end of the line and snapped
back to the start of the rune it falls in; a line past the end, or
column 0, is `address out of range`. Since the column counts bytes, a
column a tool gives in characters (pytest's, a compiler's) is the same
only on an ASCII line; elsewhere address the line and search it
(`12/name/`) or use `#n`. sam would read `12:5` as a syntax
error.
Truncating `data` or `xdata` deletes the range `addr` names and nothing
else, so a shell's `echo NEW > data` replaces that range, `: > data`
deletes it, and `>>` inserts at it; only truncating `body` empties the
whole buffer. Truncating `data` is pardes's own: acme ignores OTRUNC there
(editors/acme/fsys.c:543) and every write inserts. And as in acme a write
leaves `addr` just past what it wrote, so a second `echo x > data` deletes
the empty range there and inserts after the first rather than replacing it
again; write `addr` before each replacement. A read of `data` or `xdata`
moves `addr` past what it read, as acme's does.
`addr` belongs to the pane rather than to a client and keeps what was written
until someone writes another, so writing an address and reading it back
evaluates it, which is what acme(4) promises of its own `addr`. Unlike acme,
neither an open nor a truncation resets it: acme sets it to `#0` when the
first client opens `addr` (editors/acme/xfid.c:105-108), which suits a
client that holds the fid, but a shell opens the file anew for every
`echo /re/ > addr` and so would search from the top each time and never
advance. Here each such write searches on from the last address, as `>>`
does; write `0` to start again from the top. A search wraps at the end of
the text, so a find-all loop stops when the address comes back to where it
began, or bounds itself with `limit`.

The regular expressions are mvzr's (sets, `\d`/`\w`/`\s`, `{m,n}` and
lazy `*?` included), searched the way sam searches (editors/acme/regx.c): as
lines, so `^` and `$` match at the start and end of any line, `.` and a
negated class never match a newline, and `$` also matches at the end of a
text with no final newline. A pattern that names a newline (`\n`) runs over
the whole text instead, its `.` kept to one line; there a leading `^` still
matches at every line start, and `$` may stand just before a `\n` (where it
changes nothing). Any other `^` or `$` in such a pattern, `(^|\n)def` or
`a\nb$`, is refused with `in a pattern with \n, ^ can only come first and $
only just before a \n`, since mvzr would read it as the start or end of the
whole text: never a search that silently finds nothing. Within a line, `^`
inside an alternation (`^def|^ `) matches only where the search starts, so it
works from a line's start (an `x` over lines, a `g` on one) and not from its
middle: an mvzr limit. An expression is
evaluated from the current address, the range last written to `addr` (or
left by the last `data` write, just past it), as acme evaluates it from
`w->addr` (xfid.c:446): `.` is that address, not the selection (`dot` is
the selection's own file), and `#9/re/` searches from `#9`; in `/a/;/b/`
the second search starts at the end of the first, as acme's `;` does, where
`/a/,/b/` starts both at the current address. `/re/` searches
forward from the end of the current range to `limit` if one is set, and
otherwise wraps to the start of the text; `?re?` and `-/re/` find the last
match ending before the range, wrapping to the text's last. The match is the leftmost, but of
the alternatives at that place mvzr takes the first that matches where sam
takes the longest (`/gam|gamma/` finds `gam`); in a search begun in the
middle of a line, `^` inside an alternation can match there; and in a
pattern that spans lines, `^`, `$` and `[^...]` keep mvzr's own meaning.
mvzr backtracks without bound of its own (`a?` twenty times then twenty
`a`s is 2^20 steps from each place it tries), and a search holds the editor, so pardes patches a
step budget into mvzr's matcher (build.zig): a search that spends it,
about 300 ms, fails with `regular expression search gave up, backtracking
past its step budget` rather
than answer a match it is not sure of. Ordinary patterns spend a few
thousand steps; what runs out is exponential backtracking, and a quadratic
pattern over a very long line (`\s*(\w+)\s*=` over 20 KB of letters).
pardes has no regex engine of its own on purpose; these are its limits.
Normal mode's `s` and `S` search a selection the same way (src/regexp.zig
is the one place both call), so `^` there also means a line's start.

An address that does not evaluate fails the write with why: `bad address
syntax`, `no match for regexp`, `address out of range`, `bad regular
expression`, `regular expression search gave up, ...`, or sam's
`addresses out of order` for a range that ends before it starts
(`#100,#50`), which acme lets through. A failed write to `addr` leaves no
address at all, where acme
keeps the old one: until a good address is written, `addr` reads empty
(as an unset `limit` does), and reading, writing or truncating `data` and
`xdata` fail with `no address: the last one written to addr failed`, so a script that
missed its target cannot then write at the last one.

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.

`tag` reads the whole tag as the pane shows it: the computed path or PDF
page (no mark for unsaved text: the grip shows that, and `dirty` says it),
then the text you may edit. A write appends to that text,
newlines included, and a tag with more than one line takes a row per line on
screen; truncating `tag` clears it, as acme's `cleartag` does -- the default
words (`Del`, `Put` and the rest) with it, since they are that text until
you edit it, so `echo Make > tag` leaves only `Make` -- a truncating write
drops the one newline that ends what it wrote, which would draw an empty
row, and keeps any other; append with `>>` to
keep them, with `printf ' Make' >> tag`. The leading blank is needed: the
tag reads back with no blank after its last word, so `printf Make >> tag`
glues `Make` onto it; and `echo ' Make' >> tag` ends with a newline, which
starts a new line of the tag. The clearing is an edit of the tag like a typed one and its undo
history is kept: `u` in the tag brings back the text it cleared, words
included.

Stats report real lengths for `index`, `status`, `look`, `exec`, `listeners`,
`name`, `body`, `tag`, `sel`, `ctl`, the range files and the flag files, and
for `event` and `pty/data` the length of the record a read would
answer, which is zero when nothing is waiting; for `log`, the text an open
would freeze now. 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` is one ring (64 KiB) that records whether or not anyone reads it:
`new`, `del`, `rename` (a terminal's too, as its shell changes directory --
a new terminal is `new N /` until its shell says where it is, then renamed;
it runs `ls` once at its first prompt, a greeting that shows the directory
it opened in, since a terminal is named by its directory and nothing else
on it says where it is --
since a terminal is named by its directory), `exit <serial> <N>` before
the `del` of a terminal whose shell exited by itself, `ask <serial> <what>
<choices>` when a pane asks a question (answered by `answer` on its ctl;
`ask <serial> save path` when a Save made over 9P, on a terminal or a
scratch, opens its prompt for a path, answered `answer <path>` or `answer
-`; asked again, the one open stands, not a second),
`answer <serial> <choice|->` when it is answered, by key or ctl, `-` for
taken back or for its pane closing with the question standing,
`changed <serial>` when a pane's file changed on disk under its unsaved
edits (below), and `save <serial> <name>`,
`dump <path>` when a Dump is written and `restore <path>` in a Restore's
new log after its panes' `new`s (a relative Restore path is looked for in
`DumpDir`, then in the directory pardes started in; a bare Restore takes the
last dump this session wrote), then `restored <old> <new>` for each pane,
mapping the serial it had to the one it has now, and `restoredcol <old>
<new>` for each column, and `msg <serial|-> <text>`
for every line the editor says (with `verbose` on, that includes each
builtin announcing itself as it runs, on purpose: the log says which ran --
unless the builtin then says something of its own that starts with its
name, `Kill: nothing running`, which takes the announcement's place; a
builtin that fails a ctl write is logged by that write's `err` alone, no
announcement and no `msg`, so the same failure again is the same record
again; a line said again word for word is counted, `msg 3 Undo: nothing to
undo (x40)`, as `err` is (below); its serial is the pane it ran at, `-` when the keyboard was on a
column or workspace tag, or the line came to the root's ctl, `/tagexec` or
a column's ctl or exec), and `err <serial|->
<file>: <why>` for every write or truncation the tree refused or that
failed -- through a mount a shell sees only the errno its kernel mapped the
reply to, usually `Invalid argument`, and this is the reason (`err 3 addr:
no match for regexp`). A record said again word for word, straight after
itself, is counted rather than repeated (`err 3 addr: no match for regexp
(x4)`: four in all, counting the first), so a client retrying a failing
write does not push the rest out of the ring. A record a follower has
already read is never rewritten: the next repeat is a line of its own
carrying the running total, `(x5)`, and counting goes on from there, so a
follower sees each count as a new line. Through a kernel mount a client sees only an errno, which 9ns reads from the
error's words (cloud9's 9ns/src/nine.zig, `enameToErrno`): a malformed write
-- an unknown or ill-formed control message, `bad address syntax`, `bad
regular expression` -- is EINVAL; a lock another open holds, EBUSY; a pane
gone, ENOENT; a well-formed write that fails -- `no match for regexp`,
`address out of range`, `addresses out of order`, a search that gave up,
`<name>: Modified (Exit again to discard)` -- EIO. The err record has the
words. There is no per-pane error file to read instead:
acme's `errors` only takes text, and one record stream is simpler to watch
than a file per pane. A `msg` said while a
pane is being made can precede that pane's `new`; panes present at boot are
recorded before anything else.
Control characters in a record become spaces, so a record is one line.
(An `event` record is not: acme's `<origin><action><q0> <q1> <flag> <n>
<text>\n`, whose text may hold newlines. The origin is `E` (a 9P write to body or tag), `F` (other
files, the editor's own lines), `K` (the keyboard) or `M` (the mouse); the
action's case says where: `x`/`l` a click executed or looked at in the tag
(its offsets count the tag's whole text, path included, as `tag` reads),
`X`/`L` in the body, `I`/`D` text put in or taken out of the body, `i`/`d`
of the tag. The flag is acme's: 1 the text is a builtin's word, 2 the range
was expanded from a click (a second record gives the original range), 4 (a
look) the text is a file name or address, 8 (an exec) chorded: two records
follow, the argument's text and where it came from, `<file>:#q0,#q1`.
Written back, an `X`/`x` record executes and an `L`/`l` record looks, as
the click would have. A chorded one (flag 8) runs with its argument: the
record after it in the same write, else the one kept from the click; its
two follow-up records written back after it, together or in writes of
their own, are consumed as its, never run as commands (written back alone
with no argument kept, it waits for its argument record); the origin letter
written back is ignored but for `F` on `X`, which runs as the command it
was rather than going to a bound REPL; `I`, `D`, `i` and `d` are reports and
are refused. Read `n` bytes of the text, not up to a newline -- bytes here, where acme counts runes. Every offset and
count pardes serves is in bytes, `#n` and `q0`/`q1` too; the event count
follows them rather than switch alone, so an acme library reads pardes
correctly for ASCII text and not beyond it. Offsets are bytes, but every
address lands on a rune boundary, as sam's and acme's work in runes, never
inside a multibyte rune and never widened to a grapheme cluster: a `#n`
inside a rune snaps back to its start, a `line:col` likewise, a search's
match covers the runes it touches, and a copy of addr to dot keeps its
runes, so a lone combining mark or the `\r` of a CRLF is addressable on its
own (an Edit's `x`, `y` and `s` match the same way: `.` is one rune); `addr` reads back the snapped offset. (How the terminal draws such a
text, a cluster to a cell, is apart from this.) A click in a
tag gives offsets into the whole tag as `tag` reads it, the path first.) An
open freezes the ring's text the way `/screen` freezes a frame: reads walk it
and end. Writing `follow` to that same open makes reads past it wait for the
next record, one per read, after first reading all that the open froze (the
ring's whole history, up to 64 KiB); `follow new` skips that and waits for
what comes after, as `tail -n0 -f` does. A follower the ring outran reads
`lost N` first.
Closing the open is the only way back, as with rio's `consctl`. A follower
misses nothing within a session only: a Restore hangs its connection up
(and a 9ns mount with it, which must be restarted), so it dials again and
reads the new log from its `restore <path>`.

A read with nothing to give yet -- a following `log`, `event`, `pty/data`, a
`pty/run` before its answer -- is held, the way factotum holds its log's reads
(security/auth/factotum/log.c) and acme an event read: the core keeps it, and
whoever next has news for it (a record, output, a run's answer, the pane
closing, which answers `no such pane: its window shut`, ENOENT as any
other file of a gone pane gives, where acme says "window shut down") answers it on the
connection it came on as the turn is given up. Nothing else parked is
retried for it. The core keeps the ticket cloud9 gave the park
(`Conn.hold`) and answers only while that very park waits, so a read the
client flushed or whose fid it clunked meanwhile is dropped unanswered, no
record is spent on it, and a tag the client reuses is never answered with
what was meant for the old one. An open waits with one read at a time, as
acme's window keeps one `eventx`; a second read on it meanwhile fails with
"file in use". QUIC connections still retry their parked reads each tick.

`pty/run` runs one line at a terminal's prompt and answers how it ended, on
the same open (factotum's `rpc` shape): write the line, then read `exit N`
once the command has ended and the shell is back at a prompt, followed by
what it printed. The output is what the screen showed between the command's
start and end marks: stderr interleaved, `\r` progress collapsed to its last
state, no colour, tabs as the spaces they drew, trailing spaces trimmed and
trailing blank lines dropped (`printf 'a\n\n\n'` answers `a`), leading
whitespace kept; a program on the alternate screen (vim, less, htop) leaves
none, and rows a program redrew above its start are missed. A bash job notice
printed before its PROMPT_COMMAND lands in the next run's output. Only its
last 64 KiB are kept, from a line start, and the header then reads `exit N
cut M` (M bytes left out). `exit N cut`, with no count, says the output's
start is not there to read: it scrolled out of the history, the command
erased the screen (`clear`, `watch`, a full-screen program's redraw, a
reset -- so such a command's output may read as cut), a start or end mark
came on the alternate
screen, or the command printed more than 8192 rows, of which only the last
are read so that the answer costs the editor a bounded amount. `exit ?` is a
command whose end mark carried no status, which is not a success; `error out
of memory` is an answer that could not be made. The header is always the
whole first line. It reads
`busy` at once when a command is running or text is typed at the prompt
-- a run is a line typed at the shell's prompt, so a program holding the
terminal (a REPL, `less`) takes none: write to `pty/data` for it --
which is also when the third field of `pty/status` reads 1. `pty/status` is
one line, three right-aligned fields and a newline: the pty's columns and
rows, then busy (0 or 1). The size, and `pty/ctl`'s `winsize` read back, is
what `winsize C R` last set, until the pane itself resizes and gives the pty
its grid again. A line written
before a new terminal's shell has drawn its first prompt is not busy: it
waits for that prompt (a respawn meanwhile keeps it waiting for the new
shell's) and is sent then, so the first command a script gives a fresh
terminal is not lost. A shell that never draws a tagged prompt (one pardes
could not instrument, or a startup that hangs) leaves such a line waiting
for ever: cancel the read (interrupt it, or close the open) to give up;
`error not run` when the shell refused
the line without running it (a fish syntax error; the line is taken back off
the prompt): the shell's marks say only that it did not run, so the answer
carries no reason or code, and the shell's own complaint is in the pane's
body (`tail body`); `exit N` and what it printed when the line ended the shell
itself (`exit 3`, or `echo bye; exit 3`): its terminal closes, and a read
of the run's open still answers after the pane is gone; `error shell gone`
when the pane closed or its shell was replaced, or the shell went without
an exit status to tell; `error no prompt marks` for a shell pardes could not instrument;
`error command done; not a shell` (or `error a command runs here, not a
shell`) on a command pane, whose child is its command.
It relies on the OSC 133 marks pardes injects into bash and fish, tagged
`aid=pardes` so fish's own marks and a nested shell's are ignored. A second
line on an open whose command still runs fails the write. `exec zsh` or a
continuation prompt never reports an end; cancel the read. A read of `run`
waits in pardes, so through 9ns it needs 9ns's concurrent requests or it
holds up the rest of the mount. A record
longer than a read comes in pieces, so a shell's `read` loop works: `exec
3<>$m/log; echo follow >&3; while read -r line <&3; do ...; done`. bash's
`read` takes a chunk, keeps one line and seeks back to just past it; a
followed log, `event` and `pty/data` answer a read at an offset inside
their last answer from that answer again, so no record is lost. `tail -f`
never writes `follow`, so it sees nothing new: use the follow open instead. `/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. An open that holds something between open
and close -- a frozen screen, terminal body or log, a run, an `event` or
`pty/data` open -- takes one of 64 records (lib9p's per-fid aux, acme's
Fid), released on close or disconnect; past that such an open fails with
`ENFILE`. Other opens hold nothing and are not counted.

`-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. It is off by default
everywhere; the esp32p4 image in particular has no room for them (~1.8 MB of
source against a 1.5 MiB app partition).

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 an open of `/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.