地圖 (Jido) is a lightweight Unix TUI file explorer designed for speed and simplicity.
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}