Loading...
Loading...
Expert guidance for building TUI (Text User Interface) applications with the Textual framework. Invoke when user asks about Textual development, TUI apps, widgets, screens, CSS styling, reactive programming, or testing Textual applications.
npx skill4agent add kyleking/vcr-tui textualAppapp.run()CSS_PATHCSScompose()on_*from textual.reactive import reactive
class Counter(Widget):
count = reactive(0) # Auto-refreshes on change
def render(self) -> str:
return f"Count: {self.count}"validate_<attr>()watch_<attr>()compute_<attr>()Button {
background: $primary;
margin: 1;
}
#submit-button {
background: $success;
}
.danger {
background: $error;
}from textual.app import App, ComposeResult
from textual.widgets import Header, Footer, Static
class MyApp(App):
CSS_PATH = "app.tcss"
def compose(self) -> ComposeResult:
yield Header()
yield Static("Hello, Textual!")
yield Footer()
def on_mount(self) -> None:
"""Called after app starts."""
pass
if __name__ == "__main__":
MyApp().run()# Parent sets child attributes (down)
child.value = 10
# Child posts messages to parent (up)
class ChildWidget(Widget):
class Updated(Message):
def __init__(self, value: int) -> None:
super().__init__()
self.value = value
def update_value(self) -> None:
self.post_message(self.Updated(self.value))
# Parent handles child messages
class ParentWidget(Widget):
def on_child_widget_updated(self, message: ChildWidget.Updated) -> None:
self.log(f"Child updated: {message.value}")import pytest
from my_app import MyApp
@pytest.mark.asyncio
async def test_button_click():
app = MyApp()
async with app.run_test() as pilot:
# Simulate user interaction
await pilot.click("#submit-button")
# CRITICAL: Wait for message processing
await pilot.pause()
# Assert state changed
result = app.query_one("#status")
assert "Success" in str(result.renderable)dock: top/bottom/left/right1frVerticalHorizontalGrid# Good: Compose from smaller widgets
class UserCard(Widget):
def compose(self) -> ComposeResult:
with Vertical():
yield Avatar()
yield UserName()
yield UserEmail()# UI in widgets/
class UserPanel(Widget):
def __init__(self) -> None:
super().__init__()
self.service = UserService() # Business logic
# Business logic in business_logic/
class UserService:
async def fetch_user(self, user_id: int) -> User:
# API calls, data processing
passclass MyApp(App):
CSS_PATH = "app.tcss" # Enables live reloadStatic@lru_cachecan_focus = True$primary$error# WRONG
def on_button_pressed(self):
self.mount(Widget())
# RIGHT
async def on_button_pressed(self):
await self.mount(Widget())# WRONG - race condition
async def test_feature():
await pilot.click("#button")
assert app.query_one("#status").text == "Done"
# RIGHT
async def test_feature():
await pilot.click("#button")
await pilot.pause() # Wait for processing
assert app.query_one("#status").text == "Done"# WRONG - triggers watchers too early
def __init__(self):
super().__init__()
self.count = 10
# RIGHT - use set_reactive or on_mount
def __init__(self):
super().__init__()
self.set_reactive(MyWidget.count, 10)# WRONG
def on_button_pressed(self):
response = requests.get("https://api.example.com") # Blocks UI!
# RIGHT - use workers
from textual.worker import work
@work(exclusive=True)
async def on_button_pressed(self):
response = await httpx.get("https://api.example.com")textual consoletextual run --dev my_app.pyfrom textual import log
log("Debug message", locals())# Screenshot after 5 seconds
textual run --screenshot 5 my_app.py
# Dev mode with live CSS reload
textual run --dev my_app.pyproject/
├── src/
│ ├── app.py # Main App class
│ ├── screens/
│ │ ├── main_screen.py
│ │ └── settings_screen.py
│ ├── widgets/
│ │ ├── status_bar.py
│ │ └── data_grid.py
│ └── business_logic/
│ ├── models.py
│ └── services.py
├── static/
│ └── app.tcss # External CSS
├── tests/
│ ├── test_app.py
│ └── test_widgets/
└── pyproject.tomlButtonCheckboxInputRadioButtonSelectSwitchTextAreaLabelStaticPrettyMarkdownMarkdownViewerDataTableListViewTreeDirectoryTreeHeaderFooterTabsTabbedContentVerticalHorizontalGriddef __init__(self) -> None:
"""Widget created - don't modify reactives here."""
super().__init__()
def compose(self) -> ComposeResult:
"""Build child widgets."""
yield ChildWidget()
def on_mount(self) -> None:
"""After mounted - safe to modify reactives."""
self.set_interval(1, self.update)
def on_unmount(self) -> None:
"""Before removal - cleanup resources."""
pass/* Docking */
#header { dock: top; height: 3; }
#sidebar { dock: left; width: 30; }
/* Flexible sizing */
#content { width: 1fr; height: 1fr; }
/* Grid layout */
#container {
layout: grid;
grid-size: 3 2;
grid-columns: 1fr 2fr 1fr;
}
/* Theme colors */
Button {
background: $primary;
color: $text;
}
Button:hover {
background: $primary-lighten-1;
}