use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
use std::sync::OnceLock;
use eyre::Result;
use tera::Context;
use crate::config::settings::MisercSettings;
use crate::dirs;
use crate::env;
use crate::file;
use crate::tera::{get_miserc_tera, take_tera_accessed_files};
static MISERC: OnceLock<MisercSettings> = OnceLock::new();
pub fn init() -> Result<()> {
let settings = load_miserc_settings()?;
let _ = MISERC.set(settings);
let _ = take_tera_accessed_files();
Ok(())
}
pub fn get() -> &'static MisercSettings {
MISERC.get_or_init(|| {
let settings = load_miserc_settings().unwrap_or_default();
let _ = take_tera_accessed_files();
settings
})
}
pub fn get_env() -> Option<&'static Vec<String>> {
get().env.as_ref()
}
pub fn get_ceiling_paths() -> Option<&'static BTreeSet<PathBuf>> {
get().ceiling_paths.as_ref()
}
pub fn get_ignored_config_paths() -> Option<&'static BTreeSet<PathBuf>> {
get().ignored_config_paths.as_ref()
}
pub fn get_override_config_filenames() -> Option<&'static Vec<String>> {
get().override_config_filenames.as_ref()
}
pub fn get_override_tool_versions_filenames() -> Option<&'static Vec<String>> {
get().override_tool_versions_filenames.as_ref()
}
fn render_miserc_template(
tera: &mut Option<tera::Tera>,
content: &str,
config_root: &Path,
) -> String {
if !content.contains("{{") && !content.contains("{%") && !content.contains("{#") {
return content.to_string();
}
let tera = tera.get_or_insert_with(get_miserc_tera);
let mut context = Context::new();
context.insert("env", &*env::PRISTINE_ENV);
context.insert("config_root", config_root);
match std::env::current_dir() {
Ok(dir) => context.insert("cwd", &dir),
Err(e) => {
debug!("miserc template: could not determine cwd, `cwd` will be unavailable: {e}")
}
};
context.insert("xdg_cache_home", &*env::XDG_CACHE_HOME);
context.insert("xdg_config_home", &*env::XDG_CONFIG_HOME);
context.insert("xdg_data_home", &*env::XDG_DATA_HOME);
context.insert("xdg_state_home", &*env::XDG_STATE_HOME);
match tera.render_str(content, &context) {
Ok(rendered) => rendered,
Err(e) => {
warn!("Failed to render template in miserc: {e}");
content.to_string()
}
}
}
fn load_miserc_settings() -> Result<MisercSettings> {
let mut merged = MisercSettings::default();
let files = find_miserc_files();
let mut tera: Option<tera::Tera> = None;
for path in files.into_iter().rev() {
if let Ok(content) = file::read_to_string(&path) {
let config_root = path.parent().unwrap_or(Path::new("."));
let content = render_miserc_template(&mut tera, &content, config_root);
match toml::from_str::<MisercSettings>(&content) {
Ok(settings) => {
merge_settings(&mut merged, settings);
}
Err(e) => {
warn!("Failed to parse {}: {}", path.display(), e);
}
}
}
}
Ok(merged)
}
fn merge_settings(target: &mut MisercSettings, source: MisercSettings) {
if source.env.is_some() {
target.env = source.env;
}
if source.ceiling_paths.is_some() {
target.ceiling_paths = source.ceiling_paths;
}
if source.ignored_config_paths.is_some() {
target.ignored_config_paths = source.ignored_config_paths;
}
if source.override_config_filenames.is_some() {
target.override_config_filenames = source.override_config_filenames;
}
if source.override_tool_versions_filenames.is_some() {
target.override_tool_versions_filenames = source.override_tool_versions_filenames;
}
}
fn find_miserc_files() -> Vec<PathBuf> {
let mut files = Vec::new();
if let Ok(cwd) = std::env::current_dir() {
let home: &Path = &dirs::HOME;
for dir in cwd.ancestors() {
let path = dir.join(".miserc.toml");
if path.is_file() {
files.push(path);
}
if dir == home || dir.parent().is_none() {
break;
}
let path = dir.join(".config").join("miserc.toml");
if path.is_file() {
files.push(path);
}
}
}
let global_path = dirs::CONFIG.join("miserc.toml");
if global_path.is_file() {
files.push(global_path);
}
let system_dir = env::MISE_SYSTEM_CONFIG_DIR.clone();
let system_path = system_dir.join("miserc.toml");
if system_path.is_file() {
files.push(system_path);
}
files
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_merge_settings() {
let mut target = MisercSettings {
env: Some(vec!["base".to_string()]),
..Default::default()
};
let source = MisercSettings {
env: Some(vec!["override".to_string()]),
..Default::default()
};
merge_settings(&mut target, source);
assert_eq!(target.env, Some(vec!["override".to_string()]));
}
#[test]
fn test_parse_miserc() {
let content = r#"
env = ["development", "local"]
ceiling_paths = ["/home/user"]
"#;
let settings: MisercSettings = toml::from_str(content).unwrap();
assert_eq!(
settings.env,
Some(vec!["development".to_string(), "local".to_string()])
);
assert!(settings.ceiling_paths.is_some());
}
#[test]
fn test_render_miserc_template_no_op() {
let mut tera = None;
let content = r#"env = ["development"]"#;
let result = render_miserc_template(&mut tera, content, Path::new("/home/user"));
assert_eq!(result, content);
}
#[test]
fn test_render_miserc_template_env_var() {
let mut tera = None;
let home = env::PRISTINE_ENV
.get("HOME")
.cloned()
.unwrap_or_else(|| "/root".to_string());
let content = r#"ceiling_paths = ["{{ env.HOME }}"]"#;
let result = render_miserc_template(&mut tera, content, Path::new("/some/dir"));
assert!(
result.contains(&home),
"Expected HOME ({home}) in rendered output, got: {result}"
);
}
#[test]
fn test_render_miserc_template_config_root() {
let mut tera = None;
let config_root = Path::new("/my/project");
let content = r#"ceiling_paths = ["{{ config_root }}"]"#;
let result = render_miserc_template(&mut tera, content, config_root);
assert!(
result.contains("/my/project"),
"Expected config_root in rendered output, got: {result}"
);
}
#[test]
fn test_render_miserc_template_os_function() {
let mut tera = None;
let content = r#"env = ["{{ os() }}"]"#;
let result = render_miserc_template(&mut tera, content, Path::new("/some/dir"));
assert!(
!result.contains("{{ os() }}"),
"Template was not rendered: {result}"
);
}
#[test]
fn test_render_miserc_template_invalid_falls_back() {
let mut tera = None;
let content = r#"ceiling_paths = ["{{ undefined_function_xyz() }}"]"#;
let result = render_miserc_template(&mut tera, content, Path::new("/some/dir"));
assert_eq!(result, content);
}
}