Help us improve
Share bugs, ideas, or general feedback.
From aeo-python
Construct rich terminal interfaces using Textual for multi-pane dashboards with CSS styling and Python Prompt Toolkit for interactive line editing with completions. Covers widget composition, key bindings, TUI testing strategies, and WSL2 layout quirks. Engage when building IDE-style interfaces, REPL shells, or dashboard applications.
npx claudepluginhub aeyeops/aeo-skill-marketplace --plugin aeo-pythonHow this skill is triggered — by the user, by Claude, or both
Slash command
/aeo-python:agent-tui-expertThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Expert guidance for building professional Terminal User Interfaces in Python.
examples/ide_layout.pyexamples/minimal_textual_app.pyexamples/ptk_repl.pyreferences/integration-patterns.mdreferences/prompt-toolkit-patterns.mdreferences/testing-guide.mdreferences/textual-patterns.mdreferences/themes-and-colors.mdreferences/workflow-examples.mdreferences/wsl2-platform-issues.mdtests/test_textual_pilot.pyBuilds Textual TUI apps in Python: creates widgets, lays out screens with CSS, handles events/actions/bindings, manages reactivity, tests with Pilot, runs workers.
Builds Textual TUI apps: widgets, screen stacks, CSS layouts, events, actions, reactive attributes, Pilot testing, and background workers.
Provides design patterns for terminal user interfaces: layout paradigms, keyboard navigation, visual systems, and TUI anti-pattern validation. Works with Ratatui, Ink, Textual, Bubbletea, or any TUI framework.
Share bugs, ideas, or general feedback.
Expert guidance for building professional Terminal User Interfaces in Python.
If developing in WSL2, read @references/wsl2-platform-issues.md BEFORE starting.
WSL2 has a critical bug (Microsoft/WSL#1001) where horizontal terminal resize does not propagate SIGWINCH signals. This breaks Textual resize handling and requires a specific workaround using ioctl TIOCGWINSZ polling. The reference file contains tested workarounds for these issues.
Need multi-pane, full-screen UI?
├─ YES → Use Textual
│ (widgets, containers, CSS styling, message passing)
│
Need advanced line editing, history, completions?
├─ YES → Use Prompt Toolkit
│ (PromptSession, FileHistory, Completers, key bindings)
│
Need both?
└─ Use Textual with Suggester API for input completion
Or embed prompt_toolkit patterns in custom widgets
from textual.app import App, ComposeResult
from textual.widgets import Static, Header, Footer
class MyApp(App):
CSS = """
Screen { align: center middle; }
#content { border: solid $primary; padding: 1 2; }
"""
BINDINGS = [("q", "quit", "Quit"), ("d", "toggle_dark", "Dark Mode")]
def compose(self) -> ComposeResult:
yield Header()
yield Static("Hello, Textual!", id="content")
yield Footer()
def action_toggle_dark(self) -> None:
self.dark = not self.dark
if __name__ == "__main__":
MyApp().run()
from prompt_toolkit import PromptSession
from prompt_toolkit.history import FileHistory
from prompt_toolkit.completion import WordCompleter
session = PromptSession(
history=FileHistory(".history"),
completer=WordCompleter(["help", "quit", "status", "run"]),
)
while True:
text = session.prompt(">>> ")
if text == "quit":
break
print(f"You entered: {text}")
| File | Purpose |
|---|---|
| @references/wsl2-platform-issues.md | READ FIRST - WSL2 resize bugs, ioctl workarounds, layout gotchas |
| @references/textual-patterns.md | App lifecycle, containers, CSS styling, messages, widgets |
| @references/prompt-toolkit-patterns.md | Prompts, history, completions, key bindings, validation |
| @references/testing-guide.md | Pilot API, snapshot testing, buffer/completer testing |
| @references/themes-and-colors.md | Built-in themes, color variables, theme switching |
| @references/integration-patterns.md | MCP servers, sub-agents, CLAUDE.md patterns |
| @references/workflow-examples.md | Agent IDE, data dashboard, REPL workflows |
| File | Purpose | Use As Reference For |
|---|---|---|
| examples/minimal_textual_app.py | Simplest working Textual app | Starting any Textual project |
| examples/ide_layout.py | Multi-pane IDE layout | Building IDE-style applications |
| examples/ptk_repl.py | REPL with history and completions | Building interactive shells |
| File | Purpose |
|---|---|
| tests/test_textual_pilot.py | Canonical Pilot API test patterns |
compose() - Build widget tree (yields widgets)on_mount() - Called when app startson_ready() - Called when app is ready for inputHorizontal - Left-to-right layoutVertical - Top-to-bottom layoutContainer - Generic wrapperGrid - Grid layout with grid-size, grid-columnsScreen { background: $surface; }
#sidebar { dock: left; width: 25; }
.highlight { background: $accent; }
Widget:focus { border: thick $success; }
class MyWidget(Static):
class Changed(Message):
def __init__(self, value: str) -> None:
self.value = value
super().__init__()
def update_value(self, value: str) -> None:
self.post_message(self.Changed(value))
# In App:
def on_my_widget_changed(self, message: MyWidget.Changed) -> None:
self.log(f"Changed to: {message.value}")
BINDINGS = [
("q", "quit", "Quit"),
("ctrl+s", "save", "Save"),
("ctrl+p", "command_palette", "Commands"),
]
def action_save(self) -> None:
# Handle save
pass
Use built-in themes for professional styling out of the box:
class MyApp(App):
theme = "textual-dark" # or "nord", "gruvbox", "tokyo-night"
Theme variables: $primary, $surface, $text, $accent, $warning, $error, $success
Shade variations: $primary-lighten-1, $primary-darken-2, etc.
See @references/themes-and-colors.md for full details.
from prompt_toolkit.history import FileHistory, InMemoryHistory
# Persistent across sessions
history = FileHistory("~/.myapp_history")
# In-memory only
history = InMemoryHistory()
from prompt_toolkit.completion import WordCompleter, NestedCompleter
# Simple word list
completer = WordCompleter(["red", "green", "blue"])
# Nested commands
completer = NestedCompleter.from_nested_dict({
"show": {"status": None, "config": None},
"set": {"verbose": {"on": None, "off": None}},
})
from prompt_toolkit.key_binding import KeyBindings
bindings = KeyBindings()
@bindings.add("c-x")
def exit_handler(event):
event.app.exit()
session = PromptSession(key_bindings=bindings)
from prompt_toolkit.validation import Validator
validator = Validator.from_callable(
lambda text: text.isdigit(),
error_message="Must be a number",
)
text = prompt("Number: ", validator=validator)
import pytest
from my_app import MyApp
@pytest.mark.asyncio
async def test_button_click():
app = MyApp()
async with app.run_test() as pilot:
await pilot.click("#my-button")
assert app.query_one("#result").renderable == "Clicked!"
def test_app_snapshot(snap_compare):
assert snap_compare("path/to/app.py", press=["tab", "enter"])
class MyApp(App):
def __init__(self, mcp_client: Any) -> None:
super().__init__()
self.mcp_client = mcp_client
async def load_data(self) -> None:
result = await self.mcp_client.call_tool(
"database_query",
{"query": "SELECT * FROM items"}
)
# Update widgets with result
async def on_input_submitted(self, event: Input.Submitted) -> None:
async for chunk in self.agent_client.run_task(event.value):
self.query_one("#output", RichLog).write(chunk.content)
See @references/integration-patterns.md and @references/workflow-examples.md for complete examples.