theme.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  1. package theme
  2. import (
  3. "fmt"
  4. "regexp"
  5. "github.com/charmbracelet/lipgloss/v2"
  6. "github.com/charmbracelet/lipgloss/v2/compat"
  7. )
  8. // Theme defines the interface for all UI themes in the application.
  9. // All colors must be defined as compat.AdaptiveColor to support
  10. // both light and dark terminal backgrounds.
  11. type Theme interface {
  12. // Background colors
  13. Background() compat.AdaptiveColor // Radix 1
  14. BackgroundSubtle() compat.AdaptiveColor // Radix 2
  15. BackgroundElement() compat.AdaptiveColor // Radix 3
  16. // Border colors
  17. BorderSubtle() compat.AdaptiveColor // Radix 6
  18. Border() compat.AdaptiveColor // Radix 7
  19. BorderActive() compat.AdaptiveColor // Radix 8
  20. // Brand colors
  21. Primary() compat.AdaptiveColor // Radix 9
  22. Secondary() compat.AdaptiveColor
  23. Accent() compat.AdaptiveColor
  24. // Text colors
  25. TextMuted() compat.AdaptiveColor // Radix 11
  26. Text() compat.AdaptiveColor // Radix 12
  27. // Status colors
  28. Error() compat.AdaptiveColor
  29. Warning() compat.AdaptiveColor
  30. Success() compat.AdaptiveColor
  31. Info() compat.AdaptiveColor
  32. // Diff view colors
  33. DiffAdded() compat.AdaptiveColor
  34. DiffRemoved() compat.AdaptiveColor
  35. DiffContext() compat.AdaptiveColor
  36. DiffHunkHeader() compat.AdaptiveColor
  37. DiffHighlightAdded() compat.AdaptiveColor
  38. DiffHighlightRemoved() compat.AdaptiveColor
  39. DiffAddedBg() compat.AdaptiveColor
  40. DiffRemovedBg() compat.AdaptiveColor
  41. DiffContextBg() compat.AdaptiveColor
  42. DiffLineNumber() compat.AdaptiveColor
  43. DiffAddedLineNumberBg() compat.AdaptiveColor
  44. DiffRemovedLineNumberBg() compat.AdaptiveColor
  45. // Markdown colors
  46. MarkdownText() compat.AdaptiveColor
  47. MarkdownHeading() compat.AdaptiveColor
  48. MarkdownLink() compat.AdaptiveColor
  49. MarkdownLinkText() compat.AdaptiveColor
  50. MarkdownCode() compat.AdaptiveColor
  51. MarkdownBlockQuote() compat.AdaptiveColor
  52. MarkdownEmph() compat.AdaptiveColor
  53. MarkdownStrong() compat.AdaptiveColor
  54. MarkdownHorizontalRule() compat.AdaptiveColor
  55. MarkdownListItem() compat.AdaptiveColor
  56. MarkdownListEnumeration() compat.AdaptiveColor
  57. MarkdownImage() compat.AdaptiveColor
  58. MarkdownImageText() compat.AdaptiveColor
  59. MarkdownCodeBlock() compat.AdaptiveColor
  60. // Syntax highlighting colors
  61. SyntaxComment() compat.AdaptiveColor
  62. SyntaxKeyword() compat.AdaptiveColor
  63. SyntaxFunction() compat.AdaptiveColor
  64. SyntaxVariable() compat.AdaptiveColor
  65. SyntaxString() compat.AdaptiveColor
  66. SyntaxNumber() compat.AdaptiveColor
  67. SyntaxType() compat.AdaptiveColor
  68. SyntaxOperator() compat.AdaptiveColor
  69. SyntaxPunctuation() compat.AdaptiveColor
  70. }
  71. // BaseTheme provides a default implementation of the Theme interface
  72. // that can be embedded in concrete theme implementations.
  73. type BaseTheme struct {
  74. // Background colors
  75. BackgroundColor compat.AdaptiveColor
  76. BackgroundSubtleColor compat.AdaptiveColor
  77. BackgroundElementColor compat.AdaptiveColor
  78. // Border colors
  79. BorderSubtleColor compat.AdaptiveColor
  80. BorderColor compat.AdaptiveColor
  81. BorderActiveColor compat.AdaptiveColor
  82. // Brand colors
  83. PrimaryColor compat.AdaptiveColor
  84. SecondaryColor compat.AdaptiveColor
  85. AccentColor compat.AdaptiveColor
  86. // Text colors
  87. TextMutedColor compat.AdaptiveColor
  88. TextColor compat.AdaptiveColor
  89. // Status colors
  90. ErrorColor compat.AdaptiveColor
  91. WarningColor compat.AdaptiveColor
  92. SuccessColor compat.AdaptiveColor
  93. InfoColor compat.AdaptiveColor
  94. // Diff view colors
  95. DiffAddedColor compat.AdaptiveColor
  96. DiffRemovedColor compat.AdaptiveColor
  97. DiffContextColor compat.AdaptiveColor
  98. DiffHunkHeaderColor compat.AdaptiveColor
  99. DiffHighlightAddedColor compat.AdaptiveColor
  100. DiffHighlightRemovedColor compat.AdaptiveColor
  101. DiffAddedBgColor compat.AdaptiveColor
  102. DiffRemovedBgColor compat.AdaptiveColor
  103. DiffContextBgColor compat.AdaptiveColor
  104. DiffLineNumberColor compat.AdaptiveColor
  105. DiffAddedLineNumberBgColor compat.AdaptiveColor
  106. DiffRemovedLineNumberBgColor compat.AdaptiveColor
  107. // Markdown colors
  108. MarkdownTextColor compat.AdaptiveColor
  109. MarkdownHeadingColor compat.AdaptiveColor
  110. MarkdownLinkColor compat.AdaptiveColor
  111. MarkdownLinkTextColor compat.AdaptiveColor
  112. MarkdownCodeColor compat.AdaptiveColor
  113. MarkdownBlockQuoteColor compat.AdaptiveColor
  114. MarkdownEmphColor compat.AdaptiveColor
  115. MarkdownStrongColor compat.AdaptiveColor
  116. MarkdownHorizontalRuleColor compat.AdaptiveColor
  117. MarkdownListItemColor compat.AdaptiveColor
  118. MarkdownListEnumerationColor compat.AdaptiveColor
  119. MarkdownImageColor compat.AdaptiveColor
  120. MarkdownImageTextColor compat.AdaptiveColor
  121. MarkdownCodeBlockColor compat.AdaptiveColor
  122. // Syntax highlighting colors
  123. SyntaxCommentColor compat.AdaptiveColor
  124. SyntaxKeywordColor compat.AdaptiveColor
  125. SyntaxFunctionColor compat.AdaptiveColor
  126. SyntaxVariableColor compat.AdaptiveColor
  127. SyntaxStringColor compat.AdaptiveColor
  128. SyntaxNumberColor compat.AdaptiveColor
  129. SyntaxTypeColor compat.AdaptiveColor
  130. SyntaxOperatorColor compat.AdaptiveColor
  131. SyntaxPunctuationColor compat.AdaptiveColor
  132. }
  133. // Implement the Theme interface for BaseTheme
  134. func (t *BaseTheme) Primary() compat.AdaptiveColor { return t.PrimaryColor }
  135. func (t *BaseTheme) Secondary() compat.AdaptiveColor { return t.SecondaryColor }
  136. func (t *BaseTheme) Accent() compat.AdaptiveColor { return t.AccentColor }
  137. func (t *BaseTheme) Error() compat.AdaptiveColor { return t.ErrorColor }
  138. func (t *BaseTheme) Warning() compat.AdaptiveColor { return t.WarningColor }
  139. func (t *BaseTheme) Success() compat.AdaptiveColor { return t.SuccessColor }
  140. func (t *BaseTheme) Info() compat.AdaptiveColor { return t.InfoColor }
  141. func (t *BaseTheme) Text() compat.AdaptiveColor { return t.TextColor }
  142. func (t *BaseTheme) TextMuted() compat.AdaptiveColor { return t.TextMutedColor }
  143. func (t *BaseTheme) Background() compat.AdaptiveColor { return t.BackgroundColor }
  144. func (t *BaseTheme) BackgroundSubtle() compat.AdaptiveColor { return t.BackgroundSubtleColor }
  145. func (t *BaseTheme) BackgroundElement() compat.AdaptiveColor { return t.BackgroundElementColor }
  146. func (t *BaseTheme) Border() compat.AdaptiveColor { return t.BorderColor }
  147. func (t *BaseTheme) BorderActive() compat.AdaptiveColor { return t.BorderActiveColor }
  148. func (t *BaseTheme) BorderSubtle() compat.AdaptiveColor { return t.BorderSubtleColor }
  149. func (t *BaseTheme) DiffAdded() compat.AdaptiveColor { return t.DiffAddedColor }
  150. func (t *BaseTheme) DiffRemoved() compat.AdaptiveColor { return t.DiffRemovedColor }
  151. func (t *BaseTheme) DiffContext() compat.AdaptiveColor { return t.DiffContextColor }
  152. func (t *BaseTheme) DiffHunkHeader() compat.AdaptiveColor { return t.DiffHunkHeaderColor }
  153. func (t *BaseTheme) DiffHighlightAdded() compat.AdaptiveColor { return t.DiffHighlightAddedColor }
  154. func (t *BaseTheme) DiffHighlightRemoved() compat.AdaptiveColor { return t.DiffHighlightRemovedColor }
  155. func (t *BaseTheme) DiffAddedBg() compat.AdaptiveColor { return t.DiffAddedBgColor }
  156. func (t *BaseTheme) DiffRemovedBg() compat.AdaptiveColor { return t.DiffRemovedBgColor }
  157. func (t *BaseTheme) DiffContextBg() compat.AdaptiveColor { return t.DiffContextBgColor }
  158. func (t *BaseTheme) DiffLineNumber() compat.AdaptiveColor { return t.DiffLineNumberColor }
  159. func (t *BaseTheme) DiffAddedLineNumberBg() compat.AdaptiveColor {
  160. return t.DiffAddedLineNumberBgColor
  161. }
  162. func (t *BaseTheme) DiffRemovedLineNumberBg() compat.AdaptiveColor {
  163. return t.DiffRemovedLineNumberBgColor
  164. }
  165. func (t *BaseTheme) MarkdownText() compat.AdaptiveColor { return t.MarkdownTextColor }
  166. func (t *BaseTheme) MarkdownHeading() compat.AdaptiveColor { return t.MarkdownHeadingColor }
  167. func (t *BaseTheme) MarkdownLink() compat.AdaptiveColor { return t.MarkdownLinkColor }
  168. func (t *BaseTheme) MarkdownLinkText() compat.AdaptiveColor { return t.MarkdownLinkTextColor }
  169. func (t *BaseTheme) MarkdownCode() compat.AdaptiveColor { return t.MarkdownCodeColor }
  170. func (t *BaseTheme) MarkdownBlockQuote() compat.AdaptiveColor { return t.MarkdownBlockQuoteColor }
  171. func (t *BaseTheme) MarkdownEmph() compat.AdaptiveColor { return t.MarkdownEmphColor }
  172. func (t *BaseTheme) MarkdownStrong() compat.AdaptiveColor { return t.MarkdownStrongColor }
  173. func (t *BaseTheme) MarkdownHorizontalRule() compat.AdaptiveColor {
  174. return t.MarkdownHorizontalRuleColor
  175. }
  176. func (t *BaseTheme) MarkdownListItem() compat.AdaptiveColor { return t.MarkdownListItemColor }
  177. func (t *BaseTheme) MarkdownListEnumeration() compat.AdaptiveColor {
  178. return t.MarkdownListEnumerationColor
  179. }
  180. func (t *BaseTheme) MarkdownImage() compat.AdaptiveColor { return t.MarkdownImageColor }
  181. func (t *BaseTheme) MarkdownImageText() compat.AdaptiveColor { return t.MarkdownImageTextColor }
  182. func (t *BaseTheme) MarkdownCodeBlock() compat.AdaptiveColor { return t.MarkdownCodeBlockColor }
  183. func (t *BaseTheme) SyntaxComment() compat.AdaptiveColor { return t.SyntaxCommentColor }
  184. func (t *BaseTheme) SyntaxKeyword() compat.AdaptiveColor { return t.SyntaxKeywordColor }
  185. func (t *BaseTheme) SyntaxFunction() compat.AdaptiveColor { return t.SyntaxFunctionColor }
  186. func (t *BaseTheme) SyntaxVariable() compat.AdaptiveColor { return t.SyntaxVariableColor }
  187. func (t *BaseTheme) SyntaxString() compat.AdaptiveColor { return t.SyntaxStringColor }
  188. func (t *BaseTheme) SyntaxNumber() compat.AdaptiveColor { return t.SyntaxNumberColor }
  189. func (t *BaseTheme) SyntaxType() compat.AdaptiveColor { return t.SyntaxTypeColor }
  190. func (t *BaseTheme) SyntaxOperator() compat.AdaptiveColor { return t.SyntaxOperatorColor }
  191. func (t *BaseTheme) SyntaxPunctuation() compat.AdaptiveColor { return t.SyntaxPunctuationColor }
  192. // ParseAdaptiveColor parses a color value from the config file into a compat.AdaptiveColor.
  193. // It accepts either a string (hex color) or a map with "dark" and "light" keys.
  194. func ParseAdaptiveColor(value any) (compat.AdaptiveColor, error) {
  195. // Regular expression to validate hex color format
  196. hexColorRegex := regexp.MustCompile(`^#[0-9a-fA-F]{6}$`)
  197. // Case 1: String value (same color for both dark and light modes)
  198. if hexColor, ok := value.(string); ok {
  199. if !hexColorRegex.MatchString(hexColor) {
  200. return compat.AdaptiveColor{}, fmt.Errorf("invalid hex color format: %s", hexColor)
  201. }
  202. return compat.AdaptiveColor{
  203. Dark: lipgloss.Color(hexColor),
  204. Light: lipgloss.Color(hexColor),
  205. }, nil
  206. }
  207. // Case 2: Int value between 0 and 255
  208. if numericVal, ok := value.(float64); ok {
  209. intVal := int(numericVal)
  210. if intVal < 0 || intVal > 255 {
  211. return compat.AdaptiveColor{}, fmt.Errorf("invalid int color value (must be between 0 and 255): %d", intVal)
  212. }
  213. return compat.AdaptiveColor{
  214. Dark: lipgloss.Color(fmt.Sprintf("%d", intVal)),
  215. Light: lipgloss.Color(fmt.Sprintf("%d", intVal)),
  216. }, nil
  217. }
  218. // Case 3: Map with dark and light keys
  219. if colorMap, ok := value.(map[string]any); ok {
  220. darkVal, darkOk := colorMap["dark"]
  221. lightVal, lightOk := colorMap["light"]
  222. if !darkOk || !lightOk {
  223. return compat.AdaptiveColor{}, fmt.Errorf("color map must contain both 'dark' and 'light' keys")
  224. }
  225. darkHex, darkIsString := darkVal.(string)
  226. lightHex, lightIsString := lightVal.(string)
  227. if !darkIsString || !lightIsString {
  228. darkVal, darkIsNumber := darkVal.(float64)
  229. lightVal, lightIsNumber := lightVal.(float64)
  230. if !darkIsNumber || !lightIsNumber {
  231. return compat.AdaptiveColor{}, fmt.Errorf("color map values must be strings or ints")
  232. }
  233. darkInt := int(darkVal)
  234. lightInt := int(lightVal)
  235. return compat.AdaptiveColor{
  236. Dark: lipgloss.Color(fmt.Sprintf("%d", darkInt)),
  237. Light: lipgloss.Color(fmt.Sprintf("%d", lightInt)),
  238. }, nil
  239. }
  240. if !hexColorRegex.MatchString(darkHex) || !hexColorRegex.MatchString(lightHex) {
  241. return compat.AdaptiveColor{}, fmt.Errorf("invalid hex color format")
  242. }
  243. return compat.AdaptiveColor{
  244. Dark: lipgloss.Color(darkHex),
  245. Light: lipgloss.Color(lightHex),
  246. }, nil
  247. }
  248. return compat.AdaptiveColor{}, fmt.Errorf("color must be either a hex string or an object with dark/light keys")
  249. }