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 app::{App, AppSettings};
use app::parser::Parser;
use fmt::Format;
use term;
@ -30,7 +31,6 @@ fn str_width(s: &str) -> usize {
UnicodeWidthStr::width(s)
}
const TAB: &'static str = " ";
// 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.
///
@ -67,17 +83,19 @@ pub struct Help<'a> {
next_line_help: bool,
hide_pv: bool,
term_w: Option<usize>,
color: bool,
}
// Public Functions
impl<'a> Help<'a> {
/// 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 {
writer: w,
next_line_help: next_line_help,
hide_pv: hide_pv,
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<()> {
let nlh = parser.is_set(AppSettings::NextLineHelp);
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.
@ -176,7 +195,7 @@ impl<'a> Help<'a> {
debugln!("fn=short;");
try!(write!(self.writer, "{}", TAB));
if let Some(s) = arg.short() {
write!(self.writer, "-{}", s)
color!(self, "-{}", s, Good)
} else if arg.has_switch() {
write!(self.writer, "{}", TAB)
} else {
@ -192,26 +211,18 @@ impl<'a> Help<'a> {
}
if arg.takes_value() {
if let Some(l) = arg.long() {
try!(write!(self.writer,
"{}--{}",
if arg.short().is_some() {
", "
} else {
""
},
l));
try!(write!(self.writer, ", "));
}
try!(color!(self, "--{}", l, Good))
}
try!(write!(self.writer, " "));
} else {
if let Some(l) = arg.long() {
try!(write!(self.writer,
"{}--{}",
if arg.short().is_some() {
", "
} else {
""
},
l));
try!(write!(self.writer, ", "));
}
try!(color!(self, "--{}", l, Good));
if !self.next_line_help || !arg.is_set(ArgSettings::NextLineHelp) {
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() {
let mut it = vec.iter().peekable();
while let Some((_, val)) = it.next() {
try!(write!(self.writer, "<{}>", val));
try!(color!(self, "<{}>", val, Good));
if it.peek().is_some() {
try!(write!(self.writer, " "));
}
}
let num = vec.len();
if arg.is_set(ArgSettings::Multiple) && num == 1 {
try!(write!(self.writer, "..."));
try!(color!(self, "...", Good));
}
} else if let Some(num) = arg.num_vals() {
let mut it = (0..num).peekable();
while let Some(_) = it.next() {
try!(write!(self.writer, "<{}>", arg.name()));
try!(color!(self, "<{}>", arg.name(), Good));
if it.peek().is_some() {
try!(write!(self.writer, " "));
}
}
} else if arg.has_switch() {
try!(write!(self.writer, "<{}>", arg.name()));
try!(color!(self, "<{}>", arg.name(), Good));
} else {
try!(write!(self.writer, "{}", arg));
try!(color!(self, "{}", arg, Good));
}
if arg.has_switch() {
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() {
debugln!("Writing defaults");
return format!(" [default: {}] {}",
pv,
if self.color {
format!("{}", Format::Good(pv))
} else {
format!("{}", pv)
},
if self.hide_pv {
"".into()
} else {
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(", "))
}
} else {
"".into()
}
@ -408,7 +427,11 @@ impl<'a> Help<'a> {
debugln!("Writing values");
if let Some(ref pv) = a.possible_vals() {
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()
@ -435,12 +458,12 @@ impl<'a> Help<'a> {
let opts_flags = parser.iter_flags()
.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));
first = false;
} else {
if flags {
try!(write!(self.writer, "FLAGS:\n"));
try!(color!(self, "FLAGS:\n", Warning));
try!(self.write_args(parser.iter_flags()
.map(as_arg_trait)));
first = false;
@ -449,7 +472,7 @@ impl<'a> Help<'a> {
if !first {
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)));
first = false;
}
@ -459,7 +482,7 @@ impl<'a> Help<'a> {
if !first {
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)));
first = false;
}
@ -468,7 +491,7 @@ impl<'a> Help<'a> {
if !first {
try!(self.writer.write(b"\n"));
}
try!(write!(self.writer, "SUBCOMMANDS:\n"));
try!(color!(self, "SUBCOMMANDS:\n", Warning));
try!(self.write_subcommands(&parser));
}
@ -505,12 +528,12 @@ impl<'a> Help<'a> {
if let Some(bn) = parser.meta.bin_name.as_ref() {
if bn.contains(' ') {
// 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 {
try!(write!(self.writer, "{}", &parser.meta.name[..]))
try!(color!(self, &parser.meta.name[..], Good))
}
} else {
try!(write!(self.writer, "{}", &parser.meta.name[..]))
try!(color!(self, &parser.meta.name[..], Good))
}
Ok(())
}
@ -530,8 +553,8 @@ impl<'a> Help<'a> {
try!(write!(self.writer, "{}\n", about));
}
try!(write!(self.writer,
"\nUSAGE:\n{}{}\n\n",
try!(color!(self, "\nUSAGE:", Warning));
try!(write!(self.writer, "\n{}{}\n\n",
TAB,
parser.create_usage_no_title(&[])));

View file

@ -3,28 +3,29 @@ use std::ascii::AsciiExt;
bitflags! {
flags Flags: u32 {
const SC_NEGATE_REQS = 0b0000000000000000000001,
const SC_REQUIRED = 0b0000000000000000000010,
const A_REQUIRED_ELSE_HELP = 0b0000000000000000000100,
const GLOBAL_VERSION = 0b0000000000000000001000,
const VERSIONLESS_SC = 0b0000000000000000010000,
const UNIFIED_HELP = 0b0000000000000000100000,
const WAIT_ON_ERROR = 0b0000000000000001000000,
const SC_REQUIRED_ELSE_HELP= 0b0000000000000010000000,
const NEEDS_LONG_HELP = 0b0000000000000100000000,
const NEEDS_LONG_VERSION = 0b0000000000001000000000,
const NEEDS_SC_HELP = 0b0000000000010000000000,
const DISABLE_VERSION = 0b0000000000100000000000,
const HIDDEN = 0b0000000001000000000000,
const TRAILING_VARARG = 0b0000000010000000000000,
const NO_BIN_NAME = 0b0000000100000000000000,
const ALLOW_UNK_SC = 0b0000001000000000000000,
const UTF8_STRICT = 0b0000010000000000000000,
const UTF8_NONE = 0b0000100000000000000000,
const LEADING_HYPHEN = 0b0001000000000000000000,
const NO_POS_VALUES = 0b0010000000000000000000,
const NEXT_LINE_HELP = 0b0100000000000000000000,
const DERIVE_DISP_ORDER = 0b1000000000000000000000,
const SC_NEGATE_REQS = 0b00000000000000000000001,
const SC_REQUIRED = 0b00000000000000000000010,
const A_REQUIRED_ELSE_HELP = 0b00000000000000000000100,
const GLOBAL_VERSION = 0b00000000000000000001000,
const VERSIONLESS_SC = 0b00000000000000000010000,
const UNIFIED_HELP = 0b00000000000000000100000,
const WAIT_ON_ERROR = 0b00000000000000001000000,
const SC_REQUIRED_ELSE_HELP= 0b00000000000000010000000,
const NEEDS_LONG_HELP = 0b00000000000000100000000,
const NEEDS_LONG_VERSION = 0b00000000000001000000000,
const NEEDS_SC_HELP = 0b00000000000010000000000,
const DISABLE_VERSION = 0b00000000000100000000000,
const HIDDEN = 0b00000000001000000000000,
const TRAILING_VARARG = 0b00000000010000000000000,
const NO_BIN_NAME = 0b00000000100000000000000,
const ALLOW_UNK_SC = 0b00000001000000000000000,
const UTF8_STRICT = 0b00000010000000000000000,
const UTF8_NONE = 0b00000100000000000000000,
const LEADING_HYPHEN = 0b00001000000000000000000,
const NO_POS_VALUES = 0b00010000000000000000000,
const NEXT_LINE_HELP = 0b00100000000000000000000,
const DERIVE_DISP_ORDER = 0b01000000000000000000000,
const COLORED_HELP = 0b10000000000000000000000,
}
}
@ -71,6 +72,7 @@ impl AppFlags {
AllowLeadingHyphen => LEADING_HYPHEN,
HidePossibleValuesInHelp => NO_POS_VALUES,
NextLineHelp => NEXT_LINE_HELP,
ColoredHelp => COLORED_HELP,
DeriveDisplayOrder => DERIVE_DISP_ORDER
}
}
@ -446,6 +448,23 @@ pub enum AppSettings {
/// .get_matches();
/// ```
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)]
NeedsLongVersion,
#[doc(hidden)]
@ -478,6 +497,7 @@ impl FromStr for AppSettings {
"hidepossiblevaluesinhelp" => Ok(AppSettings::HidePossibleValuesInHelp),
"nextlinehelp" => Ok(AppSettings::NextLineHelp),
"derivedisplayorder" => Ok(AppSettings::DeriveDisplayOrder),
"coloredhelp" => Ok(AppSettings::ColoredHelp),
_ => 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!("allowleadinghyphen".parse::<AppSettings>().unwrap(), AppSettings::AllowLeadingHyphen);
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!("hahahaha".parse::<AppSettings>().is_err());
}