use std::collections::HashMap;
use colored::Colorize;
use crate::display;
use crate::option_parser::{
CommandChain, CommandOptionsParser, CommandOptionsParserBuilder, InputArgsParser, Value,
ValueTypes,
};
use crate::error::{FliError, Result};
#[derive(Debug, Clone)]
pub struct FliCallbackData {
pub command: FliCommand,
pub option_parser: CommandOptionsParser,
pub arguments: Vec<String>,
pub arg_parser: InputArgsParser,
}
impl FliCallbackData {
pub fn new(
command: FliCommand,
option_parser: CommandOptionsParser,
arguments: Vec<String>,
arg_parser: InputArgsParser,
) -> Self {
Self {
command,
option_parser,
arguments,
arg_parser,
}
}
pub fn get_option_value(&self, name: &str) -> Option<&ValueTypes> {
if name.starts_with("-") {
return self.option_parser.get_option_expected_value_type(name);
}
let short = format!("-{}", name);
if let Some(val) = self.option_parser.get_option_expected_value_type(&short) {
return Some(val);
}
let long = format!("--{}", name);
if let Some(val) = self.option_parser.get_option_expected_value_type(&long) {
return Some(val);
}
self.option_parser.get_option_expected_value_type(name)
}
pub fn get_argument_at(&self, index: usize) -> Option<&String> {
self.arguments.get(index)
}
pub fn get_arguments(&self) -> &Vec<String> {
&self.arguments
}
pub fn get_command(&self) -> &FliCommand {
&self.command
}
pub fn get_arg_parser(&self) -> &InputArgsParser {
&self.arg_parser
}
}
#[derive(Debug, Clone)]
pub struct PreservedOption {
pub long_flag: String,
pub short_flag: String,
pub value_type: ValueTypes,
pub callback: fn(&FliCallbackData),
pub stop_main_callback: bool,
}
#[derive(Debug, Clone)]
pub struct FliCommand {
pub name: String,
pub description: String,
pub option_parser_builder: CommandOptionsParserBuilder,
pub sub_commands: HashMap<String, FliCommand>,
pub callback: Option<fn(&FliCallbackData)>,
pub preserved_options: Vec<PreservedOption>,
pub preserved_short_flags: HashMap<String, usize>, pub preserved_long_flags: HashMap<String, usize>, pub expected_positional_args: usize,
pub inheritable_options: Vec<usize>,
}
impl FliCommand {
pub fn new(name: &str, description: &str) -> Self {
let mut x = Self {
name: name.to_owned(),
description: description.to_owned(),
sub_commands: HashMap::new(),
callback: None,
option_parser_builder: CommandOptionsParserBuilder::new(),
preserved_options: Vec::new(),
preserved_short_flags: HashMap::new(),
preserved_long_flags: HashMap::new(),
expected_positional_args: 0,
inheritable_options: Vec::new(),
};
x.setup_help_flag();
x
}
pub fn with_parser(
name: &str,
description: &str,
parser_builder: CommandOptionsParserBuilder,
) -> Self {
let mut x = Self {
name: name.to_owned(),
description: description.to_owned(),
sub_commands: HashMap::new(),
callback: None,
option_parser_builder: parser_builder,
preserved_options: Vec::new(),
preserved_short_flags: HashMap::new(),
preserved_long_flags: HashMap::new(),
expected_positional_args: 0,
inheritable_options: Vec::new(),
};
x.setup_help_flag();
x
}
pub fn set_expected_positional_args(&mut self, count: usize) -> &mut Self {
self.expected_positional_args = count;
self
}
pub fn get_expected_positional_args(&self) -> usize {
self.expected_positional_args
}
pub fn setup_help_flag(&mut self) {
self.add_option_with_callback(
"help",
"Display help information",
"-h",
"--help",
ValueTypes::OptionalSingle(Some(Value::Bool(false))),
true,
|data| {
let cmd = data.get_command();
display::print_section(&format!("Command: {}", cmd.get_name()));
display::print_info(cmd.get_description());
display::print_section("Usage");
let usage_patterns = Self::build_usage_patterns(cmd);
for pattern in usage_patterns {
display::print_info(&format!(" {}", pattern));
}
Self::print_options_table(&data.option_parser);
Self::print_subcommands_table(cmd);
std::process::exit(0);
},
);
}
pub fn build_usage_patterns(cmd: &FliCommand) -> Vec<String> {
let name = cmd.get_name();
let mut patterns = Vec::new();
let mut basic = format!("{}", name);
if cmd.has_sub_commands() {
basic.push_str("[SUBCOMMANDS]");
}
let expected = cmd.get_expected_positional_args();
let args_pattern: String = if expected > 0 {
let prefix = basic.clone();
let repeated = std::iter::repeat(" [ARGUMENT]")
.take(expected)
.collect::<Vec<_>>()
.join("");
let repeated_pattern = format!("{}", repeated);
repeated_pattern
} else {
String::new()
};
basic.push_str(&args_pattern);
basic.push_str(" [OPTIONS]");
patterns.push(basic);
let with_separator = format!(
"[SUBCOMMANDS] [OPTIONS] {}",
if expected > 0 {
format!("-- {}", args_pattern)
} else {
String::new()
}
);
patterns.push(with_separator);
patterns
}
pub fn print_options_table(parser: &CommandOptionsParser) {
let options = parser.get_options();
if options.is_empty() {
return;
}
display::print_section("Options");
let headers = vec!["Flag", "Long Form", "Value Type", "Description"];
let rows: Vec<Vec<&str>> = options
.iter()
.map(|opt| {
let value_type = match &opt.value {
ValueTypes::OptionalSingle(Some(Value::Bool(_))) => "flag",
ValueTypes::RequiredSingle(_) => "single (required)",
ValueTypes::OptionalSingle(_) => "single (optional)",
ValueTypes::RequiredMultiple(_, Some(n)) => {
return vec![
opt.short_flag.as_str(),
opt.long_flag.as_str(),
Box::leak(format!("multiple (exactly {})", n).into_boxed_str()),
opt.description.as_str(),
];
}
ValueTypes::RequiredMultiple(_, None) => "multiple (1+)",
ValueTypes::OptionalMultiple(_, Some(n)) => {
return vec![
opt.short_flag.as_str(),
opt.long_flag.as_str(),
Box::leak(format!("multiple (max {})", n).into_boxed_str()),
opt.description.as_str(),
];
}
ValueTypes::OptionalMultiple(_, None) => "multiple (0+)",
};
vec![
opt.short_flag.as_str(),
opt.long_flag.as_str(),
value_type,
opt.description.as_str(),
]
})
.collect();
display::print_table(&headers, &rows, None);
}
pub fn print_subcommands_table(cmd: &FliCommand) {
if !cmd.has_sub_commands() {
return;
}
display::print_section("Subcommands");
let headers = vec!["Command", "Description"];
let rows: Vec<Vec<&str>> = cmd
.get_sub_commands()
.iter()
.map(|(name, sub_cmd)| vec![name.as_str(), sub_cmd.get_description().as_str()])
.collect();
display::print_table(&headers, &rows, None);
display::print_info("Run '<command> --help' for more information on a subcommand");
}
pub fn print_arguments_section(cmd: &FliCommand) {
display::print_section("Arguments");
let info = vec![
("Positional", "Arguments passed after all options"),
(
"After --",
"All arguments after '--' separator are treated as positional",
),
];
display::print_key_value(&info);
display::print_divider(60, '─', Some(colored::Color::BrightBlack));
display::print_info("Examples:");
display::print_info(&format!(" {} file1.txt file2.txt -v", cmd.get_name()));
display::print_info(&format!(" {} -v -- file1.txt file2.txt", cmd.get_name()));
}
pub fn set_callback(&mut self, callback: fn(&FliCallbackData)) {
self.callback = Some(callback);
}
pub fn get_name(&self) -> &String {
&self.name
}
pub fn get_description(&self) -> &String {
&self.description
}
pub fn get_option_parser_builder(&self) -> &CommandOptionsParserBuilder {
&self.option_parser_builder
}
pub fn get_option_parser(&mut self) -> &mut CommandOptionsParser {
self.option_parser_builder.build()
}
pub fn get_sub_commands(&self) -> &HashMap<String, FliCommand> {
&self.sub_commands
}
pub fn has_sub_commands(&self) -> bool {
!self.sub_commands.is_empty()
}
pub fn get_sub_command(&self, name: &str) -> Option<&FliCommand> {
self.sub_commands.get(name)
}
pub fn get_sub_command_mut(&mut self, name: &str) -> Option<&mut FliCommand> {
self.sub_commands.get_mut(name)
}
pub fn has_sub_command(&self, name: &str) -> bool {
self.sub_commands.contains_key(name)
}
pub fn add_option(
&mut self,
name: &str,
description: &str,
short_flag: &str,
long_flag: &str,
value: ValueTypes,
) -> &mut Self {
self.option_parser_builder
.add_option(name, description, short_flag, long_flag, value);
self
}
pub fn add_option_with_callback(
&mut self,
name: &str,
description: &str,
short_flag: &str,
long_flag: &str,
value: ValueTypes,
stop_main_callback: bool,
callback: fn(&FliCallbackData),
) -> &mut Self {
self.option_parser_builder.add_option(
name,
description,
short_flag,
long_flag,
value.clone(),
);
let preserved = PreservedOption {
long_flag: long_flag.to_string(),
short_flag: short_flag.to_string(),
value_type: value,
callback,
stop_main_callback,
};
let idx = self.preserved_options.len();
if !preserved.short_flag.is_empty() {
self.preserved_short_flags
.insert(preserved.short_flag.clone(), idx);
}
if !preserved.long_flag.is_empty() {
self.preserved_long_flags
.insert(preserved.long_flag.clone(), idx);
}
self.preserved_options.push(preserved);
self
}
pub fn get_preserved_option(&self, name: &str) -> Option<&PreservedOption> {
if let Some(idx) = self.preserved_short_flags.get(name) {
return self.preserved_options.get(*idx);
}
if let Some(idx) = self.preserved_long_flags.get(name) {
return self.preserved_options.get(*idx);
}
let trimmed = name.trim_start_matches('-');
let variants = [
format!("-{}", trimmed),
format!("--{}", trimmed),
trimmed.to_string(),
];
for v in &variants {
if let Some(idx) = self.preserved_short_flags.get(v.as_str()) {
return self.preserved_options.get(*idx);
}
if let Some(idx) = self.preserved_long_flags.get(v.as_str()) {
return self.preserved_options.get(*idx);
}
}
None
}
pub fn has_preserved_option(&self, name: &str) -> bool {
self.get_preserved_option(name).is_some()
}
pub fn subcommand(&mut self, name: &str, description: &str) -> &mut FliCommand {
let inherited_builder = self.get_option_parser().inheritable_options_builder();
let command = FliCommand::with_parser(name, description, inherited_builder);
self.add_sub_command(command);
self.sub_commands.get_mut(name).unwrap()
}
pub fn add_sub_command(&mut self, command: FliCommand) {
self.sub_commands
.insert(command.get_name().to_owned(), command);
}
pub fn get_callback(&self) -> Option<fn(&FliCallbackData)> {
self.callback
}
pub fn run(&mut self, mut arg_parser: InputArgsParser) -> Result<()> {
arg_parser.prepare(self)?;
display::debug_print(
"App",
&format!("Parsed arguments: {:?}", arg_parser.get_command_chain()),
);
let chain = arg_parser.get_parsed_commands_chain().clone();
let mut arguments = Vec::new();
let mut next_subcommand: Option<(&String, Vec<CommandChain>, usize)> = None;
let mut preserved_option: Option<&String> = None;
for (idx, item) in chain.iter().enumerate() {
match item {
CommandChain::SubCommand(sub_name) => {
let remaining: Vec<CommandChain> = chain[idx + 1..].to_vec();
next_subcommand = Some((sub_name, remaining, idx));
break;
}
CommandChain::Argument(arg) => {
arguments.push(arg.clone());
}
CommandChain::Option(_, _) => {
}
CommandChain::IsPreservedOption(s) => {
preserved_option = Some(s);
}
}
}
if let Some((sub_name, remaining_chain, idx)) = next_subcommand {
if let Some(sub_command) = self.get_sub_command_mut(sub_name) {
let mut sub_parser = arg_parser.with_remaining_chain(idx);
sub_parser.command_chain = remaining_chain;
return sub_command.run(sub_parser);
} else {
let available: Vec<String> = self.get_sub_commands().keys().cloned().collect();
return Err(FliError::UnknownCommand(sub_name.clone(), available));
}
}
let mut callback: Option<fn(&FliCallbackData)> = None;
let callback_data = FliCallbackData::new(
self.clone(),
self.get_option_parser().clone(),
arguments,
arg_parser,
);
if let Some(_callback) = self.get_callback() {
callback = Some(_callback);
}
if let Some(preserved_name) = preserved_option {
if let Some(preserved) = self.get_preserved_option(preserved_name) {
(preserved.callback)(&callback_data);
if preserved.stop_main_callback {
return Ok(());
}
}
}
if let Some(cb) = callback {
cb(&callback_data);
} else if chain.is_empty() && preserved_option.is_none() {
return Err(FliError::InvalidUsage(
"No subcommand or callback provided for this command".to_string(),
));
}
Ok(())
}
}