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}