Repository: microsoft/terminal (language: cpp)

Issue:
Add support for the VT macro operations
# Description of the new feature/enhancement

There are a couple of operations that were introduced on the VT420 that allow an application to define a sequence of a characters as a macro ([`DECDMAC`](https://vt100.net/docs/vt510-rm/DECDMAC.html)), and then later invoke that macro to execute the content as if it had just been received from the host ([`DECINVM`](https://vt100.net/docs/vt510-rm/DECINVM.html)).

This can be a useful optimization in gaming if you need to define a sprite made up of a number of characters, spanning multiple rows, and using multiple colors. If you can upload the sequences for drawing the sprite as a macro, then any time you need to render it, you just set the cursor position and invoke it.

There is a similar use case for sound effects that need to be played more than once. Instead of having to upload a long set of `DECPS` sequences every time you play the sound, you just invoke a macro which has been predefined with the necessary sequences.

# Proposed technical implementation details (optional)

The one complication is how we get the macro content injected into the output stream when it's invoked. The way I'm handling that in my current POC is with a kind of callback function that can be registered with the state machine, and which gets executed (if set) once we return to the ground state from a `CSI` action.

So in the dispatch handler for `DECINVM`, we register a lambda callback with the state machine which then injects the additional macro content with a `ProcessString` call once the state machine has returned to ground. It's a little more complicated than that, because the `ProcessString` implementation isn't reentrant, so we also need to save some state, but that's the basic gist of it.

I don't know if there's a nicer way of handling this, but that's my current plan if you're happy to accept a PR for this.


Discussion / hints:

Fix patch:
diff --git a/.github/actions/spelling/expect/expect.txt b/.github/actions/spelling/expect/expect.txt
index 8f8f4828d0a..909d4df0481 100644
--- a/.github/actions/spelling/expect/expect.txt
+++ b/.github/actions/spelling/expect/expect.txt
@@ -410,20 +410,24 @@ DECAWM
 DECBKM
 DECCARA
 DECCKM
+DECCKSR
 DECCOLM
 DECCRA
 DECCTR
 DECDHL
 decdld
+DECDMAC
 DECDWL
 DECEKBD
 DECERA
 DECFRA
 DECID
+DECINVM
 DECKPAM
 DECKPM
 DECKPNM
 DECLRMM
+DECMSR
 DECNKM
 DECNRCM
 DECOM
@@ -2288,6 +2292,7 @@ YOffset
 YSubstantial
 YVIRTUALSCREEN
 YWalk
+zabcd
 Zabcdefghijklmnopqrstuvwxyz
 ZCmd
 ZCtrl
diff --git a/src/terminal/adapter/DispatchTypes.hpp b/src/terminal/adapter/DispatchTypes.hpp
index 44f3de5dbb3..eaa437b5320 100644
--- a/src/terminal/adapter/DispatchTypes.hpp
+++ b/src/terminal/adapter/DispatchTypes.hpp
@@ -386,6 +386,8 @@ namespace Microsoft::Console::VirtualTerminal::DispatchTypes
         OS_OperatingStatus = ANSIStandardStatus(5),
         CPR_CursorPositionReport = ANSIStandardStatus(6),
         ExCPR_ExtendedCursorPositionReport = DECPrivateStatus(6),
+        MSR_MacroSpaceReport = DECPrivateStatus(62),
+        MEM_MemoryChecksum = DECPrivateStatus(63),
     };
 
     using ANSIStandardMode = FlaggedEnumValue<0x00000000>;
@@ -507,6 +509,18 @@ namespace Microsoft::Console::VirtualTerminal::DispatchTypes
         Size96 = 1
     };
 
+    enum class MacroDeleteControl : VTInt
+    {
+        DeleteId = 0,
+        DeleteAll = 1
+    };
+
+    enum class MacroEncoding : VTInt
+    {
+        Text = 0,
+        HexPair = 1
+    };
+
     enum class ReportFormat : VTInt
     {
         TerminalStateReport = 1,
diff --git a/src/terminal/adapter/ITermDispatch.hpp b/src/terminal/adapter/ITermDispatch.hpp
index eab7c1b4000..eee04779643 100644
--- a/src/terminal/adapter/ITermDispatch.hpp
+++ b/src/terminal/adapter/ITermDispatch.hpp
@@ -112,7 +112,7 @@ class Microsoft::Console::VirtualTerminal::ITermDispatch
 
     virtual bool ResetMode(const DispatchTypes::ModeParams param) = 0; // DECRST
 
-    virtual bool DeviceStatusReport(const DispatchTypes::StatusType statusType) = 0; // DSR, DSR-OS, DSR-CPR
+    virtual bool DeviceStatusReport(const DispatchTypes::StatusType statusType, const VTParameter id) = 0; // DSR
     virtual bool DeviceAttributes() = 0; // DA1
     virtual bool SecondaryDeviceAttributes() = 0; // DA2
     virtual bool TertiaryDeviceAttributes() = 0; // DA3
@@ -159,6 +159,11 @@ class Microsoft::Console::VirtualTerminal::ITermDispatch
                                        const VTParameter cellHeight,
                                        const DispatchTypes::DrcsCharsetSize charsetSize) = 0; // DECDLD
 
+    virtual StringHandler DefineMacro(const VTInt macroId,
+                                      const DispatchTypes::MacroDeleteControl deleteControl,
+                                      const DispatchTypes::MacroEncoding encoding) = 0; // DECDMAC
+    virtual bool InvokeMacro(const VTInt macroId) = 0; // DECINVM
+
     virtual StringHandler RestoreTerminalState(const DispatchTypes::ReportFormat format) = 0; // DECRSTS
 
     virtual StringHandler RequestSetting() = 0; // DECRQSS
diff --git a/src/terminal/adapter/MacroBuffer.cpp b/src/terminal/adapter/MacroBuffer.cpp
new file mode 100644
index 00000000000..7ae1f05e5b7
--- /dev/null
+++ b/src/terminal/adapter/MacroBuffer.cpp
@@ -0,0 +1,270 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+#include "precomp.h"
+#include "MacroBuffer.hpp"
+#include "../parser/ascii.hpp"
+#include "../parser/stateMachine.hpp"
+
+using namespace Microsoft::Console::VirtualTerminal;
+
+size_t MacroBuffer::GetSpaceAvailable() const noexcept
+{
+    return MAX_SPACE - _spaceUsed;
+}
+
+uint16_t MacroBuffer::CalculateChecksum() const noexcept
+{
+    // The algorithm that we're using here is intended to match the checksums
+    // produced by the original DEC VT420 terminal. Although note that a real
+    // VT420 would have included the entire macro memory area in the checksum,
+    // which could still contain remnants of previous macro definitions that
+    // are no longer active. We don't replicate that behavior, since that's of
+    // no benefit to applications that might want to use the checksum.
+    uint16_t checksum = 0;
+    for (auto& macro : _macros)
+    {
+        for (auto ch : macro)
+        {
+            checksum -= ch;
+        }
+    }
+    return checksum;
+}
+
+void MacroBuffer::InvokeMacro(const size_t macroId, StateMachine& stateMachine)
+{
+    if (macroId < _macros.size())
+    {
+        const auto& macroSequence = til::at(_macros, macroId);
+        // Macros can invoke other macros up to a depth of 16, but we don't allow
+        // the total sequence length to exceed the maximum buffer size, since that's
+        // likely to facilitate a denial-of-service attack.
+        const auto allowedLength = MAX_SPACE - _invokedSequenceLength;
+        if (_invokedDepth < 16 && macroSequence.length() < allowedLength)
+        {
+            _invokedSequenceLength += macroSequence.length();
+            _invokedDepth++;
+            auto resetInvokeDepth = wil::scope_exit([&] {
+                // Once the invoke depth reaches zero, we know we've reached the end
+                // of the root invoke, so we can reset the sequence length tracker.
+                if (--_invokedDepth == 0)
+                {
+                    _invokedSequenceLength = 0;
+                }
+            });
+            stateMachine.ProcessString(macroSequence);
+        }
+    }
+}
+
+void MacroBuffer::ClearMacrosIfInUse()
+{
+    // If we receive an RIS from within a macro invocation, we can't release the
+    // buffer because it's still being used. Instead we'll just replace all the
+    // macro definitions with NUL characters to prevent any further output. The
+    // buffer will eventually be released once the invocation finishes.
+    if (_invokedDepth > 0)
+    {
+        for (auto& macro : _macros)
+        {
+            std::fill(macro.begin(), macro.end(), AsciiChars::NUL);
+        }
+    }
+}
+
+bool MacroBuffer::InitParser(const size_t macroId, const DispatchTypes::MacroDeleteControl deleteControl, const DispatchTypes::MacroEncoding encoding)
+{
+    // We're checking the invoked depth here to make sure we aren't defining
+    // a macro from within a macro invocation.
+    if (macroId < _macros.size() && _invokedDepth == 0)
+    {
+        _activeMacroId = macroId;
+        _decodedChar = 0;
+        _repeatPending = false;
+
+        switch (encoding)
+        {
+        case DispatchTypes::MacroEncoding::HexPair:
+            _parseState = State::ExpectingHexDigit;
+            break;
+        case DispatchTypes::MacroEncoding::Text:
+            _parseState = State::ExpectingText;
+            break;
+        default:
+            return false;
+        }
+
+        switch (deleteControl)
+        {
+        case DispatchTypes::MacroDeleteControl::DeleteId:
+            _deleteMacro(_activeMacro());
+            return true;
+        case DispatchTypes::MacroDeleteControl::DeleteAll:
+            for (auto& macro : _macros)
+            {
+                _deleteMacro(macro);
+            }
+            return true;
+        default:
+            return false;
+        }
+    }
+    return false;
+}
+
+bool MacroBuffer::ParseDefinition(const wchar_t ch)
+{
+    // Once we receive an ESC, that marks the end of the definition, but if
+    // an unterminated repeat is still pending, we should apply that now.
+    if (ch == AsciiChars::ESC)
+    {
+        if (_repeatPending && !_applyPendingRepeat())
+        {
+            _deleteMacro(_activeMacro());
+        }
+        return false;
+    }
+
+    // Any other control characters are just ignored.
+    if (ch < L' ')
+    {
+        return true;
+    }
+
+    // For "text encoded" macros, we'll always be in the ExpectingText state.
+    // For "hex encoded" macros, we'll typically be alternating between the
+    // ExpectingHexDigit and ExpectingSecondHexDigit states as we parse the two
+    // digits of each hex pair. But we also need to deal with repeat sequences,
+    // which start with `!`, followed by a numeric repeat count, and then a
+    // range of hex pairs between two `;` characters. When parsing the repeat
+    // count, we use the ExpectingRepeatCount state, but when parsing the hex
+    // pairs of the repeat, we just use the regular ExpectingHexDigit states.
+
+    auto success = true;
+    switch (_parseState)
+    {
+    case State::ExpectingText:
+        success = _appendToActiveMacro(ch);
+        break;
+    case State::ExpectingHexDigit:
+        if (_decodeHexDigit(ch))
+        {
+            _parseState = State::ExpectingSecondHexDigit;
+        }
+        else if (ch == L'!' && !_repeatPending)
+        {
+            _parseState = State::ExpectingRepeatCount;
+            _repeatCount = 0;
+        }
+        else if (ch == L';' && _repeatPending)
+        {
+            success = _applyPendingRepeat();
+        }
+        else
+        {
+            success = false;
+        }
+        break;
+    case State::ExpectingSecondHexDigit:
+        success = _decodeHexDigit(ch) && _appendToActiveMacro(_decodedChar);
+        _decodedChar = 0;
+        _parseState = State::ExpectingHexDigit;
+        break;
+    case State::ExpectingRepeatCount:
+        if (ch >= L'0' && ch <= L'9')
+        {
+            _repeatCount = _repeatCount * 10 + (ch - L'0');
+            _repeatCount = std::min<size_t>(_repeatCount, MAX_PARAMETER_VALUE);
+        }
+        else if (ch == L';')
+        {
+            _repeatPending = true;
+            _repeatStart = _activeMacro().length();
+            _parseState = State::ExpectingHexDigit;
+        }
+        else
+        {
+            success = false;
+        }
+        break;
+    default:
+        success = false;
+        break;
+    }
+
+    // If there is an error in the definition, clear everything received so far.
+    if (!success)
+    {
+        _deleteMacro(_activeMacro());
+    }
+    return success;
+}
+
+bool MacroBuffer::_decodeHexDigit(const wchar_t ch) noexcept
+{
+    _decodedChar <<= 4;
+    if (ch >= L'0' && ch <= L'9')
+    {
+        _decodedChar += (ch - L'0');
+        return true;
+    }
+    else if (ch >= L'A' && ch <= L'F')
+    {
+        _decodedChar += (ch - L'A' + 10);
+        return true;
+    }
+    else if (ch >= L'a' && ch <= L'f')
+    {
+        _decodedChar += (ch - L'a' + 10);
+        return true;
+    }
+    return false;
+}
+
+bool MacroBuffer::_appendToActiveMacro(const wchar_t ch)
+{
+    if (GetSpaceAvailable() > 0)
+    {
+        _activeMacro().push_back(ch);
+        _spaceUsed++;
+        return true;
+    }
+    return false;
+}
+
+std::wstring& MacroBuffer::_activeMacro()
+{
+    return _macros.at(_activeMacroId);
+}
+
+void MacroBuffer::_deleteMacro(std::wstring& macro) noexcept
+{
+    _spaceUsed -= macro.length();
+    std::wstring{}.swap(macro);
+}
+
+bool MacroBuffer::_applyPendingRepeat()
+{
+    if (_repeatCount > 1)
+    {
+        auto& activeMacro = _activeMacro();
+        const auto sequenceLength = activeMacro.length() - _repeatStart;
+        // Note that the repeat sequence has already been written to the buffer
+        // once while it was being parsed, so we only need to append additional
+        // copies for repeat counts that are greater than one. If there is not
+        // enough space for the additional content, we'll just abort the macro.
+        const auto spaceRequired = (_repeatCount - 1) * sequenceLength;
+        if (spaceRequired > GetSpaceAvailable())
+        {
+            return false;
+        }
+        for (size_t i = 1; i < _repeatCount; i++)
+        {
+            activeMacro.append(activeMacro.substr(_repeatStart, sequenceLength));
+            _spaceUsed += sequenceLength;
+        }
+    }
+    _repeatPending = false;
+    return true;
+}
diff --git a/src/terminal/adapter/MacroBuffer.hpp b/src/terminal/adapter/MacroBuffer.hpp
new file mode 100644
index 00000000000..92c1c07c73f
--- /dev/null
+++ b/src/terminal/adapter/MacroBuffer.hpp
@@ -0,0 +1,79 @@
+/*++
+Copyright (c) Microsoft Corporation
+Licensed under the MIT license.
+
+Module Name:
+- MacrosBuffer.hpp
+
+Abstract:
+- This manages the parsing and storage of macros defined by the DECDMAC control sequence.
+--*/
+
+#pragma once
+
+#include "DispatchTypes.hpp"
+#include <array>
+#include <bitset>
+#include <string>
+
+// fwdecl unittest classes
+#ifdef UNIT_TESTING
+class AdapterTest;
+#endif
+
+namespace Microsoft::Console::VirtualTerminal
+{
+    class StateMachine;
+
+    class MacroBuffer
+    {
+    public:
+        // The original DEC terminals only supported 6K of memory, which is
+        // probably a bit low for modern usage. But we also don't want to make
+        // this value too large, otherwise it could be used in a denial-of-
+        // service attack. So for now this is probably a sufficient limit, but
+        // we may need to increase it in the future if we intend to support
+        // macros containing sixel sequences.
+        static constexpr size_t MAX_SPACE = 0x40000;
+
+        MacroBuffer() = default;
+        ~MacroBuffer() = default;
+
+        size_t GetSpaceAvailable() const noexcept;
+        uint16_t CalculateChecksum() const noexcept;
+        void InvokeMacro(const size_t macroId, StateMachine& stateMachine);
+        void ClearMacrosIfInUse();
+        bool InitParser(const size_t macroId, const DispatchTypes::MacroDeleteControl deleteControl, const DispatchTypes::MacroEncoding encoding);
+        bool ParseDefinition(const wchar_t ch);
+
+    private:
+        bool _decodeHexDigit(const wchar_t ch) noexcept;
+        bool _appendToActiveMacro(const wchar_t ch);
+        std::wstring& _activeMacro();
+        void _deleteMacro(std::wstring& macro) noexcept;
+        bool _applyPendingRepeat();
+
+        enum class State
+        {
+            ExpectingText,
+            ExpectingHexDigit,
+            ExpectingSecondHexDigit,
+            ExpectingRepeatCount
+        };
+
+        State _parseState{ State::ExpectingText };
+        wchar_t _decodedChar{ 0 };
+        bool _repeatPending{ false };
+        size_t _repeatCount{ 0 };
+        size_t _repeatStart{ 0 };
+        std::array<std::wstring, 64> _macros;
+        size_t _activeMacroId{ 0 };
+        size_t _spaceUsed{ 0 };
+        size_t _invokedDepth{ 0 };
+        size_t _invokedSequenceLength{ 0 };
+
+#ifdef UNIT_TESTING
+        friend class AdapterTest;
+#endif
+    };
+}
diff --git a/src/terminal/adapter/adaptDispatch.cpp b/src/terminal/adapter/adaptDispatch.cpp
index daf1517553f..31eb418c324 100644
--- a/src/terminal/adapter/adaptDispatch.cpp
+++ b/src/terminal/adapter/adaptDispatch.cpp
@@ -1166,9 +1166,10 @@ bool AdaptDispatch::SetLineRendition(const LineRendition rendition)
 // - DSR - Reports status of a console property back to the STDIN based on the type of status requested.
 // Arguments:
 // - statusType - status type indicating what property we should report back
+// - id - a numeric label used to identify the request in DECCKSR reports
 // Return Value:
 // - True if handled successfully. False otherwise.
-bool AdaptDispatch::DeviceStatusReport(const DispatchTypes::StatusType statusType)
+bool AdaptDispatch::DeviceStatusReport(const DispatchTypes::StatusType statusType, const VTParameter id)
 {
     switch (statusType)
     {
@@ -1181,6 +1182,12 @@ bool AdaptDispatch::DeviceStatusReport(const DispatchTypes::StatusType statusTyp
     case DispatchTypes::StatusType::ExCPR_ExtendedCursorPositionReport:
         _CursorPositionReport(true);
         return true;
+    case DispatchTypes::StatusType::MSR_MacroSpaceReport:
+        _MacroSpaceReport();
+        return true;
+    case DispatchTypes::StatusType::MEM_MemoryChecksum:
+        _MacroChecksumReport(id);
+        return true;
     default:
         return false;
     }
@@ -1336,6 +1343,34 @@ void AdaptDispatch::_CursorPositionReport(const bool extendedReport)
     }
 }
 
+// Routine Description:
+// - DECMSR - Reports the amount of space available for macro definitions.
+// Arguments:
+// - <none>
+// Return Value:
+// - <none>
+void AdaptDispatch::_MacroSpaceReport() const
+{
+    const auto spaceInBytes = _macroBuffer ? _macroBuffer->GetSpaceAvailable() : MacroBuffer::MAX_SPACE;
+    // The available space is measured in blocks of 16 bytes, so we need to divide by 16.
+    const auto response = wil::str_printf<std::wstring>(L"\x1b[%zu*{", spaceInBytes / 16);
+    _api.ReturnResponse(response);
+}
+
+// Routine Description:
+// - DECCKSR - Reports a checksum of the current macro definitions.
+// Arguments:
+// - id - a numeric label used to identify the DSR request
+// Return Value:
+// - <none>
+void AdaptDispatch::_MacroChecksumReport(const VTParameter id) const
+{
+    const auto requestId = id.value_or(0);
+    const auto checksum = _macroBuffer ? _macroBuffer->CalculateChecksum() : 0;
+    const auto response = wil::str_printf<std::wstring>(L"\033P%d!~%04X\033\\", requestId, checksum);
+    _api.ReturnResponse(response);
+}
+
 // Routine Description:
 // - Generalizes scrolling movement for up/down
 // Arguments:
@@ -2358,6 +2393,13 @@ bool AdaptDispatch::HardReset()
     // Reset the attribute change extent (DECSACE) to a stream.
     _isChangeExtentRectangular = false;
 
+    // Clear and release the macro buffer.
+    if (_macroBuffer)
+    {
+        _macroBuffer->ClearMacrosIfInUse();
+        _macroBuffer = nullptr;
+    }
+
     // GH#2715 - If all this succeeded, but we're in a conpty, return `false` to
     // make the state machine propagate this RIS sequence to the connected
     // terminal application. We've reset our state, but the connected terminal
@@ -3177,6 +3219,60 @@ ITermDispatch::StringHandler AdaptDispatch::_CreateDrcsPassthroughHandler(const
     return nullptr;
 }
 
+// Method Description:
+// - DECDMAC - Defines a string of characters as a macro that can later be
+//   invoked with a DECINVM sequence.
+// Arguments:
+// - macroId - a number to identify the macro when invoked.
+// - deleteControl - what gets deleted before loading the new macro data.
+// - encoding - whether the data is encoded as plain text or hex digits.
+// Return Value:
+// - a function to receive the macro data or nullptr if parameters are invalid.
+ITermDispatch::StringHandler AdaptDispatch::DefineMacro(const VTInt macroId,
+                                                        const DispatchTypes::MacroDeleteControl deleteControl,
+                                                        const DispatchTypes::MacroEncoding encoding)
+{
+    if (!_macroBuffer)
+    {
+        _macroBuffer = std::make_shared<MacroBuffer>();
+    }
+
+    if (_macroBuffer->InitParser(macroId, deleteControl, encoding))
+    {
+        return [&](const auto ch) {
+            return _macroBuffer->ParseDefinition(ch);
+        };
+    }
+
+    return nullptr;
+}
+
+// Method Description:
+// - DECINVM - Invokes a previously defined macro, executing the macro content
+//   as if it had been received directly from the host.
+// Arguments:
+// - macroId - the id number of the macro to be invoked.
+// Return Value:
+// - True
+bool AdaptDispatch::InvokeMacro(const VTInt macroId)
+{
+    if (_macroBuffer)
+    {
+        // In order to inject our macro sequence into the state machine
+        // we need to register a callback that will be executed only
+        // once it has finished processing the current operation, and
+        // has returned to the ground state. Note that we're capturing
+        // a copy of the _macroBuffer pointer here to make sure it won't
+        // be deleted (e.g. from an invoked RIS) while still in use.
+        const auto macroBuffer = _macroBuffer;
+        auto& stateMachine = _api.GetStateMachine();
+        stateMachine.OnCsiComplete([=, &stateMachine]() {
+            macroBuffer->InvokeMacro(macroId, stateMachine);
+        });
+    }
+    return true;
+}
+
 // Method Description:
 // - DECRSTS - Restores the terminal state from a stream of data previously
 //   saved with a DECRQTSR query.
diff --git a/src/terminal/adapter/adaptDispatch.hpp b/src/terminal/adapter/adaptDispatch.hpp
index aa233ce145d..13617aeb786 100644
--- a/src/terminal/adapter/adaptDispatch.hpp
+++ b/src/terminal/adapter/adaptDispatch.hpp
@@ -17,10 +17,16 @@ Author(s):
 #include "termDispatch.hpp"
 #include "ITerminalApi.hpp"
 #include "FontBuffer.hpp"
+#include "MacroBuffer.hpp"
 #include "terminalOutput.hpp"
 #include "../input/terminalInput.hpp"
 #include "../../types/inc/sgrStack.hpp"
 
+// fwdecl unittest classes
+#ifdef UNIT_TESTING
+class AdapterTest;
+#endif
+
 namespace Microsoft::Console::VirtualTerminal
 {
     class AdaptDispatch : public ITermDispatch
@@ -67,7 +73,7 @@ namespace Microsoft::Console::VirtualTerminal
         bool SetCharacterProtectionAttribute(const VTParameters options) override; // DECSCA
         bool PushGraphicsRendition(const VTParameters options) override; // XTPUSHSGR
         bool PopGraphicsRendition() override; // XTPOPSGR
-        bool DeviceStatusReport(const DispatchTypes::StatusType statusType) override; // DSR, DSR-OS, DSR-CPR
+        bool DeviceStatusReport(const DispatchTypes::StatusType statusType, const VTParameter id) override; // DSR
         bool DeviceAttributes() override; // DA1
         bool SecondaryDeviceAttributes() override; // DA2
         bool TertiaryDeviceAttributes() override; // DA3
@@ -153,6 +159,11 @@ namespace Microsoft::Console::VirtualTerminal
                                    const VTParameter cellHeight,
                                    const DispatchTypes::DrcsCharsetSize charsetSize) override; // DECDLD
 
+        StringHandler DefineMacro(const VTInt macroId,
+                                  const DispatchTypes::MacroDeleteControl deleteControl,
+                                  const DispatchTypes::MacroEncoding encoding) override; // DECDMAC
+        bool InvokeMacro(const VTInt macroId) override; // DECINVM
+
         StringHandler RestoreTerminalState(const DispatchTypes::ReportFormat format) override; // DECRSTS
 
         StringHandler RequestSetting() override; // DECRQSS
@@ -213,6 +224,8 @@ namespace Microsoft::Console::VirtualTerminal
                                              const VTInt bottomMargin);
         void _OperatingStatus() const;
         void _CursorPositionReport(const bool extendedReport);
+        void _MacroSpaceReport() const;
+        void _MacroChecksumReport(const VTParameter id) const;
 
         bool _GetParserMode(const StateMachine::Mode mode) const;
         void _SetParserMode(const StateMachine::Mode mode, const bool enable);
@@ -244,6 +257,7 @@ namespace Microsoft::Console::VirtualTerminal
         TerminalInput& _terminalInput;
         TerminalOutput _termOutput;
         std::unique_ptr<FontBuffer> _fontBuffer;
+        std::shared_ptr<MacroBuffer> _macroBuffer;
         std::optional<unsigned int> _initialCodePage;
 
         // We have two instances of the saved cursor state, because we need
@@ -270,5 +284,9 @@ namespace Microsoft::Console::VirtualTerminal
                                     TextAttribute& attr) noexcept;
         void _ApplyGraphicsOptions(const VTParameters options,
                                    TextAttribute& attr) noexcept;
+
+#ifdef UNIT_TESTING
+        friend class AdapterTest;
+#endif
     };
 }
diff --git a/src/terminal/adapter/lib/adapter.vcxproj b/src/terminal/adapter/lib/adapter.vcxproj
index 5bc8488f891..3ecefa5e838 100644
--- a/src/terminal/adapter/lib/adapter.vcxproj
+++ b/src/terminal/adapter/lib/adapter.vcxproj
@@ -14,6 +14,7 @@
     <ClCompile Include="..\adaptDispatch.cpp" />
     <ClCompile Include="..\FontBuffer.cpp" />
     <ClCompile Include="..\InteractDispatch.cpp" />
+    <ClCompile Include="..\MacroBuffer.cpp" />
     <ClCompile Include="..\adaptDispatchGraphics.cpp" />
     <ClCompile Include="..\telemetry.cpp" />
     <ClCompile Include="..\terminalOutput.cpp" />
@@ -29,6 +30,7 @@
     <ClInclude Include="..\FontBuffer.hpp" />
     <ClInclude Include="..\InteractDispatch.hpp" />
     <ClInclude Include="..\ITerminalApi.hpp" />
+    <ClInclude Include="..\MacroBuffer.hpp" />
     <ClInclude Include="..\precomp.h" />
     <ClInclude Include="..\telemetry.hpp" />
     <ClInclude Include="..\terminalOutput.hpp" />
diff --git a/src/terminal/adapter/lib/adapter.vcxproj.filters b/src/terminal/adapter/lib/adapter.vcxproj.filters
index 10a5aa55ff7..21524f61cd6 100644
--- a/src/terminal/adapter/lib/adapter.vcxproj.filters
+++ b/src/terminal/adapter/lib/adapter.vcxproj.filters
@@ -39,6 +39,9 @@
     <ClCompile Include="..\FontBuffer.cpp">
       <Filter>Source Files</Filter>
     </ClCompile>
+    <ClCompile Include="..\MacroBuffer.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
   </ItemGroup>
   <ItemGroup>
     <ClInclude Include="..\adaptDispatch.hpp">
@@ -80,6 +83,9 @@
     <ClInclude Include="..\FontBuffer.hpp">
       <Filter>Header Files</Filter>
     </ClInclude>
+    <ClInclude Include="..\MacroBuffer.hpp">
+      <Filter>Header Files</Filter>
+    </ClInclude>
   </ItemGroup>
   <ItemGroup>
     <Natvis Include="$(SolutionDir)tools\ConsoleTypes.natvis" />
diff --git a/src/terminal/adapter/sources.inc b/src/terminal/adapter/sources.inc
index 87c5507685a..d1e63430bb4 100644
--- a/src/terminal/adapter/sources.inc
+++ b/src/terminal/adapter/sources.inc
@@ -33,6 +33,7 @@ SOURCES= \
     ..\adaptDispatch.cpp \
     ..\FontBuffer.cpp \
     ..\InteractDispatch.cpp \
+    ..\MacroBuffer.cpp \
     ..\adaptDispatchGraphics.cpp \
     ..\terminalOutput.cpp \
     ..\telemetry.cpp \
diff --git a/src/terminal/adapter/termDispatch.hpp b/src/terminal/adapter/termDispatch.hpp
index 683bd14e621..6ee2f413e82 100644
--- a/src/terminal/adapter/termDispatch.hpp
+++ b/src/terminal/adapter/termDispatch.hpp
@@ -105,7 +105,7 @@ class Microsoft::Console::VirtualTerminal::TermDispatch : public Microsoft::Cons
 
     bool ResetMode(const DispatchTypes::ModeParams /*param*/) override { return false; } // DECRST
 
-    bool DeviceStatusReport(const DispatchTypes::StatusType /*statusType*/) override { return false; } // DSR, DSR-OS, DSR-CPR
+    bool DeviceStatusReport(const DispatchTypes::StatusType /*statusType*/, const VTParameter /*id*/) override { return false; } // DSR
     bool DeviceAttributes() override { return false; } // DA1
     bool SecondaryDeviceAttributes() override { return false; } // DA2
     bool TertiaryDeviceAttributes() override { return false; } // DA3
@@ -152,6 +152,11 @@ class Microsoft::Console::VirtualTerminal::TermDispatch : public Microsoft::Cons
                                const VTParameter /*cellHeight*/,
                                const DispatchTypes::DrcsCharsetSize /*charsetSize*/) override { return nullptr; } // DECDLD
 
+    StringHandler DefineMacro(const VTInt /*macroId*/,
+                              const DispatchTypes::MacroDeleteControl /*deleteControl*/,
+                              const DispatchTypes::MacroEncoding /*encoding*/) override { return nullptr; } // DECDMAC
+    bool InvokeMacro(const VTInt /*macroId*/) override { return false; } // DECINVM
+
     StringHandler RestoreTerminalState(const DispatchTypes::ReportFormat /*format*/) override { return nullptr; }; // DECRSTS
 
     StringHandler RequestSetting() override { return nullptr; }; // DECRQSS
diff --git a/src/terminal/adapter/ut_adapter/adapterTest.cpp b/src/terminal/adapter/ut_adapter/adapterTest.cpp
index 74e1381328c..6bcefd3effb 100644
--- a/src/terminal/adapter/ut_adapter/adapterTest.cpp
+++ b/src/terminal/adapter/ut_adapter/adapterTest.cpp
@@ -60,8 +60,9 @@ using namespace Microsoft::Console::VirtualTerminal;
 class TestGetSet final : public ITerminalApi
 {
 public:
-    void PrintString(const std::wstring_view /*string*/) override
+    void PrintString(const std::wstring_view string) override
     {
+        _printed += string;
     }
 
     void ReturnResponse(const std::wstring_view response) override
@@ -291,6 +292,8 @@ class TestGetSet final : public ITerminalApi
 
         _response.clear();
         _retainResponse = false;
+
+        _printed.clear();
     }
 
     void PrepCursor(CursorX xact, CursorY yact)
@@ -382,6 +385,8 @@ class TestGetSet final : public ITerminalApi
     til::inclusive_rect _expectedScrollRegion;
     til::inclusive_rect _activeScrollRegion;
 
+    std::wstring _printed;
+
     til::point _expectedCursorPos;
 
     TextAttribute _expectedAttribute = {};
@@ -1358,7 +1363,7 @@ class AdapterTest
 
         Log::Comment(L"Test 1: Verify failure when using bad status.");
         _testGetSet->PrepData();
-        VERIFY_IS_FALSE(_pDispatch->DeviceStatusReport((DispatchTypes::StatusType)-1));
+        VERIFY_IS_FALSE(_pDispatch->DeviceStatusReport((DispatchTypes::StatusType)-1, {}));
     }
 
     TEST_METHOD(DeviceStatus_OperatingStatusTests)
@@ -1367,7 +1372,7 @@ class AdapterTest
 
         Log::Comment(L"Test 1: Verify good operating condition.");
         _testGetSet->PrepData();
-        VERIFY_IS_TRUE(_pDispatch->DeviceStatusReport(DispatchTypes::StatusType::OS_OperatingStatus));
+        VERIFY_IS_TRUE(_pDispatch->DeviceStatusReport(DispatchTypes::StatusType::OS_OperatingStatus, {}));
 
         _testGetSet->ValidateInputEvent(L"\x1b[0n");
     }
@@ -1390,7 +1395,7 @@ class AdapterTest
             coordCursorExpected.X++;
             coordCursorExpected.Y++;
 
-            VERIFY_IS_TRUE(_pDispatch->DeviceStatusReport(DispatchTypes::StatusType::CPR_CursorPositionReport));
+            VERIFY_IS_TRUE(_pDispatch->DeviceStatusReport(DispatchTypes::StatusType::CPR_CursorPositionReport, {}));
 
             wchar_t pwszBuffer[50];
 
@@ -1414,7 +1419,7 @@ class AdapterTest
             // Then note that VT is 1,1 based for the top left, so add 1. (The rest of the console uses 0,0 for array index bases.)
             coordCursorExpectedFirst += til::point{ 1, 1 };
 
-            VERIFY_IS_TRUE(_pDispatch->DeviceStatusReport(DispatchTypes::StatusType::CPR_CursorPositionReport));
+            VERIFY_IS_TRUE(_pDispatch->DeviceStatusReport(DispatchTypes::StatusType::CPR_CursorPositionReport, {}));
 
             auto cursorPos = _testGetSet->_textBuffer->GetCursor().GetPosition();
             cursorPos.X++;
@@ -1424,7 +1429,7 @@ class AdapterTest
             auto coordCursorExpectedSecond{ coordCursorExpectedFirst };
             coordCursorExpectedSecond += til::point{ 1, 1 };
 
-            VERIFY_IS_TRUE(_pDispatch->DeviceStatusReport(DispatchTypes::StatusType::CPR_CursorPositionReport));
+            VERIFY_IS_TRUE(_pDispatch->DeviceStatusReport(DispatchTypes::StatusType::CPR_CursorPositionReport, {}));
 
             wchar_t pwszBuffer[50];
 
@@ -1453,13 +1458,80 @@ class AdapterTest
         // Until we support paging (GH#13892) the reported page number should always be 1.
         const auto pageExpected = 1;
 
-        VERIFY_IS_TRUE(_pDispatch->DeviceStatusReport(DispatchTypes::StatusType::ExCPR_ExtendedCursorPositionReport));
+        VERIFY_IS_TRUE(_pDispatch->DeviceStatusReport(DispatchTypes::StatusType::ExCPR_ExtendedCursorPositionReport, {}));
 
         wchar_t pwszBuffer[50];
         swprintf_s(pwszBuffer, ARRAYSIZE(pwszBuffer), L"\x1b[?%d;%d;%dR", coordCursorExpected.Y, coordCursorExpected.X, pageExpected);
         _testGetSet->ValidateInputEvent(pwszBuffer);
     }
 
+    TEST_METHOD(DeviceStatus_MacroSpaceReportTest)
+    {
+        Log::Comment(L"Starting test...");
+
+        // Space is measured in blocks of 16 bytes.
+        const auto availableSpace = MacroBuffer::MAX_SPACE / 16;
+
+        Log::Comment(L"Test 1: Verify maximum space available");
+        _testGetSet->PrepData();
+        VERIFY_IS_TRUE(_pDispatch->DeviceStatusReport(DispatchTypes::StatusType::MSR_MacroSpaceReport, {}));
+
+        wchar_t pwszBuffer[50];
+        swprintf_s(pwszBuffer, ARRAYSIZE(pwszBuffer), L"\x1b[%zu*{", availableSpace);
+        _testGetSet->ValidateInputEvent(pwszBuffer);
+
+        Log::Comment(L"Test 2: Verify space decrease");
+        _testGetSet->PrepData();
+        // Define four 8-byte macros, i.e. 32 byes (2 macro blocks).
+        _stateMachine->ProcessString(L"\033P1;0;0!z12345678\033\\");
+        _stateMachine->ProcessString(L"\033P2;0;0!z12345678\033\\");
+        _stateMachine->ProcessString(L"\033P3;0;0!z12345678\033\\");
+        _stateMachine->ProcessString(L"\033P4;0;0!z12345678\033\\");
+        VERIFY_IS_TRUE(_pDispatch->DeviceStatusReport(DispatchTypes::StatusType::MSR_MacroSpaceReport, {}));
+
+        swprintf_s(pwszBuffer, ARRAYSIZE(pwszBuffer), L"\x1b[%zu*{", availableSpace - 2);
+        _testGetSet->ValidateInputEvent(pwszBuffer);
+
+        Log::Comment(L"Test 3: Verify space reset");
+        _testGetSet->PrepData();
+        VERIFY_IS_TRUE(_pDispatch->HardReset());
+        VERIFY_IS_TRUE(_pDispatch->DeviceStatusReport(DispatchTypes::StatusType::MSR_MacroSpaceReport, {}));
+
+        swprintf_s(pwszBuffer, ARRAYSIZE(pwszBuffer), L"\x1b[%zu*{", availableSpace);
+        _testGetSet->ValidateInputEvent(pwszBuffer);
+    }
+
+    TEST_METHOD(DeviceStatus_MemoryChecksumReportTest)
+    {
+        Log::Comment(L"Starting test...");
+
+        Log::Comment(L"Test 1: Verify initial checksum is 0");
+        _testGetSet->PrepData();
+        VERIFY_IS_TRUE(_pDispatch->DeviceStatusReport(DispatchTypes::StatusType::MEM_MemoryChecksum, 12));
+
+        _testGetSet->ValidateInputEvent(L"\033P12!~0000\033\\");
+
+        Log::Comment(L"Test 2: Verify checksum after macros defined");
+        _testGetSet->PrepData();
+        // Define a couple of text macros
+        _stateMachine->ProcessString(L"\033P1;0;0!zABCD\033\\");
+        _stateMachine->ProcessString(L"\033P2;0;0!zabcd\033\\");
+        VERIFY_IS_TRUE(_pDispatch->DeviceStatusReport(DispatchTypes::StatusType::MEM_MemoryChecksum, 34));
+
+        // Checksum is a 16-bit negated sum of the macro buffer characters.
+        const auto checksum = gsl::narrow_cast<uint16_t>(-('A' + 'B' + 'C' + 'D' + 'a' + 'b' + 'c' + 'd'));
+        wchar_t pwszBuffer[50];
+        swprintf_s(pwszBuffer, ARRAYSIZE(pwszBuffer), L"\033P34!~%04X\033\\", checksum);
+        _testGetSet->ValidateInputEvent(pwszBuffer);
+
+        Log::Comment(L"Test 3: Verify checksum resets to 0");
+        _testGetSet->PrepData();
+        VERIFY_IS_TRUE(_pDispatch->HardReset());
+        VERIFY_IS_TRUE(_pDispatch->DeviceStatusReport(DispatchTypes::StatusType::MEM_MemoryChecksum, 56));
+
+        _testGetSet->ValidateInputEvent(L"\033P56!~0000\033\\");
+    }
+
     TEST_METHOD(DeviceAttributesTests)
     {
         Log::Comment(L"Starting test...");
@@ -2292,6 +2364,148 @@ class AdapterTest
         VERIFY_IS_FALSE(_stateMachine->GetParserMode(StateMachine::Mode::AcceptC1));
     }
 
+    TEST_METHOD(MacroDefinitions)
+    {
+        const auto getMacroText = [&](const auto id) {
+            return _pDispatch->_macroBuffer->_macros.at(id);
+        };
+
+        Log::Comment(L"Text encoding");
+        _stateMachine->ProcessString(L"\033P1;0;0!zText Encoding\033\\");
+        VERIFY_ARE_EQUAL(L"Text Encoding", getMacroText(1));
+
+        Log::Comment(L"Hex encoding (uppercase)");
+        _stateMachine->ProcessString(L"\033P2;0;1!z486578204A4B4C4D4E4F\033\\");
+        VERIFY_ARE_EQUAL(L"Hex JKLMNO", getMacroText(2));
+
+        Log::Comment(L"Hex encoding (lowercase)");
+        _stateMachine->ProcessString(L"\033P3;0;1!z486578206a6b6c6d6e6f\033\\");
+        VERIFY_ARE_EQUAL(L"Hex jklmno", getMacroText(3));
+
+        Log::Comment(L"Default encoding is text");
+        _stateMachine->ProcessString(L"\033P4;0;!zDefault Encoding\033\\");
+        VERIFY_ARE_EQUAL(L"Default Encoding", getMacroText(4));
+
+        Log::Comment(L"Default ID is 0");
+        _stateMachine->ProcessString(L"\033P;0;0!zDefault ID\033\\");
+        VERIFY_ARE_EQUAL(L"Default ID", getMacroText(0));
+
+        Log::Comment(L"Replacing a single macro");
+        _stateMachine->ProcessString(L"\033P1;0;0!zRetained\033\\");
+        _stateMachine->ProcessString(L"\033P2;0;0!zReplaced\033\\");
+        _stateMachine->ProcessString(L"\033P2;0;0!zNew\033\\");
+        VERIFY_ARE_EQUAL(L"Retained", getMacroText(1));
+        VERIFY_ARE_EQUAL(L"New", getMacroText(2));
+
+        Log::Comment(L"Replacing all macros");
+        _stateMachine->ProcessString(L"\033P1;0;0!zErased\033\\");
+        _stateMachine->ProcessString(L"\033P2;0;0!zReplaced\033\\");
+        _stateMachine->ProcessString(L"\033P2;1;0!zNew\033\\");
+        VERIFY_ARE_EQUAL(L"", getMacroText(1));
+        VERIFY_ARE_EQUAL(L"New", getMacroText(2));
+
+        Log::Comment(L"Default replacement is a single macro");
+        _stateMachine->ProcessString(L"\033P1;0;0!zRetained\033\\");
+        _stateMachine->ProcessString(L"\033P2;0;0!zReplaced\033\\");
+        _stateMachine->ProcessString(L"\033P2;;0!zNew\033\\");
+        VERIFY_ARE_EQUAL(L"Retained", getMacroText(1));
+        VERIFY_ARE_EQUAL(L"New", getMacroText(2));
+
+        Log::Comment(L"Repeating three times");
+        _stateMachine->ProcessString(L"\033P5;0;1!z526570656174!3;206563686F;207468726565\033\\");
+        VERIFY_ARE_EQUAL(L"Repeat echo echo echo three", getMacroText(5));
+
+        Log::Comment(L"Zero repeats once");
+        _stateMachine->ProcessString(L"\033P6;0;1!z526570656174!0;206563686F;207A65726F\033\\");
+        VERIFY_ARE_EQUAL(L"Repeat echo zero", getMacroText(6));
+
+        Log::Comment(L"Default repeats once");
+        _stateMachine->ProcessString(L"\033P7;0;1!z526570656174!;206563686F;2064656661756C74\033\\");
+        VERIFY_ARE_EQUAL(L"Repeat echo default", getMacroText(7));
+
+        Log::Comment(L"Unterminated repeat sequence");
+        _stateMachine->ProcessString(L"\033P8;0;1!z556E7465726D696E61746564!3;206563686F\033\\");
+        VERIFY_ARE_EQUAL(L"Unterminated echo echo echo", getMacroText(8));
+
+        Log::Comment(L"Unexpected semicolon cancels definition");
+        _stateMachine->ProcessString(L"\033P9;0;0!zReplaced\033\\");
+        _stateMachine->ProcessString(L"\033P9;0;1!z526570656174!3;206563;686F;207468726565\033\\");
+        VERIFY_ARE_EQUAL(L"", getMacroText(9));
+
+        Log::Comment(L"Control characters in a text encoding");
+        _stateMachine->ProcessString(L"\033P10;0;0!zA\aB\bC\tD\nE\vF\fG\rH\033\\");
+        VERIFY_ARE_EQUAL(L"ABCDEFGH", getMacroText(10));
+
+        Log::Comment(L"Control characters in a hex encoding");
+        _stateMachine->ProcessString(L"\033P11;0;1!z41\a42\b43\t44\n45\v46\f47\r48\033\\");
+        VERIFY_ARE_EQUAL(L"ABCDEFGH", getMacroText(11));
+
+        Log::Comment(L"Control characters in a repeat");
+        _stateMachine->ProcessString(L"\033P12;0;1!z!\a3\b;\t4\n1\v4\f2\r4\a3\b;\033\\");
+        VERIFY_ARE_EQUAL(L"ABCABCABC", getMacroText(12));
+
+        Log::Comment(L"Encoded control characters");
+        _stateMachine->ProcessString(L"\033P13;0;1!z410742084309440A450B460C470D481B49\033\\");
+        VERIFY_ARE_EQUAL(L"A\aB\bC\tD\nE\vF\fG\rH\033I", getMacroText(13));
+
+        _pDispatch->_macroBuffer = nullptr;
+    }
+
+    TEST_METHOD(MacroInvokes)
+    {
+        _pDispatch->_macroBuffer = std::make_shared<MacroBuffer>();
+
+        const auto setMacroText = [&](const auto id, const auto value) {
+            _pDispatch->_macroBuffer->_macros.at(id) = value;
+        };
+
+        setMacroText(0, L"Macro 0");
+        setMacroText(1, L"Macro 1");
+        setMacroText(2, L"Macro 2");
+        setMacroText(63, L"Macro 63");
+
+        Log::Comment(L"Simple macro invoke");
+        _testGetSet->_printed.clear();
+        _stateMachine->ProcessString(L"\033[2*z");
+        VERIFY_ARE_EQUAL(L"Macro 2", _testGetSet->_printed);
+
+        Log::Comment(L"Default macro invoke");
+        _testGetSet->_printed.clear();
+        _stateMachine->ProcessString(L"\033[*z");
+        VERIFY_ARE_EQUAL(L"Macro 0", _testGetSet->_printed);
+
+        Log::Comment(L"Maximum ID number");
+        _testGetSet->_printed.clear();
+        _stateMachine->ProcessString(L"\033[63*z");
+        VERIFY_ARE_EQUAL(L"Macro 63", _testGetSet->_printed);
+
+        Log::Comment(L"Out of range ID number");
+        _testGetSet->_printed.clear();
+        _stateMachine->ProcessString(L"\033[64*z");
+        VERIFY_ARE_EQUAL(L"", _testGetSet->_printed);
+
+        Log::Comment(L"Only one ID parameter allowed");
+        _testGetSet->_printed.clear();
+        _stateMachine->ProcessString(L"\033[2;0;1*z");
+        VERIFY_ARE_EQUAL(L"Macro 2", _testGetSet->_printed);
+
+        Log::Comment(L"DECDMAC ignored when inside a macro");
+        setMacroText(10, L"[\033P1;0;0!zReplace Macro 1\033\\]");
+        _testGetSet->_printed.clear();
+        _stateMachine->ProcessString(L"\033[10*z");
+        _stateMachine->ProcessString(L"\033[1*z");
+        VERIFY_ARE_EQUAL(L"[]Macro 1", _testGetSet->_printed);
+
+        Log::Comment(L"Maximum recursive depth is 16");
+        setMacroText(0, L"<\033[1*z>");
+        setMacroText(1, L"[\033[0*z]");
+        _testGetSet->_printed.clear();
+        _stateMachine->ProcessString(L"\033[0*z");
+        VERIFY_ARE_EQUAL(L"<[<[<[<[<[<[<[<[]>]>]>]>]>]>]>]>", _testGetSet->_printed);
+
+        _pDispatch->_macroBuffer = nullptr;
+    }
+
 private:
     TerminalInput _terminalInput{ nullptr };
     std::unique_ptr<TestGetSet> _testGetSet;
diff --git a/src/terminal/parser/OutputStateMachineEngine.cpp b/src/terminal/parser/OutputStateMachineEngine.cpp
index c7fa93a2bbf..2cd56a3252a 100644
--- a/src/terminal/parser/OutputStateMachineEngine.cpp
+++ b/src/terminal/parser/OutputStateMachineEngine.cpp
@@ -530,11 +530,11 @@ bool OutputStateMachineEngine::ActionCsiDispatch(const VTID id, const VTParamete
         TermTelemetry::Instance().Log(TermTelemetry::Codes::SGR);
         break;
     case CsiActionCodes::DSR_DeviceStatusReport:
-        success = _dispatch->DeviceStatusReport(DispatchTypes::ANSIStandardStatus(parameters.at(0)));
+        success = _dispatch->DeviceStatusReport(DispatchTypes::ANSIStandardStatus(parameters.at(0)), parameters.at(1));
         TermTelemetry::Instance().Log(TermTelemetry::Codes::DSR);
         break;
     case CsiActionCodes::DSR_PrivateDeviceStatusReport:
-        success = _dispatch->DeviceStatusReport(DispatchTypes::DECPrivateStatus(parameters.at(0)));
+        success = _dispatch->DeviceStatusReport(DispatchTypes::DECPrivateStatus(parameters.at(0)), parameters.at(1));
         TermTelemetry::Instance().Log(TermTelemetry::Codes::DSR);
         break;
     case CsiActionCodes::DA_DeviceAttributes:
@@ -664,6 +664,10 @@ bool OutputStateMachineEngine::ActionCsiDispatch(const VTID id, const VTParamete
         success = _dispatch->SelectAttributeChangeExtent(parameters.at(0));
         TermTelemetry::Instance().Log(TermTelemetry::Codes::DECSACE);
         break;
+    case CsiActionCodes::DECINVM_InvokeMacro:
+        success = _dispatch->InvokeMacro(parameters.at(0).value_or(0));
+        TermTelemetry::Instance().Log(TermTelemetry::Codes::DECINVM);
+        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);
@@ -715,6 +719,9 @@ IStateMachineEngine::StringHandler OutputStateMachineEngine::ActionDcsDispatch(c
                                           parameters.at(6),
                                           parameters.at(7));
         break;
+    case DcsActionCodes::DECDMAC_DefineMacro:
+        handler = _dispatch->DefineMacro(parameters.at(0).value_or(0), parameters.at(1), parameters.at(2));
+        break;
     case DcsActionCodes::DECRSTS_RestoreTerminalState:
         handler = _dispatch->RestoreTerminalState(parameters.at(0));
         break;
diff --git a/src/terminal/parser/OutputStateMachineEngine.hpp b/src/terminal/parser/OutputStateMachineEngine.hpp
index b125c7b714e..c7f528ba806 100644
--- a/src/terminal/parser/OutputStateMachineEngine.hpp
+++ b/src/terminal/parser/OutputStateMachineEngine.hpp
@@ -155,6 +155,7 @@ namespace Microsoft::Console::VirtualTerminal
             DECSERA_SelectiveEraseRectangularArea = VTID("${"),
             DECSCPP_SetColumnsPerPage = VTID("$|"),
             DECSACE_SelectAttributeChangeExtent = VTID("*x"),
+            DECINVM_InvokeMacro = VTID("*z"),
             DECAC_AssignColor = VTID(",|"),
             DECPS_PlaySound = VTID(",~")
         };
@@ -162,6 +163,7 @@ namespace Microsoft::Console::VirtualTerminal
         enum DcsActionCodes : uint64_t
         {
             DECDLD_DownloadDRCS = VTID("{"),
+            DECDMAC_DefineMacro = VTID("!z"),
             DECRSTS_RestoreTerminalState = VTID("$p"),
             DECRQSS_RequestSetting = VTID("$q")
         };
diff --git a/src/terminal/parser/stateMachine.cpp b/src/terminal/parser/stateMachine.cpp
index 36848694f32..d6a9aa31489 100644
--- a/src/terminal/parser/stateMachine.cpp
+++ b/src/terminal/parser/stateMachine.cpp
@@ -1175,6 +1175,7 @@ void StateMachine::_EventCsiEntry(const wchar_t wch)
     {
         _ActionCsiDispatch(wch);
         _EnterGround();
+        _ExecuteCsiCompleteCallback();
     }
 }
 
@@ -1213,6 +1214,7 @@ void StateMachine::_EventCsiIntermediate(const wchar_t wch)
     {
         _ActionCsiDispatch(wch);
         _EnterGround();
+        _ExecuteCsiCompleteCallback();
     }
 }
 
@@ -1294,6 +1296,7 @@ void StateMachine::_EventCsiParam(const wchar_t wch)
     {
         _ActionCsiDispatch(wch);
         _EnterGround();
+        _ExecuteCsiCompleteCallback();
     }
 }
 
@@ -1996,6 +1999,18 @@ bool StateMachine::IsProcessingLastCharacter() const noexcept
     return _processingLastCharacter;
 }
 
+// Routine Description:
+// - Registers a function that will be called once the current CSI action is
+//   complete and the state machine has returned to the ground state.
+// Arguments:
+// - callback - The function that will be called
+// Return Value:
+// - <none>
+void StateMachine::OnCsiComplete(const std::function<void()> callback)
+{
+    _onCsiCompleteCallback = callback;
+}
+
 // Routine Description:
 // - Wherever the state machine is, whatever it's going, go back to ground.
 //     This is used by conhost to "jiggle the handle" - when VT support is
@@ -2058,3 +2073,24 @@ bool StateMachine::_SafeExecuteWithLog(const wchar_t wch, TLambda&& lambda)
     }
     return success;
 }
+
+void StateMachine::_ExecuteCsiCompleteCallback()
+{
+    if (_onCsiCompleteCallback)
+    {
+        // We need to save the state of the string that we're currently
+        // processing in case the callback injects another string.
+        const auto savedCurrentString = _currentString;
+        const auto savedRunOffset = _runOffset;
+        const auto savedRunSize = _runSize;
+        // We also need to take ownership of the callback function before
+        // executing it so there's no risk of it being run more than once.
+        const auto callback = std::move(_onCsiCompleteCallback);
+        callback();
+        // Once the callback has returned, we can restore the original state
+        // and continue where we left off.
+        _currentString = savedCurrentString;
+        _runOffset = savedRunOffset;
+        _runSize = savedRunSize;
+    }
+}
diff --git a/src/terminal/parser/stateMachine.hpp b/src/terminal/parser/stateMachine.hpp
index 41bce50f97e..a51351e456f 100644
--- a/src/terminal/parser/stateMachine.hpp
+++ b/src/terminal/parser/stateMachine.hpp
@@ -61,6 +61,8 @@ namespace Microsoft::Console::VirtualTerminal
         void ProcessString(const std::wstring_view string);
         bool IsProcessingLastCharacter() const noexcept;
 
+        void OnCsiComplete(const std::function<void()> callback);
+
         void ResetState() noexcept;
 
         bool FlushToTerminal();
@@ -142,6 +144,8 @@ namespace Microsoft::Console::VirtualTerminal
         template<typename TLambda>
         bool _SafeExecuteWithLog(const wchar_t wch, TLambda&& lambda);
 
+        void _ExecuteCsiCompleteCallback();
+
         enum class VTStates
         {
             Ground,
@@ -202,5 +206,7 @@ namespace Microsoft::Console::VirtualTerminal
         //   can start and finish a sequence.
         bool _processingIndividually;
         bool _processingLastCharacter;
+
+        std::function<void()> _onCsiCompleteCallback;
     };
 }
diff --git a/src/terminal/parser/telemetry.cpp b/src/terminal/parser/telemetry.cpp
index 4812fb77544..a55c76e88aa 100644
--- a/src/terminal/parser/telemetry.cpp
+++ b/src/terminal/parser/telemetry.cpp
@@ -290,6 +290,7 @@ void TermTelemetry::WriteFinalTraceLog() const
                                       TraceLoggingUInt32(_uiTimesUsed[DECERA], "DECERA"),
                                       TraceLoggingUInt32(_uiTimesUsed[DECSERA], "DECSERA"),
                                       TraceLoggingUInt32(_uiTimesUsed[DECSACE], "DECSACE"),
+                                      TraceLoggingUInt32(_uiTimesUsed[DECINVM], "DECINVM"),
                                       TraceLoggingUInt32(_uiTimesUsed[DECAC], "DECAC"),
                                       TraceLoggingUInt32(_uiTimesUsed[DECPS], "DECPS"),
                                       TraceLoggingUInt32Array(_uiTimesFailed, ARRAYSIZE(_uiTimesFailed), "Failed"),
diff --git a/src/terminal/parser/telemetry.hpp b/src/terminal/parser/telemetry.hpp
index 283a163402b..d8b00571f8b 100644
--- a/src/terminal/parser/telemetry.hpp
+++ b/src/terminal/parser/telemetry.hpp
@@ -117,6 +117,7 @@ namespace Microsoft::Console::VirtualTerminal
             DECERA,
             DECSERA,
             DECSACE,
+            DECINVM,
             DECAC,
             DECPS,
             // Only use this last enum as a count of the number of codes.
diff --git a/src/terminal/parser/ut_parser/OutputEngineTest.cpp b/src/terminal/parser/ut_parser/OutputEngineTest.cpp
index 12246eb14d1..2c70286b3f9 100644
--- a/src/terminal/parser/ut_parser/OutputEngineTest.cpp
+++ b/src/terminal/parser/ut_parser/OutputEngineTest.cpp
@@ -1203,7 +1203,7 @@ class StatefulDispatch final : public TermDispatch
     }
     CATCH_LOG_RETURN_FALSE()
 
-    bool DeviceStatusReport(const DispatchTypes::StatusType statusType) noexcept override
+    bool DeviceStatusReport(const DispatchTypes::StatusType statusType, const VTParameter /*id*/) noexcept override
     {
         _deviceStatusReport = true;
         _statusReportType = statusType;


Test patch: