#[cfg(feature = "wizard")]
pub mod wizard;
use anyhow::{anyhow, bail, Context, Result};
use dirs::{config_dir, home_dir};
use log::debug;
use serde::{Deserialize, Serialize};
use serde_toml_merge::merge;
use shellexpand_utils::{canonicalize, expand};
use std::{collections::HashMap, fs, path::PathBuf};
use toml::Value;
use crate::account::config::AccountConfig;
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct Config {
pub accounts: HashMap<String, AccountConfig>,
}
impl Config {
fn from_paths(paths: &[PathBuf]) -> Result<Self> {
match paths.len() {
0 => {
bail!("cannot read config file from empty paths");
}
1 => {
let path = &paths[0];
let ref content = fs::read_to_string(path)
.context(format!("cannot read config file at {path:?}"))?;
toml::from_str(content).context(format!("cannot parse config file at {path:?}"))
}
_ => {
let path = &paths[0];
let mut merged_content = fs::read_to_string(path)
.context(format!("cannot read config file at {path:?}"))?
.parse::<Value>()?;
for path in &paths[1..] {
match fs::read_to_string(path) {
Ok(content) => {
merged_content = merge(merged_content, content.parse()?).unwrap();
}
Err(err) => {
debug!("skipping subconfig file at {path:?}: {err}");
continue;
}
}
}
merged_content
.try_into()
.context(format!("cannot parse merged config file at {path:?}"))
}
}
}
#[cfg(feature = "wizard")]
async fn from_wizard(path: &PathBuf) -> Result<Self> {
use dialoguer::Confirm;
use std::process;
use crate::{wizard_prompt, wizard_warn};
wizard_warn!("Cannot find existing configuration at {path:?}.");
let confirm = Confirm::new()
.with_prompt(wizard_prompt!(
"Would you like to create one with the wizard?"
))
.default(true)
.interact_opt()?
.unwrap_or_default();
if !confirm {
process::exit(0);
}
wizard::configure(path).await
}
pub async fn from_default_paths() -> Result<Self> {
match Self::first_valid_default_path() {
Some(path) => Self::from_paths(&[path]),
#[cfg(feature = "wizard")]
None => Self::from_wizard(&Self::default_path()?).await,
#[cfg(not(feature = "wizard"))]
None => anyhow::bail!("cannot find config file from default paths"),
}
}
pub async fn from_paths_or_default(paths: &[PathBuf]) -> Result<Self> {
match paths.len() {
0 => Self::from_default_paths().await,
_ if paths[0].exists() => Self::from_paths(paths),
#[cfg(feature = "wizard")]
_ => Self::from_wizard(&paths[0]).await,
#[cfg(not(feature = "wizard"))]
_ => anyhow::bail!("cannot find config file from default paths"),
}
}
pub fn default_path() -> Result<PathBuf> {
Ok(config_dir()
.ok_or(anyhow!("cannot get XDG config directory"))?
.join("neverest")
.join("config.toml"))
}
pub fn first_valid_default_path() -> Option<PathBuf> {
Self::default_path()
.ok()
.filter(|p| p.exists())
.or_else(|| home_dir().map(|p| p.join(".config").join("neverest").join("config.toml")))
.filter(|p| p.exists())
.or_else(|| home_dir().map(|p| p.join(".neverestrc")))
.filter(|p| p.exists())
}
pub fn into_account_config(
&self,
account_name: Option<&str>,
) -> Result<(String, AccountConfig)> {
#[allow(unused_mut)]
let (account_name, mut account_config) = match account_name {
Some("default") | Some("") | None => self
.accounts
.iter()
.find_map(|(name, config)| {
config
.default
.filter(|default| *default)
.map(|_| (name.to_owned(), config.clone()))
})
.ok_or_else(|| anyhow!("cannot find default account")),
Some(name) => self
.accounts
.get(name)
.map(|config| (name.to_owned(), config.clone()))
.ok_or_else(|| anyhow!("cannot find account {name}")),
}?;
account_config.configure(account_name.as_str())?;
Ok((account_name, account_config))
}
}
pub fn path_parser(path: &str) -> Result<PathBuf, String> {
expand::try_path(path)
.map(canonicalize::path)
.map_err(|err| err.to_string())
}