feat: introduce theme management and performance improvements (#7)
This commit is contained in:
@@ -3,11 +3,12 @@ package cmd
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"github.com/karol-broda/snitch/internal/config"
|
||||
|
||||
"github.com/karol-broda/snitch/internal/config"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
|
||||
var (
|
||||
cfgFile string
|
||||
)
|
||||
@@ -42,7 +43,7 @@ func init() {
|
||||
|
||||
// add top's flags to root so `snitch -l` works (defaults to top command)
|
||||
cfg := config.Get()
|
||||
rootCmd.Flags().StringVar(&topTheme, "theme", cfg.Defaults.Theme, "Theme for TUI (dark, light, mono, auto)")
|
||||
rootCmd.Flags().StringVar(&topTheme, "theme", cfg.Defaults.Theme, "Theme for TUI (see 'snitch themes')")
|
||||
rootCmd.Flags().DurationVarP(&topInterval, "interval", "i", 0, "Refresh interval (default 1s)")
|
||||
rootCmd.Flags().BoolVar(&topResolveAddrs, "resolve-addrs", !cfg.Defaults.Numeric, "Resolve IP addresses to hostnames")
|
||||
rootCmd.Flags().BoolVar(&topResolvePorts, "resolve-ports", false, "Resolve port numbers to service names")
|
||||
|
||||
24
cmd/themes.go
Normal file
24
cmd/themes.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/karol-broda/snitch/internal/theme"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var themesCmd = &cobra.Command{
|
||||
Use: "themes",
|
||||
Short: "List available themes",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
fmt.Printf("Available themes (default: %s):\n\n", theme.DefaultTheme)
|
||||
for _, name := range theme.ListThemes() {
|
||||
fmt.Printf(" %s\n", name)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(themesCmd)
|
||||
}
|
||||
|
||||
@@ -2,11 +2,11 @@ package cmd
|
||||
|
||||
import (
|
||||
"log"
|
||||
"github.com/karol-broda/snitch/internal/config"
|
||||
"github.com/karol-broda/snitch/internal/tui"
|
||||
"time"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/karol-broda/snitch/internal/config"
|
||||
"github.com/karol-broda/snitch/internal/tui"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -60,7 +60,7 @@ func init() {
|
||||
cfg := config.Get()
|
||||
|
||||
// top-specific flags
|
||||
topCmd.Flags().StringVar(&topTheme, "theme", cfg.Defaults.Theme, "Theme for TUI (dark, light, mono, auto)")
|
||||
topCmd.Flags().StringVar(&topTheme, "theme", cfg.Defaults.Theme, "Theme for TUI (see 'snitch themes')")
|
||||
topCmd.Flags().DurationVarP(&topInterval, "interval", "i", time.Second, "Refresh interval")
|
||||
topCmd.Flags().BoolVar(&topResolveAddrs, "resolve-addrs", !cfg.Defaults.Numeric, "Resolve IP addresses to hostnames")
|
||||
topCmd.Flags().BoolVar(&topResolvePorts, "resolve-ports", false, "Resolve port numbers to service names")
|
||||
|
||||
@@ -11,21 +11,76 @@ import (
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
// set SNITCH_DEBUG_TIMING=1 to enable timing diagnostics
|
||||
var debugTiming = os.Getenv("SNITCH_DEBUG_TIMING") != ""
|
||||
|
||||
func logTiming(label string, start time.Time, extra ...string) {
|
||||
if !debugTiming {
|
||||
return
|
||||
}
|
||||
elapsed := time.Since(start)
|
||||
if len(extra) > 0 {
|
||||
fmt.Fprintf(os.Stderr, "[timing] %s: %v (%s)\n", label, elapsed, extra[0])
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, "[timing] %s: %v\n", label, elapsed)
|
||||
}
|
||||
}
|
||||
|
||||
// userCache caches uid to username mappings to avoid repeated lookups
|
||||
var userCache = struct {
|
||||
sync.RWMutex
|
||||
m map[int]string
|
||||
}{m: make(map[int]string)}
|
||||
|
||||
func lookupUsername(uid int) string {
|
||||
userCache.RLock()
|
||||
if username, exists := userCache.m[uid]; exists {
|
||||
userCache.RUnlock()
|
||||
return username
|
||||
}
|
||||
userCache.RUnlock()
|
||||
|
||||
start := time.Now()
|
||||
username := strconv.Itoa(uid)
|
||||
u, err := user.LookupId(strconv.Itoa(uid))
|
||||
if err == nil && u != nil {
|
||||
username = u.Username
|
||||
}
|
||||
elapsed := time.Since(start)
|
||||
if debugTiming && elapsed > 10*time.Millisecond {
|
||||
fmt.Fprintf(os.Stderr, "[timing] user.LookupId(%d) slow: %v\n", uid, elapsed)
|
||||
}
|
||||
|
||||
userCache.Lock()
|
||||
userCache.m[uid] = username
|
||||
userCache.Unlock()
|
||||
|
||||
return username
|
||||
}
|
||||
|
||||
// DefaultCollector implements the Collector interface using /proc filesystem
|
||||
type DefaultCollector struct{}
|
||||
|
||||
// GetConnections fetches all network connections by parsing /proc files
|
||||
func (dc *DefaultCollector) GetConnections() ([]Connection, error) {
|
||||
totalStart := time.Now()
|
||||
defer func() { logTiming("GetConnections total", totalStart) }()
|
||||
|
||||
inodeStart := time.Now()
|
||||
inodeMap, err := buildInodeToProcessMap()
|
||||
logTiming("buildInodeToProcessMap", inodeStart, fmt.Sprintf("%d inodes", len(inodeMap)))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to build inode map: %w", err)
|
||||
}
|
||||
|
||||
var connections []Connection
|
||||
|
||||
parseStart := time.Now()
|
||||
tcpConns, err := parseProcNet("/proc/net/tcp", "tcp", 4, inodeMap)
|
||||
if err == nil {
|
||||
connections = append(connections, tcpConns...)
|
||||
@@ -45,6 +100,7 @@ func (dc *DefaultCollector) GetConnections() ([]Connection, error) {
|
||||
if err == nil {
|
||||
connections = append(connections, udpConns6...)
|
||||
}
|
||||
logTiming("parseProcNet (all)", parseStart, fmt.Sprintf("%d connections", len(connections)))
|
||||
|
||||
return connections, nil
|
||||
}
|
||||
@@ -71,9 +127,13 @@ type processInfo struct {
|
||||
user string
|
||||
}
|
||||
|
||||
func buildInodeToProcessMap() (map[int64]*processInfo, error) {
|
||||
inodeMap := make(map[int64]*processInfo)
|
||||
type inodeEntry struct {
|
||||
inode int64
|
||||
info *processInfo
|
||||
}
|
||||
|
||||
func buildInodeToProcessMap() (map[int64]*processInfo, error) {
|
||||
readDirStart := time.Now()
|
||||
procDir, err := os.Open("/proc")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -85,47 +145,103 @@ func buildInodeToProcessMap() (map[int64]*processInfo, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// collect pids first
|
||||
pids := make([]int, 0, len(entries))
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
pid, err := strconv.Atoi(entry.Name())
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
pids = append(pids, pid)
|
||||
}
|
||||
logTiming(" readdir /proc", readDirStart, fmt.Sprintf("%d pids", len(pids)))
|
||||
|
||||
pidStr := entry.Name()
|
||||
pid, err := strconv.Atoi(pidStr)
|
||||
// process pids in parallel with limited concurrency
|
||||
scanStart := time.Now()
|
||||
const numWorkers = 8
|
||||
pidChan := make(chan int, len(pids))
|
||||
resultChan := make(chan []inodeEntry, len(pids))
|
||||
|
||||
var totalFDs atomic.Int64
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < numWorkers; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for pid := range pidChan {
|
||||
entries := scanProcessSockets(pid)
|
||||
if len(entries) > 0 {
|
||||
totalFDs.Add(int64(len(entries)))
|
||||
resultChan <- entries
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
for _, pid := range pids {
|
||||
pidChan <- pid
|
||||
}
|
||||
close(pidChan)
|
||||
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(resultChan)
|
||||
}()
|
||||
|
||||
inodeMap := make(map[int64]*processInfo)
|
||||
for entries := range resultChan {
|
||||
for _, e := range entries {
|
||||
inodeMap[e.inode] = e.info
|
||||
}
|
||||
}
|
||||
logTiming(" scan all processes", scanStart, fmt.Sprintf("%d socket fds scanned", totalFDs.Load()))
|
||||
|
||||
return inodeMap, nil
|
||||
}
|
||||
|
||||
func scanProcessSockets(pid int) []inodeEntry {
|
||||
start := time.Now()
|
||||
|
||||
procInfo, err := getProcessInfo(pid)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
pidStr := strconv.Itoa(pid)
|
||||
fdDir := filepath.Join("/proc", pidStr, "fd")
|
||||
fdEntries, err := os.ReadDir(fdDir)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var results []inodeEntry
|
||||
for _, fdEntry := range fdEntries {
|
||||
fdPath := filepath.Join(fdDir, fdEntry.Name())
|
||||
link, err := os.Readlink(fdPath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
procInfo, err := getProcessInfo(pid)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
fdDir := filepath.Join("/proc", pidStr, "fd")
|
||||
fdEntries, err := os.ReadDir(fdDir)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, fdEntry := range fdEntries {
|
||||
fdPath := filepath.Join(fdDir, fdEntry.Name())
|
||||
link, err := os.Readlink(fdPath)
|
||||
if strings.HasPrefix(link, "socket:[") && strings.HasSuffix(link, "]") {
|
||||
inodeStr := link[8 : len(link)-1]
|
||||
inode, err := strconv.ParseInt(inodeStr, 10, 64)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(link, "socket:[") && strings.HasSuffix(link, "]") {
|
||||
inodeStr := link[8 : len(link)-1]
|
||||
inode, err := strconv.ParseInt(inodeStr, 10, 64)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
inodeMap[inode] = procInfo
|
||||
}
|
||||
results = append(results, inodeEntry{inode: inode, info: procInfo})
|
||||
}
|
||||
}
|
||||
|
||||
return inodeMap, nil
|
||||
elapsed := time.Since(start)
|
||||
if debugTiming && elapsed > 20*time.Millisecond {
|
||||
fmt.Fprintf(os.Stderr, "[timing] slow process scan: pid=%d (%s) fds=%d time=%v\n",
|
||||
pid, procInfo.command, len(fdEntries), elapsed)
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
func getProcessInfo(pid int) (*processInfo, error) {
|
||||
@@ -173,12 +289,7 @@ func getProcessInfo(pid int) (*processInfo, error) {
|
||||
uid, err := strconv.Atoi(fields[1])
|
||||
if err == nil {
|
||||
info.uid = uid
|
||||
u, err := user.LookupId(strconv.Itoa(uid))
|
||||
if err == nil {
|
||||
info.user = u.Username
|
||||
} else {
|
||||
info.user = strconv.Itoa(uid)
|
||||
}
|
||||
info.user = lookupUsername(uid)
|
||||
}
|
||||
}
|
||||
break
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
//go:build linux
|
||||
|
||||
package collector
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestGetConnections(t *testing.T) {
|
||||
@@ -13,4 +16,102 @@ func TestGetConnections(t *testing.T) {
|
||||
|
||||
// connections are dynamic, so just verify function succeeded
|
||||
t.Logf("Successfully got %d connections", len(conns))
|
||||
}
|
||||
|
||||
func TestGetConnectionsPerformance(t *testing.T) {
|
||||
// measures performance to catch regressions
|
||||
// run with: go test -v -run TestGetConnectionsPerformance
|
||||
|
||||
const maxDuration = 500 * time.Millisecond
|
||||
const iterations = 5
|
||||
|
||||
// warm up caches first
|
||||
_, err := GetConnections()
|
||||
if err != nil {
|
||||
t.Fatalf("warmup failed: %v", err)
|
||||
}
|
||||
|
||||
var total time.Duration
|
||||
var maxSeen time.Duration
|
||||
|
||||
for i := 0; i < iterations; i++ {
|
||||
start := time.Now()
|
||||
conns, err := GetConnections()
|
||||
elapsed := time.Since(start)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("iteration %d failed: %v", i, err)
|
||||
}
|
||||
|
||||
total += elapsed
|
||||
if elapsed > maxSeen {
|
||||
maxSeen = elapsed
|
||||
}
|
||||
|
||||
t.Logf("iteration %d: %v (%d connections)", i+1, elapsed, len(conns))
|
||||
}
|
||||
|
||||
avg := total / time.Duration(iterations)
|
||||
t.Logf("average: %v, max: %v", avg, maxSeen)
|
||||
|
||||
if maxSeen > maxDuration {
|
||||
t.Errorf("slowest iteration took %v, expected < %v", maxSeen, maxDuration)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetConnectionsColdCache(t *testing.T) {
|
||||
// tests performance with cold user cache
|
||||
// this simulates first run or after cache invalidation
|
||||
|
||||
const maxDuration = 2 * time.Second
|
||||
|
||||
clearUserCache()
|
||||
|
||||
start := time.Now()
|
||||
conns, err := GetConnections()
|
||||
elapsed := time.Since(start)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("GetConnections() failed: %v", err)
|
||||
}
|
||||
|
||||
t.Logf("cold cache: %v (%d connections, %d cached users after)",
|
||||
elapsed, len(conns), userCacheSize())
|
||||
|
||||
if elapsed > maxDuration {
|
||||
t.Errorf("cold cache took %v, expected < %v", elapsed, maxDuration)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkGetConnections(b *testing.B) {
|
||||
// warm cache benchmark - measures typical runtime
|
||||
// run with: go test -bench=BenchmarkGetConnections -benchtime=5s
|
||||
|
||||
// warm up
|
||||
_, _ = GetConnections()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = GetConnections()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkGetConnectionsColdCache(b *testing.B) {
|
||||
// cold cache benchmark - measures worst-case with cache cleared each iteration
|
||||
// run with: go test -bench=BenchmarkGetConnectionsColdCache -benchtime=10s
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
clearUserCache()
|
||||
_, _ = GetConnections()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkBuildInodeMap(b *testing.B) {
|
||||
// benchmarks just the inode map building (most expensive part)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = buildInodeToProcessMap()
|
||||
}
|
||||
}
|
||||
18
internal/collector/collector_test_helpers_linux.go
Normal file
18
internal/collector/collector_test_helpers_linux.go
Normal file
@@ -0,0 +1,18 @@
|
||||
//go:build linux
|
||||
|
||||
package collector
|
||||
|
||||
// clearUserCache clears the user lookup cache for testing
|
||||
func clearUserCache() {
|
||||
userCache.Lock()
|
||||
userCache.m = make(map[int]string)
|
||||
userCache.Unlock()
|
||||
}
|
||||
|
||||
// userCacheSize returns the number of cached user entries
|
||||
func userCacheSize() int {
|
||||
userCache.RLock()
|
||||
defer userCache.RUnlock()
|
||||
return len(userCache.m)
|
||||
}
|
||||
|
||||
@@ -4,8 +4,10 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/karol-broda/snitch/internal/theme"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
@@ -92,7 +94,7 @@ func setDefaults(v *viper.Viper) {
|
||||
v.SetDefault("defaults.interval", "1s")
|
||||
v.SetDefault("defaults.numeric", false)
|
||||
v.SetDefault("defaults.fields", []string{"pid", "process", "user", "proto", "state", "laddr", "lport", "raddr", "rport"})
|
||||
v.SetDefault("defaults.theme", "auto")
|
||||
v.SetDefault("defaults.theme", "ansi")
|
||||
v.SetDefault("defaults.units", "auto")
|
||||
v.SetDefault("defaults.color", "auto")
|
||||
v.SetDefault("defaults.resolve", true)
|
||||
@@ -127,7 +129,7 @@ func Get() *Config {
|
||||
Interval: "1s",
|
||||
Numeric: false,
|
||||
Fields: []string{"pid", "process", "user", "proto", "state", "laddr", "lport", "raddr", "rport"},
|
||||
Theme: "auto",
|
||||
Theme: "ansi",
|
||||
Units: "auto",
|
||||
Color: "auto",
|
||||
Resolve: true,
|
||||
@@ -154,7 +156,9 @@ func (c *Config) GetInterval() time.Duration {
|
||||
|
||||
// CreateExampleConfig creates an example configuration file
|
||||
func CreateExampleConfig(path string) error {
|
||||
exampleConfig := `# snitch configuration file
|
||||
themeList := strings.Join(theme.ListThemes(), ", ")
|
||||
|
||||
exampleConfig := fmt.Sprintf(`# snitch configuration file
|
||||
# See https://github.com/you/snitch for full documentation
|
||||
|
||||
[defaults]
|
||||
@@ -167,8 +171,9 @@ numeric = false
|
||||
# Default fields to display (comma-separated list)
|
||||
fields = ["pid", "process", "user", "proto", "state", "laddr", "lport", "raddr", "rport"]
|
||||
|
||||
# Default theme for TUI (dark, light, mono, auto)
|
||||
theme = "auto"
|
||||
# Default theme for TUI (ansi inherits terminal colors)
|
||||
# Available: %s
|
||||
theme = "%s"
|
||||
|
||||
# Default units for byte display (auto, si, iec)
|
||||
units = "auto"
|
||||
@@ -187,17 +192,17 @@ ipv6 = false
|
||||
no_headers = false
|
||||
output_format = "table"
|
||||
sort_by = ""
|
||||
`
|
||||
`, themeList, theme.DefaultTheme)
|
||||
|
||||
// Ensure directory exists
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
||||
return fmt.Errorf("failed to create config directory: %w", err)
|
||||
}
|
||||
|
||||
|
||||
// Write config file
|
||||
if err := os.WriteFile(path, []byte(exampleConfig), 0644); err != nil {
|
||||
return fmt.Errorf("failed to write config file: %w", err)
|
||||
}
|
||||
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -2,12 +2,16 @@ package resolver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var debugTiming = os.Getenv("SNITCH_DEBUG_TIMING") != ""
|
||||
|
||||
// Resolver handles DNS and service name resolution with caching and timeouts
|
||||
type Resolver struct {
|
||||
timeout time.Duration
|
||||
@@ -41,6 +45,7 @@ func (r *Resolver) ResolveAddr(addr string) string {
|
||||
}
|
||||
|
||||
// Perform resolution with timeout
|
||||
start := time.Now()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), r.timeout)
|
||||
defer cancel()
|
||||
|
||||
@@ -55,6 +60,11 @@ func (r *Resolver) ResolveAddr(addr string) string {
|
||||
}
|
||||
}
|
||||
|
||||
elapsed := time.Since(start)
|
||||
if debugTiming && elapsed > 50*time.Millisecond {
|
||||
fmt.Fprintf(os.Stderr, "[timing] slow DNS lookup: %s -> %s (%v)\n", addr, resolved, elapsed)
|
||||
}
|
||||
|
||||
// Cache the result
|
||||
r.mutex.Lock()
|
||||
r.cache[addr] = resolved
|
||||
|
||||
24
internal/theme/ansi.go
Normal file
24
internal/theme/ansi.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package theme
|
||||
|
||||
// ANSI palette uses standard terminal colors (0-15)
|
||||
// this allows the theme to inherit from the user's terminal color scheme
|
||||
var paletteANSI = Palette{
|
||||
Name: "ansi",
|
||||
|
||||
Fg: "15", // bright white
|
||||
FgMuted: "7", // white
|
||||
FgSubtle: "8", // bright black (gray)
|
||||
Bg: "0", // black
|
||||
BgMuted: "0", // black
|
||||
Border: "8", // bright black (gray)
|
||||
|
||||
Red: "1", // red
|
||||
Green: "2", // green
|
||||
Yellow: "3", // yellow
|
||||
Blue: "4", // blue
|
||||
Magenta: "5", // magenta
|
||||
Cyan: "6", // cyan
|
||||
Orange: "3", // yellow (ansi has no orange, fallback to yellow)
|
||||
Gray: "8", // bright black
|
||||
}
|
||||
|
||||
87
internal/theme/catppuccin.go
Normal file
87
internal/theme/catppuccin.go
Normal file
@@ -0,0 +1,87 @@
|
||||
package theme
|
||||
|
||||
// catppuccin mocha (dark)
|
||||
// https://github.com/catppuccin/catppuccin
|
||||
var paletteCatppuccinMocha = Palette{
|
||||
Name: "catppuccin-mocha",
|
||||
|
||||
Fg: "#cdd6f4", // text
|
||||
FgMuted: "#a6adc8", // subtext0
|
||||
FgSubtle: "#6c7086", // overlay0
|
||||
Bg: "#1e1e2e", // base
|
||||
BgMuted: "#313244", // surface0
|
||||
Border: "#45475a", // surface1
|
||||
|
||||
Red: "#f38ba8",
|
||||
Green: "#a6e3a1",
|
||||
Yellow: "#f9e2af",
|
||||
Blue: "#89b4fa",
|
||||
Magenta: "#cba6f7", // mauve
|
||||
Cyan: "#94e2d5", // teal
|
||||
Orange: "#fab387", // peach
|
||||
Gray: "#585b70", // surface2
|
||||
}
|
||||
|
||||
// catppuccin macchiato (medium-dark)
|
||||
var paletteCatppuccinMacchiato = Palette{
|
||||
Name: "catppuccin-macchiato",
|
||||
|
||||
Fg: "#cad3f5", // text
|
||||
FgMuted: "#a5adcb", // subtext0
|
||||
FgSubtle: "#6e738d", // overlay0
|
||||
Bg: "#24273a", // base
|
||||
BgMuted: "#363a4f", // surface0
|
||||
Border: "#494d64", // surface1
|
||||
|
||||
Red: "#ed8796",
|
||||
Green: "#a6da95",
|
||||
Yellow: "#eed49f",
|
||||
Blue: "#8aadf4",
|
||||
Magenta: "#c6a0f6", // mauve
|
||||
Cyan: "#8bd5ca", // teal
|
||||
Orange: "#f5a97f", // peach
|
||||
Gray: "#5b6078", // surface2
|
||||
}
|
||||
|
||||
// catppuccin frappe (medium)
|
||||
var paletteCatppuccinFrappe = Palette{
|
||||
Name: "catppuccin-frappe",
|
||||
|
||||
Fg: "#c6d0f5", // text
|
||||
FgMuted: "#a5adce", // subtext0
|
||||
FgSubtle: "#737994", // overlay0
|
||||
Bg: "#303446", // base
|
||||
BgMuted: "#414559", // surface0
|
||||
Border: "#51576d", // surface1
|
||||
|
||||
Red: "#e78284",
|
||||
Green: "#a6d189",
|
||||
Yellow: "#e5c890",
|
||||
Blue: "#8caaee",
|
||||
Magenta: "#ca9ee6", // mauve
|
||||
Cyan: "#81c8be", // teal
|
||||
Orange: "#ef9f76", // peach
|
||||
Gray: "#626880", // surface2
|
||||
}
|
||||
|
||||
// catppuccin latte (light)
|
||||
var paletteCatppuccinLatte = Palette{
|
||||
Name: "catppuccin-latte",
|
||||
|
||||
Fg: "#4c4f69", // text
|
||||
FgMuted: "#6c6f85", // subtext0
|
||||
FgSubtle: "#9ca0b0", // overlay0
|
||||
Bg: "#eff1f5", // base
|
||||
BgMuted: "#ccd0da", // surface0
|
||||
Border: "#bcc0cc", // surface1
|
||||
|
||||
Red: "#d20f39",
|
||||
Green: "#40a02b",
|
||||
Yellow: "#df8e1d",
|
||||
Blue: "#1e66f5",
|
||||
Magenta: "#8839ef", // mauve
|
||||
Cyan: "#179299", // teal
|
||||
Orange: "#fe640b", // peach
|
||||
Gray: "#acb0be", // surface2
|
||||
}
|
||||
|
||||
24
internal/theme/dracula.go
Normal file
24
internal/theme/dracula.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package theme
|
||||
|
||||
// dracula theme
|
||||
// https://draculatheme.com/
|
||||
var paletteDracula = Palette{
|
||||
Name: "dracula",
|
||||
|
||||
Fg: "#f8f8f2", // foreground
|
||||
FgMuted: "#f8f8f2", // foreground
|
||||
FgSubtle: "#6272a4", // comment
|
||||
Bg: "#282a36", // background
|
||||
BgMuted: "#44475a", // selection
|
||||
Border: "#44475a", // selection
|
||||
|
||||
Red: "#ff5555",
|
||||
Green: "#50fa7b",
|
||||
Yellow: "#f1fa8c",
|
||||
Blue: "#6272a4", // dracula uses comment color for blue tones
|
||||
Magenta: "#bd93f9", // purple
|
||||
Cyan: "#8be9fd",
|
||||
Orange: "#ffb86c",
|
||||
Gray: "#6272a4", // comment
|
||||
}
|
||||
|
||||
45
internal/theme/gruvbox.go
Normal file
45
internal/theme/gruvbox.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package theme
|
||||
|
||||
// gruvbox dark
|
||||
// https://github.com/morhetz/gruvbox
|
||||
var paletteGruvboxDark = Palette{
|
||||
Name: "gruvbox-dark",
|
||||
|
||||
Fg: "#ebdbb2", // fg
|
||||
FgMuted: "#d5c4a1", // fg2
|
||||
FgSubtle: "#a89984", // fg4
|
||||
Bg: "#282828", // bg
|
||||
BgMuted: "#3c3836", // bg1
|
||||
Border: "#504945", // bg2
|
||||
|
||||
Red: "#fb4934",
|
||||
Green: "#b8bb26",
|
||||
Yellow: "#fabd2f",
|
||||
Blue: "#83a598",
|
||||
Magenta: "#d3869b", // purple
|
||||
Cyan: "#8ec07c", // aqua
|
||||
Orange: "#fe8019",
|
||||
Gray: "#928374",
|
||||
}
|
||||
|
||||
// gruvbox light
|
||||
var paletteGruvboxLight = Palette{
|
||||
Name: "gruvbox-light",
|
||||
|
||||
Fg: "#3c3836", // fg
|
||||
FgMuted: "#504945", // fg2
|
||||
FgSubtle: "#7c6f64", // fg4
|
||||
Bg: "#fbf1c7", // bg
|
||||
BgMuted: "#ebdbb2", // bg1
|
||||
Border: "#d5c4a1", // bg2
|
||||
|
||||
Red: "#cc241d",
|
||||
Green: "#98971a",
|
||||
Yellow: "#d79921",
|
||||
Blue: "#458588",
|
||||
Magenta: "#b16286", // purple
|
||||
Cyan: "#689d6a", // aqua
|
||||
Orange: "#d65d0e",
|
||||
Gray: "#928374",
|
||||
}
|
||||
|
||||
49
internal/theme/mono.go
Normal file
49
internal/theme/mono.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package theme
|
||||
|
||||
import "github.com/charmbracelet/lipgloss"
|
||||
|
||||
// createMonoTheme creates a monochrome theme (no colors)
|
||||
// useful for accessibility, piping output, or minimal terminals
|
||||
func createMonoTheme() *Theme {
|
||||
baseStyle := lipgloss.NewStyle()
|
||||
boldStyle := lipgloss.NewStyle().Bold(true)
|
||||
|
||||
return &Theme{
|
||||
Name: "mono",
|
||||
Styles: Styles{
|
||||
Header: boldStyle,
|
||||
Border: baseStyle,
|
||||
Selected: boldStyle,
|
||||
Watched: 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,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
24
internal/theme/nord.go
Normal file
24
internal/theme/nord.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package theme
|
||||
|
||||
// nord theme
|
||||
// https://www.nordtheme.com/
|
||||
var paletteNord = Palette{
|
||||
Name: "nord",
|
||||
|
||||
Fg: "#eceff4", // snow storm - nord6
|
||||
FgMuted: "#d8dee9", // snow storm - nord4
|
||||
FgSubtle: "#4c566a", // polar night - nord3
|
||||
Bg: "#2e3440", // polar night - nord0
|
||||
BgMuted: "#3b4252", // polar night - nord1
|
||||
Border: "#434c5e", // polar night - nord2
|
||||
|
||||
Red: "#bf616a", // aurora - nord11
|
||||
Green: "#a3be8c", // aurora - nord14
|
||||
Yellow: "#ebcb8b", // aurora - nord13
|
||||
Blue: "#81a1c1", // frost - nord9
|
||||
Magenta: "#b48ead", // aurora - nord15
|
||||
Cyan: "#88c0d0", // frost - nord8
|
||||
Orange: "#d08770", // aurora - nord12
|
||||
Gray: "#4c566a", // polar night - nord3
|
||||
}
|
||||
|
||||
24
internal/theme/one_dark.go
Normal file
24
internal/theme/one_dark.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package theme
|
||||
|
||||
// one dark theme (atom editor)
|
||||
// https://github.com/atom/atom/tree/master/packages/one-dark-syntax
|
||||
var paletteOneDark = Palette{
|
||||
Name: "one-dark",
|
||||
|
||||
Fg: "#abb2bf", // foreground
|
||||
FgMuted: "#9da5b4", // foreground muted
|
||||
FgSubtle: "#5c6370", // comment
|
||||
Bg: "#282c34", // background
|
||||
BgMuted: "#21252b", // gutter background
|
||||
Border: "#3e4451", // selection
|
||||
|
||||
Red: "#e06c75",
|
||||
Green: "#98c379",
|
||||
Yellow: "#e5c07b",
|
||||
Blue: "#61afef",
|
||||
Magenta: "#c678dd", // purple
|
||||
Cyan: "#56b6c2",
|
||||
Orange: "#d19a66",
|
||||
Gray: "#5c6370", // comment
|
||||
}
|
||||
|
||||
111
internal/theme/palette.go
Normal file
111
internal/theme/palette.go
Normal file
@@ -0,0 +1,111 @@
|
||||
package theme
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
// Palette defines the semantic colors for a theme
|
||||
type Palette struct {
|
||||
Name string
|
||||
|
||||
// base colors
|
||||
Fg string // primary foreground
|
||||
FgMuted string // secondary/muted foreground
|
||||
FgSubtle string // subtle/disabled foreground
|
||||
Bg string // primary background
|
||||
BgMuted string // secondary background (selections, highlights)
|
||||
Border string // border color
|
||||
|
||||
// semantic colors
|
||||
Red string
|
||||
Green string
|
||||
Yellow string
|
||||
Blue string
|
||||
Magenta string
|
||||
Cyan string
|
||||
Orange string
|
||||
Gray string
|
||||
}
|
||||
|
||||
// Color converts a palette color string to a lipgloss.TerminalColor.
|
||||
// If the string is 1-2 characters, it's treated as an ANSI color code.
|
||||
// Otherwise, it's treated as a hex color.
|
||||
func (p *Palette) Color(c string) lipgloss.TerminalColor {
|
||||
if c == "" {
|
||||
return lipgloss.NoColor{}
|
||||
}
|
||||
|
||||
if len(c) <= 2 {
|
||||
n, err := strconv.Atoi(c)
|
||||
if err == nil {
|
||||
return lipgloss.ANSIColor(n)
|
||||
}
|
||||
}
|
||||
|
||||
return lipgloss.Color(c)
|
||||
}
|
||||
|
||||
// ToTheme converts a Palette to a Theme with lipgloss styles
|
||||
func (p *Palette) ToTheme() *Theme {
|
||||
return &Theme{
|
||||
Name: p.Name,
|
||||
Styles: Styles{
|
||||
Header: lipgloss.NewStyle().
|
||||
Bold(true).
|
||||
Foreground(p.Color(p.Fg)),
|
||||
|
||||
Border: lipgloss.NewStyle().
|
||||
Foreground(p.Color(p.Border)),
|
||||
|
||||
Selected: lipgloss.NewStyle().
|
||||
Bold(true).
|
||||
Foreground(p.Color(p.Fg)),
|
||||
|
||||
Watched: lipgloss.NewStyle().
|
||||
Bold(true).
|
||||
Foreground(p.Color(p.Orange)),
|
||||
|
||||
Normal: lipgloss.NewStyle().
|
||||
Foreground(p.Color(p.FgMuted)),
|
||||
|
||||
Error: lipgloss.NewStyle().
|
||||
Foreground(p.Color(p.Red)),
|
||||
|
||||
Success: lipgloss.NewStyle().
|
||||
Foreground(p.Color(p.Green)),
|
||||
|
||||
Warning: lipgloss.NewStyle().
|
||||
Foreground(p.Color(p.Yellow)),
|
||||
|
||||
Footer: lipgloss.NewStyle().
|
||||
Foreground(p.Color(p.FgSubtle)),
|
||||
|
||||
Background: lipgloss.NewStyle(),
|
||||
|
||||
Proto: ProtoStyles{
|
||||
TCP: lipgloss.NewStyle().Foreground(p.Color(p.Green)),
|
||||
UDP: lipgloss.NewStyle().Foreground(p.Color(p.Magenta)),
|
||||
Unix: lipgloss.NewStyle().Foreground(p.Color(p.Gray)),
|
||||
TCP6: lipgloss.NewStyle().Foreground(p.Color(p.Cyan)),
|
||||
UDP6: lipgloss.NewStyle().Foreground(p.Color(p.Blue)),
|
||||
},
|
||||
|
||||
State: StateStyles{
|
||||
Listen: lipgloss.NewStyle().Foreground(p.Color(p.Green)),
|
||||
Established: lipgloss.NewStyle().Foreground(p.Color(p.Blue)),
|
||||
TimeWait: lipgloss.NewStyle().Foreground(p.Color(p.Yellow)),
|
||||
CloseWait: lipgloss.NewStyle().Foreground(p.Color(p.Orange)),
|
||||
SynSent: lipgloss.NewStyle().Foreground(p.Color(p.Magenta)),
|
||||
SynRecv: lipgloss.NewStyle().Foreground(p.Color(p.Magenta)),
|
||||
FinWait1: lipgloss.NewStyle().Foreground(p.Color(p.Red)),
|
||||
FinWait2: lipgloss.NewStyle().Foreground(p.Color(p.Red)),
|
||||
Closing: lipgloss.NewStyle().Foreground(p.Color(p.Red)),
|
||||
LastAck: lipgloss.NewStyle().Foreground(p.Color(p.Red)),
|
||||
Closed: lipgloss.NewStyle().Foreground(p.Color(p.Gray)),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
14
internal/theme/readme.md
Normal file
14
internal/theme/readme.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# theme Palettes
|
||||
|
||||
the color palettes in this directory were generated by an LLM agent (Claude Opus 4.5) using web search to fetch the official color specifications from each themes documentation
|
||||
as it is with llm agents its possible the colors may be wrong
|
||||
|
||||
Sources:
|
||||
- [Catppuccin](https://github.com/catppuccin/catppuccin)
|
||||
- [Dracula](https://draculatheme.com/)
|
||||
- [Gruvbox](https://github.com/morhetz/gruvbox)
|
||||
- [Nord](https://www.nordtheme.com/)
|
||||
- [One Dark](https://github.com/atom/one-dark-syntax)
|
||||
- [Solarized](https://ethanschoonover.com/solarized/)
|
||||
- [Tokyo Night](https://github.com/enkia/tokyo-night-vscode-theme)
|
||||
|
||||
45
internal/theme/solarized.go
Normal file
45
internal/theme/solarized.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package theme
|
||||
|
||||
// solarized dark theme
|
||||
// https://ethanschoonover.com/solarized/
|
||||
var paletteSolarizedDark = Palette{
|
||||
Name: "solarized-dark",
|
||||
|
||||
Fg: "#839496", // base0
|
||||
FgMuted: "#93a1a1", // base1
|
||||
FgSubtle: "#586e75", // base01
|
||||
Bg: "#002b36", // base03
|
||||
BgMuted: "#073642", // base02
|
||||
Border: "#073642", // base02
|
||||
|
||||
Red: "#dc322f",
|
||||
Green: "#859900",
|
||||
Yellow: "#b58900",
|
||||
Blue: "#268bd2",
|
||||
Magenta: "#d33682",
|
||||
Cyan: "#2aa198",
|
||||
Orange: "#cb4b16",
|
||||
Gray: "#657b83", // base00
|
||||
}
|
||||
|
||||
// solarized light theme
|
||||
var paletteSolarizedLight = Palette{
|
||||
Name: "solarized-light",
|
||||
|
||||
Fg: "#657b83", // base00
|
||||
FgMuted: "#586e75", // base01
|
||||
FgSubtle: "#93a1a1", // base1
|
||||
Bg: "#fdf6e3", // base3
|
||||
BgMuted: "#eee8d5", // base2
|
||||
Border: "#eee8d5", // base2
|
||||
|
||||
Red: "#dc322f",
|
||||
Green: "#859900",
|
||||
Yellow: "#b58900",
|
||||
Blue: "#268bd2",
|
||||
Magenta: "#d33682",
|
||||
Cyan: "#2aa198",
|
||||
Orange: "#cb4b16",
|
||||
Gray: "#839496", // base0
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package theme
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
@@ -52,152 +53,73 @@ type StateStyles struct {
|
||||
Closed lipgloss.Style
|
||||
}
|
||||
|
||||
var (
|
||||
themes map[string]*Theme
|
||||
)
|
||||
var themes map[string]*Theme
|
||||
|
||||
func init() {
|
||||
themes = map[string]*Theme{
|
||||
"default": createAdaptiveTheme(),
|
||||
"mono": createMonoTheme(),
|
||||
}
|
||||
themes = make(map[string]*Theme)
|
||||
|
||||
// ansi theme (default) - inherits from terminal colors
|
||||
themes["ansi"] = paletteANSI.ToTheme()
|
||||
|
||||
// catppuccin variants
|
||||
themes["catppuccin-mocha"] = paletteCatppuccinMocha.ToTheme()
|
||||
themes["catppuccin-macchiato"] = paletteCatppuccinMacchiato.ToTheme()
|
||||
themes["catppuccin-frappe"] = paletteCatppuccinFrappe.ToTheme()
|
||||
themes["catppuccin-latte"] = paletteCatppuccinLatte.ToTheme()
|
||||
|
||||
// gruvbox variants
|
||||
themes["gruvbox-dark"] = paletteGruvboxDark.ToTheme()
|
||||
themes["gruvbox-light"] = paletteGruvboxLight.ToTheme()
|
||||
|
||||
// dracula
|
||||
themes["dracula"] = paletteDracula.ToTheme()
|
||||
|
||||
// nord
|
||||
themes["nord"] = paletteNord.ToTheme()
|
||||
|
||||
// tokyo night variants
|
||||
themes["tokyo-night"] = paletteTokyoNight.ToTheme()
|
||||
themes["tokyo-night-storm"] = paletteTokyoNightStorm.ToTheme()
|
||||
themes["tokyo-night-light"] = paletteTokyoNightLight.ToTheme()
|
||||
|
||||
// solarized variants
|
||||
themes["solarized-dark"] = paletteSolarizedDark.ToTheme()
|
||||
themes["solarized-light"] = paletteSolarizedLight.ToTheme()
|
||||
|
||||
// one dark
|
||||
themes["one-dark"] = paletteOneDark.ToTheme()
|
||||
|
||||
// monochrome (no colors)
|
||||
themes["mono"] = createMonoTheme()
|
||||
}
|
||||
|
||||
// GetTheme returns a theme by name, with auto-detection support
|
||||
// DefaultTheme is the theme used when none is specified
|
||||
const DefaultTheme = "ansi"
|
||||
|
||||
// GetTheme returns a theme by name
|
||||
func GetTheme(name string) *Theme {
|
||||
if name == "auto" {
|
||||
// lipgloss handles adaptive colors, so we just return the default
|
||||
return themes["default"]
|
||||
if name == "" || name == "auto" || name == "default" {
|
||||
return themes[DefaultTheme]
|
||||
}
|
||||
|
||||
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"]
|
||||
return themes[DefaultTheme]
|
||||
}
|
||||
|
||||
// ListThemes returns available theme names
|
||||
// ListThemes returns available theme names sorted alphabetically
|
||||
func ListThemes() []string {
|
||||
var names []string
|
||||
names := make([]string, 0, len(themes))
|
||||
for name := range themes {
|
||||
names = append(names, name)
|
||||
}
|
||||
sort.Strings(names)
|
||||
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) {
|
||||
|
||||
66
internal/theme/tokyo_night.go
Normal file
66
internal/theme/tokyo_night.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package theme
|
||||
|
||||
// tokyo night theme
|
||||
// https://github.com/enkia/tokyo-night-vscode-theme
|
||||
var paletteTokyoNight = Palette{
|
||||
Name: "tokyo-night",
|
||||
|
||||
Fg: "#c0caf5", // foreground
|
||||
FgMuted: "#a9b1d6", // foreground dark
|
||||
FgSubtle: "#565f89", // comment
|
||||
Bg: "#1a1b26", // background
|
||||
BgMuted: "#24283b", // background highlight
|
||||
Border: "#414868", // border
|
||||
|
||||
Red: "#f7768e",
|
||||
Green: "#9ece6a",
|
||||
Yellow: "#e0af68",
|
||||
Blue: "#7aa2f7",
|
||||
Magenta: "#bb9af7", // purple
|
||||
Cyan: "#7dcfff",
|
||||
Orange: "#ff9e64",
|
||||
Gray: "#565f89", // comment
|
||||
}
|
||||
|
||||
// tokyo night storm variant
|
||||
var paletteTokyoNightStorm = Palette{
|
||||
Name: "tokyo-night-storm",
|
||||
|
||||
Fg: "#c0caf5", // foreground
|
||||
FgMuted: "#a9b1d6", // foreground dark
|
||||
FgSubtle: "#565f89", // comment
|
||||
Bg: "#24283b", // background (storm is slightly lighter)
|
||||
BgMuted: "#1f2335", // background dark
|
||||
Border: "#414868", // border
|
||||
|
||||
Red: "#f7768e",
|
||||
Green: "#9ece6a",
|
||||
Yellow: "#e0af68",
|
||||
Blue: "#7aa2f7",
|
||||
Magenta: "#bb9af7", // purple
|
||||
Cyan: "#7dcfff",
|
||||
Orange: "#ff9e64",
|
||||
Gray: "#565f89", // comment
|
||||
}
|
||||
|
||||
// tokyo night light variant
|
||||
var paletteTokyoNightLight = Palette{
|
||||
Name: "tokyo-night-light",
|
||||
|
||||
Fg: "#343b58", // foreground
|
||||
FgMuted: "#565a6e", // foreground dark
|
||||
FgSubtle: "#9699a3", // comment
|
||||
Bg: "#d5d6db", // background
|
||||
BgMuted: "#cbccd1", // background highlight
|
||||
Border: "#b4b5b9", // border
|
||||
|
||||
Red: "#8c4351",
|
||||
Green: "#485e30",
|
||||
Yellow: "#8f5e15",
|
||||
Blue: "#34548a",
|
||||
Magenta: "#5a4a78", // purple
|
||||
Cyan: "#0f4b6e",
|
||||
Orange: "#965027",
|
||||
Gray: "#9699a3", // comment
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user