mcpField
G
gentleman-bubbletea

gentleman-bubbletea

github.com/gentleman-programming開発7.7k88 回インストール今日更新
リポジトリ

gentleman-bubbletea について

gentleman-bubbletea は、Bubble Tea と Lipgloss を使った Go 製 TUI インストーラーを拡張するスキルです。画面状態、キー入力、描画、テストの規約を説明します。

主な機能

  1. 01model.go で画面と状態を定義
  2. 02Update() を通じた入力処理
  3. 03画面ごとのキー処理関数
  4. 04画面描画とタイトルの追加
  5. 05インストーラーのビルドとテスト

活用シーン

  1. 01インストーラー画面の追加
  2. 02キーボード操作への対応
  3. 03Lipgloss UI コンポーネントの作成
  4. 04画面遷移と状態の管理

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/gentleman-programming/engram --skill gentleman-bubbletea

インストール後、Claude Code で/gentleman-bubbleteaを入力して読み込まれたか確認します。

詳細

使用する場面

次の場合にこのスキルを使用します。

  • TUI インストーラーに新しい画面を追加する場合
  • キーボード入力やナビゲーションを処理する場合
  • Lipgloss を使って新しい UI コンポーネントを作成する場合
  • 画面遷移や状態管理に取り組む場合

重要なパターン

パターン 1:model.go の画面定数

すべての画面は、model.go で Screen 定数として定義する必要があります。

type Screen int

const (
    ScreenWelcome Screen = iota
    ScreenMainMenu
    ScreenOSSelect
    // ... new screens go here
    ScreenNewFeature      // Add new screen
    ScreenNewFeatureCat   // Add category screen if needed
)
パターン 2:Model 構造体ですべての状態を保持

model.go の Model 構造体は、アプリケーションの状態をすべて保持します。

type Model struct {
    Screen      Screen
    PrevScreen  Screen      // For back navigation
    Width       int
    Height      int
    Cursor      int
    // Add new state here
    NewFeatureData    []SomeType
    NewFeatureScroll  int
}
パターン 3:型 switch を使った Update パターン

すべての入力処理は、型 switch を使って Update() を通じて行います。

func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    switch msg := msg.(type) {
    case tea.KeyMsg:
        return m.handleKeyPress(msg)
    case tea.WindowSizeMsg:
        m.Width = msg.Width
        m.Height = msg.Height
        return m, nil
    case customMsg:
        // Handle custom messages
        return m, nil
    }
    return m, nil
}
パターン 4:キー入力ハンドラーは (Model, Cmd) を返す

画面ごとにハンドラーを分け、常に (tea.Model, tea.Cmd) を返します。

func (m Model) handleNewFeatureKeys(key string) (tea.Model, tea.Cmd) {
    options := m.GetCurrentOptions()

    switch key {
    case "up", "k":
        if m.Cursor > 0 {
            m.Cursor--
            // Skip separator
            if strings.HasPrefix(options[m.Cursor], "───") && m.Cursor > 0 {
                m.Cursor--
            }
        }
    case "down", "j":
        if m.Cursor < len(options)-1 {
            m.Cursor++
            if strings.HasPrefix(options[m.Cursor], "───") && m.Cursor < len(options)-1 {
                m.Cursor++
            }
        }
    case "enter", " ":
        // Handle selection
        return m.handleNewFeatureSelection()
    case "esc":
        m.Screen = m.PrevScreen
        m.Cursor = 0
    }
    return m, nil
}

判断ツリー

Adding a new screen?
├── Define Screen constant in model.go
├── Add state fields to Model struct
├── Add handler in handleKeyPress switch
├── Create handle{Screen}Keys function in update.go
├── Add view case in view.go
└── Add title in GetScreenTitle()

Adding navigation to existing screen?
├── Use m.PrevScreen for back navigation
├── Reset m.Cursor = 0 on screen change
└── Save scroll position if scrollable

Adding scrollable content?
├── Add {Screen}Scroll int to Model
├── Calculate visibleItems from m.Height
├── Handle up/down for scroll position
└── Reset scroll on screen exit

コード例

例 1:handleKeyPress への画面の追加
// In handleKeyPress switch statement:
case ScreenNewFeature:
    return m.handleNewFeatureKeys(key)

case ScreenNewFeatureCat:
    return m.handleNewFeatureCatKeys(key)
例 2:画面オプションのパターン
func (m Model) GetCurrentOptions() []string {
    switch m.Screen {
    case ScreenNewFeature:
        categories := make([]string, len(m.NewFeatureData)+2)
        for i, item := range m.NewFeatureData {
            categories[i] = item.Name
        }
        categories[len(m.NewFeatureData)] = "─────────────"
        categories[len(m.NewFeatureData)+1] = "← Back"
        return categories
    // ...
    }
}
例 3:スクロール可能なビューのパターン
func (m Model) handleNewFeatureCatKeys(key string) (tea.Model, tea.Cmd) {
    data := m.NewFeatureData[m.SelectedNewFeature]

    visibleItems := m.Height - 9
    if visibleItems < 5 {
        visibleItems = 5
    }

    maxScroll := len(data.Items) - visibleItems
    if maxScroll < 0 {
        maxScroll = 0
    }

    switch key {
    case "up", "k":
        if m.NewFeatureScroll > 0 {
            m.NewFeatureScroll--
        }
    case "down", "j":
        if m.NewFeatureScroll < maxScroll {
            m.NewFeatureScroll++
        }
    case "esc", "q", "enter", " ":
        m.Screen = ScreenNewFeature
        m.NewFeatureScroll = 0
    }
    return m, nil
}
例 4:カスタムメッセージのパターン
// Define message type
type newFeatureLoadedMsg struct {
    data []SomeType
    err  error
}

// Send message from command
func loadNewFeatureCmd() tea.Cmd {
    return func() tea.Msg {
        data, err := loadData()
        return newFeatureLoadedMsg{data: data, err: err}
    }
}

// Handle in Update
case newFeatureLoadedMsg:
    if msg.err != nil {
        m.ErrorMsg = msg.err.Error()
        return m, nil
    }
    m.NewFeatureData = msg.data
    return m, nil

コマンド

cd installer && go build ./cmd/gentleman-installer  # Build installer
cd installer && go test ./internal/tui/...          # Run TUI tests
cd installer && go test -run TestNewFeature         # Run specific test

リソース

  • Model:状態管理については installer/internal/tui/model.go を参照してください
  • Update:入力処理については installer/internal/tui/update.go を参照してください
  • View:描画については installer/internal/tui/view.go を参照してください
  • Styles:Lipgloss のスタイルについては installer/internal/tui/styles.go を参照してください

コメント