const std = @import("std"); const Msg = @import("root.zig").Msg; const KeyMsg = Msg.Key; const KeyKind = Msg.Key.Kind; pub const ParseError = error{ Incomplete, Invalid }; pub const Parsed = struct { msg: Msg.Key, consumed: usize, }; pub fn parse(buf: []const u8) ParseError!Parsed { if (buf.len == 0) return error.Incomplete; const b = buf[0]; switch (b) { 0x09 => return single(.tab), 0x0a, 0x0d => return single(.enter), 0x7f, 0x08 => return single(.backspace), 0x20 => return single(.space), 0x1b => return parseEscape(buf), 0x01...0x07, 0x0b, 0x0c, 0x0e...0x1a => { const letter: u21 = b + ('a' - 1); return .{ .msg = .{ .kind = .rune, .rune = letter, .mods = .{ .ctrl = true } }, .consumed = 1, }; }, 0x21...0x7e => return .{ .msg = .{ .kind = .rune, .rune = b }, .consumed = 1, }, 0x80...0xff => return parseUtf8(buf), else => return error.Invalid, } } fn single(kind: Msg.Key.Kind) Parsed { return .{ .msg = .{ .kind = kind }, .consumed = 1 }; } fn parseEscape(buf: []const u8) ParseError!Parsed { if (buf.len < 2) return error.Incomplete; if (buf[1] != '[' and buf[1] != 'O') { if (buf[1] >= 0x20 and buf[1] < 0x7f) { return .{ .msg = .{ .kind = .rune, .rune = buf[1], .mods = .{ .alt = true } }, .consumed = 2, }; } return .{ .msg = .{ .kind = .escape }, .consumed = 1 }; } if (buf.len < 3) return error.Incomplete; const final = blk: { var i: usize = 2; while (i < buf.len) : (i += 1) { const c = buf[i]; if (c >= 0x40 and c <= 0x7e) break :blk i; } return error.Incomplete; }; const params = buf[2..final]; const fin = buf[final]; const kind: Msg.Key.Kind = switch (fin) { 'A' => .up, 'B' => .down, 'C' => .right, 'D' => .left, 'H' => .home, 'F' => .end, 'P' => .f1, 'Q' => .f2, 'R' => .f3, 'S' => .f4, '~' => blk: { // ESC [ ~ const n = std.fmt.parseInt(u8, params, 10) catch return error.Invalid; break :blk switch (n) { 1, 7 => .home, 2 => .insert, 3 => .delete, 4, 8 => .end, 5 => .page_up, 6 => .page_down, else => return error.Invalid, }; }, else => return error.Invalid, }; return .{ .msg = .{ .kind = kind }, .consumed = final + 1 }; } fn parseUtf8(buf: []const u8) ParseError!Parsed { const len = std.unicode.utf8ByteSequenceLength(buf[0]) catch return error.Invalid; if (buf.len < len) return error.Incomplete; const cp = std.unicode.utf8Decode(buf[0..len]) catch return error.Invalid; return .{ .msg = .{ .kind = .rune, .rune = cp }, .consumed = len, }; } test "ascii printable" { const parsed = try parse("a"); try std.testing.expectEqual(KeyKind.rune, parsed.msg.kind); try std.testing.expectEqual(@as(u21, 'a'), parsed.msg.rune); try std.testing.expectEqual(@as(usize, 1), parsed.consumed); } test "ctrl+c" { const parsed = try parse(&.{0x03}); try std.testing.expectEqual(KeyKind.rune, parsed.msg.kind); try std.testing.expectEqual(@as(u21, 'c'), parsed.msg.rune); try std.testing.expect(parsed.msg.mods.ctrl); } test "arrow up" { const parsed = try parse("\x1b[A"); try std.testing.expectEqual(KeyKind.up, parsed.msg.kind); try std.testing.expectEqual(@as(usize, 3), parsed.consumed); } test "delete via ~" { const parsed = try parse("\x1b[3~"); try std.testing.expectEqual(KeyKind.delete, parsed.msg.kind); try std.testing.expectEqual(@as(usize, 4), parsed.consumed); } test "incomplete csi" { try std.testing.expectError(error.Incomplete, parse("\x1b[")); }