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 // Only forward shortcuts with modifiers
184 if !ctrl && !alt && !shift {
185 return false
186 }
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 == modifierCmdOrCtrl+"+"+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}