textual-app-lifecycle
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.
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
gitea-workflow
IncludedOrchestrate agile development workflows for Gitea repositories using the tea CLI. Use when working with Gitea-hosted repos and asking to 'run the workflow', 'continue working', 'what's next', 'complete the task cycle', 'start my day', 'end the sprint', 'implement the next task', or wanting guided step-by-step development assistance. Keywords: workflow, orchestrate, agile, task cycle, sprint, daily, implement, review, PR, standup, retrospective, gitea, tea.
microsoft-graph-gateway
IncludedRoute Microsoft Graph work in this workspace. Use when users want to read or write Outlook mail, calendar events, contacts, OneDrive or SharePoint files, Teams, Planner, To Do, users, groups, directory data, or arbitrary Microsoft Graph endpoints from VS Code. Prefer WorkIQ for common read scenarios. Use Microsoft Graph for write actions and gap-read scenarios that need exact Graph properties, filters, permissions, or endpoints.
copilotkit
IncludedUse when building with CopilotKit — setup, development, integrations, debugging, upgrading, or contributing. Routes to the appropriate specialized skill based on the task.
wordly-wisdom
IncludedProvides calibrated decision analysis using Charlie Munger-style multiple mental models, inversion, incentive mapping, circle-of-competence checks, misjudgment audits, second-order effects, and forecast updates. Use when the user asks for an oracle take, a hard call, a decision memo, a premortem, an outside view, a red-team, a sanity-check, what am I missing, think this through, or wants a strategy, hire, investment, plan, product, partnership, or major life choice analysed. Avoid for simple factual lookups or time-sensitive legal, medical, or market questions without fresh evidence.
swain-session
IncludedSession management and project status dashboard. Owns the full session lifecycle (start/work/close/resume), focus lane, bookmarks, worktree detection, and tab naming. Also serves as the project status dashboard — shows active epics, progress, actionable next steps, blocked items, tasks, GitHub issues, and recommendations. Worktree creation is deferred to swain-do task dispatch (SPEC-195). Triggers on: 'session', 'status', 'what's next', 'dashboard', 'overview', 'where are we', 'what should I work on', 'show me priorities', 'bookmark', 'focus on', 'session info'.
gandi
IncludedComprehensive Gandi domain registrar integration for domain and DNS management. Register and manage domains, create/update/delete DNS records (A, AAAA, CNAME, MX, TXT, SRV, and more), configure email forwarding and aliases, check SSL certificate status, create DNS snapshots for safe rollback, bulk update zone files, and monitor domain expiration. Supports multi-domain management, zone file import/export, and automated DNS backups. Includes both read-only and destructive operations with safety controls.