地圖 (Jido) is a lightweight Unix TUI file explorer designed for speed and simplicity.
at main 256 lines 7.4 kB view raw
1const std = @import("std"); 2 3const fuzzig = @import("fuzzig"); 4 5const CircStack = @import("./circ_stack.zig").CircularStack; 6const List = @import("./list.zig").List; 7 8const sort = &@import("./sort.zig"); 9const config = &@import("./config.zig").config; 10const history_len: usize = 100; 11 12const Self = @This(); 13 14alloc: std.mem.Allocator, 15dir: std.fs.Dir, 16path_buf: [std.fs.max_path_bytes]u8 = undefined, 17file: struct { 18 handle: ?std.fs.File = null, 19 data: [4096]u8 = undefined, 20 bytes_read: usize = 0, 21} = .{}, 22pdf_contents: ?[]u8 = null, 23entries: List(std.fs.Dir.Entry), 24history: CircStack(usize, history_len), 25child_entries: List([]const u8), 26searcher: fuzzig.Ascii, 27 28pub fn init(alloc: std.mem.Allocator, entry_dir: ?[]const u8) !Self { 29 const dir_path = if (entry_dir) |dir| dir else "."; 30 const dir = std.fs.cwd().openDir(dir_path, .{ .iterate = true }) catch |err| { 31 switch (err) { 32 error.FileNotFound => { 33 std.log.err("path '{s}' could not be found.", .{dir_path}); 34 return err; 35 }, 36 else => { 37 std.log.err("{}", .{err}); 38 return err; 39 }, 40 } 41 }; 42 43 return Self{ 44 .alloc = alloc, 45 .dir = dir, 46 .entries = List(std.fs.Dir.Entry).init(alloc), 47 .history = CircStack(usize, history_len).init(), 48 .child_entries = List([]const u8).init(alloc), 49 .searcher = try fuzzig.Ascii.init( 50 alloc, 51 std.fs.max_path_bytes, 52 std.fs.max_path_bytes, 53 .{ .case_sensitive = false }, 54 ), 55 }; 56} 57 58pub fn deinit(self: *Self) void { 59 self.clearEntries(); 60 self.clearChildEntries(); 61 62 self.entries.deinit(); 63 self.child_entries.deinit(); 64 65 self.dir.close(); 66 self.searcher.deinit(); 67 68 if (self.pdf_contents) |contents| self.alloc.free(contents); 69} 70 71pub fn getSelected(self: *Self) !?std.fs.Dir.Entry { 72 return self.entries.getSelected(); 73} 74 75/// Asserts there is a selected item. 76pub fn removeSelected(self: *Self) void { 77 const entry = lbl: { 78 const entry = self.getSelected() catch return std.debug.assert(false); 79 if (entry) |e| break :lbl e else return std.debug.assert(false); 80 }; 81 self.alloc.free(entry.name); 82 _ = self.entries.items.orderedRemove(self.entries.selected); 83} 84 85pub fn fullPath(self: *Self, relative_path: []const u8) ![]const u8 { 86 return try self.dir.realpath(relative_path, &self.path_buf); 87} 88 89pub fn getDirSize(self: Self, dir: std.fs.Dir) !usize { 90 var total_size: usize = 0; 91 92 var walker = try dir.walk(self.alloc); 93 defer walker.deinit(); 94 95 while (try walker.next()) |entry| { 96 switch (entry.kind) { 97 .file => { 98 const stat = try entry.dir.statFile(entry.basename); 99 total_size += stat.size; 100 }, 101 else => {}, 102 } 103 } 104 105 return total_size; 106} 107 108pub fn populateChildEntries( 109 self: *Self, 110 relative_path: []const u8, 111) !void { 112 var dir = try self.dir.openDir(relative_path, .{ .iterate = true }); 113 defer dir.close(); 114 115 var it = dir.iterate(); 116 while (try it.next()) |entry| { 117 if (std.mem.startsWith(u8, entry.name, ".") and config.show_hidden == false) { 118 continue; 119 } 120 121 if (entry.kind == .directory) { 122 try self.child_entries.append(try std.fmt.allocPrint(self.alloc, "{s}/", .{entry.name})); 123 } else { 124 try self.child_entries.append(try self.alloc.dupe(u8, entry.name)); 125 } 126 } 127 128 if (config.sort_dirs == true) { 129 std.mem.sort([]const u8, self.child_entries.all(), {}, sort.string); 130 } 131} 132 133pub fn populateEntries(self: *Self, fuzzy_search: []const u8) !void { 134 var it = self.dir.iterate(); 135 while (try it.next()) |entry| { 136 const score = self.searcher.score(entry.name, fuzzy_search) orelse 0; 137 if (fuzzy_search.len > 0 and score < 1) { 138 continue; 139 } 140 141 if (std.mem.startsWith(u8, entry.name, ".") and config.show_hidden == false) { 142 continue; 143 } 144 145 try self.entries.append(.{ 146 .kind = entry.kind, 147 .name = if (entry.kind == .directory) try std.fmt.allocPrint(self.alloc, "{s}/", .{entry.name}) else try self.alloc.dupe(u8, entry.name), 148 }); 149 } 150 151 if (config.sort_dirs == true) { 152 std.mem.sort(std.fs.Dir.Entry, self.entries.all(), {}, sort.sortDirectoryEntry); 153 } 154} 155 156pub fn clearEntries(self: *Self) void { 157 for (self.entries.all()) |entry| { 158 self.entries.alloc.free(entry.name); 159 } 160 self.entries.clear(); 161} 162 163pub fn clearChildEntries(self: *Self) void { 164 for (self.child_entries.all()) |entry| { 165 self.child_entries.alloc.free(entry); 166 } 167 self.child_entries.clear(); 168} 169 170const testing = std.testing; 171 172test "Directories: populateEntries respects show_hidden config" { 173 const local_config = &@import("./config.zig").config; 174 175 var tmp = testing.tmpDir(.{}); 176 defer tmp.cleanup(); 177 178 { 179 const visible = try tmp.dir.createFile("visible.txt", .{}); 180 visible.close(); 181 const hidden = try tmp.dir.createFile(".hidden.txt", .{}); 182 hidden.close(); 183 } 184 185 var path_buf: [std.fs.max_path_bytes]u8 = undefined; 186 const tmp_path = try tmp.dir.realpath(".", &path_buf); 187 const iter_dir = try std.fs.openDirAbsolute(tmp_path, .{ .iterate = true }); 188 189 var dirs = try Self.init(testing.allocator, null); 190 defer { 191 dirs.clearEntries(); 192 dirs.clearChildEntries(); 193 dirs.entries.deinit(); 194 dirs.child_entries.deinit(); 195 dirs.searcher.deinit(); 196 } 197 dirs.dir.close(); 198 dirs.dir = iter_dir; 199 200 local_config.show_hidden = false; 201 try dirs.populateEntries(""); 202 try testing.expectEqual(@as(usize, 1), dirs.entries.len()); 203 204 dirs.clearEntries(); 205 local_config.show_hidden = true; 206 try dirs.populateEntries(""); 207 try testing.expectEqual(@as(usize, 2), dirs.entries.len()); 208} 209 210test "Directories: fuzzy search filters entries" { 211 var tmp = testing.tmpDir(.{}); 212 defer tmp.cleanup(); 213 214 { 215 const f1 = try tmp.dir.createFile("test_file.txt", .{}); 216 f1.close(); 217 const f2 = try tmp.dir.createFile("other.txt", .{}); 218 f2.close(); 219 const f3 = try tmp.dir.createFile("test_another.txt", .{}); 220 f3.close(); 221 } 222 223 var path_buf: [std.fs.max_path_bytes]u8 = undefined; 224 const tmp_path = try tmp.dir.realpath(".", &path_buf); 225 const iter_dir = try std.fs.openDirAbsolute(tmp_path, .{ .iterate = true }); 226 227 var dirs = try Self.init(testing.allocator, null); 228 defer { 229 dirs.clearEntries(); 230 dirs.clearChildEntries(); 231 dirs.entries.deinit(); 232 dirs.child_entries.deinit(); 233 dirs.searcher.deinit(); 234 } 235 dirs.dir.close(); 236 dirs.dir = iter_dir; 237 238 try dirs.populateEntries("test"); 239 // Should match test_* 240 try testing.expect(dirs.entries.len() >= 2); 241 242 // Verify all entries contain "test" 243 for (dirs.entries.all()) |entry| { 244 try testing.expect(std.mem.indexOf(u8, entry.name, "test") != null); 245 } 246} 247 248test "Directories: fullPath resolves relative paths" { 249 var dirs = try Self.init(testing.allocator, "."); 250 defer dirs.deinit(); 251 252 const path = try dirs.fullPath("."); 253 try testing.expect(path.len > 0); 254 // Should be absolute 255 try testing.expect(std.mem.indexOf(u8, path, "/") != null); 256}