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 ActionConsumeOrExpelLeft Action = "consume_or_expel_left"
138 ActionConsumeOrExpelRight Action = "consume_or_expel_right"
139 ActionConsumeOrExpelUp Action = "consume_or_expel_up"
140 ActionConsumeOrExpelDown Action = "consume_or_expel_down"
141
142 // Pane focus navigation
143 ActionFocusRight Action = "focus_right"
144 ActionFocusLeft Action = "focus_left"
145 ActionFocusUp Action = "focus_up"
146 ActionFocusDown Action = "focus_down"
147
148 // Resize actions (modal)
149 ActionResizeIncreaseLeft Action = "resize_increase_left"
150 ActionResizeIncreaseRight Action = "resize_increase_right"
151 ActionResizeIncreaseUp Action = "resize_increase_up"
152 ActionResizeIncreaseDown Action = "resize_increase_down"
153 ActionResizeDecreaseLeft Action = "resize_decrease_left"
154 ActionResizeDecreaseRight Action = "resize_decrease_right"
155 ActionResizeDecreaseUp Action = "resize_decrease_up"
156 ActionResizeDecreaseDown Action = "resize_decrease_down"
157 ActionResizeIncrease Action = "resize_increase"
158 ActionResizeDecrease Action = "resize_decrease"
159
160 // Stack navigation (within stacked panes)
161 ActionStackNavUp Action = "stack_nav_up"
162 ActionStackNavDown Action = "stack_nav_down"
163
164 // Page navigation
165 ActionGoBack Action = "go_back"
166 ActionGoForward Action = "go_forward"
167 ActionReload Action = "reload"
168 ActionHardReload Action = "hard_reload"
169 ActionStop Action = "stop"
170
171 // Zoom
172 ActionZoomIn Action = "zoom_in"
173 ActionZoomOut Action = "zoom_out"
174 ActionZoomReset Action = "zoom_reset"
175
176 // UI
177 ActionOpenOmnibox Action = "open_omnibox"
178 ActionOpenFind Action = "open_find"
179 ActionFindNext Action = "find_next"
180 ActionFindPrev Action = "find_prev"
181 ActionCloseFind Action = "close_find"
182 ActionOpenDevTools Action = "open_devtools"
183 ActionToggleFullscreen Action = "toggle_fullscreen"
184
185 // Clipboard
186 ActionCopyURL Action = "copy_url"
187
188 // Session management
189 ActionOpenSessionManager Action = "open_session_manager"
190
191 // Application
192 ActionQuit Action = "quit"
193)
194
195// ShortcutTable maps KeyBinding to Action.
196type ShortcutTable map[KeyBinding]Action
197
198// ShortcutSet holds all shortcut tables organized by context.
199type ShortcutSet struct {
200 // Global shortcuts are always active regardless of mode.
201 Global ShortcutTable
202 // TabMode shortcuts are only active in tab mode.
203 TabMode ShortcutTable
204 // PaneMode shortcuts are only active in pane mode.
205 PaneMode ShortcutTable
206 // SessionMode shortcuts are only active in session mode.
207 SessionMode ShortcutTable
208 // ResizeMode shortcuts are only active in resize mode.
209 ResizeMode ShortcutTable
210}
211
212// NewShortcutSet creates a ShortcutSet from the configuration.
213func NewShortcutSet(ctx context.Context, cfg *config.Config) *ShortcutSet {
214 log := logging.FromContext(ctx)
215 set := &ShortcutSet{
216 Global: make(ShortcutTable),
217 TabMode: make(ShortcutTable),
218 PaneMode: make(ShortcutTable),
219 SessionMode: make(ShortcutTable),
220 ResizeMode: make(ShortcutTable),
221 }
222
223 set.buildGlobalShortcuts(ctx, cfg)
224 set.buildTabModeShortcuts(ctx, &cfg.Workspace)
225 set.buildPaneModeShortcuts(ctx, &cfg.Workspace)
226 set.buildResizeModeShortcuts(ctx, &cfg.Workspace)
227 set.buildSessionModeShortcuts(ctx, &cfg.Session)
228
229 log.Debug().
230 Int("global", len(set.Global)).
231 Int("tab", len(set.TabMode)).
232 Int("pane", len(set.PaneMode)).
233 Int("resize", len(set.ResizeMode)).
234 Int("session", len(set.SessionMode)).
235 Msg("shortcuts registered")
236
237 return set
238}
239
240// buildGlobalShortcuts populates global shortcuts from config.
241func (s *ShortcutSet) buildGlobalShortcuts(ctx context.Context, cfg *config.Config) {
242 s.registerActivationShortcuts(ctx, cfg)
243 s.registerConfiguredShortcuts(&cfg.Workspace)
244 s.registerStandardShortcuts()
245 s.registerPaneNavigationShortcuts()
246 s.registerTabSwitchShortcuts()
247}
248
249// buildTabModeShortcuts populates tab mode shortcuts from config.
250func (s *ShortcutSet) buildTabModeShortcuts(ctx context.Context, cfg *config.WorkspaceConfig) {
251 s.buildModeShortcuts(ctx, cfg.TabMode.GetKeyBindings(), s.TabMode, "tab")
252}
253
254// buildPaneModeShortcuts populates pane mode shortcuts from config.
255func (s *ShortcutSet) buildPaneModeShortcuts(ctx context.Context, cfg *config.WorkspaceConfig) {
256 s.buildModeShortcuts(ctx, cfg.PaneMode.GetKeyBindings(), s.PaneMode, "pane")
257}
258
259// buildSessionModeShortcuts populates session mode shortcuts from config.
260func (s *ShortcutSet) buildSessionModeShortcuts(ctx context.Context, cfg *config.SessionConfig) {
261 s.buildModeShortcuts(ctx, cfg.SessionMode.GetKeyBindings(), s.SessionMode, "session")
262}
263
264// buildResizeModeShortcuts populates resize mode shortcuts from config.
265func (s *ShortcutSet) buildResizeModeShortcuts(ctx context.Context, cfg *config.WorkspaceConfig) {
266 s.buildModeShortcuts(ctx, cfg.ResizeMode.GetKeyBindings(), s.ResizeMode, "resize")
267}
268
269func (s *ShortcutSet) registerActivationShortcuts(ctx context.Context, cfg *config.Config) {
270 log := logging.FromContext(ctx)
271 if binding, ok := ParseKeyString(cfg.Workspace.TabMode.ActivationShortcut); ok {
272 s.Global[binding] = ActionEnterTabMode
273 log.Trace().
274 Str("shortcut", cfg.Workspace.TabMode.ActivationShortcut).
275 Uint("keyval", binding.Keyval).
276 Uint("mod", uint(binding.Modifiers)).
277 Msg("tab mode activation registered")
278 } else {
279 log.Warn().Str("shortcut", cfg.Workspace.TabMode.ActivationShortcut).Msg("failed to parse tab mode activation shortcut")
280 }
281 if binding, ok := ParseKeyString(cfg.Workspace.PaneMode.ActivationShortcut); ok {
282 s.Global[binding] = ActionEnterPaneMode
283 log.Trace().
284 Str("shortcut", cfg.Workspace.PaneMode.ActivationShortcut).
285 Uint("keyval", binding.Keyval).
286 Uint("mod", uint(binding.Modifiers)).
287 Msg("pane mode activation registered")
288 } else {
289 log.Warn().Str("shortcut", cfg.Workspace.PaneMode.ActivationShortcut).Msg("failed to parse pane mode activation shortcut")
290 }
291 if binding, ok := ParseKeyString(cfg.Session.SessionMode.ActivationShortcut); ok {
292 s.Global[binding] = ActionEnterSessionMode
293 log.Trace().
294 Str("shortcut", cfg.Session.SessionMode.ActivationShortcut).
295 Uint("keyval", binding.Keyval).
296 Uint("mod", uint(binding.Modifiers)).
297 Msg("session mode activation registered")
298 } else {
299 log.Warn().Str("shortcut", cfg.Session.SessionMode.ActivationShortcut).Msg("failed to parse session mode activation shortcut")
300 }
301 if binding, ok := ParseKeyString(cfg.Workspace.ResizeMode.ActivationShortcut); ok {
302 s.Global[binding] = ActionEnterResizeMode
303 log.Trace().
304 Str("shortcut", cfg.Workspace.ResizeMode.ActivationShortcut).
305 Uint("keyval", binding.Keyval).
306 Uint("mod", uint(binding.Modifiers)).
307 Msg("resize mode activation registered")
308 } else {
309 log.Warn().Str("shortcut", cfg.Workspace.ResizeMode.ActivationShortcut).Msg("failed to parse resize mode activation shortcut")
310 }
311}
312
313func (s *ShortcutSet) registerConfiguredShortcuts(cfg *config.WorkspaceConfig) {
314 // Note: Ctrl+T is NOT registered globally - it enters tab mode.
315 // In tab mode, use:
316 // n = new tab
317 // x = close tab
318 // l/tab = next tab
319 // h/shift+tab = previous tab
320 // r = rename tab
321 // This follows Zellij-style modal keyboard interface.
322 //
323 // However, these standard browser shortcuts ARE global:
324 if binding, ok := ParseKeyString(cfg.Shortcuts.ClosePane); ok {
325 s.Global[binding] = ActionClosePane // Ctrl+W closes active pane
326 }
327 if binding, ok := ParseKeyString(cfg.Shortcuts.NextTab); ok {
328 s.Global[binding] = ActionNextTab // Ctrl+Tab
329 }
330 if binding, ok := ParseKeyString(cfg.Shortcuts.PreviousTab); ok {
331 s.Global[binding] = ActionPreviousTab // Ctrl+Shift+Tab
332 }
333
334 if binding, ok := ParseKeyString(cfg.Shortcuts.ConsumeOrExpelLeft); ok {
335 s.Global[binding] = ActionConsumeOrExpelLeft
336 }
337 if binding, ok := ParseKeyString(cfg.Shortcuts.ConsumeOrExpelRight); ok {
338 s.Global[binding] = ActionConsumeOrExpelRight
339 }
340 if binding, ok := ParseKeyString(cfg.Shortcuts.ConsumeOrExpelUp); ok {
341 s.Global[binding] = ActionConsumeOrExpelUp
342 }
343 if binding, ok := ParseKeyString(cfg.Shortcuts.ConsumeOrExpelDown); ok {
344 s.Global[binding] = ActionConsumeOrExpelDown
345 }
346}
347
348func (s *ShortcutSet) registerStandardShortcuts() {
349 s.Global[KeyBinding{uint(gdk.KEY_l), ModCtrl}] = ActionOpenOmnibox
350 s.Global[KeyBinding{uint(gdk.KEY_f), ModCtrl}] = ActionOpenFind
351 s.Global[KeyBinding{uint(gdk.KEY_F3), ModNone}] = ActionFindNext
352 s.Global[KeyBinding{uint(gdk.KEY_F3), ModShift}] = ActionFindPrev
353 s.Global[KeyBinding{uint(gdk.KEY_g), ModCtrl}] = ActionFindNext
354 s.Global[KeyBinding{uint(gdk.KEY_g), ModCtrl | ModShift}] = ActionFindPrev
355 s.Global[KeyBinding{uint(gdk.KEY_r), ModCtrl}] = ActionReload
356 s.Global[KeyBinding{uint(gdk.KEY_R), ModCtrl | ModShift}] = ActionHardReload
357 s.Global[KeyBinding{uint(gdk.KEY_F5), ModNone}] = ActionReload
358 s.Global[KeyBinding{uint(gdk.KEY_F5), ModCtrl}] = ActionHardReload
359 s.Global[KeyBinding{uint(gdk.KEY_F12), ModNone}] = ActionOpenDevTools
360 s.Global[KeyBinding{uint(gdk.KEY_Left), ModAlt}] = ActionGoBack
361 s.Global[KeyBinding{uint(gdk.KEY_Right), ModAlt}] = ActionGoForward
362 s.Global[KeyBinding{uint(gdk.KEY_plus), ModCtrl}] = ActionZoomIn
363 s.Global[KeyBinding{uint(gdk.KEY_equal), ModCtrl}] = ActionZoomIn // Ctrl+= (no shift needed)
364 s.Global[KeyBinding{uint(gdk.KEY_minus), ModCtrl}] = ActionZoomOut
365 s.Global[KeyBinding{uint(gdk.KEY_0), ModCtrl}] = ActionZoomReset
366 s.Global[KeyBinding{uint(gdk.KEY_q), ModCtrl}] = ActionQuit
367 s.Global[KeyBinding{uint(gdk.KEY_F11), ModNone}] = ActionToggleFullscreen
368 s.Global[KeyBinding{uint(gdk.KEY_C), ModCtrl | ModShift}] = ActionCopyURL
369 // Session management - direct shortcut to open session manager
370 s.Global[KeyBinding{uint(gdk.KEY_s), ModCtrl | ModShift}] = ActionOpenSessionManager
371}
372
373func (s *ShortcutSet) registerPaneNavigationShortcuts() {
374 s.Global[KeyBinding{uint(gdk.KEY_h), ModAlt}] = ActionFocusLeft
375 s.Global[KeyBinding{uint(gdk.KEY_l), ModAlt}] = ActionFocusRight
376 s.Global[KeyBinding{uint(gdk.KEY_k), ModAlt}] = ActionFocusUp
377 s.Global[KeyBinding{uint(gdk.KEY_j), ModAlt}] = ActionFocusDown
378
379 s.Global[KeyBinding{uint(gdk.KEY_Up), ModAlt}] = ActionFocusUp
380 s.Global[KeyBinding{uint(gdk.KEY_Down), ModAlt}] = ActionFocusDown
381}
382
383func (s *ShortcutSet) registerTabSwitchShortcuts() {
384 // NOTE: Alt+1-9, Alt+0, and Alt+Tab are now handled by GlobalShortcutHandler
385 // using GtkShortcutController with GTK_SHORTCUT_SCOPE_GLOBAL.
386 // This is necessary because WebKitGTK's WebView consumes these key events
387 // before they reach the EventControllerKey in capture phase.
388 //
389 // Only Alt+Shift+Tab remains here as a fallback binding.
390 s.Global[KeyBinding{uint(gdk.KEY_Tab), ModAlt | ModShift}] = ActionSwitchLastTab
391}
392
393func (s *ShortcutSet) buildModeShortcuts(ctx context.Context, bindings map[string]string, dest map[KeyBinding]Action, mode string) {
394 log := logging.FromContext(ctx)
395 var registered, parseErrors, unknownActions int
396 for key, configAction := range bindings {
397 if binding, ok := ParseKeyString(key); ok {
398 if action := mapConfigAction(configAction); action != "" {
399 dest[binding] = action
400 registered++
401 log.Trace().
402 Str("key", key).
403 Str("configAction", configAction).
404 Str("action", string(action)).
405 Uint("keyval", binding.Keyval).
406 Msg(mode + " mode shortcut registered")
407 } else {
408 unknownActions++
409 log.Warn().Str("key", key).Str("configAction", configAction).Msg("unknown config action in " + mode + " mode")
410 }
411 } else {
412 parseErrors++
413 log.Warn().Str("key", key).Msg("failed to parse " + mode + " mode key")
414 }
415 }
416 log.Debug().
417 Int("registered", registered).
418 Int("parseErrors", parseErrors).
419 Int("unknownActions", unknownActions).
420 Msg(mode + " mode shortcuts built")
421}
422
423var configActionToAction = map[string]Action{
424 // Tab actions
425 "new-tab": ActionNewTab,
426 "close-tab": ActionCloseTab,
427 "next-tab": ActionNextTab,
428 "previous-tab": ActionPreviousTab,
429 "rename-tab": ActionRenameTab,
430
431 // Pane actions
432 "split-right": ActionSplitRight,
433 "split-left": ActionSplitLeft,
434 "split-up": ActionSplitUp,
435 "split-down": ActionSplitDown,
436 "close-pane": ActionClosePane,
437 "stack-pane": ActionStackPane,
438
439 "consume-or-expel-left": ActionConsumeOrExpelLeft,
440 "consume-or-expel-right": ActionConsumeOrExpelRight,
441 "consume-or-expel-up": ActionConsumeOrExpelUp,
442 "consume-or-expel-down": ActionConsumeOrExpelDown,
443
444 // Focus navigation
445 "focus-right": ActionFocusRight,
446 "focus-left": ActionFocusLeft,
447 "focus-up": ActionFocusUp,
448 "focus-down": ActionFocusDown,
449
450 // Stack navigation
451 "stack-nav-up": ActionStackNavUp,
452 "stack-up": ActionStackNavUp,
453 "stack-nav-down": ActionStackNavDown,
454 "stack-down": ActionStackNavDown,
455
456 // Resize actions
457 "resize-increase-left": ActionResizeIncreaseLeft,
458 "resize-increase-right": ActionResizeIncreaseRight,
459 "resize-increase-up": ActionResizeIncreaseUp,
460 "resize-increase-down": ActionResizeIncreaseDown,
461 "resize-decrease-left": ActionResizeDecreaseLeft,
462 "resize-decrease-right": ActionResizeDecreaseRight,
463 "resize-decrease-up": ActionResizeDecreaseUp,
464 "resize-decrease-down": ActionResizeDecreaseDown,
465 "resize-increase": ActionResizeIncrease,
466 "resize-decrease": ActionResizeDecrease,
467
468 // Session actions
469 "session-manager": ActionOpenSessionManager,
470}
471
472// mapConfigAction maps config action names to Action constants.
473func mapConfigAction(configAction string) Action {
474 if configAction == "cancel" || configAction == "confirm" {
475 return ActionExitMode
476 }
477 return configActionToAction[configAction]
478}
479
480// ParseKeyString converts a config key string like "ctrl+t" to a KeyBinding.
481// Returns false if the string cannot be parsed.
482func ParseKeyString(s string) (KeyBinding, bool) {
483 if s == "" {
484 return KeyBinding{}, false
485 }
486
487 s = strings.ToLower(strings.TrimSpace(s))
488 if s == "+" {
489 return KeyBinding{Keyval: uint(gdk.KEY_plus), Modifiers: ModNone}, true
490 }
491
492 parts := strings.Split(s, "+")
493
494 var modifiers Modifier
495 var keyPart string
496
497 for _, part := range parts {
498 part = strings.TrimSpace(part)
499 if part == "" {
500 continue
501 }
502 switch part {
503 case "ctrl", "control":
504 modifiers |= ModCtrl
505 case "shift":
506 modifiers |= ModShift
507 case "alt":
508 modifiers |= ModAlt
509 default:
510 if keyPart != "" {
511 return KeyBinding{}, false
512 }
513 keyPart = part
514 }
515 }
516
517 // Allow parsing "ctrl++" / "alt+shift++" where the key is "+".
518 if keyPart == "" && strings.HasSuffix(s, "++") {
519 keyPart = "+"
520 }
521
522 if keyPart == "" {
523 return KeyBinding{}, false
524 }
525
526 keyval, ok := stringToKeyval(keyPart)
527 if !ok {
528 return KeyBinding{}, false
529 }
530
531 return KeyBinding{
532 Keyval: keyval,
533 Modifiers: modifiers,
534 }, true
535}
536
537// stringToKeyval converts a key name to its GDK keyval.
538func stringToKeyval(s string) (uint, bool) {
539 if keyval, ok := keyvalByName[s]; ok {
540 return keyval, true
541 }
542
543 // Single letter keys (a-z)
544 if len(s) == 1 && s[0] >= 'a' && s[0] <= 'z' {
545 // ASCII lowercase a=97, which matches gdk.KEY_a
546 return uint(s[0]), true
547 }
548
549 return 0, false
550}
551
552// Lookup finds an action for the given key binding in the appropriate table.
553// It first checks the mode-specific table, then falls back to global.
554func (s *ShortcutSet) Lookup(binding KeyBinding, mode Mode) (Action, bool) {
555 // Normalize the binding modifiers
556 binding.Modifiers &= modifierMask
557
558 // Check mode-specific table first
559 var modeTable ShortcutTable
560 switch mode {
561 case ModeTab:
562 modeTable = s.TabMode
563 case ModePane:
564 modeTable = s.PaneMode
565 case ModeResize:
566 modeTable = s.ResizeMode
567 case ModeSession:
568 modeTable = s.SessionMode
569 }
570
571 if modeTable != nil {
572 if action, ok := modeTable[binding]; ok {
573 return action, true
574 }
575 }
576
577 // Fall back to global
578 if action, ok := s.Global[binding]; ok {
579 return action, true
580 }
581
582 return "", false
583}
584
585// ShouldAutoExitMode returns true if the action should cause modal mode to exit.
586func ShouldAutoExitMode(action Action) bool {
587 switch action {
588 case ActionNewTab, ActionCloseTab, ActionRenameTab,
589 ActionSplitRight, ActionSplitLeft, ActionSplitUp, ActionSplitDown,
590 ActionClosePane, ActionStackPane,
591 ActionConsumeOrExpelLeft, ActionConsumeOrExpelRight, ActionConsumeOrExpelUp, ActionConsumeOrExpelDown,
592 ActionOpenSessionManager:
593 return true
594 default:
595 return false
596 }
597}