v0.30.0 eb3068f

engine_init.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close