2024-07-29 15:12:26 +02:00
|
|
|
use crate::key_value::KeyValueStore as KV;
|
|
|
|
use std::error::Error;
|
|
|
|
use std::io::{self, Write};
|
|
|
|
|
2024-07-30 03:57:24 +02:00
|
|
|
/// Text shown for an empty field.
|
2024-08-02 22:29:22 +02:00
|
|
|
const EMPTY_VALUE: &str = "(empty)";
|
2024-07-30 03:57:24 +02:00
|
|
|
/// Text shown in place of a password.
|
2024-08-02 22:29:22 +02:00
|
|
|
const HIDDEN_PASSWORD: &str = "*****";
|
2024-07-30 03:57:24 +02:00
|
|
|
|
|
|
|
// highlight some of the info
|
|
|
|
const ANSI_GROUP: &str = "\x1b[1;31m";
|
|
|
|
const ANSI_FIELD: &str = "\x1b[1;33m";
|
2024-08-02 22:29:22 +02:00
|
|
|
const ANSI_NOTICE: &str = "\x1b[33m";
|
2024-07-30 03:57:24 +02:00
|
|
|
const ANSI_RESET: &str = "\x1b[0m";
|
|
|
|
|
2024-07-29 15:12:26 +02:00
|
|
|
/// Ensure all fields with known default values exist.
|
2024-08-05 04:45:34 +02:00
|
|
|
pub fn populate_defaults(spec: &CfgSpec, config: &KV) {
|
|
|
|
for group in spec.groups {
|
2024-07-29 15:12:26 +02:00
|
|
|
for field in group.fields {
|
|
|
|
match field {
|
2024-07-30 03:57:24 +02:00
|
|
|
CfgField::Silent { default, .. } => {
|
|
|
|
config.default(&field.full_key(group.name), default)
|
2024-07-29 15:12:26 +02:00
|
|
|
},
|
2024-07-30 03:57:24 +02:00
|
|
|
CfgField::Default { default, .. } => {
|
|
|
|
config.default(&field.full_key(group.name), default)
|
2024-07-29 15:12:26 +02:00
|
|
|
},
|
2024-08-02 22:29:22 +02:00
|
|
|
_ => {},
|
2024-07-29 15:12:26 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-07-30 03:57:24 +02:00
|
|
|
/// Check all groups / fields and interactively edit as needed.
|
|
|
|
///
|
|
|
|
/// Will report if the config is fine or has missing values.
|
2024-08-05 04:45:34 +02:00
|
|
|
pub fn interactive_check(spec: &CfgSpec, config: KV) -> Result<(), Box<dyn Error>> {
|
2024-08-02 22:29:22 +02:00
|
|
|
let mut all_valid: bool = true;
|
2024-08-05 04:45:34 +02:00
|
|
|
for group in spec.groups {
|
2024-08-02 22:29:22 +02:00
|
|
|
group_header(group.name, group.description);
|
2024-07-30 03:57:24 +02:00
|
|
|
let todo = needs_info(&config, group);
|
2024-07-30 13:23:51 +02:00
|
|
|
// TODO: add distinction between edit all / edit necessary only
|
2024-07-30 03:57:24 +02:00
|
|
|
let choices = if !todo.is_empty() {
|
2024-08-02 22:29:22 +02:00
|
|
|
println!(
|
|
|
|
"{}The following fields need adjustment: {}{}",
|
|
|
|
ANSI_NOTICE,
|
|
|
|
todo.join(", "),
|
|
|
|
ANSI_RESET
|
|
|
|
);
|
2024-07-30 03:57:24 +02:00
|
|
|
&["[E]dit (default)", "[L]ist all", "[S]kip group"]
|
|
|
|
} else {
|
2024-08-02 22:29:22 +02:00
|
|
|
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"]
|
2024-07-30 03:57:24 +02:00
|
|
|
};
|
|
|
|
loop {
|
2024-08-02 22:29:22 +02:00
|
|
|
let choice = prompt_action(choices);
|
2024-07-30 03:57:24 +02:00
|
|
|
match choice {
|
2024-08-02 22:29:22 +02:00
|
|
|
'L' => {
|
|
|
|
show_group(&config, group);
|
|
|
|
},
|
2024-07-30 03:57:24 +02:00
|
|
|
'E' => {
|
|
|
|
for field in group.fields {
|
2024-08-02 22:29:22 +02:00
|
|
|
check_field(&config, group.name, field, &mut all_valid)?;
|
2024-07-30 03:57:24 +02:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
},
|
|
|
|
'S' => {
|
2024-08-02 22:29:22 +02:00
|
|
|
if !todo.is_empty() {
|
|
|
|
all_valid = false;
|
|
|
|
}
|
2024-07-30 03:57:24 +02:00
|
|
|
break;
|
|
|
|
},
|
2024-08-02 22:29:22 +02:00
|
|
|
_ => {
|
|
|
|
unreachable!();
|
|
|
|
}, // (prompt already checks)
|
2024-07-30 03:57:24 +02:00
|
|
|
}
|
2024-07-29 15:12:26 +02:00
|
|
|
}
|
|
|
|
}
|
2024-07-30 03:57:24 +02:00
|
|
|
if all_valid {
|
|
|
|
Ok(())
|
|
|
|
} else {
|
|
|
|
Err("WARNING: Config is incomplete. Re-run to fix.".into())
|
|
|
|
}
|
2024-07-29 15:12:26 +02:00
|
|
|
}
|
|
|
|
|
2024-08-02 22:29:22 +02:00
|
|
|
fn check_field(
|
|
|
|
config: &KV, grpname: &str, field: &CfgField, ok: &mut bool,
|
|
|
|
) -> Result<(), Box<dyn Error>> {
|
|
|
|
if field.is_silent() {
|
|
|
|
return Ok(());
|
|
|
|
}
|
2024-07-30 03:57:24 +02:00
|
|
|
show_field(config, grpname, field);
|
2024-07-29 15:12:26 +02:00
|
|
|
let key = field.full_key(grpname);
|
|
|
|
let value = config.get(&key).ok();
|
2024-07-30 03:57:24 +02:00
|
|
|
let mut actions: Vec<&'static str> = Vec::new();
|
2024-07-30 13:23:51 +02:00
|
|
|
// TODO: adjust order: empty password should offer input first
|
|
|
|
// TODO: RandomId should offer generating as [R]egenerate
|
2024-08-02 22:29:22 +02:00
|
|
|
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")
|
|
|
|
};
|
2024-07-30 13:23:51 +02:00
|
|
|
|
|
|
|
match prompt_action(&actions) {
|
|
|
|
'K' => {
|
|
|
|
// we allow leaving a password empty, but that means config is incomplete
|
2024-08-02 22:29:22 +02:00
|
|
|
if value.is_none() & !field.is_optional() {
|
|
|
|
*ok &= false;
|
|
|
|
}
|
2024-07-30 13:23:51 +02:00
|
|
|
},
|
|
|
|
'R' => {
|
|
|
|
match field.default_value()? {
|
2024-08-02 22:29:22 +02:00
|
|
|
Some(value) => {
|
|
|
|
config.set(&key, &value)?;
|
|
|
|
},
|
2024-07-30 13:23:51 +02:00
|
|
|
// password again
|
2024-08-02 22:29:22 +02:00
|
|
|
None => {
|
|
|
|
config.delete(&key)?;
|
|
|
|
*ok &= false;
|
|
|
|
},
|
2024-07-30 13:23:51 +02:00
|
|
|
}
|
|
|
|
},
|
|
|
|
'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)?;
|
|
|
|
},
|
2024-08-02 22:29:22 +02:00
|
|
|
_ => {
|
|
|
|
return Err("Wat.".into());
|
|
|
|
},
|
2024-07-29 15:12:26 +02:00
|
|
|
}
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2024-07-30 03:57:24 +02:00
|
|
|
/* ***** displaying various kinds of info ***** */
|
|
|
|
|
2024-08-02 22:29:22 +02:00
|
|
|
fn group_header(name: &str, description: &str) {
|
2024-07-29 15:12:26 +02:00
|
|
|
println!("==============================");
|
|
|
|
println!("{}{}{} - {}", ANSI_GROUP, name, ANSI_RESET, description);
|
|
|
|
println!("");
|
|
|
|
}
|
|
|
|
|
2024-08-02 22:29:22 +02:00
|
|
|
fn show_group(config: &KV, group: &CfgGroup) {
|
2024-07-30 03:57:24 +02:00
|
|
|
for field in group.fields {
|
2024-08-02 22:29:22 +02:00
|
|
|
show_field(&config, group.name, field);
|
2024-07-30 03:57:24 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-08-02 22:29:22 +02:00
|
|
|
fn show_field(config: &KV, grpname: &str, field: &CfgField) {
|
|
|
|
if field.is_silent() {
|
|
|
|
return;
|
|
|
|
}
|
2024-07-30 03:57:24 +02:00
|
|
|
let key = field.full_key(grpname);
|
2024-08-02 22:29:22 +02:00
|
|
|
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()
|
2024-07-30 03:57:24 +02:00
|
|
|
.map(|s| if field.is_password() { HIDDEN_PASSWORD.to_string() } else { s })
|
|
|
|
.unwrap_or(EMPTY_VALUE.to_string());
|
2024-08-02 22:29:22 +02:00
|
|
|
println!(" current: {}", value);
|
2024-07-30 03:57:24 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/* ***** basic validation ***** */
|
|
|
|
|
2024-08-02 22:29:22 +02:00
|
|
|
fn needs_info(config: &KV, group: &CfgGroup) -> Vec<String> {
|
2024-07-30 03:57:24 +02:00
|
|
|
let mut acc = Vec::new();
|
|
|
|
for field in group.fields {
|
|
|
|
if field.is_action_required(config, group.name) {
|
2024-08-02 22:29:22 +02:00
|
|
|
acc.push(field.key());
|
2024-07-30 03:57:24 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
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"`.
|
2024-08-02 22:29:22 +02:00
|
|
|
fn prompt_action(choices: &[&str]) -> char {
|
2024-07-30 03:57:24 +02:00
|
|
|
let prompt_message = choices.join(", ");
|
|
|
|
let default_choice = choices[0].to_uppercase().chars().nth(1).unwrap_or_default();
|
|
|
|
loop {
|
2024-08-02 22:29:22 +02:00
|
|
|
println!("{}", prompt_message);
|
|
|
|
print!("Select: ");
|
2024-07-30 03:57:24 +02:00
|
|
|
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
|
2024-08-02 22:29:22 +02:00
|
|
|
if choices
|
|
|
|
.iter()
|
|
|
|
.any(|&choice| choice.to_uppercase().chars().nth(1).unwrap_or_default() == input)
|
|
|
|
{
|
2024-07-30 03:57:24 +02:00
|
|
|
return input;
|
|
|
|
} else {
|
|
|
|
println!("Invalid choice. Try again!");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Read a single line of text.
|
2024-08-02 22:29:22 +02:00
|
|
|
fn prompt_single_line() -> String {
|
2024-07-29 15:12:26 +02:00
|
|
|
print!("New value: ");
|
|
|
|
io::stdout().flush().ok();
|
|
|
|
let mut input = String::new();
|
|
|
|
io::stdin().read_line(&mut input).unwrap();
|
|
|
|
input.trim().to_string()
|
|
|
|
}
|
|
|
|
|
2024-07-30 03:57:24 +02:00
|
|
|
/// Read multiple lines of text, terminated by a line containing just a '.'.
|
2024-08-02 22:29:22 +02:00
|
|
|
fn prompt_multiline() -> String {
|
2024-07-29 15:12:26 +02:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2024-07-30 03:57:24 +02:00
|
|
|
/// Read a password without echoing it.
|
2024-08-02 22:29:22 +02:00
|
|
|
fn prompt_password() -> String {
|
2024-07-30 13:23:51 +02:00
|
|
|
let pass = rpassword::prompt_password("New password (not shown) : ").unwrap();
|
|
|
|
// disabled echo means the newline also isn't shown
|
|
|
|
println!("");
|
|
|
|
pass
|
2024-07-29 15:12:26 +02:00
|
|
|
}
|
|
|
|
|
2024-07-30 03:57:24 +02:00
|
|
|
/* ***** config structure definition ***** */
|
|
|
|
|
2024-07-29 15:12:26 +02:00
|
|
|
/// Describes a field in the configuration.
|
2024-08-05 04:45:34 +02:00
|
|
|
pub enum CfgField<'a> {
|
2024-07-29 15:12:26 +02:00
|
|
|
/// 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.
|
2024-08-02 22:29:22 +02:00
|
|
|
RandomId {
|
|
|
|
key: &'a str,
|
|
|
|
generator: fn() -> Result<String, Box<dyn Error>>,
|
|
|
|
generator_description: &'a str,
|
|
|
|
description: &'a str,
|
|
|
|
},
|
2024-07-29 15:12:26 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/// A group of related config fields. The final key of an inner value will be
|
|
|
|
/// `{group.name}-{key}`.
|
2024-08-05 04:45:34 +02:00
|
|
|
pub struct CfgGroup<'a> {
|
|
|
|
pub name: &'a str,
|
|
|
|
pub description: &'a str,
|
|
|
|
pub fields: &'a [CfgField<'a>],
|
2024-07-29 15:12:26 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/// A description of the entire config, as a collection of named groups.
|
|
|
|
///
|
|
|
|
/// Used both to populate defaults and to interactively adjust the configuration.
|
2024-08-05 04:45:34 +02:00
|
|
|
pub struct CfgSpec<'a> {
|
|
|
|
pub groups: &'a [CfgGroup<'a>],
|
2024-07-29 15:12:26 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
impl<'a> CfgField<'a> {
|
2024-07-30 03:57:24 +02:00
|
|
|
/// Silent fields don't get prompted or shown.
|
2024-07-29 15:12:26 +02:00
|
|
|
fn is_silent(&self) -> bool {
|
2024-08-02 22:29:22 +02:00
|
|
|
matches!(self, CfgField::Silent { .. })
|
2024-07-29 15:12:26 +02:00
|
|
|
}
|
|
|
|
|
2024-07-30 03:57:24 +02:00
|
|
|
/// Password fields will be censored when displayed.
|
2024-07-29 15:12:26 +02:00
|
|
|
fn is_password(&self) -> bool {
|
2024-08-02 22:29:22 +02:00
|
|
|
matches!(self, CfgField::Password { .. })
|
2024-07-30 03:57:24 +02:00
|
|
|
}
|
|
|
|
|
2024-07-30 13:23:51 +02:00
|
|
|
/// Optional fields are allowed to be (and stay) empty.
|
|
|
|
fn is_optional(&self) -> bool {
|
2024-08-02 22:29:22 +02:00
|
|
|
matches!(self, CfgField::Optional { .. })
|
2024-07-30 13:23:51 +02:00
|
|
|
}
|
|
|
|
|
2024-07-30 03:57:24 +02:00
|
|
|
/// 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 {
|
2024-08-02 22:29:22 +02:00
|
|
|
if matches!(
|
|
|
|
self,
|
|
|
|
CfgField::Silent { .. } | CfgField::Default { .. } | CfgField::Optional { .. }
|
|
|
|
) {
|
2024-07-30 03:57:24 +02:00
|
|
|
false
|
|
|
|
} else {
|
|
|
|
config.get(self.full_key(grpname).as_str()).ok().is_none()
|
2024-07-29 15:12:26 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-07-30 03:57:24 +02:00
|
|
|
/// Gets `key`, which is the local part of the field name. (`.full_key` is used for the DB.)
|
2024-07-29 15:12:26 +02:00
|
|
|
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,
|
2024-08-02 22:29:22 +02:00
|
|
|
}
|
|
|
|
.to_string()
|
2024-07-29 15:12:26 +02:00
|
|
|
}
|
|
|
|
|
2024-07-30 03:57:24 +02:00
|
|
|
/// Full field name / key used in the DB (currently `{grpname}-{fieldname}`.)
|
2024-07-29 15:12:26 +02:00
|
|
|
fn full_key(&self, grpname: &str) -> String {
|
|
|
|
format!("{}-{}", grpname, self.key())
|
|
|
|
}
|
|
|
|
|
2024-07-30 03:57:24 +02:00
|
|
|
/// Gets `description` or "(none)".
|
2024-07-29 15:12:26 +02:00
|
|
|
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,
|
2024-08-02 22:29:22 +02:00
|
|
|
}
|
|
|
|
.to_string()
|
2024-07-29 15:12:26 +02:00
|
|
|
}
|
|
|
|
|
2024-07-30 03:57:24 +02:00
|
|
|
/// Gets a description of the default value if one exists.
|
|
|
|
/// (Either the value itself, or a description of the generator making a default value.)
|
2024-07-30 13:23:51 +02:00
|
|
|
fn default_description(&self) -> Option<String> {
|
2024-07-29 15:12:26 +02:00
|
|
|
match self {
|
|
|
|
CfgField::Silent { default, .. } => Some(default.to_string()),
|
|
|
|
CfgField::Default { default, .. } => Some(default.to_string()),
|
2024-08-02 22:29:22 +02:00
|
|
|
CfgField::RandomId { generator_description, .. } => {
|
|
|
|
Some(format!("{}{}{}", '(', generator_description, ')'))
|
|
|
|
},
|
2024-07-29 15:12:26 +02:00
|
|
|
_ => None,
|
|
|
|
}
|
|
|
|
}
|
2024-07-30 13:23:51 +02:00
|
|
|
|
|
|
|
/// Gets the actual default value, which may involve running a generator.
|
|
|
|
fn default_value(&self) -> Result<Option<String>, Box<dyn Error>> {
|
|
|
|
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),
|
|
|
|
}
|
|
|
|
}
|
2024-07-29 15:12:26 +02:00
|
|
|
}
|