Part of #54. This is the foundation issue — the terminal analog of the web basic catalog's --a2ui-* CSS variables.
Problem
render.Styles has six fields (CardBorder, Heading, Subheading, Caption, Button, ButtonFocused), so a host can theme those — but most chrome is hardcoded literals a host cannot touch:
- focus cues
▏/▎ (render/fields.go:70-73, :104, :145-149, :196, :228)
- button brackets
"[ "/" ]" (render/button.go:38)
- checkbox/radio marks
[x]/[ ]/(•)/( ) (render/fields.go:89-91, :130-137)
- list bullet
"• " + continuation indent (render/containers.go:175-185)
- tab separator
" │ " (render/containers.go:120), modal cues ▹/▸ (:136-139)
- divider glyphs
─/│ (render/styles.go:69, containers.go:87), slider cells █/─ (fields.go:176, :186)
- several roles share one slot: input labels, placeholders,
(empty)/(unset), {binding} diagnostics, and media placeholders all reuse Caption
Proposal
Extend Styles with per-component slots and a Glyphs struct:
type Styles struct {
// existing
CardBorder, Heading, Subheading, Caption lipgloss.Style
Button, ButtonFocused lipgloss.Style
// new component slots
ButtonPrimary, ButtonPrimaryFocused lipgloss.Style // see the primary-variant issue
Label, Placeholder lipgloss.Style // default: alias Caption
InputText, InputFocused lipgloss.Style // default: unstyled
Divider, ListBullet lipgloss.Style // default: unstyled
TabActive, TabInactive lipgloss.Style // default: Heading / unstyled
SliderFilled, SliderEmpty lipgloss.Style // default: unstyled
Glyphs Glyphs
}
type Glyphs struct {
FocusCue, FocusCueActive string // "▏", "▎"
ButtonOpen, ButtonClose string // "[ ", " ]"
CheckOn, CheckOff string // "[x]", "[ ]"
RadioOn, RadioOff string // "(•)", "( )"
ListBullet string // "• "
TabSeparator string // " │ "
ModalCue, ModalCueOpen string // "▹", "▸"
DividerH, DividerV string // "─", "│"
SliderFilled, SliderEmpty string // "█", "─"
}
Rules
- Byte-compatible defaults:
DefaultStyles() fills every slot with today's hardcoded values; all existing goldens and the no-color-SGR test (render/styles_test.go:17-37) pass unchanged.
- Alias at construction, not render: defaults that alias an existing slot (
Label = Caption) are set inside DefaultStyles(), so overriding Caption alone keeps today's behavior while a host can split the roles.
- Zero-value glyph fallback:
NewSurface backfills empty glyph strings with defaults so a partially-populated Styles from an older host doesn't render blank chrome.
- Renderers switch from literals to
s.styles.* / s.styles.Glyphs.* — no behavior change with defaults.
Out of scope
- honoring new hints (separate issues)
Surface.SetStyles (separate issue)
createSurface.theme mapping — stays a no-op per the epic's invariants
Part of #54. This is the foundation issue — the terminal analog of the web basic catalog's
--a2ui-*CSS variables.Problem
render.Styleshas six fields (CardBorder,Heading,Subheading,Caption,Button,ButtonFocused), so a host can theme those — but most chrome is hardcoded literals a host cannot touch:▏/▎(render/fields.go:70-73,:104,:145-149,:196,:228)"[ "/" ]"(render/button.go:38)[x]/[ ]/(•)/( )(render/fields.go:89-91,:130-137)"• "+ continuation indent (render/containers.go:175-185)" │ "(render/containers.go:120), modal cues▹/▸(:136-139)─/│(render/styles.go:69,containers.go:87), slider cells█/─(fields.go:176,:186)(empty)/(unset),{binding}diagnostics, and media placeholders all reuseCaptionProposal
Extend
Styleswith per-component slots and aGlyphsstruct:Rules
DefaultStyles()fills every slot with today's hardcoded values; all existing goldens and the no-color-SGR test (render/styles_test.go:17-37) pass unchanged.Label = Caption) are set insideDefaultStyles(), so overridingCaptionalone keeps today's behavior while a host can split the roles.NewSurfacebackfills empty glyph strings with defaults so a partially-populatedStylesfrom an older host doesn't render blank chrome.s.styles.*/s.styles.Glyphs.*— no behavior change with defaults.Out of scope
Surface.SetStyles(separate issue)createSurface.thememapping — stays a no-op per the epic's invariants