Reusable header/tab UI components.
Each drawable here is a self-contained fn that paints into a Lanterna
TextGraphics and — when asked — registers its OWN click region, so the
same widget can't drift between call sites (the bug that left the tab
close ✕ painted in one place and made clickable in another, or not at
all). Two kinds live here:
truncate-with-ellipsis, center-paddedclose-button!, tab-cell!Components own their visual contract AND their interaction contract
(the click region's :kind), keeping header.clj a thin layout caller.
Reusable header/tab UI components. Each drawable here is a self-contained fn that paints into a Lanterna `TextGraphics` and — when asked — registers its OWN click region, so the same widget can't drift between call sites (the bug that left the tab close `✕` painted in one place and made clickable in another, or not at all). Two kinds live here: - text layout helpers — `truncate-with-ellipsis`, `center-padded` - drawable components — `close-button!`, `tab-cell!` Components own their visual contract AND their interaction contract (the click region's `:kind`), keeping `header.clj` a thin layout caller.
(band-rule! g row cols)(band-rule! g row cols fg)Paint a full-width single-line horizontal rule across cols on row.
Paint a full-width single-line horizontal rule across `cols` on `row`.
(button! g col row label kind)(button! g
col
row
label
kind
{:keys [extra danger? accent? register?] :or {register? true}})Generic clickable button: paint label (already space-padded, e.g. " < ") as
a filled accent cap at (col,row) — brighter on hover — AND register its click
region under :kind, plus any identity keys in extra (merged into both the
region and the hover match). One widget owns its look, its hover, and its
click, so they can't drift apart. Returns the consumed width.
Hover is detected by matching the registered :kind and every extra key
against cr/hovered, so two buttons of the same kind but different ids stay
independent.
Generic clickable button: paint `label` (already space-padded, e.g. " < ") as a filled accent cap at (col,row) — brighter on hover — AND register its click region under `:kind`, plus any identity keys in `extra` (merged into both the region and the hover match). One widget owns its look, its hover, and its click, so they can't drift apart. Returns the consumed width. Hover is detected by matching the registered `:kind` and every `extra` key against `cr/hovered`, so two buttons of the same kind but different ids stay independent.
(center-padded s cell-w)Centre s inside a cell-w-wide cell with vh/tab-entry-padding
reserved on each side; ellipsises overflow.
Centre `s` inside a `cell-w`-wide cell with `vh/tab-entry-padding` reserved on each side; ellipsises overflow.
(close-button! g col row _tab-fg _tab-bg workspace-id register?)Draw a ✕ close affordance (space-padded ✕, no divider) at (col,row).
At rest the ✕ sits on its OWN chip — the shared button-fg/button-bg
pair every other TUI button uses at rest — so it reads as a SEPARATE
button pinned to the tab's right edge instead of blending into the tab
surface (a gray ✕ vanished on the inactive near-white tab; a white ✕
melted into the active blue tab). The dark button-fg glyph stays legible
on the light chip, and the chip's contrast against BOTH tab backgrounds is
the affordance.
On hover the cap escalates to a red pill (close-button-hover-fg bg behind
a white header-active-tab-fg ✕) to signal the destructive click.
Registers its :close-tab click region for workspace-id. Returns the
consumed width (close-button-width).
Draw a ` ✕ ` close affordance (space-padded ✕, no divider) at (col,row). At rest the ✕ sits on its OWN chip — the shared `button-fg`/`button-bg` pair every other TUI button uses at rest — so it reads as a SEPARATE button pinned to the tab's right edge instead of blending into the tab surface (a gray ✕ vanished on the inactive near-white tab; a white ✕ melted into the active blue tab). The dark `button-fg` glyph stays legible on the light chip, and the chip's contrast against BOTH tab backgrounds is the affordance. On hover the cap escalates to a red pill (`close-button-hover-fg` bg behind a white `header-active-tab-fg` ✕) to signal the destructive click. Registers its `:close-tab` click region for `workspace-id`. Returns the consumed width (`close-button-width`).
Cells a close-button! occupies: a leading space + 1-col ✕ + a
trailing space. No divider — the bare ✕ reads as a button, not |x.
Cells a `close-button!` occupies: a leading space + 1-col `✕` + a trailing space. No divider — the bare ✕ reads as a button, not `|x`.
(context-overlay! g cols rows _ctx scroll _expanded)Dialog showing the session's working memory - :session/tasks AND
:session/facts - the W3 user-visible panel (F2). Uses the shared
dialogs/draw-dialog-chrome! + dialog-layout so it looks like every other
modal (shadow, border, accent title bar, hint row). Title carries a
tasks-done + facts count summary; a TASKS section (status-sorted, colored
glyphs, acceptance sub-lines, verify badges) then a FACTS section (active
first, > N for file-bearing facts). expanded is the set of fact keys (as
strings) whose file list is unfolded; each file-bearing fact's meta row gets
a :toggle-fact-files click region so a click folds/unfolds its paths. The
dialog SIZES to its content (grows to fit, clamped to the terminal); overflow
scrolls through the shared scrollable-dialog-body! (same plumbing as F1
help). ctx is {:tasks ... :facts ...}. Returns
{:scroll :max-scroll :selectable-ranges}.
Dialog showing the session's working memory - `:session/tasks` AND
`:session/facts` - the W3 user-visible panel (F2). Uses the shared
`dialogs/draw-dialog-chrome!` + `dialog-layout` so it looks like every other
modal (shadow, border, accent title bar, hint row). Title carries a
tasks-done + facts count summary; a TASKS section (status-sorted, colored
glyphs, acceptance sub-lines, verify badges) then a FACTS section (active
first, `> N` for file-bearing facts). `expanded` is the set of fact keys (as
strings) whose file list is unfolded; each file-bearing fact's meta row gets
a `:toggle-fact-files` click region so a click folds/unfolds its paths. The
dialog SIZES to its content (grows to fit, clamped to the terminal); overflow
scrolls through the shared `scrollable-dialog-body!` (same plumbing as F1
help). `ctx` is `{:tasks ... :facts ...}`. Returns
`{:scroll :max-scroll :selectable-ranges}`.(dialog-close-button! g bounds kind)Draw a clickable ✕ close chip at a dialog's top-right title corner —
the SAME button every modal dialog shows — and register its kind
click region (:toggle-help / :toggle-tasks) so a mouse click
dismisses the overlay. Inverted title chip at rest; red pill on hover.
Returns nil.
Draw a clickable `✕` close chip at a dialog's top-right title corner — the SAME button every modal dialog shows — and register its `kind` click region (`:toggle-help` / `:toggle-tasks`) so a mouse click dismisses the overlay. Inverted title chip at rest; red pill on hover. Returns nil.
(find-bar! g cols text-top search)Browser-style in-session find WIDGET: a single-line BORDERED box, right-aligned at the top of the messages area, holding a WHITE input field (the live query), the i/N match count, and spaced Aa/◀/▶/✕ glyph buttons (each its own click region via button!, so the mouse drives the same :search-* events as C-p / C-n). search is app-db's :search map; no-op when inactive.
Returns the cursor cell [col row] from find-bar-cursor (nil when
inactive) so callers can park the terminal cursor in the query field.
Browser-style in-session find WIDGET: a single-line BORDERED box, right-aligned at the top of the messages area, holding a WHITE input field (the live query), the i/N match count, and spaced Aa/◀/▶/✕ glyph buttons (each its own click region via `button!`, so the mouse drives the same `:search-*` events as C-p / C-n). `search` is app-db's `:search` map; no-op when inactive. Returns the cursor cell [col row] from `find-bar-cursor` (nil when inactive) so callers can park the terminal cursor in the query field.
(find-bar-cursor cols text-top search)Screen cell [col row] where the terminal cursor belongs while the find bar
is ACTIVE — the end of the typed query inside the white field — or nil
when inactive. Both render paths (full frame + live/partial) consult this
AFTER placing the prompt-input cursor: while the bar owns the keyboard the
cursor visibly moves INTO it, and because the prompt's logical
{:lines :crow :ccol} state is never touched by the search key-loop,
closing the bar lands the cursor back exactly where it was.
Screen cell [col row] where the terminal cursor belongs while the find bar
is ACTIVE — the end of the typed query inside the white field — or nil
when inactive. Both render paths (full frame + live/partial) consult this
AFTER placing the prompt-input cursor: while the bar owns the keyboard the
cursor visibly moves INTO it, and because the prompt's logical
`{:lines :crow :ccol}` state is never touched by the search key-loop,
closing the bar lands the cursor back exactly where it was.(header-badge! g col row glyph kind register?)Paint a clickable header chip glyph at (col,row) and register a click
region of kind (e.g. :toggle-tasks / :toggle-help). Brightens on hover.
Returns the consumed width. A terminal-safe, always-visible stand-in for
the F1/F2 accelerators.
Paint a clickable header chip `glyph` at (col,row) and register a click region of `kind` (e.g. :toggle-tasks / :toggle-help). Brightens on hover. Returns the consumed width. A terminal-safe, always-visible stand-in for the F1/F2 accelerators.
(help-overlay! g cols rows scroll)Draw the keyboard-shortcut help as a dialog, using the shared
dialogs/draw-dialog-chrome! + dialog-layout so it matches the F2
context panel (shadow, border, accent title bar, centered hint row).
The body (built by box-grid-lines) is a sectioned 2-column grid: each
group rides under a bold full-width banner, so the card reads as labelled
SECTIONS instead of bare rows with blank gaps.
The chrome is drawn FIRST (the shared 5-arg arity picks a fixed modal
footprint and IGNORES the passed line count), so we learn the real inner
width, then size the desc column to what actually FITS — reserving one
scrollbar-lane column — and let box-grid-lines WRAP long descriptions
instead of clipping them. The scroll extent is the wrapped line count, so
scrolling always covers every visual line. Registers only its close-button
click region; the caller dismisses it (Ctrl+H / F1 / any key). Returns
{:scroll :max-scroll} so the caller can feed the clamp back, exactly like
context-overlay!.
Draw the keyboard-shortcut help as a dialog, using the shared
`dialogs/draw-dialog-chrome!` + `dialog-layout` so it matches the F2
context panel (shadow, border, accent title bar, centered hint row).
The body (built by `box-grid-lines`) is a sectioned 2-column grid: each
group rides under a bold full-width banner, so the card reads as labelled
SECTIONS instead of bare rows with blank gaps.
The chrome is drawn FIRST (the shared 5-arg arity picks a fixed modal
footprint and IGNORES the passed line count), so we learn the real inner
width, then size the desc column to what actually FITS — reserving one
scrollbar-lane column — and let `box-grid-lines` WRAP long descriptions
instead of clipping them. The scroll extent is the wrapped line count, so
scrolling always covers every visual line. Registers only its close-button
click region; the caller dismisses it (Ctrl+H / F1 / any key). Returns
`{:scroll :max-scroll}` so the caller can feed the clamp back, exactly like
`context-overlay!`.(id-badge! g col row text full-uuid register?)Paint the session-id COPY BUTTON text at (col,row) as a filled chip — via
the shared button!, so it reads as the SAME control as the F1/F2 buttons
instead of bare text — and register its :copy-id click region carrying the
FULL uuid (the click handler drops it on the clipboard). Brightens
to the accent on hover. No-op for blank text.
Paint the session-id COPY BUTTON `text` at (col,row) as a filled chip — via the shared `button!`, so it reads as the SAME control as the F1/F2 buttons instead of bare text — and register its `:copy-id` click region carrying the FULL uuid (the click handler drops it on the clipboard). Brightens to the accent on hover. No-op for blank text.
(nav-arrow! g row col glyph direction register?)Paint a workspace-overflow navigation arrow glyph at (col,row) as a FILLED
BUTTON CHIP - routed through the shared button!, so the < > read as the
SAME control as the F1/F2 and id-copy buttons (inverted cap at rest, lifts to
the accent + bold on hover) instead of a bare character. Registers its
:workspace-entry click region keyed by direction (:prev / :next), so a
click still cycles the visible tab window.
Paint a workspace-overflow navigation arrow `glyph` at (col,row) as a FILLED BUTTON CHIP - routed through the shared `button!`, so the < > read as the SAME control as the F1/F2 and id-copy buttons (inverted cap at rest, lifts to the accent + bold on hover) instead of a bare character. Registers its `:workspace-entry` click region keyed by `direction` (`:prev` / `:next`), so a click still cycles the visible tab window.
(notification-slot! g col row text level)Paint the header's left-slot notification/status text at (col,row),
colored by level (bold). No-op for blank text.
Paint the header's left-slot notification/status `text` at (col,row), colored by `level` (bold). No-op for blank text.
(scrollable-dialog-body! g
lines
{:keys [content-top content-h hint-row sb-col
body-right]}
scroll
paint-line)Paint the scroll plumbing both modal overlays (F1 help, F2 context) share:
clamp scroll to [0, (- (count lines) content-h)], window lines by that
effective offset and paint each visible row via paint-line (a
(fn [screen-row-i line])), draw the shared scrollbar/draw! in sb-col
when the content overflows, and a right-aligned N-M / total position hint
on hint-row (anchored to body-right). geom is
{:content-top :content-h :hint-row :sb-col :body-right}. Returns
{:scroll :max-scroll :sb? :shown-n} so callers feed the clamp back and
derive their own geometry (e.g. F2's selectable ranges).
Paint the scroll plumbing both modal overlays (F1 help, F2 context) share:
clamp `scroll` to `[0, (- (count lines) content-h)]`, window `lines` by that
effective offset and paint each visible row via `paint-line` (a
`(fn [screen-row-i line])`), draw the shared `scrollbar/draw!` in `sb-col`
when the content overflows, and a right-aligned `N-M / total` position hint
on `hint-row` (anchored to `body-right`). `geom` is
`{:content-top :content-h :hint-row :sb-col :body-right}`. Returns
`{:scroll :max-scroll :sb? :shown-n}` so callers feed the clamp back and
derive their own geometry (e.g. F2's selectable ranges).(tab-cell! g
{:keys [left row width label tab-no status active? workspace-id index
register? closable?]
:or {closable? true}})Draw one workspace tab into the band at [left,row] spanning width cells:
status-ok) under a steady,
solid underline — the "ready to check" outline;
nil -> NO border — an idle tab keeps the plain surface
background and blends in;<num> | <label>, given close-button-width fewer cells
so it never collides with the ✕;close-button! pinned to the right edge (when the
cell is wide enough to host both a title and the button).Registers the cell's :workspace-entry region FIRST, then the ✕
:close-tab region ON TOP, so a click on the glyph wins the topmost
(last-registered) lookup and closes the tab instead of selecting it.
opts keys: :left :row :width :label :tab-no :status :active? :workspace-id
:index :register? :closable?
Draw one workspace tab into the band at [left,row] spanning `width` cells:
- the bg slab (active = inverted bold; inactive = dim italic);
- a STATUS BORDER painted as a per-column UNDERLINE along the bottom of
the cell (this replaced the old 2-col status prefix / edge rails, so the
number now sits FLUSH — no leading gap). The border is all theme
colour (no block glyphs, nothing to clash with the surface) and IS the
status cue:
:running -> a CONTINUOUS amber underline with a bright band sweeping
left→right along the bottom edge (the "working" crawl,
smooth — no gaps, no flicker);
:ready -> the whole cell glows GREEN (`status-ok`) under a steady,
solid underline — the "ready to check" outline;
nil -> NO border — an idle tab keeps the plain surface
background and blends in;
- the centered `<num> | <label>`, given `close-button-width` fewer cells
so it never collides with the ✕;
- an ALWAYS-VISIBLE `close-button!` pinned to the right edge (when the
cell is wide enough to host both a title and the button).
Registers the cell's `:workspace-entry` region FIRST, then the ✕
`:close-tab` region ON TOP, so a click on the glyph wins the topmost
(last-registered) lookup and closes the tab instead of selecting it.
`opts` keys: :left :row :width :label :tab-no :status :active? :workspace-id
:index :register? :closable?(tab-divider! g col row)Paint a 1-col dotted ┊ divider (between tabs, or between header-cluster
chips) at (col,row). Painted in the high-contrast accent (dark on a light
terminal, light on a dark one) so the sparse dotted glyph is actually
visible — footer-fg washed it out.
Paint a 1-col dotted `┊` divider (between tabs, or between header-cluster chips) at (col,row). Painted in the high-contrast accent (dark on a light terminal, light on a dark one) so the sparse dotted glyph is actually visible — `footer-fg` washed it out.
(title! g row left width text)Paint an INERT, centered header title in [left,row] over width cells —
bold on the header surface, ellipsised on overflow. Registers no click
region: a single-workspace title is not a switcher and must not look or
behave like one.
Paint an INERT, centered header title in [left,row] over `width` cells — bold on the header surface, ellipsised on overflow. Registers no click region: a single-workspace title is not a switcher and must not look or behave like one.
(truncate-with-ellipsis s max-cols)Truncate s so its display width fits in max-cols. When truncation
actually happens, append vh/workspace-ellipsis so overflow is visible.
Truncate `s` so its display width fits in `max-cols`. When truncation actually happens, append `vh/workspace-ellipsis` so overflow is visible.
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 |