diff --git a/src/buffer/out/TextAttribute.cpp b/src/buffer/out/TextAttribute.cpp index da92768eef1..fe47a9108bd 100644 --- a/src/buffer/out/TextAttribute.cpp +++ b/src/buffer/out/TextAttribute.cpp @@ -20,6 +20,47 @@ void TextAttribute::SetLegacyDefaultAttributes(const WORD defaultAttributes) noe s_legacyDefaultBackground = (defaultAttributes & BG_ATTRS) >> 4; } +// Routine Description: +// Pursuant to GH#6807 +// This routine replaces VT colors from the 16-color set with the "default" +// flag. It is intended to be used as part of the "VT Quirk" in +// WriteConsole[AW]. +// +// There is going to be a very long tail of applications that will +// explicitly request VT SGR 40/37 when what they really want is to +// SetConsoleTextAttribute() with a black background/white foreground. +// Instead of making those applications look bad (and therefore making us +// look bad, because we're releasing this as an update to something that +// "looks good" already), we're introducing this compatibility hack. Before +// the color reckoning in GH#6698 + GH#6506, *every* color was subject to +// being spontaneously and erroneously turned into the default color. Now, +// only the 16-color palette value that matches the active console +// background color will be destroyed when the quirk is enabled. +// +// This is not intended to be a long-term solution. This comment will be +// discovered in forty years(*) time and people will laugh at our hubris. +// +// *it doesn't matter when you're reading this, it will always be 40 years +// from now. +TextAttribute TextAttribute::StripErroneousVT16VersionsOfLegacyDefaults(const TextAttribute& attribute) noexcept +{ + const auto fg{ attribute.GetForeground() }; + const auto bg{ attribute.GetBackground() }; + auto copy{ attribute }; + if (fg.IsIndex16() && + attribute.IsBold() == WI_IsFlagSet(s_legacyDefaultForeground, FOREGROUND_INTENSITY) && + fg.GetIndex() == (s_legacyDefaultForeground & ~FOREGROUND_INTENSITY)) + { + // We don't want to turn 1;37m into 39m (or even 1;39m), as this was meant to mimic a legacy color. + copy.SetDefaultForeground(); + } + if (bg.IsIndex16() && bg.GetIndex() == s_legacyDefaultBackground) + { + copy.SetDefaultBackground(); + } + return copy; +} + // Routine Description: // - Returns a WORD with legacy-style attributes for this textattribute. // Parameters: diff --git a/src/buffer/out/TextAttribute.hpp b/src/buffer/out/TextAttribute.hpp index 7f645942c0e..58db242ec33 100644 --- a/src/buffer/out/TextAttribute.hpp +++ b/src/buffer/out/TextAttribute.hpp @@ -60,6 +60,7 @@ class TextAttribute final } static void SetLegacyDefaultAttributes(const WORD defaultAttributes) noexcept; + static TextAttribute StripErroneousVT16VersionsOfLegacyDefaults(const TextAttribute& attribute) noexcept; WORD GetLegacyAttributes() const noexcept; COLORREF CalculateRgbForeground(std::basic_string_view colorTable, diff --git a/src/host/ApiRoutines.h b/src/host/ApiRoutines.h index 22ca2235abb..3bd93f76c66 100644 --- a/src/host/ApiRoutines.h +++ b/src/host/ApiRoutines.h @@ -98,11 +98,13 @@ class ApiRoutines : public IApiRoutines [[nodiscard]] HRESULT WriteConsoleAImpl(IConsoleOutputObject& context, const std::string_view buffer, size_t& read, + bool requiresVtQuirk, std::unique_ptr& waiter) noexcept override; [[nodiscard]] HRESULT WriteConsoleWImpl(IConsoleOutputObject& context, const std::wstring_view buffer, size_t& read, + bool requiresVtQuirk, std::unique_ptr& waiter) noexcept override; #pragma region ThreadCreationInfo diff --git a/src/host/_stream.cpp b/src/host/_stream.cpp index 6fe8be984c2..8c15892fcc9 100644 --- a/src/host/_stream.cpp +++ b/src/host/_stream.cpp @@ -971,6 +971,7 @@ constexpr unsigned int LOCAL_BUFFER_SIZE = 100; [[nodiscard]] NTSTATUS DoWriteConsole(_In_reads_bytes_(*pcbBuffer) PWCHAR pwchBuffer, _Inout_ size_t* const pcbBuffer, SCREEN_INFORMATION& screenInfo, + bool requiresVtQuirk, std::unique_ptr& waiter) { const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); @@ -981,7 +982,8 @@ constexpr unsigned int LOCAL_BUFFER_SIZE = 100; waiter = std::make_unique(screenInfo, pwchBuffer, *pcbBuffer, - gci.OutputCP); + gci.OutputCP, + requiresVtQuirk); } catch (...) { @@ -991,6 +993,19 @@ constexpr unsigned int LOCAL_BUFFER_SIZE = 100; return CONSOLE_STATUS_WAIT; } + auto restoreVtQuirk{ + wil::scope_exit([&]() { screenInfo.ResetIgnoreLegacyEquivalentVTAttributes(); }) + }; + + if (requiresVtQuirk) + { + screenInfo.SetIgnoreLegacyEquivalentVTAttributes(); + } + else + { + restoreVtQuirk.release(); + } + const auto& textBuffer = screenInfo.GetTextBuffer(); return WriteChars(screenInfo, pwchBuffer, @@ -1020,6 +1035,7 @@ constexpr unsigned int LOCAL_BUFFER_SIZE = 100; [[nodiscard]] HRESULT WriteConsoleWImplHelper(IConsoleOutputObject& context, const std::wstring_view buffer, size_t& read, + bool requiresVtQuirk, std::unique_ptr& waiter) noexcept { try @@ -1032,7 +1048,7 @@ constexpr unsigned int LOCAL_BUFFER_SIZE = 100; size_t cbTextBufferLength; RETURN_IF_FAILED(SizeTMult(buffer.size(), sizeof(wchar_t), &cbTextBufferLength)); - NTSTATUS Status = DoWriteConsole(const_cast(buffer.data()), &cbTextBufferLength, context, waiter); + NTSTATUS Status = DoWriteConsole(const_cast(buffer.data()), &cbTextBufferLength, context, requiresVtQuirk, waiter); // Convert back from bytes to characters for the resulting string length written. read = cbTextBufferLength / sizeof(wchar_t); @@ -1065,6 +1081,7 @@ constexpr unsigned int LOCAL_BUFFER_SIZE = 100; [[nodiscard]] HRESULT ApiRoutines::WriteConsoleAImpl(IConsoleOutputObject& context, const std::string_view buffer, size_t& read, + bool requiresVtQuirk, std::unique_ptr& waiter) noexcept { try @@ -1176,7 +1193,7 @@ constexpr unsigned int LOCAL_BUFFER_SIZE = 100; // Make the W version of the call size_t wcBufferWritten{}; - const auto hr{ WriteConsoleWImplHelper(screenInfo, wstr, wcBufferWritten, writeDataWaiter) }; + const auto hr{ WriteConsoleWImplHelper(screenInfo, wstr, wcBufferWritten, requiresVtQuirk, writeDataWaiter) }; // If there is no waiter, process the byte count now. if (nullptr == writeDataWaiter.get()) @@ -1254,6 +1271,7 @@ constexpr unsigned int LOCAL_BUFFER_SIZE = 100; [[nodiscard]] HRESULT ApiRoutines::WriteConsoleWImpl(IConsoleOutputObject& context, const std::wstring_view buffer, size_t& read, + bool requiresVtQuirk, std::unique_ptr& waiter) noexcept { try @@ -1262,7 +1280,7 @@ constexpr unsigned int LOCAL_BUFFER_SIZE = 100; auto unlock = wil::scope_exit([&] { UnlockConsole(); }); std::unique_ptr writeDataWaiter; - RETURN_IF_FAILED(WriteConsoleWImplHelper(context.GetActiveBuffer(), buffer, read, writeDataWaiter)); + RETURN_IF_FAILED(WriteConsoleWImplHelper(context.GetActiveBuffer(), buffer, read, requiresVtQuirk, writeDataWaiter)); // Transfer specific waiter pointer into the generic interface wrapper. waiter.reset(writeDataWaiter.release()); diff --git a/src/host/_stream.h b/src/host/_stream.h index 1a88e390384..0352b959ca2 100644 --- a/src/host/_stream.h +++ b/src/host/_stream.h @@ -93,4 +93,5 @@ Return Value: [[nodiscard]] NTSTATUS DoWriteConsole(_In_reads_bytes_(*pcbBuffer) PWCHAR pwchBuffer, _Inout_ size_t* const pcbBuffer, SCREEN_INFORMATION& screenInfo, + bool requiresVtQuirk, std::unique_ptr& waiter); diff --git a/src/host/screenInfo.cpp b/src/host/screenInfo.cpp index b7401552942..d950517cdd4 100644 --- a/src/host/screenInfo.cpp +++ b/src/host/screenInfo.cpp @@ -58,7 +58,8 @@ SCREEN_INFORMATION::SCREEN_INFORMATION( _virtualBottom{ 0 }, _renderTarget{ *this }, _currentFont{ fontInfo }, - _desiredFont{ fontInfo } + _desiredFont{ fontInfo }, + _ignoreLegacyEquivalentVTAttributes{ false } { // Check if VT mode is enabled. Note that this can be true w/o calling // SetConsoleMode, if VirtualTerminalLevel is set to !=0 in the registry. @@ -2029,6 +2030,13 @@ TextAttribute SCREEN_INFORMATION::GetPopupAttributes() const // void SCREEN_INFORMATION::SetAttributes(const TextAttribute& attributes) { + if (_ignoreLegacyEquivalentVTAttributes) + { + // See the comment on StripErroneousVT16VersionsOfLegacyDefaults for more info. + _textBuffer->SetCurrentAttributes(TextAttribute::StripErroneousVT16VersionsOfLegacyDefaults(attributes)); + return; + } + _textBuffer->SetCurrentAttributes(attributes); // If we're an alt buffer, DON'T propagate this setting up to the main buffer. @@ -2675,3 +2683,17 @@ Viewport SCREEN_INFORMATION::GetScrollingRegion() const noexcept marginsSet ? marginRect.Bottom : buffer.BottomInclusive() }); return margin; } + +// Routine Description: +// - Engages the legacy VT handling quirk; see TextAttribute::StripErroneousVT16VersionsOfLegacyDefaults +void SCREEN_INFORMATION::SetIgnoreLegacyEquivalentVTAttributes() noexcept +{ + _ignoreLegacyEquivalentVTAttributes = true; +} + +// Routine Description: +// - Disengages the legacy VT handling quirk; see TextAttribute::StripErroneousVT16VersionsOfLegacyDefaults +void SCREEN_INFORMATION::ResetIgnoreLegacyEquivalentVTAttributes() noexcept +{ + _ignoreLegacyEquivalentVTAttributes = false; +} diff --git a/src/host/screenInfo.hpp b/src/host/screenInfo.hpp index f6c86b7cf36..f1afd46f5fd 100644 --- a/src/host/screenInfo.hpp +++ b/src/host/screenInfo.hpp @@ -238,6 +238,9 @@ class SCREEN_INFORMATION : public ConsoleObjectHeader, public Microsoft::Console void InitializeCursorRowAttributes(); + void SetIgnoreLegacyEquivalentVTAttributes() noexcept; + void ResetIgnoreLegacyEquivalentVTAttributes() noexcept; + private: SCREEN_INFORMATION(_In_ Microsoft::Console::Interactivity::IWindowMetrics* pMetrics, _In_ Microsoft::Console::Interactivity::IAccessibilityNotifier* pNotifier, @@ -300,6 +303,8 @@ class SCREEN_INFORMATION : public ConsoleObjectHeader, public Microsoft::Console ScreenBufferRenderTarget _renderTarget; + bool _ignoreLegacyEquivalentVTAttributes; + #ifdef UNIT_TESTING friend class TextBufferIteratorTests; friend class ScreenBufferTests; diff --git a/src/host/ut_host/ApiRoutinesTests.cpp b/src/host/ut_host/ApiRoutinesTests.cpp index eb5d6c0a29e..437368d0bfd 100644 --- a/src/host/ut_host/ApiRoutinesTests.cpp +++ b/src/host/ut_host/ApiRoutinesTests.cpp @@ -386,7 +386,7 @@ class ApiRoutinesTests const size_t cchWriteLength = std::min(cchIncrement, cchTestText - i); // Run the test method - const HRESULT hr = _pApiRoutines->WriteConsoleAImpl(si, { pszTestText + i, cchWriteLength }, cchRead, waiter); + const HRESULT hr = _pApiRoutines->WriteConsoleAImpl(si, { pszTestText + i, cchWriteLength }, cchRead, false, waiter); VERIFY_ARE_EQUAL(S_OK, hr, L"Successful result code from writing."); if (!fInduceWait) @@ -442,7 +442,7 @@ class ApiRoutinesTests size_t cchRead = 0; std::unique_ptr waiter; - const HRESULT hr = _pApiRoutines->WriteConsoleWImpl(si, testText, cchRead, waiter); + const HRESULT hr = _pApiRoutines->WriteConsoleWImpl(si, testText, cchRead, false, waiter); VERIFY_ARE_EQUAL(S_OK, hr, L"Successful result code from writing."); if (!fInduceWait) diff --git a/src/host/ut_host/ScreenBufferTests.cpp b/src/host/ut_host/ScreenBufferTests.cpp index e948d7d3e72..22fc29c820a 100644 --- a/src/host/ut_host/ScreenBufferTests.cpp +++ b/src/host/ut_host/ScreenBufferTests.cpp @@ -212,6 +212,8 @@ class ScreenBufferTests TEST_METHOD(TestCursorIsOn); TEST_METHOD(UpdateVirtualBottomWhenCursorMovesBelowIt); + + TEST_METHOD(TestWriteConsoleVTQuirkMode); }; void ScreenBufferTests::SingleAlternateBufferCreationTest() @@ -2115,7 +2117,7 @@ void ScreenBufferTests::TestAltBufferVtDispatching() std::unique_ptr waiter; std::wstring seq = L"\x1b[5;6H"; size_t seqCb = 2 * seq.size(); - VERIFY_SUCCEEDED(DoWriteConsole(&seq[0], &seqCb, mainBuffer, waiter)); + VERIFY_SUCCEEDED(DoWriteConsole(&seq[0], &seqCb, mainBuffer, false, waiter)); VERIFY_ARE_EQUAL(COORD({ 0, 0 }), mainCursor.GetPosition()); // recall: vt coordinates are (row, column), 1-indexed @@ -2130,14 +2132,14 @@ void ScreenBufferTests::TestAltBufferVtDispatching() seq = L"\x1b[48;2;255;0;255m"; seqCb = 2 * seq.size(); - VERIFY_SUCCEEDED(DoWriteConsole(&seq[0], &seqCb, mainBuffer, waiter)); + VERIFY_SUCCEEDED(DoWriteConsole(&seq[0], &seqCb, mainBuffer, false, waiter)); VERIFY_ARE_EQUAL(expectedDefaults, mainBuffer.GetAttributes()); VERIFY_ARE_EQUAL(expectedRgb, alternate.GetAttributes()); seq = L"X"; seqCb = 2 * seq.size(); - VERIFY_SUCCEEDED(DoWriteConsole(&seq[0], &seqCb, mainBuffer, waiter)); + VERIFY_SUCCEEDED(DoWriteConsole(&seq[0], &seqCb, mainBuffer, false, waiter)); VERIFY_ARE_EQUAL(COORD({ 0, 0 }), mainCursor.GetPosition()); VERIFY_ARE_EQUAL(COORD({ 6, 4 }), altCursor.GetPosition()); @@ -5922,3 +5924,125 @@ void ScreenBufferTests::UpdateVirtualBottomWhenCursorMovesBelowIt() si.MoveToBottom(); VERIFY_ARE_EQUAL(newVirtualBottom, si.GetViewport().BottomInclusive()); } + +void ScreenBufferTests::TestWriteConsoleVTQuirkMode() +{ + BEGIN_TEST_METHOD_PROPERTIES() + TEST_METHOD_PROPERTY(L"Data:useQuirk", L"{false, true}") + END_TEST_METHOD_PROPERTIES() + + bool useQuirk; + VERIFY_SUCCEEDED(TestData::TryGetValue(L"useQuirk", useQuirk), L"whether to enable the quirk"); + + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + gci.LockConsole(); // Lock must be taken to manipulate buffer. + auto unlock = wil::scope_exit([&] { gci.UnlockConsole(); }); + + auto& mainBuffer = gci.GetActiveOutputBuffer(); + auto& cursor = mainBuffer.GetTextBuffer().GetCursor(); + // Make sure we're in VT mode + WI_SetFlag(mainBuffer.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING); + + const TextAttribute defaultAttribute{}; + // Make sure we're using the default attributes at the start of the test, + // Otherwise they could be polluted from a previous test. + mainBuffer.SetAttributes(defaultAttribute); + + const auto verifyLastAttribute = [&](const TextAttribute& expected) { + const ROW& row = mainBuffer.GetTextBuffer().GetRowByOffset(cursor.GetPosition().Y); + const auto attrRow = &row.GetAttrRow(); + auto iter{ attrRow->begin() }; + iter += cursor.GetPosition().X - 1; + VERIFY_ARE_EQUAL(expected, *iter); + }; + + std::unique_ptr waiter; + + std::wstring seq{}; + size_t seqCb{ 0 }; + + /* Write red on blue, verify that it comes through */ + { + TextAttribute vtRedOnBlueAttribute{}; + vtRedOnBlueAttribute.SetForeground(TextColor{ gsl::narrow_cast(XtermToWindowsIndex(1)), false }); + vtRedOnBlueAttribute.SetBackground(TextColor{ gsl::narrow_cast(XtermToWindowsIndex(4)), false }); + + seq = L"\x1b[31;44m"; + seqCb = 2 * seq.size(); + VERIFY_SUCCEEDED(DoWriteConsole(&seq[0], &seqCb, mainBuffer, useQuirk, waiter)); + + VERIFY_ARE_EQUAL(vtRedOnBlueAttribute, mainBuffer.GetAttributes()); + + seq = L"X"; + seqCb = 2 * seq.size(); + VERIFY_SUCCEEDED(DoWriteConsole(&seq[0], &seqCb, mainBuffer, useQuirk, waiter)); + + verifyLastAttribute(vtRedOnBlueAttribute); + } + + /* Write white on black, verify that it acts as expected for the quirk mode */ + { + TextAttribute vtWhiteOnBlackAttribute{}; + vtWhiteOnBlackAttribute.SetForeground(TextColor{ gsl::narrow_cast(XtermToWindowsIndex(7)), false }); + vtWhiteOnBlackAttribute.SetBackground(TextColor{ gsl::narrow_cast(XtermToWindowsIndex(0)), false }); + + const TextAttribute quirkExpectedAttribute{ useQuirk ? defaultAttribute : vtWhiteOnBlackAttribute }; + + seq = L"\x1b[37;40m"; // the quirk should suppress this, turning it into "defaults" + seqCb = 2 * seq.size(); + VERIFY_SUCCEEDED(DoWriteConsole(&seq[0], &seqCb, mainBuffer, useQuirk, waiter)); + + VERIFY_ARE_EQUAL(quirkExpectedAttribute, mainBuffer.GetAttributes()); + + seq = L"X"; + seqCb = 2 * seq.size(); + VERIFY_SUCCEEDED(DoWriteConsole(&seq[0], &seqCb, mainBuffer, useQuirk, waiter)); + + verifyLastAttribute(quirkExpectedAttribute); + } + + /* Write bright white on black, verify that it acts as expected for the quirk mode */ + { + TextAttribute vtBrightWhiteOnBlackAttribute{}; + vtBrightWhiteOnBlackAttribute.SetForeground(TextColor{ gsl::narrow_cast(XtermToWindowsIndex(7)), false }); + vtBrightWhiteOnBlackAttribute.SetBackground(TextColor{ gsl::narrow_cast(XtermToWindowsIndex(0)), false }); + vtBrightWhiteOnBlackAttribute.SetBold(true); + + TextAttribute vtBrightWhiteOnDefaultAttribute{ vtBrightWhiteOnBlackAttribute }; // copy the above attribute + vtBrightWhiteOnDefaultAttribute.SetDefaultBackground(); + + const TextAttribute quirkExpectedAttribute{ useQuirk ? vtBrightWhiteOnDefaultAttribute : vtBrightWhiteOnBlackAttribute }; + + seq = L"\x1b[1;37;40m"; // the quirk should suppress black only, turning it into "default background" + seqCb = 2 * seq.size(); + VERIFY_SUCCEEDED(DoWriteConsole(&seq[0], &seqCb, mainBuffer, useQuirk, waiter)); + + VERIFY_ARE_EQUAL(quirkExpectedAttribute, mainBuffer.GetAttributes()); + + seq = L"X"; + seqCb = 2 * seq.size(); + VERIFY_SUCCEEDED(DoWriteConsole(&seq[0], &seqCb, mainBuffer, useQuirk, waiter)); + + verifyLastAttribute(quirkExpectedAttribute); + } + + /* Write a 256-color white on a 256-color black, make sure the quirk does not suppress it */ + { + TextAttribute vtWhiteOnBlack256Attribute{}; + vtWhiteOnBlack256Attribute.SetForeground(TextColor{ gsl::narrow_cast(XtermToWindowsIndex(7)), true }); + vtWhiteOnBlack256Attribute.SetBackground(TextColor{ gsl::narrow_cast(XtermToWindowsIndex(0)), true }); + + // reset (disable bold from the last test) before setting both colors + seq = L"\x1b[m\x1b[38;5;7;48;5;0m"; // the quirk should *not* suppress this (!) + seqCb = 2 * seq.size(); + VERIFY_SUCCEEDED(DoWriteConsole(&seq[0], &seqCb, mainBuffer, useQuirk, waiter)); + + VERIFY_ARE_EQUAL(vtWhiteOnBlack256Attribute, mainBuffer.GetAttributes()); + + seq = L"X"; + seqCb = 2 * seq.size(); + VERIFY_SUCCEEDED(DoWriteConsole(&seq[0], &seqCb, mainBuffer, useQuirk, waiter)); + + verifyLastAttribute(vtWhiteOnBlack256Attribute); + } +} diff --git a/src/host/ut_host/TextBufferTests.cpp b/src/host/ut_host/TextBufferTests.cpp index ac4cbfab1c7..8da74705661 100644 --- a/src/host/ut_host/TextBufferTests.cpp +++ b/src/host/ut_host/TextBufferTests.cpp @@ -1573,7 +1573,7 @@ void TextBufferTests::TestBackspaceStringsAPI() std::unique_ptr waiter; size_t aCb = 2; - VERIFY_SUCCEEDED(DoWriteConsole(L"a", &aCb, si, waiter)); + VERIFY_SUCCEEDED(DoWriteConsole(L"a", &aCb, si, false, waiter)); size_t seqCb = 6; Log::Comment(NoThrowString().Format( @@ -1587,9 +1587,9 @@ void TextBufferTests::TestBackspaceStringsAPI() Log::Comment(NoThrowString().Format( L"Using DoWriteConsole, write \\b \\b as a single string.")); - VERIFY_SUCCEEDED(DoWriteConsole(L"a", &aCb, si, waiter)); + VERIFY_SUCCEEDED(DoWriteConsole(L"a", &aCb, si, false, waiter)); - VERIFY_SUCCEEDED(DoWriteConsole(str, &seqCb, si, waiter)); + VERIFY_SUCCEEDED(DoWriteConsole(str, &seqCb, si, false, waiter)); VERIFY_ARE_EQUAL(cursor.GetPosition().X, x0); VERIFY_ARE_EQUAL(cursor.GetPosition().Y, y0); } @@ -1599,10 +1599,10 @@ void TextBufferTests::TestBackspaceStringsAPI() Log::Comment(NoThrowString().Format( L"Using DoWriteConsole, write \\b \\b as separate strings.")); - VERIFY_SUCCEEDED(DoWriteConsole(L"a", &seqCb, si, waiter)); - VERIFY_SUCCEEDED(DoWriteConsole(L"\b", &seqCb, si, waiter)); - VERIFY_SUCCEEDED(DoWriteConsole(L" ", &seqCb, si, waiter)); - VERIFY_SUCCEEDED(DoWriteConsole(L"\b", &seqCb, si, waiter)); + VERIFY_SUCCEEDED(DoWriteConsole(L"a", &seqCb, si, false, waiter)); + VERIFY_SUCCEEDED(DoWriteConsole(L"\b", &seqCb, si, false, waiter)); + VERIFY_SUCCEEDED(DoWriteConsole(L" ", &seqCb, si, false, waiter)); + VERIFY_SUCCEEDED(DoWriteConsole(L"\b", &seqCb, si, false, waiter)); VERIFY_ARE_EQUAL(cursor.GetPosition().X, x0); VERIFY_ARE_EQUAL(cursor.GetPosition().Y, y0); diff --git a/src/host/writeData.cpp b/src/host/writeData.cpp index cf8bae97d7d..1385caec087 100644 --- a/src/host/writeData.cpp +++ b/src/host/writeData.cpp @@ -24,12 +24,14 @@ WriteData::WriteData(SCREEN_INFORMATION& siContext, _In_reads_bytes_(cbContext) wchar_t* const pwchContext, const size_t cbContext, - const UINT uiOutputCodepage) : + const UINT uiOutputCodepage, + const bool requiresVtQuirk) : IWaitRoutine(ReplyDataType::Write), _siContext(siContext), _pwchContext(THROW_IF_NULL_ALLOC(reinterpret_cast(new byte[cbContext]))), _cbContext(cbContext), _uiOutputCodepage(uiOutputCodepage), + _requiresVtQuirk(requiresVtQuirk), _fLeadByteCaptured(false), _fLeadByteConsumed(false), _cchUtf8Consumed(0) @@ -124,6 +126,7 @@ bool WriteData::Notify(const WaitTerminationReason TerminationReason, NTSTATUS Status = DoWriteConsole(_pwchContext, &cbContext, _siContext, + _requiresVtQuirk, waiter); if (Status == CONSOLE_STATUS_WAIT) diff --git a/src/host/writeData.hpp b/src/host/writeData.hpp index 0d6871efc28..b61cd5690f5 100644 --- a/src/host/writeData.hpp +++ b/src/host/writeData.hpp @@ -27,7 +27,8 @@ class WriteData : public IWaitRoutine WriteData(SCREEN_INFORMATION& siContext, _In_reads_bytes_(cbContext) wchar_t* const pwchContext, const size_t cbContext, - const UINT uiOutputCodepage); + const UINT uiOutputCodepage, + const bool requiresVtQuirk); ~WriteData(); void SetLeadByteAdjustmentStatus(const bool fLeadByteCaptured, @@ -47,6 +48,7 @@ class WriteData : public IWaitRoutine wchar_t* const _pwchContext; size_t const _cbContext; UINT const _uiOutputCodepage; + bool _requiresVtQuirk; bool _fLeadByteCaptured; bool _fLeadByteConsumed; size_t _cchUtf8Consumed; diff --git a/src/server/ApiDispatchers.cpp b/src/server/ApiDispatchers.cpp index 72384a58450..5ba2d547014 100644 --- a/src/server/ApiDispatchers.cpp +++ b/src/server/ApiDispatchers.cpp @@ -408,6 +408,8 @@ std::unique_ptr waiter; size_t cbRead; + const auto requiresVtQuirk{ m->GetProcessHandle()->GetShimPolicy().IsVtColorQuirkRequired() }; + // We have to hold onto the HR from the call and return it. // We can't return some other error after the actual API call. // This is because the write console function is allowed to write part of the string and then return an error. @@ -418,7 +420,7 @@ const std::wstring_view buffer(reinterpret_cast(pvBuffer), cbBufferSize / sizeof(wchar_t)); size_t cchInputRead; - hr = m->_pApiRoutines->WriteConsoleWImpl(*pScreenInfo, buffer, cchInputRead, waiter); + hr = m->_pApiRoutines->WriteConsoleWImpl(*pScreenInfo, buffer, cchInputRead, requiresVtQuirk, waiter); // We must set the reply length in bytes. Convert back from characters. LOG_IF_FAILED(SizeTMult(cchInputRead, sizeof(wchar_t), &cbRead)); @@ -428,7 +430,7 @@ const std::string_view buffer(reinterpret_cast(pvBuffer), cbBufferSize); size_t cchInputRead; - hr = m->_pApiRoutines->WriteConsoleAImpl(*pScreenInfo, buffer, cchInputRead, waiter); + hr = m->_pApiRoutines->WriteConsoleAImpl(*pScreenInfo, buffer, cchInputRead, requiresVtQuirk, waiter); // Reply length is already in bytes (chars), don't need to convert. cbRead = cchInputRead; diff --git a/src/server/ConsoleShimPolicy.cpp b/src/server/ConsoleShimPolicy.cpp index 23556ed0f3a..32243fbcf4b 100644 --- a/src/server/ConsoleShimPolicy.cpp +++ b/src/server/ConsoleShimPolicy.cpp @@ -80,3 +80,16 @@ bool ConsoleShimPolicy::IsPowershellExe() const noexcept { return _isPowershell; } + +// Method Description: +// - Returns true if the connected client application is known to +// attempt VT color promotion of legacy colors. See GH#6807 for more. +// Arguments: +// - +// Return Value: +// - True as laid out above. +bool ConsoleShimPolicy::IsVtColorQuirkRequired() const noexcept +{ + // Right now, the only client we're shimming is powershell. + return IsPowershellExe(); +} diff --git a/src/server/ConsoleShimPolicy.h b/src/server/ConsoleShimPolicy.h index 89006d28366..e3bd57ad1b8 100644 --- a/src/server/ConsoleShimPolicy.h +++ b/src/server/ConsoleShimPolicy.h @@ -25,6 +25,7 @@ class ConsoleShimPolicy bool IsCmdExe() const noexcept; bool IsPowershellExe() const noexcept; + bool IsVtColorQuirkRequired() const noexcept; private: ConsoleShimPolicy(const bool isCmd, diff --git a/src/server/IApiRoutines.h b/src/server/IApiRoutines.h index 33b7eab32f6..7b32a727223 100644 --- a/src/server/IApiRoutines.h +++ b/src/server/IApiRoutines.h @@ -113,11 +113,13 @@ class IApiRoutines [[nodiscard]] virtual HRESULT WriteConsoleAImpl(IConsoleOutputObject& context, const std::string_view buffer, size_t& read, + bool requiresVtQuirk, std::unique_ptr& waiter) noexcept = 0; [[nodiscard]] virtual HRESULT WriteConsoleWImpl(IConsoleOutputObject& context, const std::wstring_view buffer, size_t& read, + bool requiresVtQuirk, std::unique_ptr& waiter) noexcept = 0; #pragma region Thread Creation Info