#![no_std]
use menu_manager::MenuManager;
#[cfg(feature = "noline")]
use noline::{error::NolineError, history::History, line_buffer::Buffer, sync_editor::Editor};
pub mod menu_manager;
pub type MenuCallbackFn<I, T> = fn(menu: &Menu<I, T>, interface: &mut I, context: &mut T);
pub type ItemCallbackFn<I, T> =
fn(menu: &Menu<I, T>, item: &Item<I, T>, args: &[&str], interface: &mut I, 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, I, T>
where
T: 'a,
{
Callback {
function: ItemCallbackFn<I, T>,
parameters: &'a [Parameter<'a>],
},
Menu(&'a Menu<'a, I, T>),
_Dummy,
}
pub struct Item<'a, I, T>
where
T: 'a,
{
pub command: &'a str,
pub help: Option<&'a str>,
pub item_type: ItemType<'a, I, T>,
}
pub struct Menu<'a, I, T>
where
T: 'a,
{
pub label: &'a str,
pub items: &'a [&'a Item<'a, I, T>],
pub entry: Option<MenuCallbackFn<I, T>>,
pub exit: Option<MenuCallbackFn<I, T>>,
}
pub struct Runner<'a, I, T, B: ?Sized> {
buffer: &'a mut B,
used: usize,
pub interface: I,
inner: InnerRunner<'a, I, T>,
}
struct InnerRunner<'a, I, T> {
menu_mgr: menu_manager::MenuManager<'a, I, T>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Error {
NotACallbackItem,
NotFound,
}
impl core::fmt::Display for Error {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
core::fmt::Debug::fmt(self, f)
}
}
#[rustversion::since(1.81)]
impl core::error::Error for Error {}
pub fn argument_finder<'a, I, T>(
item: &'a Item<'a, I, T>,
argument_list: &'a [&'a str],
name_to_find: &'a str,
) -> Result<Option<&'a str>, Error> {
let ItemType::Callback { parameters, .. } = item.item_type else {
return Err(Error::NotACallbackItem);
};
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(Error::NotFound),
}
}
enum Outcome {
CommandProcessed,
NeedMore,
}
impl<'a, I, T> core::clone::Clone for Menu<'a, I, T> {
fn clone(&self) -> Menu<'a, I, T> {
Menu {
label: self.label,
items: self.items,
entry: self.entry,
exit: self.exit,
}
}
}
#[derive(Clone)]
enum PromptIterState {
Newline,
Menu(usize),
Arrow,
Done,
}
struct PromptIter<'a, I, T> {
menu_mgr: &'a MenuManager<'a, I, T>,
state: PromptIterState,
}
impl<I, T> Clone for PromptIter<'_, I, T> {
fn clone(&self) -> Self {
Self {
menu_mgr: self.menu_mgr,
state: self.state.clone(),
}
}
}
impl<'a, I, T> PromptIter<'a, I, T> {
fn new(menu_mgr: &'a MenuManager<'a, I, T>, newline: bool) -> Self {
let state = if newline {
PromptIterState::Newline
} else {
Self::first_menu()
};
Self { menu_mgr, state }
}
const fn first_menu() -> PromptIterState {
PromptIterState::Menu(1)
}
}
impl<'a, I, T> Iterator for PromptIter<'a, I, T> {
type Item = &'a str;
fn next(&mut self) -> Option<Self::Item> {
loop {
match self.state {
PromptIterState::Newline => {
self.state = Self::first_menu();
break Some("\n");
}
PromptIterState::Menu(i) => {
if i > self.menu_mgr.depth() {
self.state = PromptIterState::Arrow;
} else {
let menu = self.menu_mgr.get_menu(Some(i));
self.state = PromptIterState::Menu(i + 1);
break Some(menu.label);
}
}
PromptIterState::Arrow => {
self.state = PromptIterState::Done;
break Some("> ");
}
PromptIterState::Done => break None,
}
}
}
}
impl<'a, I, T, B: ?Sized> Runner<'a, I, T, B>
where
I: embedded_io::Write,
{
pub fn new(menu: Menu<'a, I, T>, buffer: &'a mut B, mut interface: I, context: &mut T) -> Self {
if let Some(cb_fn) = menu.entry {
cb_fn(&menu, &mut interface, context);
}
let mut r = Runner {
buffer,
used: 0,
interface,
inner: InnerRunner {
menu_mgr: menu_manager::MenuManager::new(menu),
},
};
r.inner.prompt(&mut r.interface, true);
r
}
}
#[cfg(feature = "noline")]
impl<'a, I, T, B, H> Runner<'a, I, T, Editor<B, H>>
where
B: Buffer,
H: History,
I: embedded_io::Read + embedded_io::Write,
{
pub fn input_line(&mut self, context: &mut T) -> Result<(), NolineError> {
let prompt = PromptIter::new(&self.inner.menu_mgr, false);
let line = self.buffer.readline(prompt, &mut self.interface)?;
#[cfg(not(feature = "echo"))]
{
write!(self.interface, "\r").unwrap();
write!(self.interface, "{}", line).unwrap();
}
self.inner
.process_command(&mut self.interface, context, line);
Ok(())
}
}
impl<I, T, B> Runner<'_, I, T, B>
where
I: embedded_io::Write,
B: AsMut<[u8]> + ?Sized,
{
pub fn input_byte(&mut self, input: u8, context: &mut T) {
if input == 0x0A {
return;
}
let buffer = self.buffer.as_mut();
let outcome = if input == 0x0D {
if let Ok(line) = core::str::from_utf8(&buffer[0..self.used]) {
#[cfg(not(feature = "echo"))]
{
write!(self.interface, "\r").unwrap();
write!(self.interface, "{}", line).unwrap();
}
self.inner
.process_command(&mut self.interface, context, line);
} else {
writeln!(self.interface, "Input was not valid UTF-8").unwrap();
}
Outcome::CommandProcessed
} else if (input == 0x08) || (input == 0x7F) {
if self.used > 0 {
write!(self.interface, "\u{0008} \u{0008}").unwrap();
self.used -= 1;
}
Outcome::NeedMore
} else if self.used < buffer.len() {
buffer[self.used] = input;
self.used += 1;
#[cfg(feature = "echo")]
{
let valid = core::str::from_utf8(&buffer[0..self.used]).is_ok();
if valid {
write!(self.interface, "\r").unwrap();
self.inner.prompt(&mut self.interface, false);
}
if let Ok(s) = core::str::from_utf8(&buffer[0..self.used]) {
write!(self.interface, "{}", s).unwrap();
}
}
Outcome::NeedMore
} else {
writeln!(self.interface, "Buffer overflow!").unwrap();
Outcome::NeedMore
};
match outcome {
Outcome::CommandProcessed => {
self.used = 0;
self.inner.prompt(&mut self.interface, true);
}
Outcome::NeedMore => {}
}
}
}
impl<I, T> InnerRunner<'_, I, T>
where
I: embedded_io::Write,
{
pub fn prompt(&mut self, interface: &mut I, newline: bool) {
let prompt = PromptIter::new(&self.menu_mgr, newline);
for part in prompt {
write!(interface, "{}", part).unwrap();
}
}
fn process_command(&mut self, interface: &mut I, context: &mut T, command_line: &str) {
writeln!(interface).unwrap();
let mut parts = command_line.split_whitespace();
if let Some(cmd) = parts.next() {
let menu = self.menu_mgr.get_menu(None);
if cmd == "help" {
match parts.next() {
Some(arg) => match menu.items.iter().find(|i| i.command == arg) {
Some(item) => {
self.print_long_help(interface, item);
}
None => {
writeln!(interface, "I can't help with {:?}", arg).unwrap();
}
},
_ => {
writeln!(interface, "AVAILABLE ITEMS:").unwrap();
for item in menu.items {
self.print_short_help(interface, item);
}
if self.menu_mgr.depth() != 0 {
self.print_short_help(
interface,
&Item {
command: "exit",
help: Some("Leave this menu."),
item_type: ItemType::_Dummy,
},
);
}
self.print_short_help(
interface,
&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.menu_mgr.depth() != 0 {
if let Some(cb_fn) = menu.exit {
cb_fn(menu, interface, context);
}
self.menu_mgr.pop_menu();
} else {
let mut found = false;
for (i, item) in menu.items.iter().enumerate() {
if cmd == item.command {
match item.item_type {
ItemType::Callback {
function,
parameters,
} => Self::call_function(
interface,
context,
function,
parameters,
menu,
item,
command_line,
),
ItemType::Menu(incoming_menu) => {
if let Some(cb_fn) = incoming_menu.entry {
cb_fn(incoming_menu, interface, context);
}
self.menu_mgr.push_menu(i);
}
ItemType::_Dummy => {
unreachable!();
}
}
found = true;
break;
}
}
if !found {
writeln!(interface, "Command {:?} not found. Try 'help'.", cmd).unwrap();
}
}
} else {
writeln!(interface, "Input was empty?").unwrap();
}
}
fn print_short_help(&mut self, interface: &mut I, item: &Item<I, T>) {
let mut has_options = false;
match item.item_type {
ItemType::Callback { parameters, .. } => {
write!(interface, " {}", item.command).unwrap();
if !parameters.is_empty() {
for param in parameters.iter() {
match param {
Parameter::Mandatory { parameter_name, .. } => {
write!(interface, " <{}>", parameter_name).unwrap();
}
Parameter::Optional { parameter_name, .. } => {
write!(interface, " [ <{}> ]", parameter_name).unwrap();
}
Parameter::Named { .. } => {
has_options = true;
}
Parameter::NamedValue { .. } => {
has_options = true;
}
}
}
}
}
ItemType::Menu(_menu) => {
write!(interface, " {}", item.command).unwrap();
}
ItemType::_Dummy => {
write!(interface, " {}", item.command).unwrap();
}
}
if has_options {
write!(interface, " [OPTIONS...]").unwrap();
}
writeln!(interface).unwrap();
}
fn print_long_help(&mut self, interface: &mut I, item: &Item<I, T>) {
writeln!(interface, "SUMMARY:").unwrap();
match item.item_type {
ItemType::Callback { parameters, .. } => {
write!(interface, " {}", item.command).unwrap();
if !parameters.is_empty() {
for param in parameters.iter() {
match param {
Parameter::Mandatory { parameter_name, .. } => {
write!(interface, " <{}>", parameter_name).unwrap();
}
Parameter::Optional { parameter_name, .. } => {
write!(interface, " [ <{}> ]", parameter_name).unwrap();
}
Parameter::Named { parameter_name, .. } => {
write!(interface, " [ --{} ]", parameter_name).unwrap();
}
Parameter::NamedValue {
parameter_name,
argument_name,
..
} => {
write!(interface, " [ --{}={} ]", parameter_name, argument_name)
.unwrap();
}
}
}
writeln!(interface, "\n\nPARAMETERS:").unwrap();
let default_help = "Undocumented option";
for param in parameters.iter() {
match param {
Parameter::Mandatory {
parameter_name,
help,
} => {
writeln!(
interface,
" <{0}>\n {1}\n",
parameter_name,
help.unwrap_or(default_help),
)
.unwrap();
}
Parameter::Optional {
parameter_name,
help,
} => {
writeln!(
interface,
" <{0}>\n {1}\n",
parameter_name,
help.unwrap_or(default_help),
)
.unwrap();
}
Parameter::Named {
parameter_name,
help,
} => {
writeln!(
interface,
" --{0}\n {1}\n",
parameter_name,
help.unwrap_or(default_help),
)
.unwrap();
}
Parameter::NamedValue {
parameter_name,
argument_name,
help,
} => {
writeln!(
interface,
" --{0}={1}\n {2}\n",
parameter_name,
argument_name,
help.unwrap_or(default_help),
)
.unwrap();
}
}
}
}
}
ItemType::Menu(_menu) => {
write!(interface, " {}", item.command).unwrap();
}
ItemType::_Dummy => {
write!(interface, " {}", item.command).unwrap();
}
}
if let Some(help) = item.help {
writeln!(interface, "\n\nDESCRIPTION:\n{}", help).unwrap();
}
}
fn call_function(
interface: &mut I,
context: &mut T,
callback_function: ItemCallbackFn<I, T>,
parameters: &[Parameter],
parent_menu: &Menu<I, T>,
item: &Item<I, T>,
command: &str,
) {
let mandatory_parameter_count = parameters
.iter()
.filter(|p| matches!(p, Parameter::Mandatory { .. }))
.count();
let positional_parameter_count = parameters
.iter()
.filter(|p| matches!(p, Parameter::Mandatory { .. } | Parameter::Optional { .. }))
.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 let Some(tail) = arg.strip_prefix("--") {
let mut found = false;
for param in parameters.iter() {
match param {
Parameter::Named { parameter_name, .. } => {
if tail == *parameter_name {
found = true;
break;
}
}
Parameter::NamedValue { parameter_name, .. } => {
if arg.contains('=') {
if let Some(given_name) = tail.split('=').next() {
if given_name == *parameter_name {
found = true;
break;
}
}
}
}
_ => {
}
}
}
if !found {
writeln!(interface, "Error: Did not understand {:?}", arg).unwrap();
return;
}
} else {
positional_arguments += 1;
}
}
if positional_arguments < mandatory_parameter_count {
writeln!(interface, "Error: Insufficient arguments given").unwrap();
} else if positional_arguments > positional_parameter_count {
writeln!(interface, "Error: Too many arguments given").unwrap();
} else {
callback_function(
parent_menu,
item,
&argument_buffer[0..argument_count],
interface,
context,
);
}
} else {
if mandatory_parameter_count == 0 {
callback_function(parent_menu, item, &[], interface, context);
} else {
writeln!(interface, "Error: Insufficient arguments given").unwrap();
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn dummy(
_menu: &Menu<(), u32>,
_item: &Item<(), u32>,
_args: &[&str],
_interface: &mut (),
_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(Error::NotFound)
);
}
#[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(Error::NotFound)
);
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(Error::NotFound)
);
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(Error::NotFound)
);
assert_eq!(argument_finder(&item, &["a"], "baz"), Ok(None));
}
}