1// Package input provides keyboard event handling and modal input mode management.
2package input
3
4import (
5 "context"
6
7 "github.com/bnema/dumber/internal/logging"
8 "github.com/jwijenbergh/puregotk/v4/gdk"
9 "github.com/jwijenbergh/puregotk/v4/glib"
10 "github.com/jwijenbergh/puregotk/v4/gtk"
11)
12
13// GlobalShortcutHandler manages keyboard shortcuts that must work globally,
14// even when WebView has focus. It uses GtkShortcutController with GTK_SHORTCUT_SCOPE_GLOBAL
15// to intercept shortcuts before they reach the WebView.
16type GlobalShortcutHandler struct {
17 controller *gtk.ShortcutController
18 onAction ActionHandler
19 ctx context.Context
20
21 // Keep references to callbacks to prevent GC from collecting them
22 callbacks []gtk.ShortcutFunc
23}
24
25// NewGlobalShortcutHandler creates a new global shortcut handler for shortcuts
26// that need to work even when WebView has focus (like Alt+1-9 for tab switching).
27func NewGlobalShortcutHandler(
28 ctx context.Context,
29 window *gtk.ApplicationWindow,
30 onAction ActionHandler,
31) *GlobalShortcutHandler {
32 log := logging.FromContext(ctx)
33 log.Debug().Msg("creating global shortcut handler")
34
35 h := &GlobalShortcutHandler{
36 controller: gtk.NewShortcutController(),
37 onAction: onAction,
38 ctx: ctx,
39 callbacks: make([]gtk.ShortcutFunc, 0),
40 }
41
42 if h.controller == nil {
43 log.Error().Msg("failed to create shortcut controller")
44 return nil
45 }
46
47 // Set global scope - this is the key to making shortcuts work
48 // even when WebView has focus
49 h.controller.SetScope(gtk.ShortcutScopeGlobalValue)
50
51 // Register Alt+1 through Alt+9 for tab switching
52 tabActions := []Action{
53 ActionSwitchTabIndex1,
54 ActionSwitchTabIndex2,
55 ActionSwitchTabIndex3,
56 ActionSwitchTabIndex4,
57 ActionSwitchTabIndex5,
58 ActionSwitchTabIndex6,
59 ActionSwitchTabIndex7,
60 ActionSwitchTabIndex8,
61 ActionSwitchTabIndex9,
62 }
63
64 for i, action := range tabActions {
65 keyval := uint(gdk.KEY_1) + uint(i) // KEY_1, KEY_2, ..., KEY_9
66 h.registerShortcut(keyval, gdk.AltMaskValue, action)
67 log.Trace().
68 Uint("keyval", keyval).
69 Str("action", string(action)).
70 Msg("registered global shortcut")
71 }
72
73 // Alt+0 for tab 10
74 h.registerShortcut(uint(gdk.KEY_0), gdk.AltMaskValue, ActionSwitchTabIndex10)
75 log.Trace().
76 Uint("keyval", uint(gdk.KEY_0)).
77 Str("action", string(ActionSwitchTabIndex10)).
78 Msg("registered global shortcut")
79
80 // Alt+Tab for switching to last active tab
81 h.registerShortcut(uint(gdk.KEY_Tab), gdk.AltMaskValue, ActionSwitchLastTab)
82 log.Trace().
83 Uint("keyval", uint(gdk.KEY_Tab)).
84 Str("action", string(ActionSwitchLastTab)).
85 Msg("registered global shortcut")
86
87 // Attach to window
88 window.AddController(&h.controller.EventController)
89
90 log.Debug().
91 Int("shortcuts", len(h.callbacks)).
92 Msg("global shortcut handler created and attached")
93
94 return h
95}
96
97// registerShortcut creates and registers a single shortcut with the controller.
98func (h *GlobalShortcutHandler) registerShortcut(keyval uint, modifiers gdk.ModifierType, action Action) {
99 // Create trigger for this key combination
100 trigger := gtk.NewKeyvalTrigger(keyval, modifiers)
101 if trigger == nil {
102 logging.FromContext(h.ctx).Error().
103 Uint("keyval", keyval).
104 Msg("failed to create keyval trigger")
105 return
106 }
107
108 // Create callback action
109 // We need to capture the action in the closure
110 actionToDispatch := action
111 callback := gtk.ShortcutFunc(func(_ uintptr, _ *glib.Variant, _ uintptr) bool {
112 log := logging.FromContext(h.ctx)
113 log.Debug().
114 Str("action", string(actionToDispatch)).
115 Msg("global shortcut triggered")
116
117 if h.onAction != nil {
118 if err := h.onAction(h.ctx, actionToDispatch); err != nil {
119 log.Error().
120 Err(err).
121 Str("action", string(actionToDispatch)).
122 Msg("global shortcut action failed")
123 }
124 }
125 return true // Event consumed
126 })
127
128 // Store callback reference to prevent GC
129 h.callbacks = append(h.callbacks, callback)
130
131 // Create the shortcut action
132 shortcutAction := gtk.NewCallbackAction(&callback, 0, nil)
133 if shortcutAction == nil {
134 logging.FromContext(h.ctx).Error().
135 Uint("keyval", keyval).
136 Msg("failed to create callback action")
137 return
138 }
139
140 // Create the shortcut combining trigger and action
141 shortcut := gtk.NewShortcut(&trigger.ShortcutTrigger, &shortcutAction.ShortcutAction)
142 if shortcut == nil {
143 logging.FromContext(h.ctx).Error().
144 Uint("keyval", keyval).
145 Msg("failed to create shortcut")
146 return
147 }
148
149 // Add to controller
150 h.controller.AddShortcut(shortcut)
151}
152
153// Detach removes the global shortcut handler from the window.
154// Note: GTK handles cleanup when the widget is destroyed,
155// but we clear our references here.
156func (h *GlobalShortcutHandler) Detach() {
157 h.controller = nil
158 h.callbacks = nil
159}