summaryrefslogtreecommitdiff
path: root/src/tty
diff options
context:
space:
mode:
Diffstat (limited to 'src/tty')
-rw-r--r--src/tty/tty.zig135
1 files changed, 127 insertions, 8 deletions
diff --git a/src/tty/tty.zig b/src/tty/tty.zig
index f0013020..60a2d958 100644
--- a/src/tty/tty.zig
+++ b/src/tty/tty.zig
@@ -91,6 +91,90 @@ const Pty = struct {
reader: std.Io.Future(anyerror!void),
};
+const KittyPlacement = struct {
+ cell_x: u16,
+ cell_y: u16,
+ options: vaxis.Image.DrawOptions,
+};
+
+fn terminalCellPixels(total_pixels: u16, cells: u16, fallback: u32) u32 {
+ if (total_pixels == 0 or cells == 0) return fallback;
+ return @max(1, @as(u32, total_pixels) / cells);
+}
+
+fn updateCoreTerminalSize(core: *pardes.Pardes, cols: u16, rows: u16, pixel_w: u16, pixel_h: u16) void {
+ core.update(.{ .resize = .{
+ .cols = cols,
+ .rows = rows,
+ .cell_pixels = .{
+ .w = @intCast(terminalCellPixels(pixel_w, cols, 8)),
+ .h = @intCast(terminalCellPixels(pixel_h, rows, 16)),
+ },
+ } });
+}
+
+/// Translate backend-neutral source/destination pixels into Kitty's source
+/// crop plus cell-sized placement. Kitty can specify only one scaled axis
+/// without distorting the image; the terminal derives the other axis.
+fn kittyPlacement(
+ place: pardes.ImagePlace,
+ screen_cols: u16,
+ screen_rows: u16,
+ screen_pixel_w: u16,
+ screen_pixel_h: u16,
+) ?KittyPlacement {
+ if (comptime pardes.pdf_enabled) {
+ if (place.native.fit == .contain) return null;
+ const cell_w = terminalCellPixels(screen_pixel_w, screen_cols, 8);
+ const cell_h = terminalCellPixels(screen_pixel_h, screen_rows, 16);
+ const body_w = std.math.mul(u32, place.w, cell_w) catch return null;
+ const body_h = std.math.mul(u32, place.h, cell_h) catch return null;
+ const geometry = pardes.image.nativeGeometry(
+ place.iw,
+ place.ih,
+ body_w,
+ body_h,
+ place.native.fit,
+ place.native.pan_x,
+ place.native.pan_y,
+ ) orelse return null;
+
+ // Every Kitty protocol crop field is u16. Reject an attachment which
+ // the wire format cannot represent instead of truncating it into
+ // another part of the page.
+ const src_x = std.math.cast(u16, geometry.src.x) orelse return null;
+ const src_y = std.math.cast(u16, geometry.src.y) orelse return null;
+ const src_w = std.math.cast(u16, geometry.src.w) orelse return null;
+ const src_h = std.math.cast(u16, geometry.src.h) orelse return null;
+ const cell_x = std.math.cast(u16, geometry.dst.x / cell_w) orelse return null;
+ const cell_y = std.math.cast(u16, geometry.dst.y / cell_h) orelse return null;
+ if (cell_x >= place.w or cell_y >= place.h) return null;
+ const pixel_x = std.math.cast(u16, geometry.dst.x % cell_w) orelse return null;
+ const pixel_y = std.math.cast(u16, geometry.dst.y % cell_h) orelse return null;
+
+ return .{
+ .cell_x = cell_x,
+ .cell_y = cell_y,
+ .options = .{
+ .clip_region = .{ .x = src_x, .y = src_y, .width = src_w, .height = src_h },
+ .pixel_offset = if (pixel_x != 0 or pixel_y != 0) .{ .x = pixel_x, .y = pixel_y } else null,
+ .size = switch (place.native.fit) {
+ .width => .{ .cols = place.w },
+ .height => .{ .rows = place.h },
+ .contain => unreachable,
+ },
+ },
+ };
+ } else {
+ return null;
+ }
+}
+
+fn kittyImageRepresentable(place: pardes.ImagePlace) bool {
+ return place.iw > 0 and place.ih > 0 and
+ place.iw <= std.math.maxInt(u16) and place.ih <= std.math.maxInt(u16);
+}
+
pub fn run(init: std.process.Init, opts: pardes.Options) !void {
const io = init.io;
const gpa = init.gpa;
@@ -143,7 +227,7 @@ pub fn run(init: std.process.Init, opts: pardes.Options) !void {
// pane unless this is in the env BEFORE bash starts (the rc is too late)
if (comptime builtin.os.tag.isDarwin()) _ = setenv("BASH_SILENCE_DEPRECATION_WARNING", "1", 1);
- const KittyCached = struct { serial: u32, image: vaxis.Image };
+ const KittyCached = struct { key: pardes.ImageCacheKey, image: vaxis.Image };
var kitty_handles: [pardes.MAX_PANES]?KittyCached = @splat(null);
defer for (&kitty_handles) |*slot| if (slot.*) |cached| {
vx.freeImage(tty.writer(), cached.image.id);
@@ -279,7 +363,10 @@ pub fn run(init: std.process.Init, opts: pardes.Options) !void {
.quit => stop = true,
.winsize => |ws| {
try vx.resize(gpa, tty.writer(), ws);
- core.update(.{ .resize = .{ .cols = @intCast(ws.cols), .rows = @intCast(ws.rows) } });
+ if (comptime pardes.pdf_enabled)
+ updateCoreTerminalSize(core, ws.cols, ws.rows, ws.x_pixel, ws.y_pixel)
+ else
+ core.update(.{ .resize = .{ .cols = ws.cols, .rows = ws.rows } });
},
.pty_read => |pr| {
core.update(.{ .output = .{ .pane = @intCast(pr.id), .bytes = pr.bytes } });
@@ -404,6 +491,13 @@ pub fn run(init: std.process.Init, opts: pardes.Options) !void {
nc.native_images = vx.caps.kitty_graphics;
core.deinit();
core = nc;
+ if (comptime pardes.pdf_enabled) {
+ // A restored core did not receive the terminal's earlier
+ // winsize event. Reapply both the grid and physical cells
+ // before its first PDF frame so pointer/pan geometry stays
+ // identical to placement.
+ updateCoreTerminalSize(core, vx.screen.width, vx.screen.height, vx.screen.width_pix, vx.screen.height_pix);
+ }
}
// live cwd for tags/look: cheap per-pane lookup, per frame
@@ -451,15 +545,19 @@ pub fn run(init: std.process.Init, opts: pardes.Options) !void {
}
}
tz_cells.end();
- // Pixel attachments (kitty graphics): transmit once per pane
+ // Pixel attachments (kitty graphics): transmit once per pixel
// generation, then re-place every frame (placements aren't
// persistent). A slot can be closed and reused for an unrelated pane,
- // so the pane number by itself is not a cache key.
+ // and a live document can render new pixels, so neither the pane
+ // number nor its lifetime serial is a complete cache key by itself.
var kitty_seen: [pardes.MAX_PANES]bool = @splat(false);
for (surface.images[0..surface.nimages]) |maybe| {
const place = maybe orelse continue;
+ if (comptime pardes.pdf_enabled) {
+ if (!kittyImageRepresentable(place)) continue;
+ }
kitty_seen[place.pane] = true;
- if (kitty_handles[place.pane]) |cached| if (cached.serial != place.serial) {
+ if (kitty_handles[place.pane]) |cached| if (!cached.key.eql(place.cacheKey())) {
vx.freeImage(tty.writer(), cached.image.id);
kitty_handles[place.pane] = null;
};
@@ -469,12 +567,33 @@ pub fn run(init: std.process.Init, opts: pardes.Options) !void {
defer gpa.free(b64);
_ = enc.encode(b64, place.rgba);
if (vx.transmitPreEncodedImage(tty.writer(), b64, @intCast(place.iw), @intCast(place.ih), .rgba) catch null) |handle|
- kitty_handles[place.pane] = .{ .serial = place.serial, .image = handle };
+ kitty_handles[place.pane] = .{ .key = place.cacheKey(), .image = handle };
} else |_| {}
}
if (kitty_handles[place.pane]) |cached| {
- const child = win.child(.{ .x_off = place.x, .y_off = place.y, .width = place.w, .height = place.h });
- cached.image.draw(child, .{ .scale = .contain }) catch {};
+ if (comptime !pardes.pdf_enabled) {
+ const child = win.child(.{ .x_off = place.x, .y_off = place.y, .width = place.w, .height = place.h });
+ cached.image.draw(child, .{ .scale = .contain }) catch {};
+ } else {
+ if (place.native.fit == .contain) {
+ const child = win.child(.{ .x_off = place.x, .y_off = place.y, .width = place.w, .height = place.h });
+ cached.image.draw(child, .{ .scale = .contain }) catch {};
+ } else if (kittyPlacement(
+ place,
+ vx.screen.width,
+ vx.screen.height,
+ vx.screen.width_pix,
+ vx.screen.height_pix,
+ )) |placement| {
+ const child = win.child(.{
+ .x_off = @as(i17, place.x) + placement.cell_x,
+ .y_off = @as(i17, place.y) + placement.cell_y,
+ .width = place.w - placement.cell_x,
+ .height = place.h - placement.cell_y,
+ });
+ cached.image.draw(child, placement.options) catch {};
+ }
+ }
}
}
// Toggling PETSCII or closing a pane removes its attachment from the