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)
|
//! // open a config file somewhere (we use a dummy to not cause side-effects)
|
||||||
//! let config = KeyValueStore::new_dummy()?;
|
//! let config = KeyValueStore::new_dummy()?;
|
||||||
//! assert_eq!( config.get("modname-user").unwrap(), "xyzzy" );
|
|
||||||
//! // always:
|
//! // always:
|
||||||
//! CONFIG_SPEC.populate_defaults(&config);
|
//! CONFIG_SPEC.populate_defaults(&config);
|
||||||
|
//! // (and then defaults will be set:)
|
||||||
|
//! assert_eq!( config.get("modname-user").unwrap(), "xyzzy" );
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
//! ```ignore
|
//! ```ignore
|
||||||
|
@ -57,11 +58,27 @@ const EMPTY_VALUE: &str = "(empty)";
|
||||||
/// Text shown in place of a password.
|
/// Text shown in place of a password.
|
||||||
const HIDDEN_PASSWORD: &str = "*****";
|
const HIDDEN_PASSWORD: &str = "*****";
|
||||||
|
|
||||||
// highlight some of the info
|
/// semantic highlighting (in contrast to the pure formatting-based stuff you get from colored)
|
||||||
const ANSI_GROUP: &str = "\x1b[1;31m";
|
#[rustfmt::skip]
|
||||||
const ANSI_FIELD: &str = "\x1b[1;33m";
|
mod style {
|
||||||
const ANSI_NOTICE: &str = "\x1b[33m";
|
use colored::{ColoredString, Colorize};
|
||||||
const ANSI_RESET: &str = "\x1b[0m";
|
/// 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 ***** */
|
/* ***** config structure definition ***** */
|
||||||
|
|
||||||
|
@ -69,7 +86,7 @@ const ANSI_RESET: &str = "\x1b[0m";
|
||||||
pub enum CfgField<'a> {
|
pub enum CfgField<'a> {
|
||||||
/// Silently writes a default to the DB if absent, never prompts for changes.
|
/// Silently writes a default to the DB if absent, never prompts for changes.
|
||||||
Silent { key: &'a str, default: &'a str },
|
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 },
|
Default { key: &'a str, default: &'a str, description: &'a str },
|
||||||
/// No default, asks for value, but can stay empty.
|
/// No default, asks for value, but can stay empty.
|
||||||
Optional { key: &'a str, description: &'a str },
|
Optional { key: &'a str, description: &'a str },
|
||||||
|
@ -110,6 +127,10 @@ impl<'a> CfgField<'a> {
|
||||||
matches!(self, CfgField::Password { .. })
|
matches!(self, CfgField::Password { .. })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_generator(&self) -> bool {
|
||||||
|
matches!(self, CfgField::Generated { .. })
|
||||||
|
}
|
||||||
|
|
||||||
/// Optional fields are allowed to be (and stay) empty.
|
/// Optional fields are allowed to be (and stay) empty.
|
||||||
fn is_optional(&self) -> bool {
|
fn is_optional(&self) -> bool {
|
||||||
matches!(self, CfgField::Optional { .. })
|
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
|
// TODO: add distinction between edit all / edit necessary only
|
||||||
let choices = if !todo.is_empty() {
|
let choices = if !todo.is_empty() {
|
||||||
println!(
|
println!(
|
||||||
"{}The following fields need adjustment: {}{}",
|
"{}",
|
||||||
ANSI_NOTICE,
|
style::notice(&format!(
|
||||||
todo.join(", "),
|
"The following fields need adjustment: {}",
|
||||||
ANSI_RESET
|
todo.join(", ")
|
||||||
|
))
|
||||||
);
|
);
|
||||||
&["[E]dit (default)", "[L]ist all", "[S]kip group"]
|
&["[F]ixes only", "[E]dit all", "[L]ist all", "[S]kip group"][..]
|
||||||
} else {
|
} else {
|
||||||
println!(
|
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 {
|
loop {
|
||||||
let choice = prompt_action(choices);
|
let choice = prompt_action(choices);
|
||||||
|
@ -237,6 +261,13 @@ pub fn interactive_check(spec: &CfgSpec, config: KV) -> Result<(), Box<dyn Error
|
||||||
}
|
}
|
||||||
break;
|
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' => {
|
'S' => {
|
||||||
if !todo.is_empty() {
|
if !todo.is_empty() {
|
||||||
all_valid = false;
|
all_valid = false;
|
||||||
|
@ -244,8 +275,8 @@ pub fn interactive_check(spec: &CfgSpec, config: KV) -> Result<(), Box<dyn Error
|
||||||
break;
|
break;
|
||||||
},
|
},
|
||||||
_ => {
|
_ => {
|
||||||
unreachable!();
|
unreachable!(); // (prompt already checks)
|
||||||
}, // (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(
|
fn check_field(
|
||||||
config: &KV, grpname: &str, field: &CfgField, ok: &mut bool,
|
config: &KV, grpname: &str, field: &CfgField, ok: &mut bool,
|
||||||
) -> Result<(), Box<dyn Error>> {
|
) -> Result<(), Box<dyn Error>> {
|
||||||
|
@ -267,20 +299,23 @@ fn check_field(
|
||||||
let value = config.get(&key).ok();
|
let value = config.get(&key).ok();
|
||||||
let mut actions: Vec<&'static str> = Vec::new();
|
let mut actions: Vec<&'static str> = Vec::new();
|
||||||
// TODO: adjust order: empty password should offer input first
|
// 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() {
|
if value.is_some() | field.is_password() | field.is_optional() {
|
||||||
actions.push("[K]eep as-is");
|
actions.push("[K]eep as-is");
|
||||||
}
|
}
|
||||||
if field.default_description().is_some() {
|
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() {
|
if field.is_password() {
|
||||||
actions.push("[R]emove");
|
actions.push("[R]emove");
|
||||||
}
|
}
|
||||||
actions.push("[I]nput new value");
|
actions.push("[I]nput new value");
|
||||||
if !field.is_password() {
|
if !field.is_password() {
|
||||||
actions.push("[L]ong (multiline) input")
|
actions.push("[L]ong (multiline) input");
|
||||||
};
|
}
|
||||||
|
|
||||||
match prompt_action(&actions) {
|
match prompt_action(&actions) {
|
||||||
'K' => {
|
'K' => {
|
||||||
|
@ -310,7 +345,7 @@ fn check_field(
|
||||||
config.set(&key, &value)?;
|
config.set(&key, &value)?;
|
||||||
},
|
},
|
||||||
_ => {
|
_ => {
|
||||||
return Err("Wat.".into());
|
unreachable!(); // (prompt already checks)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -319,8 +354,9 @@ fn check_field(
|
||||||
/* ***** displaying various kinds of info ***** */
|
/* ***** displaying various kinds of info ***** */
|
||||||
|
|
||||||
fn group_header(name: &str, description: &str) {
|
fn group_header(name: &str, description: &str) {
|
||||||
println!("==============================");
|
println!("{}", style::group(&("=".repeat(name.len() + description.len() + 3))));
|
||||||
println!("{}{}{} - {}", ANSI_GROUP, name, ANSI_RESET, description);
|
println!("{} - {}", style::group(name), style::field(description));
|
||||||
|
println!("{}", style::group(&("=".repeat(name.len() + description.len() + 3))));
|
||||||
println!("");
|
println!("");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -335,14 +371,20 @@ fn show_field(config: &KV, grpname: &str, field: &CfgField) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let key = field.full_key(grpname);
|
let key = field.full_key(grpname);
|
||||||
println!("{}{}{} - {}", ANSI_FIELD, &key, ANSI_RESET, field.description());
|
println!("{} - {}", style::field(&key), style::info(&field.description()));
|
||||||
println!(" default: {}", field.default_description().unwrap_or(EMPTY_VALUE.to_string()));
|
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
|
let value = config
|
||||||
.get(&key)
|
.get(&key)
|
||||||
.ok()
|
.ok()
|
||||||
.map(|s| if field.is_password() { HIDDEN_PASSWORD.to_string() } else { s })
|
.map(|s| if field.is_password() { HIDDEN_PASSWORD.to_string() } else { s })
|
||||||
.unwrap_or(EMPTY_VALUE.to_string());
|
.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 ***** */
|
/* ***** 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,
|
/// Ask for a choice between several options. First option is the default,
|
||||||
/// format options with the first character in brackets like so: `"[E]xample"`.
|
/// format options with the first character in brackets like so: `"[E]xample"`.
|
||||||
fn prompt_action(choices: &[&str]) -> char {
|
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();
|
let default_choice = choices[0].to_uppercase().chars().nth(1).unwrap_or_default();
|
||||||
loop {
|
loop {
|
||||||
println!("{}", prompt_message);
|
println!("{}", prompt_message);
|
||||||
|
|
Loading…
Reference in a new issue