OSC 133 命令块:shell 打上的标记,终端不去猜
「SSH 终端是怎样做成的」第 9 篇。这一篇写的是 MistTerm v1.2.38(AGPL),不是第 1–8 篇里的 v1.2.26 快照。依据是 src/terminal/command_block.rs、docs/tech/TERMINAL-BEHAVIOR.md,以及操作手册 6.5 节。
要点
回滚历史是一格一格的字符,它不知道 ls 在哪结束、下一条提示符从哪开始。OSC 133,以及 VS Code 那套同形的 OSC 633,是 shell 自己把边界打出来:提示符开始、提示符结束、可选的输出开始、命令结束和退出码。MistTerm v1.2.38 按这些标记跳转、复制。没有标记时,它不会拿回车或「像提示符的一行」去编一块。没改过的 bash 默认一个标记都不发。
第 1 篇讲到解析器和网格就停了。网格画完之后,「上一条命令」仍然是人眼看出来的,模拟器并没有单独存这么一个对象。提示符、你敲的那一行、一屏输出,都躺在同一段回滚里。想只要那次 ls 的输出,就得自己选。
按回车去猜,猜不准
有的终端会猜:每次回车当边界,或者任何以 $ 结尾的短行都当提示符。很快就会错。命令可以跨行。提示符未必以 $ 结尾,路径里倒可能有一个。 vim、less、htop 会切到备用屏幕(CSI ?1049 h,第 1 篇那个模式),满屏都是和「一条命令」无关的行。
v1.2.38 的规则是不猜。主屏幕上没见过 OSC 133 或 OSC 633,上一条 / 下一条 / 复制当前块就不会滚动视图,按键也不会转发给 shell。会弹出一条提示,指向手册。右键菜单照样放着这三项,缺了 shell integration 时你能看见,而不是觉得快捷键没了反应。
四个字母
OSC 和第 1 篇里的窗口标题是同一类序列:ESC ],一段负载,然后 BEL(0x07)或者字符串终止符 ESC \。FinalTerm 和 iTerm2 的 shell integration 用四个字母。VS Code 用 OSC 633,字母相同,所以 MistTerm 两套都认。
| 标记 | 序列 | 含义 |
|---|---|---|
| A | OSC 133 ; A 或 OSC 633 ; A | 提示符开始 |
| B | OSC 133 ; B 或 OSC 633 ; B | 提示符结束,命令输入开始 |
| C | OSC 133 ; C 或 OSC 633 ; C | 命令已提交,输出开始。可以不发 |
| D | OSC 133 ; D [; 退出码] | 命令结束。第一段十进制数是退出码 |
字母后面的附加参数,比如 iTerm 的 cl=、aid=,丢掉。只保留 D 的第一段数字。这一族里还有几样根本不当块边界:OSC 1337(iTerm 的属性,常见的当前目录就走这里)、OSC 133 ; P(VS Code 把 Cwd 放在这里)、标题用的 OSC 0/2。
所以这里的「一块」是主屏幕上的一段格子。它不是当前目录的记录。v1.2.38 也没有「把这一块再执行一遍」的动作,界面上就是跳转和复制。
这些序列不靠 $TERM 协商。terminfo 不会让 bash 自己去打印它们。要嘛写在 PROMPT_COMMAND 里,要嘛写在 zsh 的 precmd / preexec 里,要嘛那台机器上已经开了 iTerm2 或 VS Code 的 shell integration。远端账号的 ~/.bashrc 要是原样,一个标记都没有,终端没法自己通过 SSH 去要。
标记怎么变成一块
坐标在主屏幕网格上,行滚进历史时跟着平移。备用屏幕不取样。进了 vim 或 less 的那条命令仍然是一块,标成 fullscreen;复制的是主屏幕上的命令,不含全屏那一帧。
能跳过去的块,至少要见过 C 或 D。光有 A,或者 A 之后 B、人还在敲,都不是一条命令,「上一条」不会停在空提示符上。复制优先从 B 开始,这样不含提示符,终点是 D。命令还在跑,终点就是当前光标。下一次 A 会把还开着的块收口,但不把它标成已结束。
复制有上限,免得一次 yes 或一大段日志在界面线程上摊成一整串。最多 4000 行网格、20 万个字符,留开头。行数到顶的后缀是 \n… [truncated],字符到顶是 \n… [truncated, N chars omitted]。
列宽一变,存下来的坐标就丢。集成仍然算「见过」,旧的点不再用。清掉历史时,终点已经在新顶行之上的块也丢掉。
第 10 篇的本地日志可能从同一条 D 里读退出码。那次读取不是界面上的块边界。块列表和 JSONL 是两个消费者。
终端有焦点时,这些快捷键在写进 PTY 之前就被吃掉。Windows 和 Linux 用 Ctrl+Shift,macOS 用 ⌘⇧。
- Ctrl+Shift+↑:上一块
- Ctrl+Shift+↓:下一块。已经是最后一块时,回到最新输出
- Ctrl+Shift+O:复制当前块(命令和输出)
shell 配置里要有什么
那台机器上如果已经开了 iTerm2 或 VS Code 的 shell integration,留着就行。否则手册里最短的 bash 片段是下面这段。D 在下一条提示符画出来之前跑,这时 $? 还是上一条命令的退出状态。A 标出新提示符。B 塞进 PS1,落在提示符文字后面。C 可以不写;只有 A / B / D,命令结束后仍然能成一块。
__mist_cmd_block() {
local ec=$?
printf '\e]133;D;%s\a' "$ec"
printf '\e]133;A\a'
}
PROMPT_COMMAND="__mist_cmd_block${PROMPT_COMMAND:+;$PROMPT_COMMAND}"
PS1="${PS1}"'\[\e]133;B\a\]'
zsh 是同样的字母:precmd 打 D 和 A,preexec 打 C,PS1 带 B。新开一个标签页,shell 才会重新读配置。这段就是普通 shell 代码。能改 rc 的人可以删掉,任何程序也都能打印同样的字节。和第 6 篇里的带内标记一样:用来跳转和复制够用,拿来当安全边界没有意义。
小结
- 命令块是 shell 标出来的一段,不是终端按回车推断出来的。
- v1.2.38 解析 OSC 133 和 OSC 633 的 A/B/C/D。OSC 1337、
OSC 133 ; P和标题序列不认,所以不会从那些序列里拿当前目录。 - 跳转和复制都在主屏幕上。全屏程序仍是一块,复制不含备用屏幕。
- 没有标记就不滚动,也不把快捷键发给 PTY。菜单还在,并指向手册。
常见问题
没有 shell integration,终端知道一条命令从哪开始吗?
靠不住。回滚历史是一格一格的字符,不是命令列表。MistTerm v1.2.38 不会按回车、也不会按「看起来像提示符的一行」去编一块。它只认 shell 发出的 OSC 133 或 OSC 633。
OSC 133 的 A、B、C、D 分别是什么?
A 是提示符开始,B 是提示符结束、命令输入开始,C 可选,表示输出开始,D 表示命令结束。D 的第一段十进制数是退出码。OSC 633 用同样的字母。
MistTerm 会从 OSC 133 记下当前目录吗?
不会。v1.2.38 不解析 OSC 1337 和 OSC 133 ; P,字母后面的附加参数除了 D 的退出码都丢掉。一块只是主屏幕上的一段格子,用来跳转和复制,不是工作目录,也没有「再跑一次」按钮。