rtx-cli 2024.0.0

Polyglot runtime manager (asdf rust clone)
use confique::env::parse::{list_by_colon, list_by_comma};
use eyre::Result;

use std::collections::{BTreeSet, HashSet};
use std::fmt::{Debug, Display, Formatter};
use std::path::PathBuf;
use std::sync::{Arc, Once, RwLock};

use confique::{Builder, Config, Partial};
use once_cell::sync::Lazy;

use crate::env;
use serde::ser::Error;
use serde_derive::Serialize;

#[derive(Config, Debug, Clone, Serialize)]
#[config(partial_attr(derive(Clone, Serialize)))]
pub struct Settings {
    #[config(env = "RTX_EXPERIMENTAL", default = false)]
    pub experimental: bool,
    #[config(env = "RTX_COLOR", default = true)]
    pub color: bool,
    #[config(env = "RTX_ALWAYS_KEEP_DOWNLOAD", default = false)]
    pub always_keep_download: bool,
    #[config(env = "RTX_ALWAYS_KEEP_INSTALL", default = false)]
    pub always_keep_install: bool,
    #[config(env = "RTX_LEGACY_VERSION_FILE", default = true)]
    pub legacy_version_file: bool,
    #[config(env = "RTX_LEGACY_VERSION_FILE_DISABLE_TOOLS", default = [], parse_env = list_by_comma)]
    pub legacy_version_file_disable_tools: BTreeSet<String>,
    #[config(env = "RTX_PLUGIN_AUTOUPDATE_LAST_CHECK_DURATION", default = "7d")]
    pub plugin_autoupdate_last_check_duration: String,
    #[config(env = "RTX_TRUSTED_CONFIG_PATHS", default = [], parse_env = list_by_colon)]
    pub trusted_config_paths: BTreeSet<PathBuf>,
    #[config(env = "RTX_LOG_LEVEL", default = "info")]
    pub log_level: String,
    #[config(env = "RTX_TRACE", default = false)]
    pub trace: bool,
    #[config(env = "RTX_DEBUG", default = false)]
    pub debug: bool,
    #[config(env = "RTX_VERBOSE", default = false)]
    pub verbose: bool,
    #[config(env = "RTX_QUIET", default = false)]
    pub quiet: bool,
    #[config(env = "RTX_ASDF_COMPAT", default = false)]
    pub asdf_compat: bool,
    #[config(env = "RTX_JOBS", default = 4)]
    pub jobs: usize,
    #[config(env = "RTX_SHORTHANDS_FILE")]
    pub shorthands_file: Option<PathBuf>,
    #[config(env = "RTX_DISABLE_DEFAULT_SHORTHANDS", default = false)]
    pub disable_default_shorthands: bool,
    #[config(env = "RTX_DISABLE_TOOLS", default = [], parse_env = list_by_comma)]
    pub disable_tools: BTreeSet<String>,
    #[config(env = "RTX_RAW", default = false)]
    pub raw: bool,
    #[config(env = "RTX_YES", default = false)]
    pub yes: bool,
    #[config(env = "RTX_TASK_OUTPUT")]
    pub task_output: Option<String>,
    #[config(env = "RTX_NOT_FOUND_AUTO_INSTALL", default = true)]
    pub not_found_auto_install: bool,
    #[config(env = "CI", default = false)]
    pub ci: bool,
    pub cd: Option<String>,
}

pub type SettingsPartial = <Settings as Config>::Partial;

impl Default for Settings {
    fn default() -> Self {
        Settings::default_builder().load().unwrap()
    }
}

static PARTIALS: RwLock<Vec<SettingsPartial>> = RwLock::new(Vec::new());
static SETTINGS: RwLock<Option<Arc<Settings>>> = RwLock::new(None);

impl Settings {
    pub fn get() -> Arc<Self> {
        Self::try_get().unwrap()
    }
    pub fn try_get() -> Result<Arc<Self>> {
        if let Some(settings) = SETTINGS.read().unwrap().as_ref() {
            return Ok(settings.clone());
        }
        let mut settings = Self::default_builder().load()?;
        if let Some(cd) = &settings.cd {
            static ORIG_PATH: Lazy<std::io::Result<PathBuf>> = Lazy::new(env::current_dir);
            let mut cd = PathBuf::from(cd);
            if cd.is_relative() {
                cd = ORIG_PATH.as_ref()?.join(cd);
            }
            env::set_current_dir(cd)?;
        }
        if settings.raw {
            settings.jobs = 1;
        }
        if settings.debug {
            settings.log_level = "debug".to_string();
        }
        if settings.trace {
            settings.log_level = "trace".to_string();
        }
        if settings.quiet {
            settings.log_level = "warn".to_string();
        }
        if settings.log_level == "trace" || settings.log_level == "debug" {
            settings.verbose = true;
            settings.debug = true;
            if settings.log_level == "trace" {
                settings.trace = true;
            }
        }
        if settings.verbose {
            settings.quiet = false;
            if settings.log_level != "trace" {
                settings.log_level = "debug".to_string();
            }
        }
        if !settings.color {
            console::set_colors_enabled(false);
            console::set_colors_enabled_stderr(false);
        }
        if settings.ci {
            settings.yes = true;
        }
        let settings = Arc::new(settings);
        *SETTINGS.write().unwrap() = Some(settings.clone());
        Ok(settings)
    }
    pub fn add_partial(partial: SettingsPartial) {
        static ONCE: Once = Once::new();
        ONCE.call_once(|| {
            let mut p = SettingsPartial::empty();
            for arg in &*env::ARGS.read().unwrap() {
                if arg == "--" {
                    break;
                }
                if arg == "--raw" {
                    p.raw = Some(true);
                }
            }
            PARTIALS.write().unwrap().push(p);
        });
        PARTIALS.write().unwrap().push(partial);
        *SETTINGS.write().unwrap() = None;
    }
    pub fn add_cli_matches(m: &clap::ArgMatches) {
        let mut s = SettingsPartial::empty();
        if let Some(cd) = m.get_one::<String>("cd") {
            s.cd = Some(cd.to_string());
        }
        if let Some(true) = m.get_one::<bool>("yes") {
            s.yes = Some(true);
        }
        if let Some(true) = m.get_one::<bool>("quiet") {
            s.quiet = Some(true);
        }
        if let Some(true) = m.get_one::<bool>("trace") {
            s.log_level = Some("trace".to_string());
        }
        if let Some(true) = m.get_one::<bool>("debug") {
            s.log_level = Some("debug".to_string());
        }
        if let Some(log_level) = m.get_one::<String>("log-level") {
            s.log_level = Some(log_level.to_string());
        }
        if *m.get_one::<u8>("verbose").unwrap() > 0 {
            s.verbose = Some(true);
        }
        if *m.get_one::<u8>("verbose").unwrap() > 1 {
            s.log_level = Some("trace".to_string());
        }
        Self::add_partial(s);
    }

    pub fn default_builder() -> Builder<Self> {
        let mut b = Self::builder().env();
        for partial in PARTIALS.read().unwrap().iter() {
            b = b.preloaded(partial.clone());
        }
        b
    }
    pub fn hidden_configs() -> HashSet<&'static str> {
        static HIDDEN_CONFIGS: Lazy<HashSet<&'static str>> =
            Lazy::new(|| ["ci", "cd", "debug", "trace", "log_level"].into());
        HIDDEN_CONFIGS.clone()
    }

    #[cfg(test)]
    pub fn reset() {
        PARTIALS.write().unwrap().clear();
        *SETTINGS.write().unwrap() = None;
    }

    pub fn ensure_experimental(&self) -> Result<()> {
        let msg =
            "This command is experimental. Enable it with `rtx settings set experimental true`";
        ensure!(self.experimental, msg);
        Ok(())
    }
}

impl Display for Settings {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        match serde_json::to_string_pretty(self) {
            Ok(s) => write!(f, "{}", s),
            Err(e) => std::fmt::Result::Err(std::fmt::Error::custom(e)),
        }
    }
}