Skip to content

Input

Events

from termopy.events import Event, Key, KeyPress, Mouse, MouseKind, Paste

def keys(event: Event) -> bool:
    match event:
        case KeyPress(key=Key.ENTER):
            submit()
        case KeyPress(key="j"):
            move_down()
        case KeyPress(key=str() as ch) if ch.isprintable():
            type_character(ch)
        case _:
            return False
    return True

ui.on_event(keys)

Event is KeyPress | Mouse | Paste. A KeyPress carries key (a str for printable characters, or a Key enum member for named keys) and mods, a frozenset of Mod.CTRL, Mod.SHIFT, Mod.META.

Returning True consumes the event. Handlers behind it never see it. Returning False (or None) passes it on.

on_event also works as a decorator:

@ui.on_event
def fallthrough(event: Event) -> bool:
    ...

Focus

A widget that owns the keyboard joins the focus ring:

active = ui.focus("search-box")     # True if it currently holds the keyboard
if active:
    ui.on_event(handle_typing, focused=True)

focused=True handlers run before the app's own, whatever order they were registered in. A focused editor eats j wherever your global keymap was set up.

Tab cycles the ring; clicking inside a focusable takes it, provided the widget tagged itself:

view.tagged(focus_tag("search-box"))

Focus order is registration order, not layout order

Whichever focusable is constructed first owns the keyboard by default, even when it is drawn at the bottom of the screen. A filter box built before the table it sits above will swallow every keystroke meant for the app, and the symptom is "this key does nothing", which points nowhere near focus.

Say which one should own it:

ui.prefer_focus("results-table")

Other focus controls: ui.focused, ui.set_focus(key), ui.focus_next(step).

Mouse

view.on_click(lambda: pick(index))
view.on_click(callback, key=("row", index))    # stable identity
view.on_click_at(lambda ctx: pick_at(ctx.pos))
view.on_right_click(show_menu)

Handlers carry their region, translated as the view is composed, so the click lands on whatever ended up at that position. The innermost handler wins.

The key matters for anything that moves. A row that scrolls between press and release would otherwise hand its click to whatever is now at those coordinates; with a key, a click is matched by identity instead.

Bindings

There is no BINDINGS list. A key table is a dict:

ACTIONS = {"q": quit, "r": refresh, Key.ESCAPE: back}

def keys(event: Event) -> bool:
    if not isinstance(event, KeyPress):
        return False
    action = ACTIONS.get(event.key)
    if action is None:
        return False
    action()
    return True

A focused text input takes every printable key

If your app has a text box that always holds focus (a console, a search field), d is a character. Give the user a way out that is not a letter: Esc, or a modifier.