トーク:Ghosttyの紹介と役立つZigパターン
これはZig Showtimeで行ったトークを文章化したものです。動画でご覧になりたい方は、YouTubeでご視聴いただけます:Zig Showtime: Ghostty。動画の最後にはQ&Aセッションも収録されていますが、本記事では割愛しています。

こんにちは!本日はGhosttyについてお話しできることをとても嬉しく思います。Ghosttyは、Zigで一から書き上げたまったく新しいターミナルエミュレータです。
ご注意:執筆時点では、Ghosttyはまだ一般公開されていません。トーク中でも何度か触れていますが、誤解を招かないよう冒頭でお伝えしておきます。Ghosttyは2024年中にフリーかつオープンソースとして公開する予定です。現在はソースコードへのアクセスを含むクローズドベータを実施中です。この点についてはトーク内でも折に触れてお話しします。

Mitchellと申します。とにかくコードを書くのが大好きで、大好きでたまらない人間の一人です。
過去にはいくつか有名なソフトウェアプロジェクトを立ち上げました。Vagrant、Terraform、Vaultなど、ご存知の方もいらっしゃるかもしれません。いずれも何年も前から開発からは離れていますが。
そして、常に何かしらのサイドプロジェクトを抱えているタイプでもあります。ずっと、です。10年ほど前にはHearthstoneのログ解析・リプレイソフトを作り、プロのeスポーツチームに売却しました。数年前にはメールサーバーを一から自作し、今でも自分のいくつかのドメインで運用しています。そして今は――ターミナルエミュレータを作っています。
このスライドは夕食どきに作ったのでお腹がペコペコでした。そこで、自分が何かを食べている写真をたくさん並べてみました。別に食べ物に情熱があるわけではありません。ただ…今この瞬間、とてもお腹が空いていたのです。そして、部屋全体を引き締めるテーマがあると素敵だなと思っただけです。

とはいえ、今回のトークの主役はGhostty、ターミナルエミュレータです!
先に進む前に、Ghosttyがどんな見た目なのかお見せしましょう。この一枚のスクリーンショットからも、Ghosttyが非常に多機能であることがお分かりいただけると思います。
GhosttyはmacOSとLinux向けのネイティブアプリです。画面分割、トゥルーカラーに対応し、vimも問題なく動作します。ボールドやイタリックなどの複数のフォントスタイル、Kittyグラフィックスプロトコルへの対応など、枚挙にいとまがありません。これらはすべて、このスクリーンショットの中に写っているものだけです!

ターミナルエミュレータをご存知ない方のために――おそらく一度は使ったことがあるはずですが――代表的なものをいくつかご紹介します。ほかにも数十、数百と存在します。

これほどたくさんあるのに、なぜまた新しいものを作るのでしょうか。

根本的に見ると、これが私から見た現在のターミナルエミュレータの状況です。高速なターミナル、多機能なターミナル、ネイティブなターミナルがあります。選べるのは、せいぜい二つまでです。

Ghosttyは――そして私見ではすでに達成していますが――そのすべてを兼ね備えることを目指しています。

高速であること、多機能であること、ネイティブな使い心地であることは、決して互いに排他的ではありません。
Ghosttyは現時点でも十分に「緑のチェックマーク」に値する出来だと思っていますし、まだまだ改善の余地もたくさんあります。
念のため注意点を付け加えておくと、私が考える「ネイティブなプラットフォーム体験」の基準は、アプリがそのプラットフォーム専用に作られたかのように感じられ、振る舞うことです。決して無茶な基準ではないと思います。たとえば、新しいウィンドウを開くたびに別プロセスが立ち上がるAlacrittyは、ある意味ネイティブとは言い難いでしょう。あるいは、タブにネイティブでないウィジェットを使っているKittyも、ある意味ネイティブとは言えません。ほかにも、それぞれに挙げればきりがないほど例があります。

いわば「土台」のレベルで、私はこういうことを実現しようとしています。Alacritty並みの速度、Kitty並みの多機能さ、(macOSにおける)iTerm並みのネイティブ統合度、といった具合です。

ただ、それだけでは終わりたくありません。ターミナルを、テキストアプリケーション開発のためのモダンなプラットフォームにしたいのです。ちょうどブラウザがGUIアプリケーション開発のモダンなプラットフォームであるように――良くも悪くも、ですが。
ブラウザは毎年何十もの(あるいは何百もの)新機能を出荷しています。驚くべき速さで革新を続け、アプリケーション開発者をワクワクさせています。やりすぎなくらいかもしれませんが、ターミナルも今よりずっと速く革新できると思っています。
ここにいくつか例を挙げます。
- プログレスバー!毎回(しかもたいていは出来の悪い)再描画を繰り返す必要がどこにあるのでしょうか。
- ドラッグ&ドロップにより、エディタなどのアプリケーションがよりネイティブらしく振る舞えるようにします。
- タブや分割の制御により、マルチプレクサがモダンなターミナルエミュレータのパフォーマンスや機能を活かせるようにします。
- マウスジェスチャーにより、TUIがフリック、マルチタッチ、慣性スクロールなどに反応できるようにします。
- セキュリティ機能:ターミナルのエスケープシーケンスは今日では恐ろしい領域です。別の機会に詳しくお話しします。もっと良くできます。
- ほかにもいろいろあります!

Ghosttyは速いのか?どうすれば分かるのか?ターミナルエミュレータのベンチマークにはさまざまな方法がありますが、このトークで多くの時間を割くつもりはありません。お伝えしたいのは、Ghosttyは「速い」ということです。「最速」だと主張するつもりはありません。
ここでは、GhosttyでDOOMの炎のアニメーションを動かし、FPSを計測しています。大量のセルを書き換え、さらにスクロールバックも発生するため、良いストレステストになります。
ご覧のとおり、Ghosttyは480〜500FPS前後を維持し、アニメーションは非常になめらかです。比較として、素晴らしいコミュニティ製プロジェクトに水を差さないよう、あえて公式のmacOS標準Terminal.appを挙げますが、こちらは10FPS未満でひどくカクつきます。「高速」なターミナルとして知られるAlacrittyやKittyをお使いなら、Ghosttyと同等の数値が出るはずです。
言いたいのは、Ghosttyが最速だと主張したいわけではなく、「速い」というカテゴリーに十分入るということです。ですから、表の緑のチェックマークは妥当だと考えています。
ぜひご自身のターミナルでも試してみてください:https://github.com/const-void/DOOM-fire-zig/
テキスト版をご覧の方への補足:スライドの画像は静止画なので動画は再生されません。リンク先のプログラムをGhosttyで実行すると、おおよそ480〜500FPSで動作します。

Ghosttyは多機能でしょうか?これを定量的に示す方法は分かりませんが、対応している機能を定性的に並べてみました。比較的珍しいものも多く含まれています。

macOSでは、メインのGUI部分はSwiftで、AppKitとSwiftUIを使って書かれています。タブはネイティブのタブ、分割はネイティブのUIコンポーネントで、マルチウィンドウも期待どおりに動作します。Linuxでは、GTKを使い、本物のGTKウィンドウやウィジェットでGUIを構築しています。
エラーメッセージなどの機能も、専用のターミナルビューで無理やり実装しているわけではなく、実際にネイティブのUIコンポーネントを使っています。つまり、ターミナルの描画面やコアロジックはクロスプラットフォームでありながら、ユーザーとのやり取りの部分は、真にネイティブな体験を実現するためにOSごとに専用に作り込んでいるのです。

では、技術スタックとプロジェクト自体について簡単に概要を見ていきましょう。
まずはプロジェクトについて。現在はソースコードへのアクセスを伴うクローズドベータの段階です。クローズドベータにしている理由は、これが個人プロジェクトであり、自分が抱えきれなくなるのを避けたいからです。クローズドベータなら、少しずつ人を招待し、不具合を直し、また次へ進む、という進め方ができます。
最終的に公開する際は、フリーかつオープンソースで、ライセンスはおそらくGPLになる予定です。「おそらく」と言うのは、テスターの皆さんの意見も歓迎しているからです。現在活発に議論しているのは、MITのようなより緩やかなライセンスを選ぶべきかという点です。
今すぐ参加できる公開Discordがあります。ベータテスターはそのDiscordからお声がけしています。また、個人ブログで時折開発日誌も書いています。
技術面については、このトークでほとんどを詳しくお話しするので、ここでは深く立ち入りません。Zigで書かれていること、ネイティブ部分は本当にネイティブであること(たとえばmacOSではAppKit)、そしてイベントループなど多くの依存関係を自前で書いていることだけ、押さえておいてください。

では、当然の疑問から始めましょう。なぜZigなのか?

一言で言えば、コミュニティ、言語、そしてビルドシステムが気に入っているからです。言語とビルドシステムの機能が、このターミナルプロジェクトに非常によく合っていると思っており、このトークではその点を中心にご紹介します。

あれこれ詮索したければどうぞ。私はこの質問が好きではありませんし、答える気もありません。私はZigを選び、Zigが好きです。それでは先に進みましょう。

こちらはサブシステムごとのコードアーキテクチャの概要です。Ghosttyを構成する主要なサブシステムと、その説明をまとめています。次のスライドで視覚的な図をお見せします……

こちらがそれらを組み合わせたランタイムアーキテクチャです。図の中のすべては100% Zigでできていますが、macOSの「apprt」だけは一部Swiftです。
基本的な流れは次のとおりです。
- アプリが起動し、エントリーポイントは「apprt」にあります。
- Apprtは一つ以上の「surface」を生成します。surfaceとは、一つの対話的なターミナルを表すものです。Apprtはそれをウィンドウに入れるか、タブや分割に置くかを自由に決められます。コア側にとっては違いはありません!
- 各surfaceはIOスレッドとレンダラースレッドを立ち上げます。
- IOスレッドはptyのファイルディスクリプタを生成し、設定されたコマンド(通常はシェル)を実行します。ptyの読み書き、エスケープシーケンスなどのターミナルイベントの処理もIOスレッドの役割です。
- レンダラースレッドはターミナルの状態をピクセルに変換し、一定のフレームレートで描画します。フォントの整形やレンダリングもレンダラーの担当です。

ターミナルエミュレータについて語るのはとても楽しいのですが、ここはZig Showtimeですので、トークの焦点をGhosttyで使っているZigのパターンへ移していきます。
順不同でご紹介します。
オンラインでお読みの方への補足:このセクションの大部分では、実際にGhosttyのソースコード(もちろんGhosttyのターミナル内で)をタブで切り替えながら、これらのパターンをお見せしました。動画をこのスライドまで早送りしていただければ、その様子をご覧いただけます。

まずは、comptimeインターフェースです!

comptimeインターフェースとは、コンパイル時に判明している情報に応じて実装が切り替わる値のことです。
ここに示したソースコードは、Ghosttyから直接抜粋した実際の例です。ここでは、あるビルドオプションに応じて切り替わる「font face」の定義をご覧いただいています。あるビルド設定ではCoreText、別の設定ではFreeType、さらにウェブベースのCanvas実装にも対応していることが分かります!
下部のペインは、そのインターフェースが実際に使われている様子です。これらのインターフェースを使う側のコードは、内部がどう動いているかを気にする必要はありません。定義されたインターフェースがあり、それを呼び出して使えばよいのです。

comptimeインターフェースは、Ghosttyがプラットフォーム固有の機能を実装する際の主要な手段であり、フォント、レンダラー、アプリのランタイムなどで使われています。
最大の利点は、実行時に決して変わらない実装であれば、ランタイムオーバーヘッドなしで実装を切り替えられることです。フィールドアクセスや関数のディスパッチは、すべてコンパイル時に確定します。

Zigコンパイラの特徴の一つに、実際に参照されたコードだけを解析するという点があります。これはバグではなく仕様です。なぜなら、見苦しい#ifdefのようなガードで不可視にしなくても、状況依存のコードを共存させられるからです。
一方で、comptimeインターフェースでは、すべてのビルドオプションをテストしなければ、ビルドエラーを簡単に見逃してしまうという欠点があります。
GhosttyではCIですべてのビルドオプションを網羅してテストしています。

こちらは先ほどお見せしたのと同じスライドです。これらの機能やパターンによって何が実現できるのかを示すために、改めてお見せします。

同じサブシステムについて、現在存在する実装と、将来考えられる実装を並べたものです。これらの代替実装のほとんどすべてがcomptimeインターフェースです。

次は、またしてもcomptimeですが、今度はデータテーブルです。

これはデータテーブルの例です。ここではKittyキーボードプロトコルの入力エンコーディング情報の一部をご覧いただいています。
RawEntryのタプル形式は実際の利用にはあまり適していませんが、テーブルを手軽に構築するには非常に便利です。これらがpubではないことにお気づきでしょうか――このファイルの外で使われることを想定していないのです。
これらの非公開のエントリーを、次に加工していきます……

comptimeを使って、生のエントリーを加工できます。ここでは、生のエントリーのタプルを、より実行時に扱いやすい構造体へ変換しています。
変換処理はすべてコンパイル時に行われるため、実行時コストはかかりません。さらに、生のエントリーは実行時のコードからは一切参照されないので、最終的なバイナリのサイズにも影響しません。
この例では変換はほぼ単純なデータ変換ですが、もっと面白いこともできます……

プラットフォーム固有のキーコードのテーブルからの例です。生データには各プラットフォーム(Mac、Windows、xkb経由のLinux、さらにはUSBコード)のキーコードが含まれています。しかし実行時には、ビルド対象のプラットフォームだけを含むentriesへ変換します。
こうすることで、構造体はよりコンパクトになり、最終的なバイナリも小さくなり、実行時に条件分岐して探す必要もなくなります。

さらに一歩進めることもできます。こちらは、特定のモードにおける特定のキー入力に対して、ターミナルが実行中のプログラムへ送るべきエスケープシーケンスを定義するデータテーブルの例です。
その多くは、はっきりとした繰り返しパターンに従っています。いちいち手作業で全部書き出すのではなく、comptimeで実行される関数を書いて、データをプログラムで生成できます。
多くのプロジェクトでは、データ生成のためにシェルやPythonなどのスクリプトを用意しますが、Zigならそれをすべてcomptimeで完結できます。
補足:pcStyle内の関数本体をcomptime {}で囲んでいるのは、この関数が決して実行時に呼ばれないようにするための小技です。

次は、comptimeデータテーブルを発展させて、comptimeでの型生成についてお話しします。

まずは@Typeビルトインについて30秒で解説します。ご存知ない方も多いかもしれません。@Typeビルトインを使うと、comptimeに型を生成できます。とてつもなく強力であると同時に、とてつもなく恐ろしい機能でもあります。

ご注意ください!これは本当に意味のある場面に限定して使うべきものです。「完全なメタプログラミング」に走らないよう注意が必要です。可読性を損ない、コンパイルも遅くなってしまいます。

この例では、Ghosttyが対応しているさまざまな「モード」の定義方法を示しています。モードとは、実行中のプログラムが問い合わせたり設定したりできるターミナルモードで、ターミナルエミュレータの挙動を変えるものです。数百種類も存在します。
ここでもおなじみの、非公開のentriesデータテーブルというパターンを使っています。このテーブルを加工する方法の一つとして、すべてのキーを@Typeビルトインを使って網羅的なenumに変換しています。
enumを最初から定義しない理由は、各エントリーに関連する追加データがあり、それらを編集や参照がしやすいようにまとめておきたいからです。今回の例では、各モードがそれぞれ値を持っています。

こちらはFontIndex構造体の生成例です。この構造体はGhostty内で特定のフォントを参照するために使われています。上位ビットでスタイル(regular、bold、italicなど)を、下位ビットで配列へのインデックスを表します。
スタイルに必要なビット数を求めることで、インデックスに必要な正確なビット数を決めることができます。
スタイルを増やして必要ビット数が増えると、インデックスで表現できるフォント数は制限されます。
補足:これは、Styleのビットサイズを増やす際には十分意識的に行うようにするため、特定のサイズが確保されていることを保証するテストと組み合わせて使われています。

次は、GhosttyがどのようにSwiftと統合しているかについてお話しします。実のところ、これはSwiftに限らず、CのAPIを呼び出せるあらゆる言語に当てはまる、より汎用的な解決策です。

私たちが直面した課題は、現代のmacOS開発では実質的にSwiftが必須になっているということです。もちろんObjective-Cでもまともなアプリは作れますが、Appleが出してくる新しいイケてる機能やフレームワークはすべてSwift専用で、ObjCの先行きは明らかです。

Linuxでは、Zigは単にGTKのC APIを使っています。ZigがCのAPIを呼べることはよく知られています。問題は、(Macアプリ用の)SwiftがCのAPIをエクスポートできず、macOSアプリはmainを所有することを期待している点です。
しかし……SwiftはCを呼べます。そしてZigはCのAPIをエクスポートできます。

そこでGhosttyを組み込み用のC APIとしてもパッケージ化することにしました。これをスタティックライブラリとしてコンパイルし、Swift製のmacOSアプリがそれに依存して、Swift側からライブラリを呼び出す形です。

こちらは、たとえばSwiftがGhosttyの設定を初期化している例です。

この方法なら、Linuxなどほかの将来のプラットフォームとの移植性を損なうことなく、きわめてネイティブなmacOS体験を実現できます。
さらに素晴らしいのは、Zig(特にZigのビルドシステム)のおかげで、プログラムをexeとしてもlibとしてもコンパイルできるようにするのがいかに簡単だったかということです。

将来的には、このCライブラリの成果物を公式な成果物としてサポートし、誰でも完全な機能を備えた、高速でモダンなターミナルを自分のアプリケーションに組み込めるようにしたいと考えています。
ただ、現時点ではこれは非公開で、macOSアプリケーションのためだけに使われています。

以上がGhosttyで使っている主要なパターンの一巡でした。ほかにもたくさんありますが、一度のトークとしてはこれくらいで十分でしょう。中には、それだけで一本のトークになりそうなパターンもあります……
最後に、Ghosttyについてもう少しお話しして、このトークを締めくくりましょう!

では、プロジェクトの今後はどうなるのでしょうか。
ターミナルエミュレーションの機能は、かなり完成に近づいています。Ghosttyで動かない、あるいは正しく描画されないプログラムを見つけることは極めて稀です。そういったものが見つかれば最優先で修正しています。数十人のテスターが日々の業務でGhosttyを一日中使っており、安定して動作しています。
現在の主な焦点は、ネイティブなプラットフォーム体験の向上と、アプリをそのプラットフォーム専用に作られたようにより感じられるようにすることです。たとえばmacOSでは、ファイルベースの設定だけでなく、GUIの設定画面に取り組んでいます。また、Appleデバイス間でiCloudによる設定同期(データを自分で所有する)といった機能にも取り組んでいます。
ほかにもいろいろあります……

Ghosttyは速いですが、まだまだ改善の余地は大きく残されています。すでに分かっている、手を付けやすいパフォーマンス改善がたくさんあり、Ghostty自体すでにかなり速いだけに、伸びしろがあるのは嬉しいことです。
さらに、VTストリームのパーサーやハンドラーをファズテストしたいと考えています。少しファズテストを行っただけでもいくつかバグが見つかったので、まだまだ潜んでいるはずです。
起動時間や入力レイテンシーなど、これまで一度も計測したことのないパフォーマンス領域もさまざまあります。これらのコードパスを遅くしないようには心がけてきましたが、実際に計測すれば大きな改善点が見つかるはずです。
Ghosttyのメモリ使用量は、現状あまり良くありません。ひどいというほどではないですが、良いとも言えません。CPUやレンダラーのパフォーマンスを高めることには多くの時間と労力を費やしましたが、メモリ使用量については少し甘くなっていました。簡単に改善できる点もあります。たとえば現在はスクロールバックバッファ全体をあらかじめ確保しており、数メガバイトを消費していますが、これは動的に確保するようにすれば比較的簡単に改善できます。

現在、活発なベータプログラムを実施中です。プログラムに参加するにはDiscordへの参加が必須です。
私たちのプロセスは安定性を重視しています。一定数のテスターを招待し、未解決の問題がすべて解消されるまで次の波の招待は行いません。ここで言う「問題」とは、バグや重大な機能不足のことです。すべての機能リクエストを実装するわけではありません。

Ghosttyは来年(2024年)中に一般公開される予定です。それまでに、ベータプログラムを数百人規模まで拡大していく考えです。
最初のリリースバージョンはバージョン1.0、もしくは最悪でも1.0のリリース候補になる予定です。この徹底したテストプログラムの目的は、安定していて多機能なプロジェクトとして、最初から良いスタートを切ることにあります。
先ほども述べましたが、GhosttyはFOSSとして公開し、ライセンスはおそらくGPLになる予定です。「おそらく」と言うのは、テスターの皆さんからもまだ意見を募っているからですが、現時点での計画はGPLです。ほかの多くのターミナルもGPLです。libghosttyとGPLの兼ね合いについては懸念もあり、現在活発に議論されている領域です。
安定したソフトウェアにしたいということに加え、公開が遅れているもう一つの理由は、あと数週間で赤ちゃんが生まれる予定です赤ちゃんが生まれたばかりです。とてもとても忙しくなることが分かっているので、フルスケールのOSSコミュニティを育て上げ、立ち上げることまで同時に背負って自分に過度なプレッシャーをかけたくないのです。
新生児を抱えながら、どうやって新しいトークを書き、発表し、この記事まで作る時間があったのかと、何人かの親御さんから聞かれました。このトークと(この一文のような一部の追記を除く)本記事は、赤ちゃんが生まれる前に書き上げたものです。トーク自体は、授乳の合間に収めるのはそれほど難しくありませんでした。そして、ええ、眠いです。😊

❤️
記事をランダムに読む