The side-effecting driver for the TUI rendering target: it renders a Fulcro app to a terminal and runs the input loop.
This is the edge that ties together the pure TUI pipeline in com.fulcrologic.fulcro.tui (layout,
paint, diff, focus, input) with a concrete com.fulcrologic.fulcro.tui.terminal/Terminal. Use
application to build a synchronous raw Fulcro app whose renders repaint the terminal, mount!
(or run!) to attach a terminal and start the keyboard input loop, and step! to drive a single
deterministic iteration (used by tests).
State/runtime keys (single source of truth):
com.fulcrologic.fulcro.tui (see that ns).::terminal, ::prev-buffer, ::placed,
::last-size).This is JVM/babashka only (plain .clj).
Viewport scrolling: tui/place lays a viewport's single child out at its natural size into a
virtual rect, and tui/render-buffer blits only the window [scroll-x scroll-y w h] of that
virtual content into the viewport's ::rect. Scroll offsets are stored in the state-map under
:com.fulcrologic.fulcro.tui/scroll keyed by viewport id; render! injects them onto the placed
tree (inject-scroll), follow-focus! advances them after focus changes to keep the focused node
visible, and PageUp/PageDown page-scroll via viewport-scroll-key!. Up/Down arrows move focus
item-to-item (in tui/process-key!) and follow-focus! autoscrolls to track the focused item.
The side-effecting *driver* for the TUI rendering target: it renders a Fulcro app to a terminal
and runs the input loop.
This is the edge that ties together the pure TUI pipeline in `com.fulcrologic.fulcro.tui` (layout,
paint, diff, focus, input) with a concrete `com.fulcrologic.fulcro.tui.terminal/Terminal`. Use
`application` to build a synchronous raw Fulcro app whose renders repaint the terminal, `mount!`
(or `run!`) to attach a terminal and start the keyboard input loop, and `step!` to drive a single
deterministic iteration (used by tests).
State/runtime keys (single source of truth):
* Focus & carets & scroll are owned by `com.fulcrologic.fulcro.tui` (see that ns).
* The attached terminal and the bookkeeping for incremental painting live in the app
RUNTIME-ATOM under this namespace's keys (see `::terminal`, `::prev-buffer`, `::placed`,
`::last-size`).
This is JVM/babashka only (plain `.clj`).
Viewport scrolling: `tui/place` lays a viewport's single child out at its natural size into a
virtual rect, and `tui/render-buffer` blits only the window `[scroll-x scroll-y w h]` of that
virtual content into the viewport's `::rect`. Scroll offsets are stored in the state-map under
`:com.fulcrologic.fulcro.tui/scroll` keyed by viewport id; `render!` injects them onto the placed
tree (`inject-scroll`), `follow-focus!` advances them after focus changes to keep the focused node
visible, and PageUp/PageDown page-scroll via `viewport-scroll-key!`. Up/Down arrows move focus
item-to-item (in `tui/process-key!`) and `follow-focus!` autoscrolls to track the focused item.(application {:keys [root-class initial-state remotes]})Returns a synchronous raw Fulcro app suitable for TUI rendering. Builds a rapp/fulcro-app with
synchronous transactions (stx/with-synchronous-transactions), the given :root-class, optional
:remotes, and render hooks wired so that every state-change repaints through render! (when a
terminal has been attached). When :initial-state is provided the app's state is initialized from
the root class. opts:
:root-class - the (required) TUI root component class (built with tui/defsc).:initial-state - when truthy, initialize app state from root-class.:remotes - optional Fulcro remotes map.No terminal is attached here; attach one with attach!/mount!.
Returns a synchronous raw Fulcro app suitable for TUI rendering. Builds a `rapp/fulcro-app` with synchronous transactions (`stx/with-synchronous-transactions`), the given `:root-class`, optional `:remotes`, and render hooks wired so that every state-change repaints through `render!` (when a terminal has been attached). When `:initial-state` is provided the app's state is initialized from the root class. `opts`: * `:root-class` - the (required) TUI root component class (built with `tui/defsc`). * `:initial-state` - when truthy, initialize app state from `root-class`. * `:remotes` - optional Fulcro remotes map. No terminal is attached here; attach one with `attach!`/`mount!`.
(attach! app terminal)Attaches terminal to app and performs the initial paint. Stashes the terminal in the runtime
atom, enters the terminal (term/t-enter!), registers a resize handler that repaints on a terminal
size change (t-on-resize! → render!), sets initial focus to the first node in the current tree's
focus-order when ::tui/focus is unset, and renders once. Returns app. Starts no loop.
Attaches `terminal` to `app` and performs the initial paint. Stashes the terminal in the runtime atom, enters the terminal (`term/t-enter!`), registers a resize handler that repaints on a terminal size change (`t-on-resize!` → `render!`), sets initial focus to the first node in the current tree's `focus-order` when `::tui/focus` is unset, and renders once. Returns `app`. Starts no loop.
(caret-screen-position focused-node rect caret)Returns [x y visible?] for the hardware cursor given the placed focused-node (or nil), its
effective on-screen rect (or nil when the node has none / is scrolled out of view), and the
caret index caret.
For a single-line :input, the cursor is placed at the rect origin advanced by the display width of
the value up to caret, clamped to lie within the rect, and visible.
For a multiline :input (tui/multiline-input?), the value is wrapped to the rect's width, the
caret's visual [row col] is computed (tui/caret->rowcol), and the cursor is placed at
rect-origin + (row - top-line, col) where top-line is the input's injected internal scroll
(::tui/text-scroll). If that visual row is scrolled out of the rect's [0, h) window the cursor
is hidden.
For any other focused node the cursor is placed (visible) at the rect origin. When rect is nil
the cursor is hidden at the origin.
Returns `[x y visible?]` for the hardware cursor given the placed `focused-node` (or `nil`), its effective on-screen `rect` (or `nil` when the node has none / is scrolled out of view), and the caret index `caret`. For a single-line `:input`, the cursor is placed at the rect origin advanced by the display width of the value up to `caret`, clamped to lie within the rect, and visible. For a multiline `:input` (`tui/multiline-input?`), the value is wrapped to the rect's width, the caret's visual `[row col]` is computed (`tui/caret->rowcol`), and the cursor is placed at `rect-origin + (row - top-line, col)` where `top-line` is the input's injected internal scroll (`::tui/text-scroll`). If that visual row is scrolled out of the rect's `[0, h)` window the cursor is hidden. For any other focused node the cursor is placed (visible) at the rect origin. When `rect` is `nil` the cursor is hidden at the origin.
(focused-screen-rect app placed focus-id)Returns the on-screen ::tui/rect for the focused node focus-id within placed, accounting for an
enclosing scrolled viewport, or nil when the focused node is scrolled out of its viewport's window.
For a node NOT inside a viewport, returns the node's own placed ::tui/rect (its absolute rect).
For a node inside a viewport, its on-screen rect is viewport-content-origin + (virtual-origin - scroll); if that lands outside the viewport's content window, nil is returned (cursor hidden).
Returns the on-screen `::tui/rect` for the focused node `focus-id` within `placed`, accounting for an enclosing scrolled viewport, or `nil` when the focused node is scrolled out of its viewport's window. For a node NOT inside a viewport, returns the node's own placed `::tui/rect` (its absolute rect). For a node inside a viewport, its on-screen rect is `viewport-content-origin + (virtual-origin - scroll)`; if that lands outside the viewport's content window, `nil` is returned (cursor hidden).
(follow-focus! app placed)Adjusts viewport scroll state so the currently focused node stays visible, then returns app.
Using the placed tree, finds the focused node's enclosing viewport (tui/focus-viewport-context)
and the focused node's VIRTUAL rect. Computes the minimal scroll that brings that rect into the
viewport's content window (tui/scroll-to-show), clamps it (tui/clamp-scroll), and writes it to
the viewport's scroll state (tui/set-viewport-scroll!) when it differs. A no-op when the focused
node is not inside a viewport or the enclosing viewport has no :id.
Adjusts viewport scroll state so the currently focused node stays visible, then returns `app`. Using the `placed` tree, finds the focused node's enclosing viewport (`tui/focus-viewport-context`) and the focused node's VIRTUAL rect. Computes the minimal scroll that brings that rect into the viewport's content window (`tui/scroll-to-show`), clamps it (`tui/clamp-scroll`), and writes it to the viewport's scroll state (`tui/set-viewport-scroll!`) when it differs. A no-op when the focused node is not inside a viewport or the enclosing viewport has no `:id`.
(initial-node-tree app)Returns the current pure TUI node tree for app (root class + db->tree), for computing the
initial focus. Returns nil if the app has no state/root yet.
Returns the current pure TUI node tree for `app` (root class + `db->tree`), for computing the initial focus. Returns `nil` if the app has no state/root yet.
(inject-scroll app tree)Returns the placed tree with scroll state injected for the next paint, and records each multiline
input's effective wrap width on app's runtime (tui/set-input-width!).
For every :viewport node it sets ::tui/scroll from app's scroll state (the state-map key
::tui/scroll, keyed by viewport id), clamped via tui/clamp-scroll against the viewport's
::tui/virtual-size and its content-area view size. Viewports without an :id keep their default
{:x 0 :y 0}.
For every multiline :input node (tui/multiline-input?) it records the input's content width
(its placed content-area width) so key handling wraps at the painted width, then computes the
internal top visual-line offset (tui/text-scroll-top) from the input's caret so the caret row
stays visible, and assocs it under ::tui/text-scroll.
Walks the placed tree (including nested viewport content).
Returns the placed `tree` with scroll state injected for the next paint, and records each multiline
input's effective wrap width on `app`'s runtime (`tui/set-input-width!`).
For every `:viewport` node it sets `::tui/scroll` from `app`'s scroll state (the state-map key
`::tui/scroll`, keyed by viewport id), clamped via `tui/clamp-scroll` against the viewport's
`::tui/virtual-size` and its content-area view size. Viewports without an `:id` keep their default
`{:x 0 :y 0}`.
For every multiline `:input` node (`tui/multiline-input?`) it records the input's content width
(its placed content-area width) so key handling wraps at the painted width, then computes the
internal top visual-line offset (`tui/text-scroll-top`) from the input's caret so the caret row
stays visible, and assocs it under `::tui/text-scroll`.
Walks the placed tree (including nested viewport content).(mount! app)(mount! app {:keys [terminal global-keymap]})Attaches a terminal to app and starts the keyboard input loop on a new thread. opts:
:terminal - the Terminal to drive (default (term/jline-terminal); tests pass a
string-terminal).Returns a handle map {:app :terminal :thread :running?} where :running? is an atom that, when
set false, stops the loop. The loop reads keys with term/t-read-key; a nil (EOF) read or
:running? becoming false terminates it; term/t-leave! is always called on exit.
Attaches a terminal to `app` and starts the keyboard input loop on a new thread. `opts`:
* `:terminal` - the `Terminal` to drive (default `(term/jline-terminal)`; tests pass a
`string-terminal`).
Returns a handle map `{:app :terminal :thread :running?}` where `:running?` is an atom that, when
set false, stops the loop. The loop reads keys with `term/t-read-key`; a `nil` (EOF) read or
`:running?` becoming false terminates it; `term/t-leave!` is always called on exit.(placed-tree-of app)Returns the placed tree most recently painted for app (from the runtime atom), or nil.
Returns the placed tree most recently painted for `app` (from the runtime atom), or `nil`.
(position-cursor! app terminal placed)Positions the hardware cursor of terminal for app against the placed tree: finds the focused
node (tui/current-focus), computes its on-screen rect (via focused-screen-rect, which accounts
for a scrolled enclosing viewport and hides the cursor when the focused node is scrolled out of
view), computes its caret position, and calls term/t-set-cursor!. Returns app.
Positions the hardware cursor of `terminal` for `app` against the `placed` tree: finds the focused node (`tui/current-focus`), computes its on-screen rect (via `focused-screen-rect`, which accounts for a scrolled enclosing viewport and hides the cursor when the focused node is scrolled out of view), computes its caret position, and calls `term/t-set-cursor!`. Returns `app`.
(quit! handle-or-app)Stops the input loop for a mount!/run! handle (or, given an app, looks up its terminal):
sets :running? false, best-effort interrupts the loop thread, and leaves the terminal. Returns
handle-or-app.
Stops the input loop for a `mount!`/`run!` `handle` (or, given an `app`, looks up its terminal): sets `:running?` false, best-effort interrupts the loop thread, and leaves the terminal. Returns `handle-or-app`.
(render! app)Paints app to its attached terminal. This is the driver's core render and is wired as the app's
render hook so any state change repaints.
It (1) computes the pure node tree from app state (root class + db->tree), (2) reads the terminal
size, (3) lays the tree out into a placed tree and paints it into a fresh cell buffer (or, when the
terminal is below the root's declared :min-width/:min-height, a 'too small' buffer), (4) diffs
against the previously-painted buffer and writes the resulting ANSI to the terminal (wrapping in a
synchronized-output frame when the terminal supports it), (5) positions the hardware cursor at the
focused input's caret (or hides/origins it otherwise), and (6) stashes the new buffer, placed tree,
and terminal size in the runtime atom for the next frame. A change in terminal size invalidates the
previous buffer so the next frame is a full repaint. A no-op when no terminal is attached. Returns
app.
Paints `app` to its attached terminal. This is the driver's core render and is wired as the app's render hook so any state change repaints. It (1) computes the pure node tree from app state (root class + `db->tree`), (2) reads the terminal size, (3) lays the tree out into a placed tree and paints it into a fresh cell buffer (or, when the terminal is below the root's declared `:min-width`/`:min-height`, a 'too small' buffer), (4) diffs against the previously-painted buffer and writes the resulting ANSI to the terminal (wrapping in a synchronized-output frame when the terminal supports it), (5) positions the hardware cursor at the focused input's caret (or hides/origins it otherwise), and (6) stashes the new buffer, placed tree, and terminal size in the runtime atom for the next frame. A change in terminal size invalidates the previous buffer so the next frame is a full repaint. A no-op when no terminal is attached. Returns `app`.
(root-min node-tree)Returns {:min-width W :min-height H} for the root node-tree, reading the :min-width/
:min-height attrs of the root node and defaulting each to 1.
Returns `{:min-width W :min-height H}` for the root `node-tree`, reading the `:min-width`/
`:min-height` attrs of the root node and defaulting each to 1.(run-blocking! app)(run-blocking! app opts)Mounts app (see mount!) and blocks until the input loop's thread finishes (the terminal
reaches EOF or the loop is stopped). opts are passed to mount!. Returns the handle. (Named
run-blocking! rather than run! to avoid shadowing clojure.core/run!.)
Mounts `app` (see `mount!`) and blocks until the input loop's thread finishes (the terminal reaches EOF or the loop is stopped). `opts` are passed to `mount!`. Returns the handle. (Named `run-blocking!` rather than `run!` to avoid shadowing `clojure.core/run!`.)
(runtime app)Returns the (dereferenced) runtime map for app, or nil.
Returns the (dereferenced) runtime map for `app`, or `nil`.
(screen-of app)Returns the tui/screen (vector of row strings) of the buffer most recently painted for app, or
nil if nothing has been painted yet.
Returns the `tui/screen` (vector of row strings) of the buffer most recently painted for `app`, or `nil` if nothing has been painted yet.
(screen-styled-of app)Returns the tui/screen-styled (vector of rows of styled cell maps) of the buffer most recently
painted for app, or nil if nothing has been painted yet.
Returns the `tui/screen-styled` (vector of rows of styled cell maps) of the buffer most recently painted for `app`, or `nil` if nothing has been painted yet.
(step! app key-event)(step! app key-event global-keymap)Runs one deterministic driver iteration for app: dispatches key-event and then repaints
(render!). Returns app. Used by tests and the input loop.
Dispatch order:
viewport-scroll-key!): if the focused node lies inside a viewport and
key-event is PageUp/PageDown (or :up/:down on a non-input focus), the enclosing viewport
is scrolled and the focus/input pipeline is skipped.tui/process-key! handles focus/typing/activation/global-keymap.follow-focus! then adjusts viewport scroll so the (possibly newly) focused node stays visible.The placed tree from the previous frame is used to locate the enclosing viewport for steps 1 & 3.
Runs one deterministic driver iteration for `app`: dispatches `key-event` and then repaints
(`render!`). Returns `app`. Used by tests and the input loop.
Dispatch order:
1. Viewport scroll keys (`viewport-scroll-key!`): if the focused node lies inside a viewport and
`key-event` is PageUp/PageDown (or `:up`/`:down` on a non-input focus), the enclosing viewport
is scrolled and the focus/input pipeline is skipped.
2. Otherwise `tui/process-key!` handles focus/typing/activation/global-keymap.
3. `follow-focus!` then adjusts viewport scroll so the (possibly newly) focused node stays visible.
The placed tree from the previous frame is used to locate the enclosing viewport for steps 1 & 3.(terminal app)Returns the terminal currently attached to app (from its runtime atom), or nil.
Returns the terminal currently attached to `app` (from its runtime atom), or `nil`.
(too-small-buffer rows cols min-w min-h)Returns a rowsxcols cell buffer painted with a centered "terminal too small" message asking
for at least min-w x min-h. Used when the terminal is smaller than the root's declared minimum.
Returns a `rows`x`cols` cell buffer painted with a centered "terminal too small" message asking for at least `min-w` x `min-h`. Used when the terminal is smaller than the root's declared minimum.
(viewport-scroll-key! app placed key-event)If key-event is :page-up/:page-down and the currently focused node lies inside a viewport,
scrolls that viewport's scroll state by a page and returns truthy (:handled). Returns nil
(unhandled) otherwise, so the caller falls through to the normal key pipeline. placed is the
current placed tree (for locating the enclosing viewport).
:up/:down are NOT handled here: they drive focus navigation in tui/process-key! (moving
focus item-to-item through the focus ring), and follow-focus! keeps the focused item visible —
so arrowing through a focusable list autoscrolls its viewport. PageUp/PageDown remain the explicit
page-scroll for a focused viewport.
If `key-event` is `:page-up`/`:page-down` and the currently focused node lies inside a viewport, scrolls that viewport's scroll state by a page and returns truthy (`:handled`). Returns `nil` (unhandled) otherwise, so the caller falls through to the normal key pipeline. `placed` is the current placed tree (for locating the enclosing viewport). `:up`/`:down` are NOT handled here: they drive focus navigation in `tui/process-key!` (moving focus item-to-item through the focus ring), and `follow-focus!` keeps the focused item visible — so arrowing through a focusable list autoscrolls its viewport. PageUp/PageDown remain the explicit page-scroll for a focused viewport.
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 |