Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 17:58:08 +08:00
commit 9f0794c603
13 changed files with 4258 additions and 0 deletions

703
skill/SKILL.md Normal file
View File

@@ -0,0 +1,703 @@
---
name: textual-tui
description: Build modern, interactive terminal user interfaces with Textual. Use when creating command-line applications, dashboard tools, monitoring interfaces, data viewers, or any terminal-based UI. Covers architecture, widgets, layouts, styling, event handling, reactive programming, workers for background tasks, and testing patterns.
---
# Textual TUI Development
Build production-quality terminal user interfaces using Textual, a modern Python framework for creating interactive TUI applications.
## Quick Start
Install Textual:
```bash
pip install textual textual-dev
```
Basic app structure:
```python
from textual.app import App, ComposeResult
from textual.widgets import Header, Footer, Button
class MyApp(App):
"""A simple Textual app."""
def compose(self) -> ComposeResult:
"""Create child widgets."""
yield Header()
yield Button("Click me!", id="click")
yield Footer()
def on_button_pressed(self, event: Button.Pressed) -> None:
"""Handle button press."""
self.exit()
if __name__ == "__main__":
app = MyApp()
app.run()
```
Run with hot reload during development:
```bash
textual run --dev your_app.py
```
Use the Textual console for debugging:
```bash
textual console
```
## Core Architecture
### App Lifecycle
1. **Initialization**: Create App instance with config
2. **Composition**: Build widget tree via `compose()` method
3. **Mounting**: Widgets mounted to DOM
4. **Running**: Event loop processes messages and renders UI
5. **Shutdown**: Cleanup and exit
### Message Passing System
Textual uses an async message queue for all interactions:
```python
from textual.message import Message
class CustomMessage(Message):
"""Custom message with data."""
def __init__(self, value: int) -> None:
self.value = value
super().__init__()
class MyWidget(Widget):
def on_click(self) -> None:
# Post message to parent
self.post_message(CustomMessage(42))
class MyApp(App):
def on_custom_message(self, message: CustomMessage) -> None:
# Handle message with naming convention: on_{message_name}
self.log(f"Received: {message.value}")
```
### Reactive Programming
Use reactive attributes for automatic UI updates:
```python
from textual.reactive import reactive
class Counter(Widget):
count = reactive(0) # Reactive attribute
def watch_count(self, new_value: int) -> None:
"""Called automatically when count changes."""
self.refresh()
def increment(self) -> None:
self.count += 1 # Triggers watch_count
```
## Layout System
### Container Layouts
Textual provides flexible layout options:
**Vertical Layout (default)**:
```python
def compose(self) -> ComposeResult:
yield Label("Top")
yield Label("Bottom")
```
**Horizontal Layout**:
```python
class MyApp(App):
CSS = """
Screen {
layout: horizontal;
}
"""
```
**Grid Layout**:
```python
class MyApp(App):
CSS = """
Screen {
layout: grid;
grid-size: 3 2; /* 3 columns, 2 rows */
}
"""
```
### Sizing and Positioning
Control widget dimensions:
```python
class MyApp(App):
CSS = """
#sidebar {
width: 30; /* Fixed width */
height: 100%; /* Full height */
}
#content {
width: 1fr; /* Remaining space */
}
.compact {
height: auto; /* Size to content */
}
"""
```
## Styling with CSS
Textual uses CSS-like syntax for styling.
### Inline Styles
```python
class StyledWidget(Widget):
DEFAULT_CSS = """
StyledWidget {
background: $primary;
color: $text;
border: solid $accent;
padding: 1 2;
margin: 1;
}
"""
```
### External CSS Files
```python
class MyApp(App):
CSS_PATH = "app.tcss" # Load from file
```
### Color System
Use Textual's semantic colors:
```css
.error { background: $error; }
.success { background: $success; }
.warning { background: $warning; }
.primary { background: $primary; }
```
Or define custom colors:
```css
.custom {
background: #1e3a8a;
color: rgb(255, 255, 255);
}
```
## Common Widgets
### Input and Forms
```python
from textual.widgets import Input, Button, Select
from textual.containers import Container
def compose(self) -> ComposeResult:
with Container(id="form"):
yield Input(placeholder="Enter name", id="name")
yield Select(options=[("A", 1), ("B", 2)], id="choice")
yield Button("Submit", variant="primary")
def on_button_pressed(self, event: Button.Pressed) -> None:
name = self.query_one("#name", Input).value
choice = self.query_one("#choice", Select).value
```
### Data Display
```python
from textual.widgets import DataTable, Tree, Log
# DataTable for tabular data
table = DataTable()
table.add_columns("Name", "Age", "City")
table.add_row("Alice", 30, "NYC")
# Tree for hierarchical data
tree = Tree("Root")
tree.root.add("Child 1")
tree.root.add("Child 2")
# Log for streaming output
log = Log(auto_scroll=True)
log.write_line("Log entry")
```
### Containers and Layout
```python
from textual.containers import (
Container, Horizontal, Vertical,
Grid, ScrollableContainer
)
def compose(self) -> ComposeResult:
with Vertical():
yield Header()
with Horizontal():
with Container(id="sidebar"):
yield Label("Menu")
with ScrollableContainer(id="content"):
yield Label("Content...")
yield Footer()
```
## Event Handling
### Built-in Events
```python
from textual.events import Key, Click, Mount
def on_mount(self) -> None:
"""Called when widget is mounted."""
self.log("Widget mounted!")
def on_key(self, event: Key) -> None:
"""Handle all key presses."""
if event.key == "q":
self.app.exit()
def on_click(self, event: Click) -> None:
"""Handle mouse clicks."""
self.log(f"Clicked at {event.x}, {event.y}")
```
### Widget-Specific Handlers
```python
def on_input_submitted(self, event: Input.Submitted) -> None:
"""Handle input submission."""
self.query_one(Log).write(event.value)
def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None:
"""Handle table row selection."""
row_key = event.row_key
```
### Keyboard Bindings
```python
class MyApp(App):
BINDINGS = [
("q", "quit", "Quit"),
("d", "toggle_dark", "Toggle dark mode"),
("ctrl+s", "save", "Save"),
]
def action_quit(self) -> None:
self.exit()
def action_toggle_dark(self) -> None:
self.dark = not self.dark
```
## Advanced Patterns
### Custom Widgets
Create reusable components:
```python
from textual.widget import Widget
from textual.widgets import Label, Button
class StatusCard(Widget):
"""A card showing status info."""
def __init__(self, title: str, status: str) -> None:
super().__init__()
self.title = title
self.status = status
def compose(self) -> ComposeResult:
yield Label(self.title, classes="title")
yield Label(self.status, classes="status")
```
### Workers and Background Tasks
CRITICAL: Use workers for any long-running operations to prevent blocking the UI. The event loop must remain responsive.
#### Basic Worker Usage
Run tasks in background threads:
```python
from textual.worker import Worker, WorkerState
class MyApp(App):
def on_button_pressed(self, event: Button.Pressed) -> None:
# Start background task
self.run_worker(self.process_data(), exclusive=True)
async def process_data(self) -> str:
"""Long-running task."""
# Simulate work
await asyncio.sleep(5)
return "Processing complete"
```
#### Worker with Progress Updates
Update UI during processing:
```python
from textual.widgets import ProgressBar
class MyApp(App):
def compose(self) -> ComposeResult:
yield ProgressBar(total=100, id="progress")
def on_mount(self) -> None:
self.run_worker(self.long_task())
async def long_task(self) -> None:
"""Task with progress updates."""
progress = self.query_one(ProgressBar)
for i in range(100):
await asyncio.sleep(0.1)
progress.update(progress=i + 1)
# Use call_from_thread for thread safety
self.call_from_thread(progress.update, progress=i + 1)
```
#### Worker Communication Patterns
Use `call_from_thread` for thread-safe UI updates:
```python
import time
from threading import Thread
class MyApp(App):
def on_mount(self) -> None:
self.run_worker(self.fetch_data(), thread=True)
def fetch_data(self) -> None:
"""CPU-bound task in thread."""
# Blocking operation
result = expensive_computation()
# Update UI safely from thread
self.call_from_thread(self.display_result, result)
def display_result(self, result: str) -> None:
"""Called on main thread."""
self.query_one("#output").update(result)
```
#### Worker Cancellation
Cancel workers when no longer needed:
```python
class MyApp(App):
worker: Worker | None = None
def start_task(self) -> None:
# Store worker reference
self.worker = self.run_worker(self.long_task())
def cancel_task(self) -> None:
# Cancel running worker
if self.worker and not self.worker.is_finished:
self.worker.cancel()
self.notify("Task cancelled")
async def long_task(self) -> None:
for i in range(1000):
await asyncio.sleep(0.1)
# Check if cancelled
if self.worker.is_cancelled:
return
```
#### Worker Error Handling
Handle worker failures gracefully:
```python
class MyApp(App):
def on_mount(self) -> None:
worker = self.run_worker(self.risky_task())
worker.name = "data_processor" # Name for debugging
async def risky_task(self) -> str:
"""Task that might fail."""
try:
result = await fetch_from_api()
return result
except Exception as e:
self.notify(f"Error: {e}", severity="error")
raise
def on_worker_state_changed(self, event: Worker.StateChanged) -> None:
"""Handle worker state changes."""
if event.state == WorkerState.ERROR:
self.log.error(f"Worker failed: {event.worker.name}")
elif event.state == WorkerState.SUCCESS:
self.log.info(f"Worker completed: {event.worker.name}")
```
#### Multiple Workers
Manage concurrent workers:
```python
class MyApp(App):
def on_mount(self) -> None:
# Run multiple workers concurrently
self.run_worker(self.task_one(), name="task1", group="processing")
self.run_worker(self.task_two(), name="task2", group="processing")
self.run_worker(self.task_three(), name="task3", group="processing")
async def task_one(self) -> None:
await asyncio.sleep(2)
self.notify("Task 1 complete")
async def task_two(self) -> None:
await asyncio.sleep(3)
self.notify("Task 2 complete")
async def task_three(self) -> None:
await asyncio.sleep(1)
self.notify("Task 3 complete")
def cancel_all_tasks(self) -> None:
"""Cancel all workers in a group."""
for worker in self.workers:
if worker.group == "processing":
worker.cancel()
```
#### Thread vs Process Workers
Choose the right worker type:
```python
class MyApp(App):
def on_mount(self) -> None:
# Async task (default) - for I/O bound operations
self.run_worker(self.fetch_data())
# Thread worker - for CPU-bound tasks
self.run_worker(self.process_data(), thread=True)
async def fetch_data(self) -> str:
"""I/O bound: use async."""
async with httpx.AsyncClient() as client:
response = await client.get("https://api.example.com")
return response.text
def process_data(self) -> str:
"""CPU bound: use thread."""
# Heavy computation
result = [i**2 for i in range(1000000)]
return str(sum(result))
```
#### Worker Best Practices
1. **Always use workers for**:
- Network requests
- File I/O
- Database queries
- CPU-intensive computations
- Anything taking > 100ms
2. **Worker patterns**:
- Use `exclusive=True` to prevent duplicate workers
- Name workers for easier debugging
- Group related workers for batch cancellation
- Always handle worker errors
3. **Thread safety**:
- Use `call_from_thread()` for UI updates from threads
- Never modify widgets directly from threads
- Use locks for shared mutable state
4. **Cancellation**:
- Store worker references if you need to cancel
- Check `worker.is_cancelled` in long loops
- Clean up resources in finally blocks
### Modal Dialogs
```python
from textual.screen import ModalScreen
class ConfirmDialog(ModalScreen[bool]):
"""Modal confirmation dialog."""
def compose(self) -> ComposeResult:
with Container(id="dialog"):
yield Label("Are you sure?")
with Horizontal():
yield Button("Yes", variant="primary", id="yes")
yield Button("No", variant="error", id="no")
def on_button_pressed(self, event: Button.Pressed) -> None:
self.dismiss(event.button.id == "yes")
# Use in app
async def confirm_action(self) -> None:
result = await self.push_screen_wait(ConfirmDialog())
if result:
self.log("Confirmed!")
```
### Screens and Navigation
```python
from textual.screen import Screen
class MainScreen(Screen):
def compose(self) -> ComposeResult:
yield Header()
yield Button("Go to Settings")
yield Footer()
def on_button_pressed(self) -> None:
self.app.push_screen("settings")
class SettingsScreen(Screen):
def compose(self) -> ComposeResult:
yield Label("Settings")
yield Button("Back")
def on_button_pressed(self) -> None:
self.app.pop_screen()
class MyApp(App):
SCREENS = {
"main": MainScreen(),
"settings": SettingsScreen(),
}
```
## Testing
Test Textual apps with pytest and the Pilot API:
```python
import pytest
from textual.pilot import Pilot
from my_app import MyApp
@pytest.mark.asyncio
async def test_app_starts():
app = MyApp()
async with app.run_test() as pilot:
assert app.screen is not None
@pytest.mark.asyncio
async def test_button_click():
app = MyApp()
async with app.run_test() as pilot:
await pilot.click("#my-button")
# Assert expected state changes
@pytest.mark.asyncio
async def test_keyboard_input():
app = MyApp()
async with app.run_test() as pilot:
await pilot.press("q")
# Verify app exited or state changed
```
## Best Practices
### Performance
- Use `Lazy` for expensive widgets loaded on demand
- Implement efficient `render()` methods, avoid unnecessary work
- Use reactive attributes sparingly for truly dynamic values
- Batch UI updates when processing multiple changes
### State Management
- Keep app state in the App instance for global access
- Use reactive attributes for UI-bound state
- Store complex state in dedicated data models
- Avoid deeply nested widget communication
### Error Handling
```python
from textual.widgets import RichLog
def compose(self) -> ComposeResult:
yield RichLog(id="log")
async def action_risky_operation(self) -> None:
try:
result = await some_async_operation()
self.notify("Success!", severity="information")
except Exception as e:
self.notify(f"Error: {e}", severity="error")
self.query_one(RichLog).write(f"[red]Error:[/] {e}")
```
### Accessibility
- Always provide keyboard navigation
- Use semantic widget names and IDs
- Include ARIA-like descriptions where appropriate
- Test with screen reader compatibility in mind
## Development Tools
### Textual Console
Debug running apps:
```bash
# Terminal 1: Run console
textual console
# Terminal 2: Run app with console enabled
textual run --dev app.py
```
App code to enable console:
```python
self.log("Debug message") # Appears in console
self.log.info("Info level")
self.log.error("Error level")
```
### Textual Devtools
Use the devtools for live inspection:
```bash
pip install textual-dev
textual run --dev app.py # Enables hot reload
```
## References
- **Widget Gallery**: See references/widgets.md for comprehensive widget examples
- **Layout Patterns**: See references/layouts.md for common layout recipes
- **Styling Guide**: See references/styling.md for CSS patterns and themes
- **Official Guides Index**: See references/official-guides-index.md for URLs to all official Textual documentation guides (use web_fetch for detailed information on-demand)
- **Example Apps**: See assets/ for complete example applications
## Common Pitfalls
1. **Forgetting async/await**: Many Textual methods are async, always await them
2. **Blocking the event loop**: CRITICAL - Use `run_worker()` for long-running tasks (network, I/O, heavy computation). Never use `time.sleep()` or blocking operations in the main thread
3. **Incorrect message handling**: Method names must match `on_{message_name}` pattern
4. **CSS specificity issues**: Use IDs and classes appropriately for targeted styling
5. **Not using query methods**: Use `query_one()` and `query()` instead of manual traversal
6. **Thread safety violations**: Never modify widgets directly from worker threads - use `call_from_thread()`
7. **Not cancelling workers**: Workers continue running even when screens close - always cancel or store references
8. **Using time.sleep in async**: Use `await asyncio.sleep()` instead of `time.sleep()` in async functions
9. **Not handling worker errors**: Workers can fail silently - always implement error handling
10. **Wrong worker type**: Use async workers for I/O, thread workers for CPU-bound tasks

165
skill/assets/README.md Normal file
View File

@@ -0,0 +1,165 @@
# Example Textual Applications
Complete, working example applications demonstrating various Textual patterns and features.
## Running the Examples
Each example is a standalone Python file. To run:
```bash
# Install dependencies
pip install textual textual-dev psutil
# Run any example
python todo_app.py
python dashboard_app.py
python data_viewer.py
python worker_demo.py
# Or with hot reload during development
textual run --dev todo_app.py
```
## Examples
### todo_app.py - Todo List Application
A fully functional todo list demonstrating:
- Input handling and form validation
- List view with custom list items
- State management with reactive attributes
- Keyboard shortcuts and bindings
- Custom styling and theming
- Toggle states and visual feedback
**Key Features:**
- Add/delete/toggle todos
- Mark items as complete
- Statistics tracking
- Keyboard shortcuts (Ctrl+N, Space, Ctrl+D, etc.)
**Patterns Demonstrated:**
- Custom widget creation (TodoItem)
- Reactive state updates
- Event handling (button press, input submit)
- Keyboard bindings
- CSS styling with pseudo-classes
### dashboard_app.py - System Monitor Dashboard
A real-time system monitoring dashboard demonstrating:
- Grid layouts for dashboard design
- Reactive data updates
- Custom composite widgets
- Data visualization with Sparkline and ProgressBar
- Real-time monitoring with intervals
- Metric cards and charts
**Key Features:**
- Live CPU and memory monitoring
- Historical CPU usage chart
- System metrics (cores, frequency, processes, uptime)
- Auto-refresh every second
- Manual refresh with 'R' key
**Patterns Demonstrated:**
- Grid-based layouts
- Custom widget composition (MetricCard, CPUChart, MemoryChart)
- Timed updates with `set_interval()`
- Reactive attributes for live updates
- Data visualization components
### data_viewer.py - JSON/CSV Data Viewer
A file browser and data viewer demonstrating:
- File system navigation with DirectoryTree
- Multiple view modes (Table, Tree, Info) with tabs
- DataTable for tabular data
- Tree widget for hierarchical data
- Modal dialogs for errors
- Search and filter functionality
**Key Features:**
- Browse and select files from file system
- Load and display JSON and CSV files
- View data as table or tree structure
- File information panel
- Search functionality
- Error handling with modal dialogs
**Patterns Demonstrated:**
- Horizontal split layout (sidebar + content)
- Tabbed content interface
- Modal screen dialogs
- File I/O and data parsing
- Dynamic data loading
- Event handling across multiple widgets
### worker_demo.py - Background Task Processing
A comprehensive worker pattern demonstration with two apps:
**FileProcessor App:**
- Single long-running worker with progress updates
- Worker cancellation
- Real-time statistics (speed, time elapsed)
- Progress bar updates from worker
- Error handling and state management
**MultiWorkerDemo App:**
- Multiple concurrent workers
- Group-based worker management
- Batch cancellation
- Independent progress tracking per worker
**Key Features:**
- Simulated file processing with 100 files
- Real-time progress updates
- Worker state change monitoring
- Cancellation support
- Statistics tracking (files/sec, elapsed time)
**Patterns Demonstrated:**
- `run_worker()` for background tasks
- Worker lifecycle management
- Thread-safe UI updates
- Progress reporting from workers
- Worker cancellation and cleanup
- Multiple concurrent workers
- Worker groups and batch operations
- Error handling in workers
## Common Patterns
All examples demonstrate these fundamental patterns:
- Proper app structure with `compose()` method
- Header and Footer usage
- Keyboard bindings
- CSS styling with Textual's CSS system
- Event handling with `on_*` methods
- Reactive state management
- Widget querying with `query_one()` and `query()`
## Learning Path
1. **Start with todo_app.py** - Learn basic input, lists, and state management
2. **Move to dashboard_app.py** - Understand layouts, custom widgets, and real-time updates
3. **Try worker_demo.py** - Master background tasks and worker patterns (IMPORTANT!)
4. **Explore data_viewer.py** - Master complex layouts, tabs, and data handling
## Extending the Examples
Feel free to modify these examples:
- Add persistence to todo_app (save/load from file)
- Add network monitoring to dashboard_app
- Add export functionality to data_viewer
- Add real file processing to worker_demo (use actual files)
- Customize the styling and themes
- Add new features and widgets
## Resources
- Textual Documentation: https://textual.textualize.io/
- Widget Gallery: See references/widgets.md
- Layout Patterns: See references/layouts.md
- Styling Guide: See references/styling.md

View File

@@ -0,0 +1,256 @@
"""
System Monitor Dashboard - Textual TUI Example
Demonstrates:
- Grid layouts for dashboard design
- Reactive data updates
- Custom widgets
- Data visualization with Sparkline
- Real-time monitoring
- Worker threads for background tasks
"""
from textual.app import App, ComposeResult
from textual.containers import Container, Grid
from textual.widgets import Header, Footer, Static, Sparkline, ProgressBar
from textual.reactive import reactive
from textual.worker import Worker
import psutil
import time
from collections import deque
class MetricCard(Static):
"""A card displaying a metric with a label and value."""
DEFAULT_CSS = """
MetricCard {
border: solid $primary;
padding: 1;
height: 7;
background: $surface;
}
MetricCard .label {
text-style: bold;
color: $accent;
}
MetricCard .value {
text-align: center;
text-style: bold;
color: $success;
margin-top: 1;
}
"""
value = reactive("")
label = reactive("")
def __init__(self, label: str, initial_value: str = "0") -> None:
super().__init__()
self.label = label
self.value = initial_value
def compose(self) -> ComposeResult:
yield Static(self.label, classes="label")
yield Static(self.value, classes="value", id=f"value-{id(self)}")
def watch_value(self, new_value: str) -> None:
"""Update the value display when it changes."""
value_widget = self.query_one(f"#value-{id(self)}", Static)
value_widget.update(new_value)
class CPUChart(Static):
"""A widget showing CPU usage over time."""
DEFAULT_CSS = """
CPUChart {
border: solid $primary;
padding: 1;
height: 12;
background: $surface;
}
CPUChart .title {
text-style: bold;
color: $accent;
margin-bottom: 1;
}
"""
def __init__(self) -> None:
super().__init__()
self.history = deque([0] * 50, maxlen=50)
def compose(self) -> ComposeResult:
yield Static("CPU Usage History", classes="title")
yield Sparkline(list(self.history), id="cpu-sparkline")
yield ProgressBar(total=100, show_percentage=True, id="cpu-progress")
def update_cpu(self, value: float) -> None:
"""Update CPU usage display."""
self.history.append(value)
sparkline = self.query_one("#cpu-sparkline", Sparkline)
sparkline.data = list(self.history)
progress = self.query_one("#cpu-progress", ProgressBar)
progress.update(progress=value)
class MemoryChart(Static):
"""A widget showing memory usage."""
DEFAULT_CSS = """
MemoryChart {
border: solid $primary;
padding: 1;
height: 12;
background: $surface;
}
MemoryChart .title {
text-style: bold;
color: $accent;
margin-bottom: 1;
}
MemoryChart .detail {
margin-top: 1;
color: $text-muted;
}
"""
def compose(self) -> ComposeResult:
yield Static("Memory Usage", classes="title")
yield ProgressBar(total=100, show_percentage=True, id="mem-progress")
yield Static("", classes="detail", id="mem-detail")
def update_memory(self, percent: float, used: float, total: float) -> None:
"""Update memory usage display."""
progress = self.query_one("#mem-progress", ProgressBar)
progress.update(progress=percent)
detail = self.query_one("#mem-detail", Static)
detail.update(f"Used: {used:.1f} GB / Total: {total:.1f} GB")
class SystemMonitor(App):
"""A system monitoring dashboard."""
CSS = """
Screen {
background: $background;
}
Header {
background: $primary;
}
#dashboard {
padding: 1 2;
height: 1fr;
}
#metrics-grid {
grid-size: 4;
grid-gutter: 1 2;
height: auto;
margin-bottom: 1;
}
#charts-grid {
grid-size: 2;
grid-gutter: 1 2;
height: 1fr;
}
#status {
dock: bottom;
height: 1;
background: $panel;
padding: 0 2;
color: $text-muted;
}
"""
BINDINGS = [
("r", "refresh", "Refresh"),
("q", "quit", "Quit"),
]
update_count = reactive(0)
def compose(self) -> ComposeResult:
"""Compose the dashboard UI."""
yield Header(show_clock=True)
with Container(id="dashboard"):
with Grid(id="metrics-grid"):
yield MetricCard("CPU Cores", f"{psutil.cpu_count()}")
yield MetricCard("CPU Freq", "0 MHz")
yield MetricCard("Processes", "0")
yield MetricCard("Uptime", "0h 0m")
with Grid(id="charts-grid"):
yield CPUChart()
yield MemoryChart()
yield Static("", id="status")
yield Footer()
def on_mount(self) -> None:
"""Start monitoring when app starts."""
self.update_system_info()
self.set_interval(1.0, self.update_system_info)
def update_system_info(self) -> None:
"""Update all system information."""
self.update_count += 1
# Update metrics
cpu_freq = psutil.cpu_freq()
if cpu_freq:
freq_card = self.query("MetricCard")[1]
freq_card.value = f"{cpu_freq.current:.0f} MHz"
process_count = len(psutil.pids())
proc_card = self.query("MetricCard")[2]
proc_card.value = f"{process_count}"
boot_time = psutil.boot_time()
uptime_seconds = time.time() - boot_time
hours = int(uptime_seconds // 3600)
minutes = int((uptime_seconds % 3600) // 60)
uptime_card = self.query("MetricCard")[3]
uptime_card.value = f"{hours}h {minutes}m"
# Update CPU chart
cpu_percent = psutil.cpu_percent(interval=0.1)
cpu_chart = self.query_one(CPUChart)
cpu_chart.update_cpu(cpu_percent)
# Update memory chart
mem = psutil.virtual_memory()
mem_chart = self.query_one(MemoryChart)
mem_chart.update_memory(
mem.percent,
mem.used / (1024**3),
mem.total / (1024**3)
)
# Update status
status = self.query_one("#status", Static)
status.update(f"Last updated: {time.strftime('%H:%M:%S')} | Updates: {self.update_count}")
def action_refresh(self) -> None:
"""Manually refresh the data."""
self.update_system_info()
self.notify("Data refreshed!", severity="information")
if __name__ == "__main__":
app = SystemMonitor()
app.run()

304
skill/assets/data_viewer.py Normal file
View File

@@ -0,0 +1,304 @@
"""
Data Viewer Application - Textual TUI Example
Demonstrates:
- File browser and selection
- DataTable for displaying tabular data
- Tree for displaying hierarchical data
- Modal screens for dialogs
- File loading and parsing
- Search and filter functionality
"""
from textual.app import App, ComposeResult
from textual.containers import Container, Horizontal, Vertical
from textual.widgets import (
Header, Footer, DirectoryTree, DataTable, Tree, Static,
Button, Input, TabbedContent, TabPane
)
from textual.screen import ModalScreen
from textual.binding import Binding
import json
import csv
from pathlib import Path
class ErrorDialog(ModalScreen[bool]):
"""Modal dialog for displaying errors."""
DEFAULT_CSS = """
ErrorDialog {
align: center middle;
}
#error-dialog {
width: 60;
height: 11;
border: thick $error;
background: $surface;
padding: 1 2;
}
#error-title {
text-style: bold;
color: $error;
margin-bottom: 1;
}
#error-message {
margin-bottom: 1;
}
"""
def __init__(self, title: str, message: str) -> None:
super().__init__()
self.title = title
self.message = message
def compose(self) -> ComposeResult:
with Container(id="error-dialog"):
yield Static(self.title, id="error-title")
yield Static(self.message, id="error-message")
yield Button("OK", variant="error", id="ok")
def on_button_pressed(self, event: Button.Pressed) -> None:
self.dismiss(True)
class DataViewer(App):
"""A data viewer for JSON and CSV files."""
CSS = """
Screen {
background: $background;
layout: horizontal;
}
Header {
background: $primary;
}
#sidebar {
width: 35;
border-right: solid $accent;
background: $panel;
}
#file-tree {
height: 1fr;
}
#main-content {
width: 1fr;
padding: 1 2;
}
#toolbar {
height: auto;
margin-bottom: 1;
}
#search-input {
width: 1fr;
margin-right: 1;
}
#content-tabs {
height: 1fr;
}
DataTable {
height: 100%;
}
Tree {
height: 100%;
}
#info-panel {
border: solid $primary;
padding: 1;
background: $surface;
}
"""
BINDINGS = [
Binding("ctrl+o", "open_file", "Open File"),
Binding("ctrl+f", "focus_search", "Search"),
("q", "quit", "Quit"),
]
def compose(self) -> ComposeResult:
"""Compose the UI."""
yield Header(show_clock=True)
with Container(id="sidebar"):
yield Static("📁 File Browser", id="sidebar-title")
yield DirectoryTree(str(Path.home()), id="file-tree")
with Vertical(id="main-content"):
with Horizontal(id="toolbar"):
yield Input(placeholder="Search...", id="search-input")
yield Button("Clear", id="clear-button")
with TabbedContent(id="content-tabs"):
with TabPane("Table View", id="table-tab"):
yield DataTable(id="data-table", cursor_type="row")
with TabPane("Tree View", id="tree-tab"):
yield Tree("Data", id="data-tree")
with TabPane("Info", id="info-tab"):
yield Static("No file loaded", id="info-panel")
yield Footer()
def on_mount(self) -> None:
"""Initialize the app."""
table = self.query_one("#data-table", DataTable)
table.show_header = True
table.zebra_stripes = True
def on_directory_tree_file_selected(self, event: DirectoryTree.FileSelected) -> None:
"""Handle file selection from the tree."""
file_path = event.path
if file_path.suffix.lower() in ['.json', '.csv']:
self.load_file(file_path)
else:
self.notify("Only JSON and CSV files are supported", severity="warning")
def load_file(self, file_path: Path) -> None:
"""Load and display a data file."""
try:
if file_path.suffix.lower() == '.json':
self.load_json(file_path)
elif file_path.suffix.lower() == '.csv':
self.load_csv(file_path)
self.notify(f"Loaded: {file_path.name}", severity="information")
except Exception as e:
self.push_screen(
ErrorDialog("Error Loading File", f"Failed to load {file_path.name}:\n{str(e)}")
)
def load_json(self, file_path: Path) -> None:
"""Load a JSON file."""
with open(file_path, 'r') as f:
data = json.load(f)
# Update info panel
info = self.query_one("#info-panel", Static)
info.update(f"File: {file_path.name}\nType: JSON\nSize: {file_path.stat().st_size} bytes")
# Update tree view
tree = self.query_one("#data-tree", Tree)
tree.clear()
tree.root.label = file_path.name
self._build_json_tree(tree.root, data)
# Update table view (if data is a list of dicts)
if isinstance(data, list) and data and isinstance(data[0], dict):
self._populate_table_from_list(data)
else:
table = self.query_one("#data-table", DataTable)
table.clear(columns=True)
self.notify("JSON structure not suitable for table view", severity="warning")
def load_csv(self, file_path: Path) -> None:
"""Load a CSV file."""
with open(file_path, 'r') as f:
reader = csv.DictReader(f)
data = list(reader)
# Update info panel
info = self.query_one("#info-panel", Static)
info.update(
f"File: {file_path.name}\n"
f"Type: CSV\n"
f"Rows: {len(data)}\n"
f"Columns: {len(data[0]) if data else 0}"
)
# Update table view
self._populate_table_from_list(data)
# Update tree view
tree = self.query_one("#data-tree", Tree)
tree.clear()
tree.root.label = file_path.name
for i, row in enumerate(data[:100]): # Limit to 100 for performance
row_node = tree.root.add(f"Row {i+1}")
for key, value in row.items():
row_node.add_leaf(f"{key}: {value}")
def _populate_table_from_list(self, data: list) -> None:
"""Populate the DataTable from a list of dicts."""
if not data:
return
table = self.query_one("#data-table", DataTable)
table.clear(columns=True)
# Add columns
columns = list(data[0].keys())
table.add_columns(*columns)
# Add rows
for row in data:
table.add_row(*[str(row.get(col, "")) for col in columns])
def _build_json_tree(self, node, data, max_depth: int = 10, depth: int = 0) -> None:
"""Recursively build a tree from JSON data."""
if depth >= max_depth:
node.add_leaf("... (max depth reached)")
return
if isinstance(data, dict):
for key, value in data.items():
if isinstance(value, (dict, list)):
child = node.add(f"📂 {key}")
self._build_json_tree(child, value, max_depth, depth + 1)
else:
node.add_leaf(f"{key}: {value}")
elif isinstance(data, list):
for i, item in enumerate(data[:50]): # Limit to 50 items for performance
if isinstance(item, (dict, list)):
child = node.add(f"[{i}]")
self._build_json_tree(child, item, max_depth, depth + 1)
else:
node.add_leaf(f"[{i}]: {item}")
def on_input_changed(self, event: Input.Changed) -> None:
"""Handle search input changes."""
if event.input.id == "search-input":
search_term = event.value.lower()
if search_term:
self.filter_table(search_term)
def filter_table(self, search_term: str) -> None:
"""Filter table rows based on search term."""
table = self.query_one("#data-table", DataTable)
# Note: This is a simple example. Real implementation would need
# to store original data and rebuild filtered rows
pass
def on_button_pressed(self, event: Button.Pressed) -> None:
"""Handle button presses."""
if event.button.id == "clear-button":
search_input = self.query_one("#search-input", Input)
search_input.value = ""
def action_focus_search(self) -> None:
"""Focus the search input."""
self.query_one("#search-input", Input).focus()
def action_open_file(self) -> None:
"""Focus the file tree."""
self.query_one("#file-tree", DirectoryTree).focus()
if __name__ == "__main__":
app = DataViewer()
app.run()

236
skill/assets/todo_app.py Normal file
View File

@@ -0,0 +1,236 @@
"""
Todo List Application - Complete Textual TUI Example
A fully functional todo list app demonstrating:
- Input handling and forms
- List view and selection
- State management with reactive attributes
- Custom styling
- Keyboard shortcuts
"""
from textual.app import App, ComposeResult
from textual.containers import Container, Horizontal, Vertical
from textual.widgets import Header, Footer, Input, Button, ListView, ListItem, Label, Static
from textual.binding import Binding
from textual.reactive import reactive
class TodoItem(ListItem):
"""A todo list item with completion status."""
def __init__(self, text: str, completed: bool = False) -> None:
super().__init__()
self.text = text
self.completed = completed
self.update_display()
def update_display(self) -> None:
"""Update the display based on completion status."""
if self.completed:
self.add_class("completed")
prefix = ""
else:
self.remove_class("completed")
prefix = ""
# Clear and re-render
self._nodes.clear()
self._nodes.append(Label(f"{prefix} {self.text}"))
def toggle(self) -> None:
"""Toggle completion status."""
self.completed = not self.completed
self.update_display()
class TodoApp(App):
"""A todo list application."""
CSS = """
Screen {
background: $background;
layout: vertical;
}
Header {
background: $primary;
}
#main-container {
height: 1fr;
padding: 1 2;
}
#input-container {
height: auto;
margin-bottom: 1;
}
#todo-input {
width: 1fr;
margin-right: 1;
border: solid $accent;
}
#todo-input:focus {
border: solid $success;
}
#add-button {
min-width: 12;
}
#todo-list {
border: solid $primary;
height: 1fr;
background: $surface;
}
#stats {
height: 3;
margin-top: 1;
border: solid $accent;
padding: 0 2;
background: $panel;
}
TodoItem {
padding: 0 1;
}
TodoItem:hover {
background: $boost;
}
TodoItem.completed {
color: $text-muted;
text-style: strike;
}
.stat {
margin: 0 2;
}
"""
BINDINGS = [
Binding("ctrl+n", "new_todo", "New Todo"),
Binding("ctrl+d", "delete_todo", "Delete"),
Binding("space", "toggle_todo", "Toggle"),
Binding("ctrl+c", "clear_completed", "Clear Completed"),
("q", "quit", "Quit"),
]
total_count = reactive(0)
completed_count = reactive(0)
def compose(self) -> ComposeResult:
"""Compose the UI."""
yield Header(show_clock=True)
with Container(id="main-container"):
with Horizontal(id="input-container"):
yield Input(
placeholder="What needs to be done?",
id="todo-input"
)
yield Button("Add", variant="primary", id="add-button")
yield ListView(id="todo-list")
with Horizontal(id="stats"):
yield Static("", id="total-stat", classes="stat")
yield Static("", id="completed-stat", classes="stat")
yield Static("", id="remaining-stat", classes="stat")
yield Footer()
def on_mount(self) -> None:
"""Initialize the app."""
self.query_one("#todo-input").focus()
self.update_stats()
# Add some example todos
self.add_todo_item("Learn Textual", False)
self.add_todo_item("Build amazing TUI apps", False)
self.add_todo_item("Share with the world", False)
def on_button_pressed(self, event: Button.Pressed) -> None:
"""Handle button press."""
if event.button.id == "add-button":
self.add_todo_from_input()
def on_input_submitted(self, event: Input.Submitted) -> None:
"""Handle input submission."""
if event.input.id == "todo-input":
self.add_todo_from_input()
def on_list_view_selected(self, event: ListView.Selected) -> None:
"""Handle todo item selection."""
if isinstance(event.item, TodoItem):
event.item.toggle()
self.update_stats()
def add_todo_from_input(self) -> None:
"""Add a todo from the input field."""
todo_input = self.query_one("#todo-input", Input)
text = todo_input.value.strip()
if text:
self.add_todo_item(text)
todo_input.value = ""
todo_input.focus()
def add_todo_item(self, text: str, completed: bool = False) -> None:
"""Add a todo item to the list."""
list_view = self.query_one("#todo-list", ListView)
todo = TodoItem(text, completed)
list_view.append(todo)
self.update_stats()
def action_new_todo(self) -> None:
"""Focus the input for a new todo."""
self.query_one("#todo-input").focus()
def action_delete_todo(self) -> None:
"""Delete the selected todo."""
list_view = self.query_one("#todo-list", ListView)
if list_view.highlighted_child:
list_view.highlighted_child.remove()
self.update_stats()
def action_toggle_todo(self) -> None:
"""Toggle the selected todo."""
list_view = self.query_one("#todo-list", ListView)
if isinstance(list_view.highlighted_child, TodoItem):
list_view.highlighted_child.toggle()
self.update_stats()
def action_clear_completed(self) -> None:
"""Clear all completed todos."""
list_view = self.query_one("#todo-list", ListView)
for item in list_view.query(TodoItem):
if item.completed:
item.remove()
self.update_stats()
def update_stats(self) -> None:
"""Update the statistics display."""
list_view = self.query_one("#todo-list", ListView)
todos = list(list_view.query(TodoItem))
total = len(todos)
completed = sum(1 for t in todos if t.completed)
remaining = total - completed
self.total_count = total
self.completed_count = completed
self.query_one("#total-stat", Static).update(f"Total: {total}")
self.query_one("#completed-stat", Static).update(f"Completed: {completed}")
self.query_one("#remaining-stat", Static).update(f"Remaining: {remaining}")
if __name__ == "__main__":
app = TodoApp()
app.run()

406
skill/assets/worker_demo.py Normal file
View File

@@ -0,0 +1,406 @@
"""
File Processor - Textual TUI Worker Example
Demonstrates:
- Worker patterns for background tasks
- Progress updates from workers
- Worker cancellation
- Error handling in workers
- Multiple concurrent workers
- Thread-safe UI updates
"""
from textual.app import App, ComposeResult
from textual.containers import Container, Vertical, Horizontal
from textual.widgets import Header, Footer, Button, ProgressBar, Log, Static, Label
from textual.worker import Worker, WorkerState
from pathlib import Path
import asyncio
import time
import hashlib
class FileProcessor(App):
"""File processing app demonstrating worker patterns."""
CSS = """
Screen {
background: $background;
}
Header {
background: $primary;
}
#main {
padding: 1 2;
height: 1fr;
}
#controls {
height: auto;
margin-bottom: 1;
}
Button {
margin: 0 1;
}
#progress-container {
border: solid $primary;
padding: 1;
margin-bottom: 1;
height: auto;
background: $surface;
}
#progress-label {
margin-bottom: 1;
text-style: bold;
color: $accent;
}
ProgressBar {
margin-bottom: 1;
}
#stats {
height: 5;
border: solid $accent;
padding: 1;
margin-bottom: 1;
background: $panel;
}
.stat-line {
margin: 0;
}
#log {
border: solid $primary;
height: 1fr;
background: $surface;
}
"""
BINDINGS = [
("s", "start", "Start"),
("c", "cancel", "Cancel"),
("r", "reset", "Reset"),
("q", "quit", "Quit"),
]
def __init__(self) -> None:
super().__init__()
self.main_worker: Worker | None = None
self.processed_count = 0
self.total_files = 0
self.start_time = 0.0
def compose(self) -> ComposeResult:
"""Compose the UI."""
yield Header(show_clock=True)
with Container(id="main"):
with Horizontal(id="controls"):
yield Button("Start Processing", variant="success", id="start")
yield Button("Cancel", variant="error", id="cancel", disabled=True)
yield Button("Reset", id="reset")
with Container(id="progress-container"):
yield Label("Progress", id="progress-label")
yield ProgressBar(total=100, show_percentage=True, id="progress")
with Container(id="stats"):
yield Static("Status: Idle", classes="stat-line", id="status")
yield Static("Files Processed: 0 / 0", classes="stat-line", id="files")
yield Static("Time Elapsed: 0s", classes="stat-line", id="time")
yield Static("Speed: 0 files/sec", classes="stat-line", id="speed")
yield Log(id="log", auto_scroll=True)
yield Footer()
def on_mount(self) -> None:
"""Initialize on mount."""
log = self.query_one("#log", Log)
log.write_line("[bold cyan]File Processor Started[/]")
log.write_line("Click 'Start Processing' or press 'S' to begin")
def on_button_pressed(self, event: Button.Pressed) -> None:
"""Handle button presses."""
if event.button.id == "start":
self.action_start()
elif event.button.id == "cancel":
self.action_cancel()
elif event.button.id == "reset":
self.action_reset()
def action_start(self) -> None:
"""Start file processing worker."""
if self.main_worker and not self.main_worker.is_finished:
self.log_message("[yellow]Processing already in progress[/]")
return
# Disable start button, enable cancel
self.query_one("#start", Button).disabled = True
self.query_one("#cancel", Button).disabled = False
# Reset counters
self.processed_count = 0
self.total_files = 100 # Simulated file count
self.start_time = time.time()
# Start worker
self.log_message("[bold green]Starting file processing...[/]")
self.main_worker = self.run_worker(
self.process_files(),
name="file_processor",
group="processing"
)
def action_cancel(self) -> None:
"""Cancel the running worker."""
if self.main_worker and not self.main_worker.is_finished:
self.main_worker.cancel()
self.log_message("[bold red]Cancelling worker...[/]")
def action_reset(self) -> None:
"""Reset the UI."""
if self.main_worker and not self.main_worker.is_finished:
self.log_message("[yellow]Cannot reset while processing[/]")
return
self.processed_count = 0
self.total_files = 0
progress = self.query_one("#progress", ProgressBar)
progress.update(progress=0)
self.update_stats()
self.log_message("[cyan]Reset complete[/]")
async def process_files(self) -> None:
"""
Worker task: Process files in background.
Demonstrates:
- Long-running async operations
- Progress updates via call_from_thread
- Cancellation checking
- Error handling
"""
try:
for i in range(self.total_files):
# Check if cancelled
if self.main_worker and self.main_worker.is_cancelled:
self.log_message("[red]Processing cancelled![/]")
self.finalize_processing(cancelled=True)
return
# Simulate file processing (async I/O)
await asyncio.sleep(0.05)
# Simulate some work
filename = f"file_{i:04d}.txt"
hash_val = hashlib.md5(filename.encode()).hexdigest()[:8]
# Update progress (thread-safe)
self.processed_count = i + 1
progress = (self.processed_count / self.total_files) * 100
# Update UI from worker
self.update_progress(progress, filename, hash_val)
# Log every 10th file
if (i + 1) % 10 == 0:
self.log_message(
f"[green]Processed {self.processed_count}/{self.total_files} files[/]"
)
# Completed successfully
self.log_message("[bold green]✓ All files processed successfully![/]")
self.finalize_processing(cancelled=False)
except Exception as e:
self.log_message(f"[bold red]Error: {e}[/]")
self.finalize_processing(cancelled=False)
raise
def update_progress(self, progress: float, filename: str, hash_val: str) -> None:
"""Update progress bar and stats (called from worker)."""
progress_bar = self.query_one("#progress", ProgressBar)
progress_bar.update(progress=progress)
# Update stats
self.update_stats()
# Update status
status = self.query_one("#status", Static)
status.update(f"Status: Processing {filename} [{hash_val}]")
def update_stats(self) -> None:
"""Update statistics display."""
files_stat = self.query_one("#files", Static)
files_stat.update(f"Files Processed: {self.processed_count} / {self.total_files}")
elapsed = time.time() - self.start_time if self.start_time > 0 else 0
time_stat = self.query_one("#time", Static)
time_stat.update(f"Time Elapsed: {elapsed:.1f}s")
speed = self.processed_count / elapsed if elapsed > 0 else 0
speed_stat = self.query_one("#speed", Static)
speed_stat.update(f"Speed: {speed:.1f} files/sec")
def finalize_processing(self, cancelled: bool) -> None:
"""Finalize after processing completes or is cancelled."""
# Re-enable buttons
self.query_one("#start", Button).disabled = False
self.query_one("#cancel", Button).disabled = True
# Update status
status = self.query_one("#status", Static)
if cancelled:
status.update("Status: Cancelled")
else:
status.update("Status: Complete")
def log_message(self, message: str) -> None:
"""Thread-safe log message."""
log = self.query_one("#log", Log)
log.write_line(message)
def on_worker_state_changed(self, event: Worker.StateChanged) -> None:
"""Handle worker state changes."""
if event.worker.name != "file_processor":
return
if event.state == WorkerState.PENDING:
self.log_message("[cyan]Worker: Pending...[/]")
elif event.state == WorkerState.RUNNING:
self.log_message("[cyan]Worker: Running[/]")
elif event.state == WorkerState.CANCELLED:
self.log_message("[red]Worker: Cancelled[/]")
elif event.state == WorkerState.ERROR:
self.log_message(f"[bold red]Worker: Error - {event.worker.error}[/]")
elif event.state == WorkerState.SUCCESS:
self.log_message("[green]Worker: Completed successfully[/]")
class MultiWorkerDemo(App):
"""Demonstrates running multiple concurrent workers."""
CSS = """
Screen {
background: $background;
}
#main {
padding: 1 2;
}
#controls {
height: auto;
margin-bottom: 1;
}
Button {
margin: 0 1;
}
.worker-card {
border: solid $primary;
padding: 1;
margin-bottom: 1;
background: $surface;
}
.worker-card .title {
text-style: bold;
color: $accent;
margin-bottom: 1;
}
#log {
border: solid $primary;
height: 20;
}
"""
BINDINGS = [
("s", "start_all", "Start All"),
("c", "cancel_all", "Cancel All"),
("q", "quit", "Quit"),
]
def compose(self) -> ComposeResult:
yield Header()
with Container(id="main"):
with Horizontal(id="controls"):
yield Button("Start All Workers", variant="success", id="start")
yield Button("Cancel All", variant="error", id="cancel")
with Container(classes="worker-card"):
yield Label("Worker 1: Data Fetch", classes="title")
yield ProgressBar(total=100, id="worker1")
with Container(classes="worker-card"):
yield Label("Worker 2: Data Process", classes="title")
yield ProgressBar(total=100, id="worker2")
with Container(classes="worker-card"):
yield Label("Worker 3: Data Export", classes="title")
yield ProgressBar(total=100, id="worker3")
yield Log(id="log", auto_scroll=True)
yield Footer()
def on_button_pressed(self, event: Button.Pressed) -> None:
if event.button.id == "start":
self.action_start_all()
elif event.button.id == "cancel":
self.action_cancel_all()
def action_start_all(self) -> None:
"""Start all workers concurrently."""
self.run_worker(self.worker_task(1, 3.0), name="worker1", group="tasks")
self.run_worker(self.worker_task(2, 5.0), name="worker2", group="tasks")
self.run_worker(self.worker_task(3, 4.0), name="worker3", group="tasks")
log = self.query_one("#log", Log)
log.write_line("[bold cyan]Started all workers concurrently[/]")
def action_cancel_all(self) -> None:
"""Cancel all workers in the group."""
for worker in self.workers:
if worker.group == "tasks":
worker.cancel()
log = self.query_one("#log", Log)
log.write_line("[bold red]Cancelled all workers[/]")
async def worker_task(self, worker_num: int, duration: float) -> None:
"""Simulated worker task."""
log = self.query_one("#log", Log)
log.write_line(f"[cyan]Worker {worker_num} started[/]")
progress_bar = self.query_one(f"#worker{worker_num}", ProgressBar)
steps = 100
for i in range(steps):
await asyncio.sleep(duration / steps)
progress_bar.update(progress=i + 1)
log.write_line(f"[green]Worker {worker_num} completed![/]")
if __name__ == "__main__":
# Run the single worker demo
app = FileProcessor()
app.run()
# Or run the multi-worker demo
# app = MultiWorkerDemo()
# app.run()

575
skill/references/layouts.md Normal file
View File

@@ -0,0 +1,575 @@
# Textual Layout Patterns
Common layout recipes for Textual applications.
## Layout Types
### Vertical (Default)
Stack widgets vertically:
```python
from textual.app import App, ComposeResult
from textual.widgets import Label
class VerticalApp(App):
def compose(self) -> ComposeResult:
yield Label("Top")
yield Label("Middle")
yield Label("Bottom")
# Or explicit CSS
CSS = """
Screen {
layout: vertical;
}
"""
```
### Horizontal
Arrange widgets side-by-side:
```python
from textual.containers import Horizontal
def compose(self) -> ComposeResult:
with Horizontal():
yield Label("Left")
yield Label("Center")
yield Label("Right")
# Or via CSS
CSS = """
Horizontal {
height: 100%;
}
Horizontal > Label {
width: 1fr; /* Equal distribution */
}
"""
```
### Grid
Create grid layouts:
```python
class GridApp(App):
CSS = """
Screen {
layout: grid;
grid-size: 3 2; /* 3 columns, 2 rows */
grid-gutter: 1;
}
.cell {
border: solid $accent;
height: 100%;
}
"""
def compose(self) -> ComposeResult:
for i in range(6):
yield Label(f"Cell {i+1}", classes="cell")
```
Advanced grid with spanning:
```python
CSS = """
Screen {
layout: grid;
grid-size: 4; /* 4 columns, auto rows */
}
#header {
column-span: 4; /* Spans all columns */
}
#sidebar {
row-span: 2; /* Spans 2 rows */
}
"""
```
### Dock Layout
Dock widgets to edges:
```python
from textual.widgets import Header, Footer
class DockedApp(App):
def compose(self) -> ComposeResult:
yield Header() # Docked to top
yield Label("Content") # Takes remaining space
yield Footer() # Docked to bottom
# Custom docking
CSS = """
#sidebar {
dock: left;
width: 30;
}
#toolbar {
dock: top;
height: 3;
}
"""
```
## Common Patterns
### Split Screen (Vertical)
Two panels side-by-side:
```python
class SplitScreen(App):
CSS = """
Screen {
layout: horizontal;
}
#left-panel {
width: 30%;
border-right: solid $accent;
}
#right-panel {
width: 70%;
}
"""
def compose(self) -> ComposeResult:
with Container(id="left-panel"):
yield Label("Sidebar")
with Container(id="right-panel"):
yield Label("Main content")
```
### Split Screen (Horizontal)
Two panels stacked:
```python
class SplitScreenHorizontal(App):
CSS = """
Screen {
layout: vertical;
}
#top-panel {
height: 50%;
border-bottom: solid $accent;
}
#bottom-panel {
height: 50%;
}
"""
def compose(self) -> ComposeResult:
with Container(id="top-panel"):
yield Label("Top content")
with Container(id="bottom-panel"):
yield Label("Bottom content")
```
### Three-Column Layout
Classic sidebar-content-sidebar:
```python
class ThreeColumn(App):
CSS = """
Screen {
layout: horizontal;
}
#left-sidebar {
width: 20;
}
#content {
width: 1fr; /* Take remaining space */
}
#right-sidebar {
width: 25;
}
"""
def compose(self) -> ComposeResult:
with Container(id="left-sidebar"):
yield Label("Menu")
with Container(id="content"):
yield Label("Main")
with Container(id="right-sidebar"):
yield Label("Info")
```
### Dashboard Grid
Grid-based dashboard:
```python
class Dashboard(App):
CSS = """
Screen {
layout: grid;
grid-size: 2 3; /* 2 columns, 3 rows */
grid-gutter: 1 2; /* vertical horizontal */
}
#header {
column-span: 2;
height: 3;
}
.metric-card {
border: solid $primary;
padding: 1;
}
"""
def compose(self) -> ComposeResult:
yield Header(id="header")
yield Static("Users: 1,234", classes="metric-card")
yield Static("Revenue: $12K", classes="metric-card")
yield Static("Growth: +15%", classes="metric-card")
yield Static("Active: 567", classes="metric-card")
```
### Centered Content
Center content horizontally and vertically:
```python
class CenteredApp(App):
CSS = """
Screen {
align: center middle;
}
#dialog {
width: 60;
height: 20;
border: thick $accent;
padding: 2;
background: $surface;
}
"""
def compose(self) -> ComposeResult:
with Container(id="dialog"):
yield Label("Centered Dialog")
yield Button("OK")
```
### Scrollable Content
Handle overflow with scrolling:
```python
from textual.containers import ScrollableContainer
class ScrollableApp(App):
CSS = """
#content {
height: 100%;
border: solid $primary;
}
"""
def compose(self) -> ComposeResult:
with ScrollableContainer(id="content"):
for i in range(100):
yield Label(f"Line {i+1}")
```
### Tabbed Interface
Tab-based navigation:
```python
from textual.widgets import TabbedContent, TabPane
class TabbedApp(App):
def compose(self) -> ComposeResult:
with TabbedContent():
with TabPane("Dashboard"):
yield Label("Dashboard content")
with TabPane("Users"):
yield Label("Users content")
with TabPane("Settings"):
yield Label("Settings content")
```
## Sizing Strategies
### Fixed Sizes
Absolute dimensions:
```css
#widget {
width: 40; /* 40 columns */
height: 20; /* 20 rows */
}
```
### Fractional Units
Proportional sizing:
```css
#sidebar {
width: 1fr; /* 1 part */
}
#content {
width: 3fr; /* 3 parts (3x sidebar) */
}
```
### Percentage
Relative to parent:
```css
#widget {
width: 50%; /* Half of parent width */
height: 100%; /* Full parent height */
}
```
### Auto Sizing
Size to content:
```css
#widget {
width: auto; /* Width matches content */
height: auto; /* Height matches content */
}
```
### Min/Max Constraints
Bounded sizing:
```css
#widget {
width: 1fr;
min-width: 30;
max-width: 80;
}
```
## Spacing and Alignment
### Padding
Space inside widget:
```css
#widget {
padding: 1; /* All sides */
padding: 1 2; /* Vertical Horizontal */
padding: 1 2 1 2; /* Top Right Bottom Left */
padding-top: 1; /* Individual sides */
}
```
### Margin
Space outside widget:
```css
#widget {
margin: 1;
margin: 0 2; /* No vertical, 2 horizontal */
margin-left: 1;
}
```
### Alignment
Position within container:
```css
Container {
align: center middle; /* Horizontal Vertical */
align: left top;
align: right bottom;
}
/* Content alignment (for containers) */
Container {
content-align: center middle;
}
```
## Responsive Layouts
### Container Queries
Adjust based on container size:
```python
class ResponsiveApp(App):
CSS = """
Screen {
layout: horizontal;
}
/* Default mobile layout */
#content {
layout: vertical;
}
/* Desktop layout when width > 80 */
Screen:width-gt-80 #content {
layout: horizontal;
}
"""
```
### Conditional Layouts
Switch layouts based on screen size:
```python
def compose(self) -> ComposeResult:
if self.size.width > 100:
# Wide layout
with Horizontal():
yield self.make_sidebar()
yield self.make_content()
else:
# Narrow layout
with Vertical():
yield self.make_content()
```
## Advanced Patterns
### Modal Overlay
Centered modal dialog:
```python
from textual.screen import ModalScreen
from textual.containers import Container
class Modal(ModalScreen[bool]):
CSS = """
Modal {
align: center middle;
}
#dialog {
width: 50;
height: 15;
border: thick $accent;
background: $surface;
padding: 1;
}
"""
def compose(self) -> ComposeResult:
with Container(id="dialog"):
yield Label("Are you sure?")
with Horizontal():
yield Button("Yes", variant="primary")
yield Button("No", variant="error")
```
### Sidebar Toggle
Collapsible sidebar:
```python
class SidebarApp(App):
show_sidebar = reactive(True)
CSS = """
#sidebar {
width: 30;
transition: width 200ms;
}
#sidebar.hidden {
width: 0;
display: none;
}
"""
def watch_show_sidebar(self, show: bool) -> None:
sidebar = self.query_one("#sidebar")
sidebar.set_class(not show, "hidden")
```
### Masonry Layout
Staggered grid:
```python
class MasonryLayout(App):
CSS = """
Screen {
layout: grid;
grid-size: 3;
grid-gutter: 1;
}
.card {
height: auto;
border: solid $primary;
padding: 1;
}
.card.tall {
row-span: 2;
}
"""
def compose(self) -> ComposeResult:
yield Static("Short card", classes="card")
yield Static("Tall card\n\n\n", classes="card tall")
yield Static("Short", classes="card")
```
### Split Resizable
Adjustable split panels:
```python
class ResizableSplit(App):
left_width = reactive(30)
CSS = """
#left {
width: var(--left-width);
}
#right {
width: 1fr;
}
#divider {
width: 1;
background: $accent;
}
"""
def watch_left_width(self, width: int) -> None:
self.set_var("left-width", width)
```
## Layout Debugging
Use borders to visualize layout:
```css
* {
border: solid red; /* Temporary debugging */
}
Container {
border: solid blue;
}
Widget {
border: solid green;
}
```
Use Textual devtools:
```bash
textual run --dev app.py
```
Add debug info to widgets:
```python
def compose(self) -> ComposeResult:
yield Label(f"Size: {self.size}")
yield Label(f"Region: {self.region}")
```

View File

@@ -0,0 +1,284 @@
# Textual Official Guide Index
High-level index of every guide from the official Textual documentation. Use `web_fetch` to retrieve full content on-demand when needed.
## Getting Started
### Installation
**URL:** https://textual.textualize.io/getting_started/
**Topics:** Installing Textual, requirements, first app
**When to fetch:** User asks about installation, setup, or getting started
### Tutorial
**URL:** https://textual.textualize.io/tutorial/
**Topics:** Building a stopwatch app, complete walkthrough, reactive attributes, widgets
**When to fetch:** User wants step-by-step tutorial or building their first app
## Core Concepts
### App Basics
**URL:** https://textual.textualize.io/guide/app/
**Topics:** Creating apps, running apps, compose method, mounting widgets, app lifecycle, application mode, inline mode, suspending apps
**When to fetch:** Questions about app structure, lifecycle, or basic app operations
### Widgets
**URL:** https://textual.textualize.io/guide/widgets/
**Topics:** Creating custom widgets, widget communication, compound widgets, render method, line API, renderables, uni-directional data flow, widget design patterns
**When to fetch:** Creating custom widgets, widget architecture, or advanced widget patterns
### Layout
**URL:** https://textual.textualize.io/guide/layout/
**Topics:** Vertical layout, horizontal layout, grid layout, dock layout, layers, FR units, container layouts, grid-size, grid-columns, grid-rows, row-span, column-span
**When to fetch:** Layout questions, arranging widgets, grid systems, positioning
### CSS & Styling
**URL:** https://textual.textualize.io/guide/CSS/
**Topics:** Textual CSS basics, selectors (type, ID, class, universal), pseudo-classes, CSS_PATH, live editing, combinator selectors
**When to fetch:** CSS syntax, selectors, styling fundamentals
### Styles
**URL:** https://textual.textualize.io/guide/styles/
**Topics:** Style properties, colors (hex, RGB, HSL), units (%, fr, vw, vh, w, h), box model, box-sizing, dimensions, spacing
**When to fetch:** Specific style properties, units, colors, dimensions
### Design System / Themes
**URL:** https://textual.textualize.io/guide/design/
**Topics:** Theme system, built-in themes, creating custom themes, color variables, semantic colors, $text variables, theme switching, design tokens
**When to fetch:** Theming, color systems, design tokens, theme customization
## Interaction & Events
### Input
**URL:** https://textual.textualize.io/guide/input/
**Topics:** Keyboard input, mouse input, focus, key events, mouse events, input handling
**When to fetch:** Handling keyboard/mouse input, focus management, input events
### Events
**URL:** https://textual.textualize.io/guide/events/
**Topics:** Event system, message handlers, event bubbling, preventing events, custom events, event lifecycle
**When to fetch:** Event handling, custom events, event propagation
### Actions
**URL:** https://textual.textualize.io/guide/actions/
**Topics:** Action system, key bindings, BINDINGS, action_* methods, built-in actions, custom actions
**When to fetch:** Keyboard shortcuts, actions, key bindings
### Reactivity
**URL:** https://textual.textualize.io/guide/reactivity/
**Topics:** Reactive attributes, watch methods, compute methods, reactive decorators, smart refresh, recompose, data binding
**When to fetch:** Reactive programming, automatic updates, computed values, watchers
## Advanced Features
### Screens
**URL:** https://textual.textualize.io/guide/screens/
**Topics:** Screen stack, push_screen, pop_screen, ModalScreen, screen navigation, installed screens, SCREENS, screen opacity
**When to fetch:** Multi-screen apps, navigation, modal dialogs, screen management
### Query
**URL:** https://textual.textualize.io/guide/queries/
**Topics:** Querying widgets, query_one, query, set methods, DOM traversal, selectors in queries
**When to fetch:** Finding widgets, DOM navigation, bulk operations
### Workers
**URL:** https://textual.textualize.io/guide/workers/
**Topics:** Background tasks, @work decorator, run_worker, thread workers, async workers, worker lifecycle, cancellation
**When to fetch:** Background processing, async operations, threading, long-running tasks
### Animation
**URL:** https://textual.textualize.io/guide/animation/
**Topics:** Animating styles, animate method, easing functions, duration, transitions, animation callbacks
**When to fetch:** Animations, transitions, easing, style animations
### Command Palette
**URL:** https://textual.textualize.io/guide/command_palette/
**Topics:** Built-in command palette, Provider class, fuzzy matching, custom commands, command discovery
**When to fetch:** Command palette, custom commands, keyboard-driven interfaces
## Content & Display
### Content / Markup
**URL:** https://textual.textualize.io/guide/content/
**Topics:** Content markup, Rich renderables, styling text, markup tags, links, clickable actions, content objects
**When to fetch:** Text formatting, markup syntax, Rich renderables, styled text
### Rich Content
**URL:** https://textual.textualize.io/guide/rich/
**Topics:** Using Rich library, Rich renderables, tables, syntax highlighting, panels, progress bars
**When to fetch:** Rich library integration, advanced text formatting, Rich features
## Development & Debugging
### Devtools
**URL:** https://textual.textualize.io/guide/devtools/
**Topics:** textual command, console, run command, live CSS editing, --dev mode, debugging, logging
**When to fetch:** Development workflow, debugging, console logging, dev tools
### Testing
**URL:** https://textual.textualize.io/guide/testing/
**Topics:** Testing apps, Pilot API, run_test, simulating input, snapshots, unit testing
**When to fetch:** Testing, unit tests, test automation, Pilot
### Performance
**URL:** https://textual.textualize.io/guide/performance/
**Topics:** Performance optimization, profiling, rendering performance, widget efficiency, best practices
**When to fetch:** Performance issues, optimization, profiling
## Reference Documentation
### Widgets Reference
**URL:** https://textual.textualize.io/widgets/
**Topics:** Complete widget reference, all built-in widgets with examples
**When to fetch:** Looking for specific widget documentation, widget API details
### Styles Reference
**URL:** https://textual.textualize.io/styles/
**Topics:** Complete CSS properties reference, all style properties
**When to fetch:** Specific style property details, CSS reference
### API Reference
**URL:** https://textual.textualize.io/api/
**Topics:** Complete Python API reference
**When to fetch:** API details, method signatures, class documentation
## Specialized Topics
### Scrolling
**URL:** https://textual.textualize.io/guide/scrolling/
**Topics:** Scroll views, ScrollableContainer, scrolling behavior, scroll_visible, programmatic scrolling
**When to fetch:** Scrolling issues, scroll containers, programmatic scrolling
### Tooltips
**URL:** https://textual.textualize.io/guide/tooltips/
**Topics:** Adding tooltips, tooltip property, tooltip customization
**When to fetch:** Tooltips, hover help text
### Notifications
**URL:** https://textual.textualize.io/guide/notifications/
**Topics:** Toast notifications, notify method, notification severity, notification styling
**When to fetch:** Notifications, alerts, toasts
### Input Validation
**URL:** https://textual.textualize.io/guide/input_validation/
**Topics:** Validating input, Validator class, built-in validators, custom validators
**When to fetch:** Form validation, input validation, validators
### Timers
**URL:** https://textual.textualize.io/guide/timers/
**Topics:** Scheduling tasks, set_timer, set_interval, timer callbacks, timer management
**When to fetch:** Scheduled tasks, periodic updates, timers
### Paths
**URL:** https://textual.textualize.io/guide/paths/
**Topics:** File paths, resource paths, CSS_PATH, path resolution
**When to fetch:** File loading, resource paths, path management
## Additional Topics
### FAQ
**URL:** https://textual.textualize.io/FAQ/
**Topics:** Common questions, troubleshooting, best practices
**When to fetch:** Common issues, general questions, troubleshooting
### Why Textual?
**URL:** https://textual.textualize.io/guide/why/
**Topics:** Benefits of Textual, use cases, comparison with alternatives
**When to fetch:** Understanding Textual benefits, when to use Textual
## Usage Guidelines
### When to Fetch Guides
1. **Don't fetch unless needed**: The skill already covers fundamentals. Only fetch when:
- User asks about a specific topic not covered in skill
- Need detailed API information
- Complex examples required
- Latest updates needed (docs may be newer than skill)
2. **Fetch specific sections**: Use targeted URLs for relevant topics
3. **Combine with skill knowledge**: Use fetched content to supplement, not replace, skill knowledge
### Example Fetch Patterns
```python
# User asks about command palette
web_fetch("https://textual.textualize.io/guide/command_palette/")
# User needs animation details
web_fetch("https://textual.textualize.io/guide/animation/")
# User wants testing info
web_fetch("https://textual.textualize.io/guide/testing/")
# Need widget reference
web_fetch("https://textual.textualize.io/widgets/data_table/")
```
## Quick Reference by Topic
### Need information about...
**App Structure** → App Basics guide
**Layout & Positioning** → Layout guide
**Styling & CSS** → CSS guide, Styles guide, Design guide
**User Input** → Input guide, Events guide, Actions guide
**Custom Widgets** → Widgets guide
**Navigation** → Screens guide
**Async Tasks** → Workers guide
**Animations** → Animation guide
**Commands** → Command Palette guide
**Testing** → Testing guide
**Development** → Devtools guide
**Rich Integration** → Rich Content guide
**Form Validation** → Input Validation guide
**Auto-Updates** → Reactivity guide
## Widget-Specific Documentation
All built-in widgets have dedicated documentation at:
`https://textual.textualize.io/widgets/{widget_name}/`
Common widget docs:
- **Button**: https://textual.textualize.io/widgets/button/
- **Input**: https://textual.textualize.io/widgets/input/
- **DataTable**: https://textual.textualize.io/widgets/data_table/
- **Tree**: https://textual.textualize.io/widgets/tree/
- **Select**: https://textual.textualize.io/widgets/select/
- **TextArea**: https://textual.textualize.io/widgets/text_area/
- **ListView**: https://textual.textualize.io/widgets/list_view/
- **ProgressBar**: https://textual.textualize.io/widgets/progress_bar/
- **Markdown**: https://textual.textualize.io/widgets/markdown/
- **MarkdownViewer**: https://textual.textualize.io/widgets/markdown_viewer/
- **DirectoryTree**: https://textual.textualize.io/widgets/directory_tree/
- **Header**: https://textual.textualize.io/widgets/header/
- **Footer**: https://textual.textualize.io/widgets/footer/
- **Label**: https://textual.textualize.io/widgets/label/
- **Static**: https://textual.textualize.io/widgets/static/
- **Log**: https://textual.textualize.io/widgets/log/
- **RichLog**: https://textual.textualize.io/widgets/rich_log/
- **Sparkline**: https://textual.textualize.io/widgets/sparkline/
- **Switch**: https://textual.textualize.io/widgets/switch/
- **Checkbox**: https://textual.textualize.io/widgets/checkbox/
- **RadioButton**: https://textual.textualize.io/widgets/radio_button/
- **RadioSet**: https://textual.textualize.io/widgets/radio_set/
- **TabbedContent**: https://textual.textualize.io/widgets/tabbed_content/
- **ContentSwitcher**: https://textual.textualize.io/widgets/content_switcher/
- **LoadingIndicator**: https://textual.textualize.io/widgets/loading_indicator/
## Container Widget Documentation
- **Container**: https://textual.textualize.io/widgets/container/
- **Horizontal**: https://textual.textualize.io/widgets/horizontal/
- **Vertical**: https://textual.textualize.io/widgets/vertical/
- **Grid**: https://textual.textualize.io/widgets/grid/
- **ScrollableContainer**: https://textual.textualize.io/widgets/scrollable_container/
- **VerticalScroll**: https://textual.textualize.io/widgets/vertical_scroll/
- **HorizontalScroll**: https://textual.textualize.io/widgets/horizontal_scroll/
## Notes
- All URLs follow pattern: `https://textual.textualize.io/{section}/{topic}/`
- Official docs are actively maintained and may have updates not in this skill
- Use web_fetch with specific URLs when detailed or latest information needed
- Combine official docs with skill knowledge for best results

700
skill/references/styling.md Normal file
View File

@@ -0,0 +1,700 @@
# Textual CSS Styling Guide
Complete guide to styling Textual applications with TCSS (Textual CSS).
## CSS Basics
### Inline Styles
Define styles directly in widget class:
```python
class MyWidget(Widget):
DEFAULT_CSS = """
MyWidget {
background: $primary;
color: $text;
border: solid $accent;
}
"""
```
### External Stylesheets
Load from file:
```python
class MyApp(App):
CSS_PATH = "app.tcss" # Load from app.tcss file
```
### Multiple Stylesheets
Load multiple files:
```python
class MyApp(App):
CSS_PATH = ["base.tcss", "theme.tcss", "overrides.tcss"]
```
## Selectors
### Type Selectors
Target widget types:
```css
Button {
background: blue;
}
Label {
color: white;
}
```
### ID Selectors
Target specific widgets:
```css
#submit-button {
background: green;
}
#error-message {
color: red;
}
```
### Class Selectors
Target classes:
```css
.highlight {
background: yellow;
color: black;
}
.card {
border: solid white;
padding: 1;
}
```
### Pseudo-classes
Target widget states:
```css
Button:hover {
background: lighten($primary, 20%);
}
Button:focus {
border: thick $accent;
}
Input:focus {
border: solid $success;
}
/* Disabled state */
Button:disabled {
opacity: 50%;
}
```
### Descendant Selectors
Target nested widgets:
```css
/* Any Label inside a Container */
Container Label {
color: gray;
}
/* Direct children only */
Container > Label {
color: white;
}
/* Specific nesting */
#sidebar .menu-item {
padding: 1;
}
```
### Multiple Selectors
Apply same style to multiple targets:
```css
Button, Input, Select {
border: solid $accent;
}
.error, .warning {
font-weight: bold;
}
```
## Colors
### Semantic Colors
Use theme colors:
```css
Widget {
background: $background;
color: $text;
border: solid $primary;
}
/* Available semantic colors */
$primary /* Primary theme color */
$secondary /* Secondary theme color */
$accent /* Accent color */
$background /* Background color */
$surface /* Surface color */
$panel /* Panel color */
$text /* Primary text color */
$text-muted /* Muted text */
$text-disabled /* Disabled text */
$success /* Success state */
$warning /* Warning state */
$error /* Error state */
$boost /* Highlight color */
```
### Color Formats
Define custom colors:
```css
Widget {
background: #1e3a8a; /* Hex */
color: rgb(255, 255, 255); /* RGB */
border-color: rgba(255, 0, 0, 0.5); /* RGBA with alpha */
}
/* Named colors */
Widget {
background: transparent;
color: black;
border-color: white;
}
```
### Color Functions
Manipulate colors:
```css
Widget {
background: darken($primary, 20%);
color: lighten($text, 10%);
border-color: fade($accent, 50%);
}
```
## Typography
### Text Style
```css
Label {
text-style: bold; /* bold, italic, underline */
text-style: bold italic; /* Multiple styles */
text-style: reverse; /* Reverse colors */
text-style: strike; /* Strikethrough */
}
```
### Text Alignment
```css
Label {
text-align: left; /* left, center, right, justify */
}
```
### Text Opacity
```css
Label {
text-opacity: 70%; /* Semi-transparent text */
}
```
## Borders
### Border Styles
```css
Widget {
border: solid $accent; /* Solid border */
border: dashed blue; /* Dashed */
border: heavy green; /* Heavy */
border: double white; /* Double */
border: thick $primary; /* Thick */
border: none; /* No border */
}
```
### Border Sides
```css
Widget {
border-top: solid red;
border-right: dashed blue;
border-bottom: thick green;
border-left: double white;
}
```
### Border Title
```css
Widget {
border: solid $accent;
border-title-align: center; /* left, center, right */
}
```
## Dimensions
### Width
```css
Widget {
width: 40; /* Fixed columns */
width: 50%; /* Percentage of parent */
width: 1fr; /* Fractional unit */
width: auto; /* Size to content */
}
```
### Height
```css
Widget {
height: 20; /* Fixed rows */
height: 100%; /* Full parent height */
height: auto; /* Size to content */
}
```
### Min/Max Constraints
```css
Widget {
min-width: 20;
max-width: 80;
min-height: 10;
max-height: 50;
}
```
## Spacing
### Padding
Space inside widget:
```css
Widget {
padding: 1; /* All sides */
padding: 1 2; /* Vertical Horizontal */
padding: 1 2 3 4; /* Top Right Bottom Left */
}
/* Individual sides */
Widget {
padding-top: 1;
padding-right: 2;
padding-bottom: 1;
padding-left: 2;
}
```
### Margin
Space outside widget:
```css
Widget {
margin: 1;
margin: 0 2;
margin: 1 2 1 2;
}
/* Individual sides */
Widget {
margin-top: 1;
margin-right: 2;
}
```
## Layout Properties
### Display
Control visibility:
```css
Widget {
display: block; /* Visible */
display: none; /* Hidden */
}
```
### Visibility
Alternative to display:
```css
Widget {
visibility: visible;
visibility: hidden; /* Hidden but takes space */
}
```
### Opacity
Transparency:
```css
Widget {
opacity: 100%; /* Fully opaque */
opacity: 50%; /* Semi-transparent */
opacity: 0%; /* Fully transparent */
}
```
### Layout Type
```css
Container {
layout: vertical; /* Stack vertically */
layout: horizontal; /* Stack horizontally */
layout: grid; /* Grid layout */
}
```
### Grid Properties
```css
Container {
layout: grid;
grid-size: 3 2; /* 3 columns, 2 rows */
grid-gutter: 1 2; /* Vertical Horizontal gaps */
grid-rows: 10 auto 1fr; /* Row sizes */
grid-columns: 1fr 2fr; /* Column sizes */
}
/* Grid item spanning */
Widget {
column-span: 2; /* Span 2 columns */
row-span: 3; /* Span 3 rows */
}
```
### Alignment
```css
Container {
align: center middle; /* Horizontal Vertical */
align-horizontal: left; /* left, center, right */
align-vertical: top; /* top, middle, bottom */
}
/* Content alignment */
Container {
content-align: center middle;
content-align-horizontal: right;
content-align-vertical: bottom;
}
```
### Scrollbars
```css
Widget {
overflow: auto; /* Show scrollbars when needed */
overflow: scroll; /* Always show scrollbars */
overflow: hidden; /* No scrollbars */
}
/* Individual axes */
Widget {
overflow-x: auto;
overflow-y: scroll;
}
/* Scrollbar styling */
Widget {
scrollbar-background: $panel;
scrollbar-color: $primary;
scrollbar-color-hover: $accent;
scrollbar-color-active: $boost;
}
```
## Effects
### Transitions
Animate property changes:
```css
Button {
background: blue;
transition: background 300ms;
}
Button:hover {
background: lightblue; /* Animates over 300ms */
}
/* Multiple properties */
Widget {
transition: background 200ms, border 150ms;
}
```
### Offset
Position adjustment:
```css
Widget {
offset: 1 2; /* X Y offset */
offset-x: 1;
offset-y: 2;
}
```
### Layer
Z-index equivalent:
```css
Widget {
layer: above; /* Higher layer */
layer: below; /* Lower layer */
}
```
## Docking
Pin widgets to edges:
```css
#header {
dock: top;
height: 3;
}
#sidebar {
dock: left;
width: 30;
}
#footer {
dock: bottom;
height: 3;
}
```
## Theme Variables
Define reusable values:
```css
/* Define variables */
Screen {
--card-bg: #1e3a8a;
--card-border: white;
--card-padding: 1 2;
}
/* Use variables */
.card {
background: var(--card-bg);
border: solid var(--card-border);
padding: var(--card-padding);
}
```
## Complete Theme Example
```css
/* app.tcss */
/* Theme colors */
Screen {
background: #0f172a;
color: #e2e8f0;
}
/* Headers */
Header {
background: #1e293b;
color: #60a5fa;
dock: top;
height: 3;
}
Footer {
background: #1e293b;
color: #94a3b8;
dock: bottom;
height: 1;
}
/* Buttons */
Button {
background: #3b82f6;
color: white;
border: none;
margin: 0 1;
padding: 0 2;
min-width: 16;
transition: background 200ms;
}
Button:hover {
background: #60a5fa;
}
Button:focus {
border: solid #93c5fd;
}
Button.-primary {
background: #10b981;
}
Button.-primary:hover {
background: #34d399;
}
Button.-error {
background: #ef4444;
}
Button.-error:hover {
background: #f87171;
}
/* Inputs */
Input {
border: solid #475569;
background: #1e293b;
color: #e2e8f0;
padding: 0 1;
}
Input:focus {
border: solid #3b82f6;
}
/* Containers */
.card {
background: #1e293b;
border: solid #334155;
padding: 1 2;
margin: 1;
}
.card > .title {
text-style: bold;
color: #60a5fa;
margin-bottom: 1;
}
/* Data tables */
DataTable {
background: #1e293b;
}
DataTable > .datatable--header {
background: #334155;
color: #60a5fa;
text-style: bold;
}
DataTable > .datatable--cursor {
background: #3b82f6;
}
/* Scrollbars */
*::-webkit-scrollbar {
scrollbar-background: #1e293b;
scrollbar-color: #475569;
}
*::-webkit-scrollbar:hover {
scrollbar-color: #64748b;
}
```
## Dark/Light Themes
Support theme switching:
```python
class MyApp(App):
ENABLE_DARK_MODE = True
CSS = """
/* Dark theme (default) */
Screen {
background: #0f172a;
color: #e2e8f0;
}
/* Light theme */
Screen.light {
background: #f8fafc;
color: #1e293b;
}
"""
def action_toggle_theme(self) -> None:
self.dark = not self.dark
```
## Responsive Styles
Conditional styles based on size:
```css
/* Default (small screens) */
#sidebar {
width: 100%;
}
/* Medium screens */
Screen:width-gt-80 #sidebar {
width: 30;
}
/* Large screens */
Screen:width-gt-120 #sidebar {
width: 40;
}
```
## Best Practices
1. **Use semantic colors** - Prefer `$primary` over hardcoded values
2. **Organize CSS** - Group related styles together
3. **Use classes** - Reusable styles via classes, not IDs
4. **Minimize specificity** - Avoid overly specific selectors
5. **Use transitions** - Smooth state changes
6. **Test both themes** - Ensure dark/light compatibility
7. **Keep CSS DRY** - Use variables for repeated values
8. **Document custom variables** - Comment non-obvious choices
## Debugging Styles
View computed styles:
```python
def on_mount(self) -> None:
widget = self.query_one("#my-widget")
self.log(widget.styles) # Log all computed styles
```
Use Textual devtools:
```bash
textual run --dev app.py
# Press F1 to view CSS inspector
```
Temporary debugging borders:
```css
* {
border: solid red; /* See all widget boundaries */
}
```

533
skill/references/widgets.md Normal file
View File

@@ -0,0 +1,533 @@
# Textual Widget Gallery
Comprehensive examples of all built-in Textual widgets.
## Basic Widgets
### Label
Display static or dynamic text:
```python
from textual.widgets import Label
# Simple label
yield Label("Hello World")
# With styling
yield Label("Important!", classes="highlight")
# With markup
yield Label("[bold]Bold[/] and [italic]italic[/]")
# Dynamic label with reactive
class DynamicLabel(Widget):
message = reactive("Initial")
def compose(self) -> ComposeResult:
yield Label(self.message)
```
### Static
Display Rich renderables:
```python
from textual.widgets import Static
from rich.table import Table
table = Table()
table.add_column("Name")
table.add_column("Value")
table.add_row("Alpha", "100")
yield Static(table)
```
### Button
Interactive buttons with variants:
```python
from textual.widgets import Button
# Standard button
yield Button("Click me", id="action")
# Button variants
yield Button("Primary", variant="primary")
yield Button("Success", variant="success")
yield Button("Warning", variant="warning")
yield Button("Error", variant="error")
# Disabled button
yield Button("Disabled", disabled=True)
# Handle click
def on_button_pressed(self, event: Button.Pressed) -> None:
if event.button.id == "action":
self.action_perform()
```
## Input Widgets
### Input
Single-line text input:
```python
from textual.widgets import Input
# Basic input
yield Input(placeholder="Enter text...", id="name")
# Password input
yield Input(placeholder="Password", password=True, id="pass")
# Validated input
yield Input(
placeholder="Email",
validators=[Email()], # Built-in validators
id="email"
)
# Handle submission
def on_input_submitted(self, event: Input.Submitted) -> None:
value = event.value
self.log(f"Submitted: {value}")
# Handle changes
def on_input_changed(self, event: Input.Changed) -> None:
self.validate_live(event.value)
```
### TextArea
Multi-line text editor:
```python
from textual.widgets import TextArea
# Basic text area
yield TextArea(id="editor")
# With initial content
yield TextArea(
text="Initial content\nLine 2",
language="python", # Syntax highlighting
theme="monokai",
id="code"
)
# Handle changes
def on_text_area_changed(self, event: TextArea.Changed) -> None:
content = event.text_area.text
```
### Select
Dropdown selection:
```python
from textual.widgets import Select
# Basic select
options = [
("Option A", "a"),
("Option B", "b"),
("Option C", "c"),
]
yield Select(options=options, prompt="Choose...", id="choice")
# Handle selection
def on_select_changed(self, event: Select.Changed) -> None:
value = event.value # "a", "b", or "c"
self.log(f"Selected: {value}")
```
### Checkbox
Boolean input:
```python
from textual.widgets import Checkbox
yield Checkbox("Enable feature", id="feature")
# Handle changes
def on_checkbox_changed(self, event: Checkbox.Changed) -> None:
is_checked = event.value
self.toggle_feature(is_checked)
```
### RadioButton and RadioSet
Mutually exclusive options:
```python
from textual.widgets import RadioButton, RadioSet
with RadioSet(id="size"):
yield RadioButton("Small")
yield RadioButton("Medium", value=True) # Default
yield RadioButton("Large")
# Handle selection
def on_radio_set_changed(self, event: RadioSet.Changed) -> None:
selected = event.pressed.label
self.log(f"Size: {selected}")
```
### Switch
Toggle switch:
```python
from textual.widgets import Switch
yield Switch(value=True, id="notifications")
# Handle toggle
def on_switch_changed(self, event: Switch.Changed) -> None:
is_on = event.value
self.toggle_notifications(is_on)
```
## Data Display Widgets
### DataTable
Tabular data with selection and sorting:
```python
from textual.widgets import DataTable
table = DataTable(id="users")
# Add columns
table.add_columns("Name", "Age", "City")
# Add rows (returns row key)
row_key = table.add_row("Alice", 30, "NYC")
table.add_row("Bob", 25, "LA")
# Cursor control
table.cursor_type = "row" # or "cell", "column", "none"
# Handle selection
def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None:
row_key = event.row_key
row_data = event.row
self.log(f"Selected: {row_data}")
# Update cells
table.update_cell(row_key, "Age", 31)
# Remove rows
table.remove_row(row_key)
# Sort
table.sort("Age", reverse=True)
```
### Tree
Hierarchical data:
```python
from textual.widgets import Tree
tree = Tree("Root", id="file-tree")
# Add nodes
root = tree.root
folder = root.add("Folder", expand=True)
folder.add_leaf("file1.txt")
folder.add_leaf("file2.txt")
subfolder = folder.add("Subfolder")
subfolder.add_leaf("nested.txt")
# Handle selection
def on_tree_node_selected(self, event: Tree.NodeSelected) -> None:
node = event.node
self.log(f"Selected: {node.label}")
# Expand/collapse
def on_tree_node_expanded(self, event: Tree.NodeExpanded) -> None:
# Load children dynamically
node = event.node
self.load_children(node)
```
### ListView
List with selection:
```python
from textual.widgets import ListView, ListItem, Label
list_view = ListView(id="menu")
# Add items
list_view.append(ListItem(Label("Item 1")))
list_view.append(ListItem(Label("Item 2")))
list_view.append(ListItem(Label("Item 3")))
# Handle selection
def on_list_view_selected(self, event: ListView.Selected) -> None:
item = event.item
self.log(f"Selected: {item}")
```
### Log / RichLog
Scrollable log output:
```python
from textual.widgets import Log, RichLog
# Simple log
log = Log(id="output", auto_scroll=True)
log.write_line("Log entry")
log.write_lines(["Line 1", "Line 2"])
# Rich log with markup
rich_log = RichLog(id="rich-output", highlight=True)
rich_log.write("[bold green]Success![/]")
rich_log.write("[red]Error occurred[/]")
# Clear log
log.clear()
```
### ProgressBar
Progress indicator:
```python
from textual.widgets import ProgressBar
# Determinate progress
progress = ProgressBar(total=100, id="progress")
progress.advance(25) # 25%
progress.update(progress=50) # 50%
# Indeterminate progress
progress = ProgressBar(total=None) # Animated spinner
```
### Sparkline
Inline data visualization:
```python
from textual.widgets import Sparkline
data = [1, 2, 3, 5, 8, 13, 21]
yield Sparkline(data, id="chart")
# Update data
sparkline = self.query_one(Sparkline)
sparkline.data = new_data
```
## Navigation Widgets
### Header / Footer
Standard app chrome:
```python
from textual.widgets import Header, Footer
def compose(self) -> ComposeResult:
yield Header(show_clock=True)
# ... content ...
yield Footer()
```
### TabbedContent / TabPane
Tabbed interface:
```python
from textual.widgets import TabbedContent, TabPane
with TabbedContent(id="tabs"):
with TabPane("Tab 1", id="tab1"):
yield Label("Content 1")
with TabPane("Tab 2", id="tab2"):
yield Label("Content 2")
with TabPane("Tab 3", id="tab3"):
yield Label("Content 3")
# Handle tab changes
def on_tabbed_content_tab_activated(self, event: TabbedContent.TabActivated) -> None:
tab_id = event.pane.id
self.log(f"Switched to {tab_id}")
```
### ContentSwitcher
Programmatically switch content:
```python
from textual.widgets import ContentSwitcher
with ContentSwitcher(initial="view1", id="switcher"):
yield Label("View 1", id="view1")
yield Label("View 2", id="view2")
yield Label("View 3", id="view3")
# Switch views
def switch_view(self, view_id: str) -> None:
switcher = self.query_one(ContentSwitcher)
switcher.current = view_id
```
### OptionList
Selectable list of options:
```python
from textual.widgets import OptionList
from textual.widgets.option_list import Option
option_list = OptionList(
Option("Option 1", id="opt1"),
Option("Option 2", id="opt2"),
Option("Option 3", id="opt3"),
)
# Handle selection
def on_option_list_option_selected(self, event: OptionList.OptionSelected) -> None:
option_id = event.option.id
self.log(f"Selected: {option_id}")
```
## Loading Widgets
### LoadingIndicator
Spinning loader:
```python
from textual.widgets import LoadingIndicator
# Show while loading
with LoadingIndicator():
# Content loading...
pass
# Or standalone
yield LoadingIndicator(id="loader")
```
### Placeholder
Development placeholder:
```python
from textual.widgets import Placeholder
# Quick placeholder during development
yield Placeholder("Chart goes here")
yield Placeholder(label="[b]User Profile[/]", variant="text")
```
## Special Widgets
### Markdown
Render markdown:
```python
from textual.widgets import Markdown
markdown_content = """
# Title
This is **bold** and *italic*.
- Item 1
- Item 2
```python
code block
```
"""
yield Markdown(markdown_content, id="docs")
```
### DirectoryTree
File system browser:
```python
from textual.widgets import DirectoryTree
tree = DirectoryTree("/home/user", id="files")
# Handle file selection
def on_directory_tree_file_selected(self, event: DirectoryTree.FileSelected) -> None:
file_path = event.path
self.open_file(file_path)
```
## Custom Widget Example
Create composite widgets:
```python
from textual.widget import Widget
from textual.containers import Horizontal, Vertical
class UserCard(Widget):
"""Display user information."""
def __init__(self, name: str, email: str, role: str) -> None:
super().__init__()
self.name = name
self.email = email
self.role = role
def compose(self) -> ComposeResult:
with Vertical(classes="card"):
yield Label(self.name, classes="name")
yield Label(self.email, classes="email")
with Horizontal():
yield Label(f"Role: {self.role}", classes="role")
yield Button("Edit", variant="primary")
DEFAULT_CSS = """
UserCard {
border: solid $primary;
padding: 1;
margin: 1;
}
UserCard .name {
text-style: bold;
color: $text;
}
UserCard .email {
color: $text-muted;
}
"""
# Use the widget
yield UserCard("Alice Smith", "alice@example.com", "Admin")
```
## Widget Composition Patterns
### Form Layout
```python
def compose(self) -> ComposeResult:
with Container(id="form"):
yield Label("Registration Form")
yield Input(placeholder="Name", id="name")
yield Input(placeholder="Email", id="email")
yield Input(placeholder="Password", password=True, id="pass")
with Horizontal():
yield Button("Submit", variant="primary")
yield Button("Cancel")
```
### Dashboard Layout
```python
def compose(self) -> ComposeResult:
yield Header()
with Container(id="dashboard"):
with Horizontal(classes="stats"):
yield Static("[b]Users:[/] 1,234", classes="stat")
yield Static("[b]Active:[/] 567", classes="stat")
yield Static("[b]Revenue:[/] $12K", classes="stat")
with Horizontal(classes="content"):
with Vertical(id="sidebar"):
yield Label("Menu")
yield Button("Dashboard")
yield Button("Users")
yield Button("Settings")
with Container(id="main"):
yield DataTable()
yield Footer()
```