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-pythonThis skill uses the workspace's default tool permissions.
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.pySearches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Guides MCP server integration in Claude Code plugins via .mcp.json or plugin.json configs for stdio, SSE, HTTP types, enabling external services as tools.
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.