From bbd9184e0057d35cffffbf5fc44f182cc8f79ed2 Mon Sep 17 00:00:00 2001 From: Tim Culverhouse Date: Thu, 30 May 2024 09:41:45 -0500 Subject: [PATCH] loop: add an xev loop implementation This loop adds an xev.File wrapper called TtyWatcher which delivers events to the users callback. Note that this implementation does not handle any of the writes. Writes are always safe in the main thread, so we let users decide how they will schedule those (buffered writers, xev writes, etc) --- build.zig | 8 ++ build.zig.zon | 4 + examples/xev.zig | 67 +++++++++++++ src/xev.zig | 238 +++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 317 insertions(+) create mode 100644 examples/xev.zig create mode 100644 src/xev.zig diff --git a/build.zig b/build.zig index ce5eedb..38ab107 100644 --- a/build.zig +++ b/build.zig @@ -22,6 +22,10 @@ pub fn build(b: *std.Build) void { .optimize = optimize, .target = target, }); + const xev_dep = b.dependency("libxev", .{ + .optimize = optimize, + .target = target, + }); // Module const vaxis_mod = b.addModule("vaxis", .{ @@ -35,6 +39,7 @@ pub fn build(b: *std.Build) void { vaxis_mod.addImport("zigimg", zigimg_dep.module("zigimg")); vaxis_mod.addImport("gap_buffer", gap_buffer_dep.module("gap_buffer")); vaxis_mod.addImport("znvim", znvim_dep.module("znvim")); + vaxis_mod.addImport("xev", xev_dep.module("xev")); // Examples const Example = enum { @@ -45,6 +50,7 @@ pub fn build(b: *std.Build) void { table, text_input, vaxis, + xev, }; const example_option = b.option(Example, "example", "Example to run (default: text_input)") orelse .text_input; const example_step = b.step("example", "Run example"); @@ -58,6 +64,8 @@ pub fn build(b: *std.Build) void { .optimize = optimize, }); example.root_module.addImport("vaxis", vaxis_mod); + example.root_module.addImport("xev", xev_dep.module("xev")); + const example_run = b.addRunArtifact(example); example_step.dependOn(&example_run.step); diff --git a/build.zig.zon b/build.zig.zon index e751428..92b5595 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -19,6 +19,10 @@ .url = "git+https://codeberg.org/dude_the_builder/zg#16735685fcc3410de361ba3411788ad1fb4fe188", .hash = "1220fe9ac5cdb41833d327a78745614e67d472469f8666567bd8cf9f5847a52b1c51", }, + .libxev = .{ + .url = "git+https://github.com/mitchellh/libxev#a284cf851fe2f88f8947c01160c39ff216dacea1", + .hash = "12205b8ea5495b812b9a8943535a7af8da556644d2c1599dc01e1a5ea7aaf59bb2c7", + }, }, .paths = .{ "LICENSE", diff --git a/examples/xev.zig b/examples/xev.zig new file mode 100644 index 0000000..23c2c18 --- /dev/null +++ b/examples/xev.zig @@ -0,0 +1,67 @@ +const std = @import("std"); +const vaxis = @import("vaxis"); +const xev = @import("xev"); +const Cell = vaxis.Cell; + +const Event = union(enum) { + key_press: vaxis.Key, + winsize: vaxis.Winsize, +}; + +pub const panic = vaxis.panic_handler; + +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + defer { + const deinit_status = gpa.deinit(); + //fail test; can't try in defer as defer is executed after we return + if (deinit_status == .leak) { + std.log.err("memory leak", .{}); + } + } + var alloc = gpa.allocator(); + + var tty = try vaxis.Tty.init(); + + var vx = try vaxis.init(alloc, .{}); + defer vx.deinit(alloc, tty.anyWriter()); + + var loop = try xev.Loop.init(.{}); + defer loop.deinit(); + + var vx_loop: vaxis.xev.TtyWatcher(std.mem.Allocator) = undefined; + try vx_loop.init(&tty, &vx, &loop, &alloc, callback); + + try vx.enterAltScreen(tty.anyWriter()); + // send queries asynchronously + try vx.queryTerminalSend(tty.anyWriter()); + + try loop.run(.until_done); + @panic("AHHH"); +} + +fn callback( + ud: ?*std.mem.Allocator, + loop: *xev.Loop, + watcher: *vaxis.xev.TtyWatcher(std.mem.Allocator), + event: vaxis.xev.Event, +) xev.CallbackAction { + switch (event) { + .key_press => |key| { + if (key.matches('c', .{ .ctrl = true })) { + loop.stop(); + return .disarm; + } + }, + .winsize => |ws| watcher.vx.resize(ud.?.*, watcher.tty.anyWriter(), ws) catch @panic("TODO"), + else => {}, + } + std.log.debug("a", .{}); + const win = watcher.vx.window(); + win.clear(); + watcher.vx.render(watcher.tty.anyWriter()) catch { + std.log.err("couldn't render", .{}); + return .disarm; + }; + return .rearm; +} diff --git a/src/xev.zig b/src/xev.zig new file mode 100644 index 0000000..b5ed13d --- /dev/null +++ b/src/xev.zig @@ -0,0 +1,238 @@ +const std = @import("std"); +const xev = @import("xev"); + +const Tty = @import("tty.zig").Tty; +const Winsize = @import("tty.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(.xev); + +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)); + self.winsize_wakeup.notify() catch @panic("TODO"); + } + + fn ttyReadCallback( + ud: ?*Self, + loop: *xev.Loop, + c: *xev.Completion, + _: xev.File, + buf: xev.ReadBuffer, + r: xev.ReadError!usize, + ) xev.CallbackAction { + _ = c; // autofix + const n = r catch @panic("TODO"); + 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) { + const result = self.parser.parse(buf.slice[seq_start..n], null) catch @panic("TODO"); + 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 { + std.log.warn("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 }, + + // 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, + => 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 => { + self.vx.enableDetectedFeatures(self.tty.anyWriter()) catch {}; + }, + } + } + + return .rearm; + } + + fn winsizeCallback( + ud: ?*Self, + l: *xev.Loop, + _: *xev.Completion, + r: xev.Async.WaitError!void, + ) xev.CallbackAction { + _ = r catch @panic("TODO"); + const self = ud orelse @panic("TODO"); + const winsize = Tty.getWinsize(self.tty.fd) catch @panic("TODO"); + return self.callback(self.ud, l, self, .{ .winsize = winsize }); + } + }; +}