From a355aed1a2f2d6b3faf2ee83f424f1e44bee0c97 Mon Sep 17 00:00:00 2001 From: Brook Jeynes Date: Wed, 21 May 2025 22:19:51 +1000 Subject: [PATCH] feat: thread image processing (#19) This PR threads the image processing code in an attempt to reduce the terminal freezing when scrolling past or loading large images closes to #4 --- CHANGELOG.md | 4 +++ PROJECT_BOARD.md | 3 -- build.zig | 2 +- build.zig.zon | 2 +- src/app.zig | 39 ++++++++++++++------- src/drawer.zig | 77 ++++++++++++++++++++++++++---------------- src/event_handlers.zig | 6 ++-- src/events.zig | 6 ++-- 8 files changed, 87 insertions(+), 52 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 444c8cd..6f239ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 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 + ## v1.0.1 (2025-04-14) - fix(errors): Ensure logged enums are wrapped in `@tagName()` for readability. diff --git a/PROJECT_BOARD.md b/PROJECT_BOARD.md index 94cf6a3..794b132 100644 --- a/PROJECT_BOARD.md +++ b/PROJECT_BOARD.md @@ -6,7 +6,4 @@ Key: - `[x]` Done ## Backlog -- [ ] Improve image reading. - Current reading can be slow which pauses users movement if they are simply - scrolling past. - [ ] Keybind to unzip archives. diff --git a/build.zig b/build.zig index 95a583b..1ec0344 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 = 0, .patch = 1 }; +const version = std.SemanticVersion{ .major = 1, .minor = 1, .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 3f1897e..28c4dbf 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,7 +1,7 @@ .{ .name = .jido, .fingerprint = 0xee45eabe36cafb57, - .version = "1.0.1", + .version = "1.1.0", .minimum_zig_version = "0.14.0", .dependencies = .{ diff --git a/src/app.zig b/src/app.zig index fcb427b..85ecacd 100644 --- a/src/app.zig +++ b/src/app.zig @@ -73,6 +73,7 @@ pub const Action = union(enum) { }; pub const Event = union(enum) { + image_ready, key_press: Key, winsize: vaxis.Winsize, }; @@ -85,6 +86,7 @@ alloc: std.mem.Allocator, should_quit: bool, vx: vaxis.Vaxis = undefined, tty: vaxis.Tty = undefined, +loop: vaxis.Loop(Event) = undefined, state: State = .normal, actions: CircStack(Action, actions_len), command_history: CommandHistory = CommandHistory{}, @@ -99,9 +101,14 @@ text_input: vaxis.widgets.TextInput, text_input_buf: [std.fs.max_path_bytes]u8 = undefined, yanked: ?struct { dir: []const u8, entry: std.fs.Dir.Entry } = null, -image: ?vaxis.Image = null, last_known_height: usize, +image: struct { + mutex: std.Thread.Mutex = .{}, + data: ?vaxis.zigimg.Image = null, + path: ?[]const u8 = null, +} = .{}, + pub fn init(alloc: std.mem.Allocator) !App { var vx = try vaxis.init(alloc, .{ .kitty_keyboard_flags = .{ @@ -116,7 +123,7 @@ pub fn init(alloc: std.mem.Allocator) !App { var help_menu = List([]const u8).init(alloc); try help_menu.fromArray(&help_menu_items); - return App{ + var app: App = .{ .alloc = alloc, .should_quit = false, .vx = vx, @@ -127,6 +134,13 @@ pub fn init(alloc: std.mem.Allocator) !App { .actions = CircStack(Action, actions_len).init(), .last_known_height = vx.window().height, }; + + app.loop = vaxis.Loop(Event){ + .vaxis = &app.vx, + .tty = &app.tty, + }; + + return app; } pub fn deinit(self: *App) void { @@ -157,6 +171,11 @@ 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(); + } } pub fn inputToSlice(self: *App) []const u8 { @@ -176,20 +195,16 @@ pub fn repopulateDirectory(self: *App, fuzzy: []const u8) error{OutOfMemory}!voi pub fn run(self: *App) !void { try self.repopulateDirectory(""); - - var loop: vaxis.Loop(Event) = .{ - .vaxis = &self.vx, - .tty = &self.tty, - }; - try loop.start(); - defer loop.stop(); + try self.loop.start(); + defer self.loop.stop(); try self.vx.enterAltScreen(self.tty.anyWriter()); try self.vx.queryTerminal(self.tty.anyWriter(), 1 * std.time.ns_per_s); + self.vx.caps.kitty_graphics = true; while (!self.should_quit) { - loop.pollEvent(); - while (loop.tryEvent()) |event| { + self.loop.pollEvent(); + while (self.loop.tryEvent()) |event| { // Global keybinds. switch (event) { .key_press => |key| { @@ -232,7 +247,7 @@ pub fn run(self: *App) !void { // State specific keybinds. switch (self.state) { .normal => { - try EventHandlers.handleNormalEvent(self, event, &loop); + try EventHandlers.handleNormalEvent(self, event); }, .help_menu => { try EventHandlers.handleHelpMenuEvent(self, event); diff --git a/src/drawer.zig b/src/drawer.zig index d23cf4e..36ecc20 100644 --- a/src/drawer.zig +++ b/src/drawer.zig @@ -197,39 +197,37 @@ fn drawFilePreview( } if (!match) break :unsupported; - if (std.mem.eql(u8, self.last_item_path, self.current_item_path)) break :unsupported; + { + app.image.mutex.lock(); + defer app.image.mutex.unlock(); + + if (std.mem.eql(u8, self.current_item_path, app.image.path orelse "")) { + if (app.image.data == null) break :unsupported; + + if (app.vx.transmitImage(app.alloc, app.tty.anyWriter(), &app.image.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); + 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." }, + }, .{}); + }; + } else |_| { + break :unsupported; + } - var image = vaxis.zigimg.Image.fromFilePath( - app.alloc, - self.current_item_path, - ) catch { - break :unsupported; - }; - defer image.deinit(); - - if (app.vx.transmitImage(app.alloc, app.tty.anyWriter(), &image, .rgba)) |img| { - app.image = img; - } else |_| { - if (app.image) |img| { - app.vx.freeImage(app.tty.anyWriter(), img.id); + break :file; } - app.image = null; - break :unsupported; - } - if (app.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." }, - }, .{}); - - break :file; - }; + 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(); } break :file; @@ -606,3 +604,22 @@ fn drawNotification( .style = config.styles.notification.box, }, .{ .wrap = .word }); } + +fn loadImage(app: *App, path: []const u8) error{ Unsupported, OutOfMemory }!void { + const image = vaxis.zigimg.Image.fromFilePath(app.alloc, path) catch { + return error.Unsupported; + }; + + app.image.mutex.lock(); + if (app.image.data) |data| { + var img_data = data; + img_data.deinit(); + } + app.image.data = image; + + if (app.image.path) |p| app.alloc.free(p); + app.image.path = path; + app.image.mutex.unlock(); + + app.loop.postEvent(.image_ready); +} diff --git a/src/event_handlers.zig b/src/event_handlers.zig index b565caa..b8df2e2 100644 --- a/src/event_handlers.zig +++ b/src/event_handlers.zig @@ -12,7 +12,6 @@ const events = @import("./events.zig"); pub fn handleNormalEvent( app: *App, event: App.Event, - loop: *vaxis.Loop(App.Event), ) !void { switch (event) { .key_press => |key| { @@ -82,7 +81,7 @@ pub fn handleNormalEvent( } else { switch (key.codepoint) { '-', 'h', Key.left => try events.traverseLeft(app), - Key.enter, 'l', Key.right => try events.traverseRight(app, loop), + Key.enter, 'l', Key.right => try events.traverseRight(app), 'j', Key.down => app.directories.entries.next(), 'k', Key.up => app.directories.entries.previous(), 'u' => try events.undo(app), @@ -90,6 +89,7 @@ pub fn handleNormalEvent( } } }, + .image_ready => {}, .winsize => |ws| try app.vx.resize(app.alloc, app.tty.anyWriter(), ws), } } @@ -239,6 +239,7 @@ pub fn handleInputEvent(app: *App, event: App.Event) !void { }, } }, + .image_ready => {}, .winsize => |ws| try app.vx.resize(app.alloc, app.tty.anyWriter(), ws), } } @@ -253,6 +254,7 @@ pub fn handleHelpMenuEvent(app: *App, event: App.Event) !void { else => {}, } }, + .image_ready => {}, .winsize => |ws| try app.vx.resize(app.alloc, app.tty.anyWriter(), ws), } } diff --git a/src/events.zig b/src/events.zig index e898cad..245ecb1 100644 --- a/src/events.zig +++ b/src/events.zig @@ -391,7 +391,7 @@ pub fn traverseLeft(app: *App) error{OutOfMemory}!void { } } -pub fn traverseRight(app: *App, loop: *vaxis.Loop(App.Event)) !void { +pub fn traverseRight(app: *App) !void { var message: ?[]const u8 = null; defer if (message) |msg| app.alloc.free(msg); @@ -421,7 +421,7 @@ pub fn traverseRight(app: *App, loop: *vaxis.Loop(App.Event)) !void { if (environment.getEditor()) |editor| { try app.vx.exitAltScreen(app.tty.anyWriter()); try app.vx.resetState(app.tty.anyWriter()); - loop.stop(); + app.loop.stop(); environment.openFile(app.alloc, app.directories.dir, entry.name, editor) catch |err| { message = try std.fmt.allocPrint(app.alloc, "Failed to open file '{s}' - {}.", .{ entry.name, err }); @@ -429,7 +429,7 @@ pub fn traverseRight(app: *App, loop: *vaxis.Loop(App.Event)) !void { if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {}; }; - try loop.start(); + try app.loop.start(); try app.vx.enterAltScreen(app.tty.anyWriter()); try app.vx.enableDetectedFeatures(app.tty.anyWriter()); app.vx.queueRefresh(); -- 2.51.2