Skip to content

No 'text-area--gutter' key in COMPONENT_CLASSES #6208

@femto

Description

@femto

The bug

"No 'text-area--gutter' key in COMPONENT_CLASSES"

luckliy I can produce a small file that can reproduce this bug:

#!/usr/bin/env python3
"""
Minimal reproduction of Textual bug with reactive(recompose=True) and custom TextArea

Bug: When a reactive property with recompose=True changes during key event handling,
it causes a compositor error in Screen.get_style_at() -> Compositor.get_style_at()

Error traceback:
  File ".../textual/screen.py", line 1640, in _forward_event
    event.style = self.get_style_at(event.screen_x, event.screen_y)
  File ".../textual/screen.py", line 685, in get_style_at
    return self._compositor.get_style_at(x, y)
  File ".../textual/_compositor.py", line 879, in get_style_at
    lines = widget.render_lines(Region(0, y, region.width, 1))
"""

from textual.app import App, ComposeResult
from textual.containers import Container, Vertical
from textual.widgets import TextArea, Static
from textual.reactive import reactive
from textual import on
from textual.events import Key
from textual.message import Message


class CustomTextArea(TextArea):
    """Custom TextArea that posts key events to parent"""

    class KeyPressed(Message):
        """Message posted when a key is pressed"""

        def __init__(self, key: str) -> None:
            super().__init__()
            self.key = key

    def on_key(self, event: Key) -> bool:
        """Handle key events and post to parent"""
        # Post key event to parent for handling
        self.post_message(self.KeyPressed(event.key))

        # Handle Enter - prevent default and let parent handle
        if event.key == "enter":
            event.prevent_default()
            event.stop()
            return True  # Prevent TextArea from handling

        # Let TextArea handle all other keys normally
        return False


class BuggyContainer(Container):
    """Container with reactive property that causes recomposition"""

    # This reactive property with recompose=True causes the bug
    is_loading = reactive(False, recompose=True)

    def compose(self) -> ComposeResult:
        """Compose the container with conditional content based on reactive property"""
        yield Static("Type in the TextArea below and press Enter to trigger the bug")
        yield Static(f"Loading state: {self.is_loading}")

        # Conditional rendering based on reactive property
        if self.is_loading:
            yield Static("🔄 Loading... (this appears when is_loading=True)")

        yield CustomTextArea(
            text="Type here and press Enter",
            id="main_input"
        )

        yield Static("Status: Ready")

    @on(CustomTextArea.KeyPressed)
    def on_custom_textarea_key(self, event: CustomTextArea.KeyPressed):
        """Handle key events from CustomTextArea"""
        if event.key == "enter":
            # This line triggers the bug by changing a reactive property with recompose=True
            # during the key event handling, which causes recomposition while the compositor
            # is still processing the original event
            self.is_loading = not self.is_loading

            # The bug occurs because:
            # 1. User presses Enter
            # 2. CustomTextArea.on_key() posts KeyPressed message
            # 3. This handler changes is_loading (reactive with recompose=True)
            # 4. Textual starts recomposition immediately
            # 5. Meanwhile, the original key event is still being processed by Screen._forward_event
            # 6. Screen._forward_event tries to call get_style_at() on the recomposing widget
            # 7. Compositor.get_style_at() fails because widget is in inconsistent state during recomposition


class BugReproductionApp(App):
    """Minimal app to reproduce the reactive recompose bug"""

    def compose(self) -> ComposeResult:
        yield BuggyContainer()

    def on_mount(self):
        self.title = "Textual Reactive Recompose Bug Reproduction"
        # Focus the text area
        self.set_timer(0.1, self._focus_textarea)

    def _focus_textarea(self):
        try:
            textarea = self.query_one("#main_input", expect_type=CustomTextArea)
            textarea.focus()
        except:
            pass


if __name__ == "__main__":
    print("Textual Reactive Recompose Bug Reproduction")
    print("=" * 50)
    print("Steps to reproduce:")
    print("1. Run this script")
    print("2. Click in the TextArea or it should be focused automatically")
    print("3. Press Enter")
    print("4. The bug should occur with a compositor error")
    print()
    print("Expected error:")
    print("  File '.../textual/_compositor.py', line 879, in get_style_at")
    print("    lines = widget.render_lines(Region(0, y, region.width, 1))")
    print()

    app = BugReproductionApp()
    app.run()

when enter at CustomTextArea

 /Users/femtozheng/python-project/first/venv/lib/python3.11/site-packages/textual/widget.py:1134 in get_component_rich_style                                                                                        │
│                                                                                                                                                                                                                    │
│   1131 │   │   """                                                                              ╭───────────────── locals ──────────────────╮                                                                      │
│   1132 │   │                                                                                    │   names = ('text-area--gutter',)          │                                                                      │
│   1133 │   │   if names not in self._rich_style_cache:                                          │ partial = False                           │                                                                      │
│ ❱ 1134 │   │   │   component_styles = self.get_component_styles(*names)                         │    self = CustomTextArea(id='main_input') │                                                                      │
│   1135 │   │   │   style = component_styles.rich_style                                          ╰───────────────────────────────────────────╯                                                                      │
│   1136 │   │   │   text_opacity = component_styles.text_opacity                                                                                                                                                    │
│   1137 │   │   │   if text_opacity < 1 and style.bgcolor is not None:                                                                                                                                              │
│                                                                                                                                                                                                                    │
│ /Users/femtozheng/python-project/first/venv/lib/python3.11/site-packages/textual/dom.py:600 in get_component_styles                                                                                                │
│                                                                                                                                                                                                                    │
│    597 │   │                                                                                    ╭──────────────────────── locals ────────────────────────╮                                                         │
│    598 │   │   for name in names:                                                               │   name = 'text-area--gutter'                           │                                                         │
│    599 │   │   │   if name not in self._component_styles:                                       │  names = ('text-area--gutter',)                        │                                                         │
│ ❱  600 │   │   │   │   raise KeyError(f"No {name!r} key in COMPONENT_CLASSES")                  │   self = CustomTextArea(id='main_input')               │                                                         │
│    601 │   │   │   component_styles = self._component_styles[name]                              │ styles = RenderStyles(CustomTextArea(id='main_input')) │                                                         │
│    602 │   │   │   styles.node = component_styles.node                                          ╰────────────────────────────────────────────────────────╯                                                         │
│    603 │   │   │   styles.base.merge(component_styles.base)                                                                                                                                                        │
╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
KeyError: "No 'text-area--gutter' key in COMPONENT_CLASSES"

<!-- This is valid Markdown, please paste the following directly in to a GitHub issue -->
# Textual Diagnostics

## Versions

| Name    | Value  |
|---------|--------|
| Textual | 6.4.0  |
| Rich    | 14.2.0 |

## Python

| Name           | Value                                                  |
|----------------|--------------------------------------------------------|
| Version        | 3.11.4                                                 |
| Implementation | CPython                                                |
| Compiler       | Clang 13.1.6 (clang-1316.0.21.2.5)                     |
| Executable     | /Users/femtozheng/python-project/first/venv/bin/python |

## Operating System

| Name    | Value                                                                                             |
|---------|---------------------------------------------------------------------------------------------------|
| System  | Darwin                                                                                            |
| Release | 23.6.0                                                                                            |
| Version | Darwin Kernel Version 23.6.0: Mon Jul 29 21:13:00 PDT 2024; root:xnu-10063.141.2~1/RELEASE_X86_64 |

## Terminal

| Name                 | Value          |
|----------------------|----------------|
| Terminal Application | *Unknown*      |
| TERM                 | xterm-256color |
| COLORTERM            | *Not set*      |
| FORCE_COLOR          | *Not set*      |
| NO_COLOR             | *Not set*      |

## Rich Console options

| Name           | Value                |
|----------------|----------------------|
| size           | width=219, height=24 |
| legacy_windows | False                |
| min_width      | 1                    |
| max_width      | 219                  |
| is_terminal    | True                 |
| encoding       | utf-8                |
| max_height     | 24                   |
| justify        | None                 |
| overflow       | None                 |
| no_wrap        | False                |
| highlight      | None                 |
| markup         | None                 |
| height         | None                 

If you don't have the textual command on your path, you may have forgotten to install the textual-dev package.

Feel free to add screenshots and / or videos. These can be very helpful!

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions