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