summaryrefslogtreecommitdiff
path: root/src
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
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')
-rw-r--r--src/TerminalImages.zig100
-rw-r--r--src/builtins.zig11
-rw-r--r--src/config.zig11
-rw-r--r--src/image.zig21
-rw-r--r--src/ninep/ctl.zig2
-rw-r--r--src/pardes.zig1
-rw-r--r--src/tagline.zig4
-rw-r--r--src/terminal.zig105
8 files changed, 221 insertions, 34 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);
+ }
}
diff --git a/src/builtins.zig b/src/builtins.zig
index fb6aa4cf..f51409aa 100644
--- a/src/builtins.zig
+++ b/src/builtins.zig
@@ -43,12 +43,16 @@ pub const PaneKind = enum {
terminal,
pdf,
image,
+ /// An image pane, or a terminal, whose program's kitty graphics are
+ /// pictures too.
+ picture,
pub fn has(kind: PaneKind, pane: *const panes.Pane) bool {
return switch (kind) {
.terminal => pane.isTerminal(),
.pdf => pane.hasPdf(),
.image => pane.image != null,
+ .picture => pane.image != null or pane.isTerminal(),
};
}
@@ -57,6 +61,7 @@ pub const PaneKind = enum {
.terminal => "a terminal",
.pdf => "a PDF",
.image => "an image",
+ .picture => "an image or a terminal",
};
}
};
@@ -981,11 +986,13 @@ pub const PdfSections = struct {
}
};
-/// glyph art over the host's pixels
+/// glyph art over the host's pixels: an image pane's picture, or the
+/// kitty graphics a terminal's program placed (yazi's previews)
pub const Petscii = struct {
- pub const needs: PaneKind = .image;
+ pub const needs: PaneKind = .picture;
pub fn run(c: Ctx) void {
if (c.pane.image) |*state| panes.Image.toggleGlyphArt(state);
+ panes.terminal.togglePetscii(c.pane);
}
};
diff --git a/src/config.zig b/src/config.zig
index 5a9cf070..b6eb4520 100644
--- a/src/config.zig
+++ b/src/config.zig
@@ -82,6 +82,7 @@ pub const leader_path = paths: {
.BootShell = null,
.LookWord = null,
.Pager = null,
+ .TermImages = null,
.Debug = "td",
.Msg = null,
.Colors = "tc",
@@ -759,14 +760,19 @@ pub const Runtime = struct {
/// PAGER and GIT_PAGER set to `pardes -`, a +Pager pane, where the
/// user's environment set neither; `off`, the environment as it is.
pager: Pager = .pardes,
+ /// How a new terminal pane draws the kitty graphics its program sends
+ /// (yazi's previews): `real`, as pixels where the shell can, or
+ /// `petscii`, as glyph art. The Petscii builtin flips one pane.
+ term_images: TermImages = .real,
pub const ShaderAnimation = enum { off, on, always };
pub const Placement = enum { acme, pardes };
pub const BootShell = enum { keep, replace };
pub const LookWord = enum { search, list };
pub const Pager = enum { pardes, off };
+ pub const TermImages = enum { real, petscii };
/// A setting that is one of a few words, each an enum field of Runtime.
- pub const Choice = enum { placement, boot_shell, look_word, pager };
+ pub const Choice = enum { placement, boot_shell, look_word, pager, term_images };
/// shadow: a soft drop shadow. rim: a light hairline along the top of
/// the focused pane's tag rows. auto: a shadow on a light page; on a
@@ -1032,6 +1038,7 @@ pub const Runtime = struct {
.{ .word = "BootShell", .action = .{ .choice = .boot_shell }, .doc = "A document dropped where a lone untouched shell stands: keep the shell, or replace it." },
.{ .word = "LookWord", .action = .{ .choice = .look_word }, .doc = "A plain word looked at: search selects its next place in the pane, list lists every place in a +Search." },
.{ .word = "Pager", .action = .{ .choice = .pager }, .doc = "What a terminal's commands page through: pardes, a +Pager pane (pardes -), or off, the environment's own; for terminals started after it." },
+ .{ .word = "TermImages", .action = .{ .choice = .term_images }, .doc = "How a new terminal draws its program's kitty graphics: real, as pixels where it can, or petscii, as glyph art." },
};
pub fn find(name: []const u8) ?Setting {
@@ -1707,6 +1714,7 @@ pub const Runtime = struct {
.boot_shell => "replace",
.look_word => "list",
.pager => "off",
+ .term_images => "petscii",
},
.lift => "rim",
.motion => "playful",
@@ -1777,6 +1785,7 @@ pub const Runtime = struct {
.boot_shell => "BootShell replace\n",
.look_word => "LookWord list\n",
.pager => "Pager off\n",
+ .term_images => "TermImages petscii\n",
},
.lift => "Lift rim\n",
.motion => "Motion playful\n",
diff --git a/src/image.zig b/src/image.zig
index 936fe19a..3a3f1932 100644
--- a/src/image.zig
+++ b/src/image.zig
@@ -305,7 +305,6 @@ pub const GlyphArt = struct {
pub fn render(gpa: std.mem.Allocator, rgba: []const u8, iw: usize, ih: usize, max_cols: usize, max_rows: usize, pal: Palette, ascii: bool) !Grid {
if (iw == 0 or ih == 0 or max_cols == 0 or max_rows == 0) return .{ .cells = try gpa.alloc(Cell, 0), .cols = 0, .rows = 0 };
- const gset: []const Glyph = if (ascii) &glyphs_ascii else &glyphs;
// contain-fit; cells are ~twice as tall as wide, so a row spans 2 width-units.
var cols = max_cols;
var rows = (max_cols * ih) / (2 * iw);
@@ -315,16 +314,28 @@ pub const GlyphArt = struct {
}
cols = std.math.clamp(cols, 1, max_cols);
rows = std.math.clamp(rows, 1, max_rows);
+ return renderRect(gpa, rgba, iw, ih, .{ .w = @intCast(iw), .h = @intCast(ih) }, cols, rows, pal, ascii);
+ }
+ /// The pixels of `src` (a rectangle of the `iw` x `ih` image) laid
+ /// over exactly `cols` x `rows` cells, whatever their shape: a
+ /// terminal's image fragment is already the size its cells are.
+ pub fn renderRect(gpa: std.mem.Allocator, rgba: []const u8, iw: usize, ih: usize, src: PixelRect, cols: usize, rows: usize, pal: Palette, ascii: bool) !Grid {
+ const x0: usize = @min(src.x, iw);
+ const y0: usize = @min(src.y, ih);
+ const sw: usize = @min(src.w, iw - x0);
+ const sh: usize = @min(src.h, ih - y0);
+ if (sw == 0 or sh == 0 or cols == 0 or rows == 0) return .{ .cells = try gpa.alloc(Cell, 0), .cols = 0, .rows = 0 };
+ const gset: []const Glyph = if (ascii) &glyphs_ascii else &glyphs;
const cells = try gpa.alloc(Cell, cols * rows);
var cy: usize = 0;
while (cy < rows) : (cy += 1) {
- const ry0 = cy * ih / rows;
- const ry1 = @max(ry0 + 1, (cy + 1) * ih / rows);
+ const ry0 = y0 + cy * sh / rows;
+ const ry1 = @max(ry0 + 1, y0 + (cy + 1) * sh / rows);
var cx: usize = 0;
while (cx < cols) : (cx += 1) {
- const rx0 = cx * iw / cols;
- const rx1 = @max(rx0 + 1, (cx + 1) * iw / cols);
+ const rx0 = x0 + cx * sw / cols;
+ const rx1 = @max(rx0 + 1, x0 + (cx + 1) * sw / cols);
var cell: [64][3]u8 = undefined;
var sy: usize = 0;
while (sy < 8) : (sy += 1) {
diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig
index 87339b70..cdb6092a 100644
--- a/src/ninep/ctl.zig
+++ b/src/ninep/ctl.zig
@@ -1737,7 +1737,7 @@ test "a builtin for one kind of pane is refused on another, and still acts on it
defer p.deinit();
const serial = serialOf(p);
const cases = [_]struct { []const u8, []const u8 }{
- .{ "Petscii\n", "Petscii: only an image pane takes it" },
+ .{ "Petscii\n", "Petscii: only an image or a terminal pane takes it" },
.{ "Palette\n", "Palette: only an image pane takes it" },
.{ "Ascii\n", "Ascii: only an image pane takes it" },
.{ "Filter\n", "Filter: only a terminal pane takes it" },
diff --git a/src/pardes.zig b/src/pardes.zig
index aa2c252c..84b891e5 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -4976,6 +4976,7 @@ pub const Pardes = struct {
pane.serial = p.next_serial;
p.rects[id] = .{};
p.panes[id] = pane;
+ panes.terminal.adoptSettings(p, pane);
// /log hears of it once the update that made it ends, when it has
// its file, name and place (`ctlfs.events.announce`).
pane.fs.unannounced = true;
diff --git a/src/tagline.zig b/src/tagline.zig
index 9bd49eef..032fb5a0 100644
--- a/src/tagline.zig
+++ b/src/tagline.zig
@@ -124,6 +124,10 @@ pub fn pathPrefix(arena: std.mem.Allocator, pane: *Pane) ![]const u8 {
// Unsaved text shows on the grip, as acme's modbutton does, not as a
// mark in the text: the prefix is the name alone.
if (pane.file) |f| return if (pane.prompt == .name) pane.input.own orelse f.path else f.path;
+ // A terminal showing kitty graphics says how it draws them, as an image
+ // pane does, and so does one drawing them as glyph art by choice.
+ if (panes.terminal.petscii(pane)) |on| if (on or panes.terminal.hasImages(pane))
+ return std.fmt.allocPrint(arena, "{s} petscii:{s}", .{ (try shownDir(arena, pane)), if (on) "on" else "off" });
return (try shownDir(arena, pane));
}
diff --git a/src/terminal.zig b/src/terminal.zig
index e6304c0a..0850267e 100644
--- a/src/terminal.zig
+++ b/src/terminal.zig
@@ -9,6 +9,7 @@ const Surface = pardes.Surface;
const modal = @import("modal.zig");
const tracy = @import("tracy.zig");
const dump = @import("dump.zig");
+const tagline = @import("tagline.zig");
const Key = pardes.Key;
const Pane = panes.Pane;
const EditText = panes.EditText;
@@ -2797,6 +2798,36 @@ fn syncPixels(p: *const Pardes, pane: *Pane) void {
vt.height_px = @as(u32, vt.rows) * p.cell_pixels.h;
}
+/// The Petscii builtin on a terminal: its kitty graphics as glyph art,
+/// or back to pixels where the shell draws pixels.
+pub fn togglePetscii(pane: *Pane) void {
+ if (comptime !images_enabled) return;
+ const state = pane.terminal orelse return;
+ state.images.petscii = !state.images.petscii;
+}
+
+/// Whether the pane draws its kitty graphics as glyph art by choice; null
+/// for a pane with no emulator.
+pub fn petscii(pane: *const Pane) ?bool {
+ if (comptime !images_enabled) return null;
+ const state = pane.terminal orelse return null;
+ return state.images.petscii;
+}
+
+/// A new terminal takes TermImages: pixels, or glyph art.
+pub fn adoptSettings(p: *const Pardes, pane: *Pane) void {
+ if (comptime !images_enabled) return;
+ const state = pane.terminal orelse return;
+ state.images.petscii = p.settings.term_images == .petscii;
+}
+
+/// Whether the screen showing holds any kitty graphics image.
+pub fn hasImages(pane: *const Pane) bool {
+ if (comptime !images_enabled) return false;
+ const state = pane.terminal orelse return false;
+ return state.vt.screens.active.kitty_images.images.count() > 0;
+}
+
/// Draw the pane's kitty graphics images over its body rows (after the
/// rows and their colours: placeholder cells are blanked here).
pub fn drawImages(p: *Pardes, s: *Surface, pane: *Pane, id: usize, tx: u16, tw: u16, body_y: u16, body_h: u16) void {
@@ -2828,7 +2859,7 @@ pub fn drawImages(p: *Pardes, s: *Surface, pane: *Pane, id: usize, tx: u16, tw:
}
// Nothing to draw, no placeholder to blank and nothing cached to let
// go: the common case costs the rows' flags.
- if (!any_placeholder and screen.kitty_images.placements.count() == 0 and state.images.converted.count() == 0) return;
+ if (!any_placeholder and screen.kitty_images.placements.count() == 0 and state.images.converted.count() == 0 and state.images.glyphs.count() == 0) return;
state.images.draw(p, s, &state.vt, @intCast(id), pane.serial, tx, body_y, @min(tw, pane.cols), rows_of[0..vp_rows]);
}
@@ -2982,6 +3013,78 @@ test "a virtual kitty placement shows through its placeholder cells, which draw
try std.testing.expectEqualStrings("X", s.at(tx + 4, y).grapheme());
}
+test "without pixels, or by the Petscii builtin, a terminal's image is glyph art in its cells" {
+ if (comptime !images_enabled) return error.SkipZigTest;
+ const gpa = std.testing.allocator;
+ const p = try Pardes.init(gpa, .{ .cols = 80, .rows = 24, .shells = 1 });
+ defer p.deinit();
+ p.update(.{ .resize = .{ .cols = 80, .rows = 24, .cell_pixels = .{ .w = 10, .h = 20 } } });
+ var frame = std.heap.ArenaAllocator.init(gpa);
+ defer frame.deinit();
+ const pane = p.panes[0].?;
+ pane.body.mode = .tty;
+ p.update(.{ .output = .{ .pane = 0, .bytes = kittyTransmit("c=2,r=1") } });
+ const r = p.rects[0];
+ const tx = r.x + config.GUTTER;
+ const y = p.bodyTop(pane, r);
+ const commodore = pardes.image.GlyphArt.commodore;
+ const painted = struct {
+ fn check(cell: *const pardes.Cell) bool {
+ const bg = switch (cell.style.bg) {
+ .rgb => |rgb| rgb,
+ else => return false,
+ };
+ for (commodore) |c| if (std.mem.eql(u8, &c, &bg)) return true;
+ return false;
+ }
+ }.check;
+
+ // A shell that draws no pixels (a tty under no kitty graphics): forced.
+ p.native_images = false;
+ var s = try p.render(frame.allocator());
+ try std.testing.expectEqual(@as(usize, 0), imagePlaces(s).len);
+ try std.testing.expect(painted(s.at(tx, y)) and painted(s.at(tx + 1, y)));
+ try std.testing.expect(!painted(s.at(tx + 2, y)));
+ // The tag says so, as an image pane's does.
+ try std.testing.expect(std.mem.indexOf(u8, try tagline.tagPrefix(p, pane), " petscii:off") != null);
+
+ // Pixels where they can be drawn; the builtin flips the pane to glyphs
+ // and back.
+ p.native_images = true;
+ _ = frame.reset(.retain_capacity);
+ s = try p.render(frame.allocator());
+ try std.testing.expectEqual(@as(usize, 1), imagePlaces(s).len);
+ try std.testing.expect(p.executeBuiltinLine(0, "Petscii"));
+ _ = frame.reset(.retain_capacity);
+ s = try p.render(frame.allocator());
+ try std.testing.expectEqual(@as(usize, 0), imagePlaces(s).len);
+ try std.testing.expect(painted(s.at(tx, y)));
+ try std.testing.expect(std.mem.indexOf(u8, try tagline.tagPrefix(p, pane), " petscii:on") != null);
+ try std.testing.expect(p.executeBuiltinLine(0, "Petscii"));
+ _ = frame.reset(.retain_capacity);
+ s = try p.render(frame.allocator());
+ try std.testing.expectEqual(@as(usize, 1), imagePlaces(s).len);
+}
+
+test "TermImages is real by default, and petscii makes new terminals draw glyph art" {
+ if (comptime !images_enabled) return error.SkipZigTest;
+ const gpa = std.testing.allocator;
+ const p = try Pardes.init(gpa, .{ .cols = 80, .rows = 24, .shells = 1 });
+ defer p.deinit();
+ try std.testing.expectEqual(config.Runtime.TermImages.real, p.settings.term_images);
+ try std.testing.expectEqual(false, petscii(p.panes[0].?).?);
+ // A pane without images says nothing of them.
+ try std.testing.expect(std.mem.indexOf(u8, try tagline.tagPrefix(p, p.panes[0].?), "petscii") == null);
+ try std.testing.expect(p.executeBuiltinLine(0, "TermImages petscii"));
+ try std.testing.expectEqual(config.Runtime.TermImages.petscii, p.settings.term_images);
+ // The setting is for new panes: the one there keeps its choice.
+ try std.testing.expectEqual(false, petscii(p.panes[0].?).?);
+ const id = p.freeSlot().?;
+ const fresh = try p.newShell(id, "");
+ try std.testing.expectEqual(true, petscii(fresh).?);
+ try std.testing.expect(std.mem.indexOf(u8, try tagline.tagPrefix(p, fresh), " petscii:on") != null);
+}
+
test "a program asking the cell size in pixels (CSI 16 t) is told the shell's" {
if (comptime !images_enabled) return error.SkipZigTest;
const gpa = std.testing.allocator;