#![no_std]
#![deny(missing_docs)]
pub type MenuCallbackFn<T> = fn(menu: &Menu<T>, context: &mut T);
pub type ItemCallbackFn<T> = fn(menu: &Menu<T>, item: &Item<T>, args: &[&str], context: &mut T);
#[derive(Debug)]
pub enum Parameter<'a> {
Mandatory {
parameter_name: &'a str,
help: Option<&'a str>,
},
Optional {
parameter_name: &'a str,
help: Option<&'a str>,
},
Named {
parameter_name: &'a str,
help: Option<&'a str>,
},
NamedValue {
parameter_name: &'a str,
argument_name: &'a str,
help: Option<&'a str>,
},
}
pub enum ItemType<'a, T>
where
T: 'a,
{
Callback {
function: ItemCallbackFn<T>,
parameters: &'a [Parameter<'a>],
},
Menu(&'a Menu<'a, T>),
_Dummy,
}
pub struct Item<'a, T>
where
T: 'a,
{
pub command: &'a str,
pub help: Option<&'a str>,
pub item_type: ItemType<'a, T>,
}
pub struct Menu<'a, T>
where
T: 'a,
{
pub label: &'a str,
pub items: &'a [&'a Item<'a, T>],
pub entry: Option<MenuCallbackFn<T>>,
pub exit: Option<MenuCallbackFn<T>>,
}
pub struct Runner<'a, T>
where
T: core::fmt::Write,
T: 'a,
{
buffer: &'a mut [u8],
used: usize,
menus: [Option<&'a Menu<'a, T>>; 4],
depth: usize,
pub context: T,
}
pub fn argument_finder<'a, T>(
item: &'a Item<'a, T>,
argument_list: &'a [&'a str],
name_to_find: &'a str,
) -> Result<Option<&'a str>, ()> {
if let ItemType::Callback { parameters, .. } = item.item_type {
let mut found_param = None;
let mut mandatory_count = 0;
let mut optional_count = 0;
for param in parameters.iter() {
match param {
Parameter::Mandatory { parameter_name, .. } => {
mandatory_count += 1;
if *parameter_name == name_to_find {
found_param = Some((param, mandatory_count));
}
}
Parameter::Optional { parameter_name, .. } => {
optional_count += 1;
if *parameter_name == name_to_find {
found_param = Some((param, optional_count));
}
}
Parameter::Named { parameter_name, .. } => {
if *parameter_name == name_to_find {
found_param = Some((param, 0));
}
}
Parameter::NamedValue { parameter_name, .. } => {
if *parameter_name == name_to_find {
found_param = Some((param, 0));
}
}
}
}
match found_param {
Some((Parameter::Mandatory { .. }, mandatory_idx)) => {
let mut positional_args_seen = 0;
for arg in argument_list.iter().filter(|x| !x.starts_with("--")) {
positional_args_seen += 1;
if positional_args_seen == mandatory_idx {
return Ok(Some(arg));
}
}
Ok(None)
}
Some((Parameter::Optional { .. }, optional_idx)) => {
let mut positional_args_seen = 0;
for arg in argument_list.iter().filter(|x| !x.starts_with("--")) {
positional_args_seen += 1;
if positional_args_seen == (mandatory_count + optional_idx) {
return Ok(Some(arg));
}
}
Ok(None)
}
Some((Parameter::Named { parameter_name, .. }, _)) => {
for arg in argument_list {
if arg.starts_with("--") && (&arg[2..] == *parameter_name) {
return Ok(Some(""));
}
}
Ok(None)
}
Some((Parameter::NamedValue { parameter_name, .. }, _)) => {
let name_start = 2;
let equals_start = name_start + parameter_name.len();
let value_start = equals_start + 1;
for arg in argument_list {
if arg.starts_with("--")
&& (arg.len() >= value_start)
&& (arg.get(equals_start..=equals_start) == Some("="))
&& (arg.get(name_start..equals_start) == Some(*parameter_name))
{
return Ok(Some(&arg[value_start..]));
}
}
Ok(None)
}
_ => Err(()),
}
} else {
Err(())
}
}
enum Outcome {
CommandProcessed,
NeedMore,
}
impl<'a, T> Runner<'a, T>
where
T: core::fmt::Write,
{
pub fn new(menu: &'a Menu<'a, T>, buffer: &'a mut [u8], mut context: T) -> Runner<'a, T> {
if let Some(cb_fn) = menu.entry {
cb_fn(menu, &mut context);
}
let mut r = Runner {
menus: [Some(menu), None, None, None],
depth: 0,
buffer,
used: 0,
context,
};
r.prompt(true);
r
}
pub fn prompt(&mut self, newline: bool) {
if newline {
writeln!(self.context).unwrap();
}
if self.depth != 0 {
let mut depth = 1;
while depth <= self.depth {
if depth > 1 {
write!(self.context, "/").unwrap();
}
write!(self.context, "/{}", self.menus[depth].unwrap().label).unwrap();
depth += 1;
}
}
write!(self.context, "> ").unwrap();
}
pub fn input_byte(&mut self, input: u8) {
if input == 0x0A {
return;
}
let outcome = if input == 0x0D {
self.process_command();
Outcome::CommandProcessed
} else if (input == 0x08) || (input == 0x7F) {
if self.used > 0 {
write!(self.context, "\u{0008} \u{0008}").unwrap();
self.used -= 1;
}
Outcome::NeedMore
} else if self.used < self.buffer.len() {
self.buffer[self.used] = input;
self.used += 1;
let valid = core::str::from_utf8(&self.buffer[0..self.used]).is_ok();
if valid {
write!(self.context, "\r").unwrap();
self.prompt(false);
}
if let Ok(s) = core::str::from_utf8(&self.buffer[0..self.used]) {
write!(self.context, "{}", s).unwrap();
}
Outcome::NeedMore
} else {
writeln!(self.context, "Buffer overflow!").unwrap();
Outcome::NeedMore
};
match outcome {
Outcome::CommandProcessed => {
self.used = 0;
self.prompt(true);
}
Outcome::NeedMore => {}
}
}
fn process_command(&mut self) {
writeln!(self.context).unwrap();
if let Ok(command_line) = core::str::from_utf8(&self.buffer[0..self.used]) {
let mut parts = command_line.split_whitespace();
if let Some(cmd) = parts.next() {
let menu = self.menus[self.depth].unwrap();
if cmd == "help" {
match parts.next() {
Some(arg) => match menu.items.iter().find(|i| i.command == arg) {
Some(item) => {
self.print_long_help(&item);
}
None => {
writeln!(self.context, "I can't help with {:?}", arg).unwrap();
}
},
_ => {
writeln!(self.context, "AVAILABLE ITEMS:").unwrap();
for item in menu.items {
self.print_short_help(&item);
}
if self.depth != 0 {
self.print_short_help(&Item {
command: "exit",
help: Some("Leave this menu."),
item_type: ItemType::_Dummy,
});
}
self.print_short_help(&Item {
command: "help [ <command> ]",
help: Some("Show this help, or get help on a specific command."),
item_type: ItemType::_Dummy,
});
}
}
} else if cmd == "exit" && self.depth != 0 {
self.menus[self.depth] = None;
self.depth -= 1;
} else {
let mut found = false;
for item in menu.items {
if cmd == item.command {
match item.item_type {
ItemType::Callback {
function,
parameters,
} => Self::call_function(
&mut self.context,
function,
parameters,
menu,
item,
command_line,
),
ItemType::Menu(m) => {
self.depth += 1;
self.menus[self.depth] = Some(m);
}
ItemType::_Dummy => {
unreachable!();
}
}
found = true;
break;
}
}
if !found {
writeln!(self.context, "Command {:?} not found. Try 'help'.", cmd).unwrap();
}
}
} else {
writeln!(self.context, "Input was empty?").unwrap();
}
} else {
writeln!(self.context, "Input was not valid UTF-8").unwrap();
}
}
fn print_short_help(&mut self, item: &Item<T>) {
let mut has_options = false;
match item.item_type {
ItemType::Callback { parameters, .. } => {
write!(self.context, " {}", item.command).unwrap();
if !parameters.is_empty() {
for param in parameters.iter() {
match param {
Parameter::Mandatory { parameter_name, .. } => {
write!(self.context, " <{}>", parameter_name).unwrap();
}
Parameter::Optional { parameter_name, .. } => {
write!(self.context, " [ <{}> ]", parameter_name).unwrap();
}
Parameter::Named { .. } => {
has_options = true;
}
Parameter::NamedValue { .. } => {
has_options = true;
}
}
}
}
}
ItemType::Menu(_menu) => {
write!(self.context, " {}", item.command).unwrap();
}
ItemType::_Dummy => {
write!(self.context, " {}", item.command).unwrap();
}
}
if has_options {
write!(self.context, " [OPTIONS...]").unwrap();
}
writeln!(self.context).unwrap();
}
fn print_long_help(&mut self, item: &Item<T>) {
writeln!(self.context, "SUMMARY:").unwrap();
match item.item_type {
ItemType::Callback { parameters, .. } => {
write!(self.context, " {}", item.command).unwrap();
if !parameters.is_empty() {
for param in parameters.iter() {
match param {
Parameter::Mandatory { parameter_name, .. } => {
write!(self.context, " <{}>", parameter_name).unwrap();
}
Parameter::Optional { parameter_name, .. } => {
write!(self.context, " [ <{}> ]", parameter_name).unwrap();
}
Parameter::Named { parameter_name, .. } => {
write!(self.context, " [ --{} ]", parameter_name).unwrap();
}
Parameter::NamedValue {
parameter_name,
argument_name,
..
} => {
write!(self.context, " [ --{}={} ]", parameter_name, argument_name)
.unwrap();
}
}
}
writeln!(self.context, "\n\nPARAMETERS:").unwrap();
let default_help = "Undocumented option";
for param in parameters.iter() {
match param {
Parameter::Mandatory {
parameter_name,
help,
} => {
writeln!(
self.context,
" <{0}>\n {1}\n",
parameter_name,
help.unwrap_or(default_help),
)
.unwrap();
}
Parameter::Optional {
parameter_name,
help,
} => {
writeln!(
self.context,
" <{0}>\n {1}\n",
parameter_name,
help.unwrap_or(default_help),
)
.unwrap();
}
Parameter::Named {
parameter_name,
help,
} => {
writeln!(
self.context,
" --{0}\n {1}\n",
parameter_name,
help.unwrap_or(default_help),
)
.unwrap();
}
Parameter::NamedValue {
parameter_name,
argument_name,
help,
} => {
writeln!(
self.context,
" --{0}={1}\n {2}\n",
parameter_name,
argument_name,
help.unwrap_or(default_help),
)
.unwrap();
}
}
}
}
}
ItemType::Menu(_menu) => {
write!(self.context, " {}", item.command).unwrap();
}
ItemType::_Dummy => {
write!(self.context, " {}", item.command).unwrap();
}
}
if let Some(help) = item.help {
writeln!(self.context, "\n\nDESCRIPTION:\n{}", help).unwrap();
}
}
fn call_function(
context: &mut T,
callback_function: ItemCallbackFn<T>,
parameters: &[Parameter],
parent_menu: &Menu<T>,
item: &Item<T>,
command: &str,
) {
let mandatory_parameter_count = parameters
.iter()
.filter(|p| match p {
Parameter::Mandatory { .. } => true,
_ => false,
})
.count();
let positional_parameter_count = parameters
.iter()
.filter(|p| match p {
Parameter::Mandatory { .. } => true,
Parameter::Optional { .. } => true,
_ => false,
})
.count();
if command.len() >= item.command.len() {
let mut argument_buffer: [&str; 16] = [""; 16];
let mut argument_count = 0;
let mut positional_arguments = 0;
for (slot, arg) in argument_buffer
.iter_mut()
.zip(command[item.command.len()..].split_whitespace())
{
*slot = arg;
argument_count += 1;
if arg.starts_with("--") {
let mut found = false;
for param in parameters.iter() {
match param {
Parameter::Named { parameter_name, .. } => {
if &arg[2..] == *parameter_name {
found = true;
break;
}
}
Parameter::NamedValue { parameter_name, .. } => {
if arg.contains('=') {
if let Some(given_name) = arg[2..].split('=').next() {
if given_name == *parameter_name {
found = true;
break;
}
}
}
}
_ => {
}
}
}
if !found {
writeln!(context, "Error: Did not understand {:?}", arg).unwrap();
return;
}
} else {
positional_arguments += 1;
}
}
if positional_arguments < mandatory_parameter_count {
writeln!(context, "Error: Insufficient arguments given").unwrap();
} else if positional_arguments > positional_parameter_count {
writeln!(context, "Error: Too many arguments given").unwrap();
} else {
callback_function(
parent_menu,
item,
&argument_buffer[0..argument_count],
context,
);
}
} else {
if mandatory_parameter_count == 0 {
callback_function(parent_menu, item, &[], context);
} else {
writeln!(context, "Error: Insufficient arguments given").unwrap();
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn dummy(_menu: &Menu<u32>, _item: &Item<u32>, _args: &[&str], _context: &mut u32) {}
#[test]
fn find_arg_mandatory() {
let item = Item {
command: "dummy",
help: None,
item_type: ItemType::Callback {
function: dummy,
parameters: &[
Parameter::Mandatory {
parameter_name: "foo",
help: Some("Some help for foo"),
},
Parameter::Mandatory {
parameter_name: "bar",
help: Some("Some help for bar"),
},
Parameter::Mandatory {
parameter_name: "baz",
help: Some("Some help for baz"),
},
],
},
};
assert_eq!(
argument_finder(&item, &["a", "b", "c"], "foo"),
Ok(Some("a"))
);
assert_eq!(
argument_finder(&item, &["a", "b", "c"], "bar"),
Ok(Some("b"))
);
assert_eq!(
argument_finder(&item, &["a", "b", "c"], "baz"),
Ok(Some("c"))
);
assert_eq!(argument_finder(&item, &["a", "b", "c"], "quux"), Err(()));
}
#[test]
fn find_arg_optional() {
let item = Item {
command: "dummy",
help: None,
item_type: ItemType::Callback {
function: dummy,
parameters: &[
Parameter::Mandatory {
parameter_name: "foo",
help: Some("Some help for foo"),
},
Parameter::Mandatory {
parameter_name: "bar",
help: Some("Some help for bar"),
},
Parameter::Optional {
parameter_name: "baz",
help: Some("Some help for baz"),
},
],
},
};
assert_eq!(
argument_finder(&item, &["a", "b", "c"], "foo"),
Ok(Some("a"))
);
assert_eq!(
argument_finder(&item, &["a", "b", "c"], "bar"),
Ok(Some("b"))
);
assert_eq!(
argument_finder(&item, &["a", "b", "c"], "baz"),
Ok(Some("c"))
);
assert_eq!(argument_finder(&item, &["a", "b", "c"], "quux"), Err(()));
assert_eq!(argument_finder(&item, &["a", "b"], "baz"), Ok(None));
}
#[test]
fn find_arg_named() {
let item = Item {
command: "dummy",
help: None,
item_type: ItemType::Callback {
function: dummy,
parameters: &[
Parameter::Mandatory {
parameter_name: "foo",
help: Some("Some help for foo"),
},
Parameter::Named {
parameter_name: "bar",
help: Some("Some help for bar"),
},
Parameter::Named {
parameter_name: "baz",
help: Some("Some help for baz"),
},
],
},
};
assert_eq!(
argument_finder(&item, &["a", "--bar", "--baz"], "foo"),
Ok(Some("a"))
);
assert_eq!(
argument_finder(&item, &["a", "--bar", "--baz"], "bar"),
Ok(Some(""))
);
assert_eq!(
argument_finder(&item, &["a", "--bar", "--baz"], "baz"),
Ok(Some(""))
);
assert_eq!(
argument_finder(&item, &["a", "--bar", "--baz"], "quux"),
Err(())
);
assert_eq!(argument_finder(&item, &["a"], "baz"), Ok(None));
}
#[test]
fn find_arg_namedvalue() {
let item = Item {
command: "dummy",
help: None,
item_type: ItemType::Callback {
function: dummy,
parameters: &[
Parameter::Mandatory {
parameter_name: "foo",
help: Some("Some help for foo"),
},
Parameter::Named {
parameter_name: "bar",
help: Some("Some help for bar"),
},
Parameter::NamedValue {
parameter_name: "baz",
argument_name: "TEST",
help: Some("Some help for baz"),
},
],
},
};
assert_eq!(
argument_finder(&item, &["a", "--bar", "--baz"], "foo"),
Ok(Some("a"))
);
assert_eq!(
argument_finder(&item, &["a", "--bar", "--baz"], "bar"),
Ok(Some(""))
);
assert_eq!(
argument_finder(&item, &["a", "--bar", "--baz"], "baz"),
Ok(None)
);
assert_eq!(
argument_finder(&item, &["a", "--bar", "--baz="], "baz"),
Ok(Some(""))
);
assert_eq!(
argument_finder(&item, &["a", "--bar", "--baz=1"], "baz"),
Ok(Some("1"))
);
assert_eq!(
argument_finder(
&item,
&["a", "--bar", "--baz=abcdefghijklmnopqrstuvwxyz"],
"baz"
),
Ok(Some("abcdefghijklmnopqrstuvwxyz"))
);
assert_eq!(
argument_finder(&item, &["a", "--bar", "--baz"], "quux"),
Err(())
);
assert_eq!(argument_finder(&item, &["a"], "baz"), Ok(None));
}
}