Skip to content

Commit

Permalink
feat(HELP): implements optional colored help messages
Browse files Browse the repository at this point in the history
To enable, ensure `clap` is compiled with `color` cargo feature.

Then in code use `AppSettings::ColoredHelp`

Closes #483
  • Loading branch information
kbknapp committed Apr 18, 2016
1 parent dafb1b9 commit abc8f66
Show file tree
Hide file tree
Showing 2 changed files with 110 additions and 66 deletions.
111 changes: 67 additions & 44 deletions src/app/help.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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.
Expand Down Expand Up @@ -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.
///
Expand All @@ -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,
}
}

Expand All @@ -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.
Expand Down Expand Up @@ -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, Warning)
} else if arg.has_switch() {
write!(self.writer, "{}", TAB)
} else {
Expand All @@ -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));
if arg.short().is_some() {
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));
if arg.short().is_some() {
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));
}
Expand All @@ -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)) {
Expand Down Expand Up @@ -394,21 +405,33 @@ impl<'a> Help<'a> {
if let Some(ref pv) = a.default_val() {
debugln!("Writing defaults");
return format!(" [default: {}] {}",
pv,
if self.hide_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() {
format!(" [values: {}]", pv.join(", "))
} else {
} 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()
}
});
}
});
} else if !self.hide_pv {
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()
Expand All @@ -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;
Expand All @@ -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;
}
Expand All @@ -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;
}
Expand All @@ -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));
}

Expand Down Expand Up @@ -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(())
}
Expand All @@ -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(&[])));

Expand Down
65 changes: 43 additions & 22 deletions src/app/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
}

Expand Down Expand Up @@ -71,6 +72,7 @@ impl AppFlags {
AllowLeadingHyphen => LEADING_HYPHEN,
HidePossibleValuesInHelp => NO_POS_VALUES,
NextLineHelp => NEXT_LINE_HELP,
ColoredHelp => COLORED_HELP,
DeriveDisplayOrder => DERIVE_DISP_ORDER
}
}
Expand Down Expand Up @@ -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)]
Expand Down Expand Up @@ -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()),
}
}
Expand All @@ -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());
}
Expand Down

0 comments on commit abc8f66

Please sign in to comment.