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