tagctl 1.2.1

Adds or removes tags to given paths inside square brackets or to extended attributes
Documentation
/*
Copyright (C) 2022  Kody VB
This file is part of Tagctl.

Tagctl is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by the Free
Software Foundation, either version 3 of the License, or (at your option) any
later version.

Tagctl is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
details.

You should have received a copy of the GNU General Public License along with
Tagctl. If not, see <https://www.gnu.org/licenses/>.
*/

use crate::cli;
use crate::shared_logic;
use regex::Regex;
use std::path::{Path, PathBuf};

pub fn add_tag(path: &Path, data: &cli::Args) {
    // Adds tag to a path using existing [], or creating new [] if not already there
    if !path.exists() {
        eprintln!(
            "Error: Couldn't find expected file: \"{}\"",
            path.to_str().unwrap()
        );
        return;
    }

    let current_stem: String = path.file_stem().unwrap().to_str().unwrap().to_owned();
    let tag: String = if data.tag_varies {
        shared_logic::convert_tag_characters(path, &data.tag)
    } else {
        data.tag.clone()
    };

    if tag_in_brackets(&current_stem, &tag, &data.delimiter) {
        if data.verbose {
            println!("Ignored: {}", path.to_str().unwrap());
        }
        return;
    }

    let mut new_name: String;
    if contains_brackets(&current_stem) {
        new_name = add_tag_to_brackets(&current_stem, &tag, &data.delimiter);
    } else {
        new_name = String::from(&current_stem);
        new_name.push('[');
        new_name.push_str(&tag);
        new_name.push(']');
    }

    if path.extension().is_some() {
        new_name.push('.');
        new_name.push_str(path.extension().unwrap().to_str().unwrap());
    }

    let final_path: PathBuf = get_valid_path(path.parent().unwrap(), &new_name);
    if data.verbose {
        println!(
            "Moving: \"{}\" -> \"{}\"",
            path.to_str().unwrap(),
            final_path.to_str().unwrap()
        );
    }
    std::fs::rename(path.to_str().unwrap(), final_path.to_str().unwrap())
        .map_err(|err| {
            eprintln!(
                "Couldn't rename: \"{}\" -> \"{}\"\nerror: {}",
                path.to_str().unwrap(),
                final_path.to_str().unwrap(),
                err
            );
        })
        .ok();
}

pub fn remove_tag(path: &Path, data: &cli::Args) {
    // Removes tag from Path, removing [] if empty afterwards
    if !path.exists() {
        eprintln!(
            "Error: Couldn't find expected file: \"{}\"",
            path.to_str().unwrap()
        );
        return;
    }

    let current_stem: String = path.file_stem().unwrap().to_str().unwrap().to_owned();
    let tag: String = if data.tag_varies {
        shared_logic::convert_tag_characters(path, &data.tag)
    } else {
        data.tag.clone()
    };

    if !tag_in_brackets(&current_stem, &tag, &data.delimiter) {
        if data.verbose {
            println!("Ignored: \"{}\"", path.to_str().unwrap());
        }
        return;
    }

    let mut new_name: String = remove_tag_from_brackets(&current_stem, &tag, &data.delimiter);
    if path.extension().is_some() {
        new_name.push('.');
        new_name.push_str(path.extension().unwrap().to_str().unwrap());
    }

    let final_path: PathBuf = get_valid_path(path.parent().unwrap(), &new_name);
    if data.verbose {
        println!(
            "Moving: \"{}\" -> \"{}\"",
            path.to_str().unwrap(),
            final_path.to_str().unwrap()
        );
    }
    std::fs::rename(path.to_str().unwrap(), final_path.to_str().unwrap())
        .map_err(|err| {
            eprintln!(
                "Couldn't rename: \"{}\" -> \"{}\"\nerror: {}",
                path.to_str().unwrap(),
                final_path.to_str().unwrap(),
                err
            );
        })
        .ok();
}

pub fn remove_all_tags(path: &Path, data: &cli::Args) {
    // Removes all tags and [] from path
    if !path.exists() {
        eprintln!(
            "Error: Couldn't find expected file: \"{}\"",
            path.to_str().unwrap()
        );
        return;
    }

    let current_stem: String = path.file_stem().unwrap().to_str().unwrap().to_owned();
    if !contains_brackets(&current_stem) {
        if data.verbose {
            println!("Ignored: \"{}\"", path.to_str().unwrap());
        }
        return;
    }

    let mut new_name: String = String::new();
    let indices: (usize, usize) = get_bracket_indices(&current_stem);
    new_name.push_str(&current_stem.clone()[..indices.0]);
    new_name.push_str(&current_stem.clone()[indices.1..]);

    if path.extension().is_some() {
        new_name.push('.');
        new_name.push_str(path.extension().unwrap().to_str().unwrap());
    }

    let final_path: PathBuf = get_valid_path(path.parent().unwrap(), &new_name);
    if data.verbose {
        println!(
            "Moving: \"{}\" -> \"{}\"",
            path.to_str().unwrap(),
            final_path.to_str().unwrap()
        );
    }
    std::fs::rename(path.to_str().unwrap(), final_path.to_str().unwrap())
        .map_err(|err| {
            eprintln!(
                "Couldn't rename: \"{}\" -> \"{}\"\nerror: {}",
                path.to_str().unwrap(),
                final_path.to_str().unwrap(),
                err
            );
        })
        .ok();
}

pub fn get_valid_path(directory: &Path, name: &str) -> PathBuf {
    // If the name already exists in the directory, add ~# to the end, where # is the lowest available number
    let re = Regex::new("~[0-9]+$").unwrap();
    let mut new_name: String = String::from(name);
    if re.is_match(name) {
        let indices = re.find(name).unwrap();
        new_name = new_name[..indices.start()].to_owned();
    }
    if !directory.join(new_name.clone()).exists() {
        return directory.join(new_name);
    }

    let mut count: u32 = 1;
    new_name.push('~');
    while directory
        .join(new_name.clone() + count.clone().to_string().as_str())
        .exists()
    {
        count += 1;
    }
    new_name.push_str(count.to_string().as_str());
    directory.join(new_name)
}

pub fn add_tag_to_brackets(name: &str, tag: &str, delimiter: &str) -> String {
    // Adds tag to existing brackets
    let mut bracket_list: Vec<String> = split_bracket_contents(name, delimiter);
    bracket_list.push(tag.to_owned());
    let bracket_string: String = bracket_list.join(delimiter);
    let indices: (usize, usize) = get_bracket_indices(name);
    let mut new_name: String = name[..=indices.0].to_owned();
    new_name.push_str(&bracket_string);
    new_name.push_str(&name[indices.1 - 1..name.len()]);
    new_name
}

pub fn remove_tag_from_brackets(name: &str, tag: &str, delimiter: &str) -> String {
    // Removes tag from brackets, removing brackets if emptied afterwards, and
    // returns new name as string
    let mut bracket_list: Vec<String> = split_bracket_contents(name, delimiter);
    if bracket_list.contains(&tag.to_owned()) {
        bracket_list.remove(bracket_list.iter().position(|r| r == tag).unwrap());
    }

    let mut new_name: String = String::new();
    let indices: (usize, usize) = get_bracket_indices(name);
    if bracket_list.is_empty() {
        new_name.push_str(&name.to_owned()[..indices.0]);
        new_name.push_str(&name.to_owned()[indices.1..]);
        return new_name;
    }

    let bracket_string: String = bracket_list.join(delimiter);
    new_name.push_str(&name.to_owned()[..indices.0]);
    new_name.push('[');
    new_name.push_str(&bracket_string);
    new_name.push(']');
    new_name.push_str(&name.to_owned()[indices.1..]);
    new_name
}

pub fn get_bracket_indices(name: &str) -> (usize, usize) {
    // Returns the indices that the left and right brackets are at so the new
    // tags can replace what's in them
    let indices = Regex::new("\\[.*?\\]").unwrap().find(name).unwrap();
    (indices.start(), indices.end())
}

pub fn tag_in_brackets(name: &str, tag: &str, delimiter: &str) -> bool {
    // Tells whether or not a given tag is in [] within a given string
    if !contains_brackets(name) {
        return false;
    }
    let bracket_list: Vec<String> = split_bracket_contents(name, delimiter);
    if !(bracket_list.contains(&tag.to_owned())) {
        return false;
    }
    true
}

pub fn contains_brackets(name: &str) -> bool {
    // Tells whether or not a given string contains []
    Regex::new("\\[.*?\\]").unwrap().is_match(name)
}

pub fn split_bracket_contents(name: &str, delimiter: &str) -> Vec<String> {
    // Returns list of everything between first [ and ]
    let pattern = Regex::new("\\[.*?\\]").unwrap();
    let mut inside_brackets = pattern.find(name).unwrap().as_str().to_owned();
    inside_brackets = inside_brackets[1..inside_brackets.len() - 1].to_owned();
    let mut bracket_list = vec![];
    for each_item in inside_brackets.split(delimiter) {
        bracket_list.push(each_item.to_owned());
    }
    bracket_list
}