Ghostty 开发日志 003
大家好!欢迎阅读 Ghostty 👻 的第三篇正式开发日志!
如果你错过了之前的开发日志,或者想进一步了解 Ghostty 是什么,请查看本网站上的 Ghostty 页面。
社区动态:公开 Discord 服务器上线!
我很高兴地宣布,我们现在有了公开的 Discord 服务器!欢迎任何人加入,提问关于 Ghostty 的问题,聊聊终端模拟器、系统编程、GPU 编程等等。我们也在利用这个 Discord 来扩大 beta 计划的规模,所以如果你有兴趣参加 beta 测试,请加入 Discord 服务器并保持一定的活跃度。
接下来,我将于 9 月 5 日在 Zig Showtime 上做一场关于 Ghostty 的演讲。请留意 Zig Showtime 网站、Twitter、Mastodon 等渠道,以获取确切时间和链接的最新消息。演讲内容仍在准备中,但大致主题是对 Ghostty 实现的一次快速巡礼,并重点介绍几个有趣的地方。毕竟这是 Zig Showtime,所以我会更多地聚焦 Ghostty 中与 Zig 相关的部分。
键盘输入处理恨你(#282)1
我只是一个在普通美式标准键盘布局上打字的普通美国用户。在美式标准键盘上,你按 a 就得到 "a",按 '(撇号)就得到 "'"。你按 shift+a 就得到 "A"。简单,太简单了。可你正坐在谎言的王座上。
第一个麻烦的苗头是非标准但以英语为中心的键盘布局,比如 Dvorak 或 Colemak。在使用美式标准物理键盘时,Dvorak 用户按下 s 期望得到 "o",按下 ctrl+i 则期望得到 ctrl+c 的行为。
第二个麻烦的苗头是 Unicode 序列。在 Mac 上、美式标准布局下,alt+a 会产生 å。最新的 beta 用户群中新增了一位挪威用户,他指出这一点尤为重要,因为他偏好美式标准布局,但会使用 alt 组合键来输入挪威语单词。
第三个麻烦的苗头是非标准键盘布局。你知道什么是死键序列(dead key sequence)吗?我不知道(因为我只是一个普通的美式标准布局用户,记得吧)。在美式标准布局下,我们习惯于所有按键都是无状态的。要大写一个字符,你需要同时按下 shift+A。而使用死键序列时,你要先按 shift,松开,再按 a,然后就会看到 A(这并不是真实世界中的死键序列,只是演示死键序列的行为方式)。一个真实的死键序列例子,在美式国际布局上:先按 ',松开,再按 a,你就会得到 á。
当一个人在键盘上敲下一个键时,会发生几件事。键盘发送一个物理扫描码(physical scancode)。软件层面的键盘布局处理(即在 QWERTY 键盘上使用 Dvorak 布局)会将该扫描码转换为另一个扫描码(Dvorak 下 "i" 表现得像 "c")。修饰键(ctrl、alt 等)与其他键(比如字母键)的组合可能产生也可能不产生一个或多个字符(例如 macOS 上 alt+p 会产生 "π",但在美式物理键盘的 Dvorak 布局下同样的操作对应的是 alt+r)。
最后,并非所有修饰键都会被消耗掉,这对按键绑定很关键。例如,如果你在美式布局上绑定了 ctrl+=,那么它在挪威语布局上相当于 ctrl+shift+0——你必须知道 shift+0 产生了 = 但并未消耗 ctrl,因此最终输入是 ctrl+=。但如果用户同时也绑定了 ctrl+shift+0,那该用哪一个呢?🤔
以上解释都非常粗略且缺乏细节。我计划专门写一篇完整的博客文章,来讲述键盘布局、输入处理、终端键盘“协议”以及操作系统级 API 这些克苏鲁式的恐怖存在,所以本篇开发日志中就跳过细节了。敬请期待!
Ghostty 不仅必须处理这些用例(事实证明非美式标准键盘的用户是存在的),而且还必须跨平台做到这一点,因为 Ghostty 已经能在 macOS 和 Linux 上运行,并且还立志支持 Windows。😰 好吧,Ghostty 现在做到了。
唯一能展示出来的视觉效果就是死键状态的处理,现在会用最后一个待定的死键来渲染。至于其余部分已经正常工作,你就只能相信我了。
除此之外,Ghostty 现在还支持可选的显式物理键映射(physical key mapping)。如果你将某个按键绑定指定为 ctrl+physical:0(注意 physical: 前缀),那么只有物理位置上的 0 键才会触发该绑定。在某些键盘布局下,这可能使该绑定无法触发。默认情况下,按键绑定基于逻辑(转换后的)键。
Kitty Keyboard Protocol 及其他协议(#292、#295)
如果你已经厌倦了解键盘输入的知识,可以直接跳过这一节。
除了将键盘按键映射为字符输入之外,键盘事件还必须被映射为转义序列(escape sequence),这样在终端中运行的程序才能知道你按下了哪个键。
最初的终端协议只是原样发送由键盘按键产生的字符。你输入 "a",得到 "a";你输入 "A"(shift+A),得到 "A"。这有很多局限性。例如,终端程序无法区分诸如 ctrl-i 和 ctrl-shift-i 这样的修饰键组合,因为它们产生的数据完全相同。
为了解决这些缺陷,人们开发了许多额外的键盘协议。xterm 开发了 modified keys 序列,Paul Evans 开发了 fixterms 协议,Kovid Goyal(Kitty 的作者)开发了 Kitty Keyboard Protocol。这些协议的核心就是定义当按键被按下时,终端向正在运行的程序发送什么内容。
这些都不是假设性的问题。tmux、neovim 以及其他主流程序都实现了这些协议,让你能够绑定比原本多出数百个的按键绑定。
Ghostty 现在实现了所有这些协议。
据我所知,Ghostty 是极少数实现了全部这些协议的终端之一。我相信只有 iTerm、Foot,或许还有 WezTerm 同时实现了 xterm modified keys 和 Kitty 协议。我认为实现 Kitty 协议本身的也只有这几款加上 Kitty 自己。大多数终端(Terminal.app、Windows Terminal、Alacritty、Warp)则完全没有实现任何键盘协议。2
下面是一段展示 Kitty keyboard protocol 工作效果的视频:
下面这段视频展示了 xterm modified keys 协议的工作效果:
桌面 Linux 之年(GTK)
最近一批 beta 用户让我们的 Linux 全职用户数从寥寥几人增加到了大约五六人,随之而来的是 Linux 体验方面的许多改进。
首先,GTK 单实例模式(#247)现已启用并获得支持。对于 GTK,Ghostty 现在只运行单个进程。第二次启动 Ghostty 时会在现有进程中打开一个新窗口。这与标准的图形桌面环境配合良好,也让首次启动之后的 Ghostty 启动速度飞快。
其次,Ghostty 现在支持主选择剪贴板(primary selection clipboard)(#266)。Linux 桌面拥有多个剪贴板。“primary”(或 “selection”)剪贴板会在文本被选中时自动复制文本,粘贴通常通过鼠标中键完成。这是一个与 ctrl+shift+c 和 ctrl+shift+v 所用的剪贴板相互独立的剪贴板,因此你的剪贴板上可以同时存放多项内容。这个功能有时颇具争议,不过别担心,你可以通过 copy-on-select 配置项将其禁用。你可以在下面的视频中看到它的实际效果。注意,我没有按任何键盘快捷键;这里只用到了 selection 剪贴板。
Linux 现已支持自动 shell 集成(#245)。我在开发日志 001 中谈过 shell 集成。此前,Linux 用户必须设置一个环境变量来指向 Ghostty 的 share 目录。现在 Linux 应用会从 ghostty 可执行文件所在位置向上搜索具有正确目录结构的 share 目录。如果你按照标准的 FHS 目录布局(Ghostty 打包所采用的标准布局)安装 Ghostty,那么 Ghostty 会自动找到并配置你的 shell。
还有更多改进:Ghostty 应用图标能正常显示了(#251),GTK 应用可以处理非美式键盘布局了(#271),GTK 应用支持半透明窗口背景了(#264),等等。
我真心希望 Ghostty 能成为一流的基于 GTK 的 Linux 终端。我自己并不全职使用 GTK,所以我非常依赖测试者们提供反馈,来把这份体验打磨到极致。我很高兴地报告,Ghostty 在 GTK 上现在的感觉已经相当不错了!GTK 应用仍然落后于 macOS 版的体验,但我期待着让它达到同等水平!
完
最近一轮 beta 测试者真是太棒了。我要感谢 @hovsater,他暴露出了所有显而易见的键盘布局和映射问题,并督促我把它们全部修复。@cryptocode 发现了十几个与宽字符、多码点字素及其与选区和软换行交互相关的边界情况 bug。还有许多新的测试者值得感谢,他们为 Linux 方面的改进做出了贡献。谢谢大家!
如果你想持续关注最新动态,可以在 Twitter 或 Mastodon 上关注我(链接见页脚)。本博客也提供 RSS feed。
Boo。👻
脚注
这里致敬的是那篇出色的博客文章 Text Rendering Hates You(《文本渲染恨你》),我强烈推荐阅读。↩
我对上述所有终端模拟器的作者都怀有深深的敬意,希望这不会显得像任何攻击。我只是想客观陈述它们各自支持和不支持哪些功能。如果我说错了,请告诉我,我会立即更正。↩
随机一篇博客