diff --git a/examples/main.zig b/examples/main.zig index 2340d8e..727a6d3 100644 --- a/examples/main.zig +++ b/examples/main.zig @@ -93,5 +93,6 @@ pub fn main() !void { const Event = union(enum) { key_press: vaxis.Key, winsize: vaxis.Winsize, + focus_in, foo: u8, }; diff --git a/src/Key.zig b/src/Key.zig index 1bb8ae2..74b95ee 100644 --- a/src/Key.zig +++ b/src/Key.zig @@ -76,6 +76,7 @@ pub const kp_6: u21 = 57405; pub const kp_7: u21 = 57406; pub const kp_8: u21 = 57407; pub const kp_9: u21 = 57408; +pub const kp_begin: u21 = 57427; // TODO: Finish the kitty keys const MAX_UNICODE: u21 = 1_114_112; @@ -91,3 +92,13 @@ pub const f9: u21 = MAX_UNICODE + 9; pub const f10: u21 = MAX_UNICODE + 10; pub const f11: u21 = MAX_UNICODE + 11; pub const f12: u21 = MAX_UNICODE + 12; +pub const up: u21 = MAX_UNICODE + 13; +pub const down: u21 = MAX_UNICODE + 14; +pub const right: u21 = MAX_UNICODE + 15; +pub const left: u21 = MAX_UNICODE + 16; +pub const page_up: u21 = MAX_UNICODE + 17; +pub const page_down: u21 = MAX_UNICODE + 18; +pub const home: u21 = MAX_UNICODE + 19; +pub const end: u21 = MAX_UNICODE + 20; +pub const insert: u21 = MAX_UNICODE + 21; +pub const delete: u21 = MAX_UNICODE + 22; diff --git a/src/Tty.zig b/src/Tty.zig index 096ed7a..3ca08bd 100644 --- a/src/Tty.zig +++ b/src/Tty.zig @@ -126,6 +126,23 @@ pub fn run( var state: State = .ground; + // an intermediate data structure to hold sequence data while we are + // scanning more bytes. This is tailored for input parsing only + const Sequence = struct { + // private indicators are 0x3C-0x3F + private_indicator: ?u8 = null, + // we won't be handling any sequences with more than one intermediate + intermediate: ?u8 = null, + // we should absolutely never have more then 16 params + params: [16]u16 = undefined, + param_idx: usize = 0, + param_buf: [8]u8 = undefined, + param_buf_idx: usize = 0, + sub_state: std.StaticBitSet(16) = std.StaticBitSet(16).initEmpty(), + }; + + var seq: Sequence = .{}; + // Set up fds for polling var pollfds: [2]std.os.pollfd = .{ .{ .fd = self.fd, .events = std.os.POLL.IN, .revents = undefined }, @@ -143,10 +160,15 @@ pub fn run( const n = try os.read(self.fd, &buf); var i: usize = 0; + var start: usize = 0; while (i < n) : (i += 1) { const b = buf[i]; switch (state) { .ground => { + // ground state generates keypresses when parsing input. We + // generally get ascii characters, but anything less than + // 0x20 is a Ctrl+ keypress. We map these to lowercase + // ascii characters when we can const key: ?Key = switch (b) { 0x00 => Key{ .codepoint = '@', .mods = .{ .ctrl = true } }, 0x01...0x1A => Key{ .codepoint = b + 0x60, .mods = .{ .ctrl = true } }, @@ -173,7 +195,194 @@ pub fn run( } } }, - .escape => state = .ground, + .escape => { + seq = .{}; + start = i; + switch (b) { + 0x4F => state = .ss3, + 0x50 => state = .dcs, + 0x58 => state = .sos, + 0x5B => state = .csi, + 0x5D => state = .osc, + 0x5E => state = .pm, + 0x5F => state = .apc, + else => { + // Anything else is an "alt + " keypress + if (@hasField(EventType, "key_press")) { + vx.postEvent(.{ + .key_press = .{ + .codepoint = b, + .mods = .{ .alt = true }, + }, + }); + } + state = .ground; + }, + } + }, + .ss3 => { + const key: ?Key = switch (b) { + 'A' => .{ .codepoint = Key.up }, + 'B' => .{ .codepoint = Key.down }, + 'C' => .{ .codepoint = Key.right }, + 'D' => .{ .codepoint = Key.left }, + '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 => blk: { + log.warn("unhandled ss3: {x}", .{b}); + break :blk null; + }, + }; + if (key) |k| { + if (@hasField(EventType, "key_press")) { + vx.postEvent(.{ .key_press = k }); + } + } + state = .ground; + }, + .csi => { + switch (b) { + // c0 controls. we ignore these even though we should + // "execute" them. This isn't seen in practice + 0x00...0x1F => {}, + // intermediates. we only handle one. technically there + // can be more + 0x20...0x2F => seq.intermediate = b, + 0x30...0x39 => { + seq.param_buf[seq.param_buf_idx] = b; + seq.param_buf_idx += 1; + }, + // private indicators. These come before any params ('?') + 0x3C...0x3F => seq.private_indicator = b, + ';' => { + if (seq.param_buf_idx == 0) { + // empty param. default it to 1 + seq.params[seq.param_idx] = 1; + seq.param_idx += 1; + } else { + const p = try std.fmt.parseUnsigned(u16, seq.param_buf[0..seq.param_buf_idx], 10); + seq.param_buf_idx = 0; + seq.params[seq.param_idx] = p; + seq.param_idx += 1; + } + }, + ':' => { + if (seq.param_buf_idx == 0) { + // empty param. default it to 1 + seq.params[seq.param_idx] = 1; + seq.param_idx += 1; + // Set the *next* param as a subparam + seq.sub_state.set(seq.param_idx); + } else { + const p = try std.fmt.parseUnsigned(u16, seq.param_buf[0..seq.param_buf_idx], 10); + seq.param_buf_idx = 0; + seq.params[seq.param_idx] = p; + seq.param_idx += 1; + // Set the *next* param as a subparam + seq.sub_state.set(seq.param_idx); + } + }, + 0x40...0xFF => { + // dispatch our sequence + state = .ground; + const codepoint: u21 = switch (b) { + '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, + '~' => blk: { + // The first param will define this + // codepoint + if (seq.param_idx < 1) { + log.warn("unhandled csi: CSI {s}", .{buf[start + 1 .. i + 1]}); + continue; + } + switch (seq.params[0]) { + 2 => break :blk Key.insert, + 3 => break :blk Key.delete, + 5 => break :blk Key.page_up, + 6 => break :blk Key.page_down, + 7 => break :blk Key.home, + 8 => break :blk Key.end, + 11 => break :blk Key.f1, + 12 => break :blk Key.f2, + 13 => break :blk Key.f3, + 14 => break :blk Key.f4, + 15 => break :blk Key.f5, + 17 => break :blk Key.f6, + 18 => break :blk Key.f7, + 19 => break :blk Key.f8, + 20 => break :blk Key.f9, + 21 => break :blk Key.f10, + 23 => break :blk Key.f11, + 24 => break :blk Key.f12, + 200 => { + // TODO: bracketed paste + continue; + }, + 201 => { + // TODO: bracketed paste + continue; + }, + 57427 => break :blk Key.kp_begin, + else => { + log.warn("unhandled csi: CSI {s}", .{buf[start + 1 .. i + 1]}); + continue; + }, + } + }, + 'u' => blk: { + if (seq.private_indicator) |_| { + // response to our kitty query + // TODO: kitty query handling + log.warn("unhandled csi: CSI {s}", .{buf[start + 1 .. i + 1]}); + continue; + } + if (seq.param_idx == 0) { + log.warn("unhandled csi: CSI {s}", .{buf[start + 1 .. i + 1]}); + continue; + } + // In any csi u encoding, the codepoint + // directly maps to our keypoint definitions + break :blk seq.params[0]; + }, + + 'I' => { // focus in + if (@hasField(EventType, "focus_in")) { + vx.postEvent(.focus_in); + } + continue; + }, + 'O' => { // focus out + if (@hasField(EventType, "focus_out")) { + vx.postEvent(.focus_out); + } + continue; + }, + else => { + log.warn("unhandled csi: CSI {s}", .{buf[start + 1 .. i + 1]}); + continue; + }, + }; + + const key: Key = .{ .codepoint = codepoint }; + if (@hasField(EventType, "key_press")) { + vx.postEvent(.{ .key_press = key }); + } + }, + } + }, else => {}, } } diff --git a/src/ctlseqs.zig b/src/ctlseqs.zig index 59f6095..d75827b 100644 --- a/src/ctlseqs.zig +++ b/src/ctlseqs.zig @@ -2,9 +2,15 @@ pub const primary_device_attrs = "\x1b[c"; pub const tertiary_device_attrs = "\x1b[=c"; pub const xtversion = "\x1b[>0q"; +pub const decrqm_focus = "\x1b[?1004$p"; +pub const decrqm_sync = "\x1b[?2026$p"; +pub const decrqm_unicode = "\x1b[?2027$p"; +pub const decrqm_color_theme = "\x1b[?2031$p"; +pub const csi_u_query = "\x1b[?u"; +pub const kitty_graphics_query = "\x1b_Gi=1,a=q\x1b\\"; +pub const sixel_geometry_query = "\x1b[?2;1;0S"; // Key encoding -pub const csi_u = "\x1b[?u"; pub const csi_u_push = "\x1b[>{d}u"; pub const csi_u_pop = "\x1b[