最近、Goのx/termパッケージを使ってターミナルベースのタイピングゲームを作ってみた。この記事では、x/termパッケージの特徴や、TUIアプリケーション開発で気づいたことを共有していく。
x/termを使った実践的なTUIアプリケーションとして、ggcというgitのクライアントツールを開発しているので、よければStarを押してほしい。
x/term
は、Goの実験的なパッケージの一つで、ターミナル操作のための低レベルな機能を提供している。以前はgolang.org/x/crypto/ssh/terminal
だったが、現在はgolang.org/x/term
として独立したパッケージとなっている。
width, height, err := term.GetSize(int(os.Stdout.Fd()))
oldState, err := term.MakeRaw(int(os.Stdin.Fd()))
if err != nil {
log.Fatal(err)
}
defer term.Restore(int(os.Stdin.Fd()), oldState)
これらの機能を使えば、カーソル位置の制御やキー入力の即時検出など、インタラクティブなTUIアプリケーションが作れる。
タイピングゲームの核となる機能はこんな感じである:
x/termを使う上で最も気をつけたいのはターミナルの状態管理である:
// ターミナルを生モードに設定
oldState, err := term.MakeRaw(int(os.Stdin.Fd()))
if err != nil {
log.Fatal(err)
}
// プログラム終了時に元の状態に戻す
defer term.Restore(int(os.Stdin.Fd()), oldState)
// シグナルハンドリング
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)
go func() {
<-sigCh
term.Restore(int(os.Stdin.Fd()), oldState)
os.Exit(0)
}()
ANSIエスケープシーケンスを使って、画面表示を制御している:
// カーソルを非表示
fmt.Print("\033[?25l")
defer fmt.Print("\033[?25h") // 終了時に表示を戻す
// 画面クリア
fmt.Print("\033[2J")
// カーソル位置を移動(x=10, y=5の位置へ)
fmt.Printf("\033[%d;%dH", 5, 10)
表示の安定性を確保するためのポイントをいくつか紹介する:
// パニック時のリカバリ処理
defer func() {
if r := recover(); r != nil {
term.Restore(int(os.Stdin.Fd()), oldState)
fmt.Printf("Recovered from panic: %v\n", r)
}
}()
var clear string
if runtime.GOOS == "windows" {
clear = "cls"
} else {
clear = "clear"
}
cmd := exec.Command(clear)
cmd.Stdout = os.Stdout
cmd.Run()
x/termを使ったTUIアプリケーション開発は、低レベルな制御が必要だが、その分自由度が高い。今回作ったタイピングゲームの実装例は基本的なパターンを示すものだが、これを応用すれば様々なTUIアプリケーションが作れる。
実は、この技術を活用した優れたツールとしてggcというものがある。インクリメンタルサーチUIやキー入力のハンドリング、画面表示の制御など、x/termの機能を巧みに使って、直感的で使いやすいGitクライアントを実現している。興味のある方はgo install github.com/bmf-san/ggc@latest
でインストールして試してみてほしい。