一次 tmux + opencode 中文变下划线的排障:不是字体坏了,而是 client_utf8=0
先说结论
这次问题不是 opencode 不支持中文,也不是终端字体突然坏了,而是 tmux 当前 client 被判定为“不支持 UTF-8”。证据是
tmux display-message -p 'client_utf8=#{client_utf8}'返回了client_utf8=0。在这种状态下,tmux 会把非 ASCII 字符替换成下划线,所以中文在 opencode 的 TUI 里看起来就像全部变成了_。
这篇文章记录一次很小、但很典型的终端排障:同一台 Mac 上,直接运行 opencode 时中文显示正常;先进入 tmux,再运行 opencode,中文就全部变成下划线。
这种问题非常容易误判。第一反应通常是怀疑字体、终端模拟器、opencode 版本、主题、Nerd Font、宽字符宽度,甚至怀疑是不是某个 TUI 框架把 CJK 字符处理坏了。但这次真正的关键点只有一个:tmux 自己是否愿意把 UTF-8 字符写回外层终端。
为了不泄露任何私人环境信息,下面的命令输出都做了抽象化处理,不包含真实主机名、内网地址、会话名、密钥、账号或项目路径。保留的只有与问题本身有关的技术事实。
图 1:同一个 opencode,直接运行正常;经过 tmux 后中文变成下划线。
1. 问题现象:只有进 tmux 之后才坏
现象非常明确:
- 在普通终端里直接执行
opencode,中文菜单、中文提示、中文内容都能正常显示。 - 执行
tmux new -s work进入 tmux 会话。 - 在 tmux pane 里再次执行
opencode。 - 原来应该显示中文的地方,全部变成下划线。
这个边界很重要。因为如果直接运行 opencode 就已经乱码,那排查方向应该从字体、locale、终端编码、应用自身编码开始;但现在直接运行正常,只有 tmux 内部异常,就说明 opencode 的中文输出能力本身大概率没问题。真正出问题的位置,应该在“opencode 输出之后、字符到达终端之前”的那一层。
也就是说,链路大概是这样:
opencode -> tmux pane -> tmux client -> 外层终端
直接运行时链路更短:
opencode -> 外层终端
两条链路唯一明显不同的中间层就是 tmux。因此这次排障没有先去升级 opencode,也没有先换字体,而是先检查 tmux 内外的环境差异。
2. 第一轮检查:locale 看起来没坏
遇到中文显示问题,最先看的通常是 locale:
locale
printf 'TERM=%s\nLANG=%s\nLC_ALL=%s\nLC_CTYPE=%s\n' "$TERM" "$LANG" "$LC_ALL" "$LC_CTYPE"
在这次环境里,tmux pane 内看到的是 UTF-8 语言环境,例如:
TERM=tmux-256color
LANG=en_US.UTF-8
LC_ALL=
LC_CTYPE=
注意这里有一个容易被忽略的细节:LANG 是 UTF-8,但 LC_CTYPE 变量本身是空的。很多程序会根据 LANG 推导出 UTF-8 locale,所以你运行 locale 时仍然可能看到 LC_CTYPE="en_US.UTF-8"。这会让人误以为“编码环境没问题”。
但 tmux 对“是否向外层终端写 UTF-8”的判断并不只看 pane 内程序看到的 locale。它有一个更直接的 client 状态。这个状态才是本次问题的关键。
3. 关键证据:client_utf8=0
真正把问题钉住的是这条命令:
tmux display-message -p 'client_termname=#{client_termname} client_utf8=#{client_utf8}'
当时看到的关键输出是:
client_termname=xterm-256color client_utf8=0
client_utf8=0 的含义非常直接:tmux 当前认为这个外层 client 不支持 UTF-8 输出。于是它为了避免把“终端可能不理解的字符”写出去,会采用保守策略,把非 ASCII 字符替换成下划线。
这就解释了为什么现象不是普通乱码,而是非常规律的下划线:
- opencode 输出了中文。
- tmux pane 收到了这些 UTF-8 字符。
- tmux client 判断外层终端不支持 UTF-8。
- tmux 在输出边界把非 ASCII 字符替换为
_。 - 终端最终看到的是一串下划线。
这个行为不是 opencode 的 bug,也不是字体缺字。字体缺字通常会表现为方框、豆腐块、问号或空白;而这里是 tmux 按规则替换出来的下划线。
图 2:排障重点不是“中文内容能不能生成”,而是 tmux client 是否允许 UTF-8 通过。
4. 为什么直接 opencode 正常,tmux 里却坏
这类问题最迷惑的地方就在这里:同一个终端、同一个字体、同一个 opencode,为什么只要中间加了 tmux 就坏?
原因是 tmux 不是透明管道。它既是终端复用器,也是终端能力的协商者。pane 里的程序并不是直接面对外层终端,而是面对 tmux 提供的虚拟终端。tmux 再把内容转发给外层真实终端。
所以一件事要分成两层看:
- pane 内部环境:opencode 看到的
TERM、LANG、LC_CTYPE是什么。 - tmux client 环境:tmux 在连接外层终端时,是否判断这个 client 支持 UTF-8。
这次 pane 内部看起来已经是 UTF-8,但 tmux client 状态仍然是 client_utf8=0。这说明问题不在 opencode 和 pane 内部 locale,而在 tmux 连接外层终端时的判断。
tmux 手册里对这一点写得很清楚:如果没有显式使用 -u,tmux 会根据相关 locale 变量判断是否使用 UTF-8 输出;否则非 ASCII 字符会被替换成下划线。这里的“下划线”正好和现象完全吻合。
5. 最小修复:让 tmux 明确使用 UTF-8
既然根因是 tmux client 没开 UTF-8 输出,修复就不应该从 opencode 下手,而应该让 tmux 在连接外层终端时明确使用 UTF-8。
最小修复命令是:
tmux -u new -s work
如果是连接已有会话,则使用:
tmux -u attach -t work
这里的 -u 很关键。它的作用不是改变 pane 内程序的语言,而是告诉 tmux:即使环境变量判断不够明确,也要向外层终端写 UTF-8。
为了避免每次都手写 -u,可以在 shell 配置里加一个 alias:
alias tmux='tmux -u'
这样以后继续使用习惯命令:
tmux new -s work
实际执行时也会带上 -u。
6. 进一步加固:把 locale 和 tmux 配置写明确
只加 tmux -u 已经能解决这次最核心的问题。但为了让环境更稳定,我更建议同时把 shell locale 和 tmux 配置写明确。
在 ~/.zshrc 或你使用的 shell 配置里加入:
export LANG=en_US.UTF-8
export LC_CTYPE=en_US.UTF-8
alias tmux='tmux -u'
这里单独设置 LC_CTYPE 是有意义的。LANG 虽然能作为默认 locale,但一些程序或中间层会更直接地读取 LC_CTYPE 来判断字符分类和编码能力。把它显式写出来,可以少掉很多“看起来是 UTF-8,实际边界没有继承”的不确定性。
再补一个最小 ~/.tmux.conf:
set -g default-terminal "tmux-256color"
set -gq set-clipboard on
set -as terminal-features ",xterm-256color:RGB"
set -as terminal-features ",tmux-256color:RGB"
set-environment -g LANG "en_US.UTF-8"
set-environment -g LC_CTYPE "en_US.UTF-8"
这几行做了三件事:
- 把 tmux 内部默认终端类型固定为
tmux-256color。 - 给常见外层终端和 tmux 终端声明 RGB truecolor 能力。
- 让新 pane 内部明确拿到 UTF-8 locale。
需要注意的是,set-environment 影响的是 tmux 里面新启动的 pane 环境;而 client_utf8 是当前 tmux client 的状态。也就是说,source 配置可以改善后续 pane 环境,但已经 attached 的 client 是否 UTF-8,仍然要通过重新连接来改变。
图 3:最稳妥的做法,是把 shell、tmux client 和 tmux pane 三层都配置清楚。
7. 为什么只 source 配置还不够
这次还有一个容易踩的点:修改 ~/.tmux.conf 后执行:
tmux source-file ~/.tmux.conf
这当然有用,但它不会自动把当前已连接 client 的 client_utf8 从 0 变成 1。因为当前 client 是在之前连接 tmux 时就已经完成判断的。
所以更完整的操作应该是:
# 在 tmux 内 detach
# 默认快捷键是 Ctrl-b d
# 回到外层终端后
source ~/.zshrc
tmux -u attach -t work
如果不需要保留已有会话,也可以在确认没有重要任务后重启 tmux server:
tmux kill-server
tmux -u new -s work
但我不建议在排障时随手 kill-server。tmux 里经常挂着长任务、编辑器、日志窗口或远程会话。更稳妥的方式是先 detach,再用 tmux -u attach 重新连接。
8. 验证方法:不要靠眼睛猜
修完以后,至少做三步验证。
第一步,看 shell 环境:
printf 'LANG=%s\nLC_CTYPE=%s\n' "$LANG" "$LC_CTYPE"
期望看到:
LANG=en_US.UTF-8
LC_CTYPE=en_US.UTF-8
第二步,看 tmux pane 环境:
printf 'TERM=%s LANG=%s LC_CTYPE=%s\n' "$TERM" "$LANG" "$LC_CTYPE"
期望看到类似:
TERM=tmux-256color LANG=en_US.UTF-8 LC_CTYPE=en_US.UTF-8
第三步,看 tmux client 状态:
tmux display-message -p 'client_utf8=#{client_utf8} client_termname=#{client_termname}'
期望 client_utf8=1。如果仍然是 0,说明当前 client 还是旧连接,或者启动 tmux 的命令没有带上 -u,需要 detach 后重新 attach。
最后再运行:
opencode
如果中文菜单恢复正常,就说明修复点确实命中了根因。
9. 这类问题的排障顺序
以后再遇到“某个 TUI 在 tmux 里中文异常,但直接运行正常”,我建议按这个顺序查:
- 直接运行程序,确认不是应用自身输出坏了。
- 在 tmux pane 内检查
TERM、LANG、LC_CTYPE。 - 用
tmux display-message检查client_utf8。 - 临时用
tmux -u新开或重连会话,做最小验证。 - 验证通过后,再把
LC_CTYPE、alias 和~/.tmux.conf固化。
这个顺序的好处是,每一步都在缩小范围。不要一开始就换字体、换主题、升级一堆工具。终端问题最怕“同时动太多变量”,最后即使好了,也不知道到底是哪一刀起作用。
10. 一个小总结
这次问题的完整因果链是:
- opencode 直接运行时中文正常,说明 opencode 和终端字体本身不是第一嫌疑。
- 进入 tmux 后中文变下划线,说明中间层改变了字符输出行为。
client_utf8=0证明 tmux 当前 client 没被判定为 UTF-8 输出环境。- tmux 在这种状态下会把非 ASCII 字符替换为下划线,和现象一致。
- 使用
tmux -u、显式设置LC_CTYPE,并补齐~/.tmux.conf后,链路恢复正常。
我喜欢这类排障的原因是,它提醒我们:很多“看起来像应用 bug”的问题,其实发生在边界层。opencode 没变,终端没变,字体没变,真正变化的是输出链路中多了一个会做能力判断的 tmux。
所以最后的经验可以压缩成一句话:
当中文在 tmux 里规律地变成下划线时,先看
client_utf8,再谈字体和应用。