use core::{fmt::{Debug, Display}, ops::BitOrAssign, str::FromStr};
use std::{borrow::Cow, env::VarError, fs::File, path::{Path, PathBuf}, sync::{Arc, LazyLock, Mutex}};
use serde::{Deserialize, Serialize};
moddef::moddef!(
flat(pub) mod {
color_config,
domain_config,
edge_config,
edges_config,
enneagram_config,
show_config,
triad_config,
triads_config
},
mod {
presets
}
);
macro_rules! member {
([$this:expr, $conf:ident$(.$subconf:ident)*].$member:ident) => {
Config::fallback($this.$member.as_ref(), |$conf| (|| {
$(let $conf = $conf.$subconf.as_ref()?;)*
$conf.$member.as_ref()
})())
};
([$this:expr, $conf:ident$(.$subconf:ident)*].$member:ident |=) => {
Config::fallback_fold($this.$member.as_ref(), |$conf| (|| {
$(let $conf = $conf.$subconf.as_ref()?;)*
$conf.$member.as_ref()
})())
};
}
use member as member;
macro_rules! getter {
([_, $conf:ident$(.$subconf:ident)*].$member:ident$(.$deref:ident())* -> &$ty:ty) => {
pub fn $member(&self) -> &$ty
{
crate::config::member!([self, $conf$(.$subconf)*].$member)$(.$deref())*
}
};
([_, $conf:ident$(.$subconf:ident)*].$member:ident$(.$deref:ident())* -> $ty:ty) => {
pub fn $member(&self) -> $ty
{
*crate::config::member!([self, $conf$(.$subconf)*].$member)$(.$deref())*
}
};
([_, $conf:ident$(.$subconf:ident)*].$member:ident$(.$deref:ident())* |= -> $ty:ty) => {
pub fn $member(&self) -> $ty
{
crate::config::member!([self, $conf$(.$subconf)*].$member |=)$(.$deref())*
}
};
}
use getter as getter;
macro_rules! def_unitary {
(
struct $name:ident for $partial:ident
{
$($var:ident: $var_ty:ty),+$(,)?
}
) => {
#[derive(Clone, Debug, PartialEq, Eq)]
#[derive(Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct $partial<'a>
{
$(
#[serde(skip_serializing_if = "Option::is_none")]
$var: Option<Cow<'a, $var_ty>>,
)+
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct $name<'a>
{
$(
pub $var: Cow<'a, $var_ty>,
)+
}
impl<'a> std::ops::BitOrAssign<&'a $partial<'a>> for $partial<'a>
{
fn bitor_assign(&mut self, rhs: &'a $partial<'a>)
{
let Self { $($var,)+ } = rhs;
$(
if let Some(v) = $var.as_ref() { self.$var.get_or_insert_with(|| Cow::Borrowed(v.as_ref())); }
)+
}
}
impl<'a> From<&'a $partial<'a>> for $partial<'a>
{
fn from(value: &'a $partial<'a>) -> Self
{
let Self { $($var,)+ } = value;
Self {
$(
$var: $var.as_ref().map(|c| Cow::Borrowed(c.as_ref())),
)+
}
}
}
impl<'a> TryFrom<&'a $partial<'a>> for $name<'a>
{
type Error = &'a $partial<'a>;
fn try_from(value: &'a $partial<'a>) -> Result<Self, Self::Error>
{
let $partial { $($var,)+ } = value;
Ok(Self {
$(
$var: Cow::Borrowed($var.as_ref().ok_or(value)?.as_ref()),
)+
})
}
}
impl<'a> TryFrom<$partial<'a>> for $name<'a>
{
type Error = $partial<'a>;
fn try_from(value: $partial<'a>) -> Result<Self, Self::Error>
{
let $partial { $($var,)+ } = value.clone();
Ok(Self {
$(
$var: $var.ok_or(value.clone())?,
)+
})
}
}
};
}
use def_unitary as def_unitary;
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(deny_unknown_fields)]
pub struct Config
{
#[serde(skip_serializing_if = "Option::is_none")]
show: Option<ShowConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
color: Option<ColorConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
enneagram: Option<EnneagramConfig>
}
impl Config
{
crate::config::getter!([_, c].show -> &ShowConfig);
crate::config::getter!([_, c].color -> &ColorConfig);
crate::config::getter!([_, c].enneagram -> &EnneagramConfig);
}
const DEFAULT_CONFIG_FILENAME: &str = "default.yaml";
const DEFAULT_CONFIG_DATA: &str = include_str!("../../presets/default.yaml");
static DEFAULT_CONFIG_PATH: LazyLock<Cow<Path>> = LazyLock::new(|| {
Config::config_path(DEFAULT_CONFIG_FILENAME)
});
static DEFAULT_CONFIG: LazyLock<Config> = LazyLock::new(|| {
static DEFAULT_CONFIG_PRESET: LazyLock<Config> = LazyLock::new(|| {
serde_saphyr::from_str(DEFAULT_CONFIG_DATA)
.expect("Failed to parse original default config.")
});
if !DEFAULT_CONFIG_PATH.exists()
{
DEFAULT_CONFIG_PRESET.write_config(DEFAULT_CONFIG_FILENAME);
}
Config::read_config(DEFAULT_CONFIG_FILENAME)
});
static FALLBACK_FIFO: LazyLock<Arc<Mutex<Vec<&'static Config>>>> = LazyLock::new(|| {
Arc::new(Mutex::new(Vec::new()))
});
const HOME_DIRECTORY_ENV_VARIABLE: &str = "HOME";
const CONFIG_SUBDIRECTORY_UNIX: &str = ".config";
const XDG_CONFIG_HOME_DIRECTORY_ENV_VARIABLE: &str = "XDG_CONFIG_HOME";
impl Config
{
fn fallback<T>(initial: Option<&T>, getter: impl Fn(&Self) -> Option<&T>) -> &T
{
let fallback = FALLBACK_FIFO.lock()
.expect("Failed to lock fallback queue.");
initial.into_iter()
.chain(fallback.iter()
.copied()
.chain(core::iter::once(&*DEFAULT_CONFIG))
.filter_map(|config| getter(config))
)
.next()
.expect("No configuration contained the requested value.")
}
fn fallback_fold<'a, T, V>(initial: Option<&'a V>, getter: impl Fn(&'a Self) -> Option<&'a V>) -> T
where
&'a V: TryInto<T, Error = &'a V> + Into<V>,
V: TryInto<T, Error = V> + BitOrAssign<&'a V> + Debug + 'a
{
let fallback = FALLBACK_FIFO.lock()
.expect("Failed to lock fallback queue.");
let mut fold = None;
for value in initial.into_iter()
.chain(fallback.iter()
.copied()
.chain(core::iter::once(&*DEFAULT_CONFIG))
.filter_map(|config| getter(config))
)
{
let fold_taken = match fold.take()
{
None => value.into(),
Some(mut fold_taken) => {fold_taken |= value; fold_taken}
};
match fold_taken.try_into()
{
Ok(output) => return output,
Err(fold_taken) => fold = Some(fold_taken)
}
}
fold.expect("No configuration contained the requested value.")
.try_into()
.expect("Item incomplete.")
}
pub fn add_fallback(fallback_config: Config)
{
let mut fallback = FALLBACK_FIFO.lock()
.expect(&format!("Failed to lock fallback queue."));
fallback.push(Box::leak(Box::new(fallback_config)));
}
fn find_directory<'a>(dir: impl Into<Cow<'a, Path>>) -> Result<Cow<'a, Path>, FindDirectoryError>
{
let dir = dir.into();
if !dir.exists()
{
return Err(FindDirectoryError::Nonexistant {
path: dir.into()
})
}
if !dir.is_dir()
{
return Err(FindDirectoryError::NotADirectory {
path: dir.into()
})
}
Ok(dir)
}
fn find_env_directory(variable: &str) -> Result<PathBuf, FindEnvDirectoryError>
{
let env = std::env::var(variable)
.map_err(|error| FindEnvDirectoryError::MissingVariable {
error,
variable: variable.to_string()
})?;
let mut dir = match PathBuf::from_str(&env)
{
Ok(dir) => dir,
Err(infallible) => match infallible {}
};
dir = Self::find_directory(dir)
.map_err(|error| FindEnvDirectoryError::NotFound {
error,
variable: variable.to_string()
})?
.into_owned();
Ok(dir)
}
fn home_dir() -> Result<PathBuf, FindHomeDirectoryError>
{
Ok(Self::find_env_directory(HOME_DIRECTORY_ENV_VARIABLE)?)
}
fn xdg_config_home_dir() -> Result<PathBuf, FindXdgConfigHomeDirectoryError>
{
match Self::find_env_directory(XDG_CONFIG_HOME_DIRECTORY_ENV_VARIABLE)
{
Ok(dir) => Ok(dir),
Err(error) => match error.try_into()
{
Ok(error) => Err(error),
Err(dir) => Ok(dir)
}
}
}
fn create_directory(dir: &Path, upon_creation: impl Fn()) -> Result<Cow<'_, Path>, CreateDirectoryError>
{
loop
{
match Self::find_directory(dir)
{
Ok(dir) => return Ok(dir),
Err(error) => match error
{
FindDirectoryError::Nonexistant { path } => {
std::fs::create_dir(&path)
.map_err(|error| CreateDirectoryError::Failed {
path: path,
error
})?;
upon_creation()
},
FindDirectoryError::NotADirectory { path } => {
return Err(CreateDirectoryError::NotADirectory { path }.into())
}
}
}
}
}
fn config_dir() -> Result<PathBuf, FindUserConfigDirectoryError>
{
let mut config_dir = Self::xdg_config_home_dir()?
.join(Path::new("enneagram"));
config_dir = Self::create_directory(&config_dir, || {
#[cfg(feature = "presets")]
for (config_name, yaml) in presets::PRESETS
{
let config_path = Self::config_path(config_name);
std::fs::write(&config_path, yaml)
.expect(&format!("Failed to write preset '{config_name}' to '{}'", config_path.to_string_lossy()))
}
})?
.into_owned();
Ok(config_dir)
}
pub fn config_path<'a>(config: &'a str) -> Cow<'a, Path>
{
let mut config_path = Cow::from(Path::new(config));
if config_path.extension().is_none()
{
config_path = config_path.with_extension("yaml").into()
}
if config_path.components()
.last()
.is_some_and(|last| last.as_os_str().to_string_lossy() == config_path)
{
let user_config_dir = match Self::config_dir()
{
Ok(config_dir) => config_dir,
Err(error) => panic!("User configuration directory not found: {error}")
};
config_path = user_config_dir.join(config_path).into()
}
if !config_path.extension()
.and_then(|extension| extension.to_str())
.is_some_and(|extension| extension == "yaml")
{
panic!("Configuration '{}' should have extension '.yaml'", config_path.to_string_lossy())
}
config_path
}
pub fn default_config_path() -> &'static Path
{
&DEFAULT_CONFIG_PATH
}
fn write_config_path(&self, config_path: &Path)
{
let yaml = serde_saphyr::to_string_with_options(self, serde_saphyr::ser_options!{
empty_as_braces: true,
indent_step: 2,
compact_list_indent: true,
tagged_enums: false,
prefer_block_scalars: true,
quote_all: false,
yaml_12: false
})
.expect(&format!("Unable to serialize configuration '{}'.", config_path.to_string_lossy()));
std::fs::write(config_path, yaml)
.expect(&format!("Unable to create configuration '{}'.", config_path.to_string_lossy()))
}
#[allow(unused)]
pub fn write_config(&self, config: &str)
{
self.write_config_path(&Self::config_path(config))
}
fn read_config_path(config_path: &Path) -> Config
{
let yaml = File::open(config_path)
.expect(&format!("Unable to serialize configuration '{}'.", config_path.to_string_lossy()));
serde_saphyr::from_reader(yaml)
.expect(&format!("Unable to parse configuration '{}'.", config_path.to_string_lossy()))
}
pub fn read_config(config: &str) -> Config
{
Self::read_config_path(&Self::config_path(config))
}
}
enum FindDirectoryError
{
Nonexistant {
path: PathBuf
},
NotADirectory {
path: PathBuf
}
}
impl Display for FindDirectoryError
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result
{
match self
{
Self::Nonexistant { path } => write!(f, "directory '{}' doesn't exist.", path.to_string_lossy()),
Self::NotADirectory { path } => write!(f, "directory '{}' isn't a directory.", path.to_string_lossy())
}
}
}
enum FindEnvDirectoryError
{
MissingVariable {
error: VarError,
variable: String
},
NotFound {
error: FindDirectoryError,
variable: String
}
}
impl Display for FindEnvDirectoryError
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result
{
match self
{
Self::MissingVariable { error, variable } => match error
{
VarError::NotPresent => write!(f, "variable '${variable}' not defined. {error}"),
VarError::NotUnicode(_os_string) => write!(f, "unable to parse variable '${variable}'. {error}")
},
Self::NotFound { error, variable } => write!(f, "variable '${variable}' i.e. {error}")
}
}
}
struct FindHomeDirectoryError(FindEnvDirectoryError);
impl Display for FindHomeDirectoryError
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result
{
let Self(error) = self;
write!(f, "Unable to locate home-directory: {error}")
}
}
impl From<FindEnvDirectoryError> for FindHomeDirectoryError
{
fn from(error: FindEnvDirectoryError) -> Self
{
Self(error) }
}
struct FindXdgConfigHomeDirectoryError(FindEnvDirectoryError);
impl Display for FindXdgConfigHomeDirectoryError
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result
{
let Self(error) = self;
write!(f, "Unable to locate directory for user configuration: {error}")
}
}
impl TryFrom<FindEnvDirectoryError> for FindXdgConfigHomeDirectoryError
{
type Error = PathBuf;
fn try_from(error: FindEnvDirectoryError) -> Result<Self, Self::Error>
{
match &error
{
FindEnvDirectoryError::MissingVariable {
error: VarError::NotPresent,
variable
} if variable == HOME_DIRECTORY_ENV_VARIABLE => match Config::home_dir() {
Ok(home_dir) => match Config::find_directory(home_dir.join(Path::new(CONFIG_SUBDIRECTORY_UNIX)))
{
Ok(config_dir) => Err(config_dir.into_owned()),
Err(_) => Ok(Self(error))
},
Err(FindHomeDirectoryError(error)) => Ok(Self(error))
},
_ => Ok(Self(error))
}
}
}
enum CreateDirectoryError
{
Failed {
path: PathBuf,
error: std::io::Error
},
NotADirectory {
path: PathBuf
}
}
impl Display for CreateDirectoryError
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result
{
match self
{
Self::Failed {
path,
error
} => write!(f, "Creation of directory '{}' failed. {error}", path.to_string_lossy()),
Self::NotADirectory {
path
} => write!(f, "Creation of directory '{}' failed. A file by the same name already exists and is not a directory.", path.to_string_lossy())
}
}
}
enum FindUserConfigDirectoryError
{
Unavailable(FindXdgConfigHomeDirectoryError),
Creation(CreateDirectoryError)
}
impl From<FindXdgConfigHomeDirectoryError> for FindUserConfigDirectoryError
{
fn from(error: FindXdgConfigHomeDirectoryError) -> Self
{
Self::Unavailable(error)
}
}
impl From<CreateDirectoryError> for FindUserConfigDirectoryError
{
fn from(error: CreateDirectoryError) -> Self
{
Self::Creation(error)
}
}
impl Display for FindUserConfigDirectoryError
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result
{
match self
{
Self::Unavailable(error) => write!(f, "Configuration directory unavailable. {error}"),
Self::Creation(error) => write!(f, "Unable to create configuration directory. {error}")
}
}
}
#[cfg(test)]
mod test
{
use crate::config::Config;
#[test]
fn test_serde()
{
let config = Config::default();
let yaml = serde_saphyr::to_string(&config).expect("Serialization failed.");
println!("{yaml}");
let config2 = serde_saphyr::from_str(&yaml).expect("Deserialization failed.");
assert_eq!(config, config2, "Serialized then deserialized config differ from original.")
}
}