Ghostty + zellij + lazygit + Claude Code 構成に憧れて挫折し、VSCodeにタブバーを生やした
TL;DR
- Ghosttyとかでかっこよく作業したかったけど、挫折した
- VSCode/Cursor/Zedで開いた複数のウィンドウをタブ化してサクッと切り替えできる & おまけにClaude Codeの出力が終わったら通知もしてくれるアプリを作った。
はじめに:ターミナル回帰への憧れ
X(Twitter)でターミナル中心の開発環境を見かけて、憧れるようになりました。
特に憧れたのが、こんな構成です:
| 用途 | ツール | 良いところ |
|---|---|---|
| ターミナル | Ghostty | 起動が爆速 |
| 画面分割 | zellij | 設定が楽 |
| ファイラー | yazi | プレビュー付きで爆速 |
| Git | lazygit | CLIなのにGUI並みに見やすい |
| 検索 | fzf + ripgrep | Cmd+Pより速い |
| AIコーディング | Claude Code | ターミナルで完結 |
Ghostty + lazygit + zellijの組み合わせを試したとき、衝撃を受けました。タブで複数のプロジェクトを即座に切り替えられる。通知が来たらすぐわかる。余計なUIがなく、コードに集中できる。
「これがミニマルな開発環境か...」
Neovimの設定を頑張れば、VSCodeと同等の開発体験が得られるという話も聞きます。いっそのこと、VSCodeを卒業してターミナル中心の開発に移行しようか。そんなことを考えていました。
実際に試してみた感想:
- Ghostty: 本当に起動が速い。ターミナルとしての完成度が高い
- zellij: 簡単な設定で画面分割できる。必須
- lazygit: コミット、プッシュは直感的。ただdiffが見づらい(カスタマイズすればカバーできそうだけど)
- yazi: ファイル操作は快適だけど、ファイル検索の使い勝手がVSCodeに勝てなかった
- Neovim: 結局VSCodeの操作性には勝てなかった
挫折:VSCodeの操作性には勝てなかった
...結局、3日でVSCodeに戻りました。
理由はシンプルです。
- コード補完が強力すぎる: LSPの設定で同等のことはできますが、VSCodeはゼロ設定で動く
-
定義ジャンプが便利:
Cmd + Clickで飛べる体験は手放せなかった - 拡張機能のエコシステム: 必要なものがすぐ見つかる
- 慣れた操作感: 7年近く使ってきたエディタの操作は体に染み付いている
ターミナル環境の良さは理解できたけど、乗り換えコストが高すぎました。
でも、VSCodeに戻ったら戻ったで、別の問題に直面することになります。
VSCodeの限界:複数ウィンドウ管理が地獄
私は普段、3〜5つのプロジェクトを同時に開いて作業しています。フロントエンド、バックエンド、共通ライブラリ、ドキュメント...という具合に。
VSCodeには「マルチルートワークスペース」という機能があります。複数のプロジェクトを1つのウィンドウで開ける機能です。
一見便利そうですが、実際に使ってみると問題がありました。
ターミナルの管理が煩雑
マルチルートワークスペースでは、各プロジェクトのターミナルが同じウィンドウ内に縦に並びます。コンテキストは分かれているものの、どのターミナルがどのプロジェクトのものか、一目でわかりづらい。
各プロジェクトで別々のClaude Codeを動かしたいのに、「このターミナルはどのプロジェクトのClaude Codeだっけ?」と混乱することが増えました。
拡張機能との相性問題
マルチルートワークスペースに完全対応していない拡張機能もあり、微妙に期待通り動かないケースがあります。
Mission Controlでウィンドウを探す日々
結局、プロジェクトごとに別ウィンドウを開くスタイルに落ち着きました。
でも今度は、複数のVSCodeウィンドウから目的のプロジェクトを探すのが大変になります。Mission Controlを開いて、ウィンドウのタイトルを目視で確認して、クリック。Control + ↓でアプリケーションExposéを使えば同じアプリのウィンドウだけ表示できますが、それでも5つのウィンドウから目的のものを探す手間は変わりませんでした。
GhosttyならタブでCmd+1〜Cmd+9で一発切り替えできたのに...
自作することにした
「無いなら作ればいい」
Ghosttyで体験した「タブで即切り替え」をVSCodeでも実現したくなりました。ついでに、Claude Codeとの連携機能も欲しい。
作ったもの
今までのエディタを使いつつ、並列でプロジェクトを横断して開発できる状態にしたかったので、シンプルにこの2つを実現するものを作りました。
-
タブバーで即プロジェクトを切り替えできる
- 常に画面上部に表示されるタブバー
-
Cmd+1〜Cmd+9で対応するプロジェクトに切り替え - ドラッグ&ドロップでタブの順序を変更可能
-
Claude Codeの入力待ち通知
- Claude Codeが「入力待ち」状態になったらタブにバッジを表示
- 別のプロジェクトで作業中でも、完了に気づける

こんな感じで通知される!
これにより、マルチルートワークスペースを使わずに、シンプルなエディタ状態を保ちつつ、ささっと切り替えて確認できる俺得の状態にすることができました。おまけにAI出力が終わったのかも確認できるという。
ターミナルでかっこよく作業することは叶わなかったし、エディタを何個も立ち上げるので、メモリはやっぱり食うけど、満足しています。
技術選定
デスクトップアプリを作るにあたり、以下の技術を選びました。
- Tauri 2: Electronより軽量で、macOSネイティブAPIにアクセスしやすい
- React + TypeScript: フロントエンドは慣れたスタックで
- Rust: バックエンドでmacOSのAPIを呼び出す
Electronも検討しましたが、メモリ使用量が気になりました。常駐アプリなので、できるだけ軽くしたい。Tauriならバンドルサイズも小さく、メモリ消費も抑えられます。
実装のポイント
1. macOS NSWorkspaceでアプリ切り替え検知
アプリがアクティブになったことを検知するには、macOSのNSWorkspaceを使います。
// NSWorkspaceDidActivateApplicationNotification を監視
let notification_name =
NSNotificationName::from_str("NSWorkspaceDidActivateApplicationNotification");
notification_center.addObserverForName_object_queue_usingBlock(
Some(¬ification_name),
None,
Some(&main_queue),
&block,
);
VSCode/Cursor/Zedがアクティブになったら、フロントエンドにイベントを発火。タブバーの表示を更新します。
2. Accessibility API でウィンドウ操作
当初は AppleScript でウィンドウ操作を実装していましたが、使っているうちに問題が出てきました。
- タブ順序が不安定: AppleScript の結果が毎回微妙に変わり、タブの並び順が壊れる
- 遅延が大きい: 1回の呼び出しに 200-500ms かかる
- 高頻度アクセスで破綻: Cmd+Space 連打などで不安定に
そこで macOS の Accessibility API(AXUIElement)に移行しました。
use accessibility::{AXUIElement, AXUIElementActions, AXUIElementAttributes};
// プロセスIDからアプリのAXUIElementを作成
let app = AXUIElement::application(pid);
let windows = app.windows()?;
for window in windows {
let title: String = window.title()?;
let is_main: bool = window.is_main()?;
// ...
}
結果は劇的でした。
| 手法 | 速度 | 信頼性 |
|---|---|---|
| AppleScript | 200-500ms | 不安定 |
| Accessibility API | 10-50ms | 安定 |
ウィンドウタイトルからプロジェクト名を抽出する処理はRust側で行い、VSCode/Cursor/Zedそれぞれで微妙にタイトル形式が違うので、パース処理は少し工夫が必要でした。
3. Claude Code hooksとの連携
Claude Codeには「hooks」という機能があります。特定のイベントが発生したときに、任意のスクリプトを実行できる仕組みです。
これを使って「入力待ち状態」を検知します。
// ~/.claude/settings.json
{
"hooks": {
"Notification": [
{
"matcher": "permission_prompt",
"hooks": [{
"type": "command",
"command": "echo $PWD >> /tmp/claude-code-waiting"
}]
}
],
"Stop": [
{
"hooks": [{
"type": "command",
"command": "echo $PWD >> /tmp/claude-code-waiting"
}]
}
]
}
}
仕組みはシンプルです:
-
Notification(permission_prompt): 権限確認ダイアログが出たら、作業ディレクトリを/tmp/claude-code-waitingに追記 -
Stop: Claude Codeが停止(入力待ち)になったら、作業ディレクトリを追記
Editor Tab Manager側では、このファイルを500msごとにポーリングして、変更があればフロントエンドに通知します。
const CLAUDE_WAITING_FILE: &str = "/tmp/claude-code-waiting";
// 500msごとにファイルを監視
while NOTIFICATION_WATCHER_RUNNING.load(Ordering::SeqCst) {
let paths = read_waiting_paths();
if paths != last_paths {
// 変更があればイベントを発火
window.emit("claude-notification", payload);
last_paths = paths;
}
thread::sleep(Duration::from_millis(500));
}
ファイルシステム監視ではなくポーリングにしたのは、シンプルさを優先したためです。500msなら実用上問題ない応答速度ですし、CPU負荷も無視できるレベルです。
使い方・導入方法
インストール
GitHub Releasesからダウンロードできます。
-
.dmgファイルをダウンロード - マウントしてアプリケーションフォルダにドラッグ
- 初回起動時は右クリック→「開く」で起動(署名がないため)
基本的な使い方
- アプリを起動すると、画面上部にタブバーが表示されます
- VSCode/Cursor/Zedを開くと、自動的にタブが追加されます
-
Cmd+1〜Cmd+9でタブを切り替え - ドラッグ&ドロップでタブの順序を変更(順序は保存されます)
Claude Code hooksの設定
Claude Codeの通知機能を使うには、hooksの設定が必要です。
~/.claude/settings.jsonに以下を追加してください:
{
"hooks": {
"Notification": [
{
"matcher": "permission_prompt",
"hooks": [{
"type": "command",
"command": "echo $PWD >> /tmp/claude-code-waiting"
}]
}
],
"Stop": [
{
"hooks": [{
"type": "command",
"command": "echo $PWD >> /tmp/claude-code-waiting"
}]
}
]
}
}
- Notification (permission_prompt): 権限確認ダイアログが出たとき
- Stop: Claude Codeが処理を完了して入力待ちになったとき
どちらのタイミングでも「ユーザーの操作が必要」なので、タブにバッジを表示します。タブをクリックしてウィンドウをアクティブにすると、アプリ側でそのプロジェクトのパスをファイルから削除するため、追記し続けてもファイルが肥大化することはありません。
まとめ
敗北を知ったが、AIによって別の代替手段を容易に実現できることに感動
そもそもGhosttyとか最近のかっこいいターミナル環境でサクッと仕事ができればこのアプリとかも作ろうとは思わなかったわけですが、あったら良いな。が本当にすぐに実現できる時代になってとても嬉しいです。前だったら考えても実現するのに時間がかかってこうして記事にすることはできなかったと思います。
ここまで読んでいただきありがとうございました。
Discussion