gocui の基本的な使い方

[gocui](https://github.com/jroimartin/gocui) はTUIを作成しやすくするために作られた [termbox-go](https://github.com/nsf/termbox-go) をベースにしたライブラリです。

## gocui でTUIを作成するために必要な概念

### Gui
`Gui` は画面レイアウトやキーバインディングを含むTUI全体を表す。
`Gui` で表示される画面全体は `View` によって構成されるバッファーとして扱う。

### View
`View` はコンソール画面に表示させたいTUIのウィジェットを書き込むための構造体。
`io.ReadWriter` を実装しており、画面の状態(何の文字が表示されているか)を取得したいときは `Read` を、画面に書き込みたいときは `Write` を使う。

### Manager
`Manager` は画面のレイアウトを担い、コンソール上に表示される各ウィジェットを構築するために使用される。
Go的には `Layout(*Gui) error` を実装した [interface](https://godoc.org/github.com/jroimartin/gocui#Manager)
`Gui` のメインループのたびに、設定された各マネージャの `Layout` が実行される。
よって、 `Layout` 中で `View` を操作する機能を実装すれば、各ウィジェットを `Manager` 毎に分割して管理できる。
※ 画面構築のために、必ずしも `Manager` を用意する必要はない

## 基本的な考え方
1. `Gui`オブジェクトを作成する
2. `View` を設定する
3. 設定した `View` にコンテンツを書き込む
4. メインループを回す

-  Hello world を表示する最小限のサンプル

```go
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. `Manager` を `Gui` に登録する
4. メインループを回す

- `Manager` を使うと、各`View` を構造体毎に管理できるようになって、見通しが良くなる。

```go
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関数でウィジェットを自動的に重ならないように配置するサンプル

```go
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](https://github.com/gizak/termui) のようなグラフィカルなウィジェット作成には一工夫必要かもしれない
- TUIといえどステートフルなアプリになるので、FluxのようなGUI構築パターンの設計や考慮が必要になりそう