manager.go

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