Bubble Tea

TUITerminalElm-ArchitectureFunctionalGo

GitHub概要

charmbracelet/bubbletea

A powerful little TUI framework 🏗

スター33,527
ウォッチ123
フォーク945
作成日:2020年1月10日
言語:Go
ライセンス:MIT License

トピックス

clielm-architectureframeworkfunctionalgogolanghacktoberfesttui

スター履歴

charmbracelet/bubbletea Star History
データ取得日時: 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 Teatviewtermui
アーキテクチャElmウィジェットベースダッシュボード
学習コスト低〜中
エコシステム非常に大きい大きい中程度
柔軟性非常に高い高い
パフォーマンス非常に高い高い高い

まとめ

Bubble Teaは、Go言語におけるモダンなTUI開発のデファクトスタンダードです。Elmアーキテクチャの優れた設計、活発な開発コミュニティ、豊富なエコシステムにより、小規模なツールから大規模なアプリケーションまで幅広い用途に適しています。特に、保守性やテスト容易性を重視するプロジェクトにおいて最適な選択です。