Building Glamorous TUIs with Charmbracelet
Quick Router — Start Here
| I need to... |
Use |
Reference |
| Add prompts/spinners to a shell script |
Gum (no Go) |
Shell Scripts |
| Build a Go TUI |
Bubble Tea + Lip Gloss |
Go TUI |
| Build a production-grade Go TUI |
Above + elite patterns |
Production Architecture |
| Serve a TUI over SSH |
Wish + Bubble Tea |
Infrastructure |
| Record a terminal demo |
VHS |
Shell Scripts |
| Find a Bubbles component |
list, table, viewport, spinner, progress... |
Component Catalog |
| Get a copy-paste pattern |
Layouts, forms, animation, testing |
Quick Reference / Advanced Patterns |
Decision Guide
Is it a shell script?
├─ Yes → Use Gum
│ Need recording? → VHS
│ Need AI? → Mods
│
└─ No (Go application)
│
├─ Just styled output? → Lip Gloss only
├─ Simple prompts/forms? → Huh standalone
├─ Full interactive TUI? → Bubble Tea + Bubbles + Lip Gloss
│ │
│ └─ Production-grade? → Also add elite patterns:
│ (multi-view, data- two-phase async, immutable snapshots,
│ dense, must be adaptive layout, focus state machine,
│ fast & polished) semantic theming, pre-computed styles
│ → See Production Architecture reference
│
└─ Need SSH access? → Wish + Bubble Tea
Shell Scripts (No Go Required)
brew install gum # One-time install
# Input
NAME=$(gum input --placeholder "Your name")
# Selection
COLOR=$(gum choose "red" "green" "blue")
# Fuzzy filter from stdin
BRANCH=$(git branch | gum filter)
# Confirmation
gum confirm "Continue?" && echo "yes"
# Spinner
gum spin --title "Working..." -- long-command
# Styled output
gum style --border rounded --padding "1 2" "Hello"
Full Gum Reference →
VHS Recording →
Mods AI →
Go Applications
go get github.com/charmbracelet/bubbletea github.com/charmbracelet/lipgloss
Minimal TUI (Copy & Run)
package main
import (
"fmt"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
var highlight = lipgloss.NewStyle().Foreground(lipgloss.Color("212")).Bold(true)
type model struct {
items []string
cursor 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.KeyMsg:
switch msg.String() {
case "q", "ctrl+c":
return m, tea.Quit
case "up", "k":
if m.cursor > 0 { m.cursor-- }
case "down", "j":
if m.cursor < len(m.items)-1 { m.cursor++ }
case "enter":
fmt.Printf("Selected: %s\n", m.items[m.cursor])
return m, tea.Quit
}
}
return m, nil
}
func (m model) View() string {
s := ""
for i, item := range m.items {
if i == m.cursor {
s += highlight.Render("▸ "+item) + "\n"
} else {
s += " " + item + "\n"
}
}
return s + "\n(↑/↓ move, enter select, q quit)"
}
func main() {
m := model{items: []string{"Option A", "Option B", "Option C"}}
tea.NewProgram(m).Run()
}
Library Cheat Sheet
| Need |
Library |
Example |
| TUI framework |
bubbletea |
tea.NewProgram(model).Run() |
| Components |
bubbles |
list.New(), textinput.New() |
| Styling |
lipgloss |
style.Foreground(lipgloss.Color("212")) |
| Forms |
huh |
huh.NewInput().Title("Name").Run() |
| Markdown |
glamour |
glamour.Render(md, "dark") |
| Animation |
harmonica |
harmonica.NewSpring() |
Full Go TUI Guide →
All Bubbles Components →
Layout & Animation Patterns →
SSH Apps (Infrastructure)
s, _ := wish.NewServer(
wish.WithAddress(":2222"),
wish.WithHostKeyPath(".ssh/key"),
wish.WithMiddleware(
bubbletea.Middleware(handler),
logging.Middleware(),
),
)
s.ListenAndServe()
Connect: ssh localhost -p 2222
Full Infrastructure Guide →
Production TUI Architecture (Elite Patterns)
Beyond basic Bubble Tea: patterns that make TUIs feel fast, polished, and professional.
Each links to a full code example in Production Architecture.
My TUI is slow or janky
| Symptom |
Pattern |
Fix |
| UI blocks during computation |
Two-Phase Async |
Phase 1 instant, Phase 2 background goroutine |
| Render path holds mutex |
Immutable Snapshots |
Pre-build snapshot, atomic pointer swap |
| File changes cause stutter |
Background Worker |
Debounced watcher + coalescing |
| Thousands of allocs per frame |
Pre-Computed Styles |
Allocate delegate styles once at startup |
| O(n²) string concat in View() |
strings.Builder |
Pre-allocated Builder with Grow() |
| Glamour re-renders every frame |
Cached Markdown |
Cache by content hash, invalidate on width change |
| GC pauses during interaction |
Idle-Time GC |
Trigger GC during idle periods |
| Large dataset = high memory |
Object Pooling |
sync.Pool with pre-allocated slices |
| Rendering off-screen items |
Viewport Virtualization |
Only render visible rows |
My layout breaks on different terminals
| Symptom |
Pattern |
Fix |
| Hardcoded widths break |
Adaptive Layout |
3-4 responsive breakpoints (80/100/140/180 cols) |
| Colors wrong on light terminals |
Semantic Theming |
lipgloss.AdaptiveColor + WCAG AA contrast |
| Items have equal priority → list shuffles |
Deterministic Sorting |
Stable sort with tie-breaking secondary key |
| Sort mode not visible |
Dynamic Status Bar |
Left/right segments with gap-fill |
My TUI has multiple views and it's getting messy
| Symptom |
Pattern |
Fix |
| Key routing chaos |
Focus State Machine |
Explicit focus enum + modal priority layer |
| User gets lost in nested views |
Breadcrumb Navigation |
Home > Board > Priority path indicator |
| Overlay dismiss loses position |
Focus Restoration |
Save focus before overlay, restore on dismiss |
| Old async results overwrite new data |
Stale Message Detection |
Compare data hash before applying results |
| Multiple component updates per frame |
tea.Batch Accumulation |
Collect cmds in slice, return tea.Batch(cmds...) |
| Background goroutine panic kills TUI |
Error Recovery |
defer/recover wrapper for all goroutines |
I want to add data-rich visualizations
| Want |
Pattern |
Code |
| Bar charts in list columns |
Unicode Sparklines |
▇▅▂ using 8-level block characters |
| Color-by-intensity |
Perceptual Heatmaps |
gray → blue → purple → pink gradient |
| Dependency graph in terminal |
ASCII Graph Renderer |
Canvas + Manhattan routing (╭─╮│╰╯) |
| Age at a glance |
Age Color Coding |
Fresh=green, aging=yellow, stale=red |
| Borders that mean something |
Semantic Borders |
Red=blocked, green=ready, yellow=high-impact |
I want my TUI to feel polished and professional
| Want |
Pattern |
Key Idea |
Vim-style gg/G |
Vim Key Combos |
Track waitingForG state between keystrokes |
| Search without jank |
Debounced Search |
150ms timer, fire only when typing stops |
| Search across all fields at once |
Composite FilterValue |
Flatten all fields into one string |
| 4-line cards with metadata |
Rich Delegates |
Custom delegate with Height()=4 |
| Expand detail inline |
Inline Expansion |
Toggle with d, auto-collapse on j/k |
| Copy to clipboard |
Clipboard Integration |
y for ID, C for markdown + toast feedback |
? / ` / ; help |
Multi-Tier Help |
Quick ref + tutorial + persistent sidebar |
| Kanban with mode switching |
Kanban Swimlanes |
Pre-computed board states, O(1) switch |
| Collapsible tree with h/l |
Tree Navigation |
Flatten tree to visible list for j/k nav |
| Suspend TUI for vim edit |
Editor Dispatch |
tea.ExecProcess for terminal, background for GUI |
| Remember expand/collapse |
Persistent State |
Save to JSON, graceful degradation on corrupt |
| Tune via env vars |
Env Preferences |
NO_COLOR, theme, debounce, split ratio |
| Optional feature missing? |
Graceful Degradation |
Detect at startup, hide unavailable features |
Full Production Architecture Guide →
Pre-Flight Checklist (Every TUI)
For production TUIs, see the full checklist (16 must-have + 20 polish items).
When NOT to Use Charm
- Output is piped:
mytool | grep → plain text
- CI/CD: No terminal → use flags/env vars
- One simple prompt: Maybe
fmt.Scanf is fine
Escape hatch:
if !term.IsTerminal(os.Stdin.Fd()) || os.Getenv("NO_TUI") != "" {
runPlainMode()
return
}
All References
| I need... |
Read this |
| Copy-paste one-liners |
Quick Reference |
| Prompts to give Claude for TUI tasks |
Prompts |
| Gum / VHS / Mods / Freeze / Glow |
Shell Scripts |
| Bubble Tea architecture, debugging, anti-patterns |
Go TUI |
| Bubbles component APIs (list, table, viewport...) |
Component Catalog |
| Theming, layouts, animation, Huh forms, testing |
Advanced Patterns |
| Elite patterns: async, snapshots, focus machines, adaptive layout, sparklines, kanban, trees, caching |
Production Architecture |
| Wish SSH server, Soft Serve, teatest |
Infrastructure |
1---2name: tui-glamorous3description: Build terminal UIs with Charmbracelet (Bubble Tea, Lip Gloss, Gum). Use when: Go TUI, shell prompts/spinners, "make CLI prettier", adaptive layouts, async rendering, focus state machines, sparklines, heatmaps, kanban boards, SSH apps.4---56# Building Glamorous TUIs with Charmbracelet78## Quick Router — Start Here910| I need to... | Use | Reference |11|--------------|-----|-----------|12| **Add prompts/spinners to a shell script** | Gum (no Go) | [Shell Scripts](references/shell-scripts.md) |13| **Build a Go TUI** | Bubble Tea + Lip Gloss | [Go TUI](references/go-tui.md) |14| **Build a production-grade Go TUI** | Above + elite patterns | [Production Architecture](references/production-architecture.md) |15| **Serve a TUI over SSH** | Wish + Bubble Tea | [Infrastructure](references/infrastructure.md) |16| **Record a terminal demo** | VHS | [Shell Scripts](references/shell-scripts.md#vhs-terminal-recording) |17| **Find a Bubbles component** | list, table, viewport, spinner, progress... | [Component Catalog](references/component-catalog.md) |18| **Get a copy-paste pattern** | Layouts, forms, animation, testing | [Quick Reference](references/QUICK-REFERENCE.md) / [Advanced Patterns](references/advanced-patterns.md) |1920---2122## Decision Guide2324```25Is it a shell script?26├─ Yes → Use Gum27│ Need recording? → VHS28│ Need AI? → Mods29│30└─ No (Go application)31 │32 ├─ Just styled output? → Lip Gloss only33 ├─ Simple prompts/forms? → Huh standalone34 ├─ Full interactive TUI? → Bubble Tea + Bubbles + Lip Gloss35 │ │36 │ └─ Production-grade? → Also add elite patterns:37 │ (multi-view, data- two-phase async, immutable snapshots,38 │ dense, must be adaptive layout, focus state machine,39 │ fast & polished) semantic theming, pre-computed styles40 │ → See Production Architecture reference41 │42 └─ Need SSH access? → Wish + Bubble Tea43```4445---4647## Shell Scripts (No Go Required)4849```bash50brew install gum # One-time install51```5253```bash54# Input55NAME=$(gum input --placeholder "Your name")5657# Selection58COLOR=$(gum choose "red" "green" "blue")5960# Fuzzy filter from stdin61BRANCH=$(git branch | gum filter)6263# Confirmation64gum confirm "Continue?" && echo "yes"6566# Spinner67gum spin --title "Working..." -- long-command6869# Styled output70gum style --border rounded --padding "1 2" "Hello"71```7273**[Full Gum Reference →](references/shell-scripts.md#gum-the-essential-tool)**74**[VHS Recording →](references/shell-scripts.md#vhs-terminal-recording)**75**[Mods AI →](references/shell-scripts.md#mods-ai-in-terminal)**7677---7879## Go Applications8081```bash82go get github.com/charmbracelet/bubbletea github.com/charmbracelet/lipgloss83```8485### Minimal TUI (Copy & Run)8687```go88package main8990import (91 "fmt"92 tea "github.com/charmbracelet/bubbletea"93 "github.com/charmbracelet/lipgloss"94)9596var highlight = lipgloss.NewStyle().Foreground(lipgloss.Color("212")).Bold(true)9798type model struct {99 items []string100 cursor int101}102103func (m model) Init() tea.Cmd { return nil }104105func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {106 switch msg := msg.(type) {107 case tea.KeyMsg:108 switch msg.String() {109 case "q", "ctrl+c":110 return m, tea.Quit111 case "up", "k":112 if m.cursor > 0 { m.cursor-- }113 case "down", "j":114 if m.cursor < len(m.items)-1 { m.cursor++ }115 case "enter":116 fmt.Printf("Selected: %s\n", m.items[m.cursor])117 return m, tea.Quit118 }119 }120 return m, nil121}122123func (m model) View() string {124 s := ""125 for i, item := range m.items {126 if i == m.cursor {127 s += highlight.Render("▸ "+item) + "\n"128 } else {129 s += " " + item + "\n"130 }131 }132 return s + "\n(↑/↓ move, enter select, q quit)"133}134135func main() {136 m := model{items: []string{"Option A", "Option B", "Option C"}}137 tea.NewProgram(m).Run()138}139```140141### Library Cheat Sheet142143| Need | Library | Example |144|------|---------|---------|145| TUI framework | `bubbletea` | `tea.NewProgram(model).Run()` |146| Components | `bubbles` | `list.New()`, `textinput.New()` |147| Styling | `lipgloss` | `style.Foreground(lipgloss.Color("212"))` |148| Forms | `huh` | `huh.NewInput().Title("Name").Run()` |149| Markdown | `glamour` | `glamour.Render(md, "dark")` |150| Animation | `harmonica` | `harmonica.NewSpring()` |151152**[Full Go TUI Guide →](references/go-tui.md)**153**[All Bubbles Components →](references/component-catalog.md)**154**[Layout & Animation Patterns →](references/advanced-patterns.md)**155156---157158## SSH Apps (Infrastructure)159160```go161s, _ := wish.NewServer(162 wish.WithAddress(":2222"),163 wish.WithHostKeyPath(".ssh/key"),164 wish.WithMiddleware(165 bubbletea.Middleware(handler),166 logging.Middleware(),167 ),168)169s.ListenAndServe()170```171172Connect: `ssh localhost -p 2222`173174**[Full Infrastructure Guide →](references/infrastructure.md)**175176---177178## Production TUI Architecture (Elite Patterns)179180Beyond basic Bubble Tea: patterns that make TUIs feel fast, polished, and professional.181Each links to a full code example in [Production Architecture](references/production-architecture.md).182183### My TUI is slow or janky184185| Symptom | Pattern | Fix |186|---------|---------|-----|187| UI blocks during computation | [Two-Phase Async](references/production-architecture.md#two-phase-async-architecture) | Phase 1 instant, Phase 2 background goroutine |188| Render path holds mutex | [Immutable Snapshots](references/production-architecture.md#immutable-snapshot-pattern) | Pre-build snapshot, atomic pointer swap |189| File changes cause stutter | [Background Worker](references/production-architecture.md#background-worker-with-file-watching) | Debounced watcher + coalescing |190| Thousands of allocs per frame | [Pre-Computed Styles](references/production-architecture.md#pre-computed-styles-for-performance) | Allocate delegate styles once at startup |191| O(n²) string concat in View() | [strings.Builder](references/production-architecture.md#stringsbuilder-in-view) | Pre-allocated Builder with Grow() |192| Glamour re-renders every frame | [Cached Markdown](references/production-architecture.md#cached-markdown-rendering) | Cache by content hash, invalidate on width change |193| GC pauses during interaction | [Idle-Time GC](references/production-architecture.md#idle-time-gc-management) | Trigger GC during idle periods |194| Large dataset = high memory | [Object Pooling](references/production-architecture.md#object-pooling--memory-efficiency) | sync.Pool with pre-allocated slices |195| Rendering off-screen items | [Viewport Virtualization](references/production-architecture.md#viewport-virtualization) | Only render visible rows |196197### My layout breaks on different terminals198199| Symptom | Pattern | Fix |200|---------|---------|-----|201| Hardcoded widths break | [Adaptive Layout](references/production-architecture.md#adaptive-layout-engine) | 3-4 responsive breakpoints (80/100/140/180 cols) |202| Colors wrong on light terminals | [Semantic Theming](references/production-architecture.md#semantic-theming-system) | `lipgloss.AdaptiveColor` + WCAG AA contrast |203| Items have equal priority → list shuffles | [Deterministic Sorting](references/production-architecture.md#deterministic-stable-sorting) | Stable sort with tie-breaking secondary key |204| Sort mode not visible | [Dynamic Status Bar](references/production-architecture.md#status-bar-with-dynamic-segments) | Left/right segments with gap-fill |205206### My TUI has multiple views and it's getting messy207208| Symptom | Pattern | Fix |209|---------|---------|-----|210| Key routing chaos | [Focus State Machine](references/production-architecture.md#multi-view-focus-state-machine) | Explicit focus enum + modal priority layer |211| User gets lost in nested views | [Breadcrumb Navigation](references/production-architecture.md#breadcrumb-navigation) | `Home > Board > Priority` path indicator |212| Overlay dismiss loses position | [Focus Restoration](references/production-architecture.md#focus-restoration) | Save focus before overlay, restore on dismiss |213| Old async results overwrite new data | [Stale Message Detection](references/production-architecture.md#stale-message-detection) | Compare data hash before applying results |214| Multiple component updates per frame | [tea.Batch Accumulation](references/production-architecture.md#teabatch-command-accumulation) | Collect cmds in slice, return `tea.Batch(cmds...)` |215| Background goroutine panic kills TUI | [Error Recovery](references/production-architecture.md#error-recovery-in-background-goroutines) | `defer/recover` wrapper for all goroutines |216217### I want to add data-rich visualizations218219| Want | Pattern | Code |220|------|---------|------|221| Bar charts in list columns | [Unicode Sparklines](references/production-architecture.md#data-visualization-sparklines--heatmaps) | `▇▅▂` using 8-level block characters |222| Color-by-intensity | [Perceptual Heatmaps](references/production-architecture.md#data-visualization-sparklines--heatmaps) | gray → blue → purple → pink gradient |223| Dependency graph in terminal | [ASCII Graph Renderer](references/production-architecture.md#custom-asciiunicode-graph-renderer) | Canvas + Manhattan routing (╭─╮│╰╯) |224| Age at a glance | [Age Color Coding](references/production-architecture.md#data-visualization-sparklines--heatmaps) | Fresh=green, aging=yellow, stale=red |225| Borders that mean something | [Semantic Borders](references/production-architecture.md#color-coded-borders-encoding-state) | Red=blocked, green=ready, yellow=high-impact |226227### I want my TUI to feel polished and professional228229| Want | Pattern | Key Idea |230|------|---------|----------|231| Vim-style `gg`/`G` | [Vim Key Combos](references/production-architecture.md#vim-key-combo-tracking) | Track `waitingForG` state between keystrokes |232| Search without jank | [Debounced Search](references/production-architecture.md#debounced-search) | 150ms timer, fire only when typing stops |233| Search across all fields at once | [Composite FilterValue](references/production-architecture.md#composite-filtervalue-for-zero-allocation-fuzzy-search) | Flatten all fields into one string |234| 4-line cards with metadata | [Rich Delegates](references/production-architecture.md#rich-multi-line-list-delegates) | Custom delegate with Height()=4 |235| Expand detail inline | [Inline Expansion](references/production-architecture.md#inline-expansion) | Toggle with `d`, auto-collapse on j/k |236| Copy to clipboard | [Clipboard Integration](references/production-architecture.md#clipboard-integration) | `y` for ID, `C` for markdown + toast feedback |237| `?` / `` ` `` / `;` help | [Multi-Tier Help](references/production-architecture.md#multi-tier-help-system) | Quick ref + tutorial + persistent sidebar |238| Kanban with mode switching | [Kanban Swimlanes](references/production-architecture.md#kanban-board-with-swimlane-modes) | Pre-computed board states, O(1) switch |239| Collapsible tree with h/l | [Tree Navigation](references/production-architecture.md#flattened-tree-navigation) | Flatten tree to visible list for j/k nav |240| Suspend TUI for vim edit | [Editor Dispatch](references/production-architecture.md#smart-editor-dispatch) | `tea.ExecProcess` for terminal, background for GUI |241| Remember expand/collapse | [Persistent State](references/production-architecture.md#persistent-ui-state) | Save to JSON, graceful degradation on corrupt |242| Tune via env vars | [Env Preferences](references/production-architecture.md#environment-variable-preferences) | `NO_COLOR`, theme, debounce, split ratio |243| Optional feature missing? | [Graceful Degradation](references/production-architecture.md#graceful-degradation) | Detect at startup, hide unavailable features |244245**[Full Production Architecture Guide →](references/production-architecture.md)**246247---248249## Pre-Flight Checklist (Every TUI)250251- [ ] Handle `tea.WindowSizeMsg` — resize all components252- [ ] Handle `ctrl+c` — cleanup, restore terminal state253- [ ] Detect piped stdin/stdout — fall back to plain text254- [ ] Test on 80×24 minimum terminal255- [ ] Provide `--no-tui` / `NO_TUI` escape hatch256- [ ] Test with both light AND dark backgrounds257- [ ] Test with `NO_COLOR=1` and `TERM=dumb`258259For production TUIs, see the [full checklist](references/production-architecture.md#production-pre-flight-checklist) (16 must-have + 20 polish items).260261---262263## When NOT to Use Charm264265- **Output is piped:** `mytool | grep` → plain text266- **CI/CD:** No terminal → use flags/env vars267- **One simple prompt:** Maybe `fmt.Scanf` is fine268269**Escape hatch:**270```go271if !term.IsTerminal(os.Stdin.Fd()) || os.Getenv("NO_TUI") != "" {272 runPlainMode()273 return274}275```276277---278279## All References280281| I need... | Read this |282|-----------|-----------|283| Copy-paste one-liners | [Quick Reference](references/QUICK-REFERENCE.md) |284| Prompts to give Claude for TUI tasks | [Prompts](references/PROMPTS.md) |285| Gum / VHS / Mods / Freeze / Glow | [Shell Scripts](references/shell-scripts.md) |286| Bubble Tea architecture, debugging, anti-patterns | [Go TUI](references/go-tui.md) |287| Bubbles component APIs (list, table, viewport...) | [Component Catalog](references/component-catalog.md) |288| Theming, layouts, animation, Huh forms, testing | [Advanced Patterns](references/advanced-patterns.md) |289| Elite patterns: async, snapshots, focus machines, adaptive layout, sparklines, kanban, trees, caching | [Production Architecture](references/production-architecture.md) |290| Wish SSH server, Soft Serve, teatest | [Infrastructure](references/infrastructure.md) |