diff options
| author | Gabriel Schneider <[email protected]> | 2026-07-05 20:21:50 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-08-01 15:02:07 -0300 |
| commit | de4def3548a6729b0dfd2120495a61beef8c8c2c (patch) | |
| tree | 12144f0f64dc96bb4741459c1f9db57f44349330 /image.zig | |
| parent | 7988d9bc6e31210ff13994f18d5622dd57ba9341 (diff) | |
| download | pardes-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.zig | 199 |
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 {}; -} |
