1package input
2
3import (
4 "context"
5 "sync"
6 "time"
7
8 "github.com/bnema/dumber/internal/infrastructure/config"
9 "github.com/bnema/dumber/internal/logging"
10 "github.com/jwijenbergh/puregotk/v4/gdk"
11 "github.com/jwijenbergh/puregotk/v4/gtk"
12)
13
14// ActionHandler is called when a keyboard action is triggered.
15// It receives the context and the action to perform.
16// Return an error if the action fails.
17type ActionHandler func(ctx context.Context, action Action) error
18
19// KeyboardHandler processes keyboard events and dispatches actions.
20// It manages modal input modes and routes key events to the appropriate handlers.
21type KeyboardHandler struct {
22 shortcuts *ShortcutSet
23 modal *ModalState
24 cfg *config.WorkspaceConfig
25
26 // Action handler callback
27 onAction ActionHandler
28 // Optional bypass check (e.g., omnibox visible)
29 shouldBypass func() bool
30
31 // GTK controller (nil until attached)
32 controller *gtk.EventControllerKey
33
34 // Callback retention: must stay reachable by Go GC.
35 keyPressedCb func(gtk.EventControllerKey, uint, uint, gdk.ModifierType) bool
36
37 ctx context.Context
38 mu sync.RWMutex
39}
40
41// NewKeyboardHandler creates a new keyboard handler.
42func NewKeyboardHandler(
43 ctx context.Context,
44 cfg *config.WorkspaceConfig,
45) *KeyboardHandler {
46 log := logging.FromContext(ctx)
47
48 log.Debug().Msg("creating keyboard handler")
49
50 h := &KeyboardHandler{
51 shortcuts: NewShortcutSet(ctx, cfg),
52 modal: NewModalState(ctx),
53 cfg: cfg,
54 ctx: ctx,
55 }
56
57 return h
58}
59
60// SetOnAction sets the callback for when actions are triggered.
61func (h *KeyboardHandler) SetOnAction(fn ActionHandler) {
62 h.mu.Lock()
63 defer h.mu.Unlock()
64 h.onAction = fn
65}
66
67// SetOnModeChange sets the callback for mode changes (for UI updates).
68func (h *KeyboardHandler) SetOnModeChange(fn func(from, to Mode)) {
69 h.modal.SetOnModeChange(fn)
70}
71
72// SetShouldBypassInput sets a hook to bypass keyboard handling entirely.
73// When true, events propagate to focused widgets instead.
74func (h *KeyboardHandler) SetShouldBypassInput(fn func() bool) {
75 h.mu.Lock()
76 defer h.mu.Unlock()
77 h.shouldBypass = fn
78}
79
80// Mode returns the current input mode.
81func (h *KeyboardHandler) Mode() Mode {
82 return h.modal.Mode()
83}
84
85// AttachTo attaches the keyboard handler to a GTK window.
86// The handler will intercept key events in the capture phase.
87func (h *KeyboardHandler) AttachTo(window *gtk.ApplicationWindow) {
88 log := logging.FromContext(h.ctx)
89
90 if window == nil {
91 log.Error().Msg("cannot attach keyboard handler to nil window")
92 return
93 }
94
95 h.controller = gtk.NewEventControllerKey()
96 if h.controller == nil {
97 log.Error().Msg("failed to create event controller key")
98 return
99 }
100
101 // Set capture phase to intercept events before WebView gets them
102 h.controller.SetPropagationPhase(gtk.PhaseCaptureValue)
103
104 // Connect key pressed handler (retain callback to prevent GC).
105 h.keyPressedCb = func(_ gtk.EventControllerKey, keyval uint, _ uint, state gdk.ModifierType) bool {
106 return h.handleKeyPress(keyval, state)
107 }
108 h.controller.ConnectKeyPressed(&h.keyPressedCb)
109
110 // Add controller to window
111 window.AddController(&h.controller.EventController)
112
113 log.Debug().Msg("keyboard handler attached to window")
114}
115
116// Detach removes the keyboard handler.
117// Note: GTK handles cleanup when the widget is destroyed,
118// but we clear our reference here.
119func (h *KeyboardHandler) Detach() {
120 h.controller = nil
121 h.keyPressedCb = nil
122}
123
124// handleKeyPress processes a key press event.
125// Returns true if the event was handled and should not propagate further.
126func (h *KeyboardHandler) handleKeyPress(keyval uint, state gdk.ModifierType) bool {
127 h.mu.RLock()
128 shouldBypass := h.shouldBypass
129 h.mu.RUnlock()
130 if shouldBypass != nil && shouldBypass() {
131 log := logging.FromContext(h.ctx)
132 log.Debug().Uint("keyval", keyval).Uint("state", uint(state)).Msg("keyboard handler bypassed")
133 return false
134 }
135
136 // Build KeyBinding from event
137 modifiers := Modifier(state) & modifierMask
138 binding := KeyBinding{
139 Keyval: keyval,
140 Modifiers: modifiers,
141 }
142
143 mode := h.modal.Mode()
144
145 // Look up the action for this key binding
146 action, found := h.shortcuts.Lookup(binding, mode)
147
148 if !found {
149 if mode != ModeNormal {
150 // In modal mode, consume unrecognized keys to prevent WebView handling
151 return true
152 }
153 // In normal mode, let the key pass through to WebView
154 return false
155 }
156
157 // Handle special mode actions first
158 switch action {
159 case ActionEnterTabMode:
160 timeout := time.Duration(h.cfg.TabMode.TimeoutMilliseconds) * time.Millisecond
161 h.modal.EnterTabMode(h.ctx, timeout)
162 return true
163
164 case ActionEnterPaneMode:
165 timeout := time.Duration(h.cfg.PaneMode.TimeoutMilliseconds) * time.Millisecond
166 h.modal.EnterPaneMode(h.ctx, timeout)
167 return true
168
169 case ActionExitMode:
170 h.modal.ExitMode(h.ctx)
171 return true
172 }
173
174 // Dispatch action to handler
175 h.mu.RLock()
176 handler := h.onAction
177 h.mu.RUnlock()
178
179 if handler != nil {
180 if err := handler(h.ctx, action); err != nil {
181 log := logging.FromContext(h.ctx)
182 log.Error().
183 Err(err).
184 Str("action", string(action)).
185 Msg("action handler error")
186 }
187 }
188
189 // Auto-exit modal mode after certain actions
190 if mode != ModeNormal && ShouldAutoExitMode(action) {
191 h.modal.ExitMode(h.ctx)
192 }
193
194 return true // Consumed the key
195}
196
197// EnterTabMode programmatically enters tab mode.
198// Useful for testing or programmatic mode changes.
199func (h *KeyboardHandler) EnterTabMode() {
200 timeout := time.Duration(h.cfg.TabMode.TimeoutMilliseconds) * time.Millisecond
201 h.modal.EnterTabMode(h.ctx, timeout)
202}
203
204// EnterPaneMode programmatically enters pane mode.
205// Useful for testing or programmatic mode changes.
206func (h *KeyboardHandler) EnterPaneMode() {
207 timeout := time.Duration(h.cfg.PaneMode.TimeoutMilliseconds) * time.Millisecond
208 h.modal.EnterPaneMode(h.ctx, timeout)
209}
210
211// ExitMode programmatically exits modal mode.
212// Useful for testing or programmatic mode changes.
213func (h *KeyboardHandler) ExitMode() {
214 h.modal.ExitMode(h.ctx)
215}