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