1package bootstrap
2
3import (
4 "context"
5 "path/filepath"
6
7 "github.com/bnema/dumber/assets"
8 "github.com/bnema/dumber/internal/application/port"
9 "github.com/bnema/dumber/internal/application/usecase"
10 "github.com/bnema/dumber/internal/infrastructure/config"
11 "github.com/bnema/dumber/internal/infrastructure/env"
12 "github.com/bnema/dumber/internal/infrastructure/filtering"
13 "github.com/bnema/dumber/internal/infrastructure/webkit"
14 "github.com/bnema/dumber/internal/logging"
15 "github.com/bnema/dumber/internal/ui/theme"
16 "github.com/jwijenbergh/puregotk/v4/gdk"
17 "github.com/rs/zerolog"
18)
19
20// WebKitStackInput holds the input for BuildWebKitStack.
21type WebKitStackInput struct {
22 Ctx context.Context
23 Config *config.Config
24 DataDir string
25 CacheDir string
26 ThemeManager *theme.Manager
27 ColorResolver port.ColorSchemeResolver
28 Logger zerolog.Logger
29}
30
31type WebKitStack struct {
32 Context *webkit.WebKitContext
33 Settings *webkit.SettingsManager
34 Injector *webkit.ContentInjector
35 MessageRouter *webkit.MessageRouter
36 Pool *webkit.WebViewPool
37 FilterManager *filtering.Manager
38}
39
40func BuildWebKitStack(input WebKitStackInput) WebKitStack {
41 ctx := input.Ctx
42 cfg := input.Config
43 logger := input.Logger
44
45 perfSettings := surveyHardwareAndResolveProfile(ctx, cfg, logger)
46
47 configureRenderingEnvironment(ctx, cfg, &perfSettings, logger)
48 logging.Trace().Mark("render_env")
49
50 wkOpts := buildWebKitContextOptions(cfg, input.DataDir, input.CacheDir, &perfSettings)
51 logger.Info().
52 Str("cookie_policy", string(wkOpts.CookiePolicy)).
53 Bool("itp_enabled", wkOpts.ITPEnabled).
54 Msg("webkit privacy configuration")
55 wkCtx, err := webkit.NewWebKitContextWithOptions(ctx, wkOpts)
56 if err != nil {
57 logger.Fatal().Err(err).Msg("failed to initialize WebKit context")
58 }
59 logging.Trace().Mark("webkit_context")
60
61 filterManager := initFilterManager(ctx, cfg, input.DataDir, logger)
62
63 schemeHandler := webkit.NewDumbSchemeHandler(ctx)
64 schemeHandler.SetAssets(assets.WebUIAssets)
65 schemeHandler.RegisterWithContext(wkCtx)
66
67 settings, injector, messageRouter := initWebKitComponents(ctx, cfg, input.ThemeManager, input.ColorResolver, logger)
68
69 pool := initWebViewPool(ctx, wkCtx, settings, injector, messageRouter, input.ThemeManager, filterManager, &perfSettings, logger)
70
71 return WebKitStack{
72 Context: wkCtx,
73 Settings: settings,
74 Injector: injector,
75 MessageRouter: messageRouter,
76 Pool: pool,
77 FilterManager: filterManager,
78 }
79}
80
81func surveyHardwareAndResolveProfile(ctx context.Context, cfg *config.Config, logger zerolog.Logger) config.ResolvedPerformanceSettings {
82 hwSurveyor := env.NewHardwareSurveyor()
83 hwInfo := hwSurveyor.Survey(ctx)
84 logging.Trace().Mark("hardware_survey")
85 logger.Info().
86 Int("cpu_cores", hwInfo.CPUCores).
87 Int("cpu_threads", hwInfo.CPUThreads).
88 Uint64("ram_mb", hwInfo.TotalRAM/(1024*1024)).
89 Str("gpu_vendor", string(hwInfo.GPUVendor)).
90 Uint64("vram_mb", hwInfo.VRAM/(1024*1024)).
91 Msg("hardware survey completed")
92
93 perfSettings := config.ResolvePerformanceProfile(&cfg.Performance, &hwInfo)
94 logging.Trace().Mark("performance_profile")
95 logger.Info().
96 Str("profile", string(cfg.Performance.Profile)).
97 Int("skia_cpu_threads", perfSettings.SkiaCPUPaintingThreads).
98 Int("skia_gpu_threads", perfSettings.SkiaGPUPaintingThreads).
99 Int("webview_pool_prewarm", perfSettings.WebViewPoolPrewarmCount).
100 Msg("resolved performance profile")
101
102 return perfSettings
103}
104
105func buildWebKitContextOptions(
106 cfg *config.Config,
107 dataDir, cacheDir string,
108 perfSettings *config.ResolvedPerformanceSettings,
109) port.WebKitContextOptions {
110 cookiePolicy := port.WebKitCookiePolicyNoThirdParty
111 itpEnabled := true
112 if cfg != nil {
113 cookiePolicy = port.WebKitCookiePolicy(cfg.Privacy.CookiePolicy)
114 itpEnabled = cfg.Privacy.ITPEnabled
115 }
116
117 wkOpts := port.WebKitContextOptions{
118 DataDir: dataDir,
119 CacheDir: cacheDir,
120 CookiePolicy: cookiePolicy,
121 ITPEnabled: itpEnabled,
122 }
123
124 if hasWebProcessMemoryConfig(perfSettings) {
125 wkOpts.WebProcessMemory = &port.MemoryPressureConfig{
126 MemoryLimitMB: perfSettings.WebProcessMemoryLimitMB,
127 PollIntervalSec: perfSettings.WebProcessMemoryPollIntervalSec,
128 ConservativeThreshold: perfSettings.WebProcessMemoryConservativeThreshold,
129 StrictThreshold: perfSettings.WebProcessMemoryStrictThreshold,
130 }
131 }
132
133 if hasNetworkProcessMemoryConfig(perfSettings) {
134 wkOpts.NetworkProcessMemory = &port.MemoryPressureConfig{
135 MemoryLimitMB: perfSettings.NetworkProcessMemoryLimitMB,
136 PollIntervalSec: perfSettings.NetworkProcessMemoryPollIntervalSec,
137 ConservativeThreshold: perfSettings.NetworkProcessMemoryConservativeThreshold,
138 StrictThreshold: perfSettings.NetworkProcessMemoryStrictThreshold,
139 }
140 }
141
142 return wkOpts
143}
144
145func initFilterManager(ctx context.Context, cfg *config.Config, dataDir string, logger zerolog.Logger) *filtering.Manager {
146 filterStoreDir := filepath.Join(dataDir, "filters", "store")
147 filterJSONDir := filepath.Join(dataDir, "filters", "json")
148 filterManager, err := filtering.NewManager(filtering.ManagerConfig{
149 StoreDir: filterStoreDir,
150 JSONDir: filterJSONDir,
151 Enabled: cfg.ContentFiltering.Enabled,
152 AutoUpdate: cfg.ContentFiltering.AutoUpdate,
153 })
154 if err != nil {
155 logger.Warn().Err(err).Msg("failed to create filter manager, continuing without content filtering")
156 return nil
157 }
158 if err := filterManager.Initialize(ctx); err != nil {
159 logger.Warn().Err(err).Msg("failed to initialize filters, will load async")
160 }
161 logging.Trace().Mark("filter_manager")
162 return filterManager
163}
164
165func initWebKitComponents(
166 ctx context.Context,
167 cfg *config.Config,
168 themeManager *theme.Manager,
169 colorResolver port.ColorSchemeResolver,
170 logger zerolog.Logger,
171) (*webkit.SettingsManager, *webkit.ContentInjector, *webkit.MessageRouter) {
172 settings := webkit.NewSettingsManager(ctx, cfg)
173 injector := webkit.NewContentInjector(colorResolver)
174
175 injector.SetAutoCopyConfigGetter(func() bool {
176 return config.Get().Clipboard.AutoCopyOnSelection
177 })
178
179 prepareThemeUC := usecase.NewPrepareWebUIThemeUseCase(injector)
180 themeCSSText := themeManager.GetWebUIThemeCSS()
181 if err := prepareThemeUC.Execute(ctx, usecase.PrepareWebUIThemeInput{CSSVars: themeCSSText}); err != nil {
182 logger.Warn().Err(err).Msg("failed to prepare WebUI theme CSS")
183 }
184
185 messageRouter := webkit.NewMessageRouter(ctx)
186 logging.Trace().Mark("settings_manager")
187
188 return settings, injector, messageRouter
189}
190
191func initWebViewPool(
192 ctx context.Context,
193 wkCtx *webkit.WebKitContext,
194 settings *webkit.SettingsManager,
195 injector *webkit.ContentInjector,
196 messageRouter *webkit.MessageRouter,
197 themeManager *theme.Manager,
198 filterManager *filtering.Manager,
199 perfSettings *config.ResolvedPerformanceSettings,
200 logger zerolog.Logger,
201) *webkit.WebViewPool {
202 poolCfg := webkit.DefaultPoolConfig()
203 if perfSettings.WebViewPoolPrewarmCount > 0 {
204 poolCfg.PrewarmCount = perfSettings.WebViewPoolPrewarmCount
205 }
206 pool := webkit.NewWebViewPool(ctx, wkCtx, settings, poolCfg, injector, messageRouter)
207
208 bgR, bgG, bgB, bgA := themeManager.GetBackgroundRGBA()
209 pool.SetBackgroundColor(bgR, bgG, bgB, bgA)
210
211 if filterManager != nil {
212 pool.SetFilterApplier(filterManager)
213 }
214
215 if shouldPrewarmFirstWebView(gdk.DisplayGetDefault()) {
216 if err := pool.PrewarmFirst(ctx); err != nil {
217 logger.Warn().Err(err).Msg("failed to prewarm first webview, first tab may be slower")
218 }
219 } else {
220 logger.Warn().Msg("skipping first webview prewarm: no GDK display available yet")
221 }
222 logging.Trace().Mark("pool_prewarm_first")
223
224 return pool
225}
226
227func shouldPrewarmFirstWebView(display *gdk.Display) bool {
228 return display != nil
229}
230
231// hasWebProcessMemoryConfig returns true if any web process memory setting is configured.
232func hasWebProcessMemoryConfig(p *config.ResolvedPerformanceSettings) bool {
233 return p.WebProcessMemoryLimitMB > 0 ||
234 p.WebProcessMemoryPollIntervalSec > 0 ||
235 p.WebProcessMemoryConservativeThreshold > 0 ||
236 p.WebProcessMemoryStrictThreshold > 0
237}
238
239// hasNetworkProcessMemoryConfig returns true if any network process memory setting is configured.
240func hasNetworkProcessMemoryConfig(p *config.ResolvedPerformanceSettings) bool {
241 return p.NetworkProcessMemoryLimitMB > 0 ||
242 p.NetworkProcessMemoryPollIntervalSec > 0 ||
243 p.NetworkProcessMemoryConservativeThreshold > 0 ||
244 p.NetworkProcessMemoryStrictThreshold > 0
245}
246
247// configureRenderingEnvironment sets up the rendering environment before WebKit/GTK initialization.
248// Environment variables must be set before GTK/WebKit/GStreamer initializes.
249func configureRenderingEnvironment(
250 ctx context.Context,
251 cfg *config.Config,
252 perfSettings *config.ResolvedPerformanceSettings,
253 logger zerolog.Logger,
254) {
255 // Install GLib log handler FIRST if configured (must be before any GTK/GLib calls)
256 if cfg.Logging.CaptureGTKLogs {
257 enableDebug := cfg.Logging.Level == "debug" || cfg.Logging.Level == "trace"
258 logging.InstallGLibLogHandler(ctx, logger, enableDebug)
259 }
260
261 renderEnv := env.NewManager()
262 gpuVendor := renderEnv.DetectGPUVendor(ctx)
263 renderSettings := port.RenderingEnvSettings{
264 // GStreamer settings
265 ForceVSync: cfg.Media.ForceVSync,
266 GLRenderingMode: string(cfg.Media.GLRenderingMode),
267 GStreamerDebugLevel: cfg.Media.GStreamerDebugLevel,
268
269 // WebKit compositor settings
270 DisableDMABufRenderer: cfg.Rendering.DisableDMABufRenderer,
271 ForceCompositingMode: cfg.Rendering.ForceCompositingMode,
272 DisableCompositingMode: cfg.Rendering.DisableCompositingMode,
273
274 // GTK/GSK settings
275 GSKRenderer: string(cfg.Rendering.GSKRenderer),
276 DisableMipmaps: cfg.Rendering.DisableMipmaps,
277 PreferGL: cfg.Rendering.PreferGL,
278
279 // Debug settings
280 ShowFPS: cfg.Rendering.ShowFPS,
281 SampleMemory: cfg.Rendering.SampleMemory,
282 DebugFrames: cfg.Rendering.DebugFrames,
283
284 // Skia rendering thread settings (from resolved profile)
285 SkiaCPUPaintingThreads: perfSettings.SkiaCPUPaintingThreads,
286 SkiaGPUPaintingThreads: perfSettings.SkiaGPUPaintingThreads,
287 SkiaEnableCPURendering: perfSettings.SkiaEnableCPURendering,
288 }
289 if err := renderEnv.ApplyEnvironment(ctx, renderSettings); err != nil {
290 logger.Warn().Err(err).Msg("failed to apply rendering environment")
291 }
292 logger.Info().
293 Str("gpu", string(gpuVendor)).
294 Interface("vars", renderEnv.GetAppliedVars()).
295 Msg("rendering environment configured")
296}