v0.33.1 9577fee

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

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