Skip to main content

Tuiloom

Tuiloom builds typed, keyboard-navigable terminal menus with dynamic content, Unicode-safe rendering, captured task output, alerts, and free-form input. It supports Python 3.12–3.14 on Linux and macOS.

Tuiloom is not published yet. The API described here is the pre-PyPI API.

Installation

pip install tuiloom

First menu

from tuiloom import CommandContext, ScreenContext, TerminalApp, TerminalMenu

app = TerminalApp("Generator")
menu = TerminalMenu(
    app,
    ScreenContext(
        menu_name="main",
        title="Generation",
        text="Choose an operation",
        width=24,  # minimum inner width, not a fixed width
    ),
    content_source="Ready",
)

def generate(context: CommandContext) -> None:
    context.menu.set_content_source("Generated")

menu.add_command("Generate", generate)
app.set_main_menu(menu)
app.run()

TerminalApp.run() is blocking. It requires an interactive terminal and must run on the Python main thread. Terminal and cursor state are restored if a callback or renderer raises.

Navigation and focus

The menu initially has focus. Up and Down move the selected command in a loop, Enter activates it, and Escape activates the automatic final Back or Quit option. The selected row always contains > so selection remains visible without ANSI colors.

When content exists, Tab alternates focus between the menu and content boxes. With content focused, all four arrows move its viewport. Manual upward movement suspends auto_scroll="smart"; reaching the bottom resumes it.

Focused boxes use solid borders and unfocused boxes use dotted borders. A menu without content has one solid box and Tab does nothing. Use content_spacing=False to remove the otherwise single blank row between boxes.

Content sources

All four ContentSource forms are accepted:

from collections.abc import Iterator

static_text = "one\ntwo"
static_lines = ["one", "two"]

def stream() -> Iterator[str]:
    yield "one\n"
    yield "two\n"

def refreshed() -> str | list[str]:
    return ["current", "state"]

menu.set_content_source(static_text)
menu.set_content_source(static_lines)
menu.set_content_source(stream())
menu.set_content_source(refreshed)

An omitted menu source inherits TerminalApp.global_content_source. A content box is rendered only when a source exists.

Stable command handles

Adding a command returns a stable handle. Positions are zero-based; booleans, negative positions, and out-of-range positions are rejected immediately.

command = menu.add_command("Connect", connect)
menu.set_command_label(command, "Disconnect")
menu.set_command_behavior(command, disconnect)
menu.move_command(command, 0)
menu.disable_command(command)
menu.enable_command(command)
menu.set_exit_label("Close")

Submenus must belong to the same application and are validated when added:

settings = TerminalMenu(app, ScreenContext("settings", "Settings"))
open_settings = menu.add_menu(settings, "Settings", position=0)

CommandContext.command is the invoked MenuCommand or GlobalCommand. CommandContext.binding is its triggering KeyBinding. Alert confirmation uses command=None and the Enter binding.

Bindings and global commands

from tuiloom import KeyBinding, KeyMap, TerminalApp

keymap = KeyMap()
keymap.set_binding("focus", KeyBinding("f", ctrl=True))
app = TerminalApp("App", keymap=keymap)

refresh = app.add_global_command(
    KeyBinding("r", ctrl=True),
    "Refresh",
    refresh_callback,
)
app.set_global_command_binding(refresh, KeyBinding("f5"))
app.set_global_command_label(refresh, "Reload")
app.set_global_command_behavior(refresh, reload_callback)

Global commands are intentionally invisible. app.global_commands is a read-only tuple of handles whose binding, label, and callback metadata can be used to build custom help text. A menu may override or disable one locally:

menu.set_global_command_behavior(refresh, local_refresh)
menu.disable_global_command(refresh)
menu.enable_global_command(refresh)
menu.clear_global_command_behavior(refresh)

System and global bindings cannot collide. Mutations validate first and leave the old binding unchanged on failure. Terminals using legacy keyboard protocols cannot distinguish every modifier combination: Ctrl+letter is often case-insensitive and Shift may be represented only by character case.

Free-form and hidden input

def submit_password(value: str) -> None:
    if value:
        menu.leave_input_mode()

menu.enter_input_mode("Password: ", submit_password, hidden=True)

Hidden masking and Backspace operate on complete Unicode graphemes, including combining characters and emoji sequences. During free-form entry every global command is disabled. Enter submits the current value and Escape leaves input mode.

Input priority is: task-exit choice, hidden-menu handling, free-form input, global commands, alerts, then focus/navigation. Unknown terminal sequences are consumed and never block later input.

Alerts

A blocking alert has no misleading confirmation prompt and Enter does not close it:

menu.show_alert("Waiting for an external event")
menu.clear_alert()

A confirmable alert receives a CommandContext and closes only when its callback returns normally:

menu.show_alert(
    "Saved",
    on_confirm=lambda context: context.menu.set_content_source("Ready"),
    # prompt="Continue",  # optional; default is Press Enter to continue
)

Alerts preserve the content box and suspend any input prompt, buffer, hidden state, and callback. Clearing the alert restores them. Global commands remain active while an alert is shown.

Messages and visibility

from tuiloom import MessageKey

app.add_message("saved", "Saved successfully")
menu.show_message("saved")       # True when displayed
menu.clear_message()
menu.disable_message("saved")    # local suppression
app.disable_message("saved")     # application-wide suppression

menu.show_message(MessageKey.NO_CONTENT_SOURCE)

MessageKey also includes unknown-input and captured-task exit/wait messages. Message keys are validated by every enable/disable/show operation. A suppressed message returns False from show_message() without replacing the current footer.

Setting menu.show = False clears the complete frame while its loop, sources, and tasks keep running. Only global commands and Escape remain active; all other input is discarded and cannot reappear when the menu is shown again.

Captured task output and closing

def download() -> str:
    print("Downloading…")
    return "archive.zip"

menu.run_with_output(
    download,
    on_success=lambda path: menu.show_alert(f"Saved {path}"),
    on_error=lambda error: menu.show_alert(str(error)),
    description="Download in progress",
)

Only one application task may run at a time. Its callback runs on the UI thread. Capture covers print and Python writes to sys.stdout/sys.stderr; subprocess output and direct POSIX file-descriptor writes are not captured.

Quitting the root menu during a task displays:

  • 1: force quit, abandon callbacks, and discard the task's later Python output;
  • 2: wait and quit, animate the description, run the completion callback, then restore the terminal and quit even if the callback changes menus;
  • 0: cancel the exit request and restore the previous footer.

While waiting, 0 remains available. Terminal restoration is guaranteed when a completion callback raises.

Terminal hyperlinks

from tuiloom import hyperlink

label = hyperlink("Project", "https://github.com/maroard/Tuiloom")

Only absolute HTTP/HTTPS URLs with a network location are accepted. Whitespace, C0/C1 controls, Escape, and backslash are rejected. Link text is sanitized while safe SGR styles are preserved.

Development

make install
make check       # read-only lint, format, strict MyPy, tests and coverage
make fix         # the only formatting/fix target
make build       # wheel + sdist + twine check

CI runs Linux and macOS with Python 3.12, 3.13, and 3.14, then verifies the distributions and installs the wheel in a fresh environment.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

tuiloom-0.1.0.tar.gz (47.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

tuiloom-0.1.0-py3-none-any.whl (43.1 kB view details)

Uploaded Python 3

File details

Details for the file tuiloom-0.1.0.tar.gz.

File metadata

  • Download URL: tuiloom-0.1.0.tar.gz
  • Upload date:
  • Size: 47.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for tuiloom-0.1.0.tar.gz
Algorithm Hash digest
SHA256 aa2595db33dd5acb9ffe282742f5779f9d7ed5a47e7ffb88b82fb19769a92f15
MD5 fdc46782c8757ba314610ca3f9e90356
BLAKE2b-256 abd89c65cebea18cc411d4bfcea6c5344a9ead8ad9896dd304be544971578040

See more details on using hashes here.

Provenance

The following attestation bundles were made for tuiloom-0.1.0.tar.gz:

Publisher: release.yml on maroard/Tuiloom

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file tuiloom-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: tuiloom-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 43.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for tuiloom-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 e874a9e89627cd3eca7accd1ef3961654e02be25067b89ec7d9f908bbdbf2e7c
MD5 2dac8469a4892dd740fa71b7e43b74c1
BLAKE2b-256 98396e56a5ad3cc331c3ff485c594493cf6c145a4d3c63b0600648b38b507b3c

See more details on using hashes here.

Provenance

The following attestation bundles were made for tuiloom-0.1.0-py3-none-any.whl:

Publisher: release.yml on maroard/Tuiloom

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.4.0

2 files

0.3.0

2 files

0.2.1

2 files

0.2.0

2 files

0.1.1.1

2 files

0.1.1

2 files

This release

0.1.0 This release

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page