feat(Help Message): can auto wrap and aligning help text to term width

By default `clap` now automatically wraps and aligns help strings to the
term width. i.e.

```
    -o, --option <opt>    some really long help
text that should be auto aligned but isn't righ
t now
```

Now looks like this:

```
    -o, --option <opt>    some really long help
                          text that should be
                          auto aligned but isn't
                          right now
```

The wrapping also respects words, and wraps at spaces so as to not cut
words in the middle.

This requires the `libc` dep which is enabled (by default) with the
`wrap_help` cargo feature flag.

Closes #428
This commit is contained in:
Kevin K 2016-03-12 15:47:57 -05:00
parent df38346557
commit e36af02666
13 changed files with 413 additions and 171 deletions

View file

@ -14,16 +14,18 @@ keywords = ["argument", "command", "arg", "parser", "parse"]
[dependencies]
bitflags = "~0.4"
vec_map = "~0.6"
libc = { version = "~0.2.8", optional = true }
ansi_term = { version = "~0.7.2", optional = true }
strsim = { version = "~0.4.0", optional = true }
yaml-rust = { version = "~0.3", optional = true }
clippy = { version = "~0.0.48", optional = true }
[features]
default = ["suggestions", "color"]
default = ["suggestions", "color", "wrap_help"]
suggestions = ["strsim"]
color = ["ansi_term"]
yaml = ["yaml-rust"]
wrap_help = ["libc"]
lints = ["clippy", "nightly"]
nightly = [] # for building with nightly and unstable features
unstable = [] # for building with unstable features on stable Rust

View file

@ -47,7 +47,7 @@ In v2.1.1
#### Improvements
* **Documenation Examples**: The examples in the documentation have been vastly improved
* **Documentation Examples**: The examples in the documentation have been vastly improved
For full details, see [CHANGELOG.md](https://github.com/kbknapp/clap-rs/blob/master/CHANGELOG.md)
@ -433,14 +433,14 @@ default-features = false
# Cherry-pick the features you'd like to use
features = [ "suggestions", "color" ]
```
The following is a list of optional `clap` features:
* **"suggestions"**: Turns on the `Did you mean '--myoption' ?` feature for when users make typos.
* **"color"**: Turns on colored error messages. This feature only works on non-Windows OSs.
* **"lints"**: This is **not** included by default and should only be used while developing to run basic lints against changes. This can only be used on Rust nightly.
* **"suggestions"**: Turns on the `Did you mean '--myoption' ?` feature for when users make typos. (builds dependency `strsim`)
* **"color"**: Turns on colored error messages. This feature only works on non-Windows OSs. (builds dependency `ansi-term`)
* **"wrap_help"**: Automatically detects terminal width and wraps long help text lines with proper indentation alignment (builds dependency `libc`)
* **"lints"**: This is **not** included by default and should only be used while developing to run basic lints against changes. This can only be used on Rust nightly. (builds dependency `clippy`)
* **"debug"**: This is **not** included by default and should only be used while developing to display debugging information.
* **"yaml"**: This is **not** included by default. Enables building CLIs from YAML documents.
* **"yaml"**: This is **not** included by default. Enables building CLIs from YAML documents. (builds dependency `yaml-rust`)
* **"unstable"**: This is **not** included by default. Enables unstable features, unstable refers to whether or not they may change, not performance stability.
### Dependencies Tree
@ -472,7 +472,7 @@ Contributions are always welcome! And there is a multitude of ways in which you
Another really great way to help is if you find an interesting, or helpful way in which to use `clap`. You can either add it to the [examples/](examples) directory, or file an issue and tell me. I'm all about giving credit where credit is due :)
Please read [CONTRIBUTING.md](CONTRIBUTING.md) before you start contributing.
Please read [CONTRIBUTING.md](.github/CONTRIBUTING.md) before you start contributing.
### Running the tests
@ -553,6 +553,6 @@ As of 2.0.0 (From 1.x)
Old method names will be left around for several minor version bumps, or one major version bump.
As of 2.1.1:
As of 2.2.0:
* None!

View file

@ -1448,7 +1448,6 @@ impl<'a, 'b> Parser<'a, 'b> where 'a: 'b {
try!(write!(w, "\n"));
}
let tab = " ";
let longest = if !unified_help || longest_opt == 0 {
longest_flag
} else {
@ -1461,13 +1460,13 @@ impl<'a, 'b> Parser<'a, 'b> where 'a: 'b {
for f in self.flags.iter().filter(|f| !f.settings.is_set(ArgSettings::Hidden)) {
let btm = ord_m.entry(f.disp_ord).or_insert(BTreeMap::new());
let mut v = vec![];
try!(f.write_help(&mut v, tab, longest, nlh));
try!(f.write_help(&mut v, longest, nlh));
btm.insert(f.name, v);
}
for o in self.opts.iter().filter(|o| !o.settings.is_set(ArgSettings::Hidden)) {
let btm = ord_m.entry(o.disp_ord).or_insert(BTreeMap::new());
let mut v = vec![];
try!(o.write_help(&mut v, tab, longest, self.is_set(AppSettings::HidePossibleValuesInHelp), nlh));
try!(o.write_help(&mut v, longest, self.is_set(AppSettings::HidePossibleValuesInHelp), nlh));
btm.insert(o.name, v);
}
for (_, btm) in ord_m.into_iter() {
@ -1486,7 +1485,7 @@ impl<'a, 'b> Parser<'a, 'b> where 'a: 'b {
}
for (_, btm) in ord_m.into_iter() {
for (_, f) in btm.into_iter() {
try!(f.write_help(w, tab, longest, nlh));
try!(f.write_help(w, longest, nlh));
}
}
}
@ -1499,7 +1498,7 @@ impl<'a, 'b> Parser<'a, 'b> where 'a: 'b {
}
for (_, btm) in ord_m.into_iter() {
for (_, o) in btm.into_iter() {
try!(o.write_help(w, tab, longest_opt, self.is_set(AppSettings::HidePossibleValuesInHelp), nlh));
try!(o.write_help(w, longest_opt, self.is_set(AppSettings::HidePossibleValuesInHelp), nlh));
}
}
}
@ -1508,7 +1507,7 @@ impl<'a, 'b> Parser<'a, 'b> where 'a: 'b {
try!(write!(w, "\nARGS:\n"));
for v in self.positionals.values()
.filter(|p| !p.settings.is_set(ArgSettings::Hidden)) {
try!(v.write_help(w, tab, longest_pos, self.is_set(AppSettings::HidePossibleValuesInHelp), nlh));
try!(v.write_help(w, longest_pos, self.is_set(AppSettings::HidePossibleValuesInHelp), nlh));
}
}
if subcmds {
@ -1520,7 +1519,7 @@ impl<'a, 'b> Parser<'a, 'b> where 'a: 'b {
}
for (_, btm) in ord_m.into_iter() {
for (name, sc) in btm.into_iter() {
try!(write!(w, "{}{}", tab, name));
try!(write!(w, " {}", name));
write_spaces!((longest_sc + 4) - (name.len()), w);
if let Some(a) = sc.p.meta.about {
if a.contains("{n}") {

View file

@ -1,6 +1,8 @@
use std::rc::Rc;
use std::fmt::Display;
use vec_map::VecMap;
use args::settings::ArgSettings;
#[doc(hidden)]
@ -20,4 +22,8 @@ pub trait AnyArg<'n, 'e>: Display {
fn short(&self) -> Option<char>;
fn long(&self) -> Option<&'e str>;
fn val_delim(&self) -> Option<char>;
fn takes_value(&self) -> bool;
fn val_names(&self) -> Option<&VecMap<&'e str>>;
fn help(&self) -> Option<&'e str>;
fn default_val(&self) -> Option<&'n str>;
}

View file

@ -5,8 +5,10 @@ use std::io;
use std::rc::Rc;
use std::result::Result as StdResult;
use vec_map::VecMap;
use Arg;
use args::AnyArg;
use args::{AnyArg, HelpWriter};
use args::settings::{ArgFlags, ArgSettings};
#[derive(Debug)]
@ -47,9 +49,13 @@ impl<'n, 'e> FlagBuilder<'n, 'e> {
}
}
pub fn write_help<W: io::Write>(&self, w: &mut W, tab: &str, longest: usize, nlh: bool) -> io::Result<()> {
write_arg_help!(@flag self, w, tab, longest, nlh);
write!(w, "\n")
pub fn write_help<W: io::Write>(&self, w: &mut W, longest: usize, nlh: bool) -> io::Result<()> {
let hw = HelpWriter::new(
self,
longest,
nlh,
);
hw.write_to(w)
}
}
@ -98,8 +104,10 @@ impl<'n, 'e> AnyArg<'n, 'e> for FlagBuilder<'n, 'e> {
fn blacklist(&self) -> Option<&[&'e str]> { self.blacklist.as_ref().map(|o| &o[..]) }
fn is_set(&self, s: ArgSettings) -> bool { self.settings.is_set(s) }
fn has_switch(&self) -> bool { true }
fn takes_value(&self) -> bool { false }
fn set(&mut self, s: ArgSettings) { self.settings.set(s) }
fn max_vals(&self) -> Option<u64> { None }
fn val_names(&self) -> Option<&VecMap<&'e str>> { None }
fn num_vals(&self) -> Option<u64> { None }
fn possible_vals(&self) -> Option<&[&'e str]> { None }
fn validator(&self) -> Option<&Rc<Fn(String) -> StdResult<(), String>>> { None }
@ -107,6 +115,8 @@ impl<'n, 'e> AnyArg<'n, 'e> for FlagBuilder<'n, 'e> {
fn short(&self) -> Option<char> { self.short }
fn long(&self) -> Option<&'e str> { self.long }
fn val_delim(&self) -> Option<char> { None }
fn help(&self) -> Option<&'e str> { self.help }
fn default_val(&self) -> Option<&'n str> { None }
}
#[cfg(test)]

View file

@ -1,126 +0,0 @@
macro_rules! write_arg_help {
(@opt $_self:ident, $w:ident, $tab:ident, $longest:ident, $skip_pv:ident, $nlh:ident) => {
write_arg_help!(@short $_self, $w, $tab);
write_arg_help!(@opt_long $_self, $w, $nlh, $longest);
write_arg_help!(@val $_self, $w);
if !($nlh || $_self.settings.is_set(ArgSettings::NextLineHelp)) {
write_spaces!(if $_self.long.is_some() { $longest + 4 } else { $longest + 8 } - ($_self.to_string().len()), $w);
}
if let Some(h) = $_self.help {
write_arg_help!(@help $_self, $w, h, $tab, $longest, $nlh);
write_arg_help!(@spec_vals $_self, $w, $skip_pv);
}
};
(@flag $_self:ident, $w:ident, $tab:ident, $longest:ident, $nlh:ident) => {
write_arg_help!(@short $_self, $w, $tab);
write_arg_help!(@flag_long $_self, $w, $longest, $nlh);
if let Some(h) = $_self.help {
write_arg_help!(@help $_self, $w, h, $tab, $longest, $nlh);
}
};
(@pos $_self:ident, $w:ident, $tab:ident, $longest:ident, $skip_pv:ident, $nlh:ident) => {
try!(write!($w, "{}", $tab));
write_arg_help!(@val $_self, $w);
if !($nlh || $_self.settings.is_set(ArgSettings::NextLineHelp)) {
write_spaces!($longest + 4 - ($_self.to_string().len()), $w);
}
if let Some(h) = $_self.help {
write_arg_help!(@help $_self, $w, h, $tab, $longest, $nlh);
write_arg_help!(@spec_vals $_self, $w, $skip_pv);
}
};
(@short $_self:ident, $w:ident, $tab:ident) => {
try!(write!($w, "{}", $tab));
if let Some(s) = $_self.short {
try!(write!($w, "-{}", s));
} else {
try!(write!($w, "{}", $tab));
}
};
(@flag_long $_self:ident, $w:ident, $longest:ident, $nlh:ident) => {
if let Some(l) = $_self.long {
write_arg_help!(@long $_self, $w, l);
if !$nlh || !$_self.settings.is_set(ArgSettings::NextLineHelp) {
write_spaces!(($longest + 4) - (l.len() + 2), $w);
}
} else {
if !$nlh || !$_self.settings.is_set(ArgSettings::NextLineHelp) {
// 6 is tab (4) + -- (2)
write_spaces!(($longest + 6), $w);
}
}
};
(@opt_long $_self:ident, $w:ident, $nlh:ident, $longest:ident) => {
if let Some(l) = $_self.long {
write_arg_help!(@long $_self, $w, l);
}
try!(write!($w, " "));
};
(@long $_self:ident, $w:ident, $l:ident) => {
try!(write!($w,
"{}--{}",
if $_self.short.is_some() {
", "
} else {
""
},
$l));
};
(@val $_self:ident, $w:ident) => {
if let Some(ref vec) = $_self.val_names {
let mut it = vec.iter().peekable();
while let Some((_, val)) = it.next() {
try!(write!($w, "<{}>", val));
if it.peek().is_some() { try!(write!($w, " ")); }
}
let num = vec.len();
if $_self.settings.is_set(ArgSettings::Multiple) && num == 1 {
try!(write!($w, "..."));
}
} else if let Some(num) = $_self.num_vals {
for _ in 0..num {
try!(write!($w, "<{}>", $_self.name));
}
} else {
try!(write!($w,
"<{}>{}",
$_self.name,
if $_self.settings.is_set(ArgSettings::Multiple) {
"..."
} else {
""
}));
}
};
(@spec_vals $_self:ident, $w:ident, $skip_pv:ident) => {
if let Some(ref pv) = $_self.default_val {
try!(write!($w, " [default: {}]", pv));
}
if !$skip_pv {
if let Some(ref pv) = $_self.possible_vals {
try!(write!($w, " [values: {}]", pv.join(", ")));
}
}
};
(@help $_self:ident, $w:ident, $h:ident, $tab:ident, $longest:expr, $nlh:ident) => {
if $nlh || $_self.settings.is_set(ArgSettings::NextLineHelp) {
try!(write!($w, "\n{}{}", $tab, $tab));
}
if $h.contains("{n}") {
if let Some(part) = $h.split("{n}").next() {
try!(write!($w, "{}", part));
}
for part in $h.split("{n}").skip(1) {
try!(write!($w, "\n"));
if $nlh || $_self.settings.is_set(ArgSettings::NextLineHelp) {
try!(write!($w, "{}{}", $tab, $tab));
} else {
write_spaces!($longest + 12, $w);
}
try!(write!($w, "{}", part));
}
} else {
try!(write!($w, "{}", $h));
}
};
}

View file

@ -2,8 +2,6 @@ pub use self::flag::FlagBuilder;
pub use self::option::OptBuilder;
pub use self::positional::PosBuilder;
#[macro_use]
mod macros;
#[allow(dead_code)]
mod flag;
#[allow(dead_code)]

View file

@ -5,7 +5,7 @@ use std::io;
use vec_map::VecMap;
use args::{AnyArg, Arg};
use args::{AnyArg, Arg, HelpWriter};
use args::settings::{ArgFlags, ArgSettings};
#[allow(missing_debug_implementations)]
@ -104,10 +104,10 @@ impl<'n, 'e> OptBuilder<'n, 'e> {
ob
}
pub fn write_help<W: io::Write>(&self, w: &mut W, tab: &str, longest: usize, skip_pv: bool, nlh: bool) -> io::Result<()> {
debugln!("fn=write_help");
write_arg_help!(@opt self, w, tab, longest, skip_pv, nlh);
write!(w, "\n")
pub fn write_help<W: io::Write>(&self, w: &mut W, longest: usize, skip_pv: bool, nlh: bool) -> io::Result<()> {
let mut hw = HelpWriter::new(self, longest, nlh);
hw.skip_pv = skip_pv;
hw.write_to(w)
}
}
@ -116,29 +116,30 @@ impl<'n, 'e> Display for OptBuilder<'n, 'e> {
debugln!("fn=fmt");
// Write the name such --long or -l
if let Some(l) = self.long {
try!(write!(f, "--{}", l));
try!(write!(f, "--{} ", l));
} else {
try!(write!(f, "-{}", self.short.unwrap()));
try!(write!(f, "-{} ", self.short.unwrap()));
}
// Write the values such as <name1> <name2>
if let Some(ref vec) = self.val_names {
for (_, n) in vec {
debugln!("writing val_name: {}", n);
try!(write!(f, " <{}>", n));
let mut it = vec.iter().peekable();
while let Some((_, val)) = it.next() {
try!(write!(f, "<{}>", val));
if it.peek().is_some() { try!(write!(f, " ")); }
}
let num = vec.len();
if self.settings.is_set(ArgSettings::Multiple) && num == 1 {
if self.is_set(ArgSettings::Multiple) && num == 1 {
try!(write!(f, "..."));
}
} else if let Some(num) = self.num_vals {
let mut it = (0..num).peekable();
while let Some(_) = it.next() {
try!(write!(f, "<{}>", self.name));
if it.peek().is_some() { try!(write!(f, " ")); }
}
} else {
let num = self.num_vals.unwrap_or(1);
for _ in 0..num {
try!(write!(f, " <{}>", self.name));
}
if self.settings.is_set(ArgSettings::Multiple) && num == 1 {
try!(write!(f, "..."));
}
try!(write!(f, "<{}>{}", self.name, if self.is_set(ArgSettings::Multiple) { "..." } else { "" }));
}
Ok(())
@ -150,6 +151,7 @@ impl<'n, 'e> AnyArg<'n, 'e> for OptBuilder<'n, 'e> {
fn overrides(&self) -> Option<&[&'e str]> { self.overrides.as_ref().map(|o| &o[..]) }
fn requires(&self) -> Option<&[&'e str]> { self.requires.as_ref().map(|o| &o[..]) }
fn blacklist(&self) -> Option<&[&'e str]> { self.blacklist.as_ref().map(|o| &o[..]) }
fn val_names(&self) -> Option<&VecMap<&'e str>> { self.val_names.as_ref().map(|o| o) }
fn is_set(&self, s: ArgSettings) -> bool { self.settings.is_set(s) }
fn has_switch(&self) -> bool { true }
fn set(&mut self, s: ArgSettings) { self.settings.set(s) }
@ -163,6 +165,9 @@ impl<'n, 'e> AnyArg<'n, 'e> for OptBuilder<'n, 'e> {
fn short(&self) -> Option<char> { self.short }
fn long(&self) -> Option<&'e str> { self.long }
fn val_delim(&self) -> Option<char> { self.val_delim }
fn takes_value(&self) -> bool { true }
fn help(&self) -> Option<&'e str> { self.help }
fn default_val(&self) -> Option<&'n str> { self.default_val }
}
#[cfg(test)]

View file

@ -6,7 +6,7 @@ use std::io;
use vec_map::VecMap;
use Arg;
use args::AnyArg;
use args::{AnyArg, HelpWriter};
use args::settings::{ArgFlags, ArgSettings};
#[allow(missing_debug_implementations)]
@ -63,7 +63,7 @@ impl<'n, 'e> PosBuilder<'n, 'e> {
}
pub fn from_arg(a: &Arg<'n, 'e>, idx: u64, reqs: &mut Vec<&'e str>) -> Self {
assert!(a.short.is_none() || a.long.is_none(),
debug_assert!(a.short.is_none() || a.long.is_none(),
format!("Argument \"{}\" has conflicting requirements, both index() and short(), \
or long(), were supplied", a.name));
@ -105,9 +105,10 @@ impl<'n, 'e> PosBuilder<'n, 'e> {
pb
}
pub fn write_help<W: io::Write>(&self, w: &mut W, tab: &str, longest: usize, skip_pv: bool, nlh: bool) -> io::Result<()> {
write_arg_help!(@pos self, w, tab, longest, skip_pv, nlh);
write!(w, "\n")
pub fn write_help<W: io::Write>(&self, w: &mut W, longest: usize, skip_pv: bool, nlh: bool) -> io::Result<()> {
let mut hw = HelpWriter::new(self, longest, nlh);
hw.skip_pv = skip_pv;
hw.write_to(w)
}
}
@ -139,6 +140,7 @@ impl<'n, 'e> AnyArg<'n, 'e> for PosBuilder<'n, 'e> {
fn overrides(&self) -> Option<&[&'e str]> { self.overrides.as_ref().map(|o| &o[..]) }
fn requires(&self) -> Option<&[&'e str]> { self.requires.as_ref().map(|o| &o[..]) }
fn blacklist(&self) -> Option<&[&'e str]> { self.blacklist.as_ref().map(|o| &o[..]) }
fn val_names(&self) -> Option<&VecMap<&'e str>> { self.val_names.as_ref() }
fn is_set(&self, s: ArgSettings) -> bool { self.settings.is_set(s) }
fn set(&mut self, s: ArgSettings) { self.settings.set(s) }
fn has_switch(&self) -> bool { false }
@ -152,6 +154,9 @@ impl<'n, 'e> AnyArg<'n, 'e> for PosBuilder<'n, 'e> {
fn short(&self) -> Option<char> { None }
fn long(&self) -> Option<&'e str> { None }
fn val_delim(&self) -> Option<char> { self.val_delim }
fn takes_value(&self) -> bool { true }
fn help(&self) -> Option<&'e str> { self.help }
fn default_val(&self) -> Option<&'n str> { self.default_val }
}
#[cfg(test)]

256
src/args/help_writer.rs Normal file
View file

@ -0,0 +1,256 @@
use std::io;
use args::AnyArg;
use args::settings::ArgSettings;
use term;
const TAB: &'static str = " ";
pub struct HelpWriter<'a, A> where A: 'a {
a: &'a A,
l: usize,
nlh: bool,
pub skip_pv: bool,
term_w: Option<usize>,
}
impl<'a, 'n, 'e, A> HelpWriter<'a, A> where A: AnyArg<'n, 'e> {
pub fn new(a: &'a A, l: usize, nlh: bool) -> Self {
HelpWriter {
a: a,
l: l,
nlh: nlh,
skip_pv: false,
term_w: term::dimensions().map(|(w, _)| w),
}
}
pub fn write_to<W: io::Write>(&self, w: &mut W) -> io::Result<()> {
debugln!("fn=write_to;");
try!(self.short(w));
try!(self.long(w));
try!(self.val(w));
try!(self.help(w));
write!(w, "\n")
}
fn short<W>(&self, w: &mut W) -> io::Result<()>
where W: io::Write
{
debugln!("fn=short;");
try!(write!(w, "{}", TAB));
if let Some(s) = self.a.short() {
write!(w, "-{}", s)
} else if self.a.has_switch() {
write!(w, "{}", TAB)
} else {
Ok(())
}
}
fn long<W>(&self, w: &mut W) -> io::Result<()>
where W: io::Write
{
debugln!("fn=long;");
if !self.a.has_switch() {
return Ok(());
}
if self.a.takes_value() {
if let Some(l) = self.a.long() {
try!(write!(w, "{}--{}", if self.a.short().is_some() { ", " } else { "" }, l));
}
try!(write!(w, " "));
} else {
if let Some(l) = self.a.long() {
try!(write!(w, "{}--{}", if self.a.short().is_some() { ", " } else { "" }, l));
if !self.nlh || !self.a.is_set(ArgSettings::NextLineHelp) {
write_spaces!((self.l + 4) - (l.len() + 2), w);
}
} else {
if !self.nlh || !self.a.is_set(ArgSettings::NextLineHelp) {
// 6 is tab (4) + -- (2)
write_spaces!((self.l + 6), w);
}
}
}
Ok(())
}
fn val<W>(&self, w: &mut W) -> io::Result<()>
where W: io::Write
{
debugln!("fn=val;");
if !self.a.takes_value() {
return Ok(());
}
if let Some(ref vec) = self.a.val_names() {
let mut it = vec.iter().peekable();
while let Some((_, val)) = it.next() {
try!(write!(w, "<{}>", val));
if it.peek().is_some() { try!(write!(w, " ")); }
}
let num = vec.len();
if self.a.is_set(ArgSettings::Multiple) && num == 1 {
try!(write!(w, "..."));
}
} else if let Some(num) = self.a.num_vals() {
let mut it = (0..num).peekable();
while let Some(_) = it.next() {
try!(write!(w, "<{}>", self.a.name()));
if it.peek().is_some() { try!(write!(w, " ")); }
}
} else {
try!(write!(w, "<{}>{}", self.a.name(), if self.a.is_set(ArgSettings::Multiple) { "..." } else { "" }));
}
if self.a.has_switch() {
if !(self.nlh || self.a.is_set(ArgSettings::NextLineHelp)) {
let self_len = self.a.to_string().len();
// subtract ourself
let mut spcs = self.l - self_len;
// Since we're writing spaces from the tab point we first need to know if we
// had a long and short, or just short
if self.a.long().is_some() {
// Only account 4 after the val
spcs += 4;
} else {
// Only account for ', --' + 4 after the val
spcs += 8;
}
write_spaces!(spcs, w);
}
} else {
if !(self.nlh || self.a.is_set(ArgSettings::NextLineHelp)) {
write_spaces!(self.l + 4 - (self.a.to_string().len()), w);
}
}
Ok(())
}
fn help<W>(&self, w: &mut W) -> io::Result<()>
where W: io::Write
{
debugln!("fn=help;");
let spec_vals = self.spec_vals();
let mut help = String::new();
let h = self.a.help().unwrap_or("");
let spcs = if self.nlh || self.a.is_set(ArgSettings::NextLineHelp) {
8 // "tab" + "tab"
} else {
self.l + 12
};
// determine if our help fits or needs to wrap
let too_long = self.term_w.is_some() && (spcs + h.len() + spec_vals.len() >= self.term_w.unwrap_or(0));
// Is help on next line, if so newline + 2x tab
if self.nlh || self.a.is_set(ArgSettings::NextLineHelp) {
try!(write!(w, "\n{}{}", TAB, TAB));
}
debug!("Too long...");
if too_long {
sdebugln!("Yes");
if let Some(width) = self.term_w {
help.push_str(h);
help.push_str(&*spec_vals);
debugln!("term width: {}", width);
debugln!("help: {}", help);
debugln!("help len: {}", help.len());
// Determine how many newlines we need to insert
let avail_chars = width - spcs;
debugln!("Usable space: {}", avail_chars);
let mut indices = vec![];
let mut idx = 0;
loop {
idx += avail_chars - 1;
if idx >= help.len() { break; }
// 'a' arbitrary non space char
if help.chars().nth(idx).unwrap_or('a') != ' ' {
idx = find_idx_of_space(&*help, idx);
}
debugln!("Adding idx: {}", idx);
debugln!("At {}: {:?}", idx, help.chars().nth(idx));
indices.push(idx);
if &help[idx..].len() <= &avail_chars {
break;
}
}
for (i, idx) in indices.iter().enumerate() {
debugln!("iter;i={},idx={}", i, idx);
let j = idx+(2*i);
debugln!("removing: {}", j);
debugln!("at {}: {:?}", j, help.chars().nth(j));
help.remove(j);
help.insert(j, '{');
help.insert(j + 1 , 'n');
help.insert(j + 2, '}');
}
}
} else { sdebugln!("No"); }
let help = if !help.is_empty() {
&*help
} else if !spec_vals.is_empty() {
help.push_str(h);
help.push_str(&*spec_vals);
&*help
} else {
h
};
if help.contains("{n}") {
if let Some(part) = help.split("{n}").next() {
try!(write!(w, "{}", part));
}
for part in help.split("{n}").skip(1) {
try!(write!(w, "\n"));
if self.nlh || self.a.is_set(ArgSettings::NextLineHelp) {
try!(write!(w, "{}{}", TAB, TAB));
} else {
if self.a.has_switch() {
write_spaces!(self.l + 12, w);
} else {
write_spaces!(self.l + 8, w);
}
}
try!(write!(w, "{}", part));
}
} else {
try!(write!(w, "{}", help));
}
Ok(())
}
fn spec_vals(&self) -> String
{
debugln!("fn=spec_vals;");
if let Some(ref pv) = self.a.default_val() {
debugln!("Writing defaults");
return format!(" [default: {}] {}", pv,
if !self.skip_pv {
if let Some(ref pv) = self.a.possible_vals() {
format!(" [values: {}]", pv.join(", "))
} else { "".into() }
} else { "".into() }
);
} else if !self.skip_pv {
debugln!("Writing values");
if let Some(ref pv) = self.a.possible_vals() {
debugln!("Possible vals...{:?}", pv);
return format!(" [values: {}]", pv.join(", "));
}
}
String::new()
}
}
fn find_idx_of_space(full: &str, start: usize) -> usize {
debugln!("fn=find_idx_of_space;");
let haystack = &full[..start];
debugln!("haystack: {}", haystack);
for (i, c) in haystack.chars().rev().enumerate() {
debugln!("iter;c={},i={}", c, i);
if c == ' ' {
debugln!("Found space returning start-i...{}", start - (i+1));
return start - (i+1);
}
}
0
}

View file

@ -7,6 +7,7 @@ pub use self::matched_arg::MatchedArg;
pub use self::group::ArgGroup;
pub use self::any_arg::AnyArg;
pub use self::settings::ArgSettings;
pub use self::help_writer::HelpWriter;
mod arg;
pub mod any_arg;
@ -18,3 +19,4 @@ mod matched_arg;
mod group;
#[allow(dead_code)]
pub mod settings;
mod help_writer;

View file

@ -405,6 +405,8 @@ extern crate strsim;
extern crate ansi_term;
#[cfg(feature = "yaml")]
extern crate yaml_rust;
#[cfg(feature = "wrap_help")]
extern crate libc;
#[macro_use]
extern crate bitflags;
extern crate vec_map;
@ -425,6 +427,7 @@ mod fmt;
mod suggestions;
mod errors;
mod osstringext;
mod term;
const INTERNAL_ERROR_MSG: &'static str = "Fatal internal error. Please consider filing a bug \
report at https://github.com/kbknapp/clap-rs/issues";

82
src/term.rs Normal file
View file

@ -0,0 +1,82 @@
// The following was taken and adapated from exa source
// repo: https://github.com/ogham/exa
// commit: b9eb364823d0d4f9085eb220233c704a13d0f611
// license: MIT - Copyright (c) 2014 Benjamin Sago
//! System calls for getting the terminal size.
//!
//! Getting the terminal size is performed using an ioctl command that takes
//! the file handle to the terminal -- which in this case, is stdout -- and
//! populates a structure containing the values.
//!
//! The size is needed when the user wants the output formatted into columns:
//! the default grid view, or the hybrid grid-details view.
#[cfg(feature = "wrap_help")]
use std::mem::zeroed;
#[cfg(feature = "wrap_help")]
use libc::{c_int, c_ushort, c_ulong, STDOUT_FILENO};
/// The number of rows and columns of a terminal.
#[cfg(feature = "wrap_help")]
struct Winsize {
ws_row: c_ushort,
ws_col: c_ushort,
}
// Unfortunately the actual command is not standardised...
#[cfg(any(target_os = "linux", target_os = "android"))]
#[cfg(feature = "wrap_help")]
static TIOCGWINSZ: c_ulong = 0x5413;
#[cfg(any(target_os = "macos",
target_os = "ios",
target_os = "bitrig",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"))]
#[cfg(feature = "wrap_help")]
static TIOCGWINSZ: c_ulong = 0x40087468;
extern {
#[cfg(feature = "wrap_help")]
pub fn ioctl(fd: c_int, request: c_ulong, ...) -> c_int;
}
/// Runs the ioctl command. Returns (0, 0) if output is not to a terminal, or
/// there is an error. (0, 0) is an invalid size to have anyway, which is why
/// it can be used as a nil value.
#[cfg(feature = "wrap_help")]
unsafe fn get_dimensions() -> Winsize {
let mut window: Winsize = zeroed();
let result = ioctl(STDOUT_FILENO, TIOCGWINSZ, &mut window);
if result == -1 {
zeroed()
}
else {
window
}
}
/// Query the current processes's output, returning its width and height as a
/// number of characters. Returns `None` if the output isn't to a terminal.
#[cfg(feature = "wrap_help")]
pub fn dimensions() -> Option<(usize, usize)> {
let w = unsafe { get_dimensions() };
if w.ws_col == 0 || w.ws_row == 0 {
None
}
else {
Some((w.ws_col as usize, w.ws_row as usize))
}
}
#[cfg(not(feature = "wrap_help"))]
pub fn dimensions() -> Option<(usize, usize)> {
None
}