# Textual Development Skill

Guidance for developing TUI applications with the Textual framework (v7.3.0+).

## When to Use

Invoke this skill when:
- Creating or modifying Textual apps, screens, or widgets
- Fixing type errors in Textual code
- Setting up Textual project structure
- Working with reactive patterns, messages, or bindings

## Type Annotation Patterns

### App Generic Parameter

```python
from textual.app import App

class MyApp(App[None]):
    """ReturnType parameter is what app.exit() returns."""
    pass

class MyApp(App[int]):
    """App that returns an exit code."""
    def action_quit(self) -> None:
        self.exit(0)
```

### Screen Generic Parameter

```python
from textual.screen import Screen

class DashboardScreen(Screen[None]):
    """Screen returns None when dismissed."""
    pass

class DevicePickerScreen(Screen[str]):
    """Screen returns a device name when dismissed."""
    def action_select(self) -> None:
        self.dismiss("selected-device")
```

### BINDINGS Class Variable

```python
from typing import ClassVar
from textual.binding import Binding, BindingType

class MyScreen(Screen[None]):
    # MUST use BindingType, not just Binding
    BINDINGS: ClassVar[list[BindingType]] = [
        Binding("q", "quit", "Quit"),
        Binding("p", "provision", "Provision", show=True),
        ("d", "device"),  # Tuple form also valid
        ("c", "config", "Config"),  # 3-tuple form
    ]
```

### SCREENS Class Variable

```python
from typing import Any, ClassVar, Callable
from textual.screen import Screen

class MyApp(App[None]):
    # Values must be Callable[[], Screen], not type[Screen]
    SCREENS: ClassVar[dict[str, Callable[[], Screen[Any]]]] = {
        "dashboard": DashboardScreen,  # Classes are callable
        "settings": lambda: SettingsScreen(config),  # Lambdas work
    }
```

**Note:** While `type[Screen]` works at runtime (classes are callable), mypy expects `Callable[[], Screen[Any]]`. Using the class directly satisfies both.

### Widget Generic Parameters

| Widget | Generic | Purpose |
|--------|---------|---------|
| `DataTable[T]` | Cell type | Type of data in cells |
| `Select[T]` | Selection type | Type of selected value (must be Hashable) |
| `SelectionList[T]` | Selection type | Type of selection values |
| `Tree[T]` | Node data type | Type of data attached to nodes |

```python
from textual.widgets import DataTable, Select, Tree

class DeviceTable(DataTable[str]):
    """Table with string cell values."""
    pass

class DeviceSelect(Select[str]):
    """Dropdown with string options."""
    pass

class ConfigTree(Tree[dict[str, Any]]):
    """Tree with dict data in nodes."""
    pass
```

### Compose Method

```python
from textual.app import ComposeResult

def compose(self) -> ComposeResult:
    """Return type is ComposeResult, not list[Widget]."""
    yield Header()
    with Container():
        yield Static("Content")
    yield Footer()
```

### Reactive Variables

```python
from textual.reactive import var

class MyWidget(Widget):
    # Use var[] for reactive attributes
    count: var[int] = var(0)
    name: var[str] = var("default")
    items: var[list[str]] = var(list)  # Factory function
```

## Common Type Errors and Fixes

| Error | Cause | Fix |
|-------|-------|-----|
| `Missing type parameters for generic type "App"` | Unparametrized App | Use `App[None]` or `App[ReturnType]` |
| `Missing type parameters for generic type "Screen"` | Unparametrized Screen | Use `Screen[None]` or `Screen[ResultType]` |
| `Missing type parameters for generic type "DataTable"` | Unparametrized widget | Use `DataTable[str]` or appropriate type |
| `Mutable class attributes should be annotated with ClassVar` | BINDINGS/SCREENS without ClassVar | Add `ClassVar[...]` wrapper |
| `Incompatible types in assignment` for BINDINGS | Using `list[Binding]` | Use `list[BindingType]` |

## Project Structure

```
src/myapp/
├── __init__.py
├── __main__.py          # Entry point
├── app.py               # App class
├── screens/
│   ├── __init__.py
│   ├── dashboard.py
│   └── settings.py
├── widgets/
│   ├── __init__.py
│   └── custom_widget.py
├── services/            # Business logic
├── models/              # Data models
├── themes/
│   └── my_theme.py
└── styles/
    └── app.tcss
```

## Development Commands

```bash
# Run with hot reload
textual run --dev src/myapp/app.py

# Run console for debugging
textual console

# Check types
mypy src/

# Lint
ruff check .
```

## Message Handling

```python
from textual.message import Message

class DeviceSelected(Message):
    """Custom message with typed payload."""
    
    def __init__(self, device_id: str) -> None:
        super().__init__()
        self.device_id = device_id

class MyScreen(Screen[None]):
    def on_device_selected(self, event: DeviceSelected) -> None:
        """Handler name is on_<message_class_snake_case>."""
        self.notify(f"Selected: {event.device_id}")
```

## Testing

```python
import pytest
from textual.pilot import Pilot

@pytest.mark.asyncio
async def test_app_starts():
    from myapp.app import MyApp
    
    app = MyApp()
    async with app.run_test() as pilot:
        assert app.screen.name == "dashboard"
        await pilot.press("q")
```
