diff --git a/src/cascadia/PublicTerminalCore/HwndTerminal.cpp b/src/cascadia/PublicTerminalCore/HwndTerminal.cpp index 995f7ce978b..0e599b786b1 100644 --- a/src/cascadia/PublicTerminalCore/HwndTerminal.cpp +++ b/src/cascadia/PublicTerminalCore/HwndTerminal.cpp @@ -427,10 +427,8 @@ const wchar_t* _stdcall TerminalGetSelection(void* terminal) return returnText.release(); } -void _stdcall TerminalSendKeyEvent(void* terminal, WPARAM wParam) +static ControlKeyStates getControlKeyState() noexcept { - const auto publicTerminal = static_cast(terminal); - const auto scanCode = MapVirtualKeyW((UINT)wParam, MAPVK_VK_TO_VSC); struct KeyModifier { int vkey; @@ -458,10 +456,17 @@ void _stdcall TerminalSendKeyEvent(void* terminal, WPARAM wParam) } } - publicTerminal->_terminal->SendKeyEvent((WORD)wParam, (WORD)scanCode, flags); + return flags; +} + +void _stdcall TerminalSendKeyEvent(void* terminal, WORD vkey, WORD scanCode) +{ + const auto publicTerminal = static_cast(terminal); + const auto flags = getControlKeyState(); + publicTerminal->_terminal->SendKeyEvent(vkey, scanCode, flags); } -void _stdcall TerminalSendCharEvent(void* terminal, wchar_t ch) +void _stdcall TerminalSendCharEvent(void* terminal, wchar_t ch, WORD scanCode) { if (ch == '\t') { @@ -469,7 +474,8 @@ void _stdcall TerminalSendCharEvent(void* terminal, wchar_t ch) } const auto publicTerminal = static_cast(terminal); - publicTerminal->_terminal->SendCharEvent(ch); + const auto flags = getControlKeyState(); + publicTerminal->_terminal->SendCharEvent(ch, scanCode, flags); } void _stdcall DestroyTerminal(void* terminal) diff --git a/src/cascadia/PublicTerminalCore/HwndTerminal.hpp b/src/cascadia/PublicTerminalCore/HwndTerminal.hpp index 172a462b898..3e03cf94b1a 100644 --- a/src/cascadia/PublicTerminalCore/HwndTerminal.hpp +++ b/src/cascadia/PublicTerminalCore/HwndTerminal.hpp @@ -34,8 +34,8 @@ __declspec(dllexport) bool _stdcall TerminalIsSelectionActive(void* terminal); __declspec(dllexport) void _stdcall DestroyTerminal(void* terminal); __declspec(dllexport) void _stdcall TerminalSetTheme(void* terminal, TerminalTheme theme, LPCWSTR fontFamily, short fontSize, int newDpi); __declspec(dllexport) void _stdcall TerminalRegisterWriteCallback(void* terminal, const void __stdcall callback(wchar_t*)); -__declspec(dllexport) void _stdcall TerminalSendKeyEvent(void* terminal, WPARAM wParam); -__declspec(dllexport) void _stdcall TerminalSendCharEvent(void* terminal, wchar_t ch); +__declspec(dllexport) void _stdcall TerminalSendKeyEvent(void* terminal, WORD vkey, WORD scanCode); +__declspec(dllexport) void _stdcall TerminalSendCharEvent(void* terminal, wchar_t ch, WORD scanCode); __declspec(dllexport) void _stdcall TerminalBlinkCursor(void* terminal); __declspec(dllexport) void _stdcall TerminalSetCursorVisible(void* terminal, const bool visible); }; @@ -82,8 +82,8 @@ struct HwndTerminal : ::Microsoft::Console::Types::IControlAccessibilityInfo friend void _stdcall TerminalClearSelection(void* terminal); friend const wchar_t* _stdcall TerminalGetSelection(void* terminal); friend bool _stdcall TerminalIsSelectionActive(void* terminal); - friend void _stdcall TerminalSendKeyEvent(void* terminal, WPARAM wParam); - friend void _stdcall TerminalSendCharEvent(void* terminal, wchar_t ch); + friend void _stdcall TerminalSendKeyEvent(void* terminal, WORD vkey, WORD scanCode); + friend void _stdcall TerminalSendCharEvent(void* terminal, wchar_t ch, WORD scanCode); friend void _stdcall TerminalSetTheme(void* terminal, TerminalTheme theme, LPCWSTR fontFamily, short fontSize, int newDpi); friend void _stdcall TerminalBlinkCursor(void* terminal); friend void _stdcall TerminalSetCursorVisible(void* terminal, const bool visible); diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index 914d9440906..2ce794eecc1 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -650,8 +650,9 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation } const auto ch = e.Character(); - - const bool handled = _terminal->SendCharEvent(ch); + const auto scanCode = gsl::narrow_cast(e.KeyStatus().ScanCode); + const auto modifiers = _GetPressedModifierKeys(); + const bool handled = _terminal->SendCharEvent(ch, scanCode, modifiers); e.Handled(handled); } diff --git a/src/cascadia/TerminalCore/ITerminalInput.hpp b/src/cascadia/TerminalCore/ITerminalInput.hpp index 9faee0cb250..5df7a0845d3 100644 --- a/src/cascadia/TerminalCore/ITerminalInput.hpp +++ b/src/cascadia/TerminalCore/ITerminalInput.hpp @@ -18,7 +18,7 @@ namespace Microsoft::Terminal::Core virtual bool SendKeyEvent(const WORD vkey, const WORD scanCode, const ControlKeyStates states) = 0; virtual bool SendMouseEvent(const COORD viewportPos, const unsigned int uiButton, const ControlKeyStates states, const short wheelDelta) = 0; - virtual bool SendCharEvent(const wchar_t ch) = 0; + virtual bool SendCharEvent(const wchar_t ch, const WORD scanCode, const ControlKeyStates states) = 0; // void SendMouseEvent(uint row, uint col, KeyModifiers modifiers); [[nodiscard]] virtual HRESULT UserResize(const COORD size) noexcept = 0; diff --git a/src/cascadia/TerminalCore/Terminal.cpp b/src/cascadia/TerminalCore/Terminal.cpp index b3471ee68eb..457fc2c2d98 100644 --- a/src/cascadia/TerminalCore/Terminal.cpp +++ b/src/cascadia/TerminalCore/Terminal.cpp @@ -387,14 +387,21 @@ bool Terminal::IsTrackingMouseInput() const noexcept } // Method Description: -// - Send this particular key event to the terminal. The terminal will translate -// the key and the modifiers pressed into the appropriate VT sequence for that -// key chord. If we do translate the key, we'll return true. In that case, the -// event should NOT be processed any further. If we return false, the event -// was NOT translated, and we should instead use the event to try and get the -// real character out of the event. +// - Send this particular (non-character) key event to the terminal. +// - The terminal will translate the key and the modifiers pressed into the +// appropriate VT sequence for that key chord. If we do translate the key, +// we'll return true. In that case, the event should NOT be processed any further. +// - Character events (e.g. WM_CHAR) are generally the best way to properly receive +// keyboard input on Windows though, as the OS is suited best at handling the +// translation of the current keyboard layout, dead keys, etc. +// As a result of this false is returned for all key events that contain characters. +// SendCharEvent may then be called with the data obtained from a character event. +// - As a special case we'll always handle VK_TAB key events. +// This must be done due to TermControl::_KeyDownHandler (one of the callers) +// always marking tab key events as handled, causing no character event to be raised. // Arguments: -// - vkey: The vkey of the key pressed. +// - vkey: The vkey of the last pressed key. +// - scanCode: The scan code of the last pressed key. // - states: The Microsoft::Terminal::Core::ControlKeyStates representing the modifier key states. // Return Value: // - true if we translated the key event, and it should not be processed any further. @@ -402,50 +409,35 @@ bool Terminal::IsTrackingMouseInput() const noexcept bool Terminal::SendKeyEvent(const WORD vkey, const WORD scanCode, const ControlKeyStates states) { TrySnapOnInput(); + _StoreKeyEvent(vkey, scanCode); + + const auto isAltOnlyPressed = states.IsAltPressed() && !states.IsCtrlPressed(); - // Alt key sequences _require_ the char to be in the keyevent. If alt is - // pressed, manually get the character that's being typed, and put it in the - // KeyEvent. // DON'T manually handle Alt+Space - the system will use this to bring up - // the system menu for restore, min/maximize, size, move, close - wchar_t ch = UNICODE_NULL; - if (states.IsAltPressed() && vkey != VK_SPACE) + // the system menu for restore, min/maximize, size, move, close. + // (This doesn't apply to Ctrl+Alt+Space.) + if (isAltOnlyPressed && vkey == VK_SPACE) { - ch = _CharacterFromKeyEvent(vkey, scanCode, states); + return false; } - if (states.IsCtrlPressed()) - { - switch (vkey) - { - case 0x48: - // Manually handle Ctrl+H. Ctrl+H should be handled as Backspace. To do this - // correctly, the keyEvents's char needs to be set to Backspace. - // 0x48 is the VKEY for 'H', which isn't named - ch = UNICODE_BACKSPACE; - break; - case VK_SPACE: - // Manually handle Ctrl+Space here. The terminalInput translator requires - // the char to be set to Space for space handling to work correctly. - ch = UNICODE_SPACE; - break; - } - } + const auto ch = _CharacterFromKeyEvent(vkey, scanCode, states); - // Manually handle Escape here. If we let it fall through, it'll come - // back up through the character handler. It's registered as a translation - // in TerminalInput, so we'll let TerminalInput control it. - if (vkey == VK_ESCAPE) + // Delegate it to the character event handler if this key event can be + // mapped to one (see method description above). For Alt+key combinations + // we'll not receive another character event for some reason though. + // -> Don't delegate the event if this is a Alt+key combination. + // + // As a special case we'll furthermore always handle VK_TAB + // key events here instead of in Terminal::SendCharEvent. + // See the method description for more information. + if (!isAltOnlyPressed && vkey != VK_TAB && ch != UNICODE_NULL) { - ch = UNICODE_ESC; + return false; } - const bool manuallyHandled = ch != UNICODE_NULL; - KeyEvent keyEv{ true, 0, vkey, scanCode, ch, states.Value() }; - const bool translated = _terminalInput->HandleKey(&keyEv); - - return translated && manuallyHandled; + return _terminalInput->HandleKey(&keyEv); } // Method Description: @@ -474,9 +466,39 @@ bool Terminal::SendMouseEvent(const COORD viewportPos, const unsigned int uiButt return _terminalInput->HandleMouse(viewportPos, uiButton, GET_KEYSTATE_WPARAM(states.Value()), wheelDelta); } -bool Terminal::SendCharEvent(const wchar_t ch) +// Method Description: +// - Send this particular character to the terminal. +// - This method is the counterpart to SendKeyEvent and behaves almost identical. +// The difference is the focus on sending characters to the terminal, +// whereas SendKeyEvent handles the sending of keys like the arrow keys. +// Arguments: +// - ch: The UTF-16 code unit to be sent. +// - scanCode: The scan code of the last pressed key. Can be left 0. +// - states: The Microsoft::Terminal::Core::ControlKeyStates representing the modifier key states. +// Return Value: +// - true if we translated the character event, and it should not be processed any further. +// - false otherwise. +bool Terminal::SendCharEvent(const wchar_t ch, const WORD scanCode, const ControlKeyStates states) { - return _terminalInput->HandleChar(ch); + // DON'T manually handle Alt+Space - the system will use this to bring up + // the system menu for restore, min/maximize, size, move, close. + if (ch == L' ' && states.IsAltPressed() && !states.IsCtrlPressed()) + { + return false; + } + + auto vkey = _TakeVirtualKeyFromLastKeyEvent(scanCode); + if (vkey == 0 && scanCode != 0) + { + vkey = _VirtualKeyFromScanCode(scanCode); + } + if (vkey == 0) + { + vkey = _VirtualKeyFromCharacter(ch); + } + + KeyEvent keyEv{ true, 0, vkey, scanCode, ch, states.Value() }; + return _terminalInput->HandleKey(&keyEv); } // Method Description: @@ -490,6 +512,29 @@ WORD Terminal::_ScanCodeFromVirtualKey(const WORD vkey) noexcept return LOWORD(MapVirtualKeyW(vkey, MAPVK_VK_TO_VSC)); } +// Method Description: +// - Returns the virtual key code for the given keyboard's scan code. +// Arguments: +// - scanCode: The keyboard's scan code. +// Return Value: +// - The virtual key code. 0 if no mapping can be found. +WORD Terminal::_VirtualKeyFromScanCode(const WORD scanCode) noexcept +{ + return LOWORD(MapVirtualKeyW(scanCode, MAPVK_VSC_TO_VK)); +} + +// Method Description: +// - Returns any virtual key code that produces the given character. +// Arguments: +// - scanCode: The keyboard's scan code. +// Return Value: +// - The virtual key code. 0 if no mapping can be found. +WORD Terminal::_VirtualKeyFromCharacter(const wchar_t ch) noexcept +{ + const auto vkey = LOWORD(VkKeyScanW(ch)); + return vkey == -1 ? 0 : vkey; +} + // Method Description: // - Translates the specified virtual key code and keyboard state to the corresponding character. // Arguments: @@ -532,6 +577,40 @@ catch (...) return UNICODE_INVALID; } +// Method Description: +// - It's possible for a single scan code on a keyboard to +// produce different key codes depending on the keyboard state. +// MapVirtualKeyW(scanCode, MAPVK_VSC_TO_VK) will always chose one of the +// possibilities no matter what though and thus can't be used in SendCharEvent. +// - This method stores the key code from a key event (SendKeyEvent). +// If the key event contains character data, handling of the event will be +// denied, in order to delegate the work to the character event handler. +// - The character event handler (SendCharEvent) will now pick up +// the stored key code to restore the full key event data. +// Arguments: +// - vkey: The virtual key code. +// - scanCode: The scan code. +void Terminal::_StoreKeyEvent(const WORD vkey, const WORD scanCode) +{ + _lastKeyEventCodes.emplace(KeyEventCodes{ vkey, scanCode }); +} + +// Method Description: +// - This method acts as a counterpart to _StoreKeyEvent and extracts a stored +// key code. As a safety measure it'll ensure that the given scan code +// matches the stored scan code from the previous key event. +// - See _StoreKeyEvent for more information. +// Arguments: +// - scanCode: The scan code. +// Return Value: +// - The key code matching the given scan code. Otherwise 0. +WORD Terminal::_TakeVirtualKeyFromLastKeyEvent(const WORD scanCode) noexcept +{ + const auto codes = _lastKeyEventCodes.value_or(KeyEventCodes{}); + _lastKeyEventCodes.reset(); + return codes.ScanCode == scanCode ? codes.VirtualKey : 0; +} + // Method Description: // - Acquire a read lock on the terminal. // Return Value: diff --git a/src/cascadia/TerminalCore/Terminal.hpp b/src/cascadia/TerminalCore/Terminal.hpp index f5ca256e31a..ed77aec2f23 100644 --- a/src/cascadia/TerminalCore/Terminal.hpp +++ b/src/cascadia/TerminalCore/Terminal.hpp @@ -115,7 +115,7 @@ class Microsoft::Terminal::Core::Terminal final : // These methods are defined in Terminal.cpp bool SendKeyEvent(const WORD vkey, const WORD scanCode, const Microsoft::Terminal::Core::ControlKeyStates states) override; bool SendMouseEvent(const COORD viewportPos, const unsigned int uiButton, const ControlKeyStates states, const short wheelDelta) override; - bool SendCharEvent(const wchar_t ch) override; + bool SendCharEvent(const wchar_t ch, const WORD scanCode, const ControlKeyStates states) override; [[nodiscard]] HRESULT UserResize(const COORD viewportSize) noexcept override; void UserScrollViewport(const int viewTop) override; @@ -251,9 +251,22 @@ class Microsoft::Terminal::Core::Terminal final : // underneath them, while others would prefer to anchor it in place. // Either way, we should make this behavior controlled by a setting. + // Since virtual keys are non-zero, you assume that this field is empty/invalid if it is. + struct KeyEventCodes + { + WORD VirtualKey; + WORD ScanCode; + }; + std::optional _lastKeyEventCodes; + static WORD _ScanCodeFromVirtualKey(const WORD vkey) noexcept; + static WORD _VirtualKeyFromScanCode(const WORD scanCode) noexcept; + static WORD _VirtualKeyFromCharacter(const wchar_t ch) noexcept; static wchar_t _CharacterFromKeyEvent(const WORD vkey, const WORD scanCode, const ControlKeyStates states) noexcept; + void _StoreKeyEvent(const WORD vkey, const WORD scanCode); + WORD _TakeVirtualKeyFromLastKeyEvent(const WORD scanCode) noexcept; + int _VisibleStartIndex() const noexcept; int _VisibleEndIndex() const noexcept; diff --git a/src/cascadia/UnitTests_TerminalCore/InputTest.cpp b/src/cascadia/UnitTests_TerminalCore/InputTest.cpp index 6d66301e19e..f630b7da13b 100644 --- a/src/cascadia/UnitTests_TerminalCore/InputTest.cpp +++ b/src/cascadia/UnitTests_TerminalCore/InputTest.cpp @@ -48,12 +48,12 @@ namespace TerminalCoreUnitTests // Verify that Alt+a generates a lowercase a on the input expectedinput = L"\x1b" "a"; - VERIFY_IS_TRUE(term.SendKeyEvent(L'A', 0, ControlKeyStates::LeftAltPressed)); + VERIFY_IS_TRUE(term.SendCharEvent(L'a', 0, ControlKeyStates::LeftAltPressed)); // Verify that Alt+shift+a generates a uppercase a on the input expectedinput = L"\x1b" "A"; - VERIFY_IS_TRUE(term.SendKeyEvent(L'A', 0, ControlKeyStates::LeftAltPressed | ControlKeyStates::ShiftPressed)); + VERIFY_IS_TRUE(term.SendCharEvent(L'A', 0, ControlKeyStates::LeftAltPressed | ControlKeyStates::ShiftPressed)); } void InputTest::AltSpace() @@ -62,5 +62,6 @@ namespace TerminalCoreUnitTests // bring up the system menu for restore, min/maximize, size, move, // close VERIFY_IS_FALSE(term.SendKeyEvent(L' ', 0, ControlKeyStates::LeftAltPressed)); + VERIFY_IS_FALSE(term.SendCharEvent(L' ', 0, ControlKeyStates::LeftAltPressed)); } } diff --git a/src/cascadia/WpfTerminalControl/NativeMethods.cs b/src/cascadia/WpfTerminalControl/NativeMethods.cs index 447ef3f459d..28d2086fc96 100644 --- a/src/cascadia/WpfTerminalControl/NativeMethods.cs +++ b/src/cascadia/WpfTerminalControl/NativeMethods.cs @@ -207,10 +207,10 @@ public enum SetWindowPosFlags : uint public static extern void DestroyTerminal(IntPtr terminal); [DllImport("PublicTerminalCore.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)] - public static extern void TerminalSendKeyEvent(IntPtr terminal, IntPtr wParam); + public static extern void TerminalSendKeyEvent(IntPtr terminal, ushort vkey, ushort scanCode); [DllImport("PublicTerminalCore.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)] - public static extern void TerminalSendCharEvent(IntPtr terminal, char ch); + public static extern void TerminalSendCharEvent(IntPtr terminal, char ch, ushort scanCode); [DllImport("PublicTerminalCore.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)] public static extern void TerminalSetTheme(IntPtr terminal, [MarshalAs(UnmanagedType.Struct)] TerminalTheme theme, string fontFamily, short fontSize, int newDpi); diff --git a/src/cascadia/WpfTerminalControl/TerminalContainer.cs b/src/cascadia/WpfTerminalControl/TerminalContainer.cs index c84d844b079..1713faea5c5 100644 --- a/src/cascadia/WpfTerminalControl/TerminalContainer.cs +++ b/src/cascadia/WpfTerminalControl/TerminalContainer.cs @@ -231,13 +231,15 @@ private IntPtr TerminalContainer_MessageHook(IntPtr hwnd, int msg, IntPtr wParam NativeMethods.SetFocus(this.hwnd); break; case NativeMethods.WindowMessage.WM_KEYDOWN: + // WM_KEYDOWN lParam layout documentation: https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-keydown NativeMethods.TerminalSetCursorVisible(this.terminal, true); NativeMethods.TerminalClearSelection(this.terminal); - NativeMethods.TerminalSendKeyEvent(this.terminal, wParam); + NativeMethods.TerminalSendKeyEvent(this.terminal, (ushort)wParam, Marshal.ReadByte(lParam, 2)); this.blinkTimer?.Start(); break; case NativeMethods.WindowMessage.WM_CHAR: - NativeMethods.TerminalSendCharEvent(this.terminal, (char)wParam); + // WM_CHAR lParam layout documentation: https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-char + NativeMethods.TerminalSendCharEvent(this.terminal, (char)wParam, Marshal.ReadByte(lParam, 2)); break; case NativeMethods.WindowMessage.WM_WINDOWPOSCHANGED: var windowpos = (NativeMethods.WINDOWPOS)Marshal.PtrToStructure(lParam, typeof(NativeMethods.WINDOWPOS)); diff --git a/src/terminal/adapter/ut_adapter/inputTest.cpp b/src/terminal/adapter/ut_adapter/inputTest.cpp index e71e90ae0b0..1df9bcc714e 100644 --- a/src/terminal/adapter/ut_adapter/inputTest.cpp +++ b/src/terminal/adapter/ut_adapter/inputTest.cpp @@ -31,8 +31,8 @@ using namespace Microsoft::Console::VirtualTerminal; // For magic reasons, this has to live outside the class. Something wonderful about TAEF macros makes it // invisible to the linker when inside the class. -static PWSTR s_pwszInputExpected; -static wchar_t s_pwsInputBuffer[256]; +static std::wstring s_expectedInput; + class Microsoft::Console::VirtualTerminal::InputTest { public: @@ -82,10 +82,7 @@ void InputTest::s_TerminalInputTestCallback(_In_ std::deque= '0' && vkey <= 'Z')) + if (!fExpectedKeyHandled && irTest.Event.KeyEvent.uChar.UnicodeChar != 0) { - // we need to have some sort of string to compare to in the - // callback, we'll build it here. - static wchar_t keyArr[2] = { 0 }; - keyArr[0] = vkey; - s_pwszInputExpected = keyArr; + s_expectedInput.clear(); + s_expectedInput.push_back(irTest.Event.KeyEvent.uChar.UnicodeChar); fExpectedKeyHandled = true; } auto inputEvent = IInputEvent::Create(irTest); @@ -335,150 +323,143 @@ void InputTest::TerminalInputModifierKeyTests() TEST_METHOD_PROPERTY(L"Data:uiModifierKeystate", L"{0x0001, 0x0002, 0x0003, 0x0004, 0x0005, 0x0006, 0x0007, 0x0008, 0x000A, 0x000C, 0x000E, 0x0010, 0x0011, 0x0012, 0x0013}") END_TEST_METHOD_PROPERTIES() - unsigned int uiActualKeystate; - VERIFY_SUCCEEDED_RETURN(TestData::TryGetValue(L"uiModifierKeystate", uiActualKeystate)); - unsigned int uiKeystate = uiActualKeystate; + unsigned int uiKeystate; + VERIFY_SUCCEEDED_RETURN(TestData::TryGetValue(L"uiModifierKeystate", uiKeystate)); - const TerminalInput* const pInput = new TerminalInput(s_TerminalInputTestCallback); + TerminalInput* const pInput = new TerminalInput(s_TerminalInputTestCallback); const BYTE slashVkey = LOBYTE(VkKeyScanW(L'/')); + const BYTE nullVkey = LOBYTE(VkKeyScanW(0)); Log::Comment(L"Sending every possible VKEY at the input stream for interception during key DOWN."); for (BYTE vkey = 0; vkey < BYTE_MAX; vkey++) { Log::Comment(NoThrowString().Format(L"Testing Key 0x%x", vkey)); - // zero memory - memset(s_pwsInputBuffer, 0, ARRAYSIZE(s_pwsInputBuffer) * sizeof(wchar_t)); bool fExpectedKeyHandled = true; bool fModifySequence = false; INPUT_RECORD irTest = { 0 }; irTest.EventType = KEY_EVENT; - irTest.Event.KeyEvent.dwControlKeyState = uiActualKeystate; + 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(MapVirtualKeyW(vkey, MAPVK_VK_TO_CHAR)); - // Ctrl-/ is handled in another test, because it's weird. - if (ControlPressed(uiKeystate) && (vkey == VK_DIVIDE || vkey == slashVkey)) + if (ControlPressed(uiKeystate)) { - continue; + // For Ctrl-/ see DifferentModifiersTest. + if (vkey == VK_DIVIDE || vkey == slashVkey) + { + continue; + } + + // For Ctrl-@/Ctrl-Space see TerminalInputNullKeyTests. + if (vkey == nullVkey || vkey == ' ') + { + continue; + } } // Set up expected result switch (vkey) { - case '@': - case '2': - if (ControlPressed(uiKeystate)) - { - continue; - } - // C-@ gets translated to null, which doesn't play nicely with this test. - // So theres the TerminalInputNullKeyTests Test instead. - break; - case 0x20: - // Space generally gets translated to null, which again, doesn't play well. - continue; 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; - // wcscpy_s(s_pwsInputBuffer, L"\x7f"); - break; - case VK_ESCAPE: - wcscpy_s(s_pwsInputBuffer, L"\x1b"); + // s_expectedInput = L"\x7f"; break; case VK_PAUSE: - wcscpy_s(s_pwsInputBuffer, L"\x1a"); + s_expectedInput = L"\x1a"; break; case VK_UP: fModifySequence = true; - wcscpy_s(s_pwsInputBuffer, L"\x1b[1;mA"); + s_expectedInput = L"\x1b[1;mA"; break; case VK_DOWN: fModifySequence = true; - wcscpy_s(s_pwsInputBuffer, L"\x1b[1;mB"); + s_expectedInput = L"\x1b[1;mB"; break; case VK_RIGHT: fModifySequence = true; - wcscpy_s(s_pwsInputBuffer, L"\x1b[1;mC"); + s_expectedInput = L"\x1b[1;mC"; break; case VK_LEFT: fModifySequence = true; - wcscpy_s(s_pwsInputBuffer, L"\x1b[1;mD"); + s_expectedInput = L"\x1b[1;mD"; break; case VK_HOME: fModifySequence = true; - wcscpy_s(s_pwsInputBuffer, L"\x1b[1;mH"); + s_expectedInput = L"\x1b[1;mH"; break; case VK_INSERT: fModifySequence = true; - wcscpy_s(s_pwsInputBuffer, L"\x1b[2;m~"); + s_expectedInput = L"\x1b[2;m~"; break; case VK_DELETE: fModifySequence = true; - wcscpy_s(s_pwsInputBuffer, L"\x1b[3;m~"); + s_expectedInput = L"\x1b[3;m~"; break; case VK_END: fModifySequence = true; - wcscpy_s(s_pwsInputBuffer, L"\x1b[1;mF"); + s_expectedInput = L"\x1b[1;mF"; break; case VK_PRIOR: fModifySequence = true; - wcscpy_s(s_pwsInputBuffer, L"\x1b[5;m~"); + s_expectedInput = L"\x1b[5;m~"; break; case VK_NEXT: fModifySequence = true; - wcscpy_s(s_pwsInputBuffer, L"\x1b[6;m~"); + s_expectedInput = L"\x1b[6;m~"; break; case VK_F1: fModifySequence = true; - wcscpy_s(s_pwsInputBuffer, L"\x1b[1;mP"); + s_expectedInput = L"\x1b[1;mP"; break; case VK_F2: fModifySequence = true; - wcscpy_s(s_pwsInputBuffer, L"\x1b[1;mQ"); + s_expectedInput = L"\x1b[1;mQ"; break; case VK_F3: fModifySequence = true; - wcscpy_s(s_pwsInputBuffer, L"\x1b[1;mR"); + s_expectedInput = L"\x1b[1;mR"; break; case VK_F4: fModifySequence = true; - wcscpy_s(s_pwsInputBuffer, L"\x1b[1;mS"); + s_expectedInput = L"\x1b[1;mS"; break; case VK_F5: fModifySequence = true; - wcscpy_s(s_pwsInputBuffer, L"\x1b[15;m~"); + s_expectedInput = L"\x1b[15;m~"; break; case VK_F6: fModifySequence = true; - wcscpy_s(s_pwsInputBuffer, L"\x1b[17;m~"); + s_expectedInput = L"\x1b[17;m~"; break; case VK_F7: fModifySequence = true; - wcscpy_s(s_pwsInputBuffer, L"\x1b[18;m~"); + s_expectedInput = L"\x1b[18;m~"; break; case VK_F8: fModifySequence = true; - wcscpy_s(s_pwsInputBuffer, L"\x1b[19;m~"); + s_expectedInput = L"\x1b[19;m~"; break; case VK_F9: fModifySequence = true; - wcscpy_s(s_pwsInputBuffer, L"\x1b[20;m~"); + s_expectedInput = L"\x1b[20;m~"; break; case VK_F10: fModifySequence = true; - wcscpy_s(s_pwsInputBuffer, L"\x1b[21;m~"); + s_expectedInput = L"\x1b[21;m~"; break; case VK_F11: fModifySequence = true; - wcscpy_s(s_pwsInputBuffer, L"\x1b[23;m~"); + s_expectedInput = L"\x1b[23;m~"; break; case VK_F12: fModifySequence = true; - wcscpy_s(s_pwsInputBuffer, L"\x1b[24;m~"); + s_expectedInput = L"\x1b[24;m~"; break; case VK_TAB: if (AltPressed(uiKeystate)) @@ -488,62 +469,64 @@ void InputTest::TerminalInputModifierKeyTests() } else if (ShiftPressed(uiKeystate)) { - wcscpy_s(s_pwsInputBuffer, L"\x1b[Z"); - fExpectedKeyHandled = true; + s_expectedInput = L"\x1b[Z"; } else if (ControlPressed(uiKeystate)) { - wcscpy_s(s_pwsInputBuffer, L"\t"); - fExpectedKeyHandled = true; + s_expectedInput = L"\t"; } break; default: + wchar_t ch = irTest.Event.KeyEvent.uChar.UnicodeChar; + + // Alt+Key generates [0x1b, Ctrl+key] into the stream + // Pressing the control key causes all bits but the 5 least + // significant ones to be zeroed out (when using ASCII). + if (AltPressed(uiKeystate) && ControlPressed(uiKeystate) && ch >= 0x40 && ch < 0x7F) + { + s_expectedInput.clear(); + s_expectedInput.push_back(L'\x1b'); + s_expectedInput.push_back(ch & 0b11111); + break; + } + // Alt+Key generates [0x1b, key] into the stream - if (AltPressed(uiKeystate) && (vkey > 0x40 && vkey <= 0x5A)) + if (AltPressed(uiKeystate) && ch != 0) { - wcscpy_s(s_pwsInputBuffer, L"\x1bm"); - wchar_t wchShifted = vkey; - // Alt + Ctrl + key generates [0x1b, control key] in the stream. - if (ControlPressed(uiKeystate)) - { - // Generally the control key is key-0x40 - wchShifted = vkey - 0x40; - } - s_pwsInputBuffer[1] = wchShifted; - fExpectedKeyHandled = true; + s_expectedInput.clear(); + s_expectedInput.push_back(L'\x1b'); + s_expectedInput.push_back(ch); + break; } - else if (ControlPressed(uiKeystate) && (vkey >= '1' && vkey <= '9')) + + 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; } - else + + if (ch != 0) { - fExpectedKeyHandled = false; + s_expectedInput.clear(); + s_expectedInput.push_back(irTest.Event.KeyEvent.uChar.UnicodeChar); + break; } + + fExpectedKeyHandled = false; break; } - if (!fExpectedKeyHandled && ((vkey >= '0' && vkey <= 'Z') || vkey == VK_CANCEL)) - { - fExpectedKeyHandled = true; - } - if (fModifySequence) + if (fModifySequence && s_expectedInput.size() > 1) { - size_t cch = 0; - VERIFY_SUCCEEDED(StringCchLengthW(s_pwsInputBuffer, 8, &cch)); - if (cch > 1) - { - bool fShift = !!(uiKeystate & SHIFT_PRESSED); - bool fAlt = (uiKeystate & LEFT_ALT_PRESSED) || (uiKeystate & RIGHT_ALT_PRESSED); - bool fCtrl = (uiKeystate & LEFT_CTRL_PRESSED) || (uiKeystate & RIGHT_CTRL_PRESSED); - s_pwsInputBuffer[cch - 2] = L'1' + (fShift ? 1 : 0) + (fAlt ? 2 : 0) + (fCtrl ? 4 : 0); - } + bool fShift = !!(uiKeystate & SHIFT_PRESSED); + bool fAlt = (uiKeystate & LEFT_ALT_PRESSED) || (uiKeystate & RIGHT_ALT_PRESSED); + bool fCtrl = (uiKeystate & LEFT_CTRL_PRESSED) || (uiKeystate & RIGHT_CTRL_PRESSED); + s_expectedInput[s_expectedInput.size() - 2] = L'1' + (fShift ? 1 : 0) + (fAlt ? 2 : 0) + (fCtrl ? 4 : 0); } - s_pwszInputExpected = s_pwsInputBuffer; - Log::Comment(NoThrowString().Format(L"Expected, Buffer = \"%s\", \"%s\"", s_pwszInputExpected, s_pwsInputBuffer)); + + Log::Comment(NoThrowString().Format(L"Expected = \"%s\"", s_expectedInput.c_str())); auto inputEvent = IInputEvent::Create(irTest); // Send key into object (will trigger callback and verification) @@ -557,7 +540,7 @@ void InputTest::TerminalInputNullKeyTests() unsigned int uiKeystate = LEFT_CTRL_PRESSED; - const TerminalInput* const pInput = new TerminalInput(s_TerminalInputTestNullCallback); + TerminalInput* const pInput = new TerminalInput(s_TerminalInputTestNullCallback); Log::Comment(L"Sending every possible VKEY at the input stream for interception during key DOWN."); @@ -593,16 +576,9 @@ void InputTest::TerminalInputNullKeyTests() irTest.Event.KeyEvent.dwControlKeyState = uiKeystate; inputEvent = IInputEvent::Create(irTest); VERIFY_ARE_EQUAL(true, pInput->HandleKey(inputEvent.get()), L"Verify key was handled if it should have been."); - - uiKeystate = LEFT_CTRL_PRESSED | RIGHT_ALT_PRESSED; - // This is AltGr, this ISN'T handled. - Log::Comment(NoThrowString().Format(L"Testing key, state =0x%x, 0x%x", vkey, uiKeystate)); - irTest.Event.KeyEvent.dwControlKeyState = uiKeystate; - inputEvent = IInputEvent::Create(irTest); - VERIFY_ARE_EQUAL(false, pInput->HandleKey(inputEvent.get()), L"Verify key was handled if it should have been."); } -void TestKey(const TerminalInput* const pInput, const unsigned int uiKeystate, const BYTE vkey, const wchar_t wch) +void TestKey(TerminalInput* const pInput, 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)); @@ -618,69 +594,64 @@ void TestKey(const TerminalInput* const pInput, const unsigned int uiKeystate, c auto inputEvent = IInputEvent::Create(irTest); VERIFY_ARE_EQUAL(true, pInput->HandleKey(inputEvent.get()), L"Verify key was handled if it should have been."); } -void TestKey(const TerminalInput* const pInput, const unsigned int uiKeystate, const BYTE vkey) -{ - // Callers of this version don't expect the wchar to matter. - TestKey(pInput, uiKeystate, vkey, 0); -} void InputTest::DifferentModifiersTest() { Log::Comment(L"Starting test..."); - const TerminalInput* const pInput = new TerminalInput(s_TerminalInputTestCallback); + TerminalInput* const pInput = new TerminalInput(s_TerminalInputTestCallback); Log::Comment(L"Sending a bunch of keystrokes that are a little weird."); unsigned int uiKeystate = 0; BYTE vkey = VK_BACK; - s_pwszInputExpected = L"\x7f"; + s_expectedInput = L"\x7f"; TestKey(pInput, uiKeystate, vkey); uiKeystate = LEFT_CTRL_PRESSED; vkey = VK_BACK; - s_pwszInputExpected = L"\x8"; + s_expectedInput = L"\x8"; TestKey(pInput, uiKeystate, vkey, L'\x8'); uiKeystate = RIGHT_CTRL_PRESSED; TestKey(pInput, uiKeystate, vkey, L'\x8'); uiKeystate = LEFT_ALT_PRESSED; vkey = VK_BACK; - s_pwszInputExpected = L"\x1b\x7f"; + s_expectedInput = L"\x1b\x7f"; TestKey(pInput, uiKeystate, vkey, L'\x8'); uiKeystate = RIGHT_ALT_PRESSED; TestKey(pInput, uiKeystate, vkey, L'\x8'); uiKeystate = LEFT_CTRL_PRESSED; vkey = VK_DELETE; - s_pwszInputExpected = L"\x1b[3;5~"; + s_expectedInput = L"\x1b[3;5~"; TestKey(pInput, uiKeystate, vkey); uiKeystate = RIGHT_CTRL_PRESSED; TestKey(pInput, uiKeystate, vkey); uiKeystate = LEFT_ALT_PRESSED; vkey = VK_DELETE; - s_pwszInputExpected = L"\x1b[3;3~"; + s_expectedInput = L"\x1b[3;3~"; TestKey(pInput, uiKeystate, vkey); uiKeystate = RIGHT_ALT_PRESSED; TestKey(pInput, uiKeystate, vkey); uiKeystate = LEFT_CTRL_PRESSED; vkey = VK_TAB; - s_pwszInputExpected = L"\t"; + s_expectedInput = L"\t"; TestKey(pInput, uiKeystate, vkey); uiKeystate = RIGHT_CTRL_PRESSED; TestKey(pInput, uiKeystate, vkey); uiKeystate = SHIFT_PRESSED; vkey = VK_TAB; - s_pwszInputExpected = L"\x1b[Z"; + s_expectedInput = L"\x1b[Z"; TestKey(pInput, uiKeystate, vkey); // C-/ -> C-_ -> 0x1f uiKeystate = LEFT_CTRL_PRESSED; vkey = LOBYTE(VkKeyScan(L'/')); - s_pwszInputExpected = L"\x1f"; + s_expectedInput = L"\x1f"; TestKey(pInput, uiKeystate, vkey, L'/'); uiKeystate = RIGHT_CTRL_PRESSED; TestKey(pInput, uiKeystate, vkey, L'/'); @@ -688,7 +659,7 @@ void InputTest::DifferentModifiersTest() // M-/ -> ESC / uiKeystate = LEFT_ALT_PRESSED; vkey = LOBYTE(VkKeyScan(L'/')); - s_pwszInputExpected = L"\x1b/"; + s_expectedInput = L"\x1b/"; TestKey(pInput, uiKeystate, vkey, L'/'); uiKeystate = RIGHT_ALT_PRESSED; TestKey(pInput, uiKeystate, vkey, L'/'); @@ -698,7 +669,7 @@ void InputTest::DifferentModifiersTest() Log::Comment(NoThrowString().Format(L"Checking C-?")); // Use SHIFT_PRESSED to force us into differentiating between '/' and '?' vkey = LOBYTE(VkKeyScan(L'?')); - s_pwszInputExpected = L"\x7f"; + s_expectedInput = L"\x7f"; TestKey(pInput, SHIFT_PRESSED | LEFT_CTRL_PRESSED, vkey, L'?'); TestKey(pInput, SHIFT_PRESSED | RIGHT_CTRL_PRESSED, vkey, L'?'); @@ -706,7 +677,7 @@ void InputTest::DifferentModifiersTest() Log::Comment(NoThrowString().Format(L"Checking C-M-/")); uiKeystate = LEFT_CTRL_PRESSED | LEFT_ALT_PRESSED; vkey = LOBYTE(VkKeyScan(L'/')); - s_pwszInputExpected = L"\x1b\x1f"; + s_expectedInput = L"\x1b\x1f"; TestKey(pInput, LEFT_CTRL_PRESSED | LEFT_ALT_PRESSED, vkey, L'/'); TestKey(pInput, RIGHT_CTRL_PRESSED | LEFT_ALT_PRESSED, vkey, L'/'); // LEFT_CTRL_PRESSED | RIGHT_ALT_PRESSED is skipped because that's AltGr @@ -716,7 +687,7 @@ void InputTest::DifferentModifiersTest() Log::Comment(NoThrowString().Format(L"Checking C-M-?")); uiKeystate = LEFT_CTRL_PRESSED | LEFT_ALT_PRESSED; vkey = LOBYTE(VkKeyScan(L'?')); - s_pwszInputExpected = L"\x1b\x7f"; + s_expectedInput = L"\x1b\x7f"; TestKey(pInput, SHIFT_PRESSED | LEFT_CTRL_PRESSED | LEFT_ALT_PRESSED, vkey, L'?'); TestKey(pInput, SHIFT_PRESSED | RIGHT_CTRL_PRESSED | LEFT_ALT_PRESSED, vkey, L'?'); // LEFT_CTRL_PRESSED | RIGHT_ALT_PRESSED is skipped because that's AltGr @@ -727,13 +698,13 @@ void InputTest::CtrlNumTest() { Log::Comment(L"Starting test..."); - const TerminalInput* const pInput = new TerminalInput(s_TerminalInputTestCallback); + TerminalInput* const pInput = new TerminalInput(s_TerminalInputTestCallback); Log::Comment(L"Sending the various Ctrl+Num keys."); unsigned int uiKeystate = LEFT_CTRL_PRESSED; BYTE vkey = static_cast('1'); - s_pwszInputExpected = L"1"; + s_expectedInput = L"1"; TestKey(pInput, uiKeystate, vkey); Log::Comment(NoThrowString().Format( @@ -741,30 +712,30 @@ void InputTest::CtrlNumTest() L"nicely with this test. Ctrl+2 is covered by other tests in this class.")); vkey = static_cast('3'); - s_pwszInputExpected = L"\x1b"; + s_expectedInput = L"\x1b"; TestKey(pInput, uiKeystate, vkey); vkey = static_cast('4'); - s_pwszInputExpected = L"\x1c"; + s_expectedInput = L"\x1c"; TestKey(pInput, uiKeystate, vkey); vkey = static_cast('5'); - s_pwszInputExpected = L"\x1d"; + s_expectedInput = L"\x1d"; TestKey(pInput, uiKeystate, vkey); vkey = static_cast('6'); - s_pwszInputExpected = L"\x1e"; + s_expectedInput = L"\x1e"; TestKey(pInput, uiKeystate, vkey); vkey = static_cast('7'); - s_pwszInputExpected = L"\x1f"; + s_expectedInput = L"\x1f"; TestKey(pInput, uiKeystate, vkey); vkey = static_cast('8'); - s_pwszInputExpected = L"\x7f"; + s_expectedInput = L"\x7f"; TestKey(pInput, uiKeystate, vkey); vkey = static_cast('9'); - s_pwszInputExpected = L"9"; + s_expectedInput = L"9"; TestKey(pInput, uiKeystate, vkey); } diff --git a/src/terminal/input/terminalInput.cpp b/src/terminal/input/terminalInput.cpp index d2af88476e6..948d2f6ad50 100644 --- a/src/terminal/input/terminalInput.cpp +++ b/src/terminal/input/terminalInput.cpp @@ -440,120 +440,150 @@ static bool _translateDefaultMapping(const KeyEvent& keyEvent, return match.has_value(); } -bool TerminalInput::HandleKey(const IInputEvent* const pInEvent) const +// Routine Description: +// - Sends the given input event to the shell. +// - The caller should attempt to fill the char data in pInEvent if possible. +// The char data should already be translated in accordance to Ctrl/Alt/Shift +// modifiers, like the characters given by the WM_CHAR event. +// - The caller doesn't need to fill in any char data for: +// - Tab key +// - Alt+key combinations +// - This method will alias Ctrl+Space as a synonym for Ctrl+@ - the null byte. +// Arguments: +// - keyEvent - Key event to translate +// Return Value: +// - True if the event was handled. +bool TerminalInput::HandleKey(const IInputEvent* const pInEvent) { - // By default, we fail to handle the key - bool keyHandled = false; + if (!pInEvent) + { + return false; + } // On key presses, prepare to translate to VT compatible sequences - if (pInEvent->EventType() == InputEventType::KeyEvent) + if (pInEvent->EventType() != InputEventType::KeyEvent) { - const auto senderFunc = [this](const std::wstring_view seq) noexcept { _SendInputSequence(seq); }; + return false; + } - auto keyEvent = *static_cast(pInEvent); + auto keyEvent = *static_cast(pInEvent); - // Only need to handle key down. See raw key handler (see RawReadWaitRoutine in stream.cpp) - if (keyEvent.IsKeyDown()) + // Only need to handle key down. See raw key handler (see RawReadWaitRoutine in stream.cpp) + if (!keyEvent.IsKeyDown()) + { + return false; + } + + // 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 GetCharData(). + // --> Strip out the AltGr flags, in order for us to not step into the Alt/Ctrl conditions below. + if (keyEvent.IsAltGrPressed()) + { + keyEvent.DeactivateModifierKey(ModifierKeyState::LeftCtrl); + keyEvent.DeactivateModifierKey(ModifierKeyState::RightAlt); + } + + // 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 (keyEvent.IsAltPressed() && keyEvent.IsCtrlPressed()) + { + auto ch = keyEvent.GetCharData(); + if (ch == UNICODE_NULL) { - // For AltGr enabled keyboards, the Windows system will - // emit Left Ctrl + Right Alt as the modifier keys and - // will have pretranslated the UnicodeChar to the proper - // alternative value. - // Through testing with Ubuntu, PuTTY, and Emacs for - // Windows, it was discovered that any instance of Left - // Ctrl + Right Alt will strip out those two modifiers and - // send the unicode value straight through to the system. - // Holding additional modifiers in addition to Left Ctrl + - // Right Alt will then light those modifiers up again for - // the unicode value. - // Therefore to handle AltGr properly, our first step - // needs to be to check if both Left Ctrl + Right Alt are - // pressed... - // ... and if they are both pressed, strip them out of the control key state. - if (keyEvent.IsAltGrPressed()) - { - keyEvent.DeactivateModifierKey(ModifierKeyState::LeftCtrl); - keyEvent.DeactivateModifierKey(ModifierKeyState::RightAlt); - } + // For Alt+Ctrl+Key messages GetCharData() returns 0. + // -> Get the char from the virtual key. + ch = LOWORD(MapVirtualKeyW(keyEvent.GetVirtualKeyCode(), MAPVK_VK_TO_CHAR)); + } + if (ch == UNICODE_SPACE) + { + // Ctrl+@ and Ctrl+Space are supposed to send null bytes. + // -> Change Ctrl+Space to Ctrl+@ for compatibility reasons. + ch = 0x40; + } + if (ch >= 0x40 && ch < 0x7F) + { + // Pressing the control key causes all bits but the 5 least + // significant ones to be zeroed out (when using ASCII). + ch &= 0b11111; + _SendEscapedInputSequence(ch); + return true; + } + } - if (keyEvent.IsAltPressed() && - keyEvent.IsCtrlPressed() && - (keyEvent.GetCharData() == 0 || keyEvent.GetCharData() == 0x20) && - ((keyEvent.GetVirtualKeyCode() > 0x40 && keyEvent.GetVirtualKeyCode() <= 0x5A) || - keyEvent.GetVirtualKeyCode() == VK_SPACE)) - { - // For Alt+Ctrl+Key messages, the UnicodeChar is NOT the Ctrl+key char, it's null. - // So we need to get the char from the vKey. - // EXCEPT for Alt+Ctrl+Space. Then the UnicodeChar is space, not NUL. - auto wchPressedChar = gsl::narrow_cast(MapVirtualKeyW(keyEvent.GetVirtualKeyCode(), MAPVK_VK_TO_CHAR)); - // This is a trick - C-Spc is supposed to send NUL. So quick change space -> @ (0x40) - wchPressedChar = (wchPressedChar == UNICODE_SPACE) ? 0x40 : wchPressedChar; - if (wchPressedChar >= 0x40 && wchPressedChar < 0x7F) - { - //shift the char to the ctrl range - wchPressedChar -= 0x40; - _SendEscapedInputSequence(wchPressedChar); - keyHandled = true; - } - } + const auto senderFunc = [this](const std::wstring_view seq) noexcept + { + _SendInputSequence(seq); + }; - // If a modifier key was pressed, then we need to try and send the modified sequence. - if (!keyHandled && keyEvent.IsModifierPressed()) - { - // Translate the key using the modifier table - keyHandled = _searchWithModifier(keyEvent, senderFunc); - } - // ALT is a sequence of ESC + KEY. - if (!keyHandled && keyEvent.GetCharData() != 0 && keyEvent.IsAltPressed()) - { - _SendEscapedInputSequence(keyEvent.GetCharData()); - keyHandled = true; - } - if (!keyHandled && keyEvent.IsCtrlPressed()) - { - if ((keyEvent.GetCharData() == UNICODE_SPACE) || // Ctrl+Space - // when Ctrl+@ comes through, the unicodechar - // will be '\x0' (UNICODE_NULL), and the vkey will be - // VkKeyScanW(0), the vkey for null - (keyEvent.GetCharData() == UNICODE_NULL && keyEvent.GetVirtualKeyCode() == LOBYTE(VkKeyScanW(0)))) - { - _SendNullInputSequence(keyEvent.GetActiveModifierKeys()); - keyHandled = true; - } - } + // If a modifier key was pressed, then we need to try and send the modified sequence. + if (keyEvent.IsModifierPressed() && _searchWithModifier(keyEvent, senderFunc)) + { + return true; + } - if (!keyHandled) - { - // For perf optimization, filter out any typically printable Virtual Keys (e.g. A-Z) - // This is in lieu of an O(1) sparse table or other such less-maintainable methods. - // VK_CANCEL is an exception and we want to send the associated uChar as is. - if ((keyEvent.GetVirtualKeyCode() < '0' || keyEvent.GetVirtualKeyCode() > 'Z') && - keyEvent.GetVirtualKeyCode() != VK_CANCEL) - { - keyHandled = _translateDefaultMapping(keyEvent, _getKeyMapping(keyEvent, _cursorApplicationMode, _keypadApplicationMode), senderFunc); - } - else - { - WCHAR rgwchSequence[2]; - rgwchSequence[0] = keyEvent.GetCharData(); - rgwchSequence[1] = UNICODE_NULL; - _SendInputSequence(rgwchSequence); - keyHandled = true; - } - } + // This section is similar to the Alt modifier section above, + // but handles cases without Ctrl modifiers. + if (keyEvent.IsAltPressed() && keyEvent.GetCharData() != 0) + { + _SendEscapedInputSequence(keyEvent.GetCharData()); + return true; + } + + // 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 (keyEvent.IsCtrlPressed()) + { + const auto ch = keyEvent.GetCharData(); + const auto vkey = keyEvent.GetVirtualKeyCode(); + + // 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(VkKeyScanW(0)))) + { + _SendNullInputSequence(keyEvent.GetActiveModifierKeys()); + return true; } } - return keyHandled; -} + // Check any other key mappings (like those for the F1-F12 keys). + const auto mapping = _getKeyMapping(keyEvent, _cursorApplicationMode, _keypadApplicationMode); + if (_translateDefaultMapping(keyEvent, mapping, senderFunc)) + { + return true; + } -bool TerminalInput::HandleChar(const wchar_t ch) -{ - if (ch == UNICODE_BACKSPACE || ch == UNICODE_DEL) + // If all else fails we can finally try to send the character itself if there is any. + if (keyEvent.GetCharData() != 0) { - return false; + _SendChar(keyEvent.GetCharData()); + return true; } - else if (Utf16Parser::IsLeadingSurrogate(ch)) + + return false; +} + +// Routine Description: +// - Sends the given character to the shell. +// - Surrogate pairs are being aggregated by this function before being sent. +// Arguments: +// - ch: The UTF-16 character to send. +void TerminalInput::_SendChar(const wchar_t ch) +{ + if (Utf16Parser::IsLeadingSurrogate(ch)) { if (_leadingSurrogate.has_value()) { @@ -567,20 +597,14 @@ bool TerminalInput::HandleChar(const wchar_t ch) } else if (_leadingSurrogate.has_value()) { - std::wstring wstr; - wstr.reserve(2); - wstr.push_back(_leadingSurrogate.value()); - wstr.push_back(ch); + std::array wstr{ { _leadingSurrogate.value(), ch } }; _leadingSurrogate.reset(); - - _SendInputSequence(wstr); + _SendInputSequence({ wstr.data(), wstr.size() }); } else { _SendInputSequence({ &ch, 1 }); } - - return true; } // Routine Description: diff --git a/src/terminal/input/terminalInput.hpp b/src/terminal/input/terminalInput.hpp index 064cfd91684..ebaee780563 100644 --- a/src/terminal/input/terminalInput.hpp +++ b/src/terminal/input/terminalInput.hpp @@ -33,8 +33,7 @@ namespace Microsoft::Console::VirtualTerminal ~TerminalInput() = default; - bool HandleKey(const IInputEvent* const pInEvent) const; - bool HandleChar(const wchar_t ch); + bool HandleKey(const IInputEvent* const pInEvent); void ChangeKeypadMode(const bool applicationMode) noexcept; void ChangeCursorKeysMode(const bool applicationMode) noexcept; @@ -71,6 +70,7 @@ namespace Microsoft::Console::VirtualTerminal bool _keypadApplicationMode = false; bool _cursorApplicationMode = false; + void _SendChar(const wchar_t ch); void _SendNullInputSequence(const DWORD dwControlKeyState) const; void _SendInputSequence(const std::wstring_view sequence) const noexcept; void _SendEscapedInputSequence(const wchar_t wch) const;