this repo has no description
13
fork

Configure Feed

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

images: kitty image protocol works

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

+167 -265
+7 -3
examples/image.zig
··· 43 43 // _always_ be called, but is left to the application to decide when 44 44 try vx.queryTerminal(); 45 45 46 - var img = try vaxis.Image.init(alloc, .{ .path = "vaxis.png" }, 1, .kitty); 47 - defer img.deinit(); 46 + const img = try vx.loadImage(alloc, .{ .path = "vaxis.png" }); 47 + 48 + var n: usize = 0; 48 49 49 50 // The main event loop. Vaxis provides a thread safe, blocking, buffered 50 51 // queue which can serve as the primary event queue for an application ··· 56 57 // enum has the fields for those events (ie "key_press", "winsize") 57 58 switch (event) { 58 59 .key_press => |key| { 60 + n += 1; 59 61 if (key.matches('c', .{ .ctrl = true })) { 60 62 break :outer; 61 63 } else if (key.matches('l', .{ .ctrl = true })) { ··· 79 81 // the old and only updated cells will be drawn 80 82 win.clear(); 81 83 82 - try img.draw(win); 84 + const child = win.initChild(n, n, .expand, .expand); 85 + 86 + img.draw(child, false, 0); 83 87 84 88 // Render the screen 85 89 try vx.render();
+61
src/Image.zig
··· 1 + const std = @import("std"); 2 + const fmt = std.fmt; 3 + const math = std.math; 4 + const testing = std.testing; 5 + const base64 = std.base64.standard.Encoder; 6 + const zigimg = @import("zigimg"); 7 + 8 + const Window = @import("Window.zig"); 9 + const Winsize = @import("Tty.zig").Winsize; 10 + 11 + const log = std.log.scoped(.image); 12 + 13 + const Image = @This(); 14 + 15 + const transmit_opener = "\x1b_Gf=32,i={d},s={d},v={d},m={d};"; 16 + 17 + pub const Source = union(enum) { 18 + path: []const u8, 19 + mem: []const u8, 20 + }; 21 + 22 + pub const Placement = struct { 23 + img_id: u32, 24 + z_index: i32, 25 + scale: bool, 26 + }; 27 + 28 + pub const CellSize = struct { 29 + rows: usize, 30 + cols: usize, 31 + }; 32 + 33 + /// unique identifier for this image. This will be managed by the screen. 34 + id: u32, 35 + 36 + // width in pixels 37 + width: usize, 38 + // height in pixels 39 + height: usize, 40 + 41 + pub fn draw(self: Image, win: Window, scale: bool, z_index: i32) void { 42 + const p = Placement{ 43 + .img_id = self.id, 44 + .z_index = z_index, 45 + .scale = scale, 46 + }; 47 + win.writeCell(0, 0, .{ .image = p }); 48 + } 49 + 50 + pub fn cellSize(self: Image, winsize: Winsize) !CellSize { 51 + // cell geometry 52 + const pix_per_col = try std.math.divCeil(usize, winsize.x_pixel, winsize.cols); 53 + const pix_per_row = try std.math.divCeil(usize, winsize.y_pixel, winsize.rows); 54 + 55 + const cell_width = std.math.divCeil(usize, self.width, pix_per_col) catch 0; 56 + const cell_height = std.math.divCeil(usize, self.height, pix_per_row) catch 0; 57 + return .{ 58 + .rows = cell_height, 59 + .cols = cell_width, 60 + }; 61 + }
-6
src/InternalScreen.zig
··· 3 3 const Style = @import("cell.zig").Style; 4 4 const Cell = @import("cell.zig").Cell; 5 5 const Shape = @import("Mouse.zig").Shape; 6 - const Image = @import("image/image.zig").Image; 7 - const Placement = @import("Screen.zig").Placement; 8 6 9 7 const log = std.log.scoped(.internal_screen); 10 8 ··· 37 35 38 36 mouse_shape: Shape = .default, 39 37 40 - images: std.ArrayList(Placement) = undefined, 41 - 42 38 /// sets each cell to the default cell 43 39 pub fn init(alloc: std.mem.Allocator, w: usize, h: usize) !InternalScreen { 44 40 var screen = InternalScreen{ 45 41 .buf = try alloc.alloc(InternalCell, w * h), 46 - .images = std.ArrayList(Placement).init(alloc), 47 42 }; 48 43 for (screen.buf, 0..) |_, i| { 49 44 screen.buf[i] = .{ ··· 58 53 } 59 54 60 55 pub fn deinit(self: *InternalScreen, alloc: std.mem.Allocator) void { 61 - self.images.deinit(); 62 56 for (self.buf, 0..) |_, i| { 63 57 self.buf[i].char.deinit(); 64 58 self.buf[i].uri.deinit();
+10 -37
src/Screen.zig
··· 3 3 4 4 const Cell = @import("cell.zig").Cell; 5 5 const Shape = @import("Mouse.zig").Shape; 6 - const Image = @import("image/image.zig").Image; 6 + const Image = @import("Image.zig"); 7 + const Winsize = @import("Tty.zig").Winsize; 7 8 8 9 const log = std.log.scoped(.screen); 9 10 10 11 const Screen = @This(); 11 12 12 - pub const Placement = struct { 13 - img: Image, 14 - placement_id: u32, 15 - col: usize, 16 - row: usize, 17 - 18 - /// two placements are considered equal if their image id and their 19 - /// placement id are equal 20 - pub fn eql(self: Placement, tgt: Placement) bool { 21 - if (self.img.getId() != tgt.img.getId()) return false; 22 - if (self.placement_id != tgt.placement_id) return false; 23 - return true; 24 - } 25 - }; 26 - 27 13 width: usize = 0, 28 14 height: usize = 0, 15 + 16 + width_pix: usize = 0, 17 + height_pix: usize = 0, 29 18 30 19 buf: []Cell = undefined, 31 20 ··· 37 26 38 27 mouse_shape: Shape = .default, 39 28 40 - images: std.ArrayList(Placement) = undefined, 41 - 42 - pub fn init(alloc: std.mem.Allocator, w: usize, h: usize) !Screen { 29 + pub fn init(alloc: std.mem.Allocator, winsize: Winsize) !Screen { 30 + const w = winsize.cols; 31 + const h = winsize.rows; 43 32 var self = Screen{ 44 33 .buf = try alloc.alloc(Cell, w * h), 45 34 .width = w, 46 35 .height = h, 47 - .images = std.ArrayList(Placement).init(alloc), 36 + .width_pix = winsize.x_pixel, 37 + .height_pix = winsize.y_pixel, 48 38 }; 49 39 for (self.buf, 0..) |_, i| { 50 40 self.buf[i] = .{}; ··· 53 43 } 54 44 pub fn deinit(self: *Screen, alloc: std.mem.Allocator) void { 55 45 alloc.free(self.buf); 56 - self.images.deinit(); 57 46 } 58 47 59 48 /// writes a cell to a location. 0 indexed ··· 70 59 assert(i < self.buf.len); 71 60 self.buf[i] = cell; 72 61 } 73 - 74 - pub fn writeImage( 75 - self: *Screen, 76 - col: usize, 77 - row: usize, 78 - img: Image, 79 - placement_id: u32, 80 - ) !void { 81 - const p = Placement{ 82 - .img = img, 83 - .placement_id = placement_id, 84 - .col = col, 85 - .row = row, 86 - }; 87 - try self.images.append(p); 88 - }
-18
src/Window.zig
··· 69 69 self.screen.writeCell(col + self.x_off, row + self.y_off, cell); 70 70 } 71 71 72 - /// writes an image to the location in the window 73 - pub fn writeImage( 74 - self: Window, 75 - img: Image, 76 - placement_id: u32, 77 - ) !void { 78 - if (self.height == 0 or self.width == 0) return; 79 - try self.screen.writeImage(self.x_off, self.y_off, img, placement_id); 80 - } 81 - 82 72 /// fills the window with the default cell 83 73 pub fn clear(self: Window) void { 84 74 self.fill(.{}); 85 - // we clear any image with it's first cell within this window 86 - for (self.screen.images.items, 0..) |p, i| { 87 - if (p.col >= self.x_off and p.col < self.width and 88 - p.row >= self.y_off and p.row < self.height) 89 - { 90 - _ = self.screen.images.swapRemove(i); 91 - } 92 - } 93 75 } 94 76 95 77 /// returns the width of the grapheme. This depends on the terminal capabilities
+3
src/cell.zig
··· 1 + const Image = @import("Image.zig"); 2 + 1 3 pub const Cell = struct { 2 4 char: Character = .{}, 3 5 style: Style = .{}, 4 6 link: Hyperlink = .{}, 7 + image: ?Image.Placement = null, 5 8 }; 6 9 7 10 pub const Character = struct {
+5
src/ctlseqs.zig
··· 89 89 pub const osc9_notify = "\x1b]9;{s}\x1b\\"; 90 90 pub const osc777_notify = "\x1b]777;notify;{s};{s}\x1b\\"; 91 91 pub const osc22_mouse_shape = "\x1b]22;{s}\x1b\\"; 92 + 93 + // Kitty graphics 94 + pub const kitty_graphics_clear = "\x1b_Ga=d\x1b\\"; 95 + pub const kitty_graphics_place = "\x1b_Ga=p,i={d},z={d},C=1\x1b\\"; 96 + pub const kitty_graphics_scale = "\x1b_Ga=p,i={d},z={d},c={d},r={d},C=1\x1b\\";
-75
src/image/Kitty.zig
··· 1 - const std = @import("std"); 2 - const fmt = std.fmt; 3 - const math = std.math; 4 - const testing = std.testing; 5 - const base64 = std.base64.standard.Encoder; 6 - const zigimg = @import("zigimg"); 7 - 8 - const Window = @import("../Window.zig"); 9 - const Winsize = @import("../Tty.zig").Winsize; 10 - 11 - const log = std.log.scoped(.kitty); 12 - 13 - const Kitty = @This(); 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 - 21 - /// the decoded image 22 - img: zigimg.Image, 23 - 24 - /// unique identifier for this image. This will be managed by the screen. The ID 25 - /// is only null for images which have not been transmitted to the screen 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); 42 - 43 - log.debug("transmitting kitty image: id={d}, len={d}", .{ self.id, encoded.len }); 44 - 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; 56 - 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 - } 75 - }
-101
src/image/image.zig
··· 1 - const std = @import("std"); 2 - const math = std.math; 3 - const testing = std.testing; 4 - const zigimg = @import("zigimg"); 5 - 6 - const Winsize = @import("../Tty.zig").Winsize; 7 - const Window = @import("../Window.zig"); 8 - 9 - const Kitty = @import("Kitty.zig"); 10 - 11 - const log = std.log.scoped(.image); 12 - 13 - pub const Image = union(enum) { 14 - kitty: Kitty, 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 - 31 - /// initialize a new image 32 - pub fn init( 33 - alloc: std.mem.Allocator, 34 - src: Source, 35 - id: u32, 36 - protocol: Protocol, 37 - ) !Image { 38 - const img = switch (src) { 39 - .path => |path| try zigimg.Image.fromFilePath(alloc, path), 40 - .mem => |bytes| try zigimg.Image.fromMemory(alloc, bytes), 41 - }; 42 - 43 - switch (protocol) { 44 - .kitty => { 45 - return .{ 46 - .kitty = Kitty{ 47 - .alloc = alloc, 48 - .img = img, 49 - .id = id, 50 - }, 51 - }; 52 - }, 53 - } 54 - } 55 - 56 - pub fn deinit(self: Image) void { 57 - switch (self) { 58 - inline else => |*case| case.deinit(), 59 - } 60 - } 61 - 62 - pub fn draw(self: Image, win: Window) !void { 63 - switch (self) { 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), 79 - } 80 - } 81 - 82 - pub fn getId(self: Image) ?u32 { 83 - switch (self) { 84 - .kitty => |k| return k.id, 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 - }; 100 - } 101 - };
+1 -1
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; 13 + pub const Image = @import("Image.zig"); 14 14 15 15 /// Initialize a Vaxis application. 16 16 pub fn init(comptime EventType: type, opts: Options) !Vaxis(EventType) {
+80 -24
src/vaxis.zig
··· 1 1 const std = @import("std"); 2 2 const atomic = std.atomic; 3 + const base64 = std.base64.standard.Encoder; 3 4 4 5 const Queue = @import("queue.zig").Queue; 5 6 const ctlseqs = @import("ctlseqs.zig"); ··· 14 15 const Hyperlink = @import("cell.zig").Hyperlink; 15 16 const gwidth = @import("gwidth.zig"); 16 17 const Shape = @import("Mouse.zig").Shape; 17 - const Placement = Screen.Placement; 18 + const Image = @import("Image.zig"); 19 + const zigimg = @import("zigimg"); 18 20 19 21 /// Vaxis is the entrypoint for a Vaxis application. The provided type T should 20 22 /// be a tagged union which contains all of the events the application will ··· 58 60 alt_screen: bool = false, 59 61 /// if we have entered kitty keyboard 60 62 kitty_keyboard: bool = false, 63 + // TODO: should be false but we aren't querying yet 64 + kitty_graphics: bool = true, 61 65 bracketed_paste: bool = false, 62 66 mouse: bool = false, 63 67 } = .{}, ··· 71 75 /// futex times out 72 76 query_futex: atomic.Value(u32) = atomic.Value(u32).init(0), 73 77 78 + // images 79 + next_img_id: u32 = 1, 80 + 74 81 // statistics 75 82 renders: usize = 0, 76 83 render_dur: i128 = 0, ··· 151 158 pub fn resize(self: *Self, alloc: std.mem.Allocator, winsize: Winsize) !void { 152 159 log.debug("resizing screen: width={d} height={d}", .{ winsize.cols, winsize.rows }); 153 160 self.screen.deinit(alloc); 154 - self.screen = try Screen.init(alloc, winsize.cols, winsize.rows); 161 + self.screen = try Screen.init(alloc, winsize); 155 162 self.screen.unicode = self.caps.unicode; 156 163 // try self.screen.int(alloc, winsize.cols, winsize.rows); 157 164 // we only init our current screen. This has the effect of redrawing ··· 243 250 // the next render call will refresh the entire screen 244 251 pub fn queueRefresh(self: *Self) void { 245 252 self.refresh = true; 246 - self.screen_last.images.clearRetainingCapacity(); 247 253 } 248 254 249 255 /// draws the screen to the terminal ··· 280 286 var cursor: Style = .{}; 281 287 var link: Hyperlink = .{}; 282 288 283 - // remove images from the screen by looping through the last state 284 - // and comparing to the next state 285 - for (self.screen_last.images.items) |last_img| { 286 - const keep: bool = for (self.screen.images.items) |next_img| { 287 - if (last_img.eql(next_img)) break true; 288 - } else false; 289 - if (keep) continue; 290 - // TODO: remove image placements 291 - } 292 - 293 - // add new images. Could slightly optimize by knowing which images 294 - // we need to keep from the remove loop 295 - for (self.screen.images.items) |img| { 296 - const transmit: bool = for (self.screen_last.images.items) |last_img| { 297 - if (last_img.eql(img)) break false; 298 - } else true; 299 - if (!transmit) continue; 300 - // TODO: transmit the new image to the screen 301 - try img.img.transmit(tty.buffered_writer.writer()); 302 - } 289 + // Clear all images 290 + _ = try tty.write(ctlseqs.kitty_graphics_clear); 303 291 304 292 var i: usize = 0; 305 293 while (i < self.screen.buf.len) { ··· 326 314 // If cell is the same as our last frame, we don't need to do 327 315 // anything 328 316 const last = self.screen_last.buf[i]; 329 - if (!self.refresh and last.eql(cell) and !last.skipped) { 317 + if (!self.refresh and last.eql(cell) and !last.skipped and cell.image == null) { 330 318 reposition = true; 331 319 // Close any osc8 sequence we might be in before 332 320 // repositioning ··· 346 334 // reposition the cursor, if needed 347 335 if (reposition) { 348 336 try std.fmt.format(tty.buffered_writer.writer(), ctlseqs.cup, .{ row + 1, col + 1 }); 337 + } 338 + 339 + if (cell.image) |img| { 340 + try std.fmt.format(tty.buffered_writer.writer(), ctlseqs.kitty_graphics_place, .{ img.img_id, img.z_index }); 349 341 } 350 342 351 343 // something is different, so let's loop throuugh everything and ··· 580 572 _ = try tty.write(ctlseqs.mouse_reset); 581 573 try tty.flush(); 582 574 } 575 + } 576 + 577 + pub fn loadImage( 578 + self: *Self, 579 + alloc: std.mem.Allocator, 580 + src: Image.Source, 581 + ) !Image { 582 + var tty = self.tty orelse return error.NoTTY; 583 + defer self.next_img_id += 1; 584 + 585 + const writer = tty.buffered_writer.writer(); 586 + 587 + var img = switch (src) { 588 + .path => |path| try zigimg.Image.fromFilePath(alloc, path), 589 + .mem => |bytes| try zigimg.Image.fromMemory(alloc, bytes), 590 + }; 591 + defer img.deinit(); 592 + const png_buf = try alloc.alloc(u8, img.imageByteSize()); 593 + defer alloc.free(png_buf); 594 + const png = try img.writeToMemory(png_buf, .{ .png = .{} }); 595 + const b64_buf = try alloc.alloc(u8, base64.calcSize(png.len)); 596 + const encoded = base64.encode(b64_buf, png); 597 + defer alloc.free(b64_buf); 598 + 599 + const id = self.next_img_id; 600 + 601 + log.debug("transmitting kitty image: id={d}, len={d}", .{ id, encoded.len }); 602 + 603 + if (encoded.len < 4096) { 604 + try std.fmt.format( 605 + writer, 606 + "\x1b_Gf=100,i={d};{s}\x1b\\", 607 + .{ 608 + id, 609 + encoded, 610 + }, 611 + ); 612 + } else { 613 + var n: usize = 4096; 614 + 615 + try std.fmt.format( 616 + writer, 617 + "\x1b_Gf=100,i={d},m=1;{s}\x1b\\", 618 + .{ id, encoded[0..n] }, 619 + ); 620 + while (n < encoded.len) : (n += 4096) { 621 + const end: usize = @min(n + 4096, encoded.len); 622 + const m: u2 = if (end == encoded.len) 0 else 1; 623 + try std.fmt.format( 624 + writer, 625 + "\x1b_Gm={d};{s}\x1b\\", 626 + .{ 627 + m, 628 + encoded[n..end], 629 + }, 630 + ); 631 + } 632 + } 633 + try tty.buffered_writer.flush(); 634 + return Image{ 635 + .id = id, 636 + .width = img.width, 637 + .height = img.height, 638 + }; 583 639 } 584 640 }; 585 641 }