summaryrefslogtreecommitdiff
path: root/docs/typ/reference.typ
blob: 37f3acbc0c0c91255cad978eaaabc06300c4f878 (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
// The reference: every file the 9P control filesystem serves, what it
// does, how it fails, and the limits. How pardes behaves on screen is the
// guide's; recipes and traps are scripting's; how a mount cuts writes and
// the listeners are building's.
#import "style.typ": key, keys, btn, chord, word, tag, addr, file, cmd, doc, pairs

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. Paths here are the served ones (#file("/pane/2/body")); through a
mount they sit under the session's directory (`$m`; #doc("scripting", section: "find-the-session")
says how to find it). The served #file("/README") is a
one-screen summary of this page.

= The tree <tree>

```
/README        the one-screen summary (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 #file("/pane/new") makes a pane (so `ls -l` and `find` are
safe), only `rmdir` of #file("/pane/<n>") or an empty #file("/col/<n>")
removes. Tcreate is refused everywhere.

#file("/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
#file("/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 <rules>

*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
#file("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's `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`, `Modified` or an `Edit` command pardes leaves out (`w is a sam command pardes's Edit leaves out`), EIO; no refusal reads as EOPNOTSUPP.
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, #word("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.* #file("look"), #file("exec"), #file("tagexec"), the three
kinds of #file("ctl") and a column's #file("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 line runs once it is whole: a write's last line runs
without its newline, except where a mount cut the write
(#doc("building", section: "writes-through-a-mount")); end a write with a newline when its result
matters. An `Edit` whose `{` or `a`/`c`/`i` text is still
open waits for the next write on that open. A line held past 1 MiB is
refused.

*Answers.* Reading #file("look"), #file("exec") or #file("tagexec") answers
the serials the last command made, or else the pane it acted on or
focused, one a line; nothing when it did none of these (#word("Newcol") at
#file("/tagexec")). 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:
#cmd("exec 3<>$m/look; echo x >&3; cat <&3; exec 3<&-")

*Snapshots.* #file("/index"), #file("/layout"), #file("/recent"),
#file("/commands"), #file("/status"), #file("/listeners"), a read-only
#file("ctl"), #file("/log"), #file("/screen") and a terminal's
#file("body") freeze at the open, so one read in several chunks never
splices two moments; open again for now. Such an open, or a #file("run"),
#file("event") or #file("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 #file("log"),
#file("event"), #file("pty/data"), #file("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.
One connection holds at most 128 reads at once; the next is refused `too many reads waiting: 128`. Every held read is on an open that keeps state,
and those are 64 in the session (above), so 64 is the real cap on reads
held at once, through one connection or many. A mount (9ns) is one
connection, and it keeps answering everything else beside its held reads.

*Stats.* A file whose text is kept has its real length: a pane's
#file("body"), #file("tag"), #file("name"), #file("ctl"), #file("sel") and
the range and flag files, the workspace's #file("tag"), the root
#file("ctl"), #file("status"), #file("commands"), #file("README"), the
#file("look")/#file("exec") answers, #file("focus"); #file("event") and
#file("pty/data") the next record's, zero when none waits; #file("log")
what an open would freeze. A view generated by each read stats 0, as
acme's do: #file("screen"), #file("data"), #file("xdata"), #file("index"),
#file("layout"), #file("recent"), #file("listeners"), #file("pane/new").
Read those to the end rather than trust a length (`cat` does). The qid
version of #file("body"), #file("data") and #file("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 <look-and-exec>

A line written to #file("look") is a #btn("B3") click on it, on the line as
written: its leading blanks are its own (`    return x` finds that
indented line, and a diff's blank context line ` ` is a look too); only its
newline and `\r` go. An #file("exec") line's blanks at either end are
trimmed. The guide says what a look opens and where it looks for a relative
path (#doc("tags", section: "looking")); here is what is particular to the
files.

- `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 leading `~` is the home directory (`$HOME`, else the passwd entry's;
  `~user` that user's) here and wherever a path is typed (#file("name"),
  #word("Save"), #word("ThemeFile"), #word("DumpDir"), #word("Restore"),
  `pardes '~/x'`), even beside a file named `~`: write `./~` for that.
- A path to no file is a miss, as a search that finds nothing is: said on
  the message row and logged as an `err`, the write still answered. A file
  that is there but will not open (no permission to read it, say) fails the
  write, with why.
- 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>:99 has no line 99`
for a line past a file's end, `<path> has no page 99` for a PDF's page;
`<path>: no match for regexp` or `<path>: address out of range` when an
address fails), opens nothing, and leaves #file("look") reading empty; the
write succeeds. A path too long to repeat whole gives up its middle to `…`.

A line written to #file("exec") is a #btn("B2") click:

- A builtin word runs (#file("/commands") lists them). A builtin that needs
  its argument (#word("Msg"), #word("Mount"), #word("Find")) written bare is
  `wrong #args in control message "Msg"`, and so is one that takes none
  written with one (`Config extra`).
- A line starting with `#` is a comment, as in a shell: it runs as nothing,
  silently, here and in every ctl.
- The language server's words ask about a file pane's text at its cursor
  (set it with #file("addr") and `dot=addr` first): #word("Hover") fills
  `+Hover`; `Rename new` renames the symbol in the file and says how many
  ranges it changed (on the message row, and so in the log) without listing
  them, fails when there is nothing to rename, and lists the files of a
  rename the server spreads over several in `+Search`;
  #word("Diagnostics") and #word("Symbols") list the file's in `+Search`;
  #word("Lspinfo") says which server serves the file and its state;
  #word("Lspwhy") narrates the last query step by step (in `+Lsp`). The
  write returns once the answer is in; a pane with no file is refused.
- acme's words run as pardes's where it has one (`Put` is #word("Save"),
  `Delete` a #word("Del") that does not ask); the rest (`Get`, `Putall`,
  `Snarf`, `Cut`, `Paste`, `Zerox`, `Sort`, `Load`, `ID`, `Send`, `Tab`,
  `Indent`, `Local`, `Incl`, `Abort`) 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, run as the guide
  says (#doc("tags", section: "command-panes")): typed into a terminal idle
  at an empty prompt, else run in a command pane. #file("exec") reads back
  the command pane's serial; the log says `run <serial> <line>` and `exit <serial> <N|?>`. A reused pane's #file("body") keeps every earlier run
  above a `% <line>` row naming each command, so to take only the last
  run's output read from after the last `% ` row:
  #cmd("awk '/^% /{out=\"\"; next} {out = out $0 \"\\n\"} END {printf \"%s\", out}' body")
  From a pane whose directory is gone nothing runs: `exec: <dir>: no such directory` (ENOENT).

The root's #file("look") and #file("exec") act at the active pane and log as
that pane's (with no pane at all, in the session's directory: a look opens
its file, making a column as #word("New") does, and an exec runs its
command there); #file("/pane/<n>/look") and #file("exec") at pane n;
#file("/tagexec") and #file("/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 (#word("Undo"), #word("Msg"), #word("Save")) is refused
at #file("/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. #word("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, named or not, it says `Kill: nothing running` and the
write succeeds; words that name none of what runs fail it (`Kill: no running command has that first word`). #word("Kill") does not reach a
REPL's code: use `sig INT` on its #file("pty/ctl").

== Look paths and mounts <mounts>

A look resolves the OS filesystem first, then the editor's own tree.
Explicit paths skip that search:

#pairs(
  [`/n/os/proc/self`], [the host filesystem, served as #file("/os/proc/self")],
  [`/n/self/pane/2/body`, `/virtual/pane/2/body`], [this session's tree, #file("/pane/2/body")],
  [`/virtual/src/pardes.zig`], [sources embedded with `-Dembed-sources=true`, #file("/src/pardes.zig")],
  [`/n/peer/pane/2/body`], [a mounted session: the peer's #file("/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. #word("Mount") dials at once and fails if
nothing answers (`dial failed: no answer`, `timed out`, `hung up`); with no
dial it is `wrong #args`, and a dial that is no address `bad dial address`,
both EINVAL. There are eight named mounts; `os` and `self` are reserved.
#word("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 root ctl <root-ctl>

Reading #file("/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
  #word("Placement"), #word("BootShell"), `Crt`). A value it does not
  take is `bad value in control message; ...` naming what it takes;
  #file("/commands") lists them. A setting the frontend cannot show is
  refused (`Lift is GUI-only, invalid here`).
- #word("Newcol") makes an empty column right of the keyboard's, halving
  it. #word("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`.
- #word("Exit") quits. While panes hold unsaved text it refuses once
  (#doc("tags", section: "unsaved-panes")): 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. #word("Restore"), #word("Del"),
  #word("Delcol") and a pane's `get` refuse the same way with their own
  word.
- #word("Dump") writes `pardes-<date>-<time>.zon` (UTC) in #word("DumpDir"),
  logs `dump <path>`, and adds a `Restore <path>` word naming it to the
  workspace tag (the last dump's, replacing an earlier one's).
  `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.
- `Kill [word...]` (above), `Mount name dial`, `Unmount name`, `Theme x`.
- `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 (#word("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.

== Settings <settings>

A setting that chooses among words (`on`/`off` switches,
#word("Placement"), #word("BootShell"), `Crt`, `Bloom`,
`Vignette`, `Grain`, `Lift`, #word("Motion"),
`ShaderAnimation`) steps to its next value when given bare, as its
word clicked in a tag does. The same lines go in the startup file
(#doc("config")).

#pairs(
  [`Theme <name>`], [`orchard`; names as #word("Themes") lists them, #word("NextColor") walks the ring (#doc("themes"))],
  [`ThemeFile <path>`], [a `.zon` theme, relative to the config directory; reloads live when saved],
  [`FocusTint`], [on: tint the focused pane's and column's tags],
  [`SyntaxBold`], [off: bold syntax keywords],
  [`Verbose`], [on: a builtin announces its name on the message row],
  [`MessageAnimation`], [on: messages ease in and dissolve],
  [`MessageLinger`, `MessageFall`, `MessageDissolve`], [800, 180, 150 milliseconds, at most 60000],
  [`Placement acme|pardes`], [`acme`: where new panes go (#doc("tags", section: "new-panes"))],
  [`BootShell keep|replace`], [`keep`; `replace` closes the untouched lone shell a dragged document lands beside],
  [`LookWord search|list`], [`search`: a looked-at word selects its next place, or lists all in `+Search`],
  [`Shell <name or path>`], [`$SHELL` when it is executable, else `/bin/sh`: the shell the next terminal and command pane run; a bare name is looked for in the usual bin directories, not `$PATH`; bare #word("Shell") returns to the default],
  [`DumpDir <dir>`], [`$XDG_DATA_HOME/pardes`, else `~/.local/share/pardes`: where #word("Dump") writes; bare returns to the default],
  [`TreeContext`], [off: sticky declaration headers in a source pane (per pane, dumped)],
  [`TreeContextTagStyle`], [on: draw those headers in the tagline style],
  [`LocationsConfig ...`], [the layout of #word("Grep"), search and language-server results (below)],
  [`Wrap`, `Colors`, `Tagbottom`, `Debug`], [toggles],
  [`Font <name>[:<size>]`, `Fonts`], [SDL and macOS only; size 8-72 (pixels in SDL, points on macOS)],
  [`TaglineSize <1-100>`], [82: tagline face, percent; SDL and macOS],
  [`WindowOpacity <0-100>`], [100; SDL only: everything but text and the cursor],
  [`Ligatures`], [on; SDL only, macOS draws CoreText's own],
  [`Pet cat|frog|off`], [off; SDL only: a sprite in the workspace tag's blank space],
)

`LocationsConfig` with no argument prints the current settings as a line
that can be run again; with fields it changes only those:
`LocationsConfig context:5 tscontext:on tslocations:off layout:stacked`.
`context` (0) is the source lines shown above and below each match;
`tscontext` (off) includes the enclosing tree-sitter declaration headers;
`tslocations` (on) shows a location on each declaration header; `layout`
(`stacked`) puts the location on its own line, `inline` beside the source,
padded in groups of eight matches. An invalid field rejects the whole line;
a repeated field's last value wins. The settings survive Dump and Restore.
Source analysis is cached for 64 files and 64 MiB.

*Effects.* Panel transitions, one at a time, running the active one again
turning it off: #word("PanelSlide"), #word("PanelZoom"),
#word("PanelDissolve"), #word("PanelAscii"), #word("PanelVertical"),
#word("PanelEdges"), #word("PanelFall"), #word("PanelWave"),
#word("PanelCurtain"), #word("PanelScramble"), #word("PanelType"); all
start off, and the web shell has none. Scene passes (the SDL window, and
one attached to a detached session), each at a level 0-3 (`on` is 2), all
off: `Crt`, `Bloom`, `Vignette`, `Grain`.
`Shader <file.glsl>` adds a Shadertoy file written for ghostty to the
chain (`Shader off` removes it; it recompiles when saved);
`ShaderAnimation off|on|always` says when the chain animates by itself. The
focused pane can stand off the page: `Lift shadow|rim|auto|off`,
`InactiveDim <percent>`, `Motion off|crisp|smooth|bouncy|playful` (default
`smooth`), `SelectionGlow`, `HoverGlow`, `Occlusion`,
`Parallax`, #word("JumpTrail"), #word("ChipShadow"),
#word("ThumbFlash"), `CursorBlink`, `GripWidth <50-300>`. Most are
the GUI's; #word("InactiveDim") works everywhere, and #word("JumpTrail"),
#word("ChipShadow") and #word("ThumbFlash") are the terminal's. No effect
may lower the contrast of text, the selection or a focus indicator.
`EffectCode <effect>` lists that effect's sources under `/virtual` when the
build embeds them.

Resting the pointer on text for about 32 ms (`look_preview_delay_frames`,
2 frames, in `src/config.zig`; `null` turns it off) tints what a
#btn("B3") click would look at, with no other effect. A terminal's
#word("Filter") keeps each foreground at least `tty_filter_min_contrast`
(WCAG 1.5, `src/config.zig`) against its background.

= Columns and tags <columns-and-tags>

#file("/layout") has a line per column, left to right: `serial index x width current|notcurrent empty|full pane-serials...`, then `active <serial>` (the active column: where #file("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 (#doc("tags", section: "active-column")).
#file("/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, so 16 wants a window 160 cells wide or more.
#word("Newcol") halves the column it is run from, and one under 20 cells
does not split (`this one is too narrow to split`): reach 16 by writing
#word("Newcol") to the widest column's #file("exec") each time, not to the
root's, which keeps halving the one just made. #word("Newcol") is refused
as well when a narrower column would wrap its panes' tags onto more rows
and leave one under its tag and two rows (`Newcol: no space for a column: the panes' tags would not fit`); a refused #word("Newcol") logs its `err`
alone and uses up no column serial.

#file("/col/<n>/ctl") takes #word("Delcol"), #word("Joincol"),
#word("New") and #word("Tty") for that column; #file("/col/<n>/exec") is
a click in its tag; `rmdir /col/<n>` closes an empty column (one with
panes: ENOTEMPTY). Closing a column's last pane leaves it empty (the
keyboard goes to its tag, #file("focus") reads empty, the log says only
`del`); closing the session's last pane quits pardes. The log says
`newcol <serial>` and `delcol <serial>`.

#file("tag") files (#file("/tag"), #file("/col/<n>/tag"),
#file("/pane/<n>/tag")) read the whole tag, with no newline after it. 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 (`no space: over 4096 bytes`, ENOSPC; the log says `err <serial> tag: no space: ...`). All
three kinds take the same checks, whole or not at all, and a `>` whose
write is refused changes nothing: its truncation is done with the write
that fits, or at the close (or a read) when none came. Each write stands
on its own: in a `>` cut into several writes, those taken before a refused
one stay in the tag; only the refused write changes nothing. A clear is an
ordinary edit and #key("u") in the tag undoes it.

= Panes <panes>

*Making and closing.* Opening #file("/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 a #word("New") from a
tag would (#doc("tags", section: "new-panes")): 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,
#word("New") or #word("Tty") alike. A #file("pane/new") whose column is
full takes its rows from a pane in another column that has them, last
column first, as acme does, and is refused only when no pane anywhere can
give them. `rmdir /pane/<n>` closes the pane, unsaved or not.

*#file("name")* reads the file name (a terminal's directory); a write
renames the buffer (relative to the pane's directory) and marks nothing
dirty; #word("Save") then writes under the new name. A name takes any byte
a file name can hold, blanks, control bytes and bytes not UTF-8 included,
so what #file("name") reads writes back as it was; refused (EINVAL) are
only a second line and a NUL (`bad character in file name: a NUL`). Up to
255 bytes a component. The ctl word `name x` takes all after its one
blank; a second blank there is refused rather than read as the name's
first byte. A directory (`/`, `~`, `foo/`) is no file name: refused,
EISDIR (`name: /home/u is a directory, not a file`).

*#file("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 as typed keys, never a paste: no bracketed-paste marks around it,
even when the program asked for them, so a newline in it is Enter. A PDF's
body is the text layer of the page shown, read-only: it is no text of the
pane's, so #file("addr"), #file("data"), #file("dot") and a `file:<addr>`
look do not address it (a PDF's `:<n>` is its page); images and PDFs take
no write (`this pane has no text`).

*#file("sel")* reads the selected text; a write replaces it.
*#file("errors")* is write-only: text appended to the directory's
`+Errors` pane (logged as `msg` records when no column has room).

*#file("focus")* (root): a serial written gives that pane the keyboard and
makes its column the active one (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: #word("Del") (`Del k`/`Del j`, or
  #word("DelAbove")/#word("DelBelow"), give its rows to the pane above or
  below), `Save [path]` (making the directories the file goes in first,
  whichever name it writes), #word("Collapse"), #word("Undo")/#word("Redo")
  (256 steps each), `Find pat`, `Edit ...`, `Tty [shell]` (a new terminal
  in its directory), #word("Delcol") for its column, and
  #word("Left")/#word("Right")/#word("Up")/#word("Down"), which give the
  keyboard to the pane beside it that way (#key("Ctrl-w") with
  #keys("h", "l", "k", "j")), up from a column's top pane to its tag.
- `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 #file("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
#word("Save") warns once.

== Addresses and data <addresses>

#file("addr"), #file("dot") and #file("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; a pair is
checked as an address is, `addresses out of order` (`5 2`) or `address out of range` (past the text), never clamped. #file("addr") names where
#file("data") reads (to the end of text) and the range #file("xdata")
reads; #file("dot") is the selection (moving it scrolls the pane there);
#file("limit") bounds the end of a forward search and reads empty until
set. Truncating #file("dot") empties it, truncating #file("limit") lifts
it.

- A write to #file("data") or #file("xdata") *replaces* the #file("addr")
  range (`>` and `>>` alike); `: > data` deletes it. Truncating
  #file("data") is pardes's own (acme ignores OTRUNC there); only
  truncating #file("body") empties the buffer.
- A write leaves #file("addr") just past what it wrote, so a second
  `echo x > data` inserts after the first: write #file("addr") before each
  replacement. A read of #file("data")/#file("xdata") moves #file("addr")
  past what it read.
- #file("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 #file("limit").
- A failed address leaves *no address*: #file("addr") reads empty and
  #file("data")/#file("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 #file("addr"), or just past the last
#file("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 #word("Recent"), `+Search` or the #word("Jumplist")
spells a range `12:5-14:2` (through 14:2 inclusive) and #file("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 <regexp>

Patterns are 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 #key("s") and #key("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 with `^` or none (`^def|^ `
  works, `^def|x` is refused: `an alternation anchors every branch with ^ or none`). A `$` does not count: `foo$|bar` is fine.
- 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>

`Edit <sam commands>` on a pane's #file("ctl") or #file("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:
#cmd("printf 'Edit ,x/foo/{\\ni/</\\na/>/\\n}\\n' > $p/ctl")

== Flags and undo <flags>

#file("dirty"), #file("mark") and #file("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. #file("/index")'s dirty flag is #file("dirty"); a `+New` scratch
reads 1 but holds up nothing under 100 bytes.

The writes of one open of #file("data"), #file("xdata") or #file("body")
are one undo step (so a multi-line `printf` is one), as long as no other
client or keystroke edits the pane between them: such an edit ends the
step, and the open's next write starts another. To make a loop of opens
one step: `echo 1 > mark` (a point now), `echo 0 > mark`, the writes,
`echo 1 > mark`.

== event <event>

Holding #file("event") open takes the pane's look and execute clicks: they
come to the reader as records instead of acting, as do lines written to
the pane's own #file("look")/#file("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. One open
reads a pane's #file("event") at a time, as acme's is one window's: a
second open for reading fails `file in use` (EBUSY) until the first is
closed.

- 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 last record of a write needs no newline),
and an empty one (`MX12 12`) on the word or file name a click there expands
to; 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
#file("event") that writes its own pane's #file("exec") gets its command
back as a record: run it through #file("ctl") instead.

== REPLs <repls>

`Repl python` on a terminal's #file("ctl") (or in its tag) binds it as
that language's REPL: a #btn("B2") click or #key("Tab") on a `.py` body
then types the text into the REPL instead of running it; builtin words,
tag words, `Exec <text>` and #raw("@`cmd`") words still run. #word("Repl")
takes the languages a code fence names (ada, bash, c, c_sharp, clojure,
cpp, css, elixir, erlang, fortran, go, haskell, html, java, javascript,
json, kotlin, ocaml, markdown, pascal, php, powershell, python, ruby, rust,
scala, typst, zig) and aliases such as `py` and `sh`. Its tag and
#file("ctl") line show its id, `python-a`. `Repl -` unbinds, bare
#word("Repl") says the binding. With several bound, the pane asks (`ask <serial> repl a b`). Bindings are not dumped.

A 9P #file("exec") is never sent to a REPL. A script either writes the
event record `MX<q0> <q1>` to the `.py` pane's #file("event") (sent as the
click would be), or writes the REPL's #file("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 <pty>

Terminal panes also have #file("pty/"):

- #file("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).
- #file("pty/status"): one line, `cols rows busy`; busy is 1 while a
  command runs or text is typed at the prompt.
- #file("pty/ctl"): `winsize C R` (at least 2 rows, fewer refused `invalid winsize: at least 2 rows`; at most 4096 a side), `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).
- #file("pty/run"): one line at the shell's prompt, answered on the same
  open: #cmd("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.

#file("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
#file("pty/data").

= The log <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. A follower
the ring outran reads `lost N` first. A Restore hangs the follower up: dial
again and read from `restore <path>`.

#pairs(
  [`new <serial> <name>`, `del`, `rename`, `save`], [a pane made, closed, renamed (a terminal's too, as its shell changes directory), saved (`Save path` of a copy: `save <serial> <path>`, the path written)],
  [`newcol <serial>`, `delcol <serial>`], [a column made or closed],
  [`msg <serial|-> <text>`], [the editor said something (not a builtin's own name under #word("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
#file("look")/#file("exec")), `-` for the root #file("ctl"),
#file("/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 <other-files>

- #file("/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.
- #file("/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).
- #file("/recent"): up to 200 files, PDFs and images, most recent first,
  `open <path>` or `closed <path>`; kept in `$XDG_STATE_HOME/pardes/recent`.
  #word("Recent") shows them in a pane, an open one at its dot now and a
  closed one at its last (a PDF's is its page); a look at a row reopens it
  there.
- #file("/status"): `pid`, `version`, `panes`.
- #file("/listeners"): the session's dial addresses, a line each
  (#doc("building", section: "listeners")).
- #file("/os/"): existing regular files take read, write and truncation to
  zero; create, remove, rename and mode changes are refused as not
  permitted (`permission denied`, EACCES through a mount); ownership is
  synthetic. Linux v9fs's truncation `mtime` hint is accepted and dropped.
- #file("/src/") (and #file("/shaders") on GUI builds) with
  `-Dembed-sources=true`; `EffectCode <effect>` lists an effect's files
  under `/virtual`.

= Limits <limits>

#pairs(
  [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], [65536 (64 KiB) offered],
  [connections], [16 Unix and TCP, 16 QUIC],
  [opens holding state], [64],
  [held reads a connection], [128],
  [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 (about 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],
)