From 940e8d5c2a79d2fdbeecb7c28a3686be11befbe6 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Wed, 4 Feb 2026 00:44:16 +0100 Subject: [PATCH] wip --- .../ut_adapter/kittyKeyboardProtocol.cpp | 67 ++-- src/terminal/input/terminalInput.cpp | 343 +++++++++--------- src/terminal/input/terminalInput.hpp | 88 ++++- 3 files changed, 289 insertions(+), 209 deletions(-) diff --git a/src/terminal/adapter/ut_adapter/kittyKeyboardProtocol.cpp b/src/terminal/adapter/ut_adapter/kittyKeyboardProtocol.cpp index 4108448251..ae69e0c2cd 100644 --- a/src/terminal/adapter/ut_adapter/kittyKeyboardProtocol.cpp +++ b/src/terminal/adapter/ut_adapter/kittyKeyboardProtocol.cpp @@ -96,7 +96,7 @@ namespace // flags=0 (0b00000): No enhancements - legacy mode // Escape key in legacy mode: just ESC byte - TestCase{ L"Flags=0 (none) Esc key", L"\x1b", 0, true, VK_ESCAPE, 0x01, 0, 0 }, + TestCase{ L"Flags=0 (none) Esc key", L"\x1b", 0, true, VK_ESCAPE, 0x01, L'\x1b', 0 }, // flags=1 (0b00001): DisambiguateEscapeCodes only // Escape key becomes CSI 27 u @@ -104,24 +104,24 @@ namespace // flags=2 (0b00010): ReportEventTypes only // No disambiguation, so Esc is still legacy (but with event type tracking internally) - TestCase{ L"Flags=2 (EventTypes) Esc key down", L"\x1b", 2, true, VK_ESCAPE, 0x01, 0, 0 }, + TestCase{ L"Flags=2 (EventTypes) Esc key down", L"\x1b", 2, true, VK_ESCAPE, 0x01, L'\x1b', 0 }, // flags=3 (0b00011): Disambiguate + EventTypes // Escape key with event type: CSI 27;1:1 u (mod=1, event=press=1) - TestCase{ L"Flags=3 (Disambiguate+EventTypes) Esc key press", L"\x1b[27;1:1u", 3, true, VK_ESCAPE, 0x01, 0, 0 }, + TestCase{ L"Flags=3 (Disambiguate+EventTypes) Esc key press", L"\x1b[27u", 3, true, VK_ESCAPE, 0x01, 0, 0 }, // flags=4 (0b00100): ReportAlternateKeys only // Without Disambiguate, Escape is still legacy - TestCase{ L"Flags=4 (AltKeys) Esc key", L"\x1b", 4, true, VK_ESCAPE, 0x01, 0, 0 }, + TestCase{ L"Flags=4 (AltKeys) Esc key", L"\x1b", 4, true, VK_ESCAPE, 0x01, L'\x1b', 0 }, // flags=5 (0b00101): Disambiguate + AltKeys TestCase{ L"Flags=5 (Disambiguate+AltKeys) Esc key", L"\x1b[27u", 5, true, VK_ESCAPE, 0x01, 0, 0 }, // flags=6 (0b00110): EventTypes + AltKeys - TestCase{ L"Flags=6 (EventTypes+AltKeys) Esc key", L"\x1b", 6, true, VK_ESCAPE, 0x01, 0, 0 }, + TestCase{ L"Flags=6 (EventTypes+AltKeys) Esc key", L"\x1b", 6, true, VK_ESCAPE, 0x01, L'\x1b', 0 }, // flags=7 (0b00111): Disambiguate + EventTypes + AltKeys - TestCase{ L"Flags=7 (Disambiguate+EventTypes+AltKeys) Esc key press", L"\x1b[27;1:1u", 7, true, VK_ESCAPE, 0x01, 0, 0 }, + TestCase{ L"Flags=7 (Disambiguate+EventTypes+AltKeys) Esc key press", L"\x1b[27u", 7, true, VK_ESCAPE, 0x01, 0, 0 }, // flags=8 (0b01000): ReportAllKeysAsEscapeCodes only // All keys become CSI u, including Escape @@ -131,10 +131,10 @@ namespace TestCase{ L"Flags=9 (Disambiguate+AllKeys) Esc key", L"\x1b[27u", 9, true, VK_ESCAPE, 0x01, 0, 0 }, // flags=10 (0b01010): EventTypes + AllKeys - TestCase{ L"Flags=10 (EventTypes+AllKeys) Esc key press", L"\x1b[27;1:1u", 10, true, VK_ESCAPE, 0x01, 0, 0 }, + TestCase{ L"Flags=10 (EventTypes+AllKeys) Esc key press", L"\x1b[27u", 10, true, VK_ESCAPE, 0x01, 0, 0 }, // flags=11 (0b01011): Disambiguate + EventTypes + AllKeys - TestCase{ L"Flags=11 (Disambiguate+EventTypes+AllKeys) Esc key press", L"\x1b[27;1:1u", 11, true, VK_ESCAPE, 0x01, 0, 0 }, + TestCase{ L"Flags=11 (Disambiguate+EventTypes+AllKeys) Esc key press", L"\x1b[27u", 11, true, VK_ESCAPE, 0x01, 0, 0 }, // flags=12 (0b01100): AltKeys + AllKeys TestCase{ L"Flags=12 (AltKeys+AllKeys) Esc key", L"\x1b[27u", 12, true, VK_ESCAPE, 0x01, 0, 0 }, @@ -143,59 +143,59 @@ namespace TestCase{ L"Flags=13 (Disambiguate+AltKeys+AllKeys) Esc key", L"\x1b[27u", 13, true, VK_ESCAPE, 0x01, 0, 0 }, // flags=14 (0b01110): EventTypes + AltKeys + AllKeys - TestCase{ L"Flags=14 (EventTypes+AltKeys+AllKeys) Esc key press", L"\x1b[27;1:1u", 14, true, VK_ESCAPE, 0x01, 0, 0 }, + TestCase{ L"Flags=14 (EventTypes+AltKeys+AllKeys) Esc key press", L"\x1b[27u", 14, true, VK_ESCAPE, 0x01, 0, 0 }, // flags=15 (0b01111): Disambiguate + EventTypes + AltKeys + AllKeys - TestCase{ L"Flags=15 (Disambiguate+EventTypes+AltKeys+AllKeys) Esc key press", L"\x1b[27;1:1u", 15, true, VK_ESCAPE, 0x01, 0, 0 }, + TestCase{ L"Flags=15 (Disambiguate+EventTypes+AltKeys+AllKeys) Esc key press", L"\x1b[27u", 15, true, VK_ESCAPE, 0x01, 0, 0 }, // flags=16 (0b10000): ReportAssociatedText only (meaningless without AllKeys) - TestCase{ L"Flags=16 (AssocText) Esc key", L"\x1b", 16, true, VK_ESCAPE, 0x01, 0, 0 }, + TestCase{ L"Flags=16 (AssocText) Esc key", L"\x1b", 16, true, VK_ESCAPE, 0x01, L'\x1b', 0 }, // flags=17 (0b10001): Disambiguate + AssocText TestCase{ L"Flags=17 (Disambiguate+AssocText) Esc key", L"\x1b[27u", 17, true, VK_ESCAPE, 0x01, 0, 0 }, // flags=18 (0b10010): EventTypes + AssocText - TestCase{ L"Flags=18 (EventTypes+AssocText) Esc key", L"\x1b", 18, true, VK_ESCAPE, 0x01, 0, 0 }, + TestCase{ L"Flags=18 (EventTypes+AssocText) Esc key", L"\x1b", 18, true, VK_ESCAPE, 0x01, L'\x1b', 0 }, // flags=19 (0b10011): Disambiguate + EventTypes + AssocText - TestCase{ L"Flags=19 (Disambiguate+EventTypes+AssocText) Esc key press", L"\x1b[27;1:1u", 19, true, VK_ESCAPE, 0x01, 0, 0 }, + TestCase{ L"Flags=19 (Disambiguate+EventTypes+AssocText) Esc key press", L"\x1b[27u", 19, true, VK_ESCAPE, 0x01, 0, 0 }, // flags=20 (0b10100): AltKeys + AssocText - TestCase{ L"Flags=20 (AltKeys+AssocText) Esc key", L"\x1b", 20, true, VK_ESCAPE, 0x01, 0, 0 }, + TestCase{ L"Flags=20 (AltKeys+AssocText) Esc key", L"\x1b", 20, true, VK_ESCAPE, 0x01, L'\x1b', 0 }, // flags=21 (0b10101): Disambiguate + AltKeys + AssocText TestCase{ L"Flags=21 (Disambiguate+AltKeys+AssocText) Esc key", L"\x1b[27u", 21, true, VK_ESCAPE, 0x01, 0, 0 }, // flags=22 (0b10110): EventTypes + AltKeys + AssocText - TestCase{ L"Flags=22 (EventTypes+AltKeys+AssocText) Esc key", L"\x1b", 22, true, VK_ESCAPE, 0x01, 0, 0 }, + TestCase{ L"Flags=22 (EventTypes+AltKeys+AssocText) Esc key", L"\x1b", 22, true, VK_ESCAPE, 0x01, L'\x1b', 0 }, // flags=23 (0b10111): Disambiguate + EventTypes + AltKeys + AssocText - TestCase{ L"Flags=23 (Disambiguate+EventTypes+AltKeys+AssocText) Esc key press", L"\x1b[27;1:1u", 23, true, VK_ESCAPE, 0x01, 0, 0 }, + TestCase{ L"Flags=23 (Disambiguate+EventTypes+AltKeys+AssocText) Esc key press", L"\x1b[27u", 23, true, VK_ESCAPE, 0x01, 0, 0 }, // flags=24 (0b11000): AllKeys + AssocText // 'a' key with text reporting: CSI 97;;97 u - TestCase{ L"Flags=24 (AllKeys+AssocText) 'a' key", L"\x1b[97;1;97u", 24, true, 'A', 0x1E, L'a', 0 }, + TestCase{ L"Flags=24 (AllKeys+AssocText) 'a' key", L"\x1b[97;;97u", 24, true, 'A', 0x1E, L'a', 0 }, // flags=25 (0b11001): Disambiguate + AllKeys + AssocText - TestCase{ L"Flags=25 (Disambiguate+AllKeys+AssocText) 'a' key", L"\x1b[97;1;97u", 25, true, 'A', 0x1E, L'a', 0 }, + TestCase{ L"Flags=25 (Disambiguate+AllKeys+AssocText) 'a' key", L"\x1b[97;;97u", 25, true, 'A', 0x1E, L'a', 0 }, // flags=26 (0b11010): EventTypes + AllKeys + AssocText - TestCase{ L"Flags=26 (EventTypes+AllKeys+AssocText) 'a' key press", L"\x1b[97;1:1;97u", 26, true, 'A', 0x1E, L'a', 0 }, + TestCase{ L"Flags=26 (EventTypes+AllKeys+AssocText) 'a' key press", L"\x1b[97;;97u", 26, true, 'A', 0x1E, L'a', 0 }, // flags=27 (0b11011): Disambiguate + EventTypes + AllKeys + AssocText - TestCase{ L"Flags=27 (Disambiguate+EventTypes+AllKeys+AssocText) 'a' key press", L"\x1b[97;1:1;97u", 27, true, 'A', 0x1E, L'a', 0 }, + TestCase{ L"Flags=27 (Disambiguate+EventTypes+AllKeys+AssocText) 'a' key press", L"\x1b[97;;97u", 27, true, 'A', 0x1E, L'a', 0 }, // flags=28 (0b11100): AltKeys + AllKeys + AssocText - TestCase{ L"Flags=28 (AltKeys+AllKeys+AssocText) 'a' key", L"\x1b[97;1;97u", 28, true, 'A', 0x1E, L'a', 0 }, + TestCase{ L"Flags=28 (AltKeys+AllKeys+AssocText) 'a' key", L"\x1b[97;;97u", 28, true, 'A', 0x1E, L'a', 0 }, // flags=29 (0b11101): Disambiguate + AltKeys + AllKeys + AssocText - TestCase{ L"Flags=29 (Disambiguate+AltKeys+AllKeys+AssocText) 'a' key", L"\x1b[97;1;97u", 29, true, 'A', 0x1E, L'a', 0 }, + TestCase{ L"Flags=29 (Disambiguate+AltKeys+AllKeys+AssocText) 'a' key", L"\x1b[97;;97u", 29, true, 'A', 0x1E, L'a', 0 }, // flags=30 (0b11110): EventTypes + AltKeys + AllKeys + AssocText - TestCase{ L"Flags=30 (EventTypes+AltKeys+AllKeys+AssocText) 'a' key press", L"\x1b[97;1:1;97u", 30, true, 'A', 0x1E, L'a', 0 }, + TestCase{ L"Flags=30 (EventTypes+AltKeys+AllKeys+AssocText) 'a' key press", L"\x1b[97;;97u", 30, true, 'A', 0x1E, L'a', 0 }, // flags=31 (0b11111): All flags enabled - TestCase{ L"Flags=31 (all) 'a' key press", L"\x1b[97;1:1;97u", 31, true, 'A', 0x1E, L'a', 0 }, + TestCase{ L"Flags=31 (all) 'a' key press", L"\x1b[97;;97u", 31, true, 'A', 0x1E, L'a', 0 }, // ==================================================================== // SECTION 2: Modifier Combinations with Disambiguate (flag=1) @@ -297,13 +297,13 @@ namespace // ==================================================================== // Key press with Disambiguate+EventTypes (flag=3) - TestCase{ L"EventTypes: Esc press", L"\x1b[27;1:1u", 3, true, VK_ESCAPE, 0x01, 0, 0 }, + TestCase{ L"EventTypes: Esc press", L"\x1b[27u", 3, true, VK_ESCAPE, 0x01, 0, 0 }, // Key release with Disambiguate+EventTypes (flag=3) TestCase{ L"EventTypes: Esc release", L"\x1b[27;1:3u", 3, false, VK_ESCAPE, 0x01, 0, 0 }, // Key press with AllKeys+EventTypes (flag=10) - TestCase{ L"EventTypes+AllKeys: 'a' press", L"\x1b[97;1:1u", 10, true, 'A', 0x1E, L'a', 0 }, + TestCase{ L"EventTypes+AllKeys: 'a' press", L"\x1b[97u", 10, true, 'A', 0x1E, L'a', 0 }, // Key release with AllKeys+EventTypes (flag=10) TestCase{ L"EventTypes+AllKeys: 'a' release", L"\x1b[97;1:3u", 10, false, 'A', 0x1E, L'a', 0 }, @@ -314,8 +314,8 @@ namespace TestCase{ L"EventTypes+AllKeys: Tab release", L"\x1b[9;1:3u", 10, false, VK_TAB, 0x0F, L'\t', 0 }, TestCase{ L"EventTypes+AllKeys: Backspace release", L"\x1b[127;1:3u", 10, false, VK_BACK, 0x0E, L'\b', 0 }, - // Press with modifier: Shift+Esc -> CSI 27;2:1 u - TestCase{ L"EventTypes: Shift+Esc press", L"\x1b[27;2:1u", 3, true, VK_ESCAPE, 0x01, 0, SHIFT_PRESSED }, + // Press with modifier: Shift+Esc -> CSI 27;2 u + TestCase{ L"EventTypes: Shift+Esc press", L"\x1b[27;2u", 3, true, VK_ESCAPE, 0x01, 0, SHIFT_PRESSED }, // Release with modifier: Shift+Esc -> CSI 27;2:3 u TestCase{ L"EventTypes: Shift+Esc release", L"\x1b[27;2:3u", 3, false, VK_ESCAPE, 0x01, 0, SHIFT_PRESSED }, @@ -515,11 +515,11 @@ namespace // AllKeys + EventTypes + CapsLock: 'a' press with CapsLock // mod=1+64=65, event=press=1 - TestCase{ L"AllKeys+EventTypes: CapsLock+a press", L"\x1b[97;65:1u", 10, true, 'A', 0x1E, L'A', CAPSLOCK_ON }, + TestCase{ L"AllKeys+EventTypes: CapsLock+a press", L"\x1b[97;65u", 10, true, 'A', 0x1E, L'A', CAPSLOCK_ON }, // AllKeys + EventTypes + all modifiers: press // mod=1+1+2+4+64+128=200, event=1 - TestCase{ L"AllKeys+EventTypes: all mods press", L"\x1b[97;200:1u", 10, true, 'A', 0x1E, L'\x01', SHIFT_PRESSED | ALT_PRESSED | CTRL_PRESSED | CAPSLOCK_ON | NUMLOCK_ON }, + TestCase{ L"AllKeys+EventTypes: all mods press", L"\x1b[97;200u", 10, true, 'A', 0x1E, L'\x01', SHIFT_PRESSED | ALT_PRESSED | CTRL_PRESSED | CAPSLOCK_ON | NUMLOCK_ON }, // AllKeys + EventTypes + all modifiers: release TestCase{ L"AllKeys+EventTypes: all mods release", L"\x1b[97;200:3u", 10, false, 'A', 0x1E, L'\x01', SHIFT_PRESSED | ALT_PRESSED | CTRL_PRESSED | CAPSLOCK_ON | NUMLOCK_ON }, @@ -538,7 +538,7 @@ namespace // Ctrl+a produces control character (0x01), which should not be in text // Text field should be omitted for control codes - TestCase{ L"AllKeys+AssocText: Ctrl+a (no text)", L"\x1b[97;5;1u", 24, true, 'A', 0x1E, L'\x01', CTRL_PRESSED }, + TestCase{ L"AllKeys+AssocText: Ctrl+a (no text)", L"\x1b[97;5u", 24, true, 'A', 0x1E, L'\x01', CTRL_PRESSED }, // ==================================================================== // SECTION 16: Edge cases @@ -586,7 +586,8 @@ class KittyKeyboardProtocolTests auto input = createInput(tc.flags); const auto expected = TerminalInput::MakeOutput(tc.expected); const auto actual = process(input, tc.keyDown, tc.vk, tc.sc, tc.ch, tc.state); - VERIFY_ARE_EQUAL(expected, actual); + const auto msg = fmt::format(L"{} != {}", til::visualize_control_codes(expected.value_or({})), til::visualize_control_codes(actual.value_or({}))); + VERIFY_ARE_EQUAL(expected, actual, msg.c_str()); } // Repeat events require stateful testing - the same key must be pressed twice @@ -601,7 +602,7 @@ class KittyKeyboardProtocolTests // First press -> event type 1 (press) auto result1 = process(input, true, 'A', 0x1E, L'a', 0); - auto expected1 = TerminalInput::MakeOutput(L"\x1b[97;1:1u"); + auto expected1 = TerminalInput::MakeOutput(L"\x1b[97u"); VERIFY_ARE_EQUAL(expected1, result1, L"First press should be event type 1"); // Second press (same key, no release) -> event type 2 (repeat) diff --git a/src/terminal/input/terminalInput.cpp b/src/terminal/input/terminalInput.cpp index 6e056128d6..a00c5ddb0a 100644 --- a/src/terminal/input/terminalInput.cpp +++ b/src/terminal/input/terminalInput.cpp @@ -353,52 +353,32 @@ TerminalInput::OutputType TerminalInput::HandleKey(const INPUT_RECORD& event) WI_SetFlagIf(simpleKeyState, SKS_SHIFT, shiftIsPressed); WI_SetFlagIf(simpleKeyState, SKS_ENHANCED, enhanced); - const auto functionalKeyCode = _getKittyFunctionalKeyCode(keyEvent, simpleKeyState); - KeyEncodingInfo info; + const auto functionalKeyCode = _getKittyFunctionalKeyCode(keyEvent.wVirtualKeyCode, keyEvent.wVirtualScanCode, simpleKeyState); + EncodingHelper enc; - // Get the codepoint that would be generated without modifiers. + // However, we first need to query the key with the original state, to check + // whether it's a dead key. If that is the case, ToUnicodeEx should return a + // negative number, although in practice it's more likely to return a string + // of length two, with two identical characters. This is because the system + // sees this as a second press of the dead key, which would typically result + // in the combining character representation being transmitted twice. // // _getKittyFunctionalKeyCode() only returns non-zero for non-text keys. // So, using !functionalKeyCode we filter down to (possible) text keys. - // // We can also further filter down to only key-combinations with modifier, // since no modifiers means that we can just use the codepoint as is. - auto codepointWithoutModifiers = codepoint; if (!functionalKeyCode && (simpleKeyState & (SKS_CTRL | SKS_ALT)) != 0) { - // We need the current keyboard layout and state to look up the character - // that would be transmitted in that state (via the ToUnicodeEx API). - const auto hkl = GetKeyboardLayout(GetWindowThreadProcessId(GetForegroundWindow(), nullptr)); - auto keyState = _getKeyboardState(virtualKeyCode, controlKeyState); - constexpr UINT flags = 4; // Don't modify the state in the ToUnicodeEx call. - constexpr int bufferSize = 4; - wchar_t buffer[bufferSize]; - - // However, we first need to query the key with the original state, to check - // whether it's a dead key. If that is the case, ToUnicodeEx should return a - // negative number, although in practice it's more likely to return a string - // of length two, with two identical characters. This is because the system - // sees this as a second press of the dead key, which would typically result - // in the combining character representation being transmitted twice. - auto length = ToUnicodeEx(virtualKeyCode, 0, keyState.data(), buffer, bufferSize, flags, hkl); - if (length < 0 || (length == 2 && buffer[0] == buffer[1])) + const auto hkl = enc.getKeyboardLayoutCached(); + const auto cb = enc.getKeyboardKey(virtualKeyCode, controlKeyState, hkl); + if (cb.len < 0 || (cb.len == 2 && cb.buf[0] == cb.buf[1])) { return _makeNoOutput(); } - - // Once we know it's not a dead key, we run the query again, but with the - // Ctrl and Alt modifiers disabled to obtain the base character mapping. - keyState.at(VK_CONTROL) = keyState.at(VK_LCONTROL) = keyState.at(VK_RCONTROL) = 0; - keyState.at(VK_MENU) = keyState.at(VK_LMENU) = keyState.at(VK_RMENU) = 0; - length = ToUnicodeEx(virtualKeyCode, 0, keyState.data(), buffer, bufferSize, flags, hkl); - if (length == 1 || length == 2) - { - codepointWithoutModifiers = _bufferToCodepoint(buffer); - } } uint32_t kittyKeyCode = 0; - uint32_t kittyEventType = 1; + uint32_t kittyEventType = 0; uint32_t kittyAltKeyCodeShifted = 0; uint32_t kittyAltKeyCodeBase = 0; uint32_t kittyTextAsCodepoint = 0; @@ -411,22 +391,18 @@ TerminalInput::OutputType TerminalInput::HandleKey(const INPUT_RECORD& event) // KKP> report the Esc, alt+key, ctrl+key, ctrl+alt+key, shift+alt+key // KKP> keys using CSI u sequences instead of legacy ones. // KKP> Here key is any ASCII key as described in Legacy text keys. [...] + // KKP> Additionally, all non text keypad keys will be reported [...] with CSI u encoding, [...]. // // NOTE: The specification fails to mention ctrl+shift+key, but Kitty does handle it. // So really, it's actually "any modifiers, except for shift+key". - - // KKP> Legacy text keys: - // KKP> For legacy compatibility, the keys a-z 0-9 ` - = [ ] \ ; ' , . / [...] // // NOTE: The list of legacy keys doesn't really make any sense. // My interpretation is that the author meant all printable keys, // because as before, that's also how Kitty handles it. + // + // NOTE: We'll handle modifier+key combinations below, together with ReportAllKeysAsEscapeCodes. - // KKP> Additionally, all non text keypad keys will be reported [...] with CSI u encoding, [...]. - - if (virtualKeyCode == VK_ESCAPE || - (virtualKeyCode >= VK_NUMPAD0 && virtualKeyCode <= VK_DIVIDE) || - ((simpleKeyState & ~SKS_ENHANCED) > SKS_SHIFT && _codepointIsText(codepointWithoutModifiers))) + if (virtualKeyCode == VK_ESCAPE || (virtualKeyCode >= VK_NUMPAD0 && virtualKeyCode <= VK_DIVIDE)) { kittyKeyCode = functionalKeyCode <= KittyKeyCodeLegacySentinel ? 0 : functionalKeyCode; } @@ -445,22 +421,36 @@ TerminalInput::OutputType TerminalInput::HandleKey(const INPUT_RECORD& event) } } - // Enabling ReportEventTypes implies that `CSI u` is used for all key up events. - if (WI_IsFlagSet(_kittyFlags, KittyKeyboardProtocolFlags::ReportAllKeysAsEscapeCodes) || kittyEventType == 3) - { + if ( // KKP> This [...] turns on key reporting even for key events that generate text. // KKP> [...] text will not be sent, instead only key events are sent. - // // KKP> [...] with this mode, events for pressing modifier keys are reported. // // In other words: Get the functional key code if any, otherwise use the codepoint. - + WI_IsFlagSet(_kittyFlags, KittyKeyboardProtocolFlags::ReportAllKeysAsEscapeCodes) || + // A continuation of DisambiguateEscapeCodes above: modifier + text-key = CSI u. + (WI_IsFlagSet(_kittyFlags, KittyKeyboardProtocolFlags::DisambiguateEscapeCodes) && + functionalKeyCode <= KittyKeyCodeLegacySentinel && // Implies that it's a possibly a text-key + (simpleKeyState & ~SKS_ENHANCED) > SKS_SHIFT) || + // Enabling ReportEventTypes implies that `CSI u` is used for all key up events. + kittyEventType == 3) + { kittyKeyCode = functionalKeyCode <= KittyKeyCodeLegacySentinel ? 0 : functionalKeyCode; - if (kittyKeyCode == 0 && _codepointIsText(codepointWithoutModifiers)) + if (kittyKeyCode == 0) { // KKP> Note that the codepoint used is always the lower-case [...] version of the key. - kittyKeyCode = _codepointToLower(codepointWithoutModifiers); + // + // In other words, we want the "base key" of the current layout, + // effectively the key without Ctrl/Alt/Shift modifiers. + const auto hkl = enc.getKeyboardLayoutCached(); + const auto cb = enc.getKeyboardKey(virtualKeyCode, controlKeyState & ~(ALT_PRESSED | CTRL_PRESSED | SHIFT_PRESSED | CAPSLOCK_ON), hkl); + const auto cp = _bufferToCodepoint(cb.buf); + + if (_codepointIsText(cp)) + { + kittyKeyCode = cp; + } } if (WI_IsFlagSet(_kittyFlags, KittyKeyboardProtocolFlags::ReportAssociatedText)) @@ -497,44 +487,76 @@ TerminalInput::OutputType TerminalInput::HandleKey(const INPUT_RECORD& event) // KKP> Note that the shifted key must be present only if shift is also present in the modifiers. - if ((simpleKeyState & SKS_SHIFT) != 0 && _codepointIsText(codepoint)) + if ((simpleKeyState & SKS_SHIFT) != 0) { - // I'm assuming that codepoint is already the shifted version if shift is pressed. - kittyAltKeyCodeShifted = codepoint; + // This is almost identical to our computation of the "base key" for + // ReportAllKeysAsEscapeCodes above, but this time with SHIFT_PRESSED. + const auto hkl = enc.getKeyboardLayoutCached(); + const auto cb = enc.getKeyboardKey(virtualKeyCode, controlKeyState & ~(ALT_PRESSED | CTRL_PRESSED | CAPSLOCK_ON) | SHIFT_PRESSED, hkl); + const auto cp = _bufferToCodepoint(cb.buf); + + // The specification doesn't state whether the shifted key should be reported if it's identical + // to the unshifted version, so I don't (this matches the behavior for base layout key below). + if (_codepointIsText(cp) && cp != kittyKeyCode) + { + kittyAltKeyCodeShifted = cp; + } } - kittyAltKeyCodeBase = _getBaseLayoutCodepoint(virtualKeyCode); + if (keyEvent.wVirtualScanCode) + { + // > The base layout key is the key corresponding to the physical key in the standard PC-101 key layout. + static const auto usLayout = LoadKeyboardLayoutW(L"00000409", 0); + if (usLayout) + { + const auto vkey = MapVirtualKeyExW(keyEvent.wVirtualScanCode, MAPVK_VSC_TO_VK_EX, usLayout); + if (vkey) + { + kittyAltKeyCodeBase = _getKittyFunctionalKeyCode(vkey, keyEvent.wVirtualScanCode, simpleKeyState); + + if (kittyAltKeyCodeBase <= KittyKeyCodeLegacySentinel) + { + const auto cb = enc.getKeyboardKey(virtualKeyCode, controlKeyState & ~(ALT_PRESSED | CTRL_PRESSED | SHIFT_PRESSED | CAPSLOCK_ON), usLayout); + kittyAltKeyCodeBase = _bufferToCodepoint(cb.buf); + if (!_codepointIsText(kittyAltKeyCodeBase)) + { + kittyAltKeyCodeShifted = 0; + } + } + + if (kittyAltKeyCodeBase == kittyKeyCode) + { + kittyAltKeyCodeBase = 0; + } + } + } + } } } } if (kittyKeyCode) { - info.csiFinal = L'u'; - info.csiParam[0][0] = kittyKeyCode; - info.csiParam[0][1] = kittyAltKeyCodeShifted; - info.csiParam[0][2] = kittyAltKeyCodeBase; - info.csiParam[1][1] = kittyEventType; - info.csiParam[2][0] = kittyTextAsCodepoint; + enc.csiFinal = L'u'; + enc.csiParam[0][0] = kittyKeyCode; + enc.csiParam[0][1] = kittyAltKeyCodeShifted; + enc.csiParam[0][2] = kittyAltKeyCodeBase; + // enc.csiParam[1][0] contains the CSI modifier value, which gets calculated below. + enc.csiParam[1][1] = kittyEventType; + enc.csiParam[2][0] = kittyTextAsCodepoint; } else { - _fillRegularKeyEncodingInfo(info, keyEvent, simpleKeyState); + _fillRegularKeyEncodingInfo(enc, keyEvent, simpleKeyState); } std::wstring seq; - if (info.csiFinal) + if (enc.csiFinal) { - // Kitty: - // CSI unicode-key-code:alternate-key-code-shift:alternate-key-code-base ; modifiers:event-type ; text-as-codepoint u - // Regular: - // CSI final - // CSI 1; modifiers final - { // As per KKP: shift=1, alt=2, ctrl=4, super=8, hyper=16, meta=32, caps_lock=64, num_lock=128 - info.csiParam[1][0] = simpleKeyState & (SKS_SHIFT | SKS_ALT | SKS_CTRL); + enc.csiParam[1][0] = simpleKeyState & (SKS_SHIFT | SKS_ALT | SKS_CTRL); // KKP> Lock modifiers are not reported for text producing keys, [...]. // KKP> To get lock modifiers for all keys use the Report all keys as escape codes enhancement. @@ -543,23 +565,23 @@ TerminalInput::OutputType TerminalInput::HandleKey(const INPUT_RECORD& event) { if (WI_IsFlagSet(controlKeyState, CAPSLOCK_ON)) { - info.csiParam[1][0] |= 64; + enc.csiParam[1][0] |= 64; } if (WI_IsFlagSet(controlKeyState, NUMLOCK_ON)) { - info.csiParam[1][0] |= 128; + enc.csiParam[1][0] |= 128; } } - if (info.csiParam[1][0] != 0) + if (enc.csiParam[1][0] != 0) { // NOTE: The CSI modifier value is 1 based. - info.csiParam[1][0] += 1; + enc.csiParam[1][0] += 1; } } - constexpr size_t maxParamCount = std::size(info.csiParam); - constexpr size_t maxSubParamCount = std::size(info.csiParam[0]); + constexpr size_t maxParamCount = std::size(enc.csiParam); + constexpr size_t maxSubParamCount = std::size(enc.csiParam[0]); // If any sub-parameter of a parameter is set, // the first sub-parameter must be at least 1. @@ -568,25 +590,22 @@ TerminalInput::OutputType TerminalInput::HandleKey(const INPUT_RECORD& event) uint32_t bits = 0; for (size_t s = 1; s < maxSubParamCount; s++) { - bits |= info.csiParam[p][s]; + bits |= enc.csiParam[p][s]; } if (bits != 0) { - auto& dst = info.csiParam[p][0]; + auto& dst = enc.csiParam[p][0]; dst = std::max(dst, 1); } } - // Turn `CSI ; ; a` into `CSI 1 ; 1 ; a` by backfilling preceding params with 1. - // This loop also doubles as a way to count how many parameters we need to format. - size_t parameterCount = 0; - for (size_t p = std::size(info.csiParam); p != 0; p--) + size_t paramMaxIdx = 0; + for (size_t p = maxParamCount - 1; p != 0; p--) { - if (info.csiParam[p][0] != 0) + if (enc.csiParam[p][0] != 0) { - auto& dst = info.csiParam[p - 1][0]; - dst = std::max(dst, 1); - parameterCount++; + paramMaxIdx = p; + break; } } @@ -594,7 +613,7 @@ TerminalInput::OutputType TerminalInput::HandleKey(const INPUT_RECORD& event) // Format the parameters, skipping any trailing empty parameters entirely. // Empty sub-parameters are omitted, while the colons are kept. - for (size_t p = 0; p < parameterCount; p++) + for (size_t p = 0; p <= paramMaxIdx; p++) { if (p > 0) { @@ -602,43 +621,43 @@ TerminalInput::OutputType TerminalInput::HandleKey(const INPUT_RECORD& event) } // Find the last non-zero sub-parameter in this parameter. - size_t subCount = 0; - for (size_t s = 0; s < maxSubParamCount; s++) + size_t subMaxIdx = 0; + for (size_t s = maxSubParamCount - 1; s != 0; s--) { - if (info.csiParam[p][s] != 0) + if (enc.csiParam[p][s] != 0) { - subCount = s; + subMaxIdx = s; + break; } } - bool firstInParam = true; - for (size_t s = 0; s < subCount; s++) + for (size_t s = 0; s <= subMaxIdx; s++) { if (s > 0) { seq.push_back(L':'); } - if (const auto v = info.csiParam[p][s]) + if (const auto v = enc.csiParam[p][s]) { fmt::format_to(std::back_inserter(seq), FMT_COMPILE(L"{}"), v); } } } - seq.push_back(info.csiFinal); + seq.push_back(enc.csiFinal); } - else if (info.ss3Final) + else if (enc.ss3Final) { seq.append(L"\033O"); - seq.push_back(info.ss3Final); + seq.push_back(enc.ss3Final); } - else if (!info.plain.empty()) + else if (!enc.plain.empty()) { - if (info.plainAltPrefix && (simpleKeyState & SKS_ALT) && _inputMode.test(Mode::Ansi)) + if (enc.plainAltPrefix && (simpleKeyState & SKS_ALT) && _inputMode.test(Mode::Ansi)) { seq.push_back(L'\x1b'); } - seq.append(info.plain); + seq.append(enc.plain); } // If this is a modifier, it won't produce output. else if (virtualKeyCode < VK_SHIFT || virtualKeyCode > VK_MENU) @@ -684,10 +703,14 @@ TerminalInput::OutputType TerminalInput::HandleKey(const INPUT_RECORD& event) seq.push_back(L'\x1b'); } + const auto hkl = enc.getKeyboardLayoutCached(); + const auto cb = enc.getKeyboardKey(virtualKeyCode, controlKeyState & ~(ALT_PRESSED | CTRL_PRESSED), hkl); + auto cp = _bufferToCodepoint(cb.buf); + // Once we've got the base character, we can apply the Ctrl modifier. if (ctrlIsReallyPressed) { - auto cp = _makeCtrlChar(codepointWithoutModifiers); + cp = _makeCtrlChar(cp); // If we haven't found a Ctrl mapping for the key, and it's one of // the alphanumeric keys, we try again using the virtual key code. // On keyboard layouts where the alphanumeric keys are not mapped to @@ -696,12 +719,9 @@ TerminalInput::OutputType TerminalInput::HandleKey(const INPUT_RECORD& event) { cp = _makeCtrlChar(virtualKeyCode); } - _stringPushCodepoint(seq, cp); - } - else - { - _stringPushCodepoint(seq, codepointWithoutModifiers); } + + _stringPushCodepoint(seq, cp); } } @@ -761,7 +781,7 @@ DWORD TerminalInput::_trackControlKeyState(const KEY_EVENT_RECORD& key) noexcept // recent key press and associated control key state (which is all we need for // our ToUnicodeEx queries). This is a substitute for the GetKeyboardState API, // which can't be used when serving as a conpty host. -std::array TerminalInput::_getKeyboardState(const WORD virtualKeyCode, const DWORD controlKeyState) +std::array TerminalInput::_getKeyboardState(const size_t virtualKeyCode, const DWORD controlKeyState) { auto keyState = std::array{}; if (virtualKeyCode < keyState.size()) @@ -812,7 +832,7 @@ uint32_t TerminalInput::_makeCtrlChar(const uint32_t ch) noexcept TerminalInput::StringType TerminalInput::_makeCharOutput(const uint32_t cp) { const auto buf = _codepointToBuffer(cp); - return { buf.buf, buf.len }; + return { buf.buf, gsl::narrow_cast(buf.len) }; } TerminalInput::StringType TerminalInput::_makeNoOutput() noexcept @@ -820,16 +840,6 @@ TerminalInput::StringType TerminalInput::_makeNoOutput() noexcept return {}; } -// Sends the given char as a sequence representing Alt+char, also the same as Meta+char. -void TerminalInput::_escapeOutput(StringType& charSequence, const bool altIsPressed) const -{ - // Alt+char combinations are only applicable in ANSI mode. - if (altIsPressed && _inputMode.test(Mode::Ansi)) - { - charSequence.insert(0, 1, L'\x1b'); - } -} - // Turns an KEY_EVENT_RECORD into a win32-input-mode VT sequence. // It allows us to send KEY_EVENT_RECORD data losslessly to conhost. TerminalInput::OutputType TerminalInput::_makeWin32Output(const KEY_EVENT_RECORD& key) const @@ -857,7 +867,7 @@ TerminalInput::OutputType TerminalInput::_makeWin32Output(const KEY_EVENT_RECORD return fmt::format(FMT_COMPILE(L"{}{};{};{};{};{};{}_"), _csi, vk, sc, uc, kd, cs, rc); } -void TerminalInput::_fillRegularKeyEncodingInfo(KeyEncodingInfo& info, const KEY_EVENT_RECORD& key, DWORD simpleKeyState) const noexcept +void TerminalInput::_fillRegularKeyEncodingInfo(EncodingHelper& enc, const KEY_EVENT_RECORD& key, DWORD simpleKeyState) const noexcept { const auto virtualKeyCode = key.wVirtualKeyCode; const auto modified = (simpleKeyState & ~SKS_ENHANCED) != 0; @@ -875,14 +885,14 @@ void TerminalInput::_fillRegularKeyEncodingInfo(KeyEncodingInfo& info, const KEY // not standard, but a modern terminal convention). The Alt modifier adds // an ESC prefix (also not standard). case VK_BACK: - info.plainAltPrefix = true; + enc.plainAltPrefix = true; switch (simpleKeyState & ~(SKS_ALT | SKS_SHIFT)) { default: - info.plain = _inputMode.test(Mode::BackarrowKey) ? L"\b"sv : L"\x7F"sv; + enc.plain = _inputMode.test(Mode::BackarrowKey) ? L"\b"sv : L"\x7F"sv; break; case SKS_CTRL: - info.plain = _inputMode.test(Mode::BackarrowKey) ? L"\x7f"sv : L"\b"sv; + enc.plain = _inputMode.test(Mode::BackarrowKey) ? L"\x7f"sv : L"\b"sv; break; } break; @@ -890,14 +900,14 @@ void TerminalInput::_fillRegularKeyEncodingInfo(KeyEncodingInfo& info, const KEY // The Alt modifier adds an ESC prefix, although in practice all the Alt // mappings are likely to be system hotkeys. case VK_TAB: - info.plainAltPrefix = true; + enc.plainAltPrefix = true; switch (simpleKeyState & ~(SKS_ALT | SKS_CTRL)) { default: - info.plain = L"\t"sv; + enc.plain = L"\t"sv; break; case SKS_SHIFT: - info.csiFinal = L'Z'; + enc.csiFinal = L'Z'; break; } break; @@ -910,23 +920,23 @@ void TerminalInput::_fillRegularKeyEncodingInfo(KeyEncodingInfo& info, const KEY { if (_inputMode.test(Mode::Ansi)) { - info.ss3Final = L'M'; + enc.ss3Final = L'M'; } else { - info.plain = L"\033?M"sv; + enc.plain = L"\033?M"sv; } } else { - info.plainAltPrefix = true; + enc.plainAltPrefix = true; switch (simpleKeyState & ~(SKS_ALT | SKS_SHIFT | SKS_ENHANCED)) { default: - info.plain = _inputMode.test(Mode::LineFeed) ? L"\r\n"sv : L"\r"sv; + enc.plain = _inputMode.test(Mode::LineFeed) ? L"\r\n"sv : L"\r"sv; break; case SKS_CTRL: - info.plain = L"\n"sv; + enc.plain = L"\n"sv; break; } } @@ -935,7 +945,7 @@ void TerminalInput::_fillRegularKeyEncodingInfo(KeyEncodingInfo& info, const KEY // PAUSE doesn't have a VT mapping, but traditionally we've mapped it to ^Z, // regardless of modifiers. case VK_PAUSE: - info.plain = L"\x1A"sv; + enc.plain = L"\x1A"sv; break; // F1 to F4 map to the VT keypad function keys, which are SS3 sequences. @@ -952,19 +962,19 @@ void TerminalInput::_fillRegularKeyEncodingInfo(KeyEncodingInfo& info, const KEY // KKP> be encoded as both CSI R and CSI ~ [and now it doesn't]. if (kitty && virtualKeyCode == VK_F3) { - info.csiFinal = L'~'; - info.csiParam[0][0] = 13; + enc.csiFinal = L'~'; + enc.csiParam[0][0] = 13; } else { - auto& dst = kitty || modified ? info.csiFinal : info.ss3Final; + auto& dst = kitty || modified ? enc.csiFinal : enc.ss3Final; dst = L'P' + (virtualKeyCode - VK_F1); } } else { static constexpr std::wstring_view lut[] = { L"\033P", L"\033Q", L"\033R", L"\033S" }; - info.plain = lut[virtualKeyCode - VK_F1]; + enc.plain = lut[virtualKeyCode - VK_F1]; } break; @@ -992,21 +1002,21 @@ void TerminalInput::_fillRegularKeyEncodingInfo(KeyEncodingInfo& info, const KEY if (_inputMode.test(Mode::Ansi)) { static constexpr uint8_t lut[] = { 15, 17, 18, 19, 20, 21, 23, 24, 25, 26, 28, 29, 31, 32, 33, 34 }; - info.csiFinal = L'~'; - info.csiParam[0][0] = lut[virtualKeyCode - VK_F5]; + enc.csiFinal = L'~'; + enc.csiParam[0][0] = lut[virtualKeyCode - VK_F5]; } else { switch (virtualKeyCode) { case VK_F11: - info.plain = L"\033"sv; + enc.plain = L"\033"sv; break; case VK_F12: - info.plain = L"\b"sv; + enc.plain = L"\b"sv; break; case VK_F13: - info.plain = L"\n"sv; + enc.plain = L"\n"sv; break; default: break; @@ -1027,49 +1037,49 @@ void TerminalInput::_fillRegularKeyEncodingInfo(KeyEncodingInfo& info, const KEY { static constexpr uint8_t lut[] = { 'D', 'A', 'C', 'B' }; const auto csi = kitty || modified || !_inputMode.test(Mode::CursorKey); - auto& dst = csi ? info.csiFinal : info.ss3Final; + auto& dst = csi ? enc.csiFinal : enc.ss3Final; dst = lut[virtualKeyCode - VK_LEFT]; } else { static constexpr std::wstring_view lut[] = { L"\033D", L"\033A", L"\033C", L"\033B" }; - info.plain = lut[virtualKeyCode - VK_LEFT]; + enc.plain = lut[virtualKeyCode - VK_LEFT]; } break; case VK_CLEAR: if (_inputMode.test(Mode::Ansi)) { const auto csi = kitty || modified || !_inputMode.test(Mode::CursorKey); - auto& dst = csi ? info.csiFinal : info.ss3Final; + auto& dst = csi ? enc.csiFinal : enc.ss3Final; dst = L'E'; } else { - info.plain = L"\033E"sv; + enc.plain = L"\033E"sv; } break; case VK_HOME: if (_inputMode.test(Mode::Ansi)) { const auto csi = kitty || modified || !_inputMode.test(Mode::CursorKey); - auto& dst = csi ? info.csiFinal : info.ss3Final; + auto& dst = csi ? enc.csiFinal : enc.ss3Final; dst = L'H'; } else { - info.plain = L"\033H"sv; + enc.plain = L"\033H"sv; } break; case VK_END: if (_inputMode.test(Mode::Ansi)) { const auto csi = kitty || modified || !_inputMode.test(Mode::CursorKey); - auto& dst = csi ? info.csiFinal : info.ss3Final; + auto& dst = csi ? enc.csiFinal : enc.ss3Final; dst = L'F'; } else { - info.plain = L"\033F"sv; + enc.plain = L"\033F"sv; } break; @@ -1080,16 +1090,16 @@ void TerminalInput::_fillRegularKeyEncodingInfo(KeyEncodingInfo& info, const KEY case VK_DELETE: // 0x2E = 3 if (_inputMode.test(Mode::Ansi)) { - info.csiFinal = L'~'; - info.csiParam[0][0] = 2 + (virtualKeyCode - VK_INSERT); + enc.csiFinal = L'~'; + enc.csiParam[0][0] = 2 + (virtualKeyCode - VK_INSERT); } break; case VK_PRIOR: // 0x21 = 5 case VK_NEXT: // 0x22 = 6 if (_inputMode.test(Mode::Ansi)) { - info.csiFinal = L'~'; - info.csiParam[0][0] = 5 + (virtualKeyCode - VK_PRIOR); + enc.csiFinal = L'~'; + enc.csiParam[0][0] = 5 + (virtualKeyCode - VK_PRIOR); } break; @@ -1112,12 +1122,12 @@ void TerminalInput::_fillRegularKeyEncodingInfo(KeyEncodingInfo& info, const KEY { if (_inputMode.test(Mode::Ansi)) { - info.ss3Final = L'p' + (virtualKeyCode - VK_NUMPAD0); + enc.ss3Final = L'p' + (virtualKeyCode - VK_NUMPAD0); } else { static constexpr std::wstring_view lut[] = { L"\033?p", L"\033?q", L"\033?r", L"\033?s", L"\033?t", L"\033?u", L"\033?v", L"\033?w", L"\033?x", L"\033?y" }; - info.plain = lut[virtualKeyCode - VK_NUMPAD0]; + enc.plain = lut[virtualKeyCode - VK_NUMPAD0]; } } break; @@ -1131,12 +1141,12 @@ void TerminalInput::_fillRegularKeyEncodingInfo(KeyEncodingInfo& info, const KEY { if (_inputMode.test(Mode::Ansi)) { - info.ss3Final = L'j' + (virtualKeyCode - VK_MULTIPLY); + enc.ss3Final = L'j' + (virtualKeyCode - VK_MULTIPLY); } else { static constexpr std::wstring_view lut[] = { L"\033?j", L"\033?k", L"\033?l", L"\033?m", L"\033?n", L"\033?o" }; - info.plain = lut[virtualKeyCode - VK_MULTIPLY]; + enc.plain = lut[virtualKeyCode - VK_MULTIPLY]; } } break; @@ -1146,7 +1156,7 @@ void TerminalInput::_fillRegularKeyEncodingInfo(KeyEncodingInfo& info, const KEY } } -uint32_t TerminalInput::_getKittyFunctionalKeyCode(const KEY_EVENT_RECORD& key, DWORD simpleKeyState) noexcept +uint32_t TerminalInput::_getKittyFunctionalKeyCode(UINT vkey, WORD scanCode, DWORD simpleKeyState) noexcept { // Most KKP functional keys are rather predictable. This LUT helps cover most of them. // Some keys however depend on the key state (specifically the enhanced bit). @@ -1242,9 +1252,8 @@ uint32_t TerminalInput::_getKittyFunctionalKeyCode(const KEY_EVENT_RECORD& key, }(); const auto enhanced = (simpleKeyState & SKS_ENHANCED) != 0; - auto virtualKeyCode = key.wVirtualKeyCode; - switch (virtualKeyCode) + switch (vkey) { // These keys don't fall into the Private Use Area (ugh). case VK_ESCAPE: @@ -1278,20 +1287,20 @@ uint32_t TerminalInput::_getKittyFunctionalKeyCode(const KEY_EVENT_RECORD& key, case VK_SHIFT: // I've extracted the scan codes from all keyboard layouts that ship with Windows, // and I've found that all of them use scan code 0x2A for VK_LSHIFT and 0x36 for VK_RSHIFT. - virtualKeyCode = key.wVirtualScanCode == 0x36 ? VK_RSHIFT : VK_LSHIFT; + vkey = scanCode == 0x36 ? VK_RSHIFT : VK_LSHIFT; break; case VK_CONTROL: - virtualKeyCode = enhanced ? VK_RCONTROL : VK_LCONTROL; + vkey = enhanced ? VK_RCONTROL : VK_LCONTROL; break; case VK_MENU: - virtualKeyCode = enhanced ? VK_RMENU : VK_LMENU; + vkey = enhanced ? VK_RMENU : VK_LMENU; break; default: break; } - const auto v = lut[virtualKeyCode & 0xff]; + const auto v = lut[vkey & 0xff]; return v <= KittyKeyCodeLegacySentinel ? v : 0xE000 + v; } @@ -1363,19 +1372,29 @@ uint32_t TerminalInput::_bufferToLowerCodepoint(wchar_t* buf, int cap) noexcept return _bufferToCodepoint(buf); } -uint32_t TerminalInput::_getBaseLayoutCodepoint(const WORD vkey) noexcept +uint32_t TerminalInput::_getBaseLayoutCodepoint(const WORD scanCode) noexcept { + if (!scanCode) + { + return 0; + } + // > The base layout key is the key corresponding to the physical key in the standard PC-101 key layout. static const auto usLayout = LoadKeyboardLayoutW(L"00000409", 0); + if (!usLayout) + { + return 0; + } - if (!usLayout || !vkey) + const auto vkey = MapVirtualKeyExW(scanCode, MAPVK_VSC_TO_VK, usLayout); + if (!vkey) { return 0; } wchar_t baseChar[4]; const auto keyState = _getKeyboardState(vkey, 0); - const auto result = ToUnicodeEx(vkey, 0, keyState.data(), baseChar, 4, 4, usLayout); + const auto result = ToUnicodeEx(vkey, scanCode, keyState.data(), baseChar, 4, 4, usLayout); if (result == 0) { diff --git a/src/terminal/input/terminalInput.hpp b/src/terminal/input/terminalInput.hpp index 39c1b96e0e..29b69e0f3a 100644 --- a/src/terminal/input/terminalInput.hpp +++ b/src/terminal/input/terminalInput.hpp @@ -92,35 +92,97 @@ namespace Microsoft::Console::VirtualTerminal private: struct CodepointBuffer { - wchar_t buf[3]; - uint16_t len; + wchar_t buf[4]; + int len; }; - struct KeyEncodingInfo + struct EncodingHelper { - explicit KeyEncodingInfo() + explicit EncodingHelper() { memset(this, 0, sizeof(*this)); } + void disableCtrlAltInKeyboardState() noexcept + { + keyboardState[VK_CONTROL] = 0; + keyboardState[VK_MENU] = 0; + keyboardState[VK_LCONTROL] = 0; + keyboardState[VK_RCONTROL] = 0; + keyboardState[VK_LMENU] = 0; + keyboardState[VK_RMENU] = 0; + } + CodepointBuffer getKeyboardKey(UINT vkey, DWORD controlKeyState, HKL hkl) noexcept + { + CodepointBuffer cb; + + setupKeyboardState(controlKeyState); + + keyboardState[vkey] = 0x80; + cb.len = ToUnicodeEx(vkey, 0, keyboardState, cb.buf, ARRAYSIZE(cb.buf), 4, hkl); + keyboardState[vkey] = 0; + + return cb; + } + HKL getKeyboardLayoutCached() noexcept + { + if (!keyboardLayoutCached) + { + keyboardLayout = getKeyboardLayout(); + keyboardLayoutCached = true; + } + return keyboardLayout; + } + static HKL getKeyboardLayout() noexcept + { + // We need the current keyboard layout and state to look up the character + // that would be transmitted in that state (via the ToUnicodeEx API). + return GetKeyboardLayout(GetWindowThreadProcessId(GetForegroundWindow(), nullptr)); + } + void setupKeyboardState(DWORD controlKeyState) noexcept + { + const uint8_t rightAlt = WI_IsFlagSet(controlKeyState, RIGHT_ALT_PRESSED) ? 0x80 : 0; + const uint8_t leftAlt = WI_IsFlagSet(controlKeyState, LEFT_ALT_PRESSED) ? 0x80 : 0; + const uint8_t rightCtrl = WI_IsFlagSet(controlKeyState, RIGHT_CTRL_PRESSED) ? 0x80 : 0; + const uint8_t leftCtrl = WI_IsFlagSet(controlKeyState, LEFT_CTRL_PRESSED) ? 0x80 : 0; + const uint8_t leftShift = WI_IsFlagSet(controlKeyState, SHIFT_PRESSED) ? 0x80 : 0; + const uint8_t capsLock = WI_IsFlagSet(controlKeyState, CAPSLOCK_ON) ? 0x01 : 0; + + keyboardState[VK_SHIFT] = leftShift; + keyboardState[VK_CONTROL] = leftCtrl | rightCtrl; + keyboardState[VK_MENU] = leftAlt | rightAlt; + keyboardState[VK_CAPITAL] = capsLock; + keyboardState[VK_LSHIFT] = leftShift; + keyboardState[VK_LCONTROL] = leftCtrl; + keyboardState[VK_RCONTROL] = rightCtrl; + keyboardState[VK_LMENU] = leftAlt; + keyboardState[VK_RMENU] = rightAlt; + } + + HKL keyboardLayout; + uint8_t keyboardState[256]; + uint32_t codepointWithoutCtrlAlt; + + bool keyboardLayoutCached; + // A non-zero csiFinal value indicates that this key // should be encoded as `CSI $csiParam1 ; $csiFinal`. - wchar_t csiFinal = 0; + wchar_t csiFinal; // The longest sequence we currently have is Kitty's with 6 parameters: // CSI unicode-key-code:alternate-key-code-shift:alternate-key-code-base ; modifiers:event-type ; text-as-codepoint u // That's 6 parameters, but we can greatly simplify our logic if we just make it 3x3. - uint32_t csiParam[3][3] = {}; + uint32_t csiParam[3][3]; // A non-zero ss3Final value indicates that this key // should be encoded as `ESC O $ss3Final`. - wchar_t ss3Final = 0; + wchar_t ss3Final; // Any other encoding ends up as a non-zero plain value. // For instance, the Tab key gets translated to a plain "\t". std::wstring_view plain; // If true, and Alt is pressed, an ESC prefix should be added to // the final sequence. This only applies to non-KKP encodings. - bool plainAltPrefix = false; + bool plainAltPrefix; }; // storage location for the leading surrogate of a utf-16 surrogate pair @@ -150,15 +212,13 @@ namespace Microsoft::Console::VirtualTerminal void _initKeyboardMap() noexcept; DWORD _trackControlKeyState(const KEY_EVENT_RECORD& key) noexcept; - static std::array _getKeyboardState(WORD virtualKeyCode, DWORD controlKeyState); + static std::array _getKeyboardState(size_t virtualKeyCode, DWORD controlKeyState); [[nodiscard]] static uint32_t _makeCtrlChar(uint32_t ch) noexcept; [[nodiscard]] static StringType _makeCharOutput(uint32_t ch); [[nodiscard]] static StringType _makeNoOutput() noexcept; - void _escapeOutput(StringType& charSequence, bool altIsPressed) const; [[nodiscard]] OutputType _makeWin32Output(const KEY_EVENT_RECORD& key) const; - void _fillRegularKeyEncodingInfo(KeyEncodingInfo& info, const KEY_EVENT_RECORD& key, DWORD simpleKeyState) const noexcept; - static uint32_t _getKittyFunctionalKeyCode(const KEY_EVENT_RECORD& key, DWORD simpleKeyState) noexcept; - void _getKittyInfo() noexcept; + void _fillRegularKeyEncodingInfo(EncodingHelper& enc, const KEY_EVENT_RECORD& key, DWORD simpleKeyState) const noexcept; + static uint32_t _getKittyFunctionalKeyCode(UINT vkey, WORD scanCode, DWORD simpleKeyState) noexcept; std::vector& _getKittyStack() noexcept; static bool _codepointIsText(uint32_t cp) noexcept; static void _stringPushCodepoint(std::wstring& str, uint32_t cp); @@ -166,7 +226,7 @@ namespace Microsoft::Console::VirtualTerminal static uint32_t _bufferToCodepoint(const wchar_t* str) noexcept; static uint32_t _codepointToLower(uint32_t cp) noexcept; static uint32_t _bufferToLowerCodepoint(wchar_t* buf, int cap) noexcept; - static uint32_t _getBaseLayoutCodepoint(WORD vkey) noexcept; + static uint32_t _getBaseLayoutCodepoint(WORD scanCode) noexcept; #pragma region MouseInputState Management // These methods are defined in mouseInputState.cpp