Help us understand the problem. What is going on with this article?

GoでANSIエスケープコードを扱うライブラリを作った(色付け・カーソル移動等)

More than 3 years have passed since last update.

はじめに

ANSIエスケープコード(or エスケープシーケンス)とよばれる文字列を使うことで、端末上で文字に色を付けたり、文字のフォーマットを変更したり、カーソルを操作することができます。
詳細はWikipedia(英語)をご覧ください。

いろんな言語でエスケープコードを扱うライブラリが開発されており、Goでも同様のライブラリは存在します。おそらく有名なのはmgutz/ansifatih/colorでしょう。

単に色を付けるだけならこれらでもいいとは思いますが、以下の点から新たにライブラリを作りました。

  1. 色やスタイルの指定が文字列ベースでコンパイル時にチェックできない(mgutz/ansi)
  2. 3ビット(8)色は使えるが、8ビット(256)色が使いにくい。
  3. 色とスタイル以外のエスケープコードが使えない

3つ目に関しては上記のライブラリの範囲外なので仕方ないとしても、2つ目の8ビット色が使いにくいのは残念です。ちなみに、使いにくいというのは8ビット色を0から255の値で指定するということです。(例えば144といわれてもどんな色か想像できませんよね?)

ライブラリの紹介

aec(Ansi Espace Code)というライブラリを作りました。
ソースコードはGitHubにあります。

aecでは文字の色付けやスタイル変更を含めた多くのエスケープコードをサポートしています。
aecの特徴を簡単に紹介します。

  • カーソルの移動や表示・非表示の切替え
  • 画面・行に表示されている文字の消去
  • スクロール
  • 文字の色付け・スタイル変更
  • RGB(24ビット)から3ビット色や8ビット色への変換
  • エスケープコードを簡単に作成するための構文

エスケープコードが動くかどうかは端末環境に依存するので、動かないものも結構あると思いますが、Wikipediaに書いてあって、使いそうなものはほとんどサポートしています。

サポートしている具体的なエスケープコードはGitHubGoDocに書いてあるので、ここではライブラリの設計や使い方を紹介します。

ライブラリの設計

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"))

pic1.png

カーソル移動のエスケープコードなどでは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)

ちなみに関数名の末尾にあるFBForegroundBackgroundの頭文字で、文字色と背景色を意味します。

実践

aecを使って実際にプログレスバーを作ってみたいと思います。
完成するのは以下のようなものです。

sample.gif

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による色指定など、既存のライブラリよりは便利なものになったと思います。

私の環境では動作確認ができないエスケープコードもあるので、何かおかしいところがあったら教えてください。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away