# jolt-tui glimmer's **terminal** backend, behind the retained-tree C ABI — `libjolttui.so`. [glimmer-tui](https://github.com/jolt-lang/glimmer-tui) is the design this follows: the same tags, the same props, the same keyboard, and the same rule that painting goes through a grid so a test needs no terminal. What moves is where the widget layer lives. There it is jolt over ncurses; here it is Rust behind the ABI `libvidya` already exports, so the jolt side picks a GPU window or a terminal by naming a different shared object and changing nothing else. ``` glimmer (reactive cells, components, the reconciler) │ ┌────────────────┴────────────────┐ libvidya.so libjolttui.so egui, a GPU window a terminal, in cells ``` ## Why the tree is down here A reconciler needs widgets to patch. A terminal has none — it has a grid you overwrite — so the same problem egui poses turns up again, and gets the same answer: the node arena lives in Rust, the caller mutates it by integer handle, and nothing is drawn until `tui_frame` walks the whole tree at once. Two things follow, both of them the reason for the arrangement rather than accidents of it: * **FFI traffic tracks edits, not frames.** A screen that is not changing costs no crossings; only what the reconciler actually changed is sent. * **Painting is testable.** Everything above `term.rs` writes into a `Screen` — a grid of styled cells — and only `term.rs` emits an escape sequence. `tui_headless` opens a session with no terminal at all, `tui_feed_key` types into it, and `tui_screen_line` reads back what was painted. That is not a second implementation for tests; it is the same code path with the writer taken off the end. **Handlers do not cross the boundary.** A node reports that it was clicked and the caller looks up whose `:on-click` that was, exactly as with libvidya. ## The ABI [`include/jolttui.h`](include/jolttui.h) is the reference: tags, props, events, key names, and the borrowed-string rule. In outline: | | | |---|---| | session | `tui_open` / `tui_headless` / `tui_close`, `tui_should_close`, `tui_quit` | | loop | `tui_tick(timeout_ms)` handles input, `tui_frame` lays out, paints and flushes | | nodes | `tui_node_new` / `_free` / `_append` / `_remove` / `_insert_after` / `_replace` | | props | `tui_node_set_str` / `_num` / `_bool`, and the matching reads | | reading back | `tui_node_tag`, `tui_node_child_at`, `tui_tree_dump`, `tui_screen_line` | | events | `tui_tree_poll_event` and the four accessors | | input by hand | `tui_feed_key` / `_click` / `_wheel`, `tui_focus` | ## Keyboard Focus is a ring in paint order over the widgets that can take it — buttons, checkbuttons, entries, listboxes. Tab and Shift-Tab walk it, `:autofocus` claims it on the first frame, and a widget that is unmounted or turned insensitive gives it up rather than stranding the focus on nothing. | | | |---|---| | Enter, Space | activate the focused widget | | arrows, `j`/`k`, Page Up/Down, `g`/`G` | move a list's cursor | | Ctrl-A/E, Ctrl-W, Ctrl-U/K, Alt-B/F, Home/End | readline editing in an entry | | up/down, Shift-Enter (Alt-Enter, Ctrl-J) | a row at a time, and a new row, in an `:entry` of more than one `:rows` | | Esc | closes the topmost `:overlay` | | Ctrl-C, Ctrl-Q | quit | An `:entry` with `:rows` above one — or with a newline already in its text — is a box rather than a line: it wraps, it scrolls to keep the caret in view, Home and End are about the row the caret is on, and a click lands on the character it landed on rather than on the same row every time. Enter still activates, so the compose bar of a chat client sends on Enter and breaks the line on Shift-Enter; Alt-Enter and Ctrl-J do the same on a terminal that cannot tell Shift-Enter from Enter. Up and down off the ends of the text are not the field's keys and go to the caller, so arrowing out of a field still works. Anything nothing here wanted comes out as a `key` event on the focused node, named the way it is fed in — `"ctrl+u"`, `"page-down"`, `"f5"`. Bubbling it to a container's `:on-key` belongs to the caller: it holds the handlers. ## Layout Every node answers two sizes — natural and minimum. A box hands out the natural ones when there is room, shrinks them proportionally toward the minimums when there is not, and gives the surplus to whoever set `:hexpand` / `:vexpand`. `:width-request` and `:height-request` are a floor on both, so asking for four rows gets four rows even when space is short. ## Building ```bash cargo build -p jolt-tui --release # target/release/libjolttui.so cargo test -p jolt-tui # the whole widget layer, headless cargo run -p jolt-tui --example showcase # every tag, in a terminal ``` `--no-default-features` drops crossterm and with it `tui_open` — the tree, the layout, the painter and `tui_headless` all still work, which is what makes this crate buildable and checkable on a machine with no terminal crate and no TTY. ## Limits * **One session per process, on one thread.** The session lives in thread-local storage, so a call from another thread is inert rather than unsound — the same rule libvidya keeps. * **One cell per character.** A wide CJK glyph or an emoji is measured as one column and will crowd its neighbour. Widths are a table lookup away; nothing in the design is in the way of adding one. * **No `:table`, `:paginator` or `:help` yet.** glimmer-tui has them; a table is a keyed listbox of rows here for now. Each is widget-layer work in `src/paint.rs` plus a measure in `src/layout.rs` and a tag.