Skip to content

Dialogs and modals

A modal is a coroutine you await. The question and what you do with the answer stay in one place:

from termopy.widgets import dialog

async def maybe_delete() -> None:
    if await dialog.confirm(ui, "Delete bookmark?", name):
        remove(name)

ui.spawn(maybe_delete())

While a dialog is open the app underneath still draws but receives no input at all, and focus is scoped, so dismissing the dialog puts the keyboard back where it was.

The built-in dialogs

await dialog.alert(ui, "Failed", "The server said no.")
await dialog.confirm(ui, "Stop app", "Stop blog?", yes="Stop")          # -> bool
await dialog.choose(ui, "Pick", "Which one?", ["A", "B", "C"])          # -> str | None
await dialog.prompt(ui, "Name", "Call it what?", initial="")            # -> str | None

confirm treats Esc as no; choose and prompt return None when dismissed. Passing dangerous= to choose paints that option in the error colour.

Your own

ui.open_modal takes a render function and returns a future:

async def pick_colour(ui: UI) -> str | None:
    def render(handle: UI, size: Size) -> View:
        handle.focus("picker")

        def keys(event: Event) -> bool:
            if isinstance(event, KeyPress) and event.key is Key.ESCAPE:
                handle.close_modal(None)
                return True
            return False

        handle.on_event(keys, focused=True)
        return border(body(handle), title=" Colour ").center(within=size)

    chosen: str | None = await handle.open_modal(render)
    return chosen

close_modal(value) resolves the future. Annotating the awaited result, as above, is what tells the caller (and the type checker) what this particular dialog answers.

Why this and not a screen stack

Both applications ported to termopy needed overlays, and they needed different things. hop3-tui has six top-level screens that are places you navigate between, so it keeps a mode plus a stack in ui.state. Prezo has six overlays that are questions (pick a slide, type a number, show the keys) and awaits each one, with no stack at all.

Neither shape is general enough to belong in the library, so termopy has open_modal and no Screen class. That conclusion needed the second application to reach; one app would have produced an abstraction the other did not want.