View-tree constructors. Auto-imported into apps via use Harlock.App,
so most apps use text(...), vbox(...), box(...) etc. directly
without qualification.
An element is a plain struct (Harlock.Element); building a view is
just calling these functions to assemble a tree. The renderer walks
the tree once per dirty frame and produces a Frame ready for the
diff renderer.
Primitives
text/2— single-line text contenttext_input/1— single-line editable input (paired withHarlock.TextBuffer)vbox/1/hbox/1— vertical / horizontal stacks with layout constraints (:length,:percentage,:fill)box/1— single-child container with border + title + paddingspacer/0— empty element that occupies a layout slotoverlay/1— render a foreground element on top of a background with optionalfocus_traptable/1/list/2— row-based primitives with selection and focus highlightingcolumn/1— column spec fortable/1
All elements that accept focus take a :focusable opt — the runtime
walks the tree to collect focusable ids for Tab traversal.
Summary
Functions
A single-child container with a border and optional inner padding.
Build a column spec for use inside table/1.
Horizontal stack. Children share the box's height; width is split.
Single-line bar showing key bindings as [k] label [k] label.
Single-column table with chrome hidden. :row_id defaults to & &1
because lists are usually homogeneous; pass an explicit :row_id if
yours aren't.
Render over on top of child in a sub-rectangle anchored within the
parent region.
Single-line horizontal progress bar.
Empty cell that occupies a layout slot. Useful with constraints.
Single-cell animated spinner.
Single-line bar with left- and right-aligned text. Useful as the pinned-bottom row of a screen.
Table primitive.
Single-line horizontal tab bar.
A text element. content is rendered as a single line; callers split
multi-line content themselves.
Single-line text input.
Vertical stack. Children share the box's width; height is split according
to :constraints.
Scrollable container.
Functions
@spec box(keyword()) :: Harlock.Element.t()
A single-child container with a border and optional inner padding.
Required options:
:child— the element drawn inside the box
Optional:
:title— string overlaid on the top border (truncated to fit):title_align—:left(default) |:center|:right:border—:single(default) |:double|:rounded|:thick|:none:border_style—%Style{}or keyword applied to the border + title:padding— non-negative integer (uniform),{v, h}, or{top, right, bottom, left}:focusable,:focus_style— when focused, the focus style replaces the border style (the child is left alone)
For multiple children, wrap them in vbox/1 or hbox/1 and pass the
result as :child. The box reserves one cell on each side for the border
(unless :border is :none); when the region is smaller than that the
border is skipped and the child takes the full region.
@spec column(keyword()) :: Harlock.Element.Column.t()
Build a column spec for use inside table/1.
Options:
:title— header label:width— layout constraint (default{:fill, 1}):align—:left|:right|:center:render—fn row -> string | iodata
@spec hbox(keyword()) :: Harlock.Element.t()
Horizontal stack. Children share the box's height; width is split.
Options as vbox/1.
@spec keybar(keyword()) :: Harlock.Element.t()
Single-line bar showing key bindings as [k] label [k] label.
Required:
:bindings— list of{key, label}tuples.keymay be a char like?qor any atom (:tab,:enter); it's rendered viato_string/1.
Optional:
:style—%Style{}(default%Style{reverse: true}):separator— string between bindings (default" "):right— extra right-aligned text (e.g. clock, status)
@spec list( Enumerable.t(), keyword() ) :: Harlock.Element.t()
Single-column table with chrome hidden. :row_id defaults to & &1
because lists are usually homogeneous; pass an explicit :row_id if
yours aren't.
Options:
:render—fn item -> string; defaults toto_string/1- any option accepted by
table/1
@spec overlay(keyword()) :: Harlock.Element.t()
Render over on top of child in a sub-rectangle anchored within the
parent region.
Required options:
:child— the background element (rendered first):over— the foreground element (rendered on top)
Anchor + sizing:
:anchor—:center(default),:top_left,:top_right,:bottom_left,:bottom_right, or{row, col}for absolute placement:width— width of the over region in cells (default: full parent):height— height of the over region in cells (default: full parent)
Focus:
:focus_trap— when true, focus traversal wraps within theoversubtree until the overlay disappears. Prior focus is stashed and restored automatically when the overlay closes.
Overlays nest cleanly: just put another overlay as :over.
@spec progress(keyword()) :: Harlock.Element.t()
Single-line horizontal progress bar.
Required options:
:value— current value (non-negative):max— denominator (positive)
Optional:
:width— explicit bar width in cells (default: full region width):style—%Style{}for the unfilled portion:fill_style—%Style{}for the filled portion
value is clamped to [0, max]. The bar fills
round(value / max * width) cells with █ in fill_style and the
rest with space in style.
@spec spacer() :: Harlock.Element.t()
Empty cell that occupies a layout slot. Useful with constraints.
@spec spinner(keyword()) :: Harlock.Element.t()
Single-cell animated spinner.
Required options:
:tick— integer; the current animation frame counter (caller-owned in the app's model). Pair with a subscription that increments this on a timer.
Optional:
:frames— list of grapheme strings to cycle through (default: braille spinner["⠋", "⠙", …]):style—%Style{}applied to the rendered frame
Renders Enum.at(frames, rem(tick, length(frames))). The widget
doesn't subscribe to anything itself — wire Harlock.Sub.interval/2
in your app's subs/1 and increment tick in update/2.
@spec statusbar(keyword()) :: Harlock.Element.t()
Single-line bar with left- and right-aligned text. Useful as the pinned-bottom row of a screen.
Options:
:left— string (default""):right— string (default""):style—%Style{}(default%Style{reverse: true})
If left and right together exceed the region width, right is
truncated first.
@spec table(keyword()) :: Harlock.Element.t()
Table primitive.
Required options:
:columns— list ofcolumn/1specs:rows— enumerable of row data:row_id— fn(row) -> id. Row identity is by id, not index, so focus and selection survive sort/filter.
Optional:
:focused_row— currently-focused row id:selection—:none|{:single, id}|{:multi, MapSet}:show_header— defaulttrue:focusable,:focus_trap— same as other elements
@spec tabs(keyword()) :: Harlock.Element.t()
Single-line horizontal tab bar.
Required:
:items— list of{id, label}tuples:active— id of the currently active tab
Optional:
:focusable— focus id; when focused, Left/Right cycle tabs (useHarlock.Tabs.apply_key/3inupdate/2):style—%Style{}for inactive tabs (defaultTheme.get(:header)):active_style—%Style{}for the active tab (defaultTheme.get(:focus)):separator— string between tabs (default" │ ")
Renders only the tab bar — the body for the active tab is rendered separately by the app. Typical pattern:
vbox(
constraints: [length: 1, fill: 1],
children: [
tabs(items: [{:a, "Alpha"}, {:b, "Beta"}], active: m.tab, focusable: :tabs),
case m.tab do
:a -> alpha_body(m)
:b -> beta_body(m)
end
]
)
@spec text( String.t(), keyword() ) :: Harlock.Element.t()
A text element. content is rendered as a single line; callers split
multi-line content themselves.
Options:
:style—%Harlock.Render.Style{}or keyword list of style attrs.
@spec text_input(keyword()) :: Harlock.Element.t()
Single-line text input.
Required options:
:value— the current string contents (caller-owned):cursor— grapheme index of the cursor (0..length):focusable— id for focus traversal
Optional:
:placeholder— shown when value is empty and the input isn't focused:max_length— soft hint; the element doesn't enforce it, butHarlock.TextBuffer.apply_key/3respects it if you wire it in your app:style—%Style{}for the value text:placeholder_style—%Style{}for the placeholder:password— when true, render each grapheme as•
The element is a dumb renderer. The app's update/2 owns the value and
cursor; call Harlock.TextBuffer.apply_key/3 to react to key events
when this input is focused. When focused, the renderer positions the
terminal cursor at the visual column matching :cursor.
@spec vbox(keyword()) :: Harlock.Element.t()
Vertical stack. Children share the box's width; height is split according
to :constraints.
Options:
:constraints— list of layout constraints, one per child. Defaults to[fill: 1]for each child if not provided.:children— list of child elements.
@spec viewport(keyword()) :: Harlock.Element.t()
Scrollable container.
Required options:
:child— the element to scroll:offset— top-row offset into the child (0-indexed, app-owned):content_height— total rows the child occupies
Optional:
:scrollbar— render a single-column cosmetic scrollbar on the right edge (defaultfalse). The scrollbar consumes one column from the child's available width.:scrollbar_style—%Style{}for the scrollbar track + thumb
The viewport renders the child into a temporary frame of
width × content_height, then blits rows offset..offset+visible_height
into the real region. The app owns the scroll offset; pair with
Harlock.Viewport.apply_key/4 in update/2 to translate scroll-key
events into new offsets.
Vertical-only for now. The child is given full width (minus scrollbar column if enabled) so horizontal layout proceeds normally.