GoでサクッとリッチなCLIアプリケーションを作ろう!
はじめに
これはサイバーエージェントの26卒内定者がお届けする、CyberAgent 26th Fresh Engineer's Advent Calendar 2024の2日目の記事です🙌
今回はGoのライブラリである、bubbleteaの紹介と、実際に作ったCLIアプリケーションの紹介をします!
bubbleteaとは
Elmアーキテクチャに基づいて作られたGoのライブラリです。
bubbleteaは、Elmアーキテクチャとは
公式ドキュメントと同じ図ですが、Elmアーキテクチャを簡単に表すと以下のようになります。
ここでは、Elmはhtmlを出力し、コンピュータはそれを表示します。そして、コンピュータは画面を操作するユーザーからの入力を受け取り、それをMsg
としてElmに渡します。
Elmはその入力を処理し、新しいhtmlを出力します。このような流れを繰り返すことで、アプリケーションが動作する仕組みになっています。
Elmの内部では、以下の3つの要素が動作しています。
- Model: アプリケーションの状態を管理する
- Update: ユーザーの入力を受け取り、状態(Model)を更新する
- View: Modelを元にhtmlを出力する
MVC(Model-View-Controller)と似ていますが、Elmアーキテクチャは依存関係が一方向であることが特徴です。
bubbleteaの使い方
さて、早速bubbleteaを使ってみましょう!
自分はCLI上でコミットメッセージを簡単に書くためのツールを作りました。
もともとはgit-czを使っていたのですが、もう少し自分好みにカスタマイズしたかったので、bubbleteaを使って作りました。
以下のような機能があるシンプルなCLIアプリケーションです!
- コミットメッセージにつけるprefixを選んで入力を行う
- prefixに付く絵文字は自分で設定することができ、ランダムで表示される
ランダムで表示する絵文字は以下のように設定しています!これにより、コミットメッセージに付く絵文字を毎回変えることができます🙌
{
"feat": ["✨", "🚀", "🎉"],
"fix": ["🐛", "🔧", "🚑️"],
"docs": ["📚", "✏️", "📝"],
"style": ["🎨", "💄", "🎯"],
"refactor": ["♻️", "🛠️", "🔄"],
"perf": ["⚡", "🔥", "💨"],
"test": ["✅", "🧪", "📊"],
"chore": ["🧹", "📦", "🔒"]
}
全体のコードは以下のリポジトリにあるので、よかったら見てみてください!
Model
Elmアーキテクチャに基づいて、各要素を定義します!
まずはModelでアプリケーションの状態を管理します。
type model struct {
choices []string // CLIに表示するアイテム
cursor int // カーソルの位置
selected string // 選択されたアイテム
message textinput.Model // テキスト入力用のモデル
quitting bool // 終了フラグ
currentState state // 状態
}
モデルの初期化
次に、初期化関数で、Modelを初期化します。
テキストの入力を受け付けるために、textinput
を使っています。
func initialModel() model {
ti := textinput.New()
ti.Placeholder = "Enter your commit message"
ti.Focus()
ti.CharLimit = 156
ti.Width = 40
return model{
choices: []string{
"feat: A new feature",
"fix: A bug fix",
"docs: Documentation only changes",
"style: Changes that do not affect the code meaning (white-space, formatting, etc.)",
"refactor: A code change that neither fixes a bug nor adds a feature",
"perf: A code change that improves performance",
"test: Adding missing tests or correcting existing tests",
"chore: Other changes that don't modify src or test files",
},
cursor: 0,
message: ti,
currentState: choosePrefix,
}
}
次に、ModelのメソッドであるInit
関数を定義し、初期I/Oを設定します。
ここでは、テキスト入力のカーソルを点滅させるように設定しています。
func (m model) Init() tea.Cmd {
return textinput.Blink
}
Update
次に、Update関数で、ユーザーからの入力を受け付けます。その入力に応じて、Modelを更新します。
ちょっと長いですが、ここに主なロジックを記載します。
やっていることは以下の通りです!
-
tea.KeyMsg
でユーザーからの入力を受け取る -
curentState
で状態を管理し、それに応じて処理を分岐させる- 初期値ではprefixを選択する状態
- prefixを選択したら、メッセージを入力する状態に遷移する
- メッセージを入力し、enterキーが押されたら、prefixをつけてコミットする
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd
switch msg := msg.(type) {
case tea.KeyMsg:
switch m.currentState {
case choosePrefix:
switch msg.String() {
case "ctrl+c", "q":
m.quitting = true
return m, tea.Quit
case "up", "k":
if m.cursor > 0 {
m.cursor--
}
case "down", "j":
if m.cursor < len(m.choices)-1 {
m.cursor++
}
case "enter":
prefix := strings.SplitN(m.choices[m.cursor], ":", 2)[0]
m.selected = prefix + ": " + randomEmoji(prefix) + " "
m.currentState = enterMessage
}
case enterMessage:
switch msg.String() {
case "ctrl+c":
m.quitting = true
return m, tea.Quit
case "enter":
m.commit(m.selected + m.message.Value())
m.currentState = commitDone
return m, tea.Quit
}
m.message, cmd = m.message.Update(msg)
}
}
return m, cmd
}
View
CLIに表示する内容を、View関数で定義します!
現在の状態に応じて、UIをレンダリングします。
func (m model) View() string {
if m.quitting {
return "Exiting...\n"
}
switch m.currentState {
case choosePrefix:
s := "Choose a commit message prefix:\n\n"
for i, choice := range m.choices {
cursor := " "
line := itemStyle.Render(choice)
if m.cursor == i {
cursor = ">"
line = selectedItemStyle.Render(choice)
}
s += fmt.Sprintf("%s %s\n", cursor, line)
}
return s
case enterMessage:
return fmt.Sprintf("Enter your commit message (starting with %s):\n\n%s%s", m.selected, m.selected, m.message.View())
case commitDone:
return "Commit complete!\n"
}
return ""
}
main関数
最後に、main関数でbubbleteaを実行します!
絵文字の設定ファイルを読み込んで、Modelを初期化し、bubbleteaのプログラムを実行します。
func main() {
emojiFile := os.Getenv("EMOJI_FILE")
if emojiFile == "" {
log.Fatalf("EMOJI_FILE is not set")
}
loadEmojis(emojiFile)
m := initialModel()
p := tea.NewProgram(m)
if err := p.Start(); err != nil {
fmt.Fprintf(os.Stderr, "Alas, there's been an error: %v", err)
os.Exit(1)
}
}
まとめ
bubbleteaを使えば簡単にリッチなCLIアプリケーションを作ることができます!
今回はその中でもシンプルなセレクトを使いましたが、他にも様々なおしゃれな機能があります。
例も公開されているので、ぜひチェックしてみてください!!
Discussion