跳到主要内容
ansicode

ANSI 转义码常见坑点

踩一次就会终生记住的具体失败模式。每条包含「症状」、「成因」、「修复」。

最近更新

  1. 01你输出的彩色文字继续污染后续行,连用户的命令提示符都被染色。

    成因设置了颜色属性,但在最后一次换行或程序退出前没有发送 SGR 0(`\x1b[0m`)重置。

    修复始终用 `\x1b[0m`(或更窄的 `\x1b[39m` 仅重置前景色)结束彩色输出。把颜色当作 try/finally 资源管理。

    参考序列SGR 0 — 重置 / 恢复默认

  2. 02用 `\x1b]0;...` 设置窗口标题后,下一段程序输出莫名其妙消失或不渲染。

    成因OSC 必须显式以 BEL(`\x07`)或 ST(`\x1b\\`)结束。若漏写终止符,解析器会一直把后续字节当作标题文本吞掉,直到遇到终止字符。

    修复OSC 序列务必闭合:`\x1b]0;title\x07`。为兼容 xterm 优先用 BEL(`\x07`),追求 ECMA-48 严谨则用 ST(`\x1b\\`)。

    参考序列OSC 0 / 2 — 设置窗口/图标标题

  3. 03对齐良好的列表表格,一旦加上彩色就错位 —— 含彩色的那一行看起来更短。

    成因转义序列含不可打印字节,朴素的 `len(string)` 或 `printf '%-20s'` 把它们计入了宽度。

    修复先剥离 ANSI(见 `/strip`)再测量可见宽度,然后在彩色输出外补齐填充。大多数 UI 库(rich、blessed、lipgloss、ratatui)会代你处理。

    参考序列SGR 30–37 — 前景色(8 种基础色)

  4. 04用 `\x1b[?1049l` 退出备用屏幕后,光标停在错的行 —— 通常比预期高一行。

    成因DECSET 1049 会恢复保存时的光标位置,但应用在 alt screen 内最后一次写的可见行常常没有以换行结尾。

    修复退出前先发送 `\r\n`(或平台对应换行),或在 `\x1b[?1049l` 前显式 DECRC(`\x1b8`)以恢复到一个干净的行。

    参考序列DECSET 1049 — 备用屏幕缓冲

  5. 05用户抱怨工具在管道或重定向中仍输出颜色,把日志搞乱。

    成因工具无条件输出颜色,没有遵从「不要颜色」的标准信号。

    修复尊重事实标准 `NO_COLOR=1`(https://no-color.org),同时检查 `isatty(stdout)` —— 输出不是终端时禁用颜色。也应当遵守 `TERM=dumb` 与 `CLICOLOR=0`。

    参考序列SGR 30–37 — 前景色(8 种基础色)

  6. 06真彩色在你机器上正常,在其他终端却看起来偏色或错乱。

    成因并非所有终端都支持 24 位色;不支持的会把每个 RGB 量化到 256 色调色板最近邻,色相可能大幅偏移。

    修复发送 `38;2;r;g;b` 前检查 `$COLORTERM`(应为 `truecolor` 或 `24bit`)。不支持时回退到 256 色调色板索引,或 16 种基础色。

    参考序列SGR 38;2;R;G;B — 24 位真彩色前景

  7. 07色盲用户区分不出你的「成功」与「错误」输出。

    成因两个状态只用颜色区分;一旦丢掉绿/红信号,文字看起来完全一样。

    修复颜色与文本前缀或图标搭配使用:`✓ ok` 与 `✗ failed`、`[ok]` 与 `[err]`。颜色应当强化信号,绝不能单独承载信号。

    参考序列SGR 30–37 — 前景色(8 种基础色)

  8. 08TUI 崩溃后,用户 shell 的闪烁光标消失,需要 `tput cnorm` 或 `reset` 才能恢复。

    成因发送了 `\x1b[?25l`(隐藏光标)后未及发送 `\x1b[?25h`(显示光标)就崩溃了。

    修复安装信号处理器 / `defer` / `atexit` / `try/finally`,**任何**退出路径都必须重发 `\x1b[?25h`,并关闭 bracketed paste、鼠标跟踪、alt screen 等。

    参考序列DECTCEM ?25 — 显示/隐藏光标

  9. 09vim 崩溃(或任意 TUI 异常退出)后,用户 shell 中的方向键失灵 —— 按 ↑ 不再调出上一条历史命令,光标位置反而出现像 `OA` 的散字符。Backspace、Home、End 也可能异常。

    成因崩溃的应用在启动时发了 `smkx`(`\x1b[?1h\x1b=` —— DECCKM 开 + DECKPAM)把终端切到**应用键盘模式**,但退出前未能发出配套的 `rmkx`(`\x1b[?1l\x1b>` —— DECCKM 关 + DECKPNM)。DECCKM 卡在「开」上后,用户按 ↑ 时终端发 `\x1bOA`(SS3 A)—— 但用户 shell 的 readline / zsh-line-editor 只绑了*常规模式*的 `\x1b[A`(CSI A)。方向键的字节流到了却匹配不到任何绑定,readline 只能把可打印部分(`OA`)回显出来,而不是上翻历史。

    修复**立即恢复**(不必登出):跑 `tput rmkx`(或直接 `printf '\e[?1l\e>'`)—— 切换的两半都发。仍不行就 `reset` 或 `tput reset` 做更宽的硬重置。**预防**自家 TUI:注册 SIGINT / SIGTERM / SIGQUIT 处理器 + `atexit` / `defer` 块,确保**任何**退出路径(含未处理 panic)都发 `rmkx`。Go:`defer fmt.Print("\x1b[?1l\x1b>")`。Python:`atexit.register(lambda: sys.stdout.write('\x1b[?1l\x1b>'))`。Rust crossterm:在包装结构体的 `Drop` 实现里用 `LeaveAlternateScreen` + `DisableMouseCapture` 清理模式。

    参考序列DECKPAM / DECKPNM — 小键盘应用 / 数字模式(ESC = / ESC >)

  10. 10你的工具发 `\x1b[A` 期望光标上移 0 行(空操作),结果光标上跳 1 行。或者发 `\x1b[m` 后样式被清除(如愿),但发 `\x1b[J` 想啥都不做时,光标之下整屏被擦掉。

    成因**CSI 参数默认值并非统一为 `0`。** SGR(`m`)省略 Ps 时默认 `0`(重置)—— `\x1b[m` 等同 `\x1b[0m`。但光标移动 CSI(`CUU` / `CUD` / `CUF` / `CUB` = `A` / `B` / `C` / `D`)默认值是 **`1`** —— `\x1b[A` 等同 `\x1b[1A`(上 1 行),而不是 `\x1b[0A`(如果有「上 0 行」之意)。擦除指令(`ED` `\x1b[J`、`EL` `\x1b[K`)默认 `0` 表示「从光标到屏末 / 行末」—— `\x1b[J` 擦除从光标到屏末,**并非**「擦 0 个 = 空操作」。每个序列的默认值是它各自 ECMA-48 规范的一部分;把「CSI 默认 0」当统一规则迟早翻车。

    修复**值有意义时务必显式写参数**。`\x1b[1A` 表「上 1 行」(别依赖默认值)、`\x1b[0m` 表「重置 SGR」(别依赖 SGR 专属默认 —— 显式 `0` 字节数相同且读起来不含糊)。真想要「空操作」就别发任何字节 —— 别试图找一个表「不动」的参数值。每写一个 CSI 序列,查对应 `/sequence/<slug>` 页的参数表 —— 每条序列的默认值都按规范单独列出,因为规范本就是逐条定义。

    参考序列CUU / CUD / CUF / CUB — 移动光标

  11. 11TUI 退出后,用户 shell 中每次粘贴剪贴板都带上多余的 `\x1b[200~` … `\x1b[201~` 字节标记 —— 粘进去的命令不再执行,提示行上变成 `200~ls -la 201~`,更糟时这些标记会写入正在编辑的文件。

    成因退出的 TUI 在启动时发了 `\x1b[?2004h`(DEC 私有模式 2004 —— *括号粘贴*),请求终端为后续粘贴前后加哨兵标记(这样应用能区分键入 vs 粘贴),但退出前未发出配套的 `\x1b[?2004l`。终端仍处在括号粘贴模式,可用户的 shell(readline / zsh-line-editor)没绑这些标记 —— 它们就当作普通字符流过来。

    修复**立即恢复**:跑 `printf '\e[?2004l'` 关闭括号粘贴模式,或 `reset` 更宽地恢复。zsh / bash readline 8.0+ 实际上*识别*括号粘贴,绑了的话会静默吃掉标记;若仍泄漏,说明 shell 较旧或绑定被剥离 —— 查 `bind -p | grep paste`。**预防**自家 TUI:发出的每个 `\x1b[?2004h` 都在同一个清理块中配 `\x1b[?2004l`(与 `rmkx` / 光标恢复 / 鼠标禁用 / 退出 alt-screen 并列)。更稳:用 **XTSAVE/XTRESTORE 栈**(`\x1b[?2004s` 入栈、`\x1b[?2004r` 出栈),让退出时的状态回到父进程原状,而非无条件「关」。

    参考序列DECSET ?2004 — 括号粘贴模式

  12. 12在自家 TUI 里按**单独 Esc**(想退出某模式,如 vim 插入模式)后明显延迟 100–1000 ms 才有反应。更糟:连按 Esc + 别的(比如 Meta-as-Esc 约定下的 Alt+字母)有时被识别成单纯 Esc、有时被识别成组合键 —— 竞态般的飘忽。

    成因几乎所有多字节输入序列都以 `\x1b` 开头 —— 恰是用户单按 Esc 时发的同一字节。输入循环见到 `\x1b` 后没法立刻判断:「孤立 Esc」还是「`\x1b[A` / `\x1bOP` / `\x1b]…` 的首字节」?天真方案:等 `T` 时间,若 `T` 内无后续字节则判定「孤立 Esc」。ncurses 把 `T`(**ESCDELAY** 环境变量)默认为 **1000 ms** —— 太长,用户能感知卡顿。把 `T` 设太短(< 20 ms)则在慢速 tmux / SSH 链路上,真正的 `\x1b[A` 会被拆成两次读 —— 误判为「Esc」+ 字面 `[A`。

    修复**现代折衷**:`export ESCDELAY=25`(25 ms)—— 快到本地终端孤立 Esc 几无感知,慢到即使慢速 SSH 也不会把 2 字节序列拆开。**更优解**:上 **kitty 键盘协议**(CSI u —— `\x1b[>1u` 标志 1 'disambiguate escape codes')—— 若目标模拟器支持(kitty / foot / WezTerm / ghostty / Konsole 24.02+),终端会对孤立 Esc 发 `\x1b[27u`,彻底消除歧义。通过 XTGETTCAP 查 `Su` 能力探测,不支持时回退到 ESCDELAY。**分语言**:bash `read -t 0.025`、Python `select.select(..., 0.025)`、Rust crossterm `poll(Duration::from_millis(25))`、Go tcell `PollEvent` + `EventTime` 过滤。

    参考序列C1 控制字符 —— ESC 序列的 8 位单字节等价形式(0x80–0x9F)

  13. 13TUI 启动时发 `\x1b[?3h`(DECCOLM 置位 —— 请求 132 列宽屏)。直接在 xterm 下跑没事;同一二进制在 **tmux** 或 GNU screen 下跑就会出现一行(或几行)错乱输出 —— 半擦除的行、80–131 列里飘的残影字符、整片版面按错列宽渲染。调整 tmux 窗格大小常常会加剧。

    成因DECCOLM 在真 VT510 上是**物理列数切换**,所以规范规定 `\x1b[?3h`/`\x1b[?3l` 必带副作用:清屏 + 光标回家 + 重置边距(详见 `/sequence/deccolm`)。xterm 全部尊重 —— 包括通过 X11 资源 `allowC132` 真正完成 80↔132 大小切换。**tmux 做不到**:tmux 是复用器,其窗格列数由外部终端定死,无法请求父端调整。所以 tmux 对 `\x1b[?3h` 的仿真做了清屏 + 光标回家 + 重置边距(因解析器急切派发副作用),却没改实际列数 —— TUI 自以为有 132 列,开始写超宽行并在中途回卷,而 tmux 窗格仍是 80 列。错位即损坏。SIGWINCH 让陷阱更深:tmux 自身 resize 时会传播 SIGWINCH,但**不会**为撤销 DECCOLM 失配再发一次,TUI 永远不知道自己对列数的认知是错的。DECSCPP(`CSI Pn$|` —— 带参列数)同理。

    修复**复用器下不要发 DECCOLM。**通过 `$TMUX` / `$STY` 环境变量检测 tmux / screen,或读 terminfo `cols`(`tput cols`)—— 若它在非 `allowC132` 类模拟器下报固定的 80 / 132,则当前为嵌套环境。**现代方案**:用 `TIOCGWINSZ`(`ioctl`)或 `$COLUMNS` 查真实终端尺寸并按此宽度渲染 —— 2026 年绝不要把 DECCOLM 当作布局设置原语。若你确需宽屏(截图 / 打印流程),就守门:`if [ -n "$TMUX" ] || [ -n "$STY" ]; then echo '复用器下宽屏模式被禁用'; fi`,并优雅退出。**Resize 正确性**:装 `SIGWINCH` 处理器,每次信号到来重读 `TIOCGWINSZ` 并重排版面 —— 切勿在窗口事件后仍信任最后一次基于 DECCOLM 推断的列数。与 `/pitfalls/stuck-app-mode` 同属「副作用置状态、退出未撤销」的一类 bug。

    参考序列DECCOLM — 80 / 132 列模式(CSI ? 3 h / l)

  14. 14CLI 发了带 `id=` 参数的 OSC 8 超链接(意图:跨多行文本指向同一 URL:`\x1b]8;id=row42;https://x.com\x07Item\x1b]8;;\x07`)。在 **Kitty** 下,所有 `id=row42` 的行在悬停任一行时一起高亮 —— 正常。在 **iTerm2** 下,`id=` 被静默忽略 —— 每个 `\x1b]8;...\x07 … \x1b]8;;\x07` 块各自独立、无分组。在老版 **gnome-terminal**(≤ 3.36)或 **konsole**(< 21.04)下,对两个**不同 URL**重用 `id=row42`,会让第二个链接静默继承第一个 URL —— 数据悄然损坏。

    成因OSC 8 规范(gnome-terminal 作者 Egmont Koblinger 2017 年提案,Kitty / WezTerm / Ghostty / VS Code terminal / iTerm2 3.5+ / Konsole 21.04+ / Windows Terminal 1.21+ 采纳)把 `id=<token>` 定义为「**同 id 且同 URL 的相邻或非相邻字段视为同一逻辑链接**」的提示(让悬停高亮能跨行回卷而不把每个单元格当成独立链接)。规范**未**规定的:(a) 同 id 出现两个不同 URL 时的行为 —— 未定义,由实现自决。(b) 会话内是否允许重绑 id。(c) 作用域是每屏 / 每窗格 / 还是会话全局。各模拟器各自为政:Kitty 以 `id+url` 为去重键(同 id 不同 url → 两个独立链接 —— 正确);21.04 前 Konsole / 3.36 前 gnome-terminal 仅以 id 去重(同 id 第二个 url 被静默忽略 —— 数据损坏);iTerm2 完全忽略 id(每段各自独立);Windows Terminal 接受 id 但不跨行视觉聚合。

    修复**两条规则**:(1) **同一 OSC 8 会话内不要为两个不同 URL 重用同一 `id=`**。每个逻辑链接用内容哈希或单调计数器生成 id:`id=link-$(uuidgen | head -c 8)`,绝不要重复用 `id=row`。Kitty 的正确行为(`id+url` 为去重键)是上限;按 id-only 去重作为下限来兼容。(2) **如目标受众含 iTerm2 或老版 gnome-terminal / konsole,则不要依赖 `id=` 做分组**。分组是 UX 提示而非布局原语 —— 真正需要「同一 URL 跨多行」保证的内容,请每行都发完整的 `\x1b]8;;URL\x07cell-text\x1b]8;;\x07` 包络(字节多但处处可用)。把 `id=` 留给真正的「单链接跨行回卷」情形 —— Kitty / Ghostty 用户由此获得轻微 UX 提升。**探测**:OSC 8 id 语义没有类似 DA 的探针 —— 要么假设保守行为,要么在 CI 中针对目标模拟器做测试。

    参考序列OSC 8 — 内联超链接

  15. 15Shell 脚本里两种着色方式混用 —— 时而 `tput setaf 1`(基于 terminfo),时而 `printf '\033[31m'`(硬编码 SGR 31)。在现代终端里两者看起来一样,但在**黑白 TTY**、**Linux 控制台**、`TERM=dumb` / `TERM=xterm-mono` 下,`tput` 版会正确无色,`printf` 版却把 `^[[31m` 原始字节漏给输出。更糟:脚本管道进 `less`(不加 `-R`)或重定向到文件时,`printf` 字节会作为乱码显示,而 `tput` 在非 TTY 下原本会产出干净文本。

    成因`tput setaf 1` 做了 `printf '\033[31m'` 不做的两件事:(1) **查 `$TERM` 和 terminfo 数据库**返回该终端的正确转义序列 —— 比如 `xterm-256color` 返回 `\e[31m`,但 `linux` 在黑白控制台返回**空串**;`dumb` 在哪都返回空;`screen-256color` 返回 `\e[31m`。(2) **尊重 `isatty(stdout)`**:当 stdout 非 TTY(管道 / 文件 / 被 CI 捕获)时,现代 ncurses(≥ 6.2)的 `tput` 不输出任何内容 —— 静默无操作。`printf '\033[31m'` 两者都不做:无论终端能力或输出目的地如何,永远原样吐 5 个字节。陷阱在于意图错位:开发者「想要红字」→ 开发机上两者都对 → 上线 → 不同 TERM 的用户或重定向到日志文件的用户看到乱码。

    修复**双规则决策**:(1) **可移植**脚本(要派发到未知终端:系统安装脚本、发行版工具、`/etc/bashrc` 片段、CI runner)用 `tput`。性能代价(每次 `tput` fork ~1 ms)远小于正确性收益。建议在脚本顶部缓存:`red=$(tput setaf 1); reset=$(tput sgr0)`。(2) **目标终端已知且受控**的场景(Docker entrypoint、自家 CI 内构建输出 —— 你已知 `TERM=xterm-256color`、测试夹具、自家 dotfile 自家终端)才用硬编码 `printf '\033[31m'`。即便如此也要用 `[ -t 1 ]`(stdout isatty)守门,避免管道场景漏字节:`[ -t 1 ] && printf '\033[31m%s\033[0m\n' "red" || printf '%s\n' "red"`。**同一脚本内不要混用**,团队保持一种心智模型。无论哪种都要尊重 `NO_COLOR=1`。与 `/pitfalls/no-color` 和 `/pitfalls/tmux-sigwinch-deccolm` 中的 `tput cols` 建议联读。

    参考序列SGR 30–37 — 前景色(8 种基础色)

  16. 16TUI 用 `\x1b[?1049h` 进入备用屏幕,并在状态行之间只写普通 `\n`。结果每行没从下一行的第 0 列开始,输出**对角阶梯**式漂移 —— 第 2 行从第 1 行结尾处开始,第 3 行从第 2 行结尾处开始,窗格右侧被截断的回卷塞满。绕过 ncurses / TUI 库的直接写最容易暴露这个问题。

    成因`\n` 只是 **LF**(0x0A),不是 LF+CR。ECMA-48 用 LNM(行进/新行模式 —— ANSI 模式 20,通过 `\x1b[20h` / `\x1b[20l` 置/复位)控制单独 LF 是否同时把光标回到第 0 列。**LNM 默认关**:LF 只换行不归位。常规 pty 上,内核 termios 层会加 `ONLCR`(输出 LF → CR-LF 翻译),所以走 stdout 的应用免费获得「换行即归位」—— 反过来掩盖了 LNM-off 的行为。备用屏幕缓冲本身不受 termios 影响(它是终端层缓冲,不在 tty 行规则一侧),但陷阱在于:alt-screen 的输出常由绕过 stdio 缓冲的代码路径产生(对 fd 1 直接 `write()`、自研的成批转义渲染循环)—— 这些路径绕开 `ONLCR` 翻译。内核 ONLCR 只在 `write(STDOUT_FILENO, "\n", 1)` 经行规则时触发;框架渲染循环为获得完整控制常通过 `termios.c_oflag &= ~ONLCR` 关掉。结果是:alt-screen 内你的 `\n` 变成纯 LF,光标推进行号但永不归零列,输出对角阶梯。

    修复**alt-screen 渲染循环里始终发 `\r\n`。**把每个行尾视作 2 字节序列 —— shell 里 `printf '%s\r\n'`、C / Go / Rust 里 `write("row\r\n")`、Python 里 `print(line, end='\r\n')`。**不要**依赖 LNM(`\x1b[20h`)作为修复:xterm + gnome-terminal 支持,但 Windows Terminal 和部分 macOS 终端不理会,Konsole 仅部分尊重。**不要**在渲染循环内通过 `termios` 恢复 `ONLCR` —— 这会让每个 LF 都触发列归零,包括转义串内部的,弄坏像 `\x1b[Hrow1\nrow2` 这种定位写(`row2` 应在第 2 行第 1 列,**不是**第 0 列)。**可移植不变量**:alt-screen 内光标位置归你管 —— 行间显式 `\r\n`,或用 `\x1b[<row>;1H`(CUP 到下一行起点)做绝对定位。ncurses / blessed / lipgloss / ratatui 自动处理;裸转义代码路径需自己来。相关:`/sequence/alt-screen`、`/pitfalls/alt-screen-newline`(退出时光标位置话题)。

    参考序列DECSET 1049 — 备用屏幕缓冲

  17. 17TUI 画固定宽列表,列边框在纯 ASCII 行上完美对齐;一旦某行出现 **emoji**(`🎉`)、**CJK 字符**(`字`)或**组合符**(`é` = `e` + `\u0301`),该行的右边框就左右偏移 1–2 个单元格。更糟:同一二进制在 **glibc Linux** 上对齐方式与 **musl Alpine** 不同,又与 **macOS Terminal** 不同 —— 三者收到的字节流完全一样。

    成因对「该字形多少单元格宽」有四个互不相同的独立测量:(1) 你的**库**(glibc / musl 的 `wcwidth(3)`、内嵌的 jquast/wcwidth、Rust 的 `unicode-width`)按码点查表得到一个值。glibc 的表约 Unicode 13(2020);musl 更旧,部分歧义宽字符处理不同;jquast/wcwidth 跟到 Unicode 15+。(2) **终端模拟器**独立决定怎么画 —— xterm 的 `cjkWidth` 资源把 East-Asian-Width-Ambiguous 在 1 与 2 间切换;wezterm 自有一套表;Apple Terminal 把许多 emoji 硬编码为宽 1,而 wezterm / iTerm2 渲染为宽 2。(3) 模拟器挑的**字体**可能根本没那个字形,替换字形占用的单元格数不同。(4) **Unicode East-Asian-Width** 本身每个版本都在改 —— `…`(U+2026)在 Unicode 8 是 Ambiguous,11 里明确宽 1,部分亚洲 locale 又重新变回 Ambiguous。如果你的 `wcwidth` 是按某版编译、终端是按另一版编译,那已经输了。

    修复**不要把本地 wcwidth 当真理。**三档修复,按你愿付的代价挑:(1) **便宜且可移植** —— 对齐关键列限制为 ASCII(`[ -~]`);用户输入字符串若超出,就放到对宽度不在意的列(最右、或用 `…` 截断而不计单元格数)。(2) **更好** —— 内嵌 jquast/wcwidth(Python)、`unicode-width`(Rust)、Go 的 `mattn/go-runewidth`,把 Unicode 版本钉死;同一版本作为测试夹具一起发,让 glibc / musl 分歧在 CI 中暴露。**East-Asian-Ambiguous 字符**:CJK locale 用户视为宽 2,其余宽 1 —— 通过 `LANG` / `LC_CTYPE` 决断。(3) **真值** —— 问终端:发该字形,紧跟 `\x1b[6n`(DSR 光标位置查询 —— 见 `/sequence/csi-dsr`),解析 `\x1b[<row>;<col>R` 应答,与写入前的列号相减。代价:每个不确定字形一次往返 + CSI 食谱里指出的输入泄漏风险(必须消费应答,否则进入用户输入)。实战 TUI(textual、ratatui、lipgloss)走二档策略 + 不确定度超阈值时回退到三档。与 `/pitfalls/terminal-width-math`(同一对齐问题的 SGR 字节侧)联读。

    参考序列DSR — 设备状态报告(CSI 5n / CSI 6n)

  18. 18你的应用发出 `\x1b]8;;https://example.com\x07link\x1b]8;;\x07`(OSC 8 超链接)、`\x1b]52;c;...\x07`(OSC 52 剪贴板)、`\x1b]1337;File=...\x07`(iTerm2 内联图片)或 `\x1b]9;4;1;42\x07`(ConEmu 进度),直接在支持的终端运行一切正常 —— 但一旦在 **tmux 内**运行,外层终端显示原始字节(`]8;;https://example.com` 变成纯文本),功能完全失效。换个外层终端重连 tmux 也没用。

    成因tmux 是**终端复用器**,不是透传:它解析内层窗格发出的每一个转义序列,并决定哪些转发给外层终端。默认白名单覆盖基本功能(SGR、光标控制、备用屏幕、鼠标),但**默认丢弃多数 OSC 家族扩展** —— OSC 8、OSC 52、OSC 1337、OSC 9;4 都会被丢弃或转换,除非显式允许。设计动机保守:tmux 无法知道外层终端是否支持,转发可能出错(一个困惑的外层终端可能把字节回灌进 tmux 的输入流)。两套机制管控:(1) **`allow-passthrough on`** —— tmux ≥ 3.3 的选项,开启 **DCS 透传包络** `\x1bPtmux;<把每个内层 \x1b 都重复一次>\x1b\\`;要送外层的应用必须把原始转义包起来,把每个嵌入的 `\x1b` 写成 `\x1b\x1b`。(2) **逐功能 `set-option allow-*`** —— 较新 tmux 提供 `allow-set-clipboard`(OSC 52)、`allow-hyperlinks`(OSC 8 —— 3.4 起)、`allow-rename`(OSC 0/1/2),各有自己的默认值。陷阱是意图错位:内层窗格的应用通过 DA 查询(这类查询**会**穿过 tmux)检测**外层**终端的能力,却忘了检测 tmux 本身。结果:应用以为「外层支持 OSC 8,直接发」→ tmux 静默丢弃。

    修复**双管齐下。**(1) **服务侧(tmux 配置)**:在 `~/.tmux.conf` 加 —— `set -g allow-passthrough on`(tmux ≥ 3.3)、`set -g allow-hyperlinks on`(tmux ≥ 3.4 for OSC 8)、`set -g allow-set-clipboard on`(OSC 52)。`tmux source ~/.tmux.conf` 重载。权衡:任何内层应用都能毒化外层终端 —— 仅在可信环境开启。(2) **客户侧(应用)**:检测 `$TMUX` 环境变量(tmux 在它启动的每个 shell 里都设置)。一旦命中,把每个要送外层的转义包进 DCS 透传包络:`\x1bPtmux;` + 内层转义(每个 `\x1b` 改写为 `\x1b\x1b`)+ `\x1b\\`。具体例子:要在 tmux 内发 OSC 8,不再直接发 `\x1b]8;;https://example.com\x07TEXT\x1b]8;;\x07`,而是发 `\x1bPtmux;\x1b\x1b]8;;https://example.com\x07TEXT\x1b\x1b]8;;\x07\x1b\\`。注意:这只是转发字节 —— tmux 自己仍不**理解**这些字节,它的滚动历史里会留下字面转义。库层面:kitty 的 `pyperclip` 分叉、`gum` 的样式模块、Rust 的 `crossterm` 0.27+ 都会检测 `$TMUX` 并自动 DCS 包裹。相关:`/sequence/osc-hyperlink`、`/family/osc` 食谱的超链接章节(含 id-rebind 兄弟陷阱)。

    参考序列OSC 8 — 内联超链接

  19. 19你的 CLI 用 24 位真彩色 SGR(`\x1b[38;2;r;g;b m`)画一个平滑渐变,在 **Ghostty**、**WezTerm**、**kitty**、**iTerm2**、**Windows Terminal** 上呈现干净渐变 —— 数百级色阶之间柔和过渡。但在 **macOS Terminal.app**(系统自带)、**Linux 控制台**(fbcon)、旧版 **PuTTY**、未打真彩色补丁的某些 **xterm** 上,同一渐变渲染成**可见的色带 / 条纹** —— 本该平滑斜坡的位置出现 8 或 16 条独立色带。更糟:每个模拟器的色带样子还略不同,所以即便字节完全一致,不同平台的截图也对不上。

    成因通过 `$TERM=xterm-256color`(多数发行版默认)声明 256 色的终端**不会拒绝** 24 位 SGR 字节 —— 它们优雅接受,再**下采样到最近的索引调色板项**。下采样策略各家实现不同:macOS Terminal.app 映射到 256 色立方体并截断(出色带);Linux 控制台(`/dev/console`,内核无 DRM/KMS 色彩扩展)映射到 16 个 ANSI 基色 + 8 个高亮(256 级渐变塌缩到 16 条可见色带);未编译 `--enable-direct-color` 的旧 xterm 同样。即便支持真彩色的模拟器,对**特定调色板范围**有时也下采样 —— macOS Terminal 的「Pro」主题改写索引前 16,但真彩色字节原样穿过,所以混合渐变(部分 24 位、部分索引)在并排时呈现两套视觉。检测盲点:`$TERM=xterm-256color` **对真彩色支持模糊** —— 它声明 256 索引但**完全不说** 24 位。事实标准的真彩色能力标记是 `$COLORTERM=truecolor`(或 `24bit`)—— ghostty / wezterm / kitty / iTerm2 / Windows Terminal 自动设置;macOS Terminal.app / Linux 控制台 / 经旧链路的 SSH 会话**不设**。

    修复**发之前先检测。**对真彩色 SGR 加 `$COLORTERM=truecolor` 或 `$COLORTERM=24bit` 守门。缺失时**手动量化**到 256 色调色板(xterm 6×6×6 立方:`index = 16 + 36*r + 6*g + b`,其中 `r,g,b ∈ 0..5`;将每个 0..255 分量映射到 [0, 95, 135, 175, 215, 255] 中最近的)—— 改发 `\x1b[38;5;<idx>m`。这样渐变交给**你的**量化器,而非模拟器的神秘下采样。附赠:渐变在所有 256 色模拟器上看起来一致。库支持:Rust `termcolor` 的 `ColorChoice::Auto` 已做此事;Go `fatih/color` v1.16+ 检查 `$COLORTERM`;Python `rich` 的 `Console(color_system='auto')` 同逻辑。**不要**假设 `$TERM=xterm-direct` 被设置 —— 那是显式真彩色 TERM 但采用率近零;依赖 `$COLORTERM`。**不要**只在自己机器测试 —— 至少抽查 macOS Terminal.app(最常见的假阳性:它接收真彩色不抗议、却下采样得很差)。相关:`/sequence/sgr-fg-truecolor`(字节级规范)、`/family/sgr` 食谱真彩色章节。

    参考序列SGR 38;2;R;G;B — 24 位真彩色前景

  20. 20使用 NVDA、JAWS、Orca 或 VoiceOver 的视障用户反映:你的工具被读屏读成「escape bracket three one m error escape bracket zero m」而不是「error」。

    成因读屏软件逐字符朗读文本缓冲;SGR / CSI 原始字节会按其 Unicode 码点被读出。当 stdout 是真实 TTY(或读屏的辅助 pty)时,无条件输出颜色的工具把转义字节交给辅助软件 —— 它无法把这些字节渲染成样式,只能朗读。

    修复尊重 `NO_COLOR=1`(见相关误区)—— 许多读屏用户已全局设置。检测已知的可访问终端环境变量(`TERM_PROGRAM=Speakup`、`SCREEN_READER_RUNNING`)并降级为纯文本。TUI 应提供 `--plain` / `--no-color` 开关,优先使用 `error:`、`ok:` 等语义前缀而非仅靠颜色。ARIA 等价机制不适用 —— 终端没有 DOM,唯一的干预就是「不输出」那些字节。

    参考序列SGR 30–37 — 前景色(8 种基础色)