v0.20.0 c741a39

manager.go RawHistoryFolder

1package theme
2
3import (
4	"context"
5	"fmt"
6
7	"github.com/bnema/dumber/internal/infrastructure/config"
8	"github.com/bnema/dumber/internal/logging"
9	"github.com/jwijenbergh/puregotk/v4/gdk"
10	"github.com/jwijenbergh/puregotk/v4/gtk"
11)
12
13// Manager handles theme state and CSS application.
14type Manager struct {
15	scheme       string  // "light", "dark", "system"
16	prefersDark  bool    // Resolved dark mode preference
17	lightPalette Palette // Light theme colors
18	darkPalette  Palette // Dark theme colors
19	uiScale      float64 // UI scaling factor (1.0 = 100%)
20	fonts        FontConfig
21	cssProvider  *gtk.CssProvider
22}
23
24// NewManager creates a new theme manager from configuration.
25func NewManager(ctx context.Context, cfg *config.Config) *Manager {
26	log := logging.FromContext(ctx)
27
28	// Determine color scheme preference
29	scheme := "system"
30	if cfg != nil && cfg.Appearance.ColorScheme != "" {
31		scheme = cfg.Appearance.ColorScheme
32	}
33
34	// Resolve whether we should use dark mode
35	prefersDark := ResolveColorScheme(scheme)
36
37	// Build palettes from config or defaults
38	var lightPalette, darkPalette Palette
39	if cfg != nil {
40		lightPalette = PaletteFromConfig(&cfg.Appearance.LightPalette, false)
41		darkPalette = PaletteFromConfig(&cfg.Appearance.DarkPalette, true)
42	} else {
43		lightPalette = DefaultLightPalette()
44		darkPalette = DefaultDarkPalette()
45	}
46
47	// Get UI scale factor (default to 1.0 if not set)
48	uiScale := 1.0
49	if cfg != nil && cfg.DefaultUIScale > 0 {
50		uiScale = cfg.DefaultUIScale
51	}
52
53	fonts := DefaultFontConfig()
54	if cfg != nil {
55		fonts.SansFont = Coalesce(cfg.Appearance.SansFont, fonts.SansFont)
56		fonts.MonospaceFont = Coalesce(cfg.Appearance.MonospaceFont, fonts.MonospaceFont)
57	}
58
59	log.Debug().
60		Str("scheme", scheme).
61		Bool("prefers_dark", prefersDark).
62		Float64("ui_scale", uiScale).
63		Str("sans_font", fonts.SansFont).
64		Str("monospace_font", fonts.MonospaceFont).
65		Msg("theme manager initialized")
66
67	return &Manager{
68		scheme:       scheme,
69		prefersDark:  prefersDark,
70		lightPalette: lightPalette,
71		darkPalette:  darkPalette,
72		uiScale:      uiScale,
73		fonts:        fonts,
74	}
75}
76
77// PrefersDark returns true if dark mode is active.
78func (m *Manager) PrefersDark() bool {
79	return m.prefersDark
80}
81
82// GetCurrentPalette returns the active palette based on current scheme.
83func (m *Manager) GetCurrentPalette() Palette {
84	if m.prefersDark {
85		return m.darkPalette
86	}
87	return m.lightPalette
88}
89
90// GetLightPalette returns the light theme palette.
91func (m *Manager) GetLightPalette() Palette {
92	return m.lightPalette
93}
94
95// GetDarkPalette returns the dark theme palette.
96func (m *Manager) GetDarkPalette() Palette {
97	return m.darkPalette
98}
99
100// GetWebUIThemeCSS returns CSS text that defines both light and dark variables.
101// WebUI uses `:root` for light and `.dark` overrides for dark.
102func (m *Manager) GetWebUIThemeCSS() string {
103	lightVars := m.lightPalette.ToWebCSSVars()
104	darkVars := m.darkPalette.ToWebCSSVars()
105	return fmt.Sprintf(":root{\n%s}\n\n.dark{\n%s}\n", lightVars, darkVars)
106}
107
108// ApplyToDisplay loads the theme CSS into the display.
109func (m *Manager) ApplyToDisplay(ctx context.Context, display *gdk.Display) {
110	log := logging.FromContext(ctx)
111
112	if display == nil {
113		log.Warn().Msg("cannot apply theme: display is nil")
114		return
115	}
116
117	// Generate CSS with current palette, UI scale and fonts
118	palette := m.GetCurrentPalette()
119	css := GenerateCSSWithScaleAndFonts(palette, m.uiScale, m.fonts)
120
121	// Create CSS provider if needed
122	if m.cssProvider == nil {
123		m.cssProvider = gtk.NewCssProvider()
124	}
125
126	if m.cssProvider == nil {
127		log.Error().Msg("failed to create CSS provider")
128		return
129	}
130
131	// Load CSS
132	m.cssProvider.LoadFromString(css)
133	gtk.StyleContextAddProviderForDisplay(
134		display,
135		m.cssProvider,
136		uint(gtk.STYLE_PROVIDER_PRIORITY_APPLICATION),
137	)
138
139	log.Debug().
140		Bool("dark_mode", m.prefersDark).
141		Msg("theme CSS applied to display")
142}
143
144// SetColorScheme changes the active color scheme at runtime.
145func (m *Manager) SetColorScheme(ctx context.Context, scheme string, display *gdk.Display) {
146	log := logging.FromContext(ctx)
147
148	m.scheme = scheme
149	m.prefersDark = ResolveColorScheme(scheme)
150
151	log.Info().
152		Str("scheme", scheme).
153		Bool("prefers_dark", m.prefersDark).
154		Msg("color scheme changed")
155
156	// Re-apply CSS if display is available
157	if display != nil {
158		m.ApplyToDisplay(ctx, display)
159	}
160}
161
162// UpdateFromConfig updates the theme manager state from a new config.
163func (m *Manager) UpdateFromConfig(ctx context.Context, cfg *config.Config, display *gdk.Display) {
164	log := logging.FromContext(ctx)
165
166	if cfg == nil {
167		return
168	}
169
170	// Update scheme and resolve prefersDark
171	scheme := "system"
172	if cfg.Appearance.ColorScheme != "" {
173		scheme = cfg.Appearance.ColorScheme
174	}
175	m.scheme = scheme
176	m.prefersDark = ResolveColorScheme(scheme)
177
178	// Update palettes
179	m.lightPalette = PaletteFromConfig(&cfg.Appearance.LightPalette, false)
180	m.darkPalette = PaletteFromConfig(&cfg.Appearance.DarkPalette, true)
181
182	// Update UI scale
183	if cfg.DefaultUIScale > 0 {
184		m.uiScale = cfg.DefaultUIScale
185	}
186
187	defaults := DefaultFontConfig()
188	m.fonts = FontConfig{
189		SansFont:      Coalesce(cfg.Appearance.SansFont, defaults.SansFont),
190		MonospaceFont: Coalesce(cfg.Appearance.MonospaceFont, defaults.MonospaceFont),
191	}
192
193	log.Info().
194		Str("scheme", m.scheme).
195		Bool("prefers_dark", m.prefersDark).
196		Float64("ui_scale", m.uiScale).
197		Str("sans_font", m.fonts.SansFont).
198		Str("monospace_font", m.fonts.MonospaceFont).
199		Msg("theme manager updated from config")
200
201	// Re-apply CSS if display is available
202	if display != nil {
203		m.ApplyToDisplay(ctx, display)
204	}
205}

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