a modern tui library written in zig

images: kitty support works well

We still need to handle querying for support.

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

+56 -53
+2 -2
README.md
··· 33 33 | Images (half block) | ✅ | planned | ✅ | 34 34 | Images (quadrant) | ✅ | planned | ✅ | 35 35 | Images (sextant) | ❌ | ❌ | ✅ | 36 - | Images (sixel) | ✅ | planned | ✅ | 37 - | Images (kitty) | ✅ | planned | ✅ | 36 + | Images (sixel) | ✅ | debating | ✅ | 37 + | Images (kitty) | ✅ | ✅ | ✅ | 38 38 | Images (iterm2) | ❌ | ❌ | ✅ | 39 39 | Video | ❌ | ❌ | ✅ | 40 40 | Dank | 🆗 | 🆗 | ✅ |
+14 -41
examples/image.zig
··· 3 3 4 4 const log = std.log.scoped(.main); 5 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 6 const Event = union(enum) { 10 7 key_press: vaxis.Key, 11 8 winsize: vaxis.Winsize, 12 - focus_in, 13 - focus_out, 14 - foo: u8, 15 9 }; 16 10 17 11 pub fn main() !void { 18 12 var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 19 13 defer { 20 14 const deinit_status = gpa.deinit(); 21 - //fail test; can't try in defer as defer is executed after we return 22 15 if (deinit_status == .leak) { 23 16 log.err("memory leak", .{}); 24 17 } 25 18 } 26 19 const alloc = gpa.allocator(); 27 20 28 - // Initialize Vaxis with our event type 29 21 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 22 defer vx.deinit(alloc); 33 23 34 - // Start the read loop. This puts the terminal in raw mode and begins 35 - // reading user input 36 24 try vx.startReadThread(); 37 25 defer vx.stopReadThread(); 38 26 39 - // Optionally enter the alternate screen 40 27 try vx.enterAltScreen(); 41 28 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 29 try vx.queryTerminal(); 45 30 46 - const img = try vx.loadImage(alloc, .{ .path = "vaxis.png" }); 31 + const imgs = [_]vaxis.Image{ 32 + try vx.loadImage(alloc, .{ .path = "examples/zig.png" }), 33 + try vx.loadImage(alloc, .{ .path = "examples/vaxis.png" }), 34 + }; 47 35 48 36 var n: usize = 0; 49 37 50 - // The main event loop. Vaxis provides a thread safe, blocking, buffered 51 - // queue which can serve as the primary event queue for an application 52 - outer: while (true) { 53 - // nextEvent blocks until an event is in the queue 38 + while (true) { 54 39 const event = vx.nextEvent(); 55 - log.debug("event: {}\r\n", .{event}); 56 - // exhaustive switching ftw. Vaxis will send events if your EventType 57 - // enum has the fields for those events (ie "key_press", "winsize") 58 40 switch (event) { 59 41 .key_press => |key| { 60 - n += 1; 61 42 if (key.matches('c', .{ .ctrl = true })) { 62 - break :outer; 43 + return; 63 44 } else if (key.matches('l', .{ .ctrl = true })) { 64 45 vx.queueRefresh(); 65 - } else if (key.matches('n', .{ .ctrl = true })) { 66 - try vx.notify("vaxis", "hello from vaxis"); 67 - } else {} 46 + } 68 47 }, 69 - 70 48 .winsize => |ws| try vx.resize(alloc, ws), 71 - else => {}, 72 49 } 73 50 74 - // vx.window() returns the root window. This window is the size of the 75 - // terminal and can spawn child windows as logical areas. Child windows 76 - // cannot draw outside of their bounds 51 + n = (n + 1) % imgs.len; 77 52 const win = vx.window(); 78 - 79 - // Clear the entire space because we are drawing in immediate mode. 80 - // vaxis double buffers the screen. This new frame will be compared to 81 - // the old and only updated cells will be drawn 82 53 win.clear(); 83 54 84 - const child = win.initChild(n, n, .expand, .expand); 85 - 86 - img.draw(child, false, 0); 55 + const img = imgs[n]; 56 + const dims = try img.cellSize(win); 57 + const center = vaxis.alignment.center(win, dims.cols, dims.rows); 58 + const scale = false; 59 + const z_index = 0; 60 + img.draw(center, scale, z_index); 87 61 88 - // Render the screen 89 62 try vx.render(); 90 63 } 91 64 }
examples/zig.png

This is a binary file and will not be displayed.

+16 -6
src/Image.zig
··· 6 6 const zigimg = @import("zigimg"); 7 7 8 8 const Window = @import("Window.zig"); 9 - const Winsize = @import("Tty.zig").Winsize; 10 9 11 10 const log = std.log.scoped(.image); 12 11 ··· 22 21 pub const Placement = struct { 23 22 img_id: u32, 24 23 z_index: i32, 25 - scale: bool, 24 + size: ?CellSize = null, 26 25 }; 27 26 28 27 pub const CellSize = struct { ··· 42 41 const p = Placement{ 43 42 .img_id = self.id, 44 43 .z_index = z_index, 45 - .scale = scale, 44 + .size = sz: { 45 + if (!scale) break :sz null; 46 + break :sz CellSize{ 47 + .rows = win.height, 48 + .cols = win.width, 49 + }; 50 + }, 46 51 }; 47 52 win.writeCell(0, 0, .{ .image = p }); 48 53 } 49 54 50 - pub fn cellSize(self: Image, winsize: Winsize) !CellSize { 55 + pub fn cellSize(self: Image, win: Window) !CellSize { 51 56 // 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); 57 + const x_pix = win.screen.width_pix; 58 + const y_pix = win.screen.height_pix; 59 + const w = win.screen.width; 60 + const h = win.screen.height; 61 + 62 + const pix_per_col = try std.math.divCeil(usize, x_pix, w); 63 + const pix_per_row = try std.math.divCeil(usize, y_pix, h); 54 64 55 65 const cell_width = std.math.divCeil(usize, self.width, pix_per_col) catch 0; 56 66 const cell_height = std.math.divCeil(usize, self.height, pix_per_row) catch 0;
+2 -1
src/main.zig
··· 9 9 pub const Winsize = @import("Tty.zig").Winsize; 10 10 11 11 pub const widgets = @import("widgets/main.zig"); 12 + pub const alignment = widgets.alignment; 13 + pub const border = widgets.border; 12 14 13 15 pub const Image = @import("Image.zig"); 14 16 ··· 30 32 _ = @import("ctlseqs.zig"); 31 33 _ = @import("event.zig"); 32 34 _ = @import("gwidth.zig"); 33 - _ = @import("image/image.zig"); 34 35 _ = @import("queue.zig"); 35 36 _ = @import("vaxis.zig"); 36 37 }
+14 -3
src/vaxis.zig
··· 38 38 39 39 pub const Capabilities = struct { 40 40 kitty_keyboard: bool = false, 41 + kitty_graphics: bool = false, 41 42 rgb: bool = false, 42 43 unicode: bool = false, 43 44 }; ··· 60 61 alt_screen: bool = false, 61 62 /// if we have entered kitty keyboard 62 63 kitty_keyboard: bool = false, 63 - // TODO: should be false but we aren't querying yet 64 - kitty_graphics: bool = true, 65 64 bracketed_paste: bool = false, 66 65 mouse: bool = false, 67 66 } = .{}, ··· 337 336 } 338 337 339 338 if (cell.image) |img| { 340 - try std.fmt.format(tty.buffered_writer.writer(), ctlseqs.kitty_graphics_place, .{ img.img_id, img.z_index }); 339 + if (img.size) |size| { 340 + try std.fmt.format( 341 + tty.buffered_writer.writer(), 342 + ctlseqs.kitty_graphics_scale, 343 + .{ img.img_id, img.z_index, size.cols, size.rows }, 344 + ); 345 + } else { 346 + try std.fmt.format( 347 + tty.buffered_writer.writer(), 348 + ctlseqs.kitty_graphics_place, 349 + .{ img.img_id, img.z_index }, 350 + ); 351 + } 341 352 } 342 353 343 354 // something is different, so let's loop throuugh everything and
+7
src/widgets/align.zig
··· 1 + const Window = @import("../Window.zig"); 2 + 3 + pub fn center(parent: Window, cols: usize, rows: usize) Window { 4 + const y_off = (parent.height / 2) - (rows / 2); 5 + const x_off = (parent.width / 2) - (cols / 2); 6 + return parent.initChild(x_off, y_off, .{ .limit = cols }, .{ .limit = rows }); 7 + }
+1
src/widgets/main.zig
··· 1 1 pub const TextInput = @import("TextInput.zig"); 2 2 pub const border = @import("border.zig"); 3 + pub const alignment = @import("align.zig");
vaxis.png examples/vaxis.png