summaryrefslogtreecommitdiff
path: root/docs/config.md
blob: 3a6d00d26e4ba68311c239eda6ba558c48d424a5 (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
# Startup configuration

Native pardes builds use a per-user `pardes` configuration directory. Its
main command file is named `init`:

- Unix: `$XDG_CONFIG_HOME/pardes/init`, falling back to
  `~/.config/pardes/init`.
- macOS: `$XDG_CONFIG_HOME/pardes/init` when that variable is set, otherwise
  `~/Library/Application Support/pardes/init`.
- Windows: `%LOCALAPPDATA%\pardes\init`, with
  `%USERPROFILE%\AppData\Local\pardes\init` as the fallback.

On the two unixes `XDG_CONFIG_HOME` counts only when it is ABSOLUTE, as the
XDG base-directory specification requires; an empty or relative value falls
back to the home-directory form (`config.User.path`, and the test beside
it). Windows never consults it. An `init` that does not fit the `max_bytes`
read limit — 1 MiB, `config.User` in `src/config.zig` — or that cannot be read at all is
treated as no file: `load` takes the `readFileAlloc` error and keeps going. The
path still resolves, because "nothing is there yet" is the answer `Config`
exists to give. There is one case with no path at all: a native launch with no
`HOME` set (or, on Windows, neither `%LOCALAPPDATA%` nor `%USERPROFILE%`),
which `Config` reports as `no per-user config path`.

`Config` (`SPC f c`, or the word executed anywhere) opens one refreshable
`+Config` pane. It reports the startup path and every live config-like value:
theme, colors, focus tint, syntax weight, wrapping, tag position, debug mode, the requested shell and the
executable actually resolved at the last spawn, requested/effective GUI font
and size, tagline scale, window opacity, 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 five 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 only, and `tagline_font_size`
everything but the TTY and the board. 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. `Compiled default shell` is the command built into the binary; `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:

```text
Theme orchard
Font DejaVuSansMono-Regular
TaglineSize 82
Shell zsh
Wrap
```

A line matches a builtin whose name takes NO argument only as that whole word:
`Kill` runs, `Kill something` does not. A builtin that takes one
(`takes_arg` in `src/builtins.zig`, or a `settings` row whose action is
`shell`, `theme`, `font`, `tagline_size` or `window_opacity` in `config.Runtime.settings`) takes
everything after the name as the argument. On a native build that is `Theme`,
`ThemeFile`, `Font`, `TaglineSize`, `Shell`, `Save`, `Restore`, `Attach`,
`Mount`, `Unmount`, `Find`, `Grep`, `Rename`, `WsSymbols`, `Look`, `Exec`,
`Msg` and `EffectCode`.
The SDL GUI also has `WindowOpacity`, which takes one argument.
(`Peek`, `Poke`, `Hexdump` and `Gpio` take one too, but they exist only where
`builtins.Board.enabled` holds, and that build has no config file.)

`Theme <name>` wants one of the names in the compiled ring. Do not derive the
spelling — read it off `ThemeSel` (`SPC t t`), which lists every one as the
exact `Theme <name>` line that selects it. `slug` in `tools/gen_themes.zig`
lowercases, folds punctuation runs to a single `_` and then TRIMS leading and
trailing ones (`penumbra+.toml` is `penumbra`, not `penumbra_`), and every
variant read out of a zed `.json` gets `_zed` on the end so it cannot collide
with a helix theme of the same name — zed's "Ayu Mirage" is `ayu_mirage_zed`
and `ayu_mirage` is helix's `ayu_mirage.toml`. The suffix goes on all of them
rather than only the eight that clash today, so a name cannot move when either
project gains or loses a file. A name that is not in the ring is ignored.

The fifteen [native Pardes themes](themes.md) lead the ring: `orchard` (the
default), `dusk`, `ink`, `paper`, `daybreak`, `atelier`, `forge`, `lagoon`,
`solarium`, `spectrum`, `harvest`, `clay`, `forge_black`, `forge_soft`, and
`orchard_black`. `ThemeSel` lists them first under Pardes themes, followed by
a separate legacy/imported section. They add coordinated
focus, search, diagnostic and terminal colors; `ink` and `daybreak` are high
contrast dark and light options. The original `helix`, `dark` and `acme`
themes and all imported names remain available.

`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.
`ColumnTags` toggles the column command row in both GUI and TTY; it is on by
default. Add `ColumnTags` to startup configuration to reclaim that row on a
compact screen. Hiding it preserves your custom column commands.
`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.

`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. 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.

`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
```

- `context` is the number of source lines above and below each match (default
  `0`). Overlapping context is shown once.
- `tscontext` includes enclosing tree-sitter declaration headers in source
  order (default `off`).
- `tslocations` shows locations beside those declaration headers (default
  `on`). Declaration headers use the same muted color as locations, with or
  without their locations visible.

Result locations are padded so the source text aligns, preserving the source's
indentation. 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.
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 <percent>` controls the opacity of everything in the SDL window
except text and the cursor, which stay fully opaque. Use `WindowOpacity 85`
to see the desktop through the editor, or `WindowOpacity 100` to restore full
opacity (the default). The argument must be a whole number from
0 through 100; missing or invalid values
leave the setting unchanged. At zero only text and the cursor remain visible.
Add the command to `init` to persist it.

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 <path>` loads one complete theme from a `.zon` file. An absolute
path is used as written; a relative path is resolved from the `pardes`
configuration directory, not from the process working directory. A typical
layout is:

```text
~/.config/pardes/
├── init
└── themes/
    └── mine.zon
```

and the corresponding init line is:

```text
ThemeFile themes/mine.zon
```

After a successful load, hosts with document live reload watch the path with
the same parent-directory mechanism, so in-place writes and editor-style
rename-over saves reload the theme live. A malformed or incomplete save does
not replace the last valid theme; fixing and saving the file applies the next
valid snapshot. Selecting a compiled theme with `Theme <name>` or `NextColor`
stops the custom-file watch.

Execute `DumpThemes` to write every theme compiled into the executable to:

```text
<config directory>/themes/builtin/<name>.zon
```

The command replaces those generated reference files but leaves unrelated
files alone. Copy one into `themes/`, rename it, change its `.name`, and use it
as the starting point for a custom theme. The dumped file is also the complete
format, and it is `pardes.Theme` serialised by `std.zon.stringify`: the theme
name, thirteen required RGB roles (`sel_bg`, `sel_fg`, `tag_bg`, `tag_fg`,
`box`, `box_dim`, `kw`, `str`, `num`, `comment`, `lineno`, `scroll_track`,
`scroll_thumb`), nullable `bg`/`fg`, and either a 16-color RGB `palette` or
`null`. Optional nullable RGB roles extend this format:
`tag_active_bg`, `tag_active_fg`, `tag_name_fg`, `tag_active_name_fg`, `border`, `lineno_active`, `search_bg`, `search_fg`,
`diagnostic_error`, `diagnostic_warning`, `diagnostic_info`, and
`diagnostic_hint`. Missing new roles use backward-compatible defaults, so
previously exported files remain valid. [Theme customization](themes.md)
describes each role and its fallback. RGB values are three-byte arrays, and
hex literals are accepted. The original fields remain required; there is no
inheritance or partial override layer.

`tag_name_fg` gives the filename at the end of a pane's path a separate
foreground. `tag_active_name_fg` can adjust that tint for active tags; it falls
back to `tag_name_fg`. When both are omitted or `null`, the filename uses
the corresponding tag foreground. All canonical themes use a different hue
at similar perceived brightness to the surrounding text in both states.
Directory text and tag commands retain
their regular colors; selecting filename text uses the selection colors.
Terminal tags use this color for `Tty`, which comes immediately after the path.

`Filter` in a terminal's tag projects that pane's ANSI colors through the
active theme, and does it in two stages. The default foreground and background
roles are mapped FIRST, because ghostty-vt generates the whole 256-color
projection from that pair; every other color follows, by reducing it to its
nearest canonical xterm key and reading the key back out of the projection.

That reduction compares RGB triples, so it knows about hue and nothing about
the page — and the projection's cube corners ARE the two anchors, which is how
a foreground used to end up painted the exact color of the paper behind it
(`\x1b[38;2;255;255;255m` on acme's `#ffffea`, and the ANSI black a shell
writes with `\x1b[30m` on either dark theme). So a foreground additionally has
to keep `tty_filter_min_contrast` — a WCAG ratio, `1.5` by default — against
the mapped background. One that cannot is not mapped: it takes whichever of
the theme's own two anchors is still visible on that background. Backgrounds
are exempt, since a background is the page the floor is measured against. Set
the constant to `1.0` to accept every projected color, collapses included.

`Shell <name>` sets the binary that the NEXT terminal pane execs; panes
already open keep the shell they are running. A bare name is resolved against
the handful of directories a shell actually lives in, not `$PATH`.

On Linux, `Tty9p` (`SPC n 9`) starts that shell with a private kernel 9P mount,
asking sudo inside the new terminal. `$PARDES_MOUNT` names the mountpoint.
The installed `pardes-v9fs` helper lives beside the editor; development builds
can set `PARDES_V9FS_HELPER` to its absolute path. See [v9fs.md](v9fs.md).

Ctrl-B switches between raw TTY and editor mode. Plain Escape at a detected
shell prompt hops back to the previous pane. Other keys, including Ctrl-O, Ctrl-W,
paste shortcuts, and modified Escape belong
to the child. Use `Togglettymode` in the pane tag to return to editor
mode in place. Desktop paste events still feed the terminal.

`Font` and `FontSel` exist ONLY in the SDL GUI and native macOS builds — a
terminal's font belongs to its emulator and a browser's to the page — so a
`Font` line is one of the silently-ignored ones everywhere else. Both builds
resolve the name by walking the font directories on every lookup, so a face
installed a moment ago is findable.

Use `Font <name>:<size>` to change face and size together, for example
`Font MartianMono-NrRg:18` in the startup file or an editable tag. Fractional
sizes such as `:18.5` are supported; the accepted range is 8–72 (pixels in SDL,
points on macOS). `Font <name>` without a suffix preserves the current size.
Invalid sizes or unknown faces leave the current font unchanged.

The SDL GUI also has a tiny optional workspace-tag companion: `Pet cat`,
`Pet frog`, or `Pet off` (the default). Its original pixel sprite walks and
idles in the trailing blank area of the global tag only. It hides while that
tag is being edited, when the tag is full, or when the font leaves too little
room; it never replaces text, receives clicks, or appears in pane/column tags.
Animation runs at ten steps per second, uses theme ink, and requires no image
assets or shaders. Put `Pet cat` in the startup configuration to keep it;
`Pet off` disables the companion and its animation. Other hosts ignore the
startup command. `Config` reports the current choice in SDL.

`Font` is asynchronous at the renderer boundary. `Config` therefore keeps
requested name/path/size, pending state, and the effective face/point-or-pixel size
as separate facts; a failed request never gets reported as the face on screen.
Taglines use a distinct face size in both native GUI renderers. Execute
`TaglineSize <percent>` to change it live, for example `TaglineSize 70`; the
accepted range is 1 through 100 and the default comes from
`gui_tagline_font_percent` in `src/config.zig` (82). The native renderer
remeasures both the glyph and its visible tag band while retaining body-grid
pane geometry. The SDL GUI uses the smaller face's measured monospace advance
as a real per-pane tagline grid, including pointer hit testing; it does not pad
each smaller glyph back out to a body-width cell. 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 join the reduced-height global and pane tagline bands with
`gui_topbar_pane_border_px` physical pixels. Set it to zero for a direct join.
`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. With `Tagbottom` enabled, a tagline
on the final grid row is bottom-aligned so the same unused half-band does not
show beneath it. The rule itself lives in the core
(`pardes.taglineBandOffset`), and the macOS shell reaches it over the C ABI
rather than keeping its own copy — it had one, it only ever centred, and the
seam that leaves between the two bands is what this setting exists to close.

What happens to a codepoint the chosen face has no glyph for differs by shell.
The SDL GUI falls back through a chain it builds itself: embedded Adwaita
Mono, then installed `NotoSansMono-Regular`, `DejaVuSansMono`,
`SymbolsNerdFont-Regular`, `NotoSansSymbols2-Regular`,
`NotoSansSymbols-Regular`, and `DejaVuSans`, in that order, missing entries
skipped; the rasterized glyphs are retained in its GPU atlas. The macOS shell
has none of that and needs none. It embeds no font. Its default face is
`NSFont.monospacedSystemFont`, resolved through the descriptor rather than by
name, and `Menlo` only if that face turns out not to be fixed-pitch — a
proportional face in a fixed grid is a broken screen, not a cosmetic problem
(`PardesView.defaultFace`). A `Font <name>` arrives as a PATH the core already
resolved by walking the font directories, and a file CoreText cannot measure
leaves the face already on screen rather than substituting one, because the
alternative is a terminal with no way back out (`PardesView.fromFile`).
Per-codepoint fallback is CoreText's own cascade at draw time. What the shell
caches is `CGGlyph` ids, not pixels.

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:

```text
PanelSlide
PanelZoom
PanelDissolve
PanelAscii
PanelVertical
PanelEdges
PanelFall
PanelWave
PanelCurtain
PanelScramble
PanelType
```

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 curtain of glyphs marches column by column, left to right.
- `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 effects are independent switches and can be combined:

```text
Crt
Ripple
Glitch
```

They share one full-scene shader pass in SDL and macOS. With all three off the
pass is bypassed. CRT works in linear light with restrained scanlines, mask,
bloom, curvature, and noise rather than remapping the theme to a strong fixed
palette; Ripple and Glitch primarily perturb sample coordinates.

`EffectCode PanelAscii` or `EffectCode Crt` lists the current backend's
build-embedded source paths under `/virtual`. Look opens each full file;
no checkout is needed. TTY exposes grid transitions, native GUI builds also
expose scene shaders, and web has neither. Shared implementations share paths.
SDL reports whether GLSL was compiled during this build or came from the
`-Dprebuilt-shaders` snapshot paired with the committed SPIR-V.
`zig build shaders` refreshes both files of every pair together,
so editing live GLSL without that explicit refresh changes neither half of a
prebuilt executable.

## Delayed Look preview

The preview is enabled by default. Moving the pointer onto selectable text and
leaving it still for `look_preview_delay_frames` — 2 animation ticks, and
`animation.frame_ms` is 16, so about 32 ms — paints a subtle theme-derived
preview of the exact operand a right-click Look would receive.
Repeated motion reports in the same semantic grid cell do not restart the
delay. The preview uses the same side-effect-free word/path expansion as Look;
it does not focus a pane, move a cursor, install a selection, activate a PDF
page, or execute anything. Motion to another operand, pointer leave, input,
pane teardown, and relevant content changes cancel it.

Set this compile-time option in `src/config.zig` to disable the feature:

```zig
pub const look_preview_delay_frames: ?u16 = null;
```

## Build-time configuration

Runtime settings live in `src/config.zig`. Build options are listed below;
`zig build --help` lists the options available for the selected platform,
including the standard `-Dtarget` and `-Doptimize` options.

| option | values | default |
|---|---|---|
| `-Dplatform` | `tty`, `gui`, `web`, `macos`, `esp32p4` | absent builds the tty cli and the SDL gui together |
| `-Dstatic` | bool | `false` |
| `-Dquic` | bool; 9P over QUIC using system OpenSSL 3.6+ | `false` |
| `-Dmupdf` | bool | on for a native target, off for web and esp32p4 |
| `-Djpx` | bool | `true` — JPEG 2000, and with it scanned PDFs |
| `-Dtree-sitter` | `disabled`, `zig`, `minimal`, `full` | `full` natively, `zig` for web, `disabled` for esp32p4 |
| `-Dtheme-animation` | bool | on everywhere except `-Dplatform=esp32p4` |
| `-Dprebuilt-shaders` | bool | on for a bare `zig build`, off when `-Dplatform` names a shell |
| `-Dtracy` | path to a Tracy source checkout | off |
| `-Dmacos-identity` | codesigning identity for `pardes.app` | `-` (ad-hoc) |
| `-Ddump` | a `dump.zon` to embed in the web shell | none |
| `-Dtest-filter` | substring; run only tests whose name contains it | none |
| `-Dtest-rebuild` | bool; force fresh Zig test compilation, retaining cached C dependencies | `false` |
| `-Dhelix-harness` | native reference executable for live differential tests | `HX_HARNESS`, otherwise `hx-harness` on PATH |
| `-Desp32p4-cols` | u16, the board's grid width in cells | `56` |
| `-Desp32p4-rows` | u16, the board's grid height in cells | `14` |

The local board build emits an object. Firmware clock, serial port and profiling
options belong to the sibling `05-zig-p4` toolchain's build.

Two build inputs reach the running binary as ordinary values rather than as
behaviour. `build.zig` reads `.version` from `build.zig.zon` through an untyped
`@import("build.zig.zon")` — one place to bump — and `gitCommit(b)` reads the
revision at configure time. Both land in `pardes_config` and are re-exported as
`pardes.version` and `pardes.commit`, and `pardes --version` prints
`pardes <version> (<commit>)`, or `pardes <version>` alone when there is no
commit: a tarball, a container with no `git`, or a checkout outside version
control all yield null, and the flag has to work anyway. Neither is a question
asked at runtime — a binary that shelled out to `git` would describe whatever
tree it was standing in rather than the one it came from.

`Restore a.dump` first looks for the relative path in the default dump directory
(`$XDG_DATA_HOME/pardes`, or `~/.local/share/pardes`). If it is absent, Restore
uses the argument as a path as before. Absolute paths and argument-free Restore
retain their existing behavior. `Dump` still honors `$PARDES_DUMP` and otherwise
writes a timestamped file in the default directory.