image: implement transmitting images
Signed-off-by: Tim Culverhouse <tim@timculverhouse.com>
This commit is contained in:
parent
626a9101bd
commit
cc75fe6272
8 changed files with 201 additions and 38 deletions
|
@ -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
87
examples/image.zig
Normal 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();
|
||||
}
|
||||
}
|
|
@ -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{
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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],
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,39 +8,45 @@ const Window = @import("../Window.zig");
|
|||
|
||||
const Kitty = @import("Kitty.zig");
|
||||
|
||||
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 Image = union(enum) {
|
||||
kitty: Kitty,
|
||||
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,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue