libvaxis/src/Parser.zig

1131 lines
40 KiB
Zig
Raw Normal View History

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
2024-05-20 22:01:02 +02:00
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 = .{
2024-10-21 11:20:39 -05:00
.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;
2024-05-20 22:01:02 +02:00
}
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" {
2024-04-30 08:42:21 -05:00
const alloc = testing.allocator_instance.allocator();
const grapheme_data = try grapheme.GraphemeData.init(alloc);
defer grapheme_data.deinit();
const input = "a";
2024-04-30 08:42:21 -05:00
var parser: Parser = .{ .grapheme_data = &grapheme_data };
2024-05-20 22:01:02 +02:00
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" {
2024-04-30 08:42:21 -05:00
const alloc = testing.allocator_instance.allocator();
const grapheme_data = try grapheme.GraphemeData.init(alloc);
defer grapheme_data.deinit();
const input = "\x08";
2024-04-30 08:42:21 -05:00
var parser: Parser = .{ .grapheme_data = &grapheme_data };
2024-05-20 22:01:02 +02:00
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" {
2024-04-30 08:42:21 -05:00
const alloc = testing.allocator_instance.allocator();
const grapheme_data = try grapheme.GraphemeData.init(alloc);
defer grapheme_data.deinit();
const input = "ab";
2024-04-30 08:42:21 -05:00
var parser: Parser = .{ .grapheme_data = &grapheme_data };
2024-05-20 22:01:02 +02:00
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" {
2024-04-30 08:42:21 -05:00
const alloc = testing.allocator_instance.allocator();
const grapheme_data = try grapheme.GraphemeData.init(alloc);
defer grapheme_data.deinit();
const input = "\x1b";
2024-04-30 08:42:21 -05:00
var parser: Parser = .{ .grapheme_data = &grapheme_data };
2024-05-20 22:01:02 +02:00
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" {
2024-04-30 08:42:21 -05:00
const alloc = testing.allocator_instance.allocator();
const grapheme_data = try grapheme.GraphemeData.init(alloc);
defer grapheme_data.deinit();
const input = "\x01";
2024-04-30 08:42:21 -05:00
var parser: Parser = .{ .grapheme_data = &grapheme_data };
2024-05-20 22:01:02 +02:00
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" {
2024-04-30 08:42:21 -05:00
const alloc = testing.allocator_instance.allocator();
const grapheme_data = try grapheme.GraphemeData.init(alloc);
defer grapheme_data.deinit();
const input = "\x1ba";
2024-04-30 08:42:21 -05:00
var parser: Parser = .{ .grapheme_data = &grapheme_data };
2024-05-20 22:01:02 +02:00
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" {
2024-04-30 08:42:21 -05:00
const alloc = testing.allocator_instance.allocator();
const grapheme_data = try grapheme.GraphemeData.init(alloc);
defer grapheme_data.deinit();
{
// normal version
const input = "\x1b[A";
2024-04-30 08:42:21 -05:00
var parser: Parser = .{ .grapheme_data = &grapheme_data };
2024-05-20 22:01:02 +02:00
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";
2024-04-30 08:42:21 -05:00
var parser: Parser = .{ .grapheme_data = &grapheme_data };
2024-05-20 22:01:02 +02:00
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" {
2024-04-30 08:42:21 -05:00
const alloc = testing.allocator_instance.allocator();
const grapheme_data = try grapheme.GraphemeData.init(alloc);
defer grapheme_data.deinit();
const input = "\x1b[1;2A";
2024-04-30 08:42:21 -05:00
var parser: Parser = .{ .grapheme_data = &grapheme_data };
2024-05-20 22:01:02 +02:00
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" {
2024-04-30 08:42:21 -05:00
const alloc = testing.allocator_instance.allocator();
const grapheme_data = try grapheme.GraphemeData.init(alloc);
defer grapheme_data.deinit();
const input = "\x1b[2~";
2024-04-30 08:42:21 -05:00
var parser: Parser = .{ .grapheme_data = &grapheme_data };
2024-05-20 22:01:02 +02:00
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" {
2024-04-30 08:42:21 -05:00
const alloc = testing.allocator_instance.allocator();
const grapheme_data = try grapheme.GraphemeData.init(alloc);
defer grapheme_data.deinit();
const input = "\x1b[200~";
2024-04-30 08:42:21 -05:00
var parser: Parser = .{ .grapheme_data = &grapheme_data };
2024-05-20 22:01:02 +02:00
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" {
2024-04-30 08:42:21 -05:00
const alloc = testing.allocator_instance.allocator();
const grapheme_data = try grapheme.GraphemeData.init(alloc);
defer grapheme_data.deinit();
const input = "\x1b[201~";
2024-04-30 08:42:21 -05:00
var parser: Parser = .{ .grapheme_data = &grapheme_data };
2024-05-20 22:01:02 +02:00
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);
}
2024-05-20 22:01:02 +02:00
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" {
2024-04-30 08:42:21 -05:00
const alloc = testing.allocator_instance.allocator();
const grapheme_data = try grapheme.GraphemeData.init(alloc);
defer grapheme_data.deinit();
const input = "\x1b[I";
2024-04-30 08:42:21 -05:00
var parser: Parser = .{ .grapheme_data = &grapheme_data };
2024-05-20 22:01:02 +02:00
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" {
2024-04-30 08:42:21 -05:00
const alloc = testing.allocator_instance.allocator();
const grapheme_data = try grapheme.GraphemeData.init(alloc);
defer grapheme_data.deinit();
const input = "\x1b[O";
2024-04-30 08:42:21 -05:00
var parser: Parser = .{ .grapheme_data = &grapheme_data };
2024-05-20 22:01:02 +02:00
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" {
2024-04-30 08:42:21 -05:00
const alloc = testing.allocator_instance.allocator();
const grapheme_data = try grapheme.GraphemeData.init(alloc);
defer grapheme_data.deinit();
const input = "\x1b[97:65;2u";
2024-04-30 08:42:21 -05:00
var parser: Parser = .{ .grapheme_data = &grapheme_data };
2024-05-20 22:01:02 +02:00
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" {
2024-04-30 08:42:21 -05:00
const alloc = testing.allocator_instance.allocator();
const grapheme_data = try grapheme.GraphemeData.init(alloc);
defer grapheme_data.deinit();
const input = "\x1b[97:65;4u";
2024-04-30 08:42:21 -05:00
var parser: Parser = .{ .grapheme_data = &grapheme_data };
2024-05-20 22:01:02 +02:00
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" {
2024-04-30 08:42:21 -05:00
const alloc = testing.allocator_instance.allocator();
const grapheme_data = try grapheme.GraphemeData.init(alloc);
defer grapheme_data.deinit();
const input = "\x1b[97u";
2024-04-30 08:42:21 -05:00
var parser: Parser = .{ .grapheme_data = &grapheme_data };
2024-05-20 22:01:02 +02:00
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);
}
2024-04-30 08:42:21 -05:00
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 };
2024-05-20 22:01:02 +02:00
const result = try parser.parse(input, alloc);
2024-04-30 08:42:21 -05:00
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" {
2024-04-30 08:42:21 -05:00
const alloc = testing.allocator_instance.allocator();
const grapheme_data = try grapheme.GraphemeData.init(alloc);
defer grapheme_data.deinit();
const input = "🙂";
2024-04-30 08:42:21 -05:00
var parser: Parser = .{ .grapheme_data = &grapheme_data };
2024-05-20 22:01:02 +02:00
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" {
2024-04-30 08:42:21 -05:00
const alloc = testing.allocator_instance.allocator();
const grapheme_data = try grapheme.GraphemeData.init(alloc);
defer grapheme_data.deinit();
const input = "🙂a";
2024-04-30 08:42:21 -05:00
var parser: Parser = .{ .grapheme_data = &grapheme_data };
2024-05-20 22:01:02 +02:00
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" {
2024-04-30 08:42:21 -05:00
const alloc = testing.allocator_instance.allocator();
const grapheme_data = try grapheme.GraphemeData.init(alloc);
defer grapheme_data.deinit();
const input = "👩‍🚀";
2024-04-30 08:42:21 -05:00
var parser: Parser = .{ .grapheme_data = &grapheme_data };
2024-05-20 22:01:02 +02:00
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" {
2024-04-30 08:42:21 -05:00
const alloc = testing.allocator_instance.allocator();
const grapheme_data = try grapheme.GraphemeData.init(alloc);
defer grapheme_data.deinit();
const input = "👩🚀abc";
2024-04-30 08:42:21 -05:00
var parser: Parser = .{ .grapheme_data = &grapheme_data };
2024-05-20 22:01:02 +02:00
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);
}