use clap::{ArgMatches, Command};
use ratatui::{
layout::Position,
style::{Style, Stylize},
text::Line,
};
use crate::widgets::InputState;
use super::{get_arg_matches, get_current_command, get_current_subcommands, CommanderCommand};
#[derive(Debug, PartialEq, Eq)]
pub enum CommanderMode
{
Message,
Input,
}
#[derive(Debug)]
pub struct CommanderState
{
pub(crate) mode: CommanderMode,
pub(crate) prefix: String,
pub(crate) command: Command,
pub(crate) input: InputState,
pub(crate) error_message: Option<String>,
pub(crate) selected_index: Option<usize>,
pub(crate) message: String,
pub(crate) message_style: Option<Style>,
}
impl CommanderState
{
pub fn new(commands: Vec<Command>) -> Self
{
let command = Command::new("commander4512879")
.subcommand_required(true)
.infer_subcommands(false)
.subcommands(commands);
Self {
mode: CommanderMode::Message,
prefix: String::from(":"),
command,
input: InputState::new(),
error_message: None,
selected_index: None,
message: String::new(),
message_style: None,
}
}
pub fn with_prefix<S>(
mut self,
prefix: S,
) -> Self
where
S: AsRef<str>,
{
self.prefix = prefix
.as_ref()
.to_string();
self
}
pub(crate) fn get_input_filter(&self) -> String
{
let input = self
.input
.input();
let Some(split) = shlex::split(&input)
else
{
return String::new();
};
split
.last()
.cloned()
.unwrap_or_default()
}
fn get_input_command(&self) -> Option<Vec<String>>
{
let input = self
.input
.input();
let prefix = self
.command
.get_name();
let input = format!("{prefix} {input}");
shlex::split(&input)
}
pub fn filtered(
&self
) -> (
Vec<&Command>,
bool,
)
{
let filter = self.get_input_filter();
let Some(mut items) = self.get_input_command()
else
{
return (
Vec::new(),
true,
);
};
let Some((matches, filtered)) = get_arg_matches(
&self.command,
&mut items,
false,
)
else
{
return (
self.command
.get_subcommands()
.filter(|c| c.is_filtered_by(&filter))
.collect::<Vec<_>>(),
true,
);
};
let commands = get_current_subcommands(
filter,
filtered,
&self.command,
matches,
);
(
commands, filtered,
)
}
pub fn selected(&self, ) -> Option<&Command>
{
let filter = self.get_input_filter();
let mut items = self.get_input_command()?;
let Some((matches, filtered)) = get_arg_matches(
&self.command,
&mut items,
false,
)
else
{
return self
.command
.get_subcommands()
.find(|c| c.is(&filter));
};
let command = get_current_command(
filter,
filtered,
&self.command,
matches,
);
command
}
fn description_error(
&self,
width: usize,
) -> Vec<Line>
{
self.error_message
.clone()
.map(
|s| {
let mut rows = textwrap::wrap(
s.as_str(),
width,
)
.iter()
.map(
|s| {
Line::styled(
s.to_string(),
Style::new().yellow(),
)
},
)
.collect::<Vec<_>>();
rows.insert(
0,
Line::raw(""),
);
rows
},
)
.unwrap_or_default()
}
pub fn description(
&self,
width: usize,
) -> Vec<Line>
{
if let Some(selected_index) = self.selected_index
{
let (filtered, _) = self.filtered();
let Some(command) = filtered.get(selected_index)
else
{
return self.description_error(width);
};
let mut main = command.wrapped_description(width);
let mut error = self.description_error(width);
main.append(&mut error);
main
}
else
{
let Some(command) = self.selected()
else
{
return self.description_error(width);
};
let mut main = command.wrapped_description(width);
let mut error = self.description_error(width);
main.append(&mut error);
main
}
}
pub fn description_height(
&self,
width: u16,
) -> u16
{
if self
.mode
.eq(&CommanderMode::Message)
{
return 0;
}
let inner_width = width.saturating_sub(8) as usize;
let size = self
.description(inner_width)
.len();
if size == 0
{
0
}
else
{
u16::try_from(size.saturating_add(2)).unwrap_or_default()
}
}
pub fn menu_height(&self) -> u16
{
if self
.mode
.eq(&CommanderMode::Message)
{
return 0;
}
let (filtered, _) = self.filtered();
let menu_height = filtered
.len()
.div_ceil(3);
u16::try_from(menu_height).unwrap_or_default()
}
pub fn height(
&self,
width: u16,
) -> u16
{
let desc_height = self.description_height(width);
let menu_height = self.menu_height();
let input_height = 1u16;
input_height
.saturating_add(menu_height)
.saturating_add(desc_height)
}
pub fn cursor(&self) -> Option<Position>
{
self.mode
.eq(&CommanderMode::Input)
.then_some(
self.input
.cursor(),
)
}
pub fn set_message<S>(
&mut self,
message: S,
) where
S: AsRef<str>,
{
self.message = message
.as_ref()
.to_string();
}
pub fn clear_message(&mut self)
{
self.message
.clear();
}
fn clean_error(
&self,
error: String,
) -> Option<String>
{
let name = self
.command
.get_name();
let error = error.replace(
&format!("'{name}' "),
"",
);
let error = error.replace(
&format!("{name} "),
"",
);
let error = error.replace(
name, "",
);
Some(error)
}
pub(crate) fn clear_edit(&mut self)
{
self.selected_index = None;
self.input
.clear();
self.message
.clear();
self.message_style
.take();
self.mode = CommanderMode::Message;
}
pub(crate) fn execute_command(
&mut self
) -> Option<(
String,
ArgMatches,
)>
{
let Some(input) = self.get_input_command()
else
{
self.error_message = Some("Invalid input string".to_string());
return None;
};
let args = match self
.command
.clone()
.try_get_matches_from(input.iter())
{
Ok(value) => value,
Err(e) =>
{
self.error_message = self.clean_error(format!("{e}"));
return None;
}
};
let Some(name) = args
.subcommand_name()
.map(str::to_string)
else
{
self.error_message = Some("Cannot get command name from argmatches".to_string());
return None;
};
let Some(args) = args
.subcommand_matches(&name)
.cloned()
else
{
self.error_message = Some("Cannot get subcommand matched".to_string());
return None;
};
self.clear_edit();
Some((
name, args,
))
}
}