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