1---
2name: charm-bubbletea
3description: "Build terminal UIs in Go with Bubble Tea v2's Elm Architecture (Model/Update/View). Use when building Go TUI apps, tea.Model, tea.Cmd, Elm architecture, or terminal applications. NOT for pre-built TUI components (use bubbles)."
4argument-hint: "[component or pattern name]"
5---
6
7# Bubble Tea v2
8
9Build terminal UIs in Go using the Elm Architecture. Import as `tea "charm.land/bubbletea/v2"`.
10
11**$ARGUMENTS context**: If a specific component or pattern is requested, focus guidance on that area.
12
13## Quick Start
14
15Minimal working program - a counter:
16
17```go
18package main
19
20import (
21 "fmt"
22 "os"
23
24 tea "charm.land/bubbletea/v2"
25)
26
27type model struct{ count int }
28
29func (m model) Init() tea.Cmd { return nil }
30
31func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
32 switch msg := msg.(type) {
33 case tea.KeyPressMsg:
34 switch msg.String() {
35 case "q", "ctrl+c":
36 return m, tea.Quit
37 case "up", "k":
38 m.count++
39 case "down", "j":
40 m.count--
41 }
42 }
43 return m, nil
44}
45
46func (m model) View() tea.View {
47 return tea.NewView(fmt.Sprintf("Count: %d\n\nup/down to change, q to quit\n", m.count))
48}
49
50func main() {
51 if _, err := tea.NewProgram(model{}).Run(); err != nil {
52 fmt.Println("Error:", err)
53 os.Exit(1)
54 }
55}
56```
57
58## Core API Reference
59
60### The Elm Architecture
61
62Bubble Tea follows the Elm Architecture - a unidirectional data flow pattern:
63
641. **Model** holds all application state
652. **Update** receives messages, returns updated model + optional command
663. **View** renders the model to a string (called after every Update)
674. **Commands** perform async I/O, returning messages back to Update
68
69The runtime owns the loop. You never mutate state outside Update. You never call View yourself.
70
71### tea.Model Interface
72
73```go
74type Model interface {
75 Init() Cmd // initial command (return nil for none)
76 Update(Msg) (Model, Cmd) // handle messages, return new state + command
77 View() View // render UI
78}
79```
80
81### tea.Cmd and tea.Msg
82
83```go
84type Msg = any // messages can be any type
85type Cmd func() Msg // commands are functions that return a message
86```
87
88**A Cmd is a function that performs I/O and returns a Msg.** Return `nil` for no command.
89
90### tea.View
91
92```go
93// Create a view from a string
94v := tea.NewView("Hello, World!")
95
96// View struct fields (set after creation):
97v.AltScreen = true // fullscreen mode
98v.MouseMode = tea.MouseModeCellMotion // enable mouse
99v.ReportFocus = true // get FocusMsg/BlurMsg
100v.Cursor = tea.NewCursor(x, y) // show cursor at position
101v.WindowTitle = "My App" // set terminal title
102v.BackgroundColor = someColor // set terminal bg
103v.KeyboardEnhancements.ReportEventTypes = true // key release events
104```
105
106### Program
107
108```go
109p := tea.NewProgram(model{}, opts...) // create program
110m, err := p.Run() // run (blocks until quit)
111p.Send(msg) // send message from outside
112p.Quit() // quit from outside
113p.Kill() // force kill
114p.Wait() // block until shutdown
115```
116
117### Program Options
118
119```go
120tea.WithContext(ctx) // cancellable context
121tea.WithInput(reader) // custom input (nil to disable)
122tea.WithOutput(writer) // custom output
123tea.WithFilter(func(Model, Msg) Msg) // intercept/filter messages
124tea.WithFPS(fps) // custom FPS (default 60, max 120)
125tea.WithEnvironment([]string) // custom env vars (SSH)
126tea.WithWindowSize(w, h) // initial size (testing)
127tea.WithColorProfile(profile) // force color profile
128tea.WithoutRenderer() // no TUI rendering
129tea.WithoutSignalHandler() // handle signals yourself
130tea.WithoutCatchPanics() // disable panic recovery
131```
132
133### Built-in Messages
134
135| Message | When |
136|---------|------|
137| `tea.KeyPressMsg` | Key pressed. Use `msg.String()` to match (e.g. `"ctrl+c"`, `"enter"`, `"a"`) |
138| `tea.KeyReleaseMsg` | Key released (needs keyboard enhancements) |
139| `tea.WindowSizeMsg` | Terminal resized. Fields: `Width`, `Height`. Sent on startup + resize |
140| `tea.MouseClickMsg` | Mouse click. Fields: `X`, `Y`, `Button`, `Mod` |
141| `tea.MouseReleaseMsg` | Mouse button released |
142| `tea.MouseWheelMsg` | Scroll wheel |
143| `tea.MouseMotionMsg` | Mouse moved (needs AllMotion mode) |
144| `tea.FocusMsg` | Terminal gained focus (needs `ReportFocus`) |
145| `tea.BlurMsg` | Terminal lost focus |
146| `tea.PasteMsg` | Bracketed paste. Field: `Content` |
147| `tea.ColorProfileMsg` | Terminal color profile on startup |
148| `tea.BackgroundColorMsg` | Response to `RequestBackgroundColor`. Has `IsDark()` |
149| `tea.ResumeMsg` | Program resumed after suspend |
150| `tea.KeyboardEnhancementsMsg` | Terminal keyboard capabilities |
151
152### Built-in Commands
153
154| Command | What it does |
155|---------|-------------|
156| `tea.Quit` | Exit the program |
157| `tea.Suspend` | Suspend (ctrl+z behavior) |
158| `tea.Interrupt` | Interrupt (returns `ErrInterrupted`) |
159| `tea.ClearScreen` | Clear terminal |
160| `tea.Batch(cmds...)` | Run commands concurrently |
161| `tea.Sequence(cmds...)` | Run commands in order |
162| `tea.Every(dur, fn)` | Tick synced with system clock |
163| `tea.Tick(dur, fn)` | Tick from invocation time |
164| `tea.Println(args...)` | Print above TUI (persists across renders) |
165| `tea.Printf(tmpl, args...)` | Printf above TUI |
166| `tea.SetClipboard(s)` | Set system clipboard (OSC52) |
167| `tea.ReadClipboard` | Read system clipboard |
168| `tea.RequestWindowSize` | Query current window size |
169| `tea.RequestBackgroundColor` | Query terminal background color |
170| `tea.ExecProcess(cmd, callback)` | Run external process (e.g. editor) |
171| `tea.Raw(seq)` | Send raw escape sequence |
172
173### Key Handling
174
175```go
176case tea.KeyPressMsg:
177 switch msg.String() {
178 case "ctrl+c", "q": // string matching (most common)
179 return m, tea.Quit
180 case "up", "k": // arrow keys have string names
181 case "enter", "space": // special keys
182 case "ctrl+s": // modifier combos
183 case "a": // regular characters
184 }
185
186// Or use the Key struct for type-safe matching:
187case tea.KeyPressMsg:
188 key := msg.Key()
189 switch key.Code {
190 case tea.KeyEnter: // typed constant
191 case tea.KeyTab:
192 case tea.KeyEsc:
193 }
194 // key.Mod for modifiers: tea.ModCtrl, tea.ModAlt, tea.ModShift
195 // key.Text for printable characters
196```
197
198### Mouse Handling
199
200Enable mouse in View, handle in Update:
201
202```go
203func (m model) View() tea.View {
204 v := tea.NewView(m.render())
205 v.MouseMode = tea.MouseModeCellMotion // or tea.MouseModeAllMotion
206 return v
207}
208
209func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
210 switch msg := msg.(type) {
211 case tea.MouseClickMsg:
212 x, y := msg.X, msg.Y
213 if msg.Button == tea.MouseLeft { /* handle click */ }
214 case tea.MouseWheelMsg:
215 if msg.Button == tea.MouseWheelUp { /* scroll up */ }
216 case tea.MouseMsg:
217 // catches all mouse events
218 mouse := msg.Mouse()
219 }
220 return m, nil
221}
222```
223
224## Common Patterns
225
226### Pattern 1: Async I/O (HTTP, file, etc.)
227
228Define a custom Msg type. Write a Cmd that does the I/O and returns it.
229
230```go
231type statusMsg int
232type errMsg struct{ error }
233
234func checkServer() tea.Msg {
235 res, err := http.Get("https://example.com")
236 if err != nil {
237 return errMsg{err}
238 }
239 defer res.Body.Close()
240 return statusMsg(res.StatusCode)
241}
242
243func (m model) Init() tea.Cmd { return checkServer } // note: pass function, don't call it
244
245func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
246 switch msg := msg.(type) {
247 case statusMsg:
248 m.status = int(msg)
249 return m, tea.Quit
250 case errMsg:
251 m.err = msg.error
252 return m, nil
253 }
254 return m, nil
255}
256```
257
258### Pattern 2: Ticking / Timers
259
260`tea.Tick` and `tea.Every` send a single message. Re-dispatch to loop.
261
262```go
263type tickMsg time.Time
264
265func doTick() tea.Cmd {
266 return tea.Tick(time.Second, func(t time.Time) tea.Msg {
267 return tickMsg(t)
268 })
269}
270
271func (m model) Init() tea.Cmd { return doTick() }
272
273func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
274 switch msg.(type) {
275 case tickMsg:
276 m.elapsed++
277 return m, doTick() // re-dispatch to keep ticking
278 }
279 return m, nil
280}
281```
282
283**`tea.Every`** syncs with the system clock (wall-clock aligned ticks). **`tea.Tick`** starts from invocation.
284
285### Pattern 3: Composing Child Components
286
287Child components follow the same Model/Update/View pattern. Parent delegates messages.
288
289```go
290type parentModel struct {
291 spinner spinner.Model
292 input textinput.Model
293 focus int
294}
295
296func (m parentModel) Init() tea.Cmd {
297 return tea.Batch(m.spinner.Tick, m.input.Focus())
298}
299
300func (m parentModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
301 var cmds []tea.Cmd
302
303 // Always update spinner (it needs ticks regardless of focus)
304 var cmd tea.Cmd
305 m.spinner, cmd = m.spinner.Update(msg)
306 cmds = append(cmds, cmd)
307
308 // Only update focused component for key events
309 switch msg.(type) {
310 case tea.KeyPressMsg:
311 if m.focus == 0 {
312 m.input, cmd = m.input.Update(msg)
313 cmds = append(cmds, cmd)
314 }
315 }
316
317 return m, tea.Batch(cmds...)
318}
319
320func (m parentModel) View() tea.View {
321 return tea.NewView(m.spinner.View() + "\n" + m.input.View())
322}
323```
324
325### Pattern 4: External Messages via Channel
326
327Use `p.Send()` to push messages from goroutines, or use a channel-based Cmd.
328
329```go
330// Channel-based approach (preferred for subscription-like behavior)
331func waitForActivity(sub chan resultMsg) tea.Cmd {
332 return func() tea.Msg {
333 return <-sub // blocks until message arrives
334 }
335}
336
337func (m model) Init() tea.Cmd {
338 return waitForActivity(m.sub)
339}
340
341func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
342 switch msg := msg.(type) {
343 case resultMsg:
344 m.results = append(m.results, msg)
345 return m, waitForActivity(m.sub) // re-subscribe
346 }
347 return m, nil
348}
349
350// p.Send approach (from outside the program)
351go func() {
352 p.Send(myMsg{data: "hello"}) // thread-safe
353}()
354```
355
356### Pattern 5: Fullscreen with Alt Screen
357
358Set `AltScreen` in your View. No Program option needed in v2.
359
360```go
361func (m model) View() tea.View {
362 v := tea.NewView(fmt.Sprintf("Fullscreen app! Size: %dx%d", m.width, m.height))
363 v.AltScreen = true
364 return v
365}
366
367func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
368 switch msg := msg.(type) {
369 case tea.WindowSizeMsg:
370 m.width = msg.Width
371 m.height = msg.Height
372 }
373 return m, nil
374}
375```
376
377## Integration Patterns
378
379### Lip Gloss (Styling)
380
381```go
382import "charm.land/lipgloss/v2"
383
384var style = lipgloss.NewStyle().
385 Bold(true).
386 Foreground(lipgloss.Color("205")).
387 Padding(0, 1)
388
389func (m model) View() tea.View {
390 return tea.NewView(style.Render("styled text"))
391}
392```
393
394### Bubbles (Components)
395
396Standard components: `spinner`, `textinput`, `textarea`, `list`, `table`, `viewport`, `paginator`, `timer`, `stopwatch`, `help`, `key`, `filepicker`, `progress`.
397
398Import: `"charm.land/bubbles/v2/<component>"`
399
400Each bubble follows the same Init/Update/View pattern. See Pattern 3 above.
401
402### Huh (Forms)
403
404```go
405import "charm.land/huh/v2"
406
407// Huh can run standalone or embed in a bubbletea program
408form := huh.NewForm(
409 huh.NewGroup(
410 huh.NewInput().Title("Name").Value(&name),
411 ),
412)
413```
414
415## Common Mistakes
416
4171. **Calling a Cmd instead of passing it.** `Init` returns `tea.Cmd`, not `tea.Msg`. Write `return checkServer` not `return checkServer()`. The runtime calls the function.
418
4192. **Forgetting to return a Cmd from tick handlers.** `tea.Tick` and `tea.Every` fire once. You must return a new tick command in your Update handler to keep ticking.
420
4213. **Mutating the model outside Update.** The Elm Architecture requires all state changes go through Update. Don't share model pointers with goroutines. Use `p.Send()` or channel-based Cmds to communicate.
422
4234. **Not handling WindowSizeMsg.** It's sent on startup and every resize. If you do any layout math, store the dimensions and use them in View.
424
4255. **Using fmt.Println for output.** stdout is owned by the TUI. Use `tea.LogToFile()` for debug logging. Use `tea.Println()` / `tea.Printf()` to print above the TUI.
426
4276. **Returning `tea.Batch()` with nil commands.** This is safe - `tea.Batch` filters nil commands - but returning a plain `nil` is cleaner when you have no commands.
428
4297. **AltScreen in v2 is a View property, not a Program option.** Set `v.AltScreen = true` in your View method. Same for mouse mode - set `v.MouseMode` in View.
430
4318. **Blocking in Update.** Update must return quickly. Any I/O (HTTP calls, file reads, sleeps) belongs in a Cmd, not directly in Update.
432
4339. **Not handling ctrl+c.** The terminal is in raw mode - ctrl+c won't kill your app automatically. Always match `"ctrl+c"` in your KeyPressMsg handler and return `tea.Quit` or `tea.Interrupt`.
434
435## Checklist
436
437- [ ] Model implements `Init() tea.Cmd`, `Update(tea.Msg) (tea.Model, tea.Cmd)`, `View() tea.View`
438- [ ] View returns `tea.NewView(s)`, not a raw string
439- [ ] ctrl+c / q handling exists in Update
440- [ ] WindowSizeMsg handled if doing any layout
441- [ ] All I/O in Cmds, never in Update
442- [ ] Tick commands re-dispatched in Update handler
443- [ ] Child component updates collected with `tea.Batch(cmds...)`
444- [ ] No `fmt.Println` - use `tea.LogToFile` for debugging
445- [ ] AltScreen and MouseMode set in View, not as Program options
446
447## Reference
448
449For full message/type/constant listings, see [references/api.md](references/api.md).