a modern tui library written in zig
at v0.1.0 35 kB view raw
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}