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:
parent
04f1586e8f
commit
bbd9184e00
4 changed files with 317 additions and 0 deletions
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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
67
examples/xev.zig
Normal 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
238
src/xev.zig
Normal 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 });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
Loading…
Reference in a new issue