libvaxis/src/Parser.zig
2024-10-21 11:21:12 -05:00

1130 lines
40 KiB
Zig
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const std = @import("std");
const testing = std.testing;
const Color = @import("Cell.zig").Color;
const Event = @import("event.zig").Event;
const Key = @import("Key.zig");
const Mouse = @import("Mouse.zig");
const code_point = @import("code_point");
const grapheme = @import("grapheme");
const Winsize = @import("main.zig").Winsize;
const log = std.log.scoped(.vaxis_parser);
const Parser = @This();
/// The return type of our parse method. Contains an Event and the number of
/// bytes read from the buffer.
pub const Result = struct {
event: ?Event,
n: usize,
};
const mouse_bits = struct {
const motion: u8 = 0b00100000;
const buttons: u8 = 0b11000011;
const shift: u8 = 0b00000100;
const alt: u8 = 0b00001000;
const ctrl: u8 = 0b00010000;
};
// the state of the parser
const State = enum {
ground,
escape,
csi,
osc,
dcs,
sos,
pm,
apc,
ss2,
ss3,
};
// a buffer to temporarily store text in. We need this to encode
// text-as-codepoints
buf: [128]u8 = undefined,
grapheme_data: *const grapheme.GraphemeData,
/// Parse the first event from the input buffer. If a completion event is not
/// present, Result.event will be null and Result.n will be 0
///
/// If an unknown event is found, Result.event will be null and Result.n will be
/// greater than 0
pub fn parse(self: *Parser, input: []const u8, paste_allocator: ?std.mem.Allocator) !Result {
std.debug.assert(input.len > 0);
// We gate this for len > 1 so we can detect singular escape key presses
if (input[0] == 0x1b and input.len > 1) {
switch (input[1]) {
0x4F => return parseSs3(input),
0x50 => return skipUntilST(input), // DCS
0x58 => return skipUntilST(input), // SOS
0x5B => return parseCsi(input, &self.buf), // CSI
0x5D => return parseOsc(input, paste_allocator),
0x5E => return skipUntilST(input), // PM
0x5F => return parseApc(input),
else => {
// Anything else is an "alt + <char>" keypress
const key: Key = .{
.codepoint = input[1],
.mods = .{ .alt = true },
};
return .{
.event = .{ .key_press = key },
.n = 2,
};
},
}
} else return parseGround(input, self.grapheme_data);
}
/// Parse ground state
inline fn parseGround(input: []const u8, data: *const grapheme.GraphemeData) !Result {
std.debug.assert(input.len > 0);
const b = input[0];
var n: usize = 1;
// ground state generates keypresses when parsing input. We
// generally get ascii characters, but anything less than
// 0x20 is a Ctrl+<c> keypress. We map these to lowercase
// ascii characters when we can
const key: Key = switch (b) {
0x00 => .{ .codepoint = '@', .mods = .{ .ctrl = true } },
0x08 => .{ .codepoint = Key.backspace },
0x09 => .{ .codepoint = Key.tab },
0x0A,
0x0D,
=> .{ .codepoint = Key.enter },
0x01...0x07,
0x0B...0x0C,
0x0E...0x1A,
=> .{ .codepoint = b + 0x60, .mods = .{ .ctrl = true } },
0x1B => escape: {
std.debug.assert(input.len == 1); // parseGround expects len == 1 with 0x1b
break :escape .{
.codepoint = Key.escape,
};
},
0x7F => .{ .codepoint = Key.backspace },
else => blk: {
var iter: code_point.Iterator = .{ .bytes = input };
// return null if we don't have a valid codepoint
const cp = iter.next() orelse return error.InvalidUTF8;
n = cp.len;
// Check if we have a multi-codepoint grapheme
var code = cp.code;
var g_state: grapheme.State = .{};
var prev_cp = code;
while (iter.next()) |next_cp| {
if (grapheme.graphemeBreak(prev_cp, next_cp.code, data, &g_state)) {
break;
}
prev_cp = next_cp.code;
code = Key.multicodepoint;
n += next_cp.len;
}
break :blk .{ .codepoint = code, .text = input[0..n] };
},
};
return .{
.event = .{ .key_press = key },
.n = n,
};
}
inline fn parseSs3(input: []const u8) Result {
if (input.len < 3) {
return .{
.event = null,
.n = 0,
};
}
const key: Key = switch (input[2]) {
0x1B => return .{
.event = null,
.n = 2,
},
'A' => .{ .codepoint = Key.up },
'B' => .{ .codepoint = Key.down },
'C' => .{ .codepoint = Key.right },
'D' => .{ .codepoint = Key.left },
'E' => .{ .codepoint = Key.kp_begin },
'F' => .{ .codepoint = Key.end },
'H' => .{ .codepoint = Key.home },
'P' => .{ .codepoint = Key.f1 },
'Q' => .{ .codepoint = Key.f2 },
'R' => .{ .codepoint = Key.f3 },
'S' => .{ .codepoint = Key.f4 },
else => {
log.warn("unhandled ss3: {x}", .{input[2]});
return .{
.event = null,
.n = 3,
};
},
};
return .{
.event = .{ .key_press = key },
.n = 3,
};
}
inline fn parseApc(input: []const u8) Result {
if (input.len < 3) {
return .{
.event = null,
.n = 0,
};
}
const end = std.mem.indexOfScalarPos(u8, input, 2, 0x1b) orelse return .{
.event = null,
.n = 0,
};
const sequence = input[0 .. end + 1 + 1];
switch (input[2]) {
'G' => return .{
.event = .cap_kitty_graphics,
.n = sequence.len,
},
else => return .{
.event = null,
.n = sequence.len,
},
}
}
/// Skips sequences until we see an ST (String Terminator, ESC \)
inline fn skipUntilST(input: []const u8) Result {
if (input.len < 3) {
return .{
.event = null,
.n = 0,
};
}
const end = std.mem.indexOfScalarPos(u8, input, 2, 0x1b) orelse return .{
.event = null,
.n = 0,
};
if (input.len < end + 1 + 1) {
return .{
.event = null,
.n = 0,
};
}
const sequence = input[0 .. end + 1 + 1];
return .{
.event = null,
.n = sequence.len,
};
}
/// Parses an OSC sequence
inline fn parseOsc(input: []const u8, paste_allocator: ?std.mem.Allocator) !Result {
if (input.len < 3) {
return .{
.event = null,
.n = 0,
};
}
var bel_terminated: bool = false;
// end is the index of the terminating byte(s) (either the last byte of an
// ST or BEL)
const end: usize = blk: {
const esc_result = skipUntilST(input);
if (esc_result.n > 0) break :blk esc_result.n;
// No escape, could be BEL terminated
const bel = std.mem.indexOfScalarPos(u8, input, 2, 0x07) orelse return .{
.event = null,
.n = 0,
};
bel_terminated = true;
break :blk bel + 1;
};
// The complete OSC sequence
const sequence = input[0..end];
const null_event: Result = .{ .event = null, .n = sequence.len };
const semicolon_idx = std.mem.indexOfScalarPos(u8, input, 2, ';') orelse return null_event;
const ps = std.fmt.parseUnsigned(u8, input[2..semicolon_idx], 10) catch return null_event;
switch (ps) {
4 => {
const color_idx_delim = std.mem.indexOfScalarPos(u8, input, semicolon_idx + 1, ';') orelse return null_event;
const ps_idx = std.fmt.parseUnsigned(u8, input[semicolon_idx + 1 .. color_idx_delim], 10) catch return null_event;
const color_spec = if (bel_terminated)
input[color_idx_delim + 1 .. sequence.len - 1]
else
input[color_idx_delim + 1 .. sequence.len - 2];
const color = try Color.rgbFromSpec(color_spec);
const event: Color.Report = .{
.kind = .{ .index = ps_idx },
.value = color.rgb,
};
return .{
.event = .{ .color_report = event },
.n = sequence.len,
};
},
10,
11,
12,
=> {
const color_spec = if (bel_terminated)
input[semicolon_idx + 1 .. sequence.len - 1]
else
input[semicolon_idx + 1 .. sequence.len - 2];
const color = try Color.rgbFromSpec(color_spec);
const event: Color.Report = .{
.kind = switch (ps) {
10 => .fg,
11 => .bg,
12 => .cursor,
else => unreachable,
},
.value = color.rgb,
};
return .{
.event = .{ .color_report = event },
.n = sequence.len,
};
},
52 => {
if (input[semicolon_idx + 1] != 'c') return null_event;
const payload = if (bel_terminated)
input[semicolon_idx + 3 .. sequence.len - 1]
else
input[semicolon_idx + 3 .. sequence.len - 2];
const decoder = std.base64.standard.Decoder;
const text = try paste_allocator.?.alloc(u8, try decoder.calcSizeForSlice(payload));
try decoder.decode(text, payload);
log.debug("decoded paste: {s}", .{text});
return .{
.event = .{ .paste = text },
.n = sequence.len,
};
},
else => return null_event,
}
}
inline fn parseCsi(input: []const u8, text_buf: []u8) Result {
if (input.len < 3) {
return .{
.event = null,
.n = 0,
};
}
// We start iterating at index 2 to get past the '['
const sequence = for (input[2..], 2..) |b, i| {
switch (b) {
0x40...0xFF => break input[0 .. i + 1],
else => continue,
}
} else return .{ .event = null, .n = 0 };
const null_event: Result = .{ .event = null, .n = sequence.len };
const final = sequence[sequence.len - 1];
switch (final) {
'A', 'B', 'C', 'D', 'E', 'F', 'H', 'P', 'Q', 'R', 'S' => {
// Legacy keys
// CSI {ABCDEFHPQS}
// CSI 1 ; modifier:event_type {ABCDEFHPQS}
// Split first into fields delimited by ';'
var field_iter = std.mem.splitScalar(u8, sequence[2 .. sequence.len - 1], ';');
// skip the first field
_ = field_iter.next(); //
var is_release: bool = false;
var key: Key = .{
.codepoint = switch (final) {
'A' => Key.up,
'B' => Key.down,
'C' => Key.right,
'D' => Key.left,
'E' => Key.kp_begin,
'F' => Key.end,
'H' => Key.home,
'P' => Key.f1,
'Q' => Key.f2,
'R' => Key.f3,
'S' => Key.f4,
else => return null_event,
},
};
field2: {
// modifier_mask:event_type
const field_buf = field_iter.next() orelse break :field2;
var param_iter = std.mem.splitScalar(u8, field_buf, ':');
const modifier_buf = param_iter.next() orelse unreachable;
const modifier_mask = parseParam(u8, modifier_buf, 1) orelse return null_event;
key.mods = @bitCast(modifier_mask -| 1);
if (param_iter.next()) |event_type_buf| {
is_release = std.mem.eql(u8, event_type_buf, "3");
}
}
field3: {
// text_as_codepoint[:text_as_codepoint]
const field_buf = field_iter.next() orelse break :field3;
var param_iter = std.mem.splitScalar(u8, field_buf, ':');
var total: usize = 0;
while (param_iter.next()) |cp_buf| {
const cp = parseParam(u21, cp_buf, null) orelse return null_event;
total += std.unicode.utf8Encode(cp, text_buf[total..]) catch return null_event;
}
key.text = text_buf[0..total];
}
const event: Event = if (is_release) .{ .key_release = key } else .{ .key_press = key };
return .{
.event = event,
.n = sequence.len,
};
},
'~' => {
// Legacy keys
// CSI number ~
// CSI number ; modifier ~
// CSI number ; modifier:event_type ; text_as_codepoint ~
var field_iter = std.mem.splitScalar(u8, sequence[2 .. sequence.len - 1], ';');
const number_buf = field_iter.next() orelse unreachable; // always will have one field
const number = parseParam(u16, number_buf, null) orelse return null_event;
var key: Key = .{
.codepoint = switch (number) {
2 => Key.insert,
3 => Key.delete,
5 => Key.page_up,
6 => Key.page_down,
7 => Key.home,
8 => Key.end,
11 => Key.f1,
12 => Key.f2,
13 => Key.f3,
14 => Key.f4,
15 => Key.f5,
17 => Key.f6,
18 => Key.f7,
19 => Key.f8,
20 => Key.f9,
21 => Key.f10,
23 => Key.f11,
24 => Key.f12,
200 => return .{ .event = .paste_start, .n = sequence.len },
201 => return .{ .event = .paste_end, .n = sequence.len },
57427 => Key.kp_begin,
else => return null_event,
},
};
var is_release: bool = false;
field2: {
// modifier_mask:event_type
const field_buf = field_iter.next() orelse break :field2;
var param_iter = std.mem.splitScalar(u8, field_buf, ':');
const modifier_buf = param_iter.next() orelse unreachable;
const modifier_mask = parseParam(u8, modifier_buf, 1) orelse return null_event;
key.mods = @bitCast(modifier_mask -| 1);
if (param_iter.next()) |event_type_buf| {
is_release = std.mem.eql(u8, event_type_buf, "3");
}
}
field3: {
// text_as_codepoint[:text_as_codepoint]
const field_buf = field_iter.next() orelse break :field3;
var param_iter = std.mem.splitScalar(u8, field_buf, ':');
var total: usize = 0;
while (param_iter.next()) |cp_buf| {
const cp = parseParam(u21, cp_buf, null) orelse return null_event;
total += std.unicode.utf8Encode(cp, text_buf[total..]) catch return null_event;
}
key.text = text_buf[0..total];
}
const event: Event = if (is_release) .{ .key_release = key } else .{ .key_press = key };
return .{
.event = event,
.n = sequence.len,
};
},
'I' => return .{ .event = .focus_in, .n = sequence.len },
'O' => return .{ .event = .focus_out, .n = sequence.len },
'M', 'm' => return parseMouse(sequence),
'c' => {
// Primary DA (CSI ? Pm c)
std.debug.assert(sequence.len >= 4); // ESC [ ? c == 4 bytes
switch (input[2]) {
'?' => return .{ .event = .cap_da1, .n = sequence.len },
else => return null_event,
}
},
'n' => {
// Device Status Report
// CSI Ps n
// CSI ? Ps n
std.debug.assert(sequence.len >= 3);
switch (sequence[2]) {
'?' => {
const delim_idx = std.mem.indexOfScalarPos(u8, input, 3, ';') orelse return null_event;
const ps = std.fmt.parseUnsigned(u16, input[3..delim_idx], 10) catch return null_event;
switch (ps) {
997 => {
// Color scheme update (CSI 997 ; Ps n)
// See https://github.com/contour-terminal/contour/blob/master/docs/vt-extensions/color-palette-update-notifications.md
switch (sequence[delim_idx + 1]) {
'1' => return .{
.event = .{ .color_scheme = .dark },
.n = sequence.len,
},
'2' => return .{
.event = .{ .color_scheme = .light },
.n = sequence.len,
},
else => return null_event,
}
},
else => return null_event,
}
},
else => return null_event,
}
},
't' => {
// XTWINOPS
// Split first into fields delimited by ';'
var iter = std.mem.splitScalar(u8, sequence[2 .. sequence.len - 1], ';');
const ps = iter.first();
if (std.mem.eql(u8, "48", ps)) {
// in band window resize
// CSI 48 ; height ; width ; height_pix ; width_pix t
const height_char = iter.next() orelse return null_event;
const width_char = iter.next() orelse return null_event;
const height_pix = iter.next() orelse "0";
const width_pix = iter.next() orelse "0";
const winsize: Winsize = .{
.rows = std.fmt.parseUnsigned(u16, height_char, 10) catch return null_event,
.cols = std.fmt.parseUnsigned(u16, width_char, 10) catch return null_event,
.x_pixel = std.fmt.parseUnsigned(u16, width_pix, 10) catch return null_event,
.y_pixel = std.fmt.parseUnsigned(u16, height_pix, 10) catch return null_event,
};
return .{
.event = .{ .winsize = winsize },
.n = sequence.len,
};
}
return null_event;
},
'u' => {
// Kitty keyboard
// CSI unicode-key-code:alternate-key-codes ; modifiers:event-type ; text-as-codepoints u
// Not all fields will be present. Only unicode-key-code is
// mandatory
if (sequence.len > 2 and sequence[2] == '?') return .{
.event = .cap_kitty_keyboard,
.n = sequence.len,
};
var key: Key = .{
.codepoint = undefined,
};
// Split first into fields delimited by ';'
var field_iter = std.mem.splitScalar(u8, sequence[2 .. sequence.len - 1], ';');
{ // field 1
// unicode-key-code:shifted_codepoint:base_layout_codepoint
const field_buf = field_iter.next() orelse unreachable; // There will always be at least one field
var param_iter = std.mem.splitScalar(u8, field_buf, ':');
const codepoint_buf = param_iter.next() orelse unreachable;
key.codepoint = parseParam(u21, codepoint_buf, null) orelse return null_event;
if (param_iter.next()) |shifted_cp_buf| {
key.shifted_codepoint = parseParam(u21, shifted_cp_buf, null);
}
if (param_iter.next()) |base_layout_buf| {
key.base_layout_codepoint = parseParam(u21, base_layout_buf, null);
}
}
var is_release: bool = false;
field2: {
// modifier_mask:event_type
const field_buf = field_iter.next() orelse break :field2;
var param_iter = std.mem.splitScalar(u8, field_buf, ':');
const modifier_buf = param_iter.next() orelse unreachable;
const modifier_mask = parseParam(u8, modifier_buf, 1) orelse return null_event;
key.mods = @bitCast(modifier_mask -| 1);
if (param_iter.next()) |event_type_buf| {
is_release = std.mem.eql(u8, event_type_buf, "3");
}
}
field3: {
// text_as_codepoint[:text_as_codepoint]
const field_buf = field_iter.next() orelse break :field3;
var param_iter = std.mem.splitScalar(u8, field_buf, ':');
var total: usize = 0;
while (param_iter.next()) |cp_buf| {
const cp = parseParam(u21, cp_buf, null) orelse return null_event;
total += std.unicode.utf8Encode(cp, text_buf[total..]) catch return null_event;
}
key.text = text_buf[0..total];
}
const event: Event = if (is_release)
.{ .key_release = key }
else
.{ .key_press = key };
return .{ .event = event, .n = sequence.len };
},
'y' => {
// DECRPM (CSI ? Ps ; Pm $ y)
const delim_idx = std.mem.indexOfScalarPos(u8, input, 3, ';') orelse return null_event;
const ps = std.fmt.parseUnsigned(u16, input[3..delim_idx], 10) catch return null_event;
const pm = std.fmt.parseUnsigned(u8, input[delim_idx + 1 .. sequence.len - 2], 10) catch return null_event;
switch (ps) {
// Mouse Pixel reporting
1016 => switch (pm) {
0, 4 => return null_event,
else => return .{ .event = .cap_sgr_pixels, .n = sequence.len },
},
// Unicode Core, see https://github.com/contour-terminal/terminal-unicode-core
2027 => switch (pm) {
0, 4 => return null_event,
else => return .{ .event = .cap_unicode, .n = sequence.len },
},
// Color scheme reportnig, see https://github.com/contour-terminal/contour/blob/master/docs/vt-extensions/color-palette-update-notifications.md
2031 => switch (pm) {
0, 4 => return null_event,
else => return .{ .event = .cap_color_scheme_updates, .n = sequence.len },
},
else => return null_event,
}
},
else => return null_event,
}
}
/// Parse a param buffer, returning a default value if the param was empty
inline fn parseParam(comptime T: type, buf: []const u8, default: ?T) ?T {
if (buf.len == 0) return default;
return std.fmt.parseUnsigned(T, buf, 10) catch return null;
}
/// Parse a mouse event
inline fn parseMouse(input: []const u8) Result {
std.debug.assert(input.len >= 4); // ESC [ < [Mm]
const null_event: Result = .{ .event = null, .n = input.len };
if (input[2] != '<') return null_event;
const delim1 = std.mem.indexOfScalarPos(u8, input, 3, ';') orelse return null_event;
const button_mask = parseParam(u16, input[3..delim1], null) orelse return null_event;
const delim2 = std.mem.indexOfScalarPos(u8, input, delim1 + 1, ';') orelse return null_event;
const px = parseParam(u16, input[delim1 + 1 .. delim2], 1) orelse return null_event;
const py = parseParam(u16, input[delim2 + 1 .. input.len - 1], 1) orelse return null_event;
const button: Mouse.Button = @enumFromInt(button_mask & mouse_bits.buttons);
const motion = button_mask & mouse_bits.motion > 0;
const shift = button_mask & mouse_bits.shift > 0;
const alt = button_mask & mouse_bits.alt > 0;
const ctrl = button_mask & mouse_bits.ctrl > 0;
const mouse = Mouse{
.button = button,
.mods = .{
.shift = shift,
.alt = alt,
.ctrl = ctrl,
},
.col = px -| 1,
.row = py -| 1,
.type = blk: {
if (motion and button != Mouse.Button.none) {
break :blk .drag;
}
if (motion and button == Mouse.Button.none) {
break :blk .motion;
}
if (input[input.len - 1] == 'm') break :blk .release;
break :blk .press;
},
};
return .{ .event = .{ .mouse = mouse }, .n = input.len };
}
test "parse: single xterm keypress" {
const alloc = testing.allocator_instance.allocator();
const grapheme_data = try grapheme.GraphemeData.init(alloc);
defer grapheme_data.deinit();
const input = "a";
var parser: Parser = .{ .grapheme_data = &grapheme_data };
const result = try parser.parse(input, alloc);
const expected_key: Key = .{
.codepoint = 'a',
.text = "a",
};
const expected_event: Event = .{ .key_press = expected_key };
try testing.expectEqual(1, result.n);
try testing.expectEqual(expected_event, result.event);
}
test "parse: single xterm keypress backspace" {
const alloc = testing.allocator_instance.allocator();
const grapheme_data = try grapheme.GraphemeData.init(alloc);
defer grapheme_data.deinit();
const input = "\x08";
var parser: Parser = .{ .grapheme_data = &grapheme_data };
const result = try parser.parse(input, alloc);
const expected_key: Key = .{
.codepoint = Key.backspace,
};
const expected_event: Event = .{ .key_press = expected_key };
try testing.expectEqual(1, result.n);
try testing.expectEqual(expected_event, result.event);
}
test "parse: single xterm keypress with more buffer" {
const alloc = testing.allocator_instance.allocator();
const grapheme_data = try grapheme.GraphemeData.init(alloc);
defer grapheme_data.deinit();
const input = "ab";
var parser: Parser = .{ .grapheme_data = &grapheme_data };
const result = try parser.parse(input, alloc);
const expected_key: Key = .{
.codepoint = 'a',
.text = "a",
};
const expected_event: Event = .{ .key_press = expected_key };
try testing.expectEqual(1, result.n);
try testing.expectEqualStrings(expected_key.text.?, result.event.?.key_press.text.?);
try testing.expectEqualDeep(expected_event, result.event);
}
test "parse: xterm escape keypress" {
const alloc = testing.allocator_instance.allocator();
const grapheme_data = try grapheme.GraphemeData.init(alloc);
defer grapheme_data.deinit();
const input = "\x1b";
var parser: Parser = .{ .grapheme_data = &grapheme_data };
const result = try parser.parse(input, alloc);
const expected_key: Key = .{ .codepoint = Key.escape };
const expected_event: Event = .{ .key_press = expected_key };
try testing.expectEqual(1, result.n);
try testing.expectEqual(expected_event, result.event);
}
test "parse: xterm ctrl+a" {
const alloc = testing.allocator_instance.allocator();
const grapheme_data = try grapheme.GraphemeData.init(alloc);
defer grapheme_data.deinit();
const input = "\x01";
var parser: Parser = .{ .grapheme_data = &grapheme_data };
const result = try parser.parse(input, alloc);
const expected_key: Key = .{ .codepoint = 'a', .mods = .{ .ctrl = true } };
const expected_event: Event = .{ .key_press = expected_key };
try testing.expectEqual(1, result.n);
try testing.expectEqual(expected_event, result.event);
}
test "parse: xterm alt+a" {
const alloc = testing.allocator_instance.allocator();
const grapheme_data = try grapheme.GraphemeData.init(alloc);
defer grapheme_data.deinit();
const input = "\x1ba";
var parser: Parser = .{ .grapheme_data = &grapheme_data };
const result = try parser.parse(input, alloc);
const expected_key: Key = .{ .codepoint = 'a', .mods = .{ .alt = true } };
const expected_event: Event = .{ .key_press = expected_key };
try testing.expectEqual(2, result.n);
try testing.expectEqual(expected_event, result.event);
}
test "parse: xterm key up" {
const alloc = testing.allocator_instance.allocator();
const grapheme_data = try grapheme.GraphemeData.init(alloc);
defer grapheme_data.deinit();
{
// normal version
const input = "\x1b[A";
var parser: Parser = .{ .grapheme_data = &grapheme_data };
const result = try parser.parse(input, alloc);
const expected_key: Key = .{ .codepoint = Key.up };
const expected_event: Event = .{ .key_press = expected_key };
try testing.expectEqual(3, result.n);
try testing.expectEqual(expected_event, result.event);
}
{
// application keys version
const input = "\x1bOA";
var parser: Parser = .{ .grapheme_data = &grapheme_data };
const result = try parser.parse(input, alloc);
const expected_key: Key = .{ .codepoint = Key.up };
const expected_event: Event = .{ .key_press = expected_key };
try testing.expectEqual(3, result.n);
try testing.expectEqual(expected_event, result.event);
}
}
test "parse: xterm shift+up" {
const alloc = testing.allocator_instance.allocator();
const grapheme_data = try grapheme.GraphemeData.init(alloc);
defer grapheme_data.deinit();
const input = "\x1b[1;2A";
var parser: Parser = .{ .grapheme_data = &grapheme_data };
const result = try parser.parse(input, alloc);
const expected_key: Key = .{ .codepoint = Key.up, .mods = .{ .shift = true } };
const expected_event: Event = .{ .key_press = expected_key };
try testing.expectEqual(6, result.n);
try testing.expectEqual(expected_event, result.event);
}
test "parse: xterm insert" {
const alloc = testing.allocator_instance.allocator();
const grapheme_data = try grapheme.GraphemeData.init(alloc);
defer grapheme_data.deinit();
const input = "\x1b[2~";
var parser: Parser = .{ .grapheme_data = &grapheme_data };
const result = try parser.parse(input, alloc);
const expected_key: Key = .{ .codepoint = Key.insert, .mods = .{} };
const expected_event: Event = .{ .key_press = expected_key };
try testing.expectEqual(input.len, result.n);
try testing.expectEqual(expected_event, result.event);
}
test "parse: paste_start" {
const alloc = testing.allocator_instance.allocator();
const grapheme_data = try grapheme.GraphemeData.init(alloc);
defer grapheme_data.deinit();
const input = "\x1b[200~";
var parser: Parser = .{ .grapheme_data = &grapheme_data };
const result = try parser.parse(input, alloc);
const expected_event: Event = .paste_start;
try testing.expectEqual(6, result.n);
try testing.expectEqual(expected_event, result.event);
}
test "parse: paste_end" {
const alloc = testing.allocator_instance.allocator();
const grapheme_data = try grapheme.GraphemeData.init(alloc);
defer grapheme_data.deinit();
const input = "\x1b[201~";
var parser: Parser = .{ .grapheme_data = &grapheme_data };
const result = try parser.parse(input, alloc);
const expected_event: Event = .paste_end;
try testing.expectEqual(6, result.n);
try testing.expectEqual(expected_event, result.event);
}
test "parse: osc52 paste" {
const alloc = testing.allocator_instance.allocator();
const grapheme_data = try grapheme.GraphemeData.init(alloc);
defer grapheme_data.deinit();
const input = "\x1b]52;c;b3NjNTIgcGFzdGU=\x1b\\";
const expected_text = "osc52 paste";
var parser: Parser = .{ .grapheme_data = &grapheme_data };
const result = try parser.parse(input, alloc);
try testing.expectEqual(25, result.n);
switch (result.event.?) {
.paste => |text| {
defer alloc.free(text);
try testing.expectEqualStrings(expected_text, text);
},
else => try testing.expect(false),
}
}
test "parse: focus_in" {
const alloc = testing.allocator_instance.allocator();
const grapheme_data = try grapheme.GraphemeData.init(alloc);
defer grapheme_data.deinit();
const input = "\x1b[I";
var parser: Parser = .{ .grapheme_data = &grapheme_data };
const result = try parser.parse(input, alloc);
const expected_event: Event = .focus_in;
try testing.expectEqual(3, result.n);
try testing.expectEqual(expected_event, result.event);
}
test "parse: focus_out" {
const alloc = testing.allocator_instance.allocator();
const grapheme_data = try grapheme.GraphemeData.init(alloc);
defer grapheme_data.deinit();
const input = "\x1b[O";
var parser: Parser = .{ .grapheme_data = &grapheme_data };
const result = try parser.parse(input, alloc);
const expected_event: Event = .focus_out;
try testing.expectEqual(3, result.n);
try testing.expectEqual(expected_event, result.event);
}
test "parse: kitty: shift+a without text reporting" {
const alloc = testing.allocator_instance.allocator();
const grapheme_data = try grapheme.GraphemeData.init(alloc);
defer grapheme_data.deinit();
const input = "\x1b[97:65;2u";
var parser: Parser = .{ .grapheme_data = &grapheme_data };
const result = try parser.parse(input, alloc);
const expected_key: Key = .{
.codepoint = 'a',
.shifted_codepoint = 'A',
.mods = .{ .shift = true },
};
const expected_event: Event = .{ .key_press = expected_key };
try testing.expectEqual(10, result.n);
try testing.expectEqual(expected_event, result.event);
}
test "parse: kitty: alt+shift+a without text reporting" {
const alloc = testing.allocator_instance.allocator();
const grapheme_data = try grapheme.GraphemeData.init(alloc);
defer grapheme_data.deinit();
const input = "\x1b[97:65;4u";
var parser: Parser = .{ .grapheme_data = &grapheme_data };
const result = try parser.parse(input, alloc);
const expected_key: Key = .{
.codepoint = 'a',
.shifted_codepoint = 'A',
.mods = .{ .shift = true, .alt = true },
};
const expected_event: Event = .{ .key_press = expected_key };
try testing.expectEqual(10, result.n);
try testing.expectEqual(expected_event, result.event);
}
test "parse: kitty: a without text reporting" {
const alloc = testing.allocator_instance.allocator();
const grapheme_data = try grapheme.GraphemeData.init(alloc);
defer grapheme_data.deinit();
const input = "\x1b[97u";
var parser: Parser = .{ .grapheme_data = &grapheme_data };
const result = try parser.parse(input, alloc);
const expected_key: Key = .{
.codepoint = 'a',
};
const expected_event: Event = .{ .key_press = expected_key };
try testing.expectEqual(5, result.n);
try testing.expectEqual(expected_event, result.event);
}
test "parse: kitty: release event" {
const alloc = testing.allocator_instance.allocator();
const grapheme_data = try grapheme.GraphemeData.init(alloc);
defer grapheme_data.deinit();
const input = "\x1b[97;1:3u";
var parser: Parser = .{ .grapheme_data = &grapheme_data };
const result = try parser.parse(input, alloc);
const expected_key: Key = .{
.codepoint = 'a',
};
const expected_event: Event = .{ .key_release = expected_key };
try testing.expectEqual(9, result.n);
try testing.expectEqual(expected_event, result.event);
}
test "parse: single codepoint" {
const alloc = testing.allocator_instance.allocator();
const grapheme_data = try grapheme.GraphemeData.init(alloc);
defer grapheme_data.deinit();
const input = "🙂";
var parser: Parser = .{ .grapheme_data = &grapheme_data };
const result = try parser.parse(input, alloc);
const expected_key: Key = .{
.codepoint = 0x1F642,
.text = input,
};
const expected_event: Event = .{ .key_press = expected_key };
try testing.expectEqual(4, result.n);
try testing.expectEqual(expected_event, result.event);
}
test "parse: single codepoint with more in buffer" {
const alloc = testing.allocator_instance.allocator();
const grapheme_data = try grapheme.GraphemeData.init(alloc);
defer grapheme_data.deinit();
const input = "🙂a";
var parser: Parser = .{ .grapheme_data = &grapheme_data };
const result = try parser.parse(input, alloc);
const expected_key: Key = .{
.codepoint = 0x1F642,
.text = "🙂",
};
const expected_event: Event = .{ .key_press = expected_key };
try testing.expectEqual(4, result.n);
try testing.expectEqualDeep(expected_event, result.event);
}
test "parse: multiple codepoint grapheme" {
const alloc = testing.allocator_instance.allocator();
const grapheme_data = try grapheme.GraphemeData.init(alloc);
defer grapheme_data.deinit();
const input = "👩‍🚀";
var parser: Parser = .{ .grapheme_data = &grapheme_data };
const result = try parser.parse(input, alloc);
const expected_key: Key = .{
.codepoint = Key.multicodepoint,
.text = input,
};
const expected_event: Event = .{ .key_press = expected_key };
try testing.expectEqual(input.len, result.n);
try testing.expectEqual(expected_event, result.event);
}
test "parse: multiple codepoint grapheme with more after" {
const alloc = testing.allocator_instance.allocator();
const grapheme_data = try grapheme.GraphemeData.init(alloc);
defer grapheme_data.deinit();
const input = "👩🚀abc";
var parser: Parser = .{ .grapheme_data = &grapheme_data };
const result = try parser.parse(input, alloc);
const expected_key: Key = .{
.codepoint = Key.multicodepoint,
.text = "👩‍🚀",
};
try testing.expectEqual(expected_key.text.?.len, result.n);
const actual = result.event.?.key_press;
try testing.expectEqualStrings(expected_key.text.?, actual.text.?);
try testing.expectEqual(expected_key.codepoint, actual.codepoint);
}
test "parse(csi): decrpm" {
var buf: [1]u8 = undefined;
{
const input = "\x1b[?1016;1$y";
const result = parseCsi(input, &buf);
const expected: Result = .{
.event = .cap_sgr_pixels,
.n = input.len,
};
try testing.expectEqual(expected.n, result.n);
try testing.expectEqual(expected.event, result.event);
}
{
const input = "\x1b[?1016;0$y";
const result = parseCsi(input, &buf);
const expected: Result = .{
.event = null,
.n = input.len,
};
try testing.expectEqual(expected.n, result.n);
try testing.expectEqual(expected.event, result.event);
}
}
test "parse(csi): primary da" {
var buf: [1]u8 = undefined;
const input = "\x1b[?c";
const result = parseCsi(input, &buf);
const expected: Result = .{
.event = .cap_da1,
.n = input.len,
};
try testing.expectEqual(expected.n, result.n);
try testing.expectEqual(expected.event, result.event);
}
test "parse(csi): dsr" {
var buf: [1]u8 = undefined;
{
const input = "\x1b[?997;1n";
const result = parseCsi(input, &buf);
const expected: Result = .{
.event = .{ .color_scheme = .dark },
.n = input.len,
};
try testing.expectEqual(expected.n, result.n);
try testing.expectEqual(expected.event, result.event);
}
{
const input = "\x1b[?997;2n";
const result = parseCsi(input, &buf);
const expected: Result = .{
.event = .{ .color_scheme = .light },
.n = input.len,
};
try testing.expectEqual(expected.n, result.n);
try testing.expectEqual(expected.event, result.event);
}
{
const input = "\x1b[0n";
const result = parseCsi(input, &buf);
const expected: Result = .{
.event = null,
.n = input.len,
};
try testing.expectEqual(expected.n, result.n);
try testing.expectEqual(expected.event, result.event);
}
}
test "parse(csi): mouse" {
var buf: [1]u8 = undefined;
const input = "\x1b[<35;1;1m";
const result = parseCsi(input, &buf);
const expected: Result = .{
.event = .{ .mouse = .{
.col = 0,
.row = 0,
.button = .none,
.type = .motion,
.mods = .{},
} },
.n = input.len,
};
try testing.expectEqual(expected.n, result.n);
try testing.expectEqual(expected.event, result.event);
}