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