Files
termdoku/internal/theme/theme.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)
}