1package kittykey
2
3import (
4 "bytes"
5 "strconv"
6 "unicode/utf8"
7)
8
9// maxNormalizedSeq bounds one held key sequence. Associated text makes a key
10// event as long as the text an IME commits at once.
11const maxNormalizedSeq = 4096
12
13// Kitty functional key codes of the modifier and lock keys. RequestFlags
14// reports them; OuterFlags never does.
15const (
16 keyCapsLock = 57358
17 keyNumLock = 57360
18 keyLeftShift = 57441
19 keyLeftControl = 57442
20 keyRightControl = 57448
21 keyISOLevel5 = 57454
22)
23
24// Normalizer rewrites the outer terminal's RequestFlags key stream into the
25// OuterFlags encoding the rest of vev consumes, and tracks whether Ctrl is
26// physically held. Release events, modifier and lock key events are dropped,
27// repeats become presses, and text keys become their text again.
28//
29// Input already in the OuterFlags or legacy encoding passes through
30// unchanged, so enabling a Normalizer before the terminal switches flags is
31// safe. A trailing incomplete escape sequence is held until the next call. A
32// lone trailing ESC is held only once a CSI u key event proved the terminal
33// reports Escape as CSI 27 u; before that it may be a real Escape press.
34// A Normalizer is not safe for concurrent use.
35type Normalizer struct {
36 pending []byte
37 paste bool
38 ctrl bool
39 // kitty records that a CSI u key event arrived, so the terminal encodes
40 // Escape itself as one and a lone ESC can only start a split sequence.
41 kitty bool
42}
43
44// CtrlHeld reports whether Ctrl is held on its own: a Ctrl key is down and no
45// other key but a top-row digit was pressed since. Only a Ctrl key press sets
46// it, so a terminal that reports no modifier key events never shows it held.
47func (n *Normalizer) CtrlHeld() bool { return n.ctrl }
48
49// ReleaseCtrl forgets a held Ctrl, for a terminal that lost focus and may
50// never report the release.
51func (n *Normalizer) ReleaseCtrl() { n.ctrl = false }
52
53// Normalize returns data in the OuterFlags encoding, in a new slice.
54func (n *Normalizer) Normalize(data []byte) []byte {
55 if len(n.pending) > 0 {
56 data = append(n.pending, data...)
57 n.pending = nil
58 }
59 out := make([]byte, 0, len(data))
60 for i := 0; i < len(data); {
61 if n.paste {
62 end := bytes.Index(data[i:], pasteClose)
63 if end < 0 {
64 keep := len(data) - partialSuffix(data[i:], pasteClose)
65 out = append(out, data[i:keep]...)
66 n.hold(data[keep:])
67 return out
68 }
69 end += i + len(pasteClose)
70 out = append(out, data[i:end]...)
71 i = end
72 n.paste = false
73 continue
74 }
75 if data[i] != 0x1b {
76 out = append(out, data[i])
77 i++
78 continue
79 }
80 rest := data[i:]
81 if bytes.HasPrefix(rest, pasteOpen) {
82 out = append(out, pasteOpen...)
83 i += len(pasteOpen)
84 n.paste = true
85 continue
86 }
87 params, final, size, partial := scanCSI(rest)
88 if partial && len(rest) == 1 && !n.kitty {
89 out = append(out, rest...)
90 return out
91 }
92 if partial {
93 n.hold(rest)
94 return out
95 }
96 if size > 0 {
97 if repl, ok := n.rewrite(params, final); ok {
98 out = append(out, repl...)
99 i += size
100 continue
101 }
102 }
103 out = append(out, data[i])
104 i++
105 }
106 return out
107}
108
109func (n *Normalizer) hold(tail []byte) {
110 if len(tail) > 0 {
111 n.pending = append([]byte(nil), tail...)
112 }
113}
114
115// partialSuffix returns the length of the longest suffix of data that is a
116// strict prefix of marker.
117func partialSuffix(data, marker []byte) int {
118 for k := min(len(data), len(marker)-1); k > 0; k-- {
119 if bytes.HasPrefix(marker, data[len(data)-k:]) {
120 return k
121 }
122 }
123 return 0
124}
125
126// scanCSI reads ESC [ params final at the start of data, where params holds
127// only digits, ';' and ':'. size is 0 when data starts no such sequence;
128// partial reports a strict prefix that may still complete.
129func scanCSI(data []byte) (params []byte, final byte, size int, partial bool) {
130 if len(data) == 1 {
131 return nil, 0, 0, true
132 }
133 if data[1] != '[' {
134 return nil, 0, 0, false
135 }
136 for i := 2; i < len(data) && i < maxNormalizedSeq; i++ {
137 b := data[i]
138 if (b >= '0' && b <= '9') || b == ';' || b == ':' {
139 continue
140 }
141 return data[2:i], b, i + 1, false
142 }
143 return nil, 0, 0, len(data) < maxNormalizedSeq
144}
145
146// rewrite returns the OuterFlags form of one key sequence, or false when the
147// sequence is not a key event this package owns.
148func (n *Normalizer) rewrite(params []byte, final byte) ([]byte, bool) {
149 switch final {
150 case 'u':
151 if len(params) == 0 {
152 return nil, false
153 }
154 key, ok := parseParams(params)
155 if !ok {
156 return nil, false
157 }
158 n.kitty = true
159 n.track(key.ev, true)
160 return encodeOuter(key), true
161 case '~', 'A', 'B', 'C', 'D', 'E', 'F', 'H', 'P', 'Q', 'S':
162 return n.rewriteFunctional(params, final)
163 }
164 return nil, false
165}
166
167// rewriteFunctional handles CSI number;mods[:event] ~ and CSI 1;mods[:event]
168// letter forms. Forms without a modifier field are already in OuterFlags
169// form, which also keeps bracketed paste markers and replies untouched.
170func (n *Normalizer) rewriteFunctional(params []byte, final byte) ([]byte, bool) {
171 fields := splitByte(params, ';')
172 if len(fields) != 2 {
173 return nil, false
174 }
175 mods, event, ok := parseMods(fields[1])
176 if !ok {
177 return nil, false
178 }
179 n.track(Event{Mods: mods &^ modLocks, Release: event == eventRelease}, false)
180 if event == eventRelease {
181 return nil, true
182 }
183 // Functional keys keep their lock bits under OuterFlags.
184 out := []byte("\x1b[")
185 if mods != 0 || (final == '~' || string(fields[0]) != "1") {
186 out = append(out, fields[0]...)
187 }
188 if mods != 0 {
189 out = append(out, ';')
190 out = strconv.AppendInt(out, int64(mods+1), 10)
191 }
192 return append(out, final), true
193}
194
195// track updates the Ctrl state from one key event. A Ctrl key event reports
196// the Ctrl state after it. Pressing any other key ends a Ctrl held on its own,
197// except a top-row digit, the Ctrl+1..9 recent-session switch.
198func (n *Normalizer) track(ev Event, unicodeKey bool) {
199 switch {
200 case unicodeKey && (ev.Code == keyLeftControl || ev.Code == keyRightControl):
201 n.ctrl = ev.Mods&ModCtrl != 0
202 case unicodeKey && ev.Code == 0:
203 // A text-only event carries no key.
204 case unicodeKey && isModifierOrLock(ev.Code):
205 if ev.Mods&ModCtrl == 0 {
206 n.ctrl = false
207 }
208 case ev.Mods&ModCtrl == 0:
209 n.ctrl = false
210 case ev.Release:
211 case unicodeKey:
212 if _, digit := ev.Digit(); !digit {
213 n.ctrl = false
214 }
215 default:
216 n.ctrl = false
217 }
218}
219
220// encodeOuter re-encodes one RequestFlags key event as the terminal would
221// have sent it under OuterFlags, or nil when it would have sent nothing.
222func encodeOuter(key fullKey) []byte {
223 ev := key.ev
224 if ev.Release || isModifierOrLock(ev.Code) {
225 return nil
226 }
227 switch {
228 case len(key.text) > 0 || ev.Code == 0:
229 // A terminal sends the text a key generates as is, whatever the
230 // modifiers that produced it.
231 return appendRunes(nil, key.text)
232 case ev.Mods == 0 && (ev.Code == KeyEnter || ev.Code == KeyTab || ev.Code == KeyBackspace):
233 return []byte{byte(ev.Code)}
234 case ev.Mods&^ModShift == 0 && isTextKey(ev.Code):
235 return legacyText(ev)
236 }
237 // A key sent as an escape code reports the lock bits too.
238 ev.Mods |= key.locks
239 return encodeCSIu(ev, OuterFlags)
240}
241
242func isModifierOrLock(code rune) bool {
243 return (code >= keyLeftShift && code <= keyISOLevel5) || (code >= keyCapsLock && code <= keyNumLock)
244}
245
246// isTextKey reports a key whose unmodified press produces its own text.
247func isTextKey(code rune) bool {
248 return code >= 0x20 && code != KeyBackspace && (code < 0xe000 || code > 0xf8ff) && utf8.ValidRune(code)
249}
250
251func appendRunes(out []byte, runes []rune) []byte {
252 for _, r := range runes {
253 out = utf8.AppendRune(out, r)
254 }
255 return out
256}