frencli/
rename.rs

1//! Rename subcommand for actually performing file renames.
2//! 
3//! This module handles the `fren rename` command which performs the actual file renaming
4//! operations based on a preview generated by the transform command. All operations are async.
5
6use freneng::{perform_renames, FrenError, EnginePreviewResult, FileRename, log_audit_from_result};
7use freneng::history::save_history;
8use crate::ui::interactive_edit;
9use std::io::{self, Write};
10use std::env;
11use std::path::PathBuf;
12
13/// Handles the rename subcommand - actually performs the rename using the preview result.
14/// 
15/// # Arguments
16/// 
17/// * `preview_result` - The preview result from the transform command (contains the mapping)
18/// * `overwrite` - Whether to overwrite existing files
19/// * `yes` - Skip confirmation prompt (rename all)
20/// * `interactive` - Interactive mode (edit filenames individually)
21/// * `command` - Full command line for audit logging
22/// * `pattern` - Pattern used for audit logging
23/// * `enable_audit` - Whether to log to audit file (default: true)
24/// 
25/// # Returns
26/// 
27/// * `Ok(())` - Command completed successfully
28/// * `Err(FrenError)` - If renaming fails
29pub async fn handle_rename_command(
30    preview_result: EnginePreviewResult,
31    overwrite: bool,
32    yes: bool,
33    interactive: bool,
34    command: String,
35    pattern: Option<String>,
36    enable_audit: bool,
37) -> Result<(), FrenError> {
38    // Preview was already shown by transform/template --use, so we don't show it again
39
40    // Show warnings
41    if !preview_result.warnings.is_empty() {
42        println!("\nWARNINGS:");
43        for warning in &preview_result.warnings {
44            println!("  - {}", warning);
45        }
46    }
47
48    // Block if empty names
49    if preview_result.has_empty_names {
50        eprintln!("\nERROR: One or more files would have an empty name. Renaming aborted.");
51        eprintln!("Please check your pattern and ensure it generates valid filenames.");
52        std::process::exit(1);
53    }
54
55    let mut renames = preview_result.renames;
56
57    // Interactive mode: edit filenames individually
58    if interactive {
59        if !interactive_edit(&mut renames) {
60            println!("Interactive editing cancelled.");
61            return Ok(());
62        }
63
64        // Re-validate after interactive editing
65        let has_empty = renames.iter().any(|r| r.new_name.trim().is_empty());
66        if has_empty {
67            eprintln!("\nERROR: One or more files would have an empty name. Renaming aborted.");
68            eprintln!("Please check your pattern and ensure it generates valid filenames.");
69            std::process::exit(1);
70        }
71    }
72
73    // If not --yes and not --interactive, prompt for each file
74    if !yes && !interactive {
75        renames = prompt_each_rename(&renames)?;
76        if renames.is_empty() {
77            println!("No files to rename.");
78            return Ok(());
79        }
80    }
81
82    // Apply renames
83    match perform_renames(&renames, overwrite).await {
84        Ok(execution) => {
85            // Report results
86            if !execution.skipped.is_empty() {
87                for (path, reason) in &execution.skipped {
88                    println!("Skipping {}: {}", path.display(), reason);
89                }
90            }
91            
92            if !execution.errors.is_empty() {
93                eprintln!("\nErrors:");
94                for (path, err) in &execution.errors {
95                    eprintln!("  {}: {}", path.display(), err);
96                }
97            }
98            
99            println!("\nSuccessfully renamed {} file(s).", execution.successful.len());
100            
101            // Save history
102            if let Err(e) = save_history(execution.successful.clone()).await {
103                eprintln!("Warning: Failed to save rename history: {}", e);
104            }
105            
106            // Log to audit file (if enabled)
107            if enable_audit {
108                let working_dir = env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
109                
110                if let Err(e) = log_audit_from_result(
111                    &command,
112                    pattern,
113                    working_dir,
114                    &execution,
115                ).await {
116                    eprintln!("Warning: Failed to write audit log: {}", e);
117                }
118            }
119            
120            Ok(())
121        }
122        Err(e) => {
123            eprintln!("Error performing renames: {}", e);
124            Err(e)
125        }
126    }
127}
128
129/// Prompts the user for each rename operation.
130/// 
131/// Returns a filtered list of renames to apply based on user choices.
132fn prompt_each_rename(renames: &[FileRename]) -> Result<Vec<FileRename>, FrenError> {
133    println!("\nConfirm each rename (y=yes, s=skip, a=apply all remaining, q=abort):");
134    println!("{:-<80}", "");
135    
136    let mut to_apply = Vec::new();
137    let mut apply_all = false;
138    
139    for (i, rename) in renames.iter().enumerate() {
140        if apply_all {
141            to_apply.push(rename.clone());
142            continue;
143        }
144        
145        let old = rename.old_path.file_name().and_then(|n| n.to_str()).unwrap_or("?");
146        let new = &rename.new_name;
147        
148        loop {
149            print!("\n[{}] {} -> {} (y/s/a/q): ", i + 1, old, new);
150            io::stdout().flush().map_err(|e| FrenError::Pattern(format!("IO error: {}", e)))?;
151            
152            let mut input = String::new();
153            io::stdin().read_line(&mut input).map_err(|e| FrenError::Pattern(format!("IO error: {}", e)))?;
154            let input = input.trim().to_lowercase();
155            
156            match input.as_str() {
157                "y" | "yes" => {
158                    to_apply.push(rename.clone());
159                    break;
160                }
161                "s" | "skip" => {
162                    // Skip this file
163                    break;
164                }
165                "a" | "all" => {
166                    // Apply all remaining
167                    to_apply.push(rename.clone());
168                    apply_all = true;
169                    break;
170                }
171                "q" | "quit" | "abort" => {
172                    println!("Aborted.");
173                    return Ok(Vec::new());
174                }
175                _ => {
176                    println!("Invalid choice. Use: y (yes), s (skip), a (all), q (abort)");
177                    continue;
178                }
179            }
180        }
181    }
182    
183    Ok(to_apply)
184}
185