v0.12.1 1453d21

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	modifierCmdOrCtrl = "cmdorctrl"
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// AttachKeyboardBridge attaches an EventControllerKey to bridge keyboard events to JavaScript
98// This is critical for allowing JavaScript KeyboardService to receive keyboard events
99// that would otherwise be consumed by GTK shortcuts
100func (w *WebView) AttachKeyboardBridge() {
101	if w == nil || w.view == nil {
102		return
103	}
104
105	// Create event controller for keyboard
106	keyController := gtk.NewEventControllerKey()
107
108	// Set to capture phase so we see events before WebView processes them
109	keyController.SetPropagationPhase(gtk.PhaseCapture)
110
111	// Connect to key-pressed signal
112	keyController.ConnectKeyPressed(func(keyval, keycode uint, state gdk.ModifierType) bool {
113		// Check if pane mode is active - block ALL keys except pane mode actions
114		w.mu.RLock()
115		paneModeChecker := w.isPaneModeActive
116		w.mu.RUnlock()
117
118		isPaneModeActive := paneModeChecker != nil && paneModeChecker()
119
120		// Build normalized shortcut string
121		var parts []string
122
123		ctrl := state.Has(gdk.ControlMask)
124		alt := state.Has(gdk.AltMask)
125		shift := state.Has(gdk.ShiftMask)
126
127		// Build modifier prefix using switch on modifier combination
128		switch {
129		case ctrl && shift:
130			parts = append(parts, modifierCmdOrCtrl, modifierShift)
131		case ctrl && !shift:
132			parts = append(parts, modifierCmdOrCtrl)
133		case alt:
134			parts = append(parts, modifierAlt)
135		case shift:
136			parts = append(parts, modifierShift)
137		default:
138			// No modifiers - only forward if we match a key below
139		}
140
141		// Map keyval to key name
142		var keyName string
143		switch keyval {
144		case 'p', 'P':
145			keyName = keyP
146		case 'l', 'L':
147			keyName = keyL
148		case 'r', 'R':
149			keyName = keyR
150		case 'd', 'D':
151			keyName = keyD
152		case 'u', 'U':
153			keyName = keyU
154		case 'f', 'F':
155			keyName = keyF
156		case 'c', 'C':
157			keyName = keyC
158		case 'w', 'W':
159			keyName = keyW
160		case 'x', 'X':
161			keyName = keyX
162		case 's', 'S':
163			keyName = keyS
164		case gdkKeyLeft:
165			keyName = keyArrowLeft
166		case gdkKeyRight:
167			keyName = keyArrowRight
168		case gdkKeyUp:
169			keyName = keyArrowUp
170		case gdkKeyDown:
171			keyName = keyArrowDown
172		case gdkKeyEscape:
173			keyName = keyEscape
174		case gdkKeyF12:
175			keyName = keyF12
176		case gdkKeyPlus, gdkKeyEqual:
177			keyName = keyPlus
178		case gdkKeyMinus:
179			keyName = keyMinus
180		case '0':
181			keyName = keyZero
182		default:
183			// For unknown keys, let WebView handle them
184			// This allows web apps to receive Ctrl+C, Ctrl+V, Ctrl+A, etc.
185			return false
186		}
187
188		if keyName != "" {
189			parts = append(parts, keyName)
190		}
191
192		shortcut := ""
193		if len(parts) > 0 {
194			shortcut = parts[0]
195			for i := 1; i < len(parts); i++ {
196				shortcut += "+" + parts[i]
197			}
198		}
199
200		if shortcut != "" {
201			// Special case: Ctrl+P enters pane mode
202			if shortcut == modifierCmdOrCtrl+"+"+keyP {
203				w.mu.RLock()
204				handler := w.onPaneModeShortcut
205				w.mu.RUnlock()
206				if handler != nil && handler(actionEnter) {
207					log.Printf("[keyboard-bridge] Pane mode: enter")
208					return true
209				}
210				return true // Block even if no handler
211			}
212
213			// Special case: Escape exits active mode
214			if shortcut == keyEscape {
215				w.mu.RLock()
216				handler := w.onPaneModeShortcut
217				w.mu.RUnlock()
218				if handler != nil && handler(actionExit) {
219					log.Printf("[keyboard-bridge] Escape: exited mode")
220					return true
221				}
222				return false // Pass through to DOM if no active mode
223			}
224
225			// Check if shortcut maps to an action
226			if action, exists := shortcutActions[shortcut]; exists {
227				// Navigation actions (alt+arrow)
228				if action == directionLeft || action == directionRight || action == directionUp || action == directionDown {
229					w.mu.RLock()
230					handler := w.onWorkspaceNavigation
231					w.mu.RUnlock()
232					if handler != nil && handler(action) {
233						log.Printf("[keyboard-bridge] Navigation: %s", action)
234						return true
235					}
236					return true // Block anyway to prevent scrolling
237				}
238
239				// Pane mode actions (x, l, r, d, u, arrows)
240				w.mu.RLock()
241				handler := w.onPaneModeShortcut
242				w.mu.RUnlock()
243				if handler != nil && handler(action) {
244					log.Printf("[keyboard-bridge] Pane mode action: %s", action)
245					return true
246				}
247				return false // Not in pane mode, pass through
248			}
249
250			// Pane mode active: block all other shortcuts
251			if isPaneModeActive {
252				log.Printf("[keyboard-bridge] Blocking '%s' during pane mode", shortcut)
253				return true
254			}
255
256			// Forward to JavaScript using proper JSON marshaling to prevent any potential injection
257			shortcutJSON, err := json.Marshal(shortcut)
258			if err != nil {
259				log.Printf("[keyboard-bridge] Failed to marshal shortcut: %v", err)
260				return false
261			}
262			script := fmt.Sprintf(`document.dispatchEvent(new CustomEvent('dumber:key',{detail:{shortcut:%s}}));`, shortcutJSON)
263			if err := w.InjectScript(script); err != nil {
264				log.Printf("[keyboard-bridge] Failed to dispatch: %v", err)
265			}
266			return false // Allow WebKit to handle
267		}
268
269		// Pane mode active: block ALL non-shortcut keys
270		if isPaneModeActive {
271			log.Printf("[keyboard-bridge] Blocking key during pane mode: keyval=%d", keyval)
272			return true // Block all other keys
273		}
274
275		// No shortcut matched and not in pane mode - allow WebKit to handle normally
276		return false
277	})
278
279	// Attach controller to WebView
280	w.view.AddController(keyController)
281	log.Printf("[keyboard-bridge] EventControllerKey attached to WebView ID %d", w.id)
282}

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