From d6a1ff59f1509e54026543d70c214190d2daa09a Mon Sep 17 00:00:00 2001 From: Brad King Date: Tue, 6 May 2025 11:55:42 -0400 Subject: [PATCH] StdIo: Provide metadata about stdin, stdout, stderr streams Detect the kind of terminal to which they are attached, if any. Issue: #26924 --- Source/CMakeLists.txt | 2 + Source/cmStdIoInit.cxx | 22 +++++ Source/cmStdIoStream.cxx | 161 +++++++++++++++++++++++++++++++++++ Source/cmStdIoStream.h | 103 ++++++++++++++++++++++ Tests/CMakeLib/testStdIo.cxx | 35 +++++++- bootstrap | 1 + 6 files changed, 323 insertions(+), 1 deletion(-) create mode 100644 Source/cmStdIoStream.cxx create mode 100644 Source/cmStdIoStream.h diff --git a/Source/CMakeLists.txt b/Source/CMakeLists.txt index d5e267ba69..2fe67c6f19 100644 --- a/Source/CMakeLists.txt +++ b/Source/CMakeLists.txt @@ -470,6 +470,8 @@ add_library( cmStateTypes.h cmStdIoInit.h cmStdIoInit.cxx + cmStdIoStream.h + cmStdIoStream.cxx cmStringAlgorithms.cxx cmStringAlgorithms.h cmSyntheticTargetCache.h diff --git a/Source/cmStdIoInit.cxx b/Source/cmStdIoInit.cxx index 244f384b66..8c834f2dc9 100644 --- a/Source/cmStdIoInit.cxx +++ b/Source/cmStdIoInit.cxx @@ -5,6 +5,7 @@ #include #include #include +#include #include @@ -18,6 +19,8 @@ # include #endif +#include "cmStdIoStream.h" + namespace cm { namespace StdIo { @@ -82,7 +85,11 @@ struct InitStdPipes class Globals { public: + std::ios::Init InitIos; InitStdPipes InitPipes; + IStream StdIn{ std::cin, stdin }; + OStream StdOut{ std::cout, stdout }; + OStream StdErr{ std::cerr, stderr }; static Globals& Get(); }; @@ -98,5 +105,20 @@ Init::Init() Globals::Get(); } +IStream& In() +{ + return Globals::Get().StdIn; +} + +OStream& Out() +{ + return Globals::Get().StdOut; +} + +OStream& Err() +{ + return Globals::Get().StdErr; +} + } } diff --git a/Source/cmStdIoStream.cxx b/Source/cmStdIoStream.cxx new file mode 100644 index 0000000000..9f448f2c84 --- /dev/null +++ b/Source/cmStdIoStream.cxx @@ -0,0 +1,161 @@ +/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying + file LICENSE.rst or https://cmake.org/licensing for details. */ +#include "cmStdIoStream.h" + +#include +#include +#include +#include // IWYU pragma: keep +#include // IWYU pragma: keep + +#ifdef _WIN32 +# include + +# include // for _get_osfhandle +#else +# include + +# include +# include +# include + +# include +#endif + +#include "cm_fileno.hxx" + +#ifndef _WIN32 +# include "cmSystemTools.h" +#endif + +namespace cm { +namespace StdIo { + +namespace { + +#ifndef _WIN32 +// List of known `TERM` names that support VT100 escape sequences. +// Order by `LC_COLLATE=C sort` to search using `std::lower_bound`. +std::array const kVT100Names{ { + "Eterm"_s, + "alacritty"_s, + "alacritty-direct"_s, + "ansi"_s, + "color-xterm"_s, + "con132x25"_s, + "con132x30"_s, + "con132x43"_s, + "con132x60"_s, + "con80x25"_s, + "con80x28"_s, + "con80x30"_s, + "con80x43"_s, + "con80x50"_s, + "con80x60"_s, + "cons25"_s, + "console"_s, + "cygwin"_s, + "dtterm"_s, + "eterm-color"_s, + "gnome"_s, + "gnome-256color"_s, + "konsole"_s, + "konsole-256color"_s, + "kterm"_s, + "linux"_s, + "linux-c"_s, + "mach-color"_s, + "mlterm"_s, + "msys"_s, + "putty"_s, + "putty-256color"_s, + "rxvt"_s, + "rxvt-256color"_s, + "rxvt-cygwin"_s, + "rxvt-cygwin-native"_s, + "rxvt-unicode"_s, + "rxvt-unicode-256color"_s, + "screen"_s, + "screen-256color"_s, + "screen-256color-bce"_s, + "screen-bce"_s, + "screen-w"_s, + "screen.linux"_s, + "st-256color"_s, + "tmux"_s, + "tmux-256color"_s, + "vt100"_s, + "xterm"_s, + "xterm-16color"_s, + "xterm-256color"_s, + "xterm-88color"_s, + "xterm-color"_s, + "xterm-debian"_s, + "xterm-kitty"_s, + "xterm-termite"_s, +} }; + +bool TermIsVT100() +{ + if (cm::optional term = cmSystemTools::GetEnvVar("TERM")) { + // NOLINTNEXTLINE(readability-qualified-auto) + auto i = std::lower_bound(kVT100Names.begin(), kVT100Names.end(), *term); + if (i != kVT100Names.end() && *i == *term) { + return true; + } + } + return false; +} +#endif + +} // anonymous namespace + +Stream::Stream(std::ios& s, FILE* file, Direction direction) + : IOS_(s) + , FD_(cm_fileno(file)) +{ +#ifdef _WIN32 + DWORD mode; + auto h = reinterpret_cast(_get_osfhandle(this->FD_)); + if (GetConsoleMode(h, &mode)) { + this->Console_ = h; + DWORD vtMode = mode | + (direction == Direction::In ? ENABLE_VIRTUAL_TERMINAL_INPUT + : ENABLE_VIRTUAL_TERMINAL_PROCESSING); + if (SetConsoleMode(this->Console_, vtMode)) { + this->Kind_ = TermKind::VT100; + } else { + SetConsoleMode(this->Console_, mode); + this->Kind_ = TermKind::Console; + } + } +#else + static_cast(direction); + if (isatty(this->FD_) && TermIsVT100()) { + this->Kind_ = TermKind::VT100; + } +#endif +} + +IStream::IStream(std::istream& is, FILE* file) + : Stream(is, file, Direction::In) +{ +} + +std::istream& IStream::IOS() const +{ + return dynamic_cast(this->Stream::IOS()); +} + +OStream::OStream(std::ostream& os, FILE* file) + : Stream(os, file, Direction::Out) +{ +} + +std::ostream& OStream::IOS() const +{ + return dynamic_cast(this->Stream::IOS()); +} + +} +} diff --git a/Source/cmStdIoStream.h b/Source/cmStdIoStream.h new file mode 100644 index 0000000000..c3d28ce8eb --- /dev/null +++ b/Source/cmStdIoStream.h @@ -0,0 +1,103 @@ +/* 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 + +namespace cm { +namespace StdIo { + +/** + * Identify the kind of terminal to which a stream is attached, if any. + */ +enum class TermKind +{ + /** Not an interactive terminal. */ + None, + /** A VT100 terminal. */ + VT100, +#ifdef _WIN32 + /** A Windows Console that does not support VT100 sequences. */ + Console, +#endif +}; + +/** + * Represent stdin, stdout, or stderr stream metadata. + */ +class Stream +{ +public: + /** The kind of terminal to which the stream is attached, if any. */ + TermKind Kind() const { return this->Kind_; } + + /** The underlying C++ stream. */ + std::ios& IOS() const { return this->IOS_; } + + /** The underlying file descriptor. */ + int FD() const { return this->FD_; } + +#ifdef _WIN32 + /** The underlying HANDLE of an attached Windows Console, if any. */ + void* Console() const { return this->Console_; } +#endif + +protected: + enum class Direction + { + In, + Out, + }; + + Stream(std::ios& s, FILE* file, Direction direction); + +private: + std::ios& IOS_; + int FD_ = -1; + TermKind Kind_ = TermKind::None; + +#ifdef _WIN32 + void* Console_ = nullptr; +#endif +}; + +/** + * Represent stdin metadata. + */ +class IStream : public Stream +{ + friend class Globals; + IStream(std::istream& is, FILE* file); + +public: + /** The underlying C++ stream. */ + std::istream& IOS() const; +}; + +/** + * Represent stdout or stderr metadata. + */ +class OStream : public Stream +{ + friend class Globals; + OStream(std::ostream& os, FILE* file); + +public: + /** The underlying C++ stream. */ + std::ostream& IOS() const; +}; + +/** Metadata for stdin. */ +IStream& In(); + +/** Metadata for stdout. */ +OStream& Out(); + +/** Metadata for stderr. */ +OStream& Err(); + +} +} diff --git a/Tests/CMakeLib/testStdIo.cxx b/Tests/CMakeLib/testStdIo.cxx index 6274480797..cb04ae417f 100644 --- a/Tests/CMakeLib/testStdIo.cxx +++ b/Tests/CMakeLib/testStdIo.cxx @@ -1,15 +1,48 @@ /* Distributed under the OSI-approved BSD 3-Clause License. See accompanying file LICENSE.rst or https://cmake.org/licensing for details. */ +#include +#include + #include "cmStdIoInit.h" +#include "cmStdIoStream.h" #include "testCommon.h" namespace { + +void printTermKind(cm::string_view t, cm::StdIo::Stream& s) +{ + switch (s.Kind()) { + case cm::StdIo::TermKind::None: + std::cout << " " << t << " is not a terminal.\n"; + break; + case cm::StdIo::TermKind::VT100: + std::cout << " " << t << " is a VT100 terminal.\n"; + break; +#ifdef _WIN32 + case cm::StdIo::TermKind::Console: + std::cout << " " << t << " is a Windows Console.\n"; + break; +#endif + }; +} + +bool testStream() +{ + std::cout << "testStream()\n"; + printTermKind("stdin"_s, cm::StdIo::In()); + printTermKind("stdout"_s, cm::StdIo::Out()); + printTermKind("stderr"_s, cm::StdIo::Err()); + return true; +} + } int testStdIo(int /*unused*/, char* /*unused*/[]) { cm::StdIo::Init(); - return runTests({}); + return runTests({ + testStream, + }); } diff --git a/bootstrap b/bootstrap index d377af54c7..3c1c3f8f78 100755 --- a/bootstrap +++ b/bootstrap @@ -490,6 +490,7 @@ CMAKE_CXX_SOURCES="\ cmStateDirectory \ cmStateSnapshot \ cmStdIoInit \ + cmStdIoStream \ cmString \ cmStringAlgorithms \ cmStringReplaceHelper \