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

feat: Added ability to copy folders.

+4 -1
CHANGELOG.md
··· 1 # Changelog 2 3 ## v0.9.8 (2025-04-04) 4 - fix: Ensure complete Git branch is displayed. 5 - refactor: Audit try usage to improve system resiliance. 6 - refactor: Removed need for enum based notifications. 7 8 ## v0.9.7 (2025-04-01) 9 - - feat: Added ability to copy folders. 10 This is done by (y)anking the file, then (p)asting in the desired directory. 11 This action can be (u)ndone and behind the scenes is a deletion. 12 - fix: Allow the cursor to be moved left and right.
··· 1 # Changelog 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. 8 - refactor: Audit try usage to improve system resiliance. 9 - refactor: Removed need for enum based notifications. 10 11 ## v0.9.7 (2025-04-01) 12 + - feat: Added ability to copy files. 13 This is done by (y)anking the file, then (p)asting in the desired directory. 14 This action can be (u)ndone and behind the scenes is a deletion. 15 - fix: Allow the cursor to be moved left and right.
+2 -2
PROJECT_BOARD.md
··· 8 ## v1.0 release 9 10 ### New features 11 - - [ ] File/Folder movement. 12 - [x] Copy files. 13 - - [ ] Copy folders. 14 - [ ] Keybind to unzip archives. 15 - [x] Keybind to hard delete items (bypass trash). 16 - [x] Ability to unbind keys.
··· 8 ## v1.0 release 9 10 ### New features 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.
+1 -1
build.zig
··· 2 const builtin = @import("builtin"); 3 4 ///Must match the `version` in `build.zig.zon`. 5 - const version = std.SemanticVersion{ .major = 0, .minor = 9, .patch = 8 }; 6 7 const targets: []const std.Target.Query = &.{ 8 .{ .cpu_arch = .aarch64, .os_tag = .macos },
··· 2 const builtin = @import("builtin"); 3 4 ///Must match the `version` in `build.zig.zon`. 5 + const version = std.SemanticVersion{ .major = 0, .minor = 9, .patch = 9 }; 6 7 const targets: []const std.Target.Query = &.{ 8 .{ .cpu_arch = .aarch64, .os_tag = .macos },
+1 -1
build.zig.zon
··· 1 .{ 2 .name = .jido, 3 .fingerprint = 0xee45eabe36cafb57, 4 - .version = "0.9.8", 5 .minimum_zig_version = "0.14.0", 6 7 .dependencies = .{
··· 1 .{ 2 .name = .jido, 3 .fingerprint = 0xee45eabe36cafb57, 4 + .version = "0.9.9", 5 .minimum_zig_version = "0.14.0", 6 7 .dependencies = .{
+106 -23
src/events.zig
··· 174 }) orelse break :lbl null; 175 176 switch (entry.kind) { 177 - .file => { 178 break :lbl .{ 179 .dir = try app.alloc.dupe(u8, app.directories.fullPath(".") catch { 180 message = try std.fmt.allocPrint( ··· 206 } 207 } 208 209 - pub fn paste(app: *App) error{OutOfMemory}!void { 210 var message: ?[]const u8 = null; 211 defer if (message) |msg| app.alloc.free(msg); 212 ··· 220 }; 221 222 switch (yanked.entry.kind) { 223 - .file => { 224 var source_dir = std.fs.openDirAbsolute(yanked.dir, .{ .iterate = true }) catch { 225 message = try std.fmt.allocPrint(app.alloc, "Failed to copy '{s}' - unable to open directory '{s}'.", .{ yanked.entry.name, yanked.dir }); 226 app.notification.write(message.?, .err) catch {}; ··· 250 }, 251 }; 252 253 - // Append action to undo history. 254 - var new_path_abs_buf: [std.fs.max_path_bytes]u8 = undefined; 255 - const new_path_abs = app.directories.dir.realpath(new_path_res.path, &new_path_abs_buf) catch { 256 - message = try std.fmt.allocPrint( 257 - app.alloc, 258 - "Failed to push copy action for '{s}' to undo history - unable to retrieve absolute directory path for '{s}'. This action will not be able to be undone via the `undo` keybind.", 259 - .{ new_path_res.path, yanked.entry.name }, 260 - ); 261 - app.notification.write(message.?, .err) catch {}; 262 - if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {}; 263 - return; 264 - }; 265 - 266 - if (app.actions.push(.{ 267 - .paste = try app.alloc.dupe(u8, new_path_abs), 268 - })) |prev_elem| { 269 - app.alloc.free(prev_elem.delete.prev_path); 270 - app.alloc.free(prev_elem.delete.new_path); 271 - } 272 - 273 message = try std.fmt.allocPrint(app.alloc, "Copied '{s}'.", .{yanked.entry.name}); 274 app.notification.write(message.?, .info) catch {}; 275 }, ··· 278 app.notification.write(message.?, .warn) catch {}; 279 return; 280 }, 281 } 282 283 try app.repopulateDirectory("");
··· 174 }) orelse break :lbl null; 175 176 switch (entry.kind) { 177 + .file, .directory, .sym_link => { 178 break :lbl .{ 179 .dir = try app.alloc.dupe(u8, app.directories.fullPath(".") catch { 180 message = try std.fmt.allocPrint( ··· 206 } 207 } 208 209 + pub fn paste(app: *App) error{ OutOfMemory, NoSpaceLeft }!void { 210 var message: ?[]const u8 = null; 211 defer if (message) |msg| app.alloc.free(msg); 212 ··· 220 }; 221 222 switch (yanked.entry.kind) { 223 + .directory => { 224 + var source_dir = std.fs.openDirAbsolute(yanked.dir, .{ .iterate = true }) catch { 225 + message = try std.fmt.allocPrint(app.alloc, "Failed to copy '{s}' - unable to open directory '{s}'.", .{ yanked.entry.name, yanked.dir }); 226 + app.notification.write(message.?, .err) catch {}; 227 + if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {}; 228 + return; 229 + }; 230 + defer source_dir.close(); 231 + 232 + var selected_dir = source_dir.openDir(yanked.entry.name, .{ .iterate = true }) catch { 233 + message = try std.fmt.allocPrint(app.alloc, "Failed to copy '{s}' - unable to open directory '{s}'.", .{ yanked.entry.name, yanked.entry.name }); 234 + app.notification.write(message.?, .err) catch {}; 235 + if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {}; 236 + return; 237 + }; 238 + defer selected_dir.close(); 239 + 240 + var walker = selected_dir.walk(app.alloc) catch |err| { 241 + message = try std.fmt.allocPrint(app.alloc, "Failed to copy '{s}' - unable to walk directory tree due to {}.", .{ yanked.entry.name, err }); 242 + app.notification.write(message.?, .err) catch {}; 243 + if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {}; 244 + return; 245 + }; 246 + defer walker.deinit(); 247 + 248 + // Make initial dir. 249 + app.directories.dir.makeDir(new_path_res.path) catch |err| { 250 + message = try std.fmt.allocPrint(app.alloc, "Failed to copy '{s}' - unable to create new directory due to {}.", .{ yanked.entry.name, err }); 251 + app.notification.write(message.?, .err) catch {}; 252 + if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {}; 253 + return; 254 + }; 255 + 256 + var errored = false; 257 + var inner_path_buf: [std.fs.max_path_bytes]u8 = undefined; 258 + while (walker.next() catch |err| { 259 + message = try std.fmt.allocPrint(app.alloc, "Failed to copy one or more files - {}. A partial copy may have taken place.", .{err}); 260 + app.notification.write(message.?, .err) catch {}; 261 + if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {}; 262 + return; 263 + }) |entry| { 264 + const path = try std.fmt.bufPrint(&inner_path_buf, "{s}{s}{s}", .{ new_path_res.path, std.fs.path.sep_str, entry.path }); 265 + switch (entry.kind) { 266 + .directory => { 267 + app.directories.dir.makeDir(path) catch { 268 + message = try std.fmt.allocPrint(app.alloc, "Failed to copy '{s}' - unable to create containing directory '{s}'.", .{ entry.basename, path }); 269 + app.notification.write(message.?, .err) catch {}; 270 + if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {}; 271 + errored = true; 272 + }; 273 + }, 274 + .file, .sym_link => { 275 + entry.dir.copyFile(entry.basename, app.directories.dir, path, .{}) catch |err| switch (err) { 276 + error.FileNotFound => { 277 + message = try std.fmt.allocPrint(app.alloc, "Failed to copy '{s}' - the original file was deleted or moved.", .{entry.path}); 278 + app.notification.write(message.?, .err) catch {}; 279 + if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {}; 280 + errored = true; 281 + }, 282 + else => { 283 + message = try std.fmt.allocPrint(app.alloc, "Failed to copy '{s}' - {}.", .{ entry.path, err }); 284 + app.notification.write(message.?, .err) catch {}; 285 + if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {}; 286 + errored = true; 287 + }, 288 + }; 289 + }, 290 + else => { 291 + message = try std.fmt.allocPrint(app.alloc, "Failed to copy '{s}' - unsupported file type '{}'.", .{ entry.path, entry.kind }); 292 + app.notification.write(message.?, .err) catch {}; 293 + if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {}; 294 + errored = true; 295 + }, 296 + } 297 + } 298 + 299 + if (errored) { 300 + app.notification.write("Failed to copy some items, check the log file for more details.", .err) catch {}; 301 + } else { 302 + message = try std.fmt.allocPrint(app.alloc, "Copied '{s}'.", .{yanked.entry.name}); 303 + app.notification.write(message.?, .info) catch {}; 304 + } 305 + }, 306 + .file, .sym_link => { 307 var source_dir = std.fs.openDirAbsolute(yanked.dir, .{ .iterate = true }) catch { 308 message = try std.fmt.allocPrint(app.alloc, "Failed to copy '{s}' - unable to open directory '{s}'.", .{ yanked.entry.name, yanked.dir }); 309 app.notification.write(message.?, .err) catch {}; ··· 333 }, 334 }; 335 336 message = try std.fmt.allocPrint(app.alloc, "Copied '{s}'.", .{yanked.entry.name}); 337 app.notification.write(message.?, .info) catch {}; 338 }, ··· 341 app.notification.write(message.?, .warn) catch {}; 342 return; 343 }, 344 + } 345 + 346 + // Append action to undo history. 347 + var new_path_abs_buf: [std.fs.max_path_bytes]u8 = undefined; 348 + const new_path_abs = app.directories.dir.realpath(new_path_res.path, &new_path_abs_buf) catch { 349 + message = try std.fmt.allocPrint( 350 + app.alloc, 351 + "Failed to push copy action for '{s}' to undo history - unable to retrieve absolute directory path for '{s}'. This action will not be able to be undone via the `undo` keybind.", 352 + .{ new_path_res.path, yanked.entry.name }, 353 + ); 354 + app.notification.write(message.?, .err) catch {}; 355 + if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {}; 356 + return; 357 + }; 358 + 359 + if (app.actions.push(.{ 360 + .paste = try app.alloc.dupe(u8, new_path_abs), 361 + })) |prev_elem| { 362 + app.alloc.free(prev_elem.delete.prev_path); 363 + app.alloc.free(prev_elem.delete.new_path); 364 } 365 366 try app.repopulateDirectory("");