From 7d234e7f3c0ab6d6ecdb9ace3b24890976b3f875 Mon Sep 17 00:00:00 2001 From: Patrick Haun Date: Tue, 28 Nov 2023 19:44:59 +0100 Subject: [PATCH] feat: configuration file for defaults --- .editorconfig | 7 ++ Cargo.lock | 160 +++++++++++++++++++++++++++++++++++++++- Cargo.toml | 3 + default_config.toml | 5 ++ src/app.rs | 3 - src/config.rs | 91 +++++++++++++++++++++++ src/filter.rs | 41 ++++------ src/log.rs | 58 +-------------- src/log_settings.rs | 62 ++++++++++++++++ src/main.rs | 16 ++-- src/no_color_support.rs | 2 +- src/process.rs | 3 +- 12 files changed, 358 insertions(+), 93 deletions(-) create mode 100644 .editorconfig create mode 100644 default_config.toml create mode 100644 src/config.rs create mode 100644 src/log_settings.rs diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..bf6511b --- /dev/null +++ b/.editorconfig @@ -0,0 +1,7 @@ +root = true + +[*.rs] +indent_style = space +indent_size = 2 +max_line_length = 160 + diff --git a/Cargo.lock b/Cargo.lock index 3693377..a57fb11 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -65,6 +65,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.4.1" @@ -189,12 +195,39 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys", +] + [[package]] name = "either" version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + [[package]] name = "errno" version = "0.3.7" @@ -211,11 +244,14 @@ version = "4.6.0" dependencies = [ "clap", "clap_complete", + "dirs", "handlebars", "lazy_static", "mlua", "regex", + "serde", "serde_json", + "toml", "yansi", ] @@ -229,6 +265,17 @@ dependencies = [ "version_check", ] +[[package]] +name = "getrandom" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "handlebars" version = "4.5.0" @@ -243,6 +290,12 @@ dependencies = [ "thiserror", ] +[[package]] +name = "hashbrown" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" + [[package]] name = "heck" version = "0.4.1" @@ -258,6 +311,16 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "indexmap" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" +dependencies = [ + "equivalent", + "hashbrown", +] + [[package]] name = "itoa" version = "1.0.9" @@ -276,6 +339,17 @@ version = "0.2.150" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" +[[package]] +name = "libredox" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" +dependencies = [ + "bitflags 2.4.1", + "libc", + "redox_syscall", +] + [[package]] name = "linux-raw-sys" version = "0.4.11" @@ -354,6 +428,12 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "pest" version = "2.7.5" @@ -423,6 +503,26 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_users" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4" +dependencies = [ + "getrandom", + "libredox", + "thiserror", +] + [[package]] name = "regex" version = "1.10.2" @@ -464,7 +564,7 @@ version = "0.38.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc99bc2d4f1fed22595588a013687477aedf3cdcfb26558c559edb67b4d9b22e" dependencies = [ - "bitflags", + "bitflags 2.4.1", "errno", "libc", "linux-raw-sys", @@ -508,6 +608,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12022b835073e5b11e90a14f86838ceb1c8fb0325b72416845c487ac0fa95e80" +dependencies = [ + "serde", +] + [[package]] name = "sha2" version = "0.10.8" @@ -556,6 +665,40 @@ dependencies = [ "syn", ] +[[package]] +name = "toml" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a195ec8c9da26928f773888e0742ca3ca1040c6cd859c919c9f59c1954ab35" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + [[package]] name = "typenum" version = "1.17.0" @@ -586,6 +729,12 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + [[package]] name = "which" version = "5.0.0" @@ -665,6 +814,15 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +[[package]] +name = "winnow" +version = "0.5.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829846f3e3db426d4cee4510841b71a8e58aa2a76b1132579487ae430ccd9c7b" +dependencies = [ + "memchr", +] + [[package]] name = "yansi" version = "0.5.1" diff --git a/Cargo.toml b/Cargo.toml index ad9edc4..a23b33c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,9 @@ mlua = { version = "0.9", features = ["lua54", "vendored"] } lazy_static = "1.4.0" handlebars = "4" clap_complete = "4" +serde = { version = "1", features = ["derive"] } +toml = "0.8" +dirs = "5" [dependencies.clap] version = "4" diff --git a/default_config.toml b/default_config.toml new file mode 100644 index 0000000..dbe2c93 --- /dev/null +++ b/default_config.toml @@ -0,0 +1,5 @@ +message_keys = ["short_message", "msg", "message"] +time_keys = ["timestamp", "time", "@timestamp"] +level_keys = ["level", "severity", "log.level", "loglevel"] +main_line_format = "{{bold(fixed_size 19 fblog_timestamp)}} {{level_style (uppercase (fixed_size 5 fblog_level))}}:{{bold(color_rgb 138 43 226 fblog_prefix)}} {{fblog_message}}" +additional_value_format = "{{bold (color_rgb 150 150 150 (fixed_size 25 key))}}: {{value}}" diff --git a/src/app.rs b/src/app.rs index 71d4a06..b420357 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,5 +1,4 @@ use crate::substitution::Substitution; -use crate::template; use clap::{crate_version, value_parser, ArgAction, ValueHint}; use clap::{Arg, Command}; use clap_complete::Shell; @@ -107,14 +106,12 @@ pub fn app() -> Command { Arg::new("main-line-format") .long("main-line-format") .num_args(1) - .default_value(template::DEFAULT_MAIN_LINE_FORMAT) .help("Formats the main fblog output. All log values can be used. fblog provides sanitized variables starting with `fblog_`."), ) .arg( Arg::new("additional-value-format") .long("additional-value-format") .num_args(1) - .default_value(template::DEFAULT_ADDITIONAL_VALUE_FORMAT) .help("Formats the additional value fblog output."), ) .arg( diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..c79f83a --- /dev/null +++ b/src/config.rs @@ -0,0 +1,91 @@ +use std::fs; + +use serde::{Deserialize, Serialize}; + +use crate::template::{DEFAULT_ADDITIONAL_VALUE_FORMAT, DEFAULT_MAIN_LINE_FORMAT}; + +fn default_message_keys() -> Vec { + vec!["short_message".to_string(), "msg".to_string(), "message".to_string()] +} + +fn default_time_keys() -> Vec { + vec!["timestamp".to_string(), "time".to_string(), "@timestamp".to_string()] +} + +fn default_level_keys() -> Vec { + vec!["level".to_string(), "severity".to_string(), "log.level".to_string(), "loglevel".to_string()] +} + +fn default_main_line_format() -> String { + DEFAULT_MAIN_LINE_FORMAT.to_string() +} + +fn default_additional_value_format() -> String { + DEFAULT_ADDITIONAL_VALUE_FORMAT.to_string() +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Config { + #[serde(default = "default_message_keys")] + pub message_keys: Vec, + + #[serde(default = "default_time_keys")] + pub time_keys: Vec, + + #[serde(default = "default_level_keys")] + pub level_keys: Vec, + + #[serde(default = "default_main_line_format")] + pub main_line_format: String, + + #[serde(default = "default_additional_value_format")] + pub additional_value_format: String, +} + +impl Config { + pub fn load() -> Option { + let mut config_file = dirs::config_dir()?; + config_file.push("fblog.toml"); + let config_string = fs::read_to_string(config_file).ok()?; + toml::from_str(&config_string).ok()? + } + pub fn new() -> Config { + Config { + message_keys: default_message_keys(), + time_keys: default_time_keys(), + level_keys: default_level_keys(), + main_line_format: default_main_line_format(), + additional_value_format: default_additional_value_format(), + } + } + + pub fn get() -> Config { + Config::load().unwrap_or_else(Config::new) + } +} + +#[cfg(test)] +mod tests { + use std::fs; + + use super::*; + + #[test] + fn read_defaults_from_empty_config() { + let config: Config = toml::from_str( + r#" + "#, + ) + .unwrap(); + + assert_eq!(config.level_keys, default_level_keys()); + assert_eq!(config.time_keys, default_time_keys()); + assert_eq!(config.message_keys, default_message_keys()); + assert_eq!(config.main_line_format, DEFAULT_MAIN_LINE_FORMAT); + assert_eq!(config.additional_value_format, DEFAULT_ADDITIONAL_VALUE_FORMAT); + + let serialized_defaults = toml::to_string(&config).unwrap(); + let default_config_for_documentation = fs::read_to_string("default_config.toml").unwrap(); + assert_eq!(serialized_defaults, default_config_for_documentation) + } +} diff --git a/src/filter.rs b/src/filter.rs index cd01d83..51f3f00 100644 --- a/src/filter.rs +++ b/src/filter.rs @@ -1,4 +1,4 @@ -use crate::log::LogSettings; +use crate::log_settings::LogSettings; use lazy_static::lazy_static; use mlua::{Error as LuaError, Lua}; use regex::Regex; @@ -116,37 +116,33 @@ mod tests { #[test] fn allow_all() { let log_entry: Map = test_log_entry(); - assert_eq!(true, show_log_entry(&log_entry, "true", true, &LogSettings::new_default_settings()).unwrap()); + assert!(show_log_entry(&log_entry, "true", true, &LogSettings::new_default_settings()).unwrap()); } #[test] fn deny_all() { let log_entry: Map = test_log_entry(); - assert_eq!(false, show_log_entry(&log_entry, "false", true, &LogSettings::new_default_settings()).unwrap()); + assert!(!show_log_entry(&log_entry, "false", true, &LogSettings::new_default_settings()).unwrap()); } #[test] fn filter_process() { let log_entry: Map = test_log_entry(); - assert_eq!( - true, + assert!( show_log_entry(&log_entry, r#"process == "rust""#, true, &LogSettings::new_default_settings()).unwrap() ); - assert_eq!( - false, - show_log_entry(&log_entry, r#"process == "meep""#, true, &LogSettings::new_default_settings()).unwrap() + assert!( + !show_log_entry(&log_entry, r#"process == "meep""#, true, &LogSettings::new_default_settings()).unwrap() ); } #[test] fn filter_logical_operators() { let log_entry: Map = test_log_entry(); - assert_eq!( - true, + assert!( show_log_entry(&log_entry, r#"process == "rust" and fu == "bower""#, true, &LogSettings::new_default_settings()).unwrap() ); - assert_eq!( - true, + assert!( show_log_entry(&log_entry, r#"process == "rust" or fu == "bauer""#, true, &LogSettings::new_default_settings()).unwrap() ); } @@ -154,8 +150,7 @@ mod tests { #[test] fn filter_contains() { let log_entry: Map = test_log_entry(); - assert_eq!( - true, + assert!( show_log_entry( &log_entry, r#"string.find(message, "something") ~= nil"#, @@ -164,31 +159,27 @@ mod tests { ) .unwrap() ); - assert_eq!( - false, - show_log_entry(&log_entry, r#"string.find(message, "bla") ~= nil"#, true, &LogSettings::new_default_settings()).unwrap() + assert!( + !show_log_entry(&log_entry, r#"string.find(message, "bla") ~= nil"#, true, &LogSettings::new_default_settings()).unwrap() ); } #[test] fn filter_regex() { let log_entry: Map = test_log_entry(); - assert_eq!( - true, + assert!( show_log_entry(&log_entry, r#"string.find(fu, "bow.*") ~= nil"#, true, &LogSettings::new_default_settings()).unwrap() ); - assert_eq!( - false, - show_log_entry(&log_entry, r#"string.find(fu, "bow.*sd") ~= nil"#, true, &LogSettings::new_default_settings()).unwrap() + assert!( + !show_log_entry(&log_entry, r#"string.find(fu, "bow.*sd") ~= nil"#, true, &LogSettings::new_default_settings()).unwrap() ); } #[test] fn unknown_variable() { let log_entry: Map = test_log_entry(); - assert_eq!( - false, - show_log_entry( + assert!( + !show_log_entry( &log_entry, r#"sdkfjsdfjsf ~= nil and string.find(sdkfjsdfjsf, "bow.*") ~= nil"#, true, diff --git a/src/log.rs b/src/log.rs index a49ef85..5044a46 100644 --- a/src/log.rs +++ b/src/log.rs @@ -1,5 +1,5 @@ +use crate::log_settings::LogSettings; use crate::no_color_support::style; -use crate::substitution::Substitution; use handlebars::Handlebars; use serde_json::{Map, Value}; use std::borrow::ToOwned; @@ -7,61 +7,6 @@ use std::collections::BTreeMap; use std::io::Write; use yansi::Color; -pub struct LogSettings { - pub message_keys: Vec, - pub time_keys: Vec, - pub level_keys: Vec, - pub additional_values: Vec, - pub excluded_values: Vec, - pub dump_all: bool, - pub with_prefix: bool, - pub print_lua: bool, - pub substitution: Option, -} - -impl LogSettings { - pub fn new_default_settings() -> LogSettings { - LogSettings { - message_keys: vec!["short_message".to_string(), "msg".to_string(), "message".to_string()], - time_keys: vec!["timestamp".to_string(), "time".to_string(), "@timestamp".to_string()], - level_keys: vec!["level".to_string(), "severity".to_string(), "log.level".to_string(), "loglevel".to_string()], - additional_values: vec![], - excluded_values: vec![], - dump_all: false, - with_prefix: false, - print_lua: false, - substitution: None, - } - } - - pub fn add_additional_values(&mut self, mut additional_values: Vec) { - self.additional_values.append(&mut additional_values); - } - - pub fn add_message_keys(&mut self, mut message_keys: Vec) { - message_keys.append(&mut self.message_keys); - self.message_keys = message_keys; - } - - pub fn add_time_keys(&mut self, mut time_keys: Vec) { - time_keys.append(&mut self.time_keys); - self.time_keys = time_keys; - } - - pub fn add_level_keys(&mut self, mut level_keys: Vec) { - level_keys.append(&mut self.level_keys); - self.level_keys = level_keys; - } - - pub fn add_excluded_values(&mut self, mut excluded_values: Vec) { - self.excluded_values.append(&mut excluded_values); - } - - pub fn add_substitution(&mut self, message_template: Substitution) { - self.substitution = Some(message_template) - } -} - pub fn print_log_line( out: &mut dyn Write, maybe_prefix: Option<&str>, @@ -236,6 +181,7 @@ mod tests { assert_eq!(out_to_string(out), "2017-07-06T15:21:16 INFO: something happend\n"); } + #[test] fn write_log_entry_with_prefix() { let handlebars = fblog_handlebar_registry_default_format(); diff --git a/src/log_settings.rs b/src/log_settings.rs new file mode 100644 index 0000000..54fb1bb --- /dev/null +++ b/src/log_settings.rs @@ -0,0 +1,62 @@ +use crate::{config::Config, substitution::Substitution}; + +pub struct LogSettings { + pub message_keys: Vec, + pub time_keys: Vec, + pub level_keys: Vec, + pub additional_values: Vec, + pub excluded_values: Vec, + pub dump_all: bool, + pub with_prefix: bool, + pub print_lua: bool, + pub substitution: Option, +} + +impl LogSettings { + pub fn from_config(config: &Config) -> LogSettings { + LogSettings { + message_keys: config.message_keys.clone(), + time_keys: config.time_keys.clone(), + level_keys: config.level_keys.clone(), + additional_values: vec![], + excluded_values: vec![], + dump_all: false, + with_prefix: false, + print_lua: false, + substitution: None, + } + } + + #[allow(dead_code)] + pub fn new_default_settings() -> LogSettings { + let default_config = Config::new(); + LogSettings::from_config(&default_config) + } + + pub fn add_additional_values(&mut self, mut additional_values: Vec) { + self.additional_values.append(&mut additional_values); + } + + pub fn add_message_keys(&mut self, mut message_keys: Vec) { + message_keys.append(&mut self.message_keys); + self.message_keys = message_keys; + } + + pub fn add_time_keys(&mut self, mut time_keys: Vec) { + time_keys.append(&mut self.time_keys); + self.time_keys = time_keys; + } + + pub fn add_level_keys(&mut self, mut level_keys: Vec) { + level_keys.append(&mut self.level_keys); + self.level_keys = level_keys; + } + + pub fn add_excluded_values(&mut self, mut excluded_values: Vec) { + self.excluded_values.append(&mut excluded_values); + } + + pub fn add_substitution(&mut self, message_template: Substitution) { + self.substitution = Some(message_template) + } +} diff --git a/src/main.rs b/src/main.rs index 22c5eae..c63a304 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,15 +4,18 @@ use std::io; extern crate regex; mod app; +mod config; mod filter; mod log; +mod log_settings; mod no_color_support; mod process; mod substitution; mod template; -use crate::log::LogSettings; +use crate::log_settings::LogSettings; use clap_complete::{generate, Shell}; +use config::Config; use std::fs; use substitution::Substitution; @@ -20,8 +23,6 @@ fn main() { let app = app::app(); let matches = app.get_matches(); - let mut log_settings = LogSettings::new_default_settings(); - if let Some(generator) = matches.get_one::("generate-completions").copied() { let mut app = app::app(); let name = app.get_name().to_string(); @@ -30,6 +31,10 @@ fn main() { return; } + let config: Config = Config::get(); + + let mut log_settings = LogSettings::from_config(&config); + if let Some(values) = matches.get_many::("additional-value") { log_settings.add_additional_values(values.map(ToOwned::to_owned).collect()); } @@ -72,15 +77,14 @@ fn main() { let input_filename = matches.get_one::("INPUT").unwrap(); let mut input = io::BufReader::new(input_read(input_filename)); - // TODO: include profile let main_line_format = matches .get_one::("main-line-format") .map(|s| s.to_string()) - .unwrap_or_else(|| template::DEFAULT_MAIN_LINE_FORMAT.to_string()); + .unwrap_or_else(|| config.main_line_format.to_string()); let additional_value_format = matches .get_one::("additional-value-format") .map(|s| s.to_string()) - .unwrap_or_else(|| template::DEFAULT_ADDITIONAL_VALUE_FORMAT.to_string()); + .unwrap_or_else(|| config.additional_value_format.to_string()); let handlebars = template::fblog_handlebar_registry(main_line_format, additional_value_format); process::process_input(&log_settings, &mut input, maybe_filter, implicit_return, &handlebars) diff --git a/src/no_color_support.rs b/src/no_color_support.rs index 154abad..1b56205 100644 --- a/src/no_color_support.rs +++ b/src/no_color_support.rs @@ -35,5 +35,5 @@ pub fn without_style(styled: &str) -> String { use regex::Regex; let regex = Regex::new("\u{001B}\\[[\\d;]*[^\\d;]").expect("Regex should be valid"); - regex.replace_all(&styled, "").into_owned() + regex.replace_all(styled, "").into_owned() } diff --git a/src/process.rs b/src/process.rs index 3341e47..0a44b31 100644 --- a/src/process.rs +++ b/src/process.rs @@ -1,5 +1,6 @@ use crate::filter; -use crate::log::{self, LogSettings}; +use crate::log; +use crate::log_settings::LogSettings; use crate::no_color_support::style; use handlebars::Handlebars; use lazy_static::lazy_static;