Enhanced Textual TUI with proper API usage and documentation
- Fixed DataTable row selection and event handling - Added explicit column keys to prevent auto-generated keys - Implemented row-to-flow mapping for reliable selection tracking - Converted left metrics panel to horizontal top bar - Fixed all missing FlowStats/EnhancedAnalysisData attributes - Created comprehensive Textual API documentation in Documentation/textual/ - Added validation checklist to prevent future API mismatches - Preserved cursor position during data refreshes - Fixed RowKey type handling and event names The TUI now properly handles flow selection, displays metrics in a compact top bar, and correctly correlates selected rows with the details pane.
This commit is contained in:
61
Documentation/textual/README.md
Normal file
61
Documentation/textual/README.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# Textual API Reference
|
||||
|
||||
This directory contains a comprehensive reference for the Textual library API, focusing on the components used in StreamLens.
|
||||
|
||||
## Overview
|
||||
|
||||
Textual is a TUI (Text User Interface) framework for Python. This reference covers the key APIs we use.
|
||||
|
||||
## Quick Links
|
||||
|
||||
- [DataTable Widget Reference](./datatable.md) - Complete DataTable API
|
||||
- [Events Reference](./events.md) - Event handling and messages
|
||||
- [Common Widgets](./widgets.md) - Other widgets used in StreamLens
|
||||
- [Styling Guide](./styling.md) - CSS and styling reference
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### Messages and Events
|
||||
|
||||
Textual uses a message-passing system for communication between widgets:
|
||||
|
||||
1. **Events** - User interactions (keyboard, mouse)
|
||||
2. **Messages** - Widget-to-widget communication
|
||||
3. **Handlers** - Methods that respond to events/messages
|
||||
|
||||
### Event Handler Naming
|
||||
|
||||
Event handlers follow the pattern: `on_<widget_class>_<event_name>`
|
||||
|
||||
Example:
|
||||
```python
|
||||
def on_data_table_row_selected(self, event: DataTable.RowSelected):
|
||||
# Handle row selection
|
||||
```
|
||||
|
||||
### Common Pitfalls
|
||||
|
||||
1. **RowKey Type**: DataTable row keys are not strings by default
|
||||
2. **Event Names**: Check exact event class names (e.g., `RowHighlighted` not `CursorMoved`)
|
||||
3. **Method Availability**: Not all expected methods exist (e.g., no `set_row_style()`)
|
||||
|
||||
## API Validation
|
||||
|
||||
Always validate API usage before implementation:
|
||||
|
||||
```python
|
||||
# Check available events
|
||||
from textual.widgets import DataTable
|
||||
print([attr for attr in dir(DataTable) if 'Selected' in attr or 'Highlighted' in attr])
|
||||
|
||||
# Check specific method
|
||||
print(hasattr(DataTable, 'method_name'))
|
||||
```
|
||||
|
||||
## Directory Structure
|
||||
|
||||
- `datatable.md` - Complete DataTable API reference
|
||||
- `events.md` - Events and message handling
|
||||
- `widgets.md` - Common widget APIs
|
||||
- `styling.md` - CSS and theming
|
||||
- `examples/` - Working code examples
|
||||
242
Documentation/textual/datatable.md
Normal file
242
Documentation/textual/datatable.md
Normal file
@@ -0,0 +1,242 @@
|
||||
# DataTable Widget API Reference
|
||||
|
||||
## Overview
|
||||
|
||||
The DataTable widget displays tabular data with support for selection, scrolling, and styling.
|
||||
|
||||
## Import
|
||||
|
||||
```python
|
||||
from textual.widgets import DataTable
|
||||
```
|
||||
|
||||
## Class Hierarchy
|
||||
|
||||
```
|
||||
Widget → ScrollView → DataTable
|
||||
```
|
||||
|
||||
## Constructor
|
||||
|
||||
```python
|
||||
DataTable(
|
||||
*,
|
||||
show_header: bool = True,
|
||||
fixed_rows: int = 0,
|
||||
fixed_columns: int = 0,
|
||||
zebra_stripes: bool = False,
|
||||
header_height: int = 1,
|
||||
show_cursor: bool = True,
|
||||
cursor_foreground_priority: Literal["renderable", "css"] = "renderable",
|
||||
cursor_background_priority: Literal["renderable", "css"] = "renderable",
|
||||
cursor_type: CursorType = "cell",
|
||||
cell_padding: int = 1,
|
||||
name: str | None = None,
|
||||
id: str | None = None,
|
||||
classes: str | None = None,
|
||||
disabled: bool = False
|
||||
)
|
||||
```
|
||||
|
||||
## Key Properties
|
||||
|
||||
### Cursor Properties
|
||||
- `cursor_row: int` - Current cursor row (0-based)
|
||||
- `cursor_column: int` - Current cursor column (0-based)
|
||||
- `cursor_coordinate: Coordinate` - (row, column) tuple
|
||||
|
||||
### Table Properties
|
||||
- `rows: Mapping[RowKey, Row]` - Dictionary of rows
|
||||
- `columns: list[Column]` - List of columns
|
||||
- `row_count: int` - Number of rows
|
||||
- `column_count: int` - Number of columns
|
||||
|
||||
## Methods
|
||||
|
||||
### Adding Data
|
||||
|
||||
```python
|
||||
# Add columns
|
||||
add_column(label: TextType, *, width: int | None = None, key: str | None = None, default: CellType | None = None) -> ColumnKey
|
||||
|
||||
# Add multiple columns
|
||||
add_columns(*labels: TextType) -> list[ColumnKey]
|
||||
|
||||
# Add a single row
|
||||
add_row(*cells: CellType, height: int | None = 1, key: str | None = None, label: TextType | None = None) -> RowKey
|
||||
|
||||
# Add multiple rows
|
||||
add_rows(rows: Iterable[Iterable[CellType]]) -> list[RowKey]
|
||||
```
|
||||
|
||||
### Modifying Data
|
||||
|
||||
```python
|
||||
# Update a cell
|
||||
update_cell(row_key: RowKey, column_key: ColumnKey, value: CellType, *, update_width: bool = False) -> None
|
||||
|
||||
# Update cell at coordinate
|
||||
update_cell_at(coordinate: Coordinate, value: CellType, *, update_width: bool = False) -> None
|
||||
|
||||
# Remove row
|
||||
remove_row(row_key: RowKey) -> None
|
||||
|
||||
# Remove column
|
||||
remove_column(column_key: ColumnKey) -> None
|
||||
|
||||
# Clear all data
|
||||
clear(columns: bool = False) -> Self
|
||||
```
|
||||
|
||||
### Navigation
|
||||
|
||||
```python
|
||||
# Move cursor
|
||||
move_cursor(row: int | None = None, column: int | None = None, animate: bool = True, scroll: bool = True) -> None
|
||||
|
||||
# Scroll to coordinate
|
||||
scroll_to(x: float | None = None, y: float | None = None, *, animate: bool = True, speed: float | None = None, duration: float | None = None, ...) -> None
|
||||
```
|
||||
|
||||
### Sorting
|
||||
|
||||
```python
|
||||
# Sort by column(s)
|
||||
sort(*columns: ColumnKey | str, reverse: bool = False) -> Self
|
||||
|
||||
# Sort with custom key
|
||||
sort(key: Callable[[Any], Any], *, reverse: bool = False) -> Self
|
||||
```
|
||||
|
||||
## Events
|
||||
|
||||
### Selection Events
|
||||
|
||||
```python
|
||||
class RowSelected(Message):
|
||||
"""Posted when a row is selected (Enter key)."""
|
||||
row_key: RowKey
|
||||
row_index: int
|
||||
|
||||
class CellSelected(Message):
|
||||
"""Posted when a cell is selected."""
|
||||
coordinate: Coordinate
|
||||
cell_key: CellKey
|
||||
|
||||
class RowHighlighted(Message):
|
||||
"""Posted when cursor highlights a row."""
|
||||
row_key: RowKey
|
||||
row_index: int
|
||||
|
||||
class CellHighlighted(Message):
|
||||
"""Posted when cursor highlights a cell."""
|
||||
coordinate: Coordinate
|
||||
cell_key: CellKey
|
||||
|
||||
class ColumnHighlighted(Message):
|
||||
"""Posted when cursor highlights a column."""
|
||||
column_key: ColumnKey
|
||||
column_index: int
|
||||
```
|
||||
|
||||
### Header Events
|
||||
|
||||
```python
|
||||
class HeaderSelected(Message):
|
||||
"""Posted when a column header is clicked."""
|
||||
column_key: ColumnKey
|
||||
column_index: int
|
||||
label: Text
|
||||
|
||||
class RowLabelSelected(Message):
|
||||
"""Posted when a row label is clicked."""
|
||||
row_key: RowKey
|
||||
row_index: int
|
||||
label: Text
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Creating a DataTable
|
||||
|
||||
```python
|
||||
class MyWidget(Widget):
|
||||
def compose(self) -> ComposeResult:
|
||||
yield DataTable(id="my-table")
|
||||
|
||||
def on_mount(self) -> None:
|
||||
table = self.query_one("#my-table", DataTable)
|
||||
table.add_columns("Name", "Value", "Status")
|
||||
table.add_row("Item 1", "100", "Active", key="item_1")
|
||||
```
|
||||
|
||||
### Handling Selection
|
||||
|
||||
```python
|
||||
def on_data_table_row_highlighted(self, event: DataTable.RowHighlighted) -> None:
|
||||
# React to cursor movement
|
||||
self.selected_row = event.row_key
|
||||
|
||||
def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None:
|
||||
# React to Enter key press
|
||||
self.process_selection(event.row_key)
|
||||
```
|
||||
|
||||
### Preserving Cursor Position
|
||||
|
||||
```python
|
||||
def refresh_table(self):
|
||||
table = self.query_one(DataTable)
|
||||
|
||||
# Save position
|
||||
cursor_row = table.cursor_row
|
||||
selected_key = list(table.rows.keys())[cursor_row] if table.rows else None
|
||||
|
||||
# Update data
|
||||
table.clear()
|
||||
# ... add new data ...
|
||||
|
||||
# Restore position
|
||||
if selected_key and selected_key in table.rows:
|
||||
row_index = list(table.rows.keys()).index(selected_key)
|
||||
table.move_cursor(row=row_index, animate=False)
|
||||
```
|
||||
|
||||
## Styling
|
||||
|
||||
DataTable supports CSS styling but does NOT have:
|
||||
- `set_row_style()` method
|
||||
- `set_cell_style()` method
|
||||
|
||||
Use CSS classes and selectors instead:
|
||||
|
||||
```css
|
||||
DataTable > .datatable--cursor {
|
||||
background: $primary 30%;
|
||||
}
|
||||
|
||||
DataTable > .datatable--header {
|
||||
text-style: bold;
|
||||
}
|
||||
```
|
||||
|
||||
## Type Hints
|
||||
|
||||
```python
|
||||
from textual.widgets.data_table import RowKey, ColumnKey, CellKey
|
||||
from textual.coordinate import Coordinate
|
||||
|
||||
# RowKey and ColumnKey are not strings - convert with str() if needed
|
||||
row_key_str = str(row_key)
|
||||
```
|
||||
|
||||
## Common Errors and Solutions
|
||||
|
||||
1. **TypeError: 'RowKey' is not iterable**
|
||||
- Solution: Convert to string first: `str(row_key)`
|
||||
|
||||
2. **AttributeError: 'DataTable' has no attribute 'set_row_style'**
|
||||
- Solution: Use CSS styling instead
|
||||
|
||||
3. **Event not firing**
|
||||
- Check handler name: `on_data_table_row_highlighted` not `on_data_table_cursor_moved`
|
||||
200
Documentation/textual/events.md
Normal file
200
Documentation/textual/events.md
Normal file
@@ -0,0 +1,200 @@
|
||||
# Textual Events and Messages Reference
|
||||
|
||||
## Overview
|
||||
|
||||
Textual uses an event-driven architecture with two main types of communication:
|
||||
1. **Events** - User interactions (keyboard, mouse)
|
||||
2. **Messages** - Widget-to-widget communication
|
||||
|
||||
## Event Handler Patterns
|
||||
|
||||
### Basic Pattern
|
||||
|
||||
```python
|
||||
def on_<namespace>_<event_name>(self, event: EventType) -> None:
|
||||
"""Handle event"""
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
```python
|
||||
# Widget events
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
pass
|
||||
|
||||
# DataTable events
|
||||
def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None:
|
||||
pass
|
||||
|
||||
# Key events
|
||||
def on_key(self, event: events.Key) -> None:
|
||||
if event.key == "escape":
|
||||
self.exit()
|
||||
```
|
||||
|
||||
## Common Widget Events
|
||||
|
||||
### DataTable Events
|
||||
|
||||
| Event | Handler | Description |
|
||||
|-------|---------|-------------|
|
||||
| `RowHighlighted` | `on_data_table_row_highlighted` | Cursor moves to row |
|
||||
| `RowSelected` | `on_data_table_row_selected` | Row selected (Enter) |
|
||||
| `CellHighlighted` | `on_data_table_cell_highlighted` | Cursor moves to cell |
|
||||
| `CellSelected` | `on_data_table_cell_selected` | Cell selected |
|
||||
| `ColumnHighlighted` | `on_data_table_column_highlighted` | Column highlighted |
|
||||
| `HeaderSelected` | `on_data_table_header_selected` | Header clicked |
|
||||
|
||||
### Button Events
|
||||
|
||||
| Event | Handler | Description |
|
||||
|-------|---------|-------------|
|
||||
| `Pressed` | `on_button_pressed` | Button clicked/activated |
|
||||
|
||||
### Input Events
|
||||
|
||||
| Event | Handler | Description |
|
||||
|-------|---------|-------------|
|
||||
| `Changed` | `on_input_changed` | Text changed |
|
||||
| `Submitted` | `on_input_submitted` | Enter pressed |
|
||||
|
||||
## Custom Messages
|
||||
|
||||
### Creating a Custom Message
|
||||
|
||||
```python
|
||||
from textual.message import Message
|
||||
|
||||
class MyWidget(Widget):
|
||||
class DataUpdated(Message):
|
||||
"""Custom message when data updates"""
|
||||
def __init__(self, data: dict) -> None:
|
||||
self.data = data
|
||||
super().__init__()
|
||||
|
||||
def update_data(self, new_data: dict) -> None:
|
||||
# Post custom message
|
||||
self.post_message(self.DataUpdated(new_data))
|
||||
```
|
||||
|
||||
### Handling Custom Messages
|
||||
|
||||
```python
|
||||
class MyApp(App):
|
||||
def on_my_widget_data_updated(self, event: MyWidget.DataUpdated) -> None:
|
||||
"""Handle custom message"""
|
||||
self.process_data(event.data)
|
||||
```
|
||||
|
||||
## Event Properties
|
||||
|
||||
### Common Event Attributes
|
||||
|
||||
```python
|
||||
# All events have:
|
||||
event.stop() # Stop propagation
|
||||
event.prevent_default() # Prevent default behavior
|
||||
|
||||
# Keyboard events
|
||||
event.key # Key name ("a", "ctrl+c", "escape")
|
||||
event.character # Unicode character
|
||||
event.aliases # List of key aliases
|
||||
|
||||
# Mouse events
|
||||
event.x, event.y # Coordinates
|
||||
event.button # Mouse button number
|
||||
event.shift, event.ctrl, event.alt # Modifier keys
|
||||
|
||||
# DataTable events
|
||||
event.row_key # Row identifier
|
||||
event.row_index # Row number
|
||||
event.coordinate # (row, column) tuple
|
||||
```
|
||||
|
||||
## Event Flow
|
||||
|
||||
1. **Capture Phase**: Event travels down from App to target
|
||||
2. **Bubble Phase**: Event travels up from target to App
|
||||
|
||||
```python
|
||||
# Stop event propagation
|
||||
def on_key(self, event: events.Key) -> None:
|
||||
if event.key == "q":
|
||||
event.stop() # Don't let parent widgets see this
|
||||
```
|
||||
|
||||
## Message Posting
|
||||
|
||||
### Post to Self
|
||||
|
||||
```python
|
||||
self.post_message(MyMessage())
|
||||
```
|
||||
|
||||
### Post to Parent
|
||||
|
||||
```python
|
||||
self.post_message_no_wait(MyMessage()) # Don't wait for processing
|
||||
```
|
||||
|
||||
### Post to Specific Widget
|
||||
|
||||
```python
|
||||
target_widget.post_message(MyMessage())
|
||||
```
|
||||
|
||||
## Event Debugging
|
||||
|
||||
### Log All Events
|
||||
|
||||
```python
|
||||
def on_event(self, event: events.Event) -> None:
|
||||
"""Log all events for debugging"""
|
||||
self.log(f"Event: {event.__class__.__name__}")
|
||||
```
|
||||
|
||||
### Check Handler Names
|
||||
|
||||
```python
|
||||
# List all event handlers
|
||||
handlers = [m for m in dir(self) if m.startswith('on_')]
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Debouncing Events
|
||||
|
||||
```python
|
||||
from textual.timer import Timer
|
||||
|
||||
class MyWidget(Widget):
|
||||
def __init__(self):
|
||||
self._update_timer: Timer | None = None
|
||||
|
||||
def on_input_changed(self, event: Input.Changed) -> None:
|
||||
# Cancel previous timer
|
||||
if self._update_timer:
|
||||
self._update_timer.cancel()
|
||||
|
||||
# Set new timer
|
||||
self._update_timer = self.set_timer(0.3, self.perform_update)
|
||||
```
|
||||
|
||||
### Event Validation
|
||||
|
||||
```python
|
||||
def on_data_table_row_highlighted(self, event: DataTable.RowHighlighted) -> None:
|
||||
# Validate event data
|
||||
if not event.row_key:
|
||||
return
|
||||
|
||||
# Convert types if needed
|
||||
row_key_str = str(event.row_key)
|
||||
```
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
1. **Wrong handler name**: `on_data_table_cursor_moved` ❌ → `on_data_table_row_highlighted` ✅
|
||||
2. **Missing namespace**: `on_row_selected` ❌ → `on_data_table_row_selected` ✅
|
||||
3. **Wrong event class**: `DataTable.CursorMoved` ❌ → `DataTable.RowHighlighted` ✅
|
||||
4. **Not converting types**: Assuming `row_key` is string when it's `RowKey` type
|
||||
Reference in New Issue
Block a user