Repository: microsoft/terminal (language: cpp)

Issue:
Keyboard shortcuts with crtl + alt gr (like crtl + \) on a German keyboard are not working
### Windows Terminal version

1.20.231013001-experimental or 1.17.11461.0

### Windows build number

10.0.19045.3570

### Other Software

GNU nano 6.2-1 on Ubuntu 22.04.3 on WSL (also happens with nano from Git for Windows 2.42.0.2 and via SSH)

### Steps to reproduce

1. Ensure your keyboard layout is set to German.
2. Start WSL
3. Open nano.
4. Press Strg + Alt Gr + ß  to get Crtl + \  (On a US keyboard with German Layout press: Crtl + Right Alt + -) 

### Expected Behavior

Nano opens the replace dialog.

### Actual Behavior

A \ appears.

Note, that on a German keyboard layout \ is only accessible via Alt Gr + ß or Crtl + Alt + ß. To enter shortcuts like Ctrl + \ a third key is required. 
The issue also happens with conshost.exe.

showkey -a shows
\        92 0134 0x5c
but
^\      28 0034 0x1b
would be expected.


Discussion / hints:
I believe this is because Windows treats <kbd>Ctrl</kbd>+<kbd>Alt</kbd> as an alias for <kbd>AltGr</kbd>.[^ref] As a result, when you press the <kbd>AltGr</kbd> key, what the application actually receives is _two_ keypresses: a <kbd>Ctrl</kbd> key followed by an <kbd>AltGr</kbd> key. That makes it indistinguishable from when you manually press <kbd>Ctrl</kbd> along with <kbd>AltGr</kbd>.

That said, it is still possible for you to generate this shortcut on a German keyboard. All the controls that rely on punctuation keys are also accessible via the key combinations <kbd>Ctrl</kbd>+<kbd>3</kbd> through <kbd>Ctrl</kbd>+<kbd>7</kbd>. In particular, <kbd>Ctrl</kbd>+<kbd>4</kbd> is the equivalent of <kbd>Ctrl</kbd>+<kbd>\\</kbd>.

[^ref]: ["Why Ctrl+Alt shouldn’t be used as a shortcut modifier"](https://devblogs.microsoft.com/oldnewthing/20040329-00/?p=40003)
Thank you for reporting this!

This will require some further research into other terminal emulators to determine how they are handling the difference between AltGr and Alt + Ctrl. For now, hopefully the suggestion that j4james provided above is sufficient!
I've just tested a handful of terminals now, and the behavior was mostly what I expected. The Linux terminals worked correctly, i.e. <kbd>Ctrl</kbd>+<kbd>AltGr</kbd>+<kbd>ß</kbd> generated <kbd>Ctrl</kbd>+<kbd>\\</kbd>. Most Windows terminals behaved the same as we do, i.e. both <kbd>AltGr</kbd>+<kbd>ß</kbd> and <kbd>Ctrl</kbd>+<kbd>AltGr</kbd>+<kbd>ß</kbd> generate <kbd>\\</kbd>. In one case I got <kbd>Alt</kbd>+<kbd>\\</kbd> for both key combos, but that's also understandable for a Windows terminal.

The big surprise, though, was mintty, which somehow managed to get this right. That suggests it definitely must be possible to detect the <kbd>Ctrl</kbd> key independently of <kbd>AltGr</kbd> in Windows, but I have no idea how. As far as I can see, in both the conhost and WT keyboard handlers, we are receiving keydown events for both a <kbd>Ctrl</kbd> key and a <kbd>AltGr</kbd> key whenever <kbd>AltGr</kbd> is pressed. But maybe there is some lower level API which works better. Or maybe I'm just looking at it wrong.


Fix patch:
diff --git a/.github/actions/spelling/expect/expect.txt b/.github/actions/spelling/expect/expect.txt
index 6ae7a458582..92c237cdfdb 100644
--- a/.github/actions/spelling/expect/expect.txt
+++ b/.github/actions/spelling/expect/expect.txt
@@ -319,6 +319,7 @@ ctlseqs
 CTRLEVENT
 CTRLFREQUENCY
 CTRLKEYSHORTCUTS
+Ctrls
 CTRLVOLUME
 Ctxt
 CUF
@@ -401,6 +402,7 @@ DECECM
 DECEKBD
 DECERA
 DECFI
+DECFNK
 DECFRA
 DECIC
 DECID
diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp
index 4c59d027a52..0c9ccc2b18c 100644
--- a/src/cascadia/TerminalControl/TermControl.cpp
+++ b/src/cascadia/TerminalControl/TermControl.cpp
@@ -1345,7 +1345,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
         // Alt, so we should be ignoring the individual keydowns. The character
         // will be sent through the TSFInputControl. See GH#1401 for more
         // details
-        if (modifiers.IsAltPressed() &&
+        if (modifiers.IsAltPressed() && !modifiers.IsCtrlPressed() &&
             (vkey >= VK_NUMPAD0 && vkey <= VK_NUMPAD9))
         {
             e.Handled(true);
diff --git a/src/terminal/adapter/ut_adapter/inputTest.cpp b/src/terminal/adapter/ut_adapter/inputTest.cpp
index 581f03ed925..d3c4c3d0d06 100644
--- a/src/terminal/adapter/ut_adapter/inputTest.cpp
+++ b/src/terminal/adapter/ut_adapter/inputTest.cpp
@@ -66,6 +66,32 @@ class Microsoft::Console::VirtualTerminal::InputTest
     {
         return WI_IsFlagSet(uiKeystate, SHIFT_PRESSED);
     }
+
+    static void TestKey(const TerminalInput::OutputType& expected, TerminalInput& input, const unsigned int uiKeystate, const BYTE vkey, const wchar_t wch = 0)
+    {
+        Log::Comment(NoThrowString().Format(L"Testing key, state =0x%x, 0x%x", vkey, uiKeystate));
+
+        INPUT_RECORD irTest = { 0 };
+        irTest.EventType = KEY_EVENT;
+        irTest.Event.KeyEvent.wRepeatCount = 1;
+        irTest.Event.KeyEvent.bKeyDown = TRUE;
+
+        // If we want to test a key with the Right Alt modifier, we must generate
+        // an event for the Alt key first, otherwise the modifier will be dropped.
+        if (WI_IsFlagSet(uiKeystate, RIGHT_ALT_PRESSED))
+        {
+            irTest.Event.KeyEvent.wVirtualKeyCode = VK_MENU;
+            irTest.Event.KeyEvent.dwControlKeyState = uiKeystate | ENHANCED_KEY;
+            VERIFY_ARE_EQUAL(TerminalInput::MakeOutput({}), input.HandleKey(irTest));
+        }
+
+        irTest.Event.KeyEvent.dwControlKeyState = uiKeystate;
+        irTest.Event.KeyEvent.wVirtualKeyCode = vkey;
+        irTest.Event.KeyEvent.uChar.UnicodeChar = wch;
+
+        // Send key into object (will trigger callback and verification)
+        VERIFY_ARE_EQUAL(expected, input.HandleKey(irTest), L"Verify key was handled if it should have been.");
+    }
 };
 
 void InputTest::TerminalInputTests()
@@ -86,7 +112,8 @@ void InputTest::TerminalInputTests()
         irTest.Event.KeyEvent.bKeyDown = TRUE;
         irTest.Event.KeyEvent.uChar.UnicodeChar = LOWORD(OneCoreSafeMapVirtualKeyW(vkey, MAPVK_VK_TO_CHAR));
 
-        TerminalInput::OutputType expected;
+        // Unhandled keys are expected to return an empty string.
+        TerminalInput::OutputType expected = TerminalInput::MakeOutput({});
         switch (vkey)
         {
         case VK_TAB:
@@ -113,6 +140,9 @@ void InputTest::TerminalInputTests()
         case VK_LEFT:
             expected = TerminalInput::MakeOutput(L"\x1b[D");
             break;
+        case VK_CLEAR:
+            expected = TerminalInput::MakeOutput(L"\x1b[E");
+            break;
         case VK_HOME:
             expected = TerminalInput::MakeOutput(L"\x1b[H");
             break;
@@ -167,11 +197,36 @@ void InputTest::TerminalInputTests()
         case VK_F12:
             expected = TerminalInput::MakeOutput(L"\x1b[24~");
             break;
+        case VK_F13:
+            expected = TerminalInput::MakeOutput(L"\x1b[25~");
+            break;
+        case VK_F14:
+            expected = TerminalInput::MakeOutput(L"\x1b[26~");
+            break;
+        case VK_F15:
+            expected = TerminalInput::MakeOutput(L"\x1b[28~");
+            break;
+        case VK_F16:
+            expected = TerminalInput::MakeOutput(L"\x1b[29~");
+            break;
+        case VK_F17:
+            expected = TerminalInput::MakeOutput(L"\x1b[31~");
+            break;
+        case VK_F18:
+            expected = TerminalInput::MakeOutput(L"\x1b[32~");
+            break;
+        case VK_F19:
+            expected = TerminalInput::MakeOutput(L"\x1b[33~");
+            break;
+        case VK_F20:
+            expected = TerminalInput::MakeOutput(L"\x1b[34~");
+            break;
         case VK_CANCEL:
             expected = TerminalInput::MakeOutput(L"\x3");
             break;
         default:
-            if (irTest.Event.KeyEvent.uChar.UnicodeChar != 0)
+            const auto synthesizedKeyPress = vkey == VK_PACKET || vkey == 0;
+            if (irTest.Event.KeyEvent.uChar.UnicodeChar != 0 || synthesizedKeyPress)
             {
                 expected = TerminalInput::MakeOutput({ &irTest.Event.KeyEvent.uChar.UnicodeChar, 1 });
             }
@@ -194,7 +249,7 @@ void InputTest::TerminalInputTests()
         irTest.Event.KeyEvent.bKeyDown = FALSE;
 
         // Send key into object (will trigger callback and verification)
-        VERIFY_ARE_EQUAL(TerminalInput::MakeUnhandled(), input.HandleKey(irTest), L"Verify key was NOT handled.");
+        VERIFY_ARE_EQUAL(TerminalInput::MakeOutput({}), input.HandleKey(irTest), L"Verify output is blank.");
     }
 
     Log::Comment(L"Verify other types of events are not handled/intercepted.");
@@ -259,13 +314,7 @@ void InputTest::TerminalInputModifierKeyTests()
 
         auto fExpectedKeyHandled = true;
         auto fModifySequence = false;
-        INPUT_RECORD irTest = { 0 };
-        irTest.EventType = KEY_EVENT;
-        irTest.Event.KeyEvent.dwControlKeyState = uiKeystate;
-        irTest.Event.KeyEvent.wRepeatCount = 1;
-        irTest.Event.KeyEvent.wVirtualKeyCode = vkey;
-        irTest.Event.KeyEvent.bKeyDown = TRUE;
-        irTest.Event.KeyEvent.uChar.UnicodeChar = LOWORD(OneCoreSafeMapVirtualKeyW(vkey, MAPVK_VK_TO_CHAR));
+        wchar_t ch = LOWORD(OneCoreSafeMapVirtualKeyW(vkey, MAPVK_VK_TO_CHAR));
 
         if (ControlPressed(uiKeystate))
         {
@@ -282,16 +331,13 @@ void InputTest::TerminalInputModifierKeyTests()
             }
         }
 
-        TerminalInput::OutputType expected;
+        // Unhandled keys are expected to return an empty string.
+        TerminalInput::OutputType expected = TerminalInput::MakeOutput({});
         switch (vkey)
         {
         case VK_BACK:
             // Backspace is kinda different from other keys - we'll handle in another test.
-        case VK_OEM_2:
-            // VK_OEM_2 is typically the '/?' key
             continue;
-            // expected = TerminalInput::MakeOutput(L"\x7f");
-            break;
         case VK_PAUSE:
             expected = TerminalInput::MakeOutput(L"\x1a");
             break;
@@ -311,6 +357,10 @@ void InputTest::TerminalInputModifierKeyTests()
             fModifySequence = true;
             expected = TerminalInput::MakeOutput(L"\x1b[1;mD");
             break;
+        case VK_CLEAR:
+            fModifySequence = true;
+            expected = TerminalInput::MakeOutput(L"\x1b[1;mE");
+            break;
         case VK_HOME:
             fModifySequence = true;
             expected = TerminalInput::MakeOutput(L"\x1b[1;mH");
@@ -383,6 +433,55 @@ void InputTest::TerminalInputModifierKeyTests()
             fModifySequence = true;
             expected = TerminalInput::MakeOutput(L"\x1b[24;m~");
             break;
+        case VK_F13:
+            fModifySequence = true;
+            expected = TerminalInput::MakeOutput(L"\x1b[25;m~");
+            break;
+        case VK_F14:
+            fModifySequence = true;
+            expected = TerminalInput::MakeOutput(L"\x1b[26;m~");
+            break;
+        case VK_F15:
+            fModifySequence = true;
+            expected = TerminalInput::MakeOutput(L"\x1b[28;m~");
+            break;
+        case VK_F16:
+            fModifySequence = true;
+            expected = TerminalInput::MakeOutput(L"\x1b[29;m~");
+            break;
+        case VK_F17:
+            fModifySequence = true;
+            expected = TerminalInput::MakeOutput(L"\x1b[31;m~");
+            break;
+        case VK_F18:
+            fModifySequence = true;
+            expected = TerminalInput::MakeOutput(L"\x1b[32;m~");
+            break;
+        case VK_F19:
+            fModifySequence = true;
+            expected = TerminalInput::MakeOutput(L"\x1b[33;m~");
+            break;
+        case VK_F20:
+            fModifySequence = true;
+            expected = TerminalInput::MakeOutput(L"\x1b[34;m~");
+            break;
+        case VK_PACKET:
+        case 0:
+            // VK_PACKET and 0 virtual keys are used for synthesized key presses.
+            expected = TerminalInput::MakeOutput({ &ch, 1 });
+            break;
+        case VK_RETURN:
+            if (AltPressed(uiKeystate))
+            {
+                const auto str = ControlPressed(uiKeystate) ? L"\x1b\n" : L"\x1b\r";
+                expected = TerminalInput::MakeOutput(str);
+            }
+            else
+            {
+                const auto str = ControlPressed(uiKeystate) ? L"\n" : L"\r";
+                expected = TerminalInput::MakeOutput(str);
+            }
+            break;
         case VK_TAB:
             if (AltPressed(uiKeystate))
             {
@@ -398,8 +497,35 @@ void InputTest::TerminalInputModifierKeyTests()
                 expected = TerminalInput::MakeOutput(L"\t");
             }
             break;
+        case VK_OEM_2:
+        case VK_OEM_3:
+        case VK_OEM_4:
+        case VK_OEM_5:
+        case VK_OEM_6:
+        case VK_OEM_102:
+            // OEM keys require special case handling when combined with a Ctrl
+            // modifier, but otherwise work the same way as regular keys.
+            if (ControlPressed(uiKeystate))
+            {
+                continue;
+            }
+            [[fallthrough]];
         default:
-            auto ch = irTest.Event.KeyEvent.uChar.UnicodeChar;
+            if (ControlPressed(uiKeystate) && (vkey >= '1' && vkey <= '9'))
+            {
+                // The C-# keys get translated into very specific control
+                // characters that don't play nicely with this test. These keys
+                // are tested in the CtrlNumTest Test instead.
+                continue;
+            }
+
+            if (vkey >= VK_NUMPAD0 && vkey <= VK_NUMPAD9)
+            {
+                // Numpad keys have the same complications as numeric keys
+                // when used with a Ctrl modifier, and with Alt they're used
+                // for Alt-Numpad composition, so it's best we skip them.
+                continue;
+            }
 
             // Alt+Key generates [0x1b, Ctrl+key] into the stream
             // Pressing the control key causes all bits but the 5 least
@@ -408,28 +534,25 @@ void InputTest::TerminalInputModifierKeyTests()
             {
                 const wchar_t buffer[2]{ L'\x1b', gsl::narrow_cast<wchar_t>(ch & 0b11111) };
                 expected = TerminalInput::MakeOutput({ &buffer[0], 2 });
+                ch = 0;
                 break;
             }
 
             // Alt+Key generates [0x1b, key] into the stream
-            if (AltPressed(uiKeystate) && !ControlPressed(uiKeystate) && ch != 0)
+            if (AltPressed(uiKeystate) && ch != 0)
             {
                 const wchar_t buffer[2]{ L'\x1b', ch };
                 expected = TerminalInput::MakeOutput({ &buffer[0], 2 });
+                if (ControlPressed(uiKeystate))
+                {
+                    ch = 0;
+                }
                 break;
             }
 
-            if (ControlPressed(uiKeystate) && (vkey >= '1' && vkey <= '9'))
-            {
-                // The C-# keys get translated into very specific control
-                // characters that don't play nicely with this test. These keys
-                // are tested in the CtrlNumTest Test instead.
-                continue;
-            }
-
             if (ch != 0)
             {
-                expected = TerminalInput::MakeOutput({ &irTest.Event.KeyEvent.uChar.UnicodeChar, 1 });
+                expected = TerminalInput::MakeOutput({ &ch, 1 });
                 break;
             }
 
@@ -446,8 +569,7 @@ void InputTest::TerminalInputModifierKeyTests()
             str[str.size() - 2] = L'1' + (fShift ? 1 : 0) + (fAlt ? 2 : 0) + (fCtrl ? 4 : 0);
         }
 
-        // Send key into object (will trigger callback and verification)
-        VERIFY_ARE_EQUAL(expected, input.HandleKey(irTest), L"Verify key was handled if it should have been.");
+        TestKey(expected, input, uiKeystate, vkey, ch);
     }
 }
 
@@ -491,22 +613,6 @@ void InputTest::TerminalInputNullKeyTests()
     VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"\x1b\0"sv), input.HandleKey(irTest), L"Verify key was handled if it should have been.");
 }
 
-static void TestKey(const TerminalInput::OutputType& expected, TerminalInput& input, const unsigned int uiKeystate, const BYTE vkey, const wchar_t wch = 0)
-{
-    Log::Comment(NoThrowString().Format(L"Testing key, state =0x%x, 0x%x", vkey, uiKeystate));
-
-    INPUT_RECORD irTest = { 0 };
-    irTest.EventType = KEY_EVENT;
-    irTest.Event.KeyEvent.dwControlKeyState = uiKeystate;
-    irTest.Event.KeyEvent.wRepeatCount = 1;
-    irTest.Event.KeyEvent.wVirtualKeyCode = vkey;
-    irTest.Event.KeyEvent.bKeyDown = TRUE;
-    irTest.Event.KeyEvent.uChar.UnicodeChar = wch;
-
-    // Send key into object (will trigger callback and verification)
-    VERIFY_ARE_EQUAL(expected, input.HandleKey(irTest), L"Verify key was handled if it should have been.");
-}
-
 void InputTest::DifferentModifiersTest()
 {
     Log::Comment(L"Starting test...");
@@ -556,9 +662,9 @@ void InputTest::DifferentModifiersTest()
     // C-/ -> C-_ -> 0x1f
     uiKeystate = LEFT_CTRL_PRESSED;
     vkey = LOBYTE(OneCoreSafeVkKeyScanW(L'/'));
-    TestKey(TerminalInput::MakeOutput(L"\x1f"), input, uiKeystate, vkey, L'/');
+    TestKey(TerminalInput::MakeOutput(L"\x1f"), input, uiKeystate, vkey);
     uiKeystate = RIGHT_CTRL_PRESSED;
-    TestKey(TerminalInput::MakeOutput(L"\x1f"), input, uiKeystate, vkey, L'/');
+    TestKey(TerminalInput::MakeOutput(L"\x1f"), input, uiKeystate, vkey);
 
     // M-/ -> ESC /
     uiKeystate = LEFT_ALT_PRESSED;
@@ -572,26 +678,26 @@ void InputTest::DifferentModifiersTest()
     Log::Comment(NoThrowString().Format(L"Checking C-?"));
     // Use SHIFT_PRESSED to force us into differentiating between '/' and '?'
     vkey = LOBYTE(OneCoreSafeVkKeyScanW(L'?'));
-    TestKey(TerminalInput::MakeOutput(L"\x7f"), input, SHIFT_PRESSED | LEFT_CTRL_PRESSED, vkey, L'?');
-    TestKey(TerminalInput::MakeOutput(L"\x7f"), input, SHIFT_PRESSED | RIGHT_CTRL_PRESSED, vkey, L'?');
+    TestKey(TerminalInput::MakeOutput(L"\x7f"), input, SHIFT_PRESSED | LEFT_CTRL_PRESSED, vkey);
+    TestKey(TerminalInput::MakeOutput(L"\x7f"), input, SHIFT_PRESSED | RIGHT_CTRL_PRESSED, vkey);
 
     // C-M-/ -> 0x1b0x1f
     Log::Comment(NoThrowString().Format(L"Checking C-M-/"));
     uiKeystate = LEFT_CTRL_PRESSED | LEFT_ALT_PRESSED;
     vkey = LOBYTE(OneCoreSafeVkKeyScanW(L'/'));
-    TestKey(TerminalInput::MakeOutput(L"\x1b\x1f"), input, LEFT_CTRL_PRESSED | LEFT_ALT_PRESSED, vkey, L'/');
-    TestKey(TerminalInput::MakeOutput(L"\x1b\x1f"), input, RIGHT_CTRL_PRESSED | LEFT_ALT_PRESSED, vkey, L'/');
+    TestKey(TerminalInput::MakeOutput(L"\x1b\x1f"), input, LEFT_CTRL_PRESSED | LEFT_ALT_PRESSED, vkey);
+    TestKey(TerminalInput::MakeOutput(L"\x1b\x1f"), input, RIGHT_CTRL_PRESSED | LEFT_ALT_PRESSED, vkey);
     // LEFT_CTRL_PRESSED | RIGHT_ALT_PRESSED is skipped because that's AltGr
-    TestKey(TerminalInput::MakeOutput(L"\x1b\x1f"), input, RIGHT_CTRL_PRESSED | RIGHT_ALT_PRESSED, vkey, L'/');
+    TestKey(TerminalInput::MakeOutput(L"\x1b\x1f"), input, RIGHT_CTRL_PRESSED | RIGHT_ALT_PRESSED, vkey);
 
     // C-M-? -> 0x1b0x7f
     Log::Comment(NoThrowString().Format(L"Checking C-M-?"));
     uiKeystate = LEFT_CTRL_PRESSED | LEFT_ALT_PRESSED;
     vkey = LOBYTE(OneCoreSafeVkKeyScanW(L'?'));
-    TestKey(TerminalInput::MakeOutput(L"\x1b\x7f"), input, SHIFT_PRESSED | LEFT_CTRL_PRESSED | LEFT_ALT_PRESSED, vkey, L'?');
-    TestKey(TerminalInput::MakeOutput(L"\x1b\x7f"), input, SHIFT_PRESSED | RIGHT_CTRL_PRESSED | LEFT_ALT_PRESSED, vkey, L'?');
+    TestKey(TerminalInput::MakeOutput(L"\x1b\x7f"), input, SHIFT_PRESSED | LEFT_CTRL_PRESSED | LEFT_ALT_PRESSED, vkey);
+    TestKey(TerminalInput::MakeOutput(L"\x1b\x7f"), input, SHIFT_PRESSED | RIGHT_CTRL_PRESSED | LEFT_ALT_PRESSED, vkey);
     // LEFT_CTRL_PRESSED | RIGHT_ALT_PRESSED is skipped because that's AltGr
-    TestKey(TerminalInput::MakeOutput(L"\x1b\x7f"), input, SHIFT_PRESSED | RIGHT_CTRL_PRESSED | RIGHT_ALT_PRESSED, vkey, L'?');
+    TestKey(TerminalInput::MakeOutput(L"\x1b\x7f"), input, SHIFT_PRESSED | RIGHT_CTRL_PRESSED | RIGHT_ALT_PRESSED, vkey);
 }
 
 void InputTest::CtrlNumTest()
@@ -674,7 +780,7 @@ void InputTest::AutoRepeatModeTest()
     VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"A"), input.HandleKey(down));
     VERIFY_ARE_EQUAL(TerminalInput::MakeOutput({}), input.HandleKey(down));
     VERIFY_ARE_EQUAL(TerminalInput::MakeOutput({}), input.HandleKey(down));
-    VERIFY_ARE_EQUAL(TerminalInput::MakeUnhandled(), input.HandleKey(up));
+    VERIFY_ARE_EQUAL(TerminalInput::MakeOutput({}), input.HandleKey(up));
 
     Log::Comment(L"Sending repeating keypresses with DECARM enabled.");
 
@@ -682,5 +788,5 @@ void InputTest::AutoRepeatModeTest()
     VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"A"), input.HandleKey(down));
     VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"A"), input.HandleKey(down));
     VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"A"), input.HandleKey(down));
-    VERIFY_ARE_EQUAL(TerminalInput::MakeUnhandled(), input.HandleKey(up));
+    VERIFY_ARE_EQUAL(TerminalInput::MakeOutput({}), input.HandleKey(up));
 }
diff --git a/src/terminal/input/mouseInput.cpp b/src/terminal/input/mouseInput.cpp
index ddcb81b7e9b..8f066d5fcd6 100644
--- a/src/terminal/input/mouseInput.cpp
+++ b/src/terminal/input/mouseInput.cpp
@@ -11,12 +11,6 @@ using namespace Microsoft::Console::VirtualTerminal;
 
 static constexpr int s_MaxDefaultCoordinate = 94;
 
-// Alternate scroll sequences
-static constexpr std::wstring_view CursorUpSequence{ L"\x1b[A" };
-static constexpr std::wstring_view CursorDownSequence{ L"\x1b[B" };
-static constexpr std::wstring_view ApplicationUpSequence{ L"\x1bOA" };
-static constexpr std::wstring_view ApplicationDownSequence{ L"\x1bOB" };
-
 // Routine Description:
 // - Determines if the input windows message code describes a button event
 //     (left, middle, right button and any of up, down or double click)
@@ -147,11 +141,11 @@ constexpr unsigned int TerminalInput::s_GetPressedButton(const MouseButtonState
 // - modifierKeyState - the modifier keys _in console format_
 // - delta - scroll wheel delta
 // Return value:
-// - the int representing the equivalent X button encoding.
-static constexpr int _windowsButtonToXEncoding(const unsigned int button,
-                                               const bool isHover,
-                                               const short modifierKeyState,
-                                               const short delta) noexcept
+// - the character representing the equivalent X button encoding.
+static constexpr wchar_t _windowsButtonToXEncoding(const unsigned int button,
+                                                   const bool isHover,
+                                                   const short modifierKeyState,
+                                                   const short delta) noexcept
 {
     auto xvalue = 0;
     switch (button)
@@ -191,7 +185,7 @@ static constexpr int _windowsButtonToXEncoding(const unsigned int button,
     WI_UpdateFlag(xvalue, 0x08, WI_IsAnyFlagSet(modifierKeyState, ALT_PRESSED));
     WI_UpdateFlag(xvalue, 0x10, WI_IsAnyFlagSet(modifierKeyState, CTRL_PRESSED));
 
-    return xvalue;
+    return gsl::narrow_cast<wchar_t>(L' ' + xvalue);
 }
 
 // Routine Description:
@@ -270,9 +264,9 @@ static constexpr til::point _winToVTCoord(const til::point coordWinCoordinate) n
 // - sCoordinateValue - the value to encode.
 // Return value:
 // - the encoded value.
-static constexpr til::CoordType _encodeDefaultCoordinate(const til::CoordType sCoordinateValue) noexcept
+static constexpr wchar_t _encodeDefaultCoordinate(const til::CoordType sCoordinateValue) noexcept
 {
-    return sCoordinateValue + 32;
+    return gsl::narrow_cast<wchar_t>(sCoordinateValue + 32);
 }
 
 // Routine Description:
@@ -415,12 +409,9 @@ TerminalInput::OutputType TerminalInput::_GenerateDefaultSequence(const til::poi
         const auto vtCoords = _winToVTCoord(position);
         const auto encodedX = _encodeDefaultCoordinate(vtCoords.x);
         const auto encodedY = _encodeDefaultCoordinate(vtCoords.y);
+        const auto encodedButton = _windowsButtonToXEncoding(button, isHover, modifierKeyState, delta);
 
-        StringType format{ L"\x1b[Mbxy" };
-        til::at(format, 3) = gsl::narrow_cast<wchar_t>(L' ' + _windowsButtonToXEncoding(button, isHover, modifierKeyState, delta));
-        til::at(format, 4) = gsl::narrow_cast<wchar_t>(encodedX);
-        til::at(format, 5) = gsl::narrow_cast<wchar_t>(encodedY);
-        return format;
+        return fmt::format(FMT_COMPILE(L"{}M{}{}{}"), _csi, encodedButton, encodedX, encodedY);
     }
 
     return {};
@@ -458,13 +449,9 @@ TerminalInput::OutputType TerminalInput::_GenerateUtf8Sequence(const til::point
         const auto vtCoords = _winToVTCoord(position);
         const auto encodedX = _encodeDefaultCoordinate(vtCoords.x);
         const auto encodedY = _encodeDefaultCoordinate(vtCoords.y);
+        const auto encodedButton = _windowsButtonToXEncoding(button, isHover, modifierKeyState, delta);
 
-        StringType format{ L"\x1b[Mbxy" };
-        // The short cast is safe because we know s_WindowsButtonToXEncoding  never returns more than xff
-        til::at(format, 3) = gsl::narrow_cast<wchar_t>(L' ' + _windowsButtonToXEncoding(button, isHover, modifierKeyState, delta));
-        til::at(format, 4) = gsl::narrow_cast<wchar_t>(encodedX);
-        til::at(format, 5) = gsl::narrow_cast<wchar_t>(encodedY);
-        return format;
+        return fmt::format(FMT_COMPILE(L"{}M{}{}{}"), _csi, encodedButton, encodedX, encodedY);
     }
 
     return {};
@@ -487,7 +474,7 @@ TerminalInput::OutputType TerminalInput::_GenerateSGRSequence(const til::point p
     // Format for SGR events is:
     // "\x1b[<%d;%d;%d;%c", xButton, x+1, y+1, fButtonDown? 'M' : 'm'
     const auto xbutton = _windowsButtonToSGREncoding(button, isHover, modifierKeyState, delta);
-    return fmt::format(FMT_COMPILE(L"\x1b[<{};{};{}{}"), xbutton, position.x + 1, position.y + 1, isDown ? L'M' : L'm');
+    return fmt::format(FMT_COMPILE(L"{}<{};{};{}{}"), _csi, xbutton, position.x + 1, position.y + 1, isDown ? L'M' : L'm');
 }
 
 // Routine Description:
@@ -515,10 +502,10 @@ TerminalInput::OutputType TerminalInput::_makeAlternateScrollOutput(const short
 {
     if (delta > 0)
     {
-        return MakeOutput(_inputMode.test(Mode::CursorKey) ? ApplicationUpSequence : CursorUpSequence);
+        return MakeOutput(_keyMap.at(VK_UP));
     }
     else
     {
-        return MakeOutput(_inputMode.test(Mode::CursorKey) ? ApplicationDownSequence : CursorDownSequence);
+        return MakeOutput(_keyMap.at(VK_DOWN));
     }
 }
diff --git a/src/terminal/input/terminalInput.cpp b/src/terminal/input/terminalInput.cpp
index 855ba74b28a..a28864e4f34 100644
--- a/src/terminal/input/terminalInput.cpp
+++ b/src/terminal/input/terminalInput.cpp
@@ -10,226 +10,26 @@
 #include "../../interactivity/inc/VtApiRedirection.hpp"
 #include "../types/inc/IInputEvent.hpp"
 
+using namespace std::string_literals;
 using namespace Microsoft::Console::VirtualTerminal;
 
-struct TermKeyMap
+namespace
 {
-    const WORD vkey;
-    const std::wstring_view sequence;
-    const DWORD modifiers;
-
-    constexpr TermKeyMap(WORD vkey, std::wstring_view sequence) noexcept :
-        TermKeyMap(vkey, 0, sequence)
-    {
-    }
+    // These modifier constants are added to the virtual key code
+    // to produce a lookup value for determining the appropriate
+    // VT sequence for a particular modifier + key combination.
+    constexpr int VTModifier(const int m) { return m << 8; }
+    constexpr auto Unmodified = VTModifier(0);
+    constexpr auto Shift = VTModifier(1);
+    constexpr auto Alt = VTModifier(2);
+    constexpr auto Ctrl = VTModifier(4);
+    constexpr auto Enhanced = VTModifier(8);
+}
 
-    constexpr TermKeyMap(const WORD vkey, const DWORD modifiers, std::wstring_view sequence) noexcept :
-        vkey(vkey),
-        sequence(sequence),
-        modifiers(modifiers)
-    {
-    }
-};
-
-// See http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-PC-Style-Function-Keys
-//    For the source for these tables.
-// Also refer to the values in terminfo for kcub1, kcud1, kcuf1, kcuu1, kend, khome.
-//   the 'xterm' setting lists the application mode versions of these sequences.
-static constexpr std::array<TermKeyMap, 6> s_cursorKeysNormalMapping = {
-    TermKeyMap{ VK_UP, L"\x1b[A" },
-    TermKeyMap{ VK_DOWN, L"\x1b[B" },
-    TermKeyMap{ VK_RIGHT, L"\x1b[C" },
-    TermKeyMap{ VK_LEFT, L"\x1b[D" },
-    TermKeyMap{ VK_HOME, L"\x1b[H" },
-    TermKeyMap{ VK_END, L"\x1b[F" },
-};
-
-static constexpr std::array<TermKeyMap, 6> s_cursorKeysApplicationMapping{
-    TermKeyMap{ VK_UP, L"\x1bOA" },
-    TermKeyMap{ VK_DOWN, L"\x1bOB" },
-    TermKeyMap{ VK_RIGHT, L"\x1bOC" },
-    TermKeyMap{ VK_LEFT, L"\x1bOD" },
-    TermKeyMap{ VK_HOME, L"\x1bOH" },
-    TermKeyMap{ VK_END, L"\x1bOF" },
-};
-
-static constexpr std::array<TermKeyMap, 6> s_cursorKeysVt52Mapping{
-    TermKeyMap{ VK_UP, L"\033A" },
-    TermKeyMap{ VK_DOWN, L"\033B" },
-    TermKeyMap{ VK_RIGHT, L"\033C" },
-    TermKeyMap{ VK_LEFT, L"\033D" },
-    TermKeyMap{ VK_HOME, L"\033H" },
-    TermKeyMap{ VK_END, L"\033F" },
-};
-
-static constexpr std::array<TermKeyMap, 19> s_keypadNumericMapping{
-    TermKeyMap{ VK_TAB, L"\x09" },
-    TermKeyMap{ VK_PAUSE, L"\x1a" },
-    TermKeyMap{ VK_ESCAPE, L"\x1b" },
-    TermKeyMap{ VK_INSERT, L"\x1b[2~" },
-    TermKeyMap{ VK_DELETE, L"\x1b[3~" },
-    TermKeyMap{ VK_PRIOR, L"\x1b[5~" },
-    TermKeyMap{ VK_NEXT, L"\x1b[6~" },
-    TermKeyMap{ VK_F1, L"\x1bOP" }, // also \x1b[11~, PuTTY uses \x1b\x1b[A
-    TermKeyMap{ VK_F2, L"\x1bOQ" }, // also \x1b[12~, PuTTY uses \x1b\x1b[B
-    TermKeyMap{ VK_F3, L"\x1bOR" }, // also \x1b[13~, PuTTY uses \x1b\x1b[C
-    TermKeyMap{ VK_F4, L"\x1bOS" }, // also \x1b[14~, PuTTY uses \x1b\x1b[D
-    TermKeyMap{ VK_F5, L"\x1b[15~" },
-    TermKeyMap{ VK_F6, L"\x1b[17~" },
-    TermKeyMap{ VK_F7, L"\x1b[18~" },
-    TermKeyMap{ VK_F8, L"\x1b[19~" },
-    TermKeyMap{ VK_F9, L"\x1b[20~" },
-    TermKeyMap{ VK_F10, L"\x1b[21~" },
-    TermKeyMap{ VK_F11, L"\x1b[23~" },
-    TermKeyMap{ VK_F12, L"\x1b[24~" },
-};
-
-//Application mode - Some terminals support both a "Numeric" input mode, and an "Application" mode
-//  The standards vary on what each key translates to in the various modes, so I tried to make it as close
-//  to the VT220 standard as possible.
-//  The notable difference is in the arrow keys, which in application mode translate to "^[0A" (etc) as opposed to "^[[A" in numeric
-//Some very unclear documentation at http://invisible-island.net/xterm/ctlseqs/ctlseqs.html also suggests alternate encodings for F1-4
-//  which I have left in the comments on those entries as something to possibly add in the future, if need be.
-//It seems to me as though this was used for early numpad implementations, where presently numlock would enable
-//  "numeric" mode, outputting the numbers on the keys, while "application" mode does things like pgup/down, arrow keys, etc.
-//These keys aren't translated at all in numeric mode, so I figured I'd leave them out of the numeric table.
-static constexpr std::array<TermKeyMap, 19> s_keypadApplicationMapping{
-    TermKeyMap{ VK_TAB, L"\x09" },
-    TermKeyMap{ VK_PAUSE, L"\x1a" },
-    TermKeyMap{ VK_ESCAPE, L"\x1b" },
-    TermKeyMap{ VK_INSERT, L"\x1b[2~" },
-    TermKeyMap{ VK_DELETE, L"\x1b[3~" },
-    TermKeyMap{ VK_PRIOR, L"\x1b[5~" },
-    TermKeyMap{ VK_NEXT, L"\x1b[6~" },
-    TermKeyMap{ VK_F1, L"\x1bOP" }, // also \x1b[11~, PuTTY uses \x1b\x1b[A
-    TermKeyMap{ VK_F2, L"\x1bOQ" }, // also \x1b[12~, PuTTY uses \x1b\x1b[B
-    TermKeyMap{ VK_F3, L"\x1bOR" }, // also \x1b[13~, PuTTY uses \x1b\x1b[C
-    TermKeyMap{ VK_F4, L"\x1bOS" }, // also \x1b[14~, PuTTY uses \x1b\x1b[D
-    TermKeyMap{ VK_F5, L"\x1b[15~" },
-    TermKeyMap{ VK_F6, L"\x1b[17~" },
-    TermKeyMap{ VK_F7, L"\x1b[18~" },
-    TermKeyMap{ VK_F8, L"\x1b[19~" },
-    TermKeyMap{ VK_F9, L"\x1b[20~" },
-    TermKeyMap{ VK_F10, L"\x1b[21~" },
-    TermKeyMap{ VK_F11, L"\x1b[23~" },
-    TermKeyMap{ VK_F12, L"\x1b[24~" },
-    // The numpad has a variety of mappings, none of which seem standard or really configurable by the OS.
-    // See http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-PC-Style-Function-Keys
-    //   to see just how convoluted this all is.
-    // PuTTY uses a set of mappings that don't work in ViM without reamapping them back to the numpad
-    // (see http://vim.wikia.com/wiki/PuTTY_numeric_keypad_mappings#Comments)
-    // I think the best solution is to just not do any for the time being.
-    // Putty also provides configuration for choosing which of the 5 mappings it has through the settings, which is more work than we can manage now.
-    // TermKeyMap{ VK_MULTIPLY, L"\x1bOj" },     // PuTTY: \x1bOR (I believe putty is treating the top row of the numpad as PF1-PF4)
-    // TermKeyMap{ VK_ADD, L"\x1bOk" },          // PuTTY: \x1bOl, \x1bOm (with shift)
-    // TermKeyMap{ VK_SEPARATOR, L"\x1bOl" },    // ? I'm not sure which key this is...
-    // TermKeyMap{ VK_SUBTRACT, L"\x1bOm" },     // \x1bOS
-    // TermKeyMap{ VK_DECIMAL, L"\x1bOn" },      // \x1bOn
-    // TermKeyMap{ VK_DIVIDE, L"\x1bOo" },       // \x1bOQ
-    // TermKeyMap{ VK_NUMPAD0, L"\x1bOp" },
-    // TermKeyMap{ VK_NUMPAD1, L"\x1bOq" },
-    // TermKeyMap{ VK_NUMPAD2, L"\x1bOr" },
-    // TermKeyMap{ VK_NUMPAD3, L"\x1bOs" },
-    // TermKeyMap{ VK_NUMPAD4, L"\x1bOt" },
-    // TermKeyMap{ VK_NUMPAD5, L"\x1bOu" }, // \x1b0E
-    // TermKeyMap{ VK_NUMPAD5, L"\x1bOE" }, // PuTTY \x1b[G
-    // TermKeyMap{ VK_NUMPAD6, L"\x1bOv" },
-    // TermKeyMap{ VK_NUMPAD7, L"\x1bOw" },
-    // TermKeyMap{ VK_NUMPAD8, L"\x1bOx" },
-    // TermKeyMap{ VK_NUMPAD9, L"\x1bOy" },
-    // TermKeyMap{ '=', L"\x1bOX" },      // I've also seen these codes mentioned in some documentation,
-    // TermKeyMap{ VK_SPACE, L"\x1bO " }, //  but I wasn't really sure if they should be included or not...
-    // TermKeyMap{ VK_TAB, L"\x1bOI" },   // So I left them here as a reference just in case.
-};
-
-static constexpr std::array<TermKeyMap, 19> s_keypadVt52Mapping{
-    TermKeyMap{ VK_TAB, L"\x09" },
-    TermKeyMap{ VK_PAUSE, L"\x1a" },
-    TermKeyMap{ VK_ESCAPE, L"\x1b" },
-    TermKeyMap{ VK_INSERT, L"\x1b[2~" },
-    TermKeyMap{ VK_DELETE, L"\x1b[3~" },
-    TermKeyMap{ VK_PRIOR, L"\x1b[5~" },
-    TermKeyMap{ VK_NEXT, L"\x1b[6~" },
-    TermKeyMap{ VK_F1, L"\x1bP" },
-    TermKeyMap{ VK_F2, L"\x1bQ" },
-    TermKeyMap{ VK_F3, L"\x1bR" },
-    TermKeyMap{ VK_F4, L"\x1bS" },
-    TermKeyMap{ VK_F5, L"\x1b[15~" },
-    TermKeyMap{ VK_F6, L"\x1b[17~" },
-    TermKeyMap{ VK_F7, L"\x1b[18~" },
-    TermKeyMap{ VK_F8, L"\x1b[19~" },
-    TermKeyMap{ VK_F9, L"\x1b[20~" },
-    TermKeyMap{ VK_F10, L"\x1b[21~" },
-    TermKeyMap{ VK_F11, L"\x1b[23~" },
-    TermKeyMap{ VK_F12, L"\x1b[24~" },
-};
-
-// Sequences to send when a modifier is pressed with any of these keys
-// Basically, the 'm' will be replaced with a character indicating which
-//      modifier keys are pressed.
-static constexpr std::array<TermKeyMap, 22> s_modifierKeyMapping{
-    TermKeyMap{ VK_UP, L"\x1b[1;mA" },
-    TermKeyMap{ VK_DOWN, L"\x1b[1;mB" },
-    TermKeyMap{ VK_RIGHT, L"\x1b[1;mC" },
-    TermKeyMap{ VK_LEFT, L"\x1b[1;mD" },
-    TermKeyMap{ VK_HOME, L"\x1b[1;mH" },
-    TermKeyMap{ VK_END, L"\x1b[1;mF" },
-    TermKeyMap{ VK_F1, L"\x1b[1;mP" },
-    TermKeyMap{ VK_F2, L"\x1b[1;mQ" },
-    TermKeyMap{ VK_F3, L"\x1b[1;mR" },
-    TermKeyMap{ VK_F4, L"\x1b[1;mS" },
-    TermKeyMap{ VK_INSERT, L"\x1b[2;m~" },
-    TermKeyMap{ VK_DELETE, L"\x1b[3;m~" },
-    TermKeyMap{ VK_PRIOR, L"\x1b[5;m~" },
-    TermKeyMap{ VK_NEXT, L"\x1b[6;m~" },
-    TermKeyMap{ VK_F5, L"\x1b[15;m~" },
-    TermKeyMap{ VK_F6, L"\x1b[17;m~" },
-    TermKeyMap{ VK_F7, L"\x1b[18;m~" },
-    TermKeyMap{ VK_F8, L"\x1b[19;m~" },
-    TermKeyMap{ VK_F9, L"\x1b[20;m~" },
-    TermKeyMap{ VK_F10, L"\x1b[21;m~" },
-    TermKeyMap{ VK_F11, L"\x1b[23;m~" },
-    TermKeyMap{ VK_F12, L"\x1b[24;m~" },
-    // Ubuntu's inputrc also defines \x1b[5C, \x1b\x1bC (and D) as 'forward/backward-word' mappings
-    // I believe '\x1b\x1bC' is listed because the C1 ESC (x9B) gets encoded as
-    //  \xC2\x9B, but then translated to \x1b\x1b if the C1 codepoint isn't supported by the current encoding
-};
-
-// Sequences to send when a modifier is pressed with any of these keys
-// These sequences are not later updated to encode the modifier state in the
-//      sequence itself, they are just weird exceptional cases to the general
-//      rules above.
-static constexpr std::array<TermKeyMap, 11> s_simpleModifiedKeyMapping{
-    TermKeyMap{ VK_TAB, CTRL_PRESSED, L"\t" },
-    TermKeyMap{ VK_TAB, SHIFT_PRESSED, L"\x1b[Z" },
-    TermKeyMap{ VK_DIVIDE, CTRL_PRESSED, L"\x1F" },
-
-    // GH#3507 - We should also be encoding Ctrl+# according to the following table:
-    // https://vt100.net/docs/vt220-rm/table3-5.html
-    // * 1 and 9 do not send any special characters, but they _should_ send
-    //   through the character unmodified.
-    // * 0 doesn't seem to send even an unmodified '0' through.
-    // * Ctrl+2 is already special-cased below in `HandleKey`, so it's not
-    //   included here.
-    TermKeyMap{ static_cast<WORD>('1'), CTRL_PRESSED, L"1" },
-    // TermKeyMap{ static_cast<WORD>('2'), CTRL_PRESSED, L"\x00" },
-    TermKeyMap{ static_cast<WORD>('3'), CTRL_PRESSED, L"\x1B" },
-    TermKeyMap{ static_cast<WORD>('4'), CTRL_PRESSED, L"\x1C" },
-    TermKeyMap{ static_cast<WORD>('5'), CTRL_PRESSED, L"\x1D" },
-    TermKeyMap{ static_cast<WORD>('6'), CTRL_PRESSED, L"\x1E" },
-    TermKeyMap{ static_cast<WORD>('7'), CTRL_PRESSED, L"\x1F" },
-    TermKeyMap{ static_cast<WORD>('8'), CTRL_PRESSED, L"\x7F" },
-    TermKeyMap{ static_cast<WORD>('9'), CTRL_PRESSED, L"9" },
-
-    // These two are not implemented here, because they are system keys.
-    // TermKeyMap{ VK_TAB, ALT_PRESSED, L""}, This is the Windows system shortcut for switching windows.
-    // TermKeyMap{ VK_ESCAPE, ALT_PRESSED, L""}, This is another Windows system shortcut for switching windows.
-};
-
-const wchar_t* const CTRL_SLASH_SEQUENCE = L"\x1f";
-const wchar_t* const CTRL_QUESTIONMARK_SEQUENCE = L"\x7F";
-const wchar_t* const CTRL_ALT_SLASH_SEQUENCE = L"\x1b\x1f";
-const wchar_t* const CTRL_ALT_QUESTIONMARK_SEQUENCE = L"\x1b\x7F";
+TerminalInput::TerminalInput() noexcept
+{
+    _initKeyboardMap();
+}
 
 void TerminalInput::SetInputMode(const Mode mode, const bool enabled) noexcept
 {
@@ -250,6 +50,14 @@ void TerminalInput::SetInputMode(const Mode mode, const bool enabled) noexcept
     }
 
     _inputMode.set(mode, enabled);
+
+    // 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 };
+    if (keyMapModes.test(mode))
+    {
+        _initKeyboardMap();
+    }
 }
 
 bool TerminalInput::GetInputMode(const Mode mode) const noexcept
@@ -262,6 +70,7 @@ void TerminalInput::ResetInputModes() noexcept
     _inputMode = { Mode::Ansi, Mode::AutoRepeat };
     _mouseInputState.lastPos = { -1, -1 };
     _mouseInputState.lastButton = 0;
+    _initKeyboardMap();
 }
 
 void TerminalInput::ForceDisableWin32InputMode(const bool win32InputMode) noexcept
@@ -269,188 +78,6 @@ void TerminalInput::ForceDisableWin32InputMode(const bool win32InputMode) noexce
     _forceDisableWin32InputMode = win32InputMode;
 }
 
-static std::span<const TermKeyMap> _getKeyMapping(const KEY_EVENT_RECORD& keyEvent, const bool ansiMode, const bool cursorApplicationMode, const bool keypadApplicationMode) noexcept
-{
-    // Cursor keys: VK_END, VK_HOME, VK_LEFT, VK_UP, VK_RIGHT, VK_DOWN
-    const auto isCursorKey = keyEvent.wVirtualKeyCode >= VK_END && keyEvent.wVirtualKeyCode <= VK_DOWN;
-
-    if (ansiMode)
-    {
-        if (isCursorKey)
-        {
-            if (cursorApplicationMode)
-            {
-                return s_cursorKeysApplicationMapping;
-            }
-            else
-            {
-                return s_cursorKeysNormalMapping;
-            }
-        }
-        else
-        {
-            if (keypadApplicationMode)
-            {
-                return s_keypadApplicationMapping;
-            }
-            else
-            {
-                return s_keypadNumericMapping;
-            }
-        }
-    }
-    else
-    {
-        if (isCursorKey)
-        {
-            return s_cursorKeysVt52Mapping;
-        }
-        else
-        {
-            return s_keypadVt52Mapping;
-        }
-    }
-}
-
-// Routine Description:
-// - Searches the keyMapping for a entry corresponding to this key event, and returns it.
-// Arguments:
-// - keyEvent - Key event to translate
-// - keyMapping - Array of key mappings to search
-// Return Value:
-// - Has value if there was a match to a key translation.
-static std::optional<const TermKeyMap> _searchKeyMapping(const KEY_EVENT_RECORD& keyEvent,
-                                                         std::span<const TermKeyMap> keyMapping) noexcept
-{
-    for (auto& map : keyMapping)
-    {
-        if (map.vkey == keyEvent.wVirtualKeyCode)
-        {
-            // If the mapping has no modifiers set, then it doesn't really care
-            //      what the modifiers are on the key. The caller will likely do
-            //      something with them.
-            // However, if there are modifiers set, then we only want to match
-            //      if the key's modifiers are the same as the modifiers in the
-            //      mapping.
-            auto modifiersMatch = WI_AreAllFlagsClear(map.modifiers, MOD_PRESSED);
-            if (!modifiersMatch)
-            {
-                // The modifier mapping expects certain modifier keys to be
-                //      pressed. Check those as well.
-                modifiersMatch =
-                    WI_IsAnyFlagSet(map.modifiers, SHIFT_PRESSED) == WI_IsAnyFlagSet(keyEvent.dwControlKeyState, SHIFT_PRESSED) &&
-                    WI_IsAnyFlagSet(map.modifiers, ALT_PRESSED) == WI_IsAnyFlagSet(keyEvent.dwControlKeyState, ALT_PRESSED) &&
-                    WI_IsAnyFlagSet(map.modifiers, CTRL_PRESSED) == WI_IsAnyFlagSet(keyEvent.dwControlKeyState, CTRL_PRESSED);
-            }
-
-            if (modifiersMatch)
-            {
-                return map;
-            }
-        }
-    }
-    return std::nullopt;
-}
-
-// Searches the s_modifierKeyMapping for a entry corresponding to this key event.
-// Changes the second to last byte to correspond to the currently pressed modifier keys.
-TerminalInput::OutputType TerminalInput::_searchWithModifier(const KEY_EVENT_RECORD& keyEvent)
-{
-    if (const auto match = _searchKeyMapping(keyEvent, s_modifierKeyMapping))
-    {
-        const auto& v = match.value();
-        if (!v.sequence.empty())
-        {
-            const auto shift = WI_IsAnyFlagSet(keyEvent.dwControlKeyState, SHIFT_PRESSED);
-            const auto alt = WI_IsAnyFlagSet(keyEvent.dwControlKeyState, ALT_PRESSED);
-            const auto ctrl = WI_IsAnyFlagSet(keyEvent.dwControlKeyState, CTRL_PRESSED);
-            StringType str{ v.sequence };
-            str.at(str.size() - 2) = L'1' + (shift ? 1 : 0) + (alt ? 2 : 0) + (ctrl ? 4 : 0);
-            return str;
-        }
-    }
-
-    // We didn't find the key in the map of modified keys that need editing,
-    //      maybe it's in the other map of modified keys with sequences that
-    //      don't need editing before sending.
-    else if (const auto match2 = _searchKeyMapping(keyEvent, s_simpleModifiedKeyMapping))
-    {
-        // This mapping doesn't need to be changed at all.
-        return MakeOutput(match2->sequence);
-    }
-    else
-    {
-        // One last check:
-        // * C-/ is supposed to be ^_ (the C0 character US)
-        // * C-? is supposed to be DEL
-        // * C-M-/ is supposed to be ^[^_
-        // * C-M-? is supposed to be ^[^?
-        //
-        // But this whole scenario is tricky. '/' is not the same VKEY on
-        // all keyboards. On USASCII keyboards, '/' and '?' share the _same_
-        // key. So we have to figure out the vkey at runtime, and we have to
-        // determine if the key that was pressed was '?' with some
-        // modifiers, or '/' with some modifiers.
-        //
-        // These translations are not in s_simpleModifiedKeyMapping, because
-        // the aforementioned fact that they aren't the same VKEY on all
-        // keyboards.
-        //
-        // See GH#3079 for details.
-        // Also see https://github.com/microsoft/terminal/pull/4947#issuecomment-600382856
-
-        // VkKeyScan will give us both the Vkey of the key needed for this
-        // character, and the modifiers the user might need to press to get
-        // this character.
-        const auto slashKeyScan = OneCoreSafeVkKeyScanW(L'/'); // On USASCII: 0x00bf
-        const auto questionMarkKeyScan = OneCoreSafeVkKeyScanW(L'?'); //On USASCII: 0x01bf
-
-        const auto slashVkey = LOBYTE(slashKeyScan);
-        const auto questionMarkVkey = LOBYTE(questionMarkKeyScan);
-
-        const auto ctrl = WI_IsAnyFlagSet(keyEvent.dwControlKeyState, CTRL_PRESSED);
-        const auto alt = WI_IsAnyFlagSet(keyEvent.dwControlKeyState, ALT_PRESSED);
-        const auto shift = WI_IsAnyFlagSet(keyEvent.dwControlKeyState, SHIFT_PRESSED);
-
-        // From the KeyEvent we're translating, synthesize the equivalent VkKeyScan result
-        const auto vkey = keyEvent.wVirtualKeyCode;
-        const short keyScanFromEvent = vkey |
-                                       (shift ? 0x100 : 0) |
-                                       (ctrl ? 0x200 : 0) |
-                                       (alt ? 0x400 : 0);
-
-        // Make sure the VKEY is an _exact_ match, and that the modifier
-        // bits also match. This handles the hypothetical case we get a
-        // keyscan back that's ctrl+alt+some_random_VK, and some_random_VK
-        // has bits that are a superset of the bits set for question mark.
-        const auto wasQuestionMark = vkey == questionMarkVkey && WI_AreAllFlagsSet(keyScanFromEvent, questionMarkKeyScan);
-        const auto wasSlash = vkey == slashVkey && WI_AreAllFlagsSet(keyScanFromEvent, slashKeyScan);
-
-        // If the key pressed was exactly the ? key, then try to send the
-        // appropriate sequence for a modified '?'. Otherwise, check if this
-        // was a modified '/' keypress. These mappings don't need to be
-        // changed at all.
-        if ((ctrl && alt) && wasQuestionMark)
-        {
-            return MakeOutput(CTRL_ALT_QUESTIONMARK_SEQUENCE);
-        }
-        else if (ctrl && wasQuestionMark)
-        {
-            return MakeOutput(CTRL_QUESTIONMARK_SEQUENCE);
-        }
-        else if ((ctrl && alt) && wasSlash)
-        {
-            return MakeOutput(CTRL_ALT_SLASH_SEQUENCE);
-        }
-        else if (ctrl && wasSlash)
-        {
-            return MakeOutput(CTRL_SLASH_SEQUENCE);
-        }
-    }
-
-    return MakeUnhandled();
-}
-
 TerminalInput::OutputType TerminalInput::MakeUnhandled() noexcept
 {
     return {};
@@ -483,7 +110,7 @@ TerminalInput::OutputType TerminalInput::HandleKey(const INPUT_RECORD& event)
         return MakeUnhandled();
     }
 
-    auto keyEvent = event.Event.KeyEvent;
+    const auto keyEvent = event.Event.KeyEvent;
 
     // GH#4999 - If we're in win32-input mode, skip straight to doing that.
     // Since this mode handles all types of key events, do nothing else.
@@ -493,8 +120,12 @@ TerminalInput::OutputType TerminalInput::HandleKey(const INPUT_RECORD& event)
         return _makeWin32Output(keyEvent);
     }
 
+    const auto controlKeyState = _trackControlKeyState(keyEvent);
+    const auto virtualKeyCode = keyEvent.wVirtualKeyCode;
+    auto unicodeChar = keyEvent.uChar.UnicodeChar;
+
     // Check if this key matches the last recorded key code.
-    const auto matchingLastKeyPress = _lastVirtualKeyCode == keyEvent.wVirtualKeyCode;
+    const auto matchingLastKeyPress = _lastVirtualKeyCode == virtualKeyCode;
 
     // Only need to handle key down. See raw key handler (see RawReadWaitRoutine in stream.cpp)
     if (!keyEvent.bKeyDown)
@@ -504,7 +135,35 @@ TerminalInput::OutputType TerminalInput::HandleKey(const INPUT_RECORD& event)
         {
             _lastVirtualKeyCode = std::nullopt;
         }
-        return MakeUnhandled();
+        // If NumLock is on, and this is an Alt release with a unicode char,
+        // it must be the generated character from an Alt-Numpad composition.
+        if (WI_IsFlagSet(controlKeyState, NUMLOCK_ON) && virtualKeyCode == VK_MENU && unicodeChar != 0)
+        {
+            return MakeOutput({ &unicodeChar, 1 });
+        }
+        // Otherwise we should return an empty string here to prevent unwanted
+        // characters being transmitted by the release event.
+        return _makeNoOutput();
+    }
+
+    // Unpaired surrogates are no good -> early return.
+    if (til::is_leading_surrogate(unicodeChar))
+    {
+        _leadingSurrogate = unicodeChar;
+        return _makeNoOutput();
+    }
+    // Using a scope_exit ensures that a previous leading surrogate is forgotten
+    // even if the KEY_EVENT that followed didn't end up calling _makeCharOutput.
+    const auto leadingSurrogateReset = wil::scope_exit([&]() {
+        _leadingSurrogate = 0;
+    });
+
+    // If this is a VK_PACKET or 0 virtual key, it's likely a synthesized
+    // keyboard event, so the UnicodeChar is transmitted as is. This must be
+    // handled before the Auto Repeat test, other we'll end up dropping chars.
+    if (virtualKeyCode == VK_PACKET || virtualKeyCode == 0)
+    {
+        return _makeCharOutput(unicodeChar);
     }
 
     // If this is a repeat of the last recorded key press, and Auto Repeat Mode
@@ -513,209 +172,466 @@ TerminalInput::OutputType TerminalInput::HandleKey(const INPUT_RECORD& event)
     {
         // Note that we must return an empty string here to imply that we've handled
         // the event, otherwise the key press can still end up being submitted.
-        return MakeOutput({});
+        return _makeNoOutput();
     }
-    _lastVirtualKeyCode = keyEvent.wVirtualKeyCode;
+    _lastVirtualKeyCode = virtualKeyCode;
 
-    // The VK_BACK key depends on the state of Backarrow Key mode (DECBKM).
-    // If the mode is set, we should send BS. If reset, we should send DEL.
-    if (keyEvent.wVirtualKeyCode == VK_BACK)
+    // If this is a modifier, it won't produce output, so we can return early.
+    if (virtualKeyCode >= VK_SHIFT && virtualKeyCode <= VK_MENU)
     {
-        // The Ctrl modifier reverses the interpretation of DECBKM.
-        const auto backarrowMode = _inputMode.test(Mode::BackarrowKey) != WI_IsAnyFlagSet(keyEvent.dwControlKeyState, CTRL_PRESSED);
-        const auto seq = backarrowMode ? L'\x08' : L'\x7f';
-        // The Alt modifier adds an escape prefix.
-        if (WI_IsAnyFlagSet(keyEvent.dwControlKeyState, ALT_PRESSED))
-        {
-            return _makeEscapedOutput(seq);
-        }
-        else
-        {
-            return MakeOutput({ &seq, 1 });
-        }
+        return _makeNoOutput();
     }
 
-    // When the Line Feed mode is set, a VK_RETURN key should send both CR and LF.
-    // When reset, we fall through to the default behavior, which is to send just
-    // CR, or when the Ctrl modifier is pressed, just LF.
-    if (keyEvent.wVirtualKeyCode == VK_RETURN && _inputMode.test(Mode::LineFeed))
+    // Keyboards that have an AltGr key will generate both a RightAlt key press
+    // and a fake LeftCtrl key press. In order to support key combinations where
+    // the Ctrl key is manually pressed in addition to the AltGr key, we have to
+    // be able to detect when the Ctrl key isn't genuine. We do so by tracking
+    // the time between the Alt and Ctrl key presses, and only consider the Ctrl
+    // key to really be pressed if the difference is more than 50ms.
+    auto leftCtrlIsReallyPressed = WI_IsFlagSet(controlKeyState, LEFT_CTRL_PRESSED);
+    if (WI_AreAllFlagsSet(controlKeyState, LEFT_CTRL_PRESSED | RIGHT_ALT_PRESSED))
     {
-        return MakeOutput(L"\r\n");
+        const auto timeBetweenCtrlAlt = _lastRightAltTime > _lastLeftCtrlTime ?
+                                            _lastRightAltTime - _lastLeftCtrlTime :
+                                            _lastLeftCtrlTime - _lastRightAltTime;
+        leftCtrlIsReallyPressed = timeBetweenCtrlAlt > 50;
     }
 
-    // Many keyboard layouts have an AltGr key, which makes widely used characters accessible.
-    // For instance on a German keyboard layout "[" is written by pressing AltGr+8.
-    // Furthermore Ctrl+Alt is traditionally treated as an alternative way to AltGr by Windows.
-    // When AltGr is pressed, the caller needs to make sure to send us a pretranslated character in uChar.UnicodeChar.
-    // --> Strip out the AltGr flags, in order for us to not step into the Alt/Ctrl conditions below.
-    if (WI_AreAllFlagsSet(keyEvent.dwControlKeyState, LEFT_CTRL_PRESSED | RIGHT_ALT_PRESSED))
+    const auto ctrlIsPressed = WI_IsAnyFlagSet(controlKeyState, CTRL_PRESSED);
+    const auto ctrlIsReallyPressed = leftCtrlIsReallyPressed || WI_IsFlagSet(controlKeyState, RIGHT_CTRL_PRESSED);
+    const auto shiftIsPressed = WI_IsFlagSet(controlKeyState, SHIFT_PRESSED);
+    const auto altIsPressed = WI_IsAnyFlagSet(controlKeyState, ALT_PRESSED);
+    const auto altGrIsPressed = altIsPressed && ctrlIsPressed;
+
+    // If it's a numeric keypad key, and Alt is pressed (but not Ctrl), then
+    // this is an Alt-Numpad composition and we should ignore these keys. The
+    // generated character will be transmitted when the Alt is released.
+    if (virtualKeyCode >= VK_NUMPAD0 && virtualKeyCode <= VK_NUMPAD9 && altIsPressed && !ctrlIsPressed)
     {
-        WI_ClearAllFlags(keyEvent.dwControlKeyState, LEFT_CTRL_PRESSED | RIGHT_ALT_PRESSED);
+        return _makeNoOutput();
     }
 
-    // The Alt modifier initiates a so called "escape sequence".
-    // See: https://en.wikipedia.org/wiki/ANSI_escape_code#Escape_sequences
-    // See: ECMA-48, section 5.3, http://www.ecma-international.org/publications/standards/Ecma-048.htm
-    //
-    // This section in particular handles Alt+Ctrl combinations though.
-    // The Ctrl modifier causes all of the char code's bits except
-    // for the 5 least significant ones to be zeroed out.
-    if (WI_IsAnyFlagSet(keyEvent.dwControlKeyState, ALT_PRESSED) && WI_IsAnyFlagSet(keyEvent.dwControlKeyState, CTRL_PRESSED))
+    // The only enhanced key we care about is the Return key, because that
+    // indicates that it's the key on the numeric keypad, which will transmit
+    // different escape sequences when the Keypad mode is enabled.
+    const auto enhancedReturnKey = WI_IsFlagSet(controlKeyState, ENHANCED_KEY) && virtualKeyCode == VK_RETURN;
+
+    // Using the control key state that we calculated above, combined with the
+    // virtual key code, we've got a unique identifier for the key combination
+    // that we can lookup in our map of predefined key sequences.
+    auto keyCombo = virtualKeyCode;
+    WI_SetFlagIf(keyCombo, Ctrl, ctrlIsReallyPressed);
+    WI_SetFlagIf(keyCombo, Alt, altIsPressed);
+    WI_SetFlagIf(keyCombo, Shift, shiftIsPressed);
+    WI_SetFlagIf(keyCombo, Enhanced, enhancedReturnKey);
+    const auto keyMatch = _keyMap.find(keyCombo);
+    if (keyMatch != _keyMap.end())
     {
-        const auto ch = keyEvent.uChar.UnicodeChar;
-        const auto vkey = keyEvent.wVirtualKeyCode;
-
-        // For Alt+Ctrl+Key messages uChar.UnicodeChar usually returns 0.
-        // Luckily the numerical values of the ASCII characters and virtual key codes
-        // of <Space> and A-Z, as used below, are numerically identical.
-        // -> Get the char from the virtual key if it's 0.
-        const auto ctrlAltChar = keyEvent.uChar.UnicodeChar != 0 ? keyEvent.uChar.UnicodeChar : keyEvent.wVirtualKeyCode;
-
-        // Alt+Ctrl acts as a substitute for AltGr on Windows.
-        // For instance using a German keyboard both AltGr+< and Alt+Ctrl+< produce a | (pipe) character.
-        // The below condition primitively ensures that we allow all common Alt+Ctrl combinations
-        // while preserving most of the functionality of Alt+Ctrl as a substitute for AltGr.
-        if (ctrlAltChar == UNICODE_SPACE || (ctrlAltChar > 0x40 && ctrlAltChar <= 0x5A))
-        {
-            // Pressing the control key causes all bits but the 5 least
-            // significant ones to be zeroed out (when using ASCII).
-            return _makeEscapedOutput(ctrlAltChar & 0b11111);
-        }
+        return keyMatch->second;
+    }
 
-        // Currently, when we're called with Alt+Ctrl+@, ch will be 0, since Ctrl+@ equals a null byte.
-        // VkKeyScanW(0) in turn returns the vkey for the null character (ASCII @).
-        // -> Use the vkey to determine if Ctrl+@ is being pressed and produce ^[^@.
-        if (ch == UNICODE_NULL && vkey == LOBYTE(OneCoreSafeVkKeyScanW(0)))
+    // If it's not in the key map, we'll use the UnicodeChar, if provided.
+    if (unicodeChar != 0)
+    {
+        // In the case of an AltGr key, we may still need to apply a Ctrl
+        // modifier to the char, either because both Ctrl keys were pressed,
+        // or we got a LeftCtrl that was distinctly separate from the RightAlt.
+        const auto bothCtrlsArePressed = WI_AreAllFlagsSet(controlKeyState, CTRL_PRESSED);
+        const auto rightAltIsPressed = WI_IsFlagSet(controlKeyState, RIGHT_ALT_PRESSED);
+        if (altGrIsPressed && (bothCtrlsArePressed || (rightAltIsPressed && leftCtrlIsReallyPressed)))
         {
-            return _makeEscapedOutput(L'\0');
+            unicodeChar = _makeCtrlChar(unicodeChar);
         }
+        auto charSequence = _makeCharOutput(unicodeChar);
+        // We may also need to apply an Alt prefix to the char sequence, but
+        // if this is an AltGr key, we only do so if both Alts are pressed.
+        const auto bothAltsArePressed = WI_AreAllFlagsSet(controlKeyState, ALT_PRESSED);
+        _escapeOutput(charSequence, altGrIsPressed ? bothAltsArePressed : altIsPressed);
+        return charSequence;
     }
 
-    // If a modifier key was pressed, then we need to try and send the modified sequence.
-    if (WI_IsAnyFlagSet(keyEvent.dwControlKeyState, MOD_PRESSED))
+    // If we don't have a UnicodeChar, we'll try and determine what the key
+    // would have transmitted without any Ctrl or Alt modifiers applied. But
+    // this only makes sense if there were actually modifiers pressed.
+    if (!altIsPressed && !ctrlIsPressed)
     {
-        if (auto out = _searchWithModifier(keyEvent))
+        return _makeNoOutput();
+    }
+
+    // We need the current keyboard layout and state to lookup the character
+    // that would be transmitted in that state (via the ToUnicodeEx API).
+    const auto hkl = GetKeyboardLayout(GetWindowThreadProcessId(GetForegroundWindow(), nullptr));
+    auto keyState = _getKeyboardState(virtualKeyCode, controlKeyState);
+    const auto flags = 4u; // Don't modify the state in the ToUnicodeEx call.
+    const auto bufferSize = 16;
+    auto buffer = std::array<wchar_t, bufferSize>{};
+
+    // However, we first need to query the key with the original state, to check
+    // whether it's a dead key. If that is the case, ToUnicodeEx should return a
+    // negative number, although in practice it's more likely to return a string
+    // of length two, with two identical characters. This is because the system
+    // sees this as a second press of the dead key, which would typically result
+    // in the combining character representation being transmit twice.
+    auto length = ToUnicodeEx(virtualKeyCode, 0, keyState.data(), buffer.data(), bufferSize, flags, hkl);
+    if (length < 0 || (length == 2 && buffer.at(0) == buffer.at(1)))
+    {
+        return _makeNoOutput();
+    }
+
+    // Once we know it's not a dead key, we run the query again, but with the
+    // Ctrl and Alt modifiers disabled to obtain the base character mapping.
+    keyState.at(VK_CONTROL) = keyState.at(VK_LCONTROL) = keyState.at(VK_RCONTROL) = 0;
+    keyState.at(VK_MENU) = keyState.at(VK_LMENU) = keyState.at(VK_RMENU) = 0;
+    length = ToUnicodeEx(virtualKeyCode, 0, keyState.data(), buffer.data(), bufferSize, flags, hkl);
+    if (length <= 0)
+    {
+        // If we've got nothing usable, we'll just return an empty string. The event
+        // has technically still been handled, even if it's an unmapped key.
+        return _makeNoOutput();
+    }
+
+    auto charSequence = StringType{ buffer.data(), gsl::narrow_cast<size_t>(length) };
+    // Once we've got the base character, we can apply the Ctrl modifier.
+    if (ctrlIsReallyPressed && charSequence.length() == 1)
+    {
+        auto ch = _makeCtrlChar(charSequence.at(0));
+        // If we haven't found a Ctrl mapping for the key, and it's one of
+        // the alphanumeric keys, we try again using the virtual key code.
+        // On keyboard layouts where the alphanumeric keys are not mapped to
+        // their typical ASCII values, this provides a simple fallback.
+        if (ch >= L' ' && virtualKeyCode >= '2' && virtualKeyCode <= 'Z')
         {
-            return out;
+            ch = _makeCtrlChar(virtualKeyCode);
         }
+        charSequence.at(0) = ch;
     }
+    // If Alt is pressed, that also needs to be applied to the sequence.
+    _escapeOutput(charSequence, altIsPressed);
+    return charSequence;
+}
 
-    // This section is similar to the Alt modifier section above,
-    // but handles cases without Ctrl modifiers.
-    if (WI_IsAnyFlagSet(keyEvent.dwControlKeyState, ALT_PRESSED) && !WI_IsAnyFlagSet(keyEvent.dwControlKeyState, CTRL_PRESSED) && keyEvent.uChar.UnicodeChar != 0)
+TerminalInput::OutputType TerminalInput::HandleFocus(const bool focused) const
+{
+    if (!_inputMode.test(Mode::FocusEvent))
     {
-        return _makeEscapedOutput(keyEvent.uChar.UnicodeChar);
+        return MakeUnhandled();
     }
 
-    // Pressing the control key causes all bits but the 5 least
-    // significant ones to be zeroed out (when using ASCII).
-    // This results in Ctrl+Space and Ctrl+@ being equal to a null byte.
-    // Normally the C0 control code set only defines Ctrl+@,
-    // but Ctrl+Space is also widely accepted by most terminals.
-    // -> Send a "null input sequence" in that case.
-    // We don't need to handle other kinds of Ctrl combinations,
-    // as we rely on the caller to pretranslate those to characters for us.
-    if (!WI_IsAnyFlagSet(keyEvent.dwControlKeyState, ALT_PRESSED) && WI_IsAnyFlagSet(keyEvent.dwControlKeyState, CTRL_PRESSED))
-    {
-        const auto ch = keyEvent.uChar.UnicodeChar;
-        const auto vkey = keyEvent.wVirtualKeyCode;
+    return MakeOutput(focused ? _focusInSequence : _focusOutSequence);
+}
 
-        // Currently, when we're called with Ctrl+@, ch will be 0, since Ctrl+@ equals a null byte.
-        // VkKeyScanW(0) in turn returns the vkey for the null character (ASCII @).
-        // -> Use the vkey to alternatively determine if Ctrl+@ is being pressed.
-        if (ch == UNICODE_SPACE || (ch == UNICODE_NULL && vkey == LOBYTE(OneCoreSafeVkKeyScanW(0))))
+void TerminalInput::_initKeyboardMap() noexcept
+try
+{
+    auto defineKeyWithUnusedModifiers = [this](const int keyCode, const std::wstring& sequence) {
+        for (auto m = 0; m < 8; m++)
+            _keyMap[VTModifier(m) + keyCode] = sequence;
+    };
+    auto defineKeyWithAltModifier = [this](const int keyCode, const std::wstring& sequence) {
+        _keyMap[keyCode] = sequence;
+        _keyMap[Alt + keyCode] = L"\x1B" + sequence;
+    };
+    auto defineKeypadKey = [this](const int keyCode, const wchar_t* prefix, const wchar_t finalChar) {
+        _keyMap[keyCode] = fmt::format(FMT_COMPILE(L"{}{}"), prefix, finalChar);
+        for (auto m = 1; m < 8; m++)
+            _keyMap[VTModifier(m) + keyCode] = fmt::format(FMT_COMPILE(L"{}1;{}{}"), _csi, m + 1, finalChar);
+    };
+    auto defineEditingKey = [this](const int keyCode, const int parm) {
+        _keyMap[keyCode] = fmt::format(FMT_COMPILE(L"{}{}~"), _csi, parm);
+        for (auto m = 1; m < 8; m++)
+            _keyMap[VTModifier(m) + keyCode] = fmt::format(FMT_COMPILE(L"{}{};{}~"), _csi, parm, m + 1);
+    };
+    auto defineNumericKey = [this](const int keyCode, const wchar_t finalChar) {
+        _keyMap[keyCode] = fmt::format(FMT_COMPILE(L"{}{}"), _ss3, finalChar);
+        for (auto m = 1; m < 8; m++)
+            _keyMap[VTModifier(m) + keyCode] = fmt::format(FMT_COMPILE(L"{}{}{}"), _ss3, m + 1, finalChar);
+    };
+
+    _keyMap.clear();
+
+    // PAUSE doesn't have a VT mapping, but traditionally we've mapped it to ^Z,
+    // regardless of modifiers.
+    defineKeyWithUnusedModifiers(VK_PAUSE, L"\x1A"s);
+
+    // BACKSPACE maps to either DEL or BS, depending on the Backarrow Key mode.
+    // The Ctrl modifier inverts the active mode, swapping BS and DEL (this is
+    // not standard, but a modern terminal convention). The Alt modifier adds
+    // an ESC prefix (also not standard).
+    const auto backSequence = _inputMode.test(Mode::BackarrowKey) ? L"\b"s : L"\x7F"s;
+    const auto ctrlBackSequence = _inputMode.test(Mode::BackarrowKey) ? L"\x7F"s : L"\b"s;
+    defineKeyWithAltModifier(VK_BACK, backSequence);
+    defineKeyWithAltModifier(Ctrl + VK_BACK, ctrlBackSequence);
+    defineKeyWithAltModifier(Shift + VK_BACK, backSequence);
+    defineKeyWithAltModifier(Ctrl + Shift + VK_BACK, ctrlBackSequence);
+
+    // TAB maps to HT, and Shift+TAB to CBT. The Ctrl modifier has no effect.
+    // The Alt modifier adds an ESC prefix, although in practice all the Alt
+    // mappings are likely to be system hotkeys.
+    const auto shiftTabSequence = fmt::format(FMT_COMPILE(L"{}Z"), _csi);
+    defineKeyWithAltModifier(VK_TAB, L"\t"s);
+    defineKeyWithAltModifier(Ctrl + VK_TAB, L"\t"s);
+    defineKeyWithAltModifier(Shift + VK_TAB, shiftTabSequence);
+    defineKeyWithAltModifier(Ctrl + Shift + VK_TAB, shiftTabSequence);
+
+    // RETURN maps to either CR or CR LF, depending on the Line Feed mode. With
+    // a Ctrl modifier it maps to LF, because that's the expected behavior for
+    // most PC keyboard layouts. The Alt modifier adds an ESC prefix.
+    const auto returnSequence = _inputMode.test(Mode::LineFeed) ? L"\r\n"s : L"\r"s;
+    defineKeyWithAltModifier(VK_RETURN, returnSequence);
+    defineKeyWithAltModifier(Shift + VK_RETURN, returnSequence);
+    defineKeyWithAltModifier(Ctrl + VK_RETURN, L"\n"s);
+    defineKeyWithAltModifier(Ctrl + Shift + VK_RETURN, L"\n"s);
+
+    // The keypad RETURN key works the same way, except when Keypad mode is
+    // enabled, but that's handled below with the other keypad keys.
+    defineKeyWithAltModifier(Enhanced + VK_RETURN, returnSequence);
+    defineKeyWithAltModifier(Shift + Enhanced + VK_RETURN, returnSequence);
+    defineKeyWithAltModifier(Ctrl + Enhanced + VK_RETURN, L"\n"s);
+    defineKeyWithAltModifier(Ctrl + Shift + Enhanced + VK_RETURN, L"\n"s);
+
+    // SPACE maps to SP, and Ctrl+SPACE to NUL. The Shift modifier as no effect.
+    // The Alt modifier adds an ESC prefix (not standard).
+    defineKeyWithAltModifier(VK_SPACE, L" "s);
+    defineKeyWithAltModifier(Shift + VK_SPACE, L" "s);
+    defineKeyWithAltModifier(Ctrl + VK_SPACE, L"\0"s);
+    defineKeyWithAltModifier(Ctrl + Shift + VK_SPACE, L"\0"s);
+
+    if (_inputMode.test(Mode::Ansi))
+    {
+        // F1 to F4 map to the VT keypad function keys, which are SS3 sequences.
+        // When combined with a modifier, we use CSI sequences with the modifier
+        // embedded as a parameter (not standard - a modern terminal extension).
+        defineKeypadKey(VK_F1, _ss3, L'P');
+        defineKeypadKey(VK_F2, _ss3, L'Q');
+        defineKeypadKey(VK_F3, _ss3, L'R');
+        defineKeypadKey(VK_F4, _ss3, L'S');
+
+        // F5 through F20 map to the top row VT function keys. They use standard
+        // DECFNK sequences with the modifier embedded as a parameter. The first
+        // five function keys on a VT terminal are typically local functions, so
+        // there's not much need to support mappings for them.
+        for (auto vk = VK_F5; vk <= VK_F20; vk++)
         {
-            return _makeCharOutput(0);
+            static constexpr std::array<uint8_t, 16> parameters = { 15, 17, 18, 19, 20, 21, 23, 24, 25, 26, 28, 29, 31, 32, 33, 34 };
+            const auto parm = parameters.at(static_cast<size_t>(vk) - VK_F5);
+            defineEditingKey(vk, parm);
         }
 
-        // Not all keyboard layouts contain mappings for Ctrl-key combinations.
-        // For instance the US one contains a mapping of Ctrl+\ to ^\,
-        // but the UK extended layout doesn't, in which case ch is null.
-        if (ch == UNICODE_NULL)
+        // Cursor keys follow a similar pattern to the VT keypad function keys,
+        // although they only use an SS3 prefix when the Cursor Key mode is set.
+        // When combined with a modifier, they'll use CSI sequences with the
+        // modifier embedded as a parameter (again not standard).
+        const auto ckIntroducer = _inputMode.test(Mode::CursorKey) ? _ss3 : _csi;
+        defineKeypadKey(VK_UP, ckIntroducer, L'A');
+        defineKeypadKey(VK_DOWN, ckIntroducer, L'B');
+        defineKeypadKey(VK_RIGHT, ckIntroducer, L'C');
+        defineKeypadKey(VK_LEFT, ckIntroducer, L'D');
+        defineKeypadKey(VK_CLEAR, ckIntroducer, L'E');
+        defineKeypadKey(VK_HOME, ckIntroducer, L'H');
+        defineKeypadKey(VK_END, ckIntroducer, L'F');
+
+        // Editing keys follow the same pattern as the top row VT function
+        // keys, using standard DECFNK sequences with the modifier embedded.
+        defineEditingKey(VK_INSERT, 2);
+        defineEditingKey(VK_DELETE, 3);
+        defineEditingKey(VK_PRIOR, 5);
+        defineEditingKey(VK_NEXT, 6);
+
+        // Keypad keys depend on the Keypad mode. When reset, they transmit
+        // the ASCII character assigned by the keyboard layout, but when set
+        // they transmit SS3 escape sequences. When used with a modifier, the
+        // modifier is embedded as a parameter value (not standard).
+        if (_inputMode.test(Mode::Keypad))
         {
-            // -> Try to infer the character from the vkey.
-            auto mappedChar = LOWORD(OneCoreSafeMapVirtualKeyW(keyEvent.wVirtualKeyCode, MAPVK_VK_TO_CHAR));
-            if (mappedChar)
-            {
-                // Pressing the control key causes all bits but the 5 least
-                // significant ones to be zeroed out (when using ASCII).
-                mappedChar &= 0b11111;
-                return _makeCharOutput(mappedChar);
-            }
+            defineNumericKey(VK_MULTIPLY, L'j');
+            defineNumericKey(VK_ADD, L'k');
+            defineNumericKey(VK_SEPARATOR, L'l');
+            defineNumericKey(VK_SUBTRACT, L'm');
+            defineNumericKey(VK_DECIMAL, L'n');
+            defineNumericKey(VK_DIVIDE, L'o');
+
+            defineNumericKey(VK_NUMPAD0, L'p');
+            defineNumericKey(VK_NUMPAD1, L'q');
+            defineNumericKey(VK_NUMPAD2, L'r');
+            defineNumericKey(VK_NUMPAD3, L's');
+            defineNumericKey(VK_NUMPAD4, L't');
+            defineNumericKey(VK_NUMPAD5, L'u');
+            defineNumericKey(VK_NUMPAD6, L'v');
+            defineNumericKey(VK_NUMPAD7, L'w');
+            defineNumericKey(VK_NUMPAD8, L'x');
+            defineNumericKey(VK_NUMPAD9, L'y');
+
+            defineNumericKey(Enhanced + VK_RETURN, L'M');
         }
     }
-
-    // Check any other key mappings (like those for the F1-F12 keys).
-    // These mappings will kick in no matter which modifiers are pressed and as such
-    // must be checked last, or otherwise we'd override more complex key combinations.
-    const auto mapping = _getKeyMapping(keyEvent, _inputMode.test(Mode::Ansi), _inputMode.test(Mode::CursorKey), _inputMode.test(Mode::Keypad));
-    if (const auto match = _searchKeyMapping(keyEvent, mapping))
+    else
     {
-        return MakeOutput(match->sequence);
+        // In VT52 mode, the sequences tend to use the same final character as
+        // their ANSI counterparts, but with a simple ESC prefix. The modifier
+        // keys have no effect.
+
+        // VT52 only support PF1 through PF4 function keys.
+        defineKeyWithUnusedModifiers(VK_F1, L"\033P"s);
+        defineKeyWithUnusedModifiers(VK_F2, L"\033Q"s);
+        defineKeyWithUnusedModifiers(VK_F3, L"\033R"s);
+        defineKeyWithUnusedModifiers(VK_F4, L"\033S"s);
+
+        // But terminals with application functions keys would
+        // map some of them as controls keys in VT52 mode.
+        defineKeyWithUnusedModifiers(VK_F11, L"\033"s);
+        defineKeyWithUnusedModifiers(VK_F12, L"\b"s);
+        defineKeyWithUnusedModifiers(VK_F13, L"\n"s);
+
+        // Cursor keys use the same finals as the ANSI sequences.
+        defineKeyWithUnusedModifiers(VK_UP, L"\033A"s);
+        defineKeyWithUnusedModifiers(VK_DOWN, L"\033B"s);
+        defineKeyWithUnusedModifiers(VK_RIGHT, L"\033C"s);
+        defineKeyWithUnusedModifiers(VK_LEFT, L"\033D"s);
+        defineKeyWithUnusedModifiers(VK_CLEAR, L"\033E"s);
+        defineKeyWithUnusedModifiers(VK_HOME, L"\033H"s);
+        defineKeyWithUnusedModifiers(VK_END, L"\033F"s);
+
+        // Keypad keys also depend on Keypad mode, the same as ANSI mappings,
+        // but the sequences use an ESC ? prefix instead of SS3.
+        if (_inputMode.test(Mode::Keypad))
+        {
+            defineKeyWithUnusedModifiers(VK_MULTIPLY, L"\033?j"s);
+            defineKeyWithUnusedModifiers(VK_ADD, L"\033?k"s);
+            defineKeyWithUnusedModifiers(VK_SEPARATOR, L"\033?l"s);
+            defineKeyWithUnusedModifiers(VK_SUBTRACT, L"\033?m"s);
+            defineKeyWithUnusedModifiers(VK_DECIMAL, L"\033?n"s);
+            defineKeyWithUnusedModifiers(VK_DIVIDE, L"\033?o"s);
+
+            defineKeyWithUnusedModifiers(VK_NUMPAD0, L"\033?p"s);
+            defineKeyWithUnusedModifiers(VK_NUMPAD1, L"\033?q"s);
+            defineKeyWithUnusedModifiers(VK_NUMPAD2, L"\033?r"s);
+            defineKeyWithUnusedModifiers(VK_NUMPAD3, L"\033?s"s);
+            defineKeyWithUnusedModifiers(VK_NUMPAD4, L"\033?t"s);
+            defineKeyWithUnusedModifiers(VK_NUMPAD5, L"\033?u"s);
+            defineKeyWithUnusedModifiers(VK_NUMPAD6, L"\033?v"s);
+            defineKeyWithUnusedModifiers(VK_NUMPAD7, L"\033?w"s);
+            defineKeyWithUnusedModifiers(VK_NUMPAD8, L"\033?x"s);
+            defineKeyWithUnusedModifiers(VK_NUMPAD9, L"\033?y"s);
+
+            defineKeyWithUnusedModifiers(Enhanced + VK_RETURN, L"\033?M"s);
+        }
     }
 
-    // If all else fails we can finally try to send the character itself if there is any.
-    if (keyEvent.uChar.UnicodeChar != 0)
+    _focusInSequence = _csi + L"I"s;
+    _focusOutSequence = _csi + L"O"s;
+}
+CATCH_LOG()
+
+DWORD TerminalInput::_trackControlKeyState(const KEY_EVENT_RECORD& key)
+{
+    // First record which key state bits were previously off but are now on.
+    const auto pressedKeyState = ~_lastControlKeyState & key.dwControlKeyState;
+    // Then save the new key state so we can determine future state changes.
+    _lastControlKeyState = key.dwControlKeyState;
+    // But if this latest change has set the RightAlt bit, without having
+    // received a RightAlt key press, then we need to clear that bit. This
+    // can happen when pressing the AltGr key on the On-Screen keyboard. It
+    // actually generates LeftCtrl and LeftAlt key presses, but also sets
+    // the RightAlt bit on the final key state. If we don't clear that, it
+    // can be misinterpreted as an Alt+AltGr key combination.
+    const auto rightAltDown = key.bKeyDown && key.wVirtualKeyCode == VK_MENU && WI_IsFlagSet(key.dwControlKeyState, ENHANCED_KEY);
+    WI_ClearFlagIf(_lastControlKeyState, RIGHT_ALT_PRESSED, WI_IsFlagSet(pressedKeyState, RIGHT_ALT_PRESSED) && !rightAltDown);
+    // We also take this opportunity to record the time at which the LeftCtrl
+    // and RightAlt keys are pressed. This is needed to determine whether the
+    // Ctrl key was pressed by the user, or fabricated by an AltGr key press.
+    if (key.bKeyDown)
     {
-        return _makeCharOutput(keyEvent.uChar.UnicodeChar);
+        if (WI_IsFlagSet(pressedKeyState, LEFT_CTRL_PRESSED))
+        {
+            _lastLeftCtrlTime = GetTickCount64();
+        }
+        if (WI_IsFlagSet(pressedKeyState, RIGHT_ALT_PRESSED))
+        {
+            _lastRightAltTime = GetTickCount64();
+        }
     }
-
-    return MakeUnhandled();
+    return _lastControlKeyState;
 }
 
-TerminalInput::OutputType TerminalInput::HandleFocus(const bool focused) const
+// Returns a simplified representation of the keyboard state, based on the most
+// recent key press and associated control key state (which is all we need for
+// our ToUnicodeEx queries). This is a substitute for the GetKeyboardState API,
+// which can't be used when serving as a conpty host.
+std::array<byte, 256> TerminalInput::_getKeyboardState(const WORD virtualKeyCode, const DWORD controlKeyState) const
 {
-    if (!_inputMode.test(Mode::FocusEvent))
+    auto keyState = std::array<byte, 256>{};
+    if (virtualKeyCode < keyState.size())
     {
-        return MakeUnhandled();
+        keyState.at(virtualKeyCode) = 0x80;
     }
+    keyState.at(VK_LCONTROL) = WI_IsFlagSet(controlKeyState, LEFT_CTRL_PRESSED) ? 0x80 : 0;
+    keyState.at(VK_RCONTROL) = WI_IsFlagSet(controlKeyState, RIGHT_CTRL_PRESSED) ? 0x80 : 0;
+    keyState.at(VK_CONTROL) = keyState.at(VK_LCONTROL) | keyState.at(VK_RCONTROL);
+    keyState.at(VK_LMENU) = WI_IsFlagSet(controlKeyState, LEFT_ALT_PRESSED) ? 0x80 : 0;
+    keyState.at(VK_RMENU) = WI_IsFlagSet(controlKeyState, RIGHT_ALT_PRESSED) ? 0x80 : 0;
+    keyState.at(VK_MENU) = keyState.at(VK_LMENU) | keyState.at(VK_RMENU);
+    keyState.at(VK_SHIFT) = keyState.at(VK_LSHIFT) = WI_IsFlagSet(controlKeyState, SHIFT_PRESSED) ? 0x80 : 0;
+    keyState.at(VK_CAPITAL) = WI_IsFlagSet(controlKeyState, CAPSLOCK_ON);
+    return keyState;
+}
 
-    return MakeOutput(focused ? L"\x1b[I" : L"\x1b[O");
+wchar_t TerminalInput::_makeCtrlChar(const wchar_t ch)
+{
+    if (ch >= L'@' && ch <= L'~')
+    {
+        return ch & 0b11111;
+    }
+    if (ch == L'/')
+    {
+        return 0x1F;
+    }
+    if (ch == L'?')
+    {
+        return 0x7F;
+    }
+    if (ch >= L'2' && ch <= L'8')
+    {
+        constexpr auto numericCtrls = std::array<wchar_t, 7>{ 0, 27, 28, 29, 30, 31, 127 };
+        return numericCtrls.at(ch - L'2');
+    }
+    return ch;
 }
 
-// Turns the given character into OutputType.
+// Turns the given character into StringType.
 // If it encounters a surrogate pair, it'll buffer the leading character until a
 // trailing one has been received and then flush both of them simultaneously.
 // Surrogate pairs should always be handled as proper pairs after all.
-TerminalInput::OutputType TerminalInput::_makeCharOutput(const wchar_t ch)
+TerminalInput::StringType TerminalInput::_makeCharOutput(const wchar_t ch)
 {
     StringType str;
 
-    if (til::is_leading_surrogate(ch))
-    {
-        _leadingSurrogate.emplace(ch);
-    }
-    else if (_leadingSurrogate)
-    {
-        const auto lead = *_leadingSurrogate;
-        _leadingSurrogate.reset();
-
-        if (til::is_trailing_surrogate(ch))
-        {
-            str.push_back(lead);
-            str.push_back(ch);
-        }
-    }
-    else
+    if (_leadingSurrogate && til::is_trailing_surrogate(ch))
     {
-        str.push_back(ch);
+        str.push_back(_leadingSurrogate);
     }
 
+    str.push_back(ch);
     return str;
 }
 
-// Sends the given char as a sequence representing Alt+wch, also the same as Meta+wch.
-TerminalInput::OutputType TerminalInput::_makeEscapedOutput(const wchar_t wch)
+TerminalInput::StringType TerminalInput::_makeNoOutput() noexcept
 {
-    StringType str;
-    str.push_back(L'\x1b');
-    str.push_back(wch);
-    return str;
+    return {};
+}
+
+// Sends the given char as a sequence representing Alt+char, also the same as Meta+char.
+void TerminalInput::_escapeOutput(StringType& charSequence, const bool altIsPressed) const
+{
+    // Alt+char combinations are only applicable in ANSI mode.
+    if (altIsPressed && _inputMode.test(Mode::Ansi))
+    {
+        charSequence.insert(0, 1, L'\x1b');
+    }
 }
 
 // Turns an KEY_EVENT_RECORD into a win32-input-mode VT sequence.
 // It allows us to send KEY_EVENT_RECORD data losslessly to conhost.
-TerminalInput::OutputType TerminalInput::_makeWin32Output(const KEY_EVENT_RECORD& key)
+TerminalInput::OutputType TerminalInput::_makeWin32Output(const KEY_EVENT_RECORD& key) const
 {
     // .uChar.UnicodeChar must be cast to an integer because we want its numerical value.
     // Casting the rest to uint16_t as well doesn't hurt because that's MAX_PARAMETER_VALUE anyways.
@@ -728,7 +644,7 @@ TerminalInput::OutputType TerminalInput::_makeWin32Output(const KEY_EVENT_RECORD
 
     // Sequences are formatted as follows:
     //
-    // ^[ [ Vk ; Sc ; Uc ; Kd ; Cs ; Rc _
+    // CSI Vk ; Sc ; Uc ; Kd ; Cs ; Rc _
     //
     //      Vk: the value of wVirtualKeyCode - any number. If omitted, defaults to '0'.
     //      Sc: the value of wVirtualScanCode - any number. If omitted, defaults to '0'.
@@ -737,5 +653,5 @@ TerminalInput::OutputType TerminalInput::_makeWin32Output(const KEY_EVENT_RECORD
     //      Kd: the value of bKeyDown - either a '0' or '1'. If omitted, defaults to '0'.
     //      Cs: the value of dwControlKeyState - any number. If omitted, defaults to '0'.
     //      Rc: the value of wRepeatCount - any number. If omitted, defaults to '1'.
-    return fmt::format(FMT_COMPILE(L"\x1b[{};{};{};{};{};{}_"), vk, sc, uc, kd, cs, rc);
+    return fmt::format(FMT_COMPILE(L"{}{};{};{};{};{};{}_"), _csi, vk, sc, uc, kd, cs, rc);
 }
diff --git a/src/terminal/input/terminalInput.hpp b/src/terminal/input/terminalInput.hpp
index 1bb49cca7b2..84633b92d8c 100644
--- a/src/terminal/input/terminalInput.hpp
+++ b/src/terminal/input/terminalInput.hpp
@@ -46,6 +46,7 @@ namespace Microsoft::Console::VirtualTerminal
             AlternateScroll
         };
 
+        TerminalInput() noexcept;
         void SetInputMode(const Mode mode, const bool enabled) noexcept;
         bool GetInputMode(const Mode mode) const noexcept;
         void ResetInputModes() noexcept;
@@ -66,17 +67,32 @@ namespace Microsoft::Console::VirtualTerminal
 
     private:
         // storage location for the leading surrogate of a utf-16 surrogate pair
-        std::optional<wchar_t> _leadingSurrogate;
+        wchar_t _leadingSurrogate = 0;
 
         std::optional<WORD> _lastVirtualKeyCode;
+        DWORD _lastControlKeyState = 0;
+        uint64_t _lastLeftCtrlTime = 0;
+        uint64_t _lastRightAltTime = 0;
+        std::unordered_map<int, std::wstring> _keyMap;
+        std::wstring _focusInSequence;
+        std::wstring _focusOutSequence;
 
         til::enumset<Mode> _inputMode{ Mode::Ansi, Mode::AutoRepeat };
         bool _forceDisableWin32InputMode{ false };
 
-        [[nodiscard]] OutputType _makeCharOutput(wchar_t ch);
-        [[nodiscard]] static OutputType _makeEscapedOutput(wchar_t wch);
-        [[nodiscard]] static OutputType _makeWin32Output(const KEY_EVENT_RECORD& key);
-        [[nodiscard]] static OutputType _searchWithModifier(const KEY_EVENT_RECORD& keyEvent);
+        // 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";
+
+        void _initKeyboardMap() noexcept;
+        DWORD _trackControlKeyState(const KEY_EVENT_RECORD& key);
+        std::array<byte, 256> _getKeyboardState(const WORD virtualKeyCode, const DWORD controlKeyState) const;
+        [[nodiscard]] static wchar_t _makeCtrlChar(const wchar_t ch);
+        [[nodiscard]] StringType _makeCharOutput(wchar_t ch);
+        [[nodiscard]] static StringType _makeNoOutput() noexcept;
+        [[nodiscard]] void _escapeOutput(StringType& charSequence, const bool altIsPressed) const;
+        [[nodiscard]] OutputType _makeWin32Output(const KEY_EVENT_RECORD& key) const;
 
 #pragma region MouseInputState Management
         // These methods are defined in mouseInputState.cpp


Test patch: