gocui はTUIを作成しやすくするために作られた termbox-go をベースにしたライブラリです。
gocui でTUIを作成するために必要な概念
Gui
Gui
は画面レイアウトやキーバインディングを含むTUI全体を表す。
Gui
で表示される画面全体は View
によって構成されるバッファーとして扱う。
View
View
はコンソール画面に表示させたいTUIのウィジェットを書き込むための構造体。
io.ReadWriter
を実装しており、画面の状態(何の文字が表示されているか)を取得したいときは Read
を、画面に書き込みたいときは Write
を使う。
Manager
Manager
は画面のレイアウトを担い、コンソール上に表示される各ウィジェットを構築するために使用される。
Go的には Layout(*Gui) error
を実装した interface
Gui
のメインループのたびに、設定された各マネージャの Layout
が実行される。
よって、 Layout
中で View
を操作する機能を実装すれば、各ウィジェットを Manager
毎に分割して管理できる。
※ 画面構築のために、必ずしも Manager
を用意する必要はない
基本的な考え方
-
Gui
オブジェクトを作成する -
View
を設定する - 設定した
View
にコンテンツを書き込む - メインループを回す
- Hello world を表示する最小限のサンプル
package main
import (
"fmt"
"log"
"github.com/jroimartin/gocui"
)
func main() {
// 1. Guiオブジェクトを作詞する
g, err := gocui.NewGui(gocui.OutputNormal)
if err != nil {
log.Panicln(err)
}
defer g.Close()
// 2. View を設定する
if v, err := g.SetView("hello", 0,0, 13, 2); err != nil {
if err != gocui.ErrUnknownView {
log.Panicln(err)
}
// 3. 設定した View にコンテンツを書き込む
fmt.Fprintln(v, "Hello world!")
}
if err := g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit); err != nil {
log.Panicln(err)
}
// 4. メインループを回す
if err := g.MainLoop(); err != nil && err != gocui.ErrQuit {
log.Panicln(err)
}
}
func quit(g *gocui.Gui, v *gocui.View) error {
return gocui.ErrQuit
}
Manager を使用する場合の考え方
-
Gui
オブジェクトを作成する - ウィジェットを表すための構造体を定義する
-
Layout
関数を定義する (Manager interface を実装する) -
View
を設定する - 設定した
View
にコンテンツを書き込む
-
-
Manager
をGui
に登録する - メインループを回す
-
Manager
を使うと、各View
を構造体毎に管理できるようになって、見通しが良くなる。
package main
import (
"fmt"
"log"
"github.com/jroimartin/gocui"
)
// 2. ウィジェットを表すための構造体を定義する
type Widget struct {
name string
body string
x0, y0, x1, y1 int
}
// 2.1 Layout関数を定義する (Manager interface を実装する)
func (w *Widget) Layout(g *gocui.Gui) error {
// 2.2 View を設定する
v, err := g.SetView(w.name, w.x0, w.y0, w.x1, w.y1)
if err != nil {
if err != gocui.ErrUnknownView {
return err
}
// 2.3 設定した View にコンテンツを書き込む
fmt.Fprint(v, w.body)
}
return nil
}
func main() {
// 1. Guiオブジェクトを作成する
g, err := gocui.NewGui(gocui.OutputNormal)
if err != nil {
log.Panicln(err)
}
defer g.Close()
// 3. Manager を Gui に登録する
widget1 := &Widget{"widget1", "Widget 1", 0, 0, 9, 2}
widget2 := &Widget{"widget2", "Widget 2", 0, 3, 9, 5}
g.SetManager(widget1, widget2)
if err := g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit); err != nil {
log.Panicln(err)
}
// 4. メインループを回す
if err := g.MainLoop(); err != nil && err != gocui.ErrQuit {
log.Panicln(err)
}
}
func quit(g *gocui.Gui, v *gocui.View) error {
return gocui.ErrQuit
}
Manager は関数でも設定可能
-
Gui
オブジェクトを作成する - ウィジェットを表すための構造体を定義する
-
Layout
関数を定義する (Manager interface を実装する) -
View
を設定する - 設定した
View
にコンテンツを書き込む
-
-
Manager
関数を定義する -
Manager
を Gui に登録する- 関数をManager化する
- メインループを回す
- Manager関数でウィジェットを自動的に重ならないように配置するサンプル
package main
import (
"fmt"
"log"
"github.com/jroimartin/gocui"
)
// 2. ウィジェットを表すための構造体を定義する
type Widget struct {
name string
body string
x0, y0, x1, y1 int
}
// 2.1 Layout関数を定義する (Manager interface を実装する)
func (w *Widget) Layout(g *gocui.Gui) error {
// 2.2 View を設定する
v, err := g.SetView(w.name, w.x0, w.y0, w.x1, w.y1)
if err != nil {
if err != gocui.ErrUnknownView {
return err
}
// 2.3 設定した View にコンテンツを書き込む
fmt.Fprint(v, w.body)
}
return nil
}
// 3. Manager関数を定義する
func flowLayout(g *gocui.Gui) error {
views := g.Views()
x := 0
for _, v := range views {
w, h := v.Size()
_, err := g.SetView(v.Name(), x, 0, x+w+1, h+1)
if err != nil && err != gocui.ErrUnknownView {
return err
}
x += w + 2
}
return nil
}
func main() {
// 1. Guiオブジェクトを作成する
g, err := gocui.NewGui(gocui.OutputNormal)
if err != nil {
log.Panicln(err)
}
defer g.Close()
// 4. Manager を Gui に登録する
widget1 := &Widget{"widget1", "Widget 1", 0, 0, 9, 2} // 表示する座標が同じ
widget2 := &Widget{"widget2", "Widget 2", 0, 0, 9, 2} // 表示する座標を同じ
// 4.1 関数を Manager化する
managerFunc := gocui.ManagerFunc(flowLayout)
g.SetManager(widget1, widget2, managerFunc)
if err := g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit); err != nil {
log.Panicln(err)
}
// 5. メインループを回す
if err := g.MainLoop(); err != nil && err != gocui.ErrQuit {
log.Panicln(err)
}
}
func quit(g *gocui.Gui, v *gocui.View) error {
return gocui.ErrQuit
}
その他
-
Manager
を登録する場合はSetManager
で1回で登録する- 複数回
SetManager
を呼び出してマネージャーを登録すると最後に呼び出したSetmangaer
の結果のみが登録される -
SetManagerFunc
も内部でSetManager
を使うので同様
- 複数回
- プリミティブな
termbox-go
に色々な便利機能を追加したので、テキストベースでマルチウィンドウ/レイアウトなTUIを作るならgocuiで間違いなさそう- termui のようなグラフィカルなウィジェット作成には一工夫必要かもしれない
- TUIといえどステートフルなアプリになるので、FluxのようなGUI構築パターンの設計や考慮が必要になりそう