Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Async implementation #14

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -18,3 +18,4 @@ echo = []

[dev-dependencies]
pancurses = "0.16"
tokio = { version = "1", features = ["full"] }
187 changes: 123 additions & 64 deletions examples/simple.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#![feature(async_fn_in_trait)]

extern crate menu;

use menu::*;
@@ -9,7 +11,7 @@ const ROOT_MENU: Menu<Output> = 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,56 +56,66 @@ 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",
help: Some("thingamobob a baz"),
},
&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);
Ok(())
}
}

fn main() {
#[tokio::main]
async fn main() {
let window = initscr();
window.timeout(100);
window.scrollok(true);
noecho();
let mut buffer = [0u8; 64];
let mut r = Runner::new(&ROOT_MENU, &mut buffer, Output(window));
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<Output>, context: &mut Output) {
writeln!(context, "In enter_root").unwrap();
}
struct RootMenuHandler;

fn exit_root(_menu: &Menu<Output>, context: &mut Output) {
writeln!(context, "In exit_root").unwrap();
}
impl MenuHandler<Output> for RootMenuHandler {
async fn entry(&self, _menu: &Menu<'_, Output>, context: &mut Output) {
writeln!(context, "In enter_root").unwrap();
}

fn select_foo<'a>(_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();
async fn exit(&self, _menu: &Menu<'_, Output>, context: &mut Output) {
writeln!(context, "In exit_root").unwrap();
}
}

fn select_bar<'a>(_menu: &Menu<Output>, _item: &Item<Output>, args: &[&str], context: &mut Output) {
writeln!(context, "In select_bar. Args = {:?}", args).unwrap();
struct FooItemHandler;

impl ItemHandler<Output> 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<Output>, context: &mut Output) {
writeln!(context, "In enter_sub").unwrap();
struct BarHandler;

impl ItemHandler<Output> 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<Output>, context: &mut Output) {
writeln!(context, "In exit_sub").unwrap();
struct SubMenuHandler;

impl MenuHandler<Output> 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<Output>, _item: &Item<Output>, args: &[&str], context: &mut Output) {
writeln!(context, "In select_baz: Args = {:?}", args).unwrap();
struct BazHandler;

impl ItemHandler<Output> 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<Output>,
_item: &Item<Output>,
args: &[&str],
context: &mut Output,
) {
writeln!(context, "In select_quux: Args = {:?}", args).unwrap();
struct QuuxHandler;

impl ItemHandler<Output> 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();
}
}
1 change: 1 addition & 0 deletions rust-toolchain
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
nightly
190 changes: 151 additions & 39 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -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<T> {
/// 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<T> = fn(menu: &Menu<T>, context: &mut T);
pub trait BoxedMenuHandler<T> {
/// 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<T>,
context: &'a mut T,
) -> Pin<Box<dyn Future<Output = ()> + 'a>>;

/// A function to call when this menu is exited. Never called for the root menu.
fn exit<'a>(
&'a self,
menu: &'a Menu<T>,
context: &'a mut T,
) -> Pin<Box<dyn Future<Output = ()> + 'a>>;
}

/// The type of function we call when a valid command has been entered.
pub trait ItemHandler<T> {
/// 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<T> {
/// The function to call
fn handle<'a>(
&'a self,
menu: &'a Menu<T>,
item: &'a Item<T>,
args: &'a [&str],
context: &'a mut T,
) -> Pin<Box<dyn Future<Output = ()> + 'a>>;
}

/// Wrapper helper for transitioning an unboxed handler into a boxed one.
pub struct BoxedHandler<T>(pub T);

/// The type of function we call when we a valid command has been entered.
pub type ItemCallbackFn<T> = fn(menu: &Menu<T>, item: &Item<T>, args: &[&str], context: &mut T);
impl<T, H: MenuHandler<T>> BoxedMenuHandler<T> for BoxedHandler<H> {
// fn entry(&self, menu: &Menu<T>, context: &mut T) -> Pin<Box<dyn Future<Output = ()>>> {
// Box::pin(self.0.entry(menu, context))
// }

fn entry<'a>(
&'a self,
menu: &'a Menu<T>,
context: &'a mut T,
) -> Pin<Box<dyn Future<Output = ()> + 'a>> {
let fut = self.0.entry(menu, context);
Box::pin(fut)
}

fn exit<'a>(
&'a self,
menu: &'a Menu<T>,
context: &'a mut T,
) -> Pin<Box<dyn Future<Output = ()> + 'a>> {
Box::pin(self.0.exit(menu, context))
}
}

impl<T, H: ItemHandler<T>> BoxedItemHandler<T> for BoxedHandler<H> {
fn handle<'a>(
&'a self,
menu: &'a Menu<T>,
item: &'a Item<T>,
args: &'a [&str],
context: &'a mut T,
) -> Pin<Box<dyn Future<Output = ()> + '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<T>,
handler: &'a dyn BoxedItemHandler<T>,
/// 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<MenuCallbackFn<T>>,
/// A function to call when this menu is exited. Never called for the root menu.
pub exit: Option<MenuCallbackFn<T>>,
/// The menu handler.
pub handler: Option<&'a dyn BoxedMenuHandler<T>>,
}

/// 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<T>,
parameters: &[Parameter],
parent_menu: &Menu<T>,
item: &Item<T>,
callback_handler: &'h dyn BoxedItemHandler<T>,
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,15 +748,26 @@ where
mod tests {
use super::*;

fn dummy(_menu: &Menu<u32>, _item: &Item<u32>, _args: &[&str], _context: &mut u32) {}
struct Dummy;

impl ItemHandler<u32> for Dummy {
async fn handle(
&self,
_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,
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",