image: implement transmitting images

Signed-off-by: Tim Culverhouse <tim@timculverhouse.com>
This commit is contained in:
Tim Culverhouse 2024-01-30 13:30:36 -06:00
parent 626a9101bd
commit cc75fe6272
8 changed files with 201 additions and 38 deletions

View file

@ -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,
});

87
examples/image.zig Normal file
View file

@ -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();
}
}

View file

@ -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{

View file

@ -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

View file

@ -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],
},
);
}
}
}

View file

@ -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,
};
}
};

View file

@ -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);

View file

@ -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;