textual
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseTextual TUI Framework
Textual TUI 框架
Build terminal applications with Textual's web-inspired architecture: App → Screen → Widget.
使用Textual的类Web架构构建终端应用:App → Screen → Widget。
Quick Start
快速开始
python
from textual.app import App, ComposeResult
from textual.widgets import Header, Footer, Static
class MyApp(App):
CSS_PATH = "styles.tcss"
BINDINGS = [("q", "quit", "Quit"), ("d", "toggle_dark", "Dark Mode")]
def compose(self) -> ComposeResult:
yield Header()
yield Static("Hello, World!")
yield Footer()
def action_toggle_dark(self) -> None:
self.theme = "textual-dark" if self.theme == "textual-light" else "textual-light"
if __name__ == "__main__":
MyApp().run()python
from textual.app import App, ComposeResult
from textual.widgets import Header, Footer, Static
class MyApp(App):
CSS_PATH = "styles.tcss"
BINDINGS = [("q", "quit", "Quit"), ("d", "toggle_dark", "Dark Mode")]
def compose(self) -> ComposeResult:
yield Header()
yield Static("Hello, World!")
yield Footer()
def action_toggle_dark(self) -> None:
self.theme = "textual-dark" if self.theme == "textual-light" else "textual-light"
if __name__ == "__main__":
MyApp().run()Core Concepts
核心概念
Widget Lifecycle
Widget 生命周期
- →
__init__()→compose()→on_mount()/on_show()→on_hide()on_unmount()
- →
__init__()→compose()→on_mount()/on_show()→on_hide()on_unmount()
Reactivity
响应式特性
python
from textual.reactive import reactive, var
class MyWidget(Widget):
count = reactive(0) # Triggers refresh on change
internal = var("") # No automatic refresh
def watch_count(self, new_value: int) -> None:
"""Called when count changes."""
self.styles.background = "green" if new_value > 0 else "red"
def validate_count(self, value: int) -> int:
"""Constrain values."""
return max(0, min(100, value))python
from textual.reactive import reactive, var
class MyWidget(Widget):
count = reactive(0) # Triggers refresh on change
internal = var("") # No automatic refresh
def watch_count(self, new_value: int) -> None:
"""Called when count changes."""
self.styles.background = "green" if new_value > 0 else "red"
def validate_count(self, value: int) -> int:
"""Constrain values."""
return max(0, min(100, value))Events and Messages
事件与消息
python
from textual import on
from textual.message import Message
class MyWidget(Widget):
class Selected(Message):
def __init__(self, value: str) -> None:
self.value = value
super().__init__()
def on_click(self) -> None:
self.post_message(self.Selected("item"))
class MyApp(App):
# Handler naming: on_<widget>_<message>
def on_button_pressed(self, event: Button.Pressed) -> None:
self.log(f"Button {event.button.id} pressed")
@on(Button.Pressed, "#submit") # CSS selector filtering
def handle_submit(self) -> None:
passpython
from textual import on
from textual.message import Message
class MyWidget(Widget):
class Selected(Message):
def __init__(self, value: str) -> None:
self.value = value
super().__init__()
def on_click(self) -> None:
self.post_message(self.Selected("item"))
class MyApp(App):
# Handler naming: on_<widget>_<message>
def on_button_pressed(self, event: Button.Pressed) -> None:
self.log(f"Button {event.button.id} pressed")
@on(Button.Pressed, "#submit") # CSS selector filtering
def handle_submit(self) -> None:
passData Flow
数据流
- Attributes down: Parent sets child properties directly
- Messages up: Child posts messages to parent via
post_message()
- 属性向下传递:父组件直接设置子组件属性
- 消息向上传递:子组件通过向父组件发送消息
post_message()
Screens
屏幕
python
from textual.screen import Screen
class WelcomeScreen(Screen):
BINDINGS = [("escape", "app.pop_screen", "Back")]
def compose(self) -> ComposeResult:
yield Static("Welcome!")
yield Button("Continue", id="continue")
def on_button_pressed(self, event: Button.Pressed) -> None:
if event.button.id == "continue":
self.app.push_screen("main")
class MyApp(App):
SCREENS = {"welcome": WelcomeScreen, "main": MainScreen}
def on_mount(self) -> None:
self.push_screen("welcome")python
from textual.screen import Screen
class WelcomeScreen(Screen):
BINDINGS = [("escape", "app.pop_screen", "Back")]
def compose(self) -> ComposeResult:
yield Static("Welcome!")
yield Button("Continue", id="continue")
def on_button_pressed(self, event: Button.Pressed) -> None:
if event.button.id == "continue":
self.app.push_screen("main")
class MyApp(App):
SCREENS = {"welcome": WelcomeScreen, "main": MainScreen}
def on_mount(self) -> None:
self.push_screen("welcome")Custom Widgets
自定义Widget
Simple Widget
简单Widget
python
class Greeting(Widget):
def render(self) -> RenderResult:
return "Hello, [bold]World[/bold]!"python
class Greeting(Widget):
def render(self) -> RenderResult:
return "Hello, [bold]World[/bold]!"Compound Widget
复合Widget
python
class LabeledButton(Widget):
DEFAULT_CSS = """
LabeledButton { layout: horizontal; height: auto; }
LabeledButton Label { width: 1fr; }
"""
def __init__(self, label: str, button_text: str) -> None:
self.label_text = label
self.button_text = button_text
super().__init__()
def compose(self) -> ComposeResult:
yield Label(self.label_text)
yield Button(self.button_text)python
class LabeledButton(Widget):
DEFAULT_CSS = """
LabeledButton { layout: horizontal; height: auto; }
LabeledButton Label { width: 1fr; }
"""
def __init__(self, label: str, button_text: str) -> None:
self.label_text = label
self.button_text = button_text
super().__init__()
def compose(self) -> ComposeResult:
yield Label(self.label_text)
yield Button(self.button_text)Focusable Widget
可聚焦Widget
python
class Counter(Widget):
can_focus = True
BINDINGS = [("up", "increment", "+"), ("down", "decrement", "-")]
count = reactive(0)
def action_increment(self) -> None:
self.count += 1python
class Counter(Widget):
can_focus = True
BINDINGS = [("up", "increment", "+"), ("down", "decrement", "-")]
count = reactive(0)
def action_increment(self) -> None:
self.count += 1Layout Patterns
布局模式
Containers
容器
python
from textual.containers import Horizontal, Vertical, Grid, VerticalScroll
def compose(self) -> ComposeResult:
with Vertical():
with Horizontal():
yield Button("Left")
yield Button("Right")
with VerticalScroll():
for i in range(100):
yield Label(f"Item {i}")python
from textual.containers import Horizontal, Vertical, Grid, VerticalScroll
def compose(self) -> ComposeResult:
with Vertical():
with Horizontal():
yield Button("Left")
yield Button("Right")
with VerticalScroll():
for i in range(100):
yield Label(f"Item {i}")Grid CSS
Grid CSS
css
Grid {
layout: grid;
grid-size: 3 2; /* columns rows */
grid-columns: 1fr 2fr 1fr;
grid-gutter: 1 2;
}
#wide { column-span: 2; }css
Grid {
layout: grid;
grid-size: 3 2; /* columns rows */
grid-columns: 1fr 2fr 1fr;
grid-gutter: 1 2;
}
#wide { column-span: 2; }Docking
停靠布局
css
#header { dock: top; height: 3; }
#sidebar { dock: left; width: 25; }
#footer { dock: bottom; height: 1; }css
#header { dock: top; height: 3; }
#sidebar { dock: left; width: 25; }
#footer { dock: bottom; height: 1; }Workers (Async)
工作线程(异步)
python
from textual import work
class MyApp(App):
@work(exclusive=True) # Cancels previous
async def fetch_data(self, url: str) -> None:
async with httpx.AsyncClient() as client:
response = await client.get(url)
self.query_one("#result").update(response.text)
@work(thread=True) # For sync APIs
def sync_operation(self) -> None:
result = blocking_call()
self.call_from_thread(self.update_ui, result)python
from textual import work
class MyApp(App):
@work(exclusive=True) # Cancels previous
async def fetch_data(self, url: str) -> None:
async with httpx.AsyncClient() as client:
response = await client.get(url)
self.query_one("#result").update(response.text)
@work(thread=True) # For sync APIs
def sync_operation(self) -> None:
result = blocking_call()
self.call_from_thread(self.update_ui, result)Testing
测试
python
async def test_app():
app = MyApp()
async with app.run_test() as pilot:
await pilot.press("enter")
await pilot.click("#button")
await pilot.pause() # Wait for messages
assert app.query_one("#status").render() == "Done"python
async def test_app():
app = MyApp()
async with app.run_test() as pilot:
await pilot.press("enter")
await pilot.click("#button")
await pilot.pause() # Wait for messages
assert app.query_one("#status").render() == "Done"Common Operations
常见操作
python
undefinedpython
undefinedQuery widgets
Query widgets
self.query_one("#id")
self.query_one(Button)
self.query(".class")
self.query_one("#id")
self.query_one(Button)
self.query(".class")
CSS classes
CSS classes
widget.add_class("active")
widget.toggle_class("visible")
widget.set_class(condition, "active")
widget.add_class("active")
widget.toggle_class("visible")
widget.set_class(condition, "active")
Visibility
Visibility
widget.display = True/False
widget.display = True/False
Mount/remove
Mount/remove
self.mount(NewWidget())
widget.remove()
self.mount(NewWidget())
widget.remove()
Timers
Timers
self.set_interval(1.0, callback)
self.set_timer(5.0, callback)
self.set_interval(1.0, callback)
self.set_timer(5.0, callback)
Exit
Exit
self.exit(return_code=0)
undefinedself.exit(return_code=0)
undefinedReferences
参考资料
- Widget catalog and messages: See references/widgets.md
- CSS properties and selectors: See references/css.md
- Complete examples: See references/examples.md
- Official docs: https://textual.textualize.io/
- 组件目录与消息:查看 references/widgets.md
- CSS属性与选择器:查看 references/css.md
- 完整示例:查看 references/examples.md
- 官方文档:https://textual.textualize.io/