shellock-homes 0.1.0

opinonated dotfile manager
Documentation
use crate::constants::{CONFIG_FILE_NAME, SHELLOCK_HOMES};
use anyhow::Result;
use std::{env, path::PathBuf};
use thiserror::Error;

#[derive(Debug, Error)]
pub enum PathError {
    #[error("no matching directory stem, got: {got:?}, valid: {valid:?}")]
    NoMatchingStem { got: PathBuf, valid: Vec<PathBuf> },
}

pub fn home_dir() -> PathBuf {
    PathBuf::from(env::var("HOME").expect("can not read HOME environment variable"))
}

pub fn data_dir() -> PathBuf {
    let xdg_dirs = xdg::BaseDirectories::with_prefix(SHELLOCK_HOMES)
        .expect("unable to get XDG base directories");
    xdg_dirs.get_data_home()
}

pub fn config_dir() -> PathBuf {
    let xdg_dirs = xdg::BaseDirectories::with_prefix(SHELLOCK_HOMES)
        .expect("unable to get XDG base directories");
    xdg_dirs.get_config_home()
}

pub fn config_file_name() -> PathBuf {
    config_dir().join(CONFIG_FILE_NAME)
}

pub fn strip(path: PathBuf) -> PathBuf {
    if path.starts_with(data_dir()) {
        return path.strip_prefix(data_dir()).unwrap().to_path_buf();
    }
    if path.starts_with(home_dir()) {
        return path.strip_prefix(home_dir()).unwrap().to_path_buf();
    }

    path
}

pub fn flip_path(path: PathBuf) -> Result<PathBuf> {
    let home = home_dir();
    let data = data_dir();

    if path.starts_with(&data) {
        return Ok(home.join(path.strip_prefix(data)?));
    }
    if path.starts_with(&home) {
        return Ok(data.join(path.strip_prefix(home)?));
    }

    Err(PathError::NoMatchingStem {
        got: path,
        valid: vec![home, data],
    }
    .into())
}

#[cfg(test)]
mod tests {
    use super::*;
    use serial_test::serial;

    #[test]
    #[serial]
    fn test_dirs() {
        let temp = tempfile::tempdir().unwrap();
        env::set_var("HOME", temp.path().as_os_str().to_str().unwrap());
        env::set_var("XDG_DATA_HOME", "");

        let home = home_dir();
        assert_eq!(home, temp.path());

        let cfg = config_dir();
        assert_eq!(
            cfg,
            temp.path()
                .join(PathBuf::from(".config"))
                .join(PathBuf::from(SHELLOCK_HOMES))
        );

        let cfg_name = config_file_name();
        assert_eq!(
            cfg_name,
            temp.path()
                .join(PathBuf::from(".config"))
                .join(PathBuf::from(SHELLOCK_HOMES).join(PathBuf::from(CONFIG_FILE_NAME)))
        );

        let data = data_dir();
        assert_eq!(
            data,
            temp.path()
                .join(PathBuf::from(".local/share"))
                .join(PathBuf::from(SHELLOCK_HOMES))
        );
    }

    macro_rules! test_flip_path {
        ($name:ident, $input:expr) => {
            #[test]
            #[serial]
            fn $name() {
                let temp = tempfile::tempdir().unwrap();
                env::set_var("HOME", temp.path().as_os_str().to_str().unwrap());

                // flip from home to data
                let source = home_dir().join(PathBuf::from($input));
                let res = flip_path(source.clone());
                assert!(
                    res.is_ok(),
                    "result was not ok: {:?}, source: {:?}",
                    res.err(),
                    source
                );
                assert_eq!(res.unwrap(), data_dir().join(PathBuf::from($input)));

                // flip from data to home
                let source = data_dir().join(PathBuf::from($input));
                let res = flip_path(source.clone());
                assert!(
                    res.is_ok(),
                    "result was not ok: {:?}, source: {:?}",
                    res.err(),
                    source
                );
                assert_eq!(res.unwrap(), home_dir().join(PathBuf::from($input)));
            }
        };
    }

    test_flip_path!(basic, "test/1234");
}