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, ¤t))
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;