diff options
Diffstat (limited to 'docs/fs.md')
| -rw-r--r-- | docs/fs.md | 763 |
1 files changed, 0 insertions, 763 deletions
diff --git a/docs/fs.md b/docs/fs.md deleted file mode 100644 index 85be803d..00000000 --- a/docs/fs.md +++ /dev/null @@ -1,763 +0,0 @@ -# Filesystem - -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. -This page is the reference for what each file does. The served -[`/README`](../src/fs-help.txt) is its one-screen summary. - -## Connecting - -The socket is `$XDG_RUNTIME_DIR/pardes-9p-<name>.sock` (else under -`~/.local/state/pardes`), `<name>` being the pid, the `--detach=NAME`, or -`--9p=<name>`. A socket in the runtime directory is also posted in the 9P -registry as `$XDG_RUNTIME_DIR/9p/pardes/<name>` (a symlink to the socket; -[cloud9.md](cloud9.md#the-posted-9p-registry)). - -Pane shells get `PARDES_PID` (the editor's pid), `PARDES_9P` (the socket) -and `PARDES_PANE` (their pane's serial). - -**Forwarding.** `pardes FILE` run in a pane (a live `PARDES_PID`, with -`PARDES_9P` and `PARDES_PANE`) writes FILE to that pane's `look` and returns -at once, as acme's `B` does. A FILE not there yet (`pardes notes/new.txt`) -opens a new pane named for it, empty, and its Save creates the file, making -its directories first when they are not there either. A session that -answers never gets a nested editor in its pane: what it refuses (a bad -name, a pane it has not) is printed, `pardes: <file>: <why>`, and the -launch exits 1, leaving no pane behind. A missing `PARDES_9P`/`PARDES_PANE`, -or a session that does not answer, starts a separate editor instead. Bare -`pardes` in a pane refuses and names `--nested`. - -`--wait` (`-w`) returns when the pane that shows FILE is deleted (exit 0) or -the session goes away (exit 1), as acme's `E` does. Use -`EDITOR='pardes --wait'` (`GIT_EDITOR` follows `EDITOR`), so fish's Ctrl-O, -`git commit` and `crontab -e` read the file after you close its pane. -`--nested` runs a separate session whose shells do not forward to it. - -**Clients.** - -```sh -9p -a "unix!$PARDES_9P" read index # plan9port, no mount -9ns --unix "$PARDES_9P" -- sh -c 'cat "$NINE_MOUNT/index"' # private mount -9ns --mntgen # the whole registry at /mnt/9p -``` - -Under `9ns --unix` the session is `$NINE_MOUNT` itself and exists only inside -that command. Under `9ns --mntgen` every posted session is -`$NINE_MOUNT/pardes/<pid or NAME>/`; take the name from `$PARDES_9P`. A dead -session's entry stays listed and answers `Input/output error`, so name the -session rather than globbing. `Tty9p` gives one pane's shell a kernel mount -at `$PARDES_MOUNT` ([v9fs.md](v9fs.md)). - -plan9port's `9p write` always opens with OTRUNC, so `echo x | 9p write -pane/3/body` replaces the whole body where acme would append. Append with -`>>` through a mount. - -For [Linux v9fs](https://www.kernel.org/doc/html/latest/filesystems/9p.html) -use `version=9p2000,cache=none,access=any`, `trans=unix` (or `trans=tcp` -with `port=`), `uname`, `dfltuid` and `dfltgid` for the local user, and an -empty `aname`. - -**Listeners.** `--9p-tcp='tcp!127.0.0.1!5640'` adds TCP; -`--9p-quic='quic!127.0.0.1!5641'` adds QUIC (build with `-Dquic=true`, -OpenSSL 3.6+; ALPN `pardes-9p`, an ephemeral TLS identity, no peer -verification). Addresses are numeric IPv4/IPv6; port 0 picks one; `/listeners` -reads them back. Every connection has full session access, `/os` included, -and TCP is unencrypted: use loopback. Unix and TCP share 16 connection slots; -a 17th client's Tversion gets `too many connections` (and the log -`err - 9p: too many connections (N turned away)`). QUIC has 16 of its own. -Plan9port and v9fs need a userspace bridge for QUIC. - -**Look paths and mounts.** Look resolves the OS filesystem first, then the -editor's own tree. Explicit paths skip that search: - -| Look path | Meaning | Served path | -|---|---|---| -| `/n/os/proc/self` | the host filesystem | `/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 session name, an absolute -socket path, `unix!/path`, `tcp!IP!port` or `quic!IP!port`; `Unmount peer` -removes it. Mount dials at once and fails if nothing answers (`dial failed: -no answer`, `timed out`, `hung up`). 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 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 tree - -``` -/README the one-screen guide (src/fs-help.txt) -/index a line per pane: serial kind dirty name column -/status pid, version, panes -/look /exec write a line: a right / middle click at the active pane; read: the serials touched -/log the event log; write `follow` to wait for more -/screen the rendered screen as JSON -/listeners dial addresses -/focus the serial of the pane with the keyboard; write one to move it -/ctl settings and session builtins -/commands every builtin, one a line -/recent files opened lately: open|closed <path> -/layout a line per column, then `active <serial>` -/tag /tagexec the workspace tag, and a word clicked in it -/col/<n>/ tag ctl exec of column <n>; rmdir closes an empty one -/pane/new open it to make a pane; read answers the serial -/pane/<n>/ name body tag ctl addr dot limit data xdata sel dirty mark scroll - errors event look exec tagexec, and pty/{ctl,status,data,run} on terminals; - rmdir closes the pane -/os/ the host filesystem -/src/ the editor's sources (only with -Dembed-sources=true) -``` - -Panes and columns are named by serials the editor gives: stable while they -live, never reused. Nothing is created by a listing, stat, walk or read: -only an open of `/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`, -`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 `\\`, a byte that -is not UTF-8 `\xNN`. - -## 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`. 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. Through 9ns the kernel sees an errno 9ns reads -from those words (cloud9 `9ns/src/nine.zig`, `enameToErrno`): `control -message`, `invalid` or `bad ` is EINVAL (malformed input); `no such`, `not -found` ENOENT; `in use` EBUSY; `no space` ENOSPC; `denied` EACCES; anything -else, such as `no match for regexp`, `address out of range`, `Modified` or -an `Edit` command pardes leaves out (`w`, `e`, `r`, `|`: `w is a sam command -pardes's Edit leaves out`), EIO; no refusal reads as EOPNOTSUPP. The `err` record has the words; a shell sees -only the errno, most often `Invalid argument` or `Input/output error`. - -Not failures: a look that finds nothing (it answers nothing and logs one -`err`), an Edit `x` that matches nothing, `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 mount cuts a big -write into pieces of at most one message (msize 64 KiB, 65536 negotiated, -less the header), and -each line runs once it is whole. A write that does not fill its message is -whole, so its last line runs even without a newline (`printf Save > exec`), -unless it is a multiple of 4096 bytes: that is where a writer's buffer (stdio, -a mount's page cache) filled and cut a line, so its tail waits for the next -write or the close. An `Edit` whose `{` or -`a`/`c`/`i` text is still open waits for the next write on that open. A -line held to the close (a 4096-multiple write's tail, an `Edit` block never -ended) runs there, and its failure is only in the log, as its `err` record -(``err <serial> ctl: unmatched `{'`` or `a, c or i text not ended by a . -line`): the close itself reports no error, and the write that sent it had -already succeeded. A script that needs a line's result ends the write with -a newline, so the line runs, and fails, with its write. A line held past -1 MiB is refused. - -**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`). An -open that wrote reads its own last answer; an open that never wrote reads -the session's last. A read is a stream: once read, the next read on that -fid is EOF until the next write (or seek to 0). With other clients about, -write and read on one open: -`exec 3<>$m/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. Such an open, or a `run`, `event` or `pty/data` open, -takes one of 64 open records; past that the open fails `too many open -files`. - -**Held reads.** A read with nothing to give yet (a followed `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. Through a FUSE mount bash's -`read -t` cannot time out: wrap the loop in `timeout N`. - -**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; `log` what an open would freeze. A view generated by -each read stats 0, as acme's do: `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 a right 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 `exec` line's blanks at either end are trimmed. - -- a path opens the file (the pane already showing it, if any); `file:12` - selects line 12, newline included; `file:12:5` puts the caret at line 12, - byte column 5; `file:<addr>` takes any address (below), **evaluated from - the file's dot**: `file:/re/` finds the next match after the selection, - `file:0/re/` the first. `:addr` addresses the pane itself. A leading `~` - is the home directory ($HOME, else the passwd entry's; `~user` that - 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 (no permission to - read it, say) fails the write, with why. -- a relative path is resolved where the click was, then where you have - been: first against the looking pane's own directory, then against the - directory of each pane in the jump list, most recent first, each - directory tried once; the first that names a file (or a pane open on - that path) wins. A pane never visited (not in the jump list) is not - searched. `./x` and `../x` are the looking pane's directory's alone. -- `@p<serial>:<addr>` addresses a pane by serial, a terminal's logical lines - too. -- a directory types `ls` into a terminal idle there, else opens one there. -- a URL opens in the browser. -- in a diff pane, a whole line of the diff is a look at the `path:line` it - names, as a right click on its first column is - ([tags.md](tags.md#reviewing-diffs)). -- a plain word selects its next place after dot, wrapping (`LookWord list` - on the root ctl lists every place in a `+Search` pane instead). In a - terminal a word is always listed, rows spelled `@p3:12:5-9`. - -A miss logs `err <serial> look: ...` (`no match for "zzq:#3"` quoting what -was written when nothing by that name exists; `<path>:99 has no line 99` -for a line past a file's end, `<path> has no page 99` for a PDF's page; -`<path>: no match for regexp` or `<path>: address out of range` when an -address fails), opens nothing, and leaves `look` reading empty; the write -succeeds. A path too long to repeat whole gives up its middle to `…`. - -A line written to `exec` is a middle click: - -- a builtin word runs (`/commands` lists them): `Save`, `Del`, `New`, `Tty - [shell]`, `Msg text`, `Find`, `Grep`, `Edit ...`, `Mount`, ... - A builtin that needs its argument (`Msg`, `Mount`, `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 `addr` and `dot=addr` first): `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 /log) without listing them; one - that finds nothing to rename fails; a rename the server spreads over - other files lists them in `+Search`, - `Diagnostics` and `Symbols` list the file's in `+Search`, `Lspinfo` says - which server serves the file and its state, and `Lspwhy` narrates the - last query step by step (in `+Lsp`), to tell why it found nothing. 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 `Save`, `Delete` - a `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. At a terminal idle - at an empty prompt it is typed into that shell. Anywhere else it runs as - a **command pane**: a terminal running the root ctl's `Shell` (`$SHELL`, - else `/bin/sh`) with `-c` and the line, in the pane's directory, with job - control on. Its tag reads `<dir> (<line>) running`, then `exit N`; the log - says `run <serial> <line>` and `exit <serial> <N|?>`; `exec` reads back its - serial. A typo ends `exit 127`. The directory's next command reuses a - finished command pane, below what it showed; one still running gets a - second pane. 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: - `awk '/^% /{out=""; next} {out = out $0 "\n"} END {printf "%s", out}' body`. - From a column's or the workspace's tag (or `/tagexec`, `col/<n>/exec`) - a command always runs as a command pane, in the session's directory. From a pane whose directory is gone nothing runs: `exec: - <dir>: no such directory` (ENOENT). - -The root's `look` and `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 `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`. - -## 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 is `bad value in - control message; ...` naming what it takes; `/commands` lists them. A - setting the frontend cannot show is refused (`Lift is GUI-only, invalid - here`). -- `Newcol` makes an empty column right of the keyboard's, halving the active - column. `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`. -- `Exit` quits. It refuses once while panes hold unsaved text: 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. The same word - again with nothing edited since discards; after more editing it refuses - again, naming only the panes edited since. `Restore`, `Del`, `Delcol` and - a pane's `get` refuse the same way with their own word. A `+New` scratch - under 100 bytes, or a command's output, is never asked about. -- `Dump` writes `pardes-<date>-<time>.zon` (UTC) in `DumpDir` - ([config.md](config.md#dumps)), 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. Undo history, the jump list and REPL bindings are not restored; a command pane - comes back showing how it ended (`exit ?` if it was running) and does not - run again. -- `Kill [word...]` (above), `Mount name dial`, `Unmount name`, `Theme x`, - `size <cols> <rows>`. -- `size C R` sizes a `--detach` session no frontend is attached to (160x50 - until then): from 20x6 to 4096x4096, else `invalid size`; refused while a - frontend owns the size, and when a column would lose its panes' minimum - rows (`size: too small for the panes, each its tag and 2 rows`). - -A write is refused whole, before anything runs, in Plan 9's words: -`unknown control message "X"`, `wrong #args in control message "X"`, -`bad value in control message ...`, or a word of the other ctl: `not a -session control message "Undo": write it to pane/<n>/ctl`, `... "Delcol": -write it to col/<serial>/ctl`, `not a window control message "X": write it -to /ctl`. A builtin that would open a prompt (`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. - -## 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>` -(acme's activecol: 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. `/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). A column may be empty, as in acme: `Newcol` -makes one, and closing its last pane leaves it (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. [tags.md](tags.md) covers tags on screen. - -## 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 acme's makenewwindow puts a window -([tags.md](tags.md#where-new-panes-go)): 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, `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 only a second line and a -NUL (`bad character in file name: a NUL`). Up to 255 bytes a component. -The ctl word `name x` takes all after its one blank; a second blank there -is refused rather than read as the name's first byte. A directory (`/`, `~`, `foo/`) is no file name: refused, EISDIR -(`name: /home/u is a directory, not a file`). - -**`body`** reads the text; a write appends; `>` (OTRUNC) replaces it all. -A terminal's body is its history as plain text in logical lines (wrapped -rows joined), frozen per open; writing it sends input to the child as typed -keys, never a paste: no bracketed-paste marks around it, even when the -program asked for them, so a newline in it is Enter. A PDF's -body is the text layer of the page shown, read-only: it is no text of the -pane's, so `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`). - -**`sel`** reads the selected text; a write replaces it. **`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 active (a folded pane stays folded); `no such pane`, or -`ill-formed control message` for a non-number. It reads empty while a -column or workspace tag has the keyboard. - -**Pane ctl.** Reading it gives acme's status line: serial, tag length, body -length, isdir (0), dirty, width in cells, font, tab width, undo available, -redo available, then `current`/`notcurrent` and a REPL's id if bound. It -takes: - -- the pane builtins: `Del` (`Del k`/`Del j`, or `DelAbove`/`DelBelow`, give - its rows to the pane above or below), `Save [path]` (making the - directories the file goes in first, whichever name it writes), `Collapse` (fold), - `Undo`/`Redo` (256 steps each), `Find pat`, `Edit ...`, `Tty [shell]` - (a new terminal pane in its directory), and the column words acting on - its column: `Delcol`, `Left`/`Right`/`Up`/`Down`. -- `get`: reload from the file (refused once over unsaved edits, `<name>: - Modified (get again to discard)`). -- `lock`/`unlock` (acme's). The lock binds only clients that take it and - belongs to the open that wrote it: `exec 3>$pane/ctl; echo lock >&3; ...; - exec 3>&-`. A `lock` another open holds fails at once, `file in use` - (EBUSY): retry. -- `answer <choice>` to the question the pane asks on its notice band, logged - `ask <serial> <what> <choices>` (`ask 4 del k j`, `ask 4 repl a b`, `ask 4 - save path`); `answer -` takes it back. -- acme's lowercase ctl words, done by what replaces each: `name x`, `put` - (Save), `clean`/`dirty`, `del`, `delete` (no asking), `dot=addr`, - `addr=dot`, `limit=addr`, `mark`/`nomark`, `show`, `cleartag`. `dump`, - `dumpdir`, `font`, `menu`, `nomenu` are refused, EINVAL. These lowercase - words are ctl-only: written to an `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 `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 `data` or `xdata` **replaces** the `addr` range (`>` and `>>` - alike); `: > data` deletes it. Truncating `data` is pardes's own (acme - ignores OTRUNC there); only truncating `body` empties the buffer. -- A write leaves `addr` just past what it wrote, so a second `echo x > data` - inserts after the first: write `addr` before each replacement. A read of - `data`/`xdata` moves `addr` past what it read. -- `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 `limit`. -- A failed address leaves **no address**: `addr` reads empty and `data`/ - `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 `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: `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](https://github.com/mnemnion/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/` 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 <sam commands>` on a pane's `ctl` or `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: - -```sh -printf 'Edit ,x/foo/{\ni/</\na/>/\n}\n' > $p/ctl -``` - -### Flags 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. 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. - -- origin: `E` a 9P write to body or tag, `F` other files and the editor's - own lines, `K` keyboard, `M` mouse. -- action: `X`/`L` executed/looked in the body, `x`/`l` in the tag (offsets - into the whole tag, path included), `I`/`D` body text inserted/deleted, - `i`/`d` the tag's. -- flag: 1 the text's first word is a builtin, 4 (look) a file name or - address, 8 (exec) chorded: two records follow, the argument and where it - came from. pardes never sends flag 2. -- A written line, or a click in a terminal's body, has no place: `0 0` with - its text (`FX0 0 1 6 Msg hi`). - -Write a record back to have it done as the click would: `<o><a><q0> <q1>\n` -acts on that range (the last record of a write needs no newline), and an -empty one (`MX12 12`) on the word or file name a click there expands to; -the whole record as read acts on its text (the only way for `0 0`). A chorded record with its two follow-ups, in one write or -three, runs once with its argument. `I`, `D`, `i`, `d` are refused. A helper -holding `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 (names as a code fence spells them: `py`, `sh`, ...); its -tag and `ctl` line show its id, `python-a`. `Repl -` unbinds, bare `Repl` -says the binding. A middle click or the execute key 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. 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'`, `\x03` is 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. -- `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). -- `pty/run`: one line at the shell's prompt, answered on the same open: - - ```sh - exec 3<>$m/pane/$n/pty/run; echo make >&3; cat <&3; exec 3<&- - ``` - - The answer's first line is the header: `exit N` then the command's output - (as the screen showed it: no colour, `\r` progress collapsed, trailing - blanks dropped; the last 64 KiB, `exit N cut M` when M bytes were left - out, bare `cut` when the start scrolled away or was cleared). Or: `busy: - <program> is running` (bare `busy` when text is typed at the prompt), - `exit ?` (no status reported, not a success), `error not run` (the shell - refused the line, e.g. a fish syntax error), `error shell gone`, `error no - prompt marks`, and on a command pane `error a command runs here, not a - shell` (`error command done; not a shell` once it ended). A line written - before a fresh terminal's first prompt waits for it. One line per run; - a second line before the answer is refused. - -`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`. - -## /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. - -```sh -exec 3<>$m/log; echo follow >&3; while read -r line <&3; do ...; done -``` - -A follower the ring outran reads `lost N` first. A Restore hangs the -follower up: dial again and read from `restore <path>`. - -| record | when | -|---|---| -| `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' `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 -`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`: 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. -- `/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>` or `closed <path>`; kept in `$XDG_STATE_HOME/pardes/recent`. - `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. -- `/status`: `pid`, `version`, `panes`. -- `/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. -- `/src/` (and `/shaders` on GUI builds) with `-Dembed-sources=true`; - `EffectCode <effect>` lists an effect's files under `/virtual`. - -## Limits - -| | | -|---|---| -| 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+TCP, 16 QUIC | -| opens holding state | 64 | -| 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 (~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 | - -## Source and tests - -`src/ninep/`: `tree.zig` (nodes, dispatch), `pane.zig`, `ctl.zig`, -`cols.zig`, `addr.zig`, `pty.zig`, `events.zig` (event and log), `screen.zig`, -`sources.zig`. The engine is cloud9's `fs.Server` (`src/9p.zig`); transports -are in `src/9p_io.zig`; `src/fs.zig` holds host access, mounts and -resolution. `zig build fs-test` drives real sessions with `test/ninep.py`; -`zig build 9p-test` checks the engine budgets. |
