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 两套都认。

标记序列含义
AOSC 133 ; A 或 OSC 633 ; A提示符开始
BOSC 133 ; B 或 OSC 633 ; B提示符结束,命令输入开始
COSC 133 ; C 或 OSC 633 ; C命令已提交,输出开始。可以不发
DOSC 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 用 ⌘⇧。

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 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 的退出码都丢掉。一块只是主屏幕上的一段格子,用来跳转和复制,不是工作目录,也没有「再跑一次」按钮。

相关文章

← 第 8 篇 · 系列目录 · 第 10 篇 →