diff --git a/Source/CMakeLists.txt b/Source/CMakeLists.txt index 70fe9091a5..59bd8cef59 100644 --- a/Source/CMakeLists.txt +++ b/Source/CMakeLists.txt @@ -472,6 +472,8 @@ add_library( cmStdIoInit.cxx cmStdIoStream.h cmStdIoStream.cxx + cmStdIoTerminal.h + cmStdIoTerminal.cxx cmStringAlgorithms.cxx cmStringAlgorithms.h cmSyntheticTargetCache.h diff --git a/Source/cmStdIoTerminal.cxx b/Source/cmStdIoTerminal.cxx new file mode 100644 index 0000000000..dfb3aa9ea0 --- /dev/null +++ b/Source/cmStdIoTerminal.cxx @@ -0,0 +1,170 @@ +/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying + file LICENSE.rst or https://cmake.org/licensing for details. */ +#include "cmStdIoTerminal.h" + +#include +#include +#include +#include +#include + +#include +#include + +#ifdef _WIN32 +# include +#endif + +#include + +#include "cmStdIoStream.h" +#include "cmSystemTools.h" + +namespace cm { +namespace StdIo { + +namespace { + +#ifdef _WIN32 +WORD const kConsoleAttrMask = FOREGROUND_RED | FOREGROUND_GREEN | + FOREGROUND_BLUE | FOREGROUND_INTENSITY | BACKGROUND_RED | BACKGROUND_GREEN | + BACKGROUND_BLUE | BACKGROUND_INTENSITY; +std::array const kConsoleAttrs{ { + 0, // Normal + FOREGROUND_INTENSITY, // ForegroundBold + 0, // ForegroundBlack + FOREGROUND_BLUE, // ForegroundBlue + FOREGROUND_GREEN | FOREGROUND_BLUE, // ForegroundCyan + FOREGROUND_GREEN, // ForegroundGreen + FOREGROUND_RED | FOREGROUND_BLUE, // ForegroundMagenta + FOREGROUND_RED, // ForegroundRed + FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE, // ForegroundWhite + FOREGROUND_RED | FOREGROUND_GREEN, // ForegroundYellow + BACKGROUND_INTENSITY, // BackgroundBold + 0, // BackgroundBlack + BACKGROUND_BLUE, // BackgroundBlue + BACKGROUND_GREEN | BACKGROUND_BLUE, // BackgroundCyan + BACKGROUND_GREEN, // BackgroundGreen + BACKGROUND_RED | BACKGROUND_BLUE, // BackgroundMagenta + BACKGROUND_RED, // BackgroundRed + BACKGROUND_RED | BACKGROUND_GREEN | BACKGROUND_BLUE, // BackgroundWhite + BACKGROUND_RED | BACKGROUND_GREEN, // BackgroundYellow +} }; + +WORD ConsoleAttrs(WORD consoleAttrs, TermAttrSet const& attrs) +{ + consoleAttrs = + attrs.contains(TermAttr::Normal) ? consoleAttrs & kConsoleAttrMask : 0; + for (TermAttr attr : attrs) { + auto index = static_cast::type>(attr); + consoleAttrs |= kConsoleAttrs[index]; + } + return consoleAttrs; +} +#endif + +// VT100 escape sequence strings. +#if defined(__MVS__) // z/OS: assume EBCDIC +# define ESC "\47" +#else +# define ESC "\33" +#endif + +std::array const kVT100Codes{ { + ESC "[0m"_s, // Normal + ESC "[1m"_s, // ForegroundBold + ESC "[30m"_s, // ForegroundBlack + ESC "[34m"_s, // ForegroundBlue + ESC "[36m"_s, // ForegroundCyan + ESC "[32m"_s, // ForegroundGreen + ESC "[35m"_s, // ForegroundMagenta + ESC "[31m"_s, // ForegroundRed + ESC "[37m"_s, // ForegroundWhite + ESC "[33m"_s, // ForegroundYellow + ""_s, // BackgroundBold + ESC "[40m"_s, // BackgroundBlack + ESC "[44m"_s, // BackgroundBlue + ESC "[46m"_s, // BackgroundCyan + ESC "[42m"_s, // BackgroundGreen + ESC "[45m"_s, // BackgroundMagenta + ESC "[41m"_s, // BackgroundRed + ESC "[47m"_s, // BackgroundWhite + ESC "[43m"_s, // BackgroundYellow +} }; + +void SetVT100Attrs(std::ostream& os, TermAttrSet const& attrs) +{ + for (TermAttr attr : attrs) { + auto index = static_cast::type>(attr); + os << kVT100Codes[index]; + } +} + +auto const TermEnv = []() -> cm::optional { + /* Force color according to https://bixense.com/clicolors/ convention. */ + if (cm::optional cliColorForce = + cmSystemTools::GetEnvVar("CLICOLOR_FORCE")) { + if (!cliColorForce->empty() && *cliColorForce != "0"_s) { + return TermKind::VT100; + } + } + /* Disable color according to https://bixense.com/clicolors/ convention. */ + if (cm::optional cliColor = + cmSystemTools::GetEnvVar("CLICOLOR")) { + if (*cliColor == "0"_s) { + return TermKind::None; + } + } + /* GNU make 4.1+ may tell us that its output is destined for a TTY. */ + if (cm::optional makeTermOut = + cmSystemTools::GetEnvVar("MAKE_TERMOUT")) { + if (!makeTermOut->empty()) { + return TermKind::VT100; + } + } + return cm::nullopt; +}(); + +void Print(OStream& os, TermAttrSet const& attrs, + std::function const& f) +{ + TermKind kind = TermEnv ? *TermEnv : os.Kind(); + switch (kind) { + case TermKind::None: + f(os.IOS()); + break; + case TermKind::VT100: + SetVT100Attrs(os.IOS(), attrs); + f(os.IOS()); + SetVT100Attrs(os.IOS(), TermAttr::Normal); + break; +#ifdef _WIN32 + case TermKind::Console: { + HANDLE console = os.Console(); + CONSOLE_SCREEN_BUFFER_INFO sbi; + if (!attrs.empty() && GetConsoleScreenBufferInfo(console, &sbi)) { + Out().IOS().flush(); + Err().IOS().flush(); + SetConsoleTextAttribute(console, ConsoleAttrs(sbi.wAttributes, attrs)); + f(os.IOS()); + Out().IOS().flush(); + Err().IOS().flush(); + SetConsoleTextAttribute( + console, ConsoleAttrs(sbi.wAttributes, TermAttr::Normal)); + } else { + f(os.IOS()); + } + } break; +#endif + }; +} + +} // anonymous namespace + +void Print(OStream& os, TermAttrSet const& attrs, cm::string_view s) +{ + Print(os, attrs, [s](std::ostream& o) { o << s; }); +} + +} +} diff --git a/Source/cmStdIoTerminal.h b/Source/cmStdIoTerminal.h new file mode 100644 index 0000000000..d12d55d13e --- /dev/null +++ b/Source/cmStdIoTerminal.h @@ -0,0 +1,56 @@ +/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying + file LICENSE.rst or https://cmake.org/licensing for details. */ +#pragma once + +#include "cmConfigure.h" // IWYU pragma: keep + +#include +#include + +#include +#include + +namespace cm { +namespace StdIo { + +class OStream; + +/** + * Represent a text attribute. + */ +enum class TermAttr : std::uint8_t +{ + Normal, + ForegroundBold, + ForegroundBlack, + ForegroundBlue, + ForegroundCyan, + ForegroundGreen, + ForegroundMagenta, + ForegroundRed, + ForegroundWhite, + ForegroundYellow, + BackgroundBold, + BackgroundBlack, + BackgroundBlue, + BackgroundCyan, + BackgroundGreen, + BackgroundMagenta, + BackgroundRed, + BackgroundWhite, + BackgroundYellow, +}; +static constexpr std::size_t kTermAttrCount = 19; + +/** + * Represent a set of text attributes. + */ +using TermAttrSet = cm::enum_set; + +/** + * Print text to an output stream using a given set of color attributes. + */ +void Print(OStream& os, TermAttrSet const& attrs, cm::string_view text); + +} +} diff --git a/Tests/CMakeLib/testStdIo.cxx b/Tests/CMakeLib/testStdIo.cxx index f1d20344be..c6570fac7a 100644 --- a/Tests/CMakeLib/testStdIo.cxx +++ b/Tests/CMakeLib/testStdIo.cxx @@ -9,6 +9,7 @@ #include "cmStdIoConsole.h" #include "cmStdIoInit.h" #include "cmStdIoStream.h" +#include "cmStdIoTerminal.h" #include "testCommon.h" @@ -70,6 +71,54 @@ bool testConsole() return true; } +void testTerminalPrint(cm::StdIo::TermAttrSet const& attrs, + cm::string_view text) +{ + using namespace cm::StdIo; + std::cout << " "; + Print(Out(), attrs, text); +#ifdef _WIN32 + if (Out().Kind() == TermKind::Console) { + std::cout << " : "; + Print(Out(), attrs | TermAttr::BackgroundBold, text); + } +#endif + std::cout << std::endl; +} + +bool testTerminal() +{ + std::cout << "testTerminal()\n"; + using cm::StdIo::TermAttr; + testTerminalPrint(TermAttr::Normal, "Normal"_s); + testTerminalPrint(TermAttr::ForegroundBold, "Bold"_s); + testTerminalPrint(TermAttr::ForegroundBlack, "Black"_s); + testTerminalPrint(TermAttr::ForegroundBlue, "Blue"_s); + testTerminalPrint(TermAttr::ForegroundCyan, "Cyan"_s); + testTerminalPrint(TermAttr::ForegroundGreen, "Green"_s); + testTerminalPrint(TermAttr::ForegroundMagenta, "Magenta"_s); + testTerminalPrint(TermAttr::ForegroundRed, "Red"_s); + testTerminalPrint(TermAttr::ForegroundWhite, "White"_s); + testTerminalPrint(TermAttr::ForegroundYellow, "Yellow"_s); + testTerminalPrint({ TermAttr::ForegroundBold, TermAttr::BackgroundBlack }, + "Bold on Black"_s); + testTerminalPrint({ TermAttr::ForegroundBlack, TermAttr::BackgroundBlue }, + "Black on Blue"_s); + testTerminalPrint({ TermAttr::ForegroundBlack, TermAttr::BackgroundCyan }, + "Black on Cyan"_s); + testTerminalPrint({ TermAttr::ForegroundBlack, TermAttr::BackgroundGreen }, + "Black on Green"_s); + testTerminalPrint({ TermAttr::ForegroundBlack, TermAttr::BackgroundMagenta }, + "Black on Magenta"_s); + testTerminalPrint({ TermAttr::ForegroundBlack, TermAttr::BackgroundRed }, + "Black on Red"_s); + testTerminalPrint({ TermAttr::ForegroundBlack, TermAttr::BackgroundWhite }, + "Black on White"_s); + testTerminalPrint({ TermAttr::ForegroundBlack, TermAttr::BackgroundYellow }, + "Black on Yellow"_s); + return true; +} + cm::string_view const kUsage = "usage: CMakeLibTests testStdIo [--stdin]"_s; } @@ -91,5 +140,6 @@ int testStdIo(int argc, char* argv[]) return runTests({ testStream, testConsole, + testTerminal, }); } diff --git a/bootstrap b/bootstrap index 18788bea9d..a385ed22c8 100755 --- a/bootstrap +++ b/bootstrap @@ -491,6 +491,7 @@ CMAKE_CXX_SOURCES="\ cmStdIoConsole \ cmStdIoInit \ cmStdIoStream \ + cmStdIoTerminal \ cmString \ cmStringAlgorithms \ cmStringReplaceHelper \