manager.go 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  1. package theme
  2. import (
  3. "fmt"
  4. "image/color"
  5. "slices"
  6. "strconv"
  7. "strings"
  8. "sync"
  9. "github.com/alecthomas/chroma/v2/styles"
  10. "github.com/charmbracelet/lipgloss/v2"
  11. "github.com/charmbracelet/lipgloss/v2/compat"
  12. "github.com/charmbracelet/x/ansi"
  13. )
  14. // Manager handles theme registration, selection, and retrieval.
  15. // It maintains a registry of available themes and tracks the currently active theme.
  16. type Manager struct {
  17. themes map[string]Theme
  18. currentName string
  19. currentUsesAnsiCache bool // Cache whether current theme uses ANSI colors
  20. mu sync.RWMutex
  21. }
  22. // Global instance of the theme manager
  23. var globalManager = &Manager{
  24. themes: make(map[string]Theme),
  25. currentName: "",
  26. }
  27. // RegisterTheme adds a new theme to the registry.
  28. // If this is the first theme registered, it becomes the default.
  29. func RegisterTheme(name string, theme Theme) {
  30. globalManager.mu.Lock()
  31. defer globalManager.mu.Unlock()
  32. globalManager.themes[name] = theme
  33. // If this is the first theme, make it the default
  34. if globalManager.currentName == "" {
  35. globalManager.currentName = name
  36. globalManager.currentUsesAnsiCache = themeUsesAnsiColors(theme)
  37. }
  38. }
  39. // SetTheme changes the active theme to the one with the specified name.
  40. // Returns an error if the theme doesn't exist.
  41. func SetTheme(name string) error {
  42. globalManager.mu.Lock()
  43. defer globalManager.mu.Unlock()
  44. delete(styles.Registry, "charm")
  45. theme, exists := globalManager.themes[name]
  46. if !exists {
  47. return fmt.Errorf("theme '%s' not found", name)
  48. }
  49. globalManager.currentName = name
  50. globalManager.currentUsesAnsiCache = themeUsesAnsiColors(theme)
  51. return nil
  52. }
  53. // CurrentTheme returns the currently active theme.
  54. // If no theme is set, it returns nil.
  55. func CurrentTheme() Theme {
  56. globalManager.mu.RLock()
  57. defer globalManager.mu.RUnlock()
  58. if globalManager.currentName == "" {
  59. return nil
  60. }
  61. return globalManager.themes[globalManager.currentName]
  62. }
  63. // CurrentThemeName returns the name of the currently active theme.
  64. func CurrentThemeName() string {
  65. globalManager.mu.RLock()
  66. defer globalManager.mu.RUnlock()
  67. return globalManager.currentName
  68. }
  69. // AvailableThemes returns a list of all registered theme names.
  70. func AvailableThemes() []string {
  71. globalManager.mu.RLock()
  72. defer globalManager.mu.RUnlock()
  73. names := make([]string, 0, len(globalManager.themes))
  74. for name := range globalManager.themes {
  75. names = append(names, name)
  76. }
  77. slices.SortFunc(names, func(a, b string) int {
  78. if a == "opencode" {
  79. return -1
  80. } else if b == "opencode" {
  81. return 1
  82. }
  83. if a == "system" {
  84. return -1
  85. } else if b == "system" {
  86. return 1
  87. }
  88. return strings.Compare(a, b)
  89. })
  90. return names
  91. }
  92. // GetTheme returns a specific theme by name.
  93. // Returns nil if the theme doesn't exist.
  94. func GetTheme(name string) Theme {
  95. globalManager.mu.RLock()
  96. defer globalManager.mu.RUnlock()
  97. return globalManager.themes[name]
  98. }
  99. // UpdateSystemTheme updates the system theme with terminal background info
  100. func UpdateSystemTheme(terminalBg color.Color, isDark bool) {
  101. globalManager.mu.Lock()
  102. defer globalManager.mu.Unlock()
  103. dynamicTheme := NewSystemTheme(terminalBg, isDark)
  104. globalManager.themes["system"] = dynamicTheme
  105. if globalManager.currentName == "system" {
  106. globalManager.currentUsesAnsiCache = themeUsesAnsiColors(dynamicTheme)
  107. }
  108. }
  109. // CurrentThemeUsesAnsiColors returns true if the current theme uses ANSI 0-16 colors
  110. func CurrentThemeUsesAnsiColors() bool {
  111. // globalManager.mu.RLock()
  112. // defer globalManager.mu.RUnlock()
  113. return globalManager.currentUsesAnsiCache
  114. }
  115. // isAnsiColor checks if a color represents an ANSI 0-16 color
  116. func isAnsiColor(c color.Color) bool {
  117. if _, ok := c.(lipgloss.NoColor); ok {
  118. return false
  119. }
  120. if _, ok := c.(ansi.BasicColor); ok {
  121. return true
  122. }
  123. // For other color types, check if they represent ANSI colors
  124. // by examining their string representation
  125. if stringer, ok := c.(fmt.Stringer); ok {
  126. str := stringer.String()
  127. // Check if it's a numeric ANSI color (0-15)
  128. if num, err := strconv.Atoi(str); err == nil && num >= 0 && num <= 15 {
  129. return true
  130. }
  131. }
  132. return false
  133. }
  134. // adaptiveColorUsesAnsi checks if an AdaptiveColor uses ANSI colors
  135. func adaptiveColorUsesAnsi(ac compat.AdaptiveColor) bool {
  136. if isAnsiColor(ac.Dark) {
  137. return true
  138. }
  139. if isAnsiColor(ac.Light) {
  140. return true
  141. }
  142. return false
  143. }
  144. // themeUsesAnsiColors checks if a theme uses any ANSI 0-16 colors
  145. func themeUsesAnsiColors(theme Theme) bool {
  146. if theme == nil {
  147. return false
  148. }
  149. return adaptiveColorUsesAnsi(theme.Primary()) ||
  150. adaptiveColorUsesAnsi(theme.Secondary()) ||
  151. adaptiveColorUsesAnsi(theme.Accent()) ||
  152. adaptiveColorUsesAnsi(theme.Error()) ||
  153. adaptiveColorUsesAnsi(theme.Warning()) ||
  154. adaptiveColorUsesAnsi(theme.Success()) ||
  155. adaptiveColorUsesAnsi(theme.Info()) ||
  156. adaptiveColorUsesAnsi(theme.Text()) ||
  157. adaptiveColorUsesAnsi(theme.TextMuted()) ||
  158. adaptiveColorUsesAnsi(theme.Background()) ||
  159. adaptiveColorUsesAnsi(theme.BackgroundPanel()) ||
  160. adaptiveColorUsesAnsi(theme.BackgroundElement()) ||
  161. adaptiveColorUsesAnsi(theme.Border()) ||
  162. adaptiveColorUsesAnsi(theme.BorderActive()) ||
  163. adaptiveColorUsesAnsi(theme.BorderSubtle()) ||
  164. adaptiveColorUsesAnsi(theme.DiffAdded()) ||
  165. adaptiveColorUsesAnsi(theme.DiffRemoved()) ||
  166. adaptiveColorUsesAnsi(theme.DiffContext()) ||
  167. adaptiveColorUsesAnsi(theme.DiffHunkHeader()) ||
  168. adaptiveColorUsesAnsi(theme.DiffHighlightAdded()) ||
  169. adaptiveColorUsesAnsi(theme.DiffHighlightRemoved()) ||
  170. adaptiveColorUsesAnsi(theme.DiffAddedBg()) ||
  171. adaptiveColorUsesAnsi(theme.DiffRemovedBg()) ||
  172. adaptiveColorUsesAnsi(theme.DiffContextBg()) ||
  173. adaptiveColorUsesAnsi(theme.DiffLineNumber()) ||
  174. adaptiveColorUsesAnsi(theme.DiffAddedLineNumberBg()) ||
  175. adaptiveColorUsesAnsi(theme.DiffRemovedLineNumberBg()) ||
  176. adaptiveColorUsesAnsi(theme.MarkdownText()) ||
  177. adaptiveColorUsesAnsi(theme.MarkdownHeading()) ||
  178. adaptiveColorUsesAnsi(theme.MarkdownLink()) ||
  179. adaptiveColorUsesAnsi(theme.MarkdownLinkText()) ||
  180. adaptiveColorUsesAnsi(theme.MarkdownCode()) ||
  181. adaptiveColorUsesAnsi(theme.MarkdownBlockQuote()) ||
  182. adaptiveColorUsesAnsi(theme.MarkdownEmph()) ||
  183. adaptiveColorUsesAnsi(theme.MarkdownStrong()) ||
  184. adaptiveColorUsesAnsi(theme.MarkdownHorizontalRule()) ||
  185. adaptiveColorUsesAnsi(theme.MarkdownListItem()) ||
  186. adaptiveColorUsesAnsi(theme.MarkdownListEnumeration()) ||
  187. adaptiveColorUsesAnsi(theme.MarkdownImage()) ||
  188. adaptiveColorUsesAnsi(theme.MarkdownImageText()) ||
  189. adaptiveColorUsesAnsi(theme.MarkdownCodeBlock()) ||
  190. adaptiveColorUsesAnsi(theme.SyntaxComment()) ||
  191. adaptiveColorUsesAnsi(theme.SyntaxKeyword()) ||
  192. adaptiveColorUsesAnsi(theme.SyntaxFunction()) ||
  193. adaptiveColorUsesAnsi(theme.SyntaxVariable()) ||
  194. adaptiveColorUsesAnsi(theme.SyntaxString()) ||
  195. adaptiveColorUsesAnsi(theme.SyntaxNumber()) ||
  196. adaptiveColorUsesAnsi(theme.SyntaxType()) ||
  197. adaptiveColorUsesAnsi(theme.SyntaxOperator()) ||
  198. adaptiveColorUsesAnsi(theme.SyntaxPunctuation())
  199. }