Goで簡単にTUI!Bubble Teaのススメ(アーキテクチャ編)
Bubble Teaとは
Bubble Teaは、Text User Interface(TUI)を作成するためのGo言語のライブラリ(フレームワーク)です。
以下のようなCLIアプリケーションを、比較的簡単に作成できます。
Bubble Teaはフレームワーク的な部分があるため、アーキテクチャを理解することが重要です。
(理解しないと使えません。)
この記事では、Bubble Teaのアーキテクチャについて紹介します。
Elmアーキテクチャ
Bubble Teaの中心となる考え方は、Elmアーキテクチャに基づいています。
Elmアーキテクチャは、Elmという関数型のAltJS言語で考案された、Webフロントエンドのアーキテクチャです。
Elmアーキテクチャは、以下の3つの要素から構成されます。
- Model: アプリケーションの状態を表すデータ
- View: Modelの状態を表示するための関数(ElmではHTMLを生成)
- Update: Modelを更新するための関数
ユーザが何か入力したり、イベントが発生すると、ランタイムはUpdateを呼び出します。
このとき渡されるmsgは、入力やイベントを表すデータ構造です。
Updateはmsgの種類・内容に応じてModelを更新します。
(Updateはイベントハンドラのようなものですね。)
Update, Viewが純粋関数であることにより、テストが容易になったりというメリットがありそうです。
この辺は関数型っぽいですね。
Bubble Teaのアーキテクチャ
Bubble TeaはElmアーキテクチャをCLIのためにGo言語で実装したものと考えてもらってOKです。
Bubble Teaアプリとして実行するためには、以下のintefaceが実装されている必要があります。
type Model interface {
Init() Cmd
Update(Msg) (Model, Cmd)
View() string
}
Viewを見ると、ElmのViewと同じようにModelから文字列(CUI)を生成する関数であることがわかります。
一方、UpdateはElmのものと少し異なり、Msgを受け取ってModelとCmdを返しています。
Modelを返しているのは、Updateが値呼びの関数であるためです。
変更後のModelを返すことでModelの変更を実現しています。
CmdとMsg
Cmd、Msgの定義は以下の通りです。
type Msg interface{}
type Cmd func() Msg
Msgはinterface{}なので、何でもアリです。
CmdはMsgを返す関数なので、これも何でもアリの関数になります。
型にはほぼ情報が無いですね。
実際には、Cmdは主にI/O処理を実行するために使われます。
Updateから返されたCmdは、Bubble Teaランタイムによってgroutineとして非同期に実行されます。
Msgは、Updateの引数になっていることからもわかるように、ユーザの入力やイベントを表すデータです。
具体的な例を見てみましょう。
Msgの種類
Bubble Teaでは、以下のようなMsgが定義されています。
- tea.KeyMsg: キー入力
- tea.WindowSizeMsg: ウィンドウサイズ変更
- tea.MouseMsg: マウスイベント
これらのMsgは、ランタイムからUpdateに渡されます。
一方ユーザが定義するMsgには、以下のようなものがあります。
- I/O処理(Cmd)の結果を表すMsg
- エラーの発生を表すMsg
- 時間経過によるTickを表すMsg
Cmdの種類
Cmdは、基本的にはユーザが定義する関数です。
以下のようなCmdが考えられます。
- HTTPリクエストを送信、結果のMsgを返すCmd
- ファイルを読み込み、grepした結果のMsgを返すCmd
- 現在時刻を取得し、Msgにして返すCmd
また、Bubble Teaが提供するCmdもあります。
- tea.Batch: 複数のCmdを実行するCmd
- tea.Exec: 別のプログラムを実行するCmd
- tea.Quit: プログラムを終了するCmd
Updateの構造
CmdとMsgの説明を踏まえると、Updateの構造は図のようになります。
Updateに入力されるMsgは、ユーザの入力やイベントを表すMsgと、I/O処理の結果を表すMsgの2種類があります。
このCmd、Msgのループを考えると、Initの意味もわかると思います。
Initはアプリケーション開始時にどんなCmdを実行するかを指定します。
例えば、ページングがあるAPIを実行したい場合、ページ0のデータを取得するCmdをInitに指定します。
API実行結果のMsgを受け取ったら、次のページを取得するCmdを返すようにUpdateに定義します。
これで、毎回完了を待ちつつページングを順番に実行できます。
サンプルアプリ
Bubble Teaのサンプルアプリを作ってみました。
package main
import (
"fmt"
tea "github.com/charmbracelet/bubbletea"
)
type model struct {
counter int
typedKey string
}
var _ tea.Model = model{}
func (m model) Init() tea.Cmd {
return nil
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c":
return m, tea.Quit
default:
m.typedKey = msg.String()
m.counter++
}
}
return m, nil
}
func (m model) View() string {
return fmt.Sprintf("You typed: %s\nCounter is: %d\nCtrl+C to exit", m.typedKey, m.counter)
}
func main() {
m := model{}
p := tea.NewProgram(m)
p.Run()
}
実行すると、入力したキーと、入力の回数が表示されます。
詳細な説明は宿題にしてみますが、Bubble Teaのコードリーディングには以下のようなコツがあります。
- まずModelが保持している状態と、Viewの処理内容を見る
- Updateを見て、処理するMsgの種類を把握する
- Msgの型ごとに処理を見ていく
まとめ
Bubble Teaでは、こんな感じで状態を管理しつつCmdを裏で非同期実行することで、カクツキのないCUIアプリケーションを作ることができます。
一方、トップに載せたGifようなカラフルでアニメーションがあるアプリを作るのは、この記事で説明した機能だけでは難しいです。
Bubble Teaの開発チームであるCharmが作っている、Lipglossというライブラリを使うことで、文字や背景に色をつけたり枠線を書いたりと、美麗なテキストをViewで生成できます。
また、Bubblesというコンポーネント集も提供されており、組み合わせることでリッチなUIを簡単に実現で行きます。
この辺の説明は、次回以降の記事でやっていきたいと思います。
今後以下のような記事を書いていく予定です。
- デモアプリを使ったコード中心の説明
- Lipglossの紹介
- Bubblesの紹介
- モデル定義のコツ
先行記事
日本語でのBubble Teaの紹介記事があまりなかったのでこの記事を書いてみました。
少ないですが、以下のような記事があります。
名古屋のAI企業「来栖川電算」の公式publicationです。AI・ML を用いた認識技術・制御技術の研究開発を主軸に事業を展開しています。 公式HP→ kurusugawa.jp/ , 採用情報→ kurusugawa.jp/jobs/
Discussion