full2half 0.1.1

Library and CLI for converting full-width characters to half-width characters and vice versa.
Documentation
use clap::Parser;
use std::process::exit;
use std::path::PathBuf;
use std::fs::{read_to_string, write};
use full2half::CharacterWidth;

#[derive(Parser, Debug)]
#[command(version, override_help = HELP, version = "v0.1.1", arg_required_else_help = true)]
struct Args {
    /// File to do the conversion on
    #[arg(short = 'f', long)]
    file: Option<String>,

    /// Copy file instead of overwriting it
    #[arg(short = 'c', long, default_value_t = false)]
    copy: bool,

    /// Activate / Deactivate alpha numeric conversion
    #[arg(short = 'a', long, default_value_t = false)]
    alpha: bool,

    /// Activate / Deactivate symbol conversion
    #[arg(short = 's', long, default_value_t = false)]
    symbols: bool,

    /// Activate / Deactivate kana conversion
    #[arg(short = 'j', long, default_value_t = false)]
    kana: bool,

    /// Activate / Deactivate hangul conversion
    #[arg(short = 'k', long, default_value_t = false)]
    hangul: bool,

    /// Set specific characters to ignore
    #[arg(short = 'i', long, value_delimiter = ' ', num_args = 0..)]
    ignore: Option<Vec<String>>,

    /// Set conversion from halfwidth to fullwidth
    #[arg(short = 'r', long, default_value_t = false)]
    reverse: bool,

    /// String to do the conversion on
    #[arg()]
    string: Option<String>,
}

// Invisible Character (U+3164) in first line, since the first spaces are ignored
const HELP: &str = r#"ㅤ
 ________ ___  ___  ___       ___        _______  ___  ___  ________  ___       ________ 
|\  _____\\  \|\  \|\  \     |\  \      /  ___  \|\  \|\  \|\   __  \|\  \     |\  _____\
\ \  \__/\ \  \\\  \ \  \    \ \  \    /__/|_/  /\ \  \\\  \ \  \|\  \ \  \    \ \  \__/ 
 \ \   __\\ \  \\\  \ \  \    \ \  \   |__|//  / /\ \   __  \ \   __  \ \  \    \ \   __\
  \ \  \_| \ \  \\\  \ \  \____\ \  \____  /  /_/__\ \  \ \  \ \  \ \  \ \  \____\ \  \_|
   \ \__\   \ \_______\ \_______\ \_______\\________\ \__\ \__\ \__\ \__\ \_______\ \__\ 
    \|__|    \|_______|\|_______|\|_______|\|_______|\|__|\|__|\|__|\|__|\|_______|\|__| 

------------------------------------------------------------------------------------------

     CLI for converting full-width characters to half-width characters and vice versa.    

                        https://gitlab.com/pSchwietzer/full2half 

------------------------------------------------------------------------------------------

  USAGE:
    full2half [OPTIONS] <STRING>
    full2half [OPTIONS] -f <FILE>

  ARGUMENTS:
      <STRING>    String to convert, only if no file is specified
      <FILE>      File to convert, only if no string is specified

  OPTIONS:
    -f, --file          File to do the conversion on
    -c, --copy          Copy file instead of overwriting it
    -a, --alpha         Activate / Deactivate alpha numeric conversion
                        [default: true]
    -s, --symbols       Activate / Deactivate symbol conversion
                        [default: true]
    -j, --kana          Activate / Deactivate kana conversion
                        [default: true]
    -k, --hangul        Activate / Deactivate hangul conversion
                        [default: true]
    -i, --ignore        Set specific characters to ignore,
                        Characters must be part of alpha, symbols, kana or hangul sets.
    -r, --reverse       Set conversion from halfwidth to fullwidth
                        [default: false]

------------------------------------------------------------------------------------------
"#;


fn main() {
    let args = Args::parse();

    if args.string.is_some() && args.file.is_some() {
        eprintln!("You can't convert a file and string at the same time. Try only using the '-f' argument without a string or not using a string with the '-f' argument.");
        exit(1);
    }

    let ignore: Vec<&str> = match args.ignore {
        Some(ref values) => values.iter().map(|x| x.as_str()).collect(),
        None => Vec::new(),
    };

    if args.string.is_some() {
        let result = match args.reverse {
            true => args.string.unwrap().to_full_width_ext(ignore.clone(), !args.alpha, !args.symbols, !args.kana, !args.hangul),
            false => args.string.unwrap().to_half_width_ext(ignore.clone(), !args.alpha, !args.symbols, !args.kana, !args.hangul),
        };

        if result.is_ok() {
            println!("{}", result.unwrap());
            exit(0);
        } else {
            eprintln!("{:?}", result.err().unwrap());
            exit(1);
        }
    }

    if args.file.is_some() {
        let file_path = PathBuf::from(args.file.unwrap());

        if !file_path.exists() && !file_path.is_file() {
            eprintln!("'{:?}' file does not exist.", file_path);
            exit(1);
        }

        let original_content = match read_to_string(&file_path) {
            Ok(content) => content,
            Err(err) => {
                eprintln!("{}", err);
                exit(1);
            }
        };

        let converted_content = match args.reverse {
            true => original_content.to_full_width_ext(ignore.clone(), !args.alpha, !args.symbols, !args.kana, !args.hangul),
            false => original_content.to_half_width_ext(ignore.clone(), !args.alpha, !args.symbols, !args.kana, !args.hangul),
        };

        let file_path = if args.copy {
            find_file_copy_name(&file_path).unwrap_or_else(|err| {
                eprintln!("Failed to find a suitable name for the copy: {}", err);
                exit(1);
            })
        } else {
            file_path
        };

        if let Err(err) = converted_content {
            eprintln!("{:?}", err);
            exit(1);
        }
        
        if let Err(err) = write(&file_path, converted_content.unwrap()) {
            eprintln!("{}", err);
            exit(1);
        } else {
            println!("Successfully converted and written to file: {:?}.", file_path);
            exit(0);
        }
    }

    eprintln!("No string or file specified. Try using the '-f' argument with a file or a string without the '-f' argument.");
    exit(1);
}

/// This function finds a suitable name for the copy of a file.
///
/// It follows the pattern: <original_name> (<number>).<extension>
///
/// # Arguments
///
/// * `original_path` - PathBuf of the original file
///
/// # Errors
///
/// If the file does not exist or is not a file.
///
/// # Returns
///
/// A Result containing a PathBuf of the available name for the copy.
fn find_file_copy_name(original_path: &PathBuf) -> Result<PathBuf, String> {
    let mut counter = 1;
    let max_counter = 999;

    if !original_path.exists() {
        return Err(format!("File '{:?}' does not exist.", original_path));
    }

    if !original_path.is_file() {
        return Err(format!("'{:?}' is not a file.", original_path));
    }

    while counter <= max_counter {
        let mut copy_name = format!(
            "{} ({})",
            original_path.file_stem().unwrap().to_string_lossy(),
            counter
        );

        if original_path.extension().is_some() {
            copy_name.push_str(&format!(".{}", original_path.extension().unwrap().to_string_lossy()));
        }

        let copy_path = original_path.with_file_name(copy_name);

        if !copy_path.exists() {
            return Ok(copy_path);
        }

        counter += 1;
    }

    Err(format!("Exceeded maximum attempts to find a unique copy name. Maximum attempts: {}", max_counter))
}