地圖 (Jido) is a lightweight Unix TUI file explorer designed for speed and simplicity.
at v1.3.0 9.4 kB view raw
1const std = @import("std"); 2const builtin = @import("builtin"); 3const environment = @import("./environment.zig"); 4const Drawer = @import("./drawer.zig"); 5const Notification = @import("./notification.zig"); 6const config = &@import("./config.zig").config; 7const List = @import("./list.zig").List; 8const Directories = @import("./directories.zig"); 9const FileLogger = @import("./file_logger.zig"); 10const CircStack = @import("./circ_stack.zig").CircularStack; 11const zuid = @import("zuid"); 12const vaxis = @import("vaxis"); 13const Key = vaxis.Key; 14const EventHandlers = @import("./event_handlers.zig"); 15const CommandHistory = @import("./commands.zig").CommandHistory; 16 17const help_menu_items = [_][]const u8{ 18 "Global:", 19 "<CTRL-c> :Exit.", 20 "<CTRL-r> :Reload config.", 21 "", 22 "Normal mode:", 23 "j / <Down> :Go down.", 24 "k / <Up> :Go up.", 25 "h / <Left> / - :Go to the parent directory.", 26 "l / <Right> :Open item or change directory.", 27 "g :Go to the top.", 28 "G :Go to the bottom.", 29 "c :Change directory via path. Will enter input mode.", 30 "R :Rename item. Will enter input mode.", 31 "D :Delete item.", 32 "u :Undo delete/rename.", 33 "d :Create directory. Will enter input mode.", 34 "% :Create file. Will enter input mode.", 35 "/ :Fuzzy search directory. Will enter input mode.", 36 ". :Toggle hidden files.", 37 ": :Allows for Jido commands to be entered. Please refer to the ", 38 " \"Command mode\" section for available commands. Will enter ", 39 " input mode.", 40 "v :Verbose mode. Provides more information about selected entry. ", 41 "y :Yank selected item.", 42 "p :Past yanked item.", 43 "", 44 "Input mode:", 45 "<Esc> :Cancel input.", 46 "<CR> :Confirm input.", 47 "", 48 "Command mode:", 49 "<Up> / <Down> :Cycle previous commands.", 50 ":q :Exit.", 51 ":h :View available keybinds. 'q' to return to app.", 52 ":config :Navigate to config directory if it exists.", 53 ":trash :Navigate to trash directory if it exists.", 54 ":empty_trash :Empty trash if it exists. This action cannot be undone.", 55 ":cd <path> :Change directory via path. Will enter input mode.", 56}; 57 58pub const State = enum { 59 normal, 60 fuzzy, 61 new_dir, 62 new_file, 63 change_dir, 64 rename, 65 command, 66 help_menu, 67}; 68 69pub const Action = union(enum) { 70 delete: struct { prev_path: []const u8, new_path: []const u8 }, 71 rename: struct { prev_path: []const u8, new_path: []const u8 }, 72 paste: []const u8, 73}; 74 75pub const Event = union(enum) { 76 image_ready, 77 key_press: Key, 78 winsize: vaxis.Winsize, 79}; 80 81pub const Image = struct { 82 const Status = enum { 83 ready, 84 processing, 85 }; 86 87 ///Only use on first transmission. Subsequent draws should use 88 ///`Image.image`. 89 data: ?vaxis.zigimg.Image = null, 90 image: ?vaxis.Image = null, 91 path: ?[]const u8 = null, 92 status: Status = .processing, 93 94 pub fn deinit(self: @This(), alloc: std.mem.Allocator) void { 95 if (self.data) |data| { 96 var d = data; 97 d.deinit(); 98 } 99 if (self.path) |path| alloc.free(path); 100 } 101}; 102 103const actions_len = 100; 104const image_cache_cap = 100; 105 106const App = @This(); 107 108alloc: std.mem.Allocator, 109should_quit: bool, 110vx: vaxis.Vaxis = undefined, 111tty: vaxis.Tty = undefined, 112loop: vaxis.Loop(Event) = undefined, 113state: State = .normal, 114actions: CircStack(Action, actions_len), 115command_history: CommandHistory = CommandHistory{}, 116drawer: Drawer = Drawer{}, 117 118help_menu: List([]const u8), 119directories: Directories, 120notification: Notification = Notification{}, 121file_logger: ?FileLogger = null, 122 123text_input: vaxis.widgets.TextInput, 124text_input_buf: [std.fs.max_path_bytes]u8 = undefined, 125 126yanked: ?struct { dir: []const u8, entry: std.fs.Dir.Entry } = null, 127last_known_height: usize, 128 129images: struct { 130 mutex: std.Thread.Mutex = .{}, 131 cache: std.StringHashMap(Image), 132}, 133 134pub fn init(alloc: std.mem.Allocator, entry_dir: ?[]const u8) !App { 135 var vx = try vaxis.init(alloc, .{ 136 .kitty_keyboard_flags = .{ 137 .report_text = false, 138 .disambiguate = false, 139 .report_events = false, 140 .report_alternate_keys = false, 141 .report_all_as_ctl_seqs = false, 142 }, 143 }); 144 145 var help_menu = List([]const u8).init(alloc); 146 try help_menu.fromArray(&help_menu_items); 147 148 var app: App = .{ 149 .alloc = alloc, 150 .should_quit = false, 151 .vx = vx, 152 .tty = try vaxis.Tty.init(), 153 .directories = try Directories.init(alloc, entry_dir), 154 .help_menu = help_menu, 155 .text_input = vaxis.widgets.TextInput.init(alloc, &vx.unicode), 156 .actions = CircStack(Action, actions_len).init(), 157 .last_known_height = vx.window().height, 158 .images = .{ .cache = .init(alloc) }, 159 }; 160 161 app.loop = vaxis.Loop(Event){ 162 .vaxis = &app.vx, 163 .tty = &app.tty, 164 }; 165 166 return app; 167} 168 169pub fn deinit(self: *App) void { 170 while (self.actions.pop()) |action| { 171 switch (action) { 172 .delete => |a| { 173 self.alloc.free(a.new_path); 174 self.alloc.free(a.prev_path); 175 }, 176 .rename => |a| { 177 self.alloc.free(a.new_path); 178 self.alloc.free(a.prev_path); 179 }, 180 .paste => |a| self.alloc.free(a), 181 } 182 } 183 184 if (self.yanked) |yanked| { 185 self.alloc.free(yanked.dir); 186 self.alloc.free(yanked.entry.name); 187 } 188 189 self.command_history.deinit(self.alloc); 190 191 self.help_menu.deinit(); 192 self.directories.deinit(); 193 self.text_input.deinit(); 194 self.vx.deinit(self.alloc, self.tty.anyWriter()); 195 self.tty.deinit(); 196 if (self.file_logger) |file_logger| file_logger.deinit(); 197 198 var image_iter = self.images.cache.iterator(); 199 while (image_iter.next()) |img| { 200 img.value_ptr.deinit(self.alloc); 201 } 202 self.images.cache.deinit(); 203} 204 205pub fn inputToSlice(self: *App) []const u8 { 206 self.text_input.buf.cursor = self.text_input.buf.realLength(); 207 return self.text_input.sliceToCursor(&self.text_input_buf); 208} 209 210pub fn repopulateDirectory(self: *App, fuzzy: []const u8) error{OutOfMemory}!void { 211 self.directories.clearEntries(); 212 self.directories.populateEntries(fuzzy) catch |err| { 213 const message = try std.fmt.allocPrint(self.alloc, "Failed to read directory entries - {}.", .{err}); 214 defer self.alloc.free(message); 215 self.notification.write(message, .err) catch {}; 216 if (self.file_logger) |file_logger| file_logger.write(message, .err) catch {}; 217 }; 218} 219 220pub fn run(self: *App) !void { 221 try self.repopulateDirectory(""); 222 try self.loop.start(); 223 defer self.loop.stop(); 224 225 try self.vx.enterAltScreen(self.tty.anyWriter()); 226 try self.vx.queryTerminal(self.tty.anyWriter(), 1 * std.time.ns_per_s); 227 self.vx.caps.kitty_graphics = true; 228 229 while (!self.should_quit) { 230 self.loop.pollEvent(); 231 while (self.loop.tryEvent()) |event| { 232 // Global keybinds. 233 try EventHandlers.handleGlobalEvent(self, event); 234 235 // State specific keybinds. 236 switch (self.state) { 237 .normal => { 238 try EventHandlers.handleNormalEvent(self, event); 239 }, 240 .help_menu => { 241 try EventHandlers.handleHelpMenuEvent(self, event); 242 }, 243 else => { 244 try EventHandlers.handleInputEvent(self, event); 245 }, 246 } 247 } 248 249 try self.drawer.draw(self); 250 251 var buffered = self.tty.bufferedWriter(); 252 try self.vx.render(buffered.writer().any()); 253 try buffered.flush(); 254 } 255 256 if (config.empty_trash_on_exit) { 257 var trash_dir = dir: { 258 notfound: { 259 break :dir (config.trashDir() catch break :notfound) orelse break :notfound; 260 } 261 if (self.file_logger) |file_logger| file_logger.write("Failed to open trash directory.", .err) catch { 262 std.log.err("Failed to open trash directory.", .{}); 263 }; 264 return; 265 }; 266 defer trash_dir.close(); 267 268 const failed = environment.deleteContents(trash_dir) catch |err| { 269 const message = try std.fmt.allocPrint(self.alloc, "Failed to empty trash - {}.", .{err}); 270 defer self.alloc.free(message); 271 if (self.file_logger) |file_logger| file_logger.write(message, .err) catch { 272 std.log.err("Failed to empty trash - {}.", .{err}); 273 }; 274 return; 275 }; 276 if (failed > 0) { 277 const message = try std.fmt.allocPrint(self.alloc, "Failed to empty {d} items from the trash.", .{failed}); 278 defer self.alloc.free(message); 279 if (self.file_logger) |file_logger| file_logger.write(message, .err) catch { 280 std.log.err("Failed to empty {d} items from the trash.", .{failed}); 281 }; 282 } 283 } 284}