【OSS】Rust + gpuiで高速なファイルエクスプローラを作り始めた話
この記事はRust Advent Calendar 2025 14日目の記事です.
はじめに
こんにちは.普段はバックエンドを書いているsyuya2036です.
突然ですがみなさん,MacのFinder,満足していますか?
僕は満足していません.毎日触るツールだからこそ,もっと手に馴染む,エンジニアのための道具が欲しいと思い,あの爆速エディタZedでも使われているUIフレームワークgpuiを使って,新しいエクスプローラの開発を始めました.
最初はMac向けに作っていますが,将来的にはクロスプラットフォーム化も考えています.
開発を始めたばかりなのでまだファイルの一覧表示くらいしか実装できていませんが,この記事では,gpuiって何なのか/なぜ作り始めたのか,あたりをまとめます.
GitHub のリポジトリに公開していますので,興味がある方はぜひぜひ覗いてみてください.
名前はNohrs(ノアーズ)です.

日本語でも良いので,PRやIssue大歓迎です!
なぜNohrsを作り始めたのか
gpuiについては後で書くとして,まずはなぜエクスプローラを開発しようと思ったのかについて書きます.
Finder のココがツラい...
Finderは完成度が高い一方で,開発者視点だと地味にストレスが溜まります.自分が特にツラいと思ったのは以下の辺りでした.
1. 検索が微妙すぎる
Finder,検索は普通に便利なんですが,若干遅いですし,検索結果がごちゃついてると感じてしまいます.
pdfファイルが欲しくて検索してるのによくわからんスクリプトとかがヒットしてしまったりします.
NohrsではRustの高速性を活かしつつ,ユーザーが欲しいものを的確に出せるような検索体験を目指しています.
例えば,以下の要素を考慮して検索スコアを決めるようなイメージで考えています.
- 最近開いたファイルのスコアを上げる
- gitignore にマッチするファイルを除外する設定がかけられる
- ファイルタイプごとに重み付けを変える
- 例: ドキュメント系を優先
2. ファイルを作れない
サクッとテキストファイルを作って編集して保存する,みたいなことができません.
右クリックメニューから「新規フォルダ」は作れますが,「新規テキストファイル」は作れないのです.
3. 開発者向け機能が乏しい
Finderから
- Terminalでここを開きたい
- VS Codeでフォルダを開きたい
- Gitの状態だけサッと見たい
- 変更点の差分だけ確認したい
ということがたまにあると思うのですが,できません.
4. カスタム性・拡張性が弱い
OS標準なので当たり前ですが,カスタム性や拡張性が弱いです.
例えば,キーバインドをVimライクに変えたり,プラグインで機能を追加したり,といったことができません.
実装したい機能
基本的には上記の不満点を解消するような機能を実装していきたいと考えています.
その他に実装したいと考えているものを挙げます.
- S3互換ストレージやFTPサーバーなど,ローカル以外のストレージ対応
- Dockerコンテナ内のファイル操作・ボリューム操作
- 拡張機能を作成して機能追加できる仕組み
- 使用者が自分で拡張機能を作れるようにする
また,こんな機能があれば良さそうというアイデアがあったら,Issueを立ててくださると泣いて喜びます.
実装方針
まずは基本的なファイルエクスプローラとして十分に実用的なものを目指していくので,当面は
- コア
- ファイル走査
- 検索インデックス
- UIの骨格
- タブ
- 分割
- プレビュー枠
- 入出力の詰め
- ドラッグ&ドロップ
- コンテキストメニュー
を開発していくつもりです.
そのあと,拡張機能の形で,開発者向けの便利機能を追加していきたいと考えています.
gpuiとは
(ちなみに筆者はRust初学者で,勉強がてら作り始めたという背景もあるので,間違った記述があればぜひご指摘ください.)
gpuiは,Zedエディタのために作られたRust製UIフレームワークで,GPUアクセラレーションを前提にしたハイブリッドimmediate + retainedモデルのUIを提供します.
immediate + retainedモデルというのは,雑に言うと,
- 見た目は宣言的に組み立てたい
- UIツリーをそれっぽく書きたい
- でも毎フレーム全部作り直すのは避けたい
- 性能と差分更新が欲しい
- Rustの所有権と相性の良い形でイベントと状態更新を扱いたい
という要求をまとめて殴り込んだやつ,という理解です.
UIはdiv()のようなビルダーで組み立て,flexや色,フォントサイズなどをチェーンで付けていくスタイルが基本です(cssっぽい気分で書ける).
View
gpuiの中心はRenderトレイトです.Renderを実装したものが画面に描画されるViewとして扱われます.
pub trait Render: 'static + Sized {
fn render(
&mut self,
window: &mut Window,
cx: &mut Context<'_, Self>,
) -> impl IntoElement;
}
このrenderの中でdiv()などを組み立てて返す,というのが基本形です.ポイントは&mut selfで,View自身が状態を持てる設計になっていることです.
公式のHello World
Application::new().run(...)の中でwindowを開き,cx.new(|_| View)でViewを生成するという流れが最初の取っ掛かりになります.
html/css的な書き味でかけるのが個人的には良いと思っています.
use gpui::{
div, prelude::*, px, rgb, size, App, Application, Bounds, Context, SharedString, Window,
WindowBounds, WindowOptions,
};
struct HelloWorld {
text: SharedString,
}
impl Render for HelloWorld {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
div()
.flex()
.flex_col()
.gap_3()
.bg(rgb(0x505050))
.size(px(500.0))
.justify_center()
.items_center()
.shadow_lg()
.border_1()
.border_color(rgb(0x0000ff))
.text_xl()
.text_color(rgb(0xffffff))
.child(format!("Hello, {}!", &self.text))
.child(
div()
.flex()
.gap_2()
.child(div().size_8().bg(gpui::red()))
.child(div().size_8().bg(gpui::green()))
.child(div().size_8().bg(gpui::blue()))
.child(div().size_8().bg(gpui::yellow()))
.child(div().size_8().bg(gpui::black()))
.child(div().size_8().bg(gpui::white())),
)
}
}
fn main() {
Application::new().run(|cx: &mut App| {
let bounds = Bounds::centered(None, size(px(500.), px(500.0)), cx);
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
..Default::default()
},
|_, cx| {
cx.new(|_| HelloWorld {
text: "World".into(),
})
},
)
.unwrap();
});
}
イベント処理
gpuiにはクリックやマウス移動,キーイベントなどのインタラクションAPIが用意されていて,on_clickなどでイベントをバインドできます.
ここで重要なのがContext::listenerです.イベントハンドラでselfを雑にキャプチャすると所有権まわりが破綻しやすいので,gpuiはViewの状態にアクセスするための安全な窓口としてlistenerという仕組みを提供しています.
サンプル(divをボタンっぽくしてカウントアップ)
gpui単体だとButtonのような要素が最初から用意されているわけではなく,divにマウスイベントを付けてButtonっぽく扱うのが基本になります.
use gpui::{div, prelude::*, MouseButton, Window, Context};
struct Counter {
count: i32,
}
impl Render for Counter {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
.child(format!("count = {}", self.count))
.child(
div()
.child("increment")
.on_mouse_down(
MouseButton::Left,
cx.listener(|this, _evt, _window, cx| {
this.count += 1;
cx.notify(); // 再描画を要求
}),
),
)
}
}
cx.notify()が再描画を要求する部分です.イベントハンドラ内でthisを通してViewの状態にアクセスできるのがポイントです.
非同期とUIを止めない設計がやりやすい
gpui(というよりZedの設計)は「Never block the main thread」をかなり明確に打ち出していてcx.spawnやbackground_executor()などを通して,メインスレッドとバックグラウンドを分けて扱えます.
Zedの例だと,一定時間待ってから状態を戻す,みたいな処理をこんな雰囲気で書いています.
cx.spawn(|this, mut cx| async move {
cx.background_executor().timer(CURSORS_VISIBLE_FOR).await;
this.update(&mut cx, |this, cx| {
this.show_cursor_names = false;
cx.notify()
}).ok();
}).detach();
ファイルエクスプローラだと,
- バックグラウンドでディレクトリ走査
- 結果が揃ったらUI状態を
updateで反映 -
notifyで再描画
みたいな流れを,この型の上で安全に組みやすいのが大きいです.
コンポーネントライブラリ
gpui自体はあくまでUIフレームワークであり,ボタンやリスト,テキストフィールドなどのコンポーネントは含んでいません.
しかし,gpui用のコンポーネントライブラリとしてgpui componentというものがあり,InputやSelectなどの基本的なものから,高度な仮想化リストコンポーネントまで提供されています.
仮想化リストは表示可能なものだけをレンダリングすることで大規模なデータを高効率で表示できます.
エクスプローラの開発においては,大量のファイルを表示する必要があるため,この仮想化リストコンポーネントを活用しています.
おわりに
最後まで読んでいただきありがとうございました.
最強のファイルエクスプローラを目指して頑張ります!
参考リンク
Discussion