Go

gocui の基本的な使い方

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 を用意する必要はない

基本的な考え方

  1. Guiオブジェクトを作成する
  2. View を設定する
  3. 設定した View にコンテンツを書き込む
  4. メインループを回す
  • 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 を使用する場合の考え方

  1. Guiオブジェクトを作成する
  2. ウィジェットを表すための構造体を定義する
    1. Layout関数を定義する (Manager interface を実装する)
    2. View を設定する
    3. 設定した View にコンテンツを書き込む
  3. ManagerGui に登録する
  4. メインループを回す
  • 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 は関数でも設定可能

  1. Guiオブジェクトを作成する
  2. ウィジェットを表すための構造体を定義する
    1. Layout関数を定義する (Manager interface を実装する)
    2. View を設定する
    3. 設定した View にコンテンツを書き込む
  3. Manager 関数を定義する
  4. Manager を Gui に登録する
    1. 関数をManager化する
  5. メインループを回す
  • 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構築パターンの設計や考慮が必要になりそう