Merge branch 'v0.1.1' into 'main'

Added App::long_desc(self) which sets the root level description

See merge request arkham/arkham!4
This commit is contained in:
Joe Bellus 2021-11-14 02:47:55 +00:00
commit 93195bab1b
11 changed files with 302 additions and 71 deletions

58
Cargo.lock generated
View File

@ -2,14 +2,24 @@
# It is not intended for manual editing. # It is not intended for manual editing.
version = 3 version = 3
[[package]]
name = "aho-corasick"
version = "0.7.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "arkham" name = "arkham"
version = "0.1.0" version = "0.1.1"
dependencies = [ dependencies = [
"console", "console",
"criterion", "criterion",
"serde", "serde",
"serde_json", "serde_json",
"textwrap 0.14.2",
"toml", "toml",
] ]
@ -76,7 +86,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"textwrap", "textwrap 0.11.0",
"unicode-width", "unicode-width",
] ]
@ -312,6 +322,12 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "once_cell"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56"
[[package]] [[package]]
name = "oorandom" name = "oorandom"
version = "11.1.3" version = "11.1.3"
@ -404,7 +420,10 @@ version = "1.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9251239e129e16308e70d853559389de218ac275b515068abc96829d05b948a" checksum = "d9251239e129e16308e70d853559389de218ac275b515068abc96829d05b948a"
dependencies = [ dependencies = [
"aho-corasick",
"memchr",
"regex-syntax", "regex-syntax",
"thread_local",
] ]
[[package]] [[package]]
@ -508,6 +527,12 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "smawk"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043"
[[package]] [[package]]
name = "syn" name = "syn"
version = "1.0.73" version = "1.0.73"
@ -538,6 +563,26 @@ dependencies = [
"unicode-width", "unicode-width",
] ]
[[package]]
name = "textwrap"
version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80"
dependencies = [
"smawk",
"unicode-linebreak",
"unicode-width",
]
[[package]]
name = "thread_local"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8018d24e04c95ac8790716a5987d0fec4f8b27249ffa0f7d33f1369bdfb88cbd"
dependencies = [
"once_cell",
]
[[package]] [[package]]
name = "tinytemplate" name = "tinytemplate"
version = "1.2.1" version = "1.2.1"
@ -563,6 +608,15 @@ version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c" checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c"
[[package]]
name = "unicode-linebreak"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a52dcaab0c48d931f7cc8ef826fa51690a08e1ea55117ef26f89864f532383f"
dependencies = [
"regex",
]
[[package]] [[package]]
name = "unicode-width" name = "unicode-width"
version = "0.1.8" version = "0.1.8"

View File

@ -1,6 +1,6 @@
[package] [package]
name = "arkham" name = "arkham"
version = "0.1.0" version = "0.1.1"
authors = ["Joe Bellus <joe@5sigma.io>"] authors = ["Joe Bellus <joe@5sigma.io>"]
edition = "2018" edition = "2018"
description = "A framework for CLI applications" description = "A framework for CLI applications"
@ -19,11 +19,14 @@ console = "0.14.0"
serde = { version = "1.0", features = ["derive"], optional = true } serde = { version = "1.0", features = ["derive"], optional = true }
serde_json = { version = "1.0", optional = true} serde_json = { version = "1.0", optional = true}
toml = { version="0.5.8", optional = true} toml = { version="0.5.8", optional = true}
textwrap = "0.14.2"
[features] [features]
config = ["serde"] config = ["serialize"]
config_toml = ["toml", "config"] config_toml = ["toml", "config"]
config_json = ["serde_json", "config"] config_json = ["serde_json", "config"]
serialize = ["serde"]
[dev-dependencies] [dev-dependencies]
criterion = "0.3" criterion = "0.3"

View File

@ -27,4 +27,5 @@ structures: Detail lists, headers, etc.
# Basic Usage # Basic Usage
* [fib](https://git.5sigma.io/arkham/arkham/-/blob/master/examples/fib.rs) - An example using subcommands and command line options * [fib](https://git.5sigma.io/arkham/arkham/-/blob/master/examples/fib.rs) - An example using subcommands and command line options
* [config](https://git.5sigma.io/arkham/arkham/-/blob/master/examples/config.rs) - An example loading a configuration file. This must be ran with `cargo run --example config --features toml_config`

View File

@ -4,6 +4,7 @@ fn main() {
let _ = App::new() let _ = App::new()
.name("Fibonacci App") .name("Fibonacci App")
.version("1.0") .version("1.0")
.long_desc("This app can be used to calculate fibonacci numbers")
.command( .command(
Command::new("fib") Command::new("fib")
.opt( .opt(
@ -22,7 +23,7 @@ fn main() {
fn fibonacci_handler(_app: &App, ctx: &Context, _args: &[String]) { fn fibonacci_handler(_app: &App, ctx: &Context, _args: &[String]) {
let v = fibonacci( let v = fibonacci(
ctx.get_string("count") ctx.get_string("count")
.unwrap_or("1".to_string()) .unwrap_or_else(|| "1".to_string())
.parse() .parse()
.unwrap_or(1), .unwrap_or(1),
); );

View File

@ -1,4 +1,4 @@
use crate::print_command_help; use crate::{print_command_help, vox};
use super::command::{help, Command, Handler}; use super::command::{help, Command, Handler};
use super::context::Context; use super::context::Context;
@ -110,6 +110,29 @@ impl App {
self self
} }
/// Similar to `App::Command` but allows multiple commands to be added at once.
/// Adds a root level commands to the application. This command can then be executed with:
///
/// myapp command_name
///
/// Help flags will also be generated for the command which will display command
/// information for:
///
/// myapp --help command_name or myapp help command_name
///
/// Example:
/// ```rust
/// use arkham::{App, Command, Context};
/// let names = vec!["one", "two", "three"];
/// let commands = names.into_iter().map(|name| Command::new(name).handler(my_handler)).collect();
/// App::new().commands(commands);
///
/// fn my_handler(app: &App, ctx: &Context, args: &[String]) {}
/// ```
pub fn commands(self, commands: Vec<Command>) -> Self {
commands.into_iter().fold(self, |app, cmd| app.command(cmd))
}
/// Adds a root level opt/flag that is available to all commands. Opts are given a name which /// Adds a root level opt/flag that is available to all commands. Opts are given a name which
/// is used to reference them, as well as a short and long identifier. /// is used to reference them, as well as a short and long identifier.
/// ///
@ -140,9 +163,19 @@ impl App {
self self
} }
/// Execute the app and any specified handlers based on the passed arguemnts. This function is /// Sets the longe description for the bare app. This will be displayed in the help content
/// mostly used for testing or any situation where you need to pass arbitrary arguments instead /// when no additional subcommands are given.
/// of using the ones passed to the application. /// Example:
/// ```rust
/// use arkham::{App, Command, Context};
/// App::new().long_desc("This app does all the things");
/// ```
pub fn long_desc(mut self, desc: &str) -> Self {
self.root.long_desc = Some(desc.to_string());
self
}
/// Execute the app and any specified handlers based on the passed arguemnts. This function is mostly used for testing or any situation where you need to pass arbitrary arguments instead of using the ones passed to the application.
/// Example: /// Example:
/// ```rust /// ```rust
/// use arkham::{App, Command, Context, Opt}; /// use arkham::{App, Command, Context, Opt};
@ -160,11 +193,19 @@ impl App {
if let Some(filename) = self.config_filename { if let Some(filename) = self.config_filename {
ctx.load_config_file(filename); ctx.load_config_file(filename);
} }
run_command(self, &self.root, &args, &mut ctx) if let Err(e) = run_command(self, &self.root, &args, &mut ctx) {
match e {
OptError::InvalidOpt(opt) => {
vox::error(format!("Invalid options {}", &opt));
Err(OptError::InvalidOpt(opt))
}
}
} else {
Ok(())
}
} }
/// Execute the app and any specified handlers based on the arguments passsed to the /// Execute the app and any specified handlers based on the arguments passsed to the application.
/// application.
/// ///
/// Example: /// Example:
/// running with myapp --name alice /// running with myapp --name alice
@ -182,11 +223,17 @@ impl App {
pub fn run(&mut self) -> Result<()> { pub fn run(&mut self) -> Result<()> {
self.run_with(env::args().skip(1).collect()) self.run_with(env::args().skip(1).collect())
} }
/// Generates a context for the root command allowing flags to be parsed. This can be useful to predetect certian root level arguments before processing sub commands and other app configurations.
pub fn root_context(&self) -> Result<Context> {
let mut ctx = Context::new(self.root.clone());
let args = env::args().skip(1).collect::<Vec<_>>();
populate_args(&mut ctx, &self.root, &args)?;
Ok(ctx)
}
} }
/// This is the core logic for parsing arguments and executing handlers. It is ran but the App::run fn populate_args(ctx: &mut Context, cmd: &Command, args: &[String]) -> Result<Vec<String>> {
/// and App::run_with functions.
fn run_command(app: &App, cmd: &Command, args: &[String], ctx: &mut Context) -> Result<()> {
// Get an iterator for the incomming arguments // Get an iterator for the incomming arguments
let mut args = args.iter(); let mut args = args.iter();
// We will keep track of any arguments that arent consumed by the current command. // We will keep track of any arguments that arent consumed by the current command.
@ -197,7 +244,11 @@ fn run_command(app: &App, cmd: &Command, args: &[String], ctx: &mut Context) ->
while let Some(arg) = args.next() { while let Some(arg) = args.next() {
// Check for long args // Check for long args
if arg.starts_with("--") { if arg.starts_with("--") {
if let Some(opt) = cmd.opts.iter().find(|o| &o.long == &arg[2..]) { if let Some(opt) = cmd
.opts
.iter()
.find(|o| o.long == Some(arg[2..].to_string()))
{
match opt.kind { match opt.kind {
OptKind::Flag => { OptKind::Flag => {
ctx.set_flag(opt); ctx.set_flag(opt);
@ -216,28 +267,42 @@ fn run_command(app: &App, cmd: &Command, args: &[String], ctx: &mut Context) ->
continue; continue;
} }
// Check for short args // Check for short args
if arg.starts_with("-") { if arg.starts_with('-') {
if let Some(opt) = cmd.opts.iter().find(|o| &o.short == &arg[1..]) { for switch in arg.chars().skip(1) {
match opt.kind { if let Some(opt) = cmd
OptKind::Flag => { .opts
ctx.set_flag(opt); .iter()
} .find(|o| o.short == Some(switch.to_string()))
OptKind::String => { {
if let Some(value) = args.next() { match opt.kind {
ctx.set_opt(opt, value.clone()); OptKind::Flag => {
} else { ctx.set_flag(opt);
return Err(OptError::InvalidOpt(opt.name.clone())); }
OptKind::String => {
if let Some(value) = args.next() {
ctx.set_opt(opt, value.clone());
} else {
return Err(OptError::InvalidOpt(opt.name.clone()));
}
} }
} }
} else {
ignored.push(arg.clone());
} }
} else {
ignored.push(arg.clone());
} }
continue; continue;
} }
ignored.push(arg.clone()); ignored.push(arg.clone());
} }
Ok(ignored)
}
/// This is the core logic for parsing arguments and executing handlers. It is ran but the App::run
/// and App::run_with functions.
fn run_command(app: &App, cmd: &Command, args: &[String], ctx: &mut Context) -> Result<()> {
let mut ignored = populate_args(ctx, cmd, args)?;
// Find an recurse into sub commands if the remaining argumetns match any subcommand name // Find an recurse into sub commands if the remaining argumetns match any subcommand name
if let Some(cmd) = cmd if let Some(cmd) = cmd
.commands .commands
@ -250,12 +315,12 @@ fn run_command(app: &App, cmd: &Command, args: &[String], ctx: &mut Context) ->
// Automatic command help display // Automatic command help display
if ignored.iter().any(|a| a == "-h" || a == "--help") { if ignored.iter().any(|a| a == "-h" || a == "--help") {
super::command::print_command_help(cmd, &vec![]); super::command::print_command_help(cmd, &[]);
return Ok(()); return Ok(());
} }
// If any ignored parameters start with "-" we will throw an unknwon flag error. // If any ignored parameters start with "-" we will throw an unknwon flag error.
if let Some(arg) = ignored.iter().find(|a| a.starts_with("-")) { if let Some(arg) = ignored.iter().find(|a| a.starts_with('-')) {
return Err(OptError::InvalidOpt(arg.clone())); return Err(OptError::InvalidOpt(arg.clone()));
} }
@ -265,10 +330,10 @@ fn run_command(app: &App, cmd: &Command, args: &[String], ctx: &mut Context) ->
if let Some(prefix) = app.env_prefix { if let Some(prefix) = app.env_prefix {
ctx.env_prefix = Some(prefix.to_string()); ctx.env_prefix = Some(prefix.to_string());
} }
handler(app, &ctx, &ignored); handler(app, ctx, &ignored);
} else { } else {
crate::vox::print(app.application_header()); crate::vox::print(app.application_header());
print_command_help(cmd, &vec![]) print_command_help(cmd, &[])
} }
Ok(()) Ok(())
@ -328,15 +393,6 @@ mod tests {
.unwrap(); .unwrap();
} }
fn function_handler(_app: &App, _ctx: &Context, _args: &[String]) {
assert!(true);
}
#[test]
fn test_function_handler() {
App::new().handler(function_handler);
}
#[test] #[test]
fn test_extra_args() { fn test_extra_args() {
let args = vec!["somefile".to_string()]; let args = vec!["somefile".to_string()];
@ -354,12 +410,27 @@ mod tests {
App::new() App::new()
.opt(Opt::flag("verbose").short("v").long("verbose")) .opt(Opt::flag("verbose").short("v").long("verbose"))
.handler(|_, ctx, _| { .handler(|_, ctx, _| {
assert_eq!(ctx.flag("verbose"), true); assert!(ctx.flag("verbose"));
}) })
.run_with(vec!["-v".into()]) .run_with(vec!["-v".into()])
.unwrap(); .unwrap();
} }
#[test]
fn test_combined_short_flags() {
App::new()
.opt(Opt::flag("thingA").short("a").long("a"))
.opt(Opt::flag("thingB").short("b").long("b"))
.opt(Opt::flag("thingC").short("c").long("c"))
.handler(|_, ctx, _| {
assert!(ctx.flag("thingA"));
assert!(ctx.flag("thingB"));
assert!(ctx.flag("thingC"));
})
.run_with(vec!["-abc".into()])
.unwrap();
}
#[test] #[test]
fn test_invalid_long_flag() { fn test_invalid_long_flag() {
let r = App::new() let r = App::new()
@ -383,7 +454,7 @@ mod tests {
.env_prefix("ARKHAM") .env_prefix("ARKHAM")
.opt(Opt::flag("thing").short("-t").long("--t")) .opt(Opt::flag("thing").short("-t").long("--t"))
.handler(|_, ctx, _| { .handler(|_, ctx, _| {
assert_eq!(ctx.flag("thing"), true); assert!(ctx.flag("thing"));
}) })
.run_with(vec![]) .run_with(vec![])
.unwrap(); .unwrap();

View File

@ -1,3 +1,5 @@
use std::fmt::Debug;
use crate::{ use crate::{
context::Context, context::Context,
opt::{self, Opt}, opt::{self, Opt},
@ -16,6 +18,12 @@ pub struct Command {
pub short_desc: Option<String>, pub short_desc: Option<String>,
} }
impl Debug for Command {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Command").field("name", &self.name).finish()
}
}
impl Command { impl Command {
pub fn new<T: Into<String>>(name: T) -> Self { pub fn new<T: Into<String>>(name: T) -> Self {
Command { Command {
@ -61,7 +69,7 @@ pub(crate) fn print_command_help(cmd: &Command, args: &[String]) {
} }
} }
vox::print(""); vox::print("");
if let Some(desc) = cmd.long_desc.as_ref().or(cmd.short_desc.as_ref()) { if let Some(desc) = cmd.long_desc.as_ref().or_else(|| cmd.short_desc.as_ref()) {
if cmd.name != "root" { if cmd.name != "root" {
vox::header(&cmd.name.to_uppercase()); vox::header(&cmd.name.to_uppercase());
} }
@ -79,7 +87,7 @@ pub(crate) fn print_command_help(cmd: &Command, args: &[String]) {
} }
if !cmd.commands.is_empty() { if !cmd.commands.is_empty() {
vox::header("Subcommands"); vox::header("Commands");
vox::description_list( vox::description_list(
cmd.commands cmd.commands
.iter() .iter()
@ -87,4 +95,5 @@ pub(crate) fn print_command_help(cmd: &Command, args: &[String]) {
.collect(), .collect(),
) )
} }
std::process::exit(1);
} }

View File

@ -1,7 +1,7 @@
use crate::{Command, Opt}; use crate::{Command, Opt};
use std::{collections::BTreeMap, env}; use std::{collections::BTreeMap, env};
#[derive(Clone, Debug)] #[derive(Clone, Debug, Default)]
pub struct Map(BTreeMap<String, ContextValue>); pub struct Map(BTreeMap<String, ContextValue>);
impl Map { impl Map {
@ -102,9 +102,9 @@ impl ContextValue {
pub fn parse_string(value: String, kind: ValueKind) -> Option<Self> { pub fn parse_string(value: String, kind: ValueKind) -> Option<Self> {
match kind { match kind {
ValueKind::String => Some(Self::String(value)), ValueKind::String => Some(Self::String(value)),
ValueKind::Integer => value.parse::<i64>().ok().map(|v| Self::Integer(v)), ValueKind::Integer => value.parse::<i64>().ok().map(Self::Integer),
ValueKind::Float => value.parse::<f64>().ok().map(|v| Self::Float(v)), ValueKind::Float => value.parse::<f64>().ok().map(Self::Float),
ValueKind::Bool => value.parse::<bool>().ok().map(|v| Self::Bool(v)), ValueKind::Bool => value.parse::<bool>().ok().map(Self::Bool),
ValueKind::Array => None, ValueKind::Array => None,
ValueKind::Map => None, ValueKind::Map => None,
} }
@ -136,7 +136,7 @@ impl From<bool> for ContextValue {
} }
pub struct Context { pub struct Context {
pub(crate) cmd: Command, pub cmd: Command,
pub(crate) config_data: Map, pub(crate) config_data: Map,
pub(crate) env_prefix: Option<String>, pub(crate) env_prefix: Option<String>,
pub(crate) env_enabled: bool, pub(crate) env_enabled: bool,
@ -166,14 +166,14 @@ impl Context {
} }
fn get_env_value(&self, name: &str) -> Option<String> { fn get_env_value(&self, name: &str) -> Option<String> {
if self.env_enabled == false { if !self.env_enabled {
return None; return None;
} }
let name = self let name = self
.env_prefix .env_prefix
.as_ref() .as_ref()
.map(|pre| format!("{}_{}", pre, name)) .map(|pre| format!("{}_{}", pre, name))
.unwrap_or(name.to_string()); .unwrap_or_else(|| name.to_string());
env::var(name).ok() env::var(name).ok()
} }
@ -182,7 +182,7 @@ impl Context {
self.config_data self.config_data
.get(name) .get(name)
.cloned() .cloned()
.or(self.get_env_value(name).map(|v| ContextValue::String(v))) .or_else(|| self.get_env_value(name).map(ContextValue::String))
} }
/// Returns a string for the field if it is a string /// Returns a string for the field if it is a string
@ -240,7 +240,7 @@ impl Context {
/// Can be used to display the automatic help message for the current command. /// Can be used to display the automatic help message for the current command.
pub fn display_help(&self) { pub fn display_help(&self) {
crate::command::print_command_help(&self.cmd, &vec![]) crate::command::print_command_help(&self.cmd, &[])
} }
pub(crate) fn load_config_file(&mut self, filename: &str) { pub(crate) fn load_config_file(&mut self, filename: &str) {
@ -340,7 +340,7 @@ mod tests {
App::new() App::new()
.opt(Opt::flag("thing").short("-t").long("--t")) .opt(Opt::flag("thing").short("-t").long("--t"))
.handler(|_, ctx, _| { .handler(|_, ctx, _| {
assert_eq!(ctx.flag("thing"), true); assert!(ctx.flag("thing"));
}) })
.run_with(vec![]) .run_with(vec![])
.unwrap(); .unwrap();

View File

@ -1,9 +1,14 @@
#[macro_use]
mod macros;
mod app; mod app;
mod command; mod command;
mod context; mod context;
mod opt; mod opt;
pub use console;
pub mod vox; pub mod vox;
pub use app::*; pub use app::*;
pub use command::*; pub use command::*;
pub use context::*; pub use context::*;
pub use opt::*; pub use opt::*;
pub use vox::*;

8
src/macros.rs Normal file
View File

@ -0,0 +1,8 @@
#[macro_export]
macro_rules! app {
() => {
App::default()
.version(env!("CARGO_PKG_VERSION"))
.name(env!("CARGO_PKG_NAME"))
};
}

View File

@ -6,8 +6,8 @@ pub enum OptError {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Opt { pub struct Opt {
pub name: String, pub name: String,
pub short: String, pub short: Option<String>,
pub long: String, pub long: Option<String>,
pub desc: Option<String>, pub desc: Option<String>,
pub(crate) kind: OptKind, pub(crate) kind: OptKind,
} }
@ -23,8 +23,8 @@ impl Opt {
pub fn flag(name: &str) -> Self { pub fn flag(name: &str) -> Self {
Self { Self {
name: name.into(), name: name.into(),
short: "".into(), short: None,
long: "".into(), long: None,
kind: OptKind::Flag, kind: OptKind::Flag,
desc: None, desc: None,
} }
@ -40,8 +40,8 @@ impl Opt {
pub fn scalar(name: &str) -> Self { pub fn scalar(name: &str) -> Self {
Self { Self {
name: name.into(), name: name.into(),
short: "".into(), short: None,
long: "".into(), long: None,
kind: OptKind::String, kind: OptKind::String,
desc: None, desc: None,
} }
@ -55,7 +55,7 @@ impl Opt {
/// App::new().opt(Opt::scalar("user").short("u").long("user")); /// App::new().opt(Opt::scalar("user").short("u").long("user"));
///``` ///```
pub fn short(mut self, short: &str) -> Self { pub fn short(mut self, short: &str) -> Self {
self.short = short.into(); self.short = Some(short.into());
self self
} }
@ -67,7 +67,7 @@ impl Opt {
/// App::new().opt(Opt::scalar("user").short("u").long("user")); /// App::new().opt(Opt::scalar("user").short("u").long("user"));
///``` ///```
pub fn long(mut self, long: &str) -> Self { pub fn long(mut self, long: &str) -> Self {
self.long = long.into(); self.long = Some(long.into());
self self
} }
@ -92,10 +92,32 @@ impl Opt {
pub(crate) fn usage(&self) -> String { pub(crate) fn usage(&self) -> String {
match self.kind { match self.kind {
OptKind::Flag => { OptKind::Flag => {
format!("-{}, --{}", self.short, self.long) let short = self
.short
.as_ref()
.map(|s| format!("-{}", s))
.unwrap_or_else(String::new);
let long = self
.long
.as_ref()
.map(|s| format!("--{}", s))
.unwrap_or_else(String::new);
vec![short, long].join(",").trim_matches(',').to_string()
} }
OptKind::String => { OptKind::String => {
format!("-{} [value], --{} [value]", self.short, self.long) let short = self
.short
.as_ref()
.map(|s| format!("-{} [value]", s))
.unwrap_or_else(String::new);
let long = self
.long
.as_ref()
.map(|s| format!("--{} [value]", s))
.unwrap_or_else(String::new);
vec![short, long].join(",").trim_matches(',').to_string()
} }
} }
} }

View File

@ -2,6 +2,41 @@ use console::style;
use std::collections::HashMap; use std::collections::HashMap;
use std::fmt::Display; use std::fmt::Display;
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy)]
pub enum Color {
Black,
Red,
Green,
Yellow,
Blue,
Magenta,
Cyan,
White,
Color256(u8),
}
impl From<Color> for console::Color {
fn from(fr: Color) -> Self {
match fr {
Color::Black => Self::Black,
Color::Red => Self::Red,
Color::Green => Self::Green,
Color::Yellow => Self::Yellow,
Color::Blue => Self::Blue,
Color::Magenta => Self::Magenta,
Color::Cyan => Self::Cyan,
Color::White => Self::White,
Color::Color256(v) => Self::Color256(v),
}
}
}
pub fn labeled(color: Color, label: &str, msg: &str) {
let label = format!("[{}]", label);
println!("{} {}", console::style(label).fg(color.into()).bold(), msg);
}
pub fn message<T: Display>(str: T) { pub fn message<T: Display>(str: T) {
println!("{}", style(str).white().bold()); println!("{}", style(str).white().bold());
} }
@ -20,16 +55,38 @@ pub fn note<T: Display>(str: T) {
} }
pub fn description_list(list: HashMap<String, String>) { pub fn description_list(list: HashMap<String, String>) {
let max_length = list.keys().map(|v| v.len()).max().unwrap_or(10) + 3; let mut lines = list.into_iter().collect::<Vec<(String, String)>>();
for (name, desc) in list { let key_max_length = lines.iter().map(|(v, _)| v.len()).max().unwrap_or(10) + 3;
let spaced_name = format!("{:width$}", name, width = max_length); let desc_max_length = 80 - key_max_length;
println!("{}{}", style(spaced_name).bold(), style(desc).dim()) let indent_string = " ".repeat(key_max_length);
} lines.sort_by(|(a, _), (b, _)| a.cmp(b));
let output = lines.iter().fold(String::new(), |output, (name, desc)| {
let spaced_key = format!("{:width$}", name, width = key_max_length);
let mut desc_lines: Vec<String> = textwrap::fill(desc, desc_max_length)
.split('\n')
.map(|s| s.to_string())
.collect();
desc_lines.reverse();
let first_line = desc_lines.pop().unwrap_or_else(String::new);
desc_lines.reverse();
output
+ &format!("{}{}", style(spaced_key).bold(), style(first_line).dim())
+ "\n"
+ &desc_lines
.iter()
.map(|s| indent_string.clone() + s + "\n")
.collect::<String>()
});
println!("{}", output);
} }
pub fn print<T: Display>(s: T) { pub fn print<T: Display>(s: T) {
println!("{}", s); println!("{}", s);
} }
pub fn error<T: Display>(s: T) {
println!("{}", style(s).red().bold());
}
#[cfg(test)] #[cfg(test)]
mod tests {} mod tests {}