textual
Textual TUI Framework
Build terminal applications with Textual's web-inspired architecture: App → Screen → Widget.
Quick Start
from textual.app import App, ComposeResult
from textual.widgets import Header, Footer, Static
class MyApp(App):
CSS_PATH = "styles.tcss"
BINDINGS = [("q", "quit", "Quit"), ("d", "toggle_dark", "Dark Mode")]
def compose(self) -> ComposeResult:
yield Header()
yield Static("Hello, World!")
yield Footer()
def action_toggle_dark(self) -> None:
self.theme = "textual-dark" if self.theme == "textual-light" else "textual-light"
if __name__ == "__main__":
MyApp().run()
Core Concepts
Widget Lifecycle
__init__()→compose()→on_mount()→on_show()/on_hide()→on_unmount()
Reactivity
from textual.reactive import reactive, var
class MyWidget(Widget):
count = reactive(0) # Triggers refresh on change
internal = var("") # No automatic refresh
def watch_count(self, new_value: int) -> None:
"""Called when count changes."""
self.styles.background = "green" if new_value > 0 else "red"
def validate_count(self, value: int) -> int:
"""Constrain values."""
return max(0, min(100, value))
Events and Messages
from textual import on
from textual.message import Message
class MyWidget(Widget):
class Selected(Message):
def __init__(self, value: str) -> None:
self.value = value
super().__init__()
def on_click(self) -> None:
self.post_message(self.Selected("item"))
class MyApp(App):
# Handler naming: on_<widget>_<message>
def on_button_pressed(self, event: Button.Pressed) -> None:
self.log(f"Button {event.button.id} pressed")
@on(Button.Pressed, "#submit") # CSS selector filtering
def handle_submit(self) -> None:
pass
Data Flow
- Attributes down: Parent sets child properties directly
- Messages up: Child posts messages to parent via
post_message()
Screens
from textual.screen import Screen
class WelcomeScreen(Screen):
BINDINGS = [("escape", "app.pop_screen", "Back")]
def compose(self) -> ComposeResult:
yield Static("Welcome!")
yield Button("Continue", id="continue")
def on_button_pressed(self, event: Button.Pressed) -> None:
if event.button.id == "continue":
self.app.push_screen("main")
class MyApp(App):
SCREENS = {"welcome": WelcomeScreen, "main": MainScreen}
def on_mount(self) -> None:
self.push_screen("welcome")
Custom Widgets
Simple Widget
class Greeting(Widget):
def render(self) -> RenderResult:
return "Hello, [bold]World[/bold]!"
Compound Widget
class LabeledButton(Widget):
DEFAULT_CSS = """
LabeledButton { layout: horizontal; height: auto; }
LabeledButton Label { width: 1fr; }
"""
def __init__(self, label: str, button_text: str) -> None:
self.label_text = label
self.button_text = button_text
super().__init__()
def compose(self) -> ComposeResult:
yield Label(self.label_text)
yield Button(self.button_text)
Focusable Widget
class Counter(Widget):
can_focus = True
BINDINGS = [("up", "increment", "+"), ("down", "decrement", "-")]
count = reactive(0)
def action_increment(self) -> None:
self.count += 1
Layout Patterns
Containers
from textual.containers import Horizontal, Vertical, Grid, VerticalScroll
def compose(self) -> ComposeResult:
with Vertical():
with Horizontal():
yield Button("Left")
yield Button("Right")
with VerticalScroll():
for i in range(100):
yield Label(f"Item {i}")
Grid CSS
Grid {
layout: grid;
grid-size: 3 2; /* columns rows */
grid-columns: 1fr 2fr 1fr;
grid-gutter: 1 2;
}
#wide { column-span: 2; }
Docking
#header { dock: top; height: 3; }
#sidebar { dock: left; width: 25; }
#footer { dock: bottom; height: 1; }
Workers (Async)
from textual import work
class MyApp(App):
@work(exclusive=True) # Cancels previous
async def fetch_data(self, url: str) -> None:
async with httpx.AsyncClient() as client:
response = await client.get(url)
self.query_one("#result").update(response.text)
@work(thread=True) # For sync APIs
def sync_operation(self) -> None:
result = blocking_call()
self.call_from_thread(self.update_ui, result)
Testing
async def test_app():
app = MyApp()
async with app.run_test() as pilot:
await pilot.press("enter")
await pilot.click("#button")
await pilot.pause() # Wait for messages
assert app.query_one("#status").render() == "Done"
Common Operations
# Query widgets
self.query_one("#id")
self.query_one(Button)
self.query(".class")
# CSS classes
widget.add_class("active")
widget.toggle_class("visible")
widget.set_class(condition, "active")
# Visibility
widget.display = True/False
# Mount/remove
self.mount(NewWidget())
widget.remove()
# Timers
self.set_interval(1.0, callback)
self.set_timer(5.0, callback)
# Exit
self.exit(return_code=0)
References
- Widget catalog and messages: See references/widgets.md
- CSS properties and selectors: See references/css.md
- Complete examples: See references/examples.md
- Official docs: https://textual.textualize.io/
More from johnlarkin1/claude-code-extensions
manim
Create mathematical animations and visualizations using Manim (ManimCE - Community Edition). Use this skill when users want to build Manim visualizations, create math animations, animate equations, graphs, geometric proofs, 3D objects, or any programmatic video animation. Triggers on requests mentioning "manim", "mathematical animation", "animate equation", "visualize algorithm", "create animation of", "3D visualization", or building explanatory math videos.
13tauri
Comprehensive Tauri v2 development skill for building cross-platform desktop applications with Rust backends and web frontends. This skill should be used when creating new Tauri apps, adding commands and IPC communication, developing plugins, managing application state, or integrating Rust with JavaScript/TypeScript frontends. Triggers on tasks involving #[tauri::command], invoke(), Tauri plugins, desktop app development, or Rust-WebView integration.
11excalidraw
Generate Excalidraw diagrams (.excalidraw JSON files) for whiteboarding, flowcharts, architecture diagrams, sequence diagrams, mind maps, wireframes, and org charts. Use when user requests diagrams, visual documentation, system architecture visualization, process flows, or any hand-drawn style diagram. Triggers on requests mentioning Excalidraw, diagram creation, flowcharts, architecture diagrams, sequence diagrams, wireframes, or visual documentation.
11ics-generator
Generate ICS calendar files (.ics) from natural language descriptions. Use when user wants to create calendar events, meetings, appointments, reminders, recurring events, or schedule items. Triggers on requests mentioning "calendar event", "ICS file", ".ics", "meeting invite", "appointment", "recurring event", "schedule", "RRULE", "reminder", "RSVP", "calendar invite", "block my calendar", or "add to calendar".
9graphviz
Generate GraphViz DOT files (.dot) for directed/undirected graphs, hierarchical layouts, network diagrams, dependency graphs, state machines, and complex graph visualizations. Use when precise node positioning is needed, when rendering to PNG/SVG/PDF is required, when complex graph algorithms (clustering, ranking) are needed, or when dealing with large graphs (100+ nodes). Triggers on requests mentioning GraphViz, DOT language, network diagrams, dependency graphs, or when sophisticated graph layout is required.
6mermaid
Generate Mermaid diagrams (.mmd, .mermaid files, or markdown code blocks) for flowcharts, sequence diagrams, class diagrams, ER diagrams, state diagrams, Gantt charts, pie charts, mindmaps, timelines, and git graphs. Use when user requests diagrams for documentation, markdown files, README visualizations, or any text-based diagram format that renders in GitHub/GitLab. Triggers on requests mentioning Mermaid, markdown diagrams, documentation diagrams, or when output needs to be embedded in markdown.
5