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}