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

fix: Scrolling command history now provides the correct values.

+1
CHANGELOG.md
··· 2 2 3 3 ## v0.9.9 (2025-04-06) 4 4 - feat: Added ability to copy folders. 5 + - fix: Scrolling command history now provides the correct values. 5 6 6 7 ## v0.9.8 (2025-04-04) 7 8 - fix: Ensure complete Git branch is displayed.
+6 -4
PROJECT_BOARD.md
··· 11 11 - [x] File/Folder movement. 12 12 - [x] Copy files. 13 13 - [x] Copy folders. 14 - - [ ] Keybind to unzip archives. 15 14 - [x] Keybind to hard delete items (bypass trash). 16 15 - [x] Ability to unbind keys. 17 16 ··· 19 18 - [x] Better error logging. 20 19 There are many places errors could be caught, logged, and handled instead 21 20 of crashing. 21 + 22 + ### Bugs 23 + - [x] Command history is skipping items on scroll. 24 + 25 + ## Backlog 22 26 - [ ] Improve image reading. 23 27 Current reading can be slow which pauses users movement if they are simply 24 28 scrolling past. 25 - 26 - ### Bugs 27 - - [ ] Command history is skipping items on scroll. 29 + - [ ] Keybind to unzip archives.
+1 -4
src/app.zig
··· 149 149 self.alloc.free(yanked.entry.name); 150 150 } 151 151 152 - self.command_history.resetSelected(); 153 - while (self.command_history.next()) |command| { 154 - self.alloc.free(command); 155 - } 152 + self.command_history.deinit(self.alloc); 156 153 157 154 self.help_menu.deinit(); 158 155 self.directories.deinit();
+43 -23
src/commands.zig
··· 6 6 pub const CommandHistory = struct { 7 7 const history_len = 10; 8 8 9 - selected: usize = 0, 10 - len: usize = 0, 11 9 history: [history_len][]const u8 = undefined, 10 + count: usize = 0, 11 + ///Points to the oldest entry. 12 + start: usize = 0, 13 + cursor: ?usize = null, 12 14 13 - pub fn push(self: *CommandHistory, command: []const u8) ?[]const u8 { 14 - var deleted: ?[]const u8 = null; 15 - if (self.len == history_len) { 16 - deleted = self.history[0]; 17 - for (0..self.len - 1) |i| { 18 - self.history[i] = self.history[i + 1]; 19 - } 15 + pub fn deinit(self: *CommandHistory, allocator: std.mem.Allocator) void { 16 + for (self.history[0..self.count]) |entry| { 17 + allocator.free(entry); 18 + } 19 + } 20 + 21 + pub fn add(self: *CommandHistory, cmd: []const u8, allocator: std.mem.Allocator) error{OutOfMemory}!void { 22 + const index = (self.start + self.count) % history_len; 23 + 24 + if (self.count < history_len) { 25 + self.count += 1; 20 26 } else { 21 - self.len += 1; 27 + // Overwriting the oldest entry. 28 + allocator.free(self.history[self.start]); 29 + self.start = (self.start + 1) % history_len; 22 30 } 23 31 24 - self.history[self.len - 1] = command; 25 - self.selected = self.len; 32 + self.history[index] = try allocator.dupe(u8, cmd); 33 + self.cursor = null; 34 + } 26 35 27 - return deleted; 36 + pub fn previous(self: *CommandHistory) ?[]const u8 { 37 + if (self.count == 0) return null; 38 + 39 + if (self.cursor == null) { 40 + self.cursor = self.count - 1; 41 + } else if (self.cursor.? > 0) { 42 + self.cursor.? -= 1; 43 + } 44 + 45 + return self.getAtCursor(); 28 46 } 29 47 30 48 pub fn next(self: *CommandHistory) ?[]const u8 { 31 - if (self.selected == 0) return null; 32 - self.selected -= 1; 33 - return self.history[self.selected]; 34 - } 49 + if (self.count == 0 or self.cursor == null) return null; 50 + 51 + if (self.cursor.? < self.count - 1) { 52 + self.cursor.? += 1; 53 + return self.getAtCursor(); 54 + } 35 55 36 - pub fn previous(self: *CommandHistory) ?[]const u8 { 37 - if (self.selected + 1 == self.len) return null; 38 - self.selected += 1; 39 - return self.history[self.selected]; 56 + self.cursor = null; 57 + return null; 40 58 } 41 59 42 - pub fn resetSelected(self: *CommandHistory) void { 43 - self.selected = self.len; 60 + fn getAtCursor(self: *CommandHistory) ?[]const u8 { 61 + if (self.cursor == null) return null; 62 + const index = (self.start + self.cursor.?) % history_len; 63 + return self.history[index]; 44 64 } 45 65 }; 46 66
+9 -7
src/event_handlers.zig
··· 104 104 try app.repopulateDirectory(""); 105 105 app.text_input.clearAndFree(); 106 106 }, 107 - .command => app.command_history.resetSelected(), 107 + .command => app.command_history.cursor = null, 108 108 else => {}, 109 109 } 110 110 ··· 127 127 128 128 // Push command to history if it's not empty. 129 129 if (!std.mem.eql(u8, std.mem.trim(u8, command, " "), ":")) { 130 - if (app.command_history.push(try app.alloc.dupe(u8, command))) |deleted| { 131 - app.alloc.free(deleted); 132 - } 130 + app.command_history.add(command, app.alloc) catch |err| { 131 + const message = try std.fmt.allocPrint(app.alloc, "Failed to add command to history - {}.", .{err}); 132 + defer app.alloc.free(message); 133 + if (app.file_logger) |file_logger| file_logger.write(message, .err) catch {}; 134 + }; 133 135 } 134 136 135 137 supported: { ··· 167 169 try app.text_input.insertSliceAtCursor(":UnsupportedCommand"); 168 170 } 169 171 170 - app.command_history.resetSelected(); 172 + app.command_history.cursor = null; 171 173 }, 172 174 else => {}, 173 175 } ··· 179 181 Key.right => app.text_input.cursorRight(), 180 182 Key.up => { 181 183 if (app.state == .command) { 182 - if (app.command_history.next()) |command| { 184 + if (app.command_history.previous()) |command| { 183 185 app.text_input.clearAndFree(); 184 186 app.text_input.insertSliceAtCursor(command) catch |err| { 185 187 const message = try std.fmt.allocPrint(app.alloc, "Failed to get previous command history - {}.", .{err}); ··· 193 195 Key.down => { 194 196 if (app.state == .command) { 195 197 app.text_input.clearAndFree(); 196 - if (app.command_history.previous()) |command| { 198 + if (app.command_history.next()) |command| { 197 199 app.text_input.insertSliceAtCursor(command) catch |err| { 198 200 const message = try std.fmt.allocPrint(app.alloc, "Failed to get next command history - {}.", .{err}); 199 201 defer app.alloc.free(message);