summaryrefslogtreecommitdiff
path: root/image.zig
diff options
context:
space:
mode:
Diffstat (limited to 'image.zig')
-rw-r--r--image.zig125
1 files changed, 114 insertions, 11 deletions
diff --git a/image.zig b/image.zig
index 56085f5a..b6ebcf71 100644
--- a/image.zig
+++ b/image.zig
@@ -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 {};
}