| Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago | 1 | /* |
| 2 | * jolt-tui — glimmer's terminal backend, behind the retained-tree ABI. |
| 3 | * |
| 4 | * The tree half of `vidya_tree.h` with a terminal under it instead of a GPU |
| 5 | * window: nodes are integer handles, mutated by the calls below, and nothing is |
| 6 | * drawn until `tui_frame`, which lays the whole tree out and paints it at once. |
| 7 | * Interactions come back as a queue the caller drains and routes to its own |
| 8 | * handlers — a callback cannot cross this boundary, so identity does instead. |
| 9 | * |
| 10 | * It is deliberately the same shape as libvidya's, because it is the same |
| 11 | * glimmer backend on the other side: one reconciler, and a choice of shared |
| 12 | * object. The differences are the ones a terminal actually forces — a focus |
| 13 | * ring and keys instead of a pointer and hit testing, cells instead of points, |
| 14 | * and colours that are the terminal's rather than a theme's. |
| 15 | * |
| 16 | * Every call stays on the thread that opened the session, which is where the |
| 17 | * state lives; a call from another thread is inert rather than unsound. |
| 18 | * |
| 19 | * Tags are hiccup names without the colon: |
| 20 | * |
| 21 | * containers window box hbox vbox frame scroll overlay listbox |
| 22 | * widgets label title title-2 dim-label button checkbutton entry |
| 23 | * separator spacer progress spinner |
| 24 | * |
| 25 | * An unrecognized tag is kept and paints as a vertical box, so a caller ahead |
| 26 | * of this backend still sees its children. |
| 27 | * |
| 28 | * Props are string-keyed, in the same names: |
| 29 | * |
| 30 | * every node sensitive (bool), margin, padding, width-request, |
| 31 | * height-request, halign / valign |
| 32 | * ("fill"|"start"|"center"|"end"), hexpand / vexpand (bool), |
| 33 | * color, bg, bold, dim, underline, reverse, blink, italic |
| 34 | * box orientation ("horizontal"|"vertical"), spacing |
| 35 | * frame label (drawn into the top border) |
| 36 | * scroll offset (rows, clamped to the content and written back) |
| 37 | * overlay label; floats centred over everything else |
| 38 | * text widgets label, or text |
| 39 | * button label, kind ("default"|"primary"|"destructive") |
| 40 | * checkbutton label, active (bool) |
| 41 | * entry text, placeholder, rows, autofocus (bool) |
| 42 | * listbox selected (row index; -1 for no cursor). Its children are |
| 43 | * the rows, one node each. |
| 44 | * spacer size |
| 45 | * progress value (0..1), label |
| 46 | * |
| 47 | * A colour is a name (`red`, `bright-blue`, `default`), an index into the |
| 48 | * xterm 256-colour palette (`"33"`), a hex triple (`#ff6432`, `#f64`), or |
| 49 | * `"r,g,b"`. Colour and attributes are inherited by a node's whole subtree. |
| 50 | */ |
| 51 | #ifndef JOLTTUI_H |
| 52 | #define JOLTTUI_H |
| 53 | |
| 54 | #if defined(_WIN32) |
| 55 | # if defined(JOLTTUI_BUILD) |
| 56 | # define JOLTTUI_API __declspec(dllexport) |
| 57 | # else |
| 58 | # define JOLTTUI_API __declspec(dllimport) |
| 59 | # endif |
| 60 | #elif defined(__GNUC__) |
| 61 | # define JOLTTUI_API __attribute__((visibility("default"))) |
| 62 | #else |
| 63 | # define JOLTTUI_API |
| 64 | #endif |
| 65 | |
| 66 | #ifdef __cplusplus |
| 67 | extern "C" { |
| 68 | #endif |
| 69 | |
| 70 | /* The session |
| 71 | * |
| 72 | * `tui_open` takes the terminal: raw mode, the alternate screen, no cursor |
| 73 | * unless a focused entry asks for one, and mouse reporting when `mouse` is |
| 74 | * non-zero. 1 on success, 0 if a session is already open or the terminal |
| 75 | * refused. `tui_close` gives all of it back and is safe to call twice. |
| 76 | * |
| 77 | * `tui_headless` opens a session of a fixed size with no terminal at all. The |
| 78 | * whole widget layer works there — layout, painting, focus, keys fed by hand — |
| 79 | * and `tui_screen_line` reads the result back, which is what a test suite and |
| 80 | * CI use. It is the same code path a real session paints through. |
| 81 | */ |
| 82 | JOLTTUI_API int tui_open(int mouse); |
| 83 | JOLTTUI_API int tui_headless(int width, int height); |
| 84 | JOLTTUI_API void tui_close(void); |
| 85 | |
| 86 | /* 1 once Ctrl-C, Ctrl-Q or `tui_quit` has asked the loop to stop — and before |
| 87 | * a session is open, so a loop written around it cannot spin forever. */ |
| 88 | JOLTTUI_API int tui_should_close(void); |
| 89 | JOLTTUI_API void tui_quit(void); |
| 90 | |
| 91 | /* |
| 92 | * One turn of the loop. |
| 93 | * |
| 94 | * `tui_tick` waits up to `timeout_ms` for input, handles everything that |
| 95 | * arrived — keys, clicks, the wheel, a resize — and answers how many things it |
| 96 | * handled, so a caller can skip a repaint when nothing did. `tui_frame` lays |
| 97 | * the tree out, paints it, and sends only the cells that changed. |
| 98 | * |
| 99 | * A headless session ticks to 0 and is fed with the calls below instead. |
| 100 | */ |
| 101 | JOLTTUI_API int tui_tick(int timeout_ms); |
| 102 | JOLTTUI_API void tui_frame(void); |
| 103 | |
| 104 | /* The screen, in cells. 0 before a session is open. */ |
| 105 | JOLTTUI_API int tui_screen_width(void); |
| 106 | JOLTTUI_API int tui_screen_height(void); |
| 107 | |
| 108 | /* |
| 109 | * One painted row as text, trailing blanks trimmed — what a test asserts on |
| 110 | * and what a bug report pastes. The returned pointer belongs to the library |
| 111 | * and is valid only until the next string-returning read on this thread. |
| 112 | */ |
| 113 | JOLTTUI_API const char *tui_screen_line(int y); |
| 114 | |
| 115 | /* |
| 116 | * Input by hand. |
| 117 | * |
| 118 | * `tui_feed_key` takes a key by name, as the terminal's own keys are named: |
| 119 | * "a", "space", "enter", "tab", "shift+tab", "esc", "up", "page-down", |
| 120 | * "ctrl+u", "alt+f", "f5". It answers 1 when the backend acted on it and 0 |
| 121 | * when it went out as a `key` event instead. `tui_feed_click` and |
| 122 | * `tui_feed_wheel` do the same for the mouse; the wheel's `by` is in rows and |
| 123 | * negative is up. |
| 124 | * |
| 125 | * These are the same entry points a real terminal's input arrives through, so |
| 126 | * a test drives the UI exactly as a person does. |
| 127 | */ |
| 128 | JOLTTUI_API int tui_feed_key(const char *name); |
| 129 | JOLTTUI_API int tui_feed_click(int x, int y); |
| 130 | JOLTTUI_API int tui_feed_wheel(int x, int y, int by); |
| 131 | |
| 132 | /* The focused node, 0 for none. Focus follows the ring — Tab and Shift-Tab |
| 133 | * walk it in paint order, `autofocus` claims it on the first frame, and a |
| 134 | * widget that is unmounted or turned insensitive gives it up. */ |
| 135 | JOLTTUI_API int tui_focus(void); |
| 136 | |
| 137 | /* The window node, created with the session. Mount everything under it. */ |
| 138 | JOLTTUI_API int tui_tree_root(void); |
| 139 | |
| 140 | JOLTTUI_API int tui_node_new(const char *tag); |
| 141 | JOLTTUI_API void tui_node_free(int node); |
| 142 | JOLTTUI_API int tui_node_exists(int node); |
| 143 | |
| 144 | JOLTTUI_API void tui_node_set_str(int node, const char *key, const char *value); |
| 145 | JOLTTUI_API void tui_node_set_num(int node, const char *key, double value); |
| 146 | JOLTTUI_API void tui_node_set_bool(int node, const char *key, int value); |
| 147 | JOLTTUI_API void tui_node_clear_props(int node); |
| 148 | |
| 149 | /* |
| 150 | * Reads answer the empty string / 0 for a prop that is unset or of another |
| 151 | * type. The returned pointer belongs to the library and is valid only until |
| 152 | * the next string-returning call of its family on this thread — props, tags |
| 153 | * and screen lines are one family, dumps another, and an event's name and its |
| 154 | * text are each their own, so an event can be read whole. |
| 155 | */ |
| 156 | JOLTTUI_API const char *tui_node_get_str(int node, const char *key); |
| 157 | JOLTTUI_API double tui_node_get_num(int node, const char *key); |
| 158 | JOLTTUI_API int tui_node_get_bool(int node, const char *key); |
| 159 | |
| 160 | /* Reading the structure back. `tui_node_tag` answers the canonical tag name |
| 161 | * ("box" for both hbox and vbox); `tui_node_child_at` answers 0 past the end. */ |
| 162 | JOLTTUI_API const char *tui_node_tag(int node); |
| 163 | JOLTTUI_API int tui_node_parent(int node); |
| 164 | JOLTTUI_API int tui_node_child_count(int node); |
| 165 | JOLTTUI_API int tui_node_child_at(int node, int index); |
| 166 | |
| 167 | /* |
| 168 | * The subtree at `node` as pretty-printed hiccup — `[:tag {props} children...]`, |
| 169 | * one node to a line. `node` 0 means the root, so `tui_tree_dump(0)` is the |
| 170 | * whole window. It answers what the tree *is*, read back from the arena, |
| 171 | * rather than what a caller meant to build. Props are sorted, so two dumps of |
| 172 | * the same tree compare as text; handlers are absent because they never |
| 173 | * crossed this boundary. |
| 174 | */ |
| 175 | JOLTTUI_API const char *tui_tree_dump(int node); |
| 176 | |
| 177 | JOLTTUI_API int tui_node_append(int parent, int child); |
| 178 | /* Unparents AND frees `child` with everything under it. */ |
| 179 | JOLTTUI_API void tui_node_remove(int parent, int child); |
| 180 | /* Moves `child` after `sibling`; `sibling` 0 means the first position. */ |
| 181 | JOLTTUI_API int tui_node_insert_after(int parent, int child, int sibling); |
| 182 | /* Puts `new_child` where `old_child` was, and frees `old_child`. */ |
| 183 | JOLTTUI_API int tui_node_replace(int parent, int old_child, int new_child); |
| 184 | |
| 185 | /* |
| 186 | * Drain interactions. `tui_tree_poll_event` dequeues one and answers 1 while |
| 187 | * there was one; the accessors describe whichever was dequeued last. |
| 188 | * |
| 189 | * Event names are glimmer's handler props without the `on-`: |
| 190 | * |
| 191 | * click a button was pressed no payload |
| 192 | * toggled a checkbutton changed num is the new state |
| 193 | * change an entry's text changed text is the new text |
| 194 | * activate Enter in an entry or a list text is the text or the row |
| 195 | * select a list's cursor moved text is the row, num its index |
| 196 | * scroll a viewport moved num is the new offset |
| 197 | * close Esc in an overlay no payload |
| 198 | * key a key nothing here wanted text is the key's name |
| 199 | * |
| 200 | * A widget does not own its value: `toggled`, `change`, `select` and `scroll` |
| 201 | * write the new state back into the node's props as well, so a caller that |
| 202 | * ignores the event still sees a working control, and the next prop write is |
| 203 | * what settles it. |
| 204 | * |
| 205 | * A `key` event is reported on the focused node, or on the window when nothing |
| 206 | * has focus. Bubbling it to a container's `:on-key` is the caller's to do: it |
| 207 | * holds the handlers and knows the tree. |
| 208 | */ |
| 209 | JOLTTUI_API int tui_tree_poll_event(void); |
| 210 | JOLTTUI_API int tui_tree_event_node(void); |
| 211 | JOLTTUI_API const char *tui_tree_event_name(void); |
| 212 | JOLTTUI_API const char *tui_tree_event_text(void); |
| 213 | JOLTTUI_API double tui_tree_event_num(void); |
| 214 | |
| 215 | #ifdef __cplusplus |
| 216 | } |
| 217 | #endif |
| 218 | |
| 219 | #endif /* JOLTTUI_H */ |