Textual FAQ 全解析:图片、居中布局、Worker 与 ANSI 颜色等十大高频问题实战指南
【免费下载链接】textualThe lean application framework for Python. Build sophisticated user interfaces with a simple Python API. Run your apps in the terminal and a web browser.项目地址: https://gitcode.com/gh_mirrors/te/textual
导读
Textual 是一个使用纯 Python API 构建精致终端用户界面(TUI)的应用框架,它的官方 FAQ(见 docs/FAQ.md)沉淀了社区使用过程中最常被问到的十个问题——从"如何让 App 支持图片"到"为什么某些快捷键进不了应用",从"如何把组件居中"到"如何修复 WorkerDeclarationError"。本文以该 FAQ 为骨架,逐条展开每个问题的成因、解决步骤与完整可运行示例,并结合本仓库 src/textual 下的源码实现补充底层原理,帮助你在实际开发中快速定位、解决同类问题,同时理解 Textual 在颜色系统、Worker 模型与终端兼容性上的设计取舍。
1. Textual 支持图片吗?
结论:Textual 目前没有内建的图片渲染支持,但该能力已列入官方路线图。
从仓库结构看,路线图文档位于 docs/roadmap.md,图片渲染是其中的规划方向之一。FAQ 给出的过渡方案是借助 rich-pixels 项目——它提供了一种可在 Textual 中使用的 Rich renderable,把像素图片渲染进终端界面。
实用建议:
- 若你的应用需要展示图片,可优先尝试
rich-pixels这类基于 Rich 的渲染方案; - 关注官方 Roadmap 中关于图片支持的进度,内建支持落地后即可切换到官方 API;
- 由于终端渲染本质是字符与颜色块拼合,图片在终端中的实际效果受终端字体、配色与尺寸影响,建议在目标终端上做渲染验证。
2. 如何修复 "ImportError: cannot import name ComposeResult from textual.app"?
成因:本地安装的 Textual 版本过旧。
ComposeResult是 Textual 应用的核心类型注解,App 的compose方法返回它(例如 docs/guide/app.md 中的示例均为def compose(self) -> ComposeResult:)。旧版本不存在该导出名称,因此会抛出上述 ImportError。
修复方式:强制升级到最新版。
pip install textual-dev -U-U(即--upgrade)会强制 pip 升级已安装的包。升级后重新运行应用即可。若你还在使用pip install textual -U的老命令,也建议一并升级textual-dev(Textual 的开发工具集,包含textual命令等),保持两者版本一致。
补充排查:如果升级后依然报错,请确认没有其他位置安装了旧版本(如系统级与虚拟环境级重复安装),可通过
pip show textual查看当前版本。
3. 如何在 Textual 应用中选中并复制文本?
Textual 为大多数组件(widget)内建了文本选择能力:按住鼠标左键拖拽即可选中,按ctrl+c复制。
从源码实现看,这一能力由一套完整的选区机制支撑:
- src/textual/selection.py 定义了
Selection(一个 NamedTuple,记录选区的起始与结束偏移),并提供了extract方法从文本中抽取被选中的片段; - src/textual/widget.py 的
text_selection属性暴露当前组件内的选区信息,而 src/textual/widget.py 的get_selection负责把选区与组件文本结合后返回复制内容; - 组件类上还声明了"该组件是否支持自动文本选择"的能力开关(见 src/textual/widget.py),说明不同组件对选择的支持程度是有差异的。
对于还不支持文本选择的组件,可以退回到终端自身的文本选择能力:大多数终端模拟器提供一个修饰键,按住它再点击拖拽,即可恢复命令行下"直接选择屏幕文本"的原始行为。修饰键随终端和平台而异:
| 终端 | 修饰键 |
|---|---|
| iTerm(macOS) | 按住OPTION键 |
| Gnome Terminal(Linux) | 按住SHIFT键 |
| Windows Terminal | 按住SHIFT键 |
如果你的终端不在上表中,请查阅该终端模拟器的官方文档确认对应的修饰键。
4. 如何设置半透明的 App 背景?
部分终端模拟器支持半透明背景,让桌面透过终端窗口部分可见。但这一特性在 Textual 中基本不会生效,原因是:
- 半透明效果依赖 ANSI 背景色(16 色体系)的 alpha 叠加,而 Textual不使用 ANSI 颜色;
- Textual 在终端支持时使用1670 万色(Truecolor),以此保证跨平台颜色一致,并支持 ANSI 颜色无法实现的混合、明暗衍生等特效。
这一设计取舍的细节可参见下文"为什么 Textual 不支持 ANSI 主题"一节。简单说:如果你依赖终端半透明背景,请在 Textual 应用内使用不透明的背景色,或接受半透明效果被覆盖的事实;反之,如果业务上必须保留 ANSI 原生色彩行为,可参考 0.80.0 版本引入的ansi_color配置(见第 10 节),但要注意开启后会失去透明度相关效果。
5. 如何把组件在屏幕中居中?
FAQ 给出了两条清晰的路径,两者的关键差异在于:align作用于容器的子组件,而不是作用于你想居中的那个组件本身。
5.1 方式一:让 Screen 直接对齐子组件
对Screen设置align: center middle,其直接子组件就会居中显示:
from textual.app import App, ComposeResult from textual.widgets import Button class ButtonApp(App): CSS = """ Screen { align: center middle; } """ def compose(self) -> ComposeResult: yield Button("PUSH ME!") if __name__ == "__main__": ButtonApp().run()5.2 多个组件时的"左对齐"陷阱
如果按上面的写法同时放多个组件,你会发现它们虽然整体居中,但彼此之间是"左对齐"的:
+-----+ | | +-----+ +---------+ | | +---------+ +---------------+ | | +---------------+这是因为每个组件都从同一水平起点排列。如果你希望得到"每行各自居中"的布局:
+-----+ | | +-----+ +---------+ | | +---------+ +---------------+ | | +---------------+5.3 方式二:用 Center 容器逐个包裹
最佳做法是把每个组件分别放进一个Center容器中,让每个容器独立完成居中:
from textual.app import App, ComposeResult from textual.containers import Center from textual.widgets import Button class ButtonApp(App): CSS = """ Screen { align: center middle; } """ def compose(self) -> ComposeResult: yield Center(Button("PUSH ME!")) yield Center(Button("AND ME!")) yield Center(Button("ALSO PLEASE PUSH ME!")) yield Center(Button("HEY ME ALSO!!")) if __name__ == "__main__": ButtonApp().run()从源码看,Center 的默认样式是align-horizontal: center; width: 1fr; height: auto;——即横向铺满可用宽度、高度自适应,并把子组件水平居中。与之配套的还有Middle(垂直居中,src/textual/containers.py)、CenterMiddle(两轴同时居中,src/textual/containers.py)和Right(右对齐)等容器,可组合出多种对齐布局。
更全面的居中方案(包括水平/垂直/双向居中的各种场景)见仓库中的指南 docs/how-to/center-things.md。
6. 如何修复 WorkerDeclarationError?
成因:Textual 0.31.0 起,@work装饰器要求:凡是线程型 Worker(thread worker),必须显式声明thread=True。
此前版本中,把一个普通(非 async)函数直接加上@work就会被当作线程 Worker 运行,这很容易在无意识中创建线程 Worker,进而产生难以预期的并发结果,因此新版本改为强制显式声明。
6.1 声明一个线程 Worker
如果你确实需要后台线程:
@work(thread=True) def run_in_background(): ...6.2 声明一个协程 Worker
如果你不需要线程,就把工作函数写成async:
@work() async def run_in_background(): ...6.3 源码层面的强制校验
这一约束在 src/textual/_work_decorator.py 中直接落地:work装饰器在包装函数时会检查iscoroutinefunction(method),如果被装饰函数不是协程函数且未设置thread=True,立即抛出WorkerDeclarationError,提示信息为:
Can not create a worker from a non-async function unless `thread=True` is set on the work decorator.同时,@work还支持name、group、exit_on_error、exclusive、description等参数(src/textual/_work_decorator.py),用于给 Worker 命名、分组、控制异常是否退出应用等。更完整的 Worker 体系(run_worker、Worker 生命周期、group与exclusive的协作机制)可参考 docs/guide/workers.md 与 src/textual/worker.py。
7. 如何给 App 传递参数?
Textual 的App和其他 Python 类一样,直接重写__init__即可接收自定义参数,注意在自定义逻辑之后调用super().__init__()。
from textual.app import App, ComposeResult from textual.widgets import Static class Greetings(App[None]): def __init__(self, greeting: str="Hello", to_greet: str="World") -> None: self.greeting = greeting self.to_greet = to_greet super().__init__() def compose(self) -> ComposeResult: yield Static(f"{self.greeting}, {self.to_greet}")运行方式与普通 Python 传参完全一致:
# 使用默认参数运行。 Greetings().run() # 使用关键字参数运行。 Greetings(to_greet="davep").run() # 使用位置参数运行。 Greetings("Well hello", "there").run()关于App[None]这种类型参数写法(用于声明 App 的回调结果类型),以及更多 App 自定义(如__init__中常见的driver_class、css_path、watch_css等参数,见 src/textual/app.py),可参考 docs/api/app.md。另外,如果你需要把命令行参数(如argparse解析结果)注入 App,也可以在入口处先解析参数再传入App的构造函数,这与 FAQ 给出的方式是天然衔接的。
8. 为什么某些按键组合永远到不了我的应用?
核心结论:Textual 只能收到"终端应用愿意传给它"的按键组合,而终端能传递哪些键,因终端和操作系统而异。
FAQ 给出的推荐做法是:尽量使用各终端普遍支持的按键组合,包括:
- 字母键
- 数字键
- 数字功能键(尤其是 F1~F10)
- 空格
- 回车
- 方向键、Home、End 与翻页键
- Control
- Shift
在为应用创建绑定(binding)时,docs/guide/input.md 的"Bindings"一节建议优先从上述键位中选取。而终端通常不会透传的键包括:macOS 上的 Cmd 与 Option,Windows 上的 Win 键。
8.1 用textual keys实测按键
如果你需要在不同环境下验证哪些组合可用,Textual 提供了专门的调试工具:
textual keys运行后会进入一个按键演示界面,实时显示你按下的每个键及其在 Textual 中的表示,是排查按键绑定失效问题的第一手段。textual keys由textual-dev提供,安装方式见第 2 节的升级命令。
8.2 键盘协议与调度
从源码层面看,Textual 的键盘输入解析位于 src/textual/_keyboard_protocol.py(负责把终端字节流解析为按键事件),按键分派逻辑见 src/textual/_dispatch_key.py,而按键事件的完整流转可参考 src/textual/events.py 中的Key事件。所有这些环节的前提都是:终端先把手上的按键编码发给 Textual。如果终端本身吞掉了这个组合(例如 macOS Terminal 的 Cmd 组合被系统截获),Textual 无从感知。
9. 为什么 Textual 在 macOS 上显示效果不佳?
如果你使用 macOS 自带的 Terminal.app,很可能会发现 Textual(以及大部分 TUI)渲染不佳,尤其是制表符方框字符错位、线条断块等问题。
9.1 可行的修复:调整字体设置
进入 Terminal.app 的设置 → 描述文件(Profiles)→ 文本(Text)标签页调整字体参数。FAQ 中实测有效的组合是:
- 字体:Menlo Regular
- 字符间距:1
- 行间距:0.805
如果换用其他字体,需要自行微调行间距直到显示正常。
9.2 即便修复后仍有硬限制
即使做了上述设置,Terminal.app 依然存在两个明显短板:
- 只支持 256 色,无法发挥 Textual 的 1670 万色 Truecolor 能力;
- 渲染速度较慢,相比现代终端有明显差距。
9.3 推荐的 macOS 终端
FAQ 推荐以下免费终端,它们对 Truecolor 与框线字符的支持更好,能获得接近设计预期的渲染效果:
- iTerm2
- Kitty
- WezTerm
提示:本项目还专门维护了一份"为什么在 macOS 上看起来不好看"的问答记录,见 questions/why-looks-bad-on-macos.question.md;关于 Textual 的配色方案在两种终端下的视觉差异,可在第 10 节理解其颜色体系的背景下,用 examples/theme_sandbox.py 自行对比体验。
10. 为什么 Textual 不支持 ANSI 主题?
这是 FAQ 中篇幅最长、也最能体现 Textual 设计哲学的一个问题。Textual 刻意不生成 16 种可主题化 ANSI 颜色对应的转义序列,这是经过权衡的主动设计决策,原因有二:
- 并非每个人都有一个精心挑选的 ANSI 配色主题:在你机器上看着舒服的颜色组合,在别人机器上可能完全不可读。应用作者和 Textual 都无法解决这种"千人千色"的差异;要求用户自行更换主题也不是好方案,因为并非所有用户都懂得怎么配。
- ANSI 颜色无法像其他颜色那样被程序化操作:Textual 可以对颜色做混合(blend)、从原色衍生明暗色阶,从而生成可读性更好的文字与界面;这种颜色混合能力未来还会支撑无障碍(accessibility)相关的特性。这些能力是 ANSI 16 色体系无法提供的。
10.1 Textual 的设计系统(design system)
Textual 拥有一套设计系统(design system),保证应用在所有平台与终端上可读,效果优于 ANSI 颜色。当前提供浅色(light)与深色(dark)两套设计系统,更多色系在规划中;未来还支持用户在单个应用或单台机器级别自定义源色,届时你可以修改核心颜色,使其与自己的终端主题融合。
10.2 0.80.0 起新增的ansi_color开关
FAQ 特别提示(自版本 0.80.0 起):App新增了ansi_color布尔配置。将其设为True时,Textual 将不再尝试把 ANSI 颜色转换为 Truecolor,而是保留原生的 ANSI 颜色输出;但代价是会失去透明度相关效果。
从源码可以完整看到这条链路:
- src/textual/app.py 声明了
ansi_color: Reactive[bool | None] = Reactive(None),并在构造函数中接收该参数(src/textual/app.py); - 颜色管线通过
ANSIToTruecolor过滤器把 ANSI 颜色映射为 RGB(src/textual/app.py),过滤器是否启用取决于native_ansi_color; native_ansi_color(src/textual/app.py)的取值规则是:若ansi_color为None,则回退到当前主题的ansi字段;否则直接采用ansi_color的布尔值。这意味着"不设置时跟随主题,设置时以显式配置为准";- 主题切换时,
_refresh_truecolor_filter会按新的主题映射重建过滤器(src/textual/app.py)。
实际配置示例:
from textual.app import App class MyApp(App): ansi_color = True # 保留原生 ANSI 颜色,不转换;会失去透明度效果 def compose(self): yield Static("Hello")或者通过构造函数传入:
app = MyApp(ansi_color=True) app.run()10.3 相关主题配置
在App上还有两个配套响应式属性用于控制 ANSI 颜色映射:ansi_theme_dark与ansi_theme_light(src/textual/app.py),它们分别定义深色/浅色主题下 ANSI 颜色到十六进制色的映射(基于 Rich 的TerminalTheme)。配合 src/textual/theme.py 中主题的ansi字段,构成了 Textual "默认转换 ANSI、可显式关闭转换"的完整颜色策略。
结语:FAQ 背后的三条工程原则
纵观这份 FAQ 的十个问题,可以提炼出 Textual 的三条核心工程原则:
- 颜色一致性优先:宁可放弃 ANSI 生态的"半透明"与"跟随系统主题",也要保证 1670 万色下的跨平台一致渲染(问题 4、10);
- 显式优于隐式:Worker 必须显式声明线程类型,避免无意识的并发陷阱(问题 6);
- 终端能力边界是硬约束:按键透传、字体渲染、颜色深度都由终端决定,Textual 能做的是提供调试工具(
textual keys)与终端推荐(问题 8、9)。
对开发者而言,遇到上述问题时,可优先在 docs/help.md 列出的帮助渠道中检索,同时利用textual keys、docs/guide/devtools.md 中的开发者工具等内置手段快速定位,让 TUI 开发回归到"专注业务逻辑"本身。
【免费下载链接】textualThe lean application framework for Python. Build sophisticated user interfaces with a simple Python API. Run your apps in the terminal and a web browser.项目地址: https://gitcode.com/gh_mirrors/te/textual
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考