diff options
| author | Gabriel Schneider <[email protected]> | 2026-06-30 20:29:43 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-06-30 22:21:44 -0300 |
| commit | f6089186cfe1517073fafc8979a7546ec773d6c1 (patch) | |
| tree | cfb474b5a5cff5d7197aa5084ea6013a0c53df6f /image.zig | |
| parent | 8b60c2c29c016c7db0fa6ee63769cea1defd9b3c (diff) | |
| download | pardes-f6089186cfe1517073fafc8979a7546ec773d6c1.tar.gz pardes-f6089186cfe1517073fafc8979a7546ec773d6c1.zip | |
stealing petsciinatorfull-prototype
Diffstat (limited to 'image.zig')
| -rw-r--r-- | image.zig | 125 |
1 files changed, 114 insertions, 11 deletions
@@ -5,19 +5,55 @@ // 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). Call `start` once before any pane loads. +// 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("vaxis"); 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, // transmit attempted (success or failure) — try exactly once - handle: ?vaxis.Image = null, // kitty image handle once transmitted (null = pending/failed) + 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. @@ -31,8 +67,14 @@ pub fn stop() void { zstbi.deinit(); } -// Extensions stb_image can decode. Right-clicking one of these — when the terminal -// reports kitty graphics — opens an image pane instead of a text view. +// 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", @@ -44,9 +86,9 @@ pub fn isImagePath(path: []const u8) bool { return false; } -// Decode (RGBA), downscale to MAX_DIM, and transmit to the terminal exactly once. -// Runs from the render loop, which owns the tty writer. On any failure `handle` -// stays null and the pane shows just its tag. +// 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; @@ -66,6 +108,12 @@ pub fn ensureLoaded(view: *View, vx: *vaxis.Vaxis, gpa: std.mem.Allocator, write 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; @@ -75,9 +123,64 @@ pub fn ensureLoaded(view: *View, vx: *vaxis.Vaxis, gpa: std.mem.Allocator, write view.handle = vx.transmitPreEncodedImage(writer, b64, @intCast(src.width), @intCast(src.height), .rgba) catch null; } -// Re-place the image in `win`, scaled to fit while keeping aspect ratio. Kitty -// placements aren't persistent (render clears them), so this must run every frame. -pub fn draw(view: *const View, win: vaxis.Window) void { +// (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 {}; } |
