diff --git a/.github/actions/spelling/expect/expect.txt b/.github/actions/spelling/expect/expect.txt index 6c226753b98..f6ebb617f1a 100644 --- a/.github/actions/spelling/expect/expect.txt +++ b/.github/actions/spelling/expect/expect.txt @@ -29,6 +29,7 @@ allocing alpc ALTERNATENAME ALTF +ALTGR ALTNUMPAD ALWAYSTIP ansicpg @@ -79,6 +80,7 @@ autoscrolling Autowrap AVerify awch +AZERTY azurecr AZZ backgrounded @@ -867,6 +869,8 @@ kinda KIYEOK KKP KLF +klid +KLLF KLMNO KOK KPRIORITY @@ -1133,6 +1137,7 @@ NOSELECTION NOSENDCHANGING NOSIZE NOSNAPSHOT +NOTELLSHELL NOTHOUSANDS NOTICKS NOTIMEOUTIFNOTHUNG @@ -1984,8 +1989,8 @@ WRITECONSOLEINPUT WRITECONSOLEOUTPUT WRITECONSOLEOUTPUTSTRING wrkstr -wrl WRL +wrl wrp WRunoff WSLENV diff --git a/src/terminal/adapter/ut_adapter/Adapter.UnitTests.vcxproj b/src/terminal/adapter/ut_adapter/Adapter.UnitTests.vcxproj index 69bb1147c25..5e86b478de1 100644 --- a/src/terminal/adapter/ut_adapter/Adapter.UnitTests.vcxproj +++ b/src/terminal/adapter/ut_adapter/Adapter.UnitTests.vcxproj @@ -15,12 +15,14 @@ + Create + diff --git a/src/terminal/adapter/ut_adapter/Adapter.UnitTests.vcxproj.filters b/src/terminal/adapter/ut_adapter/Adapter.UnitTests.vcxproj.filters index 00713d03220..a0f54f93a0b 100644 --- a/src/terminal/adapter/ut_adapter/Adapter.UnitTests.vcxproj.filters +++ b/src/terminal/adapter/ut_adapter/Adapter.UnitTests.vcxproj.filters @@ -30,11 +30,17 @@ Source Files + + Source Files + Header Files + + Header Files + diff --git a/src/terminal/adapter/ut_adapter/TestHook.cpp b/src/terminal/adapter/ut_adapter/TestHook.cpp new file mode 100644 index 00000000000..50f6fe19569 --- /dev/null +++ b/src/terminal/adapter/ut_adapter/TestHook.cpp @@ -0,0 +1,132 @@ +#include "precomp.h" +#include "TestHook.h" + +using namespace TestHook; + +thread_local HKL g_keyboardLayout; + +extern "C" HKL TestHook_TerminalInput_KeyboardLayout() +{ + return g_keyboardLayout; +} + +static bool isPreloadedLayout(const wchar_t* klid) noexcept +{ + wil::unique_hkey preloadKey; + if (RegOpenKeyExW(HKEY_CURRENT_USER, L"Keyboard Layout\\Preload", 0, KEY_READ, preloadKey.addressof()) != ERROR_SUCCESS) + { + return false; + } + + wil::unique_hkey substitutesKey; + RegOpenKeyExW(HKEY_CURRENT_USER, L"Keyboard Layout\\Substitutes", 0, KEY_READ, substitutesKey.addressof()); + + wchar_t idx[16]; + wchar_t layoutId[KL_NAMELENGTH]; + + for (DWORD i = 0;; i++) + { + DWORD idxLen = ARRAYSIZE(idx); + DWORD layoutIdSize = sizeof(layoutId); + if (RegEnumValueW(preloadKey.get(), i, idx, &idxLen, nullptr, nullptr, reinterpret_cast(layoutId), &layoutIdSize) != ERROR_SUCCESS) + { + break; + } + + // Preload contains base language IDs (e.g. "0000040c"). + // The actual layout ID (e.g. "0001040c") may only appear in the Substitutes key. + if (substitutesKey) + { + wchar_t substitute[KL_NAMELENGTH]; + DWORD substituteSize = sizeof(substitute); + if (RegGetValueW(substitutesKey.get(), nullptr, layoutId, RRF_RT_REG_SZ, nullptr, substitute, &substituteSize) == ERROR_SUCCESS) + { + memcpy(layoutId, substitute, sizeof(layoutId)); + } + } + + if (wcscmp(layoutId, klid) == 0) + { + return true; + } + } + + return false; +} + +void LayoutGuard::_destroy() const noexcept +{ + if (g_keyboardLayout == _layout) + { + g_keyboardLayout = nullptr; + } + if (_needsUnload) + { + UnloadKeyboardLayout(_layout); + } +} + +LayoutGuard::LayoutGuard(HKL layout, bool needsUnload) noexcept : + _layout{ layout }, + _needsUnload{ needsUnload } +{ +} + +LayoutGuard::~LayoutGuard() +{ + _destroy(); +} + +LayoutGuard::LayoutGuard(LayoutGuard&& other) noexcept : + _layout{ std::exchange(other._layout, nullptr) }, + _needsUnload{ std::exchange(other._needsUnload, false) } +{ +} + +LayoutGuard& LayoutGuard::operator=(LayoutGuard&& other) noexcept +{ + if (this != &other) + { + _destroy(); + _layout = std::exchange(other._layout, nullptr); + _needsUnload = std::exchange(other._needsUnload, false); + } + return *this; +} + +LayoutGuard::operator bool() const noexcept +{ + return _layout != nullptr; +} + +LayoutGuard::operator HKL() const noexcept +{ + return _layout; +} + +LayoutGuard TestHook::SetTerminalInputKeyboardLayout(const wchar_t* klid) +{ + THROW_HR_IF_MSG(E_UNEXPECTED, g_keyboardLayout != nullptr, "Nested layout test overrides are not supported"); + + // Check if the layout is installed. LoadKeyboardLayoutW silently returns the + // current active layout if the requested one is missing. + const auto keyPath = fmt::format(FMT_COMPILE(L"SYSTEM\\CurrentControlSet\\Control\\Keyboard Layouts\\{}"), klid); + wil::unique_hkey key; + if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, keyPath.c_str(), 0, KEY_READ, key.addressof()) != ERROR_SUCCESS) + { + return {}; + } + + const auto layout = LoadKeyboardLayoutW(klid, KLF_NOTELLSHELL); + THROW_LAST_ERROR_IF_NULL(layout); + + g_keyboardLayout = layout; + + // Unload the layout if it's not one of the user's layouts. + // GetKeyboardLayoutList is unreliable for this purpose, as the keyboard layout API mutates global OS state. + // If a process crashes or exits early without calling UnloadKeyboardLayout all future processes will get it + // returned in their GetKeyboardLayoutList calls. Shell could fix it but alas. So we peek into the registry. + const auto needsUnload = !isPreloadedLayout(klid); + + return { layout, needsUnload }; +} diff --git a/src/terminal/adapter/ut_adapter/TestHook.h b/src/terminal/adapter/ut_adapter/TestHook.h new file mode 100644 index 00000000000..f7e14e25a30 --- /dev/null +++ b/src/terminal/adapter/ut_adapter/TestHook.h @@ -0,0 +1,27 @@ +#pragma once + +namespace TestHook +{ + struct LayoutGuard + { + LayoutGuard() = default; + LayoutGuard(HKL layout, bool needsUnload) noexcept; + ~LayoutGuard(); + + LayoutGuard(const LayoutGuard&) = delete; + LayoutGuard& operator=(const LayoutGuard&) = delete; + LayoutGuard(LayoutGuard&& other) noexcept; + LayoutGuard& operator=(LayoutGuard&& other) noexcept; + + explicit operator bool() const noexcept; + operator HKL() const noexcept; + + private: + void _destroy() const noexcept; + + HKL _layout = nullptr; + bool _needsUnload = false; + }; + + LayoutGuard SetTerminalInputKeyboardLayout(const wchar_t* klid); +} diff --git a/src/terminal/adapter/ut_adapter/inputTest.cpp b/src/terminal/adapter/ut_adapter/inputTest.cpp index d672004cd12..cb221906bf7 100644 --- a/src/terminal/adapter/ut_adapter/inputTest.cpp +++ b/src/terminal/adapter/ut_adapter/inputTest.cpp @@ -3,6 +3,7 @@ #include "precomp.h" +#include "TestHook.h" #include "../../../interactivity/inc/VtApiRedirection.hpp" #include "../../input/terminalInput.hpp" #include "../types/inc/IInputEvent.hpp" @@ -308,6 +309,30 @@ void InputTest::TerminalInputModifierKeyTests() const auto slashVkey = LOBYTE(OneCoreSafeVkKeyScanW(L'/')); const auto nullVkey = LOBYTE(OneCoreSafeVkKeyScanW(0)); + uint8_t keyboardState[256] = {}; + wchar_t unicodeBuf[4] = {}; + const uint8_t rightAlt = WI_IsFlagSet(uiKeystate, RIGHT_ALT_PRESSED) ? 0x80 : 0; + const uint8_t leftAlt = WI_IsFlagSet(uiKeystate, LEFT_ALT_PRESSED) ? 0x80 : 0; + const uint8_t rightCtrl = WI_IsFlagSet(uiKeystate, RIGHT_CTRL_PRESSED) ? 0x80 : 0; + const uint8_t leftCtrl = WI_IsFlagSet(uiKeystate, LEFT_CTRL_PRESSED) ? 0x80 : 0; + const uint8_t shift = WI_IsFlagSet(uiKeystate, SHIFT_PRESSED) ? 0x80 : 0; + const uint8_t capsLock = WI_IsFlagSet(uiKeystate, CAPSLOCK_ON) ? 0x01 : 0; + keyboardState[VK_SHIFT] = shift; + keyboardState[VK_CONTROL] = leftCtrl | rightCtrl; + keyboardState[VK_MENU] = leftAlt | rightAlt; + keyboardState[VK_CAPITAL] = capsLock; + keyboardState[VK_LSHIFT] = shift; + keyboardState[VK_LCONTROL] = leftCtrl; + keyboardState[VK_RCONTROL] = rightCtrl; + keyboardState[VK_LMENU] = leftAlt; + keyboardState[VK_RMENU] = rightAlt; + + const auto anyCtrlPressed = WI_IsAnyFlagSet(uiKeystate, CTRL_PRESSED); + const auto bothCtrlPressed = WI_AreAllFlagsSet(uiKeystate, CTRL_PRESSED); + const auto anyAltPressed = WI_IsAnyFlagSet(uiKeystate, ALT_PRESSED); + const auto bothAltPressed = WI_AreAllFlagsSet(uiKeystate, ALT_PRESSED); + const auto shiftPressed = WI_IsFlagSet(uiKeystate, SHIFT_PRESSED); + Log::Comment(L"Sending every possible VKEY at the input stream for interception during key DOWN."); for (BYTE vkey = 0; vkey < BYTE_MAX; vkey++) { @@ -315,9 +340,17 @@ void InputTest::TerminalInputModifierKeyTests() auto fExpectedKeyHandled = true; auto fModifySequence = false; - wchar_t ch = LOWORD(OneCoreSafeMapVirtualKeyW(vkey, MAPVK_VK_TO_CHAR)); - if (ControlPressed(uiKeystate)) + til::at(keyboardState, vkey) = 0x80; // Momentarily pretend as if the key is set + const auto unicodeLen = ToUnicodeEx(vkey, 0, &keyboardState[0], &unicodeBuf[0], ARRAYSIZE(unicodeBuf), 0b101, nullptr); + til::at(keyboardState, vkey) = 0; + + wchar_t ch = unicodeLen == 1 ? unicodeBuf[0] : 0; + const auto altGrPressed = anyAltPressed && anyCtrlPressed && (ch > 0x20 && ch != 0x7f); + const auto ctrlPressed = bothCtrlPressed || (anyCtrlPressed && !altGrPressed); + const auto altPressed = bothAltPressed || (anyAltPressed && !altGrPressed); + + if (ctrlPressed) { // For Ctrl-/ see DifferentModifiersTest. if (vkey == VK_DIVIDE || vkey == slashVkey) @@ -472,28 +505,28 @@ void InputTest::TerminalInputModifierKeyTests() expected = TerminalInput::MakeOutput({ &ch, 1 }); break; case VK_RETURN: - if (AltPressed(uiKeystate)) + if (altPressed) { - const auto str = ControlPressed(uiKeystate) ? L"\x1b\n" : L"\x1b\r"; + const auto str = ctrlPressed ? L"\x1b\n" : L"\x1b\r"; expected = TerminalInput::MakeOutput(str); } else { - const auto str = ControlPressed(uiKeystate) ? L"\n" : L"\r"; + const auto str = ctrlPressed ? L"\n" : L"\r"; expected = TerminalInput::MakeOutput(str); } break; case VK_TAB: - if (AltPressed(uiKeystate)) + if (altPressed) { // Alt+Tab isn't possible - that's reserved by the system. continue; } - else if (ShiftPressed(uiKeystate)) + else if (shiftPressed) { expected = TerminalInput::MakeOutput(L"\x1b[Z"); } - else if (ControlPressed(uiKeystate)) + else { expected = TerminalInput::MakeOutput(L"\t"); } @@ -506,13 +539,19 @@ void InputTest::TerminalInputModifierKeyTests() 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)) + if (ctrlPressed) { continue; } [[fallthrough]]; default: - if (ControlPressed(uiKeystate) && (vkey >= '1' && vkey <= '9')) + // Map VK_ESCAPE, etc., to their corresponding character value, if needed. + if (ch == 0) + { + ch = LOWORD(OneCoreSafeMapVirtualKeyW(vkey, MAPVK_VK_TO_CHAR)); + } + + if (ctrlPressed && (vkey >= '1' && vkey <= '9')) { // The C-# keys get translated into very specific control // characters that don't play nicely with this test. These keys @@ -531,7 +570,7 @@ void InputTest::TerminalInputModifierKeyTests() // 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 <= 0x5A) + if (altPressed && ctrlPressed && ch > L'@' && ch <= L'~') { const wchar_t buffer[2]{ L'\x1b', gsl::narrow_cast(ch & 0b11111) }; expected = TerminalInput::MakeOutput({ &buffer[0], 2 }); @@ -540,17 +579,25 @@ void InputTest::TerminalInputModifierKeyTests() } // Alt+Key generates [0x1b, key] into the stream - if (AltPressed(uiKeystate) && ch != 0) + if (altPressed && ch != 0) { const wchar_t buffer[2]{ L'\x1b', ch }; expected = TerminalInput::MakeOutput({ &buffer[0], 2 }); - if (ControlPressed(uiKeystate)) + if (ctrlPressed) { ch = 0; } break; } + // Ctrl+Key masks the key value. + if (ctrlPressed && ch > L'@' && ch <= L'~') + { + const auto b = gsl::narrow_cast(ch & 0b11111); + expected = TerminalInput::MakeOutput({ &b, 1 }); + break; + } + if (ch != 0) { expected = TerminalInput::MakeOutput({ &ch, 1 }); @@ -563,11 +610,14 @@ void InputTest::TerminalInputModifierKeyTests() if (fModifySequence) { - auto fShift = !!(uiKeystate & SHIFT_PRESSED); - auto fAlt = (uiKeystate & LEFT_ALT_PRESSED) || (uiKeystate & RIGHT_ALT_PRESSED); - auto fCtrl = (uiKeystate & LEFT_CTRL_PRESSED) || (uiKeystate & RIGHT_CTRL_PRESSED); + const auto mod = shiftPressed + (2 * altPressed) + (4 * ctrlPressed); + if (mod == 0) + { + continue; + } + auto& str = expected.value(); - str[str.size() - 2] = L'1' + (fShift ? 1 : 0) + (fAlt ? 2 : 0) + (fCtrl ? 4 : 0); + str[str.size() - 2] = static_cast(L'1' + mod); } TestKey(expected, input, uiKeystate, vkey, ch); @@ -578,13 +628,20 @@ void InputTest::TerminalInputNullKeyTests() { using namespace std::string_view_literals; + auto layout = TestHook::SetTerminalInputKeyboardLayout(L"00000409"); // US English + if (!layout) + { + Log::Result(TestResults::Result::Skipped); + return; + } + unsigned int uiKeystate = LEFT_CTRL_PRESSED; TerminalInput input; Log::Comment(L"Sending every possible VKEY at the input stream for interception during key DOWN."); - BYTE vkey = LOBYTE(OneCoreSafeVkKeyScanW(0)); + BYTE vkey = LOBYTE(VkKeyScanExW(0, layout)); Log::Comment(NoThrowString().Format(L"Testing key, state =0x%x, 0x%x", vkey, uiKeystate)); INPUT_RECORD irTest = { 0 }; @@ -600,7 +657,6 @@ void InputTest::TerminalInputNullKeyTests() vkey = VK_SPACE; Log::Comment(NoThrowString().Format(L"Testing key, state =0x%x, 0x%x", vkey, uiKeystate)); irTest.Event.KeyEvent.wVirtualKeyCode = vkey; - irTest.Event.KeyEvent.uChar.UnicodeChar = vkey; VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"\0"sv), input.HandleKey(irTest), L"Verify key was handled if it should have been."); uiKeystate = LEFT_CTRL_PRESSED | LEFT_ALT_PRESSED; diff --git a/src/terminal/adapter/ut_adapter/kittyKeyboardProtocol.cpp b/src/terminal/adapter/ut_adapter/kittyKeyboardProtocol.cpp index 4a46f8d0aac..44f54187016 100644 --- a/src/terminal/adapter/ut_adapter/kittyKeyboardProtocol.cpp +++ b/src/terminal/adapter/ut_adapter/kittyKeyboardProtocol.cpp @@ -6,6 +6,7 @@ #include #include +#include "TestHook.h" #include "../../input/terminalInput.hpp" using namespace WEX::TestExecution; @@ -65,24 +66,24 @@ namespace constexpr TestCase testCases[] = { // Core behavior: DisambiguateEscapeCodes (D) { L"D Esc", L"\x1b[27u", D, true, VK_ESCAPE, 1, 0, 0 }, - { L"D Ctrl+a", L"\x1b[97;5u", D, true, 'A', 0x1E, L'\x01', Ctrl }, - { L"D Ctrl+Alt+a", L"\x1b[97;7u", D, true, 'A', 0x1E, L'\x01', Ctrl | Alt }, - { L"D Shift+Alt+a", L"\x1b[97;4u", D, true, 'A', 0x1E, L'A', Shift | Alt }, - { L"D Shift+a", L"A", D, true, 'A', 0x1E, L'A', Shift }, + { L"D Ctrl+a", L"\x1b[97;5u", D, true, 'A', 0x10, 0, Ctrl }, + { L"D Ctrl+Alt+a", L"æ", D, true, 'A', 0x10, L'æ', Ctrl | Alt }, + { L"D Shift+Alt+a", L"\x1b[97;4u", D, true, 'A', 0x10, L'A', Shift | Alt }, + { L"D Shift+a", L"A", D, true, 'A', 0x10, L'A', Shift }, // Modifiers with AllKeys (K): all keys use CSI u - { L"K a", L"\x1b[97u", K, true, 'A', 0x1E, L'a', 0 }, - { L"K Shift+a", L"\x1b[97;2u", K, true, 'A', 0x1E, L'A', Shift }, - { L"K Alt+a", L"\x1b[97;3u", K, true, 'A', 0x1E, L'a', Alt }, - { L"K Ctrl+a", L"\x1b[97;5u", K, true, 'A', 0x1E, L'\x01', Ctrl }, - { L"K Shift+Alt+a", L"\x1b[97;4u", K, true, 'A', 0x1E, L'A', Shift | Alt }, - { L"K Shift+Ctrl+a", L"\x1b[97;6u", K, true, 'A', 0x1E, L'\x01', Shift | Ctrl }, - { L"K Alt+Ctrl+a", L"\x1b[97;7u", K, true, 'A', 0x1E, L'\x01', Alt | Ctrl }, - { L"K Shift+Alt+Ctrl+a", L"\x1b[97;8u", K, true, 'A', 0x1E, L'\x01', Shift | Alt | Ctrl }, - { L"K CapsLock+a", L"\x1b[97;65u", K, true, 'A', 0x1E, L'A', CAPSLOCK_ON }, - { L"K NumLock+a", L"\x1b[97;129u", K, true, 'A', 0x1E, L'a', NUMLOCK_ON }, - { L"K CapsLock+NumLock+a", L"\x1b[97;193u", K, true, 'A', 0x1E, L'A', CAPSLOCK_ON | NUMLOCK_ON }, - { L"K all mods", L"\x1b[97;200u", K, true, 'A', 0x1E, L'\x01', Shift | Alt | Ctrl | CAPSLOCK_ON | NUMLOCK_ON }, + { L"K a", L"\x1b[97u", K, true, 'A', 0x10, L'a', 0 }, + { L"K Shift+a", L"\x1b[97;2u", K, true, 'A', 0x10, L'A', Shift }, + { L"K Alt+a", L"\x1b[97;3u", K, true, 'A', 0x10, L'a', Alt }, + { L"K Ctrl+a", L"\x1b[97;5u", K, true, 'A', 0x10, 0, Ctrl }, + { L"K Shift+Alt+a", L"\x1b[97;4u", K, true, 'A', 0x10, L'A', Shift | Alt }, + { L"K Shift+Ctrl+a", L"\x1b[97;6u", K, true, 'A', 0x10, 0, Shift | Ctrl }, + { L"K Ctrl+Alt+a", L"\x1b[230u", K, true, 'A', 0x10, L'æ', Ctrl | Alt }, + { L"K Shift+Ctrl+Alt+a", L"\x1b[230;2u", K, true, 'A', 0x10, L'Æ', Shift | Ctrl | Alt }, + { L"K CapsLock+a", L"\x1b[97;65u", K, true, 'A', 0x10, L'A', CAPSLOCK_ON }, + { L"K NumLock+a", L"\x1b[97;129u", K, true, 'A', 0x10, L'a', NUMLOCK_ON }, + { L"K CapsLock+NumLock+a", L"\x1b[97;193u", K, true, 'A', 0x10, L'A', CAPSLOCK_ON | NUMLOCK_ON }, + { L"K all mods", L"\x1b[230;194u", K, true, 'A', 0x10, L'Æ', Shift | Ctrl | Alt | CAPSLOCK_ON | NUMLOCK_ON }, // Enter/Tab/Backspace: CSI u with K { L"K Enter", L"\x1b[13u", K, true, VK_RETURN, 0x1C, L'\r', 0 }, @@ -96,8 +97,8 @@ namespace // Event types (D|E, E|K): release sends ;1:3 { L"D|E Esc press", L"\x1b[27u", D | E, true, VK_ESCAPE, 1, 0, 0 }, { L"D|E Esc release", L"\x1b[27;1:3u", D | E, false, VK_ESCAPE, 1, 0, 0 }, - { L"E|K a press", L"\x1b[97u", E | K, true, 'A', 0x1E, L'a', 0 }, - { L"E|K a release", L"\x1b[97;1:3u", E | K, false, 'A', 0x1E, L'a', 0 }, + { L"E|K a press", L"\x1b[97u", E | K, true, 'A', 0x10, L'a', 0 }, + { L"E|K a release", L"\x1b[97;1:3u", E | K, false, 'A', 0x10, L'a', 0 }, { L"E|K Enter release", L"\x1b[13;1:3u", E | K, false, VK_RETURN, 0x1C, L'\r', 0 }, { L"E|K Tab release", L"\x1b[9;1:3u", E | K, false, VK_TAB, 0x0F, L'\t', 0 }, { L"E|K Backspace release", L"\x1b[127;1:3u", E | K, false, VK_BACK, 0x0E, L'\b', 0 }, @@ -161,21 +162,21 @@ namespace { L"K Shift+F13", L"\x1b[57376;2u", K, true, VK_F13, 0x64, 0, Shift }, // Alternate keys (A|K): shifted key and base layout key - { L"A|K Shift+a", L"\x1b[97:65;2u", A | K, true, 'A', 0x1E, L'A', Shift }, - { L"A|K Shift+1", L"\x1b[49:33;2u", A | K, true, '1', 0x02, L'!', Shift }, - { L"A|K a (no shift)", L"\x1b[97u", A | K, true, 'A', 0x1E, L'a', 0 }, + { L"A|K Shift+a", L"\x1b[97:65:113;2u", A | K, true, 'A', 0x10, L'A', Shift }, + { L"A|K Shift+1", L"\x1b[224:49:49;2u", A | K, true, '1', 0x02, L'!', Shift }, + { L"A|K a (no shift)", L"\x1b[97::113u", A | K, true, 'A', 0x10, L'a', 0 }, // Associated text (K|T): text codepoint in 3rd param - { L"K|T Shift+a", L"\x1b[97;2;65u", K | T, true, 'A', 0x1E, L'A', Shift }, - { L"K|T Shift+1", L"\x1b[49;2;33u", K | T, true, '1', 0x02, L'!', Shift }, - { L"K|T Ctrl+a", L"\x1b[97;5u", K | T, true, 'A', 0x1E, L'\x01', Ctrl }, // control char omitted + { L"K|T Shift+a", L"\x1b[97;2;65u", K | T, true, 'A', 0x10, L'A', Shift }, + { L"K|T Shift+1", L"\x1b[224;2;33u", K | T, true, '1', 0x02, L'!', Shift }, + { L"K|T Ctrl+a", L"\x1b[97;5u", K | T, true, 'A', 0x10, 0, Ctrl }, // control char omitted // Edge cases { L"K Keypad Enter", L"\x1b[57414u", K, true, VK_RETURN, 0x1C, L'\r', ENHANCED_KEY }, { L"K Regular Enter", L"\x1b[13u", K, true, VK_RETURN, 0x1C, L'\r', 0 }, - { L"K Shift+Alt+Ctrl+Esc", L"\x1b[27;8u", K, true, VK_ESCAPE, 1, 0, Shift | Alt | Ctrl }, - { L"E|K CapsLock+a", L"\x1b[97;65u", E | K, true, 'A', 0x1E, L'A', CAPSLOCK_ON }, - { L"E|K all mods release", L"\x1b[97;200:3u", E | K, false, 'A', 0x1E, L'\x01', Shift | Alt | Ctrl | CAPSLOCK_ON | NUMLOCK_ON }, + { L"K Shift+Ctrl+Alt+Esc", L"\x1b[27;8u", K, true, VK_ESCAPE, 1, 0, Shift | Ctrl | Alt }, + { L"E|K CapsLock+a", L"\x1b[97;65u", E | K, true, 'A', 0x10, L'A', CAPSLOCK_ON }, + { L"E|K all mods release", L"\x1b[230;194:3u", E | K, false, 'A', 0x10, L'Æ', Shift | Ctrl | Alt | CAPSLOCK_ON | NUMLOCK_ON }, // F1-F4 with kitty flags (CSI instead of SS3, F3 special case) { L"D F1", L"\x1b[P", D, true, VK_F1, 0x3B, 0, 0 }, @@ -210,20 +211,20 @@ namespace { L"E|K Up release", L"\x1b[1;1:3A", E | K, false, VK_UP, 0x48, 0, ENHANCED_KEY }, { L"E|K Insert release", L"\x1b[2;1:3~", E | K, false, VK_INSERT, 0x52, 0, ENHANCED_KEY }, // Alternate keys with modifiers - { L"A|K Shift+Ctrl+a", L"\x1b[97:65;6u", A | K, true, 'A', 0x1E, L'\x01', Shift | Ctrl }, + { L"A|K Shift+Ctrl+a", L"\x1b[97:65:113;6u", A | K, true, 'A', 0x10, 0, Shift | Ctrl }, // Associated text with plain key - { L"K|T a", L"\x1b[97;;97u", K | T, true, 'A', 0x1E, L'a', 0 }, + { L"K|T a", L"\x1b[97;;97u", K | T, true, 'A', 0x10, L'a', 0 }, // Text not reported on release - { L"E|K|T a release", L"\x1b[97;1:3u", E | K | T, false, 'A', 0x1E, L'a', 0 }, + { L"E|K|T a release", L"\x1b[97;1:3u", E | K | T, false, 'A', 0x10, L'a', 0 }, // Escape has no associated text { L"K|T Esc", L"\x1b[27u", K | T, true, VK_ESCAPE, 1, 0, 0 }, // Combined flags: alternate keys with locks - { L"A|K CapsLock+Shift+a", L"\x1b[97:65;66u", A | K, true, 'A', 0x1E, L'a', CAPSLOCK_ON | Shift }, + { L"A|K CapsLock+Shift+a", L"\x1b[97:65:113;66u", A | K, true, 'A', 0x10, L'a', CAPSLOCK_ON | Shift }, // All flags combined - { L"A|K|T Shift+a", L"\x1b[97:65;2;65u", A | K | T, true, 'A', 0x1E, L'A', Shift }, + { L"A|K|T Shift+a", L"\x1b[97:65:113;2;65u", A | K | T, true, 'A', 0x10, L'A', Shift }, // Release without EventTypes flag: no output - { L"K a release (no EventTypes)", L"", K, false, 'A', 0x1E, L'a', 0 }, + { L"K a release (no EventTypes)", L"", K, false, 'A', 0x10, L'a', 0 }, // Enter/Tab/Backspace release without AllKeys: no output { L"D|E Enter press", L"\r", D | E, true, VK_RETURN, 0x1C, L'\r', 0 }, @@ -238,8 +239,8 @@ namespace { L"E|K CapsLock release (now on)", L"\x1b[57358;65:3u", E | K, false, VK_CAPITAL, 0x3A, 0, CAPSLOCK_ON }, // Associated text filtering - { L"K|T Shift+a (text)", L"\x1b[97;2;65u", K | T, true, 'A', 0x1E, L'A', Shift }, - { L"K|T Ctrl+a (control char filtered)", L"\x1b[97;5u", K | T, true, 'A', 0x1E, L'\x01', Ctrl }, + { L"K|T Shift+a (text)", L"\x1b[97;2;65u", K | T, true, 'A', 0x10, L'A', Shift }, + { L"K|T Ctrl+a (control char filtered)", L"\x1b[97;5u", K | T, true, 'A', 0x10, 0, Ctrl }, { L"K|T Esc (no text)", L"\x1b[27u", K | T, true, VK_ESCAPE, 1, 0, 0 }, }; } @@ -251,8 +252,26 @@ extern "C" HRESULT __declspec(dllexport) __cdecl KittyKeyTestDataSource(IDataSou class KittyKeyboardProtocolTests { + TestHook::LayoutGuard layout; + TEST_CLASS(KittyKeyboardProtocolTests); + TEST_CLASS_SETUP(ClassSetup) + { + layout = TestHook::SetTerminalInputKeyboardLayout(L"0001040c"); // French (Standard, AZERTY) + if (!layout) + { + Log::Result(TestResults::Result::Skipped); + } + return true; + } + + TEST_CLASS_CLEANUP(ClassCleanup) + { + layout = {}; + return true; + } + TEST_METHOD(KeyPressTests) { BEGIN_TEST_METHOD_PROPERTIES() @@ -278,25 +297,25 @@ class KittyKeyboardProtocolTests TEST_METHOD(KeyRepeatEvents) { auto input = createInput(E | K); - VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"\x1b[97u"), process(input, true, 'A', 0x1E, L'a', 0)); - VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"\x1b[97;1:2u"), process(input, true, 'A', 0x1E, L'a', 0)); // repeat - VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"\x1b[97;1:2u"), process(input, true, 'A', 0x1E, L'a', 0)); // repeat - VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"\x1b[97;1:3u"), process(input, false, 'A', 0x1E, L'a', 0)); // release - VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"\x1b[97u"), process(input, true, 'A', 0x1E, L'a', 0)); // new press + VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"\x1b[97u"), process(input, true, 'A', 0x10, L'a', 0)); + VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"\x1b[97;1:2u"), process(input, true, 'A', 0x10, L'a', 0)); // repeat + VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"\x1b[97;1:2u"), process(input, true, 'A', 0x10, L'a', 0)); // repeat + VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"\x1b[97;1:3u"), process(input, false, 'A', 0x10, L'a', 0)); // release + VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"\x1b[97u"), process(input, true, 'A', 0x10, L'a', 0)); // new press } TEST_METHOD(KeyRepeatWithModifiers) { auto input = createInput(E | K); - VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"\x1b[97;2u"), process(input, true, 'A', 0x1E, L'A', SHIFT_PRESSED)); - VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"\x1b[97;2:2u"), process(input, true, 'A', 0x1E, L'A', SHIFT_PRESSED)); + VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"\x1b[97;2u"), process(input, true, 'A', 0x10, L'A', SHIFT_PRESSED)); + VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"\x1b[97;2:2u"), process(input, true, 'A', 0x10, L'A', SHIFT_PRESSED)); } TEST_METHOD(KeyRepeatResetOnDifferentKey) { auto input = createInput(E | K); - VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"\x1b[97u"), process(input, true, 'A', 0x1E, L'a', 0)); + VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"\x1b[97u"), process(input, true, 'A', 0x10, L'a', 0)); VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"\x1b[98u"), process(input, true, 'B', 0x30, L'b', 0)); // different key - VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"\x1b[97u"), process(input, true, 'A', 0x1E, L'a', 0)); // not repeat + VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"\x1b[97u"), process(input, true, 'A', 0x10, L'a', 0)); // not repeat } }; diff --git a/src/terminal/input/TestHook.cpp b/src/terminal/input/TestHook.cpp new file mode 100644 index 00000000000..01acd44aad4 --- /dev/null +++ b/src/terminal/input/TestHook.cpp @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +// This default no-op implementation lives in its own .obj so that the linker +// can skip it when a test DLL supplies its own definition. The classic linking +// model only pulls in .obj files from a .lib if they resolve an otherwise +// unresolved symbol - and nothing else in the test DLL refers to this file. +// See: https://devblogs.microsoft.com/oldnewthing/20250416-00/?p=111077 +extern "C" HKL TestHook_TerminalInput_KeyboardLayout() +{ + return nullptr; +} diff --git a/src/terminal/input/lib/terminalinput.vcxproj b/src/terminal/input/lib/terminalinput.vcxproj index 2db4e9fd5c2..cb8a2b57740 100644 --- a/src/terminal/input/lib/terminalinput.vcxproj +++ b/src/terminal/input/lib/terminalinput.vcxproj @@ -13,6 +13,7 @@ + Create diff --git a/src/terminal/input/mouseInput.cpp b/src/terminal/input/mouseInput.cpp index 99ba22eccba..3e8116cdeda 100644 --- a/src/terminal/input/mouseInput.cpp +++ b/src/terminal/input/mouseInput.cpp @@ -521,6 +521,6 @@ TerminalInput::OutputType TerminalInput::_makeAlternateScrollOutput(const unsign _encodeRegular(enc, key); std::wstring str; - _formatEncodingHelper(enc, str); + _formatEncodingHelper(enc, key, str); return str; } diff --git a/src/terminal/input/sources.inc b/src/terminal/input/sources.inc index cf6c95dd2c5..19de4e70dd3 100644 --- a/src/terminal/input/sources.inc +++ b/src/terminal/input/sources.inc @@ -30,6 +30,7 @@ PRECOMPILED_INCLUDE = ..\precomp.h SOURCES= \ ..\terminalInput.cpp \ ..\mouseInput.cpp \ + ..\TestHook.cpp \ INCLUDES = \ $(INCLUDES); \ diff --git a/src/terminal/input/terminalInput.cpp b/src/terminal/input/terminalInput.cpp index 2c2401cd804..4d7b57e41bd 100644 --- a/src/terminal/input/terminalInput.cpp +++ b/src/terminal/input/terminalInput.cpp @@ -277,21 +277,48 @@ TerminalInput::OutputType TerminalInput::HandleKey(const INPUT_RECORD& event) } // Keep track of key repeats. - key.keyRepeat = _lastVirtualKeyCode == key.virtualKey; + // + // For modifier keys: + // * Map the vkey to a dwControlKeyState flag + // (_controlKeyStateFromVirtualKey returns 0 for non-modifier keys) + // * Checking whether the flag was already set previously + // For standard keys: + // * Simply check if the last vkey equals the current one + // + // This split helps with international keyboard layouts that use the KLLF_ALTGR flag. + // Those generate interleaved LEFT_CTRL_PRESSED and RIGHT_ALT_PRESSED events, + // which a single _lastVirtualKeyCode field will fail to track. if (key.keyDown) { - _lastVirtualKeyCode = key.virtualKey; + if (const auto flags = _controlKeyStateFromVirtualKey(key.virtualKey, key.controlKeyState)) + { + key.keyRepeat = (_previousControlKeyState & flags) != 0; + } + else + { + key.keyRepeat = _lastVirtualKeyCode == key.virtualKey; + _lastVirtualKeyCode = key.virtualKey; + } } - else if (key.keyRepeat) + else { _lastVirtualKeyCode = std::nullopt; } - // If this is a repeat of the last recorded key press, and Auto Repeat Mode - // is disabled, then we should suppress this event. - if (key.keyRepeat && !_inputMode.test(Mode::AutoRepeat)) + if (key.keyRepeat) { - return _makeNoOutput(); + if ( + // Suppress modifier key events at all times - they aren't reported in any protocol. + (key.virtualKey >= VK_SHIFT && key.virtualKey <= VK_MENU) || + (key.virtualKey >= VK_LSHIFT && key.virtualKey <= VK_RMENU) || + (_kittyFlags != 0 ? + // If KKP is enabled, we only report repeats if ReportEventTypes is enabled. + WI_IsFlagClear(_kittyFlags, KittyKeyboardProtocolFlags::ReportEventTypes) : + // Otherwise, it depends on the classic auto-repeat mode setting. + !_inputMode.test(Mode::AutoRepeat))) + { + return _makeNoOutput(); + } } // There's a bunch of early returns we can place on key-up events, @@ -331,19 +358,42 @@ TerminalInput::OutputType TerminalInput::HandleKey(const INPUT_RECORD& event) // 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. - key.leftCtrlIsReallyPressed = WI_IsFlagSet(key.controlKeyState, LEFT_CTRL_PRESSED); + auto leftCtrlIsReallyPressed = false; if (WI_AreAllFlagsSet(key.controlKeyState, LEFT_CTRL_PRESSED | RIGHT_ALT_PRESSED)) { const auto max = std::max(_lastLeftCtrlTime, _lastRightAltTime); const auto min = std::min(_lastLeftCtrlTime, _lastRightAltTime); - key.leftCtrlIsReallyPressed = (max - min) > 50; + leftCtrlIsReallyPressed = (max - min) > 50; } + const auto anyCtrlPressed = WI_IsAnyFlagSet(key.controlKeyState, CTRL_PRESSED); + const auto bothCtrlPressed = WI_AreAllFlagsSet(key.controlKeyState, CTRL_PRESSED); + const auto anyAltPressed = WI_IsAnyFlagSet(key.controlKeyState, ALT_PRESSED); + const auto bothAltPressed = WI_AreAllFlagsSet(key.controlKeyState, ALT_PRESSED); + // We distinguish AltGr+Key / Ctrl+Alt+Key combinations on international keyboard layouts from + // genuine, intentional Ctrl+Alt+Key combinations by checking whether the codepoint is valid. + // Windows should not send a valid codepoint for e.g. Ctrl+Alt+Q on a US ANSI layout, + // so we treat it as a genuine Ctrl+Alt+Q. + // + // However, this isn't universally true and more of a heuristic. Ctrl+Alt+Esc + // for instance results in codepoint=0x1b! As such we restrict to graphical codepoints. + // This should not be considered "Reference Windows Code". It's a personal best guess. + key.altGrPressed = anyAltPressed && anyCtrlPressed && (key.codepoint > 0x20 && key.codepoint != 0x7f); + // Ctrl is a bit tricky to detect, since international keyboards with KLLF_ALTGR will + // send Left-Ctrl + Right-Alt. If both Ctrl keys are pressed it's unambiguous. + // Otherwise, if we haven't guessed this to be an AltGr key, then we can safely + // assume this to be a Ctrl combination as well. Otherwise, we also have our + // timing logic above to guess if the Left-Ctrl key was pressed by a human. + key.ctrlPressed = bothCtrlPressed || (anyCtrlPressed && !key.altGrPressed) || leftCtrlIsReallyPressed; + // Alt is a bit simpler than Ctrl and follows the same pattern. + key.altPressed = bothAltPressed || (anyAltPressed && !key.altGrPressed); + key.shiftPressed = WI_IsFlagSet(key.controlKeyState, SHIFT_PRESSED); + KeyboardHelper kbd; EncodingHelper enc; - WI_SetFlagIf(enc.csiModifier, CSI_CTRL, key.leftCtrlIsReallyPressed || WI_IsFlagSet(key.controlKeyState, RIGHT_CTRL_PRESSED)); - WI_SetFlagIf(enc.csiModifier, CSI_ALT, WI_IsAnyFlagSet(key.controlKeyState, ALT_PRESSED)); - WI_SetFlagIf(enc.csiModifier, CSI_SHIFT, WI_IsFlagSet(key.controlKeyState, SHIFT_PRESSED)); + WI_SetFlagIf(enc.csiModifier, CSI_CTRL, key.ctrlPressed); + WI_SetFlagIf(enc.csiModifier, CSI_ALT, key.altPressed); + WI_SetFlagIf(enc.csiModifier, CSI_SHIFT, key.shiftPressed); if (_kittyFlags == 0 || !_encodeKitty(kbd, enc, key)) { @@ -351,9 +401,9 @@ TerminalInput::OutputType TerminalInput::HandleKey(const INPUT_RECORD& event) } std::wstring seq; - if (!_formatEncodingHelper(enc, seq)) + if (!_formatEncodingHelper(enc, key, seq)) { - _formatFallback(kbd, enc, key, seq); + _formatFallback(kbd, key, seq); } return seq; } @@ -390,6 +440,8 @@ void TerminalInput::_initKeyboardMap() noexcept DWORD TerminalInput::_trackControlKeyState(const KEY_EVENT_RECORD& key) noexcept { + _previousControlKeyState = _lastControlKeyState; + // 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. @@ -402,21 +454,35 @@ DWORD TerminalInput::_trackControlKeyState(const KEY_EVENT_RECORD& key) noexcept // 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 _lastControlKeyState; +} + +// Maps a modifier virtual key code to its corresponding dwControlKeyState flag. +// Returns 0 for non-modifier keys. For VK_CONTROL and VK_MENU, the ENHANCED_KEY +// bit in controlKeyState disambiguates left vs. right. +DWORD TerminalInput::_controlKeyStateFromVirtualKey(uint16_t vk, uint32_t controlKeyState) noexcept +{ + switch (vk) { - if (WI_IsFlagSet(pressedKeyState, LEFT_CTRL_PRESSED)) - { - _lastLeftCtrlTime = GetTickCount64(); - } - if (WI_IsFlagSet(pressedKeyState, RIGHT_ALT_PRESSED)) - { - _lastRightAltTime = GetTickCount64(); - } + case VK_SHIFT: + case VK_LSHIFT: + case VK_RSHIFT: + return SHIFT_PRESSED; + case VK_CONTROL: + return WI_IsFlagSet(controlKeyState, ENHANCED_KEY) ? RIGHT_CTRL_PRESSED : LEFT_CTRL_PRESSED; + case VK_LCONTROL: + return LEFT_CTRL_PRESSED; + case VK_RCONTROL: + return RIGHT_CTRL_PRESSED; + case VK_MENU: + return WI_IsFlagSet(controlKeyState, ENHANCED_KEY) ? RIGHT_ALT_PRESSED : LEFT_ALT_PRESSED; + case VK_LMENU: + return LEFT_ALT_PRESSED; + case VK_RMENU: + return RIGHT_ALT_PRESSED; + default: + return 0; } - return _lastControlKeyState; } uint32_t TerminalInput::_makeCtrlChar(const uint32_t ch) noexcept @@ -623,7 +689,7 @@ bool TerminalInput::_encodeKitty(KeyboardHelper& kbd, EncodingHelper& enc, const // KKP> Note that the shifted key must be present only if shift is also present in the modifiers. - if (isTextKey(functionalKeyCode) && enc.shiftPressed()) + if (isTextKey(functionalKeyCode) && key.shiftPressed) { // This is almost identical to our computation of the "base key" for // ReportAllKeysAsEscapeCodes above, but this time with SHIFT_PRESSED. @@ -839,9 +905,9 @@ void TerminalInput::_encodeRegular(EncodingHelper& enc, const SanitizedKeyEvent& // not standard, but a modern terminal convention). The Alt modifier adds // an ESC prefix (also not standard). enc.altPrefix = true; - const auto ctrl = (enc.csiModifier & CSI_CTRL) == 0; + const auto ctrl = key.ctrlPressed; const auto back = _inputMode.test(Mode::BackarrowKey); - enc.plain = ctrl != back ? L"\x7f"sv : L"\b"sv; + enc.plain = ctrl == back ? L"\x7f"sv : L"\b"sv; break; } case VK_TAB: @@ -849,7 +915,7 @@ void TerminalInput::_encodeRegular(EncodingHelper& enc, const SanitizedKeyEvent& // The Alt modifier adds an ESC prefix, although in practice all the Alt // mappings are likely to be system hotkeys. enc.altPrefix = true; - if ((enc.csiModifier & CSI_SHIFT) == 0) + if (!key.shiftPressed) { enc.plain = L"\t"sv; } @@ -879,7 +945,7 @@ void TerminalInput::_encodeRegular(EncodingHelper& enc, const SanitizedKeyEvent& } else { - if ((enc.csiModifier & CSI_CTRL) == 0) + if (!key.ctrlPressed) { enc.plain = _inputMode.test(Mode::LineFeed) ? L"\r\n"sv : L"\r"sv; } @@ -1104,12 +1170,12 @@ void TerminalInput::_encodeRegular(EncodingHelper& enc, const SanitizedKeyEvent& } } -bool TerminalInput::_formatEncodingHelper(EncodingHelper& enc, std::wstring& seq) const +bool TerminalInput::_formatEncodingHelper(EncodingHelper& enc, const SanitizedKeyEvent& key, std::wstring& seq) const { // NOTE: altPrefix is only ever true for `_fillRegularKeyEncodingInfo` calls, // and only if one of the 3 conditions below applies. // In other words, we return with an unmodified `str` if `enc` is unmodified. - if (enc.altPrefix && enc.altPressed() && _inputMode.test(Mode::Ansi)) + if (enc.altPrefix && key.altPressed && _inputMode.test(Mode::Ansi)) { seq.push_back(L'\x1b'); } @@ -1180,7 +1246,7 @@ bool TerminalInput::_formatEncodingHelper(EncodingHelper& enc, std::wstring& seq return false; } -void TerminalInput::_formatFallback(KeyboardHelper& kbd, const EncodingHelper& enc, const SanitizedKeyEvent& key, std::wstring& seq) const +void TerminalInput::_formatFallback(KeyboardHelper& kbd, const SanitizedKeyEvent& key, std::wstring& seq) const { // If this is a modifier, it won't produce output, so we can return early. if (key.virtualKey >= VK_SHIFT && key.virtualKey <= VK_MENU) @@ -1188,41 +1254,24 @@ void TerminalInput::_formatFallback(KeyboardHelper& kbd, const EncodingHelper& e return; } - const auto anyAltPressed = key.anyAltPressed(); auto codepoint = key.codepoint; // If it's not in the key map, we'll use the UnicodeChar, if provided, // except in the case of Ctrl+Space, which is often mapped incorrectly as // a space character when it's expected to be mapped to NUL. We need to // let that fall through to the standard mapping algorithm below. - const auto ctrlSpaceKey = enc.ctrlPressed() && key.virtualKey == VK_SPACE; + const auto ctrlSpaceKey = key.ctrlPressed && key.virtualKey == VK_SPACE; if (codepoint != 0 && !ctrlSpaceKey) { - // 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 altGrPressed = key.altGrPressed(); - const auto bothAltPressed = key.bothAltPressed(); - const auto bothCtrlPressed = key.bothCtrlPressed(); - const auto rightAltPressed = key.rightAltPressed(); - - if (altGrPressed && (bothCtrlPressed || (rightAltPressed && key.leftCtrlIsReallyPressed))) + if (key.ctrlPressed) { codepoint = _makeCtrlChar(codepoint); } - - // 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 wantsEscPrefix = altGrPressed ? bothAltPressed : anyAltPressed; - if (wantsEscPrefix && _inputMode.test(Mode::Ansi)) - { - seq.push_back(L'\x1b'); - } } // 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. - else if (anyAltPressed || WI_IsAnyFlagSet(key.controlKeyState, CTRL_PRESSED)) + else if (key.altPressed || key.ctrlPressed) { // IMPORTANT NOTE: This implicitly, reliably rejects dead keys for us (good!). // @@ -1238,14 +1287,8 @@ void TerminalInput::_formatFallback(KeyboardHelper& kbd, const EncodingHelper& e return; } - // If Alt is pressed, that also needs to be applied to the sequence. - if (anyAltPressed && _inputMode.test(Mode::Ansi)) - { - seq.push_back(L'\x1b'); - } - // Once we've got the base character, we can apply the Ctrl modifier. - if (enc.ctrlPressed()) + if (key.ctrlPressed) { codepoint = _makeCtrlChar(codepoint); // If we haven't found a Ctrl mapping for the key, and it's one of @@ -1263,6 +1306,12 @@ void TerminalInput::_formatFallback(KeyboardHelper& kbd, const EncodingHelper& e return; } + // If Alt is pressed, that also needs to be applied to the sequence. + if (key.altPressed && _inputMode.test(Mode::Ansi)) + { + seq.push_back(L'\x1b'); + } + _stringPushCodepoint(seq, codepoint); } @@ -1311,9 +1360,8 @@ TerminalInput::CodepointBuffer::CodepointBuffer(uint32_t cp) noexcept void TerminalInput::CodepointBuffer::convertLowercase() noexcept { // NOTE: MSDN states that `lpSrcStr == lpDestStr` is valid for LCMAP_LOWERCASE. - len = LCMapStringW(LOCALE_INVARIANT, LCMAP_LOWERCASE, &buf[0], len, &buf[0], ARRAYSIZE(buf)); - // NOTE: LCMapStringW returns the length including the null terminator. - len -= 1; + // NOTE: LCMapStringEx does not null-terminate the output if there's insufficient space. As such we subtract 1 from the buf size. + len = LCMapStringEx(LOCALE_NAME_INVARIANT, LCMAP_LOWERCASE, &buf[0], len, &buf[0], ARRAYSIZE(buf) - 1, nullptr, nullptr, 0); } uint32_t TerminalInput::CodepointBuffer::asSingleCodepoint() const noexcept @@ -1335,49 +1383,33 @@ uint32_t TerminalInput::CodepointBuffer::asSingleCodepoint() const noexcept return InvalidCodepoint; } -bool TerminalInput::SanitizedKeyEvent::anyAltPressed() const noexcept -{ - return WI_IsAnyFlagSet(controlKeyState, ALT_PRESSED); -} - -bool TerminalInput::SanitizedKeyEvent::bothAltPressed() const noexcept +uint32_t TerminalInput::KeyboardHelper::getUnmodifiedKeyboardKey(const SanitizedKeyEvent& key) noexcept { - return WI_AreAllFlagsSet(controlKeyState, ALT_PRESSED); + return getKeyboardKeyHelper(key, ALT_PRESSED | CTRL_PRESSED, 0); } -bool TerminalInput::SanitizedKeyEvent::rightAltPressed() const noexcept +uint32_t TerminalInput::KeyboardHelper::getKittyBaseKey(const SanitizedKeyEvent& key) noexcept { - return WI_IsFlagSet(controlKeyState, RIGHT_ALT_PRESSED); + return _codepointToLower(getKeyboardKeyHelper(key, ALT_PRESSED | CTRL_PRESSED | SHIFT_PRESSED | CAPSLOCK_ON, 0)); } -bool TerminalInput::SanitizedKeyEvent::bothCtrlPressed() const noexcept +uint32_t TerminalInput::KeyboardHelper::getKittyShiftedKey(const SanitizedKeyEvent& key) noexcept { - return WI_AreAllFlagsSet(controlKeyState, CTRL_PRESSED); + return getKeyboardKeyHelper(key, ALT_PRESSED | CTRL_PRESSED | CAPSLOCK_ON, SHIFT_PRESSED); } -bool TerminalInput::SanitizedKeyEvent::altGrPressed() const noexcept -{ - return WI_IsAnyFlagSet(controlKeyState, ALT_PRESSED) && WI_IsAnyFlagSet(controlKeyState, CTRL_PRESSED); -} - -uint32_t TerminalInput::KeyboardHelper::getUnmodifiedKeyboardKey(const SanitizedKeyEvent& key) noexcept +uint32_t TerminalInput::KeyboardHelper::getKeyboardKeyHelper(const SanitizedKeyEvent& key, DWORD removeFlags, DWORD addFlags) noexcept { const auto virtualKey = key.virtualKey; - const auto controlKeyState = key.controlKeyState & ~(ALT_PRESSED | CTRL_PRESSED); - return getKeyboardKey(virtualKey, controlKeyState, nullptr); -} + auto controlKeyState = (key.controlKeyState & ~removeFlags) | addFlags; -uint32_t TerminalInput::KeyboardHelper::getKittyBaseKey(const SanitizedKeyEvent& key) noexcept -{ - const auto virtualKey = key.virtualKey; - const auto controlKeyState = key.controlKeyState & ~(ALT_PRESSED | CTRL_PRESSED | SHIFT_PRESSED | CAPSLOCK_ON); - return _codepointToLower(getKeyboardKey(virtualKey, controlKeyState, nullptr)); -} + // In the context of KKP, AltGr acts more like a keyboard "layer" toggle. + // It's not a modifier that's ever transmitted as-is and instead modifies the actual base key code. + if (key.altGrPressed) + { + controlKeyState |= LEFT_CTRL_PRESSED | RIGHT_ALT_PRESSED; + } -uint32_t TerminalInput::KeyboardHelper::getKittyShiftedKey(const SanitizedKeyEvent& key) noexcept -{ - const auto virtualKey = key.virtualKey; - const auto controlKeyState = key.controlKeyState & ~(ALT_PRESSED | CTRL_PRESSED | CAPSLOCK_ON) | SHIFT_PRESSED; return getKeyboardKey(virtualKey, controlKeyState, nullptr); } @@ -1465,9 +1497,21 @@ void TerminalInput::KeyboardHelper::init() noexcept } } +// The default no-op implementation lives in TestHook.cpp (its own .obj) so the +// linker can skip it when a test DLL supplies its own definition. +extern "C" HKL TestHook_TerminalInput_KeyboardLayout(); + void TerminalInput::KeyboardHelper::initSlow() noexcept { - _keyboardLayout = GetKeyboardLayout(GetWindowThreadProcessId(GetForegroundWindow(), nullptr)); + if (const auto hkl = TestHook_TerminalInput_KeyboardLayout()) + { + _keyboardLayout = hkl; + } + else + { + _keyboardLayout = GetKeyboardLayout(GetWindowThreadProcessId(GetForegroundWindow(), nullptr)); + } + memset(&_keyboardState[0], 0, sizeof(_keyboardState)); _initialized = true; } @@ -1476,18 +1520,3 @@ TerminalInput::EncodingHelper::EncodingHelper() noexcept { memset(this, 0, sizeof(*this)); } - -bool TerminalInput::EncodingHelper::shiftPressed() const noexcept -{ - return csiModifier & CSI_SHIFT; -} - -bool TerminalInput::EncodingHelper::altPressed() const noexcept -{ - return csiModifier & CSI_ALT; -} - -bool TerminalInput::EncodingHelper::ctrlPressed() const noexcept -{ - return csiModifier & CSI_CTRL; -} diff --git a/src/terminal/input/terminalInput.hpp b/src/terminal/input/terminalInput.hpp index 80c4be509f8..f9a2eff8f2e 100644 --- a/src/terminal/input/terminalInput.hpp +++ b/src/terminal/input/terminalInput.hpp @@ -109,15 +109,12 @@ namespace Microsoft::Console::VirtualTerminal uint16_t scanCode = 0; uint32_t codepoint = 0; uint32_t controlKeyState = 0; - bool leftCtrlIsReallyPressed = false; bool keyDown = false; bool keyRepeat = false; - - bool anyAltPressed() const noexcept; - bool bothAltPressed() const noexcept; - bool rightAltPressed() const noexcept; - bool bothCtrlPressed() const noexcept; - bool altGrPressed() const noexcept; + bool altGrPressed = false; + bool ctrlPressed = false; + bool altPressed = false; + bool shiftPressed = false; }; struct KeyboardHelper @@ -132,6 +129,7 @@ namespace Microsoft::Console::VirtualTerminal private: uint32_t getKeyboardKey(UINT vkey, DWORD controlKeyState, HKL hkl) noexcept; + uint32_t getKeyboardKeyHelper(const SanitizedKeyEvent& key, DWORD removeFlags, DWORD addFlags) noexcept; void init() noexcept; void initSlow() noexcept; @@ -145,11 +143,6 @@ namespace Microsoft::Console::VirtualTerminal struct EncodingHelper { explicit EncodingHelper() noexcept; - - bool shiftPressed() const noexcept; - bool altPressed() const noexcept; - bool ctrlPressed() const noexcept; - // The KKP CSI u sequence is a superset of other CSI sequences: // CSI unicode-key-code:alternate-key-code-shift:alternate-key-code-base ; modifiers:event-type ; text-as-codepoint u uint32_t csiUnicodeKeyCode; @@ -180,6 +173,7 @@ namespace Microsoft::Console::VirtualTerminal std::optional _lastVirtualKeyCode; DWORD _lastControlKeyState = 0; + DWORD _previousControlKeyState = 0; uint64_t _lastLeftCtrlTime = 0; uint64_t _lastRightAltTime = 0; @@ -201,6 +195,7 @@ namespace Microsoft::Console::VirtualTerminal void _initKeyboardMap() noexcept; DWORD _trackControlKeyState(const KEY_EVENT_RECORD& key) noexcept; + [[nodiscard]] static DWORD _controlKeyStateFromVirtualKey(uint16_t vk, uint32_t controlKeyState) noexcept; [[nodiscard]] static uint32_t _makeCtrlChar(uint32_t ch) noexcept; [[nodiscard]] static StringType _makeCharOutput(uint32_t ch); [[nodiscard]] static StringType _makeNoOutput() noexcept; @@ -208,8 +203,8 @@ namespace Microsoft::Console::VirtualTerminal bool _encodeKitty(KeyboardHelper& kbd, EncodingHelper& enc, const SanitizedKeyEvent& key) noexcept; static uint32_t _getKittyFunctionalKeyCode(UINT vkey, WORD scanCode, bool enhanced) noexcept; void _encodeRegular(EncodingHelper& enc, const SanitizedKeyEvent& key) const noexcept; - bool _formatEncodingHelper(EncodingHelper& enc, std::wstring& str) const; - void _formatFallback(KeyboardHelper& kbd, const EncodingHelper& enc, const SanitizedKeyEvent& key, std::wstring& seq) const; + bool _formatEncodingHelper(EncodingHelper& enc, const SanitizedKeyEvent& key, std::wstring& str) const; + void _formatFallback(KeyboardHelper& kbd, const SanitizedKeyEvent& key, std::wstring& seq) const; static void _stringPushCodepoint(std::wstring& str, uint32_t cp); static uint32_t _codepointToLower(uint32_t cp) noexcept;