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

feat: refactor file opening logic outside of the renderer

+235 -299
+37 -7
src/app.zig
··· 216 216 } 217 217 218 218 pub fn repopulateDirectory(self: *App, fuzzy: []const u8) error{OutOfMemory}!void { 219 + // Save current selection name to restore cursor position after repopulation 220 + const prev_name = if (self.directories.getSelected() catch null) |entry| 221 + try self.alloc.dupe(u8, entry.name) 222 + else 223 + null; 224 + defer if (prev_name) |name| self.alloc.free(name); 225 + 219 226 self.directories.clearEntries(); 220 227 self.directories.populateEntries(fuzzy) catch |err| { 221 228 const message = try std.fmt.allocPrint(self.alloc, "Failed to read directory entries - {}.", .{err}); ··· 223 230 self.notification.write(message, .err) catch {}; 224 231 if (self.file_logger) |file_logger| file_logger.write(message, .err) catch {}; 225 232 }; 233 + 234 + // Try to restore cursor to the same file by name 235 + if (prev_name) |name| { 236 + for (self.directories.entries.all(), 0..) |entry, i| { 237 + if (std.mem.eql(u8, entry.name, name)) { 238 + self.directories.entries.selected = i; 239 + break; 240 + } 241 + } 242 + } 243 + 244 + // Revalidate current entry for display 245 + self.preview_cache.invalidate(); 246 + Preview.loadPreviewForCurrentEntry(self) catch |err| { 247 + if (self.file_logger) |file_logger| { 248 + const msg = std.fmt.allocPrint(self.alloc, "Failed to load preview after repopulate: {}", .{err}) catch return; 249 + defer self.alloc.free(msg); 250 + file_logger.write(msg, .err) catch {}; 251 + } 252 + }; 226 253 } 227 254 228 255 pub fn run(self: *App) !void { ··· 237 264 while (!self.should_quit) { 238 265 self.loop.pollEvent(); 239 266 while (self.loop.tryEvent()) |event| { 240 - if (self.directories.getSelected()) |entry| err: { 267 + const selected = self.directories.getSelected() catch |err| err: { 268 + const message = try std.fmt.allocPrint(self.alloc, "Can not display file - {}", .{err}); 269 + defer self.alloc.free(message); 270 + self.notification.write(message, .err) catch {}; 271 + if (self.file_logger) |file_logger| file_logger.write(message, .err) catch {}; 272 + break :err null; 273 + }; 274 + 275 + if (selected) |entry| err: { 241 276 @memcpy(&self.last_item_path_buf, &self.current_item_path_buf); 242 277 self.last_item_path = self.last_item_path_buf[0..self.current_item_path.len]; 243 278 self.current_item_path = try std.fmt.bufPrint( ··· 249 284 self.notification.write(message, .err) catch {}; 250 285 if (self.file_logger) |file_logger| file_logger.write(message, .err) catch {}; 251 286 break :err; 252 - }, entry.?.name }, 287 + }, entry.name }, 253 288 ); 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 289 } 260 290 261 291 // Global keybinds.
+1
src/commands.zig
··· 2 2 3 3 const App = @import("app.zig"); 4 4 const environment = @import("environment.zig"); 5 + const Preview = @import("preview.zig"); 5 6 6 7 const user_config = &@import("./config.zig").config; 7 8
+74 -185
src/drawer.zig
··· 11 11 const Image = @import("./image.zig"); 12 12 const List = @import("./list.zig").List; 13 13 const Notification = @import("./notification.zig"); 14 + const path_utils = @import("./path_utils.zig"); 15 + const Preview = @import("./preview.zig"); 14 16 const sort = @import("./sort.zig"); 15 17 16 18 const config = &@import("./config.zig").config; ··· 113 115 if (entry) |e| break :lbl e else return; 114 116 }; 115 117 116 - switch (entry.kind) { 117 - .directory => { 118 - for (app.directories.child_entries.all(), 0..) |item, i| { 118 + const clean_name = path_utils.getCleanName(entry); 119 + const abs_path = app.directories.fullPath(clean_name) catch { 120 + _ = preview_win.print(&.{.{ .text = "Unable to get file path." }}, .{}); 121 + return; 122 + }; 123 + 124 + const preview_data = app.preview_cache.get(abs_path); 125 + if (preview_data == null) { 126 + _ = preview_win.print(&.{.{ .text = "Loading preview..." }}, .{}); 127 + return; 128 + } 129 + 130 + switch (preview_data.?.*) { 131 + .none => { 132 + _ = preview_win.print(&.{.{ .text = "No preview available." }}, .{}); 133 + }, 134 + .text, .pdf => |text| { 135 + _ = preview_win.print(&.{.{ .text = text }}, .{}); 136 + }, 137 + .directory => |entries| { 138 + for (entries.items, 0..) |item, i| { 119 139 if (std.mem.startsWith(u8, item, ".") and config.show_hidden == false) { 120 140 continue; 121 141 } 122 - if (i > preview_win.height) continue; 142 + if (i >= preview_win.height) break; 123 143 const w = preview_win.child(.{ .y_off = @intCast(i), .height = 1 }); 124 144 w.fill(vaxis.Cell{ .style = config.styles.list_item }); 125 145 _ = w.print(&.{.{ .text = item, .style = config.styles.list_item }}, .{}); 126 146 } 127 147 }, 128 - .file => file: { 129 - // Handle image. 130 - if (config.show_images == true) unsupported: { 131 - var match = false; 132 - inline for (@typeInfo(vaxis.zigimg.Image.Format).@"enum".fields) |field| { 133 - const entry_ext = std.mem.trimLeft(u8, std.fs.path.extension(entry.name), "."); 134 - if (std.mem.eql(u8, entry_ext, field.name)) match = true; 135 - } 136 - if (!match) break :unsupported; 137 - 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." }, 145 - }, .{}); 146 - break :file; 147 - } 148 - 149 - if (cache_entry.status == .failed) { 150 - _ = preview_win.print(&.{ 151 - .{ .text = "Failed to process image." }, 152 - }, .{}); 153 - break :file; 154 - } 155 - 156 - if (cache_entry.image) |img| { 157 - img.draw(preview_win, .{ .scale = .contain }) catch |err| { 158 - const message = try std.fmt.allocPrint(app.alloc, "Failed to draw image to screen - {}.", .{err}); 159 - defer app.alloc.free(message); 160 - app.notification.write(message, .err) catch {}; 161 - if (app.file_logger) |file_logger| file_logger.write(message, .err) catch {}; 162 - 163 - _ = preview_win.print(&.{ 164 - .{ .text = "Failed to draw image to screen. No preview available." }, 165 - }, .{}); 166 - cache_entry.image = null; 167 - break :file; 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; 175 - }; 176 - _ = preview_win.print(&.{ 177 - .{ .text = "Image still processing." }, 178 - }, .{}); 179 - break :file; 180 - } 148 + .archive => |entries| { 149 + for (entries.items, 0..) |item, i| { 150 + if (i >= preview_win.height) break; 151 + const w = preview_win.child(.{ .y_off = @intCast(i), .height = 1 }); 152 + w.fill(vaxis.Cell{ .style = config.styles.list_item }); 153 + _ = w.print(&.{.{ .text = item, .style = config.styles.list_item }}, .{}); 154 + } 155 + }, 156 + .image => |img_info| { 157 + if (!config.show_images) { 158 + _ = preview_win.print(&.{.{ .text = "Image preview disabled." }}, .{}); 159 + return; 160 + } 181 161 182 - if (app.vx.transmitImage(app.alloc, app.tty.writer(), &cache_entry.data.?, .rgba)) |img| { 183 - img.draw(preview_win, .{ .scale = .contain }) catch |err| { 184 - const message = try std.fmt.allocPrint(app.alloc, "Failed to draw image to screen - {}.", .{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 {}; 162 + app.images.mutex.lock(); 163 + defer app.images.mutex.unlock(); 188 164 189 - _ = preview_win.print(&.{ 190 - .{ .text = "Failed to draw image to screen. No preview available." }, 191 - }, .{}); 192 - break :file; 165 + if (app.images.cache.getPtr(img_info.cache_path)) |cache_entry| { 166 + switch (cache_entry.status) { 167 + .processing => { 168 + _ = preview_win.print(&.{.{ .text = "Image still processing..." }}, .{}); 169 + }, 170 + .failed => { 171 + _ = preview_win.print(&.{.{ .text = "Failed to process image." }}, .{}); 172 + }, 173 + .ready => { 174 + if (cache_entry.image) |image| { 175 + image.draw(preview_win, .{ .scale = .contain }) catch { 176 + _ = preview_win.print(&.{.{ .text = "Failed to draw image." }}, .{}); 177 + return; 193 178 }; 194 - cache_entry.image = img; 195 - if (cache_entry.data) |data| { 196 - var d = data; 179 + } else if (cache_entry.data) |*data| { 180 + if (app.vx.transmitImage(app.alloc, app.tty.writer(), data, .rgba)) |image| { 181 + image.draw(preview_win, .{ .scale = .contain }) catch { 182 + _ = preview_win.print(&.{.{ .text = "Failed to draw image." }}, .{}); 183 + return; 184 + }; 185 + cache_entry.image = image; 186 + var d = data.*; 197 187 d.deinit(app.alloc); 188 + cache_entry.data = null; 189 + } else |_| { 190 + _ = preview_win.print(&.{.{ .text = "Failed to transmit image." }}, .{}); 198 191 } 199 - cache_entry.data = null; 200 - } else |_| { 201 - break :unsupported; 192 + } else { 193 + _ = preview_win.print(&.{.{ .text = "Image processing..." }}, .{}); 202 194 } 203 - } 204 - 205 - break :file; 206 - } else { 207 - _ = preview_win.print(&.{ 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; 215 - }; 216 - } 217 - 218 - break :file; 219 - } 220 - 221 - // Handle pdf. 222 - if (std.mem.eql(u8, std.fs.path.extension(entry.name), ".pdf")) { 223 - const output = std.process.Child.run(.{ 224 - .allocator = app.alloc, 225 - .argv = &[_][]const u8{ 226 - "pdftotext", 227 - "-f", 228 - "0", 229 - "-l", 230 - "5", 231 - app.current_item_path, 232 - "-", 233 195 }, 234 - .cwd_dir = app.directories.dir, 235 - }) catch { 236 - _ = preview_win.print(&.{.{ 237 - .text = "No preview available. Install pdftotext to get PDF previews.", 238 - }}, .{}); 239 - break :file; 240 - }; 241 - defer app.alloc.free(output.stderr); 242 - defer app.alloc.free(output.stdout); 243 - 244 - if (output.term.Exited != 0) { 245 - _ = preview_win.print(&.{.{ 246 - .text = "No preview available. Install pdftotext to get PDF previews.", 247 - }}, .{}); 248 - break :file; 249 196 } 250 - 251 - if (app.directories.pdf_contents) |contents| app.alloc.free(contents); 252 - app.directories.pdf_contents = try app.alloc.dupe(u8, output.stdout); 253 - 254 - _ = preview_win.print(&.{ 255 - .{ .text = app.directories.pdf_contents.? }, 256 - }, .{}); 257 - break :file; 197 + } else { 198 + _ = preview_win.print(&.{.{ .text = "Image not found in cache." }}, .{}); 258 199 } 259 - 260 - // Handle archives 261 - if (Archive.ArchiveType.fromPath(entry.name)) |archive_type| { 262 - if (app.archive_files) |*files| { 263 - files.deinit(app.alloc); 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); 285 - } 286 - 287 - for (app.archive_files.?.entries.items, 0..) |path, i| { 288 - if (i >= preview_win.height) break; 289 - const w = preview_win.child(.{ .y_off = @intCast(i), .height = 1 }); 290 - w.fill(vaxis.Cell{ .style = config.styles.list_item }); 291 - _ = w.print(&.{.{ .text = path, .style = config.styles.list_item }}, .{}); 292 - } 293 - break :file; 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. 308 - _ = preview_win.print(&.{.{ .text = "No preview available." }}, .{}); 309 - }, 310 - else => { 311 - _ = preview_win.print(&.{ 312 - vaxis.Segment{ .text = app.current_item_path }, 313 - }, .{}); 314 200 }, 315 201 } 316 202 } ··· 355 241 if (entry.kind == .directory) { 356 242 maybe_meta = directories.dir.stat() catch break :lbl; 357 243 } else if (entry.kind == .file) { 358 - var file = directories.dir.openFile(entry.name, .{}) catch break :lbl; 244 + const clean_name = path_utils.getCleanName(entry); 245 + var file = directories.dir.openFile(clean_name, .{}) catch break :lbl; 359 246 maybe_meta = file.stat() catch break :lbl; 360 247 } 361 248 ··· 395 282 "r--", "r-x", "rw-", "rwx", 396 283 }; 397 284 398 - const stat = directories.dir.statFile(entry.name) catch { 285 + const clean_name = path_utils.getCleanName(entry); 286 + const stat = directories.dir.statFile(clean_name) catch { 399 287 _ = try file_perm_fbs.write("---------\n"); 400 288 break :lbl 10; 401 289 }; ··· 424 312 425 313 // Size. 426 314 const size: ?usize = lbl: { 427 - const stat = directories.dir.statFile(entry.name) catch break :lbl null; 315 + const clean_name = path_utils.getCleanName(entry); 316 + const stat = directories.dir.statFile(clean_name) catch break :lbl null; 428 317 if (entry.kind == .file) { 429 318 break :lbl stat.size; 430 319 } else if (entry.kind == .directory) { 431 320 if (config.true_dir_size) { 432 321 var dir = directories.dir.openDir( 433 - entry.name, 322 + clean_name, 434 323 .{ .iterate = true }, 435 324 ) catch break :lbl null; 436 325 defer dir.close();
+15 -61
src/event_handlers.zig
··· 9 9 const Keybinds = @import("./config.zig").Keybinds; 10 10 const environment = @import("./environment.zig"); 11 11 const events = @import("./events.zig"); 12 + const Preview = @import("./preview.zig"); 12 13 13 14 const config = &@import("./config.zig").config; 14 15 ··· 93 94 app.text_input.insertSliceAtCursor(entry.name) catch {}; 94 95 app.state = .rename; 95 96 }, 96 - 97 97 .create_dir => { 98 98 try app.repopulateDirectory(""); 99 99 app.text_input.clearAndFree(); ··· 117 117 app.text_input.insertSliceAtCursor(":") catch {}; 118 118 app.state = .command; 119 119 }, 120 - .jump_bottom => app.directories.entries.selectLast(), 121 - .jump_top => app.directories.entries.selectFirst(), 120 + .jump_bottom => { 121 + app.directories.entries.selectLast(); 122 + app.preview_cache.invalidate(); 123 + Preview.loadPreviewForCurrentEntry(app) catch {}; 124 + }, 125 + .jump_top => { 126 + app.directories.entries.selectFirst(); 127 + app.preview_cache.invalidate(); 128 + Preview.loadPreviewForCurrentEntry(app) catch {}; 129 + }, 122 130 .toggle_verbose_file_information => app.drawer.verbose = !app.drawer.verbose, 123 131 .force_delete => try events.forceDelete(app), 124 132 .yank => try events.yank(app), ··· 129 137 switch (key.codepoint) { 130 138 '-', 'h', Key.left => try events.traverseLeft(app), 131 139 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 - }, 140 + 'j', Key.down => app.directories.entries.next(), 141 + 'k', Key.up => app.directories.entries.previous(), 188 142 'u' => try events.undo(app), 189 143 else => {}, 190 144 } 145 + app.preview_cache.invalidate(); 146 + Preview.loadPreviewForCurrentEntry(app) catch {}; 191 147 } 192 148 }, 193 149 .image_ready => {}, ··· 214 170 app.state = .normal; 215 171 }, 216 172 Key.enter => { 217 - const selected = app.directories.entries.selected; 218 173 switch (app.state) { 219 174 .new_dir => try events.createNewDir(app), 220 175 .new_file => try events.createNewFile(app), ··· 282 237 } 283 238 284 239 if (app.state != .help_menu) app.state = .normal; 285 - app.directories.entries.selected = selected; 286 240 }, 287 241 Key.up => { 288 242 if (app.state == .command) {
+38 -32
src/events.zig
··· 6 6 const App = @import("./app.zig"); 7 7 const Archive = @import("./archive.zig"); 8 8 const environment = @import("./environment.zig"); 9 + const path_utils = @import("./path_utils.zig"); 10 + const Preview = @import("./preview.zig"); 9 11 10 12 const config = &@import("./config.zig").config; 11 13 ··· 17 19 app.notification.write("Can not to delete item - no item selected.", .warn) catch {}; 18 20 return; 19 21 }) orelse return; 22 + const clean_name = path_utils.getCleanName(entry); 20 23 21 24 var prev_path_buf: [std.fs.max_path_bytes]u8 = undefined; 22 - const prev_path = app.directories.dir.realpath(entry.name, &prev_path_buf) catch { 25 + const prev_path = app.directories.dir.realpath(clean_name, &prev_path_buf) catch { 23 26 message = try std.fmt.allocPrint(app.alloc, "Failed to delete '{s}' - unable to retrieve absolute path.", .{entry.name}); 24 27 app.notification.write(message.?, .err) catch {}; 25 28 if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {}; ··· 53 56 return; 54 57 } 55 58 56 - const tmp_path = try std.fmt.allocPrint(app.alloc, "{s}/{s}-{f}", .{ trash_dir_path, entry.name, zuid.new.v4() }); 57 - if (app.directories.dir.rename(entry.name, tmp_path)) { 59 + const tmp_path = try std.fmt.allocPrint(app.alloc, "{s}/{s}-{f}", .{ trash_dir_path, clean_name, zuid.new.v4() }); 60 + if (app.directories.dir.rename(clean_name, tmp_path)) { 58 61 if (app.actions.push(.{ 59 62 .delete = .{ .prev_path = prev_path_alloc, .new_path = tmp_path }, 60 63 })) |prev_elem| { ··· 65 68 app.notification.write(message.?, .info) catch {}; 66 69 67 70 app.directories.removeSelected(); 71 + Preview.loadPreviewForCurrentEntry(app) catch {}; 68 72 } else |err| { 69 73 app.alloc.free(prev_path_alloc); 70 74 app.alloc.free(tmp_path); ··· 115 119 app.alloc.free(prev_elem.rename.new_path); 116 120 } 117 121 118 - try app.repopulateDirectory(""); 122 + app.directories.clearEntries(); 123 + app.directories.populateEntries("") catch |err| { 124 + const m = try std.fmt.allocPrint(app.alloc, "Failed to read directory entries - {}.", .{err}); 125 + defer app.alloc.free(m); 126 + app.notification.write(m, .err) catch {}; 127 + if (app.file_logger) |file_logger| file_logger.write(m, .err) catch {}; 128 + }; 129 + 130 + const target_name = if (entry.kind == .directory) 131 + try std.fmt.allocPrint(app.alloc, "{s}/", .{new_path}) 132 + else 133 + new_path; 134 + defer if (entry.kind == .directory) app.alloc.free(target_name); 135 + 136 + for (app.directories.entries.all(), 0..) |e, i| { 137 + if (std.mem.eql(u8, e.name, target_name)) { 138 + app.directories.entries.selected = i; 139 + break; 140 + } 141 + } 142 + 143 + // No need to revalidate cache as we're viewing the same file 144 + Preview.loadPreviewForCurrentEntry(app) catch |err| { 145 + if (app.file_logger) |file_logger| { 146 + const msg = std.fmt.allocPrint(app.alloc, "Failed to load preview after repopulate: {}", .{err}) catch return; 147 + defer app.alloc.free(msg); 148 + file_logger.write(msg, .err) catch {}; 149 + } 150 + }; 119 151 120 152 message = try std.fmt.allocPrint(app.alloc, "Renamed '{s}' to '{s}'.", .{ entry.name, new_path }); 121 153 app.notification.write(message.?, .info) catch {}; ··· 140 172 pub fn toggleHiddenFiles(app: *App) error{OutOfMemory}!void { 141 173 config.show_hidden = !config.show_hidden; 142 174 143 - const prev_selected_name: []const u8, const prev_selected_err: bool = lbl: { 144 - const selected = app.directories.getSelected() catch break :lbl .{ "", true }; 145 - if (selected == null) break :lbl .{ "", true }; 146 - 147 - break :lbl .{ try app.alloc.dupe(u8, selected.?.name), false }; 148 - }; 149 - defer if (!prev_selected_err) app.alloc.free(prev_selected_name); 150 - 151 175 try app.repopulateDirectory(""); 152 176 app.text_input.clearAndFree(); 153 - 154 - for (app.directories.entries.all()) |entry| { 155 - if (std.mem.eql(u8, entry.name, prev_selected_name)) return; 156 - app.directories.entries.selected += 1; 157 - } 158 - 159 - // If it didn't find entry, reset selected. 160 - app.directories.entries.selected = 0; 161 177 } 162 178 163 179 pub fn yank(app: *App) error{OutOfMemory}!void { ··· 497 513 return; 498 514 }; 499 515 500 - const selected = app.directories.entries.selected; 501 - 502 516 switch (action) { 503 517 .delete => |a| { 504 518 defer app.alloc.free(a.new_path); ··· 519 533 return; 520 534 }; 521 535 522 - try app.repopulateDirectory(""); 523 - app.text_input.clearAndFree(); 524 - 525 536 message = try std.fmt.allocPrint(app.alloc, "Restored '{s}' as '{s}'.", .{ a.prev_path, new_path_res.path }); 526 537 app.notification.write(message.?, .info) catch {}; 527 538 }, ··· 544 555 return; 545 556 }; 546 557 547 - try app.repopulateDirectory(""); 548 - app.text_input.clearAndFree(); 549 - 550 558 message = try std.fmt.allocPrint(app.alloc, "Reverted renaming of '{s}', now '{s}'.", .{ a.new_path, new_path_res.path }); 551 559 app.notification.write(message.?, .info) catch {}; 552 560 }, ··· 559 567 if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {}; 560 568 return; 561 569 }; 562 - 563 - try app.repopulateDirectory(""); 564 - app.text_input.clearAndFree(); 565 570 }, 566 571 } 567 572 568 - app.directories.entries.selected = selected; 573 + try app.repopulateDirectory(""); 574 + app.text_input.clearAndFree(); 569 575 } 570 576 571 577 pub fn extractArchive(app: *App) error{OutOfMemory}!void {
+3 -3
src/notification.zig
··· 30 30 self.timer = std.time.timestamp(); 31 31 self.style = style; 32 32 33 - // if (self.loop) |loop| { 34 - // loop.postEvent(.notification); 35 - // } 33 + if (self.loop) |loop| { 34 + loop.postEvent(.notification); 35 + } 36 36 } 37 37 38 38 pub fn reset(self: *Self) void {
+8
src/path_utils.zig
··· 1 + const std = @import("std"); 2 + 3 + pub fn getCleanName(entry: std.fs.Dir.Entry) []const u8 { 4 + if (entry.kind == .directory and entry.name.len > 0 and entry.name[entry.name.len - 1] == '/') { 5 + return entry.name[0 .. entry.name.len - 1]; 6 + } 7 + return entry.name; 8 + }
+59 -11
src/preview.zig
··· 3 3 const App = @import("./app.zig"); 4 4 const Archive = @import("./archive.zig"); 5 5 const Image = @import("./image.zig"); 6 + const path_utils = @import("./path_utils.zig"); 6 7 const config = &@import("./config.zig").config; 7 8 8 9 pub const PreviewType = enum { ··· 76 77 self.current = null; 77 78 } 78 79 80 + pub fn updatePath(self: *PreviewCache, app: *App, old_path: []const u8, new_path: []const u8) error{OutOfMemory}!void { 81 + if (self.current) |*entry| { 82 + if (std.mem.eql(u8, entry.file_path, old_path)) { 83 + if (entry.preview == .image) { 84 + app.images.mutex.lock(); 85 + defer app.images.mutex.unlock(); 86 + 87 + if (app.images.cache.fetchRemove(old_path)) |kv| { 88 + app.images.cache.put(new_path, kv.value) catch |err| { 89 + kv.value.deinit(app.alloc, app.vx, &app.tty); 90 + self.clear(); 91 + return err; 92 + }; 93 + } 94 + 95 + self.alloc.free(entry.preview.image.cache_path); 96 + entry.preview.image.cache_path = try self.alloc.dupe(u8, new_path); 97 + } 98 + 99 + self.alloc.free(entry.file_path); 100 + entry.file_path = try self.alloc.dupe(u8, new_path); 101 + } 102 + } 103 + } 104 + 79 105 pub fn get(self: *PreviewCache, path: []const u8) ?*const PreviewData { 80 106 if (self.current) |*entry| { 81 107 if (entry.is_valid and std.mem.eql(u8, entry.file_path, path)) { ··· 101 127 102 128 const entry = (try app.directories.getSelected()) orelse return; 103 129 130 + const clean_name = path_utils.getCleanName(entry); 104 131 const path = try app.directories.dir.realpathAlloc( 105 132 app.alloc, 106 - entry.name, 133 + clean_name, 107 134 ); 108 135 defer app.alloc.free(path); 109 136 ··· 123 150 fn loadDirectoryPreview(app: *App, entry: std.fs.Dir.Entry) !PreviewData { 124 151 app.directories.clearChildEntries(); 125 152 126 - app.directories.populateChildEntries(entry.name) catch |err| { 153 + const clean_name = path_utils.getCleanName(entry); 154 + app.directories.populateChildEntries(clean_name) catch |err| { 127 155 const message = try std.fmt.allocPrint( 128 156 app.alloc, 129 157 "Failed to read directory entries - {}.", ··· 137 165 return PreviewData{ .none = {} }; 138 166 }; 139 167 140 - var list = std.ArrayList([]const u8).init(app.alloc); 168 + var list: std.ArrayList([]const u8) = .empty; 141 169 for (app.directories.child_entries.all()) |child| { 142 170 const owned = try app.alloc.dupe(u8, child); 143 - try list.append(owned); 171 + try list.append(app.alloc, owned); 144 172 } 145 173 146 174 return PreviewData{ .directory = list }; ··· 167 195 } 168 196 169 197 fn loadTextPreview(app: *App, entry: std.fs.Dir.Entry) !PreviewData { 198 + const clean_name = path_utils.getCleanName(entry); 170 199 var file = app.directories.dir.openFile( 171 - entry.name, 200 + clean_name, 172 201 .{ .mode = .read_only }, 173 202 ) catch |err| { 174 203 const message = try std.fmt.allocPrint( ··· 209 238 } 210 239 211 240 fn loadImagePreview(app: *App, entry: std.fs.Dir.Entry) !PreviewData { 241 + const clean_name = path_utils.getCleanName(entry); 212 242 const path = try app.directories.dir.realpathAlloc( 213 243 app.alloc, 214 - entry.name, 244 + clean_name, 215 245 ); 216 246 defer app.alloc.free(path); 217 247 ··· 235 265 } 236 266 237 267 fn loadPdfPreview(app: *App, entry: std.fs.Dir.Entry) !PreviewData { 268 + const clean_name = path_utils.getCleanName(entry); 238 269 const path = try app.directories.dir.realpathAlloc( 239 270 app.alloc, 240 - entry.name, 271 + clean_name, 241 272 ); 242 273 defer app.alloc.free(path); 243 274 ··· 274 305 entry: std.fs.Dir.Entry, 275 306 archive_type: Archive.ArchiveType, 276 307 ) !PreviewData { 308 + const clean_name = path_utils.getCleanName(entry); 277 309 var file = app.directories.dir.openFile( 278 - entry.name, 310 + clean_name, 279 311 .{ .mode = .read_only }, 280 312 ) catch |err| { 281 313 const message = try std.fmt.allocPrint( ··· 326 358 327 359 fn isImageExtension(ext: []const u8) bool { 328 360 const supported = [_][]const u8{ 329 - ".png", ".jpg", ".jpeg", ".gif", 330 - ".bmp", ".tga", ".qoi", ".pam", 331 - ".pbm", ".pgm", ".ppm", 361 + ".bmp", 362 + ".farbfeld", 363 + ".gif", 364 + ".iff", 365 + ".ilbm", 366 + ".jpeg", 367 + ".jpg", 368 + ".pam", 369 + ".pbm", 370 + ".pcx", 371 + ".pgm", 372 + ".png", 373 + ".ppm", 374 + ".qoi", 375 + ".ras", 376 + ".sgi", 377 + ".tga", 378 + ".tif", 379 + ".tiff", 332 380 }; 333 381 334 382 for (supported) |supported_ext| {