fz-browse: fzfライクな自由度の高いインタラクティブな検索ツール、ただしブラウザに表示する
fz-browseという、fzfライクなインタラクティブな検索ツールを書きました。
fz-browse自体は検索エンジンを持っているわけではなく、既存のripgrepやripgrep-allなどのツールと組み合わせて、インタラクティブな検索体験を自分で組み立てるツールです。
コマンドを組みあわせたインタラクティブな検索(絞り込み)はpercol、peco、fzfなどが有名ですが、これらは検索の結果や操作もターミナル上で行います。
ターミナルは、書籍や画像などのコンテンツを検索した結果を表示してもあまり楽しくない場合があります。 fz-browserでは既存のコマンドラインツールを組み合わせて検索するのは同じですが、この検索結果の表示や操作をウェブブラウザ上で行います。
ブラウザで表示できるので、検索結果がPDFやEpubの書籍だったらそのままビューアで開いたり、画像だったらそのまま表示できるようになっていて、テキスト以外の結果にも対応しています。
ripgrepなどの既存のコマンドを組み合わせて、自分用の検索エンジンを作るツールを書きました。
— azu (@azu_re) April 17, 2022
ブラウザ上で、ローカルの書籍を検索したり、Exifベースで画像検索できます
"fz-browse: fzfライクな自由度の高いインタラクティブな検索ツール、ただしブラウザに表示する"https://t.co/YQBptZvmXG pic.twitter.com/dxgj3WDT3z
ripgrep-allを使って、PDFやEpubを検索している様子
ExifToolを使って、画像のExifに含まれるコメントや日付を検索している様子
使い方
fz-browseはNode.jsで動いているツールなので、npmでインストールします。
npm install --global fz-browse
最初に書いていたように、fz-browse自体は検索エンジンではなく、既存のコマンドを組み合わせます。
テキストを検索する
ripgrepで検索する例です。
次のコマンドを実行すると、検索欄に入力するたびに--run
フラグに指定されているrg
コマンドで検索した結果が表示されます。
また、検索したファイルのリンクを開くと、--preview
フラグで指定されているrg
コマンドで検索した結果(前後5行)が詳細表示されます。
fz-browse --run $'rg --ignore-case {input} --json | jq \'if .type == "begin" or .type == "match" then . else empty end | [.data.path.text, .data.lines.text] | @tsv\' -r' --preview "rg --context 5 {input} {target}" --open
電子書籍を検索する
ripgrep-allを使ってPDFやepubなどの電子書籍を検索する例です。
- 必要なもの:
次のコマンドを実行すると、検索欄に入力するたびに--run
フラグに指定されているrga
コマンドで検索した結果が表示されます。
また、検索したファイルのリンクを開くと、PDFとEpubはビルトインのビューアによってそのままブラウザで開いて読めるようになっています。
それ以外のファイルは、--preview
フラグで指定されているrga
コマンドで検索した結果(前後5行)が詳細表示されます。
fz-browse --run $'rga --ignore-case {input} --json | jq \'if .type == "begin" or .type == "match" then . else empty end | [.data.path.text, .data.lines.text] | @tsv\' -r' --preview "rga --context 5 {input} {target}" --open
画像ファイルのEXIFを検索する
画像ファイルのEXIFを元に検索する例です。
- 必要なもの:
次のコマンドを実行すると、検索欄に入力するたびに--run
フラグに指定されているコマンドで、画像データのEXIFに対してgrepして画像を絞り込めます。
exiftool -q -m -p \'$Directory/$Filename $DateTimeOriginal $Comment\'
で画像ファイルのEXIFをテキストとして取り出してgrepでそれを絞り込む仕組みです。
また、検索したファイルのリンクを開くと、jpgやpngなどの画像はビルトインのビューアによってそのままブラウザで表示できます。
それ以外のファイルは、--preview
フラグで指定されているechoでファイル名が表示されます。
fz-browse --run $'find -E . -iregex ".*\.(jpg|gif|png|jpeg)$" -print0 | xargs -0 exiftool -q -m -p \'$Directory/$Filename $DateTimeOriginal $Comment\' | grep {input} | awk \'{print $1}\'' --preview "echo {target}" --open
特定のディレクトリを対象にしたい場合は、--cwd
で実行ディレクトリを指定できます。
基本的には、--run
で指定したコマンドが結果を<filePath>\t<content>
のTSVとして返すようにして、それをブラウザで逐次表示できるような形になっています。
その他の使い方は、READMEを参照してください。
また、いい使い方があったら、Wikiに追加してみてください!
仕組み
fz-browseは、ローカルサーバを立ち上げて、ブラウザから立ち上げたローカルサーバにアクセスして利用します。
検索窓にテキストを入れると、サーバにリクエストが発生して--run
で指定したコマンドを実行した結果をNode.js Streamでレスポンスとして返し、レスポンスをWeb Streams APIでストリーミングしながら取得して、表示しています。
これによって、コマンドの実行中でも検索結果が徐々に表示されていくような、ターミナルでの検索と似た体験ができるようになっています。
ストリームで取得したデータでStateを頻繁に更新すると、入力中に検索結果の要素が頻繁に更新されるため、表示が固まってしまいます。 これに対してはReact 18で追加されたuseTransitionを使って、検索結果の表示の優先度を落とすことで対応しています。
ビルドインのプレビュー表示として、PDF.jsを使ったPDFビューア、Epub.jsを使ったepubビューアを実装してあります。
また、構想としてはこのプレビュー自体のカスタマイズに対応できるようにある程度整理しています。(まだユースケースがわからないので切り出しはいないです) 興味がある人はいじってみてください。
- https://github.com/azu/fz-browse/blob/5983d39fbda238c2cb5eacc9092a537f3767ee50/app/App.tsx#L233-L242
モチベーション
モチベーションとしては、ローカルの書籍を検索したくて、ちょうど良いツールが見つからなくて作り始めました。 ripgrep-allで検索はできるのですが、ターミナルにテキストで書籍の中身が出てきてもあまり楽しくないという感覚がありました。
はてなブックマーク検索PWAやazu/mytweetsなどを書いた経験的に、検索はブラウザでインタラクティブにできてすぐ結果が出てくると楽しい感じがしました。
- モバイル/オフラインでも動作するはてなブックマーク検索のPWAを作った | Web Scratch
- 自分のTweetsをインクリメンタル検索できるサービス作成キット と Tweetsをまとめて削除するツールを書いた | Web Scratch
最初は、検索エンジン自体もRustで書こうとしていましたが、ripgrep-allとかripgrepとかugrepとか既に色々あるので、既存の検索コマンドのフロントエンドがあればいいのかなと考えました。
fzfのGUI版みたいのがあればいいのかなーと思って、PoCを書いてみたら意外と書けそうだったのでfz-browseを作りました。
PDFとepubの検索表示やってみようと思って1時間ちょっとでいい感じにできてきた。https://t.co/PsABi54m0Ahttps://t.co/kHTOg2obma
— azu (@azu_re) March 28, 2022
でそれぞれ検索のための機能があってよかった。
動画はテスト用データでやってるけど、実際のデータ数(大体数百倍)でやっても1秒以内ぐらいには結果返ってきて面白い pic.twitter.com/PGFouqfXhj
電子書籍の検索だけに絞ってもいいかなーとは思ってたんですが、色々デザイン考えていたら、ある程度汎用的な感じにできそうだったので、汎用的な仕組みなりました。
汎用的にできそうと感じたのは、コマンドの実装結果をTSVでブラウザに返すというアイデアを思いついたところあたりだった気がします。
(コマンドでJSONみたいな構造を作るのはなんか違うし、ブラウザではストリーム表示したいので行で表示できるものを探しててTSVになったという感じ。jq
がサポートしてたのも大きい)
電子書籍検索だけに絞るか、汎用的なfzf的なものにするかで迷うな
— azu (@azu_re) March 29, 2022
Node.jsのツールになった理由はあんまりありませんが、ViteのSSRサーバを書いてみたかっただけです。 開発体験的にHot Reloadとかできるので悪くはないです。サイズとかは気になるけど、結局ブラウザを扱うツールなのでJavaScriptでいいかなと。
おわりに
fz-browseという、コマンドを組み合わせて自分好みの検索ツールを作るツールを書きました。 検索自体はブラウザで行えるので、テキスト以外のものの結果も扱いやすかったり、普通に検索して表示できます。
まだ作ったばかりのツールなので、面白いアイデアがあったらIssueください。
お知らせ欄
JavaScript Primerの書籍版がAmazonで購入できます。
JavaScriptに関する最新情報は週一でJSer.infoを更新しています。
GitHub Sponsorsでの支援を募集しています。