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 code_point = @import("code_point");
8const grapheme = @import("grapheme");
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};
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
48grapheme_data: *const grapheme.GraphemeData,
49
50/// Parse the first event from the input buffer. If a completion event is not
51/// present, Result.event will be null and Result.n will be 0
52///
53/// If an unknown event is found, Result.event will be null and Result.n will be
54/// greater than 0
55pub fn parse(self: *Parser, input: []const u8, paste_allocator: ?std.mem.Allocator) !Result {
56 std.debug.assert(input.len > 0);
57
58 // We gate this for len > 1 so we can detect singular escape key presses
59 if (input[0] == 0x1b and input.len > 1) {
60 switch (input[1]) {
61 0x4F => return parseSs3(input),
62 0x50 => return skipUntilST(input), // DCS
63 0x58 => return skipUntilST(input), // SOS
64 0x5B => return parseCsi(input, &self.buf), // CSI
65 0x5D => return parseOsc(input, paste_allocator),
66 0x5E => return skipUntilST(input), // PM
67 0x5F => return parseApc(input),
68 else => {
69 // Anything else is an "alt + <char>" keypress
70 const key: Key = .{
71 .codepoint = input[1],
72 .mods = .{ .alt = true },
73 };
74 return .{
75 .event = .{ .key_press = key },
76 .n = 2,
77 };
78 },
79 }
80 } else return parseGround(input, self.grapheme_data);
81}
82
83/// Parse ground state
84inline fn parseGround(input: []const u8, data: *const grapheme.GraphemeData) !Result {
85 std.debug.assert(input.len > 0);
86
87 const b = input[0];
88 var n: usize = 1;
89 // ground state generates keypresses when parsing input. We
90 // generally get ascii characters, but anything less than
91 // 0x20 is a Ctrl+<c> keypress. We map these to lowercase
92 // ascii characters when we can
93 const key: Key = switch (b) {
94 0x00 => .{ .codepoint = '@', .mods = .{ .ctrl = true } },
95 0x08 => .{ .codepoint = Key.backspace },
96 0x09 => .{ .codepoint = Key.tab },
97 0x0A,
98 0x0D,
99 => .{ .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: grapheme.State = .{};
121 var prev_cp = code;
122 while (iter.next()) |next_cp| {
123 if (grapheme.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),
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(usize, height_char, 10) catch return null_event,
526 .cols = std.fmt.parseUnsigned(usize, width_char, 10) catch return null_event,
527 .x_pixel = std.fmt.parseUnsigned(usize, width_pix, 10) catch return null_event,
528 .y_pixel = std.fmt.parseUnsigned(usize, 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 const event: Event = if (is_release)
597 .{ .key_release = key }
598 else
599 .{ .key_press = key };
600
601 return .{ .event = event, .n = sequence.len };
602 },
603 'y' => {
604 // DECRPM (CSI ? Ps ; Pm $ y)
605 const delim_idx = std.mem.indexOfScalarPos(u8, input, 3, ';') orelse return null_event;
606 const ps = std.fmt.parseUnsigned(u16, input[3..delim_idx], 10) catch return null_event;
607 const pm = std.fmt.parseUnsigned(u8, input[delim_idx + 1 .. sequence.len - 2], 10) catch return null_event;
608 switch (ps) {
609 // Mouse Pixel reporting
610 1016 => switch (pm) {
611 0, 4 => return null_event,
612 else => return .{ .event = .cap_sgr_pixels, .n = sequence.len },
613 },
614 // Unicode Core, see https://github.com/contour-terminal/terminal-unicode-core
615 2027 => switch (pm) {
616 0, 4 => return null_event,
617 else => return .{ .event = .cap_unicode, .n = sequence.len },
618 },
619 // Color scheme reportnig, see https://github.com/contour-terminal/contour/blob/master/docs/vt-extensions/color-palette-update-notifications.md
620 2031 => switch (pm) {
621 0, 4 => return null_event,
622 else => return .{ .event = .cap_color_scheme_updates, .n = sequence.len },
623 },
624 else => return null_event,
625 }
626 },
627 else => return null_event,
628 }
629}
630
631/// Parse a param buffer, returning a default value if the param was empty
632inline fn parseParam(comptime T: type, buf: []const u8, default: ?T) ?T {
633 if (buf.len == 0) return default;
634 return std.fmt.parseUnsigned(T, buf, 10) catch return null;
635}
636
637/// Parse a mouse event
638inline fn parseMouse(input: []const u8) Result {
639 std.debug.assert(input.len >= 4); // ESC [ < [Mm]
640 const null_event: Result = .{ .event = null, .n = input.len };
641
642 if (input[2] != '<') return null_event;
643
644 const delim1 = std.mem.indexOfScalarPos(u8, input, 3, ';') orelse return null_event;
645 const button_mask = parseParam(u16, input[3..delim1], null) orelse return null_event;
646 const delim2 = std.mem.indexOfScalarPos(u8, input, delim1 + 1, ';') orelse return null_event;
647 const px = parseParam(u16, input[delim1 + 1 .. delim2], 1) orelse return null_event;
648 const py = parseParam(u16, input[delim2 + 1 .. input.len - 1], 1) orelse return null_event;
649
650 const button: Mouse.Button = @enumFromInt(button_mask & mouse_bits.buttons);
651 const motion = button_mask & mouse_bits.motion > 0;
652 const shift = button_mask & mouse_bits.shift > 0;
653 const alt = button_mask & mouse_bits.alt > 0;
654 const ctrl = button_mask & mouse_bits.ctrl > 0;
655
656 const mouse = Mouse{
657 .button = button,
658 .mods = .{
659 .shift = shift,
660 .alt = alt,
661 .ctrl = ctrl,
662 },
663 .col = px -| 1,
664 .row = py -| 1,
665 .type = blk: {
666 if (motion and button != Mouse.Button.none) {
667 break :blk .drag;
668 }
669 if (motion and button == Mouse.Button.none) {
670 break :blk .motion;
671 }
672 if (input[input.len - 1] == 'm') break :blk .release;
673 break :blk .press;
674 },
675 };
676 return .{ .event = .{ .mouse = mouse }, .n = input.len };
677}
678
679test "parse: single xterm keypress" {
680 const alloc = testing.allocator_instance.allocator();
681 const grapheme_data = try grapheme.GraphemeData.init(alloc);
682 defer grapheme_data.deinit();
683 const input = "a";
684 var parser: Parser = .{ .grapheme_data = &grapheme_data };
685 const result = try parser.parse(input, alloc);
686 const expected_key: Key = .{
687 .codepoint = 'a',
688 .text = "a",
689 };
690 const expected_event: Event = .{ .key_press = expected_key };
691
692 try testing.expectEqual(1, result.n);
693 try testing.expectEqual(expected_event, result.event);
694}
695
696test "parse: single xterm keypress backspace" {
697 const alloc = testing.allocator_instance.allocator();
698 const grapheme_data = try grapheme.GraphemeData.init(alloc);
699 defer grapheme_data.deinit();
700 const input = "\x08";
701 var parser: Parser = .{ .grapheme_data = &grapheme_data };
702 const result = try parser.parse(input, alloc);
703 const expected_key: Key = .{
704 .codepoint = Key.backspace,
705 };
706 const expected_event: Event = .{ .key_press = expected_key };
707
708 try testing.expectEqual(1, result.n);
709 try testing.expectEqual(expected_event, result.event);
710}
711
712test "parse: single xterm keypress with more buffer" {
713 const alloc = testing.allocator_instance.allocator();
714 const grapheme_data = try grapheme.GraphemeData.init(alloc);
715 defer grapheme_data.deinit();
716 const input = "ab";
717 var parser: Parser = .{ .grapheme_data = &grapheme_data };
718 const result = try parser.parse(input, alloc);
719 const expected_key: Key = .{
720 .codepoint = 'a',
721 .text = "a",
722 };
723 const expected_event: Event = .{ .key_press = expected_key };
724
725 try testing.expectEqual(1, result.n);
726 try testing.expectEqualStrings(expected_key.text.?, result.event.?.key_press.text.?);
727 try testing.expectEqualDeep(expected_event, result.event);
728}
729
730test "parse: xterm escape keypress" {
731 const alloc = testing.allocator_instance.allocator();
732 const grapheme_data = try grapheme.GraphemeData.init(alloc);
733 defer grapheme_data.deinit();
734 const input = "\x1b";
735 var parser: Parser = .{ .grapheme_data = &grapheme_data };
736 const result = try parser.parse(input, alloc);
737 const expected_key: Key = .{ .codepoint = Key.escape };
738 const expected_event: Event = .{ .key_press = expected_key };
739
740 try testing.expectEqual(1, result.n);
741 try testing.expectEqual(expected_event, result.event);
742}
743
744test "parse: xterm ctrl+a" {
745 const alloc = testing.allocator_instance.allocator();
746 const grapheme_data = try grapheme.GraphemeData.init(alloc);
747 defer grapheme_data.deinit();
748 const input = "\x01";
749 var parser: Parser = .{ .grapheme_data = &grapheme_data };
750 const result = try parser.parse(input, alloc);
751 const expected_key: Key = .{ .codepoint = 'a', .mods = .{ .ctrl = true } };
752 const expected_event: Event = .{ .key_press = expected_key };
753
754 try testing.expectEqual(1, result.n);
755 try testing.expectEqual(expected_event, result.event);
756}
757
758test "parse: xterm alt+a" {
759 const alloc = testing.allocator_instance.allocator();
760 const grapheme_data = try grapheme.GraphemeData.init(alloc);
761 defer grapheme_data.deinit();
762 const input = "\x1ba";
763 var parser: Parser = .{ .grapheme_data = &grapheme_data };
764 const result = try parser.parse(input, alloc);
765 const expected_key: Key = .{ .codepoint = 'a', .mods = .{ .alt = true } };
766 const expected_event: Event = .{ .key_press = expected_key };
767
768 try testing.expectEqual(2, result.n);
769 try testing.expectEqual(expected_event, result.event);
770}
771
772test "parse: xterm key up" {
773 const alloc = testing.allocator_instance.allocator();
774 const grapheme_data = try grapheme.GraphemeData.init(alloc);
775 defer grapheme_data.deinit();
776 {
777 // normal version
778 const input = "\x1b[A";
779 var parser: Parser = .{ .grapheme_data = &grapheme_data };
780 const result = try parser.parse(input, alloc);
781 const expected_key: Key = .{ .codepoint = Key.up };
782 const expected_event: Event = .{ .key_press = expected_key };
783
784 try testing.expectEqual(3, result.n);
785 try testing.expectEqual(expected_event, result.event);
786 }
787
788 {
789 // application keys version
790 const input = "\x1bOA";
791 var parser: Parser = .{ .grapheme_data = &grapheme_data };
792 const result = try parser.parse(input, alloc);
793 const expected_key: Key = .{ .codepoint = Key.up };
794 const expected_event: Event = .{ .key_press = expected_key };
795
796 try testing.expectEqual(3, result.n);
797 try testing.expectEqual(expected_event, result.event);
798 }
799}
800
801test "parse: xterm shift+up" {
802 const alloc = testing.allocator_instance.allocator();
803 const grapheme_data = try grapheme.GraphemeData.init(alloc);
804 defer grapheme_data.deinit();
805 const input = "\x1b[1;2A";
806 var parser: Parser = .{ .grapheme_data = &grapheme_data };
807 const result = try parser.parse(input, alloc);
808 const expected_key: Key = .{ .codepoint = Key.up, .mods = .{ .shift = true } };
809 const expected_event: Event = .{ .key_press = expected_key };
810
811 try testing.expectEqual(6, result.n);
812 try testing.expectEqual(expected_event, result.event);
813}
814
815test "parse: xterm insert" {
816 const alloc = testing.allocator_instance.allocator();
817 const grapheme_data = try grapheme.GraphemeData.init(alloc);
818 defer grapheme_data.deinit();
819 const input = "\x1b[2~";
820 var parser: Parser = .{ .grapheme_data = &grapheme_data };
821 const result = try parser.parse(input, alloc);
822 const expected_key: Key = .{ .codepoint = Key.insert, .mods = .{} };
823 const expected_event: Event = .{ .key_press = expected_key };
824
825 try testing.expectEqual(input.len, result.n);
826 try testing.expectEqual(expected_event, result.event);
827}
828
829test "parse: paste_start" {
830 const alloc = testing.allocator_instance.allocator();
831 const grapheme_data = try grapheme.GraphemeData.init(alloc);
832 defer grapheme_data.deinit();
833 const input = "\x1b[200~";
834 var parser: Parser = .{ .grapheme_data = &grapheme_data };
835 const result = try parser.parse(input, alloc);
836 const expected_event: Event = .paste_start;
837
838 try testing.expectEqual(6, result.n);
839 try testing.expectEqual(expected_event, result.event);
840}
841
842test "parse: paste_end" {
843 const alloc = testing.allocator_instance.allocator();
844 const grapheme_data = try grapheme.GraphemeData.init(alloc);
845 defer grapheme_data.deinit();
846 const input = "\x1b[201~";
847 var parser: Parser = .{ .grapheme_data = &grapheme_data };
848 const result = try parser.parse(input, alloc);
849 const expected_event: Event = .paste_end;
850
851 try testing.expectEqual(6, result.n);
852 try testing.expectEqual(expected_event, result.event);
853}
854
855test "parse: osc52 paste" {
856 const alloc = testing.allocator_instance.allocator();
857 const grapheme_data = try grapheme.GraphemeData.init(alloc);
858 defer grapheme_data.deinit();
859 const input = "\x1b]52;c;b3NjNTIgcGFzdGU=\x1b\\";
860 const expected_text = "osc52 paste";
861 var parser: Parser = .{ .grapheme_data = &grapheme_data };
862 const result = try parser.parse(input, alloc);
863
864 try testing.expectEqual(25, result.n);
865 switch (result.event.?) {
866 .paste => |text| {
867 defer alloc.free(text);
868 try testing.expectEqualStrings(expected_text, text);
869 },
870 else => try testing.expect(false),
871 }
872}
873
874test "parse: focus_in" {
875 const alloc = testing.allocator_instance.allocator();
876 const grapheme_data = try grapheme.GraphemeData.init(alloc);
877 defer grapheme_data.deinit();
878 const input = "\x1b[I";
879 var parser: Parser = .{ .grapheme_data = &grapheme_data };
880 const result = try parser.parse(input, alloc);
881 const expected_event: Event = .focus_in;
882
883 try testing.expectEqual(3, result.n);
884 try testing.expectEqual(expected_event, result.event);
885}
886
887test "parse: focus_out" {
888 const alloc = testing.allocator_instance.allocator();
889 const grapheme_data = try grapheme.GraphemeData.init(alloc);
890 defer grapheme_data.deinit();
891 const input = "\x1b[O";
892 var parser: Parser = .{ .grapheme_data = &grapheme_data };
893 const result = try parser.parse(input, alloc);
894 const expected_event: Event = .focus_out;
895
896 try testing.expectEqual(3, result.n);
897 try testing.expectEqual(expected_event, result.event);
898}
899
900test "parse: kitty: shift+a without text reporting" {
901 const alloc = testing.allocator_instance.allocator();
902 const grapheme_data = try grapheme.GraphemeData.init(alloc);
903 defer grapheme_data.deinit();
904 const input = "\x1b[97:65;2u";
905 var parser: Parser = .{ .grapheme_data = &grapheme_data };
906 const result = try parser.parse(input, alloc);
907 const expected_key: Key = .{
908 .codepoint = 'a',
909 .shifted_codepoint = 'A',
910 .mods = .{ .shift = true },
911 };
912 const expected_event: Event = .{ .key_press = expected_key };
913
914 try testing.expectEqual(10, result.n);
915 try testing.expectEqual(expected_event, result.event);
916}
917
918test "parse: kitty: alt+shift+a without text reporting" {
919 const alloc = testing.allocator_instance.allocator();
920 const grapheme_data = try grapheme.GraphemeData.init(alloc);
921 defer grapheme_data.deinit();
922 const input = "\x1b[97:65;4u";
923 var parser: Parser = .{ .grapheme_data = &grapheme_data };
924 const result = try parser.parse(input, alloc);
925 const expected_key: Key = .{
926 .codepoint = 'a',
927 .shifted_codepoint = 'A',
928 .mods = .{ .shift = true, .alt = true },
929 };
930 const expected_event: Event = .{ .key_press = expected_key };
931
932 try testing.expectEqual(10, result.n);
933 try testing.expectEqual(expected_event, result.event);
934}
935
936test "parse: kitty: a without text reporting" {
937 const alloc = testing.allocator_instance.allocator();
938 const grapheme_data = try grapheme.GraphemeData.init(alloc);
939 defer grapheme_data.deinit();
940 const input = "\x1b[97u";
941 var parser: Parser = .{ .grapheme_data = &grapheme_data };
942 const result = try parser.parse(input, alloc);
943 const expected_key: Key = .{
944 .codepoint = 'a',
945 };
946 const expected_event: Event = .{ .key_press = expected_key };
947
948 try testing.expectEqual(5, result.n);
949 try testing.expectEqual(expected_event, result.event);
950}
951
952test "parse: kitty: release event" {
953 const alloc = testing.allocator_instance.allocator();
954 const grapheme_data = try grapheme.GraphemeData.init(alloc);
955 defer grapheme_data.deinit();
956 const input = "\x1b[97;1:3u";
957 var parser: Parser = .{ .grapheme_data = &grapheme_data };
958 const result = try parser.parse(input, alloc);
959 const expected_key: Key = .{
960 .codepoint = 'a',
961 };
962 const expected_event: Event = .{ .key_release = expected_key };
963
964 try testing.expectEqual(9, result.n);
965 try testing.expectEqual(expected_event, result.event);
966}
967
968test "parse: single codepoint" {
969 const alloc = testing.allocator_instance.allocator();
970 const grapheme_data = try grapheme.GraphemeData.init(alloc);
971 defer grapheme_data.deinit();
972 const input = "🙂";
973 var parser: Parser = .{ .grapheme_data = &grapheme_data };
974 const result = try parser.parse(input, alloc);
975 const expected_key: Key = .{
976 .codepoint = 0x1F642,
977 .text = input,
978 };
979 const expected_event: Event = .{ .key_press = expected_key };
980
981 try testing.expectEqual(4, result.n);
982 try testing.expectEqual(expected_event, result.event);
983}
984
985test "parse: single codepoint with more in buffer" {
986 const alloc = testing.allocator_instance.allocator();
987 const grapheme_data = try grapheme.GraphemeData.init(alloc);
988 defer grapheme_data.deinit();
989 const input = "🙂a";
990 var parser: Parser = .{ .grapheme_data = &grapheme_data };
991 const result = try parser.parse(input, alloc);
992 const expected_key: Key = .{
993 .codepoint = 0x1F642,
994 .text = "🙂",
995 };
996 const expected_event: Event = .{ .key_press = expected_key };
997
998 try testing.expectEqual(4, result.n);
999 try testing.expectEqualDeep(expected_event, result.event);
1000}
1001
1002test "parse: multiple codepoint grapheme" {
1003 const alloc = testing.allocator_instance.allocator();
1004 const grapheme_data = try grapheme.GraphemeData.init(alloc);
1005 defer grapheme_data.deinit();
1006 const input = "👩🚀";
1007 var parser: Parser = .{ .grapheme_data = &grapheme_data };
1008 const result = try parser.parse(input, alloc);
1009 const expected_key: Key = .{
1010 .codepoint = Key.multicodepoint,
1011 .text = input,
1012 };
1013 const expected_event: Event = .{ .key_press = expected_key };
1014
1015 try testing.expectEqual(input.len, result.n);
1016 try testing.expectEqual(expected_event, result.event);
1017}
1018
1019test "parse: multiple codepoint grapheme with more after" {
1020 const alloc = testing.allocator_instance.allocator();
1021 const grapheme_data = try grapheme.GraphemeData.init(alloc);
1022 defer grapheme_data.deinit();
1023 const input = "👩🚀abc";
1024 var parser: Parser = .{ .grapheme_data = &grapheme_data };
1025 const result = try parser.parse(input, alloc);
1026 const expected_key: Key = .{
1027 .codepoint = Key.multicodepoint,
1028 .text = "👩🚀",
1029 };
1030
1031 try testing.expectEqual(expected_key.text.?.len, result.n);
1032 const actual = result.event.?.key_press;
1033 try testing.expectEqualStrings(expected_key.text.?, actual.text.?);
1034 try testing.expectEqual(expected_key.codepoint, actual.codepoint);
1035}
1036
1037test "parse(csi): decrpm" {
1038 var buf: [1]u8 = undefined;
1039 {
1040 const input = "\x1b[?1016;1$y";
1041 const result = parseCsi(input, &buf);
1042 const expected: Result = .{
1043 .event = .cap_sgr_pixels,
1044 .n = input.len,
1045 };
1046
1047 try testing.expectEqual(expected.n, result.n);
1048 try testing.expectEqual(expected.event, result.event);
1049 }
1050 {
1051 const input = "\x1b[?1016;0$y";
1052 const result = parseCsi(input, &buf);
1053 const expected: Result = .{
1054 .event = null,
1055 .n = input.len,
1056 };
1057
1058 try testing.expectEqual(expected.n, result.n);
1059 try testing.expectEqual(expected.event, result.event);
1060 }
1061}
1062
1063test "parse(csi): primary da" {
1064 var buf: [1]u8 = undefined;
1065 const input = "\x1b[?c";
1066 const result = parseCsi(input, &buf);
1067 const expected: Result = .{
1068 .event = .cap_da1,
1069 .n = input.len,
1070 };
1071
1072 try testing.expectEqual(expected.n, result.n);
1073 try testing.expectEqual(expected.event, result.event);
1074}
1075
1076test "parse(csi): dsr" {
1077 var buf: [1]u8 = undefined;
1078 {
1079 const input = "\x1b[?997;1n";
1080 const result = parseCsi(input, &buf);
1081 const expected: Result = .{
1082 .event = .{ .color_scheme = .dark },
1083 .n = input.len,
1084 };
1085
1086 try testing.expectEqual(expected.n, result.n);
1087 try testing.expectEqual(expected.event, result.event);
1088 }
1089 {
1090 const input = "\x1b[?997;2n";
1091 const result = parseCsi(input, &buf);
1092 const expected: Result = .{
1093 .event = .{ .color_scheme = .light },
1094 .n = input.len,
1095 };
1096
1097 try testing.expectEqual(expected.n, result.n);
1098 try testing.expectEqual(expected.event, result.event);
1099 }
1100 {
1101 const input = "\x1b[0n";
1102 const result = parseCsi(input, &buf);
1103 const expected: Result = .{
1104 .event = null,
1105 .n = input.len,
1106 };
1107
1108 try testing.expectEqual(expected.n, result.n);
1109 try testing.expectEqual(expected.event, result.event);
1110 }
1111}
1112
1113test "parse(csi): mouse" {
1114 var buf: [1]u8 = undefined;
1115 const input = "\x1b[<35;1;1m";
1116 const result = parseCsi(input, &buf);
1117 const expected: Result = .{
1118 .event = .{ .mouse = .{
1119 .col = 0,
1120 .row = 0,
1121 .button = .none,
1122 .type = .motion,
1123 .mods = .{},
1124 } },
1125 .n = input.len,
1126 };
1127
1128 try testing.expectEqual(expected.n, result.n);
1129 try testing.expectEqual(expected.event, result.event);
1130}