From 4d4954dc9ef5ee4e0a9e5683a2427d390b62f227 Mon Sep 17 00:00:00 2001 From: Tommaso Clini Date: Wed, 10 Jun 2026 12:37:04 +0200 Subject: [PATCH 1/9] async closures version? --- embedded-cli-macros/src/processor.rs | 10 +++++----- embedded-cli/src/cli.rs | 25 ++++++++++++++----------- embedded-cli/src/command.rs | 10 +++++----- embedded-cli/src/service.rs | 4 ++-- examples/desktop/Cargo.toml | 1 + examples/desktop/src/main.rs | 6 ++++-- 6 files changed, 31 insertions(+), 25 deletions(-) diff --git a/embedded-cli-macros/src/processor.rs b/embedded-cli-macros/src/processor.rs index 78c5e43..80a86e1 100644 --- a/embedded-cli-macros/src/processor.rs +++ b/embedded-cli-macros/src/processor.rs @@ -16,14 +16,14 @@ pub fn impl_processor(vis: &Visibility, target: &TargetType) -> Result, E: _io::Error, - F: FnMut(&mut _cli::cli::CliHandle<'_, W, E>, #ident #unnamed_lifetime) -> Result<(), E>, + F: AsyncFnMut(&mut _cli::cli::CliHandle<'_, W, E>, #ident #unnamed_lifetime) -> Result<(), E>, >( f: F, ) -> impl _cli::service::CommandProcessor { struct Processor< W: _io::Write, E: _io::Error, - F: FnMut(&mut _cli::cli::CliHandle<'_, W, E>, #ident #unnamed_lifetime) -> Result<(), E>, + F: AsyncFnMut(&mut _cli::cli::CliHandle<'_, W, E>, #ident #unnamed_lifetime) -> Result<(), E>, > { f: F, _ph: core::marker::PhantomData<(W, E)>, @@ -32,16 +32,16 @@ pub fn impl_processor(vis: &Visibility, target: &TargetType) -> Result, E: _io::Error, - F: FnMut(&mut _cli::cli::CliHandle<'_, W, E>, #ident #unnamed_lifetime) -> Result<(), E>, + F: AsyncFnMut(&mut _cli::cli::CliHandle<'_, W, E>, #ident #unnamed_lifetime) -> Result<(), E>, > _cli::service::CommandProcessor for Processor { - fn process<'a>( + async fn process<'a>( &mut self, cli: &mut _cli::cli::CliHandle<'_, W, E>, raw: _cli::command::RawCommand<'a>, ) -> Result<(), _cli::service::ProcessError<'a, E>> { let cmd = <#ident #unnamed_lifetime as _cli::service::FromRaw>::parse(raw)?; - (self.f)(cli, cmd)?; + (self.f)(cli, cmd).await?; Ok(()) } } diff --git a/embedded-cli/src/cli.rs b/embedded-cli/src/cli.rs index f2d91b7..0deb817 100644 --- a/embedded-cli/src/cli.rs +++ b/embedded-cli/src/cli.rs @@ -157,7 +157,7 @@ where /// command set and/or command processor. /// In process callback you can change some outside state /// so next calls will use different processor - pub fn process_byte>( + pub async fn process_byte>( &mut self, b: u8, processor: &mut P, @@ -165,15 +165,18 @@ where if let (Some(mut editor), Some(mut input_generator)) = (self.editor.take(), self.input_generator.take()) { - let result = input_generator - .accept(b) - .map(|input| match input { + let result = if let Some(input) = input_generator.accept(b) { + Some(match input { Input::Control(control) => { self.on_control_input::(&mut editor, control, processor) + .await } Input::Char(text) => self.on_text_input(&mut editor, text), }) - .unwrap_or(Ok(())); + } else { + None + } + .unwrap_or(Ok(())); self.editor = Some(editor); self.input_generator = Some(input_generator); @@ -244,7 +247,7 @@ where Ok(()) } - fn on_control_input>( + async fn on_control_input>( &mut self, editor: &mut Editor, control: ControlInput, @@ -259,7 +262,7 @@ where let text = editor.text_mut(); let tokens = Tokens::new(text); - self.process_input::(tokens, processor)?; + self.process_input::(tokens, processor).await?; editor.clear(); @@ -354,7 +357,7 @@ where Ok(()) } - fn process_command>( + async fn process_command>( &mut self, command: RawCommand<'_>, handler: &mut P, @@ -362,7 +365,7 @@ where let cli_writer = Writer::new(&mut self.writer); let mut handle = CliHandle::new(cli_writer); - let res = handler.process(&mut handle, command); + let res = handler.process(&mut handle, command).await; if let Some(prompt) = handle.new_prompt { self.prompt = prompt; @@ -380,7 +383,7 @@ where } #[allow(clippy::extra_unused_type_parameters)] - fn process_input>( + async fn process_input>( &mut self, tokens: Tokens<'_>, handler: &mut P, @@ -391,7 +394,7 @@ where return self.process_help::(request); } - self.process_command(command, handler)?; + self.process_command(command, handler).await?; }; Ok(()) diff --git a/embedded-cli/src/command.rs b/embedded-cli/src/command.rs index bd6cf49..e411acf 100644 --- a/embedded-cli/src/command.rs +++ b/embedded-cli/src/command.rs @@ -56,14 +56,14 @@ impl<'a> RawCommand<'a> { pub fn processor< W: Write, E: embedded_io::Error, - F: FnMut(&mut CliHandle<'_, W, E>, RawCommand<'_>) -> Result<(), E>, + F: AsyncFnMut(&mut CliHandle<'_, W, E>, RawCommand<'_>) -> Result<(), E>, >( f: F, ) -> impl CommandProcessor { struct Processor< W: Write, E: embedded_io::Error, - F: FnMut(&mut CliHandle<'_, W, E>, RawCommand<'_>) -> Result<(), E>, + F: AsyncFnMut(&mut CliHandle<'_, W, E>, RawCommand<'_>) -> Result<(), E>, > { f: F, _ph: PhantomData<(W, E)>, @@ -72,15 +72,15 @@ impl<'a> RawCommand<'a> { impl< W: Write, E: embedded_io::Error, - F: FnMut(&mut CliHandle<'_, W, E>, RawCommand<'_>) -> Result<(), E>, + F: AsyncFnMut(&mut CliHandle<'_, W, E>, RawCommand<'_>) -> Result<(), E>, > CommandProcessor for Processor { - fn process<'a>( + async fn process<'a>( &mut self, cli: &mut CliHandle<'_, W, E>, raw: RawCommand<'a>, ) -> Result<(), ProcessError<'a, E>> { - (self.f)(cli, raw)?; + (self.f)(cli, raw).await?; Ok(()) } } diff --git a/embedded-cli/src/service.rs b/embedded-cli/src/service.rs index 870e9c3..1545ab4 100644 --- a/embedded-cli/src/service.rs +++ b/embedded-cli/src/service.rs @@ -118,7 +118,7 @@ pub trait FromRaw<'a>: Sized { } pub trait CommandProcessor, E: embedded_io::Error> { - fn process<'a>( + async fn process<'a>( &mut self, cli: &mut CliHandle<'_, W, E>, raw: RawCommand<'a>, @@ -131,7 +131,7 @@ where E: embedded_io::Error, F: for<'a> FnMut(&mut CliHandle<'_, W, E>, RawCommand<'a>) -> Result<(), ProcessError<'a, E>>, { - fn process<'a>( + async fn process<'a>( &mut self, cli: &mut CliHandle<'_, W, E>, command: RawCommand<'a>, diff --git a/examples/desktop/Cargo.toml b/examples/desktop/Cargo.toml index 500d60b..82851e1 100644 --- a/examples/desktop/Cargo.toml +++ b/examples/desktop/Cargo.toml @@ -8,4 +8,5 @@ embedded-cli = { path = "../../embedded-cli" } embedded-io = "0.7.1" rand = "0.8.5" termion = "3.0.0" +tokio = { version = "1.52.3", features = ["full"] } ufmt = "0.2.0" diff --git a/examples/desktop/src/main.rs b/examples/desktop/src/main.rs index ab165e0..2517382 100644 --- a/examples/desktop/src/main.rs +++ b/examples/desktop/src/main.rs @@ -166,7 +166,8 @@ fn on_status( Ok(()) } -fn main() { +#[tokio::main] +async fn main() { let stdout = stdout().into_raw_mode().unwrap(); let writer = Writer { stdout }; @@ -238,7 +239,7 @@ Use left and right to move inside input." for byte in bytes { cli.process_byte::, _>( byte, - &mut BaseCommand::processor(|cli, command| match command { + &mut BaseCommand::processor(async |cli, command| match command { BaseCommand::Led { id, command } => on_led(cli, &mut state, id, command), BaseCommand::Adc { id, command } => on_adc(cli, &mut state, id, command), BaseCommand::Status => on_status(cli, &mut state), @@ -248,6 +249,7 @@ Use left and right to move inside input." } }), ) + .await .unwrap(); } From 49e07fbd6fe4d6d635d286bf4c39dd4b200e0f58 Mon Sep 17 00:00:00 2001 From: Tommaso Clini Date: Mon, 6 Jul 2026 18:18:19 +0200 Subject: [PATCH 2/9] blocking compatibility --- embedded-cli/Cargo.toml | 1 + embedded-cli/src/cli.rs | 10 +++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/embedded-cli/Cargo.toml b/embedded-cli/Cargo.toml index 0af3143..84c9fd5 100644 --- a/embedded-cli/Cargo.toml +++ b/embedded-cli/Cargo.toml @@ -26,6 +26,7 @@ embedded-cli-macros = { version = "0.2.1", path = "../embedded-cli-macros", opti bitflags = "2.5.0" embedded-io = "0.7.1" ufmt = "0.2.0" +embassy-futures = "0.1.2" [dev-dependencies] regex = "1.10.4" diff --git a/embedded-cli/src/cli.rs b/embedded-cli/src/cli.rs index 0deb817..2826ab4 100644 --- a/embedded-cli/src/cli.rs +++ b/embedded-cli/src/cli.rs @@ -1,6 +1,6 @@ pub use crate::builder::CliBuilder; -use core::fmt::Debug; +use core::{fmt::Debug, future::Future, pin::pin, task::Context}; #[cfg(not(feature = "history"))] use core::marker::PhantomData; @@ -153,6 +153,14 @@ where Ok(cli) } + pub fn process_byte_blocking>( + &mut self, + b: u8, + processor: &mut P, + ) -> Result<(), E> { + embassy_futures::block_on(self.process_byte::(b, processor)) + } + /// Each call to process byte can be done with different /// command set and/or command processor. /// In process callback you can change some outside state From 2cafb8c69f45a7064011c53d57fa3bddfd3f000d Mon Sep 17 00:00:00 2001 From: Tommaso Clini Date: Mon, 6 Jul 2026 19:11:13 +0200 Subject: [PATCH 3/9] don't actually block --- embedded-cli/Cargo.toml | 1 - embedded-cli/src/cli.rs | 13 ++++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/embedded-cli/Cargo.toml b/embedded-cli/Cargo.toml index 84c9fd5..0af3143 100644 --- a/embedded-cli/Cargo.toml +++ b/embedded-cli/Cargo.toml @@ -26,7 +26,6 @@ embedded-cli-macros = { version = "0.2.1", path = "../embedded-cli-macros", opti bitflags = "2.5.0" embedded-io = "0.7.1" ufmt = "0.2.0" -embassy-futures = "0.1.2" [dev-dependencies] regex = "1.10.4" diff --git a/embedded-cli/src/cli.rs b/embedded-cli/src/cli.rs index 2826ab4..18608ac 100644 --- a/embedded-cli/src/cli.rs +++ b/embedded-cli/src/cli.rs @@ -1,6 +1,11 @@ pub use crate::builder::CliBuilder; -use core::{fmt::Debug, future::Future, pin::pin, task::Context}; +use core::{ + fmt::Debug, + future::Future, + pin::pin, + task::{Context, Poll, Waker}, +}; #[cfg(not(feature = "history"))] use core::marker::PhantomData; @@ -157,8 +162,10 @@ where &mut self, b: u8, processor: &mut P, - ) -> Result<(), E> { - embassy_futures::block_on(self.process_byte::(b, processor)) + ) -> Poll> { + let p = pin!(self.process_byte::(b, processor)); + let mut cx = Context::from_waker(Waker::noop()); + p.poll(&mut cx) } /// Each call to process byte can be done with different From 3a7cabac488c99da17c79187c25ee94a04d51fa1 Mon Sep 17 00:00:00 2001 From: Tommaso Clini Date: Tue, 7 Jul 2026 10:44:26 +0200 Subject: [PATCH 4/9] tokio as a dev dependency --- embedded-cli/Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/embedded-cli/Cargo.toml b/embedded-cli/Cargo.toml index 0af3143..17bb545 100644 --- a/embedded-cli/Cargo.toml +++ b/embedded-cli/Cargo.toml @@ -30,3 +30,4 @@ ufmt = "0.2.0" [dev-dependencies] regex = "1.10.4" rstest = "0.19.0" +tokio = "1.52.3" From 377ab0f32f2fcc0329119e066327686cbaac3bbe Mon Sep 17 00:00:00 2001 From: Tommaso Clini Date: Fri, 10 Jul 2026 00:57:20 +0200 Subject: [PATCH 5/9] use of nb crate instead of Poll in blocking api --- embedded-cli/Cargo.toml | 1 + embedded-cli/src/cli.rs | 9 ++++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/embedded-cli/Cargo.toml b/embedded-cli/Cargo.toml index 17bb545..292ece2 100644 --- a/embedded-cli/Cargo.toml +++ b/embedded-cli/Cargo.toml @@ -26,6 +26,7 @@ embedded-cli-macros = { version = "0.2.1", path = "../embedded-cli-macros", opti bitflags = "2.5.0" embedded-io = "0.7.1" ufmt = "0.2.0" +nb = "1.1.0" [dev-dependencies] regex = "1.10.4" diff --git a/embedded-cli/src/cli.rs b/embedded-cli/src/cli.rs index 18608ac..f81237e 100644 --- a/embedded-cli/src/cli.rs +++ b/embedded-cli/src/cli.rs @@ -158,14 +158,17 @@ where Ok(cli) } - pub fn process_byte_blocking>( + pub fn try_process_byte>( &mut self, b: u8, processor: &mut P, - ) -> Poll> { + ) -> nb::Result<(), E> { let p = pin!(self.process_byte::(b, processor)); let mut cx = Context::from_waker(Waker::noop()); - p.poll(&mut cx) + match p.poll(&mut cx) { + Poll::Ready(res) => res.map_err(nb::Error::Other), + Poll::Pending => Err(nb::Error::WouldBlock), + } } /// Each call to process byte can be done with different From 49d3747210f705657cab6688c9c7e5f39ed9b921 Mon Sep 17 00:00:00 2001 From: Tommaso Clini Date: Fri, 10 Jul 2026 00:59:48 +0200 Subject: [PATCH 6/9] change function name --- embedded-cli/src/cli.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/embedded-cli/src/cli.rs b/embedded-cli/src/cli.rs index f81237e..d0adbf6 100644 --- a/embedded-cli/src/cli.rs +++ b/embedded-cli/src/cli.rs @@ -158,7 +158,7 @@ where Ok(cli) } - pub fn try_process_byte>( + pub fn process_byte_sync>( &mut self, b: u8, processor: &mut P, From 54354c8e87894b39cc4833be235de7c40c463d54 Mon Sep 17 00:00:00 2001 From: Tommaso Clini Date: Mon, 27 Jul 2026 21:36:26 +0200 Subject: [PATCH 7/9] make this backwards compatible --- embedded-cli/Cargo.toml | 1 - embedded-cli/src/cli.rs | 33 +++++++++++++-------- embedded-cli/src/service.rs | 59 +++++++++++++++++++++++++++++++++++-- 3 files changed, 78 insertions(+), 15 deletions(-) diff --git a/embedded-cli/Cargo.toml b/embedded-cli/Cargo.toml index 292ece2..17bb545 100644 --- a/embedded-cli/Cargo.toml +++ b/embedded-cli/Cargo.toml @@ -26,7 +26,6 @@ embedded-cli-macros = { version = "0.2.1", path = "../embedded-cli-macros", opti bitflags = "2.5.0" embedded-io = "0.7.1" ufmt = "0.2.0" -nb = "1.1.0" [dev-dependencies] regex = "1.10.4" diff --git a/embedded-cli/src/cli.rs b/embedded-cli/src/cli.rs index d0adbf6..4c45cf8 100644 --- a/embedded-cli/src/cli.rs +++ b/embedded-cli/src/cli.rs @@ -17,7 +17,10 @@ use crate::{ command::RawCommand, editor::Editor, input::{ControlInput, Input, InputGenerator}, - service::{Autocomplete, CommandProcessor, Help, ParseError, ProcessError}, + service::{ + AsyncCommandProcessor, Autocomplete, CommandProcessor, Help, ParseError, ProcessError, + SyncToAsyncAdapter, + }, token::Tokens, utils, writer::{WriteExt, Writer}, @@ -158,16 +161,22 @@ where Ok(cli) } - pub fn process_byte_sync>( + /// Each call to process byte can be done with different + /// command set and/or command processor. + /// In process callback you can change some outside state + /// so next calls will use different processor + pub fn process_byte>( &mut self, b: u8, processor: &mut P, - ) -> nb::Result<(), E> { - let p = pin!(self.process_byte::(b, processor)); - let mut cx = Context::from_waker(Waker::noop()); - match p.poll(&mut cx) { - Poll::Ready(res) => res.map_err(nb::Error::Other), - Poll::Pending => Err(nb::Error::WouldBlock), + ) -> Result<(), E> { + let w = Waker::noop(); + let mut cx = Context::from_waker(&w); + match pin!(self.process_byte_async::(b, &mut SyncToAsyncAdapter::new(processor))) + .poll(&mut cx) + { + Poll::Ready(res) => res, + Poll::Pending => unreachable!(), } } @@ -175,7 +184,7 @@ where /// command set and/or command processor. /// In process callback you can change some outside state /// so next calls will use different processor - pub async fn process_byte>( + pub async fn process_byte_async>( &mut self, b: u8, processor: &mut P, @@ -265,7 +274,7 @@ where Ok(()) } - async fn on_control_input>( + async fn on_control_input>( &mut self, editor: &mut Editor, control: ControlInput, @@ -375,7 +384,7 @@ where Ok(()) } - async fn process_command>( + async fn process_command>( &mut self, command: RawCommand<'_>, handler: &mut P, @@ -401,7 +410,7 @@ where } #[allow(clippy::extra_unused_type_parameters)] - async fn process_input>( + async fn process_input>( &mut self, tokens: Tokens<'_>, handler: &mut P, diff --git a/embedded-cli/src/service.rs b/embedded-cli/src/service.rs index 1545ab4..0a81cbe 100644 --- a/embedded-cli/src/service.rs +++ b/embedded-cli/src/service.rs @@ -1,3 +1,5 @@ +use core::marker::PhantomData; + use embedded_io::Write; use crate::{arguments::FromArgumentError, cli::CliHandle, command::RawCommand}; @@ -118,7 +120,7 @@ pub trait FromRaw<'a>: Sized { } pub trait CommandProcessor, E: embedded_io::Error> { - async fn process<'a>( + fn process<'a>( &mut self, cli: &mut CliHandle<'_, W, E>, raw: RawCommand<'a>, @@ -131,7 +133,7 @@ where E: embedded_io::Error, F: for<'a> FnMut(&mut CliHandle<'_, W, E>, RawCommand<'a>) -> Result<(), ProcessError<'a, E>>, { - async fn process<'a>( + fn process<'a>( &mut self, cli: &mut CliHandle<'_, W, E>, command: RawCommand<'a>, @@ -139,3 +141,56 @@ where self(cli, command) } } + +pub(crate) struct SyncToAsyncAdapter< + 'a, + W: Write, + E: embedded_io::Error, + P: CommandProcessor, +>(&'a mut P, PhantomData<(W, E)>); + +impl<'a, W: Write, E: embedded_io::Error, P: CommandProcessor> + SyncToAsyncAdapter<'a, W, E, P> +{ + pub const fn new(p: &'a mut P) -> Self { + Self(p, PhantomData) + } +} + +impl<'b, W: Write, E: embedded_io::Error, P: CommandProcessor> + AsyncCommandProcessor for SyncToAsyncAdapter<'b, W, E, P> +{ + async fn process<'a>( + &mut self, + cli: &mut CliHandle<'_, W, E>, + raw: RawCommand<'a>, + ) -> Result<(), ProcessError<'a, E>> { + self.0.process(cli, raw) + } +} + +pub trait AsyncCommandProcessor, E: embedded_io::Error> { + async fn process<'a>( + &mut self, + cli: &mut CliHandle<'_, W, E>, + raw: RawCommand<'a>, + ) -> Result<(), ProcessError<'a, E>>; +} + +impl AsyncCommandProcessor for F +where + W: Write, + E: embedded_io::Error, + F: for<'a> AsyncFnMut( + &mut CliHandle<'_, W, E>, + RawCommand<'a>, + ) -> Result<(), ProcessError<'a, E>>, +{ + async fn process<'a>( + &mut self, + cli: &mut CliHandle<'_, W, E>, + command: RawCommand<'a>, + ) -> Result<(), ProcessError<'a, E>> { + self(cli, command).await + } +} From e317ef78287b2f6ae74bb88d99633dcb9a30bc84 Mon Sep 17 00:00:00 2001 From: Tommaso Clini Date: Tue, 28 Jul 2026 21:08:13 +0200 Subject: [PATCH 8/9] fix error and go back to old example --- embedded-cli/src/command.rs | 10 +++++----- examples/desktop/Cargo.toml | 1 - examples/desktop/src/main.rs | 6 ++---- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/embedded-cli/src/command.rs b/embedded-cli/src/command.rs index e411acf..bd6cf49 100644 --- a/embedded-cli/src/command.rs +++ b/embedded-cli/src/command.rs @@ -56,14 +56,14 @@ impl<'a> RawCommand<'a> { pub fn processor< W: Write, E: embedded_io::Error, - F: AsyncFnMut(&mut CliHandle<'_, W, E>, RawCommand<'_>) -> Result<(), E>, + F: FnMut(&mut CliHandle<'_, W, E>, RawCommand<'_>) -> Result<(), E>, >( f: F, ) -> impl CommandProcessor { struct Processor< W: Write, E: embedded_io::Error, - F: AsyncFnMut(&mut CliHandle<'_, W, E>, RawCommand<'_>) -> Result<(), E>, + F: FnMut(&mut CliHandle<'_, W, E>, RawCommand<'_>) -> Result<(), E>, > { f: F, _ph: PhantomData<(W, E)>, @@ -72,15 +72,15 @@ impl<'a> RawCommand<'a> { impl< W: Write, E: embedded_io::Error, - F: AsyncFnMut(&mut CliHandle<'_, W, E>, RawCommand<'_>) -> Result<(), E>, + F: FnMut(&mut CliHandle<'_, W, E>, RawCommand<'_>) -> Result<(), E>, > CommandProcessor for Processor { - async fn process<'a>( + fn process<'a>( &mut self, cli: &mut CliHandle<'_, W, E>, raw: RawCommand<'a>, ) -> Result<(), ProcessError<'a, E>> { - (self.f)(cli, raw).await?; + (self.f)(cli, raw)?; Ok(()) } } diff --git a/examples/desktop/Cargo.toml b/examples/desktop/Cargo.toml index 82851e1..500d60b 100644 --- a/examples/desktop/Cargo.toml +++ b/examples/desktop/Cargo.toml @@ -8,5 +8,4 @@ embedded-cli = { path = "../../embedded-cli" } embedded-io = "0.7.1" rand = "0.8.5" termion = "3.0.0" -tokio = { version = "1.52.3", features = ["full"] } ufmt = "0.2.0" diff --git a/examples/desktop/src/main.rs b/examples/desktop/src/main.rs index 2517382..ab165e0 100644 --- a/examples/desktop/src/main.rs +++ b/examples/desktop/src/main.rs @@ -166,8 +166,7 @@ fn on_status( Ok(()) } -#[tokio::main] -async fn main() { +fn main() { let stdout = stdout().into_raw_mode().unwrap(); let writer = Writer { stdout }; @@ -239,7 +238,7 @@ Use left and right to move inside input." for byte in bytes { cli.process_byte::, _>( byte, - &mut BaseCommand::processor(async |cli, command| match command { + &mut BaseCommand::processor(|cli, command| match command { BaseCommand::Led { id, command } => on_led(cli, &mut state, id, command), BaseCommand::Adc { id, command } => on_adc(cli, &mut state, id, command), BaseCommand::Status => on_status(cli, &mut state), @@ -249,7 +248,6 @@ Use left and right to move inside input." } }), ) - .await .unwrap(); } From cbcc23cd56f0ba7de523a88bbaba0da735f333e6 Mon Sep 17 00:00:00 2001 From: Tommaso Clini Date: Tue, 28 Jul 2026 21:44:22 +0200 Subject: [PATCH 9/9] normal macros and async macros, now it should be fully working --- embedded-cli-macros/src/command/mod.rs | 4 +- embedded-cli-macros/src/group/mod.rs | 4 +- embedded-cli-macros/src/lib.rs | 46 ++++++++++- embedded-cli-macros/src/processor.rs | 110 ++++++++++++++++++------- embedded-cli/src/cli.rs | 3 +- embedded-cli/src/lib.rs | 2 +- embedded-cli/src/service.rs | 1 + 7 files changed, 131 insertions(+), 39 deletions(-) diff --git a/embedded-cli-macros/src/command/mod.rs b/embedded-cli-macros/src/command/mod.rs index f564617..903c6eb 100644 --- a/embedded-cli-macros/src/command/mod.rs +++ b/embedded-cli-macros/src/command/mod.rs @@ -24,7 +24,7 @@ struct ServiceAttrs { skip_from_raw: bool, } -pub fn derive_command(input: DeriveInput) -> Result { +pub fn derive_command(input: DeriveInput, is_async: bool) -> Result { let opts = ServiceAttrs::from_derive_input(&input)?; let DeriveInput { vis, @@ -67,7 +67,7 @@ pub fn derive_command(input: DeriveInput) -> Result { } else { parse::derive_from_raw(&target, &commands)? }; - let impl_processor = processor::impl_processor(&vis, &target)?; + let impl_processor = processor::impl_processor(&vis, &target, is_async)?; let output = quote! { #derive_autocomplete diff --git a/embedded-cli-macros/src/group/mod.rs b/embedded-cli-macros/src/group/mod.rs index ba965f1..db18a99 100644 --- a/embedded-cli-macros/src/group/mod.rs +++ b/embedded-cli-macros/src/group/mod.rs @@ -9,7 +9,7 @@ use self::command_group::CommandGroup; mod command_group; -pub fn derive_command_group(input: DeriveInput) -> Result { +pub fn derive_command_group(input: DeriveInput, is_async: bool) -> Result { let DeriveInput { vis, ident, @@ -37,7 +37,7 @@ pub fn derive_command_group(input: DeriveInput) -> Result { let derive_autocomplete = derive_autocomplete(&target, &groups); let derive_help = derive_help(&target, &groups); let derive_from_raw = derive_from_raw(&target, &groups); - let impl_processor = processor::impl_processor(&vis, &target)?; + let impl_processor = processor::impl_processor(&vis, &target, is_async)?; let output = quote! { #derive_autocomplete diff --git a/embedded-cli-macros/src/lib.rs b/embedded-cli-macros/src/lib.rs index a2db1cd..ce6e55d 100644 --- a/embedded-cli-macros/src/lib.rs +++ b/embedded-cli-macros/src/lib.rs @@ -11,7 +11,7 @@ mod utils; pub fn derive_command(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input); - let output = match command::derive_command(input) { + let output = match command::derive_command(input, false) { Ok(output) => output, Err(e) => return e.write_errors().into(), }; @@ -32,7 +32,49 @@ pub fn derive_command(input: TokenStream) -> TokenStream { pub fn derive_command_group(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input); - let output = match group::derive_command_group(input) { + let output = match group::derive_command_group(input, false) { + Ok(output) => output, + Err(e) => return e.write_errors().into(), + }; + + // wrap with anonymous scope + quote! { + const _: () = { + extern crate embedded_cli as _cli; + use _cli::__private::io as _io; + + #output + }; + } + .into() +} + +#[proc_macro_derive(CommandAsync, attributes(command, arg))] +pub fn derive_command_async(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input); + + let output = match command::derive_command(input, true) { + Ok(output) => output, + Err(e) => return e.write_errors().into(), + }; + + // wrap with anonymous scope + quote! { + const _: () = { + extern crate embedded_cli as _cli; + use _cli::__private::io as _io; + + #output + }; + } + .into() +} + +#[proc_macro_derive(CommandGroupAsync, attributes(group))] +pub fn derive_command_group_async(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input); + + let output = match group::derive_command_group(input, true) { Ok(output) => output, Err(e) => return e.write_errors().into(), }; diff --git a/embedded-cli-macros/src/processor.rs b/embedded-cli-macros/src/processor.rs index 80a86e1..b853a71 100644 --- a/embedded-cli-macros/src/processor.rs +++ b/embedded-cli-macros/src/processor.rs @@ -5,50 +5,100 @@ use syn::Visibility; use crate::utils::TargetType; -pub fn impl_processor(vis: &Visibility, target: &TargetType) -> Result { +pub fn impl_processor( + vis: &Visibility, + target: &TargetType, + is_async: bool, +) -> Result { let ident = target.ident(); let named_lifetime = target.named_lifetime(); let unnamed_lifetime = target.unnamed_lifetime(); - let output = quote! { - - impl #named_lifetime #ident #named_lifetime { - #vis fn processor< - W: _io::Write, - E: _io::Error, - F: AsyncFnMut(&mut _cli::cli::CliHandle<'_, W, E>, #ident #unnamed_lifetime) -> Result<(), E>, - >( - f: F, - ) -> impl _cli::service::CommandProcessor { - struct Processor< + let output = if is_async { + quote! { + + impl #named_lifetime #ident #named_lifetime { + #vis fn processor< W: _io::Write, E: _io::Error, F: AsyncFnMut(&mut _cli::cli::CliHandle<'_, W, E>, #ident #unnamed_lifetime) -> Result<(), E>, - > { + >( f: F, - _ph: core::marker::PhantomData<(W, E)>, - } - - impl< + ) -> impl _cli::service::AsyncCommandProcessor { + struct Processor< W: _io::Write, E: _io::Error, F: AsyncFnMut(&mut _cli::cli::CliHandle<'_, W, E>, #ident #unnamed_lifetime) -> Result<(), E>, - > _cli::service::CommandProcessor for Processor - { - async fn process<'a>( - &mut self, - cli: &mut _cli::cli::CliHandle<'_, W, E>, - raw: _cli::command::RawCommand<'a>, - ) -> Result<(), _cli::service::ProcessError<'a, E>> { - let cmd = <#ident #unnamed_lifetime as _cli::service::FromRaw>::parse(raw)?; - (self.f)(cli, cmd).await?; - Ok(()) + > { + f: F, + _ph: core::marker::PhantomData<(W, E)>, + } + + impl< + W: _io::Write, + E: _io::Error, + F: AsyncFnMut(&mut _cli::cli::CliHandle<'_, W, E>, #ident #unnamed_lifetime) -> Result<(), E>, + > _cli::service::AsyncCommandProcessor for Processor + { + async fn process<'a>( + &mut self, + cli: &mut _cli::cli::CliHandle<'_, W, E>, + raw: _cli::command::RawCommand<'a>, + ) -> Result<(), _cli::service::ProcessError<'a, E>> { + let cmd = <#ident #unnamed_lifetime as _cli::service::FromRaw>::parse(raw)?; + (self.f)(cli, cmd).await?; + Ok(()) + } + } + + Processor { + f, + _ph: core::marker::PhantomData, } } + } + } + } else { + quote! { - Processor { - f, - _ph: core::marker::PhantomData, + impl #named_lifetime #ident #named_lifetime { + #vis fn processor< + W: _io::Write, + E: _io::Error, + F: FnMut(&mut _cli::cli::CliHandle<'_, W, E>, #ident #unnamed_lifetime) -> Result<(), E>, + >( + f: F, + ) -> impl _cli::service::CommandProcessor { + struct Processor< + W: _io::Write, + E: _io::Error, + F: FnMut(&mut _cli::cli::CliHandle<'_, W, E>, #ident #unnamed_lifetime) -> Result<(), E>, + > { + f: F, + _ph: core::marker::PhantomData<(W, E)>, + } + + impl< + W: _io::Write, + E: _io::Error, + F: FnMut(&mut _cli::cli::CliHandle<'_, W, E>, #ident #unnamed_lifetime) -> Result<(), E>, + > _cli::service::CommandProcessor for Processor + { + fn process<'a>( + &mut self, + cli: &mut _cli::cli::CliHandle<'_, W, E>, + raw: _cli::command::RawCommand<'a>, + ) -> Result<(), _cli::service::ProcessError<'a, E>> { + let cmd = <#ident #unnamed_lifetime as _cli::service::FromRaw>::parse(raw)?; + (self.f)(cli, cmd)?; + Ok(()) + } + } + + Processor { + f, + _ph: core::marker::PhantomData, + } } } } diff --git a/embedded-cli/src/cli.rs b/embedded-cli/src/cli.rs index 4c45cf8..c3caf53 100644 --- a/embedded-cli/src/cli.rs +++ b/embedded-cli/src/cli.rs @@ -170,8 +170,7 @@ where b: u8, processor: &mut P, ) -> Result<(), E> { - let w = Waker::noop(); - let mut cx = Context::from_waker(&w); + let mut cx = Context::from_waker(Waker::noop()); match pin!(self.process_byte_async::(b, &mut SyncToAsyncAdapter::new(processor))) .poll(&mut cx) { diff --git a/embedded-cli/src/lib.rs b/embedded-cli/src/lib.rs index 59c8582..58afe17 100644 --- a/embedded-cli/src/lib.rs +++ b/embedded-cli/src/lib.rs @@ -25,7 +25,7 @@ pub mod writer; /// Macro available if embedded-cli is built with `features = ["macros"]`. #[cfg(feature = "embedded-cli-macros")] -pub use embedded_cli_macros::{Command, CommandGroup}; +pub use embedded_cli_macros::{Command, CommandAsync, CommandGroup, CommandGroupAsync}; // Used by generated code. Not public API. #[doc(hidden)] diff --git a/embedded-cli/src/service.rs b/embedded-cli/src/service.rs index 0a81cbe..fdfb143 100644 --- a/embedded-cli/src/service.rs +++ b/embedded-cli/src/service.rs @@ -169,6 +169,7 @@ impl<'b, W: Write, E: embedded_io::Error, P: CommandProcessor> } } +#[allow(async_fn_in_trait)] pub trait AsyncCommandProcessor, E: embedded_io::Error> { async fn process<'a>( &mut self,