292 lines
7.7 KiB
Go
292 lines
7.7 KiB
Go
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)
|
|
} |