Add Move/Copy/Symlink rename actions, with undo support for all three

- renamr-core::rename: new RenameAction enum (Move/Copy/Symlink, default Move,
  mirroring net.filebot.cli.CmdlineOperations's --action modes). execute_renames() now
  takes an action applied to the whole batch. Copy uses fs::copy (source untouched);
  Symlink uses std::os::unix::fs::symlink / std::os::windows::fs::symlink_file behind a
  cfg(unix)/cfg(windows) split. Same conflict policy (skip, never overwrite) and
  auto-created destination folders apply to all three actions. 8 new tests.
- renamr-core::history: HistoryBatch now records which action produced it
  (#[serde(default)] so old history.json entries - all implicitly Move - still
  deserialize). revert_one() branches on it: Move still moves the file back; Copy/
  Symlink undo by deleting the copy/symlink (using symlink_metadata so a broken link is
  still recognized as present and removable) - the original was never touched by those
  actions, so there's nothing to move back. 3 new tests.
- src-tauri: execute_rename command takes the chosen action and threads it through to
  both execute_renames and record_batch.
- Frontend: a Move/Copy/Symlink segmented selector in the "Confirm Rename" modal
  (remembered across modal opens, defaults to Move), action-aware subtitle/button
  copy, and a status message using the right verb (Renamed/Copied/Symlinked). Only Move
  reconciles a loaded item's id/name/bindings to the new path afterward, since Copy/
  Symlink leave the source file exactly where it was. History panel now shows a Copy/
  Symlink badge on non-Move batches and explains what Undo does for each.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
Renamr Scaffold
2026-08-29 04:31:16 +00:00
co-authored by Claude Sonnet 5
parent aab8e28246
commit fd4e6f66f0
7 changed files with 546 additions and 127 deletions
+14 -11
View File
@@ -5,7 +5,7 @@ mod settings;
use renamr_core::format::ExpressionFormat;
use renamr_core::matching::{self, EpisodeGuess, MovieGuess};
use renamr_core::rename::{self, RenamePlanItem, RenameResult};
use renamr_core::rename::{self, RenameAction, RenamePlanItem, RenameResult};
use renamr_core::string_bindings;
use std::collections::HashMap;
use tauri::AppHandle;
@@ -39,20 +39,23 @@ fn guess_movie(filename: String) -> Option<MovieGuess> {
matching::guess_movie(&filename)
}
/// Executes a batch of planned on-disk renames (optionally moving files into
/// subfolders - see `renamr_core::rename`; conflicting targets are always skipped,
/// never overwritten). The UI is expected to have already shown the user the full
/// old-path/new-path plan and gotten explicit confirmation before calling this.
/// Runs off the main thread since real filesystem I/O shouldn't block the UI.
/// Executes a batch of planned on-disk rename/copy/symlink operations (optionally
/// moving/copying files into subfolders - see `renamr_core::rename`; conflicting
/// targets are always skipped, never overwritten). `action` picks which of the three
/// happens - the UI is expected to have already shown the user the full old-path/
/// new-path plan, the chosen action, and gotten explicit confirmation before calling
/// this. Runs off the main thread since real filesystem I/O shouldn't block the UI.
///
/// Every successfully-renamed item is recorded to the rename history log afterward
/// (see `history::record_batch`) so it can be undone later from the History panel.
/// Every successfully-produced item is recorded to the rename history log afterward
/// (see `history::record_batch`), tagged with `action`, so it can be undone later from
/// the History panel - a Move undo moves the file back, a Copy/Symlink undo just
/// removes what was created.
#[tauri::command]
async fn execute_rename(app: AppHandle, items: Vec<RenamePlanItem>) -> Vec<RenameResult> {
let results = tauri::async_runtime::spawn_blocking(move || rename::execute_renames(&items))
async fn execute_rename(app: AppHandle, items: Vec<RenamePlanItem>, action: RenameAction) -> Vec<RenameResult> {
let results = tauri::async_runtime::spawn_blocking(move || rename::execute_renames(&items, action))
.await
.unwrap_or_default();
history::record_batch(&app, &results);
history::record_batch(&app, &results, action);
results
}