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

feat: Added ability to copy folders.

+4 -1
CHANGELOG.md
··· 1 1 # Changelog 2 2 3 + ## v0.9.9 (2025-04-06) 4 + - feat: Added ability to copy folders. 5 + 3 6 ## v0.9.8 (2025-04-04) 4 7 - fix: Ensure complete Git branch is displayed. 5 8 - refactor: Audit try usage to improve system resiliance. 6 9 - refactor: Removed need for enum based notifications. 7 10 8 11 ## v0.9.7 (2025-04-01) 9 - - feat: Added ability to copy folders. 12 + - feat: Added ability to copy files. 10 13 This is done by (y)anking the file, then (p)asting in the desired directory. 11 14 This action can be (u)ndone and behind the scenes is a deletion. 12 15 - fix: Allow the cursor to be moved left and right.
+2 -2
PROJECT_BOARD.md
··· 8 8 ## v1.0 release 9 9 10 10 ### New features 11 - - [ ] File/Folder movement. 11 + - [x] File/Folder movement. 12 12 - [x] Copy files. 13 - - [ ] Copy folders. 13 + - [x] Copy folders. 14 14 - [ ] Keybind to unzip archives. 15 15 - [x] Keybind to hard delete items (bypass trash). 16 16 - [x] Ability to unbind keys.
+1 -1
build.zig
··· 2 2 const builtin = @import("builtin"); 3 3 4 4 ///Must match the `version` in `build.zig.zon`. 5 - const version = std.SemanticVersion{ .major = 0, .minor = 9, .patch = 8 }; 5 + const version = std.SemanticVersion{ .major = 0, .minor = 9, .patch = 9 }; 6 6 7 7 const targets: []const std.Target.Query = &.{ 8 8 .{ .cpu_arch = .aarch64, .os_tag = .macos },
+1 -1
build.zig.zon
··· 1 1 .{ 2 2 .name = .jido, 3 3 .fingerprint = 0xee45eabe36cafb57, 4 - .version = "0.9.8", 4 + .version = "0.9.9", 5 5 .minimum_zig_version = "0.14.0", 6 6 7 7 .dependencies = .{
+106 -23
src/events.zig
··· 174 174 }) orelse break :lbl null; 175 175 176 176 switch (entry.kind) { 177 - .file => { 177 + .file, .directory, .sym_link => { 178 178 break :lbl .{ 179 179 .dir = try app.alloc.dupe(u8, app.directories.fullPath(".") catch { 180 180 message = try std.fmt.allocPrint( ··· 206 206 } 207 207 } 208 208 209 - pub fn paste(app: *App) error{OutOfMemory}!void { 209 + pub fn paste(app: *App) error{ OutOfMemory, NoSpaceLeft }!void { 210 210 var message: ?[]const u8 = null; 211 211 defer if (message) |msg| app.alloc.free(msg); 212 212 ··· 220 220 }; 221 221 222 222 switch (yanked.entry.kind) { 223 - .file => { 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 => { 224 307 var source_dir = std.fs.openDirAbsolute(yanked.dir, .{ .iterate = true }) catch { 225 308 message = try std.fmt.allocPrint(app.alloc, "Failed to copy '{s}' - unable to open directory '{s}'.", .{ yanked.entry.name, yanked.dir }); 226 309 app.notification.write(message.?, .err) catch {}; ··· 250 333 }, 251 334 }; 252 335 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 336 message = try std.fmt.allocPrint(app.alloc, "Copied '{s}'.", .{yanked.entry.name}); 274 337 app.notification.write(message.?, .info) catch {}; 275 338 }, ··· 278 341 app.notification.write(message.?, .warn) catch {}; 279 342 return; 280 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); 281 364 } 282 365 283 366 try app.repopulateDirectory("");