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>
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
// 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),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user