summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/fs.zig1
-rw-r--r--src/image.zig235
-rw-r--r--src/panes.zig231
3 files changed, 237 insertions, 230 deletions
diff --git a/src/fs.zig b/src/fs.zig
index 4fbfe9f9..8fd4d711 100644
--- a/src/fs.zig
+++ b/src/fs.zig
@@ -37,6 +37,7 @@ const source_files = [_]Source{
.{ .path = "src/pardes.zig", .contents = @embedFile("pardes.zig") },
.{ .path = "src/panes.zig", .contents = @embedFile("panes.zig") },
.{ .path = "src/Mini.zig", .contents = @embedFile("Mini.zig") },
+ .{ .path = "src/image.zig", .contents = @embedFile("image.zig") },
.{ .path = "src/layout.zig", .contents = @embedFile("layout.zig") },
.{ .path = "src/fs.zig", .contents = @embedFile("fs.zig") },
.{ .path = "src/look.zig", .contents = @embedFile("look.zig") },
diff --git a/src/image.zig b/src/image.zig
index 592230c8..09f049ca 100644
--- a/src/image.zig
+++ b/src/image.zig
@@ -7,6 +7,10 @@ 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;
+const pardes = @import("pardes.zig");
+const config = @import("config.zig");
+const filesystem = @import("fs.zig");
+const dump = @import("dump.zig");
// Glyph matching adapted from caioluders/petsciinator.
pub const GlyphArt = struct {
pub const commodore = [16][3]u8{
@@ -751,3 +755,234 @@ pub fn decode(gpa: std.mem.Allocator, bytes: []const u8) ?struct { rgba: []u8, w
const rgba = gpa.dupe(u8, src.data) catch return null;
return .{ .rgba = rgba, .w = src.width, .h = src.height };
}
+
+// ---- the image pane ----
+
+const GridKey = struct {
+ cols: u16 = 0,
+ rows: u16 = 0,
+ palette: PaletteMode = .commodore,
+ ascii: bool = true,
+};
+
+pub const State = struct {
+ path: []u8,
+ glyph_art: bool = false,
+ pmode: PaletteMode = .commodore,
+ ascii: bool = true,
+ tried: bool = false,
+ rgba: []u8 = &.{},
+ iw: usize = 0,
+ ih: usize = 0,
+ /// Dump-loaded bytes, decoded lazily by the same path as a disk image.
+ raw: []u8 = &.{},
+ grid: GlyphArt.Grid = .{ .cells = &.{}, .cols = 0, .rows = 0 },
+ grid_key: GridKey = .{},
+
+ pub fn deinit(state: *State, gpa: std.mem.Allocator) void {
+ gpa.free(state.path);
+ if (state.rgba.len > 0) gpa.free(state.rgba);
+ if (state.raw.len > 0) gpa.free(state.raw);
+ if (state.grid.cells.len > 0) gpa.free(state.grid.cells);
+ state.* = undefined;
+ }
+};
+
+/// Construct an image pane from a path and optionally transferred dump bytes.
+/// `raw` must be image_gpa-owned and ownership transfers only on success.
+/// Without them the file is read here, not at the first draw: a draw
+/// must not go out into the host (`pardes.turn`), and the path may be a
+/// mount this editor serves. A file that cannot be read draws blank.
+pub fn create(p: *pardes.Pardes, id: usize, path: []const u8, raw: []u8) !*pardes.Pane {
+ var bytes = raw;
+ var read_here = false;
+ if (bytes.len == 0) {
+ // The slot stays ours across the read: another request may
+ // make a pane meanwhile.
+ p.reserved_slots[id] = true;
+ defer p.reserved_slots[id] = false;
+ if (filesystem.read(p, path)) |from_disk| {
+ defer p.gpa.free(from_disk);
+ bytes = try p.image_gpa.dupe(u8, from_disk);
+ read_here = true;
+ } else |_| {}
+ }
+ errdefer if (read_here) p.image_gpa.free(bytes);
+ const path_copy = try p.image_gpa.dupe(u8, path);
+ errdefer p.image_gpa.free(path_copy);
+ const pane = try p.newDocPane(id);
+ pane.image = .{ .path = path_copy, .raw = bytes };
+ return pane;
+}
+
+/// Serialize the binary image record used by images and, for dump-schema
+/// compatibility, PDFs. Common pane metadata is supplied by the core.
+pub fn dumpPane(
+ p: *pardes.Pardes,
+ arena: std.mem.Allocator,
+ pane: *const pardes.Pane,
+ tag: []const u8,
+ body: []const u8,
+ scroll: usize,
+ path: []const u8,
+ raw: []const u8,
+) !dump.Pane {
+ const bytes = if (raw.len > 0) raw else filesystem.read(p, path) catch &.{};
+ defer if (raw.len == 0) p.gpa.free(bytes);
+ return .{
+ .kind = .image,
+ .tag = tag,
+ .body = body,
+ .scroll = scroll,
+ .cols = pane.cols,
+ .rows = pane.rows,
+ .vweight = pane.vweight,
+ .collapsed = pane.collapsed,
+ .image = .{
+ .path = path,
+ .bytes_b64 = if (bytes.len > 0) try dump.encodeBytes(arena, bytes) else "",
+ .petscii = if (pane.image) |state| state.glyph_art else false,
+ .palette = if (pane.image) |state| switch (state.pmode) {
+ .commodore => .commodore,
+ .terminal => .terminal,
+ } else .commodore,
+ .ascii = if (pane.image) |state| state.ascii else true,
+ },
+ };
+}
+
+pub fn restore(p: *pardes.Pardes, id: usize, src: dump.Pane) !*pardes.Pane {
+ const saved = src.image.?;
+ var raw: []u8 = if (saved.bytes_b64.len > 0)
+ try dump.decodeBytes(p.image_gpa, saved.bytes_b64)
+ else
+ &.{};
+ errdefer if (raw.len > 0) p.image_gpa.free(raw);
+ const pane = try create(p, id, saved.path, raw);
+ raw = &.{};
+ pane.image.?.glyph_art = saved.petscii;
+ pane.image.?.pmode = switch (saved.palette) {
+ .commodore => .commodore,
+ .terminal => .terminal,
+ };
+ pane.image.?.ascii = saved.ascii;
+ pane.cols = @max(1, src.cols);
+ pane.rows = @max(1, src.rows);
+ return pane;
+}
+
+pub fn toggleGlyphArt(state: *State) void {
+ state.glyph_art = !state.glyph_art;
+}
+
+pub fn togglePalette(state: *State) void {
+ state.pmode = if (state.pmode == .commodore) .terminal else .commodore;
+}
+
+pub fn toggleAscii(state: *State) void {
+ state.ascii = !state.ascii;
+}
+
+/// Image renderer choices are pane-local, not global Config values. Keep them
+/// queryable where they apply: the live tag beside the image's path.
+pub fn tagPrefix(arena: std.mem.Allocator, state: *const State) ![]u8 {
+ return std.fmt.allocPrint(
+ arena,
+ "{s} petscii:{s} palette:{s} ascii:{s} {s}",
+ .{
+ config.tag_image,
+ if (state.glyph_art) "on" else "off",
+ @tagName(state.pmode),
+ if (state.ascii) "on" else "off",
+ state.path,
+ },
+ );
+}
+
+pub fn legacySavedPrefix(state: *const State, saved_tag: []const u8) ?[]const u8 {
+ const lead = config.tag_image ++ " ";
+ if (!std.mem.startsWith(u8, saved_tag, lead)) return null;
+ const path_at = lead.len;
+ if (!std.mem.startsWith(u8, saved_tag[path_at..], state.path)) return null;
+ return saved_tag[0 .. path_at + state.path.len];
+}
+
+fn ensureDecoded(p: *pardes.Pardes, state: *State) void {
+ if (state.tried) return;
+ state.tried = true;
+ if (state.raw.len == 0) return; // nothing was readable at open
+ if (decode(p.image_gpa, state.raw)) |decoded| {
+ state.rgba = decoded.rgba;
+ state.iw = decoded.w;
+ state.ih = decoded.h;
+ }
+}
+
+fn ensureGrid(p: *pardes.Pardes, state: *State, cols: u16, rows: u16) void {
+ const wanted = GridKey{ .cols = cols, .rows = rows, .palette = state.pmode, .ascii = state.ascii };
+ if (state.grid.cells.len > 0 and std.meta.eql(state.grid_key, wanted)) return;
+ if (state.grid.cells.len > 0) p.image_gpa.free(state.grid.cells);
+ const palette = switch (state.pmode) {
+ .commodore => GlyphArt.commodore,
+ .terminal => terminal_palette,
+ };
+ state.grid = GlyphArt.render(
+ p.image_gpa,
+ state.rgba,
+ state.iw,
+ state.ih,
+ cols,
+ rows,
+ palette,
+ state.ascii,
+ ) catch .{ .cells = &.{}, .cols = 0, .rows = 0 };
+ state.grid_key = wanted;
+}
+
+pub fn draw(
+ p: *pardes.Pardes,
+ state: *State,
+ pane_id: u8,
+ serial: u32,
+ x: u16,
+ y: u16,
+ cols: u16,
+ rows: u16,
+) void {
+ ensureDecoded(p, state);
+ if (state.rgba.len == 0 or cols == 0 or rows == 0) return;
+ if (!state.glyph_art and p.native_images) {
+ _ = p.appendImagePlace(.{
+ .pane = pane_id,
+ .serial = serial,
+ .x = x,
+ .y = y,
+ .w = cols,
+ .h = rows,
+ .rgba = state.rgba,
+ .iw = state.iw,
+ .ih = state.ih,
+ });
+ return;
+ }
+
+ ensureGrid(p, state, cols, rows);
+ if (state.grid.cells.len == 0) return;
+ const offx = if (cols > state.grid.cols) (@as(usize, cols) - state.grid.cols) / 2 else 0;
+ const offy = if (rows > state.grid.rows) (@as(usize, rows) - state.grid.rows) / 2 else 0;
+ for (0..state.grid.rows) |cy| for (0..state.grid.cols) |cx| {
+ const cell = &state.grid.cells[cy * state.grid.cols + cx];
+ const fg: pardes.Color = switch (state.pmode) {
+ .commodore => .{ .rgb = GlyphArt.commodore[cell.fg] },
+ .terminal => .{ .index = cell.fg },
+ };
+ const bg: pardes.Color = switch (state.pmode) {
+ .commodore => .{ .rgb = GlyphArt.commodore[cell.bg] },
+ .terminal => .{ .index = cell.bg },
+ };
+ const sx = x + @as(u16, @intCast(offx + cx));
+ const sy = y + @as(u16, @intCast(offy + cy));
+ if (sx < p.surface.cols and sy < p.surface.rows)
+ p.surface.set(sx, sy, cell.glyph[0..cell.glyph_len], .{ .fg = fg, .bg = bg });
+ };
+}
diff --git a/src/panes.zig b/src/panes.zig
index d494cfe4..bc579a92 100644
--- a/src/panes.zig
+++ b/src/panes.zig
@@ -3176,236 +3176,7 @@ pub const Output = struct {
pub const Mini = @import("Mini.zig");
-pub const Image = struct {
- const GridKey = struct {
- cols: u16 = 0,
- rows: u16 = 0,
- palette: image.PaletteMode = .commodore,
- ascii: bool = true,
- };
-
- pub const State = struct {
- path: []u8,
- glyph_art: bool = false,
- pmode: image.PaletteMode = .commodore,
- ascii: bool = true,
- tried: bool = false,
- rgba: []u8 = &.{},
- iw: usize = 0,
- ih: usize = 0,
- /// Dump-loaded bytes, decoded lazily by the same path as a disk image.
- raw: []u8 = &.{},
- grid: image.GlyphArt.Grid = .{ .cells = &.{}, .cols = 0, .rows = 0 },
- grid_key: GridKey = .{},
-
- pub fn deinit(state: *State, gpa: std.mem.Allocator) void {
- gpa.free(state.path);
- if (state.rgba.len > 0) gpa.free(state.rgba);
- if (state.raw.len > 0) gpa.free(state.raw);
- if (state.grid.cells.len > 0) gpa.free(state.grid.cells);
- state.* = undefined;
- }
- };
-
- /// Construct an image pane from a path and optionally transferred dump bytes.
- /// `raw` must be image_gpa-owned and ownership transfers only on success.
- /// Without them the file is read here, not at the first draw: a draw
- /// must not go out into the host (`pardes.turn`), and the path may be a
- /// mount this editor serves. A file that cannot be read draws blank.
- pub fn create(p: *pardes.Pardes, id: usize, path: []const u8, raw: []u8) !*pardes.Pane {
- var bytes = raw;
- var read_here = false;
- if (bytes.len == 0) {
- // The slot stays ours across the read: another request may
- // make a pane meanwhile.
- p.reserved_slots[id] = true;
- defer p.reserved_slots[id] = false;
- if (filesystem.read(p, path)) |from_disk| {
- defer p.gpa.free(from_disk);
- bytes = try p.image_gpa.dupe(u8, from_disk);
- read_here = true;
- } else |_| {}
- }
- errdefer if (read_here) p.image_gpa.free(bytes);
- const path_copy = try p.image_gpa.dupe(u8, path);
- errdefer p.image_gpa.free(path_copy);
- const pane = try p.newDocPane(id);
- pane.image = .{ .path = path_copy, .raw = bytes };
- return pane;
- }
-
- /// Serialize the binary image record used by images and, for dump-schema
- /// compatibility, PDFs. Common pane metadata is supplied by the core.
- pub fn dumpPane(
- p: *pardes.Pardes,
- arena: std.mem.Allocator,
- pane: *const pardes.Pane,
- tag: []const u8,
- body: []const u8,
- scroll: usize,
- path: []const u8,
- raw: []const u8,
- ) !dump.Pane {
- const bytes = if (raw.len > 0) raw else filesystem.read(p, path) catch &.{};
- defer if (raw.len == 0) p.gpa.free(bytes);
- return .{
- .kind = .image,
- .tag = tag,
- .body = body,
- .scroll = scroll,
- .cols = pane.cols,
- .rows = pane.rows,
- .vweight = pane.vweight,
- .collapsed = pane.collapsed,
- .image = .{
- .path = path,
- .bytes_b64 = if (bytes.len > 0) try dump.encodeBytes(arena, bytes) else "",
- .petscii = if (pane.image) |state| state.glyph_art else false,
- .palette = if (pane.image) |state| switch (state.pmode) {
- .commodore => .commodore,
- .terminal => .terminal,
- } else .commodore,
- .ascii = if (pane.image) |state| state.ascii else true,
- },
- };
- }
-
- pub fn restore(p: *pardes.Pardes, id: usize, src: dump.Pane) !*pardes.Pane {
- const saved = src.image.?;
- var raw: []u8 = if (saved.bytes_b64.len > 0)
- try dump.decodeBytes(p.image_gpa, saved.bytes_b64)
- else
- &.{};
- errdefer if (raw.len > 0) p.image_gpa.free(raw);
- const pane = try create(p, id, saved.path, raw);
- raw = &.{};
- pane.image.?.glyph_art = saved.petscii;
- pane.image.?.pmode = switch (saved.palette) {
- .commodore => .commodore,
- .terminal => .terminal,
- };
- pane.image.?.ascii = saved.ascii;
- pane.cols = @max(1, src.cols);
- pane.rows = @max(1, src.rows);
- return pane;
- }
-
- pub fn toggleGlyphArt(state: *State) void {
- state.glyph_art = !state.glyph_art;
- }
-
- pub fn togglePalette(state: *State) void {
- state.pmode = if (state.pmode == .commodore) .terminal else .commodore;
- }
-
- pub fn toggleAscii(state: *State) void {
- state.ascii = !state.ascii;
- }
-
- /// Image renderer choices are pane-local, not global Config values. Keep them
- /// queryable where they apply: the live tag beside the image's path.
- pub fn tagPrefix(arena: std.mem.Allocator, state: *const State) ![]u8 {
- return std.fmt.allocPrint(
- arena,
- "{s} petscii:{s} palette:{s} ascii:{s} {s}",
- .{
- config.tag_image,
- if (state.glyph_art) "on" else "off",
- @tagName(state.pmode),
- if (state.ascii) "on" else "off",
- state.path,
- },
- );
- }
-
- pub fn legacySavedPrefix(state: *const State, saved_tag: []const u8) ?[]const u8 {
- const lead = config.tag_image ++ " ";
- if (!std.mem.startsWith(u8, saved_tag, lead)) return null;
- const path_at = lead.len;
- if (!std.mem.startsWith(u8, saved_tag[path_at..], state.path)) return null;
- return saved_tag[0 .. path_at + state.path.len];
- }
-
- fn ensureDecoded(p: *pardes.Pardes, state: *State) void {
- if (state.tried) return;
- state.tried = true;
- if (state.raw.len == 0) return; // nothing was readable at open
- if (image.decode(p.image_gpa, state.raw)) |decoded| {
- state.rgba = decoded.rgba;
- state.iw = decoded.w;
- state.ih = decoded.h;
- }
- }
-
- fn ensureGrid(p: *pardes.Pardes, state: *State, cols: u16, rows: u16) void {
- const wanted = GridKey{ .cols = cols, .rows = rows, .palette = state.pmode, .ascii = state.ascii };
- if (state.grid.cells.len > 0 and std.meta.eql(state.grid_key, wanted)) return;
- if (state.grid.cells.len > 0) p.image_gpa.free(state.grid.cells);
- const palette = switch (state.pmode) {
- .commodore => image.GlyphArt.commodore,
- .terminal => image.terminal_palette,
- };
- state.grid = image.GlyphArt.render(
- p.image_gpa,
- state.rgba,
- state.iw,
- state.ih,
- cols,
- rows,
- palette,
- state.ascii,
- ) catch .{ .cells = &.{}, .cols = 0, .rows = 0 };
- state.grid_key = wanted;
- }
-
- pub fn draw(
- p: *pardes.Pardes,
- state: *State,
- pane_id: u8,
- serial: u32,
- x: u16,
- y: u16,
- cols: u16,
- rows: u16,
- ) void {
- ensureDecoded(p, state);
- if (state.rgba.len == 0 or cols == 0 or rows == 0) return;
- if (!state.glyph_art and p.native_images) {
- _ = p.appendImagePlace(.{
- .pane = pane_id,
- .serial = serial,
- .x = x,
- .y = y,
- .w = cols,
- .h = rows,
- .rgba = state.rgba,
- .iw = state.iw,
- .ih = state.ih,
- });
- return;
- }
-
- ensureGrid(p, state, cols, rows);
- if (state.grid.cells.len == 0) return;
- const offx = if (cols > state.grid.cols) (@as(usize, cols) - state.grid.cols) / 2 else 0;
- const offy = if (rows > state.grid.rows) (@as(usize, rows) - state.grid.rows) / 2 else 0;
- for (0..state.grid.rows) |cy| for (0..state.grid.cols) |cx| {
- const cell = &state.grid.cells[cy * state.grid.cols + cx];
- const fg: pardes.Color = switch (state.pmode) {
- .commodore => .{ .rgb = image.GlyphArt.commodore[cell.fg] },
- .terminal => .{ .index = cell.fg },
- };
- const bg: pardes.Color = switch (state.pmode) {
- .commodore => .{ .rgb = image.GlyphArt.commodore[cell.bg] },
- .terminal => .{ .index = cell.bg },
- };
- const sx = x + @as(u16, @intCast(offx + cx));
- const sy = y + @as(u16, @intCast(offy + cy));
- if (sx < p.surface.cols and sy < p.surface.rows)
- p.surface.set(sx, sy, cell.glyph[0..cell.glyph_len], .{ .fg = fg, .bg = bg });
- };
- }
-};
+pub const Image = @import("image.zig");
pub const Pdf = struct {
pub const enabled = @import("pardes_config").mupdf;