Bubble Tea
GitHub概要
charmbracelet/bubbletea
A powerful little TUI framework 🏗
スター33,527
ウォッチ123
フォーク945
作成日:2020年1月10日
言語:Go
ライセンス:MIT License
トピックス
clielm-architectureframeworkfunctionalgogolanghacktoberfesttui
スター履歴
データ取得日時: 2025/7/25 11:09
Bubble Tea
Bubble Teaは、Elmアーキテクチャに基づくモダンなTUIフレームワークです。関数型プログラミングの概念を活用し、状態管理とUI更新を宣言的に表現できます。Charmbraceletエコシステムの中核を担い、Go言語におけるTUI開発のデファクトスタンダードとなっています。
特徴
Elmアーキテクチャ
- Model: アプリケーションの状態を表現
- View: Modelを基にUIをレンダリング
- Update: メッセージに応じてModelを更新
- メッセージ駆動: イベントと状態変更の明確な分離
パフォーマンスと機能
- フレームレートベース: スムーズなアニメーションと描画
- マウスサポート: クリック、ホイール、モーションイベント
- フォーカス管理: フォーカスイベントの追跡
- 非同期I/O: Goの並行性を活用
- プロダクションレディ: 多数の実用アプリで採用
開発者体験
- シンプルなAPI: 直感的で学習しやすい
- テスト容易性: 純粋関数と不変性によるテストのしやすさ
- デバッグ機能: 組み込みのデバッグモード
- 拡張性: カスタムレンダラーやミドルウェア
Charmbraceletエコシステム
- Lip Gloss: スタイリングライブラリ
- Bubbles: 再利用可能なコンポーネント集
- Glamour: Markdownレンダリング
- Charm: バックエンドサービス
基本的な使用方法
インストール
go get github.com/charmbracelet/bubbletea
Hello World
package main
import (
"fmt"
"os"
tea "github.com/charmbracelet/bubbletea"
)
// Modelの定義
type model struct {
cursor int
choices []string
selected map[int]struct{}
}
// 初期化関数
func initialModel() model {
return model{
choices: []string{"Buy carrots", "Buy celery", "Buy kohlrabi"},
selected: make(map[int]struct{}),
}
}
// Initコマンド
func (m model) Init() tea.Cmd {
return nil
}
// Update関数
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c", "q":
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", " ":
_, ok := m.selected[m.cursor]
if ok {
delete(m.selected, m.cursor)
} else {
m.selected[m.cursor] = struct{}{}
}
}
}
return m, nil
}
// View関数
func (m model) View() string {
s := "What should we buy at the market?\n\n"
for i, choice := range m.choices {
cursor := " "
if m.cursor == i {
cursor = ">"
}
checked := " "
if _, ok := m.selected[i]; ok {
checked = "x"
}
s += fmt.Sprintf("%s [%s] %s\n", cursor, checked, choice)
}
s += "\nPress q to quit.\n"
return s
}
func main() {
p := tea.NewProgram(initialModel())
if _, err := p.Run(); err != nil {
fmt.Printf("Alas, there's been an error: %v\n", err)
os.Exit(1)
}
}
コマンドとメッセージ
package main
import (
"time"
tea "github.com/charmbracelet/bubbletea"
)
// カスタムメッセージ
type tickMsg time.Time
// コマンドの定義
func tickCmd() tea.Cmd {
return tea.Tick(time.Second, func(t time.Time) tea.Msg {
return tickMsg(t)
})
}
type model struct {
lastTick time.Time
}
func (m model) Init() tea.Cmd {
return tickCmd()
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
if msg.String() == "q" || msg.String() == "ctrl+c" {
return m, tea.Quit
}
case tickMsg:
m.lastTick = time.Time(msg)
return m, tickCmd()
}
return m, nil
}
func (m model) View() string {
return "The time is " + m.lastTick.Format("15:04:05")
}
Lip Glossを使ったスタイリング
package main
import (
"github.com/charmbracelet/bubbles/spinner"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
var (
// スタイル定義
titleStyle = lipgloss.NewStyle().
Bold(true).
Foreground(lipgloss.Color("#7D56F4")).
PaddingTop(1).
PaddingBottom(1)
itemStyle = lipgloss.NewStyle().
PaddingLeft(4)
selectedItemStyle = lipgloss.NewStyle().
PaddingLeft(2).
Foreground(lipgloss.Color("170"))
helpStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("241")).
PaddingTop(1)
)
type model struct {
spinner spinner.Model
loading bool
items []string
cursor int
}
func initialModel() model {
s := spinner.New()
s.Spinner = spinner.Dot
s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205"))
return model{
spinner: s,
loading: true,
items: []string{},
}
}
func (m model) View() string {
if m.loading {
return "\n" + m.spinner.View() + " Loading..."
}
s := titleStyle.Render("🧋 Bubble Tea Example")
s += "\n\n"
for i, item := range m.items {
if m.cursor == i {
s += selectedItemStyle.Render("▶ " + item) + "\n"
} else {
s += itemStyle.Render(item) + "\n"
}
}
s += helpStyle.Render("\n↑/↓: navigate • q: quit")
return s
}
高度な機能
非同期I/O
package main
import (
"fmt"
"net/http"
"time"
tea "github.com/charmbracelet/bubbletea"
)
type responseMsg struct {
status int
err error
}
func checkServer(url string) tea.Cmd {
return func() tea.Msg {
client := &http.Client{Timeout: 5 * time.Second}
resp, err := client.Get(url)
if err != nil {
return responseMsg{err: err}
}
defer resp.Body.Close()
return responseMsg{status: resp.StatusCode}
}
}
type model struct {
url string
checking bool
status int
err error
}
func (m model) Init() tea.Cmd {
return checkServer(m.url)
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case responseMsg:
m.checking = false
m.status = msg.status
m.err = msg.err
return m, nil
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c", "q":
return m, tea.Quit
case "r":
m.checking = true
return m, checkServer(m.url)
}
}
return m, nil
}
カスタムレンダラー
package main
import (
tea "github.com/charmbracelet/bubbletea"
)
// カスタムレンダラー
type customRenderer struct {
tea.StandardRenderer
frames int
}
func (r *customRenderer) Render(v string) {
r.frames++
// FPSやパフォーマンス情報を追加
v += fmt.Sprintf("\n\nFrames: %d", r.frames)
r.StandardRenderer.Render(v)
}
func main() {
renderer := &customRenderer{
StandardRenderer: tea.StandardRenderer{},
}
p := tea.NewProgram(
initialModel(),
tea.WithRenderer(renderer),
tea.WithAltScreen(),
tea.WithMouseCellMotion(),
)
if _, err := p.Run(); err != nil {
log.Fatal(err)
}
}
Bubblesコンポーネントの活用
package main
import (
"github.com/charmbracelet/bubbles/textinput"
"github.com/charmbracelet/bubbles/viewport"
"github.com/charmbracelet/bubbles/list"
tea "github.com/charmbracelet/bubbletea"
)
type model struct {
textInput textinput.Model
viewport viewport.Model
list list.Model
ready bool
}
func initialModel() model {
// テキスト入力
ti := textinput.New()
ti.Placeholder = "Type something..."
ti.Focus()
ti.CharLimit = 156
ti.Width = 20
// リスト
items := []list.Item{
item{"Item 1", "Description 1"},
item{"Item 2", "Description 2"},
item{"Item 3", "Description 3"},
}
l := list.New(items, itemDelegate{}, 20, 14)
l.Title = "My List"
l.SetShowStatusBar(false)
l.SetFilteringEnabled(false)
return model{
textInput: ti,
list: l,
}
}
エコシステム
公式ツール
- Glow: Markdownビューア
- Soft Serve: Gitサーバー
- VHS: ターミナルレコーダー
- Charm: クラウドサービス
コミュニティプロジェクト
- lazygit: Git TUI
- duf: ディスク使用状況ツール
- gdu: ディスク使用状況アナライザー
- ticker: 株価トラッカー
利点
- モダンな設計: Elmアーキテクチャによる優れた設計
- 活発な開発: Charmbraceletによる継続的な改善
- 豊富なエコシステム: コンポーネントやツールが充実
- テスト容易性: 純粋関数と不変性
- パフォーマンス: 効率的なレンダリングと更新
制約事項
- 学習曲線: Elmアーキテクチャの理解が必要
- フレームワーク固有: 他のアプローチとの統合が難しい場合がある
- コミュニティ依存: Bubblesコンポーネントに依存
他のライブラリとの比較
項目 | Bubble Tea | tview | termui |
---|---|---|---|
アーキテクチャ | Elm | ウィジェットベース | ダッシュボード |
学習コスト | 中 | 低 | 低〜中 |
エコシステム | 非常に大きい | 大きい | 中程度 |
柔軟性 | 非常に高い | 高い | 中 |
パフォーマンス | 非常に高い | 高い | 高い |
まとめ
Bubble Teaは、Go言語におけるモダンなTUI開発のデファクトスタンダードです。Elmアーキテクチャの優れた設計、活発な開発コミュニティ、豊富なエコシステムにより、小規模なツールから大規模なアプリケーションまで幅広い用途に適しています。特に、保守性やテスト容易性を重視するプロジェクトにおいて最適な選択です。