use anyhow::{Context, bail};
use ron::Options;
use ron::extensions::Extensions;
use serde::de::DeserializeOwned;
use std::ffi::OsStr;
use std::path::Path;
use tracing::{debug, debug_span, info, trace, warn};
pub fn load_and_migrate_config(
config_file: &Path,
allow_migrate: bool,
) -> anyhow::Result<crate::Config> {
let _span = debug_span!("load_config", path =? config_file).entered();
if !config_file.exists() {
bail!("Config file does not exist, create it using `hyprshell config generate`");
}
#[cfg(feature = "disable_migrations")]
debug!("migrations disabled, not checking if updates are needed");
#[cfg(not(feature = "disable_migrations"))]
{
if crate::migrate::check_migration_needed(config_file)
.inspect_err(|e| warn!("Failed to check if migration is needed: {e:?}"))
.unwrap_or(false)
{
info!("Config needs migration");
if !allow_migrate {
bail!("Config file needs migration, but migration is not allowed.");
}
let migrated = crate::migrate::migrate(config_file);
match migrated {
Ok(config) => {
info!("Config migrated successfully");
let config: crate::Config = config
.try_into()
.context("Failed to convert config to internal format")?;
crate::check(&config)?;
return Ok(config);
}
Err(err) => {
bail!("Config migration failed: \n{err:?}");
}
}
}
trace!("No migration needed");
}
let config: crate::io::Config = load_config_file(config_file).with_context(|| {
format!(
"Failed to load config from file ({})",
config_file.display()
)
})?;
debug!("Loaded config");
let config: crate::Config = config
.try_into()
.context("Failed to convert config to internal format")?;
crate::check(&config)?;
Ok(config)
}
pub fn load_config_file<T: DeserializeOwned>(config_file: &Path) -> anyhow::Result<T> {
let config_file_display = config_file.display();
match config_file.extension().and_then(OsStr::to_str) {
None | Some("ron") => {
let options = Options::default()
.with_default_extension(Extensions::IMPLICIT_SOME)
.with_default_extension(Extensions::UNWRAP_NEWTYPES)
.with_default_extension(Extensions::UNWRAP_VARIANT_NEWTYPES);
let file = std::fs::File::open(config_file)
.with_context(|| format!("Failed to open RON config at ({config_file_display})"))?;
options
.from_reader(file)
.with_context(|| format!("Failed to read RON config at ({config_file_display})"))
}
#[cfg(not(feature = "json5_config"))]
Some("json") => {
let file = std::fs::File::open(config_file).with_context(|| {
format!("Failed to open JSON5 config at ({config_file_display})")
})?;
serde_json::from_reader(file)
.with_context(|| format!("Failed to read JSON5 config at ({config_file_display})"))
}
#[cfg(feature = "json5_config")]
Some("json5" | "json") => {
let file = std::fs::File::open(config_file).with_context(|| {
format!("Failed to open JSON5 config at ({config_file_display})")
})?;
serde_json5::from_reader(file)
.with_context(|| format!("Failed to read JSON5 config at ({config_file_display})"))
}
Some("toml") => {
use std::io::Read;
let mut file = std::fs::File::open(config_file).with_context(|| {
format!("Failed to open TOML config at ({config_file_display})")
})?;
let mut content = String::new();
file.read_to_string(&mut content).with_context(|| {
format!("Failed to read TOML config at ({config_file_display})")
})?;
toml::from_str(&content).context("Failed to parse TOML config")
}
Some(ext) => bail!(
"Invalid config file extension: {ext} (run with -vv and check `FEATURES: ` debug log to see enabled extensions)"
),
}
}