diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 2c514df..793e98a 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -18,10 +18,18 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Build + - name: Build (default features) run: cargo build - - name: Run Tests + - name: Build (no features) + run: cargo build --no-default-features + - name: Build (all features) + run: cargo build --all-features + - name: Run Tests (default features) run: cargo test + - name: Run Tests (no features) + run: cargo test --no-default-features + - name: Run Tests (all features) + run: cargo test --all-features clippy: runs-on: ubuntu-latest diff --git a/Cargo.toml b/Cargo.toml index 1e691e9..3408093 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,11 +9,15 @@ repository = "https://github.com/rust-embedded-community/menu" readme = "README.md" [dependencies] - +embedded-io = "0.6.1" +noline = { version = "0.5.0", optional = true } [features] default = ["echo"] echo = [] [dev-dependencies] +noline = { version = "0.5.0", features = ["std"] } pancurses = "0.16" +termion = "4.0.2" +menu = {path = ".", features = ["noline"]} diff --git a/examples/noline.rs b/examples/noline.rs new file mode 100644 index 0000000..2103eba --- /dev/null +++ b/examples/noline.rs @@ -0,0 +1,241 @@ +extern crate menu; + +use embedded_io::{ErrorType, Read as EmbRead, Write as EmbWrite}; +use menu::*; +use noline::builder::EditorBuilder; +use std::io::{self, Read as _, Stdin, Stdout, Write as _}; +use termion::raw::IntoRawMode; + +pub struct IOWrapper { + stdin: Stdin, + stdout: Stdout, +} + +impl IOWrapper { + pub fn new() -> Self { + Self { + stdin: std::io::stdin(), + stdout: std::io::stdout(), + } + } +} + +impl Default for IOWrapper { + fn default() -> Self { + Self::new() + } +} + +impl ErrorType for IOWrapper { + type Error = embedded_io::ErrorKind; +} + +impl EmbRead for IOWrapper { + fn read(&mut self, buf: &mut [u8]) -> Result { + Ok(self.stdin.read(buf).map_err(|e| e.kind())?) + } +} + +impl EmbWrite for IOWrapper { + fn write(&mut self, buf: &[u8]) -> Result { + let mut written = 0; + let parts = buf.split(|b| *b == b'\n').collect::>(); + + for (i, part) in parts.iter().enumerate() { + written += self.stdout.write(part).map_err(|e| e.kind())?; + + if i != parts.len() - 1 { + let _ = self.stdout.write(b"\r\n").map_err(|e| e.kind())?; + written += 1; + } + } + + Ok(written) + } + + fn flush(&mut self) -> Result<(), Self::Error> { + Ok(self.stdout.flush().map_err(|e| e.kind())?) + } +} + +#[derive(Default)] +struct Context { + _inner: u32, +} + +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), +}; + +fn main() { + let _stdout = io::stdout().into_raw_mode().unwrap(); + + let mut io = IOWrapper::new(); + let mut editor = EditorBuilder::new_unbounded() + .with_unbounded_history() + .build_sync(&mut io) + .unwrap(); + + let mut context = Context::default(); + let mut r = Runner::new(ROOT_MENU, &mut editor, io, &mut context); + + while let Ok(_) = r.input_line(&mut context) {} +} + +fn enter_root(_menu: &Menu, interface: &mut IOWrapper, _context: &mut Context) { + writeln!(interface, "In enter_root").unwrap(); +} + +fn exit_root(_menu: &Menu, interface: &mut IOWrapper, _context: &mut Context) { + writeln!(interface, "In exit_root").unwrap(); +} + +fn select_foo( + _menu: &Menu, + item: &Item, + args: &[&str], + interface: &mut IOWrapper, + _context: &mut Context, +) { + writeln!(interface, "In select_foo. Args = {:?}", args).unwrap(); + writeln!( + interface, + "a = {:?}", + ::menu::argument_finder(item, args, "a") + ) + .unwrap(); + writeln!( + interface, + "b = {:?}", + ::menu::argument_finder(item, args, "b") + ) + .unwrap(); + writeln!( + interface, + "verbose = {:?}", + ::menu::argument_finder(item, args, "verbose") + ) + .unwrap(); + writeln!( + interface, + "level = {:?}", + ::menu::argument_finder(item, args, "level") + ) + .unwrap(); + writeln!( + interface, + "no_such_arg = {:?}", + ::menu::argument_finder(item, args, "no_such_arg") + ) + .unwrap(); +} + +fn select_bar( + _menu: &Menu, + _item: &Item, + args: &[&str], + interface: &mut IOWrapper, + _context: &mut Context, +) { + writeln!(interface, "In select_bar. Args = {:?}", args).unwrap(); +} + +fn enter_sub(_menu: &Menu, interface: &mut IOWrapper, _context: &mut Context) { + writeln!(interface, "In enter_sub").unwrap(); +} + +fn exit_sub(_menu: &Menu, interface: &mut IOWrapper, _context: &mut Context) { + writeln!(interface, "In exit_sub").unwrap(); +} + +fn select_baz( + _menu: &Menu, + _item: &Item, + args: &[&str], + interface: &mut IOWrapper, + _context: &mut Context, +) { + writeln!(interface, "In select_baz: Args = {:?}", args).unwrap(); +} + +fn select_quux( + _menu: &Menu, + _item: &Item, + args: &[&str], + interface: &mut IOWrapper, + _context: &mut Context, +) { + writeln!(interface, "In select_quux: Args = {:?}", args).unwrap(); +} diff --git a/examples/simple.rs b/examples/simple.rs index f825adc..ed23902 100644 --- a/examples/simple.rs +++ b/examples/simple.rs @@ -1,8 +1,9 @@ extern crate menu; +use embedded_io::Write; use menu::*; use pancurses::{endwin, initscr, noecho, Input}; -use std::fmt::Write; +use std::convert::Infallible; #[derive(Default)] struct Context { @@ -87,9 +88,17 @@ It contains multiple paragraphs and should be preceeded by the parameter list. struct Output(pancurses::Window); -impl std::fmt::Write for Output { - fn write_str(&mut self, s: &str) -> Result<(), std::fmt::Error> { - self.0.printw(s); +impl embedded_io::ErrorType for Output { + type Error = Infallible; +} + +impl Write for Output { + fn write(&mut self, buf: &[u8]) -> Result { + self.0.printw(core::str::from_utf8(buf).unwrap()); + Ok(buf.len()) + } + + fn flush(&mut self) -> Result<(), Self::Error> { Ok(()) } } diff --git a/src/lib.rs b/src/lib.rs index d26502e..13d69e0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,6 +4,11 @@ //! zero heap allocation. #![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; /// The type of function we call when we enter/exit a menu. @@ -102,14 +107,15 @@ where /// 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, I, T> -where - I: core::fmt::Write, -{ - buffer: &'a mut [u8], +pub struct Runner<'a, I, T, B: ?Sized> { + buffer: &'a mut B, used: usize, - menu_mgr: menu_manager::MenuManager<'a, I, T>, pub interface: I, + inner: InnerRunner<'a, I, T>, +} + +struct InnerRunner<'a, I, T> { + menu_mgr: menu_manager::MenuManager<'a, I, T>, } /// Describes the ways in which the API can fail @@ -247,50 +253,129 @@ impl<'a, I, T> core::clone::Clone for Menu<'a, I, T> { } } -impl<'a, I, T> Runner<'a, I, T> +#[derive(Clone)] +enum PromptIterState { + Newline, + Menu(usize), + Arrow, + Done, +} + +struct PromptIter<'a, I, T> { + menu_mgr: &'a MenuManager<'a, I, T>, + state: PromptIterState, +} + +impl<'a, I, T> Clone for PromptIter<'a, 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 { + // Skip the first menu level which is root + PromptIterState::Menu(1) + } +} + +impl<'a, I, T> Iterator for PromptIter<'a, I, T> { + type Item = &'a str; + + fn next(&mut self) -> Option { + 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: core::fmt::Write, + I: embedded_io::Write, { /// 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: Menu<'a, I, T>, - buffer: &'a mut [u8], - mut interface: I, - context: &mut T, - ) -> Self { + 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 { - menu_mgr: menu_manager::MenuManager::new(menu), buffer, used: 0, interface, + inner: InnerRunner { + menu_mgr: menu_manager::MenuManager::new(menu), + }, }; - r.prompt(true); + r.inner.prompt(&mut r.interface, true); r } +} - /// Print out a new command prompt, including sub-menu names if - /// applicable. - pub fn prompt(&mut self, newline: bool) { - if newline { - writeln!(self.interface).unwrap(); - } - for i in 0..self.menu_mgr.depth() { - if i > 1 { - write!(self.interface, "/").unwrap(); - } +#[cfg(feature = "noline")] +impl<'a, I, T, B, H> Runner<'a, I, T, Editor> +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)?; - let menu = self.menu_mgr.get_menu(Some(i)); - write!(self.interface, "/{}", menu.label).unwrap(); + #[cfg(not(feature = "echo"))] + { + // Echo the command + write!(self.interface, "\r").unwrap(); + write!(self.interface, "{}", line).unwrap(); } - write!(self.interface, "> ").unwrap(); + + self.inner + .process_command(&mut self.interface, context, line); + + Ok(()) } +} +impl<'a, I, T, B> Runner<'a, I, T, B> +where + I: embedded_io::Write, + B: AsMut<[u8]>, +{ /// 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. @@ -300,17 +385,24 @@ where if input == 0x0A { return; } + let buffer = self.buffer.as_mut(); + let outcome = if input == 0x0D { - #[cfg(not(feature = "echo"))] - { - // Echo the command - write!(self.interface, "\r").unwrap(); - if let Ok(s) = core::str::from_utf8(&self.buffer[0..self.used]) { - write!(self.interface, "{}", s).unwrap(); + if let Ok(line) = core::str::from_utf8(&buffer[0..self.used]) { + #[cfg(not(feature = "echo"))] + { + // Echo the command + write!(self.interface, "\r").unwrap(); + write!(self.interface, "{}", line).unwrap(); } + // Handle the command + self.inner + .process_command(&mut self.interface, context, line); + } else { + // Hmm .. we did not have a valid string + writeln!(self.interface, "Input was not valid UTF-8").unwrap(); } - // Handle the command - self.process_command(context); + Outcome::CommandProcessed } else if (input == 0x08) || (input == 0x7F) { // Handling backspace or delete @@ -319,8 +411,8 @@ where self.used -= 1; } Outcome::NeedMore - } else if self.used < self.buffer.len() { - self.buffer[self.used] = input; + } else if self.used < buffer.len() { + buffer[self.used] = input; self.used += 1; #[cfg(feature = "echo")] @@ -329,14 +421,14 @@ where // 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(); + let valid = core::str::from_utf8(&buffer[0..self.used]).is_ok(); // Now we've released the buffer, we can draw the prompt if valid { write!(self.interface, "\r").unwrap(); - self.prompt(false); + self.inner.prompt(&mut self.interface, false); } // Grab the buffer again to render it to the screen - if let Ok(s) = core::str::from_utf8(&self.buffer[0..self.used]) { + if let Ok(s) = core::str::from_utf8(&buffer[0..self.used]) { write!(self.interface, "{}", s).unwrap(); } } @@ -348,113 +440,128 @@ where match outcome { Outcome::CommandProcessed => { self.used = 0; - self.prompt(true); + self.inner.prompt(&mut self.interface, true); } Outcome::NeedMore => {} } } +} + +impl<'a, I, T> InnerRunner<'a, I, T> +where + I: embedded_io::Write, +{ + /// Print out a new command prompt, including sub-menu names if + /// applicable. + 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(); + } + } /// Scan the buffer and do the right thing based on its contents. - fn process_command(&mut self, context: &mut T) { + fn process_command(&mut self, interface: &mut I, context: &mut T, command_line: &str) { // Go to the next line, below the prompt - writeln!(self.interface).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.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(item); - } - None => { - writeln!(self.interface, "I can't help with {:?}", arg).unwrap(); - } - }, - _ => { - writeln!(self.interface, "AVAILABLE ITEMS:").unwrap(); - for item in menu.items { - self.print_short_help(item); - } - if self.menu_mgr.depth() != 0 { - self.print_short_help(&Item { + writeln!(interface).unwrap(); + // We have a valid string + 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(&Item { + }, + ); + } + self.print_short_help( + interface, + &Item { command: "help [ ]", 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, &mut self.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( - &mut self.interface, - context, - function, - parameters, - menu, - item, - command_line, - ), - ItemType::Menu(incoming_menu) => { - if let Some(cb_fn) = incoming_menu.entry { - cb_fn(incoming_menu, &mut self.interface, context); - } - self.menu_mgr.push_menu(i); - } - ItemType::_Dummy => { - unreachable!(); + } + } 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!(self.interface, "Command {:?} not found. Try 'help'.", cmd) - .unwrap(); + found = true; + break; } } - } else { - writeln!(self.interface, "Input was empty?").unwrap(); + if !found { + writeln!(interface, "Command {:?} not found. Try 'help'.", cmd).unwrap(); + } } } else { - // Hmm .. we did not have a valid string - writeln!(self.interface, "Input was not valid UTF-8").unwrap(); + writeln!(interface, "Input was empty?").unwrap(); } } - fn print_short_help(&mut self, item: &Item) { + fn print_short_help(&mut self, interface: &mut I, item: &Item) { let mut has_options = false; match item.item_type { ItemType::Callback { parameters, .. } => { - write!(self.interface, " {}", item.command).unwrap(); + write!(interface, " {}", item.command).unwrap(); if !parameters.is_empty() { for param in parameters.iter() { match param { Parameter::Mandatory { parameter_name, .. } => { - write!(self.interface, " <{}>", parameter_name).unwrap(); + write!(interface, " <{}>", parameter_name).unwrap(); } Parameter::Optional { parameter_name, .. } => { - write!(self.interface, " [ <{}> ]", parameter_name).unwrap(); + write!(interface, " [ <{}> ]", parameter_name).unwrap(); } Parameter::Named { .. } => { has_options = true; @@ -467,50 +574,46 @@ where } } ItemType::Menu(_menu) => { - write!(self.interface, " {}", item.command).unwrap(); + write!(interface, " {}", item.command).unwrap(); } ItemType::_Dummy => { - write!(self.interface, " {}", item.command).unwrap(); + write!(interface, " {}", item.command).unwrap(); } } if has_options { - write!(self.interface, " [OPTIONS...]").unwrap(); + write!(interface, " [OPTIONS...]").unwrap(); } - writeln!(self.interface).unwrap(); + writeln!(interface).unwrap(); } - fn print_long_help(&mut self, item: &Item) { - writeln!(self.interface, "SUMMARY:").unwrap(); + fn print_long_help(&mut self, interface: &mut I, item: &Item) { + writeln!(interface, "SUMMARY:").unwrap(); match item.item_type { ItemType::Callback { parameters, .. } => { - write!(self.interface, " {}", item.command).unwrap(); + write!(interface, " {}", item.command).unwrap(); if !parameters.is_empty() { for param in parameters.iter() { match param { Parameter::Mandatory { parameter_name, .. } => { - write!(self.interface, " <{}>", parameter_name).unwrap(); + write!(interface, " <{}>", parameter_name).unwrap(); } Parameter::Optional { parameter_name, .. } => { - write!(self.interface, " [ <{}> ]", parameter_name).unwrap(); + write!(interface, " [ <{}> ]", parameter_name).unwrap(); } Parameter::Named { parameter_name, .. } => { - write!(self.interface, " [ --{} ]", parameter_name).unwrap(); + write!(interface, " [ --{} ]", parameter_name).unwrap(); } Parameter::NamedValue { parameter_name, argument_name, .. } => { - write!( - self.interface, - " [ --{}={} ]", - parameter_name, argument_name - ) - .unwrap(); + write!(interface, " [ --{}={} ]", parameter_name, argument_name) + .unwrap(); } } } - writeln!(self.interface, "\n\nPARAMETERS:").unwrap(); + writeln!(interface, "\n\nPARAMETERS:").unwrap(); let default_help = "Undocumented option"; for param in parameters.iter() { match param { @@ -519,7 +622,7 @@ where help, } => { writeln!( - self.interface, + interface, " <{0}>\n {1}\n", parameter_name, help.unwrap_or(default_help), @@ -531,7 +634,7 @@ where help, } => { writeln!( - self.interface, + interface, " <{0}>\n {1}\n", parameter_name, help.unwrap_or(default_help), @@ -543,7 +646,7 @@ where help, } => { writeln!( - self.interface, + interface, " --{0}\n {1}\n", parameter_name, help.unwrap_or(default_help), @@ -556,7 +659,7 @@ where help, } => { writeln!( - self.interface, + interface, " --{0}={1}\n {2}\n", parameter_name, argument_name, @@ -569,14 +672,14 @@ where } } ItemType::Menu(_menu) => { - write!(self.interface, " {}", item.command).unwrap(); + write!(interface, " {}", item.command).unwrap(); } ItemType::_Dummy => { - write!(self.interface, " {}", item.command).unwrap(); + write!(interface, " {}", item.command).unwrap(); } } if let Some(help) = item.help { - writeln!(self.interface, "\n\nDESCRIPTION:\n{}", help).unwrap(); + writeln!(interface, "\n\nDESCRIPTION:\n{}", help).unwrap(); } }