Skip to content

Commit 1bf4c08

Browse files
authored
Reintroduce a color compatibility hack, but only for PowerShells (#6810)
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 quirk. Before the color reckoning in #6698 + #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/foreground color will be destroyed, and only when received from specific applications. Removal will be tracked by #6807. Michael and I discussed what layer this quirk really belonged in. I originally believed it would be sufficient to detect a background color that matched the legacy default background, but @j4james provided an example of where that wouldn't work out (powershell setting the foreground color to white/gray). In addition, it was too heavyhanded: it re-broke black backgrounds for every application. Michael thought that it should live in the server, as a small VT parser that righted the wrongs coming directly out of the application. On further investigation, however, I realized that we'd need to push more information up into the server (so that it could make the decision about which VT was wrong and which was right) than should be strictly necessary. The host knows which colors are right and wrong, and it gets final say in what ends up in the buffer. Because of that, I chose to push the quirk state down through WriteConsole to DoWriteConsole and toggle state on the SCREEN_INFORMATION that indicates whether the colors coming out of the application are to be distrusted. This quirk _only applies to pwsh.exe and powershell.exe._ NOTE: This doesn't work for PowerShell the .NET Global tool, because it is run as an assembly through dotnet.exe. I have no opinion on how to fix this, or whether it is worth fixing. VALIDATION ---------- I configured my terminals to have an incredibly garish color scheme to show exactly what's going to happen as a result of this. The _default terminal background_ is purple or red, and the foreground green. I've printed out a heap of test colors to see how black interacts with them. Pull request #6810 contains the images generated from this test. The only color lines that change are the ones where black as a background or white as a foreground is selected out of the 16-color palette explicitly. Reverse video still works fine (because black is in the foreground!), and it's even possible to represent "black on default" and reverse it into "default on black", despite the black in question having been `40`. Fixes #6767.
1 parent fc08329 commit 1bf4c08

16 files changed

+258
-21
lines changed

src/buffer/out/TextAttribute.cpp

+41
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,47 @@ void TextAttribute::SetLegacyDefaultAttributes(const WORD defaultAttributes) noe
2020
s_legacyDefaultBackground = (defaultAttributes & BG_ATTRS) >> 4;
2121
}
2222

23+
// Routine Description:
24+
// Pursuant to GH#6807
25+
// This routine replaces VT colors from the 16-color set with the "default"
26+
// flag. It is intended to be used as part of the "VT Quirk" in
27+
// WriteConsole[AW].
28+
//
29+
// There is going to be a very long tail of applications that will
30+
// explicitly request VT SGR 40/37 when what they really want is to
31+
// SetConsoleTextAttribute() with a black background/white foreground.
32+
// Instead of making those applications look bad (and therefore making us
33+
// look bad, because we're releasing this as an update to something that
34+
// "looks good" already), we're introducing this compatibility hack. Before
35+
// the color reckoning in GH#6698 + GH#6506, *every* color was subject to
36+
// being spontaneously and erroneously turned into the default color. Now,
37+
// only the 16-color palette value that matches the active console
38+
// background color will be destroyed when the quirk is enabled.
39+
//
40+
// This is not intended to be a long-term solution. This comment will be
41+
// discovered in forty years(*) time and people will laugh at our hubris.
42+
//
43+
// *it doesn't matter when you're reading this, it will always be 40 years
44+
// from now.
45+
TextAttribute TextAttribute::StripErroneousVT16VersionsOfLegacyDefaults(const TextAttribute& attribute) noexcept
46+
{
47+
const auto fg{ attribute.GetForeground() };
48+
const auto bg{ attribute.GetBackground() };
49+
auto copy{ attribute };
50+
if (fg.IsIndex16() &&
51+
attribute.IsBold() == WI_IsFlagSet(s_legacyDefaultForeground, FOREGROUND_INTENSITY) &&
52+
fg.GetIndex() == (s_legacyDefaultForeground & ~FOREGROUND_INTENSITY))
53+
{
54+
// We don't want to turn 1;37m into 39m (or even 1;39m), as this was meant to mimic a legacy color.
55+
copy.SetDefaultForeground();
56+
}
57+
if (bg.IsIndex16() && bg.GetIndex() == s_legacyDefaultBackground)
58+
{
59+
copy.SetDefaultBackground();
60+
}
61+
return copy;
62+
}
63+
2364
// Routine Description:
2465
// - Returns a WORD with legacy-style attributes for this textattribute.
2566
// Parameters:

src/buffer/out/TextAttribute.hpp

+1
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ class TextAttribute final
6060
}
6161

6262
static void SetLegacyDefaultAttributes(const WORD defaultAttributes) noexcept;
63+
static TextAttribute StripErroneousVT16VersionsOfLegacyDefaults(const TextAttribute& attribute) noexcept;
6364
WORD GetLegacyAttributes() const noexcept;
6465

6566
COLORREF CalculateRgbForeground(std::basic_string_view<COLORREF> colorTable,

src/host/ApiRoutines.h

+2
Original file line numberDiff line numberDiff line change
@@ -98,11 +98,13 @@ class ApiRoutines : public IApiRoutines
9898
[[nodiscard]] HRESULT WriteConsoleAImpl(IConsoleOutputObject& context,
9999
const std::string_view buffer,
100100
size_t& read,
101+
bool requiresVtQuirk,
101102
std::unique_ptr<IWaitRoutine>& waiter) noexcept override;
102103

103104
[[nodiscard]] HRESULT WriteConsoleWImpl(IConsoleOutputObject& context,
104105
const std::wstring_view buffer,
105106
size_t& read,
107+
bool requiresVtQuirk,
106108
std::unique_ptr<IWaitRoutine>& waiter) noexcept override;
107109

108110
#pragma region ThreadCreationInfo

src/host/_stream.cpp

+22-4
Original file line numberDiff line numberDiff line change
@@ -971,6 +971,7 @@ constexpr unsigned int LOCAL_BUFFER_SIZE = 100;
971971
[[nodiscard]] NTSTATUS DoWriteConsole(_In_reads_bytes_(*pcbBuffer) PWCHAR pwchBuffer,
972972
_Inout_ size_t* const pcbBuffer,
973973
SCREEN_INFORMATION& screenInfo,
974+
bool requiresVtQuirk,
974975
std::unique_ptr<WriteData>& waiter)
975976
{
976977
const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
@@ -981,7 +982,8 @@ constexpr unsigned int LOCAL_BUFFER_SIZE = 100;
981982
waiter = std::make_unique<WriteData>(screenInfo,
982983
pwchBuffer,
983984
*pcbBuffer,
984-
gci.OutputCP);
985+
gci.OutputCP,
986+
requiresVtQuirk);
985987
}
986988
catch (...)
987989
{
@@ -991,6 +993,19 @@ constexpr unsigned int LOCAL_BUFFER_SIZE = 100;
991993
return CONSOLE_STATUS_WAIT;
992994
}
993995

996+
auto restoreVtQuirk{
997+
wil::scope_exit([&]() { screenInfo.ResetIgnoreLegacyEquivalentVTAttributes(); })
998+
};
999+
1000+
if (requiresVtQuirk)
1001+
{
1002+
screenInfo.SetIgnoreLegacyEquivalentVTAttributes();
1003+
}
1004+
else
1005+
{
1006+
restoreVtQuirk.release();
1007+
}
1008+
9941009
const auto& textBuffer = screenInfo.GetTextBuffer();
9951010
return WriteChars(screenInfo,
9961011
pwchBuffer,
@@ -1020,6 +1035,7 @@ constexpr unsigned int LOCAL_BUFFER_SIZE = 100;
10201035
[[nodiscard]] HRESULT WriteConsoleWImplHelper(IConsoleOutputObject& context,
10211036
const std::wstring_view buffer,
10221037
size_t& read,
1038+
bool requiresVtQuirk,
10231039
std::unique_ptr<WriteData>& waiter) noexcept
10241040
{
10251041
try
@@ -1032,7 +1048,7 @@ constexpr unsigned int LOCAL_BUFFER_SIZE = 100;
10321048
size_t cbTextBufferLength;
10331049
RETURN_IF_FAILED(SizeTMult(buffer.size(), sizeof(wchar_t), &cbTextBufferLength));
10341050

1035-
NTSTATUS Status = DoWriteConsole(const_cast<wchar_t*>(buffer.data()), &cbTextBufferLength, context, waiter);
1051+
NTSTATUS Status = DoWriteConsole(const_cast<wchar_t*>(buffer.data()), &cbTextBufferLength, context, requiresVtQuirk, waiter);
10361052

10371053
// Convert back from bytes to characters for the resulting string length written.
10381054
read = cbTextBufferLength / sizeof(wchar_t);
@@ -1065,6 +1081,7 @@ constexpr unsigned int LOCAL_BUFFER_SIZE = 100;
10651081
[[nodiscard]] HRESULT ApiRoutines::WriteConsoleAImpl(IConsoleOutputObject& context,
10661082
const std::string_view buffer,
10671083
size_t& read,
1084+
bool requiresVtQuirk,
10681085
std::unique_ptr<IWaitRoutine>& waiter) noexcept
10691086
{
10701087
try
@@ -1176,7 +1193,7 @@ constexpr unsigned int LOCAL_BUFFER_SIZE = 100;
11761193

11771194
// Make the W version of the call
11781195
size_t wcBufferWritten{};
1179-
const auto hr{ WriteConsoleWImplHelper(screenInfo, wstr, wcBufferWritten, writeDataWaiter) };
1196+
const auto hr{ WriteConsoleWImplHelper(screenInfo, wstr, wcBufferWritten, requiresVtQuirk, writeDataWaiter) };
11801197

11811198
// If there is no waiter, process the byte count now.
11821199
if (nullptr == writeDataWaiter.get())
@@ -1254,6 +1271,7 @@ constexpr unsigned int LOCAL_BUFFER_SIZE = 100;
12541271
[[nodiscard]] HRESULT ApiRoutines::WriteConsoleWImpl(IConsoleOutputObject& context,
12551272
const std::wstring_view buffer,
12561273
size_t& read,
1274+
bool requiresVtQuirk,
12571275
std::unique_ptr<IWaitRoutine>& waiter) noexcept
12581276
{
12591277
try
@@ -1262,7 +1280,7 @@ constexpr unsigned int LOCAL_BUFFER_SIZE = 100;
12621280
auto unlock = wil::scope_exit([&] { UnlockConsole(); });
12631281

12641282
std::unique_ptr<WriteData> writeDataWaiter;
1265-
RETURN_IF_FAILED(WriteConsoleWImplHelper(context.GetActiveBuffer(), buffer, read, writeDataWaiter));
1283+
RETURN_IF_FAILED(WriteConsoleWImplHelper(context.GetActiveBuffer(), buffer, read, requiresVtQuirk, writeDataWaiter));
12661284

12671285
// Transfer specific waiter pointer into the generic interface wrapper.
12681286
waiter.reset(writeDataWaiter.release());

src/host/_stream.h

+1
Original file line numberDiff line numberDiff line change
@@ -93,4 +93,5 @@ Return Value:
9393
[[nodiscard]] NTSTATUS DoWriteConsole(_In_reads_bytes_(*pcbBuffer) PWCHAR pwchBuffer,
9494
_Inout_ size_t* const pcbBuffer,
9595
SCREEN_INFORMATION& screenInfo,
96+
bool requiresVtQuirk,
9697
std::unique_ptr<WriteData>& waiter);

src/host/screenInfo.cpp

+23-1
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,8 @@ SCREEN_INFORMATION::SCREEN_INFORMATION(
5858
_virtualBottom{ 0 },
5959
_renderTarget{ *this },
6060
_currentFont{ fontInfo },
61-
_desiredFont{ fontInfo }
61+
_desiredFont{ fontInfo },
62+
_ignoreLegacyEquivalentVTAttributes{ false }
6263
{
6364
// Check if VT mode is enabled. Note that this can be true w/o calling
6465
// SetConsoleMode, if VirtualTerminalLevel is set to !=0 in the registry.
@@ -2029,6 +2030,13 @@ TextAttribute SCREEN_INFORMATION::GetPopupAttributes() const
20292030
// <none>
20302031
void SCREEN_INFORMATION::SetAttributes(const TextAttribute& attributes)
20312032
{
2033+
if (_ignoreLegacyEquivalentVTAttributes)
2034+
{
2035+
// See the comment on StripErroneousVT16VersionsOfLegacyDefaults for more info.
2036+
_textBuffer->SetCurrentAttributes(TextAttribute::StripErroneousVT16VersionsOfLegacyDefaults(attributes));
2037+
return;
2038+
}
2039+
20322040
_textBuffer->SetCurrentAttributes(attributes);
20332041

20342042
// 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
26752683
marginsSet ? marginRect.Bottom : buffer.BottomInclusive() });
26762684
return margin;
26772685
}
2686+
2687+
// Routine Description:
2688+
// - Engages the legacy VT handling quirk; see TextAttribute::StripErroneousVT16VersionsOfLegacyDefaults
2689+
void SCREEN_INFORMATION::SetIgnoreLegacyEquivalentVTAttributes() noexcept
2690+
{
2691+
_ignoreLegacyEquivalentVTAttributes = true;
2692+
}
2693+
2694+
// Routine Description:
2695+
// - Disengages the legacy VT handling quirk; see TextAttribute::StripErroneousVT16VersionsOfLegacyDefaults
2696+
void SCREEN_INFORMATION::ResetIgnoreLegacyEquivalentVTAttributes() noexcept
2697+
{
2698+
_ignoreLegacyEquivalentVTAttributes = false;
2699+
}

src/host/screenInfo.hpp

+5
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,9 @@ class SCREEN_INFORMATION : public ConsoleObjectHeader, public Microsoft::Console
238238

239239
void InitializeCursorRowAttributes();
240240

241+
void SetIgnoreLegacyEquivalentVTAttributes() noexcept;
242+
void ResetIgnoreLegacyEquivalentVTAttributes() noexcept;
243+
241244
private:
242245
SCREEN_INFORMATION(_In_ Microsoft::Console::Interactivity::IWindowMetrics* pMetrics,
243246
_In_ Microsoft::Console::Interactivity::IAccessibilityNotifier* pNotifier,
@@ -300,6 +303,8 @@ class SCREEN_INFORMATION : public ConsoleObjectHeader, public Microsoft::Console
300303

301304
ScreenBufferRenderTarget _renderTarget;
302305

306+
bool _ignoreLegacyEquivalentVTAttributes;
307+
303308
#ifdef UNIT_TESTING
304309
friend class TextBufferIteratorTests;
305310
friend class ScreenBufferTests;

src/host/ut_host/ApiRoutinesTests.cpp

+2-2
Original file line numberDiff line numberDiff line change
@@ -386,7 +386,7 @@ class ApiRoutinesTests
386386
const size_t cchWriteLength = std::min(cchIncrement, cchTestText - i);
387387

388388
// Run the test method
389-
const HRESULT hr = _pApiRoutines->WriteConsoleAImpl(si, { pszTestText + i, cchWriteLength }, cchRead, waiter);
389+
const HRESULT hr = _pApiRoutines->WriteConsoleAImpl(si, { pszTestText + i, cchWriteLength }, cchRead, false, waiter);
390390

391391
VERIFY_ARE_EQUAL(S_OK, hr, L"Successful result code from writing.");
392392
if (!fInduceWait)
@@ -442,7 +442,7 @@ class ApiRoutinesTests
442442

443443
size_t cchRead = 0;
444444
std::unique_ptr<IWaitRoutine> waiter;
445-
const HRESULT hr = _pApiRoutines->WriteConsoleWImpl(si, testText, cchRead, waiter);
445+
const HRESULT hr = _pApiRoutines->WriteConsoleWImpl(si, testText, cchRead, false, waiter);
446446

447447
VERIFY_ARE_EQUAL(S_OK, hr, L"Successful result code from writing.");
448448
if (!fInduceWait)

0 commit comments

Comments
 (0)