diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-30 23:04:26 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:17 -0300 |
| commit | 5e72bfc34cc97d12be5185930135b55c2eb001b4 (patch) | |
| tree | d81daad17e81789e5ce583f22280d6cc368d6ab6 | |
| parent | 074f113fa95a875a6bf17196f8e46e69806a247c (diff) | |
| download | pardes-5e72bfc34cc97d12be5185930135b55c2eb001b4.tar.gz pardes-5e72bfc34cc97d12be5185930135b55c2eb001b4.zip | |
The reference is fs.md's per-file semantics, errors and limits in Typst, with the settings table, and says what the pane ctl's Left, Right, Up and Down do
Co-Authored-By: Claude Opus 5.5 <[email protected]>
| -rw-r--r-- | docs/config.md | 202 | ||||
| -rw-r--r-- | docs/fs.md | 763 | ||||
| -rw-r--r-- | docs/macos.md | 4 | ||||
| -rw-r--r-- | docs/open-questions.md | 4 | ||||
| -rw-r--r-- | docs/typ/reference.typ | 721 | ||||
| -rw-r--r-- | next-steps.txt | 2 | ||||
| -rw-r--r-- | src/builtins.zig | 2 | ||||
| -rw-r--r-- | src/ninep/ctl.zig | 4 | ||||
| -rw-r--r-- | test/monkey9p.py | 20 |
9 files changed, 739 insertions, 983 deletions
diff --git a/docs/config.md b/docs/config.md deleted file mode 100644 index 23355b06..00000000 --- a/docs/config.md +++ /dev/null @@ -1,202 +0,0 @@ -# Configuration - -## The startup file - -Native builds read one command file, `init`, from the per-user `pardes` -configuration directory: - -- Unix: `$XDG_CONFIG_HOME/pardes/init` (only an absolute `XDG_CONFIG_HOME` - counts), else `~/.config/pardes/init`. -- macOS: `$XDG_CONFIG_HOME/pardes/init` when set, else - `~/Library/Application Support/pardes/init`. -- Windows: `%LOCALAPPDATA%\pardes\init`, else - `%USERPROFILE%\AppData\Local\pardes\init`. - -The browser build has none. A file over 1 MiB or unreadable counts as absent. - -Each line is one builtin, spelled as it would be executed in pardes: - -```text -Theme orchard -Font DejaVuSansMono-Regular -TaglineSize 82 -Shell zsh -Wrap -``` - -A builtin that takes no argument matches only as the whole line (`Kill` runs, -`Kill something` does not); one that takes an argument takes the rest of the -line. Blank, unknown, malformed or failing lines are ignored silently and do -not stop later ones. Text that is no builtin is not run as a shell command -(`Exec ...` still is). Key bindings are compile-time choices in -`src/config.zig`; `init` does not remap them. - -`Config` (`SPC f c`) opens `init` itself in a pane, or goes to its pane when -it is open. With no file there yet, the pane is named for it, empty, and -Save writes it, making its directory first. `DumpConfig` opens a -`+DumpConfig` pane with every live setting, each line the word that sets -it and its value (`Shell /bin/bash`, `PanelSlide off`, `LocationsConfig …`) -and a `# Lift unsupported` comment for one the frontend cannot show. After a blank line -come what is in effect but set by no word, such as the last shell spawned -and the font in use, and the startup path (a right click opens it), each a -`#` line. A line starting with `#` is a comment wherever a line runs (the -init file, a ctl, an exec), so the report can be written back as is. The -root `ctl` file reads the settings back in the words a write takes -([fs.md](fs.md#the-root-ctl)). - -## 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; a value it does not take is refused, naming those it -does. `/commands` lists every builtin and its values. - -| setting | default | | -|---|---|---| -| `Theme <name>` | `orchard` | names as `Themes` (`SPC t t`) lists them ([themes.md](themes.md)); `NextColor` walks the ring | -| `ThemeFile <path>` | | a `.zon` theme, relative to the config directory; reloads live when saved | -| `FocusTint` | on | tint the focused pane's and column's tags | -| `SyntaxBold` | off | bold syntax keywords | -| `Verbose` | on | a builtin announces its name on the message row | -| `MessageAnimation` | on | messages ease in and dissolve | -| `MessageLinger`, `MessageFall`, `MessageDissolve` | 800, 180, 150 | milliseconds, at most 60000 | -| `Placement acme\|pardes` | `acme` | where new panes go ([tags.md](tags.md#where-new-panes-go)) | -| `BootShell keep\|replace` | `keep` | `replace` closes the untouched lone shell a dragged document lands beside | -| `LookWord search\|list` | `search` | a looked-at word selects its next place, or lists all in `+Search` | -| `Shell <name or path>` | `$SHELL`, else the login shell, else `/bin/sh` | the shell the next terminal runs; a bare name is looked for in the usual bin directories, not `$PATH`; bare `Shell` returns to the default | -| `DumpDir <dir>` | `$XDG_DATA_HOME/pardes`, else `~/.local/share/pardes` | where `Dump` writes; `~/` is home; 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 ...` | | Search, Grep and LSP result layout (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 | - -`Tty9p` (`SPC n 9`) is described in [v9fs.md](v9fs.md); the -`PARDES_V9FS_HELPER` variable points development builds at the helper. - -### Terminals - -Ctrl-B switches a terminal between raw input and editor mode. Plain Escape -at a detected shell prompt hops back to the previous pane; other keys, -Ctrl-O, Ctrl-W and modified Escape included, go to the program. `Mode` in the -tag returns to editor mode in place. Ctrl-V types the yank register and -Ctrl-Shift-V the desktop clipboard, through bracketed paste when the program -asked for it; neither reaches the program as a keystroke. - -`Filter` in a terminal's tag maps its ANSI colours through the theme, -keeping each foreground at least `tty_filter_min_contrast` (WCAG 1.5, in -`src/config.zig`) against its background. - -### Location results - -`LocationsConfig` with no argument prints the current settings as a line -that can be run again; with fields it changes only those: - -```text -LocationsConfig context:5 tscontext:on tslocations:off layout:stacked -``` - -- `context` (0): source lines shown above and below each match. -- `tscontext` (off): include the enclosing tree-sitter declaration headers. -- `tslocations` (on): show a location on each declaration header. -- `layout` (`stacked`): the location on its own line; `inline` puts it - 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 turns it -off: `PanelSlide`, `PanelZoom`, `PanelDissolve`, `PanelAscii`, -`PanelVertical`, `PanelEdges`, `PanelFall`, `PanelWave`, `PanelCurtain`, -`PanelScramble`, `PanelType`. All start off; the web shell has none. - -Scene passes (SDL GUI, and a GUI attached to a detached session), each at a -level 0-3 (`on` is 2), all off by default: `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 GUI-only; `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 -([effects.md](effects.md)). - -`EffectCode <effect>` lists that effect's sources under `/virtual` when the -build embeds them (`-Dembed-sources=true`). `zig build shaders` refreshes -the committed SPIR-V with its GLSL. - -### Look preview - -Resting the pointer on text for about 32 ms (`look_preview_delay_frames`, 2 -frames) tints what a right click would look at, with no other effect. Set -`look_preview_delay_frames` to `null` in `src/config.zig` to turn it off. - -## Themes from files - -`ThemeFile themes/mine.zon` loads a complete theme; a malformed save keeps -the last good one, and `Theme <name>` stops the watch. `DumpThemes` writes -every compiled theme to `<config dir>/themes/builtin/<name>.zon`: copy one, -change its `.name`, and edit. The format is `pardes.Theme` as -`std.zon.stringify` writes it; [themes.md](themes.md) describes each role. - -## Dumps - -`Dump` writes `pardes-<date>-<time>.zon` (UTC) in `DumpDir`, creating the -last directory if it is missing (a missing parent fails `no such -directory`); `$PARDES_DUMP` overrides the file. `Restore <path>` looks for a -relative path in `DumpDir`, then in the directory pardes started in; bare -`Restore` takes the last dump this session wrote. `pardes -l <dump.zon>` -starts from one. - -A dump keeps panes, columns and their tags, each text pane's selection, the -theme, and the settings that differ from a fresh session's; the font stays -the frontend's. A terminal comes back with its last MiB of output as -history, a `── restored history ──` line, and a new shell in its old -directory; a command pane comes back finished (`exit ?` if it was running). -Undo history and REPL bindings are not kept. A dump holds at most 6 columns. - -## Crash records - -A panic appends two lines to `crashes` beside `init` (build, time, platform, -pid; then the panic message) before printing to stderr. There is no stack -trace in it: collecting one from a panic handler can hang the process. The -trace stays on stderr. - -## Build options - -`zig build --help` lists the options for the selected platform. - -| option | values | default | -|---|---|---| -| `-Dplatform` | `tty`, `gui`, `web`, `macos`, `esp32p4` | absent: the tty and SDL shells together, installed into `~/.local` | -| `-Dstatic` | bool | `false` | -| `-Dquic` | bool; 9P over QUIC with system OpenSSL 3.6+ | `false` | -| `-Dmupdf` | bool | on natively, off for web and esp32p4 | -| `-Djpx` | bool; JPEG 2000, and with it scanned PDFs | `true` | -| `-Dtree-sitter` | `disabled`, `zig`, `minimal`, `full` | `full` natively, `zig` for web, `disabled` for esp32p4 | -| `-Dembed-sources` | bool; serve the sources under `/src` | `false` | -| `-Dstamp-commit` | bool; the git commit in `--version` and crash records | on for release builds and the `~/.local` install | -| `-Dtheme-animation` | bool | on except for esp32p4 | -| `-Dworkspace-tag` | bool; draw the workspace tag row | on except for macOS, whose menu bar carries it | -| `-Dprebuilt-shaders` | bool; embed the committed SPIR-V | on for a bare `zig build`, off with `-Dplatform` | -| `-Dtracy` | path to a Tracy checkout | off | -| `-Dmacos-identity` | codesigning identity for `pardes.app` | `-` (ad-hoc) | -| `-Ddump` | a `dump.zon` to embed in the web shell | none | -| `-Dtest-filter` | run only tests whose name contains it | none | -| `-Dtest-rebuild` | bool; fresh Zig test compilation | `false` | -| `-Dhelix-harness` | reference executable for live differential tests | `HX_HARNESS`, else `hx-harness` on PATH | -| `-Desp32p4-cols`, `-Desp32p4-rows` | the board's grid | 56, 14 | - -The version comes from `build.zig.zon`'s `.version`. 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. diff --git a/docs/macos.md b/docs/macos.md index 5512edbd..3ee55d0a 100644 --- a/docs/macos.md +++ b/docs/macos.md @@ -462,7 +462,7 @@ after `cd` and after an idle tick. Nested launches use the same inherited 9P address and pane serial as other native hosts. They do not inspect ancestor processes or executable names. -See [the control filesystem](fs.md) for paths and transports. +See [the control filesystem](typ/reference.typ) for paths and transports. Two more things this host cannot inherit from its launcher, because a `.app` has none: @@ -1140,7 +1140,7 @@ used for the table and is not folded into the direct-path claim. `src/macos.zig` never calls `takeAttach`, so the word does nothing at all and says nothing either. Wiring it up means a unix-socket frontend loop beside the AppKit one, which is `src/detached/client.zig`'s job in the tty and SDL - shells; see `docs/detached.md`. + shells; see `docs/typ/building.typ`. - **IME and marked text.** Only finished characters reach `pardes_key`, so a dead key composes nothing and Option is Alt rather than a compose modifier. Real composition means implementing `NSTextInputClient` *and* giving the core diff --git a/docs/open-questions.md b/docs/open-questions.md index 3f365ce9..f1ffb544 100644 --- a/docs/open-questions.md +++ b/docs/open-questions.md @@ -8,10 +8,10 @@ picked up without redoing the research. None is open now. - **Where an unknown command word runs** (2026-09-28): at a terminal idle at its prompt it is typed into that shell; anywhere else it runs as a command pane, a terminal whose child is `Shell -c <line>`, reporting `exit N` in - its tag and the log ([fs.md](fs.md#look-and-exec)). Chosen over acme's + its tag and the log ([the reference](typ/reference.typ)). Chosen over acme's process writing into `+Errors` because interactive programs keep working without a streaming runner, and over checking `PATH` before typing into a shell, which misjudges builtins, aliases and functions. - **Where an exec from a code file goes** (2026-09-28): to a REPL bound for - its language, when one is ([fs.md](fs.md#repls)). Chosen over a per-file + its language, when one is ([the reference](typ/reference.typ)). Chosen over a per-file or per-directory binding, which would need a place to keep and show it. diff --git a/docs/typ/reference.typ b/docs/typ/reference.typ new file mode 100644 index 00000000..5fc2dd1d --- /dev/null +++ b/docs/typ/reference.typ @@ -0,0 +1,721 @@ +// The reference: every file the 9P control filesystem serves, what it +// does, how it fails, and the limits. How pardes behaves on screen is the +// guide's; recipes and traps are scripting's; how a mount cuts writes and +// the listeners are building's. +#import "style.typ": key, keys, btn, chord, word, tag, addr, file, cmd, doc, pairs + +Every native session serves 9P2000 (not .u, not .L) as a control +filesystem, in acme's manner: panes, columns, tags and the session are +files. Paths here are the served ones (#file("/pane/2/body")); through a +mount they sit under the session's directory (`$m`; the scripting chapter +says how to find it). The served #file("/README") is a +one-screen summary of this page. + += The tree <tree> + +``` +/README the one-screen summary (src/fs-help.txt) +/index a line per pane: serial kind dirty name column +/status pid, version, panes +/look /exec write a line: a right / middle click at the active pane; read: the serials touched +/log the event log; write `follow` to wait for more +/screen the rendered screen as JSON +/listeners dial addresses +/focus the serial of the pane with the keyboard; write one to move it +/ctl settings and session builtins +/commands every builtin, one a line +/recent files opened lately: open|closed <path> +/layout a line per column, then `active <serial>` +/tag /tagexec the workspace tag, and a word clicked in it +/col/<n>/ tag ctl exec of column <n>; rmdir closes an empty one +/pane/new open it to make a pane; read answers the serial +/pane/<n>/ name body tag ctl addr dot limit data xdata sel dirty mark scroll + errors event look exec tagexec, and pty/{ctl,status,data,run} on terminals; + rmdir closes the pane +/os/ the host filesystem +/src/ the editor's sources (only with -Dembed-sources=true) +``` + +Panes and columns are named by serials the editor gives: stable while they +live, never reused. Nothing is created by a listing, stat, walk or read: +only an open of #file("/pane/new") makes a pane (so `ls -l` and `find` are +safe), only `rmdir` of #file("/pane/<n>") or an empty #file("/col/<n>") +removes. Tcreate is refused everywhere. + +#file("/index") rows are `serial kind dirty name column`, kind `text`, +`term`, `pdf` or `image`, the name `<dir>/+New` for an unnamed scratch. +Names may hold blanks, so split `head, col = row.rsplit(maxsplit=1)`, then +`serial, kind, dirty, name = head.split(maxsplit=3)`. Names in +#file("/index"), the log and a terminal's tag are escaped: a newline `\n`, a +backslash `\\`, a byte that is not UTF-8 `\xNN`. + += Rules for every file <rules> + +*Failure.* A write fails whenever what it asked for fails, and logs one +`err <serial|-> <file>: <why>` record, with no `msg`. Only writes log: a +refused open or truncation (an OTRUNC open such as `> data` after a failed +`addr`), create or remove answers its error alone, as do a write to +#file("pane/new") (`permission denied`) and a write on a read-only fid +(`bad use of fid`). Errors are words (Plan 9's where pardes has none of its +own), never a C library string. Through 9ns the kernel sees an errno 9ns +reads from those words (cloud9's `9ns/src/nine.zig`, `enameToErrno`): +`control message`, `invalid` or `bad ` is EINVAL (malformed input); `no such`, `not found` ENOENT; `in use` EBUSY; `no space` ENOSPC; `denied` +EACCES; anything else, such as `no match for regexp`, `address out of range`, `Modified` or an `Edit` command pardes leaves out (`w is a sam command pardes's Edit leaves out`), EIO; no refusal reads as EOPNOTSUPP. +The `err` record has the words; a shell sees only the errno, most often +`Invalid argument` or `Input/output error`. + +Not failures: a look that finds nothing (it answers nothing and logs one +`err`), an Edit `x` that matches nothing, #word("Undo") with nothing to +undo (a `msg`), and a command pane's command, which ends in its own time +with an `exit` record. + +*Command lines.* #file("look"), #file("exec"), #file("tagexec"), the three +kinds of #file("ctl") and a column's #file("exec") take one command a line. +The whole write is checked first (a control character other than a tab +fails it all, EINVAL); then lines run in order, and a failing line fails +the write after the lines before it took effect, as acme's ctl does. Blank +lines are skipped. A line runs once it is whole: a write's last line runs +without its newline, except where a mount cut the write (the building +chapter has the mechanics); end a write with a newline when its result +matters. An `Edit` whose `{` or `a`/`c`/`i` text is still +open waits for the next write on that open. A line held past 1 MiB is +refused. + +*Answers.* Reading #file("look"), #file("exec") or #file("tagexec") answers +the serials the last command made, or else the pane it acted on or +focused, one a line; nothing when it did none of these (#word("Newcol") at +#file("/tagexec")). An open that wrote reads its own last answer; an open +that never wrote reads the session's last. A read is a stream: once read, +the next read on that fid is EOF until the next write (or seek to 0). With +other clients about, write and read on one open: +#cmd("exec 3<>$m/look; echo x >&3; cat <&3; exec 3<&-") + +*Snapshots.* #file("/index"), #file("/layout"), #file("/recent"), +#file("/commands"), #file("/status"), #file("/listeners"), a read-only +#file("ctl"), #file("/log"), #file("/screen") and a terminal's +#file("body") freeze at the open, so one read in several chunks never +splices two moments; open again for now. Such an open, or a #file("run"), +#file("event") or #file("pty/data") open, takes one of 64 open records; +past that the open fails `too many open files`. + +*Held reads.* A read with nothing to give yet (a followed #file("log"), +#file("event"), #file("pty/data"), #file("pty/run") before its answer) +waits in the editor and is answered when news comes. A second read on that +open meanwhile fails `file in use`. A read the client flushed is dropped. +One connection holds at most 128 reads at once; the next is refused `too many reads waiting: 128`. Every held read is on an open that keeps state, +and those are 64 in the session (above), so 64 is the real cap on reads +held at once, through one connection or many. A mount (9ns) is one +connection, and it keeps answering everything else beside its held reads. + +*Stats.* A file whose text is kept has its real length: a pane's +#file("body"), #file("tag"), #file("name"), #file("ctl"), #file("sel") and +the range and flag files, the workspace's #file("tag"), the root +#file("ctl"), #file("status"), #file("commands"), #file("README"), the +#file("look")/#file("exec") answers, #file("focus"); #file("event") and +#file("pty/data") the next record's, zero when none waits; #file("log") +what an open would freeze. A view generated by each read stats 0, as +acme's do: #file("screen"), #file("data"), #file("xdata"), #file("index"), +#file("layout"), #file("recent"), #file("listeners"), #file("pane/new"). +Read those to the end rather than trust a length (`cat` does). The qid +version of #file("body"), #file("data") and #file("xdata") is the pane's +revision, so a stat sees an edit land. Modes are 0644/0666, 0444 +read-only, 0222 write-only. + += look and exec <look-and-exec> + +A line written to #file("look") is a #btn("B3") click on it, on the line as +written: its leading blanks are its own (` return x` finds that +indented line, and a diff's blank context line ` ` is a look too); only its +newline and `\r` go. An #file("exec") line's blanks at either end are +trimmed. The guide says what a look opens and where it looks for a relative +path (#doc("tags", section: "looking")); here is what is particular to the +files. + +- `file:12` selects line 12, newline included; `file:12:5` puts the caret + at line 12, byte column 5; `file:<addr>` takes any address (below), + *evaluated from the file's dot*: `file:/re/` finds the next match after + the selection, `file:0/re/` the first. `:addr` addresses the pane itself. + `@p<serial>:<addr>` addresses a pane by serial, a terminal's logical + lines too. +- A leading `~` is the home directory (`$HOME`, else the passwd entry's; + `~user` that user's) here and wherever a path is typed (#file("name"), + #word("Save"), #word("ThemeFile"), #word("DumpDir"), #word("Restore"), + `pardes '~/x'`), even beside a file named `~`: write `./~` for that. +- A path to no file is a miss, as a search that finds nothing is: said on + the message row and logged as an `err`, the write still answered. A file + that is there but will not open (no permission to read it, say) fails the + write, with why. +- A plain word selects its next place after dot, wrapping (`LookWord list` + on the root ctl lists every place in a `+Search` pane instead). In a + terminal a word is always listed, rows spelled `@p3:12:5-9`. + +A miss logs `err <serial> look: ...` (`no match for "zzq:#3"` quoting what +was written when nothing by that name exists; `<path>:99 has no line 99` +for a line past a file's end, `<path> has no page 99` for a PDF's page; +`<path>: no match for regexp` or `<path>: address out of range` when an +address fails), opens nothing, and leaves #file("look") reading empty; the +write succeeds. A path too long to repeat whole gives up its middle to `…`. + +A line written to #file("exec") is a #btn("B2") click: + +- A builtin word runs (#file("/commands") lists them). A builtin that needs + its argument (#word("Msg"), #word("Mount"), #word("Find")) written bare is + `wrong #args in control message "Msg"`, and so is one that takes none + written with one (`Config extra`). +- A line starting with `#` is a comment, as in a shell: it runs as nothing, + silently, here and in every ctl. +- The language server's words ask about a file pane's text at its cursor + (set it with #file("addr") and `dot=addr` first): #word("Hover") fills + `+Hover`; `Rename new` renames the symbol in the file and says how many + ranges it changed (on the message row, and so in the log) without listing + them, fails when there is nothing to rename, and lists the files of a + rename the server spreads over several in `+Search`; + #word("Diagnostics") and #word("Symbols") list the file's in `+Search`; + #word("Lspinfo") says which server serves the file and its state; + #word("Lspwhy") narrates the last query step by step (in `+Lsp`). The + write returns once the answer is in; a pane with no file is refused. +- acme's words run as pardes's where it has one (`Put` is #word("Save"), + `Delete` a #word("Del") that does not ask); the rest (`Get`, `Putall`, + `Snarf`, `Cut`, `Paste`, `Zerox`, `Sort`, `Load`, `ID`, `Send`, `Tab`, + `Indent`, `Local`, `Incl`, `Abort`) are refused, `invalid: acme's Get is not a pardes builtin: ...`, never run as commands. So is a GUI-only + builtin (`Fonts`) on another frontend. +- Anything else is a command line, at most 1024 bytes, run as the guide + says (#doc("tags", section: "command-panes")): typed into a terminal idle + at an empty prompt, else run in a command pane. #file("exec") reads back + the command pane's serial; the log says `run <serial> <line>` and `exit <serial> <N|?>`. A reused pane's #file("body") keeps every earlier run + above a `% <line>` row naming each command, so to take only the last + run's output read from after the last `% ` row: + #cmd("awk '/^% /{out=\"\"; next} {out = out $0 \"\\n\"} END {printf \"%s\", out}' body") + From a pane whose directory is gone nothing runs: `exec: <dir>: no such directory` (ENOENT). + +The root's #file("look") and #file("exec") act at the active pane and log as +that pane's (with no pane at all, in the session's directory: a look opens +its file, making a column as #word("New") does, and an exec runs its +command there); #file("/pane/<n>/look") and #file("exec") at pane n; +#file("/tagexec") and #file("/col/<n>/exec") click in the workspace's or +that column's tag, run commands in the session's directory, and log as +`-`. A pane's word (#word("Undo"), #word("Msg"), #word("Save")) is refused +at #file("/tagexec") and a column's exec: `not a session control message "Undo": write it to pane/<n>/ctl`. + +A background job (`&`) outlives a command that exits on its own; its +output goes on below `exit N` until it lets go of the pty. #word("Kill") +(root ctl) stops the commands pardes started: bare, all; `Kill make ls`, +those whose line starts with one of the words. For a command pane it +signals the whole line, `&` jobs included; for a line typed into a shell +only the foreground job (SIGTERM), and the shell decides the rest. With +nothing running, named or not, it says `Kill: nothing running` and the +write succeeds; words that name none of what runs fail it (`Kill: no running command has that first word`). #word("Kill") does not reach a +REPL's code: use `sig INT` on its #file("pty/ctl"). + +== Look paths and mounts <mounts> + +A look resolves the OS filesystem first, then the editor's own tree. +Explicit paths skip that search: + +#pairs( + [`/n/os/proc/self`], [the host filesystem, served as #file("/os/proc/self")], + [`/n/self/pane/2/body`, `/virtual/pane/2/body`], [this session's tree, #file("/pane/2/body")], + [`/virtual/src/pardes.zig`], [sources embedded with `-Dembed-sources=true`, #file("/src/pardes.zig")], + [`/n/peer/pane/2/body`], [a mounted session: the peer's #file("/pane/2/body")], +) + +`--mount=peer=work` or `Mount peer <dial>` mounts a session name, an +absolute socket path, `unix!/path`, `tcp!IP!port` or `quic!IP!port`; +`Unmount peer` removes it. #word("Mount") dials at once and fails if +nothing answers (`dial failed: no answer`, `timed out`, `hung up`); with no +dial it is `wrong #args`, and a dial that is no address `bad dial address`, +both EINVAL. There are eight named mounts; `os` and `self` are reserved. +#word("Unmount") refuses a mount a pane, a working directory or a pending +Save still uses. Mounts are dumped. + +A session may open its own tree through a mount (a look at `$m/pane/2/body` +from the editor serving `$m`): requests are answered on the connection's +task while the editor waits in its syscall. Through QUIC that still hangs. + += The root ctl <root-ctl> + +Reading #file("/ctl") gives every setting, one a line, in the words a write +takes (`Theme orchard`, `Verbose on`, `Placement acme`, `DumpDir <dir>`, +`Shell /bin/bash`, ...), so writing back what it reads changes nothing. +Writes take settings and session builtins (`scope = .session` in +`src/builtins.zig`), acting at the pane with the keyboard: + +- A setting written bare steps to its next value (a switch flips; so do + #word("Placement"), #word("BootShell"), `Crt`). A value it does not + take is `bad value in control message; ...` naming what it takes; + #file("/commands") lists them. A setting the frontend cannot show is + refused (`Lift is GUI-only, invalid here`). +- #word("Newcol") makes an empty column right of the keyboard's, halving + it. #word("Joincol") folds the keyboard's column into the one on its + right (its panes go below that column's), `Joincol: no column to the right`. +- #word("Exit") quits. While panes hold unsaved text it refuses once + (#doc("tags", section: "unsaved-panes")): one `unsaved <serial> <name>` + record per pane, then the write fails `<name>: Modified (Exit again to discard)` or `4 unsaved panes: Modified (Exit again to discard)` (EIO), + and the list stays in a `+Unsaved` pane. #word("Restore"), #word("Del"), + #word("Delcol") and a pane's `get` refuse the same way with their own + word. +- #word("Dump") writes `pardes-<date>-<time>.zon` (UTC) in #word("DumpDir"), + logs `dump <path>`, and adds a `Restore <path>` word naming it to the + workspace tag (the last dump's, replacing an earlier one's). + `Restore [path]` replaces every pane (bare: the last dump this session + wrote). The Restore write is answered, then *every connection is hung + up*: dial again, and restart a 9ns mount. The new log has a `new` per + pane, `restore <path>`, then `restored <old> <new>` per pane and + `restoredcol <old> <new>` per column. +- `Kill [word...]` (above), `Mount name dial`, `Unmount name`, `Theme x`. +- `size C R` sizes a `--detach` session no frontend is attached to (160x50 + until then): from 20x6 to 4096x4096, else `invalid size`; refused while a + frontend owns the size, and when a column would lose its panes' minimum + rows (`size: too small for the panes, each its tag and 2 rows`). + +A write is refused whole, before anything runs, in Plan 9's words: +`unknown control message "X"`, `wrong #args in control message "X"`, `bad value in control message ...`, or a word of the other ctl: `not a session control message "Undo": write it to pane/<n>/ctl`, `... "Delcol": write it to col/<serial>/ctl`, `not a window control message "X": write it to /ctl`. A builtin that would open a prompt (#word("Save") on a scratch) +fails `control message needs its argument "Save"`. A line that fails as it +runs fails with the editor's words (`Mount: already mounted`, `Save /root/x: access denied`), changing nothing. + +== Settings <settings> + +A setting that chooses among words (`on`/`off` switches, +#word("Placement"), #word("BootShell"), `Crt`, `Bloom`, +`Vignette`, `Grain`, `Lift`, #word("Motion"), +`ShaderAnimation`) steps to its next value when given bare, as its +word clicked in a tag does. The same lines go in the startup file +(#doc("config")). + +#pairs( + [`Theme <name>`], [`orchard`; names as #word("Themes") lists them, #word("NextColor") walks the ring (#doc("themes"))], + [`ThemeFile <path>`], [a `.zon` theme, relative to the config directory; reloads live when saved], + [`FocusTint`], [on: tint the focused pane's and column's tags], + [`SyntaxBold`], [off: bold syntax keywords], + [`Verbose`], [on: a builtin announces its name on the message row], + [`MessageAnimation`], [on: messages ease in and dissolve], + [`MessageLinger`, `MessageFall`, `MessageDissolve`], [800, 180, 150 milliseconds, at most 60000], + [`Placement acme|pardes`], [`acme`: where new panes go (#doc("tags", section: "new-panes"))], + [`BootShell keep|replace`], [`keep`; `replace` closes the untouched lone shell a dragged document lands beside], + [`LookWord search|list`], [`search`: a looked-at word selects its next place, or lists all in `+Search`], + [`Shell <name or path>`], [`$SHELL` when it is executable, else `/bin/sh`: the shell the next terminal and command pane run; a bare name is looked for in the usual bin directories, not `$PATH`; bare #word("Shell") returns to the default], + [`DumpDir <dir>`], [`$XDG_DATA_HOME/pardes`, else `~/.local/share/pardes`: where #word("Dump") writes; bare returns to the default], + [`TreeContext`], [off: sticky declaration headers in a source pane (per pane, dumped)], + [`TreeContextTagStyle`], [on: draw those headers in the tagline style], + [`LocationsConfig ...`], [the layout of #word("Grep"), search and language-server results (below)], + [`Wrap`, `Colors`, `Tagbottom`, `Debug`], [toggles], + [`Font <name>[:<size>]`, `Fonts`], [SDL and macOS only; size 8-72 (pixels in SDL, points on macOS)], + [`TaglineSize <1-100>`], [82: tagline face, percent; SDL and macOS], + [`WindowOpacity <0-100>`], [100; SDL only: everything but text and the cursor], + [`Ligatures`], [on; SDL only, macOS draws CoreText's own], + [`Pet cat|frog|off`], [off; SDL only: a sprite in the workspace tag's blank space], +) + +`LocationsConfig` with no argument prints the current settings as a line +that can be run again; with fields it changes only those: +`LocationsConfig context:5 tscontext:on tslocations:off layout:stacked`. +`context` (0) is the source lines shown above and below each match; +`tscontext` (off) includes the enclosing tree-sitter declaration headers; +`tslocations` (on) shows a location on each declaration header; `layout` +(`stacked`) puts the location on its own line, `inline` beside the source, +padded in groups of eight matches. An invalid field rejects the whole line; +a repeated field's last value wins. The settings survive Dump and Restore. +Source analysis is cached for 64 files and 64 MiB. + +*Effects.* Panel transitions, one at a time, running the active one again +turning it off: #word("PanelSlide"), #word("PanelZoom"), +#word("PanelDissolve"), #word("PanelAscii"), #word("PanelVertical"), +#word("PanelEdges"), #word("PanelFall"), #word("PanelWave"), +#word("PanelCurtain"), #word("PanelScramble"), #word("PanelType"); all +start off, and the web shell has none. Scene passes (the SDL window, and +one attached to a detached session), each at a level 0-3 (`on` is 2), all +off: `Crt`, `Bloom`, `Vignette`, `Grain`. +`Shader <file.glsl>` adds a Shadertoy file written for ghostty to the +chain (`Shader off` removes it; it recompiles when saved); +`ShaderAnimation off|on|always` says when the chain animates by itself. The +focused pane can stand off the page: `Lift shadow|rim|auto|off`, +`InactiveDim <percent>`, `Motion off|crisp|smooth|bouncy|playful` (default +`smooth`), `SelectionGlow`, `HoverGlow`, `Occlusion`, +`Parallax`, #word("JumpTrail"), #word("ChipShadow"), +#word("ThumbFlash"), `CursorBlink`, `GripWidth <50-300>`. Most are +the GUI's; #word("InactiveDim") works everywhere, and #word("JumpTrail"), +#word("ChipShadow") and #word("ThumbFlash") are the terminal's. No effect +may lower the contrast of text, the selection or a focus indicator. +`EffectCode <effect>` lists that effect's sources under `/virtual` when the +build embeds them. + +Resting the pointer on text for about 32 ms (`look_preview_delay_frames`, +2 frames, in `src/config.zig`; `null` turns it off) tints what a +#btn("B3") click would look at, with no other effect. A terminal's +#word("Filter") keeps each foreground at least `tty_filter_min_contrast` +(WCAG 1.5, `src/config.zig`) against its background. + += Columns and tags <columns-and-tags> + +#file("/layout") has a line per column, left to right: `serial index x width current|notcurrent empty|full pane-serials...`, then `active <serial>` (the active column: where #file("pane/new") and a look put the +next pane; `-` when none). `current` is the column with the keyboard now, +which may differ from the active one (#doc("tags", section: "active-column")). +#file("/index")'s last field is each pane's column serial. + +A session holds 16 columns (`no space for a column: 16 max`, ENOSPC), each +at least 10 cells wide, so 16 wants a window 160 cells wide or more. +#word("Newcol") halves the column it is run from, and one under 20 cells +does not split (`this one is too narrow to split`): reach 16 by writing +#word("Newcol") to the widest column's #file("exec") each time, not to the +root's, which keeps halving the one just made. #word("Newcol") is refused +as well when a narrower column would wrap its panes' tags onto more rows +and leave one under its tag and two rows (`Newcol: no space for a column: the panes' tags would not fit`); a refused #word("Newcol") logs its `err` +alone and uses up no column serial. + +#file("/col/<n>/ctl") takes #word("Delcol"), #word("Joincol"), +#word("New") and #word("Tty") for that column; #file("/col/<n>/exec") is +a click in its tag; `rmdir /col/<n>` closes an empty column (one with +panes: ENOTEMPTY). Closing a column's last pane leaves it empty (the +keyboard goes to its tag, #file("focus") reads empty, the log says only +`del`); closing the session's last pane quits pardes. The log says +`newcol <serial>` and `delcol <serial>`. + +#file("tag") files (#file("/tag"), #file("/col/<n>/tag"), +#file("/pane/<n>/tag")) read the whole tag, with no newline after it. A +pane's starts with its computed path (an image's with its mode words, a +PDF's with its page), then the editable text. `>` replaces the editable +text (the default words too: `echo Make > tag` leaves only `Make`) and +drops the one newline that ends it; `>>` appends, so +`printf ' Make' >> tag` (the blank matters; `echo` would start a second +line). A pane tag may hold several lines; a column or workspace tag is +one, a newline written into it becoming a space. Control characters other +than tab, DEL, C1 controls and non-UTF-8 bytes are refused (`invalid tag text`). The editable text is at most 4096 bytes (`no space: over 4096 bytes`, ENOSPC; the log says `err <serial> tag: no space: ...`). All +three kinds take the same checks, whole or not at all, and a `>` whose +write is refused changes nothing: its truncation is done with the write +that fits, or at the close (or a read) when none came. Each write stands +on its own: in a `>` cut into several writes, those taken before a refused +one stay in the tag; only the refused write changes nothing. A clear is an +ordinary edit and #key("u") in the tag undoes it. + += Panes <panes> + +*Making and closing.* Opening #file("/pane/new") makes a scratch +`<dir>/+New` (the session's directory), and reading that open answers its +serial: `n=$(cat $m/pane/new)`. *Each open makes another pane*; two reads +of one open answer the same serial. It goes where a #word("New") from a +tag would (#doc("tags", section: "new-panes")): in the active column, +filling it if empty, else the bottom half of its last pane. A session +holds 64 panes (`no space for a pane: 64 max`), and every pane keeps its +tag and 2 rows (`no space for a pane in that column: each keeps its tag and 2 rows`); both are ENOSPC, for this open and for a look, exec, +#word("New") or #word("Tty") alike. A #file("pane/new") whose column is +full takes its rows from a pane in another column that has them, last +column first, as acme does, and is refused only when no pane anywhere can +give them. `rmdir /pane/<n>` closes the pane, unsaved or not. + +*#file("name")* reads the file name (a terminal's directory); a write +renames the buffer (relative to the pane's directory) and marks nothing +dirty; #word("Save") then writes under the new name. A name takes any byte +a file name can hold, blanks, control bytes and bytes not UTF-8 included, +so what #file("name") reads writes back as it was; refused (EINVAL) are +only a second line and a NUL (`bad character in file name: a NUL`). Up to +255 bytes a component. The ctl word `name x` takes all after its one +blank; a second blank there is refused rather than read as the name's +first byte. A directory (`/`, `~`, `foo/`) is no file name: refused, +EISDIR (`name: /home/u is a directory, not a file`). + +*#file("body")* reads the text; a write appends; `>` (OTRUNC) replaces it +all. A terminal's body is its history as plain text in logical lines +(wrapped rows joined), frozen per open; writing it sends input to the +child as typed keys, never a paste: no bracketed-paste marks around it, +even when the program asked for them, so a newline in it is Enter. A PDF's +body is the text layer of the page shown, read-only: it is no text of the +pane's, so #file("addr"), #file("data"), #file("dot") and a `file:<addr>` +look do not address it (a PDF's `:<n>` is its page); images and PDFs take +no write (`this pane has no text`). + +*#file("sel")* reads the selected text; a write replaces it. +*#file("errors")* is write-only: text appended to the directory's +`+Errors` pane (logged as `msg` records when no column has room). + +*#file("focus")* (root): a serial written gives that pane the keyboard and +makes its column the active one (a folded pane stays folded); `no such pane`, or `ill-formed control message` for a non-number. It reads empty +while a column or workspace tag has the keyboard. + +*Pane ctl.* Reading it gives acme's status line: serial, tag length, body +length, isdir (0), dirty, width in cells, font, tab width, undo available, +redo available, then `current`/`notcurrent` and a REPL's id if bound. It +takes: + +- the pane builtins: #word("Del") (`Del k`/`Del j`, or + #word("DelAbove")/#word("DelBelow"), give its rows to the pane above or + below), `Save [path]` (making the directories the file goes in first, + whichever name it writes), #word("Collapse"), #word("Undo")/#word("Redo") + (256 steps each), `Find pat`, `Edit ...`, `Tty [shell]` (a new terminal + in its directory), #word("Delcol") for its column, and + #word("Left")/#word("Right")/#word("Up")/#word("Down"), which give the + keyboard to the pane beside it that way (#key("Ctrl-w") with + #keys("h", "l", "k", "j")), up from a column's top pane to its tag. +- `get`: reload from the file (refused once over unsaved edits, `<name>: Modified (get again to discard)`). +- `lock`/`unlock` (acme's). The lock binds only clients that take it and + belongs to the open that wrote it: `exec 3>$pane/ctl; echo lock >&3; ...; exec 3>&-`. A `lock` another open holds fails at once, `file in use` + (EBUSY): retry. +- `answer <choice>` to the question the pane asks on its notice band, + logged `ask <serial> <what> <choices>` (`ask 4 del k j`, `ask 4 repl a b`, `ask 4 save path`); `answer -` takes it back. +- acme's lowercase ctl words, done by what replaces each: `name x`, `put` + (Save), `clean`/`dirty`, `del`, `delete` (no asking), `dot=addr`, + `addr=dot`, `limit=addr`, `mark`/`nomark`, `show`, `cleartag`. `dump`, + `dumpdir`, `font`, `menu`, `nomenu` are refused, EINVAL. These lowercase + words are ctl-only: written to an #file("exec"), `del` is a command line. + +A file changed on disk reloads by itself only when the buffer has no +unsaved edits; otherwise it stays dirty, says `<name> changed on disk (get reloads it, Save overwrites it)`, logs `changed <serial>`, and its next +#word("Save") warns once. + +== Addresses and data <addresses> + +#file("addr"), #file("dot") and #file("limit") read the pair of byte +offsets they take, so copying one onto another (`cp $p/addr $p/dot`) is +acme's `dot=addr`. A write is a pair or an address expression; a pair is +checked as an address is, `addresses out of order` (`5 2`) or `address out of range` (past the text), never clamped. #file("addr") names where +#file("data") reads (to the end of text) and the range #file("xdata") +reads; #file("dot") is the selection (moving it scrolls the pane there); +#file("limit") bounds the end of a forward search and reads empty until +set. Truncating #file("dot") empties it, truncating #file("limit") lifts +it. + +- A write to #file("data") or #file("xdata") *replaces* the #file("addr") + range (`>` and `>>` alike); `: > data` deletes it. Truncating + #file("data") is pardes's own (acme ignores OTRUNC there); only + truncating #file("body") empties the buffer. +- A write leaves #file("addr") just past what it wrote, so a second + `echo x > data` inserts after the first: write #file("addr") before each + replacement. A read of #file("data")/#file("xdata") moves #file("addr") + past what it read. +- #file("addr") belongs to the pane, not the client, and neither an open + nor a truncation resets it (acme resets on first open): each + `echo /re/ > addr` searches on from the last, so a find loop advances. + Write `0` to start at the top; searches wrap, so stop a loop when the + address comes back or set #file("limit"). +- A failed address leaves *no address*: #file("addr") reads empty and + #file("data")/#file("xdata") refuse (`no address: the last one written to addr failed`) until a standalone address (`2`, `#0`, `/re/`) is written, + so a missed target is never written at the old one. + +Addresses are sam's: `#n`, a line number, `/re/`, `?re?`, `-/re/`, `$`, +`.`, `0`, ranges `a,b` and `a;b`, `+`/`-`. They are evaluated from the +current address (the last written to #file("addr"), or just past the last +#file("data") write), not from the selection. pardes adds `12:5`: line 12, +*byte* column 5 from 1, clamped to the line's end (`12:0` is `address out of range: a column counts from 1`); it composes (`12:5,14:1`). A tool's +character column matches only on an ASCII line; elsewhere use `12/name/` +or `#n`. A row of #word("Recent"), `+Search` or the #word("Jumplist") +spells a range `12:5-14:2` (through 14:2 inclusive) and #file("addr") +takes it as such. + +Offsets and counts are bytes everywhere (acme counts runes), but every +address lands on a rune boundary: `#n` or `L:C` inside a rune snaps to its +start, a match covers the runes it touches, and a combining mark or a +CRLF's `\r` is addressable alone. + +Refusals: `bad address syntax`, `no match for regexp`, `address out of range`, `addresses out of order` (`#100,#50`), `bad regular expression`. + +sam details kept: `$-1` is the last line when the text ends in a newline, +else the one before; with a final newline the empty place after it is a +line (`1` of an empty text is `#0,#0`, so `Edit 1i/x/` works on an empty +file); `2,1` is an empty range at line 2's start; `/^/` finds the empty +place after a final newline; a pattern that can match empty (`^`) passes +over the match where the search starts. + +== Regular expressions <regexp> + +Patterns are mvzr's (classes, `\d\w\s`, `{m,n}`, lazy `*?`), searched as +sam searches, line by line: `^` and `$` match at any line's start and end, +`.` and `[^...]` never match a newline. The same code (`src/regexp.zig`) +serves addresses, Edit, and normal mode's #key("s") and #key("S"). The +ceiling: + +- The leftmost match wins, but among alternatives the first that matches, + not sam's longest (`/gam|gamma/` finds `gam`). mvzr keeps no submatches, + so Edit's `s` has no `\1`-`\9`. +- A pattern holding `\n` runs over the whole text: there `^` may only come + first and `$` only just before a `\n`, else it is refused. +- An alternation must anchor every branch with `^` or none (`^def|^ ` + works, `^def|x` is refused: `an alternation anchors every branch with ^ or none`). A `$` does not count: `foo$|bar` is fine. +- A class may hold non-ASCII runes (`[éa-z]`, a range up to 256 runes); a + wider range or a negated class with one (`[^é]`) is refused. +- At most 512 operations (about 512 characters, counted after that + rewriting): `bad regular expression: longer than mvzr's 512 operations (about 512 pattern characters)`. +- Each search has a step budget (about 300 ms; each search of an Edit `x` + its own): `regular expression search took too much time, gave up`. What + runs out is exponential backtracking (`a?` twenty times then twenty + `a`s) or a quadratic pattern over a very long line. + +== Edit <edit> + +`Edit <sam commands>` on a pane's #file("ctl") or #file("exec") (or the +root's, at the active pane) runs acme's Edit on the body: addresses as +above, commands `x y g v c a i d s p = m t u` and `{ }`. All changes are +one undo step, applied only if every command succeeds; a failure changes +nothing and fails the write with acme's words (`Edit: no substitution`). +An `x` that finds nothing succeeds silently. `p` and `=` print to the +directory's `+Errors`. Not there: `b B D e r w f X Y`, `< | >`, +`\1`-`\9`. In `s`, `&` is the match (`\&` a literal); in `c`, `a`, `i` it +is a literal. `y` yields the stretch before the first match too. Braces +take a command a line, so a block goes on one open, in one write or +several: +#cmd("printf 'Edit ,x/foo/{\\ni/</\\na/>/\\n}\\n' > $p/ctl") + +== Flags and undo <flags> + +#file("dirty"), #file("mark") and #file("scroll") read and take `0` or +`1`: the buffer differs from its file (a file deleted on disk counts); a +write pushes an undo point (writing `1` pushes one now); a write scrolls +the pane. #file("/index")'s dirty flag is #file("dirty"); a `+New` scratch +reads 1 but holds up nothing under 100 bytes. + +The writes of one open of #file("data"), #file("xdata") or #file("body") +are one undo step (so a multi-line `printf` is one), as long as no other +client or keystroke edits the pane between them: such an edit ends the +step, and the open's next write starts another. To make a loop of opens +one step: `echo 1 > mark` (a point now), `echo 0 > mark`, the writes, +`echo 1 > mark`. + +== event <event> + +Holding #file("event") open takes the pane's look and execute clicks: they +come to the reader as records instead of acting, as do lines written to +the pane's own #file("look")/#file("exec") (or the root's while it has the +keyboard). A record is acme's `<origin><action><q0> <q1> <flag> <n> <text>\n`; read `n` *bytes* of text, which may hold newlines. One open +reads a pane's #file("event") at a time, as acme's is one window's: a +second open for reading fails `file in use` (EBUSY) until the first is +closed. + +- origin: `E` a 9P write to body or tag, `F` other files and the editor's + own lines, `K` keyboard, `M` mouse. +- action: `X`/`L` executed/looked in the body, `x`/`l` in the tag (offsets + into the whole tag, path included), `I`/`D` body text inserted/deleted, + `i`/`d` the tag's. +- flag: 1 the text's first word is a builtin, 4 (look) a file name or + address, 8 (exec) chorded: two records follow, the argument and where it + came from. pardes never sends flag 2. +- A written line, or a click in a terminal's body, has no place: `0 0` + with its text (`FX0 0 1 6 Msg hi`). + +Write a record back to have it done as the click would: `<o><a><q0> <q1>\n` acts on that range (the last record of a write needs no newline), +and an empty one (`MX12 12`) on the word or file name a click there expands +to; the whole record as read acts on its text (the only way for `0 0`). A +chorded record with its two follow-ups, in one write or three, runs once +with its argument. `I`, `D`, `i`, `d` are refused. A helper holding +#file("event") that writes its own pane's #file("exec") gets its command +back as a record: run it through #file("ctl") instead. + +== REPLs <repls> + +`Repl python` on a terminal's #file("ctl") (or in its tag) binds it as +that language's REPL: a #btn("B2") click or #key("Tab") on a `.py` body +then types the text into the REPL instead of running it; builtin words, +tag words, `Exec <text>` and #raw("@`cmd`") words still run. #word("Repl") +takes the languages a code fence names (ada, bash, c, c_sharp, clojure, +cpp, css, elixir, erlang, fortran, go, haskell, html, java, javascript, +json, kotlin, ocaml, markdown, pascal, php, powershell, python, ruby, rust, +scala, typst, zig) and aliases such as `py` and `sh`. Its tag and +#file("ctl") line show its id, `python-a`. `Repl -` unbinds, bare +#word("Repl") says the binding. With several bound, the pane asks (`ask <serial> repl a b`). Bindings are not dumped. + +A 9P #file("exec") is never sent to a REPL. A script either writes the +event record `MX<q0> <q1>` to the `.py` pane's #file("event") (sent as the +click would be), or writes the REPL's #file("pty/data") itself: multi-line +code as a bracketed paste, `\e[200~<code>\e[201~`, then `\r` in a separate +write once the REPL has echoed the paste; a paste of more than one line +not ending in a newline needs a second `\r`. Line by line, a blank line +ends a Python block and Python 3.14 auto-indents each line. + += Terminals <pty> + +Terminal panes also have #file("pty/"): + +- #file("pty/data"): write bytes as typed (`printf 'ls\r'`, `\x03` is + Ctrl-C); read the live output stream (a consuming queue shared by + readers, not a replay). +- #file("pty/status"): one line, `cols rows busy`; busy is 1 while a + command runs or text is typed at the prompt. +- #file("pty/ctl"): `winsize C R` (at least 2 rows, fewer refused `invalid winsize: at least 2 rows`; at most 4096 a side), `sig INT|TERM|HUP|QUIT|KILL`, `exec` (restart the shell in its directory: + refused on a command pane, `a command pane does not restart`; `exec: <dir>: no such directory` if it is gone; a shell that cannot start fails + and leaves the old one running). +- #file("pty/run"): one line at the shell's prompt, answered on the same + open: #cmd("exec 3<>$m/pane/$n/pty/run; echo make >&3; cat <&3; exec 3<&-") + The answer's first line is the header: `exit N` then the command's output + (as the screen showed it: no colour, `\r` progress collapsed, trailing + blanks dropped; the last 64 KiB, `exit N cut M` when M bytes were left + out, bare `cut` when the start scrolled away or was cleared). Or: `busy: <program> is running` (bare `busy` when text is typed at the prompt), + `exit ?` (no status reported, not a success), `error not run` (the shell + refused the line, e.g. a fish syntax error), `error shell gone`, `error no prompt marks`, and on a command pane `error a command runs here, not a shell` (`error command done; not a shell` once it ended). A line written + before a fresh terminal's first prompt waits for it. One line per run; a + second line before the answer is refused. + +#file("pty/run") relies on the OSC 133 marks pardes injects into bash and +fish; `exec zsh` or a continuation prompt never reports an end, so cancel +the read. A program on the alternate screen (vim, less) leaves no output. +A program holding the terminal (a REPL, `less`) takes no run: write to +#file("pty/data"). + += The log <log> + +One 64 KiB ring, one record a line, kept whether or not anyone reads it. +An open freezes it and reads to EOF. Write `follow` to that open to then +wait for each new record (`follow new` skips the history, as `tail -n0 -f`); `tail -f` never writes `follow`, so it sees nothing new. A follower +the ring outran reads `lost N` first. A Restore hangs the follower up: dial +again and read from `restore <path>`. + +#pairs( + [`new <serial> <name>`, `del`, `rename`, `save`], [a pane made, closed, renamed (a terminal's too, as its shell changes directory), saved (`Save path` of a copy: `save <serial> <path>`, the path written)], + [`newcol <serial>`, `delcol <serial>`], [a column made or closed], + [`msg <serial|-> <text>`], [the editor said something (not a builtin's own name under #word("Verbose"))], + [`err <serial|-> <file>: <why>`], [a write was refused or failed], + [`run <serial> <line>`, `exit <serial> <N|?>`], [a command pane's command started and ended; also a terminal whose shell exited, before its `del`], + [`send <from> <to> <repl-id>`], [text went to a REPL], + [`ask <serial> <what> <choices>`, `answer <serial> <choice|->`], [a pane asked, and was answered (`-`: taken back, or the pane closed)], + [`changed <serial> [reloaded|deleted]`], [its file changed on disk (bare: under unsaved edits)], + [`unsaved <serial> <name>`], [a pane an Exit, Restore, Del or Delcol refused over, before the `err`], + [`dump <path>`, `restore <path>`], [a Dump written; a Restore, after its panes' `new`s], + [`restored <old> <new>`, `restoredcol <old> <new>`], [serial maps after a Restore], +) + +The serial is the pane the line ran at (the active pane for the root's +#file("look")/#file("exec")), `-` for the root #file("ctl"), +#file("/tagexec") and column files. A record said again straight after +itself is counted, `err 3 addr: no match for regexp (x4)`; a follower sees +each count as a new line. A `msg` is cut at 256 bytes and an `err` reason +at 200, ending in `…`. Control characters become spaces. + += Other files <other-files> + +- #file("/screen"): JSON `cols`, `rows`, `cursor`, `styles`, and row-major + `cells` of `[grapheme, style_index]`; one frame per open. Compare a + cell's style through `styles`, not the index. +- #file("/commands"): `Word [arg] root|pane|both [values] -- sentence`, one + a line, generated from the builtin registry (`both`: Edit, at the active + pane from the root). +- #file("/recent"): up to 200 files, PDFs and images, most recent first, + `open <path>` or `closed <path>`; kept in `$XDG_STATE_HOME/pardes/recent`. + #word("Recent") shows them in a pane, an open one at its dot now and a + closed one at its last (a PDF's is its page); a look at a row reopens it + there. +- #file("/status"): `pid`, `version`, `panes`. +- #file("/listeners"): the session's dial addresses, a line each (the + building chapter has the listeners). +- #file("/os/"): existing regular files take read, write and truncation to + zero; create, remove, rename and mode changes are refused as not + permitted (`permission denied`, EACCES through a mount); ownership is + synthetic. Linux v9fs's truncation `mtime` hint is accepted and dropped. +- #file("/src/") (and #file("/shaders") on GUI builds) with + `-Dembed-sources=true`; `EffectCode <effect>` lists an effect's files + under `/virtual`. + += Limits <limits> + +#pairs( + [panes], [64 (16 on the board)], + [columns], [16 (6 on the board), each at least 10 cells wide], + [rows a pane keeps], [its tag and 2], + [msize], [65536 (64 KiB) offered], + [connections], [16 Unix and TCP, 16 QUIC], + [opens holding state], [64], + [held reads a connection], [128], + [named mounts], [8], + [command line], [1024 bytes; a held line or Edit block 1 MiB], + [tag text], [4096 bytes], + [regular expression], [512 operations; a step budget per search (about 300 ms)], + [undo], [256 steps], + [log], [64 KiB ring; `msg` 256 bytes, `err` reason 200], + [`pty/run` output], [64 KiB], + [file name component], [255 bytes], +) diff --git a/next-steps.txt b/next-steps.txt index d568c1c8..1f41fc03 100644 --- a/next-steps.txt +++ b/next-steps.txt @@ -1,6 +1,6 @@ This is the original wishlist, not an implementation specification. -Current filesystem behavior is documented in docs/fs.md: default Unix 9P, +Current filesystem behavior is documented in docs/typ/reference.typ: default Unix 9P, optional TCP and QUIC, runtime Mount/Unmount, and OS-first Look resolution. FUSE and the old proof-of-concept examples have been removed. Terminal control and nested Look use 9P; a board build does not imply hardware validation. diff --git a/src/builtins.zig b/src/builtins.zig index 89d89c3a..fb6aa4cf 100644 --- a/src/builtins.zig +++ b/src/builtins.zig @@ -1314,7 +1314,7 @@ pub const Repl = struct { shown += 1; } }; - w.print(" ({d} more: docs/tags.md, Repl)", .{known - shown}) catch {}; + w.print(" ({d} more: docs/typ/reference.typ, REPLs)", .{known - shown}) catch {}; return c.p.reportFailure(c.id, w.buffered()); }; if (pane.repl) |r| if (r.lang == lang) diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig index 5b63080e..31265a94 100644 --- a/src/ninep/ctl.zig +++ b/src/ninep/ctl.zig @@ -3430,7 +3430,7 @@ test "Repl with no such language names a few whole, and where the rest are" { const refused = wr(p, Node.of(serialOf(p), .ctl), "Repl zzlang\n"); try testing.expectEqual(Status.err, refused.reply.status); try testing.expect(std.mem.indexOf(u8, refused.reply.ename, "Repl: no language \"zzlang\"; - or one like zig ada bash c c_sharp clojure (") != null); - try testing.expect(std.mem.indexOf(u8, refused.reply.ename, " more: docs/tags.md, Repl)") != null); + try testing.expect(std.mem.indexOf(u8, refused.reply.ename, " more: docs/typ/reference.typ, REPLs)") != null); } test "a 10k-line Edit text block is taken in linear time and memory" { @@ -3520,7 +3520,7 @@ test "every word's /commands description is its own doc comment's first sentence "\nJoincol root -- Fold the active pane's column into the one on its right, keeping its panes.\n", "\nExit root -- Quit the editor, refusing once while a pane holds unsaved text: an Exit with nothing edited since discards it.\n", "\nColors arg root on,off -- Syntax colours in text panes, on or off.\n", - // fs.md's examples, as they read. + // the reference's examples, as they read. "\nNewcol root -- An empty column right of the keyboard's, its tag taking the keyboard.\n", "\nSave arg pane -- Write the pane's text to its file, or to the file its argument names.\n", "\nVerbose arg root on,off -- A builtin says its own name on the message row as it runs, on or off.\n", diff --git a/test/monkey9p.py b/test/monkey9p.py index 64b82832..7899f25b 100644 --- a/test/monkey9p.py +++ b/test/monkey9p.py @@ -78,7 +78,7 @@ class Dropped(Exception): class Ended(Exception): """pardes quit cleanly: exit 0, no crash file. Closing the session's last - pane quits it (fs.md: 'Closing the session's last pane quits pardes'), + pane quits it (docs/typ/reference.typ: 'closing the session's last pane quits pardes'), and a terminal whose shell exits closes its pane, so this can follow any step; the run starts a fresh session and carries on.""" @@ -671,7 +671,7 @@ def op_write(sess, op): def served_lines(data, sizes, room): """The lines a lines file runs for `data` written in pieces of `sizes` - (fs.md, tree.zig write): each ended line, and the unended tail of a piece + (docs/typ/reference.typ, tree.zig write): each ended line, and the unended tail of a piece shorter than a Twrite holds (`room`, msize less 24), which is whole in itself, as acme takes each write; a piece that fills its Twrite may go on, and its tail waits for the next, and so does one of a multiple of @@ -693,7 +693,7 @@ def served_lines(data, sizes, room): def allowances(res, path, data, sizes, room): - """Errs a successful write may log (fs.md): a look that finds nothing logs + """Errs a successful write may log (docs/typ/reference.typ): a look that finds nothing logs one, per line, whether written to look, written back as an event record (acme's xfid.c:842) or run as the Look builtin; an Edit block whose text never ended runs, and fails, at the close.""" @@ -702,7 +702,7 @@ def allowances(res, path, data, sizes, room): lines = [line for line in served if line.strip()] if base == 'look': # A look's line is its text, blanks and all: one of blanks alone - # is a look too (fs.md); only an empty one (or a lone \r) is none. + # is a look too (docs/typ/reference.typ); only an empty one (or a lone \r) is none. n = len([line for line in served if line.rstrip(b'\r')]) elif base == 'event': n = len(lines) @@ -905,7 +905,7 @@ def op_clunk(sess, op): @op_kind('restore') def op_restore(sess, op): """Write Restore [f] to /ctl: refused with an Rerror, or the server - hangs up and serves the dump's panes again (fs.md: a Restore hangs its + hangs up and serves the dump's panes again (docs/typ/reference.typ: a Restore hangs its connection up).""" res = Result() data = dec(op['data']) @@ -1463,7 +1463,7 @@ COUNT = re.compile(r'^(.*) \(x(\d+)\)$') def occurrences(lines, kind): - """Records of `kind` (err|msg), counting `(xN)` repeats (fs.md: a record said + """Records of `kind` (err|msg), counting `(xN)` repeats (docs/typ/reference.typ: a record said again is counted; a follower sees each count as a line of its own).""" out, last_text, last_n = [], None, 0 for line in lines: @@ -1496,7 +1496,7 @@ def alive(ctx, res): @invariant def one_failure_rule(ctx, res): - """fs.md 'One rule for what fails': a write fails whenever what it asked + """docs/typ/reference.typ 'One rule for what fails': a write fails whenever what it asked for fails ... and logs its reason exactly once, as `err <serial|-> <file>: <why>`, with no `msg` for it. What is not a failure: a look that finds nothing answers nothing and logs one `err`, the write succeeding.""" @@ -1513,7 +1513,7 @@ def one_failure_rule(ctx, res): # Allowed only the Twrites that succeeded (allowances): a failed one # logs its one err. allowed = sum(r[3] for r in res.requests if r[0] == 'allow') - # fs.md 'A write of command lines ... what is left when it closes runs at + # docs/typ/reference.typ 'A write of command lines ... what is left when it closes runs at # the close ... and its failure is in the log alone'. closes = [r for r in res.requests if r[0] == 'close-runs'] lo, hi = len(failed), len(failed) + allowed + len(closes) @@ -1538,7 +1538,7 @@ LINE_FILES = ('look', 'exec', 'tagexec', 'ctl') @invariant def close_runs_only_line_files(ctx, res): - """fs.md: 'A write of command lines -- to look, exec, tagexec, a ctl of the + """docs/typ/reference.typ: 'A write of command lines -- to look, exec, tagexec, a ctl of the root, a pane or a column, or a column's exec -- runs each line once it is whole ... what is left when it closes runs at the close'. Another file that fails only at its close breaks 'a write fails whenever what it asked @@ -1562,7 +1562,7 @@ def close_runs_only_line_files(ctx, res): @invariant def index_matches_pane_dirs(ctx, res): - """fs.md /index: one line per pane; /pane/<n>/ each pane's directory.""" + """docs/typ/reference.typ /index: one line per pane; /pane/<n>/ each pane's directory.""" w = ctx.sess.wire first = w.read_path('/index') fid, err = w.walk_names([b'pane']) |
