v0.33.0 62a09f5

theme.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close