From dccc1f42407c610aea04adb20c912b4ef0e493f8 Mon Sep 17 00:00:00 2001 From: James Holderness Date: Tue, 30 Jan 2024 00:58:39 +0000 Subject: [PATCH] Refactor VT terminal input (#16511) The primary reason for this refactoring was to simplify the management of VT input sequences that vary depending on modes, adding support for the missing application keypad sequences, and preparing the way for future extensions like `S8C1T`. However, it also includes fixes for a number of keyboard related bugs, including a variety of missing or incorrect mappings for the `Ctrl` and `Ctrl`+`Alt` key combinations, ## References and Relevant Issues This PR also includes a fix for #10308, which was previously closed as a duplicate of #10551. I don't think those bugs were related, though, and although they're both supposed to be fixed in Windows 11, this PR fixes the issue in Windows 10. ## Detailed Description of the Pull Request / Additional comments The way the input now works, there's a single keyboard map that takes a virtual key code combined with `Ctrl`, `Alt`, and `Shift` modifier bits as the lookup key, and the expected VT input sequence as the value. This map is initially constructed at startup, and then regenerated whenever a keyboard mode is changed. This map takes care of the cursor keys, editing keys, function keys, and keys like `BkSp` and `Return` which can be affected by mode changes. The remaining "graphic" key combinations are determined manually at the time of input. The order of precedence looks like this: 1. If the virtual key is `0` or `VK_PACKET`, it's considered to be a synthesized keyboard event, and the `UnicodeChar` value is used exactly as given. 2. If it's a numeric keypad key, and `Alt` is pressed (but not `Ctrl`), then it's assumedly part of an Alt-Numpad composition, so the key press is ignored (the generated character will be transmitted when the `Alt` is released). 3. If the virtual key combined with modifier bits is found in the key map described above, then the matched escape sequence will be used used as the output. 4. If a `UnicodeChar` value has been provided, that will be used as the output, but possibly with additional Ctrl and Alt modifiers applied: a. If it's an `AltGr` key, and we've got either two `Ctrl` keys pressed or a left `Ctrl` key that is distinctly separate from a right `Alt` key, then we will try and convert the character into a C0 control code. b. If an `Alt` key is pressed (or in the case of an `AltGr` value, both `Alt` keys are pressed), then we will convert it into an Alt-key sequence by prefixing the character with an `ESC`. 5. If we don't have a `UnicodeChar`, we'll use the `ToUnicodeEx` API to check whether the current keyboard state reflects a dead key, and if so, return nothing. 6. Otherwise we'll make another `ToUnicodeEx` call but with any `Ctrl` and `Alt` modifiers removed from the state to determine the base key value. Once we have that, we can apply the modifiers ourself. a. If the `Ctrl` key is pressed, we'll try and convert the base value into a C0 control code. But if we can't do that, we'll try again with the virtual key code (if it's alphanumeric) as a fallback. b. If the `Alt` key is pressed, we'll convert the base value (or control code value) into an Alt-key sequence by prefixing it with an `ESC`. For step 4-a, we determine whether the left `Ctrl` key is distinctly separate from the right `Alt` key by recording the time that those keys are pressed, and checking for a time gap greater than 50ms. This is necessary to distinguish between the user pressing `Ctrl`+`AltGr`, or just pressing `AltGr` alone, which triggers a fake `Ctrl` key press at the same time. ## Validation Steps Performed I created a test script to automate key presses in the terminal window for every relevant key, along with every Ctrl/Alt/Shift modifier, and every relevant mode combination. I then compared the generated input sequences with XTerm and a DEC VT240 terminal. The idea wasn't to match either of them exactly, but to make sure the places where we differed were intentional and reasonable. This mostly dealt with the US keyboard layout. Comparing international layouts wasn't really feasible because DEC, Linux, and Windows keyboard assignments tend to be quite different. However, I've manually tested a number of different layouts, and tried to make sure that they were all working in a reasonable manner. In terms of unit testing, I haven't done much more than patching the ones that already existed to get them to pass. They're honestly not great tests, because they aren't generating events in the form that you'd expect for a genuine key press, and that can significantly affect the results, but I can't think of an easy way to improve them. ## PR Checklist - [x] Closes #16506 - [x] Closes #16508 - [x] Closes #16509 - [x] Closes #16510 - [x] Closes #3483 - [x] Closes #11194 - [x] Closes #11700 - [x] Closes #12555 - [x] Closes #13319 - [x] Closes #15367 - [x] Closes #16173 - [x] Tests added/passed --- .github/actions/spelling/expect/expect.txt | 2 + src/cascadia/TerminalControl/TermControl.cpp | 2 +- src/terminal/adapter/ut_adapter/inputTest.cpp | 218 +++- src/terminal/input/mouseInput.cpp | 43 +- src/terminal/input/terminalInput.cpp | 1004 ++++++++--------- src/terminal/input/terminalInput.hpp | 26 +- 6 files changed, 661 insertions(+), 634 deletions(-) diff --git a/.github/actions/spelling/expect/expect.txt b/.github/actions/spelling/expect/expect.txt index da8dff4ed68..c1a99686924 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 e24168488bf..5978149cd36 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -1336,7 +1336,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(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 d17482216ca..596df0360e1 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(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(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(L' ' + _windowsButtonToXEncoding(button, isHover, modifierKeyState, delta)); - til::at(format, 4) = gsl::narrow_cast(encodedX); - til::at(format, 5) = gsl::narrow_cast(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(L' ' + _windowsButtonToXEncoding(button, isHover, modifierKeyState, delta)); - til::at(format, 4) = gsl::narrow_cast(encodedX); - til::at(format, 5) = gsl::narrow_cast(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 ae5e94ed22c..bdd10ca8d63 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 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 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 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 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 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 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 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 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('1'), CTRL_PRESSED, L"1" }, - // TermKeyMap{ static_cast('2'), CTRL_PRESSED, L"\x00" }, - TermKeyMap{ static_cast('3'), CTRL_PRESSED, L"\x1B" }, - TermKeyMap{ static_cast('4'), CTRL_PRESSED, L"\x1C" }, - TermKeyMap{ static_cast('5'), CTRL_PRESSED, L"\x1D" }, - TermKeyMap{ static_cast('6'), CTRL_PRESSED, L"\x1E" }, - TermKeyMap{ static_cast('7'), CTRL_PRESSED, L"\x1F" }, - TermKeyMap{ static_cast('8'), CTRL_PRESSED, L"\x7F" }, - TermKeyMap{ static_cast('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::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, Mode::AlternateScroll }; _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 _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 _searchKeyMapping(const KEY_EVENT_RECORD& keyEvent, - std::span 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 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{}; + + // 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(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 parameters = { 15, 17, 18, 19, 20, 21, 23, 24, 25, 26, 28, 29, 31, 32, 33, 34 }; + const auto parm = parameters.at(static_cast(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 TerminalInput::_getKeyboardState(const WORD virtualKeyCode, const DWORD controlKeyState) const { - if (!_inputMode.test(Mode::FocusEvent)) + auto keyState = std::array{}; + 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{ 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 17cf2a6fef3..e8ae53267e2 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 _leadingSurrogate; + wchar_t _leadingSurrogate = 0; std::optional _lastVirtualKeyCode; + DWORD _lastControlKeyState = 0; + uint64_t _lastLeftCtrlTime = 0; + uint64_t _lastRightAltTime = 0; + std::unordered_map _keyMap; + std::wstring _focusInSequence; + std::wstring _focusOutSequence; til::enumset _inputMode{ Mode::Ansi, Mode::AutoRepeat, Mode::AlternateScroll }; 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 _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