detached 956f1eb

fix(gui): restore omnibox styling and messaging in isolated world

Fixes omnibox and GUI components appearing without styles on YouTube
and other sites with aggressive scripts.

Root causes:
- CSS variables from color-scheme.js only in main world
- webkit.messageHandlers only accessible in main world
- Component styles injected via UserStyleSheet (doesn't work with Shadow DOM)

Changes:
- Inject color-scheme.js in both main and isolated worlds for CSS variables
- Implement CustomEvent bridge (dumber:isolated-message) from isolated to main world
- Move main-world.ts injection from isolated to main world
- Change component styles to shadow root injection instead of UserStyleSheet
- Update messaging.ts to dispatch CustomEvents instead of direct webkit calls
- Register message handler in main world (not isolated)

Architecture:
GUI (isolated) → CustomEvent → main-world.ts (main) → webkit.messageHandlers → Go backend

Resolves omnibox rendering with full styles and working suggestions.
commit
956f1eb1feebbb372598299bb59bf4834faf31a8
author
bnema · · 2025-11-16 20:16 UTC
committer
bnema · · 2025-11-16 19:42 UTC
parent
e075001
signature
unsigned
8 files · +197 -75
M CHANGELOG.md +1 -0
@@ -14,6 +14,7 @@ All notable changes to this project will be documented in this file.
1414 - **Code cleanup**: Removed dead code files and emojis from GUI modules
1515
1616 ### Fixed
17+- **Isolated world GUI rendering**: Fixed omnibox and GUI components appearing without styles on YouTube and other aggressive sites. Injected `color-scheme.js` in both main and isolated worlds to provide CSS variables. Implemented CustomEvent bridge (`dumber:isolated-message`) from isolated world to main world for webkit.messageHandlers communication. Moved main-world script injection from isolated to main world. Changed component styles injection from UserStyleSheet to shadow root injection for proper Shadow DOM encapsulation.
1718 - **Favicon processing on refresh**: Skip redundant favicon processing when WebKit's cache already has the URI. Made domain propagation async to prevent blocking page loads.
1819 - **Popup handler registration timing**: Fixed `g_object_ref` assertion errors when WebKit takes ownership of popup WebViews by connecting signals immediately instead of deferring via `glib.IdleAdd`. This resolves crashes with `target="_blank"` links and `window.open()` calls.
1920
@@ -14,6 +14,7 @@ All notable changes to this project will be documented in this file.
14 - **Code cleanup**: Removed dead code files and emojis from GUI modules14 - **Code cleanup**: Removed dead code files and emojis from GUI modules
15 15
16 ### Fixed16 ### Fixed
17+- **Isolated world GUI rendering**: Fixed omnibox and GUI components appearing without styles on YouTube and other aggressive sites. Injected `color-scheme.js` in both main and isolated worlds to provide CSS variables. Implemented CustomEvent bridge (`dumber:isolated-message`) from isolated world to main world for webkit.messageHandlers communication. Moved main-world script injection from isolated to main world. Changed component styles injection from UserStyleSheet to shadow root injection for proper Shadow DOM encapsulation.
17 - **Favicon processing on refresh**: Skip redundant favicon processing when WebKit's cache already has the URI. Made domain propagation async to prevent blocking page loads.18 - **Favicon processing on refresh**: Skip redundant favicon processing when WebKit's cache already has the URI. Made domain propagation async to prevent blocking page loads.
18 - **Popup handler registration timing**: Fixed `g_object_ref` assertion errors when WebKit takes ownership of popup WebViews by connecting signals immediately instead of deferring via `glib.IdleAdd`. This resolves crashes with `target="_blank"` links and `window.open()` calls.19 - **Popup handler registration timing**: Fixed `g_object_ref` assertion errors when WebKit takes ownership of popup WebViews by connecting signals immediately instead of deferring via `glib.IdleAdd`. This resolves crashes with `target="_blank"` links and `window.open()` calls.
19 20
M assets/gui_embed.go +6 -0
@@ -12,3 +12,9 @@ var GUIScript string
1212
1313 //go:embed gui/color-scheme.js
1414 var ColorSchemeScript string
15+
16+// GUI component styles extracted from Svelte components
17+// These are injected into the isolated world using UserStyleSheet
18+//
19+//go:embed gui/gui.min.css
20+var ComponentStyles string
@@ -12,3 +12,9 @@ var GUIScript string
12 12
13 //go:embed gui/color-scheme.js13 //go:embed gui/color-scheme.js
14 var ColorSchemeScript string14 var ColorSchemeScript string
15+
16+// GUI component styles extracted from Svelte components
17+// These are injected into the isolated world using UserStyleSheet
18+//
19+//go:embed gui/gui.min.css
20+var ComponentStyles string
M gui/src/components/omnibox/messaging.ts +18 -25
@@ -8,32 +8,25 @@ import type { OmniboxMessage, OmniboxMessageBridge, Suggestion, SearchShortcut }
88 import { omniboxStore } from "./stores.svelte.ts";
99
1010 export class OmniboxBridge implements OmniboxMessageBridge {
11- // Forward messages to the native WebKit message handler when available
12- // Fallback: for navigate, perform direct location change
11+ // Forward messages via CustomEvent to main world bridge
12+ // The main world will forward to webkit.messageHandlers.dumber
1313 postMessage(msg: OmniboxMessage): void {
14- console.log("📤 [DEBUG] Posting message to backend:", msg);
15- // NOTE: Do NOT detach postMessage from its receiver. In WKWebView,
16- // postMessage must be called on the UserMessageHandler instance.
17- const bridge = window.webkit?.messageHandlers?.dumber;
18- if (bridge && typeof bridge.postMessage === "function") {
19- try {
20- console.log("📱 [DEBUG] Using webkit message handler");
21- bridge.postMessage(JSON.stringify(msg));
22- return;
23- } catch (e) {
24- console.warn(
25- "postMessage to native handler failed, using fallback:",
26- e,
27- );
28- }
29- }
30- console.log("⚠️ [DEBUG] No webkit bridge available");
31- // Fallback navigation if no native bridge is available
32- if (msg.type === "navigate" && typeof msg.url === "string" && msg.url) {
33- try {
34- window.location.href = msg.url;
35- } catch (e) {
36- console.error("Fallback navigation failed:", e);
14+ console.log("📤 [DEBUG] Posting message to backend via bridge:", msg);
15+ try {
16+ // Dispatch CustomEvent that main-world bridge will listen to
17+ document.dispatchEvent(new CustomEvent("dumber:isolated-message", {
18+ detail: { payload: msg }
19+ }));
20+ console.log("✅ [DEBUG] Dispatched isolated message event");
21+ } catch (e) {
22+ console.error("❌ [DEBUG] Failed to dispatch message:", e);
23+ // Fallback navigation if dispatch fails
24+ if (msg.type === "navigate" && typeof msg.url === "string" && msg.url) {
25+ try {
26+ window.location.href = msg.url;
27+ } catch (navError) {
28+ console.error("Fallback navigation failed:", navError);
29+ }
3730 }
3831 }
3932 }
@@ -8,32 +8,25 @@ import type { OmniboxMessage, OmniboxMessageBridge, Suggestion, SearchShortcut }
8 import { omniboxStore } from "./stores.svelte.ts";8 import { omniboxStore } from "./stores.svelte.ts";
9 9
10 export class OmniboxBridge implements OmniboxMessageBridge {10 export class OmniboxBridge implements OmniboxMessageBridge {
11- // Forward messages to the native WebKit message handler when available11+ // Forward messages via CustomEvent to main world bridge
12- // Fallback: for navigate, perform direct location change12+ // The main world will forward to webkit.messageHandlers.dumber
13 postMessage(msg: OmniboxMessage): void {13 postMessage(msg: OmniboxMessage): void {
14- console.log("📤 [DEBUG] Posting message to backend:", msg);14+ console.log("📤 [DEBUG] Posting message to backend via bridge:", msg);
15- // NOTE: Do NOT detach postMessage from its receiver. In WKWebView,15+ try {
16- // postMessage must be called on the UserMessageHandler instance.16+ // Dispatch CustomEvent that main-world bridge will listen to
17- const bridge = window.webkit?.messageHandlers?.dumber;17+ document.dispatchEvent(new CustomEvent("dumber:isolated-message", {
18- if (bridge && typeof bridge.postMessage === "function") {18+ detail: { payload: msg }
19- try {19+ }));
20- console.log("📱 [DEBUG] Using webkit message handler");20+ console.log("✅ [DEBUG] Dispatched isolated message event");
21- bridge.postMessage(JSON.stringify(msg));21+ } catch (e) {
22- return;22+ console.error("❌ [DEBUG] Failed to dispatch message:", e);
23- } catch (e) {23+ // Fallback navigation if dispatch fails
24- console.warn(24+ if (msg.type === "navigate" && typeof msg.url === "string" && msg.url) {
25- "postMessage to native handler failed, using fallback:",25+ try {
26- e,26+ window.location.href = msg.url;
27- );27+ } catch (navError) {
28- }28+ console.error("Fallback navigation failed:", navError);
29- }29+ }
30- console.log("⚠️ [DEBUG] No webkit bridge available");
31- // Fallback navigation if no native bridge is available
32- if (msg.type === "navigate" && typeof msg.url === "string" && msg.url) {
33- try {
34- window.location.href = msg.url;
35- } catch (e) {
36- console.error("Fallback navigation failed:", e);
37 }30 }
38 }31 }
39 }32 }
M gui/src/injected/main-world.ts +23 -0
@@ -909,6 +909,29 @@ interface DumberAPI {
909909 });
910910 };
911911
912+ // Bridge for isolated world communication
913+ // Listen for messages from isolated world and forward to backend via webkit.messageHandlers
914+ const setupIsolatedWorldBridge = () => {
915+ document.addEventListener("dumber:isolated-message", ((event: CustomEvent) => {
916+ try {
917+ const { payload } = event.detail;
918+ if (window.webkit?.messageHandlers?.dumber) {
919+ window.webkit.messageHandlers.dumber.postMessage(JSON.stringify(payload));
920+ console.log("[dumber-bridge] Forwarded isolated world message:", payload.type);
921+ } else {
922+ console.warn("[dumber-bridge] webkit message handler not available");
923+ }
924+ } catch (error) {
925+ console.error("[dumber-bridge] Failed to forward message:", error);
926+ }
927+ }) as EventListener);
928+
929+ console.log("[dumber-bridge] Isolated world message bridge initialized");
930+ };
931+
932+ // Initialize isolated world bridge
933+ setupIsolatedWorldBridge();
934+
912935 // Initialize console capture
913936 setupConsoleCapture();
914937 } catch (err) {
@@ -909,6 +909,29 @@ interface DumberAPI {
909 });909 });
910 };910 };
911 911
912+ // Bridge for isolated world communication
913+ // Listen for messages from isolated world and forward to backend via webkit.messageHandlers
914+ const setupIsolatedWorldBridge = () => {
915+ document.addEventListener("dumber:isolated-message", ((event: CustomEvent) => {
916+ try {
917+ const { payload } = event.detail;
918+ if (window.webkit?.messageHandlers?.dumber) {
919+ window.webkit.messageHandlers.dumber.postMessage(JSON.stringify(payload));
920+ console.log("[dumber-bridge] Forwarded isolated world message:", payload.type);
921+ } else {
922+ console.warn("[dumber-bridge] webkit message handler not available");
923+ }
924+ } catch (error) {
925+ console.error("[dumber-bridge] Failed to forward message:", error);
926+ }
927+ }) as EventListener);
928+
929+ console.log("[dumber-bridge] Isolated world message bridge initialized");
930+ };
931+
932+ // Initialize isolated world bridge
933+ setupIsolatedWorldBridge();
934+
912 // Initialize console capture935 // Initialize console capture
913 setupConsoleCapture();936 setupConsoleCapture();
914 } catch (err) {937 } catch (err) {
M gui/src/injected/modules/shadowHost.ts +39 -23
@@ -182,33 +182,49 @@ export function getGlobalShadowRoot(): ShadowRoot {
182182 `;
183183 shadowRoot.appendChild(tokensStyle);
184184
185- // Inject the global GUI stylesheet so components rendered inside the shadow
186- // root (e.g., toasts, omnibox) receive their styles
185+ // Inject the global GUI stylesheet (Tailwind) into shadow root
187186 try {
188- // Prefer constructable stylesheets when available
189- const hasConstructable = typeof CSSStyleSheet !== "undefined";
190- const supportsAdopted = "adoptedStyleSheets" in shadowRoot;
191- if (supportsAdopted && hasConstructable) {
192- const sheet = new CSSStyleSheet();
193- sheet.replaceSync(globalStyles);
194- const rootWithSheets = shadowRoot as ShadowRoot & {
195- adoptedStyleSheets: CSSStyleSheet[];
196- };
197- rootWithSheets.adoptedStyleSheets = [
198- ...(rootWithSheets.adoptedStyleSheets ?? []),
199- sheet,
200- ];
201- } else {
202- // Fallback: append a <style> element with the global CSS
203- const styleTag = document.createElement("style");
204- styleTag.textContent = globalStyles;
205- shadowRoot.appendChild(styleTag);
206- }
207- } catch {
208- // Final fallback if anything above fails
187+ // Use <style> tag injection for Tailwind global styles
209188 const styleTag = document.createElement("style");
189+ styleTag.setAttribute("data-source", "dumber-tailwind-styles");
210190 styleTag.textContent = globalStyles;
211191 shadowRoot.appendChild(styleTag);
192+
193+ // Inject component styles from Go-provided string
194+ // These are Svelte component-scoped styles that must be inside the shadow root
195+ // due to Shadow DOM style encapsulation
196+ const componentStyles = (window as any).__dumber_component_styles;
197+ if (componentStyles) {
198+ const componentStyleTag = document.createElement("style");
199+ componentStyleTag.setAttribute("data-source", "dumber-component-styles");
200+ componentStyleTag.textContent = componentStyles;
201+ shadowRoot.appendChild(componentStyleTag);
202+ console.log(
203+ "[shadowHost] Component styles injected:",
204+ componentStyles.length,
205+ "chars",
206+ );
207+ } else {
208+ console.warn("[shadowHost] Component styles not available on window");
209+ }
210+
211+ console.log(
212+ "[shadowHost] All styles injected, shadow root children:",
213+ shadowRoot.childNodes.length,
214+ );
215+ } catch (error) {
216+ console.error("[shadowHost] Failed to inject styles:", error);
217+ // Fallback attempt
218+ try {
219+ const fallbackStyle = document.createElement("style");
220+ fallbackStyle.textContent = globalStyles || "";
221+ shadowRoot.appendChild(fallbackStyle);
222+ } catch (fallbackError) {
223+ console.error(
224+ "[shadowHost] Fallback style injection also failed:",
225+ fallbackError,
226+ );
227+ }
212228 }
213229 shadowResetApplied.add(shadowRoot);
214230 }
@@ -182,33 +182,49 @@ export function getGlobalShadowRoot(): ShadowRoot {
182 `;182 `;
183 shadowRoot.appendChild(tokensStyle);183 shadowRoot.appendChild(tokensStyle);
184 184
185- // Inject the global GUI stylesheet so components rendered inside the shadow185+ // Inject the global GUI stylesheet (Tailwind) into shadow root
186- // root (e.g., toasts, omnibox) receive their styles
187 try {186 try {
188- // Prefer constructable stylesheets when available187+ // Use <style> tag injection for Tailwind global styles
189- const hasConstructable = typeof CSSStyleSheet !== "undefined";
190- const supportsAdopted = "adoptedStyleSheets" in shadowRoot;
191- if (supportsAdopted && hasConstructable) {
192- const sheet = new CSSStyleSheet();
193- sheet.replaceSync(globalStyles);
194- const rootWithSheets = shadowRoot as ShadowRoot & {
195- adoptedStyleSheets: CSSStyleSheet[];
196- };
197- rootWithSheets.adoptedStyleSheets = [
198- ...(rootWithSheets.adoptedStyleSheets ?? []),
199- sheet,
200- ];
201- } else {
202- // Fallback: append a <style> element with the global CSS
203- const styleTag = document.createElement("style");
204- styleTag.textContent = globalStyles;
205- shadowRoot.appendChild(styleTag);
206- }
207- } catch {
208- // Final fallback if anything above fails
209 const styleTag = document.createElement("style");188 const styleTag = document.createElement("style");
189+ styleTag.setAttribute("data-source", "dumber-tailwind-styles");
210 styleTag.textContent = globalStyles;190 styleTag.textContent = globalStyles;
211 shadowRoot.appendChild(styleTag);191 shadowRoot.appendChild(styleTag);
192+
193+ // Inject component styles from Go-provided string
194+ // These are Svelte component-scoped styles that must be inside the shadow root
195+ // due to Shadow DOM style encapsulation
196+ const componentStyles = (window as any).__dumber_component_styles;
197+ if (componentStyles) {
198+ const componentStyleTag = document.createElement("style");
199+ componentStyleTag.setAttribute("data-source", "dumber-component-styles");
200+ componentStyleTag.textContent = componentStyles;
201+ shadowRoot.appendChild(componentStyleTag);
202+ console.log(
203+ "[shadowHost] Component styles injected:",
204+ componentStyles.length,
205+ "chars",
206+ );
207+ } else {
208+ console.warn("[shadowHost] Component styles not available on window");
209+ }
210+
211+ console.log(
212+ "[shadowHost] All styles injected, shadow root children:",
213+ shadowRoot.childNodes.length,
214+ );
215+ } catch (error) {
216+ console.error("[shadowHost] Failed to inject styles:", error);
217+ // Fallback attempt
218+ try {
219+ const fallbackStyle = document.createElement("style");
220+ fallbackStyle.textContent = globalStyles || "";
221+ shadowRoot.appendChild(fallbackStyle);
222+ } catch (fallbackError) {
223+ console.error(
224+ "[shadowHost] Fallback style injection also failed:",
225+ fallbackError,
226+ );
227+ }
212 }228 }
213 shadowResetApplied.add(shadowRoot);229 shadowResetApplied.add(shadowRoot);
214 }230 }
M gui/vite.config.gui.ts +2 -2
@@ -5,7 +5,7 @@ import { resolve } from "path";
55 export default defineConfig({
66 plugins: [
77 svelte({
8- emitCss: false, // Inline component CSS into JS bundle
8+ emitCss: true, // Extract component CSS to separate file for isolated world injection
99 }),
1010 ],
1111 build: {
@@ -18,7 +18,7 @@ export default defineConfig({
1818 name: "__dumberGUI",
1919 inlineDynamicImports: true,
2020 manualChunks: undefined, // Prevent chunk splitting
21- assetFileNames: "[name].[ext]", // Don't hash asset names since they're inlined
21+ assetFileNames: "gui.min.[ext]", // CSS will be named gui.min.css
2222 },
2323 },
2424 emptyOutDir: false,
@@ -5,7 +5,7 @@ import { resolve } from "path";
5 export default defineConfig({5 export default defineConfig({
6 plugins: [6 plugins: [
7 svelte({7 svelte({
8- emitCss: false, // Inline component CSS into JS bundle8+ emitCss: true, // Extract component CSS to separate file for isolated world injection
9 }),9 }),
10 ],10 ],
11 build: {11 build: {
@@ -18,7 +18,7 @@ export default defineConfig({
18 name: "__dumberGUI",18 name: "__dumberGUI",
19 inlineDynamicImports: true,19 inlineDynamicImports: true,
20 manualChunks: undefined, // Prevent chunk splitting20 manualChunks: undefined, // Prevent chunk splitting
21- assetFileNames: "[name].[ext]", // Don't hash asset names since they're inlined21+ assetFileNames: "gui.min.[ext]", // CSS will be named gui.min.css
22 },22 },
23 },23 },
24 emptyOutDir: false,24 emptyOutDir: false,
M pkg/webkit/user_content.go +105 -25
@@ -9,6 +9,13 @@ import (
99 webkit "github.com/diamondburned/gotk4-webkitgtk/pkg/webkit/v6"
1010 )
1111
12+const (
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+
1219 // SetupUserContentManager configures UserContentManager for the WebView
1320 // This injects GUI scripts at document-start and registers message handlers
1421 func SetupUserContentManager(view *webkit.WebView, appearanceConfigJSON string, webviewID uint64) error {
@@ -23,22 +30,35 @@ func SetupUserContentManager(view *webkit.WebView, appearanceConfigJSON string,
2330 return nil
2431 }
2532
26- // Inject webview ID FIRST, so GUI scripts can access it immediately
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
2735 // Note: webviewID is uint64, formatted as number (not string) to avoid any injection concerns
2836 webviewIDScript := fmt.Sprintf(`
2937 window.__dumber_webview_id = %d;
3038 console.log('[webkit] WebView ID set in JavaScript:', window.__dumber_webview_id);
3139 `, webviewID)
40+
41+ // Main world (for page scripts)
3242 ucm.AddScript(webkit.NewUserScript(
3343 webviewIDScript,
34- webkit.UserContentInjectTopFrame, // Only top frame needs the webview ID
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,
3554 webkit.UserScriptInjectAtDocumentStart,
55+ DumberIsolatedWorld,
3656 nil,
3757 nil,
3858 ))
39- log.Printf("[webkit] Injected webview ID script for ID: %d", webviewID)
59+ log.Printf("[webkit] Injected webview ID script for ID: %d (main + isolated world)", webviewID)
4060
41- // Inject GTK theme detection SECOND, before color-scheme script
61+ // Inject GTK theme detection in BOTH worlds
4262 // The color-scheme.ts expects window.__dumber_gtk_prefers_dark to be set
4363 // Respect the ColorScheme config setting
4464 cfg := config.Get()
@@ -57,71 +77,131 @@ func SetupUserContentManager(view *webkit.WebView, appearanceConfigJSON string,
5777 }
5878
5979 gtkThemeScript := fmt.Sprintf(`window.__dumber_gtk_prefers_dark = %t;`, prefersDark)
80+
81+ // Main world
6082 ucm.AddScript(webkit.NewUserScript(
6183 gtkThemeScript,
62- webkit.UserContentInjectTopFrame, // Only top frame needs theme preference
84+ webkit.UserContentInjectTopFrame,
6385 webkit.UserScriptInjectAtDocumentStart,
6486 nil,
6587 nil,
6688 ))
67- log.Printf("[webkit] Injected theme preference: prefersDark=%t", prefersDark)
6889
69- // Inject palette config SECOND, before GUI scripts
90+ // Isolated world
91+ ucm.AddScript(webkit.NewUserScriptForWorld(
92+ gtkThemeScript,
93+ webkit.UserContentInjectTopFrame,
94+ webkit.UserScriptInjectAtDocumentStart,
95+ DumberIsolatedWorld,
96+ nil,
97+ nil,
98+ ))
99+ log.Printf("[webkit] Injected theme preference: prefersDark=%t (main + isolated world)", prefersDark)
100+
101+ // Inject palette config in BOTH worlds
70102 // The GUI expects window.__dumber_palette = { "light": {...}, "dark": {...} }
71103 if appearanceConfigJSON != "" {
72104 paletteScript := fmt.Sprintf(`window.__dumber_palette = %s;`, appearanceConfigJSON)
105+
106+ // Main world
73107 ucm.AddScript(webkit.NewUserScript(
74108 paletteScript,
75- webkit.UserContentInjectTopFrame, // Only top frame needs palette config
109+ webkit.UserContentInjectTopFrame,
76110 webkit.UserScriptInjectAtDocumentStart,
77111 nil,
78112 nil,
79113 ))
80- log.Printf("[webkit] Injected palette config at document-start (%d bytes)", len(paletteScript))
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+ log.Printf("[webkit] Injected palette config (%d bytes, main + isolated world)", len(paletteScript))
81125 }
82126
83- // Inject color-scheme script at document-start
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
84130 if assets.ColorSchemeScript != "" {
131+ // Main world
85132 ucm.AddScript(webkit.NewUserScript(
86133 assets.ColorSchemeScript,
87- webkit.UserContentInjectTopFrame, // Only top frame needs color scheme detection
134+ webkit.UserContentInjectTopFrame,
88135 webkit.UserScriptInjectAtDocumentStart,
89- nil, // whitelist (nil = all)
90- nil, // blacklist (nil = none)
136+ nil,
137+ nil,
91138 ))
92- log.Printf("[webkit] Injected color-scheme script (%d bytes)", len(assets.ColorSchemeScript))
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+ log.Printf("[webkit] Injected color-scheme script (%d bytes, main + isolated world)", len(assets.ColorSchemeScript))
93150 }
94151
95- // Inject main-world script at document-start
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
96155 if assets.MainWorldScript != "" {
97156 ucm.AddScript(webkit.NewUserScript(
98157 assets.MainWorldScript,
99- webkit.UserContentInjectTopFrame, // Only top frame needs bridge/UI functionality
158+ webkit.UserContentInjectTopFrame,
100159 webkit.UserScriptInjectAtDocumentStart,
101160 nil,
102161 nil,
103162 ))
104- log.Printf("[webkit] Injected main-world script (%d bytes)", len(assets.MainWorldScript))
163+ log.Printf("[webkit] Injected main-world script (%d bytes, main world)", len(assets.MainWorldScript))
105164 }
106165
107- // Inject GUI controls script at document-start
166+ // Inject GUI controls script in ISOLATED world only
167+ // This contains Svelte components and must be protected from page interference
108168 if assets.GUIScript != "" {
109- ucm.AddScript(webkit.NewUserScript(
169+ ucm.AddScript(webkit.NewUserScriptForWorld(
110170 assets.GUIScript,
111- webkit.UserContentInjectTopFrame, // Only top frame needs GUI controls
171+ webkit.UserContentInjectTopFrame,
172+ webkit.UserScriptInjectAtDocumentStart,
173+ DumberIsolatedWorld,
174+ nil,
175+ nil,
176+ ))
177+ log.Printf("[webkit] Injected GUI controls script (%d bytes, isolated world)", len(assets.GUIScript))
178+ }
179+
180+ // Inject component CSS styles as a JavaScript variable in ISOLATED world
181+ // These styles need to be injected into the shadow root by shadowHost.ts
182+ // because Shadow DOM has style encapsulation - external stylesheets don't penetrate it
183+ if assets.ComponentStyles != "" {
184+ // Use fmt.Sprintf with %q to properly escape the CSS string for JavaScript
185+ componentStylesScript := fmt.Sprintf(`window.__dumber_component_styles = %q;`, assets.ComponentStyles)
186+
187+ ucm.AddScript(webkit.NewUserScriptForWorld(
188+ componentStylesScript,
189+ webkit.UserContentInjectTopFrame,
112190 webkit.UserScriptInjectAtDocumentStart,
191+ DumberIsolatedWorld,
113192 nil,
114193 nil,
115194 ))
116- log.Printf("[webkit] Injected GUI controls script (%d bytes)", len(assets.GUIScript))
195+ log.Printf("[webkit] Injected component styles string (%d bytes, isolated world)", len(assets.ComponentStyles))
117196 }
118197
119- // Register script message handler "dumber" for communication from JS
120- // Pass empty string for worldName to use the default world
198+ // Register script message handler "dumber" in the MAIN world
199+ // Isolated world GUI scripts dispatch CustomEvents to main world, which forwards to this handler
200+ // This architecture is required because webkit.messageHandlers is only available in main world
121201 if !ucm.RegisterScriptMessageHandler("dumber", "") {
122- log.Printf("[webkit] Warning: failed to register 'dumber' script message handler")
202+ log.Printf("[webkit] Warning: failed to register 'dumber' script message handler in main world")
123203 } else {
124- log.Printf("[webkit] Registered 'dumber' script message handler")
204+ log.Printf("[webkit] Registered 'dumber' script message handler in main world (default)")
125205 }
126206
127207 return nil
@@ -9,6 +9,13 @@ import (
9 webkit "github.com/diamondburned/gotk4-webkitgtk/pkg/webkit/v6"9 webkit "github.com/diamondburned/gotk4-webkitgtk/pkg/webkit/v6"
10 )10 )
11 11
12+const (
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+
12 // SetupUserContentManager configures UserContentManager for the WebView19 // SetupUserContentManager configures UserContentManager for the WebView
13 // This injects GUI scripts at document-start and registers message handlers20 // This injects GUI scripts at document-start and registers message handlers
14 func SetupUserContentManager(view *webkit.WebView, appearanceConfigJSON string, webviewID uint64) error {21 func SetupUserContentManager(view *webkit.WebView, appearanceConfigJSON string, webviewID uint64) error {
@@ -23,22 +30,35 @@ func SetupUserContentManager(view *webkit.WebView, appearanceConfigJSON string,
23 return nil30 return nil
24 }31 }
25 32
26- // Inject webview ID FIRST, so GUI scripts can access it immediately33+ // 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
27 // Note: webviewID is uint64, formatted as number (not string) to avoid any injection concerns35 // Note: webviewID is uint64, formatted as number (not string) to avoid any injection concerns
28 webviewIDScript := fmt.Sprintf(`36 webviewIDScript := fmt.Sprintf(`
29 window.__dumber_webview_id = %d;37 window.__dumber_webview_id = %d;
30 console.log('[webkit] WebView ID set in JavaScript:', window.__dumber_webview_id);38 console.log('[webkit] WebView ID set in JavaScript:', window.__dumber_webview_id);
31 `, webviewID)39 `, webviewID)
40+
41+ // Main world (for page scripts)
32 ucm.AddScript(webkit.NewUserScript(42 ucm.AddScript(webkit.NewUserScript(
33 webviewIDScript,43 webviewIDScript,
34- webkit.UserContentInjectTopFrame, // Only top frame needs the webview ID44+ 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,
35 webkit.UserScriptInjectAtDocumentStart,54 webkit.UserScriptInjectAtDocumentStart,
55+ DumberIsolatedWorld,
36 nil,56 nil,
37 nil,57 nil,
38 ))58 ))
39- log.Printf("[webkit] Injected webview ID script for ID: %d", webviewID)59+ log.Printf("[webkit] Injected webview ID script for ID: %d (main + isolated world)", webviewID)
40 60
41- // Inject GTK theme detection SECOND, before color-scheme script61+ // Inject GTK theme detection in BOTH worlds
42 // The color-scheme.ts expects window.__dumber_gtk_prefers_dark to be set62 // The color-scheme.ts expects window.__dumber_gtk_prefers_dark to be set
43 // Respect the ColorScheme config setting63 // Respect the ColorScheme config setting
44 cfg := config.Get()64 cfg := config.Get()
@@ -57,71 +77,131 @@ func SetupUserContentManager(view *webkit.WebView, appearanceConfigJSON string,
57 }77 }
58 78
59 gtkThemeScript := fmt.Sprintf(`window.__dumber_gtk_prefers_dark = %t;`, prefersDark)79 gtkThemeScript := fmt.Sprintf(`window.__dumber_gtk_prefers_dark = %t;`, prefersDark)
80+
81+ // Main world
60 ucm.AddScript(webkit.NewUserScript(82 ucm.AddScript(webkit.NewUserScript(
61 gtkThemeScript,83 gtkThemeScript,
62- webkit.UserContentInjectTopFrame, // Only top frame needs theme preference84+ webkit.UserContentInjectTopFrame,
63 webkit.UserScriptInjectAtDocumentStart,85 webkit.UserScriptInjectAtDocumentStart,
64 nil,86 nil,
65 nil,87 nil,
66 ))88 ))
67- log.Printf("[webkit] Injected theme preference: prefersDark=%t", prefersDark)
68 89
69- // Inject palette config SECOND, before GUI scripts90+ // Isolated world
91+ ucm.AddScript(webkit.NewUserScriptForWorld(
92+ gtkThemeScript,
93+ webkit.UserContentInjectTopFrame,
94+ webkit.UserScriptInjectAtDocumentStart,
95+ DumberIsolatedWorld,
96+ nil,
97+ nil,
98+ ))
99+ log.Printf("[webkit] Injected theme preference: prefersDark=%t (main + isolated world)", prefersDark)
100+
101+ // Inject palette config in BOTH worlds
70 // The GUI expects window.__dumber_palette = { "light": {...}, "dark": {...} }102 // The GUI expects window.__dumber_palette = { "light": {...}, "dark": {...} }
71 if appearanceConfigJSON != "" {103 if appearanceConfigJSON != "" {
72 paletteScript := fmt.Sprintf(`window.__dumber_palette = %s;`, appearanceConfigJSON)104 paletteScript := fmt.Sprintf(`window.__dumber_palette = %s;`, appearanceConfigJSON)
105+
106+ // Main world
73 ucm.AddScript(webkit.NewUserScript(107 ucm.AddScript(webkit.NewUserScript(
74 paletteScript,108 paletteScript,
75- webkit.UserContentInjectTopFrame, // Only top frame needs palette config109+ webkit.UserContentInjectTopFrame,
76 webkit.UserScriptInjectAtDocumentStart,110 webkit.UserScriptInjectAtDocumentStart,
77 nil,111 nil,
78 nil,112 nil,
79 ))113 ))
80- log.Printf("[webkit] Injected palette config at document-start (%d bytes)", len(paletteScript))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+ log.Printf("[webkit] Injected palette config (%d bytes, main + isolated world)", len(paletteScript))
81 }125 }
82 126
83- // Inject color-scheme script at document-start127+ // 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
84 if assets.ColorSchemeScript != "" {130 if assets.ColorSchemeScript != "" {
131+ // Main world
85 ucm.AddScript(webkit.NewUserScript(132 ucm.AddScript(webkit.NewUserScript(
86 assets.ColorSchemeScript,133 assets.ColorSchemeScript,
87- webkit.UserContentInjectTopFrame, // Only top frame needs color scheme detection134+ webkit.UserContentInjectTopFrame,
88 webkit.UserScriptInjectAtDocumentStart,135 webkit.UserScriptInjectAtDocumentStart,
89- nil, // whitelist (nil = all)136+ nil,
90- nil, // blacklist (nil = none)137+ nil,
91 ))138 ))
92- log.Printf("[webkit] Injected color-scheme script (%d bytes)", len(assets.ColorSchemeScript))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+ log.Printf("[webkit] Injected color-scheme script (%d bytes, main + isolated world)", len(assets.ColorSchemeScript))
93 }150 }
94 151
95- // Inject main-world script at document-start152+ // 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
96 if assets.MainWorldScript != "" {155 if assets.MainWorldScript != "" {
97 ucm.AddScript(webkit.NewUserScript(156 ucm.AddScript(webkit.NewUserScript(
98 assets.MainWorldScript,157 assets.MainWorldScript,
99- webkit.UserContentInjectTopFrame, // Only top frame needs bridge/UI functionality158+ webkit.UserContentInjectTopFrame,
100 webkit.UserScriptInjectAtDocumentStart,159 webkit.UserScriptInjectAtDocumentStart,
101 nil,160 nil,
102 nil,161 nil,
103 ))162 ))
104- log.Printf("[webkit] Injected main-world script (%d bytes)", len(assets.MainWorldScript))163+ log.Printf("[webkit] Injected main-world script (%d bytes, main world)", len(assets.MainWorldScript))
105 }164 }
106 165
107- // Inject GUI controls script at document-start166+ // Inject GUI controls script in ISOLATED world only
167+ // This contains Svelte components and must be protected from page interference
108 if assets.GUIScript != "" {168 if assets.GUIScript != "" {
109- ucm.AddScript(webkit.NewUserScript(169+ ucm.AddScript(webkit.NewUserScriptForWorld(
110 assets.GUIScript,170 assets.GUIScript,
111- webkit.UserContentInjectTopFrame, // Only top frame needs GUI controls171+ webkit.UserContentInjectTopFrame,
172+ webkit.UserScriptInjectAtDocumentStart,
173+ DumberIsolatedWorld,
174+ nil,
175+ nil,
176+ ))
177+ log.Printf("[webkit] Injected GUI controls script (%d bytes, isolated world)", len(assets.GUIScript))
178+ }
179+
180+ // Inject component CSS styles as a JavaScript variable in ISOLATED world
181+ // These styles need to be injected into the shadow root by shadowHost.ts
182+ // because Shadow DOM has style encapsulation - external stylesheets don't penetrate it
183+ if assets.ComponentStyles != "" {
184+ // Use fmt.Sprintf with %q to properly escape the CSS string for JavaScript
185+ componentStylesScript := fmt.Sprintf(`window.__dumber_component_styles = %q;`, assets.ComponentStyles)
186+
187+ ucm.AddScript(webkit.NewUserScriptForWorld(
188+ componentStylesScript,
189+ webkit.UserContentInjectTopFrame,
112 webkit.UserScriptInjectAtDocumentStart,190 webkit.UserScriptInjectAtDocumentStart,
191+ DumberIsolatedWorld,
113 nil,192 nil,
114 nil,193 nil,
115 ))194 ))
116- log.Printf("[webkit] Injected GUI controls script (%d bytes)", len(assets.GUIScript))195+ log.Printf("[webkit] Injected component styles string (%d bytes, isolated world)", len(assets.ComponentStyles))
117 }196 }
118 197
119- // Register script message handler "dumber" for communication from JS198+ // Register script message handler "dumber" in the MAIN world
120- // Pass empty string for worldName to use the default world199+ // Isolated world GUI scripts dispatch CustomEvents to main world, which forwards to this handler
200+ // This architecture is required because webkit.messageHandlers is only available in main world
121 if !ucm.RegisterScriptMessageHandler("dumber", "") {201 if !ucm.RegisterScriptMessageHandler("dumber", "") {
122- log.Printf("[webkit] Warning: failed to register 'dumber' script message handler")202+ log.Printf("[webkit] Warning: failed to register 'dumber' script message handler in main world")
123 } else {203 } else {
124- log.Printf("[webkit] Registered 'dumber' script message handler")204+ log.Printf("[webkit] Registered 'dumber' script message handler in main world (default)")
125 }205 }
126 206
127 return nil207 return nil
M pkg/webkit/webview.go +3 -0
@@ -731,6 +731,9 @@ func (w *WebView) InjectScript(script string) error {
731731 }
732732
733733 // DispatchCustomEvent dispatches a custom event via JavaScript
734+// This works across JavaScript world boundaries because it uses document.dispatchEvent.
735+// GUI scripts running in the isolated world (see DumberIsolatedWorld in user_content.go)
736+// can listen for these events because they share the same Document object with the main world.
734737 func (w *WebView) DispatchCustomEvent(eventName string, data interface{}) error {
735738 // Serialize data to JSON
736739 jsonData, err := json.Marshal(data)
@@ -731,6 +731,9 @@ func (w *WebView) InjectScript(script string) error {
731 }731 }
732 732
733 // DispatchCustomEvent dispatches a custom event via JavaScript733 // DispatchCustomEvent dispatches a custom event via JavaScript
734+// This works across JavaScript world boundaries because it uses document.dispatchEvent.
735+// GUI scripts running in the isolated world (see DumberIsolatedWorld in user_content.go)
736+// can listen for these events because they share the same Document object with the main world.
734 func (w *WebView) DispatchCustomEvent(eventName string, data interface{}) error {737 func (w *WebView) DispatchCustomEvent(eventName string, data interface{}) error {
735 // Serialize data to JSON738 // Serialize data to JSON
736 jsonData, err := json.Marshal(data)739 jsonData, err := json.Marshal(data)

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