this repo has no description
13
fork

Configure Feed

Select the types of activity you want to include in your feed.

image: implement transmitting images

Signed-off-by: Tim Culverhouse <tim@timculverhouse.com>

+201 -38
+1 -1
build.zig
··· 20 20 21 21 const exe = b.addExecutable(.{ 22 22 .name = "vaxis", 23 - .root_source_file = .{ .path = "examples/text_input.zig" }, 23 + .root_source_file = .{ .path = "examples/image.zig" }, 24 24 .target = target, 25 25 .optimize = optimize, 26 26 });
+87
examples/image.zig
··· 1 + const std = @import("std"); 2 + const vaxis = @import("vaxis"); 3 + 4 + const log = std.log.scoped(.main); 5 + 6 + // Our EventType. This can contain internal events as well as Vaxis events. 7 + // Internal events can be posted into the same queue as vaxis events to allow 8 + // for a single event loop with exhaustive switching. Booya 9 + const Event = union(enum) { 10 + key_press: vaxis.Key, 11 + winsize: vaxis.Winsize, 12 + focus_in, 13 + focus_out, 14 + foo: u8, 15 + }; 16 + 17 + pub fn main() !void { 18 + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 19 + defer { 20 + const deinit_status = gpa.deinit(); 21 + //fail test; can't try in defer as defer is executed after we return 22 + if (deinit_status == .leak) { 23 + log.err("memory leak", .{}); 24 + } 25 + } 26 + const alloc = gpa.allocator(); 27 + 28 + // Initialize Vaxis with our event type 29 + var vx = try vaxis.init(Event, .{}); 30 + // deinit takes an optional allocator. If your program is exiting, you can 31 + // choose to pass a null allocator to save some exit time. 32 + defer vx.deinit(alloc); 33 + 34 + // Start the read loop. This puts the terminal in raw mode and begins 35 + // reading user input 36 + try vx.startReadThread(); 37 + defer vx.stopReadThread(); 38 + 39 + // Optionally enter the alternate screen 40 + try vx.enterAltScreen(); 41 + 42 + // Sends queries to terminal to detect certain features. This should 43 + // _always_ be called, but is left to the application to decide when 44 + try vx.queryTerminal(); 45 + 46 + var img = try vaxis.Image.init(alloc, .{ .path = "vaxis.png" }, 1, .kitty); 47 + defer img.deinit(); 48 + 49 + // The main event loop. Vaxis provides a thread safe, blocking, buffered 50 + // queue which can serve as the primary event queue for an application 51 + outer: while (true) { 52 + // nextEvent blocks until an event is in the queue 53 + const event = vx.nextEvent(); 54 + log.debug("event: {}\r\n", .{event}); 55 + // exhaustive switching ftw. Vaxis will send events if your EventType 56 + // enum has the fields for those events (ie "key_press", "winsize") 57 + switch (event) { 58 + .key_press => |key| { 59 + if (key.matches('c', .{ .ctrl = true })) { 60 + break :outer; 61 + } else if (key.matches('l', .{ .ctrl = true })) { 62 + vx.queueRefresh(); 63 + } else if (key.matches('n', .{ .ctrl = true })) { 64 + try vx.notify("vaxis", "hello from vaxis"); 65 + } else {} 66 + }, 67 + 68 + .winsize => |ws| try vx.resize(alloc, ws), 69 + else => {}, 70 + } 71 + 72 + // vx.window() returns the root window. This window is the size of the 73 + // terminal and can spawn child windows as logical areas. Child windows 74 + // cannot draw outside of their bounds 75 + const win = vx.window(); 76 + 77 + // Clear the entire space because we are drawing in immediate mode. 78 + // vaxis double buffers the screen. This new frame will be compared to 79 + // the old and only updated cells will be drawn 80 + win.clear(); 81 + 82 + try img.draw(win); 83 + 84 + // Render the screen 85 + try vx.render(); 86 + } 87 + }
+2 -2
src/Screen.zig
··· 10 10 const Screen = @This(); 11 11 12 12 pub const Placement = struct { 13 - img: *Image, 13 + img: Image, 14 14 placement_id: u32, 15 15 col: usize, 16 16 row: usize, ··· 75 75 self: *Screen, 76 76 col: usize, 77 77 row: usize, 78 - img: *Image, 78 + img: Image, 79 79 placement_id: u32, 80 80 ) !void { 81 81 const p = Placement{
+2 -2
src/Window.zig
··· 72 72 /// writes an image to the location in the window 73 73 pub fn writeImage( 74 74 self: Window, 75 - img: *Image, 75 + img: Image, 76 76 placement_id: u32, 77 77 ) !void { 78 78 if (self.height == 0 or self.width == 0) return; 79 - self.screen.writeImage(self.x_off, self.y_off, img, placement_id); 79 + try self.screen.writeImage(self.x_off, self.y_off, img, placement_id); 80 80 } 81 81 82 82 /// fills the window with the default cell
+56 -17
src/image/Kitty.zig
··· 1 1 const std = @import("std"); 2 + const fmt = std.fmt; 2 3 const math = std.math; 3 4 const testing = std.testing; 5 + const base64 = std.base64.standard.Encoder; 4 6 const zigimg = @import("zigimg"); 5 - const png = zigimg.png; 6 7 7 8 const Window = @import("../Window.zig"); 8 9 const Winsize = @import("../Tty.zig").Winsize; 10 + 11 + const log = std.log.scoped(.kitty); 9 12 10 13 const Kitty = @This(); 11 14 15 + const max_chunk: usize = 4096; 16 + 17 + const transmit_opener = "\x1b_Gf=32,i={d},s={d},v={d},m={d};"; 18 + 19 + alloc: std.mem.Allocator, 20 + 12 21 /// the decoded image 13 22 img: zigimg.Image, 14 23 15 24 /// unique identifier for this image. This will be managed by the screen. The ID 16 25 /// is only null for images which have not been transmitted to the screen 17 - id: ?u32 = null, 26 + id: u32, 27 + 28 + pub fn deinit(self: *const Kitty) void { 29 + var img = self.img; 30 + img.deinit(); 31 + } 32 + 33 + /// transmit encodes and transmits the image to the terminal 34 + pub fn transmit(self: Kitty, writer: anytype) !void { 35 + var alloc = self.alloc; 36 + const png_buf = try alloc.alloc(u8, self.img.imageByteSize()); 37 + defer alloc.free(png_buf); 38 + const png = try self.img.writeToMemory(png_buf, .{ .png = .{} }); 39 + const b64_buf = try alloc.alloc(u8, base64.calcSize(png.len)); 40 + const encoded = base64.encode(b64_buf, png); 41 + defer alloc.free(b64_buf); 18 42 19 - /// width of the image, in cells 20 - cell_width: usize, 21 - /// height of the image, in cells 22 - cell_height: usize, 43 + log.debug("transmitting kitty image: id={d}, len={d}", .{ self.id, encoded.len }); 23 44 24 - pub fn deinit(self: *Kitty) void { 25 - self.img.deinit(); 26 - } 45 + if (encoded.len < max_chunk) { 46 + try fmt.format( 47 + writer, 48 + "\x1b_Gf=100,i={d};{s}\x1b\\", 49 + .{ 50 + self.id, 51 + encoded, 52 + }, 53 + ); 54 + } else { 55 + var n: usize = max_chunk; 27 56 28 - pub fn draw(self: *Kitty, win: Window) !void { 29 - const row: u16 = @truncate(win.y_off); 30 - const col: u16 = @truncate(win.x_off); 31 - // the placement id has the high 16 bits as the column and the low 16 32 - // bits as the row. This means we can only place this image one time at 33 - // the same location - which is completely sane 34 - const pid: u32 = col << 16 | row; 35 - try win.writeImage(win.x_off, win.y_off, self, pid); 57 + try fmt.format( 58 + writer, 59 + "\x1b_Gf=100,i={d},m=1;{s}\x1b\\", 60 + .{ self.id, encoded[0..n] }, 61 + ); 62 + while (n < encoded.len) : (n += max_chunk) { 63 + const end: usize = @min(n + max_chunk, encoded.len); 64 + const m: u2 = if (end == encoded.len) 0 else 1; 65 + try fmt.format( 66 + writer, 67 + "\x1b_Gm={d};{s}\x1b\\", 68 + .{ 69 + m, 70 + encoded[n..end], 71 + }, 72 + ); 73 + } 74 + } 36 75 }
+50 -16
src/image/image.zig
··· 8 8 9 9 const Kitty = @import("Kitty.zig"); 10 10 11 - pub const Protocol = enum { 12 - kitty, 13 - // TODO: sixel, full block, half block, quad block 14 - }; 11 + const log = std.log.scoped(.image); 15 12 16 13 pub const Image = union(enum) { 17 14 kitty: Kitty, 18 15 16 + pub const Protocol = enum { 17 + kitty, 18 + // TODO: sixel, full block, half block, quad block 19 + }; 20 + 21 + pub const CellSize = struct { 22 + rows: usize, 23 + cols: usize, 24 + }; 25 + 26 + pub const Source = union(enum) { 27 + path: []const u8, 28 + mem: []const u8, 29 + }; 30 + 19 31 /// initialize a new image 20 32 pub fn init( 21 33 alloc: std.mem.Allocator, 22 - winsize: Winsize, 23 - src: []const u8, 34 + src: Source, 35 + id: u32, 24 36 protocol: Protocol, 25 37 ) !Image { 26 38 const img = switch (src) { 27 39 .path => |path| try zigimg.Image.fromFilePath(alloc, path), 28 40 .mem => |bytes| try zigimg.Image.fromMemory(alloc, bytes), 29 41 }; 30 - // cell geometry 31 - const pix_per_col = try math.divCeil(usize, winsize.x_pixel, winsize.cols); 32 - const pix_per_row = try math.divCeil(usize, winsize.y_pixel, winsize.rows); 33 - 34 - const cell_width = math.divCeil(usize, img.width, pix_per_col) catch 0; 35 - const cell_height = math.divCeil(usize, img.height, pix_per_row) catch 0; 36 42 37 43 switch (protocol) { 38 44 .kitty => { 39 45 return .{ 40 46 .kitty = Kitty{ 47 + .alloc = alloc, 41 48 .img = img, 42 - .cell_width = cell_width, 43 - .cell_height = cell_height, 49 + .id = id, 44 50 }, 45 51 }; 46 52 }, ··· 49 55 50 56 pub fn deinit(self: Image) void { 51 57 switch (self) { 52 - inline else => |case| case.deinit(), 58 + inline else => |*case| case.deinit(), 53 59 } 54 60 } 55 61 56 62 pub fn draw(self: Image, win: Window) !void { 57 63 switch (self) { 58 - inline else => |case| case.draw(win), 64 + .kitty => { 65 + const row: u16 = @truncate(win.y_off); 66 + const col: u16 = @truncate(win.x_off); 67 + // the placement id has the high 16 bits as the column and the low 16 68 + // bits as the row. This means we can only place this image one time at 69 + // the same location - which is completely sane 70 + const pid: u32 = col << 15 | row; 71 + try win.writeImage(self, pid); 72 + }, 73 + } 74 + } 75 + 76 + pub fn transmit(self: Image, writer: anytype) !void { 77 + switch (self) { 78 + .kitty => |k| return k.transmit(writer), 59 79 } 60 80 } 61 81 ··· 63 83 switch (self) { 64 84 .kitty => |k| return k.id, 65 85 } 86 + } 87 + 88 + pub fn cellSize(self: Image, winsize: Winsize) !CellSize { 89 + // cell geometry 90 + const pix_per_col = try math.divCeil(usize, winsize.x_pixel, winsize.cols); 91 + const pix_per_row = try math.divCeil(usize, winsize.y_pixel, winsize.rows); 92 + 93 + const cell_width = math.divCeil(usize, self.img.width, pix_per_col) catch 0; 94 + const cell_height = math.divCeil(usize, self.img.height, pix_per_row) catch 0; 95 + 96 + return CellSize{ 97 + .rows = cell_height, 98 + .cols = cell_width, 99 + }; 66 100 } 67 101 };
+2
src/main.zig
··· 10 10 11 11 pub const widgets = @import("widgets/main.zig"); 12 12 13 + pub const Image = @import("image/image.zig").Image; 14 + 13 15 /// Initialize a Vaxis application. 14 16 pub fn init(comptime EventType: type, opts: Options) !Vaxis(EventType) { 15 17 return Vaxis(EventType).init(opts);
+1
src/vaxis.zig
··· 298 298 } else true; 299 299 if (!transmit) continue; 300 300 // TODO: transmit the new image to the screen 301 + try img.img.transmit(tty.buffered_writer.writer()); 301 302 } 302 303 303 304 var i: usize = 0;