地圖 (Jido) is a lightweight Unix TUI file explorer designed for speed and simplicity.

feat: start architecture rework

This is the first change in fixing the rendering architecture. File
handling has been moved to the event handler with the work for
the preview cache started.

+217 -97
+31
src/app.zig
··· 16 const Image = @import("./image.zig"); 17 const List = @import("./list.zig").List; 18 const Notification = @import("./notification.zig"); 19 20 const config = &@import("./config.zig").config; 21 const help_menu_items = [_][]const u8{ ··· 113 yanked: ?struct { dir: []const u8, entry: std.fs.Dir.Entry } = null, 114 last_known_height: usize, 115 116 images: Image.Cache, 117 118 pub fn init(alloc: std.mem.Allocator, entry_dir: ?[]const u8) !App { 119 var vx = try vaxis.init(alloc, .{ ··· 139 .actions = CircStack(Action, actions_len).init(), 140 .last_known_height = vx.window().height, 141 .images = .{ .cache = .init(alloc) }, 142 }; 143 app.tty = try vaxis.Tty.init(&app.tty_buffer); 144 app.loop = vaxis.Loop(Event){ ··· 184 img.value_ptr.deinit(self.alloc, self.vx, &self.tty); 185 } 186 self.images.cache.deinit(); 187 } 188 189 /// Reads the current text input without consuming it. ··· 227 while (!self.should_quit) { 228 self.loop.pollEvent(); 229 while (self.loop.tryEvent()) |event| { 230 // Global keybinds. 231 try EventHandlers.handleGlobalEvent(self, event); 232
··· 16 const Image = @import("./image.zig"); 17 const List = @import("./list.zig").List; 18 const Notification = @import("./notification.zig"); 19 + const Preview = @import("./preview.zig"); 20 21 const config = &@import("./config.zig").config; 22 const help_menu_items = [_][]const u8{ ··· 114 yanked: ?struct { dir: []const u8, entry: std.fs.Dir.Entry } = null, 115 last_known_height: usize, 116 117 + // Used to detect whether to re-render an image. 118 + current_item_path_buf: [std.fs.max_path_bytes]u8 = undefined, 119 + current_item_path: []u8 = "", 120 + last_item_path_buf: [std.fs.max_path_bytes]u8 = undefined, 121 + last_item_path: []u8 = "", 122 + 123 images: Image.Cache, 124 + preview_cache: Preview.PreviewCache, 125 126 pub fn init(alloc: std.mem.Allocator, entry_dir: ?[]const u8) !App { 127 var vx = try vaxis.init(alloc, .{ ··· 147 .actions = CircStack(Action, actions_len).init(), 148 .last_known_height = vx.window().height, 149 .images = .{ .cache = .init(alloc) }, 150 + .preview_cache = Preview.PreviewCache.init(alloc), 151 }; 152 app.tty = try vaxis.Tty.init(&app.tty_buffer); 153 app.loop = vaxis.Loop(Event){ ··· 193 img.value_ptr.deinit(self.alloc, self.vx, &self.tty); 194 } 195 self.images.cache.deinit(); 196 + self.preview_cache.deinit(); 197 } 198 199 /// Reads the current text input without consuming it. ··· 237 while (!self.should_quit) { 238 self.loop.pollEvent(); 239 while (self.loop.tryEvent()) |event| { 240 + if (self.directories.getSelected()) |entry| err: { 241 + @memcpy(&self.last_item_path_buf, &self.current_item_path_buf); 242 + self.last_item_path = self.last_item_path_buf[0..self.current_item_path.len]; 243 + self.current_item_path = try std.fmt.bufPrint( 244 + &self.current_item_path_buf, 245 + "{s}/{s}", 246 + .{ self.directories.fullPath(".") catch { 247 + const message = try std.fmt.allocPrint(self.alloc, "Can not display file - unable to retrieve directory path.", .{}); 248 + defer self.alloc.free(message); 249 + self.notification.write(message, .err) catch {}; 250 + if (self.file_logger) |file_logger| file_logger.write(message, .err) catch {}; 251 + break :err; 252 + }, entry.?.name }, 253 + ); 254 + } else |err| { 255 + const message = try std.fmt.allocPrint(self.alloc, "Can not display file - {}", .{err}); 256 + defer self.alloc.free(message); 257 + self.notification.write(message, .err) catch {}; 258 + if (self.file_logger) |file_logger| file_logger.write(message, .err) catch {}; 259 + } 260 + 261 // Global keybinds. 262 try EventHandlers.handleGlobalEvent(self, event); 263
+5 -1
src/directories.zig
··· 14 alloc: std.mem.Allocator, 15 dir: std.fs.Dir, 16 path_buf: [std.fs.max_path_bytes]u8 = undefined, 17 - file_contents: [4096]u8 = undefined, 18 pdf_contents: ?[]u8 = null, 19 entries: List(std.fs.Dir.Entry), 20 history: CircStack(usize, history_len),
··· 14 alloc: std.mem.Allocator, 15 dir: std.fs.Dir, 16 path_buf: [std.fs.max_path_bytes]u8 = undefined, 17 + file: struct { 18 + handle: ?std.fs.File = null, 19 + data: [4096]u8 = undefined, 20 + bytes_read: usize = 0, 21 + } = .{}, 22 pdf_contents: ?[]u8 = null, 23 entries: List(std.fs.Dir.Entry), 24 history: CircStack(usize, history_len),
+29 -91
src/drawer.zig
··· 19 const top_div: u16 = 1; 20 const info_div: u16 = 1; 21 22 - // Used to detect whether to re-render an image. 23 - current_item_path_buf: [std.fs.max_path_bytes]u8 = undefined, 24 - current_item_path: []u8 = "", 25 - last_item_path_buf: [std.fs.max_path_bytes]u8 = undefined, 26 - last_item_path: []u8 = "", 27 file_info_buf: [std.fs.max_path_bytes]u8 = undefined, 28 file_name_buf: [std.fs.max_path_bytes + 2]u8 = undefined, // +2 to accomodate for [<file_name>] 29 git_branch: [1024]u8 = undefined, ··· 64 65 if (config.preview_file) { 66 const file_name_bar = try self.drawFileName(&app.directories, win); 67 - try self.drawFilePreview(app, win, file_name_bar); 68 } 69 70 const input = app.readInput(); ··· 98 } 99 100 fn drawFilePreview( 101 - self: *Drawer, 102 app: *App, 103 win: vaxis.Window, 104 file_name_win: vaxis.Window, ··· 119 if (entry) |e| break :lbl e else return; 120 }; 121 122 - @memcpy(&self.last_item_path_buf, &self.current_item_path_buf); 123 - self.last_item_path = self.last_item_path_buf[0..self.current_item_path.len]; 124 - self.current_item_path = try std.fmt.bufPrint( 125 - &self.current_item_path_buf, 126 - "{s}/{s}", 127 - .{ app.directories.fullPath(".") catch { 128 - const message = try std.fmt.allocPrint(app.alloc, "Can not display file - unable to retrieve directory path.", .{}); 129 - defer app.alloc.free(message); 130 - app.notification.write(message, .err) catch {}; 131 - if (app.file_logger) |file_logger| file_logger.write(message, .err) catch {}; 132 - 133 - _ = preview_win.print(&.{ 134 - .{ .text = "Can not display file - unable to retrieve directory path. No preview available." }, 135 - }, .{}); 136 - return; 137 - }, entry.name }, 138 - ); 139 - 140 switch (entry.kind) { 141 .directory => { 142 - app.directories.clearChildEntries(); 143 - app.directories.populateChildEntries(entry.name) catch |err| { 144 - const message = try std.fmt.allocPrint(app.alloc, "Failed to populate child directory entries - {}.", .{err}); 145 - defer app.alloc.free(message); 146 - app.notification.write(message, .err) catch {}; 147 - if (app.file_logger) |file_logger| file_logger.write(message, .err) catch {}; 148 - 149 - _ = preview_win.print(&.{ 150 - .{ .text = "Failed to populate child directory entries. No preview available." }, 151 - }, .{}); 152 - 153 - return; 154 - }; 155 - 156 for (app.directories.child_entries.all(), 0..) |item, i| { 157 if (std.mem.startsWith(u8, item, ".") and config.show_hidden == false) { 158 continue; ··· 164 } 165 }, 166 .file => file: { 167 - var file = app.directories.dir.openFile( 168 - entry.name, 169 - .{ .mode = .read_only }, 170 - ) catch |err| { 171 - const message = try std.fmt.allocPrint(app.alloc, "Failed to open file - {}.", .{err}); 172 - defer app.alloc.free(message); 173 - app.notification.write(message, .err) catch {}; 174 - if (app.file_logger) |file_logger| file_logger.write(message, .err) catch {}; 175 - 176 - _ = preview_win.print(&.{ 177 - .{ .text = "Failed to open file. No preview available." }, 178 - }, .{}); 179 - 180 - break :file; 181 - }; 182 - defer file.close(); 183 - const bytes = file.readAll(&app.directories.file_contents) catch |err| { 184 - const message = try std.fmt.allocPrint(app.alloc, "Failed to read file contents - {}.", .{err}); 185 - defer app.alloc.free(message); 186 - app.notification.write(message, .err) catch {}; 187 - if (app.file_logger) |file_logger| file_logger.write(message, .err) catch {}; 188 - 189 - _ = preview_win.print(&.{ 190 - .{ .text = "Failed to read file contents. No preview available." }, 191 - }, .{}); 192 - 193 - break :file; 194 - }; 195 - 196 // Handle image. 197 if (config.show_images == true) unsupported: { 198 var match = false; ··· 205 app.images.mutex.lock(); 206 defer app.images.mutex.unlock(); 207 208 - if (app.images.cache.getPtr(self.current_item_path)) |cache_entry| { 209 if (cache_entry.status == .processing) { 210 _ = preview_win.print(&.{ 211 .{ .text = "Image still processing." }, ··· 235 }; 236 } else { 237 if (cache_entry.data == null) { 238 - const path = try app.alloc.dupe(u8, self.current_item_path); 239 Image.processImage(app.alloc, app, path) catch { 240 app.alloc.free(path); 241 break :unsupported; ··· 275 .{ .text = "Processing image." }, 276 }, .{}); 277 278 - const path = try app.alloc.dupe(u8, self.current_item_path); 279 Image.processImage(app.alloc, app, path) catch { 280 app.alloc.free(path); 281 break :unsupported; ··· 295 "0", 296 "-l", 297 "5", 298 - self.current_item_path, 299 "-", 300 }, 301 .cwd_dir = app.directories.dir, ··· 331 app.archive_files = null; 332 } 333 334 - app.archive_files = Archive.listArchiveContents( 335 - app.alloc, 336 - file, 337 - archive_type, 338 - config.archive_traversal_limit, 339 - ) catch |err| { 340 - const message = try std.fmt.allocPrint(app.alloc, "Failed to read archive: {s}", .{@errorName(err)}); 341 - defer app.alloc.free(message); 342 - app.notification.write(message, .err) catch {}; 343 - if (app.file_logger) |file_logger| file_logger.write(message, .err) catch {}; 344 - _ = preview_win.print(&.{.{ .text = "Failed to read archive." }}, .{}); 345 - break :file; 346 - }; 347 348 if (config.sort_dirs) { 349 std.mem.sort([]const u8, app.archive_files.?.entries.items, {}, sort.string); ··· 359 } 360 361 // Handle utf-8. 362 - if (std.unicode.utf8ValidateSlice(app.directories.file_contents[0..bytes])) { 363 - _ = preview_win.print(&.{ 364 - .{ .text = app.directories.file_contents[0..bytes] }, 365 - }, .{}); 366 - break :file; 367 } 368 369 // Fallback to no preview. ··· 371 }, 372 else => { 373 _ = preview_win.print(&.{ 374 - vaxis.Segment{ .text = self.current_item_path }, 375 }, .{}); 376 }, 377 }
··· 19 const top_div: u16 = 1; 20 const info_div: u16 = 1; 21 22 file_info_buf: [std.fs.max_path_bytes]u8 = undefined, 23 file_name_buf: [std.fs.max_path_bytes + 2]u8 = undefined, // +2 to accomodate for [<file_name>] 24 git_branch: [1024]u8 = undefined, ··· 59 60 if (config.preview_file) { 61 const file_name_bar = try self.drawFileName(&app.directories, win); 62 + try drawFilePreview(app, win, file_name_bar); 63 } 64 65 const input = app.readInput(); ··· 93 } 94 95 fn drawFilePreview( 96 app: *App, 97 win: vaxis.Window, 98 file_name_win: vaxis.Window, ··· 113 if (entry) |e| break :lbl e else return; 114 }; 115 116 switch (entry.kind) { 117 .directory => { 118 for (app.directories.child_entries.all(), 0..) |item, i| { 119 if (std.mem.startsWith(u8, item, ".") and config.show_hidden == false) { 120 continue; ··· 126 } 127 }, 128 .file => file: { 129 // Handle image. 130 if (config.show_images == true) unsupported: { 131 var match = false; ··· 138 app.images.mutex.lock(); 139 defer app.images.mutex.unlock(); 140 141 + if (app.images.cache.getPtr(app.current_item_path)) |cache_entry| { 142 if (cache_entry.status == .processing) { 143 _ = preview_win.print(&.{ 144 .{ .text = "Image still processing." }, ··· 168 }; 169 } else { 170 if (cache_entry.data == null) { 171 + const path = try app.alloc.dupe(u8, app.current_item_path); 172 Image.processImage(app.alloc, app, path) catch { 173 app.alloc.free(path); 174 break :unsupported; ··· 208 .{ .text = "Processing image." }, 209 }, .{}); 210 211 + const path = try app.alloc.dupe(u8, app.current_item_path); 212 Image.processImage(app.alloc, app, path) catch { 213 app.alloc.free(path); 214 break :unsupported; ··· 228 "0", 229 "-l", 230 "5", 231 + app.current_item_path, 232 "-", 233 }, 234 .cwd_dir = app.directories.dir, ··· 264 app.archive_files = null; 265 } 266 267 + if (app.directories.file.handle) |file| { 268 + app.archive_files = Archive.listArchiveContents( 269 + app.alloc, 270 + file, 271 + archive_type, 272 + config.archive_traversal_limit, 273 + ) catch |err| { 274 + const message = try std.fmt.allocPrint(app.alloc, "Failed to read archive: {s}", .{@errorName(err)}); 275 + defer app.alloc.free(message); 276 + app.notification.write(message, .err) catch {}; 277 + if (app.file_logger) |file_logger| file_logger.write(message, .err) catch {}; 278 + _ = preview_win.print(&.{.{ .text = "Failed to read archive." }}, .{}); 279 + break :file; 280 + }; 281 + } 282 283 if (config.sort_dirs) { 284 std.mem.sort([]const u8, app.archive_files.?.entries.items, {}, sort.string); ··· 294 } 295 296 // Handle utf-8. 297 + if (app.directories.file.bytes_read > 0) { 298 + const file_contents = app.directories.file.data[0..app.directories.file.bytes_read]; 299 + if (std.unicode.utf8ValidateSlice(file_contents)) { 300 + _ = preview_win.print(&.{ 301 + .{ .text = file_contents }, 302 + }, .{}); 303 + break :file; 304 + } 305 } 306 307 // Fallback to no preview. ··· 309 }, 310 else => { 311 _ = preview_win.print(&.{ 312 + vaxis.Segment{ .text = app.current_item_path }, 313 }, .{}); 314 }, 315 }
+57 -2
src/event_handlers.zig
··· 11 const events = @import("./events.zig"); 12 13 const config = &@import("./config.zig").config; 14 pub fn handleGlobalEvent( 15 app: *App, 16 event: App.Event, ··· 128 switch (key.codepoint) { 129 '-', 'h', Key.left => try events.traverseLeft(app), 130 Key.enter, 'l', Key.right => try events.traverseRight(app), 131 - 'j', Key.down => app.directories.entries.next(), 132 - 'k', Key.up => app.directories.entries.previous(), 133 'u' => try events.undo(app), 134 else => {}, 135 }
··· 11 const events = @import("./events.zig"); 12 13 const config = &@import("./config.zig").config; 14 + 15 pub fn handleGlobalEvent( 16 app: *App, 17 event: App.Event, ··· 129 switch (key.codepoint) { 130 '-', 'h', Key.left => try events.traverseLeft(app), 131 Key.enter, 'l', Key.right => try events.traverseRight(app), 132 + 'j', 'k', Key.down, Key.up => { 133 + switch (key.codepoint) { 134 + 'j', Key.down => app.directories.entries.next(), 135 + 'k', Key.up => app.directories.entries.previous(), 136 + else => {}, 137 + } 138 + 139 + if (app.directories.entries.len() == 0 or !config.preview_file) return; 140 + const entry = (app.directories.getSelected() catch |err| { 141 + const message = try std.fmt.allocPrint(app.alloc, "Failed to read directory entries - {}.", .{err}); 142 + defer app.alloc.free(message); 143 + app.notification.write(message, .err) catch {}; 144 + if (app.file_logger) |file_logger| file_logger.write(message, .err) catch {}; 145 + return; 146 + }) orelse return; 147 + 148 + switch (entry.kind) { 149 + .directory => { 150 + app.directories.clearChildEntries(); 151 + app.directories.populateChildEntries(entry.name) catch |err| { 152 + const message = try std.fmt.allocPrint(app.alloc, "Failed to read directory entries - {}.", .{err}); 153 + defer app.alloc.free(message); 154 + app.notification.write(message, .err) catch {}; 155 + if (app.file_logger) |file_logger| file_logger.write(message, .err) catch {}; 156 + }; 157 + }, 158 + .file => { 159 + if (!std.mem.eql(u8, app.last_item_path, app.current_item_path)) { 160 + if (app.directories.file.handle) |*previous_file| { 161 + previous_file.close(); 162 + } 163 + 164 + var file = app.directories.dir.openFile( 165 + entry.name, 166 + .{ .mode = .read_only }, 167 + ) catch |err| { 168 + const message = try std.fmt.allocPrint(app.alloc, "Failed to open file - {}.", .{err}); 169 + defer app.alloc.free(message); 170 + app.notification.write("Failed to open file. No preview available.", .err) catch {}; 171 + if (app.file_logger) |file_logger| file_logger.write(message, .err) catch {}; 172 + return; 173 + }; 174 + const bytes = file.readAll(&app.directories.file.data) catch |err| { 175 + const message = try std.fmt.allocPrint(app.alloc, "Failed to read file contents - {}.", .{err}); 176 + defer app.alloc.free(message); 177 + app.notification.write("Failed to read file contents. No preview available.", .err) catch {}; 178 + if (app.file_logger) |file_logger| file_logger.write(message, .err) catch {}; 179 + return; 180 + }; 181 + app.directories.file.handle = file; 182 + app.directories.file.bytes_read = bytes; 183 + } 184 + }, 185 + else => {}, 186 + } 187 + }, 188 'u' => try events.undo(app), 189 else => {}, 190 }
+3 -3
src/notification.zig
··· 30 self.timer = std.time.timestamp(); 31 self.style = style; 32 33 - if (self.loop) |loop| { 34 - loop.postEvent(.notification); 35 - } 36 } 37 38 pub fn reset(self: *Self) void {
··· 30 self.timer = std.time.timestamp(); 31 self.style = style; 32 33 + // if (self.loop) |loop| { 34 + // loop.postEvent(.notification); 35 + // } 36 } 37 38 pub fn reset(self: *Self) void {
+92
src/preview.zig
···
··· 1 + const std = @import("std"); 2 + 3 + pub const PreviewType = enum { 4 + none, 5 + text, 6 + image, 7 + pdf, 8 + archive, 9 + directory, 10 + }; 11 + 12 + pub const PreviewData = union(PreviewType) { 13 + none: void, 14 + text: []const u8, 15 + image: ImageInfo, 16 + pdf: []const u8, 17 + archive: std.ArrayList([]const u8), 18 + directory: std.ArrayList([]const u8), 19 + }; 20 + 21 + pub const ImageInfo = struct { 22 + cache_path: []const u8, 23 + }; 24 + 25 + pub const CacheEntry = struct { 26 + file_path: []const u8, 27 + preview: PreviewData, 28 + is_valid: bool, 29 + 30 + pub fn deinit(self: *CacheEntry, alloc: std.mem.Allocator) void { 31 + alloc.free(self.file_path); 32 + switch (self.preview) { 33 + .text, .pdf => |data| alloc.free(data), 34 + .archive, .directory => |*list| { 35 + for (list.items) |item| alloc.free(item); 36 + list.deinit(alloc); 37 + }, 38 + .image => |img| alloc.free(img.cache_path), 39 + .none => {}, 40 + } 41 + } 42 + }; 43 + 44 + pub const PreviewCache = struct { 45 + alloc: std.mem.Allocator, 46 + current: ?CacheEntry, 47 + 48 + pub fn init(alloc: std.mem.Allocator) PreviewCache { 49 + return .{ 50 + .alloc = alloc, 51 + .current = null, 52 + }; 53 + } 54 + 55 + pub fn deinit(self: *PreviewCache) void { 56 + if (self.current) |*entry| { 57 + entry.deinit(self.alloc); 58 + } 59 + } 60 + 61 + pub fn invalidate(self: *PreviewCache) void { 62 + if (self.current) |*entry| { 63 + entry.is_valid = false; 64 + } 65 + } 66 + 67 + pub fn clear(self: *PreviewCache) void { 68 + if (self.current) |*entry| { 69 + entry.deinit(self.alloc); 70 + } 71 + self.current = null; 72 + } 73 + 74 + pub fn get(self: *PreviewCache, path: []const u8) ?*const PreviewData { 75 + if (self.current) |*entry| { 76 + if (entry.is_valid and std.mem.eql(u8, entry.file_path, path)) { 77 + return &entry.preview; 78 + } 79 + } 80 + return null; 81 + } 82 + 83 + pub fn set(self: *PreviewCache, path: []const u8, preview: PreviewData) !void { 84 + self.clear(); 85 + 86 + self.current = .{ 87 + .file_path = try self.alloc.dupe(u8, path), 88 + .preview = preview, 89 + .is_valid = true, 90 + }; 91 + } 92 + };