system.go 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  1. package theme
  2. import (
  3. "fmt"
  4. "image/color"
  5. "math"
  6. "github.com/charmbracelet/lipgloss/v2"
  7. "github.com/charmbracelet/lipgloss/v2/compat"
  8. )
  9. // SystemTheme is a dynamic theme that derives its gray scale colors
  10. // from the terminal's background color at runtime
  11. type SystemTheme struct {
  12. BaseTheme
  13. terminalBg color.Color
  14. terminalBgIsDark bool
  15. }
  16. // NewSystemTheme creates a new instance of the dynamic system theme
  17. func NewSystemTheme(terminalBg color.Color, isDark bool) *SystemTheme {
  18. theme := &SystemTheme{
  19. terminalBg: terminalBg,
  20. terminalBgIsDark: isDark,
  21. }
  22. theme.initializeColors()
  23. return theme
  24. }
  25. // initializeColors sets up all theme colors
  26. func (t *SystemTheme) initializeColors() {
  27. // Generate gray scale based on terminal background
  28. grays := t.generateGrayScale()
  29. // Set ANSI colors for primary colors
  30. t.PrimaryColor = compat.AdaptiveColor{
  31. Dark: lipgloss.Cyan,
  32. Light: lipgloss.Cyan,
  33. }
  34. t.SecondaryColor = compat.AdaptiveColor{
  35. Dark: lipgloss.Magenta,
  36. Light: lipgloss.Magenta,
  37. }
  38. t.AccentColor = compat.AdaptiveColor{
  39. Dark: lipgloss.Cyan,
  40. Light: lipgloss.Cyan,
  41. }
  42. // Status colors using ANSI
  43. t.ErrorColor = compat.AdaptiveColor{
  44. Dark: lipgloss.Red,
  45. Light: lipgloss.Red,
  46. }
  47. t.WarningColor = compat.AdaptiveColor{
  48. Dark: lipgloss.Yellow,
  49. Light: lipgloss.Yellow,
  50. }
  51. t.SuccessColor = compat.AdaptiveColor{
  52. Dark: lipgloss.Green,
  53. Light: lipgloss.Green,
  54. }
  55. t.InfoColor = compat.AdaptiveColor{
  56. Dark: lipgloss.Cyan,
  57. Light: lipgloss.Cyan,
  58. }
  59. // Text colors
  60. t.TextColor = compat.AdaptiveColor{
  61. Dark: lipgloss.NoColor{},
  62. Light: lipgloss.NoColor{},
  63. }
  64. // Derive muted text color from terminal foreground
  65. t.TextMutedColor = t.generateMutedTextColor()
  66. // Background colors
  67. t.BackgroundColor = compat.AdaptiveColor{
  68. Dark: lipgloss.NoColor{},
  69. Light: lipgloss.NoColor{},
  70. }
  71. t.BackgroundPanelColor = grays[2]
  72. t.BackgroundElementColor = grays[3]
  73. // Border colors
  74. t.BorderSubtleColor = grays[6]
  75. t.BorderColor = grays[7]
  76. t.BorderActiveColor = grays[8]
  77. // Diff colors using ANSI colors
  78. t.DiffAddedColor = compat.AdaptiveColor{
  79. Dark: lipgloss.Color("2"), // green
  80. Light: lipgloss.Color("2"),
  81. }
  82. t.DiffRemovedColor = compat.AdaptiveColor{
  83. Dark: lipgloss.Color("1"), // red
  84. Light: lipgloss.Color("1"),
  85. }
  86. t.DiffContextColor = grays[7] // Use gray for context
  87. t.DiffHunkHeaderColor = grays[7]
  88. t.DiffHighlightAddedColor = compat.AdaptiveColor{
  89. Dark: lipgloss.Color("2"), // green
  90. Light: lipgloss.Color("2"),
  91. }
  92. t.DiffHighlightRemovedColor = compat.AdaptiveColor{
  93. Dark: lipgloss.Color("1"), // red
  94. Light: lipgloss.Color("1"),
  95. }
  96. // Use subtle gray backgrounds for diff
  97. t.DiffAddedBgColor = grays[2]
  98. t.DiffRemovedBgColor = grays[2]
  99. t.DiffContextBgColor = grays[1]
  100. t.DiffLineNumberColor = grays[6]
  101. t.DiffAddedLineNumberBgColor = grays[3]
  102. t.DiffRemovedLineNumberBgColor = grays[3]
  103. // Markdown colors using ANSI
  104. t.MarkdownTextColor = compat.AdaptiveColor{
  105. Dark: lipgloss.NoColor{},
  106. Light: lipgloss.NoColor{},
  107. }
  108. t.MarkdownHeadingColor = compat.AdaptiveColor{
  109. Dark: lipgloss.NoColor{},
  110. Light: lipgloss.NoColor{},
  111. }
  112. t.MarkdownLinkColor = compat.AdaptiveColor{
  113. Dark: lipgloss.Color("4"), // blue
  114. Light: lipgloss.Color("4"),
  115. }
  116. t.MarkdownLinkTextColor = compat.AdaptiveColor{
  117. Dark: lipgloss.Color("6"), // cyan
  118. Light: lipgloss.Color("6"),
  119. }
  120. t.MarkdownCodeColor = compat.AdaptiveColor{
  121. Dark: lipgloss.Color("2"), // green
  122. Light: lipgloss.Color("2"),
  123. }
  124. t.MarkdownBlockQuoteColor = compat.AdaptiveColor{
  125. Dark: lipgloss.Color("3"), // yellow
  126. Light: lipgloss.Color("3"),
  127. }
  128. t.MarkdownEmphColor = compat.AdaptiveColor{
  129. Dark: lipgloss.Color("3"), // yellow
  130. Light: lipgloss.Color("3"),
  131. }
  132. t.MarkdownStrongColor = compat.AdaptiveColor{
  133. Dark: lipgloss.NoColor{},
  134. Light: lipgloss.NoColor{},
  135. }
  136. t.MarkdownHorizontalRuleColor = t.BorderColor
  137. t.MarkdownListItemColor = compat.AdaptiveColor{
  138. Dark: lipgloss.Color("4"), // blue
  139. Light: lipgloss.Color("4"),
  140. }
  141. t.MarkdownListEnumerationColor = compat.AdaptiveColor{
  142. Dark: lipgloss.Color("6"), // cyan
  143. Light: lipgloss.Color("6"),
  144. }
  145. t.MarkdownImageColor = compat.AdaptiveColor{
  146. Dark: lipgloss.Color("4"), // blue
  147. Light: lipgloss.Color("4"),
  148. }
  149. t.MarkdownImageTextColor = compat.AdaptiveColor{
  150. Dark: lipgloss.Color("6"), // cyan
  151. Light: lipgloss.Color("6"),
  152. }
  153. t.MarkdownCodeBlockColor = compat.AdaptiveColor{
  154. Dark: lipgloss.NoColor{},
  155. Light: lipgloss.NoColor{},
  156. }
  157. // Syntax colors
  158. t.SyntaxCommentColor = t.TextMutedColor // Use same as muted text
  159. t.SyntaxKeywordColor = compat.AdaptiveColor{
  160. Dark: lipgloss.Color("5"), // magenta
  161. Light: lipgloss.Color("5"),
  162. }
  163. t.SyntaxFunctionColor = compat.AdaptiveColor{
  164. Dark: lipgloss.Color("4"), // blue
  165. Light: lipgloss.Color("4"),
  166. }
  167. t.SyntaxVariableColor = compat.AdaptiveColor{
  168. Dark: lipgloss.NoColor{},
  169. Light: lipgloss.NoColor{},
  170. }
  171. t.SyntaxStringColor = compat.AdaptiveColor{
  172. Dark: lipgloss.Color("2"), // green
  173. Light: lipgloss.Color("2"),
  174. }
  175. t.SyntaxNumberColor = compat.AdaptiveColor{
  176. Dark: lipgloss.Color("3"), // yellow
  177. Light: lipgloss.Color("3"),
  178. }
  179. t.SyntaxTypeColor = compat.AdaptiveColor{
  180. Dark: lipgloss.Color("6"), // cyan
  181. Light: lipgloss.Color("6"),
  182. }
  183. t.SyntaxOperatorColor = compat.AdaptiveColor{
  184. Dark: lipgloss.Color("6"), // cyan
  185. Light: lipgloss.Color("6"),
  186. }
  187. t.SyntaxPunctuationColor = compat.AdaptiveColor{
  188. Dark: lipgloss.NoColor{},
  189. Light: lipgloss.NoColor{},
  190. }
  191. }
  192. // generateGrayScale creates a gray scale based on the terminal background
  193. func (t *SystemTheme) generateGrayScale() map[int]compat.AdaptiveColor {
  194. grays := make(map[int]compat.AdaptiveColor)
  195. r, g, b, _ := t.terminalBg.RGBA()
  196. bgR := float64(r >> 8)
  197. bgG := float64(g >> 8)
  198. bgB := float64(b >> 8)
  199. luminance := 0.299*bgR + 0.587*bgG + 0.114*bgB
  200. for i := 1; i <= 12; i++ {
  201. var stepColor string
  202. factor := float64(i) / 12.0
  203. if t.terminalBgIsDark {
  204. if luminance < 10 {
  205. grayValue := int(factor * 0.4 * 255)
  206. stepColor = fmt.Sprintf("#%02x%02x%02x", grayValue, grayValue, grayValue)
  207. } else {
  208. newLum := luminance + (255-luminance)*factor*0.4
  209. ratio := newLum / luminance
  210. newR := math.Min(bgR*ratio, 255)
  211. newG := math.Min(bgG*ratio, 255)
  212. newB := math.Min(bgB*ratio, 255)
  213. stepColor = fmt.Sprintf("#%02x%02x%02x", int(newR), int(newG), int(newB))
  214. }
  215. } else {
  216. if luminance > 245 {
  217. grayValue := int(255 - factor*0.4*255)
  218. stepColor = fmt.Sprintf("#%02x%02x%02x", grayValue, grayValue, grayValue)
  219. } else {
  220. newLum := luminance * (1 - factor*0.4)
  221. ratio := newLum / luminance
  222. newR := math.Max(bgR*ratio, 0)
  223. newG := math.Max(bgG*ratio, 0)
  224. newB := math.Max(bgB*ratio, 0)
  225. stepColor = fmt.Sprintf("#%02x%02x%02x", int(newR), int(newG), int(newB))
  226. }
  227. }
  228. grays[i] = compat.AdaptiveColor{
  229. Dark: lipgloss.Color(stepColor),
  230. Light: lipgloss.Color(stepColor),
  231. }
  232. }
  233. return grays
  234. }
  235. // generateMutedTextColor creates a muted gray color based on the terminal background
  236. func (t *SystemTheme) generateMutedTextColor() compat.AdaptiveColor {
  237. bgR, bgG, bgB, _ := t.terminalBg.RGBA()
  238. bgRf := float64(bgR >> 8)
  239. bgGf := float64(bgG >> 8)
  240. bgBf := float64(bgB >> 8)
  241. bgLum := 0.299*bgRf + 0.587*bgGf + 0.114*bgBf
  242. var grayValue int
  243. if t.terminalBgIsDark {
  244. if bgLum < 10 {
  245. // Very dark/black background
  246. // grays[3] would be around #2e (46), so we need much lighter
  247. grayValue = 180 // #b4b4b4
  248. } else {
  249. // Scale up for lighter dark backgrounds
  250. // Ensure we're always significantly brighter than BackgroundElement
  251. grayValue = min(int(160+(bgLum*0.3)), 200)
  252. }
  253. } else {
  254. if bgLum > 245 {
  255. // Very light/white background
  256. // grays[3] would be around #f5 (245), so we need much darker
  257. grayValue = 75 // #4b4b4b
  258. } else {
  259. // Scale down for darker light backgrounds
  260. // Ensure we're always significantly darker than BackgroundElement
  261. grayValue = max(int(100-((255-bgLum)*0.2)), 60)
  262. }
  263. }
  264. mutedColor := fmt.Sprintf("#%02x%02x%02x", grayValue, grayValue, grayValue)
  265. return compat.AdaptiveColor{
  266. Dark: lipgloss.Color(mutedColor),
  267. Light: lipgloss.Color(mutedColor),
  268. }
  269. }