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.