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