bubbletea-patterns
Skill3 starsChanged 8 months agoMITView on GitHub
BubbleTea v2 Elm architecture patterns for Go. Use when creating or editing tea.Model implementations, handling messages, composing components, managing state transitions, or working with commands and subscriptions. Triggers on tea.Model, Update, View, Init, tea.Cmd, tea.Msg usage.
1 other project uses a version of this.
Adopted by
Most recently taken up by
- masonhuemmer/dopsword for word, first seen Mar 24, 2026
Other versions1 project uses a version of this.
Word for word · 1
- masonhuemmer/dops★ 3100% same
Edited · 0
None found.
Wrote their own version · 0
None found.
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository19 other files it gives its agents.
- CLAUDE.mdCLAUDE.md
- charm-stylingSkill
- clean-code-boundariesSkill
- clean-code-designSkill
- clean-code-errorsSkill
- clean-code-functionsSkill
- clean-code-namingSkill
- clean-code-solidSkill
- clean-code-testingSkill
- cli-structureSkill
- create-runbookSkill
- demo-recordingSkill
- mutation-testingSkill
- shell-scriptsSkill
- spec-workflowSkill
- tdd-workflowSkill
- tui-layoutSkill
- tui-testingSkill
- wizard-formsSkill
The file
We split this skill into the rules it gives an agent
10 rules. About 0 are common, 3 are distinctive, and 7 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈3
Distinctive · estimate
This author's own choices. Worth a look.
7
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 124
Use an enum to track which view/state is active:
- Distinctive: line 179
When the app exits, BubbleTea automatically restores the original terminal buffer
- Very specific: line 183
Never render layout-dependent views before knowing the terminal size.
- Very specific: line 217
Do NOT return a pointer to model — return the value: return m, cmd
- Distinctive: line 218
Do NOT modify model fields outside of Update — the Elm architecture is message-driven
- Very specific: line 219
Do NOT block in Update — return a Cmd for async work
- Very specific: line 220
Do NOT render layouts before WindowSizeMsg — use a ready flag
- Very specific: line 221
Do NOT use tea.WithAltScreen() option — set view.AltScreen = true in View()
- Very specific: line 222
Do NOT use case " ": for space — use case "space": in v2
- Very specific: line 223
Do NOT use tea.KeyMsg for key presses — use tea.KeyPressMsg in v2
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
BubbleTea v2 Patterns
Import: charm.land/bubbletea/v2
Core Model Interface
type Model interface {
Init() tea.Cmd
Update(tea.Msg) (tea.Model, tea.Cmd)
View() tea.View // v2 returns tea.View, NOT string
}Init()returns the initial command (ornilfor none)Update()handles messages, returns updated model + next commandView()returnstea.View(usetea.NewView(s)to wrap a string)
tea.View (Declarative — v2 Change)
Terminal features are now declared in the View struct, not via commands or program options:
func (m model) View() tea.View {
v := tea.NewView(m.render())
v.AltScreen = true // was tea.WithAltScreen() option
v.MouseMode = tea.MouseModeCellMotion // was tea.WithMouseCellMotion() option
v.ReportFocus = true // was tea.WithReportFocus() option
v.WindowTitle = "My App" // was tea.SetWindowTitle() cmd
v.Cursor = tea.NewCursor(m.cursorX, m.cursorY) // was tea.ShowCursor/HideCursor cmd
return v
}These can change dynamically per render — no need for imperative toggle commands.
Messages and Commands
type Msg = uv.Event // still effectively any/interface{}
type Cmd func() Msg // a function that returns a messageKey commands:
tea.Quit— returnsQuitMsg{}tea.Batch(cmds ...Cmd)— run commands concurrentlytea.Sequence(cmds ...Cmd)— run commands sequentially (wastea.Sequentiallyin v1)tea.Every(d, fn)— tick synced with system clocktea.Tick(d, fn)— independent tick
Key Events (v2 Change)
// v2 uses KeyPressMsg, not KeyMsg
case tea.KeyPressMsg:
switch msg.String() {
case "q", "ctrl+c":
return m, tea.Quit
case "space": // was " " in v1
m.toggle()
}Key fields: msg.Code (rune), msg.Text (string), msg.Mod (KeyMod modifiers).
Mouse Events (v2 Change)
v2 splits mouse into specific types:
tea.MouseClickMsg— button presstea.MouseReleaseMsg— button releasetea.MouseWheelMsg— scrolltea.MouseMotionMsg— movement
Each has .X, .Y fields. MouseClickMsg/ReleaseMsg have .Button (e.g., tea.MouseLeft).
Startup Messages
These are sent automatically when the program starts:
tea.WindowSizeMsg{Width, Height int}— initial size + every resizetea.ColorProfileMsg— terminal color capabilitytea.EnvMsg— environment variables
Component Composition
Embed child models as struct fields. Delegate Update and View:
type model struct {
list list.Model
input textinput.Model
state viewState
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmds []tea.Cmd
// Broadcast messages all children need (e.g., WindowSizeMsg)
// Route input to focused child only
switch m.state {
case listView:
var cmd tea.Cmd
m.list, cmd = m.list.Update(msg)
cmds = append(cmds, cmd)
case inputView:
var cmd tea.Cmd
m.input, cmd = m.input.Update(msg)
cmds = append(cmds, cmd)
}
return m, tea.Batch(cmds...)
}Child components (from charm.land/bubbles/v2) return string from View(), not tea.View. Compose them inside the parent's tea.NewView().
State Machine Pattern
Use an enum to track which view/state is active:
type viewState int
const (
listView viewState = iota
formView
confirmView
)
type model struct {
state viewState
// ... child models
}Route messages and render based on state. This keeps Update clean and predictable.
Program Setup
p := tea.NewProgram(initialModel,
tea.WithContext(ctx), // cancellable
tea.WithFilter(filterFn), // suppress/transform messages
)
m, err := p.Run() // blockingUseful options:
tea.WithInput(r)/tea.WithOutput(w)— custom I/O (testing)tea.WithWindowSize(w, h)— set initial size (testing)tea.WithColorProfile(p)— force color profile (testing)tea.WithFPS(fps)— max render FPS (default 60)
External interaction:
p.Send(msg)— inject messages from outsidep.Quit()— graceful shutdownp.Println(args...)— print above the TUI
Alt Screen
In v2, alt screen is declarative only — set v.AltScreen = true on the tea.View returned from View(). There is no tea.WithAltScreen() program option in v2.
func (m model) View() tea.View {
v := tea.NewView(content)
v.AltScreen = true
return v
}Best practices:
- Alt screen can be toggled dynamically per render (e.g.,
v.AltScreen = m.fullscreen) p.Println/p.Printfare silently discarded when alt screen is active — don't use them- When the app exits, BubbleTea automatically restores the original terminal buffer
Wait for WindowSizeMsg Before Rendering
Never render layout-dependent views before knowing the terminal size. View() may be called before WindowSizeMsg arrives. If you render layout with default dimensions (80x24), it will overflow the real terminal when alt screen activates, causing clipping.
Pattern: Initialize width/height to 0 and guard in View():
type model struct {
width int // 0 until WindowSizeMsg
height int // 0 until WindowSizeMsg
}
func (m model) View() tea.View {
v := tea.View{AltScreen: true}
if m.width == 0 || m.height == 0 {
return v // empty until we know the size
}
v.Content = m.renderLayout()
return v
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
m.width = msg.Width
m.height = msg.Height
// Safe to recalculate layout now
}
// ...
}This ensures all layout calculations use real terminal dimensions.
Common Mistakes to Avoid
- Do NOT return a pointer to model — return the value:
return m, cmd - Do NOT modify model fields outside of Update — the Elm architecture is message-driven
- Do NOT block in Update — return a
Cmdfor async work - Do NOT render layouts before
WindowSizeMsg— use areadyflag - Do NOT use
tea.WithAltScreen()option — setview.AltScreen = truein View() - Do NOT use
case " ":for space — usecase "space":in v2 - Do NOT use
tea.KeyMsgfor key presses — usetea.KeyPressMsgin v2
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

