summaryrefslogtreecommitdiff
path: root/src/image.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-06 18:11:36 -0300
committerGabriel Schneider <[email protected]>2026-09-07 13:59:12 -0300
commit60367d8fe23f6af98ec28e3cf6c2094dfe332df0 (patch)
tree310fc734173cf771881f4691c71909135fadde97 /src/image.zig
parentfa82cac885cb4738fe36d1e49b4749b5a3e31a4a (diff)
downloadpardes-60367d8fe23f6af98ec28e3cf6c2094dfe332df0.tar.gz
pardes-60367d8fe23f6af98ec28e3cf6c2094dfe332df0.zip
Refactor panes and filesystem; replace FUSE with 9P
Consolidate pane, layout, memory and host code. Serve 9P by default over Unix sockets, with runtime mounts and optional TCP/QUIC transports. Remove FUSE and obsolete proof-of-concept examples. Fix highlighting and terminal-history performance, expand differential and stress-test infrastructure, sort navigation results while preserving the next occurrence, add syntax-colored Braille minimaps, remove SPC-k, and document 9P interaction as a repository skill.
Diffstat (limited to 'src/image.zig')
-rw-r--r--src/image.zig454
1 files changed, 430 insertions, 24 deletions
diff --git a/src/image.zig b/src/image.zig
index 50e9649c..592230c8 100644
--- a/src/image.zig
+++ b/src/image.zig
@@ -1,7 +1,4 @@
-//! Image panes. Decoding is zstbi (C stb_image) to RGBA, downscaled; the kept
-//! pixels render as PETSCII glyph art directly into the Surface — or, when the
-//! host supports native images (and PETSCII isn't toggled), ride the Surface as
-//! a pixel attachment the shell transmits/places (TTY: Kitty; SDL: GPU texture).
+//! Image decoding, pixel placement, and glyph approximation.
const std = @import("std");
const zstbi = @import("zstbi");
/// The 16-colour ANSI table below is the only thing this file ever wanted from
@@ -10,21 +7,434 @@ const zstbi = @import("zstbi");
const terminal_panes = @import("pardes.zig").terminal_panes;
const ghostty_vt = if (terminal_panes) @import("ghostty-vt") else struct {};
const pdf_enabled = @import("pardes_config").mupdf;
-pub const petscii = @import("petscii.zig");
+// Glyph matching adapted from caioluders/petsciinator.
+pub const GlyphArt = struct {
+ pub const commodore = [16][3]u8{
+ .{ 0x00, 0x00, 0x00 }, // 0 black
+ .{ 0xff, 0xff, 0xff }, // 1 white
+ .{ 0x68, 0x37, 0x2b }, // 2 red
+ .{ 0x70, 0xa4, 0xb2 }, // 3 cyan
+ .{ 0x6f, 0x3d, 0x86 }, // 4 purple
+ .{ 0x58, 0x8d, 0x43 }, // 5 green
+ .{ 0x35, 0x28, 0x79 }, // 6 blue
+ .{ 0xb8, 0xc7, 0x6f }, // 7 yellow
+ .{ 0x6f, 0x4f, 0x25 }, // 8 orange
+ .{ 0x43, 0x39, 0x00 }, // 9 brown
+ .{ 0x9a, 0x67, 0x59 }, // 10 light red
+ .{ 0x44, 0x44, 0x44 }, // 11 dark grey
+ .{ 0x6c, 0x6c, 0x6c }, // 12 grey
+ .{ 0x9a, 0xd2, 0x84 }, // 13 light green
+ .{ 0x6c, 0x5e, 0xb5 }, // 14 light blue
+ .{ 0x95, 0x95, 0x95 }, // 15 light grey
+ };
+
+ const Palette = [16][3]u8;
+
+ pub const Cell = struct {
+ glyph: [4]u8 = .{ ' ', 0, 0, 0 },
+ glyph_len: u3 = 1,
+ fg: u4 = 1,
+ bg: u4 = 0,
+ };
+
+ pub const Grid = struct { cells: []Cell, cols: usize, rows: usize };
+
+ fn dist2(a: [3]u8, b: [3]u8) u32 {
+ const dr = @as(i32, a[0]) - b[0];
+ const dg = @as(i32, a[1]) - b[1];
+ const db = @as(i32, a[2]) - b[2];
+ return @intCast(dr * dr + dg * dg + db * db);
+ }
+
+ fn nearest(px: [3]u8, pal: Palette) u4 {
+ var best: u4 = 0;
+ var bestd: u32 = std.math.maxInt(u32);
+ for (pal, 0..) |c, i| {
+ const d = dist2(px, c);
+ if (d < bestd) {
+ bestd = d;
+ best = @intCast(i);
+ }
+ }
+ return best;
+ }
+
+ // ---- glyph set: each is a codepoint + an 8x8 ink bitmap (bit y*8+x set = fg) ----
+ const Glyph = struct { cp: u21, bits: u64 };
+
+ fn sextantCp(p: u6) u21 {
+ return switch (p) {
+ 0 => ' ',
+ 21 => 0x258C, // left half ▌
+ 42 => 0x2590, // right half ▐
+ 63 => 0x2588, // full block █
+ else => blk: {
+ var off: u21 = @as(u21, p) - 1;
+ if (p > 21) off -= 1;
+ if (p > 42) off -= 1;
+ break :blk 0x1FB00 + off; // Symbols for Legacy Computing sextants
+ },
+ };
+ }
+
+ // the 8x8 ink bitmap for a sextant pattern (2 cols x 3 rows of subcells).
+ fn sextantBits(p: u6) u64 {
+ var bits: u64 = 0;
+ var y: usize = 0;
+ while (y < 8) : (y += 1) {
+ const band = (y * 3) / 8; // 0,0,0,1,1,1,2,2
+ var x: usize = 0;
+ while (x < 8) : (x += 1) {
+ const col: usize = if (x < 4) 0 else 1;
+ const sub: u6 = @intCast(band * 2 + col);
+ if ((p >> sub) & 1 != 0) bits |= @as(u64, 1) << @intCast(y * 8 + x);
+ }
+ }
+ return bits;
+ }
+
+ // pack 8 row-bytes (bit x set, x=0 leftmost) into the 8x8 bitmap.
+ fn glyphMask(r: [8]u8) u64 {
+ var b: u64 = 0;
+ for (r, 0..) |row, y| b |= @as(u64, row) << @intCast(y * 8);
+ return b;
+ }
+
+ // the horizontal half blocks and the ten 2x2 quadrants — sextants are 2x3, so they
+ // can't express an exact 4-row half or a quarter; these fill that gap.
+ const block_glyphs = [_]Glyph{
+ .{ .cp = 0x2580, .bits = glyphMask(.{ 0xff, 0xff, 0xff, 0xff, 0, 0, 0, 0 }) }, // ▀ top half
+ .{ .cp = 0x2584, .bits = glyphMask(.{ 0, 0, 0, 0, 0xff, 0xff, 0xff, 0xff }) }, // ▄ bottom half
+ .{ .cp = 0x2598, .bits = glyphMask(.{ 0x0f, 0x0f, 0x0f, 0x0f, 0, 0, 0, 0 }) }, // ▘ TL
+ .{ .cp = 0x259d, .bits = glyphMask(.{ 0xf0, 0xf0, 0xf0, 0xf0, 0, 0, 0, 0 }) }, // ▝ TR
+ .{ .cp = 0x2596, .bits = glyphMask(.{ 0, 0, 0, 0, 0x0f, 0x0f, 0x0f, 0x0f }) }, // ▖ BL
+ .{ .cp = 0x2597, .bits = glyphMask(.{ 0, 0, 0, 0, 0xf0, 0xf0, 0xf0, 0xf0 }) }, // ▗ BR
+ .{ .cp = 0x259a, .bits = glyphMask(.{ 0x0f, 0x0f, 0x0f, 0x0f, 0xf0, 0xf0, 0xf0, 0xf0 }) }, // ▚ TL+BR
+ .{ .cp = 0x259e, .bits = glyphMask(.{ 0xf0, 0xf0, 0xf0, 0xf0, 0x0f, 0x0f, 0x0f, 0x0f }) }, // ▞ TR+BL
+ .{ .cp = 0x259b, .bits = glyphMask(.{ 0xff, 0xff, 0xff, 0xff, 0x0f, 0x0f, 0x0f, 0x0f }) }, // ▛ ¬BR
+ .{ .cp = 0x259c, .bits = glyphMask(.{ 0xff, 0xff, 0xff, 0xff, 0xf0, 0xf0, 0xf0, 0xf0 }) }, // ▜ ¬BL
+ .{ .cp = 0x2599, .bits = glyphMask(.{ 0x0f, 0x0f, 0x0f, 0x0f, 0xff, 0xff, 0xff, 0xff }) }, // ▙ ¬TR
+ .{ .cp = 0x259f, .bits = glyphMask(.{ 0xf0, 0xf0, 0xf0, 0xf0, 0xff, 0xff, 0xff, 0xff }) }, // ▟ ¬TL
+ };
+
+ // line/diagonal glyphs add the characteristic PETSCII "drawn" look on edges.
+ const line_glyphs = [_]Glyph{
+ .{ .cp = 0x2500, .bits = glyphMask(.{ 0, 0, 0, 0xff, 0xff, 0, 0, 0 }) }, // ─
+ .{ .cp = 0x2502, .bits = glyphMask(.{ 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18 }) }, // │
+ .{ .cp = 0x253c, .bits = glyphMask(.{ 0x18, 0x18, 0x18, 0xff, 0xff, 0x18, 0x18, 0x18 }) }, // ┼
+ .{ .cp = 0x2572, .bits = glyphMask(.{ 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80 }) }, // ╲
+ .{ .cp = 0x2571, .bits = glyphMask(.{ 0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01 }) }, // ╱
+ .{ .cp = 0x2573, .bits = glyphMask(.{ 0x81, 0x42, 0x24, 0x18, 0x18, 0x24, 0x42, 0x81 }) }, // ╳
+ };
-/// the terminal's own 16 ANSI colors — what the `terminal` palette mode
-/// scores against; cells then paint as indexed colors so the real terminal
-/// resolves the RGB
-///
-/// These are GHOSTTY's sixteen, transcribed, because on a platform that has an
-/// emulator the scoring used to read them straight out of it and a build
-/// without one has to score against the identical table or render different
-/// glyph art for the same photo. Note they are the base16 Tomorrow Night set,
-/// NOT the xterm defaults: zig-pkg/ghostty-1.3.2-dev-5UdBCzeJ.../src/terminal/
-/// color.zig:389-405 (`Name.default`), which color.zig:8-15 copies into the
-/// first sixteen entries of `color.default`. The comptime block below makes
-/// the emulator prove that, so the transcription cannot rot in silence.
-const ansi_default = [16][3]u8{
+ const ascii_glyphs = [_]Glyph{
+ .{ .cp = 0x21, .bits = 0x00180018183c3c18 }, // !
+ .{ .cp = 0x22, .bits = 0x0000000000246666 }, // "
+ .{ .cp = 0x23, .bits = 0x0036367f367f3636 }, // #
+ .{ .cp = 0x24, .bits = 0x00183e603c067c18 }, // $
+ .{ .cp = 0x25, .bits = 0x0063660c18336300 }, // %
+ .{ .cp = 0x26, .bits = 0x006e333b6e1c361c }, // &
+ .{ .cp = 0x27, .bits = 0x00000000000c1818 }, // '
+ .{ .cp = 0x28, .bits = 0x0030180c0c0c1830 }, // (
+ .{ .cp = 0x29, .bits = 0x000c18303030180c }, // )
+ .{ .cp = 0x2a, .bits = 0x0000663cff3c6600 }, // *
+ .{ .cp = 0x2b, .bits = 0x000018187e181800 }, // +
+ .{ .cp = 0x2c, .bits = 0x0c18180000000000 }, // ,
+ .{ .cp = 0x2d, .bits = 0x000000007e000000 }, // -
+ .{ .cp = 0x2e, .bits = 0x0018180000000000 }, // .
+ .{ .cp = 0x2f, .bits = 0x000103060c183060 }, // /
+ .{ .cp = 0x30, .bits = 0x001c36636b63361c }, // 0
+ .{ .cp = 0x31, .bits = 0x007e181818181c18 }, // 1
+ .{ .cp = 0x32, .bits = 0x007f660c3860633e }, // 2
+ .{ .cp = 0x33, .bits = 0x003e63603c60633e }, // 3
+ .{ .cp = 0x34, .bits = 0x0078307f33363c38 }, // 4
+ .{ .cp = 0x35, .bits = 0x003e63603f03037f }, // 5
+ .{ .cp = 0x36, .bits = 0x003e63633f03061c }, // 6
+ .{ .cp = 0x37, .bits = 0x000c0c0c1830637f }, // 7
+ .{ .cp = 0x38, .bits = 0x003e63633e63633e }, // 8
+ .{ .cp = 0x39, .bits = 0x001e30607e63633e }, // 9
+ .{ .cp = 0x3a, .bits = 0x0018180000181800 }, // :
+ .{ .cp = 0x3b, .bits = 0x0c18180000181800 }, // ;
+ .{ .cp = 0x3c, .bits = 0x006030180c183060 }, // <
+ .{ .cp = 0x3d, .bits = 0x00007e00007e0000 }, // =
+ .{ .cp = 0x3e, .bits = 0x00060c1830180c06 }, // >
+ .{ .cp = 0x3f, .bits = 0x001800181830633e }, // ?
+ .{ .cp = 0x40, .bits = 0x001e037b7b7b633e }, // @
+ .{ .cp = 0x41, .bits = 0x006363637f63361c }, // A
+ .{ .cp = 0x42, .bits = 0x003f66663e66663f }, // B
+ .{ .cp = 0x43, .bits = 0x003c66030303663c }, // C
+ .{ .cp = 0x44, .bits = 0x001f36666666361f }, // D
+ .{ .cp = 0x45, .bits = 0x007f46161e16467f }, // E
+ .{ .cp = 0x46, .bits = 0x000f06161e16467f }, // F
+ .{ .cp = 0x47, .bits = 0x005c66730303663c }, // G
+ .{ .cp = 0x48, .bits = 0x006363637f636363 }, // H
+ .{ .cp = 0x49, .bits = 0x003c18181818183c }, // I
+ .{ .cp = 0x4a, .bits = 0x001e333330303078 }, // J
+ .{ .cp = 0x4b, .bits = 0x006766361e366667 }, // K
+ .{ .cp = 0x4c, .bits = 0x007f66460606060f }, // L
+ .{ .cp = 0x4d, .bits = 0x0063636b7f7f7763 }, // M
+ .{ .cp = 0x4e, .bits = 0x006363737b6f6763 }, // N
+ .{ .cp = 0x4f, .bits = 0x003e63636363633e }, // O
+ .{ .cp = 0x50, .bits = 0x000f06063e66663f }, // P
+ .{ .cp = 0x51, .bits = 0x703e73636363633e }, // Q
+ .{ .cp = 0x52, .bits = 0x006766363e66663f }, // R
+ .{ .cp = 0x53, .bits = 0x003c6630180c663c }, // S
+ .{ .cp = 0x54, .bits = 0x003c1818185a7e7e }, // T
+ .{ .cp = 0x55, .bits = 0x003e636363636363 }, // U
+ .{ .cp = 0x56, .bits = 0x001c366363636363 }, // V
+ .{ .cp = 0x57, .bits = 0x00367f6b6b636363 }, // W
+ .{ .cp = 0x58, .bits = 0x006363361c366363 }, // X
+ .{ .cp = 0x59, .bits = 0x003c18183c666666 }, // Y
+ .{ .cp = 0x5a, .bits = 0x007f664c1831637f }, // Z
+ .{ .cp = 0x5b, .bits = 0x003c0c0c0c0c0c3c }, // [
+ .{ .cp = 0x5c, .bits = 0x00406030180c0603 }, // \\
+ .{ .cp = 0x5d, .bits = 0x003c30303030303c }, // ]
+ .{ .cp = 0x5e, .bits = 0x0000000063361c08 }, // ^
+ .{ .cp = 0x5f, .bits = 0xff00000000000000 }, // _
+ .{ .cp = 0x60, .bits = 0x000000000030180c }, // `
+ .{ .cp = 0x61, .bits = 0x006e333e301e0000 }, // a
+ .{ .cp = 0x62, .bits = 0x003b6666663e0607 }, // b
+ .{ .cp = 0x63, .bits = 0x003e6303633e0000 }, // c
+ .{ .cp = 0x64, .bits = 0x006e3333333e3038 }, // d
+ .{ .cp = 0x65, .bits = 0x003e037f633e0000 }, // e
+ .{ .cp = 0x66, .bits = 0x000f06061f06663c }, // f
+ .{ .cp = 0x67, .bits = 0x1f303e33336e0000 }, // g
+ .{ .cp = 0x68, .bits = 0x006766666e360607 }, // h
+ .{ .cp = 0x69, .bits = 0x003c1818181c0018 }, // i
+ .{ .cp = 0x6a, .bits = 0x3c66666060600060 }, // j
+ .{ .cp = 0x6b, .bits = 0x0067361e36660607 }, // k
+ .{ .cp = 0x6c, .bits = 0x003c18181818181c }, // l
+ .{ .cp = 0x6d, .bits = 0x006b6b6b7f370000 }, // m
+ .{ .cp = 0x6e, .bits = 0x00666666663b0000 }, // n
+ .{ .cp = 0x6f, .bits = 0x003e6363633e0000 }, // o
+ .{ .cp = 0x70, .bits = 0x0f063e66663b0000 }, // p
+ .{ .cp = 0x71, .bits = 0x78303e33336e0000 }, // q
+ .{ .cp = 0x72, .bits = 0x000f06066e3b0000 }, // r
+ .{ .cp = 0x73, .bits = 0x003f603e037e0000 }, // s
+ .{ .cp = 0x74, .bits = 0x00386c0c0c3f0c0c }, // t
+ .{ .cp = 0x75, .bits = 0x006e333333330000 }, // u
+ .{ .cp = 0x76, .bits = 0x001c366363630000 }, // v
+ .{ .cp = 0x77, .bits = 0x00367f6b6b630000 }, // w
+ .{ .cp = 0x78, .bits = 0x0063361c36630000 }, // x
+ .{ .cp = 0x79, .bits = 0x3f607e6363630000 }, // y
+ .{ .cp = 0x7a, .bits = 0x007e4c18327e0000 }, // z
+ .{ .cp = 0x7b, .bits = 0x007018180e181870 }, // {
+ .{ .cp = 0x7c, .bits = 0x0018181818181818 }, // |
+ .{ .cp = 0x7d, .bits = 0x000e18187018180e }, // }
+ .{ .cp = 0x7e, .bits = 0x0000000000003b6e }, // ~
+ };
+
+ // the block glyph table (sextants + half/quadrant blocks + line glyphs), built at
+ // comptime, and the same set extended with the ASCII glyphs. `render(ascii=…)` picks.
+ const glyphs = blk: {
+ @setEvalBranchQuota(100000);
+ var list: [64 + block_glyphs.len + line_glyphs.len]Glyph = undefined;
+ var p: usize = 0;
+ while (p < 64) : (p += 1) list[p] = .{ .cp = sextantCp(@intCast(p)), .bits = sextantBits(@intCast(p)) };
+ for (block_glyphs, 0..) |bg, i| list[64 + i] = bg;
+ for (line_glyphs, 0..) |lg, i| list[64 + block_glyphs.len + i] = lg;
+ break :blk list;
+ };
+ const glyphs_ascii = glyphs ++ ascii_glyphs;
+
+ fn matchCell(cell: *const [64][3]u8, pal: Palette, gset: []const Glyph) Cell {
+ var counts = [_]u16{0} ** 16;
+ for (cell) |px| counts[nearest(px, pal)] += 1;
+ var cand: [4]u4 = undefined;
+ var ncand: usize = 0;
+ var used = [_]bool{false} ** 16;
+ while (ncand < 4) : (ncand += 1) {
+ var best: ?usize = null;
+ for (counts, 0..) |c, i| {
+ if (used[i] or c == 0) continue;
+ if (best == null or c > counts[best.?]) best = i;
+ }
+ if (best) |bi| {
+ cand[ncand] = @intCast(bi);
+ used[bi] = true;
+ } else break;
+ }
+ if (ncand == 0) return .{}; // can't happen (64 pixels), but keep it total
+ if (ncand == 1) return encode(' ', cand[0], cand[0]); // solid color
+
+ var best_cost: i64 = std.math.maxInt(i64);
+ var best = encode(' ', cand[0], cand[0]);
+ var fi: usize = 0;
+ while (fi < ncand) : (fi += 1) {
+ var bi: usize = 0;
+ while (bi < ncand) : (bi += 1) {
+ if (fi == bi) continue;
+ const fg = cand[fi];
+ const bg = cand[bi];
+ var dfg: [64]u32 = undefined;
+ var dbg: [64]u32 = undefined;
+ var base: i64 = 0;
+ for (cell, 0..) |px, p| {
+ dfg[p] = dist2(px, pal[fg]);
+ dbg[p] = dist2(px, pal[bg]);
+ base += dbg[p];
+ }
+ for (gset) |g| {
+ var delta: i64 = 0;
+ var bits = g.bits;
+ while (bits != 0) : (bits &= bits - 1) {
+ const p: usize = @ctz(bits);
+ delta += @as(i64, dfg[p]) - @as(i64, dbg[p]);
+ }
+ const cost = base + delta;
+ if (cost < best_cost) {
+ best_cost = cost;
+ best = encode(g.cp, fg, bg);
+ }
+ }
+ }
+ }
+ return best;
+ }
+
+ fn encode(cp: u21, fg: u4, bg: u4) Cell {
+ var c = Cell{ .fg = fg, .bg = bg };
+ const n = std.unicode.utf8Encode(cp, &c.glyph) catch 1;
+ c.glyph_len = @intCast(n);
+ return c;
+ }
+
+ 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);
+ if (rows > max_rows) {
+ rows = max_rows;
+ cols = (max_rows * 2 * iw) / ih;
+ }
+ cols = std.math.clamp(cols, 1, max_cols);
+ rows = std.math.clamp(rows, 1, max_rows);
+
+ 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);
+ var cx: usize = 0;
+ while (cx < cols) : (cx += 1) {
+ const rx0 = cx * iw / cols;
+ const rx1 = @max(rx0 + 1, (cx + 1) * iw / cols);
+ var cell: [64][3]u8 = undefined;
+ var sy: usize = 0;
+ while (sy < 8) : (sy += 1) {
+ const py0 = ry0 + sy * (ry1 - ry0) / 8;
+ const py1 = @max(py0 + 1, ry0 + (sy + 1) * (ry1 - ry0) / 8);
+ var sx: usize = 0;
+ while (sx < 8) : (sx += 1) {
+ const px0 = rx0 + sx * (rx1 - rx0) / 8;
+ const px1 = @max(px0 + 1, rx0 + (sx + 1) * (rx1 - rx0) / 8);
+ var rs: usize = 0;
+ var gs: usize = 0;
+ var bs: usize = 0;
+ var n: usize = 0;
+ var yy = py0;
+ while (yy < py1 and yy < ih) : (yy += 1) {
+ var xx = px0;
+ while (xx < px1 and xx < iw) : (xx += 1) {
+ const i = (yy * iw + xx) * 4;
+ rs += rgba[i];
+ gs += rgba[i + 1];
+ bs += rgba[i + 2];
+ n += 1;
+ }
+ }
+ if (n == 0) n = 1;
+ cell[sy * 8 + sx] = .{ @intCast(rs / n), @intCast(gs / n), @intCast(bs / n) };
+ }
+ }
+ cells[cy * cols + cx] = matchCell(&cell, pal, gset);
+ }
+ }
+ return .{ .cells = cells, .cols = cols, .rows = rows };
+ }
+
+ test "sextant codepoints: blocks + endpoints" {
+ try std.testing.expectEqual(@as(u21, ' '), sextantCp(0));
+ try std.testing.expectEqual(@as(u21, 0x2588), sextantCp(63));
+ try std.testing.expectEqual(@as(u21, 0x258C), sextantCp(21));
+ try std.testing.expectEqual(@as(u21, 0x2590), sextantCp(42));
+ try std.testing.expectEqual(@as(u21, 0x1FB00), sextantCp(1)); // first sextant
+ try std.testing.expectEqual(@as(u21, 0x1FB3B), sextantCp(62)); // last sextant
+ }
+
+ test "matchCell: solid color -> space on that bg" {
+ var cell: [64][3]u8 = undefined;
+ for (&cell) |*p| p.* = commodore[5]; // all green
+ const m = matchCell(&cell, commodore, &glyphs);
+ try std.testing.expectEqual(@as(u4, 5), m.bg);
+ try std.testing.expectEqual(@as(u8, ' '), m.glyph[0]);
+ }
+
+ test "matchCell: clean top/bottom split picks the two colors" {
+ var cell: [64][3]u8 = undefined;
+ for (0..64) |p| cell[p] = if (p < 32) commodore[1] else commodore[6]; // white over blue
+ const m = matchCell(&cell, commodore, &glyphs);
+ // both palette colors must be chosen (in some fg/bg order)
+ const a = @as(u4, @min(m.fg, m.bg));
+ const b = @as(u4, @max(m.fg, m.bg));
+ try std.testing.expectEqual(@as(u4, 1), a);
+ try std.testing.expectEqual(@as(u4, 6), b);
+ // a clean top/bottom split must resolve to a real block glyph (the top-4-rows
+ // half block ▀), never a blank cell.
+ const cp = std.unicode.utf8Decode(m.glyph[0..m.glyph_len]) catch 0;
+ try std.testing.expectEqual(@as(u21, 0x2580), cp);
+ }
+
+ test "ascii option only enables the ascii glyphs" {
+ // the ascii glyph codepoints must be reachable exactly when ascii is on.
+ var seen_block = false;
+ var seen_ascii = false;
+ for (glyphs) |g| if (g.cp == '#') {
+ seen_block = true;
+ };
+ for (glyphs_ascii) |g| if (g.cp == '#') {
+ seen_ascii = true;
+ };
+ try std.testing.expect(!seen_block); // '#' is an ascii-only glyph
+ try std.testing.expect(seen_ascii);
+ try std.testing.expectEqual(glyphs.len + ascii_glyphs.len, glyphs_ascii.len);
+ }
+
+ test "ascii glyph is chosen when a cell has its exact shape" {
+ // a cell shaped exactly like the font 'S' (ink=white on black) must match 'S'
+ // with ascii on (cost 0), and fall back to some block glyph with ascii off.
+ const s_bits: u64 = 0x003c6630180c663c;
+ var cell: [64][3]u8 = undefined;
+ for (0..64) |p| cell[p] = if ((s_bits >> @intCast(p)) & 1 != 0) commodore[1] else commodore[0];
+ const on = matchCell(&cell, commodore, &glyphs_ascii);
+ try std.testing.expectEqual(@as(u21, 'S'), std.unicode.utf8Decode(on.glyph[0..on.glyph_len]) catch 0);
+ const off = matchCell(&cell, commodore, &glyphs);
+ try std.testing.expect((std.unicode.utf8Decode(off.glyph[0..off.glyph_len]) catch 0) != 'S');
+ }
+
+ test "render: tiny image produces a grid within bounds" {
+ const a = std.testing.allocator;
+ // 2x2 checker, RGBA
+ var img = [_]u8{0} ** (2 * 2 * 4);
+ img[0] = 255;
+ img[1] = 255;
+ img[2] = 255;
+ img[3] = 255; // (0,0) white
+ img[(3) * 4 + 0] = 255;
+ img[(3) * 4 + 1] = 255;
+ img[(3) * 4 + 2] = 255;
+ img[(3) * 4 + 3] = 255; // (1,1) white
+ const g = try render(a, &img, 2, 2, 10, 10, commodore, true);
+ defer a.free(g.cells);
+ try std.testing.expect(g.cols >= 1 and g.cols <= 10);
+ try std.testing.expect(g.rows >= 1 and g.rows <= 10);
+ try std.testing.expectEqual(g.cols * g.rows, g.cells.len);
+ }
+};
+
+pub const terminal_palette = [16][3]u8{
.{ 0x1D, 0x1F, 0x21 }, // black
.{ 0xCC, 0x66, 0x66 }, // red
.{ 0xB5, 0xBD, 0x68 }, // green
@@ -44,18 +454,14 @@ const ansi_default = [16][3]u8{
};
comptime {
- if (terminal_panes) for (ansi_default, 0..) |rgb, i| {
+ if (terminal_panes) for (terminal_palette, 0..) |rgb, i| {
const c = ghostty_vt.color.default[i];
if (rgb[0] != c.r or rgb[1] != c.g or rgb[2] != c.b) @compileError(
- "src/image.zig ansi_default has drifted from ghostty's color.default",
+ "terminal palette differs from the emulator",
);
};
}
-pub fn ansiPalette() [16][3]u8 {
- return ansi_default;
-}
-
/// cap the longest side before keeping/transmitting: a pane is at most a
/// screenful of cells, multi-thousand-pixel photos waste decode/transmit time
const MAX_DIM: u32 = 1280;