1package input
2
3import (
4 "sort"
5 "sync"
6 "time"
7
8 "github.com/bnema/dumber/internal/domain/entity"
9 "github.com/bnema/dumber/internal/domain/vimkeys"
10 "github.com/bnema/puregotk/v4/gdk"
11 "github.com/rs/zerolog/log"
12)
13
14func isCountStartKey(key vimkeys.Key) bool {
15 return key.Mods == 0 && len(key.Sym) == 1 && key.Sym[0] >= '1' && key.Sym[0] <= '9'
16}
17
18// trieOwnsBinding reports whether a raw config binding belongs to the Vim
19// sequence trie rather than the legacy single-chord Vim Mode table.
20// Multi-key sequences and Vim-only chords that ParseKeyString cannot read are owned.
21func trieOwnsBinding(raw string) (vimkeys.Sequence, bool) {
22 seq, err := vimkeys.ParseBinding(raw)
23 if err != nil {
24 return nil, false
25 }
26 if len(seq) >= 2 {
27 if isCountStartKey(seq[0]) {
28 return nil, false
29 }
30 return seq, true
31 }
32 if _, ok := ParseKeyString(raw); !ok {
33 return seq, true
34 }
35 return nil, false
36}
37
38// buildVimModeTrie inserts trie-owned Vim mode bindings. Action names are
39// sorted so the first sorted name wins on Insert conflicts (validation already
40// rejects duplicates). Ownership is marked only after a successful Insert.
41func buildVimModeTrie(cfg *entity.VimModeConfig) (*vimkeys.Trie, map[string]bool) {
42 trie := vimkeys.NewTrie()
43 owned := make(map[string]bool)
44 if cfg == nil {
45 return trie, owned
46 }
47
48 actions := make([]string, 0, len(cfg.Actions))
49 for action := range cfg.Actions {
50 actions = append(actions, action)
51 }
52 sort.Strings(actions)
53
54 for _, action := range actions {
55 binding := cfg.Actions[action]
56 keys := append([]string(nil), binding.Keys...)
57 sort.Strings(keys)
58 for _, key := range keys {
59 seq, ok := trieOwnsBinding(key)
60 if !ok {
61 continue
62 }
63 if err := trie.Insert(seq, action); err != nil {
64 log.Debug().
65 Err(err).
66 Str("key", key).
67 Str("action", action).
68 Msg("vim mode trie insert skipped")
69 continue
70 }
71 owned[key] = true
72 }
73 }
74 return trie, owned
75}
76
77// VimModeSequences returns the Vim sequence trie, if any.
78func (s *ShortcutSet) VimModeSequences() *vimkeys.Trie {
79 if s == nil {
80 return nil
81 }
82 return s.vimModeSequences
83}
84
85// filterLegacyVimModeBindings copies GetKeyBindings entries that the trie does not own.
86func filterLegacyVimModeBindings(bindings map[string]string, owned map[string]bool) map[string]string {
87 legacy := make(map[string]string, len(bindings))
88 for key, action := range bindings {
89 if owned[key] {
90 continue
91 }
92 legacy[key] = action
93 }
94 return legacy
95}
96
97// sequenceTimer is the injectable ambiguity-timeout seam.
98type sequenceTimer interface {
99 Stop() bool
100}
101
102// vimModeSequenceState holds per-handler matcher state and ambiguity timing.
103type vimModeSequenceState struct {
104 mu sync.Mutex
105 matcher *vimkeys.Matcher
106 onPending func(string)
107 onAction func(string, int)
108 timeout time.Duration
109 timer sequenceTimer
110 afterFunc func(time.Duration, func()) sequenceTimer
111 scheduleOnMainThread func(func())
112 generation uint64
113}
114
115func newVimModeSequenceState(trie *vimkeys.Trie, timeout time.Duration) vimModeSequenceState {
116 return vimModeSequenceState{
117 matcher: vimkeys.NewMatcher(trie),
118 timeout: timeout,
119 afterFunc: func(d time.Duration, fn func()) sequenceTimer {
120 return time.AfterFunc(d, fn)
121 },
122 scheduleOnMainThread: func(fn func()) { fn() },
123 }
124}
125
126// SetOnPendingSequenceChange sets the callback for pending sequence text updates.
127func (h *KeyboardHandler) SetOnPendingSequenceChange(fn func(string)) {
128 h.seq.mu.Lock()
129 defer h.seq.mu.Unlock()
130 h.seq.onPending = fn
131}
132
133// SetOnSequenceAction sets the callback for completed sequence actions.
134func (h *KeyboardHandler) SetOnSequenceAction(fn func(string, int)) {
135 h.seq.mu.Lock()
136 defer h.seq.mu.Unlock()
137 h.seq.onAction = fn
138}
139
140// SetSequenceTimeout sets the ambiguity resolution timeout.
141func (h *KeyboardHandler) SetSequenceTimeout(d time.Duration) {
142 h.seq.mu.Lock()
143 defer h.seq.mu.Unlock()
144 h.seq.timeout = d
145}
146
147// SetSequenceMainThreadScheduler sets the main-thread dispatcher for ambiguity
148// resolution. Timer goroutines must not touch matcher state directly.
149func (h *KeyboardHandler) SetSequenceMainThreadScheduler(fn func(func())) {
150 h.seq.mu.Lock()
151 defer h.seq.mu.Unlock()
152 if fn == nil {
153 return
154 }
155 h.seq.scheduleOnMainThread = fn
156}
157
158// PendingSequence returns the current pending count+keys string.
159func (h *KeyboardHandler) PendingSequence() string {
160 h.seq.mu.Lock()
161 defer h.seq.mu.Unlock()
162 if h.seq.matcher == nil {
163 return ""
164 }
165 return h.seq.matcher.Pending()
166}
167
168// ResetPendingSequence clears matcher state and notifies pending listeners.
169func (h *KeyboardHandler) ResetPendingSequence() {
170 h.resetPendingSequence(true)
171}
172
173// resetPendingSequence clears matcher/timer state. When notify is false (mode
174// exit under ModalState lock), pending callbacks are skipped.
175func (h *KeyboardHandler) resetPendingSequence(notify bool) {
176 h.seq.mu.Lock()
177 h.stopSequenceTimerLocked()
178 h.seq.generation++
179 if h.seq.matcher != nil {
180 h.seq.matcher.Reset()
181 }
182 onPending := h.seq.onPending
183 h.seq.mu.Unlock()
184
185 if notify && onPending != nil {
186 onPending("")
187 }
188}
189
190// teardownSequenceState linearizably cancels sequence timing and clears
191// UI-retaining callbacks without notifying. Used by Detach/DetachForDestroy.
192// Lock order: call without holding h.mu (same as resetPendingSequence).
193func (h *KeyboardHandler) teardownSequenceState() {
194 h.seq.mu.Lock()
195 defer h.seq.mu.Unlock()
196 h.stopSequenceTimerLocked()
197 h.seq.generation++
198 if h.seq.matcher != nil {
199 h.seq.matcher.Reset()
200 }
201 h.seq.onPending = nil
202 h.seq.onAction = nil
203 h.seq.scheduleOnMainThread = nil
204 log.Debug().Msg("vim mode sequence state torn down without notify")
205}
206
207// InvalidateSequenceMatcher stops timing, bumps generation, and replaces the
208// matcher from the current VimModeSequences trie (hot reload).
209func (h *KeyboardHandler) InvalidateSequenceMatcher() {
210 h.mu.RLock()
211 var trie *vimkeys.Trie
212 if h.shortcuts != nil {
213 trie = h.shortcuts.VimModeSequences()
214 }
215 h.mu.RUnlock()
216
217 h.seq.mu.Lock()
218 defer h.seq.mu.Unlock()
219 h.stopSequenceTimerLocked()
220 h.seq.generation++
221 h.seq.matcher = vimkeys.NewMatcher(trie)
222 log.Debug().Msg("vim mode sequence matcher replaced")
223}
224
225// feedVimModeSequence feeds a key into the vim-mode sequence matcher.
226// Pending/Complete consume the event (true). Invalid clears and returns false
227// so the legacy shortcut path can handle the key.
228func (h *KeyboardHandler) feedVimModeSequence(keyval uint, state gdk.ModifierType) bool {
229 if h.modal.Mode() != ModeVim {
230 return false
231 }
232 key, ok := KeyvalToVimKey(keyval, state)
233 if !ok {
234 return false
235 }
236
237 h.seq.mu.Lock()
238 if h.seq.matcher == nil {
239 h.seq.mu.Unlock()
240 return false
241 }
242
243 h.stopSequenceTimerLocked()
244 h.seq.generation++
245 hadPending := h.seq.matcher.Pending() != ""
246
247 result := h.seq.matcher.Feed(key)
248 onPending := h.seq.onPending
249 onAction := h.seq.onAction
250
251 switch result.Kind {
252 case vimkeys.ResultPending:
253 pending := result.Pending
254 if pending == "" {
255 pending = h.seq.matcher.Pending()
256 }
257 ambiguous := h.seq.matcher.Ambiguous()
258 if ambiguous {
259 h.armAmbiguityTimeoutLocked()
260 }
261 h.seq.mu.Unlock()
262 if onPending != nil {
263 onPending(pending)
264 }
265 log.Debug().Str("pending", pending).Bool("ambiguous", ambiguous).Msg("vim mode sequence pending")
266 return true
267
268 case vimkeys.ResultComplete:
269 action := result.Action
270 count := result.Count
271 h.seq.mu.Unlock()
272 if onPending != nil {
273 onPending("")
274 }
275 if action == "confirm" {
276 h.confirmVimMode()
277 } else if onAction != nil {
278 onAction(action, count)
279 }
280 log.Debug().Str("action", action).Int("count", count).Msg("vim mode sequence complete")
281 return true
282
283 default: // ResultInvalid
284 h.seq.mu.Unlock()
285 if hadPending && onPending != nil {
286 onPending("")
287 }
288 log.Debug().Msg("vim mode sequence invalid; falling through to legacy")
289 return false
290 }
291}
292
293func (h *KeyboardHandler) stopSequenceTimerLocked() {
294 if h.seq.timer != nil {
295 h.seq.timer.Stop()
296 h.seq.timer = nil
297 }
298}
299
300func (h *KeyboardHandler) armAmbiguityTimeoutLocked() {
301 gen := h.seq.generation
302 timeout := h.seq.timeout
303 schedule := h.seq.scheduleOnMainThread
304 afterFunc := h.seq.afterFunc
305 if afterFunc == nil {
306 afterFunc = func(d time.Duration, fn func()) sequenceTimer {
307 return time.AfterFunc(d, fn)
308 }
309 }
310 if schedule == nil {
311 schedule = func(fn func()) { fn() }
312 }
313 h.seq.timer = afterFunc(timeout, func() {
314 schedule(func() {
315 h.resolveAmbiguity(gen)
316 })
317 })
318 log.Debug().Dur("timeout", timeout).Uint64("generation", gen).Msg("vim mode ambiguity timer armed")
319}
320
321func (h *KeyboardHandler) resolveAmbiguity(gen uint64) {
322 h.seq.mu.Lock()
323 if h.seq.generation != gen {
324 h.seq.mu.Unlock()
325 log.Debug().Uint64("generation", gen).Msg("stale vim mode ambiguity resolve ignored")
326 return
327 }
328 if h.seq.matcher == nil {
329 h.seq.mu.Unlock()
330 return
331 }
332 result, ok := h.seq.matcher.ResolveAmbiguity()
333 if !ok {
334 h.seq.mu.Unlock()
335 return
336 }
337 h.seq.timer = nil
338 h.seq.generation++
339 onPending := h.seq.onPending
340 onAction := h.seq.onAction
341 action := result.Action
342 count := result.Count
343 h.seq.mu.Unlock()
344
345 if onPending != nil {
346 onPending("")
347 }
348 if action == "confirm" {
349 h.confirmVimMode()
350 } else if onAction != nil {
351 onAction(action, count)
352 }
353 log.Debug().Str("action", action).Int("count", count).Msg("vim mode ambiguity resolved")
354}