flow/src/renderer/vaxis/renderer.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;
}
}
},
}
}
};