package theme import ( "errors" "io/fs" "os" "path/filepath" "strings" "github.com/BurntSushi/toml" "github.com/charmbracelet/lipgloss" "github.com/muesli/termenv" ) type Palette struct { Background string Foreground string GridLine string CellBaseBG string CellBaseFG string CellFixedFG string CellFixedBG string CellSelectedBG string CellSelectedFG string CellDuplicateBG string CellConflictBG string Accent string } type Theme struct { Name string Palette Palette } // Light defines a theme inspired by Solarized Light. func Light() Theme { return Theme{ Name: "solarized-light", Palette: Palette{ Background: "#fdf6e3", // base3 Foreground: "#586e75", // base01 GridLine: "#93a1a1", // base1 CellBaseBG: "", CellBaseFG: "#586e75", // base01 CellFixedFG: "#839496", // base0 CellFixedBG: "", CellSelectedBG: "#eee8d5", // base2 CellSelectedFG: "#586e75", // base01 CellDuplicateBG: "#f5e8c1", // Slightly darker variant CellConflictBG: "#ffe0e0", // Reddish conflict, still light Accent: "#dc322f", // red }, } } // Darcula returns a dark theme inspired by Darcula. func Darcula() Theme { return Theme{ Name: "dracula", Palette: Palette{ Background: "#282a36", // background Foreground: "#f8f8f2", // foreground GridLine: "#44475a", // current line CellBaseBG: "", CellBaseFG: "#f8f8f2", // foreground CellFixedFG: "#6272a4", // comment CellFixedBG: "", CellSelectedBG: "#44475a", // current line CellSelectedFG: "#f8f8f2", // foreground CellDuplicateBG: "#50fa7b", // green - using a highlight for duplicate CellConflictBG: "#ff5555", // red Accent: "#bd93f9", // purple }, } } // DetectTheme automatically detects the terminal background and returns appropriate theme func DetectTheme() Theme { if hasLightBackground() { return Light() } return Darcula() } // hasLightBackground attempts to detect if the terminal has a light background func hasLightBackground() bool { // Primary detection using termenv if !termenv.HasDarkBackground() { return true } // Additional environment variable checks term := strings.ToLower(os.Getenv("TERM")) colorterm := strings.ToLower(os.Getenv("COLORTERM")) termProgram := strings.ToLower(os.Getenv("TERM_PROGRAM")) // Check for light theme indicators in environment variables lightIndicators := []string{"light", "bright", "white"} for _, indicator := range lightIndicators { if strings.Contains(term, indicator) || strings.Contains(colorterm, indicator) || strings.Contains(termProgram, indicator) { return true } } // Check iTerm2 profile if iterm2Profile := strings.ToLower(os.Getenv("ITERM_PROFILE")); iterm2Profile != "" { for _, indicator := range lightIndicators { if strings.Contains(iterm2Profile, indicator) { return true } } } return false } // AdaptiveColors provides theme-aware color mappings type AdaptiveColors struct { theme Theme } func NewAdaptiveColors(t Theme) AdaptiveColors { return AdaptiveColors{theme: t} } // GetDifficultyColors returns colors for each difficulty level adapted to the theme func (ac AdaptiveColors) GetDifficultyColors() map[string]string { if ac.theme.Name == "solarized-light" { // Solarized Light theme colors return map[string]string{ "Easy": "#2aa198", // cyan "Normal": "#859900", // green "Hard": "#cb4b16", // orange "Lunatic": "#6c71c4", // violet "Daily": "#859900", // green } } // Dracula theme colors return map[string]string{ "Easy": "#8be9fd", // cyan "Normal": "#50fa7b", // green "Hard": "#ffb86c", // orange "Lunatic": "#bd93f9", // purple "Daily": "#50fa7b", // green } } // GetGradientColors returns gradient color pairs adapted to the theme func (ac AdaptiveColors) GetGradientColors() map[string][2]string { if ac.theme.Name == "solarized-light" { // Solarized Light theme gradients return map[string][2]string{ "banner": {"#6c71c4", "#b58900"}, // violet to yellow "easy": {"#2aa198", "#268bd2"}, // cyan to blue "normal": {"#859900", "#cb4b16"}, // green to orange "daily": {"#859900", "#cb4b16"}, // green to orange "hard": {"#dc322f", "#cb4b16"}, // red to orange "lunatic": {"#6c71c4", "#d33682"}, // violet to magenta "complete": {"#859900", "#268bd2"}, // success green to blue } } // Dracula theme gradients return map[string][2]string{ "banner": {"#bd93f9", "#ff79c6"}, // purple to pink "easy": {"#8be9fd", "#6272a4"}, // cyan to comment "normal": {"#50fa7b", "#ffb86c"}, // green to orange "daily": {"#50fa7b", "#ffb86c"}, // green to orange "hard": {"#ffb86c", "#ff5555"}, // orange to red "lunatic": {"#bd93f9", "#ff79c6"}, // purple to pink "complete": {"#50fa7b", "#ff79c6"}, // green to pink } } // GetAccentColors returns various accent colors adapted to the theme func (ac AdaptiveColors) GetAccentColors() map[string]string { if ac.theme.Name == "solarized-light" { // Solarized Light theme accents return map[string]string{ "selected": "#cb4b16", // orange "panel": "#839496", // base0 "success": "#859900", // green "error": "#dc322f", // red } } // Dracula theme accents return map[string]string{ "selected": "#ff79c6", // pink "panel": "#44475a", // current line "success": "#50fa7b", // green "error": "#ff5555", // red } } func BaseStyle(t Theme) lipgloss.Style { return lipgloss.NewStyle().Foreground(lipgloss.Color(t.Palette.Foreground)).Background(lipgloss.Color(t.Palette.Background)) } // LoadCustomTheme loads a custom theme from ~/.termdoku/themes/ func LoadCustomTheme(name string) (Theme, error) { h, err := os.UserHomeDir() if err != nil { return Theme{}, err } themePath := filepath.Join(h, ".termdoku", "themes", name+".toml") data, err := os.ReadFile(themePath) if err != nil { if errors.Is(err, fs.ErrNotExist) { return Theme{}, errors.New("custom theme not found: " + name) } return Theme{}, err } var theme Theme if err := toml.Unmarshal(data, &theme); err != nil { return Theme{}, err } // Set name if not specified in file if theme.Name == "" { theme.Name = name } return theme, nil } // GetTheme returns the appropriate theme based on config or auto-detection func GetTheme(configTheme string) Theme { switch configTheme { case "light": return Light() case "dark": return Darcula() case "auto", "": return DetectTheme() default: if theme, err := LoadCustomTheme(configTheme); err == nil { return theme } // Fall back to auto-detection return DetectTheme() } } // CreateExampleTheme creates an example custom theme file for users func CreateExampleTheme() error { h, err := os.UserHomeDir() if err != nil { return err } themesDir := filepath.Join(h, ".termdoku", "themes") if err := os.MkdirAll(themesDir, 0o755); err != nil { return err } examplePath := filepath.Join(themesDir, "example.toml") // Don't overwrite if exists if _, err := os.Stat(examplePath); err == nil { return nil } // Example theme inspired by Monokai example := Theme{ Name: "monokai-example", Palette: Palette{ Background: "#272822", Foreground: "#f8f8f2", GridLine: "#49483e", CellBaseBG: "", CellBaseFG: "#f8f8f2", CellFixedFG: "#75715e", CellFixedBG: "", CellSelectedBG: "#3e3d32", CellSelectedFG: "#f8f8f2", CellDuplicateBG: "#a6e22e", // Green for duplicates CellConflictBG: "#f92672", // Pink/Red for conflicts Accent: "#e6db74", // Yellow accent }, } data, err := toml.Marshal(example) if err != nil { return err } return os.WriteFile(examplePath, data, 0o644) }