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 if (i == 2 and b == '?') continue;
294 switch (b) {
295 0x40...0xFF => break input[0 .. i + 1],
296 else => continue,
297 }
298 } else return .{ .event = null, .n = 0 };
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:event_type {ABCDEFHPQS}
307
308 // Split first into fields delimited by ';'
309 var field_iter = std.mem.splitScalar(u8, sequence[2 .. sequence.len - 1], ';');
310
311 // skip the first field
312 _ = field_iter.next(); //
313
314 var is_release: bool = false;
315 var key: Key = .{
316 .codepoint = switch (final) {
317 'A' => Key.up,
318 'B' => Key.down,
319 'C' => Key.right,
320 'D' => Key.left,
321 'E' => Key.kp_begin,
322 'F' => Key.end,
323 'H' => Key.home,
324 'P' => Key.f1,
325 'Q' => Key.f2,
326 'R' => Key.f3,
327 'S' => Key.f4,
328 else => return null_event,
329 },
330 };
331
332 field2: {
333 // modifier_mask:event_type
334 const field_buf = field_iter.next() orelse break :field2;
335 var param_iter = std.mem.splitScalar(u8, field_buf, ':');
336 const modifier_buf = param_iter.next() orelse unreachable;
337 const modifier_mask = parseParam(u8, modifier_buf, 1) orelse return null_event;
338 key.mods = @bitCast(modifier_mask -| 1);
339
340 if (param_iter.next()) |event_type_buf| {
341 is_release = std.mem.eql(u8, event_type_buf, "3");
342 }
343 }
344
345 field3: {
346 // text_as_codepoint[:text_as_codepoint]
347 const field_buf = field_iter.next() orelse break :field3;
348 var param_iter = std.mem.splitScalar(u8, field_buf, ':');
349 var total: usize = 0;
350 while (param_iter.next()) |cp_buf| {
351 const cp = parseParam(u21, cp_buf, null) orelse return null_event;
352 total += std.unicode.utf8Encode(cp, text_buf[total..]) catch return null_event;
353 }
354 key.text = text_buf[0..total];
355 }
356
357 const event: Event = if (is_release) .{ .key_release = key } else .{ .key_press = key };
358 return .{
359 .event = event,
360 .n = sequence.len,
361 };
362 },
363 '~' => {
364 // Legacy keys
365 // CSI number ~
366 // CSI number ; modifier ~
367 // CSI number ; modifier:event_type ; text_as_codepoint ~
368 var field_iter = std.mem.splitScalar(u8, sequence[2 .. sequence.len - 1], ';');
369 const number_buf = field_iter.next() orelse unreachable; // always will have one field
370 const number = parseParam(u16, number_buf, null) orelse return null_event;
371
372 var key: Key = .{
373 .codepoint = switch (number) {
374 2 => Key.insert,
375 3 => Key.delete,
376 5 => Key.page_up,
377 6 => Key.page_down,
378 7 => Key.home,
379 8 => Key.end,
380 11 => Key.f1,
381 12 => Key.f2,
382 13 => Key.f3,
383 14 => Key.f4,
384 15 => Key.f5,
385 17 => Key.f6,
386 18 => Key.f7,
387 19 => Key.f8,
388 20 => Key.f9,
389 21 => Key.f10,
390 23 => Key.f11,
391 24 => Key.f12,
392 200 => return .{ .event = .paste_start, .n = sequence.len },
393 201 => return .{ .event = .paste_end, .n = sequence.len },
394 57427 => Key.kp_begin,
395 else => return null_event,
396 },
397 };
398
399 var is_release: bool = false;
400 field2: {
401 // modifier_mask:event_type
402 const field_buf = field_iter.next() orelse break :field2;
403 var param_iter = std.mem.splitScalar(u8, field_buf, ':');
404 const modifier_buf = param_iter.next() orelse unreachable;
405 const modifier_mask = parseParam(u8, modifier_buf, 1) orelse return null_event;
406 key.mods = @bitCast(modifier_mask -| 1);
407
408 if (param_iter.next()) |event_type_buf| {
409 is_release = std.mem.eql(u8, event_type_buf, "3");
410 }
411 }
412
413 field3: {
414 // text_as_codepoint[:text_as_codepoint]
415 const field_buf = field_iter.next() orelse break :field3;
416 var param_iter = std.mem.splitScalar(u8, field_buf, ':');
417 var total: usize = 0;
418 while (param_iter.next()) |cp_buf| {
419 const cp = parseParam(u21, cp_buf, null) orelse return null_event;
420 total += std.unicode.utf8Encode(cp, text_buf[total..]) catch return null_event;
421 }
422 key.text = text_buf[0..total];
423 }
424
425 const event: Event = if (is_release) .{ .key_release = key } else .{ .key_press = key };
426 return .{
427 .event = event,
428 .n = sequence.len,
429 };
430 },
431
432 'I' => return .{ .event = .focus_in, .n = sequence.len },
433 'O' => return .{ .event = .focus_out, .n = sequence.len },
434 'M', 'm' => return parseMouse(sequence),
435 'c' => {
436 // Primary DA (CSI ? Pm c)
437 std.debug.assert(sequence.len >= 4); // ESC [ ? c == 4 bytes
438 switch (input[2]) {
439 '?' => return .{ .event = .cap_da1, .n = sequence.len },
440 else => return null_event,
441 }
442 },
443 'n' => {
444 // Device Status Report
445 // CSI Ps n
446 // CSI ? Ps n
447 std.debug.assert(sequence.len >= 3);
448 switch (sequence[2]) {
449 '?' => {
450 const delim_idx = std.mem.indexOfScalarPos(u8, input, 3, ';') orelse return null_event;
451 const ps = std.fmt.parseUnsigned(u16, input[3..delim_idx], 10) catch return null_event;
452 switch (ps) {
453 997 => {
454 // Color scheme update (CSI 997 ; Ps n)
455 // See https://github.com/contour-terminal/contour/blob/master/docs/vt-extensions/color-palette-update-notifications.md
456 switch (sequence[delim_idx + 1]) {
457 '1' => return .{
458 .event = .{ .color_scheme = .dark },
459 .n = sequence.len,
460 },
461 '2' => return .{
462 .event = .{ .color_scheme = .light },
463 .n = sequence.len,
464 },
465 else => return null_event,
466 }
467 },
468 else => return null_event,
469 }
470 },
471 else => return null_event,
472 }
473 },
474 'u' => {
475 // Kitty keyboard
476 // CSI unicode-key-code:alternate-key-codes ; modifiers:event-type ; text-as-codepoints u
477 // Not all fields will be present. Only unicode-key-code is
478 // mandatory
479
480 if (sequence.len > 2 and sequence[2] == '?') return .{
481 .event = .cap_kitty_keyboard,
482 .n = sequence.len,
483 };
484
485 var key: Key = .{
486 .codepoint = undefined,
487 };
488 // Split first into fields delimited by ';'
489 var field_iter = std.mem.splitScalar(u8, sequence[2 .. sequence.len - 1], ';');
490
491 { // field 1
492 // unicode-key-code:shifted_codepoint:base_layout_codepoint
493 const field_buf = field_iter.next() orelse unreachable; // There will always be at least one field
494 var param_iter = std.mem.splitScalar(u8, field_buf, ':');
495 const codepoint_buf = param_iter.next() orelse unreachable;
496 key.codepoint = parseParam(u21, codepoint_buf, null) orelse return null_event;
497
498 if (param_iter.next()) |shifted_cp_buf| {
499 key.shifted_codepoint = parseParam(u21, shifted_cp_buf, null);
500 }
501 if (param_iter.next()) |base_layout_buf| {
502 key.base_layout_codepoint = parseParam(u21, base_layout_buf, null);
503 }
504 }
505
506 var is_release: bool = false;
507
508 field2: {
509 // modifier_mask:event_type
510 const field_buf = field_iter.next() orelse break :field2;
511 var param_iter = std.mem.splitScalar(u8, field_buf, ':');
512 const modifier_buf = param_iter.next() orelse unreachable;
513 const modifier_mask = parseParam(u8, modifier_buf, 1) orelse return null_event;
514 key.mods = @bitCast(modifier_mask -| 1);
515
516 if (param_iter.next()) |event_type_buf| {
517 is_release = std.mem.eql(u8, event_type_buf, "3");
518 }
519 }
520
521 field3: {
522 // text_as_codepoint[:text_as_codepoint]
523 const field_buf = field_iter.next() orelse break :field3;
524 var param_iter = std.mem.splitScalar(u8, field_buf, ':');
525 var total: usize = 0;
526 while (param_iter.next()) |cp_buf| {
527 const cp = parseParam(u21, cp_buf, null) orelse return null_event;
528 total += std.unicode.utf8Encode(cp, text_buf[total..]) catch return null_event;
529 }
530 key.text = text_buf[0..total];
531 }
532
533 const event: Event = if (is_release)
534 .{ .key_release = key }
535 else
536 .{ .key_press = key };
537
538 return .{ .event = event, .n = sequence.len };
539 },
540 'y' => {
541 // DECRPM (CSI ? Ps ; Pm $ y)
542 const delim_idx = std.mem.indexOfScalarPos(u8, input, 2, ';') orelse return null_event;
543 const ps = std.fmt.parseUnsigned(u16, input[3..delim_idx], 10) catch return null_event;
544 const pm = std.fmt.parseUnsigned(u8, input[delim_idx + 1 .. sequence.len - 2], 10) catch return null_event;
545 switch (ps) {
546 // Mouse Pixel reporting
547 1016 => switch (pm) {
548 0, 4 => return null_event,
549 else => return .{ .event = .cap_sgr_pixels, .n = sequence.len },
550 },
551 // Unicode Core, see https://github.com/contour-terminal/terminal-unicode-core
552 2027 => switch (pm) {
553 0, 4 => return null_event,
554 else => return .{ .event = .cap_unicode, .n = sequence.len },
555 },
556 // Color scheme reportnig, see https://github.com/contour-terminal/contour/blob/master/docs/vt-extensions/color-palette-update-notifications.md
557 2031 => switch (pm) {
558 0, 4 => return null_event,
559 else => return .{ .event = .cap_color_scheme_updates, .n = sequence.len },
560 },
561 else => return null_event,
562 }
563 },
564 else => return null_event,
565 }
566}
567
568/// Parse a param buffer, returning a default value if the param was empty
569inline fn parseParam(comptime T: type, buf: []const u8, default: ?T) ?T {
570 if (buf.len == 0) return default;
571 return std.fmt.parseUnsigned(T, buf, 10) catch return null;
572}
573
574/// Parse a mouse event
575inline fn parseMouse(input: []const u8) Result {
576 std.debug.assert(input.len >= 4); // ESC [ < [Mm]
577 const null_event: Result = .{ .event = null, .n = input.len };
578
579 if (input[2] != '<') return null_event;
580
581 const delim1 = std.mem.indexOfScalarPos(u8, input, 3, ';') orelse return null_event;
582 const button_mask = parseParam(u16, input[3..delim1], null) orelse return null_event;
583 const delim2 = std.mem.indexOfScalarPos(u8, input, delim1 + 1, ';') orelse return null_event;
584 const px = parseParam(u16, input[delim1 + 1 .. delim2], 1) orelse return null_event;
585 const py = parseParam(u16, input[delim2 + 1 .. input.len - 1], 1) orelse return null_event;
586
587 const button: Mouse.Button = @enumFromInt(button_mask & mouse_bits.buttons);
588 const motion = button_mask & mouse_bits.motion > 0;
589 const shift = button_mask & mouse_bits.shift > 0;
590 const alt = button_mask & mouse_bits.alt > 0;
591 const ctrl = button_mask & mouse_bits.ctrl > 0;
592
593 const mouse = Mouse{
594 .button = button,
595 .mods = .{
596 .shift = shift,
597 .alt = alt,
598 .ctrl = ctrl,
599 },
600 .col = px -| 1,
601 .row = py -| 1,
602 .type = blk: {
603 if (motion and button != Mouse.Button.none) {
604 break :blk .drag;
605 }
606 if (motion and button == Mouse.Button.none) {
607 break :blk .motion;
608 }
609 if (input[input.len - 1] == 'm') break :blk .release;
610 break :blk .press;
611 },
612 };
613 return .{ .event = .{ .mouse = mouse }, .n = input.len };
614}
615
616test "parse: single xterm 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 = "a";
621 var parser: Parser = .{ .grapheme_data = &grapheme_data };
622 const result = try parser.parse(input, alloc);
623 const expected_key: Key = .{
624 .codepoint = 'a',
625 .text = "a",
626 };
627 const expected_event: Event = .{ .key_press = expected_key };
628
629 try testing.expectEqual(1, result.n);
630 try testing.expectEqual(expected_event, result.event);
631}
632
633test "parse: single xterm keypress backspace" {
634 const alloc = testing.allocator_instance.allocator();
635 const grapheme_data = try grapheme.GraphemeData.init(alloc);
636 defer grapheme_data.deinit();
637 const input = "\x08";
638 var parser: Parser = .{ .grapheme_data = &grapheme_data };
639 const result = try parser.parse(input, alloc);
640 const expected_key: Key = .{
641 .codepoint = Key.backspace,
642 };
643 const expected_event: Event = .{ .key_press = expected_key };
644
645 try testing.expectEqual(1, result.n);
646 try testing.expectEqual(expected_event, result.event);
647}
648
649test "parse: single xterm keypress with more buffer" {
650 const alloc = testing.allocator_instance.allocator();
651 const grapheme_data = try grapheme.GraphemeData.init(alloc);
652 defer grapheme_data.deinit();
653 const input = "ab";
654 var parser: Parser = .{ .grapheme_data = &grapheme_data };
655 const result = try parser.parse(input, alloc);
656 const expected_key: Key = .{
657 .codepoint = 'a',
658 .text = "a",
659 };
660 const expected_event: Event = .{ .key_press = expected_key };
661
662 try testing.expectEqual(1, result.n);
663 try testing.expectEqualStrings(expected_key.text.?, result.event.?.key_press.text.?);
664 try testing.expectEqualDeep(expected_event, result.event);
665}
666
667test "parse: xterm escape keypress" {
668 const alloc = testing.allocator_instance.allocator();
669 const grapheme_data = try grapheme.GraphemeData.init(alloc);
670 defer grapheme_data.deinit();
671 const input = "\x1b";
672 var parser: Parser = .{ .grapheme_data = &grapheme_data };
673 const result = try parser.parse(input, alloc);
674 const expected_key: Key = .{ .codepoint = Key.escape };
675 const expected_event: Event = .{ .key_press = expected_key };
676
677 try testing.expectEqual(1, result.n);
678 try testing.expectEqual(expected_event, result.event);
679}
680
681test "parse: xterm ctrl+a" {
682 const alloc = testing.allocator_instance.allocator();
683 const grapheme_data = try grapheme.GraphemeData.init(alloc);
684 defer grapheme_data.deinit();
685 const input = "\x01";
686 var parser: Parser = .{ .grapheme_data = &grapheme_data };
687 const result = try parser.parse(input, alloc);
688 const expected_key: Key = .{ .codepoint = 'a', .mods = .{ .ctrl = true } };
689 const expected_event: Event = .{ .key_press = expected_key };
690
691 try testing.expectEqual(1, result.n);
692 try testing.expectEqual(expected_event, result.event);
693}
694
695test "parse: xterm alt+a" {
696 const alloc = testing.allocator_instance.allocator();
697 const grapheme_data = try grapheme.GraphemeData.init(alloc);
698 defer grapheme_data.deinit();
699 const input = "\x1ba";
700 var parser: Parser = .{ .grapheme_data = &grapheme_data };
701 const result = try parser.parse(input, alloc);
702 const expected_key: Key = .{ .codepoint = 'a', .mods = .{ .alt = true } };
703 const expected_event: Event = .{ .key_press = expected_key };
704
705 try testing.expectEqual(2, result.n);
706 try testing.expectEqual(expected_event, result.event);
707}
708
709test "parse: xterm key up" {
710 const alloc = testing.allocator_instance.allocator();
711 const grapheme_data = try grapheme.GraphemeData.init(alloc);
712 defer grapheme_data.deinit();
713 {
714 // normal version
715 const input = "\x1b[A";
716 var parser: Parser = .{ .grapheme_data = &grapheme_data };
717 const result = try parser.parse(input, alloc);
718 const expected_key: Key = .{ .codepoint = Key.up };
719 const expected_event: Event = .{ .key_press = expected_key };
720
721 try testing.expectEqual(3, result.n);
722 try testing.expectEqual(expected_event, result.event);
723 }
724
725 {
726 // application keys version
727 const input = "\x1bOA";
728 var parser: Parser = .{ .grapheme_data = &grapheme_data };
729 const result = try parser.parse(input, alloc);
730 const expected_key: Key = .{ .codepoint = Key.up };
731 const expected_event: Event = .{ .key_press = expected_key };
732
733 try testing.expectEqual(3, result.n);
734 try testing.expectEqual(expected_event, result.event);
735 }
736}
737
738test "parse: xterm shift+up" {
739 const alloc = testing.allocator_instance.allocator();
740 const grapheme_data = try grapheme.GraphemeData.init(alloc);
741 defer grapheme_data.deinit();
742 const input = "\x1b[1;2A";
743 var parser: Parser = .{ .grapheme_data = &grapheme_data };
744 const result = try parser.parse(input, alloc);
745 const expected_key: Key = .{ .codepoint = Key.up, .mods = .{ .shift = true } };
746 const expected_event: Event = .{ .key_press = expected_key };
747
748 try testing.expectEqual(6, result.n);
749 try testing.expectEqual(expected_event, result.event);
750}
751
752test "parse: xterm insert" {
753 const alloc = testing.allocator_instance.allocator();
754 const grapheme_data = try grapheme.GraphemeData.init(alloc);
755 defer grapheme_data.deinit();
756 const input = "\x1b[2~";
757 var parser: Parser = .{ .grapheme_data = &grapheme_data };
758 const result = try parser.parse(input, alloc);
759 const expected_key: Key = .{ .codepoint = Key.insert, .mods = .{} };
760 const expected_event: Event = .{ .key_press = expected_key };
761
762 try testing.expectEqual(input.len, result.n);
763 try testing.expectEqual(expected_event, result.event);
764}
765
766test "parse: paste_start" {
767 const alloc = testing.allocator_instance.allocator();
768 const grapheme_data = try grapheme.GraphemeData.init(alloc);
769 defer grapheme_data.deinit();
770 const input = "\x1b[200~";
771 var parser: Parser = .{ .grapheme_data = &grapheme_data };
772 const result = try parser.parse(input, alloc);
773 const expected_event: Event = .paste_start;
774
775 try testing.expectEqual(6, result.n);
776 try testing.expectEqual(expected_event, result.event);
777}
778
779test "parse: paste_end" {
780 const alloc = testing.allocator_instance.allocator();
781 const grapheme_data = try grapheme.GraphemeData.init(alloc);
782 defer grapheme_data.deinit();
783 const input = "\x1b[201~";
784 var parser: Parser = .{ .grapheme_data = &grapheme_data };
785 const result = try parser.parse(input, alloc);
786 const expected_event: Event = .paste_end;
787
788 try testing.expectEqual(6, result.n);
789 try testing.expectEqual(expected_event, result.event);
790}
791
792test "parse: osc52 paste" {
793 const alloc = testing.allocator_instance.allocator();
794 const grapheme_data = try grapheme.GraphemeData.init(alloc);
795 defer grapheme_data.deinit();
796 const input = "\x1b]52;c;b3NjNTIgcGFzdGU=\x1b\\";
797 const expected_text = "osc52 paste";
798 var parser: Parser = .{ .grapheme_data = &grapheme_data };
799 const result = try parser.parse(input, alloc);
800
801 try testing.expectEqual(25, result.n);
802 switch (result.event.?) {
803 .paste => |text| {
804 defer alloc.free(text);
805 try testing.expectEqualStrings(expected_text, text);
806 },
807 else => try testing.expect(false),
808 }
809}
810
811test "parse: focus_in" {
812 const alloc = testing.allocator_instance.allocator();
813 const grapheme_data = try grapheme.GraphemeData.init(alloc);
814 defer grapheme_data.deinit();
815 const input = "\x1b[I";
816 var parser: Parser = .{ .grapheme_data = &grapheme_data };
817 const result = try parser.parse(input, alloc);
818 const expected_event: Event = .focus_in;
819
820 try testing.expectEqual(3, result.n);
821 try testing.expectEqual(expected_event, result.event);
822}
823
824test "parse: focus_out" {
825 const alloc = testing.allocator_instance.allocator();
826 const grapheme_data = try grapheme.GraphemeData.init(alloc);
827 defer grapheme_data.deinit();
828 const input = "\x1b[O";
829 var parser: Parser = .{ .grapheme_data = &grapheme_data };
830 const result = try parser.parse(input, alloc);
831 const expected_event: Event = .focus_out;
832
833 try testing.expectEqual(3, result.n);
834 try testing.expectEqual(expected_event, result.event);
835}
836
837test "parse: kitty: shift+a without text reporting" {
838 const alloc = testing.allocator_instance.allocator();
839 const grapheme_data = try grapheme.GraphemeData.init(alloc);
840 defer grapheme_data.deinit();
841 const input = "\x1b[97:65;2u";
842 var parser: Parser = .{ .grapheme_data = &grapheme_data };
843 const result = try parser.parse(input, alloc);
844 const expected_key: Key = .{
845 .codepoint = 'a',
846 .shifted_codepoint = 'A',
847 .mods = .{ .shift = true },
848 };
849 const expected_event: Event = .{ .key_press = expected_key };
850
851 try testing.expectEqual(10, result.n);
852 try testing.expectEqual(expected_event, result.event);
853}
854
855test "parse: kitty: alt+shift+a without text reporting" {
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[97:65;4u";
860 var parser: Parser = .{ .grapheme_data = &grapheme_data };
861 const result = try parser.parse(input, alloc);
862 const expected_key: Key = .{
863 .codepoint = 'a',
864 .shifted_codepoint = 'A',
865 .mods = .{ .shift = true, .alt = true },
866 };
867 const expected_event: Event = .{ .key_press = expected_key };
868
869 try testing.expectEqual(10, result.n);
870 try testing.expectEqual(expected_event, result.event);
871}
872
873test "parse: kitty: a without text reporting" {
874 const alloc = testing.allocator_instance.allocator();
875 const grapheme_data = try grapheme.GraphemeData.init(alloc);
876 defer grapheme_data.deinit();
877 const input = "\x1b[97u";
878 var parser: Parser = .{ .grapheme_data = &grapheme_data };
879 const result = try parser.parse(input, alloc);
880 const expected_key: Key = .{
881 .codepoint = 'a',
882 };
883 const expected_event: Event = .{ .key_press = expected_key };
884
885 try testing.expectEqual(5, result.n);
886 try testing.expectEqual(expected_event, result.event);
887}
888
889test "parse: kitty: release event" {
890 const alloc = testing.allocator_instance.allocator();
891 const grapheme_data = try grapheme.GraphemeData.init(alloc);
892 defer grapheme_data.deinit();
893 const input = "\x1b[97;1:3u";
894 var parser: Parser = .{ .grapheme_data = &grapheme_data };
895 const result = try parser.parse(input, alloc);
896 const expected_key: Key = .{
897 .codepoint = 'a',
898 };
899 const expected_event: Event = .{ .key_release = expected_key };
900
901 try testing.expectEqual(9, result.n);
902 try testing.expectEqual(expected_event, result.event);
903}
904
905test "parse: single codepoint" {
906 const alloc = testing.allocator_instance.allocator();
907 const grapheme_data = try grapheme.GraphemeData.init(alloc);
908 defer grapheme_data.deinit();
909 const input = "🙂";
910 var parser: Parser = .{ .grapheme_data = &grapheme_data };
911 const result = try parser.parse(input, alloc);
912 const expected_key: Key = .{
913 .codepoint = 0x1F642,
914 .text = input,
915 };
916 const expected_event: Event = .{ .key_press = expected_key };
917
918 try testing.expectEqual(4, result.n);
919 try testing.expectEqual(expected_event, result.event);
920}
921
922test "parse: single codepoint with more in buffer" {
923 const alloc = testing.allocator_instance.allocator();
924 const grapheme_data = try grapheme.GraphemeData.init(alloc);
925 defer grapheme_data.deinit();
926 const input = "🙂a";
927 var parser: Parser = .{ .grapheme_data = &grapheme_data };
928 const result = try parser.parse(input, alloc);
929 const expected_key: Key = .{
930 .codepoint = 0x1F642,
931 .text = "🙂",
932 };
933 const expected_event: Event = .{ .key_press = expected_key };
934
935 try testing.expectEqual(4, result.n);
936 try testing.expectEqualDeep(expected_event, result.event);
937}
938
939test "parse: multiple codepoint grapheme" {
940 const alloc = testing.allocator_instance.allocator();
941 const grapheme_data = try grapheme.GraphemeData.init(alloc);
942 defer grapheme_data.deinit();
943 const input = "👩🚀";
944 var parser: Parser = .{ .grapheme_data = &grapheme_data };
945 const result = try parser.parse(input, alloc);
946 const expected_key: Key = .{
947 .codepoint = Key.multicodepoint,
948 .text = input,
949 };
950 const expected_event: Event = .{ .key_press = expected_key };
951
952 try testing.expectEqual(input.len, result.n);
953 try testing.expectEqual(expected_event, result.event);
954}
955
956test "parse: multiple codepoint grapheme with more after" {
957 const alloc = testing.allocator_instance.allocator();
958 const grapheme_data = try grapheme.GraphemeData.init(alloc);
959 defer grapheme_data.deinit();
960 const input = "👩🚀abc";
961 var parser: Parser = .{ .grapheme_data = &grapheme_data };
962 const result = try parser.parse(input, alloc);
963 const expected_key: Key = .{
964 .codepoint = Key.multicodepoint,
965 .text = "👩🚀",
966 };
967
968 try testing.expectEqual(expected_key.text.?.len, result.n);
969 const actual = result.event.?.key_press;
970 try testing.expectEqualStrings(expected_key.text.?, actual.text.?);
971 try testing.expectEqual(expected_key.codepoint, actual.codepoint);
972}
973
974test "parse(csi): decrpm" {
975 var buf: [1]u8 = undefined;
976 {
977 const input = "\x1b[1016;1y";
978 const result = parseCsi(input, &buf);
979 const expected: Result = .{
980 .event = .cap_sgr_pixels,
981 .n = input.len,
982 };
983
984 try testing.expectEqual(expected.n, result.n);
985 try testing.expectEqual(expected.event, result.event);
986 }
987 {
988 const input = "\x1b[1016;0y";
989 const result = parseCsi(input, &buf);
990 const expected: Result = .{
991 .event = null,
992 .n = input.len,
993 };
994
995 try testing.expectEqual(expected.n, result.n);
996 try testing.expectEqual(expected.event, result.event);
997 }
998}
999
1000test "parse(csi): primary da" {
1001 var buf: [1]u8 = undefined;
1002 const input = "\x1b[?c";
1003 const result = parseCsi(input, &buf);
1004 const expected: Result = .{
1005 .event = .cap_da1,
1006 .n = input.len,
1007 };
1008
1009 try testing.expectEqual(expected.n, result.n);
1010 try testing.expectEqual(expected.event, result.event);
1011}
1012
1013test "parse(csi): dsr" {
1014 var buf: [1]u8 = undefined;
1015 {
1016 const input = "\x1b[?997;1n";
1017 const result = parseCsi(input, &buf);
1018 const expected: Result = .{
1019 .event = .{ .color_scheme = .dark },
1020 .n = input.len,
1021 };
1022
1023 try testing.expectEqual(expected.n, result.n);
1024 try testing.expectEqual(expected.event, result.event);
1025 }
1026 {
1027 const input = "\x1b[?997;2n";
1028 const result = parseCsi(input, &buf);
1029 const expected: Result = .{
1030 .event = .{ .color_scheme = .light },
1031 .n = input.len,
1032 };
1033
1034 try testing.expectEqual(expected.n, result.n);
1035 try testing.expectEqual(expected.event, result.event);
1036 }
1037 {
1038 const input = "\x1b[0n";
1039 const result = parseCsi(input, &buf);
1040 const expected: Result = .{
1041 .event = null,
1042 .n = input.len,
1043 };
1044
1045 try testing.expectEqual(expected.n, result.n);
1046 try testing.expectEqual(expected.event, result.event);
1047 }
1048}
1049
1050test "parse(csi): mouse" {
1051 var buf: [1]u8 = undefined;
1052 const input = "\x1b[<35;1;1m";
1053 const result = parseCsi(input, &buf);
1054 const expected: Result = .{
1055 .event = .{ .mouse = .{
1056 .col = 0,
1057 .row = 0,
1058 .button = .none,
1059 .type = .motion,
1060 .mods = .{},
1061 } },
1062 .n = input.len,
1063 };
1064
1065 try testing.expectEqual(expected.n, result.n);
1066 try testing.expectEqual(expected.event, result.event);
1067}