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