libvaxis/src/xev.zig

274 lines
9.2 KiB
Zig
Raw Normal View History

const std = @import("std");
const xev = @import("xev");
const Tty = @import("main.zig").Tty;
const Winsize = @import("main.zig").Winsize;
const Vaxis = @import("Vaxis.zig");
const Parser = @import("Parser.zig");
const Key = @import("Key.zig");
const Mouse = @import("Mouse.zig");
const Color = @import("Cell.zig").Color;
const log = std.log.scoped(.tty_watcher);
pub const Event = union(enum) {
key_press: Key,
key_release: Key,
mouse: Mouse,
focus_in,
focus_out,
paste_start, // bracketed paste start
paste_end, // bracketed paste end
paste: []const u8, // osc 52 paste, caller must free
color_report: Color.Report, // osc 4, 10, 11, 12 response
color_scheme: Color.Scheme,
winsize: Winsize,
};
pub fn TtyWatcher(comptime Userdata: type) type {
return struct {
const Self = @This();
file: xev.File,
tty: *Tty,
read_buf: [4096]u8,
read_buf_start: usize,
read_cmp: xev.Completion,
winsize_wakeup: xev.Async,
winsize_cmp: xev.Completion,
callback: *const fn (
ud: ?*Userdata,
loop: *xev.Loop,
watcher: *Self,
event: Event,
) xev.CallbackAction,
ud: ?*Userdata,
vx: *Vaxis,
parser: Parser,
pub fn init(
self: *Self,
tty: *Tty,
vaxis: *Vaxis,
loop: *xev.Loop,
userdata: ?*Userdata,
callback: *const fn (
ud: ?*Userdata,
loop: *xev.Loop,
watcher: *Self,
event: Event,
) xev.CallbackAction,
) !void {
self.* = .{
.tty = tty,
.file = xev.File.initFd(tty.fd),
.read_buf = undefined,
.read_buf_start = 0,
.read_cmp = .{},
.winsize_wakeup = try xev.Async.init(),
.winsize_cmp = .{},
.callback = callback,
.ud = userdata,
.vx = vaxis,
.parser = .{ .grapheme_data = &vaxis.unicode.grapheme_data },
};
self.file.read(
loop,
&self.read_cmp,
.{ .slice = &self.read_buf },
Self,
self,
Self.ttyReadCallback,
);
self.winsize_wakeup.wait(
loop,
&self.winsize_cmp,
Self,
self,
winsizeCallback,
);
const handler: Tty.SignalHandler = .{
.context = self,
.callback = Self.signalCallback,
};
try Tty.notifyWinsize(handler);
const winsize = try Tty.getWinsize(self.tty.fd);
_ = self.callback(self.ud, loop, self, .{ .winsize = winsize });
}
fn signalCallback(ptr: *anyopaque) void {
const self: *Self = @ptrCast(@alignCast(ptr));
2024-05-30 17:06:28 +02:00
self.winsize_wakeup.notify() catch |err| {
log.warn("couldn't wake up winsize callback: {}", .{err});
};
}
fn ttyReadCallback(
ud: ?*Self,
loop: *xev.Loop,
c: *xev.Completion,
_: xev.File,
buf: xev.ReadBuffer,
r: xev.ReadError!usize,
) xev.CallbackAction {
2024-05-30 17:06:28 +02:00
const n = r catch |err| {
log.err("read error: {}", .{err});
return .disarm;
};
const self = ud orelse unreachable;
// reset read start state
self.read_buf_start = 0;
var seq_start: usize = 0;
parse_loop: while (seq_start < n) {
2024-05-30 17:06:28 +02:00
const result = self.parser.parse(buf.slice[seq_start..n], null) catch |err| {
log.err("couldn't parse input: {}", .{err});
return .disarm;
};
if (result.n == 0) {
// copy the read to the beginning. We don't use memcpy because
// this could be overlapping, and it's also rare
const initial_start = seq_start;
while (seq_start < n) : (seq_start += 1) {
self.read_buf[seq_start - initial_start] = self.read_buf[seq_start];
}
self.read_buf_start = seq_start - initial_start + 1;
return .rearm;
}
seq_start += n;
const event_inner = result.event orelse {
log.debug("unknown event: {s}", .{self.read_buf[seq_start - n + 1 .. seq_start]});
continue :parse_loop;
};
// Capture events we want to bubble up
const event: ?Event = switch (event_inner) {
.key_press => |key| .{ .key_press = key },
.key_release => |key| .{ .key_release = key },
.mouse => |mouse| .{ .mouse = mouse },
.focus_in => .focus_in,
.focus_out => .focus_out,
.paste_start => .paste_start,
.paste_end => .paste_end,
.paste => |paste| .{ .paste = paste },
.color_report => |report| .{ .color_report = report },
.color_scheme => |scheme| .{ .color_scheme = scheme },
.winsize => |ws| .{ .winsize = ws },
// capability events which we handle below
.cap_kitty_keyboard,
.cap_kitty_graphics,
.cap_rgb,
.cap_unicode,
.cap_sgr_pixels,
.cap_color_scheme_updates,
.cap_da1,
=> null, // handled below
};
if (event) |ev| {
const action = self.callback(self.ud, loop, self, ev);
switch (action) {
.disarm => return .disarm,
else => continue :parse_loop,
}
}
switch (event_inner) {
.key_press,
.key_release,
.mouse,
.focus_in,
.focus_out,
.paste_start,
.paste_end,
.paste,
.color_report,
.color_scheme,
.winsize,
=> unreachable, // handled above
.cap_kitty_keyboard => {
log.info("kitty keyboard capability detected", .{});
self.vx.caps.kitty_keyboard = true;
},
.cap_kitty_graphics => {
if (!self.vx.caps.kitty_graphics) {
log.info("kitty graphics capability detected", .{});
self.vx.caps.kitty_graphics = true;
}
},
.cap_rgb => {
log.info("rgb capability detected", .{});
self.vx.caps.rgb = true;
},
.cap_unicode => {
log.info("unicode capability detected", .{});
self.vx.caps.unicode = .unicode;
self.vx.screen.width_method = .unicode;
},
.cap_sgr_pixels => {
log.info("pixel mouse capability detected", .{});
self.vx.caps.sgr_pixels = true;
},
.cap_color_scheme_updates => {
log.info("color_scheme_updates capability detected", .{});
self.vx.caps.color_scheme_updates = true;
},
.cap_da1 => {
2024-05-30 17:06:28 +02:00
self.vx.enableDetectedFeatures(self.tty.anyWriter()) catch |err| {
log.err("couldn't enable features: {}", .{err});
};
},
}
}
self.file.read(
loop,
c,
.{ .slice = &self.read_buf },
Self,
self,
Self.ttyReadCallback,
);
return .disarm;
}
fn winsizeCallback(
ud: ?*Self,
l: *xev.Loop,
c: *xev.Completion,
r: xev.Async.WaitError!void,
) xev.CallbackAction {
2024-05-30 17:06:28 +02:00
_ = r catch |err| {
log.err("async error: {}", .{err});
return .disarm;
};
const self = ud orelse unreachable; // no userdata
const winsize = Tty.getWinsize(self.tty.fd) catch |err| {
log.err("couldn't get winsize: {}", .{err});
return .disarm;
};
const ret = self.callback(self.ud, l, self, .{ .winsize = winsize });
if (ret == .disarm) return .disarm;
self.winsize_wakeup.wait(
l,
c,
Self,
self,
winsizeCallback,
);
return .disarm;
}
};
}