summaryrefslogtreecommitdiff
path: root/image.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-07-05 20:21:50 -0300
committerGabriel Schneider <[email protected]>2026-08-01 15:02:07 -0300
commitde4def3548a6729b0dfd2120495a61beef8c8c2c (patch)
tree12144f0f64dc96bb4741459c1f9db57f44349330 /image.zig
parent7988d9bc6e31210ff13994f18d5622dd57ba9341 (diff)
downloadpardes-de4def3548a6729b0dfd2120495a61beef8c8c2c.tar.gz
pardes-de4def3548a6729b0dfd2120495a61beef8c8c2c.zip
pardes v2: the rewrite, complete and organized. src/ (core + three shells), test/ (snapshot parity harness + 18 frozen goldens). One sans-IO core, vaxis tty + SDL3 GPU native + wasm web shells, 18/18 parity with the purged prototype, 7.6k lines vs 12.1k. Fix: gui shell pre-sized the core at init so the greet-releasing resize never fired (blank panes until first interaction); live sessions now init at defaults and get the real grid as a resize event (the shell contract, documented on Options).
Diffstat (limited to 'image.zig')
-rw-r--r--image.zig199
1 files changed, 0 insertions, 199 deletions
diff --git a/image.zig b/image.zig
deleted file mode 100644
index 79e7775f..00000000
--- a/image.zig
+++ /dev/null
@@ -1,199 +0,0 @@
-// Image-viewer panes — the kitty-graphics side quest, kept out of the main editor
-// logic. A pane can carry a `View`; the render loop transmits it once (lazily,
-// because transmitting needs the tty writer) and re-places it every frame.
-//
-// Decoding is done by zstbi (C stb_image) — NOT zigimg, whose pure-Zig decoder
-// crashes the Zig 0.16 compiler when analyzed here. We decode to RGBA, downscale
-// large images so the transmit stays cheap, then hand the raw pixels to the
-// terminal via vaxis (kitty graphics). When the terminal has no kitty graphics —
-// or the user toggles "Petscii" on the pane — we instead render the kept RGBA as
-// PETSCII art (see petscii.zig). Call `start` once before any pane loads.
-const std = @import("std");
-const vaxis = @import("ui");
-const zstbi = @import("zstbi");
-const ghostty_vt = @import("ghostty-vt");
-const petscii = @import("petscii.zig");
-
-// The terminal's own 16 ANSI colors (ghostty's default palette) — what the PETSCII
-// matcher scores against; cells are then painted as indexed colors so the real
-// terminal resolves the displayed RGB (the art recolors with the terminal theme).
-fn ansiPalette() [16][3]u8 {
- var p: [16][3]u8 = undefined;
- for (0..16) |i| {
- const c = ghostty_vt.color.default[i];
- p[i] = .{ c.r, c.g, c.b };
- }
- return p;
-}
-
-// Cap the longest side before transmitting: a terminal pane is at most a screenful
-// of cells, so multi-thousand-pixel photos just waste decode/transmit time.
-const MAX_DIM: u32 = 1280;
-
-// Which 16-color palette the PETSCII art matches + paints with. `commodore` (the
-// default) is the fixed C64 palette drawn as truecolor; `terminal` scores against
-// and paints with the terminal's own ANSI palette (recolors with the terminal theme).
-pub const PaletteMode = enum { commodore, terminal };
-
-pub const View = struct {
- path: []const u8, // gpa-owned absolute path; doubles as the tag label
- tried: bool = false, // decode/transmit attempted — try exactly once
- handle: ?vaxis.Image = null, // kitty image handle once transmitted (null = none)
- // PETSCII fallback/toggle: when set we render the kept pixels as C64 glyph art
- // instead of placing the kitty image. Defaults to true on no-kitty terminals.
- petscii: bool = false,
- pmode: PaletteMode = .commodore, // color palette; terminal colors behind the toggle
- ascii: bool = true, // include ASCII glyphs in the matcher (max detail)
- rgba: []u8 = &.{}, // gpa-owned decoded+downscaled pixels, kept for PETSCII re-render
- iw: usize = 0,
- ih: usize = 0,
- grid: []petscii.Cell = &.{}, // gpa-owned cached PETSCII grid
- gw: usize = 0,
- gh: usize = 0,
- grid_w: u16 = 0, // body cols the grid was built for (recompute on change)
- grid_h: u16 = 0,
- grid_mode: PaletteMode = .commodore, // palette the grid was built for
- grid_ascii: bool = true, // ascii setting the grid was built for
- // ponytail: we never vx.freeImage on close, so the terminal-side pixel data
- // leaks until app exit. Fine for a viewer; add freeImage in the close paths
- // (needs vx + the tty writer there) if you open many large images.
-};
-
-// Initialize / tear down the stb_image allocator. Call once at startup / exit.
-pub fn start(io: std.Io, gpa: std.mem.Allocator) void {
- zstbi.init(io, gpa);
-}
-pub fn stop() void {
- zstbi.deinit();
-}
-
-// free the gpa-owned buffers a View holds (RGBA + PETSCII grid). Call on pane close.
-pub fn deinitView(view: *View, gpa: std.mem.Allocator) void {
- if (view.rgba.len > 0) gpa.free(view.rgba);
- if (view.grid.len > 0) gpa.free(view.grid);
-}
-
-// Extensions stb_image can decode. Right-clicking one of these opens an image pane
-// (kitty if supported, else PETSCII) instead of a text view.
-pub fn isImagePath(path: []const u8) bool {
- const exts = [_][]const u8{
- ".png", ".jpg", ".jpeg", ".jpe", ".bmp", ".tga",
- ".gif", ".psd", ".hdr", ".pic", ".pnm", ".ppm",
- ".pgm",
- };
- for (exts) |ext| {
- if (path.len >= ext.len and std.ascii.eqlIgnoreCase(path[path.len - ext.len ..], ext)) return true;
- }
- return false;
-}
-
-fn keepDecodedImage(view: *View, vx: *vaxis.Vaxis, gpa: std.mem.Allocator, writer: *std.Io.Writer, img: *const zstbi.Image) void {
- // downscale the longest side to MAX_DIM (keeping aspect) when it's bigger.
- var scaled: ?zstbi.Image = null;
- defer if (scaled) |*s| s.deinit();
- const longest = @max(img.width, img.height);
- const src: *const zstbi.Image = if (longest > MAX_DIM) blk: {
- const nw = @max(1, img.width * MAX_DIM / longest);
- const nh = @max(1, img.height * MAX_DIM / longest);
- scaled = img.resize(nw, nh);
- break :blk &scaled.?;
- } else img;
-
- // keep a gpa-owned copy of the pixels for the PETSCII renderer.
- view.rgba = gpa.dupe(u8, src.data) catch &.{};
- view.iw = src.width;
- view.ih = src.height;
-
- if (!vx.caps.kitty_graphics) return; // PETSCII-only terminal: nothing to transmit
- // base64-encode the raw RGBA and transmit it (terminal shows the pixels as-is,
- // no terminal-side decode); vaxis chunks the payload over the kitty protocol.
- const enc = std.base64.standard.Encoder;
- const b64 = gpa.alloc(u8, enc.calcSize(src.data.len)) catch return;
- defer gpa.free(b64);
- _ = enc.encode(b64, src.data);
- view.handle = vx.transmitPreEncodedImage(writer, b64, @intCast(src.width), @intCast(src.height), .rgba) catch null;
-}
-
-// Decode (RGBA), downscale to MAX_DIM, keep the pixels for PETSCII, and — when the
-// terminal supports kitty graphics — transmit them once. Runs from the render loop,
-// which owns the tty writer. On failure `handle` stays null (PETSCII still works).
-pub fn ensureLoaded(view: *View, vx: *vaxis.Vaxis, gpa: std.mem.Allocator, writer: *std.Io.Writer) void {
- if (view.tried) return;
- view.tried = true;
- var pathbuf: [4096]u8 = undefined;
- const pz = std.fmt.bufPrintZ(&pathbuf, "{s}", .{view.path}) catch return;
- var img = zstbi.Image.loadFromFile(pz, 4) catch return; // force 4 components = RGBA
- defer img.deinit();
- keepDecodedImage(view, vx, gpa, writer, &img);
-}
-
-pub fn ensureLoadedFromMemory(view: *View, bytes: []const u8, vx: *vaxis.Vaxis, gpa: std.mem.Allocator, writer: *std.Io.Writer) void {
- if (view.tried) return;
- view.tried = true;
- if (bytes.len == 0) return;
- var img = zstbi.Image.loadFromMemory(bytes, 4) catch return; // force 4 components = RGBA
- defer img.deinit();
- keepDecodedImage(view, vx, gpa, writer, &img);
-}
-
-// (Re)build the cached PETSCII grid for the current body size, if PETSCII drawing
-// is active and the size changed. Runs from the render loop (which has gpa).
-pub fn ensureGrid(view: *View, gpa: std.mem.Allocator, body_cols: u16, body_rows: u16) void {
- if (!petsciiActive(view) or view.rgba.len == 0 or body_cols == 0 or body_rows == 0) return;
- if (view.grid.len != 0 and view.grid_w == body_cols and view.grid_h == body_rows and
- view.grid_mode == view.pmode and view.grid_ascii == view.ascii) return;
- if (view.grid.len > 0) gpa.free(view.grid);
- const pal = switch (view.pmode) {
- .commodore => petscii.commodore,
- .terminal => ansiPalette(),
- };
- const g = petscii.render(gpa, view.rgba, view.iw, view.ih, body_cols, body_rows, pal, view.ascii) catch petscii.Grid{ .cells = &.{}, .gw = 0, .gh = 0 };
- view.grid = g.cells;
- view.gw = g.gw;
- view.gh = g.gh;
- view.grid_w = body_cols;
- view.grid_h = body_rows;
- view.grid_mode = view.pmode;
- view.grid_ascii = view.ascii;
-}
-
-// PETSCII draws when toggled on, or whenever there's no kitty handle to place.
-fn petsciiActive(view: *const View) bool {
- return view.petscii or view.handle == null;
-}
-
-// Draw the image into `win` (the pane body). PETSCII blits the cached glyph grid,
-// centered; otherwise the kitty placement is re-issued (it isn't persistent).
-pub fn draw(view: *const View, win: vaxis.Window, body_cols: u16, body_rows: u16) void {
- if (petsciiActive(view) and view.grid.len != 0) {
- const offx = if (body_cols > view.gw) (@as(usize, body_cols) - view.gw) / 2 else 0;
- const offy = if (body_rows > view.gh) (@as(usize, body_rows) - view.gh) / 2 else 0;
- var cy: usize = 0;
- while (cy < view.gh) : (cy += 1) {
- var cx: usize = 0;
- while (cx < view.gw) : (cx += 1) {
- // pointer into the retained grid — NOT a copy: vaxis keeps the
- // grapheme slice alive until vx.render(), so it must point at the
- // persistent grid, not a stack-local Cell.
- const c = &view.grid[cy * view.gw + cx];
- // commodore: paint the fixed C64 colors as truecolor. terminal:
- // paint indexed colors so the real terminal resolves the RGB.
- const fg: vaxis.Color = switch (view.pmode) {
- .commodore => .{ .rgb = petscii.commodore[c.fg] },
- .terminal => .{ .index = c.fg },
- };
- const bg: vaxis.Color = switch (view.pmode) {
- .commodore => .{ .rgb = petscii.commodore[c.bg] },
- .terminal => .{ .index = c.bg },
- };
- win.writeCell(@intCast(offx + cx), @intCast(offy + cy), .{
- .char = .{ .grapheme = c.glyph[0..c.glen] },
- .style = .{ .fg = fg, .bg = bg },
- });
- }
- }
- return;
- }
- const h = view.handle orelse return;
- h.draw(win, .{ .scale = .contain }) catch {};
-}