v0.27.2 5960dd8

webkit_stack.go RawHistoryFolder

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

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