콘텐츠로 이동

Textual Cheat Sheet

Overview

Textual is a Python framework for building sophisticated Terminal User Interface (TUI) applications. Built on top of Rich, it provides a widget system with CSS-like styling, event handling, reactive data binding, and an async-first architecture. Textual apps look and behave like modern web applications but run entirely in the terminal.

Textual supports mouse input, scrolling, animations, and rich content rendering. Its CSS-based styling system allows separation of layout and appearance from logic, and its component model makes it easy to build complex, interactive applications for data visualization, dashboards, development tools, and more.

Installation

pip install textual

# With dev tools
pip install textual-dev

# Verify
python -m textual

Core Concepts

Minimal Application

from textual.app import App, ComposeResult
from textual.widgets import Header, Footer, Static

class MyApp(App):
    """A minimal Textual application."""

    CSS = """
    Screen {
        align: center middle;
    }
    #greeting {
        width: 50;
        height: 5;
        border: heavy green;
        content-align: center middle;
    }
    """

    def compose(self) -> ComposeResult:
        yield Header()
        yield Static("Hello, Textual!", id="greeting")
        yield Footer()

if __name__ == "__main__":
    app = MyApp()
    app.run()

Widgets and Layout

from textual.app import App, ComposeResult
from textual.containers import Container, Horizontal, Vertical
from textual.widgets import Header, Footer, Button, Static, Input, Label

class DashboardApp(App):
    CSS = """
    .sidebar {
        width: 30;
        background: $surface;
        border-right: solid $primary;
        padding: 1;
    }
    .main {
        padding: 1;
    }
    Button {
        margin: 1 0;
        width: 100%;
    }
    """

    def compose(self) -> ComposeResult:
        yield Header()
        with Horizontal():
            with Vertical(classes="sidebar"):
                yield Label("Navigation")
                yield Button("Dashboard", id="btn-dash")
                yield Button("Settings", id="btn-settings")
                yield Button("Logs", id="btn-logs")
            with Container(classes="main"):
                yield Static("Welcome to the Dashboard")
                yield Input(placeholder="Search...")
        yield Footer()

    def on_button_pressed(self, event: Button.Pressed) -> None:
        self.query_one(".main > Static", Static).update(
            f"Navigated to: {event.button.label}"
        )

Common Widgets

Data Table

from textual.app import App, ComposeResult
from textual.widgets import DataTable, Header, Footer

class TableApp(App):
    def compose(self) -> ComposeResult:
        yield Header()
        yield DataTable()
        yield Footer()

    def on_mount(self) -> None:
        table = self.query_one(DataTable)
        table.add_columns("Name", "Status", "CPU", "Memory")
        table.add_rows([
            ("web-01", "Running", "23%", "512MB"),
            ("web-02", "Running", "45%", "768MB"),
            ("worker-01", "Stopped", "0%", "0MB"),
            ("db-01", "Running", "67%", "2GB"),
        ])
        table.cursor_type = "row"

    def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None:
        self.notify(f"Selected row: {event.row_key}")

Input and Forms

from textual.app import App, ComposeResult
from textual.widgets import Input, Button, Static, Label
from textual.containers import Vertical

class FormApp(App):
    CSS = """
    Input { margin: 1 0; }
    Button { margin: 1 0; }
    """

    def compose(self) -> ComposeResult:
        with Vertical():
            yield Label("Login Form")
            yield Input(placeholder="Username", id="username")
            yield Input(placeholder="Password", password=True, id="password")
            yield Button("Submit", variant="primary")
            yield Static("", id="result")

    def on_button_pressed(self, event: Button.Pressed) -> None:
        username = self.query_one("#username", Input).value
        self.query_one("#result", Static).update(
            f"Logged in as: {username}"
        )

Tabs and Content Switching

from textual.app import App, ComposeResult
from textual.widgets import TabbedContent, TabPane, Static, Log

class TabbedApp(App):
    def compose(self) -> ComposeResult:
        with TabbedContent():
            with TabPane("Overview", id="overview"):
                yield Static("System overview content")
            with TabPane("Logs", id="logs"):
                yield Log()
            with TabPane("Config", id="config"):
                yield Static("Configuration settings")

CSS Styling

External CSS File

# app.py
class MyApp(App):
    CSS_PATH = "styles.tcss"
/* styles.tcss */
Screen {
    background: $surface;
}

.sidebar {
    width: 30;
    background: $panel;
    border-right: thick $primary;
    padding: 1 2;
}

.content {
    padding: 2 4;
}

Button {
    width: 100%;
    margin: 1 0;
}

Button:hover {
    background: $accent;
}

DataTable > .datatable--header {
    background: $primary;
    color: $text;
}

#status-ok {
    color: green;
}

#status-error {
    color: red;
    text-style: bold;
}

Configuration

Bindings and Actions

from textual.app import App, ComposeResult
from textual.binding import Binding
from textual.widgets import Header, Footer, Static

class MyApp(App):
    BINDINGS = [
        Binding("q", "quit", "Quit"),
        Binding("d", "toggle_dark", "Toggle Dark Mode"),
        Binding("r", "refresh", "Refresh"),
        Binding("ctrl+s", "save", "Save"),
    ]

    def compose(self) -> ComposeResult:
        yield Header()
        yield Static("Press keys to trigger actions", id="content")
        yield Footer()

    def action_toggle_dark(self) -> None:
        self.dark = not self.dark

    def action_refresh(self) -> None:
        self.query_one("#content", Static).update("Refreshed!")

    def action_save(self) -> None:
        self.notify("Saved!", severity="information")

Reactive Properties

from textual.app import App, ComposeResult
from textual.reactive import reactive
from textual.widgets import Static

class Counter(Static):
    count: reactive[int] = reactive(0)

    def render(self) -> str:
        return f"Count: {self.count}"

    def watch_count(self, value: int) -> None:
        if value > 10:
            self.styles.color = "red"

    def on_click(self) -> None:
        self.count += 1

Advanced Usage

Workers (Background Tasks)

from textual.app import App, ComposeResult
from textual.widgets import Static, Button
from textual.worker import Worker, WorkerState

class AsyncApp(App):
    def compose(self) -> ComposeResult:
        yield Button("Fetch Data", id="fetch")
        yield Static("Ready", id="status")

    async def on_button_pressed(self, event: Button.Pressed) -> None:
        self.run_worker(self.fetch_data())

    async def fetch_data(self) -> None:
        self.query_one("#status", Static).update("Loading...")
        import asyncio
        await asyncio.sleep(2)  # Simulate API call
        self.query_one("#status", Static).update("Data loaded!")

Screens and Navigation

from textual.app import App, ComposeResult
from textual.screen import Screen
from textual.widgets import Static, Button

class SettingsScreen(Screen):
    def compose(self) -> ComposeResult:
        yield Static("Settings Page")
        yield Button("Back", id="back")

    def on_button_pressed(self, event: Button.Pressed) -> None:
        if event.button.id == "back":
            self.app.pop_screen()

class MainApp(App):
    SCREENS = {"settings": SettingsScreen}

    def compose(self) -> ComposeResult:
        yield Button("Settings", id="settings")

    def on_button_pressed(self, event: Button.Pressed) -> None:
        if event.button.id == "settings":
            self.push_screen("settings")

Dev Tools

# Run with dev tools
textual run --dev my_app.py

# Console for debugging
textual console

# Take screenshots
textual run my_app.py --screenshot screenshot.svg

Troubleshooting

IssueSolution
App crashes on startCheck compose() returns valid widgets
CSS not applyingVerify CSS_PATH or CSS string; check selectors
Key bindings not workingEnsure BINDINGS list is correct; check focus
Slow renderingReduce widget count; use virtualized lists
Mouse not workingCheck terminal supports mouse events
Colors wrongVerify terminal color support; try --force-color
# Run with dev mode
textual run --dev app.py

# Debug CSS
textual run app.py --css-variables

# Check terminal capabilities
python -m textual