Dialogs and modals¶
A modal is a coroutine you await. The question and what you do with the answer stay in one place:
from turbodesk.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
await dialog.prompt(ui, "Find", "Text?", history=asked_before) # ↑ recalls
await dialog.pick(ui, "Open", names) # -> str | None
await dialog.find_file(ui, root) # -> Path | None
pick is the fuzzy list every editor calls its file picker or command palette: type to
narrow, arrows to walk, Enter to take. turbodesk.fuzzy decides the order and which
characters matched, and the picker underlines them — a list that shows that something
matched and never why is one you squint at. find_file is pick over a directory
walk, and is the counterpart of open_file: one is a tree you walk when you want to
look around, the other a query when you already know the name.
confirm treats Esc as no; choose and prompt return None when dismissed. prompt's history is what has been answered before, oldest first, walked with ↑ and ↓; see History lists. Passing dangerous= to choose paints that option in the error colour.
Forms¶
dialog.form asks several questions at once and returns the answers by key:
answers = await dialog.form(ui, "Environment", [
dialog.Field("keymap", "Editor keymap", kind="radio", value="vim",
options=["standard", "vim", "emacs"]),
dialog.Field("bytecode", "Write bytecode", kind="check", value=True),
dialog.Field("arguments", "Run parameters", value=""),
])
if answers is not None:
apply(answers)
Three kinds of field: text (a single-line editor), check (a checkbox) and radio (a radio set over options). value is the starting value, a str for text and radio and a bool for check.
Tab moves between the controls, which the runtime does for free: a modal has its own focus ring, so the form's controls are the only ones in it.
Enter accepts and Esc cancels, and both reach the form ahead of whichever control holds the keyboard. That is Turbo Vision's rule and it is the only one that works here: checkbox answers to Enter as well as Space, so a form that let its controls go first would be one whose Enter key stopped working on the second field.
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
ui.modal_open says whether one is up, which an app needs when a key means different things with and without a dialog on screen. 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 turbodesk 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 turbodesk 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.