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