diff --git a/src/core/terminal/include/sourcemeta/core/terminal.h b/src/core/terminal/include/sourcemeta/core/terminal.h index fbbe84a7d1..527d28c0bc 100644 --- a/src/core/terminal/include/sourcemeta/core/terminal.h +++ b/src/core/terminal/include/sourcemeta/core/terminal.h @@ -76,7 +76,10 @@ enum class TerminalColorPolicy : std::uint8_t { /// https://pubs.opengroup.org/onlinepubs/9699919799/functions/isatty.html WhenInteractive, /// Styling is unconditionally suppressed. - Disabled + Disabled, + /// Styling is unconditionally enabled regardless of destination + /// interactivity. + Always }; /// @ingroup terminal @@ -328,6 +331,49 @@ SOURCEMETA_CORE_TERMINAL_EXPORT auto terminal_set_color_policy(TerminalStream stream, TerminalColorPolicy policy) noexcept -> void; +/// @ingroup terminal +/// +/// Reset the color policy across all streams to default +/// (`TerminalColorPolicy::WhenInteractive`). +/// +/// For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// sourcemeta::core::terminal_set_color_policy( +/// sourcemeta::core::TerminalColorPolicy::Disabled); +/// sourcemeta::core::terminal_reset_color_policy(); +/// assert(sourcemeta::core::terminal_color_policy() == +/// sourcemeta::core::TerminalColorPolicy::WhenInteractive); +/// ``` +SOURCEMETA_CORE_TERMINAL_EXPORT +auto terminal_reset_color_policy() noexcept -> void; + +/// @ingroup terminal +/// +/// Reset the color policy for a specific stream to default +/// (`TerminalColorPolicy::WhenInteractive`). +/// +/// For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// sourcemeta::core::terminal_set_color_policy( +/// sourcemeta::core::TerminalStream::Stderr, +/// sourcemeta::core::TerminalColorPolicy::Disabled); +/// sourcemeta::core::terminal_reset_color_policy( +/// sourcemeta::core::TerminalStream::Stderr); +/// assert(sourcemeta::core::terminal_color_policy( +/// sourcemeta::core::TerminalStream::Stderr) == +/// sourcemeta::core::TerminalColorPolicy::WhenInteractive); +/// ``` +SOURCEMETA_CORE_TERMINAL_EXPORT +auto terminal_reset_color_policy(TerminalStream stream) noexcept -> void; + /// @ingroup terminal /// /// Retrieve the current color policy for the specified stream. @@ -340,7 +386,8 @@ auto terminal_set_color_policy(TerminalStream stream, /// /// const auto policy{sourcemeta::core::terminal_color_policy()}; /// assert(policy == sourcemeta::core::TerminalColorPolicy::WhenInteractive || -/// policy == sourcemeta::core::TerminalColorPolicy::Disabled); +/// policy == sourcemeta::core::TerminalColorPolicy::Disabled || +/// policy == sourcemeta::core::TerminalColorPolicy::Always); /// ``` SOURCEMETA_CORE_TERMINAL_EXPORT auto terminal_color_policy( diff --git a/src/core/terminal/terminal.cc b/src/core/terminal/terminal.cc index d5bb4649fa..2029dfbf2e 100644 --- a/src/core/terminal/terminal.cc +++ b/src/core/terminal/terminal.cc @@ -10,7 +10,7 @@ #include // std::ostream #include // std::string #include // std::string_view -#include // std::to_underlying +#include // std::to_underlying, std::unreachable namespace { @@ -49,14 +49,30 @@ auto terminal_set_color_policy(TerminalStream stream, std::memory_order_relaxed); } +auto terminal_reset_color_policy() noexcept -> void { + terminal_set_color_policy(TerminalColorPolicy::WhenInteractive); +} + +auto terminal_reset_color_policy(TerminalStream stream) noexcept -> void { + terminal_set_color_policy(stream, TerminalColorPolicy::WhenInteractive); +} + auto terminal_color_policy(TerminalStream stream) noexcept -> TerminalColorPolicy { return stream_policies[stream_index(stream)].load(std::memory_order_relaxed); } auto terminal_color_enabled(TerminalStream stream) noexcept -> bool { - return terminal_color_policy(stream) != TerminalColorPolicy::Disabled && - terminal_is_interactive(stream); + switch (terminal_color_policy(stream)) { + case TerminalColorPolicy::WhenInteractive: + return terminal_is_interactive(stream); + case TerminalColorPolicy::Always: + return true; + case TerminalColorPolicy::Disabled: + return false; + default: + std::unreachable(); + } } auto terminal_sgr_reset() noexcept -> std::string_view { diff --git a/test/terminal/CMakeLists.txt b/test/terminal/CMakeLists.txt index d5ab8a74c0..bb17cc58ea 100644 --- a/test/terminal/CMakeLists.txt +++ b/test/terminal/CMakeLists.txt @@ -3,3 +3,20 @@ sourcemeta_test(NAMESPACE sourcemeta PROJECT core NAME terminal target_link_libraries(sourcemeta_core_terminal_unit PRIVATE sourcemeta::core::terminal) + +sourcemeta_executable(NAMESPACE sourcemeta PROJECT core NAME terminal + VARIANT test_helper SOURCES terminal_child_main.cc) +target_link_libraries(sourcemeta_core_terminal_test_helper + PRIVATE sourcemeta::core::terminal + PRIVATE sourcemeta::core::options) + +macro(add_terminal_test name) + add_test(NAME core.terminal.${name} COMMAND + "$" + "${CMAKE_CURRENT_SOURCE_DIR}/${name}.clitest" + --binary "$") +endmacro() + +add_terminal_test(color_auto) +add_terminal_test(color_always) +add_terminal_test(color_never) diff --git a/test/terminal/color_always.clitest b/test/terminal/color_always.clitest new file mode 100644 index 0000000000..773f19b133 --- /dev/null +++ b/test/terminal/color_always.clitest @@ -0,0 +1,6 @@ +RUN --color always STDIN /dev/null IN . INTO result.txt EXPECTING 0 +WRITE expected.txt UNTIL END +1> streamed +1> painted +END +COMPARE result.txt AGAINST expected.txt diff --git a/test/terminal/color_auto.clitest b/test/terminal/color_auto.clitest new file mode 100644 index 0000000000..9f4051bc3c --- /dev/null +++ b/test/terminal/color_auto.clitest @@ -0,0 +1,6 @@ +RUN --color auto STDIN /dev/null IN . INTO result.txt EXPECTING 0 +WRITE expected.txt UNTIL END +1> streamed +1> painted +END +COMPARE result.txt AGAINST expected.txt diff --git a/test/terminal/color_never.clitest b/test/terminal/color_never.clitest new file mode 100644 index 0000000000..9cf99e24f0 --- /dev/null +++ b/test/terminal/color_never.clitest @@ -0,0 +1,6 @@ +RUN --color never STDIN /dev/null IN . INTO result.txt EXPECTING 0 +WRITE expected.txt UNTIL END +1> streamed +1> painted +END +COMPARE result.txt AGAINST expected.txt diff --git a/test/terminal/terminal_child_main.cc b/test/terminal/terminal_child_main.cc new file mode 100644 index 0000000000..fc471918b7 --- /dev/null +++ b/test/terminal/terminal_child_main.cc @@ -0,0 +1,69 @@ +#include +#include + +#include +#include +#include +#include + +#if defined(_WIN32) +#include // _O_BINARY +#include // _setmode, _fileno +#endif + +namespace { + +auto run_mode(sourcemeta::core::TerminalColorPolicy policy) -> int { + sourcemeta::core::terminal_set_color_policy( + sourcemeta::core::TerminalStream::Stdout, policy); + assert(!sourcemeta::core::terminal_is_interactive( + sourcemeta::core::TerminalStream::Stdout)); + sourcemeta::core::terminal_paint( + std::cout, sourcemeta::core::TerminalStream::Stdout, "streamed", + sourcemeta::core::TerminalStyle::Red); + std::cout << "\n" + << sourcemeta::core::terminal_paint( + sourcemeta::core::TerminalStream::Stdout, "painted", + sourcemeta::core::TerminalStyle::Bold | + sourcemeta::core::TerminalStyle::Green) + << "\n" + << std::flush; + return EXIT_SUCCESS; +} + +} // namespace + +auto main(int argc, char **argv) -> int { +#if defined(_WIN32) + // Ensure standard output is binary on Windows so newlines are not converted + // to CRLF (\r\n), which would alter captured byte stream comparisons. + _setmode(_fileno(stdout), _O_BINARY); +#endif + + sourcemeta::core::Options application; + application.option("color", {}); + application.parse(argc, argv); + + auto policy{sourcemeta::core::TerminalColorPolicy::WhenInteractive}; + if (application.contains("color")) { + const auto &values{application.at("color")}; + if (values.empty()) { + std::cerr << "missing value for --color\n"; + return EXIT_FAILURE; + } + + const auto value{values.front()}; + if (value == "always") { + policy = sourcemeta::core::TerminalColorPolicy::Always; + } else if (value == "auto" || value == "when-interactive") { + policy = sourcemeta::core::TerminalColorPolicy::WhenInteractive; + } else if (value == "never" || value == "disabled") { + policy = sourcemeta::core::TerminalColorPolicy::Disabled; + } else { + std::cerr << "unknown --color option: " << value << "\n"; + return EXIT_FAILURE; + } + } + + return run_mode(policy); +} diff --git a/test/terminal/terminal_test.cc b/test/terminal/terminal_test.cc index 1b221d6590..ea2f7082f6 100644 --- a/test/terminal/terminal_test.cc +++ b/test/terminal/terminal_test.cc @@ -211,6 +211,8 @@ TEST(policy_default_argument) { } TEST(policy_lifecycle_and_isolation) { + sourcemeta::core::terminal_reset_color_policy(); + // Global configuration affects all streams sourcemeta::core::terminal_set_color_policy( sourcemeta::core::TerminalColorPolicy::WhenInteractive); @@ -251,11 +253,46 @@ TEST(policy_lifecycle_and_isolation) { EXPECT_EQ(stderr_stream.str(), "error_stream"); // Restore default policy across all streams + sourcemeta::core::terminal_reset_color_policy(); + EXPECT_EQ(sourcemeta::core::terminal_color_policy( + sourcemeta::core::TerminalStream::Stderr), + sourcemeta::core::TerminalColorPolicy::WhenInteractive); +} + +TEST(policy_reset_global) { sourcemeta::core::terminal_set_color_policy( - sourcemeta::core::TerminalColorPolicy::WhenInteractive); + sourcemeta::core::TerminalColorPolicy::Always); + EXPECT_EQ(sourcemeta::core::terminal_color_policy( + sourcemeta::core::TerminalStream::Stdout), + sourcemeta::core::TerminalColorPolicy::Always); + sourcemeta::core::terminal_reset_color_policy(); + EXPECT_EQ(sourcemeta::core::terminal_color_policy( + sourcemeta::core::TerminalStream::Stdout), + sourcemeta::core::TerminalColorPolicy::WhenInteractive); + EXPECT_EQ(sourcemeta::core::terminal_color_policy( + sourcemeta::core::TerminalStream::Stderr), + sourcemeta::core::TerminalColorPolicy::WhenInteractive); + EXPECT_EQ(sourcemeta::core::terminal_color_policy( + sourcemeta::core::TerminalStream::Stdin), + sourcemeta::core::TerminalColorPolicy::WhenInteractive); +} + +TEST(policy_reset_per_stream) { + sourcemeta::core::terminal_set_color_policy( + sourcemeta::core::TerminalStream::Stderr, + sourcemeta::core::TerminalColorPolicy::Disabled); + sourcemeta::core::terminal_set_color_policy( + sourcemeta::core::TerminalStream::Stdout, + sourcemeta::core::TerminalColorPolicy::Always); + sourcemeta::core::terminal_reset_color_policy( + sourcemeta::core::TerminalStream::Stderr); EXPECT_EQ(sourcemeta::core::terminal_color_policy( sourcemeta::core::TerminalStream::Stderr), sourcemeta::core::TerminalColorPolicy::WhenInteractive); + EXPECT_EQ(sourcemeta::core::terminal_color_policy( + sourcemeta::core::TerminalStream::Stdout), + sourcemeta::core::TerminalColorPolicy::Always); + sourcemeta::core::terminal_reset_color_policy(); } TEST(stream_detection_runs_safely) {