Claude
Skills
Sign in
Back

textual-app-lifecycle

Included with Lifetime
$97 forever

Builds Textual applications with proper app lifecycle management, screen handling, and event loops. Use when creating Textual App subclasses, implementing on_mount/on_unmount handlers, managing application state, navigating between screens, and understanding app initialization flow. Includes patterns for graceful shutdown, configuration loading, and daemon/background task management.

Productivity

What this skill does


# Textual App Lifecycle

## Purpose
Understand and implement proper Textual application lifecycle patterns including initialization, mounting, screen management, background workers, and graceful shutdown. This is the foundation for building robust TUI applications.

## Quick Start

```python
from textual.app import App, ComposeResult
from textual.binding import Binding
from textual.widgets import Header, Footer, Static
from typing import ClassVar

class MyApp(App):
    """Minimal Textual application with proper lifecycle."""

    TITLE = "My Application"
    SUB_TITLE = "Powered by Textual"

    BINDINGS: ClassVar[list[Binding]] = [
        Binding("q", "quit", "Quit", show=True, priority=True),
        Binding("ctrl+c", "quit", "Quit", show=False, priority=True),
    ]

    def compose(self) -> ComposeResult:
        """Compose the application layout."""
        yield Header(show_clock=True)
        yield Static("Welcome to Textual!", id="content")
        yield Footer()

    def on_mount(self) -> None:
        """Handle application mount event."""
        # Initialize application state, load configuration, etc.
        self.notify("Application started", severity="information")

    def action_quit(self) -> None:
        """Handle quit action with cleanup."""
        self.exit()

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

## Instructions

### Step 1: Define App Class and Metadata

Create your App subclass with proper metadata and configuration:

```python
from textual.app import App, ComposeResult
from textual.binding import Binding
from textual.screen import Screen
from typing import ClassVar

class MyApp(App[None]):  # [T] is the return type for app.run()
    """Application description."""

    # Metadata
    TITLE = "Application Title"
    SUB_TITLE = "Optional subtitle"

    # Keyboard bindings
    BINDINGS: ClassVar[list[Binding | tuple]] = [
        Binding("q", "quit", "Quit", show=True, priority=True),
        Binding("ctrl+c", "quit", "Quit", show=False, priority=True),
        ("r", "refresh", "Refresh"),
    ]

    # CSS styling (inline or separate .css file)
    CSS = """
    Screen {
        background: $surface;
    }
    """

    # Theme selection
    THEME = "dracula"  # Built-in themes: nord, dracula, monokai, solarized-dark, etc.
```

### Step 2: Implement Application Composition

Define `compose()` to return all top-level widgets:

```python
def compose(self) -> ComposeResult:
    """Compose the application layout.

    Yields:
        Application components (Header, Footer, widgets, etc).

    Yields immediately - don't use async here.
    """
    yield Header(show_clock=True)  # Optional header with clock
    yield StaticContent()           # Your custom widgets
    yield Footer()                  # Optional footer with bindings
```

**Key Points:**
- `compose()` is synchronous - don't use `await`
- Widgets are yielded in order (they appear top-to-bottom)
- Use Container, Horizontal, Vertical for layout
- Containers support CSS Grid layout

### Step 3: Handle App Initialization (on_mount)

Implement `on_mount()` for initialization after all widgets are mounted:

```python
def on_mount(self) -> None:
    """Handle application mount event.

    Called after compose() completes and all widgets are mounted.
    Use for:
    - Loading configuration
    - Initializing state
    - Starting background workers
    - Setting up timers
    - Querying mounted widgets
    """
    # Load configuration
    try:
        self._config = self._load_config()
    except Exception as e:
        self.notify(f"Config error: {e}", severity="error")
        return

    # Query and configure widgets
    content = self.query_one("#content", Static)
    content.update("Initialized!")

    # Set up auto-refresh timer (runs every N seconds)
    self.set_interval(5.0, self._on_timer)

    # Start background worker
    self.run_worker(self._background_task())

async def _background_task(self) -> None:
    """Background async task that runs concurrently."""
    while True:
        # Do async work (I/O, network, etc.)
        await asyncio.sleep(1)
        self._update_ui()

def _on_timer(self) -> None:
    """Called by timer periodically."""
    # Synchronous callback - don't use await
    pass
```

**Important Rules:**
- `on_mount()` is synchronous
- Can use `self.query_one()` to access widgets (they're mounted now)
- Use `self.run_worker()` to start async tasks
- Use `self.set_interval()` for periodic tasks
- Use `self.notify()` to show toast messages

### Step 4: Implement Action Handlers

Actions are triggered by keybindings and can be async:

```python
def action_refresh(self) -> None:
    """Synchronous action handler."""
    self.run_worker(self._async_refresh())

async def _async_refresh(self) -> None:
    """Async action implementation."""
    try:
        # Do async work (fetch data, update UI)
        data = await self._fetch_data()
        await self._update_ui(data)
    except Exception as e:
        self.notify(f"Refresh failed: {e}", severity="error")

def action_quit(self) -> None:
    """Exit the application."""
    # Cleanup happens automatically
    self.exit()

def action_add_item(self) -> None:
    """Action with optional argument."""
    self.run_worker(self._show_dialog())
```

**Pattern:**
- Keybindings call action methods
- Actions can be sync or async
- For async work, use `self.run_worker()`
- Action methods are public (no leading underscore)

### Step 5: Handle Screen Navigation

Switch between screens for modal dialogs, multi-screen apps:

```python
class MyApp(App):
    def action_show_settings(self) -> None:
        """Push settings screen on top of current screen."""
        self.push_screen(SettingsScreen())

    def action_close_settings(self) -> None:
        """Pop settings screen and return to previous."""
        self.pop_screen()

class SettingsScreen(Screen):
    """Modal settings dialog."""

    def compose(self) -> ComposeResult:
        yield Static("Settings")

    def on_mount(self) -> None:
        """Initialize settings screen."""
        pass

    def action_save(self) -> None:
        """Save settings and close."""
        self.pop_screen()  # Return to previous screen

    def action_cancel(self) -> None:
        """Close without saving."""
        self.pop_screen()
```

**Screen Stack:**
- `push_screen()` adds screen on top (modal behavior)
- `pop_screen()` removes top screen
- `switch_screen()` replaces current screen
- Screens can return values using `pop_screen(result)`

### Step 6: Implement Graceful Shutdown

Cleanup resources on application exit:

```python
def on_unmount(self) -> None:
    """Handle application unmount/exit.

    Called after widgets are unmounted but before app exits.
    Use for cleanup: closing connections, saving state, etc.
    """
    # Save state
    if self._config:
        self._config.save()

    # Close connections
    if self._connection:
        self._connection.close()

    # Cancel background tasks
    # (Textual handles this automatically)

async def on_shutdown(self) -> None:
    """Called when application is shutting down.

    This is async - use for async cleanup.
    """
    # Close async connections
    if self._async_client:
        await self._async_client.close()
```

## Examples

### Example 1: Dashboard with Auto-Refresh and Notifications

```python
import asyncio
from textual.app import App, ComposeResult
from textual.binding import Binding
from textual.widgets import Header, Footer, Static, Container
from typing import ClassVar

class DashboardApp(App):
    """Dashboard that auto-refreshes data."""

    TITLE = "Agent Dashboard"
    BINDINGS: ClassVar[list[Binding]] = [
        ("q", "quit", "Quit"),
        ("r", "refresh", "Refresh"),
    ]

    def __init__(self) -> None:
        super().__init__()
        self._data: dict | None = None
        self._refresh_task_id: str | None = None

    def compose(self) -> ComposeResult:
        

Related in Productivity