// Package theme centralises dial's visual styling. The brief makes low-light // legibility a hard requirement: generous spacing, high contrast, and no dense // dim text. These styles encode that — the "muted" tones are still clearly // readable, not the near-invisible grey common in dense pickers. package theme import ( "hash/fnv" "github.com/charmbracelet/lipgloss" ) // Theme holds the styles used across the picker. type Theme struct { Title lipgloss.Style Meta lipgloss.Style // header meta / hints Number lipgloss.Style // list index NumberSel lipgloss.Style Alias lipgloss.Style // host alias, resting AliasSel lipgloss.Style // host alias, highlighted Detail lipgloss.Style // secondary line (hostname/user) DetailSel lipgloss.Style Tag lipgloss.Style Star lipgloss.Style Cursor lipgloss.Style // the selection bar/marker Preview lipgloss.Style // preview box border PreviewK lipgloss.Style // preview key column PreviewV lipgloss.Style // preview value column FilterBar lipgloss.Style Help lipgloss.Style Warn lipgloss.Style // tagPalette is a set of distinct high-contrast hues used to colour tag // pills and tag section headers, assigned deterministically per tag name. tagPalette []lipgloss.AdaptiveColor base lipgloss.Style } // TagStyle returns the pill style for a tag, colour chosen deterministically by // the tag name so the same tag is always the same colour. An empty tag name // (used for the Favourites/untagged sections) gets a neutral style. func (t Theme) TagStyle(tag string) lipgloss.Style { if tag == "" { return t.Meta } h := fnv.New32a() _, _ = h.Write([]byte(tag)) c := t.tagPalette[int(h.Sum32())%len(t.tagPalette)] return t.base.Foreground(c) } // adaptive picks a colour for dark vs light backgrounds. func adaptive(dark, light string) lipgloss.AdaptiveColor { return lipgloss.AdaptiveColor{Dark: dark, Light: light} } // New returns the theme for the given name ("dark" or "light"). Both are built // from the same high-contrast palette; lipgloss adapts per background so a // single definition stays readable either way. func New(name string) Theme { // High-contrast, deliberately bright accents for low-light legibility. accent := adaptive("#7DD3FC", "#0369A1") // sky — highlighted alias accent2 := adaptive("#FDE047", "#A16207") // amber — numbers/stars fg := adaptive("#F1F5F9", "#0F172A") // primary text (near-white / near-black) muted := adaptive("#CBD5E1", "#334155") // secondary text — still clearly readable faint := adaptive("#94A3B8", "#475569") // hints; the dimmest we allow tag := adaptive("#86EFAC", "#15803D") // green tags warn := adaptive("#FCA5A5", "#B91C1C") base := lipgloss.NewStyle() // Distinct, high-contrast hues for tag pills (dark / light variants). tagPalette := []lipgloss.AdaptiveColor{ adaptive("#7DD3FC", "#0369A1"), // sky adaptive("#86EFAC", "#15803D"), // green adaptive("#FDBA74", "#C2410C"), // orange adaptive("#F9A8D4", "#BE185D"), // pink adaptive("#C4B5FD", "#6D28D9"), // violet adaptive("#5EEAD4", "#0F766E"), // teal adaptive("#FDE047", "#A16207"), // amber adaptive("#FCA5A5", "#B91C1C"), // red } return Theme{ base: base, tagPalette: tagPalette, Title: base.Bold(true).Foreground(fg), Meta: base.Foreground(faint), Number: base.Foreground(faint), NumberSel: base.Bold(true).Foreground(accent2), Alias: base.Foreground(fg), AliasSel: base.Bold(true).Foreground(accent), Detail: base.Foreground(muted), DetailSel: base.Foreground(muted), Tag: base.Foreground(tag), Star: base.Foreground(accent2), Cursor: base.Bold(true).Foreground(accent), Preview: base.BorderStyle(lipgloss.RoundedBorder()).BorderForeground(faint).Padding(0, 2), PreviewK: base.Foreground(faint), PreviewV: base.Foreground(fg), FilterBar: base.Bold(true).Foreground(accent2), Help: base.Foreground(faint), Warn: base.Bold(true).Foreground(warn), } }