Skip to content

Widgets

A widget is a function. It takes ui, whatever it needs to display, and returns a View. There is nothing to instantiate and nothing to keep.

from termopy.widgets import button, listbox, table, textbox

view = button(ui, "Save", on_press=save, focus="save-button")

Every widget takes an optional focus key. With one it joins the focus ring and only responds when it holds the keyboard; without one it is decoration you can still click.

All arguments after the required ones are keyword-only, so call sites read for themselves and parameter order is not part of the API.

The catalogue

Text and input

textbox(ui, *, focus, style, cursor_style, initial, width) single-line input; returns Textbox(view, value, set)
editor(ui, *, keymap, style, width, max_height, initial, single_line, highlights, focus) multi-line editor, with standard, vim or emacs keymaps
spinner(ui, *, kind, style, interval) an animated dot or line spinner

Controls

button(ui, label, on_press, *, focus, style, accent) Enter or Space when focused; also clickable
checkbox(ui, label, checked, on_toggle, *, focus, switch) a checkbox, or a toggle with switch=True
progress_bar(ui, fraction, *, width, label, style) determinate bar
tabs(ui, items, selected, on_select, *, width, focus) a row of tabs; left/right move

Lists and tables

listbox(ui, rows, selected, size, on_move, *, on_select, on_delete, focus, empty) single-column list of Row(label, style, value); arrows and j/k, fixed height, scrolls itself
table(ui, columns, rows, selected, *, size, on_move, on_select, on_delete, focus, empty, show_header) the same with Columns; rows are strings or Cell(text, style)
tree.render(node, style) Leaf / Branch / Split with box-drawing connectors
ncdu.browser(ui, root, title, *, separator, on_leaf_select, focus) a weighted-tree browser

table's columns share the row out by weight, so you say which column deserves the space and not how wide the terminal is:

COLUMNS = [
    Column("name", weight=3),
    Column("state", width=9),          # fixed
    Column("size", align="right"),
]

Framing and scrolling

border(view, *, title, subtitle, line, style, title_style, subtitle_style, padding, hide) four line styles; hide="tr" leaves sides open
scroller(ui, view, size, *, crop_width, focus, stick_to_bottom) a window with less-style keys; follows the bottom for logs
scrollbar(position, height, *, track_style, thumb_style) the bar on its own
vim_status(position, style) Top / 50% / Bot, as vim shows it

The widgets that return more than a View hand back a small NamedTuple whose first field is view: Textbox(view, value, set), Scroller(view, position, inject, scroll_to, stuck_to_bottom), Editor(view, text, set_text, cursor, mode, cursor_tag) and Pane(view, send, ...).

Charts

bar_chart(bars, ...), line_chart(lines, ...) and scatter_chart(points, ...), taking Bar(label, value, color), Line(points, label, color) and Point(x, y, color, marker). All three are built on the braille Canvas, which addresses four times the resolution of one cell.

Other

dialog (modals), tmux.pane (embed another TUI), less_keys (the navigation key table on its own).

Writing your own

There is no base class. A widget is a function that returns a View:

def field(ui: UI, label: str, value: str, on_change, *, focus=None) -> View:
    active = focus is None or ui.focus(focus)

    def keys(event: Event) -> bool:
        if active and isinstance(event, KeyPress) and isinstance(event.key, str):
            on_change(value + event.key)
            return True
        return False

    if active:
        ui.on_event(keys, focused=True)

    view = View.hcat([
        View.text(f"{label}: ", Style(fg=ui.theme.subtext0)),
        View.text(value, Style(underline=active)),
    ])
    return view.tagged(focus_tag(focus)) if focus else view

Two conventions worth following, because the built-in widgets do:

The caller owns the state. field above takes value and on_change. The app usually already has that value and wants to save it; a widget that hides it makes that impossible.

Return a View, unless the caller needs more than pixels. textbox, scroller, editor and tmux.pane return a small NamedTuple whose first field is view, because callers need the value, the scroll position or the cursor tag as well.