@samitouri / QOSAMI-WSL / commits / 59e8c1e4

CLI: Add VT Support library and update tests/callbacks to use it (#40710)

David Bennett committed Jun 12, 2026 at 12:47 UTC 59e8c1e400dfb18797279e1b0e85629d2acc6747
12 files changed +1600 -179
src/windows/common/CMakeLists.txt
+2
@@ -35,6 +35,7 @@ set(SOURCES
35 string.cpp
36 SubProcess.cpp
37 svccomm.cpp
38 + VTSupport.cpp
39 WindowsUpdateIntegration.cpp
40 WSLCContainerLauncher.cpp
41 VirtioNetworking.cpp
@@ -120,6 +121,7 @@ set(HEADERS
121 Stringify.h
122 SubProcess.h
123 svccomm.hpp
124 + VTSupport.h
125 WindowsUpdateIntegration.h
126 WSLCContainerLauncher.h
127 VirtioNetworking.h
src/windows/common/VTSupport.cpp new
+546
@@ -0,0 +1,546 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + VTSupport.cpp
8 +
9 +Abstract:
10 +
11 + This file contains the implementation of VT sequence constructors and
12 + console mode helpers declared in VTSupport.h.
13 +
14 +--*/
15 +
16 +#include "precomp.h"
17 +#include "VTSupport.h"
18 +
19 +#define WSL_WINDOWS_VT_ESCAPE L"\x1b"
20 +#define WSL_WINDOWS_VT_CSI WSL_WINDOWS_VT_ESCAPE L"["
21 +#define WSL_WINDOWS_VT_OSC WSL_WINDOWS_VT_ESCAPE L"]"
22 +
23 +// Two-level macro so the L prefix is pasted to the stringified token
24 +// before adjacent-string-literal concatenation kicks in. Without the inner
25 +// helper, `L ## #_id_` would try to token-paste L onto a string literal,
26 +// which is not a valid preprocessing token under MSVC's conforming mode.
27 +#define WSL_WINDOWS_VT_WIDEN_INNER(_s_) L##_s_
28 +#define WSL_WINDOWS_VT_WIDEN(_s_) WSL_WINDOWS_VT_WIDEN_INNER(_s_)
29 +#define WSL_WINDOWS_VT_TEXTFORMAT(_id_) WSL_WINDOWS_VT_CSI WSL_WINDOWS_VT_WIDEN(#_id_) L"m"
30 +
31 +namespace wsl::windows::common::vt {
32 +namespace {
33 + std::wstring ExtractSequence(std::wistream& inStream, std::wstring_view prefix, std::wstring_view suffix)
34 + {
35 + (void)inStream.peek();
36 +
37 + static constexpr std::streamsize s_bufferSize = 1024;
38 + wchar_t buffer[s_bufferSize];
39 +
40 + std::streamsize charsRead = inStream.readsome(buffer, s_bufferSize);
41 +
42 + std::wstring_view resultView{buffer, static_cast<size_t>(charsRead)};
43 +
44 + const size_t escapeIndex = resultView.find(L'\x1b');
45 + if (escapeIndex == std::wstring_view::npos)
46 + {
47 + return {};
48 + }
49 +
50 + resultView = resultView.substr(escapeIndex);
51 +
52 + if (resultView.length() < 1 + prefix.length() || resultView.substr(1, prefix.length()) != prefix)
53 + {
54 + return {};
55 + }
56 +
57 + const std::wstring_view body = resultView.substr(1 + prefix.length());
58 + const size_t suffixIndex = body.find(suffix);
59 + if (suffixIndex == std::wstring_view::npos)
60 + {
61 + return {};
62 + }
63 +
64 + return std::wstring{body.substr(0, suffixIndex)};
65 + }
66 +} // namespace
67 +
68 +ChangeTerminalMode::ChangeTerminalMode(HANDLE console, bool cursorVisible) : m_console(console)
69 +{
70 + if (!wsl::windows::common::wslutil::IsConsoleHandle(console))
71 + {
72 + m_console = nullptr;
73 + return;
74 + }
75 +
76 + THROW_IF_WIN32_BOOL_FALSE(GetConsoleCursorInfo(console, &m_originalCursorInfo));
77 + CONSOLE_CURSOR_INFO newCursorInfo = m_originalCursorInfo;
78 + newCursorInfo.bVisible = cursorVisible;
79 + THROW_IF_WIN32_BOOL_FALSE(SetConsoleCursorInfo(console, &newCursorInfo));
80 +}
81 +
82 +ChangeTerminalMode::~ChangeTerminalMode()
83 +{
84 + if (m_console)
85 + {
86 + LOG_IF_WIN32_BOOL_FALSE(SetConsoleCursorInfo(m_console, &m_originalCursorInfo));
87 + }
88 +}
89 +
90 +bool ChangeTerminalMode::IsConsole() const
91 +{
92 + return m_console != nullptr;
93 +}
94 +
95 +EnableVirtualTerminal::EnableVirtualTerminal(HANDLE console, Mode mode, bool disableNewlineAutoReturn)
96 +{
97 + DWORD current;
98 + if (!GetConsoleMode(console, &current))
99 + {
100 + LOG_LAST_ERROR_IF(GetLastError() != ERROR_INVALID_HANDLE);
101 + return;
102 + }
103 +
104 + if (mode == Mode::Input)
105 + {
106 + const DWORD newMode = (current & ~(ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT)) | ENABLE_EXTENDED_FLAGS | ENABLE_VIRTUAL_TERMINAL_INPUT;
107 + if (newMode == current)
108 + {
109 + // Already in the desired mode; nothing to restore, but VT input is
110 + // still enabled on the handle.
111 + m_vtEnabled = (current & ENABLE_VIRTUAL_TERMINAL_INPUT) != 0;
112 + return;
113 + }
114 +
115 + if (SetConsoleMode(console, newMode))
116 + {
117 + m_console = console;
118 + m_originalMode = current;
119 + m_vtEnabled = true;
120 + }
121 + else
122 + {
123 + LOG_LAST_ERROR_IF(GetLastError() != ERROR_INVALID_PARAMETER);
124 + }
125 + }
126 + else
127 + {
128 + auto tryEnable = [&](DWORD flags) -> bool {
129 + const DWORD newMode = current | flags;
130 + if (newMode == current)
131 + {
132 + // Flags already set; no mode change needed and nothing to restore,
133 + // but VT processing is already enabled on the handle.
134 + m_vtEnabled = true;
135 + return true;
136 + }
137 +
138 + if (SetConsoleMode(console, newMode))
139 + {
140 + m_console = console;
141 + m_originalMode = current;
142 + m_vtEnabled = true;
143 + return true;
144 + }
145 +
146 + LOG_LAST_ERROR_IF(GetLastError() != ERROR_INVALID_PARAMETER);
147 + return false;
148 + };
149 +
150 + if (disableNewlineAutoReturn && tryEnable(ENABLE_VIRTUAL_TERMINAL_PROCESSING | DISABLE_NEWLINE_AUTO_RETURN))
151 + {
152 + return;
153 + }
154 +
155 + tryEnable(ENABLE_VIRTUAL_TERMINAL_PROCESSING);
156 + }
157 +}
158 +
159 +EnableVirtualTerminal::~EnableVirtualTerminal()
160 +{
161 + if (m_console)
162 + {
163 + LOG_IF_WIN32_BOOL_FALSE(SetConsoleMode(m_console, m_originalMode));
164 + }
165 +}
166 +
167 +bool EnableVirtualTerminal::IsVTEnabled() const
168 +{
169 + return m_vtEnabled;
170 +}
171 +
172 +ConstructedSequence::ConstructedSequence()
173 +{
174 + Set(m_str);
175 +}
176 +
177 +ConstructedSequence::ConstructedSequence(std::wstring s) : m_str(std::move(s))
178 +{
179 + Set(m_str);
180 +}
181 +
182 +ConstructedSequence::ConstructedSequence(const ConstructedSequence& other) : m_str(other.m_str)
183 +{
184 + Set(m_str);
185 +}
186 +
187 +ConstructedSequence& ConstructedSequence::operator=(const ConstructedSequence& other)
188 +{
189 + m_str = other.m_str;
190 + Set(m_str);
191 + return *this;
192 +}
193 +
194 +ConstructedSequence::ConstructedSequence(ConstructedSequence&& other) noexcept : m_str(std::move(other.m_str))
195 +{
196 + Set(m_str);
197 + other.Set(other.m_str);
198 +}
199 +
200 +ConstructedSequence& ConstructedSequence::operator=(ConstructedSequence&& other) noexcept
201 +{
202 + m_str = std::move(other.m_str);
203 + Set(m_str);
204 + other.Set(other.m_str);
205 + return *this;
206 +}
207 +
208 +bool Sequence::IsColor() const
209 +{
210 + const auto sv = m_chars;
211 + if (sv.size() < 2 || sv[0] != L'\x1b')
212 + {
213 + return false;
214 + }
215 +
216 + if (sv[1] == L'[')
217 + {
218 + // CSI sequence — color if final byte is 'm' (SGR)
219 + return sv.back() == L'm';
220 + }
221 +
222 + if (sv[1] == L']')
223 + {
224 + // OSC 8 hyperlink — treated as color-adjacent
225 + return sv.size() >= 3 && sv[2] == L'8';
226 + }
227 +
228 + return false;
229 +}
230 +
231 +void ConstructedSequence::Append(const Sequence& sequence)
232 +{
233 + if (!sequence.Get().empty())
234 + {
235 + m_str += sequence.Get();
236 + Set(m_str);
237 + }
238 +}
239 +
240 +void ConstructedSequence::Clear()
241 +{
242 + m_str.clear();
243 + Set(m_str);
244 +}
245 +
246 +ConstructedSequence Sgr(std::initializer_list<int> params)
247 +{
248 + std::wostringstream result;
249 + result << WSL_WINDOWS_VT_CSI;
250 + bool first = true;
251 + for (const int param : params)
252 + {
253 + if (!first)
254 + {
255 + result << L';';
256 + }
257 + result << param;
258 + first = false;
259 + }
260 + result << L'm';
261 + return ConstructedSequence{std::move(result).str()};
262 +}
263 +
264 +PrimaryDeviceAttributes::PrimaryDeviceAttributes(std::wostream& outStream, std::wistream& inStream)
265 +{
266 + try
267 + {
268 + // Best-effort: enable VT input on the real console handle so the terminal
269 + // sends a machine-readable DA1 response. When stdin is redirected (e.g.
270 + // in unit tests that supply their own wstringstreams) this will fail, but
271 + // we still proceed — the caller is responsible for providing a readable
272 + // inStream that contains the DA1 response.
273 + EnableVirtualTerminal inputMode{GetStdHandle(STD_INPUT_HANDLE), EnableVirtualTerminal::Mode::Input};
274 +
275 + // Send DA1 Primary Device Attributes request.
276 + outStream << WSL_WINDOWS_VT_CSI L"0c";
277 + outStream.flush();
278 +
279 + // Response is of the form ESC[?<conformance level>;<extension>...c
280 + // Split returns std::vector<std::wstring> via the wstring_view template overload.
281 + std::wstring sequence = ExtractSequence(inStream, L"[?", L"c");
282 + std::vector<std::wstring> values = wsl::shared::string::Split(sequence, L';');
283 +
284 + if (!values.empty())
285 + {
286 + // Use wcstoul so the wchar_t digits are parsed directly without any
287 + // narrowing conversion.
288 + m_conformanceLevel = std::wcstoul(values[0].c_str(), nullptr, 10);
289 + }
290 +
291 + // m_extensions is a uint64_t bitmask; extension values >= 64 cannot be
292 + // represented and are silently ignored to avoid undefined behaviour from
293 + // an out-of-range shift.
294 + constexpr unsigned long c_maxExtensionBit = 63ul;
295 + for (size_t i = 1; i < values.size(); ++i)
296 + {
297 + const unsigned long ext = std::wcstoul(values[i].c_str(), nullptr, 10);
298 + if (ext <= c_maxExtensionBit)
299 + {
300 + m_extensions |= 1ull << ext;
301 + }
302 + }
303 + }
304 + CATCH_LOG();
305 +}
306 +
307 +bool PrimaryDeviceAttributes::Supports(Extension extension) const
308 +{
309 + uint64_t extensionMask = 1ull << ToIntegral(extension);
310 + return (m_extensions & extensionMask) == extensionMask;
311 +}
312 +
313 +namespace Cursor {
314 + ConstructedSequence Up(int cells)
315 + {
316 + THROW_HR_IF(E_INVALIDARG, cells < 0);
317 + if (cells == 0)
318 + {
319 + return ConstructedSequence{};
320 + }
321 + return ConstructedSequence{std::format(WSL_WINDOWS_VT_CSI L"{}A", cells)};
322 + }
323 +
324 + ConstructedSequence Down(int cells)
325 + {
326 + THROW_HR_IF(E_INVALIDARG, cells < 0);
327 + if (cells == 0)
328 + {
329 + return ConstructedSequence{};
330 + }
331 + return ConstructedSequence{std::format(WSL_WINDOWS_VT_CSI L"{}B", cells)};
332 + }
333 +
334 + ConstructedSequence Forward(int cells)
335 + {
336 + THROW_HR_IF(E_INVALIDARG, cells < 0);
337 + if (cells == 0)
338 + {
339 + return ConstructedSequence{};
340 + }
341 + return ConstructedSequence{std::format(WSL_WINDOWS_VT_CSI L"{}C", cells)};
342 + }
343 +
344 + ConstructedSequence Backward(int cells)
345 + {
346 + THROW_HR_IF(E_INVALIDARG, cells < 0);
347 + if (cells == 0)
348 + {
349 + return ConstructedSequence{};
350 + }
351 + return ConstructedSequence{std::format(WSL_WINDOWS_VT_CSI L"{}D", cells)};
352 + }
353 +
354 + ConstructedSequence MoveTo(int row, int col)
355 + {
356 + THROW_HR_IF(E_INVALIDARG, row < 1 || col < 1);
357 + return ConstructedSequence{std::format(WSL_WINDOWS_VT_CSI L"{};{}H", row, col)};
358 + }
359 +
360 + const Sequence Home{WSL_WINDOWS_VT_CSI L"H"};
361 + const Sequence EnableBlink{WSL_WINDOWS_VT_CSI L"?12h"};
362 + const Sequence DisableBlink{WSL_WINDOWS_VT_CSI L"?12l"};
363 + const Sequence Show{WSL_WINDOWS_VT_CSI L"?25h"};
364 + const Sequence Hide{WSL_WINDOWS_VT_CSI L"?25l"};
365 +
366 + const Sequence BracketedPasteOn{WSL_WINDOWS_VT_CSI L"?2004h"};
367 + const Sequence BracketedPasteOff{WSL_WINDOWS_VT_CSI L"?2004l"};
368 +} // namespace Cursor
369 +
370 +namespace Format {
371 + const Sequence Default{WSL_WINDOWS_VT_TEXTFORMAT(0)};
372 + const Sequence Negative{WSL_WINDOWS_VT_TEXTFORMAT(7)};
373 + const Sequence Bright{WSL_WINDOWS_VT_TEXTFORMAT(1)};
374 + const Sequence Dim{WSL_WINDOWS_VT_TEXTFORMAT(2)};
375 + const Sequence Normal{WSL_WINDOWS_VT_TEXTFORMAT(22)};
376 + const Sequence Italic{WSL_WINDOWS_VT_TEXTFORMAT(3)};
377 + const Sequence NoItalic{WSL_WINDOWS_VT_TEXTFORMAT(23)};
378 + const Sequence Underline{WSL_WINDOWS_VT_TEXTFORMAT(4)};
379 + const Sequence NoUnderline{WSL_WINDOWS_VT_TEXTFORMAT(24)};
380 +
381 + namespace Fg {
382 + const Sequence Black{WSL_WINDOWS_VT_TEXTFORMAT(30)};
383 + const Sequence Red{WSL_WINDOWS_VT_TEXTFORMAT(31)};
384 + const Sequence Green{WSL_WINDOWS_VT_TEXTFORMAT(32)};
385 + const Sequence Yellow{WSL_WINDOWS_VT_TEXTFORMAT(33)};
386 + const Sequence Blue{WSL_WINDOWS_VT_TEXTFORMAT(34)};
387 + const Sequence Magenta{WSL_WINDOWS_VT_TEXTFORMAT(35)};
388 + const Sequence Cyan{WSL_WINDOWS_VT_TEXTFORMAT(36)};
389 + const Sequence White{WSL_WINDOWS_VT_TEXTFORMAT(37)};
390 +
391 + const Sequence BrightBlack{WSL_WINDOWS_VT_TEXTFORMAT(90)};
392 + const Sequence BrightRed{WSL_WINDOWS_VT_TEXTFORMAT(91)};
393 + const Sequence BrightGreen{WSL_WINDOWS_VT_TEXTFORMAT(92)};
394 + const Sequence BrightYellow{WSL_WINDOWS_VT_TEXTFORMAT(93)};
395 + const Sequence BrightBlue{WSL_WINDOWS_VT_TEXTFORMAT(94)};
396 + const Sequence BrightMagenta{WSL_WINDOWS_VT_TEXTFORMAT(95)};
397 + const Sequence BrightCyan{WSL_WINDOWS_VT_TEXTFORMAT(96)};
398 + const Sequence BrightWhite{WSL_WINDOWS_VT_TEXTFORMAT(97)};
399 +
400 + ConstructedSequence Extended(const Color& color)
401 + {
402 + std::wostringstream result;
403 + result << WSL_WINDOWS_VT_CSI L"38;2;" << static_cast<uint32_t>(color.R) << L';' << static_cast<uint32_t>(color.G)
404 + << L';' << static_cast<uint32_t>(color.B) << L'm';
405 + return ConstructedSequence{std::move(result).str()};
406 + }
407 + } // namespace Fg
408 +
409 + namespace Bg {
410 + const Sequence Black{WSL_WINDOWS_VT_TEXTFORMAT(40)};
411 + const Sequence Red{WSL_WINDOWS_VT_TEXTFORMAT(41)};
412 + const Sequence Green{WSL_WINDOWS_VT_TEXTFORMAT(42)};
413 + const Sequence Yellow{WSL_WINDOWS_VT_TEXTFORMAT(43)};
414 + const Sequence Blue{WSL_WINDOWS_VT_TEXTFORMAT(44)};
415 + const Sequence Magenta{WSL_WINDOWS_VT_TEXTFORMAT(45)};
416 + const Sequence Cyan{WSL_WINDOWS_VT_TEXTFORMAT(46)};
417 + const Sequence White{WSL_WINDOWS_VT_TEXTFORMAT(47)};
418 +
419 + const Sequence BrightBlack{WSL_WINDOWS_VT_TEXTFORMAT(100)};
420 + const Sequence BrightRed{WSL_WINDOWS_VT_TEXTFORMAT(101)};
421 + const Sequence BrightGreen{WSL_WINDOWS_VT_TEXTFORMAT(102)};
422 + const Sequence BrightYellow{WSL_WINDOWS_VT_TEXTFORMAT(103)};
423 + const Sequence BrightBlue{WSL_WINDOWS_VT_TEXTFORMAT(104)};
424 + const Sequence BrightMagenta{WSL_WINDOWS_VT_TEXTFORMAT(105)};
425 + const Sequence BrightCyan{WSL_WINDOWS_VT_TEXTFORMAT(106)};
426 + const Sequence BrightWhite{WSL_WINDOWS_VT_TEXTFORMAT(107)};
427 +
428 + ConstructedSequence Extended(const Color& color)
429 + {
430 + std::wostringstream result;
431 + result << WSL_WINDOWS_VT_CSI L"48;2;" << static_cast<uint32_t>(color.R) << L';' << static_cast<uint32_t>(color.G)
432 + << L';' << static_cast<uint32_t>(color.B) << L'm';
433 + return ConstructedSequence{std::move(result).str()};
434 + }
435 + } // namespace Bg
436 +
437 + ConstructedSequence Hyperlink(const std::wstring& text, const std::wstring& ref)
438 + {
439 + std::wostringstream result;
440 + result << WSL_WINDOWS_VT_OSC L"8;;" << ref << WSL_WINDOWS_VT_ESCAPE << L"\\" << text << WSL_WINDOWS_VT_OSC << L"8;;"
441 + << WSL_WINDOWS_VT_ESCAPE << L"\\";
442 + return ConstructedSequence{std::move(result).str()};
443 + }
444 +} // namespace Format
445 +
446 +namespace Erase {
447 + const Sequence LineForward{WSL_WINDOWS_VT_CSI L"K"};
448 + const Sequence LineBackward{WSL_WINDOWS_VT_CSI L"1K"};
449 + const Sequence LineEntirely{WSL_WINDOWS_VT_CSI L"2K"};
450 + const Sequence ScreenForward{WSL_WINDOWS_VT_CSI L"J"};
451 + const Sequence ScreenBackward{WSL_WINDOWS_VT_CSI L"1J"};
452 + const Sequence ScreenEntirely{WSL_WINDOWS_VT_CSI L"2J"};
453 +} // namespace Erase
454 +
455 +namespace Progress {
456 + ConstructedSequence Construct(State state, std::optional<uint32_t> percentage)
457 + {
458 + // See https://conemu.github.io/en/AnsiEscapeCodes.html#ConEmu_specific_OSC
459 +
460 + THROW_HR_IF(E_BOUNDS, percentage.has_value() && percentage.value() > 100u);
461 +
462 + // Workaround some quirks in the Windows Terminal implementation of the progress OSC sequence
463 + switch (state)
464 + {
465 + case State::None:
466 + case State::Indeterminate:
467 + // Windows Terminal does not recognize the OSC sequence if the progress value is left out.
468 + // As a workaround, we can specify an arbitrary value since it does not matter for None and Indeterminate states.
469 + percentage = percentage.value_or(0);
470 + break;
471 + case State::Normal:
472 + case State::Error:
473 + case State::Paused:
474 + // Windows Terminal does not support switching progress states without also setting a progress value at the same time,
475 + // so we disallow this case for now.
476 + THROW_HR_IF(E_INVALIDARG, !percentage.has_value());
477 + break;
478 + }
479 +
480 + int stateId;
481 + switch (state)
482 + {
483 + case State::None:
484 + stateId = 0;
485 + break;
486 + case State::Indeterminate:
487 + stateId = 3;
488 + break;
489 + case State::Normal:
490 + stateId = 1;
491 + break;
492 + case State::Error:
493 + stateId = 2;
494 + break;
495 + case State::Paused:
496 + stateId = 4;
497 + break;
498 + default:
499 + THROW_HR(E_UNEXPECTED);
500 + }
501 +
502 + std::wostringstream result;
503 + result << WSL_WINDOWS_VT_OSC L"9;4;" << stateId << L";";
504 + if (percentage.has_value())
505 + {
506 + result << percentage.value();
507 + }
508 + result << WSL_WINDOWS_VT_ESCAPE << L"\\";
509 + return ConstructedSequence{std::move(result).str()};
510 + }
511 +} // namespace Progress
512 +
513 +std::wstring operator+(const Sequence& lhs, const Sequence& rhs)
514 +{
515 + std::wstring out;
516 + out.reserve(lhs.Get().size() + rhs.Get().size());
517 + out.append(lhs.Get()).append(rhs.Get());
518 + return out;
519 +}
520 +
521 +std::wstring operator+(const Sequence& lhs, const std::wstring& rhs)
522 +{
523 + return std::wstring{lhs.Get()} + rhs;
524 +}
525 +
526 +std::wstring operator+(const std::wstring& lhs, const Sequence& rhs)
527 +{
528 + return lhs + std::wstring{rhs.Get()};
529 +}
530 +
531 +std::wstring operator+(const Sequence& lhs, const wchar_t* rhs)
532 +{
533 + return std::wstring{lhs.Get()} + rhs;
534 +}
535 +
536 +std::wstring operator+(const wchar_t* lhs, const Sequence& rhs)
537 +{
538 + return lhs + std::wstring{rhs.Get()};
539 +}
540 +
541 +std::wstring& operator+=(std::wstring& lhs, const Sequence& rhs)
542 +{
543 + lhs.append(rhs.Get());
544 + return lhs;
545 +}
546 +} // namespace wsl::windows::common::vt
src/windows/common/VTSupport.h new
+393
@@ -0,0 +1,393 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + VTSupport.h
8 +
9 +Abstract:
10 +
11 + This file contains VT (Virtual Terminal) sequence constants, construction
12 + helpers, and console mode RAII wrappers for use in Windows WSL components.
13 +
14 +--*/
15 +
16 +#pragma once
17 +
18 +#include <cstdint>
19 +#include <format>
20 +#include <initializer_list>
21 +#include <iosfwd>
22 +#include <optional>
23 +#include <string>
24 +#include <string_view>
25 +#include <type_traits>
26 +#include "wslutil.h"
27 +
28 +namespace wsl::windows::common::vt {
29 +
30 +// Get the integral value for an enum.
31 +template <typename E>
32 +constexpr inline std::enable_if_t<std::is_enum_v<E>, std::underlying_type_t<E>> ToIntegral(E e)
33 +{
34 + return static_cast<std::underlying_type_t<E>>(e);
35 +}
36 +
37 +// Get the enum value for an integral.
38 +template <typename E>
39 +constexpr inline std::enable_if_t<std::is_enum_v<E>, E> ToEnum(std::underlying_type_t<E> ut)
40 +{
41 + return static_cast<E>(ut);
42 +}
43 +
44 +// RAII helper that changes cursor visibility on a console handle and restores
45 +// the original cursor info on destruction. No-op if the handle is not a console.
46 +class ChangeTerminalMode
47 +{
48 +public:
49 + NON_COPYABLE(ChangeTerminalMode);
50 + NON_MOVABLE(ChangeTerminalMode);
51 +
52 + ChangeTerminalMode(HANDLE console, bool cursorVisible);
53 + ~ChangeTerminalMode();
54 +
55 + bool IsConsole() const;
56 +
57 +private:
58 + HANDLE m_console{};
59 + CONSOLE_CURSOR_INFO m_originalCursorInfo{};
60 +};
61 +
62 +// RAII helper that enables VT processing on a console handle and restores the
63 +// original mode on destruction. No-op if the handle is not a console.
64 +//
65 +// Output mode (STD_OUTPUT_HANDLE): sets ENABLE_VIRTUAL_TERMINAL_PROCESSING,
66 +// optionally DISABLE_NEWLINE_AUTO_RETURN (best-effort, falls back without it).
67 +//
68 +// Input mode (STD_INPUT_HANDLE): sets ENABLE_VIRTUAL_TERMINAL_INPUT and
69 +// ENABLE_EXTENDED_FLAGS, clears ENABLE_LINE_INPUT and ENABLE_ECHO_INPUT.
70 +class EnableVirtualTerminal
71 +{
72 +public:
73 + NON_COPYABLE(EnableVirtualTerminal);
74 + NON_MOVABLE(EnableVirtualTerminal);
75 +
76 + enum class Mode
77 + {
78 + Output,
79 + Input,
80 + };
81 +
82 + explicit EnableVirtualTerminal(HANDLE console, Mode mode = Mode::Output, bool disableNewlineAutoReturn = false);
83 + ~EnableVirtualTerminal();
84 +
85 + // Returns true if VT processing is currently enabled on the console handle,
86 + // whether this instance enabled it or it was already enabled at construction.
87 + // This is the correct gate for "should we emit VT escape sequences?".
88 + // It is independent of whether the destructor will restore the prior mode
89 + // (that ownership is tracked separately by m_console).
90 + bool IsVTEnabled() const;
91 +
92 +private:
93 + HANDLE m_console = nullptr; // non-null only when this instance must restore on destruction
94 + DWORD m_originalMode = 0;
95 + bool m_vtEnabled = false; // true when VT processing is active on the console handle
96 +};
97 +
98 +// VT escape sequences are pure ASCII byte sequences (0x00-0x7F), but the
99 +// Windows WSL components are wide-string throughout (WriteConsoleW, wostream,
100 +// std::wstring buffers). Sequences are therefore stored as std::wstring /
101 +// std::wstring_view so they compose directly with the surrounding wide-string
102 +// code with no per-call widening. Use the std::formatter specialization below
103 +// for std::wformat output.
104 +
105 +// The base for all VT sequences.
106 +struct Sequence
107 +{
108 + constexpr Sequence() = default;
109 + explicit constexpr Sequence(std::wstring_view c) : m_chars(c)
110 + {
111 + }
112 +
113 + // Prevent construction from a std::wstring (lvalue or rvalue): std::wstring is
114 + // implicitly convertible to std::wstring_view, so without this guard
115 + // Sequence(someString) would compile but leave m_chars dangling once the string
116 + // is destroyed. Use ConstructedSequence for runtime / owned sequences.
117 + // A constrained template (rather than named overloads) avoids making wchar_t[]
118 + // literals ambiguous between the deleted and wstring_view constructors.
119 + template <typename T>
120 + requires std::is_same_v<std::remove_cvref_t<T>, std::wstring>
121 + explicit Sequence(T&&) = delete;
122 +
123 + std::wstring_view Get() const
124 + {
125 + return m_chars;
126 + }
127 +
128 + // Returns true if this is a color or formatting sequence (SGR or OSC 8 hyperlink)
129 + // that should be suppressed when --no-color is set.
130 + bool IsColor() const;
131 +
132 +protected:
133 + void Set(const std::wstring& s)
134 + {
135 + m_chars = s;
136 + }
137 +
138 +private:
139 + std::wstring_view m_chars;
140 +};
141 +
142 +// A VT sequence that is constructed at runtime.
143 +struct ConstructedSequence : public Sequence
144 +{
145 + ConstructedSequence();
146 + explicit ConstructedSequence(std::wstring s);
147 +
148 + ConstructedSequence(const ConstructedSequence& other);
149 + ConstructedSequence& operator=(const ConstructedSequence& other);
150 +
151 + ConstructedSequence(ConstructedSequence&& other) noexcept;
152 + ConstructedSequence& operator=(ConstructedSequence&& other) noexcept;
153 +
154 + void Append(const Sequence& sequence);
155 + void Clear();
156 +
157 +private:
158 + std::wstring m_str;
159 +};
160 +
161 +// Constructs a single SGR (Select Graphic Rendition) sequence with one or more
162 +// semicolon-separated parameters. e.g. Sgr({1, 31}) produces "\x1b[1;31m".
163 +// Prefer named constants in the Format namespace for single-parameter sequences;
164 +// use this only when a multi-parameter form is required to match specific terminal
165 +// output exactly (e.g. a shell PS1 that emits combined bold+color in one sequence).
166 +ConstructedSequence Sgr(std::initializer_list<int> params);
167 +
168 +// Below are mapped to the sequences described here:
169 +// https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences
170 +
171 +// Contains the response to a DA1 (Primary Device Attributes) request.
172 +struct PrimaryDeviceAttributes
173 +{
174 + // Queries the device attributes on creation.
175 + // Both streams must be opened in _O_U8TEXT mode (or equivalent wide mode).
176 + // outStream receives the DA1 request; inStream provides the terminal's response.
177 + PrimaryDeviceAttributes(std::wostream& outStream, std::wistream& inStream);
178 +
179 + // The extensions that a device may support.
180 + enum class Extension
181 + {
182 + Columns132 = 1,
183 + PrinterPort = 2,
184 + Sixel = 4,
185 + SelectiveErase = 6,
186 + SoftCharacterSet = 7,
187 + UserDefinedKeys = 8,
188 + NationalReplacementCharacterSets = 9,
189 + SoftCharacterSet2 = 12,
190 + EightBitInterface = 14,
191 + TechnicalCharacterSet = 15,
192 + WindowingCapability = 18,
193 + HorizontalScrolling = 21,
194 + ColorText = 22,
195 + Greek = 23,
196 + Turkish = 24,
197 + RectangularAreaOperations = 28,
198 + TextMacros = 32,
199 + ISO_Latin2CharacterSet = 42,
200 + PC_Term = 44,
201 + SoftKeyMap = 45,
202 + ASCII_Emulation = 46,
203 + };
204 +
205 + // Determines if the given extension is supported.
206 + bool Supports(Extension extension) const;
207 +
208 +private:
209 + uint32_t m_conformanceLevel = 0;
210 + uint64_t m_extensions = 0;
211 +};
212 +
213 +// Cursor movement, visibility, and input mode sequences.
214 +namespace Cursor {
215 + // Move cursor N cells in the given direction.
216 + ConstructedSequence Up(int cells);
217 + ConstructedSequence Down(int cells);
218 + ConstructedSequence Forward(int cells);
219 + ConstructedSequence Backward(int cells);
220 +
221 + // Move cursor to an absolute position (1-based row and column).
222 + ConstructedSequence MoveTo(int row, int col);
223 +
224 + // Move cursor to the top-left corner of the screen.
225 + extern const Sequence Home;
226 +
227 + // Cursor visibility.
228 + extern const Sequence EnableBlink;
229 + extern const Sequence DisableBlink;
230 + extern const Sequence Show;
231 + extern const Sequence Hide;
232 +
233 + // Bracketed paste mode causes the terminal to wrap pasted text in escape sequences
234 + // so the application can distinguish typed input from pasted input.
235 + // See https://cirw.in/blog/bracketed-paste
236 + extern const Sequence BracketedPasteOn;
237 + extern const Sequence BracketedPasteOff;
238 +} // namespace Cursor
239 +
240 +// Text formatting (color, weight, style) sequences.
241 +namespace Format {
242 + extern const Sequence Default;
243 + extern const Sequence Negative;
244 +
245 + // Intensity attributes. Normal cancels both Bright and Dim (SGR 22).
246 + extern const Sequence Bright;
247 + extern const Sequence Dim;
248 + extern const Sequence Normal;
249 +
250 + extern const Sequence Italic;
251 + extern const Sequence NoItalic;
252 + extern const Sequence Underline;
253 + extern const Sequence NoUnderline;
254 +
255 + // A color, used in constructed sequences.
256 + struct Color
257 + {
258 + uint8_t R;
259 + uint8_t G;
260 + uint8_t B;
261 + };
262 +
263 + namespace Fg {
264 + // Standard foreground colors using SGR 30-37.
265 + extern const Sequence Black;
266 + extern const Sequence Red;
267 + extern const Sequence Green;
268 + extern const Sequence Yellow;
269 + extern const Sequence Blue;
270 + extern const Sequence Magenta;
271 + extern const Sequence Cyan;
272 + extern const Sequence White;
273 +
274 + // High-intensity ("bright") foreground colors using SGR 90-97.
275 + // These are distinct from SGR 1;3x (bold + standard color), which is
276 + // a different byte sequence even though terminals often render them identically.
277 + extern const Sequence BrightBlack; // Typically rendered as dark gray.
278 + extern const Sequence BrightRed;
279 + extern const Sequence BrightGreen;
280 + extern const Sequence BrightYellow;
281 + extern const Sequence BrightBlue;
282 + extern const Sequence BrightMagenta;
283 + extern const Sequence BrightCyan;
284 + extern const Sequence BrightWhite;
285 +
286 + ConstructedSequence Extended(const Color& color);
287 + } // namespace Fg
288 +
289 + namespace Bg {
290 + // Standard background colors using SGR 40-47.
291 + extern const Sequence Black;
292 + extern const Sequence Red;
293 + extern const Sequence Green;
294 + extern const Sequence Yellow;
295 + extern const Sequence Blue;
296 + extern const Sequence Magenta;
297 + extern const Sequence Cyan;
298 + extern const Sequence White;
299 +
300 + // High-intensity ("bright") background colors using SGR 100-107.
301 + extern const Sequence BrightBlack; // Typically rendered as dark gray.
302 + extern const Sequence BrightRed;
303 + extern const Sequence BrightGreen;
304 + extern const Sequence BrightYellow;
305 + extern const Sequence BrightBlue;
306 + extern const Sequence BrightMagenta;
307 + extern const Sequence BrightCyan;
308 + extern const Sequence BrightWhite;
309 +
310 + ConstructedSequence Extended(const Color& color);
311 + } // namespace Bg
312 +
313 + ConstructedSequence Hyperlink(const std::wstring& text, const std::wstring& ref);
314 +} // namespace Format
315 +
316 +// Line and screen erasure sequences.
317 +namespace Erase {
318 + extern const Sequence LineForward;
319 + extern const Sequence LineBackward;
320 + extern const Sequence LineEntirely;
321 + extern const Sequence ScreenForward;
322 + extern const Sequence ScreenBackward;
323 + extern const Sequence ScreenEntirely;
324 +} // namespace Erase
325 +
326 +namespace Progress {
327 + enum class State
328 + {
329 + None,
330 + Indeterminate,
331 + Normal,
332 + Paused,
333 + Error
334 + };
335 +
336 + ConstructedSequence Construct(State state, std::optional<uint32_t> percentage = std::nullopt);
337 +} // namespace Progress
338 +
339 +// operator+ overloads for combining sequences with wide strings.
340 +std::wstring operator+(const Sequence& lhs, const Sequence& rhs);
341 +std::wstring operator+(const Sequence& lhs, const std::wstring& rhs);
342 +std::wstring operator+(const std::wstring& lhs, const Sequence& rhs);
343 +std::wstring operator+(const Sequence& lhs, const wchar_t* rhs);
344 +std::wstring operator+(const wchar_t* lhs, const Sequence& rhs);
345 +
346 +// operator== overloads for comparing sequences against string literals.
347 +template <typename T, typename = std::enable_if_t<std::is_base_of<Sequence, T>::value>>
348 +inline bool operator==(const T& lhs, std::wstring_view rhs)
349 +{
350 + return lhs.Get() == rhs;
351 +}
352 +
353 +template <typename T, typename = std::enable_if_t<std::is_base_of<Sequence, T>::value>>
354 +inline bool operator==(std::wstring_view lhs, const T& rhs)
355 +{
356 + return lhs == rhs.Get();
357 +}
358 +
359 +template <typename T, typename = std::enable_if_t<std::is_base_of<Sequence, T>::value>>
360 +inline bool operator==(const T& lhs, const wchar_t* rhs)
361 +{
362 + return lhs.Get() == rhs;
363 +}
364 +
365 +template <typename T, typename = std::enable_if_t<std::is_base_of<Sequence, T>::value>>
366 +inline bool operator==(const wchar_t* lhs, const T& rhs)
367 +{
368 + return lhs == rhs.Get();
369 +}
370 +
371 +// In-place wide string append.
372 +std::wstring& operator+=(std::wstring& lhs, const Sequence& rhs);
373 +
374 +} // namespace wsl::windows::common::vt
375 +
376 +// std::formatter specializations, must be outside namespace.
377 +template <>
378 +struct std::formatter<wsl::windows::common::vt::Sequence, wchar_t> : std::formatter<std::wstring_view, wchar_t>
379 +{
380 + auto format(const wsl::windows::common::vt::Sequence& s, std::wformat_context& ctx) const
381 + {
382 + return std::formatter<std::wstring_view, wchar_t>::format(s.Get(), ctx);
383 + }
384 +};
385 +
386 +template <>
387 +struct std::formatter<wsl::windows::common::vt::ConstructedSequence, wchar_t> : std::formatter<wsl::windows::common::vt::Sequence, wchar_t>
388 +{
389 + auto format(const wsl::windows::common::vt::ConstructedSequence& s, std::wformat_context& ctx) const
390 + {
391 + return std::formatter<wsl::windows::common::vt::Sequence, wchar_t>::format(s, ctx);
392 + }
393 +};
src/windows/wslc/services/BuildImageCallback.cpp
+27 -31
@@ -18,15 +18,7 @@ Abstract:
18 namespace wsl::windows::wslc::services {
19
20 using wsl::windows::common::string::MultiByteToWide;
21 -
22 -namespace {
23 - constexpr std::wstring_view c_escapeMoveCursorUpAndClear = L"\033[{}A\033[J";
24 - constexpr std::wstring_view c_escapeBrightGreen = L"\033[92m";
25 - constexpr std::wstring_view c_escapeResetAttributes = L"\033[0m";
26 - constexpr std::wstring_view c_escapeHideCursorDim = L"\033[?25l\033[2m";
27 - constexpr std::wstring_view c_escapeClearLineAndNewline = L"\033[K\n";
28 - constexpr std::wstring_view c_escapeUndimShowCursor = L"\033[22m\033[?25h";
29 -} // namespace
21 +using namespace wsl::windows::common::vt;
22
23 BuildImageCallback::~BuildImageCallback()
24 try
@@ -67,7 +59,8 @@ void BuildImageCallback::CollapseWindow()
59 {
60 if (m_displayedLines > 0)
61 {
70 - WriteTerminal(std::format(c_escapeMoveCursorUpAndClear, m_displayedLines));
62 + // Move cursor up to the start of the display area, then erase to end of screen.
63 + WriteTerminal(Cursor::Up(m_displayedLines) + Erase::ScreenForward);
64 m_displayedLines = 0;
65 }
66
@@ -184,7 +177,7 @@ try
177 const auto newlines = wide.substr(bodyLength);
178 wide.resize(bodyLength);
179
187 - WriteTerminal(std::format(L"{}{}{}{}", c_escapeBrightGreen, wide, c_escapeResetAttributes, newlines));
180 + WriteTerminal(std::format(L"{}{}{}{}", Format::Fg::BrightGreen, wide, Format::Default, newlines));
181 return S_OK;
182 }
183 CATCH_RETURN();
@@ -193,50 +186,52 @@ void BuildImageCallback::Redraw()
186 {
187 CONSOLE_SCREEN_BUFFER_INFO info{};
188 THROW_IF_WIN32_BOOL_FALSE(GetConsoleScreenBufferInfo(m_console, &info));
196 - // Use the visible window width (not buffer width), minus one column to avoid the
197 - // deferred-wrap edge case when a line is exactly the window width. Clamp to at
198 - // least zero so the value never goes negative (which would underflow when passed
199 - // to std::wstring::resize).
200 - const SHORT consoleWidth = std::max<SHORT>(0, info.srWindow.Right - info.srWindow.Left);
189 + const int consoleWidth = std::max(0, static_cast<int>(info.srWindow.Right) - info.srWindow.Left);
190
202 - // Determine how many completed lines to show, leaving room for the pending line and pull progress.
191 const bool showPending = !m_pendingLine.empty();
204 - const SHORT pullCount = static_cast<SHORT>(m_pullLines.size());
205 - SHORT completedCount = static_cast<SHORT>(m_lines.size());
206 - const SHORT reservedLines = (showPending ? 1 : 0) + pullCount;
192 + const int pullCount = static_cast<int>(m_pullLines.size());
193 + int completedCount = static_cast<int>(m_lines.size());
194 + const int reservedLines = (showPending ? 1 : 0) + pullCount;
195 if (completedCount + reservedLines > c_maxDisplayLines)
196 {
209 - completedCount = std::max<SHORT>(0, c_maxDisplayLines - reservedLines);
197 + completedCount = std::max(0, c_maxDisplayLines - reservedLines);
198 }
211 - const SHORT displayCount = completedCount + reservedLines;
199 + const int displayCount = completedCount + reservedLines;
200
201 // Build the entire frame in one buffer to minimize console writes. Hide the cursor
202 // during the redraw so the user doesn't see it bouncing through the cursor movement,
203 // then show it again at the final position. The dim attribute (\033[2m) renders the
204 // scrolling lines de-emphasized regardless of the user's theme.
217 - std::wstring buffer{c_escapeHideCursorDim};
205 + //
206 + // m_frameBuffer is a member so its backing allocation is reused across frames -
207 + // it grows to the high-water mark and is never freed between redraws.
208 + m_frameBuffer.clear();
209 + m_frameBuffer += Cursor::Hide;
210 + m_frameBuffer += Format::Dim;
211
212 // Move cursor to the start of the display area and erase from there to the end of
213 // the screen. \033[J handles the case where the new display is shorter than the
214 // previous one (e.g. when \r clears the pending line without a replacement).
215 if (m_displayedLines > 0)
216 {
224 - buffer += std::format(c_escapeMoveCursorUpAndClear, m_displayedLines);
217 + m_frameBuffer += Cursor::Up(m_displayedLines);
218 + m_frameBuffer += Erase::ScreenForward;
219 }
220
221 auto appendLine = [&](const std::string& line) {
222 auto wline = MultiByteToWide(line);
229 - if (static_cast<SHORT>(wline.size()) > consoleWidth)
223 + if (wline.size() > static_cast<size_t>(consoleWidth))
224 {
231 - wline.resize(consoleWidth);
225 + wline.resize(static_cast<size_t>(consoleWidth));
226 }
233 - buffer += wline;
234 - buffer += c_escapeClearLineAndNewline;
227 + m_frameBuffer += std::move(wline);
228 + m_frameBuffer += Erase::LineForward;
229 + m_frameBuffer += L'\n';
230 };
231
232 // Print completed lines (skip older ones if we need room for the pending line).
233 auto it = m_lines.begin();
239 - if (completedCount < static_cast<SHORT>(m_lines.size()))
234 + if (completedCount < static_cast<int>(m_lines.size()))
235 {
236 std::advance(it, m_lines.size() - completedCount);
237 }
@@ -257,9 +252,10 @@ void BuildImageCallback::Redraw()
252 appendLine(line);
253 }
254
260 - buffer += c_escapeUndimShowCursor;
255 + m_frameBuffer += Format::Normal;
256 + m_frameBuffer += Cursor::Show;
257
262 - WriteTerminal(buffer);
258 + WriteTerminal(m_frameBuffer);
259 m_displayedLines = displayCount;
260 }
261
src/windows/wslc/services/BuildImageCallback.h
+7 -4
@@ -12,8 +12,8 @@ Abstract:
12
13 --*/
14 #pragma once
15 -#include "ChangeTerminalMode.h"
15 #include "SessionService.h"
16 +#include "VTSupport.h"
17 #include <deque>
18 #include <map>
19
@@ -30,7 +30,7 @@ public:
30 HRESULT OnProgress(LPCSTR status, LPCSTR id, ULONGLONG current, ULONGLONG total) override;
31
32 private:
33 - static constexpr SHORT c_maxDisplayLines = 16;
33 + static constexpr int c_maxDisplayLines = 16;
34 static constexpr auto c_redrawInterval = std::chrono::milliseconds(50);
35 static constexpr size_t c_maxAllLinesBytes = 10 * 1024 * 1024; // 10 MiB cap on retained log output for error replay.
36
@@ -46,7 +46,7 @@ private:
46 const HANDLE m_cancelEvent;
47 HANDLE m_console = GetStdHandle(STD_OUTPUT_HANDLE);
48 bool m_isConsole = wsl::windows::common::wslutil::IsConsoleHandle(m_console);
49 - EnableVirtualTerminal m_vtMode{m_console};
49 + wsl::windows::common::vt::EnableVirtualTerminal m_vtMode{m_console};
50 std::deque<std::string> m_lines;
51 // Each entry already contains the trailing newline so the bytes match what's replayed.
52 // TODO: Track logs per step so the destructor can replay only the failing step's
@@ -54,10 +54,13 @@ private:
54 std::deque<std::string> m_allLines;
55 size_t m_allLinesBytes = 0;
56 std::string m_pendingLine;
57 - SHORT m_displayedLines = 0;
57 + int m_displayedLines = 0;
58 std::chrono::steady_clock::time_point m_lastRedraw{};
59 // Per-entry pull progress lines, keyed by entry id. Updated in place by Redraw. std::map so order is consistent.
60 std::map<std::string, std::string> m_pullLines;
61 + // Reused across Redraw() calls so the backing allocation grows to the high-water
62 + // mark and is then reused rather than re-allocated every frame.
63 + std::wstring m_frameBuffer;
64 // Captured at construction so the destructor can detect destruction during exception unwinding.
65 int m_uncaughtExceptions = std::uncaught_exceptions();
66 };
src/windows/wslc/services/ChangeTerminalMode.h deleted
-92
@@ -1,92 +0,0 @@
1 -/*++
2 -
3 -Copyright (c) Microsoft. All rights reserved.
4 -
5 -Module Name:
6 -
7 - ChangeTerminalMode.h
8 -
9 -Abstract:
10 -
11 - This file contains the ChangeTerminalMode definition.
12 -
13 ---*/
14 -#pragma once
15 -
16 -namespace wsl::windows::wslc::services {
17 -
18 -class ChangeTerminalMode
19 -{
20 -public:
21 - NON_COPYABLE(ChangeTerminalMode);
22 - NON_MOVABLE(ChangeTerminalMode);
23 -
24 - ChangeTerminalMode(HANDLE console, bool cursorVisible) : m_console(console)
25 - {
26 - if (!wsl::windows::common::wslutil::IsConsoleHandle(console))
27 - {
28 - m_console = nullptr;
29 - return;
30 - }
31 -
32 - THROW_IF_WIN32_BOOL_FALSE(GetConsoleCursorInfo(console, &m_originalCursorInfo));
33 - CONSOLE_CURSOR_INFO newCursorInfo = m_originalCursorInfo;
34 - newCursorInfo.bVisible = cursorVisible;
35 - THROW_IF_WIN32_BOOL_FALSE(SetConsoleCursorInfo(console, &newCursorInfo));
36 - }
37 -
38 - ~ChangeTerminalMode()
39 - {
40 - if (m_console)
41 - {
42 - LOG_IF_WIN32_BOOL_FALSE(SetConsoleCursorInfo(m_console, &m_originalCursorInfo));
43 - }
44 - }
45 -
46 - bool IsConsole() const
47 - {
48 - return m_console != nullptr;
49 - }
50 -
51 -private:
52 - HANDLE m_console{};
53 - CONSOLE_CURSOR_INFO m_originalCursorInfo{};
54 -};
55 -
56 -// RAII helper that enables ENABLE_VIRTUAL_TERMINAL_PROCESSING on a console handle and
57 -// restores the original mode on destruction. No-op if the handle isn't a console or
58 -// VT processing is already enabled.
59 -class EnableVirtualTerminal
60 -{
61 -public:
62 - NON_COPYABLE(EnableVirtualTerminal);
63 - NON_MOVABLE(EnableVirtualTerminal);
64 -
65 - explicit EnableVirtualTerminal(HANDLE console)
66 - {
67 - DWORD mode;
68 - if (GetConsoleMode(console, &mode))
69 - {
70 - const DWORD newMode = mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING;
71 - if (newMode != mode && SetConsoleMode(console, newMode))
72 - {
73 - m_console = console;
74 - m_originalMode = mode;
75 - }
76 - }
77 - }
78 -
79 - ~EnableVirtualTerminal()
80 - {
81 - if (m_console)
82 - {
83 - LOG_IF_WIN32_BOOL_FALSE(SetConsoleMode(m_console, m_originalMode));
84 - }
85 - }
86 -
87 -private:
88 - HANDLE m_console = nullptr;
89 - DWORD m_originalMode = 0;
90 -};
91 -
92 -} // namespace wsl::windows::wslc::services
src/windows/wslc/services/ImageProgressCallback.cpp
+15 -8
@@ -19,18 +19,25 @@ Abstract:
19
20 namespace wsl::windows::wslc::services {
21 using namespace wsl::shared;
22 +using namespace wsl::windows::common::vt;
23
23 -auto ImageProgressCallback::MoveToLine(SHORT line)
24 +void ImageProgressCallback::WriteTerminal(std::wstring_view content) const
25 +{
26 + DWORD written;
27 + LOG_IF_WIN32_BOOL_FALSE(WriteConsoleW(m_console, content.data(), static_cast<DWORD>(content.size()), &written, nullptr));
28 +}
29 +
30 +auto ImageProgressCallback::MoveToLine(int line)
31 {
32 if (line > 0)
33 {
27 - wprintf(L"\033[%iA", line);
34 + WriteTerminal(Cursor::Up(line).Get());
35 }
36
30 - return wil::scope_exit([line = line]() {
37 + return wil::scope_exit([line = line, this]() {
38 if (line > 1)
39 {
33 - wprintf(L"\033[%iB", line - 1);
40 + WriteTerminal(Cursor::Down(line - 1).Get());
41 }
42 });
43 }
@@ -46,7 +53,7 @@ HRESULT ImageProgressCallback::OnProgress(LPCSTR status, LPCSTR id, ULONGLONG cu
53
54 if (id == nullptr || *id == '\0') // Print all 'global' statuses on their own line
55 {
49 - wprintf(L"%hs\n", status);
56 + WriteTerminal(std::format(L"{}\n", status));
57 m_currentLine++;
58 return S_OK;
59 }
@@ -58,13 +65,13 @@ HRESULT ImageProgressCallback::OnProgress(LPCSTR status, LPCSTR id, ULONGLONG cu
65 {
66 // If this is the first time we see this ID, create a new line for it.
67 m_statuses.emplace(id, m_currentLine);
61 - wprintf(L"%ls\n", GenerateStatusLine(status, id, current, total, info).c_str());
68 + WriteTerminal(GenerateStatusLine(status, id, current, total, info) + L'\n');
69 m_currentLine++;
70 }
71 else
72 {
73 auto revert = MoveToLine(m_currentLine - it->second);
67 - wprintf(L"%ls\n", GenerateStatusLine(status, id, current, total, info).c_str());
74 + WriteTerminal(GenerateStatusLine(status, id, current, total, info) + L'\n');
75 }
76
77 return S_OK;
@@ -126,7 +133,7 @@ std::wstring ImageProgressCallback::GenerateStatusLine(LPCSTR status, LPCSTR id,
133 }
134
135 // Use the visible window width (not the buffer width) to prevent wrapping.
129 - const auto visibleWidth = std::max<SHORT>(0, info.srWindow.Right - info.srWindow.Left + 1);
136 + const auto visibleWidth = std::max(0, static_cast<int>(info.srWindow.Right) - info.srWindow.Left + 1);
137
138 // Truncate to console width to prevent wrapping that would break cursor repositioning.
139 if (line.size() > static_cast<size_t>(visibleWidth))
src/windows/wslc/services/ImageProgressCallback.h
+11 -7
@@ -12,8 +12,10 @@ Abstract:
12
13 --*/
14 #pragma once
15 -#include "ChangeTerminalMode.h"
15 #include "SessionService.h"
16 +#include "VTSupport.h"
17 +#include <map>
18 +#include <string>
19
20 namespace wsl::windows::wslc::services {
21
@@ -22,15 +24,17 @@ class DECLSPEC_UUID("7A1D3376-835A-471A-8DC9-23653D9962D0") ImageProgressCallbac
24 : public Microsoft::WRL::RuntimeClass<Microsoft::WRL::RuntimeClassFlags<Microsoft::WRL::ClassicCom>, IProgressCallback, IFastRundown>
25 {
26 public:
25 - auto MoveToLine(SHORT line);
27 HRESULT OnProgress(LPCSTR status, LPCSTR id, ULONGLONG current, ULONGLONG total) override;
28
29 private:
30 + auto MoveToLine(int line);
31 static CONSOLE_SCREEN_BUFFER_INFO Info();
32 + void WriteTerminal(std::wstring_view content) const;
33 std::wstring GenerateStatusLine(LPCSTR status, LPCSTR id, ULONGLONG current, ULONGLONG total, const CONSOLE_SCREEN_BUFFER_INFO& info);
31 - std::map<std::string, SHORT> m_statuses;
32 - SHORT m_currentLine = 0;
33 - EnableVirtualTerminal m_vtMode{GetStdHandle(STD_OUTPUT_HANDLE)};
34 - ChangeTerminalMode m_terminalMode{GetStdHandle(STD_OUTPUT_HANDLE), false};
34 + std::map<std::string, int> m_statuses;
35 + int m_currentLine = 0;
36 + HANDLE m_console = GetStdHandle(STD_OUTPUT_HANDLE);
37 + wsl::windows::common::vt::EnableVirtualTerminal m_vtMode{m_console};
38 + wsl::windows::common::vt::ChangeTerminalMode m_terminalMode{m_console, false};
39 };
36 -} // namespace wsl::windows::wslc::services
\ No newline at end of file
40 +} // namespace wsl::windows::wslc::services
test/windows/wslc/WSLCCLIVTSupportUnitTests.cpp new
+565
@@ -0,0 +1,565 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCCLIVTSupportUnitTests.cpp
8 +
9 +Abstract:
10 +
11 + This file contains unit tests for VT sequence construction and console mode helpers.
12 +
13 +--*/
14 +
15 +#include "precomp.h"
16 +#include "windows/Common.h"
17 +#include "VTSupport.h"
18 +
19 +using namespace WEX::Logging;
20 +using namespace WEX::Common;
21 +using namespace WEX::TestExecution;
22 +using namespace wsl::windows::common::vt;
23 +
24 +namespace WSLCCLIVTSupportUnitTests {
25 +
26 +// Creates a real console screen buffer that can be used as an output handle for console API tests.
27 +// The buffer is not attached to the visible console window, so it does not affect the test runner output.
28 +static wil::unique_hfile MakeScreenBuffer()
29 +{
30 + wil::unique_hfile handle{CreateConsoleScreenBuffer(
31 + GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, CONSOLE_TEXTMODE_BUFFER, nullptr)};
32 + THROW_LAST_ERROR_IF(!handle);
33 + return handle;
34 +}
35 +
36 +class WSLCCLIVTSupportUnitTests
37 +{
38 + WSLC_TEST_CLASS(WSLCCLIVTSupportUnitTests)
39 +
40 + TEST_METHOD(VT_Sequence)
41 + {
42 + // Constant sequence round-trips correctly.
43 + const Sequence constant{L"\x1b[0m"};
44 + VERIFY_ARE_EQUAL(L"\x1b[0m", constant);
45 +
46 + // Default-constructed ConstructedSequence is empty.
47 + ConstructedSequence empty;
48 + VERIFY_IS_TRUE(empty.Get().empty());
49 +
50 + // Construct from string.
51 + ConstructedSequence seq{L"\x1b[1m"};
52 + VERIFY_ARE_EQUAL(L"\x1b[1m", seq);
53 +
54 + // Append combines sequences.
55 + seq.Append(Sequence{L"\x1b[91m"});
56 + VERIFY_ARE_EQUAL(L"\x1b[1m\x1b[91m", seq);
57 +
58 + // Appending an empty sequence is a no-op.
59 + seq.Append(Sequence{});
60 + VERIFY_ARE_EQUAL(L"\x1b[1m\x1b[91m", seq);
61 +
62 + // Clear resets to empty.
63 + seq.Clear();
64 + VERIFY_IS_TRUE(seq.Get().empty());
65 +
66 + // Copy construction.
67 + ConstructedSequence original{L"\x1b[1m"};
68 + ConstructedSequence copy{original};
69 + VERIFY_ARE_EQUAL(original.Get(), copy.Get());
70 +
71 + // Move construction.
72 + ConstructedSequence moved{std::move(original)};
73 + VERIFY_ARE_EQUAL(L"\x1b[1m", moved);
74 +
75 + // Copy assignment.
76 + ConstructedSequence assigned;
77 + assigned = copy;
78 + VERIFY_ARE_EQUAL(copy.Get(), assigned.Get());
79 +
80 + // Move assignment.
81 + ConstructedSequence moveAssigned;
82 + moveAssigned = std::move(moved);
83 + VERIFY_ARE_EQUAL(L"\x1b[1m", moveAssigned);
84 +
85 + // SGR Construction.
86 + VERIFY_ARE_EQUAL(L"\x1b[1;31m", Sgr({1, 31}));
87 + VERIFY_ARE_EQUAL(L"\x1b[0m", Sgr({0}));
88 + }
89 +
90 + TEST_METHOD(VT_CursorSequences)
91 + {
92 + VERIFY_ARE_EQUAL(L"\x1b[3A", Cursor::Up(3));
93 + VERIFY_ARE_EQUAL(L"\x1b[5B", Cursor::Down(5));
94 + VERIFY_ARE_EQUAL(L"\x1b[2C", Cursor::Forward(2));
95 + VERIFY_ARE_EQUAL(L"\x1b[1D", Cursor::Backward(1));
96 + VERIFY_ARE_EQUAL(L"\x1b[5;10H", Cursor::MoveTo(5, 10));
97 + VERIFY_ARE_EQUAL(L"\x1b[H", Cursor::Home);
98 +
99 + // cells == 0 is a no-op — returns an empty sequence rather than
100 + // emitting ESC[0X, which most terminals treat as "move 1 cell".
101 + VERIFY_IS_TRUE(Cursor::Up(0).Get().empty());
102 + VERIFY_IS_TRUE(Cursor::Down(0).Get().empty());
103 + VERIFY_IS_TRUE(Cursor::Forward(0).Get().empty());
104 + VERIFY_IS_TRUE(Cursor::Backward(0).Get().empty());
105 +
106 + VERIFY_THROWS_SPECIFIC(
107 + Cursor::Up(-1), wil::ResultException, [](const wil::ResultException& e) { return e.GetErrorCode() == E_INVALIDARG; });
108 + VERIFY_THROWS_SPECIFIC(Cursor::MoveTo(0, 1), wil::ResultException, [](const wil::ResultException& e) {
109 + return e.GetErrorCode() == E_INVALIDARG;
110 + });
111 + VERIFY_THROWS_SPECIFIC(Cursor::MoveTo(1, 0), wil::ResultException, [](const wil::ResultException& e) {
112 + return e.GetErrorCode() == E_INVALIDARG;
113 + });
114 +
115 + VERIFY_ARE_EQUAL(L"\x1b[?2004h", Cursor::BracketedPasteOn);
116 + VERIFY_ARE_EQUAL(L"\x1b[?2004l", Cursor::BracketedPasteOff);
117 + }
118 +
119 + TEST_METHOD(VT_TextFormatSequences)
120 + {
121 + VERIFY_ARE_EQUAL(L"\x1b[0m", Format::Default);
122 + VERIFY_ARE_EQUAL(L"\x1b[7m", Format::Negative);
123 + VERIFY_ARE_EQUAL(L"\x1b[1m", Format::Bright);
124 + VERIFY_ARE_EQUAL(L"\x1b[2m", Format::Dim);
125 + VERIFY_ARE_EQUAL(L"\x1b[22m", Format::Normal);
126 + VERIFY_ARE_EQUAL(L"\x1b[3m", Format::Italic);
127 + VERIFY_ARE_EQUAL(L"\x1b[23m", Format::NoItalic);
128 + VERIFY_ARE_EQUAL(L"\x1b[4m", Format::Underline);
129 + VERIFY_ARE_EQUAL(L"\x1b[24m", Format::NoUnderline);
130 +
131 + VERIFY_ARE_EQUAL(L"\x1b[30m", Format::Fg::Black);
132 + VERIFY_ARE_EQUAL(L"\x1b[31m", Format::Fg::Red);
133 + VERIFY_ARE_EQUAL(L"\x1b[32m", Format::Fg::Green);
134 + VERIFY_ARE_EQUAL(L"\x1b[33m", Format::Fg::Yellow);
135 + VERIFY_ARE_EQUAL(L"\x1b[34m", Format::Fg::Blue);
136 + VERIFY_ARE_EQUAL(L"\x1b[35m", Format::Fg::Magenta);
137 + VERIFY_ARE_EQUAL(L"\x1b[36m", Format::Fg::Cyan);
138 + VERIFY_ARE_EQUAL(L"\x1b[37m", Format::Fg::White);
139 +
140 + VERIFY_ARE_EQUAL(L"\x1b[90m", Format::Fg::BrightBlack);
141 + VERIFY_ARE_EQUAL(L"\x1b[91m", Format::Fg::BrightRed);
142 + VERIFY_ARE_EQUAL(L"\x1b[92m", Format::Fg::BrightGreen);
143 + VERIFY_ARE_EQUAL(L"\x1b[93m", Format::Fg::BrightYellow);
144 + VERIFY_ARE_EQUAL(L"\x1b[94m", Format::Fg::BrightBlue);
145 + VERIFY_ARE_EQUAL(L"\x1b[95m", Format::Fg::BrightMagenta);
146 + VERIFY_ARE_EQUAL(L"\x1b[96m", Format::Fg::BrightCyan);
147 + VERIFY_ARE_EQUAL(L"\x1b[97m", Format::Fg::BrightWhite);
148 +
149 + VERIFY_ARE_EQUAL(L"\x1b[38;2;255;128;0m", Format::Fg::Extended(Format::Color{255, 128, 0}));
150 +
151 + VERIFY_ARE_EQUAL(L"\x1b[40m", Format::Bg::Black);
152 + VERIFY_ARE_EQUAL(L"\x1b[41m", Format::Bg::Red);
153 + VERIFY_ARE_EQUAL(L"\x1b[42m", Format::Bg::Green);
154 + VERIFY_ARE_EQUAL(L"\x1b[43m", Format::Bg::Yellow);
155 + VERIFY_ARE_EQUAL(L"\x1b[44m", Format::Bg::Blue);
156 + VERIFY_ARE_EQUAL(L"\x1b[45m", Format::Bg::Magenta);
157 + VERIFY_ARE_EQUAL(L"\x1b[46m", Format::Bg::Cyan);
158 + VERIFY_ARE_EQUAL(L"\x1b[47m", Format::Bg::White);
159 +
160 + VERIFY_ARE_EQUAL(L"\x1b[100m", Format::Bg::BrightBlack);
161 + VERIFY_ARE_EQUAL(L"\x1b[101m", Format::Bg::BrightRed);
162 + VERIFY_ARE_EQUAL(L"\x1b[102m", Format::Bg::BrightGreen);
163 + VERIFY_ARE_EQUAL(L"\x1b[103m", Format::Bg::BrightYellow);
164 + VERIFY_ARE_EQUAL(L"\x1b[104m", Format::Bg::BrightBlue);
165 + VERIFY_ARE_EQUAL(L"\x1b[105m", Format::Bg::BrightMagenta);
166 + VERIFY_ARE_EQUAL(L"\x1b[106m", Format::Bg::BrightCyan);
167 + VERIFY_ARE_EQUAL(L"\x1b[107m", Format::Bg::BrightWhite);
168 +
169 + VERIFY_ARE_EQUAL(L"\x1b[48;2;0;64;192m", Format::Bg::Extended(Format::Color{0, 64, 192}));
170 +
171 + VERIFY_ARE_EQUAL(
172 + L"\x1b]8;;https://example.com\x1b\\Click here\x1b]8;;\x1b\\",
173 + Format::Hyperlink(L"Click here", L"https://example.com"));
174 + }
175 +
176 + TEST_METHOD(VT_EraseSequences)
177 + {
178 + VERIFY_ARE_EQUAL(L"\x1b[K", Erase::LineForward);
179 + VERIFY_ARE_EQUAL(L"\x1b[1K", Erase::LineBackward);
180 + VERIFY_ARE_EQUAL(L"\x1b[2K", Erase::LineEntirely);
181 + VERIFY_ARE_EQUAL(L"\x1b[J", Erase::ScreenForward);
182 + VERIFY_ARE_EQUAL(L"\x1b[1J", Erase::ScreenBackward);
183 + VERIFY_ARE_EQUAL(L"\x1b[2J", Erase::ScreenEntirely);
184 + }
185 +
186 + TEST_METHOD(VT_ProgressSequences)
187 + {
188 + VERIFY_ARE_EQUAL(L"\x1b]9;4;0;0\x1b\\", Progress::Construct(Progress::State::None));
189 + VERIFY_ARE_EQUAL(L"\x1b]9;4;3;0\x1b\\", Progress::Construct(Progress::State::Indeterminate));
190 + VERIFY_ARE_EQUAL(L"\x1b]9;4;1;50\x1b\\", Progress::Construct(Progress::State::Normal, 50u));
191 + VERIFY_ARE_EQUAL(L"\x1b]9;4;2;75\x1b\\", Progress::Construct(Progress::State::Error, 75u));
192 + VERIFY_ARE_EQUAL(L"\x1b]9;4;4;25\x1b\\", Progress::Construct(Progress::State::Paused, 25u));
193 +
194 + VERIFY_THROWS_SPECIFIC(Progress::Construct(Progress::State::Normal), wil::ResultException, [](const wil::ResultException& e) {
195 + return e.GetErrorCode() == E_INVALIDARG;
196 + });
197 + VERIFY_THROWS_SPECIFIC(Progress::Construct(Progress::State::Normal, 101u), wil::ResultException, [](const wil::ResultException& e) {
198 + return e.GetErrorCode() == E_BOUNDS;
199 + });
200 + }
201 +
202 + TEST_METHOD(VT_IsColor)
203 + {
204 + // Named SGR sequences are color.
205 + VERIFY_IS_TRUE(Format::Bright.IsColor());
206 + VERIFY_IS_TRUE(Format::Dim.IsColor());
207 + VERIFY_IS_TRUE(Format::Fg::BrightRed.IsColor());
208 + VERIFY_IS_TRUE(Format::Default.IsColor());
209 +
210 + // Constructed multi-param SGR is color.
211 + VERIFY_IS_TRUE(Sgr({1, 31}).IsColor());
212 +
213 + // OSC 8 hyperlink is color-adjacent.
214 + VERIFY_IS_TRUE(Format::Hyperlink(L"text", L"https://example.com").IsColor());
215 +
216 + // Cursor movement is structural — not color.
217 + VERIFY_IS_FALSE(Cursor::Up(1).IsColor());
218 + VERIFY_IS_FALSE(Cursor::Home.IsColor());
219 +
220 + // Erase is structural — not color.
221 + VERIFY_IS_FALSE(Erase::LineForward.IsColor());
222 + VERIFY_IS_FALSE(Erase::ScreenForward.IsColor());
223 +
224 + // Progress is structural — not color.
225 + VERIFY_IS_FALSE(Progress::Construct(Progress::State::Normal, 50u).IsColor());
226 + }
227 +
228 + TEST_METHOD(VT_StringConcatenation)
229 + {
230 + // Sequence + Sequence
231 + VERIFY_ARE_EQUAL(std::wstring{L"\x1b[1m\x1b[0m"}, Format::Bright + Format::Default);
232 +
233 + // Sequence + string literal
234 + VERIFY_ARE_EQUAL(std::wstring{L"\x1b[1mhello"}, Format::Bright + L"hello");
235 +
236 + // string literal + Sequence
237 + VERIFY_ARE_EQUAL(std::wstring{L"hello\x1b[0m"}, L"hello" + Format::Default);
238 +
239 + // Sequence + std::wstring
240 + VERIFY_ARE_EQUAL(std::wstring{L"\x1b[1mhello"}, Format::Bright + std::wstring{L"hello"});
241 +
242 + // std::wstring + Sequence
243 + VERIFY_ARE_EQUAL(std::wstring{L"world\x1b[0m"}, std::wstring{L"world"} + Format::Default);
244 +
245 + // Chained: Sequence + Sequence + literal — verifies operator+ associativity.
246 + VERIFY_ARE_EQUAL(std::wstring{L"\x1b[?2004h\x1b[91mroot@ "}, Cursor::BracketedPasteOn + Format::Fg::BrightRed + L"root@ ");
247 +
248 + // In-place wide append.
249 + std::wstring buf;
250 + buf += Format::Default;
251 + buf += std::wstring{L"world"};
252 + VERIFY_ARE_EQUAL(std::wstring{L"\x1b[0mworld"}, buf);
253 + }
254 +
255 + TEST_METHOD(VT_ChangeTerminalMode)
256 + {
257 + auto buffer = MakeScreenBuffer();
258 + HANDLE h = buffer.get();
259 +
260 + // Capture the original cursor visibility.
261 + CONSOLE_CURSOR_INFO original{};
262 + VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleCursorInfo(h, &original));
263 +
264 + {
265 + // Hide the cursor.
266 + ChangeTerminalMode hide{h, false};
267 + VERIFY_IS_TRUE(hide.IsConsole());
268 +
269 + CONSOLE_CURSOR_INFO info{};
270 + VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleCursorInfo(h, &info));
271 + VERIFY_IS_FALSE(!!info.bVisible);
272 + }
273 +
274 + // Destructor must restore the original visibility.
275 + CONSOLE_CURSOR_INFO restored{};
276 + VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleCursorInfo(h, &restored));
277 + VERIFY_ARE_EQUAL(original.bVisible, restored.bVisible);
278 +
279 + {
280 + // Show the cursor explicitly.
281 + ChangeTerminalMode show{h, true};
282 + CONSOLE_CURSOR_INFO info{};
283 + VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleCursorInfo(h, &info));
284 + VERIFY_IS_TRUE(!!info.bVisible);
285 + }
286 +
287 + // Non-console handle (a pipe) is silently ignored — IsConsole() returns false.
288 + wil::unique_handle readPipe, writePipe;
289 + VERIFY_WIN32_BOOL_SUCCEEDED(CreatePipe(&readPipe, &writePipe, nullptr, 0));
290 + ChangeTerminalMode nonConsole{readPipe.get(), false};
291 + VERIFY_IS_FALSE(nonConsole.IsConsole());
292 + }
293 +
294 + TEST_METHOD(VT_ChangeTerminalMode_RedirectedHandles)
295 + {
296 + wil::unique_hfile file{
297 + CreateFileW(L"NUL", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, 0, nullptr)};
298 + VERIFY_IS_TRUE(!!file);
299 + {
300 + ChangeTerminalMode mode{file.get(), false};
301 + VERIFY_IS_FALSE(mode.IsConsole());
302 + }
303 +
304 + // NULL handle.
305 + {
306 + ChangeTerminalMode mode{nullptr, false};
307 + VERIFY_IS_FALSE(mode.IsConsole());
308 + }
309 +
310 + // INVALID_HANDLE_VALUE.
311 + {
312 + ChangeTerminalMode mode{INVALID_HANDLE_VALUE, false};
313 + VERIFY_IS_FALSE(mode.IsConsole());
314 + }
315 + }
316 +
317 + TEST_METHOD(VT_EnableVirtualTerminal)
318 + {
319 + auto buffer = MakeScreenBuffer();
320 + HANDLE h = buffer.get();
321 +
322 + DWORD baseline{};
323 + VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleMode(h, &baseline));
324 + VERIFY_WIN32_BOOL_SUCCEEDED(SetConsoleMode(h, baseline & ~ENABLE_VIRTUAL_TERMINAL_PROCESSING));
325 +
326 + {
327 + EnableVirtualTerminal vt{h};
328 + VERIFY_IS_TRUE(vt.IsVTEnabled());
329 +
330 + DWORD mode{};
331 + VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleMode(h, &mode));
332 + VERIFY_IS_TRUE(!!(mode & ENABLE_VIRTUAL_TERMINAL_PROCESSING));
333 + }
334 +
335 + DWORD restored{};
336 + VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleMode(h, &restored));
337 + VERIFY_IS_FALSE(!!(restored & ENABLE_VIRTUAL_TERMINAL_PROCESSING));
338 +
339 + // With DISABLE_NEWLINE_AUTO_RETURN requested.
340 + {
341 + EnableVirtualTerminal vtWithNewline{h, EnableVirtualTerminal::Mode::Output, true};
342 + VERIFY_IS_TRUE(vtWithNewline.IsVTEnabled());
343 +
344 + DWORD mode{};
345 + VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleMode(h, &mode));
346 + VERIFY_IS_TRUE(!!(mode & ENABLE_VIRTUAL_TERMINAL_PROCESSING));
347 + }
348 +
349 + // Non-console handles are silently ignored for both modes.
350 + wil::unique_handle readPipe, writePipe;
351 + VERIFY_WIN32_BOOL_SUCCEEDED(CreatePipe(&readPipe, &writePipe, nullptr, 0));
352 + VERIFY_IS_FALSE(EnableVirtualTerminal(readPipe.get(), EnableVirtualTerminal::Mode::Output).IsVTEnabled());
353 + VERIFY_IS_FALSE(EnableVirtualTerminal(readPipe.get(), EnableVirtualTerminal::Mode::Input).IsVTEnabled());
354 + }
355 +
356 + TEST_METHOD(VT_EnableVirtualTerminal_InputMode)
357 + {
358 + // CONIN$ requires an attached console. CI environments that run without one
359 + // (e.g. headless agents) cannot exercise this path; skip rather than fail.
360 + wil::unique_hfile conin{CreateFileW(
361 + L"CONIN$", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, 0, nullptr)};
362 + if (!conin)
363 + {
364 + LogSkipped("Skipping input-mode VT test: CONIN$ is not available (no attached console)");
365 + return;
366 + }
367 +
368 + DWORD inputBaseline{};
369 + VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleMode(conin.get(), &inputBaseline));
370 + {
371 + EnableVirtualTerminal vt{conin.get(), EnableVirtualTerminal::Mode::Input};
372 + VERIFY_IS_TRUE(vt.IsVTEnabled());
373 +
374 + DWORD mode{};
375 + VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleMode(conin.get(), &mode));
376 + VERIFY_IS_TRUE(!!(mode & ENABLE_VIRTUAL_TERMINAL_INPUT));
377 + VERIFY_IS_FALSE(!!(mode & ENABLE_LINE_INPUT));
378 + VERIFY_IS_FALSE(!!(mode & ENABLE_ECHO_INPUT));
379 + }
380 +
381 + DWORD inputRestored{};
382 + VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleMode(conin.get(), &inputRestored));
383 + VERIFY_ARE_EQUAL(inputBaseline, inputRestored);
384 + }
385 +
386 + TEST_METHOD(VT_EnableVirtualTerminal_RedirectedHandles)
387 + {
388 + wil::unique_hfile file{
389 + CreateFileW(L"NUL", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, 0, nullptr)};
390 + VERIFY_IS_TRUE(!!file);
391 +
392 + VERIFY_IS_FALSE(EnableVirtualTerminal(file.get(), EnableVirtualTerminal::Mode::Output).IsVTEnabled());
393 + VERIFY_IS_FALSE(EnableVirtualTerminal(file.get(), EnableVirtualTerminal::Mode::Input).IsVTEnabled());
394 + VERIFY_IS_FALSE(EnableVirtualTerminal(nullptr, EnableVirtualTerminal::Mode::Output).IsVTEnabled());
395 + VERIFY_IS_FALSE(EnableVirtualTerminal(nullptr, EnableVirtualTerminal::Mode::Input).IsVTEnabled());
396 + VERIFY_IS_FALSE(EnableVirtualTerminal(INVALID_HANDLE_VALUE, EnableVirtualTerminal::Mode::Output).IsVTEnabled());
397 + VERIFY_IS_FALSE(EnableVirtualTerminal(INVALID_HANDLE_VALUE, EnableVirtualTerminal::Mode::Input).IsVTEnabled());
398 + }
399 +
400 + TEST_METHOD(VT_EnableVirtualTerminal_AlreadyEnabled_Output)
401 + {
402 + // Regression: when VT_PROC is already set on the console, EnableVirtualTerminal
403 + // must report VT as enabled (so callers gate VT output correctly) AND must not
404 + // claim restore ownership — destruction must leave the pre-existing mode alone.
405 + auto buffer = MakeScreenBuffer();
406 + HANDLE h = buffer.get();
407 +
408 + DWORD baseline{};
409 + VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleMode(h, &baseline));
410 + const DWORD scopeExitRestore = baseline;
411 + auto restoreBaseline = wil::scope_exit([&] { ::SetConsoleMode(h, scopeExitRestore); });
412 +
413 + // Pre-enable VT processing so the constructor's tryEnable() short-circuits
414 + // on the "flags already set" path.
415 + const DWORD preEnabled = baseline | ENABLE_VIRTUAL_TERMINAL_PROCESSING;
416 + VERIFY_WIN32_BOOL_SUCCEEDED(SetConsoleMode(h, preEnabled));
417 +
418 + {
419 + EnableVirtualTerminal vt{h};
420 + VERIFY_IS_TRUE(vt.IsVTEnabled(), L"VT must be reported as enabled when it was already enabled on the handle");
421 +
422 + DWORD mode{};
423 + VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleMode(h, &mode));
424 + VERIFY_ARE_EQUAL(preEnabled, mode, L"Constructor must not change the mode when the requested flags are already set");
425 + }
426 +
427 + // Destructor must not have restored anything — the pre-existing VT_PROC
428 + // bit must still be set after the EnableVirtualTerminal instance goes
429 + // out of scope.
430 + DWORD afterScope{};
431 + VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleMode(h, &afterScope));
432 + VERIFY_ARE_EQUAL(preEnabled, afterScope, L"Destructor must not restore mode when constructor did not change it");
433 + }
434 +
435 + TEST_METHOD(VT_EnableVirtualTerminal_AlreadyEnabled_OutputWithDisableNewlineAutoReturn)
436 + {
437 + // Same regression as above, exercising the disableNewlineAutoReturn=true path
438 + // where the constructor tries (VT_PROC | DISABLE_NEWLINE_AUTO_RETURN) first.
439 + auto buffer = MakeScreenBuffer();
440 + HANDLE h = buffer.get();
441 +
442 + DWORD baseline{};
443 + VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleMode(h, &baseline));
444 + const DWORD scopeExitRestore = baseline;
445 + auto restoreBaseline = wil::scope_exit([&] { ::SetConsoleMode(h, scopeExitRestore); });
446 +
447 + const DWORD preEnabled = baseline | ENABLE_VIRTUAL_TERMINAL_PROCESSING | DISABLE_NEWLINE_AUTO_RETURN;
448 + if (!SetConsoleMode(h, preEnabled))
449 + {
450 + // DISABLE_NEWLINE_AUTO_RETURN is not supported on all conhost builds; skip
451 + // rather than fail in environments where it cannot be set.
452 + LogSkipped("Skipping DISABLE_NEWLINE_AUTO_RETURN test: SetConsoleMode rejected the flag");
453 + return;
454 + }
455 +
456 + {
457 + EnableVirtualTerminal vt{h, EnableVirtualTerminal::Mode::Output, true};
458 + VERIFY_IS_TRUE(vt.IsVTEnabled());
459 +
460 + DWORD mode{};
461 + VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleMode(h, &mode));
462 + VERIFY_ARE_EQUAL(preEnabled, mode);
463 + }
464 +
465 + DWORD afterScope{};
466 + VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleMode(h, &afterScope));
467 + VERIFY_ARE_EQUAL(preEnabled, afterScope);
468 + }
469 +
470 + TEST_METHOD(VT_EnableVirtualTerminal_AlreadyEnabled_Input)
471 + {
472 + // Regression: when CONIN$ is already in the exact target mode
473 + // (ENABLE_VIRTUAL_TERMINAL_INPUT + ENABLE_EXTENDED_FLAGS, no ENABLE_LINE_INPUT,
474 + // no ENABLE_ECHO_INPUT), the constructor's "no change needed" early-return
475 + // must still report IsVTEnabled()==true and must not touch the mode.
476 + wil::unique_hfile conin{CreateFileW(
477 + L"CONIN$", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, 0, nullptr)};
478 + if (!conin)
479 + {
480 + LogSkipped("Skipping input-mode VT already-enabled test: CONIN$ is not available (no attached console)");
481 + return;
482 + }
483 +
484 + DWORD baseline{};
485 + VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleMode(conin.get(), &baseline));
486 + const DWORD scopeExitRestore = baseline;
487 + auto restoreBaseline = wil::scope_exit([&] { ::SetConsoleMode(conin.get(), scopeExitRestore); });
488 +
489 + // Pre-configure CONIN$ to exactly match what EnableVirtualTerminal would set,
490 + // so the constructor takes the newMode == current early-return path.
491 + const DWORD preEnabled = (baseline & ~(ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT)) | ENABLE_EXTENDED_FLAGS | ENABLE_VIRTUAL_TERMINAL_INPUT;
492 + VERIFY_WIN32_BOOL_SUCCEEDED(SetConsoleMode(conin.get(), preEnabled));
493 +
494 + {
495 + EnableVirtualTerminal vt{conin.get(), EnableVirtualTerminal::Mode::Input};
496 + VERIFY_IS_TRUE(vt.IsVTEnabled(), L"VT input must be reported as enabled when CONIN$ was already in the target mode");
497 +
498 + DWORD mode{};
499 + VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleMode(conin.get(), &mode));
500 + VERIFY_ARE_EQUAL(preEnabled, mode, L"Constructor must not change input mode when it already matches the target");
501 + }
502 +
503 + DWORD afterScope{};
504 + VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleMode(conin.get(), &afterScope));
505 + VERIFY_ARE_EQUAL(preEnabled, afterScope, L"Destructor must not restore input mode when constructor did not change it");
506 + }
507 +
508 + TEST_METHOD(VT_PrimaryDeviceAttributes)
509 + {
510 + // Clean DA1 response: conformance level 62, extensions Columns132 (1) and Sixel (4).
511 + {
512 + std::wostringstream out;
513 + std::wistringstream in{L"\x1b[?62;1;4c"};
514 + PrimaryDeviceAttributes da{out, in};
515 +
516 + VERIFY_IS_TRUE(da.Supports(PrimaryDeviceAttributes::Extension::Columns132));
517 + VERIFY_IS_TRUE(da.Supports(PrimaryDeviceAttributes::Extension::Sixel));
518 + VERIFY_IS_FALSE(da.Supports(PrimaryDeviceAttributes::Extension::PrinterPort));
519 + // DA1 request must have been written to the output stream.
520 + VERIFY_ARE_EQUAL(std::wstring{L"\x1b[0c"}, out.str());
521 + }
522 +
523 + // Trailing plain text (e.g. queued user input) must not break parsing.
524 + {
525 + std::wostringstream out;
526 + std::wistringstream in{L"\x1b[?62;1;4chello"};
527 + PrimaryDeviceAttributes da{out, in};
528 +
529 + VERIFY_IS_TRUE(da.Supports(PrimaryDeviceAttributes::Extension::Columns132));
530 + VERIFY_IS_TRUE(da.Supports(PrimaryDeviceAttributes::Extension::Sixel));
531 + }
532 +
533 + // Trailing VT sequence must not corrupt suffix search or result extraction.
534 + {
535 + std::wostringstream out;
536 + std::wistringstream in{L"\x1b[?62;6c\x1b[0m"};
537 + PrimaryDeviceAttributes da{out, in};
538 +
539 + VERIFY_IS_TRUE(da.Supports(PrimaryDeviceAttributes::Extension::SelectiveErase));
540 + VERIFY_IS_FALSE(da.Supports(PrimaryDeviceAttributes::Extension::Columns132));
541 + }
542 +
543 + // Empty/malformed response — should not throw, extensions remain unset.
544 + {
545 + std::wostringstream out;
546 + std::wistringstream in{L""};
547 + PrimaryDeviceAttributes da{out, in};
548 +
549 + VERIFY_IS_FALSE(da.Supports(PrimaryDeviceAttributes::Extension::Columns132));
550 + }
551 + }
552 +
553 + TEST_METHOD(VT_PrimaryDeviceAttributes_Empty)
554 + {
555 + // Empty/malformed response — should not throw, extensions remain unset.
556 + std::wostringstream out;
557 + std::wistringstream in{L""};
558 +
559 + PrimaryDeviceAttributes da{out, in};
560 +
561 + VERIFY_IS_FALSE(da.Supports(PrimaryDeviceAttributes::Extension::Columns132));
562 + }
563 +};
564 +
565 +} // namespace WSLCCLIVTSupportUnitTests
test/windows/wslc/e2e/WSLCE2EHelpers.h
+22 -36
@@ -19,55 +19,41 @@ Abstract:
19 #include <wslc_schema.h>
20 #include <ContainerModel.h>
21 #include <WSLCContainerLauncher.h>
22 +#include "VTSupport.h"
23
24 namespace WSLCE2ETests {
25
25 -// VT100/ANSI escape sequence constants for TTY testing
26 +// VT sequence constants and helpers for TTY testing.
27 +// Sequences are sourced from wsl::windows::common::vt (VTSupport.h).
28 namespace VT {
27 -// Bracketed paste mode control sequences
28 -#define VT_B_START "\x1b[?2004h" // Enable bracketed paste mode
29 -#define VT_B_END "\x1b[?2004l" // Disable bracketed paste mode
30 -
31 -// Color/formatting sequences
32 -#define VT_RESET "\x1b[0m" // Reset all attributes
33 -#define VT_RED "\x1b[1;31m" // Bold red text
34 -
35 -// Terminal control sequences
36 -#define VT_ERASE_LINE "\x1b[K" // Erase from cursor to end of line
37 -#define VT_CR "\r" // Carriage return
38 -
39 - // Prompt patterns used in WSLC.
40 - constexpr auto SESSION_PROMPT = VT_B_START VT_RED "root@ [ " VT_RESET "/" VT_RED " ]# ";
41 -
42 - // Constexpr representations of the control sequences for use in tests.
43 - constexpr auto B_START = VT_B_START;
44 - constexpr auto B_END = VT_B_END;
45 - constexpr auto RESET = VT_RESET;
46 - constexpr auto RED = VT_RED;
47 - constexpr auto ERASE_LINE = VT_ERASE_LINE;
48 - constexpr auto CR = VT_CR;
49 -
50 -// Remove macros to avoid polluting global namespace.
51 -#undef VT_B_START
52 -#undef VT_B_END
53 -#undef VT_RESET
54 -#undef VT_RED
55 -#undef VT_ERASE_LINE
56 -#undef VT_CR
57 -
58 - // Helper function to build container prompt
29 + using namespace wsl::windows::common::vt;
30 +
31 + inline const auto& B_START = Cursor::BracketedPasteOn;
32 + inline const auto& B_END = Cursor::BracketedPasteOff;
33 + inline const auto& RESET = Format::Default;
34 + inline const auto& ERASE_LINE = Erase::LineForward;
35 + inline const Sequence CR{L"\r"};
36 +
37 + // The shell PS1 uses SGR 1;31 (bold + red) in a single sequence.
38 + // Sgr({1, 31}) produces L"\x1b[1;31m" to match exactly.
39 + inline const ConstructedSequence RED = Sgr({1, 31});
40 +
41 + // Prompt pattern used in WSLC TTY sessions.
42 + inline const std::string SESSION_PROMPT =
43 + wsl::shared::string::WideToMultiByte(B_START + RED + L"root@ [ " + RESET + L"/" + RED + L" ]# ");
44 +
45 inline std::string BuildContainerPrompt(const std::string& prompt, bool withBracketedPaste = true)
46 {
47 if (withBracketedPaste)
48 {
63 - return std::format("{}{}", B_START, prompt);
49 + return wsl::shared::string::WideToMultiByte(std::format(L"{}", B_START)) + prompt;
50 }
65 - return std::format("{}", prompt);
51 + return prompt;
52 }
53
54 inline std::string BuildContainerAttachPrompt(const std::string& prompt)
55 {
70 - return std::format("{}{}{}{}", CR, ERASE_LINE, CR, prompt);
56 + return wsl::shared::string::WideToMultiByte(std::format(L"{}{}{}", CR, ERASE_LINE, CR)) + prompt;
57 }
58 } // namespace VT
59
test/windows/wslc/e2e/WSLCExecutor.cpp
+1 -1
@@ -391,7 +391,7 @@ void WSLCInteractiveSession::ExpectStderr(const std::string& expected)
391 void WSLCInteractiveSession::ExpectCommandEcho(const std::string& command)
392 {
393 // TTY mode: expect command echo, then B_END and carriage return
394 - ExpectStdout(std::format("{}\r\n{}\r", command, VT::B_END));
394 + ExpectStdout(std::format("{}\r\n{}\r", command, wsl::shared::string::WideToMultiByte(std::wstring(VT::B_END.Get()))));
395 }
396
397 void WSLCInteractiveSession::IgnoreSequence(const std::string& sequence)
test/windows/wslc/e2e/WSLCExecutor.h
+11
@@ -17,6 +17,7 @@ Abstract:
17
18 #include "precomp.h"
19 #include "windows/Common.h"
20 +#include "VTSupport.h"
21
22 namespace WSLCE2ETests {
23
@@ -87,6 +88,16 @@ struct WSLCInteractiveSession
88 void ExpectStderr(const std::string& expected);
89 void ExpectCommandEcho(const std::string& command);
90
91 + // Convenience overloads for VT sequence helpers.
92 + void ExpectStdout(const wsl::windows::common::vt::Sequence& expected)
93 + {
94 + ExpectStdout(wsl::windows::common::string::WideToMultiByte(expected.Get()));
95 + }
96 + void ExpectStderr(const wsl::windows::common::vt::Sequence& expected)
97 + {
98 + ExpectStderr(wsl::windows::common::string::WideToMultiByte(expected.Get()));
99 + }
100 +
101 void IgnoreSequence(const std::string& sequence);
102
103 std::string GetStdoutData() const;