omp-tui 0.1.0

Retained-mode terminal UI components, rendering, input, and terminal integration for omp
Documentation
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
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
# `omp-tui`

`omp-tui` builds retained terminal interfaces from declarative component trees. You describe a screen once, route terminal events into `Ui`, and let `Ui` update and repaint the smallest safe region. `Renderer` then writes only the changed cells while preserving native terminal scrollback.

Most applications use `dom!` for their initial tree, stable `id` attributes for later updates, and `Ui::values()` to read interactive state.

## Structure

- `components` and the `dom!` macro define retained layout, text, navigation, data, and input trees; runtime `markup` and typed builders provide alternate construction paths.
- `Ui`, `App`, and the event/input modules retain widget state and route keyboard, mouse, paste, resize, and application events.
- `Frame` and `Renderer` turn component output into differential terminal updates, while `terminal`, `graphics`, `notify`, and protocol-specific modules manage lifecycle and terminal capabilities.
- `editcore`, `rich`, `markdown`, `latex`, `syntax`, `scene`, and `shader` provide editing and richer content pipelines. `build.rs` validates `icons.tsv` and generates the icon lookup catalog.

## Philosophy

Build the component tree once, preserve interaction state, and repaint only the smallest safe terminal region. Keep terminal-specific negotiation and escape handling inside the crate so applications can focus on declarative structure and explicit event-driven updates.

## Mental model

An `omp-tui` application has four layers:

1. **Components** describe layout, content, and interaction. Build them with `dom!`, runtime markup, or Rust builders.
2. **`Ui`** owns the retained component tree, focus, widget state, dom, hit regions, and the current `Frame`.
3. **Events** mutate that retained tree through `handle_key`, `handle_paste`, `handle_mouse`, `set_text`, and related methods.
4. **`Renderer`** differentially paints the resulting frame to a terminal writer.

Build the `Ui` once and keep it. Rebuilding after every key press discards editor contents, selection, focus, tab state, and scroll offsets.

## Add the crate

Inside this repository, declare internal crates at the workspace root and inherit them from the application crate:

```toml
# Cargo.toml
[workspace.dependencies]
omp-tui = { path = "crates/tui" }
```

```toml
# crates/my-app/Cargo.toml
[dependencies]
omp-tui = { workspace = true }
```

The application uses `omp_tui`; Cargo converts the package hyphen to an underscore in Rust paths.

`omp-tui` owns terminal lifecycle and input decoding; applications do not need a separate terminal-backend dependency.

## Build a first screen

`dom!` is the usual starting point. It expands to ordinary component builders and can interpolate Rust values without parsing a string at runtime.

```rust
use omp_tui::{Ui, UiContext, dom};

#[derive(Clone, Copy)]
enum RunState {
    Ready,
    Running,
    Failed,
}

fn build_ui(width: u16, state: RunState, show_help: bool) -> Ui {
    let jobs = ["index workspace", "run checks", "publish report"];

    Ui::from_root(
        dom! {
            <col gap=1 pad="1 2">
                <box border=round title="Build">
                    <col gap=1>
                        <text bold fg=accent>{"Pipeline"}</text>
                        for job in jobs {
                            <row gap=1>
                                <i:check/>
                                <text>{job}</text>
                            </row>
                        }
                        match state {
                            RunState::Ready => <text fg=muted>{"Ready"}</text>,
                            RunState::Running => <text fg=info>{"Running"}</text>,
                            RunState::Failed => <text fg=err>{"Failed"}</text>,
                        }
                    </col>
                </box>
                if show_help {
                    <text dim>{"Tab moves focus; Enter activates; Esc cancels."}</text>
                }
            </col>
        },
        width,
        UiContext::default(),
    )
}

let ui = build_ui(80, RunState::Running, true);
assert!(ui.height() > 0);
```

The macro requires one root element. Use `<col>` or `<row>` when the screen has several top-level regions.

## Choose a construction style

The library supports three ways to build the same retained component model.

| Style | Use it when | Entry point |
| --- | --- | --- |
| `dom!` | The structure lives in Rust and needs interpolation or control flow | `Ui::from_root(dom! { ... }, width, context)` |
| Runtime markup | Layout arrives as configuration, generated text, or editable source | `Ui::from_markup(source, width, context)` |
| Rust builders | A custom component or abstraction is clearer as typed Rust | `Ui::from_root(Col::new().child(...), width, context)` |

Runtime markup is parsed when `Ui` is constructed and returns `ParseError` on malformed input. It supports implicit Markdown text between tags, but it cannot execute Rust expressions or Rust control flow.

`dom!` is checked by the Rust compiler. It accepts arbitrary Rust expressions in braces and child-level `for`, `if`, and `match` constructs. Text in macro markup must be a string literal or a braced expression; unlike runtime markup, a bare word is not text.

Builders are useful for reusable functions and custom `Component` implementations. Builder children implement `IntoChildren`, so a child can be one component, `Option<T>`, an array, a `SmallVec`, or a `Vec`.

## `dom!` syntax

### Elements and attributes

Tags map to built-in component builders:

```rust
# use omp_tui::dom;
# let initial = "hello";
let tree = dom! {
    <box border=round pad="1 2" w=60% bg="#20242c">
        <input id=query value={initial} placeholder="Filter"/>
    </box>
};
# let _ = tree;
```

Attribute forms:

- `bold`, `grow`, `mask`, and similar flags mean `true`.
- `fg=accent`, `border=round`, and `align=center` are string values. A bare identifier is not a Rust variable.
- `title="Results"` and `pad="1 2"` preserve spaces.
- `w=50%` is a percentage.
- `value={initial}`, `h={rows}`, and `bold={enabled}` evaluate Rust expressions.
- Dashed names such as `pad-x` and custom names such as `data-kind` are accepted.

Known attributes become typed `Prop` entries. Unknown attributes are retained as custom properties for custom elements.

### Text and expression children

Use a string literal or expression inside text-like tags:

```rust
# use omp_tui::dom;
# let name = "Ada";
let tree = dom! {
    <col>
        <text>{"literal text"}</text>
        <text>{name}</text>
        <md>{"**Markdown** with `code`"}</md>
        <latex>{r"\frac{1}{2}"}</latex>
        <pre>{"┌──┐\n└──┘"}</pre>
    </col>
};
# let _ = tree;
```

A braced expression in a container is a component child rather than text:

```rust
# use omp_tui::{components::TextLeaf, dom};
let extra = TextLeaf::new().text("built in Rust");
let tree = dom! { <row>{extra}</row> };
# let _ = tree;
```

### Icons

`<i:name/>` is shorthand for a semantic icon. Dashed names such as `<i:log-in/>` are accepted. The active `Charset` chooses a Unicode, Nerd Font, or ASCII glyph:

```rust
# use omp_tui::dom;
let tree = dom! {
    <row gap=1>
        <i:info/>
        <text>{"Details"}</text>
    </row>
};
# let _ = tree;
```

`<icon name={name}/>` is useful when the icon name is dynamic.

### `for`, `if`, and `match`

Control flow appears wherever the owning component accepts children. Bodies may contain multiple elements and may nest other controls.

```rust
# use omp_tui::dom;
# let rows = [("alpha", true), ("beta", false)];
# let selected = Some("alpha");
let tree = dom! {
    <col>
        for (name, healthy) in rows {
            <row gap=1>
                if healthy {
                    <i:check/>
                } else {
                    <i:warning/>
                }
                <text>{name}</text>
            </row>
        }
        match selected {
            Some(name) => {
                <hr/>
                <text bold>{name}</text>
            },
            None => <text dim>{"Nothing selected"}</text>,
        }
    </col>
};
# let _ = tree;
```

These controls run when the component tree is built. They do not automatically re-run when widget state changes. For retained visibility driven by an input value, use `when=`; rebuild only when the application genuinely needs a different tree.

An `<editor>` is deliberately stricter: it accepts at most one editable input child and one `<status>`. Mutually exclusive `if` or `match` branches may choose those children, but a `for` loop cannot generate editor slots because it could produce duplicates.

### Parent-owned data tags

Some tags describe data owned by their parent rather than standalone components:

| Parent | Allowed data child | Purpose |
| --- | --- | --- |
| `<select>` | `<option>` | Choice value, label, description, preview, and `<td>` cells |
| `<option>` or `<tr>` | `<td>` | One aligned grid cell |
| `<table>` | `<tr>` | One table row of cells |
| `<status>` | `<segment>` | Status-band segment |
| `<tabs>` | `<tab>` | Named pane |
| `<tree>` or `<node>` | `<node>` | Tree item and nested items |
| `<todo>` or `<task>` | `<task>` | Todo row with status, blocker note, and nested rows |
| `<form>` | `<field>` | Form field metadata |
| `<wizard>` | `<step>` | Named wizard step |

The macro rejects a data tag under the wrong parent. Control flow may produce these data children under their correct owner.

## Element reference

Every catalog element is listed below. “Shared” means the element also accepts the shared sizing, identity, visibility, padding, border, and background properties documented in the [property reference](#property-reference). The **Props** column names behavior specific to that element; setting an unrelated known property is accepted and stored but has no effect unless a custom component reads it.

### Construction-mode availability

| Syntax | `dom!` | Runtime markup | Notes |
| --- | --- | --- | --- |
| Catalog tags below | Yes, except `<spinner>` | Yes | Both produce the same retained component types where available |
| `<i:name/>` | Yes | No | Macro shorthand for `Icon::named("name")` |
| `<ico:name/>` | No | Yes, inside text and attribute values | Resolved through the active `Charset` |
| Rust `{expr}`, `for`, `if`, `match` | Yes | No | Evaluated while the tree is built |
| Bare Markdown text | No | Yes | Runtime markup turns text between tags into Markdown leaves |
| Unknown tags | Yes | Yes | Become `CustomElement` and resolve through `UiContext::elements` |

### Layout elements

#### `<col>`

A vertical child stack. It measures children at the available width and places them from top to bottom.

- **Children:** Any component children.
- **Props:** Shared; `gap`; `align`; `valign`. Child `grow` shares extra height when the column has a fixed `h`.
- **Typical use:** The root of a screen, a form section, or the body inside a box.

```rust
# use omp_tui::dom;
let tree = dom! {
    <col h=12 gap=1 align=center>
        <text>{"Header"}</text>
        <spacer grow/>
        <text>{"Footer"}</text>
    </col>
};
# let _ = tree;
```

#### `<row>`

A horizontal child layout. It resolves child `w`, `min`, `max`, and `grow`, then distributes remaining width.

- **Children:** Any component children.
- **Props:** Shared; `gap`; `align`; `valign`; `justify`; `wrap`.
- **Special behavior:** A `<hr/>` child becomes vertical automatically. With `wrap`, children stack vertically when their minimum widths do not fit.

#### `<box>`

A bordered vertical stack. `Boxed::new()` supplies a square border by default.

- **Children:** Any component children.
- **Props:** Shared; `gap`; `align`; `valign`; `title`; `footer`; `title-align`/`footer-align`; `border`; `bc`/`edge`; `bleed`.
- **Mode detail:** Runtime markup also defaults `pad-x=1`; `dom!` does not, so set padding explicitly when the distinction matters.

#### `<scroll>`

A vertically scrollable stack. Arrow keys, Page Up/Down, and the mouse wheel move its viewport; focus movement chases focused descendants into view.

- **Children:** Any component children, stacked without an implicit gap. Wrap them in `<col gap=...>` when spacing is needed.
- **Props:** Shared; especially `h`, which fixes the viewport height.
- **Default:** Eight rows when no `h` is supplied.

#### `<hr/>`

A horizontal divider, or a vertical divider inside a row.

- **Children:** None.
- **Props:** Shared sizing; `border` chooses the glyph family; `title`; `fg`; `bc`/`edge`; `vertical`.
- **Special behavior:** `<row>` sets `vertical` on rule children automatically.

#### `<table>`, `<tr>`, and `<td>`

A columnar layout whose cells align vertically: every column is solved once
across all rows (widest cell wins), surplus width goes to `grow` cells'
columns, and a deficit shrinks the widest flexible column first.

- **`<table>` children:** `<tr>` only; each `<tr>` holds `<td>` cells.
- **`<table>` props:** Shared; `gap` (column spacing, default 2).
- **`<tr>` props:** `bg` paints a full-width row band.
- **`<td>` content:** Any children, laid out side by side; `<td>` props include `align`, `w`, `min`, `max`, `grow`, and `truncate`.
- **Truncation:** A `truncate` cell flattens `<pre>`/`<text>` children — keeping each child's own style — into one line clipped by a single ellipsis at the cell edge, so multi-toned labels collapse as a unit. `truncate=start` clips the head instead, keeping the distinctive tail of ids and paths visible.
- **Interaction:** None. Tables are layout-only; for a clickable, filterable list put the same `<td>` cells inside `<select>` options.

#### `<spacer/>`

Blank flexible space used to separate or push siblings.

- **Children:** None.
- **Props:** Shared sizing; normally `grow`, `w`, or `h`.
- **Mode detail:** Runtime markup defaults to `grow=1`. In `dom!`, write `<spacer grow/>` explicitly for flexible space.

### Text, rich content, and media

#### `<text>`

Verbatim text. It does not parse Markdown.

- **Content:** String literals or expressions in `dom!`; raw body text in runtime markup.
- **Props:** Shared; `fg`; `bold`; `dim`; `italic`; `underline`; `reverse`; `strike`; `align`; `truncate`; `wrap`; `shimmer`; `reveal`.
- **Wrapping:** Word-wraps by default. `wrap=char` flows grapheme-exact to the width like a bare terminal, and full-width rows flag their break as a soft wrap — the renderer joins such boundaries through terminal autowrap (mid-word overflow breaks join under word wrap too), so native selection copies the line unbroken, on screen and in scrollback.
- **Updates:** `Ui::set_text(id, value)` replaces its content. With `reveal`, a replacement that extends the current text continues the reveal from the shown prefix; any other replacement restarts it from nothing.

#### `<md>`

Markdown with tables, links, code highlighting, math, Mermaid, and Graphviz rendering.

- **Content:** Markdown source.
- **Diagram fences:** `mermaid`, plus `dot`/`graphviz`/`gv`; Graphviz rendering is pure Rust and never shells out to `dot`.
- **Props:** Shared; text-style props; `align`; `truncate`.
- **Runtime detail:** Noninteractive catalog or custom blocks can be embedded at line starts. Interactive tags are rejected inside Markdown.
- **Macro detail:** `dom!` accepts only string/expression content inside `<md>`; build embedded components with `Markdown`’s Rust builder.
- **Updates:** `Ui::set_text` reparses and relays out the document.

#### `<latex>`

LaTeX-style math rendered into terminal cells.

- **Content:** A string literal or expression.
- **Props:** Shared; text-style props; `align`; `truncate`.
- **Updates:** Supports `Ui::set_text`.

#### `<pre>`

Verbatim preformatted terminal art. Newlines and spacing are preserved.

- **Content:** A string literal or expression; runtime body text is trimmed only at outer line breaks.
- **Props:** Shared; text-style props; `align`.
- **Updates:** Supports `Ui::set_text`.

#### `<callout>`

A highlighted Markdown callout with an optional header, icon, and badge. The Rust builder type is `Callout`.

- **Content:** Markdown source.
- **Props:** Shared; text-style props; `title`; `icon`; `badge`; `truncate`.
- **Defaults:** Without `icon`, the active charset supplies an informational icon.
- **Updates:** Supports `Ui::set_text`.

```rust
# use omp_tui::dom;
let tree = dom! {
    <callout title="Build warning" badge=1 icon=warning fg=warn>
        {"The cache is stale; the next build will be slower."}
    </callout>
};
# let _ = tree;
```

#### `<icon>` and icon shorthand

A semantic icon resolved by `Charset`.

- **Content/name:** In `dom!`, use `<icon name={name}/>` or `<i:name/>`. Runtime markup uses `<icon>name</icon>`, `<icon icon=name/>`, or inline `<ico:name/>`.
- **Props:** Shared; `fg`; text-style props.
- **Fallback:** Unknown names render as their bare name rather than disappearing.

#### `<spinner>` — runtime markup and Rust builder

An animated indeterminate activity glyph driven by the [`App`](crate::App) loop. Tests and custom hosts can advance it directly with `Ui::tick`.

- **Availability:** Runtime markup and `components::Spinner`; `dom!` currently treats `<spinner>` as a custom tag.
- **Props:** Shared; `fg`; text-style props; an `id` allows `set_text` when constructed with the Rust builder.
- **Label:** Runtime `<spinner>` is currently glyph-only. Use `Spinner::new().label(...)` for trailing text.

#### `<img/>`

A terminal image with a cell-rendered fallback.

- **Children:** None.
- **Props:** Shared; `src`; `w`; `h`; `trim`.
- **Source:** `src` is a filesystem path to PNG or binary P6 PPM data. `trim` crops fully transparent margins before cell sampling, keeping padded logos visible as tiny thumbnails.
- **Graphics:** `UiContext::graphics` selects cells, sixel, Kitty placeholders, Kitty direct placements, or iTerm2. For protocol images, pair `Img::kitty(id, rows, cols)` with `Renderer::register_image`.

#### `Scene` — Rust-built 3D viewport

A deterministic CPU ray tracer rasterized into braille cells and animated on the shared presentation clock.

- **Availability:** Rust only — the shader is code. Mount `components::Scene` as a `dom!` expression child, or register a `<scene>` tag through `Elements::builder()` with a factory that captures your scene.
- **Props:** Shared; `bg` paints a backdrop behind unlit (transparent) cells.
- **Scene:** Build a physical scene from `scene::{World, Object, Primitive, Material, Light}` and pass its `PathTracer`, implement `scene::Trace` for custom animated shading, or pass a plain `Fn(Ray) -> (Vec3, f32)` closure for a still procedural view. `Scene::size(cols, rows)` fixes the cell viewport; `Scene::still()` paints once instead of waking every frame.
- **Transport:** Finite spheres, quads, disks, and custom geometry are accelerated by an owning BVH. The bounded integrator traces direct shadows, GGX reflection, dielectric refraction, emissive and environment illumination, indirect bounces, and Russian-roulette termination without per-ray allocation.
- **Color:** `Vec3` colors are linear light; `Vec3::rgb` decodes sRGB literals and terminal output applies the sRGB transfer function after sampling.

#### `Shader` — Rust-built fullscreen effect

A CPU fragment shader rasterized into half-block pixels (`▀` foreground over background, two pixels per cell), animated on the shared presentation clock.

- **Availability:** Rust only — the shader is code. Mount `components::Shader` as a `dom!` expression child, or register a `<shader>` tag through `Elements::builder()` with a factory that captures your program.
- **Props:** Shared; `bg` paints a backdrop behind unlit (transparent) cells.
- **Program:** Implement `shader::Program` (`advance` sees the clock and pixel resolution, `fragment` shades one pixel, `particles` splats point sprites over the field), or pass a plain `Fn(f32, f32) -> (Vec3, f32)` closure for a still field. `Shader::size(cols, rows)` fixes the cell viewport; `Shader::still()` paints once instead of waking every frame.
- **Built-in:** `shader::Eclipse` is the reference program — the stippled-eclipse landing shader ported from WebGPU. `examples/eclipse.rs` mounts it fullscreen; the chat demo's welcome card paints it as a backdrop through `Surface::render`.

### Input and action elements

#### `<input/>`

A focusable, single-line text input.

- **Children:** None.
- **Props:** Shared; `id`; `value`; `placeholder`; `mask`; `required`; `match`.
- **Value:** With `id`, `Ui::values()` returns a JSON string.
- **Validation:** `required` and `match` are enforced when the input is inside an active wizard step.

#### `<editor>`

A multiline editor shell with a replaceable editable child and optional status band.

- **Children:** At most one non-status input component and one `<status>`. With no input child, it creates the default multiline `EditInput`.
- **Props:** Shared; `id`; `value`; border and sizing props.
- **Value:** `id` and `value` are forwarded to the editable child; `Ui::values()` returns the expanded editor text.
- **Control flow:** Mutually exclusive `if`/`match` branches may choose children. `for` cannot generate editor slots because it could create duplicates.

```rust
# use omp_tui::dom;
let tree = dom! {
    <editor id=body value="Initial text" border=round>
        <status>
            <segment fg=ok>{"ready"}</segment>
            <segment fg=muted>{"UTF-8"}</segment>
        </status>
    </editor>
};
# let _ = tree;
```

#### `<button>`

A focusable action with a text label.

- **Content:** Text only. `label=` is the fallback, followed by `id` when no body label exists.
- **Props:** Shared; `id`; `label`; `submit`; `cancel`; `confirm`; `accent`.
- **Events:** `cancel` emits `UiEvent::Cancel`; `submit` emits `Submit`; otherwise an ID-bearing button emits `Pressed(id)`.
- **Confirmation:** `confirm` requires a second activation.

#### `<radio/>`

A compact, single-choice row of chips.

- **Children:** None.
- **Props:** Shared; `id`; `options`; `value`.
- **Options:** `options` is whitespace-delimited; `value` selects the initial option.
- **Value:** With `id`, exports the selected option as a JSON string.

#### `<select>` and `<option>`

A focusable choice list with optional filtering, multiple selection, previews, cell-based rows, and free-form values.

- **`<select>` children:** `<option>` only.
- **`<select>` props:** Shared; `id`; `label`; `multi`; `filter`; `custom`; `h` fixes the window height (the list scrolls); `gap` spaces option cells (default 2).
- **`<option>` content:** Its visible label, optional component preview children, and optional `<td>` cells. Cell options render as one aligned grid across every option (the label remains the filter haystack), with the shared table solver and cell `truncate` semantics.
- **`<option>` props:** `value`; `label`; `desc`; `recommended`.
- **Defaults:** An option’s `value` defaults to its label. The first `recommended` option becomes the initial single selection, and focus enters a single select on its chosen option.
- **Filtering:** A filterable single select types-to-filter directly — no `/` mode: printable keys, paste, `Backspace`, `Ctrl+U`, and `Ctrl+W` edit the query (shown with the hardware caret), matches are fuzzy-ranked best-first, `↑`/`↓` wrap, and `Esc` clears the query before bubbling `Cancel`. Multi selects keep the `/`-armed search so `Space` still toggles. `filter="text"` seeds the initial query.
- **Events:** With an `id`, cursor motion surfaces `UiEvent::Highlighted`, activation (Enter or click) `UiEvent::Changed`, and query edits `UiEvent::Filtered` — hosts drive detail panes from these without touching the widget.
- **Value:** Single selects export a string or `null`; `multi` exports an array.

```rust
# use omp_tui::dom;
let tree = dom! {
    <select id=theme label="Theme" filter>
        <option value=dark recommended desc="Low glare">{"Dark"}</option>
        <option value=light desc="High contrast">{"Light"}</option>
    </select>
};
# let _ = tree;
```

Cell options build pi-style browsers — aligned stat columns that survive narrow widths:

```rust
# use omp_tui::dom;
let tree = dom! {
    <select id=model filter h=6>
        <option value=fable label="anthropic/claude-fable-5">
            <td truncate grow><pre fg=muted>{"anthropic/"}</pre><pre>{"claude-fable-5"}</pre></td>
            <td align=end><pre fg=muted>{"1m"}</pre></td>
        </option>
    </select>
};
# let _ = tree;
```

### Structured input, navigation, and feedback

#### `<form>` and `<field>`

`<form>` renders a compact collection of typed field definitions.

- **`<form>` children:** `<field>` only.
- **`<form>` props:** Shared; `id`.
- **`<field>` props:** `id`; `kind`; `label`; `desc`; `value`; `options`; `min`; `max`; `step`; `required`; `match`.
- **Kinds:** `text` (default), `bool`, `enum`, `select`, `multi`, and `number`.
- **Value:** A form with `id` exports one JSON object keyed by field IDs. Boolean and number fields export JSON booleans and numbers; multi fields export arrays.

#### `<tabs>` and `<tab>`

A focusable tab bar with one active pane.

- **`<tabs>` children:** `<tab>` only.
- **`<tabs>` props:** Shared; `id`.
- **`<tab>` props:** `title`; `label` is a `dom!` alias for `title`.
- **Value:** An ID-bearing tab set exports the active tab title.
- **State:** Switching tabs preserves each pane’s retained subtree.

#### `<tree>` and `<node>`

An expandable hierarchy with branch toggling and selectable leaves.

- **`<tree>` children:** Root `<node>` records only.
- **`<tree>` props:** Shared; `id`; `guides` draws `├─`/`└─` connector gutters instead of plain indentation (bare flag for the square family, or `guides=round|heavy|double|dash`).
- **`<node>` children:** Nested `<node>` records.
- **`<node>` props:** `label`; `open`.
- **Value:** An ID-bearing tree exports the selected leaf path, joined with `/`, or `null`.

#### `<todo>` and `<task>`

A display-only task list in the coding agent's todo style: no focus, keys, or collapse state.

- **`<todo>` children:** Root `<task>` records only.
- **`<todo>` props:** Shared; `guides` selects the connector family (square by default).
- **`<task>` children:** Nested `<task>` records. A task with children renders as a bold group header with an automatic `done/total` count over its descendant leaves; leaves render a status checkbox and label.
- **`<task>` props:** `label`; `status=pending|active|done|dropped|blocked` (agent aliases `in_progress`, `completed`, and `abandoned` are accepted); `desc` carries the note shown as `(blocked: …)`.
- **Styling:** `done` paints ok with a struck label, `active` accent, `dropped` err struck, `blocked` warn with its note, and `pending` dim. Checkbox glyphs follow the active `Charset`.
- **Rust:** `components::Todo::counts()` returns leaf `(done, total)` for host-built headers like `3/14 tasks`.

```rust
# use omp_tui::dom;
let tree = dom! {
    <todo guides=round>
        <task label="Part A">
            <task status="done">{"write the parser"}</task>
            <task status="active">{"wire the renderer"}</task>
        </task>
    </todo>
};
# let _ = tree;
```

#### `<wizard>` and `<step>`

A multi-step flow with Back/Next navigation and validation.

- **`<wizard>` children:** `<step>` only.
- **`<wizard>` props:** Shared; `submit`.
- **`<step>` props:** `title`; `label` is a `dom!` alias for `title`.
- **Validation:** ID-bearing value components inside the active step can use `required` and `match`. Invalid input blocks Next and shows an error.
- **Completion:** `submit` makes the final Next action emit `UiEvent::Submit`.

#### `<status>` and `<segment>`

A compact status band composed of styled segments.

- **`<status>` children:** `<segment>` only.
- **`<status>` props:** Shared; `fg`; `bg`/`on`; text-style props; `align=end` mirrors the caps for a band docked against the right edge (opening cap points into the background, closing edge sits flat on the margin).
- **`<segment>` content:** Segment label text.
- **`<segment>` props:** `label`; `fg`; `bg`/`on`; text-style props.
- **Styling:** Segment style inherits the status style and may override it.

#### `<progress/>`

A determinate progress bar.

- **Children:** None.
- **Props:** Shared sizing; `value`; `max`; `label`.
- **Defaults:** `value=0`, `max=100`; values are clamped to the maximum.
- **Presentation:** The theme supplies filled, empty, label, and percentage colors.

### Custom elements

Any unknown tag becomes a `CustomElement`.

- **Children:** Any component children.
- **Props:** Every known prop plus arbitrary custom attributes.
- **Resolution:** Register the tag through `Elements::builder()` in `UiContext::elements`.
- **Fallback:** Without a matching factory, the custom element retains and paints its fallback children.

## Property reference

Known properties are parsed and type-checked in both construction modes. A property may still be ignored by a built-in that does not consume it; the element reference above names each built-in’s behavior-specific props.

### Shared sizing, identity, and chrome

These properties apply to standalone retained components. Parent-owned records such as `<option>` and `<field>` use only the props listed in their own sections.

| Prop | Accepted values | Effect |
| --- | --- | --- |
| `id` | String | Stable lookup key for updates, values, conditions, and button events |
| `when` | `"source=value"` or `"source!=value"` | Removes the component from layout, paint, focus, and values while false |
| `w` | Cell count or percentage such as `40%` | Preferred width; row parents resolve it, and images use it for sampling |
| `min` | Integer | Minimum row-child width; also the lower bound of number fields |
| `max` | Integer | Maximum row-child width; number-field upper bound; progress maximum |
| `h` | Integer rows | Fixed outer height; especially useful for scroll regions and flex columns |
| `grow` | Flag or numeric weight | Claims remaining width in a row or remaining height in a fixed-height vertical stack |
| `pad` | `N` or `"Y X"` | Vertical and horizontal inner padding |
| `pad-x` | Integer cells | Horizontal inner padding |
| `pad-y` | Integer rows | Vertical inner padding |
| `border` | `square`, `round`, `heavy`, `double`, `dash` | Adds border chrome; on `<hr>`, selects the stroke glyph family |
| `bc`, `edge` | Color or `start..end` gradient | Border color aliases; a gradient tints the border ring |
| `bleed` | Flag | Extends a background behind border cells |
| `title` | String | Border title, callout heading, tab title, or wizard-step title where applicable |
| `footer` | String | Label woven into the bottom border line of a framed container |
| `title-align`, `footer-align` | `start`/`left`, `center`/`middle`, `end`/`right` | Placement of the border title or footer along its frame line |

### Layout and text props

| Prop | Accepted values | Consumers |
| --- | --- | --- |
| `gap` | Integer | `<col>`, `<row>`, and `<box>` spacing |
| `align` | `start`/`left`, `center`/`middle`, `end`/`right` | Horizontal text placement and stack main-axis placement |
| `valign` | `start`/`top`, `center`/`middle`, `end`/`bottom`, `stretch`/`fill` | Box, column, and row cross-axis placement |
| `justify` | `start`, `center`, `end`, `between` | Row distribution of leftover width |
| `wrap` | Flag or `char` | Flag: lets a row stack vertically when it cannot fit. `char` on text: terminal-exact grapheme flow whose width breaks re-join in native copy |
| `truncate` | Flag or `start`/`end` | Clips text, Markdown, LaTeX, or callout content to one line with an ellipsis; `start` keeps the tail behind a leading ellipsis |
| `vertical` | Flag | Forces vertical rendering where supported; currently used by `<hr>` and set automatically by `<row>` |
| `guides` | Bare flag or `square`/`round`/`heavy`/`double`/`dash` | `<tree>` and `<todo>` connector gutters; the flag means square |

### Color and style props

| Prop | Accepted values | Effect |
| --- | --- | --- |
| `fg` | Theme token, CSS color, or `start..end` gradient | Foreground/style color on rendering elements; a gradient recolors painted cells |
| `bg`, `on` | Theme token, CSS color, or gradient | Background aliases; `bg` wins when both are present |
| `angle` | Degrees, optionally with `deg` | Gradient direction, normalized into `0..359` |
| `bold` | Flag | Bold text/style |
| `dim` | Flag | Dim text/style |
| `italic` | Flag | Italic text/style |
| `underline` | Flag | Underlined text/style |
| `reverse` | Flag | Swaps foreground and background |
| `strike` | Flag | Struck-through text/style |
| `anim` | Duration (`180`, `180ms`, `0.4s`; bare flag = 200ms) | Tweens `fg`/`bg`/`on`/`bc` colors, gradient endpoints, `w`, and `h` from the on-screen value whenever their target changes |
| `ease` | `linear`, `in`, `out`, `in-out` | Easing curve for `anim` transitions; defaults to `out` |
| `spin` | Duration (bare flag = 3s) | Continuously rotates any `fg`/`bg` gradient by one revolution per period, on top of `angle` |
| `shimmer` | Duration (bare flag = 2s) | Sweeps a brightness crest across `<text>` content once per period on the shared clock. Additive: resting cells keep the authored style, the crest's shoulders lift an RGB foreground one-fifth toward white, and its peak lifts two-fifths and paints bold (foregrounds without channel data brighten via bold alone) |
| `reveal` | Duration (bare flag = 250ms) | Types streamed `<text>` content out progressively by grapheme cluster instead of popping whole chunks in: the reveal drains its backlog exponentially over the given horizon (bursts catch up smoothly), never slower than 90 clusters/s, and settles once even with the text. Appends via `Ui::set_text` resume from the shown prefix; non-extending replacements restart from nothing; `reveal=0` shows text immediately |
| `hover` | Theme token, CSS color, or gradient | Border chrome while the pointer or focus rests on the component or a descendant: a solid recolors the ring, a gradient renders as a pointer-tracking glow that shimmers on the shared clock (keyboard focus paints the full ring); eases with `anim`/`lift` |
| `lift` | Flag or integer rows (bare flag = 1) | Reserves headroom above the component and raises its chrome into it while hovered, leaving a `shadow`-token drop shadow in the vacated rows |

Theme tokens are `fg`, `accent`, `info`, `ok`, `warn`, `err`, `muted`, `border`, `surface`, `hover`, `shadow`, and `contrast`. An unstyled `<box>` frame or `<hr>` uses the `border` token; `bc=`/`edge=`/`fg=` override it. CSS forms include HTML color names, `#rgb`, `#rrggbb`, `rgb(...)`, and `rgba(...)`.

Runtime markup inherits `fg`, text-style flags, and `truncate` into descendants. `dom!` builds explicit Rust components and does not perform parser inheritance, so place these props on the rendering child when inheritance matters. Animation props are not inherited: `anim` transitions fire on the component that declares them, the first paint never animates, retargeting mid-flight resumes from the on-screen value, and kind changes (solid ↔ gradient, cells ↔ percent) snap. Hosts drive playback by sleeping until `Ui::next_wake` and calling `Ui::tick`.

### Data, input, and action props

| Prop | Accepted values | Consumers |
| --- | --- | --- |
| `value` | String, number, bool, or `{expr}` | Input/editor initial text, segment selection, option value, field value, progress amount |
| `options` | Whitespace-delimited string | Segment choices and enum/select/multi form fields |
| `label` | String | Buttons, selects, options, fields, nodes, progress; macro alias for tab/step title |
| `desc` | String | Supporting text for options and form fields |
| `kind` | `text`, `bool`, `enum`, `select`, `multi`, `number` | Form field control type |
| `step` | Integer | Number-field increment |
| `multi` | Flag | Makes a select export multiple choices |
| `filter` | Flag | Enables interactive filtering on a select |
| `custom` | Flag | Allows a select’s free-form custom value |
| `mask` | Flag | Obscures an input’s displayed text without changing its exported value |
| `recommended` | Flag | Marks the initial preferred option in a single select |
| `open` | Flag | Expands a tree node initially |
| `status` | `pending`, `active`, `done`, `dropped`, `blocked` | `<task>` lifecycle state; drives its checkbox glyph and styling |
| `required` | Flag | Wizard validation for an ID-bearing value component |
| `match` | Anchored simple pattern | Wizard validation after trimming nonempty text |
| `src` | Filesystem path | PNG or P6 PPM image source |
| `icon` | Icon name | Callout leading icon; runtime `<icon>` name |
| `badge` | String | Compact callout header badge |
| `submit` | Flag | Submit button or submitting wizard |
| `cancel` | Flag | Cancel button |
| `confirm` | Flag | Requires two button activations |
| `placeholder` | String | Empty, unfocused input hint |
| `accent` | Flag | Accent-filled button treatment |
| `focus` | Flag | Joins the keyboard focus ring; a focused component renders its `hover`/`lift` chrome, and a focusable `id`-carrying `<box>` emits `Pressed` on Enter or click |

`match` is intentionally smaller than regular expressions. It is anchored at both ends and supports literals, `.` for any character, classes such as `[a-z0-9]` and `[^x]`, escapes, and postfix `*`, `+`, or `?`.

### Animation metadata props

| Prop | Accepted values | Parsed value |
| --- | --- | --- |
| `anim` | Flag, milliseconds, `250ms`, or `0.4s` | Transition duration; a bare flag means 200ms |
| `ease` | `linear`, `in`, `out`, `in-out` | Easing curve; defaults to ease-out |
| `spin` | Flag, milliseconds, or seconds | Rotation period; a bare flag means 3s |
| `shimmer` | Flag, milliseconds, or seconds | Crest sweep period; a bare flag means 2s |
| `reveal` | Flag, milliseconds, or seconds | Streamed-text catch-up horizon; a bare flag means 250ms |

These are recognized `Props` metadata for animation-aware custom components. The current catalog does not universally animate merely because these props are present; use `Ui::tick`, `PaintCtx::wake`, and the `anim` module when implementing animated components.

## Layout and styling

### Layout primitives

- `<col>` stacks children vertically.
- `<row>` places children horizontally.
- `gap=N` inserts space between adjacent children.
- `pad=N` applies padding on both axes; `pad="Y X"`, `pad-x`, and `pad-y` control them separately.
- `w=N` and `h=N` request cell dimensions; `w=N%` requests a percentage width.
- `min` and `max` constrain width where supported.
- `grow` claims remaining space on the container axis. In a row that means width; in a fixed-height column it means height.
- `wrap` lets a row stack when its minimum widths no longer fit.
- `<spacer/>` is the clearest way to push siblings apart.

The layout engine owns final geometry. Prefer constraints and flex behavior over calculating absolute cell positions in application code.

### Alignment

- `align=start|center|end` positions content on the writing axis.
- `valign=start|center|end|stretch` positions a container's children on the cross axis.
- Rows stretch children by default; `valign=start` opts out.
- `justify=center|end|between` distributes leftover row width. `between` anchors the first and last child at opposite edges.

### Borders and backgrounds

`border=square|round|heavy|double|dash` frames a box, row, or column. `title=` writes into the top border. `bc=` and `edge=` set its color.

Components are transparent until `bg=` or its alias `on=` is present. A framed background normally stops inside the border; `bleed` extends it behind the frame.

### Color and text style

Prefer semantic colors so the same screen works under a custom theme:

- `fg=accent`, `info`, `ok`, `warn`, `err`, or `muted`
- `bg=accent` or `on=muted`
- `bold`, `dim`, `italic`, `underline`, `strike`, and `reverse`

CSS-style colors are also accepted: HTML names, `#rgb`, `#rrggbb`, `rgb(...)`, and `rgba(...)`. A two-stop value such as `fg="magenta..cyan"` creates a gradient; `angle=90` makes it vertical.

## Identity, state, and updates

### Assign IDs to anything the application addresses

`id=` connects a retained component to update methods, output values, button events, and `when=` conditions:

```rust
# use omp_tui::{Ui, UiContext, dom};
let mut ui = Ui::from_root(
    dom! {
        <col>
            <text id=summary>{"Waiting"}</text>
            <scroll id=results h=8><md id="result-copy">{"No results"}</md></scroll>
            <input id=query placeholder="Filter"/>
        </col>
    },
    80,
    UiContext::default(),
);

assert!(ui.set_text("summary", "Running"));
assert!(ui.set_text("result-copy", "- alpha\n- beta"));
assert!(ui.set_height("results", 12));
```

`set_text` and `set_height` return `false` for an unknown ID; `set_text` also returns `false` when the component cannot replace text or the value did not change.

Call `invalidate(id)` after changing externally shared state read by a custom component. It remeasures and repaints the smallest safe region without replacing the component.

### Read interactive values

`Ui::values()` returns a JSON object containing every visible, ID-bearing value component:

```rust
# use omp_tui::{Ui, UiContext, dom};
let ui = Ui::from_root(
    dom! {
        <form id=settings>
            <field id=name kind=text label="Name" value="Ada"/>
            <field id=theme kind=enum label="Theme" options="dark light" value=dark/>
            <field id=verbose kind=bool label="Verbose" value=true/>
        </form>
    },
    80,
    UiContext::default(),
);

let values = ui.values();
assert_eq!(values["settings"]["name"], "Ada");
assert_eq!(values["settings"]["theme"], "dark");
assert_eq!(values["settings"]["verbose"], true);
```

Standalone `<input>`, `<editor>`, `<radio>`, and `<select>` values appear at their own IDs. A `<form id=...>` groups its field IDs into a nested object.

### Retained conditional visibility

`when="source=value"` and `when="source!=value"` show a component according to another named value:

```rust
# use omp_tui::dom;
let tree = dom! {
    <col>
        <radio id=mode options="basic advanced" value=basic/>
        <box when="mode=advanced" border=round>
            <input id="advanced-path" placeholder="Custom path"/>
        </box>
    </col>
};
# let _ = tree;
```

Conditions update after input events and text updates. Hidden components leave layout, painting, focus, and `Ui::values()` until their condition matches again.

## Route application events

[`App`](crate::App) is the canonical retained-UI host. It resolves capabilities, owns the terminal and renderer, routes native input, schedules animations, coalesces resizes, and presents damage between application events:

```rust,no_run
use std::io;

use omp_tui::{AppEvent, AppOptions, Key, Ui};

#[tokio::main]
async fn main() -> io::Result<()> {
    let mut app = AppOptions::new()
        .quit([Key::Ctrl('c'), Key::Ctrl('q')])
        .start(|env| {
            Ui::from_markup(
                r#"<scroll id="pane" h=12><text>Hello</text></scroll>"#,
                env.viewport.width,
                env.ctx,
            )
            .expect("static markup parses")
        })
        .await?;

    while let Some(event) = app.next().await? {
        if let AppEvent::Resized(viewport) = event {
            app.ui_mut()
                .set_height("pane", viewport.height.saturating_sub(2));
        }
        // Read submitted values or apply dependent updates here. App presents
        // those mutations when `next()` is called again.
    }
    Ok(())
}
```

`App::next` returns application-level outcomes after routing input into the tree:

- `Updated` means input changed or damaged the tree. Read `App::ui().values()` and apply dependent mutations before the next call presents it.
- `Submitted` means the focused widget submitted.
- `Pressed(id)` carries the ID of an activated button.
- `Resized(size)` means the resize storm settled. `Ui::resize` already applied the new width; update fixed-height components before the next call rebuilds the terminal view.

Ctrl-C quits by default. `AppOptions::quit` replaces the quit chords, and `keep_on_cancel` prevents a top-level `UiEvent::Cancel` from stopping the host. Once stopped, `next` continues to return `None`.

### Mouse reporting

Inline sessions leave the mouse to the terminal, so native text selection and scrollback keep working. Pointer-driven screens opt in with `AppOptions::mouse()` (or `TerminalOptions::mouse(true)` for immediate-mode hosts), which enables click, drag, motion, and wheel reports for the whole session. The alternate screen always enables reporting while it is active and restores the inline policy on exit.

### Async host

`App::handle` returns a cloneable `UiHandle` for tasks and synchronous threads. `update` queues an arbitrary mutation; `set_text` and `invalidate` cover the common retained-tree updates; `shutdown` cancels the host. Sends never block and become no-ops after the `App` is gone.

The App-installed image loader reads and decodes `<img>` sources on Tokio's blocking pool. Layout first paints the themed box placeholder; delivery remeasures and repaints the image at the smallest safe retained-tree region.

Hosts that need raw key or mouse events can drop down to `Terminal` and multiplex `Terminal::next` — one async mailbox of `TerminalEvent`s (decoded input, resize, debug queries) — with their own timers using `tokio::select!`. Pass terminal response events through `Terminal::handle_input_event` and resolve `TerminalEvent::Resize` with `Terminal::take_resize`. The `chat` example is the immediate-mode reference.

### Custom keybindings

Every `Terminal` starts with the default `Keymap`. Edits ship to the event actor's live decoder and apply to the next decoded chord:

```rust
# fn configure(terminal: &mut omp_tui::Terminal) {
use omp_tui::{Chord, Key, Mods};

let alt_n = Chord::new(Key::Char('n'), Mods { alt: true, ..Mods::default() });
terminal.edit_keymap(|keymap| keymap.bind(alt_n, Key::PageDown));
# }
```

`Keymap::disable` masks a chord, including its identity fallback; `Keymap::unbind` removes a table entry and restores fallback handling. Exact bindings win before shift-folded spellings and identity fallbacks. `InputDecoder` exposes `keymap()` and `keymap_mut()` accessors for applications that decode their own byte streams.

### Renderer stability boundary

The final `stable_rows` argument to `Ui::present` and `Renderer::rebuild` declares an immutable document prefix. Use `0` for a fully mutable application. Use a larger value only for rows that will never change again, such as completed transcript entries moving into native scrollback. Mutating an already committed stable row is rejected because terminal scrollback is not addressable.

### Resize without losing state

Call `ui.resize(new_width)` rather than rebuilding the `Ui`. Update fixed viewport components with `set_height`, then rebuild the renderer's terminal view. This preserves active tabs, editor text, selections, focus, and scroll positions.

## Paste and clipboard

Paste flows through one pipeline regardless of how the bytes arrive; components only ever see `Component::paste` text (or, one level up, `InputEvent::Paste`).

- **Bracketed paste** is enabled for every session. The decoder reassembles chunked payloads (64 MiB cap, 1 s inactivity recovery), decodes tmux's re-encoded control bytes, normalizes newlines, and strips C0 controls before `InputEvent::Paste` is emitted.
- **Enhanced paste (OSC 5522)** is probed via DECRQM and enabled when the terminal supports it (`TerminalCaps::paste_events`; kitty today). A terminal-level paste then arrives as an out-of-band clipboard offer instead of bracketed text, which is how an *image* paste reaches the app. `Terminal` answers the offer conversation internally — MIME listing, priority pick (`png > jpeg > webp > gif > text/plain`), chunked transfer — and the assembled `Pasted` payload surfaces through `Terminal::take_paste`, mirroring `take_resize`. `App` dispatches it automatically; immediate-mode hosts check `take_paste` after a consumed `handle_input_event` (see the `chat` example's `user_event`).
- **Ctrl+V / Ctrl+Shift+V** resolve to the semantic keys `Key::Paste` and `Key::PasteRaw` in the default `Keymap`. When the focused component leaves them unclaimed, `App` reads the system clipboard on a detached thread — image first, then file-manager file URLs, then text — and routes the result back through the paste pipeline. The raw spelling reads text only and inserts it **verbatim** via `Component::paste_raw`: no drop classification, no large-paste collapse, so bulk text stays inline and editable. An *empty* bracketed paste (macOS `Cmd+V` with an image-only pasteboard) triggers the smart read. Backends live in the `paste` module: arboard (with a process-lifetime Linux handle so the X11 selection owner survives) plus platform bridges — `pbpaste`/`osascript` file URLs, `wl-paste`/`xclip`/`xsel`, PowerShell for Windows and WSL interop, Termux. All block; see the module docs for the detached-thread contract.
- **Ordering**: input that arrives while a clipboard read is in flight is queued and replayed afterwards, so an Enter typed right after Ctrl+V submits *with* the paste instead of before it. Quit chords bypass the queue, and a read that outlives its 10 s ceiling is abandoned (its late result dropped by generation) so a hung backend can never wedge input. Reads run on detached threads — never tokio's blocking pool, which cannot abort a running task and would stall runtime shutdown behind a wedged native clipboard. The `chat` example gets the same guarantees by pausing its `terminal.next()` branch behind an absolute deadline.
- **Dropped paths**: `paste::dropped_paths` classifies pasted text that is really a drag-and-drop — quoted or backslash-escaped paths, `file://` URLs (percent-decoded), Windows drive/UNC anchors, multi-file drops, and the unescaped-space macOS screenshot form. An editor with a bound `Attachments` queue stages image paths (existing files only) as `<icon> #N` chips whose submit-time payload is the path; pasted images persist to a temp file first and route the same way.
- **Copy**: `Terminal::copy_to_clipboard` writes OSC 52 (works over SSH) and spawns a best-effort native write.

## Overlays

An overlay is a viewport layer — a model picker, a confirmation dialog, a persistent sidebar — composited above the document without disturbing it. Each overlay is its own retained `Ui` stacked on the presenting one:

```rust,no_run
# use omp_tui::{dom, OverlayAnchor, OverlayOptions, Ui, UiContext};
# let mut ui = Ui::from_markup("<text>base</text>", 80, UiContext::default()).unwrap();
let picker = ui.show_overlay(
    dom! {
        <box border=round title="Switch Model">
            <select id=model>
                <option value="fable">{"claude-fable-5"}</option>
                <option value="opus">{"claude-opus-5"}</option>
            </select>
        </box>
    },
    OverlayOptions::default().anchor(OverlayAnchor::Center).min_width(44),
);

// The overlay is a full retained Ui: address it through its id.
let choice = ui.overlay(picker).map(|overlay| overlay.values());
ui.close_overlay(picker);
```

Behavior:

- **Placement is declarative.** `OverlayOptions` resolves against the viewport at every present: `anchor` (nine positions, `Center` default), `width`/`max_height` as cells or percentages (`Dim::Cells`, `Dim::Pct`), `margin` insets, `offset_x`/`offset_y` nudges, explicit `row`/`col` overrides, and `min_viewport` to gate the layer on small terminals. The default width is `min(80, available)`.
- **Modal layers capture input (the default).** The topmost visible modal overlay receives every key and paste. A cancel from inside a layer (`Esc`, or a `<button cancel>`) dismisses it before anything else: the `App` runtime closes that layer and returns `AppEvent::OverlayClosed(id)` (quit-on-cancel only applies to the base tree), while manual hosts see `UiEvent::Cancel` and call `close_active_overlay`, which dismisses the layer that emitted it even when a higher-z non-modal pane sits above it in the stack (`close_top_overlay` pops the stack top regardless of modality). The base tree keeps its focus untouched, so closing an overlay restores the previous interaction exactly. Mouse input inside the overlay's bounds is routed to it and occluded from the document; clicks outside still reach the base tree.
- **Scrollback stays clean.** Overlays are composited by the renderer as z-ordered viewport layers. The document keeps scrolling and committing while a layer is open: a row leaving the viewport is repainted from the raw document before it enters native scrollback, so overlay cells can never leak into terminal history.
- **Stacking nests.** Later `show_overlay` calls stack on top; explicit `z` on `OverlayOptions` orders layers regardless of creation order, and ties stack newest-on-top. `set_overlay_hidden` parks a layer without losing its editor text, selection, or scroll state.

### Non-modal layers and sidebars

`OverlayOptions::non_modal()` turns a layer into a persistent pane instead of a dialog: keys and paste stay with the base tree, `Esc` never dismisses it, and the `App` runtime keeps presenting inline instead of holding the alternate screen — the document keeps committing to native scrollback beneath the pane, and a row entering history is repainted from the raw document first, so sidebar cells never reach history.

```rust,no_run
# use omp_tui::{dom, Dim, OverlayAnchor, OverlayOptions, Size, Ui, UiContext};
# let mut ui = Ui::from_markup("<text>transcript</text>", 120, UiContext::default()).unwrap();
let sidebar = ui.show_overlay(
    dom! {
        <col pad="0 1" gap=1>
            <text bold>{"Session"}</text>
            <hr/>
            <spacer grow/>
            <text dim>{"ctrl+b toggles"}</text>
        </col>
    },
    OverlayOptions::default()
        .anchor(OverlayAnchor::Right)
        .width(Dim::Cells(28))
        .non_modal()
        .fill_height()
        .min_viewport(Size::new(100, 0)),
);

// Hand the keyboard to the pane and back; a click inside or outside
// the band does the same.
ui.focus_overlay(sidebar);
assert_eq!(ui.focused_overlay(), Some(sidebar));
ui.blur_overlay();
```

Keyboard hand-off:

- The topmost visible **modal** overlay always wins the keyboard; a focused non-modal pane resumes when it closes.
- `focus_overlay` activates the pane's focus ring so its chrome shows where typing lands. A click inside the band focuses it; a click outside, an unconsumed `Esc`, or a `<button cancel>` inside blurs it back to the base tree (nothing is dismissed).
- `focused_overlay()` reports the pane holding the keyboard; `top_overlay()` reports whichever layer currently receives keys, modal or focused. The hardware caret follows the same ownership: the active layer places it (or hides it when it has no caret of its own), while passive panes let the document's caret show through.
- Hiding (`set_overlay_hidden`) or closing the focused pane returns the keyboard to the base tree.

`fill_height()` stretches a retained overlay tree to the full available viewport height on every present (margins and `max_height` still apply), so `grow` and `valign` lay the rail out like a full-height column; without it the band follows content height. Raw-frame `Layer` hosts size their frame directly instead — see `examples/chat` for a full-height, click-to-focus sidebar over an immediate-mode document.

Teardown: the final inline screen persists into native scrollback once the shell resumes, so a pane must not be left composited at exit. `App` scrubs automatically on drop; manual hosts call `Renderer::clear_layers()` (after releasing any alternate-screen hold) before dropping the `Terminal`, which repaints the pane's bands from the raw document.

Limitations: direct-drawn images (sixel, iTerm2, Kitty direct) are not occluded by overlays — cell-based Kitty placeholder graphics are. The transient resize preview paints only the document; overlays reappear at resize settle.

## Terminal lifecycle

`Terminal::enter` takes exclusive ownership of the controlling terminal, installs emergency restore hooks, enables raw input and the supported keyboard protocol, and starts the input pump. `Terminal::leave` is idempotent: it disables keyboard enhancement before draining late input, clears progress, restores the previous title and terminal modes, and finally restores raw mode. `Drop` performs normal teardown on early returns; panic and fatal-signal paths use an allocation-free blind restore. `Terminal::emergency_restore` exposes that crash-path restore when an application has its own fatal handler.

Entry resets ANSI insert mode (IRM 4) and new-line mode (LNM 20) so cell writes replace in place and Return decodes once; queried prior states are restored on normal and emergency teardown. Appearance (2031) and in-band resize (2048) notifications are enabled and disabled only when the session owns them.

`TerminalOptions::default()` lets `Terminal::enter` negotiate capabilities while feeding probe-window bytes into the same streaming decoder the live pump owns. `TerminalOptions::new(caps)` supplies capabilities resolved beforehand; add `.probe_results(probe)` when they came from `negotiate` so preserved bytes, partial escape sequences, and queried prior mode states reach entry without loss. Optional `CursorStyle`, probe timeout, and stderr-capture policy are also configured here. `Terminal::caps()` returns the resolved session capabilities. While the session is active, `Terminal::set_title` sets the window title safely and `Terminal::set_progress` reports `Progress::Value`, `Error`, `Indeterminate`, `Paused`, or `Clear`. Teardown clears both automatically.

## Detect terminal capabilities

`detect()` is the fast, environment-only path. `negotiate(timeout) -> (TerminalCaps, ProbeResults)` adds a bounded controlling-terminal probe; `ProbeResults::preserved_input` retains every non-probe byte in original order. `negotiate_async` performs the same work on Tokio's blocking pool. Prefer `Terminal::enter(TerminalOptions::default())` when capabilities are not needed beforehand: entry negotiates internally, completed key, mouse, paste, and focus events are ready on the first `read`, terminal responses remain internal, and partial sequences continue in the live pump's decoder.

When capabilities are needed before entry, pass both halves back with `TerminalOptions::new(caps).probe_results(probe)`. Keep the UI context and renderer aligned with `terminal.caps()` fields such as `graphics`, `sync_output`, `screen_to_scrollback`, `hyperlinks`, `cell_px`, and `inside_tmux`. `TerminalCaps` records terminal identity, selected graphics and notification protocols, keyboard support, pixel geometry, appearance, resize support, and multiplexer state; `TerminalCaps::resolve` applies probe results or an explicit graphics override.

The graphics detector recognizes these crate-specific overrides:

| Variable | Effect |
| --- | --- |
| `OMP_FORCE_IMAGE_PROTOCOL` | `kitty`, `iterm`/`iterm2`, or `sixel`; another nonempty value forces cell rendering |
| `OMP_TUI_CHARSET` | `ascii`, `unicode`, or `nerd` overrides the glyph tier inferred from the emulator |
| `OMP_NO_KITTY_PLACEHOLDERS` | A truthy value disables Kitty Unicode placeholders |
| `OMP_KITTY_PLACEHOLDERS` | A truthy or falsy value explicitly enables or disables placeholders |
| `OMP_NO_SYNC_OUTPUT` | Any nonempty value disables synchronized output |
| `OMP_SYNC_OUTPUT` | `1` enables and `0` disables synchronized output |
| `OMP_FORCE_SYNC_OUTPUT` | `1` enables synchronized output |
| `OMP_NO_HYPERLINKS` | `1` disables OSC 8 hyperlinks |
| `OMP_FORCE_HYPERLINKS` | `1` enables OSC 8 hyperlinks |

For placeholder overrides, truthy means `1`, `true`, `on`, `yes`, or `y`; falsy means `0`, `false`, `off`, `no`, or `n`, case-insensitively.

## Themes, glyphs, and appearance

Pass an explicit `UiContext` when the default theme or terminal capability tier is not appropriate:

```rust
# use omp_tui::{Charset, Color, Theme, Ui, UiContext, dom};
let context = UiContext {
    charset: Charset::Ascii,
    theme: Theme {
        accent: Color::Rgb(0x7c, 0x9c, 0xff),
        warn: Color::Rgb(0xff, 0xc8, 0x57),
        ..Theme::default()
    },
    ..UiContext::default()
};

let ui = Ui::from_root(dom! { <text fg=accent>{"portable"}</text> }, 40, context);
# let _ = ui;
```

`Charset::Unicode`, `NerdFont`, and `Ascii` change icons and structural glyphs without changing markup. When negotiation reports the background, `UiContext::with_terminal_caps` selects `Appearance::Dark` or `Appearance::Light` and the matching theme. `Terminal::appearance` returns the latest classification, and `Terminal::on_appearance_change` observes later terminal changes.

The context stays swappable after construction: `Ui::set_context` applies a new context to the retained tree and every stacked overlay, discarding cached themed output and relaying out — no rebuild, and widget state (scroll, selection, filter queries, animations) survives. `App` does this automatically when the terminal flips between dark and light: a stock palette follows the flip, a custom theme is preserved, and either way the host surfaces `AppEvent::Appearance` so the app can refresh colors it derived outside the theme. Structure parsed from markup is retained; swapping `elements` affects future parses only.

## Graphics protocols and images

`Graphics` selects one of five renderer paths:

- `Cells` decodes PNG or binary P6 PPM sources into colored half-block cells.
- `Sixel` materializes registered PNGs as DEC sixel images.
- `KittyPlaceholders` uses Kitty Unicode placeholder cells for registered images.
- `KittyDirect` uses cursor-positioned Kitty placements.
- `Iterm2` emits iTerm2 inline images.

`<img src=.../>` and `components::Img` always retain a cell fallback. For a protocol image, build `Img::kitty(id, rows, cols)` and register the same nonzero, 24-bit ID with `Renderer::register_image(id, png_bytes)`. Before first presentation, select `Renderer::set_graphics(caps.graphics)`, apply `caps.cell_px` with `set_cell_pixel_size`, and call `set_tmux_passthrough(caps.inside_tmux)`. The `companies` example shows the complete registration flow.

## Hyperlinks

Markdown links and autolinks carry hyperlink identity automatically. Rust-built rich text can attach a target with `Style::link(url)`. Hyperlink identities remain in the frame regardless of terminal support; `Renderer::set_hyperlinks(caps.hyperlinks)` controls whether they materialize as OSC 8 output. `OMP_NO_HYPERLINKS=1` and `OMP_FORCE_HYPERLINKS=1` override conservative detection.

## Notifications

Build a `Notification` and pass it with the detected capabilities to `notify`. Delivery selects Kitty OSC 99, OSC 9, or the terminal bell; the bell path can also use the Linux freedesktop notification service.

```rust,no_run
use std::io;

use omp_tui::{Notification, Urgency, detect, notify};

fn main() -> io::Result<()> {
    let caps = detect();
    let notification = Notification::builder()
        .title("Build complete")
        .body("All checks passed")
        .urgency(Urgency::Normal)
        .build();
    let mut out = io::stdout();
    notify(&mut out, &caps, &notification)
}
```

## Custom elements and components

Use a custom `Component` when a view needs behavior the built-ins do not provide. A component supplies properties, identity, measurement, height, painting, and optionally placement and input methods. Allocate its stable identity with `next_slot()`.

Unknown `dom!` tags become `CustomElement` instances. Register factories through `Elements::builder()` and place the resulting registry in `UiContext::elements`. This lets application markup use domain names such as `<build-summary>` while the factory returns an ordinary component tree.

Prefer composing built-ins before implementing `Component`; composition automatically inherits layout, focus routing, styling, conditional visibility, and incremental repaint behavior.

## Test without a terminal

`Ui` paints its initial `Frame` during construction, and input methods are terminal-independent. Most behavior tests need neither raw mode nor a real writer:

```rust
# use omp_tui::{Key, Ui, UiContext, UiEvent, dom};
let mut ui = Ui::from_root(
    dom! { <input id=query value="a"/> },
    20,
    UiContext::default(),
);

assert_eq!(ui.values()["query"], "a");
assert_eq!(ui.handle_key(Key::Char('b')), UiEvent::None);
assert_eq!(ui.values()["query"], "ab");
assert!(ui.frame().size().height > 0);
```

Test application outcomes and submitted values rather than builder plumbing. Use `Renderer<Vec<u8>>` only when the test specifically concerns emitted terminal bytes or differential painting.

## Debug a running app (`OMP_TTY` + `OMP_TUI_DEBUG`)

Two environment variables make a live application scriptable without a real
terminal:

- `OMP_TTY=<pty-slave-path>` reroutes every terminal open — input, rendered
  frames, capability probes — to that device. A harness holding the master
  side captures the exact byte stream a terminal would see. `SIGWINCH` does
  not reach an override device; set the window size with `TIOCSWINSZ` on the
  master and trigger the `resize` op below.
- `OMP_TUI_DEBUG=<unix-socket-path>` makes `Terminal::enter` start a server
  thread that binds a socket there and answers one JSON request per line.
  The wire speaks `TerminalEvent` directly: injected input rides the same
  mailbox as decoded terminal bytes, screen ops answer from the snapshot
  the renderer publishes on every paint, and retained-state ops ride the
  mailbox as `TerminalEvent::Debug` queries that an `App` answers from
  live retained state.

Each request is `{"op": ...}`; each response is one JSON line with `"ok"`:

| op | fields | effect |
| --- | --- | --- |
| `info` | | viewport, document height, window top, overlay summary |
| `text` | | the visible viewport as text — whatever was painted last, alternate screen included |
| `frame` | | the full document frame as text rows (`App` hosts) |
| `tree` | | component tree: kinds, `id`s, rectangles, visibility, focus, overlay bands (`App` hosts) |
| `values` | | `Ui::values()` of the base tree (`App` hosts) |
| `keys` | `keys` | inject decoded keys: `"tab C-a enter 'literal text'"` |
| `event` | `event` | inject serialized `TerminalEvent`s verbatim |
| `bytes` | `data` | feed raw bytes through the live input decoder |
| `paste` | `text` | inject a bracketed paste |
| `mouse` | `x`, `y`, `action` | inject a gesture (`click`, `drag`, `release`, `move`, `wheel-up`, ...) |
| `resize` | | re-read tty geometry, then run the normal resize/settle flow |
| `quit` | | inject `C-c`, the conventional quit chord |

Injected input lands in the ordinary event mailbox, so the host observes it
exactly like terminal input — quit chords, focus routing, and overlay
dismissal all apply. `frame`, `tree`, and `values` need a retained tree:
`App` hosts answer them through the same mailbox (`TerminalEvent::Debug` →
`omp_tui::respond_debug_query`), and the server times the request out for
hosts that ignore the query.

Inside this repository the `.omp/tools/tui.ts` agent tool wraps the whole
loop: it spawns an example or bin on a Bun-native PTY (a real controlling
terminal, so SIGWINCH resizes and immediate-mode hosts work) with
`OMP_TUI_DEBUG` set, then exposes screenshots, tree dumps, input injection,
resizes, and raw byte-stream statistics as one session-based tool.

## Common mistakes

- **Rebuilding after every event:** this resets retained widget state. Route events into the existing `Ui`.
- **Using bare text in `dom!`:** write `<text>{"hello"}</text>` or `<text>{value}</text>`. Bare implicit Markdown belongs to runtime markup.
- **Forgetting braces around Rust values:** `fg=color` means the literal string `"color"`; `fg={color}` evaluates the variable.
- **Using a data tag under the wrong owner:** `<option>` belongs under `<select>`, `<tab>` under `<tabs>`, and so on.
- **Treating construction-time `if` as reactive:** use `when=` for value-driven retained visibility.
- **Rebuilding on resize:** call `resize` and `set_height` so focus, selections, and scroll offsets survive.
- **Passing screen rows to `handle_mouse`:** translate them to document rows when content is taller than the viewport.
- **Marking mutable content stable:** keep `stable_rows` at `0` unless the prefix is permanently immutable.
- **Skipping terminal restoration:** enter through `Terminal`; its explicit `leave`, `Drop`, panic hook, and fatal-signal handlers restore the modes it owns.

## Run the bundled examples

```sh
cargo run -p omp-tui --example gallery
cargo run -p omp-tui --example chat
cargo run -p omp-tui --example companies
cargo run -p omp-tui --example footers
```

`gallery` is one tabbed application hosting every showcase pane: Markdown, Math, Mermaid, and Graphviz rendering, a `dom!`-built macro pane, a live editor-driven preview, the `Anim` prop-tween lab (autoplaying, with scene hotkeys), the `Overlay` modal demo (`Ctrl+K`/`Ctrl+G`), the fullscreen `Eclipse` shader, and the chat example's model `Picker` inline. It demonstrates `dom!`, retained updates, unclaimed-key routing (`AppEvent::Key`), mouse input, resize handling, and differential rendering in one compact application. `chat` remains the standalone interactive chat demo with its picker, sidebar, and alt-screen welcome scene.