feat(HELP): implements optional colored help messages

To enable, ensure `clap` is compiled with `color` cargo feature.

Then in code use `AppSettings::ColoredHelp`

Closes #483
This commit is contained in:
Kevin K 2016-04-17 23:45:44 -07:00
parent c4ffd2a9ce
commit 770397bcb2
2 changed files with 110 additions and 66 deletions

View file

@ -12,6 +12,7 @@ use errors::{Error, Result as ClapResult};
use args::{AnyArg, ArgSettings, DispOrder}; use args::{AnyArg, ArgSettings, DispOrder};
use app::{App, AppSettings}; use app::{App, AppSettings};
use app::parser::Parser; use app::parser::Parser;
use fmt::Format;
use term; use term;
@ -30,7 +31,6 @@ fn str_width(s: &str) -> usize {
UnicodeWidthStr::width(s) UnicodeWidthStr::width(s)
} }
const TAB: &'static str = " "; const TAB: &'static str = " ";
// These are just convenient traits to make the code easier to read. // These are just convenient traits to make the code easier to read.
@ -58,6 +58,22 @@ impl<'b, 'c> DispOrder for App<'b, 'c> {
} }
} }
macro_rules! color {
($_self:ident, $nc:expr, $c:ident) => {
if $_self.color {
write!($_self.writer, "{}", Format::$c($nc))
} else {
write!($_self.writer, "{}", $nc)
}
};
($_self:ident, $nc:expr, $i:expr, $c:ident) => {
if $_self.color {
write!($_self.writer, "{}", Format::$c(format!($nc, $i)))
} else {
write!($_self.writer, $nc, $i)
}
};
}
/// CLAP Help Writer. /// CLAP Help Writer.
/// ///
@ -67,17 +83,19 @@ pub struct Help<'a> {
next_line_help: bool, next_line_help: bool,
hide_pv: bool, hide_pv: bool,
term_w: Option<usize>, term_w: Option<usize>,
color: bool,
} }
// Public Functions // Public Functions
impl<'a> Help<'a> { impl<'a> Help<'a> {
/// Create a new Help instance. /// Create a new Help instance.
pub fn new(w: &'a mut Write, next_line_help: bool, hide_pv: bool) -> Self { pub fn new(w: &'a mut Write, next_line_help: bool, hide_pv: bool, color: bool) -> Self {
Help { Help {
writer: w, writer: w,
next_line_help: next_line_help, next_line_help: next_line_help,
hide_pv: hide_pv, hide_pv: hide_pv,
term_w: term::dimensions().map(|(w, _)| w), term_w: term::dimensions().map(|(w, _)| w),
color: color,
} }
} }
@ -92,7 +110,8 @@ impl<'a> Help<'a> {
pub fn write_parser_help(w: &'a mut Write, parser: &Parser) -> ClapResult<()> { pub fn write_parser_help(w: &'a mut Write, parser: &Parser) -> ClapResult<()> {
let nlh = parser.is_set(AppSettings::NextLineHelp); let nlh = parser.is_set(AppSettings::NextLineHelp);
let hide_v = parser.is_set(AppSettings::HidePossibleValuesInHelp); let hide_v = parser.is_set(AppSettings::HidePossibleValuesInHelp);
Self::new(w, nlh, hide_v).write_help(&parser) let color = parser.is_set(AppSettings::ColoredHelp);
Self::new(w, nlh, hide_v, color).write_help(&parser)
} }
/// Writes the parser help to the wrapped stream. /// Writes the parser help to the wrapped stream.
@ -176,7 +195,7 @@ impl<'a> Help<'a> {
debugln!("fn=short;"); debugln!("fn=short;");
try!(write!(self.writer, "{}", TAB)); try!(write!(self.writer, "{}", TAB));
if let Some(s) = arg.short() { if let Some(s) = arg.short() {
write!(self.writer, "-{}", s) color!(self, "-{}", s, Good)
} else if arg.has_switch() { } else if arg.has_switch() {
write!(self.writer, "{}", TAB) write!(self.writer, "{}", TAB)
} else { } else {
@ -192,26 +211,18 @@ impl<'a> Help<'a> {
} }
if arg.takes_value() { if arg.takes_value() {
if let Some(l) = arg.long() { if let Some(l) = arg.long() {
try!(write!(self.writer,
"{}--{}",
if arg.short().is_some() { if arg.short().is_some() {
", " try!(write!(self.writer, ", "));
} else { }
"" try!(color!(self, "--{}", l, Good))
},
l));
} }
try!(write!(self.writer, " ")); try!(write!(self.writer, " "));
} else { } else {
if let Some(l) = arg.long() { if let Some(l) = arg.long() {
try!(write!(self.writer,
"{}--{}",
if arg.short().is_some() { if arg.short().is_some() {
", " try!(write!(self.writer, ", "));
} else { }
"" try!(color!(self, "--{}", l, Good));
},
l));
if !self.next_line_help || !arg.is_set(ArgSettings::NextLineHelp) { if !self.next_line_help || !arg.is_set(ArgSettings::NextLineHelp) {
write_nspaces!(self.writer, (longest + 4) - (l.len() + 2)); write_nspaces!(self.writer, (longest + 4) - (l.len() + 2));
} }
@ -234,27 +245,27 @@ impl<'a> Help<'a> {
if let Some(ref vec) = arg.val_names() { if let Some(ref vec) = arg.val_names() {
let mut it = vec.iter().peekable(); let mut it = vec.iter().peekable();
while let Some((_, val)) = it.next() { while let Some((_, val)) = it.next() {
try!(write!(self.writer, "<{}>", val)); try!(color!(self, "<{}>", val, Good));
if it.peek().is_some() { if it.peek().is_some() {
try!(write!(self.writer, " ")); try!(write!(self.writer, " "));
} }
} }
let num = vec.len(); let num = vec.len();
if arg.is_set(ArgSettings::Multiple) && num == 1 { if arg.is_set(ArgSettings::Multiple) && num == 1 {
try!(write!(self.writer, "...")); try!(color!(self, "...", Good));
} }
} else if let Some(num) = arg.num_vals() { } else if let Some(num) = arg.num_vals() {
let mut it = (0..num).peekable(); let mut it = (0..num).peekable();
while let Some(_) = it.next() { while let Some(_) = it.next() {
try!(write!(self.writer, "<{}>", arg.name())); try!(color!(self, "<{}>", arg.name(), Good));
if it.peek().is_some() { if it.peek().is_some() {
try!(write!(self.writer, " ")); try!(write!(self.writer, " "));
} }
} }
} else if arg.has_switch() { } else if arg.has_switch() {
try!(write!(self.writer, "<{}>", arg.name())); try!(color!(self, "<{}>", arg.name(), Good));
} else { } else {
try!(write!(self.writer, "{}", arg)); try!(color!(self, "{}", arg, Good));
} }
if arg.has_switch() { if arg.has_switch() {
if !(self.next_line_help || arg.is_set(ArgSettings::NextLineHelp)) { if !(self.next_line_help || arg.is_set(ArgSettings::NextLineHelp)) {
@ -394,12 +405,20 @@ impl<'a> Help<'a> {
if let Some(ref pv) = a.default_val() { if let Some(ref pv) = a.default_val() {
debugln!("Writing defaults"); debugln!("Writing defaults");
return format!(" [default: {}] {}", return format!(" [default: {}] {}",
pv, if self.color {
format!("{}", Format::Good(pv))
} else {
format!("{}", pv)
},
if self.hide_pv { if self.hide_pv {
"".into() "".into()
} else { } else {
if let Some(ref pv) = a.possible_vals() { if let Some(ref pv) = a.possible_vals() {
if self.color {
format!(" [values: {}]", pv.iter().map(|v| format!("{}", Format::Good(v))).collect::<Vec<_>>().join(", "))
} else {
format!(" [values: {}]", pv.join(", ")) format!(" [values: {}]", pv.join(", "))
}
} else { } else {
"".into() "".into()
} }
@ -408,7 +427,11 @@ impl<'a> Help<'a> {
debugln!("Writing values"); debugln!("Writing values");
if let Some(ref pv) = a.possible_vals() { if let Some(ref pv) = a.possible_vals() {
debugln!("Possible vals...{:?}", pv); debugln!("Possible vals...{:?}", pv);
return format!(" [values: {}]", pv.join(", ")); return if self.color {
format!(" [values: {}]", pv.iter().map(|v| format!("{}", Format::Good(v))).collect::<Vec<_>>().join(", "))
} else {
format!(" [values: {}]", pv.join(", "))
};
} }
} }
String::new() String::new()
@ -435,12 +458,12 @@ impl<'a> Help<'a> {
let opts_flags = parser.iter_flags() let opts_flags = parser.iter_flags()
.map(as_arg_trait) .map(as_arg_trait)
.chain(parser.iter_opts().map(as_arg_trait)); .chain(parser.iter_opts().map(as_arg_trait));
try!(write!(self.writer, "OPTIONS:\n")); try!(color!(self, "OPTIONS:\n", Warning));
try!(self.write_args(opts_flags)); try!(self.write_args(opts_flags));
first = false; first = false;
} else { } else {
if flags { if flags {
try!(write!(self.writer, "FLAGS:\n")); try!(color!(self, "FLAGS:\n", Warning));
try!(self.write_args(parser.iter_flags() try!(self.write_args(parser.iter_flags()
.map(as_arg_trait))); .map(as_arg_trait)));
first = false; first = false;
@ -449,7 +472,7 @@ impl<'a> Help<'a> {
if !first { if !first {
try!(self.writer.write(b"\n")); try!(self.writer.write(b"\n"));
} }
try!(write!(self.writer, "OPTIONS:\n")); try!(color!(self, "OPTIONS:\n", Warning));
try!(self.write_args(parser.iter_opts().map(as_arg_trait))); try!(self.write_args(parser.iter_opts().map(as_arg_trait)));
first = false; first = false;
} }
@ -459,7 +482,7 @@ impl<'a> Help<'a> {
if !first { if !first {
try!(self.writer.write(b"\n")); try!(self.writer.write(b"\n"));
} }
try!(write!(self.writer, "ARGS:\n")); try!(color!(self, "ARGS:\n", Warning));
try!(self.write_args_unsorted(parser.iter_positionals().map(as_arg_trait))); try!(self.write_args_unsorted(parser.iter_positionals().map(as_arg_trait)));
first = false; first = false;
} }
@ -468,7 +491,7 @@ impl<'a> Help<'a> {
if !first { if !first {
try!(self.writer.write(b"\n")); try!(self.writer.write(b"\n"));
} }
try!(write!(self.writer, "SUBCOMMANDS:\n")); try!(color!(self, "SUBCOMMANDS:\n", Warning));
try!(self.write_subcommands(&parser)); try!(self.write_subcommands(&parser));
} }
@ -505,12 +528,12 @@ impl<'a> Help<'a> {
if let Some(bn) = parser.meta.bin_name.as_ref() { if let Some(bn) = parser.meta.bin_name.as_ref() {
if bn.contains(' ') { if bn.contains(' ') {
// Incase we're dealing with subcommands i.e. git mv is translated to git-mv // Incase we're dealing with subcommands i.e. git mv is translated to git-mv
try!(write!(self.writer, "{}", bn.replace(" ", "-"))) try!(color!(self, bn.replace(" ", "-"), Good))
} else { } else {
try!(write!(self.writer, "{}", &parser.meta.name[..])) try!(color!(self, &parser.meta.name[..], Good))
} }
} else { } else {
try!(write!(self.writer, "{}", &parser.meta.name[..])) try!(color!(self, &parser.meta.name[..], Good))
} }
Ok(()) Ok(())
} }
@ -530,8 +553,8 @@ impl<'a> Help<'a> {
try!(write!(self.writer, "{}\n", about)); try!(write!(self.writer, "{}\n", about));
} }
try!(write!(self.writer, try!(color!(self, "\nUSAGE:", Warning));
"\nUSAGE:\n{}{}\n\n", try!(write!(self.writer, "\n{}{}\n\n",
TAB, TAB,
parser.create_usage_no_title(&[]))); parser.create_usage_no_title(&[])));

View file

@ -3,28 +3,29 @@ use std::ascii::AsciiExt;
bitflags! { bitflags! {
flags Flags: u32 { flags Flags: u32 {
const SC_NEGATE_REQS = 0b0000000000000000000001, const SC_NEGATE_REQS = 0b00000000000000000000001,
const SC_REQUIRED = 0b0000000000000000000010, const SC_REQUIRED = 0b00000000000000000000010,
const A_REQUIRED_ELSE_HELP = 0b0000000000000000000100, const A_REQUIRED_ELSE_HELP = 0b00000000000000000000100,
const GLOBAL_VERSION = 0b0000000000000000001000, const GLOBAL_VERSION = 0b00000000000000000001000,
const VERSIONLESS_SC = 0b0000000000000000010000, const VERSIONLESS_SC = 0b00000000000000000010000,
const UNIFIED_HELP = 0b0000000000000000100000, const UNIFIED_HELP = 0b00000000000000000100000,
const WAIT_ON_ERROR = 0b0000000000000001000000, const WAIT_ON_ERROR = 0b00000000000000001000000,
const SC_REQUIRED_ELSE_HELP= 0b0000000000000010000000, const SC_REQUIRED_ELSE_HELP= 0b00000000000000010000000,
const NEEDS_LONG_HELP = 0b0000000000000100000000, const NEEDS_LONG_HELP = 0b00000000000000100000000,
const NEEDS_LONG_VERSION = 0b0000000000001000000000, const NEEDS_LONG_VERSION = 0b00000000000001000000000,
const NEEDS_SC_HELP = 0b0000000000010000000000, const NEEDS_SC_HELP = 0b00000000000010000000000,
const DISABLE_VERSION = 0b0000000000100000000000, const DISABLE_VERSION = 0b00000000000100000000000,
const HIDDEN = 0b0000000001000000000000, const HIDDEN = 0b00000000001000000000000,
const TRAILING_VARARG = 0b0000000010000000000000, const TRAILING_VARARG = 0b00000000010000000000000,
const NO_BIN_NAME = 0b0000000100000000000000, const NO_BIN_NAME = 0b00000000100000000000000,
const ALLOW_UNK_SC = 0b0000001000000000000000, const ALLOW_UNK_SC = 0b00000001000000000000000,
const UTF8_STRICT = 0b0000010000000000000000, const UTF8_STRICT = 0b00000010000000000000000,
const UTF8_NONE = 0b0000100000000000000000, const UTF8_NONE = 0b00000100000000000000000,
const LEADING_HYPHEN = 0b0001000000000000000000, const LEADING_HYPHEN = 0b00001000000000000000000,
const NO_POS_VALUES = 0b0010000000000000000000, const NO_POS_VALUES = 0b00010000000000000000000,
const NEXT_LINE_HELP = 0b0100000000000000000000, const NEXT_LINE_HELP = 0b00100000000000000000000,
const DERIVE_DISP_ORDER = 0b1000000000000000000000, const DERIVE_DISP_ORDER = 0b01000000000000000000000,
const COLORED_HELP = 0b10000000000000000000000,
} }
} }
@ -71,6 +72,7 @@ impl AppFlags {
AllowLeadingHyphen => LEADING_HYPHEN, AllowLeadingHyphen => LEADING_HYPHEN,
HidePossibleValuesInHelp => NO_POS_VALUES, HidePossibleValuesInHelp => NO_POS_VALUES,
NextLineHelp => NEXT_LINE_HELP, NextLineHelp => NEXT_LINE_HELP,
ColoredHelp => COLORED_HELP,
DeriveDisplayOrder => DERIVE_DISP_ORDER DeriveDisplayOrder => DERIVE_DISP_ORDER
} }
} }
@ -446,6 +448,23 @@ pub enum AppSettings {
/// .get_matches(); /// .get_matches();
/// ``` /// ```
DeriveDisplayOrder, DeriveDisplayOrder,
/// Uses colorized help messages.
///
/// **NOTE:** Must be compiled with the `color` cargo feature
///
/// # Platform Specific
///
/// This setting only applies to Unix, Linux, and OSX (i.e. non-Windows platforms)
///
/// # Examples
///
/// ```no_run
/// # use clap::{App, Arg, SubCommand, AppSettings};
/// App::new("myprog")
/// .setting(AppSettings::ColoredHelp)
/// .get_matches();
/// ```
ColoredHelp,
#[doc(hidden)] #[doc(hidden)]
NeedsLongVersion, NeedsLongVersion,
#[doc(hidden)] #[doc(hidden)]
@ -478,6 +497,7 @@ impl FromStr for AppSettings {
"hidepossiblevaluesinhelp" => Ok(AppSettings::HidePossibleValuesInHelp), "hidepossiblevaluesinhelp" => Ok(AppSettings::HidePossibleValuesInHelp),
"nextlinehelp" => Ok(AppSettings::NextLineHelp), "nextlinehelp" => Ok(AppSettings::NextLineHelp),
"derivedisplayorder" => Ok(AppSettings::DeriveDisplayOrder), "derivedisplayorder" => Ok(AppSettings::DeriveDisplayOrder),
"coloredhelp" => Ok(AppSettings::ColoredHelp),
_ => Err("unknown AppSetting, cannot convert from str".to_owned()), _ => Err("unknown AppSetting, cannot convert from str".to_owned()),
} }
} }
@ -504,6 +524,7 @@ mod test {
assert_eq!("allowinvalidutf8".parse::<AppSettings>().unwrap(), AppSettings::AllowInvalidUtf8); assert_eq!("allowinvalidutf8".parse::<AppSettings>().unwrap(), AppSettings::AllowInvalidUtf8);
assert_eq!("allowleadinghyphen".parse::<AppSettings>().unwrap(), AppSettings::AllowLeadingHyphen); assert_eq!("allowleadinghyphen".parse::<AppSettings>().unwrap(), AppSettings::AllowLeadingHyphen);
assert_eq!("hidepossiblevaluesinhelp".parse::<AppSettings>().unwrap(), AppSettings::HidePossibleValuesInHelp); assert_eq!("hidepossiblevaluesinhelp".parse::<AppSettings>().unwrap(), AppSettings::HidePossibleValuesInHelp);
assert_eq!("coloredhelp".parse::<AppSettings>().unwrap(), AppSettings::ColoredHelp);
assert_eq!("hidden".parse::<AppSettings>().unwrap(), AppSettings::Hidden); assert_eq!("hidden".parse::<AppSettings>().unwrap(), AppSettings::Hidden);
assert!("hahahaha".parse::<AppSettings>().is_err()); assert!("hahahaha".parse::<AppSettings>().is_err());
} }