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}