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