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();
#[derive(Serialize, Deserialize, Debug)]
#[serde(default)]
pub struct Config {
pub toolbx_names: Vec<String>,
pub distrobox_names: Vec<String>,
pub log_level: TraceLevel,
pub log_path: std::path::PathBuf,
pub custom_providers: Vec<cnf_lib::provider::custom::Custom>,
pub query_origins: Vec<EnvProvMatrix>,
pub keybindings: AppKeybinds,
pub alias_path: Option<std::path::PathBuf>,
pub max_recursion_depth: usize,
}
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,
}
}
}
#[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
}
}
#[derive(Serialize, Deserialize, Debug)]
pub struct EnvProvMatrix {
pub environment: String,
pub enabled: bool,
#[serde(default)]
pub enabled_providers: Vec<String>,
#[serde(default)]
pub disabled_providers: Vec<String>,
}
impl EnvProvMatrix {
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() {
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
)
})
}
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(())
}
}
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();
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
});
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();
}
pub(crate) fn get() -> &'static Config {
CONFIG.get().expect("config wasn't initialized yet")
}