diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f239ea..e68e9e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +## v1.2.0 (2025-05-26) +- feat(images): Cache images to avoid unecessary re-processing + ## v1.1.0 (2025-05-21) - fix(images): Improve performance by only locking critical parts of image loading - fix(images): Thread the image loading process as not to block user input diff --git a/build.zig b/build.zig index 1ec0344..6bd6d4c 100644 --- a/build.zig +++ b/build.zig @@ -2,7 +2,7 @@ const std = @import("std"); const builtin = @import("builtin"); ///Must match the `version` in `build.zig.zon`. -const version = std.SemanticVersion{ .major = 1, .minor = 1, .patch = 0 }; +const version = std.SemanticVersion{ .major = 1, .minor = 2, .patch = 0 }; const targets: []const std.Target.Query = &.{ .{ .cpu_arch = .aarch64, .os_tag = .macos }, diff --git a/build.zig.zon b/build.zig.zon index 28c4dbf..8d1c3c4 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,7 +1,7 @@ .{ .name = .jido, .fingerprint = 0xee45eabe36cafb57, - .version = "1.1.0", + .version = "1.2.0", .minimum_zig_version = "0.14.0", .dependencies = .{ diff --git a/src/app.zig b/src/app.zig index 85ecacd..d1d4d40 100644 --- a/src/app.zig +++ b/src/app.zig @@ -78,7 +78,30 @@ pub const Event = union(enum) { winsize: vaxis.Winsize, }; +pub const Image = struct { + const Status = enum { + ready, + processing, + }; + + ///Only use on first transmission. Subsequent draws should use + ///`Image.image`. + data: ?vaxis.zigimg.Image = null, + image: ?vaxis.Image = null, + path: ?[]const u8 = null, + status: Status = .processing, + + pub fn deinit(self: @This(), alloc: std.mem.Allocator) void { + if (self.data) |data| { + var d = data; + d.deinit(); + } + if (self.path) |path| alloc.free(path); + } +}; + const actions_len = 100; +const image_cache_cap = 100; const App = @This(); @@ -103,11 +126,10 @@ text_input_buf: [std.fs.max_path_bytes]u8 = undefined, yanked: ?struct { dir: []const u8, entry: std.fs.Dir.Entry } = null, last_known_height: usize, -image: struct { +images: struct { mutex: std.Thread.Mutex = .{}, - data: ?vaxis.zigimg.Image = null, - path: ?[]const u8 = null, -} = .{}, + cache: std.StringHashMap(Image), +}, pub fn init(alloc: std.mem.Allocator) !App { var vx = try vaxis.init(alloc, .{ @@ -133,6 +155,7 @@ pub fn init(alloc: std.mem.Allocator) !App { .text_input = vaxis.widgets.TextInput.init(alloc, &vx.unicode), .actions = CircStack(Action, actions_len).init(), .last_known_height = vx.window().height, + .images = .{ .cache = .init(alloc) }, }; app.loop = vaxis.Loop(Event){ @@ -171,11 +194,12 @@ pub fn deinit(self: *App) void { self.vx.deinit(self.alloc, self.tty.anyWriter()); self.tty.deinit(); if (self.file_logger) |file_logger| file_logger.deinit(); - if (self.image.path) |path| self.alloc.free(path); - if (self.image.data) |data| { - var img_data = data; - img_data.deinit(); + + var image_iter = self.images.cache.iterator(); + while (image_iter.next()) |img| { + img.value_ptr.deinit(self.alloc); } + self.images.cache.deinit(); } pub fn inputToSlice(self: *App) []const u8 { diff --git a/src/drawer.zig b/src/drawer.zig index 36ecc20..cd99da0 100644 --- a/src/drawer.zig +++ b/src/drawer.zig @@ -197,14 +197,37 @@ fn drawFilePreview( } if (!match) break :unsupported; - { - app.image.mutex.lock(); - defer app.image.mutex.unlock(); + app.images.mutex.lock(); + defer app.images.mutex.unlock(); + + if (app.images.cache.getPtr(self.current_item_path)) |cache_entry| { + if (cache_entry.status == .processing) { + _ = preview_win.print(&.{ + .{ .text = "Image still processing." }, + }, .{}); + break :file; + } - if (std.mem.eql(u8, self.current_item_path, app.image.path orelse "")) { - if (app.image.data == null) break :unsupported; + if (cache_entry.image) |img| { + img.draw(preview_win, .{ .scale = .contain }) catch |err| { + const message = try std.fmt.allocPrint(app.alloc, "Failed to draw image to screen - {}.", .{err}); + defer app.alloc.free(message); + app.notification.write(message, .err) catch {}; + if (app.file_logger) |file_logger| file_logger.write(message, .err) catch {}; + + _ = preview_win.print(&.{ + .{ .text = "Failed to draw image to screen. No preview available." }, + }, .{}); + cache_entry.image = null; + break :file; + }; + } else { + if (cache_entry.data == null) { + const path = try app.alloc.dupe(u8, self.current_item_path); + processImage(app, path) catch break :unsupported; + } - if (app.vx.transmitImage(app.alloc, app.tty.anyWriter(), &app.image.data.?, .rgba)) |img| { + if (app.vx.transmitImage(app.alloc, app.tty.anyWriter(), &cache_entry.data.?, .rgba)) |img| { img.draw(preview_win, .{ .scale = .contain }) catch |err| { const message = try std.fmt.allocPrint(app.alloc, "Failed to draw image to screen - {}.", .{err}); defer app.alloc.free(message); @@ -214,20 +237,20 @@ fn drawFilePreview( _ = preview_win.print(&.{ .{ .text = "Failed to draw image to screen. No preview available." }, }, .{}); + break :file; }; + cache_entry.image = img; + cache_entry.data.?.deinit(); + cache_entry.data = null; } else |_| { break :unsupported; } - - break :file; } + break :file; + } else { const path = try app.alloc.dupe(u8, self.current_item_path); - const load_img_thread = std.Thread.spawn(.{}, loadImage, .{ - app, - path, - }) catch break :unsupported; - load_img_thread.detach(); + processImage(app, path) catch break :unsupported; } break :file; @@ -605,21 +628,43 @@ fn drawNotification( }, .{ .wrap = .word }); } +fn processImage(app: *App, path: []const u8) error{ Unsupported, OutOfMemory }!void { + app.images.cache.put(path, .{ .path = path, .status = .processing }) catch { + const message = try std.fmt.allocPrint(app.alloc, "Failed to load image '{s}' - error occurred while attempting to add image to cache.", .{path}); + defer app.alloc.free(message); + app.notification.write(message, .err) catch {}; + if (app.file_logger) |file_logger| file_logger.write(message, .err) catch {}; + return error.Unsupported; + }; + + const load_img_thread = std.Thread.spawn(.{}, loadImage, .{ + app, + path, + }) catch return error.Unsupported; + load_img_thread.detach(); +} + fn loadImage(app: *App, path: []const u8) error{ Unsupported, OutOfMemory }!void { - const image = vaxis.zigimg.Image.fromFilePath(app.alloc, path) catch { + const data = vaxis.zigimg.Image.fromFilePath(app.alloc, path) catch { + const message = try std.fmt.allocPrint(app.alloc, "Failed to load image '{s}' - error occurred while attempting to read image from path.", .{path}); + defer app.alloc.free(message); + app.notification.write(message, .err) catch {}; + if (app.file_logger) |file_logger| file_logger.write(message, .err) catch {}; return error.Unsupported; }; - app.image.mutex.lock(); - if (app.image.data) |data| { - var img_data = data; - img_data.deinit(); + app.images.mutex.lock(); + if (app.images.cache.getPtr(path)) |entry| { + entry.status = .ready; + entry.data = data; + } else { + const message = try std.fmt.allocPrint(app.alloc, "Failed to load image '{s}' - error occurred while attempting to add image to cache.", .{path}); + defer app.alloc.free(message); + app.notification.write(message, .err) catch {}; + if (app.file_logger) |file_logger| file_logger.write(message, .err) catch {}; + return error.Unsupported; } - app.image.data = image; - - if (app.image.path) |p| app.alloc.free(p); - app.image.path = path; - app.image.mutex.unlock(); + app.images.mutex.unlock(); app.loop.postEvent(.image_ready); }