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
Import as tea "charm.land/bubbletea/v2", pinned to v2.1.0. Go blocks headed
// examples/... are trimmed from
complete programs
that build against the
pinned modules;
// excerpt blocks are API sketches.
If the project's go.mod still requires github.com/charmbracelet/bubbletea
(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.
Init, Update, and
View methods (see the loop).Init (a fetch, a tick, a focus), or nil.Update, switch on the message type, change state, and return any
follow up work as a tea.Cmd. Handle ctrl+c and tea.WindowSizeMsg.tea.View from View, setting terminal features such as
AltScreen and MouseMode as fields.tea.NewProgram(m).Run() and check its error.go vet ./... and go test ./... from the application
module, fix failures, and rerun until both pass. Then try keys and
resizing in a real terminal.The v2 delta from the usual Elm loop: View returns a tea.View, not a
string, and the runtime repaints only what changed. Msg is any; Cmd
is func() Msg, run on its own goroutine. Keep state changes in Update
and blocking work in commands.
Minimal program (examples/charmed-bubbletea/counter/main.go):
// examples/charmed-bubbletea/counter/main.go
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.KeyPressMsg:
switch msg.String() {
case "q", "ctrl+c":
return m, tea.Quit
case "up", "k", "space":
m.count++
case "down", "j":
m.count--
}
}
return m, nil
}
func (m model) View() tea.View {
return tea.NewView(fmt.Sprintf("Count: %d\n\nup/down or space to change, q to quit\n", m.count))
}
func main() {
if _, err := tea.NewProgram(model{}).Run(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}Run blocks until the program quits and returns the final model, which is
how you read results out after the UI closes (see the
testing example).
There are no WithAltScreen options and no EnterAltScreen commands. You
set fields on the tea.View you return, every frame, and the runtime
reconciles the terminal to match.
// excerpt: every tea.View field in one place; fullscreen and mouse examples set a subset
func (m model) View() tea.View {
v := tea.NewView(content) // or: var v tea.View; v.SetContent(content)
v.AltScreen = true // full window mode
v.WindowTitle = "my app" // terminal title
v.MouseMode = tea.MouseModeCellMotion
v.ReportFocus = true // get FocusMsg and BlurMsg
v.Cursor = tea.NewCursor(x, y) // show the real cursor here; nil hides it
return v
}Because the fields are re-read on every render, turning something off is just
not setting it. Conditional alt screen, a cursor that only shows while an
input is focused, a title that tracks state: all of these are plain if
statements in View.
tea.WindowSizeMsg arrives once at start and on every resize; store
Width and Height and lay out from them in View. Read
references/screen.md for a full screen layout, light
or dark background detection, and the remaining fields (terminal colors, the
native progress bar, keyboard enhancements, OnMouse, ProgramStatus).
Match on tea.KeyPressMsg. msg.String() gives a readable name and is the
usual way to switch. Space is "space", not " ".
// excerpt: key names as msg.String() reports them
case tea.KeyPressMsg:
switch msg.String() {
case "ctrl+c", "esc": return m, tea.Quit
case "enter": // ...
case "shift+tab": // ...
case "a": // plain characters are themselves
}Prefer key.Binding from Bubbles for anything beyond a toy: it gives you
one place to define keys and help text, and key.Matches(msg, binding)
works directly on a KeyPressMsg. See references/compose.md.
Read references/input.md for structured matching on
Code, Text, and Mod, key release events, paste messages, and mouse
handling.
A Cmd is func() tea.Msg. Write a function that does the work and returns
a message, then return that function (not its result) from Init or
Update.
examples/charmed-bubbletea/asyncfetch/main.go:
// examples/charmed-bubbletea/asyncfetch/main.go
type statusMsg int
type errMsg struct{ err error }
func checkServer(url string) tea.Cmd {
return func() tea.Msg {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return errMsg{err}
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return errMsg{err}
}
defer res.Body.Close()
return statusMsg(res.StatusCode)
}
}
func (m model) Init() tea.Cmd { return checkServer(m.url) }Combinators: tea.Batch(cmds...) runs commands concurrently with no
ordering; tea.Sequence(cmds...) runs them one after another. Both drop
nil entries, so collecting child commands into a slice and batching is safe.
Built in commands worth knowing: tea.Quit, tea.Interrupt (Run returns
tea.ErrInterrupted), tea.Suspend (ctrl+z behavior, then ResumeMsg),
tea.Println and tea.Printf (print above the UI; no effect in alt
screen), tea.SetClipboard, tea.ReadClipboard, tea.RequestWindowSize,
tea.RequestBackgroundColor, tea.ExecProcess. Read
references/api.md for every message type, command,
program option, and constant group.
Read references/async.md for repeating timers
(tea.Tick, tea.Every), feeding the program from a channel or
p.Send, and handing the terminal to an editor with tea.ExecProcess.
The parent stores child components, forwards messages, and batches the commands they return. Children need every message that concerns them (a spinner needs its tick even while an input has focus), so forward first and gate only where you must.
// examples/charmed-bubbletea/compose/main.go
var cmd tea.Cmd
m.spinner, cmd = m.spinner.Update(msg)
cmds = append(cmds, cmd)
m.input, cmd = m.input.Update(msg)
cmds = append(cmds, cmd)
return m, tea.Batch(cmds...)Read references/compose.md for the full parent model:
a key.Binding key map with help, focus on start, resizing children, and
placing a text input's real cursor in the view.
Update with constructed messages and inspect the returned model and command.Validate as in workflow step 6. See testing and debugging for unit tests, headless runs, color-free output, and file logging.
The shape of the change: the import path moves to charm.land/bubbletea/v2,
View() string becomes View() tea.View, tea.KeyMsg becomes
tea.KeyPressMsg with Code, Text, and Mod fields, mouse messages
split into concrete types, and every program option or command that toggled
a terminal feature (alt screen, mouse, title, cursor) becomes a tea.View
field. The tea.View change is the biggest one. The search and replace
table, in order, is in references/api.md; the
upgrade guide
has the complete list.
After applying the table, validate as in workflow step 6, check remaining
v1-only calls such as tea.EnterAltScreen, and exercise timers too.
tea.KeyMsg remains valid as an interface in v2; do not remove every occurrence.
Projects pinned to a v2 release before v2.1.0 lack ProgramStatus and a
few related calls; references/api.md
lists them.
checkServer(m.url) and tick()
return a tea.Cmd; return that command from Init or Update. tea.Quit
already has the command signature, so return it without calling it. Calling
a command inside Update runs its work synchronously and yields a message
instead of the required command.Update or View. The UI freezes.WindowSizeMsg and then wondering why layout is wrong at start.fmt while running. Use tea.Println (outside alt screen)
or tea.LogToFile.tea.Quit or tea.Interrupt." " for space, or tea.KeyMsg as a struct. Both are v1.