Some Useful Terminal Escape Sequences

最近学习到了一些 Terminal Escape Sequences, 其中尤其对 OSC52 相见恨晚. 这里稍微记录一下各种 Sequences.

Terminal Escape Sequences 是终端应用向 stdout 打出的一些具有特殊含义的字符串. 终端看到这些串之后不会显示它们, 而是执行这些串所对应的终端高级功能.

Color & Rendering

最常见的 escape sequence 就是改变字的颜色. 例如, 这个命令会打印出红色的 " Hello World ":

printf "\e[31mHello World\e[0m\n"

终端颜色最初只有 8 种, 而如今多数终端已经支持 种的 truecolor 了. 这个命令会打印出 RGB 为 (255, 100, 0) 的 " Hello World ":

printf "\e[38;2;255;100;0mHello World\e[0m\n"

不过大多数应用还是只使用 8 种颜色. 丰富的颜色主要在代码高亮里比较有用: vim 中使用set termguicolors 来打开 true color 支持, 之后便可以用 truecolor 来配置各个 highlight group 的 guifg 和 guibg.

这个有用的脚本可以打出终端支持的各种颜色, 以及其他渲染特性, 可惜大多数都没有什么应用在用. Kitty 终端下的输出是这样的:

Clipboard (OSC52)

在支持的终端里执行以下命令会将 "Hello World" 复制到剪贴板.

printf "\e]52;c;$(echo "Hello World" | base64)\a"

这个 escape sequence 一般称作 "OSC52", 其中 OSC 是 "operating system command" 的意思. OSC52 科学的解决了一个困扰我十多年的问题: 怎么在 ssh + vim/tmux 的时候复制终端上的文字到本地剪贴板?

终端自带的选中 + 复制的功能并不能很好的与 vim/tmux 这类有 "窗口" 的终端应用一起工作, 因为:

  1. 无法选中超过一屏的文字. 因为终端的选中功能无法对 vim/tmux 里的窗口进行翻页.
  2. 经常会被迫选中终端应用的 UI 字符, 尤其是当应用有多个窗口的时候. 例如当我想要选中右边两行时:

如果 vim/tmux 运行在本地, 这些问题都很容易解决: vim/tmux 各自提供了自己的选中 + 复制功能, 并且都可以读写本地的系统剪贴板. 但是当它们跑在 ssh 里的时候, 我就只能依赖 hack:

  • 如果要选中超过一屏的文字, 就把字体缩小试试能不能一屏装下..
  • 让 vim/tmux 各自把 UI 尽量关掉 (例如把窗口独立出来, 让 vim 不显示行号等等).
  • 以上都不 work 的时候就没办法了. 曾经尝试过 piknik, 但是使用太复杂了.

有了 OSC52 之后再也不会有这个问题了: ssh 里的应用只要输出了 OSC52 的控制字符, 被本地的终端看到了, 就可以写入本地的剪贴板. 具体方案可以这样:

  1. vim 里使用 vim-oscyank 插件在复制时输出 OSC52 字符.
  2. tmux 里使用set-clipboard on 选项. 这个选项同时做了两件事 (感觉官方 wiki 解释的并不清楚):
    • tmux 会将 copy-mode 里选中并复制的内容经由 OSC52 输出.
    • tmux 内运行的应用输出的 OSC52 控制字符会被 tmux 正确的转发到外面. 这样如果 tmux 里运行了 vim, 也能正确工作.
  3. 另外搞了个简单的yank 脚本用于命令行: $ run_some_command | yank.

在支持的终端里, 这个命令会输出 "This is a link", 鼠标点击输出的文字会打开 "example.com":

printf '\e]8;;http://example.com\e\\This is a link\e]8;;\e\\\n'

我与其他人共同维护了一个文档, 记录了支持 OSC8 hyperlink 的终端和会使用 OSC8 hyperlink 的应用.

由于大部分终端已经有了基于 regex 匹配文字中的 URL 的功能, 因此 hyperlink 的功能并不是十分刚需, 可能还需要应用开发者发挥更多想象力. 目前我用到的场景仅有:

  1. ls --hyperlink=auto. 使用了这个 alias 之后可以在终端里点击文件名打开文件.
  2. 在 source control 工具里, 点击 commit 打开对应的网页 (例如 github, bitbucket).
    • 类似的, 希望在 git prompt 里点击也可以打开对应 repo 或 branch 的网页.
    • Google 内部的 source control 有这些功能. 希望有人能给git log 做一个类似的.
      • UPDATE: git 可以使用 delta
  3. mdcat, 但是并不常用.

Kitty Graphics Protocol

Kitty 终端自己发明了一套 escape sequence 用于在终端中显示图片. 这样显示的图片不是用彩色的 unicode 字符拼出的高糊图, 而是正常的高清图. timg 是一个支持 Kitty protocol 的看图工具. 有了它就可以在 ssh 的时候看远端的图片了.

要注意的是, tmux 并不支持这个非标准的 protocol, 会把相应的 escape sequence 吞掉. 所幸, tmux 提供了 "passthrough" 功能: 在打开了allow-passthrough on 之后, 使用特殊的 passthrough escape sequence 可以让 tmux 把应用打出的 escape sequence 转发到外层. 由于 tmux 不支持, 在 tmux 下看图还会有位置错乱的问题. 我搞了一些 hack 勉强解决了, 就不过多解释了.

Desktop Notification (OSC9 / OSC99)

在支持的终端里, 这两个命令会弹出 "Hello World" 的通知:

printf '\e]9;Hello World\e\\'
printf '\e]99;;Hello World\e\\' # Only in kitty

主要的用途是让 ssh 远端的程序给本地发通知. tmux 同样不支持这个 sequence, 需要配合 passthrough 使用.

OSC9 的出现比较早, 兼容性会更好. OSC99 是 kitty 自己发明的版本, 支持更丰富的通知格式.

Window Title (OSC0)

这个命令让上层设置当前的窗口标题. 具体做什么由上层 (tmux 或终端) 实现决定:

printf '\e]0;Hello World\a'

用处不大. 主要是可以让 shell 自动设置标题为 PWD 或当前在执行的命令, 这样当存在多个 tmux tab 或终端 tab 的时候可以方便区分. zsh 可以这样:

function _my_update_title_cmd() { echo -ne "\e]0;${1%% *}\a" }
function _my_update_title_pwd() { echo -ne "\e]0;${(%):-"%3~"}\a" }
autoload -Uz add-zsh-hook
add-zsh-hook preexec _my_update_title_cmd
add-zsh-hook precmd _my_update_title_pwd

最后吐点槽.

有不少有用的终端 feature 仅存在于一两个终端里: 读剪贴板, 传输文件, 看图看视频, 进度条, 鼠标悬浮时显示 tooltip...

终端 feature 一直缺乏标准化: 很多 escape code 并没有详细的 spec -- 一个新的终端开发者基本上主要靠看其他终端的代码来理解它们的行为. 另外, 一些终端自己发明的 escape code 甚至会互相冲突 -- 用了别人已经用过的字符.

每个终端只会实现一部分它认为有价值的终端 feature. 这就使得大部分应用为了兼容性都不会去使用高级的 feature.

即使一个应用想要使用高级的 feature, 它也没有一个好的方法判断终端是否支持一个 feature. 在这里我发现了一个类似于浏览器的 User-Agent 的故事:

  • 最初, xterm 非常流行并且实现了各种高级 feature. 那个时候, 应用判断$TERM 环境变量里有没有 xterm 来决定要不要使用这些 feature, 判断$TERM 里有没有 "256color" 来决定要不要使用 256 色输出.
  • 后来, 更多的终端支持了这些 feature, 但是由于终端名字里没有 xterm, 应用不会使用这些 feature, 所以各个终端都把 "xterm" 加入自己的名字.
  • 直到今天, gnome-terminal, konsole, iTerm, sakura 等大多数终端默认的$TERM 名称还是 "xterm" 或 "xterm-256color". kitty 终端的名字是 "xterm-kitty".

terminfo 允许应用查询终端是否支持特定 feature, 但由于 feature 缺乏标准化, terminfo 也并没有很好的解决这个问题.

在这个混乱的情形下, 终端开发者也难以达成共识. 曾经几个终端开发者组织了个 terminal working group 来讨论各种 feature 的提案, 但最后大家不欢而散. 这个帖子记录了组织者的吐槽.

由于这些原因, 终端 feature 的演化已经基本陷入停滞. 只有少数几个终端在自己发明新 feature: 例如 iTerm2 的自酿 feature kitty 的自酿 feature. 但由于缺乏标准化, 没有对社区产生太多影响.

终端是我工作的主力工具, 希望这些问题能得到解决.

Comments