app.go RawHistoryFolder

1package cef
2
3import (
4	"os"
5	"strings"
6
7	purecef "github.com/bnema/purego-cef/cef"
8	cef2gtk "github.com/bnema/purego-cef2gtk"
9
10	"github.com/bnema/dumber/internal/logging"
11)
12
13const (
14	dumbSchemeName                     = "dumb"
15	chromiumEnableFeaturesSwitch       = "enable-features"
16	chromiumDisableFeaturesSwitch      = "disable-features"
17	chromiumDisableBlinkFeaturesSwitch = "disable-blink-features"
18	chromiumRenderNodeOverrideSwitch   = "render-node-override"
19)
20
21// Chromium's core Blink runtime flag for the Web Authentication API is
22// "WebAuth" (PublicKeyCredential IDL uses RuntimeEnabled=WebAuth). The
23// longer "WebAuthentication*" names are separate subfeatures/metrics.
24var cefWebAuthnFeaturesDisabledByPolicy = []string{
25	"WebAuth",
26}
27
28func dumbSchemeOptions() int32 {
29	return purecef.SchemeOptionsSchemeOptionStandard |
30		purecef.SchemeOptionsSchemeOptionSecure |
31		purecef.SchemeOptionsSchemeOptionCorsEnabled |
32		purecef.SchemeOptionsSchemeOptionCspBypassing |
33		purecef.SchemeOptionsSchemeOptionFetchEnabled
34}
35
36func registerDumbScheme(registrar purecef.SchemeRegistrar) {
37	if registrar == nil {
38		return
39	}
40	registrar.AddCustomScheme(dumbSchemeName, dumbSchemeOptions())
41}
42
43func configureCommandLine(commandLine purecef.CommandLine) {
44	configureCommandLineWithRenderStack(commandLine, cef2gtk.RenderStackPlan{})
45}
46
47func configureCommandLineWithRenderStack(commandLine purecef.CommandLine, renderStackPlan cef2gtk.RenderStackPlan) {
48	if commandLine == nil {
49		return
50	}
51
52	// Delegate Wayland accelerated rendering setup to the GTK bridge.
53	// Preserves any pre-existing ozone-platform value (e.g. set via
54	// CEF command-line args) — the bridge is a no-op when already present.
55	cef2gtk.ConfigureCommandLine(commandLine, cef2gtk.CommandLineOptions{RenderStackPlan: renderStackPlan})
56
57	// Enable Chromium's built-in smooth scrolling animation — without this,
58	// mouse wheel scroll jumps in discrete steps with no momentum/easing.
59	commandLine.AppendSwitch("enable-smooth-scrolling")
60
61	// Allow video autoplay without requiring user gesture — sites like
62	// Reddit autoplay muted videos in the feed; without this policy
63	// Chromium blocks them, showing an infinite spinner.
64	commandLine.AppendSwitchWithValue("autoplay-policy", "no-user-gesture-required")
65
66	configureHardwareVideoDecode(commandLine)
67	configureEnvChromiumFlags(commandLine)
68	configureRenderNodeOverride(commandLine)
69	configureWebAuthnFeaturePolicy(commandLine)
70}
71
72func configureHardwareVideoDecode(commandLine purecef.CommandLine) {
73	if commandLine == nil || !envBoolEnabled(cefEnableVAAPIEnvVar) {
74		return
75	}
76
77	appendUniqueCommaSeparatedSwitchValues(commandLine, chromiumEnableFeaturesSwitch,
78		"AcceleratedVideoDecoder",
79		"AcceleratedVideoEncoder",
80		"AcceleratedVideoDecodeLinuxGL",
81		"AcceleratedVideoDecodeLinuxZeroCopyGL",
82		"VaapiIgnoreDriverChecks",
83	)
84	appendSwitchIfMissing(commandLine, "ignore-gpu-blocklist")
85	appendSwitchIfMissing(commandLine, "enable-zero-copy")
86	appendSwitchIfMissing(commandLine, "disable-gpu-driver-bug-workaround")
87}
88
89func configureEnvChromiumFlags(commandLine purecef.CommandLine) {
90	if commandLine == nil {
91		return
92	}
93
94	for _, token := range parseChromiumFlagsEnv(os.Getenv(cefChromiumFlagsEnvVar)) {
95		applyChromiumFlagToken(commandLine, token)
96	}
97}
98
99func configureRenderNodeOverride(commandLine purecef.CommandLine) {
100	if commandLine == nil {
101		return
102	}
103	value := strings.TrimSpace(os.Getenv(cefRenderNodeEnvVar))
104	if value == "" {
105		return
106	}
107	switch strings.ToLower(value) {
108	case "auto", "default", "none", "off", "disable", "disabled":
109		commandLine.RemoveSwitch(chromiumRenderNodeOverrideSwitch)
110		return
111	}
112	commandLine.RemoveSwitch(chromiumRenderNodeOverrideSwitch)
113	commandLine.AppendSwitchWithValue(chromiumRenderNodeOverrideSwitch, value)
114}
115
116func parseChromiumFlagsEnv(raw string) []string {
117	// Reuse Dumber's command-line tokenizer so developer env flags can contain
118	// quoted values without adding a second shell-like parser in the CEF adapter.
119	return parseRelaunchCommandLineArgs(raw)
120}
121
122func applyChromiumFlagToken(commandLine purecef.CommandLine, token string) {
123	if commandLine == nil {
124		return
125	}
126	token = strings.TrimSpace(token)
127	if token == "" || token == "--" {
128		return
129	}
130	name, value, ok := parseChromiumSwitchToken(token)
131	if !ok {
132		return
133	}
134
135	switch name {
136	case chromiumEnableFeaturesSwitch, chromiumDisableFeaturesSwitch, chromiumDisableBlinkFeaturesSwitch:
137		appendUniqueCommaSeparatedSwitchValues(commandLine, name, strings.Split(value, ",")...)
138	default:
139		if value == "" {
140			appendSwitchIfMissing(commandLine, name)
141			return
142		}
143		commandLine.AppendSwitchWithValue(name, value)
144	}
145}
146
147func parseChromiumSwitchToken(token string) (name, value string, ok bool) {
148	token = strings.TrimSpace(token)
149	if token == "" {
150		return "", "", false
151	}
152
153	switch {
154	case strings.HasPrefix(token, "--"):
155		token = strings.TrimPrefix(token, "--")
156	case strings.HasPrefix(token, "-"):
157		token = strings.TrimPrefix(token, "-")
158	default:
159		return "", "", false
160	}
161
162	if token == "" || strings.HasPrefix(token, "-") {
163		return "", "", false
164	}
165	name, value, found := strings.Cut(token, "=")
166	name = strings.TrimSpace(name)
167	if name == "" {
168		return "", "", false
169	}
170	if !found {
171		return name, "", true
172	}
173	return name, value, true
174}
175
176func configureWebAuthnFeaturePolicy(commandLine purecef.CommandLine) {
177	if commandLine == nil {
178		return
179	}
180	if cefWebAuthnUnsafeEnabled() {
181		removeCommaSeparatedSwitchValues(commandLine, chromiumDisableFeaturesSwitch, cefWebAuthnFeaturesDisabledByPolicy...)
182		removeCommaSeparatedSwitchValues(commandLine, chromiumDisableBlinkFeaturesSwitch, cefWebAuthnFeaturesDisabledByPolicy...)
183		return
184	}
185	appendUniqueCommaSeparatedSwitchValues(commandLine, chromiumDisableFeaturesSwitch, cefWebAuthnFeaturesDisabledByPolicy...)
186	appendUniqueCommaSeparatedSwitchValues(commandLine, chromiumDisableBlinkFeaturesSwitch, cefWebAuthnFeaturesDisabledByPolicy...)
187}
188
189func appendUniqueCommaSeparatedSwitchValues(commandLine purecef.CommandLine, name string, values ...string) {
190	if commandLine == nil || len(values) == 0 {
191		return
192	}
193
194	seen := make(map[string]struct{}, len(values))
195	combined := make([]string, 0, len(values))
196	for value := range strings.SplitSeq(commandLine.GetSwitchValue(name), ",") {
197		value = strings.TrimSpace(value)
198		if value == "" {
199			continue
200		}
201		if _, ok := seen[value]; ok {
202			continue
203		}
204		seen[value] = struct{}{}
205		combined = append(combined, value)
206	}
207	for _, value := range values {
208		value = strings.TrimSpace(value)
209		if value == "" {
210			continue
211		}
212		if _, ok := seen[value]; ok {
213			continue
214		}
215		seen[value] = struct{}{}
216		combined = append(combined, value)
217	}
218
219	if len(combined) == 0 {
220		return
221	}
222	commandLine.RemoveSwitch(name)
223	commandLine.AppendSwitchWithValue(name, strings.Join(combined, ","))
224}
225
226func removeCommaSeparatedSwitchValues(commandLine purecef.CommandLine, name string, values ...string) {
227	if commandLine == nil || len(values) == 0 {
228		return
229	}
230
231	existing := commandLine.GetSwitchValue(name)
232	if existing == "" {
233		return
234	}
235
236	removeSet := make(map[string]struct{}, len(values))
237	for _, v := range values {
238		removeSet[strings.TrimSpace(v)] = struct{}{}
239	}
240
241	tokens := strings.Split(existing, ",")
242	cleaned := make([]string, 0, len(tokens))
243	removed := false
244	for _, token := range tokens {
245		token = strings.TrimSpace(token)
246		if token == "" {
247			continue
248		}
249		if _, ok := removeSet[token]; ok {
250			removed = true
251			continue
252		}
253		cleaned = append(cleaned, token)
254	}
255
256	if !removed {
257		return
258	}
259
260	if len(cleaned) == 0 {
261		commandLine.RemoveSwitch(name)
262		return
263	}
264
265	commandLine.RemoveSwitch(name)
266	commandLine.AppendSwitchWithValue(name, strings.Join(cleaned, ","))
267}
268
269// dumberApp implements purecef.App to provide custom scheme registration,
270// command-line configuration, and browser-process lifecycle callbacks.
271type dumberApp struct {
272	engine *Engine
273	bph    purecef.BrowserProcessHandler
274	rph    purecef.RenderProcessHandler
275}
276
277// newDumberApp creates an App whose BrowserProcessHandler handles context
278// initialization, child process launch tracking, and scheme registration.
279// Returns the raw Go implementation — InitWithApp will wrap it exactly once.
280func newDumberApp(engine *Engine) purecef.App {
281	app := &dumberApp{engine: engine}
282	app.bph = &dumberBPH{engine: engine}
283	app.rph = newComposedRenderProcessHandler()
284	return app
285}
286
287// NewSubprocessApp returns a lightweight raw App implementation for CEF
288// subprocesses re-executed from the main dumber binary. Keep the legacy V8
289// renderer bridge disabled while composing editable-focus reporting with the
290// popup-opener callbacks needed before page scripts run.
291func NewSubprocessApp() purecef.App {
292	return &subprocessApp{rph: newComposedRenderProcessHandler()}
293}
294
295// maxCmdLineLogLen limits logged command line length to avoid leaking sensitive paths.
296const maxCmdLineLogLen = 200
297
298func (a *dumberApp) OnBeforeCommandLineProcessing(processType string, commandLine purecef.CommandLine) {
299	log := logging.FromContext(a.engine.ctx)
300	if commandLine != nil {
301		configureCommandLineWithRenderStack(commandLine, a.engine.renderStackPlan)
302
303		if processType == "" {
304			if cefWebAuthnUnsafeEnabled() {
305				log.Warn().
306					Str("env_var", cefEnableWebAuthnUnsafeEnvVar).
307					Msg("cef: WebAuthn enabled via unsupported developer override")
308			} else {
309				log.Info().
310					Strs("disabled_features", cefWebAuthnFeaturesDisabledByPolicy).
311					Str("override_env_var", cefEnableWebAuthnUnsafeEnvVar).
312					Msg("cef: WebAuthn disabled for CEF windowless/Alloy runtime")
313			}
314		}
315
316		renderNodeOverride := commandLine.GetSwitchValue(chromiumRenderNodeOverrideSwitch)
317		cmdline := commandLine.GetCommandLineString()
318		if len(cmdline) > maxCmdLineLogLen {
319			runes := []rune(cmdline)
320			if len(runes) > maxCmdLineLogLen {
321				cmdline = string(runes[:maxCmdLineLogLen]) + "…"
322			}
323		}
324		log.Debug().
325			Str("process_type", processType).
326			Str("render_node_override", renderNodeOverride).
327			Str("command_line", cmdline).
328			Msg("cef: OnBeforeCommandLineProcessing")
329	}
330}
331func (a *dumberApp) OnRegisterCustomSchemes(registrar purecef.SchemeRegistrar) {
332	registerDumbScheme(registrar)
333	logging.FromContext(a.engine.ctx).Debug().Msg("cef: registered dumb:// custom scheme")
334}
335func (a *dumberApp) GetResourceBundleHandler() purecef.ResourceBundleHandler { return nil }
336func (a *dumberApp) GetBrowserProcessHandler() purecef.BrowserProcessHandler { return a.bph }
337func (a *dumberApp) GetRenderProcessHandler() purecef.RenderProcessHandler   { return a.rph }
338
339type subprocessApp struct{ rph purecef.RenderProcessHandler }
340
341func (a *subprocessApp) OnBeforeCommandLineProcessing(_ string, commandLine purecef.CommandLine) {
342	configureCommandLine(commandLine)
343}
344func (a *subprocessApp) OnRegisterCustomSchemes(registrar purecef.SchemeRegistrar) {
345	registerDumbScheme(registrar)
346}
347func (a *subprocessApp) GetResourceBundleHandler() purecef.ResourceBundleHandler { return nil }
348func (a *subprocessApp) GetBrowserProcessHandler() purecef.BrowserProcessHandler { return nil }
349func (a *subprocessApp) GetRenderProcessHandler() purecef.RenderProcessHandler   { return a.rph }
350
351// dumberBPH implements purecef.BrowserProcessHandler for context initialization,
352// child process launch tracking, and diagnostic logging.
353type dumberBPH struct {
354	engine *Engine
355}
356
357func (h *dumberBPH) OnRegisterCustomPreferences(_ purecef.PreferencesType, _ purecef.PreferenceRegistrar) {
358}
359func (h *dumberBPH) OnContextInitialized() {
360	h.engine.recordContextInitialized()
361}
362func (h *dumberBPH) OnBeforeChildProcessLaunch(commandLine purecef.CommandLine) {
363	processType := ""
364	commandLineString := ""
365	useAngle := ""
366	ozonePlatform := ""
367	renderNodeOverride := ""
368	if commandLine != nil {
369		configureCommandLineWithRenderStack(commandLine, h.engine.renderStackPlan)
370		appendSwitchIfMissing(commandLine, "no-zygote")
371		processType = commandLine.GetSwitchValue("type")
372		commandLineString = commandLine.GetCommandLineString()
373		useAngle = commandLine.GetSwitchValue("use-angle")
374		ozonePlatform = commandLine.GetSwitchValue("ozone-platform")
375		renderNodeOverride = commandLine.GetSwitchValue(chromiumRenderNodeOverrideSwitch)
376	}
377	h.engine.recordChildProcessLaunch(processType, useAngle, ozonePlatform, renderNodeOverride, commandLineString)
378}
379
380func appendSwitchIfMissing(commandLine purecef.CommandLine, name string) {
381	if commandLine == nil || commandLine.HasSwitch(name) {
382		return
383	}
384	commandLine.AppendSwitch(name)
385}
386
387func parseRelaunchCommandLineArgs(commandLine string) []string {
388	args := make([]string, 0, 4)
389	var current strings.Builder
390	var quote rune
391	escaped := false
392	flush := func() {
393		if current.Len() == 0 {
394			return
395		}
396		args = append(args, current.String())
397		current.Reset()
398	}
399
400	for _, r := range commandLine {
401		switch {
402		case escaped:
403			current.WriteRune(r)
404			escaped = false
405		case r == '\\':
406			escaped = true
407		case quote != 0:
408			if r == quote {
409				quote = 0
410				continue
411			}
412			current.WriteRune(r)
413		case r == '"' || r == '\'':
414			quote = r
415		case r == ' ' || r == '\t' || r == '\n' || r == '\r':
416			flush()
417		default:
418			current.WriteRune(r)
419		}
420	}
421
422	if escaped {
423		current.WriteRune('\\')
424	}
425	flush()
426
427	return args
428}
429
430func parseBrowseRelaunchCommandLine(commandLine purecef.CommandLine) (string, bool) {
431	if commandLine == nil {
432		return "", false
433	}
434
435	args := parseRelaunchCommandLineArgs(commandLine.GetCommandLineString())
436	if len(args) < 2 || args[1] != "browse" {
437		return "", false
438	}
439	var browseURL string
440	for index := 2; index < len(args); index++ {
441		arg := args[index]
442		switch {
443		case arg == "--instance":
444			index++
445			if index >= len(args) || args[index] == "" {
446				return "", false
447			}
448		case strings.HasPrefix(arg, "--instance="):
449			if strings.TrimPrefix(arg, "--instance=") == "" {
450				return "", false
451			}
452		case arg == "--profile" || strings.HasPrefix(arg, "--profile=") || arg == "--ephemeral":
453			// Profile-scoped launches must be handled by their own elected host;
454			// CEF's already-running callback must never route them into this one.
455			return "", false
456		case strings.HasPrefix(arg, "-"):
457			return "", false
458		case browseURL == "":
459			browseURL = arg
460		default:
461			return "", false
462		}
463	}
464	if browseURL == "" {
465		return "", false
466	}
467	return browseURL, true
468}
469
470func isBrowseRelaunchCommandLine(commandLine purecef.CommandLine) bool {
471	if commandLine == nil {
472		return false
473	}
474
475	args := parseRelaunchCommandLineArgs(commandLine.GetCommandLineString())
476	return len(args) >= 2 && args[1] == "browse"
477}
478
479func isGPUProcessRelaunchCommandLine(commandLine purecef.CommandLine) bool {
480	if commandLine == nil {
481		return false
482	}
483	for _, arg := range parseRelaunchCommandLineArgs(commandLine.GetCommandLineString()) {
484		if arg == "--type=gpu-process" {
485			return true
486		}
487	}
488	return false
489}
490
491func parseBrowseURLFromRelaunchCommandLine(commandLine purecef.CommandLine) string {
492	browseURL, _ := parseBrowseRelaunchCommandLine(commandLine)
493	return browseURL
494}
495
496func (h *dumberBPH) OnAlreadyRunningAppRelaunch(commandLine purecef.CommandLine, _ string) int32 {
497	if isGPUProcessRelaunchCommandLine(commandLine) {
498		if h != nil && h.engine != nil {
499			h.engine.recordGPURelaunch()
500		}
501		return 0
502	}
503	if browseURL, ok := parseBrowseRelaunchCommandLine(commandLine); ok {
504		if h != nil && h.engine != nil {
505			if handler := h.engine.alreadyRunningAppRelaunchCallback(); handler != nil {
506				handler(browseURL)
507			}
508		}
509		return 1
510	}
511	if isBrowseRelaunchCommandLine(commandLine) {
512		return 1
513	}
514
515	return 0
516}
517func (h *dumberBPH) GetDefaultClient() purecef.RawClient                            { return nil }
518func (h *dumberBPH) GetDefaultRequestContextHandler() purecef.RequestContextHandler { return nil }
519
520// OnScheduleMessagePumpWork is a no-op — multi-threaded message loop drives
521// its own pump. Required by the BrowserProcessHandler interface.
522func (h *dumberBPH) OnScheduleMessagePumpWork(_ int64) {}

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