this repo has no description
13
fork

Configure Feed

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

parser: refactor into testable function

Signed-off-by: Tim Culverhouse <tim@timculverhouse.com>

+375 -304
+11 -304
src/Tty.zig
··· 3 3 const os = std.os; 4 4 const vaxis = @import("main.zig"); 5 5 const Vaxis = vaxis.Vaxis; 6 + const Event = @import("event.zig").Event; 7 + const parser = @import("parser.zig"); 6 8 const Key = vaxis.Key; 7 9 8 10 const log = std.log.scoped(.tty); ··· 110 112 }; 111 113 try WinchHandler.init(vx, self.fd); 112 114 113 - // the state of the parser 114 - const State = enum { 115 - ground, 116 - escape, 117 - csi, 118 - osc, 119 - dcs, 120 - sos, 121 - pm, 122 - apc, 123 - ss2, 124 - ss3, 125 - }; 126 - 127 - var state: State = .ground; 128 - 129 - // an intermediate data structure to hold sequence data while we are 130 - // scanning more bytes. This is tailored for input parsing only 131 - const Sequence = struct { 132 - // private indicators are 0x3C-0x3F 133 - private_indicator: ?u8 = null, 134 - // we won't be handling any sequences with more than one intermediate 135 - intermediate: ?u8 = null, 136 - // we should absolutely never have more then 16 params 137 - params: [16]u16 = undefined, 138 - param_idx: usize = 0, 139 - param_buf: [8]u8 = undefined, 140 - param_buf_idx: usize = 0, 141 - sub_state: std.StaticBitSet(16) = std.StaticBitSet(16).initEmpty(), 142 - empty_state: std.StaticBitSet(16) = std.StaticBitSet(16).initEmpty(), 143 - }; 144 - 145 - var seq: Sequence = .{}; 146 - 147 115 // Set up fds for polling 148 116 var pollfds: [2]std.os.pollfd = .{ 149 117 .{ .fd = self.fd, .events = std.os.POLL.IN, .revents = undefined }, ··· 161 129 } 162 130 163 131 const n = try os.read(self.fd, &buf); 164 - var i: usize = 0; 165 132 var start: usize = 0; 166 - // parse the read into events. This parser is bespoke for input parsing 167 - // and is not suitable for reuse as a generic vt parser 168 - while (i < n) : (i += 1) { 169 - const b = buf[i]; 170 - switch (state) { 171 - .ground => { 172 - // ground state generates keypresses when parsing input. We 173 - // generally get ascii characters, but anything less than 174 - // 0x20 is a Ctrl+<c> keypress. We map these to lowercase 175 - // ascii characters when we can 176 - const key: Key = switch (b) { 177 - 0x00 => .{ .codepoint = '@', .mods = .{ .ctrl = true } }, 178 - 0x01...0x1A => .{ .codepoint = b + 0x60, .mods = .{ .ctrl = true } }, 179 - 0x1B => escape: { 180 - // NOTE: This could be an errant escape at the end 181 - // of a large read. That is _incredibly_ unlikely 182 - // given the size of read inputs and our read buffer 183 - if (i == (n - 1)) { 184 - const event = Key{ 185 - .codepoint = Key.escape, 186 - }; 187 - break :escape event; 188 - } 189 - state = .escape; 190 - continue; 191 - }, 192 - 0x20...0x7E => .{ .codepoint = b }, 193 - 0x7F => .{ .codepoint = Key.backspace }, 194 - // TODO: graphemes 195 - else => .{ .codepoint = b }, 196 - }; 197 - if (@hasField(EventType, "key_press")) { 198 - vx.postEvent(.{ .key_press = key }); 199 - } 200 - }, 201 - .escape => { 202 - seq = .{}; 203 - start = i; 204 - switch (b) { 205 - 0x4F => state = .ss3, 206 - 0x50 => state = .dcs, 207 - 0x58 => state = .sos, 208 - 0x5B => state = .csi, 209 - 0x5D => state = .osc, 210 - 0x5E => state = .pm, 211 - 0x5F => state = .apc, 212 - else => { 213 - // Anything else is an "alt + <b>" keypress 214 - if (@hasField(EventType, "key_press")) { 215 - vx.postEvent(.{ 216 - .key_press = .{ 217 - .codepoint = b, 218 - .mods = .{ .alt = true }, 219 - }, 220 - }); 221 - } 222 - state = .ground; 223 - }, 224 - } 225 - }, 226 - .ss3 => { 227 - state = .ground; 228 - const key: Key = switch (b) { 229 - 'A' => .{ .codepoint = Key.up }, 230 - 'B' => .{ .codepoint = Key.down }, 231 - 'C' => .{ .codepoint = Key.right }, 232 - 'D' => .{ .codepoint = Key.left }, 233 - 'F' => .{ .codepoint = Key.end }, 234 - 'H' => .{ .codepoint = Key.home }, 235 - 'P' => .{ .codepoint = Key.f1 }, 236 - 'Q' => .{ .codepoint = Key.f2 }, 237 - 'R' => .{ .codepoint = Key.f3 }, 238 - 'S' => .{ .codepoint = Key.f4 }, 239 - else => { 240 - log.warn("unhandled ss3: {x}", .{b}); 241 - continue; 242 - }, 243 - }; 244 - 133 + while (start < n) { 134 + const result = try parser.parse(buf[start..n]); 135 + start = result.n; 136 + log.debug("something {}", .{result}); 137 + const event = result.event orelse continue; 138 + switch (event) { 139 + .key_press => |key| { 245 140 if (@hasField(EventType, "key_press")) { 246 141 vx.postEvent(.{ .key_press = key }); 247 142 } 248 143 }, 249 - .csi => { 250 - switch (b) { 251 - // c0 controls. we ignore these even though we should 252 - // "execute" them. This isn't seen in practice 253 - 0x00...0x1F => {}, 254 - // intermediates. we only handle one. technically there 255 - // can be more 256 - 0x20...0x2F => seq.intermediate = b, 257 - 0x30...0x39 => { 258 - seq.param_buf[seq.param_buf_idx] = b; 259 - seq.param_buf_idx += 1; 260 - }, 261 - // private indicators. These come before any params ('?') 262 - 0x3C...0x3F => seq.private_indicator = b, 263 - ';' => { 264 - if (seq.param_buf_idx == 0) { 265 - // empty param. default it to 0 and set the 266 - // empty state 267 - seq.params[seq.param_idx] = 0; 268 - seq.empty_state.set(seq.param_idx); 269 - seq.param_idx += 1; 270 - } else { 271 - const p = try std.fmt.parseUnsigned(u16, seq.param_buf[0..seq.param_buf_idx], 10); 272 - seq.param_buf_idx = 0; 273 - seq.params[seq.param_idx] = p; 274 - seq.param_idx += 1; 275 - } 276 - }, 277 - ':' => { 278 - if (seq.param_buf_idx == 0) { 279 - // empty param. default it to 0 and set the 280 - // empty state 281 - seq.params[seq.param_idx] = 0; 282 - seq.empty_state.set(seq.param_idx); 283 - seq.param_idx += 1; 284 - // Set the *next* param as a subparam 285 - seq.sub_state.set(seq.param_idx); 286 - } else { 287 - const p = try std.fmt.parseUnsigned(u16, seq.param_buf[0..seq.param_buf_idx], 10); 288 - seq.param_buf_idx = 0; 289 - seq.params[seq.param_idx] = p; 290 - seq.param_idx += 1; 291 - // Set the *next* param as a subparam 292 - seq.sub_state.set(seq.param_idx); 293 - } 294 - }, 295 - 0x40...0xFF => { 296 - if (seq.param_buf_idx > 0) { 297 - const p = try std.fmt.parseUnsigned(u16, seq.param_buf[0..seq.param_buf_idx], 10); 298 - seq.param_buf_idx = 0; 299 - seq.params[seq.param_idx] = p; 300 - seq.param_idx += 1; 301 - } 302 - // dispatch the sequence 303 - state = .ground; 304 - const codepoint: u21 = switch (b) { 305 - 'A' => Key.up, 306 - 'B' => Key.down, 307 - 'C' => Key.right, 308 - 'D' => Key.left, 309 - 'E' => Key.kp_begin, 310 - 'F' => Key.end, 311 - 'H' => Key.home, 312 - 'P' => Key.f1, 313 - 'Q' => Key.f2, 314 - 'R' => Key.f3, 315 - 'S' => Key.f4, 316 - '~' => blk: { 317 - // The first param will define this 318 - // codepoint 319 - if (seq.param_idx < 1) { 320 - log.warn("unhandled csi: CSI {s}", .{buf[start + 1 .. i + 1]}); 321 - continue; 322 - } 323 - switch (seq.params[0]) { 324 - 2 => break :blk Key.insert, 325 - 3 => break :blk Key.delete, 326 - 5 => break :blk Key.page_up, 327 - 6 => break :blk Key.page_down, 328 - 7 => break :blk Key.home, 329 - 8 => break :blk Key.end, 330 - 11 => break :blk Key.f1, 331 - 12 => break :blk Key.f2, 332 - 13 => break :blk Key.f3, 333 - 14 => break :blk Key.f4, 334 - 15 => break :blk Key.f5, 335 - 17 => break :blk Key.f6, 336 - 18 => break :blk Key.f7, 337 - 19 => break :blk Key.f8, 338 - 20 => break :blk Key.f9, 339 - 21 => break :blk Key.f10, 340 - 23 => break :blk Key.f11, 341 - 24 => break :blk Key.f12, 342 - 200 => { 343 - // TODO: bracketed paste 344 - continue; 345 - }, 346 - 201 => { 347 - // TODO: bracketed paste 348 - continue; 349 - }, 350 - 57427 => break :blk Key.kp_begin, 351 - else => { 352 - log.warn("unhandled csi: CSI {s}", .{buf[start + 1 .. i + 1]}); 353 - continue; 354 - }, 355 - } 356 - }, 357 - 'u' => blk: { 358 - if (seq.private_indicator) |_| { 359 - // response to our kitty query 360 - // TODO: kitty query handling 361 - log.warn("unhandled csi: CSI {s}", .{buf[start + 1 .. i + 1]}); 362 - continue; 363 - } 364 - if (seq.param_idx == 0) { 365 - log.warn("unhandled csi: CSI {s}", .{buf[start + 1 .. i + 1]}); 366 - continue; 367 - } 368 - // In any csi u encoding, the codepoint 369 - // directly maps to our keypoint definitions 370 - break :blk seq.params[0]; 371 - }, 372 - 373 - 'I' => { // focus in 374 - if (@hasField(EventType, "focus_in")) { 375 - vx.postEvent(.focus_in); 376 - } 377 - continue; 378 - }, 379 - 'O' => { // focus out 380 - if (@hasField(EventType, "focus_out")) { 381 - vx.postEvent(.focus_out); 382 - } 383 - continue; 384 - }, 385 - else => { 386 - log.warn("unhandled csi: CSI {s}", .{buf[start + 1 .. i + 1]}); 387 - continue; 388 - }, 389 - }; 390 - 391 - var key: Key = .{ .codepoint = codepoint }; 392 - 393 - var idx: usize = 0; 394 - var field: u8 = 0; 395 - // parse the parameters 396 - while (idx < seq.param_idx) : (idx += 1) { 397 - switch (field) { 398 - 0 => { 399 - defer field += 1; 400 - // field 0 contains our codepoint. Any 401 - // subparameters shifted key code and 402 - // alternate keycode (csi u encoding) 403 - 404 - // We already handled our codepoint so 405 - // we just need to check for subs 406 - if (!seq.sub_state.isSet(idx + 1)) { 407 - continue; 408 - } 409 - idx += 1; 410 - // The first one is a shifted code if it 411 - // isn't empty 412 - if (!seq.empty_state.isSet(idx)) { 413 - key.shifted_codepoint = seq.params[idx]; 414 - } 415 - // check the next one for base layout 416 - // code 417 - if (!seq.sub_state.isSet(idx + 1)) { 418 - continue; 419 - } 420 - idx += 1; 421 - key.base_layout_codepoint = seq.params[idx]; 422 - }, 423 - 1 => { 424 - // field 1 is modifiers and optionally 425 - // the event type (csiu) 426 - const mod_mask: u8 = @truncate(seq.params[idx] - 1); 427 - key.mods = @bitCast(mod_mask); 428 - }, 429 - else => {}, 430 - } 431 - } 432 - if (@hasField(EventType, "key_press")) { 433 - vx.postEvent(.{ .key_press = key }); 434 - } 435 - }, 436 - } 437 - }, 438 - else => {}, 144 + .focus_in => {}, 145 + .focus_out => {}, 439 146 } 440 147 } 441 148 }
+9
src/event.zig
··· 1 + pub const Key = @import("Key.zig"); 2 + 3 + /// The events that Vaxis emits. This can be used as the generic EventType if 4 + /// there are no internal events 5 + pub const Event = union(enum) { 6 + key_press: Key, 7 + focus_in, 8 + focus_out, 9 + };
+2
src/main.zig
··· 20 20 _ = @import("Window.zig"); 21 21 _ = @import("cell.zig"); 22 22 _ = @import("ctlseqs.zig"); 23 + _ = @import("event.zig"); 23 24 _ = @import("queue.zig"); 25 + _ = @import("parser.zig"); 24 26 _ = @import("vaxis.zig"); 25 27 }
+353
src/parser.zig
··· 1 + const std = @import("std"); 2 + const testing = std.testing; 3 + const Event = @import("event.zig").Event; 4 + const Key = @import("Key.zig"); 5 + 6 + const log = std.log.scoped(.parser); 7 + 8 + /// The return type of our parse method. Contains an Event and the number of 9 + /// bytes read from the buffer. 10 + pub const Result = struct { 11 + event: ?Event, 12 + n: usize, 13 + }; 14 + 15 + // an intermediate data structure to hold sequence data while we are 16 + // scanning more bytes. This is tailored for input parsing only 17 + const Sequence = struct { 18 + // private indicators are 0x3C-0x3F 19 + private_indicator: ?u8 = null, 20 + // we won't be handling any sequences with more than one intermediate 21 + intermediate: ?u8 = null, 22 + // we should absolutely never have more then 16 params 23 + params: [16]u16 = undefined, 24 + param_idx: usize = 0, 25 + param_buf: [8]u8 = undefined, 26 + param_buf_idx: usize = 0, 27 + sub_state: std.StaticBitSet(16) = std.StaticBitSet(16).initEmpty(), 28 + empty_state: std.StaticBitSet(16) = std.StaticBitSet(16).initEmpty(), 29 + }; 30 + 31 + // the state of the parser 32 + const State = enum { 33 + ground, 34 + escape, 35 + csi, 36 + osc, 37 + dcs, 38 + sos, 39 + pm, 40 + apc, 41 + ss2, 42 + ss3, 43 + }; 44 + 45 + pub fn parse(input: []const u8) !Result { 46 + const n = input.len; 47 + 48 + var seq: Sequence = .{}; 49 + 50 + var state: State = .ground; 51 + 52 + var i: usize = 0; 53 + var start: usize = 0; 54 + // parse the read into events. This parser is bespoke for input parsing 55 + // and is not suitable for reuse as a generic vt parser 56 + while (i < n) : (i += 1) { 57 + const b = input[i]; 58 + switch (state) { 59 + .ground => { 60 + // ground state generates keypresses when parsing input. We 61 + // generally get ascii characters, but anything less than 62 + // 0x20 is a Ctrl+<c> keypress. We map these to lowercase 63 + // ascii characters when we can 64 + const key: Key = switch (b) { 65 + 0x00 => .{ .codepoint = '@', .mods = .{ .ctrl = true } }, 66 + 0x01...0x1A => .{ .codepoint = b + 0x60, .mods = .{ .ctrl = true } }, 67 + 0x1B => escape: { 68 + // NOTE: This could be an errant escape at the end 69 + // of a large read. That is _incredibly_ unlikely 70 + // given the size of read inputs and our read buffer 71 + if (i == (n - 1)) { 72 + const event = Key{ 73 + .codepoint = Key.escape, 74 + }; 75 + break :escape event; 76 + } 77 + state = .escape; 78 + continue; 79 + }, 80 + 0x20...0x7E => .{ .codepoint = b }, 81 + 0x7F => .{ .codepoint = Key.backspace }, 82 + // TODO: graphemes 83 + else => .{ .codepoint = b }, 84 + }; 85 + return .{ 86 + .event = .{ .key_press = key }, 87 + .n = i + 1, 88 + }; 89 + }, 90 + .escape => { 91 + seq = .{}; 92 + start = i; 93 + switch (b) { 94 + 0x4F => state = .ss3, 95 + 0x50 => state = .dcs, 96 + 0x58 => state = .sos, 97 + 0x5B => state = .csi, 98 + 0x5D => state = .osc, 99 + 0x5E => state = .pm, 100 + 0x5F => state = .apc, 101 + else => { 102 + // Anything else is an "alt + <b>" keypress 103 + const key: Key = .{ 104 + .codepoint = b, 105 + .mods = .{ .alt = true }, 106 + }; 107 + return .{ 108 + .event = .{ .key_press = key }, 109 + .n = i, 110 + }; 111 + }, 112 + } 113 + }, 114 + .ss3 => { 115 + state = .ground; 116 + const key: Key = switch (b) { 117 + 'A' => .{ .codepoint = Key.up }, 118 + 'B' => .{ .codepoint = Key.down }, 119 + 'C' => .{ .codepoint = Key.right }, 120 + 'D' => .{ .codepoint = Key.left }, 121 + 'F' => .{ .codepoint = Key.end }, 122 + 'H' => .{ .codepoint = Key.home }, 123 + 'P' => .{ .codepoint = Key.f1 }, 124 + 'Q' => .{ .codepoint = Key.f2 }, 125 + 'R' => .{ .codepoint = Key.f3 }, 126 + 'S' => .{ .codepoint = Key.f4 }, 127 + else => { 128 + log.warn("unhandled ss3: {x}", .{b}); 129 + return .{ 130 + .event = null, 131 + .n = i, 132 + }; 133 + }, 134 + }; 135 + return .{ 136 + .event = .{ .key_press = key }, 137 + .n = i, 138 + }; 139 + }, 140 + .csi => { 141 + switch (b) { 142 + // c0 controls. we ignore these even though we should 143 + // "execute" them. This isn't seen in practice 144 + 0x00...0x1F => {}, 145 + // intermediates. we only handle one. technically there 146 + // can be more 147 + 0x20...0x2F => seq.intermediate = b, 148 + 0x30...0x39 => { 149 + seq.param_buf[seq.param_buf_idx] = b; 150 + seq.param_buf_idx += 1; 151 + }, 152 + // private indicators. These come before any params ('?') 153 + 0x3C...0x3F => seq.private_indicator = b, 154 + ';' => { 155 + if (seq.param_buf_idx == 0) { 156 + // empty param. default it to 0 and set the 157 + // empty state 158 + seq.params[seq.param_idx] = 0; 159 + seq.empty_state.set(seq.param_idx); 160 + seq.param_idx += 1; 161 + } else { 162 + const p = try std.fmt.parseUnsigned(u16, seq.param_buf[0..seq.param_buf_idx], 10); 163 + seq.param_buf_idx = 0; 164 + seq.params[seq.param_idx] = p; 165 + seq.param_idx += 1; 166 + } 167 + }, 168 + ':' => { 169 + if (seq.param_buf_idx == 0) { 170 + // empty param. default it to 0 and set the 171 + // empty state 172 + seq.params[seq.param_idx] = 0; 173 + seq.empty_state.set(seq.param_idx); 174 + seq.param_idx += 1; 175 + // Set the *next* param as a subparam 176 + seq.sub_state.set(seq.param_idx); 177 + } else { 178 + const p = try std.fmt.parseUnsigned(u16, seq.param_buf[0..seq.param_buf_idx], 10); 179 + seq.param_buf_idx = 0; 180 + seq.params[seq.param_idx] = p; 181 + seq.param_idx += 1; 182 + // Set the *next* param as a subparam 183 + seq.sub_state.set(seq.param_idx); 184 + } 185 + }, 186 + 0x40...0xFF => { 187 + if (seq.param_buf_idx > 0) { 188 + const p = try std.fmt.parseUnsigned(u16, seq.param_buf[0..seq.param_buf_idx], 10); 189 + seq.param_buf_idx = 0; 190 + seq.params[seq.param_idx] = p; 191 + seq.param_idx += 1; 192 + } 193 + // dispatch the sequence 194 + state = .ground; 195 + const codepoint: u21 = switch (b) { 196 + 'A' => Key.up, 197 + 'B' => Key.down, 198 + 'C' => Key.right, 199 + 'D' => Key.left, 200 + 'E' => Key.kp_begin, 201 + 'F' => Key.end, 202 + 'H' => Key.home, 203 + 'P' => Key.f1, 204 + 'Q' => Key.f2, 205 + 'R' => Key.f3, 206 + 'S' => Key.f4, 207 + '~' => blk: { 208 + // The first param will define this 209 + // codepoint 210 + if (seq.param_idx < 1) { 211 + log.warn("unhandled csi: CSI {s}", .{input[start + 1 .. i + 1]}); 212 + continue; 213 + } 214 + switch (seq.params[0]) { 215 + 2 => break :blk Key.insert, 216 + 3 => break :blk Key.delete, 217 + 5 => break :blk Key.page_up, 218 + 6 => break :blk Key.page_down, 219 + 7 => break :blk Key.home, 220 + 8 => break :blk Key.end, 221 + 11 => break :blk Key.f1, 222 + 12 => break :blk Key.f2, 223 + 13 => break :blk Key.f3, 224 + 14 => break :blk Key.f4, 225 + 15 => break :blk Key.f5, 226 + 17 => break :blk Key.f6, 227 + 18 => break :blk Key.f7, 228 + 19 => break :blk Key.f8, 229 + 20 => break :blk Key.f9, 230 + 21 => break :blk Key.f10, 231 + 23 => break :blk Key.f11, 232 + 24 => break :blk Key.f12, 233 + 200 => { 234 + // TODO: bracketed paste 235 + continue; 236 + }, 237 + 201 => { 238 + // TODO: bracketed paste 239 + continue; 240 + }, 241 + 57427 => break :blk Key.kp_begin, 242 + else => { 243 + log.warn("unhandled csi: CSI {s}", .{input[start + 1 .. i + 1]}); 244 + continue; 245 + }, 246 + } 247 + }, 248 + 'u' => blk: { 249 + if (seq.private_indicator) |_| { 250 + // response to our kitty query 251 + // TODO: kitty query handling 252 + log.warn("unhandled csi: CSI {s}", .{input[start + 1 .. i + 1]}); 253 + continue; 254 + } 255 + if (seq.param_idx == 0) { 256 + log.warn("unhandled csi: CSI {s}", .{input[start + 1 .. i + 1]}); 257 + continue; 258 + } 259 + // In any csi u encoding, the codepoint 260 + // directly maps to our keypoint definitions 261 + break :blk seq.params[0]; 262 + }, 263 + 264 + 'I' => { // focus in 265 + return .{ .event = .focus_in, .n = i }; 266 + }, 267 + 'O' => { // focus out 268 + return .{ .event = .focus_out, .n = i }; 269 + }, 270 + else => { 271 + log.warn("unhandled csi: CSI {s}", .{input[start + 1 .. i + 1]}); 272 + continue; 273 + }, 274 + }; 275 + 276 + var key: Key = .{ .codepoint = codepoint }; 277 + 278 + var idx: usize = 0; 279 + var field: u8 = 0; 280 + // parse the parameters 281 + while (idx < seq.param_idx) : (idx += 1) { 282 + switch (field) { 283 + 0 => { 284 + defer field += 1; 285 + // field 0 contains our codepoint. Any 286 + // subparameters shifted key code and 287 + // alternate keycode (csi u encoding) 288 + 289 + // We already handled our codepoint so 290 + // we just need to check for subs 291 + if (!seq.sub_state.isSet(idx + 1)) { 292 + continue; 293 + } 294 + idx += 1; 295 + // The first one is a shifted code if it 296 + // isn't empty 297 + if (!seq.empty_state.isSet(idx)) { 298 + key.shifted_codepoint = seq.params[idx]; 299 + } 300 + // check the next one for base layout 301 + // code 302 + if (!seq.sub_state.isSet(idx + 1)) { 303 + continue; 304 + } 305 + idx += 1; 306 + key.base_layout_codepoint = seq.params[idx]; 307 + }, 308 + 1 => { 309 + // field 1 is modifiers and optionally 310 + // the event type (csiu) 311 + const mod_mask: u8 = @truncate(seq.params[idx] - 1); 312 + key.mods = @bitCast(mod_mask); 313 + }, 314 + else => {}, 315 + } 316 + } 317 + return .{ 318 + .event = .{ .key_press = key }, 319 + .n = i, 320 + }; 321 + }, 322 + } 323 + }, 324 + else => {}, 325 + } 326 + } 327 + // If we get here it means we didn't parse an event. The input buffer 328 + // perhaps didn't include a full event 329 + return .{ 330 + .event = null, 331 + .n = 0, 332 + }; 333 + } 334 + 335 + test "parse: single legacy keypress" { 336 + const input = "a"; 337 + const result = try parse(input); 338 + const expected_key: Key = .{ .codepoint = 'a' }; 339 + const expected_event: Event = .{ .key_press = expected_key }; 340 + 341 + try testing.expectEqual(1, result.n); 342 + try testing.expectEqual(expected_event, result.event); 343 + } 344 + 345 + test "parse: single legacy keypress with more buffer" { 346 + const input = "ab"; 347 + const result = try parse(input); 348 + const expected_key: Key = .{ .codepoint = 'a' }; 349 + const expected_event: Event = .{ .key_press = expected_key }; 350 + 351 + try testing.expectEqual(1, result.n); 352 + try testing.expectEqual(expected_event, result.event); 353 + }