地圖 (Jido) is a lightweight Unix TUI file explorer designed for speed and simplicity.
at main 9.5 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 notification, 78 key_press: Key, 79 winsize: vaxis.Winsize, 80}; 81 82pub const Image = struct { 83 const Status = enum { 84 ready, 85 processing, 86 failed, 87 }; 88 89 ///Only use on first transmission. Subsequent draws should use 90 ///`Image.image`. 91 data: ?vaxis.zigimg.Image = null, 92 image: ?vaxis.Image = null, 93 path: ?[]const u8 = null, 94 status: Status = .processing, 95 96 pub fn deinit(self: @This(), alloc: std.mem.Allocator, vx: vaxis.Vaxis, tty: *vaxis.Tty) void { 97 if (self.image) |image| { 98 vx.freeImage(tty.writer(), image.id); 99 } 100 if (self.data) |data| { 101 var d = data; 102 d.deinit(alloc); 103 } 104 if (self.path) |path| alloc.free(path); 105 } 106}; 107 108const actions_len = 100; 109const image_cache_cap = 100; 110 111const App = @This(); 112 113alloc: std.mem.Allocator, 114should_quit: bool, 115vx: vaxis.Vaxis = undefined, 116tty_buffer: [1024]u8 = undefined, 117tty: vaxis.Tty = undefined, 118loop: vaxis.Loop(Event) = undefined, 119state: State = .normal, 120actions: CircStack(Action, actions_len), 121command_history: CommandHistory = CommandHistory{}, 122drawer: Drawer = Drawer{}, 123 124help_menu: List([]const u8), 125directories: Directories, 126notification: Notification = Notification{}, 127file_logger: ?FileLogger = null, 128 129text_input: vaxis.widgets.TextInput, 130text_input_buf: [std.fs.max_path_bytes]u8 = undefined, 131 132yanked: ?struct { dir: []const u8, entry: std.fs.Dir.Entry } = null, 133last_known_height: usize, 134 135images: struct { 136 mutex: std.Thread.Mutex = .{}, 137 cache: std.StringHashMap(Image), 138}, 139 140pub fn init(alloc: std.mem.Allocator, entry_dir: ?[]const u8) !App { 141 var vx = try vaxis.init(alloc, .{ 142 .kitty_keyboard_flags = .{ 143 .report_text = false, 144 .disambiguate = false, 145 .report_events = false, 146 .report_alternate_keys = false, 147 .report_all_as_ctl_seqs = false, 148 }, 149 }); 150 151 var help_menu = List([]const u8).init(alloc); 152 try help_menu.fromArray(&help_menu_items); 153 154 var app: App = .{ 155 .alloc = alloc, 156 .should_quit = false, 157 .vx = vx, 158 .directories = try Directories.init(alloc, entry_dir), 159 .help_menu = help_menu, 160 .text_input = vaxis.widgets.TextInput.init(alloc), 161 .actions = CircStack(Action, actions_len).init(), 162 .last_known_height = vx.window().height, 163 .images = .{ .cache = .init(alloc) }, 164 }; 165 app.tty = try vaxis.Tty.init(&app.tty_buffer); 166 app.loop = vaxis.Loop(Event){ 167 .vaxis = &app.vx, 168 .tty = &app.tty, 169 }; 170 171 return app; 172} 173 174pub fn deinit(self: *App) void { 175 while (self.actions.pop()) |action| { 176 switch (action) { 177 .delete => |a| { 178 self.alloc.free(a.new_path); 179 self.alloc.free(a.prev_path); 180 }, 181 .rename => |a| { 182 self.alloc.free(a.new_path); 183 self.alloc.free(a.prev_path); 184 }, 185 .paste => |a| self.alloc.free(a), 186 } 187 } 188 189 if (self.yanked) |yanked| { 190 self.alloc.free(yanked.dir); 191 self.alloc.free(yanked.entry.name); 192 } 193 194 self.command_history.deinit(self.alloc); 195 196 self.help_menu.deinit(); 197 self.directories.deinit(); 198 self.text_input.deinit(); 199 self.vx.deinit(self.alloc, self.tty.writer()); 200 self.tty.deinit(); 201 if (self.file_logger) |file_logger| file_logger.deinit(); 202 203 var image_iter = self.images.cache.iterator(); 204 while (image_iter.next()) |img| { 205 img.value_ptr.deinit(self.alloc, self.vx, &self.tty); 206 } 207 self.images.cache.deinit(); 208} 209 210pub fn inputToSlice(self: *App) []const u8 { 211 self.text_input.buf.cursor = self.text_input.buf.realLength(); 212 return self.text_input.sliceToCursor(&self.text_input_buf); 213} 214 215pub fn repopulateDirectory(self: *App, fuzzy: []const u8) error{OutOfMemory}!void { 216 self.directories.clearEntries(); 217 self.directories.populateEntries(fuzzy) catch |err| { 218 const message = try std.fmt.allocPrint(self.alloc, "Failed to read directory entries - {}.", .{err}); 219 defer self.alloc.free(message); 220 self.notification.write(message, .err) catch {}; 221 if (self.file_logger) |file_logger| file_logger.write(message, .err) catch {}; 222 }; 223} 224 225pub fn run(self: *App) !void { 226 try self.repopulateDirectory(""); 227 try self.loop.start(); 228 defer self.loop.stop(); 229 230 try self.vx.enterAltScreen(self.tty.writer()); 231 try self.vx.queryTerminal(self.tty.writer(), 1 * std.time.ns_per_s); 232 self.vx.caps.kitty_graphics = true; 233 234 while (!self.should_quit) { 235 self.loop.pollEvent(); 236 while (self.loop.tryEvent()) |event| { 237 // Global keybinds. 238 try EventHandlers.handleGlobalEvent(self, event); 239 240 // State specific keybinds. 241 switch (self.state) { 242 .normal => { 243 try EventHandlers.handleNormalEvent(self, event); 244 }, 245 .help_menu => { 246 try EventHandlers.handleHelpMenuEvent(self, event); 247 }, 248 else => { 249 try EventHandlers.handleInputEvent(self, event); 250 }, 251 } 252 } 253 254 try self.drawer.draw(self); 255 256 try self.vx.render(self.tty.writer()); 257 } 258 259 if (config.empty_trash_on_exit) { 260 var trash_dir = dir: { 261 notfound: { 262 break :dir (config.trashDir() catch break :notfound) orelse break :notfound; 263 } 264 if (self.file_logger) |file_logger| file_logger.write("Failed to open trash directory.", .err) catch { 265 std.log.err("Failed to open trash directory.", .{}); 266 }; 267 return; 268 }; 269 defer trash_dir.close(); 270 271 const failed = environment.deleteContents(trash_dir) catch |err| { 272 const message = try std.fmt.allocPrint(self.alloc, "Failed to empty trash - {}.", .{err}); 273 defer self.alloc.free(message); 274 if (self.file_logger) |file_logger| file_logger.write(message, .err) catch { 275 std.log.err("Failed to empty trash - {}.", .{err}); 276 }; 277 return; 278 }; 279 if (failed > 0) { 280 const message = try std.fmt.allocPrint(self.alloc, "Failed to empty {d} items from the trash.", .{failed}); 281 defer self.alloc.free(message); 282 if (self.file_logger) |file_logger| file_logger.write(message, .err) catch { 283 std.log.err("Failed to empty {d} items from the trash.", .{failed}); 284 }; 285 } 286 } 287}