diff --git a/Cargo.toml b/Cargo.toml index 24acfa9..6e6c767 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,3 +18,4 @@ echo = [] [dev-dependencies] pancurses = "0.16" +tokio = { version = "1", features = ["full"] } diff --git a/examples/simple.rs b/examples/simple.rs index e450108..2f4e991 100644 --- a/examples/simple.rs +++ b/examples/simple.rs @@ -1,3 +1,5 @@ +#![feature(async_fn_in_trait)] + extern crate menu; use menu::*; @@ -9,7 +11,7 @@ const ROOT_MENU: Menu = Menu { items: &[ &Item { item_type: ItemType::Callback { - function: select_foo, + handler: &BoxedHandler(FooItemHandler), parameters: &[ Parameter::Mandatory { parameter_name: "a", @@ -42,7 +44,7 @@ It contains multiple paragraphs and should be preceeded by the parameter list. }, &Item { item_type: ItemType::Callback { - function: select_bar, + handler: &BoxedHandler(BarHandler), parameters: &[], }, command: "bar", @@ -54,7 +56,7 @@ It contains multiple paragraphs and should be preceeded by the parameter list. items: &[ &Item { item_type: ItemType::Callback { - function: select_baz, + handler: &BoxedHandler(BazHandler), parameters: &[], }, command: "baz", @@ -62,26 +64,34 @@ It contains multiple paragraphs and should be preceeded by the parameter list. }, &Item { item_type: ItemType::Callback { - function: select_quux, + handler: &BoxedHandler(QuuxHandler), parameters: &[], }, command: "quux", help: Some("maximum quux"), }, ], - entry: Some(enter_sub), - exit: Some(exit_sub), + handler: Some(&BoxedHandler(SubMenuHandler)), }), command: "sub", help: Some("enter sub-menu"), }, ], - entry: Some(enter_root), - exit: Some(exit_root), + handler: Some(&BoxedHandler(RootMenuHandler)), }; struct Output(pancurses::Window); +impl Output { + async fn wait_for_q(&self) { + loop { + if let Some(Input::Character('q')) = self.0.getch() { + break; + } + } + } +} + impl std::fmt::Write for Output { fn write_str(&mut self, s: &str) -> Result<(), std::fmt::Error> { self.0.printw(s); @@ -89,8 +99,10 @@ impl std::fmt::Write for Output { } } -fn main() { +#[tokio::main] +async fn main() { let window = initscr(); + window.timeout(100); window.scrollok(true); noecho(); let mut buffer = [0u8; 64]; @@ -98,12 +110,12 @@ fn main() { loop { match r.context.0.getch() { Some(Input::Character('\n')) => { - r.input_byte(b'\r'); + r.input_byte(b'\r').await; } Some(Input::Character(c)) => { let mut buf = [0; 4]; for b in c.encode_utf8(&mut buf).bytes() { - r.input_byte(b); + r.input_byte(b).await; } } Some(Input::KeyDC) => break, @@ -116,69 +128,116 @@ fn main() { endwin(); } -fn enter_root(_menu: &Menu, context: &mut Output) { - writeln!(context, "In enter_root").unwrap(); -} +struct RootMenuHandler; -fn exit_root(_menu: &Menu, context: &mut Output) { - writeln!(context, "In exit_root").unwrap(); -} +impl MenuHandler for RootMenuHandler { + async fn entry(&self, _menu: &Menu<'_, Output>, context: &mut Output) { + writeln!(context, "In enter_root").unwrap(); + } -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(); + async fn exit(&self, _menu: &Menu<'_, Output>, context: &mut Output) { + writeln!(context, "In exit_root").unwrap(); + } } -fn select_bar<'a>(_menu: &Menu, _item: &Item, args: &[&str], context: &mut Output) { - writeln!(context, "In select_bar. Args = {:?}", args).unwrap(); +struct FooItemHandler; + +impl ItemHandler for FooItemHandler { + async fn handle( + &self, + _menu: &Menu<'_, Output>, + item: &Item<'_, Output>, + 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(); + + writeln!(context, "Press 'q' to exit this handler").unwrap(); + + context.wait_for_q().await; + } } -fn enter_sub(_menu: &Menu, context: &mut Output) { - writeln!(context, "In enter_sub").unwrap(); +struct BarHandler; + +impl ItemHandler for BarHandler { + async fn handle( + &self, + _menu: &Menu<'_, Output>, + _item: &Item<'_, Output>, + args: &[&str], + context: &mut Output, + ) { + writeln!(context, "In select_bar. Args = {:?}", args).unwrap(); + } } -fn exit_sub(_menu: &Menu, context: &mut Output) { - writeln!(context, "In exit_sub").unwrap(); +struct SubMenuHandler; + +impl MenuHandler for SubMenuHandler { + async fn entry(&self, _menu: &Menu<'_, Output>, context: &mut Output) { + writeln!(context, "In enter_sub").unwrap(); + } + + async fn exit(&self, _menu: &Menu<'_, Output>, context: &mut Output) { + writeln!(context, "In exit_sub").unwrap(); + } } -fn select_baz<'a>(_menu: &Menu, _item: &Item, args: &[&str], context: &mut Output) { - writeln!(context, "In select_baz: Args = {:?}", args).unwrap(); +struct BazHandler; + +impl ItemHandler for BazHandler { + async fn handle( + &self, + _menu: &Menu<'_, Output>, + _item: &Item<'_, Output>, + args: &[&str], + context: &mut Output, + ) { + writeln!(context, "In select_baz: Args = {:?}", args).unwrap(); + } } -fn select_quux<'a>( - _menu: &Menu, - _item: &Item, - args: &[&str], - context: &mut Output, -) { - writeln!(context, "In select_quux: Args = {:?}", args).unwrap(); +struct QuuxHandler; + +impl ItemHandler for QuuxHandler { + async fn handle( + &self, + _menu: &Menu<'_, Output>, + _item: &Item<'_, Output>, + args: &[&str], + context: &mut Output, + ) { + writeln!(context, "In select_quux: Args = {:?}", args).unwrap(); + } } diff --git a/rust-toolchain b/rust-toolchain new file mode 100644 index 0000000..bf867e0 --- /dev/null +++ b/rust-toolchain @@ -0,0 +1 @@ +nightly diff --git a/src/lib.rs b/src/lib.rs index 52d7bc2..2fd7478 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,12 +4,98 @@ //! zero heap allocation. #![no_std] #![deny(missing_docs)] +#![feature(async_fn_in_trait)] + +use core::{future::Future, pin::Pin}; + +extern crate alloc; + +use alloc::boxed::Box; + +/// The type of function we call when we enter/exit a menu. +pub trait MenuHandler { + /// A function to call when this menu is entered. If this is the root menu, this is called when the runner is created. + async fn entry(&self, menu: &Menu<'_, T>, context: &mut T); + + /// A function to call when this menu is exited. Never called for the root menu. + async fn exit(&self, menu: &Menu<'_, T>, context: &mut T) { + let _ = menu; + let _ = context; + } +} /// The type of function we call when we enter/exit a menu. -pub type MenuCallbackFn = fn(menu: &Menu, context: &mut T); +pub trait BoxedMenuHandler { + /// A function to call when this menu is entered. If this is the root menu, this is called when the runner is created. + fn entry<'a>( + &'a self, + menu: &'a Menu, + context: &'a mut T, + ) -> Pin + 'a>>; + + /// A function to call when this menu is exited. Never called for the root menu. + fn exit<'a>( + &'a self, + menu: &'a Menu, + context: &'a mut T, + ) -> Pin + 'a>>; +} + +/// The type of function we call when a valid command has been entered. +pub trait ItemHandler { + /// The function to call + async fn handle(&self, menu: &Menu<'_, T>, item: &Item<'_, T>, args: &[&str], context: &mut T); +} + +/// The type of function we call when a valid command has been entered. +pub trait BoxedItemHandler { + /// The function to call + fn handle<'a>( + &'a self, + menu: &'a Menu, + item: &'a Item, + args: &'a [&str], + context: &'a mut T, + ) -> Pin + 'a>>; +} + +/// Wrapper helper for transitioning an unboxed handler into a boxed one. +pub struct BoxedHandler(pub 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); +impl> BoxedMenuHandler for BoxedHandler { + // fn entry(&self, menu: &Menu, context: &mut T) -> Pin>> { + // Box::pin(self.0.entry(menu, context)) + // } + + fn entry<'a>( + &'a self, + menu: &'a Menu, + context: &'a mut T, + ) -> Pin + 'a>> { + let fut = self.0.entry(menu, context); + Box::pin(fut) + } + + fn exit<'a>( + &'a self, + menu: &'a Menu, + context: &'a mut T, + ) -> Pin + 'a>> { + Box::pin(self.0.exit(menu, context)) + } +} + +impl> BoxedItemHandler for BoxedHandler { + fn handle<'a>( + &'a self, + menu: &'a Menu, + item: &'a Item, + args: &'a [&str], + context: &'a mut T, + ) -> Pin + 'a>> { + Box::pin(self.0.handle(menu, item, args, context)) + } +} #[derive(Debug)] /// Describes a parameter to the command @@ -55,7 +141,7 @@ where /// Call a function when this command is entered Callback { /// The function to call - function: ItemCallbackFn, + handler: &'a dyn BoxedItemHandler, /// The list of parameters for this function. Pass an empty list if there aren't any. parameters: &'a [Parameter<'a>], }, @@ -91,10 +177,8 @@ where 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>, + /// The menu handler. + pub handler: Option<&'a dyn BoxedMenuHandler>, } /// This structure handles the menu. You feed it bytes as they are read from @@ -240,8 +324,8 @@ where /// `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, &mut context); + if let Some(handler) = menu.handler { + handler.entry(menu, &mut context); } let mut r = Runner { menus: [Some(menu), None, None, None], @@ -277,7 +361,7 @@ where /// carriage-return, the buffer is scanned and the appropriate action /// performed. /// By default, an echo feature is enabled to display commands on the terminal. - pub fn input_byte(&mut self, input: u8) { + pub async fn input_byte(&mut self, input: u8) { // Strip carriage returns if input == 0x0A { return; @@ -292,7 +376,7 @@ where } } // Handle the command - self.process_command(); + self.process_command().await; Outcome::CommandProcessed } else if (input == 0x08) || (input == 0x7F) { // Handling backspace or delete @@ -337,7 +421,7 @@ where } /// Scan the buffer and do the right thing based on its contents. - fn process_command(&mut self) { + async 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]) { @@ -375,6 +459,10 @@ where } } } else if cmd == "exit" && self.depth != 0 { + if let Some(handler) = self.menus[self.depth].as_ref().unwrap().handler { + handler.exit(menu, &mut self.context).await; + } + self.menus[self.depth] = None; self.depth -= 1; } else { @@ -383,19 +471,28 @@ where if cmd == item.command { match item.item_type { ItemType::Callback { - function, - parameters, - } => Self::call_function( - &mut self.context, - function, + handler, parameters, - menu, - item, - command_line, - ), + } => { + Self::call_function( + &mut self.context, + handler, + parameters, + menu, + item, + command_line, + ) + .await + } ItemType::Menu(m) => { self.depth += 1; self.menus[self.depth] = Some(m); + + if let Some(handler) = + self.menus[self.depth].as_ref().unwrap().handler + { + handler.entry(menu, &mut self.context).await; + } } ItemType::_Dummy => { unreachable!(); @@ -552,12 +649,12 @@ where } } - fn call_function( + async fn call_function<'h>( context: &mut T, - callback_function: ItemCallbackFn, - parameters: &[Parameter], - parent_menu: &Menu, - item: &Item, + callback_handler: &'h dyn BoxedItemHandler, + parameters: &[Parameter<'h>], + parent_menu: &Menu<'h, T>, + item: &Item<'h, T>, command: &str, ) { let mandatory_parameter_count = parameters @@ -625,17 +722,21 @@ where } 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, - ); + callback_handler + .handle( + parent_menu, + item, + &argument_buffer[0..argument_count], + context, + ) + .await; } } else { // Definitely no arguments if mandatory_parameter_count == 0 { - callback_function(parent_menu, item, &[], context); + callback_handler + .handle(parent_menu, item, &[], context) + .await; } else { writeln!(context, "Error: Insufficient arguments given").unwrap(); } @@ -647,7 +748,18 @@ where mod tests { use super::*; - fn dummy(_menu: &Menu, _item: &Item, _args: &[&str], _context: &mut u32) {} + struct Dummy; + + impl ItemHandler for Dummy { + async fn handle( + &self, + _menu: &Menu<'_, u32>, + _item: &Item<'_, u32>, + _args: &[&str], + _context: &mut u32, + ) { + } + } #[test] fn find_arg_mandatory() { @@ -655,7 +767,7 @@ mod tests { command: "dummy", help: None, item_type: ItemType::Callback { - function: dummy, + handler: &BoxedHandler(Dummy), parameters: &[ Parameter::Mandatory { parameter_name: "foo", @@ -694,7 +806,7 @@ mod tests { command: "dummy", help: None, item_type: ItemType::Callback { - function: dummy, + handler: &BoxedHandler(Dummy), parameters: &[ Parameter::Mandatory { parameter_name: "foo", @@ -735,7 +847,7 @@ mod tests { command: "dummy", help: None, item_type: ItemType::Callback { - function: dummy, + handler: &BoxedHandler(Dummy), parameters: &[ Parameter::Mandatory { parameter_name: "foo", @@ -779,7 +891,7 @@ mod tests { command: "dummy", help: None, item_type: ItemType::Callback { - function: dummy, + handler: &BoxedHandler(Dummy), parameters: &[ Parameter::Mandatory { parameter_name: "foo",