Claude
Skills
Sign in
Back

textual-event-messages

Included with Lifetime
$97 forever

Handles keyboard, mouse, and custom events in Textual applications using messages and handlers. Use when implementing keyboard bindings, custom message passing, event bubbling, action dispatch, and inter-widget communication. Covers event handling patterns, message definitions, and routing.

General

What this skill does


# Textual Event and Message Handling

## Purpose
Implement robust event handling and inter-widget communication in Textual using messages, keyboard bindings, and action dispatch. Messages enable loose coupling between components.

## Quick Start

```python
from textual.message import Message
from textual.app import App, ComposeResult
from textual.widgets import Button, Static
from textual import on

class ItemWidget(Static):
    """Widget that emits custom messages."""

    class ItemSelected(Message):
        """Posted when item is selected."""
        def __init__(self, item_id: str) -> None:
            super().__init__()
            self.item_id = item_id

class MyApp(App):
    def compose(self) -> ComposeResult:
        yield ItemWidget()

    @on(ItemWidget.ItemSelected)
    def on_item_selected(self, message: ItemWidget.ItemSelected) -> None:
        """Handle item selection."""
        self.notify(f"Selected: {message.item_id}")
```

## Instructions

### Step 1: Define Custom Messages

Create message classes to communicate between widgets:

```python
from textual.message import Message
from dataclasses import dataclass

# Simple message without data
class DataRefreshed(Message):
    """Posted when data is refreshed."""
    pass

# Message with data
class ItemSelected(Message):
    """Posted when item is selected."""

    def __init__(self, item_id: str, index: int) -> None:
        """Initialize message.

        Args:
            item_id: Selected item ID.
            index: Index in list.
        """
        super().__init__()
        self.item_id = item_id
        self.index = index

# Message with complex data
@dataclass(frozen=True)
class SearchResult:
    """Result of search operation."""
    query: str
    items: list[str]
    total_count: int

class SearchCompleted(Message):
    """Posted when search completes."""

    def __init__(self, result: SearchResult) -> None:
        """Initialize with search result."""
        super().__init__()
        self.result = result
```

**Message Conventions:**
- Subclass `Message`
- Define immutable data in `__init__`
- Use descriptive PascalCase names
- Document the message purpose in docstring
- Use `frozen=True` for dataclasses to ensure immutability

### Step 2: Post and Handle Messages

Post messages from widgets and handle in parents:

```python
from textual import on
from textual.widgets import Static, ListView, ListItem
from textual.app import ComposeResult

class ItemWidget(Static):
    """Widget that posts custom messages."""

    class ItemClicked(Message):
        """Posted when item is clicked."""
        def __init__(self, item_id: str) -> None:
            super().__init__()
            self.item_id = item_id

    def __init__(self, item_id: str, label: str, **kwargs: object) -> None:
        super().__init__(**kwargs)
        self._item_id = item_id
        self._label = label

    def render(self) -> str:
        return f"[{self._item_id}] {self._label}"

    def on_click(self) -> None:
        """Post message when clicked."""
        self.post_message(self.ItemClicked(self._item_id))

class ItemListWidget(Static):
    """Parent widget that handles item messages."""

    def compose(self) -> ComposeResult:
        yield ListView(id="item-list")

    async def on_mount(self) -> None:
        """Create items."""
        list_view = self.query_one("#item-list", ListView)
        for i, item_id in enumerate(["a", "b", "c"]):
            await list_view.append(ListItem(
                ItemWidget(item_id, f"Item {item_id}")
            ))

    @on(ItemWidget.ItemClicked)
    async def on_item_clicked(self, message: ItemWidget.ItemClicked) -> None:
        """Handle item click - called automatically."""
        self.notify(f"Clicked: {message.item_id}")

# Alternative handler without @on decorator
class AltListWidget(Static):
    """Using on_* method naming convention."""

    def on_item_widget_item_clicked(self, message: ItemWidget.ItemClicked) -> None:
        """Auto-routed handler.

        Convention: on_{widget_class_snake_case}_{message_class_snake_case}
        """
        self.notify(f"Clicked: {message.item_id}")
```

**Message Routing:**
1. Widget posts message: `self.post_message(MyMessage())`
2. Message bubbles up to parent widgets
3. Parent handles with `@on(MessageType)` or `on_*` method
4. First handler to handle the message stops propagation (can call `event.stop()`)

### Step 3: Implement Keyboard Event Handlers

Handle keyboard input directly:

```python
from textual.events import Key, Paste
from textual.widgets import Static

class KeyboardWidget(Static):
    """Widget handling keyboard events."""

    def on_key(self, event: Key) -> None:
        """Called when any key is pressed.

        Args:
            event: Key event with key name.
        """
        key_name = event.key
        # Common keys: "up", "down", "left", "right", "enter", "escape"
        # Letters: "a", "b", "ctrl+a", "shift+a"

        if key_name == "enter":
            self.handle_enter()
        elif key_name == "escape":
            self.handle_escape()
        elif key_name == "ctrl+c":
            self.app.exit()

    def handle_enter(self) -> None:
        """Handle Enter key."""
        self.update("Enter pressed")

    def handle_escape(self) -> None:
        """Handle Escape key."""
        self.update("Escape pressed")

    def on_paste(self, event: Paste) -> None:
        """Called when text is pasted.

        Args:
            event: Paste event with text.
        """
        self.update(f"Pasted: {event.text}")
```

**Key Event Handling:**
- `on_key()` called for keyboard events
- Common keys: arrow keys, enter, escape, tab, delete
- Modifier keys: "ctrl+key", "shift+key", "alt+key"
- Special: "home", "end", "page_up", "page_down", "F1"-"F12"

### Step 4: Add Keyboard Bindings and Actions

Bindings provide discoverable keyboard shortcuts tied to actions:

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

class MyApp(App):
    """App with keyboard bindings."""

    BINDINGS: ClassVar[list[Binding]] = [
        # (key, action, description, show, priority)
        Binding("q", "quit", "Quit", show=True, priority=True),
        Binding("ctrl+c", "quit", "Quit", show=False, priority=True),
        Binding("r", "refresh", "Refresh"),
        Binding("n", "new_item", "New"),
        Binding("?", "show_help", "Help"),
    ]

    def compose(self) -> ComposeResult:
        yield Header()
        yield Static("Content")
        yield Footer()  # Shows bindings

    def action_refresh(self) -> None:
        """Action handler for 'refresh' binding."""
        self.notify("Refreshed")

    def action_new_item(self) -> None:
        """Action handler for 'new_item' binding."""
        self.notify("Creating new item")

    async def action_show_help(self) -> None:
        """Action handlers can be async."""
        # Show help dialog
        pass
```

**Binding Conventions:**
- Binding key to action method name
- Action method: `action_{action_name}`
- Action methods can be sync or async
- Bindings shown in Footer if `show=True`
- `priority=True` for high-priority bindings (quit)
- Bindings override key event handlers when matched

### Step 5: Handle Mouse Events

React to mouse clicks and movements:

```python
from textual.events import MouseDown, MouseUp, MouseMove
from textual.widgets import Static

class MouseWidget(Static):
    """Widget handling mouse events."""

    def on_mouse_down(self, event: MouseDown) -> None:
        """Called when mouse button pressed.

        Args:
            event: MouseDown event with position and button.
        """
        x, y = event.x, event.y
        button = event.button  # 1: left, 2: middle, 3: right

        if button == 1:  # Left click
            self.handle_click(x, y)

    def on_mouse_up(self, event: MouseUp) -> None:
        """Called when mouse button released."""
       

Related in General