fix(shortcuts): prevent window shortcuts from being blocked by webpage protection
- Remove window shortcuts from global blocking registry to allow GTK handling - Window shortcuts (ctrl+l, ctrl+f, F12, etc.) now properly bubble up to GTK - Only pane-level shortcuts are blocked from reaching webpages - Fixes omnibox not working in new panes when global shortcuts protection is active - Add dynamic global shortcuts protection system for pane shortcuts only
- commit
- d7ec53a416df205aae672f7229a91d1da06a5fb8
- author
- bamen · · 2025-09-23 05:08 UTC
- parent
- d7fd740
- signature
- unsigned
4 files · +220 -9
M pkg/webkit/keyboard_cgo.go +4 -0
| @@ -133,6 +133,10 @@ func (w *WebView) RegisterKeyboardShortcut(accel string, callback func()) error | |||
| 133 | 133 | } | |
| 134 | 134 | viewShortcuts[w.id][accel] = callback | |
| 135 | 135 | regMu.Unlock() | |
| 136 | + | ||
| 137 | + | // Register in global shortcuts registry for automatic webpage blocking | |
| 138 | + | RegisterGlobalShortcut(accel) | |
| 139 | + | ||
| 136 | 140 | return nil | |
| 137 | 141 | } | |
| 138 | 142 | ||
| @@ -133,6 +133,10 @@ func (w *WebView) RegisterKeyboardShortcut(accel string, callback func()) error | |||||
| 133 | } | 133 | } | ||
| 134 | viewShortcuts[w.id][accel] = callback | 134 | viewShortcuts[w.id][accel] = callback | ||
| 135 | regMu.Unlock() | 135 | regMu.Unlock() | ||
| 136 | + | ||||
| 137 | + | // Register in global shortcuts registry for automatic webpage blocking | |||
| 138 | + | RegisterGlobalShortcut(accel) | |||
| 139 | + | ||||
| 136 | return nil | 140 | return nil | ||
| 137 | } | 141 | } | ||
| 138 | 142 | ||||
M pkg/webkit/shortcuts_cgo.go +208 -8
| @@ -2,6 +2,14 @@ | |||
| 2 | 2 | ||
| 3 | 3 | package webkit | |
| 4 | 4 | ||
| 5 | + | import ( | |
| 6 | + | "log" | |
| 7 | + | "strings" | |
| 8 | + | "sync" | |
| 9 | + | "sync/atomic" | |
| 10 | + | "unsafe" | |
| 11 | + | ) | |
| 12 | + | ||
| 5 | 13 | /* | |
| 6 | 14 | #cgo pkg-config: gtk4 webkitgtk-6.0 | |
| 7 | 15 | #include <gtk/gtk.h> | |
| @@ -59,13 +67,6 @@ static void add_shortcut_to_controller(GtkShortcutController* controller, | |||
| 59 | 67 | */ | |
| 60 | 68 | import "C" | |
| 61 | 69 | ||
| 62 | - | import ( | |
| 63 | - | "log" | |
| 64 | - | "sync" | |
| 65 | - | "sync/atomic" | |
| 66 | - | "unsafe" | |
| 67 | - | ) | |
| 68 | - | ||
| 69 | 70 | var ( | |
| 70 | 71 | globalShortcutMu sync.Mutex | |
| 71 | 72 | globalShortcutCounter uint64 | |
| @@ -144,11 +145,210 @@ func AddShortcutToController(controller uintptr, key string, handle uintptr) err | |||
| 144 | 145 | return nil | |
| 145 | 146 | } | |
| 146 | 147 | ||
| 147 | - | // Keyboard event blocking functionality for omnibox isolation | |
| 148 | + | // Global shortcuts registry to track all registered shortcuts for automatic blocking | |
| 149 | + | var ( | |
| 150 | + | globalShortcutsRegistry = make(map[string]bool) | |
| 151 | + | shortcutsRegistryMu sync.RWMutex | |
| 152 | + | ) | |
| 153 | + | ||
| 154 | + | // RegisterGlobalShortcut adds a shortcut to the global registry for automatic webpage blocking | |
| 155 | + | func RegisterGlobalShortcut(shortcut string) { | |
| 156 | + | shortcutsRegistryMu.Lock() | |
| 157 | + | defer shortcutsRegistryMu.Unlock() | |
| 158 | + | ||
| 159 | + | // Normalize shortcut format (convert to lowercase, standardize modifiers) | |
| 160 | + | normalized := normalizeShortcut(shortcut) | |
| 161 | + | ||
| 162 | + | // Only log if this is a new shortcut registration | |
| 163 | + | if !globalShortcutsRegistry[normalized] { | |
| 164 | + | globalShortcutsRegistry[normalized] = true | |
| 165 | + | log.Printf("[shortcuts] Registered global shortcut for blocking: %s -> %s", shortcut, normalized) | |
| 166 | + | } | |
| 167 | + | } | |
| 168 | + | ||
| 169 | + | // GetRegisteredShortcuts returns a copy of all registered shortcuts | |
| 170 | + | func GetRegisteredShortcuts() []string { | |
| 171 | + | shortcutsRegistryMu.RLock() | |
| 172 | + | defer shortcutsRegistryMu.RUnlock() | |
| 173 | + | ||
| 174 | + | shortcuts := make([]string, 0, len(globalShortcutsRegistry)) | |
| 175 | + | for shortcut := range globalShortcutsRegistry { | |
| 176 | + | shortcuts = append(shortcuts, shortcut) | |
| 177 | + | } | |
| 178 | + | return shortcuts | |
| 179 | + | } | |
| 180 | + | ||
| 181 | + | // normalizeShortcut converts shortcuts to a standardized format for JavaScript blocking | |
| 182 | + | func normalizeShortcut(shortcut string) string { | |
| 183 | + | // Convert to lowercase and replace common variations | |
| 184 | + | normalized := strings.ToLower(shortcut) | |
| 185 | + | ||
| 186 | + | // Standardize modifier keys | |
| 187 | + | normalized = strings.ReplaceAll(normalized, "cmdorctrl+", "ctrl+") | |
| 188 | + | normalized = strings.ReplaceAll(normalized, "cmd+", "ctrl+") // Treat cmd as ctrl for blocking | |
| 189 | + | normalized = strings.ReplaceAll(normalized, "control+", "ctrl+") | |
| 190 | + | ||
| 191 | + | // Standardize key names | |
| 192 | + | normalized = strings.ReplaceAll(normalized, "arrowleft", "arrowleft") | |
| 193 | + | normalized = strings.ReplaceAll(normalized, "arrowright", "arrowright") | |
| 194 | + | normalized = strings.ReplaceAll(normalized, "arrowup", "arrowup") | |
| 195 | + | normalized = strings.ReplaceAll(normalized, "arrowdown", "arrowdown") | |
| 196 | + | normalized = strings.ReplaceAll(normalized, "equal", "equal") // For Ctrl+= | |
| 197 | + | normalized = strings.ReplaceAll(normalized, "plus", "equal") // Normalize + to = | |
| 198 | + | ||
| 199 | + | return normalized | |
| 200 | + | } | |
| 201 | + | ||
| 202 | + | // Keyboard event blocking functionality for global shortcuts protection | |
| 148 | 203 | ||
| 149 | 204 | // keyboardBlockerScript holds reference to the main-world keyboard blocking script | |
| 150 | 205 | var keyboardBlockerScript *C.WebKitUserScript = nil | |
| 151 | 206 | ||
| 207 | + | // EnableGlobalShortcutsProtection injects a script to block all registered global shortcuts from reaching webpages | |
| 208 | + | func (w *WebView) EnableGlobalShortcutsProtection() error { | |
| 209 | + | if w == nil || w.destroyed || w.native == nil || w.native.ucm == nil { | |
| 210 | + | return ErrWebViewNotInitialized | |
| 211 | + | } | |
| 212 | + | ||
| 213 | + | // Get all registered shortcuts | |
| 214 | + | shortcuts := GetRegisteredShortcuts() | |
| 215 | + | if len(shortcuts) == 0 { | |
| 216 | + | log.Printf("[shortcuts] No global shortcuts registered, skipping protection") | |
| 217 | + | return nil | |
| 218 | + | } | |
| 219 | + | ||
| 220 | + | // Generate JavaScript array of shortcuts to block | |
| 221 | + | shortcutsJS := "[" | |
| 222 | + | for i, shortcut := range shortcuts { | |
| 223 | + | if i > 0 { | |
| 224 | + | shortcutsJS += "," | |
| 225 | + | } | |
| 226 | + | shortcutsJS += `"` + shortcut + `"` | |
| 227 | + | } | |
| 228 | + | shortcutsJS += "]" | |
| 229 | + | ||
| 230 | + | // Create the blocking script that only blocks registered global shortcuts | |
| 231 | + | globalProtectionScript := ` | |
| 232 | + | (() => { | |
| 233 | + | if (!window.__dumber_global_shortcuts_protection) { | |
| 234 | + | window.__dumber_global_shortcuts_protection = true; | |
| 235 | + | ||
| 236 | + | // List of global shortcuts to block from reaching the webpage | |
| 237 | + | const globalShortcuts = ` + shortcutsJS + `; | |
| 238 | + | console.log('[dumber] Protecting global shortcuts:', globalShortcuts); | |
| 239 | + | ||
| 240 | + | // Function to check if a key event matches any global shortcut | |
| 241 | + | const isGlobalShortcut = (e) => { | |
| 242 | + | const key = e.key.toLowerCase(); | |
| 243 | + | const code = e.code.toLowerCase(); | |
| 244 | + | ||
| 245 | + | // Build shortcut string (similar to GTK format) | |
| 246 | + | let shortcut = ''; | |
| 247 | + | if (e.ctrlKey) shortcut += 'ctrl+'; | |
| 248 | + | if (e.altKey) shortcut += 'alt+'; | |
| 249 | + | if (e.shiftKey) shortcut += 'shift+'; | |
| 250 | + | if (e.metaKey) shortcut += 'meta+'; | |
| 251 | + | ||
| 252 | + | // Try both key and code for matching | |
| 253 | + | const keyShortcut = shortcut + key; | |
| 254 | + | const codeShortcut = shortcut + code; | |
| 255 | + | ||
| 256 | + | // Special case mappings for common shortcuts | |
| 257 | + | const specialMappings = { | |
| 258 | + | 'ctrl+equal': ['ctrl+=', 'ctrl+plus'], | |
| 259 | + | 'ctrl+minus': ['ctrl+-'], | |
| 260 | + | 'f12': ['f12'], | |
| 261 | + | 'ctrl+r': ['ctrl+r'], | |
| 262 | + | 'ctrl+shift+r': ['ctrl+shift+r'], | |
| 263 | + | 'f5': ['f5'] | |
| 264 | + | }; | |
| 265 | + | ||
| 266 | + | // Check direct matches | |
| 267 | + | if (globalShortcuts.includes(keyShortcut) || globalShortcuts.includes(codeShortcut)) { | |
| 268 | + | return true; | |
| 269 | + | } | |
| 270 | + | ||
| 271 | + | // Check special mappings | |
| 272 | + | for (const [canonical, variants] of Object.entries(specialMappings)) { | |
| 273 | + | if (variants.includes(keyShortcut) || variants.includes(codeShortcut)) { | |
| 274 | + | if (globalShortcuts.includes(canonical)) { | |
| 275 | + | return true; | |
| 276 | + | } | |
| 277 | + | } | |
| 278 | + | } | |
| 279 | + | ||
| 280 | + | return false; | |
| 281 | + | }; | |
| 282 | + | ||
| 283 | + | // Block only global shortcuts at capture phase | |
| 284 | + | const blockGlobalShortcut = (e) => { | |
| 285 | + | if (isGlobalShortcut(e)) { | |
| 286 | + | console.log('[dumber] Blocked global shortcut from reaching page:', e.key, e.ctrlKey, e.altKey, e.shiftKey); | |
| 287 | + | e.stopImmediatePropagation(); | |
| 288 | + | e.preventDefault(); | |
| 289 | + | e.stopPropagation(); | |
| 290 | + | return false; | |
| 291 | + | } | |
| 292 | + | // Let other key events through normally | |
| 293 | + | }; | |
| 294 | + | ||
| 295 | + | // Install listeners for keyboard events | |
| 296 | + | document.addEventListener('keydown', blockGlobalShortcut, true); | |
| 297 | + | document.addEventListener('keyup', blockGlobalShortcut, true); | |
| 298 | + | document.addEventListener('keypress', blockGlobalShortcut, true); | |
| 299 | + | ||
| 300 | + | // Store cleanup function | |
| 301 | + | window.__dumber_cleanup_global_shortcuts = () => { | |
| 302 | + | document.removeEventListener('keydown', blockGlobalShortcut, true); | |
| 303 | + | document.removeEventListener('keyup', blockGlobalShortcut, true); | |
| 304 | + | document.removeEventListener('keypress', blockGlobalShortcut, true); | |
| 305 | + | delete window.__dumber_global_shortcuts_protection; | |
| 306 | + | delete window.__dumber_cleanup_global_shortcuts; | |
| 307 | + | console.log('[dumber] Global shortcuts protection disabled'); | |
| 308 | + | }; | |
| 309 | + | ||
| 310 | + | console.log('[dumber] Global shortcuts protection enabled for', globalShortcuts.length, 'shortcuts'); | |
| 311 | + | } | |
| 312 | + | })(); | |
| 313 | + | ` | |
| 314 | + | ||
| 315 | + | // Inject the script into the main world at document start | |
| 316 | + | cScript := C.CString(globalProtectionScript) | |
| 317 | + | defer C.free(unsafe.Pointer(cScript)) | |
| 318 | + | ||
| 319 | + | script := C.webkit_user_script_new( | |
| 320 | + | (*C.gchar)(cScript), | |
| 321 | + | C.WEBKIT_USER_CONTENT_INJECT_TOP_FRAME, | |
| 322 | + | C.WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, | |
| 323 | + | nil, nil) // nil = main world | |
| 324 | + | ||
| 325 | + | if script == nil { | |
| 326 | + | return ErrNotImplemented | |
| 327 | + | } | |
| 328 | + | ||
| 329 | + | C.webkit_user_content_manager_add_script(w.native.ucm, script) | |
| 330 | + | C.webkit_user_script_unref(script) // Clean up reference | |
| 331 | + | ||
| 332 | + | log.Printf("[shortcuts] Global shortcuts protection enabled for %d shortcuts", len(shortcuts)) | |
| 333 | + | return nil | |
| 334 | + | } | |
| 335 | + | ||
| 336 | + | // DisableGlobalShortcutsProtection removes the global shortcuts protection | |
| 337 | + | func (w *WebView) DisableGlobalShortcutsProtection() error { | |
| 338 | + | if w == nil || w.destroyed || w.native == nil { | |
| 339 | + | return ErrWebViewNotInitialized | |
| 340 | + | } | |
| 341 | + | ||
| 342 | + | // Execute cleanup script | |
| 343 | + | cleanupScript := ` | |
| 344 | + | if (window.__dumber_cleanup_global_shortcuts) { | |
| 345 | + | window.__dumber_cleanup_global_shortcuts(); | |
| 346 | + | } | |
| 347 | + | ` | |
| 348 | + | ||
| 349 | + | return w.InjectScript(cleanupScript) | |
| 350 | + | } | |
| 351 | + | ||
| 152 | 352 | // EnablePageKeyboardBlocking injects a script into the main world to block all keyboard events | |
| 153 | 353 | // This prevents page JavaScript from receiving keyboard events while omnibox is active | |
| 154 | 354 | func (w *WebView) EnablePageKeyboardBlocking() error { | |
| @@ -2,6 +2,14 @@ | |||||
| 2 | 2 | ||||
| 3 | package webkit | 3 | package webkit | ||
| 4 | 4 | ||||
| 5 | + | import ( | |||
| 6 | + | "log" | |||
| 7 | + | "strings" | |||
| 8 | + | "sync" | |||
| 9 | + | "sync/atomic" | |||
| 10 | + | "unsafe" | |||
| 11 | + | ) | |||
| 12 | + | ||||
| 5 | /* | 13 | /* | ||
| 6 | #cgo pkg-config: gtk4 webkitgtk-6.0 | 14 | #cgo pkg-config: gtk4 webkitgtk-6.0 | ||
| 7 | #include <gtk/gtk.h> | 15 | #include <gtk/gtk.h> | ||
| @@ -59,13 +67,6 @@ static void add_shortcut_to_controller(GtkShortcutController* controller, | |||||
| 59 | */ | 67 | */ | ||
| 60 | import "C" | 68 | import "C" | ||
| 61 | 69 | ||||
| 62 | - | import ( | |||
| 63 | - | "log" | |||
| 64 | - | "sync" | |||
| 65 | - | "sync/atomic" | |||
| 66 | - | "unsafe" | |||
| 67 | - | ) | |||
| 68 | - | ||||
| 69 | var ( | 70 | var ( | ||
| 70 | globalShortcutMu sync.Mutex | 71 | globalShortcutMu sync.Mutex | ||
| 71 | globalShortcutCounter uint64 | 72 | globalShortcutCounter uint64 | ||
| @@ -144,11 +145,210 @@ func AddShortcutToController(controller uintptr, key string, handle uintptr) err | |||||
| 144 | return nil | 145 | return nil | ||
| 145 | } | 146 | } | ||
| 146 | 147 | ||||
| 147 | - | // Keyboard event blocking functionality for omnibox isolation | 148 | + | // Global shortcuts registry to track all registered shortcuts for automatic blocking |
| 149 | + | var ( | |||
| 150 | + | globalShortcutsRegistry = make(map[string]bool) | |||
| 151 | + | shortcutsRegistryMu sync.RWMutex | |||
| 152 | + | ) | |||
| 153 | + | ||||
| 154 | + | // RegisterGlobalShortcut adds a shortcut to the global registry for automatic webpage blocking | |||
| 155 | + | func RegisterGlobalShortcut(shortcut string) { | |||
| 156 | + | shortcutsRegistryMu.Lock() | |||
| 157 | + | defer shortcutsRegistryMu.Unlock() | |||
| 158 | + | ||||
| 159 | + | // Normalize shortcut format (convert to lowercase, standardize modifiers) | |||
| 160 | + | normalized := normalizeShortcut(shortcut) | |||
| 161 | + | ||||
| 162 | + | // Only log if this is a new shortcut registration | |||
| 163 | + | if !globalShortcutsRegistry[normalized] { | |||
| 164 | + | globalShortcutsRegistry[normalized] = true | |||
| 165 | + | log.Printf("[shortcuts] Registered global shortcut for blocking: %s -> %s", shortcut, normalized) | |||
| 166 | + | } | |||
| 167 | + | } | |||
| 168 | + | ||||
| 169 | + | // GetRegisteredShortcuts returns a copy of all registered shortcuts | |||
| 170 | + | func GetRegisteredShortcuts() []string { | |||
| 171 | + | shortcutsRegistryMu.RLock() | |||
| 172 | + | defer shortcutsRegistryMu.RUnlock() | |||
| 173 | + | ||||
| 174 | + | shortcuts := make([]string, 0, len(globalShortcutsRegistry)) | |||
| 175 | + | for shortcut := range globalShortcutsRegistry { | |||
| 176 | + | shortcuts = append(shortcuts, shortcut) | |||
| 177 | + | } | |||
| 178 | + | return shortcuts | |||
| 179 | + | } | |||
| 180 | + | ||||
| 181 | + | // normalizeShortcut converts shortcuts to a standardized format for JavaScript blocking | |||
| 182 | + | func normalizeShortcut(shortcut string) string { | |||
| 183 | + | // Convert to lowercase and replace common variations | |||
| 184 | + | normalized := strings.ToLower(shortcut) | |||
| 185 | + | ||||
| 186 | + | // Standardize modifier keys | |||
| 187 | + | normalized = strings.ReplaceAll(normalized, "cmdorctrl+", "ctrl+") | |||
| 188 | + | normalized = strings.ReplaceAll(normalized, "cmd+", "ctrl+") // Treat cmd as ctrl for blocking | |||
| 189 | + | normalized = strings.ReplaceAll(normalized, "control+", "ctrl+") | |||
| 190 | + | ||||
| 191 | + | // Standardize key names | |||
| 192 | + | normalized = strings.ReplaceAll(normalized, "arrowleft", "arrowleft") | |||
| 193 | + | normalized = strings.ReplaceAll(normalized, "arrowright", "arrowright") | |||
| 194 | + | normalized = strings.ReplaceAll(normalized, "arrowup", "arrowup") | |||
| 195 | + | normalized = strings.ReplaceAll(normalized, "arrowdown", "arrowdown") | |||
| 196 | + | normalized = strings.ReplaceAll(normalized, "equal", "equal") // For Ctrl+= | |||
| 197 | + | normalized = strings.ReplaceAll(normalized, "plus", "equal") // Normalize + to = | |||
| 198 | + | ||||
| 199 | + | return normalized | |||
| 200 | + | } | |||
| 201 | + | ||||
| 202 | + | // Keyboard event blocking functionality for global shortcuts protection | |||
| 148 | 203 | ||||
| 149 | // keyboardBlockerScript holds reference to the main-world keyboard blocking script | 204 | // keyboardBlockerScript holds reference to the main-world keyboard blocking script | ||
| 150 | var keyboardBlockerScript *C.WebKitUserScript = nil | 205 | var keyboardBlockerScript *C.WebKitUserScript = nil | ||
| 151 | 206 | ||||
| 207 | + | // EnableGlobalShortcutsProtection injects a script to block all registered global shortcuts from reaching webpages | |||
| 208 | + | func (w *WebView) EnableGlobalShortcutsProtection() error { | |||
| 209 | + | if w == nil || w.destroyed || w.native == nil || w.native.ucm == nil { | |||
| 210 | + | return ErrWebViewNotInitialized | |||
| 211 | + | } | |||
| 212 | + | ||||
| 213 | + | // Get all registered shortcuts | |||
| 214 | + | shortcuts := GetRegisteredShortcuts() | |||
| 215 | + | if len(shortcuts) == 0 { | |||
| 216 | + | log.Printf("[shortcuts] No global shortcuts registered, skipping protection") | |||
| 217 | + | return nil | |||
| 218 | + | } | |||
| 219 | + | ||||
| 220 | + | // Generate JavaScript array of shortcuts to block | |||
| 221 | + | shortcutsJS := "[" | |||
| 222 | + | for i, shortcut := range shortcuts { | |||
| 223 | + | if i > 0 { | |||
| 224 | + | shortcutsJS += "," | |||
| 225 | + | } | |||
| 226 | + | shortcutsJS += `"` + shortcut + `"` | |||
| 227 | + | } | |||
| 228 | + | shortcutsJS += "]" | |||
| 229 | + | ||||
| 230 | + | // Create the blocking script that only blocks registered global shortcuts | |||
| 231 | + | globalProtectionScript := ` | |||
| 232 | + | (() => { | |||
| 233 | + | if (!window.__dumber_global_shortcuts_protection) { | |||
| 234 | + | window.__dumber_global_shortcuts_protection = true; | |||
| 235 | + | ||||
| 236 | + | // List of global shortcuts to block from reaching the webpage | |||
| 237 | + | const globalShortcuts = ` + shortcutsJS + `; | |||
| 238 | + | console.log('[dumber] Protecting global shortcuts:', globalShortcuts); | |||
| 239 | + | ||||
| 240 | + | // Function to check if a key event matches any global shortcut | |||
| 241 | + | const isGlobalShortcut = (e) => { | |||
| 242 | + | const key = e.key.toLowerCase(); | |||
| 243 | + | const code = e.code.toLowerCase(); | |||
| 244 | + | ||||
| 245 | + | // Build shortcut string (similar to GTK format) | |||
| 246 | + | let shortcut = ''; | |||
| 247 | + | if (e.ctrlKey) shortcut += 'ctrl+'; | |||
| 248 | + | if (e.altKey) shortcut += 'alt+'; | |||
| 249 | + | if (e.shiftKey) shortcut += 'shift+'; | |||
| 250 | + | if (e.metaKey) shortcut += 'meta+'; | |||
| 251 | + | ||||
| 252 | + | // Try both key and code for matching | |||
| 253 | + | const keyShortcut = shortcut + key; | |||
| 254 | + | const codeShortcut = shortcut + code; | |||
| 255 | + | ||||
| 256 | + | // Special case mappings for common shortcuts | |||
| 257 | + | const specialMappings = { | |||
| 258 | + | 'ctrl+equal': ['ctrl+=', 'ctrl+plus'], | |||
| 259 | + | 'ctrl+minus': ['ctrl+-'], | |||
| 260 | + | 'f12': ['f12'], | |||
| 261 | + | 'ctrl+r': ['ctrl+r'], | |||
| 262 | + | 'ctrl+shift+r': ['ctrl+shift+r'], | |||
| 263 | + | 'f5': ['f5'] | |||
| 264 | + | }; | |||
| 265 | + | ||||
| 266 | + | // Check direct matches | |||
| 267 | + | if (globalShortcuts.includes(keyShortcut) || globalShortcuts.includes(codeShortcut)) { | |||
| 268 | + | return true; | |||
| 269 | + | } | |||
| 270 | + | ||||
| 271 | + | // Check special mappings | |||
| 272 | + | for (const [canonical, variants] of Object.entries(specialMappings)) { | |||
| 273 | + | if (variants.includes(keyShortcut) || variants.includes(codeShortcut)) { | |||
| 274 | + | if (globalShortcuts.includes(canonical)) { | |||
| 275 | + | return true; | |||
| 276 | + | } | |||
| 277 | + | } | |||
| 278 | + | } | |||
| 279 | + | ||||
| 280 | + | return false; | |||
| 281 | + | }; | |||
| 282 | + | ||||
| 283 | + | // Block only global shortcuts at capture phase | |||
| 284 | + | const blockGlobalShortcut = (e) => { | |||
| 285 | + | if (isGlobalShortcut(e)) { | |||
| 286 | + | console.log('[dumber] Blocked global shortcut from reaching page:', e.key, e.ctrlKey, e.altKey, e.shiftKey); | |||
| 287 | + | e.stopImmediatePropagation(); | |||
| 288 | + | e.preventDefault(); | |||
| 289 | + | e.stopPropagation(); | |||
| 290 | + | return false; | |||
| 291 | + | } | |||
| 292 | + | // Let other key events through normally | |||
| 293 | + | }; | |||
| 294 | + | ||||
| 295 | + | // Install listeners for keyboard events | |||
| 296 | + | document.addEventListener('keydown', blockGlobalShortcut, true); | |||
| 297 | + | document.addEventListener('keyup', blockGlobalShortcut, true); | |||
| 298 | + | document.addEventListener('keypress', blockGlobalShortcut, true); | |||
| 299 | + | ||||
| 300 | + | // Store cleanup function | |||
| 301 | + | window.__dumber_cleanup_global_shortcuts = () => { | |||
| 302 | + | document.removeEventListener('keydown', blockGlobalShortcut, true); | |||
| 303 | + | document.removeEventListener('keyup', blockGlobalShortcut, true); | |||
| 304 | + | document.removeEventListener('keypress', blockGlobalShortcut, true); | |||
| 305 | + | delete window.__dumber_global_shortcuts_protection; | |||
| 306 | + | delete window.__dumber_cleanup_global_shortcuts; | |||
| 307 | + | console.log('[dumber] Global shortcuts protection disabled'); | |||
| 308 | + | }; | |||
| 309 | + | ||||
| 310 | + | console.log('[dumber] Global shortcuts protection enabled for', globalShortcuts.length, 'shortcuts'); | |||
| 311 | + | } | |||
| 312 | + | })(); | |||
| 313 | + | ` | |||
| 314 | + | ||||
| 315 | + | // Inject the script into the main world at document start | |||
| 316 | + | cScript := C.CString(globalProtectionScript) | |||
| 317 | + | defer C.free(unsafe.Pointer(cScript)) | |||
| 318 | + | ||||
| 319 | + | script := C.webkit_user_script_new( | |||
| 320 | + | (*C.gchar)(cScript), | |||
| 321 | + | C.WEBKIT_USER_CONTENT_INJECT_TOP_FRAME, | |||
| 322 | + | C.WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, | |||
| 323 | + | nil, nil) // nil = main world | |||
| 324 | + | ||||
| 325 | + | if script == nil { | |||
| 326 | + | return ErrNotImplemented | |||
| 327 | + | } | |||
| 328 | + | ||||
| 329 | + | C.webkit_user_content_manager_add_script(w.native.ucm, script) | |||
| 330 | + | C.webkit_user_script_unref(script) // Clean up reference | |||
| 331 | + | ||||
| 332 | + | log.Printf("[shortcuts] Global shortcuts protection enabled for %d shortcuts", len(shortcuts)) | |||
| 333 | + | return nil | |||
| 334 | + | } | |||
| 335 | + | ||||
| 336 | + | // DisableGlobalShortcutsProtection removes the global shortcuts protection | |||
| 337 | + | func (w *WebView) DisableGlobalShortcutsProtection() error { | |||
| 338 | + | if w == nil || w.destroyed || w.native == nil { | |||
| 339 | + | return ErrWebViewNotInitialized | |||
| 340 | + | } | |||
| 341 | + | ||||
| 342 | + | // Execute cleanup script | |||
| 343 | + | cleanupScript := ` | |||
| 344 | + | if (window.__dumber_cleanup_global_shortcuts) { | |||
| 345 | + | window.__dumber_cleanup_global_shortcuts(); | |||
| 346 | + | } | |||
| 347 | + | ` | |||
| 348 | + | ||||
| 349 | + | return w.InjectScript(cleanupScript) | |||
| 350 | + | } | |||
| 351 | + | ||||
| 152 | // EnablePageKeyboardBlocking injects a script into the main world to block all keyboard events | 352 | // EnablePageKeyboardBlocking injects a script into the main world to block all keyboard events | ||
| 153 | // This prevents page JavaScript from receiving keyboard events while omnibox is active | 353 | // This prevents page JavaScript from receiving keyboard events while omnibox is active | ||
| 154 | func (w *WebView) EnablePageKeyboardBlocking() error { | 354 | func (w *WebView) EnablePageKeyboardBlocking() error { | ||
M pkg/webkit/webview_cgo.go +5 -1
| @@ -1990,6 +1990,11 @@ func (w *WebView) enableUserContentManager(cfg *Config) { | |||
| 1990 | 1990 | } | |
| 1991 | 1991 | } | |
| 1992 | 1992 | ||
| 1993 | + | // Enable global shortcuts protection to prevent shortcuts from leaking to webpages | |
| 1994 | + | if err := w.EnableGlobalShortcutsProtection(); err != nil { | |
| 1995 | + | log.Printf("[webkit] Warning: Failed to enable global shortcuts protection: %v", err) | |
| 1996 | + | } | |
| 1997 | + | ||
| 1993 | 1998 | // No JS fallback bridge — native UCM is active | |
| 1994 | 1999 | } | |
| 1995 | 2000 | ||
| @@ -2580,7 +2585,6 @@ func (w *WebView) ID() string { | |||
| 2580 | 2585 | return strconv.FormatUint(uint64(w.id), 10) | |
| 2581 | 2586 | } | |
| 2582 | 2587 | ||
| 2583 | - | ||
| 2584 | 2588 | // PrefersDarkTheme returns true if GTK is configured to prefer dark theme | |
| 2585 | 2589 | func PrefersDarkTheme() bool { | |
| 2586 | 2590 | return C.gtk_prefers_dark() != 0 | |
| @@ -1990,6 +1990,11 @@ func (w *WebView) enableUserContentManager(cfg *Config) { | |||||
| 1990 | } | 1990 | } | ||
| 1991 | } | 1991 | } | ||
| 1992 | 1992 | ||||
| 1993 | + | // Enable global shortcuts protection to prevent shortcuts from leaking to webpages | |||
| 1994 | + | if err := w.EnableGlobalShortcutsProtection(); err != nil { | |||
| 1995 | + | log.Printf("[webkit] Warning: Failed to enable global shortcuts protection: %v", err) | |||
| 1996 | + | } | |||
| 1997 | + | ||||
| 1993 | // No JS fallback bridge — native UCM is active | 1998 | // No JS fallback bridge — native UCM is active | ||
| 1994 | } | 1999 | } | ||
| 1995 | 2000 | ||||
| @@ -2580,7 +2585,6 @@ func (w *WebView) ID() string { | |||||
| 2580 | return strconv.FormatUint(uint64(w.id), 10) | 2585 | return strconv.FormatUint(uint64(w.id), 10) | ||
| 2581 | } | 2586 | } | ||
| 2582 | 2587 | ||||
| 2583 | - | ||||
| 2584 | // PrefersDarkTheme returns true if GTK is configured to prefer dark theme | 2588 | // PrefersDarkTheme returns true if GTK is configured to prefer dark theme | ||
| 2585 | func PrefersDarkTheme() bool { | 2589 | func PrefersDarkTheme() bool { | ||
| 2586 | return C.gtk_prefers_dark() != 0 | 2590 | return C.gtk_prefers_dark() != 0 | ||
M pkg/webkit/window_shortcuts.go +3 -0
| @@ -71,6 +71,9 @@ func (ws *WindowShortcuts) RegisterGlobalShortcut(key string, callback func()) e | |||
| 71 | 71 | return err | |
| 72 | 72 | } | |
| 73 | 73 | ||
| 74 | + | // Window shortcuts should NOT be added to global blocking registry | |
| 75 | + | // They need to bubble up to GTK for proper handling | |
| 76 | + | ||
| 74 | 77 | log.Printf("[window-shortcuts] Registered global shortcut: %s -> %s", key, gtkKey) | |
| 75 | 78 | return nil | |
| 76 | 79 | } | |
| @@ -71,6 +71,9 @@ func (ws *WindowShortcuts) RegisterGlobalShortcut(key string, callback func()) e | |||||
| 71 | return err | 71 | return err | ||
| 72 | } | 72 | } | ||
| 73 | 73 | ||||
| 74 | + | // Window shortcuts should NOT be added to global blocking registry | |||
| 75 | + | // They need to bubble up to GTK for proper handling | |||
| 76 | + | ||||
| 74 | log.Printf("[window-shortcuts] Registered global shortcut: %s -> %s", key, gtkKey) | 77 | log.Printf("[window-shortcuts] Registered global shortcut: %s -> %s", key, gtkKey) | ||
| 75 | return nil | 78 | return nil | ||
| 76 | } | 79 | } | ||