summaryrefslogtreecommitdiff
path: root/docs/typ/reference.typ
blob: a7ac997e1c25ecbfc09341e80a610d88a7b85360 (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
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
// The reference: every file the virtual filesystem serves over 9P, 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, fstree

Every native session is a virtual filesystem, served as 9P2000 (not .u,
not .L) and modelled on acme's (#doc("reference", section: "from-acme")):
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. Two builtins: #word("Look") (right-click): open what the
text names, or else find it; #word("Exec") (middle-click): run the builtin
it names, or else run it as a shell line. The files #file("look") and
#file("exec") take a line each, as those clicks would.

= The tree <tree>

#fstree(```
README          the one-screen summary (src/fs-help.txt)                   @other-files
index           a line per pane: serial kind dirty name column               @rules
status          pid, version, panes                                          @other-files
look            write a line: a #word("Look") at the pane with the keyboard; read: the serials touched  @look-and-exec
exec            write a line: an #word("Exec") there; read: the serials touched  @look-and-exec
pager           write a directory: its one +Pager, made or emptied; read: its serial  @pardes-stdin
log             the event log; write follow to wait for more                 @log
screen          the rendered screen as JSON                                  @on-screen
listeners       dial addresses                                               @other-files
focus           the serial of the pane with the keyboard; write one to move it  @columns-and-tags
ctl             settings and session builtins                                @root-ctl
commands        every builtin, one a line                                    @other-files
recent          files opened lately: open|closed <path>                      @other-files
layout          a line per column, then active <serial>                      @columns-and-tags
tag             the workspace tag                                            @columns-and-tags
tagexec         a word clicked in the workspace tag                          @columns-and-tags
col/            the columns                                                  @columns-and-tags
  <n>/          column <n>; rmdir closes it when empty                       @columns-and-tags
    tag         its tag
    ctl         its builtins (Delcol Joincol New Tty)
    exec        a word clicked in its tag
pane/           the panes                                                    @panes
  new           open it to make a pane; read answers the serial              @panes
  <n>/          pane <n>; rmdir closes it                                    @panes
    name        the file name; write to rename                               @panes
    body        the text; a write appends, > replaces                        @panes
    tag         its path, then its words; a write replaces the words         @panes
    ctl         builtins and ctl words, a line each                          @panes
    addr        the address data and xdata work on                           @addresses
    dot         the selection, as an address                                 @addresses
    limit       where a forward search stops                                 @addresses
    data        the text at addr; a write replaces it                        @addresses
    xdata       the text in addr's range only                                @addresses
    sel         the selected text; a write replaces it                       @panes
    dirty       1 while the text differs from its file                       @flags
    mark        0 or 1; a write marks an undo point                          @flags
    scroll      0 or 1; a write scrolls the pane                             @flags
    errors      write-only: text for the directory's +Errors                 @panes
    event       the pane's clicks and keys, as acme's event file             @event
    look        a #word("Look") at this pane                                 @look-and-exec
    exec        an #word("Exec") at this pane                                @look-and-exec
    tagexec     a word clicked in this pane's tag                            @look-and-exec
    pty/        terminals only                                               @pty
      ctl       winsize, sig, exec                                           @pty
      status    cols rows busy                                               @pty
      data      the live stream: bytes in as typed, output out               @pty
      run       write a command line; read: exit N, then its output          @pty
os/             the host filesystem                                          @mounts
src/            the editor's sources (only with -Dembed-sources=true)        @mounts
```)

Panes and columns are named by serials the editor gives: stable while they
live, never reused (so they can have gaps). 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` (a shell), `cmd` (a command's pane, no shell to type into), `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 `\\`, and any other control byte, DEL, a C1 control or a byte that
is not UTF-8 `\xNN`, so a name decodes to the bytes it is.

= 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`; a write that succeeds
logs none. A refused write says why in the log's last `err` record, found
with `grep '^err' log | tail -1`: after a refused #word("Del"), #word("Exit") or #word("Restore") the
newest line may be `new N .../+Unsaved`. 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; a bad setting value is quoted, the value
itself (`bad value in control message; takes on, off "maybe"`); a ctl
refusal quotes the offending word alone (`wrong #args in control message "Newcol"`). Through 9ns the kernel sees an errno 9ns reads
from those words (cloud9's `fs.enameErrno`): the first of these that the
words hold, case aside, wins, and anything else is EIO (`Modified`, an
#word("Edit") command pardes leaves out):

#pairs(
  [`control message`], [EINVAL],
  [`interrupt`], [EINTR],
  [`shut down`], [EIO],
  [`not exist`, `not found`, `no such`], [ENOENT],
  [`exists`], [EEXIST],
  [`not empty`], [ENOTEMPTY],
  [`not a dir`], [ENOTDIR],
  [`is a dir`], [EISDIR],
  [`permission`, `denied`], [EACCES],
  [`read-only`, `read only`, `readonly`], [EROFS],
  [`no space`], [ENOSPC],
  [`not allowed`, `not permitted`, `cannot`], [EPERM],
  [`fid`], [EBADF],
  [`bad offset`, `invalid`, `bad `], [EINVAL],
  [`busy`, `in use`], [EBUSY],
  [`too long`], [ENAMETOOLONG],
  [`too many open files`], [EMFILE],
  [`not supported`, `unsupported`], [EOPNOTSUPP],
  [acme's address and argument words: `no match for regexp`, `no previous regular expression`, `address out of range`, `addresses out of order`, `past end of body`, `written to addr failed`, `not locked by this open`, `too small for the panes`, `owns the size`, `no question asked`, `answer takes`], [EINVAL],
)

The `err` record has the words; a shell sees only the errno.

Not failures: a #word("Look") that finds nothing (it answers nothing and logs one
`err`), an #word("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 at its newline; the last one, unended, runs
after its open is closed, the close answered first, so its failure is only
its `err` in the log: end a write with a newline when its result matters
(#doc("building", section: "writes-through-a-mount")). `> exec` (a
truncating open) is fine through a mount. An #word("Edit") whose `{` or `a`/`c`/`i` text is still
open waits for the next write on that open. A line or #word("Edit") block over
1 MiB is refused once (EINVAL), and the rest of it, through its newline,
is dropped.

*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")). A read answers the panes touched by this open's last
write. An open that never wrote reads the session's last answer, from
whichever client wrote it, so read on the open you wrote. 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/pane/$n/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.

*Open records.* A session holds 64 open records, shared by every client.
An open that keeps state takes one: such a snapshot, a #file("run"),
#file("event") or #file("pty/data") open, a write open of #file("data"),
#file("xdata"), #file("sel") or a text pane's #file("body"), and every
write open of a command file (#file("look"), #file("exec"),
#file("tagexec"), a #file("ctl")). A plain read of a pane's text takes
none. Past 64 an open is refused `too many open files`, which a mount
reports as EMFILE. Close what you open: a shell's `exec 3>$m/exec` holds
its record until fd 3 is closed.

*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. A stream and a
view generated by each read stat 0, as acme's do: #file("log"), #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 #word("Look") 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 #word("Look") too); only its
newline and `\r` go. An #file("exec") line's blanks at either end are
trimmed. The guide says what #word("Look") opens and where it finds 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. A bare `:N` or `:N:M` (or any `:addr`) addresses the pane the #word("Look") came from.
  `@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 fails the write and is named:
  `look: <path>: permission denied`. A zero column is refused, `file:0:0`
  included: columns count from 1.
- A whole line of a diff pane written to #file("look") is the #word("Look") on its first column. The line is matched in the
  pane from its cursor row on, wrapping, and the first match wins.
- On a PDF pane `:P:H` is hit H of the pane's search on page P, as
  `file.pdf:P:H` is; a hit not there, or any H with no search active, is a
  miss, `has no search hit H on page P`. Only a `+PdfSections` row's
  second number is a section,
  `file.pdf:PAGE:SECTION`, and only such rows are checked against the
  outline: a section not there, or not on that page, is a miss.
- A plain word selects its next place after dot, wrapping. #word("LookWord") `list`
  on the root ctl lists a row per line holding it in a `+Search` pane
  instead; the next word given to #word("Look") in that directory refills it. 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 `./x` or `../x` that is not there is such a miss too
(`look: ./x: no such file`). A #word("Look") whose address fails says so and leaves open no file
it opened. A #word("Look") of `file:12:` reads as `file:12`: a trailing colon is
dropped, as a click leaves it off. A path too long to repeat whole gives up
its middle to `…`.

A line written to #file("exec") is #word("Exec"):

- 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 (#word("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`; #word("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, and fails when there is nothing to rename. One that reaches other
  files applies nothing: it opens a `+Search` preview of the edits, which
  is the pane the write answers, and says how many it lists. So does one
  the server answers in this file alone while another open file of its
  language still holds the old name, a row per such file (zls renaming at
  a declaration; from a use it reaches every file);
  #word("Diagnostics") and #word("Symbols") list the file's in `+Search`;
  #word("Lspinfo") fills `+Lsp`, the pane its write answers, with which
  server serves the file and its state (`not started yet` until its first
  query starts it, `<server> failed recently; retry in Ns` while it backs
  off);
  #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 (#word("Fonts")) on another frontend.
- Anything else is a command line (a typo ends `exit 127`), 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, and while that open stays open the pane is its
own: another client's command in the same directory gets a pane of its
own. 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, leaving out its `exit N`:
  #cmd("awk '/^% /{out=\"\"; next} /^exit [0-9?]+/{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 pane with the keyboard and log as
that pane's (with no pane at all, in the session's directory: a #word("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; #word("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").

== Paths and mounts <mounts>

#word("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 #word("Mount") `peer <dial>` mounts a dial: `unix!/path`,
`/path`, `tcp!<numeric-ip>!<port>`, a session name, or with `-Dquic`
`quic!…`; a missing socket is ENOENT, `no such socket`;
#word("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 any other `x!y` `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
#word("Save") still uses. Mounts are dumped.

A session may open its own tree through a mount (a #word("Look") of `$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 (#word("Theme") `orchard`, #word("Verbose") `on`, #word("Placement") `acme`, #word("DumpDir") `<dir>`,
#word("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"), #word("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 (in the directory of the pane with the keyboard,
  or the session's when that directory is not on disk). #word("Restore"), #word("Del"),
  #word("Delcol") and a pane's `get` refuse the same way with their own
  word, except that #word("Delcol") opens no `+Unsaved` pane: it names the
  panes only in its `unsaved` records and its notice.
- #word("Dump") writes `pardes-<date>-<time>.zon` (UTC) in #word("DumpDir"),
  logs `dump <path>`, and adds a #word("Restore") `<path>` word naming it to the
  workspace tag (the last dump's, replacing an earlier one's).
  #word("Restore") `[path]` replaces every pane (bare: the last dump this session
  wrote). The #word("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. A dump keeps an unsaved pane's
  text and the hash of the file it was read against; a clean file pane's
  text is read from disk at #word("Restore"), so a file changed since comes back as
  it is now. An unsaved one whose file changed comes back with its own
  text, marked changed on disk, and its first #word("Save") asks before
  overwriting. A PDF's fit and tint are kept too. A #word("Restore") that fails says why in
  the parser's words, a ZON error with its line (`line 3: expected ','`),
  so the errno a mount gives depends on them (usually EIO): read the `err`
  record for why. A #word("ThemeFile") the dump names that fails to load changes
  nothing.
- #word("Kill") `[word...]` (above), #word("Mount") `name dial`, #word("Unmount") `name`, #word("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 bad
  `size` is refused quoting the value it got (`"5 2"`).

A word that takes its argument after a `+` (#word("Tty+bash")) works on a ctl as
in a tag. 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"), #word("Crt"), #word("Bloom"),
#word("Vignette"), #word("Grain"), #word("Lift"), #word("Motion"),
#word("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(
  [#word("Theme") `<name>`], [`orchard`; names as #word("Themes") lists them, #word("NextColor") steps to the next in that list (#doc("themes"))],
  [#word("ThemeFile") `<path>`], [a `.zon` theme, relative to the config directory; reloads live when saved],
  [#word("FocusTint")], [on: tint the focused pane's and column's tags],
  [#word("SyntaxBold")], [off: bold syntax keywords],
  [#word("Verbose")], [on: a builtin announces its name on the message row],
  [#word("MessageAnimation")], [on: messages ease in and dissolve],
  [#word("MessageLinger"), #word("MessageFall"), #word("MessageDissolve")], [800, 180, 150 milliseconds, at most 60000],
  [#word("PagerColor")], [on: the next paged text keeps its colours],
  [#word("Pager") `pardes|off`], [`pardes`: what a terminal's commands page through, `pardes -`; `off` leaves `PAGER`, `GIT_PAGER` and `SYSTEMD_PAGER` as the environment has them (#doc("fs", section: "pardes-stdin")). It applies to terminals started after it],
  [#word("Placement") `acme|pardes`], [`acme`: where new panes go (#doc("fs", section: "placement"))],
  [#word("BootShell") `keep|replace`], [`keep`; `replace` closes the untouched lone shell a dragged document lands beside],
  [#word("LookWord") `search|list`], [`search`: a word given to #word("Look") selects its next place, or lists all in `+Search`],
  [#word("DirLook") `pane|terminal`], [`pane`: #word("Look") on a directory opens a pane listing it, as acme's directory window (#doc("reference", section: "from-acme")); `terminal` types `ls` into a terminal idle there, else opens one],
  [#word("TermImages") `real|petscii`], [`real`: a new terminal draws its program's kitty graphics (yazi's previews) as pixels where the shell can; #word("Petscii") flips one terminal; a tty under a terminal without kitty graphics draws them as glyph art either way],
  [#word("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 searched for in the usual bin directories, not `$PATH`; bare #word("Shell"): `$SHELL` if it is executable, else `/bin/sh`],
  [#word("DumpDir") `<dir>`], [`$XDG_DATA_HOME/pardes`, else `~/.local/share/pardes`: where #word("Dump") writes, an absolute or `~` path to a directory that may be written (made if missing); a relative one, or one under a directory that may not be written, is refused; bare returns to the default],
  [#word("TreeContext")], [off: sticky declaration headers in a source pane (per pane, dumped)],
  [#word("TreeContextTagStyle")], [on: draw those headers in the tagline style],
  [#word("LocationsConfig") `...`], [the layout of #word("Grep"), search and language-server results (below)],
  [#word("Wrap"), #word("Colors"), #word("Tagbottom"), #word("Debug")], [toggles],
  [#word("Font") `<name>[:<size>]`, #word("Fonts")], [SDL and macOS only; size 8-72 (pixels in SDL, points on macOS)],
  [#word("TaglineSize") `<1-100>`], [82: tagline face, percent; SDL and macOS],
  [#word("WindowOpacity") `<0-100>`], [100; SDL only: everything but text and the cursor],
  [#word("Ligatures")], [on; SDL only, macOS draws CoreText's own],
  [#word("Pet") `cat|frog|off`], [off; SDL only: a sprite in the workspace tag's blank space],
)

#word("LocationsConfig") with no argument prints the current settings as a line
that can be run again; with fields it changes only those:
#word("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 #word("Dump") and #word("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: #word("Crt"), #word("Bloom"), #word("Vignette"), #word("Grain").
#word("Shader") `<file.glsl>` adds a Shadertoy file written for ghostty to the
chain (#word("Shader") `off` removes it; it recompiles when saved);
#word("ShaderAnimation") `off|on|always` says when the chain animates by itself. The
focused pane can stand off the page: #word("Lift") `shadow|rim|auto|off`,
#word("InactiveDim") `<percent>`, #word("Motion") `off|crisp|smooth|bouncy|playful` (default
`smooth`), #word("SelectionGlow"), #word("HoverGlow"), #word("Occlusion"),
#word("Parallax"), #word("JumpTrail"), #word("ChipShadow"),
#word("ThumbFlash"), #word("CursorBlink"), #word("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.
#word("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 #word("Look") would take, 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: "command-panes")).
#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("fs", section: "placement")): 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 #word("Look"), #word("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
an empty name, a second line and a NUL (`bad character in file name: an
empty name`, `...: a NUL`). Up to
255 bytes a component. A name that can never be valid (a terminal's, a
component over 255 bytes, a path too long) is refused by the write that
holds it. 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, each line of the page's text a
line of the body, read-only: it is no text of the
pane's, so #file("addr"), #file("data"), #file("dot") and a `file:<addr>` #word("Look") do not address it (a PDF's `:<n>` is its page); images and PDFs take
no write (`this pane has no text`).

*Image panes.* An image pane's tag reads `img petscii:on|off
palette:commodore|terminal ascii:on|off <path>`. `petscii` says what is
drawn: glyph art (`on`) or pixels (`off`). With no graphics it is always
`on`; #word("Petscii") `on|off` chooses for when graphics are there. `palette` is which 16
colours the glyph art uses, the C64's (`commodore`) or the terminal
theme's own 16 (#word("Palette") toggles it); `ascii` is whether the
printable ASCII bitmaps join the glyph set the matcher picks from
(#word("Ascii") toggles it). #word("Palette") and ascii only change the glyph art.
#word("Petscii") and #word("Ascii") (`on|off`) and #word("Palette")
(`commodore|terminal`) take the value their tag word shows and set it, as
a PDF's #word("PdfFit") (`width|height`) and #word("PdfTint")
(`disabled|filtered|full`) take the value its ctl read shows; bare, they
step to the next.
A terminal pane showing kitty graphics shows `petscii:on|off` after its
directory in its tag, saying the same, and #word("Petscii") chooses there
too. A terminal with no images shows the word only while #word("Petscii")
is on.

*#file("sel")* reads the selected text; a write replaces it and leaves
the written text selected, and one open's writes run on from the last.
*#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 (1 for a directory pane, else 0), dirty, width in cells, font, tab width, undo available,
redo available, then `current`/`notcurrent`, a REPL's id if bound,
`collapsed` when it is, and for a PDF `fit:width|height
tint:disabled|filtered|full`. It
takes:

- the pane builtins: #word("Del") (from the keyboard between two panes it
  asks which takes the rows; a click, a ctl write or an `init` line never
  asks and gives them to the pane above; #word("Del") `k`/#word("Del") `j`, or
  #word("DelAbove")/#word("DelBelow"), give its rows to the pane above or
  below), #word("Save") `[path]` (bare, on a `+`-named pane that is no output,
  such as `+Tutor`, it asks for a path; making the directories the file goes in first,
  whichever name it writes), #word("Collapse"), #word("Undo")/#word("Redo")
  (256 steps each), #word("Find") `pat`, #word("Edit") `...`, #word("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)`). It is an undo step, so #word("Undo") brings back what was there: the sure way back to the file on disk.
- `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 message row,
  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`
  (#word("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: `invalid address: this pane has no text to address` (#file("addr"), #file("dot")
and #file("limit") on a terminal, an image or a PDF), `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 #word("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, #word("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 #word("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 #word("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>

#word("Edit") `<sam commands>` on a pane's #file("ctl") or #file("exec") (or the
root's, at the pane with the keyboard) runs acme's #word("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 #word("Look") and #word("Exec") 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. Reads may be shorter
than a record: a record arrives in pieces across reads, so a shell's
`read` loop works. 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. A reader whose pane has closed reads EOF.

- 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` #word("Exec")/#word("Look") 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 (#word("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. A record written back that runs a builtin which fails
(a refused #word("Del")) fails the write, EIO with its `err`, as an
#file("exec") write would. `I`, `D`, `i`, `d` are refused; the origin must be `E`, `F`, `K` or `M`,
and a number larger than a range can hold is refused `bad number`,
EINVAL. 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>

#word("Repl") `python` on a terminal's #file("ctl") (or in its tag) binds it as
that language's REPL: #word("Exec") (or #key("Tab")) on a `.py` body
then types the text into the REPL instead of running it; builtin words,
tag words, #word("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`. #word("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. A terminal whose program was
  killed on the alternate screen is at its prompt once the shell draws one
  there: busy is 0, and the next #file("pty/run") reads its output whole.
- #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 a write (two lines in one write are refused
  whole; through a mount a shell's `printf` of two lines arrives as two
  writes, so the first runs and the second is refused EINVAL), 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, tabs expanded to blanks, `\r`
  progress collapsed, trailing blanks dropped; a paging command's text
  goes to a `+Pager` pane through the terminal's `pardes -` pager; the last 64 KiB, `exit N cut M` when M bytes were left
  out, bare `cut` only when the start of that output scrolled out of the
  scrollback; after a `clear`, the answer is what the command printed from
  the clear onward). Or: `busy: <program> is running` (bare `busy` when text is typed at the prompt, `busy alternate screen` on the alternate screen with no prompt drawn),
  `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 64 KiB ring outran (it keeps only the newest records) reads `lost N` first. A #word("Restore") hangs the follower up: dial
again and read from `restore <path>`. A follower's read held when the
#word("Restore") comes takes every queued record that fits first, so records written
just before the #word("Restore") are not lost.

#pairs(
  [`new <serial> <name>`, `del`, `rename`, `save`], [a pane made, closed, renamed (a terminal's too, as its shell changes directory), saved (#word("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 #word("Exit"), #word("Restore"), #word("Del") or #word("Delcol") refused over, before the `err`],
  [`dump <path>`, `restore <path>`], [a #word("Dump") written; a #word("Restore"), after its panes' `new`s],
  [`restored <old> <new>`, `restoredcol <old> <new>`], [serial maps after a #word("Restore")],
)

The serial is the pane the line ran at (the pane with the keyboard 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`: #word("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 #word("Look") of a row reopens it
  there.
- #file("/status"): `pid`, `version`, `panes`.
- #file("/pager"): write a directory, `~` expanded and resolved (an empty
  line is the session's; one not there is refused ENOENT, a relative one
  EINVAL, one you may not write `permission denied`, a regular file `not
  a directory`, ENOTDIR), one line; what follows that line, on the same open, is the text to
  page, escapes and all. A read of that open answers the serial of the
  directory's one `+Pager`, made or emptied for it, once the text is in. This is what
  `pardes -` uses; from a directory you may not write, it pages into the
  session's `+Pager`.
- #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`; #word("EffectCode") `<effect>` lists an effect's files
  under `/virtual`.

= The pardes command <command-line>

In a pane's shell (`PARDES_PID`, `PARDES_9P` and `PARDES_PANE` set),
`pardes FILE` writes FILE to that pane's #file("look") and returns at once;
a FILE not there yet opens an empty pane that #word("Save") creates, making
its directories. What the session refuses (a bad name; a name under a
directory you may not search or write, `permission denied`) is printed and
the exit is 1. `--` ends the options (`pardes -- -name`).
`pardes --wait FILE` (`-w`) returns 0 when the pane showing FILE is
deleted, 1 when the session goes away (against a dead `$PARDES_9P` it
says the session is gone); with
`PARDES_9P` set but no pane of its own it opens FILE in that session and
waits there. Bare `pardes` in a pane refuses and names `--nested`, which
starts a separate session whose shells do not forward to it.

= Placement <placement>

#word("Placement") `acme` (the default) puts a new pane in the column whose tag
asked, else the active column (last typed or left-clicked in,
dropped into, its tag given the keyboard, or given the last new pane),
never a new column. An empty column it takes whole; #word("New") and
#file("pane/new") take the bottom half of the column's last pane; a pane
opened from a pane's text (#word("Look"), #word("Tty"), #key("Alt-n")) goes under
the pane with the most blank rows, or halves the biggest. #word("New") in a
pane's tag names its `+New` in that pane's directory and column; from a
column tag, in that column and the session's directory. A command pane
goes to the last column, or the column whose tag ran it, under either
placement. #word("Placement") `pardes` fills an empty column whose tag asked or has
the keyboard, puts a scratch or a shell under the pane that asked, and a
document beside the last one read (or in a column of its own on a wide
screen). No pane is made shorter than its tag and two rows; with no room
the pane is refused.

A command pane runs in the directory it started in, whatever its command
does with `cd`, and a relative #word("Look") in it resolves there. The next command
for that directory reuses a finished one (from a column tag, only one in
that column); one still running, one a background job still prints to, or
one a #file("exec") open still holds is never reused.

= Find and Grep <find-grep>

#word("Find") matches file names, #word("Grep") the text of lines, both literally and
ignoring ASCII case. #word("Grep") reads at most 256 KiB of a file and stops at 512
hits, #word("Find") at 512 names. The walk stops at 20000 files or 100000 entries,
16 deep, and passes over `.git`, `.jj`, `target`, `node_modules`,
`.venv`, `__pycache__`, `.zig-cache` and `zig-out`. A cap hit, or a
directory it could not open, is said at the end: `cut at 512 hits`, `N
files read only in part (first 256 KiB)`, `walk cut at N entries`, `N
directories skipped: permission denied`. A search that finds nothing and
skipped nothing fails, `Grep: text not found`, and leaves the `+Search` as
it was.

= On screen <on-screen>

- *Esc at a prompt.* pardes knows a shell prompt with nothing typed on it
  from the OSC 133 marks it injects into bash and fish (marks a shell sends
  itself do not count); for other shells, from no program holding the
  terminal, typed text or not.
- *A program's mouse.* A program that tracks the mouse (htop, vim with
  `mouse=a`) gets the left button's clicks and drags and the wheel over its
  grid; Shift-#btn("B1") selects and Shift-wheel scrolls pardes's
  scrollback, while Shift-#btn("B2") and Shift-#btn("B3") go to the program.
  A full-screen program that does not track the mouse gets the wheel as
  arrow keys. Tags, grips and gutters stay pardes's.
- *Diffs.* A `---` line opens the old file unless the `+++` under it names
  another; a removed line's `-` goes to the line now standing where it was.
  git's `a/` `b/` (and `c/ i/ w/ o/`) prefixes are dropped; a
  `--no-prefix` diff's paths are kept.
- *Sessions.* Bare `--detach` names the session after its pid; bare
  `--attach` needs exactly one session; a second `--detach=NAME` while NAME
  runs says so and exits 1. Frontends share the smallest common size. With
  none attached, `size C R` sets the screen, messages clear by the clock,
  and a pty reports its size in pixels at the last frontend's cell size
  (8×16 before any), as CSI 14, 16 and 18 t do.
- *Config.* The startup file is `$XDG_CONFIG_HOME/pardes/init` when that is
  absolute, else `~/.config/pardes/init` (macOS:
  `~/Library/Application Support/pardes/init`). A line that fails is a
  notice and `err - init file line N: why`, and the rest still run; a
  setting only the other frontend has (#word("Font") in a terminal) is skipped;
  text that is no builtin is not run; saving the init file in a pane
  applies its settings again. A dump keeps panes, columns, tags,
  selections, the theme and changed settings, but not a picture or a PDF
  that is on disk (#word("Restore") reads it from there); a terminal
  comes back with its last MiB of output and a new shell in its old
  directory; undo history and REPL bindings are not kept. A crash appends
  two lines (build, time, platform, pid; the panic message) to `crashes`
  beside `init`.

= pardes - <pardes-stdin>

`pardes -` reads stdin to its end and shows it in the directory's one
`+Pager` pane, made or emptied for it and left clean, opening at its top;
the next paged text refills it. The session parses the text with
ghostty-vt: SGR colours become the `+Pager`'s own display, never part of
its body or any read (#word("PagerColor") `off` pages plain), and every other
escape is dropped; a carriage return keeps a progress line's last state,
and CRLF becomes LF. Empty stdin shows nothing. Inside a pardes pane it returns at once; a text the session cannot
take is printed to stderr with why, and the exit is 1. From a directory
whose name holds a newline or control byte, the pane is the session
directory's. Outside a pardes pane it starts a new editor whose first pane
the text is. Stdin up to a file's limit (256 MiB) arrives whole; past that
it is cut, with a note saying so.

With #word("Pager") `pardes`, a terminal's shell gets `PAGER`, `GIT_PAGER` and
`SYSTEMD_PAGER` set to this pardes plus ` -` (the path quoted only when it
needs it), and `SYSTEMD_PAGERSECURE=0` (it has no shell escape), each only
where your environment does not set it. `man` pages through `MANPAGER`
first, then `PAGER`; your own `MANPAGER` wins.

= Other ways in <other-clients>

A session listens on `$XDG_RUNTIME_DIR/pardes-9p-<name>.sock` (else under
`~/.local/state/pardes`), `<name>` the pid, the `--detach=NAME` or
`--9p=NAME`, and posts it in the 9P registry as
`$XDG_RUNTIME_DIR/9p/pardes/<name>`. Its pane shells get `PARDES_PID`,
`PARDES_9P` and `PARDES_PANE`. A dead session's registry entry stays
listed and answers `Input/output error`: name the session, never glob.
Under `9ns --mntgen` (which sets `NINE_MNTGEN=1`) a session is at
`$NINE_MOUNT/<service>/<name>`, pardes's `$NINE_MOUNT/pardes/<name>`, and
every pane pardes spawns gets it as `$PARDES_MOUNT`. Under `9ns --unix SOCK
-- cmd` the session is `$NINE_MOUNT` itself, and `$PARDES_MOUNT` is unset.
plan9port's `9p write` opens with OTRUNC, so on #file("body") it replaces
the whole text. To append, use `>>` through a mount. Through a FUSE mount
bash's `read -t` cannot time out: wrap a follow loop in `timeout`. The
scripting chapter's rule (a mount, else `9p`) covers most uses. Two more:

- #word("Tty9p") (#key("SPC n 9")) opens a terminal with the session
  kernel-mounted (Linux v9fs): it asks for your sudo password in the pane,
  mounts the socket in a private mount namespace and starts your shell as
  you, with `PARDES_MOUNT` naming the mount. Only that shell sees it, and
  each takes one of the session's 16 connections. It needs the `9p` and
  `9pnet_fd` kernel modules and the `pardes-v9fs` helper the build installs
  beside `pardes`.
- `test/ninep.py` is a Python 9P client that exists only in the source tree
  (it is not installed). It is for when nothing is mounted and `9p` is not
  there, or when a fid must stay open (#file("event"), #file("pty/data"), a
  followed #file("log")). Its paths are the served root:

```python
import sys
from ninep import Client          # PYTHONPATH=test
with Client(sys.argv[1]) as c:    # a socket path, or (ip, port)
    print(c.read('/index').decode(), end='')
    n = int(c.read('/pane/new'))
    fid = c.open(f'/pane/{n}/event', 0)      # hold event: clicks come here
    c.write(f'/pane/{n}/exec', b'Msg hi\n')
    print(c.read_fid(fid, 0, 4096))          # b'FX0 0 1 6 Msg hi\n'
    c.close(fid)
    c.remove(f'/pane/{n}')
```

`client.screen()` returns the parsed #file("/screen").

= Coming from acme <from-acme>

#pairs(
  [`$NAMESPACE/acme`], [not posted there: a session is `$XDG_RUNTIME_DIR/9p/pardes/<name>`; reach it with `9p -a "unix!$PARDES_9P"` or a `9ns --mntgen` mount (#doc("fs", section: "other-clients"))],
  [`N/`], [#file("pane/N/"), N the pane's serial],
  [`new/ctl`], [read #file("pane/new"): it makes a pane and answers its serial; `rmdir pane/N` closes it],
  [`index`], [#file("index") lines are serial, kind, dirty, name, column (acme: id, tag and body lengths, isdir, dirty, tag)],
  [rune offsets], [bytes everywhere, event offsets and counts included; addresses snap to rune boundaries],
  [`addr` reset on first open], [#file("addr") is the pane's; no open resets it (write `0`)],
  [OTRUNC on `data` ignored], [`: > data` deletes the addressed text; OTRUNC on #file("body") (`9p write`) replaces it all],
  [event flags 1, 2, 4, 8], [1, 4 and 8; never 2, so no expansion record follows],
  [`Get`], [`get` on the pane's #file("ctl"); `Get` itself only in a directory pane],
  [`Put`, `Delete`], [#word("Save"); a #word("Del") that does not ask],
  [`Snarf`, `Cut`, `Paste`], [refused: the chords, #key("y"), #key("p")],
  [acme's other builtins], [refused: `Putall`, `Zerox`, `Sort`, `Load`, `ID`, `Send`, `Tab`, `Indent`, `Local`, `Incl`, `Abort`],
  [Edit], [no `< | >`, `X`, `Y`, `b B D e r w f`, no `\1`-`\9`; an alternation takes the first branch that matches, not the longest (#doc("fs", section: "edit"))],
  [`win`], [#word("Tty") is a VT terminal pane; #file("pty/run") runs a line at its prompt and answers `exit N` and the output; with #word("Repl") bound, #word("Exec") on a source pane types the text into it],
  [a directory window], [as acme's: a text pane named `dir/` (#file("index") kind `text`, #file("ctl") isdir 1), its entries in columns sorted bytewise, dotfiles shown, a directory's marked `/`; blanks pad the columns where acme's tabs do; a #word("Look") at an entry is from that directory, `Get` or a look at it again reads it again, and an edit is kept until then rather than lost to a resize; never dirty, #word("Save") refuses it; #word("DirLook") `terminal` types `ls` into a terminal there instead],
  [the plumber], [none: #word("Look") on an `http://` or `https://` URL runs `xdg-open` (`open` on macOS); files, addresses and directories follow built-in rules],
  [#key("Esc") selects what was typed], [#key("Esc") leaves insert mode, or goes back a pane (#doc("tags", section: "modes"))],
)

= Limits <limits>

#pairs(
  [panes], [64],
  [columns], [16, 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],
  [open records], [64 a session, every client's together],
  [held reads a connection], [128],
  [named mounts], [8],
  [command line], [1024 bytes; a held line or #word("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],
)