hyprshell-config-lib 4.10.2

A library for reading, writing and migrating configuration files for hyprshell
Documentation
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)"
        ),
    }
}