please-install 1.2.0

A unified interface package manager for many OSes
Documentation
use core::cell::RefCell;
use std::{
    borrow::Cow,
    collections::BTreeMap,
    ffi::OsString,
    fmt::{self, Display},
    sync::LazyLock,
};

use clap::ValueEnum;
use eyre::{eyre, Result};
use serde::{Serialize, Deserialize};
use strum::{EnumIter, IntoEnumIterator};

use crate::package::Package;
use crate::pls_command::PlsCommand;
use crate::reinstall::reinstall_all;
use crate::revendor::change_vendor;
use crate::run_command::run_command;
use crate::track;
use crate::vendor_data::VendorData;


#[derive(Debug, Clone, Copy, Default, EnumIter, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum Vendor {
    #[default]
    Unknown,
    Upt,
    Cargo,
    Go,
    Npm,
    Uv,
    #[cfg(not(target_os = "windows"))]
    Pkgx,
    #[cfg(target_os = "linux")]
    Apt,
    #[cfg(target_os = "linux")]
    Yay,
    #[cfg(target_os = "linux")]
    Yum,
    #[cfg(target_os = "linux")]
    Pacman,
    #[cfg(target_os = "linux")]
    Rua,
    #[cfg(target_os = "linux")]
    Apk,
    #[cfg(target_os = "linux")]
    Emerge,
    #[cfg(target_os = "linux")]
    Guix,
    #[cfg(target_os = "linux")]
    NixEnv,
    #[cfg(target_os = "linux")]
    Slackpkg,
    #[cfg(target_os = "linux")]
    Cards,
    #[cfg(target_os = "linux")]
    Dnf,
    #[cfg(target_os = "linux")]
    Eopkg,
    #[cfg(target_os = "linux")]
    Opkg,
    #[cfg(target_os = "linux")]
    Urpm,
    #[cfg(target_os = "linux")]
    Xbps,
    #[cfg(target_os = "linux")]
    Zypper,
    #[cfg(target_os = "linux")]
    Flatpak,
    #[cfg(target_os = "linux")]
    Snap,
    #[cfg(any(target_os = "freebsd", target_os = "openbsd", target_os = "dragonfly", target_os = "netbsd"))]
    Pkg,
    #[cfg(target_os = "haiku")]
    Pkgman,
    #[cfg(target_os = "macos")]
    Brew,
    #[cfg(target_os = "macos")]
    Ports,
    #[cfg(target_os = "windows")]
    Scoop,
    #[cfg(target_os = "windows")]
    Choco,
    #[cfg(target_os = "windows")]
    Winget,
    #[cfg(target_os = "android")]
    Termux,
}

impl Vendor {
    pub fn new() -> Result<Self> {
        for vendor in Vendor::iter() {
            if vendor.is_available()? {
                return Ok(vendor)
            }
        }
        Err(eyre!(
            "no vendor installed, candidates are: {}",
            Vendor::iter().map(|vendor| vendor.to_string()).collect::<Vec<String>>().join(", "),
        ))
    }

    #[allow(static_mut_refs)]
    pub fn is_available(&self) -> Result<bool> {
        unsafe {
            if let Some(&available) = AVAILABILITY.borrow().get(self) {
                return Ok(available);
            }
        }
        let vendor_data: VendorData = (*self).try_into()?;
        let available = which::which(vendor_data.1[0]).is_ok();
        unsafe {
            AVAILABILITY.borrow_mut().insert(self.clone(), available);
        }
        Ok(available)
    }

    pub fn execute(
        self,
        pls_command: &PlsCommand,
        args: Vec<String>,
        yes: bool,
        su: bool,
        dry_run: bool,
        pager: Option<String>,
        supplied_vendor: Option<Vendor>,
    ) -> Result<i32> {
        let pager = if pls_command.support_pager() {
            pager
        } else {
            None
        };

        if pls_command.to_owned() == PlsCommand::ReinstallAll {
            return reinstall_all(supplied_vendor, yes, su, dry_run, &pager, &pls_command);
        }

        if !dry_run {
            if let PlsCommand::Move { origin, destination } = pls_command {
                return change_vendor(*origin, *destination);
            }
        }

        let vendor_data: VendorData = self.try_into()?;
        let packages: Vec<Package> = args.iter()
            .map(|arg| arg.as_str().into())
            .map(|mut package: Package| {
                package.vendor = self;
                package
            })
            .collect();

        let command = pls_command.format(vendor_data, &packages, yes, &pager);

        if command.is_empty() {
            eprintln!("command not supported by the current vendor");
            return Ok(1)
        }

        if dry_run {
            println!("{}", command);
            return Ok(0);
        }

        let status = run_command(&command, su, &pager, &pls_command)?;

        if !dry_run && status == 0 {
            match pls_command {
                PlsCommand::Install => track::save_installed_packages(packages, true)?,
                PlsCommand::Remove => track::remove_installed_packages(packages)?,
                _ => (),
            }
        }

        Ok(status)
    }

    pub fn version_sep(&self) -> Cow<'static, str> {
        match self {
            Self::Pkgx |
            Self::Npm => Cow::Borrowed("@"),
            Self::Flatpak => Cow::Borrowed("//"),
            _ => Cow::Borrowed("="),
        }
    }
}

impl TryFrom<OsString> for Vendor {
    type Error = String;

    fn try_from(value: OsString) -> Result<Self, Self::Error> {
        let value = value.to_string_lossy().to_lowercase();
        for vendor in Vendor::iter() {
            if vendor.to_string().to_lowercase() == value {
                return Ok(vendor);
            }
        }
        Err(format!("invalid vendor name {}", value))
    }
}

impl Display for Vendor {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            Self::Unknown => write!(f, "unknown"),
            Self::Upt => write!(f, "upt"),
            Self::Cargo => write!(f, "cargo"),
            Self::Go => write!(f, "go"),
            Self::Uv => write!(f, "uv"),
            Self::Npm => write!(f, "npm"),
            #[cfg(not(target_os = "windows"))]
            Self::Pkgx => write!(f, "pkgx"),
            #[cfg(target_os = "linux")]
            Self::Apt => write!(f, "apt"),
            #[cfg(target_os = "linux")]
            Self::Yay => write!(f, "yay"),
            #[cfg(target_os = "linux")]
            Self::Yum => write!(f, "yum"),
            #[cfg(target_os = "linux")]
            Self::Pacman => write!(f, "pacman"),
            #[cfg(target_os = "linux")]
            Self::Rua => write!(f, "rua"),
            #[cfg(target_os = "linux")]
            Self::Apk => write!(f, "apk"),
            #[cfg(target_os = "linux")]
            Self::Emerge => write!(f, "emerge"),
            #[cfg(target_os = "linux")]
            Self::Guix => write!(f, "guix"),
            #[cfg(target_os = "linux")]
            Self::NixEnv => write!(f, "nix-env"),
            #[cfg(target_os = "linux")]
            Self::Slackpkg => write!(f, "slackpkg"),
            #[cfg(target_os = "linux")]
            Self::Cards => write!(f, "cards"),
            #[cfg(target_os = "linux")]
            Self::Dnf => write!(f, "dnf"),
            #[cfg(target_os = "linux")]
            Self::Eopkg => write!(f, "eopkg"),
            #[cfg(target_os = "linux")]
            Self::Opkg => write!(f, "opkg"),
            #[cfg(target_os = "linux")]
            Self::Urpm => write!(f, "urpm"),
            #[cfg(target_os = "linux")]
            Self::Xbps => write!(f, "xbps"),
            #[cfg(target_os = "linux")]
            Self::Zypper => write!(f, "zypper"),
            #[cfg(target_os = "linux")]
            Self::Flatpak => write!(f, "flatpak"),
            #[cfg(target_os = "linux")]
            Self::Snap => write!(f, "snap"),
            #[cfg(any(target_os = "freebsd", target_os = "openbsd", target_os = "dragonfly", target_os = "netbsd"))]
            Self::Pkg => write!(f, "pkg"),
            #[cfg(target_os = "haiku")]
            Self::Pkgman => write!(f, "pkgman"),
            #[cfg(target_os = "macos")]
            Self::Brew => write!(f, "brew"),
            #[cfg(target_os = "macos")]
            Self::Ports => write!(f, "ports"),
            #[cfg(target_os = "windows")]
            Self::Scoop => write!(f, "scoop"),
            #[cfg(target_os = "windows")]
            Self::Choco => write!(f, "choco"),
            #[cfg(target_os = "windows")]
            Self::Winget => write!(f, "winget"),
            #[cfg(target_os = "android")]
            Self::Termux => write!(f, "termux"),
        }
    }
}

impl TryFrom<&str> for Vendor {
    type Error = eyre::Error;

    fn try_from(value: &str) -> std::result::Result<Self, Self::Error> {
        let value = value.to_lowercase();
        for vendor in Vendor::iter() {
            if vendor.to_string().to_lowercase() == value {
                return Ok(vendor);
            }
        }
        Err(eyre!("invalid vendor name {}", value))
    }
}

impl ValueEnum for Vendor {
    fn value_variants<'a>() -> &'a [Self] {
        &VENDORS_SLICE
    }

    fn to_possible_value(&self) -> Option<clap::builder::PossibleValue> {
        Some(clap::builder::PossibleValue::new(self.to_string()))
    }
}

static mut AVAILABILITY: LazyLock<RefCell<BTreeMap<Vendor, bool>>> = LazyLock::new(|| RefCell::new(BTreeMap::new()));
static INNER_VENDORS: LazyLock<Vec<Vendor>> = LazyLock::new(|| Vendor::iter().collect());
static VENDORS_SLICE: LazyLock<&[Vendor]> = LazyLock::new(|| &INNER_VENDORS);