Skip to content

Commit

Permalink
Add support for the DECAC escape sequence (#13058)
Browse files Browse the repository at this point in the history
The `DECAC` (Assign Colors) escape sequence controls which color table
entries are associated with the default foreground and background
colors. This is how you would change the default colors on the the
original DEC VT525 terminals.

But `DECAC` also allows you to assign the color table entries for the
"window frame", which in our case is mapped to the tab color (just the
background for now). So this now gives us a way to control the tab color
via an escape sequence as well.

DETAILS
-------

The way this works is there are now two new entries in the color table
for the frame colors, and two new aliases in the color alias table that
are mapped to those color table entries. As previously mentioned, only
the background is used for now.

By default, the colors are set to `INVALID_COLOR`, which indicates that
the system colors should be used. But if the user has set a `tabColor`
property in their profile, the frame background will be initialized with
that value instead.

And note that some of the existing color table entries are now
renumbered for compatibility with XTerm, which uses entries 256 to 260
for special colors which we don't yet support. Our default colors are
now at 261 and 262, the frame colors are 263 and 264, and the cursor
color is 265. 

So at runtime, you can change the tab color programmatically by setting
the color table entry at index 262 using `OSC 4` (assuming you need a
specific RGB value). Otherwise if you just want to set the tab color to
an existing color index, you can use `DECAC 2`.

You can even make the tab color automatically match background color by
mapping the frame background alias to the color table entry for the
default background, using `DECAC 2;261;262` (technically this is mapping
both the the foreground and background).

This PR doesn't include support for querying the color mapping with
`DECRQSS`, and doesn't support resetting the colors with `RIS`, but
hopefully those can be figured out in a future PR - there are some
complications that'll need to be resolved first.

## Validation Steps Performed

I've added a basic unit test that confirms the `DECAC` escape sequence
updates the color aliases in the render settings as expected. I've also
manually confirmed that the tab color in Windows Terminal is updated by
`DECAC 2`, and the default colors are updated in both conhost and WT
using `DECAC 1`.

Closes #6574
  • Loading branch information
j4james authored May 12, 2022
1 parent 9cb2bcc commit a69ce89
Show file tree
Hide file tree
Showing 18 changed files with 155 additions and 47 deletions.
13 changes: 9 additions & 4 deletions src/buffer/out/TextColor.h
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ enum class ColorAlias : size_t
{
DefaultForeground,
DefaultBackground,
FrameForeground,
FrameBackground,
ENUM_COUNT // must be the last element in the enum class
};

Expand All @@ -72,10 +74,13 @@ struct TextColor
static constexpr BYTE BRIGHT_CYAN = 14;
static constexpr BYTE BRIGHT_WHITE = 15;

static constexpr size_t DEFAULT_FOREGROUND = 256;
static constexpr size_t DEFAULT_BACKGROUND = 257;
static constexpr size_t CURSOR_COLOR = 258;
static constexpr size_t TABLE_SIZE = 259;
// Entries 256 to 260 are reserved for XTerm compatibility.
static constexpr size_t DEFAULT_FOREGROUND = 261;
static constexpr size_t DEFAULT_BACKGROUND = 262;
static constexpr size_t FRAME_FOREGROUND = 263;
static constexpr size_t FRAME_BACKGROUND = 264;
static constexpr size_t CURSOR_COLOR = 265;
static constexpr size_t TABLE_SIZE = 266;

constexpr TextColor() noexcept :
_meta{ ColorType::IsDefault },
Expand Down
27 changes: 9 additions & 18 deletions src/cascadia/TerminalControl/ControlCore.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation
auto pfnTitleChanged = std::bind(&ControlCore::_terminalTitleChanged, this, std::placeholders::_1);
_terminal->SetTitleChangedCallback(pfnTitleChanged);

auto pfnTabColorChanged = std::bind(&ControlCore::_terminalTabColorChanged, this, std::placeholders::_1);
_terminal->SetTabColorChangedCallback(pfnTabColorChanged);

auto pfnScrollPositionChanged = std::bind(&ControlCore::_terminalScrollPositionChanged, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3);
_terminal->SetScrollPositionChangedCallback(pfnScrollPositionChanged);

Expand Down Expand Up @@ -126,6 +123,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
_renderer = std::make_unique<::Microsoft::Console::Render::Renderer>(renderSettings, _terminal.get(), nullptr, 0, std::move(renderThread));

_renderer->SetBackgroundColorChangedCallback([this]() { _rendererBackgroundColorChanged(); });
_renderer->SetFrameColorChangedCallback([this]() { _rendererTabColorChanged(); });

_renderer->SetRendererEnteredErrorStateCallback([weakThis = get_weak()]() {
if (auto strongThis{ weakThis.get() })
Expand Down Expand Up @@ -647,6 +645,9 @@ namespace winrt::Microsoft::Terminal::Control::implementation
// Inform the renderer of our opacity
_renderEngine->EnableTransparentBackground(_isBackgroundTransparent());

// Trigger a redraw to repaint the window background and tab colors.
_renderer->TriggerRedrawAll(true, true);

_updateAntiAliasingMode();

if (sizeChanged)
Expand Down Expand Up @@ -1144,21 +1145,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation
_TitleChangedHandlers(*this, winrt::make<TitleChangedEventArgs>(winrt::hstring{ wstr }));
}

// Method Description:
// - Called for the Terminal's TabColorChanged callback. This will re-raise
// a new winrt TypedEvent that can be listened to.
// - The listeners to this event will re-query the control for the current
// value of TabColor().
// Arguments:
// - <unused>
// Return Value:
// - <none>
void ControlCore::_terminalTabColorChanged(const std::optional<til::color> /*color*/)
{
// Raise a TabColorChanged event
_TabColorChangedHandlers(*this, nullptr);
}

// Method Description:
// - Update the position and size of the scrollbar to match the given
// viewport top, viewport height, and buffer size.
Expand Down Expand Up @@ -1356,6 +1342,11 @@ namespace winrt::Microsoft::Terminal::Control::implementation
_BackgroundColorChangedHandlers(*this, nullptr);
}

void ControlCore::_rendererTabColorChanged()
{
_TabColorChangedHandlers(*this, nullptr);
}

void ControlCore::BlinkAttributeTick()
{
auto lock = _terminal->LockForWriting();
Expand Down
2 changes: 1 addition & 1 deletion src/cascadia/TerminalControl/ControlCore.h
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation
void _terminalCopyToClipboard(std::wstring_view wstr);
void _terminalWarningBell();
void _terminalTitleChanged(std::wstring_view wstr);
void _terminalTabColorChanged(const std::optional<til::color> color);
void _terminalScrollPositionChanged(const int viewTop,
const int viewHeight,
const int bufferSize);
Expand All @@ -278,6 +277,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
void _rendererWarning(const HRESULT hr);
void _renderEngineSwapChainChanged();
void _rendererBackgroundColorChanged();
void _rendererTabColorChanged();
#pragma endregion

void _raiseReadOnlyWarning();
Expand Down
26 changes: 12 additions & 14 deletions src/cascadia/TerminalCore/Terminal.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -125,23 +125,18 @@ void Terminal::UpdateSettings(ICoreSettings settings)

if (settings.TabColor() == nullptr)
{
_tabColor = std::nullopt;
_renderSettings.SetColorTableEntry(TextColor::FRAME_BACKGROUND, INVALID_COLOR);
}
else
{
_tabColor = settings.TabColor().Value();
_renderSettings.SetColorTableEntry(TextColor::FRAME_BACKGROUND, til::color{ settings.TabColor().Value() });
}

if (!_startingTabColor && settings.StartingTabColor())
{
_startingTabColor = settings.StartingTabColor().Value();
}

if (_pfnTabColorChanged)
{
_pfnTabColorChanged(GetTabColor());
}

// TODO:MSFT:21327402 - if HistorySize has changed, resize the buffer so we
// have a smaller scrollback. We should do this carefully - if the new buffer
// size is smaller than where the mutable viewport currently is, we'll want
Expand Down Expand Up @@ -1272,11 +1267,6 @@ void Terminal::SetTitleChangedCallback(std::function<void(std::wstring_view)> pf
_pfnTitleChanged.swap(pfn);
}

void Terminal::SetTabColorChangedCallback(std::function<void(const std::optional<til::color>)> pfn) noexcept
{
_pfnTabColorChanged.swap(pfn);
}

void Terminal::SetCopyToClipboardCallback(std::function<void(std::wstring_view)> pfn) noexcept
{
_pfnCopyToClipboard.swap(pfn);
Expand Down Expand Up @@ -1357,10 +1347,18 @@ void Terminal::ClearPatternTree() noexcept

// Method Description:
// - Returns the tab color
// If the starting color exits, it's value is preferred
// If the starting color exists, its value is preferred
const std::optional<til::color> Terminal::GetTabColor() const noexcept
{
return _startingTabColor.has_value() ? _startingTabColor : _tabColor;
if (_startingTabColor.has_value())
{
return _startingTabColor;
}
else
{
const auto tabColor = _renderSettings.GetColorAlias(ColorAlias::FrameBackground);
return tabColor == INVALID_COLOR ? std::nullopt : std::optional<til::color>{ tabColor };
}
}

// Method Description:
Expand Down
3 changes: 0 additions & 3 deletions src/cascadia/TerminalCore/Terminal.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,6 @@ class Microsoft::Terminal::Core::Terminal final :
void SetWriteInputCallback(std::function<void(std::wstring_view)> pfn) noexcept;
void SetWarningBellCallback(std::function<void()> pfn) noexcept;
void SetTitleChangedCallback(std::function<void(std::wstring_view)> pfn) noexcept;
void SetTabColorChangedCallback(std::function<void(const std::optional<til::color>)> pfn) noexcept;
void SetCopyToClipboardCallback(std::function<void(std::wstring_view)> pfn) noexcept;
void SetScrollPositionChangedCallback(std::function<void(const int, const int, const int)> pfn) noexcept;
void SetCursorPositionChangedCallback(std::function<void()> pfn) noexcept;
Expand Down Expand Up @@ -267,7 +266,6 @@ class Microsoft::Terminal::Core::Terminal final :

std::function<void(const int, const int, const int)> _pfnScrollPositionChanged;
std::function<void()> _pfnCursorPositionChanged;
std::function<void(const std::optional<til::color>)> _pfnTabColorChanged;
std::function<void()> _pfnTaskbarProgressChanged;
std::function<void(bool)> _pfnShowWindowChanged;

Expand All @@ -277,7 +275,6 @@ class Microsoft::Terminal::Core::Terminal final :

std::optional<std::wstring> _title;
std::wstring _startingTitle;
std::optional<til::color> _tabColor;
std::optional<til::color> _startingTabColor;

CursorType _defaultCursorShape;
Expand Down
38 changes: 38 additions & 0 deletions src/host/ut_host/ScreenBufferTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,8 @@ class ScreenBufferTests

TEST_METHOD(SetDefaultBackgroundColor);

TEST_METHOD(AssignColorAliases);

TEST_METHOD(DeleteCharsNearEndOfLine);
TEST_METHOD(DeleteCharsNearEndOfLineSimpleFirstCase);
TEST_METHOD(DeleteCharsNearEndOfLineSimpleSecondCase);
Expand Down Expand Up @@ -3005,6 +3007,42 @@ void ScreenBufferTests::SetDefaultBackgroundColor()
VERIFY_ARE_EQUAL(originalColor, newColor);
}

void ScreenBufferTests::AssignColorAliases()
{
auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
auto& stateMachine = gci.GetActiveOutputBuffer().GetStateMachine();
auto& renderSettings = gci.GetRenderSettings();

const auto defaultFg = renderSettings.GetColorAliasIndex(ColorAlias::DefaultForeground);
const auto defaultBg = renderSettings.GetColorAliasIndex(ColorAlias::DefaultBackground);
const auto frameFg = renderSettings.GetColorAliasIndex(ColorAlias::FrameForeground);
const auto frameBg = renderSettings.GetColorAliasIndex(ColorAlias::FrameBackground);

auto resetAliases = wil::scope_exit([&] {
renderSettings.SetColorAliasIndex(ColorAlias::DefaultForeground, defaultFg);
renderSettings.SetColorAliasIndex(ColorAlias::DefaultBackground, defaultBg);
renderSettings.SetColorAliasIndex(ColorAlias::FrameForeground, frameFg);
renderSettings.SetColorAliasIndex(ColorAlias::FrameBackground, frameBg);
});

Log::Comment(L"Test invalid item color assignment");
stateMachine.ProcessString(L"\033[0;12;34,|");
VERIFY_ARE_EQUAL(defaultFg, renderSettings.GetColorAliasIndex(ColorAlias::DefaultForeground));
VERIFY_ARE_EQUAL(defaultBg, renderSettings.GetColorAliasIndex(ColorAlias::DefaultBackground));
VERIFY_ARE_EQUAL(frameFg, renderSettings.GetColorAliasIndex(ColorAlias::FrameForeground));
VERIFY_ARE_EQUAL(frameBg, renderSettings.GetColorAliasIndex(ColorAlias::FrameBackground));

Log::Comment(L"Test normal text color assignment");
stateMachine.ProcessString(L"\033[1;23;45,|");
VERIFY_ARE_EQUAL(23u, renderSettings.GetColorAliasIndex(ColorAlias::DefaultForeground));
VERIFY_ARE_EQUAL(45u, renderSettings.GetColorAliasIndex(ColorAlias::DefaultBackground));

Log::Comment(L"Test window frame color assignment");
stateMachine.ProcessString(L"\033[2;34;56,|");
VERIFY_ARE_EQUAL(34u, renderSettings.GetColorAliasIndex(ColorAlias::FrameForeground));
VERIFY_ARE_EQUAL(56u, renderSettings.GetColorAliasIndex(ColorAlias::FrameBackground));
}

void ScreenBufferTests::DeleteCharsNearEndOfLine()
{
// Created for MSFT:19888564.
Expand Down
9 changes: 8 additions & 1 deletion src/renderer/base/RenderSettings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,14 @@ RenderSettings::RenderSettings() noexcept

SetColorTableEntry(TextColor::DEFAULT_FOREGROUND, INVALID_COLOR);
SetColorTableEntry(TextColor::DEFAULT_BACKGROUND, INVALID_COLOR);
SetColorTableEntry(TextColor::FRAME_FOREGROUND, INVALID_COLOR);
SetColorTableEntry(TextColor::FRAME_BACKGROUND, INVALID_COLOR);
SetColorTableEntry(TextColor::CURSOR_COLOR, INVALID_COLOR);

SetColorAliasIndex(ColorAlias::DefaultForeground, TextColor::DARK_WHITE);
SetColorAliasIndex(ColorAlias::DefaultBackground, TextColor::DARK_BLACK);
SetColorAliasIndex(ColorAlias::FrameForeground, TextColor::FRAME_FOREGROUND);
SetColorAliasIndex(ColorAlias::FrameBackground, TextColor::FRAME_BACKGROUND);
}

// Routine Description:
Expand Down Expand Up @@ -149,7 +153,10 @@ COLORREF RenderSettings::GetColorAlias(const ColorAlias alias) const
// - tableIndex - The new position of the alias in the color table.
void RenderSettings::SetColorAliasIndex(const ColorAlias alias, const size_t tableIndex) noexcept
{
gsl::at(_colorAliasIndices, static_cast<size_t>(alias)) = tableIndex;
if (tableIndex < TextColor::TABLE_SIZE)
{
gsl::at(_colorAliasIndices, static_cast<size_t>(alias)) = tableIndex;
}
}

// Routine Description:
Expand Down
19 changes: 18 additions & 1 deletion src/renderer/base/renderer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -299,9 +299,10 @@ void Renderer::TriggerRedrawCursor(const COORD* const pcoord)
// - NOTE: Use sparingly. Try to reduce the refresh region where possible. Only use when a global state change has occurred.
// Arguments:
// - backgroundChanged - Set to true if the background color has changed.
// - frameChanged - Set to true if the frame colors have changed.
// Return Value:
// - <none>
void Renderer::TriggerRedrawAll(const bool backgroundChanged)
void Renderer::TriggerRedrawAll(const bool backgroundChanged, const bool frameChanged)
{
FOREACH_ENGINE(pEngine)
{
Expand All @@ -314,6 +315,11 @@ void Renderer::TriggerRedrawAll(const bool backgroundChanged)
{
_pfnBackgroundColorChanged();
}

if (frameChanged && _pfnFrameColorChanged)
{
_pfnFrameColorChanged();
}
}

// Method Description:
Expand Down Expand Up @@ -1348,6 +1354,17 @@ void Renderer::SetBackgroundColorChangedCallback(std::function<void()> pfn)
_pfnBackgroundColorChanged = std::move(pfn);
}

// Method Description:
// - Registers a callback for when the frame colors have changed
// Arguments:
// - pfn: the callback
// Return Value:
// - <none>
void Renderer::SetFrameColorChangedCallback(std::function<void()> pfn)
{
_pfnFrameColorChanged = std::move(pfn);
}

// Method Description:
// - Registers a callback that will be called when this renderer gives up.
// An application consuming a renderer can use this to display auxiliary Retry UI
Expand Down
4 changes: 3 additions & 1 deletion src/renderer/base/renderer.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ namespace Microsoft::Console::Render
void TriggerRedraw(const Microsoft::Console::Types::Viewport& region);
void TriggerRedraw(const COORD* const pcoord);
void TriggerRedrawCursor(const COORD* const pcoord);
void TriggerRedrawAll(const bool backgroundChanged = false);
void TriggerRedrawAll(const bool backgroundChanged = false, const bool frameChanged = false);
void TriggerTeardown() noexcept;

void TriggerSelection();
Expand Down Expand Up @@ -85,6 +85,7 @@ namespace Microsoft::Console::Render
void AddRenderEngine(_In_ IRenderEngine* const pEngine);

void SetBackgroundColorChangedCallback(std::function<void()> pfn);
void SetFrameColorChangedCallback(std::function<void()> pfn);
void SetRendererEnteredErrorStateCallback(std::function<void()> pfn);
void ResetErrorStateAndResume();

Expand Down Expand Up @@ -123,6 +124,7 @@ namespace Microsoft::Console::Render
std::vector<Cluster> _clusterBuffer;
std::vector<SMALL_RECT> _previousSelection;
std::function<void()> _pfnBackgroundColorChanged;
std::function<void()> _pfnFrameColorChanged;
std::function<void()> _pfnRendererEnteredErrorState;
bool _destructing = false;
bool _forceUpdateViewport = true;
Expand Down
6 changes: 6 additions & 0 deletions src/terminal/adapter/DispatchTypes.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,12 @@ namespace Microsoft::Console::VirtualTerminal

namespace Microsoft::Console::VirtualTerminal::DispatchTypes
{
enum class ColorItem : VTInt
{
NormalText = 1,
WindowFrame = 2,
};

enum class EraseType : VTInt
{
ToEnd = 0,
Expand Down
1 change: 1 addition & 0 deletions src/terminal/adapter/ITermDispatch.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ class Microsoft::Console::VirtualTerminal::ITermDispatch
virtual bool SetColorTableEntry(const size_t tableIndex, const DWORD color) = 0; // OSCColorTable
virtual bool SetDefaultForeground(const DWORD color) = 0; // OSCDefaultForeground
virtual bool SetDefaultBackground(const DWORD color) = 0; // OSCDefaultBackground
virtual bool AssignColor(const DispatchTypes::ColorItem item, const VTInt fgIndex, const VTInt bgIndex) = 0; // DECAC

virtual bool EraseInDisplay(const DispatchTypes::EraseType eraseType) = 0; // ED
virtual bool EraseInLine(const DispatchTypes::EraseType eraseType) = 0; // EL
Expand Down
Loading

0 comments on commit a69ce89

Please sign in to comment.