v0.13.0 976176a

keyboard_bridge.go RawHistoryFolder

1package webkit
2
3import (
4	"encoding/json"
5	"fmt"
6	"log"
7
8	"github.com/diamondburned/gotk4/pkg/gdk/v4"
9	"github.com/diamondburned/gotk4/pkg/gtk/v4"
10)
11
12// Keyboard shortcut modifier string constants
13const (
14	modifierCtrl  = "ctrl"
15	modifierShift = "shift"
16	modifierAlt   = "alt"
17)
18
19// GDK key constants for special keys
20const (
21	gdkKeyLeft   = 0xff51
22	gdkKeyRight  = 0xff53
23	gdkKeyUp     = 0xff52
24	gdkKeyDown   = 0xff54
25	gdkKeyEscape = 0xff1b
26	gdkKeyF12    = 0xffbe
27	gdkKeyPlus   = 0x2b
28	gdkKeyEqual  = 0x3d
29	gdkKeyMinus  = 0x2d
30)
31
32// Key name constants for shortcut mapping
33const (
34	keyP          = "p"
35	keyL          = "l"
36	keyR          = "r"
37	keyD          = "d"
38	keyU          = "u"
39	keyF          = "f"
40	keyC          = "c"
41	keyW          = "w"
42	keyX          = "x"
43	keyS          = "s"
44	keyEscape     = "escape"
45	keyArrowLeft  = "arrowleft"
46	keyArrowRight = "arrowright"
47	keyArrowUp    = "arrowup"
48	keyArrowDown  = "arrowdown"
49	keyF12        = "F12"
50	keyPlus       = "plus"
51	keyMinus      = "minus"
52	keyZero       = "0"
53	altmod        = "alt"
54)
55
56// Pane mode action constants
57const (
58	actionClose      = "close"
59	actionSplitLeft  = "split-left"
60	actionSplitRight = "split-right"
61	actionSplitDown  = "split-down"
62	actionSplitUp    = "split-up"
63	actionStack      = "stack"
64	actionExit       = "exit"
65	actionEnter      = "enter"
66)
67
68// Navigation direction constants
69const (
70	directionLeft  = "left"
71	directionRight = "right"
72	directionUp    = "up"
73	directionDown  = "down"
74)
75
76// Map shortcuts to actions (unified for all keyboard handling)
77var shortcutActions = map[string]string{
78	// Pane mode actions (no modifier)
79	keyX:          actionClose,
80	keyS:          actionStack,
81	keyL:          actionSplitLeft,
82	keyR:          actionSplitRight,
83	keyD:          actionSplitDown,
84	keyU:          actionSplitUp,
85	keyArrowLeft:  actionSplitLeft,
86	keyArrowRight: actionSplitRight,
87	keyArrowDown:  actionSplitDown,
88	keyArrowUp:    actionSplitUp,
89
90	// Navigation (alt modifier)
91	altmod + "+" + keyArrowLeft:  directionLeft,
92	altmod + "+" + keyArrowRight: directionRight,
93	altmod + "+" + keyArrowDown:  directionDown,
94	altmod + "+" + keyArrowUp:    directionUp,
95}
96
97// TODO(#8): Refactor this function to reduce cyclomatic complexity (52 -> <30)
98//
99// AttachKeyboardBridge attaches an EventControllerKey to bridge keyboard events to JavaScript
100// This is critical for allowing JavaScript KeyboardService to receive keyboard events
101// that would otherwise be consumed by GTK shortcuts
102func (w *WebView) AttachKeyboardBridge() {
103	if w == nil || w.view == nil {
104		return
105	}
106
107	// Create event controller for keyboard
108	keyController := gtk.NewEventControllerKey()
109
110	// Set to capture phase so we see events before WebView processes them
111	keyController.SetPropagationPhase(gtk.PhaseCapture)
112
113	// Connect to key-pressed signal
114	keyController.ConnectKeyPressed(func(keyval, keycode uint, state gdk.ModifierType) bool {
115		// Check if pane mode is active - block ALL keys except pane mode actions
116		w.mu.RLock()
117		paneModeChecker := w.isPaneModeActive
118		w.mu.RUnlock()
119
120		isPaneModeActive := paneModeChecker != nil && paneModeChecker()
121
122		// Build normalized shortcut string
123		var parts []string
124
125		ctrl := state.Has(gdk.ControlMask)
126		alt := state.Has(gdk.AltMask)
127		shift := state.Has(gdk.ShiftMask)
128
129		// Build modifier prefix using switch on modifier combination
130		switch {
131		case ctrl && shift:
132			parts = append(parts, modifierCtrl, modifierShift)
133		case ctrl && !shift:
134			parts = append(parts, modifierCtrl)
135		case alt:
136			parts = append(parts, modifierAlt)
137		case shift:
138			parts = append(parts, modifierShift)
139		default:
140			// No modifiers - only forward if we match a key below
141		}
142
143		// Map keyval to key name
144		var keyName string
145		switch keyval {
146		case 'p', 'P':
147			keyName = keyP
148		case 'l', 'L':
149			keyName = keyL
150		case 'r', 'R':
151			keyName = keyR
152		case 'd', 'D':
153			keyName = keyD
154		case 'u', 'U':
155			keyName = keyU
156		case 'f', 'F':
157			keyName = keyF
158		case 'c', 'C':
159			keyName = keyC
160		case 'w', 'W':
161			keyName = keyW
162		case 'x', 'X':
163			keyName = keyX
164		case 's', 'S':
165			keyName = keyS
166		case gdkKeyLeft:
167			keyName = keyArrowLeft
168		case gdkKeyRight:
169			keyName = keyArrowRight
170		case gdkKeyUp:
171			keyName = keyArrowUp
172		case gdkKeyDown:
173			keyName = keyArrowDown
174		case gdkKeyEscape:
175			keyName = keyEscape
176		case gdkKeyF12:
177			keyName = keyF12
178		case gdkKeyPlus, gdkKeyEqual:
179			keyName = keyPlus
180		case gdkKeyMinus:
181			keyName = keyMinus
182		case '0':
183			keyName = keyZero
184		default:
185			// For unknown keys, let WebView handle them
186			// This allows web apps to receive Ctrl+C, Ctrl+V, Ctrl+A, etc.
187			return false
188		}
189
190		if keyName != "" {
191			parts = append(parts, keyName)
192		}
193
194		shortcut := ""
195		if len(parts) > 0 {
196			shortcut = parts[0]
197			for i := 1; i < len(parts); i++ {
198				shortcut += "+" + parts[i]
199			}
200		}
201
202		if shortcut != "" {
203			// Special case: Ctrl+P enters pane mode
204			if shortcut == modifierCtrl+"+"+keyP {
205				w.mu.RLock()
206				handler := w.onPaneModeShortcut
207				w.mu.RUnlock()
208				if handler != nil && handler(actionEnter) {
209					log.Printf("[keyboard-bridge] Pane mode: enter")
210					return true
211				}
212				return true // Block even if no handler
213			}
214
215			// Special case: Escape exits active mode
216			if shortcut == keyEscape {
217				w.mu.RLock()
218				handler := w.onPaneModeShortcut
219				w.mu.RUnlock()
220				if handler != nil && handler(actionExit) {
221					log.Printf("[keyboard-bridge] Escape: exited mode")
222					return true
223				}
224				return false // Pass through to DOM if no active mode
225			}
226
227			// Check if shortcut maps to an action
228			if action, exists := shortcutActions[shortcut]; exists {
229				// Navigation actions (alt+arrow)
230				if action == directionLeft || action == directionRight || action == directionUp || action == directionDown {
231					w.mu.RLock()
232					handler := w.onWorkspaceNavigation
233					w.mu.RUnlock()
234					if handler != nil && handler(action) {
235						log.Printf("[keyboard-bridge] Navigation: %s", action)
236						return true
237					}
238					return true // Block anyway to prevent scrolling
239				}
240
241				// Pane mode actions (x, l, r, d, u, arrows)
242				w.mu.RLock()
243				handler := w.onPaneModeShortcut
244				w.mu.RUnlock()
245				if handler != nil && handler(action) {
246					log.Printf("[keyboard-bridge] Pane mode action: %s", action)
247					return true
248				}
249				return false // Not in pane mode, pass through
250			}
251
252			// Pane mode active: block all other shortcuts
253			if isPaneModeActive {
254				log.Printf("[keyboard-bridge] Blocking '%s' during pane mode", shortcut)
255				return true
256			}
257
258			// Forward to JavaScript using proper JSON marshaling to prevent any potential injection
259			shortcutJSON, err := json.Marshal(shortcut)
260			if err != nil {
261				log.Printf("[keyboard-bridge] Failed to marshal shortcut: %v", err)
262				return false
263			}
264			script := fmt.Sprintf(`document.dispatchEvent(new CustomEvent('dumber:key',{detail:{shortcut:%s}}));`, shortcutJSON)
265			if err := w.InjectScript(script); err != nil {
266				log.Printf("[keyboard-bridge] Failed to dispatch: %v", err)
267			}
268			return false // Allow WebKit to handle
269		}
270
271		// Pane mode active: block ALL non-shortcut keys
272		if isPaneModeActive {
273			log.Printf("[keyboard-bridge] Blocking key during pane mode: keyval=%d", keyval)
274			return true // Block all other keys
275		}
276
277		// No shortcut matched and not in pane mode - allow WebKit to handle normally
278		return false
279	})
280
281	// Attach controller to WebView
282	w.view.AddController(keyController)
283	log.Printf("[keyboard-bridge] EventControllerKey attached to WebView ID %d", w.id)
284}

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