Repository: casey/just
Pull request #1670 (2023-09-09 16:59:48)

Optional command color
Draft MR which attempts to resolve https://github.com/casey/just/issues/1186, ie allowing for colored command output. I can add to the list of available colors, but was hoping to get a quick review first to make sure my approach is sensible.

avi-cenna: @frederikhors   Do you mind doing a quick review of this draft MR to enable colored output for recipe lines? You were the one who originally opened the issue, and I figure you might be a little more experienced with Rust than I am.
frederikhors: I'm sorry. I'm not using it anymore.
avi-cenna: @frederikhors , no problem at all, and thank you for your reply. If I may ask, what do you use now instead of just?
poliorcetics: I was going to create an issue for this but there already is an MR, thanks @avi-cenna !

A question: why the limitation to cyan and purple instead of all the "base" colours usually found in terminal emulators ?
avi-cenna: @poliorcetics I wanted to start off with just a couple colors until someone could review and confirm that my approach was reasonable. Thank you for the review. Are you a maintainer on this project?
avi-cenna: @casey thanks for the review. I have applied your suggestions as much as possible, and added the additional colors. I've also added one integration test for the `command-color` argument.

Patch:
diff --git a/.gitignore b/.gitignore
index 17ed3095..e6c81f9f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
 .DS_Store
+.idea
 /.vagrant
 /README.html
 /book/en/build
diff --git a/completions/just.bash b/completions/just.bash
index 9c7945b2..99d7fdb7 100644
--- a/completions/just.bash
+++ b/completions/just.bash
@@ -20,7 +20,7 @@ _just() {
 
     case "${cmd}" in
         just)
-            opts=" -n -q -u -v -e -l -h -V -f -d -c -s  --check --dry-run --highlight --no-dotenv --no-highlight --quiet --shell-command --clear-shell-args --unsorted --unstable --verbose --changelog --choose --dump --edit --evaluate --fmt --init --list --summary --variables --help --version --chooser --color --dump-format --list-heading --list-prefix --justfile --set --shell --shell-arg --working-directory --command --completions --show --dotenv-filename --dotenv-path  <ARGUMENTS>... "
+            opts=" -n -q -u -v -e -l -h -V -f -d -c -s  --check --dry-run --highlight --no-dotenv --no-highlight --quiet --shell-command --clear-shell-args --unsorted --unstable --verbose --changelog --choose --dump --edit --evaluate --fmt --init --list --summary --variables --help --version --chooser --color --command-color --dump-format --list-heading --list-prefix --justfile --set --shell --shell-arg --working-directory --command --completions --show --dotenv-filename --dotenv-path  <ARGUMENTS>... "
                 if [[ ${cur} == -* ]] ; then
                     COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
                     return 0
@@ -48,6 +48,10 @@ _just() {
                     COMPREPLY=($(compgen -W "auto always never" -- "${cur}"))
                     return 0
                     ;;
+                --command-color)
+                    COMPREPLY=($(compgen -W "black blue cyan green purple red yellow" -- "${cur}"))
+                    return 0
+                    ;;
                 --dump-format)
                     COMPREPLY=($(compgen -W "just json" -- "${cur}"))
                     return 0
diff --git a/completions/just.elvish b/completions/just.elvish
index 1a208afc..1b3be811 100644
--- a/completions/just.elvish
+++ b/completions/just.elvish
@@ -16,6 +16,7 @@ edit:completion:arg-completer[just] = [@words]{
         &'just'= {
             cand --chooser 'Override binary invoked by `--choose`'
             cand --color 'Print colorful output'
+            cand --command-color 'Echo recipe lines in <COMMAND-COLOR>'
             cand --dump-format 'Dump justfile as <FORMAT>'
             cand --list-heading 'Print <TEXT> before list'
             cand --list-prefix 'Print <TEXT> before each list item'
diff --git a/completions/just.fish b/completions/just.fish
index 43d41774..0771de01 100644
--- a/completions/just.fish
+++ b/completions/just.fish
@@ -11,6 +11,7 @@ complete -c just -a '(__fish_just_complete_recipes)'
 # autogenerated completions
 complete -c just -n "__fish_use_subcommand" -l chooser -d 'Override binary invoked by `--choose`'
 complete -c just -n "__fish_use_subcommand" -l color -d 'Print colorful output' -r -f -a "auto always never"
+complete -c just -n "__fish_use_subcommand" -l command-color -d 'Echo recipe lines in <COMMAND-COLOR>' -r -f -a "black blue cyan green purple red yellow"
 complete -c just -n "__fish_use_subcommand" -l dump-format -d 'Dump justfile as <FORMAT>' -r -f -a "just json"
 complete -c just -n "__fish_use_subcommand" -l list-heading -d 'Print <TEXT> before list'
 complete -c just -n "__fish_use_subcommand" -l list-prefix -d 'Print <TEXT> before each list item'
diff --git a/completions/just.powershell b/completions/just.powershell
index 1ed04d30..0842dc8c 100644
--- a/completions/just.powershell
+++ b/completions/just.powershell
@@ -21,6 +21,7 @@ Register-ArgumentCompleter -Native -CommandName 'just' -ScriptBlock {
         'just' {
             [CompletionResult]::new('--chooser', 'chooser', [CompletionResultType]::ParameterName, 'Override binary invoked by `--choose`')
             [CompletionResult]::new('--color', 'color', [CompletionResultType]::ParameterName, 'Print colorful output')
+            [CompletionResult]::new('--command-color', 'command-color', [CompletionResultType]::ParameterName, 'Echo recipe lines in <COMMAND-COLOR>')
             [CompletionResult]::new('--dump-format', 'dump-format', [CompletionResultType]::ParameterName, 'Dump justfile as <FORMAT>')
             [CompletionResult]::new('--list-heading', 'list-heading', [CompletionResultType]::ParameterName, 'Print <TEXT> before list')
             [CompletionResult]::new('--list-prefix', 'list-prefix', [CompletionResultType]::ParameterName, 'Print <TEXT> before each list item')
diff --git a/completions/just.zsh b/completions/just.zsh
index a3aff350..4ddfa867 100644
--- a/completions/just.zsh
+++ b/completions/just.zsh
@@ -17,6 +17,7 @@ _just() {
     local common=(
 '--chooser=[Override binary invoked by `--choose`]' \
 '--color=[Print colorful output]: :(auto always never)' \
+'--command-color=[Echo recipe lines in <COMMAND-COLOR>]: :(black blue cyan green purple red yellow)' \
 '--dump-format=[Dump justfile as <FORMAT>]: :(just json)' \
 '--list-heading=[Print <TEXT> before list]' \
 '--list-prefix=[Print <TEXT> before each list item]' \
diff --git a/src/color.rs b/src/color.rs
index 2fcad9a8..56670acc 100644
--- a/src/color.rs
+++ b/src/color.rs
@@ -76,8 +76,12 @@ impl Color {
     self.restyle(Style::new().fg(Cyan).bold())
   }
 
-  pub(crate) fn command(self) -> Self {
-    self.restyle(Style::new().bold())
+  pub(crate) fn command(self, foreground: Option<ansi_term::Color>) -> Self {
+    self.restyle(Style {
+      foreground,
+      is_bold: true,
+      ..Style::default()
+    })
   }
 
   pub(crate) fn parameter(self) -> Self {
diff --git a/src/config.rs b/src/config.rs
index 14904c5b..1e6e86d1 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -15,6 +15,7 @@ pub(crate) const CHOOSE_HELP: &str = "Select one or more recipes to run using a
 pub(crate) struct Config {
   pub(crate) check: bool,
   pub(crate) color: Color,
+  pub(crate) command_color: Option<ansi_term::Color>,
   pub(crate) dotenv_filename: Option<String>,
   pub(crate) dotenv_path: Option<PathBuf>,
   pub(crate) dry_run: bool,
@@ -85,6 +86,7 @@ mod arg {
   pub(crate) const CHOOSER: &str = "CHOOSER";
   pub(crate) const CLEAR_SHELL_ARGS: &str = "CLEAR-SHELL-ARGS";
   pub(crate) const COLOR: &str = "COLOR";
+  pub(crate) const COMMAND_COLOR: &str = "COMMAND-COLOR";
   pub(crate) const DOTENV_FILENAME: &str = "DOTENV-FILENAME";
   pub(crate) const DOTENV_PATH: &str = "DOTENV-PATH";
   pub(crate) const DRY_RUN: &str = "DRY-RUN";
@@ -110,6 +112,23 @@ mod arg {
   pub(crate) const COLOR_NEVER: &str = "never";
   pub(crate) const COLOR_VALUES: &[&str] = &[COLOR_AUTO, COLOR_ALWAYS, COLOR_NEVER];
 
+  pub(crate) const COMMAND_COLOR_BLACK: &str = "black";
+  pub(crate) const COMMAND_COLOR_BLUE: &str = "blue";
+  pub(crate) const COMMAND_COLOR_CYAN: &str = "cyan";
+  pub(crate) const COMMAND_COLOR_GREEN: &str = "green";
+  pub(crate) const COMMAND_COLOR_PURPLE: &str = "purple";
+  pub(crate) const COMMAND_COLOR_RED: &str = "red";
+  pub(crate) const COMMAND_COLOR_YELLOW: &str = "yellow";
+  pub(crate) const COMMAND_COLOR_VALUES: &[&str] = &[
+    COMMAND_COLOR_BLACK,
+    COMMAND_COLOR_BLUE,
+    COMMAND_COLOR_CYAN,
+    COMMAND_COLOR_GREEN,
+    COMMAND_COLOR_PURPLE,
+    COMMAND_COLOR_RED,
+    COMMAND_COLOR_YELLOW,
+  ];
+
   pub(crate) const DUMP_FORMAT_JSON: &str = "json";
   pub(crate) const DUMP_FORMAT_JUST: &str = "just";
   pub(crate) const DUMP_FORMAT_VALUES: &[&str] = &[DUMP_FORMAT_JUST, DUMP_FORMAT_JSON];
@@ -142,6 +161,13 @@ impl Config {
           .default_value(arg::COLOR_AUTO)
           .help("Print colorful output"),
       )
+      .arg(
+        Arg::with_name(arg::COMMAND_COLOR)
+          .long("command-color")
+          .takes_value(true)
+          .possible_values(arg::COMMAND_COLOR_VALUES)
+          .help("Echo recipe lines in <COMMAND-COLOR>"),
+      )
       .arg(
         Arg::with_name(arg::DRY_RUN)
           .short("n")
@@ -396,6 +422,25 @@ impl Config {
     }
   }
 
+  fn command_color_from_matches(matches: &ArgMatches) -> ConfigResult<Option<ansi_term::Color>> {
+    if let Some(value) = matches.value_of(arg::COMMAND_COLOR) {
+      match value {
+        arg::COMMAND_COLOR_BLACK => Ok(Some(ansi_term::Color::Black)),
+        arg::COMMAND_COLOR_BLUE => Ok(Some(ansi_term::Color::Blue)),
+        arg::COMMAND_COLOR_CYAN => Ok(Some(ansi_term::Color::Cyan)),
+        arg::COMMAND_COLOR_GREEN => Ok(Some(ansi_term::Color::Green)),
+        arg::COMMAND_COLOR_PURPLE => Ok(Some(ansi_term::Color::Purple)),
+        arg::COMMAND_COLOR_RED => Ok(Some(ansi_term::Color::Red)),
+        arg::COMMAND_COLOR_YELLOW => Ok(Some(ansi_term::Color::Yellow)),
+        value => Err(ConfigError::Internal {
+          message: format!("Invalid argument `{value}` to --command-color."),
+        }),
+      }
+    } else {
+      Ok(None)
+    }
+  }
+
   fn dump_format_from_matches(matches: &ArgMatches) -> ConfigResult<DumpFormat> {
     let value = matches
       .value_of(arg::DUMP_FORMAT)
@@ -422,6 +467,7 @@ impl Config {
     };
 
     let color = Self::color_from_matches(matches)?;
+    let command_color = Self::command_color_from_matches(matches)?;
 
     let set_count = matches.occurrences_of(arg::SET);
     let mut overrides = BTreeMap::new();
@@ -593,6 +639,7 @@ impl Config {
         .unwrap_or("    ")
         .to_owned(),
       color,
+      command_color,
       invocation_directory,
       search_config,
       shell_args,
diff --git a/src/recipe.rs b/src/recipe.rs
index aa1c487a..c1fa22f3 100644
--- a/src/recipe.rs
+++ b/src/recipe.rs
@@ -182,7 +182,7 @@ impl<'src, D> Recipe<'src, D> {
         || !((quiet_command ^ self.quiet) || config.verbosity.quiet())
       {
         let color = if config.highlight {
-          config.color.command()
+          config.color.command(config.command_color)
         } else {
           config.color
         };


Test patch:
diff --git a/tests/command.rs b/tests/command.rs
index 0529958a..c1c9473e 100644
--- a/tests/command.rs
+++ b/tests/command.rs
@@ -20,6 +20,18 @@ test! {
   stdout: "foo",
 }
 
+test! {
+  name: command_color,
+  justfile: "
+    x:
+      echo XYZ
+  ",
+  args: ("--color", "always", "--command-color", "cyan"),
+  stdout: "XYZ\n",
+  stderr: "\u{1b}[1;36mecho XYZ\u{1b}[0m\n",
+  status: EXIT_SUCCESS,
+}
+
 test! {
   name: no_binary,
   justfile: "