Views¶
A View is an immutable grid of styled cells. It is the only thing your app returns,
and everything in termopy either produces one or combines them.
Making one¶
from termopy import Size, Style, View
View.text("hello") # one row
View.text("hello", Style(bold=True)) # with a style
View.rect(20, 5, Style(bg=theme.base)) # a filled block
View.blank(20, 5) # transparent: what is behind shows through
View.EMPTY # nothing, 0x0
View.text handles the awkward parts of terminal text for you. Control characters paint
nothing. Wide characters (CJK, emoji) take two cells. Combining marks ride along with
the character they modify, and a zero-width joiner welds a whole emoji sequence into one
glyph:
assert View.text("ๆฅๆฌ่ช").width == 6 # three wide characters
assert View.text("๐จโ๐ฉโ๐งโ๐ฆ").width == 2 # one glyph, not four
Combining¶
Three combinators, and they are the whole layout system:
View.hcat([left, right]) # side by side
View.vcat([top, bottom]) # stacked
View.zcat([front, back]) # layered; the first is on top
hcat and vcat pad the shorter view so the result is a rectangle. zcat is how you
put something over a background:
View.zcat([
content,
View.rect(*ui.size, Style(bg=ui.theme.base)), # fills what content leaves blank
])
Positioning¶
view.pad(left=2, top=1) # add space around it
view.crop(right=3, bottom=1) # take space off it
view.center(within=ui.size) # centre inside a box
There is no layout engine. A two-column split is arithmetic:
left_width = size.width // 2
View.hcat([
body(Size(left_width, size.height)),
sidebar(Size(size.width - left_width, size.height)),
])
That is deliberate, and it has a cost. The comparison says what.
Styles¶
Style is a NamedTuple: fg, bg, bold, italic, underline, blink, invert.
Style(fg=Color.hex("#ff8000"), bold=True)
Style(fg=ui.theme.mauve) # a theme role
base | Style(bold=True) # merge; the right-hand side wins
view.colored(fg=Color(255, 0, 0)) # fill in colours cells left unset
Colours degrade automatically: truecolor where the terminal supports it, xterm-256 or the 16-colour palette where it does not.
Themes¶
ui.theme is one of eighteen flavours, with twenty-six named roles from Catppuccin:
base, text, subtext0, surface0, overlay1, mauve, red, green, and so on.
Pick one with TERMOPY_THEME=gruvbox_dark, or pass theme= to run.
Use role names and your app follows whatever flavour the user chose.
Immutability¶
Every method returns a new View; none mutate. Try it:
That matters because View.width reads the first row's length, so a view whose rows had
different lengths would report a width that was a lie, and every crop, pad and
hcat after it would use that number. Rows are squared up in the constructor and the
object is closed afterwards.
Reading a view back¶
view.size # Size(width, height)
view.to_grid(size) # rows of (char, Style), cropped or padded to size
For tests, termopy.testing.to_text(view) gives you the characters without the styling.
What else a view carries¶
Besides cells, a View carries three things that travel with it through hcat, vcat,
pad and crop, arriving with correct absolute coordinates:
- handlers: click regions, from
on_click - tags: named regions you can look up after layout, from
tagged/find - passthroughs: regions the app paints itself, see Passthrough
tagged is how a widget finds out where it ended up: