The Charm v2 stack is a different library from v1 behind the same names. Three biases decide most calls:
- The module source outranks a remembered API shape. Training data is saturated with v1, and a v1 idiom either fails
to compile against v2 or misbehaves silently. Read
go.mod, then the module itself, before reaching for a signature.
- The View declares the terminal. Every terminal feature is a field set on every render, never a program option and
never a command.
Update is the only place state changes. A command does its work on another goroutine and returns a message;
Update applies it.
Modules
charm.land/<name>/v2 — bubbletea, bubbles, lipgloss, huh, glamour, fang, log, wish.
github.com/charmbracelet/... — ultraviolet, colorprofile, and every x/... package (x/ansi, x/term,
x/editor, x/exp/golden, x/exp/teatest/v2, x/exp/charmtone).
github.com/charmbracelet/bubbletea and its siblings resolve to v1 — never import them. charm.land/x/ansi and
charm.land/ultraviolet do not resolve at all.
- The build error
module declares its path as charm.land/... but was required as github.com/charmbracelet/... names
a stale import. Fix the import, then run go mod tidy.
The Program
View() returns tea.View, a struct. Build it with tea.NewView(content). The interface is Init() Cmd,
Update(Msg) (Model, Cmd), View() View.
- Terminal features are
tea.View fields — AltScreen, MouseMode, Cursor, WindowTitle, ReportFocus,
DisableBracketedPasteMode, BackgroundColor, ForegroundColor, ProgressBar, KeyboardEnhancements, OnMouse.
No tea.WithAltScreen() option and no tea.EnterAltScreen command exists.
- Set every field on every return path of
View(). An early return that omits v.AltScreen = true leaves the
alternate screen for that frame.
- Doc comments inside the v2.0.x source still show the beta signature
Init() (Model, Cmd) — that form does not
compile. Init returns one value.
View() runs after every Update and on the frame ticker. Keep it free of I/O.
- Handle ctrl+c explicitly. Raw mode delivers it as an ordinary key press, and nothing quits the program without a
tea.Quit.
Read [${CLAUDE_SKILL_DIR}/references/bubbletea.md] when a program option, a terminal query, an external process, or a
Kitty keyboard feature is needed — it carries the full View field catalog, the program-option set, every
request/response message pair, the exec and suspend API, and the panic and error contract.
Commands and Messages
- Never mutate the model from a
tea.Cmd. A command runs on its own goroutine — do the I/O there, return a typed
message, and apply it in Update.
- Pass a message-returning function uninvoked:
return m, tea.Quit, never tea.Quit(). The same holds for
tea.Suspend, tea.Interrupt, tea.ClearScreen, tea.RequestWindowSize, and tea.RequestBackgroundColor.
Constructors that take arguments are called: tea.Tick(d, fn), tea.Println(s), tea.ExecProcess(cmd, fn).
tea.Batch runs its commands concurrently with no ordering guarantee; tea.Sequence runs them in order.
tea.Tick and tea.Every fire once. Re-return the command from Update on every tick to keep a timer alive.
Both start their timer when the command is constructed, not when it runs.
- Inject an external event with
p.Send(msg) from any goroutine.
tea.Println and tea.Printf produce nothing visible while AltScreen is set. Debug through
tea.LogToFile(path, prefix).
Input
- Match
tea.KeyPressMsg. tea.KeyMsg is an interface covering presses and releases, so a case tea.KeyMsg runs
the handler twice per keystroke on a Kitty-protocol terminal.
msg.String() names the space bar "space". case " ": compiles and never matches.
Key.Text is empty for enter, tab, the function keys, and every modifier combination. Match on msg.String()
or on msg.Code.
- Declare bindings as
key.Binding values from charm.land/bubbles/v2/key and match with key.Matches(msg, b),
which is generic over fmt.Stringer and accepts a tea.KeyPressMsg directly. A binding's key names must equal the
String() names exactly: "space", "pgup", "ctrl+left".
- Gate every Kitty feature on
tea.KeyboardEnhancementsMsg — tea.KeyReleaseMsg, Key.IsRepeat, and
Key.ShiftedCode arrive only where the terminal reports support.
- Enable the mouse through
v.MouseMode and match the concrete message types — tea.MouseClickMsg,
tea.MouseWheelMsg, tea.MouseMotionMsg. A case tea.MouseMsg placed ahead of them shadows all three.
- Paste arrives as
tea.PasteMsg, never as a key message.
Styling
lipgloss.Style is a value and every setter returns a copy. Build a style once and derive variants by assignment.
- v2 has no
AdaptiveColor and performs no light/dark detection. Return tea.RequestBackgroundColor from Init,
read msg.IsDark() off tea.BackgroundColorMsg, and build every color through lipgloss.LightDark(isDark).
- Hold every style in one semantic struct built from
isDark and thread it to the components. A color literal in
render code is a defect.
lipgloss.Color never reports an error. A malformed string yields NoColor{}, and an integer of 256 or more is
reinterpreted as a 24-bit RGB value — lipgloss.Color("999") renders as RGB(0, 3, 231).
- Measure with
lipgloss.Width and lipgloss.Height; constrain with Style.Width and Style.Height. len() over
styled text counts escape bytes.
- Cut styled text with
ansi.Truncate, ansi.Cut, and ansi.StringWidth from github.com/charmbracelet/x/ansi.
Byte slicing corrupts escape sequences.
- Never reach for the
compat sub-package. Its AdaptiveColor and CompleteColor query the terminal at import
time through package-level variables, which competes with Bubble Tea for stdin and fails over SSH.
Read [${CLAUDE_SKILL_DIR}/references/lipgloss.md] when compositing layers, hit-testing a click, building a gradient,
or rendering a static table, tree, or list — it carries the Style API, the color system, the border set, the layer
compositor, the sub-packages, and the standalone output writers.
Layout
- Subtract the frame before rendering content:
contentW := termW - style.GetHorizontalFrameSize(). A bordered
panel costs two cells per axis.
- Size children from
tea.WindowSizeMsg, which arrives at startup and on every resize. Windows delivers no resize
event — re-query with tea.RequestWindowSize after an event that may have changed the size.
lipgloss.Place never truncates. Content larger than the box makes the call a no-op, so clamp with MaxWidth or
MaxHeight first.
lipgloss.JoinHorizontal pads shorter blocks with unstyled spaces. Set Width and Height on each block first
where the background must stay continuous.
- Size panels as a proportion of the terminal, never as fixed cell counts.
- Truncate explicitly rather than relying on wrapping inside a fixed-size panel. Emoji and some CJK glyphs occupy
one or two cells depending on the terminal, so keep variation-selector emoji out of width-critical chrome.
Components
- Every bubble is a value-type model — reassign the result:
m.table, cmd = m.table.Update(msg). Update returns
the concrete type, so no assertion is needed.
- Never drop the
tea.Cmd a component returns. Focus() on textinput and textarea, SetItems on list,
SetPercent on progress, and Tick on spinner each return the command that starts blinking, filtering, animation, or
ticking.
table.Focus() and table.Blur() return nothing. Focus a table with table.WithFocused(true) or Focus() — an
unfocused table and an unfocused textinput ignore every message, which is what a dead component almost always is.
- Construct through
New(), never as a struct literal. spinner, timer, stopwatch, and progress messages carry an
instance ID, and a zero ID collides.
- Every
New() hardcodes dark styles. Apply DefaultStyles(isDark) after background detection — help, list,
textarea, and textinput take isDark; table.DefaultStyles() and filepicker.DefaultStyles() take no argument.
- Constructor shapes differ.
viewport.New(viewport.WithWidth(w), viewport.WithHeight(h)) takes options and renders
nothing at zero size; list.New(items, delegate, width, height) takes its size positionally.
table.Column.Width is a fixed int. Compute column widths from the terminal width.
Read [${CLAUDE_SKILL_DIR}/references/bubbles.md] when wiring a specific component — it carries the per-component API
for textinput, textarea, viewport, table, list, spinner, progress, timer, stopwatch, filepicker, paginator, help, and
key, plus the real-terminal-cursor wiring.
Forms and Markdown
- Reach for Huh for any prompt or form flow rather than hand-building fields. Bind each result with
.Value(&v),
and read a keyed value only after form.State == huh.StateCompleted.
- Set a theme with
form.WithTheme(huh.ThemeFunc(huh.ThemeCharm)). huh.ThemeCharm(isDark) returns *huh.Styles,
which does not satisfy huh.Theme — huh's own upgrade guide shows that call, and it does not compile.
- Embedding a form takes two adaptations:
form.Update returns (huh.Model, tea.Cmd), so type-assert back to
*huh.Form; form.View() returns a string, so wrap it in tea.NewView.
- A standalone form writes to stderr.
WithProgramOptions replaces the whole option slice and drops that default.
- Wire
form.WithAccessible(...) in every real application. Accessible mode is the only path for screen-reader
users, and TERM=dumb enables it on its own.
huh.ErrUserAborted is the ctrl+c error from form.Run(). Exit 130 on that error.
- Glamour v2 detects nothing. Pass the style name explicitly and derive
"light" or "dark" from
tea.BackgroundColorMsg.IsDark().
- Match
glamour.WithWordWrap(n) to the content width and rebuild the renderer on every tea.WindowSizeMsg. A
renderer does not react to a resize.
glamour.WithStylePath resolves a builtin style name first and falls back to reading the argument as a JSON file,
so a misspelled style name surfaces as a file-read error.
Read [${CLAUDE_SKILL_DIR}/references/forms-and-markdown.md] when building a form or rendering markdown — it carries
every Huh field type with its builders, the dynamic-form binding rules, themes, layouts, accessible mode, and the
Glamour option set.
Architecture
- One screen and a few components — compose in the standard Elm shape. The root model owns the bubbles models,
forwards messages to them, and joins their view strings.
- Multiple screens, dialogs, streaming, or long scrollback — keep exactly one
tea.Model. Sub-components become
plain structs with imperative methods that the root calls: Render(width int) string and mutators returning
tea.Cmd. Never nest models; community tutorials teaching nested-model trees contradict the stack authors' own
production practice.
- One
switch msg.(type) routes everything. Focus state decides key routing, and dialogs draw last and take input
first.
Read [${CLAUDE_SKILL_DIR}/references/architecture.md] when the application outgrows a handful of components — it
carries the single-model contract, hybrid rendering into ultraviolet buffers, the dialog overlay stack, virtualized
lists with render caching, and the async and streaming patterns.
CLI Entry and Logging
fang.Execute(ctx, rootCmd, opts...) wraps a cobra root with styled help and errors, --version, a hidden man
command, completions, and signal handling. It prints the styled error itself — exit on the returned error without
printing it again.
- Never write a log line to the terminal a running TUI owns. Route logs to a file.
log.Fatal calls os.Exit(1), so no deferred cleanup runs and the terminal is never restored. Keep it out of TUI
code paths.
Read [${CLAUDE_SKILL_DIR}/references/cli-and-logging.md] when wiring a CLI entry point, configuring the logger, or
drawing into an ultraviolet buffer — it carries the fang option set and its limits, the log API and its slog bridge, and
the ultraviolet and x/ package inventory.
Testing
- Default to golden View tests. Render the component and compare with
golden.RequireEqual(t, ansi.Strip(m.View()))
from github.com/charmbracelet/x/exp/golden; regenerate with go test -update ./... and commit the result. A bubbles
component's View() returns a string; a tea.Model's returns a tea.View, so pass m.View().Content.
- The golden path derives from
tb.Name(). Use one t.Run subtest per golden file, and expect a rename to orphan
its golden.
- Reserve
github.com/charmbracelet/x/exp/teatest/v2 for flows that need the event loop. The non-/v2 teatest does
not compile against bubbletea v2.
- Pin
tea.WithColorProfile(colorprofile.ASCII) and teatest.WithInitialTermSize(w, h) in every teatest run. The
renderer emits escape sequences into a non-TTY buffer otherwise, which breaks substring assertions.
Read [${CLAUDE_SKILL_DIR}/references/testing.md] when writing the first test for a component or a program — it carries
the golden-test harness, the teatest driving API, and the tmux capture loop for watching a TUI run.
Application
When writing Charm TUI code, apply these conventions silently — do not narrate a rule while following it. Read
go.mod for the pinned versions and confirm an uncertain signature against the module source rather than memory. Where
existing code contradicts a convention, follow the codebase and flag the divergence once.
When reviewing Charm TUI code, cite the violation and show the fix inline. Do not lecture. Treat every v1 idiom as a
defect, including the ones that look right.
Bad: "In v2 the terminal features moved onto the View struct, so the alt screen is..."
Good: tea.NewProgram(m, tea.WithAltScreen()) -> v.AltScreen = true on every View() return
Integration
The golang skill governs every Go decision outside the Charm API — naming, error handling, testing conventions, and
the toolchain — and wins on any question of how the Go code reads. This skill governs the Charm v2 stack. The coding
skill governs workflow. All are active at once.
The lipgloss table, tree, and list sub-packages render static output with no Bubble Tea program — reach for them
before hand-aligning terminal output in a plain CLI.
The terminal belongs to the renderer: state arrives as messages, and the view declares what should be on screen.
1---2name: charm-tui3description: Write and review Charmbracelet v2 terminal UIs in Go: Bubble Tea programs, Bubbles components, Lip Gloss styling and layout, Huh forms, Glamour markdown, fang CLI entry, and TUI testing.4---56The Charm v2 stack is a different library from v1 behind the same names. Three biases decide most calls:78- **The module source outranks a remembered API shape.** Training data is saturated with v1, and a v1 idiom either fails9 to compile against v2 or misbehaves silently. Read `go.mod`, then the module itself, before reaching for a signature.10- **The View declares the terminal.** Every terminal feature is a field set on every render, never a program option and11 never a command.12- **`Update` is the only place state changes.** A command does its work on another goroutine and returns a message;13 `Update` applies it.1415## Modules1617- **`charm.land/<name>/v2`** — bubbletea, bubbles, lipgloss, huh, glamour, fang, log, wish.18- **`github.com/charmbracelet/...`** — ultraviolet, colorprofile, and every `x/...` package (`x/ansi`, `x/term`,19 `x/editor`, `x/exp/golden`, `x/exp/teatest/v2`, `x/exp/charmtone`).20- **`github.com/charmbracelet/bubbletea` and its siblings resolve to v1** — never import them. `charm.land/x/ansi` and21 `charm.land/ultraviolet` do not resolve at all.22- **The build error `module declares its path as charm.land/... but was required as github.com/charmbracelet/...` names23 a stale import.** Fix the import, then run `go mod tidy`.2425## The Program2627- **`View()` returns `tea.View`, a struct.** Build it with `tea.NewView(content)`. The interface is `Init() Cmd`,28 `Update(Msg) (Model, Cmd)`, `View() View`.29- **Terminal features are `tea.View` fields** — `AltScreen`, `MouseMode`, `Cursor`, `WindowTitle`, `ReportFocus`,30 `DisableBracketedPasteMode`, `BackgroundColor`, `ForegroundColor`, `ProgressBar`, `KeyboardEnhancements`, `OnMouse`.31 No `tea.WithAltScreen()` option and no `tea.EnterAltScreen` command exists.32- **Set every field on every return path of `View()`.** An early return that omits `v.AltScreen = true` leaves the33 alternate screen for that frame.34- **Doc comments inside the v2.0.x source still show the beta signature `Init() (Model, Cmd)`** — that form does not35 compile. `Init` returns one value.36- **`View()` runs after every `Update` and on the frame ticker.** Keep it free of I/O.37- **Handle ctrl+c explicitly.** Raw mode delivers it as an ordinary key press, and nothing quits the program without a38 `tea.Quit`.3940Read [`${CLAUDE_SKILL_DIR}/references/bubbletea.md`] when a program option, a terminal query, an external process, or a41Kitty keyboard feature is needed — it carries the full View field catalog, the program-option set, every42request/response message pair, the exec and suspend API, and the panic and error contract.4344## Commands and Messages4546- **Never mutate the model from a `tea.Cmd`.** A command runs on its own goroutine — do the I/O there, return a typed47 message, and apply it in `Update`.48- **Pass a message-returning function uninvoked**: `return m, tea.Quit`, never `tea.Quit()`. The same holds for49 `tea.Suspend`, `tea.Interrupt`, `tea.ClearScreen`, `tea.RequestWindowSize`, and `tea.RequestBackgroundColor`.50 Constructors that take arguments are called: `tea.Tick(d, fn)`, `tea.Println(s)`, `tea.ExecProcess(cmd, fn)`.51- **`tea.Batch` runs its commands concurrently with no ordering guarantee; `tea.Sequence` runs them in order.**52- **`tea.Tick` and `tea.Every` fire once.** Re-return the command from `Update` on every tick to keep a timer alive.53 Both start their timer when the command is constructed, not when it runs.54- **Inject an external event with `p.Send(msg)`** from any goroutine.55- **`tea.Println` and `tea.Printf` produce nothing visible while `AltScreen` is set.** Debug through56 `tea.LogToFile(path, prefix)`.5758## Input5960- **Match `tea.KeyPressMsg`.** `tea.KeyMsg` is an interface covering presses and releases, so a `case tea.KeyMsg` runs61 the handler twice per keystroke on a Kitty-protocol terminal.62- **`msg.String()` names the space bar `"space"`.** `case " ":` compiles and never matches.63- **`Key.Text` is empty for `enter`, `tab`, the function keys, and every modifier combination.** Match on `msg.String()`64 or on `msg.Code`.65- **Declare bindings as `key.Binding` values** from `charm.land/bubbles/v2/key` and match with `key.Matches(msg, b)`,66 which is generic over `fmt.Stringer` and accepts a `tea.KeyPressMsg` directly. A binding's key names must equal the67 `String()` names exactly: `"space"`, `"pgup"`, `"ctrl+left"`.68- **Gate every Kitty feature on `tea.KeyboardEnhancementsMsg`** — `tea.KeyReleaseMsg`, `Key.IsRepeat`, and69 `Key.ShiftedCode` arrive only where the terminal reports support.70- **Enable the mouse through `v.MouseMode` and match the concrete message types** — `tea.MouseClickMsg`,71 `tea.MouseWheelMsg`, `tea.MouseMotionMsg`. A `case tea.MouseMsg` placed ahead of them shadows all three.72- **Paste arrives as `tea.PasteMsg`**, never as a key message.7374## Styling7576- **`lipgloss.Style` is a value and every setter returns a copy.** Build a style once and derive variants by assignment.77- **v2 has no `AdaptiveColor` and performs no light/dark detection.** Return `tea.RequestBackgroundColor` from `Init`,78 read `msg.IsDark()` off `tea.BackgroundColorMsg`, and build every color through `lipgloss.LightDark(isDark)`.79- **Hold every style in one semantic struct built from `isDark`** and thread it to the components. A color literal in80 render code is a defect.81- **`lipgloss.Color` never reports an error.** A malformed string yields `NoColor{}`, and an integer of 256 or more is82 reinterpreted as a 24-bit RGB value — `lipgloss.Color("999")` renders as RGB(0, 3, 231).83- **Measure with `lipgloss.Width` and `lipgloss.Height`; constrain with `Style.Width` and `Style.Height`.** `len()` over84 styled text counts escape bytes.85- **Cut styled text with `ansi.Truncate`, `ansi.Cut`, and `ansi.StringWidth`** from `github.com/charmbracelet/x/ansi`.86 Byte slicing corrupts escape sequences.87- **Never reach for the `compat` sub-package.** Its `AdaptiveColor` and `CompleteColor` query the terminal at import88 time through package-level variables, which competes with Bubble Tea for stdin and fails over SSH.8990Read [`${CLAUDE_SKILL_DIR}/references/lipgloss.md`] when compositing layers, hit-testing a click, building a gradient,91or rendering a static table, tree, or list — it carries the Style API, the color system, the border set, the layer92compositor, the sub-packages, and the standalone output writers.9394## Layout9596- **Subtract the frame before rendering content**: `contentW := termW - style.GetHorizontalFrameSize()`. A bordered97 panel costs two cells per axis.98- **Size children from `tea.WindowSizeMsg`**, which arrives at startup and on every resize. Windows delivers no resize99 event — re-query with `tea.RequestWindowSize` after an event that may have changed the size.100- **`lipgloss.Place` never truncates.** Content larger than the box makes the call a no-op, so clamp with `MaxWidth` or101 `MaxHeight` first.102- **`lipgloss.JoinHorizontal` pads shorter blocks with unstyled spaces.** Set `Width` and `Height` on each block first103 where the background must stay continuous.104- **Size panels as a proportion of the terminal**, never as fixed cell counts.105- **Truncate explicitly rather than relying on wrapping inside a fixed-size panel.** Emoji and some CJK glyphs occupy106 one or two cells depending on the terminal, so keep variation-selector emoji out of width-critical chrome.107108## Components109110- **Every bubble is a value-type model — reassign the result**: `m.table, cmd = m.table.Update(msg)`. `Update` returns111 the concrete type, so no assertion is needed.112- **Never drop the `tea.Cmd` a component returns.** `Focus()` on textinput and textarea, `SetItems` on list,113 `SetPercent` on progress, and `Tick` on spinner each return the command that starts blinking, filtering, animation, or114 ticking.115- **`table.Focus()` and `table.Blur()` return nothing.** Focus a table with `table.WithFocused(true)` or `Focus()` — an116 unfocused table and an unfocused textinput ignore every message, which is what a dead component almost always is.117- **Construct through `New()`, never as a struct literal.** spinner, timer, stopwatch, and progress messages carry an118 instance ID, and a zero ID collides.119- **Every `New()` hardcodes dark styles.** Apply `DefaultStyles(isDark)` after background detection — help, list,120 textarea, and textinput take `isDark`; `table.DefaultStyles()` and `filepicker.DefaultStyles()` take no argument.121- **Constructor shapes differ.** `viewport.New(viewport.WithWidth(w), viewport.WithHeight(h))` takes options and renders122 nothing at zero size; `list.New(items, delegate, width, height)` takes its size positionally.123- **`table.Column.Width` is a fixed int.** Compute column widths from the terminal width.124125Read [`${CLAUDE_SKILL_DIR}/references/bubbles.md`] when wiring a specific component — it carries the per-component API126for textinput, textarea, viewport, table, list, spinner, progress, timer, stopwatch, filepicker, paginator, help, and127key, plus the real-terminal-cursor wiring.128129## Forms and Markdown130131- **Reach for Huh for any prompt or form flow** rather than hand-building fields. Bind each result with `.Value(&v)`,132 and read a keyed value only after `form.State == huh.StateCompleted`.133- **Set a theme with `form.WithTheme(huh.ThemeFunc(huh.ThemeCharm))`.** `huh.ThemeCharm(isDark)` returns `*huh.Styles`,134 which does not satisfy `huh.Theme` — huh's own upgrade guide shows that call, and it does not compile.135- **Embedding a form takes two adaptations**: `form.Update` returns `(huh.Model, tea.Cmd)`, so type-assert back to136 `*huh.Form`; `form.View()` returns a `string`, so wrap it in `tea.NewView`.137- **A standalone form writes to stderr.** `WithProgramOptions` replaces the whole option slice and drops that default.138- **Wire `form.WithAccessible(...)` in every real application.** Accessible mode is the only path for screen-reader139 users, and `TERM=dumb` enables it on its own.140- **`huh.ErrUserAborted` is the ctrl+c error from `form.Run()`.** Exit 130 on that error.141- **Glamour v2 detects nothing.** Pass the style name explicitly and derive `"light"` or `"dark"` from142 `tea.BackgroundColorMsg.IsDark()`.143- **Match `glamour.WithWordWrap(n)` to the content width and rebuild the renderer on every `tea.WindowSizeMsg`.** A144 renderer does not react to a resize.145- **`glamour.WithStylePath` resolves a builtin style name first and falls back to reading the argument as a JSON file**,146 so a misspelled style name surfaces as a file-read error.147148Read [`${CLAUDE_SKILL_DIR}/references/forms-and-markdown.md`] when building a form or rendering markdown — it carries149every Huh field type with its builders, the dynamic-form binding rules, themes, layouts, accessible mode, and the150Glamour option set.151152## Architecture153154- **One screen and a few components — compose in the standard Elm shape.** The root model owns the bubbles models,155 forwards messages to them, and joins their view strings.156- **Multiple screens, dialogs, streaming, or long scrollback — keep exactly one `tea.Model`.** Sub-components become157 plain structs with imperative methods that the root calls: `Render(width int) string` and mutators returning158 `tea.Cmd`. Never nest models; community tutorials teaching nested-model trees contradict the stack authors' own159 production practice.160- **One `switch msg.(type)` routes everything.** Focus state decides key routing, and dialogs draw last and take input161 first.162163Read [`${CLAUDE_SKILL_DIR}/references/architecture.md`] when the application outgrows a handful of components — it164carries the single-model contract, hybrid rendering into ultraviolet buffers, the dialog overlay stack, virtualized165lists with render caching, and the async and streaming patterns.166167## CLI Entry and Logging168169- **`fang.Execute(ctx, rootCmd, opts...)` wraps a cobra root** with styled help and errors, `--version`, a hidden `man`170 command, completions, and signal handling. It prints the styled error itself — exit on the returned error without171 printing it again.172- **Never write a log line to the terminal a running TUI owns.** Route logs to a file.173- **`log.Fatal` calls `os.Exit(1)`**, so no deferred cleanup runs and the terminal is never restored. Keep it out of TUI174 code paths.175176Read [`${CLAUDE_SKILL_DIR}/references/cli-and-logging.md`] when wiring a CLI entry point, configuring the logger, or177drawing into an ultraviolet buffer — it carries the fang option set and its limits, the log API and its slog bridge, and178the ultraviolet and `x/` package inventory.179180## Testing181182- **Default to golden View tests.** Render the component and compare with `golden.RequireEqual(t, ansi.Strip(m.View()))`183 from `github.com/charmbracelet/x/exp/golden`; regenerate with `go test -update ./...` and commit the result. A bubbles184 component's `View()` returns a `string`; a `tea.Model`'s returns a `tea.View`, so pass `m.View().Content`.185- **The golden path derives from `tb.Name()`.** Use one `t.Run` subtest per golden file, and expect a rename to orphan186 its golden.187- **Reserve `github.com/charmbracelet/x/exp/teatest/v2` for flows that need the event loop.** The non-`/v2` teatest does188 not compile against bubbletea v2.189- **Pin `tea.WithColorProfile(colorprofile.ASCII)` and `teatest.WithInitialTermSize(w, h)` in every teatest run.** The190 renderer emits escape sequences into a non-TTY buffer otherwise, which breaks substring assertions.191192Read [`${CLAUDE_SKILL_DIR}/references/testing.md`] when writing the first test for a component or a program — it carries193the golden-test harness, the teatest driving API, and the tmux capture loop for watching a TUI run.194195## Application196197When **writing** Charm TUI code, apply these conventions silently — do not narrate a rule while following it. Read198`go.mod` for the pinned versions and confirm an uncertain signature against the module source rather than memory. Where199existing code contradicts a convention, follow the codebase and flag the divergence once.200201When **reviewing** Charm TUI code, cite the violation and show the fix inline. Do not lecture. Treat every v1 idiom as a202defect, including the ones that look right.203204```205Bad: "In v2 the terminal features moved onto the View struct, so the alt screen is..."206Good: tea.NewProgram(m, tea.WithAltScreen()) -> v.AltScreen = true on every View() return207```208209## Integration210211The **golang** skill governs every Go decision outside the Charm API — naming, error handling, testing conventions, and212the toolchain — and wins on any question of how the Go code reads. This skill governs the Charm v2 stack. The **coding**213skill governs workflow. All are active at once.214215The lipgloss `table`, `tree`, and `list` sub-packages render static output with no Bubble Tea program — reach for them216before hand-aligning terminal output in a plain CLI.217218**The terminal belongs to the renderer: state arrives as messages, and the view declares what should be on screen.**