592 lines
22 KiB
Zig
592 lines
22 KiB
Zig
const std = @import("std");
|
|
const cbor = @import("cbor");
|
|
const log = @import("log");
|
|
const Style = @import("theme").Style;
|
|
const Color = @import("theme").Color;
|
|
const vaxis = @import("vaxis");
|
|
const input = @import("input");
|
|
const builtin = @import("builtin");
|
|
|
|
pub const Plane = @import("Plane.zig");
|
|
pub const Cell = @import("Cell.zig");
|
|
pub const CursorShape = vaxis.Cell.CursorShape;
|
|
|
|
pub const style = @import("style.zig").StyleBits;
|
|
|
|
const Self = @This();
|
|
pub const log_name = "vaxis";
|
|
|
|
allocator: std.mem.Allocator,
|
|
|
|
tty: vaxis.Tty,
|
|
vx: vaxis.Vaxis,
|
|
|
|
no_alternate: bool,
|
|
event_buffer: std.ArrayList(u8),
|
|
input_buffer: std.ArrayList(u8),
|
|
mods: vaxis.Key.Modifiers = .{},
|
|
|
|
bracketed_paste: bool = false,
|
|
bracketed_paste_buffer: std.ArrayList(u8),
|
|
|
|
handler_ctx: *anyopaque,
|
|
dispatch_input: ?*const fn (ctx: *anyopaque, cbor_msg: []const u8) void = null,
|
|
dispatch_mouse: ?*const fn (ctx: *anyopaque, y: c_int, x: c_int, cbor_msg: []const u8) void = null,
|
|
dispatch_mouse_drag: ?*const fn (ctx: *anyopaque, y: c_int, x: c_int, cbor_msg: []const u8) void = null,
|
|
dispatch_event: ?*const fn (ctx: *anyopaque, cbor_msg: []const u8) void = null,
|
|
|
|
logger: log.Logger,
|
|
|
|
loop: Loop,
|
|
|
|
pub fn init(allocator: std.mem.Allocator, handler_ctx: *anyopaque, no_alternate: bool, _: *const fn (ctx: *anyopaque) void) !Self {
|
|
const opts: vaxis.Vaxis.Options = .{
|
|
.kitty_keyboard_flags = .{
|
|
.disambiguate = true,
|
|
.report_events = true,
|
|
.report_alternate_keys = true,
|
|
.report_all_as_ctl_seqs = true,
|
|
.report_text = true,
|
|
},
|
|
.system_clipboard_allocator = allocator,
|
|
};
|
|
return .{
|
|
.allocator = allocator,
|
|
.tty = try vaxis.Tty.init(),
|
|
.vx = try vaxis.init(allocator, opts),
|
|
.no_alternate = no_alternate,
|
|
.event_buffer = std.ArrayList(u8).init(allocator),
|
|
.input_buffer = std.ArrayList(u8).init(allocator),
|
|
.bracketed_paste_buffer = std.ArrayList(u8).init(allocator),
|
|
.handler_ctx = handler_ctx,
|
|
.logger = log.logger(log_name),
|
|
.loop = undefined,
|
|
};
|
|
}
|
|
|
|
pub fn deinit(self: *Self) void {
|
|
panic_cleanup = null;
|
|
self.loop.stop();
|
|
self.vx.deinit(self.allocator, self.tty.anyWriter());
|
|
self.tty.deinit();
|
|
self.bracketed_paste_buffer.deinit();
|
|
self.input_buffer.deinit();
|
|
self.event_buffer.deinit();
|
|
}
|
|
|
|
var panic_cleanup: ?struct {
|
|
allocator: std.mem.Allocator,
|
|
tty: *vaxis.Tty,
|
|
vx: *vaxis.Vaxis,
|
|
} = null;
|
|
pub fn panic(msg: []const u8, error_return_trace: ?*std.builtin.StackTrace, ret_addr: ?usize) noreturn {
|
|
const cleanup = panic_cleanup;
|
|
panic_cleanup = null;
|
|
if (cleanup) |self| {
|
|
self.vx.deinit(self.allocator, self.tty.anyWriter());
|
|
self.tty.deinit();
|
|
}
|
|
return std.builtin.default_panic(msg, error_return_trace, ret_addr orelse @returnAddress());
|
|
}
|
|
|
|
pub fn run(self: *Self) !void {
|
|
self.vx.sgr = .legacy;
|
|
self.vx.conpty_hacks = true;
|
|
|
|
panic_cleanup = .{ .allocator = self.allocator, .tty = &self.tty, .vx = &self.vx };
|
|
if (!self.no_alternate) try self.vx.enterAltScreen(self.tty.anyWriter());
|
|
if (builtin.os.tag == .windows) {
|
|
try self.resize(.{ .rows = 25, .cols = 80, .x_pixel = 0, .y_pixel = 0 }); // dummy resize to fully init vaxis
|
|
} else {
|
|
try self.sigwinch();
|
|
}
|
|
try self.vx.setBracketedPaste(self.tty.anyWriter(), true);
|
|
try self.vx.queryTerminalSend(self.tty.anyWriter());
|
|
|
|
self.loop = Loop.init(&self.tty, &self.vx);
|
|
try self.loop.start();
|
|
}
|
|
|
|
pub fn render(self: *Self) !void {
|
|
var bufferedWriter = self.tty.bufferedWriter();
|
|
try self.vx.render(bufferedWriter.writer().any());
|
|
try bufferedWriter.flush();
|
|
}
|
|
|
|
pub fn sigwinch(self: *Self) !void {
|
|
if (builtin.os.tag == .windows or self.vx.state.in_band_resize) return;
|
|
try self.resize(try vaxis.Tty.getWinsize(self.input_fd_blocking()));
|
|
}
|
|
|
|
fn resize(self: *Self, ws: vaxis.Winsize) !void {
|
|
try self.vx.resize(self.allocator, self.tty.anyWriter(), ws);
|
|
self.vx.queueRefresh();
|
|
if (self.dispatch_event) |f| f(self.handler_ctx, try self.fmtmsg(.{"resize"}));
|
|
}
|
|
|
|
pub fn stop(self: *Self) void {
|
|
_ = self;
|
|
}
|
|
|
|
pub fn stdplane(self: *Self) Plane {
|
|
const name = "root";
|
|
var plane: Plane = .{
|
|
.window = self.vx.window(),
|
|
.name_buf = undefined,
|
|
.name_len = name.len,
|
|
};
|
|
@memcpy(plane.name_buf[0..name.len], name);
|
|
return plane;
|
|
}
|
|
|
|
pub fn input_fd_blocking(self: Self) i32 {
|
|
return self.tty.fd;
|
|
}
|
|
|
|
pub fn process_renderer_event(self: *Self, msg: []const u8) !void {
|
|
var input_: []const u8 = undefined;
|
|
var text_: []const u8 = undefined;
|
|
if (!try cbor.match(msg, .{ "RDR", cbor.extract(&input_), cbor.extract(&text_) }))
|
|
return error.UnexpectedRendererEvent;
|
|
const text = if (text_.len > 0) text_ else null;
|
|
const event = std.mem.bytesAsValue(vaxis.Event, input_);
|
|
switch (event.*) {
|
|
.key_press => |key__| {
|
|
const key_ = filter_mods(normalize_shifted_alphas(key__));
|
|
try self.sync_mod_state(key_.codepoint, key_.mods);
|
|
const cbor_msg = try self.fmtmsg(.{
|
|
"I",
|
|
input.event.press,
|
|
key_.base_layout_codepoint orelse key_.codepoint,
|
|
key_.shifted_codepoint orelse key_.codepoint,
|
|
text orelse "",
|
|
@as(u8, @bitCast(key_.mods)),
|
|
});
|
|
if (self.bracketed_paste and self.handle_bracketed_paste_input(cbor_msg) catch |e| return self.handle_bracketed_paste_error(e)) {
|
|
// we have stored it to handle on .paste_end, so do nothing more here
|
|
} else if (self.dispatch_input) |f| f(self.handler_ctx, cbor_msg);
|
|
},
|
|
.key_release => |key__| {
|
|
const key_ = filter_mods(normalize_shifted_alphas(key__));
|
|
const cbor_msg = try self.fmtmsg(.{
|
|
"I",
|
|
input.event.release,
|
|
key_.base_layout_codepoint orelse key_.codepoint,
|
|
key_.shifted_codepoint orelse key_.codepoint,
|
|
text orelse "",
|
|
@as(u8, @bitCast(key_.mods)),
|
|
});
|
|
if (self.bracketed_paste) {} else if (self.dispatch_input) |f| f(self.handler_ctx, cbor_msg);
|
|
},
|
|
.mouse => |mouse_| {
|
|
const mouse = self.vx.translateMouse(mouse_);
|
|
try self.sync_mod_state(0, .{ .ctrl = mouse.mods.ctrl, .shift = mouse.mods.shift, .alt = mouse.mods.alt });
|
|
if (self.dispatch_mouse) |f| switch (mouse.type) {
|
|
.motion => f(self.handler_ctx, @intCast(mouse.row), @intCast(mouse.col), try self.fmtmsg(.{
|
|
"M",
|
|
mouse.col,
|
|
mouse.row,
|
|
mouse.xoffset,
|
|
mouse.yoffset,
|
|
})),
|
|
.press => f(self.handler_ctx, @intCast(mouse.row), @intCast(mouse.col), try self.fmtmsg(.{
|
|
"B",
|
|
input.event.press,
|
|
@intFromEnum(mouse.button),
|
|
input.utils.button_id_string(mouse.button),
|
|
mouse.col,
|
|
mouse.row,
|
|
mouse.xoffset,
|
|
mouse.yoffset,
|
|
})),
|
|
.release => f(self.handler_ctx, @intCast(mouse.row), @intCast(mouse.col), try self.fmtmsg(.{
|
|
"B",
|
|
input.event.release,
|
|
@intFromEnum(mouse.button),
|
|
input.utils.button_id_string(mouse.button),
|
|
mouse.col,
|
|
mouse.row,
|
|
mouse.xoffset,
|
|
mouse.yoffset,
|
|
})),
|
|
.drag => if (self.dispatch_mouse_drag) |f_|
|
|
f_(self.handler_ctx, @intCast(mouse.row), @intCast(mouse.col), try self.fmtmsg(.{
|
|
"D",
|
|
input.event.press,
|
|
@intFromEnum(mouse.button),
|
|
input.utils.button_id_string(mouse.button),
|
|
mouse.col,
|
|
mouse.row,
|
|
mouse.xoffset,
|
|
mouse.yoffset,
|
|
})),
|
|
};
|
|
},
|
|
.focus_in => {
|
|
if (self.dispatch_event) |f| f(self.handler_ctx, try self.fmtmsg(.{"focus_in"}));
|
|
},
|
|
.focus_out => {
|
|
if (self.dispatch_event) |f| f(self.handler_ctx, try self.fmtmsg(.{"focus_out"}));
|
|
},
|
|
.paste_start => try self.handle_bracketed_paste_start(),
|
|
.paste_end => try self.handle_bracketed_paste_end(),
|
|
.paste => |_| {
|
|
if (self.dispatch_event) |f| f(self.handler_ctx, try self.fmtmsg(.{ "system_clipboard", text }));
|
|
},
|
|
.color_report => {},
|
|
.color_scheme => {},
|
|
.winsize => |ws| {
|
|
if (!self.vx.state.in_band_resize) {
|
|
self.vx.state.in_band_resize = true;
|
|
self.logger.print("in band resize capability detected", .{});
|
|
}
|
|
try self.resize(ws);
|
|
},
|
|
|
|
.cap_unicode => {
|
|
self.logger.print("unicode capability detected", .{});
|
|
self.vx.caps.unicode = .unicode;
|
|
self.vx.screen.width_method = .unicode;
|
|
},
|
|
.cap_sgr_pixels => {
|
|
self.logger.print("pixel mouse capability detected", .{});
|
|
self.vx.caps.sgr_pixels = true;
|
|
},
|
|
.cap_da1 => {
|
|
self.vx.enableDetectedFeatures(self.tty.anyWriter()) catch |e| self.logger.err("enable features", e);
|
|
try self.vx.setMouseMode(self.tty.anyWriter(), true);
|
|
},
|
|
.cap_kitty_keyboard => {
|
|
self.logger.print("kitty keyboard capability detected", .{});
|
|
self.vx.caps.kitty_keyboard = true;
|
|
},
|
|
.cap_kitty_graphics => {
|
|
if (!self.vx.caps.kitty_graphics) {
|
|
self.vx.caps.kitty_graphics = true;
|
|
}
|
|
},
|
|
.cap_rgb => {
|
|
self.logger.print("rgb capability detected", .{});
|
|
self.vx.caps.rgb = true;
|
|
},
|
|
.cap_color_scheme_updates => {},
|
|
}
|
|
}
|
|
|
|
fn fmtmsg(self: *Self, value: anytype) ![]const u8 {
|
|
self.event_buffer.clearRetainingCapacity();
|
|
try cbor.writeValue(self.event_buffer.writer(), value);
|
|
return self.event_buffer.items;
|
|
}
|
|
|
|
fn handle_bracketed_paste_input(self: *Self, cbor_msg: []const u8) !bool {
|
|
var keypress: input.Key = undefined;
|
|
var egc_: input.Key = undefined;
|
|
if (try cbor.match(cbor_msg, .{ "I", cbor.number, cbor.extract(&keypress), cbor.extract(&egc_), cbor.string, 0 })) {
|
|
switch (keypress) {
|
|
input.key.enter => try self.bracketed_paste_buffer.appendSlice("\n"),
|
|
input.key.tab => try self.bracketed_paste_buffer.appendSlice("\t"),
|
|
else => if (!input.is_non_input_key(keypress)) {
|
|
var buf: [6]u8 = undefined;
|
|
const bytes = try input.ucs32_to_utf8(&[_]u32{egc_}, &buf);
|
|
try self.bracketed_paste_buffer.appendSlice(buf[0..bytes]);
|
|
} else {
|
|
var buf: [6]u8 = undefined;
|
|
const bytes = try input.ucs32_to_utf8(&[_]u32{egc_}, &buf);
|
|
self.logger.print("unexpected codepoint in paste: {d} {s}", .{ keypress, buf[0..bytes] });
|
|
},
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
fn handle_bracketed_paste_start(self: *Self) !void {
|
|
self.bracketed_paste = true;
|
|
self.bracketed_paste_buffer.clearRetainingCapacity();
|
|
}
|
|
|
|
fn handle_bracketed_paste_end(self: *Self) !void {
|
|
defer {
|
|
self.bracketed_paste_buffer.clearAndFree();
|
|
self.bracketed_paste = false;
|
|
}
|
|
if (!self.bracketed_paste) return;
|
|
if (self.dispatch_event) |f| f(self.handler_ctx, try self.fmtmsg(.{ "system_clipboard", self.bracketed_paste_buffer.items }));
|
|
}
|
|
|
|
fn handle_bracketed_paste_error(self: *Self, e: anytype) !void {
|
|
self.logger.err("bracketed paste", e);
|
|
self.bracketed_paste_buffer.clearAndFree();
|
|
self.bracketed_paste = false;
|
|
return e;
|
|
}
|
|
|
|
pub fn set_terminal_title(self: *Self, text: []const u8) void {
|
|
self.vx.setTitle(self.tty.anyWriter(), text) catch {};
|
|
}
|
|
|
|
pub fn set_terminal_style(self: *Self, style_: Style) void {
|
|
if (style_.fg) |color|
|
|
self.vx.setTerminalForegroundColor(self.tty.anyWriter(), vaxis.Cell.Color.rgbFromUint(@intCast(color.color)).rgb) catch {};
|
|
if (style_.bg) |color|
|
|
self.vx.setTerminalBackgroundColor(self.tty.anyWriter(), vaxis.Cell.Color.rgbFromUint(@intCast(color.color)).rgb) catch {};
|
|
}
|
|
|
|
pub fn set_terminal_cursor_color(self: *Self, color: Color) void {
|
|
self.vx.setTerminalCursorColor(self.tty.anyWriter(), vaxis.Cell.Color.rgbFromUint(@intCast(color.color)).rgb) catch {};
|
|
}
|
|
|
|
pub fn set_terminal_working_directory(self: *Self, absolute_path: []const u8) void {
|
|
self.vx.setTerminalWorkingDirectory(self.tty.anyWriter(), absolute_path) catch {};
|
|
}
|
|
|
|
pub fn copy_to_system_clipboard(self: *Self, text: []const u8) void {
|
|
var bufferedWriter = self.tty.bufferedWriter();
|
|
self.vx.copyToSystemClipboard(bufferedWriter.writer().any(), text, self.allocator) catch |e| log.logger(log_name).err("copy_to_system_clipboard", e);
|
|
bufferedWriter.flush() catch @panic("flush failed");
|
|
}
|
|
|
|
pub fn request_system_clipboard(self: *Self) void {
|
|
self.vx.requestSystemClipboard(self.tty.anyWriter()) catch |e| log.logger(log_name).err("request_system_clipboard", e);
|
|
}
|
|
|
|
const win32 = struct {
|
|
const windows = std.os.windows;
|
|
pub extern "user32" fn OpenClipboard(hWndNewOwner: ?windows.HWND) callconv(windows.WINAPI) windows.BOOL;
|
|
pub extern "user32" fn CloseClipboard() callconv(windows.WINAPI) windows.BOOL;
|
|
pub extern "user32" fn SetClipboardData(uFormat: windows.UINT, hMem: windows.HANDLE) callconv(windows.WINAPI) ?windows.HANDLE;
|
|
pub extern "user32" fn GetClipboardData(uFormat: windows.UINT) callconv(windows.WINAPI) ?windows.HANDLE;
|
|
pub extern "user32" fn EmptyClipboard() windows.BOOL;
|
|
pub extern "kernel32" fn GlobalAlloc(flags: c_int, size: usize) ?windows.HANDLE;
|
|
pub extern "kernel32" fn GlobalFree(hMem: windows.HANDLE) windows.BOOL;
|
|
pub extern "kernel32" fn GlobalLock(hMem: windows.HANDLE) ?windows.LPVOID;
|
|
pub extern "kernel32" fn GlobalUnlock(hMem: windows.HANDLE) windows.BOOL;
|
|
const CF_TEXT = @as(c_int, 1);
|
|
const GMEM_MOVEABLE = @as(c_int, 2);
|
|
};
|
|
|
|
pub fn copy_to_windows_clipboard(text: []const u8) !void {
|
|
const mem = win32.GlobalAlloc(win32.GMEM_MOVEABLE, text.len + 1) orelse return error.GlobalAllocFalied;
|
|
const data: [*c]u8 = @ptrCast(win32.GlobalLock(mem) orelse return error.ClipboardDataLockFailed);
|
|
@memcpy(data[0..text.len], text);
|
|
data[text.len] = 0;
|
|
_ = win32.GlobalUnlock(mem);
|
|
|
|
if (win32.OpenClipboard(null) == 0) {
|
|
_ = win32.GlobalFree(mem);
|
|
return error.OpenClipBoardFailed;
|
|
}
|
|
defer _ = win32.CloseClipboard();
|
|
|
|
_ = win32.EmptyClipboard();
|
|
if (win32.SetClipboardData(win32.CF_TEXT, mem) == null) {
|
|
_ = win32.GlobalFree(mem);
|
|
}
|
|
}
|
|
|
|
pub fn request_windows_clipboard(allocator: std.mem.Allocator) ![]u8 {
|
|
if (win32.OpenClipboard(null) == 0)
|
|
return error.OpenClipBoardFailed;
|
|
defer _ = win32.CloseClipboard();
|
|
|
|
const mem = win32.GetClipboardData(win32.CF_TEXT) orelse return error.ClipboardDataRetrievalFailed;
|
|
const data: [*c]u8 = @ptrCast(win32.GlobalLock(mem) orelse return error.ClipboardDataLockFailed);
|
|
const text = std.mem.span(data);
|
|
defer _ = win32.GlobalUnlock(mem);
|
|
|
|
return allocator.dupe(u8, text);
|
|
}
|
|
|
|
pub fn request_mouse_cursor_text(self: *Self, push_or_pop: bool) void {
|
|
if (push_or_pop) self.vx.setMouseShape(.text) else self.vx.setMouseShape(.default);
|
|
}
|
|
|
|
pub fn request_mouse_cursor_pointer(self: *Self, push_or_pop: bool) void {
|
|
if (push_or_pop) self.vx.setMouseShape(.pointer) else self.vx.setMouseShape(.default);
|
|
}
|
|
|
|
pub fn request_mouse_cursor_default(self: *Self, push_or_pop: bool) void {
|
|
if (push_or_pop) self.vx.setMouseShape(.default) else self.vx.setMouseShape(.default);
|
|
}
|
|
|
|
pub fn cursor_enable(self: *Self, y: c_int, x: c_int, shape: CursorShape) !void {
|
|
self.vx.screen.cursor_vis = true;
|
|
self.vx.screen.cursor_row = @intCast(y);
|
|
self.vx.screen.cursor_col = @intCast(x);
|
|
self.vx.screen.cursor_shape = shape;
|
|
}
|
|
|
|
pub fn cursor_disable(self: *Self) void {
|
|
self.vx.screen.cursor_vis = false;
|
|
}
|
|
|
|
fn sync_mod_state(self: *Self, keypress: u32, modifiers: vaxis.Key.Modifiers) !void {
|
|
if (modifiers.ctrl and !self.mods.ctrl and !(keypress == input.key.left_control or keypress == input.key.right_control))
|
|
try self.send_sync_key(input.event.press, input.key.left_control, "", modifiers);
|
|
if (!modifiers.ctrl and self.mods.ctrl and !(keypress == input.key.left_control or keypress == input.key.right_control))
|
|
try self.send_sync_key(input.event.release, input.key.left_control, "", modifiers);
|
|
if (modifiers.alt and !self.mods.alt and !(keypress == input.key.left_alt or keypress == input.key.right_alt))
|
|
try self.send_sync_key(input.event.press, input.key.left_alt, "", modifiers);
|
|
if (!modifiers.alt and self.mods.alt and !(keypress == input.key.left_alt or keypress == input.key.right_alt))
|
|
try self.send_sync_key(input.event.release, input.key.left_alt, "", modifiers);
|
|
if (modifiers.shift and !self.mods.shift and !(keypress == input.key.left_shift or keypress == input.key.right_shift))
|
|
try self.send_sync_key(input.event.press, input.key.left_shift, "", modifiers);
|
|
if (!modifiers.shift and self.mods.shift and !(keypress == input.key.left_shift or keypress == input.key.right_shift))
|
|
try self.send_sync_key(input.event.release, input.key.left_shift, "", modifiers);
|
|
self.mods = modifiers;
|
|
}
|
|
|
|
fn send_sync_key(self: *Self, event: input.Event, keypress: u32, key_string: []const u8, modifiers: vaxis.Key.Modifiers) !void {
|
|
if (self.dispatch_input) |f| f(
|
|
self.handler_ctx,
|
|
try self.fmtmsg(.{
|
|
"I",
|
|
event,
|
|
keypress,
|
|
keypress,
|
|
key_string,
|
|
@as(u8, @bitCast(modifiers)),
|
|
}),
|
|
);
|
|
}
|
|
|
|
fn filter_mods(key_: vaxis.Key) vaxis.Key {
|
|
var key__ = key_;
|
|
key__.mods = .{
|
|
.shift = key_.mods.shift,
|
|
.alt = key_.mods.alt,
|
|
.ctrl = key_.mods.ctrl,
|
|
};
|
|
return key__;
|
|
}
|
|
|
|
fn normalize_shifted_alphas(key_: vaxis.Key) vaxis.Key {
|
|
if (!key_.mods.shift) return key_;
|
|
var key = key_;
|
|
const shifted_codepoint = key.shifted_codepoint orelse key.codepoint;
|
|
const base_layout_codepoint = key.base_layout_codepoint orelse key.codepoint;
|
|
if (shifted_codepoint == base_layout_codepoint and 'a' <= shifted_codepoint and shifted_codepoint <= 'z')
|
|
key.shifted_codepoint = shifted_codepoint - 0x20;
|
|
return key;
|
|
}
|
|
|
|
const Loop = struct {
|
|
tty: *vaxis.Tty,
|
|
vaxis: *vaxis.Vaxis,
|
|
pid: tp.pid,
|
|
|
|
thread: ?std.Thread = null,
|
|
should_quit: bool = false,
|
|
|
|
const tp = @import("thespian");
|
|
|
|
pub fn init(tty: *vaxis.Tty, vaxis_: *vaxis.Vaxis) Loop {
|
|
return .{
|
|
.tty = tty,
|
|
.vaxis = vaxis_,
|
|
.pid = tp.self_pid().clone(),
|
|
};
|
|
}
|
|
|
|
pub fn deinit(self: *Loop) void {
|
|
self.pid.deinit();
|
|
}
|
|
|
|
/// spawns the input thread to read input from the tty
|
|
pub fn start(self: *Loop) !void {
|
|
if (self.thread) |_| return;
|
|
self.thread = try std.Thread.spawn(.{}, Loop.ttyRun, .{self});
|
|
}
|
|
|
|
/// stops reading from the tty.
|
|
pub fn stop(self: *Loop) void {
|
|
self.should_quit = true;
|
|
// trigger a read
|
|
self.vaxis.deviceStatusReport(self.tty.anyWriter()) catch {};
|
|
|
|
if (self.thread) |thread| {
|
|
thread.join();
|
|
self.thread = null;
|
|
self.should_quit = false;
|
|
}
|
|
}
|
|
|
|
fn postEvent(self: *Loop, event: vaxis.Event) void {
|
|
var text: []const u8 = "";
|
|
var free_text: bool = false;
|
|
switch (event) {
|
|
.key_press => |key_| {
|
|
if (key_.text) |text_| text = text_;
|
|
},
|
|
.key_release => |key_| {
|
|
if (key_.text) |text_| text = text_;
|
|
},
|
|
.paste => |text_| {
|
|
text = text_;
|
|
free_text = true;
|
|
},
|
|
else => {},
|
|
}
|
|
self.pid.send(.{ "RDR", std.mem.asBytes(&event), text }) catch @panic("send RDR event failed");
|
|
if (free_text)
|
|
self.vaxis.opts.system_clipboard_allocator.?.free(text);
|
|
}
|
|
|
|
fn ttyRun(self: *Loop) !void {
|
|
switch (builtin.os.tag) {
|
|
.windows => {
|
|
var parser: vaxis.Parser = .{
|
|
.grapheme_data = &self.vaxis.unicode.width_data.g_data,
|
|
};
|
|
const a = self.vaxis.opts.system_clipboard_allocator orelse @panic("no tty allocator");
|
|
while (!self.should_quit) {
|
|
self.postEvent(try self.tty.nextEvent(&parser, a));
|
|
}
|
|
},
|
|
else => {
|
|
var parser: vaxis.Parser = .{
|
|
.grapheme_data = &self.vaxis.unicode.width_data.g_data,
|
|
};
|
|
|
|
const a = self.vaxis.opts.system_clipboard_allocator orelse @panic("no tty allocator");
|
|
|
|
var buf = try a.alloc(u8, 512);
|
|
defer a.free(buf);
|
|
var n: usize = 0;
|
|
var need_read = false;
|
|
|
|
while (!self.should_quit) {
|
|
if (n >= buf.len) {
|
|
const buf_grow = try a.alloc(u8, buf.len * 2);
|
|
@memcpy(buf_grow[0..buf.len], buf);
|
|
a.free(buf);
|
|
buf = buf_grow;
|
|
}
|
|
if (n == 0 or need_read) {
|
|
const n_ = try self.tty.read(buf[n..]);
|
|
n = n + n_;
|
|
need_read = false;
|
|
}
|
|
const result = try parser.parse(buf[0..n], a);
|
|
if (result.n == 0) {
|
|
need_read = true;
|
|
continue;
|
|
}
|
|
if (result.event) |event| {
|
|
self.postEvent(event);
|
|
}
|
|
if (result.n < n) {
|
|
const buf_move = try a.alloc(u8, buf.len);
|
|
@memcpy(buf_move[0 .. n - result.n], buf[result.n..n]);
|
|
a.free(buf);
|
|
buf = buf_move;
|
|
n = n - result.n;
|
|
} else {
|
|
n = 0;
|
|
}
|
|
}
|
|
},
|
|
}
|
|
}
|
|
};
|