diff --git a/src/windows/common/CMakeLists.txt b/src/windows/common/CMakeLists.txt index 9551fcbaf..ba9f9706e 100644 --- a/src/windows/common/CMakeLists.txt +++ b/src/windows/common/CMakeLists.txt @@ -35,6 +35,7 @@ set(SOURCES string.cpp SubProcess.cpp svccomm.cpp + VTSupport.cpp WindowsUpdateIntegration.cpp WSLCContainerLauncher.cpp VirtioNetworking.cpp @@ -119,6 +120,7 @@ set(HEADERS Stringify.h SubProcess.h svccomm.hpp + VTSupport.h WindowsUpdateIntegration.h WSLCContainerLauncher.h VirtioNetworking.h diff --git a/src/windows/common/VTSupport.cpp b/src/windows/common/VTSupport.cpp new file mode 100644 index 000000000..b8f3c4316 --- /dev/null +++ b/src/windows/common/VTSupport.cpp @@ -0,0 +1,381 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + VTSupport.cpp + +Abstract: + + This file contains the implementation of VT sequence constructors and + console mode helpers declared in VTSupport.h. + +--*/ + +#include "precomp.h" +#include "VTSupport.h" + +#define WSL_WINDOWS_VT_ESCAPE "\x1b" +#define WSL_WINDOWS_VT_CSI WSL_WINDOWS_VT_ESCAPE "[" +#define WSL_WINDOWS_VT_OSC WSL_WINDOWS_VT_ESCAPE "]" +#define WSL_WINDOWS_VT_TEXTFORMAT(_id_) WSL_WINDOWS_VT_CSI #_id_ "m" + +// Wide-string equivalents used for wostream output in PrimaryDeviceAttributes. +#define WSL_WINDOWS_VT_ESCAPE_W L"\x1b" +#define WSL_WINDOWS_VT_CSI_W WSL_WINDOWS_VT_ESCAPE_W L"[" + +namespace wsl::windows::common::vt { +namespace { + // Extracts a VT sequence of the form ESC + prefix + result + suffix from a + // wide input stream, returning the result part as a std::wstring. + // + // Reads up to s_bufferSize wide characters from inStream; calls peek() first + // to prompt the stream buffer to fill from the underlying device, then uses + // readsome() to drain only what is immediately available. If the buffer fills + // completely the suffix may not be present, in which case an empty wstring is + // returned — the same outcome as any other parse failure. Any characters after + // the suffix (e.g. queued user input) are ignored. + // + // The DA1 response bytes are pure ASCII (0x00–0x7F); under _O_U8TEXT mode the + // CRT decodes each byte to the identical wchar_t value, so wide reads are + // lossless for all VT escape sequence content. + // + // Note: peek() may block briefly on a real console stdin until the terminal + // delivers its response (typically a few milliseconds for DA1). It will not + // block indefinitely because all supported Windows console hosts (Windows Terminal, + // conhost.exe) respond to ESC[0c, and non-console streams (pipes, wstringstreams) + // have data already buffered by the caller. + std::wstring ExtractSequence(std::wistream& inStream, std::wstring_view prefix, std::wstring_view suffix) + { + // Force discovery of available input. + std::ignore = inStream.peek(); + + static constexpr std::streamsize s_bufferSize = 1024; + wchar_t buffer[s_bufferSize]; + + // readsome() returns at most s_bufferSize wide characters, so == s_bufferSize + // is the full-buffer case, not overflow. If the suffix is still within those + // characters the parse succeeds normally; if not, the suffix-not-found path + // below returns {}. + std::streamsize charsRead = inStream.readsome(buffer, s_bufferSize); + + std::wstring_view resultView{buffer, static_cast(charsRead)}; + + // Locate the escape character that begins the sequence. + const size_t escapeIndex = resultView.find(L'\x1b'); + if (escapeIndex == std::wstring_view::npos) + { + return {}; + } + + resultView = resultView.substr(escapeIndex); + + // Verify the prefix immediately follows the escape character. + if (resultView.length() < 1 + prefix.length() || resultView.substr(1, prefix.length()) != prefix) + { + return {}; + } + + // Find the suffix anywhere after the prefix. + const std::wstring_view body = resultView.substr(1 + prefix.length()); + const size_t suffixIndex = body.find(suffix); + if (suffixIndex == std::wstring_view::npos) + { + return {}; + } + + return std::wstring{body.substr(0, suffixIndex)}; + } +} // namespace + +bool Sequence::IsColor() const +{ + const auto sv = m_chars; + if (sv.size() < 2 || sv[0] != '\x1b') + { + return false; + } + + if (sv[1] == '[') + { + // CSI sequence — color if final byte is 'm' (SGR) + return sv.back() == 'm'; + } + + if (sv[1] == ']') + { + // OSC 8 hyperlink — treated as color-adjacent + return sv.size() >= 3 && sv[2] == '8'; + } + + return false; +} + +void ConstructedSequence::Append(const Sequence& sequence) +{ + if (!sequence.Get().empty()) + { + m_str += sequence.Get(); + Set(m_str); + } +} + +void ConstructedSequence::Clear() +{ + m_str.clear(); + Set(m_str); +} + +ConstructedSequence Sgr(std::initializer_list params) +{ + std::ostringstream result; + result << WSL_WINDOWS_VT_CSI; + bool first = true; + for (const int param : params) + { + if (!first) + { + result << ';'; + } + result << param; + first = false; + } + result << 'm'; + return ConstructedSequence{std::move(result).str()}; +} + +PrimaryDeviceAttributes::PrimaryDeviceAttributes(std::wostream& outStream, std::wistream& inStream) +{ + try + { + // Best-effort: enable VT input on the real console handle so the terminal + // sends a machine-readable DA1 response. When stdin is redirected (e.g. + // in unit tests that supply their own wstringstreams) this will fail, but + // we still proceed — the caller is responsible for providing a readable + // inStream that contains the DA1 response. + EnableVirtualTerminal inputMode{GetStdHandle(STD_INPUT_HANDLE), EnableVirtualTerminal::Mode::Input}; + + // Send DA1 Primary Device Attributes request. + // The CSI sequence bytes are pure ASCII; L"..." widening is lossless. + outStream << WSL_WINDOWS_VT_CSI_W << L"0c"; + outStream.flush(); + + // Response is of the form ESC[?;...c + // Split returns std::vector via the wstring_view template overload. + std::wstring sequence = ExtractSequence(inStream, L"[?", L"c"); + std::vector values = wsl::shared::string::Split(sequence, L';'); + + if (!values.empty()) + { + // Use wcstoul so the wchar_t digits are parsed directly without any + // narrowing conversion. + m_conformanceLevel = std::wcstoul(values[0].c_str(), nullptr, 10); + } + + // m_extensions is a uint64_t bitmask; extension values >= 64 cannot be + // represented and are silently ignored to avoid undefined behaviour from + // an out-of-range shift. + constexpr unsigned long c_maxExtensionBit = 63ul; + for (size_t i = 1; i < values.size(); ++i) + { + const unsigned long ext = std::wcstoul(values[i].c_str(), nullptr, 10); + if (ext <= c_maxExtensionBit) + { + m_extensions |= 1ull << ext; + } + } + } + CATCH_LOG(); +} + +bool PrimaryDeviceAttributes::Supports(Extension extension) const +{ + uint64_t extensionMask = 1ull << ToIntegral(extension); + return (m_extensions & extensionMask) == extensionMask; +} + +namespace Cursor { + ConstructedSequence Up(int cells) + { + THROW_HR_IF(E_INVALIDARG, cells < 0); + return ConstructedSequence{std::format(WSL_WINDOWS_VT_CSI "{}A", cells)}; + } + + ConstructedSequence Down(int cells) + { + THROW_HR_IF(E_INVALIDARG, cells < 0); + return ConstructedSequence{std::format(WSL_WINDOWS_VT_CSI "{}B", cells)}; + } + + ConstructedSequence Forward(int cells) + { + THROW_HR_IF(E_INVALIDARG, cells < 0); + return ConstructedSequence{std::format(WSL_WINDOWS_VT_CSI "{}C", cells)}; + } + + ConstructedSequence Backward(int cells) + { + THROW_HR_IF(E_INVALIDARG, cells < 0); + return ConstructedSequence{std::format(WSL_WINDOWS_VT_CSI "{}D", cells)}; + } + + ConstructedSequence MoveTo(int row, int col) + { + THROW_HR_IF(E_INVALIDARG, row < 1 || col < 1); + return ConstructedSequence{std::format(WSL_WINDOWS_VT_CSI "{};{}H", row, col)}; + } + + const Sequence Home{WSL_WINDOWS_VT_CSI "H"}; + const Sequence EnableBlink{WSL_WINDOWS_VT_CSI "?12h"}; + const Sequence DisableBlink{WSL_WINDOWS_VT_CSI "?12l"}; + const Sequence Show{WSL_WINDOWS_VT_CSI "?25h"}; + const Sequence Hide{WSL_WINDOWS_VT_CSI "?25l"}; + + const Sequence BracketedPasteOn{WSL_WINDOWS_VT_CSI "?2004h"}; + const Sequence BracketedPasteOff{WSL_WINDOWS_VT_CSI "?2004l"}; +} // namespace Cursor + +namespace Format { + const Sequence Default{WSL_WINDOWS_VT_TEXTFORMAT(0)}; + const Sequence Negative{WSL_WINDOWS_VT_TEXTFORMAT(7)}; + const Sequence Bright{WSL_WINDOWS_VT_TEXTFORMAT(1)}; + const Sequence Dim{WSL_WINDOWS_VT_TEXTFORMAT(2)}; + const Sequence Normal{WSL_WINDOWS_VT_TEXTFORMAT(22)}; + const Sequence Italic{WSL_WINDOWS_VT_TEXTFORMAT(3)}; + const Sequence NoItalic{WSL_WINDOWS_VT_TEXTFORMAT(23)}; + const Sequence Underline{WSL_WINDOWS_VT_TEXTFORMAT(4)}; + const Sequence NoUnderline{WSL_WINDOWS_VT_TEXTFORMAT(24)}; + + namespace Fg { + const Sequence Black{WSL_WINDOWS_VT_TEXTFORMAT(30)}; + const Sequence Red{WSL_WINDOWS_VT_TEXTFORMAT(31)}; + const Sequence Green{WSL_WINDOWS_VT_TEXTFORMAT(32)}; + const Sequence Yellow{WSL_WINDOWS_VT_TEXTFORMAT(33)}; + const Sequence Blue{WSL_WINDOWS_VT_TEXTFORMAT(34)}; + const Sequence Magenta{WSL_WINDOWS_VT_TEXTFORMAT(35)}; + const Sequence Cyan{WSL_WINDOWS_VT_TEXTFORMAT(36)}; + const Sequence White{WSL_WINDOWS_VT_TEXTFORMAT(37)}; + + const Sequence BrightBlack{WSL_WINDOWS_VT_TEXTFORMAT(90)}; + const Sequence BrightRed{WSL_WINDOWS_VT_TEXTFORMAT(91)}; + const Sequence BrightGreen{WSL_WINDOWS_VT_TEXTFORMAT(92)}; + const Sequence BrightYellow{WSL_WINDOWS_VT_TEXTFORMAT(93)}; + const Sequence BrightBlue{WSL_WINDOWS_VT_TEXTFORMAT(94)}; + const Sequence BrightMagenta{WSL_WINDOWS_VT_TEXTFORMAT(95)}; + const Sequence BrightCyan{WSL_WINDOWS_VT_TEXTFORMAT(96)}; + const Sequence BrightWhite{WSL_WINDOWS_VT_TEXTFORMAT(97)}; + + ConstructedSequence Extended(const Color& color) + { + std::ostringstream result; + result << WSL_WINDOWS_VT_CSI "38;2;" << static_cast(color.R) << ';' << static_cast(color.G) << ';' + << static_cast(color.B) << 'm'; + return ConstructedSequence{std::move(result).str()}; + } + } // namespace Fg + + namespace Bg { + const Sequence Black{WSL_WINDOWS_VT_TEXTFORMAT(40)}; + const Sequence Red{WSL_WINDOWS_VT_TEXTFORMAT(41)}; + const Sequence Green{WSL_WINDOWS_VT_TEXTFORMAT(42)}; + const Sequence Yellow{WSL_WINDOWS_VT_TEXTFORMAT(43)}; + const Sequence Blue{WSL_WINDOWS_VT_TEXTFORMAT(44)}; + const Sequence Magenta{WSL_WINDOWS_VT_TEXTFORMAT(45)}; + const Sequence Cyan{WSL_WINDOWS_VT_TEXTFORMAT(46)}; + const Sequence White{WSL_WINDOWS_VT_TEXTFORMAT(47)}; + + const Sequence BrightBlack{WSL_WINDOWS_VT_TEXTFORMAT(100)}; + const Sequence BrightRed{WSL_WINDOWS_VT_TEXTFORMAT(101)}; + const Sequence BrightGreen{WSL_WINDOWS_VT_TEXTFORMAT(102)}; + const Sequence BrightYellow{WSL_WINDOWS_VT_TEXTFORMAT(103)}; + const Sequence BrightBlue{WSL_WINDOWS_VT_TEXTFORMAT(104)}; + const Sequence BrightMagenta{WSL_WINDOWS_VT_TEXTFORMAT(105)}; + const Sequence BrightCyan{WSL_WINDOWS_VT_TEXTFORMAT(106)}; + const Sequence BrightWhite{WSL_WINDOWS_VT_TEXTFORMAT(107)}; + + ConstructedSequence Extended(const Color& color) + { + std::ostringstream result; + result << WSL_WINDOWS_VT_CSI "48;2;" << static_cast(color.R) << ';' << static_cast(color.G) << ';' + << static_cast(color.B) << 'm'; + return ConstructedSequence{std::move(result).str()}; + } + } // namespace Bg + + ConstructedSequence Hyperlink(const std::string& text, const std::string& ref) + { + std::ostringstream result; + result << WSL_WINDOWS_VT_OSC "8;;" << ref << WSL_WINDOWS_VT_ESCAPE << "\\" << text << WSL_WINDOWS_VT_OSC << "8;;" + << WSL_WINDOWS_VT_ESCAPE << "\\"; + return ConstructedSequence{std::move(result).str()}; + } +} // namespace Format + +namespace Erase { + const Sequence LineForward{WSL_WINDOWS_VT_CSI "K"}; + const Sequence LineBackward{WSL_WINDOWS_VT_CSI "1K"}; + const Sequence LineEntirely{WSL_WINDOWS_VT_CSI "2K"}; + const Sequence ScreenForward{WSL_WINDOWS_VT_CSI "J"}; + const Sequence ScreenBackward{WSL_WINDOWS_VT_CSI "1J"}; + const Sequence ScreenEntirely{WSL_WINDOWS_VT_CSI "2J"}; +} // namespace Erase + +namespace Progress { + ConstructedSequence Construct(State state, std::optional percentage) + { + // See https://conemu.github.io/en/AnsiEscapeCodes.html#ConEmu_specific_OSC + + THROW_HR_IF(E_BOUNDS, percentage.has_value() && percentage > 100u); + + // Workaround some quirks in the Windows Terminal implementation of the progress OSC sequence + switch (state) + { + case State::None: + case State::Indeterminate: + // Windows Terminal does not recognize the OSC sequence if the progress value is left out. + // As a workaround, we can specify an arbitrary value since it does not matter for None and Indeterminate states. + percentage = percentage.value_or(0); + break; + case State::Normal: + case State::Error: + case State::Paused: + // Windows Terminal does not support switching progress states without also setting a progress value at the same time, + // so we disallow this case for now. + THROW_HR_IF(E_INVALIDARG, !percentage.has_value()); + break; + } + + int stateId; + switch (state) + { + case State::None: + stateId = 0; + break; + case State::Indeterminate: + stateId = 3; + break; + case State::Normal: + stateId = 1; + break; + case State::Error: + stateId = 2; + break; + case State::Paused: + stateId = 4; + break; + default: + THROW_HR(E_UNEXPECTED); + } + + std::ostringstream result; + result << WSL_WINDOWS_VT_OSC "9;4;" << stateId << ";"; + if (percentage.has_value()) + { + result << percentage.value(); + } + result << WSL_WINDOWS_VT_ESCAPE << "\\"; + return ConstructedSequence{std::move(result).str()}; + } +} // namespace Progress +} // namespace wsl::windows::common::vt diff --git a/src/windows/common/VTSupport.h b/src/windows/common/VTSupport.h new file mode 100644 index 000000000..895e97635 --- /dev/null +++ b/src/windows/common/VTSupport.h @@ -0,0 +1,592 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + VTSupport.h + +Abstract: + + This file contains VT (Virtual Terminal) sequence constants, construction + helpers, and console mode RAII wrappers for use in Windows WSL components. + +--*/ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include "wslutil.h" + +namespace wsl::windows::common::vt { + +// Get the integral value for an enum. +template +constexpr inline std::enable_if_t, std::underlying_type_t> ToIntegral(E e) +{ + return static_cast>(e); +} + +// Get the enum value for an integral. +template +constexpr inline std::enable_if_t, E> ToEnum(std::underlying_type_t ut) +{ + return static_cast(ut); +} + +// RAII helper that changes cursor visibility on a console handle and restores +// the original cursor info on destruction. No-op if the handle is not a console. +class ChangeTerminalMode +{ +public: + NON_COPYABLE(ChangeTerminalMode); + NON_MOVABLE(ChangeTerminalMode); + + ChangeTerminalMode(HANDLE console, bool cursorVisible) : m_console(console) + { + if (!wsl::windows::common::wslutil::IsConsoleHandle(console)) + { + m_console = nullptr; + return; + } + + THROW_IF_WIN32_BOOL_FALSE(GetConsoleCursorInfo(console, &m_originalCursorInfo)); + CONSOLE_CURSOR_INFO newCursorInfo = m_originalCursorInfo; + newCursorInfo.bVisible = cursorVisible; + THROW_IF_WIN32_BOOL_FALSE(SetConsoleCursorInfo(console, &newCursorInfo)); + } + + ~ChangeTerminalMode() + { + if (m_console) + { + LOG_IF_WIN32_BOOL_FALSE(SetConsoleCursorInfo(m_console, &m_originalCursorInfo)); + } + } + + bool IsConsole() const + { + return m_console != nullptr; + } + +private: + HANDLE m_console{}; + CONSOLE_CURSOR_INFO m_originalCursorInfo{}; +}; + +// RAII helper that enables VT processing on a console handle and restores the +// original mode on destruction. No-op if the handle is not a console. +// +// Output mode (STD_OUTPUT_HANDLE): sets ENABLE_VIRTUAL_TERMINAL_PROCESSING, +// optionally DISABLE_NEWLINE_AUTO_RETURN (best-effort, falls back without it). +// +// Input mode (STD_INPUT_HANDLE): sets ENABLE_VIRTUAL_TERMINAL_INPUT and +// ENABLE_EXTENDED_FLAGS, clears ENABLE_LINE_INPUT and ENABLE_ECHO_INPUT. +class EnableVirtualTerminal +{ +public: + NON_COPYABLE(EnableVirtualTerminal); + NON_MOVABLE(EnableVirtualTerminal); + + enum class Mode + { + Output, + Input, + }; + + explicit EnableVirtualTerminal(HANDLE console, Mode mode = Mode::Output, bool disableNewlineAutoReturn = false) + { + DWORD current; + if (!GetConsoleMode(console, ¤t)) + { + LOG_LAST_ERROR_IF(GetLastError() != ERROR_INVALID_HANDLE); + return; + } + + if (mode == Mode::Input) + { + const DWORD newMode = (current & ~(ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT)) | ENABLE_EXTENDED_FLAGS | ENABLE_VIRTUAL_TERMINAL_INPUT; + if (SetConsoleMode(console, newMode)) + { + m_console = console; + m_originalMode = current; + } + else + { + LOG_LAST_ERROR_IF(GetLastError() != ERROR_INVALID_PARAMETER); + } + } + else + { + // Attempts to apply the given extra flags on top of the current mode. + // Returns true on success (including when the flags are already set), + // false if SetConsoleMode rejected them. + auto tryEnable = [&](DWORD flags) -> bool { + const DWORD newMode = current | flags; + if (newMode == current) + { + // The requested flags are already active; report success without + // calling SetConsoleMode. The destructor will restore current to + // itself (a no-op) which is correct and harmless. + m_console = console; + m_originalMode = current; + return true; + } + + if (SetConsoleMode(console, newMode)) + { + m_console = console; + m_originalMode = current; + return true; + } + + LOG_LAST_ERROR_IF(GetLastError() != ERROR_INVALID_PARAMETER); + return false; + }; + + // When DISABLE_NEWLINE_AUTO_RETURN is requested, try it first and fall + // back to plain ENABLE_VIRTUAL_TERMINAL_PROCESSING if the flag is + // unsupported. When it is not requested, only one attempt is needed. + if (disableNewlineAutoReturn && tryEnable(ENABLE_VIRTUAL_TERMINAL_PROCESSING | DISABLE_NEWLINE_AUTO_RETURN)) + { + return; + } + + tryEnable(ENABLE_VIRTUAL_TERMINAL_PROCESSING); + } + } + + ~EnableVirtualTerminal() + { + if (m_console) + { + LOG_IF_WIN32_BOOL_FALSE(SetConsoleMode(m_console, m_originalMode)); + } + } + + bool IsVTEnabled() const + { + return m_console != nullptr; + } + +private: + HANDLE m_console = nullptr; + DWORD m_originalMode = 0; +}; + +// VT escape sequences are pure ASCII byte sequences (0x00-0x7F), so all sequences +// are stored and manipulated as narrow strings (std::string / std::string_view). +// Widening to std::wstring happens only at the stream output boundary; see +// operator<<(std::wostream&, const Sequence&) below. + +// The base for all VT sequences. +struct Sequence +{ + constexpr Sequence() = default; + explicit constexpr Sequence(std::string_view c) : m_chars(c) + { + } + + // Prevent construction from a std::string (lvalue or rvalue): std::string is + // implicitly convertible to std::string_view, so without this guard + // Sequence(someString) would compile but leave m_chars dangling once the string + // is destroyed. Use ConstructedSequence for runtime / owned sequences. + // A constrained template (rather than named overloads) avoids making char[] + // literals ambiguous between the deleted and string_view constructors. + template + requires std::is_same_v, std::string> + explicit Sequence(T&&) = delete; + + std::string_view Get() const + { + return m_chars; + } + + // Returns true if this is a color or formatting sequence (SGR or OSC 8 hyperlink) + // that should be suppressed when --no-color is set. + bool IsColor() const; + +protected: + void Set(const std::string& s) + { + m_chars = s; + } + +private: + std::string_view m_chars; +}; + +// A VT sequence that is constructed at runtime. +struct ConstructedSequence : public Sequence +{ + ConstructedSequence() + { + Set(m_str); + } + + explicit ConstructedSequence(std::string s) : m_str(std::move(s)) + { + Set(m_str); + } + + ConstructedSequence(const ConstructedSequence& other) : m_str(other.m_str) + { + Set(m_str); + } + + ConstructedSequence& operator=(const ConstructedSequence& other) + { + m_str = other.m_str; + Set(m_str); + return *this; + } + + ConstructedSequence(ConstructedSequence&& other) noexcept : m_str(std::move(other.m_str)) + { + Set(m_str); + other.Set(other.m_str); + } + + ConstructedSequence& operator=(ConstructedSequence&& other) noexcept + { + m_str = std::move(other.m_str); + Set(m_str); + other.Set(other.m_str); + return *this; + } + + void Append(const Sequence& sequence); + + void Clear(); + +private: + std::string m_str; +}; + +// Constructs a single SGR (Select Graphic Rendition) sequence with one or more +// semicolon-separated parameters. e.g. Sgr({1, 31}) produces "\x1b[1;31m". +// Prefer named constants in the Format namespace for single-parameter sequences; +// use this only when a multi-parameter form is required to match specific terminal +// output exactly (e.g. a shell PS1 that emits combined bold+color in one sequence). +ConstructedSequence Sgr(std::initializer_list params); + +// Below are mapped to the sequences described here: +// https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences + +// Contains the response to a DA1 (Primary Device Attributes) request. +struct PrimaryDeviceAttributes +{ + // Queries the device attributes on creation. + // Both streams must be opened in _O_U8TEXT mode (or equivalent wide mode). + // outStream receives the DA1 request; inStream provides the terminal's response. + // All DA1 sequence bytes are pure ASCII so widening/narrowing is lossless. + PrimaryDeviceAttributes(std::wostream& outStream, std::wistream& inStream); + + // The extensions that a device may support. + enum class Extension + { + Columns132 = 1, + PrinterPort = 2, + Sixel = 4, + SelectiveErase = 6, + SoftCharacterSet = 7, + UserDefinedKeys = 8, + NationalReplacementCharacterSets = 9, + SoftCharacterSet2 = 12, + EightBitInterface = 14, + TechnicalCharacterSet = 15, + WindowingCapability = 18, + HorizontalScrolling = 21, + ColorText = 22, + Greek = 23, + Turkish = 24, + RectangularAreaOperations = 28, + TextMacros = 32, + ISO_Latin2CharacterSet = 42, + PC_Term = 44, + SoftKeyMap = 45, + ASCII_Emulation = 46, + }; + + // Determines if the given extension is supported. + bool Supports(Extension extension) const; + +private: + uint32_t m_conformanceLevel = 0; + uint64_t m_extensions = 0; +}; + +// Cursor movement, visibility, and input mode sequences. +namespace Cursor { + // Move cursor N cells in the given direction. + ConstructedSequence Up(int cells); + ConstructedSequence Down(int cells); + ConstructedSequence Forward(int cells); + ConstructedSequence Backward(int cells); + + // Move cursor to an absolute position (1-based row and column). + ConstructedSequence MoveTo(int row, int col); + + // Move cursor to the top-left corner of the screen. + extern const Sequence Home; + + // Cursor visibility. + extern const Sequence EnableBlink; + extern const Sequence DisableBlink; + extern const Sequence Show; + extern const Sequence Hide; + + // Bracketed paste mode causes the terminal to wrap pasted text in escape sequences + // so the application can distinguish typed input from pasted input. + // See https://cirw.in/blog/bracketed-paste + extern const Sequence BracketedPasteOn; + extern const Sequence BracketedPasteOff; +} // namespace Cursor + +// Text formatting (color, weight, style) sequences. +namespace Format { + extern const Sequence Default; + extern const Sequence Negative; + + // Intensity attributes. Normal cancels both Bright and Dim (SGR 22). + extern const Sequence Bright; + extern const Sequence Dim; + extern const Sequence Normal; + + extern const Sequence Italic; + extern const Sequence NoItalic; + extern const Sequence Underline; + extern const Sequence NoUnderline; + + // A color, used in constructed sequences. + struct Color + { + uint8_t R; + uint8_t G; + uint8_t B; + }; + + namespace Fg { + // Standard foreground colors using SGR 30-37. + extern const Sequence Black; + extern const Sequence Red; + extern const Sequence Green; + extern const Sequence Yellow; + extern const Sequence Blue; + extern const Sequence Magenta; + extern const Sequence Cyan; + extern const Sequence White; + + // High-intensity ("bright") foreground colors using SGR 90-97. + // These are distinct from SGR 1;3x (bold + standard color), which is + // a different byte sequence even though terminals often render them identically. + extern const Sequence BrightBlack; // Typically rendered as dark gray. + extern const Sequence BrightRed; + extern const Sequence BrightGreen; + extern const Sequence BrightYellow; + extern const Sequence BrightBlue; + extern const Sequence BrightMagenta; + extern const Sequence BrightCyan; + extern const Sequence BrightWhite; + + ConstructedSequence Extended(const Color& color); + } // namespace Fg + + namespace Bg { + // Standard background colors using SGR 40-47. + extern const Sequence Black; + extern const Sequence Red; + extern const Sequence Green; + extern const Sequence Yellow; + extern const Sequence Blue; + extern const Sequence Magenta; + extern const Sequence Cyan; + extern const Sequence White; + + // High-intensity ("bright") background colors using SGR 100-107. + extern const Sequence BrightBlack; // Typically rendered as dark gray. + extern const Sequence BrightRed; + extern const Sequence BrightGreen; + extern const Sequence BrightYellow; + extern const Sequence BrightBlue; + extern const Sequence BrightMagenta; + extern const Sequence BrightCyan; + extern const Sequence BrightWhite; + + ConstructedSequence Extended(const Color& color); + } // namespace Bg + + ConstructedSequence Hyperlink(const std::string& text, const std::string& ref); +} // namespace Format + +// Line and screen erasure sequences. +namespace Erase { + extern const Sequence LineForward; + extern const Sequence LineBackward; + extern const Sequence LineEntirely; + extern const Sequence ScreenForward; + extern const Sequence ScreenBackward; + extern const Sequence ScreenEntirely; +} // namespace Erase + +namespace Progress { + enum class State + { + None, + Indeterminate, + Normal, + Paused, + Error + }; + + ConstructedSequence Construct(State state, std::optional percentage = std::nullopt); +} // namespace Progress + +// operator<< for stream output. +// Widens the narrow sequence bytes (all ASCII) into a wide string for wostream output. +inline std::wostream& operator<<(std::wostream& o, const Sequence& s) +{ + const auto sv = s.Get(); + return (o << std::wstring(sv.begin(), sv.end())); +} + +inline std::ostream& operator<<(std::ostream& o, const Sequence& s) +{ + return (o << s.Get()); +} + +// operator+ overloads for direct std::string / std::wstring concatenation with sequences. +// These allow sequences to be combined with string literals and std::string without +// manually calling .Get() or wrapping in std::string{...}. + +inline std::string operator+(const Sequence& lhs, const Sequence& rhs) +{ + return std::string{lhs.Get()} + std::string{rhs.Get()}; +} + +inline std::string operator+(const Sequence& lhs, const std::string& rhs) +{ + return std::string{lhs.Get()} + rhs; +} + +inline std::string operator+(const std::string& lhs, const Sequence& rhs) +{ + return lhs + std::string{rhs.Get()}; +} + +inline std::string operator+(const Sequence& lhs, const char* rhs) +{ + return std::string{lhs.Get()} + rhs; +} + +inline std::string operator+(const char* lhs, const Sequence& rhs) +{ + return lhs + std::string{rhs.Get()}; +} + +// Wide string variants — sequences are ASCII so widening is lossless. +inline std::wstring operator+(const Sequence& lhs, const std::wstring& rhs) +{ + const auto sv = lhs.Get(); + return std::wstring(sv.begin(), sv.end()) + rhs; +} + +inline std::wstring operator+(const std::wstring& lhs, const Sequence& rhs) +{ + const auto sv = rhs.Get(); + return lhs + std::wstring(sv.begin(), sv.end()); +} + +// operator== overloads so any Sequence-derived type can be compared directly against +// string literals and std::string_view without calling .Get() at every call site. +// Templated to cover both Sequence and ConstructedSequence without separate overloads. + +template ::value>> +inline bool operator==(const T& lhs, std::string_view rhs) +{ + return lhs.Get() == rhs; +} + +template ::value>> +inline bool operator==(std::string_view lhs, const T& rhs) +{ + return lhs == rhs.Get(); +} + +template ::value>> +inline bool operator==(const T& lhs, const char* rhs) +{ + return lhs.Get() == rhs; +} + +template ::value>> +inline bool operator==(const char* lhs, const T& rhs) +{ + return lhs == rhs.Get(); +} + +// operator+= overloads for in-place wide string appending. +// Appends the ASCII sequence bytes directly into lhs without creating a temporary +// std::wstring, making them more efficient than ToWide() when building a frame buffer. +inline std::wstring& operator+=(std::wstring& lhs, const Sequence& rhs) +{ + const auto sv = rhs.Get(); + lhs.append(sv.begin(), sv.end()); + return lhs; +} + +// Widens a Sequence's ASCII bytes into a std::wstring. +// Use when building a wide string buffer that mixes VT sequences with wide content. +inline std::wstring ToWide(const Sequence& s) +{ + const auto sv = s.Get(); + return std::wstring(sv.begin(), sv.end()); +} + +} // namespace wsl::windows::common::vt + +// std::formatter specializations allowing Sequence to be used directly in std::format. +template <> +struct std::formatter : std::formatter +{ + auto format(const wsl::windows::common::vt::Sequence& s, std::format_context& ctx) const + { + return std::formatter::format(s.Get(), ctx); + } +}; + +template <> +struct std::formatter : std::formatter +{ + auto format(const wsl::windows::common::vt::Sequence& s, std::wformat_context& ctx) const + { + const auto sv = s.Get(); + const std::wstring wide(sv.begin(), sv.end()); + return std::formatter::format(wide, ctx); + } +}; + +template <> +struct std::formatter : std::formatter +{ + auto format(const wsl::windows::common::vt::ConstructedSequence& s, std::format_context& ctx) const + { + return std::formatter::format(s, ctx); + } +}; + +template <> +struct std::formatter : std::formatter +{ + auto format(const wsl::windows::common::vt::ConstructedSequence& s, std::wformat_context& ctx) const + { + return std::formatter::format(s, ctx); + } +}; diff --git a/src/windows/wslc/services/BuildImageCallback.cpp b/src/windows/wslc/services/BuildImageCallback.cpp index 9cd1c639d..83d431738 100644 --- a/src/windows/wslc/services/BuildImageCallback.cpp +++ b/src/windows/wslc/services/BuildImageCallback.cpp @@ -18,15 +18,7 @@ Module Name: namespace wsl::windows::wslc::services { using wsl::windows::common::string::MultiByteToWide; - -namespace { - constexpr std::wstring_view c_escapeMoveCursorUpAndClear = L"\033[{}A\033[J"; - constexpr std::wstring_view c_escapeBrightGreen = L"\033[92m"; - constexpr std::wstring_view c_escapeResetAttributes = L"\033[0m"; - constexpr std::wstring_view c_escapeHideCursorDim = L"\033[?25l\033[2m"; - constexpr std::wstring_view c_escapeClearLineAndNewline = L"\033[K\n"; - constexpr std::wstring_view c_escapeUndimShowCursor = L"\033[22m\033[?25h"; -} // namespace +using namespace wsl::windows::common::vt; BuildImageCallback::~BuildImageCallback() try @@ -67,7 +59,8 @@ void BuildImageCallback::CollapseWindow() { if (m_displayedLines > 0) { - WriteTerminal(std::format(c_escapeMoveCursorUpAndClear, m_displayedLines)); + // Move cursor up to the start of the display area, then erase to end of screen. + WriteTerminal(MultiByteToWide(Cursor::Up(m_displayedLines) + Erase::ScreenForward)); m_displayedLines = 0; } @@ -184,7 +177,7 @@ try const auto newlines = wide.substr(bodyLength); wide.resize(bodyLength); - WriteTerminal(std::format(L"{}{}{}{}", c_escapeBrightGreen, wide, c_escapeResetAttributes, newlines)); + WriteTerminal(std::format(L"{}{}{}{}", Format::Fg::BrightGreen, wide, Format::Default, newlines)); return S_OK; } CATCH_RETURN(); @@ -193,50 +186,52 @@ void BuildImageCallback::Redraw() { CONSOLE_SCREEN_BUFFER_INFO info{}; THROW_IF_WIN32_BOOL_FALSE(GetConsoleScreenBufferInfo(m_console, &info)); - // Use the visible window width (not buffer width), minus one column to avoid the - // deferred-wrap edge case when a line is exactly the window width. Clamp to at - // least zero so the value never goes negative (which would underflow when passed - // to std::wstring::resize). - const SHORT consoleWidth = std::max(0, info.srWindow.Right - info.srWindow.Left); + const int consoleWidth = std::max(0, static_cast(info.srWindow.Right) - info.srWindow.Left); - // Determine how many completed lines to show, leaving room for the pending line and pull progress. const bool showPending = !m_pendingLine.empty(); - const SHORT pullCount = static_cast(m_pullLines.size()); - SHORT completedCount = static_cast(m_lines.size()); - const SHORT reservedLines = (showPending ? 1 : 0) + pullCount; + const int pullCount = static_cast(m_pullLines.size()); + int completedCount = static_cast(m_lines.size()); + const int reservedLines = (showPending ? 1 : 0) + pullCount; if (completedCount + reservedLines > c_maxDisplayLines) { - completedCount = std::max(0, c_maxDisplayLines - reservedLines); + completedCount = std::max(0, c_maxDisplayLines - reservedLines); } - const SHORT displayCount = completedCount + reservedLines; + const int displayCount = completedCount + reservedLines; // Build the entire frame in one buffer to minimize console writes. Hide the cursor // during the redraw so the user doesn't see it bouncing through the cursor movement, // then show it again at the final position. The dim attribute (\033[2m) renders the // scrolling lines de-emphasized regardless of the user's theme. - std::wstring buffer{c_escapeHideCursorDim}; + // + // m_frameBuffer is a member so its backing allocation is reused across frames - + // it grows to the high-water mark and is never freed between redraws. + m_frameBuffer.clear(); + m_frameBuffer += Cursor::Hide; + m_frameBuffer += Format::Dim; // Move cursor to the start of the display area and erase from there to the end of // the screen. \033[J handles the case where the new display is shorter than the // previous one (e.g. when \r clears the pending line without a replacement). if (m_displayedLines > 0) { - buffer += std::format(c_escapeMoveCursorUpAndClear, m_displayedLines); + m_frameBuffer += Cursor::Up(m_displayedLines); + m_frameBuffer += Erase::ScreenForward; } auto appendLine = [&](const std::string& line) { auto wline = MultiByteToWide(line); - if (static_cast(wline.size()) > consoleWidth) + if (wline.size() > static_cast(consoleWidth)) { - wline.resize(consoleWidth); + wline.resize(static_cast(consoleWidth)); } - buffer += wline; - buffer += c_escapeClearLineAndNewline; + m_frameBuffer += std::move(wline); + m_frameBuffer += Erase::LineForward; + m_frameBuffer += L'\n'; }; // Print completed lines (skip older ones if we need room for the pending line). auto it = m_lines.begin(); - if (completedCount < static_cast(m_lines.size())) + if (completedCount < static_cast(m_lines.size())) { std::advance(it, m_lines.size() - completedCount); } @@ -257,9 +252,10 @@ void BuildImageCallback::Redraw() appendLine(line); } - buffer += c_escapeUndimShowCursor; + m_frameBuffer += Format::Normal; + m_frameBuffer += Cursor::Show; - WriteTerminal(buffer); + WriteTerminal(m_frameBuffer); m_displayedLines = displayCount; } diff --git a/src/windows/wslc/services/BuildImageCallback.h b/src/windows/wslc/services/BuildImageCallback.h index 97ef88df3..2067e2525 100644 --- a/src/windows/wslc/services/BuildImageCallback.h +++ b/src/windows/wslc/services/BuildImageCallback.h @@ -12,8 +12,8 @@ Module Name: --*/ #pragma once -#include "ChangeTerminalMode.h" #include "SessionService.h" +#include "VTSupport.h" #include #include @@ -30,7 +30,7 @@ class DECLSPEC_UUID("3EDD5DBF-CA6C-4CF7-923A-AD94B6A732E5") BuildImageCallback HRESULT OnProgress(LPCSTR status, LPCSTR id, ULONGLONG current, ULONGLONG total) override; private: - static constexpr SHORT c_maxDisplayLines = 16; + static constexpr int c_maxDisplayLines = 16; static constexpr auto c_redrawInterval = std::chrono::milliseconds(50); static constexpr size_t c_maxAllLinesBytes = 10 * 1024 * 1024; // 10 MiB cap on retained log output for error replay. @@ -46,7 +46,7 @@ class DECLSPEC_UUID("3EDD5DBF-CA6C-4CF7-923A-AD94B6A732E5") BuildImageCallback const HANDLE m_cancelEvent; HANDLE m_console = GetStdHandle(STD_OUTPUT_HANDLE); bool m_isConsole = wsl::windows::common::wslutil::IsConsoleHandle(m_console); - EnableVirtualTerminal m_vtMode{m_console}; + wsl::windows::common::vt::EnableVirtualTerminal m_vtMode{m_console}; std::deque m_lines; // Each entry already contains the trailing newline so the bytes match what's replayed. // TODO: Track logs per step so the destructor can replay only the failing step's @@ -54,10 +54,13 @@ class DECLSPEC_UUID("3EDD5DBF-CA6C-4CF7-923A-AD94B6A732E5") BuildImageCallback std::deque m_allLines; size_t m_allLinesBytes = 0; std::string m_pendingLine; - SHORT m_displayedLines = 0; + int m_displayedLines = 0; std::chrono::steady_clock::time_point m_lastRedraw{}; // Per-entry pull progress lines, keyed by entry id. Updated in place by Redraw. std::map so order is consistent. std::map m_pullLines; + // Reused across Redraw() calls so the backing allocation grows to the high-water + // mark and is then reused rather than re-allocated every frame. + std::wstring m_frameBuffer; // Captured at construction so the destructor can detect destruction during exception unwinding. int m_uncaughtExceptions = std::uncaught_exceptions(); }; diff --git a/src/windows/wslc/services/ChangeTerminalMode.h b/src/windows/wslc/services/ChangeTerminalMode.h deleted file mode 100644 index 1d55a4da8..000000000 --- a/src/windows/wslc/services/ChangeTerminalMode.h +++ /dev/null @@ -1,92 +0,0 @@ -/*++ - -Copyright (c) Microsoft. All rights reserved. - -Module Name: - - ChangeTerminalMode.h - -Abstract: - - This file contains the ChangeTerminalMode definition. - ---*/ -#pragma once - -namespace wsl::windows::wslc::services { - -class ChangeTerminalMode -{ -public: - NON_COPYABLE(ChangeTerminalMode); - NON_MOVABLE(ChangeTerminalMode); - - ChangeTerminalMode(HANDLE console, bool cursorVisible) : m_console(console) - { - if (!wsl::windows::common::wslutil::IsConsoleHandle(console)) - { - m_console = nullptr; - return; - } - - THROW_IF_WIN32_BOOL_FALSE(GetConsoleCursorInfo(console, &m_originalCursorInfo)); - CONSOLE_CURSOR_INFO newCursorInfo = m_originalCursorInfo; - newCursorInfo.bVisible = cursorVisible; - THROW_IF_WIN32_BOOL_FALSE(SetConsoleCursorInfo(console, &newCursorInfo)); - } - - ~ChangeTerminalMode() - { - if (m_console) - { - LOG_IF_WIN32_BOOL_FALSE(SetConsoleCursorInfo(m_console, &m_originalCursorInfo)); - } - } - - bool IsConsole() const - { - return m_console != nullptr; - } - -private: - HANDLE m_console{}; - CONSOLE_CURSOR_INFO m_originalCursorInfo{}; -}; - -// RAII helper that enables ENABLE_VIRTUAL_TERMINAL_PROCESSING on a console handle and -// restores the original mode on destruction. No-op if the handle isn't a console or -// VT processing is already enabled. -class EnableVirtualTerminal -{ -public: - NON_COPYABLE(EnableVirtualTerminal); - NON_MOVABLE(EnableVirtualTerminal); - - explicit EnableVirtualTerminal(HANDLE console) - { - DWORD mode; - if (GetConsoleMode(console, &mode)) - { - const DWORD newMode = mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING; - if (newMode != mode && SetConsoleMode(console, newMode)) - { - m_console = console; - m_originalMode = mode; - } - } - } - - ~EnableVirtualTerminal() - { - if (m_console) - { - LOG_IF_WIN32_BOOL_FALSE(SetConsoleMode(m_console, m_originalMode)); - } - } - -private: - HANDLE m_console = nullptr; - DWORD m_originalMode = 0; -}; - -} // namespace wsl::windows::wslc::services diff --git a/src/windows/wslc/services/ImageProgressCallback.cpp b/src/windows/wslc/services/ImageProgressCallback.cpp index 25172988f..894324834 100644 --- a/src/windows/wslc/services/ImageProgressCallback.cpp +++ b/src/windows/wslc/services/ImageProgressCallback.cpp @@ -19,18 +19,25 @@ Module Name: namespace wsl::windows::wslc::services { using namespace wsl::shared; +using namespace wsl::windows::common::vt; -auto ImageProgressCallback::MoveToLine(SHORT line) +void ImageProgressCallback::WriteTerminal(std::wstring_view content) const +{ + DWORD written; + LOG_IF_WIN32_BOOL_FALSE(WriteConsoleW(m_console, content.data(), static_cast(content.size()), &written, nullptr)); +} + +auto ImageProgressCallback::MoveToLine(int line) { if (line > 0) { - wprintf(L"\033[%iA", line); + WriteTerminal(ToWide(Cursor::Up(line))); } - return wil::scope_exit([line = line]() { + return wil::scope_exit([line = line, this]() { if (line > 1) { - wprintf(L"\033[%iB", line - 1); + WriteTerminal(ToWide(Cursor::Down(line - 1))); } }); } @@ -46,7 +53,7 @@ HRESULT ImageProgressCallback::OnProgress(LPCSTR status, LPCSTR id, ULONGLONG cu if (id == nullptr || *id == '\0') // Print all 'global' statuses on their own line { - wprintf(L"%hs\n", status); + WriteTerminal(std::format(L"{}\n", status)); m_currentLine++; return S_OK; } @@ -58,13 +65,13 @@ HRESULT ImageProgressCallback::OnProgress(LPCSTR status, LPCSTR id, ULONGLONG cu { // If this is the first time we see this ID, create a new line for it. m_statuses.emplace(id, m_currentLine); - wprintf(L"%ls\n", GenerateStatusLine(status, id, current, total, info).c_str()); + WriteTerminal(GenerateStatusLine(status, id, current, total, info) + L'\n'); m_currentLine++; } else { auto revert = MoveToLine(m_currentLine - it->second); - wprintf(L"%ls\n", GenerateStatusLine(status, id, current, total, info).c_str()); + WriteTerminal(GenerateStatusLine(status, id, current, total, info) + L'\n'); } return S_OK; @@ -126,7 +133,7 @@ std::wstring ImageProgressCallback::GenerateStatusLine(LPCSTR status, LPCSTR id, } // Use the visible window width (not the buffer width) to prevent wrapping. - const auto visibleWidth = std::max(0, info.srWindow.Right - info.srWindow.Left + 1); + const auto visibleWidth = std::max(0, static_cast(info.srWindow.Right) - info.srWindow.Left + 1); // Truncate to console width to prevent wrapping that would break cursor repositioning. if (line.size() > static_cast(visibleWidth)) diff --git a/src/windows/wslc/services/ImageProgressCallback.h b/src/windows/wslc/services/ImageProgressCallback.h index e91aef2b7..f4bcc7b42 100644 --- a/src/windows/wslc/services/ImageProgressCallback.h +++ b/src/windows/wslc/services/ImageProgressCallback.h @@ -12,8 +12,10 @@ Module Name: --*/ #pragma once -#include "ChangeTerminalMode.h" #include "SessionService.h" +#include "VTSupport.h" +#include +#include namespace wsl::windows::wslc::services { @@ -22,15 +24,17 @@ class DECLSPEC_UUID("7A1D3376-835A-471A-8DC9-23653D9962D0") ImageProgressCallbac : public Microsoft::WRL::RuntimeClass, IProgressCallback, IFastRundown> { public: - auto MoveToLine(SHORT line); HRESULT OnProgress(LPCSTR status, LPCSTR id, ULONGLONG current, ULONGLONG total) override; private: + auto MoveToLine(int line); static CONSOLE_SCREEN_BUFFER_INFO Info(); + void WriteTerminal(std::wstring_view content) const; std::wstring GenerateStatusLine(LPCSTR status, LPCSTR id, ULONGLONG current, ULONGLONG total, const CONSOLE_SCREEN_BUFFER_INFO& info); - std::map m_statuses; - SHORT m_currentLine = 0; - EnableVirtualTerminal m_vtMode{GetStdHandle(STD_OUTPUT_HANDLE)}; - ChangeTerminalMode m_terminalMode{GetStdHandle(STD_OUTPUT_HANDLE), false}; + std::map m_statuses; + int m_currentLine = 0; + HANDLE m_console = GetStdHandle(STD_OUTPUT_HANDLE); + wsl::windows::common::vt::EnableVirtualTerminal m_vtMode{m_console}; + wsl::windows::common::vt::ChangeTerminalMode m_terminalMode{m_console, false}; }; -} // namespace wsl::windows::wslc::services \ No newline at end of file +} // namespace wsl::windows::wslc::services diff --git a/test/windows/wslc/WSLCCLIVTSupportUnitTests.cpp b/test/windows/wslc/WSLCCLIVTSupportUnitTests.cpp new file mode 100644 index 000000000..94ccaf7b5 --- /dev/null +++ b/test/windows/wslc/WSLCCLIVTSupportUnitTests.cpp @@ -0,0 +1,473 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + WSLCCLIVTSupportUnitTests.cpp + +Abstract: + + This file contains unit tests for VT sequence construction and console mode helpers. + +--*/ + +#include "precomp.h" +#include "windows/Common.h" +#include "VTSupport.h" + +using namespace WEX::Logging; +using namespace WEX::Common; +using namespace WEX::TestExecution; +using namespace wsl::windows::common::vt; + +namespace WSLCCLIVTSupportUnitTests { + +// Creates a real console screen buffer that can be used as an output handle for console API tests. +// The buffer is not attached to the visible console window, so it does not affect the test runner output. +static wil::unique_hfile MakeScreenBuffer() +{ + wil::unique_hfile handle{CreateConsoleScreenBuffer( + GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, CONSOLE_TEXTMODE_BUFFER, nullptr)}; + THROW_LAST_ERROR_IF(!handle); + return handle; +} + +class WSLCCLIVTSupportUnitTests +{ + WSLC_TEST_CLASS(WSLCCLIVTSupportUnitTests) + + TEST_METHOD(VT_Sequence) + { + // Constant sequence round-trips correctly. + const Sequence constant{"\x1b[0m"}; + VERIFY_ARE_EQUAL("\x1b[0m", constant); + + // Default-constructed ConstructedSequence is empty. + ConstructedSequence empty; + VERIFY_IS_TRUE(empty.Get().empty()); + + // Construct from string. + ConstructedSequence seq{"\x1b[1m"}; + VERIFY_ARE_EQUAL("\x1b[1m", seq); + + // Append combines sequences. + seq.Append(Sequence{"\x1b[91m"}); + VERIFY_ARE_EQUAL("\x1b[1m\x1b[91m", seq); + + // Appending an empty sequence is a no-op. + seq.Append(Sequence{}); + VERIFY_ARE_EQUAL("\x1b[1m\x1b[91m", seq); + + // Clear resets to empty. + seq.Clear(); + VERIFY_IS_TRUE(seq.Get().empty()); + + // Copy construction. + ConstructedSequence original{"\x1b[1m"}; + ConstructedSequence copy{original}; + VERIFY_ARE_EQUAL(original.Get(), copy.Get()); + + // Move construction. + ConstructedSequence moved{std::move(original)}; + VERIFY_ARE_EQUAL("\x1b[1m", moved); + + // Copy assignment. + ConstructedSequence assigned; + assigned = copy; + VERIFY_ARE_EQUAL(copy.Get(), assigned.Get()); + + // Move assignment. + ConstructedSequence moveAssigned; + moveAssigned = std::move(moved); + VERIFY_ARE_EQUAL("\x1b[1m", moveAssigned); + + // SGR Construction. + VERIFY_ARE_EQUAL("\x1b[1;31m", Sgr({1, 31})); + VERIFY_ARE_EQUAL("\x1b[0m", Sgr({0})); + } + + TEST_METHOD(VT_CursorSequences) + { + VERIFY_ARE_EQUAL("\x1b[3A", Cursor::Up(3)); + VERIFY_ARE_EQUAL("\x1b[5B", Cursor::Down(5)); + VERIFY_ARE_EQUAL("\x1b[2C", Cursor::Forward(2)); + VERIFY_ARE_EQUAL("\x1b[1D", Cursor::Backward(1)); + VERIFY_ARE_EQUAL("\x1b[5;10H", Cursor::MoveTo(5, 10)); + VERIFY_ARE_EQUAL("\x1b[H", Cursor::Home); + + VERIFY_THROWS_SPECIFIC( + Cursor::Up(-1), wil::ResultException, [](const wil::ResultException& e) { return e.GetErrorCode() == E_INVALIDARG; }); + VERIFY_THROWS_SPECIFIC(Cursor::MoveTo(0, 1), wil::ResultException, [](const wil::ResultException& e) { + return e.GetErrorCode() == E_INVALIDARG; + }); + VERIFY_THROWS_SPECIFIC(Cursor::MoveTo(1, 0), wil::ResultException, [](const wil::ResultException& e) { + return e.GetErrorCode() == E_INVALIDARG; + }); + + VERIFY_ARE_EQUAL("\x1b[?2004h", Cursor::BracketedPasteOn); + VERIFY_ARE_EQUAL("\x1b[?2004l", Cursor::BracketedPasteOff); + } + + TEST_METHOD(VT_TextFormatSequences) + { + VERIFY_ARE_EQUAL("\x1b[0m", Format::Default); + VERIFY_ARE_EQUAL("\x1b[7m", Format::Negative); + VERIFY_ARE_EQUAL("\x1b[1m", Format::Bright); + VERIFY_ARE_EQUAL("\x1b[2m", Format::Dim); + VERIFY_ARE_EQUAL("\x1b[22m", Format::Normal); + VERIFY_ARE_EQUAL("\x1b[3m", Format::Italic); + VERIFY_ARE_EQUAL("\x1b[23m", Format::NoItalic); + VERIFY_ARE_EQUAL("\x1b[4m", Format::Underline); + VERIFY_ARE_EQUAL("\x1b[24m", Format::NoUnderline); + + VERIFY_ARE_EQUAL("\x1b[30m", Format::Fg::Black); + VERIFY_ARE_EQUAL("\x1b[31m", Format::Fg::Red); + VERIFY_ARE_EQUAL("\x1b[32m", Format::Fg::Green); + VERIFY_ARE_EQUAL("\x1b[33m", Format::Fg::Yellow); + VERIFY_ARE_EQUAL("\x1b[34m", Format::Fg::Blue); + VERIFY_ARE_EQUAL("\x1b[35m", Format::Fg::Magenta); + VERIFY_ARE_EQUAL("\x1b[36m", Format::Fg::Cyan); + VERIFY_ARE_EQUAL("\x1b[37m", Format::Fg::White); + + VERIFY_ARE_EQUAL("\x1b[90m", Format::Fg::BrightBlack); + VERIFY_ARE_EQUAL("\x1b[91m", Format::Fg::BrightRed); + VERIFY_ARE_EQUAL("\x1b[92m", Format::Fg::BrightGreen); + VERIFY_ARE_EQUAL("\x1b[93m", Format::Fg::BrightYellow); + VERIFY_ARE_EQUAL("\x1b[94m", Format::Fg::BrightBlue); + VERIFY_ARE_EQUAL("\x1b[95m", Format::Fg::BrightMagenta); + VERIFY_ARE_EQUAL("\x1b[96m", Format::Fg::BrightCyan); + VERIFY_ARE_EQUAL("\x1b[97m", Format::Fg::BrightWhite); + + VERIFY_ARE_EQUAL("\x1b[38;2;255;128;0m", Format::Fg::Extended(Format::Color{255, 128, 0})); + + VERIFY_ARE_EQUAL("\x1b[40m", Format::Bg::Black); + VERIFY_ARE_EQUAL("\x1b[41m", Format::Bg::Red); + VERIFY_ARE_EQUAL("\x1b[42m", Format::Bg::Green); + VERIFY_ARE_EQUAL("\x1b[43m", Format::Bg::Yellow); + VERIFY_ARE_EQUAL("\x1b[44m", Format::Bg::Blue); + VERIFY_ARE_EQUAL("\x1b[45m", Format::Bg::Magenta); + VERIFY_ARE_EQUAL("\x1b[46m", Format::Bg::Cyan); + VERIFY_ARE_EQUAL("\x1b[47m", Format::Bg::White); + + VERIFY_ARE_EQUAL("\x1b[100m", Format::Bg::BrightBlack); + VERIFY_ARE_EQUAL("\x1b[101m", Format::Bg::BrightRed); + VERIFY_ARE_EQUAL("\x1b[102m", Format::Bg::BrightGreen); + VERIFY_ARE_EQUAL("\x1b[103m", Format::Bg::BrightYellow); + VERIFY_ARE_EQUAL("\x1b[104m", Format::Bg::BrightBlue); + VERIFY_ARE_EQUAL("\x1b[105m", Format::Bg::BrightMagenta); + VERIFY_ARE_EQUAL("\x1b[106m", Format::Bg::BrightCyan); + VERIFY_ARE_EQUAL("\x1b[107m", Format::Bg::BrightWhite); + + VERIFY_ARE_EQUAL("\x1b[48;2;0;64;192m", Format::Bg::Extended(Format::Color{0, 64, 192})); + + VERIFY_ARE_EQUAL( + "\x1b]8;;https://example.com\x1b\\Click here\x1b]8;;\x1b\\", Format::Hyperlink("Click here", "https://example.com")); + } + + TEST_METHOD(VT_EraseSequences) + { + VERIFY_ARE_EQUAL("\x1b[K", Erase::LineForward); + VERIFY_ARE_EQUAL("\x1b[1K", Erase::LineBackward); + VERIFY_ARE_EQUAL("\x1b[2K", Erase::LineEntirely); + VERIFY_ARE_EQUAL("\x1b[J", Erase::ScreenForward); + VERIFY_ARE_EQUAL("\x1b[1J", Erase::ScreenBackward); + VERIFY_ARE_EQUAL("\x1b[2J", Erase::ScreenEntirely); + } + + TEST_METHOD(VT_ProgressSequences) + { + VERIFY_ARE_EQUAL("\x1b]9;4;0;0\x1b\\", Progress::Construct(Progress::State::None)); + VERIFY_ARE_EQUAL("\x1b]9;4;3;0\x1b\\", Progress::Construct(Progress::State::Indeterminate)); + VERIFY_ARE_EQUAL("\x1b]9;4;1;50\x1b\\", Progress::Construct(Progress::State::Normal, 50u)); + VERIFY_ARE_EQUAL("\x1b]9;4;2;75\x1b\\", Progress::Construct(Progress::State::Error, 75u)); + VERIFY_ARE_EQUAL("\x1b]9;4;4;25\x1b\\", Progress::Construct(Progress::State::Paused, 25u)); + + VERIFY_THROWS_SPECIFIC(Progress::Construct(Progress::State::Normal), wil::ResultException, [](const wil::ResultException& e) { + return e.GetErrorCode() == E_INVALIDARG; + }); + VERIFY_THROWS_SPECIFIC(Progress::Construct(Progress::State::Normal, 101u), wil::ResultException, [](const wil::ResultException& e) { + return e.GetErrorCode() == E_BOUNDS; + }); + } + + TEST_METHOD(VT_IsColor) + { + // Named SGR sequences are color. + VERIFY_IS_TRUE(Format::Bright.IsColor()); + VERIFY_IS_TRUE(Format::Dim.IsColor()); + VERIFY_IS_TRUE(Format::Fg::BrightRed.IsColor()); + VERIFY_IS_TRUE(Format::Default.IsColor()); + + // Constructed multi-param SGR is color. + VERIFY_IS_TRUE(Sgr({1, 31}).IsColor()); + + // OSC 8 hyperlink is color-adjacent. + VERIFY_IS_TRUE(Format::Hyperlink("text", "https://example.com").IsColor()); + + // Cursor movement is structural — not color. + VERIFY_IS_FALSE(Cursor::Up(1).IsColor()); + VERIFY_IS_FALSE(Cursor::Home.IsColor()); + + // Erase is structural — not color. + VERIFY_IS_FALSE(Erase::LineForward.IsColor()); + VERIFY_IS_FALSE(Erase::ScreenForward.IsColor()); + + // Progress is structural — not color. + VERIFY_IS_FALSE(Progress::Construct(Progress::State::Normal, 50u).IsColor()); + } + + TEST_METHOD(VT_StringConcatenation) + { + // Sequence + Sequence + VERIFY_ARE_EQUAL("\x1b[1m\x1b[0m", Format::Bright + Format::Default); + + // Sequence + string literal + VERIFY_ARE_EQUAL("\x1b[1mhello", Format::Bright + "hello"); + + // string literal + Sequence + VERIFY_ARE_EQUAL("hello\x1b[0m", "hello" + Format::Default); + + // Sequence + std::string + VERIFY_ARE_EQUAL("\x1b[1mhello", Format::Bright + std::string{"hello"}); + + // std::string + Sequence + VERIFY_ARE_EQUAL("world\x1b[0m", std::string{"world"} + Format::Default); + + // Chained: Sequence + Sequence + literal — verifies operator+ associativity. + VERIFY_ARE_EQUAL("\x1b[?2004h\x1b[91mroot@ ", Cursor::BracketedPasteOn + Format::Fg::BrightRed + "root@ "); + + // Sequence + std::wstring + VERIFY_ARE_EQUAL(std::wstring{L"\x1b[0mworld"}, Format::Default + std::wstring{L"world"}); + + // std::wstring + Sequence + VERIFY_ARE_EQUAL(std::wstring{L"world\x1b[0m"}, std::wstring{L"world"} + Format::Default); + } + + TEST_METHOD(VT_StreamOperators) + { + const Sequence seq{"\x1b[1m"}; + + // Narrow stream writes bytes as-is. + std::ostringstream oss; + oss << seq; + VERIFY_ARE_EQUAL(std::string{"\x1b[1m"}, oss.str()); + + // Multiple sequences can be streamed in one expression. + std::ostringstream multi; + multi << Format::Bright << "text" << Format::Default; + VERIFY_ARE_EQUAL(std::string{"\x1b[1mtext\x1b[0m"}, multi.str()); + + // Wide stream correctly widens the ASCII sequence bytes. + std::wostringstream woss; + woss << seq; + VERIFY_ARE_EQUAL(std::wstring{L"\x1b[1m"}, woss.str()); + + // std::format — narrow. + VERIFY_ARE_EQUAL(std::string{"\x1b[92mhello\x1b[0m"}, std::format("{}{}{}", Format::Fg::BrightGreen, "hello", Format::Default)); + + // std::format — wide, sequence bytes widened losslessly. + VERIFY_ARE_EQUAL(std::wstring{L"\x1b[92mhello\x1b[0m"}, std::format(L"{}{}{}", Format::Fg::BrightGreen, L"hello", Format::Default)); + + // std::format — ConstructedSequence via Sequence base. + VERIFY_ARE_EQUAL(std::string{"\x1b[3A text"}, std::format("{} text", Cursor::Up(3))); + + // std::format — Sgr multi-parameter sequence. + VERIFY_ARE_EQUAL(std::string{"\x1b[1;31mtext\x1b[0m"}, std::format("{}text{}", Sgr({1, 31}), Format::Default)); + } + + TEST_METHOD(VT_ChangeTerminalMode) + { + auto buffer = MakeScreenBuffer(); + HANDLE h = buffer.get(); + + // Capture the original cursor visibility. + CONSOLE_CURSOR_INFO original{}; + VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleCursorInfo(h, &original)); + + { + // Hide the cursor. + ChangeTerminalMode hide{h, false}; + VERIFY_IS_TRUE(hide.IsConsole()); + + CONSOLE_CURSOR_INFO info{}; + VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleCursorInfo(h, &info)); + VERIFY_IS_FALSE(!!info.bVisible); + } + + // Destructor must restore the original visibility. + CONSOLE_CURSOR_INFO restored{}; + VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleCursorInfo(h, &restored)); + VERIFY_ARE_EQUAL(original.bVisible, restored.bVisible); + + { + // Show the cursor explicitly. + ChangeTerminalMode show{h, true}; + CONSOLE_CURSOR_INFO info{}; + VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleCursorInfo(h, &info)); + VERIFY_IS_TRUE(!!info.bVisible); + } + + // Non-console handle (a pipe) is silently ignored — IsConsole() returns false. + wil::unique_handle readPipe, writePipe; + VERIFY_WIN32_BOOL_SUCCEEDED(CreatePipe(&readPipe, &writePipe, nullptr, 0)); + ChangeTerminalMode nonConsole{readPipe.get(), false}; + VERIFY_IS_FALSE(nonConsole.IsConsole()); + } + + TEST_METHOD(VT_ChangeTerminalMode_RedirectedHandles) + { + wil::unique_hfile file{ + CreateFileW(L"NUL", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, 0, nullptr)}; + VERIFY_IS_TRUE(!!file); + { + ChangeTerminalMode mode{file.get(), false}; + VERIFY_IS_FALSE(mode.IsConsole()); + } + + // NULL handle. + { + ChangeTerminalMode mode{nullptr, false}; + VERIFY_IS_FALSE(mode.IsConsole()); + } + + // INVALID_HANDLE_VALUE. + { + ChangeTerminalMode mode{INVALID_HANDLE_VALUE, false}; + VERIFY_IS_FALSE(mode.IsConsole()); + } + } + + TEST_METHOD(VT_EnableVirtualTerminal) + { + auto buffer = MakeScreenBuffer(); + HANDLE h = buffer.get(); + + DWORD baseline{}; + VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleMode(h, &baseline)); + VERIFY_WIN32_BOOL_SUCCEEDED(SetConsoleMode(h, baseline & ~ENABLE_VIRTUAL_TERMINAL_PROCESSING)); + + { + EnableVirtualTerminal vt{h}; + VERIFY_IS_TRUE(vt.IsVTEnabled()); + + DWORD mode{}; + VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleMode(h, &mode)); + VERIFY_IS_TRUE(!!(mode & ENABLE_VIRTUAL_TERMINAL_PROCESSING)); + } + + DWORD restored{}; + VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleMode(h, &restored)); + VERIFY_IS_FALSE(!!(restored & ENABLE_VIRTUAL_TERMINAL_PROCESSING)); + + // With DISABLE_NEWLINE_AUTO_RETURN requested. + { + EnableVirtualTerminal vtWithNewline{h, EnableVirtualTerminal::Mode::Output, true}; + VERIFY_IS_TRUE(vtWithNewline.IsVTEnabled()); + + DWORD mode{}; + VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleMode(h, &mode)); + VERIFY_IS_TRUE(!!(mode & ENABLE_VIRTUAL_TERMINAL_PROCESSING)); + } + + // Input mode. + wil::unique_hfile conin{CreateFileW( + L"CONIN$", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, 0, nullptr)}; + VERIFY_IS_TRUE(!!conin); + + DWORD inputBaseline{}; + VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleMode(conin.get(), &inputBaseline)); + { + EnableVirtualTerminal vt{conin.get(), EnableVirtualTerminal::Mode::Input}; + VERIFY_IS_TRUE(vt.IsVTEnabled()); + + DWORD mode{}; + VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleMode(conin.get(), &mode)); + VERIFY_IS_TRUE(!!(mode & ENABLE_VIRTUAL_TERMINAL_INPUT)); + VERIFY_IS_FALSE(!!(mode & ENABLE_LINE_INPUT)); + VERIFY_IS_FALSE(!!(mode & ENABLE_ECHO_INPUT)); + } + + DWORD inputRestored{}; + VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleMode(conin.get(), &inputRestored)); + VERIFY_ARE_EQUAL(inputBaseline, inputRestored); + + // Non-console handles are silently ignored for both modes. + wil::unique_handle readPipe, writePipe; + VERIFY_WIN32_BOOL_SUCCEEDED(CreatePipe(&readPipe, &writePipe, nullptr, 0)); + VERIFY_IS_FALSE(EnableVirtualTerminal(readPipe.get(), EnableVirtualTerminal::Mode::Output).IsVTEnabled()); + VERIFY_IS_FALSE(EnableVirtualTerminal(readPipe.get(), EnableVirtualTerminal::Mode::Input).IsVTEnabled()); + } + + TEST_METHOD(VT_EnableVirtualTerminal_RedirectedHandles) + { + wil::unique_hfile file{ + CreateFileW(L"NUL", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, 0, nullptr)}; + VERIFY_IS_TRUE(!!file); + + VERIFY_IS_FALSE(EnableVirtualTerminal(file.get(), EnableVirtualTerminal::Mode::Output).IsVTEnabled()); + VERIFY_IS_FALSE(EnableVirtualTerminal(file.get(), EnableVirtualTerminal::Mode::Input).IsVTEnabled()); + VERIFY_IS_FALSE(EnableVirtualTerminal(nullptr, EnableVirtualTerminal::Mode::Output).IsVTEnabled()); + VERIFY_IS_FALSE(EnableVirtualTerminal(nullptr, EnableVirtualTerminal::Mode::Input).IsVTEnabled()); + VERIFY_IS_FALSE(EnableVirtualTerminal(INVALID_HANDLE_VALUE, EnableVirtualTerminal::Mode::Output).IsVTEnabled()); + VERIFY_IS_FALSE(EnableVirtualTerminal(INVALID_HANDLE_VALUE, EnableVirtualTerminal::Mode::Input).IsVTEnabled()); + } + + TEST_METHOD(VT_PrimaryDeviceAttributes) + { + // Clean DA1 response: conformance level 62, extensions Columns132 (1) and Sixel (4). + { + std::wostringstream out; + std::wistringstream in{L"\x1b[?62;1;4c"}; + PrimaryDeviceAttributes da{out, in}; + + VERIFY_IS_TRUE(da.Supports(PrimaryDeviceAttributes::Extension::Columns132)); + VERIFY_IS_TRUE(da.Supports(PrimaryDeviceAttributes::Extension::Sixel)); + VERIFY_IS_FALSE(da.Supports(PrimaryDeviceAttributes::Extension::PrinterPort)); + // DA1 request must have been written to the output stream. + VERIFY_ARE_EQUAL(std::wstring{L"\x1b[0c"}, out.str()); + } + + // Trailing plain text (e.g. queued user input) must not break parsing. + { + std::wostringstream out; + std::wistringstream in{L"\x1b[?62;1;4chello"}; + PrimaryDeviceAttributes da{out, in}; + + VERIFY_IS_TRUE(da.Supports(PrimaryDeviceAttributes::Extension::Columns132)); + VERIFY_IS_TRUE(da.Supports(PrimaryDeviceAttributes::Extension::Sixel)); + } + + // Trailing VT sequence must not corrupt suffix search or result extraction. + { + std::wostringstream out; + std::wistringstream in{L"\x1b[?62;6c\x1b[0m"}; + PrimaryDeviceAttributes da{out, in}; + + VERIFY_IS_TRUE(da.Supports(PrimaryDeviceAttributes::Extension::SelectiveErase)); + VERIFY_IS_FALSE(da.Supports(PrimaryDeviceAttributes::Extension::Columns132)); + } + + // Empty/malformed response — should not throw, extensions remain unset. + { + std::wostringstream out; + std::wistringstream in{L""}; + PrimaryDeviceAttributes da{out, in}; + + VERIFY_IS_FALSE(da.Supports(PrimaryDeviceAttributes::Extension::Columns132)); + } + } + + TEST_METHOD(VT_PrimaryDeviceAttributes_Empty) + { + // Empty/malformed response — should not throw, extensions remain unset. + std::wostringstream out; + std::wistringstream in{L""}; + + PrimaryDeviceAttributes da{out, in}; + + VERIFY_IS_FALSE(da.Supports(PrimaryDeviceAttributes::Extension::Columns132)); + } +}; + +} // namespace WSLCCLIVTSupportUnitTests diff --git a/test/windows/wslc/e2e/WSLCE2EHelpers.h b/test/windows/wslc/e2e/WSLCE2EHelpers.h index 926c8112c..d368afd15 100644 --- a/test/windows/wslc/e2e/WSLCE2EHelpers.h +++ b/test/windows/wslc/e2e/WSLCE2EHelpers.h @@ -19,50 +19,35 @@ Module Name: #include #include #include +#include "VTSupport.h" namespace WSLCE2ETests { -// VT100/ANSI escape sequence constants for TTY testing +// VT sequence constants and helpers for TTY testing. +// Sequences are sourced from wsl::windows::common::vt (VTSupport.h). namespace VT { -// Bracketed paste mode control sequences -#define VT_B_START "\x1b[?2004h" // Enable bracketed paste mode -#define VT_B_END "\x1b[?2004l" // Disable bracketed paste mode - -// Color/formatting sequences -#define VT_RESET "\x1b[0m" // Reset all attributes -#define VT_RED "\x1b[1;31m" // Bold red text - -// Terminal control sequences -#define VT_ERASE_LINE "\x1b[K" // Erase from cursor to end of line -#define VT_CR "\r" // Carriage return - - // Prompt patterns used in WSLC. - constexpr auto SESSION_PROMPT = VT_B_START VT_RED "root@ [ " VT_RESET "/" VT_RED " ]# "; - - // Constexpr representations of the control sequences for use in tests. - constexpr auto B_START = VT_B_START; - constexpr auto B_END = VT_B_END; - constexpr auto RESET = VT_RESET; - constexpr auto RED = VT_RED; - constexpr auto ERASE_LINE = VT_ERASE_LINE; - constexpr auto CR = VT_CR; - -// Remove macros to avoid polluting global namespace. -#undef VT_B_START -#undef VT_B_END -#undef VT_RESET -#undef VT_RED -#undef VT_ERASE_LINE -#undef VT_CR - - // Helper function to build container prompt + using namespace wsl::windows::common::vt; + + inline const auto& B_START = Cursor::BracketedPasteOn; + inline const auto& B_END = Cursor::BracketedPasteOff; + inline const auto& RESET = Format::Default; + inline const auto& ERASE_LINE = Erase::LineForward; + inline const Sequence CR{"\r"}; + + // The shell PS1 uses SGR 1;31 (bold + red) in a single sequence. + // Sgr({1, 31}) produces "\x1b[1;31m" to match exactly. + inline const ConstructedSequence RED = Sgr({1, 31}); + + // Prompt pattern used in WSLC TTY sessions. + inline const std::string SESSION_PROMPT = B_START + RED + "root@ [ " + RESET + "/" + RED + " ]# "; + inline std::string BuildContainerPrompt(const std::string& prompt, bool withBracketedPaste = true) { if (withBracketedPaste) { return std::format("{}{}", B_START, prompt); } - return std::format("{}", prompt); + return prompt; } inline std::string BuildContainerAttachPrompt(const std::string& prompt) diff --git a/test/windows/wslc/e2e/WSLCExecutor.cpp b/test/windows/wslc/e2e/WSLCExecutor.cpp index 4a2166b21..145ca112a 100644 --- a/test/windows/wslc/e2e/WSLCExecutor.cpp +++ b/test/windows/wslc/e2e/WSLCExecutor.cpp @@ -459,4 +459,4 @@ int WSLCInteractiveSession::ExitAndVerifyNoErrors(DWORD timeoutMs) return exitCode; } -} // namespace WSLCE2ETests \ No newline at end of file +} // namespace WSLCE2ETests diff --git a/test/windows/wslc/e2e/WSLCExecutor.h b/test/windows/wslc/e2e/WSLCExecutor.h index 98cc45de9..a6660d1ff 100644 --- a/test/windows/wslc/e2e/WSLCExecutor.h +++ b/test/windows/wslc/e2e/WSLCExecutor.h @@ -17,6 +17,7 @@ Module Name: #include "precomp.h" #include "windows/Common.h" +#include "VTSupport.h" namespace WSLCE2ETests { @@ -74,6 +75,16 @@ struct WSLCInteractiveSession void ExpectStderr(const std::string& expected); void ExpectCommandEcho(const std::string& command); + // Convenience overloads for VT sequence helpers. + void ExpectStdout(const wsl::windows::common::vt::Sequence& expected) + { + ExpectStdout(std::string(expected.Get())); + } + void ExpectStderr(const wsl::windows::common::vt::Sequence& expected) + { + ExpectStderr(std::string(expected.Get())); + } + bool IsRunning() const; void CloseStdin(); std::optional GetExitCode() const;