Files
snitch/internal/theme/theme.go
2025-12-16 22:52:07 +01:00

248 lines
6.8 KiB
Go

package theme
import (
"strings"
"github.com/charmbracelet/lipgloss"
)
// Theme represents the visual styling for the TUI
type Theme struct {
Name string
Styles Styles
}
// Styles contains all the styling definitions
type Styles struct {
Header lipgloss.Style
Border lipgloss.Style
Selected lipgloss.Style
Watched lipgloss.Style
Normal lipgloss.Style
Error lipgloss.Style
Success lipgloss.Style
Warning lipgloss.Style
Proto ProtoStyles
State StateStyles
Footer lipgloss.Style
Background lipgloss.Style
}
// ProtoStyles contains protocol-specific colors
type ProtoStyles struct {
TCP lipgloss.Style
UDP lipgloss.Style
Unix lipgloss.Style
TCP6 lipgloss.Style
UDP6 lipgloss.Style
}
// StateStyles contains connection state-specific colors
type StateStyles struct {
Listen lipgloss.Style
Established lipgloss.Style
TimeWait lipgloss.Style
CloseWait lipgloss.Style
SynSent lipgloss.Style
SynRecv lipgloss.Style
FinWait1 lipgloss.Style
FinWait2 lipgloss.Style
Closing lipgloss.Style
LastAck lipgloss.Style
Closed lipgloss.Style
}
var (
themes map[string]*Theme
)
func init() {
themes = map[string]*Theme{
"default": createAdaptiveTheme(),
"mono": createMonoTheme(),
}
}
// GetTheme returns a theme by name, with auto-detection support
func GetTheme(name string) *Theme {
if name == "auto" {
// lipgloss handles adaptive colors, so we just return the default
return themes["default"]
}
if theme, exists := themes[name]; exists {
return theme
}
// a specific theme was requested (e.g. "dark", "light"), but we now use adaptive
// so we can just return the default theme and lipgloss will handle it
if name == "dark" || name == "light" {
return themes["default"]
}
// fallback to default
return themes["default"]
}
// ListThemes returns available theme names
func ListThemes() []string {
var names []string
for name := range themes {
names = append(names, name)
}
return names
}
// createAdaptiveTheme creates a clean, minimal theme
func createAdaptiveTheme() *Theme {
return &Theme{
Name: "default",
Styles: Styles{
Header: lipgloss.NewStyle().
Bold(true).
Foreground(lipgloss.AdaptiveColor{Light: "#1F2937", Dark: "#F9FAFB"}),
Watched: lipgloss.NewStyle().
Bold(true).
Foreground(lipgloss.AdaptiveColor{Light: "#D97706", Dark: "#F59E0B"}),
Border: lipgloss.NewStyle().
Foreground(lipgloss.AdaptiveColor{Light: "#D1D5DB", Dark: "#374151"}),
Selected: lipgloss.NewStyle().
Bold(true).
Foreground(lipgloss.AdaptiveColor{Light: "#1F2937", Dark: "#F9FAFB"}),
Normal: lipgloss.NewStyle().
Foreground(lipgloss.AdaptiveColor{Light: "#6B7280", Dark: "#9CA3AF"}),
Error: lipgloss.NewStyle().
Foreground(lipgloss.AdaptiveColor{Light: "#DC2626", Dark: "#F87171"}),
Success: lipgloss.NewStyle().
Foreground(lipgloss.AdaptiveColor{Light: "#059669", Dark: "#34D399"}),
Warning: lipgloss.NewStyle().
Foreground(lipgloss.AdaptiveColor{Light: "#D97706", Dark: "#FBBF24"}),
Footer: lipgloss.NewStyle().
Foreground(lipgloss.AdaptiveColor{Light: "#9CA3AF", Dark: "#6B7280"}),
Background: lipgloss.NewStyle(),
Proto: ProtoStyles{
TCP: lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#059669", Dark: "#34D399"}),
UDP: lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#7C3AED", Dark: "#A78BFA"}),
Unix: lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#6B7280", Dark: "#9CA3AF"}),
TCP6: lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#059669", Dark: "#34D399"}),
UDP6: lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#7C3AED", Dark: "#A78BFA"}),
},
State: StateStyles{
Listen: lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#059669", Dark: "#34D399"}),
Established: lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#2563EB", Dark: "#60A5FA"}),
TimeWait: lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#D97706", Dark: "#FBBF24"}),
CloseWait: lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#D97706", Dark: "#FBBF24"}),
SynSent: lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#7C3AED", Dark: "#A78BFA"}),
SynRecv: lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#7C3AED", Dark: "#A78BFA"}),
FinWait1: lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#DC2626", Dark: "#F87171"}),
FinWait2: lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#DC2626", Dark: "#F87171"}),
Closing: lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#DC2626", Dark: "#F87171"}),
LastAck: lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#DC2626", Dark: "#F87171"}),
Closed: lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#9CA3AF", Dark: "#6B7280"}),
},
},
}
}
// createMonoTheme creates a monochrome theme (no colors)
func createMonoTheme() *Theme {
baseStyle := lipgloss.NewStyle()
boldStyle := lipgloss.NewStyle().Bold(true)
return &Theme{
Name: "mono",
Styles: Styles{
Header: boldStyle,
Border: baseStyle,
Selected: boldStyle,
Normal: baseStyle,
Error: boldStyle,
Success: boldStyle,
Warning: boldStyle,
Footer: baseStyle,
Background: baseStyle,
Proto: ProtoStyles{
TCP: baseStyle,
UDP: baseStyle,
Unix: baseStyle,
TCP6: baseStyle,
UDP6: baseStyle,
},
State: StateStyles{
Listen: baseStyle,
Established: baseStyle,
TimeWait: baseStyle,
CloseWait: baseStyle,
SynSent: baseStyle,
SynRecv: baseStyle,
FinWait1: baseStyle,
FinWait2: baseStyle,
Closing: baseStyle,
LastAck: baseStyle,
Closed: baseStyle,
},
},
}
}
// GetProtoStyle returns the appropriate style for a protocol
func (s *Styles) GetProtoStyle(proto string) lipgloss.Style {
switch strings.ToLower(proto) {
case "tcp":
return s.Proto.TCP
case "udp":
return s.Proto.UDP
case "unix":
return s.Proto.Unix
case "tcp6":
return s.Proto.TCP6
case "udp6":
return s.Proto.UDP6
default:
return s.Normal
}
}
// GetStateStyle returns the appropriate style for a connection state
func (s *Styles) GetStateStyle(state string) lipgloss.Style {
switch strings.ToUpper(state) {
case "LISTEN":
return s.State.Listen
case "ESTABLISHED":
return s.State.Established
case "TIME_WAIT":
return s.State.TimeWait
case "CLOSE_WAIT":
return s.State.CloseWait
case "SYN_SENT":
return s.State.SynSent
case "SYN_RECV":
return s.State.SynRecv
case "FIN_WAIT1":
return s.State.FinWait1
case "FIN_WAIT2":
return s.State.FinWait2
case "CLOSING":
return s.State.Closing
case "LAST_ACK":
return s.State.LastAck
case "CLOSED":
return s.State.Closed
default:
return s.Normal
}
}