地圖 (Jido) is a lightweight Unix TUI file explorer designed for speed and simplicity.
at main 388 lines 11 kB view raw
1const std = @import("std"); 2 3const App = @import("./app.zig"); 4const Archive = @import("./archive.zig"); 5const Image = @import("./image.zig"); 6const path_utils = @import("./path_utils.zig"); 7const config = &@import("./config.zig").config; 8 9pub const PreviewType = enum { 10 none, 11 text, 12 image, 13 pdf, 14 archive, 15 directory, 16}; 17 18pub const PreviewData = union(PreviewType) { 19 none: void, 20 text: []const u8, 21 image: ImageInfo, 22 pdf: []const u8, 23 archive: std.ArrayList([]const u8), 24 directory: std.ArrayList([]const u8), 25}; 26 27pub const ImageInfo = struct { 28 cache_path: []const u8, 29}; 30 31pub const CacheEntry = struct { 32 file_path: []const u8, 33 preview: PreviewData, 34 is_valid: bool, 35 36 pub fn deinit(self: *CacheEntry, alloc: std.mem.Allocator) void { 37 alloc.free(self.file_path); 38 switch (self.preview) { 39 .text, .pdf => |data| alloc.free(data), 40 .archive, .directory => |*list| { 41 for (list.items) |item| alloc.free(item); 42 list.deinit(alloc); 43 }, 44 .image => |img| alloc.free(img.cache_path), 45 .none => {}, 46 } 47 } 48}; 49 50pub const PreviewCache = struct { 51 alloc: std.mem.Allocator, 52 current: ?CacheEntry, 53 54 pub fn init(alloc: std.mem.Allocator) PreviewCache { 55 return .{ 56 .alloc = alloc, 57 .current = null, 58 }; 59 } 60 61 pub fn deinit(self: *PreviewCache) void { 62 if (self.current) |*entry| { 63 entry.deinit(self.alloc); 64 } 65 } 66 67 pub fn invalidate(self: *PreviewCache) void { 68 if (self.current) |*entry| { 69 entry.is_valid = false; 70 } 71 } 72 73 pub fn clear(self: *PreviewCache) void { 74 if (self.current) |*entry| { 75 entry.deinit(self.alloc); 76 } 77 self.current = null; 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 105 pub fn get(self: *PreviewCache, path: []const u8) ?*const PreviewData { 106 if (self.current) |*entry| { 107 if (entry.is_valid and std.mem.eql(u8, entry.file_path, path)) { 108 return &entry.preview; 109 } 110 } 111 return null; 112 } 113 114 pub fn set(self: *PreviewCache, path: []const u8, preview: PreviewData) !void { 115 self.clear(); 116 117 self.current = .{ 118 .file_path = try self.alloc.dupe(u8, path), 119 .preview = preview, 120 .is_valid = true, 121 }; 122 } 123}; 124 125pub fn loadPreviewForCurrentEntry(app: *App) !void { 126 if (!config.preview_file) return; 127 128 const entry = (try app.directories.getSelected()) orelse return; 129 130 const clean_name = path_utils.getCleanName(entry); 131 const path = try app.directories.dir.realpathAlloc( 132 app.alloc, 133 clean_name, 134 ); 135 defer app.alloc.free(path); 136 137 if (app.preview_cache.get(path)) |_| { 138 return; 139 } 140 141 const preview = switch (entry.kind) { 142 .directory => try loadDirectoryPreview(app, entry), 143 .file => try loadFilePreview(app, entry), 144 else => PreviewData{ .none = {} }, 145 }; 146 147 try app.preview_cache.set(path, preview); 148} 149 150fn loadDirectoryPreview(app: *App, entry: std.fs.Dir.Entry) !PreviewData { 151 app.directories.clearChildEntries(); 152 153 const clean_name = path_utils.getCleanName(entry); 154 app.directories.populateChildEntries(clean_name) catch |err| { 155 const message = try std.fmt.allocPrint( 156 app.alloc, 157 "Failed to read directory entries - {}.", 158 .{err}, 159 ); 160 defer app.alloc.free(message); 161 app.notification.write(message, .err) catch {}; 162 if (app.file_logger) |file_logger| { 163 file_logger.write(message, .err) catch {}; 164 } 165 return PreviewData{ .none = {} }; 166 }; 167 168 var list: std.ArrayList([]const u8) = .empty; 169 for (app.directories.child_entries.all()) |child| { 170 const owned = try app.alloc.dupe(u8, child); 171 try list.append(app.alloc, owned); 172 } 173 174 return PreviewData{ .directory = list }; 175} 176 177fn loadFilePreview(app: *App, entry: std.fs.Dir.Entry) !PreviewData { 178 const file_ext = std.fs.path.extension(entry.name); 179 180 if (config.show_images) { 181 if (isImageExtension(file_ext)) { 182 return try loadImagePreview(app, entry); 183 } 184 } 185 186 if (std.mem.eql(u8, file_ext, ".pdf")) { 187 return try loadPdfPreview(app, entry); 188 } 189 190 if (Archive.ArchiveType.fromPath(entry.name)) |archive_type| { 191 return try loadArchivePreview(app, entry, archive_type); 192 } 193 194 return try loadTextPreview(app, entry); 195} 196 197fn loadTextPreview(app: *App, entry: std.fs.Dir.Entry) !PreviewData { 198 const clean_name = path_utils.getCleanName(entry); 199 var file = app.directories.dir.openFile( 200 clean_name, 201 .{ .mode = .read_only }, 202 ) catch |err| { 203 const message = try std.fmt.allocPrint( 204 app.alloc, 205 "Failed to open file - {}.", 206 .{err}, 207 ); 208 defer app.alloc.free(message); 209 app.notification.write(message, .err) catch {}; 210 if (app.file_logger) |file_logger| { 211 file_logger.write(message, .err) catch {}; 212 } 213 return PreviewData{ .none = {} }; 214 }; 215 defer file.close(); 216 217 var buffer: [4096]u8 = undefined; 218 const bytes = file.readAll(&buffer) catch |err| { 219 const message = try std.fmt.allocPrint( 220 app.alloc, 221 "Failed to read file contents - {}.", 222 .{err}, 223 ); 224 defer app.alloc.free(message); 225 app.notification.write(message, .err) catch {}; 226 if (app.file_logger) |file_logger| { 227 file_logger.write(message, .err) catch {}; 228 } 229 return PreviewData{ .none = {} }; 230 }; 231 232 if (std.unicode.utf8ValidateSlice(buffer[0..bytes])) { 233 const text = try app.alloc.dupe(u8, buffer[0..bytes]); 234 return PreviewData{ .text = text }; 235 } 236 237 return PreviewData{ .none = {} }; 238} 239 240fn loadImagePreview(app: *App, entry: std.fs.Dir.Entry) !PreviewData { 241 const clean_name = path_utils.getCleanName(entry); 242 const path = try app.directories.dir.realpathAlloc( 243 app.alloc, 244 clean_name, 245 ); 246 defer app.alloc.free(path); 247 248 app.images.mutex.lock(); 249 const exists = app.images.cache.contains(path); 250 app.images.mutex.unlock(); 251 252 if (!exists) { 253 const owned_path = try app.alloc.dupe(u8, path); 254 Image.processImage(app.alloc, app, owned_path) catch { 255 app.alloc.free(owned_path); 256 return PreviewData{ .none = {} }; 257 }; 258 } 259 260 return PreviewData{ 261 .image = .{ 262 .cache_path = try app.alloc.dupe(u8, path), 263 }, 264 }; 265} 266 267fn loadPdfPreview(app: *App, entry: std.fs.Dir.Entry) !PreviewData { 268 const clean_name = path_utils.getCleanName(entry); 269 const path = try app.directories.dir.realpathAlloc( 270 app.alloc, 271 clean_name, 272 ); 273 defer app.alloc.free(path); 274 275 const result = std.process.Child.run(.{ 276 .allocator = app.alloc, 277 .argv = &[_][]const u8{ 278 "pdftotext", 279 "-f", 280 "0", 281 "-l", 282 "5", 283 path, 284 "-", 285 }, 286 .cwd_dir = app.directories.dir, 287 }) catch { 288 app.notification.write("No preview available. Install pdftotext to get PDF previews.", .err) catch {}; 289 return PreviewData{ .none = {} }; 290 }; 291 defer app.alloc.free(result.stdout); 292 defer app.alloc.free(result.stderr); 293 294 if (result.term.Exited != 0) { 295 app.notification.write("No preview available. Install pdftotext to get PDF previews.", .err) catch {}; 296 return PreviewData{ .none = {} }; 297 } 298 299 const text = try app.alloc.dupe(u8, result.stdout); 300 return PreviewData{ .pdf = text }; 301} 302 303fn loadArchivePreview( 304 app: *App, 305 entry: std.fs.Dir.Entry, 306 archive_type: Archive.ArchiveType, 307) !PreviewData { 308 const clean_name = path_utils.getCleanName(entry); 309 var file = app.directories.dir.openFile( 310 clean_name, 311 .{ .mode = .read_only }, 312 ) catch |err| { 313 const message = try std.fmt.allocPrint( 314 app.alloc, 315 "Failed to open archive - {}.", 316 .{err}, 317 ); 318 defer app.alloc.free(message); 319 app.notification.write(message, .err) catch {}; 320 if (app.file_logger) |file_logger| { 321 file_logger.write(message, .err) catch {}; 322 } 323 return PreviewData{ .none = {} }; 324 }; 325 defer file.close(); 326 327 const archive_contents = Archive.listArchiveContents( 328 app.alloc, 329 file, 330 archive_type, 331 config.archive_traversal_limit, 332 ) catch |err| { 333 const message = try std.fmt.allocPrint( 334 app.alloc, 335 "Failed to read archive: {s}", 336 .{@errorName(err)}, 337 ); 338 defer app.alloc.free(message); 339 app.notification.write(message, .err) catch {}; 340 if (app.file_logger) |file_logger| { 341 file_logger.write(message, .err) catch {}; 342 } 343 return PreviewData{ .none = {} }; 344 }; 345 346 if (config.sort_dirs) { 347 const sort_mod = @import("./sort.zig"); 348 std.mem.sort( 349 []const u8, 350 archive_contents.entries.items, 351 {}, 352 sort_mod.string, 353 ); 354 } 355 356 return PreviewData{ .archive = archive_contents.entries }; 357} 358 359fn isImageExtension(ext: []const u8) bool { 360 const supported = [_][]const u8{ 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", 380 }; 381 382 for (supported) |supported_ext| { 383 if (std.ascii.eqlIgnoreCase(ext, supported_ext)) { 384 return true; 385 } 386 } 387 return false; 388}