Source-verified skills for the Charm terminal stack: Bubble Tea v2, Bubbles v2, Lip Gloss v2, Huh v2, Log v2, Glamour v2, plus the gum and vhs CLIs.
100
100%
Does it follow best practices?
Impact
100%
1.02xAverage score across 9 eval scenarios
Passed
No findings from the security scan
Add ready-made TUI components to a Bubble Tea v2 program: lists, tables, text inputs, viewports, spinners, progress bars, file pickers, trees, and timers, each wired through the same Init, Update, and View contract.
Import each component from its own package ("charm.land/bubbles/v2/list",
.../table, .../viewport, and so on), pinned to v2.2.1. Go blocks headed
// examples/... are trimmed from
complete programs that build against the
pinned modules.
If the project's go.mod still requires github.com/charmbracelet/bubbles
(pre-v2), the v2 APIs here do not apply. Use go doc against the
project's version and keep its import paths unless the user asks to
migrate.
isDark), and store it as a value field.Init: batch its Init(), Focus(), or Tick
command with your own.Update, size it from tea.WindowSizeMsg, send key presses only to
the focused bubble, forward every other message, store the returned
bubble back, and batch the returned commands.View() string inside your tea.View.go vet ./... and go build ./..., then run the program in a
terminal. If a bubble is frozen, empty, or ignores keys, check
Mistakes that cost time, fix, and rerun.An interactive bubble is a struct with Update(tea.Msg) (Model, tea.Cmd)
and View() string. Note both differences from your top level model:
Update returns the bubble's own concrete type (no type assertion needed),
and View returns a plain string that you place inside your tea.View.
Not every package fits this shape: help.Model.View(k KeyMap) takes the
key map to render, key is a plain data package with no model, and
cursor is driven by the inputs that embed it. Do not build an interface
over "all bubbles" that assumes a zero argument View.
A complete program with one bubble (examples/charmed-bubbles/spinner/main.go); list, table, viewport, and the others slot into the same places:
// examples/charmed-bubbles/spinner/main.go
package main
import (
"fmt"
"os"
"charm.land/bubbles/v2/spinner"
tea "charm.land/bubbletea/v2"
)
type model struct {
spinner spinner.Model
}
func (m model) Init() tea.Cmd {
return m.spinner.Tick // a method value is a tea.Cmd
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if msg, ok := msg.(tea.KeyPressMsg); ok && (msg.String() == "q" || msg.String() == "ctrl+c") {
return m, tea.Quit
}
var cmd tea.Cmd
m.spinner, cmd = m.spinner.Update(msg) // store it back; bubbles are values
return m, cmd
}
func (m model) View() tea.View {
return tea.NewView(m.spinner.View() + " working, q to quit\n")
}
func main() {
m := model{spinner: spinner.New(spinner.WithSpinner(spinner.Dot))}
if _, err := tea.NewProgram(m).Run(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}Some bubbles have an Init() tea.Cmd that must run (filepicker reads its
directory, timer starts ticking); others start on a method that returns a
Cmd (textinput.Focus(), spinner.Tick). Batch these from your own
Init.
Rules of thumb:
Update; if you gate on key presses only, they stall.SetWidth, SetHeight, SetSize, Width(),
Height(). Call them from your tea.WindowSizeMsg handler.isDark bool: list.DefaultStyles,
list.NewDefaultItemStyles, textinput.DefaultStyles,
textarea.DefaultStyles, help.DefaultStyles, tree.DefaultStyles.
Two do not: table.DefaultStyles() and filepicker.DefaultStyles() take
no argument. Get the bool from tea.BackgroundColorMsg (see
charmed-bubbletea).textinput.DefaultKeyMap(), not a package
variable.key.Binding pairs the keys that trigger an action with the help text
shown for it. key.Matches(msg, binding) works on a tea.KeyPressMsg.
A type with ShortHelp() []key.Binding and FullHelp() [][]key.Binding
satisfies help.KeyMap, and help.Model.View(keymap) renders it in one
line (or all groups when ShowAll is true).
// examples/charmed-bubbletea/compose/main.go
var keys = keyMap{
Submit: key.NewBinding(key.WithKeys("enter"), key.WithHelp("enter", "submit")),
Quit: key.NewBinding(key.WithKeys("ctrl+c", "esc"), key.WithHelp("esc", "quit")),
}
case tea.KeyPressMsg:
switch {
case key.Matches(msg, keys.Quit):
return m, tea.Quit
}
m.help.View(keys) // "enter submit • esc quit"Disable a binding with SetEnabled(false) and it drops out of help too.
| Component | Reference | Read it for |
|---|---|---|
| textinput, textarea, cursor | references/text-input.md | focus switching, echo modes, validation, suggestions, the real cursor, cursor modes |
| list | references/list.md | items and delegates, filtering, item mutation, default keys |
| table | references/table.md | columns, rows, styles, selection, loading delimited text |
| viewport | references/viewport.md | sizing, wrapping, horizontal scroll, search highlights, gutters |
| spinner, progress | references/progress-spinner.md | spinner presets, animated and static bars, color blends |
| filepicker, tree | references/filepicker-tree.md | allowed types, selection checks, tree nodes, open and close keys |
| paginator, timer, stopwatch | references/small-widgets.md | slice bounds, countdown timeout, start, stop, toggle |
Import paths move to charm.land/bubbles/v2/<pkg>, tea.KeyMsg cases become
tea.KeyPressMsg, width and height fields become SetWidth, SetHeight,
and Width() methods, default key maps become functions, NewModel
constructors become New, style constructors take isDark, spinner.Tick
becomes a method, and the progress gradient options become WithColors
and WithDefaultBlend. Read references/migration.md
for the full search and replace table and the upstream upgrade guide. After
migrating, run go vet ./... and go build ./... and fix what remains.
The skill describes v2.2.1. The interactive tree package was added in
v2.2, so earlier v2 releases do not have it. The list's Quit binding on
v (see references/list.md) is v2.2.1 behavior;
check the pinned version's DefaultKeyMap after a bump.
Init() on filepicker or timer. Nothing loads, nothing runs.SetSize
from WindowSizeMsg. It renders empty.SettingFilter() first.Update to a new variable and dropping it.
Bubbles are values; store them back into the model.