pub use self::error::CliError;
pub use self::help::CliHelpScreen;
pub use self::macros::*;
pub use self::request::{CliFormat, CliRequest};
pub use self::router::CliRouter;
pub use anyhow;
pub use indexmap::{IndexMap, indexmap};
use rpassword::read_password;
use std::fmt::Display;
use std::hash::Hash;
use std::process::{Command, exit};
use std::str::FromStr;
use std::{env, fs};
use zxcvbn::zxcvbn;
pub mod error;
mod help;
pub mod macros;
mod request;
mod router;
pub trait CliCommand {
fn process(&self, req: &CliRequest) -> anyhow::Result<()>;
fn help(&self) -> CliHelpScreen;
}
pub fn cli_run(router: &mut CliRouter) {
let (req, cmd) = match router.lookup() {
Some(r) => r,
None => {
CliHelpScreen::render_index(&router);
exit(0);
}
};
if req.is_help {
CliHelpScreen::render(&cmd, &req.cmd_alias, &req.shortcuts);
} else if let Err(e) = cmd.process(&req) {
cli_send!("ERROR: {}\n", e);
}
}
pub fn cli_header(text: &str) {
println!("------------------------------");
println!("-- {}", text);
println!("------------------------------\n");
}
pub fn cli_get_option<K, V>(question: &str, options: &IndexMap<K, V>) -> K
where
K: Display + Eq + PartialEq + Hash + FromStr,
<K as FromStr>::Err: Display,
V: Display,
{
let message = format!("{}\n\n", question);
cli_send!(&message);
for (key, value) in options.iter() {
cli_send!(&format!(" [{}] {}\n", key, value));
}
cli_send!("\nSelect One: ");
let mut input: String;
loop {
input = String::new();
io::stdin().read_line(&mut input).expect("Failed to read line");
let input = input.trim();
if let Ok(value) = input.parse::<K>() {
if options.contains_key(&value) {
return value;
}
}
print!("\r\nInvalid option, try again: ");
io::stdout().flush().unwrap();
}
}
pub fn cli_get_input(message: &str, default_value: &str) -> String {
cli_send!(message);
io::stdout().flush().unwrap();
let mut input = String::new();
io::stdin().read_line(&mut input).expect("Failed to read line");
let mut input = input.trim();
if input.trim().is_empty() {
input = default_value;
}
input.to_string()
}
pub fn cli_get_multiline_input(message: &str) -> String {
cli_send!(&format!("{} (empty line to stop)\n\n", message));
io::stdout().flush().unwrap();
let mut res: Vec<String> = Vec::new();
loop {
let mut input = String::new();
io::stdin().read_line(&mut input).expect("Failed to read line");
let input = input.trim();
if input.is_empty() {
break;
}
res.push(input.to_string());
}
res.join("\n").to_string()
}
pub fn cli_confirm(message: &str) -> bool {
let confirm_message = format!("{} (y/n): ", message);
cli_send!(&confirm_message);
let mut _input = "".to_string();
loop {
_input = String::new();
io::stdin().read_line(&mut _input).expect("Failed to read line");
let _input = _input.trim().to_lowercase();
if _input != "y" && _input != "n" {
cli_send!("Invalid option, please try again. Enter (y/n): ");
} else {
break;
}
}
let res_char = _input.chars().next().unwrap();
res_char == 'y'
}
#[cfg(not(feature="mock"))]
pub fn cli_get_password(message: &str, allow_blank: bool) -> String {
let password_message = if message.is_empty() {
"Password: "
} else {
message
};
let mut _password = String::new();
loop {
cli_send!(password_message);
_password = read_password().unwrap();
if _password.is_empty() && !allow_blank {
cli_send!("You did not specify a password");
} else {
break;
}
}
_password
}
#[cfg(feature="mock")]
pub fn cli_get_password(message: &str, allow_blank: bool) -> String {
cli_get_input(message, if allow_blank { "" } else { "password" })
}
#[cfg(not(feature = "mock"))]
pub fn cli_get_new_password(req_strength: u8) -> String {
let mut _password = String::new();
let mut _confirm_password = String::new();
loop {
cli_send!("Desired Password: ");
_password = read_password().unwrap();
if _password.is_empty() {
cli_send!("You did not specify a password");
continue;
}
let strength = zxcvbn(&_password, &[]).unwrap();
if strength.score() < req_strength {
cli_send!("Password is not strong enough. Please try again.\n\n");
continue;
}
cli_send!("Confirm Password: ");
_confirm_password = read_password().unwrap();
if _password != _confirm_password {
cli_send!("Passwords do not match, please try again.\n\n");
continue;
}
break;
}
_password
}
#[cfg(feature = "mock")]
pub fn cli_get_new_password(req_strength: u8) -> String {
let mut _password = String::new();
let mut _confirm_password = String::new();
loop {
_password = cli_get_input("Desired Password: ", "");
if _password.is_empty() {
cli_send!("You did not specify a password");
continue;
}
let strength = zxcvbn(&_password, &[]).unwrap();
if strength.score() < req_strength {
cli_send!("Password is not strong enough. Please try again.\n\n");
continue;
}
_confirm_password = cli_get_input("Confirm Password: ", "");
if _password != _confirm_password {
cli_send!("Passwords do not match, please try again.\n\n");
continue;
}
break;
}
_password
}
pub fn cli_display_table<C: Display, R: Display>(columns: &[C], rows: &[Vec<R>]) {
if rows.is_empty() {
println!("No rows to display.\n");
return;
}
let mut sizes: Vec<usize> = vec![0; columns.len()];
for (i, col) in columns.iter().enumerate() {
let col_str = col.to_string();
sizes[i] = col_str.len();
}
for row in rows {
for (i, val) in row.iter().enumerate() {
if i < sizes.len() {
let val_str = val.to_string();
let val_len = val_str.len();
if val_len > sizes[i] {
sizes[i] = val_len;
}
}
}
}
for size in sizes.iter_mut() {
*size += 3;
}
let mut header = String::from("+");
let mut col_header = String::from("|");
for (i, col) in columns.iter().enumerate() {
let col_str = col.to_string();
let padded_col = format!("{}{}", col_str, " ".repeat(sizes[i] - col_str.len()));
header += &("-".repeat(sizes[i] + 1) + "+");
col_header += &format!(" {}|", padded_col);
}
println!("{}\n{}\n{}", header, col_header, header);
for row in rows {
let mut line = String::from("|");
for (i, val) in row.iter().enumerate() {
if i < sizes.len() {
let val_str = val.to_string();
let padded_val = format!(" {}{}", val_str, " ".repeat(sizes[i] - val_str.len()));
line += &format!("{}|", padded_val);
}
}
println!("{}", line);
}
println!("{}\n", header);
}
pub fn cli_display_array<K: Display, V: Display>(rows: &IndexMap<K, V>) {
let mut size = 0;
for key in rows.keys() {
let key_str = key.to_string();
if key_str.len() + 8 > size {
size = key_str.len() + 8;
}
}
let indent = " ".repeat(size);
let indent_size = size - 4;
for (key, value) in rows {
let key_str = key.to_string();
let value_str = value.to_string();
let left_col = format!(" {}{}", key_str, " ".repeat(indent_size - key_str.len()));
let options =
textwrap::Options::new(75).initial_indent(&left_col).subsequent_indent(&indent);
let line = textwrap::fill(&value_str, &options);
println!("{}", line);
}
println!("");
}
pub fn cli_clear_screen() {
print!("\x1B[2J");
}
pub fn cli_text_editor(contents: &str) -> Result<String, CliError> {
let temp_dir = env::temp_dir();
let temp_file = temp_dir.join(format!(
"cli_edit_{}.tmp",
std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_millis()
));
fs::write(&temp_file, contents)
.map_err(|e| CliError::Generic(format!("Failed to create temp file: {}", e)))?;
let editor = get_editor();
let status = if cfg!(target_os = "windows") {
Command::new("cmd")
.args(&["/C", &format!("{} \"{}\"", editor, temp_file.display())])
.status()
} else {
Command::new(&editor).arg(&temp_file).status()
};
match status {
Ok(exit_status) if exit_status.success() => {
let result = fs::read_to_string(&temp_file).unwrap_or_else(|_| String::new());
let _ = fs::remove_file(&temp_file);
Ok(result)
}
Ok(_) => {
let _ = fs::remove_file(&temp_file);
Err(CliError::Generic("Editor exited with error".to_string()))
}
Err(e) => {
let _ = fs::remove_file(&temp_file);
Err(CliError::Generic(format!("Failed to launch editor: {}", e)))
}
}
}
fn get_editor() -> String {
if let Ok(editor) = env::var("VISUAL") {
return editor;
}
if let Ok(editor) = env::var("EDITOR") {
return editor;
}
if cfg!(target_os = "windows") {
if Command::new("notepad++").arg("--version").output().is_ok() {
"notepad++".to_string()
} else {
"notepad".to_string()
}
} else if cfg!(target_os = "macos") {
if Command::new("which").arg("nano").output().is_ok() {
"nano".to_string()
} else {
"vim".to_string()
}
} else {
for editor in &["nano", "vim", "vi"] {
if Command::new("which")
.arg(editor)
.output()
.map(|o| o.status.success())
.unwrap_or(false)
{
return editor.to_string();
}
}
"vi".to_string() }
}
pub fn cli_progress_bar(message: &str, total: usize) -> CliProgressBar {
let bar = CliProgressBar {
value: 0,
total,
message: message.to_string(),
};
bar.start();
bar
}
pub struct CliProgressBar {
pub value: usize,
pub total: usize,
pub message: String,
}
impl CliProgressBar {
pub fn start(&self) {
self.render();
}
pub fn increment(&mut self, num: usize) {
self.value = self.value.saturating_add(num).min(self.total);
self.render();
}
pub fn set(&mut self, value: usize) {
self.value = value.min(self.total);
self.render();
}
pub fn finish(&mut self) {
self.value = self.total;
self.render();
println!("");
}
fn render(&self) {
let percent = if self.total > 0 {
(self.value * 100) / self.total
} else {
0
};
let percent_str = format!("{}", percent);
let fixed_overhead = 8 + percent_str.len();
let available = 75_usize.saturating_sub(fixed_overhead);
let bar_size = 10;
let message_max = available.saturating_sub(bar_size);
let display_message = if self.message.len() > message_max {
format!("{}...", &self.message[..message_max.saturating_sub(3)])
} else {
self.message.clone()
};
let bar_width = available.saturating_sub(display_message.len()).max(8);
let filled = (bar_width * self.value) / self.total.max(1);
let empty = bar_width.saturating_sub(filled);
let bar = format!("{}{}", "*".repeat(filled), " ".repeat(empty));
print!("\r[ {}% ] {} [{}]", percent, display_message, bar);
io::stdout().flush().unwrap();
if self.value >= self.total {
println!();
}
}
}