Skip to content

Getting started

Install

termopy needs Python 3.12 or later, and a POSIX terminal.

uv add termopy      # or: pip install termopy

Its only dependency is Rich, which it uses to measure glyph widths correctly and to draw Rich renderables into a view. Rich's heavy parts (Pygments, markdown-it) are lazily imported, so importing termopy costs about 20 ms.

Your first app

from termopy import UI, Style, View, run
from termopy.events import Event, Key, KeyPress


def app(ui: UI) -> View:
    name, set_name = ui.state("world")

    def keys(event: Event) -> bool:
        if isinstance(event, KeyPress) and event.key is Key.BACKSPACE:
            set_name(name[:-1])
            return True
        if isinstance(event, KeyPress) and isinstance(event.key, str):
            set_name(name + event.key)
            return True
        return False

    ui.on_event(keys)

    return View.vcat([
        View.text(f"Hello, {name}!", Style(bold=True, fg=ui.theme.mauve)),
        View.text(""),
        View.text("Type to change it. Ctrl-C to quit.", Style(fg=ui.theme.subtext0)),
    ]).center(within=ui.size)


if __name__ == "__main__":
    run(app)

Three things are happening.

ui.state("world") asks for a value that survives re-renders, and gets back the value and a setter. Calling the setter marks the frame dirty, so app runs again.

ui.on_event subscribes to input for this frame. Returning True consumes the event so nothing behind it sees it.

The return value is a View: an immutable grid of styled cells. View.vcat stacks, center positions. Nothing was mutated and nothing was drawn; run takes care of that.

Running it

python hello.py

Ctrl-C quits. run puts the terminal in raw mode on the alternate screen and restores it on the way out, including if your app raises.

Testing it

The same function, without a terminal:

from termopy.events import KeyPress
from termopy.testing import render, to_text
from termopy.view import Size


def test_typing_changes_the_greeting():
    view = render(app, size=Size(40, 10), events=[KeyPress("!")])

    assert "Hello, world!!" in to_text(view)

render runs the app, feeds it the events, and returns the final View. to_text throws away the styling and gives you the characters. The thing under test is a function, so you call it, with no pilot and no async harness to set up.

See Testing for asserting on colours, geometry and timers.

The guide covers each piece in turn. If you would rather read code, examples/ in the repository has about twenty small programs, one per idea, and porting/ has two complete applications.