From 45f5eeee57ab820d9c261edaaf3aa9b5a6bafc57 Mon Sep 17 00:00:00 2001 From: nobody Date: Tue, 30 Jul 2024 13:23:51 +0200 Subject: [PATCH] config check: mostly functional now --- src/config_check.rs | 73 ++++++++++++++++++++++++++++++++------------- 1 file changed, 52 insertions(+), 21 deletions(-) diff --git a/src/config_check.rs b/src/config_check.rs index 37eff64..0b74b22 100644 --- a/src/config_check.rs +++ b/src/config_check.rs @@ -80,6 +80,7 @@ pub fn interactive_check(config: KV) -> Result<(), Box> { for group in CONFIG_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"] @@ -118,24 +119,36 @@ fn check_field( config: &KV, grpname: &str, field: &CfgField, ok: &mut bool ) -> let key = field.full_key(grpname); let value = config.get(&key).ok(); let mut actions: Vec<&'static str> = Vec::new(); - if value.is_some() { actions.push("[K]eep as-is"); } - if field.is_password() { - match value { - Some(_) => { - println!(" current: {}", HIDDEN_PASSWORD); - }, - None => { - println!(" current: {}", EMPTY_VALUE); - actions.push("[K]eep as-is"); // we allow leaving the password empty if you don't want to set it - }, - }; - //todo!() - // TODO: [K]eep as-is (if exists), [R]emove, [I]nput - } else { - println!( " current: {}", value.unwrap_or(EMPTY_VALUE.to_string()) ); - // TODO: [K]eep as-is, [R]eset to default, [I]nput, [L]ong (multiline) input + // 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()? { + 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()); } } - // Action - Ok(()) } @@ -157,7 +170,7 @@ 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().unwrap_or(EMPTY_VALUE.to_string())); + 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()); @@ -228,7 +241,10 @@ fn prompt_multiline( ) -> String { /// Read a password without echoing it. fn prompt_password( ) -> String { - rpassword::prompt_password("New password (not shown) : ").unwrap() + let pass = rpassword::prompt_password("New password (not shown) : ").unwrap(); + // disabled echo means the newline also isn't shown + println!(""); + pass } /* ***** config structure definition ***** */ @@ -244,7 +260,7 @@ enum CfgField<'a> { /// 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() -> String, generator_description: &'a str, description: &'a str }, + RandomId { key: &'a str, generator: fn() -> Result>, generator_description: &'a str, description: &'a str }, } /// A group of related config fields. The final key of an inner value will be @@ -273,6 +289,11 @@ impl<'a> CfgField<'a> { 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 @@ -315,7 +336,7 @@ impl<'a> CfgField<'a> { /// 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(&self) -> Option { + fn default_description(&self) -> Option { match self { CfgField::Silent { default, .. } => Some(default.to_string()), CfgField::Default { default, .. } => Some(default.to_string()), @@ -323,4 +344,14 @@ impl<'a> CfgField<'a> { _ => None, } } + + /// Gets the actual default value, which may involve running a generator. + fn default_value(&self) -> Result, Box> { + match self { + CfgField::Silent { default, .. } => Ok(Some(default.to_string())), + CfgField::Default { default, .. } => Ok(Some(default.to_string())), + CfgField::RandomId { generator, .. } => generator().map(Some), + _ => Ok(None), + } + } }