From 862862a5cb9ad7986427355716f525635eae3b8e Mon Sep 17 00:00:00 2001 From: David Bennett Date: Thu, 4 Jun 2026 04:40:01 -0700 Subject: [PATCH 01/14] Add VT Support library and update tests/callbacks to use it --- src/windows/common/CMakeLists.txt | 2 + src/windows/common/VTSupport.cpp | 322 ++++++++++++ src/windows/common/VTSupport.h | 475 ++++++++++++++++++ .../wslc/services/BuildImageCallback.cpp | 46 +- .../wslc/services/BuildImageCallback.h | 8 +- .../wslc/services/ChangeTerminalMode.h | 92 ---- .../wslc/services/ImageProgressCallback.cpp | 5 +- .../wslc/services/ImageProgressCallback.h | 8 +- .../wslc/WSLCCLIVTSupportUnitTests.cpp | 408 +++++++++++++++ test/windows/wslc/e2e/WSLCE2EHelpers.h | 57 +-- test/windows/wslc/e2e/WSLCExecutor.cpp | 4 +- test/windows/wslc/e2e/WSLCExecutor.h | 11 + 12 files changed, 1271 insertions(+), 167 deletions(-) create mode 100644 src/windows/common/VTSupport.cpp create mode 100644 src/windows/common/VTSupport.h delete mode 100644 src/windows/wslc/services/ChangeTerminalMode.h create mode 100644 test/windows/wslc/WSLCCLIVTSupportUnitTests.cpp 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..1267d25b1 --- /dev/null +++ b/src/windows/common/VTSupport.cpp @@ -0,0 +1,322 @@ +// Copyright (C) Microsoft Corporation. All rights reserved. +#include "precomp.h" +#include "VTSupport.h" + +namespace wsl::windows::common::vt +{ + namespace + { + // Extracts a VT sequence, expected one of the form ESCAPE + prefix + result + suffix, returning the result part. + std::string ExtractSequence(std::istream& inStream, std::string_view prefix, std::string_view suffix) + { + // Force discovery of available input + std::ignore = inStream.peek(); + + static constexpr std::streamsize s_bufferSize = 1024; + char buffer[s_bufferSize]; + std::streamsize bytesRead = inStream.readsome(buffer, s_bufferSize); + THROW_HR_IF(E_UNEXPECTED, bytesRead >= s_bufferSize); + + std::string_view resultView{ buffer, static_cast(bytesRead) }; + size_t escapeIndex = resultView.find(WSL_WINDOWS_VT_ESCAPE[0]); + if (escapeIndex == std::string_view::npos) + { + return {}; + } + + resultView = resultView.substr(escapeIndex); + size_t overheadLength = 1 + prefix.length() + suffix.length(); + if (resultView.length() <= overheadLength || + resultView.substr(1, prefix.length()) != prefix || + resultView.substr(resultView.length() - suffix.length()) != suffix) + { + return {}; + } + + return std::string{ resultView.substr(1 + prefix.length(), resultView.length() - overheadLength) }; + } + } + + 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); + } + +} // namespace wsl::windows::common::vt + +// The beginning of a Control Sequence Introducer +#define WSL_WINDOWS_VT_CSI WSL_WINDOWS_VT_ESCAPE "[" + +// The beginning of an Operating system command +#define WSL_WINDOWS_VT_OSC WSL_WINDOWS_VT_ESCAPE "]" + +// Define a text formatting sequence with an integer id +#define WSL_WINDOWS_VT_TEXTFORMAT(_id_) WSL_WINDOWS_VT_CSI #_id_ "m" + +namespace wsl::windows::common::vt +{ + 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::ostream& outStream, std::istream& inStream) + { + try + { + EnableVirtualTerminal inputMode{ GetStdHandle(STD_INPUT_HANDLE), EnableVirtualTerminal::Mode::Input }; + if (!inputMode.IsVTEnabled()) + { + return; + } + + // Send DA1 Primary Device Attributes request + outStream << WSL_WINDOWS_VT_CSI << "0c"; + outStream.flush(); + + // Response is of the form WSL_WINDOWS_VT_CSI ? ; ( ;)* c + std::string sequence = ExtractSequence(inStream, "[?", "c"); + std::vector values = wsl::shared::string::Split(sequence, ';'); + + if (!values.empty()) + { + m_conformanceLevel = std::stoul(values[0]); + } + + for (size_t i = 1; i < values.size(); ++i) + { + m_extensions |= 1ull << std::stoul(values[i]); + } + } + 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); + std::ostringstream result; + result << WSL_WINDOWS_VT_CSI << cells << 'A'; + return ConstructedSequence{ std::move(result).str() }; + } + + ConstructedSequence Down(int cells) + { + THROW_HR_IF(E_INVALIDARG, cells < 0); + std::ostringstream result; + result << WSL_WINDOWS_VT_CSI << cells << 'B'; + return ConstructedSequence{ std::move(result).str() }; + } + + ConstructedSequence Forward(int cells) + { + THROW_HR_IF(E_INVALIDARG, cells < 0); + std::ostringstream result; + result << WSL_WINDOWS_VT_CSI << cells << 'C'; + return ConstructedSequence{ std::move(result).str() }; + } + + ConstructedSequence Backward(int cells) + { + THROW_HR_IF(E_INVALIDARG, cells < 0); + std::ostringstream result; + result << WSL_WINDOWS_VT_CSI << cells << 'D'; + return ConstructedSequence{ std::move(result).str() }; + } + + ConstructedSequence MoveTo(int row, int col) + { + THROW_HR_IF(E_INVALIDARG, row < 1 || col < 1); + std::ostringstream result; + result << WSL_WINDOWS_VT_CSI << row << ';' << col << 'H'; + return ConstructedSequence{ std::move(result).str() }; + } + + const Sequence Home{ WSL_WINDOWS_VT_CSI "H" }; + const Sequence SavePos{ WSL_WINDOWS_VT_CSI "s" }; + const Sequence RestorePos{ WSL_WINDOWS_VT_CSI "u" }; + + 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 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 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() }; + } + } + + 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 Erase + { + const Sequence LineForward{ WSL_WINDOWS_VT_CSI "0K" }; + 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 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..586a15c43 --- /dev/null +++ b/src/windows/common/VTSupport.h @@ -0,0 +1,475 @@ +// Copyright (C) Microsoft Corporation. All rights reserved. +#pragma once + +#include +#include +#include +#include +#include +#include "wslutil.h" + +// The escape character that begins all VT sequences +#define WSL_WINDOWS_VT_ESCAPE "\x1b" + +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 + { + const DWORD preferredFlags = + ENABLE_VIRTUAL_TERMINAL_PROCESSING | (disableNewlineAutoReturn ? DISABLE_NEWLINE_AUTO_RETURN : 0); + + for (DWORD flags : {preferredFlags, static_cast(ENABLE_VIRTUAL_TERMINAL_PROCESSING)}) + { + const DWORD newMode = current | flags; + if (newMode == current) + { + return; + } + if (SetConsoleMode(console, newMode)) + { + m_console = console; + m_originalMode = current; + return; + } + LOG_LAST_ERROR_IF(GetLastError() != ERROR_INVALID_PARAMETER); + } + } + } + + ~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) {} + + std::string_view Get() const { return m_chars; } + + 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); } + ConstructedSequence& operator=(ConstructedSequence&& other) noexcept { m_str = std::move(other.m_str); Set(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. + PrimaryDeviceAttributes(std::ostream& outStream, std::istream& 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; + } + + // 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; + + static Color GetAccentColor(); + }; + + 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 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); + } + + ConstructedSequence Hyperlink(const std::string& text, const std::string& ref); + + // Constructs a single SGR sequence with one or more semicolon-separated parameters. + // e.g. Sgr({1, 31}) produces "\x1b[1;31m" — bold + red in one sequence. + // Prefer named constants for single-parameter sequences; use this only when + // a multi-parameter form is required to match specific terminal output exactly. + ConstructedSequence Sgr(std::initializer_list params); + } + + // 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 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(); + } + + // 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 diff --git a/src/windows/wslc/services/BuildImageCallback.cpp b/src/windows/wslc/services/BuildImageCallback.cpp index 9cd1c639d..9787b646e 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(Format::Fg::BrightGreen + wide + Format::Default + newlines); return S_OK; } CATCH_RETURN(); @@ -197,46 +190,45 @@ void BuildImageCallback::Redraw() // 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}; + std::wostringstream buffer; + buffer << Cursor::Hide << 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); + buffer << Cursor::Up(m_displayedLines) << 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; + buffer << wline << Erase::LineForward << 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 +249,9 @@ void BuildImageCallback::Redraw() appendLine(line); } - buffer += c_escapeUndimShowCursor; + buffer << Format::Normal << Cursor::Show; - WriteTerminal(buffer); + WriteTerminal(buffer.str()); m_displayedLines = displayCount; } diff --git a/src/windows/wslc/services/BuildImageCallback.h b/src/windows/wslc/services/BuildImageCallback.h index 97ef88df3..e7ba2305b 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,7 +54,7 @@ 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; 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..216b15cc2 100644 --- a/src/windows/wslc/services/ImageProgressCallback.cpp +++ b/src/windows/wslc/services/ImageProgressCallback.cpp @@ -19,18 +19,19 @@ Module Name: namespace wsl::windows::wslc::services { using namespace wsl::shared; +using namespace wsl::windows::common::vt; auto ImageProgressCallback::MoveToLine(SHORT line) { if (line > 0) { - wprintf(L"\033[%iA", line); + std::wcout << Cursor::Up(static_cast(line)); } return wil::scope_exit([line = line]() { if (line > 1) { - wprintf(L"\033[%iB", line - 1); + std::wcout << Cursor::Down(static_cast(line - 1)); } }); } diff --git a/src/windows/wslc/services/ImageProgressCallback.h b/src/windows/wslc/services/ImageProgressCallback.h index e91aef2b7..f1dad3c0a 100644 --- a/src/windows/wslc/services/ImageProgressCallback.h +++ b/src/windows/wslc/services/ImageProgressCallback.h @@ -12,8 +12,8 @@ Module Name: --*/ #pragma once -#include "ChangeTerminalMode.h" #include "SessionService.h" +#include "VTSupport.h" namespace wsl::windows::wslc::services { @@ -30,7 +30,7 @@ class DECLSPEC_UUID("7A1D3376-835A-471A-8DC9-23653D9962D0") ImageProgressCallbac 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}; + wsl::windows::common::vt::EnableVirtualTerminal m_vtMode{GetStdHandle(STD_OUTPUT_HANDLE)}; + wsl::windows::common::vt::ChangeTerminalMode m_terminalMode{GetStdHandle(STD_OUTPUT_HANDLE), 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..675d47728 --- /dev/null +++ b/test/windows/wslc/WSLCCLIVTSupportUnitTests.cpp @@ -0,0 +1,408 @@ +/*++ + +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[0K", 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_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()); + } + + 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)); + 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) + { + // Feed a synthetic DA1 response: ESC [ ? 62 ; 1 ; 4 c + // Conformance level 62 (VT400), extensions: Columns132 (1), Sixel (4). + // ExtractSequence is exercised indirectly through PrimaryDeviceAttributes. + std::ostringstream out; + std::istringstream in{"\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)); + + // Verify the DA1 request was sent. + VERIFY_ARE_EQUAL("\x1b[0c", out.str()); + } + + TEST_METHOD(VT_PrimaryDeviceAttributes_Empty) + { + // Empty/malformed response — should not throw, extensions remain unset. + std::ostringstream out; + std::istringstream in{""}; + + 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..7bd53a047 100644 --- a/test/windows/wslc/e2e/WSLCE2EHelpers.h +++ b/test/windows/wslc/e2e/WSLCE2EHelpers.h @@ -19,55 +19,40 @@ 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. + // Format::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 B_START + prompt; } - return std::format("{}", prompt); + return prompt; } inline std::string BuildContainerAttachPrompt(const std::string& prompt) { - return std::format("{}{}{}{}", CR, ERASE_LINE, CR, prompt); + return CR + ERASE_LINE + CR + prompt; } } // namespace VT diff --git a/test/windows/wslc/e2e/WSLCExecutor.cpp b/test/windows/wslc/e2e/WSLCExecutor.cpp index 4a2166b21..791547f18 100644 --- a/test/windows/wslc/e2e/WSLCExecutor.cpp +++ b/test/windows/wslc/e2e/WSLCExecutor.cpp @@ -326,7 +326,7 @@ void WSLCInteractiveSession::ExpectStderr(const std::string& expected) void WSLCInteractiveSession::ExpectCommandEcho(const std::string& command) { // TTY mode: expect command echo, then B_END and carriage return - ExpectStdout(std::format("{}\r\n{}\r", command, VT::B_END)); + ExpectStdout(std::format("{}\r\n{}\r", command, std::string(VT::B_END.Get()))); } void WSLCInteractiveSession::Write(const std::string& data) @@ -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; From a61f91720547779d71733083eb2e33b1263b16bd Mon Sep 17 00:00:00 2001 From: David Bennett Date: Thu, 4 Jun 2026 04:51:03 -0700 Subject: [PATCH 02/14] Fix incorrect comment headers --- src/windows/common/VTSupport.cpp | 16 +++++++++++++++- src/windows/common/VTSupport.h | 16 +++++++++++++++- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/src/windows/common/VTSupport.cpp b/src/windows/common/VTSupport.cpp index 1267d25b1..1cb389155 100644 --- a/src/windows/common/VTSupport.cpp +++ b/src/windows/common/VTSupport.cpp @@ -1,4 +1,18 @@ -// Copyright (C) Microsoft Corporation. All rights reserved. +/*++ + +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" diff --git a/src/windows/common/VTSupport.h b/src/windows/common/VTSupport.h index 586a15c43..a05ad011e 100644 --- a/src/windows/common/VTSupport.h +++ b/src/windows/common/VTSupport.h @@ -1,4 +1,18 @@ -// Copyright (C) Microsoft Corporation. All rights reserved. +/*++ + +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 From 637e6d87b2378df1d6b06787042d4ecb1ffb527a Mon Sep 17 00:00:00 2001 From: David Bennett Date: Thu, 4 Jun 2026 05:26:50 -0700 Subject: [PATCH 03/14] Add format specialization and use std::format in a few places, fix imageprogresscallback --- src/windows/common/VTSupport.h | 41 +++++++++++++++++++ .../wslc/services/BuildImageCallback.cpp | 2 +- .../wslc/services/ImageProgressCallback.cpp | 8 ++-- .../wslc/services/ImageProgressCallback.h | 6 +-- .../wslc/WSLCCLIVTSupportUnitTests.cpp | 12 ++++++ test/windows/wslc/e2e/WSLCE2EHelpers.h | 4 +- 6 files changed, 63 insertions(+), 10 deletions(-) diff --git a/src/windows/common/VTSupport.h b/src/windows/common/VTSupport.h index a05ad011e..820029869 100644 --- a/src/windows/common/VTSupport.h +++ b/src/windows/common/VTSupport.h @@ -487,3 +487,44 @@ namespace wsl::windows::common::vt { } } // 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 9787b646e..e9ff6a771 100644 --- a/src/windows/wslc/services/BuildImageCallback.cpp +++ b/src/windows/wslc/services/BuildImageCallback.cpp @@ -177,7 +177,7 @@ try const auto newlines = wide.substr(bodyLength); wide.resize(bodyLength); - WriteTerminal(Format::Fg::BrightGreen + wide + Format::Default + newlines); + WriteTerminal(std::format(L"{}{}{}{}", Format::Fg::BrightGreen, wide, Format::Default, newlines)); return S_OK; } CATCH_RETURN(); diff --git a/src/windows/wslc/services/ImageProgressCallback.cpp b/src/windows/wslc/services/ImageProgressCallback.cpp index 216b15cc2..9b243043e 100644 --- a/src/windows/wslc/services/ImageProgressCallback.cpp +++ b/src/windows/wslc/services/ImageProgressCallback.cpp @@ -21,17 +21,17 @@ namespace wsl::windows::wslc::services { using namespace wsl::shared; using namespace wsl::windows::common::vt; -auto ImageProgressCallback::MoveToLine(SHORT line) +auto ImageProgressCallback::MoveToLine(int line) { if (line > 0) { - std::wcout << Cursor::Up(static_cast(line)); + wprintf(std::format(L"{}", Cursor::Up(line)).c_str()); } return wil::scope_exit([line = line]() { if (line > 1) { - std::wcout << Cursor::Down(static_cast(line - 1)); + wprintf(std::format(L"{}", Cursor::Down(line - 1)).c_str()); } }); } @@ -127,7 +127,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 f1dad3c0a..2ede30b17 100644 --- a/src/windows/wslc/services/ImageProgressCallback.h +++ b/src/windows/wslc/services/ImageProgressCallback.h @@ -22,14 +22,14 @@ class DECLSPEC_UUID("7A1D3376-835A-471A-8DC9-23653D9962D0") ImageProgressCallbac : public Microsoft::WRL::RuntimeClass, IProgressCallback, IFastRundown> { public: - auto MoveToLine(SHORT line); + auto MoveToLine(int line); HRESULT OnProgress(LPCSTR status, LPCSTR id, ULONGLONG current, ULONGLONG total) override; private: static CONSOLE_SCREEN_BUFFER_INFO Info(); 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; + std::map m_statuses; + int m_currentLine = 0; wsl::windows::common::vt::EnableVirtualTerminal m_vtMode{GetStdHandle(STD_OUTPUT_HANDLE)}; wsl::windows::common::vt::ChangeTerminalMode m_terminalMode{GetStdHandle(STD_OUTPUT_HANDLE), false}; }; diff --git a/test/windows/wslc/WSLCCLIVTSupportUnitTests.cpp b/test/windows/wslc/WSLCCLIVTSupportUnitTests.cpp index 675d47728..145c9ffc4 100644 --- a/test/windows/wslc/WSLCCLIVTSupportUnitTests.cpp +++ b/test/windows/wslc/WSLCCLIVTSupportUnitTests.cpp @@ -236,6 +236,18 @@ class WSLCCLIVTSupportUnitTests 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) diff --git a/test/windows/wslc/e2e/WSLCE2EHelpers.h b/test/windows/wslc/e2e/WSLCE2EHelpers.h index 7bd53a047..46483026e 100644 --- a/test/windows/wslc/e2e/WSLCE2EHelpers.h +++ b/test/windows/wslc/e2e/WSLCE2EHelpers.h @@ -45,14 +45,14 @@ namespace VT { { if (withBracketedPaste) { - return B_START + prompt; + return std::format("{}{}", B_START, prompt); } return prompt; } inline std::string BuildContainerAttachPrompt(const std::string& prompt) { - return CR + ERASE_LINE + CR + prompt; + return std::format("{}{}{}{}", CR, ERASE_LINE, CR, prompt); } } // namespace VT From 8d0fe9bb41e68cc51ef90d4dee1cba75ce7bbb81 Mon Sep 17 00:00:00 2001 From: David Bennett Date: Thu, 4 Jun 2026 05:32:12 -0700 Subject: [PATCH 04/14] Restore executor std format use. --- test/windows/wslc/e2e/WSLCExecutor.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/windows/wslc/e2e/WSLCExecutor.cpp b/test/windows/wslc/e2e/WSLCExecutor.cpp index 791547f18..145ca112a 100644 --- a/test/windows/wslc/e2e/WSLCExecutor.cpp +++ b/test/windows/wslc/e2e/WSLCExecutor.cpp @@ -326,7 +326,7 @@ void WSLCInteractiveSession::ExpectStderr(const std::string& expected) void WSLCInteractiveSession::ExpectCommandEcho(const std::string& command) { // TTY mode: expect command echo, then B_END and carriage return - ExpectStdout(std::format("{}\r\n{}\r", command, std::string(VT::B_END.Get()))); + ExpectStdout(std::format("{}\r\n{}\r", command, VT::B_END)); } void WSLCInteractiveSession::Write(const std::string& data) From e08127c057e78c13dea3129273257ee578b01599 Mon Sep 17 00:00:00 2001 From: David Bennett Date: Thu, 4 Jun 2026 06:08:34 -0700 Subject: [PATCH 05/14] Formatting and copilot feedback --- src/windows/common/VTSupport.cpp | 525 ++++++------ src/windows/common/VTSupport.h | 802 +++++++++--------- .../wslc/services/ImageProgressCallback.cpp | 4 +- .../wslc/WSLCCLIVTSupportUnitTests.cpp | 48 +- 4 files changed, 716 insertions(+), 663 deletions(-) diff --git a/src/windows/common/VTSupport.cpp b/src/windows/common/VTSupport.cpp index 1cb389155..dca8fcd1c 100644 --- a/src/windows/common/VTSupport.cpp +++ b/src/windows/common/VTSupport.cpp @@ -16,321 +16,334 @@ Module Name: #include "precomp.h" #include "VTSupport.h" -namespace wsl::windows::common::vt -{ - namespace +namespace wsl::windows::common::vt { +namespace { + // Extracts a VT sequence, expected one of the form ESCAPE + prefix + result + suffix, returning the result part. + // Any bytes following the suffix (e.g. queued user input) are ignored. + std::string ExtractSequence(std::istream& inStream, std::string_view prefix, std::string_view suffix) { - // Extracts a VT sequence, expected one of the form ESCAPE + prefix + result + suffix, returning the result part. - std::string ExtractSequence(std::istream& inStream, std::string_view prefix, std::string_view suffix) - { - // Force discovery of available input - std::ignore = inStream.peek(); + // Force discovery of available input + std::ignore = inStream.peek(); - static constexpr std::streamsize s_bufferSize = 1024; - char buffer[s_bufferSize]; - std::streamsize bytesRead = inStream.readsome(buffer, s_bufferSize); - THROW_HR_IF(E_UNEXPECTED, bytesRead >= s_bufferSize); + static constexpr std::streamsize s_bufferSize = 1024; + char buffer[s_bufferSize]; + std::streamsize bytesRead = inStream.readsome(buffer, s_bufferSize); + THROW_HR_IF(E_UNEXPECTED, bytesRead >= s_bufferSize); - std::string_view resultView{ buffer, static_cast(bytesRead) }; - size_t escapeIndex = resultView.find(WSL_WINDOWS_VT_ESCAPE[0]); - if (escapeIndex == std::string_view::npos) - { - return {}; - } + std::string_view resultView{buffer, static_cast(bytesRead)}; - resultView = resultView.substr(escapeIndex); - size_t overheadLength = 1 + prefix.length() + suffix.length(); - if (resultView.length() <= overheadLength || - resultView.substr(1, prefix.length()) != prefix || - resultView.substr(resultView.length() - suffix.length()) != suffix) - { - return {}; - } + // Locate the escape character that begins the sequence. + const size_t escapeIndex = resultView.find(WSL_WINDOWS_VT_ESCAPE[0]); + if (escapeIndex == std::string_view::npos) + { + return {}; + } + + resultView = resultView.substr(escapeIndex); - return std::string{ resultView.substr(1 + prefix.length(), resultView.length() - overheadLength) }; + // Verify the prefix immediately follows the escape character. + if (resultView.length() < 1 + prefix.length() || resultView.substr(1, prefix.length()) != prefix) + { + return {}; } - } - void ConstructedSequence::Append(const Sequence& sequence) - { - if (!sequence.Get().empty()) + // Find the suffix anywhere after the prefix. + const std::string_view body = resultView.substr(1 + prefix.length()); + const size_t suffixIndex = body.find(suffix); + if (suffixIndex == std::string_view::npos) { - m_str += sequence.Get(); - Set(m_str); + return {}; } + + return std::string{body.substr(0, suffixIndex)}; } +} // namespace - void ConstructedSequence::Clear() +void ConstructedSequence::Append(const Sequence& sequence) +{ + if (!sequence.Get().empty()) { - m_str.clear(); + m_str += sequence.Get(); Set(m_str); } +} + +void ConstructedSequence::Clear() +{ + m_str.clear(); + Set(m_str); +} } // namespace wsl::windows::common::vt // The beginning of a Control Sequence Introducer -#define WSL_WINDOWS_VT_CSI WSL_WINDOWS_VT_ESCAPE "[" +#define WSL_WINDOWS_VT_CSI WSL_WINDOWS_VT_ESCAPE "[" // The beginning of an Operating system command -#define WSL_WINDOWS_VT_OSC WSL_WINDOWS_VT_ESCAPE "]" +#define WSL_WINDOWS_VT_OSC WSL_WINDOWS_VT_ESCAPE "]" // Define a text formatting sequence with an integer id -#define WSL_WINDOWS_VT_TEXTFORMAT(_id_) WSL_WINDOWS_VT_CSI #_id_ "m" +#define WSL_WINDOWS_VT_TEXTFORMAT(_id_) WSL_WINDOWS_VT_CSI #_id_ "m" -namespace wsl::windows::common::vt +namespace wsl::windows::common::vt { +ConstructedSequence Sgr(std::initializer_list params) { - ConstructedSequence Sgr(std::initializer_list params) + std::ostringstream result; + result << WSL_WINDOWS_VT_CSI; + bool first = true; + for (const int param : params) { - std::ostringstream result; - result << WSL_WINDOWS_VT_CSI; - bool first = true; - for (const int param : params) + if (!first) { - if (!first) - { - result << ';'; - } - result << param; - first = false; + result << ';'; } - result << 'm'; - return ConstructedSequence{ std::move(result).str() }; + result << param; + first = false; } + result << 'm'; + return ConstructedSequence{std::move(result).str()}; +} - PrimaryDeviceAttributes::PrimaryDeviceAttributes(std::ostream& outStream, std::istream& inStream) +PrimaryDeviceAttributes::PrimaryDeviceAttributes(std::ostream& outStream, std::istream& inStream) +{ + try { - 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 stringstreams) 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 + outStream << WSL_WINDOWS_VT_CSI << "0c"; + outStream.flush(); + + // Response is of the form WSL_WINDOWS_VT_CSI ? ; ( ;)* c + std::string sequence = ExtractSequence(inStream, "[?", "c"); + std::vector values = wsl::shared::string::Split(sequence, ';'); + + if (!values.empty()) { - EnableVirtualTerminal inputMode{ GetStdHandle(STD_INPUT_HANDLE), EnableVirtualTerminal::Mode::Input }; - if (!inputMode.IsVTEnabled()) - { - return; - } - - // Send DA1 Primary Device Attributes request - outStream << WSL_WINDOWS_VT_CSI << "0c"; - outStream.flush(); - - // Response is of the form WSL_WINDOWS_VT_CSI ? ; ( ;)* c - std::string sequence = ExtractSequence(inStream, "[?", "c"); - std::vector values = wsl::shared::string::Split(sequence, ';'); - - if (!values.empty()) - { - m_conformanceLevel = std::stoul(values[0]); - } + m_conformanceLevel = std::stoul(values[0]); + } - for (size_t i = 1; i < values.size(); ++i) + // 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::stoul(values[i]); + if (ext <= c_maxExtensionBit) { - m_extensions |= 1ull << std::stoul(values[i]); + m_extensions |= 1ull << ext; } } - CATCH_LOG(); } + CATCH_LOG(); +} + +bool PrimaryDeviceAttributes::Supports(Extension extension) const +{ + uint64_t extensionMask = 1ull << ToIntegral(extension); + return (m_extensions & extensionMask) == extensionMask; +} - bool PrimaryDeviceAttributes::Supports(Extension extension) const +namespace Cursor { + ConstructedSequence Up(int cells) { - uint64_t extensionMask = 1ull << ToIntegral(extension); - return (m_extensions & extensionMask) == extensionMask; + THROW_HR_IF(E_INVALIDARG, cells < 0); + std::ostringstream result; + result << WSL_WINDOWS_VT_CSI << cells << 'A'; + return ConstructedSequence{std::move(result).str()}; } - namespace Cursor + ConstructedSequence Down(int cells) { - ConstructedSequence Up(int cells) - { - THROW_HR_IF(E_INVALIDARG, cells < 0); - std::ostringstream result; - result << WSL_WINDOWS_VT_CSI << cells << 'A'; - return ConstructedSequence{ std::move(result).str() }; - } + THROW_HR_IF(E_INVALIDARG, cells < 0); + std::ostringstream result; + result << WSL_WINDOWS_VT_CSI << cells << 'B'; + return ConstructedSequence{std::move(result).str()}; + } - ConstructedSequence Down(int cells) - { - THROW_HR_IF(E_INVALIDARG, cells < 0); - std::ostringstream result; - result << WSL_WINDOWS_VT_CSI << cells << 'B'; - return ConstructedSequence{ std::move(result).str() }; - } + ConstructedSequence Forward(int cells) + { + THROW_HR_IF(E_INVALIDARG, cells < 0); + std::ostringstream result; + result << WSL_WINDOWS_VT_CSI << cells << 'C'; + return ConstructedSequence{std::move(result).str()}; + } - ConstructedSequence Forward(int cells) - { - THROW_HR_IF(E_INVALIDARG, cells < 0); - std::ostringstream result; - result << WSL_WINDOWS_VT_CSI << cells << 'C'; - return ConstructedSequence{ std::move(result).str() }; - } + ConstructedSequence Backward(int cells) + { + THROW_HR_IF(E_INVALIDARG, cells < 0); + std::ostringstream result; + result << WSL_WINDOWS_VT_CSI << cells << 'D'; + return ConstructedSequence{std::move(result).str()}; + } - ConstructedSequence Backward(int cells) + ConstructedSequence MoveTo(int row, int col) + { + THROW_HR_IF(E_INVALIDARG, row < 1 || col < 1); + std::ostringstream result; + result << WSL_WINDOWS_VT_CSI << row << ';' << col << 'H'; + return ConstructedSequence{std::move(result).str()}; + } + + const Sequence Home{WSL_WINDOWS_VT_CSI "H"}; + const Sequence SavePos{WSL_WINDOWS_VT_CSI "s"}; + const Sequence RestorePos{WSL_WINDOWS_VT_CSI "u"}; + + 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) { - THROW_HR_IF(E_INVALIDARG, cells < 0); std::ostringstream result; - result << WSL_WINDOWS_VT_CSI << cells << 'D'; - return ConstructedSequence{ std::move(result).str() }; + 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()}; } - - ConstructedSequence MoveTo(int row, int col) + } // 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) { - THROW_HR_IF(E_INVALIDARG, row < 1 || col < 1); std::ostringstream result; - result << WSL_WINDOWS_VT_CSI << row << ';' << col << 'H'; - return ConstructedSequence{ std::move(result).str() }; + 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 - const Sequence Home{ WSL_WINDOWS_VT_CSI "H" }; - const Sequence SavePos{ WSL_WINDOWS_VT_CSI "s" }; - const Sequence RestorePos{ WSL_WINDOWS_VT_CSI "u" }; - - 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" }; + 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 Format + +namespace Erase { + const Sequence LineForward{WSL_WINDOWS_VT_CSI "0K"}; + 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) { - 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() }; - } - } + // See https://conemu.github.io/en/AnsiEscapeCodes.html#ConEmu_specific_OSC - namespace Bg + 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) { - 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() }; - } + 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; } - ConstructedSequence Hyperlink(const std::string& text, const std::string& ref) + int stateId; + switch (state) { - 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() }; + 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); } - } - - namespace Erase - { - const Sequence LineForward{ WSL_WINDOWS_VT_CSI "0K" }; - 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 Progress - { - ConstructedSequence Construct(State state, std::optional percentage) + std::ostringstream result; + result << WSL_WINDOWS_VT_OSC "9;4;" << stateId << ";"; + if (percentage.has_value()) { - // 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() }; + result << percentage.value(); } - } // namespace Progress - } // namespace wsl::windows::common::vt + 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 index 820029869..41900201d 100644 --- a/src/windows/common/VTSupport.h +++ b/src/windows/common/VTSupport.h @@ -16,480 +16,496 @@ Module Name: #pragma once #include +#include #include #include #include #include +#include #include "wslutil.h" // The escape character that begins all VT sequences -#define WSL_WINDOWS_VT_ESCAPE "\x1b" +#define WSL_WINDOWS_VT_ESCAPE "\x1b" 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 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); - } +// 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); +// 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) + ChangeTerminalMode(HANDLE console, bool cursorVisible) : m_console(console) + { + if (!wsl::windows::common::wslutil::IsConsoleHandle(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)); + m_console = nullptr; + return; } - ~ChangeTerminalMode() - { - if (m_console) - { - LOG_IF_WIN32_BOOL_FALSE(SetConsoleCursorInfo(m_console, &m_originalCursorInfo)); - } - } + 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)); + } - bool IsConsole() const + ~ChangeTerminalMode() + { + if (m_console) { - return m_console != nullptr; + LOG_IF_WIN32_BOOL_FALSE(SetConsoleCursorInfo(m_console, &m_originalCursorInfo)); } + } - private: - HANDLE m_console{}; - CONSOLE_CURSOR_INFO 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 +// 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 { - public: - NON_COPYABLE(EnableVirtualTerminal); - NON_MOVABLE(EnableVirtualTerminal); + Output, + Input, + }; - enum class Mode + explicit EnableVirtualTerminal(HANDLE console, Mode mode = Mode::Output, bool disableNewlineAutoReturn = false) + { + DWORD current; + if (!GetConsoleMode(console, ¤t)) { - Output, - Input, - }; + LOG_LAST_ERROR_IF(GetLastError() != ERROR_INVALID_HANDLE); + return; + } - explicit EnableVirtualTerminal(HANDLE console, Mode mode = Mode::Output, bool disableNewlineAutoReturn = false) + if (mode == Mode::Input) { - DWORD current; - if (!GetConsoleMode(console, ¤t)) + const DWORD newMode = (current & ~(ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT)) | ENABLE_EXTENDED_FLAGS | ENABLE_VIRTUAL_TERMINAL_INPUT; + if (SetConsoleMode(console, newMode)) { - LOG_LAST_ERROR_IF(GetLastError() != ERROR_INVALID_HANDLE); - return; + m_console = console; + m_originalMode = current; } + else + { + LOG_LAST_ERROR_IF(GetLastError() != ERROR_INVALID_PARAMETER); + } + } + else + { + const DWORD preferredFlags = ENABLE_VIRTUAL_TERMINAL_PROCESSING | (disableNewlineAutoReturn ? DISABLE_NEWLINE_AUTO_RETURN : 0); - if (mode == Mode::Input) + for (DWORD flags : {preferredFlags, static_cast(ENABLE_VIRTUAL_TERMINAL_PROCESSING)}) { - const DWORD newMode = - (current & ~(ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT)) | ENABLE_EXTENDED_FLAGS | ENABLE_VIRTUAL_TERMINAL_INPUT; + const DWORD newMode = current | flags; + if (newMode == current) + { + return; + } if (SetConsoleMode(console, newMode)) { m_console = console; m_originalMode = current; + return; } - else - { - LOG_LAST_ERROR_IF(GetLastError() != ERROR_INVALID_PARAMETER); - } - } - else - { - const DWORD preferredFlags = - ENABLE_VIRTUAL_TERMINAL_PROCESSING | (disableNewlineAutoReturn ? DISABLE_NEWLINE_AUTO_RETURN : 0); - - for (DWORD flags : {preferredFlags, static_cast(ENABLE_VIRTUAL_TERMINAL_PROCESSING)}) - { - const DWORD newMode = current | flags; - if (newMode == current) - { - return; - } - if (SetConsoleMode(console, newMode)) - { - m_console = console; - m_originalMode = current; - return; - } - LOG_LAST_ERROR_IF(GetLastError() != ERROR_INVALID_PARAMETER); - } - } - } - - ~EnableVirtualTerminal() - { - if (m_console) - { - LOG_IF_WIN32_BOOL_FALSE(SetConsoleMode(m_console, m_originalMode)); + LOG_LAST_ERROR_IF(GetLastError() != ERROR_INVALID_PARAMETER); } } + } - bool IsVTEnabled() const + ~EnableVirtualTerminal() + { + if (m_console) { - return m_console != nullptr; + LOG_IF_WIN32_BOOL_FALSE(SetConsoleMode(m_console, m_originalMode)); } + } - 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 + bool IsVTEnabled() const { - constexpr Sequence() = default; - explicit constexpr Sequence(std::string_view c) : m_chars(c) {} + return m_console != nullptr; + } - std::string_view Get() const { return m_chars; } +private: + HANDLE m_console = nullptr; + DWORD m_originalMode = 0; +}; - protected: - void Set(const std::string& s) { m_chars = s; } +// 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. - private: - std::string_view m_chars; - }; - - // A VT sequence that is constructed at runtime. - struct ConstructedSequence : public Sequence +// The base for all VT sequences. +struct Sequence +{ + constexpr Sequence() = default; + explicit constexpr Sequence(std::string_view c) : m_chars(c) { - 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); } - ConstructedSequence& operator=(ConstructedSequence&& other) noexcept { m_str = std::move(other.m_str); Set(m_str); return *this; } - - void Append(const Sequence& sequence); - - void Clear(); + } - private: - std::string m_str; - }; + std::string_view Get() const + { + return m_chars; + } - // 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); +protected: + void Set(const std::string& s) + { + m_chars = s; + } - // Below are mapped to the sequences described here: - // https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences +private: + std::string_view m_chars; +}; - // Contains the response to a DA1 (Primary Device Attributes) request. - struct PrimaryDeviceAttributes +// A VT sequence that is constructed at runtime. +struct ConstructedSequence : public Sequence +{ + ConstructedSequence() { - // Queries the device attributes on creation. - PrimaryDeviceAttributes(std::ostream& outStream, std::istream& 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; - }; + Set(m_str); + } + explicit ConstructedSequence(std::string s) : m_str(std::move(s)) + { + Set(m_str); + } - // Cursor movement, visibility, and input mode sequences. - namespace Cursor + ConstructedSequence(const ConstructedSequence& other) : m_str(other.m_str) { - // 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; + Set(m_str); + } + ConstructedSequence& operator=(const ConstructedSequence& other) + { + m_str = other.m_str; + Set(m_str); + return *this; } - // Text formatting (color, weight, style) sequences. - namespace Format + ConstructedSequence(ConstructedSequence&& other) noexcept : m_str(std::move(other.m_str)) + { + Set(m_str); + } + ConstructedSequence& operator=(ConstructedSequence&& other) noexcept { - extern const Sequence Default; - extern const Sequence Negative; + m_str = std::move(other.m_str); + Set(m_str); + return *this; + } - // Intensity attributes. Normal cancels both Bright and Dim (SGR 22). - extern const Sequence Bright; - extern const Sequence Dim; - extern const Sequence Normal; + void Append(const Sequence& sequence); - extern const Sequence Italic; - extern const Sequence NoItalic; - extern const Sequence Underline; - extern const Sequence NoUnderline; + void Clear(); - // A color, used in constructed sequences. - struct Color - { - uint8_t R; - uint8_t G; - uint8_t B; +private: + std::string m_str; +}; - static Color GetAccentColor(); - }; +// 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); - 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); - } +// Below are mapped to the sequences described here: +// https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences - 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); - } +// Contains the response to a DA1 (Primary Device Attributes) request. +struct PrimaryDeviceAttributes +{ + // Queries the device attributes on creation. + PrimaryDeviceAttributes(std::ostream& outStream, std::istream& inStream); - ConstructedSequence Hyperlink(const std::string& text, const std::string& ref); + // 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, + }; - // Constructs a single SGR sequence with one or more semicolon-separated parameters. - // e.g. Sgr({1, 31}) produces "\x1b[1;31m" — bold + red in one sequence. - // Prefer named constants for single-parameter sequences; use this only when - // a multi-parameter form is required to match specific terminal output exactly. - ConstructedSequence Sgr(std::initializer_list params); - } + // Determines if the given extension is supported. + bool Supports(Extension extension) const; - // 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; - } +private: + uint32_t m_conformanceLevel = 0; + uint64_t m_extensions = 0; +}; - namespace Progress +// 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 { - 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())); - } + uint8_t R; + uint8_t G; + uint8_t B; + }; - inline std::ostream& operator<<(std::ostream& o, const Sequence& s) + 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 { - return (o << s.Get()); - } + None, + Indeterminate, + Normal, + Paused, + Error + }; - // 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{...}. + ConstructedSequence Construct(State state, std::optional percentage = std::nullopt); +} // namespace Progress - inline std::string operator+(const Sequence& lhs, const Sequence& rhs) - { - return std::string{ lhs.Get() } + std::string{ rhs.Get() }; - } +// 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::string operator+(const Sequence& lhs, const std::string& rhs) - { - return std::string{ lhs.Get() } + rhs; - } +inline std::ostream& operator<<(std::ostream& o, const Sequence& s) +{ + return (o << s.Get()); +} - inline std::string operator+(const std::string& lhs, const Sequence& rhs) - { - return lhs + std::string{ rhs.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 char* rhs) - { - return std::string{ lhs.Get() } + rhs; - } +inline std::string operator+(const Sequence& lhs, const Sequence& rhs) +{ + return std::string{lhs.Get()} + std::string{rhs.Get()}; +} - inline std::string operator+(const char* lhs, const Sequence& rhs) - { - return lhs + std::string{ rhs.Get() }; - } +inline std::string operator+(const Sequence& lhs, const std::string& rhs) +{ + return std::string{lhs.Get()} + rhs; +} - // 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::string operator+(const std::string& lhs, const Sequence& rhs) +{ + return lhs + std::string{rhs.Get()}; +} - inline std::wstring operator+(const std::wstring& lhs, const Sequence& rhs) - { - const auto sv = rhs.Get(); - return lhs + std::wstring(sv.begin(), sv.end()); - } +inline std::string operator+(const Sequence& lhs, const char* rhs) +{ + return std::string{lhs.Get()} + rhs; +} - // 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. +inline std::string operator+(const char* lhs, const Sequence& rhs) +{ + return lhs + std::string{rhs.Get()}; +} - template::value>> - inline bool operator==(const T& lhs, std::string_view rhs) - { - return lhs.Get() == rhs; - } +// 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; +} - template::value>> - inline bool operator==(std::string_view lhs, const T& rhs) - { - return lhs == rhs.Get(); - } +inline std::wstring operator+(const std::wstring& lhs, const Sequence& rhs) +{ + const auto sv = rhs.Get(); + return lhs + std::wstring(sv.begin(), sv.end()); +} - template::value>> - inline bool operator==(const T& lhs, const char* rhs) - { - return lhs.Get() == rhs; - } +// 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 char* lhs, const T& rhs) - { - return lhs == rhs.Get(); - } +template ::value>> +inline bool operator==(const T& lhs, std::string_view rhs) +{ + return lhs.Get() == rhs; +} - // 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()); - } +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(); +} + +// 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<> +template <> struct std::formatter : std::formatter { auto format(const wsl::windows::common::vt::Sequence& s, std::format_context& ctx) const @@ -498,7 +514,7 @@ struct std::formatter : std::formatter } }; -template<> +template <> struct std::formatter : std::formatter { auto format(const wsl::windows::common::vt::Sequence& s, std::wformat_context& ctx) const @@ -509,9 +525,8 @@ struct std::formatter : std::format } }; -template<> -struct std::formatter - : std::formatter +template <> +struct std::formatter : std::formatter { auto format(const wsl::windows::common::vt::ConstructedSequence& s, std::format_context& ctx) const { @@ -519,9 +534,8 @@ struct std::formatter } }; -template<> -struct std::formatter - : std::formatter +template <> +struct std::formatter : std::formatter { auto format(const wsl::windows::common::vt::ConstructedSequence& s, std::wformat_context& ctx) const { diff --git a/src/windows/wslc/services/ImageProgressCallback.cpp b/src/windows/wslc/services/ImageProgressCallback.cpp index 9b243043e..b9978cb2f 100644 --- a/src/windows/wslc/services/ImageProgressCallback.cpp +++ b/src/windows/wslc/services/ImageProgressCallback.cpp @@ -25,13 +25,13 @@ auto ImageProgressCallback::MoveToLine(int line) { if (line > 0) { - wprintf(std::format(L"{}", Cursor::Up(line)).c_str()); + wprintf(L"%ls", std::format(L"{}", Cursor::Up(line)).c_str()); } return wil::scope_exit([line = line]() { if (line > 1) { - wprintf(std::format(L"{}", Cursor::Down(line - 1)).c_str()); + wprintf(L"%ls", std::format(L"{}", Cursor::Down(line - 1)).c_str()); } }); } diff --git a/test/windows/wslc/WSLCCLIVTSupportUnitTests.cpp b/test/windows/wslc/WSLCCLIVTSupportUnitTests.cpp index 145c9ffc4..c76b59f20 100644 --- a/test/windows/wslc/WSLCCLIVTSupportUnitTests.cpp +++ b/test/windows/wslc/WSLCCLIVTSupportUnitTests.cpp @@ -389,20 +389,46 @@ class WSLCCLIVTSupportUnitTests TEST_METHOD(VT_PrimaryDeviceAttributes) { - // Feed a synthetic DA1 response: ESC [ ? 62 ; 1 ; 4 c - // Conformance level 62 (VT400), extensions: Columns132 (1), Sixel (4). - // ExtractSequence is exercised indirectly through PrimaryDeviceAttributes. - std::ostringstream out; - std::istringstream in{"\x1b[?62;1;4c"}; + // Clean DA1 response: conformance level 62, extensions Columns132 (1) and Sixel (4). + { + std::ostringstream out; + std::istringstream in{"\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)); + VERIFY_ARE_EQUAL("\x1b[0c", out.str()); + } - PrimaryDeviceAttributes da{out, in}; + // Trailing plain text (e.g. queued user input) must not break parsing. + { + std::ostringstream out; + std::istringstream in{"\x1b[?62;1;4chello"}; + PrimaryDeviceAttributes da{out, in}; + + VERIFY_IS_TRUE(da.Supports(PrimaryDeviceAttributes::Extension::Columns132)); + VERIFY_IS_TRUE(da.Supports(PrimaryDeviceAttributes::Extension::Sixel)); + } - VERIFY_IS_TRUE(da.Supports(PrimaryDeviceAttributes::Extension::Columns132)); - VERIFY_IS_TRUE(da.Supports(PrimaryDeviceAttributes::Extension::Sixel)); - VERIFY_IS_FALSE(da.Supports(PrimaryDeviceAttributes::Extension::PrinterPort)); + // Trailing VT sequence must not corrupt suffix search or result extraction. + { + std::ostringstream out; + std::istringstream in{"\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)); + } - // Verify the DA1 request was sent. - VERIFY_ARE_EQUAL("\x1b[0c", out.str()); + // Empty/malformed response — should not throw, extensions remain unset. + { + std::ostringstream out; + std::istringstream in{""}; + PrimaryDeviceAttributes da{out, in}; + + VERIFY_IS_FALSE(da.Supports(PrimaryDeviceAttributes::Extension::Columns132)); + } } TEST_METHOD(VT_PrimaryDeviceAttributes_Empty) From 4f8704600a6fc9b077a0ab7ed97de3e32adacb23 Mon Sep 17 00:00:00 2001 From: David Bennett Date: Thu, 4 Jun 2026 06:13:04 -0700 Subject: [PATCH 06/14] Copilot header add --- src/windows/common/VTSupport.h | 1 + 1 file changed, 1 insertion(+) diff --git a/src/windows/common/VTSupport.h b/src/windows/common/VTSupport.h index 41900201d..ef040a941 100644 --- a/src/windows/common/VTSupport.h +++ b/src/windows/common/VTSupport.h @@ -16,6 +16,7 @@ Module Name: #pragma once #include +#include #include #include #include From 7b01abacb310b92676162b5d4d4297ddb859ff12 Mon Sep 17 00:00:00 2001 From: David Bennett Date: Thu, 4 Jun 2026 12:10:38 -0700 Subject: [PATCH 07/14] PR feedback update --- src/windows/common/VTSupport.cpp | 12 +++++++++--- src/windows/common/VTSupport.h | 10 ++++++++++ src/windows/wslc/services/ImageProgressCallback.h | 2 ++ test/windows/wslc/e2e/WSLCE2EHelpers.h | 2 +- 4 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/windows/common/VTSupport.cpp b/src/windows/common/VTSupport.cpp index dca8fcd1c..adb0d6c36 100644 --- a/src/windows/common/VTSupport.cpp +++ b/src/windows/common/VTSupport.cpp @@ -18,8 +18,11 @@ Module Name: namespace wsl::windows::common::vt { namespace { - // Extracts a VT sequence, expected one of the form ESCAPE + prefix + result + suffix, returning the result part. - // Any bytes following the suffix (e.g. queued user input) are ignored. + // Extracts a VT sequence of the form ESC + prefix + result + suffix, returning + // the result part. Reads up to s_bufferSize bytes non-blockingly; if the buffer + // fills completely the suffix may not be present, in which case an empty string + // is returned — the same outcome as any other parse failure. Any bytes after + // the suffix (e.g. queued user input) are ignored. std::string ExtractSequence(std::istream& inStream, std::string_view prefix, std::string_view suffix) { // Force discovery of available input @@ -27,8 +30,11 @@ namespace { static constexpr std::streamsize s_bufferSize = 1024; char buffer[s_bufferSize]; + + // readsome() returns at most s_bufferSize, so == s_bufferSize is the full-buffer + // case, not overflow. If the suffix is still within those bytes the parse succeeds + // normally; if not, the suffix-not-found path below returns {}. std::streamsize bytesRead = inStream.readsome(buffer, s_bufferSize); - THROW_HR_IF(E_UNEXPECTED, bytesRead >= s_bufferSize); std::string_view resultView{buffer, static_cast(bytesRead)}; diff --git a/src/windows/common/VTSupport.h b/src/windows/common/VTSupport.h index ef040a941..4e1ce7708 100644 --- a/src/windows/common/VTSupport.h +++ b/src/windows/common/VTSupport.h @@ -179,6 +179,16 @@ struct Sequence { } + // 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; diff --git a/src/windows/wslc/services/ImageProgressCallback.h b/src/windows/wslc/services/ImageProgressCallback.h index 2ede30b17..26e4850b5 100644 --- a/src/windows/wslc/services/ImageProgressCallback.h +++ b/src/windows/wslc/services/ImageProgressCallback.h @@ -14,6 +14,8 @@ Module Name: #pragma once #include "SessionService.h" #include "VTSupport.h" +#include +#include namespace wsl::windows::wslc::services { diff --git a/test/windows/wslc/e2e/WSLCE2EHelpers.h b/test/windows/wslc/e2e/WSLCE2EHelpers.h index 46483026e..d368afd15 100644 --- a/test/windows/wslc/e2e/WSLCE2EHelpers.h +++ b/test/windows/wslc/e2e/WSLCE2EHelpers.h @@ -35,7 +35,7 @@ namespace VT { inline const Sequence CR{"\r"}; // The shell PS1 uses SGR 1;31 (bold + red) in a single sequence. - // Format::Sgr({1, 31}) produces "\x1b[1;31m" to match exactly. + // Sgr({1, 31}) produces "\x1b[1;31m" to match exactly. inline const ConstructedSequence RED = Sgr({1, 31}); // Prompt pattern used in WSLC TTY sessions. From 5531262721130fd7fe9cd6064d1c1dc8fdb1e98e Mon Sep 17 00:00:00 2001 From: David Bennett Date: Thu, 4 Jun 2026 13:54:07 -0700 Subject: [PATCH 08/14] Fix erase line forward default parameter --- src/windows/common/VTSupport.cpp | 2 +- test/windows/wslc/WSLCCLIVTSupportUnitTests.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/windows/common/VTSupport.cpp b/src/windows/common/VTSupport.cpp index adb0d6c36..1ac337805 100644 --- a/src/windows/common/VTSupport.cpp +++ b/src/windows/common/VTSupport.cpp @@ -287,7 +287,7 @@ namespace Format { } // namespace Format namespace Erase { - const Sequence LineForward{WSL_WINDOWS_VT_CSI "0K"}; + const Sequence LineForward{WSL_WINDOWS_VT_CSI "K"}; // ESC[K (0 is the default parameter, omitted to match terminal output) const Sequence LineBackward{WSL_WINDOWS_VT_CSI "1K"}; const Sequence LineEntirely{WSL_WINDOWS_VT_CSI "2K"}; const Sequence ScreenForward{WSL_WINDOWS_VT_CSI "J"}; diff --git a/test/windows/wslc/WSLCCLIVTSupportUnitTests.cpp b/test/windows/wslc/WSLCCLIVTSupportUnitTests.cpp index c76b59f20..39c12daf0 100644 --- a/test/windows/wslc/WSLCCLIVTSupportUnitTests.cpp +++ b/test/windows/wslc/WSLCCLIVTSupportUnitTests.cpp @@ -167,7 +167,7 @@ class WSLCCLIVTSupportUnitTests TEST_METHOD(VT_EraseSequences) { - VERIFY_ARE_EQUAL("\x1b[0K", Erase::LineForward); + 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); From bf4833431871cd13532b622b328f43ffaa2bea4b Mon Sep 17 00:00:00 2001 From: David Bennett Date: Fri, 5 Jun 2026 13:34:25 -0700 Subject: [PATCH 09/14] copilot feedback fixes --- src/windows/common/VTSupport.cpp | 37 +++++-------------- src/windows/common/VTSupport.h | 3 -- .../wslc/services/ImageProgressCallback.cpp | 18 ++++++--- .../wslc/services/ImageProgressCallback.h | 6 ++- .../wslc/WSLCCLIVTSupportUnitTests.cpp | 2 +- 5 files changed, 27 insertions(+), 39 deletions(-) diff --git a/src/windows/common/VTSupport.cpp b/src/windows/common/VTSupport.cpp index 1ac337805..49d015ae2 100644 --- a/src/windows/common/VTSupport.cpp +++ b/src/windows/common/VTSupport.cpp @@ -16,6 +16,11 @@ Module Name: #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" + namespace wsl::windows::common::vt { namespace { // Extracts a VT sequence of the form ESC + prefix + result + suffix, returning @@ -80,18 +85,6 @@ void ConstructedSequence::Clear() Set(m_str); } -} // namespace wsl::windows::common::vt - -// The beginning of a Control Sequence Introducer -#define WSL_WINDOWS_VT_CSI WSL_WINDOWS_VT_ESCAPE "[" - -// The beginning of an Operating system command -#define WSL_WINDOWS_VT_OSC WSL_WINDOWS_VT_ESCAPE "]" - -// Define a text formatting sequence with an integer id -#define WSL_WINDOWS_VT_TEXTFORMAT(_id_) WSL_WINDOWS_VT_CSI #_id_ "m" - -namespace wsl::windows::common::vt { ConstructedSequence Sgr(std::initializer_list params) { std::ostringstream result; @@ -160,41 +153,31 @@ namespace Cursor { ConstructedSequence Up(int cells) { THROW_HR_IF(E_INVALIDARG, cells < 0); - std::ostringstream result; - result << WSL_WINDOWS_VT_CSI << cells << 'A'; - return ConstructedSequence{std::move(result).str()}; + return ConstructedSequence{std::format(WSL_WINDOWS_VT_CSI "{}A", cells)}; } ConstructedSequence Down(int cells) { THROW_HR_IF(E_INVALIDARG, cells < 0); - std::ostringstream result; - result << WSL_WINDOWS_VT_CSI << cells << 'B'; - return ConstructedSequence{std::move(result).str()}; + return ConstructedSequence{std::format(WSL_WINDOWS_VT_CSI "{}B", cells)}; } ConstructedSequence Forward(int cells) { THROW_HR_IF(E_INVALIDARG, cells < 0); - std::ostringstream result; - result << WSL_WINDOWS_VT_CSI << cells << 'C'; - return ConstructedSequence{std::move(result).str()}; + return ConstructedSequence{std::format(WSL_WINDOWS_VT_CSI "{}C", cells)}; } ConstructedSequence Backward(int cells) { THROW_HR_IF(E_INVALIDARG, cells < 0); - std::ostringstream result; - result << WSL_WINDOWS_VT_CSI << cells << 'D'; - return ConstructedSequence{std::move(result).str()}; + 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); - std::ostringstream result; - result << WSL_WINDOWS_VT_CSI << row << ';' << col << 'H'; - return ConstructedSequence{std::move(result).str()}; + return ConstructedSequence{std::format(WSL_WINDOWS_VT_CSI "{};{}H", row, col)}; } const Sequence Home{WSL_WINDOWS_VT_CSI "H"}; diff --git a/src/windows/common/VTSupport.h b/src/windows/common/VTSupport.h index 4e1ce7708..623616219 100644 --- a/src/windows/common/VTSupport.h +++ b/src/windows/common/VTSupport.h @@ -25,9 +25,6 @@ Module Name: #include #include "wslutil.h" -// The escape character that begins all VT sequences -#define WSL_WINDOWS_VT_ESCAPE "\x1b" - namespace wsl::windows::common::vt { // Get the integral value for an enum. diff --git a/src/windows/wslc/services/ImageProgressCallback.cpp b/src/windows/wslc/services/ImageProgressCallback.cpp index b9978cb2f..894324834 100644 --- a/src/windows/wslc/services/ImageProgressCallback.cpp +++ b/src/windows/wslc/services/ImageProgressCallback.cpp @@ -21,17 +21,23 @@ namespace wsl::windows::wslc::services { using namespace wsl::shared; using namespace wsl::windows::common::vt; +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"%ls", std::format(L"{}", Cursor::Up(line)).c_str()); + WriteTerminal(ToWide(Cursor::Up(line))); } - return wil::scope_exit([line = line]() { + return wil::scope_exit([line = line, this]() { if (line > 1) { - wprintf(L"%ls", std::format(L"{}", Cursor::Down(line - 1)).c_str()); + WriteTerminal(ToWide(Cursor::Down(line - 1))); } }); } @@ -47,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; } @@ -59,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; diff --git a/src/windows/wslc/services/ImageProgressCallback.h b/src/windows/wslc/services/ImageProgressCallback.h index 26e4850b5..996e65681 100644 --- a/src/windows/wslc/services/ImageProgressCallback.h +++ b/src/windows/wslc/services/ImageProgressCallback.h @@ -29,10 +29,12 @@ class DECLSPEC_UUID("7A1D3376-835A-471A-8DC9-23653D9962D0") ImageProgressCallbac private: 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; int m_currentLine = 0; - wsl::windows::common::vt::EnableVirtualTerminal m_vtMode{GetStdHandle(STD_OUTPUT_HANDLE)}; - wsl::windows::common::vt::ChangeTerminalMode m_terminalMode{GetStdHandle(STD_OUTPUT_HANDLE), false}; + 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 diff --git a/test/windows/wslc/WSLCCLIVTSupportUnitTests.cpp b/test/windows/wslc/WSLCCLIVTSupportUnitTests.cpp index 39c12daf0..5d50c65fc 100644 --- a/test/windows/wslc/WSLCCLIVTSupportUnitTests.cpp +++ b/test/windows/wslc/WSLCCLIVTSupportUnitTests.cpp @@ -319,7 +319,7 @@ class WSLCCLIVTSupportUnitTests DWORD baseline{}; VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleMode(h, &baseline)); - SetConsoleMode(h, baseline & ~ENABLE_VIRTUAL_TERMINAL_PROCESSING); + VERIFY_WIN32_BOOL_SUCCEEDED(SetConsoleMode(h, baseline & ~ENABLE_VIRTUAL_TERMINAL_PROCESSING)); { EnableVirtualTerminal vt{h}; From d4195757d289ed7b00aff19353dcc2ae6537b143 Mon Sep 17 00:00:00 2001 From: David Bennett Date: Fri, 5 Jun 2026 13:47:19 -0700 Subject: [PATCH 10/14] Update peek comment --- src/windows/common/VTSupport.cpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/windows/common/VTSupport.cpp b/src/windows/common/VTSupport.cpp index 49d015ae2..3b5b25c7e 100644 --- a/src/windows/common/VTSupport.cpp +++ b/src/windows/common/VTSupport.cpp @@ -24,10 +24,18 @@ Module Name: namespace wsl::windows::common::vt { namespace { // Extracts a VT sequence of the form ESC + prefix + result + suffix, returning - // the result part. Reads up to s_bufferSize bytes non-blockingly; if the buffer + // the result part. Reads up to s_bufferSize bytes 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 string // is returned — the same outcome as any other parse failure. Any bytes after // the suffix (e.g. queued user input) are ignored. + // + // 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, stringstreams) + // have data already buffered by the caller. std::string ExtractSequence(std::istream& inStream, std::string_view prefix, std::string_view suffix) { // Force discovery of available input From 4457b0870a2df76181a0a1162558f226926b5b7e Mon Sep 17 00:00:00 2001 From: David Bennett Date: Fri, 5 Jun 2026 15:04:40 -0700 Subject: [PATCH 11/14] More copilot feedback --- src/windows/common/VTSupport.h | 40 ++++++++++++++++--- .../wslc/services/BuildImageCallback.cpp | 24 ++++++----- .../wslc/services/BuildImageCallback.h | 3 ++ 3 files changed, 51 insertions(+), 16 deletions(-) diff --git a/src/windows/common/VTSupport.h b/src/windows/common/VTSupport.h index 623616219..5f08dc560 100644 --- a/src/windows/common/VTSupport.h +++ b/src/windows/common/VTSupport.h @@ -125,23 +125,41 @@ class EnableVirtualTerminal } else { - const DWORD preferredFlags = ENABLE_VIRTUAL_TERMINAL_PROCESSING | (disableNewlineAutoReturn ? DISABLE_NEWLINE_AUTO_RETURN : 0); - - for (DWORD flags : {preferredFlags, static_cast(ENABLE_VIRTUAL_TERMINAL_PROCESSING)}) - { + // 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) { - return; + // 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; + 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); } } @@ -502,6 +520,16 @@ 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) diff --git a/src/windows/wslc/services/BuildImageCallback.cpp b/src/windows/wslc/services/BuildImageCallback.cpp index e9ff6a771..83d431738 100644 --- a/src/windows/wslc/services/BuildImageCallback.cpp +++ b/src/windows/wslc/services/BuildImageCallback.cpp @@ -186,10 +186,6 @@ 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 int consoleWidth = std::max(0, static_cast(info.srWindow.Right) - info.srWindow.Left); const bool showPending = !m_pendingLine.empty(); @@ -206,15 +202,20 @@ void BuildImageCallback::Redraw() // 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::wostringstream buffer; - buffer << Cursor::Hide << Format::Dim; + // + // 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 << Cursor::Up(m_displayedLines) << Erase::ScreenForward; + m_frameBuffer += Cursor::Up(m_displayedLines); + m_frameBuffer += Erase::ScreenForward; } auto appendLine = [&](const std::string& line) { @@ -223,7 +224,9 @@ void BuildImageCallback::Redraw() { wline.resize(static_cast(consoleWidth)); } - buffer << wline << Erase::LineForward << L'\n'; + 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). @@ -249,9 +252,10 @@ void BuildImageCallback::Redraw() appendLine(line); } - buffer << Format::Normal << Cursor::Show; + m_frameBuffer += Format::Normal; + m_frameBuffer += Cursor::Show; - WriteTerminal(buffer.str()); + WriteTerminal(m_frameBuffer); m_displayedLines = displayCount; } diff --git a/src/windows/wslc/services/BuildImageCallback.h b/src/windows/wslc/services/BuildImageCallback.h index e7ba2305b..2067e2525 100644 --- a/src/windows/wslc/services/BuildImageCallback.h +++ b/src/windows/wslc/services/BuildImageCallback.h @@ -58,6 +58,9 @@ class DECLSPEC_UUID("3EDD5DBF-CA6C-4CF7-923A-AD94B6A732E5") BuildImageCallback 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(); }; From bfc038121cd7aa64cb4d5189faa7cc07ba869166 Mon Sep 17 00:00:00 2001 From: David Bennett Date: Sat, 6 Jun 2026 14:09:22 -0700 Subject: [PATCH 12/14] Update for wide output --- src/windows/common/VTSupport.cpp | 75 +++++++++++-------- src/windows/common/VTSupport.h | 5 +- .../wslc/WSLCCLIVTSupportUnitTests.cpp | 23 +++--- 3 files changed, 61 insertions(+), 42 deletions(-) diff --git a/src/windows/common/VTSupport.cpp b/src/windows/common/VTSupport.cpp index 3b5b25c7e..b28a0e251 100644 --- a/src/windows/common/VTSupport.cpp +++ b/src/windows/common/VTSupport.cpp @@ -21,39 +21,50 @@ Module Name: #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, returning - // the result part. Reads up to s_bufferSize bytes 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 string - // is returned — the same outcome as any other parse failure. Any bytes after + // 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, stringstreams) + // conhost.exe) respond to ESC[0c, and non-console streams (pipes, wstringstreams) // have data already buffered by the caller. - std::string ExtractSequence(std::istream& inStream, std::string_view prefix, std::string_view suffix) + std::wstring ExtractSequence(std::wistream& inStream, std::wstring_view prefix, std::wstring_view suffix) { - // Force discovery of available input + // Force discovery of available input. std::ignore = inStream.peek(); static constexpr std::streamsize s_bufferSize = 1024; - char buffer[s_bufferSize]; + wchar_t buffer[s_bufferSize]; - // readsome() returns at most s_bufferSize, so == s_bufferSize is the full-buffer - // case, not overflow. If the suffix is still within those bytes the parse succeeds - // normally; if not, the suffix-not-found path below returns {}. - std::streamsize bytesRead = inStream.readsome(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::string_view resultView{buffer, static_cast(bytesRead)}; + std::wstring_view resultView{buffer, static_cast(charsRead)}; // Locate the escape character that begins the sequence. - const size_t escapeIndex = resultView.find(WSL_WINDOWS_VT_ESCAPE[0]); - if (escapeIndex == std::string_view::npos) + const size_t escapeIndex = resultView.find(L'\x1b'); + if (escapeIndex == std::wstring_view::npos) { return {}; } @@ -67,14 +78,14 @@ namespace { } // Find the suffix anywhere after the prefix. - const std::string_view body = resultView.substr(1 + prefix.length()); + const std::wstring_view body = resultView.substr(1 + prefix.length()); const size_t suffixIndex = body.find(suffix); - if (suffixIndex == std::string_view::npos) + if (suffixIndex == std::wstring_view::npos) { return {}; } - return std::string{body.substr(0, suffixIndex)}; + return std::wstring{body.substr(0, suffixIndex)}; } } // namespace @@ -111,28 +122,32 @@ ConstructedSequence Sgr(std::initializer_list params) return ConstructedSequence{std::move(result).str()}; } -PrimaryDeviceAttributes::PrimaryDeviceAttributes(std::ostream& outStream, std::istream& inStream) +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 stringstreams) this will fail, but + // 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 - outStream << WSL_WINDOWS_VT_CSI << "0c"; + // 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 WSL_WINDOWS_VT_CSI ? ; ( ;)* c - std::string sequence = ExtractSequence(inStream, "[?", "c"); - std::vector values = wsl::shared::string::Split(sequence, ';'); + // 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()) { - m_conformanceLevel = std::stoul(values[0]); + // 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 @@ -141,7 +156,7 @@ PrimaryDeviceAttributes::PrimaryDeviceAttributes(std::ostream& outStream, std::i constexpr unsigned long c_maxExtensionBit = 63ul; for (size_t i = 1; i < values.size(); ++i) { - const unsigned long ext = std::stoul(values[i]); + const unsigned long ext = std::wcstoul(values[i].c_str(), nullptr, 10); if (ext <= c_maxExtensionBit) { m_extensions |= 1ull << ext; @@ -278,7 +293,7 @@ namespace Format { } // namespace Format namespace Erase { - const Sequence LineForward{WSL_WINDOWS_VT_CSI "K"}; // ESC[K (0 is the default parameter, omitted to match terminal output) + 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"}; diff --git a/src/windows/common/VTSupport.h b/src/windows/common/VTSupport.h index 5f08dc560..9bc9780ce 100644 --- a/src/windows/common/VTSupport.h +++ b/src/windows/common/VTSupport.h @@ -275,7 +275,10 @@ ConstructedSequence Sgr(std::initializer_list params); struct PrimaryDeviceAttributes { // Queries the device attributes on creation. - PrimaryDeviceAttributes(std::ostream& outStream, std::istream& inStream); + // 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 diff --git a/test/windows/wslc/WSLCCLIVTSupportUnitTests.cpp b/test/windows/wslc/WSLCCLIVTSupportUnitTests.cpp index 5d50c65fc..4935d8f38 100644 --- a/test/windows/wslc/WSLCCLIVTSupportUnitTests.cpp +++ b/test/windows/wslc/WSLCCLIVTSupportUnitTests.cpp @@ -391,20 +391,21 @@ class WSLCCLIVTSupportUnitTests { // Clean DA1 response: conformance level 62, extensions Columns132 (1) and Sixel (4). { - std::ostringstream out; - std::istringstream in{"\x1b[?62;1;4c"}; + 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)); - VERIFY_ARE_EQUAL("\x1b[0c", out.str()); + // 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::ostringstream out; - std::istringstream in{"\x1b[?62;1;4chello"}; + std::wostringstream out; + std::wistringstream in{L"\x1b[?62;1;4chello"}; PrimaryDeviceAttributes da{out, in}; VERIFY_IS_TRUE(da.Supports(PrimaryDeviceAttributes::Extension::Columns132)); @@ -413,8 +414,8 @@ class WSLCCLIVTSupportUnitTests // Trailing VT sequence must not corrupt suffix search or result extraction. { - std::ostringstream out; - std::istringstream in{"\x1b[?62;6c\x1b[0m"}; + std::wostringstream out; + std::wistringstream in{L"\x1b[?62;6c\x1b[0m"}; PrimaryDeviceAttributes da{out, in}; VERIFY_IS_TRUE(da.Supports(PrimaryDeviceAttributes::Extension::SelectiveErase)); @@ -423,8 +424,8 @@ class WSLCCLIVTSupportUnitTests // Empty/malformed response — should not throw, extensions remain unset. { - std::ostringstream out; - std::istringstream in{""}; + std::wostringstream out; + std::wistringstream in{L""}; PrimaryDeviceAttributes da{out, in}; VERIFY_IS_FALSE(da.Supports(PrimaryDeviceAttributes::Extension::Columns132)); @@ -434,8 +435,8 @@ class WSLCCLIVTSupportUnitTests TEST_METHOD(VT_PrimaryDeviceAttributes_Empty) { // Empty/malformed response — should not throw, extensions remain unset. - std::ostringstream out; - std::istringstream in{""}; + std::wostringstream out; + std::wistringstream in{L""}; PrimaryDeviceAttributes da{out, in}; From 08a767ca35eb236e4bd9324bd73d2aefe990cb15 Mon Sep 17 00:00:00 2001 From: David Bennett Date: Mon, 8 Jun 2026 03:12:10 -0700 Subject: [PATCH 13/14] Update for color detection, fix feedback item --- src/windows/common/VTSupport.cpp | 23 ++++++++++++++++ src/windows/common/VTSupport.h | 4 +++ .../wslc/services/ImageProgressCallback.h | 2 +- .../wslc/WSLCCLIVTSupportUnitTests.cpp | 26 +++++++++++++++++++ 4 files changed, 54 insertions(+), 1 deletion(-) diff --git a/src/windows/common/VTSupport.cpp b/src/windows/common/VTSupport.cpp index b28a0e251..16d63e6bb 100644 --- a/src/windows/common/VTSupport.cpp +++ b/src/windows/common/VTSupport.cpp @@ -89,6 +89,29 @@ namespace { } } // 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()) diff --git a/src/windows/common/VTSupport.h b/src/windows/common/VTSupport.h index 9bc9780ce..a7f1975eb 100644 --- a/src/windows/common/VTSupport.h +++ b/src/windows/common/VTSupport.h @@ -209,6 +209,10 @@ struct Sequence 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) { diff --git a/src/windows/wslc/services/ImageProgressCallback.h b/src/windows/wslc/services/ImageProgressCallback.h index 996e65681..f4bcc7b42 100644 --- a/src/windows/wslc/services/ImageProgressCallback.h +++ b/src/windows/wslc/services/ImageProgressCallback.h @@ -24,10 +24,10 @@ class DECLSPEC_UUID("7A1D3376-835A-471A-8DC9-23653D9962D0") ImageProgressCallbac : public Microsoft::WRL::RuntimeClass, IProgressCallback, IFastRundown> { public: - auto MoveToLine(int 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); diff --git a/test/windows/wslc/WSLCCLIVTSupportUnitTests.cpp b/test/windows/wslc/WSLCCLIVTSupportUnitTests.cpp index 4935d8f38..94ccaf7b5 100644 --- a/test/windows/wslc/WSLCCLIVTSupportUnitTests.cpp +++ b/test/windows/wslc/WSLCCLIVTSupportUnitTests.cpp @@ -191,6 +191,32 @@ class WSLCCLIVTSupportUnitTests }); } + 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 From 4850570ab67623086b0845bdcc189752c04b3abf Mon Sep 17 00:00:00 2001 From: David Bennett Date: Mon, 8 Jun 2026 03:28:22 -0700 Subject: [PATCH 14/14] Copilot review adjustments --- src/windows/common/VTSupport.cpp | 3 --- src/windows/common/VTSupport.h | 5 +++++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/windows/common/VTSupport.cpp b/src/windows/common/VTSupport.cpp index 16d63e6bb..b8f3c4316 100644 --- a/src/windows/common/VTSupport.cpp +++ b/src/windows/common/VTSupport.cpp @@ -227,9 +227,6 @@ namespace Cursor { } const Sequence Home{WSL_WINDOWS_VT_CSI "H"}; - const Sequence SavePos{WSL_WINDOWS_VT_CSI "s"}; - const Sequence RestorePos{WSL_WINDOWS_VT_CSI "u"}; - const Sequence EnableBlink{WSL_WINDOWS_VT_CSI "?12h"}; const Sequence DisableBlink{WSL_WINDOWS_VT_CSI "?12l"}; const Sequence Show{WSL_WINDOWS_VT_CSI "?25h"}; diff --git a/src/windows/common/VTSupport.h b/src/windows/common/VTSupport.h index a7f1975eb..895e97635 100644 --- a/src/windows/common/VTSupport.h +++ b/src/windows/common/VTSupport.h @@ -230,6 +230,7 @@ struct ConstructedSequence : public Sequence { Set(m_str); } + explicit ConstructedSequence(std::string s) : m_str(std::move(s)) { Set(m_str); @@ -239,6 +240,7 @@ struct ConstructedSequence : public Sequence { Set(m_str); } + ConstructedSequence& operator=(const ConstructedSequence& other) { m_str = other.m_str; @@ -249,11 +251,14 @@ struct ConstructedSequence : public Sequence 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; }