diff --git a/Cargo.lock b/Cargo.lock index ad6377d..4d09dc1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -74,6 +74,15 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "atomic" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d818003e740b63afc82337e3160717f4f63078720a810b7b903e70a5d1d2994" +dependencies = [ + "bytemuck", +] + [[package]] name = "backtrace" version = "0.3.69" @@ -129,6 +138,12 @@ dependencies = [ "serde", ] +[[package]] +name = "bytemuck" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374d28ec25809ee0e23827c2ab573d729e293f281dfe393500e7ad618baa61c6" + [[package]] name = "cc" version = "1.0.83" @@ -270,6 +285,20 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "figment" +version = "0.10.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7629b8c7bcd214a072c2c88b263b5bb3ceb54c34365d8c41c1665461aeae0993" +dependencies = [ + "atomic", + "pear", + "serde", + "toml", + "uncased", + "version_check", +] + [[package]] name = "form_urlencoded" version = "1.2.1" @@ -403,6 +432,12 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "inlinable_string" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb" + [[package]] name = "is-terminal" version = "0.4.9" @@ -603,6 +638,29 @@ version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" +[[package]] +name = "pear" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ccca0f6c17acc81df8e242ed473ec144cbf5c98037e69aa6d144780aad103c8" +dependencies = [ + "inlinable_string", + "pear_codegen", + "yansi", +] + +[[package]] +name = "pear_codegen" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e22670e8eb757cff11d6c199ca7b987f352f0346e0be4dd23869ec72cb53c77" +dependencies = [ + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -679,6 +737,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "proc-macro2-diagnostics" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "version_check", + "yansi", +] + [[package]] name = "quote" version = "1.0.35" @@ -819,6 +890,7 @@ version = "0.1.0" dependencies = [ "clap", "dirs", + "figment", "git2", "globset", "handlebars", @@ -1005,6 +1077,15 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" +[[package]] +name = "uncased" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b9bc53168a4be7402ab86c3aad243a84dd7381d09be0eddc81280c1da95ca68" +dependencies = [ + "version_check", +] + [[package]] name = "unicode-bidi" version = "0.3.14" @@ -1244,3 +1325,9 @@ checksum = "b7520bbdec7211caa7c4e682eb1fbe07abe20cee6756b6e00f537c82c11816aa" dependencies = [ "memchr", ] + +[[package]] +name = "yansi" +version = "1.0.0-rc.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1367295b8f788d371ce2dbc842c7b709c73ee1364d30351dd300ec2203b12377" diff --git a/Cargo.toml b/Cargo.toml index b597398..b2e2fc5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ edition = "2021" [dependencies] clap = { version = "4.4.14", features = ["derive", "env"] } dirs = "5.0.1" +figment = { version = "0.10.13", features = ["toml", "env"] } git2 = "0.18.1" globset = { version = "0.4.14", features = ["serde", "serde1"] } handlebars = "5.0.0" diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..143ef88 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,40 @@ +use std::{collections::HashMap, path::Path}; + +use figment::{ + providers::{Env, Format, Serialized, Toml}, + Figment, +}; +use miette::{Context, IntoDiagnostic, Result}; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct SiloConfig { + /// Diff tool used to display file differences + pub diff_tool: String, + /// Context values for handlebars that are available globally under the `ctx` variable + pub template_context: HashMap, +} + +impl Default for SiloConfig { + fn default() -> Self { + Self { + diff_tool: String::from("diff"), + template_context: HashMap::new(), + } + } +} + +/// Read the configuration file from the user config directory +/// with overrides from the `repo.toml` file +/// and the `repo.local.toml` config file +/// and environment variables prefixed with `SILO_`` +pub fn read_config(repo: &Path) -> Result { + Figment::from(Serialized::defaults(SiloConfig::default())) + .merge(Toml::file(dirs::config_dir().unwrap().join("silo.toml"))) + .merge(Toml::file(repo.join("repo.toml"))) + .merge(Toml::file(repo.join("repo.local.toml"))) + .merge(Env::prefixed("SILO_")) + .extract() + .into_diagnostic() + .context("parsing config file") +} diff --git a/src/main.rs b/src/main.rs index e5126fb..76517ef 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,6 +6,7 @@ use miette::{Context, IntoDiagnostic, Result}; use repo::SiloRepo; mod args; +mod config; mod repo; mod templating; @@ -17,9 +18,11 @@ fn main() -> Result<()> { args::Command::Init => init(&args)?, args::Command::Apply => apply(&args)?, args::Command::Context => { + let repo = SiloRepo::open(&args.repo)?; println!( "{}", - serde_json::to_string_pretty(templating::context()).into_diagnostic()? + serde_json::to_string_pretty(&templating::context(repo.config.template_context)) + .into_diagnostic()? ) } args::Command::Repo => { @@ -59,6 +62,10 @@ fn init(args: &Args) -> Result<()> { let _gitrepo = git2::Repository::init(&args.repo) .into_diagnostic() .with_context(|| format!("initializing repository at {:?}", args.repo))?; + fs::write(args.repo.join(".gitignore"), "repo.local.toml\n") + .into_diagnostic() + .context("adding default .gitignore")?; + log::info!("Repo initialized at {:?}", args.repo); Ok(()) diff --git a/src/repo.rs b/src/repo.rs index 6b81d2f..ff32476 100644 --- a/src/repo.rs +++ b/src/repo.rs @@ -8,12 +8,16 @@ use std::{ rc::Rc, }; -use crate::templating; +use crate::{ + config::{read_config, SiloConfig}, + templating, +}; use lazy_static::lazy_static; #[derive(Clone, Debug)] pub struct SiloRepo { pub root: DirEntry, + pub config: SiloConfig, } impl SiloRepo { @@ -23,6 +27,7 @@ impl SiloRepo { } Ok(Self { + config: read_config(path)?, root: DirEntry::parse(Rc::new(ParseContext::default()), path.to_owned())?, }) } @@ -31,7 +36,10 @@ impl SiloRepo { let cwd = env::current_dir() .into_diagnostic() .context("get current dir")?; - self.root.apply(&cwd) + let ctx = ApplyContext { + config: self.config.clone(), + }; + self.root.apply(&ctx, &cwd) } } @@ -53,6 +61,10 @@ impl Default for ParseContext { } } +pub struct ApplyContext { + config: SiloConfig, +} + lazy_static! { static ref IGNORED_PATHS: GlobSet = GlobSetBuilder::new() .add(Glob::new("**/.git").unwrap()) @@ -118,9 +130,9 @@ impl DirEntry { } } - fn apply(&self, cwd: &Path) -> Result<()> { + fn apply(&self, ctx: &ApplyContext, cwd: &Path) -> Result<()> { match self { - DirEntry::File(file) => file.apply(cwd), + DirEntry::File(file) => file.apply(ctx, cwd), DirEntry::Dir(p, children) => { let cwd = if p != cwd { let cwd = cwd.join(p.file_name().unwrap()); @@ -135,14 +147,17 @@ impl DirEntry { p.to_owned() }; for child in children { - child.apply(&cwd)?; + child.apply(ctx, &cwd)?; } Ok(()) } DirEntry::Root(_, data, children) => { let engine = templating::engine(); let rendered_path = engine - .render_template(&data.path, templating::context()) + .render_template( + &data.path, + &templating::context(&ctx.config.template_context), + ) .into_diagnostic() .with_context(|| format!("render template {}", data.path))?; @@ -155,7 +170,7 @@ impl DirEntry { .with_context(|| format!("Creating directory {cwd:?}"))?; } for child in children { - child.apply(&cwd)?; + child.apply(ctx, &cwd)?; } Ok(()) } @@ -180,14 +195,17 @@ impl FileEntry { } } - fn apply(&self, cwd: &Path) -> Result<()> { + fn apply(&self, ctx: &ApplyContext, cwd: &Path) -> Result<()> { match self { FileEntry::Template(path) => { log::debug!("Processing template {path:?}"); let contents = fs::read_to_string(path).into_diagnostic()?; let rendered = templating::engine() - .render_template(&contents, templating::context()) + .render_template( + &contents, + &templating::context(&ctx.config.template_context), + ) .into_diagnostic() .with_context(|| format!("rendering template {path:?}"))?; @@ -237,7 +255,7 @@ impl RootDirData { .into_diagnostic() .with_context(|| format!("reading metadata file {path:?}"))?; let rendered = templating::engine() - .render_template(&contents, templating::context()) + .render_template(&contents, &templating::context(())) .into_diagnostic() .with_context(|| format!("processing template {path:?}"))?; toml::from_str(&rendered) diff --git a/src/templating.rs b/src/templating.rs index 1f0feb3..b462965 100644 --- a/src/templating.rs +++ b/src/templating.rs @@ -11,11 +11,18 @@ pub fn engine<'a>() -> Handlebars<'a> { hb } -pub fn context<'a>() -> &'a ContextData { +pub fn context<'a, T: Serialize>(ctx: T) -> WrappedContext<'a, T> { lazy_static! { static ref CTX: ContextData = ContextData::default(); } - &*CTX + WrappedContext { data: &*CTX, ctx } +} + +#[derive(Serialize)] +pub struct WrappedContext<'a, T: Serialize> { + #[serde(flatten)] + data: &'a ContextData, + ctx: T, } #[derive(Clone, Debug, Serialize, Default)]