地圖 (Jido) is a lightweight Unix TUI file explorer designed for speed and simplicity.
1const std = @import("std");
2
3const App = @import("app.zig");
4const environment = @import("environment.zig");
5const Preview = @import("preview.zig");
6
7const user_config = &@import("./config.zig").config;
8
9pub const CommandHistory = struct {
10 const history_len = 10;
11
12 history: [history_len][]const u8 = undefined,
13 count: usize = 0,
14 ///Points to the oldest entry.
15 start: usize = 0,
16 cursor: ?usize = null,
17
18 pub fn deinit(self: *CommandHistory, allocator: std.mem.Allocator) void {
19 for (self.history[0..self.count]) |entry| {
20 allocator.free(entry);
21 }
22 }
23
24 pub fn add(self: *CommandHistory, cmd: []const u8, allocator: std.mem.Allocator) error{OutOfMemory}!void {
25 const index = (self.start + self.count) % history_len;
26
27 if (self.count < history_len) {
28 self.count += 1;
29 } else {
30 // Overwriting the oldest entry.
31 allocator.free(self.history[self.start]);
32 self.start = (self.start + 1) % history_len;
33 }
34
35 self.history[index] = try allocator.dupe(u8, cmd);
36 self.cursor = null;
37 }
38
39 pub fn previous(self: *CommandHistory) ?[]const u8 {
40 if (self.count == 0) return null;
41
42 if (self.cursor == null) {
43 self.cursor = self.count - 1;
44 } else if (self.cursor.? > 0) {
45 self.cursor.? -= 1;
46 }
47
48 return self.getAtCursor();
49 }
50
51 pub fn next(self: *CommandHistory) ?[]const u8 {
52 if (self.count == 0 or self.cursor == null) return null;
53
54 if (self.cursor.? < self.count - 1) {
55 self.cursor.? += 1;
56 return self.getAtCursor();
57 }
58
59 self.cursor = null;
60 return null;
61 }
62
63 fn getAtCursor(self: *CommandHistory) ?[]const u8 {
64 if (self.cursor == null) return null;
65 const index = (self.start + self.cursor.?) % history_len;
66 return self.history[index];
67 }
68};
69
70///Navigate the user to the config dir.
71pub fn config(app: *App) error{OutOfMemory}!void {
72 const dir = dir: {
73 notfound: {
74 break :dir (user_config.configDir() catch break :notfound) orelse break :notfound;
75 }
76 const message = try std.fmt.allocPrint(app.alloc, "Failed to navigate to config directory - unable to retrieve config directory.", .{});
77 defer app.alloc.free(message);
78 app.notification.write(message, .err) catch {};
79 if (app.file_logger) |file_logger| file_logger.write(message, .err) catch {};
80 return;
81 };
82
83 app.directories.dir.close();
84 app.directories.dir = dir;
85 try app.repopulateDirectory("");
86}
87
88///Navigate the user to the trash dir.
89pub fn trash(app: *App) error{OutOfMemory}!void {
90 const dir = dir: {
91 notfound: {
92 break :dir (user_config.trashDir() catch break :notfound) orelse break :notfound;
93 }
94 const message = try std.fmt.allocPrint(app.alloc, "Failed to navigate to trash directory - unable to retrieve trash directory.", .{});
95 defer app.alloc.free(message);
96 app.notification.write(message, .err) catch {};
97 if (app.file_logger) |file_logger| file_logger.write(message, .err) catch {};
98 return;
99 };
100
101 app.directories.dir.close();
102 app.directories.dir = dir;
103 try app.repopulateDirectory("");
104}
105
106///Empty the trash.
107pub fn emptyTrash(app: *App) error{OutOfMemory}!void {
108 var message: ?[]const u8 = null;
109 defer if (message) |msg| app.alloc.free(msg);
110
111 var dir = dir: {
112 notfound: {
113 break :dir (user_config.trashDir() catch break :notfound) orelse break :notfound;
114 }
115 message = try std.fmt.allocPrint(app.alloc, "Failed to navigate to trash directory - unable to retrieve trash directory.", .{});
116 app.notification.write(message.?, .err) catch {};
117 if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {};
118 return;
119 };
120 defer dir.close();
121
122 const failed = environment.deleteContents(dir) catch |err| lbl: {
123 message = try std.fmt.allocPrint(app.alloc, "Failed to empty trash - {}.", .{err});
124 app.notification.write(message.?, .err) catch {};
125 if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {};
126 break :lbl 0;
127 };
128 if (failed > 0) {
129 message = try std.fmt.allocPrint(app.alloc, "Failed to empty {d} items from the trash.", .{failed});
130 app.notification.write(message.?, .err) catch {};
131 if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {};
132 }
133
134 try app.repopulateDirectory("");
135}
136
137pub fn resolvePath(buf: *[std.fs.max_path_bytes]u8, path: []const u8, dir: std.fs.Dir) []const u8 {
138 const resolved_path = if (std.mem.startsWith(u8, path, "~")) path: {
139 var home_dir = (environment.getHomeDir() catch break :path path) orelse break :path path;
140 defer home_dir.close();
141 const relative = std.mem.trim(u8, path[1..], std.fs.path.sep_str);
142 return home_dir.realpath(
143 if (relative.len == 0) "." else relative,
144 buf,
145 ) catch path;
146 } else path;
147
148 return dir.realpath(resolved_path, buf) catch path;
149}
150
151///Change directory.
152pub fn cd(app: *App, path: []const u8) error{OutOfMemory}!void {
153 var message: ?[]const u8 = null;
154 defer if (message) |msg| app.alloc.free(msg);
155
156 var path_buf: [std.fs.max_path_bytes]u8 = undefined;
157 const resolved_path = resolvePath(&path_buf, path, app.directories.dir);
158
159 const dir = app.directories.dir.openDir(resolved_path, .{ .iterate = true }) catch |err| {
160 message = switch (err) {
161 error.FileNotFound => try std.fmt.allocPrint(app.alloc, "Failed to navigate to '{s}' - directory does not exist.", .{resolved_path}),
162 error.NotDir => try std.fmt.allocPrint(app.alloc, "Failed to navigate to '{s}' - item is not a directory.", .{resolved_path}),
163 else => try std.fmt.allocPrint(app.alloc, "Failed to read directory entries - {}.", .{err}),
164 };
165 app.notification.write(message.?, .err) catch {};
166 if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {};
167 return;
168 };
169 app.directories.dir.close();
170 app.directories.dir = dir;
171
172 message = try std.fmt.allocPrint(app.alloc, "Navigated to directory '{s}'.", .{resolved_path});
173 app.notification.write(message.?, .info) catch {};
174
175 try app.repopulateDirectory("");
176 app.directories.history.reset();
177}
178
179const testing = std.testing;
180
181test "CommandHistory: add and retrieve commands" {
182 var history = CommandHistory{};
183 defer history.deinit(testing.allocator);
184
185 try history.add(":cd /tmp", testing.allocator);
186 try history.add(":config", testing.allocator);
187
188 try testing.expectEqual(@as(usize, 2), history.count);
189}
190
191test "CommandHistory: previous/next navigation" {
192 var history = CommandHistory{};
193 defer history.deinit(testing.allocator);
194
195 try history.add(":cmd1", testing.allocator);
196 try history.add(":cmd2", testing.allocator);
197 try history.add(":cmd3", testing.allocator);
198
199 const cmd3 = history.previous();
200 try testing.expectEqualStrings(":cmd3", cmd3.?);
201
202 const cmd2 = history.previous();
203 try testing.expectEqualStrings(":cmd2", cmd2.?);
204
205 const cmd3_again = history.next();
206 try testing.expectEqualStrings(":cmd3", cmd3_again.?);
207
208 const at_end = history.next();
209 try testing.expect(at_end == null);
210}
211
212test "CommandHistory: wraparound at capacity" {
213 var history = CommandHistory{};
214 defer history.deinit(testing.allocator);
215
216 var i: u32 = 0;
217 while (i < 15) : (i += 1) {
218 const cmd = try std.fmt.allocPrint(testing.allocator, ":cmd{}", .{i});
219 defer testing.allocator.free(cmd);
220 try history.add(cmd, testing.allocator);
221 }
222
223 try testing.expectEqual(@as(usize, 10), history.count);
224
225 const recent = history.previous();
226 try testing.expectEqualStrings(":cmd14", recent.?);
227}