Skip to content

Using Rich renderables

termopy depends on Rich and can draw any Rich renderable into a View:

from rich.table import Table
from rich.syntax import Syntax
from termopy.rich import to_view

body = to_view(Syntax(source, "python"), width=60)
return View.vcat([header, body, footer])

Anything with a __rich_console__ works: Table, Panel, Syntax, Markdown, or something of your own.

How it works

Rich renders to a stream of Segment(text, style) with newlines as their own segments. A View is a grid of (char, Style) cells. The bridge splits on newlines and maps the style; that is all of it.

to_view(renderable, width, *, height=None, base=None, dim_darkens=True)

The result is exactly width columns wide. Rich renders some things to their content width (a bare Text does), but a View is a rectangle and callers compose them side by side, so it is padded out. height pads or crops to exactly that many rows.

What does not survive

Rich's dim and strike have no termopy equivalent. dim darkens the colour instead (pass dim_darkens=False to ignore it); strike is dropped. Links and control segments are dropped too, because a View is a picture.

Everything else maps one to one: truecolor, 8-bit and named colours, bold, italic, underline, blink, reverse.

Where the line is

The bridge goes one way, and that is not an oversight. Rich renders content; termopy composes interactive views. A View carries click handlers, focus tags and passthrough regions, and Rich's segment stream has no notion of any of them. Rich content goes into a view; a view does not come back out.

In practice: use Rich for what it is superb at (tables, syntax highlighting, markdown, anything measured and wrapped) and termopy for the parts that respond.

Why Rich is a hard dependency

Two reasons, and the first is the important one.

Glyph widths. termopy used to measure text itself, and it was wrong: a family emoji (four wide characters welded by three zero-width joiners) measured 8 cells, when the correct answer is 2, misaligning any layout containing modern emoji. Rich's cell_len is right, because it has absorbed years of terminal bug reports. That correctness is not worth re-earning by hand.

Interop. An application that already has Rich renderables can move to termopy without rewriting them. Prezo's 1,174-line layout engine (columns, boxes and dividers, all built on Rich's protocol) ported unchanged, along with its 1,149 lines of tests.

The cost is small: importing Rich's core is about 20 ms, and its heavy parts (Pygments, markdown-it) load only if you use rich.syntax or rich.markdown.

What termopy still does itself

termopy.markup looks like Rich's markup and is deliberately not the same:

from termopy import markup

markup.render(ui.theme, "[green]Running:[/] 4")

It maps tags onto theme roles, so [lavender] follows whatever flavour the user chose. More importantly, it leaves unrecognised tags alone. Rich swallows [b], [d] and [str], which in a terminal UI are usually a key hint or a type annotation:

markup.render(theme, "[b] Create backup")     # renders "[b] Create backup"
Text.from_markup("[b] Create backup")         # renders " Create backup"