rskiller 0.2.1

Find and clean Rust project build artifacts and caches with parallel processing
Documentation
use clap::{Parser, ValueEnum};
use clap_complete::Shell;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use crate::config::Config;

#[derive(Parser, Clone, Debug)]
#[command(
    name = "rskiller",
    about = "Find and clean Rust project build artifacts and caches"
)]
pub struct Cli {
    /// Directory to start searching from
    #[arg(short, long)]
    pub directory: Option<PathBuf>,

    /// Search from user's home directory
    #[arg(short = 'f', long)]
    pub full: bool,

    /// Target directories to search for (default: target)
    #[arg(short, long)]
    pub target: Option<String>,

    /// Sort results by size, path, or last modified
    #[arg(short, long, value_enum)]
    pub sort: Option<SortBy>,

    /// Show sizes in gigabytes instead of megabytes
    #[arg(long)]
    pub gb: bool,

    /// Exclude directories from search (comma-separated)
    #[arg(short = 'E', long)]
    pub exclude: Option<String>,

    /// Exclude hidden directories
    #[arg(short = 'x', long)]
    pub exclude_hidden: bool,

    /// Hide errors
    #[arg(short = 'e', long)]
    pub hide_errors: bool,

    /// Automatically delete all found directories
    #[arg(short = 'D', long)]
    pub delete_all: bool,

    /// Dry run - don't actually delete anything
    #[arg(long)]
    pub dry_run: bool,

    /// Just list projects without interactive mode
    #[arg(short, long)]
    pub list_only: bool,

    /// Show additional Rust-specific directories (registry cache, git cache, etc.)
    #[arg(long)]
    pub include_cargo_cache: bool,

    /// Color scheme for the interface
    #[arg(short = 'c', long, value_enum)]
    pub color: Option<Color>,

    /// Don't check for updates
    #[arg(long)]
    pub no_check_update: bool,

    /// Use specific configuration file
    #[arg(long)]
    pub config: Option<PathBuf>,

    /// Print effective configuration (after merging)
    #[arg(long)]
    pub print_config: bool,

    /// Generate sample configuration file
    #[arg(long)]
    pub init_config: bool,

    /// Configuration file format for --init-config
    #[arg(long, value_enum, default_value = "toml")]
    pub format: ConfigFormat,

    /// Generate global user configuration instead of project-local
    #[arg(long)]
    pub global: bool,

    /// Validate configuration file
    #[arg(long)]
    pub validate_config: Option<PathBuf>,

    /// Show configuration file locations and precedence
    #[clap(long)]
    pub config_info: bool,

    /// Generate shell completions
    #[clap(long, value_enum)]
    pub completion: Option<Shell>,
}

#[derive(ValueEnum, Clone, Debug, Serialize, Deserialize, PartialEq)]
pub enum SortBy {
    Size,
    Path,
    LastMod,
}

#[derive(ValueEnum, Clone, Debug, Serialize, Deserialize, PartialEq)]
pub enum Color {
    Blue,
    Cyan,
    Magenta,
    White,
    Red,
    Yellow,
}

#[derive(ValueEnum, Clone, Debug)]
pub enum ConfigFormat {
    Toml,
    Json,
}

impl From<ConfigFormat> for crate::config::ConfigFormat {
    fn from(format: ConfigFormat) -> Self {
        match format {
            ConfigFormat::Toml => crate::config::ConfigFormat::Toml,
            ConfigFormat::Json => crate::config::ConfigFormat::Json,
        }
    }
}

impl Cli {
    /// Create CLI from configuration with command line overrides
    pub fn from_config_and_args(config: Config) -> Self {
        let mut cli = Self::parse();
        
        // Apply config defaults where CLI args are not provided
        if cli.directory.is_none() {
            cli.directory = config.search.directory;
        }
        
        if !cli.full && config.search.full.unwrap_or(false) {
            cli.full = true;
        }
        
        if cli.target.is_none() {
            cli.target = config.search.target;
        }
        
        if cli.sort.is_none() {
            cli.sort = config.output.sort;
        }
        
        if !cli.gb && config.output.gb.unwrap_or(false) {
            cli.gb = true;
        }
        
        if cli.exclude.is_none() && config.search.exclude.is_some() {
            cli.exclude = config.search.exclude.map(|dirs| dirs.join(","));
        }
        
        if !cli.exclude_hidden && config.search.exclude_hidden.unwrap_or(false) {
            cli.exclude_hidden = true;
        }
        
        if !cli.hide_errors && config.output.hide_errors.unwrap_or(false) {
            cli.hide_errors = true;
        }
        
        if !cli.delete_all && config.behavior.delete_all.unwrap_or(false) {
            cli.delete_all = true;
        }
        
        if !cli.dry_run && config.behavior.dry_run.unwrap_or(false) {
            cli.dry_run = true;
        }
        
        if !cli.list_only && config.behavior.list_only.unwrap_or(false) {
            cli.list_only = true;
        }
        
        if !cli.include_cargo_cache && config.search.include_cargo_cache.unwrap_or(false) {
            cli.include_cargo_cache = true;
        }
        
        if cli.color.is_none() {
            cli.color = config.output.color;
        }
        
        if !cli.no_check_update && config.behavior.no_check_update.unwrap_or(false) {
            cli.no_check_update = true;
        }
        
        cli
    }

    pub fn get_search_directory(&self) -> PathBuf {
        if self.full {
            dirs::home_dir().unwrap_or_else(|| PathBuf::from("/"))
        } else {
            self.directory.clone().unwrap_or_else(|| {
                dirs::home_dir().unwrap_or_else(|| PathBuf::from("."))
            })
        }
    }

    pub fn get_excluded_dirs(&self) -> Vec<String> {
        self.exclude
            .as_ref()
            .map(|s| {
                s.split(',')
                    .map(|dir| dir.trim().to_string())
                    .collect()
            })
            .unwrap_or_default()
    }

    pub fn get_target_name(&self) -> String {
        self.target.clone().unwrap_or_else(|| "target".to_string())
    }

    pub fn get_sort_by(&self) -> SortBy {
        self.sort.clone().unwrap_or(SortBy::Size)
    }

    pub fn get_color(&self) -> Color {
        self.color.clone().unwrap_or(Color::Blue)
    }
}