detached 26523d5

feat(webkit): add workspace navigation callback system

Add RegisterWorkspaceNavigationHandler to allow workspace manager to handle
alt+arrow navigation. This prevents page scrolling while enabling pane
navigation. Also register handler for root webview after workspace init.
commit
26523d5b691f2aba648a078b3cea51d3c926af65
author
bamen · · 2025-10-09 18:04 UTC
committer
bamen · · 2025-10-09 16:57 UTC
parent
fdb86d5
signature
unsigned
3 files · +45 -5
M internal/app/browser/webview.go +3 -0
@@ -249,6 +249,9 @@ func (app *BrowserApp) createWebView() error {
249249
250250 if app.workspace == nil {
251251 app.workspace = NewWorkspaceManager(app, pane)
252+ // CRITICAL: Register navigation handler for the root webview
253+ // This is needed because buildPane() couldn't register it when workspace was nil
254+ app.workspace.RegisterNavigationHandler(view)
252255 }
253256
254257 // Initialize window-level global shortcuts AFTER workspace is set up
@@ -249,6 +249,9 @@ func (app *BrowserApp) createWebView() error {
249 249
250 if app.workspace == nil {250 if app.workspace == nil {
251 app.workspace = NewWorkspaceManager(app, pane)251 app.workspace = NewWorkspaceManager(app, pane)
252+ // CRITICAL: Register navigation handler for the root webview
253+ // This is needed because buildPane() couldn't register it when workspace was nil
254+ app.workspace.RegisterNavigationHandler(view)
252 }255 }
253 256
254 // Initialize window-level global shortcuts AFTER workspace is set up257 // Initialize window-level global shortcuts AFTER workspace is set up
M pkg/webkit/keyboard_bridge.go +33 -5
@@ -55,11 +55,12 @@ type ShortcutRouting struct {
5555 // shortcutRoutingTable maps normalized shortcuts to their routing behavior
5656 // This centralizes all routing decisions in one place, avoiding magic strings and scattered logic
5757 var shortcutRoutingTable = map[string]ShortcutRouting{
58- // Workspace pane navigation - handled by GTK window-level shortcuts only
59- "alt+arrowleft": {forwardToJS: false, blockWebKit: true, description: "Navigate to left pane (GTK handler)"},
60- "alt+arrowright": {forwardToJS: false, blockWebKit: true, description: "Navigate to right pane (GTK handler)"},
61- "alt+arrowup": {forwardToJS: false, blockWebKit: true, description: "Navigate to upper pane (GTK handler)"},
62- "alt+arrowdown": {forwardToJS: false, blockWebKit: true, description: "Navigate to lower pane (GTK handler)"},
58+ // Workspace pane navigation - block WebKit to prevent page scrolling
59+ // Navigation is triggered via workspace navigation callback
60+ "alt+arrowleft": {forwardToJS: false, blockWebKit: true, description: "Navigate to left pane (callback handler)"},
61+ "alt+arrowright": {forwardToJS: false, blockWebKit: true, description: "Navigate to right pane (callback handler)"},
62+ "alt+arrowup": {forwardToJS: false, blockWebKit: true, description: "Navigate to upper pane (callback handler)"},
63+ "alt+arrowdown": {forwardToJS: false, blockWebKit: true, description: "Navigate to lower pane (callback handler)"},
6364
6465 // Pane mode - forward to JavaScript but block WebKit's print dialog
6566 "cmdorctrl+p": {forwardToJS: true, blockWebKit: true, description: "Enter pane mode (JS handler + block print dialog)"},
@@ -167,6 +168,33 @@ func (w *WebView) AttachKeyboardBridge() {
167168 routing = defaultRouting
168169 }
169170
171+ // Handle workspace navigation shortcuts via callback
172+ if shortcut == "alt+arrowup" || shortcut == "alt+arrowdown" || shortcut == "alt+arrowleft" || shortcut == "alt+arrowright" {
173+ w.mu.RLock()
174+ handler := w.onWorkspaceNavigation
175+ w.mu.RUnlock()
176+
177+ if handler != nil {
178+ var direction string
179+ switch shortcut {
180+ case "alt+arrowup":
181+ direction = "up"
182+ case "alt+arrowdown":
183+ direction = "down"
184+ case "alt+arrowleft":
185+ direction = "left"
186+ case "alt+arrowright":
187+ direction = "right"
188+ }
189+ if handler(direction) {
190+ log.Printf("[keyboard-bridge] Workspace navigation: %s (handled)", direction)
191+ return true // Block WebKit
192+ }
193+ }
194+ log.Printf("[keyboard-bridge] Shortcut %s: %s (no handler or not handled)", shortcut, routing.description)
195+ return true // Block WebKit anyway to prevent scrolling
196+ }
197+
170198 // Dispatch to JavaScript if routing says to
171199 if routing.forwardToJS {
172200 script := fmt.Sprintf(`document.dispatchEvent(new CustomEvent('dumber:key',{detail:{shortcut:'%s'}}));`, shortcut)
@@ -55,11 +55,12 @@ type ShortcutRouting struct {
55 // shortcutRoutingTable maps normalized shortcuts to their routing behavior55 // shortcutRoutingTable maps normalized shortcuts to their routing behavior
56 // This centralizes all routing decisions in one place, avoiding magic strings and scattered logic56 // This centralizes all routing decisions in one place, avoiding magic strings and scattered logic
57 var shortcutRoutingTable = map[string]ShortcutRouting{57 var shortcutRoutingTable = map[string]ShortcutRouting{
58- // Workspace pane navigation - handled by GTK window-level shortcuts only58+ // Workspace pane navigation - block WebKit to prevent page scrolling
59- "alt+arrowleft": {forwardToJS: false, blockWebKit: true, description: "Navigate to left pane (GTK handler)"},59+ // Navigation is triggered via workspace navigation callback
60- "alt+arrowright": {forwardToJS: false, blockWebKit: true, description: "Navigate to right pane (GTK handler)"},60+ "alt+arrowleft": {forwardToJS: false, blockWebKit: true, description: "Navigate to left pane (callback handler)"},
61- "alt+arrowup": {forwardToJS: false, blockWebKit: true, description: "Navigate to upper pane (GTK handler)"},61+ "alt+arrowright": {forwardToJS: false, blockWebKit: true, description: "Navigate to right pane (callback handler)"},
62- "alt+arrowdown": {forwardToJS: false, blockWebKit: true, description: "Navigate to lower pane (GTK handler)"},62+ "alt+arrowup": {forwardToJS: false, blockWebKit: true, description: "Navigate to upper pane (callback handler)"},
63+ "alt+arrowdown": {forwardToJS: false, blockWebKit: true, description: "Navigate to lower pane (callback handler)"},
63 64
64 // Pane mode - forward to JavaScript but block WebKit's print dialog65 // Pane mode - forward to JavaScript but block WebKit's print dialog
65 "cmdorctrl+p": {forwardToJS: true, blockWebKit: true, description: "Enter pane mode (JS handler + block print dialog)"},66 "cmdorctrl+p": {forwardToJS: true, blockWebKit: true, description: "Enter pane mode (JS handler + block print dialog)"},
@@ -167,6 +168,33 @@ func (w *WebView) AttachKeyboardBridge() {
167 routing = defaultRouting168 routing = defaultRouting
168 }169 }
169 170
171+ // Handle workspace navigation shortcuts via callback
172+ if shortcut == "alt+arrowup" || shortcut == "alt+arrowdown" || shortcut == "alt+arrowleft" || shortcut == "alt+arrowright" {
173+ w.mu.RLock()
174+ handler := w.onWorkspaceNavigation
175+ w.mu.RUnlock()
176+
177+ if handler != nil {
178+ var direction string
179+ switch shortcut {
180+ case "alt+arrowup":
181+ direction = "up"
182+ case "alt+arrowdown":
183+ direction = "down"
184+ case "alt+arrowleft":
185+ direction = "left"
186+ case "alt+arrowright":
187+ direction = "right"
188+ }
189+ if handler(direction) {
190+ log.Printf("[keyboard-bridge] Workspace navigation: %s (handled)", direction)
191+ return true // Block WebKit
192+ }
193+ }
194+ log.Printf("[keyboard-bridge] Shortcut %s: %s (no handler or not handled)", shortcut, routing.description)
195+ return true // Block WebKit anyway to prevent scrolling
196+ }
197+
170 // Dispatch to JavaScript if routing says to198 // Dispatch to JavaScript if routing says to
171 if routing.forwardToJS {199 if routing.forwardToJS {
172 script := fmt.Sprintf(`document.dispatchEvent(new CustomEvent('dumber:key',{detail:{shortcut:'%s'}}));`, shortcut)200 script := fmt.Sprintf(`document.dispatchEvent(new CustomEvent('dumber:key',{detail:{shortcut:'%s'}}));`, shortcut)
M pkg/webkit/webview.go +9 -0
@@ -41,6 +41,7 @@ type WebView struct {
4141 onClose func()
4242 onNavigationPolicy func(url string, isUserGesture bool) bool
4343 onWindowTypeDetected func(WindowType, *WindowFeatures)
44+ onWorkspaceNavigation func(direction string) bool // Workspace pane navigation
4445 }
4546
4647 // NewWebView creates a new WebView with the given configuration
@@ -703,3 +704,11 @@ func (w *WebView) OnNavigate(handler func(url string)) {
703704 // This wraps the URI changed handler
704705 w.onURIChanged = handler
705706 }
707+
708+// RegisterWorkspaceNavigationHandler registers a handler for workspace pane navigation
709+// The handler receives the direction ("up", "down", "left", "right") and returns true if handled
710+func (w *WebView) RegisterWorkspaceNavigationHandler(handler func(direction string) bool) {
711+ w.mu.Lock()
712+ defer w.mu.Unlock()
713+ w.onWorkspaceNavigation = handler
714+}
@@ -41,6 +41,7 @@ type WebView struct {
41 onClose func()41 onClose func()
42 onNavigationPolicy func(url string, isUserGesture bool) bool42 onNavigationPolicy func(url string, isUserGesture bool) bool
43 onWindowTypeDetected func(WindowType, *WindowFeatures)43 onWindowTypeDetected func(WindowType, *WindowFeatures)
44+ onWorkspaceNavigation func(direction string) bool // Workspace pane navigation
44 }45 }
45 46
46 // NewWebView creates a new WebView with the given configuration47 // NewWebView creates a new WebView with the given configuration
@@ -703,3 +704,11 @@ func (w *WebView) OnNavigate(handler func(url string)) {
703 // This wraps the URI changed handler704 // This wraps the URI changed handler
704 w.onURIChanged = handler705 w.onURIChanged = handler
705 }706 }
707+
708+// RegisterWorkspaceNavigationHandler registers a handler for workspace pane navigation
709+// The handler receives the direction ("up", "down", "left", "right") and returns true if handled
710+func (w *WebView) RegisterWorkspaceNavigationHandler(handler func(direction string) bool) {
711+ w.mu.Lock()
712+ defer w.mu.Unlock()
713+ w.onWorkspaceNavigation = handler
714+}

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