config_spec: various improvements
- more color, using colored - better wording of options - highlight default choices - various other small fixes
This commit is contained in:
parent
29e6285e3c
commit
62d12e314c
|
@ -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<dyn Error
|
|||
// 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
|
||||
"{}",
|
||||
style::notice(&format!(
|
||||
"The following fields need adjustment: {}",
|
||||
todo.join(", ")
|
||||
))
|
||||
);
|
||||
&["[E]dit (default)", "[L]ist all", "[S]kip group"]
|
||||
&["[F]ixes only", "[E]dit all", "[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
|
||||
"{}",
|
||||
style::notice(
|
||||
"This group looks fine. OK to [S]kip, unless you want to adjust values here."
|
||||
)
|
||||
);
|
||||
&["[S]kip group (default)", "[E]dit", "[L]ist all"]
|
||||
&["[S]kip group", "[E]dit all", "[L]ist all"][..]
|
||||
};
|
||||
loop {
|
||||
let choice = prompt_action(choices);
|
||||
|
@ -237,6 +261,13 @@ pub fn interactive_check(spec: &CfgSpec, config: KV) -> Result<(), Box<dyn Error
|
|||
}
|
||||
break;
|
||||
},
|
||||
'F' => {
|
||||
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<dyn Error
|
|||
break;
|
||||
},
|
||||
_ => {
|
||||
unreachable!();
|
||||
}, // (prompt already checks)
|
||||
unreachable!(); // (prompt already checks)
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -256,6 +287,7 @@ pub fn interactive_check(spec: &CfgSpec, config: KV) -> Result<(), Box<dyn Error
|
|||
}
|
||||
}
|
||||
|
||||
/// Checks a single field and asks what to do with it.
|
||||
fn check_field(
|
||||
config: &KV, grpname: &str, field: &CfgField, ok: &mut bool,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
|
@ -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<String> {
|
|||
/// 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);
|
||||
|
|
Loading…
Reference in a new issue