frs 0.4.2

Rename files with RegEx patterns
Documentation
use crate::cli::Cli;
use crate::fs::RenameInfo;
use cli_table::{
    format::{Border, Justify, Separator},
    print_stdout, Cell as TableCell, Table,
};
use colored::Colorize;
use std::cell::Cell;
use terminal_size::{terminal_size, Width};

#[cfg(test)]
#[path = "./stats_test.rs"]
pub mod stats_test;

macro_rules! add_info {
    ($infos:ident, $num_formater:ident, $info_name:literal, $elem:expr, $icon:expr, $color:ident) => {
        let count = $elem.get();
        if count != 0 {
            $infos.push(vec![
                format!("{}{}", $icon.$color(), $info_name).cell(),
                $num_formater
                    .format(count as f64)
                    .$color()
                    .cell()
                    .justify(Justify::Right),
            ]);
        }
    };
}

pub struct Stats {
    show_renames: bool,
    show_summary: bool,
    operation_mode: String,
    base_path: String,
    errors: Cell<u32>,
    renamed_files: Cell<u32>,
    renamed_directories: Cell<u32>,
    renamed_symlinks: Cell<u32>,
    middle_col: usize,
    max_indent: Cell<usize>,
    rename_arrow: String,
    error_icon: String,
    file_icon: String,
    dir_icon: String,
    symlink_icon: String,
}

impl Default for Stats {
    fn default() -> Self {
        Self {
            show_renames: false,
            show_summary: false,
            operation_mode: String::new(),
            base_path: String::new(),
            errors: Cell::new(0),
            renamed_files: Cell::new(0),
            renamed_directories: Cell::new(0),
            renamed_symlinks: Cell::new(0),
            rename_arrow: "=>".to_string(),
            middle_col: 0,
            max_indent: Cell::new(0),
            error_icon: String::new(),
            file_icon: String::new(),
            dir_icon: String::new(),
            symlink_icon: String::new(),
        }
    }
}

impl Stats {
    pub fn new() -> Self {
        let middle_col = match terminal_size() {
            Some((Width(w), _)) => (w as usize / 2).saturating_sub(2),
            None => 0,
        };

        Self {
            middle_col,
            ..Default::default()
        }
    }

    pub fn set_cli_opts(&mut self, opts: &Cli) {
        self.show_renames = opts.verbose >= 2;
        self.show_summary = opts.verbose >= 1;
        self.base_path = opts.base_path.to_string_lossy().to_string();
        if opts.run {
            self.operation_mode = "RUN".to_string();
        } else if opts.dry_run {
            self.operation_mode = "DRY-RUN".to_string();
        }
        if opts.icons {
            self.rename_arrow = "\u{21d2}".to_string();
            self.error_icon = "\u{f00d} ".to_string();
            self.file_icon = "\u{f15b} ".to_string();
            self.dir_icon = "\u{f07c} ".to_string();
            self.symlink_icon = "\u{f481} ".to_string();
        }
    }

    pub fn error(&self, error: &dyn std::fmt::Display) {
        eprintln!("{} {}!", "Error:".bright_red(), error);
        if self.show_summary {
            self.errors.set(self.errors.get() + 1);
        }
    }

    pub fn rename(&self, rename_info: &RenameInfo) {
        if self.show_renames {
            let old_path = rename_info.old_file.path.to_string_lossy();
            self.max_indent
                .set(self.max_indent.get().max(old_path.len()));
            println!(
                "{old_path} {empty:indent$}{arrow} {new_path}",
                old_path = old_path.red(),
                new_path = rename_info.new_path.to_string_lossy().green(),
                arrow = self.rename_arrow.blue(),
                empty = "",
                indent = self
                    .middle_col
                    .min(self.max_indent.get())
                    .saturating_sub(old_path.len()),
            );
        }
        if self.show_summary {
            if rename_info.old_file.file_type.is_file() {
                self.renamed_files.set(self.renamed_files.get() + 1);
            } else if rename_info.old_file.file_type.is_dir() {
                self.renamed_directories
                    .set(self.renamed_directories.get() + 1);
            } else if rename_info.old_file.file_type.is_symlink() {
                self.renamed_symlinks.set(self.renamed_symlinks.get() + 1);
            }
        }
    }

    fn has_output(&self) -> bool {
        self.errors.get() != 0
            || (self.show_renames
                && (self.renamed_files.get()
                    + self.renamed_directories.get()
                    + self.renamed_symlinks.get())
                    != 0)
    }

    pub fn print_summary(&self) {
        if !self.show_summary {
            return;
        }

        let mut num_formater = human_format::Formatter::new();
        let num_formater = num_formater.with_decimals(0).with_separator("");

        if self.has_output() {
            println!();
        }

        println!(
            "{} for {} ({}):",
            "Results".bold().bright_magenta(),
            self.base_path.underline().italic(),
            self.operation_mode
        );

        let mut infos = Vec::new();

        add_info!(
            infos,
            num_formater,
            "Errors",
            self.errors,
            self.error_icon,
            bright_red
        );
        add_info!(
            infos,
            num_formater,
            "Files",
            self.renamed_files,
            self.file_icon,
            bright_yellow
        );
        add_info!(
            infos,
            num_formater,
            "Directories",
            self.renamed_directories,
            self.dir_icon,
            blue
        );
        add_info!(
            infos,
            num_formater,
            "Symlinks",
            self.renamed_symlinks,
            self.symlink_icon,
            yellow
        );

        if infos.is_empty() {
            infos.push(vec!["Actions".cell(), 0.cell()]);
        }

        let info_table = infos
            .table()
            .border(Border::builder().build())
            .separator(Separator::builder().build());

        if let Err(error) = print_stdout(info_table) {
            self.error(&error);
        }
    }
}