Go TUI
Build Go terminal UIs that look like gonzo, btop, lazygit, and posting — colorful,
dense, graph-and-badge-heavy — not like cat output. The Charm stack (Bubble Tea +
Lip Gloss + Bubbles) is the foundation; ntcharts adds real charts; a verified screenshot
workflow lets you see your UI in color and fix what's wrong.
Two things make a TUI good, and this skill is organized around both:
- Sound architecture — clean Elm-style state/update/view, correct sizing, the right
component for each job.
- A strong visual look — semantic color, data shown as shapes (bars/sparklines/
heatmaps), status as colored badges, dense panels over prose.
You cannot judge the second from source code. So the core loop is: build → screenshot
in color → look → fix.
The aesthetic this skill is for (the through-line)
When the user wants a TUI, they almost always want it to look good and modern — and the
bar this skill aims for is more colour, more graphs, less text. The failure mode to
design against is not ugliness; it's under-baking — rendering a plain number or a flat
one-color bar when the same five lines could render a smooth gradient meter or a sparkline.
Reach for the rich representation by default; the plain one is the draft you didn't finish.
The default-visual contract. Don't decide case-by-case whether to visualize — map the
data type to its visual and follow it:
| You have… |
Default to… |
Not… |
| a bounded value (%, ratio, 0–100) |
a gradient meter (Blend1D, green→red across the fill) |
a bare number, or a flat single-color bar |
| a time series / recent history |
a sparkline (braille ⣀⣤⣶⣿ or ▁▂▃▄▅▆▇█) |
a single latest number |
| a category distribution |
a bar chart or stacked bar, colored per category |
a list of "name: count" lines |
| counts over time × category |
a heatmap row per category, brightness = magnitude |
a table of numbers |
| a discrete status/level/method |
a colored badge (dark ink on saturated bg) |
plain colored text alone |
| genuine free narrative (logs, detail) |
text — and only inside a log/detail panel |
text as the main surface |
Three rules that keep this from tipping into noise:
- Color is semantic, never decorative. One color = one meaning everywhere (red =
error/critical, blue = info/normal, green = ok). The moment a hue means two things,
glanceability dies. "More colour" means more meaningful color, not a rainbow.
- Make gradients smooth. A gradient meter should blend across many steps
(
Blend1D(width, …) — one color per cell), and a heatmap cell should pick from a
~24-step ramp, so the color reads as continuous, not as 4 chunky ░▒▓█ blocks. Finer is
better here.
- Dim the chrome, saturate the signal. Borders, labels, inactive panels recede in
gray; data and the focused panel are bright. Contrast is what makes density legible.
The "less text" check (do this on the screenshot): look at any non-log panel. If it's
more than about half prose/plain numbers, it's under-visualized — there's a meter, bar,
sparkline, or badge waiting to replace those characters. A dashboard's first frame should
read as mostly graphs and badges, with sentences confined to a log/detail viewport.
under-baked → the bar this skill aims for
CPU: 78% CPU ███████████▓▒░░ 78% (gradient green→red)
errors: 89 ERR ▁▂▄▆█▇▅▃▂ peak 89 (sparkline + peak badge)
status: running [ RUNNING ] (badge, dark ink on green)
references/color-and-aesthetics.md has the verified palette and copy-paste patterns for
gradient meters, sparklines, intensity heatmaps, badges, and focused panels.
references/reference-apps.md says exactly what to steal from each best-in-class TUI.
Core workflow
Inspect before importing. For an existing repo, follow go.mod and current
imports; do not migrate v1↔v2 unless asked. For a new app, default to v2:
charm.land/bubbletea/v2, charm.land/lipgloss/v2, charm.land/bubbles/v2/...,
github.com/lrstanley/bubblezone/v2, and github.com/NimbleMarkets/ntcharts/v2 for
charts. (These paths are verified to resolve.)
Design state first, then render. Model owns state; Init starts commands; Update
folds tea.Msg → new state + tea.Cmd; View renders. Keep all I/O in commands. See
references/bubbletea-architecture.md.
Reuse components; don't reinvent. Tables, lists, viewports, inputs, spinners,
progress, help/key — all in Bubbles. Charts/sparklines/heatmaps in ntcharts. Mouse
regions via BubbleZone. The full map and verified snippets are in
references/components-and-charts.md.
Lay out with a size budget. Capture tea.WindowSizeMsg, subtract chrome, split into
fixed panel boxes, render inner content, then JoinHorizontal/JoinVertical. Never
concatenate strings. Never len() for width — use lipgloss.Width. See
references/layout-patterns.md.
Style with Lip Gloss. Colors are image/color values in v2. Use Blend1D for
gradients, Darken/Lighten/Alpha for tints, a centralized theme struct for tokens.
See references/lipgloss-styling.md.
Screenshot it in color and look. This is non-negotiable for any visual work — you
can't see color or alignment from code. Procedure below; full detail in
references/screenshot-workflow.md.
Test logic + lock layouts. Drive Update in unit tests; golden-snapshot complex
renders; screenshot for design. See references/testing.md.
Seeing your UI: the screenshot loop
A plain text capture strips the color that is the point. This pipeline turns a running
TUI into a color-accurate PNG you can Read. Verified end-to-end.
run TUI on a private tmux socket → capture WITH colors (-e) → ansi-to-png.ts → Read PNG → fix
# 1. run the app on a private tmux socket, at the size you want to judge:
OUT=$(mktemp -d); SOCK=gotui-$$; SESS=tui-$$ # unique per run: a fixed name collides with a parallel capture
tmux -f /dev/null -L "$SOCK" new-session -d -s "$SESS" -x 80 -y 24 "go run ."
# 2. capture WITH color escapes once the first frame is drawn (poll — a fixed sleep races the draw):
for _ in $(seq 40); do sleep 0.25; tmux -f /dev/null -L "$SOCK" capture-pane -p -e -t "$SESS" >"$OUT/tui.ansi" 2>/dev/null && grep -q $'\x1b' "$OUT/tui.ansi" && break; done
tmux -f /dev/null -L "$SOCK" kill-window -t "$SESS" # its only window: the session and the private server end with it
# 3. render to a 2x color PNG (Bun + aha + a Chromium-family browser, offscreen):
bun run scripts/ansi-to-png.ts "$OUT/tui.ansi" "$OUT/tui.png" 900x600
# 4. Read("$OUT/tui.png") and judge: semantic color, alignment, density, no black gaps.
# Then fix the code and repeat. Capture one narrow (80x24) AND one wide size.
Key gotchas (the rest are in references/screenshot-workflow.md):
-e is mandatory — it keeps the color; without it you get plain text. No ESC byte
in the file means the capture failed: render nothing from it, conclude nothing.
-f /dev/null and -L "$SOCK" on every invocation. A bare tmux reaches your
interactive server, and a ~/.tmux.conf that auto-creates sessions would spawn them all.
- Through the terminal MCP instead:
mcp__plugin_terminal_mux__start-and-watch({ slot: 2, isolated: true, command: "go run .", pattern: "<a panel title>" }), then
mcp__plugin_terminal_mux__screenshot-pane({ slot: 2 }) returns a viewable PNG directly;
mcp__plugin_terminal_mux__close-pane({ slot: 2 }) when done. The slot tools set no
geometry, so the Bash route above is the one for the two mandated sizes.
scripts/ansi-to-png.ts needs aha (brew install aha) and a Chromium-family browser;
it detects both cross-platform and renders at 2× for crisp blocks/braille/gradients.
Best practices
- Centralize palette + styles in a theme struct once the app has more than one screen.
- Give complex screens explicit
SetSize(w, h) methods; children never read global size.
- Prefer
key.Binding + help.Model for the shortcut bar over hardcoded footer text.
- The gradient meter is the canonical bar —
Blend1D across the fill is ~5 lines and
is the default, not an upgrade. A flat single-color ███░░░ bar is the under-baked
version; only drop to it if a gradient genuinely doesn't fit. Badges and heatmap rows are
the same: a few lines of Lip Gloss, used by default. Use ntcharts for genuine charts
(multi-bar, time series, 2D heatmaps).
- Fix background-fill "black gaps" with
Place + WithWhitespaceStyle (see
lipgloss-styling.md).
- Don't
fmt.Println from a running TUI — use tea.LogToFile.
- Acceptance:
go build ./..., go test ./..., go vet ./..., plus a color screenshot
for any visual change.
When to read what
references/bubbletea-architecture.md — app shape, commands/messages, sizing, keyboard,
mouse, v1↔v2, debugging.
references/lipgloss-styling.md — styles, the image/color v2 model, verified color
helpers (Blend1D, Darken…), layout joins, background-fill fixes.
references/components-and-charts.md — Bubbles component map, key/help, BubbleZone, and
verified ntcharts (sparkline/barchart/heatmap) snippets.
references/color-and-aesthetics.md — the visual core: semantic palette + patterns for
badges, gradient meters, heatmap rows, sparklines, focused panels; anti-patterns.
references/layout-patterns.md — size-budget method, common arrangements, responsive
rules, splitting into child models.
references/reference-apps.md — gonzo/btop/lazygit/posting/rustnet/Claude Code: what to
study and steal from each.
references/screenshot-workflow.md — full color-screenshot procedure and gotchas.
references/testing.md — Update unit tests, teatest, golden snapshots, visual QA.
scripts/ansi-to-png.ts — bundled, verified ANSI→PNG converter.
Source links
1---2name: go-tui3description: Build, review, or debug Go terminal UIs with the Charm stack — Bubble Tea, Lip Gloss, Bubbles, ntcharts. Use for any Go TUI, dashboard, or full-screen CLI, including colour-accurate screenshots.4---56# Go TUI78Build Go terminal UIs that look like **gonzo, btop, lazygit, and posting** — colorful,9dense, graph-and-badge-heavy — not like `cat` output. The Charm stack (Bubble Tea +10Lip Gloss + Bubbles) is the foundation; ntcharts adds real charts; a verified screenshot11workflow lets you *see your UI in color* and fix what's wrong.1213Two things make a TUI good, and this skill is organized around both:141. **Sound architecture** — clean Elm-style state/update/view, correct sizing, the right15 component for each job.162. **A strong visual look** — semantic color, data shown as shapes (bars/sparklines/17 heatmaps), status as colored badges, dense panels over prose.1819You cannot judge the second from source code. So the core loop is: **build → screenshot20in color → look → fix.**2122## The aesthetic this skill is for (the through-line)2324When the user wants a TUI, they almost always want it to look *good and modern* — and the25bar this skill aims for is **more colour, more graphs, less text**. The failure mode to26design against is not ugliness; it's *under-baking* — rendering a plain number or a flat27one-color bar when the same five lines could render a smooth gradient meter or a sparkline.28Reach for the rich representation by default; the plain one is the draft you didn't finish.2930**The default-visual contract.** Don't decide case-by-case whether to visualize — map the31data type to its visual and follow it:3233| You have… | Default to… | Not… |34|---|---|---|35| a bounded value (%, ratio, 0–100) | a **gradient meter** (`Blend1D`, green→red across the fill) | a bare number, or a flat single-color bar |36| a time series / recent history | a **sparkline** (braille `⣀⣤⣶⣿` or `▁▂▃▄▅▆▇█`) | a single latest number |37| a category distribution | a **bar chart** or stacked bar, colored per category | a list of "name: count" lines |38| counts over time × category | a **heatmap row** per category, brightness = magnitude | a table of numbers |39| a discrete status/level/method | a **colored badge** (dark ink on saturated bg) | plain colored text alone |40| genuine free narrative (logs, detail) | text — **and only inside a log/detail panel** | text as the main surface |4142Three rules that keep this from tipping into noise:4344- **Color is semantic, never decorative.** One color = one meaning everywhere (red =45 error/critical, blue = info/normal, green = ok). The moment a hue means two things,46 glanceability dies. "More colour" means more *meaningful* color, not a rainbow.47- **Make gradients smooth.** A gradient meter should blend across many steps48 (`Blend1D(width, …)` — one color per cell), and a heatmap cell should pick from a49 ~24-step ramp, so the color reads as continuous, not as 4 chunky `░▒▓█` blocks. Finer is50 better here.51- **Dim the chrome, saturate the signal.** Borders, labels, inactive panels recede in52 gray; data and the focused panel are bright. Contrast is what makes density legible.5354**The "less text" check (do this on the screenshot):** look at any non-log panel. If it's55more than about half prose/plain numbers, it's under-visualized — there's a meter, bar,56sparkline, or badge waiting to replace those characters. A dashboard's first frame should57read as *mostly graphs and badges*, with sentences confined to a log/detail viewport.5859```60under-baked → the bar this skill aims for61CPU: 78% CPU ███████████▓▒░░ 78% (gradient green→red)62errors: 89 ERR ▁▂▄▆█▇▅▃▂ peak 89 (sparkline + peak badge)63status: running [ RUNNING ] (badge, dark ink on green)64```6566`references/color-and-aesthetics.md` has the verified palette and copy-paste patterns for67gradient meters, sparklines, intensity heatmaps, badges, and focused panels.68`references/reference-apps.md` says exactly what to steal from each best-in-class TUI.6970## Core workflow71721. **Inspect before importing.** For an existing repo, follow `go.mod` and current73 imports; do not migrate v1↔v2 unless asked. For a new app, default to v2:74 `charm.land/bubbletea/v2`, `charm.land/lipgloss/v2`, `charm.land/bubbles/v2/...`,75 `github.com/lrstanley/bubblezone/v2`, and `github.com/NimbleMarkets/ntcharts/v2` for76 charts. (These paths are verified to resolve.)77782. **Design state first, then render.** Model owns state; `Init` starts commands; `Update`79 folds `tea.Msg` → new state + `tea.Cmd`; `View` renders. Keep all I/O in commands. See80 `references/bubbletea-architecture.md`.81823. **Reuse components; don't reinvent.** Tables, lists, viewports, inputs, spinners,83 progress, help/key — all in Bubbles. Charts/sparklines/heatmaps in ntcharts. Mouse84 regions via BubbleZone. The full map and verified snippets are in85 `references/components-and-charts.md`.86874. **Lay out with a size budget.** Capture `tea.WindowSizeMsg`, subtract chrome, split into88 fixed panel boxes, render inner content, then `JoinHorizontal`/`JoinVertical`. Never89 concatenate strings. Never `len()` for width — use `lipgloss.Width`. See90 `references/layout-patterns.md`.91925. **Style with Lip Gloss.** Colors are `image/color` values in v2. Use `Blend1D` for93 gradients, `Darken`/`Lighten`/`Alpha` for tints, a centralized theme struct for tokens.94 See `references/lipgloss-styling.md`.95966. **Screenshot it in color and look.** This is non-negotiable for any visual work — you97 can't see color or alignment from code. Procedure below; full detail in98 `references/screenshot-workflow.md`.991007. **Test logic + lock layouts.** Drive `Update` in unit tests; golden-snapshot complex101 renders; screenshot for design. See `references/testing.md`.102103## Seeing your UI: the screenshot loop104105A plain text capture strips the color that *is the point*. This pipeline turns a **running106TUI into a color-accurate PNG** you can `Read`. Verified end-to-end.107108```109run TUI on a private tmux socket → capture WITH colors (-e) → ansi-to-png.ts → Read PNG → fix110```111112```bash113# 1. run the app on a private tmux socket, at the size you want to judge:114OUT=$(mktemp -d); SOCK=gotui-$$; SESS=tui-$$ # unique per run: a fixed name collides with a parallel capture115tmux -f /dev/null -L "$SOCK" new-session -d -s "$SESS" -x 80 -y 24 "go run ."116117# 2. capture WITH color escapes once the first frame is drawn (poll — a fixed sleep races the draw):118for _ in $(seq 40); do sleep 0.25; tmux -f /dev/null -L "$SOCK" capture-pane -p -e -t "$SESS" >"$OUT/tui.ansi" 2>/dev/null && grep -q $'\x1b' "$OUT/tui.ansi" && break; done119tmux -f /dev/null -L "$SOCK" kill-window -t "$SESS" # its only window: the session and the private server end with it120121# 3. render to a 2x color PNG (Bun + aha + a Chromium-family browser, offscreen):122bun run scripts/ansi-to-png.ts "$OUT/tui.ansi" "$OUT/tui.png" 900x600123124# 4. Read("$OUT/tui.png") and judge: semantic color, alignment, density, no black gaps.125# Then fix the code and repeat. Capture one narrow (80x24) AND one wide size.126```127128Key gotchas (the rest are in `references/screenshot-workflow.md`):129- **`-e` is mandatory** — it keeps the color; without it you get plain text. No `ESC` byte130 in the file means the capture failed: render nothing from it, conclude nothing.131- **`-f /dev/null` and `-L "$SOCK"` on every invocation.** A bare `tmux` reaches your132 interactive server, and a `~/.tmux.conf` that auto-creates sessions would spawn them all.133- Through the terminal MCP instead: `mcp__plugin_terminal_mux__start-and-watch({ slot: 2,134 isolated: true, command: "go run .", pattern: "<a panel title>" })`, then135 `mcp__plugin_terminal_mux__screenshot-pane({ slot: 2 })` returns a viewable PNG directly;136 `mcp__plugin_terminal_mux__close-pane({ slot: 2 })` when done. The slot tools set no137 geometry, so the Bash route above is the one for the two mandated sizes.138- `scripts/ansi-to-png.ts` needs `aha` (`brew install aha`) and a Chromium-family browser;139 it detects both cross-platform and renders at 2× for crisp blocks/braille/gradients.140141## Best practices142143- Centralize palette + styles in a **theme struct** once the app has more than one screen.144- Give complex screens explicit `SetSize(w, h)` methods; children never read global size.145- Prefer `key.Binding` + `help.Model` for the shortcut bar over hardcoded footer text.146- The **gradient meter** is the canonical bar — `Blend1D` across the fill is ~5 lines and147 is the *default*, not an upgrade. A flat single-color `███░░░` bar is the under-baked148 version; only drop to it if a gradient genuinely doesn't fit. Badges and heatmap rows are149 the same: a few lines of Lip Gloss, used by default. Use ntcharts for genuine charts150 (multi-bar, time series, 2D heatmaps).151- Fix background-fill "black gaps" with `Place` + `WithWhitespaceStyle` (see152 `lipgloss-styling.md`).153- Don't `fmt.Println` from a running TUI — use `tea.LogToFile`.154- Acceptance: `go build ./...`, `go test ./...`, `go vet ./...`, **plus a color screenshot**155 for any visual change.156157## When to read what158159- `references/bubbletea-architecture.md` — app shape, commands/messages, sizing, keyboard,160 mouse, v1↔v2, debugging.161- `references/lipgloss-styling.md` — styles, the `image/color` v2 model, verified color162 helpers (`Blend1D`, `Darken`…), layout joins, background-fill fixes.163- `references/components-and-charts.md` — Bubbles component map, key/help, BubbleZone, and164 verified ntcharts (sparkline/barchart/heatmap) snippets.165- `references/color-and-aesthetics.md` — the visual core: semantic palette + patterns for166 badges, gradient meters, heatmap rows, sparklines, focused panels; anti-patterns.167- `references/layout-patterns.md` — size-budget method, common arrangements, responsive168 rules, splitting into child models.169- `references/reference-apps.md` — gonzo/btop/lazygit/posting/rustnet/Claude Code: what to170 study and steal from each.171- `references/screenshot-workflow.md` — full color-screenshot procedure and gotchas.172- `references/testing.md` — Update unit tests, teatest, golden snapshots, visual QA.173- `scripts/ansi-to-png.ts` — bundled, verified ANSI→PNG converter.174175## Source links176177- Bubble Tea: https://github.com/charmbracelet/bubbletea178- Lip Gloss: https://github.com/charmbracelet/lipgloss179- Bubbles: https://github.com/charmbracelet/bubbles180- BubbleZone: https://github.com/lrstanley/bubblezone181- ntcharts: https://github.com/NimbleMarkets/ntcharts