detached 38d2b5d

refactor(injector): inline dark mode handler script

commit
38d2b5d381ccfa03780c056b1ad2bbff3a5a845e
author
brice · · 2025-12-16 20:50 UTC
committer
brice · · 2025-12-16 21:47 UTC
parent
bf08946
signature
unsigned
1 file · +66 -21
M internal/infrastructure/webkit/injector.go +66 -21
@@ -4,7 +4,6 @@ import (
44 "context"
55 "fmt"
66
7- "github.com/bnema/dumber/assets"
87 "github.com/bnema/dumber/internal/logging"
98 "github.com/bnema/puregotk-webkit/webkit"
109 )
@@ -16,6 +15,56 @@ const (
1615 MessageHandlerName = "dumber"
1716 )
1817
18+// darkModeScript patches window.matchMedia to respect GTK's dark mode preference.
19+// It must be injected at document start, after __dumber_gtk_prefers_dark is set.
20+const darkModeScript = `(function() {
21+ const prefersDark = window.__dumber_gtk_prefers_dark || false;
22+ const originalMatchMedia = window.matchMedia.bind(window);
23+
24+ // Inject color-scheme meta tag
25+ const meta = document.createElement('meta');
26+ meta.name = 'color-scheme';
27+ meta.content = prefersDark ? 'dark light' : 'light dark';
28+ document.documentElement.appendChild(meta);
29+
30+ // Inject root color-scheme style
31+ const style = document.createElement('style');
32+ style.setAttribute('data-dumber-theme', '');
33+ style.textContent = ':root{color-scheme:' + (prefersDark ? 'dark' : 'light') + ';}';
34+ document.documentElement.appendChild(style);
35+
36+ // Patch matchMedia for prefers-color-scheme queries
37+ window.matchMedia = function(query) {
38+ if (typeof query === 'string') {
39+ if (query.includes('prefers-color-scheme: dark')) {
40+ return {
41+ matches: prefersDark,
42+ media: query,
43+ onchange: null,
44+ addListener: function() {},
45+ removeListener: function() {},
46+ addEventListener: function() {},
47+ removeEventListener: function() {},
48+ dispatchEvent: function() { return false; }
49+ };
50+ }
51+ if (query.includes('prefers-color-scheme: light')) {
52+ return {
53+ matches: !prefersDark,
54+ media: query,
55+ onchange: null,
56+ addListener: function() {},
57+ removeListener: function() {},
58+ addEventListener: function() {},
59+ removeEventListener: function() {},
60+ dispatchEvent: function() { return false; }
61+ };
62+ }
63+ }
64+ return originalMatchMedia(query);
65+ };
66+})();`
67+
1968 // ContentInjector encapsulates script injection into WebViews.
2069 // It injects minimal scripts for dark mode detection in web pages.
2170 type ContentInjector struct {
@@ -45,7 +94,7 @@ func (ci *ContentInjector) SetPrefersDark(prefersDark bool) {
4594 // Only injects:
4695 // - window.__dumber_gtk_prefers_dark flag
4796 // - window.__dumber_webview_id (for debugging)
48-// - color-scheme.js (patches matchMedia for prefers-color-scheme)
97+// - darkModeScript (patches matchMedia for prefers-color-scheme)
4998 func (ci *ContentInjector) InjectScripts(ctx context.Context, ucm *webkit.UserContentManager, webviewID WebViewID) {
5099 log := logging.FromContext(ctx).With().Str("component", "content-injector").Logger()
51100
@@ -63,17 +112,17 @@ func (ci *ContentInjector) InjectScripts(ctx context.Context, ucm *webkit.UserCo
63112 log.Debug().Str("script", label).Msg("injected user script")
64113 }
65114
66- // 1. Inject GTK dark mode preference (must be before color-scheme.js)
67- darkModeScript := fmt.Sprintf("window.__dumber_gtk_prefers_dark=%t;", ci.prefersDark)
115+ // 1. Inject GTK dark mode preference (must be before dark mode handler)
116+ darkModePrefScript := fmt.Sprintf("window.__dumber_gtk_prefers_dark=%t;", ci.prefersDark)
68117 addScript(
69118 webkit.NewUserScript(
70- darkModeScript,
119+ darkModePrefScript,
71120 webkit.UserContentInjectTopFrameValue,
72121 webkit.UserScriptInjectAtDocumentStartValue,
73122 nil,
74123 nil,
75124 ),
76- "gtk-dark-mode",
125+ "gtk-dark-mode-pref",
77126 )
78127
79128 // 2. Inject WebView ID for debugging
@@ -91,21 +140,17 @@ func (ci *ContentInjector) InjectScripts(ctx context.Context, ucm *webkit.UserCo
91140 )
92141 }
93142
94- // 3. Inject color-scheme handler (uses __dumber_gtk_prefers_dark to patch matchMedia)
95- if assets.ColorSchemeScript != "" {
96- addScript(
97- webkit.NewUserScript(
98- assets.ColorSchemeScript,
99- webkit.UserContentInjectTopFrameValue,
100- webkit.UserScriptInjectAtDocumentStartValue,
101- nil,
102- nil,
103- ),
104- "color-scheme",
105- )
106- } else {
107- log.Warn().Msg("ColorSchemeScript asset is empty; dark mode may not work correctly")
108- }
143+ // 3. Inject dark mode handler (patches matchMedia using __dumber_gtk_prefers_dark)
144+ addScript(
145+ webkit.NewUserScript(
146+ darkModeScript,
147+ webkit.UserContentInjectTopFrameValue,
148+ webkit.UserScriptInjectAtDocumentStartValue,
149+ nil,
150+ nil,
151+ ),
152+ "dark-mode-handler",
153+ )
109154
110155 log.Debug().Bool("prefers_dark", ci.prefersDark).Msg("minimal scripts injected")
111156 }
@@ -4,7 +4,6 @@ import (
4 "context"4 "context"
5 "fmt"5 "fmt"
6 6
7- "github.com/bnema/dumber/assets"
8 "github.com/bnema/dumber/internal/logging"7 "github.com/bnema/dumber/internal/logging"
9 "github.com/bnema/puregotk-webkit/webkit"8 "github.com/bnema/puregotk-webkit/webkit"
10 )9 )
@@ -16,6 +15,56 @@ const (
16 MessageHandlerName = "dumber"15 MessageHandlerName = "dumber"
17 )16 )
18 17
18+// darkModeScript patches window.matchMedia to respect GTK's dark mode preference.
19+// It must be injected at document start, after __dumber_gtk_prefers_dark is set.
20+const darkModeScript = `(function() {
21+ const prefersDark = window.__dumber_gtk_prefers_dark || false;
22+ const originalMatchMedia = window.matchMedia.bind(window);
23+
24+ // Inject color-scheme meta tag
25+ const meta = document.createElement('meta');
26+ meta.name = 'color-scheme';
27+ meta.content = prefersDark ? 'dark light' : 'light dark';
28+ document.documentElement.appendChild(meta);
29+
30+ // Inject root color-scheme style
31+ const style = document.createElement('style');
32+ style.setAttribute('data-dumber-theme', '');
33+ style.textContent = ':root{color-scheme:' + (prefersDark ? 'dark' : 'light') + ';}';
34+ document.documentElement.appendChild(style);
35+
36+ // Patch matchMedia for prefers-color-scheme queries
37+ window.matchMedia = function(query) {
38+ if (typeof query === 'string') {
39+ if (query.includes('prefers-color-scheme: dark')) {
40+ return {
41+ matches: prefersDark,
42+ media: query,
43+ onchange: null,
44+ addListener: function() {},
45+ removeListener: function() {},
46+ addEventListener: function() {},
47+ removeEventListener: function() {},
48+ dispatchEvent: function() { return false; }
49+ };
50+ }
51+ if (query.includes('prefers-color-scheme: light')) {
52+ return {
53+ matches: !prefersDark,
54+ media: query,
55+ onchange: null,
56+ addListener: function() {},
57+ removeListener: function() {},
58+ addEventListener: function() {},
59+ removeEventListener: function() {},
60+ dispatchEvent: function() { return false; }
61+ };
62+ }
63+ }
64+ return originalMatchMedia(query);
65+ };
66+})();`
67+
19 // ContentInjector encapsulates script injection into WebViews.68 // ContentInjector encapsulates script injection into WebViews.
20 // It injects minimal scripts for dark mode detection in web pages.69 // It injects minimal scripts for dark mode detection in web pages.
21 type ContentInjector struct {70 type ContentInjector struct {
@@ -45,7 +94,7 @@ func (ci *ContentInjector) SetPrefersDark(prefersDark bool) {
45 // Only injects:94 // Only injects:
46 // - window.__dumber_gtk_prefers_dark flag95 // - window.__dumber_gtk_prefers_dark flag
47 // - window.__dumber_webview_id (for debugging)96 // - window.__dumber_webview_id (for debugging)
48-// - color-scheme.js (patches matchMedia for prefers-color-scheme)97+// - darkModeScript (patches matchMedia for prefers-color-scheme)
49 func (ci *ContentInjector) InjectScripts(ctx context.Context, ucm *webkit.UserContentManager, webviewID WebViewID) {98 func (ci *ContentInjector) InjectScripts(ctx context.Context, ucm *webkit.UserContentManager, webviewID WebViewID) {
50 log := logging.FromContext(ctx).With().Str("component", "content-injector").Logger()99 log := logging.FromContext(ctx).With().Str("component", "content-injector").Logger()
51 100
@@ -63,17 +112,17 @@ func (ci *ContentInjector) InjectScripts(ctx context.Context, ucm *webkit.UserCo
63 log.Debug().Str("script", label).Msg("injected user script")112 log.Debug().Str("script", label).Msg("injected user script")
64 }113 }
65 114
66- // 1. Inject GTK dark mode preference (must be before color-scheme.js)115+ // 1. Inject GTK dark mode preference (must be before dark mode handler)
67- darkModeScript := fmt.Sprintf("window.__dumber_gtk_prefers_dark=%t;", ci.prefersDark)116+ darkModePrefScript := fmt.Sprintf("window.__dumber_gtk_prefers_dark=%t;", ci.prefersDark)
68 addScript(117 addScript(
69 webkit.NewUserScript(118 webkit.NewUserScript(
70- darkModeScript,119+ darkModePrefScript,
71 webkit.UserContentInjectTopFrameValue,120 webkit.UserContentInjectTopFrameValue,
72 webkit.UserScriptInjectAtDocumentStartValue,121 webkit.UserScriptInjectAtDocumentStartValue,
73 nil,122 nil,
74 nil,123 nil,
75 ),124 ),
76- "gtk-dark-mode",125+ "gtk-dark-mode-pref",
77 )126 )
78 127
79 // 2. Inject WebView ID for debugging128 // 2. Inject WebView ID for debugging
@@ -91,21 +140,17 @@ func (ci *ContentInjector) InjectScripts(ctx context.Context, ucm *webkit.UserCo
91 )140 )
92 }141 }
93 142
94- // 3. Inject color-scheme handler (uses __dumber_gtk_prefers_dark to patch matchMedia)143+ // 3. Inject dark mode handler (patches matchMedia using __dumber_gtk_prefers_dark)
95- if assets.ColorSchemeScript != "" {144+ addScript(
96- addScript(145+ webkit.NewUserScript(
97- webkit.NewUserScript(146+ darkModeScript,
98- assets.ColorSchemeScript,147+ webkit.UserContentInjectTopFrameValue,
99- webkit.UserContentInjectTopFrameValue,148+ webkit.UserScriptInjectAtDocumentStartValue,
100- webkit.UserScriptInjectAtDocumentStartValue,149+ nil,
101- nil,150+ nil,
102- nil,151+ ),
103- ),152+ "dark-mode-handler",
104- "color-scheme",153+ )
105- )
106- } else {
107- log.Warn().Msg("ColorSchemeScript asset is empty; dark mode may not work correctly")
108- }
109 154
110 log.Debug().Bool("prefers_dark", ci.prefersDark).Msg("minimal scripts injected")155 log.Debug().Bool("prefers_dark", ci.prefersDark).Msg("minimal scripts injected")
111 }156 }

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