Ghostty 开发日志 001
原文由 Mitchell Hashimoto 于 发布,订阅该博客
这是 Ghostty 👻 的首篇开发日志!Ghostty 是我自 2021 年以来一直在业余时间开发的一款终端模拟器。我最近宣布了 Ghostty 的存在,此后它受到了极大的关注!
我还没准备好将其公开发布(但我保证最终会完全开源),不过出于记录历史的考虑,我想开始写开发日志,分享这款终端的细节以及整体的进展情况。我觉得有不少值得分享的有趣工程话题,希望大家会喜欢,也能从中学到点东西(或者,也欢迎来教我点东西)。
这些开发日志都会以非常轻松、近乎闲聊的口吻来写。不会有固定的更新周期。内容会涵盖与终端相关的任何话题:功能、奇怪的 bug、我自己的无知,等等。
技术栈
既然这是有史以来的第一篇开发日志,就先来聊聊 Ghostty 背后的技术栈。Ghostty 是一款跨平台(目前支持 macOS 和 Linux,但以未来可支持 Windows 的方式编写)、GPU 加速的终端模拟器。
- 使用 Zig 编写
- Libxev 事件循环(专门为 Ghostty 编写并抽离出来)
- Linux 上使用 OpenGL,macOS 上使用 Metal
- Linux 上通过 Fontconfig、Freetype、Harfbuzz 进行字体渲染
- macOS 上通过 CoreText 和 Harfbuzz 进行字体渲染
- macOS 前端使用 SwiftUI,Linux 上使用 GTK,同时也提供 GLFW 选项
- 其余几乎所有部分都使用 Zig 完全自研
我不打算解释选择这些技术的“原因”。只是在此原样列出技术栈,供感兴趣的人参考。怎么理解,随你们。
自动注入 Shell 集成(#191)
来聊聊我们的第一个功能!
你知道现代终端模拟器在正确配置后可以与 Shell 通信,从而实现大量实用的功能吗?据我与身边开发者朋友的交流,大多数人并不知道这一点,也没有启用它,或者根本没有使用支持这些功能的终端模拟器。😢
终端与 Shell 的集成可以实现很多功能(可参考 Kitty 的介绍)。其中有几项基础功能完全不需要改变工作流,却能显著提升使用体验:
- 调整大小时重绘复杂提示符。这能让在使用现代复杂提示符时,调整终端窗口大小不再那么错乱、卡顿。
- 工作目录上报。这能让新建标签页、分屏等继承之前获得焦点终端的工作目录。
- 活动进程检测。如果终端模拟器知道用户正停留在空闲的 Shell 提示符前,就可以无需用户确认而安全地关闭终端。
我喜欢用演示来说明,所以来看一个例子:复杂提示符的重绘。下面的视频并非 Ghostty,而是 macOS 上另一款流行的终端模拟器。它展示了在未启用 Shell 集成的状态下调整我的 fish 提示符大小时的效果。
可以看到提示符变得一团糟。终端模拟器本身并没有做错什么。它只是在重排文本,但由于 Shell 根本不知道(目前也无法知道)提示符被换行到了多行,提示符就会在底部的新行上重绘,并大量重复。
接下来是已配置 Shell 集成的 Ghostty。
好多了!Shell 可以告知终端,它能够在调整大小时重绘提示符。调整大小时,Ghostty 会清空终端所在行(你能看到闪烁——之后我想改进这一点)。这样可以防止文本重排,避免产生新行,从而实现完美的重排效果。
好吧,那该如何配置呢?这需要针对不同 Shell 的特定配置(zsh 用 zsh 的代码,fish 用 fish 的代码,bash 用的就是一堆垃圾,等等)才能生效。在 #191 之前,我只是写了文档,告诉用户如何手动设置。
但现在,Ghostty 会 🪄 自动 🪄 完成这些(目前已支持 fish 和 zsh)。这个功能可以关闭,其原理是对你的 Shell 做一次简单的 basename 判断,如果是“fish”就认为是 fish,如果是“zsh”就认为是“zsh”,然后设置相应的环境变量,让一些内嵌的配置文件随 Shell 一起加载,自动完成所有设置。
由于在撰写本文时 Ghostty 尚未公开发布,目前 Shell 集成做得最好的终端是 Kitty。Kitty 同样支持自动 Shell 集成。如果你现在就想体验这类功能,可以使用 Kitty。其他一些终端模拟器也支持其中的部分功能!
自动斜体化字体(#179)
这次既修复了一个 bug,也新增了一个功能。
先说基础知识:常规、粗体、斜体和粗斜体是不同的字体样式。如果你有一个字体文件 MyAwesomeFont.ttf——它通常只包含一种样式(多半是常规)。这里我做了很多简化,但大致如此。
当用户说想使用 MyAwesomeFont 时,Ghostty 不仅要找到这个字体,还要找到与之关联的所有样式。很多编程字体并没有斜体或粗体样式。(顺带一提,查找字体及所有关联样式的过程通常被称为字体发现问题。这是一个更宽泛的问题:比如如果用户没有指定字体,默认该用什么字体?等等。)
首先,来看这个 Bug
另一个重要的背景知识是:字体往往并不包含所有可能的字形(单个可绘制字符)。例如,标准的等宽编程字体很可能不包含汉字(比如“草”这个字,如果你能正常看到的话)。
如果程序试图绘制当前配置字体中不存在的字符,Ghostty 会尝试寻找其他包含该字符的字体。这通常比直接渲染成方框、强迫字体文件包含世间所有字形要好。至于如何让这个字形与你配置的字体的风格保持一致,里面还有一些复杂问题,但这不是本文重点——只需知道这些问题确实存在,而我个人觉得,去解决它们简直像是一种自我折磨。
之前的 bug 是这样的:如果某个字符在你所用字体的给定样式中不存在,Ghostty 会去搜索包含该字符且为同一风格的任意字体。结果……渲染效果非常难看,因为如果某个等宽字体缺少某个字符,Ghostty 可能会找到一个非等宽字体来替代,最终看起来像这样:

太糟了。修复方法是:如果某个字符在给定样式中不存在,且该样式不是“常规”,就先尝试用常规样式来查找。结果就是虽然会忽略样式,但至少能正确渲染。不错。
注意,Ghostty 本来就会优先搜索等宽字体。完全禁止使用非等宽(比例)字体看起来似乎合理,但像汉字这类字符,用比例字体渲染通常也没什么问题,所以我认为没有必要做这种限制。
更进一步,用程序生成斜体
对于斜体,我们可以做得更好。如果某个字体没有提供斜体字形,我们可以倾斜常规字形来伪造斜体。效果肯定不如边品单品手冲边精雕细琢的真正斜体那么好,但……也算过得去。
所以 Ghostty 现在就是这么做的。如果请求的是斜体字符,而当前字体不支持斜体,我们就会在光栅化时将常规字形倾斜,生成伪斜体:

上面的字体本身不支持斜体。这些斜体都是伪造的。看起来还不错!而且肯定比完全不显示斜体要好。
如果你好奇内部是怎么实现的:像 FreeType 或 CoreText 这样的字体光栅化库允许调用者提供变换矩阵。对于 CoreText,可以在通过 CTFontCreateCopyWithAttributes 初始化字体时传入变换矩阵。
在 Ghostty 中,我们只是硬编码了一个 15 度的倾斜(见下方),但你可以看到,你完全可以自定义这个矩阵,把字形倾斜成任何你想要的角度。
pub const italic_skew: macos.graphics.AffineTransform = .{
.a = 1,
.b = 0,
.c = 0.267949, // approx. tan(15)
.d = 1,
.tx = 0,
.ty = 0,
};结语
首篇 Ghostty 开发日志到此结束。希望你学到了点东西,或是觉得有趣。由于是第一篇,本来还有很多可以写的内容,但我会留到以后的日志里再分享(不过,我会尽量聚焦于更新的进展)。
如果你想保持关注,可以在 Twitter 或 Mastodon 上关注我(链接在页脚)。本博客也有 RSS 订阅。
Boo. 👻
随机一篇博客
评论
登录后参与讨论