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 }); + } + }; +}