diff --git a/src/config_spec.rs b/src/config_spec.rs index 58f64e4..38aa73e 100644 --- a/src/config_spec.rs +++ b/src/config_spec.rs @@ -37,9 +37,10 @@ //! }; //! // open a config file somewhere (we use a dummy to not cause side-effects) //! let config = KeyValueStore::new_dummy()?; -//! assert_eq!( config.get("modname-user").unwrap(), "xyzzy" ); //! // always: //! CONFIG_SPEC.populate_defaults(&config); +//! // (and then defaults will be set:) +//! assert_eq!( config.get("modname-user").unwrap(), "xyzzy" ); //! ``` //! //! ```ignore @@ -57,11 +58,27 @@ const EMPTY_VALUE: &str = "(empty)"; /// Text shown in place of a password. const HIDDEN_PASSWORD: &str = "*****"; -// highlight some of the info -const ANSI_GROUP: &str = "\x1b[1;31m"; -const ANSI_FIELD: &str = "\x1b[1;33m"; -const ANSI_NOTICE: &str = "\x1b[33m"; -const ANSI_RESET: &str = "\x1b[0m"; +/// semantic highlighting (in contrast to the pure formatting-based stuff you get from colored) +#[rustfmt::skip] +mod style { + use colored::{ColoredString, Colorize}; + /// general emphasis + pub fn emph (text: &str) -> ColoredString { text.underline() } + /// group header + pub fn group (text: &str) -> ColoredString { text.bold() .cyan() } + /// field name or group-level fields + pub fn field (text: &str) -> ColoredString { text.bold() .blue() } + /// low priority (generally field-level extra info) + pub fn info (text: &str) -> ColoredString { text .blue() } + /// default value, or unchanged value + pub fn default (text: &str) -> ColoredString { text .green() } + /// generator (or description thereof) + pub fn generator (text: &str) -> ColoredString { text.bold() .yellow() } + /// changed value + pub fn changed (text: &str) -> ColoredString { text .magenta() } + /// informative messages + pub fn notice (text: &str) -> ColoredString { text .bright_yellow() } +} /* ***** config structure definition ***** */ @@ -69,7 +86,7 @@ const ANSI_RESET: &str = "\x1b[0m"; pub enum CfgField<'a> { /// Silently writes a default to the DB if absent, never prompts for changes. Silent { key: &'a str, default: &'a str }, - /// Writes a default, asks for changes. + /// Writes a default, asks for / allows changes. Default { key: &'a str, default: &'a str, description: &'a str }, /// No default, asks for value, but can stay empty. Optional { key: &'a str, description: &'a str }, @@ -110,6 +127,10 @@ impl<'a> CfgField<'a> { matches!(self, CfgField::Password { .. }) } + fn is_generator(&self) -> bool { + matches!(self, CfgField::Generated { .. }) + } + /// Optional fields are allowed to be (and stay) empty. fn is_optional(&self) -> bool { matches!(self, CfgField::Optional { .. }) @@ -212,18 +233,21 @@ pub fn interactive_check(spec: &CfgSpec, config: KV) -> Result<(), Box Result<(), Box { + for field in group.fields { + if field.is_action_required(&config, group.name) { + check_field(&config, group.name, field, &mut all_valid)?; + } + } + }, 'S' => { if !todo.is_empty() { all_valid = false; @@ -244,8 +275,8 @@ pub fn interactive_check(spec: &CfgSpec, config: KV) -> Result<(), Box { - unreachable!(); - }, // (prompt already checks) + unreachable!(); // (prompt already checks) + }, } } } @@ -256,6 +287,7 @@ pub fn interactive_check(spec: &CfgSpec, config: KV) -> Result<(), Box Result<(), Box> { @@ -267,20 +299,23 @@ fn check_field( let value = config.get(&key).ok(); let mut actions: Vec<&'static str> = Vec::new(); // TODO: adjust order: empty password should offer input first - // TODO: RandomId should offer generating as [R]egenerate if value.is_some() | field.is_password() | field.is_optional() { actions.push("[K]eep as-is"); } if field.default_description().is_some() { - actions.push("[R]eset to default"); + if field.is_generator() { + actions.push("[R]e-generate value"); + } else { + actions.push("[R]eset to default"); + } } if field.is_password() { actions.push("[R]emove"); } actions.push("[I]nput new value"); if !field.is_password() { - actions.push("[L]ong (multiline) input") - }; + actions.push("[L]ong (multiline) input"); + } match prompt_action(&actions) { 'K' => { @@ -310,7 +345,7 @@ fn check_field( config.set(&key, &value)?; }, _ => { - return Err("Wat.".into()); + unreachable!(); // (prompt already checks) }, } Ok(()) @@ -319,8 +354,9 @@ fn check_field( /* ***** displaying various kinds of info ***** */ fn group_header(name: &str, description: &str) { - println!("=============================="); - println!("{}{}{} - {}", ANSI_GROUP, name, ANSI_RESET, description); + println!("{}", style::group(&("=".repeat(name.len() + description.len() + 3)))); + println!("{} - {}", style::group(name), style::field(description)); + println!("{}", style::group(&("=".repeat(name.len() + description.len() + 3)))); println!(""); } @@ -335,14 +371,20 @@ fn show_field(config: &KV, grpname: &str, field: &CfgField) { return; } let key = field.full_key(grpname); - println!("{}{}{} - {}", ANSI_FIELD, &key, ANSI_RESET, field.description()); - println!(" default: {}", field.default_description().unwrap_or(EMPTY_VALUE.to_string())); + println!("{} - {}", style::field(&key), style::info(&field.description())); + let default_desc = field.default_description().unwrap_or(EMPTY_VALUE.to_string()); + println!( + " {} {}", + style::info("default:"), + (if field.is_generator() { style::generator } else { style::default })(&default_desc) + ); let value = config .get(&key) .ok() .map(|s| if field.is_password() { HIDDEN_PASSWORD.to_string() } else { s }) .unwrap_or(EMPTY_VALUE.to_string()); - println!(" current: {}", value); + let fmt = if value == default_desc { style::default } else { style::changed }; + println!(" {} {}", style::info("current:"), fmt(&value)); } /* ***** basic validation ***** */ @@ -362,7 +404,16 @@ fn needs_info(config: &KV, group: &CfgGroup) -> Vec { /// Ask for a choice between several options. First option is the default, /// format options with the first character in brackets like so: `"[E]xample"`. fn prompt_action(choices: &[&str]) -> char { - let prompt_message = choices.join(", "); + let prompt_message = { + let mut formatted_choices = Vec::new(); + if let Some((first, rest)) = choices.split_first() { + formatted_choices.push(format!("{} (default)", style::emph(first))); + formatted_choices.extend(rest.iter().map(|&choice| choice.to_string())); + } else { + panic!("Empty list of choices."); + } + formatted_choices.join(", ") + }; let default_choice = choices[0].to_uppercase().chars().nth(1).unwrap_or_default(); loop { println!("{}", prompt_message);