Repository: microsoft/terminal (language: cpp)

Issue:
Feature Request: command or proprietary escape codes to set a tab's color
# Description of the new feature/enhancement

The request is to be able to set a tab's color via a typed command, e.g. `colortab red`. iTerm2 does this via [proprietary escape codes](https://iterm2.com/documentation-escape-codes.html); this combination of three `echo` commands will color a tab pink:

```sh
echo -e "\033]6;1;bg;red;brightness;255\a"
echo -e "\033]6;1;bg;green;brightness;0\a"
echo -e "\033]6;1;bg;blue;brightness;255\a"
```

My scenario is that I want a `.cmd` file to be able to color its resulting tab without manual intervention, e.g. as a blue build process, a green chat bot, a red utility program, etc.

P.S. great job landing #2994!


Discussion / hints:
Why the heck is that sequence formatted that way? Why isn't it just `^[]6;1;r;g;b^G`? 

I mean I'm generally fine with the idea, but I'd just imagine that they would have designed a better sequence tbh. What happens when you only emit 
```
echo -e "\033]6;1;bg;red;brightness;255\a"
```
?

I could see this being useful as another alternative to #3004, where an app could change the tab color as a kind of notification.
I'm alright committing this to the backlog w/ Help Wanted. Yanking triage.
I believe this originally came from Eterm, which is why it doesn't share the typical XTerm color syntax. And to be honest, I'd probably recommend coming up with our own sequence if we want to support this functionality (or at least looking for an alternative that other terminals might be using). I can't see OSC 6 ever being widely used, because it already has at least two other meanings. In XTerm it's used to enable special colors. In Terminal.app and VTE it's used to set the current file path.
I had an idea for a sequence we could possibly use for this. On the DEC VT525, there's a `DECAC` (Assign Color) control sequence which is used to set the default foreground and background colors (essentially the equivalent of the XTerm `OSC 10` and `OSC 11` sequences). But it can also be used to set the _Window Frame_ color, which I think could be considered somewhat equivalent to our tabs.

The VT525 has a concept of multiple sessions, and when the _Frame Windows_ option is selected, a row of icons is displayed across the top of the screen to keep track of the different sessions, very similar in concept to tabs. You can see an image of a VT520 screen showing the session icons here:

https://i.imgur.com/8ujq0uN.jpg

The second session isn't named in that image, so it's not that obvious that you've got two sessions, but you can still see how similar those session identifiers are to tabs. This particular image is from a monochrome device, but I believe both the window frame and the icons would be displayed using the _Window Frame_ colors, so I don't think it would be unreasonable for us to also use those colors for our tabs.
Although now that I'm rereading the documentation, I realise you can only specify a color index when setting these colors. And technically the VT525 only supported index values in the range 0 to 15, but we could probably support all 256 colors from the XTerm color table and still be backwards compatible. But if you were hoping to be able to specify a full RGB value, then this isn't quite what you want.
Just wondering: is the only reason why this is stalling that there's no agreement on what the escape sequence should be?
Personally I'm quite keen to implement the `DECAC` sequence (assuming that's something the core devs would be happy to accept), just because I'd like for us to support as many of the standard DEC sequences as possible. I don't know if that would necessarily satisfy those of you that requested this functionality, though.

So while I'm not exactly waiting for agreement on a sequence (I'd probably submit a PR for `DECAC` at some point anyway), the fact that nobody seemed interested in my proposed solution made me think it wasn't worth prioritizing.
It's probably 50% "no agreement on what the sequence should be" and 50% "there's just not enough demand for us to bother figuring it out". 

Using OSC 6 is a bad idea because it's used for other things. `DECAC` is no good either? ~~So we'd have to come up with our own sequence, and frankly we just haven't done that yet. So this would be setting a precedent in that regard, and isn't something we'd want to do _wrong_.~~ 

~~In previous issues we've mentioned using the OSC prefix `^[]9001` for WT-specific sequences, and I'm still down with that. So this would be something like `^[]9001;1;r;g;b`~~

As I'm typing this up I see j4james's reply. I'm fine with the DECAC solution if he is. He's got a better head for what is or isn't compatible.

I think mostly, there's just not that much priority on such a feature. I think most people have been doing alright with the "set tab color" actions and right click menu, so this just hasn't bubbled up the task list.
I think not being able to specify any RGB color would be quite a limitation / inconsistency with other colors being specified as RGB.

Just my €0.02.

About the priority: yes "set tab color" can be done, but what I'd like to get (and frankly I can't be alone, and those who don't upvote this should really reconsider their line of thinking) is the ability to set the color e.g. when I switch projects, when something special occurs within the tab, to show that some long-lasting command has completed.... etc.
> when something special occurs within the tab

Might want to check https://github.com/microsoft/terminal/issues/6700
> > when something special occurs within the tab
> 
> Might want to check #6700

Interesting indeed, for instance the BEL detection.

Can't wait to have all this released 😆
As noted in https://github.com/microsoft/terminal/issues/11181#issuecomment-918273715, there's an iTerm2 sequence for setting the tab color, which is a little weird but it's an OSC so of course it is. That's a seemingly unambiguous way of implementing this.
I've grown fond of using the functionality in iTerm2 when working from a Mac, via shell functions setting tab color based on keywords indicating what development host I'm logged into or other parameters. When I move to WSL and Windows Terminal for the same kind of work, I keep invoking the shell functions out of habit :). While those iTerm2 sequences are awkward, they are at least functional, and can be encapsulated once in shell functions/scripts, which may be the most common context for using them. Better sequence alternatives would also be OK, though the logic for determining on a remote host what kind of terminal emulator has logged into it could be unpleasant.
Are you referring to the commands used at https://kendsnyder.com/tab-colors-in-iterm2-v10/?

Just tried this in WSL2 running within Windows Terminal Preview 1.13.10395.0, this doesn't change the tab's color...

Did I miss something?

> Are you referring to the commands used at https://kendsnyder.com/tab-colors-in-iterm2-v10/?
> 
> Just tried this in WSL2 running within Windows Terminal Preview 1.13.10395.0, this doesn't change the tab's color...
> 
> Did I miss something?

You didn't miss anything. They do not work in Windows Terminal (Preview or otherwise) today. Under discussion here was the idea of adding a feature to Windows Terminal so that they *do* work (or some other, similar but better escape sequences could be made to work in lieu of these).
I actually had a plan for this that I've been meaning to propose, which doesn't require any proprietary sequences, but would still give you full RGB control of the color. 

The idea is you'd use the `DECAC` sequence to control the palette index for the tab (as suggested above), but then set the actual RGB value of that palette entry with one of the existing palette control sequences: `OSC 4` or `DECRSTS` (once I get around to adding that).

This would work the same way we now handle default foreground and background colors, with the new color alias system. So we'd create a new entry in the color table outside the base 256 range, and that's what the tab color would point to by default.

The simplest way to change the tab color would then be with a palette update sequence setting palette entry 258 (or whatever position we decide to assign to it). If you want to be more robust, though, you could lookup the actual index first with a `DECRQSS` query.

One of the neat benefits of this approach, is you also have the option of setting the tab color index to match the default background color index, and that way you'd have your tab automatically matching your background color (the UI concept proposed in #702).
I would not like a link between background color and tab color, 'cos I want to stick to my (dark grey) background color for my shells, yet I want to be able to easily tell tabs apart thanks to their color.
Oh interesting, so it'd be like:
* By default, the tab color would be set to something like index 258 in the color table.
* You could set the tab color using DECAC to the color of the DARK_RED entry in the table (index 1), or the default BG color (index... 257?)
* Alternatively, you could just change the tab color by setting index 258 directly with OSC4. 

IMO that's a really clever, elegant solution. 

As a sidebar, I actually think there's an API in Window 11 for setting the window frame color. So, we may want to actually extend DECAC here with additional parameters for frame, tab color, etc. I'll leave that best judgement to j4james though, since he's got a better knack for the compat concerns there. 


note to self: [DECAC docs](http://web.mit.edu/dosathena/doc/www/ek-vt520-rm.pdf) (search for "DECAC—Assign Color"). Couldn't find a better source. 

Fix patch:
diff --git a/src/buffer/out/TextColor.h b/src/buffer/out/TextColor.h
index c521ab4cab4..b975028760c 100644
--- a/src/buffer/out/TextColor.h
+++ b/src/buffer/out/TextColor.h
@@ -49,6 +49,8 @@ enum class ColorAlias : size_t
 {
     DefaultForeground,
     DefaultBackground,
+    FrameForeground,
+    FrameBackground,
     ENUM_COUNT // must be the last element in the enum class
 };
 
@@ -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 },
diff --git a/src/cascadia/TerminalControl/ControlCore.cpp b/src/cascadia/TerminalControl/ControlCore.cpp
index be5e7a70c1a..82d2a69224c 100644
--- a/src/cascadia/TerminalControl/ControlCore.cpp
+++ b/src/cascadia/TerminalControl/ControlCore.cpp
@@ -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);
 
@@ -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() })
@@ -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)
@@ -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.
@@ -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();
diff --git a/src/cascadia/TerminalControl/ControlCore.h b/src/cascadia/TerminalControl/ControlCore.h
index 1a8b744cb7f..b235a93fc0b 100644
--- a/src/cascadia/TerminalControl/ControlCore.h
+++ b/src/cascadia/TerminalControl/ControlCore.h
@@ -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);
@@ -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();
diff --git a/src/cascadia/TerminalCore/Terminal.cpp b/src/cascadia/TerminalCore/Terminal.cpp
index 648d7fff902..e18be09b270 100644
--- a/src/cascadia/TerminalCore/Terminal.cpp
+++ b/src/cascadia/TerminalCore/Terminal.cpp
@@ -125,11 +125,11 @@ 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())
@@ -137,11 +137,6 @@ void Terminal::UpdateSettings(ICoreSettings settings)
         _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
@@ -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);
@@ -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:
diff --git a/src/cascadia/TerminalCore/Terminal.hpp b/src/cascadia/TerminalCore/Terminal.hpp
index e89e9167b47..677a0501e89 100644
--- a/src/cascadia/TerminalCore/Terminal.hpp
+++ b/src/cascadia/TerminalCore/Terminal.hpp
@@ -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;
@@ -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;
 
@@ -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;
diff --git a/src/host/ut_host/ScreenBufferTests.cpp b/src/host/ut_host/ScreenBufferTests.cpp
index 27c887d699c..09a5af2c3a7 100644
--- a/src/host/ut_host/ScreenBufferTests.cpp
+++ b/src/host/ut_host/ScreenBufferTests.cpp
@@ -171,6 +171,8 @@ class ScreenBufferTests
 
     TEST_METHOD(SetDefaultBackgroundColor);
 
+    TEST_METHOD(AssignColorAliases);
+
     TEST_METHOD(DeleteCharsNearEndOfLine);
     TEST_METHOD(DeleteCharsNearEndOfLineSimpleFirstCase);
     TEST_METHOD(DeleteCharsNearEndOfLineSimpleSecondCase);
@@ -3004,6 +3006,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.
diff --git a/src/renderer/base/RenderSettings.cpp b/src/renderer/base/RenderSettings.cpp
index e889adccbf7..fab1f7dc10e 100644
--- a/src/renderer/base/RenderSettings.cpp
+++ b/src/renderer/base/RenderSettings.cpp
@@ -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:
@@ -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:
diff --git a/src/renderer/base/renderer.cpp b/src/renderer/base/renderer.cpp
index 73991da11e9..b75b55de0ba 100644
--- a/src/renderer/base/renderer.cpp
+++ b/src/renderer/base/renderer.cpp
@@ -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)
     {
@@ -314,6 +315,11 @@ void Renderer::TriggerRedrawAll(const bool backgroundChanged)
     {
         _pfnBackgroundColorChanged();
     }
+
+    if (frameChanged && _pfnFrameColorChanged)
+    {
+        _pfnFrameColorChanged();
+    }
 }
 
 // Method Description:
@@ -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
diff --git a/src/renderer/base/renderer.hpp b/src/renderer/base/renderer.hpp
index ff9bcebd943..009f3a954f9 100644
--- a/src/renderer/base/renderer.hpp
+++ b/src/renderer/base/renderer.hpp
@@ -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();
@@ -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();
 
@@ -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;
diff --git a/src/terminal/adapter/DispatchTypes.hpp b/src/terminal/adapter/DispatchTypes.hpp
index dd35c858ad4..8a6e14d1864 100644
--- a/src/terminal/adapter/DispatchTypes.hpp
+++ b/src/terminal/adapter/DispatchTypes.hpp
@@ -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,
diff --git a/src/terminal/adapter/ITermDispatch.hpp b/src/terminal/adapter/ITermDispatch.hpp
index 0433d9662d7..f2dcb1c2a8d 100644
--- a/src/terminal/adapter/ITermDispatch.hpp
+++ b/src/terminal/adapter/ITermDispatch.hpp
@@ -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
diff --git a/src/terminal/adapter/adaptDispatch.cpp b/src/terminal/adapter/adaptDispatch.cpp
index 2dca06ebe4b..bce951e5938 100644
--- a/src/terminal/adapter/adaptDispatch.cpp
+++ b/src/terminal/adapter/adaptDispatch.cpp
@@ -2241,9 +2241,13 @@ bool AdaptDispatch::SetColorTableEntry(const size_t tableIndex, const DWORD dwCo
     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);
+
     // Update the screen colors if we're not a pty
     // No need to force a redraw in pty mode.
-    _renderer.TriggerRedrawAll(backgroundChanged);
+    _renderer.TriggerRedrawAll(backgroundChanged, frameChanged);
     return true;
 }
 
@@ -2271,6 +2275,42 @@ bool AdaptDispatch::SetDefaultBackground(const DWORD dwColor)
     return SetColorTableEntry(TextColor::DEFAULT_BACKGROUND, dwColor);
 }
 
+// Method Description:
+// DECAC - Assigns the foreground and background color indexes that should be
+//   used for a given aspect of the user interface.
+// Arguments:
+// - item: The aspect of the interface that will have its colors altered.
+// - fgIndex: The color table index to be used for the foreground.
+// - bgIndex: The color table index to be used for the background.
+// Return Value:
+// True if handled successfully. False otherwise.
+bool AdaptDispatch::AssignColor(const DispatchTypes::ColorItem item, const VTInt fgIndex, const VTInt bgIndex)
+{
+    switch (item)
+    {
+    case DispatchTypes::ColorItem::NormalText:
+        _renderSettings.SetColorAliasIndex(ColorAlias::DefaultForeground, fgIndex);
+        _renderSettings.SetColorAliasIndex(ColorAlias::DefaultBackground, bgIndex);
+        break;
+    case DispatchTypes::ColorItem::WindowFrame:
+        _renderSettings.SetColorAliasIndex(ColorAlias::FrameForeground, fgIndex);
+        _renderSettings.SetColorAliasIndex(ColorAlias::FrameBackground, bgIndex);
+        break;
+    default:
+        return false;
+    }
+
+    // No need to force a redraw in pty mode.
+    const auto inPtyMode = _api.IsConsolePty();
+    if (!inPtyMode)
+    {
+        const auto backgroundChanged = item == DispatchTypes::ColorItem::NormalText;
+        const auto frameChanged = item == DispatchTypes::ColorItem::WindowFrame;
+        _renderer.TriggerRedrawAll(backgroundChanged, frameChanged);
+    }
+    return !inPtyMode;
+}
+
 //Routine Description:
 // Window Manipulation - Performs a variety of actions relating to the window,
 //      such as moving the window position, resizing the window, querying
diff --git a/src/terminal/adapter/adaptDispatch.hpp b/src/terminal/adapter/adaptDispatch.hpp
index 722645dc1a5..e132444182c 100644
--- a/src/terminal/adapter/adaptDispatch.hpp
+++ b/src/terminal/adapter/adaptDispatch.hpp
@@ -119,6 +119,7 @@ namespace Microsoft::Console::VirtualTerminal
                                 const DWORD color) override; // OSCColorTable
         bool SetDefaultForeground(const DWORD color) override; // OSCDefaultForeground
         bool SetDefaultBackground(const DWORD color) override; // OSCDefaultBackground
+        bool AssignColor(const DispatchTypes::ColorItem item, const VTInt fgIndex, const VTInt bgIndex) override; // DECAC
 
         bool WindowManipulation(const DispatchTypes::WindowManipulationType function,
                                 const VTParameter parameter1,
diff --git a/src/terminal/adapter/termDispatch.hpp b/src/terminal/adapter/termDispatch.hpp
index 386d1ced717..ad724641160 100644
--- a/src/terminal/adapter/termDispatch.hpp
+++ b/src/terminal/adapter/termDispatch.hpp
@@ -78,6 +78,7 @@ class Microsoft::Console::VirtualTerminal::TermDispatch : public Microsoft::Cons
     bool SetColorTableEntry(const size_t /*tableIndex*/, const DWORD /*color*/) override { return false; } // OSCColorTable
     bool SetDefaultForeground(const DWORD /*color*/) override { return false; } // OSCDefaultForeground
     bool SetDefaultBackground(const DWORD /*color*/) override { return false; } // OSCDefaultBackground
+    bool AssignColor(const DispatchTypes::ColorItem /*item*/, const VTInt /*fgIndex*/, const VTInt /*bgIndex*/) override { return false; } // DECAC
 
     bool EraseInDisplay(const DispatchTypes::EraseType /* eraseType*/) override { return false; } // ED
     bool EraseInLine(const DispatchTypes::EraseType /* eraseType*/) override { return false; } // EL
diff --git a/src/terminal/parser/OutputStateMachineEngine.cpp b/src/terminal/parser/OutputStateMachineEngine.cpp
index 864aa8555b5..244e7c64979 100644
--- a/src/terminal/parser/OutputStateMachineEngine.cpp
+++ b/src/terminal/parser/OutputStateMachineEngine.cpp
@@ -606,19 +606,20 @@ bool OutputStateMachineEngine::ActionCsiDispatch(const VTID id, const VTParamete
         success = _dispatch->SoftReset();
         TermTelemetry::Instance().Log(TermTelemetry::Codes::DECSTR);
         break;
-
     case CsiActionCodes::XT_PushSgr:
     case CsiActionCodes::XT_PushSgrAlias:
         success = _dispatch->PushGraphicsRendition(parameters);
         TermTelemetry::Instance().Log(TermTelemetry::Codes::XTPUSHSGR);
         break;
-
     case CsiActionCodes::XT_PopSgr:
     case CsiActionCodes::XT_PopSgrAlias:
         success = _dispatch->PopGraphicsRendition();
         TermTelemetry::Instance().Log(TermTelemetry::Codes::XTPOPSGR);
         break;
-
+    case CsiActionCodes::DECAC_AssignColor:
+        success = _dispatch->AssignColor(parameters.at(0), parameters.at(1).value_or(0), parameters.at(2).value_or(0));
+        TermTelemetry::Instance().Log(TermTelemetry::Codes::DECAC);
+        break;
     default:
         // If no functions to call, overall dispatch was a failure.
         success = false;
diff --git a/src/terminal/parser/OutputStateMachineEngine.hpp b/src/terminal/parser/OutputStateMachineEngine.hpp
index 8f6305bdc7c..20083b474d8 100644
--- a/src/terminal/parser/OutputStateMachineEngine.hpp
+++ b/src/terminal/parser/OutputStateMachineEngine.hpp
@@ -142,6 +142,7 @@ namespace Microsoft::Console::VirtualTerminal
             XT_PushSgr = VTID("#{"),
             XT_PopSgr = VTID("#}"),
             DECSCPP_SetColumnsPerPage = VTID("$|"),
+            DECAC_AssignColor = VTID(",|")
         };
 
         enum DcsActionCodes : uint64_t
diff --git a/src/terminal/parser/telemetry.cpp b/src/terminal/parser/telemetry.cpp
index 831a474f1c1..a40711ef506 100644
--- a/src/terminal/parser/telemetry.cpp
+++ b/src/terminal/parser/telemetry.cpp
@@ -280,6 +280,7 @@ void TermTelemetry::WriteFinalTraceLog() const
                                       TraceLoggingUInt32(_uiTimesUsed[DECALN], "DECALN"),
                                       TraceLoggingUInt32(_uiTimesUsed[XTPUSHSGR], "XTPUSHSGR"),
                                       TraceLoggingUInt32(_uiTimesUsed[XTPOPSGR], "XTPOPSGR"),
+                                      TraceLoggingUInt32(_uiTimesUsed[DECAC], "DECAC"),
                                       TraceLoggingUInt32Array(_uiTimesFailed, ARRAYSIZE(_uiTimesFailed), "Failed"),
                                       TraceLoggingUInt32(_uiTimesFailedOutsideRange, "FailedOutsideRange"));
         }
diff --git a/src/terminal/parser/telemetry.hpp b/src/terminal/parser/telemetry.hpp
index 7dd58353ccf..196d13016bd 100644
--- a/src/terminal/parser/telemetry.hpp
+++ b/src/terminal/parser/telemetry.hpp
@@ -107,6 +107,7 @@ namespace Microsoft::Console::VirtualTerminal
             OSCSCB,
             XTPUSHSGR,
             XTPOPSGR,
+            DECAC,
             // Only use this last enum as a count of the number of codes.
             NUMBER_OF_CODES
         };


Test patch: