From d069317b221400bc4575b0a00e846fabb05eac3c Mon Sep 17 00:00:00 2001 From: Adam Jeniski Date: Wed, 21 Jan 2026 08:00:47 -1000 Subject: [PATCH] Delete bubbletea-guide.md --- bubbletea-guide.md | 1091 -------------------------------------------- 1 file changed, 1091 deletions(-) delete mode 100644 bubbletea-guide.md diff --git a/bubbletea-guide.md b/bubbletea-guide.md deleted file mode 100644 index e5b168d..0000000 --- a/bubbletea-guide.md +++ /dev/null @@ -1,1091 +0,0 @@ -# Bubbletea TUI Framework Guide - -A comprehensive guide to Charm's Bubbletea - a Go framework for building terminal user interfaces based on The Elm Architecture. - -## Table of Contents - -1. [Overview](#overview) -2. [The Elm Architecture](#the-elm-architecture) -3. [Core Concepts](#core-concepts) -4. [Messages and Commands](#messages-and-commands) -5. [Built-in Commands](#built-in-commands) -6. [Input Handling](#input-handling) -7. [Rendering](#rendering) -8. [Program Options](#program-options) -9. [Examples](#examples) - ---- - -## Overview - -Bubbletea is a functional, Elm-inspired framework for building terminal UIs in Go. It enforces a unidirectional data flow pattern that makes applications predictable, testable, and maintainable. - -**Key characteristics:** -- Functional reactive programming model -- Immutable state updates -- Message-driven architecture -- All side effects handled through commands -- Frame-rate limited rendering (60 FPS default) -- Delta rendering for performance - ---- - -## The Elm Architecture - -Bubbletea implements the Elm Architecture pattern: - -``` -Input → Message → Update → Model → View → Render - ↓ - Command → Message → (repeat) -``` - -The core principle: **Your model never directly performs I/O.** All side effects flow through Commands, creating predictable, testable code. - -### The Three Core Methods - -Every Bubbletea program implements the `tea.Model` interface: - -```go -type Model interface { - Init() Cmd // Returns initial command - Update(Msg) (Model, Cmd) // Handles messages, returns new state - View() string // Renders current state as string -} -``` - ---- - -## Core Concepts - -### Model - -The model holds all application state. It should be a struct containing everything needed to render the UI: - -```go -type model struct { - cursor int // Current cursor position - choices []string // Available options - selected map[int]struct{} // Selected items - quitting bool -} -``` - -### Init - -Called once when the program starts. Returns an optional command to run: - -```go -func (m model) Init() tea.Cmd { - // Return nil for no initial command - return nil - - // Or return a command to start something - return tea.Batch( - spinner.Tick, - fetchInitialData, - ) -} -``` - -### Update - -Receives messages and returns updated model plus optional command: - -```go -func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - switch msg := msg.(type) { - case tea.KeyMsg: - switch msg.String() { - case "q", "ctrl+c": - 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++ - } - } - } - return m, nil -} -``` - -### View - -Returns a string representation of the current state: - -```go -func (m model) View() string { - if m.quitting { - return "Goodbye!\n" - } - - s := "What do you want for lunch?\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 -} -``` - ---- - -## Messages and Commands - -### Messages (Msg) - -Messages are events that trigger state updates. They can be: - -**Built-in messages:** -- `tea.KeyMsg` - Keyboard input -- `tea.MouseMsg` - Mouse events -- `tea.WindowSizeMsg` - Terminal resize -- `tea.FocusMsg` - Terminal gained focus -- `tea.BlurMsg` - Terminal lost focus -- `tea.QuitMsg` - Program should exit - -**Custom messages:** - -```go -// Define your own message types -type tickMsg time.Time -type errMsg struct{ error } -type responseMsg struct { - status int - body string -} -``` - -### Commands (Cmd) - -Commands are functions that perform I/O and return messages: - -```go -// A Cmd is a function that returns a Msg -type Cmd func() Msg -``` - -**Creating commands:** - -```go -// Simple command returning a message -func tick() tea.Msg { - time.Sleep(time.Second) - return tickMsg{} -} - -// HTTP request command -func checkServer(url string) tea.Cmd { - return func() tea.Msg { - resp, err := http.Get(url) - if err != nil { - return errMsg{err} - } - defer resp.Body.Close() - return responseMsg{status: resp.StatusCode} - } -} -``` - ---- - -## Built-in Commands - -### Program Control - -```go -tea.Quit // Graceful shutdown (returns QuitMsg) -tea.Suspend // Suspend program (Unix, Ctrl+Z) -``` - -### Command Composition - -```go -// Run multiple commands concurrently -tea.Batch(cmd1, cmd2, cmd3) - -// Run commands sequentially (each waits for previous) -tea.Sequence(cmd1, cmd2, cmd3) -``` - -### Timers - -```go -// Fire once after duration -tea.Tick(time.Second, func(t time.Time) tea.Msg { - return tickMsg(t) -}) - -// Fire synchronized to system clock -tea.Every(time.Minute, func(t time.Time) tea.Msg { - return tickMsg(t) -}) -``` - -### Screen Control - -```go -tea.ClearScreen // Clear terminal -tea.EnterAltScreen // Full-window mode (no scrollback) -tea.ExitAltScreen // Return to normal mode -tea.ShowCursor // Make cursor visible -tea.HideCursor // Hide cursor -``` - -### Mouse Control - -```go -tea.EnableMouseCellMotion // Clicks, release, drag -tea.EnableMouseAllMotion // All motion including hover -tea.DisableMouse // Disable mouse -``` - -### External Process Execution - -```go -// Run external command (like $EDITOR) -cmd := exec.Command("vim", "file.txt") -tea.ExecProcess(cmd, func(err error) tea.Msg { - return editorFinishedMsg{err} -}) -``` - ---- - -## Input Handling - -### Keyboard Input (KeyMsg) - -```go -case tea.KeyMsg: - switch msg.String() { - // Character keys - case "a", "b", "c": - - // Special keys - case "enter", "tab", "backspace", "delete": - case "up", "down", "left", "right": - case "home", "end", "pgup", "pgdown": - - // Function keys - case "f1", "f2", "f12": - - // Control combinations - case "ctrl+c", "ctrl+d", "ctrl+z": - case "alt+enter", "shift+tab": - - // Escape - case "esc": - } - - // Check key type - switch msg.Type { - case tea.KeyRunes: - // Regular character input - char := string(msg.Runes) - case tea.KeyEnter: - case tea.KeyCtrlC: - } -``` - -### Mouse Input (MouseMsg) - -```go -case tea.MouseMsg: - switch msg.Button { - case tea.MouseButtonLeft: - if msg.Action == tea.MouseActionPress { - // Handle left click at msg.X, msg.Y - } - case tea.MouseButtonWheelUp: - // Scroll up - case tea.MouseButtonWheelDown: - // Scroll down - } -``` - -### Window Resize - -```go -case tea.WindowSizeMsg: - m.width = msg.Width - m.height = msg.Height -``` - ---- - -## Rendering - -### How Rendering Works - -1. After each Update, View() is called -2. The returned string is queued for rendering -3. A ticker fires at configured FPS (default 60) -4. Renderer compares with previous frame (delta detection) -5. Only changed lines are written to terminal - -### Rendering Tips - -- Return plain strings from View() -- Use newlines to separate lines -- Use [Lipgloss](https://github.com/charmbracelet/lipgloss) for styling -- Terminal clears and redraws automatically - -```go -func (m model) View() string { - // Build your view as a string - var b strings.Builder - - b.WriteString("Header\n\n") - - for _, item := range m.items { - b.WriteString(item + "\n") - } - - b.WriteString("\nFooter") - - return b.String() -} -``` - ---- - -## Program Options - -Configure program behavior with options: - -```go -p := tea.NewProgram( - initialModel, - tea.WithAltScreen(), // Use alternate screen buffer - tea.WithMouseCellMotion(), // Enable mouse (click/drag) - tea.WithMouseAllMotion(), // Enable all mouse events - tea.WithoutSignalHandler(), // Handle Ctrl+C yourself - tea.WithFPS(120), // Custom frame rate - tea.WithReportFocus(), // Get focus/blur events - tea.WithInput(customReader), // Custom input source - tea.WithOutput(customWriter), // Custom output destination -) - -// Run the program -finalModel, err := p.Run() -``` - ---- - -## Examples - -### Example 1: Simple Counter - -A minimal example demonstrating the core pattern: - -```go -package main - -import ( - "fmt" - tea "github.com/charmbracelet/bubbletea" -) - -type model struct { - count int -} - -func (m model) Init() tea.Cmd { - return nil -} - -func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - switch msg := msg.(type) { - case tea.KeyMsg: - switch msg.String() { - case "q", "ctrl+c": - return m, tea.Quit - case "up", "k": - m.count++ - case "down", "j": - m.count-- - } - } - return m, nil -} - -func (m model) View() string { - return fmt.Sprintf( - "Count: %d\n\nPress up/down to change, q to quit.\n", - m.count, - ) -} - -func main() { - p := tea.NewProgram(model{count: 0}) - if _, err := p.Run(); err != nil { - fmt.Println("Error:", err) - } -} -``` - -### Example 2: Countdown Timer with Commands - -Demonstrates async commands and timers: - -```go -package main - -import ( - "fmt" - "time" - tea "github.com/charmbracelet/bubbletea" -) - -type tickMsg time.Time - -type model struct { - seconds int - done bool -} - -func (m model) Init() tea.Cmd { - return tick() -} - -func tick() tea.Cmd { - return tea.Tick(time.Second, func(t time.Time) tea.Msg { - return tickMsg(t) - }) -} - -func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - switch msg := msg.(type) { - case tea.KeyMsg: - if msg.String() == "q" { - return m, tea.Quit - } - case tickMsg: - m.seconds-- - if m.seconds <= 0 { - m.done = true - return m, tea.Quit - } - return m, tick() - } - return m, nil -} - -func (m model) View() string { - if m.done { - return "Time's up!\n" - } - return fmt.Sprintf("Countdown: %d seconds remaining...\n\nPress q to quit.\n", m.seconds) -} - -func main() { - p := tea.NewProgram(model{seconds: 10}) - if _, err := p.Run(); err != nil { - fmt.Println("Error:", err) - } -} -``` - -### Example 3: HTTP Request - -Async HTTP requests with error handling: - -```go -package main - -import ( - "fmt" - "net/http" - "time" - tea "github.com/charmbracelet/bubbletea" -) - -const url = "https://httpstat.us/200" - -type statusMsg int -type errMsg struct{ error } - -type model struct { - status int - err error - loading bool -} - -func checkServer() tea.Msg { - client := &http.Client{Timeout: 10 * time.Second} - resp, err := client.Get(url) - if err != nil { - return errMsg{err} - } - defer resp.Body.Close() - return statusMsg(resp.StatusCode) -} - -func (m model) Init() tea.Cmd { - return checkServer -} - -func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - switch msg := msg.(type) { - case tea.KeyMsg: - if msg.String() == "q" { - return m, tea.Quit - } - case statusMsg: - m.status = int(msg) - m.loading = false - case errMsg: - m.err = msg.error - m.loading = false - } - return m, nil -} - -func (m model) View() string { - if m.loading { - return "Checking server...\n" - } - if m.err != nil { - return fmt.Sprintf("Error: %v\n\nPress q to quit.\n", m.err) - } - return fmt.Sprintf("Server returned: %d\n\nPress q to quit.\n", m.status) -} - -func main() { - p := tea.NewProgram(model{loading: true}) - if _, err := p.Run(); err != nil { - fmt.Println("Error:", err) - } -} -``` - -### Example 4: Multiple Choice Selection - -Interactive list with cursor and selection: - -```go -package main - -import ( - "fmt" - tea "github.com/charmbracelet/bubbletea" -) - -type model struct { - cursor int - choices []string - selected map[int]struct{} -} - -func initialModel() model { - return model{ - choices: []string{"Pizza", "Sushi", "Tacos", "Burger"}, - selected: make(map[int]struct{}), - } -} - -func (m model) Init() tea.Cmd { - return nil -} - -func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - switch msg := msg.(type) { - case tea.KeyMsg: - switch msg.String() { - case "q", "ctrl+c": - 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", " ": - if _, ok := m.selected[m.cursor]; ok { - delete(m.selected, m.cursor) - } else { - m.selected[m.cursor] = struct{}{} - } - } - } - return m, nil -} - -func (m model) View() string { - s := "What do you want for lunch?\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 space to select, q to quit.\n" - return s -} - -func main() { - p := tea.NewProgram(initialModel()) - if _, err := p.Run(); err != nil { - fmt.Println("Error:", err) - } -} -``` - -### Example 5: Multiple Views / State Machine - -Switch between different screens: - -```go -package main - -import ( - "fmt" - tea "github.com/charmbracelet/bubbletea" -) - -type viewState int - -const ( - menuView viewState = iota - detailView -) - -type model struct { - state viewState - cursor int - items []string - selected string -} - -func initialModel() model { - return model{ - state: menuView, - items: []string{"Option A", "Option B", "Option C"}, - } -} - -func (m model) Init() tea.Cmd { - return nil -} - -func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - switch msg := msg.(type) { - case tea.KeyMsg: - switch msg.String() { - case "q", "ctrl+c": - return m, tea.Quit - case "esc": - if m.state == detailView { - m.state = menuView - } - case "up", "k": - if m.state == menuView && m.cursor > 0 { - m.cursor-- - } - case "down", "j": - if m.state == menuView && m.cursor < len(m.items)-1 { - m.cursor++ - } - case "enter": - if m.state == menuView { - m.selected = m.items[m.cursor] - m.state = detailView - } - } - } - return m, nil -} - -func (m model) View() string { - switch m.state { - case menuView: - return m.menuView() - case detailView: - return m.detailView() - } - return "" -} - -func (m model) menuView() string { - s := "Select an option:\n\n" - for i, item := range m.items { - cursor := " " - if m.cursor == i { - cursor = ">" - } - s += fmt.Sprintf("%s %s\n", cursor, item) - } - s += "\nPress enter to select, q to quit.\n" - return s -} - -func (m model) detailView() string { - return fmt.Sprintf( - "You selected: %s\n\nPress esc to go back, q to quit.\n", - m.selected, - ) -} - -func main() { - p := tea.NewProgram(initialModel()) - if _, err := p.Run(); err != nil { - fmt.Println("Error:", err) - } -} -``` - -### Example 6: Component Composition - -Using sub-components (like spinners): - -```go -package main - -import ( - "fmt" - "time" - - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/bubbles/spinner" -) - -type model struct { - spinner spinner.Model - loading bool - result string -} - -type doneMsg struct{ result string } - -func doWork() tea.Msg { - time.Sleep(3 * time.Second) - return doneMsg{result: "Work complete!"} -} - -func initialModel() model { - s := spinner.New() - s.Spinner = spinner.Dot - return model{ - spinner: s, - loading: true, - } -} - -func (m model) Init() tea.Cmd { - return tea.Batch( - m.spinner.Tick, - doWork, - ) -} - -func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - switch msg := msg.(type) { - case tea.KeyMsg: - if msg.String() == "q" { - return m, tea.Quit - } - case doneMsg: - m.loading = false - m.result = msg.result - return m, nil - case spinner.TickMsg: - var cmd tea.Cmd - m.spinner, cmd = m.spinner.Update(msg) - return m, cmd - } - return m, nil -} - -func (m model) View() string { - if m.loading { - return fmt.Sprintf("%s Loading...\n", m.spinner.View()) - } - return fmt.Sprintf("%s\n\nPress q to quit.\n", m.result) -} - -func main() { - p := tea.NewProgram(initialModel()) - if _, err := p.Run(); err != nil { - fmt.Println("Error:", err) - } -} -``` - -### Example 7: Text Input Form - -Multiple text inputs with focus management: - -```go -package main - -import ( - "fmt" - "strings" - - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/bubbles/textinput" -) - -type model struct { - inputs []textinput.Model - focused int - done bool -} - -func initialModel() model { - inputs := make([]textinput.Model, 3) - - inputs[0] = textinput.New() - inputs[0].Placeholder = "Name" - inputs[0].Focus() - inputs[0].CharLimit = 50 - - inputs[1] = textinput.New() - inputs[1].Placeholder = "Email" - inputs[1].CharLimit = 100 - - inputs[2] = textinput.New() - inputs[2].Placeholder = "Message" - inputs[2].CharLimit = 500 - - return model{inputs: inputs} -} - -func (m model) Init() tea.Cmd { - return textinput.Blink -} - -func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - var cmds []tea.Cmd - - switch msg := msg.(type) { - case tea.KeyMsg: - switch msg.String() { - case "ctrl+c": - return m, tea.Quit - case "tab", "shift+tab", "down", "up": - // Cycle focus - if msg.String() == "shift+tab" || msg.String() == "up" { - m.focused-- - if m.focused < 0 { - m.focused = len(m.inputs) - 1 - } - } else { - m.focused++ - if m.focused >= len(m.inputs) { - m.focused = 0 - } - } - for i := range m.inputs { - if i == m.focused { - cmds = append(cmds, m.inputs[i].Focus()) - } else { - m.inputs[i].Blur() - } - } - case "enter": - if m.focused == len(m.inputs)-1 { - m.done = true - return m, tea.Quit - } - m.focused++ - for i := range m.inputs { - if i == m.focused { - cmds = append(cmds, m.inputs[i].Focus()) - } else { - m.inputs[i].Blur() - } - } - } - } - - // Update all inputs - for i := range m.inputs { - var cmd tea.Cmd - m.inputs[i], cmd = m.inputs[i].Update(msg) - cmds = append(cmds, cmd) - } - - return m, tea.Batch(cmds...) -} - -func (m model) View() string { - if m.done { - return fmt.Sprintf( - "Submitted!\nName: %s\nEmail: %s\nMessage: %s\n", - m.inputs[0].Value(), - m.inputs[1].Value(), - m.inputs[2].Value(), - ) - } - - var b strings.Builder - b.WriteString("Contact Form\n\n") - - for i, input := range m.inputs { - b.WriteString(input.View()) - if i < len(m.inputs)-1 { - b.WriteString("\n") - } - } - - b.WriteString("\n\nTab to move, Enter to submit, Ctrl+C to quit.\n") - return b.String() -} - -func main() { - p := tea.NewProgram(initialModel()) - if _, err := p.Run(); err != nil { - fmt.Println("Error:", err) - } -} -``` - -### Example 8: Full-Screen with Alt Screen and Mouse - -Full-window application with mouse support: - -```go -package main - -import ( - "fmt" - tea "github.com/charmbracelet/bubbletea" -) - -type model struct { - width int - height int - mouseX int - mouseY int - clicks int -} - -func (m model) Init() tea.Cmd { - return nil -} - -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 tea.WindowSizeMsg: - m.width = msg.Width - m.height = msg.Height - case tea.MouseMsg: - m.mouseX = msg.X - m.mouseY = msg.Y - if msg.Action == tea.MouseActionPress { - m.clicks++ - } - } - return m, nil -} - -func (m model) View() string { - return fmt.Sprintf( - "Window: %dx%d\nMouse: (%d, %d)\nClicks: %d\n\nClick anywhere! Press q to quit.", - m.width, m.height, - m.mouseX, m.mouseY, - m.clicks, - ) -} - -func main() { - p := tea.NewProgram( - model{}, - tea.WithAltScreen(), // Full-screen mode - tea.WithMouseAllMotion(), // Track all mouse movement - ) - if _, err := p.Run(); err != nil { - fmt.Println("Error:", err) - } -} -``` - ---- - -## Key Architectural Patterns - -### 1. Unidirectional Data Flow -All data flows in one direction: Input → Message → Update → Model → View - -### 2. Immutability -Update returns a new model rather than mutating the existing one. - -### 3. Commands for Side Effects -All I/O happens through Commands - never in Update or View. - -### 4. Message Types for Events -Use custom message types to represent domain events. - -### 5. Component Delegation -Sub-components handle their own Update and View: - -```go -var cmd tea.Cmd -m.subComponent, cmd = m.subComponent.Update(msg) -return m, cmd -``` - -### 6. State Machine for Views -Use an enum to track which view is active: - -```go -switch m.state { -case loading: - return loadingView(m) -case ready: - return mainView(m) -case error: - return errorView(m) -} -``` - ---- - -## Related Libraries - -- **[Bubbles](https://github.com/charmbracelet/bubbles)** - Pre-built components (spinner, textinput, list, table, etc.) -- **[Lipgloss](https://github.com/charmbracelet/lipgloss)** - Styling and layout -- **[Harmonica](https://github.com/charmbracelet/harmonica)** - Smooth animations -- **[Glamour](https://github.com/charmbracelet/glamour)** - Markdown rendering - ---- - -## Resources - -- [Bubbletea GitHub](https://github.com/charmbracelet/bubbletea) -- [Official Examples](https://github.com/charmbracelet/bubbletea/tree/master/examples) -- [The Elm Architecture](https://guide.elm-lang.org/architecture/) -- [Charm Website](https://charm.sh)