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| {
try vx.resize(alloc, ws);
},
.cap_rgb => continue,
.cap_kitty_keyboard => try vx.enableKittyKeyboard(.{}),
else => {},
}
@ -96,5 +98,7 @@ const Event = union(enum) {
key_press: vaxis.Key,
winsize: vaxis.Winsize,
focus_in,
cap_rgb,
cap_kitty_keyboard,
foo: u8,
};

View file

@ -15,6 +15,14 @@ pub const Modifiers = packed struct(u8) {
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.
codepoint: u21,

View file

@ -7,6 +7,8 @@ const graphemeBreak = @import("ziglyph").graphemeBreak;
const log = std.log.scoped(.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 {
@ -44,7 +46,11 @@ const State = enum {
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;
var seq: Sequence = .{};
@ -349,10 +355,26 @@ pub fn parse(input: []const u8) !Result {
key.base_layout_codepoint = seq.params[idx];
},
1 => {
defer field += 1;
// field 1 is modifiers and optionally
// the event type (csiu)
const mod_mask: u8 = @truncate(seq.params[idx] - 1);
key.mods = @bitCast(mod_mask);
// the event type (csiu). It can be empty
if (seq.empty_state.isSet(idx)) {
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 => {},
}
@ -377,7 +399,8 @@ pub fn parse(input: []const u8) !Result {
test "parse: single xterm keypress" {
const input = "a";
const result = try parse(input);
var parser: Parser = .{};
const result = try parser.parse(input);
const expected_key: Key = .{
.codepoint = 'a',
.text = "a",
@ -390,7 +413,8 @@ test "parse: single xterm keypress" {
test "parse: single xterm keypress with more buffer" {
const input = "ab";
const result = try parse(input);
var parser: Parser = .{};
const result = try parser.parse(input);
const expected_key: Key = .{
.codepoint = 'a',
.text = "a",
@ -404,7 +428,8 @@ test "parse: single xterm keypress with more buffer" {
test "parse: xterm escape keypress" {
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_event: Event = .{ .key_press = expected_key };
@ -414,7 +439,8 @@ test "parse: xterm escape keypress" {
test "parse: xterm ctrl+a" {
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_event: Event = .{ .key_press = expected_key };
@ -424,7 +450,8 @@ test "parse: xterm ctrl+a" {
test "parse: xterm alt+a" {
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_event: Event = .{ .key_press = expected_key };
@ -434,7 +461,8 @@ test "parse: xterm alt+a" {
test "parse: xterm invalid ss3" {
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(null, result.event);
@ -444,7 +472,8 @@ test "parse: xterm key up" {
{
// normal version
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_event: Event = .{ .key_press = expected_key };
@ -455,7 +484,8 @@ test "parse: xterm key up" {
{
// application keys version
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_event: Event = .{ .key_press = expected_key };
@ -466,7 +496,8 @@ test "parse: xterm key up" {
test "parse: xterm shift+up" {
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_event: Event = .{ .key_press = expected_key };
@ -476,7 +507,8 @@ test "parse: xterm shift+up" {
test "parse: xterm insert" {
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_event: Event = .{ .key_press = expected_key };
@ -486,7 +518,8 @@ test "parse: xterm insert" {
test "parse: paste_start" {
const input = "\x1b[200~";
const result = try parse(input);
var parser: Parser = .{};
const result = try parser.parse(input);
const expected_event: Event = .paste_start;
try testing.expectEqual(6, result.n);
@ -495,7 +528,8 @@ test "parse: paste_start" {
test "parse: paste_end" {
const input = "\x1b[201~";
const result = try parse(input);
var parser: Parser = .{};
const result = try parser.parse(input);
const expected_event: Event = .paste_end;
try testing.expectEqual(6, result.n);
@ -504,7 +538,8 @@ test "parse: paste_end" {
test "parse: focus_in" {
const input = "\x1b[I";
const result = try parse(input);
var parser: Parser = .{};
const result = try parser.parse(input);
const expected_event: Event = .focus_in;
try testing.expectEqual(3, result.n);
@ -513,7 +548,8 @@ test "parse: focus_in" {
test "parse: focus_out" {
const input = "\x1b[O";
const result = try parse(input);
var parser: Parser = .{};
const result = try parser.parse(input);
const expected_event: Event = .focus_out;
try testing.expectEqual(3, result.n);
@ -522,7 +558,8 @@ test "parse: focus_out" {
test "parse: kitty: shift+a without text reporting" {
const input = "\x1b[97:65;2u";
const result = try parse(input);
var parser: Parser = .{};
const result = try parser.parse(input);
const expected_key: Key = .{
.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" {
const input = "\x1b[97:65;4u";
const result = try parse(input);
var parser: Parser = .{};
const result = try parser.parse(input);
const expected_key: Key = .{
.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" {
const input = "\x1b[97u";
const result = try parse(input);
var parser: Parser = .{};
const result = try parser.parse(input);
const expected_key: Key = .{
.codepoint = 'a',
};
@ -562,7 +601,8 @@ test "parse: kitty: a without text reporting" {
test "parse: single codepoint" {
const input = "🙂";
const result = try parse(input);
var parser: Parser = .{};
const result = try parser.parse(input);
const expected_key: Key = .{
.codepoint = 0x1F642,
.text = input,
@ -575,7 +615,8 @@ test "parse: single codepoint" {
test "parse: single codepoint with more in buffer" {
const input = "🙂a";
const result = try parse(input);
var parser: Parser = .{};
const result = try parser.parse(input);
const expected_key: Key = .{
.codepoint = 0x1F642,
.text = "🙂",
@ -590,7 +631,8 @@ test "parse: multiple codepoint grapheme" {
// TODO: this test is passing but throws a warning. Not sure how we'll
// handle graphemes yet
const input = "👩‍🚀";
const result = try parse(input);
var parser: Parser = .{};
const result = try parser.parse(input);
const expected_key: Key = .{
.codepoint = Key.multicodepoint,
.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
// handle graphemes yet
const input = "👩🚀abc";
const result = try parse(input);
var parser: Parser = .{};
const result = try parser.parse(input);
const expected_key: Key = .{
.codepoint = Key.multicodepoint,
.text = "👩‍🚀",

View file

@ -4,7 +4,7 @@ const os = std.os;
const vaxis = @import("main.zig");
const Vaxis = vaxis.Vaxis;
const Event = @import("event.zig").Event;
const parser = @import("parser.zig");
const Parser = @import("Parser.zig");
const Key = vaxis.Key;
const GraphemeCache = @import("GraphemeCache.zig");
@ -122,6 +122,8 @@ pub fn run(
.{ .fd = pipe[0], .events = std.os.POLL.IN, .revents = undefined },
};
var parser: Parser = .{};
// initialize the read buffer
var buf: [1024]u8 = undefined;
// read loop
@ -180,6 +182,11 @@ pub fn run(
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,
paste_start,
paste_end,
// these are delivered as discovered terminal capabilities
cap_kitty_keyboard,
cap_rgb,
};

View file

@ -20,6 +20,7 @@ test {
_ = @import("GraphemeCache.zig");
_ = @import("Key.zig");
_ = @import("Options.zig");
_ = @import("Parser.zig");
_ = @import("Screen.zig");
_ = @import("Tty.zig");
_ = @import("Window.zig");
@ -27,6 +28,5 @@ test {
_ = @import("ctlseqs.zig");
_ = @import("event.zig");
_ = @import("queue.zig");
_ = @import("parser.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: bool,
/// if we have entered kitty keyboard
kitty_keyboard: bool = false,
/// if we should redraw the entire screen on the next render
refresh: bool = false,
@ -74,6 +77,10 @@ pub fn Vaxis(comptime T: type) type {
_ = tty.write(ctlseqs.rmcup) catch {};
tty.flush() catch {};
}
if (self.kitty_keyboard) {
_ = tty.write(ctlseqs.csi_u_pop) catch {};
tty.flush() catch {};
}
tty.deinit();
}
if (alloc) |a| {
@ -169,7 +176,9 @@ pub fn Vaxis(comptime T: type) type {
if (std.mem.eql(u8, colorterm, "truecolor") or
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
@ -411,6 +420,17 @@ pub fn Vaxis(comptime T: type) type {
_ = 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;
while (iter.next()) |grapheme| {
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, .{
.char = .{
.grapheme = g,