#![forbid(unsafe_code)] use clap::{crate_authors, crate_version, App, AppSettings, Arg}; use crossbeam::crossbeam_channel::unbounded; use json5::from_str; use notify::{RecommendedWatcher, RecursiveMode, Watcher}; use serde_json::Value; use std::env; use std::error::Error; use std::fs::{create_dir_all, read_dir, read_to_string, write}; use std::process; use tera::{Context, Error as TeraError, Tera}; const DEFAULT_TEMPLATE_DIR: &str = "templates"; const DEFAULT_OUTPUT_DIR: &str = "output"; fn main() -> Result<(), Box> { process::exit(match run() { Ok(_) => 0, Err(e) => { eprintln!("{}", e); 1 } }) } fn env_or_default(env_name: &str, default: &str) -> String { env::var(env_name).unwrap_or_else(|_| String::from(default)) } fn run() -> Result<(), Box> { let config: Value = from_str(&read_to_string("config.json5")?)?; let template_dir = env_or_default("PANRES_TEMPLATE_DIR", DEFAULT_TEMPLATE_DIR); let output_dir = env_or_default("PANRES_OUTPUT_DIR", DEFAULT_OUTPUT_DIR); let matches = get_args(); let tera = Tera::new(&format!("{}/**/*", template_dir))?; let mut context = Context::new(); context.insert("config", &config); let outputs: Vec = matches .values_of("output-format") .unwrap_or_default() .map(String::from) .collect(); output(&tera, &context, &output_dir, &outputs, &template_dir)?; if matches.is_present("watch") { watch_mode(tera, context, output_dir, outputs, template_dir)?; } Ok(()) } /// Returns the args passed by the user. fn get_args() -> clap::ArgMatches<'static> { App::new("panres") .version(crate_version!()) .author(crate_authors!()) .about("Universal resume formatter") .arg(Arg::with_name("watch").short("w").help("Watch for changes")) .arg( Arg::with_name("output-format") .help("Specifies which output format you want") .required(true) .multiple(true), ) .settings(&[AppSettings::ArgRequiredElseHelp]) .get_matches() } /// Usually never returns, unless there was an error initializing the watcher. /// Handles watching for file changes, and reloads tera if there's a change. fn watch_mode<'a>( mut engine: Tera, context: Context, dir: String, outputs: Vec, template_dir: String, ) -> Result<(), Box> { let (tx, rx) = unbounded(); let mut watcher: RecommendedWatcher = Watcher::new_immediate(move |res| tx.send(res).unwrap())?; watcher.watch(&template_dir, RecursiveMode::Recursive)?; for res in rx { match res { Err(e) => println!("{}", e), Ok(event) => { println!("got event {:?}", event); engine.full_reload().expect("Failed to perform full reload"); output(&engine, &context, &dir, &outputs, &template_dir) .expect("Failed to call output"); } } } Ok(()) } /// Parses the output values and generates a file for each format specified or /// found, if told to generate all outputs. fn output<'a>( engine: &Tera, context: &Context, dir: &str, outputs: &[String], template_dir: &'a str, ) -> Result<(), Box> { if outputs.contains(&String::from("all")) { for output in read_dir(template_dir)? { write_file(engine, context, dir, &output?.file_name().to_str().unwrap())?; } } else { for output in outputs { write_file(engine, context, dir, &output)?; } } Ok(()) } /// Write out the post-template file to the output dir. fn write_file(engine: &Tera, context: &Context, dir: &str, format: &str) -> Result<(), TeraError> { create_dir_all(dir).expect("Could not create output dir"); write( format!("{}/{}", dir, format), engine.render(format, context)?, ) .expect("to be able to write to output folder"); Ok(()) }