地圖 (Jido) is a lightweight Unix TUI file explorer designed for speed and simplicity.
at main 227 lines 8.0 kB view raw
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}