cnf 0.6.0

Distribution-agnostic 'command not found'-handler
Documentation
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: (C) 2023 Andreas Hartmann <hartan@7x.de>
// This file is part of cnf, available at <https://gitlab.com/hartang/rust/cnf>

//! # Configuration for CNF
//!
//! Reads the user configuration from the configuration file `~/.config/cnf/cnf.yml` and
//! deserializes it into a struct for convenient handling.
use std::{str::FromStr, sync::OnceLock};

use crate::ui::keybinds::AppKeybinds;
use anyhow::Context;
use logerr::LoggableError;
use serde_derive::{Deserialize, Serialize};
use tracing::level_filters::LevelFilter;

#[doc(hidden)]
pub static APP_NAME: &str = env!("CARGO_PKG_NAME");
static CONFIG: OnceLock<Config> = OnceLock::new();

/// Configuration struct.
#[derive(Serialize, Deserialize, Debug)]
#[serde(default)]
pub struct Config {
    /// Name of one or more Toolbx containers to forward commands to that weren't found on the
    /// host.
    ///
    /// ## Example
    /// ```yaml
    /// toolbx_names:
    ///   # The empty string uses the default toolbx container
    ///   - ""
    ///   - "foo"
    /// ```
    pub toolbx_names: Vec<String>,
    /// Name of one or mote distrobox containers to forward commands to that weren't found on the
    /// host.
    ///
    /// ## Example
    /// ```yaml
    /// distrobox_names:
    ///   # The empty string uses the default distrobox container
    ///   - ""
    ///   - "bar"
    /// ```
    pub distrobox_names: Vec<String>,
    /// Set the debug verbosity level (off, error, warn, info, debug, trace)
    ///
    /// ## Example
    /// ```yaml
    /// log_level: info
    /// ```
    pub log_level: TraceLevel,
    /// Log file location
    ///
    /// ## Example
    /// ```yaml
    /// log_path: /tmp/cnf.log
    /// ```
    pub log_path: std::path::PathBuf,
    /// Custom providers to add
    ///
    /// ## Example
    /// Please refer to the [custom provider docs].
    ///
    /// [custom provider docs]: cnf_lib::provider::custom#configuring-custom-providers
    pub custom_providers: Vec<cnf_lib::provider::custom::Custom>,
    /// Sources to query for missing commands.
    pub query_origins: Vec<EnvProvMatrix>,
    /// Application keybindings
    ///
    /// ## Example
    /// Please refer to the [keybindings docs].
    ///
    /// [keybindings docs]: crate::ui::keybinds#configuring-keybinds
    pub keybindings: AppKeybinds,
    /// Path to store shell-based aliases in
    pub alias_path: Option<std::path::PathBuf>,
    /// Maximum recursion depth of nested `cnf` calls allowed during e.g. alias execution. Set to 0
    /// to disable recursions completely
    pub max_recursion_depth: usize,
}

/// Default configuration.
impl std::default::Default for Config {
    fn default() -> Self {
        Self {
            toolbx_names: vec![],
            distrobox_names: vec![],
            log_level: TraceLevel::default(),
            log_path: std::path::PathBuf::from("/tmp/cnf.log"),
            custom_providers: vec![],
            query_origins: vec![],
            keybindings: AppKeybinds::default(),
            alias_path: None,
            max_recursion_depth: 3,
        }
    }
}

/// `serde`-enabled wrapper around [`LevelFilter`].
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(try_from = "String", into = "String")]
pub struct TraceLevel(LevelFilter);

impl Default for TraceLevel {
    fn default() -> Self {
        Self(LevelFilter::INFO)
    }
}

impl From<TraceLevel> for String {
    fn from(value: TraceLevel) -> Self {
        value.0.to_string()
    }
}

impl TryFrom<String> for TraceLevel {
    type Error = anyhow::Error;

    fn try_from(value: String) -> Result<Self, Self::Error> {
        let level = LevelFilter::from_str(&value[..]).map_err(anyhow::Error::new)?;
        Ok(Self(level))
    }
}

impl std::ops::Deref for TraceLevel {
    type Target = LevelFilter;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

/// Configuration structure for [Environment]s and [Provider]s.
///
/// Allow configuring, per [Environment], which [Provider]s to query. The `environment`,
/// `active_providers` and `inactive_providers` configuration elements expect to receive a
/// [`String`] representing the respective configuration item. This string must match the output of
/// [`std::fmt::Display`], i.e. the string that is displayed in the TUI for each entity.
///
/// Note that Unknown [Environment]s and [Provider]s are reported at runtime.
///
/// [Environment]: cnf_lib::Environment
/// [Provider]: cnf_lib::Provider
#[derive(Serialize, Deserialize, Debug)]
pub struct EnvProvMatrix {
    /// The environment to configure.
    pub environment: String,
    /// Set to false to completely disable this environment from being used.
    pub enabled: bool,
    /// Providers to query from this environment. Any providers not mentioned here will be ignored.
    /// Unless you have very specific needs, prefer inactivating providers to get new providers
    /// from application updates.
    #[serde(default)]
    pub enabled_providers: Vec<String>,
    /// Providers **not** to query from this environment. Overwrites the configuration from
    /// `active_providers`.
    #[serde(default)]
    pub disabled_providers: Vec<String>,
}

impl EnvProvMatrix {
    /// Check if the environment in this config entry exists.
    ///
    /// Given a vector of all environments used in the application, checks if the entry in this
    /// block is present and throws an error if it isn't.
    pub fn check_env_exists(
        &self,
        all_envs: &[std::sync::Arc<cnf_lib::Environment>],
    ) -> anyhow::Result<()> {
        for env in all_envs {
            if self.environment == env.to_string() {
                // This env exists
                return Ok(());
            }
        }
        Err(anyhow::anyhow!(
            "detected envs are: \n- {}",
            all_envs
                .iter()
                .map(|entry| entry.to_string())
                .collect::<Vec<String>>()
                .join("\n- ")
        ))
        .with_context(|| {
            format!(
                "config contains settings for env '{}', but this env doesn't exist",
                self.environment
            )
        })
    }

    /// Check if all the providers in this config entry exist.
    ///
    /// Given a vector of all providers used in the application, checks if the entries in this
    /// block are present and throws an error if one of them isn't.
    pub fn check_providers_exist(
        &self,
        all_providers: &[std::sync::Arc<cnf_lib::Provider>],
    ) -> anyhow::Result<()> {
        if self.enabled_providers.is_empty() && self.disabled_providers.is_empty() {
            return Ok(());
        }
        let all_provider_names = all_providers
            .iter()
            .map(|p| p.to_string())
            .collect::<Vec<_>>();
        let config_provider_names = self
            .enabled_providers
            .iter()
            .chain(&self.disabled_providers);

        for cpn in config_provider_names {
            if !all_provider_names.contains(cpn) {
                return Err(anyhow::anyhow!(
                    "detected providers are: \n- {}",
                    all_provider_names.join("\n- ")
                ))
                .with_context(|| {
                    format!(
                        "config contains settings for provider '{}', but this provider doesn't exist",
                        cpn
                    )
                });
            }
        }

        Ok(())
    }
}

/// Read program configuration.
///
/// Recovers from errors caused by invalid markup formatting by printing an error message and
/// using the default config instead. The loaded config isn't returned but stored in runtime memory
/// instead. To access the configuration, refer to [`get()`].
///
/// **Important**: This function must be called exactly once before [`get()`] is called for the
/// first time.
pub(crate) fn load() {
    let config: Config = confy::load(APP_NAME, APP_NAME)
        .context("Config file is invalid, please regenerate config. Using default instead.")
        .to_stderr()
        .to_log()
        .unwrap_or_default();

    // Sanity checks for config
    // Assert that all envs are configured only once
    let mut env_names = config
        .query_origins
        .iter()
        .map(|item| &item.environment)
        .collect::<Vec<_>>();
    env_names.sort();
    let _ = env_names.iter().reduce(|last_env, cur_env| {
        if last_env == cur_env {
            Err::<(), _>(anyhow::anyhow!(
                "query origin for env '{}' is configured more than once",
                cur_env
            ))
            .to_log()
            .unwrap();
        }
        cur_env
    });
    // Inform user that inactive providers override active providers
    for entry in &config.query_origins {
        if !entry.disabled_providers.is_empty() && !entry.enabled_providers.is_empty() {
            tracing::info!(
                "active providers in env '{}' are ignored: inactive providers are configured",
                entry.environment,
            );
        }
    }

    CONFIG.set(config).unwrap();
    crate::directories::init();
}

/// Get a reference to the application configuration.
///
/// The configuration is loaded by calling [`load()`] once during startup and before the first call
/// to this function. Failure to do so will cause a runtime panic the first time the config is
/// accessed.
///
/// The returned reference has a `'static` lifetime, so parts of it can easily be included in
/// higher-level datatypes.
pub(crate) fn get() -> &'static Config {
    CONFIG.get().expect("config wasn't initialized yet")
}