a modern tui library written in zig
1const std = @import("std");
2const testing = std.testing;
3const Event = @import("event.zig").Event;
4const Key = @import("Key.zig");
5const Mouse = @import("Mouse.zig");
6const CodePointIterator = @import("ziglyph").CodePointIterator;
7const graphemeBreak = @import("ziglyph").graphemeBreak;
8
9const log = std.log.scoped(.parser);
10
11const Parser = @This();
12
13/// The return type of our parse method. Contains an Event and the number of
14/// bytes read from the buffer.
15pub const Result = struct {
16 event: ?Event,
17 n: usize,
18};
19
20// an intermediate data structure to hold sequence data while we are
21// scanning more bytes. This is tailored for input parsing only
22const Sequence = struct {
23 // private indicators are 0x3C-0x3F
24 private_indicator: ?u8 = null,
25 // we won't be handling any sequences with more than one intermediate
26 intermediate: ?u8 = null,
27 // we should absolutely never have more then 16 params
28 params: [16]u16 = undefined,
29 param_idx: usize = 0,
30 param_buf: [8]u8 = undefined,
31 param_buf_idx: usize = 0,
32 sub_state: std.StaticBitSet(16) = std.StaticBitSet(16).initEmpty(),
33 empty_state: std.StaticBitSet(16) = std.StaticBitSet(16).initEmpty(),
34};
35
36const mouse_bits = struct {
37 const motion: u8 = 0b00100000;
38 const buttons: u8 = 0b11000011;
39 const shift: u8 = 0b00000100;
40 const alt: u8 = 0b00001000;
41 const ctrl: u8 = 0b00010000;
42};
43
44// the state of the parser
45const State = enum {
46 ground,
47 escape,
48 csi,
49 osc,
50 dcs,
51 sos,
52 pm,
53 apc,
54 ss2,
55 ss3,
56};
57
58// a buffer to temporarily store text in. We need this to encode
59// text-as-codepoints
60buf: [128]u8 = undefined,
61
62pub fn parse(self: *Parser, input: []const u8) !Result {
63 const n = input.len;
64
65 var seq: Sequence = .{};
66
67 var state: State = .ground;
68
69 var i: usize = 0;
70 var start: usize = 0;
71 // parse the read into events. This parser is bespoke for input parsing
72 // and is not suitable for reuse as a generic vt parser
73 while (i < n) : (i += 1) {
74 const b = input[i];
75 switch (state) {
76 .ground => {
77 // ground state generates keypresses when parsing input. We
78 // generally get ascii characters, but anything less than
79 // 0x20 is a Ctrl+<c> keypress. We map these to lowercase
80 // ascii characters when we can
81 const key: Key = switch (b) {
82 0x00 => .{ .codepoint = '@', .mods = .{ .ctrl = true } },
83 0x08 => .{ .codepoint = Key.backspace },
84 0x01...0x07,
85 0x09...0x1A,
86 => .{ .codepoint = b + 0x60, .mods = .{ .ctrl = true } },
87 0x1B => escape: {
88 // NOTE: This could be an errant escape at the end
89 // of a large read. That is _incredibly_ unlikely
90 // given the size of read inputs and our read buffer
91 if (i == (n - 1)) {
92 const event = Key{
93 .codepoint = Key.escape,
94 };
95 break :escape event;
96 }
97 state = .escape;
98 continue;
99 },
100 0x7F => .{ .codepoint = Key.backspace },
101 else => blk: {
102 var iter: CodePointIterator = .{ .bytes = input[i..] };
103 // return null if we don't have a valid codepoint
104 var cp = iter.next() orelse return .{ .event = null, .n = 0 };
105
106 var code = cp.code;
107 i += cp.len - 1; // subtract one for the loop iter
108 var g_state: u3 = 0;
109 while (iter.next()) |next_cp| {
110 if (graphemeBreak(cp.code, next_cp.code, &g_state)) {
111 break;
112 }
113 code = Key.multicodepoint;
114 i += next_cp.len;
115 cp = next_cp;
116 }
117
118 break :blk .{ .codepoint = code, .text = input[start .. i + 1] };
119 },
120 };
121 return .{
122 .event = .{ .key_press = key },
123 .n = i + 1,
124 };
125 },
126 .escape => {
127 seq = .{};
128 start = i;
129 switch (b) {
130 0x4F => state = .ss3,
131 0x50 => state = .dcs,
132 0x58 => state = .sos,
133 0x5B => state = .csi,
134 0x5D => state = .osc,
135 0x5E => state = .pm,
136 0x5F => state = .apc,
137 else => {
138 // Anything else is an "alt + <b>" keypress
139 const key: Key = .{
140 .codepoint = b,
141 .mods = .{ .alt = true },
142 };
143 return .{
144 .event = .{ .key_press = key },
145 .n = i + 1,
146 };
147 },
148 }
149 },
150 .ss3 => {
151 const key: Key = switch (b) {
152 'A' => .{ .codepoint = Key.up },
153 'B' => .{ .codepoint = Key.down },
154 'C' => .{ .codepoint = Key.right },
155 'D' => .{ .codepoint = Key.left },
156 'F' => .{ .codepoint = Key.end },
157 'H' => .{ .codepoint = Key.home },
158 'P' => .{ .codepoint = Key.f1 },
159 'Q' => .{ .codepoint = Key.f2 },
160 'R' => .{ .codepoint = Key.f3 },
161 'S' => .{ .codepoint = Key.f4 },
162 else => {
163 log.warn("unhandled ss3: {x}", .{b});
164 return .{
165 .event = null,
166 .n = i + 1,
167 };
168 },
169 };
170 return .{
171 .event = .{ .key_press = key },
172 .n = i + 1,
173 };
174 },
175 .csi => {
176 switch (b) {
177 // c0 controls. we ignore these even though we should
178 // "execute" them. This isn't seen in practice
179 0x00...0x1F => {},
180 // intermediates. we only handle one. technically there
181 // can be more
182 0x20...0x2F => seq.intermediate = b,
183 0x30...0x39 => {
184 seq.param_buf[seq.param_buf_idx] = b;
185 seq.param_buf_idx += 1;
186 },
187 // private indicators. These come before any params ('?')
188 0x3C...0x3F => seq.private_indicator = b,
189 ';' => {
190 if (seq.param_buf_idx == 0) {
191 // empty param. default it to 0 and set the
192 // empty state
193 seq.params[seq.param_idx] = 0;
194 seq.empty_state.set(seq.param_idx);
195 seq.param_idx += 1;
196 } else {
197 const p = try std.fmt.parseUnsigned(u16, seq.param_buf[0..seq.param_buf_idx], 10);
198 seq.param_buf_idx = 0;
199 seq.params[seq.param_idx] = p;
200 seq.param_idx += 1;
201 }
202 },
203 ':' => {
204 if (seq.param_buf_idx == 0) {
205 // empty param. default it to 0 and set the
206 // empty state
207 seq.params[seq.param_idx] = 0;
208 seq.empty_state.set(seq.param_idx);
209 seq.param_idx += 1;
210 // Set the *next* param as a subparam
211 seq.sub_state.set(seq.param_idx);
212 } else {
213 const p = try std.fmt.parseUnsigned(u16, seq.param_buf[0..seq.param_buf_idx], 10);
214 seq.param_buf_idx = 0;
215 seq.params[seq.param_idx] = p;
216 seq.param_idx += 1;
217 // Set the *next* param as a subparam
218 seq.sub_state.set(seq.param_idx);
219 }
220 },
221 0x40...0xFF => {
222 if (seq.param_buf_idx > 0) {
223 const p = try std.fmt.parseUnsigned(u16, seq.param_buf[0..seq.param_buf_idx], 10);
224 seq.param_buf_idx = 0;
225 seq.params[seq.param_idx] = p;
226 seq.param_idx += 1;
227 }
228 // dispatch the sequence
229 state = .ground;
230 const codepoint: u21 = switch (b) {
231 'A' => Key.up,
232 'B' => Key.down,
233 'C' => Key.right,
234 'D' => Key.left,
235 'E' => Key.kp_begin,
236 'F' => Key.end,
237 'H' => Key.home,
238 'M', 'm' => { // mouse event
239 const priv = seq.private_indicator orelse {
240 log.warn("unhandled csi: CSI {s}", .{input[start + 1 .. i + 1]});
241 return .{ .event = null, .n = i + 1 };
242 };
243 if (priv != '<') {
244 log.warn("unhandled csi: CSI {s}", .{input[start + 1 .. i + 1]});
245 return .{ .event = null, .n = i + 1 };
246 }
247 if (seq.param_idx != 3) {
248 log.warn("unhandled csi: CSI {s}", .{input[start + 1 .. i + 1]});
249 return .{ .event = null, .n = i + 1 };
250 }
251 const button: Mouse.Button = @enumFromInt(seq.params[0] & mouse_bits.buttons);
252 const motion = seq.params[0] & mouse_bits.motion > 0;
253 const shift = seq.params[0] & mouse_bits.shift > 0;
254 const alt = seq.params[0] & mouse_bits.alt > 0;
255 const ctrl = seq.params[0] & mouse_bits.ctrl > 0;
256 const col: usize = seq.params[1] - 1;
257 const row: usize = seq.params[2] - 1;
258
259 const mouse = Mouse{
260 .button = button,
261 .mods = .{
262 .shift = shift,
263 .alt = alt,
264 .ctrl = ctrl,
265 },
266 .col = col,
267 .row = row,
268 .type = blk: {
269 if (motion and button != Mouse.Button.none) {
270 break :blk .drag;
271 }
272 if (motion and button == Mouse.Button.none) {
273 break :blk .motion;
274 }
275 if (b == 'm') break :blk .release;
276 break :blk .press;
277 },
278 };
279 return .{ .event = .{ .mouse = mouse }, .n = i + 1 };
280 },
281 'P' => Key.f1,
282 'Q' => Key.f2,
283 'R' => Key.f3,
284 'S' => Key.f4,
285 '~' => blk: {
286 // The first param will define this
287 // codepoint
288 if (seq.param_idx < 1) {
289 log.warn("unhandled csi: CSI {s}", .{input[start + 1 .. i + 1]});
290 return .{
291 .event = null,
292 .n = i + 1,
293 };
294 }
295 switch (seq.params[0]) {
296 2 => break :blk Key.insert,
297 3 => break :blk Key.delete,
298 5 => break :blk Key.page_up,
299 6 => break :blk Key.page_down,
300 7 => break :blk Key.home,
301 8 => break :blk Key.end,
302 11 => break :blk Key.f1,
303 12 => break :blk Key.f2,
304 13 => break :blk Key.f3,
305 14 => break :blk Key.f4,
306 15 => break :blk Key.f5,
307 17 => break :blk Key.f6,
308 18 => break :blk Key.f7,
309 19 => break :blk Key.f8,
310 20 => break :blk Key.f9,
311 21 => break :blk Key.f10,
312 23 => break :blk Key.f11,
313 24 => break :blk Key.f12,
314 200 => {
315 return .{
316 .event = .paste_start,
317 .n = i + 1,
318 };
319 },
320 201 => {
321 return .{
322 .event = .paste_end,
323 .n = i + 1,
324 };
325 },
326 57427 => break :blk Key.kp_begin,
327 else => {
328 log.warn("unhandled csi: CSI {s}", .{input[start + 1 .. i + 1]});
329 return .{
330 .event = null,
331 .n = i + 1,
332 };
333 },
334 }
335 },
336 'u' => blk: {
337 if (seq.private_indicator) |priv| {
338 // response to our kitty query
339 if (priv == '?') {
340 return .{
341 .event = .cap_kitty_keyboard,
342 .n = i + 1,
343 };
344 } else {
345 log.warn("unhandled csi: CSI {s}", .{input[start + 1 .. i + 1]});
346 return .{
347 .event = null,
348 .n = i + 1,
349 };
350 }
351 }
352 if (seq.param_idx == 0) {
353 log.warn("unhandled csi: CSI {s}", .{input[start + 1 .. i + 1]});
354 return .{
355 .event = null,
356 .n = i + 1,
357 };
358 }
359 // In any csi u encoding, the codepoint
360 // directly maps to our keypoint definitions
361 break :blk seq.params[0];
362 },
363
364 'I' => { // focus in
365 return .{ .event = .focus_in, .n = i + 1 };
366 },
367 'O' => { // focus out
368 return .{ .event = .focus_out, .n = i + 1 };
369 },
370 'y' => { // DECRQM response
371 const priv = seq.private_indicator orelse {
372 log.warn("unhandled csi: CSI {s}", .{input[start + 1 .. i + 1]});
373 return .{ .event = null, .n = i + 1 };
374 };
375 if (priv != '?') {
376 log.warn("unhandled csi: CSI {s}", .{input[start + 1 .. i + 1]});
377 return .{ .event = null, .n = i + 1 };
378 }
379 const intm = seq.intermediate orelse {
380 log.warn("unhandled csi: CSI {s}", .{input[start + 1 .. i + 1]});
381 return .{ .event = null, .n = i + 1 };
382 };
383 if (intm != '$') {
384 log.warn("unhandled csi: CSI {s}", .{input[start + 1 .. i + 1]});
385 return .{ .event = null, .n = i + 1 };
386 }
387 if (seq.param_idx != 2) {
388 log.warn("unhandled csi: CSI {s}", .{input[start + 1 .. i + 1]});
389 return .{ .event = null, .n = i + 1 };
390 }
391 // We'll get two fields, the first is the mode
392 // we requested, the second is the status of the
393 // mode
394 // 0: not recognize
395 // 1: set
396 // 2: reset
397 // 3: permanently set
398 // 4: permanently reset
399 switch (seq.params[0]) {
400 2027 => {
401 switch (seq.params[1]) {
402 0, 4 => return .{ .event = null, .n = i + 1 },
403 else => return .{ .event = .cap_unicode, .n = i + 1 },
404 }
405 },
406 2031 => {},
407 else => {
408 log.warn("unhandled DECRPM: CSI {s}", .{input[start + 1 .. i + 1]});
409 return .{ .event = null, .n = i + 1 };
410 },
411 }
412 log.warn("unhandled csi: CSI {s}", .{input[start + 1 .. i + 1]});
413 return .{ .event = null, .n = i + 1 };
414 },
415 'c' => { // DA1 response
416 const priv = seq.private_indicator orelse {
417 log.warn("unhandled csi: CSI {s}", .{input[start + 1 .. i + 1]});
418 return .{ .event = null, .n = i + 1 };
419 };
420 if (priv != '?') {
421 log.warn("unhandled csi: CSI {s}", .{input[start + 1 .. i + 1]});
422 return .{ .event = null, .n = i + 1 };
423 }
424 return .{ .event = .cap_da1, .n = i + 1 };
425 },
426 else => {
427 log.warn("unhandled csi: CSI {s}", .{input[start + 1 .. i + 1]});
428 return .{
429 .event = null,
430 .n = i + 1,
431 };
432 },
433 };
434
435 var key: Key = .{ .codepoint = codepoint };
436
437 var idx: usize = 0;
438 var field: u8 = 0;
439 // parse the parameters
440 while (idx < seq.param_idx) : (idx += 1) {
441 switch (field) {
442 0 => {
443 defer field += 1;
444 // field 0 contains our codepoint. Any
445 // subparameters shifted key code and
446 // alternate keycode (csi u encoding)
447
448 // We already handled our codepoint so
449 // we just need to check for subs
450 if (!seq.sub_state.isSet(idx + 1)) {
451 continue;
452 }
453 idx += 1;
454 // The first one is a shifted code if it
455 // isn't empty
456 if (!seq.empty_state.isSet(idx)) {
457 key.shifted_codepoint = seq.params[idx];
458 }
459 // check the next one for base layout
460 // code
461 if (!seq.sub_state.isSet(idx + 1)) {
462 continue;
463 }
464 idx += 1;
465 key.base_layout_codepoint = seq.params[idx];
466 },
467 1 => {
468 defer field += 1;
469 // field 1 is modifiers and optionally
470 // the event type (csiu). It can be empty
471 if (seq.empty_state.isSet(idx)) {
472 continue;
473 }
474 // default of 1
475 const ps: u8 = blk: {
476 if (seq.params[idx] == 0) break :blk 1;
477 break :blk @truncate(seq.params[idx]);
478 };
479 key.mods = @bitCast(ps - 1);
480 },
481 2 => {
482 // field 2 is text, as codepoints
483 var total: usize = 0;
484 while (idx < seq.param_idx) : (idx += 1) {
485 total += try std.unicode.utf8Encode(seq.params[idx], self.buf[total..]);
486 }
487 key.text = self.buf[0..total];
488 },
489 else => {},
490 }
491 }
492 return .{
493 .event = .{ .key_press = key },
494 .n = i + 1,
495 };
496 },
497 }
498 },
499 .apc => {
500 switch (b) {
501 0x1B => {
502 state = .ground;
503 // advance one more for the backslash
504 i += 1;
505 switch (input[start + 1]) {
506 'G' => {
507 return .{
508 .event = .cap_kitty_graphics,
509 .n = i + 1,
510 };
511 },
512 else => {
513 log.warn("unhandled apc: APC {s}", .{input[start + 1 .. i + 1]});
514 return .{
515 .event = null,
516 .n = i + 1,
517 };
518 },
519 }
520 },
521 else => {},
522 }
523 },
524 .sos, .pm => {
525 switch (b) {
526 0x1B => {
527 state = .ground;
528 // advance one more for the backslash
529 i += 1;
530 log.warn("unhandled sos/pm: SOS/PM {s}", .{input[start + 1 .. i + 1]});
531 return .{
532 .event = null,
533 .n = i + 1,
534 };
535 },
536 else => {},
537 }
538 },
539 else => {},
540 }
541 }
542 // If we get here it means we didn't parse an event. The input buffer
543 // perhaps didn't include a full event
544 return .{
545 .event = null,
546 .n = 0,
547 };
548}
549
550test "parse: single xterm keypress" {
551 const input = "a";
552 var parser: Parser = .{};
553 const result = try parser.parse(input);
554 const expected_key: Key = .{
555 .codepoint = 'a',
556 .text = "a",
557 };
558 const expected_event: Event = .{ .key_press = expected_key };
559
560 try testing.expectEqual(1, result.n);
561 try testing.expectEqual(expected_event, result.event);
562}
563
564test "parse: single xterm keypress backspace" {
565 const input = "\x08";
566 var parser: Parser = .{};
567 const result = try parser.parse(input);
568 const expected_key: Key = .{
569 .codepoint = Key.backspace,
570 };
571 const expected_event: Event = .{ .key_press = expected_key };
572
573 try testing.expectEqual(1, result.n);
574 try testing.expectEqual(expected_event, result.event);
575}
576
577test "parse: single xterm keypress with more buffer" {
578 const input = "ab";
579 var parser: Parser = .{};
580 const result = try parser.parse(input);
581 const expected_key: Key = .{
582 .codepoint = 'a',
583 .text = "a",
584 };
585 const expected_event: Event = .{ .key_press = expected_key };
586
587 try testing.expectEqual(1, result.n);
588 try testing.expectEqualStrings(expected_key.text.?, result.event.?.key_press.text.?);
589 try testing.expectEqualDeep(expected_event, result.event);
590}
591
592test "parse: xterm escape keypress" {
593 const input = "\x1b";
594 var parser: Parser = .{};
595 const result = try parser.parse(input);
596 const expected_key: Key = .{ .codepoint = Key.escape };
597 const expected_event: Event = .{ .key_press = expected_key };
598
599 try testing.expectEqual(1, result.n);
600 try testing.expectEqual(expected_event, result.event);
601}
602
603test "parse: xterm ctrl+a" {
604 const input = "\x01";
605 var parser: Parser = .{};
606 const result = try parser.parse(input);
607 const expected_key: Key = .{ .codepoint = 'a', .mods = .{ .ctrl = true } };
608 const expected_event: Event = .{ .key_press = expected_key };
609
610 try testing.expectEqual(1, result.n);
611 try testing.expectEqual(expected_event, result.event);
612}
613
614test "parse: xterm alt+a" {
615 const input = "\x1ba";
616 var parser: Parser = .{};
617 const result = try parser.parse(input);
618 const expected_key: Key = .{ .codepoint = 'a', .mods = .{ .alt = true } };
619 const expected_event: Event = .{ .key_press = expected_key };
620
621 try testing.expectEqual(2, result.n);
622 try testing.expectEqual(expected_event, result.event);
623}
624
625test "parse: xterm invalid ss3" {
626 const input = "\x1bOZ";
627 var parser: Parser = .{};
628 const result = try parser.parse(input);
629
630 try testing.expectEqual(3, result.n);
631 try testing.expectEqual(null, result.event);
632}
633
634test "parse: xterm key up" {
635 {
636 // normal version
637 const input = "\x1bOA";
638 var parser: Parser = .{};
639 const result = try parser.parse(input);
640 const expected_key: Key = .{ .codepoint = Key.up };
641 const expected_event: Event = .{ .key_press = expected_key };
642
643 try testing.expectEqual(3, result.n);
644 try testing.expectEqual(expected_event, result.event);
645 }
646
647 {
648 // application keys version
649 const input = "\x1b[2~";
650 var parser: Parser = .{};
651 const result = try parser.parse(input);
652 const expected_key: Key = .{ .codepoint = Key.insert };
653 const expected_event: Event = .{ .key_press = expected_key };
654
655 try testing.expectEqual(4, result.n);
656 try testing.expectEqual(expected_event, result.event);
657 }
658}
659
660test "parse: xterm shift+up" {
661 const input = "\x1b[1;2A";
662 var parser: Parser = .{};
663 const result = try parser.parse(input);
664 const expected_key: Key = .{ .codepoint = Key.up, .mods = .{ .shift = true } };
665 const expected_event: Event = .{ .key_press = expected_key };
666
667 try testing.expectEqual(6, result.n);
668 try testing.expectEqual(expected_event, result.event);
669}
670
671test "parse: xterm insert" {
672 const input = "\x1b[1;2A";
673 var parser: Parser = .{};
674 const result = try parser.parse(input);
675 const expected_key: Key = .{ .codepoint = Key.up, .mods = .{ .shift = true } };
676 const expected_event: Event = .{ .key_press = expected_key };
677
678 try testing.expectEqual(6, result.n);
679 try testing.expectEqual(expected_event, result.event);
680}
681
682test "parse: paste_start" {
683 const input = "\x1b[200~";
684 var parser: Parser = .{};
685 const result = try parser.parse(input);
686 const expected_event: Event = .paste_start;
687
688 try testing.expectEqual(6, result.n);
689 try testing.expectEqual(expected_event, result.event);
690}
691
692test "parse: paste_end" {
693 const input = "\x1b[201~";
694 var parser: Parser = .{};
695 const result = try parser.parse(input);
696 const expected_event: Event = .paste_end;
697
698 try testing.expectEqual(6, result.n);
699 try testing.expectEqual(expected_event, result.event);
700}
701
702test "parse: focus_in" {
703 const input = "\x1b[I";
704 var parser: Parser = .{};
705 const result = try parser.parse(input);
706 const expected_event: Event = .focus_in;
707
708 try testing.expectEqual(3, result.n);
709 try testing.expectEqual(expected_event, result.event);
710}
711
712test "parse: focus_out" {
713 const input = "\x1b[O";
714 var parser: Parser = .{};
715 const result = try parser.parse(input);
716 const expected_event: Event = .focus_out;
717
718 try testing.expectEqual(3, result.n);
719 try testing.expectEqual(expected_event, result.event);
720}
721
722test "parse: kitty: shift+a without text reporting" {
723 const input = "\x1b[97:65;2u";
724 var parser: Parser = .{};
725 const result = try parser.parse(input);
726 const expected_key: Key = .{
727 .codepoint = 'a',
728 .shifted_codepoint = 'A',
729 .mods = .{ .shift = true },
730 };
731 const expected_event: Event = .{ .key_press = expected_key };
732
733 try testing.expectEqual(10, result.n);
734 try testing.expectEqual(expected_event, result.event);
735}
736
737test "parse: kitty: alt+shift+a without text reporting" {
738 const input = "\x1b[97:65;4u";
739 var parser: Parser = .{};
740 const result = try parser.parse(input);
741 const expected_key: Key = .{
742 .codepoint = 'a',
743 .shifted_codepoint = 'A',
744 .mods = .{ .shift = true, .alt = true },
745 };
746 const expected_event: Event = .{ .key_press = expected_key };
747
748 try testing.expectEqual(10, result.n);
749 try testing.expectEqual(expected_event, result.event);
750}
751
752test "parse: kitty: a without text reporting" {
753 const input = "\x1b[97u";
754 var parser: Parser = .{};
755 const result = try parser.parse(input);
756 const expected_key: Key = .{
757 .codepoint = 'a',
758 };
759 const expected_event: Event = .{ .key_press = expected_key };
760
761 try testing.expectEqual(5, result.n);
762 try testing.expectEqual(expected_event, result.event);
763}
764
765test "parse: single codepoint" {
766 const input = "🙂";
767 var parser: Parser = .{};
768 const result = try parser.parse(input);
769 const expected_key: Key = .{
770 .codepoint = 0x1F642,
771 .text = input,
772 };
773 const expected_event: Event = .{ .key_press = expected_key };
774
775 try testing.expectEqual(4, result.n);
776 try testing.expectEqual(expected_event, result.event);
777}
778
779test "parse: single codepoint with more in buffer" {
780 const input = "🙂a";
781 var parser: Parser = .{};
782 const result = try parser.parse(input);
783 const expected_key: Key = .{
784 .codepoint = 0x1F642,
785 .text = "🙂",
786 };
787 const expected_event: Event = .{ .key_press = expected_key };
788
789 try testing.expectEqual(4, result.n);
790 try testing.expectEqualDeep(expected_event, result.event);
791}
792
793test "parse: multiple codepoint grapheme" {
794 const input = "👩🚀";
795 var parser: Parser = .{};
796 const result = try parser.parse(input);
797 const expected_key: Key = .{
798 .codepoint = Key.multicodepoint,
799 .text = input,
800 };
801 const expected_event: Event = .{ .key_press = expected_key };
802
803 try testing.expectEqual(input.len, result.n);
804 try testing.expectEqual(expected_event, result.event);
805}
806
807test "parse: multiple codepoint grapheme with more after" {
808 const input = "👩🚀abc";
809 var parser: Parser = .{};
810 const result = try parser.parse(input);
811 const expected_key: Key = .{
812 .codepoint = Key.multicodepoint,
813 .text = "👩🚀",
814 };
815
816 try testing.expectEqual(expected_key.text.?.len, result.n);
817 const actual = result.event.?.key_press;
818 try testing.expectEqualStrings(expected_key.text.?, actual.text.?);
819 try testing.expectEqual(expected_key.codepoint, actual.codepoint);
820}