diff --git a/Cargo.toml b/Cargo.toml index 0e61c5f..093688b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,5 +4,10 @@ version = "0.1.1" authors = ["Jonathan 'theJPster' Pallant "] description = "A simple #[no_std] command line interface." license = "MIT OR Apache-2.0" +edition = "2018" [dependencies] + + +[dev-dependencies] +pancurses = "0.16" diff --git a/README.md b/README.md index 8dcfb84..7a209ed 100644 --- a/README.md +++ b/README.md @@ -1,39 +1,179 @@ # Menu +## Introduction + A simple command-line menu system in Rust. Works on embedded systems, but also on your command-line. -``` -$ cargo run --example simple - Compiling menu v0.1.0 (file:///home/jonathan/Documents/programming/menu) +**NOTE:** This crates works only in `&str` - there's no heap allocation, but +there's also no automatic conversion to integers, boolean, etc. + +```console +user@host: ~/menu $ cargo run --example simple + Compiling menu v0.2.0 (file:///home/user/menu) Finished dev [unoptimized + debuginfo] target(s) in 0.84 secs Running `target/debug/examples/simple` In enter_root() > help -foo - makes a foo appear -bar - fandoggles a bar -sub - enter sub-menu -help - print this help text. +AVAILABLE ITEMS: + foo [ ] [ --verbose ] [ --level=INT ] - Makes a foo appear. + bar - fandoggles a bar + sub - enter sub-menu + help [ ] - Show this help, or get help on a specific command. + + +> help foo +SUMMARY: + foo [ ] [ --verbose ] [ --level=INT ] + +PARAMETERS: + + - This is the help text for 'a' + + - No help text found + --verbose + - No help text found + --level=INT + - Set the level of the dangle + + +DESCRIPTION: +Makes a foo appear. + +This is some extensive help text. + +It contains multiple paragraphs and should be preceeded by the parameter list. + +> foo --level=3 --verbose 1 2 +In select_foo. Args = ["--level=3", "--verbose", "1", "2"] +a = Ok(Some("1")) +b = Ok(Some("2")) +verbose = Ok(Some("")) +level = Ok(Some("3")) +no_such_arg = Err(()) + > foo -In select_foo(): foo +Error: Insufficient arguments given! + +> foo 1 2 3 3 +Error: Too many arguments given > sub -sub> help -baz - thingamobob a baz -quux - maximum quux -exit - leave this menu. -help - print this help text. +/sub> help +AVAILABLE ITEMS: + baz - thingamobob a baz + quux - maximum quux + exit - Leave this menu. + help [ ] - Show this help, or get help on a specific command. + > exit > help -foo - makes a foo appear -bar - fandoggles a bar -sub - enter sub-menu -help - print this help text. +AVAILABLE ITEMS: + foo [ ] [ --verbose ] [ --level=INT ] - Makes a foo appear. + bar - fandoggles a bar + sub - enter sub-menu + help [ ] - Show this help, or get help on a specific command. + > ^C -$ +user@host: ~/menu $ ``` + +## Using + +See `examples/simple.rs` for a working example that runs on Linux or Windows. Here's the menu definition from that example: + +```rust +const ROOT_MENU: Menu = Menu { + label: "root", + items: &[ + &Item { + item_type: ItemType::Callback { + function: select_foo, + parameters: &[ + Parameter::Mandatory { + parameter_name: "a", + help: Some("This is the help text for 'a'"), + }, + Parameter::Optional { + parameter_name: "b", + help: None, + }, + Parameter::Named { + parameter_name: "verbose", + help: None, + }, + Parameter::NamedValue { + parameter_name: "level", + argument_name: "INT", + help: Some("Set the level of the dangle"), + }, + ], + }, + command: "foo", + help: Some( + "Makes a foo appear. + +This is some extensive help text. + +It contains multiple paragraphs and should be preceeded by the parameter list. +", + ), + }, + &Item { + item_type: ItemType::Callback { + function: select_bar, + parameters: &[], + }, + command: "bar", + help: Some("fandoggles a bar"), + }, + &Item { + item_type: ItemType::Menu(&Menu { + label: "sub", + items: &[ + &Item { + item_type: ItemType::Callback { + function: select_baz, + parameters: &[], + }, + command: "baz", + help: Some("thingamobob a baz"), + }, + &Item { + item_type: ItemType::Callback { + function: select_quux, + parameters: &[], + }, + command: "quux", + help: Some("maximum quux"), + }, + ], + entry: Some(enter_sub), + exit: Some(exit_sub), + }), + command: "sub", + help: Some("enter sub-menu"), + }, + ], + entry: Some(enter_root), + exit: Some(exit_root), +}; + +``` + +## Changelog + +### Unreleased changes + +* Parameter / Argument support +* Re-worked help text system +* Example uses `pancurses` + +### v0.1.0 + +* First release diff --git a/examples/simple.rs b/examples/simple.rs index 35d0b49..e450108 100644 --- a/examples/simple.rs +++ b/examples/simple.rs @@ -1,109 +1,184 @@ extern crate menu; -use std::io::{self, Read, Write}; use menu::*; - -const FOO_ITEM: Item = Item { - item_type: ItemType::Callback(select_foo), - command: "foo", - help: Some("makes a foo appear"), -}; - -const BAR_ITEM: Item = Item { - item_type: ItemType::Callback(select_bar), - command: "bar", - help: Some("fandoggles a bar"), -}; - -const ENTER_ITEM: Item = Item { - item_type: ItemType::Menu(&SUB_MENU), - command: "sub", - help: Some("enter sub-menu"), -}; +use pancurses::{endwin, initscr, noecho, Input}; +use std::fmt::Write; const ROOT_MENU: Menu = Menu { label: "root", - items: &[&FOO_ITEM, &BAR_ITEM, &ENTER_ITEM], + items: &[ + &Item { + item_type: ItemType::Callback { + function: select_foo, + parameters: &[ + Parameter::Mandatory { + parameter_name: "a", + help: Some("This is the help text for 'a'"), + }, + Parameter::Optional { + parameter_name: "b", + help: None, + }, + Parameter::Named { + parameter_name: "verbose", + help: None, + }, + Parameter::NamedValue { + parameter_name: "level", + argument_name: "INT", + help: Some("Set the level of the dangle"), + }, + ], + }, + command: "foo", + help: Some( + "Makes a foo appear. + +This is some extensive help text. + +It contains multiple paragraphs and should be preceeded by the parameter list. +", + ), + }, + &Item { + item_type: ItemType::Callback { + function: select_bar, + parameters: &[], + }, + command: "bar", + help: Some("fandoggles a bar"), + }, + &Item { + item_type: ItemType::Menu(&Menu { + label: "sub", + items: &[ + &Item { + item_type: ItemType::Callback { + function: select_baz, + parameters: &[], + }, + command: "baz", + help: Some("thingamobob a baz"), + }, + &Item { + item_type: ItemType::Callback { + function: select_quux, + parameters: &[], + }, + command: "quux", + help: Some("maximum quux"), + }, + ], + entry: Some(enter_sub), + exit: Some(exit_sub), + }), + command: "sub", + help: Some("enter sub-menu"), + }, + ], entry: Some(enter_root), exit: Some(exit_root), }; -const BAZ_ITEM: Item = Item { - item_type: ItemType::Callback(select_baz), - command: "baz", - help: Some("thingamobob a baz"), -}; - -const QUUX_ITEM: Item = Item { - item_type: ItemType::Callback(select_quux), - command: "quux", - help: Some("maximum quux"), -}; - -const SUB_MENU: Menu = Menu { - label: "sub", - items: &[&BAZ_ITEM, &QUUX_ITEM], - entry: Some(enter_sub), - exit: Some(exit_sub), -}; - -struct Output; +struct Output(pancurses::Window); impl std::fmt::Write for Output { fn write_str(&mut self, s: &str) -> Result<(), std::fmt::Error> { - let mut stdout = io::stdout(); - write!(stdout, "{}", s).unwrap(); - stdout.flush().unwrap(); + self.0.printw(s); Ok(()) } } fn main() { + let window = initscr(); + window.scrollok(true); + noecho(); let mut buffer = [0u8; 64]; - let mut o = Output; - let mut r = Runner::new(&ROOT_MENU, &mut buffer, &mut o); + let mut r = Runner::new(&ROOT_MENU, &mut buffer, Output(window)); loop { - let mut ch = [0x00u8; 1]; - // Wait for char - if let Ok(_) = io::stdin().read(&mut ch) { - // Fix newlines - if ch[0] == 0x0A { - ch[0] = 0x0D; + match r.context.0.getch() { + Some(Input::Character('\n')) => { + r.input_byte(b'\r'); + } + Some(Input::Character(c)) => { + let mut buf = [0; 4]; + for b in c.encode_utf8(&mut buf).bytes() { + r.input_byte(b); + } + } + Some(Input::KeyDC) => break, + Some(input) => { + r.context.0.addstr(&format!("{:?}", input)); } - // Feed char to runner - r.input_byte(ch[0]); + None => (), } } + endwin(); } -fn enter_root(_menu: &Menu) { - println!("In enter_root"); +fn enter_root(_menu: &Menu, context: &mut Output) { + writeln!(context, "In enter_root").unwrap(); } -fn exit_root(_menu: &Menu) { - println!("In exit_root"); +fn exit_root(_menu: &Menu, context: &mut Output) { + writeln!(context, "In exit_root").unwrap(); } -fn select_foo<'a>(_menu: &Menu, _item: &Item, input: &str, _context: &mut Output) { - println!("In select_foo: {}", input); +fn select_foo<'a>(_menu: &Menu, item: &Item, args: &[&str], context: &mut Output) { + writeln!(context, "In select_foo. Args = {:?}", args).unwrap(); + writeln!( + context, + "a = {:?}", + ::menu::argument_finder(item, args, "a") + ) + .unwrap(); + writeln!( + context, + "b = {:?}", + ::menu::argument_finder(item, args, "b") + ) + .unwrap(); + writeln!( + context, + "verbose = {:?}", + ::menu::argument_finder(item, args, "verbose") + ) + .unwrap(); + writeln!( + context, + "level = {:?}", + ::menu::argument_finder(item, args, "level") + ) + .unwrap(); + writeln!( + context, + "no_such_arg = {:?}", + ::menu::argument_finder(item, args, "no_such_arg") + ) + .unwrap(); } -fn select_bar<'a>(_menu: &Menu, _item: &Item, input: &str, _context: &mut Output) { - println!("In select_bar: {}", input); +fn select_bar<'a>(_menu: &Menu, _item: &Item, args: &[&str], context: &mut Output) { + writeln!(context, "In select_bar. Args = {:?}", args).unwrap(); } -fn enter_sub(_menu: &Menu) { - println!("In enter_sub"); +fn enter_sub(_menu: &Menu, context: &mut Output) { + writeln!(context, "In enter_sub").unwrap(); } -fn exit_sub(_menu: &Menu) { - println!("In exit_sub"); +fn exit_sub(_menu: &Menu, context: &mut Output) { + writeln!(context, "In exit_sub").unwrap(); } -fn select_baz<'a>(_menu: &Menu, _item: &Item, input: &str, _context: &mut Output) { - println!("In select_baz: {}", input); +fn select_baz<'a>(_menu: &Menu, _item: &Item, args: &[&str], context: &mut Output) { + writeln!(context, "In select_baz: Args = {:?}", args).unwrap(); } -fn select_quux<'a>(_menu: &Menu, _item: &Item, input: &str, _context: &mut Output) { - println!("In select_quux: {}", input); +fn select_quux<'a>( + _menu: &Menu, + _item: &Item, + args: &[&str], + context: &mut Output, +) { + writeln!(context, "In select_quux: Args = {:?}", args).unwrap(); } diff --git a/src/lib.rs b/src/lib.rs index 0e8a29b..bf9adec 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,37 +1,105 @@ +//! # Menu +//! +//! A basic command-line interface for `#![no_std]` Rust programs. Peforms +//! zero heap allocation. #![no_std] +#![deny(missing_docs)] -type MenuCallbackFn = fn(menu: &Menu); -type ItemCallbackFn = fn(menu: &Menu, item: &Item, args: &str, context: &mut T); +/// The type of function we call when we enter/exit a menu. +pub type MenuCallbackFn = fn(menu: &Menu, context: &mut T); +/// The type of function we call when we a valid command has been entered. +pub type ItemCallbackFn = fn(menu: &Menu, item: &Item, args: &[&str], context: &mut T); + +#[derive(Debug)] +/// Describes a parameter to the command +pub enum Parameter<'a> { + /// A mandatory positional parameter + Mandatory { + /// A name for this mandatory positional parameter + parameter_name: &'a str, + /// Help text + help: Option<&'a str>, + }, + /// An optional positional parameter. Must come after the mandatory positional arguments. + Optional { + /// A name for this optional positional parameter + parameter_name: &'a str, + /// Help text + help: Option<&'a str>, + }, + /// An optional named parameter with no argument (e.g. `--verbose` or `--dry-run`) + Named { + /// The bit that comes after the `--` + parameter_name: &'a str, + /// Help text + help: Option<&'a str>, + }, + /// A optional named parameter with argument (e.g. `--mode=foo` or `--level=3`) + NamedValue { + /// The bit that comes after the `--` + parameter_name: &'a str, + /// The bit that comes after the `--name=`, e.g. `INT` or `FILE`. It's mostly for help text. + argument_name: &'a str, + /// Help text + help: Option<&'a str>, + }, +} + +/// Do we enter a sub-menu when this command is entered, or call a specific +/// function? pub enum ItemType<'a, T> where T: 'a, { - Callback(ItemCallbackFn), + /// Call a function when this command is entered + Callback { + /// The function to call + function: ItemCallbackFn, + /// The list of parameters for this function. Pass an empty list if there aren't any. + parameters: &'a [Parameter<'a>], + }, + /// This item is a sub-menu you can enter Menu(&'a Menu<'a, T>), + /// Internal use only - do not use + _Dummy, } -/// Menu Item +/// An `Item` is a what our menus are made from. Each item has a `name` which +/// you have to enter to select this item. Each item can also have zero or +/// more parameters, and some optional help text. pub struct Item<'a, T> where T: 'a, { + /// The word you need to enter to activate this item. It is recommended + /// that you avoid whitespace in this string. pub command: &'a str, + /// Optional help text. Printed if you enter `help`. pub help: Option<&'a str>, + /// The type of this item - menu, callback, etc. pub item_type: ItemType<'a, T>, } -/// A Menu is made of Items +/// A `Menu` is made of one or more `Item`s. pub struct Menu<'a, T> where T: 'a, { + /// Each menu has a label which is visible in the prompt, unless you are + /// the root menu. pub label: &'a str, + /// A slice of menu items in this menu. pub items: &'a [&'a Item<'a, T>], + /// A function to call when this menu is entered. If this is the root menu, this is called when the runner is created. pub entry: Option>, + /// A function to call when this menu is exited. Never called for the root menu. pub exit: Option>, } +/// This structure handles the menu. You feed it bytes as they are read from +/// the console and it executes menu actions when commands are typed in +/// (followed by Enter). pub struct Runner<'a, T> where T: core::fmt::Write, @@ -42,7 +110,120 @@ where /// Maximum four levels deep menus: [Option<&'a Menu<'a, T>>; 4], depth: usize, - pub context: &'a mut T, + /// The context object the `Runner` carries around. + pub context: T, +} + +/// Looks for the named parameter in the parameter list of the item, then +/// finds the correct argument. +/// +/// * Returns `Ok(None)` if `parameter_name` gives an optional or named +/// parameter and that argument was not given. +/// * Returns `Ok(arg)` if the argument corresponding to `parameter_name` was +/// found. `arg` is the empty string if the parameter was `Parameter::Named` +/// (and hence doesn't take a value). +/// * Returns `Err(())` if `parameter_name` was not in `item.parameter_list` +/// or `item` wasn't an Item::Callback +pub fn argument_finder<'a, T>( + item: &'a Item<'a, T>, + argument_list: &'a [&'a str], + name_to_find: &'a str, +) -> Result, ()> { + if let ItemType::Callback { parameters, .. } = item.item_type { + // Step 1 - Find `name_to_find` in the parameter list. + 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)); + } + } + } + } + // Step 2 - What sort of parameter is it? + match found_param { + // Step 2a - Mandatory Positional + Some((Parameter::Mandatory { .. }, mandatory_idx)) => { + // We want positional parameter number `mandatory_idx`. + let mut positional_args_seen = 0; + for arg in argument_list.iter().filter(|x| !x.starts_with("--")) { + // Positional + positional_args_seen += 1; + if positional_args_seen == mandatory_idx { + return Ok(Some(arg)); + } + } + // Valid thing to ask for but we don't have it + Ok(None) + } + // Step 2b - Optional Positional + Some((Parameter::Optional { .. }, optional_idx)) => { + // We want positional parameter number `mandatory_count + optional_idx`. + let mut positional_args_seen = 0; + for arg in argument_list.iter().filter(|x| !x.starts_with("--")) { + // Positional + positional_args_seen += 1; + if positional_args_seen == (mandatory_count + optional_idx) { + return Ok(Some(arg)); + } + } + // Valid thing to ask for but we don't have it + Ok(None) + } + // Step 2c - Named (e.g. `--verbose`) + Some((Parameter::Named { parameter_name, .. }, _)) => { + for arg in argument_list { + if arg.starts_with("--") && (&arg[2..] == *parameter_name) { + return Ok(Some("")); + } + } + // Valid thing to ask for but we don't have it + Ok(None) + } + // Step 2d - NamedValue (e.g. `--level=123`) + 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..])); + } + } + // Valid thing to ask for but we don't have it + Ok(None) + } + // Step 2e - not found + _ => Err(()), + } + } else { + // Not an item with arguments + Err(()) + } } enum Outcome { @@ -54,9 +235,13 @@ impl<'a, T> Runner<'a, T> where T: core::fmt::Write, { - pub fn new(menu: &'a Menu<'a, T>, buffer: &'a mut [u8], context: &'a mut T) -> Runner<'a, T> { + /// Create a new `Runner`. You need to supply a top-level menu, and a + /// buffer that the `Runner` can use. Feel free to pass anything as the + /// `context` type - the only requirement is that the `Runner` can + /// `write!` to the context, which it will do for all text output. + 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); + cb_fn(menu, &mut context); } let mut r = Runner { menus: [Some(menu), None, None, None], @@ -69,9 +254,11 @@ where r } + /// Print out a new command prompt, including sub-menu names if + /// applicable. pub fn prompt(&mut self, newline: bool) { if newline { - write!(self.context, "\n").unwrap(); + writeln!(self.context).unwrap(); } if self.depth != 0 { let mut depth = 1; @@ -86,70 +273,20 @@ where write!(self.context, "> ").unwrap(); } + /// Add a byte to the menu runner's buffer. If this byte is a + /// carriage-return, the buffer is scanned and the appropriate action + /// performed. pub fn input_byte(&mut self, input: u8) { // Strip carriage returns if input == 0x0A { return; } let outcome = if input == 0x0D { - write!(self.context, "\n").unwrap(); - if let Ok(s) = core::str::from_utf8(&self.buffer[0..self.used]) { - if s == "help" { - let menu = self.menus[self.depth].unwrap(); - for item in menu.items { - if let Some(help) = item.help { - writeln!(self.context, "{} - {}", item.command, help).unwrap(); - } else { - writeln!(self.context, "{}", item.command).unwrap(); - } - } - if self.depth != 0 { - writeln!(self.context, "exit - leave this menu.").unwrap(); - } - writeln!(self.context, "help - print this help text.").unwrap(); - Outcome::CommandProcessed - } else if s == "exit" && self.depth != 0 { - if self.depth == self.menus.len() { - writeln!(self.context, "Can't enter menu - structure too deep.").unwrap(); - } else { - self.menus[self.depth] = None; - self.depth -= 1; - } - Outcome::CommandProcessed - } else { - let mut parts = s.split(' '); - if let Some(cmd) = parts.next() { - let mut found = false; - let menu = self.menus[self.depth].unwrap(); - for item in menu.items { - if cmd == item.command { - match item.item_type { - ItemType::Callback(f) => f(menu, item, s, &mut self.context), - ItemType::Menu(m) => { - self.depth += 1; - self.menus[self.depth] = Some(m); - } - } - found = true; - break; - } - } - if !found { - writeln!(self.context, "Command {:?} not found. Try 'help'.", cmd) - .unwrap(); - } - Outcome::CommandProcessed - } else { - writeln!(self.context, "Input empty").unwrap(); - Outcome::CommandProcessed - } - } - } else { - writeln!(self.context, "Input not valid UTF8").unwrap(); - Outcome::CommandProcessed - } - } else if input == 0x08 { - // Handling backspace + // Handle the command + self.process_command(); + Outcome::CommandProcessed + } else if (input == 0x08) || (input == 0x7F) { + // Handling backspace or delete if self.used > 0 { write!(self.context, "\u{0008} \u{0008}").unwrap(); self.used -= 1; @@ -159,15 +296,17 @@ where self.buffer[self.used] = input; self.used += 1; - let valid = if let Ok(_) = core::str::from_utf8(&self.buffer[0..self.used]) { - true - } else { - false - }; + // We have to do this song and dance because `self.prompt()` needs + // a mutable reference to self, and we can't have that while + // holding a reference to the buffer at the same time. + // This line grabs the buffer, checks it's OK, then releases it again + let valid = core::str::from_utf8(&self.buffer[0..self.used]).is_ok(); + // Now we've released the buffer, we can draw the prompt if valid { write!(self.context, "\r").unwrap(); self.prompt(false); } + // Grab the buffer again to render it to the screen if let Ok(s) = core::str::from_utf8(&self.buffer[0..self.used]) { write!(self.context, "{}", s).unwrap(); } @@ -184,12 +323,509 @@ where Outcome::NeedMore => {} } } + + /// Scan the buffer and do the right thing based on its contents. + fn process_command(&mut self) { + // Go to the next line, below the prompt + writeln!(self.context).unwrap(); + if let Ok(command_line) = core::str::from_utf8(&self.buffer[0..self.used]) { + // We have a valid string + 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 [ ]", + 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 { + // Hmm .. we did not have a valid string + writeln!(self.context, "Input was not valid UTF-8").unwrap(); + } + } + + fn print_short_help(&mut self, item: &Item) { + 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(); + } + } + } + } + } + ItemType::Menu(_menu) => { + write!(self.context, " {}", item.command).unwrap(); + } + ItemType::_Dummy => { + write!(self.context, " {}", item.command).unwrap(); + } + } + if let Some(help) = item.help { + let mut help_line_iter = help.split('\n'); + writeln!(self.context, " - {}", help_line_iter.next().unwrap()).unwrap(); + } + } + + fn print_long_help(&mut self, item: &Item) { + 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(); + for param in parameters.iter() { + match param { + Parameter::Mandatory { + parameter_name, + help, + } => { + writeln!( + self.context, + " <{0}>\n - {1}", + parameter_name, + help.unwrap_or(""), + ) + .unwrap(); + } + Parameter::Optional { + parameter_name, + help, + } => { + writeln!( + self.context, + " <{0}>\n - {1}", + parameter_name, + help.unwrap_or("No help text found"), + ) + .unwrap(); + } + Parameter::Named { + parameter_name, + help, + } => { + writeln!( + self.context, + " --{0}\n - {1}", + parameter_name, + help.unwrap_or("No help text found"), + ) + .unwrap(); + } + Parameter::NamedValue { + parameter_name, + argument_name, + help, + } => { + writeln!( + self.context, + " --{0}={1}\n - {2}", + parameter_name, + argument_name, + help.unwrap_or("No help text found"), + ) + .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, + parameters: &[Parameter], + parent_menu: &Menu, + item: &Item, + 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() { + // Maybe arguments + 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("--") { + // Validate named argument + 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; + } + } + } + } + _ => { + // Ignore + } + } + } + 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 { + // Definitely no arguments + 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, _item: &Item, _args: &[&str], _context: &mut u32) {} + #[test] - fn it_works() { - assert_eq!(2 + 2, 4); + 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")) + ); + // Not an argument + 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")) + ); + // Not an argument + assert_eq!(argument_finder(&item, &["a", "b", "c"], "quux"), Err(())); + // Missing optional + 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("")) + ); + // Not an argument + assert_eq!( + argument_finder(&item, &["a", "--bar", "--baz"], "quux"), + Err(()) + ); + // Missing named + 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("")) + ); + // No argument so mark as not found + assert_eq!( + argument_finder(&item, &["a", "--bar", "--baz"], "baz"), + Ok(None) + ); + // Empty argument + assert_eq!( + argument_finder(&item, &["a", "--bar", "--baz="], "baz"), + Ok(Some("")) + ); + // Short argument + assert_eq!( + argument_finder(&item, &["a", "--bar", "--baz=1"], "baz"), + Ok(Some("1")) + ); + // Long argument + assert_eq!( + argument_finder( + &item, + &["a", "--bar", "--baz=abcdefghijklmnopqrstuvwxyz"], + "baz" + ), + Ok(Some("abcdefghijklmnopqrstuvwxyz")) + ); + // Not an argument + assert_eq!( + argument_finder(&item, &["a", "--bar", "--baz"], "quux"), + Err(()) + ); + // Missing named + assert_eq!(argument_finder(&item, &["a"], "baz"), Ok(None)); } }