Repository: microsoft/terminal (language: cpp)

Issue:
Add support for the S8C1T/S7C1T escape sequences
# Description of the new feature/enhancement

The `S8C1T` and `S7C1T` commands enable an application to choose whether the terminal should use C1 controls when sending key sequences and query responses. For example, instead of encoding a `CSI` prefix as `ESC` `[`, the terminal would use a single `CSI` codepoint (i.e. `U+009B`).

Back in the days of the 8-bit hardware terminals, this had the advantage of being slightly more efficient (your sequence introducers would take 1 byte rather than 2). And another bonus was that it made it easy to distinguish an <kbd>Esc</kbd> key (assuming your keyboard had one) from other key sequences that merely started with the `ESC` character.

However, these days you're more likely to be using a UTF-8 encoding, in which case a C1 control character is still going to occupy two bytes, so there is no advantage in terms of sequence length. And the ability to distinguish an <kbd>Esc</kbd> key from other key sequences is somewhat diminished by the modern usage of `ESC` as a prefix for <kbd>Alt</kbd> key combinations.

In short, there's probably not much demand for this functionality outside of people wanting to connect to legacy systems, but it would be nice to have just for completeness sake (it's a requirement for VT level 2 conformance).

# Proposed technical implementation details (optional)

Some of the framework for the key sequence encoding was already added in PR #16511, so that just needs to be hooked up to a new input mode which can be toggled by the `S8C1T` and `S7C1T` escape sequences. We need to do something similar in `AdaptDispatch` for all the query responses, but that's also fairly straightforward.

However, if we want to support 8-bit C1 sequences, as used in legacy systems, we also need a way to switch the input code page from UTF-8 to something that can cleanly pass through 8-bit bytes (like ISO-8859-1). Fortunately we already have a `DOCS` escape sequence which sets the *output* code page to ISO-8859-1, and I think it's reasonable to extend that to set the input code page at the same time.

The catch is that this won't work in a WSL shell, because WSL doesn't pay any attention to the input code page. And while it is potentially usable from a Windows shell, the typical use case will involve connecting to a remote system, which means you're going to ned a ssh or telnet client that is 8-bit capable (and I think that rules out the Windows inbox clients).

The bottom line is there are a few hoops you need to jump through to get this to work. Given these limitations, is it still worth adding?


Discussion / hints:

Fix patch:
diff --git a/src/cascadia/TerminalCore/Terminal.hpp b/src/cascadia/TerminalCore/Terminal.hpp
index e9d3f1abd0a..94d2a938a81 100644
--- a/src/cascadia/TerminalCore/Terminal.hpp
+++ b/src/cascadia/TerminalCore/Terminal.hpp
@@ -140,8 +140,10 @@ class Microsoft::Terminal::Core::Terminal final :
     void SetWindowTitle(const std::wstring_view title) override;
     CursorType GetUserDefaultCursorStyle() const noexcept override;
     bool ResizeWindow(const til::CoordType width, const til::CoordType height) override;
-    void SetConsoleOutputCP(const unsigned int codepage) noexcept override;
-    unsigned int GetConsoleOutputCP() const noexcept override;
+    void SetCodePage(const unsigned int codepage) noexcept override;
+    void ResetCodePage() noexcept override;
+    unsigned int GetOutputCodePage() const noexcept override;
+    unsigned int GetInputCodePage() const noexcept override;
     void CopyToClipboard(wil::zwstring_view content) override;
     void SetTaskbarProgress(const ::Microsoft::Console::VirtualTerminal::DispatchTypes::TaskbarState state, const size_t progress) override;
     void SetWorkingDirectory(std::wstring_view uri) override;
diff --git a/src/cascadia/TerminalCore/TerminalApi.cpp b/src/cascadia/TerminalCore/TerminalApi.cpp
index 40ff599e115..e6edec3ee92 100644
--- a/src/cascadia/TerminalCore/TerminalApi.cpp
+++ b/src/cascadia/TerminalCore/TerminalApi.cpp
@@ -116,14 +116,25 @@ bool Terminal::ResizeWindow(const til::CoordType width, const til::CoordType hei
     return false;
 }
 
-void Terminal::SetConsoleOutputCP(const unsigned int /*codepage*/) noexcept
+void Terminal::SetCodePage(const unsigned int /*codepage*/) noexcept
 {
-    // TODO: This will be needed to support 8-bit charsets and DOCS sequences.
+    // Code pages are dealt with in ConHost, so this isn't needed.
 }
 
-unsigned int Terminal::GetConsoleOutputCP() const noexcept
+void Terminal::ResetCodePage() noexcept
 {
-    // TODO: See SetConsoleOutputCP above.
+    // There is nothing to reset, since the code page never changes.
+}
+
+unsigned int Terminal::GetOutputCodePage() const noexcept
+{
+    // See above. The code page is always UTF-8.
+    return CP_UTF8;
+}
+
+unsigned int Terminal::GetInputCodePage() const noexcept
+{
+    // See above. The code page is always UTF-8.
     return CP_UTF8;
 }
 
diff --git a/src/host/getset.cpp b/src/host/getset.cpp
index f199362f7af..b8109bca718 100644
--- a/src/host/getset.cpp
+++ b/src/host/getset.cpp
@@ -1140,7 +1140,6 @@ void ApiRoutines::GetLargestConsoleWindowSizeImpl(const SCREEN_INFORMATION& cont
     {
         // Set new code page
         gci.OutputCP = codepage;
-
         SetConsoleCPInfo(TRUE);
     }
 
@@ -1157,13 +1156,36 @@ void ApiRoutines::GetLargestConsoleWindowSizeImpl(const SCREEN_INFORMATION& cont
 {
     try
     {
+        auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
         LockConsole();
         auto Unlock = wil::scope_exit([&] { UnlockConsole(); });
-        return DoSrvSetConsoleOutputCodePage(codepage);
+        RETURN_IF_FAILED(DoSrvSetConsoleOutputCodePage(codepage));
+        // Setting the code page via the API also updates the default value.
+        // This is how the initial code page is set to UTF-8 in a WSL shell.
+        gci.DefaultOutputCP = codepage;
+        return S_OK;
     }
     CATCH_RETURN();
 }
 
+[[nodiscard]] HRESULT DoSrvSetConsoleInputCodePage(const unsigned int codepage)
+{
+    auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
+
+    // Return if it's not known as a valid codepage ID.
+    RETURN_HR_IF(E_INVALIDARG, !(IsValidCodePage(codepage)));
+
+    // Do nothing if no change.
+    if (gci.CP != codepage)
+    {
+        // Set new code page
+        gci.CP = codepage;
+        SetConsoleCPInfo(FALSE);
+    }
+
+    return S_OK;
+}
+
 // Routine Description:
 // - Sets the codepage used for translating text when calling A versions of functions affecting the input buffer.
 // Arguments:
@@ -1177,19 +1199,10 @@ void ApiRoutines::GetLargestConsoleWindowSizeImpl(const SCREEN_INFORMATION& cont
         auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
         LockConsole();
         auto Unlock = wil::scope_exit([&] { UnlockConsole(); });
-
-        // Return if it's not known as a valid codepage ID.
-        RETURN_HR_IF(E_INVALIDARG, !(IsValidCodePage(codepage)));
-
-        // Do nothing if no change.
-        if (gci.CP != codepage)
-        {
-            // Set new code page
-            gci.CP = codepage;
-
-            SetConsoleCPInfo(FALSE);
-        }
-
+        RETURN_IF_FAILED(DoSrvSetConsoleInputCodePage(codepage));
+        // Setting the code page via the API also updates the default value.
+        // This is how the initial code page is set to UTF-8 in a WSL shell.
+        gci.DefaultCP = codepage;
         return S_OK;
     }
     CATCH_RETURN();
diff --git a/src/host/getset.h b/src/host/getset.h
index 4c042813c27..f3030eb6050 100644
--- a/src/host/getset.h
+++ b/src/host/getset.h
@@ -19,3 +19,4 @@ Revision History:
 class SCREEN_INFORMATION;
 
 [[nodiscard]] HRESULT DoSrvSetConsoleOutputCodePage(const unsigned int codepage);
+[[nodiscard]] HRESULT DoSrvSetConsoleInputCodePage(const unsigned int codepage);
diff --git a/src/host/output.cpp b/src/host/output.cpp
index e57412f5310..729821c841a 100644
--- a/src/host/output.cpp
+++ b/src/host/output.cpp
@@ -35,6 +35,8 @@ using namespace Microsoft::Console::Interactivity;
     // codepage by console.cpl or shell32. The default codepage is OEMCP.
     gci.CP = gci.GetCodePage();
     gci.OutputCP = gci.GetCodePage();
+    gci.DefaultCP = gci.GetCodePage();
+    gci.DefaultOutputCP = gci.GetCodePage();
 
     gci.Flags |= CONSOLE_USE_PRIVATE_FLAGS;
 
diff --git a/src/host/outputStream.cpp b/src/host/outputStream.cpp
index d88be99ef33..3fbeb3cafaf 100644
--- a/src/host/outputStream.cpp
+++ b/src/host/outputStream.cpp
@@ -221,27 +221,52 @@ void ConhostInternalGetSet::ShowWindow(bool showOrHide)
 }
 
 // Routine Description:
-// - Connects the SetConsoleOutputCP API call directly into our Driver Message servicing call inside Conhost.exe
+// - Set the code page used for translating text when calling A versions of I/O functions.
 // Arguments:
-// - codepage - the new output codepage of the console.
+// - codepage - the new code page of the console.
 // Return Value:
 // - <none>
-void ConhostInternalGetSet::SetConsoleOutputCP(const unsigned int codepage)
+void ConhostInternalGetSet::SetCodePage(const unsigned int codepage)
 {
-    THROW_IF_FAILED(DoSrvSetConsoleOutputCodePage(codepage));
+    LOG_IF_FAILED(DoSrvSetConsoleOutputCodePage(codepage));
+    LOG_IF_FAILED(DoSrvSetConsoleInputCodePage(codepage));
 }
 
 // Routine Description:
-// - Gets the codepage used for translating text when calling A versions of functions affecting the output buffer.
+// - Reset the code pages to their default values.
 // Arguments:
 // - <none>
 // Return Value:
-// - the outputCP of the console.
-unsigned int ConhostInternalGetSet::GetConsoleOutputCP() const
+// - <none>
+void ConhostInternalGetSet::ResetCodePage()
+{
+    const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
+    LOG_IF_FAILED(DoSrvSetConsoleOutputCodePage(gci.DefaultOutputCP));
+    LOG_IF_FAILED(DoSrvSetConsoleInputCodePage(gci.DefaultCP));
+}
+
+// Routine Description:
+// - Gets the code page used for translating text when calling A versions of output functions.
+// Arguments:
+// - <none>
+// Return Value:
+// - the output code page of the console.
+unsigned int ConhostInternalGetSet::GetOutputCodePage() const
 {
     return ServiceLocator::LocateGlobals().getConsoleInformation().OutputCP;
 }
 
+// Routine Description:
+// - Gets the code page used for translating text when calling A versions of input functions.
+// Arguments:
+// - <none>
+// Return Value:
+// - the input code page of the console.
+unsigned int ConhostInternalGetSet::GetInputCodePage() const
+{
+    return ServiceLocator::LocateGlobals().getConsoleInformation().CP;
+}
+
 // Routine Description:
 // - Copies the given content to the clipboard.
 // Arguments:
diff --git a/src/host/outputStream.hpp b/src/host/outputStream.hpp
index 6b564abbf8d..7b76e5f3d0a 100644
--- a/src/host/outputStream.hpp
+++ b/src/host/outputStream.hpp
@@ -53,8 +53,10 @@ class ConhostInternalGetSet final : public Microsoft::Console::VirtualTerminal::
 
     bool ResizeWindow(const til::CoordType width, const til::CoordType height) override;
 
-    void SetConsoleOutputCP(const unsigned int codepage) override;
-    unsigned int GetConsoleOutputCP() const override;
+    void SetCodePage(const unsigned int codepage) override;
+    void ResetCodePage() override;
+    unsigned int GetOutputCodePage() const override;
+    unsigned int GetInputCodePage() const override;
 
     void CopyToClipboard(const wil::zwstring_view content) override;
     void SetTaskbarProgress(const ::Microsoft::Console::VirtualTerminal::DispatchTypes::TaskbarState state, const size_t progress) override;
diff --git a/src/host/server.h b/src/host/server.h
index 1e14a94b731..0a0f905777b 100644
--- a/src/host/server.h
+++ b/src/host/server.h
@@ -90,6 +90,9 @@ class CONSOLE_INFORMATION :
     // the following fields are used for ansi-unicode translation
     UINT CP = 0;
     UINT OutputCP = 0;
+    // the VT RIS sequence uses these default values to reset the code pages
+    UINT DefaultCP = 0;
+    UINT DefaultOutputCP = 0;
 
     ULONG CtrlFlags = 0; // indicates outstanding ctrl requests
     ULONG LimitingProcessId = 0;
diff --git a/src/terminal/adapter/ITermDispatch.hpp b/src/terminal/adapter/ITermDispatch.hpp
index 3df5ef30178..8ec9e33bdc0 100644
--- a/src/terminal/adapter/ITermDispatch.hpp
+++ b/src/terminal/adapter/ITermDispatch.hpp
@@ -122,6 +122,7 @@ class Microsoft::Console::VirtualTerminal::ITermDispatch
     virtual void LockingShiftRight(const VTInt gsetNumber) = 0; // LS1R, LS2R, LS3R
     virtual void SingleShift(const VTInt gsetNumber) = 0; // SS2, SS3
     virtual void AcceptC1Controls(const bool enabled) = 0; // DECAC1
+    virtual void SendC1Controls(const bool enabled) = 0; // S8C1T, S7C1T
     virtual void AnnounceCodeStructure(const VTInt ansiLevel) = 0; // ACS
 
     virtual void SoftReset() = 0; // DECSTR
diff --git a/src/terminal/adapter/ITerminalApi.hpp b/src/terminal/adapter/ITerminalApi.hpp
index b874bfb8cd4..532133f9a84 100644
--- a/src/terminal/adapter/ITerminalApi.hpp
+++ b/src/terminal/adapter/ITerminalApi.hpp
@@ -72,8 +72,10 @@ namespace Microsoft::Console::VirtualTerminal
 
         virtual void ShowWindow(bool showOrHide) = 0;
 
-        virtual void SetConsoleOutputCP(const unsigned int codepage) = 0;
-        virtual unsigned int GetConsoleOutputCP() const = 0;
+        virtual void SetCodePage(const unsigned int codepage) = 0;
+        virtual void ResetCodePage() = 0;
+        virtual unsigned int GetOutputCodePage() const = 0;
+        virtual unsigned int GetInputCodePage() const = 0;
 
         virtual void CopyToClipboard(const wil::zwstring_view content) = 0;
         virtual void SetTaskbarProgress(const DispatchTypes::TaskbarState state, const size_t progress) = 0;
diff --git a/src/terminal/adapter/InteractDispatch.cpp b/src/terminal/adapter/InteractDispatch.cpp
index fa1a0826047..a8fc483aee6 100644
--- a/src/terminal/adapter/InteractDispatch.cpp
+++ b/src/terminal/adapter/InteractDispatch.cpp
@@ -62,7 +62,7 @@ void InteractDispatch::WriteString(const std::wstring_view string)
 {
     if (!string.empty())
     {
-        const auto codepage = _api.GetConsoleOutputCP();
+        const auto codepage = _api.GetOutputCodePage();
         InputEventQueue keyEvents;
 
         for (const auto& wch : string)
diff --git a/src/terminal/adapter/adaptDispatch.cpp b/src/terminal/adapter/adaptDispatch.cpp
index 3899c7cd84d..9ee93ede8b8 100644
--- a/src/terminal/adapter/adaptDispatch.cpp
+++ b/src/terminal/adapter/adaptDispatch.cpp
@@ -1243,7 +1243,7 @@ void AdaptDispatch::FillRectangularArea(const VTParameter ch, const VTInt top, c
     const auto charValue = ch.value_or(0) == 0 ? 32 : ch.value();
     const auto glChar = (charValue >= 32 && charValue <= 126);
     const auto grChar = (charValue >= 160 && charValue <= 255);
-    const auto unicodeChar = (charValue >= 256 && charValue <= 65535 && _api.GetConsoleOutputCP() == CP_UTF8);
+    const auto unicodeChar = (charValue >= 256 && charValue <= 65535 && _api.GetOutputCodePage() == CP_UTF8);
     if (glChar || grChar || unicodeChar)
     {
         const auto fillChar = _termOutput.TranslateKey(gsl::narrow_cast<wchar_t>(charValue));
@@ -1377,8 +1377,7 @@ void AdaptDispatch::RequestChecksumRectangularArea(const VTInt id, const VTInt p
             }
         }
     }
-    const auto response = wil::str_printf<std::wstring>(L"\033P%d!~%04X\033\\", id, checksum);
-    _api.ReturnResponse(response);
+    _ReturnDcsResponse(wil::str_printf<std::wstring>(L"%d!~%04X", id, checksum));
 }
 
 // Routine Description:
@@ -1484,7 +1483,7 @@ void AdaptDispatch::DeviceAttributes()
     // 32 = Text macros
     // 42 = ISO Latin-2 character set
 
-    _api.ReturnResponse(L"\x1b[?61;4;6;7;14;21;22;23;24;28;32;42c");
+    _ReturnCsiResponse(L"?61;4;6;7;14;21;22;23;24;28;32;42c");
 }
 
 // Routine Description:
@@ -1496,7 +1495,7 @@ void AdaptDispatch::DeviceAttributes()
 // - <none>
 void AdaptDispatch::SecondaryDeviceAttributes()
 {
-    _api.ReturnResponse(L"\x1b[>0;10;1c");
+    _ReturnCsiResponse(L">0;10;1c");
 }
 
 // Routine Description:
@@ -1506,7 +1505,7 @@ void AdaptDispatch::SecondaryDeviceAttributes()
 // - <none>
 void AdaptDispatch::TertiaryDeviceAttributes()
 {
-    _api.ReturnResponse(L"\x1bP!|00000000\x1b\\");
+    _ReturnDcsResponse(L"!|00000000");
 }
 
 // Routine Description:
@@ -1544,10 +1543,10 @@ void AdaptDispatch::RequestTerminalParameters(const DispatchTypes::ReportingPerm
     switch (permission)
     {
     case DispatchTypes::ReportingPermission::Unsolicited:
-        _api.ReturnResponse(L"\x1b[2;1;1;128;128;1;0x");
+        _ReturnCsiResponse(L"2;1;1;128;128;1;0x");
         break;
     case DispatchTypes::ReportingPermission::Solicited:
-        _api.ReturnResponse(L"\x1b[3;1;1;128;128;1;0x");
+        _ReturnCsiResponse(L"3;1;1;128;128;1;0x");
         break;
     default:
         break;
@@ -1562,7 +1561,7 @@ void AdaptDispatch::RequestTerminalParameters(const DispatchTypes::ReportingPerm
 // - <none>
 void AdaptDispatch::_DeviceStatusReport(const wchar_t* parameters) const
 {
-    _api.ReturnResponse(fmt::format(FMT_COMPILE(L"\033[{}n"), parameters));
+    _ReturnCsiResponse(fmt::format(FMT_COMPILE(L"{}n"), parameters));
 }
 
 // Routine Description:
@@ -1598,14 +1597,12 @@ void AdaptDispatch::_CursorPositionReport(const bool extendedReport)
     {
         // An extended report also includes the page number.
         const auto pageNumber = page.Number();
-        const auto response = wil::str_printf<std::wstring>(L"\x1b[?%d;%d;%dR", cursorPosition.y, cursorPosition.x, pageNumber);
-        _api.ReturnResponse(response);
+        _ReturnCsiResponse(wil::str_printf<std::wstring>(L"?%d;%d;%dR", cursorPosition.y, cursorPosition.x, pageNumber));
     }
     else
     {
         // The standard report only returns the cursor position.
-        const auto response = wil::str_printf<std::wstring>(L"\x1b[%d;%dR", cursorPosition.y, cursorPosition.x);
-        _api.ReturnResponse(response);
+        _ReturnCsiResponse(wil::str_printf<std::wstring>(L"%d;%dR", cursorPosition.y, cursorPosition.x));
     }
 }
 
@@ -1619,8 +1616,7 @@ 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);
+    _ReturnCsiResponse(wil::str_printf<std::wstring>(L"%zu*{", spaceInBytes / 16));
 }
 
 // Routine Description:
@@ -1633,8 +1629,7 @@ 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);
+    _ReturnDcsResponse(wil::str_printf<std::wstring>(L"%d!~%04X", requestId, checksum));
 }
 
 // Routine Description:
@@ -1732,7 +1727,7 @@ void AdaptDispatch::RequestDisplayedExtent()
     const auto height = page.Viewport().height();
     const auto left = page.XPanOffset() + 1;
     const auto top = page.YPanOffset() + 1;
-    _api.ReturnResponse(fmt::format(FMT_COMPILE(L"\033[{};{};{};{};{}\"w"), height, width, left, top, page.Number()));
+    _ReturnCsiResponse(fmt::format(FMT_COMPILE(L"{};{};{};{};{}\"w"), height, width, left, top, page.Number()));
 }
 
 // Routine Description:
@@ -2068,7 +2063,7 @@ void AdaptDispatch::RequestMode(const DispatchTypes::ModeParams param)
         prefix = L"?";
     }
 
-    _api.ReturnResponse(fmt::format(FMT_COMPILE(L"\x1b[{}{};{}$y"), prefix, mode, state));
+    _ReturnCsiResponse(fmt::format(FMT_COMPILE(L"{}{};{}$y"), prefix, mode, state));
 }
 
 // - DECKPAM, DECKPNM - Sets the keypad input mode to either Application mode or Numeric mode (true, false respectively)
@@ -2785,22 +2780,15 @@ void AdaptDispatch::_InitTabStopsForWidth(const VTInt width)
 // - codingSystem - The coding system that will be selected.
 void AdaptDispatch::DesignateCodingSystem(const VTID codingSystem)
 {
-    // If we haven't previously saved the initial code page, do so now.
-    // This will be used to restore the code page in response to a reset.
-    if (!_initialCodePage.has_value())
-    {
-        _initialCodePage = _api.GetConsoleOutputCP();
-    }
-
     switch (codingSystem)
     {
     case DispatchTypes::CodingSystem::ISO2022:
-        _api.SetConsoleOutputCP(28591);
+        _api.SetCodePage(28591);
         AcceptC1Controls(true);
         _termOutput.EnableGrTranslation(true);
         break;
     case DispatchTypes::CodingSystem::UTF8:
-        _api.SetConsoleOutputCP(CP_UTF8);
+        _api.SetCodePage(CP_UTF8);
         AcceptC1Controls(false);
         _termOutput.EnableGrTranslation(false);
         break;
@@ -2871,6 +2859,24 @@ void AdaptDispatch::AcceptC1Controls(const bool enabled)
     _api.GetStateMachine().SetParserMode(StateMachine::Mode::AcceptC1, enabled);
 }
 
+//Routine Description:
+// S8C1T/S7C1T - Enable or disable the sending of C1 controls in key sequences
+//   and query responses. When this is enabled, C1 controls are sent as a single
+//   codepoint. When disabled, they're sent as a two character escape sequence.
+//Arguments:
+// - enabled - true to send C1 controls, false to send escape sequences.
+void AdaptDispatch::SendC1Controls(const bool enabled)
+{
+    // If this is an attempt to enable C1 controls, the input code page must be
+    // one of the DOCS choices (UTF-8 or ISO-8859-1), otherwise there's a risk
+    // that those controls won't have a valid encoding.
+    const auto codepage = _api.GetInputCodePage();
+    if (enabled == false || codepage == CP_UTF8 || codepage == 28591)
+    {
+        _terminalInput.SetInputMode(TerminalInput::Mode::SendC1, enabled);
+    }
+}
+
 //Routine Description:
 // ACS - Announces the ANSI conformance level for subsequent data exchange.
 //  This requires certain character sets to be mapped into the terminal's
@@ -3003,13 +3009,11 @@ void AdaptDispatch::HardReset()
 
     // Completely reset the TerminalOutput state.
     _termOutput = {};
-    if (_initialCodePage.has_value())
-    {
-        // Restore initial code page if previously changed by a DOCS sequence.
-        _api.SetConsoleOutputCP(_initialCodePage.value());
-    }
-    // Disable parsing of C1 control codes.
+    // Reset the code page to the default value.
+    _api.ResetCodePage();
+    // Disable parsing and sending of C1 control codes.
     AcceptC1Controls(false);
+    SendC1Controls(false);
 
     // Sets the SGR state to normal - this must be done before EraseInDisplay
     //      to ensure that it clears with the default background color.
@@ -3276,7 +3280,7 @@ void AdaptDispatch::RequestColorTableEntry(const size_t tableIndex)
     {
         const til::color c{ color };
         // Scale values up to match xterm's 16-bit color report format.
-        _api.ReturnResponse(fmt::format(FMT_COMPILE(L"\033]4;{};rgb:{:04x}/{:04x}/{:04x}\033\\"), tableIndex, c.r * 0x0101, c.g * 0x0101, c.b * 0x0101));
+        _ReturnOscResponse(fmt::format(FMT_COMPILE(L"4;{};rgb:{:04x}/{:04x}/{:04x}"), tableIndex, c.r * 0x0101, c.g * 0x0101, c.b * 0x0101));
     }
 }
 
@@ -3321,7 +3325,7 @@ void AdaptDispatch::RequestXtermColorResource(const size_t resource)
         {
             const til::color c{ color };
             // Scale values up to match xterm's 16-bit color report format.
-            _api.ReturnResponse(fmt::format(FMT_COMPILE(L"\033]{};rgb:{:04x}/{:04x}/{:04x}\033\\"), resource, c.r * 0x0101, c.g * 0x0101, c.b * 0x0101));
+            _ReturnOscResponse(fmt::format(FMT_COMPILE(L"{};rgb:{:04x}/{:04x}/{:04x}"), resource, c.r * 0x0101, c.g * 0x0101, c.b * 0x0101));
         }
     }
 }
@@ -3377,7 +3381,7 @@ void AdaptDispatch::WindowManipulation(const DispatchTypes::WindowManipulationTy
 
     const auto reportSize = [&](const auto size) {
         const auto reportType = function - 10;
-        _api.ReturnResponse(fmt::format(FMT_COMPILE(L"\033[{};{};{}t"), reportType, size.height, size.width));
+        _ReturnCsiResponse(fmt::format(FMT_COMPILE(L"{};{};{}t"), reportType, size.height, size.width));
     };
 
     switch (function)
@@ -3844,7 +3848,7 @@ void AdaptDispatch::RequestUserPreferenceCharset()
 {
     const auto size = _termOutput.GetUserPreferenceCharsetSize();
     const auto id = _termOutput.GetUserPreferenceCharsetId();
-    _api.ReturnResponse(fmt::format(FMT_COMPILE(L"\033P{}!u{}\033\\"), (size == 96 ? 1 : 0), id));
+    _ReturnDcsResponse(fmt::format(FMT_COMPILE(L"{}!u{}"), (size == 96 ? 1 : 0), id));
 }
 
 // Method Description:
@@ -3976,9 +3980,9 @@ void AdaptDispatch::_ReportColorTable(const DispatchTypes::ColorModel colorModel
 {
     using namespace std::string_view_literals;
 
-    // A valid response always starts with DCS 2 $ s.
+    // A valid response always starts with 2 $ s.
     fmt::basic_memory_buffer<wchar_t, TextColor::TABLE_SIZE * 18> response;
-    response.append(L"\033P2$s"sv);
+    response.append(L"2$s"sv);
 
     const auto modelNumber = static_cast<int>(colorModel);
     for (size_t colorNumber = 0; colorNumber < TextColor::TABLE_SIZE; colorNumber++)
@@ -4003,9 +4007,7 @@ void AdaptDispatch::_ReportColorTable(const DispatchTypes::ColorModel colorModel
         }
     }
 
-    // An ST ends the sequence.
-    response.append(L"\033\\"sv);
-    _api.ReturnResponse({ response.data(), response.size() });
+    _ReturnDcsResponse({ response.data(), response.size() });
 }
 
 // Method Description:
@@ -4108,7 +4110,7 @@ ITermDispatch::StringHandler AdaptDispatch::RequestSetting()
                 _ReportDECACSetting(VTParameter{ parameter });
                 break;
             default:
-                _api.ReturnResponse(L"\033P0$r\033\\");
+                _ReturnDcsResponse(L"0$r");
                 break;
             }
             return false;
@@ -4147,10 +4149,10 @@ void AdaptDispatch::_ReportSGRSetting() const
 {
     using namespace std::string_view_literals;
 
-    // A valid response always starts with DCS 1 $ r.
+    // A valid response always starts with 1 $ r.
     // Then the '0' parameter is to reset the SGR attributes to the defaults.
     fmt::basic_memory_buffer<wchar_t, 64> response;
-    response.append(L"\033P1$r0"sv);
+    response.append(L"1$r0"sv);
 
     const auto& attr = _pages.ActivePage().Attributes();
     const auto ulStyle = attr.GetUnderlineStyle();
@@ -4202,9 +4204,9 @@ void AdaptDispatch::_ReportSGRSetting() const
     addColor(40, attr.GetBackground());
     addColor(50, attr.GetUnderlineColor());
 
-    // The 'm' indicates this is an SGR response, and ST ends the sequence.
-    response.append(L"m\033\\"sv);
-    _api.ReturnResponse({ response.data(), response.size() });
+    // The 'm' indicates this is an SGR response.
+    response.append(L"m"sv);
+    _ReturnDcsResponse({ response.data(), response.size() });
 }
 
 // Method Description:
@@ -4217,10 +4219,9 @@ void AdaptDispatch::_ReportDECSTBMSetting()
 {
     const auto page = _pages.ActivePage();
     const auto [marginTop, marginBottom] = _GetVerticalMargins(page, false);
-    // A valid response always starts with DCS 1 $ r, the 'r' indicates this
-    // is a DECSTBM response, and ST ends the sequence.
+    // A valid response always starts with 1 $ r and the final 'r' indicates this is a DECSTBM response.
     // VT origin is at 1,1 so we need to add 1 to these margins.
-    _api.ReturnResponse(fmt::format(FMT_COMPILE(L"\033P1$r{};{}r\033\\"), marginTop + 1, marginBottom + 1));
+    _ReturnDcsResponse(fmt::format(FMT_COMPILE(L"1$r{};{}r"), marginTop + 1, marginBottom + 1));
 }
 
 // Method Description:
@@ -4233,10 +4234,9 @@ void AdaptDispatch::_ReportDECSLRMSetting()
 {
     const auto pageWidth = _pages.ActivePage().Width();
     const auto [marginLeft, marginRight] = _GetHorizontalMargins(pageWidth);
-    // A valid response always starts with DCS 1 $ r, the 's' indicates this
-    // is a DECSLRM response, and ST ends the sequence.
+    // A valid response always starts with 1 $ r and the 's' indicates this is a DECSLRM response.
     // VT origin is at 1,1 so we need to add 1 to these margins.
-    _api.ReturnResponse(fmt::format(FMT_COMPILE(L"\033P1$r{};{}s\033\\"), marginLeft + 1, marginRight + 1));
+    _ReturnDcsResponse(fmt::format(FMT_COMPILE(L"1$r{};{}s"), marginLeft + 1, marginRight + 1));
 }
 
 // Method Description:
@@ -4249,26 +4249,26 @@ void AdaptDispatch::_ReportDECSCUSRSetting() const
 {
     const auto& cursor = _pages.ActivePage().Cursor();
     const auto blinking = cursor.IsBlinkingAllowed();
-    // A valid response always starts with DCS 1 $ r. This is followed by a
+    // A valid response always starts with 1 $ r. This is followed by a
     // number from 1 to 6 representing the cursor style. The ' q' indicates
-    // this is a DECSCUSR response, and ST ends the sequence.
+    // this is a DECSCUSR response.
     switch (cursor.GetType())
     {
     case CursorType::FullBox:
-        _api.ReturnResponse(blinking ? L"\033P1$r1 q\033\\" : L"\033P1$r2 q\033\\");
+        _ReturnDcsResponse(blinking ? L"1$r1 q" : L"1$r2 q");
         break;
     case CursorType::Underscore:
-        _api.ReturnResponse(blinking ? L"\033P1$r3 q\033\\" : L"\033P1$r4 q\033\\");
+        _ReturnDcsResponse(blinking ? L"1$r3 q" : L"1$r4 q");
         break;
     case CursorType::VerticalBar:
-        _api.ReturnResponse(blinking ? L"\033P1$r5 q\033\\" : L"\033P1$r6 q\033\\");
+        _ReturnDcsResponse(blinking ? L"1$r5 q" : L"1$r6 q");
         break;
     default:
         // If we have a non-standard style, this is likely because it's the
         // user's chosen default style, so we report a default value of 0.
         // That way, if an application later tries to restore the cursor with
         // the returned value, it should be reset to its original state.
-        _api.ReturnResponse(L"\033P1$r0 q\033\\");
+        _ReturnDcsResponse(L"1$r0 q");
         break;
     }
 }
@@ -4282,10 +4282,10 @@ void AdaptDispatch::_ReportDECSCUSRSetting() const
 void AdaptDispatch::_ReportDECSCASetting() const
 {
     const auto isProtected = _pages.ActivePage().Attributes().IsProtected();
-    // A valid response always starts with DCS 1 $ r. This is followed by '1' if
-    // the protected attribute is set, or '0' if not. The '"q' indicates this is
-    // a DECSCA response, and ST ends the sequence.
-    _api.ReturnResponse(isProtected ? L"\033P1$r1\"q\033\\" : L"\033P1$r0\"q\033\\");
+    // A valid response always starts with 1 $ r. This is followed by '1' if the
+    // protected attribute is set, or '0' if not. The '"q' indicates this is a
+    // DECSCA response.
+    _ReturnDcsResponse(isProtected ? L"1$r1\"q" : L"1$r0\"q");
 }
 
 // Method Description:
@@ -4297,10 +4297,10 @@ void AdaptDispatch::_ReportDECSCASetting() const
 void AdaptDispatch::_ReportDECSACESetting() const
 {
     const auto rectangularExtent = _modes.test(Mode::RectangularChangeExtent);
-    // A valid response always starts with DCS 1 $ r. This is followed by '2' if
+    // A valid response always starts with 1 $ r. This is followed by '2' if
     // the extent is rectangular, or '1' if it's a stream. The '*x' indicates
-    // this is a DECSACE response, and ST ends the sequence.
-    _api.ReturnResponse(rectangularExtent ? L"\033P1$r2*x\033\\" : L"\033P1$r1*x\033\\");
+    // this is a DECSACE response.
+    _ReturnDcsResponse(rectangularExtent ? L"1$r2*x" : L"1$r1*x");
 }
 
 // Method Description:
@@ -4324,12 +4324,11 @@ void AdaptDispatch::_ReportDECACSetting(const VTInt itemNumber) const
         bgIndex = _renderSettings.GetColorAliasIndex(ColorAlias::FrameBackground);
         break;
     default:
-        _api.ReturnResponse(L"\033P0$r\033\\");
+        _ReturnDcsResponse(L"0$r");
         return;
     }
-    // A valid response always starts with DCS 1 $ r, the ',|' indicates this
-    // is a DECAC response, and ST ends the sequence.
-    _api.ReturnResponse(fmt::format(FMT_COMPILE(L"\033P1$r{};{};{},|\033\\"), itemNumber, fgIndex, bgIndex));
+    // A valid response always starts with 1 $ r and the ',|' indicates this is a DECAC response.
+    _ReturnDcsResponse(fmt::format(FMT_COMPILE(L"1$r{};{};{},|"), itemNumber, fgIndex, bgIndex));
 }
 
 // Routine Description:
@@ -4442,9 +4441,9 @@ void AdaptDispatch::_ReportCursorInformation()
     const auto charset2 = _termOutput.GetCharsetId(2);
     const auto charset3 = _termOutput.GetCharsetId(3);
 
-    // A valid response always starts with DCS 1 $ u and ends with ST.
+    // A valid response always starts with 1 $ u.
     const auto response = fmt::format(
-        FMT_COMPILE(L"\033P1$u{};{};{};{};{};{};{};{};{};{}{}{}{}\033\\"),
+        FMT_COMPILE(L"1$u{};{};{};{};{};{};{};{};{};{}{}{}{}"),
         cursorPosition.y,
         cursorPosition.x,
         page.Number(),
@@ -4458,7 +4457,7 @@ void AdaptDispatch::_ReportCursorInformation()
         charset1,
         charset2,
         charset3);
-    _api.ReturnResponse({ response.data(), response.size() });
+    _ReturnDcsResponse({ response.data(), response.size() });
 }
 
 // Method Description:
@@ -4623,9 +4622,9 @@ void AdaptDispatch::_ReportTabStops()
 
     using namespace std::string_view_literals;
 
-    // A valid response always starts with DCS 2 $ u.
+    // A valid response always starts with 2 $ u.
     fmt::basic_memory_buffer<wchar_t, 64> response;
-    response.append(L"\033P2$u"sv);
+    response.append(L"2$u"sv);
 
     auto need_separator = false;
     for (auto column = 0; column < width; column++)
@@ -4638,9 +4637,7 @@ void AdaptDispatch::_ReportTabStops()
         }
     }
 
-    // An ST ends the sequence.
-    response.append(L"\033\\"sv);
-    _api.ReturnResponse({ response.data(), response.size() });
+    _ReturnDcsResponse({ response.data(), response.size() });
 }
 
 // Method Description:
@@ -4685,6 +4682,26 @@ ITermDispatch::StringHandler AdaptDispatch::_RestoreTabStops()
     };
 }
 
+void AdaptDispatch::_ReturnCsiResponse(const std::wstring_view response) const
+{
+    const auto csi = _terminalInput.GetInputMode(TerminalInput::Mode::SendC1) ? L"\x9B" : L"\x1B[";
+    _api.ReturnResponse(fmt::format(FMT_COMPILE(L"{}{}"), csi, response));
+}
+
+void AdaptDispatch::_ReturnDcsResponse(const std::wstring_view response) const
+{
+    const auto dcs = _terminalInput.GetInputMode(TerminalInput::Mode::SendC1) ? L"\x90" : L"\x1BP";
+    const auto st = _terminalInput.GetInputMode(TerminalInput::Mode::SendC1) ? L"\x9C" : L"\x1B\\";
+    _api.ReturnResponse(fmt::format(FMT_COMPILE(L"{}{}{}"), dcs, response, st));
+}
+
+void AdaptDispatch::_ReturnOscResponse(const std::wstring_view response) const
+{
+    const auto osc = _terminalInput.GetInputMode(TerminalInput::Mode::SendC1) ? L"\x9D" : L"\x1B]";
+    const auto st = _terminalInput.GetInputMode(TerminalInput::Mode::SendC1) ? L"\x9C" : L"\x1B\\";
+    _api.ReturnResponse(fmt::format(FMT_COMPILE(L"{}{}{}"), osc, response, st));
+}
+
 // Routine Description:
 // - DECPS - Plays a sequence of musical notes.
 // Arguments:
diff --git a/src/terminal/adapter/adaptDispatch.hpp b/src/terminal/adapter/adaptDispatch.hpp
index 4fc85ebc92a..161e21d358d 100644
--- a/src/terminal/adapter/adaptDispatch.hpp
+++ b/src/terminal/adapter/adaptDispatch.hpp
@@ -121,6 +121,7 @@ namespace Microsoft::Console::VirtualTerminal
         void LockingShiftRight(const VTInt gsetNumber) override; // LS1R, LS2R, LS3R
         void SingleShift(const VTInt gsetNumber) noexcept override; // SS2, SS3
         void AcceptC1Controls(const bool enabled) override; // DECAC1
+        void SendC1Controls(const bool enabled) override; // S8C1T, S7C1T
         void AnnounceCodeStructure(const VTInt ansiLevel) override; // ACS
         void SoftReset() override; // DECSTR
         void HardReset() override; // RIS
@@ -289,6 +290,10 @@ namespace Microsoft::Console::VirtualTerminal
         void _ReportTabStops();
         StringHandler _RestoreTabStops();
 
+        void _ReturnCsiResponse(const std::wstring_view response) const;
+        void _ReturnDcsResponse(const std::wstring_view response) const;
+        void _ReturnOscResponse(const std::wstring_view response) const;
+
         std::vector<uint8_t> _tabStopColumns;
         bool _initDefaultTabStops = true;
 
@@ -302,7 +307,6 @@ namespace Microsoft::Console::VirtualTerminal
         std::shared_ptr<SixelParser> _sixelParser;
         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
         // one for the main buffer (at index 0), and another for the alt buffer
diff --git a/src/terminal/adapter/termDispatch.hpp b/src/terminal/adapter/termDispatch.hpp
index bb12dc32880..bd91d38d7e9 100644
--- a/src/terminal/adapter/termDispatch.hpp
+++ b/src/terminal/adapter/termDispatch.hpp
@@ -115,6 +115,7 @@ class Microsoft::Console::VirtualTerminal::TermDispatch : public Microsoft::Cons
     void LockingShiftRight(const VTInt /*gsetNumber*/) override {} // LS1R, LS2R, LS3R
     void SingleShift(const VTInt /*gsetNumber*/) override {} // SS2, SS3
     void AcceptC1Controls(const bool /*enabled*/) override {} // DECAC1
+    void SendC1Controls(const bool /*enabled*/) override {} // S8C1T, S7C1T
     void AnnounceCodeStructure(const VTInt /*ansiLevel*/) override {} // ACS
 
     void SoftReset() override {} // DECSTR
diff --git a/src/terminal/adapter/ut_adapter/adapterTest.cpp b/src/terminal/adapter/ut_adapter/adapterTest.cpp
index ab2e08d3b85..5fab90f3d39 100644
--- a/src/terminal/adapter/ut_adapter/adapterTest.cpp
+++ b/src/terminal/adapter/ut_adapter/adapterTest.cpp
@@ -158,17 +158,28 @@ class TestGetSet final : public ITerminalApi
         return true;
     }
 
-    void SetConsoleOutputCP(const unsigned int codepage) override
+    void SetCodePage(const unsigned int codepage) override
     {
-        Log::Comment(L"SetConsoleOutputCP MOCK called...");
-        THROW_HR_IF(E_FAIL, !_setConsoleOutputCPResult);
-        VERIFY_ARE_EQUAL(_expectedOutputCP, codepage);
+        Log::Comment(L"SetCodePage MOCK called...");
+        THROW_HR_IF(E_FAIL, !_setCodePageResult);
+        VERIFY_ARE_EQUAL(_expectedCodePage, codepage);
     }
 
-    unsigned int GetConsoleOutputCP() const override
+    void ResetCodePage() override
     {
-        Log::Comment(L"GetConsoleOutputCP MOCK called...");
-        return _expectedOutputCP;
+        Log::Comment(L"ResetCodePage MOCK called...");
+    }
+
+    unsigned int GetOutputCodePage() const override
+    {
+        Log::Comment(L"GetOutputCodePage MOCK called...");
+        return _expectedCodePage;
+    }
+
+    unsigned int GetInputCodePage() const override
+    {
+        Log::Comment(L"GetInputCodePage MOCK called...");
+        return _expectedCodePage;
     }
 
     void CopyToClipboard(const wil::zwstring_view /*content*/)
@@ -367,7 +378,7 @@ class TestGetSet final : public ITerminalApi
     til::point _expectedCursorPos;
 
     TextAttribute _expectedAttribute = {};
-    unsigned int _expectedOutputCP = 0;
+    unsigned int _expectedCodePage = 0;
     bool _isPty = false;
 
     bool _returnResponseResult = false;
@@ -376,8 +387,7 @@ class TestGetSet final : public ITerminalApi
 
     bool _setWindowTitleResult = false;
     std::wstring_view _expectedWindowTitle{};
-    bool _setConsoleOutputCPResult = false;
-    bool _getConsoleOutputCPResult = false;
+    bool _setCodePageResult = false;
     bool _expectedShowWindow = false;
 
     std::wstring _expectedMenuJson{};
@@ -3529,15 +3539,15 @@ class AdapterTest
 
         Log::Comment(L"3. Designate ISO-2022 coding system");
         // Code page should be set to ISO-8859-1 and C1 parsing enabled
-        _testGetSet->_setConsoleOutputCPResult = true;
-        _testGetSet->_expectedOutputCP = 28591;
+        _testGetSet->_setCodePageResult = true;
+        _testGetSet->_expectedCodePage = 28591;
         _pDispatch->DesignateCodingSystem(DispatchTypes::CodingSystem::ISO2022);
         VERIFY_IS_TRUE(_stateMachine->GetParserMode(StateMachine::Mode::AcceptC1));
 
         Log::Comment(L"4. Designate UTF-8 coding system");
         // Code page should be set to UTF-8 and C1 parsing disabled
-        _testGetSet->_setConsoleOutputCPResult = true;
-        _testGetSet->_expectedOutputCP = CP_UTF8;
+        _testGetSet->_setCodePageResult = true;
+        _testGetSet->_expectedCodePage = CP_UTF8;
         _pDispatch->DesignateCodingSystem(DispatchTypes::CodingSystem::UTF8);
         VERIFY_IS_FALSE(_stateMachine->GetParserMode(StateMachine::Mode::AcceptC1));
     }
@@ -3962,6 +3972,39 @@ class AdapterTest
         _pDispatch->PagePositionAbsolute(1);
     }
 
+    TEST_METHOD(SendC1ControlTest)
+    {
+        const auto S7C1T = L"\033 F";
+        const auto S8C1T = L"\033 G";
+
+        _testGetSet->PrepData();
+        _testGetSet->_expectedCodePage = CP_UTF8;
+
+        Log::Comment(L"Generating reports with C1 control sequences");
+        _stateMachine->ProcessString(S8C1T);
+
+        _pDispatch->SecondaryDeviceAttributes();
+        _testGetSet->ValidateInputEvent(L"\x9b>0;10;1c");
+
+        _pDispatch->TertiaryDeviceAttributes();
+        _testGetSet->ValidateInputEvent(L"\x90!|00000000\x9c");
+
+        _pDispatch->RequestColorTableEntry(0);
+        _testGetSet->ValidateInputEvent(L"\x9d\x34;0;rgb:0c0c/0c0c/0c0c\x9c");
+
+        Log::Comment(L"Generating reports with 7-bit escape sequence");
+        _stateMachine->ProcessString(S7C1T);
+
+        _pDispatch->SecondaryDeviceAttributes();
+        _testGetSet->ValidateInputEvent(L"\x1b[>0;10;1c");
+
+        _pDispatch->TertiaryDeviceAttributes();
+        _testGetSet->ValidateInputEvent(L"\x1bP!|00000000\x1b\\");
+
+        _pDispatch->RequestColorTableEntry(0);
+        _testGetSet->ValidateInputEvent(L"\x1b]4;0;rgb:0c0c/0c0c/0c0c\x1b\\");
+    }
+
 private:
     TerminalInput _terminalInput;
     std::unique_ptr<TestGetSet> _testGetSet;
diff --git a/src/terminal/adapter/ut_adapter/inputTest.cpp b/src/terminal/adapter/ut_adapter/inputTest.cpp
index d3c4c3d0d06..ab893be7724 100644
--- a/src/terminal/adapter/ut_adapter/inputTest.cpp
+++ b/src/terminal/adapter/ut_adapter/inputTest.cpp
@@ -36,6 +36,7 @@ class Microsoft::Console::VirtualTerminal::InputTest
     TEST_METHOD(CtrlNumTest);
     TEST_METHOD(BackarrowKeyModeTest);
     TEST_METHOD(AutoRepeatModeTest);
+    TEST_METHOD(SendC1ControlTest);
 
     wchar_t GetModifierChar(const bool fShift, const bool fAlt, const bool fCtrl)
     {
@@ -790,3 +791,20 @@ void InputTest::AutoRepeatModeTest()
     VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"A"), input.HandleKey(down));
     VERIFY_ARE_EQUAL(TerminalInput::MakeOutput({}), input.HandleKey(up));
 }
+
+void InputTest::SendC1ControlTest()
+{
+    TerminalInput input;
+
+    Log::Comment(L"Sending keys with C1 control sequences");
+    input.SetInputMode(TerminalInput::Mode::SendC1, true);
+
+    TestKey(TerminalInput::MakeOutput(L"\x9bH"), input, 0, VK_HOME);
+    TestKey(TerminalInput::MakeOutput(L"\x8fP"), input, 0, VK_F1);
+
+    Log::Comment(L"Sending keys with 7-bit escape sequence");
+    input.SetInputMode(TerminalInput::Mode::SendC1, false);
+
+    TestKey(TerminalInput::MakeOutput(L"\x1b[H"), input, 0, VK_HOME);
+    TestKey(TerminalInput::MakeOutput(L"\x1bOP"), input, 0, VK_F1);
+}
diff --git a/src/terminal/input/terminalInput.cpp b/src/terminal/input/terminalInput.cpp
index 7c09ef46155..ac0795bdc41 100644
--- a/src/terminal/input/terminalInput.cpp
+++ b/src/terminal/input/terminalInput.cpp
@@ -53,7 +53,7 @@ void TerminalInput::SetInputMode(const Mode mode, const bool enabled) noexcept
 
     // If we've changed one of the modes that alter the VT input sequences,
     // we'll need to regenerate our keyboard map.
-    static constexpr auto keyMapModes = til::enumset<Mode>{ Mode::LineFeed, Mode::Ansi, Mode::Keypad, Mode::CursorKey, Mode::BackarrowKey };
+    static constexpr auto keyMapModes = til::enumset<Mode>{ Mode::LineFeed, Mode::Ansi, Mode::Keypad, Mode::CursorKey, Mode::BackarrowKey, Mode::SendC1 };
     if (keyMapModes.test(mode))
     {
         _initKeyboardMap();
@@ -353,6 +353,19 @@ try
 
     _keyMap.clear();
 
+    // The CSI and SS3 introducers are C1 control codes, which can either be
+    // sent as a single codepoint, or as a two character escape sequence.
+    if (_inputMode.test(Mode::SendC1))
+    {
+        _csi = L"\x9B";
+        _ss3 = L"\x8F";
+    }
+    else
+    {
+        _csi = L"\x1B[";
+        _ss3 = L"\x1BO";
+    }
+
     // PAUSE doesn't have a VT mapping, but traditionally we've mapped it to ^Z,
     // regardless of modifiers.
     defineKeyWithUnusedModifiers(VK_PAUSE, L"\x1A"s);
diff --git a/src/terminal/input/terminalInput.hpp b/src/terminal/input/terminalInput.hpp
index e8ae53267e2..40488269569 100644
--- a/src/terminal/input/terminalInput.hpp
+++ b/src/terminal/input/terminalInput.hpp
@@ -33,6 +33,7 @@ namespace Microsoft::Console::VirtualTerminal
             CursorKey,
             BackarrowKey,
             Win32,
+            SendC1,
 
             Utf8MouseEncoding,
             SgrMouseEncoding,
@@ -80,10 +81,8 @@ namespace Microsoft::Console::VirtualTerminal
         til::enumset<Mode> _inputMode{ Mode::Ansi, Mode::AutoRepeat, Mode::AlternateScroll };
         bool _forceDisableWin32InputMode{ false };
 
-        // In the future, if we add support for "8-bit" input mode, these prefixes
-        // will sometimes be replaced with equivalent C1 control characters.
-        static constexpr auto _csi = L"\x1B[";
-        static constexpr auto _ss3 = L"\x1BO";
+        const wchar_t* _csi = L"\x1B[";
+        const wchar_t* _ss3 = L"\x1BO";
 
         void _initKeyboardMap() noexcept;
         DWORD _trackControlKeyState(const KEY_EVENT_RECORD& key);
@@ -108,9 +107,9 @@ namespace Microsoft::Console::VirtualTerminal
 #pragma endregion
 
 #pragma region MouseInput
-        [[nodiscard]] static OutputType _GenerateDefaultSequence(til::point position, unsigned int button, bool isHover, short modifierKeyState, short delta);
-        [[nodiscard]] static OutputType _GenerateUtf8Sequence(til::point position, unsigned int button, bool isHover, short modifierKeyState, short delta);
-        [[nodiscard]] static OutputType _GenerateSGRSequence(til::point position, unsigned int button, bool isDown, bool isHover, short modifierKeyState, short delta);
+        [[nodiscard]] OutputType _GenerateDefaultSequence(til::point position, unsigned int button, bool isHover, short modifierKeyState, short delta);
+        [[nodiscard]] OutputType _GenerateUtf8Sequence(til::point position, unsigned int button, bool isHover, short modifierKeyState, short delta);
+        [[nodiscard]] OutputType _GenerateSGRSequence(til::point position, unsigned int button, bool isDown, bool isHover, short modifierKeyState, short delta);
 
         [[nodiscard]] OutputType _makeAlternateScrollOutput(short delta) const;
 
diff --git a/src/terminal/parser/OutputStateMachineEngine.cpp b/src/terminal/parser/OutputStateMachineEngine.cpp
index 5339c29bdf9..edb53e100d0 100644
--- a/src/terminal/parser/OutputStateMachineEngine.cpp
+++ b/src/terminal/parser/OutputStateMachineEngine.cpp
@@ -256,6 +256,12 @@ bool OutputStateMachineEngine::ActionEscDispatch(const VTID id)
     case EscActionCodes::DECAC1_AcceptC1Controls:
         _dispatch->AcceptC1Controls(true);
         break;
+    case EscActionCodes::S7C1T_Send7bitC1Controls:
+        _dispatch->SendC1Controls(false);
+        break;
+    case EscActionCodes::S8C1T_Send8bitC1Controls:
+        _dispatch->SendC1Controls(true);
+        break;
     case EscActionCodes::ACS_AnsiLevel1:
         _dispatch->AnnounceCodeStructure(1);
         break;
diff --git a/src/terminal/parser/OutputStateMachineEngine.hpp b/src/terminal/parser/OutputStateMachineEngine.hpp
index ad594c269cd..ab41e066cfa 100644
--- a/src/terminal/parser/OutputStateMachineEngine.hpp
+++ b/src/terminal/parser/OutputStateMachineEngine.hpp
@@ -77,6 +77,8 @@ namespace Microsoft::Console::VirtualTerminal
             LS2R_LockingShift = VTID("}"),
             LS3R_LockingShift = VTID("|"),
             DECAC1_AcceptC1Controls = VTID(" 7"),
+            S7C1T_Send7bitC1Controls = VTID(" F"),
+            S8C1T_Send8bitC1Controls = VTID(" G"),
             ACS_AnsiLevel1 = VTID(" L"),
             ACS_AnsiLevel2 = VTID(" M"),
             ACS_AnsiLevel3 = VTID(" N"),
diff --git a/src/terminal/parser/stateMachine.cpp b/src/terminal/parser/stateMachine.cpp
index 65e7460e9b8..12954512bdb 100644
--- a/src/terminal/parser/stateMachine.cpp
+++ b/src/terminal/parser/stateMachine.cpp
@@ -25,6 +25,9 @@ StateMachine::StateMachine(std::unique_ptr<IStateMachineEngine> engine, const bo
     _oscString{},
     _cachedSequence{ std::nullopt }
 {
+    // The state machine must always accept C1 controls for the input engine,
+    // otherwise it won't work when the ConPTY terminal has S8C1T enabled.
+    _parserMode.set(Mode::AcceptC1, _isEngineForInput);
     _ActionClear();
 }
 


Test patch: