Get the FREE Ultimate OpenClaw Setup Guide →

textual

Scanned
npx machina-cli add skill johnlarkin1/claude-code-extensions/textual --openclaw
Files (1)
SKILL.md
5.9 KB

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

  1. __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

Source

git clone https://github.com/johnlarkin1/claude-code-extensions/blob/main/skills/textual/SKILL.mdView on GitHub

Overview

Textual enables building terminal-based user interfaces using a web-inspired App → Screen → Widget architecture. Start quickly with a minimal example and harness reactive state, event-driven messages, and screen navigation to create responsive TUI apps. It also covers custom widgets and TCSS styling to craft polished terminal interfaces.

How This Skill Works

A Textual app is composed of App, Screen, and Widget objects arranged via compose(). State is made reactive with reactive() and var, which trigger UI refreshes, while events and messages flow upward and downward through post_message() and on_<widget>_<message> handlers. Screens can be pushed or popped to navigate views, and styling is applied with TCSS.

When to Use It

  • Starting a new Textual project to build a terminal UI
  • Adding and navigating multiple screens and widgets
  • Theming and styling with TCSS for consistent visuals
  • Implementing reactive state and event-driven interactions
  • Testing, debugging, and refining TUI behavior

Quick Start

  1. Step 1: Create a Textual App class and implement compose() to build UI elements (Header, Static, Footer) and define any bindings.
  2. Step 2: Add reactive state and handlers (e.g., on_button_pressed) to drive UI changes.
  3. Step 3: Run the script (e.g., python your_app.py) and iterate with TCSS styling and screen navigation.

Best Practices

  • Define a clear App → Screen → Widget hierarchy to keep complexity manageable
  • Use reactive and watch_<name> methods to drive UI changes rather than manual refreshes
  • Keep data flow predictable: pass attributes down, post messages up with post_message()
  • Handle events with on_<widget>_<message> and optionally constrain with CSS selectors
  • Build small, reusable custom widgets (Simple/Compound) and test them in isolation

Example Use Cases

  • Welcome screen with a 'Continue' button that pushes the main screen
  • A Hello World Static widget used to verify rendering
  • A Compound Widget combining a label and a button with DEFAULT_CSS
  • A Focusable Counter widget with keyboard bindings to increment/decrement
  • A simple layout using Vertical/Horizontal containers to organize content

Frequently Asked Questions

Add this skill to your agents
Sponsor this space

Reach thousands of developers