From 41f76e8f0325cc3cf7f265749466d59e109c906f Mon Sep 17 00:00:00 2001 From: Tim Culverhouse Date: Mon, 29 Apr 2024 12:26:50 -0500 Subject: [PATCH] vaxis: refactor to split Loop and Vaxis Refactor the main API to split Loop and Vaxis types. This enables the core Vaxis type to be more easily referenced in other types, since it doesn't require a comptime type parameter. This will make the switch to the zg unicode library easier. --- examples/text_input.zig | 15 +- src/Loop.zig | 61 ++++ src/Options.zig | 2 - src/Tty.zig | 38 +-- src/Vaxis.zig | 636 ++++++++++++++++++++++++++++++++++++ src/main.zig | 8 +- src/vaxis.zig | 708 ---------------------------------------- 7 files changed, 730 insertions(+), 738 deletions(-) create mode 100644 src/Loop.zig delete mode 100644 src/Options.zig create mode 100644 src/Vaxis.zig delete mode 100644 src/vaxis.zig diff --git a/examples/text_input.zig b/examples/text_input.zig index 2bde6ed..54722cc 100644 --- a/examples/text_input.zig +++ b/examples/text_input.zig @@ -29,16 +29,21 @@ pub fn main() !void { } const alloc = gpa.allocator(); - // Initialize Vaxis with our event type - var vx = try vaxis.init(Event, .{}); + // Initialize Vaxis + var vx = try vaxis.init(.{}); // deinit takes an optional allocator. If your program is exiting, you can // choose to pass a null allocator to save some exit time. defer vx.deinit(alloc); + // create our event loop + var loop: vaxis.Loop(Event) = .{ + .vaxis = &vx, + }; + // Start the read loop. This puts the terminal in raw mode and begins // reading user input - try vx.startReadThread(); - defer vx.stopReadThread(); + try loop.run(); + defer loop.stop(); // Optionally enter the alternate screen try vx.enterAltScreen(); @@ -61,7 +66,7 @@ pub fn main() !void { // queue which can serve as the primary event queue for an application while (true) { // nextEvent blocks until an event is in the queue - const event = vx.nextEvent(); + const event = loop.nextEvent(); log.debug("event: {}", .{event}); // exhaustive switching ftw. Vaxis will send events if your Event // enum has the fields for those events (ie "key_press", "winsize") diff --git a/src/Loop.zig b/src/Loop.zig new file mode 100644 index 0000000..38009d0 --- /dev/null +++ b/src/Loop.zig @@ -0,0 +1,61 @@ +const std = @import("std"); + +const Queue = @import("queue.zig").Queue; +const Tty = @import("Tty.zig"); +const Vaxis = @import("Vaxis.zig"); + +pub fn Loop(comptime T: type) type { + return struct { + const Self = @This(); + + const log = std.log.scoped(.loop); + + queue: Queue(T, 512) = .{}, + + thread: ?std.Thread = null, + + vaxis: *Vaxis, + + /// spawns the input thread to read input from the tty + pub fn run(self: *Self) !void { + if (self.thread) |_| return; + if (self.vaxis.tty == null) self.vaxis.tty = try Tty.init(); + self.thread = try std.Thread.spawn(.{}, Tty.run, .{ &self.vaxis.tty.?, T, self }); + } + + /// stops reading from the tty and returns it to it's initial state + pub fn stop(self: *Self) void { + if (self.vaxis.tty) |*tty| tty.stop(); + 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); + } + }; +} diff --git a/src/Options.zig b/src/Options.zig deleted file mode 100644 index 301afb7..0000000 --- a/src/Options.zig +++ /dev/null @@ -1,2 +0,0 @@ -/// Runtime options -const Options = @This(); diff --git a/src/Tty.zig b/src/Tty.zig index 13da20d..d8684c4 100644 --- a/src/Tty.zig +++ b/src/Tty.zig @@ -1,7 +1,7 @@ const std = @import("std"); const builtin = @import("builtin"); const posix = std.posix; -const Vaxis = @import("vaxis.zig").Vaxis; +const Loop = @import("Loop.zig").Loop; const Parser = @import("Parser.zig"); const GraphemeCache = @import("GraphemeCache.zig"); const ctlseqs = @import("ctlseqs.zig"); @@ -57,13 +57,13 @@ pub fn stop(self: *Tty) void { pub fn run( self: *Tty, comptime Event: type, - vx: *Vaxis(Event), + loop: *Loop(Event), ) !void { // get our initial winsize const winsize = try getWinsize(self.fd); if (@hasField(Event, "winsize")) { - vx.postEvent(.{ .winsize = winsize }); + loop.postEvent(.{ .winsize = winsize }); } // Build a winch handler. We need build this struct to get an anonymous @@ -72,10 +72,10 @@ pub fn run( const WinchHandler = struct { const Self = @This(); - var vx_winch: *Vaxis(Event) = undefined; + var vx_winch: *Loop(Event) = undefined; var fd: posix.fd_t = undefined; - fn init(vx_arg: *Vaxis(Event), fd_arg: posix.fd_t) !void { + fn init(vx_arg: *Loop(Event), fd_arg: posix.fd_t) !void { vx_winch = vx_arg; fd = fd_arg; var act = posix.Sigaction{ @@ -100,7 +100,7 @@ pub fn run( } } }; - try WinchHandler.init(vx, self.fd); + try WinchHandler.init(loop, self.fd); // initialize a grapheme cache var cache: GraphemeCache = .{}; @@ -131,55 +131,55 @@ pub fn run( if (key.text) |text| { mut_key.text = cache.put(text); } - vx.postEvent(.{ .key_press = mut_key }); + loop.postEvent(.{ .key_press = mut_key }); } }, .mouse => |mouse| { if (@hasField(Event, "mouse")) { - vx.postEvent(.{ .mouse = mouse }); + loop.postEvent(.{ .mouse = mouse }); } }, .focus_in => { if (@hasField(Event, "focus_in")) { - vx.postEvent(.focus_in); + loop.postEvent(.focus_in); } }, .focus_out => { if (@hasField(Event, "focus_out")) { - vx.postEvent(.focus_out); + loop.postEvent(.focus_out); } }, .paste_start => { if (@hasField(Event, "paste_start")) { - vx.postEvent(.paste_start); + loop.postEvent(.paste_start); } }, .paste_end => { if (@hasField(Event, "paste_end")) { - vx.postEvent(.paste_end); + loop.postEvent(.paste_end); } }, .cap_kitty_keyboard => { log.info("kitty keyboard capability detected", .{}); - vx.caps.kitty_keyboard = true; + loop.vaxis.caps.kitty_keyboard = true; }, .cap_kitty_graphics => { - if (!vx.caps.kitty_graphics) { + if (!loop.vaxis.caps.kitty_graphics) { log.info("kitty graphics capability detected", .{}); - vx.caps.kitty_graphics = true; + loop.vaxis.caps.kitty_graphics = true; } }, .cap_rgb => { log.info("rgb capability detected", .{}); - vx.caps.rgb = true; + loop.vaxis.caps.rgb = true; }, .cap_unicode => { log.info("unicode capability detected", .{}); - vx.caps.unicode = true; - vx.screen.unicode = true; + loop.vaxis.caps.unicode = true; + loop.vaxis.screen.unicode = true; }, .cap_da1 => { - std.Thread.Futex.wake(&vx.query_futex, 10); + std.Thread.Futex.wake(&loop.vaxis.query_futex, 10); }, } } diff --git a/src/Vaxis.zig b/src/Vaxis.zig new file mode 100644 index 0000000..c88dca2 --- /dev/null +++ b/src/Vaxis.zig @@ -0,0 +1,636 @@ +const std = @import("std"); +const atomic = std.atomic; +const base64 = std.base64; +const zigimg = @import("zigimg"); + +const Cell = @import("Cell.zig"); +const Image = @import("Image.zig"); +const InternalScreen = @import("InternalScreen.zig"); +const Key = @import("Key.zig"); +const Mouse = @import("Mouse.zig"); +const Screen = @import("Screen.zig"); +const Tty = @import("Tty.zig"); +const Unicode = @import("Unicode.zig"); +const Window = @import("Window.zig"); + +const Hyperlink = Cell.Hyperlink; +const Shape = Mouse.Shape; +const Style = Cell.Style; +const Winsize = Tty.Winsize; + +const ctlseqs = @import("ctlseqs.zig"); +const gwidth = @import("gwidth.zig"); + +const Vaxis = @This(); + +const log = std.log.scoped(.vaxis); + +pub const Capabilities = struct { + kitty_keyboard: bool = false, + kitty_graphics: bool = false, + rgb: bool = false, + unicode: bool = false, +}; + +pub const Options = struct {}; + +tty: ?Tty, + +/// the screen we write to +screen: Screen, +/// The last screen we drew. We keep this so we can efficiently update on +/// the next render +screen_last: InternalScreen = undefined, + +state: struct { + /// if we are in the alt screen + alt_screen: bool = false, + /// if we have entered kitty keyboard + kitty_keyboard: bool = false, + bracketed_paste: bool = false, + mouse: bool = false, +} = .{}, + +caps: Capabilities = .{}, + +/// if we should redraw the entire screen on the next render +refresh: bool = false, + +/// blocks the main thread until a DA1 query has been received, or the +/// futex times out +query_futex: atomic.Value(u32) = atomic.Value(u32).init(0), + +// images +next_img_id: u32 = 1, + +// statistics +renders: usize = 0, +render_dur: i128 = 0, +render_timer: std.time.Timer, + +/// Initialize Vaxis with runtime options +pub fn init(_: Options) !Vaxis { + return .{ + .tty = null, + .screen = .{}, + .screen_last = .{}, + .render_timer = try std.time.Timer.start(), + }; +} + +/// Resets the terminal to it's original state. If an allocator is +/// passed, this will free resources associated with Vaxis. This is left as an +/// optional so applications can choose to not free resources when the +/// application will be exiting anyways +pub fn deinit(self: *Vaxis, alloc: ?std.mem.Allocator) void { + if (self.tty) |_| { + var tty = &self.tty.?; + if (self.state.kitty_keyboard) { + _ = tty.write(ctlseqs.csi_u_pop) catch {}; + } + if (self.state.mouse) { + _ = tty.write(ctlseqs.mouse_reset) catch {}; + } + if (self.state.bracketed_paste) { + _ = tty.write(ctlseqs.bp_reset) catch {}; + } + if (self.state.alt_screen) { + _ = tty.write(ctlseqs.rmcup) catch {}; + } + // always show the cursor on exit + _ = tty.write(ctlseqs.show_cursor) catch {}; + tty.flush() catch {}; + tty.deinit(); + } + if (alloc) |a| { + self.screen.deinit(a); + self.screen_last.deinit(a); + } + if (self.renders > 0) { + const tpr = @divTrunc(self.render_dur, self.renders); + log.debug("total renders = {d}", .{self.renders}); + log.debug("microseconds per render = {d}", .{tpr}); + } +} + +/// resize allocates a slice of cells equal to the number of cells +/// required to display the screen (ie width x height). Any previous screen is +/// freed when resizing +pub fn resize(self: *Vaxis, alloc: std.mem.Allocator, winsize: Winsize) !void { + log.debug("resizing screen: width={d} height={d}", .{ winsize.cols, winsize.rows }); + self.screen.deinit(alloc); + self.screen = try Screen.init(alloc, winsize); + self.screen.unicode = self.caps.unicode; + // try self.screen.int(alloc, winsize.cols, winsize.rows); + // we only init our current screen. This has the effect of redrawing + // every cell + self.screen_last.deinit(alloc); + self.screen_last = try InternalScreen.init(alloc, winsize.cols, winsize.rows); + // try self.screen_last.resize(alloc, winsize.cols, winsize.rows); +} + +/// returns a Window comprising of the entire terminal screen +pub fn window(self: *Vaxis) Window { + return .{ + .x_off = 0, + .y_off = 0, + .width = self.screen.width, + .height = self.screen.height, + .screen = &self.screen, + }; +} + +/// enter the alternate screen. The alternate screen will automatically +/// be exited if calling deinit while in the alt screen +pub fn enterAltScreen(self: *Vaxis) !void { + if (self.state.alt_screen) return; + var tty = self.tty orelse return; + _ = try tty.write(ctlseqs.smcup); + try tty.flush(); + self.state.alt_screen = true; +} + +/// exit the alternate screen +pub fn exitAltScreen(self: *Vaxis) !void { + if (!self.state.alt_screen) return; + var tty = self.tty orelse return; + _ = try tty.write(ctlseqs.rmcup); + try tty.flush(); + self.state.alt_screen = false; +} + +/// write queries to the terminal to determine capabilities. Individual +/// capabilities will be delivered to the client and possibly intercepted by +/// Vaxis to enable features +pub fn queryTerminal(self: *Vaxis) !void { + var tty = self.tty orelse return; + + // TODO: re-enable this + // const colorterm = std.posix.getenv("COLORTERM") orelse ""; + // if (std.mem.eql(u8, colorterm, "truecolor") or + // std.mem.eql(u8, colorterm, "24bit")) + // { + // if (@hasField(Event, "cap_rgb")) { + // self.postEvent(.cap_rgb); + // } + // } + + // TODO: decide if we actually want to query for focus and sync. It + // doesn't hurt to blindly use them + // _ = try tty.write(ctlseqs.decrqm_focus); + // _ = try tty.write(ctlseqs.decrqm_sync); + _ = try tty.write(ctlseqs.decrqm_unicode); + _ = try tty.write(ctlseqs.decrqm_color_theme); + // TODO: XTVERSION has a DCS response. uncomment when we can parse + // that + // _ = try tty.write(ctlseqs.xtversion); + _ = try tty.write(ctlseqs.csi_u_query); + _ = try tty.write(ctlseqs.kitty_graphics_query); + // TODO: sixel geometry query interferes with F4 keys. + // _ = try tty.write(ctlseqs.sixel_geometry_query); + + // TODO: XTGETTCAP queries ("RGB", "Smulx") + + _ = try tty.write(ctlseqs.primary_device_attrs); + try tty.flush(); + + // 1 second timeout + std.Thread.Futex.timedWait(&self.query_futex, 0, 1 * std.time.ns_per_s) catch {}; + + // enable detected features + if (self.caps.kitty_keyboard) { + try self.enableKittyKeyboard(.{}); + } + if (self.caps.unicode) { + _ = try tty.write(ctlseqs.unicode_set); + } +} + +// the next render call will refresh the entire screen +pub fn queueRefresh(self: *Vaxis) void { + self.refresh = true; +} + +/// draws the screen to the terminal +pub fn render(self: *Vaxis) !void { + var tty = self.tty orelse return; + self.renders += 1; + self.render_timer.reset(); + defer { + self.render_dur += self.render_timer.read() / std.time.ns_per_us; + } + + defer self.refresh = false; + defer tty.flush() catch {}; + + // Set up sync before we write anything + // TODO: optimize sync so we only sync _when we have changes_. This + // requires a smarter buffered writer, we'll probably have to write + // our own + _ = try tty.write(ctlseqs.sync_set); + defer _ = tty.write(ctlseqs.sync_reset) catch {}; + + // Send the cursor to 0,0 + // TODO: this needs to move after we optimize writes. We only do + // this if we have an update to make. We also need to hide cursor + // and then reshow it if needed + _ = try tty.write(ctlseqs.hide_cursor); + _ = try tty.write(ctlseqs.home); + _ = try tty.write(ctlseqs.sgr_reset); + + // initialize some variables + var reposition: bool = false; + var row: usize = 0; + var col: usize = 0; + var cursor: Style = .{}; + var link: Hyperlink = .{}; + + // Clear all images + _ = try tty.write(ctlseqs.kitty_graphics_clear); + + var i: usize = 0; + while (i < self.screen.buf.len) { + const cell = self.screen.buf[i]; + defer { + // advance by the width of this char mod 1 + const w = blk: { + if (cell.char.width != 0) break :blk cell.char.width; + + const method: gwidth.Method = if (self.caps.unicode) .unicode else .wcwidth; + const width = gwidth.gwidth(cell.char.grapheme, method) catch 1; + break :blk @max(1, width); + }; + std.debug.assert(w > 0); + var j = i + 1; + while (j < i + w) : (j += 1) { + if (j >= self.screen_last.buf.len) break; + self.screen_last.buf[j].skipped = true; + } + col += w; + i += w; + } + if (col >= self.screen.width) { + row += 1; + col = 0; + reposition = true; + } + // If cell is the same as our last frame, we don't need to do + // anything + const last = self.screen_last.buf[i]; + if (!self.refresh and last.eql(cell) and !last.skipped and cell.image == null) { + reposition = true; + // Close any osc8 sequence we might be in before + // repositioning + if (link.uri.len > 0) { + _ = try tty.write(ctlseqs.osc8_clear); + } + continue; + } + self.screen_last.buf[i].skipped = false; + defer { + cursor = cell.style; + link = cell.link; + } + // Set this cell in the last frame + self.screen_last.writeCell(col, row, cell); + + // reposition the cursor, if needed + if (reposition) { + try std.fmt.format(tty.buffered_writer.writer(), ctlseqs.cup, .{ row + 1, col + 1 }); + } + + if (cell.image) |img| { + if (img.size) |size| { + try std.fmt.format( + tty.buffered_writer.writer(), + ctlseqs.kitty_graphics_scale, + .{ img.img_id, img.z_index, size.cols, size.rows }, + ); + } else { + try std.fmt.format( + tty.buffered_writer.writer(), + ctlseqs.kitty_graphics_place, + .{ img.img_id, img.z_index }, + ); + } + } + + // something is different, so let's loop through everything and + // find out what + + // foreground + if (!std.meta.eql(cursor.fg, cell.style.fg)) { + const writer = tty.buffered_writer.writer(); + switch (cell.style.fg) { + .default => _ = try tty.write(ctlseqs.fg_reset), + .index => |idx| { + switch (idx) { + 0...7 => try std.fmt.format(writer, ctlseqs.fg_base, .{idx}), + 8...15 => try std.fmt.format(writer, ctlseqs.fg_bright, .{idx - 8}), + else => try std.fmt.format(writer, ctlseqs.fg_indexed, .{idx}), + } + }, + .rgb => |rgb| { + try std.fmt.format(writer, ctlseqs.fg_rgb, .{ rgb[0], rgb[1], rgb[2] }); + }, + } + } + // background + if (!std.meta.eql(cursor.bg, cell.style.bg)) { + const writer = tty.buffered_writer.writer(); + switch (cell.style.bg) { + .default => _ = try tty.write(ctlseqs.bg_reset), + .index => |idx| { + switch (idx) { + 0...7 => try std.fmt.format(writer, ctlseqs.bg_base, .{idx}), + 8...15 => try std.fmt.format(writer, ctlseqs.bg_bright, .{idx - 8}), + else => try std.fmt.format(writer, ctlseqs.bg_indexed, .{idx}), + } + }, + .rgb => |rgb| { + try std.fmt.format(writer, ctlseqs.bg_rgb, .{ rgb[0], rgb[1], rgb[2] }); + }, + } + } + // underline color + if (!std.meta.eql(cursor.ul, cell.style.ul)) { + const writer = tty.buffered_writer.writer(); + switch (cell.style.bg) { + .default => _ = try tty.write(ctlseqs.ul_reset), + .index => |idx| { + try std.fmt.format(writer, ctlseqs.ul_indexed, .{idx}); + }, + .rgb => |rgb| { + try std.fmt.format(writer, ctlseqs.ul_rgb, .{ rgb[0], rgb[1], rgb[2] }); + }, + } + } + // underline style + if (!std.meta.eql(cursor.ul_style, cell.style.ul_style)) { + const seq = switch (cell.style.ul_style) { + .off => ctlseqs.ul_off, + .single => ctlseqs.ul_single, + .double => ctlseqs.ul_double, + .curly => ctlseqs.ul_curly, + .dotted => ctlseqs.ul_dotted, + .dashed => ctlseqs.ul_dashed, + }; + _ = try tty.write(seq); + } + // bold + if (cursor.bold != cell.style.bold) { + const seq = switch (cell.style.bold) { + true => ctlseqs.bold_set, + false => ctlseqs.bold_dim_reset, + }; + _ = try tty.write(seq); + if (cell.style.dim) { + _ = try tty.write(ctlseqs.dim_set); + } + } + // dim + if (cursor.dim != cell.style.dim) { + const seq = switch (cell.style.dim) { + true => ctlseqs.dim_set, + false => ctlseqs.bold_dim_reset, + }; + _ = try tty.write(seq); + if (cell.style.bold) { + _ = try tty.write(ctlseqs.bold_set); + } + } + // dim + if (cursor.italic != cell.style.italic) { + const seq = switch (cell.style.italic) { + true => ctlseqs.italic_set, + false => ctlseqs.italic_reset, + }; + _ = try tty.write(seq); + } + // dim + if (cursor.blink != cell.style.blink) { + const seq = switch (cell.style.blink) { + true => ctlseqs.blink_set, + false => ctlseqs.blink_reset, + }; + _ = try tty.write(seq); + } + // reverse + if (cursor.reverse != cell.style.reverse) { + const seq = switch (cell.style.reverse) { + true => ctlseqs.reverse_set, + false => ctlseqs.reverse_reset, + }; + _ = try tty.write(seq); + } + // invisible + if (cursor.invisible != cell.style.invisible) { + const seq = switch (cell.style.invisible) { + true => ctlseqs.invisible_set, + false => ctlseqs.invisible_reset, + }; + _ = try tty.write(seq); + } + // strikethrough + if (cursor.strikethrough != cell.style.strikethrough) { + const seq = switch (cell.style.strikethrough) { + true => ctlseqs.strikethrough_set, + false => ctlseqs.strikethrough_reset, + }; + _ = try tty.write(seq); + } + + // url + if (!std.meta.eql(link.uri, cell.link.uri)) { + var ps = cell.link.params; + if (cell.link.uri.len == 0) { + // Empty out the params no matter what if we don't have + // a url + ps = ""; + } + const writer = tty.buffered_writer.writer(); + try std.fmt.format(writer, ctlseqs.osc8, .{ ps, cell.link.uri }); + } + _ = try tty.write(cell.char.grapheme); + } + if (self.screen.cursor_vis) { + try std.fmt.format( + tty.buffered_writer.writer(), + ctlseqs.cup, + .{ + self.screen.cursor_row + 1, + self.screen.cursor_col + 1, + }, + ); + _ = try tty.write(ctlseqs.show_cursor); + } + if (self.screen.mouse_shape != self.screen_last.mouse_shape) { + try std.fmt.format( + tty.buffered_writer.writer(), + ctlseqs.osc22_mouse_shape, + .{@tagName(self.screen.mouse_shape)}, + ); + self.screen_last.mouse_shape = self.screen.mouse_shape; + } + if (self.screen.cursor_shape != self.screen_last.cursor_shape) { + try std.fmt.format( + tty.buffered_writer.writer(), + ctlseqs.cursor_shape, + .{@intFromEnum(self.screen.cursor_shape)}, + ); + self.screen_last.cursor_shape = self.screen.cursor_shape; + } +} + +fn enableKittyKeyboard(self: *Vaxis, flags: Key.KittyFlags) !void { + self.state.kitty_keyboard = true; + const flag_int: u5 = @bitCast(flags); + try std.fmt.format( + self.tty.?.buffered_writer.writer(), + ctlseqs.csi_u_push, + .{ + flag_int, + }, + ); + try self.tty.?.flush(); +} + +/// send a system notification +pub fn notify(self: *Vaxis, title: ?[]const u8, body: []const u8) !void { + if (self.tty == null) return; + if (title) |t| { + try std.fmt.format( + self.tty.?.buffered_writer.writer(), + ctlseqs.osc777_notify, + .{ t, body }, + ); + } else { + try std.fmt.format( + self.tty.?.buffered_writer.writer(), + ctlseqs.osc9_notify, + .{body}, + ); + } + try self.tty.?.flush(); +} + +/// sets the window title +pub fn setTitle(self: *Vaxis, title: []const u8) !void { + if (self.tty == null) return; + try std.fmt.format( + self.tty.?.buffered_writer.writer(), + ctlseqs.osc2_set_title, + .{title}, + ); + try self.tty.?.flush(); +} + +// turn bracketed paste on or off. An event will be sent at the +// beginning and end of a detected paste. All keystrokes between these +// events were pasted +pub fn setBracketedPaste(self: *Vaxis, enable: bool) !void { + if (self.tty == null) return; + self.state.bracketed_paste = enable; + const seq = if (enable) + ctlseqs.bp_set + else + ctlseqs.bp_reset; + _ = try self.tty.?.write(seq); + try self.tty.?.flush(); +} + +/// set the mouse shape +pub fn setMouseShape(self: *Vaxis, shape: Shape) void { + self.screen.mouse_shape = shape; +} + +/// turn mouse reporting on or off +pub fn setMouseMode(self: *Vaxis, enable: bool) !void { + var tty = self.tty orelse return; + self.state.mouse = enable; + if (enable) { + _ = try tty.write(ctlseqs.mouse_set); + try tty.flush(); + } else { + _ = try tty.write(ctlseqs.mouse_reset); + try tty.flush(); + } +} + +pub fn loadImage( + self: *Vaxis, + alloc: std.mem.Allocator, + src: Image.Source, +) !Image { + if (!self.caps.kitty_graphics) return error.NoGraphicsCapability; + var tty = self.tty orelse return error.NoTTY; + defer self.next_img_id += 1; + + const writer = tty.buffered_writer.writer(); + + var img = switch (src) { + .path => |path| try zigimg.Image.fromFilePath(alloc, path), + .mem => |bytes| try zigimg.Image.fromMemory(alloc, bytes), + }; + defer img.deinit(); + const png_buf = try alloc.alloc(u8, img.imageByteSize()); + defer alloc.free(png_buf); + const png = try img.writeToMemory(png_buf, .{ .png = .{} }); + const b64_buf = try alloc.alloc(u8, base64.calcSize(png.len)); + const encoded = base64.encode(b64_buf, png); + defer alloc.free(b64_buf); + + const id = self.next_img_id; + + log.debug("transmitting kitty image: id={d}, len={d}", .{ id, encoded.len }); + + if (encoded.len < 4096) { + try std.fmt.format( + writer, + "\x1b_Gf=100,i={d};{s}\x1b\\", + .{ + id, + encoded, + }, + ); + } else { + var n: usize = 4096; + + try std.fmt.format( + writer, + "\x1b_Gf=100,i={d},m=1;{s}\x1b\\", + .{ id, encoded[0..n] }, + ); + while (n < encoded.len) : (n += 4096) { + const end: usize = @min(n + 4096, encoded.len); + const m: u2 = if (end == encoded.len) 0 else 1; + try std.fmt.format( + writer, + "\x1b_Gm={d};{s}\x1b\\", + .{ + m, + encoded[n..end], + }, + ); + } + } + try tty.buffered_writer.flush(); + return .{ + .id = id, + .width = img.width, + .height = img.height, + }; +} + +/// deletes an image from the terminal's memory +pub fn freeImage(self: Vaxis, id: u32) void { + var tty = self.tty orelse return; + const writer = tty.buffered_writer.writer(); + std.fmt.format(writer, "\x1b_Ga=d,d=I,i={d};\x1b\\", .{id}) catch |err| { + log.err("couldn't delete image {d}: {}", .{ id, err }); + return; + }; + tty.buffered_writer.flush() catch |err| { + log.err("couldn't flush writer: {}", .{err}); + }; +} diff --git a/src/main.zig b/src/main.zig index 9644603..5887f27 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,7 +1,7 @@ const std = @import("std"); -pub const Vaxis = @import("vaxis.zig").Vaxis; -pub const Options = @import("Options.zig"); +pub const Vaxis = @import("Vaxis.zig"); +pub const Loop = @import("Loop.zig").Loop; pub const Queue = @import("queue.zig").Queue; pub const Key = @import("Key.zig"); @@ -22,8 +22,8 @@ pub const widgets = @import("widgets.zig"); pub const gwidth = @import("gwidth.zig"); /// Initialize a Vaxis application. -pub fn init(comptime Event: type, opts: Options) !Vaxis(Event) { - return Vaxis(Event).init(opts); +pub fn init(opts: Vaxis.Options) !Vaxis { + return Vaxis.init(opts); } test { diff --git a/src/vaxis.zig b/src/vaxis.zig deleted file mode 100644 index c1e1cd2..0000000 --- a/src/vaxis.zig +++ /dev/null @@ -1,708 +0,0 @@ -const std = @import("std"); -const builtin = @import("builtin"); -const atomic = std.atomic; -const base64 = std.base64.standard.Encoder; - -const Queue = @import("queue.zig").Queue; -const ctlseqs = @import("ctlseqs.zig"); -const Tty = @import("Tty.zig"); -const Winsize = Tty.Winsize; -const Key = @import("Key.zig"); -const Screen = @import("Screen.zig"); -const InternalScreen = @import("InternalScreen.zig"); -const Window = @import("Window.zig"); -const Options = @import("Options.zig"); -const Style = @import("Cell.zig").Style; -const Hyperlink = @import("Cell.zig").Hyperlink; -const gwidth = @import("gwidth.zig"); -const Shape = @import("Mouse.zig").Shape; -const Image = @import("Image.zig"); -const zigimg = @import("zigimg"); - -/// Vaxis is the entrypoint for a Vaxis application. The provided type T should -/// be a tagged union which contains all of the events the application will -/// handle. Vaxis will look for the following fields on the union and, if -/// found, emit them via the "nextEvent" method -/// -/// The following events are available: -/// - `key_press: Key`, for key press events -/// - `winsize: Winsize`, for resize events. Must call app.resize when receiving -/// this event -/// - `focus_in` and `focus_out` for focus events -pub fn Vaxis(comptime T: type) type { - return struct { - const Self = @This(); - - const log = std.log.scoped(.vaxis); - - pub const Event = T; - - pub const Capabilities = struct { - kitty_keyboard: bool = false, - kitty_graphics: bool = false, - rgb: bool = false, - unicode: bool = false, - }; - - /// the event queue for Vaxis - // - // TODO: is 512 ok? - queue: Queue(T, 512), - - tty: ?Tty, - - read_thread: ?std.Thread, - - /// the screen we write to - screen: Screen, - /// The last screen we drew. We keep this so we can efficiently update on - /// the next render - screen_last: InternalScreen = undefined, - - state: struct { - /// if we are in the alt screen - alt_screen: bool = false, - /// if we have entered kitty keyboard - kitty_keyboard: bool = false, - bracketed_paste: bool = false, - mouse: bool = false, - } = .{}, - - caps: Capabilities = .{}, - - /// if we should redraw the entire screen on the next render - refresh: bool = false, - - /// blocks the main thread until a DA1 query has been received, or the - /// futex times out - query_futex: atomic.Value(u32) = atomic.Value(u32).init(0), - - // images - next_img_id: u32 = 1, - - // statistics - renders: usize = 0, - render_dur: i128 = 0, - render_timer: std.time.Timer, - - /// Initialize Vaxis with runtime options - pub fn init(_: Options) !Self { - return .{ - .queue = .{}, - .tty = null, - .screen = .{}, - .screen_last = .{}, - .render_timer = try std.time.Timer.start(), - .read_thread = null, - }; - } - - /// Resets the terminal to it's original state. If an allocator is - /// passed, this will free resources associated with Vaxis. This is left as an - /// optional so applications can choose to not free resources when the - /// application will be exiting anyways - pub fn deinit(self: *Self, alloc: ?std.mem.Allocator) void { - if (self.tty) |_| { - var tty = &self.tty.?; - if (self.state.kitty_keyboard) { - _ = tty.write(ctlseqs.csi_u_pop) catch {}; - } - if (self.state.mouse) { - _ = tty.write(ctlseqs.mouse_reset) catch {}; - } - if (self.state.bracketed_paste) { - _ = tty.write(ctlseqs.bp_reset) catch {}; - } - if (self.state.alt_screen) { - _ = tty.write(ctlseqs.rmcup) catch {}; - } - // always show the cursor on exit - _ = tty.write(ctlseqs.show_cursor) catch {}; - tty.flush() catch {}; - tty.deinit(); - } - if (alloc) |a| { - self.screen.deinit(a); - self.screen_last.deinit(a); - } - if (self.renders > 0) { - const tpr = @divTrunc(self.render_dur, self.renders); - log.debug("total renders = {d}", .{self.renders}); - log.debug("microseconds per render = {d}", .{tpr}); - } - } - - /// spawns the input thread to start listening to the tty for input - pub fn startReadThread(self: *Self) !void { - self.tty = try Tty.init(); - // run our tty read loop in it's own thread - self.read_thread = try std.Thread.spawn(.{}, Tty.run, .{ &self.tty.?, T, self }); - } - - /// stops reading from the tty - pub fn stopReadThread(self: *Self) void { - if (self.tty) |_| { - var tty = &self.tty.?; - tty.stop(); - if (self.read_thread) |thread| { - thread.join(); - self.read_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) ?Event { - 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); - } - - /// resize allocates a slice of cells equal to the number of cells - /// required to display the screen (ie width x height). Any previous screen is - /// freed when resizing - pub fn resize(self: *Self, alloc: std.mem.Allocator, winsize: Winsize) !void { - log.debug("resizing screen: width={d} height={d}", .{ winsize.cols, winsize.rows }); - self.screen.deinit(alloc); - self.screen = try Screen.init(alloc, winsize); - self.screen.unicode = self.caps.unicode; - // try self.screen.int(alloc, winsize.cols, winsize.rows); - // we only init our current screen. This has the effect of redrawing - // every cell - self.screen_last.deinit(alloc); - self.screen_last = try InternalScreen.init(alloc, winsize.cols, winsize.rows); - // try self.screen_last.resize(alloc, winsize.cols, winsize.rows); - } - - /// returns a Window comprising of the entire terminal screen - pub fn window(self: *Self) Window { - return .{ - .x_off = 0, - .y_off = 0, - .width = self.screen.width, - .height = self.screen.height, - .screen = &self.screen, - }; - } - - /// enter the alternate screen. The alternate screen will automatically - /// be exited if calling deinit while in the alt screen - pub fn enterAltScreen(self: *Self) !void { - if (self.state.alt_screen) return; - var tty = self.tty orelse return; - _ = try tty.write(ctlseqs.smcup); - try tty.flush(); - self.state.alt_screen = true; - } - - /// exit the alternate screen - pub fn exitAltScreen(self: *Self) !void { - if (!self.state.alt_screen) return; - var tty = self.tty orelse return; - _ = try tty.write(ctlseqs.rmcup); - try tty.flush(); - self.state.alt_screen = false; - } - - /// write queries to the terminal to determine capabilities. Individual - /// capabilities will be delivered to the client and possibly intercepted by - /// Vaxis to enable features - pub fn queryTerminal(self: *Self) !void { - var tty = self.tty orelse return; - - const colorterm = std.posix.getenv("COLORTERM") orelse ""; - if (std.mem.eql(u8, colorterm, "truecolor") or - std.mem.eql(u8, colorterm, "24bit")) - { - if (@hasField(Event, "cap_rgb")) { - self.postEvent(.cap_rgb); - } - } - - // TODO: decide if we actually want to query for focus and sync. It - // doesn't hurt to blindly use them - // _ = try tty.write(ctlseqs.decrqm_focus); - // _ = try tty.write(ctlseqs.decrqm_sync); - _ = try tty.write(ctlseqs.decrqm_unicode); - _ = try tty.write(ctlseqs.decrqm_color_theme); - // TODO: XTVERSION has a DCS response. uncomment when we can parse - // that - // _ = try tty.write(ctlseqs.xtversion); - _ = try tty.write(ctlseqs.csi_u_query); - _ = try tty.write(ctlseqs.kitty_graphics_query); - // TODO: sixel geometry query interferes with F4 keys. - // _ = try tty.write(ctlseqs.sixel_geometry_query); - - // TODO: XTGETTCAP queries ("RGB", "Smulx") - - _ = try tty.write(ctlseqs.primary_device_attrs); - try tty.flush(); - - // 1 second timeout - std.Thread.Futex.timedWait(&self.query_futex, 0, 1 * std.time.ns_per_s) catch {}; - - // enable detected features - if (self.caps.kitty_keyboard) { - try self.enableKittyKeyboard(.{}); - } - if (self.caps.unicode) { - _ = try tty.write(ctlseqs.unicode_set); - } - } - - // the next render call will refresh the entire screen - pub fn queueRefresh(self: *Self) void { - self.refresh = true; - } - - /// draws the screen to the terminal - pub fn render(self: *Self) !void { - var tty = self.tty orelse return; - self.renders += 1; - self.render_timer.reset(); - defer { - self.render_dur += self.render_timer.read() / std.time.ns_per_us; - } - - defer self.refresh = false; - defer tty.flush() catch {}; - - // Set up sync before we write anything - // TODO: optimize sync so we only sync _when we have changes_. This - // requires a smarter buffered writer, we'll probably have to write - // our own - _ = try tty.write(ctlseqs.sync_set); - defer _ = tty.write(ctlseqs.sync_reset) catch {}; - - // Send the cursor to 0,0 - // TODO: this needs to move after we optimize writes. We only do - // this if we have an update to make. We also need to hide cursor - // and then reshow it if needed - _ = try tty.write(ctlseqs.hide_cursor); - _ = try tty.write(ctlseqs.home); - _ = try tty.write(ctlseqs.sgr_reset); - - // initialize some variables - var reposition: bool = false; - var row: usize = 0; - var col: usize = 0; - var cursor: Style = .{}; - var link: Hyperlink = .{}; - - // Clear all images - _ = try tty.write(ctlseqs.kitty_graphics_clear); - - var i: usize = 0; - while (i < self.screen.buf.len) { - const cell = self.screen.buf[i]; - defer { - // advance by the width of this char mod 1 - const w = blk: { - if (cell.char.width != 0) break :blk cell.char.width; - - const method: gwidth.Method = if (self.caps.unicode) .unicode else .wcwidth; - const width = gwidth.gwidth(cell.char.grapheme, method) catch 1; - break :blk @max(1, width); - }; - std.debug.assert(w > 0); - var j = i + 1; - while (j < i + w) : (j += 1) { - if (j >= self.screen_last.buf.len) break; - self.screen_last.buf[j].skipped = true; - } - col += w; - i += w; - } - if (col >= self.screen.width) { - row += 1; - col = 0; - reposition = true; - } - // If cell is the same as our last frame, we don't need to do - // anything - const last = self.screen_last.buf[i]; - if (!self.refresh and last.eql(cell) and !last.skipped and cell.image == null) { - reposition = true; - // Close any osc8 sequence we might be in before - // repositioning - if (link.uri.len > 0) { - _ = try tty.write(ctlseqs.osc8_clear); - } - continue; - } - self.screen_last.buf[i].skipped = false; - defer { - cursor = cell.style; - link = cell.link; - } - // Set this cell in the last frame - self.screen_last.writeCell(col, row, cell); - - // reposition the cursor, if needed - if (reposition) { - try std.fmt.format(tty.buffered_writer.writer(), ctlseqs.cup, .{ row + 1, col + 1 }); - } - - if (cell.image) |img| { - if (img.size) |size| { - try std.fmt.format( - tty.buffered_writer.writer(), - ctlseqs.kitty_graphics_scale, - .{ img.img_id, img.z_index, size.cols, size.rows }, - ); - } else { - try std.fmt.format( - tty.buffered_writer.writer(), - ctlseqs.kitty_graphics_place, - .{ img.img_id, img.z_index }, - ); - } - } - - // something is different, so let's loop through everything and - // find out what - - // foreground - if (!std.meta.eql(cursor.fg, cell.style.fg)) { - const writer = tty.buffered_writer.writer(); - switch (cell.style.fg) { - .default => _ = try tty.write(ctlseqs.fg_reset), - .index => |idx| { - switch (idx) { - 0...7 => try std.fmt.format(writer, ctlseqs.fg_base, .{idx}), - 8...15 => try std.fmt.format(writer, ctlseqs.fg_bright, .{idx - 8}), - else => try std.fmt.format(writer, ctlseqs.fg_indexed, .{idx}), - } - }, - .rgb => |rgb| { - try std.fmt.format(writer, ctlseqs.fg_rgb, .{ rgb[0], rgb[1], rgb[2] }); - }, - } - } - // background - if (!std.meta.eql(cursor.bg, cell.style.bg)) { - const writer = tty.buffered_writer.writer(); - switch (cell.style.bg) { - .default => _ = try tty.write(ctlseqs.bg_reset), - .index => |idx| { - switch (idx) { - 0...7 => try std.fmt.format(writer, ctlseqs.bg_base, .{idx}), - 8...15 => try std.fmt.format(writer, ctlseqs.bg_bright, .{idx - 8}), - else => try std.fmt.format(writer, ctlseqs.bg_indexed, .{idx}), - } - }, - .rgb => |rgb| { - try std.fmt.format(writer, ctlseqs.bg_rgb, .{ rgb[0], rgb[1], rgb[2] }); - }, - } - } - // underline color - if (!std.meta.eql(cursor.ul, cell.style.ul)) { - const writer = tty.buffered_writer.writer(); - switch (cell.style.bg) { - .default => _ = try tty.write(ctlseqs.ul_reset), - .index => |idx| { - try std.fmt.format(writer, ctlseqs.ul_indexed, .{idx}); - }, - .rgb => |rgb| { - try std.fmt.format(writer, ctlseqs.ul_rgb, .{ rgb[0], rgb[1], rgb[2] }); - }, - } - } - // underline style - if (!std.meta.eql(cursor.ul_style, cell.style.ul_style)) { - const seq = switch (cell.style.ul_style) { - .off => ctlseqs.ul_off, - .single => ctlseqs.ul_single, - .double => ctlseqs.ul_double, - .curly => ctlseqs.ul_curly, - .dotted => ctlseqs.ul_dotted, - .dashed => ctlseqs.ul_dashed, - }; - _ = try tty.write(seq); - } - // bold - if (cursor.bold != cell.style.bold) { - const seq = switch (cell.style.bold) { - true => ctlseqs.bold_set, - false => ctlseqs.bold_dim_reset, - }; - _ = try tty.write(seq); - if (cell.style.dim) { - _ = try tty.write(ctlseqs.dim_set); - } - } - // dim - if (cursor.dim != cell.style.dim) { - const seq = switch (cell.style.dim) { - true => ctlseqs.dim_set, - false => ctlseqs.bold_dim_reset, - }; - _ = try tty.write(seq); - if (cell.style.bold) { - _ = try tty.write(ctlseqs.bold_set); - } - } - // dim - if (cursor.italic != cell.style.italic) { - const seq = switch (cell.style.italic) { - true => ctlseqs.italic_set, - false => ctlseqs.italic_reset, - }; - _ = try tty.write(seq); - } - // dim - if (cursor.blink != cell.style.blink) { - const seq = switch (cell.style.blink) { - true => ctlseqs.blink_set, - false => ctlseqs.blink_reset, - }; - _ = try tty.write(seq); - } - // reverse - if (cursor.reverse != cell.style.reverse) { - const seq = switch (cell.style.reverse) { - true => ctlseqs.reverse_set, - false => ctlseqs.reverse_reset, - }; - _ = try tty.write(seq); - } - // invisible - if (cursor.invisible != cell.style.invisible) { - const seq = switch (cell.style.invisible) { - true => ctlseqs.invisible_set, - false => ctlseqs.invisible_reset, - }; - _ = try tty.write(seq); - } - // strikethrough - if (cursor.strikethrough != cell.style.strikethrough) { - const seq = switch (cell.style.strikethrough) { - true => ctlseqs.strikethrough_set, - false => ctlseqs.strikethrough_reset, - }; - _ = try tty.write(seq); - } - - // url - if (!std.meta.eql(link.uri, cell.link.uri)) { - var ps = cell.link.params; - if (cell.link.uri.len == 0) { - // Empty out the params no matter what if we don't have - // a url - ps = ""; - } - const writer = tty.buffered_writer.writer(); - try std.fmt.format(writer, ctlseqs.osc8, .{ ps, cell.link.uri }); - } - _ = try tty.write(cell.char.grapheme); - } - if (self.screen.cursor_vis) { - try std.fmt.format( - tty.buffered_writer.writer(), - ctlseqs.cup, - .{ - self.screen.cursor_row + 1, - self.screen.cursor_col + 1, - }, - ); - _ = try tty.write(ctlseqs.show_cursor); - } - if (self.screen.mouse_shape != self.screen_last.mouse_shape) { - try std.fmt.format( - tty.buffered_writer.writer(), - ctlseqs.osc22_mouse_shape, - .{@tagName(self.screen.mouse_shape)}, - ); - self.screen_last.mouse_shape = self.screen.mouse_shape; - } - if (self.screen.cursor_shape != self.screen_last.cursor_shape) { - try std.fmt.format( - tty.buffered_writer.writer(), - ctlseqs.cursor_shape, - .{@intFromEnum(self.screen.cursor_shape)}, - ); - self.screen_last.cursor_shape = self.screen.cursor_shape; - } - } - - fn enableKittyKeyboard(self: *Self, flags: Key.KittyFlags) !void { - self.state.kitty_keyboard = true; - const flag_int: u5 = @bitCast(flags); - try std.fmt.format( - self.tty.?.buffered_writer.writer(), - ctlseqs.csi_u_push, - .{ - flag_int, - }, - ); - try self.tty.?.flush(); - } - - /// send a system notification - pub fn notify(self: *Self, title: ?[]const u8, body: []const u8) !void { - if (self.tty == null) return; - if (title) |t| { - try std.fmt.format( - self.tty.?.buffered_writer.writer(), - ctlseqs.osc777_notify, - .{ t, body }, - ); - } else { - try std.fmt.format( - self.tty.?.buffered_writer.writer(), - ctlseqs.osc9_notify, - .{body}, - ); - } - try self.tty.?.flush(); - } - - /// sets the window title - pub fn setTitle(self: *Self, title: []const u8) !void { - if (self.tty == null) return; - try std.fmt.format( - self.tty.?.buffered_writer.writer(), - ctlseqs.osc2_set_title, - .{title}, - ); - try self.tty.?.flush(); - } - - // turn bracketed paste on or off. An event will be sent at the - // beginning and end of a detected paste. All keystrokes between these - // events were pasted - pub fn setBracketedPaste(self: *Self, enable: bool) !void { - if (self.tty == null) return; - self.state.bracketed_paste = enable; - const seq = if (enable) - ctlseqs.bp_set - else - ctlseqs.bp_reset; - _ = try self.tty.?.write(seq); - try self.tty.?.flush(); - } - - /// set the mouse shape - pub fn setMouseShape(self: *Self, shape: Shape) void { - self.screen.mouse_shape = shape; - } - - /// turn mouse reporting on or off - pub fn setMouseMode(self: *Self, enable: bool) !void { - var tty = self.tty orelse return; - self.state.mouse = enable; - if (enable) { - _ = try tty.write(ctlseqs.mouse_set); - try tty.flush(); - } else { - _ = try tty.write(ctlseqs.mouse_reset); - try tty.flush(); - } - } - - pub fn loadImage( - self: *Self, - alloc: std.mem.Allocator, - src: Image.Source, - ) !Image { - if (!self.caps.kitty_graphics) return error.NoGraphicsCapability; - var tty = self.tty orelse return error.NoTTY; - defer self.next_img_id += 1; - - const writer = tty.buffered_writer.writer(); - - var img = switch (src) { - .path => |path| try zigimg.Image.fromFilePath(alloc, path), - .mem => |bytes| try zigimg.Image.fromMemory(alloc, bytes), - }; - defer img.deinit(); - const png_buf = try alloc.alloc(u8, img.imageByteSize()); - defer alloc.free(png_buf); - const png = try img.writeToMemory(png_buf, .{ .png = .{} }); - const b64_buf = try alloc.alloc(u8, base64.calcSize(png.len)); - const encoded = base64.encode(b64_buf, png); - defer alloc.free(b64_buf); - - const id = self.next_img_id; - - log.debug("transmitting kitty image: id={d}, len={d}", .{ id, encoded.len }); - - if (encoded.len < 4096) { - try std.fmt.format( - writer, - "\x1b_Gf=100,i={d};{s}\x1b\\", - .{ - id, - encoded, - }, - ); - } else { - var n: usize = 4096; - - try std.fmt.format( - writer, - "\x1b_Gf=100,i={d},m=1;{s}\x1b\\", - .{ id, encoded[0..n] }, - ); - while (n < encoded.len) : (n += 4096) { - const end: usize = @min(n + 4096, encoded.len); - const m: u2 = if (end == encoded.len) 0 else 1; - try std.fmt.format( - writer, - "\x1b_Gm={d};{s}\x1b\\", - .{ - m, - encoded[n..end], - }, - ); - } - } - try tty.buffered_writer.flush(); - return .{ - .id = id, - .width = img.width, - .height = img.height, - }; - } - - /// deletes an image from the terminal's memory - pub fn freeImage(self: Self, id: u32) void { - var tty = self.tty orelse return; - const writer = tty.buffered_writer.writer(); - std.fmt.format(writer, "\x1b_Ga=d,d=I,i={d};\x1b\\", .{id}) catch |err| { - log.err("couldn't delete image {d}: {}", .{ id, err }); - return; - }; - tty.buffered_writer.flush() catch |err| { - log.err("couldn't flush writer: {}", .{err}); - }; - } - }; -} - -// test "Vaxis: event queueing" { -// const Event = union(enum) { -// key: void, -// }; -// var vx: Vaxis(Event) = try Vaxis(Event).init(.{}); -// defer vx.deinit(null); -// vx.postEvent(.{ .key = {} }); -// const event = vx.nextEvent(); -// try std.testing.expect(event == .key); -// }