parser: more progress on CSI parsing
Add additional CSI parsing for keys Signed-off-by: Tim Culverhouse <tim@timculverhouse.com>
This commit is contained in:
parent
462a303903
commit
93d9ead99c
5 changed files with 259 additions and 2 deletions
|
@ -93,5 +93,6 @@ pub fn main() !void {
|
||||||
const Event = union(enum) {
|
const Event = union(enum) {
|
||||||
key_press: vaxis.Key,
|
key_press: vaxis.Key,
|
||||||
winsize: vaxis.Winsize,
|
winsize: vaxis.Winsize,
|
||||||
|
focus_in,
|
||||||
foo: u8,
|
foo: u8,
|
||||||
};
|
};
|
||||||
|
|
11
src/Key.zig
11
src/Key.zig
|
@ -76,6 +76,7 @@ pub const kp_6: u21 = 57405;
|
||||||
pub const kp_7: u21 = 57406;
|
pub const kp_7: u21 = 57406;
|
||||||
pub const kp_8: u21 = 57407;
|
pub const kp_8: u21 = 57407;
|
||||||
pub const kp_9: u21 = 57408;
|
pub const kp_9: u21 = 57408;
|
||||||
|
pub const kp_begin: u21 = 57427;
|
||||||
// TODO: Finish the kitty keys
|
// TODO: Finish the kitty keys
|
||||||
|
|
||||||
const MAX_UNICODE: u21 = 1_114_112;
|
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 f10: u21 = MAX_UNICODE + 10;
|
||||||
pub const f11: u21 = MAX_UNICODE + 11;
|
pub const f11: u21 = MAX_UNICODE + 11;
|
||||||
pub const f12: u21 = MAX_UNICODE + 12;
|
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;
|
||||||
|
|
211
src/Tty.zig
211
src/Tty.zig
|
@ -126,6 +126,23 @@ pub fn run(
|
||||||
|
|
||||||
var state: State = .ground;
|
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
|
// Set up fds for polling
|
||||||
var pollfds: [2]std.os.pollfd = .{
|
var pollfds: [2]std.os.pollfd = .{
|
||||||
.{ .fd = self.fd, .events = std.os.POLL.IN, .revents = undefined },
|
.{ .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);
|
const n = try os.read(self.fd, &buf);
|
||||||
var i: usize = 0;
|
var i: usize = 0;
|
||||||
|
var start: usize = 0;
|
||||||
while (i < n) : (i += 1) {
|
while (i < n) : (i += 1) {
|
||||||
const b = buf[i];
|
const b = buf[i];
|
||||||
switch (state) {
|
switch (state) {
|
||||||
.ground => {
|
.ground => {
|
||||||
|
// ground state generates keypresses when parsing input. We
|
||||||
|
// generally get ascii characters, but anything less than
|
||||||
|
// 0x20 is a Ctrl+<c> keypress. We map these to lowercase
|
||||||
|
// ascii characters when we can
|
||||||
const key: ?Key = switch (b) {
|
const key: ?Key = switch (b) {
|
||||||
0x00 => Key{ .codepoint = '@', .mods = .{ .ctrl = true } },
|
0x00 => Key{ .codepoint = '@', .mods = .{ .ctrl = true } },
|
||||||
0x01...0x1A => Key{ .codepoint = b + 0x60, .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 + <b>" 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 => {},
|
else => {},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,9 +2,15 @@
|
||||||
pub const primary_device_attrs = "\x1b[c";
|
pub const primary_device_attrs = "\x1b[c";
|
||||||
pub const tertiary_device_attrs = "\x1b[=c";
|
pub const tertiary_device_attrs = "\x1b[=c";
|
||||||
pub const xtversion = "\x1b[>0q";
|
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
|
// Key encoding
|
||||||
pub const csi_u = "\x1b[?u";
|
|
||||||
pub const csi_u_push = "\x1b[>{d}u";
|
pub const csi_u_push = "\x1b[>{d}u";
|
||||||
pub const csi_u_pop = "\x1b[<u";
|
pub const csi_u_pop = "\x1b[<u";
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,7 @@ const Style = @import("cell.zig").Style;
|
||||||
/// - `key_press: Key`, for key press events
|
/// - `key_press: Key`, for key press events
|
||||||
/// - `winsize: Winsize`, for resize events. Must call app.resize when receiving
|
/// - `winsize: Winsize`, for resize events. Must call app.resize when receiving
|
||||||
/// this event
|
/// this event
|
||||||
|
/// - `focus_in` and `focus_out` for focus events
|
||||||
pub fn Vaxis(comptime T: type) type {
|
pub fn Vaxis(comptime T: type) type {
|
||||||
return struct {
|
return struct {
|
||||||
const Self = @This();
|
const Self = @This();
|
||||||
|
@ -149,6 +150,35 @@ pub fn Vaxis(comptime T: type) type {
|
||||||
self.alt_screen = false;
|
self.alt_screen = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// write queries to the terminal to determine capabilities. Individual
|
||||||
|
/// capabilities will be delivered to the client and possibly intercepted by
|
||||||
|
/// Vaxis to enable features
|
||||||
|
pub fn queryTerminal(self: *Self) !void {
|
||||||
|
var tty = self.tty orelse return;
|
||||||
|
|
||||||
|
const colorterm = std.os.getenv("COLORTERM") orelse "";
|
||||||
|
if (std.mem.eql(u8, colorterm, "truecolor" or
|
||||||
|
std.mem.eql(u8, colorterm, "24bit")))
|
||||||
|
{
|
||||||
|
// TODO: Notify rgb support
|
||||||
|
}
|
||||||
|
|
||||||
|
const writer = tty.buffered_writer.writer();
|
||||||
|
_ = try writer.write(ctlseqs.decrqm_focus);
|
||||||
|
_ = try writer.write(ctlseqs.decrqm_sync);
|
||||||
|
_ = try writer.write(ctlseqs.decrqm_unicode);
|
||||||
|
_ = try writer.write(ctlseqs.decrqm_color_theme);
|
||||||
|
_ = try writer.write(ctlseqs.xtversion);
|
||||||
|
_ = try writer.write(ctlseqs.csi_u_query);
|
||||||
|
_ = try writer.write(ctlseqs.kitty_graphics_query);
|
||||||
|
_ = try writer.write(ctlseqs.sixel_geometry_query);
|
||||||
|
|
||||||
|
// TODO: XTGETTCAP queries ("RGB", "Smulx")
|
||||||
|
|
||||||
|
_ = try writer.write(ctlseqs.primary_device_attrs);
|
||||||
|
try writer.flush();
|
||||||
|
}
|
||||||
|
|
||||||
/// draws the screen to the terminal
|
/// draws the screen to the terminal
|
||||||
pub fn render(self: *Self) !void {
|
pub fn render(self: *Self) !void {
|
||||||
var tty = self.tty orelse return;
|
var tty = self.tty orelse return;
|
||||||
|
|
Loading…
Reference in a new issue