use crate::is_dry_run; use crate::key_value::KeyValueStore as KV; use std::error::Error; use std::io::{self, Write}; /// Text shown for an empty field. 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"; /* ***** config structure definition ***** */ /// Describes a field in the configuration. 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. 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 }, /// Empty by default, required, will prompt without echo. Password { key: &'a str, description: &'a str }, /// Empty by default, can be user-provided, or will be generated randomly. RandomId { key: &'a str, generator: fn(config: &KV, is_dry_run: bool) -> Result>, generator_description: &'a str, description: &'a str, }, } /// A group of related config fields. The final key of an inner value will be /// `{group.name}-{key}`. pub struct CfgGroup<'a> { pub name: &'a str, pub description: &'a str, pub fields: &'a [CfgField<'a>], } /// A description of the entire config, as a collection of named groups. /// /// Used both to populate defaults and to interactively adjust the configuration. pub struct CfgSpec<'a> { pub groups: &'a [CfgGroup<'a>], } impl<'a> CfgField<'a> { /// Silent fields don't get prompted or shown. fn is_silent(&self) -> bool { matches!(self, CfgField::Silent { .. }) } /// Password fields will be censored when displayed. fn is_password(&self) -> bool { matches!(self, CfgField::Password { .. }) } /// Optional fields are allowed to be (and stay) empty. fn is_optional(&self) -> bool { matches!(self, CfgField::Optional { .. }) } /// Reports if the field needs changing or is ok as-is. /// /// (Because of the default population at boot, Silent and Default must /// *necessarily* always have a value. Optional is optional. Only /// Password and RandomId might need actions if they are missing.) fn is_action_required(&self, config: &KV, grpname: &str) -> bool { if matches!( self, CfgField::Silent { .. } | CfgField::Default { .. } | CfgField::Optional { .. } ) { false } else { config.get(self.full_key(grpname).as_str()).ok().is_none() } } /// Gets `key`, which is the local part of the field name. (`.full_key` is used for the DB.) fn key(&self) -> String { match self { CfgField::Silent { key, .. } => key, CfgField::Default { key, .. } => key, CfgField::Optional { key, .. } => key, CfgField::Password { key, .. } => key, CfgField::RandomId { key, .. } => key, } .to_string() } /// Full field name / key used in the DB (currently `{grpname}-{fieldname}`.) fn full_key(&self, grpname: &str) -> String { format!("{}-{}", grpname, self.key()) } /// Gets `description` or "(none)". fn description(&self) -> String { match self { CfgField::Silent { .. } => "(none)", CfgField::Default { description, .. } => description, CfgField::Optional { description, .. } => description, CfgField::Password { description, .. } => description, CfgField::RandomId { description, .. } => description, } .to_string() } /// Gets a description of the default value if one exists. /// (Either the value itself, or a description of the generator making a default value.) fn default_description(&self) -> Option { match self { CfgField::Silent { default, .. } => Some(default.to_string()), CfgField::Default { default, .. } => Some(default.to_string()), CfgField::RandomId { generator_description, .. } => { Some(format!("{}{}{}", '(', generator_description, ')')) }, _ => None, } } /// Gets the actual default value, which may involve running a generator. fn default_value(&self, config: &KV) -> Result, Box> { match self { CfgField::Silent { default, .. } => Ok(Some(default.to_string())), CfgField::Default { default, .. } => Ok(Some(default.to_string())), CfgField::RandomId { generator, .. } => generator(config, is_dry_run()).map(Some), _ => Ok(None), } } } /// Ensure all fields with known default values exist. pub fn populate_defaults(spec: &CfgSpec, config: &KV) { for group in spec.groups { for field in group.fields { match field { CfgField::Silent { default, .. } => { config.default(&field.full_key(group.name), default) }, CfgField::Default { default, .. } => { config.default(&field.full_key(group.name), default) }, _ => {}, } } } } /// Check all groups / fields and interactively edit as needed. /// /// Will report if the config is fine or has missing values. pub fn interactive_check(spec: &CfgSpec, config: KV) -> Result<(), Box> { let mut all_valid: bool = true; for group in spec.groups { group_header(group.name, group.description); let todo = needs_info(&config, group); // TODO: add distinction between edit all / edit necessary only let choices = if !todo.is_empty() { println!( "{}The following fields need adjustment: {}{}", ANSI_NOTICE, todo.join(", "), ANSI_RESET ); &["[E]dit (default)", "[L]ist all", "[S]kip group"] } else { println!( "{}This group looks fine. OK to [S]kip, unless you want to adjust values here.{}", ANSI_NOTICE, ANSI_RESET ); &["[S]kip group (default)", "[E]dit", "[L]ist all"] }; loop { let choice = prompt_action(choices); match choice { 'L' => { show_group(&config, group); }, 'E' => { for field in group.fields { check_field(&config, group.name, field, &mut all_valid)?; } break; }, 'S' => { if !todo.is_empty() { all_valid = false; } break; }, _ => { unreachable!(); }, // (prompt already checks) } } } if all_valid { Ok(()) } else { Err("WARNING: Config is incomplete. Re-run to fix.".into()) } } fn check_field( config: &KV, grpname: &str, field: &CfgField, ok: &mut bool, ) -> Result<(), Box> { if field.is_silent() { return Ok(()); } show_field(config, grpname, field); let key = field.full_key(grpname); 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_password() { actions.push("[R]emove"); } actions.push("[I]nput new value"); if !field.is_password() { actions.push("[L]ong (multiline) input") }; match prompt_action(&actions) { 'K' => { // we allow leaving a password empty, but that means config is incomplete if value.is_none() & !field.is_optional() { *ok &= false; } }, 'R' => { match field.default_value(&config)? { Some(value) => { config.set(&key, &value)?; }, // password again None => { config.delete(&key)?; *ok &= false; }, } }, 'I' => { let value = if field.is_password() { prompt_password() } else { prompt_single_line() }; config.set(&key, &value)?; }, 'L' => { let value = prompt_multiline(); config.set(&key, &value)?; }, _ => { return Err("Wat.".into()); }, } Ok(()) } /* ***** displaying various kinds of info ***** */ fn group_header(name: &str, description: &str) { println!("=============================="); println!("{}{}{} - {}", ANSI_GROUP, name, ANSI_RESET, description); println!(""); } fn show_group(config: &KV, group: &CfgGroup) { for field in group.fields { show_field(&config, group.name, field); } } fn show_field(config: &KV, grpname: &str, field: &CfgField) { if field.is_silent() { 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())); 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); } /* ***** basic validation ***** */ fn needs_info(config: &KV, group: &CfgGroup) -> Vec { let mut acc = Vec::new(); for field in group.fields { if field.is_action_required(config, group.name) { acc.push(field.key()); } } acc } /* ***** prompting (with actual I/O) ***** */ /// 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 default_choice = choices[0].to_uppercase().chars().nth(1).unwrap_or_default(); loop { println!("{}", prompt_message); print!("Select: "); io::stdout().flush().ok(); // Read line and take first non-space character let mut input = String::new(); io::stdin().read_line(&mut input).unwrap(); let input = input.trim().to_uppercase().chars().next().unwrap_or(default_choice); // Check list of choices for match if choices .iter() .any(|&choice| choice.to_uppercase().chars().nth(1).unwrap_or_default() == input) { return input; } else { println!("Invalid choice. Try again!"); } } } /// Read a single line of text. fn prompt_single_line() -> String { print!("New value: "); io::stdout().flush().ok(); let mut input = String::new(); io::stdin().read_line(&mut input).unwrap(); input.trim().to_string() } /// Read multiple lines of text, terminated by a line containing just a '.'. fn prompt_multiline() -> String { println!("Enter new value: (end with '.' on a new line)"); let mut acc = String::new(); loop { let mut line = String::new(); io::stdin().read_line(&mut line).unwrap(); if line.trim() == "." { break; } acc.push_str(&line); } acc } /// Read a password without echoing it. fn prompt_password() -> String { let pass = rpassword::prompt_password("New password (not shown) : ").unwrap(); // disabled echo means the newline also isn't shown println!(""); pass }