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)
This commit is contained in:
Tim Culverhouse 2024-05-30 09:41:45 -05:00
parent 04f1586e8f
commit bbd9184e00
4 changed files with 317 additions and 0 deletions

View file

@ -22,6 +22,10 @@ pub fn build(b: *std.Build) void {
.optimize = optimize, .optimize = optimize,
.target = target, .target = target,
}); });
const xev_dep = b.dependency("libxev", .{
.optimize = optimize,
.target = target,
});
// Module // Module
const vaxis_mod = b.addModule("vaxis", .{ 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("zigimg", zigimg_dep.module("zigimg"));
vaxis_mod.addImport("gap_buffer", gap_buffer_dep.module("gap_buffer")); vaxis_mod.addImport("gap_buffer", gap_buffer_dep.module("gap_buffer"));
vaxis_mod.addImport("znvim", znvim_dep.module("znvim")); vaxis_mod.addImport("znvim", znvim_dep.module("znvim"));
vaxis_mod.addImport("xev", xev_dep.module("xev"));
// Examples // Examples
const Example = enum { const Example = enum {
@ -45,6 +50,7 @@ pub fn build(b: *std.Build) void {
table, table,
text_input, text_input,
vaxis, vaxis,
xev,
}; };
const example_option = b.option(Example, "example", "Example to run (default: text_input)") orelse .text_input; const example_option = b.option(Example, "example", "Example to run (default: text_input)") orelse .text_input;
const example_step = b.step("example", "Run example"); const example_step = b.step("example", "Run example");
@ -58,6 +64,8 @@ pub fn build(b: *std.Build) void {
.optimize = optimize, .optimize = optimize,
}); });
example.root_module.addImport("vaxis", vaxis_mod); example.root_module.addImport("vaxis", vaxis_mod);
example.root_module.addImport("xev", xev_dep.module("xev"));
const example_run = b.addRunArtifact(example); const example_run = b.addRunArtifact(example);
example_step.dependOn(&example_run.step); example_step.dependOn(&example_run.step);

View file

@ -19,6 +19,10 @@
.url = "git+https://codeberg.org/dude_the_builder/zg#16735685fcc3410de361ba3411788ad1fb4fe188", .url = "git+https://codeberg.org/dude_the_builder/zg#16735685fcc3410de361ba3411788ad1fb4fe188",
.hash = "1220fe9ac5cdb41833d327a78745614e67d472469f8666567bd8cf9f5847a52b1c51", .hash = "1220fe9ac5cdb41833d327a78745614e67d472469f8666567bd8cf9f5847a52b1c51",
}, },
.libxev = .{
.url = "git+https://github.com/mitchellh/libxev#a284cf851fe2f88f8947c01160c39ff216dacea1",
.hash = "12205b8ea5495b812b9a8943535a7af8da556644d2c1599dc01e1a5ea7aaf59bb2c7",
},
}, },
.paths = .{ .paths = .{
"LICENSE", "LICENSE",

67
examples/xev.zig Normal file
View file

@ -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;
}

238
src/xev.zig Normal file
View file

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