Liking cljdoc? Tell your friends :D

com.blockether.vis.ext.channel-tui.transient

Magit-style TRANSIENT as a REUSABLE, EMBEDDABLE component.

A transient is a VALUE — a spec — and this namespace knows how to lay it out, paint it into a rectangle, and (optionally) run its key loop. It owns NO screen, NO dialog chrome and NO Lanterna types, so ANY surface that can hand over a TextGraphics and answer keystrokes can embed one: the magit status buffer, the provider dialog, a modal of its own (dialogs/transient-dialog!), or a panel that does not exist yet.

THREE values, three concerns:

spec WHAT the transient is — a pure, reusable description: {:title "Commit" :groups [{:title "Arguments" :items [item …]} …] :read-option (fn [item current] str|nil)} where an item is {:key "h" :type :switch|:option|:action :id :no-verify :label "Disable hooks" :arg "--no-verify" :secret? false}. FLAGS (:switch / :option) render with magit's leading - and TOGGLE — press once to arm, again to disarm — while COMMANDS (:action) fire once and close. Keys are CASE-SENSITIVE, exactly like magit (-f is not -F). :read-option (impure, optional) fetches an OPTION's value; nil (Esc) leaves it unchanged.

region WHERE it sits — the host's rectangle: {:left :inner-w :hint-row :text-w :min-row :restore!}. :left/:inner-w are the host frame's border column and inner width, :hint-row the row the host's hint bar owns (the popup REPLACES it), :text-w the text budget inside the padding. :min-row is the first row the popup may touch — a tall transient stops there instead of climbing over whatever the host keeps visible. A band wipes ITS OWN rows and nothing above them; a host that pages bands of DIFFERENT heights passes :restore! ((fn [from to]), from dialogs/frame-restorer), and the rows a taller band covered are given back to the HOST instead of blanked.

host HOW to talk to the terminal — the ONLY impure dependency: {:g TextGraphics to paint into :hint-bar! (fn [g left row inner-w pairs]) :refresh! (fn []) flush the frame :read-key! (fn []):esc| Character | nil} nil from :read-key! means "nothing actionable" — the loop simply repaints. dialogs/transient-host builds the standard modal one.

The popup is bottom-anchored INSIDE the host's frame and reads ─── / bold title / ─── / groups / ─── / hint bar: the same chrome the host gives any other titled section, never a second window pasted on top.

Magit-style TRANSIENT as a REUSABLE, EMBEDDABLE component.

A transient is a VALUE — a `spec` — and this namespace knows how to lay it
out, paint it into a rectangle, and (optionally) run its key loop. It owns NO
screen, NO dialog chrome and NO Lanterna types, so ANY surface that can hand
over a `TextGraphics` and answer keystrokes can embed one: the magit status
buffer, the provider dialog, a modal of its own (`dialogs/transient-dialog!`),
or a panel that does not exist yet.

THREE values, three concerns:

  `spec`   WHAT the transient is — a pure, reusable description:
           `{:title "Commit"
             :groups [{:title "Arguments" :items [item …]} …]
             :read-option (fn [item current] str|nil)}`
           where an `item` is
           `{:key "h" :type :switch|:option|:action :id :no-verify
             :label "Disable hooks" :arg "--no-verify" :secret? false}`.
           FLAGS (`:switch` / `:option`) render with magit's leading `-` and
           TOGGLE — press once to arm, again to disarm — while COMMANDS
           (`:action`) fire once and close. Keys are CASE-SENSITIVE, exactly
           like magit (`-f` is not `-F`). `:read-option` (impure, optional)
           fetches an OPTION's value; nil (Esc) leaves it unchanged.

  `region`  WHERE it sits — the host's rectangle:
           `{:left :inner-w :hint-row :text-w :min-row :restore!}`.
           `:left`/`:inner-w` are the host frame's border column and inner
           width, `:hint-row` the row the host's hint bar owns (the popup
           REPLACES it), `:text-w` the text budget inside the padding.
           `:min-row` is the first row the popup may touch — a tall transient
           stops there instead of climbing over whatever the host keeps
           visible. A band wipes ITS OWN rows and nothing above them; a host
           that pages bands of DIFFERENT heights passes `:restore!`
           (`(fn [from to])`, from `dialogs/frame-restorer`), and the rows a
           taller band covered are given back to the HOST instead of blanked.

  `host`   HOW to talk to the terminal — the ONLY impure dependency:
           `{:g          TextGraphics to paint into
             :hint-bar!  (fn [g left row inner-w pairs])
             :refresh!   (fn [])         flush the frame
             :read-key!  (fn [])         `:esc` | Character | nil}`
           nil from `:read-key!` means "nothing actionable" — the loop simply
           repaints. `dialogs/transient-host` builds the standard modal one.

The popup is bottom-anchored INSIDE the host's frame and reads
`───` / bold title / `───` / groups / `───` / hint bar: the same chrome the
host gives any other titled section, never a second window pasted on top.
raw docstring

band-body-padclj

Blank display rows of BREATHING SPACE at the TOP and BOTTOM of every pane: the band's body is not glued to the rule that carries its title, nor to the closing rule above the hint bar. It is part of the BODY, not of the chrome, so a pane the terminal is too short for loses a padding row before it loses a verb.

Blank display rows of BREATHING SPACE at the TOP and BOTTOM of every pane: the
band's body is not glued to the rule that carries its title, nor to the closing
rule above the hint bar. It is part of the BODY, not of the chrome, so a pane
the terminal is too short for loses a padding row before it loses a verb.
sourceraw docstring

band-geometryclj

(band-geometry region n)
(band-geometry {:keys [hint-row min-row]} n is-title)

PURE: which row of region every part of a band n display rows tall lands on.

:sep-row the band's opening separator — the FIRST row it paints :title-row the bold title, alone on its row (title-less band: the separator's own row, so nothing extra is painted) :title-rule-row the title's OWN rule, closing the title band :body-top first display row :visible display rows that actually fit (overflow is dropped) :foot-rule-row the rule directly above the hint bar :foot-row the hint bar (the host's own hint row) :wipe-top first row the popup wipes before painting

Everything is clamped to :min-row, so a tall band — or a short terminal — stops at the host's content top instead of climbing over it.

The row count is the parameter, not the spec, because a band is not always a transient: the human-input form paints its own plan rows into exactly this geometry, and both must land on the same chrome.

A band is TITLE-LESS by default — the rule, then the body — because the C-x hydra's own heading said nothing its columns did not. A band that asks ONE question (dialogs/band-question-frame!, the human-input form) passes is-title true, because there the title IS the question.

PURE: which row of `region` every part of a band `n` display rows tall
lands on.

  `:sep-row`         the band's opening separator — the FIRST row it paints
  `:title-row`       the bold title, alone on its row (title-less band: the
                     separator's own row, so nothing extra is painted)
  `:title-rule-row`  the title's OWN rule, closing the title band
  `:body-top`        first display row
  `:visible`         display rows that actually fit (overflow is dropped)
  `:foot-rule-row`   the rule directly above the hint bar
  `:foot-row`        the hint bar (the host's own hint row)
  `:wipe-top`        first row the popup wipes before painting

Everything is clamped to `:min-row`, so a tall band — or a short
terminal — stops at the host's content top instead of climbing over it.

The row count is the parameter, not the spec, because a band is not always a
transient: the human-input form paints its own plan rows into exactly this
geometry, and both must land on the same chrome.

A band is TITLE-LESS by default — the rule, then the body — because the C-x
hydra's own heading said nothing its columns did not. A band that asks ONE
question (`dialogs/band-question-frame!`, the human-input form) passes
`is-title` true, because there the title IS the question.
sourceraw docstring

band-regionclj

(band-region cols rows content-top)
(band-region cols rows content-top prompt-h)

PURE: the rectangle an in-session BAND paints into on a cols×rows terminal whose content starts at content-top and whose prompt box is prompt-h rows tall.

The session frame is SIDELESS — the prompt is two horizontal rules with no rails — so a band that takes it over borrows exactly that: rules inset [[band-pad]] columns, text one column further in, and :is-sideless true so the chrome paints rules instead of ├───┤ junctions and wipes the FULL terminal width (a band sits on the live transcript, not on a modal's own paper).

The band sits ABOVE THE PROMPT, never over it. :hint-row is the echo-area row directly above the input box (rows - prompt-h - 3, mirroring screen's own input-top/echo-row math), so the thing the human is typing into — and the footer under it — stay visible and in place while a transient, the C-x hydra or a human-input form is up. :min-row is the floor, so however tall the band the header and the top of the transcript stay on screen.

PURE: the rectangle an in-session BAND paints into on a `cols`×`rows`
terminal whose content starts at `content-top` and whose prompt box is
`prompt-h` rows tall.

The session frame is SIDELESS — the prompt is two horizontal rules with no
`│` rails — so a band that takes it over borrows exactly that: rules inset
[[band-pad]] columns, text one column further in, and `:is-sideless true`
so the chrome paints rules instead of `├───┤` junctions and wipes the FULL
terminal width (a band sits on the live transcript, not on a modal's own
paper).

The band sits ABOVE THE PROMPT, never over it. `:hint-row` is the echo-area
row directly above the input box (`rows - prompt-h - 3`, mirroring `screen`'s
own `input-top`/`echo-row` math), so the thing the human is typing into — and
the footer under it — stay visible and in place while a transient, the C-x
hydra or a human-input form is up. `:min-row` is the floor, so however tall
the band the header and the top of the transcript stay on screen.
sourceraw docstring

checkclj

(check spec)

nil when spec is a legal transient, else ONE line saying why. The same judge run! uses, answering instead of throwing: a producer's own test — or a caller assembling a spec from live data — asks BEFORE a terminal is involved, and gets prose a human can read.

nil when `spec` is a legal transient, else ONE line saying why. The same
judge [[run!]] uses, answering instead of throwing: a producer's own test —
or a caller assembling a spec from live data — asks BEFORE a terminal is
involved, and gets prose a human can read.
sourceraw docstring

clear-rows!clj

(clear-rows! g {:keys [left inner-w is-sideless cols]} from to)

Blank rows from..to (INCLUSIVE) inside the host's frame: dialog paper across the inner columns and the frame's plain edge back in both border columns.

Wiping only the band's INNER columns leaves whatever the host painted in the two border columns: a status buffer's own section separator survived as stray / junctions beside the popup. Capping separators put their own junctions back afterwards.

A SIDELESS region (band-region) has no border columns, but its paper is still its OWN rectangle: it fills the band's inner columns with the band's bg and repaints the [[band-pad]] margins on each side with the TERMINAL's own background, so the band is a slab inset to its rules instead of a colour wash from screen edge to screen edge, and no stale transcript survives beside it either.

A transient erases exactly the rows it paints. A host that repaints BANDS OF DIFFERENT HEIGHTS into one rectangle — the model picker paging a catalog — owns the rows between its content and the band, and erases them with this before running the next page.

Blank rows `from`..`to` (INCLUSIVE) inside the host's frame: dialog paper
across the inner columns and the frame's plain edge back in both border
columns.

Wiping only the band's INNER columns leaves whatever the host painted in the
two border columns: a status buffer's own section separator survived as stray
`├`/`┤` junctions beside the popup. Capping separators put their own junctions
back afterwards.

A SIDELESS region (`band-region`) has no border columns, but its paper is
still its OWN rectangle: it fills the band's inner columns with the band's
bg and repaints the [[band-pad]] margins on each side with the TERMINAL's
own background, so the band is a slab inset to its rules instead of a colour
wash from screen edge to screen edge, and no stale transcript survives beside
it either.

A transient erases exactly the rows it paints. A host that repaints BANDS OF
DIFFERENT HEIGHTS into one rectangle — the model picker paging a catalog —
owns the rows between its content and the band, and erases them with this
before running the next page.
sourceraw docstring

columnsclj

(columns spec)

PURE: {:key-w :label-w} column widths for the transient grid. Every group's items share one key column and one description column, so flags and commands line up as a grid exactly like magit's popup.

PURE: `{:key-w :label-w}` column widths for the transient grid. Every group's
items share one key column and one description column, so flags and commands
line up as a grid exactly like magit's popup.
sourceraw docstring

draw-rule!clj

(draw-rule! g region row)
(draw-rule! g {:keys [left inner-w is-sideless]} row label)

One horizontal rule of the popup's chrome, on whatever frame region names.

A framed popup draws a capped ├───┤ separator that joins the host's rails; a SIDELESS band has no rails for a junction to join, so it draws the prompt's own inset line instead. One function, so a band and a modal wear the same chrome and neither grows a second copy of it.

One horizontal rule of the popup's chrome, on whatever frame `region` names.

A framed popup draws a capped `├───┤` separator that joins the host's rails;
a SIDELESS band has no rails for a junction to join, so it draws the prompt's
own inset line instead. One function, so a band and a modal wear the same
chrome and neither grows a second copy of it.
sourceraw docstring

geometryclj

(geometry region spec)

PURE: band-geometry for spec's OWN display rows — where every part of the popup lands.

PURE: [[band-geometry]] for `spec`'s OWN display rows — where every part of
the popup lands.
sourceraw docstring

heightclj

(height spec)

PURE: rows the popup needs INSIDE the host's frame — its opening separator and every display row (the padding blanks included). The closing rule and the hint bar are the host's OWN bottom chrome, so they are not counted; a host sizes its box with this before it paints one.

PURE: rows the popup needs INSIDE the host's frame — its opening separator
and every display row (the padding blanks included). The closing rule and the
hint bar are the host's OWN bottom chrome, so they are not counted; a host
sizes its box with this before it paints one.
sourceraw docstring

hint-pairsclj

(hint-pairs spec)

The footer THIS transient shows: what a command key does, the way out, and what a flag key does ONLY when the spec actually has a flag. A band whose keys are all commands (the draft band spends c/d/s/k on verbs) must not advertise a -key nothing responds to.

The footer THIS transient shows: what a command key does, the way out, and
what a flag key does ONLY when the spec actually has a flag. A band whose
keys are all commands (the draft band spends `c`/`d`/`s`/`k` on verbs) must
not advertise a `-key` nothing responds to.
sourceraw docstring

item-argclj

(item-arg {:keys [type arg secret?]} value)

PURE: the trailing git-argument cell magit shows for a FLAG: (--no-verify) for a switch, (%topic=fix) for an option carrying value. nil for commands (a command contributes no argument) and for flags that name none.

A :secret? true option NEVER renders what it holds: an armed API key shows as (••••••), so a credential can be carried by a transient without being echoed onto the screen or into a screenshot.

PURE: the trailing git-argument cell magit shows for a FLAG: `(--no-verify)` for
a switch, `(%topic=fix)` for an option carrying `value`. nil for commands (a
command contributes no argument) and for flags that name none.

A `:secret? true` option NEVER renders what it holds: an armed API key shows
as `(••••••)`, so a credential can be carried by a transient without being
echoed onto the screen or into a screenshot.
sourceraw docstring

item-by-idclj

(item-by-id spec id)

PURE: the spec item carrying id, scanning every group in order. nil when the band does not offer that command right now — which is how a caller that fires a command WITHOUT a keystroke (a slash that names it) finds out the band never showed it.

PURE: the spec item carrying `id`, scanning every group in order. nil when
the band does not offer that command right now — which is how a caller that
fires a command WITHOUT a keystroke (a slash that names it) finds out the
band never showed it.
sourceraw docstring

item-by-keyclj

(item-by-key spec ch)

PURE: the spec item bound to single character ch (a Character or string), scanning every group in order. nil when nothing is bound.

PURE: the spec item bound to single character `ch` (a Character or string),
scanning every group in order. nil when nothing is bound.
sourceraw docstring

key-glyphclj

(key-glyph {:keys [type key]})

PURE: the key column glyph magit paints for one item. FLAGS (:switch / :option) carry magit's leading --h, -t — so a toggle can never be mistaken for a fire-once verb; COMMANDS (sp/command-types) show the bare key. Tolerant of a partial item: an unknown type is not a command.

PURE: the key column glyph magit paints for one item. FLAGS (`:switch` /
`:option`) carry magit's leading `-` — `-h`, `-t` — so a toggle can never be
mistaken for a fire-once verb; COMMANDS (`sp/command-types`) show the bare
key. Tolerant of a partial item: an unknown type is not a command.
sourceraw docstring

layoutclj

(layout spec)
(layout spec region)

PURE: everything ONE frame needs from spec, computed ONCE — the display rows, the panes they are dealt into for region, the grid columns every row aligns to, the hint-pairs its footer shows, the height a host sizes its box with, and the keystroke index its key loop dispatches on.

Painting used to walk the whole spec twice per frame (once for geometry, once for the body) and scan every group again for each keystroke; a run builds this once and the loop reads it.

Without a region the layout is the single column: nothing has told it how wide or how tall the host is, so nothing may wrap.

PURE: everything ONE frame needs from `spec`, computed ONCE — the display
[[rows]], the [[panes]] they are dealt into for `region`, the grid [[columns]]
every row aligns to, the [[hint-pairs]] its footer shows, the [[height]] a
host sizes its box with, and the keystroke index its key loop dispatches on.

Painting used to walk the whole spec twice per frame (once for geometry,
once for the body) and scan every group again for each keystroke; a run
builds this once and the loop reads it.

Without a `region` the layout is the single column: nothing has told it how
wide or how tall the host is, so nothing may wrap.
sourceraw docstring

paint!clj

(paint! host region spec state)

Paint ONE frame of spec at state into region on host. Pure geometry, one pass, no key handling — a host that owns its own event loop embeds a transient with this plus toggle; run! is the batteries-included loop. A host that paints many frames computes layout once instead.

Paint ONE frame of `spec` at `state` into `region` on `host`. Pure geometry,
one pass, no key handling — a host that owns its own event loop embeds a
transient with this plus `toggle`; `run!` is the batteries-included loop.
A host that paints many frames computes [[layout]] once instead.
sourceraw docstring

pane-countclj

(pane-count spec region)

PURE: how many side-by-side panes spec is dealt into inside region.

EVERY CATEGORY GETS ITS OWN COLUMN: as many panes as the spec has groups, so a heading is never stacked under another heading while the terminal is wide enough to stand them side by side. The only bound is the width one pane needs for its own key and label columns; when the groups outnumber that capacity the leftovers are packed by panes. No region ⇒ one column.

PURE: how many side-by-side panes `spec` is dealt into inside `region`.

EVERY CATEGORY GETS ITS OWN COLUMN: as many panes as the spec has groups, so
a heading is never stacked under another heading while the terminal is wide
enough to stand them side by side. The only bound is the width one pane needs
for its own key and label columns; when the groups outnumber that capacity the
leftovers are packed by [[panes]]. No region ⇒ one column.
sourceraw docstring

pane-widthclj

(pane-width region spec n)

PURE: the columns ONE of n panes gets inside region's inner width, the gaps between them already paid for.

A pane is only as wide as its own grid needs — gutter, key column, label — so the columns sit BESIDE each other like which-key's, instead of one heading floating alone at the far edge of a wide terminal. The even split is the ceiling, never the target.

PURE: the columns ONE of `n` panes gets inside `region`'s inner width, the
gaps between them already paid for.

A pane is only as wide as its own grid needs — gutter, key column, label — so
the columns sit BESIDE each other like which-key's, instead of one heading
floating alone at the far edge of a wide terminal. The even split is the
ceiling, never the target.
sourceraw docstring

panesclj

(panes spec n)

PURE: spec's groups dealt into at most n side-by-side panes, in order, ONE GROUP PER PANE while there is a pane for it — a category IS a column, heading and verbs together, so the grid reads like which-key's own. Only when there are more groups than panes are neighbours packed together, balanced by row count and still NEVER splitting a group.

Every pane is padded with blanks to the tallest, so pane j row i is always the cell at that grid position and a painter walks a rectangle instead of a ragged list.

n = 1 is exactly rows: one pane IS the single column.

PURE: `spec`'s groups dealt into at most `n` side-by-side panes, in order,
ONE GROUP PER PANE while there is a pane for it — a category IS a column,
heading and verbs together, so the grid reads like which-key's own. Only when
there are more groups than panes are neighbours packed together, balanced by
row count and still NEVER splitting a group.

Every pane is padded with blanks to the tallest, so pane `j` row `i` is always
the cell at that grid position and a painter walks a rectangle instead of a
ragged list.

`n` = 1 is exactly [[rows]]: one pane IS the single column.
sourceraw docstring

prompt-rowsclj

Rows the session's PROMPT box occupies at its resting single-line size: its two rules and one text row (render/input-pad-y is 0). A caller that knows the LIVE height — screen's input-box-h, which grows with what is typed — passes that instead, so the band stays glued to the prompt however tall it got.

Rows the session's PROMPT box occupies at its resting single-line size: its two
rules and one text row (`render/input-pad-y` is 0). A caller that knows the
LIVE height — `screen`'s `input-box-h`, which grows with what is typed — passes
that instead, so the band stays glued to the prompt however tall it got.
sourceraw docstring

rowsclj

(rows spec)

PURE: the popup's display rows, top to bottom — {:kind :header :text}, {:kind :item :item} and {:kind :blank} spacers between groups, wrapped in the band-body-pad blanks the band breathes with.

The title carries its OWN rule underneath (see geometry), so the first group header needs no blank margin of its own; groups after it still get one from the trailing blank of the group before.

PURE: the popup's display rows, top to bottom — `{:kind :header :text}`,
`{:kind :item :item}` and `{:kind :blank}` spacers between groups, wrapped in
the [[band-body-pad]] blanks the band breathes with.

The title carries its OWN rule underneath (see `geometry`), so the first
group header needs no blank margin of its own; groups after it still get one
from the trailing blank of the group before.
sourceraw docstring

run!clj

(run! {:keys [read-key! refresh!] :as host} region spec)

Paint spec into region on host and run its key loop until an ACTION fires or the user cancels. Returns {:action id :switches #{…} :options {…}}, or nil on Esc. Flags toggle in place and keep the popup open; an OPTION calls the spec's :read-option for a value.

The contract is checked ONCE per run, here: an illegal spec, an unpaintable region or a host that cannot answer keystrokes throws :vis/transient-invalid-spec / -invalid-region / -invalid-host before a single cell is painted, and a :read-option that hands back something no row can carry throws :vis/transient-invalid-option instead of painting it. Not once per keystroke: the loop below runs on data already known legal.

Paint `spec` into `region` on `host` and run its key loop until an ACTION
fires or the user cancels. Returns `{:action id :switches #{…} :options {…}}`,
or nil on Esc. Flags toggle in place and keep the popup open; an OPTION calls
the spec's `:read-option` for a value.

The contract is checked ONCE per run, here: an illegal spec, an unpaintable
region or a host that cannot answer keystrokes throws
`:vis/transient-invalid-spec` / `-invalid-region` / `-invalid-host` before a
single cell is painted, and a `:read-option` that hands back something no row
can carry throws `:vis/transient-invalid-option` instead of painting it. Not
once per keystroke: the loop below runs on data already known legal.
sourceraw docstring

toggleclj

(toggle spec state ch)

PURE reducer for ONE keystroke against a transient state ({:switches #{ids} :options {id val}}). Returns a map whose :kind tells the impure caller what to do next: {:kind :continue :state s'} a SWITCH flipped (or an unbound key — no-op) {:kind :option :item it} an OPTION was hit; caller reads a value then re-enters with it stored under [:options id] {:kind :action :item it} an ACTION fires; caller runs it and closes. Switches are the only kind this fn mutates; options/actions leave state untouched (the impure loop finishes their job).

PURE reducer for ONE keystroke against a transient `state`
(`{:switches #{ids} :options {id val}}`). Returns a map whose `:kind` tells
the impure caller what to do next:
  {:kind :continue :state s'}  a SWITCH flipped (or an unbound key — no-op)
  {:kind :option   :item it}   an OPTION was hit; caller reads a value then
                               re-enters with it stored under [:options id]
  {:kind :action   :item it}   an ACTION fires; caller runs it and closes.
Switches are the only kind this fn mutates; options/actions leave `state`
untouched (the impure loop finishes their job).
sourceraw docstring

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
Move to previous article
Move to next article
Ctrl+/Jump to the search field
× close