home / skills / gentleman-programming / gentleman.dots / gentleman-bubbletea

gentleman-bubbletea skill

/skills/gentleman-bubbletea

This skill helps you implement and navigate Bubbletea TUI screens in the Gentleman installer, streamlining state, input handling, and screen transitions.

npx playbooks add skill gentleman-programming/gentleman.dots --skill gentleman-bubbletea

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
5.7 KB
---
name: gentleman-bubbletea
description: >
  Bubbletea TUI patterns for Gentleman.Dots installer.
  Trigger: When editing Go files in installer/internal/tui/, working on TUI screens, or adding new UI features.
license: Apache-2.0
metadata:
  author: gentleman-programming
  version: "1.0"
---

## When to Use

Use this skill when:
- Adding new screens to the TUI installer
- Handling keyboard input or navigation
- Creating new UI components with Lipgloss
- Working on screen transitions or state management

---

## Critical Patterns

### Pattern 1: Screen Constants in model.go

All screens MUST be defined as `Screen` constants in `model.go`:

```go
type Screen int

const (
    ScreenWelcome Screen = iota
    ScreenMainMenu
    ScreenOSSelect
    // ... new screens go here
    ScreenNewFeature      // Add new screen
    ScreenNewFeatureCat   // Add category screen if needed
)
```

### Pattern 2: Model Struct Holds All State

The `Model` struct in `model.go` holds ALL application state:

```go
type Model struct {
    Screen      Screen
    PrevScreen  Screen      // For back navigation
    Width       int
    Height      int
    Cursor      int
    // Add new state here
    NewFeatureData    []SomeType
    NewFeatureScroll  int
}
```

### Pattern 3: Update Pattern with Type Switch

All input handling goes through `Update()` with a type switch:

```go
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
}
```

### Pattern 4: Key Handlers Return (Model, Cmd)

Separate handler per screen, always return `(tea.Model, tea.Cmd)`:

```go
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
}
```

---

## Decision Tree

```
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
```

---

## Code Examples

### Example 1: Adding Screen to handleKeyPress

```go
// In handleKeyPress switch statement:
case ScreenNewFeature:
    return m.handleNewFeatureKeys(key)

case ScreenNewFeatureCat:
    return m.handleNewFeatureCatKeys(key)
```

### Example 2: Screen Options Pattern

```go
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
    // ...
    }
}
```

### Example 3: Scrollable View Pattern

```go
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
}
```

### Example 4: Custom Message Pattern

```go
// 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
```

---

## Commands

```bash
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
```

---

## Resources

- **Model**: See `installer/internal/tui/model.go` for state management
- **Update**: See `installer/internal/tui/update.go` for input handling
- **View**: See `installer/internal/tui/view.go` for rendering
- **Styles**: See `installer/internal/tui/styles.go` for Lipgloss styles

Overview

This skill provides Bubbletea TUI patterns and conventions for the Gentleman.Dots installer. It documents required screen constants, state layout, input handling, key handler contracts, and scrolling patterns so new UI features integrate consistently. Use it to add screens, navigation, or scrollable components with predictable behavior.

How this skill works

The patterns define a single Model struct that holds all UI state and a Screen enum for every TUI screen. All messages flow through Update() with a type switch; input is delegated to per-screen key handlers that return (tea.Model, tea.Cmd). View rendering uses a switch on m.Screen and GetScreenTitle() supplies screen titles. Scroll, cursor, and back-navigation state are explicit fields on the Model so transitions and state restoration are simple.

When to use it

  • Adding a new installer screen or category
  • Implementing keyboard navigation or selection handling
  • Creating scrollable lists or long content views
  • Introducing Lipgloss-styled components or new layout states
  • Saving and restoring navigation/scroll state across screens

Best practices

  • Declare every screen as a Screen constant in model.go using iota
  • Keep all UI state in the Model struct; add explicit fields for new screens (cursor, scroll, selections)
  • Handle all msg types in Update() and delegate key events to screen-specific handlers
  • Always return (tea.Model, tea.Cmd) from key handlers to follow Bubbletea conventions
  • Reset cursor/scroll when exiting a screen and use PrevScreen for back navigation
  • Compute visible items from m.Height and clamp scroll ranges before applying changes

Example use cases

  • Add ScreenNewFeature constant, add NewFeatureData and scroll fields to Model, wire handler in handleKeyPress, add view case and title entry
  • Implement a category list: build GetCurrentOptions() to include separators and a Back item, handle up/down skipping separators, handle enter to open category screen
  • Create a long README viewer: add ScreenReadme, ReadmeScroll int, compute visibleItems from m.Height, clamp maxScroll and update on up/down
  • Load remote data asynchronously: return a custom msg from a command, handle newFeatureLoadedMsg in Update to populate Model and clear errors
  • Test TUI code: build installer binary or run targeted tests in installer/internal/tui

FAQ

Where do I add a new screen constant?

Add it to the Screen const block in model.go so all code references a single canonical list.

How do I handle back navigation?

Set m.PrevScreen before changing Screen and restore it on esc; reset cursor/scroll to sensible defaults on change.