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: >-4 Build terminal UIs with Charmbracelet (Bubble Tea, Lip Gloss, Gum). Use when:5 Go TUI, shell prompts/spinners, "make CLI prettier", adaptive layouts, async6 rendering, focus state machines, sparklines, heatmaps, kanban boards, SSH apps.7---89# Building Glamorous TUIs with Charmbracelet1011## Quick Router — Start Here1213| I need to... | Use | Reference |14|--------------|-----|-----------|15| **Add prompts/spinners to a shell script** | Gum (no Go) | [Shell Scripts](references/shell-scripts.md) |16| **Build a Go TUI** | Bubble Tea + Lip Gloss | [Go TUI](references/go-tui.md) |17| **Build a production-grade Go TUI** | Above + elite patterns | [Production Architecture](references/production-architecture.md) |18| **Serve a TUI over SSH** | Wish + Bubble Tea | [Infrastructure](references/infrastructure.md) |19| **Record a terminal demo** | VHS | [Shell Scripts](references/shell-scripts.md#vhs-terminal-recording) |20| **Find a Bubbles component** | list, table, viewport, spinner, progress... | [Component Catalog](references/component-catalog.md) |21| **Get a copy-paste pattern** | Layouts, forms, animation, testing | [Quick Reference](references/QUICK-REFERENCE.md) / [Advanced Patterns](references/advanced-patterns.md) |2223---2425## Decision Guide2627```28Is it a shell script?29├─ Yes → Use Gum30│ Need recording? → VHS31│ Need AI? → Mods32│33└─ No (Go application)34 │35 ├─ Just styled output? → Lip Gloss only36 ├─ Simple prompts/forms? → Huh standalone37 ├─ Full interactive TUI? → Bubble Tea + Bubbles + Lip Gloss38 │ │39 │ └─ Production-grade? → Also add elite patterns:40 │ (multi-view, data- two-phase async, immutable snapshots,41 │ dense, must be adaptive layout, focus state machine,42 │ fast & polished) semantic theming, pre-computed styles43 │ → See Production Architecture reference44 │45 └─ Need SSH access? → Wish + Bubble Tea46```4748---4950## Shell Scripts (No Go Required)5152```bash53brew install gum # One-time install54```5556```bash57# Input58NAME=$(gum input --placeholder "Your name")5960# Selection61COLOR=$(gum choose "red" "green" "blue")6263# Fuzzy filter from stdin64BRANCH=$(git branch | gum filter)6566# Confirmation67gum confirm "Continue?" && echo "yes"6869# Spinner70gum spin --title "Working..." -- long-command7172# Styled output73gum style --border rounded --padding "1 2" "Hello"74```7576**[Full Gum Reference →](references/shell-scripts.md#gum-the-essential-tool)**77**[VHS Recording →](references/shell-scripts.md#vhs-terminal-recording)**78**[Mods AI →](references/shell-scripts.md#mods-ai-in-terminal)**7980---8182## Go Applications8384```bash85go get github.com/charmbracelet/bubbletea github.com/charmbracelet/lipgloss86```8788### Minimal TUI (Copy & Run)8990```go91package main9293import (94 "fmt"95 tea "github.com/charmbracelet/bubbletea"96 "github.com/charmbracelet/lipgloss"97)9899var highlight = lipgloss.NewStyle().Foreground(lipgloss.Color("212")).Bold(true)100101type model struct {102 items []string103 cursor int104}105106func (m model) Init() tea.Cmd { return nil }107108func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {109 switch msg := msg.(type) {110 case tea.KeyMsg:111 switch msg.String() {112 case "q", "ctrl+c":113 return m, tea.Quit114 case "up", "k":115 if m.cursor > 0 { m.cursor-- }116 case "down", "j":117 if m.cursor < len(m.items)-1 { m.cursor++ }118 case "enter":119 fmt.Printf("Selected: %s\n", m.items[m.cursor])120 return m, tea.Quit121 }122 }123 return m, nil124}125126func (m model) View() string {127 s := ""128 for i, item := range m.items {129 if i == m.cursor {130 s += highlight.Render("▸ "+item) + "\n"131 } else {132 s += " " + item + "\n"133 }134 }135 return s + "\n(↑/↓ move, enter select, q quit)"136}137138func main() {139 m := model{items: []string{"Option A", "Option B", "Option C"}}140 tea.NewProgram(m).Run()141}142```143144### Library Cheat Sheet145146| Need | Library | Example |147|------|---------|---------|148| TUI framework | `bubbletea` | `tea.NewProgram(model).Run()` |149| Components | `bubbles` | `list.New()`, `textinput.New()` |150| Styling | `lipgloss` | `style.Foreground(lipgloss.Color("212"))` |151| Forms | `huh` | `huh.NewInput().Title("Name").Run()` |152| Markdown | `glamour` | `glamour.Render(md, "dark")` |153| Animation | `harmonica` | `harmonica.NewSpring()` |154155**[Full Go TUI Guide →](references/go-tui.md)**156**[All Bubbles Components →](references/component-catalog.md)**157**[Layout & Animation Patterns →](references/advanced-patterns.md)**158159---160161## SSH Apps (Infrastructure)162163```go164s, _ := wish.NewServer(165 wish.WithAddress(":2222"),166 wish.WithHostKeyPath(".ssh/key"),167 wish.WithMiddleware(168 bubbletea.Middleware(handler),169 logging.Middleware(),170 ),171)172s.ListenAndServe()173```174175Connect: `ssh localhost -p 2222`176177**[Full Infrastructure Guide →](references/infrastructure.md)**178179---180181## Production TUI Architecture (Elite Patterns)182183Beyond basic Bubble Tea: patterns that make TUIs feel fast, polished, and professional.184Each links to a full code example in [Production Architecture](references/production-architecture.md).185186### My TUI is slow or janky187188| Symptom | Pattern | Fix |189|---------|---------|-----|190| UI blocks during computation | [Two-Phase Async](references/production-architecture.md#two-phase-async-architecture) | Phase 1 instant, Phase 2 background goroutine |191| Render path holds mutex | [Immutable Snapshots](references/production-architecture.md#immutable-snapshot-pattern) | Pre-build snapshot, atomic pointer swap |192| File changes cause stutter | [Background Worker](references/production-architecture.md#background-worker-with-file-watching) | Debounced watcher + coalescing |193| Thousands of allocs per frame | [Pre-Computed Styles](references/production-architecture.md#pre-computed-styles-for-performance) | Allocate delegate styles once at startup |194| O(n²) string concat in View() | [strings.Builder](references/production-architecture.md#stringsbuilder-in-view) | Pre-allocated Builder with Grow() |195| Glamour re-renders every frame | [Cached Markdown](references/production-architecture.md#cached-markdown-rendering) | Cache by content hash, invalidate on width change |196| GC pauses during interaction | [Idle-Time GC](references/production-architecture.md#idle-time-gc-management) | Trigger GC during idle periods |197| Large dataset = high memory | [Object Pooling](references/production-architecture.md#object-pooling--memory-efficiency) | sync.Pool with pre-allocated slices |198| Rendering off-screen items | [Viewport Virtualization](references/production-architecture.md#viewport-virtualization) | Only render visible rows |199200### My layout breaks on different terminals201202| Symptom | Pattern | Fix |203|---------|---------|-----|204| Hardcoded widths break | [Adaptive Layout](references/production-architecture.md#adaptive-layout-engine) | 3-4 responsive breakpoints (80/100/140/180 cols) |205| Colors wrong on light terminals | [Semantic Theming](references/production-architecture.md#semantic-theming-system) | `lipgloss.AdaptiveColor` + WCAG AA contrast |206| Items have equal priority → list shuffles | [Deterministic Sorting](references/production-architecture.md#deterministic-stable-sorting) | Stable sort with tie-breaking secondary key |207| Sort mode not visible | [Dynamic Status Bar](references/production-architecture.md#status-bar-with-dynamic-segments) | Left/right segments with gap-fill |208209### My TUI has multiple views and it's getting messy210211| Symptom | Pattern | Fix |212|---------|---------|-----|213| Key routing chaos | [Focus State Machine](references/production-architecture.md#multi-view-focus-state-machine) | Explicit focus enum + modal priority layer |214| User gets lost in nested views | [Breadcrumb Navigation](references/production-architecture.md#breadcrumb-navigation) | `Home > Board > Priority` path indicator |215| Overlay dismiss loses position | [Focus Restoration](references/production-architecture.md#focus-restoration) | Save focus before overlay, restore on dismiss |216| Old async results overwrite new data | [Stale Message Detection](references/production-architecture.md#stale-message-detection) | Compare data hash before applying results |217| Multiple component updates per frame | [tea.Batch Accumulation](references/production-architecture.md#teabatch-command-accumulation) | Collect cmds in slice, return `tea.Batch(cmds...)` |218| Background goroutine panic kills TUI | [Error Recovery](references/production-architecture.md#error-recovery-in-background-goroutines) | `defer/recover` wrapper for all goroutines |219220### I want to add data-rich visualizations221222| Want | Pattern | Code |223|------|---------|------|224| Bar charts in list columns | [Unicode Sparklines](references/production-architecture.md#data-visualization-sparklines--heatmaps) | `▇▅▂` using 8-level block characters |225| Color-by-intensity | [Perceptual Heatmaps](references/production-architecture.md#data-visualization-sparklines--heatmaps) | gray → blue → purple → pink gradient |226| Dependency graph in terminal | [ASCII Graph Renderer](references/production-architecture.md#custom-asciiunicode-graph-renderer) | Canvas + Manhattan routing (╭─╮│╰╯) |227| Age at a glance | [Age Color Coding](references/production-architecture.md#data-visualization-sparklines--heatmaps) | Fresh=green, aging=yellow, stale=red |228| Borders that mean something | [Semantic Borders](references/production-architecture.md#color-coded-borders-encoding-state) | Red=blocked, green=ready, yellow=high-impact |229230### I want my TUI to feel polished and professional231232| Want | Pattern | Key Idea |233|------|---------|----------|234| Vim-style `gg`/`G` | [Vim Key Combos](references/production-architecture.md#vim-key-combo-tracking) | Track `waitingForG` state between keystrokes |235| Search without jank | [Debounced Search](references/production-architecture.md#debounced-search) | 150ms timer, fire only when typing stops |236| 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 |237| 4-line cards with metadata | [Rich Delegates](references/production-architecture.md#rich-multi-line-list-delegates) | Custom delegate with Height()=4 |238| Expand detail inline | [Inline Expansion](references/production-architecture.md#inline-expansion) | Toggle with `d`, auto-collapse on j/k |239| Copy to clipboard | [Clipboard Integration](references/production-architecture.md#clipboard-integration) | `y` for ID, `C` for markdown + toast feedback |240| `?` / `` ` `` / `;` help | [Multi-Tier Help](references/production-architecture.md#multi-tier-help-system) | Quick ref + tutorial + persistent sidebar |241| Kanban with mode switching | [Kanban Swimlanes](references/production-architecture.md#kanban-board-with-swimlane-modes) | Pre-computed board states, O(1) switch |242| Collapsible tree with h/l | [Tree Navigation](references/production-architecture.md#flattened-tree-navigation) | Flatten tree to visible list for j/k nav |243| Suspend TUI for vim edit | [Editor Dispatch](references/production-architecture.md#smart-editor-dispatch) | `tea.ExecProcess` for terminal, background for GUI |244| Remember expand/collapse | [Persistent State](references/production-architecture.md#persistent-ui-state) | Save to JSON, graceful degradation on corrupt |245| Tune via env vars | [Env Preferences](references/production-architecture.md#environment-variable-preferences) | `NO_COLOR`, theme, debounce, split ratio |246| Optional feature missing? | [Graceful Degradation](references/production-architecture.md#graceful-degradation) | Detect at startup, hide unavailable features |247248**[Full Production Architecture Guide →](references/production-architecture.md)**249250---251252## Pre-Flight Checklist (Every TUI)253254- [ ] Handle `tea.WindowSizeMsg` — resize all components255- [ ] Handle `ctrl+c` — cleanup, restore terminal state256- [ ] Detect piped stdin/stdout — fall back to plain text257- [ ] Test on 80×24 minimum terminal258- [ ] Provide `--no-tui` / `NO_TUI` escape hatch259- [ ] Test with both light AND dark backgrounds260- [ ] Test with `NO_COLOR=1` and `TERM=dumb`261262For production TUIs, see the [full checklist](references/production-architecture.md#production-pre-flight-checklist) (16 must-have + 20 polish items).263264---265266## When NOT to Use Charm267268- **Output is piped:** `mytool | grep` → plain text269- **CI/CD:** No terminal → use flags/env vars270- **One simple prompt:** Maybe `fmt.Scanf` is fine271272**Escape hatch:**273```go274if !term.IsTerminal(os.Stdin.Fd()) || os.Getenv("NO_TUI") != "" {275 runPlainMode()276 return277}278```279280---281282## All References283284| I need... | Read this |285|-----------|-----------|286| Copy-paste one-liners | [Quick Reference](references/QUICK-REFERENCE.md) |287| Prompts to give Claude for TUI tasks | [Prompts](references/PROMPTS.md) |288| Gum / VHS / Mods / Freeze / Glow | [Shell Scripts](references/shell-scripts.md) |289| Bubble Tea architecture, debugging, anti-patterns | [Go TUI](references/go-tui.md) |290| Bubbles component APIs (list, table, viewport...) | [Component Catalog](references/component-catalog.md) |291| Theming, layouts, animation, Huh forms, testing | [Advanced Patterns](references/advanced-patterns.md) |292| Elite patterns: async, snapshots, focus machines, adaptive layout, sparklines, kanban, trees, caching | [Production Architecture](references/production-architecture.md) |293| Wish SSH server, Soft Serve, teatest | [Infrastructure](references/infrastructure.md) |