diff --git a/examples/text_input.zig b/examples/text_input.zig index aa6a825..3fe7c9b 100644 --- a/examples/text_input.zig +++ b/examples/text_input.zig @@ -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, }; diff --git a/src/Key.zig b/src/Key.zig index dc3e41a..1540166 100644 --- a/src/Key.zig +++ b/src/Key.zig @@ -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, diff --git a/src/parser.zig b/src/Parser.zig similarity index 88% rename from src/parser.zig rename to src/Parser.zig index 2bf2e54..64fab62 100644 --- a/src/parser.zig +++ b/src/Parser.zig @@ -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 = "👩‍🚀", diff --git a/src/Tty.zig b/src/Tty.zig index 7b16cda..0be97e1 100644 --- a/src/Tty.zig +++ b/src/Tty.zig @@ -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); + } + }, } } } diff --git a/src/event.zig b/src/event.zig index 1f75be9..441d1a0 100644 --- a/src/event.zig +++ b/src/event.zig @@ -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, }; diff --git a/src/main.zig b/src/main.zig index 19f4bb5..e9b140a 100644 --- a/src/main.zig +++ b/src/main.zig @@ -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"); } diff --git a/src/vaxis.zig b/src/vaxis.zig index 48f9f71..8c9f0fb 100644 --- a/src/vaxis.zig +++ b/src/vaxis.zig @@ -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, + }, + ); + } }; } diff --git a/src/widgets/TextInput.zig b/src/widgets/TextInput.zig index 45b80ce..74966d5 100644 --- a/src/widgets/TextInput.zig +++ b/src/widgets/TextInput.zig @@ -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,