v0.28.0 3cb3770

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/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}

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