Files
dial/internal/theme/theme.go
T
bsncubed 8af44dfcd2 Initial commit: dial — interactive SSH host picker
A fast, read-only ~/.ssh/config picker that exec's the system ssh client.
Includes: static high-contrast picker with recency/frequency/name sort and
tag grouping, `dial keygen` (native ed25519/RSA + optional config entry), and
an optional offline YubiKey launch gate with one-time recovery codes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 23:18:23 +10:00

106 lines
4.0 KiB
Go

// 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),
}
}