blog.syfm

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

go-fuzzyfinder で色をつけられるようになった

個人的にほそぼそと開発をしていた ktr0731/go-fuzzyfinder を使うと fzf のような UI を持つ CLI ツールを非常に簡単につくることができます。go-fuzzyfinder では、Preview Window という、現在選択しているアイテムの詳細情報を知らせるための表示領域を持っています。これは非常に便利な機能ですが、一方で未実装の機能が一つありました。

それが Preview Window 内の文字列にスタイルをつけて表示する機能です。fzf の --preview フラグでは同様に Preview Window を表示できますが、こちらでは色をつけた表示も可能であるため、go-fuzzyfinder もこの機能を導入したいと考えていました。

そして最近になってようやくこの機能を入れることができました。この記事では、この機能をどのように使うことができるのか、そしてどのような設計・実装になっているのかを紹介します。

使い方

API に変更はありません。go-fuzzyfinder を v0.7.0 にアップデートするだけでこの機能を使えるようになります。 PreviewWindow に表示される文字列に ANSI エスケープシーケンス が含まれている場合、go-fuzzyfinder はそれを解釈し、パラメータに応じてスタイルが適用されます。

たとえば、ターミナルで以下のようなコマンドを実行してみます。Hello, の部分はボールドされており、World! の部分は赤色になっています。

$ echo '\x1b[1mHello, \x1b[0;31mWorld!'

go-fuzzyfinder の PreviewWindow でも同じ文字列を表示してみます。

ターミナルで表示したときとまったく同じような表示となりました。

同様に、ファイルを bat で開き、その出力を go-fuzzyfinder の PreviewWindow で表示してみた例です。bat は cat のような振る舞いをしますが、シンタックスハイライト付きのリッチな表示をします。PreviewWindow でも同様にカラフルな表示となりました。

このように、go-fuzzyfinder がエスケープシーケンスを解釈できるようになり、PreviewWindow の表示をよりリッチにできるようになりました。

Select Graphic Rendition

Terminal で色を付けるための機能は Select Graphic Rendition (SGR) と言います。これはANSI エスケープシーケンスの仕様の一つで、色だけでなくボールドやイタリックといった表現もできます。

たとえば先ほどの例では以下のような文字列でした。

$ echo '\x1b[1mHello, \x1b[0;31mWorld!'

\x1b[CSI (Control Sequence Introducer) と呼ばれるもので、これにより ANSI エスケープシーケンスの開始を示しています。あとに続くセミコロンで区切られた数値はパラメータで、このパラメータによりどのような表示にするかを指定できます。最後に m が付き、これによりパラメータの終了を示します。 今回の例では、先頭にさっそく CSI が現れており、パラメータとして 1 が指定されています。これはボールドを示しており、表示を見ると Hello, が太字になっているのが分かると思います。また、後続の CSI では 0 および 31 が指定されています。0 は既存のスタイルをすべてリセットし、31 は文字列を赤色にします。

色の指定にはいくつかの種類があり、それぞれ指定方法が異なっています。 もっとも基本的なものは 16 色のみの指定ができ、単一の値を取ります。

\x1b[31m

次に 256 色の指定方法があります。パラメータは文字色は 38;5、背景色は 48;5 から始まり、n の値によりどの色を選択するかを指定します。

\x1b[38;5;<n>m
\x1b[48;5;<n>m

次に RGB の指定方法があります。これは True Color とも呼ばれています。パラメータは文字色は 38;2、背景色は 48;2 から始まり、<r><g><b> の値によって色を指定します。 RGB で 16 色および 256 色のそれぞれの色も表現できますが、これらは区別できなければいけません。ターミナルにカスタムテーマが適用されている場合、RGB による指定ではこの設定が無視されます。

\x1b[38;2;<r>;<g>;<b>m
\x1b[48;2;<r>;<g>;<b>m

SGR と gdamore/tcell

go-fuzzyfinder はその描画に gdamore/tcell を利用しています。tcell でもスタイルを適用できますが、SGR をそのまま解釈できるわけではありません。そのため、入力となる文字列をなんらかの形でパースし、tcell の API に適合できるように落とし込んで上げる必要があります。 SGR のシーケンスを含んだ文字列をパースできるライブラリはいくつか存在していました。ただ、一部の機能が不十分だったり、go-fuzzyfinder の既存実装にうまく馴染むものがありませんでした。

そこで、1 からライブラリを実装することにしました。それが ktr0731/go-ansisgr です。

ktr0731/go-ansisgr

go-ansisgr では、NewIterator という関数があり、これが唯一のエントリポイントとなっています。この関数はイテレータを返し、このイテレータエスケープシーケンスを除いた文字と、その文字に適用されているスタイルを返します。

実際の例を見てみましょう。

in := "\x1b[1mHello, \x1b[0;31mWorld!"
iter := ansisgr.NewIterator(in)

for {
        r, style, ok := iter.Next()
        if !ok {
                break
        }

        // do nothing.
}

iter.Next() による 3 つの戻り値があります。

okイテレータの終端を表します。 r は次の文字を表す rune 型の値です。この例ではループごとに Hello …のようになります。

style はその文字に適用されているスタイルです。こちらは少し複雑になっています。ForegroundBackground というメソッドがあり、それぞれ文字色と文字の背景色を表しています。このメソッドの戻り値により、それぞれに色が設定されているかどうかがわかります。

color, ok := style.Foreground()

oktrue だった場合、色が設定されています。color を見ることでどんな色が設定されているかがわかります。 color.Mode() は、その色が 16 色、256 色、RGB のどの形式で指定されているかを表しています。また、color.Value() はその形式におけるパラメータの値です。たとえば \x1m[31m では 31、\x1m[38;5;120m では 120、\x1m[38;2;10;20;30m では 660510 (0x0a141e) となります。 RGB の場合、color.RGB() により R、G、B それぞれの値を取得することもできます。

また、style には Bold()Italic() のような bool を返すメソッドがあり、この値によってどのスタイルが適用されているかを表します。

API の一覧について、より詳しくは ドキュメント を参照してください。

go-fuzzyfinder への組み込み

こうしてできた go-ansisgr を go-fuzzyfinder へ組み込みました。gdamore/tcell は一文字ずつ描画していくため、go-ansisgr のイテレータのようなインターフェースとの組み合わせが良く、go-fuzzyfinder へ組み込んだときも大きなコードの変更なしに導入できました。*1

実際の diff は こちら から見ることができます。既存の描画のためのループ内で iter.Next() を呼び、描画対象文字とそのスタイルを順番に取得しています。

まとめ

go-fuzzyfinder で SGR がサポートされ、スタイルを適用した文字列を PreviewWindow で表示できるようになりました。 この機能の大部分は go-ansisgr によって実装・提供されており、このライブラリは入力となる SGR を含んだ文字列をイテレータ形式で扱うことができ、特に gdamore/tcell のような一文字ずつ描画していくようなライブラリとの相性が良いです。

go-fuzzyfinder も go-ansisgr もぜひお試しください!

*1:そうなるように API を設計したので当たり前といえば当たり前ですが…