From 6d4c180e3186fe02605a21a3888a2f32f7746df5 Mon Sep 17 00:00:00 2001 From: Tim Culverhouse Date: Tue, 11 Jun 2024 14:00:37 -0500 Subject: [PATCH] widgets(terminal): implement legacy key encoding --- examples/vt.zig | 2 +- src/widgets/terminal/Screen.zig | 2 + src/widgets/terminal/Terminal.zig | 21 +--- src/widgets/terminal/key.zig | 172 ++++++++++++++++++++++++++++++ 4 files changed, 178 insertions(+), 19 deletions(-) create mode 100644 src/widgets/terminal/key.zig diff --git a/examples/vt.zig b/examples/vt.zig index 855ae5e..853ff5c 100644 --- a/examples/vt.zig +++ b/examples/vt.zig @@ -82,8 +82,8 @@ pub fn main() !void { } const win = vx.window(); - win.clear(); win.hideCursor(); + win.clear(); const child = win.child(.{ .x_off = 4, .y_off = 2, diff --git a/src/widgets/terminal/Screen.zig b/src/widgets/terminal/Screen.zig index 247501c..e040edb 100644 --- a/src/widgets/terminal/Screen.zig +++ b/src/widgets/terminal/Screen.zig @@ -90,6 +90,8 @@ buf: []Cell = undefined, cursor: Cursor = .{}, +csi_u_flags: vaxis.Key.KittyFlags = @bitCast(@as(u5, 0)), + /// sets each cell to the default cell pub fn init(alloc: std.mem.Allocator, w: usize, h: usize) !Screen { var screen = Screen{ diff --git a/src/widgets/terminal/Terminal.zig b/src/widgets/terminal/Terminal.zig index d2537c2..abf292c 100644 --- a/src/widgets/terminal/Terminal.zig +++ b/src/widgets/terminal/Terminal.zig @@ -14,6 +14,7 @@ const DisplayWidth = @import("DisplayWidth"); const Key = vaxis.Key; const Queue = vaxis.Queue(Event, 16); const code_point = @import("code_point"); +const key = @import("key.zig"); pub const Event = union(enum) { exited, @@ -207,7 +208,7 @@ pub fn draw(self: *Terminal, win: vaxis.Window) !void { if (self.mode.cursor) { win.setCursorShape(self.front_screen.cursor.shape); win.showCursor(self.front_screen.cursor.col, self.front_screen.cursor.row); - } else win.hideCursor(); + } } pub fn tryEvent(self: *Terminal) ?Event { @@ -216,7 +217,7 @@ pub fn tryEvent(self: *Terminal) ?Event { pub fn update(self: *Terminal, event: InputEvent) !void { switch (event) { - .key_press => |key| try self.encodeKey(key, true), + .key_press => |k| try key.encode(self.anyWriter(), k, true, self.back_screen.csi_u_flags), } } @@ -698,22 +699,6 @@ pub fn setMode(self: *Terminal, mode: u16, val: bool) void { } } -pub fn encodeKey(self: *Terminal, key: vaxis.Key, press: bool) !void { - switch (press) { - true => { - if (key.text) |text| { - try self.anyWriter().writeAll(text); - return; - } - switch (key.codepoint) { - 0x00...0x7F => try self.anyWriter().writeByte(@intCast(key.codepoint)), - else => {}, - } - }, - false => {}, - } -} - pub fn carriageReturn(self: *Terminal) void { self.back_screen.cursor.pending_wrap = false; self.back_screen.cursor.col = if (self.mode.origin) diff --git a/src/widgets/terminal/key.zig b/src/widgets/terminal/key.zig new file mode 100644 index 0000000..7858054 --- /dev/null +++ b/src/widgets/terminal/key.zig @@ -0,0 +1,172 @@ +const std = @import("std"); +const vaxis = @import("../../main.zig"); + +pub fn encode( + writer: std.io.AnyWriter, + key: vaxis.Key, + press: bool, + kitty_flags: vaxis.Key.KittyFlags, +) !void { + const flags: u5 = @bitCast(kitty_flags); + switch (press) { + true => { + switch (flags) { + 0 => try legacy(writer, key), + else => unreachable, // TODO: kitty encodings + } + }, + false => {}, + } +} + +fn legacy(writer: std.io.AnyWriter, key: vaxis.Key) !void { + // If we have text, we always write it directly + if (key.text) |text| { + try writer.writeAll(text); + return; + } + + const shift = 0b00000001; + const alt = 0b00000010; + const ctrl = 0b00000100; + + const effective_mods: u8 = blk: { + const mods: u8 = @bitCast(key.mods); + break :blk mods & (shift | alt | ctrl); + }; + + // If we have no mods and an ascii byte, write it directly + if (effective_mods == 0 and key.codepoint <= 0x7F) { + const b: u8 = @truncate(key.codepoint); + try writer.writeByte(b); + return; + } + + // If we are lowercase ascii and ctrl, we map to a control byte + if (effective_mods == ctrl and key.codepoint >= 'a' and key.codepoint <= 'z') { + const b: u8 = @truncate(key.codepoint); + try writer.writeByte(b -| 0x60); + return; + } + + // If we are printable ascii + alt + if (effective_mods == alt and key.codepoint >= ' ' and key.codepoint < 0x7F) { + const b: u8 = @truncate(key.codepoint); + try writer.print("\x1b{c}", .{b}); + return; + } + + // If we are ctrl + alt + lowercase ascii + if (effective_mods == (ctrl | alt) and key.codepoint >= 'a' and key.codepoint <= 'z') { + // convert to control sequence + try writer.print("\x1b{d}", .{key.codepoint - 0x60}); + } + + const def = switch (key.codepoint) { + vaxis.Key.escape => escape, + vaxis.Key.enter, + vaxis.Key.kp_enter, + => enter, + vaxis.Key.tab => tab, + vaxis.Key.backspace => backspace, + vaxis.Key.insert, + vaxis.Key.kp_insert, + => insert, + vaxis.Key.delete, + vaxis.Key.kp_delete, + => delete, + vaxis.Key.left, + vaxis.Key.kp_left, + => left, + vaxis.Key.right, + vaxis.Key.kp_right, + => right, + vaxis.Key.up, + vaxis.Key.kp_up, + => up, + vaxis.Key.down, + vaxis.Key.kp_down, + => down, + vaxis.Key.page_up, + vaxis.Key.kp_page_up, + => page_up, + vaxis.Key.page_down, + vaxis.Key.kp_page_down, + => page_down, + vaxis.Key.home, + vaxis.Key.kp_home, + => home, + vaxis.Key.end, + vaxis.Key.kp_end, + => end, + vaxis.Key.f1 => f1, + vaxis.Key.f2 => f2, + vaxis.Key.f3 => f3_legacy, + vaxis.Key.f4 => f4, + vaxis.Key.f5 => f5, + vaxis.Key.f6 => f6, + vaxis.Key.f7 => f7, + vaxis.Key.f8 => f8, + vaxis.Key.f9 => f9, + vaxis.Key.f10 => f10, + vaxis.Key.f11 => f11, + vaxis.Key.f12 => f12, + else => return, // TODO: more keys + }; + + switch (effective_mods) { + 0 => { + if (def.number == 1) + switch (key.codepoint) { + vaxis.Key.f1, + vaxis.Key.f2, + vaxis.Key.f3, + vaxis.Key.f4, + => try writer.print("\x1bO{c}", .{def.suffix}), + else => try writer.print("\x1b[{c}", .{def.suffix}), + } + else + try writer.print("\x1b[{d}{c}", .{ def.number, def.suffix }); + }, + else => try writer.print("\x1b[{d};{d}{c}", .{ def.number, effective_mods + 1, def.suffix }), + } +} + +const Definition = struct { + number: u21, + suffix: u8, +}; + +const escape: Definition = .{ .number = 27, .suffix = 'u' }; +const enter: Definition = .{ .number = 13, .suffix = 'u' }; +const tab: Definition = .{ .number = 9, .suffix = 'u' }; +const backspace: Definition = .{ .number = 127, .suffix = 'u' }; +const insert: Definition = .{ .number = 2, .suffix = '~' }; +const delete: Definition = .{ .number = 3, .suffix = '~' }; +const left: Definition = .{ .number = 1, .suffix = 'D' }; +const right: Definition = .{ .number = 1, .suffix = 'C' }; +const up: Definition = .{ .number = 1, .suffix = 'A' }; +const down: Definition = .{ .number = 1, .suffix = 'B' }; +const page_up: Definition = .{ .number = 5, .suffix = '~' }; +const page_down: Definition = .{ .number = 6, .suffix = '~' }; +const home: Definition = .{ .number = 1, .suffix = 'H' }; +const end: Definition = .{ .number = 1, .suffix = 'F' }; +const caps_lock: Definition = .{ .number = 57358, .suffix = 'u' }; +const scroll_lock: Definition = .{ .number = 57359, .suffix = 'u' }; +const num_lock: Definition = .{ .number = 57360, .suffix = 'u' }; +const print_screen: Definition = .{ .number = 57361, .suffix = 'u' }; +const pause: Definition = .{ .number = 57362, .suffix = 'u' }; +const menu: Definition = .{ .number = 57363, .suffix = 'u' }; +const f1: Definition = .{ .number = 1, .suffix = 'P' }; +const f2: Definition = .{ .number = 1, .suffix = 'Q' }; +const f3: Definition = .{ .number = 13, .suffix = '~' }; +const f3_legacy: Definition = .{ .number = 1, .suffix = 'R' }; +const f4: Definition = .{ .number = 1, .suffix = 'S' }; +const f5: Definition = .{ .number = 15, .suffix = '~' }; +const f6: Definition = .{ .number = 17, .suffix = '~' }; +const f7: Definition = .{ .number = 18, .suffix = '~' }; +const f8: Definition = .{ .number = 19, .suffix = '~' }; +const f9: Definition = .{ .number = 20, .suffix = '~' }; +const f10: Definition = .{ .number = 21, .suffix = '~' }; +const f11: Definition = .{ .number = 23, .suffix = '~' }; +const f12: Definition = .{ .number = 24, .suffix = '~' };