tinted-builder-rust 0.19.0

Simple Base16, Base24 and Tinted8 compliant rendering of mustache templates
Documentation
mod cli;
mod operations {
    pub mod build;
    pub mod sync;
}
mod helpers;

use crate::cli::get_matches;
use anyhow::{anyhow, Result};
use std::{borrow, path::PathBuf};

const REPO_NAME: &str = env!("CARGO_PKG_NAME");

fn main() -> Result<()> {
    let matches = get_matches();
    let data_path_result: Result<PathBuf> =
        if let Some(data_dir_path) = matches.get_one::<String>("data-dir") {
            replace_tilde_slash_with_home(data_dir_path)
        } else {
            Ok(dirs::data_dir()
                .ok_or_else(|| anyhow!("Error getting data directory"))?
                .join(format!("tinted-theming/{REPO_NAME}")))
        };
    let data_path = data_path_result?;
    let data_schemes_path = data_path.join("schemes");

    let schemes_path_result: Result<PathBuf> =
        if let Some(schemes_dir) = matches.get_one::<String>("schemes-dir") {
            let schemes_path = PathBuf::from(schemes_dir);
            if !schemes_path.exists() {
                return Err(anyhow!(
                    "The provided schemes path does not exist: {schemes_dir}"
                ));
            }

            replace_tilde_slash_with_home(schemes_dir)
        } else {
            Ok(data_path.join("schemes"))
        };
    let schemes_path = schemes_path_result?;

    match matches.subcommand() {
        Some(("build", sub_matches)) => {
            let ignores = {
                let mut matches = sub_matches
                    .get_many::<String>("ignore")
                    .unwrap_or_default()
                    .cloned()
                    .collect::<Vec<String>>();

                // Ignore common repo files by default
                if matches.is_empty() {
                    matches = vec![
                        "**/*.md".to_string(),
                        "**/.*".to_string(),
                        "**/LICENSE".to_string(),
                    ];
                }

                matches
            };
            let sync = sub_matches
                .get_one::<bool>("sync")
                .is_some_and(ToOwned::to_owned);
            let is_quiet = sub_matches
                .get_one::<bool>("quiet")
                .is_some_and(ToOwned::to_owned);
            let template_dir = sub_matches
                .get_one::<String>("template-dir")
                .cloned()
                .ok_or_else(|| anyhow!("template-dir is required"))?;

            let template_path = PathBuf::from(template_dir);

            if sync {
                operations::sync::sync(&data_schemes_path, is_quiet)?;
            }

            operations::build::build(&template_path, &schemes_path, &ignores, is_quiet)?;
        }
        Some(("sync", sub_matches)) => {
            let is_quiet: bool = sub_matches
                .get_one::<bool>("quiet")
                .is_some_and(borrow::ToOwned::to_owned);

            operations::sync::sync(&data_schemes_path, is_quiet)?;
        }
        _ => {
            println!("Basic usage: {REPO_NAME} apply <SCHEME_NAME>");
            println!("For more information try --help");
        }
    }

    Ok(())
}

/// Expands a leading `~/` to the current user's home directory.
///
/// Returns the original input as a `PathBuf` if not prefixed with `~/`.
fn replace_tilde_slash_with_home(path_str: &str) -> Result<PathBuf> {
    let trimmed_path_str = path_str.trim();
    if trimmed_path_str.starts_with("~/") {
        dirs::home_dir().map_or_else(
            || Err(anyhow!("Unable to determine a home directory for \"{trimmed_path_str}\", please use an absolute path instead")),
            |home_dir|
                Ok(PathBuf::from(trimmed_path_str.replacen(
                    "~/",
                    format!("{}/", home_dir.display()).as_str(),
                    1,
                )))
        )
    } else {
        Ok(PathBuf::from(trimmed_path_str))
    }
}

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

    #[test]
    fn test_replace_tilde_slash_with_home_expands_tilde() {
        let result = replace_tilde_slash_with_home("~/some/path").unwrap();
        let home = dirs::home_dir().unwrap();
        assert_eq!(result, home.join("some/path"));
    }

    #[test]
    fn test_replace_tilde_slash_with_home_absolute_path() {
        let result = replace_tilde_slash_with_home("/absolute/path").unwrap();
        assert_eq!(result, PathBuf::from("/absolute/path"));
    }

    #[test]
    fn test_replace_tilde_slash_with_home_relative_path() {
        let result = replace_tilde_slash_with_home("relative/path").unwrap();
        assert_eq!(result, PathBuf::from("relative/path"));
    }

    #[test]
    fn test_replace_tilde_slash_with_home_trims_whitespace() {
        let result = replace_tilde_slash_with_home("  ~/some/path  ").unwrap();
        let home = dirs::home_dir().unwrap();
        assert_eq!(result, home.join("some/path"));
    }

    #[test]
    fn test_replace_tilde_slash_with_home_tilde_only_no_slash() {
        let result = replace_tilde_slash_with_home("~notapath").unwrap();
        assert_eq!(result, PathBuf::from("~notapath"));
    }
}