diff options
Diffstat (limited to 'docs')
| -rw-r--r-- | docs/cloud9.md | 139 | ||||
| -rw-r--r-- | docs/config.md | 790 | ||||
| -rw-r--r-- | docs/detached.md | 76 | ||||
| -rw-r--r-- | docs/divergences.md | 106 | ||||
| -rw-r--r-- | docs/fs.md | 1689 | ||||
| -rw-r--r-- | docs/open-questions.md | 83 | ||||
| -rw-r--r-- | docs/tags.md | 408 | ||||
| -rw-r--r-- | docs/v9fs.md | 130 |
8 files changed, 1006 insertions, 2415 deletions
diff --git a/docs/cloud9.md b/docs/cloud9.md index 7365bf65..cd92fd05 100644 --- a/docs/cloud9.md +++ b/docs/cloud9.md @@ -1,107 +1,46 @@ -# cloud9 integration +# cloud9 -The published `cloud9` package owns the base 9P2000 wire format, client and server -connections, and TCP/Unix/QUIC transports. `build.zig.zon` pins a commit from -`[email protected]:~gbrls/cloud9`, so `zig build` fetches it into `zig-pkg/` like every -other dependency; no sibling checkout is required. Re-pin with -`zig fetch --save=cloud9 git+https://git.sr.ht/~gbrls/cloud9#<commit>`, and swap in -`.cloud9 = .{ .path = "../cloud9" }` while editing both packages at once. +The `cloud9` package owns the 9P2000 wire format, client and server +connections, the file-server engine and the Unix/TCP/QUIC transports. +`build.zig.zon` pins a commit from `git.sr.ht/~gbrls/cloud9`, fetched into +`zig-pkg/` like any dependency. Re-pin with +`zig fetch --save=cloud9 git+https://git.sr.ht/~gbrls/cloud9#<commit>`, or +use `.cloud9 = .{ .path = "../cloud9" }` while editing both. -The file-server engine (fids, jobs, parking, flush, hangup) is cloud9's -`fs.Server`; the control tree in `src/ninep/` is its backend, using cloud9's -`fs.Req`, `fs.Op`, `fs.Status`, `fs.E` and `fs.ReplyWith` (extended with the -editor's reply payload locator). `src/9p.zig` names the editor's and the -board's `fs.Options` and re-exports the wire names the transports use. -Mounting, Unix namespace discovery and permissions, the editor event loop, -connection limits, and exported tree policy remain here. The Unix and TCP -listeners run on cloud9's `serve.Runner` (`std.Io`: an accept task per -listener, a reader and a writer task per connection, four slots); its handler -answers every backend request on the connection's task, taking the editor's -turn with the core (`pardes.turn`) while the editor waits for input or is out -in a syscall. A request that would change a pane while the editor is mid-step -is parked with `Status.again` -- the engine parks reads, writes, opens, -truncations, clunks and removes -- and retried by `Runner.wakeAll` when the -editor next rests. A read with nothing yet parks the same way, but is never -retried for news: the core holds it (`ctlfs.hold`) and `answerHeld` answers -it through the ticket `Conn.hold` gave its park, with `Conn.answerWith`, -which makes the answer only while that very park still waits (not flushed, -its fid not clunked, not out being retried) and under the same lock. `src/9p_quic.zig` selects the existing `pardes-9p` ALPN for -cloud9's optional OpenSSL transport; QUIC still runs on the poll loop in -`src/9p_io.zig` on the editor's thread, since cloud9's QUIC adapter is -nonblocking-descriptor based rather than `std.Io` based. -The standalone protocol/GPIO tests import the same module. The separate -`05-zig-p4` build also supplies cloud9 for the GPIO firmware entry. +- The engine (fids, jobs, parking, flush, hangup) is cloud9's `fs.Server`; + the control tree in `src/ninep/` is its backend. `src/9p.zig` names the + editor's and the board's `fs.Options` (msize 8192, 256 fids). +- Unix and TCP listeners run on cloud9's `serve.Runner` (`std.Io`: an accept + task per listener, a reader and a writer task per connection, 16 + connections). Requests are answered on the connection's task, which takes + the editor's turn (`pardes.turn`) while the editor waits for input or is + out in a syscall. A request that would change a pane while the editor is + mid-step parks (`Status.again`) and is retried when the editor rests. A + read with nothing to answer yet is held by the core and answered through + the ticket `Conn.hold` gave it, only while that park still waits. +- QUIC (`src/9p_quic.zig`, ALPN `pardes-9p`) still runs on the editor's + poll loop in `src/9p_io.zig`, since cloud9's QUIC adapter is + nonblocking-descriptor based. -Run `zig build 9p-test` for the engine configurations and -`zig build 9p-io-test -Dquic=true` for native transport/client integration. Run cloud9's `zig build test`, -`transport-test`, `quic-test -Dquic=true`, `fuzz`, and `differential` steps for the -shared implementation. See cloud9's `docs/validation.md` for recorded runs and -known test-environment limits. - -Invalid framing now terminates a server connection. Cloud9 also checks reply -counts and reserves tags until flush completion. Client metadata is slightly -larger to track those reservations, and its bounds tests reflect that fixed cost. +Tests: `zig build 9p-test` (engine configurations), `zig build 9p-io-test +-Dquic=true` (transports and client). cloud9's own `zig build test`, +`transport-test`, `quic-test -Dquic=true`, `fuzz` and `differential` cover +the shared code. ## The posted-9P registry -`$XDG_RUNTIME_DIR/9p` is this machine's `/srv`: a server posts itself in it -under a name, and clients dial names rather than paths. cloud9 owns both -sides (`cloud9.post`), and `9ns --mntgen` mounts the whole registry at -`/mnt/9p` for programs that want it as a filesystem. pardes's only part in -it is to put itself there. - -**Serving.** A listening editor advertises itself at -`$XDG_RUNTIME_DIR/9p/pardes/<name>`, a symlink to the socket it already -binds. One directory for the program, one entry per editor, so several -editors group instead of crowding the registry root — the layout zmx posts -its sessions under. The socket itself does not move: adopting the registry -only advertises. Only the runtime-directory socket posts; an instance that -fell back to `~/.local/state/pardes` stays out of the user's registry, the -way a private `ZMX_DIR` does for zmx. Stopping unposts, and only while the -entry is still ours, so a name another editor has since claimed is never -unlinked. - -An exit that cannot run any code of its own — an aborted test, a kill, a -crash — leaves its entry and its socket behind, so posting first sweeps the -group: every entry that is a symlink and whose socket answers a connect with -a definite ECONNREFUSED is unlinked, along with the socket it points at when -a `stat` agrees that is a socket of ours. Anything that is not a symlink is -somebody else's, and any other answer — connected, busy, refused permission, -a surprise — counts as live, because uncertainty belongs to the server that -owns the socket rather than to the sweeper. That is `cloud9.post.Probe`'s -classification, repeated in `src/9p_io.zig` only because `post.probe` is raw -Linux syscalls and pardes also builds for darwin. - -**Consuming.** Nothing. `9ns --mntgen` mounts the whole registry at -`/mnt/9p`, and an interactive fish already self-wraps in one, so a pardes -started from a terminal sees every posted service as ordinary files — -`/mnt/9p/harness/active/...` is read with the same code that reads any other -path. Teaching pardes to dial the registry itself would put discovery in a -second place for no gain: mounting is the client's job and 9ns is the -client. `--mount=<name>=<dial>` keeps meaning exactly what it always did, -and a dial keeps resolving exactly as it always did — a bare name is another -pardes session, and anything with a slash is a path, relative ones included. - -The one case that is not free: a pardes started outside a mntgen mount has -no `/mnt/9p`. That is 9ns's problem to solve — by being in the namespace — -not a reason for pardes to carry its own registry client. - -### What this diverges from, deliberately +`$XDG_RUNTIME_DIR/9p` is this machine's `/srv`: servers post themselves +there by name, and `9ns --mntgen` mounts the whole registry (default +`/mnt/9p`). A pardes whose socket is in the runtime directory posts +`$XDG_RUNTIME_DIR/9p/pardes/<name>`, a symlink to its socket (one directory +per program, as zmx posts its sessions); a socket that fell back to +`~/.local/state/pardes` is not posted. Stopping unposts the entry if it is +still ours. Posting first sweeps the group: an entry that is a symlink whose +socket refuses a connect (ECONNREFUSED) is removed with its socket; anything +else counts as live. -* **pardes binds its own socket; it does not post through `cloud9.post`.** - `post` would give us its hardened claim protocol (temp-bind plus atomic - rename under a lock) instead of the stale-socket retry in `listen`, but it - claims *flat* names only: `legalName` rejects `/`, and `claimName` derives - its lock directory by stripping `/9p` from the registry path, so a name - inside a subdirectory cannot go through it. zmx hand-rolls the same - symlink for the same reason. Unifying them means teaching `post` a group — - passing the lock directory in rather than deriving it — and that is a - change to adversarially-hardened code, not a rename. -* **The registry entry is a symlink, not the socket.** A reader that expects - every registry entry to be a socket must `stat` following symlinks. - `9ns --mntgen` and `cloud9.post.dial` both do. -* **Dialing is untouched.** An earlier draft taught `resolve` to fall back - to the registry for a bare name and to read `<group>/<name>` as a - subdirectory entry. Both were reverted: the second reinterpreted relative - dials, which are a feature, and the first duplicated what 9ns already - does. `src/9p_io.zig`'s `resolve` is byte-identical to what it was. +pardes does not dial the registry: `9ns` is the client, and `--mount` dials +resolve as always (a bare name is a pardes session, anything with a slash a +path). pardes binds its own socket instead of posting through `cloud9.post`, +because `post` takes only flat names and pardes posts into a group +directory. A reader of the registry must `stat` through the symlink. diff --git a/docs/config.md b/docs/config.md index 349814a6..f25c8746 100644 --- a/docs/config.md +++ b/docs/config.md @@ -1,79 +1,20 @@ -# Startup configuration +# Configuration -Native pardes builds use a per-user `pardes` configuration directory. Its -main command file is named `init`: +## The startup file -- Unix: `$XDG_CONFIG_HOME/pardes/init`, falling back to - `~/.config/pardes/init`. -- macOS: `$XDG_CONFIG_HOME/pardes/init` when that variable is set, otherwise - `~/Library/Application Support/pardes/init`. -- Windows: `%LOCALAPPDATA%\pardes\init`, with - `%USERPROFILE%\AppData\Local\pardes\init` as the fallback. - -On the two unixes `XDG_CONFIG_HOME` counts only when it is ABSOLUTE, as the -XDG base-directory specification requires; an empty or relative value falls -back to the home-directory form (`config.User.path`, and the test beside -it). Windows never consults it. An `init` that does not fit the `max_bytes` -read limit — 1 MiB, `config.User` in `src/config.zig` — or that cannot be read at all is -treated as no file: `load` takes the `readFileAlloc` error and keeps going. The -path still resolves, because "nothing is there yet" is the answer `Config` -exists to give. There is one case with no path at all: a native launch with no -`HOME` set (or, on Windows, neither `%LOCALAPPDATA%` nor `%USERPROFILE%`), -which `Config` reports as `no per-user config path`. - -`Config` (`SPC f c`, or the word executed anywhere) opens one refreshable -`+Config` pane. It reports the startup path and every live config-like value: -theme, colors, focus tint, syntax weight, wrapping, tag position, debug mode, the requested shell and the -executable actually resolved at the last spawn, requested/effective GUI font -and size, tagline scale, window opacity, ligatures (SDL GUI only), panel transition, -scene effects, hover delay, platform, native-image support, and (on SDL) whether -the executable uses live-built shaders or the paired prebuilt shader snapshot. -Platform-dependent rows say `unsupported` instead of looking like an off or -empty supported setting. The fields of `config.Runtime.Capabilities` gate -them and are stated once as plain data in `builtins.capabilities`: -`font_picker` is the SDL GUI and -native macOS only, `scene_shaders` the same two, `panel_transitions` every -hosted shell, `window_opacity` the SDL GUI and macOS, `window_blur` macOS -only, `ligatures` the SDL GUI only, and `tagline_font_size` -everything but the TTY and the board. `ligatures` is the exception to -`unsupported`: where it is off, `Ligatures` is not a builtin at all and -`Config` has no row for it. So -the TTY reports Font, TaglineSize and the scene shaders as unsupported; the -browser reports Font, panel transitions and the scene shaders as unsupported, -and its TaglineSize row reads `82 (build-time only)` — tagline font size is -its own capability precisely because GUI font SELECTION is native-only while -the browser still applies the compiled percentage to its DOM glyphs. -The startup path is printed whether or not a file exists — that is usually when -it is most useful — and is ordinary selectable text, so a right click on it -opens the file. Shell follows the same requested/effective/pending model as -Font. `Default shell` is the one used while no `Shell` is set: `$SHELL`, the -user's login shell, else `/bin/sh` (also when `$SHELL` names nothing -executable); an explicit `Shell` overrides it. `Shell <name or path>` is -refused unless it names an executable file (`Shell: shell "x" not found (...)`, or -`Shell: not a shell: /etc is a directory`; a bare name is looked for in the -usual bin directories), as `Tty <shell>` is, and a -bare `Shell` goes back to the default. The root ctl reads `Shell <the one the -next terminal runs>`. `Shell -effective (last spawn)` is the executable the native host really chose after -installation lookup and fallback. A changed request remains pending until a -terminal is spawned, because the core does not resolve native executables. +Native builds read one command file, `init`, from the per-user `pardes` +configuration directory: -The mutable global values live together in the plain `config.Runtime` record. -One plain capability record gates the setting registry, leader table, -`EffectCode`, and report; the compile-time setting table generates both setter -builtins and their `Config` rows. Exhaustive checks require every table-backed -toggle, transition, and scene-effect switch to occur exactly once, so those -generated setting builtins cannot quietly lose their query row or leave a -renderer switch unnamed. Manual pane-local actions remain with their payload (for -example an image tag reports its renderer choices); they are not global -configuration. +- 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 no local user-config path and does not load this file. -(Nor does it have `Font`, the effect builtins or `EffectCode`, a language -backend, or ptys of its own — see `docs/web.md`.) +The browser build has none. A file over 1 MiB or unreadable counts as absent. -The format is one existing builtin command per line, using the same spelling -and argument parsing as commands executed inside pardes: +Each line is one builtin, spelled as it would be executed in pardes: ```text Theme orchard @@ -83,620 +24,171 @@ Shell zsh Wrap ``` -A line matches a builtin whose name takes NO argument only as that whole word: -`Kill` runs, `Kill something` does not. A builtin that takes one -(`takes_arg` in `src/builtins.zig`, or a `settings` row whose action is -`shell`, `theme`, `font`, `tagline_size` or `window_opacity` in `config.Runtime.settings`) takes -everything after the name as the argument. On a native build that is `Theme`, -`ThemeFile`, `Font`, `TaglineSize`, `Shell`, `Save`, `Restore`, `Attach`, -`Mount`, `Unmount`, `Find`, `Grep`, `Rename`, `WsSymbols`, `Look`, `Exec`, -`Msg` and `EffectCode`. -The SDL GUI also has `WindowOpacity`, which takes one argument. -(`Peek`, `Poke`, `Hexdump` and `Gpio` take one too, but they exist only where -`builtins.Board.enabled` holds, and that build has no config file.) - -`Theme <name>` wants one of the names in the compiled ring. Do not derive the -spelling — read it off `Themes` (`SPC t t`), which lists every one as the -exact `Theme <name>` line that selects it. `slug` in `tools/gen_themes.zig` -lowercases, folds punctuation runs to a single `_` and then TRIMS leading and -trailing ones (`penumbra+.toml` is `penumbra`, not `penumbra_`), and every -variant read out of a zed `.json` gets `_zed` on the end so it cannot collide -with a helix theme of the same name — zed's "Ayu Mirage" is `ayu_mirage_zed` -and `ayu_mirage` is helix's `ayu_mirage.toml`. The suffix goes on all of them -rather than only the eight that clash today, so a name cannot move when either -project gains or loses a file. A name that is not in the ring is ignored. - -The fifteen [native Pardes themes](themes.md) lead the ring: `orchard` (the -default), `dusk`, `ink`, `paper`, `daybreak`, `atelier`, `forge`, `lagoon`, -`solarium`, `spectrum`, `harvest`, `clay`, `forge_black`, `forge_soft`, and -`orchard_black`, then `acme` and `lapis`. `Themes` lists them first under -Pardes themes. They add coordinated -focus, search, diagnostic and terminal colors; `ink` and `daybreak` are high -contrast dark and light options. After them come the -[faithful ports](themes.md#faithful-ports) of well-known themes, a section per -family, then the legacy `helix` and `dark`, then everything imported. Where a -port takes an imported theme's name (`dracula`), the imported one gains -`_helix` (`dracula_helix`), as zed's carry `_zed`. `NextColor` walks the same -ring in the same order. - -`FocusTint` toggles the focused pane and column tag tints; it is on by default. -Workspace, column and pane command text can be edited directly; see -[editable tags](tags.md) for naming and saved-workspace behavior. -The column command row is always shown; an old `ColumnTags` init line is -ignored with a message. -`Placement acme` (the default) puts new panes where acme would, in the active -column; `Placement pardes` brings back pardes's own rules, which open a first -document in a column of its own. See [where new panes go](tags.md#where-new-panes-go). -`BootShell keep` (the default) leaves a shell alone when a document is dragged -into the left column beside it, whatever it holds; `BootShell replace` closes -such a shell there when it is the column's only one and nobody has typed -into it (no scrollback, cursor still on the first prompt line), the boot's -placeholder giving its rows to the document. Bare `BootShell` flips it, as -bare `Placement` does: a setting that chooses among words (`on`/`off` -switches, `Placement`, `BootShell`, `Crt`, `Bloom`, `Vignette`, `Grain`, `Lift`, `Motion`, -`ShaderAnimation`) steps to its -next value when written bare, as its word clicked in a tag does, and a value -it does not take is refused with the values it takes, which `/commands` also -lists. -`Verbose` toggles the message-row announcement every builtin makes of its own -name before it runs; it is on by default, and the builtins that own the message -row themselves (`Msg`) never announce. Turning it off leaves the row to the -messages a builtin chooses to write. -`MessageAnimation` toggles how a message comes and goes; it is on by default. -A message eases down into its row, fast at first and settling at the end, its -colour whole by half way (graphical frontends slide it out from under the -tagline as it fades up; a terminal only fades it in), stays until the next key or click as it -always has, then lingers for `MessageLinger` milliseconds (default 800) before -it dissolves into the page. `MessageFall` (default 180) and `MessageDissolve` -(default 150: leaving is quicker than arriving) set how long the fall and the dissolve take, also in -milliseconds; each timing is at most 60000, and `Config` reports all three. `MessageLinger 0` with -`MessageAnimation off` restores the old behaviour, a message cleared by the -very input that follows it. An updated line on a row already showing one swaps -its text in place rather than falling again. -`SyntaxBold` toggles bold syntax keywords; it is off by default. These -settings are shared by GUI and TTY, and `Config` reports their current -states. Like `Colors` and `Wrap`, these commands take no argument and invert -the current value. A `FocusTint` line in a fresh startup configuration disables -the tint; a `SyntaxBold` line enables the stronger keyword weight. Those two -appearance controls leave focus, selections and editing behavior unchanged. - -`Ligatures` (SDL GUI only) toggles a font's programming ligatures, such as -Maple Mono's `->` and `!=` drawn across their cells; it is on by default. -Off, every cell draws its own glyph, exactly as a font without ligatures does. -`Ligatures on` and `Ligatures off` set it explicitly. The native macOS shell -draws CoreText ligatures of its own, which this setting does not reach. - -`TreeContext` toggles sticky declaration headers for the current source pane. -It is off by default and appears in the default pane tag when a tree-sitter -grammar supports that file. `TreeContext on` and `TreeContext off` set it -explicitly. Scrolling inside a function, type or module keeps its enclosing -declarations above the body, with source line numbers, syntax colors and a -subtle background tint. Clicking a header moves the cursor there without -scrolling; scrolling upward reveals that source line as the headers recede. -`Dump` and `Restore` preserve the setting per pane. Customized tags retain -their text; the command can still be executed from any source pane. - -`TreeContextTagStyle` toggles the experimental tagline treatment for those -headers and is on by default. In graphical frontends it uses the tagline font, -line height and thin border. A matching separator marks gaps between the -headers' source lines. Turning it off restores body-sized context rows. -`TreeContext` itself still defaults off; this appearance option does not enable -it. `Config` reports the appearance option, and dumps preserve it. - -Search, Grep and LSP location-result panes include `LocationsConfig` in their -default tags. Custom tags keep their edits. - -Location highlighting is enabled by the command that produces a location list. -Plain reports, including `LocationsConfig`, keep ordinary text colors even -when their text resembles a location. `Mini` keeps its own syntax colors. -LSP references and goto results highlight the exact symbol span supplied by -the server, using the search-result colors. Source indentation is retained so -those byte ranges stay aligned; context and descriptive labels remain unmarked. - -`LocationsConfig` prints the current settings for Search, Grep and LSP location -results in an output pane. Execute the printed line to apply it again, or -supply just the fields to change: - -```text -LocationsConfig context:5 tscontext:on tslocations:off layout:stacked -``` - -- `context` is the number of source lines above and below each match (default - `0`). Overlapping context is shown once. These neighboring lines omit their - locations and keep their code aligned with the matching result. -- `tscontext` includes enclosing tree-sitter declaration headers in source - order (default `off`). Preceding neighboring context stops at the outermost - enclosing declaration, keeping unrelated lines above it out of the result. -- `tslocations` shows a location on the first line of each declaration header - (default `on`); continuation lines keep the same alignment without repeating - the location. Declaration headers use the same muted color as locations, with or - without their locations visible. -- `layout:stacked` (default) puts each location on its own line, followed by its - source preview. Hidden context locations do not add an empty address line. - Use `layout:inline` to put locations beside the source instead. +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. -In inline layout, result locations share padding in groups of eight matches, so a long path only -widens its own group. Context does not count toward the eight; at group boundaries, -it aligns with the nearer match (ties stay with the preceding group). Source -indentation is preserved. All visible locations start flush left; `n` and `N` -still stop on matches. -With `tscontext` enabled, asterisks after a match location show its declaration -depth (for example, `main.zig:42 ***`). Ordinary neighboring source lines retain -syntax colors; declaration headers do not. -A declaration context line ends with ` ...` when source lines are omitted -before the next displayed row from that file. -Source analysis is reused across result queries while the source bytes stay the -same. The cache retains at most 64 files and 64 MiB; it checks current buffer or -filesystem contents on each refresh. -Open file buffers supply context from their current edits. Missing files still -leave the original result available. These settings apply to subsequent result -generation and survive `Dump`/`Restore`. Invalid fields reject the entire -update; if a field appears twice, its last value wins. +`Config` (`SPC f c`) opens a `+Config` pane with the startup path (a right +click opens it) and every live setting; a setting the frontend cannot show +reads `unsupported`. The root `ctl` file reads the settings back in the +words a write takes ([fs.md](fs.md#the-root-ctl)). -`WindowOpacity <percent>` controls the opacity of everything in the SDL window -except text and the cursor, which stay fully opaque. Use `WindowOpacity 85` -to see the desktop through the editor, or `WindowOpacity 100` to restore full -opacity (the default). The argument must be a whole number from -0 through 100; missing or invalid values -leave the setting unchanged. At zero only text and the cursor remain visible. -Add the command to `init` to persist it. +## Settings -The same opacity applies to editor and embedded terminal backgrounds, UI -chrome, borders, scrollbars, gutters, and images. Overlapping non-text drawing -does not make those areas more opaque. Regular text, syntax colors, tagline -text, terminal glyphs, and the cursor keep their normal opacity. This does -not blend foreground colors into their cell backgrounds. The TTY -and other non-SDL hosts do not emulate this effect; their `Config` report says -`WindowOpacity unsupported`. +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. -On native Wayland, Pardes uses an alpha-capable transparent surface. It does -not use whole-window opacity protocols such as `wp_alpha_modifier_v1`, because -those would also fade the text. Presentation uses the existing GPU offscreen -renderer followed by a readback and SDL renderer upload per presented frame, -which adds rendering cost. Other SDL drivers can report background transparency -as unsupported unless their rendering configuration is alpha-capable. -If the rendering backend cannot apply the request, Pardes reports the error -and keeps the last successfully applied opacity. `Config` reports the -percentage and marks a request as pending until the SDL host handles it. - -## Crash records - -A panic appends to `crashes` in that same directory, beside `init`, and only -then prints to stderr (`src/crash.zig`, wired into the panic handlers in -`main.zig` and — because the macOS build roots there — `macos.zig`). stderr is -the one place this program cannot keep a trace: in the TTY shell stderr IS the -screen, so the trace lands on a grid the terminal is being reset out of; the SDL -and AppKit shells have no terminal at all; and a `--detach` session's stderr -goes wherever its launcher left it. The file is appended, never rewritten, and -each record is two lines — build metadata, then the panic message: - -```text -pardes 0.0.2 (a1b2c3d) 2026-09-03T11:20:44Z linux-x86_64 pid 48812 -panic: index out of bounds: index 4, len 4 -``` - -NO STACK TRACE, and that is a measured decision rather than an omission. The -frames stay on stderr, where `std.debug.defaultPanic` prints them. Collecting -them here instead HANGS the process: `writeCurrentStackTrace` called from a -panic handler before `defaultPanic` has run wedges at 0% CPU, and -`captureCurrentStackTrace` — which looks like the safe half — takes `SelfInfo`'s -rwlock exclusively on its first call, so a panic inside the walk leaves that -lock held and `defaultPanic` then waits on it forever. What makes `defaultPanic` -survive the same hazard is its own private `panic_stage`, which nothing outside -`std.debug` can reach. A crash that becomes a hang is worse than the crash, so -this file keeps only what it can gather without asking the process any -questions: which build, when, where, and what it said. - -Everything about it is best effort and silent: no config directory (a launch -with no `HOME`) means no file, and a directory that cannot be created or opened -leaves the panic exactly as it was before — stderr alone. The directory itself -is created if it does not exist, because the user who never wrote an `init` is -as likely as any other to hit a bug. One record at a time: two threads panicking -at once would otherwise interleave into one buffer, so the second falls straight -through to stderr. Only panics come here; a SIGSEGV is caught one level lower -(`main.zig`'s `debug.handleSegfault`) and unwinding one needs the signal's saved -CPU context. - -## Runtime theme files - -`ThemeFile <path>` loads one complete theme from a `.zon` file. An absolute -path is used as written; a relative path is resolved from the `pardes` -configuration directory, not from the process working directory. A typical -layout is: - -```text -~/.config/pardes/ -├── init -└── themes/ - └── mine.zon -``` - -and the corresponding init line is: - -```text -ThemeFile themes/mine.zon -``` - -After a successful load, hosts with document live reload watch the path with -the same parent-directory mechanism, so in-place writes and editor-style -rename-over saves reload the theme live. A malformed or incomplete save does -not replace the last valid theme; fixing and saving the file applies the next -valid snapshot. Selecting a compiled theme with `Theme <name>` or `NextColor` -stops the custom-file watch. - -Execute `DumpThemes` to write every theme compiled into the executable to: - -```text -<config directory>/themes/builtin/<name>.zon -``` - -The command replaces those generated reference files but leaves unrelated -files alone. Copy one into `themes/`, rename it, change its `.name`, and use it -as the starting point for a custom theme. The dumped file is also the complete -format, and it is `pardes.Theme` serialised by `std.zon.stringify`: the theme -name, thirteen required RGB roles (`sel_bg`, `sel_fg`, `tag_bg`, `tag_fg`, -`box`, `box_dim`, `kw`, `str`, `num`, `comment`, `lineno`, `scroll_track`, -`scroll_thumb`), nullable `bg`/`fg`, and either a 16-color RGB `palette` or -`null`. Optional nullable RGB roles extend this format: -`tag_active_bg`, `tag_active_fg`, `tag_name_fg`, `tag_active_name_fg`, `border`, `empty_col`, `lineno_active`, `search_bg`, `search_fg`, -`diagnostic_error`, `diagnostic_warning`, `diagnostic_info`, -`diagnostic_hint`, `tag_sel_bg`, `tag_rule`, `box_border` and `box_dirty`; -`sweep_bg` and `sweep_fg` take three triples, and `rule_px` and `rail_px` a -pixel count. Missing new roles use backward-compatible defaults, so -previously exported files remain valid. [Theme customization](themes.md) -describes each role and its fallback. RGB values are three-byte arrays, and -hex literals are accepted. The original fields remain required; there is no -inheritance or partial override layer. - -`tag_name_fg` gives the filename at the end of a pane's path a separate -foreground. `tag_active_name_fg` can adjust that tint for active tags; it falls -back to `tag_name_fg`. When both are omitted or `null`, the filename uses -the corresponding tag foreground. All canonical themes use a different hue -at similar perceived brightness to the surrounding text in both states. -Directory text and tag commands retain -their regular colors; selecting filename text uses the selection colors. -Terminal tags use this color for `Tty`, which comes immediately after the path. - -`Filter` in a terminal's tag projects that pane's ANSI colors through the -active theme, and does it in two stages. The default foreground and background -roles are mapped FIRST, because ghostty-vt generates the whole 256-color -projection from that pair; every other color follows, by reducing it to its -nearest canonical xterm key and reading the key back out of the projection. - -That reduction compares RGB triples, so it knows about hue and nothing about -the page — and the projection's cube corners ARE the two anchors, which is how -a foreground used to end up painted the exact color of the paper behind it -(`\x1b[38;2;255;255;255m` on acme's `#ffffea`, and the ANSI black a shell -writes with `\x1b[30m` on either dark theme). So a foreground additionally has -to keep `tty_filter_min_contrast` — a WCAG ratio, `1.5` by default — against -the mapped background. One that cannot is not mapped: it takes whichever of -the theme's own two anchors is still visible on that background. Backgrounds -are exempt, since a background is the page the floor is measured against. Set -the constant to `1.0` to accept every projected color, collapses included. - -`Shell <name>` sets the binary that the NEXT terminal pane execs; panes -already open keep the shell they are running. A bare name is resolved against -the handful of directories a shell actually lives in, not `$PATH`. - -On Linux, `Tty9p` (`SPC n 9`) starts that shell with a private kernel 9P mount, -asking sudo inside the new terminal. `$PARDES_MOUNT` names the mountpoint. -The installed `pardes-v9fs` helper lives beside the editor; development builds -can set `PARDES_V9FS_HELPER` to its absolute path. See [v9fs.md](v9fs.md). - -Ctrl-B switches between raw TTY and editor mode. Plain Escape at a detected -shell prompt hops back to the previous pane. Other keys, including Ctrl-O, -Ctrl-W and modified Escape, belong to the child. Use `Mode` in the pane tag to -return to editor mode in place. Desktop paste events still feed the terminal. - -The paste chords are the exception the window keeps. Ctrl-V types the yank -register at the program, and Ctrl-Shift-V asks the desktop for its clipboard -and types that; both go through the program's bracketed paste when it has -asked for one. Neither reaches the child as a keystroke, so an application -that would otherwise answer Ctrl-V by reading the system clipboard itself -never gets the chance to read the wrong thing. - -`Font` and `Fonts` exist ONLY in the SDL GUI and native macOS builds — a -terminal's font belongs to its emulator and a browser's to the page — so a -`Font` line is one of the silently-ignored ones everywhere else. Both builds -resolve the name by walking the font directories on every lookup, so a face -installed a moment ago is findable. - -Use `Font <name>:<size>` to change face and size together, for example -`Font MartianMono-NrRg:18` in the startup file or an editable tag. Fractional -sizes such as `:18.5` are supported; the accepted range is 8–72 (pixels in SDL, -points on macOS). `Font <name>` without a suffix preserves the current size. -Invalid sizes or unknown faces leave the current font unchanged. - -The SDL GUI also has a tiny optional workspace-tag companion: `Pet cat`, -`Pet frog`, or `Pet off` (the default). Its original pixel sprite walks and -idles in the trailing blank area of the global tag only. It hides while that -tag is being edited, when the tag is full, or when the font leaves too little -room; it never replaces text, receives clicks, or appears in pane/column tags. -Animation runs at ten steps per second, uses theme ink, and requires no image -assets or shaders. Put `Pet cat` in the startup configuration to keep it; -`Pet off` disables the companion and its animation. Other hosts ignore the -startup command. `Config` reports the current choice in SDL. +| 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 | -`Font` is asynchronous at the renderer boundary. `Config` therefore keeps -requested name/path/size, pending state, and the effective face/point-or-pixel size -as separate facts; a failed request never gets reported as the face on screen. -Taglines use a distinct face size in both native GUI renderers. Execute -`TaglineSize <percent>` to change it live, for example `TaglineSize 70`; the -accepted range is 1 through 100 and the default comes from -`gui_tagline_font_percent` in `src/config.zig` (82). The native renderer -remeasures both the glyph and its visible tag band while retaining body-grid -pane geometry. Graphical tag text uses the smaller face's measured monospace -advance and fills the available pixel width, including workspace and column -tags. Text selection and scrolling use that same capacity; drag grips retain -their physical body-grid width and position. The 100% ceiling is -deliberate: a tagline remains exactly one logical grid row, so a larger face or -band would overlap its pane body or a neighbour instead of leaving the body -grid stable. `Config` reports the active percentage. The browser applies the -same compiled percentage to its DOM glyphs but has no runtime setter. +`Tty9p` (`SPC n 9`) is described in [v9fs.md](v9fs.md); the +`PARDES_V9FS_HELPER` variable points development builds at the helper. -Both native GUIs separate the reduced-height global and pane tagline bands with -a `gui_topbar_pane_border_px` physical-pixel rule. Set it to zero to leave it out. -`gui_topbar_pane_border_rgb` can pin an RGB color; its default `null` follows -the theme's `border` role in SDL (falling back to `scroll_track` for older -themes), and `scroll_track` in the macOS shell. Every band is centred in its -row, so the workspace, column and pane tag text share one baseline offset and -the anchors get the same margin above, below and to the left. With `Tagbottom` -enabled, a tagline on the final grid row is bottom-aligned so the unused -half-band does not show beneath it. The rule lives in the core -(`pardes.taglineBandOffset`), and the macOS shell reaches it over the C ABI -rather than keeping its own copy. When the window is not a whole number of cells, -bands and rules at the right and bottom edges run on through the leftover pixels. +### Terminals -What happens to a codepoint the chosen face has no glyph for differs by shell. -The SDL GUI falls back through a chain it builds itself: embedded Adwaita -Mono, then installed `NotoSansMono-Regular`, `DejaVuSansMono`, -`SymbolsNerdFont-Regular`, `NotoSansSymbols2-Regular`, -`NotoSansSymbols-Regular`, and `DejaVuSans`, in that order, missing entries -skipped; the rasterized glyphs are retained in its GPU atlas. The macOS shell -has none of that and needs none. It embeds no font. Its default face is -`NSFont.monospacedSystemFont`, resolved through the descriptor rather than by -name, and `Menlo` only if that face turns out not to be fixed-pitch — a -proportional face in a fixed grid is a broken screen, not a cosmetic problem -(`PardesView.defaultFace`). A `Font <name>` arrives as a PATH the core already -resolved by walking the font directories, and a file CoreText cannot measure -leaves the face already on screen rather than substituting one, because the -alternative is a terminal with no way back out (`PardesView.fromFile`). -Per-codepoint fallback is CoreText's own cascade at draw time. What the shell -caches is `CGGlyph` ids, not pixels. +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. -Blank, unknown, malformed, or unsuccessful lines are ignored silently, and a -bad line does not prevent later lines from running. Top-level text that is not -a builtin is not sent to a shell. (`Exec ...` remains an ordinary builtin and -therefore keeps its normal behavior.) Key bindings remain compile-time choices -in `src/config.zig`; this startup file does not remap them. +`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. -## Panel and scene effects +### Location results -Exactly one panel transition is selected at a time. Executing its builtin a -second time turns it off; selecting another replaces it: +`LocationsConfig` with no argument prints the current settings as a line +that can be run again; with fields it changes only those: ```text -PanelSlide -PanelZoom -PanelDissolve -PanelAscii -PanelVertical -PanelEdges -PanelFall -PanelWave -PanelCurtain -PanelScramble -PanelType +LocationsConfig context:5 tscontext:on tslocations:off layout:stacked ``` -All panel transitions start off. To disable one, execute its builtin again: -`PanelDissolve` turns off an active dissolve, and `PanelAscii` turns off an -active ASCII transition. `Config` shows the active command under -`Panel transition`. Remove that command from your startup configuration to -keep it off after restarting. Executing a different transition enables that -one instead; these commands are toggles, not an unconditional animation-off command. +- `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. -Slide uses cubic ease-out and zoom uses an -overshooting ease-out-back. Dissolve and ASCII compare the last successfully -presented grid with the new one. Dissolve switches visually changed cells at -stable noise thresholds. -ASCII walks every changed single-byte printable glyph -from its old `u8` value to its new one, spending that byte distance as the -frames of the walk. The walk is eased in and out: a glyph creeps at both ends -and crosses the middle of its distance in a few large skips, inside the same -number of frames a constant one-value-per-frame walk would have taken. -Glyph-stable style changes -and non-ASCII graphemes become canonical immediately. A walk is capped at -twelve movement frames, and each pane lasts only as long as its longest walk. -The core computes and composes that semantic diff once for every backend; -pixel attachments, which have no character value, pass through unchanged. +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. -Six further transitions are character *motion* over the same frozen/new grid -pair, and are composed in the core the same way: +### Effects -- `PanelEdges` — whole rows slide in from alternating screen edges. -- `PanelFall` — columns rain down into place, each with its own head start. -- `PanelWave` — a vertical ripple travels across the pane and decays. -- `PanelCurtain` — a wipe, left to right, behind a soft edge two cells wide. -- `PanelScramble` — every cell churns through printable ASCII and locks onto - its final glyph at its own stable noise threshold. -- `PanelType` — reading-order reveal with a caret sitting on the write head. +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. -Unlike dissolve and ASCII these carry *every* glyph in the pane, changed or -not: text flying in from a screen edge has to bring its unchanged glyphs with -it. A cell whose glyph has not arrived shows the frozen old cell rather than a -blank or a blend, so every intermediate frame is made of real characters. No -cell is a valid input target until its own glyph has settled. +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. -Vertical is a pane-lifecycle effect: a newly added -pane rises from below inside its own fixed box, and a deleted pane's frozen -content drops back down; surviving panes are never animated. The TTY -implementation performs its remaining geometry/dissolve operations directly -on a copy of the core-composed presentation grid. -The SDL and native macOS GUI implementations pass plain panel tracks to their -GPU shaders, including native image/PDF pixels; layout itself commits -immediately and remains the one authoritative geometry. DOM web intentionally -does not expose these builtins: its renderer is selectable HTML/CSS and has no -canvas or shader stage. +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)). -The scene effect is `Crt`, at a level from 0 (off) to 3; `on` is 2: +`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. -```text -Crt -Crt 3 -``` +### Look preview -The SDL GUI runs it as the bundled pass of its post chain, which also takes -Shadertoy files written for ghostty (`Shader ~/crt.glsl`, `Shader off`; -a file compiles again when it is saved, and a save that fails to compile -keeps the last good one and says why), and -`ShaderAnimation off|on|always` says when the chain animates on its own. A -GUI attached to a detached session runs the session's chain the same way. With -the chain empty the pass is bypassed. CRT works in linear light with restrained -scanlines, mask, bloom and vignette, and no curvature, so clicks land where -they are drawn. Its slow hum and dither move on their own, so with Crt on the -GUI keeps drawing while idle (under `ShaderAnimation on`, while focused); -the other bundled passes are still and let it rest. +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. -Three more bundled passes take the same levels, each off by default and -each costing nothing while off: +## Themes from files -```text -Bloom 2 the brightest ink glows a little (only what is brighter than - the page; a dual Kawase blur at half size and down) -Vignette 2 the window's corners fall a little into shade -Grain 2 the page's own ground takes a fine, still grain, like paper -``` +`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. -None of them touches a tag, a grip, a notice or the cursor, and each is -capped per theme so text and the selection keep their own contrast or 4.5 -(docs/effects.md). - -The focused pane can stand off the page (SDL GUI, off by default): - -```text -Lift shadow a soft drop shadow, on the other panes' bodies only -Lift rim a hairline just above the focused tag (light, or shade on a light page) -Lift auto a shadow on a light page; on a dark one the others recede -Lift off -InactiveDim 30 the unfocused panes' text fades 30% toward its ground -Motion smooth off, crisp, smooth (the default), bouncy or playful -SelectionGlow on a soft halo of the selection's colour round it in the body, - fading in over 100 ms, never over a tag, a grip or the cursor -HoverGlow on a soft glow under the word a look-hover would open, fading - in over 80 ms -Occlusion on pane bodies darken faintly toward their edges (2%), never - over the cursor -Parallax on a theme's page pattern (lapis's dots) moves with the text - at a quarter of its speed -JumpTrail on in a terminal, a jump of the cursor of three cells or more - leaves a trail of a few cells that fades in 120 ms - (truecolor terminals; a pixel shell glides instead) -ChipShadow on in a terminal, a notice chip casts a shadow a cell right and - down: half blocks on blank cells, a darker ground on text -ThumbFlash on in a terminal, a pane's scroll thumb brightens as it scrolls - and fades back over 250 ms -CursorBlink on the cursor blinks, solid while typing and half a second - after, eased at each edge, solid after 10 idle seconds -GripWidth 150 the grip's button and the scrollbar under it, percent of the - theme's rail_px (12px at a 17px tagline; 50 to 300) -``` +## Dumps -`InactiveDim` works everywhere (a grid shows it at once) and never takes a -pair below its own contrast or 4.5. Under `Lift auto` on a dark page it is 30 -while unset. `Motion` sets how every such effect moves: see docs/effects.md. +`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. -`EffectCode PanelAscii` or `EffectCode Crt` lists the current backend's -build-embedded source paths under `/virtual`. Look opens each full file; -no checkout is needed, but the build must carry them (`-Dembed-sources=true`, -off by default); otherwise the command reports them unavailable. TTY exposes grid transitions, native GUI builds also -expose scene shaders, and web has neither. Shared implementations share paths. -SDL reports whether GLSL was compiled during this build or came from the -`-Dprebuilt-shaders` snapshot paired with the committed SPIR-V. -`zig build shaders` refreshes both files of every pair together, -so editing live GLSL without that explicit refresh changes neither half of a -prebuilt executable. +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. -## Delayed Look preview - -The preview is enabled by default. Moving the pointer onto selectable text and -leaving it still for `look_preview_delay_frames` — 2 animation ticks, and -`animation.frame_ms` is 16, so about 32 ms — paints a subtle theme-derived -preview of the exact operand a right-click Look would receive. -Repeated motion reports in the same semantic grid cell do not restart the -delay. The preview uses the same side-effect-free word/path expansion as Look; -it does not focus a pane, move a cursor, install a selection, activate a PDF -page, or execute anything. Motion to another operand, pointer leave, input, -pane teardown, and relevant content changes cancel it. - -Set this compile-time option in `src/config.zig` to disable the feature: +## Crash records -```zig -pub const look_preview_delay_frames: ?u16 = null; -``` +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-time configuration +## Build options -Runtime settings live in `src/config.zig`. Build options are listed below; -`zig build --help` lists the options available for the selected platform, -including the standard `-Dtarget` and `-Doptimize` options. +`zig build --help` lists the options for the selected platform. | option | values | default | |---|---|---| -| `-Dplatform` | `tty`, `gui`, `web`, `macos`, `esp32p4` | absent builds the tty cli and the SDL gui together | +| `-Dplatform` | `tty`, `gui`, `web`, `macos`, `esp32p4` | absent: the tty and SDL shells together, installed into `~/.local` | | `-Dstatic` | bool | `false` | -| `-Dquic` | bool; 9P over QUIC using system OpenSSL 3.6+ | `false` | -| `-Dmupdf` | bool | on for a native target, off for web and esp32p4 | -| `-Djpx` | bool | `true` — JPEG 2000, and with it scanned PDFs | +| `-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 | -| `-Dtheme-animation` | bool | on everywhere except `-Dplatform=esp32p4` | -| `-Dworkspace-tag` | bool; draw the workspace tag row — the macOS shell hands its commands to the native menu bar instead | on except on `-Dplatform=macos` | -| `-Dprebuilt-shaders` | bool | on for a bare `zig build`, off when `-Dplatform` names a shell | -| `-Dtracy` | path to a Tracy source checkout | off | +| `-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` | substring; run only tests whose name contains it | none | -| `-Dtest-rebuild` | bool; force fresh Zig test compilation, retaining cached C dependencies | `false` | -| `-Dhelix-harness` | native reference executable for live differential tests | `HX_HARNESS`, otherwise `hx-harness` on PATH | -| `-Desp32p4-cols` | u16, the board's grid width in cells | `56` | -| `-Desp32p4-rows` | u16, the board's grid height in cells | `14` | - -The local board build emits an object. Firmware clock, serial port and profiling -options belong to the sibling `05-zig-p4` toolchain's build. - -Two build inputs reach the running binary as ordinary values rather than as -behaviour. `build.zig` reads `.version` from `build.zig.zon` through an untyped -`@import("build.zig.zon")` — one place to bump — and `gitCommit(b)` reads the -revision at configure time. Both land in `pardes_config` and are re-exported as -`pardes.version` and `pardes.commit`, and `pardes --version` prints -`pardes <version> (<commit>)`, or `pardes <version>` alone when there is no -commit: a tarball, a container with no `git`, or a checkout outside version -control all yield null, and the flag has to work anyway. Neither is a question -asked at runtime — a binary that shelled out to `git` would describe whatever -tree it was standing in rather than the one it came from. - -`Restore a.dump` first looks for the relative path in the dump directory: -`DumpDir <path>` when set (a leading `~/` is your home; bare `DumpDir` returns -to the default), else `$XDG_DATA_HOME/pardes` or `~/.local/share/pardes`. -`Config` reports the directory in effect as `DumpDir <path>`, so the line can -be fed back as configuration. If it is absent, Restore -uses the argument as a path as before. Absolute paths and argument-free Restore -retain their existing behavior. `Dump` still honors `$PARDES_DUMP` and otherwise -writes a timestamped file in the default directory. - -A dump keeps each text pane's dot (its selection, or the caret), as acme's -keeps a window's q0 and q1, and a Restore puts it back where the kept view -shows it. It keeps the settings that differ from a fresh session's (`Placement -pardes`, `Verbose off`, a shader), as the root ctl reads them, and a Restore -sets them again; the theme is kept with the layout, and the font stays the -frontend's. REPL bindings are not kept. +| `-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 | -A restored terminal comes back live: its recorded output (the last MiB of it) -is replayed as history, a dim `── restored history ──` line marks where it -ends, and a new shell starts below it in the directory the old one was in, -the same shell it ran (a `Tty bash` pane comes back running bash). A -view left scrolled back stays where it was. Only the shell is new; nothing the -old one was running is restarted. A command pane comes back finished, with -what it printed and its `exit N`, and is not run again (a command still -running when dumped comes back as `exit ?`). The web shell, which has no ptys, still -shows the history alone. +The version comes from `build.zig.zon`'s `.version`. diff --git a/docs/detached.md b/docs/detached.md index 46e93fd5..e010677c 100644 --- a/docs/detached.md +++ b/docs/detached.md @@ -1,7 +1,7 @@ # Detached sessions -A detached session owns the editor core, pane shells, files, undo history and -layout. TTY and SDL frontends can join and leave without ending the session. +A detached session owns the editor core, pane shells, files, undo history +and layout. TTY and SDL frontends join and leave without ending it. ```sh pardes --detach=work & @@ -9,61 +9,41 @@ pardes --attach=work pardes-gui --attach=work ``` -Bare `--detach` names the session after its process ID. Bare `--attach` -requires exactly one listening session. The detached process runs in the -foreground unless the shell backgrounds it. +Bare `--detach` names the session after its pid; bare `--attach` needs +exactly one listening session. The detached process runs in the foreground +unless the shell backgrounds it. Inside an editor, `Attach work` (`SPC s a`) +switches this window to that session and `Detach` (`SPC s D`) closes only +this frontend. -Inside an editor, `Attach work` (`SPC s a`) switches the current window to -that session. `Detach` (`SPC s D`) closes only that frontend. It does not -turn a local editor into a detached session. - -A message stays until the next key or click dismisses it. With no frontend -attached there is none to come, so a detached session drops each pane's -messages once the newest has been up `MessageLinger` on the clock, checked -by the session's own loop between the editor's steps; /screen then shows -what is current. +With no frontend attached, `/ctl`'s `size C R` sets the screen (160x50 until +then; [fs.md](fs.md#the-root-ctl)), and messages clear after `MessageLinger` +on the clock, since no key will come to dismiss them. ## Files and transport The frontend socket is `pardes-detached-<name>.sock` under -`$XDG_RUNTIME_DIR`, or `~/.local/state/pardes` when no runtime directory is -set. A second session cannot replace a live listener with the same name. -Shared Unix socket handling lives in `src/9p_io.zig`. - -The session also opens its default 9P socket. Optional TCP and QUIC listeners, -runtime mounts and the control filesystem belong to the session, not its -frontends. See [fs.md](fs.md). +`$XDG_RUNTIME_DIR`, else `~/.local/state/pardes`, beside the session's 9P +socket `pardes-9p-<name>.sock`. A second session cannot take a live name. +TCP and QUIC listeners, mounts and the control filesystem belong to the +session, not its frontends. ## Ownership -One poll loop owns all core mutation, frontend connections, PTY I/O and file -watch notifications. LSP and selection-pipe workers own request snapshots and -post completions through a bounded mailbox. Each subprocess is reaped by its -owner; PTY reaping cannot consume a language server or filter's exit status. - -Restore constructs a replacement core before changing the current one. It then -joins old work, clears obsolete completions, replaces panes and watches, and -sends a fresh frame to the existing frontends. - -Frontends provide input and presentation. Clipboard writes are broadcast; -clipboard reads, browser opens and Detach go to the originating frontend, -falling back to the primary attachment. Frontends never spawn pane shells, -write session files or install file watches. +One poll loop owns core mutation, frontend connections, PTY I/O and file +watches; LSP and selection-pipe workers post completions through a bounded +mailbox. Restore builds the replacement core before changing the current +one, then sends existing frontends a fresh frame. Frontends provide input +and presentation only: clipboard writes are broadcast; clipboard reads, +browser opens and Detach go to the frontend that asked (else the first +attached). Frontends never spawn shells, write session files or watch files. ## Wire and tests -`src/detached/wire.zig` owns the versioned frontend protocol. Frames are full -grids or changes relative to each frontend's last queued frame. A new -attachment receives a full grid. Output queues and per-poll work are bounded; -a lagging frontend cannot hold the session's event loop. Protocol version 6 -also carries the pointer shape, compact source-context body layers, source-gap -separators and independently sized tag text. Pointer -updates work even when no grid cells change, and graphical clients use the -same compact row and tag geometry for drawing and mouse input. Tag text positions -are separate from physical pane and drag-handle coordinates. Terminal clients retain -the ordinary fixed grid. +`src/detached/wire.zig` owns the versioned frontend protocol (version 8). +Frames are full grids or changes against each frontend's last frame; a new +attachment gets a full grid. Queues are bounded, so a lagging frontend +cannot hold up the session. -`zig build unit-test` covers encoding, session ownership, real frontend -connections, worker completion and Restore. `zig build fs-test` drives -detached sessions through an independent 9P client. The snapshot suites also -exercise attach, detach and shared screen behavior. +`zig build unit-test` covers the wire, ownership, real frontend connections +and Restore; `zig build fs-test` drives detached sessions over 9P; the +snapshot suites exercise attach, detach and shared screens. diff --git a/docs/divergences.md b/docs/divergences.md index 6679d962..6560368c 100644 --- a/docs/divergences.md +++ b/docs/divergences.md @@ -1,94 +1,26 @@ # Divergences -What is not on `main`, and what on `main` is known to be wrong. Written so -that moving a bookmark does not quietly orphan work or hide a failure. +What is not on `main`, and what on `main` is known to be wrong, so that +moving a bookmark does not quietly orphan work or hide a failure. -## Two lines, forked at `01104e7c` +## Bookmarks off `main` -`main` is not the only living line, and the other one is not behind it — -they are siblings: +| bookmark | | +|---|---| +| `reload-perf-wip` | unfinished: Reload presentation transport regression | +| `reload` | reload core code with shell-owned allocators | +| `reload-start` | names the Core/Shell seam, deferred Reload request | +| `tty-colors-mouse`, `vibes-ghostty`, `mouse` | older prototypes (divergent), also on the `vps` remote | -``` -◆ rruwvuzm 09-17 editor work: syntax, panes, modal, gui, fs, output -│ ◆ xqxpolmw 2b547e15 main 09-20 "Serve Unix and TCP 9P through cloud9.serve" -├─╯ -◆ lsnxpxtq 01104e7c 09-16 "Add macOS backdrop blur and preserve PDF ink opacity" -``` +`ninep`, `fx`, `macos-fix`, `full-prototype`, `term`, and the `before-*` and +`merged-*` bookmarks mark points already in `main`'s history. Only +`main` is pushed to the FreeBSD mirror, which serves a public site. -* **`main`** carries the 9P work: the `cloud9.serve` runner, and now the - posted-9P registry (`docs/cloud9.md`). -* **`rruwvuzm`** carries editor work — roughly 1300 lines across - `src/syntax.zig`, `src/panes.zig`, `src/modal.zig`, `src/gui/gui.zig`, - `src/fs.zig`, `src/pardes.zig`, `test/output.zig`, `docs/fs.md`. Reach it - with `jj edit rruwvuzm`. +## Known wrong on `main` -The fork matters for one concrete reason: **the cloud9 pin lives on `main` -only**. `rruwvuzm` still pins `ae310a20` (2026-09-14), `main` now pins -`9c4d668c`. Rebasing or merging the editor line will want the newer pin, or -`zig build` there fetches a cloud9 that predates `fs.Server`'s current shape. - -## Other bookmarks - -| bookmark | | | -|---|---|---| -| `reload-perf-wip` | 09-11 | unfinished: Reload presentation transport regression | -| `reload` | 09-10 | reload core code with shell-owned allocators | -| `reload-start` | 09-10 | names the Core/Shell seam, deferred Reload request | -| `ninep` | 08-27 | a 9P design note and a design registry to argue it in | -| `macos-fix` | 07-23 | | -| `full-prototype`, `term`, `tty-colors-mouse`, `vibes-ghostty`, `mouse` | 06-xx | older prototypes, also on the `vps` remote | - -None of these are published to the FreeBSD mirror: only `main` is pushed -there, deliberately, because that box serves a public site. - -## Known-failing on `main`, not caused by the 9P work - -* **`zig build snap` fails `nested-optout`, and the cause is a real bug.** - At the SECOND level of nesting -- a pardes whose shell runs a pardes whose - shell runs a command -- two U+E016 codepoints are prepended to whatever is - typed. U+E016 is 57366, which is vaxis's private-use spelling of **F3** - (`Key.zig`, "kitty encodes these keys directly in the private use area"), so - something in the chain is decoding a capability-query REPLY as a key and - forwarding it to the shell as text. bash then sees - `$'\356\200\226\356\200\226echo'` and answers `command not found`; with a - path it answers `No such file or directory` for a path that exists and runs - perfectly from the same shell a moment later. - - Minimal repro, in a snapshot script: - - ``` - dirmk innerdir - file innerdir/deeper.txt deepest-marker\n - start 31 100 - wait 8000 $ - stable 700 20000 - text (cd innerdir && $(readlink /proc/$PPID/exe) --nested) - key enter - wait 20000 cwd/innerdir - stable 700 20000 - text E=$(readlink /proc/$PPID/exe); "$E" deeper.txt - key enter - settle 5000 - stable 700 10000 - snap probe - ``` - - `bash: E=/home/.../pardes: No such file or directory` -- bash did not even - parse the assignment, because the line does not start where it looks like it - starts. `--nested` is exactly the flag meant to opt a child out of this, so - the fix belongs next to the key-forwarding path in `panes.Terminal.forwardKey` - and whatever answers terminal capability queries on a nested stdin. - -* **pardes does not start headless.** `pardes --9p=<name>` with no terminal - exits 1 from the argument-forwarding path; the installed build fails - earlier still, with `error.NoDevice` opening a terminal device. So the - registry posting above is covered by unit tests - (`zig build 9p-io-test`) rather than by running the editor. - -## Upstream - -`build.zig.zon` pins cloud9 `9c4d668c`. That commit exists because pinning -cloud9 `534c084f` here failed: cloud9's `build.zig` `@import`s each program's -build fragment, and `9harness` was missing from its `.paths`, so the -published package built from a checkout and not from a tarball. The pardes -build was the first consumer to notice. +- `Dump` of a session with more than 6 columns fails `Dump: bad dump + columns` (`src/dump.zig` `max_cols` is 6; a session holds 16). +- `pardes FILE` in a pane forwards only a FILE that exists; a new file name + starts a separate editor instead of opening a pane in the session. +- `Dump` fails `no such directory` when the default `DumpDir`'s parent + (`$XDG_DATA_HOME`, or `~/.local/share`) does not exist. @@ -1,1142 +1,659 @@ # Filesystem -Every native session serves 9P2000 on a Unix socket. Pane shells receive -`PARDES_PID` (the editor's process id), `PARDES_9P` (socket path) and -`PARDES_PANE` (pane serial). The socket is -`$XDG_RUNTIME_DIR/pardes-9p-<pid>.sock`, or lives under -`~/.local/state/pardes` when XDG_RUNTIME_DIR is unset. Detached sessions use -their session name; `--9p=<name>` overrides it. +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. -A `pardes <file>` launched from a pane forwards Look to that pane over 9P. -`PARDES_PID` alone says the shell is inside pardes; `PARDES_9P` and -`PARDES_PANE` say how to reach it, and a launch that has the first without the -other two refuses rather than opening a second editor. `--nested` opens a -separate editor and withholds `PARDES_PID` from its pane shells, so a pardes -started in one of them runs a session of its own; its 9P service stays -available. +## Connecting -A forwarded launch returns at once, as acme's `B` does. `--wait` (`-w`) -returns only once the pane the file landed in -- the one already showing -it, if any -- is deleted (exit 0), or the session goes away (exit 1), as -acme's `E` does: what an `$EDITOR` must do, since fish's Ctrl-O -(`edit_command_buffer`), `git commit` and `crontab -e` read the file back -when the editor exits. Set `EDITOR='pardes --wait'`; `GIT_EDITOR` follows -`EDITOR` when unset. Outside pardes `--wait` changes nothing, a session -blocking anyway. +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)). -Look resolves the OS filesystem first, then the editor's virtual filesystem. -Explicit paths bypass that search: +Pane shells get `PARDES_PID` (the editor's pid), `PARDES_9P` (the socket) +and `PARDES_PANE` (their pane's serial). -| Editor path | Meaning | 9P server path | -|---|---|---| -| `/n/os/proc/self` | OS filesystem | `/os/proc/self` | -| `/n/self/pane/2/body` | pane 2's text | `/pane/2/body` | -| `/virtual/pane/2/body` | the same, in the editor's own spelling | `/pane/2/body` | -| `/virtual/src/pardes.zig` | source embedded in this build | `/src/pardes.zig` | -| `/n/peer/pane/2/body` | another session's text | peer's `/pane/2/body` | +**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. FILE must already exist; a name that does not +resolve, or a missing `PARDES_9P`/`PARDES_PANE`, starts a separate editor +instead. Bare `pardes` in a pane refuses and names `--nested`. -The mount name `self` is reserved and maps to the server root, so `/n/self/X` -and `/virtual/X` both name the served `/X`. +`--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. -`--mount=peer=work` mounts the named session `work`; the dial can also be an -absolute socket path, `unix!/path`, `tcp!IP!port`, or `quic!IP!port`. -At runtime, use `Mount peer dial` and -`Unmount peer`. Mount dials the peer when it mounts it and fails its write -if nothing answers, `Mount peer /tmp/s: dial failed: no answer` (or `timed -out`, `hung up`), mounting nothing; a peer that goes away later is found out -by the next use, as `look: /n/peer/f: dial failed: no answer`. There are eight named mounts; `os` and `self` are reserved. -Unmount refuses mounts still used by a pane, its working directory, or a -pending Save. Mounts are saved in dumps. Save uses the file's original mount. +**Clients.** -`pardes --9p-tcp='tcp!127.0.0.1!5640'` adds a TCP listener alongside the Unix -socket. Build with `-Dquic=true` and system OpenSSL 3.6+ to enable QUIC; -`--9p-quic='quic!127.0.0.1!5641'` adds its listener. Both accept numeric -IPv4/IPv6 addresses, not DNS names. Listener port zero chooses a free port; -`/listeners` reports all active dial addresses. +```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 +``` -All connections have session access, including `os`. TCP is unencrypted. -QUIC uses an ephemeral TLS identity without peer verification or login. -It carries 9P2000 on one bidirectional stream with ALPN `pardes-9p`. -Unix and TCP connections share sixteen slots served by cloud9's `std.Io` -runner; QUIC has sixteen of its own on the editor's poll loop. A client -that finds every Unix/TCP slot taken gets an Rerror `too many connections` -to its Tversion, and the log an `err - 9p: too many connections` record. OpenSSL's -internal buffers are separate, dynamically allocated memory. +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 client](https://9fans.github.io/plan9port/man/man1/9p.html) can -drive Unix or TCP without a kernel mount, and `9ns` mounts the tree in a -private namespace: +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. -```sh -9p -n -a "unix!$PARDES_9P" read index -9p -n -a 'tcp!127.0.0.1!5640' ls pane/1 -9ns --unix "$PARDES_9P" -- sh -c 'cat "$NINE_MOUNT/index"' -``` +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: -(Under `9ns --unix`, `$NINE_MOUNT` is that session's root itself.) +| 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` | -A `9ns --unix` mount lives in the private namespace of the command it runs, -and nothing outside that command sees it. The mount everyone on the machine -shares is the registry one, `9ns --mntgen` (default `/mnt/9p`): every running -editor posts itself there, so `$NINE_MOUNT/pardes/<pid>/` is that editor's tree -for any process, and `$NINE_MOUNT/pardes/NAME/` a `--detach=NAME` session's. -9ns exports `$NINE_MOUNT` to everything it starts, so a script checks that -variable to know the mount is there, and takes the name from `$PARDES_9P` -(`pardes-9p-<pid or NAME>.sock`). A new pane made through `pane/new` is a scratch named -`<dir>/+New` until it is given a name, where `<dir>` is the session's -directory (the one pardes started in), whichever pane last had the keyboard: -no pane asked for it, as acme's new window has acme's directory. So is a -`New` written to a column's `exec` or to `/tagexec`, a word in a tag no -pane owns. A column may hold no pane, as in acme: -`Newcol` makes one empty, and closing a column's last pane leaves it empty -with its tag holding the keyboard (`focus` reads empty) and logs only the -`del`. `pane/new` places its pane as acme's makenewwindow(nil) does: in the -active column, filling it when it is empty, else taking the bottom half of its -last pane ([where new panes go](tags.md#where-new-panes-go)). `Delcol` closes the column. Closing the -session's last pane quits pardes; see [tags](tags.md#empty-columns). +`--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. -For [Linux v9fs](https://www.kernel.org/doc/html/latest/filesystems/9p.html), -use `version=9p2000,cache=none,access=any` and `trans=unix`, or `trans=tcp` -with `port=5640`. Set `uname`, `dfltuid`, and `dfltgid` for the local user. -Leave `aname` empty. The opt-in -[Linux v9fs experiment](v9fs.md) tests a kernel mount in a separate subprocess -namespace (`zig build v9fs-test`, requiring explicit mount authorization). -Neither 9P2000.u nor 9P2000.L is implemented. -Existing Plan9port/v9fs clients need a userspace bridge for QUIC. +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 served tree +## The tree ``` -/README this guide, also src/fs-help.txt -/index one line per pane: serial, kind (text|term|pdf|image), dirty flag, name, column serial - (the name as the log shows it: one line of UTF-8, a newline in it `\n`, - a backslash `\\`, a byte not UTF-8 `\xNN`) -/status pid, version and pane count -/look write a line: a right click on it at the active pane; read: the serials the last - look, exec or ctl write touched (made, else acted at) -/exec write a line: a middle click; read the same serials -/log recent events, one a line: new|del|rename|save <serial> <name>, msg <serial|-> <text>, - dump|restore <path>, err <serial|-> <file>: <why>; - write follow to that open to wait for more -/screen rendered screen JSON; frozen per open handle -/listeners the session's dial addresses -/focus the serial of the pane with the keyboard; write a serial to give it the keyboard, - which makes its column the active one as a click there would -/ctl the settings, one a line as a write takes them; write a setting or a session builtin; - `size <cols> <rows>` sets the screen of a session no frontend is attached to - (`--detach`, 160x50 until then; refused while a frontend owns the size), - from 20x6 to 4096x4096 (outside that, `invalid size`, EINVAL), and refused - when a column has not the rows for its panes' minima, each its tag and 2 - rows, the minimum placement keeps; so a size once taken is taken again, - and growing is never refused. A pane the resize took under its minimum - gets its rows back from its column's others; a terminal's pty follows its - pane on every resize -/commands every builtin: word, `arg` if it takes one, `root`, `pane` or `both` (the ctl that takes it: - Edit is both, at the active pane from the root; a pane's word such as Undo or Msg - is refused at the root), a setting's values, then ` -- ` and what it does -/recent the files opened lately, closed ones too, most recent first, a line each: - `open <path>` or `closed <path>` (read-only; `Recent` shows them in a pane, - a look at a row reopening the file at its last dot; kept across sessions - in $XDG_STATE_HOME/pardes/recent, 200 files, the oldest closed one dropped - first, never an open one; only files on disk, not a name never saved nor - /virtual/; a name escaped as /index's is; acme has none, its dump and - Load the nearest) -/layout one line per column (16 at most; the board 6; Newcol past that fails, - `no space for a column: 16 max`, ENOSPC; and each at least 10 cells - wide, so Newcol from a column under 20 fails `this one is too - narrow to split`, ENOSPC; the root's Newcol halves the active column, - so reaching 16 means running Newcol from the widest column's tag), - left to right: serial index x width current|notcurrent - (the column with the keyboard now) empty|full pane-serials...; then active - <serial>: acme's activecol, which the keyboard leaving for another column's - tag does not move, so the two can differ -- the active column, where - pane/new and a look place a pane next (- when there is none) -/tag the workspace tag; > replaces it, >> appends, one line; a control character - but a tab (a NUL too), DEL, a C1 control or bytes not UTF-8 are refused, `invalid - tag text: ...` (EINVAL), in any tag, a pane's or a column's too -/tagexec write a word: a middle click on it in the workspace tag; read as /exec. A - pane's word (Undo, Msg, Save, Edit too) is refused there and at a column's exec, - `not a session control message "Undo": write it to pane/<n>/ctl` (EINVAL), - never done at the pane with the keyboard; a tag's own words (New, Tty, - Find, Grep in a column's) run -/col/<n>/tag the tag of the column with serial n, the same way -/col/<n>/ctl write Delcol, Joincol, New or Tty: each acts on that column, as from its tag -/col/<n>/exec write a word: a middle click on it in that column's tag; read as /exec; rmdir col/<n> closes - an empty column (a column with panes is refused, ENOTEMPTY) -/pane/new open it to make a pane (the bottom half of the active column's last - pane, acme's coladd; with no room there, last all the same, taking half - the tallest pane's rows); the read answers that pane's serial. A session holds - 64 panes (16 on the board); at that, every route that would open one -- this - open, look, exec, New, Tty -- fails with `no space for a pane: 64 max` (ENOSPC through 9ns, which has - no word for ENFILE) and an err - record, and look reads back empty; a column with no room for one - (each pane keeps its tag and 2 rows) refuses it the same way, - `no space for a pane in that column` (docs/tags.md) -/pane/<n>/ name body tag ctl addr dot limit data xdata sel dirty mark scroll - errors event look exec tagexec (a word as a click in its tag), plus - pty/{ctl,status,data} on terminals -/os/ the host filesystem -/src/ the editor's embedded sources, only when built with -Dembed-sources=true +/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) ``` -`/layout`, `/tag` and `/col` go past acme, which serves no column files -- -its columns are only where a window sits. They are here so a script can see -where the panes are (`/index`'s last word is each one's column serial) and -edit the tags a person clicks in: a column tag is one line, a newline -written into it a space, and a truncating write (`echo Make > col/3/tag`) -clears it and drops the newline that ends it, as a pane tag's does. A -column is named by its serial, as a pane is: it stays while the column -lives, whatever opens or closes beside it, and is never reused; /layout -gives each column's serial and its index left to right, and the log says -`newcol <serial>` and `delcol <serial>` as columns come and go, and after a -Restore `restoredcol <old> <new>` for each column as `restored` does for -panes. Joincol folds a column into the one on its right, which keeps its own -serial and tag; the joined column's panes go below that column's own, in -their order, and its serial is gone (`delcol`). The root ctl -takes no column word: its `Delcol` is refused, pointing at -`col/<serial>/ctl`. +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. -Control messages are split by what they act on, as acme keeps window verbs -on a window's ctl and webfs and upas/fs keep session settings on a root ctl. -Each builtin declares its scope in src/builtins.zig (`scope = .session`; -every setting is one, the rest act on a pane). `/ctl` takes the session's -builtins, one a line, at whichever pane has the keyboard as each runs -- -`Newcol`, `Dump`, `Mount name dial`, `Theme ink`, `Verbose off`; `Exit`, -which quits the editor as acme's does (it refuses once over the panes with -unsaved text, a `+New` scratch of 100 bytes or more too, whether it came -through a `ctl` write or a click: first one `/log` record per pane, -`unsaved <serial> <name>`, then the write fails with one line that is -never a list cut short, `<name>: Modified (Exit again to discard)` for one -pane, `4 unsaved panes: Modified (Exit again to discard)` for more, and -the `err` record says the same. On screen the notice is short, `3 unsaved -panes — Exit again to discard`, and the whole list stays in one `+Unsaved` -pane, filled again by each refusal: a row a pane, `<name>: Modified` (a -`+New` scratch with its serial, as scratches share a name, `/dir/+New -(pane 12): Modified`), then `Exit again to discard`. Restore, -Del and Delcol refuse the same way, with their own word; an `Exit` after more editing refuses -again naming only the panes edited since the last refusal, as acme's does, -and an `Exit` with nothing edited since quits, throwing all of it away; a scratch or a -command's output under 100 bytes is not asked about, as acme's winclean -asks about no small unnamed window (so a scratch's `dirty` of 1 in `/index` -blocks nothing until it holds 100 bytes: it has no file to be out of step -with, and a few lines typed to try something are not work to lose); `Restore`, which replaces every pane, -asks the same first -- `Dump` writes `pardes-<date>-<time>.zon` (UTC) in -`DumpDir` (default `$XDG_DATA_HOME/pardes`, else `~/.local/share/pardes`) -and logs `dump <path>`, and `Restore` with no path takes the last one; a -Restore puts a new editor under every client, so the write of it is -answered and then every connection is hung up, their fids naming the old -editor's panes (a 9ns older than cloud9 2a7137c could fail the write with -ECONNRESET all the same, when another request's send met the hang-up -before the answer was read; the log is the authority): dial again -- a 9ns -mount is one such connection, so after a Restore stop it and start 9ns -again, or every file under it fails -- and the new -log names the restored panes and -`restore <path>`. Undo history does not survive a Restore: a restored pane -starts with none. Restored panes have new serials (`restored <old> -<new>` maps them) and so may columns (`restoredcol`; a fresh editor counts -column serials from 1 again, so they often come back the same). A command -pane comes back showing what it showed, its tag saying how it ended, -`exit 0` (`exit ?` for one still running when dumped, whose end nobody -saw): its command is not run again. REPL bindings are not dumped. The answer has 200 ms to leave before the cut, so a slow -client may see only the cut; the log's `restore <path>` is what says the -Restore happened. Keeping connections across it would mean carrying serials -and opens into the new editor, which acme, whose Load only adds windows, -never needed); and `Kill`, which -does not quit but stops commands, as acme's does: bare, every command pardes -started, and `Kill make ls`, those whose line begins with one of the words. A -command pardes started is a command pane's, until its child exits -- Kill -sends SIGTERM to its running job, to its shell and to every `&` job the -line started, so the whole command stops and leaves nothing running (of -`sleep 30; echo done` the `echo` never runs; an `&` job outlives only a -command that exits on its own) -- or a line it typed into -a terminal (a -word written to `exec`, a middle click on one, a `pty/run`), from its shell's -start mark (C) to its end mark (D), where Kill sends its foreground job SIGTERM -- acme posts the -"kill" note, which ends a process -- and never signals the shell itself; -in a shell running without job control (`set +m`) the job shares the -shell's group, so there is none to signal: Kill says `Kill: no job to -signal`, and a write of it to `ctl` fails with that; with nothing running -it says `Kill: nothing running`; and, of a typed line, only the -foreground job, after which what the rest of the line does is the shell's -affair: of `sleep 30; echo done` typed at a prompt, bash (saying -`Terminated`) and fish (saying `Job 1, 'sleep 30' terminated by signal -SIGTERM`) both go on and run the `echo`: SIGTERM, unlike an interrupt, -ends only the job, not the line -- -and reads every setting there is, one a line, in the words a write of it -takes (`Verbose on`, `WindowOpacity 70`, `PanelSlide off`, `DumpDir -<the directory in effect>`, `LocationsConfig ...`), so writing what it reads -back changes nothing; a setting the frontend cannot show (`Lift`, -`GripWidth` on a terminal) is refused as `Lift is GUI-only, invalid here` -(EINVAL: a request this build cannot take); acme's own words run as -pardes's where it has one -- `Put` is `Save`, `Delete` a `Del` that does -not ask -- and the rest (`Get`, `Putall`, `Snarf`, `Cut`, `Paste`, `Zerox`, -`Sort`, `Load`, `ID`, `Send`) are refused, EINVAL, `invalid: acme's Snarf -is not a pardes builtin`, never run as shell commands; and so is a builtin only the -GUI has (`Fonts`), written to an `exec` too, where it would otherwise run as -a shell command; a `Shell` or `Tty` naming no -shell says `shell "x" not found` (ENOENT); a `DumpDir` whose last -directory is missing has it made at the Dump, and one further up missing -says `Dump <path>: no such directory` (ENOENT), one that is no directory -(`/dev/null`) `Dump /dev/null/pardes-<time>.zon: /dev/null is not a -directory`, each naming the dump file it would have written; platform and startup facts are `/status`'s and the -Config window's, not settings. A pane's `ctl` takes the builtins that act on -a pane (`Del`, or `Del k`/`Del j` to give its rows to the pane above or -below, `Save f`, `Collapse`, which folds that pane, `Undo` and -`Redo`, which step its body through its last 256 edits -- with none left -they say `Undo: nothing to undo` and the write still succeeds, as acme's -Undo is silent --, `Find pat`) -beside acme's `get`, `lock` and `unlock`; acme's other ctl words are taken -too, done by the file or builtin that replaces each: `name x` (the `name` -file), `put` (`Save`), `clean` and `dirty` (`dirty`), `del` (`Del`), -`delete` (a `Del` that does not ask), `dot=addr`, `addr=dot`, -`limit=addr`, `mark`, `nomark` (`mark`), `show` and `cleartag`; `dump`, -`dumpdir`, `font`, `menu` and `nomenu` are refused with why, EINVAL. The column words are pane words -too, acting on the column that pane is in: `Delcol`, `DelAbove`, `DelBelow` -and the focus moves `Left`/`Right`/`Up`/`Down` from it. `Joincol` and -`Newcol` are the root's: they act on the column of the pane with the -keyboard, and `Joincol` needs a column to its right (`Joincol: no column to -the right`). The words are case-sensitive and do not alias: acme's verbs -are lowercase and the builtins keep their tag spelling, so `Get` is no word -and `del` none either. +`/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`. -A write is checked whole before any line of it runs, and a line is refused -in Plan 9's words for a ctl (kernel/misc/parse.c:82-97), quoting the line: -`unknown control message "X"`; `wrong #args in control message "X"` for an -argument to a builtin that takes none, or none to one that needs it (`Msg`, -`Mount`, `Find`, a setting's value but a switch's, which flips bare); -`bad value in control message "X"` for a setting's value it does not take -(for `Theme`, whose names are any case, naming every theme that shares -the name's first letter when they fit the 128 bytes a 9P error carries, -else the nearest few, since all of them, `Themes`'s list, are too many); -and `not a session control message "X": write it to pane/<n>/ctl` or `not -a window control message "X": write it to /ctl` for a word of the other ctl. 9ns maps them all to EINVAL, and a write -refused here has done nothing. A line that then fails as it runs fails the -write with the error the editor reports for it, in its own words, which -name what failed, the line not quoted after them (only a line refused -before it runs, as no message at all, is quoted), e.g. `Mount: already -mounted` (EIO), and `control message needs its -argument "Save"` (EINVAL), for a builtin that would have asked at a prompt -(a `Save` on a scratch) rather than open one nobody is there to answer. A -builtin that means nothing without its argument (`Mount`, `Msg`, `Find`), -written bare to an `exec` or `/tagexec`, is refused the same way, `wrong -#args in control message "Mount"` (EINVAL), before it runs. The -lines before a failing one have taken effect and those after it never run, -which is what acme's ctl loop does (editors/acme/xfid.c:600-790). An error -that only happens as the editor performs what a line asked for -- a `Save` -whose disk write fails -- fails the write too, once the editor has tried -(`Save /root/x.txt: access denied`, which 9ns reads as EACCES, a shell's -`Permission denied`), and changes nothing: a scratch -keeps its name and stays a scratch, a clean file stays clean, a dirty one -dirty. Like any write, a ctl write answers once the editor has -performed what it asked for (a save written, a shell started). A click on -the same word, or the word written to `exec`, still opens its prompt. +## Rules for every file -`/commands` lists every builtin the registry holds, in registry order, one -a line: its word, `arg` when it takes one, `root`, `pane` or `both` for the -ctl that takes it, for a setting that chooses among words those words, -comma-joined, then ` -- ` and one sentence of what it does (a builtin's doc -comment's first sentence, a setting's doc), e.g. +**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 +`tag: over 4096 bytes`, EIO. The `err` record has the words; a shell sees +only the errno, most often `Invalid argument` or `Input/output error`. -``` -Newcol root -- An empty column right of the keyboard's, its tag taking the keyboard. -Save arg pane -- Write the pane's text to its file, or to the file its argument names. -Verbose arg root on,off -- A builtin says its own name on the message row as it runs, on or off. -Placement arg root acme,pardes -- Where a new pane goes: acme, as makenewwindow does, or pardes, the older rules. -``` +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 8192, 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`). An `Edit` whose `{` or +`a`/`c`/`i` text is still open waits for the next write on that open, and +fails at the close if it never ends (``unmatched `{'``). 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. 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. Through a FUSE mount bash's `read -t` +cannot time out: wrap the loop in `timeout N`. + +**Stats.** Lengths are real (for `event` and `pty/data` the next record's, +zero when none waits; for `log` what an open would freeze). 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: + +- 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. +- `@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. +- 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>: no match for regexp` +or `address out of range` when the address fails), opens nothing, and leaves +`look` reading empty; the write succeeds. + +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"`. +- 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`) 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. 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; +`/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 it says +`Kill: nothing running`. 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)) and logs `dump <path>`. `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 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 -Every builtin has its sentence; a test fails the build of one without. Such a setting written bare steps to its -next value, so a two-valued one flips (`Crt`, `Placement`, `BootShell`), as -its word clicked in a tag does; a value it does not take is refused with -`bad value in control message; takes ...` naming those it does. It is -generated from the registry, so it is always this build's own list. +`/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. -`/focus` reads the serial of the pane with the keyboard, and a serial written -to it gives that pane the keyboard, off any column or workspace tag that had -it -- rio's `current` written to a window's `wctl`, named once for the whole -tree since there is one keyboard. While a column's or the workspace's tag has -the keyboard no pane does: `/focus` reads empty and every pane's `ctl` says -`notcurrent`. A write gives the keyboard and nothing else: a folded pane -stays folded (unfold it with `Collapse` on its ctl), as rio keeps `current` -apart from `unhide`. A serial no pane has fails with `no such -pane`; anything but a number, with `ill-formed control message`. +A session holds 16 columns (`no space for a column: 16 max`, ENOSPC), each at +least 10 cells wide (`Newcol` from a column under 20 fails `this one is too +narrow to split`). Since root `Newcol` halves the active column, reach 16 by +writing `Newcol` to the widest column's `exec`. -A pane is made by **opening** `/pane/new`, and closed by Tremove on -`/pane/<n>` (`rmdir`), which is the only remove the tree serves; Tcreate is -refused everywhere, as it is in acme. Reading the open fid answers the serial -of the pane that open made, so `n=$(cat /pane/new)` makes one and names it in -a line. Each open makes another pane, and two reads of one fid answer the same -serial: the open acted, the read only observes. Closing the fid leaves the -pane. +`/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>`. -This is `/net/tcp/clone`'s mechanism, not acme's `new`, and the difference is -deliberate. acme allocates during the *walk* and lets the walk land inside the -new window, so `/dev/new/body` works in one step (acme(4): "accessing any file -in `new` creates a new window"). acme can also afford to list `new`, because a -Plan 9 directory read carries the stat of every entry and nothing walks. A -kernel or FUSE mount is not so lucky: it walks and stats each name a listing -gave it, so an allocate-on-walk name would make a pane per `ls -l`. Allocating -on open instead keeps `new` listed and `ls` honest — a stat is not an open — -at the cost of acme's one-step `new/body`. Nothing in the tree is created by -list, stat, walk or read; only that one open. Every other name in `/pane` is a -serial. +`tag` files (`/tag`, `/col/<n>/tag`, `/pane/<n>/tag`) read the whole tag. +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 (`tag: over 4096 bytes`, EIO). A clear is an ordinary edit +and `u` in the tag undoes it. [tags.md](tags.md) covers tags on screen. -A session can open its own tree through a mount: a Look at -`/mnt/9p/pardes/<me>/pane/2/body` from inside that very editor opens it, and -a Save of that pane writes back through the mount into pane 2. Requests on -the Unix and TCP listeners are answered on the 9P connection's own task, not -by the editor's loop, so the realpath, the stat and the read the editor makes -out through the mount come back while it waits for them. The one rule is -whose turn it is with the core (`pardes.turn`): the editor has it, and gives -it up while it waits for input and while a step of it is out in a syscall. A -step of a connection task's own -- a Look written to `look` -- goes out the -same way, and the editor waits for it to return before it takes a step of -its own. While any step is out, a request that would change a pane (a write, -a truncation, an rmdir) parks in the engine until none is; everything else, -opening `pane/new` and `screen` included, is answered at once, which is why a -Look at any path in the tree comes back. A write into the tree from the -editor itself only ever happens between steps (a Save), so nothing it waits -on out there is a request that has to park. QUIC is still served on the -editor's loop, so through QUIC the old hang remains. `/n/self/...` names the -same tree without leaving the process. +## Panes -`/look` and `/exec` are the editor's two clicks, one per line of a write: +**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. +`rmdir /pane/<n>` closes the pane, unsaved or not. -- a line written to `look` is a right click on it: a path opens a file, - `file:12` jumps to a line, a directory types `ls` into a terminal idle - at an empty prompt there, else opens a terminal there that runs - `ls` once (pardes has no directory listing pane of acme's kind; a shell - in it is where one goes on from a directory), a URL opens in the - browser, and a plain word, as acme's look3 does, selects its next - place in that pane after the dot, wrapping at the end, opening nothing - (`LookWord list` on the root ctl lists every place in a `+Search` pane - instead, as pardes did before; `LookWord search` is the default). In a - terminal, which has no dot to go on from, a plain word is always listed - in a `+Search` pane, its rows naming the terminal as - `@p<serial>:<line>:<cols>`, the columns a range, `@p3:12:5-9` (bytes 5 - through 9 of logical line 12, 1-based, both ends in), which a look of the - row selects, - and look reads that pane back. -- a line written to `exec` is a middle click: a command word from - `src/builtins.zig` (`Save`, `Del`, `New`, `Newcol`, `Mount NAME DIAL`, - `Unmount NAME`, `Dump`, `Restore`, `Msg TEXT`, `Find`, `Grep`, `Tty`, ...; - `Tty`'s argument is the shell it runs, `Tty fish`, and `Tty` on a pane's - ctl opens a new terminal pane, not in that one: in the active column, as - acme's makenewwindow puts a new window (util.c:456-467: the active column - first, then the pane's own), under the text with room or halving the - tallest, never shorter than its tag and 2 rows), - or anything else, a command line. Written at a terminal idle at an EMPTY - prompt it is typed into that shell -- any line that is no builtin, so - `Delcol x` at a terminal goes to the shell -- (and a terminal whose shell - exits, `exit` typed or run, closes its pane). Nothing is ever typed over - text someone typed at a prompt and did not send. From anywhere else -- a - file, a scratch, a tag, a terminal with a line typed at its prompt or - whose tty a program holds -- it runs as a command pane (from a pane whose - directory is not there it runs nothing and makes no pane, `exec: <dir>: - no such directory`, ENOENT, as `Tty` there does; one whose shell the host - cannot start ends at once, `exit <serial> 127` in the log, its tag no - longer running, nothing for Kill): a - terminal whose child is the root ctl's `Shell` ($SHELL, else /bin/sh, unless set) run - with `-c` and the line, in the pane's directory, with job control on - (bash, sh, dash, zsh, ksh `-m`; fish `status job-control full`), - which shows its output and then `exit N` (its tag reads `<dir> (<line>) - running`, then `exit N`), and stays. The command is over when its process - exits, as in acme, not when its terminal closes: a job it left in the - background prints on below `exit N` until it lets go of the pty, and a - command that lets go of its terminal early runs on to its own exit. A - background job outlives a command that exits on its own (Kill stops it too): job control gives it a process - group of its own, so the hangup the kernel sends the terminal's - foreground group when the shell exits misses it. It survives the pane - closing too, but its writes to the terminal then fail, so start one that - must keep writing with `nohup` or its output redirected. The directory's - next command runs in that pane once it is done and nothing holds its pty, below what it showed, after a `% <line>` line; - one still running gets a second pane. Before the next command the pane - leaves any alternate screen and turns off the modes a program left on - (mouse reports, bracketed paste, a hidden cursor); a command that clears - the screen and its scrollback (`clear`, ED3) erases the history above it. A command pane's own exec starts - the next command there too. The log says `run <serial> <line>` and `exit - <serial> <N|?>`; `exec` reads back the command pane's serial; Kill stops - its whole line, `&` jobs included; a command line is at most 1024 bytes, and a - longer one written to an exec fails the write (EINVAL, `invalid command - line: a command line is at most 1024 bytes`, and one with a control - character (DEL too) but a tab, `invalid command line: it holds a control - character (or DEL) other than a tab`; the root's exec logs either against the pane it would - have run at) before anything in it runs; a builtin's line (a long - `Msg`, an Edit block) may be longer. To run a command again, execute - its line again from its directory: `echo 'make test' > pane/<n>/exec` on - the command pane runs it there, below the last run. A misspelled word is a - command that says so and ends `exit 127`. `echo Tty > pane/<n>/ctl` makes - an interactive terminal in that pane's directory. +**`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 is one line; refused (EINVAL) are a +second line, a blank at either end, control bytes and non-UTF-8 (`bad +character in file name: a blank at its start`, ...). Up to 255 bytes a +component. -A terminal can be bound as a language's REPL: `Repl python` in its tag or -on its `ctl` (the language names are the syntax table's, or a code fence's -alias such as `py`, in any case; `Repl -` unbinds, -`Repl` bare says the binding, `Repl python` again changes nothing). Its tag -shows its id, `python-a`, `python-b` for the next, a freed letter reused, -and so does the end of its `ctl` line, after `current`/`notcurrent`. -A builtin's word still runs first, bound or not: `Del` clicked in the file -closes its pane. Then any other exec made by a gesture on the body of a file -in that language -- a -middle click, the execute key, on a selection or a single word, even `make` -in a comment -- or on the REPL's own body is typed into the REPL instead of -run: bracketed paste when its program asked for it (DECSET 2004), else line -by line, where a blank line inside a Python block ends the block (said -once), then Enter. The message row says `→ python-a` in the tag's name tint -and the log `send <from> <to> <id>`. With several REPLs bound for the -language the pane asks which, on its notice band, one key answering and Esc -sending nowhere; nothing is remembered. A REPL takes text only while its -program has the terminal: a command pane's until its command is done (the -binding goes with it, and a done one cannot be bound), an interactive -terminal's while a program other than its shell holds the tty -- after -Ctrl-D the shell would run the text, so nothing is sent and the pane says -so. Still commands, whatever is bound: a word in a tag (so -the tag is how to run `make` from that file), `Exec <text>` run by name -(typed, a 2-1 chord onto `Exec`, a `ctl` line) -- in a `.py` pane bound to -a REPL, `Exec print(1)` runs `print(1)` as a command, never in the REPL -- -and a command word @`cmd` in -the text, looked at or clicked (`# @`pytest -x`` in a script). A 9P `exec` -is no gesture and is never sent: a script writes to the REPL pane's -`pty/data`, multi-line code as a bracketed paste (`\e[200~<code>\e[201~`, -then `\r` in a write of its own once the REPL has echoed the paste -- -Python 3.13's REPL takes an Enter read with the paste as part of it, even a -one-line one -- and, for a paste of more than one line that does not end -in a newline, a second `\r`: one Enter leaves a multi-line input at `...`; a middle click does all of this -itself, holding the Enter until the REPL answers the paste), since line by line a blank line ends a Python block and Python -3.14's REPL auto-indents each line typed into it. The REPL gets the text -wherever it is -- at a `pdb` or `input()` prompt too. Over 9P, a range of a -`.py` pane goes to its bound REPL as a click would: write the event record -`MX<q0> <q1>` back to the `.py` pane's `event` (a range past its end is -refused, `range past end of body`). `Kill` does not stop -what a REPL runs, since pardes did not start it; `sig INT` on the REPL -pane's `pty/ctl` interrupts it as Ctrl-C would. Bindings are not dumped, so a Restore leaves none. +**`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. A PDF's +body is the text layer of the page shown; images and PDFs take no write +(`this pane has no text`). -The root's pair clicks at the active pane and `/pane/<n>/look` and -`/pane/<n>/exec` at that pane; `/tagexec` and `/col/<n>/exec` click in the -workspace's or that column's tag (never an event reader's, which hears -only its pane's; a command they run starts in the session's directory, -where pardes started, not the focused pane's), and what the word says is logged as the session's, -`msg -`. Blank lines are skipped, and every other line -is checked before any of them runs, so a control character fails the whole -write with EINVAL; a builtin that fails there fails the write as well -(below: one rule). Reading any of these files answers the -serials of the panes the last command created, or, when it created none, the -pane a look focused or the pane an exec acted on (even one it closed), one -per line; a look that found text answers the pane the text is selected in, -not the hits buffer it opened. +**`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). -Reads are a stream: once an open has read the answer, a second read on it -gives EOF (on an open that wrote, until its next write); open it again, or -seek to 0, to read it again. The answer belongs to the open, as -/net/tcp/clone's does: an open that -wrote reads what its own last write touched, from the start after each -write, whatever offset the read comes at (a shell's `exec 3<>look` shares -one offset between its write and its read, as with `pty/run`); an open -that never wrote reads the session's last answer as it stood when it was -opened. So `echo x > look; cat look` works for one client, but two -clients doing it at once may read each other's; for that, write and read -on one open: `exec 3<>$m/look; echo x >&3; cat <&3; exec 3<&-`. +**`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. -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: a mount cuts a big write at its message size (4 KiB through the -kernel's, 8 KiB from a client that asks), anywhere, and each piece comes as -a write of its own. A write that fills its piece may go on in the next, so -the open keeps its last line with no newline until then; a write shorter -than a piece is the whole of what was written, as acme takes each write, so -its last line runs with it even with no newline (`printf Save > exec`), and -a failure is that write's. Only what needs more is held: an Edit block -whose `{` or `a`/`c`/`i` text has not ended waits for its next write, and -what is left when the file closes runs at the close -- where an Edit block -whose `{` or `a`/`c`/`i` text never ended fails and changes nothing -(``unmatched `{'``, or `a, c or i text not ended by a . line`, logged as an -`err`): sam takes the end of input for a `.`, but a block that reaches Edit -unfinished was cut short. A fragment never runs on its -own. A line or Edit block held past 1 MiB is refused (EINVAL). +**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: -`@p<serial>:<address>` addresses the pane with that serial as `file:<address>` -does a file, with any sam address (`/re/`, `?re?`, `#n`, `$`, `0/re/`, a range), -a terminal too: its body is then the text of its logical lines (the lines -a `+Search` of it lists), addressed from its cursor, and the match is -selected. A miss, and a serial no pane has (with a line, `@p77:3`, as -with any other address), each log an `err`, and look reads back empty. A look takes acme's addresses after a colon (editors/acme/look.c:450): a -line written to `look` as `file:/re/`, `file:?re?`, `file:#n`, `file:$` or any address -opens (or finds) the file and selects what the address names. **The -address is evaluated from the file's dot**, as acme's is: `file:/re/` -finds the next match after the current selection, not the first in the -file. For the first, start at the top: `file:0/re/` (or `file:#0/re/`). -A pattern that can match empty, such as `^`, passes over the empty match -at #0 as sam does (a search never answers where it started), so it finds -the next one: for the start itself write `file:0` or `file:#0`. -`:addr` does the same in the pane itself, and a pattern may hold blanks -(`calc.py:/return a/`). `file:12` selects line 12, its newline included, -as acme's does; `file:12:5` puts the caret at line 12, column 5 (columns count bytes from 1, -as `addr`'s `12:5` does and as the rows of Grep, +Search and the language -servers write them). A bare -`/re/` is a path, as in acme, and failing that a search for its text. A -look that finds nothing, an address that does not evaluate, or a line past -the file's end (`calc.py:99`) says so on the message row and in the log -(`err <serial> look: ...`), focuses and opens nothing, keeps the -selection, and leaves `look` reading back empty. +- the pane builtins: `Del` (`Del k`/`Del j`, or `DelAbove`/`DelBelow`, give + its rows to the pane above or below), `Save [path]`, `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. -`/pane/<n>/name` reads the pane's file name (a terminal's directory) and -writing it renames the buffer; a relative name resolves against the pane's -directory. A name alone is no edit: the pane's `dirty` stays what its text made it -(a renamed clean file is still 0, and nothing asks about it at Exit, -Restore or Del, which ask only about text edited), and `Save` writes it -under the new name all the same. An open's writes are one name: held -until its newline, applied once however the writes cut it; a write with -no newline that is whole in its Twrite is the name then, so a bad one fails -that write, not the close. Nothing else is trimmed. Two lines are refused, EINVAL, -in one write or as a second line on the same open (bash's `printf -'a\nb\n' > name` writes a line at a time: the first names it). A blank inside a name is taken -(`two words.zig`); refused, EINVAL, in acme's words (xfid.c:650) and why, -are a blank at either end (not quietly cut off), `bad character in file -name: a blank at its start` (or `end`), a second line, `...: a newline (a name is one -line)`, a control byte, DEL or a C1 control (U+0080-U+009F), `...: a -control character`, and bytes that are not UTF-8, `...: not UTF-8`. `body` appends on write and replaces on truncating open. A -terminal's `body` is its history as plain text, frozen per open, in logical -lines: a row the terminal wrapped is joined back to the row before it (the -wrap is ghostty's, as `pty/run`'s output unwraps), and the last line ends -with a newline. `sel` -reads the selected text and writing it replaces the selection, leaving dot -just past the text written (a `data` write moves dot as its text moves it, -so a dot at the address it wrote at ends just past that text too). `errors` -appends to the directory's `+Errors` pane; with no room for that pane in -any column, what it would have shown is logged as `msg` records instead, a -line each, and the write still succeeds. Holding `event` open redirects the -pane's Look and Exec clicks to that client, and so does a line written to the -pane's own `look` or `exec`, or to the root's while that pane has the -keyboard (never its `tagexec`, which is a click in its tag and runs), a -click with no place in the text: an `F` record at `0 0` carrying -the line. So a client holding `event` that wants a command run gets its own -exec back as a record: it runs it through `ctl`, or writes the record back. -Writing a record back performs the action, as the click would have (acme's -xfideventwrite): a body `X` goes to a REPL bound for the text as a middle -click does, where an `F` record, a line written to `exec`, runs as the -command it was. acme takes back only `<origin> -<action><q0> <q1>`, the text of that range; pardes takes the record whole as -it was read too, and for an empty range acts on its text, which is how such -a line is done. A click in a file's body carries the offsets of the text it -took; one in a terminal's body cannot, since that body is a history -snapshot, and is also at `0 0` with its text. A click that takes no text -sends nothing, as in acme. `ctl` reads acme's window status line, field for -field as plan9port's — serial, tag length, body length, isdir (0), the dirty -flag, the width in cells, the font, the tab width, whether Undo has a step -(1) and whether Redo has one — followed by pardes's own: rio's `current` or -`notcurrent` (rio(4), `wctl`), whether the pane has the keyboard, and a -REPL's id. It takes the pane's builtins (below), -`get`, which reloads the buffer from the name it -carries (unsaved edits are refused once, listed in `+Unsaved` with a short -notice as Exit's are, the write failing `<name>: Modified (get again to -discard)`, as acme's get asks winclean, exec.c:513). A file that changes on -disk reloads by itself only into a buffer with no unsaved edits; one with -them keeps its text and stays dirty, says `<name> changed on disk (get -reloads it, Save overwrites it)` and logs `changed <serial>`, and then its -`Save` warns once, `<name> modified on disk since read (Save again to -overwrite)`, as acme's Put does (exec.c:577) -- acme reloads nothing by -itself. `answer <choice>` -for the question the pane asks on its notice band, which the log names as -`ask <serial> <what> <choices>` -- `ask 4 del k j` for Del's side from the -keyboard (`k` the pane above takes the rows, `j` the one below), `ask 4 -repl a b` for which bound REPL takes an exec (a REPL's letter) -- `answer -` -taking it back as Esc does (a choice the question does not offer is refused, -naming those it does, and the question stands; with no question standing, -`answer` is refused), -and acme's `lock` and `unlock` (editors/acme/xfid.c:603-611), for an -edit of several writes to `addr` and `data` that another client must not -land in the middle of. As in acme the lock binds only the clients that take -it: a `lock` while another open holds it fails at once with `file in use` -(EBUSY), to be tried again, until that open writes `unlock` or closes (or -the pane does) -- where acme's blocks, because through a kernel or FUSE -mount a blocked write would hold up the holder's own `unlock` and close on -that file -- and nothing else is refused for it -- -not a write to any other file, not the person at the keyboard. It belongs to -the open that wrote it, so only that open's `unlock` is taken; a write on an -open that cannot write (or the editor's own, on none) cannot lock. From a -shell the lock needs an open held across the edit, since `echo lock > ctl` -closes, and so unlocks, at once: `exec 3>ctl; echo lock >&3; ...edits...; -exec 3>&-`. +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. -The three range files `addr`, `dot` and `limit` each read the pair of offsets -they also accept, so copying one onto another is all that acme's `addr=dot`, -`dot=addr` and `limit=addr` ever were. A write is either that pair or an -address expression (`#0,#5`, `/pattern/`, `2+1`, and pardes's own `12:5`, -below); `addr` selects where `data` reads from -- to the end of the text, -as acme's does, so `cat data` reads everything after the address -- and -the range `xdata` reads, just that; either replaces the range, `dot` is the editor's own selection and moving it -scrolls the pane into view, and `limit` bounds only the end of a forward -search, as acme's does, and reads empty until it is set. Truncating `dot` empties it, truncating `limit` lifts it -- -though a write after the truncation that fails puts the old limit back, so -`echo /bad/ > limit` changes nothing -- and truncating `addr` leaves it as -it is (below). +### Addresses and data -A rename everywhere, or any other sam edit, is one write to the pane's -`ctl`: `Edit ,x/foo/c/bar/` runs acme's Edit (docs/tags.md) on the body as -one undo step; a failure fails the write with acme's words and changes -nothing. A write is one message a line, except that an `Edit` line takes -the lines after it while its `{` group is open or its `a`, `c` or `i` text -block waits for its `.` line, on a pane's `ctl`, the root's (at the active -pane) and `exec` alike; an unclosed group is refused (``unmatched `{'``). -A block may come in several writes on one open (bash's builtin `printf` -writes a line at a time): the open holds it until it ends (below). +`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. `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 line number counts newlines as sam's lineaddr does (editors/sam/ -address.c:180), so the empty line just past a text's last newline is an -address: `1` of an empty text is `#0,#0`, `2` of `a\n` is `#2,#2`, and -`Edit 1i/header/` on an empty file inserts; a line past that is `address -out of range`. +- 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. -`line:col` is a pardes extension to sam's addresses, the spelling Look -takes in `file:12:5`: `12:5` is the point at line 12, column 5, and it -composes like any simple address (`12:5,14:1`, `12:5+#3`). The column is -in bytes from 1, as Look's is, clamped to the end of the line and snapped -back to the start of the rune it falls in; a line past the end, or -column 0, is `address out of range`. Since the column counts bytes, a -column a tool gives in characters (pytest's, a compiler's) is the same -only on an ASCII line; elsewhere address the line and search it -(`12/name/`) or use `#n`. sam would read `12:5` as a syntax -error. The rows of Recent, a +Search and the Jumplist spell a range -`12:5-14:2` (or `12:5-9` on one line), and `addr` takes that too, so a row -pastes in; the two spellings side by side: +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. - 12:5,14:2 sam's range: from line 12 column 5 up to the point at 14:2 - 12:5-14:2 a row's range: 12:5 through the character at 14:2, inclusive +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. -A `-` right after `L:C` and a digit is this range, not sam's "back N -lines" from that point. -A write to `data` or `xdata` replaces the range `addr` names, as acme's -does (editors/acme/xfid.c:491-523: it deletes the range, then inserts), so -`echo NEW > data` and `echo NEW >> data` both replace that range, and -truncating deletes it and nothing else, so `: > data` deletes it; only -truncating `body` empties the whole buffer. Truncating `data` is pardes's -own: acme ignores OTRUNC there (editors/acme/fsys.c:543). And as in acme a write -leaves `addr` just past what it wrote, so a second `echo x > data` deletes -the empty range there and inserts after the first rather than replacing it -again; write `addr` before each replacement. A read of `data` or `xdata` -moves `addr` past what it read, as acme's does. -`addr` belongs to the pane rather than to a client and keeps what was written -until someone writes another, so writing an address and reading it back -evaluates it, which is what acme(4) promises of its own `addr`. Unlike acme, -neither an open nor a truncation resets it: acme sets it to `#0` when the -first client opens `addr` (editors/acme/xfid.c:105-108), which suits a -client that holds the fid, but a shell opens the file anew for every -`echo /re/ > addr` and so would search from the top each time and never -advance. Here each such write searches on from the last address, as `>>` -does; write `0` to start again from the top. A search wraps at the end of -the text, so a find-all loop stops when the address comes back to where it -began, or bounds itself with `limit`. +Refusals: `bad address syntax`, `no match for regexp`, `address out of +range`, `addresses out of order` (`#100,#50`), `bad regular expression`. -The regular expressions are mvzr's (sets, `\d`/`\w`/`\s`, `{m,n}` and -lazy `*?` included), searched the way sam searches (editors/acme/regx.c): as -lines, so `^` and `$` match at the start and end of any line, `.` and a -negated class never match a newline, and `$` also matches at the end of a -text with no final newline. A pattern that names a newline (`\n`) runs over -the whole text instead, its `.` kept to one line; there a leading `^` still -matches at every line start (`$` on a CRLF line sits before the `\r`, -as the line's end is its `\r\n`), and `$` may stand just before a `\n` (where it -changes nothing). Any other `^` or `$` in such a pattern, `(^|\n)def` or -`a\nb$`, is refused, EINVAL, with `bad regular expression: in a pattern with \n, ^ can only come first and $ -only just before a \n`, since mvzr would read it as the start or end of the -whole text: never a search that silently finds nothing. An alternation -whose every branch starts with `^` (`^def|^ `) finds a line that starts -either way (it is taken as `^(def| )`); one that mixes anchored and -unanchored branches (`^def|x`) is refused, `bad regular expression`, since -mvzr keeps `^` only first. A pattern may be up to 512 of mvzr's operations, -about 512 characters (mvzr's own is 64; pardes builds it with more); a -longer one is refused, `bad regular expression: longer than mvzr's 512 -operations`. A class may hold non-ASCII runes (`[éa-z]`, `[à-ÿ]`): it is -taken as an alternation of them (`(é|[a-z])`), a range of up to 256 runes -spelled out, and the 512 counts the pattern as rewritten. A wider range -(`a range of runes wider than 256 in [...] is not supported`) and a negated -class with a non-ASCII rune (`[^é]`) are refused, since mvzr's classes hold -bytes. An expression is -evaluated from the current address, the range last written to `addr` (or -left by the last `data` write, just past it), as acme evaluates it from -`w->addr` (xfid.c:446): `.` is that address, not the selection (`dot` is -the selection's own file), and `#9/re/` searches from `#9`; in `/a/;/b/` -the second search starts at the end of the first, as acme's `;` does, where -`/a/,/b/` starts both at the current address. `/re/` searches -forward from the end of the current range to `limit` if one is set, and -otherwise wraps to the start of the text; `?re?` and `-/re/` find the last -match ending before the range, wrapping to the text's last. The match is the leftmost, but of -the alternatives at that place mvzr takes the first that matches where sam -takes the longest (`/gam|gamma/` finds `gam`); in a search begun in the -middle of a line, `^` inside a group can match there; and in a -pattern that spans lines, `^`, `$` and `[^...]` keep mvzr's own meaning. -mvzr backtracks without bound of its own (`a?` twenty times then twenty -`a`s is 2^20 steps from each place it tries), and a search holds the editor, so pardes patches a -step budget into mvzr's matcher (build.zig): a search that spends it, -about 300 ms, fails with `regular expression search took too much time, -gave up` rather -than answer a match it is not sure of. The budget is each search's: an -Edit `x` over 100k lines makes 100k searches, each with its own. Ordinary patterns spend a few -thousand steps; what runs out is exponential backtracking, and a quadratic -pattern over a very long line (`\s*(\w+)\s*=` over 20 KB of letters). -pardes has no regex engine of its own on purpose; these are its limits. -Normal mode's `s` and `S` search a selection the same way (src/regexp.zig -is the one place both call), so `^` there also means a line's start. +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. -An address that does not evaluate fails the write with why: `bad address -syntax`, `no match for regexp`, `address out of range`, `bad regular -expression`, `regular expression search gave up, ...`, or sam's -`addresses out of order` for a range that ends before it starts -(`#100,#50`), which acme lets through. Each end is checked first, so in a -text shorter than 100 bytes `#100,#50` says `address out of range`. A failed write to `addr` leaves no -address at all, where acme -keeps the old one: until a good address is written, `addr` reads empty -(as an unset `limit` does), and reading, writing or truncating `data` and -`xdata` fail with `no address: the last one written to addr failed`, so a script that -missed its target cannot then write at the last one; so does an address -written to `addr` that goes from the current one (`.`, `+1`, `-/re/`), which -there is none of, until an address that stands alone (`2`, `#0`, `/re/`) is. +### Regular expressions -The three flag files `dirty`, `mark` and `scroll` read `0` or `1` and take -`0` or `1`: whether the buffer differs from its file (a file deleted on -disk counts, as in acme: its text is only here now, and Del, Exit and the -rest ask first), whether a write pushes -an undo point (writing `1` pushes one now), and whether a write scrolls the -pane. The writes of one open of `data`, `xdata` or `body` are one undo -step while `mark` is 1 (so `printf 'a\nb\n' > data` is one, though a -shell writes it a line at a time), and the next open starts another. The -pane has one history, so two opens writing at once take turns in it: each -open's first write after the other's starts a step of its own; to -make a loop's writes one step, write `1` (an undo point here), then `0`, -the writes, then `1` again. An open's writes in a row to one place -- an -append to `body`, inserts going on at `data`'s address -- are held and go -in as one edit when anything else comes (another request, the close, or -the editor's step once they pause 20 ms), so a 10 MB write is one copy, not -one per 8 KB piece; any read sees them. +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: -One rule for what fails: a write fails whenever what it asked for fails, -whether it came to a `ctl` (the root's, a pane's, a column's, a pane's -`pty/ctl` -- its `exec` included), `look`, `exec`, `tagexec` or a -column's `exec`, or to any other file -- with an -errno that fits, EINVAL for malformed input (an unknown word, a control -character, a command line over 1024 bytes, a `size` or `winsize` out of -range, a bad address or event record), else EIO or the errno the words -name (ENOENT for a pane, file or directory gone -- `look .` from a pane -whose directory is gone says `look: <dir>: no such directory`, and a -`./zz.txt` or `../x` that is not there `look: ./zz.txt: no such file`, while a -plain `zz.txt` is looked for as text, a miss logged as any look's -- and for a -Find or Grep that finds nothing, `Grep: pattern not found`; Grep walks -every pane's directory on this host, passing over panes of the served -tree (`/virtual/`, a peer's `/n/<name>/`) and directories not there, -so none of them spoils the rest; Find, Grep and a language server's lists -(Symbols, Diagnostics, Callers and the rest) share one `+Search` a -directory, each run replacing what the last showed, as acme reuses a -directory's `+Errors` (the exec reads that pane back; one that finds -nothing empties it rather than leave the last rows), while a plain -word's `LookWord list` search keeps a pane a pattern, ENOSPC for no room or -slot, EBUSY for a held lock) -- and logs its reason exactly once, as `err <serial|-> -<file>: <why>`, with no `msg` for it. A builtin a click runs (Save, get's -`Modified`, Tty with no room, Edit) is no exception. The rule is a write's: -a refused open or truncation -- an OTRUNC open, such as `data`'s after a -failed `addr` --, create or remove answers its error and -logs no `err`, and so do a write to `pane/new` (`permission denied`: it is -only read) and a write on a fid opened OREAD (`bad use of fid`). Every -refusal is said in words, Plan 9's where pardes has none of its own -(`permission denied`, `file does not exist`, `bad argument`), never a C -library string such as `Operation not permitted`. What is not a -failure: a look that finds nothing answers nothing and logs one `err` -(`look: no match for ...`, quoting what was written in every form -- -`no match for "zzq:#3"`, `no match for "zzq:2"` -- the same miss again counted, `(x2)`, as any -repeated `err` is), the write succeeding; and a command line run in -a command pane ends in its own time, told by its `exit` record. +- 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 or none (`^def|^ ` works, + `^def|x` is refused). +- 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). 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. + +- 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 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), `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 +``` -`tag` reads the whole tag as the pane shows it: the computed path or PDF -page (no mark for unsaved text: the grip shows that, and `dirty` says it), -then the text you may edit. An image's tag begins with its mode words, not -its path: `img petscii:off palette:commodore ascii:on <path>` by default, the modes -it is drawn in, then the file. The editable text is 4096 bytes at most: a -write that would pass that is refused whole, ENOSPC, `tag: over 4096 -bytes`. A write appends to that text, -newlines included, and a tag with more than one line takes a row per line on -screen; truncating `tag` clears it, as acme's `cleartag` does -- the default -words (`Save Tty Collapse Del` and the rest) with it, since they are that text until -you edit it, so `echo Make > tag` leaves only `Make` -- a truncating write -drops the one newline that ends what it wrote, which would draw an empty -row, and keeps any other; append with `>>` to -keep them, with `printf ' Make' >> tag`. The leading blank is needed: the -tag reads back with no blank after its last word, so `printf Make >> tag` -glues `Make` onto it; and `echo ' Make' >> tag` ends with a newline, which -starts a new line of the tag. The clearing is an edit of the tag like a typed one and its undo -history is kept: `u` in the tag brings back the text it cleared, words -included. +A follower the ring outran reads `lost N` first. A Restore hangs the +follower up: dial again and read from `restore <path>`. -Stats report real lengths for `index`, `status`, `look`, `exec`, `listeners`, -`name`, `body`, `tag`, `sel`, `ctl`, the range files and the flag files, and -for `event` and `pty/data` the length of the record a read would -answer, which is zero when nothing is waiting; for `log`, the text an open -would freeze now. Modes are 0644/0666 (0444 for -read-only files, 0222 for write-only); mtime is the pane's last edit or the -process start. The qid version of `body`, `data` and `xdata` is the pane's -revision, so a stat sees an edit land without reading the text; every other -file leaves it zero rather than promise a version it cannot keep. Directory -entries carry no sizes, and neither does `/screen`, which has no length until -an open renders its frame; stat the entry. +| record | when | +|---|---| +| `new <serial> <name>`, `del`, `rename`, `save` | a pane made, closed, renamed (a terminal's too, as its shell changes directory), saved | +| `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 | -`/log` is one ring (64 KiB) that records whether or not anyone reads it: -`new`, `del`, `rename` (a terminal's too, as its shell changes directory -- -a new terminal is `new N <dir>`, named by the directory it is started in, -and renamed only when its shell goes elsewhere (the boot's, started with -no directory given, is `new N /` until its shell says where it is); it -runs `ls` once, at its shell's first prompt only, never later over what -someone ran or typed first, a greeting that shows the directory it opened -in, since a terminal is named by its directory and nothing else on it says -where it is), `exit <serial> <N>` before -the `del` of a terminal whose shell exited by itself, `ask <serial> <what> -<choices>` when a pane asks a question (answered by `answer` on its ctl; -`ask <serial> save path` when a Save made over 9P, on a terminal or a -scratch, opens its prompt for a path, answered `answer <path>` or `answer --`; asked again, the one open stands, not a second), -`answer <serial> <choice|->` when it is answered, by key or ctl, `-` for -taken back or for its pane closing with the question standing, -`changed <serial>` when a pane's file changed on disk under its unsaved -edits (below), `changed <serial> reloaded` when a pane with none reloaded -it, and `changed <serial> deleted` when it was deleted or moved away, `unsaved <serial> <name>` for each pane an Exit, Restore, -Del or Delcol refuses over (before that write's `err`), and `save <serial> <name>`, -`dump <path>` when a Dump is written and `restore <path>` in a Restore's -new log after its panes' `new`s (a relative Restore path is looked for in -`DumpDir`, then in the directory pardes started in; a bare Restore takes the -last dump this session wrote), then `restored <old> <new>` for each pane, -mapping the serial it had to the one it has now, and `restoredcol <old> -<new>` for each column, and `msg <serial|-> <text>` -for every line the editor says (a builtin announcing its own name as it -runs, with `Verbose` on, is the message row's alone, never logged: a `msg` -is something said, `Kill: nothing running`). A builtin that -fails its write (`ctl`, `exec`, `/tagexec` or a column's `exec`, and an -open of `pane/new`) is logged by that write's `err` alone, no `msg`, so -the same failure again is the same record again; a line said again word for word is counted, `msg 3 Undo: nothing to -undo (x40)`, as `err` is (below); a text past 256 bytes is cut there, -between words, and ends in an ellipsis, `…`, as an `err`'s reason is past -200; its serial is the pane it ran at -- a line written to the root's -`exec` or `look` runs at the active pane, and is logged as that pane's, -even while the keyboard is on a column's or the workspace's tag -- and `-` -for a line to the root's `ctl`, `/tagexec`, or a column's `ctl` or `exec`, -which run in a tag no pane owns), and `err <serial|-> -<file>: <why>` (the serial is that of the pane whose file it is, and for -the root's `exec` and `look` the pane the line ran at, `err 3 exec: ...`; -`-` for another root file) for every write or truncation the tree refused or that -failed -- through a mount a shell sees only the errno its kernel mapped the -reply to, usually `Invalid argument`, and this is the reason (`err 3 addr: -no match for regexp`). A record said again word for word, straight after -itself, is counted rather than repeated (`err 3 addr: no match for regexp -(x4)`: four in all, counting the first), so a client retrying a failing -write does not push the rest out of the ring. A record a follower has -already read is never rewritten: the next repeat is a line of its own -carrying the running total, `(x5)`, and counting goes on from there, so a -follower sees each count as a new line: with a follower attached, a repeat -is a new line carrying its `(xN)` count, never an edit of the one read. Through a kernel mount a client sees only an errno, which 9ns reads from the -error's words (cloud9's 9ns/src/nine.zig, `enameToErrno`): a malformed write --- an unknown or ill-formed control message, `bad address syntax`, `bad -regular expression` -- is EINVAL; a lock another open holds, EBUSY; a pane -gone, ENOENT; a well-formed write that fails -- `no match for regexp`, -`address out of range`, `addresses out of order`, a search that gave up, -`<name>: Modified (Exit again to discard)` -- EIO. The err record has the -words. There is no per-pane error file to read instead: -acme's `errors` only takes text, and one record stream is simpler to watch -than a file per pane. A `msg` said while a -pane is being made can precede that pane's `new`; panes present at boot are -recorded before anything else. -Every EINVAL a write gets says why, in its reply and in its `err` record -(`bad character in file name: an empty name`, `invalid write to log: it -takes \`follow\` or \`follow new\``, `invalid write: this pane has no -text`), never a bare `Invalid argument`. -Control characters, DEL and C1 controls (U+0080-U+009F) in a record become -spaces, and a byte that is not UTF-8 is written `\xNN`, so a record is one -line of UTF-8; a pane's name, in a record, in `/index` and in a terminal's -tag alike, shows a newline in it as `\n` rather than a space and its own -backslash as `\\`, so a directory named with a newline and one named -with a backslash and an `n` read back differently, as what they are. -(An `event` record is not: acme's `<origin><action><q0> <q1> <flag> <n> -<text>\n`, whose text may hold newlines. The origin is `E` (a 9P write to body or tag), `F` (other -files, the editor's own lines), `K` (the keyboard) or `M` (the mouse); the -action's case says where: `x`/`l` a click executed or looked at in the tag -(its offsets count the tag's whole text, path included, as `tag` reads), -`X`/`L` in the body, `I`/`D` text put in or taken out of the body, `i`/`d` -of the tag. The flag is acme's: 1 the text's first word is a builtin's, 2 -an expansion record follows (acme's look.c:42-43, exec.c:154-157; pardes -never sends one: a click's record already carries the word it took, its -range and text, so 2 is never set, whatever the origin), 4 (a look) the -text is a file name or address, 8 (an exec) chorded: two records -follow, the argument's text and where it came from, `<file>:#q0,#q1`, each -with no place of its own: offsets `0 0` and flag 0, `Mx0 0 0 5 hello` (as -acme's exec.c:182 writes them). -Written back, an `X`/`x` record executes and an `L`/`l` record looks, as -the click would have. A chorded one (flag 8) runs with its argument: the -record after it in the same write, else the one kept from the click; its -two follow-up records written back after it, together or in writes of -their own, are consumed as its, never run as commands (written back alone -with no argument kept, it waits for its argument record); the origin letter -written back is ignored but for `F` on `X`, which runs as the command it -was rather than going to a bound REPL; `I`, `D`, `i` and `d` are reports and -are refused. Read `n` bytes of the text, not up to a newline -- bytes here, where acme counts runes. Every offset and -count pardes serves is in bytes, `#n` and `q0`/`q1` too; the event count -follows them rather than switch alone, so an acme library reads pardes -correctly for ASCII text and not beyond it. Offsets are bytes, but every -address lands on a rune boundary, as sam's and acme's work in runes, never -inside a multibyte rune and never widened to a grapheme cluster: a `#n` -inside a rune snaps back to its start, a `line:col` likewise, a search's -match covers the runes it touches, and a copy of addr to dot keeps its -runes, so a lone combining mark or the `\r` of a CRLF is addressable on its -own (an Edit's `x`, `y` and `s` match the same way: `.` is one rune); `addr` reads back the snapped offset. (How the terminal draws such a -text, a cluster to a cell, is apart from this.) A click in a -tag gives offsets into the whole tag as `tag` reads it, the path first.) An -open freezes the ring's text the way `/screen` freezes a frame: reads walk it -and end. Writing `follow` to that same open makes reads past it wait for the -next record, one per read, after first reading all that the open froze (the -ring's whole history, up to 64 KiB); `follow new` skips that and waits for -what comes after, as `tail -n0 -f` does. A follower the ring outran reads -`lost N` first. -Closing the open is the only way back, as with rio's `consctl`. A follower -misses nothing within a session only: a Restore hangs its connection up -(and a 9ns mount with it, which must be restarted), so it dials again and -reads the new log from its `restore <path>`. +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. -A read with nothing to give yet -- a following `log`, `event`, `pty/data`, a -`pty/run` before its answer -- is held, the way factotum holds its log's reads -(security/auth/factotum/log.c) and acme an event read: the core keeps it, and -whoever next has news for it (a record, output, a run's answer, the pane -closing, which answers `no such pane: its window shut`, ENOENT as any -other file of a gone pane gives, where acme says "window shut down") answers it on the -connection it came on as the turn is given up. Nothing else parked is -retried for it. The core keeps the ticket cloud9 gave the park -(`Conn.hold`) and answers only while that very park waits, so a read the -client flushed or whose fid it clunked meanwhile is dropped unanswered, no -record is spent on it, and a tag the client reuses is never answered with -what was meant for the old one. An open waits with one read at a time, as -acme's window keeps one `eventx`; a second read on it meanwhile fails with -"file in use". QUIC connections still retry their parked reads each tick. +## Other files -`pty/run` runs one line at a terminal's prompt and answers how it ended, on -the same open (factotum's `rpc` shape): write the line, then read `exit N` -once the command has ended and the shell is back at a prompt, followed by -what it printed. One line a run: two lines in one write, or a next line on -the open before the last answer is read (bash writes `printf 'a\nb\n'` a -line at a time), are refused, EINVAL. The output is what the screen showed between the command's -start and end marks: stderr interleaved, `\r` progress collapsed to its last -state, no colour, tabs as the spaces they drew, trailing spaces trimmed and -trailing blank lines dropped (`printf 'a\n\n\n'` answers `a`), leading -whitespace kept; a program on the alternate screen (vim, less, htop) leaves -none, and rows a program redrew above its start are missed. A bash job notice -printed before its PROMPT_COMMAND lands in the next run's output. Only its -last 64 KiB are kept, from a line start, and the header then reads `exit N -cut M` (M bytes left out). `exit N cut`, with no count, says the output's -start is not there to read: it scrolled out of the history, the command -erased the screen (`clear`, `watch`, a full-screen program's redraw, a -reset -- so such a command's output may read as cut), a start or end mark -came on the alternate -screen, or the command printed more than 8192 rows, of which only the last -are read so that the answer costs the editor a bounded amount. `exit ?` is a -command whose end mark carried no status, which is not a success; `error out -of memory` is an answer that could not be made. The header is always the -whole first line. It reads -`busy: <program> is running` at once when a command is running (bare `busy` -where the host cannot name the program, and when text is typed at the prompt) --- a run is a line typed at the shell's prompt, so a program holding the -terminal (a REPL, `less`) takes none: write to `pty/data` for it -- -which is also when the third field of `pty/status` reads 1. `pty/status` is -one line, three right-aligned fields and a newline: the pty's columns and -rows, then busy (0 or 1). `pty/ctl` takes `winsize C R`, `sig -INT|TERM|HUP|QUIT|KILL` and `exec`, which starts the pane's shell again in -its directory (a command pane's child is its command, which does not -restart: `exec` there is refused, EINVAL, `a command pane does not -restart`): one that is gone is refused before anything runs, `exec: -<dir>: no such directory` (ENOENT), and a shell the host cannot start (not -there, not executable, a script whose interpreter is not there) fails the -write with why -- `shell: shell not found`, or `shell: interpreter -/no/such/interp not found` for a script whose `#!` names a program that is -not there, ENOENT -- keeping the terminal and its running shell: a shell -not there is refused before anything runs, and the host starts the new one -before the old goes, so one that cannot start leaves the old be; a `Tty` naming such a shell or -script is refused before anything runs (`Tty: interpreter ... not found`, -its `err` the only record), and one whose shell cannot start fails the -same way and leaves no pane. The host knows before it answers: the child reports a failed exec -through a close-on-exec pipe. A directory removed -under a running shell leaves the pane its name (never `... (deleted)`), so -an `exec` works there once the directory is back. The size, and `pty/ctl`'s `winsize` read back, is -what `winsize C R` last set (R of 1 is taken as 2: a one-row pty loses -its prompt's mark and would read busy for ever), until the pane itself -resizes and gives the pty its grid again. A line written -before a new terminal's shell has drawn its first prompt is not busy: it -waits for that prompt (a respawn meanwhile keeps it waiting for the new -shell's) and is sent then, so the first command a script gives a fresh -terminal is not lost. A shell that never draws a tagged prompt (one pardes -could not instrument, or a startup that hangs) leaves such a line waiting -for ever: cancel the read (interrupt it, or close the open) to give up; -`error not run` when the shell refused -the line without running it (a fish syntax error; the line is taken back off -the prompt): the shell's marks say only that it did not run, so the answer -carries no reason or code, and the shell's own complaint is in the pane's -body (`tail body`); `exit N` and what it printed when the line ended the shell -itself (`exit 3`, or `echo bye; exit 3`): its terminal closes, and a read -of the run's open still answers after the pane is gone; `error shell gone` -when the pane closed or its shell was replaced, the shell went without -an exit status to tell, or it never started (a `Tty` in a directory that is -not there is refused before, `Tty: <dir>: no such directory`, and makes no -pane); `error no prompt marks` for a shell pardes could not instrument; -`error command done; not a shell` (or `error a command runs here, not a -shell`) on a command pane, whose child is its command. -It relies on the OSC 133 marks pardes injects into bash and fish, tagged -`aid=pardes` so fish's own marks and a nested shell's are ignored. A second -line on an open whose command still runs fails the write. `exec zsh` or a -continuation prompt never reports an end; cancel the read. A read of `run` -waits in pardes, so through 9ns it needs 9ns's concurrent requests or it -holds up the rest of the mount. A record -longer than a read comes in pieces, so a shell's `read` loop works: `exec -3<>$m/log; echo follow >&3; while read -r line <&3; do ...; done`. bash's -`read` takes a chunk, keeps one line and seeks back to just past it; a -followed log, `event` and `pty/data` answer a read at an offset inside -their last answer from that answer again, so no record is lost. Through a -FUSE mount (9ns --mntgen) bash's `read -t` does not time out on a followed -file: the read waits in the mount, where its timer cannot cut it; wrap the -loop in `timeout N`, or read with `cat` in the background and poll what it -wrote. `tail -f` -never writes `follow`, so it sees nothing new: use the follow open instead. `/screen` returns JSON with -`cols`, `rows`, `cursor`, a `styles` table, and row-major `cells` of -`[grapheme, style_index]`. Each open freezes one frame until close. A -terminal `body` freezes its history on the first read of each open handle; -a PDF's `body` reads the text layer of the page it shows (MuPDF's -extraction; turn the page for another), and it, as an image's, takes no -write (`this pane has no text`, EINVAL); -`pty/data` streams live output. The listings -- `/index`, `/layout`, -`/recent`, `/commands`, `/status`, `/listeners`, and a `ctl` opened only to -read -- freeze at the open too, so one read in several chunks never splices -two moments; open again for what is there now. An open that holds something between open -and close -- a frozen screen, listing, terminal body or log, a run, an `event` or -`pty/data` open -- takes one of 64 records (lib9p's per-fid aux, acme's -Fid), released on close or disconnect; past that such an open fails with -`ENFILE`. Other opens hold nothing and are not counted. +- `/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, most recent first, `open <path>` or `closed + <path>`; kept in `$XDG_STATE_HOME/pardes/recent`. `Recent` shows them in a + pane; a look at a row reopens the file at its last dot. +- `/status`: `pid`, `version`, `panes`. +- `/os/`: existing regular files take read, write and truncation to zero; + create, remove, rename and metadata changes are refused; 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`. -`-Dembed-sources=true` embeds the editor's sources and serves them under -`/src` (and `/shaders` on GUI builds). `EffectCode <effect>` lists the current -backend's implementation files under `/virtual`, which Look opens; without the -option the command reports the sources as unavailable. It is off by default -everywhere; the esp32p4 image in particular has no room for them (~1.8 MB of -source against a 1.5 MiB app partition). +## Limits -This is a control filesystem, not a complete POSIX export. Native filenames -may contain up to 255 bytes. Existing regular OS files support read, write, -and truncation to zero; under `/os` protocol create, remove, rename and other -metadata changes are refused, as is every create in the control tree and every -remove in it but a pane directory's. Ownership and permissions under `/os` are -synthetic. -Zero-length truncation accepts the accompanying `mtime` hint sent by Linux -v9fs; the hint is not stored. Standalone timestamp changes remain refused. +| | | +|---|---| +| 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 | 8192 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 | +| a dump | 6 columns (so `Dump` of more fails `bad dump columns`) | +| file name component | 255 bytes | -The tree lives in `src/ninep/`: `tree.zig` (nodes, lookup, readdir, dispatch, -and the editor's reply payload over cloud9's backend contract), `pane.zig` -(pane files), `ctl.zig`, `addr.zig`, `pty.zig`, `events.zig` (event and log -streams), `screen.zig` and `sources.zig`. The protocol engine is cloud9's -`fs.Server`, configured in `src/9p.zig` (the editor's and the board's -capacities); the transports are `src/9p_io.zig` (cloud9's `serve.Runner` -for Unix and TCP, a poll loop for QUIC, and the 9P client for mounts); -`src/fs.zig` keeps host access, mounts, resolution, find and grep. +## Source and tests -`zig build fs-test` drives real sessions using the independent Python client -in `test/ninep.py`; `zig build fs-discovery-test` checks that browsing -creates nothing, that an open of `/pane/new` and a remove work, and that -`look`, `exec`, `name`, `sel` and `log` behave. `zig build -9p-test` checks the two engine configurations' budgets (the engine's own -tests are cloud9's `zig build test`); `zig build fs-bench` measures -filesystem transactions in the core. -`zig build fs-test quic-test -Dquic=true` also exercises QUIC mounts and I/O. +`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/open-questions.md b/docs/open-questions.md index d4b5699c..3f365ce9 100644 --- a/docs/open-questions.md +++ b/docs/open-questions.md @@ -1,76 +1,17 @@ # Open questions Decisions deliberately left open, with what is known so far, so they can be -picked up without redoing the research. +picked up without redoing the research. None is open now. -## Where does an unknown command word run? +## Decided -Raised 2026-09-27 in the review of pardes against acme (Plan 9 source at -`~/05-genizah/principia-softwarica`). - -**Decided 2026-09-28: a command pane.** Clicked or written at an interactive -terminal at its prompt, the line is typed into that shell, whose state the -clicker can see. From anywhere else it runs as its own terminal pane whose -child is the root ctl's `Shell` run with `-c` and the line, in the pane's -directory: full emulation -(colours, `less`, `vim`, `sudo`'s prompt work), the exit status from waiting -on the child (`exit N`, `exit 127` for a misspelling, no prompt marks -needed), Kill signalling its process group, `run`/`exit` records in the log, -and `exec` answering its serial. One command pane per directory is reused -once done, and keeps what it showed, as acme appends to `+Errors` and never -clears it (util.c:213-221). Chosen over acme's process-into-`+Errors` because -it keeps interactive programs working without a streaming runner, and over -a PATH check before typing into a shell, which misjudges builtins, aliases -and functions and leaves the shell's state in the way. The analysis below is -what it was decided from. - -**Today.** A middle-click, an `exec` write or a tag word that is not a builtin -is typed into a terminal pane: the pane itself when it takes a command line, -else a shell for the pane's directory (`execute`, `ttyForDir` in -`src/exec.zig`). The command shares that shell's cwd, environment, history -and aliases, and its output lands in the terminal. - -**acme.** An external command runs as its own process: stdin from -`/dev/null`, stdout and stderr to the directory's `+Errors` window, `$winid` -and `%` set for it (`editors/acme/exec.c`, `run()` and its callers around -:1180-1300 and :1425). No shell state is shared between commands. - -**What each costs.** - -* Typing into a shell: interactive programs work, and so do aliases and - functions. But every command depends on that shell's state (is it busy, - which directory it is in, what was typed at its prompt), and a misspelled - builtin silently becomes a shell command. The 9P `exec` file cannot report - that the command failed. -* A process per command: the misspelling hazard goes away at the root, the - editor knows each command's exit status and output, and `exec` could answer - the way `pty/run` does. But aliases, shell functions and interactive - programs need a terminal of their own, and output goes to a `+Errors`-style - buffer rather than a live terminal. - -**Evidence from use (2026-09-28).** A fresh agent driving pardes through -9P wrote a misspelled word to `exec`; it was typed into a terminal's fish -shell ("Unknown command"), nothing reached `log`, and the write succeeded. -Its report ranked this among the confusing behaviours. - -**Related.** `pty/run` (see `docs/fs.md`) already gives an agent the second -behaviour inside a chosen terminal: a line in, `exit N` and its output out. -The planned root `ctl` (session builtins) and pane `ctl` (pane builtins) -refuse unknown words, whatever is decided here; the question is only what -`exec` and the middle-click do with them. - -## Where does an exec from a code file go? - -**Decided 2026-09-28: to a REPL bound for its language, when one is.** -`Repl python` binds a terminal; then an exec made by a gesture (a middle -click, the execute key, a selection or a single word) on the body of a file -in that language, or on the REPL's own body, is typed into the REPL -instead of run. What stays a command whatever is bound: a builtin's word, -a word in a tag, `Exec <text>` run by name, a command word @`cmd` in the -text, and a 9P `exec` write (a script writes the REPL's `pty/data`). An -event record written back is done as the click it was, REPL and all. With -several bound the pane asks which, one key answering, and remembers -nothing. A REPL takes text only while its program has the terminal, and a -done command pane cannot be bound. Chosen over a per-file or per-directory -binding, which would need a place to keep and show it; bindings are not -dumped. `docs/fs.md` has the behaviour. +- **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 + 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 + or per-directory binding, which would need a place to keep and show it. diff --git a/docs/tags.md b/docs/tags.md index 7671e056..8fe63804 100644 --- a/docs/tags.md +++ b/docs/tags.md @@ -1,313 +1,161 @@ -# Editable tags +# Tags and columns -Pardes has three levels of command text: the workspace tag, one tag per -column, and each pane's tag. Column commands act on that column and run in -its active pane (or its first pane when focus comes from another column); a -column with no pane is described under [empty columns](#empty-columns). This makes -`New`, `Tty`, `Find`, and `Grep` available beside the work they act on. -A command or a `Tty` run from the workspace tag or a column tag starts in -the session's directory, where pardes was started, as acme's row and column -tags have no directory of their own; one run from a pane's tag or text -starts in that pane's directory. -`New` appears only in the column tag by default; pane tags keep their own -save, terminal, close, and collapse commands. `Tty` opens a new embedded terminal. -In a terminal's own tag the word names the shell it runs, `Tty+fish`, tinted -with the tag's name colour: `Tty+arg` is one word a tag can hold for `Tty -arg`, so clicking it opens another terminal on that shell, as `Tty fish` -would. Only a builtin that declares `plus_arg` reads a `+` so; `Tty` is the -one, since other arguments (a dump's name) may contain `+`. A command pane's tag reads `<dir> (<line>) running` or `exit N` -and offers `Kill`. `Repl python` in a terminal's tag binds it as Python's -REPL, its id, `python-a`, beside the Tty word: a middle click or the execute -key on a `.py` body then types the text into it rather than running it, and -several bound ask which. `Repl` takes any language a code fence names -(`py`, `Python`, `sh`): 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; -an unknown one is refused, naming a few of these. The tag's own words, `Exec <text>` by name and a -command word @`cmd` still run as commands (docs/fs.md). -A tag word runs as acme's does, in the pane's directory with no file named: -`wc` alone waits on its stdin. pardes has no `$%` for the pane's file; name -it (`wc notes.txt`), or select the name and middle-click `wc`, which takes a -held selection as its argument. -Pane and column command text leave a small gap after their aligned drag grips. -GUI pane mode symbols are centered by their visible ink; changing the font or -its size refreshes the cached measurements. -The column grip has a different -color from a pane grip. It stays blank and muted even when its column is active, -and lights in its full accent while it is held. -Drag it past a neighbour's middle to move the whole column there; dropped short -of that, it moves the column's left edge instead, as a border drag would. While -it is held, a dashed rail in the grip's accent stands where the column's left -edge will land, beside the rule as a border drag's rail does. Only columns -between the old and new positions shift. Each column keeps its width, panes and -command text. - -Two panes in a column are resized as acme's windows are, by the grip: drag -a pane's grip up or down its own column and its top follows, the pane above -taking or giving the rows, either one down to its tag alone (acme's -coldragwin keeps a window its tag line); drop it in another column and the -pane moves there. In the GUI the 2 px rule between two panes also drags -their seam. No row of text is a handle: a pane's last row takes a click like -any other, and a terminal, which draws no rule, resizes by the grip. A -column's right edge still drags its width. - -When Look opens the first document and its source column is too narrow for -a new column, it splits below the originating pane, just like `Tty`, instead -of inserting at the top of the leftmost column. The originating terminal is -kept. As with `Tty`, a tagline-only source uses a roomier split parent. - -`Collapse` reduces a pane to just its tagline, giving all its body space to -the nearest expanded pane above, or below if none is above. Other panes keep -their heights. Execute it again to reclaim its former height from that one -neighbor, as far as available space allows. If every pane is collapsed, the -unused area stays blank. -Its text and terminal process are kept; -the command stays available in the visible tag. Every default pane tag includes -`Collapse`, including file, terminal, image, and PDF panes. - -`Edit` is acme's: the rest of the line is sam's command language, run on -the pane's body (`Edit ,x/foo/c/bar/` renames every `foo`), from a tag, a -pane's `ctl` or `exec`. Addresses are the `addr` file's, `line:col` -included; the commands are `x y g v c a i d s p = m t`, `u` alone, and `{ }` -with a command per line. All its changes are one undo step, applied only -if every command ran; an error says why in acme's words and changes -nothing. `p` and `=` print to the directory's `+Errors`. Left out: the file -commands `b B D e r w f X Y`, the pipes `< | >`, and `\1`-`\9` in `s`, -since mvzr keeps no submatches; acme applies changes that come out of -sequence with a warning, pardes refuses the Edit. In `s`, `&` in the -replacement is the matched text (`\&` a plain `&`); in `c`, `a` and `i` an -`&` is only an `&`, as in sam. A pattern that finds nothing says so with the -pattern (`no match for regexp /nomatch/`), and an `s` on an empty dot says -`no substitution: dot is empty`. A loop that finds nothing is no error, as -in sam: `Edit ,x/zzz/c/bar/` with no `zzz` changes nothing and succeeds, -silent, where `,s/zzz/bar/` says `no substitution`. - -`Undo` and `Redo` are acme's: typed or clicked in a pane's tag, or written -to its `ctl`, they step the body back and forward through its edits, as the -`u` and `U` keys do. They are not in the default tags. +Pardes has three levels of command text: the workspace tag, a tag per +column, and each pane's tag. A column tag's words act on that column and run +in its active pane (its first pane when focus comes from another column). A +command or `Tty` run from the workspace or a column tag starts in the +session's directory (where pardes started), as acme's row and column tags +have none of their own; one run from a pane's tag or text starts in that +pane's directory. Over 9P the tags are `/tag`, `/col/<n>/tag` and +`/pane/<n>/tag` ([fs.md](fs.md#columns-and-tags)). -`Del` closes a pane and gives its rows to one neighbor; the rest of the -column keeps its heights. A pane with unsaved text is refused once, as -acme's Del warns (exec.c del, wind.c winclean): a short notice, `1 unsaved -pane — Del again to discard`, the pane listed in a kept `+Unsaved` (`<name>: -Modified`, then `Del again to discard`), and the same `Del` again, nothing edited since, closes it and -throws the text away; `Delcol` refuses a column holding such a pane the same -way (exec.c delcol), but changes nothing else in refusing -- no `+Unsaved` -opens, no focus moves; its `unsaved` records and notice say which. Each word warns on its own, from a key, a tag, `exec` -or a ctl, where the refusal fails the write with EIO. `Del k` (or `DelAbove`) gives them to the nearest -expanded pane above, `Del j` (or `DelBelow`) to the one below, each falling -back to the other side. Bare `Del` from the keyboard (`SPC d`, or Enter on -the word) on a pane with expanded panes both above and below asks on the -pane's notice band: `k` or Up gives the rows above, `j` or Down below, and -any other key keeps the pane. A click, a 9P write or a startup line never -asks; there bare `Del` gives the rows to the nearest document above, as it -always has. A collapsed pane is never asked about, and collapsed neighbors -are passed over: they only keep a weight for later. +## Default words -A tag is a text like a pane's body: pane tags, column tags and the -workspace tag all edit with the body's own normal and insert modes, undo -included. A pane's tag may hold more than one line, and a line wider than -the pane wraps onto more rows, as acme's tag does: it takes a row per shown -line, up to eight, and leaves its body at least one row. A line exactly as -wide as the pane stays one row, its caret at the tag's edge past the last -character, as acme's tick is (in a terminal, on the last cell). A word the -wrap breaks across rows is still one word to a click. A collapsed pane shows -only the first row. +- Workspace: `Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor + Debug Exit`. +- Column: `New Tty Find Grep Joincol Delcol`. +- File pane: `Save Tty Collapse Del`; a source file with a grammar adds + `TreeContext`, a result list `LocationsConfig`. PDF: `manual.pdf [1/12] + Tty Del PdfSections PdfTint Collapse`. +- Terminal: `Tty+bash Save Mode Filter Collapse Del`. `Tty+bash` is one word + for `Tty bash`, opening another terminal on that shell (only `Tty` reads a + `+` so). `Mode` cycles raw terminal input, normal mode and insert mode. +- Command pane: `<dir> (<line>) running`, then `exit N`, and `Kill`. -A pane tag starts expanded, showing all its rows, and can be collapsed to -one row, as acme's Tagup does. In the tag's insert mode Up on its first row -collapses it and Down on its last row expands it; Alt-Up and Alt-Down do -the same in either mode. acme expands or collapses on any arrow key typed in -the tag (Up collapses, the others expand); here arrows keep moving the -cursor, and only these keys at the tag's edges change its height. The column and workspace tags -are one line, as acme's (cols.c:244 gives a column tag one font height): a -newline typed, pasted or written into one becomes a space, and a dump that -holds a column or workspace tag of several lines, from before, comes back -with its lines joined by spaces. The path and a PDF's page at the start of a -pane tag are computed, never stored, and read-only. The keyboard reaches -them as the mouse does: `0` goes to the line's start, the path's, as in -acme, and motions select and yank across the path and the commands. An edit -that would change them is refused and leaves the cursor where it was; typing -into a file's path instead drafts a new name, as clicking it does. When they -grow or shrink (a rename, a PDF's page) the cursor keeps -its place in the text after them. +`Undo`, `Redo` and `Mode` (on files) work typed or clicked though they are +not in the default tags. Customized tags keep their text. -Left-click a tag or a header to type into it at the click, in insert mode. -A pane tag wraps instead of scrolling sideways; a column or workspace tag -reveals the caret horizontally when its text is wider than its column. -Esc is normal mode, where everything a body's normal mode does works. There -Tab executes the word under the cursor (or an explicit selection), and Enter -looks it up in a pane's tag but runs it in a column or workspace tag, whose -words are all commands; in insert mode Enter is a new line. Executing gives -the keyboard back to the body first, so `Del`, `Kill` and `Restore` never return into a tag that is gone. -Pasted text goes to the focused tag, not to the file or embedded shell -beneath it. +A tag word runs as acme's does, in the pane's directory with no file named: +`wc` alone waits on stdin. There is no `$%`; name the file (`wc notes.txt`), +or select the name and middle-click `wc`, which takes a held selection as its +argument. -`:` is the one key a tag and a body do not share: in the body's normal mode -it focuses the pane's tag in normal mode, and in the tag's normal mode it -goes back to the body; in a column or workspace tag it goes back to the -active pane. Each pane, column and workspace tag remembers its own cursor -during the session; the first `:` into a pane tag starts on its `Save`. A -tag's text changing under it (a 9P write, a shorter tag) pulls the cursor -back inside it. +`Repl python` in a terminal's tag binds it as that language's REPL +([fs.md](fs.md#repls)). `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`. -Moving between tags is the window keys' job, as it is between bodies -(`Ctrl-w` or `SPC w` with `h/j/k/l`): they move to the neighbouring pane's -body. Up from a pane with nothing above it reaches its column's tag, then -the workspace's; Down comes back the same way, and Left and Right walk the -column tags. +## Pane builtins -Search (`/`), `s`/`S`, pipe (`|`) and Save's path prompt are not typed into -the tag: each gets a line of its own on the pane's notice band, with the -body's insert-mode keys. Pressed in a tag, `s`, `S` and `|` answer for the -tag's own text, as they do for a body, and a header's go on the active pane's -band; `/` searches the body wherever it is pressed, as acme's Look from a tag -does. +`Collapse` folds a pane to its tagline, giving its rows to the nearest +expanded pane above (else below); again, it takes them back. Its text and +process are kept. -Pane filenames and commands now have a single separator space rather than -generated right-alignment padding. Intentionally customized spacing is kept. +`Del` closes a pane, giving its rows to one neighbour. `Del k` (`DelAbove`) +gives them to the nearest expanded pane above, `Del j` (`DelBelow`) below, +each falling back to the other side. Bare `Del` from the keyboard (`SPC d`), +with expanded panes above and below, asks on the notice band (`k`/Up above, +`j`/Down below, any other key keeps the pane); a click, a 9P write or an +`init` line never asks and gives the rows above. -## File names +A pane with unsaved text is refused once: a notice `1 unsaved pane — Del +again to discard`, the pane listed in `+Unsaved`, and over 9P the write +fails (EIO). The same `Del` again, nothing edited since, discards. `Delcol` +refuses a column holding such a pane the same way, without opening +`+Unsaved` or moving focus. `Exit` and `Restore` do the same over the whole +session. A `+New` scratch under 100 bytes is never asked about. -Clicking a file pane's name, or typing into it from the tag, starts a draft -of it, typed into where it was clicked or typed. -Enter or Tab confirms the new buffer name; Escape or leaving the pane -cancels the draft. Confirmation changes the -buffer's save target and marks it unsaved. It does **not** rename, create or -overwrite a disk file. A subsequent explicit `Save` writes the buffer to its -committed name. Executing a pane-tag command confirms a valid name draft -first, so a visible draft cannot silently save to the previous name. +`Edit` runs sam's command language on the body ([fs.md](fs.md#edit)). +`Undo` and `Redo` step the body through its last 256 edits, as `u` and `U`. -Terminal working directories and generated image/PDF status remain managed -by their corresponding commands. Their command tails are editable just like -file command tails. +Unsaved text shows on the pane's grip, as acme's modbutton, not in the tag. +In a terminal the grip is two cells: the pane's mode (blank normal, `^` +insert, `$` tty mode), then `*` while unsaved. -PDF tags follow the same filename-first layout: `manual.pdf [1/12] Tty Del -PdfSections PdfTint Collapse`. Sections and tint commands remain in the editable -tail, without displaying the current tint state. `PdfFit` (`SPC t z`) remains -available, as do the shortcuts for `PdfTint` (`SPC t i`) and `PdfSections` -(`SPC t s`, or `f` on a PDF). +## Editing tags -Terminal tags include `Mode`, which cycles through raw terminal input, normal -editor mode, insert mode, and back to terminal input. On files and text output -panes, `Mode` cycles between normal and insert mode; it is available as a command -but does not appear in their default tags. Images and PDFs keep their normal mode. -Executing `Mode` from a tag leaves the tag and advances the parked body mode. +A tag is text like a body, with the body's normal and insert modes and undo. +A pane tag may hold several lines and wraps, taking a row per shown line up +to eight and leaving its body at least one; a collapsed pane shows the first. +Up on its first row in insert mode (or Alt-Up in either mode) folds a tag to +one row, Down on its last (Alt-Down) unfolds it. Column and workspace tags are +one line: a newline typed, pasted or written into one becomes a space. -Ctrl-B keeps its terminal/editor toggle. Existing custom tags can still use -`Togglettymode` for that two-way terminal toggle. Old default terminal tags -upgrade to `Mode`; customized command text is preserved. +The path (and a PDF's page) at the start of a pane tag is computed and +read-only; `0` goes to its start, and motions select and yank across it. +Typing into a file's path, or clicking it, starts a draft of a new name: +Enter or Tab confirms it, Escape or leaving the pane cancels. Confirming +changes the buffer's save target and marks it unsaved; nothing on disk is +renamed until `Save`. A pane-tag command confirms a valid draft first. -## Saved workspaces +Left-click a tag to type at the click, in insert mode. Esc is normal mode: +there Tab executes the word under the cursor (or the selection); Enter looks +it up in a pane's tag and runs it in a column or workspace tag. Executing +gives the keyboard back to the body first. Paste goes to the focused tag. -`Dump` and `Restore` preserve customized workspace and column tags, including -intentionally empty tags, and columns that hold no pane. New columns start with the standard column tag. -Closing a column keeps surviving columns' tags; `Joincol` keeps the destination -column's tag. Old dumps without these optional fields retain the defaults. -The automatic `Restore` shortcut does not overwrite a customized workspace -tag. +`:` in a body's normal mode focuses its tag (the first time, on `Save`); `:` +in a tag goes back. The window keys (`Ctrl-w`, `SPC w` with `h/j/k/l`) move +between panes; up from the top pane reaches its column's tag, then the +workspace's, and Left/Right walk the column tags. Search, `s`/`S`, pipe and +Save's path prompt get a line on the pane's notice band; in a tag `s`, `S` +and `|` act on the tag's own text, while `/` searches the body. -The column row stays above panes with `TagBottom` enabled. On screens shorter -than three rows it is omitted so a pane still has room. SDL and TTY share the -same tag text, editing and layout; SDL additionally uses compact font sizing -and subtle pixel separators. +## Moving and resizing -The column row is always shown. An old `ColumnTags` line in an init file is -ignored, with a message saying so. -`FocusTint` controls active-column and active-pane emphasis. SDL also honors -the shared bold, underline, and strikethrough attributes, including diagnostic -underlines and the optional `SyntaxBold` keyword weight. +Drag a pane's grip up or down its column and its top follows, the pane +above giving or taking rows, down to its tag alone; drop it in another +column and it moves there. In the GUI the 2 px rule between panes drags too. +A column's right edge drags its width. Drag a column's grip past a +neighbour's middle to move the whole column there; short of that it moves +the column's left edge. -Unsaved text shows on the pane's grip button, as acme's modbutton does, -not in the tag: the tag names the file and nothing more. In a terminal the -grip is two cells: the second is `*` while the pane holds unsaved text, and -the first is the pane's mode: blank in normal mode, `^` in insert, `$` when -the keyboard goes to a terminal's program (tty mode). The `dirty` file -and `index`'s flag say the same to a script. +A terminal keeps its tag and 2 body rows: no drag, squeeze or smaller +window takes it below that, and `pty/ctl`'s `winsize` gives a pty 2 rows at +least. A text pane can be dragged down to its tag. ## Empty columns A column can hold no pane, as acme's can: its tag stands over blank space in -the theme's `empty_col` colour, white in the acme theme as acme paints it -(cols.c:186-188), and the frame's border fill in themes that do not set it. `Newcol` makes an -empty column right of the keyboard's and gives its tag the keyboard. Closing a -column's last pane (`Del`, `Del k`/`Del j`, a shell exiting, a drag to another -column) leaves the column empty where it was, and the keyboard goes to its tag -if it was on that pane. Only `Delcol` and `Joincol` take a column away. - -`Delcol` and `Joincol` from a column's tag act on that column; `Delcol` -written to a pane's ctl closes that pane's column. A pane dragged onto an -empty column fills it. +the theme's `empty_col` colour. `Newcol` makes one right of the keyboard's +and gives its tag the keyboard. Closing a column's last pane leaves the +column empty, the keyboard on its tag. Only `Delcol` and `Joincol` take a +column away; `Joincol` keeps the right column's tag, its panes below. A pane +dragged onto an empty column fills it. `Delcol` of the last column leaves +the workspace tag alone; `Newcol` or `New` starts again. Unlike acme, pardes +quits when the session's last pane closes. ## Where new panes go -Every new pane goes through one placement, chosen by the `Placement` setting: -`acme` (the default) or `pardes`. `Placement pardes` or `Placement acme` sets -it, in an init file, a tag or the root ctl; bare `Placement` flips it; `SPC c -p` is its leader path, and `Config` reports it. +Every new pane goes through one placement, chosen by `Placement acme` (the +default) or `Placement pardes` (bare flips it; `SPC c p`). -Under either, no placement leaves a pane, new or split, shorter than its tag -and two body rows (acme's minht keeps one). Where the place chosen has not -that room, the column's tallest pane is halved instead; where no one pane -can give it but the column's rows hold every pane's tag and two rows with -the new one's, the rows are shared out again, each at least its minimum; -only when they do not -- the arithmetic, not the halving, decides -- is -the new pane refused and closed, with `no space for a -pane in that column: each keeps its tag and 2 rows` (a 9P write or open of -`pane/new` fails with it, ENOSPC). A pane alone in its column always fits. +No placement leaves a pane shorter than its tag and 2 body rows. Where the +chosen place has not that room, the column's tallest pane is halved; where +no one pane can give it but the column holds every pane's minimum with the +new one's, the rows are shared out again; otherwise the pane is refused, +`no space for a pane in that column: each keeps its tag and 2 rows` (ENOSPC +over 9P). A pane alone in its column always fits. -After placement a terminal keeps that floor too: no drag of a grip, no -squeeze of its column by the others' weights and no smaller window takes -it below its tag and two body rows (a one-row terminal loses its prompt's -mark and would read busy for ever); its neighbours give the rows, and only -a window too short for every floor leaves it less. A text pane keeps -acme's way and can be dragged down to its tag alone; acme has no -terminals to follow here. A folded terminal (Collapse) is a tag by choice. -`pty/ctl`'s `winsize` likewise gives a pty two rows at least. -A `+Errors` pane goes to any column with room, the last first; with none, -what it would have shown (an Edit's `p` or `=`, a write to `errors`) is -logged as `msg` records, a line each, and the Edit still succeeds. +`Placement acme` is acme's makenewwindow. The active column is the one last +typed or clicked in, dropped into, whose tag was given the keyboard, or that +was given the last new pane; a Look moves the keyboard, not the active +column. A new pane goes into the column whose tag the command came from, +else the active column, never into a new column: -`Placement acme` is acme's makenewwindow (util.c:449-495). The core keeps -acme's *active column* (activecol, dat.c:37): the column last typed in -(acme.c:487), clicked in with the select button (acme.c:659), dropped into by -a grip (acme.c:640), whose tag was given the keyboard (`Newcol`, an emptied -column, `Ctrl-w k`), or that was given the last new pane (util.c:467). A Look -click moves the keyboard but not the active column, as button 3 does not in -acme. A new pane goes into the column a command's tag belongs to when it came -from a column tag, else the active column, else the keyboard's pane's, and -never into a new column: +- an empty column it takes whole; +- from a tag, or 9P's `pane/new`, it takes the bottom half of the column's + last pane; +- from a pane's text (a Look, `Tty`, `Alt-n`, a Grep or Find listing), it + goes under the text of the pane with the most blank rows when that is + more than 15 rows, or more than 3 and more than half the biggest pane; + otherwise it halves the biggest pane, or the asking pane when that is in + the column and not much smaller; +- `New` goes to the bottom half of its own column's last pane; +- a command pane or `+Errors` pane goes to the last column's last pane (a + command from a column's tag to that column, reusing a finished command + pane only there). With no room anywhere, `+Errors` text is logged as `msg` + records. -- an empty column it takes whole (util.c:468-469); -- from a tag, or 9P's `pane/new` (acme's `t->w == nil`), it takes the bottom - half of the column's last pane (coladd, cols.c:62-65); -- from a pane's text (a Look, `Tty`, `Alt-n`, a Grep or Find listing), it goes - right under the text of the pane with the most blank rows when that is more - than 15 rows, or more than 3 and more than half the biggest pane - (util.c:482-486); otherwise it halves the biggest pane, or the asking pane - when that is in the column and not much smaller (util.c:487-491); -- `New` goes into its own column, the bottom half of its last pane - (look.c:921-923); -- a command pane or a `+Errors` pane goes to the last column, the bottom - half of its last pane (util.c:94-98); a command run from a column's tag - goes to that column instead, and reuses a finished command pane only in - that column. +`Placement pardes`: an empty column whose tag asked, or has the keyboard, is +filled; a scratch goes under the pane that asked; a shell under it or the +nearest pane with room; a document beside the last one read, or in a column +of its own on the left when there is none and the column is at least 200 +cells wide; a command pane at the foot of the last column. -`Placement pardes` is what pardes did before: an empty column whose tag asked, -or has the keyboard, is filled; a scratch goes right under the pane that asked; -a shell under it or the nearest pane with room; a document beside the last one -read, or in a column of its own on the left when there is none and the column -is at least 200 cells wide; a command pane at the foot of the last column. - -`BootShell replace` brings back one more placeholder: a document dragged into -the left column closes the column's lone shell if nobody has typed into it. -`BootShell keep`, the default, never closes a pane for another one. - -Down from an empty column's tag stays there, and the pane-to-pane keys pass -over an empty column; Left and Right from a tag walk every column's tag. +## Saved workspaces -`Delcol` of the last column does as acme's does: the column goes and the -window stays, empty but for the workspace tag, where `Newcol` (or `New`, -which makes the column it goes in) starts it again. One divergence from -acme remains: acme keeps running when its last window closes; pardes quits -when the session's last pane closes by `Del`, a shell exiting or the like. +`Dump` and `Restore` keep workspace and column tags (empty ones too) and +empty columns ([config.md](config.md#dumps)). The column row stays above +the panes with `Tagbottom` on; on screens under three rows it is left out. diff --git a/docs/v9fs.md b/docs/v9fs.md index a105fe06..26460658 100644 --- a/docs/v9fs.md +++ b/docs/v9fs.md @@ -1,114 +1,56 @@ -# Linux terminals with a kernel 9P mount +# Tty9p: a terminal with a kernel 9P mount -Execute `Tty9p`, or press `SPC n 9` in editor mode, to open a terminal below -this pane with the current Pardes session mounted through Linux v9fs. - -The new pane asks for your sudo password when needed. After mounting, it starts -your configured shell as your normal user, with your account's supplementary -groups. The shell receives `PARDES_MOUNT`, the absolute mountpoint: +On Linux, `Tty9p` (`SPC n 9`) opens a terminal below this pane with the +session mounted through the kernel's v9fs. The pane asks for your sudo +password, mounts, and starts your shell as your normal user (with your +supplementary groups). The shell gets `PARDES_MOUNT`, the mountpoint: ```sh -ls "$PARDES_MOUNT/pane" cat "$PARDES_MOUNT/index" -cat "$PARDES_MOUNT/README" cat "$PARDES_MOUNT/pane/$PARDES_PANE/body" echo 'Msg hello' > "$PARDES_MOUNT/exec" n=$(cat "$PARDES_MOUNT/pane/new") ``` -**Opening** `pane/new` makes a pane, and reading that open file answers its -serial; address the pane as `pane/<serial>` from then on. Each open makes -another one, so read it once and keep the number. A *stat* makes nothing, -which is why `new` can be listed at all: `ls`, `ls -l` and `find` over the -whole mount create nothing, because none of them open it. - -The mount belongs to that pane's subprocess tree. Other panes and the editor -core keep their original mount namespace. It works in native Linux TTY and SDL -sessions, including detached sessions. A frontend attaching from elsewhere does -not perform the mount; the session host starts the new terminal. - -## Build and setup - -The normal Linux build installs the ordinary `pardes-v9fs` executable beside -`pardes` and `pardes-gui`: - -```sh -zig build -``` +The files are those of [fs.md](fs.md). The mount lives in that pane's +private mount namespace: other panes and the editor do not see it, so a +Look at a path under `$PARDES_MOUNT` opens nothing. Each `Tty9p` makes its +own mount and takes one of the session's 16 Unix/TCP connection slots. It +works in TTY, SDL and detached sessions (the session host starts the shell, +not an attached frontend). Dump/Restore does not remake the mount. -Start an updated editor to get the builtin. An already running core retains -its old code. The host resolves the helper beside its own executable; -`PARDES_V9FS_HELPER=/absolute/path/to/pardes-v9fs` overrides this for development -builds whose editor and helper live in different build-cache directories. +## Setup -Linux must support `9p` and its Unix transport (`9pnet_fd`). v9fs mounting needs -`CAP_SYS_ADMIN` in the initial user namespace, which sudo supplies. The launcher -uses `sudo -E` to retain the shell environment; local sudo policy must allow -that. It restores the caller's PATH after dropping privileges because sudo's -`secure_path` can replace it even with `-E`. +The build installs `pardes-v9fs` beside `pardes`; the host looks for it +beside its own executable, or at `PARDES_V9FS_HELPER` (an absolute path). +A running editor keeps the code it started with. -No setuid installation, passwordless sudo policy, system group, or FUSE is -installed. The helper currently accepts explicit mount paths and a command; -it is not a restricted privilege broker. Do not grant it blanket passwordless -access. A group-authorized helper would require a separate restricted design. +Linux needs `9p` and its Unix transport (`9pnet_fd`); mounting needs +`CAP_SYS_ADMIN`, which sudo supplies. The launcher runs `sudo -E` (local +policy must allow it) and restores the caller's `PATH` after dropping +privileges. Nothing setuid, no passwordless sudo rule and no FUSE is +installed. The helper takes explicit mount paths and a command; it is not a +restricted privilege broker, so do not grant it passwordless sudo. -## Runtime organization +## How it works -`src/linux/v9fs.zig` builds `pardes-v9fs` and provides helper discovery to the -native host. `Tty9p` marks the new pane for a mounted shell; `host_io.forkShell` -starts the normal interactive shell and queues a quoted helper command. For -bash and fish, the command waits for the shell's prompt-ready mark. The helper -runs as a foreground shell job, so sudo uses the terminal like a manually run -command. Authentication failure or cancellation returns to the original shell; -exiting the mounted shell also returns there. +`Tty9p` starts the normal shell and queues a quoted helper command, which +bash and fish run once their prompt is ready, as a foreground job, so sudo +uses the terminal. A failed or cancelled authentication returns to that +shell, as does exiting the mounted one. The unprivileged launcher makes a +private temporary mountpoint and runs sudo; the elevated helper makes a +private mount namespace, mounts the session's socket with +`trans=unix,version=9p2000,cache=none,access=any,nosuid,nodev,noexec`, drops +every root id and capability, and executes the shell. The namespace, and +the mount, go when its last process exits. Code: `src/linux/v9fs.zig`. -The unprivileged launcher creates a private temporary mountpoint and invokes -sudo inside the new PTY. The elevated helper creates a private mount namespace, -makes propagation recursively private, and mounts the session's Unix socket -with `version=9p2000,cache=none,access=any,nosuid,nodev,noexec`. It restores the -calling user's account groups, drops all real/effective/saved root IDs and -mount capabilities, and executes the shell. Ordinary commands such as sudo -remain available for subsequent explicit authentication. The launcher waits for sudo, -forwards termination signals, and removes its empty temporary directory on exit. -Namespace destruction releases the mount when its last process exits. - -The core stays outside the mount namespace, which is the helper's private -one: a path under `$PARDES_MOUNT` names nothing in the editor's own namespace, -so a Look on one from that shell opens nothing. - -Each `Tty9p` currently creates its own mount and consumes a server connection; -the session has four application connection slots across all transports. This -is the explicit per-pane version. A shared launcher for all pane shells remains -future work. Detached sessions retain the running mounted pane when frontends -leave; Dump/Restore does not reconstruct mounted-shell namespaces. Descendants -that deliberately outlive the terminal may retain their namespace until exit. +Linux follows `O_TRUNC` with a `Twstat` of zero length and an `mtime` hint; +pardes takes the truncation and drops the hint. ## Tests ```sh -zig build v9fs-terminal-test -Dplatform=tty -zig build v9fs-driver-test -Dplatform=tty -zig build 9p-test -Dplatform=tty -sudo -v -zig build v9fs-test -Dplatform=tty +zig build v9fs-terminal-test -Dplatform=tty # builtin, launcher, cleanup; a sudo stand-in, no mount +zig build v9fs-driver-test -Dplatform=tty # the probe launcher, no privileges +sudo -v; zig build v9fs-test -Dplatform=tty # a real kernel mount (sudo -n); fails, not skips, without it ``` - -`v9fs-terminal-test` exercises the builtin, real launcher, PTY input, session -environment, quoted helper paths, hidden password input, and core responsiveness -using an unprivileged sudo stand-in. It checks that authentication failure and -interruption clean up the temporary mountpoint and leave the original shell usable. It does not claim kernel-mount coverage. - -`v9fs-test` mounts through the same runtime helper. Its driver keeps the editor -unprivileged and uses `sudo -n`, retaining the calling terminal's authorization. -Missing authorization or kernel support fails the test instead of skipping it. -It checks mount isolation, privilege dropping, inherited access, directory -refresh, body reads and truncation, independent wire updates, addressed edits, -shell redirection to name, Exec dispatch, rendered screen JSON, and OS-file reads. - -Linux follows `O_TRUNC` with a `Twstat` carrying zero length and an `mtime` hint. -Pardes accepts this truncation without storing caller-selected timestamps; -standalone timestamp, permission, and ownership changes remain unsupported. - -The initial kernel probe passed on this host on 2026-09-14. The broader -`fs-test` has an existing syntax-bold assertion failure (now `test/fs.py:615`), -also reproduced on the cached editor binary preceding the truncation fix. |
