-
Notifications
You must be signed in to change notification settings - Fork 1.8k
CLI: Add VT Support library and update tests/callbacks to use it #40710
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
dkbennett
wants to merge
14
commits into
master
Choose a base branch
from
user/dkbennett/vt2
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 8 commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
862862a
Add VT Support library and update tests/callbacks to use it
dkbennett a61f917
Fix incorrect comment headers
dkbennett 637e6d8
Add format specialization and use std::format in a few places, fix im…
dkbennett 8d0fe9b
Restore executor std format use.
dkbennett e08127c
Formatting and copilot feedback
dkbennett 4f87046
Copilot header add
dkbennett 7b01aba
PR feedback update
dkbennett 5531262
Fix erase line forward default parameter
dkbennett bf48334
copilot feedback fixes
dkbennett d419575
Update peek comment
dkbennett 4457b08
More copilot feedback
dkbennett bfc0381
Update for wide output
dkbennett 08a767c
Update for color detection, fix feedback item
dkbennett 4850570
Copilot review adjustments
dkbennett File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,355 @@ | ||
| /*++ | ||
|
|
||
| Copyright (c) Microsoft. All rights reserved. | ||
|
|
||
| Module Name: | ||
|
|
||
| VTSupport.cpp | ||
|
|
||
| Abstract: | ||
|
|
||
| This file contains the implementation of VT sequence constructors and | ||
| console mode helpers declared in VTSupport.h. | ||
|
|
||
| --*/ | ||
|
|
||
| #include "precomp.h" | ||
| #include "VTSupport.h" | ||
|
|
||
| namespace wsl::windows::common::vt { | ||
| namespace { | ||
| // Extracts a VT sequence of the form ESC + prefix + result + suffix, returning | ||
| // the result part. Reads up to s_bufferSize bytes non-blockingly; if the buffer | ||
| // fills completely the suffix may not be present, in which case an empty string | ||
| // is returned — the same outcome as any other parse failure. Any bytes after | ||
| // the suffix (e.g. queued user input) are ignored. | ||
| std::string ExtractSequence(std::istream& inStream, std::string_view prefix, std::string_view suffix) | ||
| { | ||
| // Force discovery of available input | ||
| std::ignore = inStream.peek(); | ||
|
|
||
| static constexpr std::streamsize s_bufferSize = 1024; | ||
| char buffer[s_bufferSize]; | ||
|
|
||
| // readsome() returns at most s_bufferSize, so == s_bufferSize is the full-buffer | ||
| // case, not overflow. If the suffix is still within those bytes the parse succeeds | ||
| // normally; if not, the suffix-not-found path below returns {}. | ||
| std::streamsize bytesRead = inStream.readsome(buffer, s_bufferSize); | ||
|
|
||
|
dkbennett marked this conversation as resolved.
|
||
| std::string_view resultView{buffer, static_cast<size_t>(bytesRead)}; | ||
|
|
||
| // Locate the escape character that begins the sequence. | ||
| const size_t escapeIndex = resultView.find(WSL_WINDOWS_VT_ESCAPE[0]); | ||
| if (escapeIndex == std::string_view::npos) | ||
| { | ||
| return {}; | ||
| } | ||
|
|
||
| resultView = resultView.substr(escapeIndex); | ||
|
|
||
| // Verify the prefix immediately follows the escape character. | ||
| if (resultView.length() < 1 + prefix.length() || resultView.substr(1, prefix.length()) != prefix) | ||
| { | ||
| return {}; | ||
| } | ||
|
|
||
| // Find the suffix anywhere after the prefix. | ||
| const std::string_view body = resultView.substr(1 + prefix.length()); | ||
| const size_t suffixIndex = body.find(suffix); | ||
| if (suffixIndex == std::string_view::npos) | ||
| { | ||
| return {}; | ||
| } | ||
|
|
||
| return std::string{body.substr(0, suffixIndex)}; | ||
| } | ||
| } // namespace | ||
|
|
||
| void ConstructedSequence::Append(const Sequence& sequence) | ||
| { | ||
| if (!sequence.Get().empty()) | ||
| { | ||
| m_str += sequence.Get(); | ||
| Set(m_str); | ||
| } | ||
| } | ||
|
|
||
| void ConstructedSequence::Clear() | ||
| { | ||
| m_str.clear(); | ||
| Set(m_str); | ||
| } | ||
|
|
||
| } // namespace wsl::windows::common::vt | ||
|
|
||
| // The beginning of a Control Sequence Introducer | ||
| #define WSL_WINDOWS_VT_CSI WSL_WINDOWS_VT_ESCAPE "[" | ||
|
|
||
| // The beginning of an Operating system command | ||
| #define WSL_WINDOWS_VT_OSC WSL_WINDOWS_VT_ESCAPE "]" | ||
|
|
||
| // Define a text formatting sequence with an integer id | ||
| #define WSL_WINDOWS_VT_TEXTFORMAT(_id_) WSL_WINDOWS_VT_CSI #_id_ "m" | ||
|
|
||
| namespace wsl::windows::common::vt { | ||
| ConstructedSequence Sgr(std::initializer_list<int> params) | ||
| { | ||
| std::ostringstream result; | ||
| result << WSL_WINDOWS_VT_CSI; | ||
| bool first = true; | ||
| for (const int param : params) | ||
| { | ||
| if (!first) | ||
| { | ||
| result << ';'; | ||
| } | ||
| result << param; | ||
| first = false; | ||
| } | ||
| result << 'm'; | ||
| return ConstructedSequence{std::move(result).str()}; | ||
| } | ||
|
|
||
| PrimaryDeviceAttributes::PrimaryDeviceAttributes(std::ostream& outStream, std::istream& inStream) | ||
| { | ||
| try | ||
| { | ||
| // Best-effort: enable VT input on the real console handle so the terminal | ||
| // sends a machine-readable DA1 response. When stdin is redirected (e.g. | ||
| // in unit tests that supply their own stringstreams) this will fail, but | ||
| // we still proceed — the caller is responsible for providing a readable | ||
| // inStream that contains the DA1 response. | ||
| EnableVirtualTerminal inputMode{GetStdHandle(STD_INPUT_HANDLE), EnableVirtualTerminal::Mode::Input}; | ||
|
|
||
| // Send DA1 Primary Device Attributes request | ||
| outStream << WSL_WINDOWS_VT_CSI << "0c"; | ||
| outStream.flush(); | ||
|
|
||
| // Response is of the form WSL_WINDOWS_VT_CSI ? <conformance level> ; (<extension number> ;)* c | ||
| std::string sequence = ExtractSequence(inStream, "[?", "c"); | ||
| std::vector<std::string> values = wsl::shared::string::Split(sequence, ';'); | ||
|
|
||
| if (!values.empty()) | ||
| { | ||
| m_conformanceLevel = std::stoul(values[0]); | ||
| } | ||
|
|
||
| // m_extensions is a uint64_t bitmask; extension values >= 64 cannot be | ||
| // represented and are silently ignored to avoid undefined behaviour from | ||
| // an out-of-range shift. | ||
| constexpr unsigned long c_maxExtensionBit = 63ul; | ||
| for (size_t i = 1; i < values.size(); ++i) | ||
| { | ||
| const unsigned long ext = std::stoul(values[i]); | ||
| if (ext <= c_maxExtensionBit) | ||
| { | ||
| m_extensions |= 1ull << ext; | ||
| } | ||
| } | ||
| } | ||
| CATCH_LOG(); | ||
| } | ||
|
|
||
| bool PrimaryDeviceAttributes::Supports(Extension extension) const | ||
| { | ||
| uint64_t extensionMask = 1ull << ToIntegral(extension); | ||
| return (m_extensions & extensionMask) == extensionMask; | ||
| } | ||
|
|
||
| namespace Cursor { | ||
| ConstructedSequence Up(int cells) | ||
| { | ||
| THROW_HR_IF(E_INVALIDARG, cells < 0); | ||
| std::ostringstream result; | ||
| result << WSL_WINDOWS_VT_CSI << cells << 'A'; | ||
| return ConstructedSequence{std::move(result).str()}; | ||
| } | ||
|
|
||
| ConstructedSequence Down(int cells) | ||
| { | ||
| THROW_HR_IF(E_INVALIDARG, cells < 0); | ||
| std::ostringstream result; | ||
| result << WSL_WINDOWS_VT_CSI << cells << 'B'; | ||
| return ConstructedSequence{std::move(result).str()}; | ||
| } | ||
|
|
||
| ConstructedSequence Forward(int cells) | ||
| { | ||
| THROW_HR_IF(E_INVALIDARG, cells < 0); | ||
| std::ostringstream result; | ||
| result << WSL_WINDOWS_VT_CSI << cells << 'C'; | ||
| return ConstructedSequence{std::move(result).str()}; | ||
| } | ||
|
|
||
| ConstructedSequence Backward(int cells) | ||
| { | ||
| THROW_HR_IF(E_INVALIDARG, cells < 0); | ||
| std::ostringstream result; | ||
| result << WSL_WINDOWS_VT_CSI << cells << 'D'; | ||
| return ConstructedSequence{std::move(result).str()}; | ||
| } | ||
|
|
||
| ConstructedSequence MoveTo(int row, int col) | ||
| { | ||
| THROW_HR_IF(E_INVALIDARG, row < 1 || col < 1); | ||
| std::ostringstream result; | ||
| result << WSL_WINDOWS_VT_CSI << row << ';' << col << 'H'; | ||
| return ConstructedSequence{std::move(result).str()}; | ||
| } | ||
|
|
||
| const Sequence Home{WSL_WINDOWS_VT_CSI "H"}; | ||
| const Sequence SavePos{WSL_WINDOWS_VT_CSI "s"}; | ||
| const Sequence RestorePos{WSL_WINDOWS_VT_CSI "u"}; | ||
|
dkbennett marked this conversation as resolved.
Outdated
|
||
|
|
||
| const Sequence EnableBlink{WSL_WINDOWS_VT_CSI "?12h"}; | ||
| const Sequence DisableBlink{WSL_WINDOWS_VT_CSI "?12l"}; | ||
| const Sequence Show{WSL_WINDOWS_VT_CSI "?25h"}; | ||
| const Sequence Hide{WSL_WINDOWS_VT_CSI "?25l"}; | ||
|
|
||
| const Sequence BracketedPasteOn{WSL_WINDOWS_VT_CSI "?2004h"}; | ||
| const Sequence BracketedPasteOff{WSL_WINDOWS_VT_CSI "?2004l"}; | ||
| } // namespace Cursor | ||
|
|
||
| namespace Format { | ||
| const Sequence Default{WSL_WINDOWS_VT_TEXTFORMAT(0)}; | ||
| const Sequence Negative{WSL_WINDOWS_VT_TEXTFORMAT(7)}; | ||
| const Sequence Bright{WSL_WINDOWS_VT_TEXTFORMAT(1)}; | ||
| const Sequence Dim{WSL_WINDOWS_VT_TEXTFORMAT(2)}; | ||
| const Sequence Normal{WSL_WINDOWS_VT_TEXTFORMAT(22)}; | ||
| const Sequence Italic{WSL_WINDOWS_VT_TEXTFORMAT(3)}; | ||
| const Sequence NoItalic{WSL_WINDOWS_VT_TEXTFORMAT(23)}; | ||
| const Sequence Underline{WSL_WINDOWS_VT_TEXTFORMAT(4)}; | ||
| const Sequence NoUnderline{WSL_WINDOWS_VT_TEXTFORMAT(24)}; | ||
|
|
||
| namespace Fg { | ||
| const Sequence Black{WSL_WINDOWS_VT_TEXTFORMAT(30)}; | ||
| const Sequence Red{WSL_WINDOWS_VT_TEXTFORMAT(31)}; | ||
| const Sequence Green{WSL_WINDOWS_VT_TEXTFORMAT(32)}; | ||
| const Sequence Yellow{WSL_WINDOWS_VT_TEXTFORMAT(33)}; | ||
| const Sequence Blue{WSL_WINDOWS_VT_TEXTFORMAT(34)}; | ||
| const Sequence Magenta{WSL_WINDOWS_VT_TEXTFORMAT(35)}; | ||
| const Sequence Cyan{WSL_WINDOWS_VT_TEXTFORMAT(36)}; | ||
| const Sequence White{WSL_WINDOWS_VT_TEXTFORMAT(37)}; | ||
|
|
||
| const Sequence BrightBlack{WSL_WINDOWS_VT_TEXTFORMAT(90)}; | ||
| const Sequence BrightRed{WSL_WINDOWS_VT_TEXTFORMAT(91)}; | ||
| const Sequence BrightGreen{WSL_WINDOWS_VT_TEXTFORMAT(92)}; | ||
| const Sequence BrightYellow{WSL_WINDOWS_VT_TEXTFORMAT(93)}; | ||
| const Sequence BrightBlue{WSL_WINDOWS_VT_TEXTFORMAT(94)}; | ||
| const Sequence BrightMagenta{WSL_WINDOWS_VT_TEXTFORMAT(95)}; | ||
| const Sequence BrightCyan{WSL_WINDOWS_VT_TEXTFORMAT(96)}; | ||
| const Sequence BrightWhite{WSL_WINDOWS_VT_TEXTFORMAT(97)}; | ||
|
|
||
| ConstructedSequence Extended(const Color& color) | ||
| { | ||
| std::ostringstream result; | ||
| result << WSL_WINDOWS_VT_CSI "38;2;" << static_cast<uint32_t>(color.R) << ';' << static_cast<uint32_t>(color.G) << ';' | ||
| << static_cast<uint32_t>(color.B) << 'm'; | ||
| return ConstructedSequence{std::move(result).str()}; | ||
| } | ||
| } // namespace Fg | ||
|
|
||
| namespace Bg { | ||
| const Sequence Black{WSL_WINDOWS_VT_TEXTFORMAT(40)}; | ||
| const Sequence Red{WSL_WINDOWS_VT_TEXTFORMAT(41)}; | ||
| const Sequence Green{WSL_WINDOWS_VT_TEXTFORMAT(42)}; | ||
| const Sequence Yellow{WSL_WINDOWS_VT_TEXTFORMAT(43)}; | ||
| const Sequence Blue{WSL_WINDOWS_VT_TEXTFORMAT(44)}; | ||
| const Sequence Magenta{WSL_WINDOWS_VT_TEXTFORMAT(45)}; | ||
| const Sequence Cyan{WSL_WINDOWS_VT_TEXTFORMAT(46)}; | ||
| const Sequence White{WSL_WINDOWS_VT_TEXTFORMAT(47)}; | ||
|
|
||
| const Sequence BrightBlack{WSL_WINDOWS_VT_TEXTFORMAT(100)}; | ||
| const Sequence BrightRed{WSL_WINDOWS_VT_TEXTFORMAT(101)}; | ||
| const Sequence BrightGreen{WSL_WINDOWS_VT_TEXTFORMAT(102)}; | ||
| const Sequence BrightYellow{WSL_WINDOWS_VT_TEXTFORMAT(103)}; | ||
| const Sequence BrightBlue{WSL_WINDOWS_VT_TEXTFORMAT(104)}; | ||
| const Sequence BrightMagenta{WSL_WINDOWS_VT_TEXTFORMAT(105)}; | ||
| const Sequence BrightCyan{WSL_WINDOWS_VT_TEXTFORMAT(106)}; | ||
| const Sequence BrightWhite{WSL_WINDOWS_VT_TEXTFORMAT(107)}; | ||
|
|
||
| ConstructedSequence Extended(const Color& color) | ||
| { | ||
| std::ostringstream result; | ||
| result << WSL_WINDOWS_VT_CSI "48;2;" << static_cast<uint32_t>(color.R) << ';' << static_cast<uint32_t>(color.G) << ';' | ||
| << static_cast<uint32_t>(color.B) << 'm'; | ||
| return ConstructedSequence{std::move(result).str()}; | ||
| } | ||
| } // namespace Bg | ||
|
|
||
| ConstructedSequence Hyperlink(const std::string& text, const std::string& ref) | ||
| { | ||
| std::ostringstream result; | ||
| result << WSL_WINDOWS_VT_OSC "8;;" << ref << WSL_WINDOWS_VT_ESCAPE << "\\" << text << WSL_WINDOWS_VT_OSC << "8;;" | ||
| << WSL_WINDOWS_VT_ESCAPE << "\\"; | ||
| return ConstructedSequence{std::move(result).str()}; | ||
| } | ||
| } // namespace Format | ||
|
|
||
| namespace Erase { | ||
| const Sequence LineForward{WSL_WINDOWS_VT_CSI "K"}; // ESC[K (0 is the default parameter, omitted to match terminal output) | ||
| const Sequence LineBackward{WSL_WINDOWS_VT_CSI "1K"}; | ||
| const Sequence LineEntirely{WSL_WINDOWS_VT_CSI "2K"}; | ||
| const Sequence ScreenForward{WSL_WINDOWS_VT_CSI "J"}; | ||
| const Sequence ScreenBackward{WSL_WINDOWS_VT_CSI "1J"}; | ||
| const Sequence ScreenEntirely{WSL_WINDOWS_VT_CSI "2J"}; | ||
| } // namespace Erase | ||
|
|
||
| namespace Progress { | ||
| ConstructedSequence Construct(State state, std::optional<uint32_t> percentage) | ||
| { | ||
| // See https://conemu.github.io/en/AnsiEscapeCodes.html#ConEmu_specific_OSC | ||
|
|
||
| THROW_HR_IF(E_BOUNDS, percentage.has_value() && percentage > 100u); | ||
|
|
||
| // Workaround some quirks in the Windows Terminal implementation of the progress OSC sequence | ||
| switch (state) | ||
| { | ||
| case State::None: | ||
| case State::Indeterminate: | ||
| // Windows Terminal does not recognize the OSC sequence if the progress value is left out. | ||
| // As a workaround, we can specify an arbitrary value since it does not matter for None and Indeterminate states. | ||
| percentage = percentage.value_or(0); | ||
| break; | ||
| case State::Normal: | ||
| case State::Error: | ||
| case State::Paused: | ||
| // Windows Terminal does not support switching progress states without also setting a progress value at the same time, | ||
| // so we disallow this case for now. | ||
| THROW_HR_IF(E_INVALIDARG, !percentage.has_value()); | ||
| break; | ||
| } | ||
|
|
||
| int stateId; | ||
| switch (state) | ||
| { | ||
| case State::None: | ||
| stateId = 0; | ||
| break; | ||
| case State::Indeterminate: | ||
| stateId = 3; | ||
| break; | ||
| case State::Normal: | ||
| stateId = 1; | ||
| break; | ||
| case State::Error: | ||
| stateId = 2; | ||
| break; | ||
| case State::Paused: | ||
| stateId = 4; | ||
| break; | ||
| default: | ||
| THROW_HR(E_UNEXPECTED); | ||
| } | ||
|
|
||
| std::ostringstream result; | ||
| result << WSL_WINDOWS_VT_OSC "9;4;" << stateId << ";"; | ||
| if (percentage.has_value()) | ||
| { | ||
| result << percentage.value(); | ||
| } | ||
| result << WSL_WINDOWS_VT_ESCAPE << "\\"; | ||
| return ConstructedSequence{std::move(result).str()}; | ||
| } | ||
| } // namespace Progress | ||
| } // namespace wsl::windows::common::vt | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.