こんにちは。今日もいいターミナル日和ですね。
今回はGo製の新たなCUIライブラリ goban を作ったので、その紹介と簡単な使い方を説明します。
(追記)罫線と文字幅について
日本語環境では罫線が全角幅で表示されてしまい、綺麗に出力されない問題があります。その際は、以下の一行をMainの前に挿入してください。
runewidth.DefaultCondition = &runewidth.Condition{EastAsianWidth: false}
ただし、これは ambiguous width に分類される文字すべてを半角幅で表示してしまうため、記号などを用いたときに別の問題が発生する場合があります。注意して使用してください。
今後、CJK環境では罫線を諦めてASCIIでボックスを表示する実装を入れる予定です。
CUIライブラリについて
ターミナルで動くグラフィカルなアプリを作るためのライブラリです。著名なものに gocui, tviewなどがあります。
gobanはこれらと比較してよりシンプルに、よりGoらしく書けることを意識しています。
gobanの特徴
- シンプルながら強力なAPI
- ボックス描画
- グリッドレイアウト
- チャネル経由でイベントを処理する。(not イベントハンドラ)
- 分離されたビューとコントローラー
- 容易なビューの組み合わせ
Hello, World!
READMEにも記載していますが、こちらがHello Worldです。
package main
import (
"context"
"github.com/eihigh/goban"
)
func main() {
goban.Main(app, view)
}
func app(_ context.Context, es goban.Events) error {
goban.Show()
es.ReadKey()
return nil
}
func view() {
goban.Screen().Enclose("hello").Prints("Hello World!\nPress any key to exit.")
}
-
goban.Main
で画面を作成し、メイン処理を呼び出します。 -
app
がユーザーが記述するメインの処理です。app
が終了すると、Main
も終了します。 -
goban.Show()
で描画を行います。(後述) -
es.ReadKey()
はキーイベントを待って、読み取る関数です。キー以外のイベントは無視されます。 -
return nil
でappを終了します。その後Mainも終了し、appの返したエラーを返します。
上述したとおり、イベントはチャネル経由で流れてきます。そのため、「ふつうの」プログラムのように流れ通りに処理を書いて、適時イベントを待ったり描画を行ったりする形になります。この辺はまさにGoのパワーが活きています。
描画システム
Hello Worldでは view
という一つの描画関数のみを用いています。
-
goban.Main(app, view)
でview
を描画関数として登録しています。 -
goban.Show()
で描画関数が呼び出されます。
描画関数は複数登録でき、登録した順に呼び出されます。なので、ポップアップなどは以下のように適時ビューを追加することで、上にビューを重ねることができます。
func main() {
goban.Main(app)
}
func app(_ context.Context, es goban.Events) error {
// 下敷きとなるview
v := func() {
b := goban.Screen()
b.Puts("Press any key to open popup")
b.Puts("Ctrl+C to exit")
}
goban.PushViewFunc(v)
for {
goban.Show()
es.ReadKey()
popup(es)
}
}
func popup(es goban.Events) {
// ポップアップのview
v := func() {
b := goban.NewBox(0, 0, 40, 5).Enclose("popup window")
b.Prints("Press any key to close popup")
}
// viewを上に追加する。
// ポップアップのようにモーダルビューを用いるときは、
// このように PushView と defer PopView をペアで使うのが推奨です。
goban.PushViewFunc(v)
defer goban.PopView()
goban.Show()
es.ReadKey()
}
ポイントは defer PopView
で、これを用いることによって関数を脱出した際に自動的にビューを削除することができます。このように、通常状態遷移が必要な処理も、このように非常にシンプルに書くことができます。
グリッドレイアウト
gobanでは Box
を用いてさまざまな描画を行います。その補助として、グリッドレイアウトを使用することができます。
var (
grid = goban.NewGrid(
" 1fr 1fr 1fr ",
"1fr header header header",
"3fr side content ",
"1fr footer footer footer",
)
)
func main() {
goban.Main(app, view)
}
func app(_ context.Context, es goban.Events) error {
goban.Show()
es.ReadKey()
return nil
}
func view() {
b := goban.Screen().Enclose("")
header := b.GridItem(grid, "header").DrawSides("", 0, 0, 0, 1)
header.Prints("Header")
// 以下略
}
NewGrid
関数にレイアウト文字列を渡して、直感的にグリッドレイアウトを作成できます。
- 長さの単位には
fr
またはem
を使用できます。 - 1行目には列のサイズを、2行目以降は行のサイズ+エリア名を記述します。同じエリア名を連続させることで、範囲を連結させることができます。
-
goban.Box#GridItem
関数で、グリッドに相当する範囲のボックスを取得します。
今回は文字列でグリッドを生成していますが、行と列の大きさを直接指定したり、範囲を直接指定して切り出したり(goban.Box#GridCell
)することができます。
ビューモデル
ここまで、viewはすべて関数として解説してきましたが、実際には goban.View
というインターフェースとして定義されています。(関数を渡すと内部でインターフェースを満たす型にキャストされています。)
このことを利用して、viewを状態を持つ型として定義することで、よりviewを強力にすることができます。
func main() {
goban.Main(app)
}
func app(_ context.Context, es goban.Events) error {
v := &menuView{
items: []string{
"foo", "bar", "baz",
},
}
goban.PushView(v)
for {
goban.Show()
switch k := es.ReadKey(); k.Key() {
case tcell.KeyUp:
v.cursor--
case tcell.KeyDown:
v.cursor++
}
}
}
// goban.Viewを実装している。
type menuView struct {
cursor int
items []string
}
func (v *menuView) View() {
b := goban.NewBox(0, 0, 50, 20).Enclose("menu")
b.Puts("↑, ↓: move cursor")
for i, item := range v.items {
if i == v.cursor {
b.Print("> ")
}
b.Puts(item)
}
}
app側で menuView
を作成し、viewとして追加しています。このパターンによって、
- カーソル位置などの状態を持ちやすくなる。
- appとviewをより独立に書くことができる。
- appでview modelのスコープを明確にできる(
defer PopView
/defer RemoveView(v)
と組み合わせてください)。
などのメリットが得られます。
以上、基本的な機能の解説でした!
これから
鋭意開発中ですが、基本機能のAPIはそう変わらない予定です。
今後はより見た目を改善する機能や(中央揃えなど)、エスケープシーケンスへの対応を広げたいです。
(追記) 試行錯誤中ですが、マウスサポートも追加しました。
また、拡張性を生かして、基本APIはそのままに別パッケージとして便利なウィジェットを多数提供する予定です。たとえば、
if confirm(events, msg) { /* 確認画面でOKされたときの処理 */ }
// goban.Eventsを渡せば描画から確認完了までやってくれるのがポイント!
など。(コードの見た目が Immediate GUI っぽくなりますね)
気になった方には是非examplesを覗いていただいて、気に入れば使っていただけると嬉しいです。p-rも歓迎です! goban
、よろしくお願いします。