diff --git a/CHANGELOG.md b/CHANGELOG.md index 5312f1a..8353bef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,15 @@ # Changelog +## v0.9.9 (2025-04-06) +- feat: Added ability to copy folders. + ## v0.9.8 (2025-04-04) - fix: Ensure complete Git branch is displayed. - refactor: Audit try usage to improve system resiliance. - refactor: Removed need for enum based notifications. ## v0.9.7 (2025-04-01) -- feat: Added ability to copy folders. +- feat: Added ability to copy files. This is done by (y)anking the file, then (p)asting in the desired directory. This action can be (u)ndone and behind the scenes is a deletion. - fix: Allow the cursor to be moved left and right. diff --git a/PROJECT_BOARD.md b/PROJECT_BOARD.md index 3f0d44c..0ecb4c3 100644 --- a/PROJECT_BOARD.md +++ b/PROJECT_BOARD.md @@ -8,9 +8,9 @@ Key: ## v1.0 release ### New features -- [ ] File/Folder movement. +- [x] File/Folder movement. - [x] Copy files. - - [ ] Copy folders. + - [x] Copy folders. - [ ] Keybind to unzip archives. - [x] Keybind to hard delete items (bypass trash). - [x] Ability to unbind keys. diff --git a/build.zig b/build.zig index 2712253..eac1f3b 100644 --- a/build.zig +++ b/build.zig @@ -2,7 +2,7 @@ const std = @import("std"); const builtin = @import("builtin"); ///Must match the `version` in `build.zig.zon`. -const version = std.SemanticVersion{ .major = 0, .minor = 9, .patch = 8 }; +const version = std.SemanticVersion{ .major = 0, .minor = 9, .patch = 9 }; const targets: []const std.Target.Query = &.{ .{ .cpu_arch = .aarch64, .os_tag = .macos }, diff --git a/build.zig.zon b/build.zig.zon index 0d70f85..53085aa 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,7 +1,7 @@ .{ .name = .jido, .fingerprint = 0xee45eabe36cafb57, - .version = "0.9.8", + .version = "0.9.9", .minimum_zig_version = "0.14.0", .dependencies = .{ diff --git a/src/events.zig b/src/events.zig index ea18cf9..b35b2bc 100644 --- a/src/events.zig +++ b/src/events.zig @@ -174,7 +174,7 @@ pub fn yank(app: *App) error{OutOfMemory}!void { }) orelse break :lbl null; switch (entry.kind) { - .file => { + .file, .directory, .sym_link => { break :lbl .{ .dir = try app.alloc.dupe(u8, app.directories.fullPath(".") catch { message = try std.fmt.allocPrint( @@ -206,7 +206,7 @@ pub fn yank(app: *App) error{OutOfMemory}!void { } } -pub fn paste(app: *App) error{OutOfMemory}!void { +pub fn paste(app: *App) error{ OutOfMemory, NoSpaceLeft }!void { var message: ?[]const u8 = null; defer if (message) |msg| app.alloc.free(msg); @@ -220,7 +220,90 @@ pub fn paste(app: *App) error{OutOfMemory}!void { }; switch (yanked.entry.kind) { - .file => { + .directory => { + var source_dir = std.fs.openDirAbsolute(yanked.dir, .{ .iterate = true }) catch { + message = try std.fmt.allocPrint(app.alloc, "Failed to copy '{s}' - unable to open directory '{s}'.", .{ yanked.entry.name, yanked.dir }); + app.notification.write(message.?, .err) catch {}; + if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {}; + return; + }; + defer source_dir.close(); + + var selected_dir = source_dir.openDir(yanked.entry.name, .{ .iterate = true }) catch { + message = try std.fmt.allocPrint(app.alloc, "Failed to copy '{s}' - unable to open directory '{s}'.", .{ yanked.entry.name, yanked.entry.name }); + app.notification.write(message.?, .err) catch {}; + if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {}; + return; + }; + defer selected_dir.close(); + + var walker = selected_dir.walk(app.alloc) catch |err| { + message = try std.fmt.allocPrint(app.alloc, "Failed to copy '{s}' - unable to walk directory tree due to {}.", .{ yanked.entry.name, err }); + app.notification.write(message.?, .err) catch {}; + if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {}; + return; + }; + defer walker.deinit(); + + // Make initial dir. + app.directories.dir.makeDir(new_path_res.path) catch |err| { + message = try std.fmt.allocPrint(app.alloc, "Failed to copy '{s}' - unable to create new directory due to {}.", .{ yanked.entry.name, err }); + app.notification.write(message.?, .err) catch {}; + if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {}; + return; + }; + + var errored = false; + var inner_path_buf: [std.fs.max_path_bytes]u8 = undefined; + while (walker.next() catch |err| { + message = try std.fmt.allocPrint(app.alloc, "Failed to copy one or more files - {}. A partial copy may have taken place.", .{err}); + app.notification.write(message.?, .err) catch {}; + if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {}; + return; + }) |entry| { + const path = try std.fmt.bufPrint(&inner_path_buf, "{s}{s}{s}", .{ new_path_res.path, std.fs.path.sep_str, entry.path }); + switch (entry.kind) { + .directory => { + app.directories.dir.makeDir(path) catch { + message = try std.fmt.allocPrint(app.alloc, "Failed to copy '{s}' - unable to create containing directory '{s}'.", .{ entry.basename, path }); + app.notification.write(message.?, .err) catch {}; + if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {}; + errored = true; + }; + }, + .file, .sym_link => { + entry.dir.copyFile(entry.basename, app.directories.dir, path, .{}) catch |err| switch (err) { + error.FileNotFound => { + message = try std.fmt.allocPrint(app.alloc, "Failed to copy '{s}' - the original file was deleted or moved.", .{entry.path}); + app.notification.write(message.?, .err) catch {}; + if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {}; + errored = true; + }, + else => { + message = try std.fmt.allocPrint(app.alloc, "Failed to copy '{s}' - {}.", .{ entry.path, err }); + app.notification.write(message.?, .err) catch {}; + if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {}; + errored = true; + }, + }; + }, + else => { + message = try std.fmt.allocPrint(app.alloc, "Failed to copy '{s}' - unsupported file type '{}'.", .{ entry.path, entry.kind }); + app.notification.write(message.?, .err) catch {}; + if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {}; + errored = true; + }, + } + } + + if (errored) { + app.notification.write("Failed to copy some items, check the log file for more details.", .err) catch {}; + } else { + message = try std.fmt.allocPrint(app.alloc, "Copied '{s}'.", .{yanked.entry.name}); + app.notification.write(message.?, .info) catch {}; + } + }, + .file, .sym_link => { var source_dir = std.fs.openDirAbsolute(yanked.dir, .{ .iterate = true }) catch { message = try std.fmt.allocPrint(app.alloc, "Failed to copy '{s}' - unable to open directory '{s}'.", .{ yanked.entry.name, yanked.dir }); app.notification.write(message.?, .err) catch {}; @@ -250,26 +333,6 @@ pub fn paste(app: *App) error{OutOfMemory}!void { }, }; - // Append action to undo history. - var new_path_abs_buf: [std.fs.max_path_bytes]u8 = undefined; - const new_path_abs = app.directories.dir.realpath(new_path_res.path, &new_path_abs_buf) catch { - message = try std.fmt.allocPrint( - app.alloc, - "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.", - .{ new_path_res.path, yanked.entry.name }, - ); - app.notification.write(message.?, .err) catch {}; - if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {}; - return; - }; - - if (app.actions.push(.{ - .paste = try app.alloc.dupe(u8, new_path_abs), - })) |prev_elem| { - app.alloc.free(prev_elem.delete.prev_path); - app.alloc.free(prev_elem.delete.new_path); - } - message = try std.fmt.allocPrint(app.alloc, "Copied '{s}'.", .{yanked.entry.name}); app.notification.write(message.?, .info) catch {}; }, @@ -280,6 +343,26 @@ pub fn paste(app: *App) error{OutOfMemory}!void { }, } + // Append action to undo history. + var new_path_abs_buf: [std.fs.max_path_bytes]u8 = undefined; + const new_path_abs = app.directories.dir.realpath(new_path_res.path, &new_path_abs_buf) catch { + message = try std.fmt.allocPrint( + app.alloc, + "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.", + .{ new_path_res.path, yanked.entry.name }, + ); + app.notification.write(message.?, .err) catch {}; + if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {}; + return; + }; + + if (app.actions.push(.{ + .paste = try app.alloc.dupe(u8, new_path_abs), + })) |prev_elem| { + app.alloc.free(prev_elem.delete.prev_path); + app.alloc.free(prev_elem.delete.new_path); + } + try app.repopulateDirectory(""); app.text_input.clearAndFree(); }