blog.syfm

徒然なるままに考えていることなどを書いていくブログ

fzf ライクな fuzzy-finder を提供する Go ライブラリを書いた

f:id:ktr_0731:20190209113907p:plain

fuzzy-finder

fzffzyskim などの fuzzy-search (あいまい検索) を提供する CLI ツールの登場により、コマンドラインでの操作はますます表現豊かになっています。
例えば、カレントシェルのコマンドヒストリの一覧から fuzzy-finder を使って選択したり、ghq + fuzzy-finder + cd の組み合わせで選択したリポジトリのあるディレクトリへ移動したりといったケースが挙げられます。

また、fuzzy-finder それ自体をコア機能として組み込んだツールも増えています。例えば、cd コマンドの拡張である enhancd はパスの履歴を保持し、fuzzy-finder を使って移動したいディレクトリへすぐに移動することができます。また、拙作の itunes-cli は、iTunes で管理されている曲の一覧を入力にし、fuzzy-finder で選択・再生を行う機能を提供しています。

fuzzy-finder ツールの限界

しかし、fuzzy-finder は同名の要素を扱うことができない問題があります。
たとえば、先程紹介した itunes-cli を例として考えてみます。曲名はユニークではないので、当然衝突する可能性があります。そういった場合に fuzzy-finder で適切に選択するにはその他の情報も曲名と一緒に表示する必要がありますが、

  • 曲名は同じだが、アーティストが違う
  • 曲名もアーティストも同じだが、収録アルバムが違う
  • 曲名も収録アルバムも同じだが、アーティストが違う

ちょっと考えただけでこれだけのケースが挙げられます。そして、これらのケースをユニークに扱うためには、一行に曲名、アーティスト名、収録アルバム名を表示しなくてはならないため、ユーザの視点から見ると非常に冗長だと感じるでしょう。

インストールの複雑さ

インストールの複雑さに関する問題は、fuzzy-finder を組み込むツールをインストールする場合に発生します。そういったツールは内部でコマンド実行をすることで fuzzy-finder を起動しますが、そのためには fuzzy-finder が既にインストールされていなければいけません。もしその環境に fuzzy-finder がインストールされていないのであれば、ツールとは別に fuzzy-finder をインストールしなければいけません。また、ツールが検知するために環境変数などで fuzzy-finder のパスを指定しなければいけないかもしれません。

fuzzy-finder をライブラリとして提供する

github.com

上記二つのような問題を解決するために go-fuzzyfinder をつくりました。例えば以下のようなコードを書くだけで簡単に fuzzy-finder を起動できます。

type Track struct {
    Name      string
    AlbumName string
    Artist    string
}
var tracks = []Track{
    {"foo", "album1", "artist1"},
    {"bar", "album1", "artist1"},
    {"foo", "album2", "artist1"},
    {"baz", "album2", "artist2"},
    {"baz", "album3", "artist2"},
}

idx, err := fuzzyfinder.Find(tracks, func(i int) string {
    return tracks[i].Name
})

f:id:ktr_0731:20190208235230p:plain

この関数は戻り値として選択されたスライスのインデックスを返します。
しかし、上記の UI では先程挙げた、同名の行をうまく扱えない問題が発生しています。普通の fuzzy-finder の場合、この問題を解決するには一行あたりの情報量を増やすしかありません。 go-fuzzyfinder ではこの問題に対処するためにプレビューウインドウを提供しています。この機能は fzf を非常に参考にしています。 プレビューウインドウ付きで fuzzy-finder を起動するためには、以下のようにコードを変更します。

idx, _ := fuzzyfinder.Find(
    tracks,
    func(i int) string {
        return tracks[i].Name
    },
    fuzzyfinder.WithPreviewWindow(func(i, w, h int) string {
        return fmt.Sprintf("%s\n\nArtist: %s\nAlbum:  %s",
            tracks[i].Name,
            tracks[i].Artist,
            tracks[i].AlbumName)
    }))

f:id:ktr_0731:20190209000431p:plainf:id:ktr_0731:20190209000439p:plain
従来の方法とプレビューウインドウを使う方法

左の画像は従来の fuzzy-finder が取らなければならない方法で、右の画像がプレビューウインドウを使った方法です。付随する情報をプレビューウインドウへ分離したことでシンプルなインターフェースになっています。

この他にもいくつか機能がありますが、詳しくは GoDoc をご覧ください。

まとめ

fuzzy-finder をライブラリ化したことで、従来では難しかったことも実現できるようになりました。ぜひ使ってみてください!