detached 82506a7

refactor(bootstrap): wire ColorSchemeResolver through initialization, enable adwaita detector after adw.Init()

commit
82506a7499a8fccaadc878aedbdeb546e8209d72
author
bnema · · 2025-12-29 20:51 UTC
parent
2553067
signature
GPG Unverified2B82207D370127A0 The key is unknown
5 files · +167 -77
M cmd/dumber/main.go +25 -7
@@ -18,6 +18,7 @@ import (
1818 "github.com/bnema/dumber/internal/domain/repository"
1919 "github.com/bnema/dumber/internal/infrastructure/cache"
2020 "github.com/bnema/dumber/internal/infrastructure/clipboard"
21+ "github.com/bnema/dumber/internal/infrastructure/colorscheme"
2122 "github.com/bnema/dumber/internal/infrastructure/config"
2223 "github.com/bnema/dumber/internal/infrastructure/deps"
2324 "github.com/bnema/dumber/internal/infrastructure/favicon"
@@ -130,7 +131,11 @@ func runGUI() int {
130131 timer.Mark("use_cases")
131132
132133 // Build UI
133- uiDeps := buildUIDependencies(ctx, cfg, initResult.ThemeManager, &stack, repos, useCases, idleInhibitor, browserSession.Session.ID)
134+ uiDeps := buildUIDependencies(
135+ ctx, cfg, initResult.ThemeManager,
136+ initResult.ColorResolver, initResult.AdwaitaDetector,
137+ &stack, repos, useCases, idleInhibitor, browserSession.Session.ID,
138+ )
134139 configureDeferredInit(uiDeps, cfg, browserSession)
135140 app, err := ui.New(uiDeps)
136141 if err != nil {
@@ -171,11 +176,12 @@ func initStackAndRepos(
171176 if needsEagerDB {
172177 // Parallel phase 2: Database + WebKit stack initialize concurrently
173178 dbWebKit, err := bootstrap.RunParallelDBWebKit(bootstrap.ParallelDBWebKitInput{
174- Ctx: ctx,
175- Config: cfg,
176- DataDir: initResult.DataDir,
177- CacheDir: initResult.CacheDir,
178- ThemeManager: initResult.ThemeManager,
179+ Ctx: ctx,
180+ Config: cfg,
181+ DataDir: initResult.DataDir,
182+ CacheDir: initResult.CacheDir,
183+ ThemeManager: initResult.ThemeManager,
184+ ColorResolver: initResult.ColorResolver,
179185 })
180186 if err != nil {
181187 return bootstrap.WebKitStack{}, nil, nil, err
@@ -184,7 +190,15 @@ func initStackAndRepos(
184190 }
185191
186192 log := logging.FromContext(ctx)
187- stack := bootstrap.BuildWebKitStack(ctx, cfg, initResult.DataDir, initResult.CacheDir, initResult.ThemeManager, *log)
193+ stack := bootstrap.BuildWebKitStack(bootstrap.WebKitStackInput{
194+ Ctx: ctx,
195+ Config: cfg,
196+ DataDir: initResult.DataDir,
197+ CacheDir: initResult.CacheDir,
198+ ThemeManager: initResult.ThemeManager,
199+ ColorResolver: initResult.ColorResolver,
200+ Logger: *log,
201+ })
188202
189203 lazyDB, err := bootstrap.CreateLazyDatabase()
190204 if err != nil {
@@ -398,6 +412,8 @@ func buildUIDependencies(
398412 ctx context.Context,
399413 cfg *config.Config,
400414 themeManager *theme.Manager,
415+ colorResolver port.ColorSchemeResolver,
416+ adwaitaDetector *colorscheme.AdwaitaDetector,
401417 stack *bootstrap.WebKitStack,
402418 repos *repositories,
403419 uc *useCases,
@@ -410,6 +426,8 @@ func buildUIDependencies(
410426 InitialURL: initialURL,
411427 RestoreSessionID: restoreSessionID,
412428 Theme: themeManager,
429+ ColorResolver: colorResolver,
430+ AdwaitaDetector: adwaitaDetector,
413431 WebContext: stack.Context,
414432 Pool: stack.Pool,
415433 Settings: stack.Settings,
@@ -18,6 +18,7 @@ import (
18 "github.com/bnema/dumber/internal/domain/repository"18 "github.com/bnema/dumber/internal/domain/repository"
19 "github.com/bnema/dumber/internal/infrastructure/cache"19 "github.com/bnema/dumber/internal/infrastructure/cache"
20 "github.com/bnema/dumber/internal/infrastructure/clipboard"20 "github.com/bnema/dumber/internal/infrastructure/clipboard"
21+ "github.com/bnema/dumber/internal/infrastructure/colorscheme"
21 "github.com/bnema/dumber/internal/infrastructure/config"22 "github.com/bnema/dumber/internal/infrastructure/config"
22 "github.com/bnema/dumber/internal/infrastructure/deps"23 "github.com/bnema/dumber/internal/infrastructure/deps"
23 "github.com/bnema/dumber/internal/infrastructure/favicon"24 "github.com/bnema/dumber/internal/infrastructure/favicon"
@@ -130,7 +131,11 @@ func runGUI() int {
130 timer.Mark("use_cases")131 timer.Mark("use_cases")
131 132
132 // Build UI133 // Build UI
133- uiDeps := buildUIDependencies(ctx, cfg, initResult.ThemeManager, &stack, repos, useCases, idleInhibitor, browserSession.Session.ID)134+ uiDeps := buildUIDependencies(
135+ ctx, cfg, initResult.ThemeManager,
136+ initResult.ColorResolver, initResult.AdwaitaDetector,
137+ &stack, repos, useCases, idleInhibitor, browserSession.Session.ID,
138+ )
134 configureDeferredInit(uiDeps, cfg, browserSession)139 configureDeferredInit(uiDeps, cfg, browserSession)
135 app, err := ui.New(uiDeps)140 app, err := ui.New(uiDeps)
136 if err != nil {141 if err != nil {
@@ -171,11 +176,12 @@ func initStackAndRepos(
171 if needsEagerDB {176 if needsEagerDB {
172 // Parallel phase 2: Database + WebKit stack initialize concurrently177 // Parallel phase 2: Database + WebKit stack initialize concurrently
173 dbWebKit, err := bootstrap.RunParallelDBWebKit(bootstrap.ParallelDBWebKitInput{178 dbWebKit, err := bootstrap.RunParallelDBWebKit(bootstrap.ParallelDBWebKitInput{
174- Ctx: ctx,179+ Ctx: ctx,
175- Config: cfg,180+ Config: cfg,
176- DataDir: initResult.DataDir,181+ DataDir: initResult.DataDir,
177- CacheDir: initResult.CacheDir,182+ CacheDir: initResult.CacheDir,
178- ThemeManager: initResult.ThemeManager,183+ ThemeManager: initResult.ThemeManager,
184+ ColorResolver: initResult.ColorResolver,
179 })185 })
180 if err != nil {186 if err != nil {
181 return bootstrap.WebKitStack{}, nil, nil, err187 return bootstrap.WebKitStack{}, nil, nil, err
@@ -184,7 +190,15 @@ func initStackAndRepos(
184 }190 }
185 191
186 log := logging.FromContext(ctx)192 log := logging.FromContext(ctx)
187- stack := bootstrap.BuildWebKitStack(ctx, cfg, initResult.DataDir, initResult.CacheDir, initResult.ThemeManager, *log)193+ stack := bootstrap.BuildWebKitStack(bootstrap.WebKitStackInput{
194+ Ctx: ctx,
195+ Config: cfg,
196+ DataDir: initResult.DataDir,
197+ CacheDir: initResult.CacheDir,
198+ ThemeManager: initResult.ThemeManager,
199+ ColorResolver: initResult.ColorResolver,
200+ Logger: *log,
201+ })
188 202
189 lazyDB, err := bootstrap.CreateLazyDatabase()203 lazyDB, err := bootstrap.CreateLazyDatabase()
190 if err != nil {204 if err != nil {
@@ -398,6 +412,8 @@ func buildUIDependencies(
398 ctx context.Context,412 ctx context.Context,
399 cfg *config.Config,413 cfg *config.Config,
400 themeManager *theme.Manager,414 themeManager *theme.Manager,
415+ colorResolver port.ColorSchemeResolver,
416+ adwaitaDetector *colorscheme.AdwaitaDetector,
401 stack *bootstrap.WebKitStack,417 stack *bootstrap.WebKitStack,
402 repos *repositories,418 repos *repositories,
403 uc *useCases,419 uc *useCases,
@@ -410,6 +426,8 @@ func buildUIDependencies(
410 InitialURL: initialURL,426 InitialURL: initialURL,
411 RestoreSessionID: restoreSessionID,427 RestoreSessionID: restoreSessionID,
412 Theme: themeManager,428 Theme: themeManager,
429+ ColorResolver: colorResolver,
430+ AdwaitaDetector: adwaitaDetector,
413 WebContext: stack.Context,431 WebContext: stack.Context,
414 Pool: stack.Pool,432 Pool: stack.Pool,
415 Settings: stack.Settings,433 Settings: stack.Settings,
M internal/bootstrap/gui.go +46 -17
@@ -10,7 +10,9 @@ import (
1010 "sync"
1111 "time"
1212
13+ "github.com/bnema/dumber/internal/application/port"
1314 "github.com/bnema/dumber/internal/application/usecase"
15+ "github.com/bnema/dumber/internal/infrastructure/colorscheme"
1416 "github.com/bnema/dumber/internal/infrastructure/config"
1517 "github.com/bnema/dumber/internal/infrastructure/deps"
1618 "github.com/bnema/dumber/internal/infrastructure/media"
@@ -28,10 +30,12 @@ type DatabaseResult struct {
2830
2931 // ParallelInitResult holds the results of parallel initialization phase.
3032 type ParallelInitResult struct {
31- DataDir string
32- CacheDir string
33- ThemeManager *theme.Manager
34- Duration time.Duration
33+ DataDir string
34+ CacheDir string
35+ ThemeManager *theme.Manager
36+ ColorResolver port.ColorSchemeResolver
37+ AdwaitaDetector *colorscheme.AdwaitaDetector
38+ Duration time.Duration
3539 }
3640
3741 // DeferredInitResult holds results from deferred initialization checks.
@@ -84,17 +88,30 @@ func (e *RuntimeRequirementsError) LogDetails(ctx context.Context) {
8488 }
8589
8690 // RunParallelInit runs the essential parallel initialization phase.
87-// This includes directory resolution and theme creation.
91+// This includes directory resolution, color scheme resolver creation, and theme creation.
8892 // Returns the first fatal error encountered, or nil with the results.
8993 func RunParallelInit(input ParallelInitInput) (*ParallelInitResult, error) {
9094 var (
9195 dataDir, cacheDir string
92- themeManager *theme.Manager
9396 dirsErr error
9497 wg sync.WaitGroup
9598 )
9699
97100 start := time.Now()
101+
102+ // Create color scheme resolver with initial detectors.
103+ // AdwaitaDetector is created but NOT marked available yet (requires adw.Init()).
104+ configAdapter := colorscheme.NewConfigAdapter(input.Config)
105+ resolver := colorscheme.NewResolver(configAdapter)
106+
107+ // Register detectors that are available before GTK init
108+ resolver.RegisterDetector(colorscheme.NewEnvDetector())
109+ resolver.RegisterDetector(colorscheme.NewGsettingsDetector())
110+
111+ // Create adwaita detector (will be marked available after adw.Init())
112+ adwaitaDetector := colorscheme.NewAdwaitaDetector()
113+ resolver.RegisterDetector(adwaitaDetector)
114+
98115 wg.Add(2)
99116
100117 // Resolve directories
@@ -104,9 +121,10 @@ func RunParallelInit(input ParallelInitInput) (*ParallelInitResult, error) {
104121 }()
105122
106123 // Theme manager (CPU-bound, no I/O)
124+ var themeManager *theme.Manager
107125 go func() {
108126 defer wg.Done()
109- themeManager = theme.NewManager(input.Ctx, input.Config)
127+ themeManager = theme.NewManager(input.Ctx, input.Config, resolver)
110128 }()
111129
112130 wg.Wait()
@@ -118,10 +136,12 @@ func RunParallelInit(input ParallelInitInput) (*ParallelInitResult, error) {
118136 }
119137
120138 return &ParallelInitResult{
121- DataDir: dataDir,
122- CacheDir: cacheDir,
123- ThemeManager: themeManager,
124- Duration: duration,
139+ DataDir: dataDir,
140+ CacheDir: cacheDir,
141+ ThemeManager: themeManager,
142+ ColorResolver: resolver,
143+ AdwaitaDetector: adwaitaDetector,
144+ Duration: duration,
125145 }, nil
126146 }
127147
@@ -253,11 +273,12 @@ type ParallelDBWebKitResult struct {
253273
254274 // ParallelDBWebKitInput holds inputs for parallel DB and WebKit initialization.
255275 type ParallelDBWebKitInput struct {
256- Ctx context.Context
257- Config *config.Config
258- DataDir string // For WebKit context
259- CacheDir string // For WebKit cache
260- ThemeManager *theme.Manager
276+ Ctx context.Context
277+ Config *config.Config
278+ DataDir string // For WebKit context
279+ CacheDir string // For WebKit cache
280+ ThemeManager *theme.Manager
281+ ColorResolver port.ColorSchemeResolver
261282 }
262283
263284 // Note: Database path is resolved via config.GetDatabaseFile() internally.
@@ -291,7 +312,15 @@ func RunParallelDBWebKit(input ParallelDBWebKitInput) (*ParallelDBWebKitResult,
291312 }()
292313
293314 // WebKit stack on main thread (GTK requirement)
294- stack := BuildWebKitStack(input.Ctx, input.Config, input.DataDir, input.CacheDir, input.ThemeManager, *log)
315+ stack := BuildWebKitStack(WebKitStackInput{
316+ Ctx: input.Ctx,
317+ Config: input.Config,
318+ DataDir: input.DataDir,
319+ CacheDir: input.CacheDir,
320+ ThemeManager: input.ThemeManager,
321+ ColorResolver: input.ColorResolver,
322+ Logger: *log,
323+ })
295324
296325 // Wait for database
297326 dbRes := <-dbCh
@@ -10,7 +10,9 @@ import (
10 "sync"10 "sync"
11 "time"11 "time"
12 12
13+ "github.com/bnema/dumber/internal/application/port"
13 "github.com/bnema/dumber/internal/application/usecase"14 "github.com/bnema/dumber/internal/application/usecase"
15+ "github.com/bnema/dumber/internal/infrastructure/colorscheme"
14 "github.com/bnema/dumber/internal/infrastructure/config"16 "github.com/bnema/dumber/internal/infrastructure/config"
15 "github.com/bnema/dumber/internal/infrastructure/deps"17 "github.com/bnema/dumber/internal/infrastructure/deps"
16 "github.com/bnema/dumber/internal/infrastructure/media"18 "github.com/bnema/dumber/internal/infrastructure/media"
@@ -28,10 +30,12 @@ type DatabaseResult struct {
28 30
29 // ParallelInitResult holds the results of parallel initialization phase.31 // ParallelInitResult holds the results of parallel initialization phase.
30 type ParallelInitResult struct {32 type ParallelInitResult struct {
31- DataDir string33+ DataDir string
32- CacheDir string34+ CacheDir string
33- ThemeManager *theme.Manager35+ ThemeManager *theme.Manager
34- Duration time.Duration36+ ColorResolver port.ColorSchemeResolver
37+ AdwaitaDetector *colorscheme.AdwaitaDetector
38+ Duration time.Duration
35 }39 }
36 40
37 // DeferredInitResult holds results from deferred initialization checks.41 // DeferredInitResult holds results from deferred initialization checks.
@@ -84,17 +88,30 @@ func (e *RuntimeRequirementsError) LogDetails(ctx context.Context) {
84 }88 }
85 89
86 // RunParallelInit runs the essential parallel initialization phase.90 // RunParallelInit runs the essential parallel initialization phase.
87-// This includes directory resolution and theme creation.91+// This includes directory resolution, color scheme resolver creation, and theme creation.
88 // Returns the first fatal error encountered, or nil with the results.92 // Returns the first fatal error encountered, or nil with the results.
89 func RunParallelInit(input ParallelInitInput) (*ParallelInitResult, error) {93 func RunParallelInit(input ParallelInitInput) (*ParallelInitResult, error) {
90 var (94 var (
91 dataDir, cacheDir string95 dataDir, cacheDir string
92- themeManager *theme.Manager
93 dirsErr error96 dirsErr error
94 wg sync.WaitGroup97 wg sync.WaitGroup
95 )98 )
96 99
97 start := time.Now()100 start := time.Now()
101+
102+ // Create color scheme resolver with initial detectors.
103+ // AdwaitaDetector is created but NOT marked available yet (requires adw.Init()).
104+ configAdapter := colorscheme.NewConfigAdapter(input.Config)
105+ resolver := colorscheme.NewResolver(configAdapter)
106+
107+ // Register detectors that are available before GTK init
108+ resolver.RegisterDetector(colorscheme.NewEnvDetector())
109+ resolver.RegisterDetector(colorscheme.NewGsettingsDetector())
110+
111+ // Create adwaita detector (will be marked available after adw.Init())
112+ adwaitaDetector := colorscheme.NewAdwaitaDetector()
113+ resolver.RegisterDetector(adwaitaDetector)
114+
98 wg.Add(2)115 wg.Add(2)
99 116
100 // Resolve directories117 // Resolve directories
@@ -104,9 +121,10 @@ func RunParallelInit(input ParallelInitInput) (*ParallelInitResult, error) {
104 }()121 }()
105 122
106 // Theme manager (CPU-bound, no I/O)123 // Theme manager (CPU-bound, no I/O)
124+ var themeManager *theme.Manager
107 go func() {125 go func() {
108 defer wg.Done()126 defer wg.Done()
109- themeManager = theme.NewManager(input.Ctx, input.Config)127+ themeManager = theme.NewManager(input.Ctx, input.Config, resolver)
110 }()128 }()
111 129
112 wg.Wait()130 wg.Wait()
@@ -118,10 +136,12 @@ func RunParallelInit(input ParallelInitInput) (*ParallelInitResult, error) {
118 }136 }
119 137
120 return &ParallelInitResult{138 return &ParallelInitResult{
121- DataDir: dataDir,139+ DataDir: dataDir,
122- CacheDir: cacheDir,140+ CacheDir: cacheDir,
123- ThemeManager: themeManager,141+ ThemeManager: themeManager,
124- Duration: duration,142+ ColorResolver: resolver,
143+ AdwaitaDetector: adwaitaDetector,
144+ Duration: duration,
125 }, nil145 }, nil
126 }146 }
127 147
@@ -253,11 +273,12 @@ type ParallelDBWebKitResult struct {
253 273
254 // ParallelDBWebKitInput holds inputs for parallel DB and WebKit initialization.274 // ParallelDBWebKitInput holds inputs for parallel DB and WebKit initialization.
255 type ParallelDBWebKitInput struct {275 type ParallelDBWebKitInput struct {
256- Ctx context.Context276+ Ctx context.Context
257- Config *config.Config277+ Config *config.Config
258- DataDir string // For WebKit context278+ DataDir string // For WebKit context
259- CacheDir string // For WebKit cache279+ CacheDir string // For WebKit cache
260- ThemeManager *theme.Manager280+ ThemeManager *theme.Manager
281+ ColorResolver port.ColorSchemeResolver
261 }282 }
262 283
263 // Note: Database path is resolved via config.GetDatabaseFile() internally.284 // Note: Database path is resolved via config.GetDatabaseFile() internally.
@@ -291,7 +312,15 @@ func RunParallelDBWebKit(input ParallelDBWebKitInput) (*ParallelDBWebKitResult,
291 }()312 }()
292 313
293 // WebKit stack on main thread (GTK requirement)314 // WebKit stack on main thread (GTK requirement)
294- stack := BuildWebKitStack(input.Ctx, input.Config, input.DataDir, input.CacheDir, input.ThemeManager, *log)315+ stack := BuildWebKitStack(WebKitStackInput{
316+ Ctx: input.Ctx,
317+ Config: input.Config,
318+ DataDir: input.DataDir,
319+ CacheDir: input.CacheDir,
320+ ThemeManager: input.ThemeManager,
321+ ColorResolver: input.ColorResolver,
322+ Logger: *log,
323+ })
295 324
296 // Wait for database325 // Wait for database
297 dbRes := <-dbCh326 dbRes := <-dbCh
M internal/bootstrap/webkit_stack.go +69 -48
@@ -15,6 +15,17 @@ import (
1515 "github.com/rs/zerolog"
1616 )
1717
18+// WebKitStackInput holds the input for BuildWebKitStack.
19+type WebKitStackInput struct {
20+ Ctx context.Context
21+ Config *config.Config
22+ DataDir string
23+ CacheDir string
24+ ThemeManager *theme.Manager
25+ ColorResolver port.ColorSchemeResolver
26+ Logger zerolog.Logger
27+}
28+
1829 type WebKitStack struct {
1930 Context *webkit.WebKitContext
2031 Settings *webkit.SettingsManager
@@ -24,14 +35,14 @@ type WebKitStack struct {
2435 FilterManager *filtering.Manager
2536 }
2637
27-func BuildWebKitStack(
28- ctx context.Context,
29- cfg *config.Config,
30- dataDir string,
31- cacheDir string,
32- themeManager *theme.Manager,
33- logger zerolog.Logger,
34-) WebKitStack {
38+func BuildWebKitStack(input WebKitStackInput) WebKitStack {
39+ ctx := input.Ctx
40+ cfg := input.Config
41+ dataDir := input.DataDir
42+ cacheDir := input.CacheDir
43+ themeManager := input.ThemeManager
44+ colorResolver := input.ColorResolver
45+ logger := input.Logger
3546 // Detect hardware for performance profile scaling
3647 hwSurveyor := env.NewHardwareSurveyor()
3748 hwInfo := hwSurveyor.Survey(ctx)
@@ -52,45 +63,8 @@ func BuildWebKitStack(
5263 Int("webview_pool_prewarm", perfSettings.WebViewPoolPrewarmCount).
5364 Msg("resolved performance profile")
5465
55- // CRITICAL: Configure rendering environment BEFORE WebKit/GTK initialization.
56- // Environment variables must be set before GTK/WebKit/GStreamer initializes.
57- renderEnv := env.NewManager()
58- gpuVendor := renderEnv.DetectGPUVendor(ctx)
59- renderSettings := port.RenderingEnvSettings{
60- // GStreamer settings
61- ForceVSync: cfg.Media.ForceVSync,
62- GLRenderingMode: string(cfg.Media.GLRenderingMode),
63- GStreamerDebugLevel: cfg.Media.GStreamerDebugLevel,
64- VideoBufferSizeMB: cfg.Media.VideoBufferSizeMB,
65- QueueBufferTimeSec: cfg.Media.QueueBufferTimeSec,
66-
67- // WebKit compositor settings
68- DisableDMABufRenderer: cfg.Rendering.DisableDMABufRenderer,
69- ForceCompositingMode: cfg.Rendering.ForceCompositingMode,
70- DisableCompositingMode: cfg.Rendering.DisableCompositingMode,
71-
72- // GTK/GSK settings
73- GSKRenderer: string(cfg.Rendering.GSKRenderer),
74- DisableMipmaps: cfg.Rendering.DisableMipmaps,
75- PreferGL: cfg.Rendering.PreferGL,
76-
77- // Debug settings
78- ShowFPS: cfg.Rendering.ShowFPS,
79- SampleMemory: cfg.Rendering.SampleMemory,
80- DebugFrames: cfg.Rendering.DebugFrames,
81-
82- // Skia rendering thread settings (from resolved profile)
83- SkiaCPUPaintingThreads: perfSettings.SkiaCPUPaintingThreads,
84- SkiaGPUPaintingThreads: perfSettings.SkiaGPUPaintingThreads,
85- SkiaEnableCPURendering: perfSettings.SkiaEnableCPURendering,
86- }
87- if err := renderEnv.ApplyEnvironment(ctx, renderSettings); err != nil {
88- logger.Warn().Err(err).Msg("failed to apply rendering environment")
89- }
90- logger.Info().
91- Str("gpu", string(gpuVendor)).
92- Interface("vars", renderEnv.GetAppliedVars()).
93- Msg("rendering environment configured")
66+ // Configure rendering environment (must happen before WebKit/GTK init)
67+ configureRenderingEnvironment(ctx, cfg, &perfSettings, logger)
9468
9569 // Build WebKitContext options with memory pressure settings
9670 wkOpts := port.WebKitContextOptions{
@@ -146,7 +120,7 @@ func BuildWebKitStack(
146120 schemeHandler.RegisterWithContext(wkCtx)
147121
148122 settings := webkit.NewSettingsManager(ctx, cfg)
149- injector := webkit.NewContentInjector(themeManager.PrefersDark())
123+ injector := webkit.NewContentInjector(colorResolver)
150124
151125 prepareThemeUC := usecase.NewPrepareWebUIThemeUseCase(injector)
152126 themeCSSText := themeManager.GetWebUIThemeCSS()
@@ -203,3 +177,50 @@ func hasNetworkProcessMemoryConfig(p *config.ResolvedPerformanceSettings) bool {
203177 p.NetworkProcessMemoryStrictThreshold > 0 ||
204178 p.NetworkProcessMemoryKillThreshold >= 0
205179 }
180+
181+// configureRenderingEnvironment sets up the rendering environment before WebKit/GTK initialization.
182+// Environment variables must be set before GTK/WebKit/GStreamer initializes.
183+func configureRenderingEnvironment(
184+ ctx context.Context,
185+ cfg *config.Config,
186+ perfSettings *config.ResolvedPerformanceSettings,
187+ logger zerolog.Logger,
188+) {
189+ renderEnv := env.NewManager()
190+ gpuVendor := renderEnv.DetectGPUVendor(ctx)
191+ renderSettings := port.RenderingEnvSettings{
192+ // GStreamer settings
193+ ForceVSync: cfg.Media.ForceVSync,
194+ GLRenderingMode: string(cfg.Media.GLRenderingMode),
195+ GStreamerDebugLevel: cfg.Media.GStreamerDebugLevel,
196+ VideoBufferSizeMB: cfg.Media.VideoBufferSizeMB,
197+ QueueBufferTimeSec: cfg.Media.QueueBufferTimeSec,
198+
199+ // WebKit compositor settings
200+ DisableDMABufRenderer: cfg.Rendering.DisableDMABufRenderer,
201+ ForceCompositingMode: cfg.Rendering.ForceCompositingMode,
202+ DisableCompositingMode: cfg.Rendering.DisableCompositingMode,
203+
204+ // GTK/GSK settings
205+ GSKRenderer: string(cfg.Rendering.GSKRenderer),
206+ DisableMipmaps: cfg.Rendering.DisableMipmaps,
207+ PreferGL: cfg.Rendering.PreferGL,
208+
209+ // Debug settings
210+ ShowFPS: cfg.Rendering.ShowFPS,
211+ SampleMemory: cfg.Rendering.SampleMemory,
212+ DebugFrames: cfg.Rendering.DebugFrames,
213+
214+ // Skia rendering thread settings (from resolved profile)
215+ SkiaCPUPaintingThreads: perfSettings.SkiaCPUPaintingThreads,
216+ SkiaGPUPaintingThreads: perfSettings.SkiaGPUPaintingThreads,
217+ SkiaEnableCPURendering: perfSettings.SkiaEnableCPURendering,
218+ }
219+ if err := renderEnv.ApplyEnvironment(ctx, renderSettings); err != nil {
220+ logger.Warn().Err(err).Msg("failed to apply rendering environment")
221+ }
222+ logger.Info().
223+ Str("gpu", string(gpuVendor)).
224+ Interface("vars", renderEnv.GetAppliedVars()).
225+ Msg("rendering environment configured")
226+}
@@ -15,6 +15,17 @@ import (
15 "github.com/rs/zerolog"15 "github.com/rs/zerolog"
16 )16 )
17 17
18+// WebKitStackInput holds the input for BuildWebKitStack.
19+type WebKitStackInput struct {
20+ Ctx context.Context
21+ Config *config.Config
22+ DataDir string
23+ CacheDir string
24+ ThemeManager *theme.Manager
25+ ColorResolver port.ColorSchemeResolver
26+ Logger zerolog.Logger
27+}
28+
18 type WebKitStack struct {29 type WebKitStack struct {
19 Context *webkit.WebKitContext30 Context *webkit.WebKitContext
20 Settings *webkit.SettingsManager31 Settings *webkit.SettingsManager
@@ -24,14 +35,14 @@ type WebKitStack struct {
24 FilterManager *filtering.Manager35 FilterManager *filtering.Manager
25 }36 }
26 37
27-func BuildWebKitStack(38+func BuildWebKitStack(input WebKitStackInput) WebKitStack {
28- ctx context.Context,39+ ctx := input.Ctx
29- cfg *config.Config,40+ cfg := input.Config
30- dataDir string,41+ dataDir := input.DataDir
31- cacheDir string,42+ cacheDir := input.CacheDir
32- themeManager *theme.Manager,43+ themeManager := input.ThemeManager
33- logger zerolog.Logger,44+ colorResolver := input.ColorResolver
34-) WebKitStack {45+ logger := input.Logger
35 // Detect hardware for performance profile scaling46 // Detect hardware for performance profile scaling
36 hwSurveyor := env.NewHardwareSurveyor()47 hwSurveyor := env.NewHardwareSurveyor()
37 hwInfo := hwSurveyor.Survey(ctx)48 hwInfo := hwSurveyor.Survey(ctx)
@@ -52,45 +63,8 @@ func BuildWebKitStack(
52 Int("webview_pool_prewarm", perfSettings.WebViewPoolPrewarmCount).63 Int("webview_pool_prewarm", perfSettings.WebViewPoolPrewarmCount).
53 Msg("resolved performance profile")64 Msg("resolved performance profile")
54 65
55- // CRITICAL: Configure rendering environment BEFORE WebKit/GTK initialization.66+ // Configure rendering environment (must happen before WebKit/GTK init)
56- // Environment variables must be set before GTK/WebKit/GStreamer initializes.67+ configureRenderingEnvironment(ctx, cfg, &perfSettings, logger)
57- renderEnv := env.NewManager()
58- gpuVendor := renderEnv.DetectGPUVendor(ctx)
59- renderSettings := port.RenderingEnvSettings{
60- // GStreamer settings
61- ForceVSync: cfg.Media.ForceVSync,
62- GLRenderingMode: string(cfg.Media.GLRenderingMode),
63- GStreamerDebugLevel: cfg.Media.GStreamerDebugLevel,
64- VideoBufferSizeMB: cfg.Media.VideoBufferSizeMB,
65- QueueBufferTimeSec: cfg.Media.QueueBufferTimeSec,
66-
67- // WebKit compositor settings
68- DisableDMABufRenderer: cfg.Rendering.DisableDMABufRenderer,
69- ForceCompositingMode: cfg.Rendering.ForceCompositingMode,
70- DisableCompositingMode: cfg.Rendering.DisableCompositingMode,
71-
72- // GTK/GSK settings
73- GSKRenderer: string(cfg.Rendering.GSKRenderer),
74- DisableMipmaps: cfg.Rendering.DisableMipmaps,
75- PreferGL: cfg.Rendering.PreferGL,
76-
77- // Debug settings
78- ShowFPS: cfg.Rendering.ShowFPS,
79- SampleMemory: cfg.Rendering.SampleMemory,
80- DebugFrames: cfg.Rendering.DebugFrames,
81-
82- // Skia rendering thread settings (from resolved profile)
83- SkiaCPUPaintingThreads: perfSettings.SkiaCPUPaintingThreads,
84- SkiaGPUPaintingThreads: perfSettings.SkiaGPUPaintingThreads,
85- SkiaEnableCPURendering: perfSettings.SkiaEnableCPURendering,
86- }
87- if err := renderEnv.ApplyEnvironment(ctx, renderSettings); err != nil {
88- logger.Warn().Err(err).Msg("failed to apply rendering environment")
89- }
90- logger.Info().
91- Str("gpu", string(gpuVendor)).
92- Interface("vars", renderEnv.GetAppliedVars()).
93- Msg("rendering environment configured")
94 68
95 // Build WebKitContext options with memory pressure settings69 // Build WebKitContext options with memory pressure settings
96 wkOpts := port.WebKitContextOptions{70 wkOpts := port.WebKitContextOptions{
@@ -146,7 +120,7 @@ func BuildWebKitStack(
146 schemeHandler.RegisterWithContext(wkCtx)120 schemeHandler.RegisterWithContext(wkCtx)
147 121
148 settings := webkit.NewSettingsManager(ctx, cfg)122 settings := webkit.NewSettingsManager(ctx, cfg)
149- injector := webkit.NewContentInjector(themeManager.PrefersDark())123+ injector := webkit.NewContentInjector(colorResolver)
150 124
151 prepareThemeUC := usecase.NewPrepareWebUIThemeUseCase(injector)125 prepareThemeUC := usecase.NewPrepareWebUIThemeUseCase(injector)
152 themeCSSText := themeManager.GetWebUIThemeCSS()126 themeCSSText := themeManager.GetWebUIThemeCSS()
@@ -203,3 +177,50 @@ func hasNetworkProcessMemoryConfig(p *config.ResolvedPerformanceSettings) bool {
203 p.NetworkProcessMemoryStrictThreshold > 0 ||177 p.NetworkProcessMemoryStrictThreshold > 0 ||
204 p.NetworkProcessMemoryKillThreshold >= 0178 p.NetworkProcessMemoryKillThreshold >= 0
205 }179 }
180+
181+// configureRenderingEnvironment sets up the rendering environment before WebKit/GTK initialization.
182+// Environment variables must be set before GTK/WebKit/GStreamer initializes.
183+func configureRenderingEnvironment(
184+ ctx context.Context,
185+ cfg *config.Config,
186+ perfSettings *config.ResolvedPerformanceSettings,
187+ logger zerolog.Logger,
188+) {
189+ renderEnv := env.NewManager()
190+ gpuVendor := renderEnv.DetectGPUVendor(ctx)
191+ renderSettings := port.RenderingEnvSettings{
192+ // GStreamer settings
193+ ForceVSync: cfg.Media.ForceVSync,
194+ GLRenderingMode: string(cfg.Media.GLRenderingMode),
195+ GStreamerDebugLevel: cfg.Media.GStreamerDebugLevel,
196+ VideoBufferSizeMB: cfg.Media.VideoBufferSizeMB,
197+ QueueBufferTimeSec: cfg.Media.QueueBufferTimeSec,
198+
199+ // WebKit compositor settings
200+ DisableDMABufRenderer: cfg.Rendering.DisableDMABufRenderer,
201+ ForceCompositingMode: cfg.Rendering.ForceCompositingMode,
202+ DisableCompositingMode: cfg.Rendering.DisableCompositingMode,
203+
204+ // GTK/GSK settings
205+ GSKRenderer: string(cfg.Rendering.GSKRenderer),
206+ DisableMipmaps: cfg.Rendering.DisableMipmaps,
207+ PreferGL: cfg.Rendering.PreferGL,
208+
209+ // Debug settings
210+ ShowFPS: cfg.Rendering.ShowFPS,
211+ SampleMemory: cfg.Rendering.SampleMemory,
212+ DebugFrames: cfg.Rendering.DebugFrames,
213+
214+ // Skia rendering thread settings (from resolved profile)
215+ SkiaCPUPaintingThreads: perfSettings.SkiaCPUPaintingThreads,
216+ SkiaGPUPaintingThreads: perfSettings.SkiaGPUPaintingThreads,
217+ SkiaEnableCPURendering: perfSettings.SkiaEnableCPURendering,
218+ }
219+ if err := renderEnv.ApplyEnvironment(ctx, renderSettings); err != nil {
220+ logger.Warn().Err(err).Msg("failed to apply rendering environment")
221+ }
222+ logger.Info().
223+ Str("gpu", string(gpuVendor)).
224+ Interface("vars", renderEnv.GetAppliedVars()).
225+ Msg("rendering environment configured")
226+}
M internal/ui/app.go +22 -3
@@ -160,6 +160,13 @@ func (a *App) Run(ctx context.Context, args []string) int {
160160 // This also initializes GTK implicitly.
161161 adw.Init()
162162
163+ // Mark adwaita detector as available now that adw.Init() is complete.
164+ // This enables the highest-priority color scheme detector.
165+ if a.deps != nil && a.deps.AdwaitaDetector != nil {
166+ a.deps.AdwaitaDetector.MarkAvailable()
167+ log.Debug().Msg("adwaita detector marked available")
168+ }
169+
163170 // TODO: Use AppID once puregotk GC bug is fixed (nullable-string-gc-memory-corruption)
164171 a.gtkApp = gtk.NewApplication(nil, gio.GApplicationFlagsNoneValue)
165172 if a.gtkApp == nil {
@@ -249,11 +256,24 @@ func (a *App) applyGTKColorSchemePreference(ctx context.Context) {
249256 }
250257
251258 styleMgr.SetColorScheme(adwScheme)
252- prefersDark := styleMgr.GetDark()
259+
260+ // Refresh the color resolver now that adwaita detector is available.
261+ // This ensures all components get the correct preference.
262+ var pref string
263+ var prefersDark bool
264+ if a.deps != nil && a.deps.ColorResolver != nil {
265+ result := a.deps.ColorResolver.Refresh()
266+ prefersDark = result.PrefersDark
267+ pref = result.Source
268+ } else {
269+ prefersDark = styleMgr.GetDark()
270+ pref = "adw.StyleManager"
271+ }
253272
254273 log.Debug().
255274 Str("scheme", scheme).
256275 Bool("prefers_dark", prefersDark).
276+ Str("source", pref).
257277 Msg("applied color scheme via adw.StyleManager")
258278 }
259279
@@ -1732,9 +1752,8 @@ func (a *App) applyAppearanceConfig(ctx context.Context, cfg *config.Config) {
17321752 }
17331753 a.deps.Theme.UpdateFromConfig(ctx, cfg, display)
17341754
1735- // Keep injector's dark-mode flag in sync for future navigations
1755+ // Update find highlight CSS for future navigations
17361756 if a.injector != nil {
1737- a.injector.SetPrefersDark(a.deps.Theme.PrefersDark())
17381757 findCSS := theme.GenerateFindHighlightCSS(a.deps.Theme.GetCurrentPalette())
17391758 if err := a.injector.InjectFindHighlightCSS(ctx, findCSS); err != nil {
17401759 log.Warn().Err(err).Msg("failed to update find highlight CSS")
@@ -160,6 +160,13 @@ func (a *App) Run(ctx context.Context, args []string) int {
160 // This also initializes GTK implicitly.160 // This also initializes GTK implicitly.
161 adw.Init()161 adw.Init()
162 162
163+ // Mark adwaita detector as available now that adw.Init() is complete.
164+ // This enables the highest-priority color scheme detector.
165+ if a.deps != nil && a.deps.AdwaitaDetector != nil {
166+ a.deps.AdwaitaDetector.MarkAvailable()
167+ log.Debug().Msg("adwaita detector marked available")
168+ }
169+
163 // TODO: Use AppID once puregotk GC bug is fixed (nullable-string-gc-memory-corruption)170 // TODO: Use AppID once puregotk GC bug is fixed (nullable-string-gc-memory-corruption)
164 a.gtkApp = gtk.NewApplication(nil, gio.GApplicationFlagsNoneValue)171 a.gtkApp = gtk.NewApplication(nil, gio.GApplicationFlagsNoneValue)
165 if a.gtkApp == nil {172 if a.gtkApp == nil {
@@ -249,11 +256,24 @@ func (a *App) applyGTKColorSchemePreference(ctx context.Context) {
249 }256 }
250 257
251 styleMgr.SetColorScheme(adwScheme)258 styleMgr.SetColorScheme(adwScheme)
252- prefersDark := styleMgr.GetDark()259+
260+ // Refresh the color resolver now that adwaita detector is available.
261+ // This ensures all components get the correct preference.
262+ var pref string
263+ var prefersDark bool
264+ if a.deps != nil && a.deps.ColorResolver != nil {
265+ result := a.deps.ColorResolver.Refresh()
266+ prefersDark = result.PrefersDark
267+ pref = result.Source
268+ } else {
269+ prefersDark = styleMgr.GetDark()
270+ pref = "adw.StyleManager"
271+ }
253 272
254 log.Debug().273 log.Debug().
255 Str("scheme", scheme).274 Str("scheme", scheme).
256 Bool("prefers_dark", prefersDark).275 Bool("prefers_dark", prefersDark).
276+ Str("source", pref).
257 Msg("applied color scheme via adw.StyleManager")277 Msg("applied color scheme via adw.StyleManager")
258 }278 }
259 279
@@ -1732,9 +1752,8 @@ func (a *App) applyAppearanceConfig(ctx context.Context, cfg *config.Config) {
1732 }1752 }
1733 a.deps.Theme.UpdateFromConfig(ctx, cfg, display)1753 a.deps.Theme.UpdateFromConfig(ctx, cfg, display)
1734 1754
1735- // Keep injector's dark-mode flag in sync for future navigations1755+ // Update find highlight CSS for future navigations
1736 if a.injector != nil {1756 if a.injector != nil {
1737- a.injector.SetPrefersDark(a.deps.Theme.PrefersDark())
1738 findCSS := theme.GenerateFindHighlightCSS(a.deps.Theme.GetCurrentPalette())1757 findCSS := theme.GenerateFindHighlightCSS(a.deps.Theme.GetCurrentPalette())
1739 if err := a.injector.InjectFindHighlightCSS(ctx, findCSS); err != nil {1758 if err := a.injector.InjectFindHighlightCSS(ctx, findCSS); err != nil {
1740 log.Warn().Err(err).Msg("failed to update find highlight CSS")1759 log.Warn().Err(err).Msg("failed to update find highlight CSS")
M internal/ui/deps.go +5 -2
@@ -8,6 +8,7 @@ import (
88 "github.com/bnema/dumber/internal/application/usecase"
99 "github.com/bnema/dumber/internal/domain/entity"
1010 "github.com/bnema/dumber/internal/domain/repository"
11+ "github.com/bnema/dumber/internal/infrastructure/colorscheme"
1112 "github.com/bnema/dumber/internal/infrastructure/config"
1213 "github.com/bnema/dumber/internal/infrastructure/favicon"
1314 "github.com/bnema/dumber/internal/infrastructure/filtering"
@@ -25,8 +26,10 @@ type Dependencies struct {
2526 RestoreSessionID string // Session ID to restore on startup (optional)
2627 OnFirstWebViewShown func(context.Context)
2728
28- // Theme management
29- Theme *theme.Manager
29+ // Theme and color scheme management
30+ Theme *theme.Manager
31+ ColorResolver port.ColorSchemeResolver
32+ AdwaitaDetector *colorscheme.AdwaitaDetector
3033
3134 // WebKit infrastructure
3235 WebContext *webkit.WebKitContext
@@ -8,6 +8,7 @@ import (
8 "github.com/bnema/dumber/internal/application/usecase"8 "github.com/bnema/dumber/internal/application/usecase"
9 "github.com/bnema/dumber/internal/domain/entity"9 "github.com/bnema/dumber/internal/domain/entity"
10 "github.com/bnema/dumber/internal/domain/repository"10 "github.com/bnema/dumber/internal/domain/repository"
11+ "github.com/bnema/dumber/internal/infrastructure/colorscheme"
11 "github.com/bnema/dumber/internal/infrastructure/config"12 "github.com/bnema/dumber/internal/infrastructure/config"
12 "github.com/bnema/dumber/internal/infrastructure/favicon"13 "github.com/bnema/dumber/internal/infrastructure/favicon"
13 "github.com/bnema/dumber/internal/infrastructure/filtering"14 "github.com/bnema/dumber/internal/infrastructure/filtering"
@@ -25,8 +26,10 @@ type Dependencies struct {
25 RestoreSessionID string // Session ID to restore on startup (optional)26 RestoreSessionID string // Session ID to restore on startup (optional)
26 OnFirstWebViewShown func(context.Context)27 OnFirstWebViewShown func(context.Context)
27 28
28- // Theme management29+ // Theme and color scheme management
29- Theme *theme.Manager30+ Theme *theme.Manager
31+ ColorResolver port.ColorSchemeResolver
32+ AdwaitaDetector *colorscheme.AdwaitaDetector
30 33
31 // WebKit infrastructure34 // WebKit infrastructure
32 WebContext *webkit.WebKitContext35 WebContext *webkit.WebKitContext

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