diff --git a/Cargo.toml b/Cargo.toml index 80cb6b0fed3..f148ffa737a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -94,6 +94,7 @@ string = ["clap_builder/string"] # Allow runtime generated strings # In-work features unstable-v5 = ["clap_builder/unstable-v5", "clap_derive?/unstable-v5", "deprecated"] +unstable-styles = ["clap_builder/unstable-styles"] [lib] bench = false diff --git a/Makefile b/Makefile index 6f54f577464..1c203cedbc9 100644 --- a/Makefile +++ b/Makefile @@ -16,7 +16,7 @@ _FEATURES = minimal default wasm full debug release _FEATURES_minimal = --no-default-features --features "std" _FEATURES_default = _FEATURES_wasm = --no-default-features --features "std help usage error-context suggestions" --features "deprecated derive cargo env unicode string" -_FEATURES_full = --features "deprecated derive cargo env unicode string wrap_help" +_FEATURES_full = --features "deprecated derive cargo env unicode string wrap_help unstable-styles" _FEATURES_next = ${_FEATURES_full} --features unstable-v5 _FEATURES_debug = ${_FEATURES_full} --features debug --features clap_complete/debug _FEATURES_release = ${_FEATURES_full} --release diff --git a/clap_builder/Cargo.toml b/clap_builder/Cargo.toml index 1bd8cbfad91..91c9833e1b4 100644 --- a/clap_builder/Cargo.toml +++ b/clap_builder/Cargo.toml @@ -32,11 +32,11 @@ tag-name = "v{{version}}" [features] default = ["std", "color", "help", "usage", "error-context", "suggestions"] debug = ["dep:backtrace"] # Enables debug messages -unstable-doc = ["cargo", "wrap_help", "env", "unicode", "string"] # for docs.rs +unstable-doc = ["cargo", "wrap_help", "env", "unicode", "string", "unstable-styles"] # for docs.rs # Used in default -std = [] # support for no_std in a backwards-compatible way -color = ["dep:anstyle", "dep:anstream"] +std = ["anstyle/std"] # support for no_std in a backwards-compatible way +color = ["dep:anstream"] help = [] usage = [] error-context = [] @@ -52,6 +52,7 @@ string = [] # Allow runtime generated strings # In-work features unstable-v5 = ["deprecated"] +unstable-styles = ["color"] [lib] bench = false @@ -62,7 +63,7 @@ bitflags = "1.2.0" unicase = { version = "2.6.0", optional = true } strsim = { version = "0.10.0", optional = true } anstream = { version = "0.3.0", optional = true } -anstyle = { version = "1.0.0", features = ["std"], optional = true } +anstyle = "1.0.0" terminal_size = { version = "0.2.1", optional = true } backtrace = { version = "0.3.67", optional = true } unicode-width = { version = "0.1.9", optional = true } diff --git a/clap_builder/src/builder/arg.rs b/clap_builder/src/builder/arg.rs index 399ac66dea0..ce7e02d8712 100644 --- a/clap_builder/src/builder/arg.rs +++ b/clap_builder/src/builder/arg.rs @@ -17,6 +17,7 @@ use crate::builder::OsStr; use crate::builder::PossibleValue; use crate::builder::Str; use crate::builder::StyledStr; +use crate::builder::Styles; use crate::builder::ValueRange; use crate::util::AnyValueId; use crate::ArgAction; @@ -4271,49 +4272,74 @@ impl Arg { } } - pub(crate) fn stylized(&self, required: Option) -> StyledStr { + pub(crate) fn stylized(&self, styles: &Styles, required: Option) -> StyledStr { + use std::fmt::Write as _; + let literal = styles.get_literal(); + let mut styled = StyledStr::new(); // Write the name such --long or -l if let Some(l) = self.get_long() { - styled.literal("--"); - styled.literal(l); + let _ = write!( + styled, + "{}--{l}{}", + literal.render(), + literal.render_reset() + ); } else if let Some(s) = self.get_short() { - styled.literal("-"); - styled.literal(s); + let _ = write!(styled, "{}-{s}{}", literal.render(), literal.render_reset()); } - styled.push_styled(&self.stylize_arg_suffix(required)); + styled.push_styled(&self.stylize_arg_suffix(styles, required)); styled } - pub(crate) fn stylize_arg_suffix(&self, required: Option) -> StyledStr { + pub(crate) fn stylize_arg_suffix(&self, styles: &Styles, required: Option) -> StyledStr { + use std::fmt::Write as _; + let literal = styles.get_literal(); + let placeholder = styles.get_placeholder(); let mut styled = StyledStr::new(); let mut need_closing_bracket = false; if self.is_takes_value_set() && !self.is_positional() { let is_optional_val = self.get_min_vals() == 0; - if self.is_require_equals_set() { + let (style, start) = if self.is_require_equals_set() { if is_optional_val { need_closing_bracket = true; - styled.placeholder("[="); + (placeholder, "[=") } else { - styled.literal("="); + (literal, "=") } } else if is_optional_val { need_closing_bracket = true; - styled.placeholder(" ["); + (placeholder, " [") } else { - styled.placeholder(" "); - } + (placeholder, " ") + }; + let _ = write!(styled, "{}{start}{}", style.render(), style.render_reset()); } if self.is_takes_value_set() || self.is_positional() { let required = required.unwrap_or_else(|| self.is_required_set()); let arg_val = self.render_arg_val(required); - styled.placeholder(arg_val); + let _ = write!( + styled, + "{}{arg_val}{}", + placeholder.render(), + placeholder.render_reset() + ); } else if matches!(*self.get_action(), ArgAction::Count) { - styled.placeholder("..."); + let _ = write!( + styled, + "{}...{}", + placeholder.render(), + placeholder.render_reset() + ); } if need_closing_bracket { - styled.placeholder("]"); + let _ = write!( + styled, + "{}]{}", + placeholder.render(), + placeholder.render_reset() + ); } styled @@ -4401,7 +4427,8 @@ impl Eq for Arg {} impl Display for Arg { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - self.stylized(None).fmt(f) + let plain = Styles::plain(); + self.stylized(&plain, None).fmt(f) } } diff --git a/clap_builder/src/builder/command.rs b/clap_builder/src/builder/command.rs index a17b51fb1e3..abd743382e6 100644 --- a/clap_builder/src/builder/command.rs +++ b/clap_builder/src/builder/command.rs @@ -17,6 +17,7 @@ use crate::builder::IntoResettable; use crate::builder::PossibleValue; use crate::builder::Str; use crate::builder::StyledStr; +use crate::builder::Styles; use crate::builder::{Arg, ArgGroup, ArgPredicate}; use crate::error::ErrorKind; use crate::error::Result as ClapResult; @@ -1080,6 +1081,31 @@ impl Command { } } + /// Sets when to color output. + /// + /// **NOTE:** This choice is propagated to all child subcommands. + /// + /// **NOTE:** Default behaviour is [`ColorChoice::Auto`]. + /// + /// # Examples + /// + /// ```no_run + /// # use clap_builder as clap; + /// # use clap::{Command, ColorChoice}; + /// Command::new("myprog") + /// .color(ColorChoice::Never) + /// .get_matches(); + /// ``` + /// [`ColorChoice::Auto`]: crate::ColorChoice::Auto + #[cfg(feature = "color")] + #[inline] + #[must_use] + #[cfg(feature = "unstable-styles")] + pub fn styles(mut self, styles: Styles) -> Self { + self.app_ext.set(styles); + self + } + /// Sets the terminal width at which to wrap help messages. /// /// Using `0` will ignore terminal widths and use source formatting. @@ -3338,6 +3364,10 @@ impl Command { } } + pub(crate) fn get_styles(&self) -> &Styles { + self.app_ext.get().unwrap_or_default() + } + /// Iterate through the set of subcommands, getting a reference to each. #[inline] pub fn get_subcommands(&self) -> impl Iterator { @@ -4321,9 +4351,9 @@ impl Command { .collect::>() .join("|"); let mut styled = StyledStr::new(); - styled.none("<"); - styled.none(g_string); - styled.none(">"); + styled.push_str("<"); + styled.push_string(g_string); + styled.push_str(">"); styled } } @@ -4649,7 +4679,7 @@ impl fmt::Display for Command { } } -trait AppTag: crate::builder::ext::Extension {} +pub(crate) trait AppTag: crate::builder::ext::Extension {} #[derive(Default, Copy, Clone, Debug)] struct TermWidth(usize); diff --git a/clap_builder/src/builder/mod.rs b/clap_builder/src/builder/mod.rs index 76331694d67..495c8458762 100644 --- a/clap_builder/src/builder/mod.rs +++ b/clap_builder/src/builder/mod.rs @@ -35,6 +35,8 @@ pub use range::ValueRange; pub use resettable::IntoResettable; pub use resettable::Resettable; pub use styled_str::StyledStr; +#[cfg(feature = "unstable-styles")] +pub use styled_str::Styles; pub use value_hint::ValueHint; pub use value_parser::_AutoValueParser; pub use value_parser::via_prelude; @@ -59,3 +61,6 @@ pub use value_parser::_AnonymousValueParser; pub(crate) use self::str::Inner as StrInner; pub(crate) use action::CountType; pub(crate) use arg_settings::{ArgFlags, ArgSettings}; +pub(crate) use command::AppTag; +#[cfg(not(feature = "unstable-styles"))] +pub(crate) use styled_str::Styles; diff --git a/clap_builder/src/builder/styled_str.rs b/clap_builder/src/builder/styled_str.rs index 94c838aaabc..e4acc63ffba 100644 --- a/clap_builder/src/builder/styled_str.rs +++ b/clap_builder/src/builder/styled_str.rs @@ -33,39 +33,14 @@ impl StyledStr { self.0.as_str() } - pub(crate) fn header(&mut self, msg: impl Into) { - self.stylize(Style::Header, msg.into()); - } - - pub(crate) fn literal(&mut self, msg: impl Into) { - self.stylize(Style::Literal, msg.into()); - } - - pub(crate) fn placeholder(&mut self, msg: impl Into) { - self.stylize(Style::Placeholder, msg.into()); - } - - #[cfg_attr(not(feature = "error-context"), allow(dead_code))] - pub(crate) fn good(&mut self, msg: impl Into) { - self.stylize(Style::Good, msg.into()); - } - - #[cfg_attr(not(feature = "error-context"), allow(dead_code))] - pub(crate) fn warning(&mut self, msg: impl Into) { - self.stylize(Style::Warning, msg.into()); - } - - pub(crate) fn error(&mut self, msg: impl Into) { - self.stylize(Style::Error, msg.into()); - } - - #[allow(dead_code)] - pub(crate) fn hint(&mut self, msg: impl Into) { - self.stylize(Style::Hint, msg.into()); + /// May allow the compiler to consolidate the `Drop`s for `msg`, reducing code size compared to + /// `styled.push_str(&msg)` + pub(crate) fn push_string(&mut self, msg: String) { + self.0.push_str(&msg); } - pub(crate) fn none(&mut self, msg: impl Into) { - self.0.push_str(&msg.into()); + pub(crate) fn push_str(&mut self, msg: &str) { + self.0.push_str(msg); } pub(crate) fn trim(&mut self) { @@ -122,21 +97,6 @@ impl StyledStr { self.0 = new; } - #[cfg(feature = "color")] - fn stylize(&mut self, style: Style, msg: String) { - if !msg.is_empty() { - use std::fmt::Write as _; - - let style = style.as_style(); - let _ = write!(self.0, "{}{}{}", style.render(), msg, style.render_reset()); - } - } - - #[cfg(not(feature = "color"))] - fn stylize(&mut self, _style: Style, msg: String) { - self.0.push_str(&msg); - } - #[inline(never)] #[cfg(feature = "help")] pub(crate) fn display_width(&self) -> usize { @@ -194,7 +154,7 @@ impl From for StyledStr { impl From<&'_ std::string::String> for StyledStr { fn from(name: &'_ std::string::String) -> Self { let mut styled = StyledStr::new(); - styled.none(name); + styled.push_str(name); styled } } @@ -202,7 +162,7 @@ impl From<&'_ std::string::String> for StyledStr { impl From<&'static str> for StyledStr { fn from(name: &'static str) -> Self { let mut styled = StyledStr::new(); - styled.none(name); + styled.push_str(name); styled } } @@ -238,28 +198,163 @@ impl std::fmt::Display for StyledStr { } } -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub(crate) enum Style { - Header, - Literal, - Placeholder, - Good, - Warning, - Error, - Hint, +/// Terminal styling definitions +#[derive(Clone, Debug)] +#[allow(missing_copy_implementations)] // Large enough type that I want an explicit `clone()` for now +pub struct Styles { + header: anstyle::Style, + error: anstyle::Style, + usage: anstyle::Style, + literal: anstyle::Style, + placeholder: anstyle::Style, + valid: anstyle::Style, + invalid: anstyle::Style, } -impl Style { - #[cfg(feature = "color")] - fn as_style(&self) -> anstyle::Style { - match self { - Style::Header => (anstyle::Effects::BOLD | anstyle::Effects::UNDERLINE).into(), - Style::Literal => anstyle::Effects::BOLD.into(), - Style::Placeholder => anstyle::Style::default(), - Style::Good => anstyle::AnsiColor::Green.on_default(), - Style::Warning => anstyle::AnsiColor::Yellow.on_default(), - Style::Error => anstyle::AnsiColor::Red.on_default() | anstyle::Effects::BOLD, - Style::Hint => anstyle::Effects::DIMMED.into(), +impl Styles { + /// No terminal styling + pub const fn plain() -> Self { + Self { + header: anstyle::Style::new(), + error: anstyle::Style::new(), + usage: anstyle::Style::new(), + literal: anstyle::Style::new(), + placeholder: anstyle::Style::new(), + valid: anstyle::Style::new(), + invalid: anstyle::Style::new(), + } + } + + /// Default terminal styling + pub const fn styled() -> Self { + #[cfg(feature = "color")] + { + Self { + header: anstyle::Style::new().bold().underline(), + error: anstyle::Style::new() + .fg_color(Some(anstyle::Color::Ansi(anstyle::AnsiColor::Red))) + .bold(), + usage: anstyle::Style::new().bold().underline(), + literal: anstyle::Style::new().bold(), + placeholder: anstyle::Style::new(), + valid: anstyle::Style::new() + .fg_color(Some(anstyle::Color::Ansi(anstyle::AnsiColor::Green))), + invalid: anstyle::Style::new() + .fg_color(Some(anstyle::Color::Ansi(anstyle::AnsiColor::Yellow))), + } + } + #[cfg(not(feature = "color"))] + { + Self::plain() } } + + /// General Heading style, e.g. [`help_heading`][crate::Arg::help_heading] + #[inline] + pub const fn header(mut self, style: anstyle::Style) -> Self { + self.header = style; + self + } + + /// Error heading + #[inline] + pub const fn error(mut self, style: anstyle::Style) -> Self { + self.error = style; + self + } + + /// Usage heading + #[inline] + pub const fn usage(mut self, style: anstyle::Style) -> Self { + self.usage = style; + self + } + + /// Literal command-line syntax, e.g. `--help` + #[inline] + pub const fn literal(mut self, style: anstyle::Style) -> Self { + self.literal = style; + self + } + + /// Descriptions within command-line syntax, e.g. [`value_name`][crate::Arg::value_name] + #[inline] + pub const fn placeholder(mut self, style: anstyle::Style) -> Self { + self.placeholder = style; + self + } + + /// Highlight suggested usage + #[inline] + pub const fn valid(mut self, style: anstyle::Style) -> Self { + self.valid = style; + self + } + + /// Highlight invalid usage + #[inline] + pub const fn invalid(mut self, style: anstyle::Style) -> Self { + self.invalid = style; + self + } +} + +/// Reflection +impl Styles { + /// General Heading style, e.g. [`help_heading`][crate::Arg::help_heading] + #[inline(always)] + pub const fn get_header(&self) -> &anstyle::Style { + &self.header + } + + /// Error heading + #[inline(always)] + pub const fn get_error(&self) -> &anstyle::Style { + &self.error + } + + /// Usage heading + #[inline(always)] + pub const fn get_usage(&self) -> &anstyle::Style { + &self.usage + } + + /// Literal command-line syntax, e.g. `--help` + #[inline(always)] + pub const fn get_literal(&self) -> &anstyle::Style { + &self.literal + } + + /// Descriptions within command-line syntax, e.g. [`value_name`][crate::Arg::value_name] + #[inline(always)] + pub const fn get_placeholder(&self) -> &anstyle::Style { + &self.placeholder + } + + /// Highlight suggested usage + #[inline(always)] + pub const fn get_valid(&self) -> &anstyle::Style { + &self.valid + } + + /// Highlight invalid usage + #[inline(always)] + pub const fn get_invalid(&self) -> &anstyle::Style { + &self.invalid + } +} + +impl super::AppTag for Styles {} + +impl Default for Styles { + fn default() -> Self { + Self::styled() + } +} + +impl Default for &'_ Styles { + fn default() -> Self { + const STYLES: Styles = Styles::styled(); + &STYLES + } } diff --git a/clap_builder/src/error/format.rs b/clap_builder/src/error/format.rs index 5ada0abe566..b5f0c126459 100644 --- a/clap_builder/src/error/format.rs +++ b/clap_builder/src/error/format.rs @@ -5,6 +5,7 @@ use crate::builder::Command; use crate::builder::StyledStr; +use crate::builder::Styles; #[cfg(feature = "error-context")] use crate::error::ContextKind; #[cfg(feature = "error-context")] @@ -29,16 +30,19 @@ pub struct KindFormatter; impl ErrorFormatter for KindFormatter { fn format_error(error: &crate::error::Error) -> StyledStr { + use std::fmt::Write as _; + let styles = &error.inner.styles; + let mut styled = StyledStr::new(); - start_error(&mut styled); + start_error(&mut styled, styles); if let Some(msg) = error.kind().as_str() { - styled.none(msg.to_owned()); + styled.push_str(msg); } else if let Some(source) = error.inner.source.as_ref() { - styled.none(source.to_string()); + let _ = write!(styled, "{}", source); } else { - styled.none("unknown cause"); + styled.push_str("unknown cause"); } - styled.none("\n"); + styled.push_str("\n"); styled } } @@ -53,53 +57,60 @@ pub struct RichFormatter; #[cfg(feature = "error-context")] impl ErrorFormatter for RichFormatter { fn format_error(error: &crate::error::Error) -> StyledStr { + use std::fmt::Write as _; + let styles = &error.inner.styles; + let valid = &styles.get_valid(); + let mut styled = StyledStr::new(); - start_error(&mut styled); + start_error(&mut styled, styles); - if !write_dynamic_context(error, &mut styled) { + if !write_dynamic_context(error, &mut styled, styles) { if let Some(msg) = error.kind().as_str() { - styled.none(msg.to_owned()); + styled.push_str(msg); } else if let Some(source) = error.inner.source.as_ref() { - styled.none(source.to_string()); + let _ = write!(styled, "{}", source); } else { - styled.none("unknown cause"); + styled.push_str("unknown cause"); } } let mut suggested = false; if let Some(valid) = error.get(ContextKind::SuggestedSubcommand) { - styled.none("\n"); + styled.push_str("\n"); if !suggested { - styled.none("\n"); + styled.push_str("\n"); suggested = true; } - did_you_mean(&mut styled, "subcommand", valid); + did_you_mean(&mut styled, styles, "subcommand", valid); } if let Some(valid) = error.get(ContextKind::SuggestedArg) { - styled.none("\n"); + styled.push_str("\n"); if !suggested { - styled.none("\n"); + styled.push_str("\n"); suggested = true; } - did_you_mean(&mut styled, "argument", valid); + did_you_mean(&mut styled, styles, "argument", valid); } if let Some(valid) = error.get(ContextKind::SuggestedValue) { - styled.none("\n"); + styled.push_str("\n"); if !suggested { - styled.none("\n"); + styled.push_str("\n"); suggested = true; } - did_you_mean(&mut styled, "value", valid); + did_you_mean(&mut styled, styles, "value", valid); } let suggestions = error.get(ContextKind::Suggested); if let Some(ContextValue::StyledStrs(suggestions)) = suggestions { if !suggested { - styled.none("\n"); + styled.push_str("\n"); } for suggestion in suggestions { - styled.none("\n"); - styled.none(TAB); - styled.good("tip: "); + let _ = write!( + styled, + "\n{TAB}{}tip:{} ", + valid.render(), + valid.render_reset() + ); styled.push_styled(suggestion); } } @@ -109,20 +120,30 @@ impl ErrorFormatter for RichFormatter { put_usage(&mut styled, usage); } - try_help(&mut styled, error.inner.help_flag); + try_help(&mut styled, styles, error.inner.help_flag); styled } } -fn start_error(styled: &mut StyledStr) { - styled.error("error:"); - styled.none(" "); +fn start_error(styled: &mut StyledStr, styles: &Styles) { + use std::fmt::Write as _; + let error = &styles.get_error(); + let _ = write!(styled, "{}error:{} ", error.render(), error.render_reset()); } #[must_use] #[cfg(feature = "error-context")] -fn write_dynamic_context(error: &crate::error::Error, styled: &mut StyledStr) -> bool { +fn write_dynamic_context( + error: &crate::error::Error, + styled: &mut StyledStr, + styles: &Styles, +) -> bool { + use std::fmt::Write as _; + let valid = styles.get_valid(); + let invalid = styles.get_invalid(); + let literal = styles.get_literal(); + match error.kind() { ErrorKind::ArgumentConflict => { let invalid_arg = error.get(ContextKind::InvalidArg); @@ -131,30 +152,42 @@ fn write_dynamic_context(error: &crate::error::Error, styled: &mut StyledStr) -> (invalid_arg, prior_arg) { if ContextValue::String(invalid_arg.clone()) == *prior_arg { - styled.none("the argument '"); - styled.warning(invalid_arg); - styled.none("' cannot be used multiple times"); + let _ = write!( + styled, + "the argument '{}{invalid_arg}{}' cannot be used multiple times", + invalid.render(), + invalid.render_reset() + ); } else { - styled.none("the argument '"); - styled.warning(invalid_arg); - styled.none("' cannot be used with"); + let _ = write!( + styled, + "the argument '{}{invalid_arg}{}' cannot be used with", + invalid.render(), + invalid.render_reset() + ); match prior_arg { ContextValue::Strings(values) => { - styled.none(":"); + styled.push_str(":"); for v in values { - styled.none("\n"); - styled.none(TAB); - styled.warning(&**v); + let _ = write!( + styled, + "\n{TAB}{}{v}{}", + invalid.render(), + invalid.render_reset() + ); } } ContextValue::String(value) => { - styled.none(" '"); - styled.warning(value); - styled.none("'"); + let _ = write!( + styled, + " '{}{value}{}'", + invalid.render(), + invalid.render_reset() + ); } _ => { - styled.none(" one or more of the other specified arguments"); + styled.push_str(" one or more of the other specified arguments"); } } } @@ -166,9 +199,12 @@ fn write_dynamic_context(error: &crate::error::Error, styled: &mut StyledStr) -> ErrorKind::NoEquals => { let invalid_arg = error.get(ContextKind::InvalidArg); if let Some(ContextValue::String(invalid_arg)) = invalid_arg { - styled.none("equal sign is needed when assigning values to '"); - styled.warning(invalid_arg); - styled.none("'"); + let _ = write!( + styled, + "equal sign is needed when assigning values to '{}{invalid_arg}{}'", + invalid.render(), + invalid.render_reset() + ); true } else { false @@ -183,31 +219,46 @@ fn write_dynamic_context(error: &crate::error::Error, styled: &mut StyledStr) -> ) = (invalid_arg, invalid_value) { if invalid_value.is_empty() { - styled.none("a value is required for '"); - styled.warning(invalid_arg); - styled.none("' but none was supplied"); + let _ = write!( + styled, + "a value is required for '{}{invalid_arg}{}' but none was supplied", + invalid.render(), + invalid.render_reset() + ); } else { - styled.none("invalid value '"); - styled.none(invalid_value); - styled.none("' for '"); - styled.warning(invalid_arg); - styled.none("'"); + let _ = write!( + styled, + "invalid value '{}{invalid_value}{}' for '{}{invalid_arg}{}'", + invalid.render(), + invalid.render_reset(), + literal.render(), + literal.render_reset() + ); } let possible_values = error.get(ContextKind::ValidValue); if let Some(ContextValue::Strings(possible_values)) = possible_values { if !possible_values.is_empty() { - styled.none("\n"); - styled.none(TAB); - styled.none("[possible values: "); + let _ = write!(styled, "\n{TAB}[possible values: "); if let Some((last, elements)) = possible_values.split_last() { for v in elements { - styled.good(escape(v)); - styled.none(", "); + let _ = write!( + styled, + "{}{}{}, ", + valid.render(), + Escape(v), + valid.render_reset() + ); } - styled.good(escape(last)); + let _ = write!( + styled, + "{}{}{}", + valid.render(), + Escape(last), + valid.render_reset() + ); } - styled.none("]"); + styled.push_str("]"); } } true @@ -218,9 +269,12 @@ fn write_dynamic_context(error: &crate::error::Error, styled: &mut StyledStr) -> ErrorKind::InvalidSubcommand => { let invalid_sub = error.get(ContextKind::InvalidSubcommand); if let Some(ContextValue::String(invalid_sub)) = invalid_sub { - styled.none("unrecognized subcommand '"); - styled.warning(invalid_sub); - styled.none("'"); + let _ = write!( + styled, + "unrecognized subcommand '{}{invalid_sub}{}'", + invalid.render(), + invalid.render_reset() + ); true } else { false @@ -229,11 +283,14 @@ fn write_dynamic_context(error: &crate::error::Error, styled: &mut StyledStr) -> ErrorKind::MissingRequiredArgument => { let invalid_arg = error.get(ContextKind::InvalidArg); if let Some(ContextValue::Strings(invalid_arg)) = invalid_arg { - styled.none("the following required arguments were not provided:"); + styled.push_str("the following required arguments were not provided:"); for v in invalid_arg { - styled.none("\n"); - styled.none(TAB); - styled.good(&**v); + let _ = write!( + styled, + "\n{TAB}{}{v}{}", + valid.render(), + valid.render_reset() + ); } true } else { @@ -243,24 +300,36 @@ fn write_dynamic_context(error: &crate::error::Error, styled: &mut StyledStr) -> ErrorKind::MissingSubcommand => { let invalid_sub = error.get(ContextKind::InvalidSubcommand); if let Some(ContextValue::String(invalid_sub)) = invalid_sub { - styled.none("'"); - styled.warning(invalid_sub); - styled.none("' requires a subcommand but one was not provided"); + let _ = write!( + styled, + "'{}{invalid_sub}{}' requires a subcommand but one was not provided", + invalid.render(), + invalid.render_reset() + ); let possible_values = error.get(ContextKind::ValidSubcommand); if let Some(ContextValue::Strings(possible_values)) = possible_values { if !possible_values.is_empty() { - styled.none("\n"); - styled.none(TAB); - styled.none("[subcommands: "); + let _ = write!(styled, "\n{TAB}[subcommands: "); if let Some((last, elements)) = possible_values.split_last() { for v in elements { - styled.good(escape(v)); - styled.none(", "); + let _ = write!( + styled, + "{}{}{}, ", + valid.render(), + Escape(v), + valid.render_reset() + ); } - styled.good(escape(last)); + let _ = write!( + styled, + "{}{}{}", + valid.render(), + Escape(last), + valid.render_reset() + ); } - styled.none("]"); + styled.push_str("]"); } } @@ -278,11 +347,14 @@ fn write_dynamic_context(error: &crate::error::Error, styled: &mut StyledStr) -> Some(ContextValue::String(invalid_value)), ) = (invalid_arg, invalid_value) { - styled.none("unexpected value '"); - styled.warning(invalid_value); - styled.none("' for '"); - styled.warning(invalid_arg); - styled.none("' found; no more were expected"); + let _ = write!( + styled, + "unexpected value '{}{invalid_value}{}' for '{}{invalid_arg}{}' found; no more were expected", + invalid.render(), + invalid.render_reset(), + literal.render(), + literal.render_reset(), + ); true } else { false @@ -299,12 +371,16 @@ fn write_dynamic_context(error: &crate::error::Error, styled: &mut StyledStr) -> ) = (invalid_arg, actual_num_values, min_values) { let were_provided = singular_or_plural(*actual_num_values as usize); - styled.warning(min_values.to_string()); - styled.none(" more values required by '"); - styled.warning(invalid_arg); - styled.none("'; only "); - styled.warning(actual_num_values.to_string()); - styled.none(were_provided); + let _ = write!( + styled, + "{}{min_values}{} more values required by '{}{invalid_arg}{}'; only {}{actual_num_values}{}{were_provided}", + valid.render(), + valid.render_reset(), + literal.render(), + literal.render_reset(), + invalid.render(), + invalid.render_reset(), + ); true } else { false @@ -318,15 +394,16 @@ fn write_dynamic_context(error: &crate::error::Error, styled: &mut StyledStr) -> Some(ContextValue::String(invalid_value)), ) = (invalid_arg, invalid_value) { - styled.none("invalid value '"); - styled.warning(invalid_value); - styled.none("' for '"); - styled.warning(invalid_arg); + let _ = write!( + styled, + "invalid value '{}{invalid_value}{}' for '{}{invalid_arg}{}'", + invalid.render(), + invalid.render_reset(), + literal.render(), + literal.render_reset(), + ); if let Some(source) = error.inner.source.as_deref() { - styled.none("': "); - styled.none(source.to_string()); - } else { - styled.none("'"); + let _ = write!(styled, ": {}", source); } true } else { @@ -344,12 +421,16 @@ fn write_dynamic_context(error: &crate::error::Error, styled: &mut StyledStr) -> ) = (invalid_arg, actual_num_values, num_values) { let were_provided = singular_or_plural(*actual_num_values as usize); - styled.warning(num_values.to_string()); - styled.none(" values required for '"); - styled.warning(invalid_arg); - styled.none("' but "); - styled.warning(actual_num_values.to_string()); - styled.none(were_provided); + let _ = write!( + styled, + "{}{num_values}{} values required for '{}{invalid_arg}{}' but {}{actual_num_values}{}{were_provided}", + valid.render(), + valid.render_reset(), + literal.render(), + literal.render_reset(), + invalid.render(), + invalid.render_reset(), + ); true } else { false @@ -358,9 +439,12 @@ fn write_dynamic_context(error: &crate::error::Error, styled: &mut StyledStr) -> ErrorKind::UnknownArgument => { let invalid_arg = error.get(ContextKind::InvalidArg); if let Some(ContextValue::String(invalid_arg)) = invalid_arg { - styled.none("unexpected argument '"); - styled.warning(invalid_arg.to_string()); - styled.none("' found"); + let _ = write!( + styled, + "unexpected argument '{}{invalid_arg}{}' found", + invalid.render(), + invalid.render_reset(), + ); true } else { false @@ -376,17 +460,18 @@ fn write_dynamic_context(error: &crate::error::Error, styled: &mut StyledStr) -> pub(crate) fn format_error_message( message: &str, + styles: &Styles, cmd: Option<&Command>, usage: Option<&StyledStr>, ) -> StyledStr { let mut styled = StyledStr::new(); - start_error(&mut styled); - styled.none(message); + start_error(&mut styled, styles); + styled.push_str(message); if let Some(usage) = usage { put_usage(&mut styled, usage); } if let Some(cmd) = cmd { - try_help(&mut styled, get_help_flag(cmd)); + try_help(&mut styled, styles, get_help_flag(cmd)); } styled } @@ -401,7 +486,7 @@ fn singular_or_plural(n: usize) -> &'static str { } fn put_usage(styled: &mut StyledStr, usage: &StyledStr) { - styled.none("\n\n"); + styled.push_str("\n\n"); styled.push_styled(usage); } @@ -415,52 +500,66 @@ pub(crate) fn get_help_flag(cmd: &Command) -> Option<&'static str> { } } -fn try_help(styled: &mut StyledStr, help: Option<&str>) { +fn try_help(styled: &mut StyledStr, styles: &Styles, help: Option<&str>) { if let Some(help) = help { - styled.none("\n\nFor more information, try '"); - styled.literal(help.to_owned()); - styled.none("'.\n"); + use std::fmt::Write as _; + let literal = &styles.get_literal(); + let _ = write!( + styled, + "\n\nFor more information, try '{}{help}{}'.\n", + literal.render(), + literal.render_reset() + ); } else { - styled.none("\n"); + styled.push_str("\n"); } } #[cfg(feature = "error-context")] -fn did_you_mean(styled: &mut StyledStr, context: &str, valid: &ContextValue) { - styled.none(TAB); - styled.good("tip:"); +fn did_you_mean(styled: &mut StyledStr, styles: &Styles, context: &str, valid: &ContextValue) { + use std::fmt::Write as _; + + let _ = write!( + styled, + "{TAB}{}tip:{}", + styles.get_valid().render(), + styles.get_valid().render_reset() + ); if let ContextValue::String(valid) = valid { - styled.none(" a similar "); - styled.none(context); - styled.none(" exists: '"); - styled.good(valid); - styled.none("'"); + let _ = write!( + styled, + " a similar {context} exists: '{}{valid}{}'", + styles.get_valid().render(), + styles.get_valid().render_reset() + ); } else if let ContextValue::Strings(valid) = valid { if valid.len() == 1 { - styled.none(" a similar "); - styled.none(context); - styled.none(" exists: "); + let _ = write!(styled, " a similar {context} exists: ",); } else { - styled.none(" some similar "); - styled.none(context); - styled.none("s exist: "); + let _ = write!(styled, " some similar {context}s exist: ",); } for (i, valid) in valid.iter().enumerate() { if i != 0 { - styled.none(", "); + styled.push_str(", "); } - styled.none("'"); - styled.good(valid); - styled.none("'"); + let _ = write!( + styled, + "'{}{valid}{}'", + styles.get_valid().render(), + styles.get_valid().render_reset() + ); } } } -fn escape(s: impl AsRef) -> String { - let s = s.as_ref(); - if s.contains(char::is_whitespace) { - format!("{s:?}") - } else { - s.to_owned() +struct Escape<'s>(&'s str); + +impl<'s> std::fmt::Display for Escape<'s> { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + if self.0.contains(char::is_whitespace) { + std::fmt::Debug::fmt(self.0, f) + } else { + self.0.fmt(f) + } } } diff --git a/clap_builder/src/error/mod.rs b/clap_builder/src/error/mod.rs index 52f17bd5649..cb362966944 100644 --- a/clap_builder/src/error/mod.rs +++ b/clap_builder/src/error/mod.rs @@ -18,6 +18,7 @@ use std::{ // Internal use crate::builder::StyledStr; +use crate::builder::Styles; use crate::output::fmt::Colorizer; use crate::output::fmt::Stream; use crate::parser::features::suggestions; @@ -69,6 +70,7 @@ struct ErrorInner { message: Option, source: Option>, help_flag: Option<&'static str>, + styles: Styles, color_when: ColorChoice, color_help_when: ColorChoice, backtrace: Option, @@ -132,6 +134,7 @@ impl Error { message: None, source: None, help_flag: None, + styles: Styles::plain(), color_when: ColorChoice::Never, color_help_when: ColorChoice::Never, backtrace: Backtrace::new(), @@ -144,7 +147,8 @@ impl Error { /// /// Generally, this is used with [`Error::new`] pub fn with_cmd(self, cmd: &Command) -> Self { - self.set_color(cmd.get_color()) + self.set_styles(cmd.get_styles().clone()) + .set_color(cmd.get_color()) .set_colored_help(cmd.color_help()) .set_help_flag(format::get_help_flag(cmd)) } @@ -295,6 +299,11 @@ impl Error { self } + pub(crate) fn set_styles(mut self, styles: Styles) -> Self { + self.inner.styles = styles; + self + } + pub(crate) fn set_color(mut self, color_when: ColorChoice) -> Self { self.inner.color_when = color_when; self @@ -434,18 +443,23 @@ impl Error { name: String, usage: Option, ) -> Self { + use std::fmt::Write as _; + let styles = cmd.get_styles(); + let invalid = &styles.get_invalid(); + let valid = &styles.get_valid(); let mut err = Self::new(ErrorKind::InvalidSubcommand).with_cmd(cmd); #[cfg(feature = "error-context")] { let mut styled_suggestion = StyledStr::new(); - styled_suggestion.none("to pass '"); - styled_suggestion.warning(&subcmd); - styled_suggestion.none("' as a value, use '"); - styled_suggestion.good(name); - styled_suggestion.good(" -- "); - styled_suggestion.good(&subcmd); - styled_suggestion.none("'"); + let _ = write!( + styled_suggestion, + "to pass '{}{subcmd}{}' as a value, use '{}{name} -- {subcmd}{}'", + invalid.render(), + invalid.render_reset(), + valid.render(), + valid.render_reset() + ); err = err.extend_context_unchecked([ (ContextKind::InvalidSubcommand, ContextValue::String(subcmd)), @@ -661,6 +675,10 @@ impl Error { suggested_trailing_arg: bool, usage: Option, ) -> Self { + use std::fmt::Write as _; + let styles = cmd.get_styles(); + let invalid = &styles.get_invalid(); + let valid = &styles.get_valid(); let mut err = Self::new(ErrorKind::UnknownArgument).with_cmd(cmd); #[cfg(feature = "error-context")] @@ -668,12 +686,14 @@ impl Error { let mut suggestions = vec![]; if suggested_trailing_arg { let mut styled_suggestion = StyledStr::new(); - styled_suggestion.none("to pass '"); - styled_suggestion.warning(&arg); - styled_suggestion.none("' as a value, use '"); - styled_suggestion.good("-- "); - styled_suggestion.good(&arg); - styled_suggestion.none("'"); + let _ = write!( + styled_suggestion, + "to pass '{}{arg}{}' as a value, use '{}-- {arg}{}'", + invalid.render(), + invalid.render_reset(), + valid.render(), + valid.render_reset() + ); suggestions.push(styled_suggestion); } @@ -686,12 +706,12 @@ impl Error { match did_you_mean { Some((flag, Some(sub))) => { let mut styled_suggestion = StyledStr::new(); - styled_suggestion.none("'"); - styled_suggestion.good(sub); - styled_suggestion.none(" "); - styled_suggestion.good("--"); - styled_suggestion.good(flag); - styled_suggestion.none("' exists"); + let _ = write!( + styled_suggestion, + "'{}{sub} --{flag}{}' exists", + valid.render(), + valid.render_reset() + ); suggestions.push(styled_suggestion); } Some((flag, None)) => { @@ -718,16 +738,23 @@ impl Error { arg: String, usage: Option, ) -> Self { + use std::fmt::Write as _; + let styles = cmd.get_styles(); + let invalid = &styles.get_invalid(); + let valid = &styles.get_valid(); let mut err = Self::new(ErrorKind::UnknownArgument).with_cmd(cmd); #[cfg(feature = "error-context")] { let mut styled_suggestion = StyledStr::new(); - styled_suggestion.none("subcommand '"); - styled_suggestion.good(&arg); - styled_suggestion.none("' exists; to use it, remove the '"); - styled_suggestion.warning("--"); - styled_suggestion.none("' before it"); + let _ = write!( + styled_suggestion, + "subcommand '{}{arg}{}' exists; to use it, remove the '{}--{}' before it", + valid.render(), + valid.render_reset(), + invalid.render(), + invalid.render_reset() + ); err = err.extend_context_unchecked([ (ContextKind::InvalidArg, ContextValue::String(arg)), @@ -747,7 +774,7 @@ impl Error { fn formatted(&self) -> Cow<'_, StyledStr> { if let Some(message) = self.inner.message.as_ref() { - message.formatted() + message.formatted(&self.inner.styles) } else { let styled = F::format_error(self); Cow::Owned(styled) @@ -806,7 +833,12 @@ impl Message { let mut message = String::new(); std::mem::swap(s, &mut message); - let styled = format::format_error_message(&message, Some(cmd), usage.as_ref()); + let styled = format::format_error_message( + &message, + cmd.get_styles(), + Some(cmd), + usage.as_ref(), + ); *self = Self::Formatted(styled); } @@ -814,10 +846,10 @@ impl Message { } } - fn formatted(&self) -> Cow { + fn formatted(&self, styles: &Styles) -> Cow { match self { Message::Raw(s) => { - let styled = format::format_error_message(s, None, None); + let styled = format::format_error_message(s, styles, None, None); Cow::Owned(styled) } diff --git a/clap_builder/src/lib.rs b/clap_builder/src/lib.rs index a8c1201e0e4..97c32c141bd 100644 --- a/clap_builder/src/lib.rs +++ b/clap_builder/src/lib.rs @@ -16,6 +16,10 @@ clippy::single_char_pattern )] #![forbid(unsafe_code)] +// Wanting consistency in our calls +#![allow(clippy::write_with_newline)] +// Gets in the way of logging +#![allow(clippy::let_and_return)] // HACK https://github.com/rust-lang/rust-clippy/issues/7290 #![allow(clippy::single_component_path_imports)] #![allow(clippy::branches_sharing_code)] diff --git a/clap_builder/src/macros.rs b/clap_builder/src/macros.rs index 82a8811984f..59135e21252 100644 --- a/clap_builder/src/macros.rs +++ b/clap_builder/src/macros.rs @@ -628,12 +628,13 @@ macro_rules! impl_settings { #[cfg(feature = "debug")] macro_rules! debug { ($($arg:tt)*) => ({ - let prefix = format!("[{:>w$}] \t", module_path!(), w = 28); + use std::fmt::Write as _; + let hint = anstyle::Style::new().dimmed(); + + let module_path = module_path!(); let body = format!($($arg)*); let mut styled = $crate::builder::StyledStr::new(); - styled.hint(prefix); - styled.hint(body); - styled.none("\n"); + let _ = write!(styled, "{}[{module_path:>28}]{body}{}\n", hint.render(), hint.render_reset()); let color = $crate::output::fmt::Colorizer::new($crate::output::fmt::Stream::Stderr, $crate::ColorChoice::Auto).with_content(styled); let _ = color.print(); }) diff --git a/clap_builder/src/output/help.rs b/clap_builder/src/output/help.rs index 4921f5f71f3..410616eb1b8 100644 --- a/clap_builder/src/output/help.rs +++ b/clap_builder/src/output/help.rs @@ -33,5 +33,5 @@ pub(crate) fn write_help(writer: &mut StyledStr, cmd: &Command, usage: &Usage<'_ // Remove any extra lines caused by book keeping writer.trim(); // Ensure there is still a trailing newline - writer.none("\n"); + writer.push_str("\n"); } diff --git a/clap_builder/src/output/help_template.rs b/clap_builder/src/output/help_template.rs index cea9a7687bb..86a61169c32 100644 --- a/clap_builder/src/output/help_template.rs +++ b/clap_builder/src/output/help_template.rs @@ -1,3 +1,8 @@ +// HACK: for rust 1.64 (1.68 doesn't need this since this is in lib.rs) +// +// Wanting consistency in our calls +#![allow(clippy::write_with_newline)] + // Std use std::borrow::Cow; use std::cmp; @@ -7,6 +12,7 @@ use std::usize; use crate::builder::PossibleValue; use crate::builder::Str; use crate::builder::StyledStr; +use crate::builder::Styles; use crate::builder::{Arg, Command}; use crate::output::display_width; use crate::output::wrap; @@ -74,6 +80,7 @@ const DEFAULT_NO_ARGS_TEMPLATE: &str = "\ pub(crate) struct HelpTemplate<'cmd, 'writer> { writer: &'writer mut StyledStr, cmd: &'cmd Command, + styles: &'cmd Styles, usage: &'cmd Usage<'cmd>, next_line_help: bool, term_w: usize, @@ -112,6 +119,7 @@ impl<'cmd, 'writer> HelpTemplate<'cmd, 'writer> { HelpTemplate { writer, cmd, + styles: cmd.get_styles(), usage, next_line_help, term_w, @@ -126,10 +134,11 @@ impl<'cmd, 'writer> HelpTemplate<'cmd, 'writer> { /// [`Command::help_template`]: Command::help_template() pub(crate) fn write_templated_help(&mut self, template: &str) { debug!("HelpTemplate::write_templated_help"); + use std::fmt::Write as _; let mut parts = template.split('{'); if let Some(first) = parts.next() { - self.none(first); + self.writer.push_str(first); } for part in parts { if let Some((tag, rest)) = part.split_once('}') { @@ -163,7 +172,12 @@ impl<'cmd, 'writer> HelpTemplate<'cmd, 'writer> { self.write_about(true, true); } "usage-heading" => { - self.header("Usage:"); + let _ = write!( + self.writer, + "{}Usage:{}", + self.styles.get_usage().render(), + self.styles.get_usage().render_reset() + ); } "usage" => { self.writer.push_styled( @@ -193,7 +207,7 @@ impl<'cmd, 'writer> HelpTemplate<'cmd, 'writer> { self.write_subcommands(self.cmd); } "tab" => { - self.none(TAB); + self.writer.push_str(TAB); } "after-help" => { self.write_after_help(); @@ -202,12 +216,10 @@ impl<'cmd, 'writer> HelpTemplate<'cmd, 'writer> { self.write_before_help(); } _ => { - self.none("{"); - self.none(tag); - self.none("}"); + let _ = write!(self.writer, "{{{tag}}}"); } } - self.none(rest); + self.writer.push_str(rest); } } } @@ -227,7 +239,7 @@ impl<'cmd, 'writer> HelpTemplate<'cmd, 'writer> { .replace("{n}", "\n"), self.term_w, ); - self.none(&display_name); + self.writer.push_string(display_name); } /// Writes binary name of a Parser Object to the wrapped stream. @@ -245,7 +257,7 @@ impl<'cmd, 'writer> HelpTemplate<'cmd, 'writer> { } else { wrap(&self.cmd.get_name().replace("{n}", "\n"), self.term_w) }; - self.none(&bin_name); + self.writer.push_string(bin_name); } fn write_version(&mut self) { @@ -254,18 +266,18 @@ impl<'cmd, 'writer> HelpTemplate<'cmd, 'writer> { .get_version() .or_else(|| self.cmd.get_long_version()); if let Some(output) = version { - self.none(wrap(output, self.term_w)); + self.writer.push_string(wrap(output, self.term_w)); } } fn write_author(&mut self, before_new_line: bool, after_new_line: bool) { if let Some(author) = self.cmd.get_author() { if before_new_line { - self.none("\n"); + self.writer.push_str("\n"); } - self.none(wrap(author, self.term_w)); + self.writer.push_string(wrap(author, self.term_w)); if after_new_line { - self.none("\n"); + self.writer.push_str("\n"); } } } @@ -278,14 +290,14 @@ impl<'cmd, 'writer> HelpTemplate<'cmd, 'writer> { }; if let Some(output) = about { if before_new_line { - self.none("\n"); + self.writer.push_str("\n"); } let mut output = output.clone(); output.replace_newline_var(); output.wrap(self.term_w); self.writer.push_styled(&output); if after_new_line { - self.none("\n"); + self.writer.push_str("\n"); } } } @@ -304,7 +316,7 @@ impl<'cmd, 'writer> HelpTemplate<'cmd, 'writer> { output.replace_newline_var(); output.wrap(self.term_w); self.writer.push_styled(&output); - self.none("\n\n"); + self.writer.push_str("\n\n"); } } @@ -318,7 +330,7 @@ impl<'cmd, 'writer> HelpTemplate<'cmd, 'writer> { self.cmd.get_after_help() }; if let Some(output) = after_help { - self.none("\n\n"); + self.writer.push_str("\n\n"); let mut output = output.clone(); output.replace_newline_var(); output.wrap(self.term_w); @@ -333,6 +345,9 @@ impl<'cmd, 'writer> HelpTemplate<'cmd, 'writer> { /// including titles of a Parser Object to the wrapped stream. pub(crate) fn write_all_args(&mut self) { debug!("HelpTemplate::write_all_args"); + use std::fmt::Write as _; + let header = &self.styles.get_header(); + let pos = self .cmd .get_positionals() @@ -357,39 +372,52 @@ impl<'cmd, 'writer> HelpTemplate<'cmd, 'writer> { if subcmds { if !first { - self.none("\n\n"); + self.writer.push_str("\n\n"); } first = false; let default_help_heading = Str::from("Commands"); - self.header( - self.cmd - .get_subcommand_help_heading() - .unwrap_or(&default_help_heading), + let help_heading = self + .cmd + .get_subcommand_help_heading() + .unwrap_or(&default_help_heading); + let _ = write!( + self.writer, + "{}{help_heading}:{}\n", + header.render(), + header.render_reset() ); - self.header(":"); - self.none("\n"); self.write_subcommands(self.cmd); } if !pos.is_empty() { if !first { - self.none("\n\n"); + self.writer.push_str("\n\n"); } first = false; // Write positional args if any - self.header("Arguments:"); - self.none("\n"); + let help_heading = "Arguments"; + let _ = write!( + self.writer, + "{}{help_heading}:{}\n", + header.render(), + header.render_reset() + ); self.write_args(&pos, "Arguments", positional_sort_key); } if !non_pos.is_empty() { if !first { - self.none("\n\n"); + self.writer.push_str("\n\n"); } first = false; - self.header("Options:"); - self.none("\n"); + let help_heading = "Options"; + let _ = write!( + self.writer, + "{}{help_heading}:{}\n", + header.render(), + header.render_reset() + ); self.write_args(&non_pos, "Options", option_sort_key); } if !custom_headings.is_empty() { @@ -408,12 +436,15 @@ impl<'cmd, 'writer> HelpTemplate<'cmd, 'writer> { if !args.is_empty() { if !first { - self.none("\n\n"); + self.writer.push_str("\n\n"); } first = false; - self.header(heading); - self.header(":"); - self.none("\n"); + let _ = write!( + self.writer, + "{}{heading}:{}\n", + header.render(), + header.render_reset() + ); self.write_args(&args, heading, option_sort_key); } } @@ -451,9 +482,9 @@ impl<'cmd, 'writer> HelpTemplate<'cmd, 'writer> { for (i, (_, arg)) in ord_v.iter().enumerate() { if i != 0 { - self.none("\n"); + self.writer.push_str("\n"); if next_line_help && self.use_long { - self.none("\n"); + self.writer.push_str("\n"); } } self.write_arg(arg, next_line_help, longest); @@ -464,10 +495,11 @@ impl<'cmd, 'writer> HelpTemplate<'cmd, 'writer> { fn write_arg(&mut self, arg: &Arg, next_line_help: bool, longest: usize) { let spec_vals = &self.spec_vals(arg); - self.none(TAB); + self.writer.push_str(TAB); self.short(arg); self.long(arg); - self.writer.push_styled(&arg.stylize_arg_suffix(None)); + self.writer + .push_styled(&arg.stylize_arg_suffix(self.styles, None)); self.align_to_about(arg, next_line_help, longest); let about = if self.use_long { @@ -486,22 +518,37 @@ impl<'cmd, 'writer> HelpTemplate<'cmd, 'writer> { /// Writes argument's short command to the wrapped stream. fn short(&mut self, arg: &Arg) { debug!("HelpTemplate::short"); + use std::fmt::Write as _; + let literal = &self.styles.get_literal(); if let Some(s) = arg.get_short() { - self.literal(format!("-{s}")); + let _ = write!( + self.writer, + "{}-{s}{}", + literal.render(), + literal.render_reset() + ); } else if arg.get_long().is_some() { - self.none(" "); + self.writer.push_str(" "); } } /// Writes argument's long command to the wrapped stream. fn long(&mut self, arg: &Arg) { debug!("HelpTemplate::long"); + use std::fmt::Write as _; + let literal = &self.styles.get_literal(); + if let Some(long) = arg.get_long() { if arg.get_short().is_some() { - self.none(", "); + self.writer.push_str(", "); } - self.literal(format!("--{long}")); + let _ = write!( + self.writer, + "{}--{long}{}", + literal.render(), + literal.render_reset() + ); } } @@ -513,9 +560,10 @@ impl<'cmd, 'writer> HelpTemplate<'cmd, 'writer> { next_line_help, longest ); - if self.use_long || next_line_help { + let padding = if self.use_long || next_line_help { // long help prints messages on the next line so it doesn't need to align text debug!("HelpTemplate::align_to_about: printing long help so skip alignment"); + 0 } else if !arg.is_positional() { let self_len = display_width(&arg.to_string()); // Since we're writing spaces from the tab point we first need to know if we @@ -533,7 +581,7 @@ impl<'cmd, 'writer> HelpTemplate<'cmd, 'writer> { self_len, spcs ); - self.spaces(spcs); + spcs } else { let self_len = display_width(&arg.to_string()); let padding = TAB_WIDTH; @@ -543,8 +591,10 @@ impl<'cmd, 'writer> HelpTemplate<'cmd, 'writer> { self_len, spcs ); - self.spaces(spcs); - } + spcs + }; + + self.write_padding(padding); } /// Writes argument's help to the wrapped stream. @@ -557,13 +607,15 @@ impl<'cmd, 'writer> HelpTemplate<'cmd, 'writer> { longest: usize, ) { debug!("HelpTemplate::help"); + use std::fmt::Write as _; + let literal = &self.styles.get_literal(); // Is help on next line, if so then indent if next_line_help { debug!("HelpTemplate::help: Next Line...{:?}", next_line_help); - self.none("\n"); - self.none(TAB); - self.none(NEXT_LINE_INDENT); + self.writer.push_str("\n"); + self.writer.push_str(TAB); + self.writer.push_str(NEXT_LINE_INDENT); } let spaces = if next_line_help { @@ -585,9 +637,9 @@ impl<'cmd, 'writer> HelpTemplate<'cmd, 'writer> { } else { " " }; - help.none(sep); + help.push_str(sep); } - help.none(spec_vals); + help.push_str(spec_vals); } let avail_chars = self.term_w.saturating_sub(spaces); debug!( @@ -612,11 +664,6 @@ impl<'cmd, 'writer> HelpTemplate<'cmd, 'writer> { "HelpTemplate::help: Found possible vals...{:?}", possible_vals ); - if !help_is_empty { - self.none("\n\n"); - self.spaces(spaces); - } - self.none("Possible values:"); let longest = possible_vals .iter() .filter_map(|f| f.get_visible_quoted_name().map(|name| display_width(&name))) @@ -641,21 +688,29 @@ impl<'cmd, 'writer> HelpTemplate<'cmd, 'writer> { }; let trailing_indent = self.get_spaces(trailing_indent); + if !help_is_empty { + let _ = write!(self.writer, "\n\n{:spaces$}", ""); + } + self.writer.push_str("Possible values:"); for pv in possible_vals.iter().filter(|pv| !pv.is_hide_set()) { - self.none("\n"); - self.spaces(spaces); - self.none("- "); - self.literal(pv.get_name()); + let name = pv.get_name(); + let _ = write!( + self.writer, + "\n{:spaces$}- {}{name}{}", + "", + literal.render(), + literal.render_reset() + ); if let Some(help) = pv.get_help() { debug!("HelpTemplate::help: Possible Value help"); if possible_value_new_line { - self.none(":\n"); - self.spaces(trailing_indent.len()); + let padding = trailing_indent.len(); + let _ = write!(self.writer, ":\n{:padding$}", ""); } else { - self.none(": "); // To align help messages - self.spaces(longest - display_width(pv.get_name())); + let padding = longest - display_width(pv.get_name()); + let _ = write!(self.writer, ": {:padding$}", ""); } let avail_chars = if self.term_w > trailing_indent.len() { @@ -801,24 +856,13 @@ impl<'cmd, 'writer> HelpTemplate<'cmd, 'writer> { spec_vals.join(connector) } - fn header>(&mut self, msg: T) { - self.writer.header(msg); - } - - fn literal>(&mut self, msg: T) { - self.writer.literal(msg); - } - - fn none>(&mut self, msg: T) { - self.writer.none(msg); - } - fn get_spaces(&self, n: usize) -> String { " ".repeat(n) } - fn spaces(&mut self, n: usize) { - self.none(self.get_spaces(n)); + fn write_padding(&mut self, amount: usize) { + use std::fmt::Write as _; + let _ = write!(self.writer, "{:amount$}", ""); } } @@ -827,6 +871,9 @@ impl<'cmd, 'writer> HelpTemplate<'cmd, 'writer> { /// Writes help for subcommands of a Parser Object to the wrapped stream. fn write_subcommands(&mut self, cmd: &Command) { debug!("HelpTemplate::write_subcommands"); + use std::fmt::Write as _; + let literal = &self.styles.get_literal(); + // The shortest an arg can legally be is 2 (i.e. '-x') let mut longest = 2; let mut ord_v = Vec::new(); @@ -835,14 +882,28 @@ impl<'cmd, 'writer> HelpTemplate<'cmd, 'writer> { .filter(|subcommand| should_show_subcommand(subcommand)) { let mut styled = StyledStr::new(); - styled.literal(subcommand.get_name()); + let name = subcommand.get_name(); + let _ = write!( + styled, + "{}{name}{}", + literal.render(), + literal.render_reset() + ); if let Some(short) = subcommand.get_short_flag() { - styled.none(", "); - styled.literal(format!("-{short}")); + let _ = write!( + styled, + ", {}-{short}{}", + literal.render(), + literal.render_reset() + ); } if let Some(long) = subcommand.get_long_flag() { - styled.none(", "); - styled.literal(format!("--{long}")); + let _ = write!( + styled, + ", {}--{long}{}", + literal.render(), + literal.render_reset() + ); } longest = longest.max(styled.display_width()); ord_v.push((subcommand.get_display_order(), styled, subcommand)); @@ -858,7 +919,7 @@ impl<'cmd, 'writer> HelpTemplate<'cmd, 'writer> { if first { first = false; } else { - self.none("\n"); + self.writer.push_str("\n"); } self.write_subcommand(sc_str, sc, next_line_help, longest); } @@ -942,12 +1003,12 @@ impl<'cmd, 'writer> HelpTemplate<'cmd, 'writer> { /// Writes subcommand to the wrapped stream. fn subcmd(&mut self, sc_str: StyledStr, next_line_help: bool, longest: usize) { - let width = sc_str.display_width(); - - self.none(TAB); + self.writer.push_str(TAB); self.writer.push_styled(&sc_str); if !next_line_help { - self.spaces(longest + TAB_WIDTH - width); + let width = sc_str.display_width(); + let padding = longest + TAB_WIDTH - width; + self.write_padding(padding); } } } diff --git a/clap_builder/src/output/usage.rs b/clap_builder/src/output/usage.rs index c49ca2cb063..dd99c63c7b7 100644 --- a/clap_builder/src/output/usage.rs +++ b/clap_builder/src/output/usage.rs @@ -5,6 +5,7 @@ // Internal use crate::builder::StyledStr; +use crate::builder::Styles; use crate::builder::{ArgPredicate, Command}; use crate::parser::ArgMatcher; use crate::util::ChildGraph; @@ -15,6 +16,7 @@ static DEFAULT_SUB_VALUE_NAME: &str = "COMMAND"; pub(crate) struct Usage<'cmd> { cmd: &'cmd Command, + styles: &'cmd Styles, required: Option<&'cmd ChildGraph>, } @@ -22,6 +24,7 @@ impl<'cmd> Usage<'cmd> { pub(crate) fn new(cmd: &'cmd Command) -> Self { Usage { cmd, + styles: cmd.get_styles(), required: None, } } @@ -37,9 +40,14 @@ impl<'cmd> Usage<'cmd> { debug!("Usage::create_usage_with_title"); let usage = some!(self.create_usage_no_title(used)); + use std::fmt::Write as _; let mut styled = StyledStr::new(); - styled.header("Usage:"); - styled.none(" "); + let _ = write!( + styled, + "{}Usage:{} ", + self.styles.get_usage().render(), + self.styles.get_usage().render_reset() + ); styled.push_styled(&usage); Some(styled) } @@ -72,16 +80,33 @@ impl<'cmd> Usage<'cmd> { // Creates a usage string for display in help messages (i.e. not for errors) fn create_help_usage(&self, incl_reqs: bool) -> StyledStr { debug!("Usage::create_help_usage; incl_reqs={:?}", incl_reqs); + use std::fmt::Write as _; + let literal = &self.styles.get_literal(); + let placeholder = &self.styles.get_placeholder(); let mut styled = StyledStr::new(); + let name = self .cmd .get_usage_name() .or_else(|| self.cmd.get_bin_name()) .unwrap_or_else(|| self.cmd.get_name()); - styled.literal(name); + if !name.is_empty() { + // the trim won't properly remove a leading space due to the formatting + let _ = write!( + styled, + "{}{name}{}", + literal.render(), + literal.render_reset() + ); + } if self.needs_options_tag() { - styled.placeholder(" [OPTIONS]"); + let _ = write!( + styled, + "{} [OPTIONS]{}", + placeholder.render(), + placeholder.render_reset() + ); } self.write_args(&[], !incl_reqs, &mut styled); @@ -90,32 +115,45 @@ impl<'cmd> Usage<'cmd> { if self.cmd.has_visible_subcommands() && incl_reqs || self.cmd.is_allow_external_subcommands_set() { - let placeholder = self + let value_name = self .cmd .get_subcommand_value_name() .unwrap_or(DEFAULT_SUB_VALUE_NAME); if self.cmd.is_subcommand_negates_reqs_set() || self.cmd.is_args_conflicts_with_subcommands_set() { - styled.none("\n"); - styled.none(" "); + let _ = write!(styled, "\n "); if self.cmd.is_args_conflicts_with_subcommands_set() { // Short-circuit full usage creation since no args will be relevant - styled.literal(name); + let _ = write!( + styled, + "{}{name}{}", + literal.render(), + literal.render_reset() + ); } else { styled.push_styled(&self.create_help_usage(false)); } - styled.placeholder(" <"); - styled.placeholder(placeholder); - styled.placeholder(">"); + let _ = write!( + styled, + " {}<{value_name}>{}", + placeholder.render(), + placeholder.render_reset() + ); } else if self.cmd.is_subcommand_required_set() { - styled.placeholder(" <"); - styled.placeholder(placeholder); - styled.placeholder(">"); + let _ = write!( + styled, + " {}<{value_name}>{}", + placeholder.render(), + placeholder.render_reset() + ); } else { - styled.placeholder(" ["); - styled.placeholder(placeholder); - styled.placeholder("]"); + let _ = write!( + styled, + " {}[{value_name}]{}", + placeholder.render(), + placeholder.render_reset() + ); } } styled.trim(); @@ -127,25 +165,36 @@ impl<'cmd> Usage<'cmd> { // args, and requirements fn create_smart_usage(&self, used: &[Id]) -> StyledStr { debug!("Usage::create_smart_usage"); + use std::fmt::Write; + let literal = &self.styles.get_literal(); + let placeholder = &self.styles.get_placeholder(); let mut styled = StyledStr::new(); - styled.literal( - self.cmd - .get_usage_name() - .or_else(|| self.cmd.get_bin_name()) - .unwrap_or_else(|| self.cmd.get_name()), + let bin_name = self + .cmd + .get_usage_name() + .or_else(|| self.cmd.get_bin_name()) + .unwrap_or_else(|| self.cmd.get_name()); + let _ = write!( + styled, + "{}{bin_name}{}", + literal.render(), + literal.render_reset() ); self.write_args(used, false, &mut styled); if self.cmd.is_subcommand_required_set() { - styled.placeholder(" <"); - styled.placeholder( - self.cmd - .get_subcommand_value_name() - .unwrap_or(DEFAULT_SUB_VALUE_NAME), + let value_name = self + .cmd + .get_subcommand_value_name() + .unwrap_or(DEFAULT_SUB_VALUE_NAME); + let _ = write!( + styled, + " {}<{value_name}>{}", + placeholder.render(), + placeholder.render_reset() ); - styled.placeholder(">"); } styled } @@ -189,13 +238,15 @@ impl<'cmd> Usage<'cmd> { // Returns the required args in usage string form by fully unrolling all groups pub(crate) fn write_args(&self, incls: &[Id], force_optional: bool, styled: &mut StyledStr) { for required in self.get_args(incls, force_optional) { - styled.none(" "); + styled.push_str(" "); styled.push_styled(&required); } } pub(crate) fn get_args(&self, incls: &[Id], force_optional: bool) -> Vec { debug!("Usage::get_args: incls={:?}", incls,); + use std::fmt::Write as _; + let literal = &self.styles.get_literal(); let required_owned; let required = if let Some(required) = self.required { @@ -247,7 +298,7 @@ impl<'cmd> Usage<'cmd> { continue; } - let stylized = arg.stylized(Some(!force_optional)); + let stylized = arg.stylized(self.styles, Some(!force_optional)); if let Some(index) = arg.get_index() { let new_len = index + 1; if required_positionals.len() < new_len { @@ -279,7 +330,7 @@ impl<'cmd> Usage<'cmd> { if pos.is_last_set() { let styled = required_positionals[index].take().unwrap(); let mut new = StyledStr::new(); - new.literal("-- "); + let _ = write!(new, "{}--{} ", literal.render(), literal.render_reset()); new.push_styled(&styled); required_positionals[index] = Some(new); } @@ -287,11 +338,11 @@ impl<'cmd> Usage<'cmd> { let mut styled; if pos.is_last_set() { styled = StyledStr::new(); - styled.literal("[-- "); - styled.push_styled(&pos.stylized(Some(true))); - styled.literal("]"); + let _ = write!(styled, "{}[--{} ", literal.render(), literal.render_reset()); + styled.push_styled(&pos.stylized(self.styles, Some(true))); + let _ = write!(styled, "{}]{}", literal.render(), literal.render_reset()); } else { - styled = pos.stylized(Some(false)); + styled = pos.stylized(self.styles, Some(false)); } required_positionals[index] = Some(styled); } @@ -411,7 +462,7 @@ impl<'cmd> Usage<'cmd> { continue; } - let stylized = arg.stylized(Some(true)); + let stylized = arg.stylized(self.styles, Some(true)); if let Some(index) = arg.get_index() { if !arg.is_last_set() || incl_last { let new_len = index + 1; diff --git a/src/_features.rs b/src/_features.rs index b47ee259c28..27b567c9842 100644 --- a/src/_features.rs +++ b/src/_features.rs @@ -26,3 +26,4 @@ //! **Warning:** These may contain breaking changes between minor releases. //! //! * **unstable-v5**: Preview features which will be stable on the v5.0 release +//! * **unstable-unstable-styles**: Custom theming support for clap