use super::{CliCommand, CliHelpScreen, CliRequest};
use crate::*;
use std::collections::HashMap;
use std::env;
use strsim::levenshtein;
#[derive(Default)]
pub struct CliRouter {
pub app_name: String,
pub version_message: String,
pub handler_alias: Option<String>,
pub handlers: HashMap<String, CliHandler>,
pub commands: HashMap<String, Box<dyn CliCommand>>,
pub categories: HashMap<String, CliCategory>,
pub ignore_flags: HashMap<String, bool>,
pub global_flags: Vec<CliGlobalFlag>,
pub parsed_global_flags: bool,
pub children: HashMap<String, Box<CliRouter>>,
}
#[derive(Clone)]
pub struct CliHandler {
pub alias: String,
pub shortcuts: Vec<String>,
pub value_flags: Vec<String>,
}
#[derive(Clone)]
pub struct CliCategory {
pub alias: String,
pub title: String,
pub description: String,
}
#[derive(Clone, Default)]
pub struct CliGlobalFlag {
pub short: String,
pub long: String,
pub desc: String,
pub is_value: bool,
pub has: bool,
pub value: Option<String>,
}
impl CliRouter {
pub fn new() -> Self {
Self::default()
}
pub fn add<T>(&mut self, alias: &str, shortcuts: Vec<&str>, value_flags: Vec<&str>)
where
T: CliCommand + Default + 'static,
{
let handler = CliHandler {
alias: alias.to_lowercase(),
shortcuts: shortcuts.clone().into_iter().map(|s| s.to_string()).collect(),
value_flags: value_flags.clone().into_iter().map(|s| s.to_string()).collect(),
};
self.handlers.insert(alias.to_string(), handler.clone());
self.commands.insert(alias.to_lowercase(), Box::<T>::default());
let mut queue: Vec<String> = shortcuts.clone().into_iter().map(|s| s.to_string()).collect();
queue.insert(0, alias.to_string());
for cmd_alias in queue.iter() {
let mut child = &mut *self;
for segment in cmd_alias.split_whitespace() {
child =
child.children.entry(segment.to_string()).or_insert(Box::new(CliRouter::new()));
}
child.handler_alias = Some(handler.alias.to_string());
}
}
pub fn app_name(&mut self, name: &str) {
self.app_name = name.to_string();
}
pub fn version_message(&mut self, msg: &str) {
self.version_message = msg.to_string();
}
pub fn global(&mut self, short: &str, long: &str, is_value: bool, desc: &str) {
self.global_flags.push(CliGlobalFlag {
short: short.to_string(),
long: long.to_string(),
is_value,
desc: desc.to_string(),
..Default::default()
});
}
pub fn has_global(&mut self, flag: &str) -> bool {
if !self.parsed_global_flags {
self.get_raw_args();
}
let flag_chk = flag.to_string();
if let Some(index) =
self.global_flags.iter().position(|gf| gf.short == flag_chk || gf.long == flag_chk)
{
return self.global_flags[index].has;
}
false
}
pub fn get_global(&mut self, flag: &str) -> Option<String> {
if !self.parsed_global_flags {
self.get_raw_args();
}
let flag_chk = flag.to_string();
if let Some(index) =
self.global_flags.iter().position(|gf| gf.short == flag_chk || gf.long == flag_chk)
{
return self.global_flags[index].value.clone();
}
None
}
pub fn ignore(&mut self, flag: &str, is_value: bool) {
self.ignore_flags.insert(flag.to_string(), is_value);
}
pub fn lookup(&mut self) -> Option<(CliRequest, &Box<dyn CliCommand>)> {
let mut args = self.get_raw_args()?;
let is_help = self.is_help(&mut args);
let handler = self.lookup_handler(&mut args)?;
let (flags, flag_values) = self.gather_flags(&mut args, &handler);
let req = CliRequest {
cmd_alias: handler.alias.to_string(),
is_help,
args,
flags,
flag_values,
shortcuts: handler.shortcuts.to_vec(),
};
let cmd = self.commands.get(&handler.alias).unwrap();
Some((req, cmd))
}
fn get_raw_args(&mut self) -> Option<Vec<String>> {
let mut cmd_args = vec![];
let mut skip_next = true;
let mut global_value_index: Option<usize> = None;
self.parsed_global_flags = true;
for value in env::args() {
if skip_next {
skip_next = false;
if let Some(index) = global_value_index {
self.global_flags[index].value = Some(value.to_string());
global_value_index = None;
}
continue;
}
if ["-v", "--version"].contains(&value.as_str()) && !self.version_message.is_empty() {
println!("{}", self.version_message);
std::process::exit(0);
} else if let Some(is_value) = self.ignore_flags.get(&value) {
skip_next = *is_value;
} else if let Some(index) = self
.global_flags
.iter()
.position(|gf| [gf.short.to_string(), gf.long.to_string()].contains(&value))
{
skip_next = self.global_flags[index].is_value;
if skip_next {
global_value_index = Some(index);
}
} else {
cmd_args.push(value.to_string());
}
}
if !cmd_args.is_empty() {
Some(cmd_args)
} else {
None
}
}
fn is_help(&self, args: &mut Vec<String>) -> bool {
let mut is_help = false;
if ["help", "-h"].contains(&args[0].as_str()) {
is_help = true;
args.remove(0);
if args.is_empty() {
CliHelpScreen::render_index(self);
}
let cat_alias = args.join(" ").to_string();
if self.categories.contains_key(&cat_alias) {
CliHelpScreen::render_category(&self, &cat_alias);
}
}
is_help
}
fn lookup_handler(&self, args: &mut Vec<String>) -> Option<CliHandler> {
let mut h_alias: Option<String> = None;
let (mut start, mut length) = (0, 0);
let mut child = self;
for (pos, segment) in args.iter().enumerate() {
if segment.starts_with("-") {
continue;
}
if let Some(next) = child.children.get(&segment.to_lowercase()) {
if length == 0 {
(start, length) = (pos, 1);
} else {
length += 1;
}
if let Some(h_child) = &next.handler_alias {
h_alias = Some(h_child.clone());
}
child = next;
} else if h_alias.is_some() {
break;
} else {
child = self;
length = 0;
}
}
if h_alias.is_none() {
h_alias = self.lookup_similar(args);
} else if h_alias.is_some() {
args.drain(start..start + length);
} else {
return None;
}
let handler = self.handlers.get(&h_alias?)?;
Some(handler.clone())
}
fn gather_flags(
&self,
args: &mut Vec<String>,
handler: &CliHandler,
) -> (Vec<String>, HashMap<String, String>) {
let mut incl_value = false;
let mut flags = vec![];
let mut flag_values: HashMap<String, String> = HashMap::new();
let mut final_args = vec![];
for (pos, value) in args.iter().enumerate() {
if incl_value {
flag_values.insert(args[pos - 1].to_string(), value.to_string());
incl_value = false;
} else if value.starts_with("-") && handler.value_flags.contains(&value) {
incl_value = true;
} else if value.starts_with("--") {
flags.push(value.to_string());
} else if value.starts_with("-") {
for char in value[1..].chars() {
flags.push(format!("-{}", char));
}
} else {
final_args.push(value.to_string());
}
}
*args = final_args;
(flags, flag_values)
}
fn lookup_similar(&self, args: &mut Vec<String>) -> Option<String> {
let start = args.iter().position(|a| !a.starts_with("-")).unwrap_or(0);
let search_args =
args.clone().into_iter().filter(|a| !a.starts_with("-")).collect::<Vec<String>>();
let mut commands: Vec<String> = self.commands.keys().map(|c| c.to_string()).collect();
commands.sort_by(|a, b| {
let a_count = a.chars().filter(|c| c.is_whitespace()).count();
let b_count = b.chars().filter(|c| c.is_whitespace()).count();
b_count.cmp(&a_count)
});
let (mut distance, mut bin_length, mut found_cmd) = (0, 0, String::new());
for chk_alias in commands {
let length = chk_alias.chars().filter(|c| c.is_whitespace()).count() + 1;
if bin_length != length && bin_length > 0 && distance > 0 && distance < 4 {
let confirm_msg = format!(
"No command with that name exists, but a similar command with the name '{}' does exist. Is this the command you wish to run?",
found_cmd
);
if cli_confirm(&confirm_msg) {
let end = (start + length).min(args.len());
args.drain(start..end);
return Some(found_cmd);
} else {
return None;
}
} else if bin_length != length {
bin_length = length;
distance = 0;
found_cmd = String::new();
}
let end = search_args.len().min(length);
let search_str = search_args[..end].join(" ").to_string();
let chk_distance = levenshtein(&chk_alias, &search_str);
if chk_distance < distance || distance == 0 {
distance = chk_distance;
found_cmd = chk_alias.to_string();
}
}
None
}
pub fn add_category(&mut self, alias: &str, title: &str, description: &str) {
self.categories.insert(
alias.to_lowercase(),
CliCategory {
alias: alias.to_lowercase(),
title: title.to_string(),
description: description.to_string(),
},
);
}
}