this repo has no description
1const std = @import("std");
2const testing = std.testing;
3const Color = @import("Cell.zig").Color;
4const Event = @import("event.zig").Event;
5const Key = @import("Key.zig");
6const Mouse = @import("Mouse.zig");
7const uucode = @import("uucode");
8const Winsize = @import("main.zig").Winsize;
9
10const log = std.log.scoped(.vaxis_parser);
11
12const Parser = @This();
13
14/// The return type of our parse method. Contains an Event and the number of
15/// bytes read from the buffer.
16pub const Result = struct {
17 event: ?Event,
18 n: usize,
19};
20
21const mouse_bits = struct {
22 const motion: u8 = 0b00100000;
23 const buttons: u8 = 0b11000011;
24 const shift: u8 = 0b00000100;
25 const alt: u8 = 0b00001000;
26 const ctrl: u8 = 0b00010000;
27 const leave: u16 = 0b100000000;
28};
29
30// the state of the parser
31const State = enum {
32 ground,
33 escape,
34 csi,
35 osc,
36 dcs,
37 sos,
38 pm,
39 apc,
40 ss2,
41 ss3,
42};
43
44// a buffer to temporarily store text in. We need this to encode
45// text-as-codepoints
46buf: [128]u8 = undefined,
47
48/// Parse the first event from the input buffer. If a completion event is not
49/// present, Result.event will be null and Result.n will be 0
50///
51/// If an unknown event is found, Result.event will be null and Result.n will be
52/// greater than 0
53pub fn parse(self: *Parser, input: []const u8, paste_allocator: ?std.mem.Allocator) !Result {
54 std.debug.assert(input.len > 0);
55
56 // We gate this for len > 1 so we can detect singular escape key presses
57 if (input[0] == 0x1b and input.len > 1) {
58 switch (input[1]) {
59 0x4F => return parseSs3(input),
60 0x50 => return skipUntilST(input), // DCS
61 0x58 => return skipUntilST(input), // SOS
62 0x5B => return parseCsi(input, &self.buf), // CSI
63 0x5D => return parseOsc(input, paste_allocator),
64 0x5E => return skipUntilST(input), // PM
65 0x5F => return parseApc(input),
66 else => {
67 // Anything else is an "alt + <char>" keypress
68 const key: Key = .{
69 .codepoint = input[1],
70 .mods = .{ .alt = true },
71 };
72 return .{
73 .event = .{ .key_press = key },
74 .n = 2,
75 };
76 },
77 }
78 } else return parseGround(input);
79}
80
81/// Parse ground state
82inline fn parseGround(input: []const u8) !Result {
83 std.debug.assert(input.len > 0);
84
85 const b = input[0];
86 var n: usize = 1;
87 // ground state generates keypresses when parsing input. We
88 // generally get ascii characters, but anything less than
89 // 0x20 is a Ctrl+<c> keypress. We map these to lowercase
90 // ascii characters when we can
91 const key: Key = switch (b) {
92 0x00 => .{ .codepoint = '@', .mods = .{ .ctrl = true } },
93 0x08 => .{ .codepoint = Key.backspace },
94 0x09 => .{ .codepoint = Key.tab },
95 0x0A => .{ .codepoint = 'j', .mods = .{ .ctrl = true } },
96 0x0D => .{ .codepoint = Key.enter },
97 0x01...0x07,
98 0x0B...0x0C,
99 0x0E...0x1A,
100 => .{ .codepoint = b + 0x60, .mods = .{ .ctrl = true } },
101 0x1B => escape: {
102 std.debug.assert(input.len == 1); // parseGround expects len == 1 with 0x1b
103 break :escape .{
104 .codepoint = Key.escape,
105 };
106 },
107 0x7F => .{ .codepoint = Key.backspace },
108 else => blk: {
109 var iter = uucode.utf8.Iterator.init(input);
110 // return null if we don't have a valid codepoint
111 const first_cp = iter.next() orelse return error.InvalidUTF8;
112
113 n = std.unicode.utf8CodepointSequenceLength(first_cp) catch return error.InvalidUTF8;
114
115 // Check if we have a multi-codepoint grapheme
116 var code = first_cp;
117 var grapheme_iter = uucode.grapheme.Iterator(uucode.utf8.Iterator).init(.init(input));
118 var grapheme_len: usize = 0;
119 var cp_count: usize = 0;
120
121 while (grapheme_iter.next()) |result| {
122 cp_count += 1;
123 if (result.is_break) {
124 // Found the first grapheme boundary
125 grapheme_len = grapheme_iter.i;
126 break;
127 }
128 }
129
130 if (grapheme_len > 0) {
131 n = grapheme_len;
132 if (cp_count > 1) {
133 code = Key.multicodepoint;
134 }
135 }
136
137 break :blk .{ .codepoint = code, .text = input[0..n] };
138 },
139 };
140
141 return .{
142 .event = .{ .key_press = key },
143 .n = n,
144 };
145}
146
147inline fn parseSs3(input: []const u8) Result {
148 if (input.len < 3) {
149 return .{
150 .event = null,
151 .n = 0,
152 };
153 }
154 const key: Key = switch (input[2]) {
155 0x1B => return .{
156 .event = null,
157 .n = 2,
158 },
159 'A' => .{ .codepoint = Key.up },
160 'B' => .{ .codepoint = Key.down },
161 'C' => .{ .codepoint = Key.right },
162 'D' => .{ .codepoint = Key.left },
163 'E' => .{ .codepoint = Key.kp_begin },
164 'F' => .{ .codepoint = Key.end },
165 'H' => .{ .codepoint = Key.home },
166 'P' => .{ .codepoint = Key.f1 },
167 'Q' => .{ .codepoint = Key.f2 },
168 'R' => .{ .codepoint = Key.f3 },
169 'S' => .{ .codepoint = Key.f4 },
170 else => {
171 log.warn("unhandled ss3: {x}", .{input[2]});
172 return .{
173 .event = null,
174 .n = 3,
175 };
176 },
177 };
178 return .{
179 .event = .{ .key_press = key },
180 .n = 3,
181 };
182}
183
184inline fn parseApc(input: []const u8) Result {
185 if (input.len < 3) {
186 return .{
187 .event = null,
188 .n = 0,
189 };
190 }
191 const end = std.mem.indexOfScalarPos(u8, input, 2, 0x1b) orelse return .{
192 .event = null,
193 .n = 0,
194 };
195 const sequence = input[0 .. end + 1 + 1];
196
197 switch (input[2]) {
198 'G' => return .{
199 .event = .cap_kitty_graphics,
200 .n = sequence.len,
201 },
202 else => return .{
203 .event = null,
204 .n = sequence.len,
205 },
206 }
207}
208
209/// Skips sequences until we see an ST (String Terminator, ESC \)
210inline fn skipUntilST(input: []const u8) Result {
211 if (input.len < 3) {
212 return .{
213 .event = null,
214 .n = 0,
215 };
216 }
217 const end = std.mem.indexOfScalarPos(u8, input, 2, 0x1b) orelse return .{
218 .event = null,
219 .n = 0,
220 };
221 if (input.len < end + 1 + 1) {
222 return .{
223 .event = null,
224 .n = 0,
225 };
226 }
227 const sequence = input[0 .. end + 1 + 1];
228 return .{
229 .event = null,
230 .n = sequence.len,
231 };
232}
233
234/// Parses an OSC sequence
235inline fn parseOsc(input: []const u8, paste_allocator: ?std.mem.Allocator) !Result {
236 if (input.len < 3) {
237 return .{
238 .event = null,
239 .n = 0,
240 };
241 }
242 var bel_terminated: bool = false;
243 // end is the index of the terminating byte(s) (either the last byte of an
244 // ST or BEL)
245 const end: usize = blk: {
246 const esc_result = skipUntilST(input);
247 if (esc_result.n > 0) break :blk esc_result.n;
248
249 // No escape, could be BEL terminated
250 const bel = std.mem.indexOfScalarPos(u8, input, 2, 0x07) orelse return .{
251 .event = null,
252 .n = 0,
253 };
254 bel_terminated = true;
255 break :blk bel + 1;
256 };
257
258 // The complete OSC sequence
259 const sequence = input[0..end];
260
261 const null_event: Result = .{ .event = null, .n = sequence.len };
262
263 const semicolon_idx = std.mem.indexOfScalarPos(u8, input, 2, ';') orelse return null_event;
264 const ps = std.fmt.parseUnsigned(u8, input[2..semicolon_idx], 10) catch return null_event;
265
266 switch (ps) {
267 4 => {
268 const color_idx_delim = std.mem.indexOfScalarPos(u8, input, semicolon_idx + 1, ';') orelse return null_event;
269 const ps_idx = std.fmt.parseUnsigned(u8, input[semicolon_idx + 1 .. color_idx_delim], 10) catch return null_event;
270 const color_spec = if (bel_terminated)
271 input[color_idx_delim + 1 .. sequence.len - 1]
272 else
273 input[color_idx_delim + 1 .. sequence.len - 2];
274
275 const color = try Color.rgbFromSpec(color_spec);
276 const event: Color.Report = .{
277 .kind = .{ .index = ps_idx },
278 .value = color.rgb,
279 };
280 return .{
281 .event = .{ .color_report = event },
282 .n = sequence.len,
283 };
284 },
285 10,
286 11,
287 12,
288 => {
289 const color_spec = if (bel_terminated)
290 input[semicolon_idx + 1 .. sequence.len - 1]
291 else
292 input[semicolon_idx + 1 .. sequence.len - 2];
293
294 const color = try Color.rgbFromSpec(color_spec);
295 const event: Color.Report = .{
296 .kind = switch (ps) {
297 10 => .fg,
298 11 => .bg,
299 12 => .cursor,
300 else => unreachable,
301 },
302 .value = color.rgb,
303 };
304 return .{
305 .event = .{ .color_report = event },
306 .n = sequence.len,
307 };
308 },
309 52 => {
310 if (input[semicolon_idx + 1] != 'c') return null_event;
311 const payload = if (bel_terminated)
312 input[semicolon_idx + 3 .. sequence.len - 1]
313 else
314 input[semicolon_idx + 3 .. sequence.len - 2];
315 const decoder = std.base64.standard.Decoder;
316 const text = try paste_allocator.?.alloc(u8, try decoder.calcSizeForSlice(payload));
317 try decoder.decode(text, payload);
318 log.debug("decoded paste: {s}", .{text});
319 return .{
320 .event = .{ .paste = text },
321 .n = sequence.len,
322 };
323 },
324 else => return null_event,
325 }
326}
327
328inline fn parseCsi(input: []const u8, text_buf: []u8) Result {
329 if (input.len < 3) {
330 return .{
331 .event = null,
332 .n = 0,
333 };
334 }
335 // We start iterating at index 2 to get past the '['
336 const sequence = for (input[2..], 2..) |b, i| {
337 switch (b) {
338 0x40...0xFF => break input[0 .. i + 1],
339 else => continue,
340 }
341 } else return .{ .event = null, .n = 0 };
342 const null_event: Result = .{ .event = null, .n = sequence.len };
343
344 const final = sequence[sequence.len - 1];
345 switch (final) {
346 'A', 'B', 'C', 'D', 'E', 'F', 'H', 'P', 'Q', 'R', 'S' => {
347 // Legacy keys
348 // CSI {ABCDEFHPQS}
349 // CSI 1 ; modifier:event_type {ABCDEFHPQS}
350
351 // Split first into fields delimited by ';'
352 var field_iter = std.mem.splitScalar(u8, sequence[2 .. sequence.len - 1], ';');
353
354 // skip the first field
355 _ = field_iter.next(); //
356
357 var is_release: bool = false;
358 var key: Key = .{
359 .codepoint = switch (final) {
360 'A' => Key.up,
361 'B' => Key.down,
362 'C' => Key.right,
363 'D' => Key.left,
364 'E' => Key.kp_begin,
365 'F' => Key.end,
366 'H' => Key.home,
367 'P' => Key.f1,
368 'Q' => Key.f2,
369 'R' => Key.f3,
370 'S' => Key.f4,
371 else => return null_event,
372 },
373 };
374
375 field2: {
376 // modifier_mask:event_type
377 const field_buf = field_iter.next() orelse break :field2;
378 var param_iter = std.mem.splitScalar(u8, field_buf, ':');
379 const modifier_buf = param_iter.next() orelse unreachable;
380 const modifier_mask = parseParam(u8, modifier_buf, 1) orelse return null_event;
381 key.mods = @bitCast(modifier_mask -| 1);
382
383 if (param_iter.next()) |event_type_buf| {
384 is_release = std.mem.eql(u8, event_type_buf, "3");
385 }
386 }
387
388 field3: {
389 // text_as_codepoint[:text_as_codepoint]
390 const field_buf = field_iter.next() orelse break :field3;
391 var param_iter = std.mem.splitScalar(u8, field_buf, ':');
392 var total: usize = 0;
393 while (param_iter.next()) |cp_buf| {
394 const cp = parseParam(u21, cp_buf, null) orelse return null_event;
395 total += std.unicode.utf8Encode(cp, text_buf[total..]) catch return null_event;
396 }
397 key.text = text_buf[0..total];
398 }
399
400 const event: Event = if (is_release) .{ .key_release = key } else .{ .key_press = key };
401 return .{
402 .event = event,
403 .n = sequence.len,
404 };
405 },
406 'Z' => {
407 // Legacy keys
408 // CSI Z
409 return .{
410 .event = .{ .key_press = .{
411 .codepoint = Key.tab,
412 .mods = .{ .shift = true },
413 } },
414 .n = sequence.len,
415 };
416 },
417 '~' => {
418 // Legacy keys
419 // CSI number ~
420 // CSI number ; modifier ~
421 // CSI number ; modifier:event_type ; text_as_codepoint ~
422 var field_iter = std.mem.splitScalar(u8, sequence[2 .. sequence.len - 1], ';');
423 const number_buf = field_iter.next() orelse unreachable; // always will have one field
424 const number = parseParam(u16, number_buf, null) orelse return null_event;
425
426 var key: Key = .{
427 .codepoint = switch (number) {
428 2 => Key.insert,
429 3 => Key.delete,
430 5 => Key.page_up,
431 6 => Key.page_down,
432 7 => Key.home,
433 8 => Key.end,
434 11 => Key.f1,
435 12 => Key.f2,
436 13 => Key.f3,
437 14 => Key.f4,
438 15 => Key.f5,
439 17 => Key.f6,
440 18 => Key.f7,
441 19 => Key.f8,
442 20 => Key.f9,
443 21 => Key.f10,
444 23 => Key.f11,
445 24 => Key.f12,
446 200 => return .{ .event = .paste_start, .n = sequence.len },
447 201 => return .{ .event = .paste_end, .n = sequence.len },
448 57427 => Key.kp_begin,
449 else => return null_event,
450 },
451 };
452
453 var is_release: bool = false;
454 field2: {
455 // modifier_mask:event_type
456 const field_buf = field_iter.next() orelse break :field2;
457 var param_iter = std.mem.splitScalar(u8, field_buf, ':');
458 const modifier_buf = param_iter.next() orelse unreachable;
459 const modifier_mask = parseParam(u8, modifier_buf, 1) orelse return null_event;
460 key.mods = @bitCast(modifier_mask -| 1);
461
462 if (param_iter.next()) |event_type_buf| {
463 is_release = std.mem.eql(u8, event_type_buf, "3");
464 }
465 }
466
467 field3: {
468 // text_as_codepoint[:text_as_codepoint]
469 const field_buf = field_iter.next() orelse break :field3;
470 var param_iter = std.mem.splitScalar(u8, field_buf, ':');
471 var total: usize = 0;
472 while (param_iter.next()) |cp_buf| {
473 const cp = parseParam(u21, cp_buf, null) orelse return null_event;
474 total += std.unicode.utf8Encode(cp, text_buf[total..]) catch return null_event;
475 }
476 key.text = text_buf[0..total];
477 }
478
479 const event: Event = if (is_release) .{ .key_release = key } else .{ .key_press = key };
480 return .{
481 .event = event,
482 .n = sequence.len,
483 };
484 },
485
486 'I' => return .{ .event = .focus_in, .n = sequence.len },
487 'O' => return .{ .event = .focus_out, .n = sequence.len },
488 'M', 'm' => return parseMouse(sequence, input),
489 'c' => {
490 // Primary DA (CSI ? Pm c)
491 std.debug.assert(sequence.len >= 4); // ESC [ ? c == 4 bytes
492 switch (input[2]) {
493 '?' => return .{ .event = .cap_da1, .n = sequence.len },
494 else => return null_event,
495 }
496 },
497 'n' => {
498 // Device Status Report
499 // CSI Ps n
500 // CSI ? Ps n
501 std.debug.assert(sequence.len >= 3);
502 switch (sequence[2]) {
503 '?' => {
504 const delim_idx = std.mem.indexOfScalarPos(u8, input, 3, ';') orelse return null_event;
505 const ps = std.fmt.parseUnsigned(u16, input[3..delim_idx], 10) catch return null_event;
506 switch (ps) {
507 997 => {
508 // Color scheme update (CSI 997 ; Ps n)
509 // See https://github.com/contour-terminal/contour/blob/master/docs/vt-extensions/color-palette-update-notifications.md
510 switch (sequence[delim_idx + 1]) {
511 '1' => return .{
512 .event = .{ .color_scheme = .dark },
513 .n = sequence.len,
514 },
515 '2' => return .{
516 .event = .{ .color_scheme = .light },
517 .n = sequence.len,
518 },
519 else => return null_event,
520 }
521 },
522 else => return null_event,
523 }
524 },
525 else => return null_event,
526 }
527 },
528 't' => {
529 // XTWINOPS
530 // Split first into fields delimited by ';'
531 var iter = std.mem.splitScalar(u8, sequence[2 .. sequence.len - 1], ';');
532 const ps = iter.first();
533 if (std.mem.eql(u8, "48", ps)) {
534 // in band window resize
535 // CSI 48 ; height ; width ; height_pix ; width_pix t
536 const height_char = iter.next() orelse return null_event;
537 const width_char = iter.next() orelse return null_event;
538 const height_pix = iter.next() orelse "0";
539 const width_pix = iter.next() orelse "0";
540
541 const winsize: Winsize = .{
542 .rows = std.fmt.parseUnsigned(u16, height_char, 10) catch return null_event,
543 .cols = std.fmt.parseUnsigned(u16, width_char, 10) catch return null_event,
544 .x_pixel = std.fmt.parseUnsigned(u16, width_pix, 10) catch return null_event,
545 .y_pixel = std.fmt.parseUnsigned(u16, height_pix, 10) catch return null_event,
546 };
547 return .{
548 .event = .{ .winsize = winsize },
549 .n = sequence.len,
550 };
551 }
552 return null_event;
553 },
554 'u' => {
555 // Kitty keyboard
556 // CSI unicode-key-code:alternate-key-codes ; modifiers:event-type ; text-as-codepoints u
557 // Not all fields will be present. Only unicode-key-code is
558 // mandatory
559
560 if (sequence.len > 2 and sequence[2] == '?') return .{
561 .event = .cap_kitty_keyboard,
562 .n = sequence.len,
563 };
564
565 var key: Key = .{
566 .codepoint = undefined,
567 };
568 // Split first into fields delimited by ';'
569 var field_iter = std.mem.splitScalar(u8, sequence[2 .. sequence.len - 1], ';');
570
571 { // field 1
572 // unicode-key-code:shifted_codepoint:base_layout_codepoint
573 const field_buf = field_iter.next() orelse unreachable; // There will always be at least one field
574 var param_iter = std.mem.splitScalar(u8, field_buf, ':');
575 const codepoint_buf = param_iter.next() orelse unreachable;
576 key.codepoint = parseParam(u21, codepoint_buf, null) orelse return null_event;
577
578 if (param_iter.next()) |shifted_cp_buf| {
579 key.shifted_codepoint = parseParam(u21, shifted_cp_buf, null);
580 }
581 if (param_iter.next()) |base_layout_buf| {
582 key.base_layout_codepoint = parseParam(u21, base_layout_buf, null);
583 }
584 }
585
586 var is_release: bool = false;
587
588 field2: {
589 // modifier_mask:event_type
590 const field_buf = field_iter.next() orelse break :field2;
591 var param_iter = std.mem.splitScalar(u8, field_buf, ':');
592 const modifier_buf = param_iter.next() orelse unreachable;
593 const modifier_mask = parseParam(u8, modifier_buf, 1) orelse return null_event;
594 key.mods = @bitCast(modifier_mask -| 1);
595
596 if (param_iter.next()) |event_type_buf| {
597 is_release = std.mem.eql(u8, event_type_buf, "3");
598 }
599 }
600
601 field3: {
602 // text_as_codepoint[:text_as_codepoint]
603 const field_buf = field_iter.next() orelse break :field3;
604 var param_iter = std.mem.splitScalar(u8, field_buf, ':');
605 var total: usize = 0;
606 while (param_iter.next()) |cp_buf| {
607 const cp = parseParam(u21, cp_buf, null) orelse return null_event;
608 total += std.unicode.utf8Encode(cp, text_buf[total..]) catch return null_event;
609 }
610 key.text = text_buf[0..total];
611 }
612
613 {
614 // We check if we have *only* shift, no text, and a printable character. This can
615 // happen when we have disambiguate on and a key is pressed and encoded as CSI u,
616 // for example shift + space can produce CSI 32 ; 2 u
617 const mod_test: Key.Modifiers = .{
618 .shift = true,
619 .caps_lock = key.mods.caps_lock,
620 .num_lock = key.mods.num_lock,
621 };
622 if (key.text == null and
623 key.mods.eql(mod_test) and
624 key.codepoint <= std.math.maxInt(u8) and
625 std.ascii.isPrint(@intCast(key.codepoint)))
626 {
627 // Encode the codepoint as upper
628 const upper = std.ascii.toUpper(@intCast(key.codepoint));
629 const n = std.unicode.utf8Encode(upper, text_buf) catch unreachable;
630 key.text = text_buf[0..n];
631 key.shifted_codepoint = upper;
632 }
633 }
634
635 const event: Event = if (is_release)
636 .{ .key_release = key }
637 else
638 .{ .key_press = key };
639
640 return .{ .event = event, .n = sequence.len };
641 },
642 'y' => {
643 // DECRPM (CSI ? Ps ; Pm $ y)
644 const delim_idx = std.mem.indexOfScalarPos(u8, input, 3, ';') orelse return null_event;
645 const ps = std.fmt.parseUnsigned(u16, input[3..delim_idx], 10) catch return null_event;
646 const pm = std.fmt.parseUnsigned(u8, input[delim_idx + 1 .. sequence.len - 2], 10) catch return null_event;
647 switch (ps) {
648 // Mouse Pixel reporting
649 1016 => switch (pm) {
650 0, 4 => return null_event,
651 else => return .{ .event = .cap_sgr_pixels, .n = sequence.len },
652 },
653 // Unicode Core, see https://github.com/contour-terminal/terminal-unicode-core
654 2027 => switch (pm) {
655 0, 4 => return null_event,
656 else => return .{ .event = .cap_unicode, .n = sequence.len },
657 },
658 // Color scheme reportnig, see https://github.com/contour-terminal/contour/blob/master/docs/vt-extensions/color-palette-update-notifications.md
659 2031 => switch (pm) {
660 0, 4 => return null_event,
661 else => return .{ .event = .cap_color_scheme_updates, .n = sequence.len },
662 },
663 else => return null_event,
664 }
665 },
666 'q' => {
667 // kitty multi cursor cap (CSI > 1;2;3;29;30;40;100;101 TRAILER) (TRAILER is " q")
668 const second_final = sequence[sequence.len - 2];
669 if (second_final != ' ') return null_event;
670 // check for any digits. we're not too picky about checking the supported cursor types here
671 for (sequence[0 .. sequence.len - 2]) |c| switch (c) {
672 '0'...'9' => return .{ .event = .cap_multi_cursor, .n = sequence.len },
673 else => continue,
674 };
675 return null_event;
676 },
677 else => return null_event,
678 }
679}
680
681/// Parse a param buffer, returning a default value if the param was empty
682inline fn parseParam(comptime T: type, buf: []const u8, default: ?T) ?T {
683 if (buf.len == 0) return default;
684 return std.fmt.parseInt(T, buf, 10) catch return null;
685}
686
687/// Parse a mouse event
688inline fn parseMouse(input: []const u8, full_input: []const u8) Result {
689 const null_event: Result = .{ .event = null, .n = input.len };
690
691 var button_mask: u16 = undefined;
692 var px: i16 = undefined;
693 var py: i16 = undefined;
694 var xterm: bool = undefined;
695 if (input.len == 3 and (input[2] == 'M') and full_input.len >= 6) {
696 xterm = true;
697 button_mask = full_input[3] - 32;
698 px = full_input[4] - 32;
699 py = full_input[5] - 32;
700 } else if (input.len >= 4 and input[2] == '<') {
701 xterm = false;
702 const delim1 = std.mem.indexOfScalarPos(u8, input, 3, ';') orelse return null_event;
703 button_mask = parseParam(u16, input[3..delim1], null) orelse return null_event;
704 const delim2 = std.mem.indexOfScalarPos(u8, input, delim1 + 1, ';') orelse return null_event;
705 px = parseParam(i16, input[delim1 + 1 .. delim2], 1) orelse return null_event;
706 py = parseParam(i16, input[delim2 + 1 .. input.len - 1], 1) orelse return null_event;
707 } else {
708 return null_event;
709 }
710
711 if (button_mask & mouse_bits.leave > 0)
712 return .{ .event = .mouse_leave, .n = if (xterm) 6 else input.len };
713
714 const button: Mouse.Button = @enumFromInt(button_mask & mouse_bits.buttons);
715 const motion = button_mask & mouse_bits.motion > 0;
716 const shift = button_mask & mouse_bits.shift > 0;
717 const alt = button_mask & mouse_bits.alt > 0;
718 const ctrl = button_mask & mouse_bits.ctrl > 0;
719
720 const mouse = Mouse{
721 .button = button,
722 .mods = .{
723 .shift = shift,
724 .alt = alt,
725 .ctrl = ctrl,
726 },
727 .col = px -| 1,
728 .row = py -| 1,
729 .type = blk: {
730 if (motion and button != Mouse.Button.none) {
731 break :blk .drag;
732 }
733 if (motion and button == Mouse.Button.none) {
734 break :blk .motion;
735 }
736 if (xterm) {
737 if (button == Mouse.Button.none) {
738 break :blk .release;
739 }
740 break :blk .press;
741 }
742 if (input[input.len - 1] == 'm') break :blk .release;
743 break :blk .press;
744 },
745 };
746 return .{ .event = .{ .mouse = mouse }, .n = if (xterm) 6 else input.len };
747}
748
749test "parse: single xterm keypress" {
750 const alloc = testing.allocator_instance.allocator();
751 const input = "a";
752 var parser: Parser = .{};
753 const result = try parser.parse(input, alloc);
754 const expected_key: Key = .{
755 .codepoint = 'a',
756 .text = "a",
757 };
758 const expected_event: Event = .{ .key_press = expected_key };
759
760 try testing.expectEqual(1, result.n);
761 try testing.expectEqual(expected_event, result.event);
762}
763
764test "parse: single xterm keypress backspace" {
765 const alloc = testing.allocator_instance.allocator();
766 const input = "\x08";
767 var parser: Parser = .{};
768 const result = try parser.parse(input, alloc);
769 const expected_key: Key = .{
770 .codepoint = Key.backspace,
771 };
772 const expected_event: Event = .{ .key_press = expected_key };
773
774 try testing.expectEqual(1, result.n);
775 try testing.expectEqual(expected_event, result.event);
776}
777
778test "parse: single xterm keypress with more buffer" {
779 const alloc = testing.allocator_instance.allocator();
780 const input = "ab";
781 var parser: Parser = .{};
782 const result = try parser.parse(input, alloc);
783 const expected_key: Key = .{
784 .codepoint = 'a',
785 .text = "a",
786 };
787 const expected_event: Event = .{ .key_press = expected_key };
788
789 try testing.expectEqual(1, result.n);
790 try testing.expectEqualStrings(expected_key.text.?, result.event.?.key_press.text.?);
791 try testing.expectEqualDeep(expected_event, result.event);
792}
793
794test "parse: xterm escape keypress" {
795 const alloc = testing.allocator_instance.allocator();
796 const input = "\x1b";
797 var parser: Parser = .{};
798 const result = try parser.parse(input, alloc);
799 const expected_key: Key = .{ .codepoint = Key.escape };
800 const expected_event: Event = .{ .key_press = expected_key };
801
802 try testing.expectEqual(1, result.n);
803 try testing.expectEqual(expected_event, result.event);
804}
805
806test "parse: xterm ctrl+a" {
807 const alloc = testing.allocator_instance.allocator();
808 const input = "\x01";
809 var parser: Parser = .{};
810 const result = try parser.parse(input, alloc);
811 const expected_key: Key = .{ .codepoint = 'a', .mods = .{ .ctrl = true } };
812 const expected_event: Event = .{ .key_press = expected_key };
813
814 try testing.expectEqual(1, result.n);
815 try testing.expectEqual(expected_event, result.event);
816}
817
818test "parse: xterm alt+a" {
819 const alloc = testing.allocator_instance.allocator();
820 const input = "\x1ba";
821 var parser: Parser = .{};
822 const result = try parser.parse(input, alloc);
823 const expected_key: Key = .{ .codepoint = 'a', .mods = .{ .alt = true } };
824 const expected_event: Event = .{ .key_press = expected_key };
825
826 try testing.expectEqual(2, result.n);
827 try testing.expectEqual(expected_event, result.event);
828}
829
830test "parse: xterm key up" {
831 const alloc = testing.allocator_instance.allocator();
832 {
833 // normal version
834 const input = "\x1b[A";
835 var parser: Parser = .{};
836 const result = try parser.parse(input, alloc);
837 const expected_key: Key = .{ .codepoint = Key.up };
838 const expected_event: Event = .{ .key_press = expected_key };
839
840 try testing.expectEqual(3, result.n);
841 try testing.expectEqual(expected_event, result.event);
842 }
843
844 {
845 // application keys version
846 const input = "\x1bOA";
847 var parser: Parser = .{};
848 const result = try parser.parse(input, alloc);
849 const expected_key: Key = .{ .codepoint = Key.up };
850 const expected_event: Event = .{ .key_press = expected_key };
851
852 try testing.expectEqual(3, result.n);
853 try testing.expectEqual(expected_event, result.event);
854 }
855}
856
857test "parse: xterm shift+up" {
858 const alloc = testing.allocator_instance.allocator();
859 const input = "\x1b[1;2A";
860 var parser: Parser = .{};
861 const result = try parser.parse(input, alloc);
862 const expected_key: Key = .{ .codepoint = Key.up, .mods = .{ .shift = true } };
863 const expected_event: Event = .{ .key_press = expected_key };
864
865 try testing.expectEqual(6, result.n);
866 try testing.expectEqual(expected_event, result.event);
867}
868
869test "parse: xterm shift+tab" {
870 const alloc = testing.allocator_instance.allocator();
871 const input = "\x1b[Z";
872 var parser: Parser = .{};
873 const result = try parser.parse(input, alloc);
874 const expected_key: Key = .{ .codepoint = Key.tab, .mods = .{ .shift = true } };
875 const expected_event: Event = .{ .key_press = expected_key };
876
877 try testing.expectEqual(3, result.n);
878 try testing.expectEqual(expected_event, result.event);
879}
880
881test "parse: xterm insert" {
882 const alloc = testing.allocator_instance.allocator();
883 const input = "\x1b[2~";
884 var parser: Parser = .{};
885 const result = try parser.parse(input, alloc);
886 const expected_key: Key = .{ .codepoint = Key.insert, .mods = .{} };
887 const expected_event: Event = .{ .key_press = expected_key };
888
889 try testing.expectEqual(input.len, result.n);
890 try testing.expectEqual(expected_event, result.event);
891}
892
893test "parse: paste_start" {
894 const alloc = testing.allocator_instance.allocator();
895 const input = "\x1b[200~";
896 var parser: Parser = .{};
897 const result = try parser.parse(input, alloc);
898 const expected_event: Event = .paste_start;
899
900 try testing.expectEqual(6, result.n);
901 try testing.expectEqual(expected_event, result.event);
902}
903
904test "parse: paste_end" {
905 const alloc = testing.allocator_instance.allocator();
906 const input = "\x1b[201~";
907 var parser: Parser = .{};
908 const result = try parser.parse(input, alloc);
909 const expected_event: Event = .paste_end;
910
911 try testing.expectEqual(6, result.n);
912 try testing.expectEqual(expected_event, result.event);
913}
914
915test "parse: osc52 paste" {
916 const alloc = testing.allocator_instance.allocator();
917 const input = "\x1b]52;c;b3NjNTIgcGFzdGU=\x1b\\";
918 const expected_text = "osc52 paste";
919 var parser: Parser = .{};
920 const result = try parser.parse(input, alloc);
921
922 try testing.expectEqual(25, result.n);
923 switch (result.event.?) {
924 .paste => |text| {
925 defer alloc.free(text);
926 try testing.expectEqualStrings(expected_text, text);
927 },
928 else => try testing.expect(false),
929 }
930}
931
932test "parse: focus_in" {
933 const alloc = testing.allocator_instance.allocator();
934 const input = "\x1b[I";
935 var parser: Parser = .{};
936 const result = try parser.parse(input, alloc);
937 const expected_event: Event = .focus_in;
938
939 try testing.expectEqual(3, result.n);
940 try testing.expectEqual(expected_event, result.event);
941}
942
943test "parse: focus_out" {
944 const alloc = testing.allocator_instance.allocator();
945 const input = "\x1b[O";
946 var parser: Parser = .{};
947 const result = try parser.parse(input, alloc);
948 const expected_event: Event = .focus_out;
949
950 try testing.expectEqual(3, result.n);
951 try testing.expectEqual(expected_event, result.event);
952}
953
954test "parse: kitty: shift+a without text reporting" {
955 const alloc = testing.allocator_instance.allocator();
956 const input = "\x1b[97:65;2u";
957 var parser: Parser = .{};
958 const result = try parser.parse(input, alloc);
959 const expected_key: Key = .{
960 .codepoint = 'a',
961 .shifted_codepoint = 'A',
962 .mods = .{ .shift = true },
963 .text = "A",
964 };
965 const expected_event: Event = .{ .key_press = expected_key };
966
967 try testing.expectEqual(10, result.n);
968 try testing.expectEqualDeep(expected_event, result.event);
969}
970
971test "parse: kitty: alt+shift+a without text reporting" {
972 const alloc = testing.allocator_instance.allocator();
973 const input = "\x1b[97:65;4u";
974 var parser: Parser = .{};
975 const result = try parser.parse(input, alloc);
976 const expected_key: Key = .{
977 .codepoint = 'a',
978 .shifted_codepoint = 'A',
979 .mods = .{ .shift = true, .alt = true },
980 };
981 const expected_event: Event = .{ .key_press = expected_key };
982
983 try testing.expectEqual(10, result.n);
984 try testing.expectEqual(expected_event, result.event);
985}
986
987test "parse: kitty: a without text reporting" {
988 const alloc = testing.allocator_instance.allocator();
989 const input = "\x1b[97u";
990 var parser: Parser = .{};
991 const result = try parser.parse(input, alloc);
992 const expected_key: Key = .{
993 .codepoint = 'a',
994 };
995 const expected_event: Event = .{ .key_press = expected_key };
996
997 try testing.expectEqual(5, result.n);
998 try testing.expectEqual(expected_event, result.event);
999}
1000
1001test "parse: kitty: release event" {
1002 const alloc = testing.allocator_instance.allocator();
1003 const input = "\x1b[97;1:3u";
1004 var parser: Parser = .{};
1005 const result = try parser.parse(input, alloc);
1006 const expected_key: Key = .{
1007 .codepoint = 'a',
1008 };
1009 const expected_event: Event = .{ .key_release = expected_key };
1010
1011 try testing.expectEqual(9, result.n);
1012 try testing.expectEqual(expected_event, result.event);
1013}
1014
1015test "parse: single codepoint" {
1016 const alloc = testing.allocator_instance.allocator();
1017 const input = "🙂";
1018 var parser: Parser = .{};
1019 const result = try parser.parse(input, alloc);
1020 const expected_key: Key = .{
1021 .codepoint = 0x1F642,
1022 .text = input,
1023 };
1024 const expected_event: Event = .{ .key_press = expected_key };
1025
1026 try testing.expectEqual(4, result.n);
1027 try testing.expectEqual(expected_event, result.event);
1028}
1029
1030test "parse: single codepoint with more in buffer" {
1031 const alloc = testing.allocator_instance.allocator();
1032 const input = "🙂a";
1033 var parser: Parser = .{};
1034 const result = try parser.parse(input, alloc);
1035 const expected_key: Key = .{
1036 .codepoint = 0x1F642,
1037 .text = "🙂",
1038 };
1039 const expected_event: Event = .{ .key_press = expected_key };
1040
1041 try testing.expectEqual(4, result.n);
1042 try testing.expectEqualDeep(expected_event, result.event);
1043}
1044
1045test "parse: multiple codepoint grapheme" {
1046 const alloc = testing.allocator_instance.allocator();
1047 const input = "👩🚀";
1048 var parser: Parser = .{};
1049 const result = try parser.parse(input, alloc);
1050 const expected_key: Key = .{
1051 .codepoint = Key.multicodepoint,
1052 .text = input,
1053 };
1054 const expected_event: Event = .{ .key_press = expected_key };
1055
1056 try testing.expectEqual(input.len, result.n);
1057 try testing.expectEqual(expected_event, result.event);
1058}
1059
1060test "parse: multiple codepoint grapheme with more after" {
1061 const alloc = testing.allocator_instance.allocator();
1062 const input = "👩🚀abc";
1063 var parser: Parser = .{};
1064 const result = try parser.parse(input, alloc);
1065 const expected_key: Key = .{
1066 .codepoint = Key.multicodepoint,
1067 .text = "👩🚀",
1068 };
1069
1070 try testing.expectEqual(expected_key.text.?.len, result.n);
1071 const actual = result.event.?.key_press;
1072 try testing.expectEqualStrings(expected_key.text.?, actual.text.?);
1073 try testing.expectEqual(expected_key.codepoint, actual.codepoint);
1074}
1075
1076test "parse: flag emoji" {
1077 const alloc = testing.allocator_instance.allocator();
1078 const input = "🇺🇸";
1079 var parser: Parser = .{};
1080 const result = try parser.parse(input, alloc);
1081 const expected_key: Key = .{
1082 .codepoint = Key.multicodepoint,
1083 .text = input,
1084 };
1085 const expected_event: Event = .{ .key_press = expected_key };
1086
1087 try testing.expectEqual(input.len, result.n);
1088 try testing.expectEqual(expected_event, result.event);
1089}
1090
1091test "parse: combining mark" {
1092 const alloc = testing.allocator_instance.allocator();
1093 // a with combining acute accent (NFD form)
1094 const input = "a\u{0301}";
1095 var parser: Parser = .{};
1096 const result = try parser.parse(input, alloc);
1097 const expected_key: Key = .{
1098 .codepoint = Key.multicodepoint,
1099 .text = input,
1100 };
1101 const expected_event: Event = .{ .key_press = expected_key };
1102
1103 try testing.expectEqual(input.len, result.n);
1104 try testing.expectEqual(expected_event, result.event);
1105}
1106
1107test "parse: skin tone emoji" {
1108 const alloc = testing.allocator_instance.allocator();
1109 const input = "👋🏿";
1110 var parser: Parser = .{};
1111 const result = try parser.parse(input, alloc);
1112 const expected_key: Key = .{
1113 .codepoint = Key.multicodepoint,
1114 .text = input,
1115 };
1116 const expected_event: Event = .{ .key_press = expected_key };
1117
1118 try testing.expectEqual(input.len, result.n);
1119 try testing.expectEqual(expected_event, result.event);
1120}
1121
1122test "parse: text variation selector" {
1123 const alloc = testing.allocator_instance.allocator();
1124 // Heavy black heart with text variation selector
1125 const input = "❤︎";
1126 var parser: Parser = .{};
1127 const result = try parser.parse(input, alloc);
1128 const expected_key: Key = .{
1129 .codepoint = Key.multicodepoint,
1130 .text = input,
1131 };
1132 const expected_event: Event = .{ .key_press = expected_key };
1133
1134 try testing.expectEqual(input.len, result.n);
1135 try testing.expectEqual(expected_event, result.event);
1136}
1137
1138test "parse: keycap sequence" {
1139 const alloc = testing.allocator_instance.allocator();
1140 const input = "1️⃣";
1141 var parser: Parser = .{};
1142 const result = try parser.parse(input, alloc);
1143 const expected_key: Key = .{
1144 .codepoint = Key.multicodepoint,
1145 .text = input,
1146 };
1147 const expected_event: Event = .{ .key_press = expected_key };
1148
1149 try testing.expectEqual(input.len, result.n);
1150 try testing.expectEqual(expected_event, result.event);
1151}
1152
1153test "parse(csi): kitty multi cursor" {
1154 var buf: [1]u8 = undefined;
1155 {
1156 const input = "\x1b[>1;2;3;29;30;40;100;101 q";
1157 const result = parseCsi(input, &buf);
1158 const expected: Result = .{
1159 .event = .cap_multi_cursor,
1160 .n = input.len,
1161 };
1162
1163 try testing.expectEqual(expected.n, result.n);
1164 try testing.expectEqual(expected.event, result.event);
1165 }
1166 {
1167 const input = "\x1b[> q";
1168 const result = parseCsi(input, &buf);
1169 const expected: Result = .{
1170 .event = null,
1171 .n = input.len,
1172 };
1173
1174 try testing.expectEqual(expected.n, result.n);
1175 try testing.expectEqual(expected.event, result.event);
1176 }
1177}
1178
1179test "parse(csi): decrpm" {
1180 var buf: [1]u8 = undefined;
1181 {
1182 const input = "\x1b[?1016;1$y";
1183 const result = parseCsi(input, &buf);
1184 const expected: Result = .{
1185 .event = .cap_sgr_pixels,
1186 .n = input.len,
1187 };
1188
1189 try testing.expectEqual(expected.n, result.n);
1190 try testing.expectEqual(expected.event, result.event);
1191 }
1192 {
1193 const input = "\x1b[?1016;0$y";
1194 const result = parseCsi(input, &buf);
1195 const expected: Result = .{
1196 .event = null,
1197 .n = input.len,
1198 };
1199
1200 try testing.expectEqual(expected.n, result.n);
1201 try testing.expectEqual(expected.event, result.event);
1202 }
1203}
1204
1205test "parse(csi): primary da" {
1206 var buf: [1]u8 = undefined;
1207 const input = "\x1b[?c";
1208 const result = parseCsi(input, &buf);
1209 const expected: Result = .{
1210 .event = .cap_da1,
1211 .n = input.len,
1212 };
1213
1214 try testing.expectEqual(expected.n, result.n);
1215 try testing.expectEqual(expected.event, result.event);
1216}
1217
1218test "parse(csi): dsr" {
1219 var buf: [1]u8 = undefined;
1220 {
1221 const input = "\x1b[?997;1n";
1222 const result = parseCsi(input, &buf);
1223 const expected: Result = .{
1224 .event = .{ .color_scheme = .dark },
1225 .n = input.len,
1226 };
1227
1228 try testing.expectEqual(expected.n, result.n);
1229 try testing.expectEqual(expected.event, result.event);
1230 }
1231 {
1232 const input = "\x1b[?997;2n";
1233 const result = parseCsi(input, &buf);
1234 const expected: Result = .{
1235 .event = .{ .color_scheme = .light },
1236 .n = input.len,
1237 };
1238
1239 try testing.expectEqual(expected.n, result.n);
1240 try testing.expectEqual(expected.event, result.event);
1241 }
1242 {
1243 const input = "\x1b[0n";
1244 const result = parseCsi(input, &buf);
1245 const expected: Result = .{
1246 .event = null,
1247 .n = input.len,
1248 };
1249
1250 try testing.expectEqual(expected.n, result.n);
1251 try testing.expectEqual(expected.event, result.event);
1252 }
1253}
1254
1255test "parse(csi): mouse" {
1256 var buf: [1]u8 = undefined;
1257 const input = "\x1b[<35;1;1m";
1258 const result = parseCsi(input, &buf);
1259 const expected: Result = .{
1260 .event = .{ .mouse = .{
1261 .col = 0,
1262 .row = 0,
1263 .button = .none,
1264 .type = .motion,
1265 .mods = .{},
1266 } },
1267 .n = input.len,
1268 };
1269
1270 try testing.expectEqual(expected.n, result.n);
1271 try testing.expectEqual(expected.event, result.event);
1272}
1273
1274test "parse(csi): mouse (negative)" {
1275 var buf: [1]u8 = undefined;
1276 const input = "\x1b[<35;-50;-100m";
1277 const result = parseCsi(input, &buf);
1278 const expected: Result = .{
1279 .event = .{ .mouse = .{
1280 .col = -51,
1281 .row = -101,
1282 .button = .none,
1283 .type = .motion,
1284 .mods = .{},
1285 } },
1286 .n = input.len,
1287 };
1288
1289 try testing.expectEqual(expected.n, result.n);
1290 try testing.expectEqual(expected.event, result.event);
1291}
1292
1293test "parse(csi): xterm mouse" {
1294 var buf: [1]u8 = undefined;
1295 const input = "\x1b[M\x20\x21\x21";
1296 const result = parseCsi(input, &buf);
1297 const expected: Result = .{
1298 .event = .{ .mouse = .{
1299 .col = 0,
1300 .row = 0,
1301 .button = .left,
1302 .type = .press,
1303 .mods = .{},
1304 } },
1305 .n = input.len,
1306 };
1307
1308 try testing.expectEqual(expected.n, result.n);
1309 try testing.expectEqual(expected.event, result.event);
1310}
1311
1312test "parse: disambiguate shift + space" {
1313 const alloc = testing.allocator_instance.allocator();
1314 const input = "\x1b[32;2u";
1315 var parser: Parser = .{};
1316 const result = try parser.parse(input, alloc);
1317 const expected_key: Key = .{
1318 .codepoint = ' ',
1319 .shifted_codepoint = ' ',
1320 .mods = .{ .shift = true },
1321 .text = " ",
1322 };
1323 const expected_event: Event = .{ .key_press = expected_key };
1324
1325 try testing.expectEqual(7, result.n);
1326 try testing.expectEqualDeep(expected_event, result.event);
1327}