From 57b30ba3e38153a4446626449b0fed5120da954c Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Tue, 29 Sep 2026 19:25:00 -0300 Subject: The docs and the 9P skill say each fact once, in the file that owns it, and say only what a live session does Co-Authored-By: Claude Opus 5.5 --- docs/cloud9.md | 147 ++-- docs/config.md | 834 +++++------------------ docs/detached.md | 76 +-- docs/divergences.md | 116 +--- docs/fs.md | 1773 ++++++++++++++++++------------------------------ docs/open-questions.md | 87 +-- docs/tags.md | 452 ++++-------- docs/v9fs.md | 142 ++-- 8 files changed, 1109 insertions(+), 2518 deletions(-) (limited to 'docs') 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 - -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 -`git@git.sr.ht:~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#`, and swap in -`.cloud9 = .{ .path = "../cloud9" }` while editing both packages at once. - -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. - -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. +# cloud9 + +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#`, or +use `.cloud9 = .{ .path = "../cloud9" }` while editing both. + +- 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. + +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/`, 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==` 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 - -* **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 `/` 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. +`$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/`, 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 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 +Native builds read one command file, `init`, from the per-user `pardes` +configuration directory: + +- Unix: `$XDG_CONFIG_HOME/pardes/init` (only an absolute `XDG_CONFIG_HOME` + counts), else `~/.config/pardes/init`. +- macOS: `$XDG_CONFIG_HOME/pardes/init` when set, else `~/Library/Application Support/pardes/init`. -- Windows: `%LOCALAPPDATA%\pardes\init`, 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 ` 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 ` is, and a -bare `Shell` goes back to the default. The root ctl reads `Shell `. `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. - -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. - -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 format is one existing builtin command per line, using the same spelling -and argument parsing as commands executed inside pardes: +- Windows: `%LOCALAPPDATA%\pardes\init`, else + `%USERPROFILE%\AppData\Local\pardes\init`. + +The browser build has none. A file over 1 MiB or unreadable counts as absent. + +Each line is one builtin, spelled as it would be executed in pardes: ```text Theme orchard @@ -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 ` 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 ` 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. - -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. - -`WindowOpacity ` 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. - -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`. - -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 ` 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 ` or `NextColor` -stops the custom-file watch. - -Execute `DumpThemes` to write every theme compiled into the executable to: - -```text -/themes/builtin/.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 ` 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 :` 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 ` 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. - -`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 ` 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. - -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. - -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 ` 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. - -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. - -## Panel and scene effects - -Exactly one panel transition is selected at a time. Executing its builtin a -second time turns it off; selecting another replaces it: +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. -```text -PanelSlide -PanelZoom -PanelDissolve -PanelAscii -PanelVertical -PanelEdges -PanelFall -PanelWave -PanelCurtain -PanelScramble -PanelType -``` +`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)). -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. - -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. - -Six further transitions are character *motion* over the same frozen/new grid -pair, and are composed in the core the same way: - -- `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. - -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. - -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 scene effect is `Crt`, at a level from 0 (off) to 3; `on` is 2: +## Settings -```text -Crt -Crt 3 -``` +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. -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. - -Three more bundled passes take the same levels, each off by default and -each costing nothing while off: +| setting | default | | +|---|---|---| +| `Theme ` | `orchard` | names as `Themes` (`SPC t t`) lists them ([themes.md](themes.md)); `NextColor` walks the ring | +| `ThemeFile ` | | 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 ` | `$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 ` | `$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 [:]`, `Fonts` | | SDL and macOS only; size 8-72 (pixels in SDL, points on macOS) | +| `TaglineSize <1-100>` | 82 | tagline face, percent; SDL and macOS | +| `WindowOpacity <0-100>` | 100 | SDL only: everything but text and the cursor | +| `Ligatures` | on | SDL only; macOS draws CoreText's own | +| `Pet cat\|frog\|off` | off | SDL only: a sprite in the workspace tag's blank space | + +`Tty9p` (`SPC n 9`) is described in [v9fs.md](v9fs.md); the +`PARDES_V9FS_HELPER` variable points development builds at the helper. + +### Terminals + +Ctrl-B switches a terminal between raw input and editor mode. Plain Escape +at a detected shell prompt hops back to the previous pane; other keys, +Ctrl-O, Ctrl-W and modified Escape included, go to the program. `Mode` in the +tag returns to editor mode in place. Ctrl-V types the yank register and +Ctrl-Shift-V the desktop clipboard, through bracketed paste when the program +asked for it; neither reaches the program as a keystroke. + +`Filter` in a terminal's tag maps its ANSI colours through the theme, +keeping each foreground at least `tty_filter_min_contrast` (WCAG 1.5, in +`src/config.zig`) against its background. + +### Location results + +`LocationsConfig` with no argument prints the current settings as a line +that can be run again; with fields it changes only those: ```text -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 +LocationsConfig context:5 tscontext:on tslocations:off layout:stacked ``` -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): +- `context` (0): source lines shown above and below each match. +- `tscontext` (off): include the enclosing tree-sitter declaration headers. +- `tslocations` (on): show a location on each declaration header. +- `layout` (`stacked`): the location on its own line; `inline` puts it + beside the source, padded in groups of eight matches. + +An invalid field rejects the whole line; a repeated field's last value wins. +The settings survive Dump and Restore. Source analysis is cached for 64 +files and 64 MiB. + +### Effects + +Panel transitions, one at a time; running the active one again turns it +off: `PanelSlide`, `PanelZoom`, `PanelDissolve`, `PanelAscii`, +`PanelVertical`, `PanelEdges`, `PanelFall`, `PanelWave`, `PanelCurtain`, +`PanelScramble`, `PanelType`. All start off; the web shell has none. + +Scene passes (SDL GUI, and a GUI attached to a detached session), each at a +level 0-3 (`on` is 2), all off by default: `Crt`, `Bloom`, `Vignette`, +`Grain`. `Shader ` adds a Shadertoy file written for ghostty to +the chain (`Shader off` removes it; it recompiles when saved); +`ShaderAnimation off|on|always` says when the chain animates by itself. + +The focused pane can stand off the page: `Lift shadow|rim|auto|off`, +`InactiveDim `, `Motion off|crisp|smooth|bouncy|playful` +(default `smooth`), `SelectionGlow`, `HoverGlow`, `Occlusion`, `Parallax`, +`JumpTrail`, `ChipShadow`, `ThumbFlash`, `CursorBlink`, `GripWidth <50-300>`. +Most are GUI-only; `InactiveDim` works everywhere, and `JumpTrail`, +`ChipShadow` and `ThumbFlash` are the terminal's. No effect may lower the +contrast of text, the selection or a focus indicator +([effects.md](effects.md)). + +`EffectCode ` lists that effect's sources under `/virtual` when the +build embeds them (`-Dembed-sources=true`). `zig build shaders` refreshes +the committed SPIR-V with its GLSL. + +### Look preview + +Resting the pointer on text for about 32 ms (`look_preview_delay_frames`, 2 +frames) tints what a right click would look at, with no other effect. Set +`look_preview_delay_frames` to `null` in `src/config.zig` to turn it off. + +## Themes from files + +`ThemeFile themes/mine.zon` loads a complete theme; a malformed save keeps +the last good one, and `Theme ` stops the watch. `DumpThemes` writes +every compiled theme to `/themes/builtin/.zon`: copy one, +change its `.name`, and edit. The format is `pardes.Theme` as +`std.zon.stringify` writes it; [themes.md](themes.md) describes each role. + +## Dumps + +`Dump` writes `pardes--