用 prompt_toolkit + Rich 打造一个现代终端应用

目录

  • 引子
  • rich
  • prompt_toolkit
    • 快速开始
    • Printing(以及使用)格式化文本
      • [Printing plain text(打印普通文本)](#Printing plain text(打印普通文本))
      • [Formatted text(格式化文本)](#Formatted text(格式化文本))
        • HTML
        • ANSI
        • [(style, text) 元组](#(style, text) 元组)
        • [Pygments (Token, text) 元组](#Pygments (Token, text) 元组)
        • to_formatted_text
    • [Asking for input (prompts)(输入提示 / prompts)](#Asking for input (prompts)(输入提示 / prompts))
      • [Hello world](#Hello world)
        • [The PromptSession object(PromptSession 对象)](#The PromptSession object(PromptSession 对象))
      • [Syntax highlighting(语法高亮)](#Syntax highlighting(语法高亮))
      • Colors(颜色)
        • [Using a Pygments style(使用 Pygments 样式)](#Using a Pygments style(使用 Pygments 样式))
      • 自动补全(Autocompletion)
        • [嵌套补全(Nested completion)](#嵌套补全(Nested completion))
        • [自定义补全器(A custom completer)](#自定义补全器(A custom completer))
        • [单个补全项的样式(Styling individual completions)](#单个补全项的样式(Styling individual completions))
        • [模糊补全(Fuzzy completion)](#模糊补全(Fuzzy completion))
        • [异步补全(Asynchronous completion)](#异步补全(Asynchronous completion))
      • [输入校验(Input validation)](#输入校验(Input validation))
        • [使用函数创建验证器(Validator from a callable)](#使用函数创建验证器(Validator from a callable))
      • 历史记录(History)
      • [自动建议(Auto suggestion)](#自动建议(Auto suggestion))
      • [添加底部工具栏(Adding a bottom toolbar)](#添加底部工具栏(Adding a bottom toolbar))
      • [添加右侧提示(Adding a right prompt)](#添加右侧提示(Adding a right prompt))
      • [Vi 输入模式(Vi input mode)](#Vi 输入模式(Vi input mode))
      • [添加自定义按键绑定(Adding custom key bindings)](#添加自定义按键绑定(Adding custom key bindings))
        • [根据条件启用按键绑定(Enable key bindings according to a condition)](#根据条件启用按键绑定(Enable key bindings according to a condition))
        • [在 Emacs 和 Vi 模式之间动态切换(Dynamically switch between Emacs and Vi mode)](#在 Emacs 和 Vi 模式之间动态切换(Dynamically switch between Emacs and Vi mode))
        • [使用 control-space 触发补全(Using control-space for completion)](#使用 control-space 触发补全(Using control-space for completion))
      • [其他 prompt 选项](#其他 prompt 选项)
        • [多行输入(Multiline input)](#多行输入(Multiline input))
        • [设置默认值(Passing a default)](#设置默认值(Passing a default))
        • [鼠标支持(Mouse support)](#鼠标支持(Mouse support))
        • [换行方式(Line wrapping)](#换行方式(Line wrapping))
        • [密码输入(Password input)](#密码输入(Password input))
      • [光标形状(Cursor shapes)](#光标形状(Cursor shapes))
      • [添加边框(Adding a frame)](#添加边框(Adding a frame))
      • [在 asyncio 应用中使用 prompt(Prompt in an asyncio application)](#在 asyncio 应用中使用 prompt(Prompt in an asyncio application))
      • [不显示 prompt,逐键读取 stdin(Reading keys from stdin, one key at a time, but without a prompt)](#不显示 prompt,逐键读取 stdin(Reading keys from stdin, one key at a time, but without a prompt))
    • [询问选择(Asking for a choice)](#询问选择(Asking for a choice))
      • [选项着色(Coloring the options)](#选项着色(Coloring the options))
      • [添加边框(Adding a frame)](#添加边框(Adding a frame))
      • [添加底部工具栏(Adding a bottom toolbar)](#添加底部工具栏(Adding a bottom toolbar))
    • 对话框(Dialogs)
      • [消息框(Message box)](#消息框(Message box))
      • [输入框(Input box)](#输入框(Input box))
      • [是/否确认对话框(Yes/No confirmation dialog)](#是/否确认对话框(Yes/No confirmation dialog))
      • [按钮对话框(Button dialog)](#按钮对话框(Button dialog))
      • [单选列表对话框(Radio list dialog)](#单选列表对话框(Radio list dialog))
      • [多选列表对话框(Checkbox list dialog)](#多选列表对话框(Checkbox list dialog))
      • [对话框样式(Styling of dialogs)](#对话框样式(Styling of dialogs))
  • 结尾

引子

使用Claude Code 这种工具的时候,其实挺容易产生一种落差感。

你会发现,同样是在终端里,它的体验却完全不像"终端程序":

输入是流畅的,有补全、有结构感,不再是生硬的一行行文本;

输出是有层次的,颜色、布局、甚至信息密度都被精心设计过,看起来更像一个界面,而不是简单的 print。

那一刻很容易产生一个想法------

这玩意到底是怎么做出来的?

更具体一点说:

为什么我们自己写的 CLI,永远停留在 input + print 的阶段,

而这些工具,却已经把终端做成了一种"轻量级 UI"?

一开始可能会觉得,这背后是不是一整套很重的前端体系,甚至是某种黑科技。

但真正去拆的时候会发现,答案反而很"朴素":

输入和输出,被拆开各自做到极致。

在 Python 生态里,对应的其实正是两个库:prompt_toolkit 和 Rich。

前者解决的是"你如何与用户交互";

后者解决的是"你如何把信息展示出来"。

它们并不是什么新东西,但当你把这两块拼在一起的时候,就会突然意识到一件事:

原来限制终端体验的,从来不是终端本身,而是我们写代码的方式。

rich

很多人第一次意识到终端也可以"很好看",往往不是因为自己写代码,而是因为用了某个工具。

日志是分层的,错误是高亮的,表格是规整的,甚至连一段普通的文本,都有清晰的结构和节奏感。那种体验很微妙------你明知道它只是终端输出,但看起来却更像一个被设计过的界面。

反过来看我们自己写的程序,大多数时候却停留在另一种状态:

print 到处都是,信息堆在一起,没有层次,也没有重点。代码逻辑也许没问题,但一旦输出复杂起来,可读性就迅速崩掉。

问题其实很简单:
终端并不缺能力,缺的是"渲染"。

在 Python 生态里,Rich 做的正是这件事------

它不是简单地给 print 加点颜色,而是把"终端输出"这件事,当成一个可以被设计的系统来对待。

快速开始

Rich 是一个用于在终端输出富文本(包含颜色和样式)的 Python 库,同时还支持展示更复杂的内容,例如表格、Markdown,以及带语法高亮的代码。

你可以使用 Rich 来让命令行程序更具视觉表现力,并以更易读的方式展示数据。此外,Rich 还可以作为调试工具,通过美化输出和语法高亮,让数据结构更加清晰直观。

Rich 支持以下操作系统:

  • macOS
  • Linux
  • Windows

在 Windows 上,既支持传统的 cmd.exe 终端,也支持新的 Windows Terminal(后者对颜色和样式的支持更好)。

Rich 需要 Python 3.8 及以上版本。

注意

如果你使用的是 PyCharm,需要在运行/调试配置中开启 "emulate terminal",否则无法看到样式效果。

或者可以直接使用git bash或者cmdline去查看样式:

我们可以通过 pip 安装:

bash 复制代码
pip install rich

如果已经安装,可以使用 -U 参数升级到最新版本:

bash 复制代码
pip install -U rich

如果你打算在 Jupyter 中使用 Rich,需要额外安装依赖:

bash 复制代码
pip install "rich[jupyter]"

你可以通过以下命令快速验证是否安装成功,并查看 Rich 的效果:

bash 复制代码
python -m rich

最直接的入门方式,是直接使用 Rich 提供的 print 函数,它的参数和 Python 内置的 print 完全一致,可以作为替代品使用:

python 复制代码
from rich import print

之后你就可以像平常一样输出内容,Rich 会自动进行基础的语法高亮,并对数据结构进行格式化,使其更易读。

字符串中还可以使用 Console Markup 来添加颜色和样式。

例如:

python 复制代码
print("[italic red]Hello[/italic red] World!", locals())

输出效果(包含颜色和样式)类似如下:

复制代码
Hello World!
{
    '__annotations__': {},
    '__builtins__': <module 'builtins' (built-in)>,
    '__doc__': None,
    '__loader__': <class '_frozen_importlib.BuiltinImporter'>,
    '__name__': '__main__',
    '__package__': None,
    '__spec__': None,
    'print': <function print at 0x1027fd4c0>,
}

如果你不想覆盖内置的 print,也可以这样导入:

python 复制代码
from rich import print as rprint

Rich 可以直接集成到 Python REPL 中,使输出自动具备高亮和美化效果:

python 复制代码
from rich import pretty
pretty.install()
["Rich and pretty", True]

此外,你还可以用它来测试 Rich 的渲染组件(renderables):

python 复制代码
from rich.panel import Panel
Panel.fit("[bold yellow]Hi, I'm a Panel", border_style="red")

Rich 还提供了 IPython 扩展,可以实现自动美化输出和更友好的异常信息:

python 复制代码
%load_ext rich

你也可以在 IPython 配置中,将 "rich" 添加到扩展列表中,实现自动加载。

(c.InteractiveShellApp.extension)

Rich 提供了一个非常实用的调试工具:inspect(),可以生成任意 Python 对象的详细报告。

示例:

python 复制代码
from rich import inspect
from rich.color import Color

color = Color.parse("red")
inspect(color, methods=True)

这个功能在调试时非常有用,也很好地展示了 Rich 的输出能力。

终端输出控制台

如果你希望对终端输出拥有更完整的控制能力,Rich 提供了一个核心类:Console

在大多数应用中,通常只需要一个全局的 Console 实例。因此,一个常见的做法是在项目中创建一个统一的入口,比如新建一个 console.py

python 复制代码
from rich.console import Console
console = Console()

之后你可以在项目的任意位置直接导入使用:

python 复制代码
from my_project.console import console

Console 本质上负责一件事情:

👉 把你写的内容,转换成终端能够理解的格式(ANSI 转义序列),从而实现颜色、样式等效果。

同时,它还会自动检测当前终端的能力,比如:

  • 是否支持颜色
  • 支持多少种颜色
  • 是否需要做兼容转换

换句话说,你只需要"描述你想要的效果",剩下的交给 Console。

而快速开始提到的print方法实际上也是基于默认的Console进行输出的:

属性

Console 会自动检测一些与渲染相关的重要属性:

  • size

    当前终端的尺寸(窗口大小改变时会动态变化)

  • encoding

    默认编码(通常是 "utf-8"

  • is_terminal

    是否正在输出到真正的终端(布尔值)

  • color_system

    当前终端支持的颜色系统(下面会详细说明)

颜色系统

终端的颜色并不是统一标准的,不同环境支持的能力差别很大。

Rich 会自动检测当前终端支持的颜色系统,但你也可以在创建 Console 时手动指定:

python 复制代码
Console(color_system="256")

支持的取值如下:

  • None

    完全禁用颜色

  • "auto"

    自动检测(默认)

  • "standard"

    支持 8 种基础颜色(加亮色共 16 种)

  • "256"

    支持 256 色(在 standard 基础上增加 240 种固定颜色)

  • "truecolor"

    支持约 1670 万种颜色(基本等同于显示器能力)

  • "windows"

    旧版 Windows 终端(仅支持 8 色)

    (新 Windows Terminal 实际支持 truecolor)

⚠️ 注意

手动设置颜色系统时需要谨慎:

如果你设置的颜色能力 高于终端实际支持的能力,输出可能会出现异常,甚至变得不可读。

输出(打印)

要向终端输出富文本内容,可以使用 console.print() 方法。

Rich 会自动将任意对象通过其 __str__ 方法转换为字符串,并进行基础的语法高亮。同时,对于容器类型(如 dictlist),还会进行美化排版(pretty print)。

如果输出的是字符串,还会解析其中的 Console Markup(用于添加颜色和样式)。

例如:

python 复制代码
console.print([1, 2, 3])
console.print("[blue underline]Looks like a link")
console.print(locals())
console.print("FOO", style="white on blue")

此外,print() 还可以直接渲染支持 Console Protocol 的对象。这包括 Rich 内置的组件,例如 TextTableSyntax,以及你自定义的对象。

日志输出

console.log() 方法在 print() 的基础上增加了一些对调试非常有用的功能。

它会在输出中自动附加:

  • 左侧:当前时间
  • 右侧:调用该方法的文件名和行号

示例:

python 复制代码
console.log("Hello, World!")

输出类似:

id="k4gq2k" 复制代码
[16:32:08] Hello, World!                                         <stdin>:1

此外,log() 还支持一个很实用的参数:log_locals=True

开启后,Rich 会自动打印当前作用域中的局部变量(以表格形式展示),对调试非常友好。

python 复制代码
from rich.console import Console

console = Console()

a= 10

if __name__ == '__main__':
    console.log("hello",log_locals=True)

JSON 输出

console.print_json() 方法可以对 JSON 字符串进行美化输出(格式化 + 语法高亮)。

例如:

python 复制代码
console.print_json('[false, true, null, "foo"]')

你也可以通过 log() 输出 JSON 对象:

python 复制代码
from rich.json import JSON
console.log(JSON('["foo", "bar"]'))

由于这是一个常见需求,Rich 也提供了更简便的导入方式:

python 复制代码
from rich import print_json

此外,还可以直接在命令行中使用:

bash 复制代码
python -m rich.json cats.json

底层输出

除了 print()log(),Rich 还提供了一个更底层的方法:console.out()

这个方法的特点是:

  • 只会将参数转换为字符串
  • 不会进行美化排版(pretty print)
  • 不会自动换行
  • 不会解析 markup

但仍然可以:

  • 应用基础样式
  • 可选开启高亮

示例:

python 复制代码
console.out("Locals", locals())

👉 print / log / out 本质上是 三个不同抽象层级的输出接口

  • print:面向展示(默认最智能)
  • log:面向调试(附带上下文)
  • out:面向控制(最接近底层)

分割线

console.rule() 方法可以绘制一条横线,并可选地带一个标题,非常适合用来对终端输出进行分段。

例如:

python 复制代码
console.rule("[bold red]Chapter 2")

效果类似:

id="v4d8tm" 复制代码
─────────────────────────────── Chapter 2 ───────────────────────────────

此外,rule() 还支持:

  • style:设置横线样式
  • align:控制标题对齐方式("left""center""right"

状态指示(加载动画)

Rich 可以显示带有"加载动画(spinner)"的状态信息,并且不会影响正常的终端输出。

你可以运行以下命令查看效果:

bash 复制代码
python -m rich.status

在代码中,可以通过 console.status() 来使用这个功能。它返回的是一个上下文管理器,会在代码块执行期间显示状态,执行结束后自动关闭。

示例:

python 复制代码
with console.status("Working..."):
    do_work()

你还可以通过 spinner 参数自定义动画样式:

python 复制代码
with console.status("Monkeying around...", spinner="monkey"):
    do_work()

可用的 spinner 列表可以通过以下命令查看:

bash 复制代码
python -m rich.spinner

对齐方式

console.print()console.log() 都支持一个 justify 参数,用于控制文本对齐方式,可选值包括:

  • "default"(默认)
  • "left"
  • "right"
  • "center"
  • "full"

它们的含义如下:

  • left:左对齐
  • right:右对齐
  • center:居中
  • full:两端对齐(类似书籍排版)

默认的 "default" 看起来和 "left" 很像,但实际上有一个细微差别:

  • "left":会用空格填充右侧
  • "default":不会填充

这个差异通常只有在设置了背景色时才明显。

示例:

python 复制代码
from rich.console import Console

console = Console(width=20)

style = "bold white on blue"
console.print("Rich", style=style)
console.print("Rich", style=style, justify="left")
console.print("Rich", style=style, justify="center")
console.print("Rich", style=style, justify="right")

输出效果类似:

id="c7h4vx" 复制代码
Rich
Rich                
        Rich        
                Rich

👉 Rich 已经不只是"打印文本",而是在做终端排版系统

  • rule → 分割结构
  • status → 动态状态
  • justify → 排版控制

这已经开始有点像"终端里的 UI layout"了,而不是简单输出。

溢出处理

所谓 Overflow(溢出),指的是你输出的文本长度超过了当前可用空间。

这种情况通常会出现在:

  • 很长的单词(例如 URL)
  • 表格单元格空间不足
  • Panel 内部宽度受限
  • 终端窗口较窄

你可以通过 print()overflow 参数来控制 Rich 如何处理这些超出的内容。

可选值包括:

  • "fold"
  • "crop"
  • "ellipsis"
  • "ignore"

默认值是:

python 复制代码
overflow="fold"

fold(默认)

把超出的文本折叠到下一行,直到全部显示完。

python 复制代码
console.print(text, overflow="fold")

效果:

id="m2v5qt" 复制代码
supercalifragi
listicexpialid
ocious

crop

直接裁剪超出的部分,不再显示。

python 复制代码
console.print(text, overflow="crop")

效果:

id="k6n1ew" 复制代码
supercalifragi

ellipsis

裁剪超出的部分,并在结尾加上省略号。

python 复制代码
console.print(text, overflow="ellipsis")

效果:

id="g3j8hf" 复制代码
supercalifrag...

ignore

忽略宽度限制,让文本继续延伸。

python 复制代码
console.print(text, overflow="ignore")

通常看起来和 crop 类似,除非同时设置:

python 复制代码
crop=False

控制台样式

Console 对象有一个 style 属性,可以为所有输出统一设置默认样式。

默认值是:

python 复制代码
style=None

例如:

python 复制代码
from rich.console import Console

blue_console = Console(style="white on blue")
blue_console.print("I'm blue.")

这样这个 Console 输出的所有内容都会带上相同的风格。

软换行

默认情况下,Rich 会自动对长文本进行换行,使内容适配终端宽度。

如果你希望关闭这种行为,可以设置:

python 复制代码
soft_wrap=True

示例:

python 复制代码
console.print(text, soft_wrap=True)

开启后,文本会像 Python 原生 print() 一样自然延续到下一行。

裁剪

print() 方法还有一个布尔参数:

python 复制代码
crop=True

默认情况下,Rich 会裁剪那些本来会溢出的内容。

一般来说你很少需要手动控制这个参数,因为 Rich 会尽量自动适配当前终端宽度。

注意

如果设置了:

python 复制代码
soft_wrap=True

那么 crop 会自动失效。

输入

除了输出之外,Console 还提供了一个 input() 方法。

它和 Python 内置的 input() 用法相同,但提示文字可以使用 Rich 的样式。

例如:

python 复制代码
from rich.console import Console

console = Console()
console.input("What is [i]your[/i] [bold red]name[/]? :smiley: ")

你可以得到一个带颜色、样式甚至 emoji 的输入提示。

如果系统提前加载了 Python 的 readline 模块,console.input() 还可以支持:

  • 行内编辑
  • 输入历史
  • 更自然的命令行体验

导出

Console 不仅可以输出到终端,还可以将所有输出内容导出为:

  • 文本(text)
  • SVG
  • HTML

要启用导出功能,需要在创建 Console 时开启 record=True

python 复制代码
from rich.console import Console

console = Console(record=True)

这个参数的作用是:

👉 让 Rich 记录你所有通过 print()log() 输出的内容。

在输出完成之后,你可以通过以下方法获取结果(字符串形式):

python 复制代码
console.export_text()
console.export_svg()
console.export_html()

或者直接写入文件:

python 复制代码
console.save_text("output.txt")
console.save_svg("output.svg")
console.save_html("output.html")
导出 SVG

当使用 export_svg()save_svg() 时:

  • SVG 的宽度会与当前终端宽度(字符数)一致
  • 高度会根据内容自动扩展

生成的 SVG 可以:

  • 直接在浏览器中打开
  • <img> 标签嵌入网页
  • 或直接嵌入 HTML 代码中

你还可以自定义 SVG 的主题(颜色风格):

python 复制代码
from rich.console import Console
from rich.terminal_theme import MONOKAI

console = Console(record=True)
console.save_svg("example.svg", theme=MONOKAI)

如果默认主题不满足需求,也可以自己构建一个 TerminalTheme

注意

导出的 SVG 默认使用 Fira Code 字体

如果你将 SVG 嵌入网页,建议同时引入该字体的 CSS。

错误控制台

默认情况下,Console 的输出会写入:

python 复制代码
sys.stdout

也就是标准输出(终端正常显示内容)。

如果你在创建 Console 时设置:

python 复制代码
Console(stderr=True)

那么输出会写入:

python 复制代码
sys.stderr

也就是标准错误流。

这在工程中很有用,你可以专门创建一个"错误控制台",用于区分普通输出和错误信息:

python 复制代码
from rich.console import Console

error_console = Console(stderr=True)

你还可以给错误输出设置统一风格,例如:

python 复制代码
error_console = Console(stderr=True, style="bold red")

这样错误信息会在视觉上更加明显。

文件输出

你可以通过在创建 Console 时指定 file 参数,让输出直接写入文件,而不是终端。

这个参数需要传入一个可写的类文件对象(file-like object)

例如:

python 复制代码
import sys
from rich.console import Console
from datetime import datetime

with open("report.txt", "wt") as report_file:
    console = Console(file=report_file)
    console.rule(f"Report Generated {datetime.now().ctime()}")

这样所有输出都会写入 report.txt,不会出现在终端。

需要注意的是:

👉 写入文件时,建议显式指定 width 参数

否则 Rich 会按照当前终端宽度进行换行。

输出捕获

有时候你可能不希望直接输出,而是捕获输出结果(例如用于日志处理或测试)。

可以使用 capture() 方法,它返回一个上下文管理器。

在代码块结束后,通过 get() 获取输出内容:

python 复制代码
from rich.console import Console

console = Console()

with console.capture() as capture:
    console.print("[bold red]Hello[/] World")

str_output = capture.get()

另一种更常见的方式(尤其是单元测试中)是使用 io.StringIO

python 复制代码
from io import StringIO
from rich.console import Console

console = Console(file=StringIO())
console.print("[bold red]Hello[/] World")

str_output = console.file.getvalue()

👉 这种方式更适合测试场景。

分页显示

当输出内容很长时,可以使用分页器(pager)来展示。

分页器通常支持:

  • 按键滚动
  • 上下浏览
  • 搜索等功能(取决于系统工具)

使用方式如下:

python 复制代码
from rich.__main__ import make_test_card
from rich.console import Console

console = Console()

with console.pager():
    console.print(make_test_card())

当退出 pager 时,内容会被发送到分页器中显示。

需要注意:

👉 大多数默认分页器不支持颜色,因此 Rich 会自动移除样式。

如果你确认分页器支持颜色,可以这样开启:

python 复制代码
console.pager(styles=True)

补充

Rich 会优先读取环境变量:

  • MANPAGER
  • PAGER

在 Linux / macOS 上,你可以设置:

bash 复制代码
less -r

来支持 ANSI 颜色输出。

备用屏幕(全屏模式)

⚠️ 注意:该功能目前仍属于实验性特性

终端通常支持一种"备用屏幕(alternate screen)"模式:

  • 类似全屏界面
  • 不会影响原有终端内容
  • 退出后恢复原状

Rich 通过 screen() 提供了这个能力(推荐用法),它返回一个上下文管理器:

python 复制代码
from time import sleep
from rich.console import Console

console = Console()

with console.screen():
    console.print(locals())
    sleep(5)

运行效果:

👉 在一个"临时全屏界面"中显示内容

👉 5 秒后自动返回终端

你还可以动态更新屏幕内容:

python 复制代码
from time import sleep
from rich.console import Console
from rich.align import Align
from rich.text import Text
from rich.panel import Panel

console = Console()

with console.screen(style="bold white on red") as screen:
    for count in range(5, 0, -1):
        text = Align.center(
            Text.from_markup(f"[blink]Don't Panic![/blink]\n{count}", justify="center"),
            vertical="middle",
        )
        screen.update(Panel(text))
        sleep(1)

这种方式可以实现:

  • 动态刷新内容
  • 自动裁剪屏幕
  • 不产生滚动

如果你需要构建更复杂的全屏界面,可以进一步了解:

👉 Live Display(Rich 的动态 UI 系统)

注意

如果程序异常退出导致终端卡在"备用屏幕模式",可以在终端输入:

bash 复制代码
reset

来恢复。

终端检测

Rich 会自动检测当前输出是否指向一个"终端"。

如果检测到不是终端(例如输出被重定向到文件),Rich 会自动移除控制字符(如颜色、样式对应的 ANSI 转义序列),只保留纯文本。

如果你希望即使写入文件也保留这些控制字符,可以在创建 Console 时设置:

python 复制代码
Console(force_terminal=True)

这种自动检测机制其实很实用:

👉 当你把输出通过管道传递给文件或其他程序时,Rich 会自动降级为纯文本输出,避免产生乱码。

交互模式

当输出目标不是终端时,Rich 默认会关闭一些"交互式效果",例如:

  • 进度条(progress bar)
  • 状态动画(spinner)

因为这些内容通常不适合写入文件。

你可以通过 force_interactive 参数手动控制这一行为:

python 复制代码
Console(force_interactive=True)   # 强制开启动画
Console(force_interactive=False)  # 强制关闭动画

环境变量

Rich 支持并遵循一些常见的环境变量,用于控制行为。

TERM

bash 复制代码
TERM=dumb 或 unknown

会禁用:

  • 颜色 / 样式
  • 依赖光标控制的功能(如进度条)

FORCE_COLOR

bash 复制代码
FORCE_COLOR=1

即使 TERM 不支持,也强制开启颜色。

NO_COLOR

bash 复制代码
NO_COLOR=1

完全禁用颜色输出(优先级高于 FORCE_COLOR)。

注意

NO_COLOR 只会移除"颜色",但不会移除:

  • bold
  • italic
  • underline
    等样式

TTY_COMPATIBLE

用于覆盖 Rich 的终端检测逻辑:

  • 1 → 强制认为是终端(支持 ANSI)
  • 0 → 强制认为不是终端
  • 未设置 → 自动检测

TTY_INTERACTIVE

用于控制是否启用"交互模式":

  • 1 → 强制开启
  • 0 → 强制关闭
  • 未设置 → 自动检测

💡 在 CI / GitHub Actions 中的建议

如果你希望在 CI 环境中使用 Rich:

bash 复制代码
TTY_COMPATIBLE=1
TTY_INTERACTIVE=0

这表示:

  • 可以输出 ANSI 样式
  • 但没有真实用户交互(因此关闭动画)

COLUMNS / LINES

如果没有在 Console 构造函数中指定:

python 复制代码
Console(width=..., height=...)

则可以通过环境变量控制终端尺寸:

bash 复制代码
COLUMNS=120
LINES=40

在 Jupyter 中对应的是:

bash 复制代码
JUPYTER_COLUMNS
JUPYTER_LINES

优先级说明:

环境变量只是提供默认值:

👉 如果你在 Console 构造函数中显式传参,这些参数会覆盖环境变量。

样式

在 Rich 的各个 API 中,你经常会看到一个 style 参数。

它用于定义文本的显示样式,包括:

  • 颜色
  • 加粗、斜体等属性

一个样式可以是:

  • 字符串(最常见)
  • Style 类的实例

定义样式

样式本质上是一个字符串,由一个或多个"关键词"组成,用来描述颜色和样式属性。

比如前景色(文字颜色)可以直接使用 256 色标准名称,例如:

python 复制代码
console.print("Hello", style="magenta")

也可以用颜色编号(0~255):

python 复制代码
console.print("Hello", style="color(5)")

使用 RGB / HEX(推荐)你还可以用类似 CSS 的方式定义颜色:

python 复制代码
console.print("Hello", style="#af00ff")
console.print("Hello", style="rgb(175,0,255)")

👉 这种方式支持 truecolor(约 1670 万种颜色)

注意

有些终端只支持 256 色,Rich 会自动选择最接近的颜色。

背景色使用 on 关键字:

python 复制代码
console.print("DANGER!", style="red on white")

默认颜色可以使用:

python 复制代码
style="default"

或者:

python 复制代码
style="default on default"

恢复终端默认颜色。

你还可以在 style 中添加以下关键词:

  • bold / b → 加粗
  • italic / i → 斜体(Windows 不支持)
  • underline / u → 下划线
  • strike / s → 删除线
  • reverse / r → 前景色和背景色反转
  • blink → 闪烁(慎用)
  • blink2 → 快速闪烁(大多终端不支持)
  • conceal → 隐藏文本(很少支持)

不太通用的样式(可能不生效)

  • underline2 / uu → 双下划线
  • frame → 边框
  • encircle → 圆圈包围
  • overline / o → 上划线

颜色和样式可以自由组合:

python 复制代码
console.print(
    "Danger, Will Robinson!",
    style="blink bold red underline on white"
)

可以通过 not 前缀关闭某些样式:

python 复制代码
console.print("foo [not bold]bar[/not bold] baz", style="bold")

效果:

  • foo → 加粗
  • bar → 普通
  • baz → 加粗

样式还可以包含 link 属性,使文本变成可点击链接(如果终端支持):

python 复制代码
console.print("Google", style="link https://google.com")

说明

如果你熟悉 HTML,可能会觉得这种写法有点奇怪。

但在终端里,"链接"本质上只是一个样式属性,就像 bold、italic 一样。

👉 Rich 的 style 本质上是一种"终端版 CSS"

  • 前景色 / 背景色 → 类似 color / background
  • 属性(bold / underline) → 类似 font-weight / text-decoration
  • link → 类似

样式类

最终,样式定义会被解析,并生成一个 Style 类的实例。

如果你愿意,也可以直接使用 Style 类来代替样式字符串定义。例如:

python 复制代码
from rich.style import Style
danger_style = Style(color="red", blink=True, bold=True)
console.print("Danger, Will Robinson!", style=danger_style)

以这种方式构造 Style 类会稍微更快一些,因为样式字符串需要解析------不过这种开销只发生在第一次调用时,因为 Rich 会缓存已解析的样式定义。

样式可以通过相加的方式进行组合,这在你想基于已有样式进行修改时非常有用。例如:

python 复制代码
from rich.console import Console
from rich.style import Style

console = Console()

base_style = Style.parse("cyan")
console.print("Hello, World", style=base_style + Style(underline=True))

你也可以使用 parse() 方法显式解析样式定义,它会接收一个样式字符串并返回一个 Style 实例。例如,下面两行是等价的:

python 复制代码
style = Style(color="magenta", bgcolor="yellow", italic=True)
style = Style.parse("italic magenta on yellow")

样式主题

如果你在多个地方重复使用样式,当你想修改某个属性或颜色时,就会变得难以维护------因为你需要逐一修改每一处使用该样式的代码。

Rich 提供了一个 Theme 类,可以用来定义自定义样式,并通过名称引用。这样你只需要在一个地方修改样式即可。

样式主题还能让代码更具语义性,例如使用 "warning" 这样的名称,比 "italic magenta underline" 更能表达意图。

要使用样式主题,可以创建一个 Theme 实例,并传入 Console 构造函数。例如:

python 复制代码
from rich.console import Console
from rich.theme import Theme

custom_theme = Theme({
    "info": "dim cyan",
    "warning": "magenta",
    "danger": "bold red"
})

console = Console(theme=custom_theme)

console.print("This is information", style="info")
console.print("[warning]The pod bay doors are locked[/warning]")
console.print("Something terrible happened!", style="danger")

注意

样式名称必须:

  • 全部小写
  • 以字母开头
  • 只包含字母或字符 ".""-""_"
自定义默认样式

Theme 类会继承 Rich 内置的默认样式。

如果你的自定义主题中包含已有样式的名称,则会覆盖默认样式。

这意味着你可以像定义新样式一样,轻松修改默认行为。例如,下面的代码修改了 Rich 对数字的高亮方式:

python 复制代码
from rich.console import Console
from rich.theme import Theme

console = Console(theme=Theme({"repr.number": "bold green blink"}))
console.print("The total is 128")

你可以在 rich.theme.Theme 构造函数中设置 inherit=False,来禁用继承默认主题。

要查看默认主题,可以运行以下命令:

bash 复制代码
python -m rich.theme
python -m rich.default_styles
加载主题

如果你更倾向于将样式写在外部配置文件中,而不是写在 Python 代码里,也可以这样做。格式如下:

ini 复制代码
[styles]
info = dim cyan
warning = magenta
danger = bold red

你可以使用 read() 方法来读取这些配置文件。

控制台标记

Rich 支持一种简单的标记语法,你可以在几乎所有接受字符串的地方(例如 print()log())插入颜色和样式。

可以运行以下命令查看示例:

bash 复制代码
python -m rich.markup

语法

Console markup 使用一种类似 bbcode 的语法。如果你在方括号中写入样式(见 Styles),例如 [bold red],那么该样式会一直生效,直到遇到对应的关闭标签 [/bold red]

示例:

python 复制代码
from rich import print
print("[bold red]alert![/bold red] Something happened")

如果没有关闭标签,样式会持续到字符串末尾。有时候这在处理单行文本时会比较方便。例如:

python 复制代码
print("[bold italic yellow on red blink]This text is impossible to read")

关闭标签也有简写形式。如果在关闭标签中省略样式名,Rich 会关闭最近一次开启的样式。例如:

python 复制代码
print("[bold red]Bold and red[/] not bold or red")

这些标记标签可以相互组合使用,并且不需要严格嵌套。下面的示例演示了标签的交叉使用:

python 复制代码
print("[bold]Bold[italic] bold and italic [/bold]italic[/italic]")
错误

如果标记中存在以下错误,Rich 会抛出 MarkupError

  • 标签不匹配,例如:"[bold]Hello[/red]"
  • 没有匹配的关闭标签,例如:"no tags[/]"
链接

Console markup 支持通过以下语法输出超链接:

text 复制代码
[link=URL]text[/link]

示例:

python 复制代码
print("Visit my [link=https://www.willmcgugan.com]blog[/link]!")

如果你的终端支持超链接,你可以点击 "blog" 打开浏览器;如果不支持,则只会显示文本但不可点击。

转义(Escaping)

有时你可能希望输出的内容不要被当作 markup 解析。可以在标签前加反斜杠进行转义。例如:

python 复制代码
from rich import print
print(r"foo\[bar]")

输出:

text 复制代码
foo[bar]

如果没有反斜杠,Rich 会将 [bar] 当作标签处理,并在不存在该样式时移除。

注意

如果你既想输出反斜杠,又不希望它转义标签,可以使用两个反斜杠。

函数 escape() 可以帮助你自动完成转义。

在动态生成字符串(例如使用 str.format 或 f-string)时,转义非常重要,否则可能会意外注入标签。例如:

python 复制代码
def greet(name):
    console.print(f"Hello {name}!")

调用:

python 复制代码
greet("[blink]Gotcha![/blink]")

会产生闪烁文本,这可能不是你想要的。

解决方法是对输入进行转义:

python 复制代码
from rich.markup import escape

def greet(name):
    console.print(f"Hello {escape(name)}!")
表情符号

如果在 markup 中加入 emoji 代码,它会被替换为对应的 Unicode 字符。

emoji 代码由冒号包裹,例如:

python 复制代码
from rich import print
print(":warning:")

输出:

text 复制代码
⚠️

有些 emoji 有两种变体:

  • emoji(彩色)
  • text(单色)

可以通过添加 -emoji-text 指定。例如:

python 复制代码
from rich import print
print(":red_heart-emoji:")
print(":red_heart-text:")

查看所有可用 emoji:

bash 复制代码
python -m rich.emoji

渲染标记

默认情况下,当你显式传入字符串给 print(),或者在其他可渲染对象(如 Table、Panel)中嵌入字符串时,Rich 会自动解析 markup。

如果 markup 语法与文本内容冲突,你可以关闭它:

python 复制代码
print("...", markup=False)

或者在创建 Console 时关闭:

python 复制代码
Console(markup=False)
标记 API

你可以使用 from_markup() 将字符串转换为带样式的文本,它会返回一个 Text 实例,你可以继续打印或添加样式。

富文本

Rich 提供了一个 Text 类,你可以用它为字符串添加颜色和样式属性。

在所有可以接受字符串的地方,你也可以使用 Text 实例,这让你对展示效果拥有更高的控制力。

你可以把这个类理解为:

👉 一个带有"样式标记区域"的字符串。

与内置的 str 不同,Text可变对象(mutable) ,并且大多数方法都是原地修改(in-place),而不是返回新对象。

一种给 Text 添加样式的方式是使用 stylize() 方法,它可以对指定范围(起始和结束偏移)应用样式。例如:

python 复制代码
from rich.console import Console
from rich.text import Text

console = Console()
text = Text("Hello, World!")
text.stylize("bold magenta", 0, 6)
console.print(text)

这会输出 "Hello, World!",其中前 6 个字符(Hello)为加粗洋红色。

另一种方式是通过 append() 构造带样式的文本:

python 复制代码
text = Text()
text.append("Hello", style="bold magenta")
text.append(" World!")
console.print(text)

如果你已经有带 ANSI 控制码的字符串,可以使用 from_ansi() 转换为 Text 对象:

python 复制代码
text = Text.from_ansi("\033[1;35mHello\033[0m, World!")
console.print(text.spans)

ANSI控制码(ANSI Escape

Code)是一种用于控制终端显示的字符序列,通常以ESC(转义字符)开头,后面跟随特定的字符和参数。这些控制码可以用于改变文本的颜色、样式(如粗体、下划线)以及光标的位置等功能。ANSI控制码最早在20世纪70年代引入,成为终端设备的标准之一,至今仍被广泛使用。

常用的ANSI控制码

颜色设置: 通过ANSI控制码,可以设置文本的前景色和背景色。例如: \x1b[31m:设置文本颜色为红色。

\x1b[32m:设置文本颜色为绿色。 \x1b[0m:重置所有属性。 文本样式: 可以使用控制码来设置文本样式,例如:

\x1b[1m:设置为粗体。 \x1b[4m:设置为下划线。 \x1b[22m:重置为正常字体。 光标控制:

ANSI控制码还可以用于控制光标的位置,例如: \x1b[H:将光标移动到屏幕的左上角。

\x1b[10;10H:将光标移动到第10行第10列。

由于按片段构建 Text 很常见,Rich 提供了 assemble() 方法,可以组合字符串或(字符串 + 样式)对,并返回一个 Text 实例。例如:

python 复制代码
text = Text.assemble(("Hello", "bold magenta"), ", World!")
console.print(text)

这个效果与上面的 ANSI 示例等价。

你还可以通过以下方法对文本进行高亮:

  • highlight_words():高亮指定单词
  • highlight_regex():使用正则表达式高亮

文本属性

Text 类在构造时可以设置一些参数,用于控制显示方式:

  • justify

    可选 "left""center""right""full",会覆盖默认对齐方式

  • overflow

    可选 "fold""crop""ellipsis",会覆盖默认溢出处理

  • no_wrap

    如果文本长度超过宽度,禁止自动换行

  • tab_size

    设置 tab 的字符宽度

Text 实例几乎可以在 Rich API 的所有位置替代普通字符串,这让你可以精细控制文本在其他组件中的渲染方式。

例如,下面的代码会在 Panel 中将文本右对齐:

python 复制代码
from rich import print
from rich.panel import Panel
from rich.text import Text

panel = Panel(Text("Hello", justify="right"))
print(panel)

👉 Text 是"内容 + 样式"的载体

👉 PanelTable 这些是"容器"

这种分层其实已经非常接近 UI 系统的设计了(内容 vs 容器)。

高亮

Rich 会自动识别并高亮文本中的一些模式,例如:

  • 数字

  • 字符串

  • 集合类型

  • 布尔值

  • None

  • 以及一些更复杂的模式,例如:

    • 文件路径
    • URL
    • UUID

你可以通过以下方式关闭高亮:

  • print()log() 中设置 highlight=False
  • 或在 Console 构造函数中设置 highlight=False(全局关闭)

如果在构造函数中关闭了高亮,你仍然可以在 print / log 中单独启用 highlight=True

自定义高亮器

如果默认高亮不符合需求,你可以定义自己的高亮器。

最简单的方式是继承 RegexHighlighter 类,它会对匹配正则表达式的文本应用样式。

下面是一个示例:对"像邮箱地址的文本"进行高亮

python 复制代码
from rich.console import Console
from rich.highlighter import RegexHighlighter
from rich.theme import Theme

class EmailHighlighter(RegexHighlighter):
    """对类似邮箱的文本应用样式"""

    base_style = "example."
    highlights = [r"(?P<email>[\w-]+@([\w-]+\.)+[\w-]+)"]

theme = Theme({"example.email": "bold magenta"})
console = Console(highlighter=EmailHighlighter(), theme=theme)

console.print("Send funds to money@example.org")

highlights 类变量需要包含一个正则表达式列表。

匹配表达式中的"命名分组"会被 base_style 前缀修饰,并作为样式使用。

在上面的例子中:

  • 匹配到的邮箱 → 使用 example.email 样式
  • 该样式在自定义 Theme 中定义为 "bold magenta"

如果你在 Console 中设置 highlighter,那么所有输出都会应用该高亮(如果启用)。

你也可以更细粒度地使用高亮器:直接调用实例并打印返回结果。例如:

python 复制代码
console = Console(theme=theme)
highlight_emails = EmailHighlighter()
console.print(highlight_emails("Send funds to money@example.org"))

虽然 RegexHighlighter 很强大,但你也可以继承 Highlighter 基类来自定义高亮逻辑。

它包含一个 highlight 方法,接收需要高亮的 Text 对象。

下面是一个简单但"有点无聊"的例子:给每个字符随机上色

python 复制代码
from random import randint

from rich import print
from rich.highlighter import Highlighter


class RainbowHighlighter(Highlighter):
    def highlight(self, text):
        for index in range(len(text)):
            text.stylize(f"color({randint(16, 255)})", index, index + 1)


rainbow = RainbowHighlighter()
print(rainbow("I must not fear. Fear is the mind-killer."))

内置高亮器

Rich 提供了以下内置高亮器:

  • ISO8601Highlighter:高亮 ISO8601 日期时间字符串
  • JSONHighlighter:高亮 JSON 格式字符串

美化打印

Rich 除了语法高亮之外,还会对容器类型进行格式化(即 pretty print),例如:

  • list(列表)
  • dict(字典)
  • set(集合)

可以运行以下命令查看美化输出示例:

bash 复制代码
python -m rich.pretty

注意:输出会根据终端宽度自动调整布局。

pprint 方法(pprint method)

pprint() 方法提供了一些额外参数,用于调整对象的美化输出方式。导入方式如下:

python 复制代码
from rich.pretty import pprint
pprint(locals())
缩进指引线(Indent guides)

Rich 可以绘制缩进指引线,用来强调数据结构的层级关系,这对于深层嵌套结构特别有帮助。

pprint 方法默认启用缩进指引线,你可以通过设置 indent_guides=False 来关闭。

全部展开(Expand all)

Rich 通常会比较保守,只在一行内尽量多地展示内容。

如果你希望完全展开数据结构,可以设置 expand_all=True。例如:

python 复制代码
pprint(["eggs", "ham"], expand_all=True)
截断美化输出(Truncating pretty output)

非常大的数据结构会影响可读性,甚至导致你在终端中翻很多页才能找到目标数据。

Rich 支持对容器和长字符串进行截断,以提供概览信息而不会淹没终端输出。

如果设置 max_length 为整数:

  • 容器超过该长度会被截断
  • 会显示 ... 以及未显示的元素数量

示例:

python 复制代码
pprint(locals(), max_length=2)

如果设置 max_string 为整数:

  • 字符串超过该长度会被截断
  • 会显示未显示字符数量

示例:

python 复制代码
pprint("Where there is a Will, there is a Way", max_string=21)

可渲染的 Pretty

Rich 提供了 Pretty 类,可以将美化后的数据嵌入到其他渲染组件中。

例如,在 Panel 中显示美化数据:

python 复制代码
from rich import print
from rich.pretty import Pretty
from rich.panel import Panel

pretty = Pretty(locals())
panel = Panel(pretty)
print(panel)

Pretty 提供了大量参数用于调整格式化效果,详情可参考 Pretty 的完整文档。

Rich Repr 协议(Rich Repr Protocol)

Rich 能对任何输出进行语法高亮,但格式化能力主要限制在以下类型:

  • 内置容器(list / dict / set 等)
  • dataclass
  • 以及 Rich 已知的对象(例如 attrs 库生成的对象)

如果你想让自定义对象也支持 Rich 的格式化,可以实现 Rich repr 协议

可以运行以下命令查看示例:

bash 复制代码
python -m rich.repr

先看一个普通类,它可能会从 Rich repr 中获益:

python 复制代码
class Bird:
    def __init__(self, name, eats=None, fly=True, extinct=False):
        self.name = name
        self.eats = list(eats) if eats else []
        self.fly = fly
        self.extinct = extinct

    def __repr__(self):
        return f"Bird({self.name!r}, eats={self.eats!r}, fly={self.fly!r}, extinct={self.extinct!r})"
python 复制代码
BIRDS = {
    "gull": Bird("gull", eats=["fish", "chips", "ice cream", "sausage rolls"]),
    "penguin": Bird("penguin", eats=["fish"], fly=False),
    "dodo": Bird("dodo", eats=["fruit"], fly=False, extinct=True)
}
print(BIRDS)

输出结果类似:

text 复制代码
{'gull': Bird(...), 'penguin': Bird(...), 'dodo': Bird(...)}

这个输出的问题是:

  • 太长,会换行
  • repr 信息冗余(包含默认参数)
  • 可读性较差

如果用 Rich 打印,会稍微好一些:

text 复制代码
{
    'gull': Bird(...),
    'penguin': Bird(...),
    'dodo': Bird(...)
}

虽然 Rich 能格式化 dict,但:

  • repr 仍然冗长
  • 输出仍然可能换行
  • 默认参数仍然显示

我们可以为类添加 __rich_repr__ 方法:

python 复制代码
def __rich_repr__(self):
    yield self.name
    yield "eats", self.eats
    yield "fly", self.fly, True
    yield "extinct", self.extinct, False

再次用 Rich 打印:

text 复制代码
{
    'gull': Bird(
        'gull',
        eats=['fish', 'chips', 'ice cream', 'sausage rolls']
    ),
    'penguin': Bird('penguin', eats=['fish'], fly=False),
    'dodo': Bird('dodo', eats=['fruit'], fly=False, extinct=True)
}

改进点:

  • 默认值被省略
  • 输出更紧凑
  • 即使嵌套结构也仍然可读
python 复制代码
{
    'gull': Bird(
        'gull',
        eats=[
            'fish',
            'chips',
            'ice cream',
            'sausage rolls'
        ]
    ),
    'penguin': Bird(
        'penguin',
        eats=['fish'],
        fly=False
    ),
    'dodo': Bird(
        'dodo',
        eats=['fruit'],
        fly=False,
        extinct=True
    )
}

你可以在任何类中添加 __rich_repr__ 方法来启用 Rich 格式化。

这个方法应该返回一个可迭代对象(iterable),通常用 yield 实现会更方便(生成器形式)。

每个 yield 表示一个输出字段:

  • yield value → 生成位置参数
  • yield name, value → 生成关键字参数
  • yield name, value, default → 如果 value != default 才输出

如果使用 None 作为 name,那么它会被当作位置参数处理,用于支持 tuple 类型的参数。

你也可以让 Rich 生成"尖括号风格"的 repr,这种风格通常用于无法轻易重建构造函数的对象。

做法是:

python 复制代码
__rich_repr__.angular = True

这样输出会变成:

text 复制代码
{
    'gull': <Bird 'gull' eats=['fish', 'chips', 'ice cream', 'sausage rolls']>,
    'penguin': <Bird 'penguin' eats=['fish'] fly=False>,
    'dodo': <Bird 'dodo' eats=['fruit'] fly=False extinct=True>
}

注意,你可以在第三方库中添加 __rich_repr__,而不需要将 Rich 作为依赖。

如果没有安装 Rich,程序不会受到任何影响。

(💡 注:这个设计其实很工程化,本质是"可选增强协议",不会侵入原对象逻辑)

类型支持(Typing)

如果你想为 rich repr 添加类型标注,可以导入 rich.repr.Result

python 复制代码
import rich.repr


class Bird:
    def __init__(self, name, eats=None, fly=True, extinct=False):
        self.name = name
        self.eats = list(eats) if eats else []
        self.fly = fly
        self.extinct = extinct

    def __rich_repr__(self) -> rich.repr.Result:
        yield self.name
        yield "eats", self.eats
        yield "fly", self.fly, True
        yield "extinct", self.extinct, False

自动 Rich Repr(Automatic Rich Repr)

如果参数名与属性名一致,Rich 可以自动生成 repr。

使用 auto 装饰器:

python 复制代码
import rich.repr

@rich.repr.auto
class Bird:
    def __init__(self, name, eats=None, fly=True, extinct=False):
        self.name = name
        self.eats = list(eats) if eats else []
        self.fly = fly
        self.extinct = extinct
python 复制代码
BIRDS = {
    "gull": Bird("gull", eats=["fish", "chips", "ice cream", "sausage rolls"]),
    "penguin": Bird("penguin", eats=["fish"], fly=False),
    "dodo": Bird("dodo", eats=["fruit"], fly=False, extinct=True)
}

from rich import print
print(BIRDS)

需要注意的是:

  • 这个装饰器会自动生成 __repr__
  • 即使不使用 Rich 输出,也会影响默认 repr

如果你想生成"尖括号风格 repr",可以这样写:

python 复制代码
@rich.repr.auto(angular=True)
class Bird:
    def __init__(self, name, eats=None, fly=True, extinct=False):
        self.name = name
        self.eats = list(eats) if eats else []
        self.fly = fly
        self.extinct = False

日志处理器(Logging Handler)

Rich 提供了一个 logging handler(日志处理器),可以对 Python logging 模块输出的文本进行格式化与彩色高亮

下面是一个基础的 Rich logger 配置示例:

python 复制代码
import logging
from rich.logging import RichHandler

FORMAT = "%(message)s"
logging.basicConfig(
    level="NOTSET",
    format=FORMAT,
    datefmt="[%X]",
    handlers=[RichHandler()]
)

log = logging.getLogger("rich")
log.info("Hello, World!")

这里的核心点是:

  • RichHandler() 接管 logging 输出
  • 终端日志会自动变成"带颜色 + 更可读"的格式
  • 本质是"把 logging 输出重新渲染成 Rich renderable"

默认情况下,Rich 的 logging 不会解析 Console Markup

原因是:

大多数 logging 库不会自动转义 [],直接开启 markup 容易引发冲突或注入问题。

如果你确实需要,可以在 handler 上启用:

python 复制代码
RichHandler(markup=True)

或者在单条日志中启用:

python 复制代码
log.error(
    "[bold red blink]Server is shutting down![/]",
    extra={"markup": True}
)

你也可以关闭单条日志的高亮:

python 复制代码
log.error("123 will not be highlighted", extra={"highlighter": None})

异常处理(Handle exceptions)

Rich 的 RichHandler 可以结合 Rich 的 Traceback 来美化异常输出,比 Python 默认 traceback 更详细。

启用方式:

python 复制代码
import logging
from rich.logging import RichHandler

logging.basicConfig(
    level="NOTSET",
    format="%(message)s",
    datefmt="[%X]",
    handlers=[RichHandler(rich_tracebacks=True)]
)

log = logging.getLogger("rich")

try:
    print(1 / 0)
except Exception:
    log.exception("unable print!")

RichHandler 还有很多可配置项,可以参考官方 reference。

抑制框架栈(Suppressing Frames)

如果你在使用框架(例如 click、django 等),通常你只关心:

"你自己写的代码出了什么问题"

而不想看到框架内部调用栈。

可以通过 tracebacks_suppress 参数屏蔽框架代码:

python 复制代码
import click
import logging
from rich.logging import RichHandler

logging.basicConfig(
    level="NOTSET",
    format="%(message)s",
    datefmt="[%X]",
    handlers=[
        RichHandler(
            rich_tracebacks=True,
            tracebacks_suppress=[click]
        )
    ]
)

被 suppress 的 frame 会变成:

  • 只显示文件名
  • 只显示行号
  • 不显示源码内容

异常回溯(Traceback)

Rich 可以对 Python traceback(异常回溯)进行语法高亮与结构化格式化

相比标准 Python traceback,它的特点是:

  • 更易读
  • 展示更多上下文代码
  • 层级结构更清晰

运行以下命令可以看到 Rich traceback 示例:

bash 复制代码
python -m rich.traceback

打印异常(Printing tracebacks)

print_exception() 方法可以打印当前正在处理的异常。

python 复制代码
from rich.console import Console
console = Console()

try:
    do_something()
except Exception:
    console.print_exception(show_locals=True)
  • show_locals=True
    会在每一层 stack frame 中显示局部变量的值

💡(经验提示):这个功能在 debug 复杂状态流时非常有用,尤其是多层函数调用 + 状态依赖场景。

Traceback Handler(全局异常处理器)

Rich 可以安装为默认异常处理器,让所有未捕获异常都自动美化输出:

python 复制代码
from rich.traceback import install
install(show_locals=True)

install() 支持多个参数配置 traceback 行为(详见官方 install 文档)。

自动安装 Traceback(Automatic Traceback Handler)

在某些情况下,你可能希望:

不修改业务代码,也能自动启用 Rich traceback

可以通过 sitecustomize.py 实现。

文件路径示例:

复制代码
./.venv/lib/python3.9/site-packages/sitecustomize.py

如果文件不存在,可以创建:

bash 复制代码
touch .venv/lib/python3.9/site-packages/sitecustomize.py

然后写入:

python 复制代码
from rich.traceback import install
install(show_locals=True)

一旦配置完成:

  • 虚拟环境内所有 Python 程序
  • 都会自动使用 Rich traceback

💡这个机制本质是利用 Python import hook,在解释器启动阶段"注入调试能力",适合内部环境,但不太建议在对外发布的项目里默认开启。

注意事项,如果是对外项目:

建议在 main entry point 中显式调用 install()

而不是依赖 sitecustomize。

抑制框架栈(Suppressing Frames)

在使用框架(click / django 等)时,通常只关心:

自己业务代码的 traceback

可以过滤框架代码:

python 复制代码
import click
from rich.traceback import install

install(suppress=[click])

也可以用于:

  • Traceback
  • install
  • Console.print_exception
  • RichHandler

被 suppress 的 frame 会变成:

  • 只显示文件名
  • 只显示行号
  • 不显示代码内容

最大帧数(Max Frames)

递归错误可能产生非常大的 traceback:

  • 渲染慢
  • 重复帧很多
  • 可读性差

Rich 默认限制:

复制代码
max_frames = 100

行为:

  • 前 50 帧
  • 后 50 帧

可以关闭限制:

python 复制代码
console.print_exception(max_frames=0)

或设置限制数量:

python 复制代码
console.print_exception(max_frames=20)

示例:递归错误

python 复制代码
from rich.console import Console

def foo(n):
    return bar(n)

def bar(n):
    return foo(n)

console = Console()

try:
    foo(1)
except Exception:
    console.print_exception(max_frames=20)

提示输入(Prompt)

Rich 提供了一组 Prompt 类,用于向用户请求输入,并会循环询问直到得到有效输入为止

这些 Prompt 内部都基于 Console API 实现。

python 复制代码
from rich.prompt import Prompt

name = Prompt.ask("Enter your name")

提示内容可以是:

  • 普通字符串(支持 Console Markup 和 emoji)
  • Text 对象

💡Prompt 本质就是一个"带验证循环的输入封装器",比 input() 多了规则控制能力。

如果用户直接回车不输入内容,可以返回默认值:

python 复制代码
from rich.prompt import Prompt

name = Prompt.ask("Enter your name", default="Paul Atreides")

选项限制(choices):

如果提供 choices,用户必须输入其中之一,否则会反复提示:

python 复制代码
from rich.prompt import Prompt

name = Prompt.ask(
    "Enter your name",
    choices=["Paul", "Jessica", "Duncan"],
    default="Paul"
)

大小写控制(case sensitivity):

默认是区分大小写的:

python 复制代码
from rich.prompt import Prompt

name = Prompt.ask(
    "Enter your name",
    choices=["Paul", "Jessica", "Duncan"],
    default="Paul",
    case_sensitive=False
)

这样:

  • Paul
  • paul

都会被接受。

Rich 还提供了类型化输入:

整数输入:

python 复制代码
from rich.prompt import IntPrompt

value = IntPrompt.ask("Enter a number")

浮点数输入:

python 复制代码
from rich.prompt import FloatPrompt

value = FloatPrompt.ask("Enter a float")

确认类(Confirm):

Confirm 用于简单的 yes / no 问题:

python 复制代码
from rich.prompt import Confirm

is_rich_great = Confirm.ask("Do you like rich?")
assert is_rich_great

Confirm 本质是:

"带默认解析规则的二值 Prompt"

通常会解析:

  • y / yes → True
  • n / no → False

Prompt 类是可继承的,设计目标就是可定制:

  • 自定义输入规则
  • 自定义校验逻辑
  • 自定义提示格式

相关示例可以查看 prompt.py

运行:

bash 复制代码
python -m rich.prompt

可以看到所有 Prompt 的实际交互效果。

列布局(Columns)

Rich 可以使用 Columns 类,将文本或其他 Rich 可渲染对象以整齐的"多列布局"方式展示。

使用方法是:

  1. 构造 Columns 对象
  2. 传入一个可迭代的 renderables(可渲染对象)
  3. 打印到 Console

下面这个例子模拟了 Linux / macOS 中的 ls 命令,用于列出目录内容:

python 复制代码
import os
import sys

from rich import print
from rich.columns import Columns

if len(sys.argv) < 2:
    print("Usage: python columns.py DIRECTORY")
else:
    directory = os.listdir(sys.argv[1])
    columns = Columns(directory, equal=True, expand=True)
    print(columns)
  • equal=True

    → 每一列宽度尽量一致(强制对齐视觉)

  • expand=True

    → 尽可能占满终端宽度(类似"自适应布局")

Columns 本质就是一个:

"终端版 flex/grid 布局器"

它做的事情不是排序数据,而是把线性数据重新排版成视觉网格

Rich 的 Columns 不仅可以显示字符串,还可以显示:

  • Table
  • Panel
  • Syntax 高亮代码
  • 自定义 renderable 对象

📌 官方也提供了一个更复杂的示例:

复制代码
columns.py

用于展示"多类型 renderable 混排列布局"。

渲染组(Render Groups)

Rich 提供了 Group 类,用于将多个 renderable(可渲染对象)组合在一起,使它们可以在"只接受单个 renderable 的上下文"中一起渲染。

有些组件(例如 Panel)一次只能接收一个 renderable,但你又想放多个内容,这时候就需要 Group。

示例:在 Panel 中嵌套多个 Panel

python 复制代码
from rich import print
from rich.console import Group
from rich.panel import Panel

panel_group = Group(
    Panel("Hello", style="on blue"),
    Panel("World", style="on red"),
)

print(Panel(panel_group))

这个结构的含义可以理解为:

Group = "把多个 UI 块打包成一个整体"

然后再交给外层 Panel 渲染。

这其实就是一个"渲染层级容器":

  • Panel 负责"边框 UI"
  • Group 负责"子组件组合"
  • Rich renderer 负责递归渲染树结构

如果 renderables 是动态生成的:

  • 手动写 Group 会变得很麻烦
  • 尤其是数量较多或来源是循环时

Rich 提供了 group() 装饰器,用来从迭代器构建 Group。

示例:用装饰器构建 Group

python 复制代码
from rich import print
from rich.console import group
from rich.panel import Panel

@group()
def get_panels():
    yield Panel("Hello", style="on blue")
    yield Panel("World", style="on red")

print(Panel(get_panels()))

本质区别(理解重点):

手动 Group:

  • 适合"静态 UI"
  • 结构固定
  • 适合 demo 或布局简单场景

group() 装饰器:

  • 适合"动态 UI"
  • 支持循环生成
  • 更像 UI pipeline(流式构建)

Markdown 渲染(Markdown)

Rich 可以将 Markdown 渲染到终端中显示。

使用方式是:

  1. 构造 Markdown 对象
  2. 交给 Console 打印

Markdown 是一种非常适合在命令行程序中展示结构化内容的方式。

示例用法:

python 复制代码
MARKDOWN = """
# This is an h1

Rich can do a pretty *decent* job of rendering markdown.

1. This is a list item
2. This is another list item
"""

from rich.console import Console
from rich.markdown import Markdown

console = Console()
md = Markdown(MARKDOWN)
console.print(md)

特性说明:

✔ 支持标题

例如:

md 复制代码
# H1
## H2

✔ 支持列表

md 复制代码
1. item one
2. item two

✔ 支持强调

md 复制代码
*italic*  
**bold**

✔ 支持代码块(重点)

Rich 的一个关键能力是:

Markdown 里的代码块会自动进行语法高亮

这在 CLI 工具里非常实用。

你也可以直接在命令行渲染 Markdown 文件:

bash 复制代码
python -m rich.markdown README.md

查看帮助信息:

bash 复制代码
python -m rich.markdown -h

Rich 的 Markdown 支持本质是:

把"文档系统"直接搬进终端 UI

它让 CLI 程序可以天然具备:

  • 文档展示能力
  • 结构化排版能力
  • 代码高亮能力

属于 CLI 体验升级的核心模块之一。

内边距(Padding)

Rich 提供了 Padding 类,用于在文本或其他 renderable(可渲染对象)周围添加空白区域。

下面这个例子会在 "Hello" 周围添加 1 个字符的 padding:

  • 上下各一行空白
  • 左右各一个空格
python 复制代码
from rich import print
from rich.padding import Padding

test = Padding("Hello", 1)
print(test)

Rich 支持用 tuple 精细控制 padding,规则类似 CSS 的 margin / padding 写法。

2 个值的情况:

python 复制代码
Padding("Hello", (2, 4))

含义:

  • 2 → 上 / 下
  • 4 → 左 / 右

效果:

  • 上下各 2 行空白
  • 左右各 4 个空格

4 个值的情况:

python 复制代码
(top, right, bottom, left)

即分别控制四个方向。

这个设计基本就是:

"把 UI spacing 抽象成 CSS box model"

Rich 的 Padding 还支持 style 参数,用来控制背景色或整体样式。

示例:带背景色的 padding

python 复制代码
from rich import print
from rich.padding import Padding

test = Padding("Hello", (2, 4), style="on blue", expand=False)
print(test)
  • expand=True(默认行为之一)

    → padding 会扩展到终端全宽

  • expand=False

    → 不强制填满终端宽度

这个参数本质就是:

控制"这个 UI block 是流式布局还是固定内容块"

Rich 的 Padding 可以用在任何 renderable 上,例如:

  • Table 行高调整
  • Panel 内边距控制
  • 强调某一条日志或数据

示例思路:

在 Table 中强调某一行:

text 复制代码
Padding + background color = "高亮行效果"

面板(Panel)

Rich 可以使用 Panel 来为文本或其他 renderable 绘制一个边框容器

基本用法,将 renderable 作为第一个参数传入 Panel

python 复制代码
from rich import print
from rich.panel import Panel

print(Panel("Hello, [red]World!"))

Rich 允许你通过 box 参数改变边框样式。

不同的 box 样式可以参考 Box 定义(例如不同风格的线框、双线框等)。

默认情况下:

Panel 会自动扩展到终端的全宽

也就是说它是"block-level UI"。

如果你不想让 Panel 撑满整行,可以:

方法 1:expand=False

python 复制代码
from rich import print
from rich.panel import Panel

print(Panel("Hello, [red]World!", expand=False))

方法 2:fit()

python 复制代码
from rich import print
from rich.panel import Panel

print(Panel.fit("Hello, [red]World!"))

Rich 的 Panel 支持:

  • title(顶部标题)
  • subtitle(底部副标题)

示例:

python 复制代码
from rich import print
from rich.panel import Panel

print(Panel(
    "Hello, [red]World!",
    title="Welcome",
    subtitle="Thank you"
))

Rich 的 Panel 本质是:

一个"终端 UI 的卡片容器(Card Component)"

它对应到前端概念就是:

  • Card / Container
  • 带 border + title + layout control
  • 可嵌套 renderable

如果你把 Rich 当成"终端 UI 框架",Panel 就是最基础的 UI 盒子组件之一。

明白,这次问题不在"翻译",而在输出形态被我拆成了"讲解稿"而不是"文档翻译"

你要的是:

✔ 结构完整

✔ 段落连续

✔ 不被拆碎

✔ 允许少量注释,但不能破坏原文流

进度显示(Progress Display)

Rich 可以用于展示长时间运行任务(如文件复制、下载等)的实时进度信息。显示内容是可配置的,默认包括任务描述、进度条、完成百分比以及预计剩余时间。

Rich 的进度系统支持多个任务同时显示,每个任务都有自己的进度条与状态信息。你可以用它来追踪线程或进程中的并发任务。

如果想查看效果,可以在命令行运行:

bash 复制代码
python -m rich.progress

注意:Progress 在 Jupyter Notebook 中也可以使用,但不会自动刷新,需要手动调用 refresh(),或者在 update() 时设置 refresh=True。也可以使用 track(),它会在每次循环中自动刷新。

基本用法

最简单的使用方式是调用 track() 函数,它接受一个序列(如 list 或 range),以及一个可选的任务描述。track() 会在遍历序列时自动更新进度。

python 复制代码
import time
from rich.progress import track

for i in track(range(20), description="Processing..."):
    time.sleep(1)

高级用法

如果你需要多个任务,或者想自定义进度条的列结构,可以直接使用 Progress 类。

创建 Progress 对象后,通过 add_task() 添加任务,通过 update() 更新进度。

Progress 通常作为上下文管理器使用,会自动开始和结束进度显示。

python 复制代码
import time
from rich.progress import Progress

with Progress() as progress:

    task1 = progress.add_task("[red]Downloading...", total=1000)
    task2 = progress.add_task("[green]Processing...", total=1000)
    task3 = progress.add_task("[cyan]Cooking...", total=1000)

    while not progress.finished:
        progress.update(task1, advance=0.5)
        progress.update(task2, advance=0.3)
        progress.update(task3, advance=0.9)
        time.sleep(0.02)

这里 total 表示完成 100% 所需的总步数。这个"步数"是业务定义的,比如文件字节数、图片数量或处理次数。

启动与停止

推荐使用上下文管理器。如果不使用,则必须手动调用 start()stop()

python 复制代码
import time
from rich.progress import Progress

progress = Progress()
progress.start()

try:
    task1 = progress.add_task("[red]Downloading...", total=1000)
    task2 = progress.add_task("[green]Processing...", total=1000)
    task3 = progress.add_task("[cyan]Cooking...", total=1000)

    while not progress.finished:
        progress.update(task1, advance=0.5)
        progress.update(task2, advance=0.3)
        progress.update(task3, advance=0.9)
        time.sleep(0.02)

finally:
    progress.stop()

这里必须使用 try/finally,确保即使异常发生也能正确关闭进度显示。

更新任务

add_task() 会返回一个 Task ID,用于后续更新。

更新任务时通常有两种方式:

  • completed:直接设置完成进度
  • advance:在当前基础上增加进度

此外,update() 还支持传入额外字段,这些字段会存入 task.fields,可用于自定义显示列。

隐藏任务

任务默认是可见的,也可以通过 visible=False 创建隐藏任务,或动态控制可见性。

临时进度(Transient)

默认情况下,退出进度上下文后,最后一帧会保留在终端中。

如果设置 transient=True,退出时进度条会自动清除:

python 复制代码
with Progress(transient=True) as progress:
    task = progress.add_task("Working", total=100)
    do_work(task)

这种模式适合需要"干净输出"的 CLI 工具。

不确定进度(Indeterminate)

如果任务开始时无法确定总量(例如等待服务器响应或扫描文件数量),可以使用不确定模式:

  • start=False
  • total=None

此时会显示一个"呼吸式动画",表示任务正在进行但进度未知。

当确定步数后,可以调用 start_task(),再进入正常进度更新流程。

自动刷新(Auto refresh)

Rich 的进度系统默认会以 每秒 10 次 的频率刷新进度信息。

你可以通过 Progress 构造函数中的 refresh_per_second 参数来调整刷新频率。

如果你知道更新不会那么频繁,建议把这个值调低,以减少性能开销。

如果你的任务更新非常不频繁,也可以完全关闭自动刷新:

python 复制代码
auto_refresh=False

关闭后,你需要在每次更新任务后手动调用 refresh() 来刷新界面。

展开模式(Expand)

默认情况下,进度条只会占用显示任务信息所需的最小宽度。

如果设置:

python 复制代码
expand=True

Rich 会将整个进度显示扩展到终端的全宽。

列(Columns)

进度显示的结构可以通过 Progress 构造函数的位置参数来自定义,这些参数被称为"列"。

列可以是:

  • 格式字符串(format string)
  • ProgressColumn 对象

格式字符串中可以访问一个 task 对象,例如:

  • {task.description} → 任务描述
  • {task.completed} of {task.total} → 已完成 / 总数

通过 update() 传入的额外字段会存储在:

task.fields

访问方式:

text 复制代码
{task.fields[extra]}

Rich 的默认 Progress 等价于:

python 复制代码
progress = Progress(
    TextColumn("[progress.description]{task.description}"),
    BarColumn(),
    TaskProgressColumn(),
    TimeRemainingColumn(),
)

如果你想在默认列基础上增加内容,可以使用:

python 复制代码
Progress.get_default_columns()

示例:

python 复制代码
progress = Progress(
    SpinnerColumn(),
    *Progress.get_default_columns(),
    TimeElapsedColumn(),
)

Rich 提供的内置列包括:

  • BarColumn → 进度条
  • TextColumn → 文本信息
  • TimeElapsedColumn → 已用时间
  • TimeRemainingColumn → 预计剩余时间
  • MofNCompleteColumn → "已完成/总数"
  • FileSizeColumn → 文件大小进度
  • TotalFileSizeColumn → 总文件大小
  • DownloadColumn → 下载进度
  • TransferSpeedColumn → 传输速度
  • SpinnerColumn → 动画 spinner
  • RenderableColumn → 自定义 renderable
自定义表格列宽(Table Columns)

Rich 在内部会用 Table 来渲染任务列表。

你可以通过 Column(ratio=...) 控制各列占比。

示例:

python 复制代码
from time import sleep

from rich.table import Column
from rich.progress import Progress, BarColumn, TextColumn

text_column = TextColumn("{task.description}", table_column=Column(ratio=1))
bar_column = BarColumn(bar_width=None, table_column=Column(ratio=2))

progress = Progress(text_column, bar_column, expand=True)

with progress:
    for n in progress.track(range(100)):
        sleep(0.1)
打印 / 日志输出(Print / Log)

Rich 的 Progress 内部会创建一个 Console 对象:

python 复制代码
progress.console

你可以直接在进度运行时打印内容,它会显示在进度条上方:

python 复制代码
with Progress() as progress:
    task = progress.add_task("working", total=10)
    for job in range(10):
        progress.console.print(f"Working on job #{job}")
        progress.advance(task)

也可以传入你自己的 Console:

python 复制代码
with Progress(console=my_console) as progress:
    my_console.print("Starting work")
stdout / stderr 重定向

Rich 默认会重定向:

  • stdout
  • stderr

这样可以避免 print() 破坏进度条布局。

可以关闭:

python 复制代码
redirect_stdout=False
redirect_stderr=False
自定义进度显示(Customization)

如果默认 Progress 不满足需求,可以重写:

python 复制代码
get_renderables()

示例:

python 复制代码
from rich.panel import Panel
from rich.progress import Progress

class MyProgress(Progress):
    def get_renderables(self):
        yield Panel(self.make_tasks_table(self.tasks))
文件读取进度

Rich 支持直接对文件读取加进度条。

读取文件(open)

python 复制代码
import json
import rich.progress

with rich.progress.open("data.json", "rb") as file:
    data = json.load(file)

包装已有文件对象(wrap_file)

python 复制代码
from urllib.request import urlopen
from rich.progress import wrap_file

response = urlopen("https://www.textualize.io")
size = int(response.headers["Content-Length"])

with wrap_file(response, size) as file:
    for line in file:
        print(line.decode())

Rich 的 Progress 本质是:

一个"可插拔列系统 + 实时刷新渲染器"的终端任务 UI 框架

核心能力:

  • 多任务并发展示
  • 可扩展 UI 列
  • 文件 / 流进度包装
  • stdout/stderr 不干扰 UI
  • 高度可定制布局

嵌套进度条(Nesting Progress Bars)

Rich 支持在一个进度任务的上下文中再创建新的进度条。

当你在已有进度条(通过 track()Progress 上下文)内部再创建一个进度条时,子进度条会被显示在主进度条下方。

嵌套示例:

python 复制代码
from rich.progress import track
from time import sleep

for count in track(range(10)):
    for letter in track("ABCDEF", transient=True):
        print(f"Stage {count}{letter}")
        sleep(0.1)
    sleep(0.1)

行为说明:

  • 外层 track(range(10)) 是主进度条
  • 内层 track("ABCDEF") 会生成子进度条
  • 子进度条显示在主进度条下方

嵌套进度条的刷新频率:

由外层进度条的 refresh_per_second 控制

也就是说:

  • 外层 = 调度器
  • 内层 = 被调度 UI

多 Progress 实例(Multiple Progress)

Rich 不支持在单个 Progress 实例中为不同任务设置不同列布局。

但你可以:

同时创建多个 Progress 实例

并将它们放入 Live Display 中。

相关示例:

  • live_progress.py
  • dynamic_progress.py

语法高亮(Syntax)

Rich 支持对多种编程语言进行语法高亮显示,并且可以显示行号。

要进行语法高亮,需要创建一个 Syntax 对象,然后打印到控制台:

python 复制代码
from rich.console import Console
from rich.syntax import Syntax

console = Console()

with open("syntax.py", "rt") as code_file:
    syntax = Syntax(code_file.read(), "python")

console.print(syntax)

Rich 提供了更方便的构造方式 from_path(),可以直接从磁盘读取文件并自动识别类型。

上面的例子可以写成:

python 复制代码
from rich.console import Console
from rich.syntax import Syntax

console = Console()

syntax = Syntax.from_path("syntax.py")
console.print(syntax)

如果设置:

python 复制代码
line_numbers=True

Rich 会在左侧显示行号列。

Syntax 构造函数(以及 from_path())支持 theme 参数。

它接受:

  • Pygments 主题名称

  • 或特殊主题:

    • ansi_dark
    • ansi_light

这些会使用终端默认的颜色方案。

你可以覆盖主题背景色:

python 复制代码
background_color="red"
background_color="#ff0000"
background_color="rgb(255,0,0)"

也可以设置:

text 复制代码
"default"

表示使用终端默认背景色。

Rich 也支持直接在命令行使用:

bash 复制代码
python -m rich.syntax syntax.py

查看完整参数:

bash 复制代码
python -m rich.syntax -h

Rich 的 Syntax 模块本质是:

一个"终端代码渲染器(syntax highlighting renderer)"

能力包括:

  • 多语言高亮(基于 Pygments)
  • 行号支持
  • 主题系统
  • 背景色控制
  • CLI 直接使用

表格(Tables)

Rich 的 Table 类提供了多种方式将表格数据渲染到终端。

要渲染表格,需要先创建 Table 对象,通过 add_column() 添加列,再通过 add_row() 添加行,最后打印到控制台。

示例:

python 复制代码
from rich.console import Console
from rich.table import Table

table = Table(title="Star Wars Movies")

table.add_column("Released", justify="right", style="cyan", no_wrap=True)
table.add_column("Title", style="magenta")
table.add_column("Box Office", justify="right", style="green")

table.add_row("Dec 20, 2019", "Star Wars: The Rise of Skywalker", "$952,110,690")
table.add_row("May 25, 2018", "Solo: A Star Wars Story", "$393,151,347")
table.add_row("Dec 15, 2017", "Star Wars Ep. V111: The Last Jedi", "$1,332,539,889")
table.add_row("Dec 16, 2016", "Rogue One: A Star Wars Story", "$1,332,439,889")

console = Console()
console.print(table)

输出效果如下:

Rich 会自动计算每一列的最优宽度,以适配内容;如果终端宽度不够,会自动换行。

表格特性(Table Options)

Table 构造函数支持很多参数,用来控制整体样式:

  • title:表格标题(顶部)
  • caption:表格注释(底部)
  • width:固定宽度(关闭自动计算)
  • min_width:最小宽度
  • box:边框样式(见 Box)
  • safe_box:强制 ASCII 边框(兼容旧终端)
  • padding:单元格内边距(支持 Padding 语义)
  • collapse_padding:合并相邻单元格 padding
  • pad_edge:是否保留边缘 padding
  • expand:是否撑满终端宽度
  • show_header:是否显示表头
  • show_footer:是否显示表尾
  • show_edge:是否显示外边框
  • show_lines:是否显示行间分隔线
  • leading:行与行之间额外间距
  • style:整体样式
  • row_styles:行交替样式(类似 zebra stripe)
  • header_style / footer_style
  • border_style
  • title_style / caption_style
  • title_justify / caption_justify
  • highlight:是否自动高亮内容

边框样式(Border Styles)

可以通过 Box 预设修改边框风格:

python 复制代码
from rich import box
table = Table(title="Star Wars Movies", box=box.MINIMAL_DOUBLE_HEAD)

也可以直接:

python 复制代码
box=None

👉 这时表格会变成"无边框输出",适合嵌入式 UI。

行线(Lines)

默认只有表头下有一条线。

如果想每行都有分隔线:

python 复制代码
show_lines=True

或者:

  • end_section=True:强制插入分割线
  • add_section():手动插入分割线

空表处理(Empty Tables)

没有列的表打印结果是空行。

建议动态处理:

python 复制代码
if table.columns:
    print(table)
else:
    print("[i]No data...[/i]")

添加列(Adding Columns)

可以在构造函数直接写列名:

python 复制代码
table = Table("Released", "Title", "Box Office", title="Star Wars Movies")

如果需要更细控制:

python 复制代码
from rich.table import Column, Table

table = Table(
    "Released",
    "Title",
    Column(header="Box Office", justify="right"),
    title="Star Wars Movies"
)

列选项(Column Options)

每一列也有独立配置:

  • header_style:表头样式
  • footer_style
  • style:整列样式(比如整列高亮)
  • justify:对齐方式(left/center/right/full)
  • vertical:垂直对齐(top/middle/bottom)
  • width:固定列宽
  • min_width / max_width
  • ratio:宽度比例分配(类似 flex)
  • no_wrap:禁止换行
  • highlight:自动语法高亮

垂直对齐(Vertical Alignment)

可以按列设置:

python 复制代码
vertical="middle"

也可以单独控制某个 cell:

python 复制代码
from rich.align import Align
table.add_row(Align("Title", vertical="middle"))

网格布局(Grids)

Table 不只是"表格",还能当布局系统用(类似 CLI UI layout engine)。

关闭表头和边框后可以做排版:

python 复制代码
from rich.table import Table

grid = Table.grid(expand=True)
grid.add_column()
grid.add_column(justify="right")
grid.add_row("Raising shields", "[bold magenta]COMPLETED [green]:heavy_check_mark:")
print(grid)

树形结构(Tree)

Rich 提供了一个 Tree 类,可以在终端中生成树状视图。树形结构非常适合展示文件系统目录结构,或任何层级(hierarchical)数据。

树的每个分支都可以带有标签(label),标签可以是普通文本,也可以是任意 Rich 可渲染对象(renderable)。

可以运行下面命令查看 Tree 示例:

bash 复制代码
python -m rich.tree

下面代码创建一个带简单文本标签的树并打印:

python 复制代码
from rich.tree import Tree
from rich import print

tree = Tree("Rich Tree")
print(tree)

此时只有一个节点,因此输出仅是:

复制代码
Rich Tree

真正有用的是通过 add() 添加子节点:

python 复制代码
tree.add("foo")
tree.add("bar")
print(tree)

此时树会出现两个分支,并通过"连接线"与根节点关联。

add() 返回的是一个新的 Tree 实例,因此可以继续在子节点上扩展,形成递归结构:

python 复制代码
baz_tree = tree.add("baz")
baz_tree.add("[red]Red").add("[green]Green").add("[blue]Blue")
print(tree)

树样式(Tree Styles)

Tree 构造函数和 add() 方法支持两个关键样式参数:

  • style:作用于整个分支
  • guide_style:作用于连接线(树枝线)

这些样式会继承到子树节点

例如:

  • guide_style="bold":使用更粗的 Unicode 树线
  • guide_style="underline2":使用双线风格

如果设置不同 guide_style,树的"结构线条"视觉风格会完全不同,这在终端 UI 设计中非常重要(尤其是做 CLI dashboard 时)。

更实用的例子可以参考:

bash 复制代码
tree.py

它可以生成磁盘目录结构的树形视图,用于展示文件系统层级结构。

Live 显示(Live Display)

进度条和状态指示器本质上都依赖"实时刷新终端"的能力。Rich 提供 Live 类,用于构建自定义的实时界面(live display)。

运行以下命令可以看到 Live 的演示:

bash 复制代码
python -m rich.live

📌 注意:如果看到 "..." 省略号,说明终端高度不够,内容被截断了。

基本用法(Basic Usage)

创建 Live 对象时传入一个 renderable(可渲染对象),并用上下文管理器启动:

python 复制代码
import time

from rich.live import Live
from rich.table import Table

table = Table()
table.add_column("Row ID")
table.add_column("Description")
table.add_column("Level")

with Live(table, refresh_per_second=4):  # 每秒刷新4次(视觉更流畅)
    for row in range(12):
        time.sleep(0.4)
        table.add_row(f"{row}", f"description {row}", "[red]ERROR")

👉 核心理解:

Live 的本质是一个"不断重绘 renderable 的循环刷新器",不是线程UI,而是"终端帧刷新"。

动态更新 renderable(Updating Renderable)

你也可以完全替换 renderable,而不是只修改内容:

python 复制代码
import random
import time

from rich.live import Live
from rich.table import Table


def generate_table() -> Table:
    table = Table()
    table.add_column("ID")
    table.add_column("Value")
    table.add_column("Status")

    for row in range(random.randint(2, 6)):
        value = random.random() * 100
        table.add_row(
            f"{row}",
            f"{value:3.2f}",
            "[red]ERROR" if value < 50 else "[green]SUCCESS"
        )
    return table


with Live(generate_table(), refresh_per_second=4) as live:
    for _ in range(40):
        time.sleep(0.4)
        live.update(generate_table())

全屏模式(Alternate Screen)

设置 screen=True 可以进入"终端替代屏幕":

  • Live 占满整个屏幕
  • 退出后恢复原终端

适合做 CLI 应用 / dashboard / pseudo GUI。

临时显示(Transient Display)

默认退出 Live 后,最后一帧会留在终端。

如果希望"退出即清空":

python 复制代码
Live(..., transient=True)

自动刷新(Auto Refresh)

默认刷新频率:4 FPS

可通过:

python 复制代码
refresh_per_second

调整。

如果:

python 复制代码
auto_refresh=False

则需要手动调用:

  • refresh()
  • update(..., refresh=True)

👉 实战建议:

低频更新(比如 IO/网络)建议关闭 auto refresh,避免 CPU 空转。

垂直溢出(Vertical Overflow)

当内容比终端高时:

  • "crop":直接截断
  • "ellipsis":最后一行显示 "..."(默认)
  • "visible":完整显示(但无法正确清屏)

👉 经验理解:
visible 适合调试,不适合生产 CLI UI。

print / log 输出

Live 内部有一个 Console:

python 复制代码
live.console

可以在 Live 运行时打印日志:

python 复制代码
live.console.print("Working...")

输出会显示在 Live 上方。

stdout / stderr 重定向

为了避免 print() 干扰 UI:

  • 默认会重定向 stdout / stderr
  • 可以关闭:
python 复制代码
redirect_stdout=False
redirect_stderr=False

嵌套 Live(Nesting)

可以在一个 Live 中嵌套另一个 Live:

  • 内层内容显示在外层下方
  • 早期版本会直接报错(LiveError)

更复杂案例:

  • table_movie.py
  • top_lite_simulator.py

用于展示多 Live + 动态 UI 的实际应用。

布局(Layout)

Rich 提供了 Layout 类,可以将终端屏幕划分为多个区域(分区),每个区域可以独立渲染不同内容。

它既可以和 Live Display 一起用于构建全屏 CLI 应用,也可以单独使用。

运行下面命令可以查看 Layout 示例:

bash 复制代码
python -m rich.layout

创建布局(Creating Layouts)

创建 Layout 并打印:

python 复制代码
from rich import print
from rich.layout import Layout

layout = Layout()
print(layout)

此时会看到一个"占位框",因为还没有任何内容。

布局(Layout)

Rich 提供了一个 Layout 类,可以将屏幕区域划分为多个部分,每个部分可以包含独立内容。它可以和 Live Display 一起使用来创建全屏"应用程序",也可以单独使用。

要查看 Layout 示例,可以在命令行运行:

bash 复制代码
python -m rich.layout

创建布局

要定义一个布局,先构造一个 Layout 对象并打印:

python 复制代码
from rich import print
from rich.layout import Layout

layout = Layout()
print(layout)

这会绘制一个与终端大小相同的框,并显示一些布局信息。

这个框是一个"占位符",因为我们还没有添加任何内容。

在继续之前,我们通过 split_column() 方法把布局拆分成两个子布局:

python 复制代码
layout.split_column(
    Layout(name="upper"),
    Layout(name="lower")
)
print(layout)

这会把屏幕分成上下两个等分区域。

name 属性是内部标识符,用于后续访问子布局。

接下来我们继续拆分 lower,这次使用 split_row()

python 复制代码
layout["lower"].split_row(
    Layout(name="left"),
    Layout(name="right"),
)
print(layout)

此时屏幕会被分成三个区域:

  • 上半部分 upper
  • 下半部分再分为 left / right 两个四分区

你可以继续这样调用 split(),不断拆分出更多区域。

设置渲染内容(Renderables)

Layout 的第一个位置参数可以是任意 Rich renderable,它会自动适配布局区域。

例如,我们可以把右侧拆成两个面板:

python 复制代码
from rich.panel import Panel

layout["right"].split(
    Layout(Panel("Hello")),
    Layout(Panel("World!"))
)

你也可以用 update() 替换当前内容:

python 复制代码
layout["left"].update(
    "The mystery of life isn't a problem to solve, but a reality to experience."
)
print(layout)

固定大小(Fixed size)

可以设置固定尺寸:

python 复制代码
layout["upper"].size = 10
print(layout)

这会让 upper 区域始终占 10 行,不受终端大小影响。

如果是水平布局,则 size 表示字符宽度,而不是行数。

比例布局(Ratio)

除了固定大小,还可以使用比例布局:

python 复制代码
layout["upper"].size = None
layout["upper"].ratio = 2
print(layout)

这会让 upper 占据 2/3 的空间。

原因是:

  • lower 默认 ratio = 1
  • 总比例 = 2 + 1 = 3

所以:

  • upper = 2/3
  • lower = 1/3

比例布局也可以配合最小尺寸:

python 复制代码
layout["lower"].minimum_size = 10

用于防止布局被压缩得过小。

可见性(Visibility)

可以隐藏某个区域:

python 复制代码
layout["upper"].visible = False
print(layout)

隐藏后 lower 会自动扩展填充空间。

恢复:

python 复制代码
layout["upper"].visible = True
print(layout)

结构树(Tree)

可以用 tree 属性查看布局结构:

python 复制代码
print(layout.tree)

用于调试复杂布局结构。

完整全屏应用示例:

bash 复制代码
python -m rich.layout

参考:

控制台协议

Rich 支持一种简单的"协议机制",让自定义对象也能拥有富文本渲染能力,这样你在 print()log() 时,就可以直接输出带颜色、样式和结构化信息的对象。

这种机制的价值在于:不仅用于展示,更适合做调试输出增强 ------尤其是当 __repr__ 已经不够表达结构信息时。

控制台自定义

最简单的方式是实现 __rich__ 方法。该方法不接收参数,返回一个 Rich 能渲染的对象,比如 TextTable,甚至普通字符串。

如果返回字符串,它会按 Console Markup 解析。

示例:

python 复制代码
class MyObject:
    def __rich__(self) -> str:
        return "[bold cyan]MyObject()"

如果打印 MyObject(),输出会以青色加粗 形式显示 MyObject()

控制台渲染(高级)

__rich__ 只能返回单个渲染对象,如果你需要更复杂的输出结构,就要用 __rich_console__

这个方法接收两个参数:

  • Console
  • ConsoleOptions

返回的是一个可迭代对象(通常用 yield 实现)。

示例:

python 复制代码
from dataclasses import dataclass
from rich.console import Console, ConsoleOptions, RenderResult
from rich.table import Table

@dataclass
class Student:
    id: int
    name: str
    age: int

    def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
        yield f"[b]Student:[/b] #{self.id}"
        table = Table("Attribute", "Value")
        table.add_row("name", self.name)
        table.add_row("age", str(self.age))
        yield table

如果打印 Student,会输出一段标题 + 表格结构。

底层渲染(Segment)

如果你想完全控制终端输出(比如逐字颜色控制),可以返回 Segment 对象。

Segment = 文本片段 + 可选样式。

示例:

python 复制代码
class MyObject:
    def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
        yield Segment("My", Style(color="magenta"))
        yield Segment("Object", Style(color="green"))
        yield Segment("()", Style(color="cyan"))

可视化测量(Measuring Renderables)

有时候 Rich 需要知道"一个对象渲染后占多少宽度",比如 Table 会用它来计算列宽。

如果是自定义对象,需要实现:

__rich_measure__

返回 Measurement(min, max),表示最小/最大宽度。

示例:

python 复制代码
class ChessBoard:
    def __rich_measure__(self, console: Console, options: ConsoleOptions) -> Measurement:
        return Measurement(8, options.max_width)

附录

prompt_toolkit

如果说 Rich 解决的是"终端如何显示得更好看、更结构化",那么 prompt_toolkit 解决的就是另一个更底层的问题:如何让终端交互变得像一个现代编辑器一样可控、可扩展、甚至智能化

在传统的命令行程序里,交互通常是非常朴素的------input() 读取一行文本,然后程序做出响应。这种模式简单但能力有限:没有自动补全、没有语法高亮、没有多行编辑能力,也很难处理复杂的交互逻辑。

prompt_toolkit 的出现,正是为了填补这一层能力空白。它提供了一整套构建交互式命令行应用的工具,包括:

  • 强大的输入补全机制(类似 shell / IDE)
  • 多行编辑与历史记录支持
  • 可定制的 key bindings(按键行为)
  • 语法高亮与动态提示
  • 支持异步与复杂事件循环的输入系统
  • 以及可以组合出"类 TUI 应用"的布局能力

更重要的是,它不是一个简单的"增强 input 的库",而是一个完整的终端交互框架。你可以用它做一个 REPL、一个 CLI 工具,甚至是一个轻量级的终端应用界面。

快速开始

使用 pip 安装:

bash 复制代码
pip install prompt_toolkit

如果使用 Conda:

bash 复制代码
conda install -c https://conda.anaconda.org/conda-forge prompt_toolkit

prompt_toolkit 最初的设计目标是作为 readline 的替代品,用来增强命令行输入能力。

但随着项目逐渐成熟,它的能力被不断扩展,开发者发现:它已经具备构建**完整终端应用(full screen applications)**所需的全部基础组件。因此,像 pyvim 和 pymux 这样的全屏终端应用也开始基于它构建。

从架构上看,prompt_toolkit 的核心是一个布局引擎(layout engine),它支持:

  • 水平 / 垂直分割布局
  • 浮动窗口(float)
  • 每个"窗口"都可以渲染一个自定义 UI 控件(user control)

这一套 API 表面上简单,但表达能力非常强,可以组合出复杂的终端界面。

prompt_toolkit 作为 readline 替代品使用时(例如只做简单输入),它会使用一个内置的默认布局,包括:

  • 输入框(input buffer)
  • 提示符(prompt)
  • 自动补全浮窗(autocompletion float)
  • 输入校验工具栏(默认隐藏)

而在全屏应用模式下,通常需要开发者自己设计整个布局结构。

此外,它还提供了一个非常灵活的按键绑定系统(key binding system),可以针对全屏应用进行深度定制。

下面是最基础的用法,通过 prompt() 获取用户输入,效果类似 input()

python 复制代码
from prompt_toolkit import prompt

text = prompt("Give me some input: ")
print(f"You said: {text}")

为了更好理解 prompt_toolkit,建议按以下顺序学习:

首先学习文本输出(printing text),因为它会涉及"格式化文本(formatted text)",这是后续在任何地方使用颜色和样式的基础。

然后学习输入系统(asking for input),这是最核心的部分,几乎所有用法都会涉及,包括:

  • 自动补全
  • 语法高亮
  • 按键绑定

接着可以学习 Dialogs(对话框),这一部分通常比较直观,也更容易上手。

最后再深入学习全屏应用开发以及相关的高级主题。

Printing(以及使用)格式化文本

prompt_toolkit 提供了一个 print_formatted_text() 函数,它在尽可能兼容内置 print 的同时,还支持颜色和文本样式。

在不同系统上的表现:

  • 在 Linux 上:输出 VT100 转义序列
  • 在 Windows 上:使用 Win32 API 或 VT100(取决于环境支持)

Printing plain text(打印普通文本)

python 复制代码
from prompt_toolkit import print_formatted_text

print_formatted_text('Hello world')

你也可以直接替换内置 print

python 复制代码
from prompt_toolkit import print_formatted_text as print

print('Hello world')

Formatted text(格式化文本)

有多种方式可以显示颜色:

  • 通过创建一个 HTML 对象
  • 通过创建一个包含 ANSI 转义序列的 ANSI 对象
  • 通过创建一个 (style, text) 元组列表
  • 通过创建一个 (pygments.Token, text) 元组列表 ,并用 PygmentsTokens 包装

上述四种对象中的任意一种实例,都被称为 "formatted text(格式化文本)" 。在 prompt_toolkit 的许多地方,不仅可以传入普通字符串(plain text),也可以传入这种格式化文本。

HTML

HTML 可以用于表示字符串中包含类似 HTML 的格式信息。它支持以下基础标签:

  • <b> 粗体
  • <i> 斜体
  • <u> 下划线
python 复制代码
from prompt_toolkit import print_formatted_text, HTML

print_formatted_text(HTML('<b>This is bold</b>'))
print_formatted_text(HTML('<i>This is italic</i>'))
print_formatted_text(HTML('<u>This is underlined</u>'))

此外,也可以使用标签来表示前景色:

python 复制代码
# ANSI 调色板中的颜色
print_formatted_text(HTML('<ansired>This is red</ansired>'))
print_formatted_text(HTML('<ansigreen>This is green</ansigreen>'))

# 命名颜色(256 色或 true color,取决于输出环境)
print_formatted_text(HTML('<skyblue>This is sky blue</skyblue>'))
print_formatted_text(HTML('<seagreen>This is sea green</seagreen>'))
print_formatted_text(HTML('<violet>This is violet</violet>'))

前景色和背景色也可以通过 HTML 标签的 fgbg 属性同时指定:

python 复制代码
# ANSI 调色板颜色
print_formatted_text(HTML('<aaa fg="ansiwhite" bg="ansigreen">White on green</aaa>'))

在底层,所有 HTML 标签都会被映射到样式表中的类(classes),因此你可以为自定义标签分配样式:

python 复制代码
from prompt_toolkit import print_formatted_text, HTML
from prompt_toolkit.styles import Style

style = Style.from_dict({
    'aaa': '#ff0066',
    'bbb': '#44ff00 italic',
})

print_formatted_text(HTML('<aaa>Hello</aaa> <bbb>world</bbb>!'), style=style)
ANSI

有些人更喜欢使用 VT100 ANSI 转义序列来生成输出。虽然这种方式原生只在 VT100 终端中支持,但 prompt_toolkit 可以解析这些序列,并将其转换为格式化文本实例,这意味着它们在 Windows 上同样可以工作。这一过程由 ANSI 类负责处理。

python 复制代码
from prompt_toolkit import print_formatted_text, ANSI

print_formatted_text(ANSI('\x1b[31mhello \x1b[32mworld'))

需要注意的是,即使在 Linux 的 VT100 终端中,prompt_toolkit 最终生成的输出也不一定完全一致。根据颜色深度的不同,颜色可能会被映射为其他颜色,未知标签也可能被移除。

(style, text) 元组

在内部,HTML 和 ANSI 对象最终都会被转换为 (style, text) 元组列表。不过,你也可以使用 FormattedText 类手动构建这样的列表。这种方式稍显冗长,但也是表达格式化文本最强大的一种方式。

python 复制代码
from prompt_toolkit import print_formatted_text
from prompt_toolkit.formatted_text import FormattedText

text = FormattedText([
    ('#ff0066', 'Hello'),
    ('', ' '),
    ('#44ff00 italic', 'World'),
])

print_formatted_text(text)

类似 HTML 示例,你也可以使用类名,并将样式与文本分离到样式表中:

python 复制代码
from prompt_toolkit import print_formatted_text
from prompt_toolkit.formatted_text import FormattedText
from prompt_toolkit.styles import Style

# 文本
text = FormattedText([
    ('class:aaa', 'Hello'),
    ('', ' '),
    ('class:bbb', 'World'),
])

# 样式表
style = Style.from_dict({
    'aaa': '#ff0066',
    'bbb': '#44ff00 italic',
})

print_formatted_text(text, style=style)
Pygments (Token, text) 元组

当你拥有一组 Pygments 的 (Token, text) 元组时,可以通过 PygmentsTokens 包装后进行输出:

python 复制代码
from pygments.token import Token
from prompt_toolkit import print_formatted_text
from prompt_toolkit.formatted_text import PygmentsTokens

text = [
    (Token.Keyword, 'print'),
    (Token.Punctuation, '('),
    (Token.Literal.String.Double, '"'),
    (Token.Literal.String.Double, 'hello'),
    (Token.Literal.String.Double, '"'),
    (Token.Punctuation, ')'),
    (Token.Text, '\n'),
]

print_formatted_text(PygmentsTokens(text))

同样,也可以直接输出 Pygments lexer 的结果:

python 复制代码
import pygments
from pygments.token import Token
from pygments.lexers.python import PythonLexer

from prompt_toolkit.formatted_text import PygmentsTokens
from prompt_toolkit import print_formatted_text

# 输出 pygments lexer 的结果
tokens = list(pygments.lex('print("Hello")', lexer=PythonLexer()))
print_formatted_text(PygmentsTokens(tokens))

prompt_toolkit 内置了一套默认配色方案,其风格与 Pygments 类似。如果你想修改颜色,需要了解 Pygments token 到 class 名的映射关系:

复制代码
pygments.Token

prompt_toolkit classname

Token.Keyword

Token.Punctuation

Token.Literal.String.Double

Token.Text

Token

"class:pygments.keyword"

"class:pygments.punctuation"

"class:pygments.literal.string.double"

"class:pygments.text"

"class:pygments"

类似 pygments.literal.string.double 这样的类名,实际上会被拆解为以下四个层级:

复制代码
pygments
pygments.literal
pygments.literal.string
pygments.literal.string.double

最终样式是通过组合这四个 class 的样式计算得到的。因此,可以通过如下方式修改这些 token 的样式:

python 复制代码
from prompt_toolkit.styles import Style

style = Style.from_dict({
    'pygments.keyword': 'underline',
    'pygments.literal.string': 'bg:#00ff00 #ffffff',
})
print_formatted_text(PygmentsTokens(tokens), style=style)
to_formatted_text

一个很有用的函数是 to_formatted_text()。它可以确保输入被转换为合法的格式化文本,同时还可以附加额外样式。

python 复制代码
from prompt_toolkit.formatted_text import to_formatted_text, HTML
from prompt_toolkit import print_formatted_text

html = HTML('<aaa>Hello</aaa> <bbb>world</bbb>!')
text = to_formatted_text(html, style='class:my_html bg:#00ff00 italic')

print_formatted_text(text)

Asking for input (prompts)(输入提示 / prompts)

Hello world

下面的代码是最简单的示例,使用 prompt() 函数向用户请求输入,并返回输入文本,效果类似 (raw_)input

python 复制代码
from prompt_toolkit import prompt

text = prompt("Give me some input: ")
print(f"You said: {text}")

我们得到的是一个简单的 prompt,它支持类似 readline 的 Emacs 键位绑定,但除此之外没有特别之处。不过,prompt() 有很多可配置项,接下来会逐步介绍这些参数。

The PromptSession object(PromptSession 对象)

除了直接调用 prompt() 函数,还可以创建一个 PromptSession 实例,并通过调用其 prompt() 方法来获取输入。这相当于创建了一个输入会话。

python 复制代码
from prompt_toolkit import PromptSession

# 创建 prompt 对象
session = PromptSession()

# 多次获取输入
text1 = session.prompt()
text2 = session.prompt()

这样做主要有两个优点:

  • 多次调用 prompt() 时,输入历史会被保留
  • PromptSession() 和它的 prompt() 方法接受几乎相同的参数(例如高亮、补全等)。
    如果你需要多次输入且参数基本一致,可以把这些参数传给 PromptSession(),也可以在单次 prompt() 调用时覆盖它们

Syntax highlighting(语法高亮)

添加语法高亮非常简单,只需要提供一个 lexer。所有 Pygments 的 lexer 都可以使用,只需用 PygmentsLexer 包装。你也可以通过实现 Lexer 抽象基类来自定义 lexer。

python 复制代码
from pygments.lexers.html import HtmlLexer
from prompt_toolkit.shortcuts import prompt
from prompt_toolkit.lexers import PygmentsLexer

text = prompt("Enter HTML: ", lexer=PygmentsLexer(HtmlLexer))
print(f"You said: {text}")

prompt_toolkit 默认已经包含了一个与 Pygments 类似的配色方案。如果你想使用其他 Pygments 风格,可以这样做:

python 复制代码
from pygments.lexers.html import HtmlLexer
from pygments.styles import get_style_by_name
from prompt_toolkit.shortcuts import prompt
from prompt_toolkit.lexers import PygmentsLexer
from prompt_toolkit.styles.pygments import style_from_pygments_cls

style = style_from_pygments_cls(get_style_by_name("monokai"))
text = prompt(
    "Enter HTML: ",
    lexer=PygmentsLexer(HtmlLexer),
    style=style,
    include_default_pygments_style=False
)
print(f"You said: {text}")

这里设置 include_default_pygments_style=False,是因为否则默认样式会与自定义样式合并,可能导致某些颜色与预期略有差异(尤其是自定义样式未覆盖的部分)。

Colors(颜色)

语法高亮的颜色由 Style 实例定义。默认使用一个中性的内置样式,但你可以传入任意自定义样式。

创建样式的一种简单方式是使用 from_dict()

python 复制代码
from pygments.lexers.html import HtmlLexer
from prompt_toolkit.shortcuts import prompt
from prompt_toolkit.styles import Style
from prompt_toolkit.lexers import PygmentsLexer

our_style = Style.from_dict({
    "pygments.comment": "#888888 bold",
    "pygments.keyword": "#ff88ff bold",
})

text = prompt(
    "Enter HTML: ",
    lexer=PygmentsLexer(HtmlLexer),
    style=our_style
)

样式字典与 Pygments 的样式字典非常相似,但有以下区别:

  • romansansmonoborder 选项会被忽略
  • 增加了额外样式:blinknoblinkreversenoreverse
  • 颜色既可以用 #ff0000 格式,也可以使用 ANSI 颜色名(对应终端的 16 色调色板)
Using a Pygments style(使用 Pygments 样式)

所有 Pygments 样式类都可以通过 style_from_pygments_cls() 使用。

例如使用 TangoStyle

python 复制代码
from prompt_toolkit.shortcuts import prompt
from prompt_toolkit.styles import style_from_pygments_cls
from prompt_toolkit.lexers import PygmentsLexer
from pygments.styles.tango import TangoStyle
from pygments.lexers.html import HtmlLexer

tango_style = style_from_pygments_cls(TangoStyle)

text = prompt(
    "Enter HTML: ",
    lexer=PygmentsLexer(HtmlLexer),
    style=tango_style
)

创建自定义样式(在 Pygments 样式基础上扩展):

python 复制代码
from prompt_toolkit.shortcuts import prompt
from prompt_toolkit.styles import Style, style_from_pygments_cls, merge_styles
from prompt_toolkit.lexers import PygmentsLexer

from pygments.styles.tango import TangoStyle
from pygments.lexers.html import HtmlLexer

our_style = merge_styles([
    style_from_pygments_cls(TangoStyle),
    Style.from_dict({
        "pygments.comment": "#888888 bold",
        "pygments.keyword": "#ff88ff bold",
    })
])

text = prompt(
    "Enter HTML: ",
    lexer=PygmentsLexer(HtmlLexer),
    style=our_style
)

你也可以为 prompt 本身添加颜色。为此,需要构造一个 formatted text。

一种方式是使用 (style, text) 元组列表,并通过 class 名引用样式:

python 复制代码
from prompt_toolkit.shortcuts import prompt
from prompt_toolkit.styles import Style

style = Style.from_dict({
    # 用户输入(默认文本)
    "":          "#ff0066",

    # Prompt 各部分
    "username": "#884444",
    "at":       "#00aa00",
    "colon":    "#0000aa",
    "pound":    "#00aa00",
    "host":     "#00ffff bg:#444400",
    "path":     "ansicyan underline",
})

message = [
    ("class:username", "john"),
    ("class:at",       "@"),
    ("class:host",     "localhost"),
    ("class:colon",    ":"),
    ("class:path",     "/user/john"),
    ("class:pound",    "# "),
]

text = prompt(message, style=style)

message 可以是任意形式的 formatted text(如前文所述),也可以是一个返回 formatted text 的函数。

默认情况下,颜色使用 256 色调色板。如果你想使用 24 位真彩色,可以在 prompt() 中指定:

python 复制代码
from prompt_toolkit.output import ColorDepth

text = prompt(message, style=style, color_depth=ColorDepth.TRUE_COLOR)

自动补全(Autocompletion)

可以通过传入 completer 参数来添加自动补全功能。这个参数应该是一个 Completer 抽象基类的实例。WordCompleter 是实现了该接口的一个示例补全器。

python 复制代码
from prompt_toolkit import prompt
from prompt_toolkit.completion import WordCompleter

html_completer = WordCompleter(["<html>", "<body>", "<head>", "<title>"])
text = prompt("Enter HTML: ", completer=html_completer)
print(f"You said: {text}")

WordCompleter 是一个简单的补全器,它会使用给定的词列表来补全光标前的最后一个单词。

需要注意,在 prompt_toolkit 2.0 中,自动补全变成了同步执行。这意味着如果补全计算耗时较长,会阻塞事件循环和输入处理。

对于较重的补全逻辑,建议将补全器包装在 ThreadedCompleter 中,以便在后台线程运行。

嵌套补全(Nested completion)

有时你会遇到这样的命令行接口:补全内容依赖于之前输入的内容。例如路由器或交换机的 CLI。

这种情况下,简单的 WordCompleter 不够用,我们需要多层级的补全结构。NestedCompleter 可以解决这个问题:

python 复制代码
from prompt_toolkit import prompt
from prompt_toolkit.completion import NestedCompleter

completer = NestedCompleter.from_nested_dict({
    "show": {
        "version": None,
        "clock": None,
        "ip": {
            "interface": {"brief"}
        }
    },
    "exit": None,
})

text = prompt("# ", completer=completer)
print(f"You said: {text}")

当字典中的值为 None 时,表示该位置不再有更深层的补全。

如果一个字典的所有值都是 None,也可以直接用 set 来替代。

自定义补全器(A custom completer)

对于更复杂的场景,可以自己实现一个补全器:

python 复制代码
from prompt_toolkit import prompt
from prompt_toolkit.completion import Completer, Completion

class MyCustomCompleter(Completer):
    def get_completions(self, document, complete_event):
        yield Completion("completion", start_position=0)

text = prompt("> ", completer=MyCustomCompleter())

Completer 类需要实现一个生成器方法 get_completions(),该方法接收 Document,并生成 Completion 实例。

每个 Completion 包含:

  • 要插入的文本
  • 替换位置(start_position)

这个位置用于修改光标前的文本。例如:

  • 可以把小写转成大写(用于大小写不敏感补全)
  • 或者修正拼写错误(模糊匹配)

如果 start_position 是负数,表示向前删除对应数量的字符再替换。

单个补全项的样式(Styling individual completions)

每个补全项都可以提供自定义样式,这个样式会在补全菜单或工具栏中渲染时使用。可以通过为每个 Completion 实例传入 style 参数来实现。

python 复制代码
from prompt_toolkit.completion import Completer, Completion

class MyCustomCompleter(Completer):
    def get_completions(self, document, complete_event):
        # 黑底黄字
        yield Completion(
            "completion1",
            start_position=0,
            style="bg:ansiyellow fg:ansiblack"
        )

        # 下划线
        yield Completion(
            "completion2",
            start_position=0,
            style="underline"
        )

        # 使用 class(在样式表中定义)
        yield Completion(
            "completion3",
            start_position=0,
            style="class:special-completion"
        )

示例 colorful-prompts.py 展示了补全样式效果:

可以为 Completion 提供格式化文本作为显示内容(display),从而实现更灵活的展示:

python 复制代码
from prompt_toolkit.completion import Completer, Completion
from prompt_toolkit.formatted_text import HTML

class MyCustomCompleter(Completer):
    def get_completions(self, document, complete_event):
        yield Completion(
            "completion1",
            start_position=0,
            display=HTML("<b>completion</b><ansired>1</ansired>"),
            style="bg:ansiyellow"
        )
模糊补全(Fuzzy completion)

如果某个可能的补全项是 "django_migrations",那么模糊补全允许你只输入 "djm"(这个字符串的一部分字符子集)就能匹配到它。

prompt_toolkit 自带了 FuzzyCompleterFuzzyWordCompleter 类。这些类提供了实现这种"模糊补全"的能力。第一个可以接收任意 completer 实例并将其包装,使其变成一个模糊补全器;第二个的行为类似于将 WordCompleter 包装进一个 FuzzyCompleter

自动补全可以在输入时自动触发,也可以在用户按下 Tab 键时触发。可以通过 complete_while_typing 选项进行配置:

python 复制代码
text = prompt(
    "Enter HTML: ",
    completer=my_completer,
    complete_while_typing=True
)

注意,这个设置与 enable_history_search 选项是不兼容的。原因是上下方向键的绑定会发生冲突。因此,请确保为此禁用历史搜索功能。

异步补全(Asynchronous completion)

当生成补全结果耗时较长时,最好将其放到后台线程中执行。这可以通过将 completer 包装为 ThreadedCompleter 来实现,也可以通过传入 complete_in_thread=True 参数来实现。

python 复制代码
text = prompt("> ", completer=MyCustomCompleter(), complete_in_thread=True)

输入校验(Input validation)

一个 prompt 可以附加一个验证器(validator)。这是一段代码,用来检查用户输入是否有效。只有在输入符合要求时才会返回,否则会显示错误信息,并将光标移动到指定位置。

一个验证器需要实现 Validator 抽象基类。它只需要实现一个方法:validate,该方法接收一个 Document 作为输入,当验证失败时抛出 ValidationError

python 复制代码
from prompt_toolkit.validation import Validator, ValidationError
from prompt_toolkit import prompt

class NumberValidator(Validator):
    def validate(self, document):
        text = document.text

        if text and not text.isdigit():
            i = 0

            # 获取第一个非数字字符的索引。
            # 我们希望将光标移动到这里。
            for i, c in enumerate(text):
                if not c.isdigit():
                    break

            raise ValidationError(
                message="This input contains non-numeric characters",
                cursor_position=i
            )

number = int(prompt("Give a number: ", validator=NumberValidator()))
print(f"You said: {number}")

默认情况下,在用户输入的过程中会实时进行校验,但 prompt_toolkit 也支持在用户按下回车后再进行校验:

python 复制代码
prompt(
    "Give a number: ",
    validator=NumberValidator(),
    validate_while_typing=False
)

如果输入校验包含一些计算密集型(CPU-heavy)的代码,但你又不希望阻塞事件循环,建议将验证器包装在 ThreadedValidator 中。

使用函数创建验证器(Validator from a callable)

除了实现 Validator 抽象基类外,也可以从一个简单函数开始,通过 from_callable() 类方法创建验证器。这种方式更简单,且对大约 90% 的验证场景已经足够。

示例如下:

python 复制代码
from prompt_toolkit.validation import Validator
from prompt_toolkit import prompt

def is_number(text):
    return text.isdigit()

validator = Validator.from_callable(
    is_number,
    error_message="This input contains non-numeric characters",
    move_cursor_to_end=True
)

number = int(prompt("Give a number: ", validator=validator))
print(f"You said: {number}")

这里我们定义了一个函数,接收字符串并返回布尔值来表示输入是否有效。from_callable() 会将其转换为一个 Validator 实例。需要注意的是,这种方式无法设置光标位置。

历史记录(History)

History 对象用于记录所有之前输入过的字符串,这样就可以通过上方向键查看历史输入。

推荐的方式是使用 PromptSession,它默认会在整个会话中使用一个 InMemoryHistory

如下示例默认就带有历史功能:

python 复制代码
from prompt_toolkit import PromptSession

session = PromptSession()

while True:
    session.prompt()

如果希望将历史记录持久化到磁盘,可以使用 FileHistory 替代默认的 InMemoryHistory。这个历史对象可以传给 PromptSessionprompt() 函数:

python 复制代码
from prompt_toolkit import PromptSession
from prompt_toolkit.history import FileHistory

session = PromptSession(history=FileHistory("~/.myhistory"))

while True:
    session.prompt()

自动建议(Auto suggestion)

自动建议是一种类似 fish shell 的输入提示方式,用于向用户建议可能的补全内容。

通常情况下,输入内容会与历史记录进行比对,当存在以当前输入开头的历史记录时,会在当前输入后以灰色文本显示建议内容。按右箭头 → 或 c-e 可以插入该建议,按 alt-f 可以插入建议的第一个单词。

注意(Note)

当建议基于历史记录时,请确保在多次 prompt() 调用之间共享同一个 History 对象。使用 PromptSession 会自动帮你完成这一点。

示例(Example):

python 复制代码
from prompt_toolkit import PromptSession
from prompt_toolkit.history import InMemoryHistory
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory

session = PromptSession()

while True:
    text = session.prompt("> ", auto_suggest=AutoSuggestFromHistory())
    print(f"You said: {text}")

自动建议不一定必须来自历史记录。任何实现了 AutoSuggest 抽象基类的对象都可以作为参数传入。

下面是**完整逐段翻译(不删减、不省略)**👇


添加底部工具栏(Adding a bottom toolbar)

添加一个底部工具栏非常简单,只需要向 prompt() 传入一个 bottom_toolbar 参数即可。这个参数可以是:

  • 普通文本
  • 格式化文本
  • 或一个返回普通文本或格式化文本的可调用对象(函数)

当传入函数时,每次渲染 prompt 时都会调用它,因此底部工具栏可以用于显示动态信息。

当 prompt 返回时,工具栏总是会被清除。下面是一个示例,使用一个返回 HTML 对象的函数。默认情况下,工具栏使用反色(reversed)样式,因此这里设置的是背景色而不是前景色。

python 复制代码
from prompt_toolkit import prompt
from prompt_toolkit.formatted_text import HTML

def bottom_toolbar():
    return HTML('This is a <b><style bg="ansired">Toolbar</style></b>!')

text = prompt("> ", bottom_toolbar=bottom_toolbar)
print(f"You said: {text}")

.../_images/bottom-toolbar.png

类似地,也可以使用 (style, text) 元组列表:

python 复制代码
from prompt_toolkit import prompt
from prompt_toolkit.styles import Style

def bottom_toolbar():
    return [("class:bottom-toolbar", " This is a toolbar. ")]

style = Style.from_dict({
    "bottom-toolbar": "#ffffff bg:#333333",
})

text = prompt("> ", bottom_toolbar=bottom_toolbar, style=style)
print(f"You said: {text}")

默认的类名是 bottom-toolbar,它也会用于填充工具栏的背景。

添加右侧提示(Adding a right prompt)

prompt() 函数也原生支持右侧提示(right prompt)。熟悉 ZSH 的用户可能会把它理解为 RPROMPT 选项。

因此,类似添加底部工具栏,我们可以传入一个 rprompt 参数。这个参数可以是:

  • 普通文本
  • 格式化文本
  • 或一个返回这些内容的可调用对象
python 复制代码
from prompt_toolkit import prompt
from prompt_toolkit.styles import Style

example_style = Style.from_dict({
    "rprompt": "bg:#ff0066 #ffffff",
})

def get_rprompt():
    return "<rprompt>"

answer = prompt("> ", rprompt=get_rprompt, style=example_style)

get_rprompt 函数可以返回任意格式化文本(例如 HTML)。同样,也可以直接向 prompt()rprompt 参数传入文本,而不一定要使用函数。

Vi 输入模式(Vi input mode)

prompt_toolkit 同时支持 Emacs 和 Vi 两种按键绑定(类似 Readline)。

默认情况下,prompt() 使用 Emacs 绑定。这是因为在大多数操作系统中,Bash shell 默认也是使用 Emacs 绑定,更符合直觉。

如果需要使用 Vi 绑定,只需传入 vi_mode=True

python 复制代码
from prompt_toolkit import prompt

prompt("> ", vi_mode=True)

添加自定义按键绑定(Adding custom key bindings)

默认情况下,每个 prompt 已经内置了一套按键绑定,实现了常见的 Vi 或 Emacs 行为。我们可以通过向 prompt() 函数或 PromptSession 类传入一个 KeyBindings 实例,来扩展这些绑定(通过 key_bindings 参数)。

下面是一个示例:当按下 Control-T 时打印 'hello world'

python 复制代码
from prompt_toolkit import prompt
from prompt_toolkit.application import run_in_terminal
from prompt_toolkit.key_binding import KeyBindings

bindings = KeyBindings()

@bindings.add("c-t")
def _(event):
    " 当按下 `c-t` 时输出 'hello'。 "
    def print_hello():
        print("hello world")
    run_in_terminal(print_hello)

@bindings.add("c-x")
def _(event):
    " 当按下 `c-x` 时退出。 "
    event.app.exit()

text = prompt("> ", key_bindings=bindings)
print(f"You said: {text}")

注意,在第一个按键绑定中我们使用了 run_in_terminal()。这样可以确保 print 输出不会和 prompt 的显示混在一起。如果按键绑定本身不产生输出,则可以直接处理,而不需要嵌套函数。

根据条件启用按键绑定(Enable key bindings according to a condition)

通常,有些按键绑定需要根据某些条件启用或禁用。例如,Emacs 和 Vi 的按键绑定不会同时生效,但可以在运行时切换。

要根据条件启用按键绑定,需要传入一个 Filter(通常是 Condition 实例)。

(更多内容请参考 filters 文档。)

python 复制代码
from prompt_toolkit import prompt
from prompt_toolkit.filters import Condition
from prompt_toolkit.key_binding import KeyBindings

bindings = KeyBindings()

@Condition
def is_active():
    " 仅在每分钟的后半段激活该按键绑定。 "
    return datetime.datetime.now().second > 30

@bindings.add("c-t", filter=is_active)
def _(event):
    # ...
    pass

prompt("> ", key_bindings=bindings)
在 Emacs 和 Vi 模式之间动态切换(Dynamically switch between Emacs and Vi mode)

Application 对象有一个 editing_mode 属性。我们可以通过修改这个属性,在 EditingMode.VIEditingMode.EMACS 之间切换按键绑定。

python 复制代码
from prompt_toolkit import prompt
from prompt_toolkit.application.current import get_app
from prompt_toolkit.enums import EditingMode
from prompt_toolkit.key_binding import KeyBindings

def run():
    # 创建一组按键绑定
    bindings = KeyBindings()

    # 添加一个额外的按键绑定,用于切换模式
    @bindings.add("f4")
    def _(event):
        " 在 Emacs 和 Vi 模式之间切换。 "
        app = event.app

        if app.editing_mode == EditingMode.VI:
            app.editing_mode = EditingMode.EMACS
        else:
            app.editing_mode = EditingMode.VI

    # 在底部添加一个工具栏,显示当前输入模式
    def bottom_toolbar():
        " 显示当前输入模式。 "
        text = "Vi" if get_app().editing_mode == EditingMode.VI else "Emacs"
        return [
            ("class:toolbar", " [F4] %s " % text)
        ]

    prompt("> ", key_bindings=bindings, bottom_toolbar=bottom_toolbar)

run()
使用 control-space 触发补全(Using control-space for completion)

一种常见的快捷方式是使用 Control-Space 来打开自动补全菜单,而不是使用 Tab 键。可以通过如下按键绑定实现:

python 复制代码
kb = KeyBindings()

@kb.add("c-space")
def _(event):
    " 初始化自动补全,或选择下一个补全项。 "
    buff = event.app.current_buffer
    if buff.complete_state:
        buff.complete_next()
    else:
        buff.start_completion(select_first=False)

其他 prompt 选项

多行输入(Multiline input)

读取多行输入只需要传入 multiline=True 参数即可。

python 复制代码
from prompt_toolkit import prompt

prompt("> ", multiline=True)

这样做的一个副作用是:回车键现在会插入换行符,而不是提交输入。用户需要按 Meta+Enter(或先按 Escape 再按 Enter)来提交输入。

可以指定一个"续行提示"(continuation prompt)。通过向 prompt() 传入 prompt_continuation 参数实现,该参数是一个可调用对象。这个函数需要返回格式化文本,或 (style, text) 元组列表。返回文本的宽度不能超过给定宽度(提示区域的宽度由 prompt 决定)。

python 复制代码
from prompt_toolkit import prompt

def prompt_continuation(width, line_number, is_soft_wrap):
    return "." * width
    # 或:return [("", "." * width)]

prompt(
    "multiline input> ",
    multiline=True,
    prompt_continuation=prompt_continuation
)
设置默认值(Passing a default)

可以提供一个默认值:

python 复制代码
from prompt_toolkit import prompt
import getpass

prompt("What is your name: ", default=f"{getpass.getuser()}")
鼠标支持(Mouse support)

支持有限的鼠标操作,包括:

  • 光标定位
  • 滚动(用于多行输入)
  • 点击自动补全菜单

通过 mouse_support=True 启用:

python 复制代码
from prompt_toolkit import prompt

prompt("What is your name: ", mouse_support=True)
换行方式(Line wrapping)

默认启用自动换行,这也是大多数用户熟悉的行为(与 GNU Readline 一致)。

如果关闭,则输入内容会横向滚动。

python 复制代码
from prompt_toolkit import prompt

prompt("What is your name: ", wrap_lines=False)
密码输入(Password input)

当设置 is_password=True 时,输入内容会被星号(*)替代显示:

python 复制代码
from prompt_toolkit import prompt

prompt("Enter password: ", is_password=True)

光标形状(Cursor shapes)

许多终端支持不同的光标形状,例如:方块(block)、竖线(beam)、下划线(underscore),以及是否闪烁。

可以在输入时指定光标样式,或者在 Vi 模式下根据不同模式动态变化。

python 复制代码
from prompt_toolkit import prompt
from prompt_toolkit.cursor_shapes import CursorShape, ModalCursorShapeConfig

prompt(">", cursor=CursorShape.BLOCK)
prompt(">", cursor=CursorShape.UNDERLINE)
prompt(">", cursor=CursorShape.BEAM)
prompt(">", cursor=CursorShape.BLINKING_BLOCK)
prompt(">", cursor=CursorShape.BLINKING_UNDERLINE)
prompt(">", cursor=CursorShape.BLINKING_BEAM)
prompt(">", cursor=ModalCursorShapeConfig())

添加边框(Adding a frame)

可以通过 show_frame=True 给输入框加一个边框。边框颜色可以通过样式中的 frame.border 控制:

python 复制代码
from prompt_toolkit import prompt
from prompt_toolkit.styles import Style

style = Style.from_dict(
    {
        "frame.border": "#884444",
    }
)

answer = prompt("Say something > ", style=style, show_frame=True)
print(f"You said: {answer}")

也可以通过 filter 控制显示条件,例如只在输入时显示边框:

python 复制代码
from prompt_toolkit import prompt
from prompt_toolkit.filters import is_done

answer = prompt("Say something > ", show_frame=~is_done)
print(f"You said: {answer}")

在 asyncio 应用中使用 prompt(Prompt in an asyncio application)

在 asyncio 应用中,绝不能阻塞事件循环。但 prompt() 是阻塞的,会冻结整个应用,因此不能在协程中直接调用。

解决方案是使用 prompt_async(),它返回一个 coroutine,可以使用 await

python 复制代码
from prompt_toolkit import PromptSession
from prompt_toolkit.patch_stdout import patch_stdout

async def my_coroutine():
    session = PromptSession()
    while True:
        with patch_stdout():
            result = await session.prompt_async("Say something: ")
        print(f"You said: {result}")

patch_stdout() 是可选的,但推荐使用,因为其他协程可能会向 stdout 输出内容,它可以避免这些输出破坏 prompt 显示。

不显示 prompt,逐键读取 stdin(Reading keys from stdin, one key at a time, but without a prompt)

如果你只想逐个读取按键(而不显示 prompt),也可以做到:

python 复制代码
import asyncio

from prompt_toolkit.input import create_input
from prompt_toolkit.keys import Keys


async def main() -> None:
    done = asyncio.Event()
    input = create_input()

    def keys_ready():
        for key_press in input.read_keys():
            print(key_press)

            if key_press.key == Keys.ControlC:
                done.set()

    with input.raw_mode():
        with input.attach(keys_ready):
            await done.wait()


if __name__ == "__main__":
    asyncio.run(main())

这个示例会在每次按键时打印一个 KeyPress 对象。该方式是跨平台的,在 Windows 上也可以正常工作。

询问选择(Asking for a choice)

类似于 prompt() 函数用于文本输入,prompt_toolkit 也提供了 choice() 函数,用于从一组选项中进行选择:

python 复制代码
from prompt_toolkit.shortcuts import choice

result = choice(
    message="Please choose a dish:",
    options=[
        ("pizza", "Pizza with mushrooms"),
        ("salad", "Salad with tomatoes"),
        ("sushi", "Sushi"),
    ],
    default="salad",
)
print(f"You have chosen: {result}")

选项着色(Coloring the options)

可以自定义颜色和样式。message 参数可以接受任意格式化文本(formatted text),而 options 中的标签(即每个选项的第二个参数)同样也可以是格式化文本。此外,我们还可以通过 from_dict() 函数传入一个 Style 实例:

python 复制代码
from prompt_toolkit.formatted_text import HTML
from prompt_toolkit.shortcuts import choice
from prompt_toolkit.styles import Style

style = Style.from_dict(
    {
        "input-selection": "fg:#ff0000",
        "number": "fg:#884444 bold",
        "selected-option": "underline",
    }
)

result = choice(
    message=HTML("<u>Please select a dish</u>:"),
    options=[
        ("pizza", "Pizza with mushrooms"),
        (
            "salad",
            HTML("<ansigreen>Salad</ansigreen> with <ansired>tomatoes</ansired>"),
        ),
        ("sushi", "Sushi"),
    ],
    style=style,
)
print(f"You have chosen: {result}")

添加边框(Adding a frame)

choice() 函数支持 show_frame 参数。

当设置为 True 时,输入区域会显示在一个边框中。也可以传入 filter(例如 ~is_done),使边框只在输入阶段显示,提交后隐藏。

python 复制代码
from prompt_toolkit.shortcuts import choice
from prompt_toolkit.filters import is_done
from prompt_toolkit.styles import Style

style = Style.from_dict(
    {
        "frame.border": "#884444",
        "selected-option": "bold",
    }
)

result = choice(
    message="Please select a dish:",
    options=[
        ("pizza", "Pizza with mushrooms"),
        ("salad", "Salad with tomatoes"),
        ("sushi", "Sushi"),
    ],
    style=style,
    show_frame=~is_done,
)
print(f"You have chosen: {result}")

添加底部工具栏(Adding a bottom toolbar)

可以通过 bottom_toolbar 参数添加底部工具栏。该参数可以是:

  • 普通文本
  • 格式化文本
  • 返回文本/格式化文本的函数
python 复制代码
from prompt_toolkit.filters import is_done
from prompt_toolkit.formatted_text import HTML
from prompt_toolkit.shortcuts import choice
from prompt_toolkit.styles import Style

style = Style.from_dict(
    {
        "frame.border": "#ff4444",
        "selected-option": "bold",
        # (noreverse:因为默认工具栏样式使用 reverse)
        "bottom-toolbar": "#ffffff bg:#333333 noreverse",
    }
)

result = choice(
    message=HTML("<u>Please select a dish</u>:"),
    options=[
        ("pizza", "Pizza with mushrooms"),
        ("salad", "Salad with tomatoes"),
        ("sushi", "Sushi"),
    ],
    style=style,
    bottom_toolbar=HTML(
        " Press <b>[Up]</b>/<b>[Down]</b> to select, <b>[Enter]</b> to accept."
    ),
    show_frame=~is_done,
)
print(f"You have chosen: {result}")

对话框(Dialogs)

prompt_toolkit 提供了一套高级 API,用于显示对话框,类似 Whiptail 程序,但完全用 Python 实现。

消息框(Message box)

使用 message_dialog() 函数可以显示一个简单的消息框。例如:

python 复制代码
from prompt_toolkit.shortcuts import message_dialog

message_dialog(
    title='Example dialog window',
    text='Do you want to continue?\nPress ENTER to quit.'
).run()

输入框(Input box)

input_dialog() 函数可以显示一个输入框,并返回用户输入的字符串。

python 复制代码
from prompt_toolkit.shortcuts import input_dialog

text = input_dialog(
    title='Input dialog example',
    text='Please type your name:'
).run()

可以传入 password=True 参数,将其变为密码输入框。

是/否确认对话框(Yes/No confirmation dialog)

yes_no_dialog() 函数用于显示一个是/否确认框,返回布尔值(True 或 False)。

python 复制代码
from prompt_toolkit.shortcuts import yes_no_dialog

result = yes_no_dialog(
    title='Yes/No dialog example',
    text='Do you want to confirm?'
).run()

按钮对话框(Button dialog)

button_dialog() 用于显示带按钮选项的对话框。按钮以元组列表形式提供,每个元组包含:

  • 标签(label)
  • 点击后返回值(return value)
python 复制代码
from prompt_toolkit.shortcuts import button_dialog

result = button_dialog(
    title='Button dialog example',
    text='Do you want to confirm?',
    buttons=[
        ('Yes', True),
        ('No', False),
        ('Maybe...', None)
    ],
).run()

单选列表对话框(Radio list dialog)

radiolist_dialog() 显示单选列表。值以元组列表提供,每个元组包含:

  • 返回值(第一个元素)
  • 显示值(第二个元素)
python 复制代码
from prompt_toolkit.shortcuts import radiolist_dialog

result = radiolist_dialog(
    title="RadioList dialog",
    text="Which breakfast would you like ?",
    values=[
        ("breakfast1", "Eggs and beacon"),
        ("breakfast2", "French breakfast"),
        ("breakfast3", "Equestrian breakfast")
    ]
).run()

多选列表对话框(Checkbox list dialog)

checkboxlist_dialog() 与单选列表类似,但允许选择多个值,因此返回的是一个数组。

python 复制代码
from prompt_toolkit.shortcuts import checkboxlist_dialog

results_array = checkboxlist_dialog(
    title="CheckboxList dialog",
    text="What would you like in your breakfast ?",
    values=[
        ("eggs", "Eggs"),
        ("bacon", "Bacon"),
        ("croissants", "20 Croissants"),
        ("daily", "The breakfast of the day")
    ]
).run()

对话框样式(Styling of dialogs)

所有对话框都可以传入自定义 Style 实例来覆盖默认样式。

此外,文本本身也可以通过 HTML 进行样式化。

python 复制代码
from prompt_toolkit.formatted_text import HTML
from prompt_toolkit.shortcuts import message_dialog
from prompt_toolkit.styles import Style

example_style = Style.from_dict({
    'dialog':             'bg:#88ff88',
    'dialog frame.label': 'bg:#ffffff #000000',
    'dialog.body':        'bg:#000000 #00ff00',
    'dialog shadow':      'bg:#00aa00',
})

message_dialog(
    title=HTML('<style bg="blue" fg="white">Styled</style> '
               '<style fg="ansired">dialog</style> window'),
    text='Do you want to continue?\nPress ENTER to quit.',
    style=example_style
).run()

结尾

很多人看到这篇 prompt_toolkit 的内容时,第一反应其实不是"这是一个Python库",而是"这不就是某些现代终端产品的实现方式吗"。

比如进度条,看起来就像 pip install 或各种 CLI 工具里那种动态加载条,而且配色、动画逻辑都很接近。再比如表格输出,很容易让人联想到类似 Claude Code 这类工具在终端里展示结构化数据的方式。甚至 Markdown 渲染、富文本样式、边框容器这些能力组合在一起,会让人产生一种错觉:终端已经"进化"成一个轻量 Web UI 运行环境了。

尤其是 choice() 这种交互组件,本质上就是一个"终端弹窗 UI",再配合底部 toolbar、frame、radio list、checkbox list,这些东西组合起来,确实很容易让人想到现代 AI coding assistant 的交互界面。比如你在 Claude Code 里看到的选择框、快捷提示栏,本质上就是同一类思想的实现:把结构化 UI 从 Web 挪进 terminal。

但关键点在于,这些并不是 Claude Code 独有的设计,也不是某个框架"突然变高级了",而是这一整套 terminal UI 技术早就存在了。prompt_toolkit、rich、click 这些库,本质上是在不同层次上把"终端变成可交互 UI"这件事做了封装。模型在训练数据里见过这些模式,所以当你让它"做一个类似 Claude Code 的 CLI",它确实能很自然地拼出一个看起来非常像的系统。

你说"那这些不都是 Python 写的吗?"------是的,但这恰恰不重要。Linux 下很多系统工具本身就是 Python 写的,而且它们的启动方式就是通过 shebang 指定解释器。也就是说,从运行模型的角度来看,语言只是实现手段,不是系统边界。

在 agent 场景里,真正的 IO 结构是网络请求 + 多轮模型调用 + 工具执行流,而不是 TS、JS、Python 之间的差异。只要能调用工具、能输出 UI、能处理状态机,这些语言都只是"载体"。

至于打包问题,其实也是一个工程取舍问题。像 PyInstaller 确实简单粗暴,会把解释器一起打进去,体积很大;但 Nuitka 这类方案可以把 Python 编译成本地二进制,虽然对复杂依赖不是 100% 兼容,但做一个 agent 或 CLI 工具通常已经足够用了。

所以回到最初那个感觉:

"这不就是把 Web UI 塞进终端了吗?"

某种程度上,是的。区别只是------它不是 HTML/CSS/JS,而是另一套"终端 UI DSL"。而 prompt_toolkit 做的事情,就是把这套 DSL 变得足够工程化,让你真的可以在 CLI 里搭一个像应用一样完整的交互系统。

相关推荐
AI木马人12 小时前
3.【Prompt工程实战】如何设计一个可复用的Prompt系统?(避免每次手写提示词)
linux·服务器·人工智能·深度学习·prompt
胡利光1 天前
Harness Engineering 01|从 Prompt Engineering 到 Harness Engineering
prompt
干洋芋果果2 天前
AI念咒_浏览器测试自动化
prompt
qcx232 天前
【解构】DeepSeek V4 发布:技术报告深度解读 + 横向对比六大开源模型,我们的判断是……
人工智能·chatgpt·prompt
蓝色的音乐2 天前
GPT Image 2 提示词怎么写?分享一个 400+ 案例 Prompt Gallery
gpt·prompt
迦南的迦 亚索的索2 天前
AI_05_基于Prompt工程的金融行业项目
人工智能·金融·prompt
2501_940041743 天前
AI创建小游戏指令词
人工智能·游戏·prompt
2501_940041743 天前
投喂:AI生成各类游戏提示词
人工智能·游戏·prompt
renhongxia13 天前
计算机视觉实战:图像去噪模型训练与应用
开发语言·人工智能·机器学习·计算机视觉·prompt