From cc75fe62724d4cfc10a8981b70bb37b131e9f325 Mon Sep 17 00:00:00 2001 From: Tim Culverhouse Date: Tue, 30 Jan 2024 13:30:36 -0600 Subject: [PATCH] image: implement transmitting images Signed-off-by: Tim Culverhouse --- build.zig | 2 +- examples/image.zig | 87 +++++++++++++++++++++++++++++++++++++++++++++ src/Screen.zig | 4 +-- src/Window.zig | 4 +-- src/image/Kitty.zig | 73 ++++++++++++++++++++++++++++--------- src/image/image.zig | 66 +++++++++++++++++++++++++--------- src/main.zig | 2 ++ src/vaxis.zig | 1 + 8 files changed, 201 insertions(+), 38 deletions(-) create mode 100644 examples/image.zig diff --git a/build.zig b/build.zig index e60c15a..cc3840d 100644 --- a/build.zig +++ b/build.zig @@ -20,7 +20,7 @@ pub fn build(b: *std.Build) void { const exe = b.addExecutable(.{ .name = "vaxis", - .root_source_file = .{ .path = "examples/text_input.zig" }, + .root_source_file = .{ .path = "examples/image.zig" }, .target = target, .optimize = optimize, }); diff --git a/examples/image.zig b/examples/image.zig new file mode 100644 index 0000000..501e02d --- /dev/null +++ b/examples/image.zig @@ -0,0 +1,87 @@ +const std = @import("std"); +const vaxis = @import("vaxis"); + +const log = std.log.scoped(.main); + +// Our EventType. This can contain internal events as well as Vaxis events. +// Internal events can be posted into the same queue as vaxis events to allow +// for a single event loop with exhaustive switching. Booya +const Event = union(enum) { + key_press: vaxis.Key, + winsize: vaxis.Winsize, + focus_in, + focus_out, + foo: u8, +}; + +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) { + log.err("memory leak", .{}); + } + } + const alloc = gpa.allocator(); + + // Initialize Vaxis with our event type + var vx = try vaxis.init(Event, .{}); + // 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); + + // Start the read loop. This puts the terminal in raw mode and begins + // reading user input + try vx.startReadThread(); + defer vx.stopReadThread(); + + // Optionally enter the alternate screen + try vx.enterAltScreen(); + + // Sends queries to terminal to detect certain features. This should + // _always_ be called, but is left to the application to decide when + try vx.queryTerminal(); + + var img = try vaxis.Image.init(alloc, .{ .path = "vaxis.png" }, 1, .kitty); + defer img.deinit(); + + // The main event loop. Vaxis provides a thread safe, blocking, buffered + // queue which can serve as the primary event queue for an application + outer: while (true) { + // nextEvent blocks until an event is in the queue + const event = vx.nextEvent(); + log.debug("event: {}\r\n", .{event}); + // exhaustive switching ftw. Vaxis will send events if your EventType + // enum has the fields for those events (ie "key_press", "winsize") + switch (event) { + .key_press => |key| { + if (key.matches('c', .{ .ctrl = true })) { + break :outer; + } else if (key.matches('l', .{ .ctrl = true })) { + vx.queueRefresh(); + } else if (key.matches('n', .{ .ctrl = true })) { + try vx.notify("vaxis", "hello from vaxis"); + } else {} + }, + + .winsize => |ws| try vx.resize(alloc, ws), + else => {}, + } + + // vx.window() returns the root window. This window is the size of the + // terminal and can spawn child windows as logical areas. Child windows + // cannot draw outside of their bounds + const win = vx.window(); + + // Clear the entire space because we are drawing in immediate mode. + // vaxis double buffers the screen. This new frame will be compared to + // the old and only updated cells will be drawn + win.clear(); + + try img.draw(win); + + // Render the screen + try vx.render(); + } +} diff --git a/src/Screen.zig b/src/Screen.zig index d8c2511..abb7c62 100644 --- a/src/Screen.zig +++ b/src/Screen.zig @@ -10,7 +10,7 @@ const log = std.log.scoped(.screen); const Screen = @This(); pub const Placement = struct { - img: *Image, + img: Image, placement_id: u32, col: usize, row: usize, @@ -75,7 +75,7 @@ pub fn writeImage( self: *Screen, col: usize, row: usize, - img: *Image, + img: Image, placement_id: u32, ) !void { const p = Placement{ diff --git a/src/Window.zig b/src/Window.zig index b0dcaf3..e15de67 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -72,11 +72,11 @@ pub fn writeCell(self: Window, col: usize, row: usize, cell: Cell) void { /// writes an image to the location in the window pub fn writeImage( self: Window, - img: *Image, + img: Image, placement_id: u32, ) !void { if (self.height == 0 or self.width == 0) return; - self.screen.writeImage(self.x_off, self.y_off, img, placement_id); + try self.screen.writeImage(self.x_off, self.y_off, img, placement_id); } /// fills the window with the default cell diff --git a/src/image/Kitty.zig b/src/image/Kitty.zig index 6e10a75..cce4827 100644 --- a/src/image/Kitty.zig +++ b/src/image/Kitty.zig @@ -1,36 +1,75 @@ const std = @import("std"); +const fmt = std.fmt; const math = std.math; const testing = std.testing; +const base64 = std.base64.standard.Encoder; const zigimg = @import("zigimg"); -const png = zigimg.png; const Window = @import("../Window.zig"); const Winsize = @import("../Tty.zig").Winsize; +const log = std.log.scoped(.kitty); + const Kitty = @This(); +const max_chunk: usize = 4096; + +const transmit_opener = "\x1b_Gf=32,i={d},s={d},v={d},m={d};"; + +alloc: std.mem.Allocator, + /// the decoded image img: zigimg.Image, /// unique identifier for this image. This will be managed by the screen. The ID /// is only null for images which have not been transmitted to the screen -id: ?u32 = null, +id: u32, -/// width of the image, in cells -cell_width: usize, -/// height of the image, in cells -cell_height: usize, - -pub fn deinit(self: *Kitty) void { - self.img.deinit(); +pub fn deinit(self: *const Kitty) void { + var img = self.img; + img.deinit(); } -pub fn draw(self: *Kitty, win: Window) !void { - const row: u16 = @truncate(win.y_off); - const col: u16 = @truncate(win.x_off); - // the placement id has the high 16 bits as the column and the low 16 - // bits as the row. This means we can only place this image one time at - // the same location - which is completely sane - const pid: u32 = col << 16 | row; - try win.writeImage(win.x_off, win.y_off, self, pid); +/// transmit encodes and transmits the image to the terminal +pub fn transmit(self: Kitty, writer: anytype) !void { + var alloc = self.alloc; + const png_buf = try alloc.alloc(u8, self.img.imageByteSize()); + defer alloc.free(png_buf); + const png = try self.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); + + log.debug("transmitting kitty image: id={d}, len={d}", .{ self.id, encoded.len }); + + if (encoded.len < max_chunk) { + try fmt.format( + writer, + "\x1b_Gf=100,i={d};{s}\x1b\\", + .{ + self.id, + encoded, + }, + ); + } else { + var n: usize = max_chunk; + + try fmt.format( + writer, + "\x1b_Gf=100,i={d},m=1;{s}\x1b\\", + .{ self.id, encoded[0..n] }, + ); + while (n < encoded.len) : (n += max_chunk) { + const end: usize = @min(n + max_chunk, encoded.len); + const m: u2 = if (end == encoded.len) 0 else 1; + try fmt.format( + writer, + "\x1b_Gm={d};{s}\x1b\\", + .{ + m, + encoded[n..end], + }, + ); + } + } } diff --git a/src/image/image.zig b/src/image/image.zig index 24ba4e5..c686ad1 100644 --- a/src/image/image.zig +++ b/src/image/image.zig @@ -8,39 +8,45 @@ const Window = @import("../Window.zig"); const Kitty = @import("Kitty.zig"); -pub const Protocol = enum { - kitty, - // TODO: sixel, full block, half block, quad block -}; +const log = std.log.scoped(.image); pub const Image = union(enum) { kitty: Kitty, + pub const Protocol = enum { + kitty, + // TODO: sixel, full block, half block, quad block + }; + + pub const CellSize = struct { + rows: usize, + cols: usize, + }; + + pub const Source = union(enum) { + path: []const u8, + mem: []const u8, + }; + /// initialize a new image pub fn init( alloc: std.mem.Allocator, - winsize: Winsize, - src: []const u8, + src: Source, + id: u32, protocol: Protocol, ) !Image { const img = switch (src) { .path => |path| try zigimg.Image.fromFilePath(alloc, path), .mem => |bytes| try zigimg.Image.fromMemory(alloc, bytes), }; - // cell geometry - const pix_per_col = try math.divCeil(usize, winsize.x_pixel, winsize.cols); - const pix_per_row = try math.divCeil(usize, winsize.y_pixel, winsize.rows); - - const cell_width = math.divCeil(usize, img.width, pix_per_col) catch 0; - const cell_height = math.divCeil(usize, img.height, pix_per_row) catch 0; switch (protocol) { .kitty => { return .{ .kitty = Kitty{ + .alloc = alloc, .img = img, - .cell_width = cell_width, - .cell_height = cell_height, + .id = id, }, }; }, @@ -49,13 +55,27 @@ pub const Image = union(enum) { pub fn deinit(self: Image) void { switch (self) { - inline else => |case| case.deinit(), + inline else => |*case| case.deinit(), } } pub fn draw(self: Image, win: Window) !void { switch (self) { - inline else => |case| case.draw(win), + .kitty => { + const row: u16 = @truncate(win.y_off); + const col: u16 = @truncate(win.x_off); + // the placement id has the high 16 bits as the column and the low 16 + // bits as the row. This means we can only place this image one time at + // the same location - which is completely sane + const pid: u32 = col << 15 | row; + try win.writeImage(self, pid); + }, + } + } + + pub fn transmit(self: Image, writer: anytype) !void { + switch (self) { + .kitty => |k| return k.transmit(writer), } } @@ -64,4 +84,18 @@ pub const Image = union(enum) { .kitty => |k| return k.id, } } + + pub fn cellSize(self: Image, winsize: Winsize) !CellSize { + // cell geometry + const pix_per_col = try math.divCeil(usize, winsize.x_pixel, winsize.cols); + const pix_per_row = try math.divCeil(usize, winsize.y_pixel, winsize.rows); + + const cell_width = math.divCeil(usize, self.img.width, pix_per_col) catch 0; + const cell_height = math.divCeil(usize, self.img.height, pix_per_row) catch 0; + + return CellSize{ + .rows = cell_height, + .cols = cell_width, + }; + } }; diff --git a/src/main.zig b/src/main.zig index 2367f88..a173708 100644 --- a/src/main.zig +++ b/src/main.zig @@ -10,6 +10,8 @@ pub const Winsize = @import("Tty.zig").Winsize; pub const widgets = @import("widgets/main.zig"); +pub const Image = @import("image/image.zig").Image; + /// Initialize a Vaxis application. pub fn init(comptime EventType: type, opts: Options) !Vaxis(EventType) { return Vaxis(EventType).init(opts); diff --git a/src/vaxis.zig b/src/vaxis.zig index f28383e..1af2aeb 100644 --- a/src/vaxis.zig +++ b/src/vaxis.zig @@ -298,6 +298,7 @@ pub fn Vaxis(comptime T: type) type { } else true; if (!transmit) continue; // TODO: transmit the new image to the screen + try img.img.transmit(tty.buffered_writer.writer()); } var i: usize = 0;