4.0 KiB
4.0 KiB
Textual Basics
App Structure
Every Textual app follows this pattern:
from textual.app import App, ComposeResult
from textual.widgets import Widget
class MyApp(App):
"""Docstring describing the app."""
# Optional: Link to external CSS file
CSS_PATH = "app.tcss"
# Optional: Inline CSS
CSS = """
Screen {
align: center middle;
}
"""
def compose(self) -> ComposeResult:
"""Create child widgets."""
yield Widget()
def on_mount(self) -> None:
"""Called when app is mounted and ready."""
pass
if __name__ == "__main__":
app = MyApp()
app.run()
Compose Method
The compose() method yields widgets to add to the app. It's called once during initialization:
def compose(self) -> ComposeResult:
yield Header()
yield ContentWidget()
yield Footer()
Mounting
on_mount(): Called when the app/widget is fully mounted and readymount(): Dynamically add widgets after app starts (returns a coroutine)
async def on_key(self) -> None:
# Must await when modifying mounted widgets
await self.mount(NewWidget())
self.query_one(Button).label = "Modified!"
Reactive Attributes
Reactive attributes automatically update the UI when changed:
from textual.reactive import reactive
class Counter(Widget):
count = reactive(0) # Initial value
def watch_count(self, new_value: int) -> None:
"""Called automatically when count changes."""
self.query_one(Label).update(f"Count: {new_value}")
def increment(self) -> None:
self.count += 1 # Triggers watch_count
Reactive with Bindings
Set bindings=True to auto-refresh footer bindings when reactive changes:
class MyApp(App):
page = reactive(0, bindings=True)
def check_action(self, action: str, parameters) -> bool | None:
"""Return None to disable action."""
if action == "next" and self.page == MAX_PAGES:
return None # Dims the key in footer
return True
Querying Widgets
Find widgets in the DOM:
# Get one widget (raises if not found)
button = self.query_one(Button)
button = self.query_one("#my-id")
# Get multiple widgets
all_buttons = self.query(Button)
for button in all_buttons:
pass
# Get with CSS selector
widget = self.query_one("#container .special-class")
Messages and Events
Built-in Events
Handle with on_<event> methods:
def on_mount(self) -> None:
"""When mounted."""
pass
def on_key(self, event: events.Key) -> None:
"""Key pressed."""
if event.key == "escape":
self.exit()
Widget Messages
Handle messages from child widgets:
def on_button_pressed(self, event: Button.Pressed) -> None:
"""Button was clicked."""
self.notify(f"Button {event.button.id} clicked!")
def on_input_changed(self, event: Input.Changed) -> None:
"""Input text changed."""
self.value = event.value
Custom Messages
Define custom messages in your widgets:
from textual.message import Message
class MyWidget(Widget):
class ValueChanged(Message):
"""Posted when value changes."""
def __init__(self, value: int) -> None:
super().__init__()
self.value = value
def update_value(self, new_value: int) -> None:
self.value = new_value
self.post_message(self.ValueChanged(new_value))
# Handle in parent
def on_my_widget_value_changed(self, event: MyWidget.ValueChanged) -> None:
self.notify(f"New value: {event.value}")
Preventing Message Propagation
Stop messages from bubbling to parent:
def on_switch_changed(self, event: Switch.Changed) -> None:
event.stop() # Don't propagate to parent
# Handle here
Preventing Reactive Watchers
Temporarily prevent reactive watchers from firing:
with self.prevent(MyWidget.ValueChanged):
self.value = new_value # Won't trigger watch_value or post message