今川館

都内勤務の地味OLです

ANSIエスケープコードを使ってコンソールアニメーション

プログラミング言語Go』のスピナー

予約語「go」を使うとメインgoroutineと別のgoroutineで動かせる

プログラミング言語Go』の「ゴルーチンとチャネル」という章を読んでいて、メインルーチンの処理中にスピナーを回すサンプルコードが出てきたので自分でも試していた。

この、スピナーのコードが以下の通り、1文字出力してはキャリッジリターン「\r」でカーソル位置を戻して上書きを繰り返すものなのだが、これに予約語「go」を付けると別ルーチンで動いてくれる。

func spinner(delay time.Duration) {
	for {
		for _, r := range `-\|/` {
			fmt.Printf("\r%c", r)
			time.Sleep(delay)
		}
	}
}

func main() {
	go spinner(100 * time.Millisecond)
	// 中略: 変数accに処理時間を累積していく
	fmt.Printf("%v かかりました\n", acc)
}

ところが、スピナーが出力する文字列を以下のように変えてしまったら問題が起きた。

func spinner(message string, delay time.Duration) {
	for {
		for _, r := range `-\|/` {
			fmt.Printf("\r%c%v", r, message)
			time.Sleep(delay)
		}
	}
}

func main() {
	go spinner("コンピュータ思考中...", 140*time.Millisecond)
	// 中略: 変数accに処理時間を累積していく
	fmt.Printf("\r%v かかりました\n", acc)
}

スピナーで使った文字列が残ってしまう

これだと、処理が終わるまでは「-コンピュータ思考中...」というメッセージの先頭文字が切り替わり続けているが、終了時に

2.5s かかりました中...

このように、何ともマヌケな表示になってしまう。

これを、

2.5s かかりました

このように改めるにはどうした良いのかわからなかった。
キャリッジリターンを出力するとある程度文字を書き換えられるのだから、同じような特殊文字を出力すれば良いだろうとは思っていたのだが。

ANSIエスケープコード

調べてみると、stackoverflowの記事がヒットし、「ANSIエスケープコード」というものを出力すれば良いらしい。

How can I clear the terminal screen in Go? - Stack Overflow

You could do it with ANSI escape codes:

print("\033[H\033[2J")

そういえば.vimrcや.bashrcを書き換えて、プロンプトやlsで表示される文字色を変えたときに「\033[チョメチョメ」という表記を見かけたなぁと思った。

カーソル移動と文字消去

ANSIエスケープコード - コンソール制御 - 碧色工房

このページに非常に詳しく書いてあるのだが、カーソル移動や行や文字を消すコードがちゃんと設けられている。

ESC[nG
カーソルを現在の横位置に関係なく左端からnの場所に移動させる。 (左端を1とする。nには整数が入る、省略すると1)

ESC[nK
行消去、nを省略、もしくは0を指定した場合、カーソルより後ろを消去、 1を指定するとカーソルより前を消去、2を指定すると行全体を消去となる。

最初のESCは表示文字でないので便宜上このように表示しているが、(中略)
どちらも同じものを表現しているが、8進数を利用した\033で書かれる場合が多いようだ。

上記から、行を消し、その後カーソルを行頭に移動するには以下の出力をしてあげれば良い。

fmt.Printf("\033[2K\033[G")

文字に色をつける

更に、出力する文字の色を変えることも可能らしい。
そういえばAnsibleやgrepコマンドの出力結果には適宜色がついていたなぁと思い出す。

そこで、以下の通りプログラムを書き換えて、終了メッセージの先頭に「[ok] 」というプレフィクスを緑色で表示してみることにした。

fmt.Printf("\033[32m[ok]\033[39m %v かかりました\n", acc)

すると、このように、ちゃんと色がついた。

f:id:imagawa_yakata:20161211171023p:plain