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