😛

GoでサクッとリッチなCLIアプリケーションを作ろう!

2024/12/01に公開

はじめに

これはサイバーエージェントの26卒内定者がお届けする、CyberAgent 26th Fresh Engineer's Advent Calendar 2024の2日目の記事です🙌
今回はGoのライブラリである、bubbleteaの紹介と、実際に作ったCLIアプリケーションの紹介をします!

bubbleteaとは

https://github.com/charmbracelet/bubbletea
bubbleteaは、Elmアーキテクチャに基づいて作られたGoのライブラリです。

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": ["🧹", "📦", "🔒"]
}

全体のコードは以下のリポジトリにあるので、よかったら見てみてください!

https://github.com/mshr0969/simple-git-cz

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アプリケーションを作ることができます!
今回はその中でもシンプルなセレクトを使いましたが、他にも様々なおしゃれな機能があります。
例も公開されているので、ぜひチェックしてみてください!!
https://github.com/charmbracelet/bubbletea/tree/master/examples

GitHubで編集を提案

Discussion