symp 0.5.0

symlink farm manager that utilizes configuration files to define symlink mappings
//! `symp` (short for '**sym**link **p**ackages') is a symlink farm manager
//! that utilizes configuration files ('symp.toml' files) to define symlink
//! mappings (_links_), which are grouped into _packages_ that can be
//! independently installed and uninstalled onto the system.

#![allow(clippy::single_component_path_imports)]

mod cli;
mod config;
mod filesystem;
mod lock;
mod table;

use std::path::Path;

use anyhow::Result;
use clap::Parser;

use crate::cli::Cli;
use crate::cli::{AddArgs, Command, RemoveArgs, StatusArgs, SyncArgs};
use crate::filesystem::AbsPath;
use crate::lock::Lock;
use crate::table::Table;

fn resolve_path_to_file(path: &Path, file_name: &str) -> Result<AbsPath> {
    let mut path = path.canonicalize()?;
    if path.is_dir() {
        path.push(file_name);
    }
    Ok(AbsPath::from_owned_path(path)?)
}

fn load_table_and_lock(config_path: &Path) -> Result<(Table, Lock)> {
    let symp_toml_file = resolve_path_to_file(config_path, "symp.toml")?;
    let symp_lock_file = AbsPath::from_owned_path(
        symp_toml_file
            .parent()
            .expect("parent path should exist")
            .join({
                let mut file_name = symp_toml_file
                    .file_stem()
                    .expect("is only None if path terminates in ..")
                    .to_os_string();
                file_name.push(".lock");
                file_name
            }),
    )?;
    let table = Table::load(&symp_toml_file)?;
    let lock = Lock::load(&symp_lock_file)?;
    Ok((table, lock))
}

fn load_lock(config_path: &Path) -> Result<Lock> {
    let symp_toml_file = resolve_path_to_file(config_path, "symp.toml")?;
    let symp_lock_file = AbsPath::from_owned_path(
        symp_toml_file
            .parent()
            .expect("parent path should exist")
            .join({
                let mut file_name = symp_toml_file
                    .file_stem()
                    .expect("is only None if path terminates in ..")
                    .to_os_string();
                file_name.push(".lock");
                file_name
            }),
    )?;
    let lock = Lock::load(&symp_lock_file)?;
    Ok(lock)
}

// Commands

fn add(args: AddArgs) -> Result<()> {
    let (table, mut lock) = load_table_and_lock(&args.config)?;
    if args.all || (args.packages.is_empty() && args.profile.is_empty()) {
        lock.mark_for_removal(lock.all_package_names().iter());
        lock.add(table.links.iter());
        table.validate_table_packages(table.packages.iter().map(|pkg| &pkg.package_name))?;
    } else {
        let mut package_names = Vec::new();
        package_names.extend(args.packages.iter().cloned());
        for profile in args.profile.iter() {
            package_names.extend(table.get_package_names_from_profile(profile)?);
        }
        package_names.sort();
        package_names.dedup();
        if !package_names.is_empty() {
            table.check_packages_exist(package_names.iter())?;
            let dropped_package_names: Vec<_> = lock
                .links
                .keys()
                .filter_map(|name| {
                    match !table.packages.iter().any(|pkg| pkg.package_name.eq(name)) {
                        true => Some(name.to_string()),
                        false => None,
                    }
                })
                .collect();
            lock.mark_for_removal(dropped_package_names.iter());
            lock.mark_for_removal(package_names.iter());
            lock.add(
                table
                    .links
                    .iter()
                    .filter(|link| package_names.contains(&link.package_data.package_name)),
            );
            table.validate_table_packages(package_names.iter())?;
        }
    }
    if !args.no_sync {
        lock.sync()?;
    }
    lock.save()?;
    Ok(())
}

fn remove(args: RemoveArgs) -> Result<()> {
    let (table, mut lock) = load_table_and_lock(&args.config)?;
    if args.all || (args.packages.is_empty() && args.profile.is_empty()) {
        lock.mark_for_removal(lock.all_package_names().iter());
        table.validate_table_packages(table.packages.iter().map(|pkg| &pkg.package_name))?;
    } else {
        let mut package_names = Vec::new();
        package_names.extend(args.packages.iter().cloned());
        for profile in args.profile.iter() {
            package_names.extend(table.get_package_names_from_profile(profile)?);
        }
        package_names.sort();
        package_names.dedup();
        if !package_names.is_empty() {
            table.check_packages_exist(package_names.iter())?;
            let dropped_package_names: Vec<_> = lock
                .links
                .keys()
                .filter_map(|name| {
                    match !table.packages.iter().any(|pkg| pkg.package_name.eq(name)) {
                        true => Some(name.to_string()),
                        false => None,
                    }
                })
                .collect();
            lock.mark_for_removal(dropped_package_names.iter());
            lock.mark_for_removal(package_names.iter());
            table.validate_table_packages(package_names.iter())?;
        }
    }
    if !args.no_sync {
        lock.sync()?;
    }
    lock.save()?;
    Ok(())
}

fn sync(args: SyncArgs) -> Result<()> {
    let mut lock = load_lock(&args.config)?;
    lock.sync()?;
    lock.save()?;
    Ok(())
}

fn status(args: StatusArgs) -> Result<()> {
    let (table, lock) = load_table_and_lock(&args.config)?;
    if args.all || (args.packages.is_empty() && args.profile.is_empty()) {
        table.validate_table_packages(table.packages.iter().map(|pkg| &pkg.package_name))?;
        table.print_status(table.packages.iter().map(|pkg| &pkg.package_name), &lock)?;
    } else {
        let mut package_names = Vec::new();
        package_names.extend(args.packages.iter().cloned());
        for profile in args.profile.iter() {
            package_names.extend(table.get_package_names_from_profile(profile)?);
        }
        package_names.sort();
        package_names.dedup();
        if !package_names.is_empty() {
            table.check_packages_exist(package_names.iter())?;
            table.validate_table_packages(package_names.iter())?;
            table.print_status(package_names.iter(), &lock)?;
        }
    }
    Ok(())
}

fn blahaj() {
    println!("🦈 says:\n\n🏳️‍⚧️ Trans Rights are Human Rights! 🏳️‍⚧️");
}

fn simp() {
    println!("https://www.youtube.com/watch?v=vYVhUvNINTA");
}

// Entrypoint

fn main() -> Result<()> {
    let cli = Cli::parse();
    #[cfg(debug_assertions)]
    println!("{:#?}", cli);
    match cli.command {
        Command::Add(args) => add(args)?,
        Command::Remove(args) => remove(args)?,
        Command::Sync(args) => sync(args)?,
        Command::Status(args) => status(args)?,
        // Easter Eggs
        Command::Blahaj => blahaj(),
        Command::Simp => simp(),
    };
    Ok(())
}