1// Package input provides keyboard event handling and modal input mode management.
2package input
3
4import (
5 "context"
6 "strings"
7
8 "github.com/bnema/dumber/internal/infrastructure/config"
9 "github.com/bnema/dumber/internal/logging"
10 "github.com/jwijenbergh/puregotk/v4/gdk"
11)
12
13// Modifier represents keyboard modifier flags.
14type Modifier uint
15
16const (
17 // ModNone indicates no modifier is pressed.
18 ModNone Modifier = 0
19 // ModShift indicates the Shift key is pressed.
20 ModShift Modifier = Modifier(gdk.ShiftMaskValue)
21 // ModCtrl indicates the Control key is pressed.
22 ModCtrl Modifier = Modifier(gdk.ControlMaskValue)
23 // ModAlt indicates the Alt key is pressed.
24 ModAlt Modifier = Modifier(gdk.AltMaskValue)
25)
26
27// modifierMask filters out non-standard modifiers from GDK state.
28const modifierMask = ModCtrl | ModShift | ModAlt
29
30var keyvalByName = map[string]uint{
31 "escape": uint(gdk.KEY_Escape),
32 "esc": uint(gdk.KEY_Escape),
33 "return": uint(gdk.KEY_Return),
34 "enter": uint(gdk.KEY_Return),
35 "tab": uint(gdk.KEY_Tab),
36 "space": uint(gdk.KEY_space),
37 "backspace": uint(gdk.KEY_BackSpace),
38 "delete": uint(gdk.KEY_Delete),
39 "del": uint(gdk.KEY_Delete),
40 "home": uint(gdk.KEY_Home),
41 "end": uint(gdk.KEY_End),
42 "pageup": uint(gdk.KEY_Page_Up),
43 "page_up": uint(gdk.KEY_Page_Up),
44 "pagedown": uint(gdk.KEY_Page_Down),
45 "page_down": uint(gdk.KEY_Page_Down),
46 "left": uint(gdk.KEY_Left),
47 "arrowleft": uint(gdk.KEY_Left),
48 "right": uint(gdk.KEY_Right),
49 "arrowright": uint(gdk.KEY_Right),
50 "up": uint(gdk.KEY_Up),
51 "arrowup": uint(gdk.KEY_Up),
52 "down": uint(gdk.KEY_Down),
53 "arrowdown": uint(gdk.KEY_Down),
54 "f1": uint(gdk.KEY_F1),
55 "f2": uint(gdk.KEY_F2),
56 "f3": uint(gdk.KEY_F3),
57 "f4": uint(gdk.KEY_F4),
58 "f5": uint(gdk.KEY_F5),
59 "f6": uint(gdk.KEY_F6),
60 "f7": uint(gdk.KEY_F7),
61 "f8": uint(gdk.KEY_F8),
62 "f9": uint(gdk.KEY_F9),
63 "f10": uint(gdk.KEY_F10),
64 "f11": uint(gdk.KEY_F11),
65 "f12": uint(gdk.KEY_F12),
66 "plus": uint(gdk.KEY_plus),
67 "+": uint(gdk.KEY_plus),
68 "minus": uint(gdk.KEY_minus),
69 "-": uint(gdk.KEY_minus),
70 "equal": uint(gdk.KEY_equal),
71 "=": uint(gdk.KEY_equal),
72 "0": uint(gdk.KEY_0),
73 "1": uint(gdk.KEY_1),
74 "2": uint(gdk.KEY_2),
75 "3": uint(gdk.KEY_3),
76 "4": uint(gdk.KEY_4),
77 "5": uint(gdk.KEY_5),
78 "6": uint(gdk.KEY_6),
79 "7": uint(gdk.KEY_7),
80 "8": uint(gdk.KEY_8),
81 "9": uint(gdk.KEY_9),
82
83 "bracketleft": uint(gdk.KEY_bracketleft),
84 "bracketright": uint(gdk.KEY_bracketright),
85 "[": uint(gdk.KEY_bracketleft),
86 "]": uint(gdk.KEY_bracketright),
87 "braceleft": uint(gdk.KEY_braceleft),
88 "braceright": uint(gdk.KEY_braceright),
89 "{": uint(gdk.KEY_braceleft),
90 "}": uint(gdk.KEY_braceright),
91}
92
93// KeyBinding represents a single key combination.
94type KeyBinding struct {
95 Keyval uint // GDK key value (e.g., gdk.KEY_t)
96 Modifiers Modifier // Combined modifiers
97}
98
99// Action represents what happens when a shortcut is triggered.
100type Action string
101
102// Predefined actions for the keyboard system.
103const (
104 // Mode management
105 ActionEnterTabMode Action = "enter_tab_mode"
106 ActionEnterPaneMode Action = "enter_pane_mode"
107 ActionEnterSessionMode Action = "enter_session_mode"
108 ActionEnterResizeMode Action = "enter_resize_mode"
109 ActionExitMode Action = "exit_mode"
110
111 // Tab actions (global and modal)
112 ActionNewTab Action = "new_tab"
113 ActionCloseTab Action = "close_tab"
114 ActionNextTab Action = "next_tab"
115 ActionPreviousTab Action = "previous_tab"
116 ActionRenameTab Action = "rename_tab"
117 ActionSwitchLastTab Action = "switch_last_tab" // Alt+Tab style switching
118 ActionSwitchTabIndex1 Action = "switch_tab_1"
119 ActionSwitchTabIndex2 Action = "switch_tab_2"
120 ActionSwitchTabIndex3 Action = "switch_tab_3"
121 ActionSwitchTabIndex4 Action = "switch_tab_4"
122 ActionSwitchTabIndex5 Action = "switch_tab_5"
123 ActionSwitchTabIndex6 Action = "switch_tab_6"
124 ActionSwitchTabIndex7 Action = "switch_tab_7"
125 ActionSwitchTabIndex8 Action = "switch_tab_8"
126 ActionSwitchTabIndex9 Action = "switch_tab_9"
127 ActionSwitchTabIndex10 Action = "switch_tab_10"
128
129 // Pane actions (modal)
130 ActionSplitRight Action = "split_right"
131 ActionSplitLeft Action = "split_left"
132 ActionSplitUp Action = "split_up"
133 ActionSplitDown Action = "split_down"
134 ActionClosePane Action = "close_pane"
135 ActionStackPane Action = "stack_pane"
136
137 ActionMovePaneToTab Action = "move_pane_to_tab"
138 ActionMovePaneToNextTab Action = "move_pane_to_next_tab"
139
140 ActionConsumeOrExpelLeft Action = "consume_or_expel_left"
141 ActionConsumeOrExpelRight Action = "consume_or_expel_right"
142 ActionConsumeOrExpelUp Action = "consume_or_expel_up"
143 ActionConsumeOrExpelDown Action = "consume_or_expel_down"
144
145 // Pane focus navigation
146 ActionFocusRight Action = "focus_right"
147 ActionFocusLeft Action = "focus_left"
148 ActionFocusUp Action = "focus_up"
149 ActionFocusDown Action = "focus_down"
150
151 // Resize actions (modal)
152 ActionResizeIncreaseLeft Action = "resize_increase_left"
153 ActionResizeIncreaseRight Action = "resize_increase_right"
154 ActionResizeIncreaseUp Action = "resize_increase_up"
155 ActionResizeIncreaseDown Action = "resize_increase_down"
156 ActionResizeDecreaseLeft Action = "resize_decrease_left"
157 ActionResizeDecreaseRight Action = "resize_decrease_right"
158 ActionResizeDecreaseUp Action = "resize_decrease_up"
159 ActionResizeDecreaseDown Action = "resize_decrease_down"
160 ActionResizeIncrease Action = "resize_increase"
161 ActionResizeDecrease Action = "resize_decrease"
162
163 // Stack navigation (within stacked panes)
164 ActionStackNavUp Action = "stack_nav_up"
165 ActionStackNavDown Action = "stack_nav_down"
166
167 // Page navigation
168 ActionGoBack Action = "go_back"
169 ActionGoForward Action = "go_forward"
170 ActionReload Action = "reload"
171 ActionHardReload Action = "hard_reload"
172 ActionStop Action = "stop"
173 ActionPrintPage Action = "print_page"
174
175 // Zoom
176 ActionZoomIn Action = "zoom_in"
177 ActionZoomOut Action = "zoom_out"
178 ActionZoomReset Action = "zoom_reset"
179
180 // UI
181 ActionOpenOmnibox Action = "open_omnibox"
182 ActionOpenFind Action = "open_find"
183 ActionFindNext Action = "find_next"
184 ActionFindPrev Action = "find_prev"
185 ActionCloseFind Action = "close_find"
186 ActionOpenDevTools Action = "open_devtools"
187 ActionToggleFullscreen Action = "toggle_fullscreen"
188
189 // Clipboard
190 ActionCopyURL Action = "copy_url"
191
192 // Session management
193 ActionOpenSessionManager Action = "open_session_manager"
194
195 // Application
196 ActionQuit Action = "quit"
197)
198
199// ShortcutTable maps KeyBinding to Action.
200type ShortcutTable map[KeyBinding]Action
201
202// ShortcutSet holds all shortcut tables organized by context.
203type ShortcutSet struct {
204 // Global shortcuts are always active regardless of mode.
205 Global ShortcutTable
206 // TabMode shortcuts are only active in tab mode.
207 TabMode ShortcutTable
208 // PaneMode shortcuts are only active in pane mode.
209 PaneMode ShortcutTable
210 // SessionMode shortcuts are only active in session mode.
211 SessionMode ShortcutTable
212 // ResizeMode shortcuts are only active in resize mode.
213 ResizeMode ShortcutTable
214}
215
216// NewShortcutSet creates a ShortcutSet from the configuration.
217func NewShortcutSet(ctx context.Context, cfg *config.Config) *ShortcutSet {
218 log := logging.FromContext(ctx)
219 set := &ShortcutSet{
220 Global: make(ShortcutTable),
221 TabMode: make(ShortcutTable),
222 PaneMode: make(ShortcutTable),
223 SessionMode: make(ShortcutTable),
224 ResizeMode: make(ShortcutTable),
225 }
226
227 set.buildGlobalShortcuts(ctx, cfg)
228 set.buildTabModeShortcuts(ctx, &cfg.Workspace)
229 set.buildPaneModeShortcuts(ctx, &cfg.Workspace)
230 set.buildResizeModeShortcuts(ctx, &cfg.Workspace)
231 set.buildSessionModeShortcuts(ctx, &cfg.Session)
232
233 log.Debug().
234 Int("global", len(set.Global)).
235 Int("tab", len(set.TabMode)).
236 Int("pane", len(set.PaneMode)).
237 Int("resize", len(set.ResizeMode)).
238 Int("session", len(set.SessionMode)).
239 Msg("shortcuts registered")
240
241 return set
242}
243
244// buildGlobalShortcuts populates global shortcuts from config.
245func (s *ShortcutSet) buildGlobalShortcuts(ctx context.Context, cfg *config.Config) {
246 s.registerActivationShortcuts(ctx, cfg)
247 s.registerConfiguredShortcuts(&cfg.Workspace)
248 s.registerStandardShortcuts()
249 s.registerPaneNavigationShortcuts()
250 s.registerTabSwitchShortcuts()
251}
252
253// buildTabModeShortcuts populates tab mode shortcuts from config.
254func (s *ShortcutSet) buildTabModeShortcuts(ctx context.Context, cfg *config.WorkspaceConfig) {
255 s.buildModeShortcuts(ctx, cfg.TabMode.GetKeyBindings(), s.TabMode, "tab")
256}
257
258// buildPaneModeShortcuts populates pane mode shortcuts from config.
259func (s *ShortcutSet) buildPaneModeShortcuts(ctx context.Context, cfg *config.WorkspaceConfig) {
260 s.buildModeShortcuts(ctx, cfg.PaneMode.GetKeyBindings(), s.PaneMode, "pane")
261}
262
263// buildSessionModeShortcuts populates session mode shortcuts from config.
264func (s *ShortcutSet) buildSessionModeShortcuts(ctx context.Context, cfg *config.SessionConfig) {
265 s.buildModeShortcuts(ctx, cfg.SessionMode.GetKeyBindings(), s.SessionMode, "session")
266}
267
268// buildResizeModeShortcuts populates resize mode shortcuts from config.
269func (s *ShortcutSet) buildResizeModeShortcuts(ctx context.Context, cfg *config.WorkspaceConfig) {
270 s.buildModeShortcuts(ctx, cfg.ResizeMode.GetKeyBindings(), s.ResizeMode, "resize")
271}
272
273func (s *ShortcutSet) registerActivationShortcuts(ctx context.Context, cfg *config.Config) {
274 log := logging.FromContext(ctx)
275 if binding, ok := ParseKeyString(cfg.Workspace.TabMode.ActivationShortcut); ok {
276 s.Global[binding] = ActionEnterTabMode
277 log.Trace().
278 Str("shortcut", cfg.Workspace.TabMode.ActivationShortcut).
279 Uint("keyval", binding.Keyval).
280 Uint("mod", uint(binding.Modifiers)).
281 Msg("tab mode activation registered")
282 } else {
283 log.Warn().Str("shortcut", cfg.Workspace.TabMode.ActivationShortcut).Msg("failed to parse tab mode activation shortcut")
284 }
285 if binding, ok := ParseKeyString(cfg.Workspace.PaneMode.ActivationShortcut); ok {
286 s.Global[binding] = ActionEnterPaneMode
287 log.Trace().
288 Str("shortcut", cfg.Workspace.PaneMode.ActivationShortcut).
289 Uint("keyval", binding.Keyval).
290 Uint("mod", uint(binding.Modifiers)).
291 Msg("pane mode activation registered")
292 } else {
293 log.Warn().Str("shortcut", cfg.Workspace.PaneMode.ActivationShortcut).Msg("failed to parse pane mode activation shortcut")
294 }
295 if binding, ok := ParseKeyString(cfg.Session.SessionMode.ActivationShortcut); ok {
296 s.Global[binding] = ActionEnterSessionMode
297 log.Trace().
298 Str("shortcut", cfg.Session.SessionMode.ActivationShortcut).
299 Uint("keyval", binding.Keyval).
300 Uint("mod", uint(binding.Modifiers)).
301 Msg("session mode activation registered")
302 } else {
303 log.Warn().Str("shortcut", cfg.Session.SessionMode.ActivationShortcut).Msg("failed to parse session mode activation shortcut")
304 }
305 if binding, ok := ParseKeyString(cfg.Workspace.ResizeMode.ActivationShortcut); ok {
306 s.Global[binding] = ActionEnterResizeMode
307 log.Trace().
308 Str("shortcut", cfg.Workspace.ResizeMode.ActivationShortcut).
309 Uint("keyval", binding.Keyval).
310 Uint("mod", uint(binding.Modifiers)).
311 Msg("resize mode activation registered")
312 } else {
313 log.Warn().Str("shortcut", cfg.Workspace.ResizeMode.ActivationShortcut).Msg("failed to parse resize mode activation shortcut")
314 }
315}
316
317func (s *ShortcutSet) registerConfiguredShortcuts(cfg *config.WorkspaceConfig) {
318 // Note: Ctrl+T is NOT registered globally - it enters tab mode.
319 // In tab mode, use:
320 // n = new tab
321 // x = close tab
322 // l/tab = next tab
323 // h/shift+tab = previous tab
324 // r = rename tab
325 // This follows Zellij-style modal keyboard interface.
326 //
327 // However, these standard browser shortcuts ARE global:
328 if binding, ok := ParseKeyString(cfg.Shortcuts.ClosePane); ok {
329 s.Global[binding] = ActionClosePane // Ctrl+W closes active pane
330 }
331 if binding, ok := ParseKeyString(cfg.Shortcuts.NextTab); ok {
332 s.Global[binding] = ActionNextTab // Ctrl+Tab
333 }
334 if binding, ok := ParseKeyString(cfg.Shortcuts.PreviousTab); ok {
335 s.Global[binding] = ActionPreviousTab // Ctrl+Shift+Tab
336 }
337
338 if binding, ok := ParseKeyString(cfg.Shortcuts.ConsumeOrExpelLeft); ok {
339 s.Global[binding] = ActionConsumeOrExpelLeft
340 }
341 if binding, ok := ParseKeyString(cfg.Shortcuts.ConsumeOrExpelRight); ok {
342 s.Global[binding] = ActionConsumeOrExpelRight
343 }
344 if binding, ok := ParseKeyString(cfg.Shortcuts.ConsumeOrExpelUp); ok {
345 s.Global[binding] = ActionConsumeOrExpelUp
346 }
347 if binding, ok := ParseKeyString(cfg.Shortcuts.ConsumeOrExpelDown); ok {
348 s.Global[binding] = ActionConsumeOrExpelDown
349 }
350}
351
352func (s *ShortcutSet) registerStandardShortcuts() {
353 s.Global[KeyBinding{uint(gdk.KEY_l), ModCtrl}] = ActionOpenOmnibox
354 s.Global[KeyBinding{uint(gdk.KEY_f), ModCtrl}] = ActionOpenFind
355 s.Global[KeyBinding{uint(gdk.KEY_F3), ModNone}] = ActionFindNext
356 s.Global[KeyBinding{uint(gdk.KEY_F3), ModShift}] = ActionFindPrev
357 s.Global[KeyBinding{uint(gdk.KEY_g), ModCtrl}] = ActionFindNext
358 s.Global[KeyBinding{uint(gdk.KEY_g), ModCtrl | ModShift}] = ActionFindPrev
359 s.Global[KeyBinding{uint(gdk.KEY_r), ModCtrl}] = ActionReload
360 s.Global[KeyBinding{uint('r'), ModCtrl | ModShift}] = ActionHardReload
361 s.Global[KeyBinding{uint(gdk.KEY_F5), ModNone}] = ActionReload
362 s.Global[KeyBinding{uint(gdk.KEY_F5), ModCtrl}] = ActionHardReload
363 s.Global[KeyBinding{uint(gdk.KEY_F12), ModNone}] = ActionOpenDevTools
364 s.Global[KeyBinding{uint(gdk.KEY_Left), ModCtrl}] = ActionGoBack
365 s.Global[KeyBinding{uint(gdk.KEY_Right), ModCtrl}] = ActionGoForward
366 s.Global[KeyBinding{uint(gdk.KEY_plus), ModCtrl}] = ActionZoomIn
367 s.Global[KeyBinding{uint(gdk.KEY_equal), ModCtrl}] = ActionZoomIn // Ctrl+= (no shift needed)
368 s.Global[KeyBinding{uint(gdk.KEY_minus), ModCtrl}] = ActionZoomOut
369 s.Global[KeyBinding{uint(gdk.KEY_0), ModCtrl}] = ActionZoomReset
370 s.Global[KeyBinding{uint(gdk.KEY_q), ModCtrl}] = ActionQuit
371 s.Global[KeyBinding{uint(gdk.KEY_F11), ModNone}] = ActionToggleFullscreen
372 s.Global[KeyBinding{uint('c'), ModCtrl | ModShift}] = ActionCopyURL
373 s.Global[KeyBinding{uint('p'), ModCtrl | ModShift}] = ActionPrintPage
374 // Session management - direct shortcut to open session manager
375 s.Global[KeyBinding{uint(gdk.KEY_s), ModCtrl | ModShift}] = ActionOpenSessionManager
376}
377
378func (s *ShortcutSet) registerPaneNavigationShortcuts() {
379 s.Global[KeyBinding{uint(gdk.KEY_h), ModAlt}] = ActionFocusLeft
380 s.Global[KeyBinding{uint(gdk.KEY_l), ModAlt}] = ActionFocusRight
381 s.Global[KeyBinding{uint(gdk.KEY_k), ModAlt}] = ActionFocusUp
382 s.Global[KeyBinding{uint(gdk.KEY_j), ModAlt}] = ActionFocusDown
383
384 s.Global[KeyBinding{uint(gdk.KEY_Left), ModAlt}] = ActionFocusLeft
385 s.Global[KeyBinding{uint(gdk.KEY_Right), ModAlt}] = ActionFocusRight
386 s.Global[KeyBinding{uint(gdk.KEY_Up), ModAlt}] = ActionFocusUp
387 s.Global[KeyBinding{uint(gdk.KEY_Down), ModAlt}] = ActionFocusDown
388}
389
390func (s *ShortcutSet) registerTabSwitchShortcuts() {
391 // NOTE: Alt+1-9, Alt+0, and Alt+Tab are now handled by GlobalShortcutHandler
392 // using GtkShortcutController with GTK_SHORTCUT_SCOPE_GLOBAL.
393 // This is necessary because WebKitGTK's WebView consumes these key events
394 // before they reach the EventControllerKey in capture phase.
395 //
396 // Only Alt+Shift+Tab remains here as a fallback binding.
397 s.Global[KeyBinding{uint(gdk.KEY_Tab), ModAlt | ModShift}] = ActionSwitchLastTab
398}
399
400func (s *ShortcutSet) buildModeShortcuts(ctx context.Context, bindings map[string]string, dest map[KeyBinding]Action, mode string) {
401 log := logging.FromContext(ctx)
402 var registered, parseErrors, unknownActions int
403 for key, configAction := range bindings {
404 if binding, ok := ParseKeyString(key); ok {
405 if action := mapConfigAction(configAction); action != "" {
406 dest[binding] = action
407 registered++
408 log.Trace().
409 Str("key", key).
410 Str("configAction", configAction).
411 Str("action", string(action)).
412 Uint("keyval", binding.Keyval).
413 Msg(mode + " mode shortcut registered")
414 } else {
415 unknownActions++
416 log.Warn().Str("key", key).Str("configAction", configAction).Msg("unknown config action in " + mode + " mode")
417 }
418 } else {
419 parseErrors++
420 log.Warn().Str("key", key).Msg("failed to parse " + mode + " mode key")
421 }
422 }
423 log.Debug().
424 Int("registered", registered).
425 Int("parseErrors", parseErrors).
426 Int("unknownActions", unknownActions).
427 Msg(mode + " mode shortcuts built")
428}
429
430var configActionToAction = map[string]Action{
431 // Tab actions
432 "new-tab": ActionNewTab,
433 "close-tab": ActionCloseTab,
434 "next-tab": ActionNextTab,
435 "previous-tab": ActionPreviousTab,
436 "rename-tab": ActionRenameTab,
437
438 // Pane actions
439 "split-right": ActionSplitRight,
440 "split-left": ActionSplitLeft,
441 "split-up": ActionSplitUp,
442 "split-down": ActionSplitDown,
443 "close-pane": ActionClosePane,
444 "stack-pane": ActionStackPane,
445 "move-pane-to-tab": ActionMovePaneToTab,
446 "move-pane-to-next-tab": ActionMovePaneToNextTab,
447
448 "consume-or-expel-left": ActionConsumeOrExpelLeft,
449 "consume-or-expel-right": ActionConsumeOrExpelRight,
450 "consume-or-expel-up": ActionConsumeOrExpelUp,
451 "consume-or-expel-down": ActionConsumeOrExpelDown,
452
453 // Focus navigation
454 "focus-right": ActionFocusRight,
455 "focus-left": ActionFocusLeft,
456 "focus-up": ActionFocusUp,
457 "focus-down": ActionFocusDown,
458
459 // Stack navigation
460 "stack-nav-up": ActionStackNavUp,
461 "stack-up": ActionStackNavUp,
462 "stack-nav-down": ActionStackNavDown,
463 "stack-down": ActionStackNavDown,
464
465 // Resize actions
466 "resize-increase-left": ActionResizeIncreaseLeft,
467 "resize-increase-right": ActionResizeIncreaseRight,
468 "resize-increase-up": ActionResizeIncreaseUp,
469 "resize-increase-down": ActionResizeIncreaseDown,
470 "resize-decrease-left": ActionResizeDecreaseLeft,
471 "resize-decrease-right": ActionResizeDecreaseRight,
472 "resize-decrease-up": ActionResizeDecreaseUp,
473 "resize-decrease-down": ActionResizeDecreaseDown,
474 "resize-increase": ActionResizeIncrease,
475 "resize-decrease": ActionResizeDecrease,
476
477 // Session actions
478 "session-manager": ActionOpenSessionManager,
479}
480
481// mapConfigAction maps config action names to Action constants.
482func mapConfigAction(configAction string) Action {
483 if configAction == "cancel" || configAction == "confirm" {
484 return ActionExitMode
485 }
486 return configActionToAction[configAction]
487}
488
489// ParseKeyString converts a config key string like "ctrl+t" to a KeyBinding.
490// Returns false if the string cannot be parsed.
491func ParseKeyString(s string) (KeyBinding, bool) {
492 if s == "" {
493 return KeyBinding{}, false
494 }
495
496 s = strings.TrimSpace(s)
497 if s == "" {
498 return KeyBinding{}, false
499 }
500 if s == "+" {
501 return KeyBinding{Keyval: uint(gdk.KEY_plus), Modifiers: ModNone}, true
502 }
503
504 parts := strings.Split(s, "+")
505
506 var modifiers Modifier
507 var keyPart string
508
509 for _, part := range parts {
510 part = strings.TrimSpace(part)
511 if part == "" {
512 continue
513 }
514
515 lower := strings.ToLower(part)
516 switch lower {
517 case "ctrl", "control":
518 modifiers |= ModCtrl
519 case "shift":
520 modifiers |= ModShift
521 case "alt":
522 modifiers |= ModAlt
523 default:
524 if keyPart != "" {
525 return KeyBinding{}, false
526 }
527 keyPart = part
528 }
529 }
530
531 // Allow parsing "ctrl++" / "alt+shift++" where the key is "+".
532 if keyPart == "" && strings.HasSuffix(s, "++") {
533 keyPart = "+"
534 }
535
536 if keyPart == "" {
537 return KeyBinding{}, false
538 }
539
540 // Treat uppercase single-letter keys as Shift+<letter>.
541 if len(keyPart) == 1 && keyPart[0] >= 'A' && keyPart[0] <= 'Z' {
542 modifiers |= ModShift
543 keyPart = strings.ToLower(keyPart)
544 }
545
546 keyval, ok := stringToKeyval(keyPart)
547 if !ok {
548 return KeyBinding{}, false
549 }
550
551 return KeyBinding{
552 Keyval: keyval,
553 Modifiers: modifiers,
554 }, true
555}
556
557// stringToKeyval converts a key name to its GDK keyval.
558func stringToKeyval(s string) (uint, bool) {
559 if s == "" {
560 return 0, false
561 }
562
563 if keyval, ok := keyvalByName[strings.ToLower(s)]; ok {
564 return keyval, true
565 }
566
567 // Single letter keys (a-z)
568 if len(s) == 1 && s[0] >= 'a' && s[0] <= 'z' {
569 // ASCII lowercase a=97, which matches gdk.KEY_a
570 return uint(s[0]), true
571 }
572
573 return 0, false
574}
575
576// Lookup finds an action for the given key binding in the appropriate table.
577// It first checks the mode-specific table, then falls back to global.
578func (s *ShortcutSet) Lookup(binding KeyBinding, mode Mode) (Action, bool) {
579 // Normalize the binding modifiers
580 binding.Modifiers &= modifierMask
581
582 // Check mode-specific table first
583 var modeTable ShortcutTable
584 switch mode {
585 case ModeTab:
586 modeTable = s.TabMode
587 case ModePane:
588 modeTable = s.PaneMode
589 case ModeResize:
590 modeTable = s.ResizeMode
591 case ModeSession:
592 modeTable = s.SessionMode
593 }
594
595 if modeTable != nil {
596 if action, ok := modeTable[binding]; ok {
597 return action, true
598 }
599 }
600
601 // Fall back to global
602 if action, ok := s.Global[binding]; ok {
603 return action, true
604 }
605
606 return "", false
607}
608
609// ShouldAutoExitMode returns true if the action should cause modal mode to exit.
610func ShouldAutoExitMode(action Action) bool {
611 switch action {
612 case ActionNewTab, ActionCloseTab, ActionRenameTab,
613 ActionSplitRight, ActionSplitLeft, ActionSplitUp, ActionSplitDown,
614 ActionClosePane, ActionStackPane,
615 ActionMovePaneToTab, ActionMovePaneToNextTab,
616 ActionConsumeOrExpelLeft, ActionConsumeOrExpelRight, ActionConsumeOrExpelUp, ActionConsumeOrExpelDown,
617 ActionOpenSessionManager:
618 return true
619 default:
620 return false
621 }
622}