Skip to content

Widgets

Tachikoma includes dozens of widgets covering text display, input controls, data visualization, navigation, and containers. All widgets follow the render protocol: render(widget, area::Rect, buf::Buffer).

Value Protocol

Many widgets support a unified value interface:

julia
value(widget)            # get the widget's current value
set_value!(widget, v)    # set the widget's value
valid(widget)            # check if the current value is valid (default: true)

Text Display

All text rendering in Tachikoma is grapheme-aware. Unicode combining marks (e.g. , ), precomposed characters, and CJK wide characters are handled correctly across all widgets. Combining marks attach to their base character without consuming an extra cell, and wide characters occupy two cells with proper alignment.

Block

Bordered panel with optional title. The workhorse container widget:

julia
block = Block(; title="Panel", border_style=tstyle(:border),
               title_style=tstyle(:title, bold=true), box=BOX_ROUNDED)
inner = render(block, area, buf)   # returns inner Rect after drawing borders
block_basic example

Box styles: BOX_ROUNDED, BOX_HEAVY, BOX_DOUBLE, BOX_PLAIN.

Paragraph

Styled text with wrapping and alignment:

julia
para = Paragraph([
    Span("Bold text ", tstyle(:text, bold=true)),
    Span("and dim text", tstyle(:text_dim)),
]; wrap=word_wrap, alignment=align_center)

render(para, area, buf)
paragraph_basic example
julia
para = Paragraph([Span("Hello ", tstyle(:text)), Span("world", tstyle(:accent))])
paragraph_line_count(para, 40)   # count wrapped lines for a given width

Wrap modes: no_wrap, word_wrap, char_wrap. Alignment: align_left, align_center, align_right.

ANSI Escape Sequences

Strings containing ANSI escape sequences are automatically parsed into styled spans — no manual Span construction needed:

julia
text = "\e[1mBold\e[0m \e[3;32mitalic green\e[0m \e[38;5;208m256-color\e[0m"
para = Paragraph(text; wrap=char_wrap)
render(para, area, buf)
paragraph_ansi example

Supported SGR codes: standard colors (30–37, 40–47), bright colors (90–97, 100–107), 256-color (38;5;n / 48;5;n), 24-bit RGB (38;2;r;g;b / 48;2;r;g;b), bold, dim, italic, underline, strikethrough, reverse video, and reset. Non-SGR escape sequences (cursor movement, window titles, etc.) are silently stripped.

Disable per-widget with ansi=false — escape sequences are stripped and text is shown unstyled:

julia
para = Paragraph("\e[31mred\e[0m"; ansi=false)
# renders as plain "red" without color

To inspect the literal escape codes (useful for debugging), use raw=true — the ESC byte is replaced with the visible symbol:

julia
para = Paragraph("\e[31mred\e[0m"; raw=true)
# renders as "␛[31mred␛[0m"

Use parse_ansi directly to convert ANSI strings into Span vectors for reuse:

julia
spans = parse_ansi("\e[1;31mError:\e[0m something broke")
Paragraph(spans)

Span

Inline styled text fragment, used inside Paragraph and StatusBar:

julia
Span("text", tstyle(:primary, bold=true))
span_demo example

BigText

Large block-character text (5 rows tall):

julia
bt = BigText("12:34"; style=tstyle(:primary, bold=true))
render(bt, area, buf)
bigtext_basic example
julia
intrinsic_size(BigText("12:34"))   # (width, height) in terminal cells

StatusBar

Full-width bar with left and right aligned spans:

julia
render(StatusBar(
    left=[Span("  Status: OK ", tstyle(:success))],
    right=[Span("[q] quit ", tstyle(:text_dim))],
), area, buf)
statusbar_basic example

Separator

Visual divider line:

julia
render(Separator(), area, buf)
separator_basic example

Input Widgets

TextInput

Single-line text editor with optional validation:

julia
input = TextInput(; text="initial", label="Name:", focused=true,
                   validator=s -> length(s) < 2 ? "Min 2 chars" : nothing)
textinput_demo example
julia
handle_key!(input, evt)   # returns true if consumed
text(input)                # get current text
set_text!(input, "new")    # set text
value(input)               # same as text()
valid(input)               # true if validator returns nothing

The validator function receives the current text and returns nothing (valid) or an error message string.

TextArea

Multi-line text editor:

julia
area = TextArea(; text="", label="Bio:", focused=true)
textarea_demo example
julia
handle_key!(area, evt)
handle_mouse!(area, evt, rect)
text(area)
set_text!(area, "multi\nline")

CodeEditor

Syntax-highlighted code editor:

julia
CodeEditor(; text="function greet(name)\n    println(\"Hello, \$name!\")\nend",
    focused=true, block=Block(title="editor.jl", border_style=tstyle(:border),
    title_style=tstyle(:title)))
codeeditor_demo example
julia
handle_key!(editor, evt)
editor_mode(editor)        # current mode symbol

Supports Julia syntax highlighting with token types: token_keyword, token_string, token_comment, token_number, token_plain.

Checkbox

Boolean toggle:

julia
cb = Checkbox("Enable notifications"; focused=false)
checkbox_demo example
julia
handle_key!(cb, evt)       # space toggles
value(cb)                  # true/false
set_value!(cb, true)

RadioGroup

Mutually exclusive selection:

julia
rg = RadioGroup(["Admin", "Editor", "Viewer"])
radiogroup_demo example
julia
handle_key!(rg, evt)       # up/down + space/enter to select
value(rg)                  # selected index (Int)
set_value!(rg, 2)

Select from a dropdown list:

julia
dd = DropDown(["Tokyo", "Berlin", "NYC", "London"])
dropdown_demo example
julia
handle_key!(dd, evt)       # enter opens, up/down navigates, enter selects
value(dd)                  # selected index (Int)

Calendar

Date picker widget:

julia
cal = Calendar(2026, 2; today=19)
render(cal, area, buf)
calendar_basic example

Selection & Navigation

SelectableList

Keyboard and mouse navigable list:

julia
list = SelectableList(["Alpha", "Beta", "Gamma", "Delta", "Epsilon"];
                      selected=1, focused=true,
                      block=Block(title="Items"),
                      highlight_style=tstyle(:accent, bold=true),
                      marker=MARKER)
selectablelist_demo example
julia
handle_key!(list, evt)
value(list)                # selected index
set_value!(list, 2)
list_hit(list, x, y, area)    # hit test → index or nothing
list_scroll(list, lines)       # scroll by n lines

With styled items:

julia
items = [ListItem("Item 1", tstyle(:text)),
         ListItem("Item 2", tstyle(:warning))]
list = SelectableList(items; selected=1)

TreeView / TreeNode

Hierarchical tree display:

julia
root = TreeNode("Root", [
    TreeNode("Child 1", [
        TreeNode("Leaf A"),
        TreeNode("Leaf B"),
    ]),
    TreeNode("Child 2"),
])

tree = TreeView(root; block=Block(title="Tree"))
render(tree, area, buf)
treeview_basic example
julia
handle_key!(tree, evt)     # up/down navigate, enter expand/collapse

TabBar

Tab switching:

julia
tabs = TabBar(["Overview", "Details", "Settings"]; active=2)
render(tabs, area, buf)
tabbar_basic example
julia
handle_key!(tabs, evt)     # left/right/tab to switch
handle_mouse!(tabs, evt)   # click to switch (returns :changed or :none)
value(tabs)                # selected tab index (1-based)
set_value!(tabs, 2)        # set active tab programmatically

Tab appearance is controlled by a TabBarStyle{D} with one of three built-in decoration types:

julia
# Default bracket style: [Active]  Inactive
TabBar(["Tab 1", "Tab 2"])

# Box tabs with heavy borders (requires height ≥ 3)
TabBar(["Tab 1", "Tab 2"]; tab_style=TabBarStyle(decoration=BoxTabs(box=BOX_HEAVY)))

# Plain text tabs
TabBar(["Tab 1", "Tab 2"]; tab_style=TabBarStyle(decoration=PlainTabs()))

TabBarStyle keyword arguments:

ArgumentDefaultDescription
decorationBracketTabs()BracketTabs(), BoxTabs(; box=…), or PlainTabs()
activetstyle(:accent, bold=true)Style for the active tab label
inactivetstyle(:text_dim)Style for inactive tab labels
separator" │ "String placed between tabs (not used for BoxTabs)
overflow_char'…'Character shown when tabs overflow the available width
tab_colorsStyle[]Per-tab color overrides (empty = use active/inactive)

BoxTabs requires at least 3 rows of height. If given less, it falls back to BracketTabs.

When there are more tabs than fit in the available width, overflow indicators () appear automatically and the visible window scrolls to keep the active tab in view.

Store the TabBar in your model to preserve state across frames:

julia
@kwdef mutable struct App <: Model
    tabs::TabBar = TabBar(["Overview", "Details", "Settings"]; focused=true)
end

function update!(m::App, e::KeyEvent)
    handle_key!(m.tabs, e)
end
function update!(m::App, e::MouseEvent)
    handle_mouse!(m.tabs, e)
end

function view(m::App, f::Frame)
    render(m.tabs, area, f.buffer)
    # Use value(m.tabs) to decide which pane to show
end

Confirmation dialog:

julia
modal = Modal(; title="Delete?", message="This cannot be undone.",
               confirm_label="Delete", cancel_label="Cancel",
               selected=:cancel)
render(modal, area, buf)
modal_basic example

Data Visualization

Sparkline

Mini line chart from a data vector:

julia
Sparkline(data; style=tstyle(:accent))
sparkline_demo example

Gauge

Progress bar (0.0 to 1.0):

julia
Gauge(progress;
    filled_style=tstyle(:primary),
    empty_style=tstyle(:text_dim, dim=true),
    tick=tick)
gauge_demo example

BarChart

Bar chart with labeled entries:

julia
entries = [BarEntry("CPU", 65.0), BarEntry("MEM", 42.0), BarEntry("DSK", 78.0)]
render(BarChart(entries; block=Block(title="Usage")), area, buf)
barchart_basic example

Chart

Line and scatter plots with multiple data series:

julia
cpu_data = Float64[0.3 + 0.2 * sin(i * 0.3) for i in 1:30]
mem_data = Float64[0.5 + 0.1 * cos(i * 0.2) for i in 1:30]
series = [
    DataSeries(cpu_data; label="CPU", style=tstyle(:primary)),
    DataSeries(mem_data; label="Mem", style=tstyle(:secondary)),
]
render(Chart(series; block=Block(title="System")), area, buf)
chart_basic example

Chart types: chart_line, chart_scatter.

Table

Simple row/column table:

julia
headers = ["Name", "Status", "CPU"]
rows = [["nginx", "running", "12%"],
        ["postgres", "running", "8%"]]

render(Table(headers, rows;
    block=Block(title="Processes"),
    header_style=tstyle(:title, bold=true),
    row_style=tstyle(:text),
    alt_row_style=tstyle(:text_dim)), area, buf)
table_basic example

DataTable

Sortable, filterable data table with pagination:

julia
dt = DataTable([
    DataColumn("Name",  ["Alice", "Bob", "Carol"]),
    DataColumn("Score", [95, 82, 91]; align=col_right),
    DataColumn("Grade", ["A", "B", "A"]; align=col_center),
]; selected=1)
render(dt, area, buf)
datatable_basic example

Sort directions: sort_none, sort_asc, sort_desc. Column alignment: col_left, col_right, col_center.

With the Tables.jl extension, DataTable accepts any Tables.jl source:

julia
using Tables
dt = DataTable(my_dataframe)

Containers & Control

Form / FormField

Multi-field form with focus navigation and validation:

julia
form = Form([
    FormField("Name", TextInput(; validator=s -> isempty(s) ? "Required" : nothing);
              required=true),
    FormField("Bio", TextArea()),
    FormField("Notify", Checkbox("Enable notifications")),
    FormField("Role", RadioGroup(["Admin", "Editor", "Viewer"])),
    FormField("City", DropDown(["Tokyo", "Berlin", "NYC"])),
]; submit_label="Submit",
   block=Block(title="Registration"))
render(form, area, buf)
form_basic example
julia
handle_key!(form, evt)     # Tab/Shift-Tab navigation, widget key handling
value(form)                # Dict{String, Any} of field label → value
valid(form)                # true if all required fields are valid

Button

Clickable button:

julia
btn = Button("Submit"; focused=true)
render(btn, area, buf)
button_basic example
julia
handle_key!(btn, evt)      # enter/space triggers
handle_mouse!(btn, evt)    # left-click triggers; returns true if hit

Button appearance is controlled by a ButtonStyle{D} with one of three built-in decoration types:

julia
# Default bracket button: [ Label ]
Button("Click me")

# Bordered button with rounded box (requires height ≥ 3)
Button("Submit"; button_style=ButtonStyle(decoration=BorderedButton()))

# Plain text button
Button("Cancel"; button_style=ButtonStyle(decoration=PlainButton()))

ButtonStyle keyword arguments:

ArgumentDefaultDescription
decorationBracketButton()BracketButton(), BorderedButton(; box=…), or PlainButton()
normaltstyle(:text)Style when unfocused
focusedtstyle(:accent, bold=true)Style when focused

BorderedButton accepts a box keyword (e.g. BOX_ROUNDED, BOX_HEAVY, BOX_DOUBLE). It requires at least 3 rows of height; if given less it falls back to BracketButton.

Buttons can play a flash animation when focused using the following parameters:

  • flash_frames::Int: Number of frames the animation lasts.

  • flash_style::Function: A function with the signature flash_style(btn::Button)::Style that returns the button style during the animation. The field btn.flash_remaining can be used to check how many frames remain.

ScrollPane

Scrollable container for content:

julia
sp = ScrollPane(["Line 1", "Line 2", "Line 3"]; following=true)
push_line!(sp, "new line")           # append content
render(sp, area, buf)
scrollpane_basic example
julia
handle_mouse!(sp, evt, area)         # scrollbar drag + scroll wheel

ScrollPane automatically parses ANSI escape sequences in String content, just like Paragraph. This works with both the non-wrap and word_wrap=true paths:

julia
lines = ["\e[32m[OK]\e[0m Server started", "\e[31m[ERR]\e[0m Connection refused"]
sp = ScrollPane(lines; word_wrap=true)

Disable with ansi=false:

julia
sp = ScrollPane(lines; ansi=false)

Scrollbar

Standalone scrollbar indicator:

julia
sb = Scrollbar(100, 20, 0)
render(sb, area, buf)
scrollbar_basic example

WidgetScroll

Scrollable 2D viewport that wraps any widget. Renders the inner widget into a virtual buffer larger than the viewport, then displays the visible portion with optional scrollbars.

julia
ws = WidgetScroll(my_widget;
    virtual_width=200, virtual_height=120,
    block=Block(title="Viewport"),
    show_vertical_scrollbar=true,
    show_horizontal_scrollbar=false)
render(ws, area, buf)

Navigation:

julia
handle_key!(ws, evt)     # arrow keys, Page Up/Down, Home/End
handle_mouse!(ws, evt)   # click-drag panning, scroll wheel
value(ws)                # returns (offset_x, offset_y)

The virtual buffer is cached and reused across frames to avoid per-frame allocation.

ProgressList / ProgressItem

Task status list with status icons:

julia
items = [
    ProgressItem("Build"; status=task_done),
    ProgressItem("Test"; status=task_running),
    ProgressItem("Deploy"; status=task_pending),
]
render(ProgressList(items; tick=tick), area, buf)
progresslist_basic example

Task statuses: task_pending, task_running, task_done, task_error, task_skipped.

FocusRing

Tab/Shift-Tab navigation manager — cycles focus between panes or widgets (see Input & Events for the full example):

julia
ring = FocusRing([widget1, widget2, widget3])
handle_key!(ring, evt)
current(ring)
next!(ring)
prev!(ring)
focusring_widget_demo example

Container

Group widgets with automatic layout:

julia
container = Container(
    [widget1, widget2, widget3],
    Layout(Vertical, [Fixed(3), Fill(), Fixed(1)]),
    Block(title="Metrics")
)
container_basic example

MarkdownPane

Scrollable CommonMark viewer with styled headings, bold/italic, inline code, code blocks with syntax highlighting, lists, block quotes, and horizontal rules. Requires the markdown extension (enable_markdown() or using CommonMark).

julia
enable_markdown()
pane = MarkdownPane("# Hello\n\n**Bold**, *italic*, `code`.\n\n- Item 1\n- Item 2";
    block=Block(title="Docs"))
render(pane, area, buf)
markdownpane_basic example

Update content dynamically with set_markdown!:

julia
set_markdown!(pane, "# Updated\n\nNew content here.")

Supports keyboard scrolling (//Page Up/Page Down) and mouse wheel. Automatically reflows text when the render width changes.