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