1// Package keys routes terminal input bytes into vev Alt/ESC bindings or PTY
2// passthrough bytes.
3package keys
4
5import (
6 "bytes"
7 "slices"
8 "sync"
9 "sync/atomic"
10 "time"
11 "unicode/utf8"
12
13 "github.com/bnema/vev/internal/ports"
14)
15
16const (
17 ESC byte = 0x1b
18 ESCDelay = 40 * time.Millisecond
19)
20
21// Action is an intercepted vev key binding.
22type Action int
23
24const (
25 ActionOpenPalette Action = iota
26 ActionSwitchTab1
27 ActionSwitchTab2
28 ActionSwitchTab3
29 ActionSwitchTab4
30 ActionSwitchTab5
31 ActionSwitchTab6
32 ActionSwitchTab7
33 ActionSwitchTab8
34 ActionSwitchTab9
35 ActionJumpAttention
36 ActionFocusPaneLeft
37 ActionFocusPaneRight
38 ActionFocusPaneUp
39 ActionFocusPaneDown
40 ActionToggleFloatingPane
41 ActionGrowPaneWidth
42 ActionShrinkPaneWidth
43 ActionGrowPaneHeight
44 ActionShrinkPaneHeight
45 ActionEqualizePanes
46 ActionConsumeOrExpelPaneLeft
47 ActionConsumeOrExpelPaneRight
48)
49
50// Handler receives router outputs. Forward is called only for bytes that should
51// reach the PTY; Action receives intercepted bindings and the exact consumed
52// terminal bytes. Both byte slices are owned copies.
53type Handler interface {
54 Forward([]byte)
55 Action(Action, []byte)
56}
57
58// Router implements vev's Alt/ESC input routing. It retains a trailing ESC for
59// ESCDelay so split Alt-key sequences can still be intercepted without delaying
60// known terminal control prefixes (ESC [ and ESC O), which pass through.
61type Router struct {
62 clock ports.Clock
63 delay time.Duration
64 h Handler
65 bindings *atomic.Pointer[Bindings]
66
67 mu sync.Mutex
68 pending bool
69 pendingAlt []byte
70 pendingHandler Handler
71 timer ports.Timer
72 pendingDone chan struct{}
73}
74
75// NewRouter constructs a Router. bindings may be nil, in which case
76// currentBindings falls back to defaultBindings.
77func NewRouter(clock ports.Clock, h Handler, bindings *atomic.Pointer[Bindings]) *Router {
78 return &Router{clock: clock, delay: ESCDelay, h: h, bindings: bindings}
79}
80
81// Route routes one transport read with the router's default handler.
82func (r *Router) Route(data []byte) { r.RouteWithHandler(data, r.h) }
83
84// RouteWithHandler binds every synchronous or delayed result from data to the
85// supplied handler. A retained ESC therefore cannot gain authority from a
86// later transport frame after the original frame's ownership token is stale.
87func (r *Router) RouteWithHandler(data []byte, h Handler) {
88 if len(data) == 0 {
89 return
90 }
91 if h == nil {
92 h = r.h
93 }
94 r.mu.Lock()
95 var pendingAlt []byte
96 if r.pending {
97 pendingAlt = append([]byte(nil), r.pendingAlt...)
98 }
99 pendingHandler := r.pendingHandler
100 wasPending := r.pending
101 if wasPending {
102 r.stopTimer()
103 r.pending = false
104 r.pendingAlt = nil
105 r.pendingHandler = nil
106 }
107 r.mu.Unlock()
108 if wasPending {
109 combined := append(append([]byte(nil), pendingAlt...), data...)
110 if consumed := r.routeAfterPendingESC(combined, len(pendingAlt), pendingHandler); consumed > len(pendingAlt) {
111 data = data[consumed-len(pendingAlt):]
112 }
113 }
114 r.route(data, h)
115}
116
117func (r *Router) route(data []byte, h Handler) {
118 buf := make([]byte, 0, len(data))
119 flush := func() {
120 if len(buf) == 0 {
121 return
122 }
123 r.forward(buf, h)
124 buf = buf[:0]
125 }
126 for i := 0; i < len(data); {
127 if data[i] != ESC {
128 buf = append(buf, data[i])
129 i++
130 continue
131 }
132 if i == len(data)-1 {
133 flush()
134 r.retainESC(h)
135 return
136 }
137 remaining := data[i+1:]
138 if bytes.HasPrefix(data[i:], ports.BracketedPasteOpenMarker) {
139 // A bracketed paste whose closing marker is in this same frame is
140 // forwarded verbatim: pasted content bytes (including embedded
141 // ESC+letter Alt lookalikes or ESC [ 1 ; 3 A sequences) must never
142 // fire an Action mid-paste. Without a closing marker in-frame we
143 // fall through to today's per-byte routing (the '[' passes through
144 // as a control prefix); Part A keeps pastes single-frame in
145 // practice, so no cross-frame paste state is tracked here.
146 if rel := bytes.Index(data[i:], ports.BracketedPasteCloseMarker); rel >= 0 {
147 end := i + rel + len(ports.BracketedPasteCloseMarker)
148 buf = append(buf, data[i:end]...)
149 i = end
150 continue
151 }
152 }
153 if action, size, ok, partial := r.altArrowCSI(remaining); ok {
154 flush()
155 r.action(action, data[i:i+1+size], h)
156 i += 1 + size
157 continue
158 } else if partial {
159 flush()
160 r.retainESC(h, remaining)
161 return
162 }
163 next := data[i+1]
164 if passThroughPrefix(next) {
165 buf = append(buf, ESC, next)
166 i += 2
167 continue
168 }
169 if action, size, ok := r.binding(remaining); ok {
170 flush()
171 r.action(action, data[i:i+1+size], h)
172 i += 1 + size
173 continue
174 }
175 if partialUTF8Rune(remaining) {
176 flush()
177 r.retainESC(h, remaining)
178 return
179 }
180 buf = append(buf, ESC)
181 i++
182 }
183 flush()
184}
185
186func (r *Router) routeAfterPendingESC(data []byte, pendingAltLen int, h Handler) int {
187 if action, size, ok, partial := r.altArrowCSI(data); ok {
188 r.action(action, append([]byte{ESC}, data[:size]...), h)
189 return size
190 } else if partial {
191 r.retainESC(h, data)
192 return len(data)
193 }
194 next := data[0]
195 if action, size, ok := r.binding(data); ok {
196 r.action(action, append([]byte{ESC}, data[:size]...), h)
197 return size
198 }
199 if partialUTF8Rune(data) {
200 r.retainESC(h, data)
201 return len(data)
202 }
203 _, size := utf8.DecodeRune(data)
204 if size > 1 {
205 r.forward(append([]byte{ESC}, data[:size]...), h)
206 return size
207 }
208 if pendingAltLen > 0 {
209 r.forward(append([]byte{ESC}, data[:pendingAltLen]...), h)
210 return pendingAltLen
211 }
212 if passThroughPrefix(next) {
213 r.forward([]byte{ESC, next}, h)
214 return 1
215 }
216 r.forward([]byte{ESC}, h)
217 return 0
218}
219
220func (r *Router) retainESC(h Handler, altBytes ...[]byte) {
221 r.mu.Lock()
222 r.pending = true
223 r.pendingAlt = nil
224 r.pendingHandler = h
225 if len(altBytes) > 0 {
226 r.pendingAlt = append([]byte(nil), altBytes[0]...)
227 }
228 r.timer = r.clock.NewTimer(r.delay)
229 r.pendingDone = make(chan struct{})
230 timer, done := r.timer, r.pendingDone
231 r.mu.Unlock()
232 go func(timer ports.Timer, done <-chan struct{}) {
233 select {
234 case <-timer.C():
235 case <-done:
236 return
237 }
238 var (
239 data []byte
240 h Handler
241 )
242 r.mu.Lock()
243 if r.pending && r.timer == timer {
244 r.pending = false
245 data = append([]byte{ESC}, r.pendingAlt...)
246 h = r.pendingHandler
247 r.pendingAlt = nil
248 r.pendingHandler = nil
249 r.timer = nil
250 r.pendingDone = nil
251 }
252 r.mu.Unlock()
253 if h != nil {
254 r.forward(data, h)
255 }
256 }(timer, done)
257}
258
259func (r *Router) stopTimer() {
260 if r.timer != nil {
261 r.timer.Stop()
262 r.timer = nil
263 }
264 if r.pendingDone != nil {
265 close(r.pendingDone)
266 r.pendingDone = nil
267 }
268}
269
270func partialUTF8Rune(data []byte) bool {
271 if len(data) == 0 || utf8.FullRune(data) {
272 return false
273 }
274 key, size := utf8.DecodeRune(data)
275 return key == utf8.RuneError && size == 1
276}
277
278func (r *Router) forward(data []byte, h Handler) {
279 cp := append([]byte(nil), data...)
280 h.Forward(cp)
281}
282
283func (r *Router) action(action Action, raw []byte, h Handler) {
284 cp := append([]byte(nil), raw...)
285 h.Action(action, cp)
286}
287
288func passThroughPrefix(b byte) bool { return b == '[' || b == 'O' }
289
290func (r *Router) currentBindings() *Bindings {
291 if r.bindings == nil {
292 return defaultBindings
293 }
294 bindings := r.bindings.Load()
295 if bindings == nil {
296 return defaultBindings
297 }
298 return bindings
299}
300
301func (r *Router) altArrowCSI(data []byte) (Action, int, bool, bool) {
302 const seqLen = len("[1;3A")
303 if len(data) < seqLen {
304 return 0, 0, false, hasAltArrowCSIPrefix(data)
305 }
306 seq := data[:seqLen]
307 if seq[0] != '[' || seq[1] != '1' || seq[2] != ';' || (seq[3] != '3' && seq[3] != '9') {
308 return 0, 0, false, false
309 }
310 if action, ok := r.currentBindings().actionForAltArrow(seq[4]); ok {
311 return action, seqLen, true, false
312 }
313 return 0, 0, false, false
314}
315
316func hasAltArrowCSIPrefix(data []byte) bool {
317 if len(data) == 0 || len(data) >= len("[1;3A") {
318 return false
319 }
320 return matchesPrefix(data, "[1;3") || matchesPrefix(data, "[1;9")
321}
322
323func matchesPrefix(data []byte, want string) bool {
324 if len(data) > len(want) {
325 return false
326 }
327 for i := range data {
328 if data[i] != want[i] {
329 return false
330 }
331 }
332 return true
333}
334
335func (r *Router) binding(data []byte) (Action, int, bool) {
336 return r.currentBindings().actionForAltBytes(data)
337}
338
339var topRowDigitAliases = [][]rune{
340 {'1', '&'},
341 {'2', 'é'},
342 {'3', '"'},
343 {'4', '\''},
344 {'5', '('},
345 {'6', '-', '§'},
346 {'7', 'è'},
347 {'8', '_', '!'},
348 {'9', 'ç'},
349}
350
351// topRowDigitIndex maps symbols emitted by physical top-row digit keys to
352// zero-based digit positions. It is modifier-agnostic so Alt+digit and future
353// Ctrl+digit bindings can share the same layout support.
354func topRowDigitIndex(key rune) (int, bool) {
355 for idx, aliases := range topRowDigitAliases {
356 if slices.Contains(aliases, key) {
357 return idx, true
358 }
359 }
360 return 0, false
361}