// Verwende nun die Module mod cli; mod config; mod sanitizer; use anyhow::{Context, Result}; use clap::Parser; use cli::Cli; use colored::*; use config::Config; use glob::Pattern; use indicatif::{ProgressBar, ProgressStyle}; use log::{debug, error, info}; use sanitizer::{clean_filename, is_excluded, is_safe_rename, Sequence}; use std::fs; use std::io::IsTerminal; use std::path::PathBuf; use walkdir::WalkDir; // Standard-Ausschlussmuster (ähnlich wie detox) const DEFAULT_EXCLUDES: &[&str] = &[ ".git", ".git/**", ".svn", ".svn/**", "node_modules", "node_modules/**", ".cache", ".cache/**", "__pycache__", "__pycache__/**", ]; /// Repräsentiert eine geplante Umbenennungsoperation #[derive(Debug)] struct RenameOperation { old_path: PathBuf, new_path: PathBuf, } /// Prüft ob farbige Ausgabe aktiviert sein soll fn should_use_color(no_color_flag: bool) -> bool { !no_color_flag && std::io::stdout().is_terminal() } /// Startpunkt des Programms fn main() -> Result<()> { // Initialisiere Logger env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init(); // Argumente parsen let args = Cli::parse(); // Farben konfigurieren if !should_use_color(args.no_color) { colored::control::set_override(false); } // -L Option: Liste Sequences und beende if args.list_sequences { list_sequences(&args); return Ok(()); } // Sequence auswählen let sequence = if let Some(seq_name) = &args.sequence { Sequence::find(seq_name).ok_or_else(|| { anyhow::anyhow!( "Unbekannte Sequence: '{}'. Nutze -L um verfügbare Sequences anzuzeigen.", seq_name ) })? } else { Sequence::default() }; if args.verbose { info!("Verwende Sequence: {}", sequence.name); } // Config-Datei laden: entweder --conf oder Standard-Hierarchie let config = if let Some(config_path) = &args.config_file { Config::from_file(config_path, args.verbose).with_context(|| { format!( "Fehler beim Laden der Konfiguration: {}", config_path.display() ) })? } else { Config::from_default_locations(args.verbose)? }; // Ausschlussmuster (Glob-Patterns) vorbereiten - Default-Excludes + User-Excludes let mut all_excludes = DEFAULT_EXCLUDES .iter() .map(|s| s.to_string()) .collect::>(); all_excludes.extend(args.exclude.clone()); let exclude_patterns = all_excludes .iter() .map(|pattern| { Pattern::new(pattern) .with_context(|| format!("Ungültiges Ausschlussmuster: {}", pattern)) }) .collect::>>()?; if args.verbose && !exclude_patterns.is_empty() { debug!("Folgende Exclude-Pattern werden genutzt:"); for p in &exclude_patterns { debug!(" {}", p.as_str()); } } // Für alle angegebenen Pfade for path in &args.paths { // Alle Einträge sammeln, damit zuerst die tiefsten umbenannt werden let mut entries = Vec::new(); let walker = if args.recursive { // Recursive: mit optionaler max_depth let mut w = WalkDir::new(path).follow_links(false); // Symlinks nicht folgen if let Some(depth) = args.max_depth { w = w.max_depth(depth); if args.verbose { info!("Maximale Rekursionstiefe: {}", depth); } } w } else { // Non-recursive: max_depth(1) verarbeitet nur direkte Kinder WalkDir::new(path).max_depth(1).follow_links(false) }; for entry_result in walker .into_iter() .filter_entry(|e| !is_excluded(e, &exclude_patterns)) { if let Ok(entry) = entry_result { entries.push(entry); } else if let Err(e) = entry_result { error!( "{}", format!("Fehler beim Durchlaufen von {}: {}", path.display(), e).red() ); } } // Aufsteigend nach Tiefe sortieren, dann umkehren => tiefste Einträge zuerst entries.sort_by_key(|e| e.depth()); entries.reverse(); // Fortschrittsbalken bei größeren Dateimengen let progress_bar = if !args.quiet && entries.len() > 50 { let bar = ProgressBar::new(entries.len() as u64); bar.set_style(ProgressStyle::default_bar() .template("{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} ({eta}) {msg}")? .progress_chars("#>-")); Some(bar) } else { None }; // Berechne Umbenennungen // Hinweis: Die Reihenfolge (tiefste zuerst) muss erhalten bleiben, // damit Parent-Verzeichnisse nicht vor ihren Kindern umbenannt werden. let rename_ops: Vec = entries .iter() .filter_map(|entry| { let old_path = entry.path(); // Ebenentiefe 0 -> überspringen if entry.depth() == 0 && entry.file_type().is_dir() && !args.modify_root { return None; } // Special Files (Symlinks, Sockets, etc.) nur mit --special let file_type = entry.file_type(); if !args.special && (!file_type.is_file() && !file_type.is_dir()) { if args.verbose && file_type.is_symlink() { debug!("Überspringe Symlink: {} (nutze --special um Symlink-Namen zu bereinigen)", old_path.display()); } return None; } // Dateiname ermitteln und bereinigen let filename = old_path.file_name()?; let new_name = clean_filename(filename, &config, &sequence, false)?; let new_path = old_path.with_file_name(&new_name); Some(RenameOperation { old_path: old_path.to_path_buf(), new_path, }) }) .collect(); // Statistiken let total_processed = entries.len(); let total_planned = rename_ops.len(); let mut renamed_count = 0; let mut skipped_count = 0; // Umbenennungen sequenziell ausführen for op in rename_ops { if let Some(bar) = &progress_bar { bar.inc(1); } if !args.quiet { if args.dry_run { info!( "{} {} {}", op.old_path.display().to_string().dimmed(), "->".yellow(), op.new_path.display().to_string().yellow() ); } else { info!( "{} {} {}", op.old_path.display().to_string().dimmed(), "->".green(), op.new_path.display().to_string().green() ); } } if args.verbose { debug!("Rename: {:?} -> {:?}", op.old_path, op.new_path); } if !args.dry_run { if is_safe_rename(&op.old_path, &op.new_path, args.force) { match fs::rename(&op.old_path, &op.new_path) { Ok(_) => renamed_count += 1, Err(e) => { error!( "{}", format!( "Fehler beim Umbenennen: {} -> {}: {}", op.old_path.display(), op.new_path.display(), e ) .red() ); skipped_count += 1; } } } else { skipped_count += 1; } } } if let Some(bar) = &progress_bar { bar.finish_with_message("Verarbeitung abgeschlossen"); } // Zusammenfassung ausgeben (außer im quiet mode) if !args.quiet { info!(""); info!( "{}", format!("=== Zusammenfassung für {} ===", path.display()) .cyan() .bold() ); info!( "Verarbeitete Dateien/Verzeichnisse: {}", total_processed.to_string().cyan() ); info!( "Umbenennungen geplant: {}", total_planned.to_string().cyan() ); if args.dry_run { info!( "Modus: {}", "Dry-run (keine Änderungen)".yellow() ); } else { info!( "Erfolgreich umbenannt: {}", renamed_count.to_string().green().bold() ); if skipped_count > 0 { info!( "Übersprungen/Fehler: {}", skipped_count.to_string().red() ); } } } } Ok(()) } /// Listet alle verfügbaren Sequences auf fn list_sequences(args: &Cli) { println!("Verfügbare Sequences:"); println!(); for seq in Sequence::all() { println!(" {}", seq.name.bold()); if args.verbose { println!(" Description: {}", seq.description); println!( " Umlauts → ASCII: {}", if seq.apply_umlauts { "yes" } else { "no" } ); println!(" Case transform: {:?}", seq.apply_case); println!( " Emoji handling: {}", if seq.apply_emojis { "replace" } else { "keep" } ); println!( " Mode: {}", if seq.minimal_mode { "minimal" } else { "full" } ); } else { println!(" {}", seq.description); } println!(); } if !args.verbose { println!("Nutze -L -v für detaillierte Informationen über jede Sequence."); } }