this repo has no description
13
fork

Configure Feed

Select the types of activity you want to include in your feed.

at 8a9c2d5e1b3778f1ea43c9bd5d325cfa72016584 1304 lines 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}