Add support for OSC 104, 110, 111, 112 and 117 (resets) (#18767)

This pull request adds support for resetting the various color table
entries and xterm resource values back to their defaults.

Building on the default color table James introduced in #17879, it was
relatively straightforward to add support for resetting specific
entries.

This implementation cleaves tightly to observed behavior in xterm(379)
rather than observed behavior in libvte(0.70.6). They differ in the
following ways:

- xterm rejects any OSC [110..119] with any number of parameters; libvte
accepts it but only resets the first color.
- When passed a list of color indices to reset in 104, xterm resets any
colors up until the first one which fails to parse as an integer and
does _not_ reset the rest; libvte resets all parseable color indices.

I was unable to verify how these reset commands interact with colors set
via `DECAC Assign Color` so I went with the implementation that made the
most sense:

- Resetting the background color with `110` also restores the background
color alias entry to its pre-`DECAC` value; this results in the
perceived background color returning to e.g. index 0 in conhost and the
`background` color in Terminal.
- _ibid._ for the foreground color

Refs #18695
Refs #17879
Closes #3719
This commit is contained in:
Dustin L. Howett 2025-04-09 19:11:47 -05:00 committed by GitHub
parent 22c509f426
commit 5f311506dc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 217 additions and 9 deletions

View File

@ -112,6 +112,20 @@ COLORREF RenderSettings::GetColorTableEntry(const size_t tableIndex) const
return _colorTable.at(tableIndex);
}
// Routine Description:
// - Restores all of the xterm-addressable colors to the ones saved in SaveDefaultSettings.
void RenderSettings::RestoreDefaultIndexed256ColorTable()
{
std::copy_n(_defaultColorTable.begin(), 256, _colorTable.begin());
}
// Routine Description:
// - Restores a color table entry to the value saved in SaveDefaultSettings.
void RenderSettings::RestoreDefaultColorTableEntry(const size_t tableIndex)
{
_colorTable.at(tableIndex) = _defaultColorTable.at(tableIndex);
}
// Routine Description:
// - Sets the position in the color table for the given color alias and updates the color.
// Arguments:
@ -159,6 +173,11 @@ size_t RenderSettings::GetColorAliasIndex(const ColorAlias alias) const noexcept
return gsl::at(_colorAliasIndices, static_cast<size_t>(alias));
}
void RenderSettings::RestoreDefaultColorAliasIndex(const ColorAlias alias) noexcept
{
gsl::at(_colorAliasIndices, static_cast<size_t>(alias)) = gsl::at(_defaultColorAliasIndices, static_cast<size_t>(alias));
}
// Routine Description:
// - Calculates the RGB colors of a given text attribute, using the current
// color table configuration and active render settings.

View File

@ -37,10 +37,13 @@ namespace Microsoft::Console::Render
void ResetColorTable() noexcept;
void SetColorTableEntry(const size_t tableIndex, const COLORREF color);
COLORREF GetColorTableEntry(const size_t tableIndex) const;
void RestoreDefaultIndexed256ColorTable();
void RestoreDefaultColorTableEntry(const size_t tableIndex);
void SetColorAlias(const ColorAlias alias, const size_t tableIndex, const COLORREF color);
COLORREF GetColorAlias(const ColorAlias alias) const;
void SetColorAliasIndex(const ColorAlias alias, const size_t tableIndex) noexcept;
size_t GetColorAliasIndex(const ColorAlias alias) const noexcept;
void RestoreDefaultColorAliasIndex(const ColorAlias alias) noexcept;
std::pair<COLORREF, COLORREF> GetAttributeColors(const TextAttribute& attr) const noexcept;
std::pair<COLORREF, COLORREF> GetAttributeColorsWithAlpha(const TextAttribute& attr) const noexcept;
COLORREF GetAttributeUnderlineColor(const TextAttribute& attr) const noexcept;

View File

@ -78,8 +78,11 @@ public:
virtual void TabSet(const VTParameter setType) = 0; // DECST8C
virtual void SetColorTableEntry(const size_t tableIndex, const DWORD color) = 0; // OSCSetColorTable
virtual void RequestColorTableEntry(const size_t tableIndex) = 0; // OSCGetColorTable
virtual void SetXtermColorResource(const size_t resource, const DWORD color) = 0; // OSCSetDefaultForeground, OSCSetDefaultBackground, OSCSetCursorColor, OSCResetCursorColor
virtual void ResetColorTable() = 0; // OSCResetColorTable
virtual void ResetColorTableEntry(const size_t tableIndex) = 0; // OSCResetColorTable
virtual void SetXtermColorResource(const size_t resource, const DWORD color) = 0; // OSCSetDefaultForeground, OSCSetDefaultBackground, OSCSetCursorColor
virtual void RequestXtermColorResource(const size_t resource) = 0; // OSCGetDefaultForeground, OSCGetDefaultBackground, OSCGetCursorColor
virtual void ResetXtermColorResource(const size_t resource) = 0; // OSCResetForegroundColor, OSCResetBackgroundColor, OSCResetCursorColor, OSCResetHighlightColor
virtual void AssignColor(const DispatchTypes::ColorItem item, const VTInt fgIndex, const VTInt bgIndex) = 0; // DECAC
virtual void EraseInDisplay(const DispatchTypes::EraseType eraseType) = 0; // ED

View File

@ -3292,6 +3292,40 @@ void AdaptDispatch::RequestColorTableEntry(const size_t tableIndex)
}
}
void AdaptDispatch::ResetColorTable()
{
_renderSettings.RestoreDefaultIndexed256ColorTable();
if (_renderer)
{
// This is pessimistic because it's unlikely that the frame or background changed,
// but let's tell the renderer that both changed anyway.
_renderer->TriggerRedrawAll(true, true);
}
}
// Method Description:
// - Restores a single color table entry to its default user-specified value
// Arguments:
// - tableIndex: The VT color table index
void AdaptDispatch::ResetColorTableEntry(const size_t tableIndex)
{
_renderSettings.RestoreDefaultColorTableEntry(tableIndex);
if (_renderer)
{
// If we're updating the background color, we need to let the renderer
// know, since it may want to repaint the window background to match.
const auto backgroundIndex = _renderSettings.GetColorAliasIndex(ColorAlias::DefaultBackground);
const auto backgroundChanged = (tableIndex == backgroundIndex);
// Similarly for the frame color, the tab may need to be repainted.
const auto frameIndex = _renderSettings.GetColorAliasIndex(ColorAlias::FrameBackground);
const auto frameChanged = (tableIndex == frameIndex);
_renderer->TriggerRedrawAll(backgroundChanged, frameChanged);
}
}
// Method Description:
// - Sets one Xterm Color Resource such as Default Foreground, Background, Cursor
void AdaptDispatch::SetXtermColorResource(const size_t resource, const DWORD color)
@ -3338,6 +3372,25 @@ void AdaptDispatch::RequestXtermColorResource(const size_t resource)
}
}
// Method Description:
// - Restores to the original user-provided value one Xterm Color Resource such as Default Foreground, Background, Cursor
void AdaptDispatch::ResetXtermColorResource(const size_t resource)
{
assert(resource >= 10);
const auto mappingIndex = resource - 10;
const auto& oscMapping = XtermResourceColorTableMappings.at(mappingIndex);
if (oscMapping.ColorTableIndex > 0)
{
if (oscMapping.AliasIndex >= 0)
{
// If this color reset applies to an aliased color, point the alias back at the original color
_renderSettings.RestoreDefaultColorAliasIndex(static_cast<ColorAlias>(oscMapping.AliasIndex));
}
ResetColorTableEntry(oscMapping.ColorTableIndex);
}
}
// Method Description:
// DECAC - Assigns the foreground and background color indexes that should be
// used for a given aspect of the user interface.

View File

@ -133,8 +133,11 @@ namespace Microsoft::Console::VirtualTerminal
void SetColorTableEntry(const size_t tableIndex,
const DWORD color) override; // OSCSetColorTable
void RequestColorTableEntry(const size_t tableIndex) override; // OSCGetColorTable
void SetXtermColorResource(const size_t resource, const DWORD color) override; // OSCSetDefaultForeground, OSCSetDefaultBackground, OSCSetCursorColor, OSCResetCursorColor
void ResetColorTable() override; // OSCResetColorTable
void ResetColorTableEntry(const size_t tableIndex) override; // OSCResetColorTable
void SetXtermColorResource(const size_t resource, const DWORD color) override; // OSCSetDefaultForeground, OSCSetDefaultBackground, OSCSetCursorColor
void RequestXtermColorResource(const size_t resource) override; // OSCGetDefaultForeground, OSCGetDefaultBackground, OSCGetCursorColor
void ResetXtermColorResource(const size_t resource) override; // OSCResetForegroundColor, OSCResetBackgroundColor, OSCResetCursorColor, OSCResetHighlightColor
void AssignColor(const DispatchTypes::ColorItem item, const VTInt fgIndex, const VTInt bgIndex) override; // DECAC
void WindowManipulation(const DispatchTypes::WindowManipulationType function,

View File

@ -71,8 +71,11 @@ public:
void TabSet(const VTParameter /*setType*/) override {} // DECST8C
void SetColorTableEntry(const size_t /*tableIndex*/, const DWORD /*color*/) override {} // OSCSetColorTable
void RequestColorTableEntry(const size_t /*tableIndex*/) override {} // OSCGetColorTable
void SetXtermColorResource(const size_t /*resource*/, const DWORD /*color*/) override {} // OSCSetDefaultForeground, OSCSetDefaultBackground, OSCSetCursorColor, OSCResetCursorColor
void ResetColorTable() override {} // OSCResetColorTable
void ResetColorTableEntry(const size_t /*tableIndex*/) override {} // OSCResetColorTable
void SetXtermColorResource(const size_t /*resource*/, const DWORD /*color*/) override {} // OSCSetDefaultForeground, OSCSetDefaultBackground, OSCSetCursorColor
void RequestXtermColorResource(const size_t /*resource*/) override {} // OSCGetDefaultForeground, OSCGetDefaultBackground, OSCGetCursorColor
void ResetXtermColorResource(const size_t /*resource*/) override {} // OSCResetForegroundColor, OSCResetBackgroundColor, OSCResetCursorColor, OSCResetHighlightColor
void AssignColor(const DispatchTypes::ColorItem /*item*/, const VTInt /*fgIndex*/, const VTInt /*bgIndex*/) override {} // DECAC
void EraseInDisplay(const DispatchTypes::EraseType /* eraseType*/) override {} // ED

View File

@ -810,10 +810,40 @@ bool OutputStateMachineEngine::ActionOscDispatch(const size_t parameter, const s
}
break;
}
case OscActionCodes::ResetCursorColor:
case OscActionCodes::ResetColor:
{
// The reset codes for xterm dynamic resources are the set codes + 100
_dispatch->SetXtermColorResource(parameter - 100u, INVALID_COLOR);
if (string.empty())
{
_dispatch->ResetColorTable();
}
else
{
for (auto&& c : til::split_iterator{ string, L';' })
{
if (const auto index{ til::parse_unsigned<size_t>(c, 10) }; index)
{
_dispatch->ResetColorTableEntry(*index);
}
else
{
// NOTE: xterm stops at the first unparseable index whereas VTE keeps going.
break;
}
}
}
break;
}
case OscActionCodes::ResetForegroundColor:
case OscActionCodes::ResetBackgroundColor:
case OscActionCodes::ResetCursorColor:
case OscActionCodes::ResetHighlightColor:
{
// NOTE: xterm ignores the request if there's any parameters whereas VTE resets the provided index and ignores the rest
if (string.empty())
{
// The reset codes for xterm dynamic resources are the set codes + 100
_dispatch->ResetXtermColorResource(parameter - 100u);
}
break;
}
case OscActionCodes::Hyperlink:

View File

@ -214,9 +214,11 @@ namespace Microsoft::Console::VirtualTerminal
SetHighlightColor = 17,
DECSWT_SetWindowTitle = 21,
SetClipboard = 52,
ResetForegroundColor = 110, // Not implemented
ResetBackgroundColor = 111, // Not implemented
ResetColor = 104,
ResetForegroundColor = 110,
ResetBackgroundColor = 111,
ResetCursorColor = 112,
ResetHighlightColor = 117,
FinalTermAction = 133,
VsCodeAction = 633,
ITerm2Action = 1337,

View File

@ -1163,7 +1163,8 @@ public:
_hyperlinkMode{ false },
_options{ s_cMaxOptions, static_cast<DispatchTypes::GraphicsOptions>(s_uiGraphicsCleared) }, // fill with cleared option
_colorTable{},
_setColorTableEntry{ false }
_setColorTableEntry{ false },
_resetAllColors{ false }
{
}
@ -1390,6 +1391,16 @@ public:
_colorTableEntriesRequested.push_back(tableIndex);
}
void ResetColorTable() noexcept override
{
_resetAllColors = true;
}
void ResetColorTableEntry(const size_t tableIndex) noexcept override
{
_colorTableEntriesReset.push_back(tableIndex);
}
void SetXtermColorResource(const size_t resource, const DWORD color) override
{
_xtermResourcesChanged.push_back(resource);
@ -1401,6 +1412,11 @@ public:
_xtermResourcesRequested.push_back(resource);
}
void ResetXtermColorResource(const size_t resource) override
{
_xtermResourcesReset.push_back(resource);
}
void SetClipboard(wil::zwstring_view content) noexcept override
{
_copyContent = content;
@ -1476,8 +1492,11 @@ public:
std::vector<size_t> _xtermResourcesChanged;
std::vector<DWORD> _xtermResourceValues;
std::vector<size_t> _xtermResourcesRequested;
std::vector<size_t> _xtermResourcesReset;
bool _setColorTableEntry;
std::vector<size_t> _colorTableEntriesRequested;
bool _resetAllColors;
std::vector<size_t> _colorTableEntriesReset;
bool _hyperlinkMode;
std::wstring _copyContent;
std::wstring _uri;
@ -3221,6 +3240,79 @@ class StateMachineExternalTest final
pDispatch->ClearState();
}
TEST_METHOD(TestOscXtermResourceReset)
{
auto dispatch = std::make_unique<StatefulDispatch>();
auto pDispatch = dispatch.get();
auto engine = std::make_unique<OutputStateMachineEngine>(std::move(dispatch));
StateMachine mach(std::move(engine));
mach.ProcessString(L"\033]110\033\\");
VERIFY_ARE_EQUAL(0u, pDispatch->_xtermResourcesChanged.size());
VERIFY_ARE_EQUAL(0u, pDispatch->_xtermResourcesRequested.size());
VERIFY_ARE_EQUAL(1u, pDispatch->_xtermResourcesReset.size());
VERIFY_ARE_EQUAL(10u, pDispatch->_xtermResourcesReset[0]);
pDispatch->ClearState();
mach.ProcessString(L"\033]111;\033\\"); // dangling ;
VERIFY_ARE_EQUAL(0u, pDispatch->_xtermResourcesChanged.size());
VERIFY_ARE_EQUAL(0u, pDispatch->_xtermResourcesRequested.size());
VERIFY_ARE_EQUAL(1u, pDispatch->_xtermResourcesReset.size());
VERIFY_ARE_EQUAL(11u, pDispatch->_xtermResourcesReset[0]);
pDispatch->ClearState();
mach.ProcessString(L"\033]111;110\033\\");
// NOTE: this is xterm behavior - ignore the entire sequence if any params exist
VERIFY_ARE_EQUAL(0u, pDispatch->_xtermResourcesChanged.size());
VERIFY_ARE_EQUAL(0u, pDispatch->_xtermResourcesRequested.size());
VERIFY_ARE_EQUAL(0u, pDispatch->_xtermResourcesReset.size());
pDispatch->ClearState();
}
TEST_METHOD(TestOscColorTableReset)
{
auto dispatch = std::make_unique<StatefulDispatch>();
auto pDispatch = dispatch.get();
auto engine = std::make_unique<OutputStateMachineEngine>(std::move(dispatch));
StateMachine mach(std::move(engine));
mach.ProcessString(L"\033]104\033\\");
VERIFY_IS_TRUE(pDispatch->_resetAllColors);
VERIFY_ARE_EQUAL(0u, pDispatch->_colorTableEntriesReset.size());
VERIFY_ARE_EQUAL(0u, pDispatch->_colorTableEntriesRequested.size());
VERIFY_ARE_EQUAL(0u, pDispatch->_xtermResourcesReset.size());
pDispatch->ClearState();
mach.ProcessString(L"\033]104;1;3;5;7;9\033\\");
VERIFY_IS_FALSE(pDispatch->_resetAllColors);
VERIFY_ARE_EQUAL(5u, pDispatch->_colorTableEntriesReset.size());
VERIFY_ARE_EQUAL(0u, pDispatch->_colorTableEntriesRequested.size());
VERIFY_ARE_EQUAL(0u, pDispatch->_xtermResourcesReset.size());
VERIFY_ARE_EQUAL(1u, pDispatch->_colorTableEntriesReset[0]);
VERIFY_ARE_EQUAL(3u, pDispatch->_colorTableEntriesReset[1]);
VERIFY_ARE_EQUAL(5u, pDispatch->_colorTableEntriesReset[2]);
VERIFY_ARE_EQUAL(7u, pDispatch->_colorTableEntriesReset[3]);
VERIFY_ARE_EQUAL(9u, pDispatch->_colorTableEntriesReset[4]);
pDispatch->ClearState();
// NOTE: xterm behavior - stop after first failed parse
mach.ProcessString(L"\033]104;1;a;3\033\\");
VERIFY_IS_FALSE(pDispatch->_resetAllColors);
VERIFY_IS_FALSE(pDispatch->_setColorTableEntry);
VERIFY_ARE_EQUAL(1u, pDispatch->_colorTableEntriesReset.size());
VERIFY_ARE_EQUAL(0u, pDispatch->_colorTableEntriesRequested.size());
VERIFY_ARE_EQUAL(0u, pDispatch->_xtermResourcesReset.size());
VERIFY_ARE_EQUAL(1u, pDispatch->_colorTableEntriesReset[0]);
pDispatch->ClearState();
mach.ProcessString(L"\033]104;;;\033\\");
VERIFY_IS_FALSE(pDispatch->_setColorTableEntry);
VERIFY_ARE_EQUAL(0u, pDispatch->_colorTableEntriesReset.size());
VERIFY_ARE_EQUAL(0u, pDispatch->_colorTableEntriesRequested.size());
VERIFY_ARE_EQUAL(0u, pDispatch->_xtermResourcesReset.size());
pDispatch->ClearState();
}
TEST_METHOD(TestOscSetWindowTitle)
{
BEGIN_TEST_METHOD_PROPERTIES()