this repo has no description
13
fork

Configure Feed

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

refactor(parser): make parser easier to read and more robust

Refactor the parser to be more robust and easier to read.

+592 -692
+592 -692
src/Parser.zig
··· 18 18 n: usize, 19 19 }; 20 20 21 - // an intermediate data structure to hold sequence data while we are 22 - // scanning more bytes. This is tailored for input parsing only 23 - const Sequence = struct { 24 - // private indicators are 0x3C-0x3F 25 - private_indicator: ?u8 = null, 26 - // we won't be handling any sequences with more than one intermediate 27 - intermediate: ?u8 = null, 28 - // we should absolutely never have more then 16 params 29 - params: [16]u16 = undefined, 30 - param_idx: usize = 0, 31 - param_buf: [8]u8 = undefined, 32 - param_buf_idx: usize = 0, 33 - sub_state: std.StaticBitSet(16) = std.StaticBitSet(16).initEmpty(), 34 - empty_state: std.StaticBitSet(16) = std.StaticBitSet(16).initEmpty(), 35 - }; 36 - 37 21 const mouse_bits = struct { 38 22 const motion: u8 = 0b00100000; 39 23 const buttons: u8 = 0b11000011; ··· 62 46 63 47 grapheme_data: *const grapheme.GraphemeData, 64 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 65 54 pub fn parse(self: *Parser, input: []const u8, paste_allocator: ?std.mem.Allocator) !Result { 66 - const n = input.len; 55 + std.debug.assert(input.len > 0); 67 56 68 - var seq: Sequence = .{}; 69 - 70 - var state: State = .ground; 71 - 72 - var i: usize = 0; 73 - var start: usize = 0; 74 - // parse the read into events. This parser is bespoke for input parsing 75 - // and is not suitable for reuse as a generic vt parser 76 - while (i < n) : (i += 1) { 77 - const b = input[i]; 78 - switch (state) { 79 - .ground => { 80 - // ground state generates keypresses when parsing input. We 81 - // generally get ascii characters, but anything less than 82 - // 0x20 is a Ctrl+<c> keypress. We map these to lowercase 83 - // ascii characters when we can 84 - const key: Key = switch (b) { 85 - 0x00 => .{ .codepoint = '@', .mods = .{ .ctrl = true } }, 86 - 0x08 => .{ .codepoint = Key.backspace }, 87 - 0x09 => .{ .codepoint = Key.tab }, 88 - 0x0A, 89 - 0x0D, 90 - => .{ .codepoint = Key.enter }, 91 - 0x01...0x07, 92 - 0x0B...0x0C, 93 - 0x0E...0x1A, 94 - => .{ .codepoint = b + 0x60, .mods = .{ .ctrl = true } }, 95 - 0x1B => escape: { 96 - // NOTE: This could be an errant escape at the end 97 - // of a large read. That is _incredibly_ unlikely 98 - // given the size of read inputs and our read buffer 99 - if (i == (n - 1)) { 100 - const event = Key{ 101 - .codepoint = Key.escape, 102 - }; 103 - break :escape event; 104 - } 105 - state = .escape; 106 - continue; 107 - }, 108 - 0x7F => .{ .codepoint = Key.backspace }, 109 - else => blk: { 110 - var iter: code_point.Iterator = .{ .bytes = input[i..] }; 111 - // return null if we don't have a valid codepoint 112 - var cp = iter.next() orelse return .{ .event = null, .n = 0 }; 113 - 114 - var code = cp.code; 115 - i += cp.len - 1; // subtract one for the loop iter 116 - var g_state: grapheme.State = .{}; 117 - while (iter.next()) |next_cp| { 118 - if (grapheme.graphemeBreak(cp.code, next_cp.code, self.grapheme_data, &g_state)) { 119 - break; 120 - } 121 - code = Key.multicodepoint; 122 - i += next_cp.len; 123 - cp = next_cp; 124 - } 125 - 126 - break :blk .{ .codepoint = code, .text = input[start .. i + 1] }; 127 - }, 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 }, 128 72 }; 129 73 return .{ 130 74 .event = .{ .key_press = key }, 131 - .n = i + 1, 75 + .n = 2, 132 76 }; 133 77 }, 134 - .escape => { 135 - seq = .{}; 136 - start = i; 137 - switch (b) { 138 - 0x4F => state = .ss3, 139 - 0x50 => state = .dcs, 140 - 0x58 => state = .sos, 141 - 0x5B => state = .csi, 142 - 0x5D => state = .osc, 143 - 0x5E => state = .pm, 144 - 0x5F => state = .apc, 145 - else => { 146 - // Anything else is an "alt + <b>" keypress 147 - const key: Key = .{ 148 - .codepoint = b, 149 - .mods = .{ .alt = true }, 150 - }; 151 - return .{ 152 - .event = .{ .key_press = key }, 153 - .n = i + 1, 154 - }; 155 - }, 78 + } 79 + } else return parseGround(input, self.grapheme_data); 80 + } 81 + 82 + /// Parse ground state 83 + inline 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; 156 124 } 157 - }, 158 - .ss3 => { 159 - const key: Key = switch (b) { 160 - 'A' => .{ .codepoint = Key.up }, 161 - 'B' => .{ .codepoint = Key.down }, 162 - 'C' => .{ .codepoint = Key.right }, 163 - 'D' => .{ .codepoint = Key.left }, 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}", .{b}); 172 - return .{ 173 - .event = null, 174 - .n = i + 1, 175 - }; 176 - }, 177 - }; 178 - return .{ 179 - .event = .{ .key_press = key }, 180 - .n = i + 1, 181 - }; 182 - }, 183 - .csi => { 184 - switch (b) { 185 - // c0 controls. we ignore these even though we should 186 - // "execute" them. This isn't seen in practice 187 - 0x00...0x1F => {}, 188 - // intermediates. we only handle one. technically there 189 - // can be more 190 - 0x20...0x2F => seq.intermediate = b, 191 - 0x30...0x39 => { 192 - seq.param_buf[seq.param_buf_idx] = b; 193 - seq.param_buf_idx += 1; 194 - }, 195 - // private indicators. These come before any params ('?') 196 - 0x3C...0x3F => seq.private_indicator = b, 197 - ';' => { 198 - if (seq.param_buf_idx == 0) { 199 - // empty param. default it to 0 and set the 200 - // empty state 201 - seq.params[seq.param_idx] = 0; 202 - seq.empty_state.set(seq.param_idx); 203 - seq.param_idx += 1; 204 - } else { 205 - const p = try std.fmt.parseUnsigned(u16, seq.param_buf[0..seq.param_buf_idx], 10); 206 - seq.param_buf_idx = 0; 207 - seq.params[seq.param_idx] = p; 208 - seq.param_idx += 1; 209 - } 210 - }, 211 - ':' => { 212 - if (seq.param_buf_idx == 0) { 213 - // empty param. default it to 0 and set the 214 - // empty state 215 - seq.params[seq.param_idx] = 0; 216 - seq.empty_state.set(seq.param_idx); 217 - seq.param_idx += 1; 218 - // Set the *next* param as a subparam 219 - seq.sub_state.set(seq.param_idx); 220 - } else { 221 - const p = try std.fmt.parseUnsigned(u16, seq.param_buf[0..seq.param_buf_idx], 10); 222 - seq.param_buf_idx = 0; 223 - seq.params[seq.param_idx] = p; 224 - seq.param_idx += 1; 225 - // Set the *next* param as a subparam 226 - seq.sub_state.set(seq.param_idx); 227 - } 228 - }, 229 - 0x40...0xFF => { 230 - if (seq.param_buf_idx > 0) { 231 - const p = try std.fmt.parseUnsigned(u16, seq.param_buf[0..seq.param_buf_idx], 10); 232 - seq.param_buf_idx = 0; 233 - seq.params[seq.param_idx] = p; 234 - seq.param_idx += 1; 235 - } 236 - // dispatch the sequence 237 - state = .ground; 238 - const codepoint: u21 = switch (b) { 239 - 'A' => Key.up, 240 - 'B' => Key.down, 241 - 'C' => Key.right, 242 - 'D' => Key.left, 243 - 'E' => Key.kp_begin, 244 - 'F' => Key.end, 245 - 'H' => Key.home, 246 - 'M', 'm' => { // mouse event 247 - const priv = seq.private_indicator orelse { 248 - log.warn("unhandled csi: CSI {s}", .{input[start + 1 .. i + 1]}); 249 - return .{ .event = null, .n = i + 1 }; 250 - }; 251 - if (priv != '<') { 252 - log.warn("unhandled csi: CSI {s}", .{input[start + 1 .. i + 1]}); 253 - return .{ .event = null, .n = i + 1 }; 254 - } 255 - if (seq.param_idx != 3) { 256 - log.warn("unhandled csi: CSI {s}", .{input[start + 1 .. i + 1]}); 257 - return .{ .event = null, .n = i + 1 }; 258 - } 259 - const button: Mouse.Button = @enumFromInt(seq.params[0] & mouse_bits.buttons); 260 - const motion = seq.params[0] & mouse_bits.motion > 0; 261 - const shift = seq.params[0] & mouse_bits.shift > 0; 262 - const alt = seq.params[0] & mouse_bits.alt > 0; 263 - const ctrl = seq.params[0] & mouse_bits.ctrl > 0; 264 - const col: usize = if (seq.params[1] > 0) seq.params[1] - 1 else 0; 265 - const row: usize = if (seq.params[2] > 0) seq.params[2] - 1 else 0; 125 + prev_cp = next_cp.code; 126 + code = Key.multicodepoint; 127 + n += next_cp.len; 128 + } 266 129 267 - const mouse = Mouse{ 268 - .button = button, 269 - .mods = .{ 270 - .shift = shift, 271 - .alt = alt, 272 - .ctrl = ctrl, 273 - }, 274 - .col = col, 275 - .row = row, 276 - .type = blk: { 277 - if (motion and button != Mouse.Button.none) { 278 - break :blk .drag; 279 - } 280 - if (motion and button == Mouse.Button.none) { 281 - break :blk .motion; 282 - } 283 - if (b == 'm') break :blk .release; 284 - break :blk .press; 285 - }, 286 - }; 287 - return .{ .event = .{ .mouse = mouse }, .n = i + 1 }; 288 - }, 289 - 'P' => Key.f1, 290 - 'Q' => Key.f2, 291 - 'R' => Key.f3, 292 - 'S' => Key.f4, 293 - '~' => blk: { 294 - // The first param will define this 295 - // codepoint 296 - if (seq.param_idx < 1) { 297 - log.warn("unhandled csi: CSI {s}", .{input[start + 1 .. i + 1]}); 298 - return .{ 299 - .event = null, 300 - .n = i + 1, 301 - }; 302 - } 303 - switch (seq.params[0]) { 304 - 2 => break :blk Key.insert, 305 - 3 => break :blk Key.delete, 306 - 5 => break :blk Key.page_up, 307 - 6 => break :blk Key.page_down, 308 - 7 => break :blk Key.home, 309 - 8 => break :blk Key.end, 310 - 11 => break :blk Key.f1, 311 - 12 => break :blk Key.f2, 312 - 13 => break :blk Key.f3, 313 - 14 => break :blk Key.f4, 314 - 15 => break :blk Key.f5, 315 - 17 => break :blk Key.f6, 316 - 18 => break :blk Key.f7, 317 - 19 => break :blk Key.f8, 318 - 20 => break :blk Key.f9, 319 - 21 => break :blk Key.f10, 320 - 23 => break :blk Key.f11, 321 - 24 => break :blk Key.f12, 322 - 200 => { 323 - return .{ 324 - .event = .paste_start, 325 - .n = i + 1, 326 - }; 327 - }, 328 - 201 => { 329 - return .{ 330 - .event = .paste_end, 331 - .n = i + 1, 332 - }; 333 - }, 334 - 57427 => break :blk Key.kp_begin, 335 - else => { 336 - log.warn("unhandled csi: CSI {s}", .{input[start + 1 .. i + 1]}); 337 - return .{ 338 - .event = null, 339 - .n = i + 1, 340 - }; 341 - }, 342 - } 343 - }, 344 - 'n' => { 345 - switch (seq.params[0]) { 346 - 5 => { 347 - // "Ok" response 348 - return .{ 349 - .event = null, 350 - .n = i + 1, 351 - }; 352 - }, 353 - 997 => { 354 - switch (seq.params[1]) { 355 - 1 => { 356 - return .{ 357 - .event = .{ .color_scheme = .dark }, 358 - .n = i + 1, 359 - }; 360 - }, 361 - 2 => { 362 - return .{ 363 - .event = .{ .color_scheme = .dark }, 364 - .n = i + 1, 365 - }; 366 - }, 367 - else => { 368 - log.warn("unhandled csi: CSI {s}", .{input[start + 1 .. i + 1]}); 369 - return .{ 370 - .event = null, 371 - .n = i + 1, 372 - }; 373 - }, 374 - } 375 - }, 376 - else => { 377 - log.warn("unhandled csi: CSI {s}", .{input[start + 1 .. i + 1]}); 378 - return .{ 379 - .event = null, 380 - .n = i + 1, 381 - }; 382 - }, 383 - } 384 - }, 385 - 'u' => blk: { 386 - if (seq.private_indicator) |priv| { 387 - // response to our kitty query 388 - if (priv == '?') { 389 - return .{ 390 - .event = .cap_kitty_keyboard, 391 - .n = i + 1, 392 - }; 393 - } else { 394 - log.warn("unhandled csi: CSI {s}", .{input[start + 1 .. i + 1]}); 395 - return .{ 396 - .event = null, 397 - .n = i + 1, 398 - }; 399 - } 400 - } 401 - if (seq.param_idx == 0) { 402 - log.warn("unhandled csi: CSI {s}", .{input[start + 1 .. i + 1]}); 403 - return .{ 404 - .event = null, 405 - .n = i + 1, 406 - }; 407 - } 408 - // In any csi u encoding, the codepoint 409 - // directly maps to our keypoint definitions 410 - break :blk seq.params[0]; 411 - }, 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 + 140 + inline 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 + 168 + inline 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]; 412 175 413 - 'I' => { // focus in 414 - return .{ .event = .focus_in, .n = i + 1 }; 415 - }, 416 - 'O' => { // focus out 417 - return .{ .event = .focus_out, .n = i + 1 }; 418 - }, 419 - 'y' => { // DECRQM response 420 - const priv = seq.private_indicator orelse { 421 - log.warn("unhandled csi: CSI {s}", .{input[start + 1 .. i + 1]}); 422 - return .{ .event = null, .n = i + 1 }; 423 - }; 424 - if (priv != '?') { 425 - log.warn("unhandled csi: CSI {s}", .{input[start + 1 .. i + 1]}); 426 - return .{ .event = null, .n = i + 1 }; 427 - } 428 - const intm = seq.intermediate orelse { 429 - log.warn("unhandled csi: CSI {s}", .{input[start + 1 .. i + 1]}); 430 - return .{ .event = null, .n = i + 1 }; 431 - }; 432 - if (intm != '$') { 433 - log.warn("unhandled csi: CSI {s}", .{input[start + 1 .. i + 1]}); 434 - return .{ .event = null, .n = i + 1 }; 435 - } 436 - if (seq.param_idx != 2) { 437 - log.warn("unhandled csi: CSI {s}", .{input[start + 1 .. i + 1]}); 438 - return .{ .event = null, .n = i + 1 }; 439 - } 440 - // We'll get two fields, the first is the mode 441 - // we requested, the second is the status of the 442 - // mode 443 - // 0: not recognize 444 - // 1: set 445 - // 2: reset 446 - // 3: permanently set 447 - // 4: permanently reset 448 - switch (seq.params[0]) { 449 - 1016 => { 450 - switch (seq.params[1]) { 451 - 0, 4 => return .{ .event = null, .n = i + 1 }, 452 - else => return .{ .event = .cap_sgr_pixels, .n = i + 1 }, 453 - } 454 - }, 455 - 2027 => { 456 - switch (seq.params[1]) { 457 - 0, 4 => return .{ .event = null, .n = i + 1 }, 458 - else => return .{ .event = .cap_unicode, .n = i + 1 }, 459 - } 460 - }, 461 - 2031 => { 462 - switch (seq.params[1]) { 463 - 0, 4 => return .{ .event = null, .n = i + 1 }, 464 - else => return .{ .event = .cap_color_scheme_updates, .n = i + 1 }, 465 - } 466 - }, 467 - else => { 468 - log.warn("unhandled DECRPM: CSI {s}", .{input[start + 1 .. i + 1]}); 469 - return .{ .event = null, .n = i + 1 }; 470 - }, 471 - } 472 - log.warn("unhandled csi: CSI {s}", .{input[start + 1 .. i + 1]}); 473 - return .{ .event = null, .n = i + 1 }; 474 - }, 475 - 'c' => { // DA1 response 476 - const priv = seq.private_indicator orelse { 477 - log.warn("unhandled csi: CSI {s}", .{input[start + 1 .. i + 1]}); 478 - return .{ .event = null, .n = i + 1 }; 479 - }; 480 - if (priv != '?') { 481 - log.warn("unhandled csi: CSI {s}", .{input[start + 1 .. i + 1]}); 482 - return .{ .event = null, .n = i + 1 }; 483 - } 484 - return .{ .event = .cap_da1, .n = i + 1 }; 485 - }, 486 - else => { 487 - log.warn("unhandled csi: CSI {s}", .{input[start + 1 .. i + 1]}); 488 - return .{ 489 - .event = null, 490 - .n = i + 1, 491 - }; 492 - }, 493 - }; 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 + } 494 187 495 - var key: Key = .{ .codepoint = codepoint }; 496 - var is_release: bool = false; 188 + /// Skips sequences until we see an ST (String Terminator, ESC \) 189 + inline 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 + } 497 201 498 - var idx: usize = 0; 499 - var field: u8 = 0; 500 - // parse the parameters 501 - while (idx < seq.param_idx) : (idx += 1) { 502 - switch (field) { 503 - 0 => { 504 - defer field += 1; 505 - // field 0 contains our codepoint. Any 506 - // subparameters shifted key code and 507 - // alternate keycode (csi u encoding) 202 + /// Parses an OSC sequence 203 + inline 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; 508 210 509 - // We already handled our codepoint so 510 - // we just need to check for subs 511 - if (!seq.sub_state.isSet(idx + 1)) { 512 - continue; 513 - } 514 - idx += 1; 515 - // The first one is a shifted code if it 516 - // isn't empty 517 - if (!seq.empty_state.isSet(idx)) { 518 - key.shifted_codepoint = seq.params[idx]; 519 - } 520 - // check the next one for base layout 521 - // code 522 - if (!seq.sub_state.isSet(idx + 1)) { 523 - continue; 524 - } 525 - idx += 1; 526 - key.base_layout_codepoint = seq.params[idx]; 527 - }, 528 - 1 => { 529 - defer field += 1; 530 - // field 1 is modifiers and optionally 531 - // the event type (csiu). It can be empty 532 - if (seq.empty_state.isSet(idx)) { 533 - continue; 534 - } 535 - // default of 1 536 - const ps: u8 = blk: { 537 - if (seq.params[idx] == 0) break :blk 1; 538 - break :blk @truncate(seq.params[idx]); 539 - }; 540 - key.mods = @bitCast(ps - 1); 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 + }; 541 219 542 - // check if an event type exists 543 - if (!seq.sub_state.isSet(idx + 1)) { 544 - continue; 545 - } 546 - idx += 1; 547 - if (seq.params[idx] == 3) is_release = true; 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 + 290 + inline 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 + switch (b) { 294 + 0x40...0xFF => break input[0 .. i + 1], 295 + else => continue, 296 + } 297 + } else return .{ .event = null, .n = 0 }; 298 + 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 {ABCDEFHPQS} 307 + 308 + const modifiers: Key.Modifiers = if (sequence.len > 3) mods: { 309 + // ESC [ 1 ; <modifier_buf> {ABCDEFHPQS} 310 + const modifier_buf = sequence[4 .. sequence.len - 1]; 311 + const modifiers = parseParam(u8, modifier_buf, 1) orelse return null_event; 312 + break :mods @bitCast(modifiers -| 1); 313 + } else .{}; 314 + 315 + const key: Key = .{ 316 + .mods = modifiers, 317 + .codepoint = switch (final) { 318 + 'A' => Key.up, 319 + 'B' => Key.down, 320 + 'C' => Key.right, 321 + 'D' => Key.left, 322 + 'E' => Key.kp_begin, 323 + 'F' => Key.end, 324 + 'H' => Key.home, 325 + 'P' => Key.f1, 326 + 'Q' => Key.f2, 327 + 'R' => Key.f3, 328 + 'S' => Key.f4, 329 + else => return null_event, 330 + }, 331 + }; 332 + return .{ 333 + .event = .{ .key_press = key }, 334 + .n = sequence.len, 335 + }; 336 + }, 337 + '~' => { 338 + // Legacy keys 339 + // CSI number ~ 340 + // CSI number ; modifier ~ 341 + var field_iter = std.mem.splitScalar(u8, sequence[2 .. sequence.len - 1], ';'); 342 + const number_buf = field_iter.next() orelse unreachable; // always will have one field 343 + const number = parseParam(u16, number_buf, null) orelse return null_event; 344 + 345 + const key_code = switch (number) { 346 + 2 => Key.insert, 347 + 3 => Key.delete, 348 + 5 => Key.page_up, 349 + 6 => Key.page_down, 350 + 7 => Key.home, 351 + 8 => Key.end, 352 + 11 => Key.f1, 353 + 12 => Key.f2, 354 + 13 => Key.f3, 355 + 14 => Key.f4, 356 + 15 => Key.f5, 357 + 17 => Key.f6, 358 + 18 => Key.f7, 359 + 19 => Key.f8, 360 + 20 => Key.f9, 361 + 21 => Key.f10, 362 + 23 => Key.f11, 363 + 24 => Key.f12, 364 + 200 => return .{ .event = .paste_start, .n = sequence.len }, 365 + 201 => return .{ .event = .paste_end, .n = sequence.len }, 366 + 57427 => Key.kp_begin, 367 + else => return null_event, 368 + }; 369 + 370 + const modifiers: Key.Modifiers = if (field_iter.next()) |modifier_buf| mods: { 371 + const modifiers = parseParam(u8, modifier_buf, 1) orelse return null_event; 372 + break :mods @bitCast(modifiers -| 1); 373 + } else .{}; 374 + 375 + const key: Key = .{ 376 + .codepoint = key_code, 377 + .mods = modifiers, 378 + }; 379 + 380 + return .{ 381 + .event = .{ .key_press = key }, 382 + .n = sequence.len, 383 + }; 384 + }, 385 + 386 + 'I' => return .{ .event = .focus_in, .n = sequence.len }, 387 + 'O' => return .{ .event = .focus_out, .n = sequence.len }, 388 + 'M', 'm' => return parseMouse(sequence), 389 + 'c' => { 390 + // Primary DA (CSI ? Pm c) 391 + std.debug.assert(sequence.len >= 4); // ESC [ ? c == 4 bytes 392 + switch (input[2]) { 393 + '?' => return .{ .event = .cap_da1, .n = sequence.len }, 394 + else => return null_event, 395 + } 396 + }, 397 + 'n' => { 398 + // Device Status Report 399 + // CSI Ps n 400 + // CSI ? Ps n 401 + std.debug.assert(sequence.len >= 3); 402 + switch (sequence[2]) { 403 + '?' => { 404 + const delim_idx = std.mem.indexOfScalarPos(u8, input, 3, ';') orelse return null_event; 405 + const ps = std.fmt.parseUnsigned(u16, input[3..delim_idx], 10) catch return null_event; 406 + switch (ps) { 407 + 997 => { 408 + // Color scheme update (CSI 997 ; Ps n) 409 + // See https://github.com/contour-terminal/contour/blob/master/docs/vt-extensions/color-palette-update-notifications.md 410 + switch (sequence[delim_idx + 1]) { 411 + '1' => return .{ 412 + .event = .{ .color_scheme = .dark }, 413 + .n = sequence.len, 548 414 }, 549 - 2 => { 550 - // field 2 is text, as codepoints 551 - var total: usize = 0; 552 - while (idx < seq.param_idx) : (idx += 1) { 553 - total += try std.unicode.utf8Encode(seq.params[idx], self.buf[total..]); 554 - } 555 - key.text = self.buf[0..total]; 415 + '2' => return .{ 416 + .event = .{ .color_scheme = .light }, 417 + .n = sequence.len, 556 418 }, 557 - else => {}, 419 + else => return null_event, 558 420 } 559 - } 560 - const event: Event = if (is_release) 561 - .{ .key_release = key } 562 - else 563 - .{ .key_press = key }; 564 - return .{ 565 - .event = event, 566 - .n = i + 1, 567 - }; 568 - }, 569 - } 570 - }, 571 - .apc => { 572 - switch (b) { 573 - 0x1B => { 574 - state = .ground; 575 - // advance one more for the backslash 576 - i += 1; 577 - switch (input[start + 1]) { 578 - 'G' => { 579 - return .{ 580 - .event = .cap_kitty_graphics, 581 - .n = i + 1, 582 - }; 583 - }, 584 - else => { 585 - log.warn("unhandled apc: APC {s}", .{input[start + 1 .. i + 1]}); 586 - return .{ 587 - .event = null, 588 - .n = i + 1, 589 - }; 590 - }, 591 - } 592 - }, 593 - else => {}, 421 + }, 422 + else => return null_event, 423 + } 424 + }, 425 + else => return null_event, 426 + } 427 + }, 428 + 'u' => { 429 + // Kitty keyboard 430 + // CSI unicode-key-code:alternate-key-codes ; modifiers:event-type ; text-as-codepoints u 431 + // Not all fields will be present. Only unicode-key-code is 432 + // mandatory 433 + 434 + var key: Key = .{ 435 + .codepoint = undefined, 436 + }; 437 + // Split first into fields delimited by ';' 438 + var field_iter = std.mem.splitScalar(u8, sequence[2 .. sequence.len - 1], ';'); 439 + 440 + { // field 1 441 + // unicode-key-code:shifted_codepoint:base_layout_codepoint 442 + const field_buf = field_iter.next() orelse unreachable; // There will always be at least one field 443 + var param_iter = std.mem.splitScalar(u8, field_buf, ':'); 444 + const codepoint_buf = param_iter.next() orelse unreachable; 445 + key.codepoint = parseParam(u21, codepoint_buf, null) orelse return null_event; 446 + 447 + if (param_iter.next()) |shifted_cp_buf| { 448 + key.shifted_codepoint = parseParam(u21, shifted_cp_buf, null); 594 449 } 595 - }, 596 - .sos, .pm => { 597 - switch (b) { 598 - 0x1B => { 599 - state = .ground; 600 - // advance one more for the backslash 601 - i += 1; 602 - log.warn("unhandled sos/pm: SOS/PM {s}", .{input[start + 1 .. i + 1]}); 603 - return .{ 604 - .event = null, 605 - .n = i + 1, 606 - }; 607 - }, 608 - else => {}, 450 + if (param_iter.next()) |base_layout_buf| { 451 + key.base_layout_codepoint = parseParam(u21, base_layout_buf, null); 609 452 } 610 - }, 611 - .osc => { 612 - switch (b) { 613 - 0x07, 0x1B => { 614 - state = .ground; 615 - if (b == 0x1b) { 616 - // advance one more for the backslash 617 - i += 1; 618 - } 619 - log.warn("unhandled osc: OSC {s}", .{input[start + 1 .. i + 1]}); 620 - return .{ 621 - .event = null, 622 - .n = i + 1, 623 - }; 624 - }, 625 - 0x30...0x39 => { 626 - seq.param_buf[seq.param_buf_idx] = b; 627 - seq.param_buf_idx += 1; 628 - }, 629 - ';' => { 630 - if (seq.param_buf_idx == 0) { 631 - seq.param_idx += 1; 632 - } 633 - if (seq.param_idx == 0) { 634 - const p = try std.fmt.parseUnsigned(u16, seq.param_buf[0..seq.param_buf_idx], 10); 635 - seq.param_buf_idx = 0; 636 - seq.param_idx += 1; 637 - switch (p) { 638 - 4, 639 - 10, 640 - 11, 641 - 12, 642 - => { 643 - i += 1; 644 - const index: ?u8 = if (p == 4) blk: { 645 - const index_start = i; 646 - const end: usize = while (i < n) : (i += 1) { 647 - if (input[i] == ';') { 648 - i += 1; 649 - break i - 1; 650 - } 651 - } else unreachable; // invalid input 652 - break :blk try std.fmt.parseUnsigned(u8, input[index_start..end], 10); 653 - } else null; 654 - const spec_start = i; 655 - const end: usize = while (i < n) : (i += 1) { 656 - if (input[i] == 0x1B) { 657 - // advance one more for the backslash 658 - i += 1; 659 - break i - 1; 660 - } 661 - } else return .{ 662 - .event = null, 663 - .n = i, 664 - }; 665 - const color = try Color.rgbFromSpec(input[spec_start..end]); 453 + } 454 + 455 + var is_release: bool = false; 456 + 457 + field2: { 458 + // modifier_mask:event_type 459 + const field_buf = field_iter.next() orelse break :field2; 460 + var param_iter = std.mem.splitScalar(u8, field_buf, ':'); 461 + const modifier_buf = param_iter.next() orelse unreachable; 462 + const modifier_mask = parseParam(u8, modifier_buf, 1) orelse return null_event; 463 + key.mods = @bitCast(modifier_mask -| 1); 666 464 667 - const event: Color.Report = .{ 668 - .kind = switch (p) { 669 - 4 => .{ .index = index.? }, 670 - 10 => .fg, 671 - 11 => .bg, 672 - 12 => .cursor, 673 - else => unreachable, 674 - }, 675 - .value = color.rgb, 676 - }; 677 - return .{ 678 - .event = .{ .color_report = event }, 679 - .n = i, 680 - }; 681 - }, 682 - 52 => { 683 - var payload: ?std.ArrayList(u8) = if (paste_allocator) |allocator| 684 - std.ArrayList(u8).init(allocator) 685 - else 686 - null; 687 - defer if (payload) |_| payload.?.deinit(); 465 + if (param_iter.next()) |event_type_buf| { 466 + is_release = std.mem.eql(u8, event_type_buf, "3"); 467 + } 468 + } 688 469 689 - while (i < n) : (i += 1) { 690 - const b_ = input[i]; 691 - switch (b_) { 692 - ';' => { 693 - if (seq.param_buf_idx == 0) { 694 - // empty param. default it to 0 and set the 695 - // empty state 696 - seq.params[seq.param_idx] = 0; 697 - seq.empty_state.set(seq.param_idx); 698 - seq.param_idx += 1; 699 - } else { 700 - seq.params[seq.param_idx] = @intCast(b_); 701 - seq.param_buf_idx = 0; 702 - seq.param_idx += 1; 703 - } 704 - }, 705 - 0x07, 0x1B => { 706 - state = .ground; 707 - if (b == 0x1b) { 708 - // advance one more for the backslash 709 - i += 1; 710 - } 711 - if (payload) |_| { 712 - log.debug("decoding paste: {s}", .{payload.?.items}); 713 - const decoder = std.base64.standard.Decoder; 714 - const text = try paste_allocator.?.alloc(u8, try decoder.calcSizeForSlice(payload.?.items)); 715 - try decoder.decode(text, payload.?.items); 716 - log.debug("decoded paste: {s}", .{text}); 717 - return .{ 718 - .event = .{ .paste = text }, 719 - .n = i + 2, 720 - }; 721 - } else return .{ 722 - .event = null, 723 - .n = i + 2, 724 - }; 725 - }, 726 - else => if (seq.param_idx == 3 and payload != null) try payload.?.append(b_), 727 - } 728 - } 729 - }, 730 - else => {}, 731 - } 732 - } 733 - }, 734 - else => {}, 470 + field3: { 471 + // text_as_codepoint[:text_as_codepoint] 472 + const field_buf = field_iter.next() orelse break :field3; 473 + var param_iter = std.mem.splitScalar(u8, field_buf, ':'); 474 + var total: usize = 0; 475 + while (param_iter.next()) |cp_buf| { 476 + const cp = parseParam(u21, cp_buf, null) orelse return null_event; 477 + total += std.unicode.utf8Encode(cp, text_buf[total..]) catch return null_event; 735 478 } 736 - }, 737 - else => {}, 738 - } 479 + key.text = text_buf[0..total]; 480 + } 481 + 482 + const event: Event = if (is_release) 483 + .{ .key_release = key } 484 + else 485 + .{ .key_press = key }; 486 + 487 + return .{ .event = event, .n = sequence.len }; 488 + }, 489 + 'y' => { 490 + // DECRPM (CSI Ps ; Pm y) 491 + const delim_idx = std.mem.indexOfScalarPos(u8, input, 2, ';') orelse return null_event; 492 + const ps = std.fmt.parseUnsigned(u16, input[2..delim_idx], 10) catch return null_event; 493 + const pm = std.fmt.parseUnsigned(u8, input[delim_idx + 1 .. sequence.len - 1], 10) catch return null_event; 494 + switch (ps) { 495 + // Mouse Pixel reporting 496 + 1016 => switch (pm) { 497 + 0, 4 => return null_event, 498 + else => return .{ .event = .cap_sgr_pixels, .n = sequence.len }, 499 + }, 500 + // Unicode Core, see https://github.com/contour-terminal/terminal-unicode-core 501 + 2027 => switch (pm) { 502 + 0, 4 => return null_event, 503 + else => return .{ .event = .cap_unicode, .n = sequence.len }, 504 + }, 505 + // Color scheme reportnig, see https://github.com/contour-terminal/contour/blob/master/docs/vt-extensions/color-palette-update-notifications.md 506 + 2031 => switch (pm) { 507 + 0, 4 => return null_event, 508 + else => return .{ .event = .cap_color_scheme_updates, .n = sequence.len }, 509 + }, 510 + else => return null_event, 511 + } 512 + }, 513 + else => return null_event, 739 514 } 740 - // If we get here it means we didn't parse an event. The input buffer 741 - // perhaps didn't include a full event 742 - return .{ 743 - .event = null, 744 - .n = 0, 515 + } 516 + 517 + /// Parse a param buffer, returning a default value if the param was empty 518 + inline fn parseParam(comptime T: type, buf: []const u8, default: ?T) ?T { 519 + if (buf.len == 0) return default; 520 + return std.fmt.parseUnsigned(T, buf, 10) catch return null; 521 + } 522 + 523 + /// Parse a mouse event 524 + inline fn parseMouse(input: []const u8) Result { 525 + std.debug.assert(input.len >= 4); // ESC [ < [Mm] 526 + const null_event: Result = .{ .event = null, .n = input.len }; 527 + 528 + if (input[2] != '<') return null_event; 529 + 530 + const delim1 = std.mem.indexOfScalarPos(u8, input, 3, ';') orelse return null_event; 531 + const button_mask = parseParam(u16, input[3..delim1], null) orelse return null_event; 532 + const delim2 = std.mem.indexOfScalarPos(u8, input, delim1 + 1, ';') orelse return null_event; 533 + const px = parseParam(u16, input[delim1 + 1 .. delim2], 1) orelse return null_event; 534 + const py = parseParam(u16, input[delim2 + 1 .. input.len - 1], 1) orelse return null_event; 535 + 536 + const button: Mouse.Button = @enumFromInt(button_mask & mouse_bits.buttons); 537 + const motion = button_mask & mouse_bits.motion > 0; 538 + const shift = button_mask & mouse_bits.shift > 0; 539 + const alt = button_mask & mouse_bits.alt > 0; 540 + const ctrl = button_mask & mouse_bits.ctrl > 0; 541 + 542 + const mouse = Mouse{ 543 + .button = button, 544 + .mods = .{ 545 + .shift = shift, 546 + .alt = alt, 547 + .ctrl = ctrl, 548 + }, 549 + .col = px -| 1, 550 + .row = py -| 1, 551 + .type = blk: { 552 + if (motion and button != Mouse.Button.none) { 553 + break :blk .drag; 554 + } 555 + if (motion and button == Mouse.Button.none) { 556 + break :blk .motion; 557 + } 558 + if (input[input.len - 1] == 'm') break :blk .release; 559 + break :blk .press; 560 + }, 745 561 }; 562 + return .{ .event = .{ .mouse = mouse }, .n = input.len }; 746 563 } 747 564 748 565 test "parse: single xterm keypress" { ··· 838 655 try testing.expectEqual(expected_event, result.event); 839 656 } 840 657 841 - test "parse: xterm invalid ss3" { 842 - const alloc = testing.allocator_instance.allocator(); 843 - const grapheme_data = try grapheme.GraphemeData.init(alloc); 844 - defer grapheme_data.deinit(); 845 - const input = "\x1bOZ"; 846 - var parser: Parser = .{ .grapheme_data = &grapheme_data }; 847 - const result = try parser.parse(input, alloc); 848 - 849 - try testing.expectEqual(3, result.n); 850 - try testing.expectEqual(null, result.event); 851 - } 852 - 853 658 test "parse: xterm key up" { 854 659 const alloc = testing.allocator_instance.allocator(); 855 660 const grapheme_data = try grapheme.GraphemeData.init(alloc); 856 661 defer grapheme_data.deinit(); 857 662 { 858 663 // normal version 859 - const input = "\x1bOA"; 664 + const input = "\x1b[A"; 860 665 var parser: Parser = .{ .grapheme_data = &grapheme_data }; 861 666 const result = try parser.parse(input, alloc); 862 667 const expected_key: Key = .{ .codepoint = Key.up }; ··· 868 673 869 674 { 870 675 // application keys version 871 - const input = "\x1b[2~"; 676 + const input = "\x1bOA"; 872 677 var parser: Parser = .{ .grapheme_data = &grapheme_data }; 873 678 const result = try parser.parse(input, alloc); 874 - const expected_key: Key = .{ .codepoint = Key.insert }; 679 + const expected_key: Key = .{ .codepoint = Key.up }; 875 680 const expected_event: Event = .{ .key_press = expected_key }; 876 681 877 - try testing.expectEqual(4, result.n); 682 + try testing.expectEqual(3, result.n); 878 683 try testing.expectEqual(expected_event, result.event); 879 684 } 880 685 } ··· 897 702 const alloc = testing.allocator_instance.allocator(); 898 703 const grapheme_data = try grapheme.GraphemeData.init(alloc); 899 704 defer grapheme_data.deinit(); 900 - const input = "\x1b[1;2A"; 705 + const input = "\x1b[2~"; 901 706 var parser: Parser = .{ .grapheme_data = &grapheme_data }; 902 707 const result = try parser.parse(input, alloc); 903 - const expected_key: Key = .{ .codepoint = Key.up, .mods = .{ .shift = true } }; 708 + const expected_key: Key = .{ .codepoint = Key.insert, .mods = .{} }; 904 709 const expected_event: Event = .{ .key_press = expected_key }; 905 710 906 - try testing.expectEqual(6, result.n); 711 + try testing.expectEqual(input.len, result.n); 907 712 try testing.expectEqual(expected_event, result.event); 908 713 } 909 714 ··· 1114 919 try testing.expectEqualStrings(expected_key.text.?, actual.text.?); 1115 920 try testing.expectEqual(expected_key.codepoint, actual.codepoint); 1116 921 } 922 + 923 + test "parse(csi): decrpm" { 924 + var buf: [1]u8 = undefined; 925 + { 926 + const input = "\x1b[1016;1y"; 927 + const result = parseCsi(input, &buf); 928 + const expected: Result = .{ 929 + .event = .cap_sgr_pixels, 930 + .n = input.len, 931 + }; 932 + 933 + try testing.expectEqual(expected.n, result.n); 934 + try testing.expectEqual(expected.event, result.event); 935 + } 936 + { 937 + const input = "\x1b[1016;0y"; 938 + const result = parseCsi(input, &buf); 939 + const expected: Result = .{ 940 + .event = null, 941 + .n = input.len, 942 + }; 943 + 944 + try testing.expectEqual(expected.n, result.n); 945 + try testing.expectEqual(expected.event, result.event); 946 + } 947 + } 948 + 949 + test "parse(csi): primary da" { 950 + var buf: [1]u8 = undefined; 951 + const input = "\x1b[?c"; 952 + const result = parseCsi(input, &buf); 953 + const expected: Result = .{ 954 + .event = .cap_da1, 955 + .n = input.len, 956 + }; 957 + 958 + try testing.expectEqual(expected.n, result.n); 959 + try testing.expectEqual(expected.event, result.event); 960 + } 961 + 962 + test "parse(csi): dsr" { 963 + var buf: [1]u8 = undefined; 964 + { 965 + const input = "\x1b[?997;1n"; 966 + const result = parseCsi(input, &buf); 967 + const expected: Result = .{ 968 + .event = .{ .color_scheme = .dark }, 969 + .n = input.len, 970 + }; 971 + 972 + try testing.expectEqual(expected.n, result.n); 973 + try testing.expectEqual(expected.event, result.event); 974 + } 975 + { 976 + const input = "\x1b[?997;2n"; 977 + const result = parseCsi(input, &buf); 978 + const expected: Result = .{ 979 + .event = .{ .color_scheme = .light }, 980 + .n = input.len, 981 + }; 982 + 983 + try testing.expectEqual(expected.n, result.n); 984 + try testing.expectEqual(expected.event, result.event); 985 + } 986 + { 987 + const input = "\x1b[0n"; 988 + const result = parseCsi(input, &buf); 989 + const expected: Result = .{ 990 + .event = null, 991 + .n = input.len, 992 + }; 993 + 994 + try testing.expectEqual(expected.n, result.n); 995 + try testing.expectEqual(expected.event, result.event); 996 + } 997 + } 998 + 999 + test "parse(csi): mouse" { 1000 + var buf: [1]u8 = undefined; 1001 + const input = "\x1b[<35;1;1m"; 1002 + const result = parseCsi(input, &buf); 1003 + const expected: Result = .{ 1004 + .event = .{ .mouse = .{ 1005 + .col = 0, 1006 + .row = 0, 1007 + .button = .none, 1008 + .type = .motion, 1009 + .mods = .{}, 1010 + } }, 1011 + .n = input.len, 1012 + }; 1013 + 1014 + try testing.expectEqual(expected.n, result.n); 1015 + try testing.expectEqual(expected.event, result.event); 1016 + }