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
|
// 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
/pager write a directory: its one +Pager, made or emptied; read: its serial
/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` (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 Del, Exit or 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
`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 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 or 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 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. A bare `:N` or `:N:M` (or any `:addr`) addresses the pane looked 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 look a
#btn("B3") click on its first column makes. 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. `LookWord list`
on the root ctl lists a row per line holding it in a `+Search` pane
instead; the next word looked at 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 look whose address fails says so and leaves open no file
it opened. A 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 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, 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;
#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 (`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, 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 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!host!port` or `quic!host!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 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
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 (in the active pane's directory,
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 `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. 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 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 Save asks before
overwriting. A PDF's fit and tint are kept too. A 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 ThemeFile the dump names that fails to load changes
nothing.
- `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 bad
`size` is refused quoting the value it got (`"5 2"`).
A word that takes its argument after a `+` (`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"), `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],
[`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],
[`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`],
[`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],
[`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"): `$SHELL` if it is executable, else `/bin/sh`],
[`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],
[`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
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. 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>`
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` is glyph art
instead of pixels (#word("Petscii") toggles it); `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). 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` or `petscii:off`
after its directory in its tag; #word("Petscii") toggles it there too: on
draws the program's images as glyph art instead of host pixels, off goes
back to pixels. A terminal with no images shows the word only while it 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 (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") (`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)`). It is an undo step, so 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 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: `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 `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. 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` 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. 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>
`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. 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 ring outran reads `lost N` first. A Restore hangs the follower up: dial
again and read from `restore <path>`. A follower's read held when the
Restore comes takes every queued record that fits first, so records written
just before the 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 (`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("/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); a read on
the same open answers the serial of that directory's one `+Pager`, made
or emptied for it. It takes one directory a write. 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`; `EffectCode <effect>` lists an effect's files
under `/virtual`.
= 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; the next paged text
refills it. Terminal escapes go (colour, hyperlinks, a man page's
overstrike), a carriage return keeps a progress line's last state, CRLF
becomes LF, and NUL, BEL, SO and SI are dropped. 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. It writes stdin through one open of the body, so any size up
to a file's limit (256 MiB) arrives whole; past that it is cut, with a note
saying so.
With `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>
The scripting chapter's rule (a mount, else plan9port's `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").
= 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],
[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 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],
)
|