okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
f7fcae1
main
name: charm-tui description: Design, implement, review, and refine Go terminal interfaces using Charm's Bubble Tea, Bubbles, Lip Gloss, and Huh. Use for interactive CLIs, pickers, forms, dashboards, streaming views, keyboard/focus bugs, terminal layout, or Charm API migrations. Emphasizes behavior-preserving changes, terminal-native interaction, correct asynchronous state, and restrained visual design. Also use to decide whether a full-screen TUI is appropriate; do not add Charm to ordinary noninteractive Go commands without a need.
Charm TUI craft
Build a terminal tool that is easy to understand, quick to operate, and reliable under real input, data, and terminal conditions. Learn Charm's interaction and engineering practices, not just its palette.
Work from an example
Open Do by example and choose the closest task before inventing a new pattern. Each task pairs a concrete user request with before/after code, the full implementation, a sequence to try, and a regression test. Use the example's mechanism; preserve the target application's contract.
For a new picker, begin with task 1. For an existing application, jump directly to focus and modals, cell-aware layout, async search, pipeline output, stream anchors, subscription closure, or behavior-preserving reduction.
Example: fix the owner of a key, not the key
Given “typing q in search quits,” do not remove the browse-mode q shortcut. In the key handler, route to the focused input before considering browse bindings. This is an excerpt from the complete picker; k is its mode-specific keymap:
if m .input .Focused () {if key .Matches (msg ,k .back ,k .accept ) {m .input .Blur ()m .clampWindow ()return m ,nil }return m ,m .updateInput (msg ) }
The modal branch precedes this and consumes its key even when closing. The browse branch follows it. Keep TestPrintableKeysBelongToSearch and TestModalEscapeDoesNotQuitOrReopen, adapting their setup rather than merely repeating the rule.
Example: invalidate before waiting
Given “old results flash during debounce,” change the generation when the query changes. Then test the precise race, using the state implementation from the search program:
a := s .change ("a" )s .begin (a .generation )b := s .change ("b" )// B has not started yet. if s .complete (a .generation , []string {"old" },nil ) {t .Fatal ("accepted an old result during the new debounce" ) }s .begin (b .generation )
The full example also cancels A's context, tags errors, rejects duplicate starts, and retains child input commands. The regression is in request_test.go.
Example: preserve the allocated box width
Given a 40-column dialog using Lip Gloss v2, with zero margins:
box := lipgloss .NewStyle ().Border (lipgloss .NormalBorder ()).Padding (0 ,1 )outerWidth := 40 innerWidth := outerWidth - box .GetHorizontalFrameSize ()content := ansi .Truncate (label ,innerWidth ,"…" )rendered := box .Width (outerWidth ).Render (content )
Do not feed innerWidth back into box.Width: it already includes the internal frame. The complete helper handles tiny widths by removing the frame, and the supplied rendering test measures actual output. Use cell-width assertions, not byte-length assertions.
These example fragments are version-specific excerpts, not independent complete programs. The three complete programs supply imports, models, entry points, and tests. Read verification before repeating any claim that they were compiled or terminal-tested.
Start with the existing contract
Before editing, inspect go.mod, the entry point, the root model, keybindings, styles, output handling, and relevant tests. Run the existing UI when possible. Record its main task, normal flow, cancellation behavior, output format, and supported environments. For a new tool, derive these from the user's request.
Treat an existing interface as a contract. Preserve its keys, focus behavior, inline/full-screen mode, sizing, counters, custom visuals, and result semantics unless the requested change requires otherwise. Replacing a bespoke picker with a stock bubbles/list is not a behavior-preserving cleanup. Reuse component mechanics beneath the interface when useful.
Identify the repository's actual Charm major versions. Do not paste v1 examples into v2 code, upgrade dependencies as incidental cleanup, or assume every Charm package uses the same module path. Read versions and component selection when selecting libraries, writing version-sensitive code, or migrating.
Choose the smallest suitable surface
| User's task | Starting point |
|---|---|
| Run once, return a value, integrate into scripts | Flags plus ordinary output; styling is optional |
| Pick, confirm, or enter a few values | Inline interaction; Huh for forms, Gum for shell composition |
| Browse, inspect, filter, or monitor repeatedly | Bubble Tea application with appropriate Bubbles |
| Read formatted prose | Glamour plus a viewport when scrolling is needed |
| Operate through SSH | A session-scoped TUI; investigate Wish only when remote interaction is actually required |
These are choices, not mandatory layers. A command need not gain a configuration framework, router, event bus, or full-screen shell to become pleasant. Preserve the project's CLI conventions; use ordinary Go functions and structs before adding infrastructure.
Design the interaction before decorating it
Write the primary flow in one sentence: “Filter candidates, inspect the current item, then accept or cancel.” Identify the first useful action and make it visible without opening help.
Distinguish focus (which control receives input), cursor (which item is highlighted), selection (which IDs have been chosen), and operation state (what work is happening). Do not overload one field or one color with these meanings.
Define loading, ready, empty, no-match, recoverable-error, and terminal-error states where applicable. Give each a truthful message and a useful next action. Preserve valid data during refresh when it remains useful; label it as refreshing or stale. Do not turn every transient failure into an empty screen.
Use a compact hierarchy: task/context, primary content, local status, contextual controls. Start with the terminal's background, readable text, one accent role, a clear current-row marker, and quiet metadata. Add borders only where they clarify grouping. Personality can live in a title, status wording, or small animation; it must not displace content or obscure state.
Do not impose a pink palette, rounded boxes, gradients, a large logo, or dashboard cards merely because the task mentions Charm. Read design and interaction for concrete patterns and tradeoffs.
Keep state ownership obvious
The model's update path owns mutable UI state. Commands perform I/O or expensive work and return typed results. Rendering describes the current state; it does not initiate network requests, start timers, or secretly change the user's selection.
Use either a small tree of independently useful components or one root model with simple imperative helpers. Both are legitimate. Extract a component when it owns a coherent interaction or lifecycle, not for every rectangle. A local state change does not need a command that sends a message back to the same model.
When forwarding an event to a Bubbles component, retain both its returned model and command. Do not broadcast keystrokes to every child. Route non-input results to their owner even when that owner is not visible, when the lifecycle requires it.
For asynchronous work:
- Capture inputs before returning a command. Do not let the command read or mutate a live model, slice, or map concurrently.
- Cancel superseded work and tag results with a generation or operation ID. Invalidate the old generation when intent changes, not only when the next request starts. Ignore obsolete successes, failures, and completion flags.
- Use
tea.Batchfor independent work. Start result-dependent work from the matching result handler; command ordering is not a data dependency. - Give timers, subscriptions, and workers an owner and a stop condition. Re-arm a one-shot subscription only once. Stop on channel closure. Bound backlog and work.
Read architecture and effects for the precise request lifecycle and streaming policy. The search walkthrough wires this into an actual input model; the smaller contract examples isolate the arithmetic/state rules.
Route input deliberately
Set precedence explicitly: emergency/application-level controls, then the active modal or popup, then the focused control, then the current screen's unconsumed navigation keys. The exact interrupt policy is application-specific; document whether it cancels work, backs out, or exits.
Printable characters belong to an active editor. Typing q, j, k, /, ?, or space must not trigger unrelated navigation or quit. Preserve component editing, paste, cursor, and focus commands. Handle a modal's consumed key exactly once; closing it must not activate the item underneath.
Use key.Binding and help.Model when appropriate so active controls and displayed help share definitions. Disable unavailable actions in both routing and help. Escape should unwind a visible interaction predictably; do not unexpectedly discard a draft. Restore focus after closing overlays.
Keyboard operation must remain complete without a mouse. Mouse hit testing must use the same geometry as rendering. Keep ordinary terminal selection/copy practical; do not enable aggressive mouse capture without a use case. Do not silently take over the clipboard.
Treat layout as cell arithmetic
Let the parent allocate bounds. Measure rendered headers, prompts, help, and status at the current width; subtract them from available space. Clamp derived dimensions. Keep margins, borders, padding, and child content separate. Never depend on a fixed “subtract 4” unless those four rows are a tested invariant.
In Lip Gloss v2, Width(n) describes the block including borders and padding, before margins. Do not subtract a frame twice. Details and a worked example are in layout and rendering.
Measure display cells with the relevant Lip Gloss/ANSI helpers, not len, byte slicing, or rune counts. Account for styled content, CJK, combining marks, emoji, tabs, prompts, and selection prefixes. A helper that preserves ANSI is not a security sanitizer.
On narrow terminals, drop optional columns or switch composition before making the primary task unusable. Keep a compact safe view for extremely small dimensions and handle initial zero-size events. Reflow on width changes; clamp or preserve the scroll anchor intentionally. At ordinary sizes, controls should not jump as counts or status strings change.
Make output and lifecycle part of the UI
For a pipeline-oriented command, reserve stdout for its machine-consumable result. Use stderr or an explicitly chosen terminal for interaction, and print the final result only after the UI has finished. Do not mix logging with a live renderer, even on stderr if that is the renderer's output.
Define noninteractive behavior before opening a terminal. Do not hang CI waiting for a key. Distinguish accepted, cancelled, failed, and interrupted outcomes; cancellation must not accidentally emit a valid-looking selection. Respect existing exit-code conventions.
Let the runtime restore terminal modes. Return through cleanup paths; avoid os.Exit before deferred cleanup. Use the runtime's supported handoff for an interactive child process instead of letting two programs read the same terminal. For SSH, keep model, theme/capability state, cancellation, and authentication boundaries session-scoped.
Offer a plain or accessible path when relevant. No-color mode is not equivalent to screen-reader accessibility. Avoid reliance on a special font, animation, mouse, terminal graphics, or color alone.
Optimize the work, not the appearance of work
First measure startup, keystroke-to-update responsiveness, rendering cost, allocations, and idle CPU for representative data. Keep inexpensive local filtering simple. Move expensive search/render work out of the update/render path when measurements justify it.
For large lists, avoid formatting all rows just to display one viewport. Cache costly Markdown or highlighting with explicit data, width, and theme invalidation. Do not compute full-history height every frame merely to decide whether content overflows. A cache has a correctness contract and a memory cost; do not add one reflexively.
For streams, coalesce redraws without silently dropping durable data. Follow new output only while the user is following the tail; reading older content must not be interrupted by incoming events. Bound history or make its retention policy explicit.
Verify the contract, then finish
Read testing and release. At minimum test state transitions and command results, focus and modal routing, zero/one/many/no-match data, stale completion, resize, Unicode, and stdout separation. Include direct model tests; use the matching-major teatest package for integration where suitable.
Inspect actual terminal output at a normal size and a small size. For existing UIs, compare before and after on the same input. Exercise paste, cancel, error recovery, repeated refresh, light/dark or explicit-theme behavior, and terminal restoration. Use deterministic snapshots or VHS recordings for appearance; they complement behavioral assertions, not replace them.
Use evaluation scenarios as adversarial review prompts, selecting relevant cases. These are evaluation specifications, not a claim that the target application has passed.
When delivering work, state the interaction preserved or changed, the implementation choices that mattered, the checks actually run, and any unverified terminal behavior. Distinguish source-reviewed, compiled, unit-tested, and terminal-tested. Do not claim polish from compilation alone.
Evidence and further reading
The case studies separate observed application behavior from recommendations, including a concrete example where implementation and maintainer notes disagree. The source ledger records primary sources, inspected revisions, and scope. This skill is an independent synthesis, not an official Charm standard.
1--- 2name : charm-tui 3description : Design, implement, review, and refine Go terminal interfaces using Charm's Bubble Tea, Bubbles, Lip Gloss, and Huh. Use for interactive CLIs, pickers, forms, dashboards, streaming views, keyboard/focus bugs, terminal layout, or Charm API migrations. Emphasizes behavior-preserving changes, terminal-native interaction, correct asynchronous state, and restrained visual design. Also use to decide whether a full-screen TUI is appropriate; do not add Charm to ordinary noninteractive Go commands without a need. 4--- 5 6# Charm TUI craft 7 8Build a terminal tool that is easy to understand, quick to operate, and reliable under real input, data, and terminal conditions. Learn Charm's interaction and engineering practices, not just its palette. 9 10## Work from an example 11 12Open [Do by example](examples/WORKED-EXAMPLES.md) and choose the closest task before inventing a new pattern. Each task pairs a concrete user request with before/after code, the full implementation, a sequence to try, and a regression test. Use the example's mechanism; preserve the target application's contract. 13 14For a new picker, begin with [task 1](examples/WORKED-EXAMPLES.md#1-build-a-compact-picker). For an existing application, jump directly to [focus and modals](examples/WORKED-EXAMPLES.md#2-fix-search-keys-and-modal-fall-through), [cell-aware layout](examples/WORKED-EXAMPLES.md#3-fit-a-narrow-terminal-without-losing-identity), [async search](examples/WORKED-EXAMPLES.md#4-implement-latest-query-wins-search), [pipeline output](examples/WORKED-EXAMPLES.md#5-return-a-result-without-polluting-stdout), [stream anchors](examples/WORKED-EXAMPLES.md#6-keep-a-streaming-view-from-jumping), [subscription closure](examples/WORKED-EXAMPLES.md#7-stop-subscriptions-without-leaks-or-busy-loops), or [behavior-preserving reduction](examples/WORKED-EXAMPLES.md#8-reduce-code-without-redesigning-the-interface). 15 16### Example: fix the owner of a key, not the key 17 18Given “typing q in search quits,” do not remove the browse-mode q shortcut. In the key handler, route to the focused input before considering browse bindings. This is an excerpt from the [complete picker](examples/worked/cmd/picker/model.go); `k` is its mode-specific keymap: 19 20``` go 21if m . input . Focused () { 22if key . Matches ( msg , k . back , k . accept ) { 23m . input . Blur () 24m . clampWindow () 25return m , nil 26} 27return m , m . updateInput ( msg ) 28} 29``` 30 31The modal branch precedes this and consumes its key even when closing. The browse branch follows it. Keep `TestPrintableKeysBelongToSearch` and `TestModalEscapeDoesNotQuitOrReopen`, adapting their setup rather than merely repeating the rule. 32 33### Example: invalidate before waiting 34 35Given “old results flash during debounce,” change the generation when the query changes. Then test the precise race, using the state implementation from [the search program](examples/worked/cmd/search/request.go): 36 37``` go 38a := s . change ( "a" ) 39s. begin ( a . generation ) 40b := s . change ( "b" ) // B has not started yet. 41if s . complete ( a . generation , [] string { "old" }, nil ) { 42t . Fatal ( "accepted an old result during the new debounce" ) 43} 44s. begin ( b . generation ) 45``` 46 47The full example also cancels A's context, tags errors, rejects duplicate starts, and retains child input commands. The regression is in [request_test.go](examples/worked/cmd/search/request_test.go). 48 49### Example: preserve the allocated box width 50 51Given a 40-column dialog using Lip Gloss v2, with zero margins: 52 53``` go 54box := lipgloss . NewStyle (). Border ( lipgloss . NormalBorder ()). Padding ( 0 , 1 ) 55outerWidth := 40 56innerWidth := outerWidth - box . GetHorizontalFrameSize () 57content := ansi . Truncate ( label , innerWidth , "…" ) 58rendered := box . Width ( outerWidth ). Render ( content ) 59``` 60 61Do not feed `innerWidth` back into `box.Width`: it already includes the internal frame. The [complete helper](examples/worked/internal/display/render.go) handles tiny widths by removing the frame, and the supplied rendering test measures actual output. Use cell-width assertions, not byte-length assertions. 62 63These example fragments are version-specific excerpts, not independent complete programs. The [three complete programs](examples/worked/README.md) supply imports, models, entry points, and tests. Read [verification](VERIFICATION.md) before repeating any claim that they were compiled or terminal-tested. 64 65## Start with the existing contract 66 67Before editing, inspect `go.mod`, the entry point, the root model, keybindings, styles, output handling, and relevant tests. Run the existing UI when possible. Record its main task, normal flow, cancellation behavior, output format, and supported environments. For a new tool, derive these from the user's request. 68 69Treat an existing interface as a contract. Preserve its keys, focus behavior, inline/full-screen mode, sizing, counters, custom visuals, and result semantics unless the requested change requires otherwise. Replacing a bespoke picker with a stock `bubbles/list` is not a behavior-preserving cleanup. Reuse component mechanics beneath the interface when useful. 70 71Identify the repository's actual Charm major versions. Do not paste v1 examples into v2 code, upgrade dependencies as incidental cleanup, or assume every Charm package uses the same module path. Read [versions and component selection](references/versions-and-components.md) when selecting libraries, writing version-sensitive code, or migrating. 72 73## Choose the smallest suitable surface 74 75| User's task | Starting point | 76| --- | --- | 77| Run once, return a value, integrate into scripts | Flags plus ordinary output; styling is optional | 78| Pick, confirm, or enter a few values | Inline interaction; Huh for forms, Gum for shell composition | 79| Browse, inspect, filter, or monitor repeatedly | Bubble Tea application with appropriate Bubbles | 80| Read formatted prose | Glamour plus a viewport when scrolling is needed | 81| Operate through SSH | A session-scoped TUI; investigate Wish only when remote interaction is actually required | 82 83These are choices, not mandatory layers. A command need not gain a configuration framework, router, event bus, or full-screen shell to become pleasant. Preserve the project's CLI conventions; use ordinary Go functions and structs before adding infrastructure. 84 85## Design the interaction before decorating it 86 87Write the primary flow in one sentence: “Filter candidates, inspect the current item, then accept or cancel.” Identify the first useful action and make it visible without opening help. 88 89Distinguish **focus** (which control receives input), **cursor** (which item is highlighted), **selection** (which IDs have been chosen), and **operation state** (what work is happening). Do not overload one field or one color with these meanings. 90 91Define loading, ready, empty, no-match, recoverable-error, and terminal-error states where applicable. Give each a truthful message and a useful next action. Preserve valid data during refresh when it remains useful; label it as refreshing or stale. Do not turn every transient failure into an empty screen. 92 93Use a compact hierarchy: task/context, primary content, local status, contextual controls. Start with the terminal's background, readable text, one accent role, a clear current-row marker, and quiet metadata. Add borders only where they clarify grouping. Personality can live in a title, status wording, or small animation; it must not displace content or obscure state. 94 95Do not impose a pink palette, rounded boxes, gradients, a large logo, or dashboard cards merely because the task mentions Charm. Read [design and interaction](references/design-and-interaction.md) for concrete patterns and tradeoffs. 96 97## Keep state ownership obvious 98 99The model's update path owns mutable UI state. Commands perform I/O or expensive work and return typed results. Rendering describes the current state; it does not initiate network requests, start timers, or secretly change the user's selection. 100 101Use either a small tree of independently useful components or one root model with simple imperative helpers. Both are legitimate. Extract a component when it owns a coherent interaction or lifecycle, not for every rectangle. A local state change does not need a command that sends a message back to the same model. 102 103When forwarding an event to a Bubbles component, retain both its returned model and command. Do not broadcast keystrokes to every child. Route non-input results to their owner even when that owner is not visible, when the lifecycle requires it. 104 105For asynchronous work: 106 107- Capture inputs before returning a command. Do not let the command read or mutate a live model, slice, or map concurrently. 108- Cancel superseded work and tag results with a generation or operation ID. Invalidate the old generation **when intent changes**, not only when the next request starts. Ignore obsolete successes, failures, and completion flags. 109- Use `tea.Batch` for independent work. Start result-dependent work from the matching result handler; command ordering is not a data dependency. 110- Give timers, subscriptions, and workers an owner and a stop condition. Re-arm a one-shot subscription only once. Stop on channel closure. Bound backlog and work. 111 112Read [architecture and effects](references/architecture-and-effects.md) for the precise request lifecycle and streaming policy. The [search walkthrough](examples/WORKED-EXAMPLES.md#4-implement-latest-query-wins-search) wires this into an actual input model; the smaller [contract examples](examples/contracts/README.md) isolate the arithmetic/state rules. 113 114## Route input deliberately 115 116Set precedence explicitly: emergency/application-level controls, then the active modal or popup, then the focused control, then the current screen's unconsumed navigation keys. The exact interrupt policy is application-specific; document whether it cancels work, backs out, or exits. 117 118Printable characters belong to an active editor. Typing `q`, `j`, `k`, `/`, `?`, or space must not trigger unrelated navigation or quit. Preserve component editing, paste, cursor, and focus commands. Handle a modal's consumed key exactly once; closing it must not activate the item underneath. 119 120Use `key.Binding` and `help.Model` when appropriate so active controls and displayed help share definitions. Disable unavailable actions in both routing and help. Escape should unwind a visible interaction predictably; do not unexpectedly discard a draft. Restore focus after closing overlays. 121 122Keyboard operation must remain complete without a mouse. Mouse hit testing must use the same geometry as rendering. Keep ordinary terminal selection/copy practical; do not enable aggressive mouse capture without a use case. Do not silently take over the clipboard. 123 124## Treat layout as cell arithmetic 125 126Let the parent allocate bounds. Measure rendered headers, prompts, help, and status at the current width; subtract them from available space. Clamp derived dimensions. Keep margins, borders, padding, and child content separate. Never depend on a fixed “subtract 4” unless those four rows are a tested invariant. 127 128In **Lip Gloss v2**, `Width(n)` describes the block including borders and padding, before margins. Do not subtract a frame twice. Details and a worked example are in [layout and rendering](references/layout-and-rendering.md). 129 130Measure display cells with the relevant Lip Gloss/ANSI helpers, not `len`, byte slicing, or rune counts. Account for styled content, CJK, combining marks, emoji, tabs, prompts, and selection prefixes. A helper that preserves ANSI is not a security sanitizer. 131 132On narrow terminals, drop optional columns or switch composition before making the primary task unusable. Keep a compact safe view for extremely small dimensions and handle initial zero-size events. Reflow on width changes; clamp or preserve the scroll anchor intentionally. At ordinary sizes, controls should not jump as counts or status strings change. 133 134## Make output and lifecycle part of the UI 135 136For a pipeline-oriented command, reserve stdout for its machine-consumable result. Use stderr or an explicitly chosen terminal for interaction, and print the final result only after the UI has finished. Do not mix logging with a live renderer, even on stderr if that is the renderer's output. 137 138Define noninteractive behavior before opening a terminal. Do not hang CI waiting for a key. Distinguish accepted, cancelled, failed, and interrupted outcomes; cancellation must not accidentally emit a valid-looking selection. Respect existing exit-code conventions. 139 140Let the runtime restore terminal modes. Return through cleanup paths; avoid `os.Exit` before deferred cleanup. Use the runtime's supported handoff for an interactive child process instead of letting two programs read the same terminal. For SSH, keep model, theme/capability state, cancellation, and authentication boundaries session-scoped. 141 142Offer a plain or accessible path when relevant. No-color mode is not equivalent to screen-reader accessibility. Avoid reliance on a special font, animation, mouse, terminal graphics, or color alone. 143 144## Optimize the work, not the appearance of work 145 146First measure startup, keystroke-to-update responsiveness, rendering cost, allocations, and idle CPU for representative data. Keep inexpensive local filtering simple. Move expensive search/render work out of the update/render path when measurements justify it. 147 148For large lists, avoid formatting all rows just to display one viewport. Cache costly Markdown or highlighting with explicit data, width, and theme invalidation. Do not compute full-history height every frame merely to decide whether content overflows. A cache has a correctness contract and a memory cost; do not add one reflexively. 149 150For streams, coalesce redraws without silently dropping durable data. Follow new output only while the user is following the tail; reading older content must not be interrupted by incoming events. Bound history or make its retention policy explicit. 151 152## Verify the contract, then finish 153 154Read [testing and release](references/testing-and-release.md). At minimum test state transitions and command results, focus and modal routing, zero/one/many/no-match data, stale completion, resize, Unicode, and stdout separation. Include direct model tests; use the **matching-major** teatest package for integration where suitable. 155 156Inspect actual terminal output at a normal size and a small size. For existing UIs, compare before and after on the same input. Exercise paste, cancel, error recovery, repeated refresh, light/dark or explicit-theme behavior, and terminal restoration. Use deterministic snapshots or VHS recordings for appearance; they complement behavioral assertions, not replace them. 157 158Use [evaluation scenarios](evals/scenarios.json) as adversarial review prompts, selecting relevant cases. These are evaluation specifications, not a claim that the target application has passed. 159 160When delivering work, state the interaction preserved or changed, the implementation choices that mattered, the checks actually run, and any unverified terminal behavior. Distinguish source-reviewed, compiled, unit-tested, and terminal-tested. Do not claim polish from compilation alone. 161 162## Evidence and further reading 163 164The [case studies](references/case-studies.md) separate observed application behavior from recommendations, including a concrete example where implementation and maintainer notes disagree. The [source ledger](references/sources.md) records primary sources, inspected revisions, and scope. This skill is an independent synthesis, not an official Charm standard.