system.go 8.1 KB

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