This commit is contained in:
Leonard Hecker 2026-02-04 00:44:16 +01:00
parent 173f897751
commit 940e8d5c2a
3 changed files with 289 additions and 209 deletions

View File

@ -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)

View File

@ -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<uint32_t>(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<uint32_t>(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<byte, 256> TerminalInput::_getKeyboardState(const WORD virtualKeyCode, const DWORD controlKeyState)
std::array<byte, 256> TerminalInput::_getKeyboardState(const size_t virtualKeyCode, const DWORD controlKeyState)
{
auto keyState = std::array<byte, 256>{};
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<size_t>(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)
{

View File

@ -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<byte, 256> _getKeyboardState(WORD virtualKeyCode, DWORD controlKeyState);
static std::array<byte, 256> _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<uint8_t>& _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