Low-level drawing primitives wrapping Lanterna TextGraphics. All rendering code should use these instead of raw Java interop.
String width here means display columns the terminal will paint,
NOT Java .length() and NOT (count s). Those count UTF-16 code units;
a single emoji like 📁 is two code units but two columns,
a CJK glyph like 日 is one code unit but two columns, a flag
sequence 🇵🇱 is four code units, one grapheme,
two columns. Mix the two and your alignment drifts.
Use display-width and truncate-cols for any string that could
contain non-ASCII. Plain count/subs are still allowed for ASCII
internals (box-drawing strings we authored, single-glyph keystroke
labels, etc.) where the answer is identical and the call is a hot path.
Low-level drawing primitives wrapping Lanterna TextGraphics. All rendering code should use these instead of raw Java interop. Width-math contract =================== String width here means **display columns the terminal will paint**, NOT Java `.length()` and NOT (count s). Those count UTF-16 code units; a single emoji like 📁 is two code units but two columns, a CJK glyph like 日 is one code unit but two columns, a flag sequence 🇵🇱 is four code units, one grapheme, two columns. Mix the two and your alignment drifts. Use `display-width` and `truncate-cols` for any string that could contain non-ASCII. Plain `count`/`subs` are still allowed for ASCII internals (box-drawing strings we authored, single-glyph keystroke labels, etc.) where the answer is identical and the call is a hot path.
(align s width mode)Align s to width display columns by mode:
:left pad on the right (default for any unknown mode)
:right pad on the left
:center pad both sides
:justify full-justify the words (flush both margins).
Align `s` to `width` display columns by `mode`: :left pad on the right (default for any unknown mode) :right pad on the left :center pad both sides :justify full-justify the words (flush both margins).
(center-text s w)Center string within w terminal columns, padding both sides.
Truncates (column-aware) if too wide.
Center string within `w` terminal columns, padding both sides. Truncates (column-aware) if too wide.
(clear-styles! g)Remove all active text styles.
Remove all active text styles.
(col-prefix-end s max-cols)Return the char-index i such that (subs s 0 i) is the longest
prefix of s whose display-width is <= max-cols AND that does NOT
split a grapheme cluster.
Use this when you need both the prefix and the position of the
un-consumed remainder (e.g. in word-wrapping). For just the prefix,
truncate-cols is friendlier.
Returns 0 for nil/empty or non-positive max-cols.
Return the char-index `i` such that `(subs s 0 i)` is the longest prefix of `s` whose `display-width` is <= `max-cols` AND that does NOT split a grapheme cluster. Use this when you need both the prefix and the position of the un-consumed remainder (e.g. in word-wrapping). For just the prefix, `truncate-cols` is friendlier. Returns 0 for nil/empty or non-positive `max-cols`.
(cursor-pos col row)Create a TerminalPosition for cursor placement.
Create a TerminalPosition for cursor placement.
(disable! g & modifiers)Disable one or more text styles.
Disable one or more text styles.
(display-width s)Number of terminal columns s will occupy when painted by lanterna.
Built on TextCharacter/fromString, the same routine
AbstractTextGraphics.putString uses internally after PR #625, so
what we measure matches what the renderer actually paints - grapheme
clusters honoured (BreakIterator-based), CJK + emoji counted as two
columns, ASCII as one.
Inline span sentinels (INLINE_*_ON/OFF, range ...)
count as zero columns: they're invisible style toggles, never
painted, never advance the cursor.
Stray ASCII control bytes (0x00-0x1F) get sanitized to /
before reaching Lanterna - see sanitize-control-chars for the
why. Without that, a single rogue in a paint string used to
take down the entire render thread.
Returns 0 for nil/empty input.
Number of terminal columns `s` will occupy when painted by lanterna. Built on `TextCharacter/fromString`, the same routine `AbstractTextGraphics.putString` uses internally after PR #625, so what we measure matches what the renderer actually paints - grapheme clusters honoured (BreakIterator-based), CJK + emoji counted as two columns, ASCII as one. Inline span sentinels (`INLINE_*_ON`/`OFF`, range ...) count as zero columns: they're invisible style toggles, never painted, never advance the cursor. Stray ASCII control bytes (0x00-0x1F) get sanitized to `/` before reaching Lanterna - see `sanitize-control-chars` for the why. Without that, a single rogue ` ` in a paint string used to take down the entire render thread. Returns 0 for nil/empty input.
(dot-cell! g col row fg)Overlay a bottom-flush ▁ mark in fg colour onto the SINGLE cell at
(col,row), keeping its existing background, but ONLY when that cell is
currently a blank space OR an existing ▁ mark. ▁ (LOWER ONE EIGHTH
BLOCK) sits flush on the cell's bottom edge — the SAME row as an
underline-cell! border — so the
running tab's marching marks line up with the ready tab's underline instead
of floating one line high like a baseline . did (which read as flicker).
Only blank padding cells are touched, so the number, | separator,
label, and close ✕ (non-blank) are never clobbered — but a prior ▁ may be
RE-COLOURED, which lets a running tab paint a dim base line then overlay a
bright marching band on top. No-op off-screen or over
any non-blank glyph.
Overlay a bottom-flush `▁` mark in `fg` colour onto the SINGLE cell at (col,row), keeping its existing background, but ONLY when that cell is currently a blank space OR an existing `▁` mark. `▁` (LOWER ONE EIGHTH BLOCK) sits flush on the cell's bottom edge — the SAME row as an `underline-cell!` border — so the running tab's marching marks line up with the ready tab's underline instead of floating one line high like a baseline `.` did (which read as flicker). Only blank padding cells are touched, so the number, ` | ` separator, label, and close ✕ (non-blank) are never clobbered — but a prior `▁` may be RE-COLOURED, which lets a running tab paint a dim base line then overlay a bright marching band on top. No-op off-screen or over any non-blank glyph.
(draw-box! g left top w h)Draw a single-line bordered box at (left, top) of size wxh. Draws corners, edges. Does NOT fill interior.
Draw a single-line bordered box at (left, top) of size wxh. Draws corners, edges. Does NOT fill interior.
(draw-centered! g left row width text)Draw text centered at row within [left, left+width).
Draw text centered at row within [left, left+width).
(draw-selection-marker! g col row selected?)(draw-selection-marker! g col row selected? marker-fg)Paint the selection marker at (col, row) when selected? is
truthy. Unselected rows get nothing — the surrounding row fill is
expected to already cover those cells.
The glyph is rendered BOLD. Provide marker-fg to give it its
own color (e.g. dialog-hint-key); when omitted, whatever fg is
currently set on g is used.
See the Selection marker block above for the project-wide
rationale. Callers that need the prefix as a STRING (to inline
into a row label) should use selection-prefix instead.
Paint the selection marker at (col, row) when `selected?` is truthy. Unselected rows get nothing — the surrounding row fill is expected to already cover those cells. The glyph is rendered BOLD. Provide `marker-fg` to give it its own color (e.g. `dialog-hint-key`); when omitted, whatever fg is currently set on `g` is used. See the `Selection marker` block above for the project-wide rationale. Callers that need the prefix as a STRING (to inline into a row label) should use `selection-prefix` instead.
(draw-separator! g left right row)Draw a horizontal separator with T-junctions at left/right edges.
Draw a horizontal separator with T-junctions at left/right edges.
(draw-space-around! g left row width items)Draw items spread across row within [left, left+width) with space-around.
Draw items spread across row within [left, left+width) with space-around.
(draw-space-between! g left row width items)Draw items spread across row within [left, left+width) with space-between.
Draw items spread across row within [left, left+width) with space-between.
(draw-tabs! g
tabs
{:keys [left row width active-id gap fg bg active-fg active-bg
inactive-fg inactive-bg bordered?]})Draw a tab strip and return its geometry.
Required opts: :left, :row, :width, :active-id, :fg, :bg,
:active-fg, :active-bg, :inactive-fg, :inactive-bg.
Optional: :gap, :bordered?.
This primitive knows layout and drawing only. Callers own domain actions and click-region registration.
Draw a tab strip and return its geometry. Required opts: `:left`, `:row`, `:width`, `:active-id`, `:fg`, `:bg`, `:active-fg`, `:active-bg`, `:inactive-fg`, `:inactive-bg`. Optional: `:gap`, `:bordered?`. This primitive knows layout and drawing only. Callers own domain actions and click-region registration.
(enable! g & modifiers)Enable one or more text styles. modifiers are SGR constants (BOLD, ITALIC, etc.).
Enable one or more text styles. `modifiers` are SGR constants (BOLD, ITALIC, etc.).
(fill-rect! g col row w h)(fill-rect! g col row w h ch)Fill a rectangle at (col, row) of size wxh with the given char (default space).
Fill a rectangle at (col, row) of size wxh with the given char (default space).
(fold-cols s max-cols)Character-fold s into a vector of segments, each at most max-cols
display columns wide, never splitting a grapheme cluster.
This is a terminal-style SOFT WRAP, the soft-wrap primitive shared by
the agent code rail and the tool-result code blocks: unlike word-wrap it
never drops or reflows whitespace — the bytes are preserved exactly and a
break is inserted only at the column boundary. So a pathologically wide
single line (a one-line git_commit message arg, a wide clj_eval
value map) folds at the bubble edge instead of overflowing or being
clipped, while indentation and in-row column alignment survive.
The fold itself is lanterna's TerminalTextUtils/foldColumns — the same
grapheme/EAW-aware text-flow family the screen paints with (displayWidth
/ wordWrap / justify). A line already within budget comes back [s]
unchanged, so normal multi-line source is untouched and only the over-wide
rows fold. nil/empty returns [""].
Input carrying an ESC (0x1b) is returned unfolded — foldColumns is
plain-text-only (the grapheme splitter throws on ESC); callers that need
ANSI-aware width clip such rows separately.
Character-fold `s` into a vector of segments, each at most `max-cols` display columns wide, never splitting a grapheme cluster. This is a terminal-style SOFT WRAP, the soft-wrap primitive shared by the agent code rail and the tool-result code blocks: unlike word-wrap it never drops or reflows whitespace — the bytes are preserved exactly and a break is inserted only at the column boundary. So a pathologically wide single line (a one-line `git_commit` message arg, a wide `clj_eval` value map) folds at the bubble edge instead of overflowing or being clipped, while indentation and in-row column alignment survive. The fold itself is lanterna's `TerminalTextUtils/foldColumns` — the same grapheme/EAW-aware text-flow family the screen paints with (`displayWidth` / `wordWrap` / `justify`). A line already within budget comes back `[s]` unchanged, so normal multi-line source is untouched and only the over-wide rows fold. nil/empty returns `[""]`. Input carrying an ESC (0x1b) is returned unfolded — `foldColumns` is plain-text-only (the grapheme splitter throws on ESC); callers that need ANSI-aware width clip such rows separately.
(horiz-line n)Return a string of n horizontal box-drawing chars.
Return a string of `n` horizontal box-drawing chars.
(inline-sentinel? g)True when g (a single grapheme String) is one of the eight inline
span sentinels above. Cheap range check, no map lookup.
True when `g` (a single grapheme String) is one of the eight inline span sentinels above. Cheap range check, no map lookup.
(justify s width)(justify s width justify-last?)Word-wrap s to width columns, then FULL-JUSTIFY every line, leaving each
paragraph's final line left-aligned (the standard typographic convention).
Pass justify-last? true to stretch the final line too. Returns a vec of
lines.
Word-wrap `s` to `width` columns, then FULL-JUSTIFY every line, leaving each paragraph's final line left-aligned (the standard typographic convention). Pass `justify-last?` true to stretch the final line too. Returns a vec of lines.
(justify-line line width)Full-justify the words on line to EXACTLY width display columns by
distributing inter-word spaces (flush to both margins). A blank or
single-word line is left-aligned (right-padded) instead — nothing to
stretch.
Full-justify the words on `line` to EXACTLY `width` display columns by distributing inter-word spaces (flush to both margins). A blank or single-word line is left-aligned (right-padded) instead — nothing to stretch.
(pad-left s w)Pad string to w terminal columns, left-filling with spaces.
Truncates (column-aware) if too wide.
Pad string to `w` terminal columns, left-filling with spaces. Truncates (column-aware) if too wide.
(pad-right s w)Pad string to w terminal columns, right-filling with spaces.
Truncates (column-aware) if too wide.
Pad string to `w` terminal columns, right-filling with spaces. Truncates (column-aware) if too wide.
(paint-styled-line! g x y line base-fg base-bg code-fg code-bg)Paint a single line at (x, y) honouring inline span sentinels.
The line may contain interleaved text and INLINE_*_ON/OFF
sentinels (range \uE110...\uE117). Sentinels themselves are NEVER
painted; they toggle the SGR style (BOLD/ITALIC/CROSSED-OUT) or
the fg/bg colors (CODE) for the spans that follow.
base-fg / base-bg are the colors used for non-code spans.
Code spans force code-fg / code-bg until INLINE_CODE_OFF.
Robustness contract:
TextCharacter/fromString),
so column tracking is exact for emoji + CJK.g at entry (e.g.
a wrapping (p/styled g [p/ITALIC] ...) for blockquotes). Inline
toggles STACK on top of the inherited set: > **bold** inside a
quote renders as bold-italic, not bold-without-italic. The pre-fix
version called clearModifiers at entry and silently dropped the
wrapping italic - user-visible bug on every blockquote that
contained inline emphasis.put-str! call - the same as the old non-styled path - so the
hot path is not penalised for the common case.This is the entry point that lets the markdown renderer support
bold / italic / ~~strike~~ / code mid-line without
replacing the marker-prefix-per-line architecture above.
Paint a single line at (x, y) honouring inline span sentinels. The line may contain interleaved text and `INLINE_*_ON`/`OFF` sentinels (range \uE110...\uE117). Sentinels themselves are NEVER painted; they toggle the SGR style (BOLD/ITALIC/CROSSED-OUT) or the fg/bg colors (CODE) for the spans that follow. `base-fg` / `base-bg` are the colors used for non-code spans. Code spans force `code-fg` / `code-bg` until INLINE_CODE_OFF. Robustness contract: - Walks by GRAPHEME CLUSTER (lanterna's `TextCharacter/fromString`), so column tracking is exact for emoji + CJK. - INHERITS any SGR modifiers already enabled on `g` at entry (e.g. a wrapping `(p/styled g [p/ITALIC] ...)` for blockquotes). Inline toggles STACK on top of the inherited set: `> **bold**` inside a quote renders as bold-italic, not bold-without-italic. The pre-fix version called `clearModifiers` at entry and silently dropped the wrapping italic - user-visible bug on every blockquote that contained inline emphasis. - Unmatched / dangling sentinels (e.g. an OFF without a prior ON, or a line that ends mid-bold) are tolerated: at exit we restore exactly the inherited modifier set, so SGR state never leaks past the call. - When NO sentinel appears in the line this collapses to a single `put-str!` call - the same as the old non-styled path - so the hot path is not penalised for the common case. This is the entry point that lets the markdown renderer support **bold** / *italic* / ~~strike~~ / `code` mid-line without replacing the marker-prefix-per-line architecture above.
(put-str! g col row text)Draw a string at (col, row). Control characters are sanitized.
Draw a string at (col, row). Control characters are sanitized.
(selection-prefix selected?)Return the leading marker string for a list/menu row.
Use this for rows where the marker can be inlined into the body
text (simple list items, checkbox rows, slash-command rows). For
rows where the marker must live OUTSIDE a fixed-column body
(file/session pickers, provider/model cards) call
draw-selection-marker! from the caller's row loop instead.
Return the leading marker string for a list/menu row. Use this for rows where the marker can be inlined into the body text (simple list items, checkbox rows, slash-command rows). For rows where the marker must live OUTSIDE a fixed-column body (file/session pickers, provider/model cards) call `draw-selection-marker!` from the caller's row loop instead.
Two-col selection marker. Selected rows show •+space, unselected
rows show two spaces, so the body content stays column-aligned.
The glyph MUST be display-width 1 so glyph+space == 2 cols and
exactly fills the reserved gutter. • (U+2022) is East-Asian
ambiguous width; the lanterna fork (>= 3.1.5-vis.7) scores EAW=A
as NARROW by default, so • is 2 cols. (A regression in vis.6
briefly made it wide — the 'marker eats a character' bug.)
Two-col selection marker. Selected rows show `•`+space, unselected rows show two spaces, so the body content stays column-aligned. The glyph MUST be display-width 1 so glyph+space == 2 cols and exactly fills the reserved gutter. `•` (U+2022) is East-Asian *ambiguous* width; the lanterna fork (>= 3.1.5-vis.7) scores EAW=A as NARROW by default, so `• ` is 2 cols. (A regression in vis.6 briefly made it wide — the 'marker eats a character' bug.)
(set-char! g col row ch)Draw a single character at (col, row).
Draw a single character at (col, row).
(set-colors! g fg bg)Set both foreground and background color in one call.
Set both foreground and background color in one call.
(space-around items w)Distribute items across w terminal columns with equal space
around each item. Like CSS justify-content: space-around.
Distribute items across `w` terminal columns with equal space around each item. Like CSS justify-content: space-around.
(space-between items w)Distribute items across w terminal columns with equal gaps.
First item flush-left, last item flush-right, rest evenly spaced.
Like CSS justify-content: space-between.
Distribute items across `w` terminal columns with equal gaps. First item flush-left, last item flush-right, rest evenly spaced. Like CSS justify-content: space-between.
(status-mark! g col row glyph fg bg)Paint a 1-col status glyph in colour fg on bg at (col,row) and return
the next col (col + STATUS_WIDTH) so the caller can place the label right
after. The reusable mark behind settings rows + resource rows.
Paint a 1-col status `glyph` in colour `fg` on `bg` at (col,row) and return the next col (`col + STATUS_WIDTH`) so the caller can place the label right after. The reusable mark behind settings rows + resource rows.
(styled g styles & body)Draw with styles temporarily enabled. Restores previous styles after body. Usage: (styled g [BOLD ITALIC] (put-str! g 0 0 "hello"))
Draw with styles temporarily enabled. Restores previous styles after body. Usage: (styled g [BOLD ITALIC] (put-str! g 0 0 "hello"))
(tab-at layout col)Return the tab geometry under col, or nil. Expects tab-layout output.
Return the tab geometry under `col`, or nil. Expects `tab-layout` output.
(tab-display-label {:keys [id label dirty? state]})Compact label for a tab map.
Supported tab keys:
:label / :id - visible base label.:dirty? - appends a dirty marker.:state - appends a compact state badge for running / verified /
accepted / error tabs.This is terminal-column aware when later passed through tab-layout, so
callers may use emoji or CJK labels without hand-padding.
Compact label for a tab map. Supported tab keys: - `:label` / `:id` - visible base label. - `:dirty?` - appends a dirty marker. - `:state` - appends a compact state badge for running / verified / accepted / error tabs. This is terminal-column aware when later passed through `tab-layout`, so callers may use emoji or CJK labels without hand-padding.
(tab-layout tabs left width active-id)(tab-layout tabs left width active-id {:keys [gap]})Return tab geometry for a horizontal tab strip.
tabs is a seq of maps. Returned maps include :left, :width,
:active?, and :text truncated to fit :width. Width math uses terminal
columns, not Java chars. When the strip is too narrow, later tabs may get
zero width; draw helpers skip those safely.
Return tab geometry for a horizontal tab strip. `tabs` is a seq of maps. Returned maps include `:left`, `:width`, `:active?`, and `:text` truncated to fit `:width`. Width math uses terminal columns, not Java chars. When the strip is too narrow, later tabs may get zero width; draw helpers skip those safely.
(truncate-cols s max-cols)Return the longest prefix of s that fits in at most max-cols
terminal columns, never splitting a grapheme cluster.
Edge cases honoured:
max-cols <= 0 returns "".display-width is exactly max-cols. This keeps
pad-right / pad-left idempotent under repeated truncation.Return the longest prefix of `s` that fits in at most `max-cols` terminal columns, never splitting a grapheme cluster. Edge cases honoured: - nil or `max-cols <= 0` returns `""`. - If a double-width grapheme would straddle the cut, it is dropped (NOT half-included), and one space is appended in its place so the returned string's `display-width` is exactly `max-cols`. This keeps `pad-right` / `pad-left` idempotent under repeated truncation.
(underline-cell! g col row)(underline-cell! g col row fg)Add the UNDERLINE modifier to the SINGLE already-painted cell at (col,row),
preserving its glyph + background. Reads the cell back via getCharacter
and re-sets it with the modifier folded in, so callers can paint a
PER-COLUMN border (a steady / marching underline) that the global
enable!/UNDERLINE attribute — which would underline every cell touched
after it — can't express. No-op when the cell is empty/off-screen.
3-arity keeps the cell's own foreground (the underline inherits it).
4-arity RECOLOURS the foreground to fg first, so the underline — which a
terminal draws in the cell's foreground colour — renders in fg. Recolour
is uniform across blank padding AND glyph cells, giving ONE consistent
underline shape/weight everywhere (no block-glyph vs SGR-line mismatch).
Add the UNDERLINE modifier to the SINGLE already-painted cell at (col,row), preserving its glyph + background. Reads the cell back via `getCharacter` and re-sets it with the modifier folded in, so callers can paint a PER-COLUMN border (a steady / marching underline) that the global `enable!`/`UNDERLINE` attribute — which would underline every cell touched after it — can't express. No-op when the cell is empty/off-screen. 3-arity keeps the cell's own foreground (the underline inherits it). 4-arity RECOLOURS the foreground to `fg` first, so the underline — which a terminal draws in the cell's foreground colour — renders in `fg`. Recolour is uniform across blank padding AND glyph cells, giving ONE consistent underline shape/weight everywhere (no block-glyph vs SGR-line mismatch).
(v-center-offset content-h container-h)Compute vertical offset to center content-h rows within container-h rows.
Compute vertical offset to center `content-h` rows within `container-h` rows.
(with-style g styles body-fn)Execute body-fn (fn [g] ...) with the given styles enabled, then restore.
Returns the result of body-fn.
Execute `body-fn` (fn [g] ...) with the given styles enabled, then restore. Returns the result of `body-fn`.
(word-wrap s width)Greedy word-wrap s into a vec of lines, each fitting width DISPLAY
columns. Breaks on whitespace; a token wider than width is hard-split at
grapheme boundaries (never mid-cluster); embedded newlines are honoured.
Blank input or width <= 0 yields [""].
Greedy word-wrap `s` into a vec of lines, each fitting `width` DISPLAY columns. Breaks on whitespace; a token wider than `width` is hard-split at grapheme boundaries (never mid-cluster); embedded newlines are honoured. Blank input or `width` <= 0 yields `[""]`.
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 |