detached 76cb885

fix(shortcuts): implement C-level to GTK4 bridge for Alt+Arrow workspace navigation

commit
76cb885171039d98adf2a7af2f752b953f20b093
author
bamen · · 2025-09-22 18:39 UTC
committer
bamen · · 2025-09-22 21:58 UTC
parent
549d2ce
signature
unsigned
8 files · +293 -71
M CHANGELOG.md +1 -0
@@ -8,6 +8,7 @@ All notable changes to this project will be documented in this file.
88 - **Print functionality**: Added Ctrl+Shift+P shortcut to open native print dialogs from any pane. Ctrl+P is blocked at WebKit level to prevent conflicts with pane mode shortcuts
99
1010 ### Fixed
11+- **Alt+Arrow workspace navigation**: Fixed Alt+Arrow keys not working for pane navigation after WebView focus changes. Implemented C-level to GTK4 shortcut bridge that ensures workspace navigation works consistently across all panes
1112 - **Global shortcuts protection**: Fixed window shortcuts (Ctrl+L, Ctrl+F, F12) not working in new panes due to overly aggressive webpage protection. Window shortcuts now properly bubble up to GTK while pane shortcuts remain blocked from webpages. Resolves omnibox and developer tools not responding to keyboard shortcuts in newly created panes
1213 - **Omnibox keyboard event isolation**: Fixed keyboard event leakage from omnibox to underlying webpages that caused unintended page actions (e.g., typing 's' in omnibox triggering GitHub search). Implemented WebKit-level main-world event blocking that activates when omnibox opens, preventing page JavaScript from receiving keyboard events while preserving native GTK shortcuts (Ctrl+L, Ctrl+F, etc.) and omnibox functionality
1314 - **Active pane detection**: Replaced complex pane ID abstraction with persistent webview IDs, fixing omnibox unresponsiveness after navigation. Omnibox now works immediately after page navigation without requiring mouse movement
@@ -8,6 +8,7 @@ All notable changes to this project will be documented in this file.
8 - **Print functionality**: Added Ctrl+Shift+P shortcut to open native print dialogs from any pane. Ctrl+P is blocked at WebKit level to prevent conflicts with pane mode shortcuts8 - **Print functionality**: Added Ctrl+Shift+P shortcut to open native print dialogs from any pane. Ctrl+P is blocked at WebKit level to prevent conflicts with pane mode shortcuts
9 9
10 ### Fixed10 ### Fixed
11+- **Alt+Arrow workspace navigation**: Fixed Alt+Arrow keys not working for pane navigation after WebView focus changes. Implemented C-level to GTK4 shortcut bridge that ensures workspace navigation works consistently across all panes
11 - **Global shortcuts protection**: Fixed window shortcuts (Ctrl+L, Ctrl+F, F12) not working in new panes due to overly aggressive webpage protection. Window shortcuts now properly bubble up to GTK while pane shortcuts remain blocked from webpages. Resolves omnibox and developer tools not responding to keyboard shortcuts in newly created panes12 - **Global shortcuts protection**: Fixed window shortcuts (Ctrl+L, Ctrl+F, F12) not working in new panes due to overly aggressive webpage protection. Window shortcuts now properly bubble up to GTK while pane shortcuts remain blocked from webpages. Resolves omnibox and developer tools not responding to keyboard shortcuts in newly created panes
12 - **Omnibox keyboard event isolation**: Fixed keyboard event leakage from omnibox to underlying webpages that caused unintended page actions (e.g., typing 's' in omnibox triggering GitHub search). Implemented WebKit-level main-world event blocking that activates when omnibox opens, preventing page JavaScript from receiving keyboard events while preserving native GTK shortcuts (Ctrl+L, Ctrl+F, etc.) and omnibox functionality13 - **Omnibox keyboard event isolation**: Fixed keyboard event leakage from omnibox to underlying webpages that caused unintended page actions (e.g., typing 's' in omnibox triggering GitHub search). Implemented WebKit-level main-world event blocking that activates when omnibox opens, preventing page JavaScript from receiving keyboard events while preserving native GTK shortcuts (Ctrl+L, Ctrl+F, etc.) and omnibox functionality
13 - **Active pane detection**: Replaced complex pane ID abstraction with persistent webview IDs, fixing omnibox unresponsiveness after navigation. Omnibox now works immediately after page navigation without requiring mouse movement14 - **Active pane detection**: Replaced complex pane ID abstraction with persistent webview IDs, fixing omnibox unresponsiveness after navigation. Omnibox now works immediately after page navigation without requiring mouse movement
M internal/app/browser/window_shortcuts.go +20 -0
@@ -65,6 +65,11 @@ func (h *WindowShortcutHandler) registerGlobalShortcuts() error {
6565 {"ctrl+equal", h.handleZoomIn, "Zoom in (=)"},
6666 {"ctrl+minus", h.handleZoomOut, "Zoom out"},
6767 {"ctrl+0", h.handleZoomReset, "Zoom reset"},
68+ // Workspace navigation shortcuts - global level for proper active pane targeting
69+ {"alt+ArrowLeft", func() { h.handleWorkspaceNavigation("left") }, "Navigate left pane"},
70+ {"alt+ArrowRight", func() { h.handleWorkspaceNavigation("right") }, "Navigate right pane"},
71+ {"alt+ArrowUp", func() { h.handleWorkspaceNavigation("up") }, "Navigate up pane"},
72+ {"alt+ArrowDown", func() { h.handleWorkspaceNavigation("down") }, "Navigate down pane"},
6873 }
6974
7075 for _, shortcut := range shortcuts {
@@ -286,6 +291,21 @@ func (h *WindowShortcutHandler) handleUIToggle(lastToggle *time.Time, featureNam
286291 }
287292 }
288293
294+func (h *WindowShortcutHandler) handleWorkspaceNavigation(direction string) {
295+ if h.app == nil || h.app.workspace == nil {
296+ log.Printf("[window-shortcuts] No workspace for navigation")
297+ return
298+ }
299+
300+ log.Printf("[window-shortcuts] Workspace navigation: %s", direction)
301+
302+ if h.app.workspace.FocusNeighbor(direction) {
303+ log.Printf("[window-shortcuts] Workspace navigation %s successful", direction)
304+ } else {
305+ log.Printf("[window-shortcuts] Workspace navigation %s failed", direction)
306+ }
307+}
308+
289309 // Cleanup releases resources
290310 func (h *WindowShortcutHandler) Cleanup() {
291311 if h.shortcuts != nil {
@@ -65,6 +65,11 @@ func (h *WindowShortcutHandler) registerGlobalShortcuts() error {
65 {"ctrl+equal", h.handleZoomIn, "Zoom in (=)"},65 {"ctrl+equal", h.handleZoomIn, "Zoom in (=)"},
66 {"ctrl+minus", h.handleZoomOut, "Zoom out"},66 {"ctrl+minus", h.handleZoomOut, "Zoom out"},
67 {"ctrl+0", h.handleZoomReset, "Zoom reset"},67 {"ctrl+0", h.handleZoomReset, "Zoom reset"},
68+ // Workspace navigation shortcuts - global level for proper active pane targeting
69+ {"alt+ArrowLeft", func() { h.handleWorkspaceNavigation("left") }, "Navigate left pane"},
70+ {"alt+ArrowRight", func() { h.handleWorkspaceNavigation("right") }, "Navigate right pane"},
71+ {"alt+ArrowUp", func() { h.handleWorkspaceNavigation("up") }, "Navigate up pane"},
72+ {"alt+ArrowDown", func() { h.handleWorkspaceNavigation("down") }, "Navigate down pane"},
68 }73 }
69 74
70 for _, shortcut := range shortcuts {75 for _, shortcut := range shortcuts {
@@ -286,6 +291,21 @@ func (h *WindowShortcutHandler) handleUIToggle(lastToggle *time.Time, featureNam
286 }291 }
287 }292 }
288 293
294+func (h *WindowShortcutHandler) handleWorkspaceNavigation(direction string) {
295+ if h.app == nil || h.app.workspace == nil {
296+ log.Printf("[window-shortcuts] No workspace for navigation")
297+ return
298+ }
299+
300+ log.Printf("[window-shortcuts] Workspace navigation: %s", direction)
301+
302+ if h.app.workspace.FocusNeighbor(direction) {
303+ log.Printf("[window-shortcuts] Workspace navigation %s successful", direction)
304+ } else {
305+ log.Printf("[window-shortcuts] Workspace navigation %s failed", direction)
306+ }
307+}
308+
289 // Cleanup releases resources309 // Cleanup releases resources
290 func (h *WindowShortcutHandler) Cleanup() {310 func (h *WindowShortcutHandler) Cleanup() {
291 if h.shortcuts != nil {311 if h.shortcuts != nil {
M internal/app/browser/workspace_manager.go +4 -53
@@ -67,52 +67,7 @@ const (
6767 multiPaneClass = "workspace-multi-pane"
6868 )
6969
70-// registerWorkspaceShortcuts registers global workspace navigation shortcuts on the given webView.
71-func (wm *WorkspaceManager) registerWorkspaceShortcuts(webView *webkit.WebView) {
72- if wm == nil || webView == nil {
73- return
74- }
75-
76- registerFocusMove := func(key, dir string) {
77- _ = webView.RegisterKeyboardShortcut(key, func() {
78- log.Printf("[workspace] %s callback triggered on webView=%p", key, webView)
79- if wm.app == nil {
80- log.Printf("[workspace] %s rejected: app is nil", key)
81- return
82- }
83-
84- // Only allow the active webview to handle workspace navigation shortcuts
85- if wm.app.activePane == nil || wm.app.activePane.webView != webView {
86- log.Printf("[workspace] %s rejected: not active pane (active=%p, caller=%p)",
87- key, wm.app.activePane.webView, webView)
88- return
89- }
90-
91- log.Printf("[workspace] %s calling FocusNeighbor(%s)", key, dir)
92- if wm.FocusNeighbor(dir) {
93- log.Printf("Shortcut: focus pane %s", dir)
94- } else {
95- log.Printf("[workspace] FocusNeighbor(%s) returned false", dir)
96- }
97- })
98- }
99-
100- // Pane navigation shortcuts
101- registerFocusMove("alt+ArrowLeft", "left")
102- registerFocusMove("alt+ArrowRight", "right")
103- registerFocusMove("alt+ArrowUp", "up")
104- registerFocusMove("alt+ArrowDown", "down")
105- registerFocusMove("cmdorctrl+ArrowUp", "up")
106- registerFocusMove("cmdorctrl+ArrowDown", "down")
107-
108- // Zoom shortcuts are now handled by global window shortcuts in window_shortcuts.go
109- // for consistency with other global shortcuts like Ctrl+L, Ctrl+F, etc.
110-
111- // Reduced logging: only log shortcuts registration during initialization, not on every hover
112- if wm.app.config != nil && wm.app.config.Debug.EnableWorkspaceDebug {
113- log.Printf("[workspace] registered navigation shortcuts on webView=%p", webView)
114- }
115-}
70+// Workspace navigation shortcuts are now handled globally by WindowShortcutHandler
11671
11772 // NewWorkspaceManager builds a workspace manager rooted at the provided pane.
11873 func NewWorkspaceManager(app *BrowserApp, rootPane *BrowserPane) *WorkspaceManager {
@@ -163,7 +118,7 @@ func NewWorkspaceManager(app *BrowserApp, rootPane *BrowserPane) *WorkspaceManag
163118
164119 app.workspace = manager
165120 manager.focusNode(root)
166- manager.registerWorkspaceShortcuts(root.pane.webView)
121+ // Workspace navigation shortcuts are now handled globally by WindowShortcutHandler
167122 // Ensure initial CSS classes are applied
168123 manager.ensurePaneBaseClasses()
169124 return manager
@@ -433,8 +388,7 @@ func (wm *WorkspaceManager) focusNode(node *paneNode) {
433388 node.pane.webView.SetActive(true)
434389 }
435390
436- // Re-register workspace navigation shortcuts on the newly focused webView
437- wm.registerWorkspaceShortcuts(node.pane.webView)
391+ // Shortcuts are registered once during webView creation, no need to re-register on focus
438392
439393 if handler := node.pane.messageHandler; handler != nil {
440394 handler.SetWorkspaceObserver(wm)
@@ -1514,10 +1468,7 @@ func (wm *WorkspaceManager) HandlePopup(source *webkit.WebView, url string) *web
15141468 return nil
15151469 }
15161470
1517- // Register workspace navigation shortcuts on popup WebView before creating pane
1518- // This ensures shortcuts are available immediately when the WebView is ready
1519- wm.registerWorkspaceShortcuts(newView)
1520- log.Printf("[workspace] Registered workspace shortcuts for new WebView")
1471+ // Workspace navigation shortcuts are now handled globally by WindowShortcutHandler
15211472
15221473 // Create a pane for the new WebView
15231474 newPane, err := wm.createPane(newView)
@@ -67,52 +67,7 @@ const (
67 multiPaneClass = "workspace-multi-pane"67 multiPaneClass = "workspace-multi-pane"
68 )68 )
69 69
70-// registerWorkspaceShortcuts registers global workspace navigation shortcuts on the given webView.70+// Workspace navigation shortcuts are now handled globally by WindowShortcutHandler
71-func (wm *WorkspaceManager) registerWorkspaceShortcuts(webView *webkit.WebView) {
72- if wm == nil || webView == nil {
73- return
74- }
75-
76- registerFocusMove := func(key, dir string) {
77- _ = webView.RegisterKeyboardShortcut(key, func() {
78- log.Printf("[workspace] %s callback triggered on webView=%p", key, webView)
79- if wm.app == nil {
80- log.Printf("[workspace] %s rejected: app is nil", key)
81- return
82- }
83-
84- // Only allow the active webview to handle workspace navigation shortcuts
85- if wm.app.activePane == nil || wm.app.activePane.webView != webView {
86- log.Printf("[workspace] %s rejected: not active pane (active=%p, caller=%p)",
87- key, wm.app.activePane.webView, webView)
88- return
89- }
90-
91- log.Printf("[workspace] %s calling FocusNeighbor(%s)", key, dir)
92- if wm.FocusNeighbor(dir) {
93- log.Printf("Shortcut: focus pane %s", dir)
94- } else {
95- log.Printf("[workspace] FocusNeighbor(%s) returned false", dir)
96- }
97- })
98- }
99-
100- // Pane navigation shortcuts
101- registerFocusMove("alt+ArrowLeft", "left")
102- registerFocusMove("alt+ArrowRight", "right")
103- registerFocusMove("alt+ArrowUp", "up")
104- registerFocusMove("alt+ArrowDown", "down")
105- registerFocusMove("cmdorctrl+ArrowUp", "up")
106- registerFocusMove("cmdorctrl+ArrowDown", "down")
107-
108- // Zoom shortcuts are now handled by global window shortcuts in window_shortcuts.go
109- // for consistency with other global shortcuts like Ctrl+L, Ctrl+F, etc.
110-
111- // Reduced logging: only log shortcuts registration during initialization, not on every hover
112- if wm.app.config != nil && wm.app.config.Debug.EnableWorkspaceDebug {
113- log.Printf("[workspace] registered navigation shortcuts on webView=%p", webView)
114- }
115-}
116 71
117 // NewWorkspaceManager builds a workspace manager rooted at the provided pane.72 // NewWorkspaceManager builds a workspace manager rooted at the provided pane.
118 func NewWorkspaceManager(app *BrowserApp, rootPane *BrowserPane) *WorkspaceManager {73 func NewWorkspaceManager(app *BrowserApp, rootPane *BrowserPane) *WorkspaceManager {
@@ -163,7 +118,7 @@ func NewWorkspaceManager(app *BrowserApp, rootPane *BrowserPane) *WorkspaceManag
163 118
164 app.workspace = manager119 app.workspace = manager
165 manager.focusNode(root)120 manager.focusNode(root)
166- manager.registerWorkspaceShortcuts(root.pane.webView)121+ // Workspace navigation shortcuts are now handled globally by WindowShortcutHandler
167 // Ensure initial CSS classes are applied122 // Ensure initial CSS classes are applied
168 manager.ensurePaneBaseClasses()123 manager.ensurePaneBaseClasses()
169 return manager124 return manager
@@ -433,8 +388,7 @@ func (wm *WorkspaceManager) focusNode(node *paneNode) {
433 node.pane.webView.SetActive(true)388 node.pane.webView.SetActive(true)
434 }389 }
435 390
436- // Re-register workspace navigation shortcuts on the newly focused webView391+ // Shortcuts are registered once during webView creation, no need to re-register on focus
437- wm.registerWorkspaceShortcuts(node.pane.webView)
438 392
439 if handler := node.pane.messageHandler; handler != nil {393 if handler := node.pane.messageHandler; handler != nil {
440 handler.SetWorkspaceObserver(wm)394 handler.SetWorkspaceObserver(wm)
@@ -1514,10 +1468,7 @@ func (wm *WorkspaceManager) HandlePopup(source *webkit.WebView, url string) *web
1514 return nil1468 return nil
1515 }1469 }
1516 1470
1517- // Register workspace navigation shortcuts on popup WebView before creating pane1471+ // Workspace navigation shortcuts are now handled globally by WindowShortcutHandler
1518- // This ensures shortcuts are available immediately when the WebView is ready
1519- wm.registerWorkspaceShortcuts(newView)
1520- log.Printf("[workspace] Registered workspace shortcuts for new WebView")
1521 1472
1522 // Create a pane for the new WebView1473 // Create a pane for the new WebView
1523 newPane, err := wm.createPane(newView)1474 newPane, err := wm.createPane(newView)
M internal/app/browser/workspace_manager_test.go +78 -0
@@ -191,3 +191,81 @@ func TestFocusNeighborWithoutPeer(t *testing.T) {
191191 t.Fatalf("expected focus move to fail when only one pane exists")
192192 }
193193 }
194+
195+// TestWorkspaceNavigationAfterFocusChanges tests that workspace navigation
196+// works correctly after focus changes between panes. This validates that
197+// the FocusNeighbor functionality works regardless of which pane previously
198+// had focus (since Alt+Arrow shortcuts are now handled globally).
199+func TestWorkspaceNavigationAfterFocusChanges(t *testing.T) {
200+ wm := newTestWorkspaceManager(t)
201+
202+ // Split to create two panes
203+ right, err := wm.splitNode(wm.root, "right")
204+ if err != nil {
205+ t.Fatalf("split failed: %v", err)
206+ }
207+
208+ left := right.parent.left
209+
210+ // Set up geometry for navigation calculations
211+ webkit.SetWidgetBoundsForTesting(left.container, webkit.WidgetBounds{X: 0, Y: 0, Width: 100, Height: 100})
212+ webkit.SetWidgetBoundsForTesting(right.container, webkit.WidgetBounds{X: 120, Y: 0, Width: 100, Height: 100})
213+
214+ // Initially, the active pane should be the right one (newly created)
215+ if wm.GetActiveNode() != right {
216+ t.Fatalf("expected right pane to be active initially")
217+ }
218+
219+ // Test that navigation works from right to left
220+ if !wm.FocusNeighbor("left") {
221+ t.Fatalf("expected focus neighbor left to succeed from right pane")
222+ }
223+
224+ if wm.GetActiveNode() != left {
225+ t.Fatalf("expected focus to move to left pane")
226+ }
227+
228+ // Test that navigation works from left to right
229+ if !wm.FocusNeighbor("right") {
230+ t.Fatalf("expected focus neighbor right to succeed from left pane")
231+ }
232+
233+ if wm.GetActiveNode() != right {
234+ t.Fatalf("expected focus to move back to right pane")
235+ }
236+
237+ // Simulate multiple focus changes to verify that workspace navigation
238+ // works consistently (the old bug was per-webview shortcut registration)
239+ for i := 0; i < 3; i++ {
240+ // Move focus left
241+ wm.focusNode(left)
242+ if wm.GetActiveNode() != left {
243+ t.Fatalf("expected left pane to be active after focusNode call %d", i)
244+ }
245+
246+ // Test navigation still works
247+ if !wm.FocusNeighbor("right") {
248+ t.Fatalf("expected navigation right to work after focus change %d", i)
249+ }
250+
251+ // Move focus right
252+ wm.focusNode(right)
253+ if wm.GetActiveNode() != right {
254+ t.Fatalf("expected right pane to be active after focusNode call %d", i)
255+ }
256+
257+ // Test navigation still works
258+ if !wm.FocusNeighbor("left") {
259+ t.Fatalf("expected navigation left to work after focus change %d", i)
260+ }
261+ }
262+
263+ // Final verification that we can navigate in both directions
264+ if !wm.FocusNeighbor("right") {
265+ t.Fatalf("expected final navigation right to work")
266+ }
267+
268+ if wm.GetActiveNode() != right {
269+ t.Fatalf("expected final active pane to be right")
270+ }
271+}
@@ -191,3 +191,81 @@ func TestFocusNeighborWithoutPeer(t *testing.T) {
191 t.Fatalf("expected focus move to fail when only one pane exists")191 t.Fatalf("expected focus move to fail when only one pane exists")
192 }192 }
193 }193 }
194+
195+// TestWorkspaceNavigationAfterFocusChanges tests that workspace navigation
196+// works correctly after focus changes between panes. This validates that
197+// the FocusNeighbor functionality works regardless of which pane previously
198+// had focus (since Alt+Arrow shortcuts are now handled globally).
199+func TestWorkspaceNavigationAfterFocusChanges(t *testing.T) {
200+ wm := newTestWorkspaceManager(t)
201+
202+ // Split to create two panes
203+ right, err := wm.splitNode(wm.root, "right")
204+ if err != nil {
205+ t.Fatalf("split failed: %v", err)
206+ }
207+
208+ left := right.parent.left
209+
210+ // Set up geometry for navigation calculations
211+ webkit.SetWidgetBoundsForTesting(left.container, webkit.WidgetBounds{X: 0, Y: 0, Width: 100, Height: 100})
212+ webkit.SetWidgetBoundsForTesting(right.container, webkit.WidgetBounds{X: 120, Y: 0, Width: 100, Height: 100})
213+
214+ // Initially, the active pane should be the right one (newly created)
215+ if wm.GetActiveNode() != right {
216+ t.Fatalf("expected right pane to be active initially")
217+ }
218+
219+ // Test that navigation works from right to left
220+ if !wm.FocusNeighbor("left") {
221+ t.Fatalf("expected focus neighbor left to succeed from right pane")
222+ }
223+
224+ if wm.GetActiveNode() != left {
225+ t.Fatalf("expected focus to move to left pane")
226+ }
227+
228+ // Test that navigation works from left to right
229+ if !wm.FocusNeighbor("right") {
230+ t.Fatalf("expected focus neighbor right to succeed from left pane")
231+ }
232+
233+ if wm.GetActiveNode() != right {
234+ t.Fatalf("expected focus to move back to right pane")
235+ }
236+
237+ // Simulate multiple focus changes to verify that workspace navigation
238+ // works consistently (the old bug was per-webview shortcut registration)
239+ for i := 0; i < 3; i++ {
240+ // Move focus left
241+ wm.focusNode(left)
242+ if wm.GetActiveNode() != left {
243+ t.Fatalf("expected left pane to be active after focusNode call %d", i)
244+ }
245+
246+ // Test navigation still works
247+ if !wm.FocusNeighbor("right") {
248+ t.Fatalf("expected navigation right to work after focus change %d", i)
249+ }
250+
251+ // Move focus right
252+ wm.focusNode(right)
253+ if wm.GetActiveNode() != right {
254+ t.Fatalf("expected right pane to be active after focusNode call %d", i)
255+ }
256+
257+ // Test navigation still works
258+ if !wm.FocusNeighbor("left") {
259+ t.Fatalf("expected navigation left to work after focus change %d", i)
260+ }
261+ }
262+
263+ // Final verification that we can navigate in both directions
264+ if !wm.FocusNeighbor("right") {
265+ t.Fatalf("expected final navigation right to work")
266+ }
267+
268+ if wm.GetActiveNode() != right {
269+ t.Fatalf("expected final active pane to be right")
270+ }
271+}
M pkg/webkit/keyboard_cgo.go +63 -0
@@ -146,6 +146,7 @@ func goOnKeyPress(id C.ulong, keyval C.guint, state C.GdkModifierType) C.gboolea
146146 uid := uintptr(id)
147147 kv := uint(keyval)
148148 st := uint(state)
149+
149150 if dispatchAccelerator(uid, kv, st) {
150151 return C.gboolean(1)
151152 }
@@ -219,6 +220,11 @@ func dispatchAccelerator(uid uintptr, keyval uint, state uint) bool {
219220 return false
220221 }
221222
223+ // GTK-level fallback blocking for global shortcuts
224+ if checkGlobalShortcutBlocking(keyName, ctrl, alt, shift) {
225+ return true // Block the event from reaching WebKit
226+ }
227+
222228 now := time.Now()
223229 rawKey := fmt.Sprintf("raw:%v:%v:%v:%s", ctrl, alt, shift, keyName)
224230 if shouldDebounceShortcut(uid, rawKey, now) {
@@ -275,6 +281,7 @@ func dispatchAccelerator(uid uintptr, keyval uint, state uint) bool {
275281 if reg == nil {
276282 return handled
277283 }
284+
278285 for _, name := range candidates {
279286 if cb, ok := reg[name]; ok {
280287 if shouldDebounceShortcut(uid, "shortcut:"+name, now) {
@@ -290,6 +297,62 @@ func dispatchAccelerator(uid uintptr, keyval uint, state uint) bool {
290297 return handled
291298 }
292299
300+// checkGlobalShortcutBlocking checks if a keyboard event should be blocked at GTK level
301+// This provides defense-in-depth protection when JavaScript-level blocking fails
302+func checkGlobalShortcutBlocking(keyName string, ctrl, alt, shift bool) bool {
303+ if keyName == "" {
304+ return false
305+ }
306+
307+ // Build shortcut string similar to the JavaScript protection logic
308+ var shortcut strings.Builder
309+ if ctrl {
310+ shortcut.WriteString("ctrl+")
311+ }
312+ if alt {
313+ shortcut.WriteString("alt+")
314+ }
315+ if shift {
316+ shortcut.WriteString("shift+")
317+ }
318+ shortcut.WriteString(strings.ToLower(keyName))
319+
320+ shortcutStr := shortcut.String()
321+
322+ // Check if this shortcut is in the global registry
323+ shortcuts := GetRegisteredShortcuts()
324+ for _, registered := range shortcuts {
325+ if registered == shortcutStr {
326+ // Trigger the callback handle if available (especially for Alt+Arrow shortcuts)
327+ triggerGlobalShortcutCallback(registered)
328+ return true
329+ }
330+ // Also check common variations
331+ if registered == strings.ReplaceAll(shortcutStr, "arrowleft", "left") ||
332+ registered == strings.ReplaceAll(shortcutStr, "arrowright", "right") ||
333+ registered == strings.ReplaceAll(shortcutStr, "arrowup", "up") ||
334+ registered == strings.ReplaceAll(shortcutStr, "arrowdown", "down") {
335+ // Trigger the callback handle for the registered variant
336+ triggerGlobalShortcutCallback(registered)
337+ return true
338+ }
339+ }
340+
341+ return false
342+}
343+
344+// triggerGlobalShortcutCallback looks up and invokes the callback handle for a given shortcut
345+func triggerGlobalShortcutCallback(shortcut string) {
346+ // Access the global shortcut handles map from shortcuts_cgo.go
347+ shortcutsRegistryMu.RLock()
348+ handle, exists := globalShortcutHandles[shortcut]
349+ shortcutsRegistryMu.RUnlock()
350+
351+ if exists && handle != 0 {
352+ invokeWindowShortcutCallback(handle)
353+ }
354+}
355+
293356 func nextViewID() uintptr {
294357 return uintptr(atomic.AddUint64(&viewIDCounter, 1))
295358 }
@@ -146,6 +146,7 @@ func goOnKeyPress(id C.ulong, keyval C.guint, state C.GdkModifierType) C.gboolea
146 uid := uintptr(id)146 uid := uintptr(id)
147 kv := uint(keyval)147 kv := uint(keyval)
148 st := uint(state)148 st := uint(state)
149+
149 if dispatchAccelerator(uid, kv, st) {150 if dispatchAccelerator(uid, kv, st) {
150 return C.gboolean(1)151 return C.gboolean(1)
151 }152 }
@@ -219,6 +220,11 @@ func dispatchAccelerator(uid uintptr, keyval uint, state uint) bool {
219 return false220 return false
220 }221 }
221 222
223+ // GTK-level fallback blocking for global shortcuts
224+ if checkGlobalShortcutBlocking(keyName, ctrl, alt, shift) {
225+ return true // Block the event from reaching WebKit
226+ }
227+
222 now := time.Now()228 now := time.Now()
223 rawKey := fmt.Sprintf("raw:%v:%v:%v:%s", ctrl, alt, shift, keyName)229 rawKey := fmt.Sprintf("raw:%v:%v:%v:%s", ctrl, alt, shift, keyName)
224 if shouldDebounceShortcut(uid, rawKey, now) {230 if shouldDebounceShortcut(uid, rawKey, now) {
@@ -275,6 +281,7 @@ func dispatchAccelerator(uid uintptr, keyval uint, state uint) bool {
275 if reg == nil {281 if reg == nil {
276 return handled282 return handled
277 }283 }
284+
278 for _, name := range candidates {285 for _, name := range candidates {
279 if cb, ok := reg[name]; ok {286 if cb, ok := reg[name]; ok {
280 if shouldDebounceShortcut(uid, "shortcut:"+name, now) {287 if shouldDebounceShortcut(uid, "shortcut:"+name, now) {
@@ -290,6 +297,62 @@ func dispatchAccelerator(uid uintptr, keyval uint, state uint) bool {
290 return handled297 return handled
291 }298 }
292 299
300+// checkGlobalShortcutBlocking checks if a keyboard event should be blocked at GTK level
301+// This provides defense-in-depth protection when JavaScript-level blocking fails
302+func checkGlobalShortcutBlocking(keyName string, ctrl, alt, shift bool) bool {
303+ if keyName == "" {
304+ return false
305+ }
306+
307+ // Build shortcut string similar to the JavaScript protection logic
308+ var shortcut strings.Builder
309+ if ctrl {
310+ shortcut.WriteString("ctrl+")
311+ }
312+ if alt {
313+ shortcut.WriteString("alt+")
314+ }
315+ if shift {
316+ shortcut.WriteString("shift+")
317+ }
318+ shortcut.WriteString(strings.ToLower(keyName))
319+
320+ shortcutStr := shortcut.String()
321+
322+ // Check if this shortcut is in the global registry
323+ shortcuts := GetRegisteredShortcuts()
324+ for _, registered := range shortcuts {
325+ if registered == shortcutStr {
326+ // Trigger the callback handle if available (especially for Alt+Arrow shortcuts)
327+ triggerGlobalShortcutCallback(registered)
328+ return true
329+ }
330+ // Also check common variations
331+ if registered == strings.ReplaceAll(shortcutStr, "arrowleft", "left") ||
332+ registered == strings.ReplaceAll(shortcutStr, "arrowright", "right") ||
333+ registered == strings.ReplaceAll(shortcutStr, "arrowup", "up") ||
334+ registered == strings.ReplaceAll(shortcutStr, "arrowdown", "down") {
335+ // Trigger the callback handle for the registered variant
336+ triggerGlobalShortcutCallback(registered)
337+ return true
338+ }
339+ }
340+
341+ return false
342+}
343+
344+// triggerGlobalShortcutCallback looks up and invokes the callback handle for a given shortcut
345+func triggerGlobalShortcutCallback(shortcut string) {
346+ // Access the global shortcut handles map from shortcuts_cgo.go
347+ shortcutsRegistryMu.RLock()
348+ handle, exists := globalShortcutHandles[shortcut]
349+ shortcutsRegistryMu.RUnlock()
350+
351+ if exists && handle != 0 {
352+ invokeWindowShortcutCallback(handle)
353+ }
354+}
355+
293 func nextViewID() uintptr {356 func nextViewID() uintptr {
294 return uintptr(atomic.AddUint64(&viewIDCounter, 1))357 return uintptr(atomic.AddUint64(&viewIDCounter, 1))
295 }358 }
M pkg/webkit/shortcuts_cgo.go +113 -16
@@ -3,6 +3,7 @@
33 package webkit
44
55 import (
6+ "fmt"
67 "log"
78 "strings"
89 "sync"
@@ -149,21 +150,47 @@ func AddShortcutToController(controller uintptr, key string, handle uintptr) err
149150 var (
150151 globalShortcutsRegistry = make(map[string]bool)
151152 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
158+var (
159+ activeWebViews = make(map[*WebView]bool)
160+ activeWebViewsMu sync.RWMutex
152161 )
153162
154163 // RegisterGlobalShortcut adds a shortcut to the global registry for automatic webpage blocking
155164 func RegisterGlobalShortcut(shortcut string) {
156165 shortcutsRegistryMu.Lock()
157- defer shortcutsRegistryMu.Unlock()
158-
159166 // Normalize shortcut format (convert to lowercase, standardize modifiers)
160167 normalized := normalizeShortcut(shortcut)
161168
162- // Only log if this is a new shortcut registration
163- if !globalShortcutsRegistry[normalized] {
169+ // Only proceed if this is a new shortcut registration
170+ wasNew := !globalShortcutsRegistry[normalized]
171+ if wasNew {
164172 globalShortcutsRegistry[normalized] = true
165173 log.Printf("[shortcuts] Registered global shortcut for blocking: %s -> %s", shortcut, normalized)
166174 }
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
184+func 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()
167194 }
168195
169196 // GetRegisteredShortcuts returns a copy of all registered shortcuts
@@ -178,6 +205,51 @@ func GetRegisteredShortcuts() []string {
178205 return shortcuts
179206 }
180207
208+// RegisterActiveWebView adds a WebView to the active registry for dynamic protection updates
209+func 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
220+func 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
231+func 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+
181253 // normalizeShortcut converts shortcuts to a standardized format for JavaScript blocking
182254 func normalizeShortcut(shortcut string) string {
183255 // Convert to lowercase and replace common variations
@@ -188,11 +260,19 @@ func normalizeShortcut(shortcut string) string {
188260 normalized = strings.ReplaceAll(normalized, "cmd+", "ctrl+") // Treat cmd as ctrl for blocking
189261 normalized = strings.ReplaceAll(normalized, "control+", "ctrl+")
190262
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")
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
196276 normalized = strings.ReplaceAll(normalized, "equal", "equal") // For Ctrl+=
197277 normalized = strings.ReplaceAll(normalized, "plus", "equal") // Normalize + to =
198278
@@ -227,15 +307,25 @@ func (w *WebView) EnableGlobalShortcutsProtection() error {
227307 }
228308 shortcutsJS += "]"
229309
230- // Create the blocking script that only blocks registered global shortcuts
310+ // Create a version-based blocking script that can be updated dynamically
311+ protectionVersion := len(shortcuts) // Use shortcuts count as version
231312 globalProtectionScript := `
232313 (() => {
233- if (!window.__dumber_global_shortcuts_protection) {
234- window.__dumber_global_shortcuts_protection = true;
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;
235325
236326 // List of global shortcuts to block from reaching the webpage
237327 const globalShortcuts = ` + shortcutsJS + `;
238- console.log('[dumber] Protecting global shortcuts:', globalShortcuts);
328+ console.log('[dumber] Protecting global shortcuts (v' + newVersion + '):', globalShortcuts);
239329
240330 // Function to check if a key event matches any global shortcut
241331 const isGlobalShortcut = (e) => {
@@ -260,7 +350,12 @@ func (w *WebView) EnableGlobalShortcutsProtection() error {
260350 'f12': ['f12'],
261351 'ctrl+r': ['ctrl+r'],
262352 'ctrl+shift+r': ['ctrl+shift+r'],
263- 'f5': ['f5']
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']
264359 };
265360
266361 // Check direct matches
@@ -302,12 +397,14 @@ func (w *WebView) EnableGlobalShortcutsProtection() error {
302397 document.removeEventListener('keydown', blockGlobalShortcut, true);
303398 document.removeEventListener('keyup', blockGlobalShortcut, true);
304399 document.removeEventListener('keypress', blockGlobalShortcut, true);
305- delete window.__dumber_global_shortcuts_protection;
400+ delete window.__dumber_shortcuts_protection_version;
306401 delete window.__dumber_cleanup_global_shortcuts;
307402 console.log('[dumber] Global shortcuts protection disabled');
308403 };
309404
310- console.log('[dumber] Global shortcuts protection enabled for', globalShortcuts.length, 'shortcuts');
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 + ')');
311408 }
312409 })();
313410 `
@@ -3,6 +3,7 @@
3 package webkit3 package webkit
4 4
5 import (5 import (
6+ "fmt"
6 "log"7 "log"
7 "strings"8 "strings"
8 "sync"9 "sync"
@@ -149,21 +150,47 @@ func AddShortcutToController(controller uintptr, key string, handle uintptr) err
149 var (150 var (
150 globalShortcutsRegistry = make(map[string]bool)151 globalShortcutsRegistry = make(map[string]bool)
151 shortcutsRegistryMu sync.RWMutex152 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
158+var (
159+ activeWebViews = make(map[*WebView]bool)
160+ activeWebViewsMu sync.RWMutex
152 )161 )
153 162
154 // RegisterGlobalShortcut adds a shortcut to the global registry for automatic webpage blocking163 // RegisterGlobalShortcut adds a shortcut to the global registry for automatic webpage blocking
155 func RegisterGlobalShortcut(shortcut string) {164 func RegisterGlobalShortcut(shortcut string) {
156 shortcutsRegistryMu.Lock()165 shortcutsRegistryMu.Lock()
157- defer shortcutsRegistryMu.Unlock()
158-
159 // Normalize shortcut format (convert to lowercase, standardize modifiers)166 // Normalize shortcut format (convert to lowercase, standardize modifiers)
160 normalized := normalizeShortcut(shortcut)167 normalized := normalizeShortcut(shortcut)
161 168
162- // Only log if this is a new shortcut registration169+ // Only proceed if this is a new shortcut registration
163- if !globalShortcutsRegistry[normalized] {170+ wasNew := !globalShortcutsRegistry[normalized]
171+ if wasNew {
164 globalShortcutsRegistry[normalized] = true172 globalShortcutsRegistry[normalized] = true
165 log.Printf("[shortcuts] Registered global shortcut for blocking: %s -> %s", shortcut, normalized)173 log.Printf("[shortcuts] Registered global shortcut for blocking: %s -> %s", shortcut, normalized)
166 }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
184+func 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()
167 }194 }
168 195
169 // GetRegisteredShortcuts returns a copy of all registered shortcuts196 // GetRegisteredShortcuts returns a copy of all registered shortcuts
@@ -178,6 +205,51 @@ func GetRegisteredShortcuts() []string {
178 return shortcuts205 return shortcuts
179 }206 }
180 207
208+// RegisterActiveWebView adds a WebView to the active registry for dynamic protection updates
209+func 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
220+func 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
231+func 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+
181 // normalizeShortcut converts shortcuts to a standardized format for JavaScript blocking253 // normalizeShortcut converts shortcuts to a standardized format for JavaScript blocking
182 func normalizeShortcut(shortcut string) string {254 func normalizeShortcut(shortcut string) string {
183 // Convert to lowercase and replace common variations255 // Convert to lowercase and replace common variations
@@ -188,11 +260,19 @@ func normalizeShortcut(shortcut string) string {
188 normalized = strings.ReplaceAll(normalized, "cmd+", "ctrl+") // Treat cmd as ctrl for blocking260 normalized = strings.ReplaceAll(normalized, "cmd+", "ctrl+") // Treat cmd as ctrl for blocking
189 normalized = strings.ReplaceAll(normalized, "control+", "ctrl+")261 normalized = strings.ReplaceAll(normalized, "control+", "ctrl+")
190 262
191- // Standardize key names263+ // Standardize arrow key names (handle both formats)
192- normalized = strings.ReplaceAll(normalized, "arrowleft", "arrowleft")264+ normalized = strings.ReplaceAll(normalized, "left", "arrowleft")
193- normalized = strings.ReplaceAll(normalized, "arrowright", "arrowright")265+ normalized = strings.ReplaceAll(normalized, "right", "arrowright")
194- normalized = strings.ReplaceAll(normalized, "arrowup", "arrowup")266+ normalized = strings.ReplaceAll(normalized, "up", "arrowup")
195- normalized = strings.ReplaceAll(normalized, "arrowdown", "arrowdown")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
196 normalized = strings.ReplaceAll(normalized, "equal", "equal") // For Ctrl+=276 normalized = strings.ReplaceAll(normalized, "equal", "equal") // For Ctrl+=
197 normalized = strings.ReplaceAll(normalized, "plus", "equal") // Normalize + to =277 normalized = strings.ReplaceAll(normalized, "plus", "equal") // Normalize + to =
198 278
@@ -227,15 +307,25 @@ func (w *WebView) EnableGlobalShortcutsProtection() error {
227 }307 }
228 shortcutsJS += "]"308 shortcutsJS += "]"
229 309
230- // Create the blocking script that only blocks registered global shortcuts310+ // Create a version-based blocking script that can be updated dynamically
311+ protectionVersion := len(shortcuts) // Use shortcuts count as version
231 globalProtectionScript := `312 globalProtectionScript := `
232 (() => {313 (() => {
233- if (!window.__dumber_global_shortcuts_protection) {314+ const newVersion = ` + fmt.Sprintf("%d", protectionVersion) + `;
234- window.__dumber_global_shortcuts_protection = true;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;
235 325
236 // List of global shortcuts to block from reaching the webpage326 // List of global shortcuts to block from reaching the webpage
237 const globalShortcuts = ` + shortcutsJS + `;327 const globalShortcuts = ` + shortcutsJS + `;
238- console.log('[dumber] Protecting global shortcuts:', globalShortcuts);328+ console.log('[dumber] Protecting global shortcuts (v' + newVersion + '):', globalShortcuts);
239 329
240 // Function to check if a key event matches any global shortcut330 // Function to check if a key event matches any global shortcut
241 const isGlobalShortcut = (e) => {331 const isGlobalShortcut = (e) => {
@@ -260,7 +350,12 @@ func (w *WebView) EnableGlobalShortcutsProtection() error {
260 'f12': ['f12'],350 'f12': ['f12'],
261 'ctrl+r': ['ctrl+r'],351 'ctrl+r': ['ctrl+r'],
262 'ctrl+shift+r': ['ctrl+shift+r'],352 'ctrl+shift+r': ['ctrl+shift+r'],
263- 'f5': ['f5']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']
264 };359 };
265 360
266 // Check direct matches361 // Check direct matches
@@ -302,12 +397,14 @@ func (w *WebView) EnableGlobalShortcutsProtection() error {
302 document.removeEventListener('keydown', blockGlobalShortcut, true);397 document.removeEventListener('keydown', blockGlobalShortcut, true);
303 document.removeEventListener('keyup', blockGlobalShortcut, true);398 document.removeEventListener('keyup', blockGlobalShortcut, true);
304 document.removeEventListener('keypress', blockGlobalShortcut, true);399 document.removeEventListener('keypress', blockGlobalShortcut, true);
305- delete window.__dumber_global_shortcuts_protection;400+ delete window.__dumber_shortcuts_protection_version;
306 delete window.__dumber_cleanup_global_shortcuts;401 delete window.__dumber_cleanup_global_shortcuts;
307 console.log('[dumber] Global shortcuts protection disabled');402 console.log('[dumber] Global shortcuts protection disabled');
308 };403 };
309 404
310- console.log('[dumber] Global shortcuts protection enabled for', globalShortcuts.length, 'shortcuts');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 + ')');
311 }408 }
312 })();409 })();
313 `410 `
M pkg/webkit/webview_cgo.go +7 -0
@@ -1614,6 +1614,10 @@ func (w *WebView) Destroy() error {
16141614
16151615 w.releaseNativeWidgets()
16161616 w.destroyed = true
1617+
1618+ // Unregister from dynamic protection updates
1619+ UnregisterActiveWebView(w)
1620+
16171621 unregisterView(w.id)
16181622 return nil
16191623 }
@@ -2028,6 +2032,9 @@ func (w *WebView) enableUserContentManager(cfg *Config) {
20282032 log.Printf("[webkit] Warning: Failed to enable global shortcuts protection: %v", err)
20292033 }
20302034
2035+ // Register this WebView for dynamic protection updates
2036+ RegisterActiveWebView(w)
2037+
20312038 // No JS fallback bridge — native UCM is active
20322039 }
20332040
@@ -1614,6 +1614,10 @@ func (w *WebView) Destroy() error {
1614 1614
1615 w.releaseNativeWidgets()1615 w.releaseNativeWidgets()
1616 w.destroyed = true1616 w.destroyed = true
1617+
1618+ // Unregister from dynamic protection updates
1619+ UnregisterActiveWebView(w)
1620+
1617 unregisterView(w.id)1621 unregisterView(w.id)
1618 return nil1622 return nil
1619 }1623 }
@@ -2028,6 +2032,9 @@ func (w *WebView) enableUserContentManager(cfg *Config) {
2028 log.Printf("[webkit] Warning: Failed to enable global shortcuts protection: %v", err)2032 log.Printf("[webkit] Warning: Failed to enable global shortcuts protection: %v", err)
2029 }2033 }
2030 2034
2035+ // Register this WebView for dynamic protection updates
2036+ RegisterActiveWebView(w)
2037+
2031 // No JS fallback bridge — native UCM is active2038 // No JS fallback bridge — native UCM is active
2032 }2039 }
2033 2040
M pkg/webkit/window_shortcuts.go +7 -2
@@ -71,8 +71,13 @@ func (ws *WindowShortcuts) RegisterGlobalShortcut(key string, callback func()) e
7171 return err
7272 }
7373
74- // Window shortcuts should NOT be added to global blocking registry
75- // They need to bubble up to GTK for proper handling
74+ // Add Alt+Arrow shortcuts to global blocking registry with callback handle for C-level triggering
75+ if strings.HasPrefix(key, "alt+Arrow") {
76+ RegisterGlobalShortcutWithHandle(key, handle)
77+ } else {
78+ // For non-Alt+Arrow shortcuts, just register for blocking without callback handle
79+ RegisterGlobalShortcut(key)
80+ }
7681
7782 log.Printf("[window-shortcuts] Registered global shortcut: %s -> %s", key, gtkKey)
7883 return nil
@@ -71,8 +71,13 @@ func (ws *WindowShortcuts) RegisterGlobalShortcut(key string, callback func()) e
71 return err71 return err
72 }72 }
73 73
74- // Window shortcuts should NOT be added to global blocking registry74+ // Add Alt+Arrow shortcuts to global blocking registry with callback handle for C-level triggering
75- // They need to bubble up to GTK for proper handling75+ if strings.HasPrefix(key, "alt+Arrow") {
76+ RegisterGlobalShortcutWithHandle(key, handle)
77+ } else {
78+ // For non-Alt+Arrow shortcuts, just register for blocking without callback handle
79+ RegisterGlobalShortcut(key)
80+ }
76 81
77 log.Printf("[window-shortcuts] Registered global shortcut: %s -> %s", key, gtkKey)82 log.Printf("[window-shortcuts] Registered global shortcut: %s -> %s", key, gtkKey)
78 return nil83 return nil

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