跳到主要内容
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=dumbCLICOLOR=0

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

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

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

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

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

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

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

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

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

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

    成因发送了 \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>')—— 切换的两半都发。仍不行就 resettput 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[JEL \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 把 TESCDELAY 环境变量)默认为 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 colstput cols)—— 若它在非 allowC132 类模拟器下报固定的 80 / 132,则当前为嵌套环境。现代方案:用 TIOCGWINSZioctl)或 $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)。在现代终端里两者看起来一样,但在黑白 TTYLinux 控制台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)画一个平滑渐变,在 GhosttyWezTermkittyiTerm2Windows 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 termcolorColorChoice::Auto 已做此事;Go fatih/color v1.16+ 检查 $COLORTERM;Python richConsole(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=SpeakupSCREEN_READER_RUNNING)并降级为纯文本。TUI 应提供 --plain / --no-color 开关,优先使用 error:ok: 等语义前缀而非仅靠颜色。ARIA 等价机制不适用 —— 终端没有 DOM,唯一的干预就是「不输出」那些字节。

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

  21. 21\x1b[0;0H(或 \x1b[0H)看起来没有效果 —— 光标停在原位,或比预期靠下/靠右一行一列。

    成因CUP(\x1b[<row>;<col>H)由 ECMA-48 §8.3.21 规定为从 1 开始计数 —— \x1b[1;1H 是左上角,不是 \x1b[0;0H。按 §5.4,省略的 CSI 参数默认为 1,所以 \x1b[H(无参数)等价于 \x1b[1;1H,而非 \x1b[0;0H。多数终端把 0 静默归为 1,因此症状常常只在你用从 0 基坐标系(canvas / framebuffer / DOM)换算来的 \x1b[<top-1>;<left-1>H 时才显现。

    修复从 0 基坐标系迁移时记得加 1:printf '\x1b[%d;%dH' (row + 1) (col + 1)\x1b[H 是「回到左上角」的惯用简写。另外 CUP 顺序是行;列y;x),不是 x;y —— 来自 canvas / GL 坐标 API(x 在前 y 在后)的人需调换参数。\x1b[H\x1b[2J 是「清屏并回到左上角」的通用范式;单发 \x1b[2J 不会移动光标。完整 CUP 字节规范见 /sequence/cursor-position

    参考序列CUP — 设置光标位置

  22. 22你的 clear 风格命令发了 \x1b[2J,可视区域清空了,但用户向上滚动还能看到刚刚的内容。更糟的是 —— 用户的 clear 别名能清回滚缓冲而你的 TUI 不能,于是用户报告你「泄漏历史」。

    成因ED(\x1b[<n>J,ECMA-48 §8.3.39)有四种选择模式:0(光标到屏幕末,默认)、1(屏幕首到光标)、2(整个可视屏幕)和 3(整个屏幕包含回滚缓冲,xterm 扩展,xterm-ctlseqs 记为 Ps = 3)。\x1b[2J 是回滚出现之前定义的 —— 它针对可视屏幕,不针对模拟器保存历史行的环形缓冲。xterm 作者 2002 年加入 3 正是因为用户想要清回滚而标准没有。Linux console、kitty、alacritty、wezterm、Windows Terminal、iTerm2、gnome-terminal、macOS Terminal.app 都支持 3J;老式 DEC VT 与一些嵌入式终端会静默忽略。

    修复按需选择。如果希望用户仍能上滚查看历史(TUI 重绘、全屏菜单),用 \x1b[H\x1b[2J —— 仅清可视区,保留回滚。如果要真正的 clear 等价(欢迎屏、安全敏感 CLI),发 \x1b[H\x1b[2J\x1b[3J —— 3J 扩展会清掉环形缓冲。ED 移动光标;惯用做法是配 \x1b[H 回到左上角。注意 macOS Terminal.app 仅在 Edit > Clear Buffer 快捷键策略保持默认时遵循 3J;iTerm2 等其他终端无条件遵循。完整四模式字节表见 /sequence/erase-display

    参考序列ED — 清屏 (`\x1b[2J`)

  23. 23程序写够 80 个字符填满第 1 行后,后续 CSI 光标定位指令看起来差一格 —— \x1b[1;1H 不能干净复位,或 \x1b[B(光标下移)一次跳两行而非一行。如果 DECAWM 关闭,症状不同:超过右边界的字符被静默丢弃而非换行,状态行文字消失。

    成因DECAWM(\x1b[?7h / \x1b[?7l,DEC 私有模式 7,所有现代终端默认开)定义打印字符到达最右列时的行为。DECAWM 时,光标停在 N 列(最后一列)的「待换行」状态 —— 直到下一个打印字符到达才进入下一行第 1 列。这期间发的光标位置查询返回行 R / 列 N,而不是直觉以为的行 R+1 / 列 1。CSI 光标移动(\x1b[B\x1b[C)会清掉「待换行」标记并按从 N 列出发执行。DECAWM \x1b[?7l)时,超出字符被静默丢弃 —— 光标停在 N 列,后续字符原地覆盖写入。

    修复交互式 TUI:写完最右列后不要靠列位置算式 —— 每次状态行重绘前显式发 \x1b[<row>;<col>H(CUP)消除歧义。不希望换到下一行的状态行(右沿时钟、滚动指示):写之前关掉 DECAWM \x1b[?7l,写完恢复 \x1b[?7h,并在代码里自行截断 —— 别指望终端在跨模拟器上都优雅丢弃溢出(Linux console 与 xterm 在此边缘行为不同)。DSR \x1b[6n 光标位置查询:注意「待换行」状态下回复是 R;N 而非 R+1;1,解析代码要容忍。完整 DECAWM 字节规范见 /sequence/dec-line-wrap,光标回报机制见 /sequence/csi-dsr

    参考序列DECAWM ?7 — 自动换行模式

  24. 24同一条 256 色 SGR 指令在不同终端看起来差异明显 —— 在 xterm 调成「橙色」的 \x1b[38;5;208m,到 iTerm2 上变「棕色」,macOS Terminal 默认 Basic 配色变「土红色」,solarized / dracula / one-dark 等主题下又略微脱饱和。

    成因xterm 256 色调色板分三段(ECMA-48 §SGR-Indexed-Color、xterm-ctlseqs §256-Color):0–15 槽是用户可配的 ANSI / aixterm 16 色(由配色 / 主题映射),16–231 槽是 6×6×6 RGB 立方(16 + 36*r + 6*g + br,g,b ∈ 0..5,分量值依次对应 0/95/135/175/215/255),232–255 槽是 24 阶灰度梯度。6 色立方区段是规范化的,但立方的锚点值并非如此 —— 终端只在文档里承诺索引→RGB 函数,不少终端故意覆盖。iTerm2 有 Indexed Colours 配置(Profile → Colours → Color Presets)整体覆盖 16–231 槽;macOS Terminal Basic 配色重新平衡了中间调;真彩色模拟终端(kitty、alacritty、wezterm)严格遵守 xterm 立方。具体到 208 槽:xterm 立方 → (5,2,0)(255,135,0)(橙色);iTerm2 默认 → (225,135,40)(偏棕);macOS Terminal Basic → (220,120,40)(在亮背景下接近土红)。

    修复优先语义而非索引。若需品牌色精准(logo 橙、跨机器一致的状态绿),用真彩色 \x1b[38;2;255;135;0m(用 $COLORTERM=truecolor 守门)—— 6 色立方只能给「大致橙色」。若需稳定标识(CI / 日志高亮,关心角色而非视觉),用 0–15 槽 —— 与用户配色绑定,所以红色是用户口中的红色,这才是对的取舍。用 6 色立方做用户会跨模拟器并排比较的事 —— 他们会比较,差异是真实的。调色板调试:在每个目标终端跑 for i in {0..255}; do printf '\x1b[48;5;${i}m %3d \x1b[0m' $i; done 视觉对比。库支持:Python rich、Rust crossterm 自带 6 立方→RGB 量化器;Go fatih/color 交由终端解释。完整规范见 /sequence/sgr-fg-256,真彩色逃生口见 /sequence/sgr-fg-truecolor

    参考序列SGR 38;5;n — 256 色前景

  25. 25你发 \x1b[38;5;208m WARNING: disk full 然后单纯 \n,下一段程序输出(有时连用户的 shell 提示符)会沿用 208 号色。这与未重置的 \x1b[31m 是同一族「颜色泄漏」问题,但 256 色槽常常看起来「差不多正常」,泄漏只在读到提示符时才明显。

    成因SGR 状态是粘性的 —— 终端会保留当前的前景 / 背景 / 属性,直到你显式改它。\n 是 C0 LF(\x0a),只让光标下移,复位 SGR。窄复位 \x1b[39m 仅把前景恢复为默认(也就是 shell 提示符选的颜色,并非「无色」)。全复位 \x1b[0m 清掉一切(fg、bg、bold、dim、italic、underline……)。256 色相关的工具范例常省略尾部重置,孤立测试时 OK,集成进真实 CLI 输出就泄漏。加剧因素:用 printf / echo 的人常写 printf '\x1b[38;5;208mWARN\n' 而不是 printf '\x1b[38;5;208mWARN\x1b[0m\n' —— 差就差一段转义。

    修复换行前与退出时都要重置。惯用范式是 \x1b[<set>m<text>\x1b[0m\n —— 重置在换行之前而非之后,因为如果下游工具把输出管到分页器 / less / Slack 转义剥离器,尾部换行可能是截断前的最后一字节。长生命周期 TUI 应在 atexit / 信号处理器里发 \x1b[0m —— 即便用户中途 Ctrl-C,提示符也能恢复干净。更窄重置:仅设前景时 \x1b[39m 是精确还原(保留 bg 与属性);背景对应 \x1b[49m,单属性对应 \x1b[22m(关 bold)、\x1b[24m(关 underline)等。与其他样式组合时用窄重置。SGR-0 家族误区见 /pitfalls#color-bleed,重置字节速查见 /sequence/sgr-reset

    参考序列SGR 38;5;n — 256 色前景

  26. 26从 0 基 canvas 思路(顶行 0、24 行终端下页脚是行 23)出发,发 \x1b[0;23r 想保留上下页边距 —— 实际滚动行为完全不符:滚动会跨过想保留的静态行,或完全无视 DECSTBM 把整屏当作滚动区。从另一头同样翻车:在 25 行终端上发 \x1b[2;24r,期望 2..24 行滚动、第 25 行作状态栏,结果状态栏在第一次滚动时就消失。

    成因DECSTBM(\x1b[<top>;<bottom>r,DEC 私有,参见 xterm-ctlseqs §DECSTBM)使用从 1 起、包含端点的行索引。24 行终端上行编号 1..24:\x1b[1;24r 是「整屏滚动」(默认),\x1b[2;24r 保留第 1 行作静态页眉,\x1b[1;23r 保留第 24 行作静态页脚,\x1b[2;23r 同时保留第 1 与第 24。top=0 的 DECSTBM 按规范未定义 —— 多数终端静默归为 top=1,所以症状是「行为不符预期」而非「报错」。另外:DECSTBM 会把光标移到滚动区内部的 (1,1)(VT100 / xterm-ctlseqs 行为),所以发完 DECSTBM 后旧的 CUP 位置可能与你以为的不同。底必须 ≥ 顶的约束会静默拒绝反向区间(\x1b[24;2r),xterm-ctlseqs 记录了「回退到上一区域」的 no-op 行为。

    修复在边界上换算,别在脑子里换算。应用内部用 0 基行号,那就在调 DECSTBM 那一刻做 +1printf '\x1b[%d;%dr' (top + 1) (bottom + 1)。把行 1 当作「第一行可见」,N 行终端把 N 当作「最后一行可见」。N 用 tput lines 取(拿不到 shell 时退而用 DSR \x1b[18t),不要硬编码 24 或 25。DECSTBM 后总是显式跟一个 CUP(\x1b[<row>;<col>H)—— DECSTBM 把光标搬到区内 (1,1) 的副作用很少是你想要的,宁可覆盖也别记。嵌套 TUI(tmux、screen)场景:发 DECSTBM 前先按 $TERM 探测多路复用器的支持情况 —— 复用器可能改写或截断滚动区。字节规范见 /sequence/decstbm

    参考序列DECSTBM — 设置上下边距(CSI r)

  27. 27TUI 设了滚动区(\x1b[2;23r 保留页眉页脚)后崩溃 / 被 kill,没复位。用户回到 shell 输入命令 —— 输出只在 2..23 行内滚动;提示符可能钉在区内最后一行,而原页眉(现在是 shell 输出)和原页脚(现在是提示符碎片)静止不可访问。clear 也救不了。改变窗口大小有时能修(部分终端在 SIGWINCH 时重置 DECSTBM),有时更糟。

    成因DECSTBM 是持久性终端状态 —— 一旦发了 \x1b[<top>;<bottom>r,滚动区一直生效,直到终端收到另一条 \x1b[r(或 \x1b[1;<N>r,N = 当前行数,二者都表示「整屏」)、RIS 复位(\x1bc)或 DECSTR 软复位(\x1b[!p)。它在 TUI 退出、shell 重画提示符、clear(其实就是 \x1b[H\x1b[2J —— 都不动 DECSTBM)或子进程 fork 时复位。Alt-screen 退出(\x1b[?1049l在 xterm 与多数现代终端上复位 DECSTBM(属于 alt-screen 对的保存状态契约)—— 但仅当 TUI 真的用了 alt-screen。在主屏崩溃且设了 DECSTBM 的 TUI 会卡死用户。

    修复DECSTBM 必须配清理。把滚动区设置当 try/finally 资源:正常退出、各信号处理器(至少 SIGINT、SIGTERM、SIGHUP)、Drop / defer 块里,在交还终端之前\x1b[r(整屏滚动)。长生命周期 TUI 优先 alt-screen + DECSTBM 一起用,让 alt-screen 退出处理器代你清理 DECSTBM。用户侧恢复:解卡咒语是 printf '\x1b[r'(或 tput rs1,或 coreutils 的 reset —— 但 reset 会重探 $TERM 较慢);在 TUI 的 troubleshooting / README 里写明白。指望用户知道。需要一起测试的恶性组合:DECSTBM + DECCKM + DECKPAM 都设、都不复位 —— 这就是「崩溃的 nvim 留下方向键、提示符、滚动三重失灵」。字节规范见 /sequence/decstbm,DECCKM 持久化误区见 /pitfalls#stuck-app-mode

    参考序列DECSTBM — 设置上下边距(CSI r)