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}