master
cpp 553 lines 17.5 KB
Raw
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
445 ConstructedSequence LinkOpen(const std::wstring& url)
446 {
447 return ConstructedSequence{std::format(WSL_WINDOWS_VT_OSC L"8;;{}" WSL_WINDOWS_VT_ESCAPE L"\\", url)};
448 }
449
450 const Sequence LinkClose{WSL_WINDOWS_VT_OSC L"8;;" WSL_WINDOWS_VT_ESCAPE L"\\"};
451 } // namespace Format
452
453 namespace Erase {
454 const Sequence LineForward{WSL_WINDOWS_VT_CSI L"K"};
455 const Sequence LineBackward{WSL_WINDOWS_VT_CSI L"1K"};
456 const Sequence LineEntirely{WSL_WINDOWS_VT_CSI L"2K"};
457 const Sequence ScreenForward{WSL_WINDOWS_VT_CSI L"J"};
458 const Sequence ScreenBackward{WSL_WINDOWS_VT_CSI L"1J"};
459 const Sequence ScreenEntirely{WSL_WINDOWS_VT_CSI L"2J"};
460 } // namespace Erase
461
462 namespace Progress {
463 ConstructedSequence Construct(State state, std::optional<uint32_t> percentage)
464 {
465 // See https://conemu.github.io/en/AnsiEscapeCodes.html#ConEmu_specific_OSC
466
467 THROW_HR_IF(E_BOUNDS, percentage.has_value() && percentage.value() > 100u);
468
469 // Workaround some quirks in the Windows Terminal implementation of the progress OSC sequence
470 switch (state)
471 {
472 case State::None:
473 case State::Indeterminate:
474 // Windows Terminal does not recognize the OSC sequence if the progress value is left out.
475 // As a workaround, we can specify an arbitrary value since it does not matter for None and Indeterminate states.
476 percentage = percentage.value_or(0);
477 break;
478 case State::Normal:
479 case State::Error:
480 case State::Paused:
481 // Windows Terminal does not support switching progress states without also setting a progress value at the same time,
482 // so we disallow this case for now.
483 THROW_HR_IF(E_INVALIDARG, !percentage.has_value());
484 break;
485 }
486
487 int stateId;
488 switch (state)
489 {
490 case State::None:
491 stateId = 0;
492 break;
493 case State::Indeterminate:
494 stateId = 3;
495 break;
496 case State::Normal:
497 stateId = 1;
498 break;
499 case State::Error:
500 stateId = 2;
501 break;
502 case State::Paused:
503 stateId = 4;
504 break;
505 default:
506 THROW_HR(E_UNEXPECTED);
507 }
508
509 std::wostringstream result;
510 result << WSL_WINDOWS_VT_OSC L"9;4;" << stateId << L";";
511 if (percentage.has_value())
512 {
513 result << percentage.value();
514 }
515 result << WSL_WINDOWS_VT_ESCAPE << L"\\";
516 return ConstructedSequence{std::move(result).str()};
517 }
518 } // namespace Progress
519
520 std::wstring operator+(const Sequence& lhs, const Sequence& rhs)
521 {
522 std::wstring out;
523 out.reserve(lhs.Get().size() + rhs.Get().size());
524 out.append(lhs.Get()).append(rhs.Get());
525 return out;
526 }
527
528 std::wstring operator+(const Sequence& lhs, const std::wstring& rhs)
529 {
530 return std::wstring{lhs.Get()} + rhs;
531 }
532
533 std::wstring operator+(const std::wstring& lhs, const Sequence& rhs)
534 {
535 return lhs + std::wstring{rhs.Get()};
536 }
537
538 std::wstring operator+(const Sequence& lhs, const wchar_t* rhs)
539 {
540 return std::wstring{lhs.Get()} + rhs;
541 }
542
543 std::wstring operator+(const wchar_t* lhs, const Sequence& rhs)
544 {
545 return lhs + std::wstring{rhs.Get()};
546 }
547
548 std::wstring& operator+=(std::wstring& lhs, const Sequence& rhs)
549 {
550 lhs.append(rhs.Get());
551 return lhs;
552 }
553 } // namespace wsl::windows::common::vt