Repository: microsoft/terminal (language: cpp)

Issue:
Support resetting the colors via OSC 104, 110, 111...
<!-- 
🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨

I ACKNOWLEDGE THE FOLLOWING BEFORE PROCEEDING:
1. If I delete this entire template and go my own path, the core team may close my issue without further explanation or engagement.
2. If I list multiple bugs/concerns in this one issue, the core team may close my issue without further explanation or engagement.
3. If I write an issue that has many duplicates, the core team may close my issue without further explanation or engagement (and without necessarily spending time to find the exact duplicate ID number).
4. If I leave the title incomplete when filing the issue, the core team may close my issue without further explanation or engagement.
5. If I file something completely blank in the body, the core team may close my issue without further explanation or engagement.

All good? Then proceed!
-->

# Description of the new feature/enhancement

These are the counterparts of OSC 4, 10, 11 to reset the default color. (And continue the line with 117, 119 if [17, 19](https://github.com/microsoft/terminal/issues/3580#issuecomment-553975138) get implemented, etc.)

# Proposed technical implementation details (optional)

In VTE + GNOME Terminal we figured out the best is if the OSC 4, 10, 11 sequences have precedence over the UI / config file settings. That is, for each color slot, if its value is defined via OSC 4, 10, 11 then that one is used and the one in the settings is ignored. If a value hasn't been defined via OSC 4, 10, 11, or has been reset via OSC 104, 110, 111 then the value specified in the terminal emulator's settings is used. This way re-applying the same settings is an idempotent operation. (Previously the two sources were fighting with each other, both overwriting the value in the same slot. That way re-applying the user config (e.g. "altering" a color to the same value) would invalidate a previous OSC, which was bad.)



Discussion / hints:
I've just bounced off this. I'm using a script with OSC 10 and 11 to switch from Solarized Dark to Solarized Light in Administrator PowerShell sessions. However, editing my config then causes all such sessions to have their colours reset, except for a bunch of stuff that seems to keep the colour scheme from the time it was written (under PowerShell, I guess this is PS-Readline at work).

I'm particularly interested in the config layering behaviour, but without OSC 104/110/111, being unable to apply config changes is probably a UX regression.

Are there any particular concerns with the proposed behaviour, e.g. standarisation/commonality? Or is it just a case of implementing the relevant code in this repo?

Or (based on the milestone being a Windows release) does it actually require MS-internal work first to support resetting the colours?
Just speaking for myself, this is quite low down my priority list, because I think it's going to be a bit of a pain. It needs to be implemented twice, because the color palette is one of those things that has to be handled separately by conhost and Windows Terminal. And on the conhost side there really isn't a concept of "default palette" to reset to, because the initial colors can come from a variety of sources (e.g. shortcut links and registry entries), and the source can change at run time.

I also have plans to refactor some of the palette management code, and I'm not keen to add any more palette functionality until that refactoring is out of the way. Not that the refactoring is necessarily a prerequisite for this feature, but I'd personally prefer to get that cleaned up first. 

So basically it seems like a lot of work for a fairly obscure feature, and one which I suspect few people will care about. But that's just my point of view. It's possible someone else may consider this more of a priority and decide to take it on sooner.
Makes sense. It's not in any way a blocker for me, so I'm happy to wait for any planned refactoring to land (and perhaps #942), before I start trying to pick-off low hanging fruit.

Fix patch:
diff --git a/src/renderer/base/RenderSettings.cpp b/src/renderer/base/RenderSettings.cpp
index 0410211143d..371b591b32e 100644
--- a/src/renderer/base/RenderSettings.cpp
+++ b/src/renderer/base/RenderSettings.cpp
@@ -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.
diff --git a/src/renderer/inc/RenderSettings.hpp b/src/renderer/inc/RenderSettings.hpp
index a9c370133c6..84443889bd7 100644
--- a/src/renderer/inc/RenderSettings.hpp
+++ b/src/renderer/inc/RenderSettings.hpp
@@ -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;
diff --git a/src/terminal/adapter/ITermDispatch.hpp b/src/terminal/adapter/ITermDispatch.hpp
index 1576571aa9c..da24f71b6fd 100644
--- a/src/terminal/adapter/ITermDispatch.hpp
+++ b/src/terminal/adapter/ITermDispatch.hpp
@@ -78,8 +78,11 @@ class Microsoft::Console::VirtualTerminal::ITermDispatch
     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
diff --git a/src/terminal/adapter/adaptDispatch.cpp b/src/terminal/adapter/adaptDispatch.cpp
index fe1b3c9a8af..9158b962ab3 100644
--- a/src/terminal/adapter/adaptDispatch.cpp
+++ b/src/terminal/adapter/adaptDispatch.cpp
@@ -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.
diff --git a/src/terminal/adapter/adaptDispatch.hpp b/src/terminal/adapter/adaptDispatch.hpp
index 2f63e782ca0..313611a7323 100644
--- a/src/terminal/adapter/adaptDispatch.hpp
+++ b/src/terminal/adapter/adaptDispatch.hpp
@@ -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,
diff --git a/src/terminal/adapter/termDispatch.hpp b/src/terminal/adapter/termDispatch.hpp
index f6dab368f82..0f3908331b0 100644
--- a/src/terminal/adapter/termDispatch.hpp
+++ b/src/terminal/adapter/termDispatch.hpp
@@ -71,8 +71,11 @@ class Microsoft::Console::VirtualTerminal::TermDispatch : public Microsoft::Cons
     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
diff --git a/src/terminal/parser/OutputStateMachineEngine.cpp b/src/terminal/parser/OutputStateMachineEngine.cpp
index c8a1367a260..4febc78ee84 100644
--- a/src/terminal/parser/OutputStateMachineEngine.cpp
+++ b/src/terminal/parser/OutputStateMachineEngine.cpp
@@ -810,10 +810,40 @@ bool OutputStateMachineEngine::ActionOscDispatch(const size_t parameter, const s
         }
         break;
     }
+    case OscActionCodes::ResetColor:
+    {
+        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:
     {
-        // The reset codes for xterm dynamic resources are the set codes + 100
-        _dispatch->SetXtermColorResource(parameter - 100u, INVALID_COLOR);
+        // 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:
diff --git a/src/terminal/parser/OutputStateMachineEngine.hpp b/src/terminal/parser/OutputStateMachineEngine.hpp
index ab41e066cfa..d36789e1085 100644
--- a/src/terminal/parser/OutputStateMachineEngine.hpp
+++ b/src/terminal/parser/OutputStateMachineEngine.hpp
@@ -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,
diff --git a/src/terminal/parser/ut_parser/OutputEngineTest.cpp b/src/terminal/parser/ut_parser/OutputEngineTest.cpp
index 416ed6681d9..f12dac966cf 100644
--- a/src/terminal/parser/ut_parser/OutputEngineTest.cpp
+++ b/src/terminal/parser/ut_parser/OutputEngineTest.cpp
@@ -1163,7 +1163,8 @@ class StatefulDispatch final : public TermDispatch
         _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 @@ class StatefulDispatch final : public TermDispatch
         _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 @@ class StatefulDispatch final : public TermDispatch
         _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 @@ class StatefulDispatch final : public TermDispatch
     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()


Test patch: