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