opencode/packages/tui/internal/theme/manager.go
2025-06-26 11:32:25 -05:00

229 lines
6.9 KiB
Go

package theme
import (
"fmt"
"image/color"
"slices"
"strconv"
"strings"
"sync"
"github.com/alecthomas/chroma/v2/styles"
"github.com/charmbracelet/lipgloss/v2"
"github.com/charmbracelet/lipgloss/v2/compat"
"github.com/charmbracelet/x/ansi"
)
// Manager handles theme registration, selection, and retrieval.
// It maintains a registry of available themes and tracks the currently active theme.
type Manager struct {
themes map[string]Theme
currentName string
currentUsesAnsiCache bool // Cache whether current theme uses ANSI colors
mu sync.RWMutex
}
// Global instance of the theme manager
var globalManager = &Manager{
themes: make(map[string]Theme),
currentName: "",
}
// RegisterTheme adds a new theme to the registry.
// If this is the first theme registered, it becomes the default.
func RegisterTheme(name string, theme Theme) {
globalManager.mu.Lock()
defer globalManager.mu.Unlock()
globalManager.themes[name] = theme
// If this is the first theme, make it the default
if globalManager.currentName == "" {
globalManager.currentName = name
globalManager.currentUsesAnsiCache = themeUsesAnsiColors(theme)
}
}
// SetTheme changes the active theme to the one with the specified name.
// Returns an error if the theme doesn't exist.
func SetTheme(name string) error {
globalManager.mu.Lock()
defer globalManager.mu.Unlock()
delete(styles.Registry, "charm")
theme, exists := globalManager.themes[name]
if !exists {
return fmt.Errorf("theme '%s' not found", name)
}
globalManager.currentName = name
globalManager.currentUsesAnsiCache = themeUsesAnsiColors(theme)
return nil
}
// CurrentTheme returns the currently active theme.
// If no theme is set, it returns nil.
func CurrentTheme() Theme {
globalManager.mu.RLock()
defer globalManager.mu.RUnlock()
if globalManager.currentName == "" {
return nil
}
return globalManager.themes[globalManager.currentName]
}
// CurrentThemeName returns the name of the currently active theme.
func CurrentThemeName() string {
globalManager.mu.RLock()
defer globalManager.mu.RUnlock()
return globalManager.currentName
}
// AvailableThemes returns a list of all registered theme names.
func AvailableThemes() []string {
globalManager.mu.RLock()
defer globalManager.mu.RUnlock()
names := make([]string, 0, len(globalManager.themes))
for name := range globalManager.themes {
names = append(names, name)
}
slices.SortFunc(names, func(a, b string) int {
if a == "opencode" {
return -1
} else if b == "opencode" {
return 1
}
if a == "system" {
return -1
} else if b == "system" {
return 1
}
return strings.Compare(a, b)
})
return names
}
// GetTheme returns a specific theme by name.
// Returns nil if the theme doesn't exist.
func GetTheme(name string) Theme {
globalManager.mu.RLock()
defer globalManager.mu.RUnlock()
return globalManager.themes[name]
}
// UpdateSystemTheme updates the system theme with terminal background info
func UpdateSystemTheme(terminalBg color.Color, isDark bool) {
globalManager.mu.Lock()
defer globalManager.mu.Unlock()
dynamicTheme := NewSystemTheme(terminalBg, isDark)
globalManager.themes["system"] = dynamicTheme
if globalManager.currentName == "system" {
globalManager.currentUsesAnsiCache = themeUsesAnsiColors(dynamicTheme)
}
}
// CurrentThemeUsesAnsiColors returns true if the current theme uses ANSI 0-16 colors
func CurrentThemeUsesAnsiColors() bool {
// globalManager.mu.RLock()
// defer globalManager.mu.RUnlock()
return globalManager.currentUsesAnsiCache
}
// isAnsiColor checks if a color represents an ANSI 0-16 color
func isAnsiColor(c color.Color) bool {
if _, ok := c.(lipgloss.NoColor); ok {
return false
}
if _, ok := c.(ansi.BasicColor); ok {
return true
}
// For other color types, check if they represent ANSI colors
// by examining their string representation
if stringer, ok := c.(fmt.Stringer); ok {
str := stringer.String()
// Check if it's a numeric ANSI color (0-15)
if num, err := strconv.Atoi(str); err == nil && num >= 0 && num <= 15 {
return true
}
}
return false
}
// adaptiveColorUsesAnsi checks if an AdaptiveColor uses ANSI colors
func adaptiveColorUsesAnsi(ac compat.AdaptiveColor) bool {
if isAnsiColor(ac.Dark) {
return true
}
if isAnsiColor(ac.Light) {
return true
}
return false
}
// themeUsesAnsiColors checks if a theme uses any ANSI 0-16 colors
func themeUsesAnsiColors(theme Theme) bool {
if theme == nil {
return false
}
return adaptiveColorUsesAnsi(theme.Primary()) ||
adaptiveColorUsesAnsi(theme.Secondary()) ||
adaptiveColorUsesAnsi(theme.Accent()) ||
adaptiveColorUsesAnsi(theme.Error()) ||
adaptiveColorUsesAnsi(theme.Warning()) ||
adaptiveColorUsesAnsi(theme.Success()) ||
adaptiveColorUsesAnsi(theme.Info()) ||
adaptiveColorUsesAnsi(theme.Text()) ||
adaptiveColorUsesAnsi(theme.TextMuted()) ||
adaptiveColorUsesAnsi(theme.Background()) ||
adaptiveColorUsesAnsi(theme.BackgroundPanel()) ||
adaptiveColorUsesAnsi(theme.BackgroundElement()) ||
adaptiveColorUsesAnsi(theme.Border()) ||
adaptiveColorUsesAnsi(theme.BorderActive()) ||
adaptiveColorUsesAnsi(theme.BorderSubtle()) ||
adaptiveColorUsesAnsi(theme.DiffAdded()) ||
adaptiveColorUsesAnsi(theme.DiffRemoved()) ||
adaptiveColorUsesAnsi(theme.DiffContext()) ||
adaptiveColorUsesAnsi(theme.DiffHunkHeader()) ||
adaptiveColorUsesAnsi(theme.DiffHighlightAdded()) ||
adaptiveColorUsesAnsi(theme.DiffHighlightRemoved()) ||
adaptiveColorUsesAnsi(theme.DiffAddedBg()) ||
adaptiveColorUsesAnsi(theme.DiffRemovedBg()) ||
adaptiveColorUsesAnsi(theme.DiffContextBg()) ||
adaptiveColorUsesAnsi(theme.DiffLineNumber()) ||
adaptiveColorUsesAnsi(theme.DiffAddedLineNumberBg()) ||
adaptiveColorUsesAnsi(theme.DiffRemovedLineNumberBg()) ||
adaptiveColorUsesAnsi(theme.MarkdownText()) ||
adaptiveColorUsesAnsi(theme.MarkdownHeading()) ||
adaptiveColorUsesAnsi(theme.MarkdownLink()) ||
adaptiveColorUsesAnsi(theme.MarkdownLinkText()) ||
adaptiveColorUsesAnsi(theme.MarkdownCode()) ||
adaptiveColorUsesAnsi(theme.MarkdownBlockQuote()) ||
adaptiveColorUsesAnsi(theme.MarkdownEmph()) ||
adaptiveColorUsesAnsi(theme.MarkdownStrong()) ||
adaptiveColorUsesAnsi(theme.MarkdownHorizontalRule()) ||
adaptiveColorUsesAnsi(theme.MarkdownListItem()) ||
adaptiveColorUsesAnsi(theme.MarkdownListEnumeration()) ||
adaptiveColorUsesAnsi(theme.MarkdownImage()) ||
adaptiveColorUsesAnsi(theme.MarkdownImageText()) ||
adaptiveColorUsesAnsi(theme.MarkdownCodeBlock()) ||
adaptiveColorUsesAnsi(theme.SyntaxComment()) ||
adaptiveColorUsesAnsi(theme.SyntaxKeyword()) ||
adaptiveColorUsesAnsi(theme.SyntaxFunction()) ||
adaptiveColorUsesAnsi(theme.SyntaxVariable()) ||
adaptiveColorUsesAnsi(theme.SyntaxString()) ||
adaptiveColorUsesAnsi(theme.SyntaxNumber()) ||
adaptiveColorUsesAnsi(theme.SyntaxType()) ||
adaptiveColorUsesAnsi(theme.SyntaxOperator()) ||
adaptiveColorUsesAnsi(theme.SyntaxPunctuation())
}