##はじめに
ANSIエスケープコード(or エスケープシーケンス)とよばれる文字列を使うことで、端末上で文字に色を付けたり、文字のフォーマットを変更したり、カーソルを操作することができます。
詳細はWikipedia(英語)をご覧ください。
いろんな言語でエスケープコードを扱うライブラリが開発されており、Goでも同様のライブラリは存在します。おそらく有名なのはmgutz/ansiかfatih/colorでしょう。
単に色を付けるだけならこれらでもいいとは思いますが、以下の点から新たにライブラリを作りました。
- 色やスタイルの指定が文字列ベースでコンパイル時にチェックできない(mgutz/ansi)
- 3ビット(8)色は使えるが、8ビット(256)色が使いにくい。
- 色とスタイル以外のエスケープコードが使えない
3つ目に関しては上記のライブラリの範囲外なので仕方ないとしても、2つ目の8ビット色が使いにくいのは残念です。ちなみに、使いにくいというのは8ビット色を0から255の値で指定するということです。(例えば144といわれてもどんな色か想像できませんよね?)
ライブラリの紹介
aec(Ansi Espace Code)というライブラリを作りました。
ソースコードはGitHubにあります。
aecでは文字の色付けやスタイル変更を含めた多くのエスケープコードをサポートしています。
aecの特徴を簡単に紹介します。
- カーソルの移動や表示・非表示の切替え
- 画面・行に表示されている文字の消去
- スクロール
- 文字の色付け・スタイル変更
- RGB(24ビット)から3ビット色や8ビット色への変換
- エスケープコードを簡単に作成するための構文
エスケープコードが動くかどうかは端末環境に依存するので、動かないものも結構あると思いますが、Wikipediaに書いてあって、使いそうなものはほとんどサポートしています。
サポートしている具体的なエスケープコードはGitHubやGoDocに書いてあるので、ここではライブラリの設計や使い方を紹介します。
ライブラリの設計
aecでは一部を除いたすべてのエスケープコードをANSI
というインターフェースで表現しています。
// ANSI represents ANSI escape code.
type ANSI interface {
fmt.Stringer
// With adapts a given ANSI.
With(ANSI) ANSI
// Apply wraps given string in ANSI.
Apply(string) string
}
ANSI.Apply
では与えられた文字列にエスケープコードを適用できます。
また、fmt.Stringer
をサポートしているので、ANSI.String()
によってエスケープコードを取り出すことができます。
以下の2つは同じ出力をします。
ansi := aec.RedB
fmt.Println(ansi.String() + "Hello" + aec.Reset)
fmt.Println(ansi.Apply("Hello"))
カーソル移動のエスケープコードなどではaec.Reset
が不要なので、少しでもパフォーマンスにこだわる場合にはANSI.Apply
は使わない方がいいと思います。
ANSI.With
ではエスケープコードを合成することができます。
例えば、背景が緑、文字が黒、右に2つ移動するというエスケープコードは以下のように表すことができます。
ansi := aec.GreenB.With(aec.BlackF).With(aec.Right(2))
これを少し簡単に組み立てるための構文(ビルダー)も用意しています。
ansi := aec.EmptyBuilder.GreenB().BlackF().Right(2).ANSI
ビルダーを使った方が読みやすいと思います。
RGBによる色付け
aecではRGB(24ビット)によってより直感的に色の指定を行うことができます。
例えば、RGBから3ビット色への変換は以下のように行うことができます。
color3bit := aec.NewRGB3Bit(255, 200, 100)
ansi := aec.Color3BitF(color3bit)
これによって黒・赤・緑・黄・青・マゼンタ・シアン・白のうち最も近い色に変換されます。
上記の例では(255, 255, 0)
として認識されて黄色になります。
同様の方法に8ビット色へも変換することができます。
ただし、8ビット色はR,G,Bそれぞれを6色とした216色になります。
多くの端末ではサポートされていないと思いますが、フルカラーの出力を行うこともできます。
ansi := aec.FullColorF(255, 200, 100)
ちなみに関数名の末尾にあるF
とB
はForeground
とBackground
の頭文字で、文字色と背景色を意味します。
実践
aecを使って実際にプログレスバーを作ってみたいと思います。
完成するのは以下のようなものです。
1行のプログレスバーであれば\r
(キャリッジリターン)を使って書くこともできますが、このような複数行にわたるものであれば、エスケープコードやcurses
を使うことになると思います。
ソースコードは以下のようになっています。
package main
import (
"fmt"
"strings"
"time"
"github.com/morikuni/aec"
)
func main() {
const n = 20
builder := aec.EmptyBuilder
up2 := aec.Up(2)
col := aec.Column(n + 2)
bar := aec.Color8BitF(aec.NewRGB8Bit(64, 255, 64))
label := builder.LightRedF().Underline().With(col).Right(1).ANSI
// for up2
fmt.Println()
fmt.Println()
for i := 0; i <= n; i++ {
fmt.Print(up2)
fmt.Println(label.Apply(fmt.Sprint(i, "/", n)))
fmt.Print("[")
fmt.Print(bar.Apply(strings.Repeat("=", i)))
fmt.Println(col.Apply("]"))
time.Sleep(100 * time.Millisecond)
}
}
up2
,col
,bar
,label
がエスケープコードを表しています。
それぞれの意味は
-
up2
: カーソルを上に2つ移動する -
col
: カーソルを左からn+1
番目の位置に移動する -
bar
: 文字色を(64, 255, 64)
の8ビット色にする -
label
: 文字色を明るい赤にして、下線を引いて、col
と同じ動作をして、カーソルを右に1つ移動する
となっています。
その後のfmt.Println()
×2はup2
を最初に実行するときのために空白を出力しています。
for
内では、up2
によってカーソルを初期位置に移動し、label
でラベルを表示、bar
でプログレスバーを表示し、0.1秒停止しています。
注目してもらいたいのは、builder
によって複雑なフォーマットでも簡単に組み立てられるということと、fmt.Print(up2)
のように文字列を修飾する必要がない場合には普通の文字のように扱えるということです。
これによって簡単にエスケープコードを使うことができます。
まとめ
既存のライブラリでは少し物足りない感じがしたので、新たにエスケープコードを扱うライブラリ aec を作成しました。
複雑なエスケープコードを組み立てる方法やRGBによる色指定など、既存のライブラリよりは便利なものになったと思います。
私の環境では動作確認ができないエスケープコードもあるので、何かおかしいところがあったら教えてください。