1package keys
2
3import (
4 "bytes"
5 "unicode/utf8"
6
7 "github.com/bnema/vev/internal/ports"
8 "github.com/bnema/vev/internal/usecase/keys/kittykey"
9)
10
11// escKind classifies the input introduced by one ESC byte.
12type escKind uint8
13
14const (
15 // escIncomplete means more bytes are needed before the ESC can be
16 // classified: a lone trailing ESC, a partial Alt-arrow CSI, or a partial
17 // UTF-8 rune after ESC.
18 escIncomplete escKind = iota
19 // escPaste is a complete bracketed paste, open marker through close marker.
20 escPaste
21 // escAltArrow is a complete ESC [ 1 ; 3|9 <A-D> sequence.
22 escAltArrow
23 // escControlPrefix is ESC [ or ESC O, the start of a terminal control
24 // sequence that passes through untouched.
25 escControlPrefix
26 // escAltRune is ESC followed by one complete, valid UTF-8 rune.
27 escAltRune
28 // escKittyKey is a complete kitty keyboard protocol CSI u key event.
29 escKittyKey
30 // escBare is an ESC that starts none of the above.
31 escBare
32)
33
34// escToken is one lexed ESC-introduced unit. raw always starts with ESC and
35// aliases the scanned input.
36type escToken struct {
37 kind escKind
38 raw []byte
39 rune rune // escAltRune and escControlPrefix: the rune after ESC
40 arrow byte // escAltArrow: the final byte, one of 'A'..'D'
41 key kittykey.Event
42}
43
44const altArrowLen = len("\x1b[1;3A")
45
46// scanEscape lexes the unit starting at data[0], which must be ESC. It only
47// classifies bytes; binding lookups and forwarding decisions belong to Router.
48func scanEscape(data []byte, kitty bool) escToken {
49 if len(data) == 1 {
50 return escToken{kind: escIncomplete, raw: data}
51 }
52 if bytes.HasPrefix(data, ports.BracketedPasteOpenMarker) {
53 if rel := bytes.Index(data, ports.BracketedPasteCloseMarker); rel >= 0 {
54 return escToken{kind: escPaste, raw: data[:rel+len(ports.BracketedPasteCloseMarker)]}
55 }
56 }
57 if final, ok := altArrowFinal(data); ok {
58 return escToken{kind: escAltArrow, raw: data[:altArrowLen], arrow: final}
59 }
60 if isAltArrowPrefix(data) {
61 return escToken{kind: escIncomplete, raw: data}
62 }
63 if kitty {
64 if ev, n, ok, partial := kittykey.Parse(data); ok {
65 return escToken{kind: escKittyKey, raw: data[:n], key: ev}
66 } else if partial {
67 return escToken{kind: escIncomplete, raw: data}
68 }
69 }
70 next := data[1]
71 if next == '[' || next == 'O' {
72 return escToken{kind: escControlPrefix, raw: data[:2], rune: rune(next)}
73 }
74 rest := data[1:]
75 if partialUTF8Rune(rest) {
76 return escToken{kind: escIncomplete, raw: data}
77 }
78 if key, size := utf8.DecodeRune(rest); key != utf8.RuneError || size > 1 {
79 return escToken{kind: escAltRune, raw: data[:1+size], rune: key}
80 }
81 return escToken{kind: escBare, raw: data[:1]}
82}
83
84// altArrowFinal reports the arrow final byte of a complete Alt (;3) or Meta
85// (;9) modified arrow CSI at the start of data.
86func altArrowFinal(data []byte) (byte, bool) {
87 if len(data) < altArrowLen {
88 return 0, false
89 }
90 seq := data[:altArrowLen]
91 if seq[1] != '[' || seq[2] != '1' || seq[3] != ';' || (seq[4] != '3' && seq[4] != '9') {
92 return 0, false
93 }
94 switch final := seq[5]; final {
95 case 'A', 'B', 'C', 'D':
96 return final, true
97 default:
98 return 0, false
99 }
100}
101
102// isAltArrowPrefix reports whether data is a strict prefix of an Alt/Meta
103// arrow CSI, meaning the next read may still complete it.
104func isAltArrowPrefix(data []byte) bool {
105 if len(data) < 2 || len(data) >= altArrowLen {
106 return false
107 }
108 return matchesPrefix(data, "\x1b[1;3") || matchesPrefix(data, "\x1b[1;9")
109}
110
111func matchesPrefix(data []byte, want string) bool {
112 return len(data) <= len(want) && string(data) == want[:len(data)]
113}
114
115func partialUTF8Rune(data []byte) bool {
116 if len(data) == 0 || utf8.FullRune(data) {
117 return false
118 }
119 key, size := utf8.DecodeRune(data)
120 return key == utf8.RuneError && size == 1
121}