summaryrefslogtreecommitdiff
path: root/docs/design.typ
blob: 8b93614e473e06dadbc57f3a579d9a2010cf0f58 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
#set page(margin: 2.2cm)
#set text(font: "New Computer Modern", size: 10.5pt)
#set par(justify: true)
#show heading: set block(above: 1.4em, below: 0.8em)

#align(center)[
  #text(17pt)[*Pardes: A Text Environment*]

  #text(11pt)[design for the rewrite — 2026-07-05]
]

= 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, 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 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 four thin shells.

= The shape

Pardes is a *library*, in the way ghostty-vt is a library: you feed it bytes and
events, and you read state out of it. Every platform owns its own event loop and
its own renderer; the core owns everything the user would recognize as pardes.

#table(
  columns: (auto, 1fr),
  stroke: 0.4pt,
  [*core*], [layout, panes, modes, selection, click semantics, themes, text of the
    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 eight 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).],
)

Native shells share `shell_bin`'s OSC 133 startup snippets, but not their
files. Each host owns a private `mkstemp` pair for its lifetime, writes and
closes both before the first fork, passes those unpredictable paths directly
in child argv, and unlinks them at teardown. Concurrent tty, SDL and macOS
launches therefore cannot truncate, source, or replace one another's startup
files.

This also collapses the prototype's *two* applications into one: the browser
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, images and PDFs from their bytes. Editable tag
tails are stored separately from their dynamic live prefixes, and image records
retain PETSCII, palette, and ASCII renderer choices. 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. The reader validates weights, scroll
ranges, pane references, and bounded tag tails before constructing anything;
invalid base64 fails instead of silently becoming empty content. A PDF record
whose path cannot be opened—or a build without MuPDF—falls back to an ordinary
file pane with those exact embedded bytes and its original editable tail. Same
core, no viewer fork.

The boundary is two data types, both plain values:

*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, four dialects translated at the door.

*Surface* (out): `cols`, `rows`, a grid of cells — grapheme, fg, bg, attrs — a
cursor, pixel attachments, and a bounded list of plain panel-transition tracks.
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 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. Transition tracks identify their pane by slot and
serial and carry only phase, effect, frame, and from/to cell boxes. Canonical
layout is committed immediately; tracks are finite presentation data.)

*Effect* (out, queued): `spawn{pane, cwd}`, `write{pane, bytes}`,
`resize_pty{pane, cols, rows}`, `open_link`,
`save_file{pane}`, `save_text{pane, serial, path}`, `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 a text file or PDF
moved under it. The tty and SDL hosts currently implement that path with Linux
inotify; the native macOS host watches both each file and its parent directory
with debounced DispatchSources, then restats the exact path under a pane-generation
guard. The file catches in-place writes while the parent follows rename-over
saves. Text snapshots are filtered by their content hash; PDFs use bounded
inode/size/time identity and commit it only when equal stats bracket a
successful transactional MuPDF reopen. A mismatched transaction gets one
bounded self-retry. This catches rename-over saves without reading a large PDF
merely to notice it changed or spinning on a malformed one. Web has no
filesystem watcher. A PDF response retains its
reading position and pane settings. 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.

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, macos };
// in look.zig:
if (platform == .web and target.kind == .url)
    return .{ .open_link = target.text };
```

There is one such file by design: `look.zig` holds path/`:line` resolution,
URL detection, and the per-platform outcomes. The core's one `pointerOperand`
primitive owns click-word expansion and is shared verbatim by right-click and
the delayed hover preview; that policy stays beside input because it also
observes live pane selections and wrapped grid coordinates.

Pane implementations are similarly flat and direct: `file_pane.zig`,
`term_pane.zig`, `image_pane.zig`, `output_pane.zig`, and `pdf_pane.zig` own
their kind-specific storage and operations. `pardes.zig` keeps the layout,
input dispatch, cross-pane invariants, and the small calls joining those
modules. There is no pane vtable or callback layer; the kind is already plain
data, so a direct switch/call is the shortest boundary.
The large end-to-end PDF cases live in `pdf_pane_integration_test.zig`, keeping
pane-specific fixtures and raster assertions out of that core file as well.

= Data structures

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.

User-settable runtime choices are one plain `runtime_config.State`: booleans,
theme index, owned bounded shell/font strings, requested/effective font facts,
one panel-transition enum, and scene-effect booleans. A compile-time `settings`
array generates each setting builtin and the rows of the single `Config`
query. It has no callbacks and no parallel query registry to drift from it.

Horizontal layout weights are fixed-point integers and geometry rounds
cumulative boundaries. Splitting a column replaces only its weight `W` by
`A+B=W` at the same position. Therefore every boundary outside the source
column is bit-identical before and after the split, including at awkward
non-dyadic screen widths; only the source and new column can receive movement
tracks. Vertical splits apply the corresponding rule to the source pane's
weight.

= Presentation animation

`panel_animation.zig` is backend-neutral data and math: five transitions,
their easing, exact endpoint progress, stable per-cell noise, and a POD track.
The core detects opening/moving rectangles when it commits layout and publishes
only active tracks. It retains the last successfully presented canonical grid
and a typed old/new cell diff; unused grapheme bytes do not manufacture a
change. A separate dense closing-track list is presentation-only state for a
pane whose functional lifetime has already ended. Pointer input inverts the
presented slide/zoom/vertical rectangle back to the canonical grid, lets
unchanged dissolve/ASCII cells through immediately, and rejects closing
pixels, so pixels and gestures cannot disagree during a transition.

Pardes core first composes each ASCII-qualified diff by incrementing or
decrementing its printable byte. Short walks move once per frame; longer walks
use integer ease-out skips and finish within twelve movements. Style-only and
non-ASCII changes pass through, and every backend receives the same
presentation cells.
TTY copies that `Surface` grid into a compositor scratch grid, clears
slide/zoom destinations, then paints moving, opening, and closing panels in
order.
Cleared geometry uses the theme page color when it is explicit and the host
terminal default only for transparent themes, so a light theme cannot flash a
dark gap. Slide/zoom change the copied rectangle. Dissolve changes only diff
cells from their old value to their new value; vertical raises only an opening
or frozen closing pane
inside its own clip. SDL supplies old/new glyph data, diff flags,
final/presented boxes, and effect parameters to the glyph and native-image
shaders. Pixel attachments bypass ASCII because they have no character byte.
macOS passes the same records across its plain C ABI and composites
old/new panel images in Metal/Core Image. Scene `Crt`, `Ripple`, and
`Glitch` bits share one full-window pass in each native GUI. DOM web is a
separate platform, not a shader GUI: retaining selectable HTML/CSS is more
important than duplicating the renderer in canvas, so it exposes neither
effect family.

`EffectCode <effect>` writes the actual backend math, host submission, and
shader/grid source segments embedded by the build into an ordinary output
pane. This makes the implementation inspectable
after installation and makes sharing explicit: several builtins can quite
honestly print the same shader with different uniform bits.

```zig
Pardes
  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
  settings:  runtime_config.State  // theme, shell/font, display/effect choices
  rects:     [16]Rect       // where each pane landed, this frame
  panel_tracks: [16]?Track          // live panes, serial-guarded
  closing_panel_tracks: [16]Track   // dense visual tombstones, no pane owner
  presented_cells + panel_cell_diffs  // acknowledged baseline + semantic diff
  effects:   [4096]Effect + head/len   // a fixed ring

Pane
  tag:   TagLine            // live prefix (cwd/path) + editable tail
  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_pane.State / image: ?image_pane.State / pdf: ?pdf_pane.State
                            // payload presence is the kind; none = terminal
  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 + msel/vsel + sels[63] // per-button block sels, the line and
                            // char modal ones, and up to 64 cursors
  ovl:   ?term_pane.EditBuffer // ONE typed run, anchored to an absolute row
  undo:  two stacks, not one — term_pane.Snapshot history for an edit buffer,
                            // and file_pane.State history for file content

file_pane.State  = path + bytes + line index + Syn (tree-sitter highlight bytes)
image_pane.State = decoded RGBA + petscii grid cache
pdf_pane.State   = MuPDF document + continuous layout + search/selection/outline
        + bounded per-page raster relay + frame placement decisions
```

Layout is arithmetic, not objects: columns are weights over the width, panes are
weights over the column. `splitBelow` shrinks only the source pane;
`absorbVWeight` gives a dying pane's weight to one sibling; an emptied column
hands its width to a neighbor. Minimal motion is the invariant: an operation on
one pane may not move panes it does not touch.

= Style

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 rewrite used source size as a pressure toward direct code, and that remains
useful when a refactor deletes duplicate policy or state. A checked-in line-count
inventory does not: it goes stale whenever a pane kind, backend, or generated
asset moves. Measure the current tree when making that comparison; keep this
document about ownership and invariants that should survive the next edit.

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
`zig build snap` step): it forks either binary in a pty, feeds it an *event
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 covered the
checklist in Appendix A at the rewrite; ninety-five 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.

Two of those pins were doing damage rather than work. A capture used to restate
the whole screen, so 82% of golden lines were a copy of the line above, every
golden carried the topbar and a tag row, and adding one builtin word to a
tagline rewrote 78 of them — 3,758 lines of diff for a change no test was
about. A capture is now a *delta* against the previous capture of the same kind
in the same script: the first is the whole screen, the rest are only the rows
that changed. The corpus went from 16,700 lines to 5,722 and the same one-word
edit now moves 337. A capture whose only change is the cursor is the empty
delta its script always meant.

And a click used to name a screen column, which is a coordinate into that same
chrome. When `Newtty`, `Joincol` and `Changelog` were added, seven scripts began
clicking the word next door — `tutor.snap` clicked `Grep`, `exec.snap` clicked
`Newtty` where it meant `Del`, `tagbottomimage.snap` clicked blank space 176
columns from the `Del` whose effect it asserted — and `--update` blessed the
result, leaving 256 golden lines green while asserting the opposite of their own
first line. A click may now name the word (`press middle @Del 2`, `@Del#2` for
the second pane on a row, `@Save-2` for a column beside one), so the word is
either there to be clicked or the script fails.

Regeneration was the other half of that failure: `--update` captured once,
serially, and wrote whatever it saw, which is how six wrong clicks became
goldens. It now captures in parallel at the widest probe settings the harness
has and then runs the ordinary verify pass over what it wrote, so a capture that
does not reproduce is reported instead of committed (77s to 41s, and the retry
machinery that serial update never had).

Three deliberate deviations surfaced by the oracle, kept after review: the
greeting `ls` waits for the exact OSC 133 B input mark after the real resize
(the prototype raced bash's startup and won only by allocator luck); shells
without prompt integration omit that cosmetic greeting rather than guessing.
Commands which create a fresh shell are owned by its terminal pane until the
host reports the actual prompt capability, then wait for the same mark when it
exists. Dump files compare with base64 pty
history elided (it encodes prompt-redraw micro-timing, not state — the cleaned
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 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

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 + 29 grammars for 28 languages (markdown takes two: block and
inline) — 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; working-tree `.zig` sources →
the web shell's read-only archive; eight GLSL shaders → SPIR-V. That last one is
the only build input wanting a tool a stock machine lacks (`glslc`), so it is
also the only one whose output is committed: `zig build shaders` refreshes each
paired `shaders/prebuilt/*.spv` binary and `.glsl` source snapshot together.
`-Dprebuilt-shaders` embeds that exact pair rather than shelling out, so
`EffectCode` cannot describe different shader text from the binary on screen;
this is also what lets a gui build need nothing but a C toolchain. 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
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 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
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 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); `Newcol` takes width only from the source
column and cannot resize any unrelated column; dying pane's weight absorbed by one
sibling; emptied column hands width to a neighbor; border-drag resize on a
pane's own trailing edge (v and h), hover shows `╎`/`╌` glyph-only hints;
move-drag via the gutter box with preview; Alt-n new shell below, Alt-c move
pane to new column; Ctrl-w h/j/k/l directional focus; body-normal Esc hops to
the pane you were in before this one, alternating between two (the same
builtin as SPC j j);
`--tty` single-pane mode.

*Mouse.* Left: select (block, stays highlighted after drag, pins cursor,
enters normal), click clears, tag-row click enters tag edit, scrollbar
click scrolls up-to-row (right button: down), border/move drags. Middle:
execute — no-drag expands to file-ish word (alnum `.-+/:@_~`); builtin or send
to shell; does not focus. Right: look — peel `:NNN`, resolve against the
clicked pane's dir (`/proc/pid/cwd`, or a file's dirname) and, if that fails,
against every other live pane's, most-recently-focused first (the jump stack
backwards, duplicate dirs skipped; absolute words try one dir and stop) so a
relative name is openable from any window that can see it; dir → shell + `ls`
in source column (dedup by cwd), file → file pane at line (dedup by path,
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, `.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. A stationary pointer gets a
delayed, theme-derived highlight of the exact side-effect-free selection that
Look would expand; it neither focuses nor installs that selection, and pointer
leave/input/content invalidation cancels it. Wheel: scroll hovered pane, batched.

*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,
promptClickMove on entry, prompts visible (hidden in the other modes via
OSC 133). `y` fills the yank register and `p` pastes it; neither touches the
system clipboard, which is helix's five words — `SPC y/Y/p/P/R`, out via
`set_clipboard` and back via `read_clipboard` — and nothing else, so a delete
cannot clobber what the desktop was holding. A paste from an outer terminal
arrives bracketed, as one `paste` event.

*Tag.* Live prefix (mode indicator, cwd or path) + editable tail with the
full modal editor. Save leads the tail of every pane holding text of its own:
`Save New Newtty Del` for a file or an output buffer,
`Save New Newtty Del Filter` for a terminal, and the plain `New Newtty Del`
for an image or a PDF, with `*` after an unsaved file's name; an image tag
reports
`img petscii:<on|off> palette:<commodore|terminal> ascii:<on|off> <path>`
before the ordinary tail (its renderer toggles are builtins under `SPC t
p/l/a`). Topbar:
`New Newcol Find Grep Help Tutor Dump NextColor Debug Kill` — execute-only
(left click inert);
Colors and Crt left it for their leader paths.

*Panes.* Terminal: ghostty-vt, 16 MiB scrollback, OSC 133 prompt semantics,
OSC 7 cwd, DSR/DA/kitty-query replies (write_pty + device_attributes — the
nushell/helix regression), a default-on pane-local Filter which uses ghostty-vt's
theme-derived 256-colour generation and keys rendered cell foreground/background
truecolour and OSC overrides through that palette without mutating emulator state,
greeting `ls`, auto-follow output unless
navigating. File: line-number gutter (fixed width), tree-sitter highlights
(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). 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, 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. 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, 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
click) and the theme goes on; n/N select such a row WHOLE, since a command
line holds no place to pick out of it — the third grain of that motion, the
other two being one stop per ROW in a results list (the location at its head,
never the matched text after it) and every look-able word in free text. Native
shells also accept `ThemeFile <path>`: one complete ZON `Theme`, loaded at
runtime and, where document watches are available, watched with the same
parent-directory/rename-over semantics. `DumpThemes` materializes the compiled
ring under `<config>/themes/builtin/`, providing the schema and a copyable
starting point without adding inheritance or a second theme vocabulary.
Colors toggles
all recolor passes; Debug stats overlay; Dump writes state ZON.

*Web (replay viewer parity).* Embedded dump; focus, border/move drags, wheel,
scrollbar, Colors/NextColor, and link-LOOK opening a new
tab (new — the prototype has no web link handling). One-finger touch is
deliberately complete by itself: tap = LOOK; drag past a small slop = natural
scroll, with no LOOK on release. A build-generated read-only archive lets LOOK
open the current contents of tracked or new/nonignored Pardes `.zig` files despite the web
shell having no host filesystem. The published launcher dump is captured from
a running Pardes TTY after
`git ls-files --cached --others --exclude-standard -- '*.zig' | sort`, so its
complete terminal listing is the same tracked-plus-new/nonignored set the user
can open. The freestanding module carries no host
libc, SDL, or WebGL; Tree-sitter's C runtime and the selected parsers link into
the module through a tiny local ABI shim (Zig is the compact web default). A body gesture retains
tap-LOOK/drag-scroll, but finger-down on
a tagline or a one-cell-tolerant pane separator latches to a left-mouse gesture
for its lifetime, keeping layout drags out of the scroll heuristic. Touch
input translation and scroll/tap state live entirely in the JavaScript shell.
The native SDL shell uses a FreeType light-hinted grayscale atlas over the SDL GPU API (SPIR-V).
The browser exposes each cell as selectable, inspectable text and applies the
surface styles with CSS. The browser `.snap` harness drives real Chromium touch
input and reads both text and per-cell styles from the DOM renderer's packed
surface. Joystick cursor for the steamdeck is *new* scope — the prototype ships
no gamepad input.