hyprshell-core-lib 0.7.0

hyprshell is a Rust-based GUI designed to enhance window management in hyprland
use crate::config::generate::tui::{CONFIGURABLE_LAUNCHER_PLUGINS, WEB_SEARCH_ENGINES};
use crate::config::structs::{
    Config, KeyMaybeMod, Launcher, Mod, Navigate, OpenOverview, OpenSwitch, Overview, Reverse,
    Switch, Windows,
};
use crate::config::Plugin;
use anyhow::{bail, Context};
use ron::extensions::Extensions;
use ron::ser::PrettyConfig;
use ron::Options;
use std::ffi::OsStr;
use std::fs::{create_dir_all, write, File};
use std::path::Path;
use tracing::{info, span, Level};

#[derive(Debug)]
pub struct ConfigData {
    pub enable_launcher: bool,
    pub default_terminal: Option<Box<str>>,
    pub overview: Option<(Mod, KeyMaybeMod)>,
    pub switch: Option<Mod>,
    pub launcher_plugins: Vec<Box<str>>,
    pub launcher_engines: Vec<Box<str>>,
    pub grave_reverse: bool,
}

pub fn generate_config(data: ConfigData) -> Config {
    Config {
        launcher: if data.enable_launcher {
            Some(Launcher {
                default_terminal: data.default_terminal,
                plugins: data
                    .launcher_plugins
                    .into_iter()
                    .filter_map(|plugin| {
                        CONFIGURABLE_LAUNCHER_PLUGINS
                            .iter()
                            .find(|(name, _)| *name == plugin.as_ref())
                            .map(|(_, constructor)| constructor())
                            .map(|plugin| {
                                if let Plugin::WebSearch(_) = plugin {
                                    Plugin::WebSearch(
                                        data.launcher_engines
                                            .iter()
                                            .filter_map(|engine| {
                                                WEB_SEARCH_ENGINES
                                                    .iter()
                                                    .find(|(name, _)| *name == engine.as_ref())
                                                    .map(|(_, constructor)| constructor())
                                            })
                                            .collect(),
                                    )
                                } else {
                                    plugin
                                }
                            })
                    })
                    .collect(),
                ..Default::default()
            })
        } else {
            None
        },
        windows: Some(Windows {
            overview: if let Some(overview) = data.overview {
                Some(Overview {
                    open: OpenOverview {
                        modifier: overview.0,
                        key: overview.1,
                    },
                    navigate: Navigate {
                        reverse: if data.grave_reverse {
                            Reverse::Key("grave".to_string())
                        } else {
                            Reverse::Mod(Mod::Shift)
                        },
                        ..Default::default()
                    },
                    ..Default::default()
                })
            } else {
                None
            },
            switch: if let Some(switch_mod) = data.switch {
                Some(Switch {
                    open: OpenSwitch {
                        modifier: switch_mod,
                    },
                    navigate: Navigate {
                        reverse: if data.grave_reverse {
                            Reverse::Key("grave".to_string())
                        } else {
                            Reverse::Mod(Mod::Shift)
                        },
                        ..Default::default()
                    },
                    ..Default::default()
                })
            } else {
                None
            },
            ..Default::default()
        }),
        ..Default::default()
    }
}

pub fn write_config(config_path: &Path, config: Config, override_file: bool) -> anyhow::Result<()> {
    let _span = span!(Level::TRACE, "write_config").entered();

    if config_path.exists() && !override_file {
        bail!("Config file at {config_path:?} already exists, delete it before generating a new one or use -f to override");
    }
    if let Some(parent) = config_path.parent() {
        create_dir_all(parent)
            .with_context(|| format!("Failed to create config dir at ({parent:?})"))?;
    }
    match config_path.extension().and_then(OsStr::to_str) {
        None | Some("ron") => {
            let file = File::create(config_path)
                .with_context(|| format!("Failed to create config at ({config_path:?})"))?;
            Options::default()
                .with_default_extension(Extensions::IMPLICIT_SOME)
                .with_default_extension(Extensions::UNWRAP_NEWTYPES)
                .with_default_extension(Extensions::UNWRAP_VARIANT_NEWTYPES)
                .to_io_writer_pretty(file, &config, PrettyConfig::default())
                .context("Failed to write ron config")?;
        }
        Some("json") => {
            let file = File::create(config_path)
                .with_context(|| format!("Failed to create config at ({config_path:?})"))?;
            serde_json::to_writer_pretty(file, &config).context("Failed to write json config")?
        }
        #[cfg(feature = "toml_config")]
        Some("toml") => {
            let str = toml::to_string_pretty(&config).context("Failed to write toml config")?;
            write(config_path, str)
                .with_context(|| format!("Failed to create config at ({config_path:?})"))?;
        }
        Some(ext) => bail!("Invalid config file extension: {} (check `FEATURES: ` debug log to see enabled extensions)", ext),
    };

    info!("Config file generated successfully at {:?}", config_path);
    Ok(())
}