1// Package input provides keyboard event handling and modal input mode management.
2package input
3
4import (
5 "context"
6 "fmt"
7 "strings"
8 "time"
9
10 "github.com/bnema/dumber/internal/domain/entity"
11 "github.com/bnema/dumber/internal/logging"
12 "github.com/bnema/puregotk/v4/gdk"
13 "github.com/bnema/puregotk/v4/glib"
14 "github.com/bnema/puregotk/v4/gtk"
15 "github.com/rs/zerolog"
16)
17
18// GlobalShortcutHandler manages keyboard shortcuts that must work globally,
19// even when WebView has focus. It uses GtkShortcutController with GTK_SHORTCUT_SCOPE_GLOBAL
20// to intercept shortcuts before they reach the WebView.
21type GlobalShortcutHandler struct {
22 controller *gtk.ShortcutController
23 releaseController *gtk.EventControllerKey
24 window *gtk.ApplicationWindow
25 kbHandler *KeyboardHandler
26 onAction ActionHandler
27 ctx context.Context
28 registered map[KeyBinding]Action
29 lastDispatchAt map[Action]time.Time
30 heldShortcuts map[globalShortcutHoldKey]struct{}
31 keyReleasedCb func(gtk.EventControllerKey, uint, uint, gdk.ModifierType)
32 keyReleasedHandlerID uint
33 // generation is mutated only from the GTK main thread alongside controller replacement.
34 generation uint64
35
36 // Keep references to callbacks to prevent GC from collecting them
37 callbacks []gtk.ShortcutFunc
38}
39
40// NewGlobalShortcutHandler creates a new global shortcut handler for shortcuts
41// that need to work even when WebView has focus (like Alt+1-9 for tab switching).
42func NewGlobalShortcutHandler(
43 ctx context.Context,
44 window *gtk.ApplicationWindow,
45 workspace *entity.WorkspaceConfig,
46 session *entity.SessionConfig,
47 kbHandler *KeyboardHandler,
48 onAction ActionHandler,
49) *GlobalShortcutHandler {
50 log := logging.FromContext(ctx)
51 log.Debug().Msg("creating global shortcut handler")
52
53 h := &GlobalShortcutHandler{
54 controller: gtk.NewShortcutController(),
55 releaseController: gtk.NewEventControllerKey(),
56 window: window,
57 kbHandler: kbHandler,
58 onAction: onAction,
59 ctx: ctx,
60 callbacks: make([]gtk.ShortcutFunc, 0),
61 registered: make(map[KeyBinding]Action),
62 lastDispatchAt: make(map[Action]time.Time),
63 heldShortcuts: make(map[globalShortcutHoldKey]struct{}),
64 }
65
66 if h.controller == nil {
67 log.Error().Msg("failed to create shortcut controller")
68 return nil
69 }
70 if h.releaseController == nil {
71 log.Error().Msg("failed to create global shortcut release controller")
72 return nil
73 }
74
75 h.configureGlobalControllers()
76 h.registerDefaultGlobalShortcuts(log)
77
78 if workspace != nil {
79 actionMap := globalShortcutActionMap()
80
81 for actionName, actionBinding := range workspace.Shortcuts.Actions {
82 action, ok := actionMap[actionName]
83 if !ok {
84 continue
85 }
86 for _, keyStr := range actionBinding.Keys {
87 binding, ok := ParseKeyString(keyStr)
88 if !ok {
89 log.Warn().Str("shortcut", keyStr).Str("action", string(action)).Msg("failed to parse global shortcut")
90 continue
91 }
92 if h.registerShortcut(binding.Keyval, gdk.ModifierType(binding.Modifiers), action) {
93 log.Trace().Str("shortcut", keyStr).Str("action", string(action)).Msg("registered global shortcut")
94 }
95 }
96 }
97
98 occupied := make(map[KeyBinding]Action, len(h.registered))
99 for binding, action := range h.registered {
100 occupied[binding] = action
101 }
102 for _, shortcut := range collectFloatingProfileShortcutsFromWorkspace(ctx, workspace, occupied) {
103 if !h.registerShortcut(shortcut.Binding.Keyval, gdk.ModifierType(shortcut.Binding.Modifiers), shortcut.Action) {
104 continue
105 }
106 if url, ok := ParseFloatingProfileAction(shortcut.Action); ok {
107 log.Trace().
108 Str("shortcut", formatBinding(shortcut.Binding)).
109 Str("url", url).
110 Msg("registered floating profile global shortcut")
111 }
112 }
113
114 // Register all app-reserved shortcuts (mode activations, Ctrl+L, Ctrl+F, etc.)
115 // so they work even when WebView has focus.
116 shortcuts := NewShortcutSet(ctx, workspace, session)
117 for binding, action := range shortcuts.Global {
118 if _, exists := h.registered[binding]; exists {
119 continue
120 }
121 h.registerShortcut(binding.Keyval, gdk.ModifierType(binding.Modifiers), action)
122 }
123 }
124
125 // Attach to window
126 window.AddController(&h.releaseController.EventController)
127 window.AddController(&h.controller.EventController)
128
129 log.Debug().
130 Int("shortcuts", len(h.callbacks)).
131 Msg("global shortcut handler created and attached")
132
133 return h
134}
135
136func (h *GlobalShortcutHandler) configureGlobalControllers() {
137 // Set global scope - this is the key to making shortcuts work
138 // even when WebView has focus.
139 h.controller.SetScope(gtk.ShortcutScopeGlobalValue)
140 h.releaseController.SetPropagationPhase(gtk.PhaseCaptureValue)
141 h.keyReleasedCb = func(_ gtk.EventControllerKey, keyval uint, keycode uint, _ gdk.ModifierType) {
142 h.releaseHeldGlobalShortcuts(keyval, keycode)
143 }
144 h.keyReleasedHandlerID = h.releaseController.ConnectKeyReleased(&h.keyReleasedCb)
145}
146
147func (h *GlobalShortcutHandler) registerDefaultGlobalShortcuts(log *zerolog.Logger) {
148 // Register Alt+1 through Alt+9 for tab switching.
149 tabActions := []Action{
150 ActionSwitchTabIndex1,
151 ActionSwitchTabIndex2,
152 ActionSwitchTabIndex3,
153 ActionSwitchTabIndex4,
154 ActionSwitchTabIndex5,
155 ActionSwitchTabIndex6,
156 ActionSwitchTabIndex7,
157 ActionSwitchTabIndex8,
158 ActionSwitchTabIndex9,
159 }
160
161 for i, action := range tabActions {
162 keyval := uint(gdk.KEY_1) + uint(i) // KEY_1, KEY_2, ..., KEY_9
163 h.registerShortcut(keyval, gdk.AltMaskValue, action)
164 log.Trace().
165 Uint("keyval", keyval).
166 Str("action", string(action)).
167 Msg("registered global shortcut")
168 }
169
170 // Alt+0 for tab 10.
171 h.registerShortcut(uint(gdk.KEY_0), gdk.AltMaskValue, ActionSwitchTabIndex10)
172 log.Trace().
173 Uint("keyval", uint(gdk.KEY_0)).
174 Str("action", string(ActionSwitchTabIndex10)).
175 Msg("registered global shortcut")
176
177 // Alt+Tab for switching to last active tab.
178 h.registerShortcut(uint(gdk.KEY_Tab), gdk.AltMaskValue, ActionSwitchLastTab)
179 log.Trace().
180 Uint("keyval", uint(gdk.KEY_Tab)).
181 Str("action", string(ActionSwitchLastTab)).
182 Msg("registered global shortcut")
183
184 // Ctrl+Shift+S for direct session manager access (needs global scope for WebView focus).
185 h.registerShortcut(uint(gdk.KEY_s), gdk.ControlMaskValue|gdk.ShiftMaskValue, ActionOpenSessionManager)
186 log.Trace().
187 Uint("keyval", uint(gdk.KEY_s)).
188 Str("action", string(ActionOpenSessionManager)).
189 Msg("registered global shortcut")
190}
191
192// registerShortcut creates and registers a single shortcut with the controller.
193func (h *GlobalShortcutHandler) registerShortcut(keyval uint, modifiers gdk.ModifierType, action Action) bool {
194 binding := KeyBinding{Keyval: keyval, Modifiers: Modifier(modifiers) & modifierMask}
195 if existing, exists := h.registered[binding]; exists {
196 logging.FromContext(h.ctx).Warn().
197 Str("existing_action", string(existing)).
198 Str("new_action", string(action)).
199 Str("shortcut", formatBinding(binding)).
200 Msg("global shortcut conflict, skipping")
201 return false
202 }
203
204 // Create trigger for this key combination
205 trigger := gtk.NewKeyvalTrigger(keyval, modifiers)
206 if trigger == nil {
207 logging.FromContext(h.ctx).Error().
208 Uint("keyval", keyval).
209 Msg("failed to create keyval trigger")
210 return false
211 }
212
213 // Create callback action.
214 // Capture the generation so callbacks from removed/reloaded controllers cannot
215 // dispatch one-shot actions after a shortcut reload.
216 actionToDispatch := action
217 generation := h.generation
218 bindingForLog := binding
219 callback := gtk.ShortcutFunc(func(_ uintptr, _ *glib.Variant, _ uintptr) bool {
220 log := logging.FromContext(h.ctx)
221 if h.isStaleGeneration(generation) {
222 log.Trace().
223 Str("action", string(actionToDispatch)).
224 Str("shortcut", formatBinding(bindingForLog)).
225 Uint64("callback_generation", generation).
226 Uint64("current_generation", h.generation).
227 Msg("stale global shortcut callback ignored")
228 return true
229 }
230 if !h.isActiveWindowShortcutHandler() {
231 log.Trace().
232 Str("action", string(actionToDispatch)).
233 Str("shortcut", formatBinding(bindingForLog)).
234 Msg("inactive window global shortcut callback ignored")
235 return false
236 }
237 eventInfo := h.inspectCurrentShortcutEvent()
238 if !shouldDispatchGlobalShortcutEvent(eventInfo) {
239 appendGlobalShortcutEventFields(log.Trace(), bindingForLog, actionToDispatch, eventInfo).
240 Msg("global shortcut ignored without current key event")
241 return true
242 }
243 if h.suppressHeldShortcut(actionToDispatch, eventInfo) {
244 log.Trace().
245 Str("action", string(actionToDispatch)).
246 Str("shortcut", formatBinding(bindingForLog)).
247 Msg("held global shortcut repeat suppressed")
248 return true
249 }
250 if h.suppressRepeatedShortcut(actionToDispatch, time.Now()) {
251 log.Trace().
252 Str("action", string(actionToDispatch)).
253 Str("shortcut", formatBinding(bindingForLog)).
254 Msg("global shortcut repeat suppressed")
255 return true
256 }
257 appendGlobalShortcutEventFields(log.Debug(), bindingForLog, actionToDispatch, eventInfo).
258 Msg("global shortcut triggered")
259
260 // Mode-enter/exit actions go through KeyboardHandler for modal state
261 if isModeAction(actionToDispatch) {
262 if h.kbHandler != nil {
263 h.kbHandler.DispatchAction(actionToDispatch)
264 return true
265 }
266 log.Warn().
267 Str("action", string(actionToDispatch)).
268 Msg("mode action triggered but keyboard handler not set, falling through to default handler")
269 }
270
271 if h.onAction != nil {
272 if err := h.onAction(h.ctx, actionToDispatch); err != nil {
273 log.Error().
274 Err(err).
275 Str("action", string(actionToDispatch)).
276 Msg("global shortcut action failed")
277 }
278 }
279 return true // Event consumed
280 })
281
282 // Store callback reference to prevent GC
283 h.callbacks = append(h.callbacks, callback)
284
285 // Create the shortcut action
286 shortcutAction := gtk.NewCallbackAction(&callback, 0, nil)
287 if shortcutAction == nil {
288 logging.FromContext(h.ctx).Error().
289 Uint("keyval", keyval).
290 Msg("failed to create callback action")
291 return false
292 }
293
294 // Create the shortcut combining trigger and action
295 shortcut := gtk.NewShortcut(&trigger.ShortcutTrigger, &shortcutAction.ShortcutAction)
296 if shortcut == nil {
297 logging.FromContext(h.ctx).Error().
298 Uint("keyval", keyval).
299 Msg("failed to create shortcut")
300 return false
301 }
302
303 // Add to controller
304 h.controller.AddShortcut(shortcut)
305 h.registered[binding] = action
306 return true
307}
308
309// globalShortcutActionMap returns a fresh map of workspace action names to global shortcut actions.
310type globalShortcutHoldKey struct {
311 keyval uint
312 keycode uint
313}
314
315type globalShortcutEventInfo struct {
316 hasCurrentEvent bool
317 eventType gdk.EventType
318 eventKeyval uint
319 eventKeycode uint
320 eventLayout uint
321 eventLevel uint
322 eventState gdk.ModifierType
323 eventConsumedModifiers gdk.ModifierType
324 eventTime uint32
325 hasDevice bool
326 deviceName string
327 deviceSource gdk.InputSource
328 controllerState gdk.ModifierType
329 controllerTime uint32
330}
331
332func (h *GlobalShortcutHandler) inspectCurrentShortcutEvent() globalShortcutEventInfo {
333 var info globalShortcutEventInfo
334 if h == nil || h.controller == nil {
335 return info
336 }
337
338 info.controllerState = h.controller.GetCurrentEventState()
339 info.controllerTime = h.controller.GetCurrentEventTime()
340 if device := h.controller.GetCurrentEventDevice(); device != nil {
341 info.hasDevice = true
342 info.deviceName = device.GetName()
343 info.deviceSource = device.GetSource()
344 }
345
346 event := h.controller.GetCurrentEvent()
347 if event == nil {
348 return info
349 }
350 info.hasCurrentEvent = true
351 info.eventType = event.GetEventType()
352 info.eventState = event.GetModifierState()
353 info.eventTime = event.GetTime()
354 if !info.hasDevice {
355 if device := event.GetDevice(); device != nil {
356 info.hasDevice = true
357 info.deviceName = device.GetName()
358 info.deviceSource = device.GetSource()
359 }
360 }
361
362 switch info.eventType {
363 case gdk.KeyPressValue, gdk.KeyReleaseValue:
364 keyEvent := gdk.KeyEventNewFromInternalPtr(event.GoPointer())
365 info.eventKeyval = keyEvent.GetKeyval()
366 info.eventKeycode = keyEvent.GetKeycode()
367 info.eventLayout = keyEvent.GetLayout()
368 info.eventLevel = keyEvent.GetLevel()
369 info.eventConsumedModifiers = keyEvent.GetConsumedModifiers()
370 }
371
372 return info
373}
374
375func shouldDispatchGlobalShortcutEvent(info globalShortcutEventInfo) bool {
376 return info.hasCurrentEvent && info.eventType == gdk.KeyPressValue
377}
378
379func appendGlobalShortcutEventFields(evt *zerolog.Event, binding KeyBinding, action Action, info globalShortcutEventInfo) *zerolog.Event {
380 if evt == nil {
381 return nil
382 }
383 evt = evt.
384 Str("action", string(action)).
385 Str("shortcut", formatBinding(binding)).
386 Uint("shortcut_keyval", binding.Keyval).
387 Str("shortcut_key", formatKeyval(binding.Keyval)).
388 Str("shortcut_modifiers", formatModifierMask(gdk.ModifierType(binding.Modifiers))).
389 Str("controller_modifiers", formatModifierMask(info.controllerState)).
390 Uint32("controller_event_time", info.controllerTime).
391 Bool("has_current_event", info.hasCurrentEvent)
392 if info.hasCurrentEvent {
393 evt = evt.
394 Str("event_type", formatEventType(info.eventType)).
395 Int("event_type_value", int(info.eventType)).
396 Str("event_modifiers", formatModifierMask(info.eventState)).
397 Uint32("event_time", info.eventTime)
398 if info.eventKeyval != 0 {
399 evt = evt.
400 Uint("event_keyval", info.eventKeyval).
401 Str("event_key", formatKeyval(info.eventKeyval))
402 }
403 if info.eventKeycode != 0 {
404 evt = evt.Uint("event_keycode", info.eventKeycode)
405 }
406 if info.eventLayout != 0 {
407 evt = evt.Uint("event_layout", info.eventLayout)
408 }
409 if info.eventLevel != 0 {
410 evt = evt.Uint("event_level", info.eventLevel)
411 }
412 if info.eventConsumedModifiers != 0 {
413 evt = evt.Str("event_consumed_modifiers", formatModifierMask(info.eventConsumedModifiers))
414 }
415 }
416 if info.hasDevice {
417 evt = evt.
418 Str("event_device", info.deviceName).
419 Int("event_device_source", int(info.deviceSource))
420 }
421 return evt
422}
423
424func globalShortcutActionMap() map[string]Action {
425 return map[string]Action{
426 "toggle_floating_pane": ActionToggleFloatingPane,
427 "toggle-floating-pane": ActionToggleFloatingPane,
428 "consume_or_expel_left": ActionConsumeOrExpelLeft,
429 "consume-or-expel-left": ActionConsumeOrExpelLeft,
430 "consume_or_expel_right": ActionConsumeOrExpelRight,
431 "consume-or-expel-right": ActionConsumeOrExpelRight,
432 "consume_or_expel_up": ActionConsumeOrExpelUp,
433 "consume-or-expel-up": ActionConsumeOrExpelUp,
434 "consume_or_expel_down": ActionConsumeOrExpelDown,
435 "consume-or-expel-down": ActionConsumeOrExpelDown,
436 "toggle_history_systemview": ActionToggleHistorySystemView,
437 "toggle-history-systemview": ActionToggleHistorySystemView,
438 "toggle_favorites_systemview": ActionToggleFavoritesSystemView,
439 "toggle-favorites-systemview": ActionToggleFavoritesSystemView,
440 "toggle_config_systemview": ActionToggleConfigSystemView,
441 "toggle-config-systemview": ActionToggleConfigSystemView,
442 }
443}
444
445// ReloadShortcuts rebuilds global shortcuts from a new config.
446// It replaces the GTK shortcut controller to ensure stale shortcuts
447// are removed.
448func (h *GlobalShortcutHandler) ReloadShortcuts(ctx context.Context, workspace *entity.WorkspaceConfig, session *entity.SessionConfig) {
449 log := logging.FromContext(ctx)
450 h.ctx = ctx
451 h.generation++
452
453 if h.window == nil || h.controller == nil {
454 return
455 }
456
457 // Remove old controller from window
458 h.window.RemoveController(&h.controller.EventController)
459
460 // Create fresh controller
461 h.controller = gtk.NewShortcutController()
462 if h.controller == nil {
463 log.Error().Msg("failed to create shortcut controller during reload")
464 return
465 }
466 h.controller.SetScope(gtk.ShortcutScopeGlobalValue)
467 h.callbacks = make([]gtk.ShortcutFunc, 0)
468 h.registered = make(map[KeyBinding]Action)
469 h.lastDispatchAt = make(map[Action]time.Time)
470 h.heldShortcuts = make(map[globalShortcutHoldKey]struct{})
471
472 h.registerDefaultGlobalShortcuts(log)
473
474 // Re-register config-driven shortcuts
475 if workspace != nil {
476 actionMap := globalShortcutActionMap()
477
478 for actionName, actionBinding := range workspace.Shortcuts.Actions {
479 action, ok := actionMap[actionName]
480 if !ok {
481 continue
482 }
483 for _, keyStr := range actionBinding.Keys {
484 binding, ok := ParseKeyString(keyStr)
485 if !ok {
486 continue
487 }
488 h.registerShortcut(binding.Keyval, gdk.ModifierType(binding.Modifiers), action)
489 }
490 }
491
492 occupied := make(map[KeyBinding]Action, len(h.registered))
493 for binding, action := range h.registered {
494 occupied[binding] = action
495 }
496 for _, shortcut := range collectFloatingProfileShortcutsFromWorkspace(ctx, workspace, occupied) {
497 if h.registerShortcut(shortcut.Binding.Keyval, gdk.ModifierType(shortcut.Binding.Modifiers), shortcut.Action) {
498 if url, ok := ParseFloatingProfileAction(shortcut.Action); ok {
499 log.Trace().Str("shortcut", formatBinding(shortcut.Binding)).Str("url", url).Msg("registered floating profile global shortcut")
500 }
501 }
502 }
503
504 shortcuts := NewShortcutSet(ctx, workspace, session)
505 for binding, action := range shortcuts.Global {
506 if _, exists := h.registered[binding]; exists {
507 continue
508 }
509 h.registerShortcut(binding.Keyval, gdk.ModifierType(binding.Modifiers), action)
510 }
511 }
512
513 // Attach new controller to window
514 h.window.AddController(&h.controller.EventController)
515
516 log.Debug().
517 Int("shortcuts", len(h.callbacks)).
518 Msg("global shortcuts reloaded")
519}
520
521// Detach removes the global shortcut handler from the window.
522// Note: GTK handles cleanup when the widget is destroyed,
523// but we clear our references here.
524func (h *GlobalShortcutHandler) Detach() {
525 h.generation++
526 if h.window != nil && h.controller != nil {
527 h.window.RemoveController(&h.controller.EventController)
528 }
529 if h.releaseController != nil && h.keyReleasedHandlerID != 0 {
530 h.releaseController.DisconnectSignal(h.keyReleasedHandlerID)
531 }
532 if h.window != nil && h.releaseController != nil {
533 h.window.RemoveController(&h.releaseController.EventController)
534 }
535 h.controller = nil
536 h.releaseController = nil
537 h.keyReleasedCb = nil
538 h.keyReleasedHandlerID = 0
539 h.callbacks = nil
540 h.registered = nil
541 h.lastDispatchAt = nil
542 h.heldShortcuts = nil
543}
544
545const globalShortcutRepeatSuppressWindow = 250 * time.Millisecond
546
547func (h *GlobalShortcutHandler) isStaleGeneration(generation uint64) bool {
548 return h == nil || generation != h.generation
549}
550
551func (h *GlobalShortcutHandler) isActiveWindowShortcutHandler() bool {
552 return h != nil && h.controller != nil && h.window != nil && h.window.IsActive()
553}
554
555func (h *GlobalShortcutHandler) suppressHeldShortcut(action Action, info globalShortcutEventInfo) bool {
556 if h == nil || !isHeldGlobalShortcutSuppressed(action) || !info.hasCurrentEvent {
557 return false
558 }
559 if h.heldShortcuts == nil {
560 // Defensive: if hold tracking is unavailable during detach/reload,
561 // suppress rather than risk dispatching a stale global callback.
562 return true
563 }
564 key := globalShortcutHoldKey{keyval: normalizeKeyval(info.eventKeyval), keycode: info.eventKeycode}
565 if key.keyval == 0 && key.keycode == 0 {
566 return false
567 }
568 if _, ok := h.heldShortcuts[key]; ok {
569 return true
570 }
571 h.heldShortcuts[key] = struct{}{}
572 return false
573}
574
575func (h *GlobalShortcutHandler) releaseHeldGlobalShortcuts(keyval, keycode uint) {
576 if h == nil || len(h.heldShortcuts) == 0 {
577 return
578 }
579 keyval = normalizeKeyval(keyval)
580 for key := range h.heldShortcuts {
581 if (keyval != 0 && key.keyval == keyval) || (keycode != 0 && key.keycode == keycode) {
582 delete(h.heldShortcuts, key)
583 }
584 }
585}
586
587func (h *GlobalShortcutHandler) suppressRepeatedShortcut(action Action, now time.Time) bool {
588 if h == nil {
589 return false
590 }
591 if !isRepeatedGlobalShortcutSuppressed(action) {
592 if _, ok := ParseFloatingProfileTarget(action); !ok {
593 return false
594 }
595 }
596 if h.lastDispatchAt == nil {
597 // Detached/reloading handlers clear dispatch state; consume one-shot shortcuts
598 // rather than letting a stale GTK callback repeat destructive actions.
599 return true
600 }
601 if last, ok := h.lastDispatchAt[action]; ok && now.Sub(last) < globalShortcutRepeatSuppressWindow {
602 return true
603 }
604 h.lastDispatchAt[action] = now
605 return false
606}
607
608func isHeldGlobalShortcutSuppressed(action Action) bool {
609 if isRepeatedGlobalShortcutSuppressed(action) || isModeAction(action) {
610 return true
611 }
612 _, ok := ParseFloatingProfileTarget(action)
613 return ok
614}
615
616func isRepeatedGlobalShortcutSuppressed(action Action) bool {
617 switch action {
618 case ActionGoBack,
619 ActionGoForward,
620 ActionZoomReset,
621 ActionReload,
622 ActionHardReload,
623 ActionPrintPage,
624 ActionOpenOmnibox,
625 ActionOpenFind,
626 ActionFindNext,
627 ActionFindPrev,
628 ActionOpenDevTools,
629 ActionToggleFullscreen,
630 ActionToggleFloatingPane,
631 ActionToggleHistorySystemView,
632 ActionToggleFavoritesSystemView,
633 ActionToggleConfigSystemView,
634 ActionCopyURL,
635 ActionConsumeOrExpelLeft,
636 ActionConsumeOrExpelRight,
637 ActionConsumeOrExpelUp,
638 ActionConsumeOrExpelDown,
639 ActionClosePane,
640 ActionCloseTab,
641 ActionQuit,
642 ActionOpenSessionManager,
643 ActionSwitchTabIndex1,
644 ActionSwitchTabIndex2,
645 ActionSwitchTabIndex3,
646 ActionSwitchTabIndex4,
647 ActionSwitchTabIndex5,
648 ActionSwitchTabIndex6,
649 ActionSwitchTabIndex7,
650 ActionSwitchTabIndex8,
651 ActionSwitchTabIndex9,
652 ActionSwitchTabIndex10,
653 ActionSwitchLastTab:
654 return true
655 default:
656 return false
657 }
658}
659
660func formatBinding(binding KeyBinding) string {
661 parts := make([]string, 0, 3)
662 if binding.Modifiers&ModCtrl != 0 {
663 parts = append(parts, "ctrl")
664 }
665 if binding.Modifiers&ModShift != 0 {
666 parts = append(parts, "shift")
667 }
668 if binding.Modifiers&ModAlt != 0 {
669 parts = append(parts, "alt")
670 }
671 parts = append(parts, formatKeyval(binding.Keyval))
672 return strings.Join(parts, "+")
673}
674
675func formatKeyval(keyval uint) string {
676 if keyval == 0 {
677 return ""
678 }
679 keyName := gdk.KeyvalName(keyval)
680 if keyName == "" {
681 return fmt.Sprintf("0x%x", keyval)
682 }
683 return strings.ToLower(keyName)
684}
685
686func formatModifierMask(modifiers gdk.ModifierType) string {
687 if modifiers == 0 {
688 return "none"
689 }
690 parts := make([]string, 0, 4)
691 if modifiers&gdk.ControlMaskValue != 0 {
692 parts = append(parts, "ctrl")
693 }
694 if modifiers&gdk.ShiftMaskValue != 0 {
695 parts = append(parts, "shift")
696 }
697 if modifiers&gdk.AltMaskValue != 0 {
698 parts = append(parts, "alt")
699 }
700 remaining := modifiers &^ (gdk.ControlMaskValue | gdk.ShiftMaskValue | gdk.AltMaskValue)
701 if remaining != 0 {
702 parts = append(parts, fmt.Sprintf("0x%x", uint(remaining)))
703 }
704 return strings.Join(parts, "+")
705}
706
707func formatEventType(eventType gdk.EventType) string {
708 switch eventType {
709 case gdk.KeyPressValue:
710 return "key-press"
711 case gdk.KeyReleaseValue:
712 return "key-release"
713 case gdk.ButtonPressValue:
714 return "button-press"
715 case gdk.ButtonReleaseValue:
716 return "button-release"
717 case gdk.MotionNotifyValue:
718 return "motion"
719 case gdk.ScrollValue:
720 return "scroll"
721 case gdk.TouchBeginValue:
722 return "touch-begin"
723 case gdk.TouchUpdateValue:
724 return "touch-update"
725 case gdk.TouchEndValue:
726 return "touch-end"
727 case gdk.FocusChangeValue:
728 return "focus-change"
729 case gdk.DeleteValue:
730 return "delete"
731 default:
732 return fmt.Sprintf("event-%d", int(eventType))
733 }
734}
735
736func isModeAction(action Action) bool {
737 switch action {
738 case ActionEnterTabMode, ActionEnterPaneMode, ActionEnterSessionMode, ActionEnterResizeMode, ActionExitMode:
739 return true
740 default:
741 return false
742 }
743}