From cc20a1d2f7351c8da303cfcfdb2f235274524802 Mon Sep 17 00:00:00 2001 From: Jade Lovelace Date: Sun, 5 Jan 2025 19:46:10 +0100 Subject: [PATCH] implement CLICOLOR_FORCE environment variable This allows to force colors but not disable other features such as the progress bar. This is for instance useful in CI environments. Adapted from https://git.lix.systems/lix-project/lix/commit/378ec5fb0611e314511a7afc806664205846fc2e and others. --- src/libcmd/markdown.cc | 2 +- src/libmain/progress-bar.cc | 2 +- src/libmain/shared.cc | 4 +++- src/libutil/logging.cc | 2 +- src/libutil/terminal.cc | 26 ++++++++++++++++++++------ src/libutil/terminal.hh | 34 +++++++++++++++++++++++++++++++++- src/nix-env/nix-env.cc | 2 +- src/nix/main.cc | 2 +- src/nix/prefetch.cc | 2 +- 9 files changed, 62 insertions(+), 14 deletions(-) diff --git a/src/libcmd/markdown.cc b/src/libcmd/markdown.cc index 4566e6ba63c..7492cb3793e 100644 --- a/src/libcmd/markdown.cc +++ b/src/libcmd/markdown.cc @@ -64,7 +64,7 @@ static std::string doRenderMarkdownToTerminal(std::string_view markdown) if (!rndr_res) throw Error("allocation error while rendering Markdown"); - return filterANSIEscapes(std::string(buf->data, buf->size), !isTTY()); + return filterANSIEscapes(std::string(buf->data, buf->size), !shouldANSI()); } std::string renderMarkdownToTerminal(std::string_view markdown) diff --git a/src/libmain/progress-bar.cc b/src/libmain/progress-bar.cc index fa0b73ebef3..94bd1c94a9d 100644 --- a/src/libmain/progress-bar.cc +++ b/src/libmain/progress-bar.cc @@ -557,7 +557,7 @@ class ProgressBar : public Logger Logger * makeProgressBar() { - return new ProgressBar(isTTY()); + return new ProgressBar(shouldANSI()); } void startProgressBar() diff --git a/src/libmain/shared.cc b/src/libmain/shared.cc index 50f90bfb314..60ac8e496b8 100644 --- a/src/libmain/shared.cc +++ b/src/libmain/shared.cc @@ -6,6 +6,8 @@ #include "loggers.hh" #include "progress-bar.hh" #include "signals.hh" +#include "terminal.hh" + #include #include @@ -370,7 +372,7 @@ int handleExceptions(const std::string & programName, std::function fun) RunPager::RunPager() { - if (!isatty(STDOUT_FILENO)) return; + if (!isOutputARealTerminal(StandardOutputStream::Stdout)) return; char * pager = getenv("NIX_PAGER"); if (!pager) pager = getenv("PAGER"); if (pager && ((std::string) pager == "" || (std::string) pager == "cat")) return; diff --git a/src/libutil/logging.cc b/src/libutil/logging.cc index a5add5565df..aa86cc0f37d 100644 --- a/src/libutil/logging.cc +++ b/src/libutil/logging.cc @@ -54,7 +54,7 @@ class SimpleLogger : public Logger : printBuildLogs(printBuildLogs) { systemd = getEnv("IN_SYSTEMD") == "1"; - tty = isTTY(); + tty = shouldANSI(); } bool isVerbose() override { diff --git a/src/libutil/terminal.cc b/src/libutil/terminal.cc index 8a8373f1bf9..8e53e4e9ab3 100644 --- a/src/libutil/terminal.cc +++ b/src/libutil/terminal.cc @@ -61,14 +61,28 @@ inline std::pair charWidthUTF8Helper(std::string_view s) namespace nix { -bool isTTY() +bool isOutputARealTerminal(StandardOutputStream fileno) { - static const bool tty = - isatty(STDERR_FILENO) - && getEnv("TERM").value_or("dumb") != "dumb" - && !(getEnv("NO_COLOR").has_value() || getEnv("NOCOLOR").has_value()); + return isatty(int(fileno)) && getEnv("TERM").value_or("dumb") != "dumb"; +} - return tty; +bool shouldANSI(StandardOutputStream fileno) +{ + // Implements the behaviour described by https://bixense.com/clicolors/ + // As well as https://force-color.org/ for compatibility, since it fits in the same shape. + // NO_COLOR CLICOLOR CLICOLOR_FORCE Colours? + // set x x No + // unset x set Yes + // unset x unset If attached to a terminal + // [we choose the "modern" approach of colour-by-default] + auto compute = [](StandardOutputStream fileno) -> bool { + bool mustNotColour = getEnv("NO_COLOR").has_value() || getEnv("NOCOLOR").has_value(); + bool shouldForce = getEnv("CLICOLOR_FORCE").has_value() || getEnv("FORCE_COLOR").has_value(); + bool isTerminal = isOutputARealTerminal(fileno); + return !mustNotColour && (shouldForce || isTerminal); + }; + static bool cached[2] = {compute(StandardOutputStream::Stdout), compute(StandardOutputStream::Stderr)}; + return cached[int(fileno) - 1]; } std::string filterANSIEscapes(std::string_view s, bool filterAll, unsigned int width) diff --git a/src/libutil/terminal.hh b/src/libutil/terminal.hh index 7ff05a487c3..29a0a83dc20 100644 --- a/src/libutil/terminal.hh +++ b/src/libutil/terminal.hh @@ -5,11 +5,43 @@ #include namespace nix { + +enum class StandardOutputStream { + Stdout = 1, + Stderr = 2, +}; + +/** + * Determine whether the output is a real terminal (i.e. not dumb, not a pipe). + * + * This is probably not what you want, you may want shouldANSI() or something + * more specific. Think about how the output should work with a pager or + * entirely non-interactive scripting use. + * + * The user may be redirecting the Lix output to a pager, but have stderr + * connected to a terminal. Think about where you are outputting the text when + * deciding whether to use STDERR_FILENO or STDOUT_FILENO. + * + * \param fileno file descriptor number to check if it is a tty + */ +bool isOutputARealTerminal(StandardOutputStream fileno); + /** * Determine whether ANSI escape sequences are appropriate for the * present output. + * + * This follows the rules described on https://bixense.com/clicolors/ + * with CLICOLOR defaulted to enabled (and thus ignored). + * + * That is to say, the following procedure is followed in order: + * - NO_COLOR or NOCOLOR set -> always disable colour + * - CLICOLOR_FORCE or FORCE_COLOR set -> enable colour + * - The output is a tty; TERM != "dumb" -> enable colour + * - Otherwise -> disable colour + * + * \param fileno which file descriptor number to consider. Use the one you are outputting to */ -bool isTTY(); +bool shouldANSI(StandardOutputStream fileno = StandardOutputStream::Stderr); /** * Truncate a string to 'width' printable characters. If 'filterAll' diff --git a/src/nix-env/nix-env.cc b/src/nix-env/nix-env.cc index e9eb5270895..2b9181e5edd 100644 --- a/src/nix-env/nix-env.cc +++ b/src/nix-env/nix-env.cc @@ -1091,7 +1091,7 @@ static void opQuery(Globals & globals, Strings opFlags, Strings opArgs) return; } - bool tty = isTTY(); + bool tty = shouldANSI(); RunPager pager; Table table; diff --git a/src/nix/main.cc b/src/nix/main.cc index b0e26e093f1..4d8b454fbe5 100644 --- a/src/nix/main.cc +++ b/src/nix/main.cc @@ -412,7 +412,7 @@ void mainWrapped(int argc, char * * argv) settings.verboseBuild = false; // If on a terminal, progress will be displayed via progress bars etc. (thus verbosity=notice) - if (nix::isTTY()) { + if (nix::isOutputARealTerminal(StandardOutputStream::Stderr)) { verbosity = lvlNotice; } else { verbosity = lvlInfo; diff --git a/src/nix/prefetch.cc b/src/nix/prefetch.cc index db7d9e4efe6..547cc60571d 100644 --- a/src/nix/prefetch.cc +++ b/src/nix/prefetch.cc @@ -191,7 +191,7 @@ static int main_nix_prefetch_url(int argc, char * * argv) Finally f([]() { stopProgressBar(); }); - if (isTTY()) + if (isOutputARealTerminal(StandardOutputStream::Stderr)) startProgressBar(); auto store = openStore();