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

fix: Scrolling command history now provides the correct values.

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