dte test coverage


Directory: ./
File: src/terminal/parse.c
Date: 2025-09-07 23:01:39
Exec Total Coverage
Lines: 312 342 91.2%
Functions: 13 13 100.0%
Branches: 221 251 88.0%

Line Branch Exec Source
1 // Parser for escape sequences sent by terminals to clients.
2 // Copyright © Craig Barnes.
3 // SPDX-License-Identifier: GPL-2.0-only
4 // See also:
5 // • https://invisible-island.net/xterm/ctlseqs/ctlseqs.html
6 // • https://sw.kovidgoyal.net/kitty/keyboard-protocol/
7 // • ECMA-48 §5.4 (https://ecma-international.org/publications-and-standards/standards/ecma-48/)
8 // • https://vt100.net/emu/dec_ansi_parser
9
10 #include "parse.h"
11 #include "query.h"
12 #include "util/ascii.h"
13 #include "util/debug.h"
14 #include "util/log.h"
15 #include "util/unicode.h"
16
17 typedef enum {
18 BYTE_CONTROL, // 0x00..0x1F
19 BYTE_INTERMEDIATE, // 0x20..0x2F
20 BYTE_PARAMETER, // 0x30..0x3F
21 BYTE_FINAL, // 0x40..0x6F
22 BYTE_FINAL_PRIVATE, // 0x70..0x7E
23 BYTE_DELETE, // 0x7F
24 BYTE_OTHER, // 0x80..0xFF
25 } Ecma48ByteType;
26
27 // https://sw.kovidgoyal.net/kitty/keyboard-protocol/#legacy-functional-keys
28 503 static KeyCode decode_key_from_final_byte(uint8_t byte)
29 {
30
14/14
✓ Branch 0 (2→3) taken 43 times.
✓ Branch 1 (2→4) taken 43 times.
✓ Branch 2 (2→5) taken 43 times.
✓ Branch 3 (2→6) taken 43 times.
✓ Branch 4 (2→7) taken 43 times.
✓ Branch 5 (2→8) taken 46 times.
✓ Branch 6 (2→9) taken 1 times.
✓ Branch 7 (2→10) taken 1 times.
✓ Branch 8 (2→11) taken 42 times.
✓ Branch 9 (2→12) taken 42 times.
✓ Branch 10 (2→13) taken 42 times.
✓ Branch 11 (2→14) taken 42 times.
✓ Branch 12 (2→15) taken 25 times.
✓ Branch 13 (2→16) taken 47 times.
503 switch (byte) {
31 case 'A': return KEY_UP;
32 43 case 'B': return KEY_DOWN;
33 43 case 'C': return KEY_RIGHT;
34 43 case 'D': return KEY_LEFT;
35 43 case 'E': return KEY_BEGIN; // (keypad '5')
36 43 case 'F': return KEY_END;
37 46 case 'H': return KEY_HOME;
38 1 case 'L': return KEY_INSERT;
39 1 case 'M': return KEY_ENTER;
40 42 case 'P': return KEY_F1;
41 42 case 'Q': return KEY_F2;
42 42 case 'R': return KEY_F3;
43 42 case 'S': return KEY_F4;
44 }
45 25 return KEY_IGNORE;
46 }
47
48 // https://sw.kovidgoyal.net/kitty/keyboard-protocol/#legacy-functional-keys
49 // https://gitlab.com/craigbarnes/dte/-/commit/f540904cfdbb04b4cafdff0d7b15e3fd188395d4
50 // https://gitlab.com/craigbarnes/dte/-/issues/121
51 995 static KeyCode decode_key_from_param(uint32_t param)
52 {
53
28/28
✓ Branch 0 (2→3) taken 41 times.
✓ Branch 1 (2→4) taken 41 times.
✓ Branch 2 (2→5) taken 1 times.
✓ Branch 3 (2→6) taken 41 times.
✓ Branch 4 (2→7) taken 44 times.
✓ Branch 5 (2→8) taken 1 times.
✓ Branch 6 (2→9) taken 41 times.
✓ Branch 7 (2→10) taken 41 times.
✓ Branch 8 (2→11) taken 41 times.
✓ Branch 9 (2→12) taken 41 times.
✓ Branch 10 (2→13) taken 41 times.
✓ Branch 11 (2→14) taken 41 times.
✓ Branch 12 (2→15) taken 41 times.
✓ Branch 13 (2→16) taken 41 times.
✓ Branch 14 (2→17) taken 41 times.
✓ Branch 15 (2→18) taken 41 times.
✓ Branch 16 (2→19) taken 41 times.
✓ Branch 17 (2→20) taken 41 times.
✓ Branch 18 (2→21) taken 41 times.
✓ Branch 19 (2→22) taken 41 times.
✓ Branch 20 (2→23) taken 41 times.
✓ Branch 21 (2→24) taken 41 times.
✓ Branch 22 (2→25) taken 41 times.
✓ Branch 23 (2→26) taken 40 times.
✓ Branch 24 (2→27) taken 40 times.
✓ Branch 25 (2→28) taken 41 times.
✓ Branch 26 (2→29) taken 6 times.
✓ Branch 27 (2→30) taken 2 times.
995 switch (param) {
54 case 1: return KEY_HOME;
55 41 case 2: return KEY_INSERT;
56 41 case 3: return KEY_DELETE;
57 1 case 4: return KEY_END;
58 41 case 5: return KEY_PAGE_UP;
59 44 case 6: return KEY_PAGE_DOWN;
60 case 7: return KEY_HOME;
61 1 case 8: return KEY_END;
62 41 case 11: return KEY_F1;
63 41 case 12: return KEY_F2;
64 41 case 13: return KEY_F3;
65 41 case 14: return KEY_F4;
66 41 case 15: return KEY_F5;
67 41 case 17: return KEY_F6;
68 41 case 18: return KEY_F7;
69 41 case 19: return KEY_F8;
70 41 case 20: return KEY_F9;
71 41 case 21: return KEY_F10;
72 41 case 23: return KEY_F11;
73 41 case 24: return KEY_F12;
74 41 case 25: return KEY_F13;
75 41 case 26: return KEY_F14;
76 41 case 28: return KEY_F15;
77 41 case 29: return KEY_F16;
78 41 case 31: return KEY_F17;
79 40 case 32: return KEY_F18;
80 40 case 33: return KEY_F19;
81 41 case 34: return KEY_F20;
82 }
83 6 return KEY_IGNORE;
84 }
85
86 // See: https://sw.kovidgoyal.net/kitty/keyboard-protocol/#modifiers
87 // ----------------------------
88 // Shift 0b1 (1)
89 // Alt 0b10 (2)
90 // Ctrl 0b100 (4)
91 // Super 0b1000 (8)
92 // Hyper 0b10000 (16)
93 // Meta 0b100000 (32)
94 // Capslock 0b1000000 (64)
95 // Numlock 0b10000000 (128)
96 // ----------------------------
97 1666 static KeyCode decode_modifiers(uint32_t n)
98 {
99 1666 n--;
100
2/2
✓ Branch 0 (2→3) taken 1455 times.
✓ Branch 1 (2→4) taken 211 times.
1666 if (unlikely(n > 255)) {
101 return KEY_IGNORE;
102 }
103
104 1455 static_assert(1 == MOD_SHIFT >> KEYCODE_MODIFIER_OFFSET);
105 1455 static_assert(2 == MOD_META >> KEYCODE_MODIFIER_OFFSET);
106 1455 static_assert(4 == MOD_CTRL >> KEYCODE_MODIFIER_OFFSET);
107 1455 static_assert(8 == MOD_SUPER >> KEYCODE_MODIFIER_OFFSET);
108 1455 static_assert(16 == MOD_HYPER >> KEYCODE_MODIFIER_OFFSET);
109 1455 static_assert(31 == MOD_MASK >> KEYCODE_MODIFIER_OFFSET);
110
111 // Decode Meta and/or Alt as MOD_META and ignore Capslock/Numlock
112 1455 KeyCode mods = (n & 31) | ((n & 32) >> 4);
113 1455 return mods << KEYCODE_MODIFIER_OFFSET;
114 }
115
116 // Normalize KeyCode values originating from `CSI u` sequences.
117 // Note that, unlike normalize_csi_27_tilde_keycode(), the `mods`
118 // parameter is only passed in for context and not expected to be
119 // included in the returned value.
120 78 static KeyCode normalize_csi_u_keycode(KeyCode mods, KeyCode key)
121 {
122 // https://sw.kovidgoyal.net/kitty/keyboard-protocol/#functional-key-definitions
123
49/51
✓ Branch 0 (2→3) taken 1 times.
✓ Branch 1 (2→4) taken 1 times.
✗ Branch 2 (2→5) not taken.
✓ Branch 3 (2→6) taken 3 times.
✓ Branch 4 (2→7) taken 1 times.
✓ Branch 5 (2→8) taken 2 times.
✓ Branch 6 (2→9) taken 1 times.
✓ Branch 7 (2→10) taken 1 times.
✓ Branch 8 (2→11) taken 1 times.
✓ Branch 9 (2→12) taken 1 times.
✓ Branch 10 (2→13) taken 1 times.
✓ Branch 11 (2→14) taken 1 times.
✓ Branch 12 (2→15) taken 1 times.
✓ Branch 13 (2→16) taken 1 times.
✓ Branch 14 (2→17) taken 1 times.
✓ Branch 15 (2→18) taken 1 times.
✓ Branch 16 (2→19) taken 1 times.
✓ Branch 17 (2→20) taken 1 times.
✓ Branch 18 (2→21) taken 1 times.
✓ Branch 19 (2→22) taken 1 times.
✓ Branch 20 (2→23) taken 1 times.
✓ Branch 21 (2→24) taken 1 times.
✓ Branch 22 (2→25) taken 1 times.
✓ Branch 23 (2→26) taken 1 times.
✓ Branch 24 (2→27) taken 1 times.
✓ Branch 25 (2→28) taken 1 times.
✓ Branch 26 (2→29) taken 1 times.
✓ Branch 27 (2→30) taken 1 times.
✓ Branch 28 (2→31) taken 1 times.
✓ Branch 29 (2→32) taken 1 times.
✓ Branch 30 (2→33) taken 1 times.
✓ Branch 31 (2→34) taken 1 times.
✓ Branch 32 (2→35) taken 1 times.
✓ Branch 33 (2→36) taken 1 times.
✓ Branch 34 (2→37) taken 1 times.
✓ Branch 35 (2→38) taken 1 times.
✓ Branch 36 (2→39) taken 1 times.
✓ Branch 37 (2→40) taken 1 times.
✓ Branch 38 (2→41) taken 1 times.
✓ Branch 39 (2→42) taken 1 times.
✓ Branch 40 (2→43) taken 1 times.
✓ Branch 41 (2→44) taken 1 times.
✓ Branch 42 (2→45) taken 1 times.
✓ Branch 43 (2→46) taken 1 times.
✓ Branch 44 (2→47) taken 1 times.
✓ Branch 45 (2→48) taken 1 times.
✓ Branch 46 (2→49) taken 1 times.
✓ Branch 47 (2→50) taken 1 times.
✓ Branch 48 (2→51) taken 1 times.
✓ Branch 49 (2→52) taken 27 times.
✗ Branch 50 (2→56) not taken.
78 switch (key) {
124 case '\b': return KEY_BACKSPACE; // BS; Kitty never emits this, but (buggy) WezTerm does
125 1 case '\r': return KEY_ENTER;
126 1 case '\t': return KEY_TAB;
127 case '\n': return KEY_ENTER; // Kitty never emits this
128 3 case 27: return KEY_ESCAPE; // ESC
129 case 127: return KEY_BACKSPACE; // DEL
130 1 case 57359: return KEY_SCROLL_LOCK;
131 2 case 57361: return KEY_PRINT_SCREEN;
132 1 case 57362: return KEY_PAUSE;
133 1 case 57363: return KEY_MENU;
134 1 case 57376: return KEY_F13;
135 1 case 57377: return KEY_F14;
136 1 case 57378: return KEY_F15;
137 1 case 57379: return KEY_F16;
138 1 case 57380: return KEY_F17;
139 1 case 57381: return KEY_F18;
140 1 case 57382: return KEY_F19;
141 1 case 57383: return KEY_F20;
142 1 case 57384: return KEY_F21;
143 1 case 57385: return KEY_F22;
144 1 case 57386: return KEY_F23;
145 1 case 57387: return KEY_F24;
146 // Keypad:
147 1 case 57399: return '0';
148 1 case 57400: return '1';
149 1 case 57401: return '2';
150 1 case 57402: return '3';
151 1 case 57403: return '4';
152 1 case 57404: return '5';
153 1 case 57405: return '6';
154 1 case 57406: return '7';
155 1 case 57407: return '8';
156 1 case 57408: return '9';
157 1 case 57409: return '.';
158 1 case 57410: return '/';
159 1 case 57411: return '*';
160 1 case 57412: return '-';
161 1 case 57413: return '+';
162 1 case 57414: return KEY_ENTER;
163 1 case 57415: return '=';
164 1 case 57416: return KEY_IGNORE; // KP_SEPARATOR
165 1 case 57417: return KEY_LEFT;
166 1 case 57418: return KEY_RIGHT;
167 1 case 57419: return KEY_UP;
168 1 case 57420: return KEY_DOWN;
169 1 case 57421: return KEY_PAGE_UP;
170 1 case 57422: return KEY_PAGE_DOWN;
171 1 case 57423: return KEY_HOME;
172 1 case 57424: return KEY_END;
173 1 case 57425: return KEY_INSERT;
174 1 case 57426: return KEY_DELETE;
175 1 case 57427: return KEY_BEGIN;
176 }
177
178
2/2
✓ Branch 0 (52→53) taken 15 times.
✓ Branch 1 (52→56) taken 12 times.
27 if (unlikely(key < 32 || (key >= 57344 && key <= 63743))) {
179 // Ignore values (not already handled above) that correspond
180 // to C0 controls or the Kitty private use range
181 return KEY_IGNORE;
182 }
183
184
3/4
✓ Branch 0 (53→54) taken 1 times.
✓ Branch 1 (53→56) taken 14 times.
✗ Branch 2 (54→55) not taken.
✓ Branch 3 (54→56) taken 1 times.
15 if (u_is_ascii_upper(key) && (mods & MOD_CTRL)) {
185 // This was originally done for the sake of iTerm2's `CSI u` mode,
186 // which could be activated with `CSI > 1 u` but didn't fully
187 // conform to Kitty's key encoding scheme. This mode has seemingly
188 // been replaced with more complete support for the Kitty protocol
189 // and explicit support for activating the old mode has been also
190 // been removed from dte. However, this code is retained for now,
191 // since the `CSI u` encoding pre-dates the Kitty keyboard protocol
192 // and it's not clear yet whether removing it would regress support
193 // in less modern terminals and/or improve correctness for exotic
194 // keyboard layouts in modern (Kitty protocol supporting) terminals.
195 // See also:
196 // • https://gitlab.com/craigbarnes/dte/-/issues/130#note_870592688
197 // • https://gitlab.com/craigbarnes/dte/-/issues/130#note_864512674
198 // • https://gitlab.com/gnachman/iterm2/-/issues/10017
199 // • https://gitlab.com/gnachman/iterm2/-/commit/9cd0241afd0655024153c8730d5b3ed1fe41faf7
200 // • https://gitlab.com/gnachman/iterm2/-/commit/9cd0241afd0655024153c8730d5b3ed1fe41faf7#1d96fc7f79950509a8bc22bc59a1a82a438c890d_0_17
201 // • https://gitlab.com/gnachman/iterm2/-/issues/7440#note_129599012
202 // • https://sw.kovidgoyal.net/kitty/keyboard-protocol/#bugs-in-fixterms:~:text=Incorrectly%20encoding%20shifted%20keys%20when%20shift%20modifier%20is%20used
203 return ascii_tolower(key);
204 }
205
206 return key;
207 }
208
209 // Normalize KeyCode values originating from xterm-style "modifyOtherKeys"
210 // sequences (CSI 27 ; <modifiers> ; <key> ~)
211 18 static KeyCode normalize_csi_27_tilde_keycode(KeyCode mods, KeyCode key)
212 {
213
6/7
✓ Branch 0 (2→3) taken 3 times.
✓ Branch 1 (2→4) taken 2 times.
✓ Branch 2 (2→5) taken 1 times.
✗ Branch 3 (2→6) not taken.
✓ Branch 4 (2→7) taken 3 times.
✓ Branch 5 (2→8) taken 3 times.
✓ Branch 6 (2→9) taken 6 times.
18 switch (key) {
214 // https://codeberg.org/dnkl/foot/pulls/791#issuecomment-279784
215 3 case '\b': return mods | KEY_BACKSPACE;
216 2 case '\r': return mods | KEY_ENTER;
217 1 case '\t': return mods | KEY_TAB;
218 case '\n': return mods | KEY_ENTER;
219 3 case 27: return mods | KEY_ESCAPE; // ESC
220 3 case 127: return mods | KEY_BACKSPACE; // DEL
221 }
222
223
1/2
✓ Branch 0 (9→10) taken 6 times.
✗ Branch 1 (9→14) not taken.
6 if (unlikely(key < 32)) {
224 return KEY_IGNORE;
225 }
226
227
2/2
✓ Branch 0 (10→11) taken 2 times.
✓ Branch 1 (10→13) taken 4 times.
6 if (u_is_ascii_upper(key)) {
228
1/2
✓ Branch 0 (11→12) taken 2 times.
✗ Branch 1 (11→14) not taken.
2 if ((mods & ~MOD_SHIFT) == 0) {
229 // [A-Z] and Shift+[A-Z] should be encoded as just [A-Z]
230 return key;
231 }
232 // [A-Z] with any other combination of modifiers should be
233 // converted to lowercase and have the MOD_SHIFT bit set.
234 // This is done in a "blanket" fashion and covers sequences
235 // that xterm never emits, because some terminals (e.g. tmux)
236 // emulate the protocol imperfectly.
237 2 return mods | MOD_SHIFT | key | 0x20;
238 }
239
240 4 return mods | key;
241 }
242
243 73 static ssize_t parse_ss3(const char *buf, size_t length, size_t i, KeyCode *k)
244 {
245
2/2
✓ Branch 0 (2→3) taken 36 times.
✓ Branch 1 (2→13) taken 37 times.
73 if (unlikely(i >= length)) {
246 return TPARSE_PARTIAL_MATCH;
247 }
248
249 36 const char ch = buf[i++];
250 36 KeyCode key = decode_key_from_final_byte(ch);
251
2/2
✓ Branch 0 (3→4) taken 12 times.
✓ Branch 1 (3→5) taken 24 times.
36 if (key != KEY_IGNORE) {
252 12 *k = key;
253
2/2
✓ Branch 0 (5→6) taken 1 times.
✓ Branch 1 (5→7) taken 23 times.
24 } else if (ch == 'X') {
254 1 *k = '=';
255
2/2
✓ Branch 0 (7→8) taken 1 times.
✓ Branch 1 (7→9) taken 22 times.
23 } else if (ch == ' ') {
256 1 *k = KEY_SPACE;
257
2/2
✓ Branch 0 (9→10) taken 17 times.
✓ Branch 1 (9→11) taken 5 times.
22 } else if ((ch >= 'j' && ch <= 'y') || ch == 'I') {
258 17 *k = ch - 64;
259 } else {
260 5 *k = KEY_IGNORE;
261 }
262
263 36 return i;
264 }
265
266 1959 static Ecma48ByteType get_byte_type(unsigned char byte)
267 {
268 1959 enum {
269 C = BYTE_CONTROL,
270 I = BYTE_INTERMEDIATE,
271 P = BYTE_PARAMETER,
272 F = BYTE_FINAL,
273 f = BYTE_FINAL_PRIVATE,
274 x = BYTE_OTHER,
275 };
276
277 // ECMA-48 divides bytes ("bit combinations") into rows of 16 columns.
278 // The byte classifications mostly fall into their own rows:
279 1959 static const uint8_t rows[16] = {
280 C, C, I, P, F, F, F, f,
281 x, x, x, x, x, x, x, x
282 };
283
284 // ... with the exception of byte 127 (DEL), which falls into rows[7]
285 // but isn't a private final byte like the others in that row:
286 1959 static_assert(BYTE_FINAL_PRIVATE + 1 == BYTE_DELETE);
287 1959 unsigned int del_offset = (byte == 127);
288 1959 return rows[byte >> 4] + del_offset;
289 }
290
291 #define UNHANDLED(var, ...) unhandled(var, __LINE__, __VA_ARGS__)
292
293 PRINTF(3)
294 398 static void unhandled(bool *var, int line, const char *fmt, ...)
295 {
296
2/2
✓ Branch 0 (2→3) taken 92 times.
✓ Branch 1 (2→5) taken 306 times.
398 if (*var) {
297 // Only log the first error in a sequence
298 return;
299 }
300
301 92 *var = true;
302 92 if (DEBUG_LOGGING_ENABLED) {
303 92 va_list ap;
304 92 va_start(ap, fmt);
305 92 log_msgv(LOG_LEVEL_DEBUG, __FILE__, line, fmt, ap);
306 92 va_end(ap);
307 }
308 }
309
310 7476 size_t term_parse_csi_params(const char *buf, size_t len, size_t i, TermControlParams *csi)
311 {
312 7476 size_t nparams = 0;
313 7476 size_t nr_intermediate = 0;
314 7476 size_t sub = 0;
315 7476 size_t digits = 0;
316 7476 uint32_t num = 0;
317 7476 bool have_subparams = false;
318 7476 bool err = false;
319
320
2/2
✓ Branch 0 (36→3) taken 23664 times.
✓ Branch 1 (36→37) taken 5637 times.
29301 while (i < len) {
321 23664 const char ch = buf[i++];
322
4/4
✓ Branch 0 (3→4) taken 17162 times.
✓ Branch 1 (3→7) taken 4509 times.
✓ Branch 2 (3→11) taken 34 times.
✓ Branch 3 (3→15) taken 1959 times.
23664 switch (ch) {
323 17162 case '0': case '1': case '2': case '3': case '4':
324 case '5': case '6': case '7': case '8': case '9':
325 17162 num = (num * 10) + (ch - '0');
326
2/2
✓ Branch 0 (4→5) taken 370 times.
✓ Branch 1 (4→6) taken 16792 times.
17162 if (unlikely(num > UNICODE_MAX_VALID_CODEPOINT)) {
327 370 UNHANDLED(&err, "CSI param overflow");
328 }
329 17162 digits++;
330 17162 continue;
331 4509 case ';':
332
1/2
✗ Branch 0 (7→8) not taken.
✓ Branch 1 (7→10) taken 4509 times.
4509 if (unlikely(nparams >= ARRAYLEN(csi->params))) {
333 UNHANDLED(&err, "too many params in CSI sequence");
334 continue;
335 }
336 4509 csi->nsub[nparams] = sub + 1;
337 4509 csi->params[nparams++][sub] = num;
338 4509 num = 0;
339 4509 digits = 0;
340 4509 sub = 0;
341 4509 continue;
342 34 case ':':
343
1/2
✗ Branch 0 (11→12) not taken.
✓ Branch 1 (11→14) taken 34 times.
34 if (unlikely(sub >= ARRAYLEN(csi->params[0]))) {
344 UNHANDLED(&err, "too many sub-params in CSI sequence");
345 continue;
346 }
347 34 csi->params[nparams][sub++] = num;
348 34 num = 0;
349 34 digits = 0;
350 34 have_subparams = true;
351 34 continue;
352 }
353
354
5/7
✓ Branch 0 (15→16) taken 100 times.
✓ Branch 1 (15→20) taken 1834 times.
✓ Branch 2 (15→25) taken 12 times.
✓ Branch 3 (15→27) taken 9 times.
✓ Branch 4 (15→31) taken 4 times.
✗ Branch 5 (15→32) not taken.
✗ Branch 6 (15→34) not taken.
1959 switch (get_byte_type(ch)) {
355 100 case BYTE_INTERMEDIATE:
356
2/2
✓ Branch 0 (16→17) taken 11 times.
✓ Branch 1 (16→18) taken 89 times.
100 if (unlikely(nr_intermediate >= ARRAYLEN(csi->intermediate))) {
357 11 UNHANDLED(&err, "too many intermediate bytes in CSI sequence");
358 } else {
359 89 csi->intermediate[nr_intermediate++] = ch;
360 }
361 100 continue;
362 1834 case BYTE_FINAL:
363 case BYTE_FINAL_PRIVATE:
364 1834 csi->final_byte = ch;
365
2/2
✓ Branch 0 (20→21) taken 1817 times.
✓ Branch 1 (20→24) taken 17 times.
1834 if (digits > 0) {
366
1/2
✗ Branch 0 (21→22) not taken.
✓ Branch 1 (21→23) taken 1817 times.
1817 if (unlikely(
367 nparams >= ARRAYLEN(csi->params)
368 || sub >= ARRAYLEN(csi->params[0])
369 )) {
370 UNHANDLED(&err, "too many params/sub-params in CSI sequence");
371 } else {
372 1817 csi->nsub[nparams] = sub + 1;
373 1817 csi->params[nparams++][sub] = num;
374 }
375 }
376 1834 goto exit_loop;
377 12 case BYTE_PARAMETER:
378 // ECMA-48 §5.4.2: "bit combinations 03/12 to 03/15 are
379 // reserved for future standardization except when used
380 // as the first bit combination of the parameter string."
381 // (03/12 to 03/15 == '<' to '?')
382 12 UNHANDLED(&err, "unhandled CSI param byte: '%c'", ch);
383 12 continue;
384 9 case BYTE_CONTROL:
385
3/3
✓ Branch 0 (27→28) taken 2 times.
✓ Branch 1 (27→29) taken 3 times.
✓ Branch 2 (27→31) taken 4 times.
9 switch (ch) {
386 2 case 0x1B: // ESC
387 // Don't consume ESC; it's the start of another sequence
388 2 i--;
389 // Fallthrough
390 5 case 0x18: // CAN
391 case 0x1A: // SUB
392 5 UNHANDLED(&err, "CSI sequence cancelled by 0x%02hhx", ch);
393 5 csi->final_byte = ch;
394 5 goto exit_loop;
395 }
396 // Fallthrough
397 case BYTE_DELETE:
398 8 continue;
399 case BYTE_OTHER:
400 UNHANDLED(&err, "unhandled byte in CSI sequence: 0x%02hhx", ch);
401 continue;
402 }
403
404 BUG("unhandled byte type");
405 err = true;
406 }
407
408 5637 exit_loop:
409 7476 csi->nparams = nparams;
410 7476 csi->nr_intermediate = nr_intermediate;
411 7476 csi->have_subparams = have_subparams;
412 7476 csi->unhandled_bytes = err;
413 7476 return i;
414 }
415
416 7471 static ssize_t parse_csi(const char *buf, size_t len, size_t i, KeyCode *k)
417 {
418 7471 TermControlParams csi = {.nparams = 0};
419
6/6
✓ Branch 0 (2→3) taken 6419 times.
✓ Branch 1 (2→6) taken 1052 times.
✓ Branch 2 (3→4) taken 267 times.
✓ Branch 3 (3→6) taken 6152 times.
✓ Branch 4 (4→5) taken 253 times.
✓ Branch 5 (4→6) taken 14 times.
7471 bool maybe_query_reply = (i < len && buf[i] >= '<' && buf[i] <= '?');
420 253 uint8_t param_prefix = maybe_query_reply ? buf[i] : 0;
421 7471 i = term_parse_csi_params(buf, len, i + (maybe_query_reply ? 1 : 0), &csi);
422
423
2/2
✓ Branch 0 (7→8) taken 5637 times.
✓ Branch 1 (7→10) taken 1834 times.
7471 if (unlikely(csi.final_byte == 0)) {
424 5637 BUG_ON(i < len);
425 return TPARSE_PARTIAL_MATCH;
426 }
427
2/2
✓ Branch 0 (10→11) taken 17 times.
✓ Branch 1 (10→12) taken 1817 times.
1834 if (unlikely(csi.unhandled_bytes)) {
428 17 goto ignore;
429 }
430
431
2/2
✓ Branch 0 (12→13) taken 38 times.
✓ Branch 1 (12→15) taken 1779 times.
1817 if (maybe_query_reply) {
432 38 *k = parse_csi_query_reply(&csi, param_prefix);
433 38 return i;
434 }
435
436
2/2
✓ Branch 0 (15→16) taken 4 times.
✓ Branch 1 (15→17) taken 1775 times.
1779 if (unlikely(csi.nr_intermediate)) {
437 4 goto ignore;
438 }
439
440 /*
441 * This handles the basic CSI u ("fixterms") encoding and also the
442 * extended kitty keyboard encoding.
443 *
444 * • https://www.leonerd.org.uk/hacks/fixterms/
445 * • https://sw.kovidgoyal.net/kitty/keyboard-protocol/
446 * • https://invisible-island.net/xterm/manpage/xterm.html#VT100-Widget-Resources:formatOtherKeys
447 *
448 * kitty params: key:unshifted-key:base-layout-key ; mods:event-type ; text
449 */
450 1775 KeyCode key, mods = 0;
451
2/2
✓ Branch 0 (17→18) taken 80 times.
✓ Branch 1 (17→33) taken 1695 times.
1775 if (csi.final_byte == 'u') {
452
3/6
✓ Branch 0 (18→19) taken 80 times.
✗ Branch 1 (18→21) not taken.
✓ Branch 2 (19→20) taken 80 times.
✗ Branch 3 (19→21) not taken.
✗ Branch 4 (20→21) not taken.
✓ Branch 5 (20→22) taken 80 times.
80 if (unlikely(csi.nsub[0] > 3 || csi.nsub[1] > 2 || csi.nsub[2] > 1)) {
453 // Don't allow unknown sub-params
454 goto ignore;
455 }
456 // Use the "base layout key", if present
457
2/2
✓ Branch 0 (22→23) taken 79 times.
✓ Branch 1 (22→24) taken 1 times.
80 key = csi.params[0][csi.nsub[0] == 3 ? 2 : 0];
458
2/3
✓ Branch 0 (24→25) taken 22 times.
✓ Branch 1 (24→29) taken 58 times.
✗ Branch 2 (24→32) not taken.
80 switch (csi.nparams) {
459 22 case 3:
460 case 2:
461
2/2
✓ Branch 0 (25→26) taken 1 times.
✓ Branch 1 (25→27) taken 21 times.
22 if (unlikely(csi.params[1][1] > 2)) {
462 // Key release event
463 1 goto ignore;
464 }
465 21 mods = decode_modifiers(csi.params[1][0]);
466
2/2
✓ Branch 0 (27→28) taken 1 times.
✓ Branch 1 (27→29) taken 20 times.
21 if (unlikely(mods == KEY_IGNORE)) {
467 1 goto ignore;
468 }
469 // Fallthrough
470 case 1:
471 78 key = normalize_csi_u_keycode(mods, key);
472
2/2
✓ Branch 0 (29→30) taken 13 times.
✓ Branch 1 (29→31) taken 65 times.
78 if (unlikely(key == KEY_IGNORE)) {
473 13 goto ignore;
474 }
475 65 *k = mods | key;
476 65 return i;
477 }
478 goto ignore;
479 }
480
481
1/2
✗ Branch 0 (33→34) not taken.
✓ Branch 1 (33→35) taken 1695 times.
1695 if (unlikely(csi.have_subparams)) {
482 goto ignore;
483 }
484
485
3/4
✓ Branch 0 (35→36) taken 1157 times.
✗ Branch 1 (35→51) not taken.
✓ Branch 2 (35→53) taken 2 times.
✓ Branch 3 (35→56) taken 536 times.
1695 switch (csi.final_byte) {
486 1157 case '~':
487
3/4
✓ Branch 0 (36→37) taken 18 times.
✓ Branch 1 (36→42) taken 1107 times.
✓ Branch 2 (36→44) taken 32 times.
✗ Branch 3 (36→50) not taken.
1157 switch (csi.nparams) {
488 18 case 3:
489
1/2
✗ Branch 0 (37→38) not taken.
✓ Branch 1 (37→39) taken 18 times.
18 if (unlikely(csi.params[0][0] != 27)) {
490 goto ignore;
491 }
492 18 mods = decode_modifiers(csi.params[1][0]);
493
1/2
✗ Branch 0 (39→40) not taken.
✓ Branch 1 (39→41) taken 18 times.
18 if (unlikely(mods == KEY_IGNORE)) {
494 goto ignore;
495 }
496 // xterm-style modifyOtherKeys encoding
497 18 key = csi.params[2][0];
498 18 *k = normalize_csi_27_tilde_keycode(mods, key);
499 18 return i;
500 1107 case 2:
501 1107 mods = decode_modifiers(csi.params[1][0]);
502
2/2
✓ Branch 0 (42→43) taken 144 times.
✓ Branch 1 (42→44) taken 963 times.
1107 if (unlikely(mods == KEY_IGNORE)) {
503 144 goto ignore;
504 }
505 // Fallthrough
506 case 1:
507 995 key = decode_key_from_param(csi.params[0][0]);
508
2/2
✓ Branch 0 (44→45) taken 6 times.
✓ Branch 1 (44→49) taken 989 times.
995 if (key == KEY_IGNORE) {
509
1/4
✗ Branch 0 (45→46) not taken.
✓ Branch 1 (45→48) taken 6 times.
✗ Branch 2 (46→47) not taken.
✗ Branch 3 (46→48) not taken.
6 if (csi.params[0][0] == 200 && mods == 0) {
510 *k = KEYCODE_BRACKETED_PASTE;
511 return i;
512 }
513 6 goto ignore;
514 }
515 989 *k = mods | key;
516 989 return i;
517 }
518 goto ignore;
519 case 't':
520 *k = parse_xtwinops_query_reply(&csi);
521 return i;
522 2 case 'Z':
523
2/2
✓ Branch 0 (53→54) taken 1 times.
✓ Branch 1 (53→55) taken 1 times.
2 if (unlikely(csi.nparams != 0)) {
524 1 goto ignore;
525 }
526 1 *k = MOD_SHIFT | KEY_TAB;
527 1 return i;
528 }
529
530
3/3
✓ Branch 0 (56→57) taken 520 times.
✓ Branch 1 (56→60) taken 13 times.
✓ Branch 2 (56→63) taken 3 times.
536 switch (csi.nparams) {
531 520 case 2:
532 520 mods = decode_modifiers(csi.params[1][0]);
533
3/4
✓ Branch 0 (57→58) taken 454 times.
✓ Branch 1 (57→59) taken 66 times.
✗ Branch 2 (58→59) not taken.
✓ Branch 3 (58→60) taken 454 times.
520 if (unlikely(mods == KEY_IGNORE || csi.params[0][0] != 1)) {
534 66 goto ignore;
535 }
536 // Fallthrough
537 case 0:
538 467 key = decode_key_from_final_byte(csi.final_byte);
539
2/2
✓ Branch 0 (60→61) taken 1 times.
✓ Branch 1 (60→62) taken 466 times.
467 if (unlikely(key == KEY_IGNORE)) {
540 1 goto ignore;
541 }
542 466 *k = mods | key;
543 466 return i;
544 }
545
546 257 ignore:
547 257 *k = KEY_IGNORE;
548 257 return i;
549 }
550
551 29 static ssize_t parse_osc(const char *buf, size_t len, size_t i, KeyCode *k)
552 {
553 29 char data[4096];
554
2/2
✓ Branch 0 (13→3) taken 87 times.
✓ Branch 1 (13→14) taken 25 times.
112 for (size_t pos = 0; i < len; ) {
555 87 unsigned char ch = buf[i++];
556
2/2
✓ Branch 0 (3→4) taken 4 times.
✓ Branch 1 (3→10) taken 83 times.
87 if (unlikely(ch < 0x20)) {
557
2/4
✗ Branch 0 (4→5) not taken.
✓ Branch 1 (4→6) taken 2 times.
✓ Branch 2 (4→7) taken 2 times.
✗ Branch 3 (4→9) not taken.
4 switch (ch) {
558 case 0x18: // CAN
559 case 0x1A: // SUB
560 *k = KEY_IGNORE;
561 return i;
562 2 case 0x1B: // ESC (https://vt100.net/emu/dec_ansi_parser#STESC)
563 2 i--;
564 // Fallthrough
565 4 case 0x07: // BEL
566 4 *k = parse_osc_query_reply(data, pos, pos >= sizeof(data));
567 4 return i;
568 }
569 continue;
570 }
571 // Collect 0x20..0xFF (UTF-8 allowed)
572
1/2
✓ Branch 0 (10→11) taken 83 times.
✗ Branch 1 (10→12) not taken.
83 if (likely(pos < sizeof(data))) {
573 83 data[pos++] = ch;
574 }
575 }
576
577 // Unterminated sequence (possibly truncated)
578 return TPARSE_PARTIAL_MATCH;
579 }
580
581 200 static ssize_t parse_dcs(const char *buf, size_t len, size_t i, KeyCode *k)
582 {
583 200 char data[4096];
584
2/2
✓ Branch 0 (12→3) taken 1653 times.
✓ Branch 1 (12→13) taken 185 times.
1838 for (size_t pos = 0; i < len; ) {
585 1653 unsigned char ch = buf[i++];
586
2/2
✓ Branch 0 (3→4) taken 15 times.
✓ Branch 1 (3→9) taken 1638 times.
1653 if (unlikely(ch < 0x20 || ch == 0x7F)) {
587
1/3
✗ Branch 0 (4→5) not taken.
✓ Branch 1 (4→6) taken 15 times.
✗ Branch 2 (4→8) not taken.
15 switch (ch) {
588 case 0x18: // CAN
589 case 0x1A: // SUB
590 *k = KEY_IGNORE;
591 return i;
592 15 case 0x1B: // ESC (https://vt100.net/emu/dec_ansi_parser#STESC)
593 15 *k = parse_dcs_query_reply(data, pos, pos >= sizeof(data));
594 15 return i - 1;
595 }
596 continue;
597 }
598 // Collect 0x20..0xFF (excluding 0x7F)
599
1/2
✓ Branch 0 (9→10) taken 1638 times.
✗ Branch 1 (9→11) not taken.
1638 if (likely(pos < sizeof(data))) {
600 1638 data[pos++] = ch;
601 }
602 }
603
604 // Unterminated sequence (possibly truncated)
605 return TPARSE_PARTIAL_MATCH;
606 }
607
608 /*
609 * Some terminals emit output resembling CSI/SS3 sequences without properly
610 * conforming to ECMA-48 §5.4, so we use an approach that accommodates
611 * terminal-specific special cases. This more or less precludes the use of
612 * a state machine (like e.g. the "dec_ansi_parser" mentioned at the top of
613 * this file). There's no real standard for "terminal to host" communications,
614 * although conforming to ECMA-48 §5.4 has been a de facto standard since the
615 * DEC VTs, with only a few (unnecessary and most likely not deliberate)
616 * exceptions in some emulators.
617 *
618 * See also: rxvt.c and linux.c
619 */
620 8935 ssize_t term_parse_sequence(const char *buf, size_t length, KeyCode *k)
621 {
622
4/4
✓ Branch 0 (2→3) taken 8934 times.
✓ Branch 1 (2→11) taken 1 times.
✓ Branch 2 (3→4) taken 8933 times.
✓ Branch 3 (3→11) taken 1 times.
8935 if (unlikely(length == 0 || buf[0] != '\033')) {
623 return 0;
624
2/2
✓ Branch 0 (4→5) taken 7775 times.
✓ Branch 1 (4→11) taken 1158 times.
8933 } else if (unlikely(length == 1)) {
625 return TPARSE_PARTIAL_MATCH;
626 }
627
628
6/6
✓ Branch 0 (5→6) taken 73 times.
✓ Branch 1 (5→7) taken 200 times.
✓ Branch 2 (5→8) taken 7471 times.
✓ Branch 3 (5→9) taken 29 times.
✓ Branch 4 (5→10) taken 1 times.
✓ Branch 5 (5→11) taken 1 times.
7775 switch (buf[1]) {
629 73 case 'O': return parse_ss3(buf, length, 2, k);
630 200 case 'P': return parse_dcs(buf, length, 2, k);
631 7471 case '[': return parse_csi(buf, length, 2, k);
632 29 case ']': return parse_osc(buf, length, 2, k);
633 // String Terminator (https://vt100.net/emu/dec_ansi_parser#STESC)
634 1 case '\\': *k = KEY_IGNORE; return 2;
635 }
636
637 return 0;
638 }
639