a modern tui library written in zig
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 '~' => {
407 // Legacy keys
408 // CSI number ~
409 // CSI number ; modifier ~
410 // CSI number ; modifier:event_type ; text_as_codepoint ~
411 var field_iter = std.mem.splitScalar(u8, sequence[2 .. sequence.len - 1], ';');
412 const number_buf = field_iter.next() orelse unreachable; // always will have one field
413 const number = parseParam(u16, number_buf, null) orelse return null_event;
414
415 var key: Key = .{
416 .codepoint = switch (number) {
417 2 => Key.insert,
418 3 => Key.delete,
419 5 => Key.page_up,
420 6 => Key.page_down,
421 7 => Key.home,
422 8 => Key.end,
423 11 => Key.f1,
424 12 => Key.f2,
425 13 => Key.f3,
426 14 => Key.f4,
427 15 => Key.f5,
428 17 => Key.f6,
429 18 => Key.f7,
430 19 => Key.f8,
431 20 => Key.f9,
432 21 => Key.f10,
433 23 => Key.f11,
434 24 => Key.f12,
435 200 => return .{ .event = .paste_start, .n = sequence.len },
436 201 => return .{ .event = .paste_end, .n = sequence.len },
437 57427 => Key.kp_begin,
438 else => return null_event,
439 },
440 };
441
442 var is_release: bool = false;
443 field2: {
444 // modifier_mask:event_type
445 const field_buf = field_iter.next() orelse break :field2;
446 var param_iter = std.mem.splitScalar(u8, field_buf, ':');
447 const modifier_buf = param_iter.next() orelse unreachable;
448 const modifier_mask = parseParam(u8, modifier_buf, 1) orelse return null_event;
449 key.mods = @bitCast(modifier_mask -| 1);
450
451 if (param_iter.next()) |event_type_buf| {
452 is_release = std.mem.eql(u8, event_type_buf, "3");
453 }
454 }
455
456 field3: {
457 // text_as_codepoint[:text_as_codepoint]
458 const field_buf = field_iter.next() orelse break :field3;
459 var param_iter = std.mem.splitScalar(u8, field_buf, ':');
460 var total: usize = 0;
461 while (param_iter.next()) |cp_buf| {
462 const cp = parseParam(u21, cp_buf, null) orelse return null_event;
463 total += std.unicode.utf8Encode(cp, text_buf[total..]) catch return null_event;
464 }
465 key.text = text_buf[0..total];
466 }
467
468 const event: Event = if (is_release) .{ .key_release = key } else .{ .key_press = key };
469 return .{
470 .event = event,
471 .n = sequence.len,
472 };
473 },
474
475 'I' => return .{ .event = .focus_in, .n = sequence.len },
476 'O' => return .{ .event = .focus_out, .n = sequence.len },
477 'M', 'm' => return parseMouse(sequence, input),
478 'c' => {
479 // Primary DA (CSI ? Pm c)
480 std.debug.assert(sequence.len >= 4); // ESC [ ? c == 4 bytes
481 switch (input[2]) {
482 '?' => return .{ .event = .cap_da1, .n = sequence.len },
483 else => return null_event,
484 }
485 },
486 'n' => {
487 // Device Status Report
488 // CSI Ps n
489 // CSI ? Ps n
490 std.debug.assert(sequence.len >= 3);
491 switch (sequence[2]) {
492 '?' => {
493 const delim_idx = std.mem.indexOfScalarPos(u8, input, 3, ';') orelse return null_event;
494 const ps = std.fmt.parseUnsigned(u16, input[3..delim_idx], 10) catch return null_event;
495 switch (ps) {
496 997 => {
497 // Color scheme update (CSI 997 ; Ps n)
498 // See https://github.com/contour-terminal/contour/blob/master/docs/vt-extensions/color-palette-update-notifications.md
499 switch (sequence[delim_idx + 1]) {
500 '1' => return .{
501 .event = .{ .color_scheme = .dark },
502 .n = sequence.len,
503 },
504 '2' => return .{
505 .event = .{ .color_scheme = .light },
506 .n = sequence.len,
507 },
508 else => return null_event,
509 }
510 },
511 else => return null_event,
512 }
513 },
514 else => return null_event,
515 }
516 },
517 't' => {
518 // XTWINOPS
519 // Split first into fields delimited by ';'
520 var iter = std.mem.splitScalar(u8, sequence[2 .. sequence.len - 1], ';');
521 const ps = iter.first();
522 if (std.mem.eql(u8, "48", ps)) {
523 // in band window resize
524 // CSI 48 ; height ; width ; height_pix ; width_pix t
525 const height_char = iter.next() orelse return null_event;
526 const width_char = iter.next() orelse return null_event;
527 const height_pix = iter.next() orelse "0";
528 const width_pix = iter.next() orelse "0";
529
530 const winsize: Winsize = .{
531 .rows = std.fmt.parseUnsigned(u16, height_char, 10) catch return null_event,
532 .cols = std.fmt.parseUnsigned(u16, width_char, 10) catch return null_event,
533 .x_pixel = std.fmt.parseUnsigned(u16, width_pix, 10) catch return null_event,
534 .y_pixel = std.fmt.parseUnsigned(u16, height_pix, 10) catch return null_event,
535 };
536 return .{
537 .event = .{ .winsize = winsize },
538 .n = sequence.len,
539 };
540 }
541 return null_event;
542 },
543 'u' => {
544 // Kitty keyboard
545 // CSI unicode-key-code:alternate-key-codes ; modifiers:event-type ; text-as-codepoints u
546 // Not all fields will be present. Only unicode-key-code is
547 // mandatory
548
549 if (sequence.len > 2 and sequence[2] == '?') return .{
550 .event = .cap_kitty_keyboard,
551 .n = sequence.len,
552 };
553
554 var key: Key = .{
555 .codepoint = undefined,
556 };
557 // Split first into fields delimited by ';'
558 var field_iter = std.mem.splitScalar(u8, sequence[2 .. sequence.len - 1], ';');
559
560 { // field 1
561 // unicode-key-code:shifted_codepoint:base_layout_codepoint
562 const field_buf = field_iter.next() orelse unreachable; // There will always be at least one field
563 var param_iter = std.mem.splitScalar(u8, field_buf, ':');
564 const codepoint_buf = param_iter.next() orelse unreachable;
565 key.codepoint = parseParam(u21, codepoint_buf, null) orelse return null_event;
566
567 if (param_iter.next()) |shifted_cp_buf| {
568 key.shifted_codepoint = parseParam(u21, shifted_cp_buf, null);
569 }
570 if (param_iter.next()) |base_layout_buf| {
571 key.base_layout_codepoint = parseParam(u21, base_layout_buf, null);
572 }
573 }
574
575 var is_release: bool = false;
576
577 field2: {
578 // modifier_mask:event_type
579 const field_buf = field_iter.next() orelse break :field2;
580 var param_iter = std.mem.splitScalar(u8, field_buf, ':');
581 const modifier_buf = param_iter.next() orelse unreachable;
582 const modifier_mask = parseParam(u8, modifier_buf, 1) orelse return null_event;
583 key.mods = @bitCast(modifier_mask -| 1);
584
585 if (param_iter.next()) |event_type_buf| {
586 is_release = std.mem.eql(u8, event_type_buf, "3");
587 }
588 }
589
590 field3: {
591 // text_as_codepoint[:text_as_codepoint]
592 const field_buf = field_iter.next() orelse break :field3;
593 var param_iter = std.mem.splitScalar(u8, field_buf, ':');
594 var total: usize = 0;
595 while (param_iter.next()) |cp_buf| {
596 const cp = parseParam(u21, cp_buf, null) orelse return null_event;
597 total += std.unicode.utf8Encode(cp, text_buf[total..]) catch return null_event;
598 }
599 key.text = text_buf[0..total];
600 }
601
602 {
603 // We check if we have *only* shift, no text, and a printable character. This can
604 // happen when we have disambiguate on and a key is pressed and encoded as CSI u,
605 // for example shift + space can produce CSI 32 ; 2 u
606 const mod_test: Key.Modifiers = .{
607 .shift = true,
608 .caps_lock = key.mods.caps_lock,
609 .num_lock = key.mods.num_lock,
610 };
611 if (key.text == null and
612 key.mods.eql(mod_test) and
613 key.codepoint <= std.math.maxInt(u8) and
614 std.ascii.isPrint(@intCast(key.codepoint)))
615 {
616 // Encode the codepoint as upper
617 const upper = std.ascii.toUpper(@intCast(key.codepoint));
618 const n = std.unicode.utf8Encode(upper, text_buf) catch unreachable;
619 key.text = text_buf[0..n];
620 key.shifted_codepoint = upper;
621 }
622 }
623
624 const event: Event = if (is_release)
625 .{ .key_release = key }
626 else
627 .{ .key_press = key };
628
629 return .{ .event = event, .n = sequence.len };
630 },
631 'y' => {
632 // DECRPM (CSI ? Ps ; Pm $ y)
633 const delim_idx = std.mem.indexOfScalarPos(u8, input, 3, ';') orelse return null_event;
634 const ps = std.fmt.parseUnsigned(u16, input[3..delim_idx], 10) catch return null_event;
635 const pm = std.fmt.parseUnsigned(u8, input[delim_idx + 1 .. sequence.len - 2], 10) catch return null_event;
636 switch (ps) {
637 // Mouse Pixel reporting
638 1016 => switch (pm) {
639 0, 4 => return null_event,
640 else => return .{ .event = .cap_sgr_pixels, .n = sequence.len },
641 },
642 // Unicode Core, see https://github.com/contour-terminal/terminal-unicode-core
643 2027 => switch (pm) {
644 0, 4 => return null_event,
645 else => return .{ .event = .cap_unicode, .n = sequence.len },
646 },
647 // Color scheme reportnig, see https://github.com/contour-terminal/contour/blob/master/docs/vt-extensions/color-palette-update-notifications.md
648 2031 => switch (pm) {
649 0, 4 => return null_event,
650 else => return .{ .event = .cap_color_scheme_updates, .n = sequence.len },
651 },
652 else => return null_event,
653 }
654 },
655 'q' => {
656 // kitty multi cursor cap (CSI > 1;2;3;29;30;40;100;101 TRAILER) (TRAILER is " q")
657 const second_final = sequence[sequence.len - 2];
658 if (second_final != ' ') return null_event;
659 // check for any digits. we're not too picky about checking the supported cursor types here
660 for (sequence[0 .. sequence.len - 2]) |c| switch (c) {
661 '0'...'9' => return .{ .event = .cap_multi_cursor, .n = sequence.len },
662 else => continue,
663 };
664 return null_event;
665 },
666 else => return null_event,
667 }
668}
669
670/// Parse a param buffer, returning a default value if the param was empty
671inline fn parseParam(comptime T: type, buf: []const u8, default: ?T) ?T {
672 if (buf.len == 0) return default;
673 return std.fmt.parseInt(T, buf, 10) catch return null;
674}
675
676/// Parse a mouse event
677inline fn parseMouse(input: []const u8, full_input: []const u8) Result {
678 const null_event: Result = .{ .event = null, .n = input.len };
679
680 var button_mask: u16 = undefined;
681 var px: i16 = undefined;
682 var py: i16 = undefined;
683 var xterm: bool = undefined;
684 if (input.len == 3 and (input[2] == 'M') and full_input.len >= 6) {
685 xterm = true;
686 button_mask = full_input[3] - 32;
687 px = full_input[4] - 32;
688 py = full_input[5] - 32;
689 } else if (input.len >= 4 and input[2] == '<') {
690 xterm = false;
691 const delim1 = std.mem.indexOfScalarPos(u8, input, 3, ';') orelse return null_event;
692 button_mask = parseParam(u16, input[3..delim1], null) orelse return null_event;
693 const delim2 = std.mem.indexOfScalarPos(u8, input, delim1 + 1, ';') orelse return null_event;
694 px = parseParam(i16, input[delim1 + 1 .. delim2], 1) orelse return null_event;
695 py = parseParam(i16, input[delim2 + 1 .. input.len - 1], 1) orelse return null_event;
696 } else {
697 return null_event;
698 }
699
700 if (button_mask & mouse_bits.leave > 0)
701 return .{ .event = .mouse_leave, .n = if (xterm) 6 else input.len };
702
703 const button: Mouse.Button = @enumFromInt(button_mask & mouse_bits.buttons);
704 const motion = button_mask & mouse_bits.motion > 0;
705 const shift = button_mask & mouse_bits.shift > 0;
706 const alt = button_mask & mouse_bits.alt > 0;
707 const ctrl = button_mask & mouse_bits.ctrl > 0;
708
709 const mouse = Mouse{
710 .button = button,
711 .mods = .{
712 .shift = shift,
713 .alt = alt,
714 .ctrl = ctrl,
715 },
716 .col = px -| 1,
717 .row = py -| 1,
718 .type = blk: {
719 if (motion and button != Mouse.Button.none) {
720 break :blk .drag;
721 }
722 if (motion and button == Mouse.Button.none) {
723 break :blk .motion;
724 }
725 if (xterm) {
726 if (button == Mouse.Button.none) {
727 break :blk .release;
728 }
729 break :blk .press;
730 }
731 if (input[input.len - 1] == 'm') break :blk .release;
732 break :blk .press;
733 },
734 };
735 return .{ .event = .{ .mouse = mouse }, .n = if (xterm) 6 else input.len };
736}
737
738test "parse: single xterm keypress" {
739 const alloc = testing.allocator_instance.allocator();
740 const input = "a";
741 var parser: Parser = .{};
742 const result = try parser.parse(input, alloc);
743 const expected_key: Key = .{
744 .codepoint = 'a',
745 .text = "a",
746 };
747 const expected_event: Event = .{ .key_press = expected_key };
748
749 try testing.expectEqual(1, result.n);
750 try testing.expectEqual(expected_event, result.event);
751}
752
753test "parse: single xterm keypress backspace" {
754 const alloc = testing.allocator_instance.allocator();
755 const input = "\x08";
756 var parser: Parser = .{};
757 const result = try parser.parse(input, alloc);
758 const expected_key: Key = .{
759 .codepoint = Key.backspace,
760 };
761 const expected_event: Event = .{ .key_press = expected_key };
762
763 try testing.expectEqual(1, result.n);
764 try testing.expectEqual(expected_event, result.event);
765}
766
767test "parse: single xterm keypress with more buffer" {
768 const alloc = testing.allocator_instance.allocator();
769 const input = "ab";
770 var parser: Parser = .{};
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 input = "\x1b";
786 var parser: Parser = .{};
787 const result = try parser.parse(input, alloc);
788 const expected_key: Key = .{ .codepoint = Key.escape };
789 const expected_event: Event = .{ .key_press = expected_key };
790
791 try testing.expectEqual(1, result.n);
792 try testing.expectEqual(expected_event, result.event);
793}
794
795test "parse: xterm ctrl+a" {
796 const alloc = testing.allocator_instance.allocator();
797 const input = "\x01";
798 var parser: Parser = .{};
799 const result = try parser.parse(input, alloc);
800 const expected_key: Key = .{ .codepoint = 'a', .mods = .{ .ctrl = true } };
801 const expected_event: Event = .{ .key_press = expected_key };
802
803 try testing.expectEqual(1, result.n);
804 try testing.expectEqual(expected_event, result.event);
805}
806
807test "parse: xterm alt+a" {
808 const alloc = testing.allocator_instance.allocator();
809 const input = "\x1ba";
810 var parser: Parser = .{};
811 const result = try parser.parse(input, alloc);
812 const expected_key: Key = .{ .codepoint = 'a', .mods = .{ .alt = true } };
813 const expected_event: Event = .{ .key_press = expected_key };
814
815 try testing.expectEqual(2, result.n);
816 try testing.expectEqual(expected_event, result.event);
817}
818
819test "parse: xterm key up" {
820 const alloc = testing.allocator_instance.allocator();
821 {
822 // normal version
823 const input = "\x1b[A";
824 var parser: Parser = .{};
825 const result = try parser.parse(input, alloc);
826 const expected_key: Key = .{ .codepoint = Key.up };
827 const expected_event: Event = .{ .key_press = expected_key };
828
829 try testing.expectEqual(3, result.n);
830 try testing.expectEqual(expected_event, result.event);
831 }
832
833 {
834 // application keys version
835 const input = "\x1bOA";
836 var parser: Parser = .{};
837 const result = try parser.parse(input, alloc);
838 const expected_key: Key = .{ .codepoint = Key.up };
839 const expected_event: Event = .{ .key_press = expected_key };
840
841 try testing.expectEqual(3, result.n);
842 try testing.expectEqual(expected_event, result.event);
843 }
844}
845
846test "parse: xterm shift+up" {
847 const alloc = testing.allocator_instance.allocator();
848 const input = "\x1b[1;2A";
849 var parser: Parser = .{};
850 const result = try parser.parse(input, alloc);
851 const expected_key: Key = .{ .codepoint = Key.up, .mods = .{ .shift = true } };
852 const expected_event: Event = .{ .key_press = expected_key };
853
854 try testing.expectEqual(6, result.n);
855 try testing.expectEqual(expected_event, result.event);
856}
857
858test "parse: xterm insert" {
859 const alloc = testing.allocator_instance.allocator();
860 const input = "\x1b[2~";
861 var parser: Parser = .{};
862 const result = try parser.parse(input, alloc);
863 const expected_key: Key = .{ .codepoint = Key.insert, .mods = .{} };
864 const expected_event: Event = .{ .key_press = expected_key };
865
866 try testing.expectEqual(input.len, result.n);
867 try testing.expectEqual(expected_event, result.event);
868}
869
870test "parse: paste_start" {
871 const alloc = testing.allocator_instance.allocator();
872 const input = "\x1b[200~";
873 var parser: Parser = .{};
874 const result = try parser.parse(input, alloc);
875 const expected_event: Event = .paste_start;
876
877 try testing.expectEqual(6, result.n);
878 try testing.expectEqual(expected_event, result.event);
879}
880
881test "parse: paste_end" {
882 const alloc = testing.allocator_instance.allocator();
883 const input = "\x1b[201~";
884 var parser: Parser = .{};
885 const result = try parser.parse(input, alloc);
886 const expected_event: Event = .paste_end;
887
888 try testing.expectEqual(6, result.n);
889 try testing.expectEqual(expected_event, result.event);
890}
891
892test "parse: osc52 paste" {
893 const alloc = testing.allocator_instance.allocator();
894 const input = "\x1b]52;c;b3NjNTIgcGFzdGU=\x1b\\";
895 const expected_text = "osc52 paste";
896 var parser: Parser = .{};
897 const result = try parser.parse(input, alloc);
898
899 try testing.expectEqual(25, result.n);
900 switch (result.event.?) {
901 .paste => |text| {
902 defer alloc.free(text);
903 try testing.expectEqualStrings(expected_text, text);
904 },
905 else => try testing.expect(false),
906 }
907}
908
909test "parse: focus_in" {
910 const alloc = testing.allocator_instance.allocator();
911 const input = "\x1b[I";
912 var parser: Parser = .{};
913 const result = try parser.parse(input, alloc);
914 const expected_event: Event = .focus_in;
915
916 try testing.expectEqual(3, result.n);
917 try testing.expectEqual(expected_event, result.event);
918}
919
920test "parse: focus_out" {
921 const alloc = testing.allocator_instance.allocator();
922 const input = "\x1b[O";
923 var parser: Parser = .{};
924 const result = try parser.parse(input, alloc);
925 const expected_event: Event = .focus_out;
926
927 try testing.expectEqual(3, result.n);
928 try testing.expectEqual(expected_event, result.event);
929}
930
931test "parse: kitty: shift+a without text reporting" {
932 const alloc = testing.allocator_instance.allocator();
933 const input = "\x1b[97:65;2u";
934 var parser: Parser = .{};
935 const result = try parser.parse(input, alloc);
936 const expected_key: Key = .{
937 .codepoint = 'a',
938 .shifted_codepoint = 'A',
939 .mods = .{ .shift = true },
940 .text = "A",
941 };
942 const expected_event: Event = .{ .key_press = expected_key };
943
944 try testing.expectEqual(10, result.n);
945 try testing.expectEqualDeep(expected_event, result.event);
946}
947
948test "parse: kitty: alt+shift+a without text reporting" {
949 const alloc = testing.allocator_instance.allocator();
950 const input = "\x1b[97:65;4u";
951 var parser: Parser = .{};
952 const result = try parser.parse(input, alloc);
953 const expected_key: Key = .{
954 .codepoint = 'a',
955 .shifted_codepoint = 'A',
956 .mods = .{ .shift = true, .alt = true },
957 };
958 const expected_event: Event = .{ .key_press = expected_key };
959
960 try testing.expectEqual(10, result.n);
961 try testing.expectEqual(expected_event, result.event);
962}
963
964test "parse: kitty: a without text reporting" {
965 const alloc = testing.allocator_instance.allocator();
966 const input = "\x1b[97u";
967 var parser: Parser = .{};
968 const result = try parser.parse(input, alloc);
969 const expected_key: Key = .{
970 .codepoint = 'a',
971 };
972 const expected_event: Event = .{ .key_press = expected_key };
973
974 try testing.expectEqual(5, result.n);
975 try testing.expectEqual(expected_event, result.event);
976}
977
978test "parse: kitty: release event" {
979 const alloc = testing.allocator_instance.allocator();
980 const input = "\x1b[97;1:3u";
981 var parser: Parser = .{};
982 const result = try parser.parse(input, alloc);
983 const expected_key: Key = .{
984 .codepoint = 'a',
985 };
986 const expected_event: Event = .{ .key_release = expected_key };
987
988 try testing.expectEqual(9, result.n);
989 try testing.expectEqual(expected_event, result.event);
990}
991
992test "parse: single codepoint" {
993 const alloc = testing.allocator_instance.allocator();
994 const input = "🙂";
995 var parser: Parser = .{};
996 const result = try parser.parse(input, alloc);
997 const expected_key: Key = .{
998 .codepoint = 0x1F642,
999 .text = input,
1000 };
1001 const expected_event: Event = .{ .key_press = expected_key };
1002
1003 try testing.expectEqual(4, result.n);
1004 try testing.expectEqual(expected_event, result.event);
1005}
1006
1007test "parse: single codepoint with more in buffer" {
1008 const alloc = testing.allocator_instance.allocator();
1009 const input = "🙂a";
1010 var parser: Parser = .{};
1011 const result = try parser.parse(input, alloc);
1012 const expected_key: Key = .{
1013 .codepoint = 0x1F642,
1014 .text = "🙂",
1015 };
1016 const expected_event: Event = .{ .key_press = expected_key };
1017
1018 try testing.expectEqual(4, result.n);
1019 try testing.expectEqualDeep(expected_event, result.event);
1020}
1021
1022test "parse: multiple codepoint grapheme" {
1023 const alloc = testing.allocator_instance.allocator();
1024 const input = "👩🚀";
1025 var parser: Parser = .{};
1026 const result = try parser.parse(input, alloc);
1027 const expected_key: Key = .{
1028 .codepoint = Key.multicodepoint,
1029 .text = input,
1030 };
1031 const expected_event: Event = .{ .key_press = expected_key };
1032
1033 try testing.expectEqual(input.len, result.n);
1034 try testing.expectEqual(expected_event, result.event);
1035}
1036
1037test "parse: multiple codepoint grapheme with more after" {
1038 const alloc = testing.allocator_instance.allocator();
1039 const input = "👩🚀abc";
1040 var parser: Parser = .{};
1041 const result = try parser.parse(input, alloc);
1042 const expected_key: Key = .{
1043 .codepoint = Key.multicodepoint,
1044 .text = "👩🚀",
1045 };
1046
1047 try testing.expectEqual(expected_key.text.?.len, result.n);
1048 const actual = result.event.?.key_press;
1049 try testing.expectEqualStrings(expected_key.text.?, actual.text.?);
1050 try testing.expectEqual(expected_key.codepoint, actual.codepoint);
1051}
1052
1053test "parse: flag emoji" {
1054 const alloc = testing.allocator_instance.allocator();
1055 const input = "🇺🇸";
1056 var parser: Parser = .{};
1057 const result = try parser.parse(input, alloc);
1058 const expected_key: Key = .{
1059 .codepoint = Key.multicodepoint,
1060 .text = input,
1061 };
1062 const expected_event: Event = .{ .key_press = expected_key };
1063
1064 try testing.expectEqual(input.len, result.n);
1065 try testing.expectEqual(expected_event, result.event);
1066}
1067
1068test "parse: combining mark" {
1069 const alloc = testing.allocator_instance.allocator();
1070 // a with combining acute accent (NFD form)
1071 const input = "a\u{0301}";
1072 var parser: Parser = .{};
1073 const result = try parser.parse(input, alloc);
1074 const expected_key: Key = .{
1075 .codepoint = Key.multicodepoint,
1076 .text = input,
1077 };
1078 const expected_event: Event = .{ .key_press = expected_key };
1079
1080 try testing.expectEqual(input.len, result.n);
1081 try testing.expectEqual(expected_event, result.event);
1082}
1083
1084test "parse: skin tone emoji" {
1085 const alloc = testing.allocator_instance.allocator();
1086 const input = "👋🏿";
1087 var parser: Parser = .{};
1088 const result = try parser.parse(input, alloc);
1089 const expected_key: Key = .{
1090 .codepoint = Key.multicodepoint,
1091 .text = input,
1092 };
1093 const expected_event: Event = .{ .key_press = expected_key };
1094
1095 try testing.expectEqual(input.len, result.n);
1096 try testing.expectEqual(expected_event, result.event);
1097}
1098
1099test "parse: text variation selector" {
1100 const alloc = testing.allocator_instance.allocator();
1101 // Heavy black heart with text variation selector
1102 const input = "❤︎";
1103 var parser: Parser = .{};
1104 const result = try parser.parse(input, alloc);
1105 const expected_key: Key = .{
1106 .codepoint = Key.multicodepoint,
1107 .text = input,
1108 };
1109 const expected_event: Event = .{ .key_press = expected_key };
1110
1111 try testing.expectEqual(input.len, result.n);
1112 try testing.expectEqual(expected_event, result.event);
1113}
1114
1115test "parse: keycap sequence" {
1116 const alloc = testing.allocator_instance.allocator();
1117 const input = "1️⃣";
1118 var parser: Parser = .{};
1119 const result = try parser.parse(input, alloc);
1120 const expected_key: Key = .{
1121 .codepoint = Key.multicodepoint,
1122 .text = input,
1123 };
1124 const expected_event: Event = .{ .key_press = expected_key };
1125
1126 try testing.expectEqual(input.len, result.n);
1127 try testing.expectEqual(expected_event, result.event);
1128}
1129
1130test "parse(csi): kitty multi cursor" {
1131 var buf: [1]u8 = undefined;
1132 {
1133 const input = "\x1b[>1;2;3;29;30;40;100;101 q";
1134 const result = parseCsi(input, &buf);
1135 const expected: Result = .{
1136 .event = .cap_multi_cursor,
1137 .n = input.len,
1138 };
1139
1140 try testing.expectEqual(expected.n, result.n);
1141 try testing.expectEqual(expected.event, result.event);
1142 }
1143 {
1144 const input = "\x1b[> q";
1145 const result = parseCsi(input, &buf);
1146 const expected: Result = .{
1147 .event = null,
1148 .n = input.len,
1149 };
1150
1151 try testing.expectEqual(expected.n, result.n);
1152 try testing.expectEqual(expected.event, result.event);
1153 }
1154}
1155
1156test "parse(csi): decrpm" {
1157 var buf: [1]u8 = undefined;
1158 {
1159 const input = "\x1b[?1016;1$y";
1160 const result = parseCsi(input, &buf);
1161 const expected: Result = .{
1162 .event = .cap_sgr_pixels,
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[?1016;0$y";
1171 const result = parseCsi(input, &buf);
1172 const expected: Result = .{
1173 .event = null,
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
1182test "parse(csi): primary da" {
1183 var buf: [1]u8 = undefined;
1184 const input = "\x1b[?c";
1185 const result = parseCsi(input, &buf);
1186 const expected: Result = .{
1187 .event = .cap_da1,
1188 .n = input.len,
1189 };
1190
1191 try testing.expectEqual(expected.n, result.n);
1192 try testing.expectEqual(expected.event, result.event);
1193}
1194
1195test "parse(csi): dsr" {
1196 var buf: [1]u8 = undefined;
1197 {
1198 const input = "\x1b[?997;1n";
1199 const result = parseCsi(input, &buf);
1200 const expected: Result = .{
1201 .event = .{ .color_scheme = .dark },
1202 .n = input.len,
1203 };
1204
1205 try testing.expectEqual(expected.n, result.n);
1206 try testing.expectEqual(expected.event, result.event);
1207 }
1208 {
1209 const input = "\x1b[?997;2n";
1210 const result = parseCsi(input, &buf);
1211 const expected: Result = .{
1212 .event = .{ .color_scheme = .light },
1213 .n = input.len,
1214 };
1215
1216 try testing.expectEqual(expected.n, result.n);
1217 try testing.expectEqual(expected.event, result.event);
1218 }
1219 {
1220 const input = "\x1b[0n";
1221 const result = parseCsi(input, &buf);
1222 const expected: Result = .{
1223 .event = null,
1224 .n = input.len,
1225 };
1226
1227 try testing.expectEqual(expected.n, result.n);
1228 try testing.expectEqual(expected.event, result.event);
1229 }
1230}
1231
1232test "parse(csi): mouse" {
1233 var buf: [1]u8 = undefined;
1234 const input = "\x1b[<35;1;1m";
1235 const result = parseCsi(input, &buf);
1236 const expected: Result = .{
1237 .event = .{ .mouse = .{
1238 .col = 0,
1239 .row = 0,
1240 .button = .none,
1241 .type = .motion,
1242 .mods = .{},
1243 } },
1244 .n = input.len,
1245 };
1246
1247 try testing.expectEqual(expected.n, result.n);
1248 try testing.expectEqual(expected.event, result.event);
1249}
1250
1251test "parse(csi): mouse (negative)" {
1252 var buf: [1]u8 = undefined;
1253 const input = "\x1b[<35;-50;-100m";
1254 const result = parseCsi(input, &buf);
1255 const expected: Result = .{
1256 .event = .{ .mouse = .{
1257 .col = -51,
1258 .row = -101,
1259 .button = .none,
1260 .type = .motion,
1261 .mods = .{},
1262 } },
1263 .n = input.len,
1264 };
1265
1266 try testing.expectEqual(expected.n, result.n);
1267 try testing.expectEqual(expected.event, result.event);
1268}
1269
1270test "parse(csi): xterm mouse" {
1271 var buf: [1]u8 = undefined;
1272 const input = "\x1b[M\x20\x21\x21";
1273 const result = parseCsi(input, &buf);
1274 const expected: Result = .{
1275 .event = .{ .mouse = .{
1276 .col = 0,
1277 .row = 0,
1278 .button = .left,
1279 .type = .press,
1280 .mods = .{},
1281 } },
1282 .n = input.len,
1283 };
1284
1285 try testing.expectEqual(expected.n, result.n);
1286 try testing.expectEqual(expected.event, result.event);
1287}
1288
1289test "parse: disambiguate shift + space" {
1290 const alloc = testing.allocator_instance.allocator();
1291 const input = "\x1b[32;2u";
1292 var parser: Parser = .{};
1293 const result = try parser.parse(input, alloc);
1294 const expected_key: Key = .{
1295 .codepoint = ' ',
1296 .shifted_codepoint = ' ',
1297 .mods = .{ .shift = true },
1298 .text = " ",
1299 };
1300 const expected_event: Event = .{ .key_press = expected_key };
1301
1302 try testing.expectEqual(7, result.n);
1303 try testing.expectEqualDeep(expected_event, result.event);
1304}