8af44dfcd2
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>
106 lines
4.0 KiB
Go
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),
|
|
}
|
|
}
|