diff --git a/src/app.zig b/src/app.zig index 773612f..ce7be6d 100644 --- a/src/app.zig +++ b/src/app.zig @@ -16,6 +16,7 @@ const FileLogger = @import("./file_logger.zig"); const Image = @import("./image.zig"); const List = @import("./list.zig").List; const Notification = @import("./notification.zig"); +const Preview = @import("./preview.zig"); const config = &@import("./config.zig").config; const help_menu_items = [_][]const u8{ @@ -113,7 +114,14 @@ 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, +// Used to detect whether to re-render an image. +current_item_path_buf: [std.fs.max_path_bytes]u8 = undefined, +current_item_path: []u8 = "", +last_item_path_buf: [std.fs.max_path_bytes]u8 = undefined, +last_item_path: []u8 = "", + images: Image.Cache, +preview_cache: Preview.PreviewCache, pub fn init(alloc: std.mem.Allocator, entry_dir: ?[]const u8) !App { var vx = try vaxis.init(alloc, .{ @@ -139,6 +147,7 @@ pub fn init(alloc: std.mem.Allocator, entry_dir: ?[]const u8) !App { .actions = CircStack(Action, actions_len).init(), .last_known_height = vx.window().height, .images = .{ .cache = .init(alloc) }, + .preview_cache = Preview.PreviewCache.init(alloc), }; app.tty = try vaxis.Tty.init(&app.tty_buffer); app.loop = vaxis.Loop(Event){ @@ -184,6 +193,7 @@ pub fn deinit(self: *App) void { img.value_ptr.deinit(self.alloc, self.vx, &self.tty); } self.images.cache.deinit(); + self.preview_cache.deinit(); } /// Reads the current text input without consuming it. @@ -227,6 +237,27 @@ pub fn run(self: *App) !void { while (!self.should_quit) { self.loop.pollEvent(); while (self.loop.tryEvent()) |event| { + if (self.directories.getSelected()) |entry| err: { + @memcpy(&self.last_item_path_buf, &self.current_item_path_buf); + self.last_item_path = self.last_item_path_buf[0..self.current_item_path.len]; + self.current_item_path = try std.fmt.bufPrint( + &self.current_item_path_buf, + "{s}/{s}", + .{ self.directories.fullPath(".") catch { + const message = try std.fmt.allocPrint(self.alloc, "Can not display file - unable to retrieve directory path.", .{}); + defer self.alloc.free(message); + self.notification.write(message, .err) catch {}; + if (self.file_logger) |file_logger| file_logger.write(message, .err) catch {}; + break :err; + }, entry.?.name }, + ); + } else |err| { + const message = try std.fmt.allocPrint(self.alloc, "Can not display file - {}", .{err}); + defer self.alloc.free(message); + self.notification.write(message, .err) catch {}; + if (self.file_logger) |file_logger| file_logger.write(message, .err) catch {}; + } + // Global keybinds. try EventHandlers.handleGlobalEvent(self, event); diff --git a/src/directories.zig b/src/directories.zig index 9610ffe..507dfed 100644 --- a/src/directories.zig +++ b/src/directories.zig @@ -14,7 +14,11 @@ const Self = @This(); alloc: std.mem.Allocator, dir: std.fs.Dir, path_buf: [std.fs.max_path_bytes]u8 = undefined, -file_contents: [4096]u8 = undefined, +file: struct { + handle: ?std.fs.File = null, + data: [4096]u8 = undefined, + bytes_read: usize = 0, +} = .{}, pdf_contents: ?[]u8 = null, entries: List(std.fs.Dir.Entry), history: CircStack(usize, history_len), diff --git a/src/drawer.zig b/src/drawer.zig index 72b677c..81e96ed 100644 --- a/src/drawer.zig +++ b/src/drawer.zig @@ -19,11 +19,6 @@ const Drawer = @This(); const top_div: u16 = 1; const info_div: u16 = 1; -// Used to detect whether to re-render an image. -current_item_path_buf: [std.fs.max_path_bytes]u8 = undefined, -current_item_path: []u8 = "", -last_item_path_buf: [std.fs.max_path_bytes]u8 = undefined, -last_item_path: []u8 = "", file_info_buf: [std.fs.max_path_bytes]u8 = undefined, file_name_buf: [std.fs.max_path_bytes + 2]u8 = undefined, // +2 to accomodate for [] git_branch: [1024]u8 = undefined, @@ -64,7 +59,7 @@ pub fn draw(self: *Drawer, app: *App) error{ OutOfMemory, NoSpaceLeft }!void { if (config.preview_file) { const file_name_bar = try self.drawFileName(&app.directories, win); - try self.drawFilePreview(app, win, file_name_bar); + try drawFilePreview(app, win, file_name_bar); } const input = app.readInput(); @@ -98,7 +93,6 @@ fn drawFileName( } fn drawFilePreview( - self: *Drawer, app: *App, win: vaxis.Window, file_name_win: vaxis.Window, @@ -119,40 +113,8 @@ fn drawFilePreview( if (entry) |e| break :lbl e else return; }; - @memcpy(&self.last_item_path_buf, &self.current_item_path_buf); - self.last_item_path = self.last_item_path_buf[0..self.current_item_path.len]; - self.current_item_path = try std.fmt.bufPrint( - &self.current_item_path_buf, - "{s}/{s}", - .{ app.directories.fullPath(".") catch { - const message = try std.fmt.allocPrint(app.alloc, "Can not display file - unable to retrieve directory path.", .{}); - 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 = "Can not display file - unable to retrieve directory path. No preview available." }, - }, .{}); - return; - }, entry.name }, - ); - switch (entry.kind) { .directory => { - app.directories.clearChildEntries(); - app.directories.populateChildEntries(entry.name) catch |err| { - const message = try std.fmt.allocPrint(app.alloc, "Failed to populate child directory entries - {}.", .{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 populate child directory entries. No preview available." }, - }, .{}); - - return; - }; - for (app.directories.child_entries.all(), 0..) |item, i| { if (std.mem.startsWith(u8, item, ".") and config.show_hidden == false) { continue; @@ -164,35 +126,6 @@ fn drawFilePreview( } }, .file => file: { - var file = app.directories.dir.openFile( - entry.name, - .{ .mode = .read_only }, - ) catch |err| { - const message = try std.fmt.allocPrint(app.alloc, "Failed to open file - {}.", .{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 open file. No preview available." }, - }, .{}); - - break :file; - }; - defer file.close(); - const bytes = file.readAll(&app.directories.file_contents) catch |err| { - const message = try std.fmt.allocPrint(app.alloc, "Failed to read file contents - {}.", .{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 read file contents. No preview available." }, - }, .{}); - - break :file; - }; - // Handle image. if (config.show_images == true) unsupported: { var match = false; @@ -205,7 +138,7 @@ fn drawFilePreview( app.images.mutex.lock(); defer app.images.mutex.unlock(); - if (app.images.cache.getPtr(self.current_item_path)) |cache_entry| { + if (app.images.cache.getPtr(app.current_item_path)) |cache_entry| { if (cache_entry.status == .processing) { _ = preview_win.print(&.{ .{ .text = "Image still processing." }, @@ -235,7 +168,7 @@ fn drawFilePreview( }; } else { if (cache_entry.data == null) { - const path = try app.alloc.dupe(u8, self.current_item_path); + const path = try app.alloc.dupe(u8, app.current_item_path); Image.processImage(app.alloc, app, path) catch { app.alloc.free(path); break :unsupported; @@ -275,7 +208,7 @@ fn drawFilePreview( .{ .text = "Processing image." }, }, .{}); - const path = try app.alloc.dupe(u8, self.current_item_path); + const path = try app.alloc.dupe(u8, app.current_item_path); Image.processImage(app.alloc, app, path) catch { app.alloc.free(path); break :unsupported; @@ -295,7 +228,7 @@ fn drawFilePreview( "0", "-l", "5", - self.current_item_path, + app.current_item_path, "-", }, .cwd_dir = app.directories.dir, @@ -331,19 +264,21 @@ fn drawFilePreview( app.archive_files = null; } - app.archive_files = Archive.listArchiveContents( - app.alloc, - file, - archive_type, - config.archive_traversal_limit, - ) catch |err| { - const message = try std.fmt.allocPrint(app.alloc, "Failed to read archive: {s}", .{@errorName(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 read archive." }}, .{}); - break :file; - }; + if (app.directories.file.handle) |file| { + app.archive_files = Archive.listArchiveContents( + app.alloc, + file, + archive_type, + config.archive_traversal_limit, + ) catch |err| { + const message = try std.fmt.allocPrint(app.alloc, "Failed to read archive: {s}", .{@errorName(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 read archive." }}, .{}); + break :file; + }; + } if (config.sort_dirs) { std.mem.sort([]const u8, app.archive_files.?.entries.items, {}, sort.string); @@ -359,11 +294,14 @@ fn drawFilePreview( } // Handle utf-8. - if (std.unicode.utf8ValidateSlice(app.directories.file_contents[0..bytes])) { - _ = preview_win.print(&.{ - .{ .text = app.directories.file_contents[0..bytes] }, - }, .{}); - break :file; + if (app.directories.file.bytes_read > 0) { + const file_contents = app.directories.file.data[0..app.directories.file.bytes_read]; + if (std.unicode.utf8ValidateSlice(file_contents)) { + _ = preview_win.print(&.{ + .{ .text = file_contents }, + }, .{}); + break :file; + } } // Fallback to no preview. @@ -371,7 +309,7 @@ fn drawFilePreview( }, else => { _ = preview_win.print(&.{ - vaxis.Segment{ .text = self.current_item_path }, + vaxis.Segment{ .text = app.current_item_path }, }, .{}); }, } diff --git a/src/event_handlers.zig b/src/event_handlers.zig index 7a0d339..e39f6aa 100644 --- a/src/event_handlers.zig +++ b/src/event_handlers.zig @@ -11,6 +11,7 @@ const environment = @import("./environment.zig"); const events = @import("./events.zig"); const config = &@import("./config.zig").config; + pub fn handleGlobalEvent( app: *App, event: App.Event, @@ -128,8 +129,62 @@ pub fn handleNormalEvent( switch (key.codepoint) { '-', 'h', Key.left => try events.traverseLeft(app), Key.enter, 'l', Key.right => try events.traverseRight(app), - 'j', Key.down => app.directories.entries.next(), - 'k', Key.up => app.directories.entries.previous(), + 'j', 'k', Key.down, Key.up => { + switch (key.codepoint) { + 'j', Key.down => app.directories.entries.next(), + 'k', Key.up => app.directories.entries.previous(), + else => {}, + } + + if (app.directories.entries.len() == 0 or !config.preview_file) return; + const entry = (app.directories.getSelected() catch |err| { + const message = try std.fmt.allocPrint(app.alloc, "Failed to read directory entries - {}.", .{err}); + defer app.alloc.free(message); + app.notification.write(message, .err) catch {}; + if (app.file_logger) |file_logger| file_logger.write(message, .err) catch {}; + return; + }) orelse return; + + switch (entry.kind) { + .directory => { + app.directories.clearChildEntries(); + app.directories.populateChildEntries(entry.name) catch |err| { + const message = try std.fmt.allocPrint(app.alloc, "Failed to read directory entries - {}.", .{err}); + defer app.alloc.free(message); + app.notification.write(message, .err) catch {}; + if (app.file_logger) |file_logger| file_logger.write(message, .err) catch {}; + }; + }, + .file => { + if (!std.mem.eql(u8, app.last_item_path, app.current_item_path)) { + if (app.directories.file.handle) |*previous_file| { + previous_file.close(); + } + + var file = app.directories.dir.openFile( + entry.name, + .{ .mode = .read_only }, + ) catch |err| { + const message = try std.fmt.allocPrint(app.alloc, "Failed to open file - {}.", .{err}); + defer app.alloc.free(message); + app.notification.write("Failed to open file. No preview available.", .err) catch {}; + if (app.file_logger) |file_logger| file_logger.write(message, .err) catch {}; + return; + }; + const bytes = file.readAll(&app.directories.file.data) catch |err| { + const message = try std.fmt.allocPrint(app.alloc, "Failed to read file contents - {}.", .{err}); + defer app.alloc.free(message); + app.notification.write("Failed to read file contents. No preview available.", .err) catch {}; + if (app.file_logger) |file_logger| file_logger.write(message, .err) catch {}; + return; + }; + app.directories.file.handle = file; + app.directories.file.bytes_read = bytes; + } + }, + else => {}, + } + }, 'u' => try events.undo(app), else => {}, } diff --git a/src/notification.zig b/src/notification.zig index fcec962..ea0cd4b 100644 --- a/src/notification.zig +++ b/src/notification.zig @@ -30,9 +30,9 @@ pub fn write(self: *Self, text: []const u8, style: Style) !void { self.timer = std.time.timestamp(); self.style = style; - if (self.loop) |loop| { - loop.postEvent(.notification); - } + // if (self.loop) |loop| { + // loop.postEvent(.notification); + // } } pub fn reset(self: *Self) void { diff --git a/src/preview.zig b/src/preview.zig new file mode 100644 index 0000000..e51258c --- /dev/null +++ b/src/preview.zig @@ -0,0 +1,92 @@ +const std = @import("std"); + +pub const PreviewType = enum { + none, + text, + image, + pdf, + archive, + directory, +}; + +pub const PreviewData = union(PreviewType) { + none: void, + text: []const u8, + image: ImageInfo, + pdf: []const u8, + archive: std.ArrayList([]const u8), + directory: std.ArrayList([]const u8), +}; + +pub const ImageInfo = struct { + cache_path: []const u8, +}; + +pub const CacheEntry = struct { + file_path: []const u8, + preview: PreviewData, + is_valid: bool, + + pub fn deinit(self: *CacheEntry, alloc: std.mem.Allocator) void { + alloc.free(self.file_path); + switch (self.preview) { + .text, .pdf => |data| alloc.free(data), + .archive, .directory => |*list| { + for (list.items) |item| alloc.free(item); + list.deinit(alloc); + }, + .image => |img| alloc.free(img.cache_path), + .none => {}, + } + } +}; + +pub const PreviewCache = struct { + alloc: std.mem.Allocator, + current: ?CacheEntry, + + pub fn init(alloc: std.mem.Allocator) PreviewCache { + return .{ + .alloc = alloc, + .current = null, + }; + } + + pub fn deinit(self: *PreviewCache) void { + if (self.current) |*entry| { + entry.deinit(self.alloc); + } + } + + pub fn invalidate(self: *PreviewCache) void { + if (self.current) |*entry| { + entry.is_valid = false; + } + } + + pub fn clear(self: *PreviewCache) void { + if (self.current) |*entry| { + entry.deinit(self.alloc); + } + self.current = null; + } + + pub fn get(self: *PreviewCache, path: []const u8) ?*const PreviewData { + if (self.current) |*entry| { + if (entry.is_valid and std.mem.eql(u8, entry.file_path, path)) { + return &entry.preview; + } + } + return null; + } + + pub fn set(self: *PreviewCache, path: []const u8, preview: PreviewData) !void { + self.clear(); + + self.current = .{ + .file_path = try self.alloc.dupe(u8, path), + .preview = preview, + .is_valid = true, + }; + } +};