地圖 (Jido) is a lightweight Unix TUI file explorer designed for speed and simplicity.

feat: allow user to customize keybinds.

+29 -11
README.md
··· 28 28 - A terminal supporting the `kitty image protocol` to view images. 29 29 30 30 ## Key manual 31 + Below are the default keybinds. Keybinds can be overwritten via the `Keybinds` 32 + config option. See [Configuration](#configuration) for more information. 33 + 31 34 ``` 32 35 Global: 33 36 <CTRL-c> :Exit. ··· 65 68 :cd <path> :Change directory via path. Will enter input mode. 66 69 ``` 67 70 68 - 69 71 ## Configuration 70 72 Configure `jido` by editing the external configuration file located at either: 71 73 - `$HOME/.jido/config.json` ··· 84 86 .show_images: bool, 85 87 .preview_file: bool, 86 88 .empty_trash_on_exit: bool, 87 - .styles: Styles, 89 + .keybinds: Keybinds, 90 + .styles: Styles 91 + } 92 + 93 + Keybinds = struct { 94 + .toggle_hidden_files: Char, 95 + .delete: Char, 96 + .rename: Char, 97 + .create_dir: Char, 98 + .create_file: Char, 99 + .fuzzy_find: Char, 100 + .change_dir: Char, 101 + .enter_command_mode: Char 102 + .jump_top: Char 103 + .jump_bottom: Char 88 104 } 89 105 90 106 NotificationStyles = struct { 91 - box: vaxis.Style, 92 - err: vaxis.Style, 93 - warn: vaxis.Style, 94 - info: vaxis.Style, 95 - }; 107 + .box: vaxis.Style, 108 + .err: vaxis.Style, 109 + .warn: vaxis.Style, 110 + .info: vaxis.Style 111 + } 96 112 97 113 Styles = struct { 98 114 .selected_list_item: Style, ··· 100 116 .file_name: Style, 101 117 .file_information: Style 102 118 .notification: NotificationStyles, 103 - .git_branch: Style, 119 + .git_branch: Style 104 120 } 105 121 106 122 Style = struct { ··· 113 129 double, 114 130 curly, 115 131 dotted, 116 - dashed, 132 + dashed 117 133 } 118 134 .bold: bool, 119 135 .dim: bool, ··· 121 137 .blink: bool, 122 138 .reverse: bool, 123 139 .invisible: bool, 124 - .strikethrough: bool, 140 + .strikethrough: bool 125 141 } 126 142 127 143 Color = enum{ 128 144 default, 129 145 index: u8, 130 - rgb: [3]u8, 146 + rgb: [3]u8 131 147 } 148 + 149 + Char = enum(u21) 132 150 ``` 133 151 134 152 ## Contributing
+3
example-config.json
··· 3 3 "sort_dirs": false, 4 4 "show_images": true, 5 5 "preview_file": true, 6 + "keybinds": { 7 + "toggle_hidden_files": "h" 8 + }, 6 9 "styles": { 7 10 "selected_list_item": { 8 11 "bg": {
+57 -4
src/config.zig
··· 17 17 empty_trash_on_exit: bool = false, 18 18 // TODO(10-01-25): This needs to be implemented. 19 19 // command_history_len: usize = 10, 20 - styles: Styles = Styles{}, 20 + styles: Styles = .{}, 21 + keybinds: Keybinds = .{}, 21 22 22 23 config_dir: ?std.fs.Dir = null, 23 24 ··· 51 52 try home_dir.makeDir(XDG_CONFIG_HOME_DIR_NAME); 52 53 } 53 54 54 - const jido_dir = try home_dir.openDir(XDG_CONFIG_HOME_DIR_NAME, .{ .iterate = true }); 55 + const jido_dir = try home_dir.openDir( 56 + XDG_CONFIG_HOME_DIR_NAME, 57 + .{ .iterate = true }, 58 + ); 55 59 self.config_dir = jido_dir; 56 60 57 61 if (environment.fileExists(jido_dir, CONFIG_NAME)) { ··· 70 74 try home_dir.makeDir(HOME_DIR_NAME); 71 75 } 72 76 73 - const jido_dir = try home_dir.openDir(HOME_DIR_NAME, .{ .iterate = true }); 77 + const jido_dir = try home_dir.openDir( 78 + HOME_DIR_NAME, 79 + .{ .iterate = true }, 80 + ); 74 81 self.config_dir = jido_dir; 75 82 76 83 if (environment.fileExists(jido_dir, CONFIG_NAME)) { ··· 93 100 94 101 self.* = parsed_config.value; 95 102 self.config_dir = dir; 103 + 104 + // Check duplicate keybinds 105 + { 106 + var key_map = std.AutoHashMap(u21, []const u8).init(alloc); 107 + defer { 108 + var it = key_map.iterator(); 109 + while (it.next()) |entry| { 110 + alloc.free(entry.value_ptr.*); 111 + } 112 + key_map.deinit(); 113 + } 114 + 115 + inline for (std.meta.fields(Keybinds)) |field| { 116 + const codepoint = @intFromEnum(@field(self.keybinds, field.name)); 117 + 118 + const res = try key_map.getOrPut(codepoint); 119 + if (res.found_existing) { 120 + return error.DuplicateKeybind; 121 + } 122 + res.value_ptr.* = try alloc.dupe(u8, field.name); 123 + } 124 + } 125 + 96 126 return; 97 127 } 98 128 }; ··· 125 155 }, 126 156 }; 127 157 158 + pub const Keybinds = struct { 159 + pub const Char = enum(u21) { 160 + _, 161 + pub fn jsonParse(alloc: std.mem.Allocator, source: anytype, options: std.json.ParseOptions) !@This() { 162 + const parsed = try std.json.innerParse([]const u8, alloc, source, options); 163 + if (std.mem.eql(u8, parsed, "")) return error.InvalidCharacter; 164 + const unicode = std.unicode.utf8Decode(parsed) catch return error.InvalidCharacter; 165 + return @enumFromInt(unicode); 166 + } 167 + }; 168 + 169 + toggle_hidden_files: Char = @enumFromInt('.'), 170 + delete: Char = @enumFromInt('D'), 171 + rename: Char = @enumFromInt('R'), 172 + create_dir: Char = @enumFromInt('d'), 173 + create_file: Char = @enumFromInt('%'), 174 + fuzzy_find: Char = @enumFromInt('/'), 175 + change_dir: Char = @enumFromInt('c'), 176 + enter_command_mode: Char = @enumFromInt(':'), 177 + jump_top: Char = @enumFromInt('g'), 178 + jump_bottom: Char = @enumFromInt('G'), 179 + }; 180 + 128 181 const Styles = struct { 129 182 selected_list_item: vaxis.Style = vaxis.Style{ 130 183 .bg = .{ .rgb = Colours.grey }, ··· 144 197 }, 145 198 }; 146 199 147 - pub var config: Config = Config{ .styles = Styles{} }; 200 + pub var config: Config = Config{};
+257 -237
src/event_handlers.zig
··· 6 6 const Key = vaxis.Key; 7 7 const config = &@import("./config.zig").config; 8 8 const commands = @import("./commands.zig"); 9 + const Keybinds = @import("./config.zig").Keybinds; 9 10 10 11 pub fn inputToSlice(self: *App) []const u8 { 11 12 self.text_input.buf.cursor = self.text_input.buf.realLength(); ··· 19 20 ) !void { 20 21 switch (event) { 21 22 .key_press => |key| { 22 - switch (key.codepoint) { 23 - '-', 'h', Key.left => { 24 - app.text_input.clearAndFree(); 23 + @setEvalBranchQuota( 24 + std.meta.fields(Keybinds).len * 1000, 25 + ); 26 + 27 + const maybe_remap: ?std.meta.FieldEnum(Keybinds) = lbl: { 28 + inline for (std.meta.fields(Keybinds)) |field| { 29 + if (key.codepoint == @intFromEnum(@field(config.keybinds, field.name))) { 30 + break :lbl comptime std.meta.stringToEnum(std.meta.FieldEnum(Keybinds), field.name) orelse unreachable; 31 + } 32 + } 33 + break :lbl null; 34 + }; 35 + 36 + if (maybe_remap) |action| { 37 + switch (action) { 38 + .toggle_hidden_files => { 39 + config.show_hidden = !config.show_hidden; 40 + 41 + const prev_selected_name: []const u8, const prev_selected_err: bool = lbl: { 42 + const selected = app.directories.getSelected() catch break :lbl .{ "", true }; 43 + if (selected == null) break :lbl .{ "", true }; 25 44 26 - if (app.directories.dir.openDir("../", .{ .iterate = true })) |dir| { 27 - app.directories.dir.close(); 28 - app.directories.dir = dir; 45 + break :lbl .{ try app.alloc.dupe(u8, selected.?.name), false }; 46 + }; 47 + defer if (!prev_selected_err) app.alloc.free(prev_selected_name); 29 48 30 49 app.directories.clearEntries(); 31 - const fuzzy = inputToSlice(app); 32 - app.directories.populateEntries(fuzzy) catch |err| { 50 + app.text_input.clearAndFree(); 51 + app.directories.populateEntries("") catch |err| { 33 52 switch (err) { 34 53 error.AccessDenied => try app.notification.writeErr(.PermissionDenied), 35 54 else => try app.notification.writeErr(.UnknownError), 36 55 } 37 56 }; 38 57 39 - if (app.directories.history.pop()) |history| { 40 - if (history.selected < app.directories.entries.len()) { 41 - app.directories.entries.selected = history.selected; 42 - app.directories.entries.offset = history.offset; 43 - } 58 + for (app.directories.entries.all()) |entry| { 59 + // Update offset as we search for last selected entry. 60 + app.directories.entries.updateOffset(app.last_known_height, .next); 61 + if (std.mem.eql(u8, entry.name, prev_selected_name)) return; 62 + app.directories.entries.selected += 1; 44 63 } 45 - } else |err| { 46 - switch (err) { 47 - error.AccessDenied => try app.notification.writeErr(.PermissionDenied), 48 - else => try app.notification.writeErr(.UnknownError), 49 - } 50 - } 51 - }, 52 - Key.enter, 'l', Key.right => { 53 - const entry = lbl: { 54 - const entry = app.directories.getSelected() catch return; 55 - if (entry) |e| break :lbl e else return; 56 - }; 57 64 58 - switch (entry.kind) { 59 - .directory => { 60 - app.text_input.clearAndFree(); 65 + // If it didn't find entry, reset selected. 66 + app.directories.entries.selected = 0; 67 + }, 68 + .delete => { 69 + const entry = lbl: { 70 + const entry = app.directories.getSelected() catch { 71 + try app.notification.writeErr(.UnableToDelete); 72 + return; 73 + }; 74 + if (entry) |e| break :lbl e else return; 75 + }; 61 76 62 - if (app.directories.dir.openDir(entry.name, .{ .iterate = true })) |dir| { 63 - app.directories.dir.close(); 64 - app.directories.dir = dir; 77 + var old_path_buf: [std.fs.max_path_bytes]u8 = undefined; 78 + const old_path = try app.alloc.dupe(u8, try app.directories.dir.realpath(entry.name, &old_path_buf)); 65 79 66 - _ = app.directories.history.push(.{ 67 - .selected = app.directories.entries.selected, 68 - .offset = app.directories.entries.offset, 69 - }); 80 + var trash_dir = dir: { 81 + notfound: { 82 + break :dir (config.trashDir() catch break :notfound) orelse break :notfound; 83 + } 84 + app.alloc.free(old_path); 85 + try app.notification.writeErr(.ConfigPathNotFound); 86 + return; 87 + }; 88 + defer trash_dir.close(); 89 + var trash_dir_path_buf: [std.fs.max_path_bytes]u8 = undefined; 90 + const trash_dir_path = try trash_dir.realpath(".", &trash_dir_path_buf); 70 91 71 - app.directories.clearEntries(); 72 - const fuzzy = inputToSlice(app); 73 - app.directories.populateEntries(fuzzy) catch |err| { 74 - switch (err) { 75 - error.AccessDenied => try app.notification.writeErr(.PermissionDenied), 76 - else => try app.notification.writeErr(.UnknownError), 77 - } 78 - }; 79 - } else |err| { 80 - switch (err) { 81 - error.AccessDenied => try app.notification.writeErr(.PermissionDenied), 82 - else => try app.notification.writeErr(.UnknownError), 83 - } 92 + if (std.mem.eql(u8, old_path, trash_dir_path)) { 93 + try app.notification.writeErr(.CannotDeleteTrashDir); 94 + app.alloc.free(old_path); 95 + return; 96 + } 97 + 98 + var tmp_path_buf: [std.fs.max_path_bytes]u8 = undefined; 99 + const tmp_path = try app.alloc.dupe(u8, try std.fmt.bufPrint(&tmp_path_buf, "{s}/{s}-{s}", .{ trash_dir_path, entry.name, zuid.new.v4() })); 100 + 101 + if (app.directories.dir.rename(entry.name, tmp_path)) { 102 + if (app.actions.push(.{ 103 + .delete = .{ .old = old_path, .new = tmp_path }, 104 + })) |prev_elem| { 105 + app.alloc.free(prev_elem.delete.old); 106 + app.alloc.free(prev_elem.delete.new); 84 107 } 85 - }, 86 - .file => { 87 - if (environment.getEditor()) |editor| { 88 - try app.vx.exitAltScreen(app.tty.anyWriter()); 89 - try app.vx.resetState(app.tty.anyWriter()); 90 - loop.stop(); 91 108 92 - environment.openFile(app.alloc, app.directories.dir, entry.name, editor) catch { 93 - try app.notification.writeErr(.UnableToOpenFile); 94 - }; 109 + try app.notification.writeInfo(.Deleted); 110 + app.directories.removeSelected(); 111 + } else |err| { 112 + switch (err) { 113 + error.RenameAcrossMountPoints => try app.notification.writeErr(.UnableToDeleteAcrossMountPoints), 114 + else => try app.notification.writeErr(.UnableToDelete), 115 + } 116 + app.alloc.free(old_path); 117 + app.alloc.free(tmp_path); 118 + } 119 + }, 120 + .rename => { 121 + app.text_input.clearAndFree(); 122 + app.state = .rename; 95 123 96 - try loop.start(); 97 - try app.vx.enterAltScreen(app.tty.anyWriter()); 98 - try app.vx.enableDetectedFeatures(app.tty.anyWriter()); 99 - app.vx.queueRefresh(); 100 - } else { 101 - try app.notification.writeErr(.EditorNotSet); 124 + const entry = lbl: { 125 + const entry = app.directories.getSelected() catch { 126 + app.state = .normal; 127 + try app.notification.writeErr(.UnableToRename); 128 + return; 129 + }; 130 + if (entry) |e| break :lbl e else { 131 + app.state = .normal; 132 + return; 102 133 } 103 - }, 104 - else => {}, 105 - } 106 - }, 107 - 'j', Key.down => { 108 - app.directories.entries.next(app.last_known_height); 109 - }, 110 - 'k', Key.up => { 111 - app.directories.entries.previous(app.last_known_height); 112 - }, 113 - 'G' => { 114 - app.directories.entries.selectLast(app.last_known_height); 115 - }, 116 - 'g' => app.directories.entries.selectFirst(), 117 - 'D' => { 118 - const entry = lbl: { 119 - const entry = app.directories.getSelected() catch { 120 - try app.notification.writeErr(.UnableToDelete); 134 + }; 135 + 136 + app.text_input.insertSliceAtCursor(entry.name) catch { 137 + app.state = .normal; 138 + try app.notification.writeErr(.UnableToRename); 121 139 return; 122 140 }; 123 - if (entry) |e| break :lbl e else return; 124 - }; 125 - 126 - var old_path_buf: [std.fs.max_path_bytes]u8 = undefined; 127 - const old_path = try app.alloc.dupe(u8, try app.directories.dir.realpath(entry.name, &old_path_buf)); 128 - 129 - var trash_dir = dir: { 130 - notfound: { 131 - break :dir (config.trashDir() catch break :notfound) orelse break :notfound; 132 - } 133 - app.alloc.free(old_path); 134 - try app.notification.writeErr(.ConfigPathNotFound); 135 - return; 136 - }; 137 - defer trash_dir.close(); 138 - var trash_dir_path_buf: [std.fs.max_path_bytes]u8 = undefined; 139 - const trash_dir_path = try trash_dir.realpath(".", &trash_dir_path_buf); 141 + }, 142 + .create_dir => { 143 + app.text_input.clearAndFree(); 144 + app.directories.clearEntries(); 145 + app.directories.populateEntries("") catch |err| { 146 + switch (err) { 147 + error.AccessDenied => try app.notification.writeErr(.PermissionDenied), 148 + else => try app.notification.writeErr(.UnknownError), 149 + } 150 + }; 151 + app.state = .new_dir; 152 + }, 153 + .create_file => { 154 + app.text_input.clearAndFree(); 155 + app.directories.clearEntries(); 156 + app.directories.populateEntries("") catch |err| { 157 + switch (err) { 158 + error.AccessDenied => try app.notification.writeErr(.PermissionDenied), 159 + else => try app.notification.writeErr(.UnknownError), 160 + } 161 + }; 162 + app.state = .new_file; 163 + }, 164 + .fuzzy_find => { 165 + app.text_input.clearAndFree(); 166 + app.state = .fuzzy; 167 + }, 168 + .change_dir => { 169 + app.text_input.clearAndFree(); 170 + app.state = .change_dir; 171 + }, 172 + .enter_command_mode => { 173 + app.text_input.clearAndFree(); 174 + app.text_input.insertSliceAtCursor(":") catch {}; 175 + app.state = .command; 176 + }, 177 + .jump_bottom => { 178 + app.directories.entries.selectLast(app.last_known_height); 179 + }, 180 + .jump_top => app.directories.entries.selectFirst(), 181 + } 182 + } else { 183 + switch (key.codepoint) { 184 + '-', 'h', Key.left => { 185 + app.text_input.clearAndFree(); 140 186 141 - if (std.mem.eql(u8, old_path, trash_dir_path)) { 142 - try app.notification.writeErr(.CannotDeleteTrashDir); 143 - app.alloc.free(old_path); 144 - return; 145 - } 187 + if (app.directories.dir.openDir("../", .{ .iterate = true })) |dir| { 188 + app.directories.dir.close(); 189 + app.directories.dir = dir; 146 190 147 - var tmp_path_buf: [std.fs.max_path_bytes]u8 = undefined; 148 - const tmp_path = try app.alloc.dupe(u8, try std.fmt.bufPrint(&tmp_path_buf, "{s}/{s}-{s}", .{ trash_dir_path, entry.name, zuid.new.v4().toArray() })); 191 + app.directories.clearEntries(); 192 + const fuzzy = inputToSlice(app); 193 + app.directories.populateEntries(fuzzy) catch |err| { 194 + switch (err) { 195 + error.AccessDenied => try app.notification.writeErr(.PermissionDenied), 196 + else => try app.notification.writeErr(.UnknownError), 197 + } 198 + }; 149 199 150 - if (app.directories.dir.rename(entry.name, tmp_path)) { 151 - if (app.actions.push(.{ 152 - .delete = .{ .old = old_path, .new = tmp_path }, 153 - })) |prev_elem| { 154 - app.alloc.free(prev_elem.delete.old); 155 - app.alloc.free(prev_elem.delete.new); 200 + if (app.directories.history.pop()) |history| { 201 + if (history.selected < app.directories.entries.len()) { 202 + app.directories.entries.selected = history.selected; 203 + app.directories.entries.offset = history.offset; 204 + } 205 + } 206 + } else |err| { 207 + switch (err) { 208 + error.AccessDenied => try app.notification.writeErr(.PermissionDenied), 209 + else => try app.notification.writeErr(.UnknownError), 210 + } 156 211 } 212 + }, 213 + Key.enter, 'l', Key.right => { 214 + const entry = lbl: { 215 + const entry = app.directories.getSelected() catch return; 216 + if (entry) |e| break :lbl e else return; 217 + }; 157 218 158 - try app.notification.writeInfo(.Deleted); 159 - app.directories.removeSelected(); 160 - } else |err| { 161 - switch (err) { 162 - error.RenameAcrossMountPoints => try app.notification.writeErr(.UnableToDeleteAcrossMountPoints), 163 - else => try app.notification.writeErr(.UnableToDelete), 164 - } 165 - app.alloc.free(old_path); 166 - app.alloc.free(tmp_path); 167 - } 168 - }, 169 - 'd' => { 170 - app.text_input.clearAndFree(); 171 - app.directories.clearEntries(); 172 - app.directories.populateEntries("") catch |err| { 173 - switch (err) { 174 - error.AccessDenied => try app.notification.writeErr(.PermissionDenied), 175 - else => try app.notification.writeErr(.UnknownError), 176 - } 177 - }; 178 - app.state = .new_dir; 179 - }, 180 - '%' => { 181 - app.text_input.clearAndFree(); 182 - app.directories.clearEntries(); 183 - app.directories.populateEntries("") catch |err| { 184 - switch (err) { 185 - error.AccessDenied => try app.notification.writeErr(.PermissionDenied), 186 - else => try app.notification.writeErr(.UnknownError), 187 - } 188 - }; 189 - app.state = .new_file; 190 - }, 191 - 'u' => { 192 - if (app.actions.pop()) |action| { 193 - const selected = app.directories.entries.selected; 219 + switch (entry.kind) { 220 + .directory => { 221 + app.text_input.clearAndFree(); 222 + 223 + if (app.directories.dir.openDir(entry.name, .{ .iterate = true })) |dir| { 224 + app.directories.dir.close(); 225 + app.directories.dir = dir; 194 226 195 - switch (action) { 196 - .delete => |a| { 197 - defer app.alloc.free(a.new); 198 - defer app.alloc.free(a.old); 227 + _ = app.directories.history.push(.{ 228 + .selected = app.directories.entries.selected, 229 + .offset = app.directories.entries.offset, 230 + }); 199 231 200 - // TODO: Will overwrite an item if it has the same name. 201 - if (app.directories.dir.rename(a.new, a.old)) { 202 232 app.directories.clearEntries(); 203 233 const fuzzy = inputToSlice(app); 204 234 app.directories.populateEntries(fuzzy) catch |err| { ··· 207 237 else => try app.notification.writeErr(.UnknownError), 208 238 } 209 239 }; 210 - try app.notification.writeInfo(.RestoredDelete); 211 - } else |_| { 212 - try app.notification.writeErr(.UnableToUndo); 240 + } else |err| { 241 + switch (err) { 242 + error.AccessDenied => try app.notification.writeErr(.PermissionDenied), 243 + else => try app.notification.writeErr(.UnknownError), 244 + } 213 245 } 214 246 }, 215 - .rename => |a| { 216 - defer app.alloc.free(a.new); 217 - defer app.alloc.free(a.old); 247 + .file => { 248 + if (environment.getEditor()) |editor| { 249 + try app.vx.exitAltScreen(app.tty.anyWriter()); 250 + try app.vx.resetState(app.tty.anyWriter()); 251 + loop.stop(); 218 252 219 - // TODO: Will overwrite an item if it has the same name. 220 - if (app.directories.dir.rename(a.new, a.old)) { 221 - app.directories.clearEntries(); 222 - const fuzzy = inputToSlice(app); 223 - app.directories.populateEntries(fuzzy) catch |err| { 224 - switch (err) { 225 - error.AccessDenied => try app.notification.writeErr(.PermissionDenied), 226 - else => try app.notification.writeErr(.UnknownError), 227 - } 253 + environment.openFile(app.alloc, app.directories.dir, entry.name, editor) catch { 254 + try app.notification.writeErr(.UnableToOpenFile); 228 255 }; 229 - try app.notification.writeInfo(.RestoredRename); 230 - } else |_| { 231 - try app.notification.writeErr(.UnableToUndo); 256 + 257 + try loop.start(); 258 + try app.vx.enterAltScreen(app.tty.anyWriter()); 259 + try app.vx.enableDetectedFeatures(app.tty.anyWriter()); 260 + app.vx.queueRefresh(); 261 + } else { 262 + try app.notification.writeErr(.EditorNotSet); 232 263 } 233 264 }, 265 + else => {}, 234 266 } 267 + }, 268 + 'j', Key.down => { 269 + app.directories.entries.next(app.last_known_height); 270 + }, 271 + 'k', Key.up => { 272 + app.directories.entries.previous(app.last_known_height); 273 + }, 274 + 'u' => { 275 + if (app.actions.pop()) |action| { 276 + const selected = app.directories.entries.selected; 235 277 236 - app.directories.entries.selected = selected; 237 - } else { 238 - try app.notification.writeInfo(.EmptyUndo); 239 - } 240 - }, 241 - '/' => { 242 - app.text_input.clearAndFree(); 243 - app.state = .fuzzy; 244 - }, 245 - 'R' => { 246 - app.text_input.clearAndFree(); 247 - app.state = .rename; 278 + switch (action) { 279 + .delete => |a| { 280 + defer app.alloc.free(a.new); 281 + defer app.alloc.free(a.old); 248 282 249 - const entry = lbl: { 250 - const entry = app.directories.getSelected() catch { 251 - app.state = .normal; 252 - try app.notification.writeErr(.UnableToRename); 253 - return; 254 - }; 255 - if (entry) |e| break :lbl e else { 256 - app.state = .normal; 257 - return; 258 - } 259 - }; 283 + // TODO: Will overwrite an item if it has the same name. 284 + if (app.directories.dir.rename(a.new, a.old)) { 285 + app.directories.clearEntries(); 286 + const fuzzy = inputToSlice(app); 287 + app.directories.populateEntries(fuzzy) catch |err| { 288 + switch (err) { 289 + error.AccessDenied => try app.notification.writeErr(.PermissionDenied), 290 + else => try app.notification.writeErr(.UnknownError), 291 + } 292 + }; 293 + try app.notification.writeInfo(.RestoredDelete); 294 + } else |_| { 295 + try app.notification.writeErr(.UnableToUndo); 296 + } 297 + }, 298 + .rename => |a| { 299 + defer app.alloc.free(a.new); 300 + defer app.alloc.free(a.old); 260 301 261 - app.text_input.insertSliceAtCursor(entry.name) catch { 262 - app.state = .normal; 263 - try app.notification.writeErr(.UnableToRename); 264 - return; 265 - }; 266 - }, 267 - 'c' => { 268 - app.text_input.clearAndFree(); 269 - app.state = .change_dir; 270 - }, 271 - ':' => { 272 - app.text_input.clearAndFree(); 273 - app.text_input.insertSliceAtCursor(":") catch {}; 274 - app.state = .command; 275 - }, 276 - '.' => { 277 - config.show_hidden = !config.show_hidden; 302 + // TODO: Will overwrite an item if it has the same name. 303 + if (app.directories.dir.rename(a.new, a.old)) { 304 + app.directories.clearEntries(); 305 + const fuzzy = inputToSlice(app); 306 + app.directories.populateEntries(fuzzy) catch |err| { 307 + switch (err) { 308 + error.AccessDenied => try app.notification.writeErr(.PermissionDenied), 309 + else => try app.notification.writeErr(.UnknownError), 310 + } 311 + }; 312 + try app.notification.writeInfo(.RestoredRename); 313 + } else |_| { 314 + try app.notification.writeErr(.UnableToUndo); 315 + } 316 + }, 317 + } 278 318 279 - const prev_selected_name: []const u8, const prev_selected_err: bool = lbl: { 280 - const selected = app.directories.getSelected() catch break :lbl .{ "", true }; 281 - if (selected == null) break :lbl .{ "", true }; 282 - 283 - break :lbl .{ try app.alloc.dupe(u8, selected.?.name), false }; 284 - }; 285 - defer if (!prev_selected_err) app.alloc.free(prev_selected_name); 286 - 287 - app.directories.clearEntries(); 288 - app.directories.populateEntries("") catch |err| { 289 - switch (err) { 290 - error.AccessDenied => try app.notification.writeErr(.PermissionDenied), 291 - else => try app.notification.writeErr(.UnknownError), 319 + app.directories.entries.selected = selected; 320 + } else { 321 + try app.notification.writeInfo(.EmptyUndo); 292 322 } 293 - }; 294 - 295 - for (app.directories.entries.all()) |entry| { 296 - // Update offset as we search for last selected entry. 297 - app.directories.entries.updateOffset(app.last_known_height, .next); 298 - if (std.mem.eql(u8, entry.name, prev_selected_name)) return; 299 - app.directories.entries.selected += 1; 300 - } 301 - 302 - // If it didn't find entry, reset selected. 303 - app.directories.entries.selected = 0; 304 - }, 305 - else => {}, 323 + }, 324 + else => {}, 325 + } 306 326 } 307 327 }, 308 328 .winsize => |ws| try app.vx.resize(app.alloc, app.tty.anyWriter(), ws),
+6
src/main.zig
··· 30 30 error.SyntaxError => { 31 31 try app.notification.writeErr(.ConfigSyntaxError); 32 32 }, 33 + error.InvalidCharacter => { 34 + try app.notification.writeErr(.InvalidKeybind); 35 + }, 36 + error.DuplicateKeybind => { 37 + try app.notification.writeErr(.DuplicateKeybinds); 38 + }, 33 39 else => { 34 40 try app.notification.writeErr(.ConfigUnknownError); 35 41 },
+4
src/notification.zig
··· 28 28 ConfigUnknownError, 29 29 ConfigPathNotFound, 30 30 CannotDeleteTrashDir, 31 + DuplicateKeybinds, 32 + InvalidKeybind, 31 33 NotADir, 32 34 }; 33 35 ··· 82 84 .ConfigUnknownError => self.write("Could not read config due to an unknown error.", .err), 83 85 .ConfigPathNotFound => self.write("Could not read config due to unset env variables. Please set either $HOME or $XDG_CONFIG_HOME.", .err), 84 86 .CannotDeleteTrashDir => self.write("Cannot delete trash directory.", .err), 87 + .DuplicateKeybinds => self.write("Config has keybinds with the same key. This can lead to undefined behaviour. Check log file for more information.", .err), 88 + .InvalidKeybind => self.write("Config has keybind(s) with invalid key(s).", .err), 85 89 }; 86 90 } 87 91