1package entity
2
3import "regexp"
4
5// ThemeSourceKind describes where the resolved palette colors originated.
6type ThemeSourceKind string
7
8const (
9 // ThemeSourceDefault indicates palette colors come from built-in defaults.
10 ThemeSourceDefault ThemeSourceKind = "default"
11 // ThemeSourceConfig indicates palette colors come from user configuration.
12 ThemeSourceConfig ThemeSourceKind = "config"
13 // ThemeSourceExternal indicates palette colors come from an external theme source.
14 ThemeSourceExternal ThemeSourceKind = "external"
15)
16
17// ThemeSourceMetadata describes the palette source used for a resolved theme.
18type ThemeSourceMetadata struct {
19 Kind ThemeSourceKind
20 Provider string
21 LastGood bool
22}
23
24// ThemeWarning represents a non-fatal issue encountered during theme resolution.
25type ThemeWarning struct {
26 Field string
27 Message string
28}
29
30// ThemeFonts holds the resolved font families and default size.
31type ThemeFonts struct {
32 SansFont string
33 SerifFont string
34 MonospaceFont string
35 GtkFont string
36 DefaultSize int
37}
38
39// ThemeModeColors holds mode indicator colors.
40// All values must be CSS-safe hex colors (e.g., "#4A90E2").
41type ThemeModeColors struct {
42 PaneMode string
43 VimMode string
44 TabMode string
45 SessionMode string
46 ResizeMode string
47}
48
49// ResolvedThemePalette is a CSS-safe hex color palette.
50// Alias for ColorPalette with the contract that all color strings
51// are valid CSS hex colors (validated during resolution).
52type ResolvedThemePalette = ColorPalette
53
54// ExternalTheme represents palette data from an external source (e.g., Noctalia).
55// External themes override palette colors only; fonts, UI scale, and mode colors stay in Dumber config.
56// It must not depend on GTK, WebKit, CEF, lipgloss, fsnotify, Viper, or infrastructure config.
57type ExternalTheme struct {
58 Name string
59 Provider string
60
61 // LightPalette and DarkPalette may contain partial overrides.
62 // Nil palette pointers are malformed; empty fields mean "use the resolved config/default field".
63 LightPalette *ColorPalette
64 DarkPalette *ColorPalette
65}
66
67// ResolvedTheme is the final resolved theme output.
68// It contains palettes, fonts, colors, scale, and metadata.
69// All palette colors are guaranteed to be CSS-safe hex colors.
70// CSS generation is NOT part of this core type — it is a separate adapter concern.
71type ResolvedTheme struct {
72 LightPalette ResolvedThemePalette
73 DarkPalette ResolvedThemePalette
74 ActivePalette ResolvedThemePalette
75 PrefersDark bool
76 ColorSchemeSource string
77 ThemeSource ThemeSourceMetadata
78 Fonts ThemeFonts
79 UIScale float64
80 ModeColors ThemeModeColors
81 Warnings []ThemeWarning
82}
83
84// hexColorRegex matches valid CSS-safe #RRGGBB hex colors (strict).
85var hexColorRegex = regexp.MustCompile(`^#[0-9a-fA-F]{6}$`)
86
87// IsValidHex returns true if s is a valid CSS-safe hex color (#RRGGBB format only).
88func IsValidHex(s string) bool {
89 return hexColorRegex.MatchString(s)
90}
91
92// DefaultThemeFonts returns Dumber's appearance font defaults.
93func DefaultThemeFonts() ThemeFonts {
94 return ThemeFonts{
95 SansFont: "Fira Sans",
96 SerifFont: "Fira Sans",
97 MonospaceFont: "Fira Code",
98 GtkFont: "Adwaita Sans",
99 DefaultSize: 16,
100 }
101}
102
103// DefaultThemeModeColors returns the default mode indicator colors.
104// These match the existing default mode colors from config/infrastructure.
105func DefaultThemeModeColors() ThemeModeColors {
106 return ThemeModeColors{
107 PaneMode: "#4A90E2",
108 VimMode: "#F7768E",
109 TabMode: "#FFA500",
110 SessionMode: "#9B59B6",
111 ResizeMode: "#00D4AA",
112 }
113}
114
115// DefaultDarkPalette returns the built-in dark theme palette.
116// These values mirror the config default dark palette.
117func DefaultDarkPalette() ColorPalette {
118 return ColorPalette{
119 Background: "#0a0a0b",
120 Surface: "#18181b",
121 SurfaceVariant: "#27272a",
122 Text: "#fafafa",
123 Muted: "#a1a1aa",
124 Accent: "#4ade80",
125 Border: "#3f3f46",
126 }
127}
128
129// DefaultLightPalette returns the built-in light theme palette.
130// These values mirror the config default light palette.
131func DefaultLightPalette() ColorPalette {
132 return ColorPalette{
133 Background: "#fafafa",
134 Surface: "#f4f4f5",
135 SurfaceVariant: "#e4e4e7",
136 Text: "#18181b",
137 Muted: "#71717a",
138 Accent: "#22c55e",
139 Border: "#d4d4d8",
140 }
141}
142
143// ValidatePaletteHex checks every field in the palette is a valid #RRGGBB hex color.
144// Returns a slice of warnings for each invalid field. Empty slice means valid.
145func ValidatePaletteHex(p *ColorPalette, prefix string) []ThemeWarning {
146 if p == nil {
147 return []ThemeWarning{{Field: prefix, Message: "palette is nil"}}
148 }
149 var warnings []ThemeWarning
150 checkRequiredHex(&warnings, prefix, "background", p.Background)
151 checkRequiredHex(&warnings, prefix, "surface", p.Surface)
152 checkRequiredHex(&warnings, prefix, "surface_variant", p.SurfaceVariant)
153 checkRequiredHex(&warnings, prefix, "text", p.Text)
154 checkRequiredHex(&warnings, prefix, "muted", p.Muted)
155 checkRequiredHex(&warnings, prefix, "accent", p.Accent)
156 checkRequiredHex(&warnings, prefix, "border", p.Border)
157 return warnings
158}
159
160// ValidatePaletteOverrideHex checks non-empty override fields are valid #RRGGBB hex colors.
161// Empty fields are accepted and mean "inherit from the fallback palette".
162func ValidatePaletteOverrideHex(p *ColorPalette, prefix string) []ThemeWarning {
163 if p == nil {
164 return []ThemeWarning{{Field: prefix, Message: "palette is nil"}}
165 }
166 var warnings []ThemeWarning
167 checkOptionalHex(&warnings, prefix, "background", p.Background)
168 checkOptionalHex(&warnings, prefix, "surface", p.Surface)
169 checkOptionalHex(&warnings, prefix, "surface_variant", p.SurfaceVariant)
170 checkOptionalHex(&warnings, prefix, "text", p.Text)
171 checkOptionalHex(&warnings, prefix, "muted", p.Muted)
172 checkOptionalHex(&warnings, prefix, "accent", p.Accent)
173 checkOptionalHex(&warnings, prefix, "border", p.Border)
174 return warnings
175}
176
177func checkRequiredHex(warnings *[]ThemeWarning, prefix, field, value string) {
178 if !IsValidHex(value) {
179 *warnings = append(*warnings, ThemeWarning{
180 Field: prefix + "." + field,
181 Message: "not a valid CSS-safe hex color (#RRGGBB)",
182 })
183 }
184}
185
186func checkOptionalHex(warnings *[]ThemeWarning, prefix, field, value string) {
187 if value != "" && !IsValidHex(value) {
188 *warnings = append(*warnings, ThemeWarning{
189 Field: prefix + "." + field,
190 Message: "not a valid CSS-safe hex color (#RRGGBB)",
191 })
192 }
193}
194
195// IsPaletteValid returns true if every field in the palette is a valid #RRGGBB hex color.
196func IsPaletteValid(p *ColorPalette) bool {
197 return len(ValidatePaletteHex(p, "palette")) == 0
198}
199
200// IsPaletteOverrideValid returns true if every non-empty field in the palette is a valid #RRGGBB hex color.
201func IsPaletteOverrideValid(p *ColorPalette) bool {
202 return len(ValidatePaletteOverrideHex(p, "palette")) == 0
203}
204
205// MergePalette returns a new ColorPalette where empty fields in src are filled from defaults.
206// Both src and defaults may be nil. Returns a copy, never mutates inputs.
207func MergePalette(src, defaults *ColorPalette) ColorPalette {
208 var d ColorPalette
209 if defaults != nil {
210 d = *defaults
211 }
212 if src == nil {
213 return d
214 }
215 result := *src
216 if result.Background == "" {
217 result.Background = d.Background
218 }
219 if result.Surface == "" {
220 result.Surface = d.Surface
221 }
222 if result.SurfaceVariant == "" {
223 result.SurfaceVariant = d.SurfaceVariant
224 }
225 if result.Text == "" {
226 result.Text = d.Text
227 }
228 if result.Muted == "" {
229 result.Muted = d.Muted
230 }
231 if result.Accent == "" {
232 result.Accent = d.Accent
233 }
234 if result.Border == "" {
235 result.Border = d.Border
236 }
237 return result
238}
239
240// MergeFonts returns a ThemeFonts where zero/empty fields in src are filled from defaults.
241func MergeFonts(src *ThemeFonts, defaults ThemeFonts) ThemeFonts {
242 if src == nil {
243 return defaults
244 }
245 result := *src
246 if result.SansFont == "" {
247 result.SansFont = defaults.SansFont
248 }
249 if result.SerifFont == "" {
250 result.SerifFont = defaults.SerifFont
251 }
252 if result.MonospaceFont == "" {
253 result.MonospaceFont = defaults.MonospaceFont
254 }
255 if result.GtkFont == "" {
256 result.GtkFont = defaults.GtkFont
257 }
258 if result.DefaultSize <= 0 {
259 result.DefaultSize = defaults.DefaultSize
260 }
261 return result
262}
263
264// ValidateModeColorsHex checks every mode color field is a valid #RRGGBB hex color.
265func ValidateModeColorsHex(colors *ThemeModeColors, prefix string) []ThemeWarning {
266 if colors == nil {
267 return []ThemeWarning{{Field: prefix, Message: "mode colors are nil"}}
268 }
269 var warnings []ThemeWarning
270 checkRequiredHex(&warnings, prefix, "pane_mode", colors.PaneMode)
271 checkRequiredHex(&warnings, prefix, "vim_mode", colors.VimMode)
272 checkRequiredHex(&warnings, prefix, "tab_mode", colors.TabMode)
273 checkRequiredHex(&warnings, prefix, "session_mode", colors.SessionMode)
274 checkRequiredHex(&warnings, prefix, "resize_mode", colors.ResizeMode)
275 return warnings
276}
277
278// IsModeColorsValid returns true when every mode color is a valid #RRGGBB hex color.
279func IsModeColorsValid(colors *ThemeModeColors) bool {
280 return len(ValidateModeColorsHex(colors, "mode_colors")) == 0
281}
282
283// MergeModeColors returns a ThemeModeColors where empty fields in src are filled from defaults.
284func MergeModeColors(src *ThemeModeColors, defaults ThemeModeColors) ThemeModeColors {
285 if src == nil {
286 return defaults
287 }
288 result := *src
289 if result.PaneMode == "" {
290 result.PaneMode = defaults.PaneMode
291 }
292 if result.VimMode == "" {
293 result.VimMode = defaults.VimMode
294 }
295 if result.TabMode == "" {
296 result.TabMode = defaults.TabMode
297 }
298 if result.SessionMode == "" {
299 result.SessionMode = defaults.SessionMode
300 }
301 if result.ResizeMode == "" {
302 result.ResizeMode = defaults.ResizeMode
303 }
304 return result
305}