detached 76a4000

Merge pull request #68 from bnema/fix/dark-mode-injection-artifacts

fix(webkit): remove color-scheme injection to prevent form artifacts
commit
76a4000cb31efb34442e6f46912c0022be99955b
author
bnema · · 2025-12-29 18:49 UTC
committer
GitHub · · 2025-12-29 18:49 UTC
merge of
4c18f79 + 9307970 · diff against the first parent
signature
GPG UnverifiedB5690EEEBB952194 The key is unknown
3 files · +68 -108
M internal/bootstrap/webkit_stack.go +0 -3
@@ -148,9 +148,6 @@ func BuildWebKitStack(
148148 settings := webkit.NewSettingsManager(ctx, cfg)
149149 injector := webkit.NewContentInjector(themeManager.PrefersDark())
150150
151- // Set background color on injector for early CSS injection (prevents white flash)
152- injector.SetBackgroundColor(themeManager.GetCurrentPalette().Background)
153-
154151 prepareThemeUC := usecase.NewPrepareWebUIThemeUseCase(injector)
155152 themeCSSText := themeManager.GetWebUIThemeCSS()
156153 if err := prepareThemeUC.Execute(ctx, usecase.PrepareWebUIThemeInput{CSSVars: themeCSSText}); err != nil {
@@ -148,9 +148,6 @@ func BuildWebKitStack(
148 settings := webkit.NewSettingsManager(ctx, cfg)148 settings := webkit.NewSettingsManager(ctx, cfg)
149 injector := webkit.NewContentInjector(themeManager.PrefersDark())149 injector := webkit.NewContentInjector(themeManager.PrefersDark())
150 150
151- // Set background color on injector for early CSS injection (prevents white flash)
152- injector.SetBackgroundColor(themeManager.GetCurrentPalette().Background)
153-
154 prepareThemeUC := usecase.NewPrepareWebUIThemeUseCase(injector)151 prepareThemeUC := usecase.NewPrepareWebUIThemeUseCase(injector)
155 themeCSSText := themeManager.GetWebUIThemeCSS()152 themeCSSText := themeManager.GetWebUIThemeCSS()
156 if err := prepareThemeUC.Execute(ctx, usecase.PrepareWebUIThemeInput{CSSVars: themeCSSText}); err != nil {153 if err := prepareThemeUC.Execute(ctx, usecase.PrepareWebUIThemeInput{CSSVars: themeCSSText}); err != nil {
M internal/infrastructure/webkit/injector.go +37 -95
@@ -25,51 +25,23 @@ const themeCSSScript = `(function() {
2525 (document.head || document.documentElement).appendChild(style);
2626 })();`
2727
28-// earlyBackgroundScript injects a temporary background color to prevent white flash.
29-// It's injected at document start and removed after the page loads.
30-// The %s placeholder is replaced with the hex color (e.g., "#0a0a0b").
31-const earlyBackgroundScript = `(function() {
32- var style = document.createElement('style');
33- style.id = '__dumber_early_bg';
34- style.textContent = 'html,body{background-color:%s!important}';
35- document.documentElement.appendChild(style);
36-
37- // Remove the style after page content loads to avoid overriding page styles
38- function removeEarlyBg() {
39- var s = document.getElementById('__dumber_early_bg');
40- if (s) s.remove();
41- }
42-
43- // Try multiple events to ensure removal
44- if (document.readyState === 'complete') {
45- setTimeout(removeEarlyBg, 100);
46- } else {
47- window.addEventListener('DOMContentLoaded', function() {
48- setTimeout(removeEarlyBg, 100);
49- }, {once: true});
50- window.addEventListener('load', function() {
51- setTimeout(removeEarlyBg, 100);
52- }, {once: true});
53- }
54-})();`
55-
56-// darkModeScript patches window.matchMedia and sets theme class on <html>.
57-// It must be injected at document start, after __dumber_gtk_prefers_dark is set.
58-// This script:
59-// 1. Sets dark/light class on <html> for CSS-based theme detection
60-// 2. Injects color-scheme meta tag and CSS for browser UA style hints
61-// 3. Patches matchMedia to return the GTK theme preference for prefers-color-scheme queries
28+// internalDarkModeScript is injected ONLY on internal pages (dumb://*).
29+// It sets dark/light class on <html> for Tailwind CSS theming and patches matchMedia
30+// for JS-based dark mode detection in WebUI components.
31+//
32+// NOTE: This script is NOT injected on external pages. External pages receive
33+// dark mode preference via libadwaita's StyleManager, which WebKit respects
34+// for the native prefers-color-scheme media query.
6235 //
6336 // The matchMedia patch handles various query formats:
6437 // - (prefers-color-scheme: dark)
6538 // - (prefers-color-scheme:dark) -- no space
6639 // - screen and (prefers-color-scheme: dark)
67-// And provides proper addEventListener/removeEventListener stubs for compatibility.
68-const darkModeScript = `(function() {
40+const internalDarkModeScript = `(function() {
6941 var prefersDark = window.__dumber_gtk_prefers_dark || false;
7042 var originalMatchMedia = window.matchMedia.bind(window);
7143
72- // Apply dark/light class to document element for CSS theming
44+ // Apply dark/light class to document element for Tailwind CSS theming
7345 if (prefersDark) {
7446 document.documentElement.classList.add('dark');
7547 document.documentElement.classList.remove('light');
@@ -78,20 +50,7 @@ const darkModeScript = `(function() {
7850 document.documentElement.classList.remove('dark');
7951 }
8052
81- // Inject color-scheme meta tag
82- var meta = document.createElement('meta');
83- meta.name = 'color-scheme';
84- meta.content = prefersDark ? 'dark light' : 'light dark';
85- document.documentElement.appendChild(meta);
86-
87- // Inject root color-scheme style
88- var style = document.createElement('style');
89- style.setAttribute('data-dumber-color-scheme', '');
90- style.textContent = ':root{color-scheme:' + (prefersDark ? 'dark' : 'light') + ';}';
91- document.documentElement.appendChild(style);
92-
9353 // Helper: Check if query is a prefers-color-scheme query
94- // Normalizes by removing whitespace and lowercasing
9554 function isColorSchemeQuery(query, scheme) {
9655 if (typeof query !== 'string') return false;
9756 var normalized = query.replace(/\s+/g, '').toLowerCase();
@@ -108,7 +67,6 @@ const darkModeScript = `(function() {
10867 media: query,
10968 get onchange() { return onchangeHandler; },
11069 set onchange(fn) { onchangeHandler = fn; },
111- // Deprecated but still used by some sites
11270 addListener: function(cb) {
11371 if (typeof cb === 'function') listeners.push(cb);
11472 },
@@ -116,7 +74,6 @@ const darkModeScript = `(function() {
11674 var idx = listeners.indexOf(cb);
11775 if (idx !== -1) listeners.splice(idx, 1);
11876 },
119- // Modern API
12077 addEventListener: function(type, cb) {
12178 if (type === 'change' && typeof cb === 'function') {
12279 listeners.push(cb);
@@ -152,15 +109,18 @@ const darkModeScript = `(function() {
152109 };
153110 })();`
154111
112+// internalPageAllowList restricts script injection to internal dumb:// pages only.
113+var internalPageAllowList = []string{"dumb://*"}
114+
155115 // ContentInjector encapsulates script injection into WebViews.
156-// It injects minimal scripts for dark mode detection in web pages
157-// and theme CSS variables for internal pages (dumb://).
116+// It injects dark mode detection scripts for internal pages (dumb://)
117+// and theme CSS variables for WebUI styling.
118+// External pages receive dark mode preference via libadwaita's StyleManager.
158119 // Implements port.ContentInjector interface.
159120 type ContentInjector struct {
160121 prefersDark bool
161122 themeCSSVars string // CSS custom property declarations for WebUI
162123 findCSS string // CSS for find-in-page highlight styling
163- bgColor string // Background color hex for early injection (prevents white flash)
164124 }
165125
166126 // NewContentInjector creates a new injector instance.
@@ -200,18 +160,14 @@ func (ci *ContentInjector) SetPrefersDark(prefersDark bool) {
200160 ci.prefersDark = prefersDark
201161 }
202162
203-// SetBackgroundColor sets the background color for early CSS injection.
204-// This color is applied to html/body at document start to prevent white flash.
205-// The hex color should include the # prefix (e.g., "#0a0a0b").
206-func (ci *ContentInjector) SetBackgroundColor(hex string) {
207- ci.bgColor = hex
208-}
209-
210-// InjectScripts adds the minimal dark mode detection scripts to the given content manager.
211-// Only injects:
212-// - window.__dumber_gtk_prefers_dark flag
213-// - window.__dumber_webview_id (for debugging)
214-// - darkModeScript (patches matchMedia for prefers-color-scheme)
163+// InjectScripts adds scripts to the given content manager.
164+// For internal pages (dumb://*):
165+// - Injects dark mode detection (class on <html>, matchMedia patch)
166+// - Injects theme CSS variables for WebUI styling
167+//
168+// For external pages:
169+// - Only injects find highlight CSS
170+// - Dark mode is handled natively via libadwaita's StyleManager
215171 func (ci *ContentInjector) InjectScripts(ctx context.Context, ucm *webkit.UserContentManager, webviewID WebViewID) {
216172 log := logging.FromContext(ctx).With().Str("component", "content-injector").Logger()
217173
@@ -229,35 +185,20 @@ func (ci *ContentInjector) InjectScripts(ctx context.Context, ucm *webkit.UserCo
229185 log.Debug().Str("script", label).Msg("injected user script")
230186 }
231187
232- // 1. Inject early background color to prevent white flash (must be first!)
233- if ci.bgColor != "" {
234- earlyBgScript := fmt.Sprintf(earlyBackgroundScript, ci.bgColor)
235- addScript(
236- webkit.NewUserScript(
237- earlyBgScript,
238- webkit.UserContentInjectTopFrameValue,
239- webkit.UserScriptInjectAtDocumentStartValue,
240- nil,
241- nil,
242- ),
243- "early-background",
244- )
245- }
246-
247- // 2. Inject GTK dark mode preference (must be before dark mode handler)
188+ // 1. Inject GTK dark mode preference for internal pages only
248189 darkModePrefScript := fmt.Sprintf("window.__dumber_gtk_prefers_dark=%t;", ci.prefersDark)
249190 addScript(
250191 webkit.NewUserScript(
251192 darkModePrefScript,
252193 webkit.UserContentInjectTopFrameValue,
253194 webkit.UserScriptInjectAtDocumentStartValue,
254- nil,
195+ internalPageAllowList,
255196 nil,
256197 ),
257198 "gtk-dark-mode-pref",
258199 )
259200
260- // 3. Inject WebView ID for debugging
201+ // 2. Inject WebView ID for debugging (internal pages only)
261202 if webviewID != 0 {
262203 idScript := fmt.Sprintf("window.__dumber_webview_id=%d;", uint64(webviewID))
263204 addScript(
@@ -265,26 +206,27 @@ func (ci *ContentInjector) InjectScripts(ctx context.Context, ucm *webkit.UserCo
265206 idScript,
266207 webkit.UserContentInjectTopFrameValue,
267208 webkit.UserScriptInjectAtDocumentStartValue,
268- nil,
209+ internalPageAllowList,
269210 nil,
270211 ),
271212 "webview-id",
272213 )
273214 }
274215
275- // 4. Inject dark mode handler (patches matchMedia using __dumber_gtk_prefers_dark)
216+ // 3. Inject dark mode handler for internal pages only
217+ // This sets .dark/.light class on <html> and patches matchMedia for WebUI
276218 addScript(
277219 webkit.NewUserScript(
278- darkModeScript,
220+ internalDarkModeScript,
279221 webkit.UserContentInjectTopFrameValue,
280222 webkit.UserScriptInjectAtDocumentStartValue,
281- nil,
223+ internalPageAllowList,
282224 nil,
283225 ),
284- "dark-mode-handler",
226+ "internal-dark-mode-handler",
285227 )
286228
287- // 5. Inject theme CSS for internal pages (dumb://* only)
229+ // 4. Inject theme CSS for internal pages (dumb://* only)
288230 if ci.themeCSSVars != "" {
289231 // Escape for JS string literal
290232 escapedCSS := strings.ReplaceAll(ci.themeCSSVars, "\\", "\\\\")
@@ -296,15 +238,15 @@ func (ci *ContentInjector) InjectScripts(ctx context.Context, ucm *webkit.UserCo
296238 themeCSSInjectionScript,
297239 webkit.UserContentInjectTopFrameValue,
298240 webkit.UserScriptInjectAtDocumentEndValue,
299- nil, // Inject on all pages - allowlist patterns don't work for custom schemes
241+ internalPageAllowList,
300242 nil,
301243 ),
302244 "theme-css-vars",
303245 )
304- log.Debug().Msg("theme CSS vars injection configured")
246+ log.Debug().Msg("theme CSS vars injection configured for internal pages")
305247 }
306248
307- // 6. Inject find highlight CSS for all pages
249+ // 5. Inject find highlight CSS for all pages
308250 if ci.findCSS != "" {
309251 stylesheet := webkit.NewUserStyleSheet(
310252 ci.findCSS,
@@ -321,5 +263,5 @@ func (ci *ContentInjector) InjectScripts(ctx context.Context, ucm *webkit.UserCo
321263 }
322264 }
323265
324- log.Debug().Bool("prefers_dark", ci.prefersDark).Msg("minimal scripts injected")
266+ log.Debug().Bool("prefers_dark", ci.prefersDark).Msg("scripts injected")
325267 }
@@ -25,51 +25,23 @@ const themeCSSScript = `(function() {
25 (document.head || document.documentElement).appendChild(style);25 (document.head || document.documentElement).appendChild(style);
26 })();`26 })();`
27 27
28-// earlyBackgroundScript injects a temporary background color to prevent white flash.28+// internalDarkModeScript is injected ONLY on internal pages (dumb://*).
29-// It's injected at document start and removed after the page loads.29+// It sets dark/light class on <html> for Tailwind CSS theming and patches matchMedia
30-// The %s placeholder is replaced with the hex color (e.g., "#0a0a0b").30+// for JS-based dark mode detection in WebUI components.
31-const earlyBackgroundScript = `(function() {31+//
32- var style = document.createElement('style');32+// NOTE: This script is NOT injected on external pages. External pages receive
33- style.id = '__dumber_early_bg';33+// dark mode preference via libadwaita's StyleManager, which WebKit respects
34- style.textContent = 'html,body{background-color:%s!important}';34+// for the native prefers-color-scheme media query.
35- document.documentElement.appendChild(style);
36-
37- // Remove the style after page content loads to avoid overriding page styles
38- function removeEarlyBg() {
39- var s = document.getElementById('__dumber_early_bg');
40- if (s) s.remove();
41- }
42-
43- // Try multiple events to ensure removal
44- if (document.readyState === 'complete') {
45- setTimeout(removeEarlyBg, 100);
46- } else {
47- window.addEventListener('DOMContentLoaded', function() {
48- setTimeout(removeEarlyBg, 100);
49- }, {once: true});
50- window.addEventListener('load', function() {
51- setTimeout(removeEarlyBg, 100);
52- }, {once: true});
53- }
54-})();`
55-
56-// darkModeScript patches window.matchMedia and sets theme class on <html>.
57-// It must be injected at document start, after __dumber_gtk_prefers_dark is set.
58-// This script:
59-// 1. Sets dark/light class on <html> for CSS-based theme detection
60-// 2. Injects color-scheme meta tag and CSS for browser UA style hints
61-// 3. Patches matchMedia to return the GTK theme preference for prefers-color-scheme queries
62 //35 //
63 // The matchMedia patch handles various query formats:36 // The matchMedia patch handles various query formats:
64 // - (prefers-color-scheme: dark)37 // - (prefers-color-scheme: dark)
65 // - (prefers-color-scheme:dark) -- no space38 // - (prefers-color-scheme:dark) -- no space
66 // - screen and (prefers-color-scheme: dark)39 // - screen and (prefers-color-scheme: dark)
67-// And provides proper addEventListener/removeEventListener stubs for compatibility.40+const internalDarkModeScript = `(function() {
68-const darkModeScript = `(function() {
69 var prefersDark = window.__dumber_gtk_prefers_dark || false;41 var prefersDark = window.__dumber_gtk_prefers_dark || false;
70 var originalMatchMedia = window.matchMedia.bind(window);42 var originalMatchMedia = window.matchMedia.bind(window);
71 43
72- // Apply dark/light class to document element for CSS theming44+ // Apply dark/light class to document element for Tailwind CSS theming
73 if (prefersDark) {45 if (prefersDark) {
74 document.documentElement.classList.add('dark');46 document.documentElement.classList.add('dark');
75 document.documentElement.classList.remove('light');47 document.documentElement.classList.remove('light');
@@ -78,20 +50,7 @@ const darkModeScript = `(function() {
78 document.documentElement.classList.remove('dark');50 document.documentElement.classList.remove('dark');
79 }51 }
80 52
81- // Inject color-scheme meta tag
82- var meta = document.createElement('meta');
83- meta.name = 'color-scheme';
84- meta.content = prefersDark ? 'dark light' : 'light dark';
85- document.documentElement.appendChild(meta);
86-
87- // Inject root color-scheme style
88- var style = document.createElement('style');
89- style.setAttribute('data-dumber-color-scheme', '');
90- style.textContent = ':root{color-scheme:' + (prefersDark ? 'dark' : 'light') + ';}';
91- document.documentElement.appendChild(style);
92-
93 // Helper: Check if query is a prefers-color-scheme query53 // Helper: Check if query is a prefers-color-scheme query
94- // Normalizes by removing whitespace and lowercasing
95 function isColorSchemeQuery(query, scheme) {54 function isColorSchemeQuery(query, scheme) {
96 if (typeof query !== 'string') return false;55 if (typeof query !== 'string') return false;
97 var normalized = query.replace(/\s+/g, '').toLowerCase();56 var normalized = query.replace(/\s+/g, '').toLowerCase();
@@ -108,7 +67,6 @@ const darkModeScript = `(function() {
108 media: query,67 media: query,
109 get onchange() { return onchangeHandler; },68 get onchange() { return onchangeHandler; },
110 set onchange(fn) { onchangeHandler = fn; },69 set onchange(fn) { onchangeHandler = fn; },
111- // Deprecated but still used by some sites
112 addListener: function(cb) {70 addListener: function(cb) {
113 if (typeof cb === 'function') listeners.push(cb);71 if (typeof cb === 'function') listeners.push(cb);
114 },72 },
@@ -116,7 +74,6 @@ const darkModeScript = `(function() {
116 var idx = listeners.indexOf(cb);74 var idx = listeners.indexOf(cb);
117 if (idx !== -1) listeners.splice(idx, 1);75 if (idx !== -1) listeners.splice(idx, 1);
118 },76 },
119- // Modern API
120 addEventListener: function(type, cb) {77 addEventListener: function(type, cb) {
121 if (type === 'change' && typeof cb === 'function') {78 if (type === 'change' && typeof cb === 'function') {
122 listeners.push(cb);79 listeners.push(cb);
@@ -152,15 +109,18 @@ const darkModeScript = `(function() {
152 };109 };
153 })();`110 })();`
154 111
112+// internalPageAllowList restricts script injection to internal dumb:// pages only.
113+var internalPageAllowList = []string{"dumb://*"}
114+
155 // ContentInjector encapsulates script injection into WebViews.115 // ContentInjector encapsulates script injection into WebViews.
156-// It injects minimal scripts for dark mode detection in web pages116+// It injects dark mode detection scripts for internal pages (dumb://)
157-// and theme CSS variables for internal pages (dumb://).117+// and theme CSS variables for WebUI styling.
118+// External pages receive dark mode preference via libadwaita's StyleManager.
158 // Implements port.ContentInjector interface.119 // Implements port.ContentInjector interface.
159 type ContentInjector struct {120 type ContentInjector struct {
160 prefersDark bool121 prefersDark bool
161 themeCSSVars string // CSS custom property declarations for WebUI122 themeCSSVars string // CSS custom property declarations for WebUI
162 findCSS string // CSS for find-in-page highlight styling123 findCSS string // CSS for find-in-page highlight styling
163- bgColor string // Background color hex for early injection (prevents white flash)
164 }124 }
165 125
166 // NewContentInjector creates a new injector instance.126 // NewContentInjector creates a new injector instance.
@@ -200,18 +160,14 @@ func (ci *ContentInjector) SetPrefersDark(prefersDark bool) {
200 ci.prefersDark = prefersDark160 ci.prefersDark = prefersDark
201 }161 }
202 162
203-// SetBackgroundColor sets the background color for early CSS injection.163+// InjectScripts adds scripts to the given content manager.
204-// This color is applied to html/body at document start to prevent white flash.164+// For internal pages (dumb://*):
205-// The hex color should include the # prefix (e.g., "#0a0a0b").165+// - Injects dark mode detection (class on <html>, matchMedia patch)
206-func (ci *ContentInjector) SetBackgroundColor(hex string) {166+// - Injects theme CSS variables for WebUI styling
207- ci.bgColor = hex167+//
208-}168+// For external pages:
209-169+// - Only injects find highlight CSS
210-// InjectScripts adds the minimal dark mode detection scripts to the given content manager.170+// - Dark mode is handled natively via libadwaita's StyleManager
211-// Only injects:
212-// - window.__dumber_gtk_prefers_dark flag
213-// - window.__dumber_webview_id (for debugging)
214-// - darkModeScript (patches matchMedia for prefers-color-scheme)
215 func (ci *ContentInjector) InjectScripts(ctx context.Context, ucm *webkit.UserContentManager, webviewID WebViewID) {171 func (ci *ContentInjector) InjectScripts(ctx context.Context, ucm *webkit.UserContentManager, webviewID WebViewID) {
216 log := logging.FromContext(ctx).With().Str("component", "content-injector").Logger()172 log := logging.FromContext(ctx).With().Str("component", "content-injector").Logger()
217 173
@@ -229,35 +185,20 @@ func (ci *ContentInjector) InjectScripts(ctx context.Context, ucm *webkit.UserCo
229 log.Debug().Str("script", label).Msg("injected user script")185 log.Debug().Str("script", label).Msg("injected user script")
230 }186 }
231 187
232- // 1. Inject early background color to prevent white flash (must be first!)188+ // 1. Inject GTK dark mode preference for internal pages only
233- if ci.bgColor != "" {
234- earlyBgScript := fmt.Sprintf(earlyBackgroundScript, ci.bgColor)
235- addScript(
236- webkit.NewUserScript(
237- earlyBgScript,
238- webkit.UserContentInjectTopFrameValue,
239- webkit.UserScriptInjectAtDocumentStartValue,
240- nil,
241- nil,
242- ),
243- "early-background",
244- )
245- }
246-
247- // 2. Inject GTK dark mode preference (must be before dark mode handler)
248 darkModePrefScript := fmt.Sprintf("window.__dumber_gtk_prefers_dark=%t;", ci.prefersDark)189 darkModePrefScript := fmt.Sprintf("window.__dumber_gtk_prefers_dark=%t;", ci.prefersDark)
249 addScript(190 addScript(
250 webkit.NewUserScript(191 webkit.NewUserScript(
251 darkModePrefScript,192 darkModePrefScript,
252 webkit.UserContentInjectTopFrameValue,193 webkit.UserContentInjectTopFrameValue,
253 webkit.UserScriptInjectAtDocumentStartValue,194 webkit.UserScriptInjectAtDocumentStartValue,
254- nil,195+ internalPageAllowList,
255 nil,196 nil,
256 ),197 ),
257 "gtk-dark-mode-pref",198 "gtk-dark-mode-pref",
258 )199 )
259 200
260- // 3. Inject WebView ID for debugging201+ // 2. Inject WebView ID for debugging (internal pages only)
261 if webviewID != 0 {202 if webviewID != 0 {
262 idScript := fmt.Sprintf("window.__dumber_webview_id=%d;", uint64(webviewID))203 idScript := fmt.Sprintf("window.__dumber_webview_id=%d;", uint64(webviewID))
263 addScript(204 addScript(
@@ -265,26 +206,27 @@ func (ci *ContentInjector) InjectScripts(ctx context.Context, ucm *webkit.UserCo
265 idScript,206 idScript,
266 webkit.UserContentInjectTopFrameValue,207 webkit.UserContentInjectTopFrameValue,
267 webkit.UserScriptInjectAtDocumentStartValue,208 webkit.UserScriptInjectAtDocumentStartValue,
268- nil,209+ internalPageAllowList,
269 nil,210 nil,
270 ),211 ),
271 "webview-id",212 "webview-id",
272 )213 )
273 }214 }
274 215
275- // 4. Inject dark mode handler (patches matchMedia using __dumber_gtk_prefers_dark)216+ // 3. Inject dark mode handler for internal pages only
217+ // This sets .dark/.light class on <html> and patches matchMedia for WebUI
276 addScript(218 addScript(
277 webkit.NewUserScript(219 webkit.NewUserScript(
278- darkModeScript,220+ internalDarkModeScript,
279 webkit.UserContentInjectTopFrameValue,221 webkit.UserContentInjectTopFrameValue,
280 webkit.UserScriptInjectAtDocumentStartValue,222 webkit.UserScriptInjectAtDocumentStartValue,
281- nil,223+ internalPageAllowList,
282 nil,224 nil,
283 ),225 ),
284- "dark-mode-handler",226+ "internal-dark-mode-handler",
285 )227 )
286 228
287- // 5. Inject theme CSS for internal pages (dumb://* only)229+ // 4. Inject theme CSS for internal pages (dumb://* only)
288 if ci.themeCSSVars != "" {230 if ci.themeCSSVars != "" {
289 // Escape for JS string literal231 // Escape for JS string literal
290 escapedCSS := strings.ReplaceAll(ci.themeCSSVars, "\\", "\\\\")232 escapedCSS := strings.ReplaceAll(ci.themeCSSVars, "\\", "\\\\")
@@ -296,15 +238,15 @@ func (ci *ContentInjector) InjectScripts(ctx context.Context, ucm *webkit.UserCo
296 themeCSSInjectionScript,238 themeCSSInjectionScript,
297 webkit.UserContentInjectTopFrameValue,239 webkit.UserContentInjectTopFrameValue,
298 webkit.UserScriptInjectAtDocumentEndValue,240 webkit.UserScriptInjectAtDocumentEndValue,
299- nil, // Inject on all pages - allowlist patterns don't work for custom schemes241+ internalPageAllowList,
300 nil,242 nil,
301 ),243 ),
302 "theme-css-vars",244 "theme-css-vars",
303 )245 )
304- log.Debug().Msg("theme CSS vars injection configured")246+ log.Debug().Msg("theme CSS vars injection configured for internal pages")
305 }247 }
306 248
307- // 6. Inject find highlight CSS for all pages249+ // 5. Inject find highlight CSS for all pages
308 if ci.findCSS != "" {250 if ci.findCSS != "" {
309 stylesheet := webkit.NewUserStyleSheet(251 stylesheet := webkit.NewUserStyleSheet(
310 ci.findCSS,252 ci.findCSS,
@@ -321,5 +263,5 @@ func (ci *ContentInjector) InjectScripts(ctx context.Context, ucm *webkit.UserCo
321 }263 }
322 }264 }
323 265
324- log.Debug().Bool("prefers_dark", ci.prefersDark).Msg("minimal scripts injected")266+ log.Debug().Bool("prefers_dark", ci.prefersDark).Msg("scripts injected")
325 }267 }
M internal/ui/app.go +31 -10
@@ -27,6 +27,7 @@ import (
2727 "github.com/bnema/dumber/internal/ui/layout"
2828 "github.com/bnema/dumber/internal/ui/theme"
2929 "github.com/bnema/dumber/internal/ui/window"
30+ "github.com/jwijenbergh/puregotk/v4/adw"
3031 "github.com/jwijenbergh/puregotk/v4/gdk"
3132 "github.com/jwijenbergh/puregotk/v4/gio"
3233 "github.com/jwijenbergh/puregotk/v4/glib"
@@ -155,6 +156,10 @@ func (a *App) Run(ctx context.Context, args []string) int {
155156 log := logging.FromContext(ctx)
156157 log.Debug().Msg("creating GTK application")
157158
159+ // Initialize libadwaita once (required before using StyleManager).
160+ // This also initializes GTK implicitly.
161+ adw.Init()
162+
158163 // TODO: Use AppID once puregotk GC bug is fixed (nullable-string-gc-memory-corruption)
159164 a.gtkApp = gtk.NewApplication(nil, gio.GApplicationFlagsNoneValue)
160165 if a.gtkApp == nil {
@@ -217,23 +222,39 @@ func (a *App) onActivate(ctx context.Context) {
217222 func (a *App) applyGTKColorSchemePreference(ctx context.Context) {
218223 log := logging.FromContext(ctx)
219224
220- // Propagate app color scheme preference to GTK (and WebKit)
221- // so web content can correctly evaluate prefers-color-scheme.
222- settings := gtk.SettingsGetDefault()
223- if settings == nil {
224- return
225- }
226-
225+ // Get the config color scheme preference (default follows system theme)
227226 scheme := "default"
228227 if a.deps != nil && a.deps.Config != nil && a.deps.Config.Appearance.ColorScheme != "" {
229228 scheme = a.deps.Config.Appearance.ColorScheme
230229 }
231- prefersDark := theme.ResolveColorScheme(scheme)
232- settings.SetPropertyGtkApplicationPreferDarkTheme(prefersDark)
230+
231+ // Apply color scheme via libadwaita's StyleManager.
232+ // This properly communicates the preference to GTK and WebKit,
233+ // so web content can correctly evaluate prefers-color-scheme media queries.
234+ styleMgr := adw.StyleManagerGetDefault()
235+ if styleMgr == nil {
236+ log.Warn().Msg("failed to get adw.StyleManager")
237+ return
238+ }
239+
240+ var adwScheme adw.ColorScheme
241+ switch scheme {
242+ case "dark", "prefer-dark":
243+ adwScheme = adw.ColorSchemeForceDarkValue
244+ case "light", "prefer-light":
245+ adwScheme = adw.ColorSchemeForceLightValue
246+ default:
247+ // "default" or empty - follow system preference
248+ adwScheme = adw.ColorSchemeDefaultValue
249+ }
250+
251+ styleMgr.SetColorScheme(adwScheme)
252+ prefersDark := styleMgr.GetDark()
253+
233254 log.Debug().
234255 Str("scheme", scheme).
235256 Bool("prefers_dark", prefersDark).
236- Msg("set gtk-application-prefer-dark-theme")
257+ Msg("applied color scheme via adw.StyleManager")
237258 }
238259
239260 func (a *App) setupPoolBackgroundColor(ctx context.Context) {
@@ -27,6 +27,7 @@ import (
27 "github.com/bnema/dumber/internal/ui/layout"27 "github.com/bnema/dumber/internal/ui/layout"
28 "github.com/bnema/dumber/internal/ui/theme"28 "github.com/bnema/dumber/internal/ui/theme"
29 "github.com/bnema/dumber/internal/ui/window"29 "github.com/bnema/dumber/internal/ui/window"
30+ "github.com/jwijenbergh/puregotk/v4/adw"
30 "github.com/jwijenbergh/puregotk/v4/gdk"31 "github.com/jwijenbergh/puregotk/v4/gdk"
31 "github.com/jwijenbergh/puregotk/v4/gio"32 "github.com/jwijenbergh/puregotk/v4/gio"
32 "github.com/jwijenbergh/puregotk/v4/glib"33 "github.com/jwijenbergh/puregotk/v4/glib"
@@ -155,6 +156,10 @@ func (a *App) Run(ctx context.Context, args []string) int {
155 log := logging.FromContext(ctx)156 log := logging.FromContext(ctx)
156 log.Debug().Msg("creating GTK application")157 log.Debug().Msg("creating GTK application")
157 158
159+ // Initialize libadwaita once (required before using StyleManager).
160+ // This also initializes GTK implicitly.
161+ adw.Init()
162+
158 // TODO: Use AppID once puregotk GC bug is fixed (nullable-string-gc-memory-corruption)163 // TODO: Use AppID once puregotk GC bug is fixed (nullable-string-gc-memory-corruption)
159 a.gtkApp = gtk.NewApplication(nil, gio.GApplicationFlagsNoneValue)164 a.gtkApp = gtk.NewApplication(nil, gio.GApplicationFlagsNoneValue)
160 if a.gtkApp == nil {165 if a.gtkApp == nil {
@@ -217,23 +222,39 @@ func (a *App) onActivate(ctx context.Context) {
217 func (a *App) applyGTKColorSchemePreference(ctx context.Context) {222 func (a *App) applyGTKColorSchemePreference(ctx context.Context) {
218 log := logging.FromContext(ctx)223 log := logging.FromContext(ctx)
219 224
220- // Propagate app color scheme preference to GTK (and WebKit)225+ // Get the config color scheme preference (default follows system theme)
221- // so web content can correctly evaluate prefers-color-scheme.
222- settings := gtk.SettingsGetDefault()
223- if settings == nil {
224- return
225- }
226-
227 scheme := "default"226 scheme := "default"
228 if a.deps != nil && a.deps.Config != nil && a.deps.Config.Appearance.ColorScheme != "" {227 if a.deps != nil && a.deps.Config != nil && a.deps.Config.Appearance.ColorScheme != "" {
229 scheme = a.deps.Config.Appearance.ColorScheme228 scheme = a.deps.Config.Appearance.ColorScheme
230 }229 }
231- prefersDark := theme.ResolveColorScheme(scheme)230+
232- settings.SetPropertyGtkApplicationPreferDarkTheme(prefersDark)231+ // Apply color scheme via libadwaita's StyleManager.
232+ // This properly communicates the preference to GTK and WebKit,
233+ // so web content can correctly evaluate prefers-color-scheme media queries.
234+ styleMgr := adw.StyleManagerGetDefault()
235+ if styleMgr == nil {
236+ log.Warn().Msg("failed to get adw.StyleManager")
237+ return
238+ }
239+
240+ var adwScheme adw.ColorScheme
241+ switch scheme {
242+ case "dark", "prefer-dark":
243+ adwScheme = adw.ColorSchemeForceDarkValue
244+ case "light", "prefer-light":
245+ adwScheme = adw.ColorSchemeForceLightValue
246+ default:
247+ // "default" or empty - follow system preference
248+ adwScheme = adw.ColorSchemeDefaultValue
249+ }
250+
251+ styleMgr.SetColorScheme(adwScheme)
252+ prefersDark := styleMgr.GetDark()
253+
233 log.Debug().254 log.Debug().
234 Str("scheme", scheme).255 Str("scheme", scheme).
235 Bool("prefers_dark", prefersDark).256 Bool("prefers_dark", prefersDark).
236- Msg("set gtk-application-prefer-dark-theme")257+ Msg("applied color scheme via adw.StyleManager")
237 }258 }
238 259
239 func (a *App) setupPoolBackgroundColor(ctx context.Context) {260 func (a *App) setupPoolBackgroundColor(ctx context.Context) {

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