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

feat: move file loading logic to preview.zig

+248
+248
src/preview.zig
··· 1 1 const std = @import("std"); 2 2 3 + const App = @import("./app.zig"); 4 + const Archive = @import("./archive.zig"); 5 + const Image = @import("./image.zig"); 6 + const config = &@import("./config.zig").config; 7 + 3 8 pub const PreviewType = enum { 4 9 none, 5 10 text, ··· 90 95 }; 91 96 } 92 97 }; 98 + 99 + pub fn loadPreviewForCurrentEntry(app: *App) !void { 100 + if (!config.preview_file) return; 101 + 102 + const entry = (try app.directories.getSelected()) orelse return; 103 + 104 + const path = try app.directories.dir.realpathAlloc( 105 + app.alloc, 106 + entry.name, 107 + ); 108 + defer app.alloc.free(path); 109 + 110 + if (app.preview_cache.get(path)) |_| { 111 + return; 112 + } 113 + 114 + const preview = switch (entry.kind) { 115 + .directory => try loadDirectoryPreview(app, entry), 116 + .file => try loadFilePreview(app, entry), 117 + else => PreviewData{ .none = {} }, 118 + }; 119 + 120 + try app.preview_cache.set(path, preview); 121 + } 122 + 123 + fn loadDirectoryPreview(app: *App, entry: std.fs.Dir.Entry) !PreviewData { 124 + app.directories.clearChildEntries(); 125 + 126 + app.directories.populateChildEntries(entry.name) catch |err| { 127 + const message = try std.fmt.allocPrint( 128 + app.alloc, 129 + "Failed to read directory entries - {}.", 130 + .{err}, 131 + ); 132 + defer app.alloc.free(message); 133 + app.notification.write(message, .err) catch {}; 134 + if (app.file_logger) |file_logger| { 135 + file_logger.write(message, .err) catch {}; 136 + } 137 + return PreviewData{ .none = {} }; 138 + }; 139 + 140 + var list = std.ArrayList([]const u8).init(app.alloc); 141 + for (app.directories.child_entries.all()) |child| { 142 + const owned = try app.alloc.dupe(u8, child); 143 + try list.append(owned); 144 + } 145 + 146 + return PreviewData{ .directory = list }; 147 + } 148 + 149 + fn loadFilePreview(app: *App, entry: std.fs.Dir.Entry) !PreviewData { 150 + const file_ext = std.fs.path.extension(entry.name); 151 + 152 + if (config.show_images) { 153 + if (isImageExtension(file_ext)) { 154 + return try loadImagePreview(app, entry); 155 + } 156 + } 157 + 158 + if (std.mem.eql(u8, file_ext, ".pdf")) { 159 + return try loadPdfPreview(app, entry); 160 + } 161 + 162 + if (Archive.ArchiveType.fromPath(entry.name)) |archive_type| { 163 + return try loadArchivePreview(app, entry, archive_type); 164 + } 165 + 166 + return try loadTextPreview(app, entry); 167 + } 168 + 169 + fn loadTextPreview(app: *App, entry: std.fs.Dir.Entry) !PreviewData { 170 + var file = app.directories.dir.openFile( 171 + entry.name, 172 + .{ .mode = .read_only }, 173 + ) catch |err| { 174 + const message = try std.fmt.allocPrint( 175 + app.alloc, 176 + "Failed to open file - {}.", 177 + .{err}, 178 + ); 179 + defer app.alloc.free(message); 180 + app.notification.write(message, .err) catch {}; 181 + if (app.file_logger) |file_logger| { 182 + file_logger.write(message, .err) catch {}; 183 + } 184 + return PreviewData{ .none = {} }; 185 + }; 186 + defer file.close(); 187 + 188 + var buffer: [4096]u8 = undefined; 189 + const bytes = file.readAll(&buffer) catch |err| { 190 + const message = try std.fmt.allocPrint( 191 + app.alloc, 192 + "Failed to read file contents - {}.", 193 + .{err}, 194 + ); 195 + defer app.alloc.free(message); 196 + app.notification.write(message, .err) catch {}; 197 + if (app.file_logger) |file_logger| { 198 + file_logger.write(message, .err) catch {}; 199 + } 200 + return PreviewData{ .none = {} }; 201 + }; 202 + 203 + if (std.unicode.utf8ValidateSlice(buffer[0..bytes])) { 204 + const text = try app.alloc.dupe(u8, buffer[0..bytes]); 205 + return PreviewData{ .text = text }; 206 + } 207 + 208 + return PreviewData{ .none = {} }; 209 + } 210 + 211 + fn loadImagePreview(app: *App, entry: std.fs.Dir.Entry) !PreviewData { 212 + const path = try app.directories.dir.realpathAlloc( 213 + app.alloc, 214 + entry.name, 215 + ); 216 + defer app.alloc.free(path); 217 + 218 + app.images.mutex.lock(); 219 + const exists = app.images.cache.contains(path); 220 + app.images.mutex.unlock(); 221 + 222 + if (!exists) { 223 + const owned_path = try app.alloc.dupe(u8, path); 224 + Image.processImage(app.alloc, app, owned_path) catch { 225 + app.alloc.free(owned_path); 226 + return PreviewData{ .none = {} }; 227 + }; 228 + } 229 + 230 + return PreviewData{ 231 + .image = .{ 232 + .cache_path = try app.alloc.dupe(u8, path), 233 + }, 234 + }; 235 + } 236 + 237 + fn loadPdfPreview(app: *App, entry: std.fs.Dir.Entry) !PreviewData { 238 + const path = try app.directories.dir.realpathAlloc( 239 + app.alloc, 240 + entry.name, 241 + ); 242 + defer app.alloc.free(path); 243 + 244 + const result = std.process.Child.run(.{ 245 + .allocator = app.alloc, 246 + .argv = &[_][]const u8{ 247 + "pdftotext", 248 + "-f", 249 + "0", 250 + "-l", 251 + "5", 252 + path, 253 + "-", 254 + }, 255 + .cwd_dir = app.directories.dir, 256 + }) catch { 257 + app.notification.write("No preview available. Install pdftotext to get PDF previews.", .err) catch {}; 258 + return PreviewData{ .none = {} }; 259 + }; 260 + defer app.alloc.free(result.stdout); 261 + defer app.alloc.free(result.stderr); 262 + 263 + if (result.term.Exited != 0) { 264 + app.notification.write("No preview available. Install pdftotext to get PDF previews.", .err) catch {}; 265 + return PreviewData{ .none = {} }; 266 + } 267 + 268 + const text = try app.alloc.dupe(u8, result.stdout); 269 + return PreviewData{ .pdf = text }; 270 + } 271 + 272 + fn loadArchivePreview( 273 + app: *App, 274 + entry: std.fs.Dir.Entry, 275 + archive_type: Archive.ArchiveType, 276 + ) !PreviewData { 277 + var file = app.directories.dir.openFile( 278 + entry.name, 279 + .{ .mode = .read_only }, 280 + ) catch |err| { 281 + const message = try std.fmt.allocPrint( 282 + app.alloc, 283 + "Failed to open archive - {}.", 284 + .{err}, 285 + ); 286 + defer app.alloc.free(message); 287 + app.notification.write(message, .err) catch {}; 288 + if (app.file_logger) |file_logger| { 289 + file_logger.write(message, .err) catch {}; 290 + } 291 + return PreviewData{ .none = {} }; 292 + }; 293 + defer file.close(); 294 + 295 + const archive_contents = Archive.listArchiveContents( 296 + app.alloc, 297 + file, 298 + archive_type, 299 + config.archive_traversal_limit, 300 + ) catch |err| { 301 + const message = try std.fmt.allocPrint( 302 + app.alloc, 303 + "Failed to read archive: {s}", 304 + .{@errorName(err)}, 305 + ); 306 + defer app.alloc.free(message); 307 + app.notification.write(message, .err) catch {}; 308 + if (app.file_logger) |file_logger| { 309 + file_logger.write(message, .err) catch {}; 310 + } 311 + return PreviewData{ .none = {} }; 312 + }; 313 + 314 + if (config.sort_dirs) { 315 + const sort_mod = @import("./sort.zig"); 316 + std.mem.sort( 317 + []const u8, 318 + archive_contents.entries.items, 319 + {}, 320 + sort_mod.string, 321 + ); 322 + } 323 + 324 + return PreviewData{ .archive = archive_contents.entries }; 325 + } 326 + 327 + fn isImageExtension(ext: []const u8) bool { 328 + const supported = [_][]const u8{ 329 + ".png", ".jpg", ".jpeg", ".gif", 330 + ".bmp", ".tga", ".qoi", ".pam", 331 + ".pbm", ".pgm", ".ppm", 332 + }; 333 + 334 + for (supported) |supported_ext| { 335 + if (std.ascii.eqlIgnoreCase(ext, supported_ext)) { 336 + return true; 337 + } 338 + } 339 + return false; 340 + }