v0.26.2 3cd9d2e

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

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