A terminal (TUI) rendering target for Fulcro.
Fulcro normally renders through React to the browser DOM. This namespace provides an alternative
rendering target that lays out a tree of terminal-native nodes (boxes, text, inputs, buttons) and
paints them into a character cell buffer for display via a JLine terminal (see
com.fulcrologic.fulcro.tui.terminal).
The UI is described as a tree of nodes. A node is a plain map of the form:
{:com.fulcrologic.fulcro.tui/tag :vbox ; one of `tags`
:com.fulcrologic.fulcro.tui/attrs {...} ; layout/style/event attributes
:com.fulcrologic.fulcro.tui/children [...]} ; child nodes, strings, and numbers
Use the element generators (vbox, hbox, box, text, input, button, line, viewport)
to build the tree rather than constructing the maps by hand.
A terminal (TUI) rendering target for Fulcro.
Fulcro normally renders through React to the browser DOM. This namespace provides an alternative
rendering target that lays out a tree of terminal-native nodes (boxes, text, inputs, buttons) and
paints them into a character cell buffer for display via a JLine terminal (see
`com.fulcrologic.fulcro.tui.terminal`).
The UI is described as a tree of *nodes*. A node is a plain map of the form:
```
{:com.fulcrologic.fulcro.tui/tag :vbox ; one of `tags`
:com.fulcrologic.fulcro.tui/attrs {...} ; layout/style/event attributes
:com.fulcrologic.fulcro.tui/children [...]} ; child nodes, strings, and numbers
```
Use the element generators (`vbox`, `hbox`, `box`, `text`, `input`, `button`, `line`, `viewport`)
to build the tree rather than constructing the maps by hand.The Fulcro app the walker is rendering for. Bound by render-tree so that
factory can stamp the app onto the instances it creates. Defaults to nil.
The Fulcro app the walker is rendering for. Bound by `render-tree` so that `factory` can stamp the app onto the instances it creates. Defaults to `nil`.
The :id of the currently focused node, bound by the walker/driver during a
render so that render code can call focused?. Defaults to nil.
The `:id` of the currently focused node, bound by the walker/driver during a render so that render code can call `focused?`. Defaults to `nil`.
The component instance currently being rendered (the parent of the children a
factory is about to create). Bound by the walker as it descends. Defaults to nil.
The component instance currently being rendered (the parent of the children a `factory` is about to create). Bound by the walker as it descends. Defaults to `nil`.
(activate! node)Invokes the :on-activate handler attribute of TUI node (if present) with no
arguments and returns its result. Returns nil when there is no handler. Used to
simulate activating a button/control.
Invokes the `:on-activate` handler attribute of TUI `node` (if present) with no arguments and returns its result. Returns `nil` when there is no handler. Used to simulate activating a button/control.
(active-tree node-tree)Returns the subtree that should receive focus and keyboard input. When an overlay is open it is
the topmost (last in document order) OPEN :modal node — trapping focus to the overlay; otherwise it
is node-tree itself. In either case any other (closed or nested) :modal nodes are stripped, so a
closed modal's contents are never focusable and only the active layer's controls take keys.
Returns the subtree that should receive focus and keyboard input. When an overlay is open it is the topmost (last in document order) OPEN `:modal` node — trapping focus to the overlay; otherwise it is `node-tree` itself. In either case any other (closed or nested) `:modal` nodes are stripped, so a closed modal's contents are never focusable and only the active layer's controls take keys.
(align-offset align extent size)Returns the offset of a child of size within a track of extent for the given align keyword.
:center/:middle center it, :end/:right/:bottom push it to the far edge, anything else
(default) keeps it at the near edge.
Returns the offset of a child of `size` within a track of `extent` for the given `align` keyword. `:center`/`:middle` center it, `:end`/`:right`/`:bottom` push it to the far edge, anything else (default) keeps it at the near edge.
(all-scroll app-or-state)Returns the whole viewport-scroll map {viewport-id {:x :y}} from app-or-state (the state-map
key ::scroll), or {} when none is recorded. Accepts a Fulcro app (reads its state-atom) or a
raw state-map.
Returns the whole viewport-scroll map `{viewport-id {:x :y}}` from `app-or-state` (the state-map
key `::scroll`), or `{}` when none is recorded. Accepts a Fulcro app (reads its state-atom) or a
raw state-map.(apply-edit value caret key-event)Returns {:value :caret} after applying the editing key-event to controlled
input text value with the cursor at caret (a 0-based index, clamped into
[0, (count value)]). Handles:
:char is a 1-char string) — insert at the caret, caret += 1:backspace — delete the char left of the caret, caret -= 1:delete — delete the char right of the caret, caret unchanged:left/:right — move the caret one cell (clamped):home/:end — move the caret to the start/endAny other key leaves value and (clamped) caret unchanged. Pure.
Returns `{:value :caret}` after applying the editing `key-event` to controlled
input text `value` with the cursor at `caret` (a 0-based index, clamped into
`[0, (count value)]`). Handles:
* a printable key (`:char` is a 1-char string) — insert at the caret, caret += 1
* `:backspace` — delete the char left of the caret, caret -= 1
* `:delete` — delete the char right of the caret, caret unchanged
* `:left`/`:right` — move the caret one cell (clamped)
* `:home`/`:end` — move the caret to the start/end
Any other key leaves `value` and (clamped) `caret` unchanged. Pure.(apply-edit-multiline value caret width key-event)Returns {:value :caret} after applying editing key-event to multiline input text value (a
string that may contain \n) with the cursor at caret, given the input's display width (the
wrap width, used for visual-line navigation). caret is clamped into [0, (count value)]. Differs
from apply-edit for a single-line input in three ways:
:enter inserts a newline (\n) at the caret and advances it (it does NOT submit).:up/:down move the caret one VISUAL line (per the wrapped layout at width), preserving the
current visual column as the target column (clamped to the destination row's length).:home/:end move to the start/end of the current VISUAL line (not the whole value).Printable insertion, :backspace, :delete, and :left/:right behave as in apply-edit;
because \n is an ordinary character of value, left/right and backspace/delete cross line
boundaries naturally. Any other key leaves value and (clamped) caret unchanged. Pure.
Returns `{:value :caret}` after applying editing `key-event` to multiline input text `value` (a
string that may contain `\n`) with the cursor at `caret`, given the input's display `width` (the
wrap width, used for visual-line navigation). `caret` is clamped into `[0, (count value)]`. Differs
from `apply-edit` for a single-line input in three ways:
* `:enter` inserts a newline (`\n`) at the caret and advances it (it does NOT submit).
* `:up`/`:down` move the caret one VISUAL line (per the wrapped layout at `width`), preserving the
current visual column as the target column (clamped to the destination row's length).
* `:home`/`:end` move to the start/end of the current VISUAL line (not the whole value).
Printable insertion, `:backspace`, `:delete`, and `:left`/`:right` behave as in `apply-edit`;
because `\n` is an ordinary character of `value`, left/right and backspace/delete cross line
boundaries naturally. Any other key leaves `value` and (clamped) `caret` unchanged. Pure.(apply-focus-change! app node-tree old-id new-id)Fires focus-transition handlers when focus moves from old-id to new-id within
node-tree. When the ids differ, the node with old-id has its :on-lost-focus
handler invoked with old-id, and the node with new-id has its :on-focus
handler invoked with new-id (both found via find-by-id, matching the
interaction-util handler-arg convention). Returns app. A no-op when the ids are
equal.
Fires focus-transition handlers when focus moves from `old-id` to `new-id` within `node-tree`. When the ids differ, the node with `old-id` has its `:on-lost-focus` handler invoked with `old-id`, and the node with `new-id` has its `:on-focus` handler invoked with `new-id` (both found via `find-by-id`, matching the interaction-util handler-arg convention). Returns `app`. A no-op when the ids are equal.
(as-node c)Returns c unchanged if it is a node, otherwise wraps it as a :text node. Lets containers hold
bare strings/numbers, which become text lines.
Returns `c` unchanged if it is a node, otherwise wraps it as a `:text` node. Lets containers hold bare strings/numbers, which become text lines.
(blit dest src src-x src-y w h dest-x dest-y clip)Returns dest buffer with the [src-x src-y w h] window of the src buffer copied so that the
window's top-left lands at dest-x,dest-y in dest. Each copied cell is written via put-cell,
so writes are clipped to dest's bounds; in addition, copies are clipped to clip (a dest-space
rect). Cells read from outside src's bounds are skipped. Pure.
Returns `dest` buffer with the `[src-x src-y w h]` window of the `src` buffer copied so that the window's top-left lands at `dest-x`,`dest-y` in `dest`. Each copied cell is written via `put-cell`, so writes are clipped to `dest`'s bounds; in addition, copies are clipped to `clip` (a dest-space rect). Cells read from outside `src`'s bounds are skipped. Pure.
(box & args)Returns a :box node: a single styling/padding/border container around children. An optional
leading attribute map sets layout/style attributes.
Returns a `:box` node: a single styling/padding/border container around `children`. An optional leading attribute map sets layout/style attributes.
(button & args)Returns a :button node rendering children as its label. An optional leading attribute map sets
:id, :on-activate, and style attributes.
Returns a `:button` node rendering `children` as its label. An optional leading attribute map sets `:id`, `:on-activate`, and style attributes.
(caret->rowcol value width caret)Returns the visual [row col] of caret index caret within the layout of value wrapped to
width columns. The caret is clamped into [0, (count value)]. At a soft-wrap boundary the caret
resolves to the END of the earlier row (see the wrap-boundary convention above). Pure.
Returns the visual `[row col]` of caret index `caret` within the layout of `value` wrapped to `width` columns. The caret is clamped into `[0, (count value)]`. At a soft-wrap boundary the caret resolves to the END of the earlier row (see the wrap-boundary convention above). Pure.
(child-size c)Returns the intrinsic {:w :h} of a child c, which may be a node, string, or number.
Returns the intrinsic `{:w :h}` of a child `c`, which may be a node, string, or number.
(clamp-scroll scroll virtual-size view-size)Returns the scroll offset {:x :y} clamped so the visible window stays within the virtual
content. Given the virtual-size {:w :h} of the laid-out child and the view-size {:w :h}
of the viewport's content area, each axis is clamped to 0..(max 0 (- virtual view)). When the
virtual content is no larger than the view, that axis clamps to 0. Pure.
Returns the `scroll` offset `{:x :y}` clamped so the visible window stays within the virtual
content. Given the `virtual-size` `{:w :h}` of the laid-out child and the `view-size` `{:w :h}`
of the viewport's content area, each axis is clamped to `0..(max 0 (- virtual view))`. When the
virtual content is no larger than the view, that axis clamps to 0. Pure.(clear-caret! app id)Removes any stored caret for input id from app's runtime caret store
(::carets). Returns app.
Removes any stored caret for input `id` from `app`'s runtime caret store (`::carets`). Returns `app`.
(code-point-width cp)Returns the terminal column width (0, 1, or 2) of Unicode code point cp, using a compact
approximation of POSIX wcwidth: 0 for NUL, control characters, and combining/zero-width marks;
2 for East Asian wide/fullwidth characters and common emoji; 1 otherwise.
Returns the terminal column width (0, 1, or 2) of Unicode code point `cp`, using a compact approximation of POSIX `wcwidth`: 0 for NUL, control characters, and combining/zero-width marks; 2 for East Asian wide/fullwidth characters and common emoji; 1 otherwise.
(collect-overlays node-tree)Returns a vector of the OPEN :modal nodes in node-tree (those whose :open? attr is truthy), in
document (pre-order, depth-first) order. Closed modals are omitted. The last element is the topmost
overlay (a later sibling/descendant draws over earlier ones).
Returns a vector of the OPEN `:modal` nodes in `node-tree` (those whose `:open?` attr is truthy), in document (pre-order, depth-first) order. Closed modals are omitted. The last element is the topmost overlay (a later sibling/descendant draws over earlier ones).
(computed-factory class)Returns a factory like factory, but whose returned function accepts an optional
trailing computed map: (f props) or (f props computed-map). The computed-map
is attached to props via rc/computed so that rc/get-computed can retrieve it
inside the component's render.
Returns a factory like `factory`, but whose returned function accepts an optional trailing computed map: `(f props)` or `(f props computed-map)`. The `computed-map` is attached to `props` via `rc/computed` so that `rc/get-computed` can retrieve it inside the component's render.
(content-rect node)Returns the inner content rectangle of placed node: its ::rect shrunk by the edge insets implied
by its attrs (:border?/:padding).
Returns the inner content rectangle of placed `node`: its `::rect` shrunk by the edge insets implied by its attrs (`:border?`/`:padding`).
(content-size node)Returns the content-driven intrinsic {:w :h} of node for its tag, before the node's own fixed
:width/:height or :min-* overrides are applied. Container sizes include edge insets.
Returns the content-driven intrinsic `{:w :h}` of `node` for its tag, before the node's own fixed
`:width`/`:height` or `:min-*` overrides are applied. Container sizes include edge insets.(content-view-size node)Returns the content-area size {:w :h} of a placed node: its ::rect size shrunk by the edge
insets implied by its attrs (:border?/:padding). This is the visible window size of a viewport.
Returns the content-area size `{:w :h}` of a placed `node`: its `::rect` size shrunk by the edge
insets implied by its attrs (`:border?`/`:padding`). This is the visible window size of a viewport.(continuation-cell? prev cell)Returns true if cell is a wide-char continuation marker: a space whose preceding cell prev holds
a 2-column character.
Returns true if `cell` is a wide-char continuation marker: a space whose preceding cell `prev` holds a 2-column character.
(current-focus app-or-state)Returns the currently focused node :id from app-or-state. Accepts either a
Fulcro app (reads its state-atom) or a raw state-map. Returns nil when nothing
is focused.
Returns the currently focused node `:id` from `app-or-state`. Accepts either a Fulcro app (reads its state-atom) or a raw state-map. Returns `nil` when nothing is focused.
(current-node-tree app)Returns the current pure TUI node tree for app, computed from its state. The root
class is read from the runtime atom
(:com.fulcrologic.fulcro.application/root-class); its props are obtained via
fdn/db->tree of the root query over the state-map. *current-focus* is bound to
the current ::focus while rendering so render code can call focused?.
Returns the current pure TUI node tree for `app`, computed from its state. The root class is read from the runtime atom (`:com.fulcrologic.fulcro.application/root-class`); its props are obtained via `fdn/db->tree` of the root query over the state-map. `*current-focus*` is bound to the current `::focus` while rendering so render code can call `focused?`.
(defsc & args)Define a React-free TUI stateful component. Mirrors com.fulcrologic.fulcro.components/defsc's
argument list and option handling (query/ident/initial-state validation and codegen via
macro-support) but emits NO React: it defs a faux Fulcro class whose :render is a plain
(fn [this] ...) that destructures props/computed/extra from the instance via the raw
component API.
(tui/defsc Item [this {:keys [item/id item/label]} {:keys [on-select]}]
{:query [:item/id :item/label]
:ident :item/id}
(tui/button {:id (str id) :on-activate on-select} label))
The class is usable with rc/component-options, rc/get-query, rc/get-ident, and the
factory/render-tree walker in this namespace.
Define a React-free TUI stateful component. Mirrors `com.fulcrologic.fulcro.components/defsc`'s
argument list and option handling (query/ident/initial-state validation and codegen via
`macro-support`) but emits NO React: it `def`s a faux Fulcro class whose `:render` is a plain
`(fn [this] ...)` that destructures `props`/`computed`/`extra` from the instance via the raw
component API.
```
(tui/defsc Item [this {:keys [item/id item/label]} {:keys [on-select]}]
{:query [:item/id :item/label]
:ident :item/id}
(tui/button {:id (str id) :on-activate on-select} label))
```
The class is usable with `rc/component-options`, `rc/get-query`, `rc/get-ident`, and the
`factory`/`render-tree` walker in this namespace.(diff prev next)Returns a vector of run ops describing how to turn buffer prev into buffer next. Each op is
{:row r :col c :sgr <codes-vector> :text "..."}; horizontally adjacent changed cells sharing a
style are coalesced into a single op and unchanged cells produce no ops. When prev is nil or has
different dimensions than next, a full repaint is emitted (every non-default cell, still
coalesced per row).
Returns a vector of run ops describing how to turn buffer `prev` into buffer `next`. Each op is
`{:row r :col c :sgr <codes-vector> :text "..."}`; horizontally adjacent changed cells sharing a
style are coalesced into a single op and unchanged cells produce no ops. When `prev` is `nil` or has
different dimensions than `next`, a full repaint is emitted (every non-default cell, still
coalesced per row).(distribute-main extent cross-extent children main-attr)Returns a vector of resolved main-axis sizes (one per child) that partition extent. Children
with a :grow weight share the space left over after fixed/fraction/content-sized children;
leftover is split by weight with any rounding remainder given to the last grow child. cross-extent
is the cross-axis track size each child will fill, used to resolve height-for-width wrapping text.
Returns a vector of resolved main-axis sizes (one per child) that partition `extent`. Children with a `:grow` weight share the space left over after fixed/fraction/content-sized children; leftover is split by weight with any rounding remainder given to the last grow child. `cross-extent` is the cross-axis track size each child will fill, used to resolve height-for-width wrapping text.
(draw-border buffer rect style clip)Returns buffer with a single-cell box-drawing border (┌┐└┘ corners, ─ top/bottom, │ sides)
drawn around the outer rect, clipped to clip. Boxes smaller than 2x2 are not drawn.
Returns `buffer` with a single-cell box-drawing border (`┌┐└┘` corners, `─` top/bottom, `│` sides) drawn around the outer `rect`, clipped to `clip`. Boxes smaller than 2x2 are not drawn.
(edge-insets attrs)Returns {:l :r :t :b} per-edge insets implied by attrs. :border? adds 1 to every edge and
:padding (a non-negative integer) adds its value to every edge.
Returns `{:l :r :t :b}` per-edge insets implied by `attrs`. `:border?` adds 1 to every edge and
`:padding` (a non-negative integer) adds its value to every edge.(element tag args)Returns a TUI node with the given tag built from args. If the first of args is a map it is
used as the node's attributes (otherwise attributes default to {}); the remaining args become
the node's children (flattened, with nils removed). Prefer the named generators (vbox, etc.)
over calling this directly.
Returns a TUI node with the given `tag` built from `args`. If the first of `args` is a map it is
used as the node's attributes (otherwise attributes default to `{}`); the remaining `args` become
the node's children (flattened, with `nil`s removed). Prefer the named generators (`vbox`, etc.)
over calling this directly.(factory class)Returns an element-factory (fn [props & children] instance-map) for the TUI
component class. Calling the returned factory builds a React-free component
instance map carrying the given props (under :fulcro$value), the dynamically
bound *app*, and (when props carries a :react-key) a :fulcro$reactKey. The
resulting instance works with rc/props, rc/get-computed, rc/get-ident, and
rc/component-options, and is recognized by rc/component-instance?. The walker
(render-tree) consumes such instances.
Returns an element-factory `(fn [props & children] instance-map)` for the TUI component `class`. Calling the returned factory builds a React-free component *instance* map carrying the given `props` (under `:fulcro$value`), the dynamically bound `*app*`, and (when `props` carries a `:react-key`) a `:fulcro$reactKey`. The resulting instance works with `rc/props`, `rc/get-computed`, `rc/get-ident`, and `rc/component-options`, and is recognized by `rc/component-instance?`. The walker (`render-tree`) consumes such instances.
(fill-rect buffer rect style clip)Returns buffer with every cell of rect set to a space in style, clipped to clip.
Returns `buffer` with every cell of `rect` set to a space in `style`, clipped to `clip`.
(find-by-id node id)Returns the first node (depth-first, pre-order) within node whose :id attribute
equals id, or nil if none is found. Strings/numbers and non-node children are
skipped. node may itself be the match.
Returns the first node (depth-first, pre-order) within `node` whose `:id` attribute equals `id`, or `nil` if none is found. Strings/numbers and non-node children are skipped. `node` may itself be the match.
(flatten-children children)Returns a vector of children with nested sequential collections flattened and nils removed.
Nodes, strings, and numbers are retained as-is, in order. This lets callers splice seqs of
children (e.g. from map) directly into an element's argument list.
Returns a vector of `children` with nested sequential collections flattened and `nil`s removed. Nodes, strings, and numbers are retained as-is, in order. This lets callers splice seqs of children (e.g. from `map`) directly into an element's argument list.
(focus! app id)Sets the focused node :id to id in the app's state-map at ::focus (the
single source of truth) via a direct swap!. If the app declares a renderer
(:com.fulcrologic.fulcro.application/render-root!/schedule-render!) it triggers
a render so the change is reflected on screen, but it remains usable without a
terminal. Returns the app.
Sets the focused node `:id` to `id` in the app's state-map at `::focus` (the single source of truth) via a direct `swap!`. If the app declares a renderer (`:com.fulcrologic.fulcro.application/render-root!`/`schedule-render!`) it triggers a render so the change is reflected on screen, but it remains usable without a terminal. Returns the app.
(focus-order focusables)Returns focusables sorted into focus-traversal order: by :priority
descending, then by :dfs (document order) ascending.
Returns `focusables` sorted into focus-traversal order: by `:priority` descending, then by `:dfs` (document order) ascending.
(focus-viewport-context placed-tree focus-id)Returns {:viewport <placed-viewport-node> :virtual-rect <rect>} describing the innermost placed
:viewport in placed-tree whose virtual content subtree (::viewport-content) contains the node
with :id focus-id, along with that focused node's VIRTUAL ::rect (0-based within the viewport).
Returns nil when focus-id is not inside any viewport. When viewports are nested, the innermost
enclosing viewport is returned.
Returns `{:viewport <placed-viewport-node> :virtual-rect <rect>}` describing the innermost placed
`:viewport` in `placed-tree` whose virtual content subtree (`::viewport-content`) contains the node
with `:id` `focus-id`, along with that focused node's VIRTUAL `::rect` (0-based within the viewport).
Returns `nil` when `focus-id` is not inside any viewport. When viewports are nested, the innermost
enclosing viewport is returned.(focusable-node? node)Returns true if node can receive focus. A node is focusable when it has an
:id attribute AND either: its attrs set :focusable? true, its tag is :input
or :button, or it carries any of the focus-relevant handler attributes
(:on-key, :on-activate, :on-change, :on-focus, :on-lost-focus).
Returns true if `node` can receive focus. A node is focusable when it has an `:id` attribute AND either: its attrs set `:focusable? true`, its tag is `:input` or `:button`, or it carries any of the focus-relevant handler attributes (`:on-key`, `:on-activate`, `:on-change`, `:on-focus`, `:on-lost-focus`).
(focusables node-tree)Returns an ordered vector of focusable descriptors for every focusable node in
node-tree, in document (pre-order, depth-first) order. Each descriptor is a map
{:id :priority :dfs :node} where :priority is the node's :priority attr
(default 0) and :dfs is the node's pre-order index among all visited nodes.
Returns an ordered vector of focusable descriptors for every focusable node in
`node-tree`, in document (pre-order, depth-first) order. Each descriptor is a map
`{:id :priority :dfs :node}` where `:priority` is the node's `:priority` attr
(default 0) and `:dfs` is the node's pre-order index among all visited nodes.(focused? id)Returns true if id is the id of the node that currently has focus (per the
dynamically bound *current-focus*).
Returns true if `id` is the id of the node that currently has focus (per the dynamically bound `*current-focus*`).
(frame->ansi prev next opts)Returns the ANSI string to render the transition from buffer prev to buffer next, by diffing
them and serializing the ops. When :sync? in opts is true the whole sequence is wrapped in DEC
private mode 2026 ("\u001b[?2026h" … "\u001b[?2026l") so the terminal applies it as a
single atomic frame; otherwise no wrapper is added. When :clear? in opts is true a clear-screen
plus cursor-home is prepended to the body — used on a resize repaint, where a plain full repaint only
writes non-blank cells and would otherwise leave stale content from the previous size on screen.
Returns the ANSI string to render the transition from buffer `prev` to buffer `next`, by diffing them and serializing the ops. When `:sync?` in `opts` is true the whole sequence is wrapped in DEC private mode 2026 (`"\u001b[?2026h"` … `"\u001b[?2026l"`) so the terminal applies it as a single atomic frame; otherwise no wrapper is added. When `:clear?` in `opts` is true a clear-screen plus cursor-home is prepended to the body — used on a resize repaint, where a plain full repaint only writes non-blank cells and would otherwise leave stale content from the previous size on screen.
(get-caret app id default)Returns the caret index stored for input id in app's runtime caret store
(::carets), or default (the end of the value) when none is stored.
Returns the caret index stored for input `id` in `app`'s runtime caret store (`::carets`), or `default` (the end of the value) when none is stored.
(handle-input-key! app input-node key-event)Applies key key-event to focused input-node (an :input node) in app,
mutating value and caret per the controlled-input model. Reads the node's current
:value attr and caret (from the runtime store, or — when the node opts in with a
:caret attr — from that attr), computes the edit, then calls the node's :on-change
handler with the new value and caret (interaction-util convention). The new caret is
written to the runtime store UNLESS the node opted into caret-in-state (has a :caret
attr), in which case the store is left for the node to manage.
Single-line vs multiline (:multiline? true):
:enter key invokes the node's :on-submit handler with the current value
(it does not edit); other keys use apply-edit.apply-edit-multiline with the input's effective wrap width (from the
runtime ::input-widths store, falling back to the node's :width attr, then a large default
so a not-yet-painted input wraps only on hard newlines). :enter inserts a \n (it does NOT
submit), and :up/:down move the caret by one visual line.Returns app.
Applies key `key-event` to focused `input-node` (an `:input` node) in `app`,
mutating value and caret per the controlled-input model. Reads the node's current
`:value` attr and caret (from the runtime store, or — when the node opts in with a
`:caret` attr — from that attr), computes the edit, then calls the node's `:on-change`
handler with the new value and caret (interaction-util convention). The new caret is
written to the runtime store UNLESS the node opted into caret-in-state (has a `:caret`
attr), in which case the store is left for the node to manage.
Single-line vs multiline (`:multiline? true`):
* Single-line: an `:enter` key invokes the node's `:on-submit` handler with the current value
(it does not edit); other keys use `apply-edit`.
* Multiline: editing uses `apply-edit-multiline` with the input's effective wrap width (from the
runtime `::input-widths` store, falling back to the node's `:width` attr, then a large default
so a not-yet-painted input wraps only on hard newlines). `:enter` inserts a `\n` (it does NOT
submit), and `:up`/`:down` move the caret by one visual line.
Returns `app`.(hard-break s width)Returns a vector of pieces of string s, each at most width display columns wide, splitting s
greedily at code-point boundaries (measured with code-point-width). Used to break an over-long
word that cannot fit on a single wrapped line. A piece never exceeds width columns, so a wide
(2-column) character is never split. Returns [] for the empty string.
Returns a vector of pieces of string `s`, each at most `width` display columns wide, splitting `s` greedily at code-point boundaries (measured with `code-point-width`). Used to break an over-long word that cannot fit on a single wrapped line. A piece never exceeds `width` columns, so a wide (2-column) character is never split. Returns `[]` for the empty string.
(hbox & args)Returns an :hbox node that stacks children horizontally. An optional leading attribute map
sets layout/style attributes for the container.
Returns an `:hbox` node that stacks `children` horizontally. An optional leading attribute map sets layout/style attributes for the container.
(in-bounds? buffer x y)Returns true if the 0-based column x and row y lie inside the rowsxcols buffer.
Returns true if the 0-based column `x` and row `y` lie inside the `rows`x`cols` `buffer`.
(in-clip? clip x y)Returns true if column x, row y is inside the clip rect {:x :y :w :h}.
Returns true if column `x`, row `y` is inside the `clip` rect `{:x :y :w :h}`.
(input attrs)Returns an :input leaf node from the given attrs map. Inputs are controlled: :value (and
optionally :caret) come from props, and :on-change receives proposed edits.
Returns an `:input` leaf node from the given `attrs` map. Inputs are controlled: `:value` (and optionally `:caret`) come from props, and `:on-change` receives proposed edits.
(input-width app id default)Returns the effective content width (in columns) recorded for multiline input id in app's
runtime (::input-widths, populated by the driver during render), or default when none is
recorded. The width is used by handle-input-key!/the driver to wrap the value and run visual
caret navigation.
Returns the effective content width (in columns) recorded for multiline input `id` in `app`'s runtime (`::input-widths`, populated by the driver during render), or `default` when none is recorded. The width is used by `handle-input-key!`/the driver to wrap the value and run visual caret navigation.
(intrinsic-size node)Returns the intrinsic {:w :h} of node in terminal cells. The content size (text length,
stacked/abutted children, plus any :border?/:padding insets) is overridden by the node's own
fixed :width/:height when those are integers, and then floored by :min-width/:min-height.
Fraction and :grow sizing are not resolved here — they require a concrete parent rectangle and
are handled by the place pass.
Returns the intrinsic `{:w :h}` of `node` in terminal cells. The content size (text length,
stacked/abutted children, plus any `:border?`/`:padding` insets) is overridden by the node's own
fixed `:width`/`:height` when those are integers, and then floored by `:min-width`/`:min-height`.
Fraction and `:grow` sizing are not resolved here — they require a concrete parent rectangle and
are handled by the place pass.(key-chord event)Returns the normalized chord for key event event, used to look up a handler in a
global keymap. With no modifiers, a special key returns its :key keyword (e.g.
:tab) and a printable key returns its 1-char string (e.g. "a"). When a
modifier is set, returns a vector of the active modifier keywords (in
[:ctrl :alt :shift] order) followed by the base key, e.g. [:ctrl "q"].
Returns the normalized chord for key event `event`, used to look up a handler in a global keymap. With no modifiers, a special key returns its `:key` keyword (e.g. `:tab`) and a printable key returns its 1-char string (e.g. `"a"`). When a modifier is set, returns a vector of the active modifier keywords (in `[:ctrl :alt :shift]` order) followed by the base key, e.g. `[:ctrl "q"]`.
(line & args)Returns a :line leaf node (a rule). An optional leading attribute map sets orientation/style.
Returns a `:line` leaf node (a rule). An optional leading attribute map sets orientation/style.
(main-content-size c cross-extent main-attr)Returns the content-driven main-axis size of child c being stacked along main-attr
(:height for a vbox, :width for an hbox) given the cross-extent (the cross-axis track size
the child will fill). For a wrapping :text stacked vertically this is its wrapped line count at
the available content width (cross-extent minus the node's own insets); otherwise it is the
child's ordinary intrinsic size on the main axis.
Returns the content-driven main-axis size of child `c` being stacked along `main-attr` (`:height` for a vbox, `:width` for an hbox) given the `cross-extent` (the cross-axis track size the child will fill). For a wrapping `:text` stacked vertically this is its wrapped line count at the available content width (`cross-extent` minus the node's own insets); otherwise it is the child's ordinary intrinsic size on the main axis.
(make-buffer rows cols)Returns a blank cell buffer of rows by cols. Every cell is a space with the default (empty)
style. Cells are stored row-major in a flat vector of length rows*cols.
Returns a blank cell buffer of `rows` by `cols`. Every cell is a space with the default (empty) style. Cells are stored row-major in a flat vector of length `rows`*`cols`.
(modal & args)Returns a :modal overlay node stacking children vertically inside a window that the driver
floats over the rest of the UI (compositing it on top and trapping focus/keyboard input to it).
An optional leading attribute map sets:
:id - (recommended) identity for focus/queries.:open? - the overlay is active (composited + focus-trapped) only when truthy. When falsy
the modal renders nothing.:width/:height - the window size in cells (a number, or a [:fraction f] of the screen);
defaults to the modal's intrinsic content size when omitted.:align - position on screen, one of :center (default), :start, :end.:border? - draw a border (default true).:title - a string painted onto the top border.:on-dismiss- a zero-arg handler invoked when Escape is pressed while the modal is active.Lifecycle (toggling :open?, recording any selection) is the application's responsibility via its
own state/mutations — this node owns no state.
Returns a `:modal` overlay node stacking `children` vertically inside a window that the driver
floats over the rest of the UI (compositing it on top and trapping focus/keyboard input to it).
An optional leading attribute map sets:
* `:id` - (recommended) identity for focus/queries.
* `:open?` - the overlay is active (composited + focus-trapped) only when truthy. When falsy
the modal renders nothing.
* `:width`/`:height` - the window size in cells (a number, or a `[:fraction f]` of the screen);
defaults to the modal's intrinsic content size when omitted.
* `:align` - position on screen, one of `:center` (default), `:start`, `:end`.
* `:border?` - draw a border (default `true`).
* `:title` - a string painted onto the top border.
* `:on-dismiss`- a zero-arg handler invoked when Escape is pressed while the modal is active.
Lifecycle (toggling `:open?`, recording any selection) is the application's responsibility via its
own state/mutations — this node owns no state.(modal-node? node)Returns true if node is a :modal overlay node.
Returns true if `node` is a `:modal` overlay node.
(multiline-input? node)Returns true if node is an :input node whose attrs request multiline editing (:multiline? true).
Returns true if `node` is an `:input` node whose attrs request multiline editing (`:multiline? true`).
(neighbor-id order current-id step)Returns the id of the focusable neighbor of current-id within ordered order,
stepping by step (+1 for next, -1 for previous), wrapping around. Returns the
first id (or nil) when current-id is absent/nil or order is empty.
Returns the id of the focusable neighbor of `current-id` within ordered `order`, stepping by `step` (+1 for next, -1 for previous), wrapping around. Returns the first id (or `nil`) when `current-id` is absent/nil or `order` is empty.
(next-focus order current-id)Returns the id of the next focusable after current-id in order (the result of
focus-order), wrapping to the first. When current-id is absent or nil,
returns the first id. Returns nil when order is empty.
Returns the id of the next focusable after `current-id` in `order` (the result of `focus-order`), wrapping to the first. When `current-id` is absent or `nil`, returns the first id. Returns `nil` when `order` is empty.
(node-attr node k)Returns the value of attribute k on TUI node (from its ::attrs), or nil if
node is not a node or lacks the attribute.
Returns the value of attribute `k` on TUI `node` (from its `::attrs`), or `nil` if `node` is not a node or lacks the attribute.
(node-path node-tree target-id)Returns a vector of nodes from node-tree's root down to (and including) the node
whose :id is target-id, in root→target order, or nil when not found.
Returns a vector of nodes from `node-tree`'s root down to (and including) the node whose `:id` is `target-id`, in root→target order, or `nil` when not found.
(node-style attrs)Returns the ::style map implied by a node's attrs. :highlight true sets :reverse?,
:color <kw> sets :fg, :bg <kw> sets :bg, and :bold true sets :bold?.
Returns the `::style` map implied by a node's `attrs`. `:highlight true` sets `:reverse?`, `:color <kw>` sets `:fg`, `:bg <kw>` sets `:bg`, and `:bold true` sets `:bold?`.
(node-text node)Returns the concatenated text of the node subtree: the string/number children of
the node and, recursively, of all of its descendant nodes, joined left-to-right.
Non-node, non-text children contribute nothing. A bare string/number returns its
own string.
Returns the concatenated text of the `node` subtree: the string/number children of the node and, recursively, of all of its descendant nodes, joined left-to-right. Non-node, non-text children contribute nothing. A bare string/number returns its own string.
(node? x)Returns true if x is a TUI node: a map carrying a legal ::tag.
Returns true if `x` is a TUI node: a map carrying a legal `::tag`.
(ops->ansi ops)Returns a single ANSI string that applies the run ops to a terminal. Each op emits a cursor move
"\u001b[<row+1>;<col+1>H", an SGR sequence only when the pen style changes from the previous
op, then the op's text. A trailing reset ("\u001b[0m") is emitted when the final pen style is
non-default.
Returns a single ANSI string that applies the run `ops` to a terminal. Each op emits a cursor move `"\u001b[<row+1>;<col+1>H"`, an SGR sequence only when the pen style changes from the previous op, then the op's text. A trailing reset (`"\u001b[0m"`) is emitted when the final pen style is non-default.
(overlay-window-rect modal-node screen-rect)Returns the on-screen ::rect at which open modal-node should be placed within screen-rect. The
window's :width/:height (a number or [:fraction f], defaulting to the modal's intrinsic size)
are resolved against the screen and clamped to it, and the window is positioned by the modal's
:align (:center by default) on both axes.
Returns the on-screen `::rect` at which open `modal-node` should be placed within `screen-rect`. The window's `:width`/`:height` (a number or `[:fraction f]`, defaulting to the modal's intrinsic size) are resolved against the screen and clamped to it, and the window is positioned by the modal's `:align` (`:center` by default) on both axes.
(paint buffer node clip)Returns buffer after painting placed node (and its descendants) into it, clipping all writes to
clip. Uses the painter's algorithm: a node paints itself (border/background or its text) and then
its children, so children draw over parents.
Returns `buffer` after painting placed `node` (and its descendants) into it, clipping all writes to `clip`. Uses the painter's algorithm: a node paints itself (border/background or its text) and then its children, so children draw over parents.
(paint-node buffer node clip)Returns buffer after painting placed node and its descendants, clipping every write to clip
(the intersection of ancestor rects). Containers draw their border/background then recurse; leaves
write their text/value/rule into their content rect.
Returns `buffer` after painting placed `node` and its descendants, clipping every write to `clip` (the intersection of ancestor rects). Containers draw their border/background then recurse; leaves write their text/value/rule into their content rect.
Map of palette color keywords to their foreground SGR base codes. Standard colors map to 30..37 and bright colors to 90..97. A background code is the foreground code plus 10.
Map of palette color keywords to their foreground SGR base codes. Standard colors map to 30..37 and bright colors to 90..97. A background code is the foreground code plus 10.
(picker {:keys [id open? title width height options on-select on-cancel]})Returns a :modal overlay presenting options as a scrollable list of selectable rows — a list
picker. Each row is a focusable button, so the focused row IS the highlighted choice: the focus
ring moves the highlight (Up/Down/Tab), the enclosing :viewport auto-scrolls to keep it visible
(PageUp/PageDown page it), Enter selects, and Escape cancels. State and selection handling are the
application's responsibility — this is a pure composition of existing nodes. opts:
:id - (required) base keyword for the modal and per-row focus ids.:open? - the picker is shown only when truthy.:title - optional title painted on the modal border.:width/:height - window size in cells (default 40 x 12).:options - a vector of {:value :label} maps (:value a keyword/string/symbol, :label
the displayed text).:on-select - one-arg handler called with a row's :value when its row is activated (Enter).:on-cancel - zero-arg handler called on Escape (wired to the modal's :on-dismiss).Returns a `:modal` overlay presenting `options` as a scrollable list of selectable rows — a list
picker. Each row is a focusable button, so the focused row IS the highlighted choice: the focus
ring moves the highlight (Up/Down/Tab), the enclosing `:viewport` auto-scrolls to keep it visible
(PageUp/PageDown page it), Enter selects, and Escape cancels. State and selection handling are the
application's responsibility — this is a pure composition of existing nodes. `opts`:
* `:id` - (required) base keyword for the modal and per-row focus ids.
* `:open?` - the picker is shown only when truthy.
* `:title` - optional title painted on the modal border.
* `:width`/`:height` - window size in cells (default 40 x 12).
* `:options` - a vector of `{:value :label}` maps (`:value` a keyword/string/symbol, `:label`
the displayed text).
* `:on-select` - one-arg handler called with a row's `:value` when its row is activated (Enter).
* `:on-cancel` - zero-arg handler called on Escape (wired to the modal's `:on-dismiss`).(place node rect)Returns node laid out within the outer rect {:x :y :w :h}. The node is annotated with its
::rect, and its children are replaced with placed children (each carrying its own ::rect).
:vbox/:hbox partition their content area among children (honoring fixed/:half/:fraction/
:grow/content sizing and :align); :box fills its content area with each child; leaves keep
their string/number children for the paint pass. :border?/:padding inset the content area.
A :viewport is special: it has a bounded outer ::rect (sized like any box), but its single
child is laid out at the child's NATURAL height (and the viewport's content width) into a VIRTUAL
rect {:x 0 :y 0 :w content-w :h (max content-h natural-h)} — i.e. in 0-based virtual coordinates
rather than absolute screen coordinates. The placed virtual child subtree is stored under
::viewport-content, the virtual content size under ::virtual-size {:w :h}, and a default
::scroll {:x 0 :y 0} is attached (the driver later injects the real scroll). The viewport's
::children are also placed (in virtual coords) so generic walkers still see them.
Returns `node` laid out within the outer `rect` `{:x :y :w :h}`. The node is annotated with its
`::rect`, and its children are replaced with placed children (each carrying its own `::rect`).
`:vbox`/`:hbox` partition their content area among children (honoring fixed/`:half`/`:fraction`/
`:grow`/content sizing and `:align`); `:box` fills its content area with each child; leaves keep
their string/number children for the paint pass. `:border?`/`:padding` inset the content area.
A `:viewport` is special: it has a bounded outer `::rect` (sized like any box), but its single
child is laid out at the child's NATURAL height (and the viewport's content width) into a VIRTUAL
rect `{:x 0 :y 0 :w content-w :h (max content-h natural-h)}` — i.e. in 0-based virtual coordinates
rather than absolute screen coordinates. The placed virtual child subtree is stored under
`::viewport-content`, the virtual content size under `::virtual-size {:w :h}`, and a default
`::scroll {:x 0 :y 0}` is attached (the driver later injects the real scroll). The viewport's
`::children` are also placed (in virtual coords) so generic walkers still see them.(place-stack axis content children)Places children within the content rectangle along axis (:v stacks vertically, :h
horizontally). Main-axis sizes are distributed by distribute-main; on the cross axis each child
fills the track unless it declares a size, in which case :align positions it. Returns the vector
of placed children.
Places `children` within the `content` rectangle along `axis` (`:v` stacks vertically, `:h` horizontally). Main-axis sizes are distributed by `distribute-main`; on the cross axis each child fills the track unless it declares a size, in which case `:align` positions it. Returns the vector of placed children.
(placed-viewports tree)Returns a vector of every placed :viewport node found (depth-first, pre-order) within the placed
tree, including any nested inside another viewport's virtual content subtree (::viewport-content).
Each returned node carries its ::rect, ::virtual-size, ::scroll, and ::viewport-content.
Returns a vector of every placed `:viewport` node found (depth-first, pre-order) within the placed `tree`, including any nested inside another viewport's virtual content subtree (`::viewport-content`). Each returned node carries its `::rect`, `::virtual-size`, `::scroll`, and `::viewport-content`.
(press! node k)Invokes the :on-key handler attribute of TUI node (if present) with the key
k and returns its result. Returns nil when there is no handler. Used to
simulate a key press delivered to a focused node.
Invokes the `:on-key` handler attribute of TUI `node` (if present) with the key `k` and returns its result. Returns `nil` when there is no handler. Used to simulate a key press delivered to a focused node.
(prev-focus order current-id)Returns the id of the previous focusable before current-id in order (the result
of focus-order), wrapping to the last. When current-id is absent or nil,
returns the first id. Returns nil when order is empty.
Returns the id of the previous focusable before `current-id` in `order` (the result of `focus-order`), wrapping to the last. When `current-id` is absent or `nil`, returns the first id. Returns `nil` when `order` is empty.
(process-key! app key-event)(process-key! app key-event global-keymap)Single-step driver: applies key event key-event to app, mutating focus and/or
state synchronously, and returns app.
Steps:
current-node-tree).:tab → focus the next-focus in the focus ring;:backtab or Shift-:tab → focus the prev-focus;:down → focus the next-focus; :up → focus the prev-focus
(arrow focus navigation, mirroring :tab/:backtab in the ring);
(all of the above then focus! + fire apply-focus-change!):input → controlled-input editing
(handle-input-key!);route-key (focused :on-key → bubble to ancestors → global keymap).Arrow-key capture rule: :up/:down move focus EXCEPT when the focused node is an
:input whose attrs set :multiline? true — such an input captures :up/:down,
routing them to handle-input-key! (reserved for future multiline caret movement)
rather than navigating focus. A single-line :input (no :multiline?) does NOT
capture them, so :up/:down navigate focus while it uses only left/right/home/end
for the caret.
global-keymap (optional) maps normalized chords (see key-chord) to handlers
invoked as (handler app key-event). Works on a synchronous raw app with NO
terminal attached.
TODO: viewport scrolling / follow-focus (keeping the focused node's placed rect visible) is intentionally NOT implemented here — it requires placed rects and the terminal size and is handled by a later task.
Single-step driver: applies key event `key-event` to `app`, mutating focus and/or
state synchronously, and returns `app`.
Steps:
1. Compute the current node tree from app state (`current-node-tree`).
2. Dispatch by precedence:
* `:tab` → focus the `next-focus` in the focus ring;
* `:backtab` or Shift-`:tab` → focus the `prev-focus`;
* `:down` → focus the `next-focus`; `:up` → focus the `prev-focus`
(arrow focus navigation, mirroring `:tab`/`:backtab` in the ring);
(all of the above then `focus!` + fire `apply-focus-change!`)
* else if the focused node is an `:input` → controlled-input editing
(`handle-input-key!`);
* else → `route-key` (focused `:on-key` → bubble to ancestors → global keymap).
3. After dispatch, re-resolve focus against the (possibly new) node tree: if the
focused id vanished, advance to the next focusable (else first; else nil),
firing transitions.
Arrow-key capture rule: `:up`/`:down` move focus EXCEPT when the focused node is an
`:input` whose attrs set `:multiline? true` — such an input *captures* `:up`/`:down`,
routing them to `handle-input-key!` (reserved for future multiline caret movement)
rather than navigating focus. A single-line `:input` (no `:multiline?`) does NOT
capture them, so `:up`/`:down` navigate focus while it uses only left/right/home/end
for the caret.
`global-keymap` (optional) maps normalized chords (see `key-chord`) to handlers
invoked as `(handler app key-event)`. Works on a synchronous raw app with NO
terminal attached.
TODO: viewport scrolling / follow-focus (keeping the focused node's placed rect
visible) is intentionally NOT implemented here — it requires placed rects and the
terminal size and is handled by a later task.(put-cell buffer x y ch style)Returns buffer with the cell at 0-based column x, row y set to character ch with style.
Writes that fall outside the buffer bounds are ignored (clipped), returning the buffer unchanged.
Returns `buffer` with the cell at 0-based column `x`, row `y` set to character `ch` with `style`. Writes that fall outside the buffer bounds are ignored (clipped), returning the buffer unchanged.
(put-str buffer x y s style clip-rect)Returns buffer with string s written starting at 0-based column x, row y, advancing by each
character's display width and styling each written cell with style. Writes are clipped to both
the clip-rect {:x :y :w :h} and the buffer bounds.
Wide-char cell scheme: a 2-column character is written into its starting cell and a continuation
marker (a space with the same style) is written into the next cell. The screen helper drops
these continuation cells so screen strings read naturally. A wide char is written only if BOTH its
cells pass the clip/bounds test, so a wide char is never split across a clip boundary.
Returns `buffer` with string `s` written starting at 0-based column `x`, row `y`, advancing by each
character's display width and styling each written cell with `style`. Writes are clipped to both
the `clip-rect` `{:x :y :w :h}` and the buffer bounds.
Wide-char cell scheme: a 2-column character is written into its starting cell and a continuation
marker (a space with the same `style`) is written into the next cell. The `screen` helper drops
these continuation cells so screen strings read naturally. A wide char is written only if BOTH its
cells pass the clip/bounds test, so a wide char is never split across a clip boundary.(rect-intersection a b)Returns the rectangle that is the intersection of rects a and b. When they do not overlap the
result has zero (or negative-clamped) width/height.
Returns the rectangle that is the intersection of rects `a` and `b`. When they do not overlap the result has zero (or negative-clamped) width/height.
(render-buffer placed-root rows cols)Returns a fresh rowsxcols buffer with the placed tree rooted at placed-root painted into it.
Painting walks the tree in order (painter's algorithm), clipping every write to the screen bounds.
Returns a fresh `rows`x`cols` buffer with the placed tree rooted at `placed-root` painted into it. Painting walks the tree in order (painter's algorithm), clipping every write to the screen bounds.
(render-children children)Returns a vector of the results of render-tree over each child in children,
dropping nil results and splicing vector results in as siblings.
Returns a vector of the results of `render-tree` over each child in `children`, dropping `nil` results and splicing vector results in as siblings.
(render-instance instance)Returns the node (or vector of nodes/strings/numbers) produced by calling the
:render of the component instance's class. Returns nil if the class has no
:render.
Returns the node (or vector of nodes/strings/numbers) produced by calling the `:render` of the component `instance`'s class. Returns `nil` if the class has no `:render`.
(render-root class props)(render-root class props app)Returns the pure TUI node tree for a root component class given its props tree.
Builds a root instance via (factory class) (binding *app* to app, default
nil) and walks it with render-tree.
Returns the pure TUI node tree for a root component `class` given its `props` tree. Builds a root instance via `(factory class)` (binding `*app*` to `app`, default `nil`) and walks it with `render-tree`.
(render-tree x)Returns a pure TUI node tree for x, recursively replacing every component
instance with the node tree its render produces. The result contains no component
instances and is suitable to hand to place. Each kind of x is handled as:
node?) - recurse into its ::children (rendering each child),
keeping the node's tag/attrs;rc/component-instance?) - bind *parent* to it, call its
:render, then recurse into the returned node (or splice a returned vector of
siblings);nil - returned as nil (callers/render-children drop it);When the render of a component yields several siblings, this returns a vector of nodes; otherwise it returns a single node (or scalar).
Returns a pure TUI node tree for `x`, recursively replacing every component instance with the node tree its render produces. The result contains no component instances and is suitable to hand to `place`. Each kind of `x` is handled as: * a TUI node (`node?`) - recurse into its `::children` (rendering each child), keeping the node's tag/attrs; * a component instance (`rc/component-instance?`) - bind `*parent*` to it, call its `:render`, then recurse into the returned node (or splice a returned vector of siblings); * a string or number - returned unchanged; * `nil` - returned as `nil` (callers/`render-children` drop it); * a sequential collection - rendered element-wise and returned as a vector of siblings (spliced by the caller). When the render of a component yields several siblings, this returns a vector of nodes; otherwise it returns a single node (or scalar).
(resolve-focus! app node-tree)Re-resolves focus after a dispatch may have changed app's state/tree. Recomputes
focusables from the freshly rendered node-tree; if the currently focused id is no
longer focusable it is moved to the next id in focus-order (else the first; else
nil), firing the appropriate :on-lost-focus/:on-focus transitions. Returns
app.
Re-resolves focus after a dispatch may have changed `app`'s state/tree. Recomputes focusables from the freshly rendered `node-tree`; if the currently focused id is no longer focusable it is moved to the next id in `focus-order` (else the first; else `nil`), firing the appropriate `:on-lost-focus`/`:on-focus` transitions. Returns `app`.
(resolve-size spec extent intrinsic)Resolves a child size spec against the available main-axis extent and the child's intrinsic
size. A number is a fixed cell count; :half is half the extent; [:fraction f] is that fraction
of the extent (rounded); nil (or anything else) falls back to the intrinsic size.
Resolves a child size `spec` against the available main-axis `extent` and the child's `intrinsic` size. A number is a fixed cell count; `:half` is half the extent; `[:fraction f]` is that fraction of the extent (rounded); `nil` (or anything else) falls back to the intrinsic size.
(route-key context node-tree focus-id key-event global-keymap)Routes key event key-event through node-tree, returning a truthy value when the
event was handled. Dispatch precedence:
(a) the focused node's :on-key handler (focused node = the one whose :id is
focus-id), then
(b) the :on-key handlers of its ancestors, from nearest to root (bubbling),
stopping as soon as a handler returns a truthy value (e.g. :handled); then
(c) if still unhandled, the handler in global-keymap (a map from a normalized
chord — see key-chord — to a handler) for this event's chord.
:on-key handlers receive the key-event (interaction-util convention); a global
keymap handler receives context and the key-event. Returns the truthy handler
result, or false when nothing handled the event.
Routes key event `key-event` through `node-tree`, returning a truthy value when the
event was handled. Dispatch precedence:
(a) the focused node's `:on-key` handler (focused node = the one whose `:id` is
`focus-id`), then
(b) the `:on-key` handlers of its ancestors, from nearest to root (bubbling),
stopping as soon as a handler returns a truthy value (e.g. `:handled`); then
(c) if still unhandled, the handler in `global-keymap` (a map from a normalized
chord — see `key-chord` — to a handler) for this event's chord.
`:on-key` handlers receive the `key-event` (interaction-util convention); a global
keymap handler receives `context` and the `key-event`. Returns the truthy handler
result, or `false` when nothing handled the event.(row-ops row cols next-cells prev-cells)Returns the vector of run ops for a single row of next-buf, comparing against prev-cells (the
same row of the previous buffer, or nil for a full repaint). Adjacent changed cells that share a
style are coalesced into one op; on a full repaint every non-default cell is emitted (still
coalesced).
Returns the vector of run ops for a single `row` of `next-buf`, comparing against `prev-cells` (the same row of the previous buffer, or `nil` for a full repaint). Adjacent changed cells that share a style are coalesced into one op; on a full repaint every non-default cell is emitted (still coalesced).
(rowcol->caret value width row col)Returns the caret index into value for the visual position [row col] in the layout of value
wrapped to width columns. row is clamped into the row range and col is clamped to the target
row's length, so an off-the-end position lands at that row's end. The inverse of caret->rowcol
(modulo the wrap-boundary convention). Pure.
Returns the caret index into `value` for the visual position `[row col]` in the layout of `value` wrapped to `width` columns. `row` is clamped into the row range and `col` is clamped to the target row's length, so an off-the-end position lands at that row's end. The inverse of `caret->rowcol` (modulo the wrap-boundary convention). Pure.
(same-dims? a b)Returns true if buffers a and b have identical row and column counts.
Returns true if buffers `a` and `b` have identical row and column counts.
(screen buffer)Returns a vector of rows strings, one per buffer row, formed by joining each row's cell
characters. Wide-char continuation cells (the blank that follows a 2-column character) are dropped
so the strings read naturally.
Returns a vector of `rows` strings, one per buffer row, formed by joining each row's cell characters. Wide-char continuation cells (the blank that follows a 2-column character) are dropped so the strings read naturally.
(screen-styled buffer)Returns a vector (one per buffer row) of vectors of cell maps {:ch :sgr}, exposing the full styled
contents of buffer for fine-grained assertions.
Returns a vector (one per buffer row) of vectors of cell maps `{:ch :sgr}`, exposing the full styled
contents of `buffer` for fine-grained assertions.(scroll-to-show scroll virtual-rect view-size)Returns the new scroll offset {:x :y} for a viewport so that the focused node's virtual-rect
becomes visible within the view-size {:w :h} content window, given the current scroll {:x :y}.
Performs the MINIMAL scroll on each axis: if the rect is above/left of the window, scroll back to its
near edge; if below/right, scroll forward so its far edge is the last visible cell; otherwise leave
that axis unchanged. Does not clamp to the virtual size (callers clamp via clamp-scroll). Pure.
Returns the new scroll offset `{:x :y}` for a viewport so that the focused node's `virtual-rect`
becomes visible within the `view-size` `{:w :h}` content window, given the current `scroll` `{:x :y}`.
Performs the MINIMAL scroll on each axis: if the rect is above/left of the window, scroll back to its
near edge; if below/right, scroll forward so its far edge is the last visible cell; otherwise leave
that axis unchanged. Does not clamp to the virtual size (callers clamp via `clamp-scroll`). Pure.(set-caret! app id caret)Stores caret index caret for input id in app's runtime caret store
(::carets) via swap! on the runtime atom. Returns app.
Stores caret index `caret` for input `id` in `app`'s runtime caret store (`::carets`) via `swap!` on the runtime atom. Returns `app`.
(set-input-width! app id width)Records the effective content width (in columns) for multiline input id in app's runtime
(::input-widths) via swap! on the runtime atom, so subsequent key handling wraps and navigates
at the same width the input is painted with. Returns app.
Records the effective content `width` (in columns) for multiline input `id` in `app`'s runtime (`::input-widths`) via `swap!` on the runtime atom, so subsequent key handling wraps and navigates at the same width the input is painted with. Returns `app`.
(set-viewport-scroll! app vp-id scroll)Sets the scroll offset {:x :y} for the viewport with id vp-id in app's state-map (under
::scroll) via a direct swap!. Returns app.
Sets the scroll offset `{:x :y}` for the viewport with id `vp-id` in `app`'s state-map (under
`::scroll`) via a direct `swap!`. Returns `app`.(sgr-string codes)Returns the ANSI SGR escape string for the vector of integer codes, e.g. [1 31] becomes the CSI
select-graphic-rendition sequence "\u001b[1;31m". An empty codes vector yields the reset
sequence "\u001b[0m".
Returns the ANSI SGR escape string for the vector of integer `codes`, e.g. `[1 31]` becomes the CSI select-graphic-rendition sequence `"\u001b[1;31m"`. An empty `codes` vector yields the reset sequence `"\u001b[0m"`.
The set of recognized non-printable :key values in a key event.
The set of recognized non-printable `:key` values in a key event.
(string-width s)Returns the total terminal column width of string s (the sum of its per-code-point widths).
Control characters, including newlines, contribute 0; callers that care about multi-line text
should split on newlines and measure each line.
Returns the total terminal column width of string `s` (the sum of its per-code-point widths). Control characters, including newlines, contribute 0; callers that care about multi-line text should split on newlines and measure each line.
(strip-overlays node-tree)Returns node-tree with every :modal node (open or closed) removed from its children, recursively.
The base layout/paint walks this stripped tree so overlays never occupy space or paint inline — they
are composited separately by the driver.
Returns `node-tree` with every `:modal` node (open or closed) removed from its children, recursively. The base layout/paint walks this stripped tree so overlays never occupy space or paint inline — they are composited separately by the driver.
(style->sgr-codes style)Returns the ordered vector of SGR integer codes for style. Attribute codes come first (reverse=7,
then bold=1), followed by the foreground color code and then the background color code (foreground
base + 10). The empty/default style yields an empty vector.
Returns the ordered vector of SGR integer codes for `style`. Attribute codes come first (reverse=7, then bold=1), followed by the foreground color code and then the background color code (foreground base + 10). The empty/default style yields an empty vector.
The set of legal TUI node tags.
:vbox - A container that stacks its children vertically (dividing height).:hbox - A container that stacks its children horizontally (dividing width).:box - A single styling/padding/border container.:text - A leaf that renders string/number children as text.:input - A leaf editable text field.:button - A leaf activatable control rendering its children as a label.:line - A leaf horizontal/vertical rule.:viewport - A fixed-size container whose larger child scrolls within it.:modal - An overlay window (stacks children vertically, like :vbox) that the driver hoists
to a screen-positioned rect and paints on top of the rest of the UI.The set of legal TUI node tags.
* `:vbox` - A container that stacks its children vertically (dividing height).
* `:hbox` - A container that stacks its children horizontally (dividing width).
* `:box` - A single styling/padding/border container.
* `:text` - A leaf that renders string/number children as text.
* `:input` - A leaf editable text field.
* `:button` - A leaf activatable control rendering its children as a label.
* `:line` - A leaf horizontal/vertical rule.
* `:viewport` - A fixed-size container whose larger child scrolls within it.
* `:modal` - An overlay window (stacks children vertically, like `:vbox`) that the driver hoists
to a screen-positioned rect and paints on top of the rest of the UI.(text & args)Returns a :text node rendering its string/number children as text. An optional leading
attribute map sets style attributes (e.g. :color, :highlight).
Returns a `:text` node rendering its string/number `children` as text. An optional leading attribute map sets style attributes (e.g. `:color`, `:highlight`).
(text-content node)Returns the string formed by concatenating the string/number children of node (its text/label).
Returns the string formed by concatenating the string/number children of `node` (its text/label).
(text-scroll-top value width caret height)Returns the internal top visual-line offset for a multiline input so that the caret's visual row is
visible within a height-row window, given the value wrapped to width columns and the current
caret. Performs minimal scrolling: if the caret row is above the current window it would scroll
to it; here, with no prior offset tracked, it returns the smallest non-negative top such that the
caret row lies in [top, top+height) and the window does not run past the last row unnecessarily.
Concretely it returns (max 0 (min (- caret-row (dec height)) ...)) clamped so the caret row is the
last visible row when it would otherwise be below the window, and 0 when the caret row already fits
from the top. Pure.
Returns the internal top visual-line offset for a multiline input so that the caret's visual row is visible within a `height`-row window, given the `value` wrapped to `width` columns and the current `caret`. Performs minimal scrolling: if the caret row is above the current window it would scroll to it; here, with no prior offset tracked, it returns the smallest non-negative top such that the caret row lies in `[top, top+height)` and the window does not run past the last row unnecessarily. Concretely it returns `(max 0 (min (- caret-row (dec height)) ...))` clamped so the caret row is the last visible row when it would otherwise be below the window, and 0 when the caret row already fits from the top. Pure.
(transact! app-or-component tx)(transact! app-or-component tx options)Re-export of com.fulcrologic.fulcro.raw.components/transact! for use by TUI
event handlers. Submits transaction tx against app-or-component (optionally
with options), returning the transaction id.
Re-export of `com.fulcrologic.fulcro.raw.components/transact!` for use by TUI event handlers. Submits transaction `tx` against `app-or-component` (optionally with `options`), returning the transaction id.
(tui-defsc* env args)Returns the macroexpansion of a tui/defsc form from the macro env and args
(the raw defsc argument list). Validates the args/options via macro-support's
::ms/args/::ms/options specs and builds :ident/:query/:initial-state
forms via the ms/build-* helpers (so prop-destructuring-vs-query and
ident-in-query are validated for free). Emits a plain def of the class built by
rc/configure-anonymous-component! from an options map containing the codegen'd
:query/:ident/:initial-state, the :componentName, and a React-free
:render (fn [this] (let [props (rc/props this) ...] body)). No React is
emitted, and no cljs.analyzer is touched (errors come from macro-support's
default ex-info).
Returns the macroexpansion of a `tui/defsc` form from the macro `env` and `args` (the raw `defsc` argument list). Validates the args/options via macro-support's `::ms/args`/`::ms/options` specs and builds `:ident`/`:query`/`:initial-state` forms via the `ms/build-*` helpers (so prop-destructuring-vs-query and ident-in-query are validated for free). Emits a plain `def` of the class built by `rc/configure-anonymous-component!` from an options map containing the codegen'd `:query`/`:ident`/`:initial-state`, the `:componentName`, and a React-free `:render` `(fn [this] (let [props (rc/props this) ...] body))`. No React is emitted, and no `cljs.analyzer` is touched (errors come from macro-support's default `ex-info`).
(type! node s)Invokes the :on-change handler attribute of TUI node (if present) with the
string s and returns its result. Returns nil when there is no handler. Used to
simulate typing the proposed value s into an input.
Invokes the `:on-change` handler attribute of TUI `node` (if present) with the string `s` and returns its result. Returns `nil` when there is no handler. Used to simulate typing the proposed value `s` into an input.
(vbox & args)Returns a :vbox node that stacks children vertically. An optional leading attribute map sets
layout/style attributes for the container.
Returns a `:vbox` node that stacks `children` vertically. An optional leading attribute map sets layout/style attributes for the container.
(viewport & args)Returns a :viewport node: a fixed-size container whose (potentially larger) children scroll
within its bounds. An optional leading attribute map sets size and :id (for scroll state).
Returns a `:viewport` node: a fixed-size container whose (potentially larger) `children` scroll within its bounds. An optional leading attribute map sets size and `:id` (for scroll state).
(viewport-scroll app-or-state vp-id)Returns the scroll offset {:x :y} recorded for the viewport with id vp-id in app-or-state
(under the state-map key ::scroll), defaulting to {:x 0 :y 0} when none is recorded.
Returns the scroll offset `{:x :y}` recorded for the viewport with id `vp-id` in `app-or-state`
(under the state-map key `::scroll`), defaulting to `{:x 0 :y 0}` when none is recorded.(viewport? node)Returns true if node is a :viewport node.
Returns true if `node` is a `:viewport` node.
(wrap-layout value width)Returns the visual-line layout of string value wrapped to width display columns: a vector of
{:start :len :text} maps, one per visual row, in order. :text is the row's wrapped string,
:start is the caret index (into value) of the row's first character, and :len is the number
of value characters the row's text spans. Characters dropped at a soft-wrap boundary (the
collapsed space between two wrapped words) and the hard \n are NOT counted in any row's :len;
they live in the gap between one row's :start + :len and the next row's :start. Reuses
wrap-text so the rows match what paint displays.
Returns the visual-line layout of string `value` wrapped to `width` display columns: a vector of
`{:start :len :text}` maps, one per visual row, in order. `:text` is the row's wrapped string,
`:start` is the caret index (into `value`) of the row's first character, and `:len` is the number
of `value` characters the row's text spans. Characters dropped at a soft-wrap boundary (the
collapsed space between two wrapped words) and the hard `\n` are NOT counted in any row's `:len`;
they live in the gap between one row's `:start + :len` and the next row's `:start`. Reuses
`wrap-text` so the rows match what `paint` displays.(wrap-segment segment width)Returns a vector of visual lines for a single newline-free segment greedily word-wrapped to
width display columns (words split on a single space, widths measured with string-width).
Words are packed onto a line separated by single spaces while they fit; a word that does not fit
starts a new line; a word wider than width is hard-broken (hard-break). When width is 0 or
negative the whole segment is returned as one line. An empty/blank segment yields a single empty
line.
Returns a vector of visual lines for a single newline-free `segment` greedily word-wrapped to `width` display columns (words split on a single space, widths measured with `string-width`). Words are packed onto a line separated by single spaces while they fit; a word that does not fit starts a new line; a word wider than `width` is hard-broken (`hard-break`). When `width` is 0 or negative the whole segment is returned as one line. An empty/blank segment yields a single empty line.
(wrap-text s width)Returns a vector of visual-line strings produced by word-wrapping string s to width display
columns. s is first split on hard newlines (\n), then each segment is greedily word-wrapped
(wrap-segment): words are packed left-to-right separated by single spaces while they fit on the
current line; a word that does not fit moves to the next line; a word wider than width is broken
hard at width columns (so a single over-long token never overflows). All widths are measured
with string-width/code-point-width, never count, so wide and zero-width characters are
handled correctly.
Conventions:
s is preserved
as an empty visual line).width <= 0 disables wrapping: each newline-separated segment becomes exactly one line.Returns a vector of visual-line strings produced by word-wrapping string `s` to `width` display
columns. `s` is first split on hard newlines (`\n`), then each segment is greedily word-wrapped
(`wrap-segment`): words are packed left-to-right separated by single spaces while they fit on the
current line; a word that does not fit moves to the next line; a word wider than `width` is broken
hard at `width` columns (so a single over-long token never overflows). All widths are measured
with `string-width`/`code-point-width`, never `count`, so wide and zero-width characters are
handled correctly.
Conventions:
* An empty or all-spaces segment yields a single empty line (so a blank line in `s` is preserved
as an empty visual line).
* `width <= 0` disables wrapping: each newline-separated segment becomes exactly one line.
* Leading/trailing spaces and runs of spaces inside a line may collapse when wrapping forces a
break, but spaces are otherwise preserved within a line.(wrapped-line-count node width)Returns the number of visual lines that wrapping-text node occupies when laid out into a
content width of width columns (the width inside any of the node's own insets). Always at least
one line.
Returns the number of visual lines that wrapping-`text` `node` occupies when laid out into a content width of `width` columns (the width inside any of the node's own insets). Always at least one line.
(wrapping-text? node)Returns true if node is a :text node whose attrs request line wrapping (:wrap true). Such a
text is laid out height-for-width: at place time its height is the number of visual lines its
content wraps to at its assigned width.
Returns true if `node` is a `:text` node whose attrs request line wrapping (`:wrap true`). Such a text is laid out *height-for-width*: at place time its height is the number of visual lines its content wraps to at its assigned width.
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |