Reference
Every native session is a virtual filesystem, served as 9P2000 (not .u, not .L) and modelled on acme’s (→ Reference: Coming from acme): panes, columns, tags and the session are files. Paths here are the served ones (/pane/2/body); through a mount they sit under the session’s directory ($m; → Scripting: Finding the session says how to find it). The served /README is a one-screen summary of this page. Two builtins: Look (right-click): open what the text names, or else find it; Exec (middle-click): run the builtin it names, or else run it as a shell line. The files look and exec take a line each, as those clicks would.
The 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 write a line: aLookat the pane with the keyboard; read: the serials touched ├── exec write a line: anExecthere; 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 the workspace tag ├── tagexec a word clicked in the workspace tag ├── col/ the columns │ └── <n>/ column <n>; rmdir closes it when empty │ ├── tag its tag │ ├── ctl its builtins (Delcol Joincol New Tty) │ └── exec a word clicked in its tag ├── pane/ the panes │ ├── new open it to make a pane; read answers the serial │ └── <n>/ pane <n>; rmdir closes it │ ├── name the file name; write to rename │ ├── body the text; a write appends, > replaces │ ├── tag its path, then its words; a write replaces the words │ ├── ctl builtins and ctl words, a line each │ ├── addr the address data and xdata work on │ ├── dot the selection, as an address │ ├── limit where a forward search stops │ ├── data the text at addr; a write replaces it │ ├── xdata the text in addr's range only │ ├── sel the selected text; a write replaces it │ ├── dirty 1 while the text differs from its file │ ├── mark 0 or 1; a write marks an undo point │ ├── scroll 0 or 1; a write scrolls the pane │ ├── errors write-only: text for the directory's +Errors │ ├── event the pane's clicks and keys, as acme's event file │ ├── look aLookat this pane │ ├── exec anExecat this pane │ ├── tagexec a word clicked in this pane's tag │ └── pty/ terminals only │ ├── ctl winsize, sig, exec │ ├── status cols rows busy │ ├── data the live stream: bytes in as typed, output out │ └── run write a command line; read: exit N, then its output ├── 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 (so they can have gaps). Nothing is created by a listing, stat, walk or read: only an open of /pane/new makes a pane (so ls -l and find are safe), only rmdir of /pane/<n> or an empty /col/<n> removes. Tcreate is refused everywhere.
/index rows are serial kind dirty name column, kind text, term (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 /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
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 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, a sam command pardes’s Edit leaves out, B <cmd):
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, Undo with nothing to undo (a msg), and a command pane’s command, which ends in its own time with an exit record.
Command lines. look, exec, tagexec, the three kinds of ctl and a column’s exec take one command a line. The whole write is checked first (a control character other than a tab fails it all, EINVAL); then lines run in order, and a failing line fails the write after the lines before it took effect, as acme’s ctl does. Blank lines are skipped. A 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 (→ Building, for contributors: Writes through a mount). > exec (a truncating open) is fine through a mount. 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 look, exec or 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 (Newcol at /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:
exec 3<>$m/pane/$n/look; echo x >&3; cat <&3; exec 3<&-Snapshots. /index, /layout, /recent, /commands, /status, /listeners, a read-only ctl, /log, /screen and a terminal’s body freeze at the open, so one read in several chunks never splices two moments; open again for now.
Open records. A session holds 64 open records, shared by every client. An open that keeps state takes one: such a snapshot, a run, event or pty/data open, a write open of data, xdata, sel or a text pane’s body, and every write open of a command file (look, exec, tagexec, a 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 log, event, pty/data, pty/run before its answer) waits in the editor and is answered when news comes. A second read on that open meanwhile fails file in use. A read the client flushed is dropped. 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 body, tag, name, ctl, sel and the range and flag files, the workspace’s tag, the root ctl, status, commands, README, the look/exec answers, focus event and 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: log, screen, data, xdata, index, layout, recent, listeners, pane/new. Read those to the end rather than trust a length (cat does). The qid version of body, data and xdata is the pane’s revision, so a stat sees an edit land. Modes are 0644/0666, 0444 read-only, 0222 write-only.
look and exec
A line written to look is 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 Look too); only its newline and \r go. An exec line’s blanks at either end are trimmed. The guide says what Look opens and where it finds a relative path (→ Guide: Look); here is what is particular to the files.
file:12selects line 12, newline included;file:12:5puts 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:Nor:N:M(or any:addr) addresses the pane theLookcame 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;~userthat user’s) here and wherever a path is typed (name,Save,ThemeFile,DumpDir,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:0included: columns count from 1. - A whole line of a diff pane written to
lookis theLookon 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:His hit H of the pane’s search on page P, asfile.pdf:P:His; a hit not there, or any H with no search active, is a miss,has no search hit H on page P. Only a+PdfSectionsrow’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.
LookWordliston the root ctl lists a row per line holding it in a+Searchpane instead; the next word given toLookin 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 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 exec is Exec:
- A builtin word runs (
/commandslists them). A builtin that needs its argument (Msg,Mount,Find) written bare iswrong #args in control message "Msg", and so is one that takes none written with one (Configextra). - 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
addranddot=addrfirst):Hoverfills+Hover;Renamenewrenames 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+Searchpreview 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);DiagnosticsandSymbolslist the file’s in+Search;Lspinfofills+Lsp, the pane its write answers, with which server serves the file and its state (not started yetuntil its first query starts it,<server> failed recently; retry in Nswhile it backs off);Lspwhynarrates 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 (
PutisSave,LoadisRestore,DeleteaDelthat does not ask);Get,Putall,Zerox,TabandInclare builtins of their own; the rest (Snarf,Cut,Paste,Sort,ID,Send,Indent,Local,Abort) are refused,invalid: acme's Snarf 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 (a typo ends
exit 127), at most 1024 bytes, run as the guide says (→ Guide: Where commands run and panes go): typed into a terminal idle at an empty prompt, else run in a command pane.execreads 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 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:
awk '/^% /{out=""; next} /^exit [0-9?]+/{next} {out = out $0 "\n"} END {printf "%s", out}' bodyFrom a pane whose directory is gone nothing runs: exec: <dir>: no such directory (ENOENT).
The root’s look and 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 Look opens its file, making a column as New does, and an exec runs its command there); /pane/<n>/look and exec at pane n; /tagexec and /col/<n>/exec click in the workspace’s or that column’s tag, run commands in the session’s directory, and log as -. A pane’s word (Undo, Msg, Save) is refused at /tagexec and a column’s exec: not a session control message "Undo": write it to pane/<n>/ctl.
A background job (&) outlives a command that exits on its own; its output goes on below exit N until it lets go of the pty. Kill (root ctl) stops the commands pardes started: bare, all; Kill make ls, those whose line starts with one of the words. For a command pane it signals the whole line, & jobs included; for a line typed into a shell only the foreground job (SIGTERM), and the shell decides the rest. With nothing running, 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). Kill does not reach a REPL’s code: use sig INT on its pty/ctl.
Paths and mounts
Look resolves the OS filesystem first, then the editor’s own tree. Explicit paths skip that search:
/n/os/proc/self | the host filesystem, served as /os/proc/self |
/n/self/pane/2/body, /virtual/pane/2/body | this session’s tree, /pane/2/body |
/virtual/src/pardes.zig | sources embedded with -Dembed-sources=true, /src/pardes.zig |
/n/peer/pane/2/body | a mounted session: the peer’s /pane/2/body |
--mount=peer=work or Mount peer <dial> mounts a dial: unix!/path, /path, tcp!<numeric-ip>!<port>, a session name, or with -Dquic quic!…; a missing socket is ENOENT, no such socket; Unmount peer removes it. 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. 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 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
Reading /ctl gives every setting, one a line, in the words a write takes (Theme orchard, Verbose on, Placement acme, DumpDir <dir>, Shell /bin/bash, …), so writing back what it reads changes nothing. Writes take settings and session builtins (scope = .session in src/builtins.zig), acting at the pane with the keyboard:
- A setting written bare steps to its next value (a switch flips; so do
Placement,BootShell,Crt). A value it does not take isbad value in control message; ...naming what it takes;/commandslists them. A setting the frontend cannot show is refused (Lift is GUI-only, invalid here). Newcolmakes an empty column right of the keyboard’s, halving it.Joincolfolds the keyboard’s column into the one on its right (its panes go below that column’s),Joincol: no column to the right.Exitquits. While panes hold unsaved text it refuses once (→ Guide: Unsaved panes): oneunsaved <serial> <name>record per pane, then the write fails<name>: Modified (Exit again to discard)or4 unsaved panes: Modified (Exit again to discard)(EIO), and the list stays in a+Unsavedpane (in the directory of the pane with the keyboard, or the session’s when that directory is not on disk).Restore,Del,Delcoland a pane’sgetrefuse the same way with their own word, except thatDelcolopens no+Unsavedpane: it names the panes only in itsunsavedrecords and its notice.Dumpwritespardes-<date>-<time>.zon(UTC) inDumpDir, logsdump <path>, and adds aRestore<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). TheRestorewrite is answered, then every connection is hung up: dial again, and restart a 9ns mount. The new log has anewper pane,restore <path>, thenrestored <old> <new>per pane andrestoredcol <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 atRestore, 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 firstSaveasks before overwriting. A PDF’s fit and tint are kept too. ARestorethat 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 theerrrecord for why. AThemeFilethe dump names that fails to load changes nothing.Kill[word...](above),Mountname dial,Unmountname,Themex.size C Rsizes a--detachsession no frontend is attached to (160x50 until then): from 20x6 to 4096x4096, elseinvalid 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 badsizeis 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 (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
A setting that chooses among words (on/off switches, Placement, BootShell, Crt, Bloom, Vignette, Grain, Lift, 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 (→ Guide: Config).
Theme <name> | orchard; names as Themes lists them, NextColor steps to the next in that list (→ 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 |
PagerColor | on: the next paged text keeps its colours |
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 (→ Reference: pardes -). It applies to terminals started after it |
Placement acme|pardes | acme: where new panes go (→ Reference: Placement) |
BootShell keep|replace | keep; replace closes the untouched lone shell a dragged document lands beside |
LookWord search|list | search: a word given to Look selects its next place, or lists all in +Search |
DirLook pane|terminal | pane: Look on a directory opens a pane listing it, as acme’s directory window (→ Reference: Coming from acme); terminal types ls into a terminal idle there, else opens one |
JumpScope file|all | file: Back and Forward keep to the focused pane’s file, that pane’s places first, then the file’s in another pane, and go to another file only when the file has no place left that way; from a terminal or a pane with no file they step through every place; all: every place, in the order it was jumped to |
TermImages real|petscii | real: a new terminal draws its program’s kitty graphics (yazi’s previews) as pixels where the shell can; 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 searched for in the usual bin directories, not $PATH; bare Shell: $SHELL if it is executable, else /bin/sh |
DumpDir <dir> | $XDG_DATA_HOME/pardes, else ~/.local/share/pardes: where 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 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: PanelSlide, PanelZoom, PanelDissolve, PanelAscii, PanelVertical, PanelEdges, PanelFall, PanelWave, PanelCurtain, PanelScramble, 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, JumpTrail, ChipShadow, ThumbFlash, CursorBlink, GripWidth <50-300>. Most are the GUI’s; InactiveDim works everywhere, and JumpTrail, ChipShadow and 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 Look would take, with no other effect. A terminal’s Filter keeps each foreground at least tty_filter_min_contrast (WCAG 1.5, src/config.zig) against its background.
Columns and tags
/layout has a line per column, left to right: serial index x width current|notcurrent empty|full pane-serials..., then active <serial> (the active column: where pane/new and a look put the next pane; - when none). current is the column with the keyboard now, which may differ from the active one (→ Guide: Where commands run and panes go). /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. 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 Newcol to the widest column’s exec each time, not to the root’s, which keeps halving the one just made. 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 Newcol logs its err alone and uses up no column serial.
/col/<n>/ctl takes Delcol, Joincol, New and Tty for that column; /col/<n>/exec is a click in its tag; rmdir /col/<n> closes an empty column (one with panes: ENOTEMPTY). Closing a column’s last pane leaves it empty (the keyboard goes to its tag, focus reads empty, the log says only del); closing the session’s last pane quits pardes. The log says newcol <serial> and delcol <serial>.
tag files (/tag, /col/<n>/tag, /pane/<n>/tag) read the whole tag, 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 u in the tag undoes it.
Panes
Making and closing. Opening /pane/new makes a scratch <dir>/+New (the session’s directory), and reading that open answers its serial: n=$(cat $m/pane/new). Each open makes another pane; two reads of one open answer the same serial. It goes where a New from a tag would (→ Reference: 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 Look, Exec, New or Tty alike. A 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.
name reads the file name (a terminal’s directory); a write renames the buffer (relative to the pane’s directory) and marks nothing dirty; Save then writes under the new name. A name takes any byte a file name can hold, blanks, control bytes and bytes not UTF-8 included, so what 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).
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 addr, data, 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 says what is drawn: glyph art (on) or pixels (off). With no graphics it is always on; 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 (Palette toggles it); ascii is whether the printable ASCII bitmaps join the glyph set the matcher picks from (Ascii toggles it). Palette and ascii only change the glyph art. Petscii and Ascii (on|off) and Palette (commodore|terminal) take the value their tag word shows and set it, as a PDF’s PdfFit (width|height) and 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 Petscii chooses there too. A terminal with no images shows the word only while Petscii is on.
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. errors is write-only: text appended to the directory’s +Errors pane (logged as msg records when no column has room).
focus (root): a serial written gives that pane the keyboard and makes its column 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. It takes:
tint:disabled|filtered|full
- the pane builtins:
Del(from the keyboard between two panes it asks which takes the rows; a click, a ctl write or aninitline never asks and gives them to the pane above;Delk/Delj, orDelAbove/DelBelow, give its rows to the pane above or below),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),Collapse,Undo/Redo(256 steps each),Findpat,Edit...,Tty[shell](a new terminal in its directory),Delcolfor its column, andLeft/Right/Up/Down, which give the keyboard to the pane beside it that way (Ctrlw with 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, soUndobrings 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>&-. Alockanother open holds fails at once,file in use(EBUSY): retry.answer <choice>to the question the pane asks on its message row, loggedask <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,nomenuare refused, EINVAL. These lowercase words are ctl-only: written to anexec,delis a command line.
A file changed on disk reloads by itself only when the buffer has no unsaved edits; otherwise it stays dirty, says <name> changed on disk (get reloads it, Save overwrites it), logs changed <serial>, and its next Save warns once.
Addresses and data
addr, dot and limit read the pair of byte offsets they take, so copying one onto another (cp $p/addr $p/dot) is acme’s dot=addr. A write is a pair or an address expression; a pair is checked as an address is, addresses out of order (5 2) or address out of range (past the text), never clamped. addr names where data reads (to the end of text) and the range xdata reads; dot is the selection (moving it scrolls the pane there); limit bounds the end of a forward search and reads empty until set. Truncating dot empties it, truncating limit lifts it.
- A write to
dataorxdatareplaces theaddrrange (>and>>alike);: > datadeletes it. Truncatingdatais pardes’s own (acme ignores OTRUNC there); only truncatingbodyempties the buffer. - A write leaves
addrjust past what it wrote, so a secondecho x > datainserts after the first: writeaddrbefore each replacement. A read ofdata/xdatamovesaddrpast what it read. addrbelongs to the pane, not the client, and neither an open nor a truncation resets it (acme resets on first open): eachecho /re/ > addrsearches on from the last, so a find loop advances. Write0to start at the top; searches wrap, so stop a loop when the address comes back or setlimit.- A failed address leaves no address:
addrreads empty anddata/xdatarefuse (no address: the last one written to addr failed) until a standalone address (2,#0,/re/) is written, so a missed target is never written at the old one.
Addresses are sam’s: #n, a line number, /re/, ?re?, -/re/, $, ., 0, ranges a,b and a;b, +/-. They are evaluated from the current address (the last written to addr, or just past the last data write), not from the selection. pardes adds 12:5: line 12, byte column 5 from 1, clamped to the line’s end (12:0 is address out of range: a column counts from 1); it composes (12:5,14:1). A tool’s character column matches only on an ASCII line; elsewhere use 12/name/ or #n. A row of Recent, +Search or the Jumplist spells a range 12:5-14:2 (through 14:2 inclusive) and addr takes it as such.
Offsets and counts are bytes everywhere (acme counts runes), but every address lands on a rune boundary: #n or L:C inside a rune snaps to its start, a match covers the runes it touches, and a combining mark or a CRLF’s \r is addressable alone.
Refusals: invalid address: this pane has no text to address (addr, dot and 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
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 s and S. The ceiling:
- The leftmost match wins, but among alternatives the first that matches, not sam’s longest (
/gam|gamma/findsgam). mvzr keeps no submatches, soEdit’sshas no\1-\9. - A pattern holding
\nruns 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|xis refused:an alternation anchors every branch with ^ or none). A$does not count:foo$|baris 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
Editxits own):regular expression search took too much time, gave up. What runs out is exponential backtracking (a?twenty times then twentyas) or a quadratic pattern over a very long line.
Edit
Edit <sam commands> on a pane’s ctl or exec (or the root’s, at the pane with the keyboard) runs acme’s Edit over the open text panes, from that pane’s body. Addresses are as above, and "re" is the one open file whose line matches. Commands:
x y g v c a i d s p = m t uand{ }.X/Yrun a command in each open file whose line (+. /path,'when edited) matches, or does not, in the order the panes were opened (asindexlists them):X/'/wwrites every edited file.bmakes a file current;Bopens files, every name checked before any opens;Dcloses panes (an edited one on the second asking, asDel).eloads a file over the whole text asGetfiledoes, once the rest of the Edit is done: the pane takes its name and is clean, one undo step puts both back, and unsaved edits are asked about once; the file takes no other command in that Edit.rreads a file over dot.wwrites all of it, or the address, to its file or a name; all of it to its own name leaves it clean.fnames the pane and prints its line. File names are relative to the file’s directory, and~is the home directory.<cmdreplaces dot with whatcmdwrites,|cmdpipes dot through it,>cmdsends dot to it and prints what it writes.
Each command runs in its file’s directory through Shell, off the loop, so the editor goes on. The write that ran the Edit is held and answered when they are done, its connection serving other requests meanwhile (a command may read the session through its mount); a flush of that write (an interrupted writer) kills the commands and changes nothing. Such an Edit is a write of its own: one with other lines is refused before any runs. One Edit’s commands run at a time in a session, at most 1024 of them. A command that fails (an exit status, 10 seconds, 1 MiB of output) fails the Edit, its stderr in +Errors. Each file’s 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), as does a file edited while the commands ran. An x that finds nothing succeeds silently. p, = and > print to the directory’s +Errors. Not there: B <cmd, D <cmd, the ' address, \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:
printf 'Edit ,x/foo/{\ni/</\na/>/\n}\n' > $p/ctlFlags and undo
dirty, mark and scroll read and take 0 or 1: the buffer differs from its file (a file deleted on disk counts); a write pushes an undo point (writing 1 pushes one now); a write scrolls the pane. /index’s dirty flag is dirty a +New scratch reads 1 but holds up nothing under 100 bytes.
The writes of one open of data, xdata or body are one undo step (so a multi-line printf is one), 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
Holding event open takes the pane’s Look and Exec clicks: they come to the reader as records instead of acting, as do lines written to the pane’s own look/exec (or the root’s while it has the keyboard). A record is acme’s <origin><action><q0> <q1> <flag> <n> <text>\n; read n bytes of text, which may hold newlines. 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 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:
Ea 9P write to body or tag,Fother files and the editor’s own lines,Kkeyboard,Mmouse. - action:
X/LExec/Lookin the body,x/lin the tag (offsets into the whole tag, path included),I/Dbody text inserted/deleted,i/dthe 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 0with 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 Del) fails the write, EIO with its err, as an 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 event that writes its own pane’s exec gets its command back as a record: run it through ctl instead.
REPLs
Repl python on a terminal’s ctl (or in its tag) binds it as that language’s REPL: Exec (or Tab) on a .py body then types the text into the REPL instead of running it; builtin words, tag words, Exec <text> and @`cmd` words still run. 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 ctl line show its id, python-a. Repl - unbinds, bare Repl says the binding. With several bound, the pane asks (ask <serial> repl a b). Bindings are not dumped.
A 9P exec is never sent to a REPL. A script either writes the event record MX<q0> <q1> to the .py pane’s event (sent as the click would be), or writes the REPL’s pty/data itself: multi-line code as a bracketed paste, \e[200~<code>\e[201~, then \r in a separate write once the REPL has echoed the paste; a paste of more than one line not ending in a newline needs a second \r. Line by line, a blank line ends a Python block and Python 3.14 auto-indents each line.
Terminals
Terminal panes also have pty/:
pty/data: write bytes as typed (printf 'ls\r',\x03is Ctrl-C); read the live output stream (a consuming queue shared by readers, not a replay).pty/status: one line,cols rows busy; busy is 1 while a command runs or text is typed at the prompt. 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 nextpty/runreads its output whole.pty/ctl:winsize C R(at least 2 rows, fewer refusedinvalid 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 directoryif it is gone; a shell that cannot start fails and leaves the old one running).pty/run: one line a write (two lines in one write are refused whole; through a mount a shell’sprintfof 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:The answer’s first line is the header:exec 3<>$m/pane/$n/pty/run; echo make >&3; cat <&3; exec 3<&-exit Nthen the command’s output (as the screen showed it: no colour, tabs expanded to blanks,\rprogress collapsed, trailing blanks dropped; a paging command’s text goes to a+Pagerpane through the terminal’spardes -pager; the last 64 KiB,exit N cut Mwhen M bytes were left out, barecutonly when the start of that output scrolled out of the scrollback; after aclear, the answer is what the command printed from the clear onward). Or:busy: <program> is running(barebusywhen text is typed at the prompt,busy alternate screenon 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 paneerror a command runs here, not a shell(error command done; not a shellonce it ended). A line written before a fresh terminal’s first prompt waits for it. One line per run; a second line before the answer is refused.
pty/run relies on the OSC 133 marks pardes injects into bash and fish; exec zsh or a continuation prompt never reports an end, so cancel the read. A program on the alternate screen (vim, less) leaves no output. A program holding the terminal (a REPL, less) takes no run: write to pty/data.
The 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 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.
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 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’ news |
restored <old> <new>, restoredcol <old> <new> | serial maps after a Restore |
The serial is the pane the line ran at (the pane with the keyboard for the root’s look/exec), - for the root ctl, /tagexec and column files. A record said again straight after itself is counted, err 3 addr: no match for regexp (x4); a follower sees each count as a new line. A msg is cut at 256 bytes and an err reason at 200, ending in …. Control characters become spaces.
Other files
/screen: JSONcols,rows,cursor,styles, and row-majorcellsof[grapheme, style_index]; one frame per open. Compare a cell’s style throughstyles, not the index./commands:Word [arg] root|pane|both [values] -- sentence, one a line, generated from the builtin registry (both:Edit, at the active pane from the root)./recent: up to 200 files, PDFs and images, most recent first,open <path>orclosed <path>; kept in$XDG_STATE_HOME/pardes/recent.Recentshows them in a pane, an open one at its dot now and a closed one at its last (a PDF’s is its page); aLookof a row reopens it there./status:pid,version,panes./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 writepermission denied, a regular filenot, 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
a directory+Pager, made or emptied for it, once the text is in. This is whatpardes -uses; from a directory you may not write, it pages into the session’s+Pager./listeners: the session’s dial addresses, a line each (→ Building, for contributors: Listeners)./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 truncationmtimehint is accepted and dropped./src/(and/shaderson GUI builds) with-Dembed-sources=true;EffectCode<effect>lists an effect’s files under/virtual.
The pardes command
In a pane’s shell (PARDES_PID, PARDES_9P and PARDES_PANE set), pardes FILE writes FILE to that pane’s look and returns at once; a FILE not there yet opens an empty pane that 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 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; New and pane/new take the bottom half of the column’s last pane; a pane opened from a pane’s text (Look, Tty, Altn) goes under the pane with the most blank rows, or halves the biggest. 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. 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 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 exec open still holds is never reused.
Find and Grep
Find matches file names, Grep the text of lines, both literally and ignoring ASCII case. Grep reads at most 256 KiB of a file and stops at 512 hits, 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. A search that finds nothing and skipped nothing fails,
directories skipped: permission deniedGrep: text not found, and leaves the +Search as it was.
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-left-click selects and Shift-wheel scrolls pardes’s scrollback, while Shift-middle-click and Shift-right-click 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’sa/b/(andc/ i/ w/ o/) prefixes are dropped; a--no-prefixdiff’s paths are kept. - Sessions. Bare
--detachnames the session after its pid; bare--attachneeds exactly one session; a second--detach=NAMEwhile NAME runs says so and exits 1. Frontends share the smallest common size. With none attached,size C Rsets 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/initwhen that is absolute, else~/.config/pardes/init(macOS:~/Library/Application Support/pardes/init). A line that fails is a notice anderr - init file line N: why, and the rest still run; a setting only the other frontend has (Fontin 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 (Restorereads 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) tocrashesbesideinit.
pardes -
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 (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 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
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 the session is
-- cmd$NINE_MOUNT itself, and $PARDES_MOUNT is unset. plan9port’s 9p write opens with OTRUNC, so on 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:
Tty9p(SPCn9) 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, withPARDES_MOUNTnaming the mount. Only that shell sees it, and each takes one of the session’s 16 connections. It needs the9pand9pnet_fdkernel modules and thepardes-v9fshelper the build installs besidepardes.test/ninep.pyis a Python 9P client that exists only in the source tree (it is not installed). It is for when nothing is mounted and9pis not there, or when a fid must stay open (event,pty/data, a followedlog). Its paths are the served root:
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 /screen.
Coming from acme
$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 (→ Reference: Other ways in) |
N/ | pane/N/, N the pane’s serial |
new/ctl | read pane/new: it makes a pane and answers its serial; rmdir pane/N closes it |
index | 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 | addr is the pane’s; no open resets it (write 0) |
OTRUNC on data ignored | : > data deletes the addressed text; OTRUNC on 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 reads the file again, refusing unsaved edits once; Get file loads that file into the pane, which takes its name, and one that fails changes nothing; Undo after it, or after Edit’s e, puts the old name back with the old text and its saved state; in a directory pane Get reads the directory again and Get file is refused (Look opens the file) (also get on ctl) |
Put, Delete, Load | Save a Del that does not ask; Restore |
Putall | Putall: every pane with unsaved edits to a file is saved, a refused one said; with a refusal, its write is answered once the other saves have landed |
Zerox | Zerox: a second pane on the buffer, one text, undo and unsaved state, its own scroll and cursor; an event reader on one hears no edit made through the other; the twins are kept across Dump and Restore |
Tab | Tab N, 1-16: for every pane, not per window; bare, it says the width |
Incl | Incl: for the session, not per window, seeded with /usr/include and /usr/local/include; it takes absolute (or ~) directories that exist, and Incl - alone clears the list; a name found nowhere else (stdio.h, <stdio.h>) is tried there |
Indent | always on: Enter, o and O keep the line’s indent as it is |
Snarf, Cut, Paste | refused: the chords, y, p |
Font, Send, ID | Font Repl and Exec $winid |
| acme’s other builtins | refused: Sort, Local, Abort (plan9port’s debugging word) |
| Edit | every acme command but B <cmd, D <cmd and the ' address; no \1-\9, and an alternation takes the first branch that matches, not the longest; a failing < | > command changes nothing, where acme puts in what it wrote (→ Reference: Edit) |
win | Tty is a VT terminal pane; pty/run runs a line at its prompt and answers exit N and the output; with Repl bound, Exec on a source pane types the text into it |
| a directory window | as acme’s: a text pane named dir/ (index kind text, 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 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, Save refuses it; DirLook terminal types ls into a terminal there instead |
| the plumber | none: Look on an http:// or https:// URL runs xdg-open (open on macOS); files, addresses and directories follow built-in rules |
| Esc selects what was typed | Esc leaves insert mode, or goes back a pane (→ Guide: Modes) |
Limits
| 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 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 |