1//go:build webkit_cgo
2
3package webkit
4
5import (
6 "fmt"
7 "log"
8 "strings"
9 "sync"
10 "sync/atomic"
11 "unsafe"
12)
13
14/*
15#cgo pkg-config: gtk4 webkitgtk-6.0
16#include <gtk/gtk.h>
17#include <webkit/webkit.h>
18#include <stdint.h>
19#include <stdlib.h>
20
21extern void goHandleWindowShortcut(uintptr_t handle);
22
23static GtkShortcutController* create_global_shortcut_controller(GtkWindow* window) {
24 if (!window) {
25 return NULL;
26 }
27 GtkEventController* event_controller = gtk_shortcut_controller_new();
28 if (!event_controller) {
29 return NULL;
30 }
31 GtkShortcutController* controller = GTK_SHORTCUT_CONTROLLER(event_controller);
32 gtk_shortcut_controller_set_scope(controller, GTK_SHORTCUT_SCOPE_GLOBAL);
33 gtk_widget_add_controller(GTK_WIDGET(window), event_controller);
34 return controller;
35}
36
37static gboolean shortcut_callback_bridge(GtkWidget* widget, GVariant* args, gpointer user_data) {
38 (void)widget;
39 (void)args;
40 uintptr_t handle = (uintptr_t)user_data;
41 if (handle == 0) {
42 return FALSE;
43 }
44 goHandleWindowShortcut(handle);
45 return TRUE; // Indicate we handled the shortcut
46}
47
48static void add_shortcut_to_controller(GtkShortcutController* controller,
49 const char* trigger_str,
50 uintptr_t handle) {
51 if (!controller || !trigger_str || handle == 0) {
52 return;
53 }
54 GtkShortcutTrigger* trigger = gtk_shortcut_trigger_parse_string(trigger_str);
55 if (!trigger) {
56 return;
57 }
58 GtkShortcutAction* action = gtk_callback_action_new((GtkShortcutFunc)shortcut_callback_bridge,
59 (gpointer)handle,
60 NULL);
61 if (!action) {
62 g_object_unref(trigger);
63 return;
64 }
65 GtkShortcut* shortcut = gtk_shortcut_new(trigger, action);
66 gtk_shortcut_controller_add_shortcut(controller, shortcut);
67}
68*/
69import "C"
70
71var (
72 globalShortcutMu sync.Mutex
73 globalShortcutCounter uint64
74 globalShortcutCallbacks = make(map[uintptr]func())
75)
76
77func registerWindowShortcutCallback(cb func()) uintptr {
78 if cb == nil {
79 return 0
80 }
81 id := uintptr(atomic.AddUint64(&globalShortcutCounter, 1))
82 if id == 0 {
83 // Should never happen, but guard against wrapping to 0
84 id = uintptr(atomic.AddUint64(&globalShortcutCounter, 1))
85 }
86 globalShortcutMu.Lock()
87 globalShortcutCallbacks[id] = cb
88 globalShortcutMu.Unlock()
89 return id
90}
91
92func unregisterWindowShortcutCallback(id uintptr) {
93 if id == 0 {
94 return
95 }
96 globalShortcutMu.Lock()
97 delete(globalShortcutCallbacks, id)
98 globalShortcutMu.Unlock()
99}
100
101func invokeWindowShortcutCallback(id uintptr) {
102 if id == 0 {
103 return
104 }
105 globalShortcutMu.Lock()
106 cb := globalShortcutCallbacks[id]
107 globalShortcutMu.Unlock()
108 if cb == nil {
109 log.Printf("[shortcuts] no handler for shortcut id=%d", id)
110 return
111 }
112 cb()
113}
114
115//export goHandleWindowShortcut
116func goHandleWindowShortcut(handle C.uintptr_t) {
117 invokeWindowShortcutCallback(uintptr(handle))
118}
119
120// Go wrapper functions for CGO - these can be called from other Go files
121
122// CreateGlobalShortcutController creates a GTK4 global shortcut controller for a window
123func CreateGlobalShortcutController(window uintptr) uintptr {
124 if window == 0 {
125 return 0
126 }
127 controller := C.create_global_shortcut_controller((*C.GtkWindow)(unsafe.Pointer(window)))
128 return uintptr(unsafe.Pointer(controller))
129}
130
131// AddShortcutToController adds a shortcut to the controller with a callback handle
132func AddShortcutToController(controller uintptr, key string, handle uintptr) error {
133 if controller == 0 || key == "" || handle == 0 {
134 return ErrNotImplemented
135 }
136
137 keyStr := C.CString(key)
138 defer C.free(unsafe.Pointer(keyStr))
139
140 C.add_shortcut_to_controller(
141 (*C.GtkShortcutController)(unsafe.Pointer(controller)),
142 keyStr,
143 C.uintptr_t(handle),
144 )
145
146 return nil
147}
148
149// Global shortcuts registry to track all registered shortcuts for automatic blocking
150var (
151 globalShortcutsRegistry = make(map[string]bool)
152 shortcutsRegistryMu sync.RWMutex
153 // Map from shortcut key to callback handle for C-level triggering
154 globalShortcutHandles = make(map[string]uintptr)
155)
156
157// WebView tracking for dynamic protection refresh
158var (
159 activeWebViews = make(map[*WebView]bool)
160 activeWebViewsMu sync.RWMutex
161)
162
163// RegisterGlobalShortcut adds a shortcut to the global registry for automatic webpage blocking
164func RegisterGlobalShortcut(shortcut string) {
165 shortcutsRegistryMu.Lock()
166 // Normalize shortcut format (convert to lowercase, standardize modifiers)
167 normalized := normalizeShortcut(shortcut)
168
169 // Only proceed if this is a new shortcut registration
170 wasNew := !globalShortcutsRegistry[normalized]
171 if wasNew {
172 globalShortcutsRegistry[normalized] = true
173 log.Printf("[shortcuts] Registered global shortcut for blocking: %s -> %s", shortcut, normalized)
174 }
175 shortcutsRegistryMu.Unlock()
176
177 // If we added a new shortcut, refresh protection on all active WebViews
178 if wasNew {
179 RefreshGlobalShortcutsProtection()
180 }
181}
182
183// RegisterGlobalShortcutWithHandle adds a shortcut with its callback handle for C-level triggering
184func RegisterGlobalShortcutWithHandle(shortcut string, handle uintptr) {
185 // First register the shortcut for blocking
186 RegisterGlobalShortcut(shortcut)
187
188 // Then store the callback handle for C-level triggering
189 shortcutsRegistryMu.Lock()
190 normalized := normalizeShortcut(shortcut)
191 globalShortcutHandles[normalized] = handle
192 log.Printf("[shortcuts] Registered callback handle for shortcut: %s", shortcut)
193 shortcutsRegistryMu.Unlock()
194}
195
196// GetRegisteredShortcuts returns a copy of all registered shortcuts
197func GetRegisteredShortcuts() []string {
198 shortcutsRegistryMu.RLock()
199 defer shortcutsRegistryMu.RUnlock()
200
201 shortcuts := make([]string, 0, len(globalShortcutsRegistry))
202 for shortcut := range globalShortcutsRegistry {
203 shortcuts = append(shortcuts, shortcut)
204 }
205 return shortcuts
206}
207
208// RegisterActiveWebView adds a WebView to the active registry for dynamic protection updates
209func RegisterActiveWebView(w *WebView) {
210 if w == nil {
211 return
212 }
213 activeWebViewsMu.Lock()
214 defer activeWebViewsMu.Unlock()
215 activeWebViews[w] = true
216 // WebView registered for protection updates
217}
218
219// UnregisterActiveWebView removes a WebView from the active registry
220func UnregisterActiveWebView(w *WebView) {
221 if w == nil {
222 return
223 }
224 activeWebViewsMu.Lock()
225 defer activeWebViewsMu.Unlock()
226 delete(activeWebViews, w)
227 // WebView unregistered from protection updates
228}
229
230// RefreshGlobalShortcutsProtection updates all active WebViews with current shortcut protection
231func RefreshGlobalShortcutsProtection() {
232 activeWebViewsMu.RLock()
233 webViews := make([]*WebView, 0, len(activeWebViews))
234 for wv := range activeWebViews {
235 webViews = append(webViews, wv)
236 }
237 activeWebViewsMu.RUnlock()
238
239 if len(webViews) == 0 {
240 return
241 }
242
243 for _, wv := range webViews {
244 if wv != nil && !wv.destroyed {
245 // Re-enable protection which will inject updated shortcut list
246 if err := wv.EnableGlobalShortcutsProtection(); err != nil {
247 log.Printf("[shortcuts] Failed to refresh protection for WebView: %v", err)
248 }
249 }
250 }
251}
252
253// normalizeShortcut converts shortcuts to a standardized format for JavaScript blocking
254func normalizeShortcut(shortcut string) string {
255 // Convert to lowercase and replace common variations
256 normalized := strings.ToLower(shortcut)
257
258 // Standardize modifier keys
259 normalized = strings.ReplaceAll(normalized, "cmdorctrl+", "ctrl+")
260 normalized = strings.ReplaceAll(normalized, "cmd+", "ctrl+") // Treat cmd as ctrl for blocking
261 normalized = strings.ReplaceAll(normalized, "control+", "ctrl+")
262
263 // Standardize arrow key names (handle both formats)
264 normalized = strings.ReplaceAll(normalized, "left", "arrowleft")
265 normalized = strings.ReplaceAll(normalized, "right", "arrowright")
266 normalized = strings.ReplaceAll(normalized, "up", "arrowup")
267 normalized = strings.ReplaceAll(normalized, "down", "arrowdown")
268
269 // Fix double arrow prefixes that might occur from above replacements
270 normalized = strings.ReplaceAll(normalized, "arrowarrowleft", "arrowleft")
271 normalized = strings.ReplaceAll(normalized, "arrowarrowright", "arrowright")
272 normalized = strings.ReplaceAll(normalized, "arrowarrowup", "arrowup")
273 normalized = strings.ReplaceAll(normalized, "arrowarrowdown", "arrowdown")
274
275 // Other key normalizations
276 normalized = strings.ReplaceAll(normalized, "equal", "equal") // For Ctrl+=
277 normalized = strings.ReplaceAll(normalized, "plus", "equal") // Normalize + to =
278
279 return normalized
280}
281
282// Keyboard event blocking functionality for global shortcuts protection
283
284// keyboardBlockerScript holds reference to the main-world keyboard blocking script
285var keyboardBlockerScript *C.WebKitUserScript = nil
286
287// EnableGlobalShortcutsProtection injects a script to block all registered global shortcuts from reaching webpages
288func (w *WebView) EnableGlobalShortcutsProtection() error {
289 if w == nil || w.destroyed || w.native == nil || w.native.ucm == nil {
290 return ErrWebViewNotInitialized
291 }
292
293 // Get all registered shortcuts
294 shortcuts := GetRegisteredShortcuts()
295 if len(shortcuts) == 0 {
296 log.Printf("[shortcuts] No global shortcuts registered, skipping protection")
297 return nil
298 }
299
300 // Generate JavaScript array of shortcuts to block
301 shortcutsJS := "["
302 for i, shortcut := range shortcuts {
303 if i > 0 {
304 shortcutsJS += ","
305 }
306 shortcutsJS += `"` + shortcut + `"`
307 }
308 shortcutsJS += "]"
309
310 // Create a version-based blocking script that can be updated dynamically
311 protectionVersion := len(shortcuts) // Use shortcuts count as version
312 globalProtectionScript := `
313 (() => {
314 const newVersion = ` + fmt.Sprintf("%d", protectionVersion) + `;
315 const currentVersion = window.__dumber_shortcuts_protection_version || 0;
316
317 // Only update if this is a newer version or first time
318 if (newVersion > currentVersion) {
319 // Clean up old protection if it exists
320 if (window.__dumber_cleanup_global_shortcuts) {
321 window.__dumber_cleanup_global_shortcuts();
322 }
323
324 window.__dumber_shortcuts_protection_version = newVersion;
325
326 // List of global shortcuts to block from reaching the webpage
327 const globalShortcuts = ` + shortcutsJS + `;
328 console.log('[dumber] Protecting global shortcuts (v' + newVersion + '):', globalShortcuts);
329
330 // Function to check if a key event matches any global shortcut
331 const isGlobalShortcut = (e) => {
332 const key = e.key.toLowerCase();
333 const code = e.code.toLowerCase();
334
335 // Build shortcut string (similar to GTK format)
336 let shortcut = '';
337 if (e.ctrlKey) shortcut += 'ctrl+';
338 if (e.altKey) shortcut += 'alt+';
339 if (e.shiftKey) shortcut += 'shift+';
340 if (e.metaKey) shortcut += 'meta+';
341
342 // Try both key and code for matching
343 const keyShortcut = shortcut + key;
344 const codeShortcut = shortcut + code;
345
346 // Special case mappings for common shortcuts
347 const specialMappings = {
348 'ctrl+equal': ['ctrl+=', 'ctrl+plus'],
349 'ctrl+minus': ['ctrl+-'],
350 'f12': ['f12'],
351 'ctrl+r': ['ctrl+r'],
352 'ctrl+shift+r': ['ctrl+shift+r'],
353 'f5': ['f5'],
354 // Alt+Arrow key mappings (handle both key and code variations)
355 'alt+arrowleft': ['alt+left', 'alt+arrowleft'],
356 'alt+arrowright': ['alt+right', 'alt+arrowright'],
357 'alt+arrowup': ['alt+up', 'alt+arrowup'],
358 'alt+arrowdown': ['alt+down', 'alt+arrowdown']
359 };
360
361 // Check direct matches
362 if (globalShortcuts.includes(keyShortcut) || globalShortcuts.includes(codeShortcut)) {
363 return true;
364 }
365
366 // Check special mappings
367 for (const [canonical, variants] of Object.entries(specialMappings)) {
368 if (variants.includes(keyShortcut) || variants.includes(codeShortcut)) {
369 if (globalShortcuts.includes(canonical)) {
370 return true;
371 }
372 }
373 }
374
375 return false;
376 };
377
378 // Block only global shortcuts at capture phase
379 const blockGlobalShortcut = (e) => {
380 if (isGlobalShortcut(e)) {
381 console.log('[dumber] Blocked global shortcut from reaching page:', e.key, e.ctrlKey, e.altKey, e.shiftKey);
382 e.stopImmediatePropagation();
383 e.preventDefault();
384 e.stopPropagation();
385 return false;
386 }
387 // Let other key events through normally
388 };
389
390 // Install listeners for keyboard events
391 document.addEventListener('keydown', blockGlobalShortcut, true);
392 document.addEventListener('keyup', blockGlobalShortcut, true);
393 document.addEventListener('keypress', blockGlobalShortcut, true);
394
395 // Store cleanup function
396 window.__dumber_cleanup_global_shortcuts = () => {
397 document.removeEventListener('keydown', blockGlobalShortcut, true);
398 document.removeEventListener('keyup', blockGlobalShortcut, true);
399 document.removeEventListener('keypress', blockGlobalShortcut, true);
400 delete window.__dumber_shortcuts_protection_version;
401 delete window.__dumber_cleanup_global_shortcuts;
402 console.log('[dumber] Global shortcuts protection disabled');
403 };
404
405 console.log('[dumber] Global shortcuts protection enabled for', globalShortcuts.length, 'shortcuts (v' + newVersion + ')');
406 } else {
407 console.log('[dumber] Global shortcuts protection already up to date (v' + currentVersion + ')');
408 }
409 })();
410 `
411
412 // Inject the script into the main world at document start
413 cScript := C.CString(globalProtectionScript)
414 defer C.free(unsafe.Pointer(cScript))
415
416 script := C.webkit_user_script_new(
417 (*C.gchar)(cScript),
418 C.WEBKIT_USER_CONTENT_INJECT_TOP_FRAME,
419 C.WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START,
420 nil, nil) // nil = main world
421
422 if script == nil {
423 return ErrNotImplemented
424 }
425
426 C.webkit_user_content_manager_add_script(w.native.ucm, script)
427 C.webkit_user_script_unref(script) // Clean up reference
428
429 log.Printf("[shortcuts] Global shortcuts protection enabled for %d shortcuts", len(shortcuts))
430 return nil
431}
432
433// DisableGlobalShortcutsProtection removes the global shortcuts protection
434func (w *WebView) DisableGlobalShortcutsProtection() error {
435 if w == nil || w.destroyed || w.native == nil {
436 return ErrWebViewNotInitialized
437 }
438
439 // Execute cleanup script
440 cleanupScript := `
441 if (window.__dumber_cleanup_global_shortcuts) {
442 window.__dumber_cleanup_global_shortcuts();
443 }
444 `
445
446 return w.InjectScript(cleanupScript)
447}
448
449// EnablePageKeyboardBlocking injects a script into the main world to block all keyboard events
450// This prevents page JavaScript from receiving keyboard events while omnibox is active
451func (w *WebView) EnablePageKeyboardBlocking() error {
452 if w == nil || w.destroyed || w.native == nil || w.native.ucm == nil {
453 return ErrWebViewNotInitialized
454 }
455
456 // Remove any existing blocker script first
457 w.DisablePageKeyboardBlocking()
458
459 // Script to block all keyboard and related events in the main world
460 blockingScript := `
461 (() => {
462 // Store original state to restore later
463 if (!window.__dumber_event_blocker_installed) {
464 window.__dumber_event_blocker_installed = true;
465
466 // List of events to block
467 const eventTypes = [
468 'keydown', 'keyup', 'keypress',
469 'input', 'beforeinput',
470 'compositionstart', 'compositionupdate', 'compositionend'
471 ];
472
473 // Block all keyboard-related events at capture phase
474 const blockEvent = (e) => {
475 e.stopImmediatePropagation();
476 e.preventDefault();
477 e.stopPropagation();
478 return false;
479 };
480
481 // Install capture-phase event blockers
482 eventTypes.forEach(type => {
483 document.addEventListener(type, blockEvent, true);
484 });
485
486 // Store cleanup function
487 window.__dumber_cleanup_event_blocker = () => {
488 eventTypes.forEach(type => {
489 document.removeEventListener(type, blockEvent, true);
490 });
491 delete window.__dumber_event_blocker_installed;
492 delete window.__dumber_cleanup_event_blocker;
493 };
494
495 console.log('[dumber] Page keyboard event blocking enabled');
496 }
497 })();
498 `
499
500 cScript := C.CString(blockingScript)
501 defer C.free(unsafe.Pointer(cScript))
502
503 // Inject into main world (nil world parameter = main world)
504 keyboardBlockerScript = C.webkit_user_script_new(
505 (*C.gchar)(cScript),
506 C.WEBKIT_USER_CONTENT_INJECT_TOP_FRAME,
507 C.WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START,
508 nil, nil) // nil = main world
509
510 if keyboardBlockerScript == nil {
511 return ErrNotImplemented
512 }
513
514 C.webkit_user_content_manager_add_script(w.native.ucm, keyboardBlockerScript)
515 log.Printf("[webkit] Page keyboard event blocking enabled")
516 return nil
517}
518
519// DisablePageKeyboardBlocking removes the keyboard blocking script and restores normal page behavior
520func (w *WebView) DisablePageKeyboardBlocking() error {
521 if w == nil || w.destroyed || w.native == nil || w.native.ucm == nil {
522 return ErrWebViewNotInitialized
523 }
524
525 // Execute cleanup script to remove event listeners
526 cleanupScript := `
527 if (window.__dumber_cleanup_event_blocker) {
528 window.__dumber_cleanup_event_blocker();
529 console.log('[dumber] Page keyboard event blocking disabled');
530 }
531 `
532 // Double-check WebView is still valid before script injection
533 if !w.IsDestroyed() {
534 w.InjectScript(cleanupScript)
535 }
536
537 // Remove the user script if it exists
538 if keyboardBlockerScript != nil {
539 C.webkit_user_content_manager_remove_script(w.native.ucm, keyboardBlockerScript)
540 C.webkit_user_script_unref(keyboardBlockerScript)
541 keyboardBlockerScript = nil
542 log.Printf("[webkit] Keyboard blocker script removed")
543 }
544
545 return nil
546}