59abd7d7d4
Refactor to move the tty out of the vaxis struct. All vaxis writes now take an io.AnyWriter
241 lines
9.6 KiB
Zig
241 lines
9.6 KiB
Zig
const std = @import("std");
|
|
const builtin = @import("builtin");
|
|
|
|
const grapheme = @import("grapheme");
|
|
|
|
const GraphemeCache = @import("GraphemeCache.zig");
|
|
const Parser = @import("Parser.zig");
|
|
const Queue = @import("queue.zig").Queue;
|
|
const tty = @import("tty.zig");
|
|
const Tty = tty.Tty;
|
|
const Vaxis = @import("Vaxis.zig");
|
|
|
|
pub fn Loop(comptime T: type) type {
|
|
return struct {
|
|
const Self = @This();
|
|
|
|
const Event = T;
|
|
|
|
const log = std.log.scoped(.loop);
|
|
|
|
tty: *Tty,
|
|
vaxis: *Vaxis,
|
|
|
|
queue: Queue(T, 512) = .{},
|
|
thread: ?std.Thread = null,
|
|
should_quit: bool = false,
|
|
|
|
/// Initialize the event loop. This is an intrusive init so that we have
|
|
/// a stable pointer to register signal callbacks with posix TTYs
|
|
pub fn init(self: *Self) !void {
|
|
switch (builtin.os.tag) {
|
|
.windows => @compileError("windows not supported"),
|
|
else => {
|
|
const handler: Tty.SignalHandler = .{
|
|
.context = self,
|
|
.callback = Self.winsizeCallback,
|
|
};
|
|
try Tty.notifyWinsize(handler);
|
|
},
|
|
}
|
|
}
|
|
|
|
/// spawns the input thread to read input from the tty
|
|
pub fn start(self: *Self) !void {
|
|
if (self.thread) |_| return;
|
|
self.thread = try std.Thread.spawn(.{}, Self.ttyRun, .{
|
|
self,
|
|
&self.vaxis.unicode.grapheme_data,
|
|
self.vaxis.opts.system_clipboard_allocator,
|
|
});
|
|
}
|
|
|
|
/// stops reading from the tty and returns it to it's initial state
|
|
pub fn stop(self: *Self) void {
|
|
self.should_quit = true;
|
|
// trigger a read
|
|
self.vaxis.deviceStatusReport(self.tty.anyWriter()) catch {};
|
|
|
|
if (self.thread) |thread| {
|
|
thread.join();
|
|
self.thread = null;
|
|
}
|
|
}
|
|
|
|
/// returns the next available event, blocking until one is available
|
|
pub fn nextEvent(self: *Self) T {
|
|
return self.queue.pop();
|
|
}
|
|
|
|
/// blocks until an event is available. Useful when your application is
|
|
/// operating on a poll + drain architecture (see tryEvent)
|
|
pub fn pollEvent(self: *Self) void {
|
|
self.queue.poll();
|
|
}
|
|
|
|
/// returns an event if one is available, otherwise null. Non-blocking.
|
|
pub fn tryEvent(self: *Self) ?T {
|
|
return self.queue.tryPop();
|
|
}
|
|
|
|
/// posts an event into the event queue. Will block if there is not
|
|
/// capacity for the event
|
|
pub fn postEvent(self: *Self, event: T) void {
|
|
self.queue.push(event);
|
|
}
|
|
|
|
pub fn tryPostEvent(self: *Self, event: T) bool {
|
|
return self.queue.tryPush(event);
|
|
}
|
|
|
|
pub fn winsizeCallback(ptr: *anyopaque) void {
|
|
const self: *Self = @ptrCast(@alignCast(ptr));
|
|
|
|
const winsize = Tty.getWinsize(self.tty.fd) catch return;
|
|
if (@hasField(Event, "winsize")) {
|
|
self.postEvent(.{ .winsize = winsize });
|
|
}
|
|
}
|
|
|
|
/// read input from the tty. This is run in a separate thread
|
|
fn ttyRun(
|
|
self: *Self,
|
|
grapheme_data: *const grapheme.GraphemeData,
|
|
paste_allocator: ?std.mem.Allocator,
|
|
) !void {
|
|
// get our initial winsize
|
|
const winsize = try Tty.getWinsize(self.tty.fd);
|
|
if (@hasField(Event, "winsize")) {
|
|
self.postEvent(.{ .winsize = winsize });
|
|
}
|
|
|
|
// initialize a grapheme cache
|
|
var cache: GraphemeCache = .{};
|
|
|
|
var parser: Parser = .{
|
|
.grapheme_data = grapheme_data,
|
|
};
|
|
|
|
// initialize the read buffer
|
|
var buf: [1024]u8 = undefined;
|
|
var read_start: usize = 0;
|
|
// read loop
|
|
while (!self.should_quit) {
|
|
const n = try self.tty.read(buf[read_start..]);
|
|
var seq_start: usize = 0;
|
|
while (seq_start < n) {
|
|
const result = try parser.parse(buf[seq_start..n], paste_allocator);
|
|
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) {
|
|
buf[seq_start - initial_start] = buf[seq_start];
|
|
}
|
|
read_start = seq_start - initial_start + 1;
|
|
continue;
|
|
}
|
|
read_start = 0;
|
|
seq_start += result.n;
|
|
|
|
const event = result.event orelse continue;
|
|
switch (event) {
|
|
.key_press => |key| {
|
|
if (@hasField(Event, "key_press")) {
|
|
// HACK: yuck. there has to be a better way
|
|
var mut_key = key;
|
|
if (key.text) |text| {
|
|
mut_key.text = cache.put(text);
|
|
}
|
|
self.postEvent(.{ .key_press = mut_key });
|
|
}
|
|
},
|
|
.key_release => |*key| {
|
|
if (@hasField(Event, "key_release")) {
|
|
// HACK: yuck. there has to be a better way
|
|
var mut_key = key;
|
|
if (key.text) |text| {
|
|
mut_key.text = cache.put(text);
|
|
}
|
|
self.postEvent(.{ .key_release = mut_key });
|
|
}
|
|
},
|
|
.mouse => |mouse| {
|
|
if (@hasField(Event, "mouse")) {
|
|
self.postEvent(.{ .mouse = self.vaxis.translateMouse(mouse) });
|
|
}
|
|
},
|
|
.focus_in => {
|
|
if (@hasField(Event, "focus_in")) {
|
|
self.postEvent(.focus_in);
|
|
}
|
|
},
|
|
.focus_out => {
|
|
if (@hasField(Event, "focus_out")) {
|
|
self.postEvent(.focus_out);
|
|
}
|
|
},
|
|
.paste_start => {
|
|
if (@hasField(Event, "paste_start")) {
|
|
self.postEvent(.paste_start);
|
|
}
|
|
},
|
|
.paste_end => {
|
|
if (@hasField(Event, "paste_end")) {
|
|
self.postEvent(.paste_end);
|
|
}
|
|
},
|
|
.paste => |text| {
|
|
if (@hasField(Event, "paste")) {
|
|
self.postEvent(.{ .paste = text });
|
|
} else {
|
|
if (paste_allocator) |_|
|
|
paste_allocator.?.free(text);
|
|
}
|
|
},
|
|
.color_report => |report| {
|
|
if (@hasField(Event, "color_report")) {
|
|
self.postEvent(.{ .color_report = report });
|
|
}
|
|
},
|
|
.color_scheme => |scheme| {
|
|
if (@hasField(Event, "color_scheme")) {
|
|
self.postEvent(.{ .color_scheme = scheme });
|
|
}
|
|
},
|
|
.cap_kitty_keyboard => {
|
|
log.info("kitty keyboard capability detected", .{});
|
|
self.vaxis.caps.kitty_keyboard = true;
|
|
},
|
|
.cap_kitty_graphics => {
|
|
if (!self.vaxis.caps.kitty_graphics) {
|
|
log.info("kitty graphics capability detected", .{});
|
|
self.vaxis.caps.kitty_graphics = true;
|
|
}
|
|
},
|
|
.cap_rgb => {
|
|
log.info("rgb capability detected", .{});
|
|
self.vaxis.caps.rgb = true;
|
|
},
|
|
.cap_unicode => {
|
|
log.info("unicode capability detected", .{});
|
|
self.vaxis.caps.unicode = .unicode;
|
|
self.vaxis.screen.width_method = .unicode;
|
|
},
|
|
.cap_sgr_pixels => {
|
|
log.info("pixel mouse capability detected", .{});
|
|
self.vaxis.caps.sgr_pixels = true;
|
|
},
|
|
.cap_color_scheme_updates => {
|
|
log.info("color_scheme_updates capability detected", .{});
|
|
self.vaxis.caps.color_scheme_updates = true;
|
|
},
|
|
.cap_da1 => {
|
|
std.Thread.Futex.wake(&self.vaxis.query_futex, 10);
|
|
},
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
}
|