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.(band-geometry {:keys [hint-row min-row]} n)PURE: which row of region every part of a band n display rows tall
lands on.
:sep-row the band's opening separator
:title-row the bold title, alone on its row
: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.
PURE: which row of `region` every part of a band `n` display rows tall lands on. `:sep-row` the band's opening separator `:title-row` the bold title, alone on its row `: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.
(band-region cols rows content-top)PURE: the rectangle an in-session BAND paints into on a cols×rows
terminal whose content starts at content-top.
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). :hint-row is the prompt box's own closing rule (always rows - 3,
whatever height the editor grew to), which keeps the echo area's two footer
rows below the band alive; :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`. 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). `:hint-row` is the prompt box's own closing rule (always `rows - 3`, whatever height the editor grew to), which keeps the echo area's two footer rows below the band alive; `:min-row` is the floor, so however tall the band the header and the top of the transcript stay on screen.
(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.
(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 and no paper of its
own: it wipes the FULL terminal width, because anything it does not repaint
is the live transcript showing through between its rules.
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 and no paper of its own: it wipes the FULL terminal width, because anything it does not repaint is the live transcript showing through between its rules. 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.
(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.(draw-rule! g {:keys [left inner-w is-sideless]} row)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.
(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.
(height spec)PURE: rows the popup needs INSIDE the host's frame — its opening separator, the title, the title's rule, and every display row. 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, the title, the title's rule, and every display row. 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.
(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.
(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.
(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.
(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.
(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.
(layout spec)PURE: everything ONE frame needs from spec, computed ONCE — the display
rows and how many there are, 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.
PURE: everything ONE frame needs from `spec`, computed ONCE — the display [[rows]] and how many there are, 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.
(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.
(rows spec)PURE: the popup's display rows, top to bottom — {:kind :header :text},
{:kind :item :item} and {:kind :blank} spacers between groups.
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.
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.(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.(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).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 |