summaryrefslogtreecommitdiff
path: root/src/TerminalImages.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-10-01 00:45:14 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:53:56 -0300
commit0bb65902ebec104dac954b59f28500812419e1af (patch)
tree1c2d756ce89fcf6059bacf62928804da5585d36b /src/TerminalImages.zig
parent28c74918cde4092d964daa847c026ed3de282683 (diff)
downloadpardes-0bb65902ebec104dac954b59f28500812419e1af.tar.gz
pardes-0bb65902ebec104dac954b59f28500812419e1af.zip
A terminal's kitty graphics draw as glyph art where pixels cannot, and by choice: Petscii on the pane, TermImages for new ones
Under an outer terminal without kitty graphics a tty pardes cannot pass a terminal pane's images on, so it draws them itself as an image pane's Petscii does: each row of an image is matched to C64-palette glyph art over the cells it covers (GlyphArt.renderRect, the exact-grid half of GlyphArt.render), cached while it is on screen. yazi is still told the kitty protocol works, so it keeps sending its previews. The same art is a choice anywhere: the Petscii builtin now takes a terminal pane too (PaneKind.picture), flipping its images between pixels and glyphs, and a terminal holding images, or drawing them as glyphs by choice, says petscii:on|off in its tag as an image pane does. TermImages real|petscii (default real) is what a new terminal starts with; it is in DumpConfig, /ctl and the reference. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src/TerminalImages.zig')
-rw-r--r--src/TerminalImages.zig100
1 files changed, 76 insertions, 24 deletions
diff --git a/src/TerminalImages.zig b/src/TerminalImages.zig
index 773297b2..bc1b2ea8 100644
--- a/src/TerminalImages.zig
+++ b/src/TerminalImages.zig
@@ -2,8 +2,8 @@
//! previews, `kitten icat`) drawn where it placed them. ghostty-vt keeps
//! the images and placements; this reads them each frame, cuts each one
//! into the cell rows it covers, clipped to the rows the pane shows, and
-//! hands them on as pixels (`ImagePlace`, exact geometry) to a shell that
-//! draws them.
+//! hands them on: as pixels (`ImagePlace`, exact geometry) to a shell that
+//! draws them, else as glyph art in the cells, as an image pane's Petscii.
//! Both kinds of placement are drawn: one on a cell (`a=T`/`a=p`), which
//! moves with the text it was put on, and a virtual one (`U=1`) shown
//! through the U+10EEEE placeholder cells the program prints.
@@ -14,17 +14,25 @@ const image = @import("image.zig");
const TerminalImages = @This();
const gfx = ghostty_vt.kitty.graphics;
+const GlyphArt = image.GlyphArt;
const PixelRect = image.PixelRect;
/// The bytes ghostty-vt may hold for one screen's images (it evicts the
/// oldest past this): a 4K picture in RGBA, with room for a second.
pub const storage_limit = 64 * 1024 * 1024;
+/// This pane draws its images as glyph art even where pixels could be
+/// drawn (the Petscii builtin; TermImages sets it for a new pane).
+petscii: bool = false,
/// Pixels in RGBA for images ghostty-vt holds in another format (RGB,
/// grey): only for images on screen, dropped the frame they are not.
converted: std.AutoHashMapUnmanaged(u32, Converted) = .empty,
+/// Glyph art for the fragments drawn as cells, kept while on screen.
+glyphs: std.AutoHashMapUnmanaged(GlyphKey, Glyphs) = .empty,
const Converted = struct { generation: u64, rgba: []u8, used: bool = true };
+const Glyphs = struct { cells: []GlyphArt.Cell, used: bool = true };
+const GlyphKey = struct { id: u32, generation: u64, src: PixelRect, cols: u16 };
/// One image's rectangle in the pane, in pixels from the viewport's top
/// left corner (above or left of it is negative).
@@ -34,6 +42,9 @@ pub fn deinit(ti: *TerminalImages, gpa: std.mem.Allocator) void {
var conv = ti.converted.valueIterator();
while (conv.next()) |c| gpa.free(c.rgba);
ti.converted.deinit(gpa);
+ var gl = ti.glyphs.valueIterator();
+ while (gl.next()) |g| gpa.free(g.cells);
+ ti.glyphs.deinit(gpa);
}
/// Accept PNG (`f=100`) as well as raw pixels: ghostty-vt built as a
@@ -64,6 +75,8 @@ pub fn draw(
) void {
var conv = ti.converted.valueIterator();
while (conv.next()) |c| c.used = false;
+ var gl = ti.glyphs.valueIterator();
+ while (gl.next()) |g| g.used = false;
defer ti.prune(p.gpa);
const screen = vt.screens.active;
@@ -73,7 +86,8 @@ pub fn draw(
const bottom = pages.getBottomRight(.viewport) orelse return;
const cw: u32 = @max(1, p.cell_pixels.w);
const ch: u32 = @max(1, p.cell_pixels.h);
- const ctx: Ctx = .{ .ti = ti, .p = p, .pane = pane_id, .serial = serial, .tx = tx, .body_y = body_y, .cols = cols, .rows_of = rows_of, .cw = cw, .ch = ch };
+ const pixels = p.native_images and !ti.petscii;
+ const ctx: Ctx = .{ .ti = ti, .p = p, .s = s, .pane = pane_id, .serial = serial, .tx = tx, .body_y = body_y, .cols = cols, .rows_of = rows_of, .cw = cw, .ch = ch, .pixels = pixels };
// Virtual placements: shown through the placeholder cells, one run of
// them on a row at a time. The placeholders themselves are not text.
@@ -130,6 +144,7 @@ pub fn draw(
const Ctx = struct {
ti: *TerminalImages,
p: *pardes.Pardes,
+ s: *pardes.Surface,
pane: u8,
serial: u32,
tx: u16,
@@ -138,6 +153,7 @@ const Ctx = struct {
rows_of: []const ?u16,
cw: u32,
ch: u32,
+ pixels: bool,
/// Draw `src` of `img` over `dst`, one cell row of it at a time: a row
/// is shown, or not, or shown elsewhere, as the body lays it out.
@@ -165,27 +181,37 @@ const Ctx = struct {
};
const c0: u16 = @intCast(@divFloor(x0, cw));
const c1: u16 = @intCast(@divFloor(x1 + cw - 1, cw));
- _ = ctx.p.appendImagePlace(.{
- .pane = ctx.pane,
- .serial = ctx.serial,
- .native = .{
- .page = img.id,
- .revision = @truncate(img.generation),
- .exact = true,
- .geometry = .{ .src = piece, .dst = .{
- .x = @intCast(x0 - @as(i64, c0) * cw),
- .y = @intCast(y0 - v * ch),
- .w = @intCast(x1 - x0),
- .h = @intCast(y1 - y0),
- } },
- },
- .x = ctx.tx + c0,
- .y = ctx.body_y + row,
- .w = c1 - c0,
- .h = 1,
- .rgba = rgba,
- .iw = img.width,
- .ih = img.height,
+ if (ctx.pixels) {
+ _ = ctx.p.appendImagePlace(.{
+ .pane = ctx.pane,
+ .serial = ctx.serial,
+ .native = .{
+ .page = img.id,
+ .revision = @truncate(img.generation),
+ .exact = true,
+ .geometry = .{ .src = piece, .dst = .{
+ .x = @intCast(x0 - @as(i64, c0) * cw),
+ .y = @intCast(y0 - v * ch),
+ .w = @intCast(x1 - x0),
+ .h = @intCast(y1 - y0),
+ } },
+ },
+ .x = ctx.tx + c0,
+ .y = ctx.body_y + row,
+ .w = c1 - c0,
+ .h = 1,
+ .rgba = rgba,
+ .iw = img.width,
+ .ih = img.height,
+ });
+ continue;
+ }
+ // Glyph art fills whole cells: a sliver of a row is left out.
+ if ((y1 - y0) * 2 < ch) continue;
+ const cells = ctx.ti.glyphsOf(ctx.p.gpa, img, rgba, piece, c1 - c0) orelse continue;
+ for (cells, c0..) |*cell, x| ctx.s.set(ctx.tx + @as(u16, @intCast(x)), ctx.body_y + row, cell.glyph[0..cell.glyph_len], .{
+ .fg = .{ .rgb = GlyphArt.commodore[cell.fg] },
+ .bg = .{ .rgb = GlyphArt.commodore[cell.bg] },
});
}
}
@@ -234,6 +260,24 @@ fn convert(gpa: std.mem.Allocator, img: gfx.Image, n: usize) ?[]u8 {
return out;
}
+fn glyphsOf(ti: *TerminalImages, gpa: std.mem.Allocator, img: gfx.Image, rgba: []const u8, piece: PixelRect, cols: u16) ?[]const GlyphArt.Cell {
+ const key: GlyphKey = .{ .id = img.id, .generation = img.generation, .src = piece, .cols = cols };
+ if (ti.glyphs.getPtr(key)) |hit| {
+ hit.used = true;
+ return hit.cells;
+ }
+ const grid = GlyphArt.renderRect(gpa, rgba, img.width, img.height, piece, cols, 1, GlyphArt.commodore, true) catch return null;
+ if (grid.cells.len != cols) {
+ gpa.free(grid.cells);
+ return null;
+ }
+ ti.glyphs.put(gpa, key, .{ .cells = grid.cells }) catch {
+ gpa.free(grid.cells);
+ return null;
+ };
+ return grid.cells;
+}
+
/// Drop what this frame did not draw: the caches hold only what is shown.
fn prune(ti: *TerminalImages, gpa: std.mem.Allocator) void {
while (true) {
@@ -244,4 +288,12 @@ fn prune(ti: *TerminalImages, gpa: std.mem.Allocator) void {
gpa.free(ti.converted.get(stale).?.rgba);
_ = ti.converted.remove(stale);
}
+ while (true) {
+ var gl = ti.glyphs.iterator();
+ const stale = while (gl.next()) |entry| {
+ if (!entry.value_ptr.used) break entry.key_ptr.*;
+ } else break;
+ gpa.free(ti.glyphs.get(stale).?.cells);
+ _ = ti.glyphs.remove(stale);
+ }
}