manager.go

  1package theme
  2
  3import (
  4	"fmt"
  5	"image/color"
  6	"slices"
  7	"strings"
  8	"sync"
  9
 10	"github.com/alecthomas/chroma/v2/styles"
 11	"github.com/opencode-ai/opencode/internal/config"
 12	"github.com/opencode-ai/opencode/internal/logging"
 13)
 14
 15// Manager handles theme registration, selection, and retrieval.
 16// It maintains a registry of available themes and tracks the currently active theme.
 17type Manager struct {
 18	themes      map[string]Theme
 19	currentName string
 20	mu          sync.RWMutex
 21}
 22
 23// Global instance of the theme manager
 24var globalManager = &Manager{
 25	themes:      make(map[string]Theme),
 26	currentName: "",
 27}
 28
 29// RegisterTheme adds a new theme to the registry.
 30// If this is the first theme registered, it becomes the default.
 31func RegisterTheme(name string, theme Theme) {
 32	globalManager.mu.Lock()
 33	defer globalManager.mu.Unlock()
 34
 35	globalManager.themes[name] = theme
 36
 37	// If this is the first theme, make it the default
 38	if globalManager.currentName == "" {
 39		globalManager.currentName = name
 40	}
 41}
 42
 43// SetTheme changes the active theme to the one with the specified name.
 44// Returns an error if the theme doesn't exist.
 45func SetTheme(name string) error {
 46	globalManager.mu.Lock()
 47	defer globalManager.mu.Unlock()
 48
 49	delete(styles.Registry, "charm")
 50	if _, exists := globalManager.themes[name]; !exists {
 51		return fmt.Errorf("theme '%s' not found", name)
 52	}
 53
 54	globalManager.currentName = name
 55
 56	// Update the config file using viper
 57	if err := updateConfigTheme(name); err != nil {
 58		// Log the error but don't fail the theme change
 59		logging.Warn("Warning: Failed to update config file with new theme", "err", err)
 60	}
 61
 62	return nil
 63}
 64
 65// CurrentTheme returns the currently active theme.
 66// If no theme is set, it returns nil.
 67func CurrentTheme() Theme {
 68	globalManager.mu.RLock()
 69	defer globalManager.mu.RUnlock()
 70
 71	if globalManager.currentName == "" {
 72		return nil
 73	}
 74
 75	return globalManager.themes[globalManager.currentName]
 76}
 77
 78func GetColor(c color.Color) string {
 79	rgba := color.RGBAModel.Convert(c).(color.RGBA)
 80	return fmt.Sprintf("#%02x%02x%02x", rgba.R, rgba.G, rgba.B)
 81}
 82
 83// CurrentThemeName returns the name of the currently active theme.
 84func CurrentThemeName() string {
 85	globalManager.mu.RLock()
 86	defer globalManager.mu.RUnlock()
 87
 88	return globalManager.currentName
 89}
 90
 91// AvailableThemes returns a list of all registered theme names.
 92func AvailableThemes() []string {
 93	globalManager.mu.RLock()
 94	defer globalManager.mu.RUnlock()
 95
 96	names := make([]string, 0, len(globalManager.themes))
 97	for name := range globalManager.themes {
 98		names = append(names, name)
 99	}
100	slices.SortFunc(names, func(a, b string) int {
101		if a == "opencode" {
102			return -1
103		} else if b == "opencode" {
104			return 1
105		}
106		return strings.Compare(a, b)
107	})
108	return names
109}
110
111// GetTheme returns a specific theme by name.
112// Returns nil if the theme doesn't exist.
113func GetTheme(name string) Theme {
114	globalManager.mu.RLock()
115	defer globalManager.mu.RUnlock()
116
117	return globalManager.themes[name]
118}
119
120// updateConfigTheme updates the theme setting in the configuration file
121func updateConfigTheme(themeName string) error {
122	// Use the config package to update the theme
123	return config.UpdateTheme(themeName)
124}