Repository: clap-rs/clap (language: rust)

Issue:
Make colored help output more compatible with an applications overall theme
### Please complete the following tasks

- [X] I have searched the [discussions](https://github.com/clap-rs/clap/discussions)
- [X] I have searched the existing issues

### Rust Version

rustc 1.55.0 (c8dfcfe04 2021-09-06)

### Clap Version

master

### Steps to reproduce the bug with the above code

Run [enquirer](https://github.com/termapps/enquirer#multi-select-prompt) and compare the program's regular output with its `--help` (after enabling colored help)

### Actual Behaviour

The colors clash with the application's overall theme

### Expected Behaviour

The colors are tastefully align with the rest of the theme

### Additional Context

See also #2906

In discussing [the concerns raised #2845](https://github.com/clap-rs/clap/pull/2845#issuecomment-945185102) (see also https://github.com/clap-rs/clap/discussions/2906), we realized that at least one way to describe / categorize the problem with colored help is how well it meshes with the rest of an applications theme.

One of our aims is to provide a low-effort polished ("modern") experience.  A subset of our target users will polish the rest of their app over time, including color support and will run into theme compatibility issues. If we can provide both a polished low-effort experience and increase our theme compatibility, that seems like an overall win for our users.

This could be solved by manual help generation (like today or with #2913) or by providing theming support (#1790) but these increase the friction for a polished experience, making it less likely for them to do so.

If we go with changing the current coloring, it doesn't mean we can't use color it has to be a lot more selective and semantically relevant, like with errors.  Other tools at our disposal are dim, bold, underline, and italics.


Discussion / hints:
If you want to guarantee better default colors regardless of a user's terminal theme, you do have the TrueColor escape codes at your disposal, as supported in modern terminals like Kitty. You can also figure out if you're in a light or dark colored terminal with the XTerm escape `\e]11;?\a`, supported in even older terminals.

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 42b5410335e..25891b8d0dc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -82,6 +82,7 @@ MSRV is now 1.60.0
 - *(assert)* Ensure subcommand names are not duplicated
 - *(help)* Use `Command::display_name` in the help title rather than `Command::bin_name`
 - *(help)* Show when a flag is `ArgAction::Count` by adding an `...`
+- *(help)* Use a more neutral palette for coloring
 - *(version)* Use `Command::display_name` rather than `Command::bin_name`
 - *(parser)* Assert on unknown args when using external subcommands (#3703)
 - *(parser)* Always fill in `""` argument for external subcommands (#3263)
diff --git a/src/builder/arg.rs b/src/builder/arg.rs
index f01b7c61e5e..ee97a5779b3 100644
--- a/src/builder/arg.rs
+++ b/src/builder/arg.rs
@@ -3979,11 +3979,11 @@ impl Arg {
         let mut styled = StyledStr::new();
         // Write the name such --long or -l
         if let Some(l) = self.get_long() {
-            styled.none("--");
-            styled.none(l);
+            styled.literal("--");
+            styled.literal(l);
         } else if let Some(s) = self.get_short() {
-            styled.none("-");
-            styled.none(s);
+            styled.literal("-");
+            styled.literal(s);
         }
         styled.extend(self.stylize_arg_suffix().into_iter());
         styled
@@ -3995,29 +3995,28 @@ impl Arg {
         let mut need_closing_bracket = false;
         if self.is_takes_value_set() && !self.is_positional() {
             let is_optional_val = self.get_min_vals() == 0;
-            let sep = if self.is_require_equals_set() {
+            if self.is_require_equals_set() {
                 if is_optional_val {
                     need_closing_bracket = true;
-                    "[="
+                    styled.placeholder("[=");
                 } else {
-                    "="
+                    styled.literal("=");
                 }
             } else if is_optional_val {
                 need_closing_bracket = true;
-                " ["
+                styled.placeholder(" [");
             } else {
-                " "
-            };
-            styled.good(sep);
+                styled.placeholder(" ");
+            }
         }
         if self.is_takes_value_set() || self.is_positional() {
             let arg_val = self.render_arg_val();
-            styled.good(arg_val);
+            styled.placeholder(arg_val);
         } else if matches!(*self.get_action(), ArgAction::Count) {
-            styled.good("...");
+            styled.placeholder("...");
         }
         if need_closing_bracket {
-            styled.none("]");
+            styled.placeholder("]");
         }
 
         styled
diff --git a/src/builder/styled_str.rs b/src/builder/styled_str.rs
index 1cae9022d7b..68057f9f7f2 100644
--- a/src/builder/styled_str.rs
+++ b/src/builder/styled_str.rs
@@ -13,6 +13,18 @@ impl StyledStr {
         Self { pieces: Vec::new() }
     }
 
+    pub(crate) fn header(&mut self, msg: impl Into<String>) {
+        self.stylize_(Some(Style::Header), msg.into());
+    }
+
+    pub(crate) fn literal(&mut self, msg: impl Into<String>) {
+        self.stylize_(Some(Style::Literal), msg.into());
+    }
+
+    pub(crate) fn placeholder(&mut self, msg: impl Into<String>) {
+        self.stylize_(Some(Style::Placeholder), msg.into());
+    }
+
     pub(crate) fn good(&mut self, msg: impl Into<String>) {
         self.stylize_(Some(Style::Good), msg.into());
     }
@@ -137,6 +149,16 @@ impl StyledStr {
         for (style, content) in &self.pieces {
             let mut color = termcolor::ColorSpec::new();
             match style {
+                Some(Style::Header) => {
+                    color.set_bold(true);
+                    color.set_underline(true);
+                }
+                Some(Style::Literal) => {
+                    color.set_bold(true);
+                }
+                Some(Style::Placeholder) => {
+                    color.set_dimmed(true);
+                }
                 Some(Style::Good) => {
                     color.set_fg(Some(termcolor::Color::Green));
                 }
@@ -212,6 +234,9 @@ impl std::fmt::Display for StyledStr {
 
 #[derive(Copy, Clone, Debug, PartialEq, Eq)]
 pub(crate) enum Style {
+    Header,
+    Literal,
+    Placeholder,
     Good,
     Warning,
     Error,
diff --git a/src/output/help.rs b/src/output/help.rs
index 890fd7f878a..786db8db6ac 100644
--- a/src/output/help.rs
+++ b/src/output/help.rs
@@ -155,7 +155,7 @@ impl<'cmd, 'writer> Help<'cmd, 'writer> {
                         self.write_about(true, true);
                     }
                     "usage-heading" => {
-                        self.warning("USAGE:");
+                        self.header("USAGE:");
                     }
                     "usage" => {
                         self.writer
@@ -215,7 +215,7 @@ impl<'cmd, 'writer> Help<'cmd, 'writer> {
                 .replace("{n}", "\n"),
             self.term_w,
         );
-        self.good(&display_name);
+        self.none(&display_name);
     }
 
     /// Writes binary name of a Parser Object to the wrapped stream.
@@ -232,7 +232,7 @@ impl<'cmd, 'writer> Help<'cmd, 'writer> {
         } else {
             wrap(&self.cmd.get_name().replace("{n}", "\n"), self.term_w)
         };
-        self.good(&bin_name);
+        self.none(&bin_name);
     }
 
     fn write_version(&mut self) {
@@ -340,7 +340,7 @@ impl<'cmd, 'writer> Help<'cmd, 'writer> {
 
         let mut first = if !pos.is_empty() {
             // Write positional args if any
-            self.warning("ARGS:\n");
+            self.header("ARGS:\n");
             self.write_args(&pos, "ARGS", positional_sort_key);
             false
         } else {
@@ -351,7 +351,7 @@ impl<'cmd, 'writer> Help<'cmd, 'writer> {
             if !first {
                 self.none("\n\n");
             }
-            self.warning("OPTIONS:\n");
+            self.header("OPTIONS:\n");
             self.write_args(&non_pos, "OPTIONS", option_sort_key);
             first = false;
         }
@@ -373,7 +373,7 @@ impl<'cmd, 'writer> Help<'cmd, 'writer> {
                     if !first {
                         self.none("\n\n");
                     }
-                    self.warning(format!("{}:\n", heading));
+                    self.header(format!("{}:\n", heading));
                     self.write_args(&args, heading, option_sort_key);
                     first = false
                 }
@@ -386,12 +386,12 @@ impl<'cmd, 'writer> Help<'cmd, 'writer> {
             }
 
             let default_help_heading = Str::from("SUBCOMMANDS");
-            self.warning(
+            self.header(
                 self.cmd
                     .get_subcommand_help_heading()
                     .unwrap_or(&default_help_heading),
             );
-            self.warning(":\n");
+            self.header(":\n");
 
             self.write_subcommands(self.cmd);
         }
@@ -465,7 +465,7 @@ impl<'cmd, 'writer> Help<'cmd, 'writer> {
         debug!("Help::short");
 
         if let Some(s) = arg.get_short() {
-            self.good(format!("-{}", s));
+            self.literal(format!("-{}", s));
         } else if arg.get_long().is_some() {
             self.none(TAB)
         }
@@ -478,7 +478,7 @@ impl<'cmd, 'writer> Help<'cmd, 'writer> {
             if arg.short.is_some() {
                 self.none(", ");
             }
-            self.good(format!("--{}", long));
+            self.literal(format!("--{}", long));
         }
     }
 
@@ -622,7 +622,7 @@ impl<'cmd, 'writer> Help<'cmd, 'writer> {
                     self.none("\n");
                     self.spaces(spaces);
                     self.none("- ");
-                    self.good(pv.get_name());
+                    self.literal(pv.get_name());
                     if let Some(help) = pv.get_help() {
                         debug!("Help::help: Possible Value help");
 
@@ -775,12 +775,12 @@ impl<'cmd, 'writer> Help<'cmd, 'writer> {
         self.writer.extend(msg.iter());
     }
 
-    fn good<T: Into<String>>(&mut self, msg: T) {
-        self.writer.good(msg);
+    fn header<T: Into<String>>(&mut self, msg: T) {
+        self.writer.header(msg);
     }
 
-    fn warning<T: Into<String>>(&mut self, msg: T) {
-        self.writer.warning(msg);
+    fn literal<T: Into<String>>(&mut self, msg: T) {
+        self.writer.literal(msg);
     }
 
     fn none<T: Into<String>>(&mut self, msg: T) {
@@ -915,7 +915,7 @@ impl<'cmd, 'writer> Help<'cmd, 'writer> {
     /// Writes subcommand to the wrapped stream.
     fn subcmd(&mut self, sc_str: &str, next_line_help: bool, longest: usize) {
         self.none(TAB);
-        self.good(sc_str);
+        self.literal(sc_str);
         if !next_line_help {
             let width = display_width(sc_str);
             self.spaces(width.max(longest + 4) - width);
diff --git a/src/output/usage.rs b/src/output/usage.rs
index 9545af5dc69..eb37bce41c3 100644
--- a/src/output/usage.rs
+++ b/src/output/usage.rs
@@ -32,7 +32,7 @@ impl<'cmd> Usage<'cmd> {
     pub(crate) fn create_usage_with_title(&self, used: &[Id]) -> StyledStr {
         debug!("Usage::create_usage_with_title");
         let mut styled = StyledStr::new();
-        styled.none("USAGE:\n    ");
+        styled.header("USAGE:\n    ");
         styled.extend(self.create_usage_no_title(used).into_iter());
         styled
     }
@@ -58,10 +58,10 @@ impl<'cmd> Usage<'cmd> {
             .get_usage_name()
             .or_else(|| self.cmd.get_bin_name())
             .unwrap_or_else(|| self.cmd.get_name());
-        styled.none(name);
+        styled.literal(name);
 
         if self.needs_options_tag() {
-            styled.none(" [OPTIONS]");
+            styled.placeholder(" [OPTIONS]");
         }
 
         let allow_missing_positional = self.cmd.is_allow_missing_positional_set();
@@ -80,15 +80,15 @@ impl<'cmd> Usage<'cmd> {
             && !(self.cmd.has_visible_subcommands() || self.cmd.is_allow_external_subcommands_set())
             && !has_last
         {
-            styled.none(" [--]");
+            styled.placeholder(" [--]");
         }
         let not_req_or_hidden =
             |p: &Arg| (!p.is_required_set() || p.is_last_set()) && !p.is_hide_set();
         if self.cmd.get_positionals().any(not_req_or_hidden) {
             if let Some(args_tag) = self.get_args_tag(incl_reqs) {
-                styled.none(&*args_tag);
+                styled.placeholder(&*args_tag);
             } else {
-                styled.none(" [ARGS]");
+                styled.placeholder(" [ARGS]");
             }
             if has_last && incl_reqs {
                 let pos = self
@@ -102,17 +102,18 @@ impl<'cmd> Usage<'cmd> {
                 );
                 let req = pos.is_required_set();
                 if req && self.cmd.get_positionals().any(|p| !p.is_required_set()) {
-                    styled.none(" -- <");
+                    styled.literal(" -- ");
+                    styled.placeholder("<");
                 } else if req {
-                    styled.none(" [--] <");
+                    styled.placeholder(" [--] <");
                 } else {
-                    styled.none(" [-- <");
+                    styled.placeholder(" [-- <");
                 }
-                styled.none(&*pos.name_no_brackets());
-                styled.none('>');
-                styled.none(pos.multiple_str());
+                styled.placeholder(&*pos.name_no_brackets());
+                styled.placeholder('>');
+                styled.placeholder(pos.multiple_str());
                 if !req {
-                    styled.none(']');
+                    styled.placeholder(']');
                 }
             }
         }
@@ -136,19 +137,19 @@ impl<'cmd> Usage<'cmd> {
                 if !self.cmd.is_args_conflicts_with_subcommands_set() {
                     styled.extend(self.create_help_usage(false).into_iter());
                 } else {
-                    styled.none(name);
+                    styled.literal(name);
                 }
-                styled.none(" <");
-                styled.none(placeholder);
-                styled.none(">");
+                styled.placeholder(" <");
+                styled.placeholder(placeholder);
+                styled.placeholder(">");
             } else if self.cmd.is_subcommand_required_set() {
-                styled.none(" <");
-                styled.none(placeholder);
-                styled.none(">");
+                styled.placeholder(" <");
+                styled.placeholder(placeholder);
+                styled.placeholder(">");
             } else {
-                styled.none(" [");
-                styled.none(placeholder);
-                styled.none("]");
+                styled.placeholder(" [");
+                styled.placeholder(placeholder);
+                styled.placeholder("]");
             }
         }
         styled.trim();
@@ -162,7 +163,7 @@ impl<'cmd> Usage<'cmd> {
         debug!("Usage::create_smart_usage");
         let mut styled = StyledStr::new();
 
-        styled.none(
+        styled.literal(
             self.cmd
                 .get_usage_name()
                 .or_else(|| self.cmd.get_bin_name())
@@ -172,13 +173,13 @@ impl<'cmd> Usage<'cmd> {
         self.write_required_usage_from(used, None, true, &mut styled);
 
         if self.cmd.is_subcommand_required_set() {
-            styled.none(" <");
-            styled.none(
+            styled.placeholder(" <");
+            styled.placeholder(
                 self.cmd
                     .get_subcommand_value_name()
                     .unwrap_or(DEFAULT_SUB_VALUE_NAME),
             );
-            styled.none(">");
+            styled.placeholder(">");
         }
         styled
     }


Test patch: