I am LAZY bones?
AN ancient AND boring SITE

端里的链接那些事:从 OSC 到 OSC 8

前两天在自己维护的 Notchy(一个藏在 MacBook 刘 海里的终端)里跑 Claude Code,它回复里带了个 GitHub 链接。同样一条消息,在 iTerm2 里链接是蓝色的 ,下面还有一条虚线,一眼就知道能点;到了 Notchy 里却是白花花的普通文字,非得按住 Cmd、把鼠标挪上 去,下划线才肯冒出来。

同一条 Claude Code 消息,iTerm2 里链接是蓝色带虚 线,Notchy 里是普通文字

我第一反应是:iTerm2 的链接识别比 SwiftTerm 聪明?结果一路查下去发现,根本不是”识别”的问题,而是 Claude Code 压根就给两个终端发了不一样的东西。这里面牵扯到 OSC、OSC 8、终端能力探测,挺有意思的,值得写一篇,分享给有需要的朋友。

OSC 是什么?

终端本质上就是在显示一串字节流,程序想让它做点”显示文字以外”的事,比如改颜色、挪光标、改窗口标题 ,就得在字节流里夹带转义序列(escape sequence)。这些序列按开头分成几大类,大家最熟的是 ESC [ 开头的 CSI,像 \e[31m 变红色就是它。

OSC 是另一类,全称 Operating System Command,格式是:

结尾的 ST(String Terminator)是 ESC \,很多终端也接受一个 BEL 字符(\a)。中间的编号决定干什么,常见的有这么几个:

1. OSC 0 / 2:设置窗口或标签的标题
2. OSC 7:告诉终端当前所在目录(新开标签能继承目录就靠它)
3. OSC 8:超链接,今天的主角
4. OSC 52:读写系统剪贴板(ssh 到远端也能复制到本机,就是它)
5. OSC 133:标记提示符和命令输出的边界,”跳到上一条命令”这类功能靠它

说起来 OSC 0 我早就用过,只是当时不知道它叫这名字。2009 年我写过一篇修改 gnome-terminal 的 title 的办法和杂七杂八的 ANSI 控制码记录,为了区分连着不同服务器的一堆终端窗口,从 Portage 的源码里抄了一句 echo -ne "\e]0;test\a" 来改标题,那正是一个以 BEL 结尾的 OSC 0。文章后半截记的颜色、光标那些,则大多是 CSI。十七年过 去了,还在跟这些转义序列打交道,哈哈。

不认识某个 OSC 的终端会直接把它吞掉,不会显示乱码,这也是 OSC 能一直往上加新功能的原因。

OSC 8:终端里的 <a href>

OSC 8 干的事,就跟网页里的 <a href> 一模一样:把一段文字和一个 URL 绑在一起。

它其实很年轻,2017 年才由 GNOME VTE 的维护者 Egmont Koblinger 提出,规范就是他的这篇 gist。没走什么标准组织,全靠各家终端自觉跟进,当年最早支持的是 GNOME Terminal(VTE 0.50)和 iTerm2 3.1。也就是说,我 2009 年 折腾 OSC 0 的时候,OSC 8 还得再等八年才出生(那会儿终端里的链接,全靠终端自己猜)。格式长这样:

前面一段打开链接,后面一个 URL 为空的 OSC 8 把链接关上,夹在中间的文字就是可点击的部分。参数一般 留空,也可以写 id=xxx,让终端把被换行拆开的同一个链接当成一个整体。

想自己试试的话,在终端里执行:

屏幕上只会出现”点我去 GitHub”几个字,URL 是藏在后面的。我在 iTerm2 和 Notchy 里都跑了一下,两边按住 Cmd 点都能打开 github.com;区别是不按 Cmd 的时候,iTerm2 下面有一条虚线,Notchy 啥也没有(这个后面细说)。

终端里的链接其实有两种

搞明白 OSC 8 之后,就能理解终端里的链接其实分两种来源:

显式链接:程序用 OSC 8 明确告诉终端”这几个字是链接,指向哪”。终端在存每个字符格子的时候, 顺手把链接信息记在格子属性里,所以随时都知道哪里能点,想一直画个下划线也毫无压力。显示的文字也可 以是任意内容,比如 Markdown 里的 [docs](https://...),屏幕上就只显示 docs。

隐式链接:程序就是输出了一串普通文字,终端自己拿正则去扫,猜哪段长得像 URL 或文件路径。这 种只能显示 URL 本身,边界还容易猜错(结尾的标点、括号、紧挨着的中文,都可能被多吃或少吃)。更关键的是正则不便宜,要一直画下划线,就得每次屏幕变化都把整屏扫一遍。所以很多终端(包括 Notchy 用的 SwiftTerm)都只在你按住 Cmd 悬停的时候,才对鼠标底下那一行临时算一次。

所以开头那个现象,原因其实是:在 iTerm2 里,Claude Code 发的是显式链接;在 Notchy 里,发的是普通文字。那条蓝色和虚线,都是显式链接才有的待遇。

程序怎么知道终端支不支持 OSC 8?

问题来了,终端吞掉不认识的 OSC 不会乱码,那程序为啥不干脆一律发 OSC 8?

因为没法问。终端没有一个标准的方式告诉程序”我支持超链接”,程序只能看环境变量去猜。我把 Claude Code(2.1.312)的可执行文件扒了一下,它的判断分两层。

第一层是 Node 生态里常用的 supports-hyperlinks 库,按 TERM_PROGRAM 和版本号判断,前提是标准输出得是个终端:

眼熟吧?iTerm2 的门槛 3.1,正是它开始支持 OSC 8 的版本。

第二层是 Claude Code 自己补的一个名单,不看版本:

另外还有一些零碎的条件:LC_TERMINAL 在上面名单里(iTerm2 会设 LC_TERMINAL=iTerm2,这样 ssh 出去也能带上)、Windows Terminal 的 WT_SESSION、JetBrains 的 TERMINAL_EMULATOR=JetBrains-JediTerm、tmux 3.4 以上、基于 VTE 的终端(GNOME Terminal 之类)等等。

Notchy 设置的是 TERM_PROGRAM=Notchy.app,哪个名单都不在,所以 Claude Code 老老实实输出了普通文字。白名单这种东西,小众终端天然就吃亏。。。

FORCE_HYPERLINK:自己声明一下

好在所有判断之前,还有一个优先级最高的开关:环境变量 FORCE_HYPERLINK。设成 1 就直接 当作支持,设成 0 就当作不支持,后面的名单统统不看。

在 Notchy 里这么启动一下:

同一条消息里的链接立马变蓝了,按住 Cmd 一点就开,跟 iTerm2 的效果基本一致。

有人可能会想:那把 TERM_PROGRAM 改成 iTerm.app 不也行?千万别。。。冒充别的终端,其他程序会按 iTerm2 的能力来跟你说话,什么专有转义序列、图片协议都可能发过来,那就是给 自己找麻烦了。FORCE_HYPERLINK 本来就是设计给”不在名单里、但确实支持”的终端用来自报家门的,用它才是正路。

需要注意的几点

1. FORCE_HYPERLINK 会跳过”是不是终端”的检查。supports-hyperlinks 一看到这个变量就直接返回 ,不再检查输出是不是 tty。所以在设了它的 shell 里,用这个库的 Node 命令行工具,把输出重定向到文件时可能会夹带 OSC 8 序列。Claude Code 自己倒没这个问题,我试了 FORCE_HYPERLINK=1 claude -p ... > out.txt,文件里干干净净。

2. 显式链接不等于一直有下划线。程序发了 OSC 8,终端怎么画是终端自己的事。iTerm2 默认给显式链接一直画虚线;SwiftTerm 默认是 .hoverWithModifier,不管显式隐式,都要按住 Cmd 悬停才画。SwiftTerm 其实还有个 .always 模式,可它有两个副作用:不按 Cmd 单击就会打开链接,而且正则识别的隐式链接直接不能点了。对一个经常要 Cmd+点击文件路径的终端来说,这代价太大。

3. 环境变量是启动时定死的。给终端加了 FORCE_HYPERLINK 之后,已经开着的标签不 会生效,得新开一个。

好吧,如果你也用 Notchy 可以先自己把 FORCE_HYPERLINK 加上,或者我下个版本加个配置项,默认注入这个环境变量吧;至于”不按 Cmd 也有下划线”,还得去给 SwiftTerm 提个 PR,加一个跟 iTerm2 一样的模式。偏偏上游正在憋 2.0 的大重 构,1.x 已经不发版了,这个 PR 就算合了,估计也还要点时间。。。但我觉得,在APP没有输出 OSC 8 的情况下,Notchy识别链接其实是比 iTerm2 准的,不信的话,你用下就知道了!

最后修改时间: 2026年11月05日 20:46

本文章发表于: 2026年11月05日 20:45 | 所属分类:经验技巧, 编程相关. | 您可以在此订阅本文章的所有评论. | 您也可以发表评论, 或从您的网站trackback.

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注