diff --git a/core/config/engine.cpp b/core/config/engine.cpp index fa1be2d30d55..2a3791b76637 100644 --- a/core/config/engine.cpp +++ b/core/config/engine.cpp @@ -383,6 +383,14 @@ String Engine::get_shader_cache_path() const { return shader_cache_path; } +void Engine::set_color_standard_output(bool p_enable) { + color_standard_output = p_enable; +} + +bool Engine::is_coloring_standard_output() const { + return color_standard_output; +} + Engine *Engine::get_singleton() { return singleton; } diff --git a/core/config/engine.h b/core/config/engine.h index ab03111f2426..df0ef23f87be 100644 --- a/core/config/engine.h +++ b/core/config/engine.h @@ -101,6 +101,8 @@ class Engine { bool freeze_time_scale = false; + bool color_standard_output = true; + public: static Engine *get_singleton(); @@ -194,6 +196,9 @@ class Engine { void set_shader_cache_path(const String &p_path); String get_shader_cache_path() const; + void set_color_standard_output(bool p_enable); + bool is_coloring_standard_output() const; + bool is_abort_on_gpu_errors_enabled() const; bool is_validation_layers_enabled() const; bool is_generate_spirv_debug_info_enabled() const; diff --git a/core/string/print_string.cpp b/core/string/print_string.cpp index 177ddc3c77a8..c85857c85015 100644 --- a/core/string/print_string.cpp +++ b/core/string/print_string.cpp @@ -131,145 +131,147 @@ void __print_line_rich(const String &p_string) { pos = brk_end + 1; output += txt; + const bool should_color = Engine::get_singleton()->is_coloring_standard_output(); + String tag = p_string.substr(brk_pos + 1, brk_end - brk_pos - 1); if (tag == "b") { - output += "\u001b[1m"; + output += should_color ? "\u001b[1m" : ""; } else if (tag == "/b") { - output += "\u001b[22m"; + output += should_color ? "\u001b[22m" : ""; } else if (tag == "i") { - output += "\u001b[3m"; + output += should_color ? "\u001b[3m" : ""; } else if (tag == "/i") { - output += "\u001b[23m"; + output += should_color ? "\u001b[23m" : ""; } else if (tag == "u") { - output += "\u001b[4m"; + output += should_color ? "\u001b[4m" : ""; } else if (tag == "/u") { - output += "\u001b[24m"; + output += should_color ? "\u001b[24m" : ""; } else if (tag == "s") { - output += "\u001b[9m"; + output += should_color ? "\u001b[9m" : ""; } else if (tag == "/s") { - output += "\u001b[29m"; + output += should_color ? "\u001b[29m" : ""; } else if (tag == "indent") { - output += " "; + output += should_color ? " " : ""; } else if (tag == "/indent") { output += ""; } else if (tag == "code") { - output += "\u001b[2m"; + output += should_color ? "\u001b[2m" : ""; } else if (tag == "/code") { - output += "\u001b[22m"; + output += should_color ? "\u001b[22m" : ""; } else if (tag == "url") { output += ""; } else if (tag == "/url") { output += ""; } else if (tag == "center") { - output += "\n\t\t\t"; + output += should_color ? "\n\t\t\t" : ""; } else if (tag == "/center") { output += ""; } else if (tag == "right") { - output += "\n\t\t\t\t\t\t"; + output += should_color ? "\n\t\t\t\t\t\t" : ""; } else if (tag == "/right") { output += ""; } else if (tag.begins_with("color=")) { String color_name = tag.trim_prefix("color="); if (color_name == "black") { - output += "\u001b[30m"; + output += should_color ? "\u001b[30m" : ""; } else if (color_name == "red") { - output += "\u001b[91m"; + output += should_color ? "\u001b[91m" : ""; } else if (color_name == "green") { - output += "\u001b[92m"; + output += should_color ? "\u001b[92m" : ""; } else if (color_name == "lime") { - output += "\u001b[92m"; + output += should_color ? "\u001b[92m" : ""; } else if (color_name == "yellow") { - output += "\u001b[93m"; + output += should_color ? "\u001b[93m" : ""; } else if (color_name == "blue") { - output += "\u001b[94m"; + output += should_color ? "\u001b[94m" : ""; } else if (color_name == "magenta") { - output += "\u001b[95m"; + output += should_color ? "\u001b[95m" : ""; } else if (color_name == "pink") { - output += "\u001b[38;5;218m"; + output += should_color ? "\u001b[38;5;218m" : ""; } else if (color_name == "purple") { - output += "\u001b[38;5;98m"; + output += should_color ? "\u001b[38;5;98m" : ""; } else if (color_name == "cyan") { - output += "\u001b[96m"; + output += should_color ? "\u001b[96m" : ""; } else if (color_name == "white") { - output += "\u001b[97m"; + output += should_color ? "\u001b[97m" : ""; } else if (color_name == "orange") { - output += "\u001b[38;5;208m"; + output += should_color ? "\u001b[38;5;208m" : ""; } else if (color_name == "gray") { - output += "\u001b[90m"; + output += should_color ? "\u001b[90m" : ""; } else { Color c = Color::from_string(color_name, Color()); - output += vformat("\u001b[38;2;%d;%d;%dm", c.r * 255, c.g * 255, c.b * 255); + output += should_color ? vformat("\u001b[38;2;%d;%d;%dm", c.r * 255, c.g * 255, c.b * 255) : ""; } } else if (tag == "/color") { - output += "\u001b[39m"; + output += should_color ? "\u001b[39m" : ""; } else if (tag.begins_with("bgcolor=")) { String color_name = tag.trim_prefix("bgcolor="); if (color_name == "black") { - output += "\u001b[40m"; + output += should_color ? "\u001b[40m" : ""; } else if (color_name == "red") { - output += "\u001b[101m"; + output += should_color ? "\u001b[101m" : ""; } else if (color_name == "green") { - output += "\u001b[102m"; + output += should_color ? "\u001b[102m" : ""; } else if (color_name == "lime") { - output += "\u001b[102m"; + output += should_color ? "\u001b[102m" : ""; } else if (color_name == "yellow") { - output += "\u001b[103m"; + output += should_color ? "\u001b[103m" : ""; } else if (color_name == "blue") { - output += "\u001b[104m"; + output += should_color ? "\u001b[104m" : ""; } else if (color_name == "magenta") { - output += "\u001b[105m"; + output += should_color ? "\u001b[105m" : ""; } else if (color_name == "pink") { - output += "\u001b[48;5;218m"; + output += should_color ? "\u001b[48;5;218m" : ""; } else if (color_name == "purple") { - output += "\u001b[48;5;98m"; + output += should_color ? "\u001b[48;5;98m" : ""; } else if (color_name == "cyan") { - output += "\u001b[106m"; + output += should_color ? "\u001b[106m" : ""; } else if (color_name == "white") { - output += "\u001b[107m"; + output += should_color ? "\u001b[107m" : ""; } else if (color_name == "orange") { - output += "\u001b[48;5;208m"; + output += should_color ? "\u001b[48;5;208m" : ""; } else if (color_name == "gray") { - output += "\u001b[100m"; + output += should_color ? "\u001b[100m" : ""; } else { Color c = Color::from_string(color_name, Color()); - output += vformat("\u001b[48;2;%d;%d;%dm", c.r * 255, c.g * 255, c.b * 255); + output += should_color ? vformat("\u001b[48;2;%d;%d;%dm", c.r * 255, c.g * 255, c.b * 255) : ""; } } else if (tag == "/bgcolor") { - output += "\u001b[49m"; + output += should_color ? "\u001b[49m" : ""; } else if (tag.begins_with("fgcolor=")) { String color_name = tag.trim_prefix("fgcolor="); if (color_name == "black") { - output += "\u001b[30;40m"; + output += should_color ? "\u001b[30;40m" : ""; } else if (color_name == "red") { - output += "\u001b[91;101m"; + output += should_color ? "\u001b[91;101m" : ""; } else if (color_name == "green") { - output += "\u001b[92;102m"; + output += should_color ? "\u001b[92;102m" : ""; } else if (color_name == "lime") { - output += "\u001b[92;102m"; + output += should_color ? "\u001b[92;102m" : ""; } else if (color_name == "yellow") { - output += "\u001b[93;103m"; + output += should_color ? "\u001b[93;103m" : ""; } else if (color_name == "blue") { - output += "\u001b[94;104m"; + output += should_color ? "\u001b[94;104m" : ""; } else if (color_name == "magenta") { - output += "\u001b[95;105m"; + output += should_color ? "\u001b[95;105m" : ""; } else if (color_name == "pink") { - output += "\u001b[38;5;218;48;5;218m"; + output += should_color ? "\u001b[38;5;218;48;5;218m" : ""; } else if (color_name == "purple") { - output += "\u001b[38;5;98;48;5;98m"; + output += should_color ? "\u001b[38;5;98;48;5;98m" : ""; } else if (color_name == "cyan") { - output += "\u001b[96;106m"; + output += should_color ? "\u001b[96;106m" : ""; } else if (color_name == "white") { - output += "\u001b[97;107m"; + output += should_color ? "\u001b[97;107m" : ""; } else if (color_name == "orange") { - output += "\u001b[38;5;208;48;5;208m"; + output += should_color ? "\u001b[38;5;208;48;5;208m" : ""; } else if (color_name == "gray") { - output += "\u001b[90;100m"; + output += should_color ? "\u001b[90;100m" : ""; } else { Color c = Color::from_string(color_name, Color()); - output += vformat("\u001b[38;2;%d;%d;%d;48;2;%d;%d;%dm", c.r * 255, c.g * 255, c.b * 255, c.r * 255, c.g * 255, c.b * 255); + output += should_color ? vformat("\u001b[38;2;%d;%d;%d;48;2;%d;%d;%dm", c.r * 255, c.g * 255, c.b * 255, c.r * 255, c.g * 255, c.b * 255) : ""; } } else if (tag == "/fgcolor") { - output += "\u001b[39;49m"; + output += should_color ? "\u001b[39;49m" : ""; } else { output += "["; pos = brk_pos + 1; diff --git a/doc/classes/@GlobalScope.xml b/doc/classes/@GlobalScope.xml index f04f500f823c..9c55df170881 100644 --- a/doc/classes/@GlobalScope.xml +++ b/doc/classes/@GlobalScope.xml @@ -877,6 +877,8 @@ [/codeblocks] [b]Note:[/b] Consider using [method push_error] and [method push_warning] to print error and warning messages instead of [method print] or [method print_rich]. This distinguishes them from print messages used for debugging purposes, while also displaying a stack trace when an error or warning is printed. [b]Note:[/b] Output displayed in the editor supports clickable [code skip-lint][url=address]text[/url][/code] tags. The [code skip-lint][url][/code] tag's [code]address[/code] value is handled by [method OS.shell_open] when clicked. + [b]Note:[/b] On Windows, only Windows 10 and later correctly displays ANSI escape codes in standard output. + [b]Note:[/b] Color/formatting tags are only effective if the engine has colored console output enabled. This is the case by default, but it can be disabled if stdout is not a TTY (e.g. if writing to a file or under a continuous integration setup) or if the [code]NO_COLOR[/code] environment variable is set to a non-empty string. The [code]--color auto|always|never[/code] command line argument can be used to override this behavior, with [code]auto[/code] being the default. diff --git a/drivers/unix/os_unix.cpp b/drivers/unix/os_unix.cpp index 31f7392d9b4d..8b5b449c4a72 100644 --- a/drivers/unix/os_unix.cpp +++ b/drivers/unix/os_unix.cpp @@ -1206,20 +1206,17 @@ void UnixTerminalLogger::log_error(const char *p_function, const char *p_file, i err_details = p_code; } - // Disable color codes if stdout is not a TTY. - // This prevents Godot from writing ANSI escape codes when redirecting - // stdout and stderr to a file. - const bool tty = isatty(fileno(stdout)); - const char *gray = tty ? "\E[0;90m" : ""; - const char *red = tty ? "\E[0;91m" : ""; - const char *red_bold = tty ? "\E[1;31m" : ""; - const char *yellow = tty ? "\E[0;93m" : ""; - const char *yellow_bold = tty ? "\E[1;33m" : ""; - const char *magenta = tty ? "\E[0;95m" : ""; - const char *magenta_bold = tty ? "\E[1;35m" : ""; - const char *cyan = tty ? "\E[0;96m" : ""; - const char *cyan_bold = tty ? "\E[1;36m" : ""; - const char *reset = tty ? "\E[0m" : ""; + const bool should_color = Engine::get_singleton() ? Engine::get_singleton()->is_coloring_standard_output() : false; + const char *gray = should_color ? "\E[0;90m" : ""; + const char *red = should_color ? "\E[0;91m" : ""; + const char *red_bold = should_color ? "\E[1;31m" : ""; + const char *yellow = should_color ? "\E[0;93m" : ""; + const char *yellow_bold = should_color ? "\E[1;33m" : ""; + const char *magenta = should_color ? "\E[0;95m" : ""; + const char *magenta_bold = should_color ? "\E[1;35m" : ""; + const char *cyan = should_color ? "\E[0;96m" : ""; + const char *cyan_bold = should_color ? "\E[1;36m" : ""; + const char *reset = should_color ? "\E[0m" : ""; const char *bold_color; const char *normal_color; diff --git a/main/main.cpp b/main/main.cpp index 62dafe881d4a..0aa060352559 100644 --- a/main/main.cpp +++ b/main/main.cpp @@ -147,6 +147,15 @@ #endif // TOOLS_ENABLED && !GDSCRIPT_NO_LSP #endif // MODULE_GDSCRIPT_ENABLED +#ifdef WINDOWS_ENABLED +#include +#include +#define isatty _isatty +#define fileno _fileno +#else +#include +#endif + /* Static members */ // Singletons @@ -539,6 +548,7 @@ void Main::print_help(const char *p_binary) { print_help_option("-v, --verbose", "Use verbose stdout mode.\n"); print_help_option("--quiet", "Quiet mode, silences stdout messages. Errors are still displayed.\n"); print_help_option("--no-header", "Do not print engine version and rendering method header on startup.\n"); + print_help_option("--color", "Use colors for console output (\"auto\", \"always\", \"never\").\n"); print_help_title("Run options"); print_help_option("--, ++", "Separator for user-provided arguments. Following arguments are not used by the engine, but can be read from `OS.get_cmdline_user_args()`.\n"); @@ -1049,6 +1059,16 @@ Error Main::setup(const char *execpath, int argc, char *argv[], bool p_second_ph packed_data = memnew(PackedData); } + // Disable color codes if stdout is not a TTY, or if the `NO_COLOR` environment variable + // is set to a non-empty string (https://no-color.org/). + // This prevents Godot from writing ANSI escape codes when redirecting stdout and stderr to a file. + // If we are running in a CI environment or `CLICOLOR_FORCE` is set to `1`, force colored output. + // + // These options are overridden by the `--color` command line argument. + const bool no_color = !OS::get_singleton()->get_environment("NO_COLOR").is_empty(); + const bool force_color = bool(OS::get_singleton()->get_environment("CLICOLOR_FORCE").to_int()) || bool(OS::get_singleton()->get_environment("CI").to_int()); + Engine::get_singleton()->set_color_standard_output(no_color ? false : (force_color || isatty(fileno(stdout)))); + #ifdef MINIZIP_ENABLED //XXX: always get_singleton() == 0x0 @@ -1134,7 +1154,23 @@ Error Main::setup(const char *execpath, int argc, char *argv[], bool p_second_ph } else if (arg == "--no-header") { Engine::get_singleton()->_print_header = false; + } else if (arg == "--color") { // Color console output. + if (N) { + // The "auto" case is already handled above. + if (N->get() == "always") { + Engine::get_singleton()->set_color_standard_output(true); + } else if (N->get() == "never") { + Engine::get_singleton()->set_color_standard_output(false); + } else if (N->get() != "auto") { + OS::get_singleton()->print("Unknown color argument '%s', aborting.\nValid options are 'auto', 'always' and 'never'.\n", N->get().utf8().get_data()); + goto error; + } + N = N->next(); + } else { + OS::get_singleton()->print("Missing color argument, aborting.\nValid options are 'auto', 'always' and 'never'.\n"); + goto error; + } } else if (arg == "--audio-driver") { // audio driver if (N) {