summaryrefslogtreecommitdiff
path: root/docs/design.typ
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-08-12 13:32:43 -0300
committerGabriel Schneider <[email protected]>2026-08-12 16:07:33 -0300
commitbc89f57cb576e23a58572ec35f96db068367f1b4 (patch)
tree06f1ea2b7e95f229c10b316214ae904b9d43e343 /docs/design.typ
parentb424164922842796619cb6894ec46d729a8a6826 (diff)
downloadpardes-bc89f57cb576e23a58572ec35f96db068367f1b4.tar.gz
pardes-bc89f57cb576e23a58572ec35f96db068367f1b4.zip
docs: the tutor taught three keystrokes wrong, and the rest had drifted
The documentation had gone stale in the ordinary way -- claims that were true when they were written and that nothing since had been obliged to re-read. Some of them were load-bearing. THE TUTOR. It still said there is no multi-cursor, that NextColor cycles three themes, and that its practice blocks "are also run as unit tests (generated from this file by tutor_gen)" -- a tool that appears nowhere in the tree, and nothing anywhere parses a `# keys:` block. Left alone, that claim is what makes the next wrong block survive. Three of those blocks WERE wrong, and all three for one reason: since the helix motion model landed, w/e/f/t SELECT the range they cross, so `i` after one inserts at the SELECTION'S START. `w i Z esc` on "foo bar" gives "Zfoo bar", not the "foo Zbar" the file promised. They were written against a vim reading of the same keys. Every block in the file has now been run through `zig build hxdiff` against the real core and matches byte for byte, and the trap itself is written down in 3.3 rather than left to be rediscovered. The tutor gains a PART 4 for everything added since it was written -- PDF panes, the in-process ZLS backend, themes and fonts, the startup file -- and PART 3 gains counts (and which keys ignore one), f/F/t/T, the whole g table (bare `G` is a no-op; `ge` is the START of the last line), multiple cursors and the s/S regex pair, `m`, `]`/`[`, `|`, insert mode, and all fifty leader paths. THE REST. design.typ's line table claimed 7,626 lines against a real 38,048, and its rows did not sum to its own total; its Event/Effect boundary contract -- the part a shell author writes against -- named four variants that do not exist and omitted fourteen that do. lsp.md's probe count. config.md's theme-name rules, which as written could not reach a zed theme at all. helix-keys.md's Skipped section, holding five families that have since landed. macos.md's menu bar, undocumented, along with sixteen other claims. web.md on what the browser build can actually do. SOURCE COMMENTS that had rotted alongside them: `tag_normal` is a space, not the `•` its own comment describes; Wrap is ON by default, not off; a FontSel row is SELECTED by n and RUN by Tab, not run by n; the SPC paths in lsp.zig lost their `l` group prefix when the language group moved; and the differential suites are 481 and 561 cases, not 360 and 440. TWO THINGS FOUND BY DOCUMENTING THEM, both left standing and written down rather than papered over. Typing `[^\n]` at an s/S prompt panics: the live preview compiles every prefix, and `[^\` indexes an empty slice in mvzr's parseCharSet. Both the tutor and a waiver recommended that pattern as the workaround for `.` matching a newline; they now say what it costs and what would make it sayable. And `Exec` is a builtin, so an `Exec` line in the startup config types that command into a shell before the first frame -- the tutor said nothing in that file is ever sent to one. Nine adversarial reviews over two rounds, each with the hxdiff harness to execute what it doubted. The second round exists because the first round's fixes needed checking too, and it caught three regressions of my own -- one of them a probe count I had "corrected" away from the truth. Verified: unit-test, snap 87/87, hxdiff 481/0, hxparity 561/0, mupdf-check. docs/design.pdf regenerated. The tutor's first seventeen lines are byte- identical, which is what tutor.golden pins.
Diffstat (limited to 'docs/design.typ')
-rw-r--r--docs/design.typ284
1 files changed, 203 insertions, 81 deletions
diff --git a/docs/design.typ b/docs/design.typ
index 2492329d..ff013f49 100644
--- a/docs/design.typ
+++ b/docs/design.typ
@@ -12,16 +12,17 @@
= What it is
Pardes is a text environment in the acme tradition: columns of panes, each pane a
-tag line plus a body; the body is a live terminal, a file, or an image. The mouse
-carries meaning — left selects, middle executes, right looks. Everything on screen
-is text and all text is equally alive, whether the shell printed it or the user
-typed it.
+tag line plus a body; the body is a live terminal, a file, an image, or a PDF. The
+mouse carries meaning — left selects, middle executes, right looks. Everything on
+screen is text and all text is equally alive, whether the shell printed it or the
+user typed it.
-It runs in three places: the terminal (libvaxis), a native SDL3 window (the
-steamdeck), and the browser (a freestanding wasm core driven by vanilla
-JavaScript and rendered as HTML/CSS). The rewrite exists because
+It runs in four places: the terminal (libvaxis), a native SDL3 window (the
+steamdeck), the browser (a freestanding wasm core driven by vanilla
+JavaScript and rendered as HTML/CSS), and a native macOS app (an AppKit and
+CoreText shell over a static `libpardes.a`). The rewrite exists because
the prototype grew three parallel implementations of one program. The rewrite has
-exactly one program and three thin shells.
+exactly one program and four thin shells.
= The shape
@@ -33,9 +34,18 @@ its own renderer; the core owns everything the user would recognize as pardes.
columns: (auto, 1fr),
stroke: 0.4pt,
[*core*], [layout, panes, modes, selection, click semantics, themes, text of the
- UI. Pure state machine: `update(event)` mutates, `surface()` describes. No
- syscalls, no rendering, no event loop.],
- [*shell*], [one file per platform. Owns the event loop, translates native input
+ UI. Pure state machine: `update(event)` mutates, `render(arena)` returns the
+ surface. No rendering, no event loop, and no IO that has an effect for it —
+ but "no syscalls" was never true and is less true now: `look` reads a file
+ a Look opened, walks a directory for Find and reads every candidate for
+ Grep, `fonts` walks the font directories, and MuPDF opens a `.pdf`. Those
+ are the ones that are cheaper done in place than round-tripped through an
+ effect and back; everything with a lifetime — a pty, a window, the
+ clipboard — is still asked for.],
+ [*shell*], [one MODULE per platform (tty is one file; gui adds the CRT and
+ gamepad files, a C font loader and twelve GLSL shaders; web and macOS each
+ add a host language).
+ Owns the event loop, translates native input
into core events, renders the core's surface, and performs the core's
requested effects (spawn a shell, write a pty, open a link).],
)
@@ -45,34 +55,48 @@ build today is a separate 800-line read-only replay viewer. In the rewrite,
loading a dump of another instance is a first-class feature of the one
application, the way acme handles dumps: `pardes -l state.zon` on every
platform reconstructs the panes (terminals by replaying their raw VT streams
-into fresh emulators, files and images from their bytes). The web shell embeds
+into fresh emulators, files, images and PDFs from their bytes — a PDF rides the
+dump's `image` kind, carrying its path, its bytes and the page it was on). The web shell embeds
a dump and leaves `spawn` unanswered. Same core, no viewer fork.
The boundary is two data types, both plain values:
-*Event* (in): `key`, `text`, `mouse` (press/release/motion; button left, middle,
-right; cell position), `wheel`, `pinch`, `resize`, and `output` (bytes a pty
-produced, tagged with the pane id). Touch policy belongs to the shell: web maps
+*Event* (in): `key` (which carries typed `text`), `mouse` (press/release/motion/
+drag; button left, middle, right and the four wheel directions; cell position;
+and the `ctrl` flag, because Ctrl-left-click is goto-definition), `pinch`,
+`touch_scroll`, `resize` (cols, rows, and — in a MuPDF build — the cell's pixel
+size), `paste`, `pdf_scroll`,
+`output` (bytes a pty produced, tagged with the pane id), `eof`, `command` (a
+builtin line arriving from another process over the nested socket), `tick`, and
+the answers to an ask: `lsp_resp`, `pipe_resp`, `file_changed`. Touch
+policy belongs to the shell: web maps
a one-finger tap to right-button LOOK and a drag past its tap slop to natural
scrolling; native SDL keeps its two-finger gestures. A joystick cursor is a
motion plus buttons. The shells do this translation and nothing else with
-input: one event vocabulary, three dialects translated at the door.
+input: one event vocabulary, four dialects translated at the door.
-*Surface* (out): a grid of cells — grapheme, fg, bg, attrs — plus a cursor and
-per-pane rectangles. This is the *canonical interface*: it is literally what the
+*Surface* (out): `cols`, `rows`, a grid of cells — grapheme, fg, bg, attrs — a
+cursor, and the pixel attachments. This is the *canonical interface*: it is literally what the
tty shell hands to vaxis, cell by cell. SDL rasterizes the same grid through a
glyph atlas; the browser reads a packed copy and patches native DOM cells. If it
cannot be expressed in the surface, it does not
-exist in pardes. (Pixel images ride along as an attachment per pane: the SDL
-shells blit RGBA, the tty shell falls back to the petscii matcher, kitty
-graphics when available.)
+exist in pardes. (Pixel images ride along as a list of PLACEMENTS rather than
+one per pane — a PDF pane contributes every page its viewport intersects, and
+pages can be arbitrarily short. The SDL shells blit RGBA, the tty shell falls
+back to the petscii matcher, kitty
+graphics when available. The per-pane rectangles live on `Pardes`, not on the
+surface: a shell that wants them asks the core.)
*Effect* (out, queued): `spawn{pane, cwd}`, `write{pane, bytes}`,
-`resize_pty{pane, cols, rows}`, `open_link{url}`, `read_file{path}`,
-`new_file{pane, serial}`, `set_clipboard`, `read_clipboard`, `bell`, `quit`.
-The core never performs IO; it asks — and `read_clipboard` is the one ask with
-an answer coming back, an ordinary `paste` event, or nothing at all when the
-shell cannot read the clipboard (most terminals refuse the OSC 52 read). This
+`resize_pty{pane, cols, rows}`, `open_link`, `new_file{pane, serial}`,
+`save_file{pane}`, `write_dump`, `set_clipboard`, `read_clipboard`, `lsp`,
+`pipe`, `watch`, `quit`.
+The core never performs IO for any of these; it asks — and four of the asks have an
+answer coming back: `read_clipboard` returns an ordinary `paste` event (or
+nothing at all when the shell cannot read the clipboard — most terminals refuse
+the OSC 52 read), `lsp` returns `lsp_resp`, `pipe` returns `pipe_resp`, and
+`watch` returns `file_changed` whenever the shell notices the file moved under
+it. This
is what makes the browser build honest instead of a
fork: a wasm shell simply
answers `spawn` differently (or not at all) — the core does not know.
@@ -81,7 +105,7 @@ Platform divergence inside the core is a comptime tag, used the way the stdlib
uses `os.tag`:
```zig
-pub const Platform = enum { tty, gui, web };
+pub const Platform = enum { tty, gui, web, macos };
// in look.zig:
if (platform == .web and target.kind == .url)
return .{ .open_link = target.text };
@@ -94,32 +118,43 @@ divergence is visible in one screenful.
= Data structures
-The whole state is one struct, sized at init, no hidden allocation after:
+The whole state is one struct, fixed-size where it can be. Panes themselves are
+heap-allocated on demand and their contents (file bytes, the yank register, PDF
+rasters, tree-sitter state) grow with what you open; everything else is sized
+at init.
```zig
Pardes
- cols: []Col // col: weight + pane ids, top to bottom
- panes: []Pane // slot array; id = index
- active: Pane.Id
- drag: Drag // none | select | move | border | scrollbar
- theme: usize // index into themes (228 of them)
+ ncol + col_weight[6], col_terms[6][16], col_n[6] // columns as flat arrays
+ panes: [16]?*Pane // slot array; id = index
+ active: usize
+ drag: Drag // none | select | move | border_v | border_h | tag
+ theme_idx: usize // index into themes (228 of them)
colors_on: bool
- surface: Surface // rebuilt by surface(), arena-backed
- effects: Fifo(Effect)
+ rects: [16]Rect // where each pane landed, this frame
+ effects: [4096]Effect + head/len // a fixed ring
Pane
- tag: TagLine // live prefix (mode, cwd/path) + editable tail
- kind: union { term: Term, file: File, image: Image }
+ tag: TagLine // live prefix (cwd/path) + editable tail
+ kind: enum { terminal, file, image, pdf } // + independent optional payloads
+ vt, stream: ghostty-vt Terminal and its stream — on EVERY pane, not just
+ terminals, which is what lets the same keys and the
+ same parity suite drive a file and a shell
+ file: ?File / image: ?Image / pdf: ?PdfView
vweight: f32
mode: enum { normal, insert, tty } // helix-modal; tty = raw to the pty
+ // `v` adds a fourth thing to DISPLAY, "select",
+ // which is a bool on top of normal, not a mode
cursor: absolute body position // rides the scrollback, not the screen
- sel: [3]Sel + line/char modal sels // per-button block sels, msel, vsel
- edits: Splice // typed runs anchored to absolute rows
- undo: edit snapshots // unified undo across term edits and file content
+ sel: [3]Sel + msel/vsel + sels[63] // per-button block sels, the line and
+ // char modal ones, and up to 64 cursors
+ ovl: ?Ovl // ONE typed run, anchored to an absolute row
+ undo: two stacks, not one — ed_undo[64] of overlay snapshots for a terminal,
+ // and File.undo of content snapshots for a file
-Term = ghostty-vt Terminal + its stream (bytes in via Event.output)
File = path + bytes + line index + Syn (tree-sitter highlight bytes)
Image = decoded RGBA + petscii grid cache
+PdfView = MuPDF document + per-page rasters + cached outline
```
Layout is arithmetic, not objects: columns are weights over the width, panes are
@@ -133,21 +168,43 @@ one pane may not move panes it does not touch.
TigerStyle, plus house rules proven in the prototype: imperative and flat; one
big `update` dispatch, not handler objects; no one-line helpers — inline the
four-line scan; assert invariants at entry (`assert(vsum > 0)`); static
-allocation at init, arenas per frame. The metric is lines of code; the number
-only goes down. As built, against the prototype's 12,072 lines of Zig:
+allocation at init, arenas per frame.
+
+The metric was lines of code, and for the rewrite itself it held: 7,626 lines
+replaced the prototype's 12,072 with three backends instead of one and a half.
+It has not held since, and the table below is the honest version rather than
+the flattering one. Everything after the rewrite — PDF, a language backend, a
+fourth shell, 228 themes, multiple cursors — was added, not traded for
+something removed:
#table(
- columns: (auto, auto, 1fr),
+ columns: (auto, auto, auto, 1fr),
stroke: 0.4pt,
- [*module*], [*lines*], [],
- [core (pardes.zig + look.zig + syntax + image + dump)], [4,289], [all semantics, all platforms],
- [modal.zig + petscii.zig (pure, unit-testable)], [1,410], [carried over, unchanged],
- [shells (tty.zig; gui.zig = SDL native; web.zig + web/ = browser)], [2,768], [translate + render only],
- [main.zig + build.zig], [484], [three targets, codegen for queries],
- [*application total*], [*7,626*], [vs 12,072 — three full backends instead of one and a half],
- [snapshot harness (snapshot.zig + e2e_harness.zig)], [831], [the parity oracle, kept],
+ [*module*], [*at the rewrite*], [*now*], [],
+ [core and the pieces that grew out of it — 25 files: pardes, look, syntax, image, dump, builtins, config, output/file/term panes, normal\_input, nested, fonts, themes, …], [4,289], [22,220], [all semantics, all platforms],
+ [modal.zig + petscii.zig (pure, unit-testable)], [1,410], [2,569], [carried over],
+ [pdf.zig], [—], [1,581], [MuPDF, `-Dmupdf`],
+ [lsp/ (seam + in-process ZLS backend)], [—], [1,663], [`.zig` only],
+ [shells: tty 1,396; gui 4,968; web 601; macos.zig 1,579], [2,768], [8,544], [translate + render only],
+ [main.zig + build.zig], [484], [1,471], [four targets, four codegen passes],
+ [*application total* — every `.zig` under `src/`, plus `build.zig`], [*7,626*], [*38,048*], [vs the prototype's 12,072],
)
+Those six rows are a partition: no file is in two of them and none is left out,
+which the old table could not say (its rows summed to 1,325 more than its own
+total, and twenty-six files were in no row at all). What the total deliberately
+does NOT count, because none of it is Zig under `src/`: 2,250 lines of Swift for
+the macOS app and its icon generator, 1,541 lines of C bridging MuPDF, 549 of
+JavaScript, HTML and CSS for the web shell, 216 of C for the SDL font loader,
+`mupdf.zig` (355) at the root, and `tools/` + `build/` (896). The test tree is
+another 5,234 lines of Zig across eight harnesses, plus 1,028 of Swift and 610
+of JavaScript — against 831 at the rewrite.
+
+Two things that number is not. It is not one program's worth of growth — the
+macOS and web shells and the PDF and language work are four products sharing a
+core. And it is not licence: the rule that survives is the local one, that a
+change should leave the file it touches no longer than it found it.
+
= Testing: the old program is the oracle
Before the rewrite compiled, the prototype got a harness (`snapshot.zig`, the
@@ -156,8 +213,9 @@ script* (one line per input: keys, SGR mouse, resizes, sync points), and
captures the rendered grid — text, cursor, and per-cell style runs — through
its own ghostty terminal. Goldens are generated from the old binary
(`zig build snap -- --update`); the new binary must reproduce them byte for
-byte (`pardes-snap pardes/zig-out/bin/pardes`). Eighteen scripts cover the
-checklist in Appendix A; all pass. Determinism pins: fixed workdir paths (they
+byte (`pardes-snap pardes/zig-out/bin/pardes`). Eighteen scripts covered the
+checklist in Appendix A at the rewrite; eighty-seven cover it and everything
+since. All pass. Determinism pins: fixed workdir paths (they
appear in tags), a controlled `$HOME` with `PS1='$ '`, `LC_ALL=C`, and a
grid-stability sync primitive instead of timing guesses.
@@ -169,36 +227,66 @@ text fields are the contract); and typed insert runs don't survive a dump
replay (they are an overlay, not pty bytes — the prototype's replay viewer had
the same semantics).
-Shell correctness (that SDL draws the grid it was given, that vaxis diffs
-correctly) is out of snapshot scope and covered by each shell's single smoke
-test.
+Shell correctness is no longer out of scope, and that is the biggest change to
+this section since the rewrite. `web-snap` / `web-e2e` drive headless Chromium
+over CDP with real DOM pointer and touch events and diff
+`test/web-snapshots/`; `image-harness` and `pdf-harness` snapshot native PIXEL
+output through kitty graphics and SDL; `macos-e2e` is an offscreen AppKit
+snapshot suite over its own seven scripts. What remains untested by a snapshot
+is the last hop — that vaxis diffs correctly onto a real terminal — plus each
+shell's inline unit tests (the FreeType atlas raster, the trackpad and rotation
+maths, the gamepad replay).
= Build
-`zig build` produces three statically linked artifacts: `pardes` (vaxis),
-`pardes-gui` (SDL3), `pardes.wasm` + a vanilla DOM shell (wasm32-freestanding).
-Native dependencies are ghostty, vaxis, uucode (shared config), zstbi, SDL
-(castholm, lazy), FreeType, and zig-tree-sitter + grammars — all pinned through
-`zig fetch` and wired in `build.zig`. Codegen during build, consumed by comptime:
-`highlights.scm` → options module; tutor text → embedded pane content. Debug
-builds are incremental for the seconds-loop; release builds are the product.
+Every `zig build` builds ONE platform: `platform` is an option defaulting to
+`.tty`, so a bare `zig build` gives `pardes` (vaxis) and the other three are
+separate invocations — `-Dplatform=gui` for `pardes-gui` (SDL3),
+`-Dplatform=web -Dtarget=wasm32-freestanding -Ddump=<dump.zon>` for
+`pardes.wasm` plus a vanilla DOM shell (any other spelling of that one fails on
+purpose rather than building something silently wrong), and `-Dplatform=macos`
+plus the `macos-app` step for `pardes.app` wrapped around a static
+`libpardes.a` on a Darwin host. Native dependencies are ghostty, vaxis, uucode (shared config),
+zstbi, SDL (a pinned fork, lazy), FreeType, MuPDF (`-Dmupdf`, on by default
+everywhere but the web, lazy), ZLS, mvzr (the regex engine behind `s`/`S`), and
+zig-tree-sitter + 26 grammars — all pinned
+through `zig fetch` and wired in `build.zig`. Generated during the build:
+`highlights.scm` → an options module; the vendored helix/zed theme
+sources → the generated half of the theme ring; the tracked `.zig` sources →
+the web shell's read-only archive; eight GLSL shaders → SPIR-V. The tutor and
+the embedded font are plain `@embedFile`s, not codegen. Debug builds are
+incremental for the seconds-loop; release builds are the product.
= What is deliberately absent
No render abstraction over the shells (the surface *is* the abstraction). No
-config files. No plugin system. No async runtime in the core — the shells may
+plugin system. No async runtime in the core — the shells may
thread, the core is single-threaded by construction. No 9P yet — but the
-library boundary is exactly where acme put the file server, and a fourth shell
-could serve `Surface` and `Event` over 9P without touching the core.
+library boundary is exactly where acme put the file server, and a fifth shell
+could serve `Surface` and `Event` over 9P without touching the core. Half of
+that is already built and unnamed: `src/nested.zig` listens on a unix socket at
+`$XDG_RUNTIME_DIR/pardes-<pid>.sock` (`~/.local/state/pardes` without one),
+accepting exactly one verb — `Look <path>`, and nothing else, which is the
+security property — and that is how
+a `pardes <file>` run inside a pardes hands the file to the outer session
+instead of stacking a second full-screen UI inside one of its panes.
+
+"No config files" held until the startup file (`docs/config.md`) arrived, and
+that is the narrowest thing the phrase could still cover: a list of builtin
+COMMANDS run before the first frame — no schema, no new vocabulary, and no key
+remapping. The keymap is `src/config.zig`, compiled in, where a wrong binding
+is a compile error rather than a silent no-op.
= Appendix A: feature parity checklist
From the prototype survey; every line is covered by at least one of the
-eighteen event scripts in `test/snapshots/` (boot, tty, edit, scroll, modal,
-look-file, look-dir, exec, tag, theme, tutor, windowops, dump, load, ttyonly,
-syntax, fileedit, images).
+event scripts in `test/snapshots/` — the original eighteen (boot, tty, edit,
+scroll, modal, look-file, look-dir, exec, tag, theme, tutor, windowops, dump,
+load, ttyonly, syntax, fileedit, images), and sixty-nine more added since for
+everything below that the prototype never had.
-*Layout.* Columns by weight (≤6), panes by vweight (≤16); global topbar;
+*Layout.* Columns by weight (≤6), panes by vweight (16 panes in total, not
+per column); global topbar;
per-pane gutter (move box + scrollbar) and tag row; `splitBelow` shrinks only
the source (cursor row kept visible); dying pane's weight absorbed by one
sibling; emptied column hands width to a neighbor; border-drag resize on a
@@ -223,13 +311,29 @@ first doc opens left column and may evict a lone pristine shell; later docs
split below the existing doc — an output pane, `+Search`/`+Help`, is not a
doc for either half of that rule: it never claims a column, it splits below
whatever pane asked for it, and nothing splits from it), image ext → image
-pane. Middle+left chord: kept
+pane, `.pdf` → PDF pane. Ctrl+left is goto-definition, the one chord borrowed
+from every editor with a language backend. Two spellings the word expansion
+has beyond a path: `` @`ls -la` `` is taken WHOLE and runs as a command rather
+than opening as a file, and `@p7:10:5` addresses a live pane by number for the
+things — terminals, output buffers — that have no path to name.
+Middle+left chord: kept
left selection appended as trailing CLI argument. Wheel: scroll hovered pane,
batched.
-*Modes.* normal: helix motions (`h j k l w b e W B E 0 $ ^ gg ge gh gl G`,
-`Ctrl-d/u/f/b`, `zt zz zb`), insert entries (`i a I A o O`), `v`/`x`
-selections, `d c y p u U`, Enter=look Tab=execute at cursor. insert:
+*Modes.* normal: helix motions (`h j k l w b e W B E 0 $ ^`, `f F t T` and
+`Alt-.`, counts, `gg ge gh gs gl g| G`, `Ctrl-d/u/f/b`, `zt zz zb zj zk`),
+insert entries (`i a I A o O`), selections (`v` extend — displayed as a fourth
+mode name, "select" — `x`/`X`/`Alt-x`, `%`, `;`/`Alt-;`, `_`), MULTIPLE CURSORS
+up to 64 (`C`/`Alt-C` copy, `s`/`S` select and split by regex with a live
+preview, `Alt-s` split on newline, `,`/`Alt-,` keep and remove the primary,
+`)`/`(` rotate, `Alt--`/`Alt-_` merge), operators (`d c y p P R u U`, `Alt-d`
+delete-noyank, `J`, `>`/`<`, `~`/`` ` ``/`` Alt-` ``, `Ctrl-a`/`Ctrl-x`,
+`Ctrl-c` comment-toggle), textobjects and surrounds under `m`
+(`mm mi ma ms mr md`), the `]`/`[` pairs (`]p ]d ]D ]<space>`), `/` with `n`/`N`,
+`|` to filter the selection through a command, `:` for the tag as a command
+line, and Enter=look Tab=execute at cursor. Since the helix motion model
+landed, a traversal motion SELECTS the range it crossed — which is why there is
+no verb+noun grammar and why `i` after `w` types at the selection's start. insert:
click-and-type; terminals get splice runs (shift right, never overwrite;
absolute-row anchored), files get real edits. tty: raw pty forwarding
(Ctrl-key toggle, default Ctrl-b, `--tty-toggle`), mouse still usable,
@@ -252,21 +356,39 @@ Colors and Crt left it for their leader paths.
OSC 7 cwd, DSR/DA/kitty-query replies (write_pty + device_attributes — the
nushell/helix regression), greeting `ls`, auto-follow output unless
navigating. File: line-number gutter (fixed width), tree-sitter highlights
-(c/cpp/zig minimal tier; 25 grammars full tier; re-highlight on edit,
+(c/cpp/zig minimal tier; 26 grammars full tier; re-highlight on edit,
visible-range first), Save, open-at-line, undo/redo. Image: zstbi decode,
kitty graphics when available, petscii matcher fallback (C64/terminal
-palettes, ascii glyph set toggle). Tutor: embedded text as file pane.
+palettes, ascii glyph set toggle). PDF (`-Dmupdf`, native default on): MuPDF
+rendering as one continuous page strip, real text search, mouse text
+selection, the document outline into `+PdfSections`, fit-width/fit-height and
+a themed duotone tint — the last three named in the pane's own live tag.
+Tutor: embedded text as file pane.
+
+*Language.* ZLS compiled in and called IN-PROCESS — no subprocess, no
+JSON-RPC, no daemon and nothing cached between queries — behind a
+one-function seam (`lsp.query`) so that swapping a backend touches nothing
+else. `.zig` only, and on demand only: helix's five `g` gotos, ten builtins
+under `SPC l`, `]d`/`[d`, `=`, and insert-mode Tab after a `.`. Answers become
+rows in `+Search`, `+Hover` or `+Lsp` — output buffers of the same kind `/`,
+Find and Help fill — or, for a rename, byte ranges the core applies in one undo. The
+web shell has no threads and therefore no backend at all.
*Chrome.* One theme per `.zig` file, folded into the ring at comptime: ours
first — helix (default; near-black page, chrome one grey-ramp step off it,
-colors lifted from the helix editor's own theme), dark, acme-light — then
+colors lifted from the helix editor's own theme), dark, and the acme-light one
+named simply `acme` — then
everything `tools/gen_themes.zig` exports at build time out of the vendored
helix `.toml` and zed `.json` sources in `vendor/themes`, which is all 214 helix
-ships plus zed's 11, sorted by name. helix and dark leave
-a child's ANSI palette native; acme-light and the zed exports resolve it onto
+ships plus zed's 11, sorted by name. Names are unique by construction: helix's
+own acme is vendored as `acme_helix.toml` so it cannot collide with ours, and
+every zed theme takes a `_zed` suffix for the same reason. helix and dark leave
+a child's ANSI palette native; acme and the zed exports resolve it onto
the page so shell output stays readable on a light one. A theme owns the page,
-the tag bar, the gutter and the move box; the selection colors stay fixed
-because they encode which button you pressed. `NextColor` browses the ring one
+the tag bar, the gutter, the move box and the SELECTION: `sel_bg`/`sel_fg` are
+one pair per theme, and the three per-button tints and the dimmed extra cursors
+are mixed off it, so what stays fixed is the distinction between buttons and not
+the colours. `NextColor` browses the ring one
step at a time — at 228 it is no longer how you REACH one —
`Theme <name>` jumps to one and `ThemeSel` (`SPC t t`) lists them all into an
output buffer whose rows are those very commands — execute a row (Tab, middle