intuitils 0.0.8

utilities for intuis projects
Documentation
use std::{
    fs::File,
    io::{self, ErrorKind, Read, Write},
    path::PathBuf,
    sync::OnceLock,
};

use color_eyre::{eyre::Context, Result};
use serde::de::DeserializeOwned;
use thiserror::Error;
use xdg::BaseDirectories;

pub mod keybindings;

pub trait IntuiConfig: Sized + DeserializeOwned {
    fn app_name() -> &'static str;
    fn filename() -> &'static str;
    fn default_config() -> &'static str;

    fn should_exit_if_not_found() -> bool;
    fn message_if_not_found() -> Option<String>;

    fn path() -> &'static PathBuf {
        static PATH: OnceLock<PathBuf> = OnceLock::new();
        PATH.get_or_init(|| get_config_path(Self::filename(), Self::app_name()))
    }

    fn init() -> Result<Self> {
        match fetch_config::<Self>(Self::filename(), Self::app_name()) {
            Ok(mut config) => {
                config.post_init();
                Ok(config)
            }
            Err(e) => match e {
                ConfigFetchingError::Io(e) if e.kind() == ErrorKind::NotFound => {
                    let config = put_config::<Self>(
                        Self::default_config(),
                        Self::filename(),
                        Self::app_name(),
                    )?;

                    if let Some(message) = Self::message_if_not_found() {
                        eprintln!("{}", message);
                    }

                    if Self::should_exit_if_not_found() {
                        std::process::exit(0);
                    } else {
                        Ok(config)
                    }
                }
                ConfigFetchingError::Toml(e) => Err(e).with_context(|| {
                    format!(
                        "Failed to parse config located at {:?}",
                        get_config_path(Self::filename(), Self::app_name())
                    )
                }),
                _ => color_eyre::eyre::bail!(e),
            },
        }
    }

    fn post_init(&mut self) {}
}

fn fetch_config<T: DeserializeOwned>(
    config_name: &str,
    app_name: &'static str,
) -> Result<T, ConfigFetchingError> {
    let config_path = xdg_dirs(app_name)
        .find_config_file(config_name)
        .ok_or_else(|| {
            io::Error::new(io::ErrorKind::NotFound, format!("{config_name} not found"))
        })?;

    let mut config_buf = String::new();
    let mut config_file = File::open(config_path)?;
    config_file.read_to_string(&mut config_buf)?;

    Ok(toml::from_str(&config_buf)?)
}

fn put_config<T: DeserializeOwned>(
    content: &'static str,
    filename: &str,
    app_name: &'static str,
) -> Result<T, ConfigFetchingError> {
    let config_path = get_config_path(filename, app_name);
    let mut config_file = File::create(config_path)?;
    config_file.write_all(content.as_bytes())?;
    Ok(toml::from_str(content)?)
}

fn get_config_path(filename: &str, app_name: &'static str) -> PathBuf {
    xdg_dirs(app_name).place_config_file(filename).unwrap()
}

fn xdg_dirs(app_name: &'static str) -> BaseDirectories {
    xdg::BaseDirectories::with_prefix(app_name).unwrap()
}

#[derive(Error, Debug)]
pub enum ConfigFetchingError {
    #[error(transparent)]
    Io(#[from] io::Error),
    #[error(transparent)]
    Toml(#[from] toml::de::Error),
}