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