key: enable kitty keyboard

Signed-off-by: Tim Culverhouse <tim@timculverhouse.com>
This commit is contained in:
Tim Culverhouse 2024-01-23 13:25:31 -06:00
parent 44ff960cb0
commit aaa1c17a81
8 changed files with 114 additions and 29 deletions

View file

@ -61,6 +61,8 @@ pub fn main() !void {
.winsize => |ws| { .winsize => |ws| {
try vx.resize(alloc, ws); try vx.resize(alloc, ws);
}, },
.cap_rgb => continue,
.cap_kitty_keyboard => try vx.enableKittyKeyboard(.{}),
else => {}, else => {},
} }
@ -96,5 +98,7 @@ const Event = union(enum) {
key_press: vaxis.Key, key_press: vaxis.Key,
winsize: vaxis.Winsize, winsize: vaxis.Winsize,
focus_in, focus_in,
cap_rgb,
cap_kitty_keyboard,
foo: u8, foo: u8,
}; };

View file

@ -15,6 +15,14 @@ pub const Modifiers = packed struct(u8) {
num_lock: bool = false, num_lock: bool = false,
}; };
pub const KittyFlags = packed struct(u5) {
disambiguate: bool = true,
report_events: bool = false,
report_alternate_keys: bool = true,
report_all_as_ctl_seqs: bool = true,
report_text: bool = true,
};
/// the unicode codepoint of the key event. /// the unicode codepoint of the key event.
codepoint: u21, codepoint: u21,

View file

@ -7,6 +7,8 @@ const graphemeBreak = @import("ziglyph").graphemeBreak;
const log = std.log.scoped(.parser); const log = std.log.scoped(.parser);
const Parser = @This();
/// The return type of our parse method. Contains an Event and the number of /// The return type of our parse method. Contains an Event and the number of
/// bytes read from the buffer. /// bytes read from the buffer.
pub const Result = struct { pub const Result = struct {
@ -44,7 +46,11 @@ const State = enum {
ss3, ss3,
}; };
pub fn parse(input: []const u8) !Result { // a buffer to temporarily store text in. We need this to encode
// text-as-codepoints
buf: [128]u8 = undefined,
pub fn parse(self: *Parser, input: []const u8) !Result {
const n = input.len; const n = input.len;
var seq: Sequence = .{}; var seq: Sequence = .{};
@ -349,10 +355,26 @@ pub fn parse(input: []const u8) !Result {
key.base_layout_codepoint = seq.params[idx]; key.base_layout_codepoint = seq.params[idx];
}, },
1 => { 1 => {
defer field += 1;
// field 1 is modifiers and optionally // field 1 is modifiers and optionally
// the event type (csiu) // the event type (csiu). It can be empty
const mod_mask: u8 = @truncate(seq.params[idx] - 1); if (seq.empty_state.isSet(idx)) {
key.mods = @bitCast(mod_mask); continue;
}
// default of 1
const ps: u8 = blk: {
if (seq.params[idx] == 0) break :blk 1;
break :blk @truncate(seq.params[idx]);
};
key.mods = @bitCast(ps - 1);
},
2 => {
// field 2 is text, as codepoints
var total: usize = 0;
while (idx < seq.param_idx) : (idx += 1) {
total += try std.unicode.utf8Encode(seq.params[idx], self.buf[total..]);
}
key.text = self.buf[0..total];
}, },
else => {}, else => {},
} }
@ -377,7 +399,8 @@ pub fn parse(input: []const u8) !Result {
test "parse: single xterm keypress" { test "parse: single xterm keypress" {
const input = "a"; const input = "a";
const result = try parse(input); var parser: Parser = .{};
const result = try parser.parse(input);
const expected_key: Key = .{ const expected_key: Key = .{
.codepoint = 'a', .codepoint = 'a',
.text = "a", .text = "a",
@ -390,7 +413,8 @@ test "parse: single xterm keypress" {
test "parse: single xterm keypress with more buffer" { test "parse: single xterm keypress with more buffer" {
const input = "ab"; const input = "ab";
const result = try parse(input); var parser: Parser = .{};
const result = try parser.parse(input);
const expected_key: Key = .{ const expected_key: Key = .{
.codepoint = 'a', .codepoint = 'a',
.text = "a", .text = "a",
@ -404,7 +428,8 @@ test "parse: single xterm keypress with more buffer" {
test "parse: xterm escape keypress" { test "parse: xterm escape keypress" {
const input = "\x1b"; const input = "\x1b";
const result = try parse(input); var parser: Parser = .{};
const result = try parser.parse(input);
const expected_key: Key = .{ .codepoint = Key.escape }; const expected_key: Key = .{ .codepoint = Key.escape };
const expected_event: Event = .{ .key_press = expected_key }; const expected_event: Event = .{ .key_press = expected_key };
@ -414,7 +439,8 @@ test "parse: xterm escape keypress" {
test "parse: xterm ctrl+a" { test "parse: xterm ctrl+a" {
const input = "\x01"; const input = "\x01";
const result = try parse(input); var parser: Parser = .{};
const result = try parser.parse(input);
const expected_key: Key = .{ .codepoint = 'a', .mods = .{ .ctrl = true } }; const expected_key: Key = .{ .codepoint = 'a', .mods = .{ .ctrl = true } };
const expected_event: Event = .{ .key_press = expected_key }; const expected_event: Event = .{ .key_press = expected_key };
@ -424,7 +450,8 @@ test "parse: xterm ctrl+a" {
test "parse: xterm alt+a" { test "parse: xterm alt+a" {
const input = "\x1ba"; const input = "\x1ba";
const result = try parse(input); var parser: Parser = .{};
const result = try parser.parse(input);
const expected_key: Key = .{ .codepoint = 'a', .mods = .{ .alt = true } }; const expected_key: Key = .{ .codepoint = 'a', .mods = .{ .alt = true } };
const expected_event: Event = .{ .key_press = expected_key }; const expected_event: Event = .{ .key_press = expected_key };
@ -434,7 +461,8 @@ test "parse: xterm alt+a" {
test "parse: xterm invalid ss3" { test "parse: xterm invalid ss3" {
const input = "\x1bOZ"; const input = "\x1bOZ";
const result = try parse(input); var parser: Parser = .{};
const result = try parser.parse(input);
try testing.expectEqual(3, result.n); try testing.expectEqual(3, result.n);
try testing.expectEqual(null, result.event); try testing.expectEqual(null, result.event);
@ -444,7 +472,8 @@ test "parse: xterm key up" {
{ {
// normal version // normal version
const input = "\x1bOA"; const input = "\x1bOA";
const result = try parse(input); var parser: Parser = .{};
const result = try parser.parse(input);
const expected_key: Key = .{ .codepoint = Key.up }; const expected_key: Key = .{ .codepoint = Key.up };
const expected_event: Event = .{ .key_press = expected_key }; const expected_event: Event = .{ .key_press = expected_key };
@ -455,7 +484,8 @@ test "parse: xterm key up" {
{ {
// application keys version // application keys version
const input = "\x1b[2~"; const input = "\x1b[2~";
const result = try parse(input); var parser: Parser = .{};
const result = try parser.parse(input);
const expected_key: Key = .{ .codepoint = Key.insert }; const expected_key: Key = .{ .codepoint = Key.insert };
const expected_event: Event = .{ .key_press = expected_key }; const expected_event: Event = .{ .key_press = expected_key };
@ -466,7 +496,8 @@ test "parse: xterm key up" {
test "parse: xterm shift+up" { test "parse: xterm shift+up" {
const input = "\x1b[1;2A"; const input = "\x1b[1;2A";
const result = try parse(input); var parser: Parser = .{};
const result = try parser.parse(input);
const expected_key: Key = .{ .codepoint = Key.up, .mods = .{ .shift = true } }; const expected_key: Key = .{ .codepoint = Key.up, .mods = .{ .shift = true } };
const expected_event: Event = .{ .key_press = expected_key }; const expected_event: Event = .{ .key_press = expected_key };
@ -476,7 +507,8 @@ test "parse: xterm shift+up" {
test "parse: xterm insert" { test "parse: xterm insert" {
const input = "\x1b[1;2A"; const input = "\x1b[1;2A";
const result = try parse(input); var parser: Parser = .{};
const result = try parser.parse(input);
const expected_key: Key = .{ .codepoint = Key.up, .mods = .{ .shift = true } }; const expected_key: Key = .{ .codepoint = Key.up, .mods = .{ .shift = true } };
const expected_event: Event = .{ .key_press = expected_key }; const expected_event: Event = .{ .key_press = expected_key };
@ -486,7 +518,8 @@ test "parse: xterm insert" {
test "parse: paste_start" { test "parse: paste_start" {
const input = "\x1b[200~"; const input = "\x1b[200~";
const result = try parse(input); var parser: Parser = .{};
const result = try parser.parse(input);
const expected_event: Event = .paste_start; const expected_event: Event = .paste_start;
try testing.expectEqual(6, result.n); try testing.expectEqual(6, result.n);
@ -495,7 +528,8 @@ test "parse: paste_start" {
test "parse: paste_end" { test "parse: paste_end" {
const input = "\x1b[201~"; const input = "\x1b[201~";
const result = try parse(input); var parser: Parser = .{};
const result = try parser.parse(input);
const expected_event: Event = .paste_end; const expected_event: Event = .paste_end;
try testing.expectEqual(6, result.n); try testing.expectEqual(6, result.n);
@ -504,7 +538,8 @@ test "parse: paste_end" {
test "parse: focus_in" { test "parse: focus_in" {
const input = "\x1b[I"; const input = "\x1b[I";
const result = try parse(input); var parser: Parser = .{};
const result = try parser.parse(input);
const expected_event: Event = .focus_in; const expected_event: Event = .focus_in;
try testing.expectEqual(3, result.n); try testing.expectEqual(3, result.n);
@ -513,7 +548,8 @@ test "parse: focus_in" {
test "parse: focus_out" { test "parse: focus_out" {
const input = "\x1b[O"; const input = "\x1b[O";
const result = try parse(input); var parser: Parser = .{};
const result = try parser.parse(input);
const expected_event: Event = .focus_out; const expected_event: Event = .focus_out;
try testing.expectEqual(3, result.n); try testing.expectEqual(3, result.n);
@ -522,7 +558,8 @@ test "parse: focus_out" {
test "parse: kitty: shift+a without text reporting" { test "parse: kitty: shift+a without text reporting" {
const input = "\x1b[97:65;2u"; const input = "\x1b[97:65;2u";
const result = try parse(input); var parser: Parser = .{};
const result = try parser.parse(input);
const expected_key: Key = .{ const expected_key: Key = .{
.codepoint = 'a', .codepoint = 'a',
.shifted_codepoint = 'A', .shifted_codepoint = 'A',
@ -536,7 +573,8 @@ test "parse: kitty: shift+a without text reporting" {
test "parse: kitty: alt+shift+a without text reporting" { test "parse: kitty: alt+shift+a without text reporting" {
const input = "\x1b[97:65;4u"; const input = "\x1b[97:65;4u";
const result = try parse(input); var parser: Parser = .{};
const result = try parser.parse(input);
const expected_key: Key = .{ const expected_key: Key = .{
.codepoint = 'a', .codepoint = 'a',
.shifted_codepoint = 'A', .shifted_codepoint = 'A',
@ -550,7 +588,8 @@ test "parse: kitty: alt+shift+a without text reporting" {
test "parse: kitty: a without text reporting" { test "parse: kitty: a without text reporting" {
const input = "\x1b[97u"; const input = "\x1b[97u";
const result = try parse(input); var parser: Parser = .{};
const result = try parser.parse(input);
const expected_key: Key = .{ const expected_key: Key = .{
.codepoint = 'a', .codepoint = 'a',
}; };
@ -562,7 +601,8 @@ test "parse: kitty: a without text reporting" {
test "parse: single codepoint" { test "parse: single codepoint" {
const input = "🙂"; const input = "🙂";
const result = try parse(input); var parser: Parser = .{};
const result = try parser.parse(input);
const expected_key: Key = .{ const expected_key: Key = .{
.codepoint = 0x1F642, .codepoint = 0x1F642,
.text = input, .text = input,
@ -575,7 +615,8 @@ test "parse: single codepoint" {
test "parse: single codepoint with more in buffer" { test "parse: single codepoint with more in buffer" {
const input = "🙂a"; const input = "🙂a";
const result = try parse(input); var parser: Parser = .{};
const result = try parser.parse(input);
const expected_key: Key = .{ const expected_key: Key = .{
.codepoint = 0x1F642, .codepoint = 0x1F642,
.text = "🙂", .text = "🙂",
@ -590,7 +631,8 @@ test "parse: multiple codepoint grapheme" {
// TODO: this test is passing but throws a warning. Not sure how we'll // TODO: this test is passing but throws a warning. Not sure how we'll
// handle graphemes yet // handle graphemes yet
const input = "👩‍🚀"; const input = "👩‍🚀";
const result = try parse(input); var parser: Parser = .{};
const result = try parser.parse(input);
const expected_key: Key = .{ const expected_key: Key = .{
.codepoint = Key.multicodepoint, .codepoint = Key.multicodepoint,
.text = input, .text = input,
@ -605,7 +647,8 @@ test "parse: multiple codepoint grapheme with more after" {
// TODO: this test is passing but throws a warning. Not sure how we'll // TODO: this test is passing but throws a warning. Not sure how we'll
// handle graphemes yet // handle graphemes yet
const input = "👩🚀abc"; const input = "👩🚀abc";
const result = try parse(input); var parser: Parser = .{};
const result = try parser.parse(input);
const expected_key: Key = .{ const expected_key: Key = .{
.codepoint = Key.multicodepoint, .codepoint = Key.multicodepoint,
.text = "👩‍🚀", .text = "👩‍🚀",

View file

@ -4,7 +4,7 @@ const os = std.os;
const vaxis = @import("main.zig"); const vaxis = @import("main.zig");
const Vaxis = vaxis.Vaxis; const Vaxis = vaxis.Vaxis;
const Event = @import("event.zig").Event; const Event = @import("event.zig").Event;
const parser = @import("parser.zig"); const Parser = @import("Parser.zig");
const Key = vaxis.Key; const Key = vaxis.Key;
const GraphemeCache = @import("GraphemeCache.zig"); const GraphemeCache = @import("GraphemeCache.zig");
@ -122,6 +122,8 @@ pub fn run(
.{ .fd = pipe[0], .events = std.os.POLL.IN, .revents = undefined }, .{ .fd = pipe[0], .events = std.os.POLL.IN, .revents = undefined },
}; };
var parser: Parser = .{};
// initialize the read buffer // initialize the read buffer
var buf: [1024]u8 = undefined; var buf: [1024]u8 = undefined;
// read loop // read loop
@ -180,6 +182,11 @@ pub fn run(
vx.postEvent(.cap_kitty_keyboard); vx.postEvent(.cap_kitty_keyboard);
} }
}, },
.cap_rgb => {
if (@hasField(EventType, "cap_rgb")) {
vx.postEvent(.cap_rgb);
}
},
} }
} }
} }

View file

@ -8,5 +8,8 @@ pub const Event = union(enum) {
focus_out, focus_out,
paste_start, paste_start,
paste_end, paste_end,
// these are delivered as discovered terminal capabilities
cap_kitty_keyboard, cap_kitty_keyboard,
cap_rgb,
}; };

View file

@ -20,6 +20,7 @@ test {
_ = @import("GraphemeCache.zig"); _ = @import("GraphemeCache.zig");
_ = @import("Key.zig"); _ = @import("Key.zig");
_ = @import("Options.zig"); _ = @import("Options.zig");
_ = @import("Parser.zig");
_ = @import("Screen.zig"); _ = @import("Screen.zig");
_ = @import("Tty.zig"); _ = @import("Tty.zig");
_ = @import("Window.zig"); _ = @import("Window.zig");
@ -27,6 +28,5 @@ test {
_ = @import("ctlseqs.zig"); _ = @import("ctlseqs.zig");
_ = @import("event.zig"); _ = @import("event.zig");
_ = @import("queue.zig"); _ = @import("queue.zig");
_ = @import("parser.zig");
_ = @import("vaxis.zig"); _ = @import("vaxis.zig");
} }

View file

@ -45,6 +45,9 @@ pub fn Vaxis(comptime T: type) type {
/// alt_screen state. We track so we can exit on deinit /// alt_screen state. We track so we can exit on deinit
alt_screen: bool, alt_screen: bool,
/// if we have entered kitty keyboard
kitty_keyboard: bool = false,
/// if we should redraw the entire screen on the next render /// if we should redraw the entire screen on the next render
refresh: bool = false, refresh: bool = false,
@ -74,6 +77,10 @@ pub fn Vaxis(comptime T: type) type {
_ = tty.write(ctlseqs.rmcup) catch {}; _ = tty.write(ctlseqs.rmcup) catch {};
tty.flush() catch {}; tty.flush() catch {};
} }
if (self.kitty_keyboard) {
_ = tty.write(ctlseqs.csi_u_pop) catch {};
tty.flush() catch {};
}
tty.deinit(); tty.deinit();
} }
if (alloc) |a| { if (alloc) |a| {
@ -169,7 +176,9 @@ pub fn Vaxis(comptime T: type) type {
if (std.mem.eql(u8, colorterm, "truecolor") or if (std.mem.eql(u8, colorterm, "truecolor") or
std.mem.eql(u8, colorterm, "24bit")) std.mem.eql(u8, colorterm, "24bit"))
{ {
// TODO: Notify rgb support if (@hasField(EventType, "cap_rgb")) {
self.postEvent(.cap_rgb);
}
} }
// TODO: decide if we actually want to query for focus and sync. It // TODO: decide if we actually want to query for focus and sync. It
@ -411,6 +420,17 @@ pub fn Vaxis(comptime T: type) type {
_ = try tty.write(ctlseqs.show_cursor); _ = try tty.write(ctlseqs.show_cursor);
} }
} }
pub fn enableKittyKeyboard(self: *Self, flags: Key.KittyFlags) !void {
const flag_int: u5 = @bitCast(flags);
try std.fmt.format(
self.tty.?.buffered_writer.writer(),
ctlseqs.csi_u_push,
.{
flag_int,
},
);
}
}; };
} }

View file

@ -67,7 +67,7 @@ pub fn draw(self: *TextInput, win: Window) void {
var cursor_idx: usize = 0; var cursor_idx: usize = 0;
while (iter.next()) |grapheme| { while (iter.next()) |grapheme| {
const g = grapheme.slice(self.buf.items); const g = grapheme.slice(self.buf.items);
const w = strWidth(g, .full) catch 1; const w = strWidth(g, .half) catch 1;
win.writeCell(col, 0, .{ win.writeCell(col, 0, .{
.char = .{ .char = .{
.grapheme = g, .grapheme = g,