manager.go 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  1. package theme
  2. import (
  3. "fmt"
  4. "log/slog"
  5. "slices"
  6. "strings"
  7. "sync"
  8. "github.com/alecthomas/chroma/v2/styles"
  9. "github.com/sst/opencode/internal/config"
  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. slog.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. slog.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. case "syntaxcomment":
  178. theme.SyntaxCommentColor = adaptiveColor
  179. case "syntaxkeyword":
  180. theme.SyntaxKeywordColor = adaptiveColor
  181. case "syntaxfunction":
  182. theme.SyntaxFunctionColor = adaptiveColor
  183. case "syntaxvariable":
  184. theme.SyntaxVariableColor = adaptiveColor
  185. case "syntaxstring":
  186. theme.SyntaxStringColor = adaptiveColor
  187. case "syntaxnumber":
  188. theme.SyntaxNumberColor = adaptiveColor
  189. case "syntaxtype":
  190. theme.SyntaxTypeColor = adaptiveColor
  191. case "syntaxoperator":
  192. theme.SyntaxOperatorColor = adaptiveColor
  193. case "syntaxpunctuation":
  194. theme.SyntaxPunctuationColor = adaptiveColor
  195. case "markdowntext":
  196. theme.MarkdownTextColor = adaptiveColor
  197. case "markdownheading":
  198. theme.MarkdownHeadingColor = adaptiveColor
  199. case "markdownlink":
  200. theme.MarkdownLinkColor = adaptiveColor
  201. case "markdownlinktext":
  202. theme.MarkdownLinkTextColor = adaptiveColor
  203. case "markdowncode":
  204. theme.MarkdownCodeColor = adaptiveColor
  205. case "markdownblockquote":
  206. theme.MarkdownBlockQuoteColor = adaptiveColor
  207. case "markdownemph":
  208. theme.MarkdownEmphColor = adaptiveColor
  209. case "markdownstrong":
  210. theme.MarkdownStrongColor = adaptiveColor
  211. case "markdownhorizontalrule":
  212. theme.MarkdownHorizontalRuleColor = adaptiveColor
  213. case "markdownlistitem":
  214. theme.MarkdownListItemColor = adaptiveColor
  215. case "markdownlistitemenum":
  216. theme.MarkdownListEnumerationColor = adaptiveColor
  217. case "markdownimage":
  218. theme.MarkdownImageColor = adaptiveColor
  219. case "markdownimagetext":
  220. theme.MarkdownImageTextColor = adaptiveColor
  221. case "markdowncodeblock":
  222. theme.MarkdownCodeBlockColor = adaptiveColor
  223. case "markdownlistenumeration":
  224. theme.MarkdownListEnumerationColor = adaptiveColor
  225. default:
  226. slog.Warn("Unknown color key in custom theme", "key", key)
  227. }
  228. }
  229. return theme, nil
  230. }
  231. // updateConfigTheme updates the theme setting in the configuration file
  232. func updateConfigTheme(themeName string) error {
  233. // Use the config package to update the theme
  234. return config.UpdateTheme(themeName)
  235. }