manager.go 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. package theme
  2. import (
  3. "fmt"
  4. "slices"
  5. "strings"
  6. "sync"
  7. "github.com/alecthomas/chroma/v2/styles"
  8. "github.com/opencode-ai/opencode/internal/config"
  9. "github.com/opencode-ai/opencode/internal/logging"
  10. )
  11. // Manager handles theme registration, selection, and retrieval.
  12. // It maintains a registry of available themes and tracks the currently active theme.
  13. type Manager struct {
  14. themes map[string]Theme
  15. currentName string
  16. mu sync.RWMutex
  17. }
  18. // Global instance of the theme manager
  19. var globalManager = &Manager{
  20. themes: make(map[string]Theme),
  21. currentName: "",
  22. }
  23. // Default theme instance for custom theme defaulting
  24. var defaultThemeColors = NewOpenCodeTheme()
  25. // RegisterTheme adds a new theme to the registry.
  26. // If this is the first theme registered, it becomes the default.
  27. func RegisterTheme(name string, theme Theme) {
  28. globalManager.mu.Lock()
  29. defer globalManager.mu.Unlock()
  30. globalManager.themes[name] = theme
  31. // If this is the first theme, make it the default
  32. if globalManager.currentName == "" {
  33. globalManager.currentName = name
  34. }
  35. }
  36. // SetTheme changes the active theme to the one with the specified name.
  37. // Returns an error if the theme doesn't exist.
  38. func SetTheme(name string) error {
  39. globalManager.mu.Lock()
  40. defer globalManager.mu.Unlock()
  41. delete(styles.Registry, "charm")
  42. // Handle custom theme
  43. if name == "custom" {
  44. cfg := config.Get()
  45. if cfg == nil || cfg.TUI.CustomTheme == nil || len(cfg.TUI.CustomTheme) == 0 {
  46. return fmt.Errorf("custom theme selected but no custom theme colors defined in config")
  47. }
  48. customTheme, err := LoadCustomTheme(cfg.TUI.CustomTheme)
  49. if err != nil {
  50. return fmt.Errorf("failed to load custom theme: %w", err)
  51. }
  52. // Register the custom theme
  53. globalManager.themes["custom"] = customTheme
  54. } else if _, exists := globalManager.themes[name]; !exists {
  55. return fmt.Errorf("theme '%s' not found", name)
  56. }
  57. globalManager.currentName = name
  58. // Update the config file using viper
  59. if err := updateConfigTheme(name); err != nil {
  60. // Log the error but don't fail the theme change
  61. logging.Warn("Warning: Failed to update config file with new theme", "err", err)
  62. }
  63. return nil
  64. }
  65. // CurrentTheme returns the currently active theme.
  66. // If no theme is set, it returns nil.
  67. func CurrentTheme() Theme {
  68. globalManager.mu.RLock()
  69. defer globalManager.mu.RUnlock()
  70. if globalManager.currentName == "" {
  71. return nil
  72. }
  73. return globalManager.themes[globalManager.currentName]
  74. }
  75. // CurrentThemeName returns the name of the currently active theme.
  76. func CurrentThemeName() string {
  77. globalManager.mu.RLock()
  78. defer globalManager.mu.RUnlock()
  79. return globalManager.currentName
  80. }
  81. // AvailableThemes returns a list of all registered theme names.
  82. func AvailableThemes() []string {
  83. globalManager.mu.RLock()
  84. defer globalManager.mu.RUnlock()
  85. names := make([]string, 0, len(globalManager.themes))
  86. for name := range globalManager.themes {
  87. names = append(names, name)
  88. }
  89. slices.SortFunc(names, func(a, b string) int {
  90. if a == "opencode" {
  91. return -1
  92. } else if b == "opencode" {
  93. return 1
  94. }
  95. return strings.Compare(a, b)
  96. })
  97. return names
  98. }
  99. // GetTheme returns a specific theme by name.
  100. // Returns nil if the theme doesn't exist.
  101. func GetTheme(name string) Theme {
  102. globalManager.mu.RLock()
  103. defer globalManager.mu.RUnlock()
  104. return globalManager.themes[name]
  105. }
  106. // LoadCustomTheme creates a new theme instance based on the custom theme colors
  107. // defined in the configuration. It uses the default OpenCode theme as a base
  108. // and overrides colors that are specified in the customTheme map.
  109. func LoadCustomTheme(customTheme map[string]any) (Theme, error) {
  110. // Create a new theme based on the default OpenCode theme
  111. theme := NewOpenCodeTheme()
  112. // Process each color in the custom theme map
  113. for key, value := range customTheme {
  114. adaptiveColor, err := ParseAdaptiveColor(value)
  115. if err != nil {
  116. logging.Warn("Invalid color definition in custom theme", "key", key, "error", err)
  117. continue // Skip this color but continue processing others
  118. }
  119. // Set the color in the theme based on the key
  120. switch strings.ToLower(key) {
  121. case "primary":
  122. theme.PrimaryColor = adaptiveColor
  123. case "secondary":
  124. theme.SecondaryColor = adaptiveColor
  125. case "accent":
  126. theme.AccentColor = adaptiveColor
  127. case "error":
  128. theme.ErrorColor = adaptiveColor
  129. case "warning":
  130. theme.WarningColor = adaptiveColor
  131. case "success":
  132. theme.SuccessColor = adaptiveColor
  133. case "info":
  134. theme.InfoColor = adaptiveColor
  135. case "text":
  136. theme.TextColor = adaptiveColor
  137. case "textmuted":
  138. theme.TextMutedColor = adaptiveColor
  139. case "textemphasized":
  140. theme.TextEmphasizedColor = adaptiveColor
  141. case "background":
  142. theme.BackgroundColor = adaptiveColor
  143. case "backgroundsecondary":
  144. theme.BackgroundSecondaryColor = adaptiveColor
  145. case "backgrounddarker":
  146. theme.BackgroundDarkerColor = adaptiveColor
  147. case "bordernormal":
  148. theme.BorderNormalColor = adaptiveColor
  149. case "borderfocused":
  150. theme.BorderFocusedColor = adaptiveColor
  151. case "borderdim":
  152. theme.BorderDimColor = adaptiveColor
  153. case "diffadded":
  154. theme.DiffAddedColor = adaptiveColor
  155. case "diffremoved":
  156. theme.DiffRemovedColor = adaptiveColor
  157. case "diffcontext":
  158. theme.DiffContextColor = adaptiveColor
  159. case "diffhunkheader":
  160. theme.DiffHunkHeaderColor = adaptiveColor
  161. case "diffhighlightadded":
  162. theme.DiffHighlightAddedColor = adaptiveColor
  163. case "diffhighlightremoved":
  164. theme.DiffHighlightRemovedColor = adaptiveColor
  165. case "diffaddedbg":
  166. theme.DiffAddedBgColor = adaptiveColor
  167. case "diffremovedbg":
  168. theme.DiffRemovedBgColor = adaptiveColor
  169. case "diffcontextbg":
  170. theme.DiffContextBgColor = adaptiveColor
  171. case "difflinenumber":
  172. theme.DiffLineNumberColor = adaptiveColor
  173. case "diffaddedlinenumberbg":
  174. theme.DiffAddedLineNumberBgColor = adaptiveColor
  175. case "diffremovedlinenumberbg":
  176. theme.DiffRemovedLineNumberBgColor = adaptiveColor
  177. default:
  178. logging.Warn("Unknown color key in custom theme", "key", key)
  179. }
  180. }
  181. return theme, nil
  182. }
  183. // updateConfigTheme updates the theme setting in the configuration file
  184. func updateConfigTheme(themeName string) error {
  185. // Use the config package to update the theme
  186. return config.UpdateTheme(themeName)
  187. }