diff --git a/Cargo.toml b/Cargo.toml index 128acbe0..21535223 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -67,8 +67,7 @@ maintenance = {status = "actively-developed"} [dependencies] clap_derive = { path = "./clap_derive", version = "3.0.0-beta.2", optional = true } bitflags = "1.2" -textwrap = { version = "0.13", default-features = false, features = [] } -unicode-width = { version = "0.1", optional = true } +textwrap = { version = "0.13.3", default-features = false, features = [] } indexmap = "1.0" os_str_bytes = { version = "2.4", features = ["raw"] } vec_map = "0.8" @@ -92,7 +91,7 @@ std = [] # support for no_std in a backwards-compatible way suggestions = ["strsim"] color = ["atty", "termcolor"] wrap_help = ["terminal_size", "textwrap/terminal_size"] -unicode_help= ["unicode-width", "textwrap/unicode-width"] # Enable if any unicode in help message +unicode_help= ["textwrap/unicode-width"] # Enable if any unicode in help message derive = ["clap_derive", "lazy_static"] yaml = ["yaml-rust"] cargo = ["lazy_static"] # Disable if you're not using Cargo, enables Cargo-env-var-dependent macros diff --git a/src/output/help.rs b/src/output/help.rs index 360b66ac..ba3690dc 100644 --- a/src/output/help.rs +++ b/src/output/help.rs @@ -18,6 +18,7 @@ use crate::{ // Third party use indexmap::IndexSet; +use textwrap::core::display_width; pub(crate) fn dimensions() -> Option<(usize, usize)> { #[cfg(not(feature = "wrap_help"))] @@ -27,14 +28,6 @@ pub(crate) fn dimensions() -> Option<(usize, usize)> { terminal_size::terminal_size().map(|(w, h)| (w.0.into(), h.0.into())) } -fn str_width(s: &str) -> usize { - #[cfg(not(feature = "unicode_help"))] - return s.len(); - - #[cfg(feature = "unicode_help")] - unicode_width::UnicodeWidthStr::width(s) -} - const TAB: &str = " "; pub(crate) enum HelpWriter<'writer> { @@ -182,7 +175,7 @@ impl<'help, 'app, 'parser, 'writer> Help<'help, 'app, 'parser, 'writer> { .filter(|arg| should_show_arg(self.use_long, *arg)) { if arg.longest_filter() { - longest = longest.max(str_width(arg.to_string().as_str())); + longest = longest.max(display_width(arg.to_string().as_str())); } arg_v.push(arg) } @@ -212,7 +205,7 @@ impl<'help, 'app, 'parser, 'writer> Help<'help, 'app, 'parser, 'writer> { }) { if arg.longest_filter() { debug!("Help::write_args: Current Longest...{}", longest); - longest = longest.max(str_width(arg.to_string().as_str())); + longest = longest.max(display_width(arg.to_string().as_str())); debug!("Help::write_args: New Longest...{}", longest); } let btm = ord_m.entry(arg.disp_ord).or_insert(BTreeMap::new()); @@ -370,7 +363,7 @@ impl<'help, 'app, 'parser, 'writer> Help<'help, 'app, 'parser, 'writer> { debug!("Yes"); debug!("Help::val: nlh...{:?}", next_line_help); if !next_line_help { - let self_len = str_width(arg.to_string().as_str()); + let self_len = display_width(arg.to_string().as_str()); // subtract ourself let mut spcs = longest - self_len; // Since we're writing spaces from the tab point we first need to know if we @@ -387,7 +380,7 @@ impl<'help, 'app, 'parser, 'writer> Help<'help, 'app, 'parser, 'writer> { } } else if !next_line_help { debug!("No, and not next_line"); - self.spaces(longest + 4 - str_width(&arg.to_string()))?; + self.spaces(longest + 4 - display_width(&arg.to_string()))?; } else { debug!("No"); } @@ -447,7 +440,7 @@ impl<'help, 'app, 'parser, 'writer> Help<'help, 'app, 'parser, 'writer> { longest + 12 }; - let too_long = spaces + str_width(about) + str_width(spec_vals) >= self.term_w; + let too_long = spaces + display_width(about) + display_width(spec_vals) >= self.term_w; // Is help on next line, if so then indent if next_line_help { @@ -458,7 +451,7 @@ impl<'help, 'app, 'parser, 'writer> Help<'help, 'app, 'parser, 'writer> { if too_long && spaces <= self.term_w { debug!("Yes"); debug!("Help::help: help...{}", help); - debug!("Help::help: help width...{}", str_width(&help)); + debug!("Help::help: help width...{}", display_width(&help)); // Determine how many newlines we need to insert let avail_chars = self.term_w - spaces; debug!("Help::help: Usable space...{}", avail_chars); @@ -522,7 +515,7 @@ impl<'help, 'app, 'parser, 'writer> Help<'help, 'app, 'parser, 'writer> { } else { // force_next_line let h = arg.about.unwrap_or(""); - let h_w = str_width(h) + str_width(spec_vals); + let h_w = display_width(h) + display_width(spec_vals); let taken = longest + 12; self.term_w >= taken && (taken as f32 / self.term_w as f32) > 0.40 @@ -726,7 +719,7 @@ impl<'help, 'app, 'parser, 'writer> Help<'help, 'app, 'parser, 'writer> { } else { // force_next_line let h = app.about.unwrap_or(""); - let h_w = str_width(h) + str_width(spec_vals); + let h_w = display_width(h) + display_width(spec_vals); let taken = longest + 12; self.term_w >= taken && (taken as f32 / self.term_w as f32) > 0.40 @@ -739,7 +732,7 @@ impl<'help, 'app, 'parser, 'writer> Help<'help, 'app, 'parser, 'writer> { self.none(TAB)?; self.good(sc_str)?; if !next_line_help { - let width = str_width(sc_str); + let width = display_width(sc_str); self.spaces(width.max(longest + 4) - width)?; } Ok(()) @@ -895,7 +888,7 @@ impl<'help, 'app, 'parser, 'writer> Help<'help, 'app, 'parser, 'writer> { .map_or(String::new(), |c| format!("--{}, ", c)), ); sc_str.push_str(&subcommand.name); - longest = longest.max(str_width(&sc_str)); + longest = longest.max(display_width(&sc_str)); btm.insert(sc_str, subcommand.clone()); } @@ -1073,11 +1066,32 @@ fn text_wrapper(help: &str, width: usize) -> String { #[cfg(test)] mod test { - use super::text_wrapper; + use super::*; #[test] fn wrap_help_last_word() { let help = String::from("foo bar baz"); assert_eq!(text_wrapper(&help, 5), "foo\nbar\nbaz"); } + + #[test] + fn display_width_handles_non_ascii() { + // Popular Danish tounge-twister, the name of a fruit dessert. + let text = "rødgrød med fløde"; + assert_eq!(display_width(text), 17); + // Note that the string width is smaller than the string + // length. This is due to the precomposed non-ASCII letters: + assert_eq!(text.len(), 20); + } + + #[test] + fn display_width_handles_emojis() { + let text = "😂"; + // There is a single `char`... + assert_eq!(text.chars().count(), 1); + // but it is double-width: + assert_eq!(display_width(text), 2); + // This is much less than the byte length: + assert_eq!(text.len(), 4); + } }