Skip to content

Commit

Permalink
feat: support named options and flags in macro
Browse files Browse the repository at this point in the history
  • Loading branch information
funbiscuit committed Jan 27, 2024
1 parent 153ee60 commit 0419c5c
Show file tree
Hide file tree
Showing 12 changed files with 678 additions and 127 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ for now. If you have suggestions - open an Issue or a Pull Request.
- [x] No dynamic dispatch
- [x] Configurable memory usage
- [x] Declaration of commands with enums
- [x] Options and flags support
- [x] Left/right support (move inside current input)
- [x] Parsing of arguments to common types
- [x] Autocompletion of command names (with tab)
Expand All @@ -34,7 +35,6 @@ for now. If you have suggestions - open an Issue or a Pull Request.
- [x] Formatted write with [ufmt](https://github.com/japaric/ufmt)
- [x] No panicking branches in generated code, when optimized
- [x] Any byte-stream interface is supported (`embedded_io::Write` as output stream, input bytes are given one-by-one)
- [ ] Move inside current input with left/right keypress
- [ ] Colors through ANSI escape sequences
- [ ] Navigation through history with search of current input
- [ ] Support wrapping of generated str slices in user macro (useful for arduino progmem)
Expand Down
50 changes: 36 additions & 14 deletions demo.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,34 +9,39 @@
# ffmpeg -i embedded-cli.mp4 frame%04d.png
# gifski -o demo.gif -Q 50 frame*.png

SPEED=1

declare -i DELAY=(300)/SPEED
declare -i LONGER_DELAY=(350)/SPEED

type () {
xdotool type --delay 300 -- "$1"
xdotool type --delay $DELAY -- "$1"
}
submit () {
xdotool key --delay 500 Return
xdotool key --delay $LONGER_DELAY Return
}
backspace () {
local repeat=${1:-1}
xdotool key --delay 400 --repeat $repeat BackSpace
xdotool key --delay $DELAY --repeat $repeat BackSpace
}
tab () {
xdotool key --delay 500 Tab
xdotool key --delay $LONGER_DELAY Tab
}
left () {
local repeat=${1:-1}
xdotool key --delay 400 --repeat $repeat Left
xdotool key --delay $DELAY --repeat $repeat Left
}
right () {
local repeat=${1:-1}
xdotool key --delay 400 --repeat $repeat Right
xdotool key --delay $DELAY --repeat $repeat Right
}
up () {
local repeat=${1:-1}
xdotool key --delay 800 --repeat $repeat Up
xdotool key --delay $LONGER_DELAY --repeat $repeat Up
}
down () {
local repeat=${1:-1}
xdotool key --delay 800 --repeat $repeat Down
xdotool key --delay $LONGER_DELAY --repeat $repeat Down
}

echo "Demo started"
Expand All @@ -57,12 +62,9 @@ submit
type "h"
tab

sleep 0.5

type "l"
tab

sleep 0.5
submit

up
Expand All @@ -72,13 +74,23 @@ left 2
type "e"
submit

sleep 0.5

up 2

type "Rust"
submit

up
type " -V"
submit

up
backspace
type "-verbose"
submit

type "hello -- --Rust--"
submit

type "got-l"
left 5
type "help "
Expand All @@ -95,7 +107,7 @@ tab
type "-"
tab

type "12"
type "--led 12"
submit

up 2
Expand All @@ -109,10 +121,16 @@ submit

up
type " 123 789"
submit

up
backspace 3
type "456"
left 4
backspace 2
left
type "--id "
right
type "01"
submit

Expand All @@ -125,6 +143,10 @@ backspace 4
type "\\\"abc\\\" "
submit

up
type " -- --arg"
submit

# Wait until keys disappear
sleep 5
echo "Demo is finished"
212 changes: 163 additions & 49 deletions embedded-cli-macros/src/command/help.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use quote::quote;
use super::{model::Command, TargetType};

#[cfg(feature = "help")]
use super::model::CommandArgs;
use super::model::{CommandArgType, CommandArgs};

#[cfg(feature = "help")]
pub fn derive_help(
Expand All @@ -14,10 +14,7 @@ pub fn derive_help(
commands: &[Command],
) -> Result<TokenStream> {
let help_all = create_help_all(commands, help_title)?;
let commands_help = commands
.iter()
.map(create_command_help)
.collect::<Result<Vec<_>>>()?;
let commands_help = commands.iter().map(create_command_help).collect::<Vec<_>>();

let ident = target.ident();
let named_lifetime = target.named_lifetime();
Expand Down Expand Up @@ -90,7 +87,7 @@ fn create_help_all(commands: &[Command], title: &str) -> Result<TokenStream> {
}

#[cfg(feature = "help")]
fn create_command_help(command: &Command) -> Result<TokenStream> {
fn create_command_help(command: &Command) -> TokenStream {
let name = command.name();

let help = if let Some(help) = command.help().long() {
Expand All @@ -102,69 +99,186 @@ fn create_command_help(command: &Command) -> Result<TokenStream> {
quote! {}
};

let (args_str, args_help) = match command.args() {
CommandArgs::None => (quote! { "" }, quote! {}),
let usage = create_usage(command.args());
let args_help = create_args_help(command.args());
let options_help = create_options_help(command.args());

quote! {
#name => {
#help
#usage
#args_help
#options_help
},
}
}

#[cfg(feature = "help")]
fn create_args_help(args: &CommandArgs) -> TokenStream {
let help_lines = match args {
CommandArgs::None => vec![],
CommandArgs::Named(args) => {
let args_str = args
// 2 is added to account for brackets
let longest_arg = args
.iter()
.map(|arg| {
if arg.is_optional() {
format!(" [{}]", arg.name().to_uppercase())
} else {
format!(" <{}>", arg.name().to_uppercase())
.filter(|a| a.arg_type().is_positional())
.map(|a| a.name().len() + 2)
.max()
.unwrap_or(0);

args.iter()
.filter_map(|arg| match arg.arg_type() {
CommandArgType::Positional => {
let name = if arg.is_optional() {
format!("[{}]", arg.name().to_uppercase())
} else {
format!("<{}>", arg.name().to_uppercase())
};

let arg_help = arg.help().short().unwrap_or("");

Some(quote! {
writer.write_list_element(#name, #arg_help, #longest_arg)?;
})
}
_ => None,
})
.collect::<String>();
let longest_arg = args.iter().map(|a| a.name().len() + 2).max().unwrap_or(0);
.collect::<Vec<_>>()
}
};

let args_help = args
.iter()
.map(|arg| {
let name = if arg.is_optional() {
if help_lines.is_empty() {
quote! {}
} else {
quote! {
writer.write_title("Arguments:")?;
writer.writeln_str("")?;
#(#help_lines)*
writer.writeln_str("")?;
}
}
}

#[cfg(feature = "help")]
fn create_options_help(args: &CommandArgs) -> TokenStream {
struct OptionHelp {
name: String,
help: String,
}

let mut help_lines = match args {
CommandArgs::None => vec![],
CommandArgs::Named(args) => args
.iter()
.filter_map(|arg| match arg.arg_type() {
CommandArgType::Flag { long, short } => {
let name = short
.map(|name| format!("-{}", name))
.into_iter()
.chain(long.iter().map(|name| format!("--{}", name)))
.collect::<Vec<_>>()
.join(", ");

let help = arg.help().short().unwrap_or("").to_string();

Some(OptionHelp { name, help })
}
CommandArgType::Option { long, short } => {
let name = short
.map(|name| format!("-{}", name))
.into_iter()
.chain(long.iter().map(|name| format!("--{}", name)))
.collect::<Vec<_>>()
.join(", ");

let value = if arg.is_optional() {
format!("[{}]", arg.name().to_uppercase())
} else {
format!("<{}>", arg.name().to_uppercase())
};

let arg_help = arg.help().short().unwrap_or("");
let name = format!("{} {}", name, value);

let help = arg.help().short().unwrap_or("").to_string();

Some(OptionHelp { name, help })
}
CommandArgType::Positional => None,
})
.collect::<Vec<_>>(),
};
help_lines.push(OptionHelp {
name: "-h, --help".to_string(),
help: "Print help".to_string(),
});
let longest_name = help_lines.iter().map(|a| a.name.len()).max().unwrap();

let help_lines = help_lines
.into_iter()
.map(|help| {
let name = help.name;
let help = help.help;
quote! {
writer.write_list_element(#name, #help, #longest_name)?;
}
})
.collect::<Vec<_>>();

quote! {
writer.write_title("Options:")?;
writer.writeln_str("")?;
#(#help_lines)*
}
}

#[cfg(feature = "help")]
fn create_usage(args: &CommandArgs) -> TokenStream {
let has_options;
let usage_args;

match args {
CommandArgs::None => {
has_options = false;
usage_args = vec![quote! { writer.writeln_str("")?; }];
}
CommandArgs::Named(args) => {
has_options = args.iter().any(|arg| !arg.arg_type().is_positional());

usage_args = args
.iter()
.filter_map(|arg| match arg.arg_type() {
crate::command::model::CommandArgType::Positional => {
let name = if arg.is_optional() {
format!("[{}]", arg.name().to_uppercase())
} else {
format!("<{}>", arg.name().to_uppercase())
};
Some(name)
}
_ => None,
})
.map(|line| {
quote! {
writer.write_list_element(#name, #arg_help, #longest_arg)?;
writer.write_str(" ")?;
writer.writeln_str(#line)?;
}
})
.collect::<Vec<_>>();

let args_str = quote! { #args_str };
let args_help = if args_help.is_empty() {
quote! {}
} else {
quote! {
writer.write_title("Arguments:")?;
writer.writeln_str("")?;
#(#args_help)*
writer.writeln_str("")?;
}
};

(args_str, args_help)
.collect::<Vec<_>>()
}
};

Ok(quote! {
#name => {
#help
let options = if has_options {
quote! { writer.write_str(" [OPTIONS]")?; }
} else {
quote! {}
};

quote! {
writer.write_title("Usage:")?;
writer.write_str(" ")?;
writer.write_str(command)?;
writer.writeln_str(#args_str)?;
#options
#(#usage_args)*
writer.writeln_str("")?;

#args_help

writer.write_title("Options:")?;
writer.writeln_str("")?;
writer.write_list_element("-h, --help", "Print help", 10)?;
},
})
}
}
Loading

0 comments on commit 0419c5c

Please sign in to comment.