The application/lifecycle front door for the TUI rendering target: build a Fulcro app, attach a terminal, run the input loop, and stop it. This is also where the side-effecting render driver lives (it renders a Fulcro app to a terminal and runs the keyboard input loop).
This is the edge that ties together the pure TUI pipeline in com.fulcrologic.fulcro.tui.engine
(layout, paint, diff, focus, input) and the element generators in
com.fulcrologic.fulcro.tui.elements 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-blocking!) to attach a terminal and start the keyboard input loop, quit! to stop it,
and step! to drive a single deterministic iteration (used by tests).
State/runtime keys (single source of truth):
com.fulcrologic.fulcro.tui.engine (see that ns).::terminal, ::prev-buffer, ::placed,
::last-size).This is JVM/babashka only (plain .clj).
Viewport scrolling: engine/place lays a viewport's single child out at its natural size into a
virtual rect, and engine/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.engine/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 engine/process-key!) and follow-focus! autoscrolls to track the focused item.
The application/lifecycle front door for the TUI rendering target: build a Fulcro app, attach a
terminal, run the input loop, and stop it. This is also where the side-effecting render driver
lives (it renders a Fulcro app to a terminal and runs the keyboard input loop).
This is the edge that ties together the pure TUI pipeline in `com.fulcrologic.fulcro.tui.engine`
(layout, paint, diff, focus, input) and the element generators in
`com.fulcrologic.fulcro.tui.elements` 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-blocking!`) to attach a terminal and start the keyboard input loop, `quit!` to stop it,
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.engine` (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: `engine/place` lays a viewport's single child out at its natural size into a
virtual rect, and `engine/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.engine/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 `engine/process-key!`) and `follow-focus!` autoscrolls to track the focused item.(application {:keys [root-class inspect? global-keymap]
:or {inspect? (= "true" (System/getProperty "tui.inspect"))}
:as opts})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,
and render hooks wired so that every state-change repaints through TUI rendering.
By DEFAULT the app's state is initialized from the root class's
declared :initial-state (the idiomatic Fulcro pattern — initial state is declared on Root and
composes down the UI tree).
Additional options from this library:
:global-keymap - optional default key-chord -> (fn [app key-event]) map registered on the
app; mount!/run-blocking!/start! use it unless they are passed their
own :global-keymap. Handy for a quit chord without repeating it per run.:inspect? - DEBUG ONLY (JVM, not babashka). When truthy, attaches Fulcro Inspect so a
running standalone Inspect (Electron) app on localhost:8237 observes this
app's transactions / network / state. Defaults to the tui.inspect system
property (-Dtui.inspect=true). The shim
(com.fulcrologic.fulcro.tui.inspect) ships in the library (src/main) but
is loaded lazily via requiring-resolve only when this is truthy, so normal
and babashka runs never load it (and its devtools deps stay off the path).See Fulcro's rapp/fulcro-app for additional options.
Typically you will use run-blocking! to actually run the application.
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`,
and render hooks wired so that every state-change repaints through TUI rendering.
By DEFAULT the app's state is initialized from the root class's
declared `:initial-state` (the idiomatic Fulcro pattern — initial state is declared on Root and
composes down the UI tree).
Additional options from this library:
* `:global-keymap` - optional default `key-chord` -> `(fn [app key-event])` map registered on the
app; `mount!`/`run-blocking!`/`start!` use it unless they are passed their
own `:global-keymap`. Handy for a quit chord without repeating it per run.
* `:inspect?` - DEBUG ONLY (JVM, not babashka). When truthy, attaches Fulcro Inspect so a
running standalone Inspect (Electron) app on localhost:8237 observes this
app's transactions / network / state. Defaults to the `tui.inspect` system
property (`-Dtui.inspect=true`). The shim
(`com.fulcrologic.fulcro.tui.inspect`) ships in the library (`src/main`) but
is loaded lazily via `requiring-resolve` only when this is truthy, so normal
and babashka runs never load it (and its devtools deps stay off the path).
See Fulcro's rapp/fulcro-app for additional options.
Typically you will use `run-blocking!` to actually run the application.(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 ::engine/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 `::engine/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 (engine/multiline-input?), the value is wrapped to the rect's width, the
caret's visual [row col] is computed (engine/caret->rowcol), and the cursor is placed at
rect-origin + (row - top-line, col) where top-line is the input's injected internal scroll
(::engine/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` (`engine/multiline-input?`), the value is wrapped to the rect's width, the caret's visual `[row col]` is computed (`engine/caret->rowcol`), and the cursor is placed at `rect-origin + (row - top-line, col)` where `top-line` is the input's injected internal scroll (`::engine/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.
(dispatch-key! app key-event global-keymap)Dispatches key-event against app WITHOUT rendering, returning app. This is the state-mutating
half of step! (the render is the caller's responsibility, so the live loop can use a throttled
request-render! while step!/tests render synchronously).
Dispatch order:
viewport-scroll-key!): if the focused node lies inside a viewport and
key-event is PageUp/PageDown, the enclosing viewport is scrolled and the focus/input pipeline
is skipped.engine/process-key! handles focus/typing/activation/global-keymap, then
follow-focus! 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 & 2.
Dispatches `key-event` against `app` WITHOUT rendering, returning `app`. This is the state-mutating half of `step!` (the render is the caller's responsibility, so the live loop can use a throttled `request-render!` while `step!`/tests render synchronously). Dispatch order: 1. Viewport scroll keys (`viewport-scroll-key!`): if the focused node lies inside a viewport and `key-event` is PageUp/PageDown, the enclosing viewport is scrolled and the focus/input pipeline is skipped. 2. Otherwise `engine/process-key!` handles focus/typing/activation/global-keymap, then `follow-focus!` 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 & 2.
(focused-screen-rect app placed focus-id)Returns the on-screen ::engine/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 ::engine/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 `::engine/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 `::engine/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 (engine/focus-viewport-context)
and the focused node's VIRTUAL rect. Computes the minimal scroll that brings that rect into the
viewport's content window (engine/scroll-to-show), clamps it (engine/clamp-scroll), and writes it to
the viewport's scroll state (engine/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 (`engine/focus-viewport-context`) and the focused node's VIRTUAL rect. Computes the minimal scroll that brings that rect into the viewport's content window (`engine/scroll-to-show`), clamps it (`engine/clamp-scroll`), and writes it to the viewport's scroll state (`engine/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 (engine/set-input-width!).
For every :viewport node it sets ::engine/scroll from app's scroll state (the state-map key
::engine/scroll, keyed by viewport id), clamped via engine/clamp-scroll against the viewport's
::engine/virtual-size and its content-area view size. Viewports without an :id keep their default
{:x 0 :y 0}.
For every multiline :input node (engine/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 (engine/text-scroll-top) from the input's caret so the caret row
stays visible, and assocs it under ::engine/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 (`engine/set-input-width!`).
For every `:viewport` node it sets `::engine/scroll` from `app`'s scroll state (the state-map key
`::engine/scroll`, keyed by viewport id), clamped via `engine/clamp-scroll` against the viewport's
`::engine/virtual-size` and its content-area view size. Viewports without an `:id` keep their default
`{:x 0 :y 0}`.
For every multiline `:input` node (`engine/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 (`engine/text-scroll-top`) from the input's caret so the caret row
stays visible, and assocs it under `::engine/text-scroll`.
Walks the placed tree (including nested viewport content).(mount! app)(mount! app {:keys [terminal global-keymap on-error max-fps]})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).:global-keymap - optional map of key-chord -> (fn [app key-event]). These reserved
chords (e.g. a quit chord) are dispatched at the loop level so they fire
regardless of which node has focus; everything else goes through the
focus/input pipeline (step!). Defaults to the keymap registered on the
app at application/start! time (::global-keymap), if any.:on-error - optional (fn [app throwable]) called if the input loop throws. The loop
always also stashes the throwable on the handle's :error atom and leaves
the terminal.:max-fps - optional max live render frequency (frames/sec). When present and positive the
input loop and resize/render hooks coalesce repaints to at most one per
(quot 1000 max-fps) ms (trailing-edge debounce via request-render!). When
absent (the default for a directly-called mount!, e.g. tests) throttling is
DISABLED and every render path is synchronous == step!. run-blocking!/
start! default this to 15.Returns a handle map {:app :terminal :thread :running? :error}. :running? is an atom that, when
set false, stops the loop; :error is an atom holding any uncaught loop exception (else nil).
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 (in a finally).
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`).
* `:global-keymap` - optional map of `key-chord` -> `(fn [app key-event])`. These reserved
chords (e.g. a quit chord) are dispatched at the loop level so they fire
regardless of which node has focus; everything else goes through the
focus/input pipeline (`step!`). Defaults to the keymap registered on the
app at `application`/`start!` time (`::global-keymap`), if any.
* `:on-error` - optional `(fn [app throwable])` called if the input loop throws. The loop
always also stashes the throwable on the handle's `:error` atom and leaves
the terminal.
* `:max-fps` - optional max live render frequency (frames/sec). When present and positive the
input loop and resize/render hooks coalesce repaints to at most one per
`(quot 1000 max-fps)` ms (trailing-edge debounce via `request-render!`). When
absent (the default for a directly-called `mount!`, e.g. tests) throttling is
DISABLED and every render path is synchronous == `step!`. `run-blocking!`/
`start!` default this to 15.
Returns a handle map `{:app :terminal :thread :running? :error}`. `:running?` is an atom that, when
set false, stops the loop; `:error` is an atom holding any uncaught loop exception (else `nil`).
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 (in a `finally`).(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 (engine/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 (`engine/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-blocking! handle (or, given an app, looks up its
handle/terminal). Returns handle-or-app. Steps, in order:
:running? false (so the loop won't process the next key).t-on-resize! with nil) — otherwise a stray
SIGWINCH delivered after the terminal is closed would invoke render! against a closed
terminal (C1).t-leave!). For a real JLine terminal this CLOSES the terminal, which
forces a thread parked in the blocking t-read-key to return EOF — this (not the interrupt)
is what actually unblocks and ends a programmatically-quit loop (C3)..interrupt of the loop thread as a fallback.Residual limitation: unblocking the blocked read depends on JLine closing the input on .close;
if a transport does not, the loop ends on the next keypress/EOF instead.
Stops the input loop for a `mount!`/`run-blocking!` `handle` (or, given an `app`, looks up its handle/terminal). Returns `handle-or-app`. Steps, in order: 1. Sets `:running?` false (so the loop won't process the next key). 2. Unregisters the terminal's resize handler (`t-on-resize!` with `nil`) — otherwise a stray SIGWINCH delivered after the terminal is closed would invoke `render!` against a closed terminal (C1). 3. Leaves the terminal (`t-leave!`). For a real JLine terminal this CLOSES the terminal, which forces a thread parked in the blocking `t-read-key` to return EOF — this (not the interrupt) is what actually unblocks and ends a programmatically-quit loop (C3). 4. Best-effort `.interrupt` of the loop thread as a fallback. Residual limitation: unblocking the blocked read depends on JLine closing the input on `.close`; if a transport does not, the loop ends on the next keypress/EOF instead.
(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`.
(request-render! app)Requests a repaint of app, coalescing bursts into at most one render per throttle window.
The throttle interval (ms) is read from the runtime atom (::render-throttle-ms, default 0). When
the interval is <= 0 (the default for attach!/step!/direct mount! — i.e. the deterministic
test path) this renders SYNCHRONOUSLY and is identical to calling render!.
When the interval is > 0 (the live run path: run-blocking!/start!) it is a leading+trailing
edge throttle. If at least the interval has elapsed since the last render it renders immediately
(leading edge) and records the time. Otherwise it schedules ONE daemon thread that sleeps the
remaining time and then renders the LATEST app state (trailing edge); further requests inside that
window coalesce into that single scheduled render (it reads current state at paint time, so the
final state is always painted). The deferred paint is wrapped in try/catch so a render that lands
after quit! closes the terminal cannot crash the daemon. Returns app.
Requests a repaint of `app`, coalescing bursts into at most one render per throttle window. The throttle interval (ms) is read from the runtime atom (`::render-throttle-ms`, default `0`). When the interval is `<= 0` (the default for `attach!`/`step!`/direct `mount!` — i.e. the deterministic test path) this renders SYNCHRONOUSLY and is identical to calling `render!`. When the interval is `> 0` (the live run path: `run-blocking!`/`start!`) it is a leading+trailing edge throttle. If at least the interval has elapsed since the last render it renders immediately (leading edge) and records the time. Otherwise it schedules ONE daemon thread that sleeps the remaining time and then renders the LATEST app state (trailing edge); further requests inside that window coalesce into that single scheduled render (it reads current state at paint time, so the final state is always painted). The deferred paint is wrapped in try/catch so a render that lands after `quit!` closes the terminal cannot crash the daemon. 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!.)
Unlike a bare mount!, the live run path DEFAULTS to :max-fps 30 (≈33ms trailing-edge debounce)
so a real interactive session caps repaints; override with an explicit :max-fps in opts (use
0/negative to disable throttling).
When the fulcro.tui.perf system property is set (see com.fulcrologic.fulcro.tui.perf), the
whole session is profiled automatically and a self-time report is printed to stdout once the loop
ends — by then mount!'s finally has left/restored the terminal, so the table prints cleanly.
Without the property the perf/profile wrapper compiles away entirely (zero overhead).
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!`.) Unlike a bare `mount!`, the live run path DEFAULTS to `:max-fps 30` (≈33ms trailing-edge debounce) so a real interactive session caps repaints; override with an explicit `:max-fps` in `opts` (use `0`/negative to disable throttling). When the `fulcro.tui.perf` system property is set (see `com.fulcrologic.fulcro.tui.perf`), the whole session is profiled automatically and a self-time report is printed to stdout once the loop ends — by then `mount!`'s `finally` has left/restored the terminal, so the table prints cleanly. Without the property the `perf/profile` wrapper compiles away entirely (zero overhead).
(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 engine/screen (vector of row strings) of the buffer most recently painted for app, or
nil if nothing has been painted yet.
Returns the `engine/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 engine/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 `engine/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.
(start! app-opts)(start! app-opts run-opts)Builds an application and runs it to completion in one call — the convenience entrypoint for
the common case. (start! app-opts) or (start! app-opts run-opts) is equivalent to
(run-blocking! (application app-opts) run-opts). app-opts are application's options
(:root-class, :initial-state, :remotes, :global-keymap, :inspect?); run-opts are
mount!'s (:terminal, :global-keymap, :on-error, :max-fps). Like run-blocking!, live
rendering DEFAULTS to :max-fps 30; override via run-opts. Blocks until the loop ends
(EOF/quit!). Returns the handle.
Builds an `application` and runs it to completion in one call — the convenience entrypoint for the common case. `(start! app-opts)` or `(start! app-opts run-opts)` is equivalent to `(run-blocking! (application app-opts) run-opts)`. `app-opts` are `application`'s options (`:root-class`, `:initial-state`, `:remotes`, `:global-keymap`, `:inspect?`); `run-opts` are `mount!`'s (`:terminal`, `:global-keymap`, `:on-error`, `:max-fps`). Like `run-blocking!`, live rendering DEFAULTS to `:max-fps 30`; override via `run-opts`. Blocks until the loop ends (EOF/`quit!`). Returns the handle.
(step! app key-event)(step! app key-event global-keymap)Runs one deterministic driver iteration for app: dispatches key-event (dispatch-key!) and
then repaints SYNCHRONOUSLY (render!). Returns app. Used by tests (which read the screen right
after step! returns) and historically by the input loop. The live input loop now dispatches and
then requests a THROTTLED render (request-render!) instead of calling step!, so this stays
synchronous for deterministic tests.
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.engine/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` (`dispatch-key!`) and then repaints SYNCHRONOUSLY (`render!`). Returns `app`. Used by tests (which read the screen right after `step!` returns) and historically by the input loop. The live input loop now dispatches and then requests a THROTTLED render (`request-render!`) instead of calling `step!`, so this stays synchronous for deterministic tests. 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 `engine/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 engine/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 `engine/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 |