v0.15.0 9006957

user_content.go RawHistoryFolder

1package webkit
2
3import (
4	"fmt"
5
6	"github.com/bnema/dumber/assets"
7	"github.com/bnema/dumber/internal/config"
8	"github.com/bnema/dumber/internal/logging"
9	webkit "github.com/diamondburned/gotk4-webkitgtk/pkg/webkit/v6"
10)
11
12const (
13	// DumberIsolatedWorld is the name of the isolated JavaScript world
14	// where Dumber's GUI scripts run. This prevents page scripts from
15	// interfering with the browser's UI components.
16	DumberIsolatedWorld = "dumber-isolated"
17)
18
19// SetupUserContentManager configures UserContentManager for the WebView
20// This injects GUI scripts at document-start and registers message handlers
21func SetupUserContentManager(view *webkit.WebView, appearanceConfigJSON string, webviewID uint64) error {
22	if view == nil {
23		return nil
24	}
25
26	// Get the UserContentManager from the WebView
27	ucm := view.UserContentManager()
28	if ucm == nil {
29		logging.Warn(fmt.Sprintf("[webkit] UserContentManager is nil, skipping script injection"))
30		return nil
31	}
32
33	// Inject webview ID in BOTH main world and isolated world
34	// The isolated world (GUI scripts) needs this, and pages might use it for communication
35	// Note: webviewID is uint64, formatted as number (not string) to avoid any injection concerns
36	webviewIDScript := fmt.Sprintf(`
37		window.__dumber_webview_id = %d;
38		console.log('[webkit] WebView ID set in JavaScript:', window.__dumber_webview_id);
39	`, webviewID)
40
41	// Main world (for page scripts)
42	ucm.AddScript(webkit.NewUserScript(
43		webviewIDScript,
44		webkit.UserContentInjectTopFrame,
45		webkit.UserScriptInjectAtDocumentStart,
46		nil,
47		nil,
48	))
49
50	// Isolated world (for GUI scripts)
51	ucm.AddScript(webkit.NewUserScriptForWorld(
52		webviewIDScript,
53		webkit.UserContentInjectTopFrame,
54		webkit.UserScriptInjectAtDocumentStart,
55		DumberIsolatedWorld,
56		nil,
57		nil,
58	))
59	logging.Debug(fmt.Sprintf("[webkit] Injected webview ID script for ID: %d (main + isolated world)", webviewID))
60
61	// Inject GTK theme detection in BOTH worlds
62	// The color-scheme.ts expects window.__dumber_gtk_prefers_dark to be set
63	// Respect the ColorScheme config setting
64	cfg := config.Get()
65	var prefersDark bool
66	switch cfg.Appearance.ColorScheme {
67	case "prefer-dark":
68		prefersDark = true
69		logging.Debug(fmt.Sprintf("[webkit] Using config-forced dark theme (color_scheme: prefer-dark)"))
70	case "prefer-light":
71		prefersDark = false
72		logging.Debug(fmt.Sprintf("[webkit] Using config-forced light theme (color_scheme: prefer-light)"))
73	default:
74		// "default" or empty - follow system GTK preference
75		prefersDark = PrefersDarkTheme()
76		logging.Debug(fmt.Sprintf("[webkit] Using system GTK theme preference (color_scheme: %s)", cfg.Appearance.ColorScheme))
77	}
78
79	gtkThemeScript := fmt.Sprintf(`window.__dumber_gtk_prefers_dark = %t;`, prefersDark)
80
81	// Main world
82	ucm.AddScript(webkit.NewUserScript(
83		gtkThemeScript,
84		webkit.UserContentInjectTopFrame,
85		webkit.UserScriptInjectAtDocumentStart,
86		nil,
87		nil,
88	))
89
90	// Isolated world
91	ucm.AddScript(webkit.NewUserScriptForWorld(
92		gtkThemeScript,
93		webkit.UserContentInjectTopFrame,
94		webkit.UserScriptInjectAtDocumentStart,
95		DumberIsolatedWorld,
96		nil,
97		nil,
98	))
99	logging.Debug(fmt.Sprintf("[webkit] Injected theme preference: prefersDark=%t (main + isolated world)", prefersDark))
100
101	// Inject palette config in BOTH worlds
102	// The GUI expects window.__dumber_palette = { "light": {...}, "dark": {...} }
103	if appearanceConfigJSON != "" {
104		paletteScript := fmt.Sprintf(`window.__dumber_palette = %s;`, appearanceConfigJSON)
105
106		// Main world
107		ucm.AddScript(webkit.NewUserScript(
108			paletteScript,
109			webkit.UserContentInjectTopFrame,
110			webkit.UserScriptInjectAtDocumentStart,
111			nil,
112			nil,
113		))
114
115		// Isolated world
116		ucm.AddScript(webkit.NewUserScriptForWorld(
117			paletteScript,
118			webkit.UserContentInjectTopFrame,
119			webkit.UserScriptInjectAtDocumentStart,
120			DumberIsolatedWorld,
121			nil,
122			nil,
123		))
124		logging.Debug(fmt.Sprintf("[webkit] Injected palette config (%d bytes, main + isolated world)", len(paletteScript)))
125	}
126
127	// Inject color-scheme script in BOTH worlds
128	// Main world: manipulates document.documentElement.classList for page scripts
129	// Isolated world: sets CSS variables that GUI scripts can read via getComputedStyle
130	if assets.ColorSchemeScript != "" {
131		// Main world
132		ucm.AddScript(webkit.NewUserScript(
133			assets.ColorSchemeScript,
134			webkit.UserContentInjectTopFrame,
135			webkit.UserScriptInjectAtDocumentStart,
136			nil,
137			nil,
138		))
139
140		// Isolated world - needed so GUI scripts can access CSS variables
141		ucm.AddScript(webkit.NewUserScriptForWorld(
142			assets.ColorSchemeScript,
143			webkit.UserContentInjectTopFrame,
144			webkit.UserScriptInjectAtDocumentStart,
145			DumberIsolatedWorld,
146			nil,
147			nil,
148		))
149		logging.Debug(fmt.Sprintf("[webkit] Injected color-scheme script (%d bytes, main + isolated world)", len(assets.ColorSchemeScript)))
150	}
151
152	// Inject main-world script in MAIN world
153	// This script needs webkit.messageHandlers access to forward isolated world messages
154	// It also handles theme, zoom, and other main-world only APIs
155	if assets.MainWorldScript != "" {
156		ucm.AddScript(webkit.NewUserScript(
157			assets.MainWorldScript,
158			webkit.UserContentInjectTopFrame,
159			webkit.UserScriptInjectAtDocumentStart,
160			nil,
161			nil,
162		))
163		logging.Debug(fmt.Sprintf("[webkit] Injected main-world script (%d bytes, main world)", len(assets.MainWorldScript)))
164	}
165
166	// Inject GUI controls script in ISOLATED world at document-start
167	// This contains Svelte components and must be protected from page interference
168	// Loading at document-start ensures the omnibox is available as early as possible
169	if assets.GUIScript != "" {
170		ucm.AddScript(webkit.NewUserScriptForWorld(
171			assets.GUIScript,
172			webkit.UserContentInjectTopFrame,
173			webkit.UserScriptInjectAtDocumentStart,
174			DumberIsolatedWorld,
175			nil,
176			nil,
177		))
178		logging.Debug(fmt.Sprintf("[webkit] Injected GUI controls script (%d bytes, isolated world, document-start)", len(assets.GUIScript)))
179	}
180
181	// Inject component CSS styles as a JavaScript variable in ISOLATED world at document-start
182	// These styles need to be injected into the shadow root by shadowHost.ts
183	// because Shadow DOM has style encapsulation - external stylesheets don't penetrate it
184	// Keep at document-start to prevent flash of unstyled content
185	if assets.ComponentStyles != "" {
186		// Use fmt.Sprintf with %q to properly escape the CSS string for JavaScript
187		componentStylesScript := fmt.Sprintf(`window.__dumber_component_styles = %q;`, assets.ComponentStyles)
188
189		ucm.AddScript(webkit.NewUserScriptForWorld(
190			componentStylesScript,
191			webkit.UserContentInjectTopFrame,
192			webkit.UserScriptInjectAtDocumentStart,
193			DumberIsolatedWorld,
194			nil,
195			nil,
196		))
197		logging.Debug(fmt.Sprintf("[webkit] Injected component styles string (%d bytes, isolated world, document-start)", len(assets.ComponentStyles)))
198	}
199
200	// Register script message handler "dumber" in the MAIN world
201	// Isolated world GUI scripts dispatch CustomEvents to main world, which forwards to this handler
202	// This architecture is required because webkit.messageHandlers is only available in main world
203	if !ucm.RegisterScriptMessageHandler("dumber", "") {
204		logging.Warn(fmt.Sprintf("[webkit] Warning: failed to register 'dumber' script message handler in main world"))
205	} else {
206		logging.Debug(fmt.Sprintf("[webkit] Registered 'dumber' script message handler in main world (default)"))
207	}
208
209	return nil
210}

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