Compare commits

...

15 commits
v0.0.1 ... main

9 changed files with 249 additions and 59 deletions

View file

@ -1,15 +1,15 @@
[package]
name = "oink"
version = "0.0.1"
version = "0.2.3"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
#copy_dir = "0.1.3"
pico-args = "0.5.0"
tera = "1.19.0"
termion = "2.0.1"
toml = "0.7.6"
upon = "0.8.1"
[profile.release]
opt-level = "s"

View file

@ -17,9 +17,30 @@ oink full
```
## Configuration
Variables are split into two sections for ease-of-use: `vars` and `colors`.
Targets need two fields, `name` and `path`, containing a unique name and their
destination file respectively. They should go into the `target` array.
```toml
[vars]
name = "Test User"
[colors]
example = "1A1A1A"
[[target]]
name = "oink"
path = "/home/test/.config/oink/oink.toml"
```
## Libraries
- [pico-args](https://crates.io/crates/pico-args) — argument parsing
- [tera](https://crates.io/crates/tera) — template replacement
- [termion](https://crates.io/crates/termion) — ANSI formatting
- [toml](https://crates.io/crates/toml) — configuration parsing
- [upon](https://crates.io/crates/upon) — template engine

34
man/oink.1 Normal file
View file

@ -0,0 +1,34 @@
.Dd $Mdocdate$
.Dt OINK 1
.Os
.Sh NAME
.Nm oink
.Nd a configuration file preprocessor
.Sh SYNOPSIS
.Nm
.Ar command
.Sh DESCRIPTION
.Nm
is a configuration file preprocessor focused on centralizing configuration changes.
.Ss COMMANDS
.Bl -tag -width Ds
.It apply
Copies the last generated files to their target paths.
.It build
Generates files from their templates.
.It full
Runs 'build', then 'apply'.
.El
.Sh EXIT STATUS
.Bl -tag -width Ds
.It 1
No command was given.
.It 2
The configuration file has no items in the target array.
.It 3
Failed to create the configuration directories.
.El
.Sh AUTHORS
.An -nosplit
.An Valerie Wolfe Aq Mt sleeplessval@gmail.com

View file

@ -1,7 +1,7 @@
//! configuration struct and implementations
use std::{
collections::HashMap,
collections::BTreeMap,
env::var,
fs::{ create_dir, read_to_string, File },
io::Error,
@ -9,13 +9,16 @@ use std::{
process::exit
};
use tera::Context;
use upon::Value as ContextValue;
use toml::{
map::Map,
Value
};
use crate::error;
use crate::{ error, util };
pub type Context = BTreeMap<String, ContextValue>;
pub type Table = toml::map::Map<String, Value>;
/// configuration struct
pub struct Config {
@ -53,27 +56,54 @@ impl Config {
}
}
/// build tera context from "vars" and "colors" config sections
pub fn context(&self) -> Context {
/// build context from "vars" and "colors" config sections
pub fn context(&self, target: &Table) -> Context {
let mut output = Context::new();
let mut def: Vec<ContextValue> = Vec::new();
let vars = self.inner.get("vars");
if vars.is_some() {
let vars = vars.unwrap().as_table().unwrap();
// pull global vars
if let Some(Value::Table(vars)) = self.inner.get("vars") {
for (key, value) in vars.iter() {
output.insert(key, value.as_str().unwrap());
let key = key.to_owned();
if let Some(value) = util::convert(value) {
output.insert(key.clone(), value);
def.push(ContextValue::String(key));
}
}
}
let colors = self.inner.get("colors");
if colors.is_some() {
let colors = colors.unwrap().as_table().unwrap();
let mut map = HashMap::<&str, &str>::new();
for (key, value) in colors.iter() {
map.insert(key, value.as_str().unwrap());
// pull target values
for (key, value) in target.iter() {
if key.to_uppercase() == *key {
if let Some(value) = util::convert(value) {
let key = key.to_owned();
output.insert(key.clone(), value);
def.push(ContextValue::String(key));
}
}
output.insert("colors", &map);
}
// pull palette
let palette_name: Option<String> =
if let Some(Value::String(name)) = target.get("use_palette") { Some(name.clone()) }
else if let Some(Value::String(name)) = self.inner.get("use_palette") { Some(name.clone()) }
else { None };
if let Some(Value::Array(array)) = self.inner.get("palette") {
let palette = util::matches(array.to_owned(), palette_name.unwrap_or("default".to_string()));
if let Some(Value::Table(palette)) = palette {
let colors = Context::new();
for(key, value) in palette.iter() {
output.insert(key.to_owned(), value.as_str().unwrap().into());
}
let key = "palette".to_string();
output.insert(key.clone(), colors.into());
def.push(ContextValue::String(key));
}
}
// insert set vars list
output.insert("def".to_string(), def.into());
output
}

View file

@ -3,6 +3,11 @@ use std::{
process::exit
};
pub fn no_command() {
crate::help_text();
exit(1);
}
pub fn no_targets() {
println!("oink: configuration has no targets");
exit(2);

7
src/filter.rs Normal file
View file

@ -0,0 +1,7 @@
use upon::Value;
pub fn has(list: Vec<Value>, key: String) -> bool {
return list.contains(&Value::String(key));
}

View file

@ -1,11 +1,12 @@
use std::process::exit;
use pico_args::Arguments;
use tera::Tera;
mod config;
mod error;
mod filter;
mod operation;
mod util;
use crate::config::Config;
fn main() {
@ -19,33 +20,27 @@ fn main() {
// init configuration
let config = Config::new();
// tera init
let context = config.context();
let template_dir = format!("{}/templates/*", &(config.dir));
let mut tera = Tera::new(&template_dir).unwrap();
// build template dir
let template_dir = format!("{}/templates/", &(config.dir));
let targets = config.targets();
if targets.len() == 0 { error::no_targets(); }
match operation.as_deref() {
Some("apply")
=> {
operation::apply(&targets);
},
=> operation::apply(&targets),
Some("build")
=> {
operation::build(&targets, &mut tera, &context);
},
=> operation::build(&targets, template_dir, &config),
Some("full")
=> {
operation::build(&targets, &mut tera, &context);
operation::build(&targets, template_dir, &config);
println!();
operation::apply(&targets);
},
_
=> {
help_text();
exit(1);
}
_ => error::no_command()
}
}
@ -59,7 +54,7 @@ usage: oink [operation]
oink operations:
apply Copies the last built files to their destination paths.
build Runs oink preprocessing on configuration files.
build Runs oink preprocessing on configuration files.
full Runs 'build' and 'apply'");
}

View file

@ -1,18 +1,41 @@
//! higher-level operation functions
use std::{
env::var,
fs::{ self, File },
fs::{self, read_to_string, File },
io::Write,
path::Path
path::{ Path, PathBuf },
time::SystemTime
};
use termion::{
color::{ self, Fg },
style::{
Bold as BOLD,
Faint as FAINT,
Italic as ITALIC,
Reset as RESET
}
};
use toml::{ map::Map, Value };
use tera::{ Context, Tera };
use upon::Engine;
use crate::{
config::Config,
filter,
util::time
};
static SUCCESS: Fg<color::Green> = Fg(color::Green);
static WARNING: Fg<color::Yellow> = Fg(color::Yellow);
static FAILURE: Fg<color::Red> = Fg(color::Red);
pub fn apply(targets: &Vec<Map<String, Value>>) {
let start = SystemTime::now();
let home = var("HOME").unwrap();
println!("running apply:");
for target in targets {
let start = SystemTime::now();
// get path and name
let path = target.get("path");
let i_name = target.get("name");
@ -21,12 +44,12 @@ pub fn apply(targets: &Vec<Map<String, Value>>) {
if path.is_none() {
if i_name.is_some() {
let name = i_name.unwrap().as_str().unwrap();
println!(" \"{name}\" is missing its path property; skipping");
} else { println!(" skipping empty target"); }
println!(" {WARNING}\"{name}\" is missing its path property; skipping{RESET}");
} else { println!(" {WARNING}skipping empty target{RESET}"); }
continue;
}
if i_name.is_none() {
println!(" target missing name; skipping");
println!(" {WARNING}target missing name; skipping{RESET}");
continue;
}
@ -41,20 +64,36 @@ pub fn apply(targets: &Vec<Map<String, Value>>) {
// copy and print
let result = fs::copy(source, destination);
if result.is_err() { println!(" failed to copy!"); }
else { println!(" completed successfully"); }
let time = time(start);
if result.is_err() {
print!(" {BOLD}{FAILURE}failed to copy{RESET}");
}
else {
print!(" {BOLD}{SUCCESS}completed{RESET}");
}
println!(" {FAINT}({time}){RESET}");
}
let time = time(start);
println!("{FAINT}(apply: {time}){RESET}");
}
pub fn build(targets: &Vec<Map<String, Value>>, tera: &mut Tera, context: &Context) {
pub fn build(targets: &Vec<Map<String, Value>>, template_dir: String, config: &Config) {
let start = SystemTime::now();
let home = var("HOME").unwrap();
println!("running build:");
let mut engine = Engine::new();
engine.add_filter("has", filter::has);
for target in targets {
let start = SystemTime::now();
let context = config.context(target);
// get name property
let i_name = target.get("name");
// handle empty names gracefully
if i_name.is_none() {
println!(" target missing name; skipping");
println!(" {WARNING}target missing name; skipping{RESET}");
continue;
}
@ -62,13 +101,32 @@ pub fn build(targets: &Vec<Map<String, Value>>, tera: &mut Tera, context: &Conte
let name = i_name.unwrap().as_str().unwrap();
println!(" building \"{name}\":");
// render template
let render = tera.render(name, context);
// handle rendering errors gracefully
if render.is_err() {
println!(" failed to render template");
// compile
let compile_start = SystemTime::now();
print!(" {ITALIC}compiling{RESET}");
let mut path = PathBuf::from(&template_dir);
if let Some(Value::String(base)) = target.get("base") { path.push(base); }
else { path.push(name); }
let content = read_to_string(path).unwrap();
let template = engine.compile(&content);
let compile_time = time(compile_start);
print!(" {FAINT}({compile_time}){RESET}");
if let Err(error) = template {
println!("\n {BOLD}{FAILURE}failed to compile template:{RESET}\n {FAILURE}{error}\n {BOLD}skipping{RESET}");
continue;
}
} else { println!(); }
// render
let render_start = SystemTime::now();
print!(" {ITALIC}rendering{RESET}");
let render = template.unwrap().render(&engine, &context).to_string();
let render_time = time(render_start);
print!(" {FAINT}({render_time}){RESET}");
if let Err(error) = render {
println!("\n {BOLD}{FAILURE}failed to render template:{RESET}\n {FAILURE}{error}\n {BOLD}skipping{RESET}");
continue;
} else { println!(); }
// get rendered text and open destination file
let output = render.unwrap();
@ -76,14 +134,19 @@ pub fn build(targets: &Vec<Map<String, Value>>, tera: &mut Tera, context: &Conte
let path = Path::new(&destination);
let i_file = File::create(path);
if i_file.is_err() {
println!(" failed to create destination file at {path:?}");
println!(" {BOLD}{FAILURE}failed to create destination file at {path:?}{RESET}");
continue;
}
let mut file = i_file.unwrap();
// write to destination file
let written = write!(&mut file, "{output}");
if written.is_err() { println!(" failed to write to destination file at {path:?}"); }
else { println!(" completed successfully"); }
if written.is_err() { println!(" {FAILURE}failed to write to destination file at {path:?}{RESET}"); }
else {
let time = time(start);
println!(" {BOLD}{SUCCESS}completed{RESET} {FAINT}({time}){RESET}");
}
}
let time = time(start);
println!("{FAINT}(build: {time}){RESET}");
}

35
src/util.rs Normal file
View file

@ -0,0 +1,35 @@
use std::time::SystemTime;
use upon::Value as ContextValue;
use toml::{ value::Array, Value };
pub fn matches(array: Array, to_match: String) -> Option<Value> {
array.iter().filter(|value| {
if let Value::Table(table) = value {
if let Some(Value::String(name)) = table.get("name") {
return *name == to_match;
}
}
return false;
}).map(|value| value.to_owned()).nth(0)
}
pub fn convert(value: &Value) -> Option<ContextValue> {
match value.clone() {
Value::Boolean(bool) => Some(bool.into()),
Value::Float(float) => Some(float.into()),
Value::Integer(int) => Some(int.into()),
Value::String(string) => Some(string.into()),
_ => None
}
}
pub fn time(start: SystemTime) -> String {
let now = SystemTime::now();
if let Ok(duration) = now.duration_since(start) {
let ms = duration.as_millis();
if ms > 0 { format!("{ms} ms") }
else { "< 1 ms".to_owned() }
} else { String::new() }
}