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