diff --git a/src/terminal/input/terminalInput.cpp b/src/terminal/input/terminalInput.cpp index 4654bc918b..7e788994c0 100644 --- a/src/terminal/input/terminalInput.cpp +++ b/src/terminal/input/terminalInput.cpp @@ -29,14 +29,28 @@ TerminalInput::TerminalInput() noexcept _initKeyboardMap(); } -void TerminalInput::UseAlternateScreenBuffer() noexcept -{ - _inAlternateBuffer = true; -} - void TerminalInput::UseMainScreenBuffer() noexcept { + if (!_inAlternateBuffer) + { + return; + } + _inAlternateBuffer = false; + _kittyAltStack.clear(); + _kittyFlags = _kittyMainStack.empty() ? 0 : _kittyMainStack.back(); +} + +void TerminalInput::UseAlternateScreenBuffer() noexcept +{ + if (_inAlternateBuffer) + { + return; + } + + _inAlternateBuffer = true; + _kittyAltStack.clear(); + _kittyFlags = 0; } void TerminalInput::SetInputMode(const Mode mode, const bool enabled) noexcept @@ -93,6 +107,8 @@ void TerminalInput::ForceDisableKittyKeyboardProtocol(const bool disable) noexce if (disable) { _kittyFlags = 0; + _kittyMainStack.clear(); + _kittyAltStack.clear(); } } @@ -132,7 +148,7 @@ void TerminalInput::PushKittyFlags(const uint8_t flags) noexcept } auto& stack = _getKittyStack(); - // Evict oldest entry if stack is full (DoS prevention) + // > If a push request is received and the stack is full, the oldest entry from the stack must be evicted. if (stack.size() >= KittyStackMaxSize) { stack.erase(stack.begin()); @@ -141,22 +157,26 @@ void TerminalInput::PushKittyFlags(const uint8_t flags) noexcept _kittyFlags = flags & KittyKeyboardProtocolFlags::All; } -void TerminalInput::PopKittyFlags(const size_t count) noexcept +void TerminalInput::PopKittyFlags(size_t count) noexcept { - auto& stack = _getKittyStack(); - // If pop request exceeds stack size, reset all flags per spec: - // "If a pop request is received that empties the stack, all flags are reset." - if (count > stack.size()) + // NOTE: It's not just an optimization to return early here. + if (count == 0) { - stack.clear(); - _kittyFlags = 0; return; } - // Pop the requested number of entries, restoring flags from last popped - for (size_t i = 0; i < count; ++i) + + auto& stack = _getKittyStack(); + + if (count >= stack.size()) { - _kittyFlags = stack.back(); - stack.pop_back(); + // > If a pop request is received that empties the stack, all flags are reset. + _kittyFlags = 0; + stack.clear(); + } + else + { + _kittyFlags = stack.at(stack.size() - count); + stack.erase(stack.end() - count, stack.end()); } } @@ -204,26 +224,27 @@ TerminalInput::OutputType TerminalInput::HandleKey(const INPUT_RECORD& event) // GH#4999 - If we're in win32-input mode, skip straight to doing that. // Since this mode handles all types of key events, do nothing else. // Only do this if win32-input-mode support isn't manually disabled. - if (_inputMode.test(Mode::Win32) && !_forceDisableWin32InputMode) + if (_inputMode.test(Mode::Win32) && !_forceDisableWin32InputMode && !_kittyFlags) { return _makeWin32Output(keyEvent); } const auto controlKeyState = _trackControlKeyState(keyEvent); + if (_kittyFlags) + { + if (auto ret = _makeKittyOutput(keyEvent, controlKeyState)) + { + return ret; + } + } + const auto virtualKeyCode = keyEvent.wVirtualKeyCode; auto unicodeChar = keyEvent.uChar.UnicodeChar; // Check if this key matches the last recorded key code. const auto matchingLastKeyPress = _lastVirtualKeyCode == virtualKeyCode; - // If kitty keyboard mode is active, use kitty keyboard protocol. - // This handles release events when ReportEventTypes flag is set. - if (_kittyFlags != 0) - { - return _makeKittyOutput(keyEvent, controlKeyState); - } - // Only need to handle key down. See raw key handler (see RawReadWaitRoutine in stream.cpp) if (!keyEvent.bKeyDown) { @@ -664,7 +685,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) const +std::array TerminalInput::_getKeyboardState(const WORD virtualKeyCode, const DWORD controlKeyState) { auto keyState = std::array{}; if (virtualKeyCode < keyState.size()) @@ -768,414 +789,359 @@ TerminalInput::OutputType TerminalInput::_makeWin32Output(const KEY_EVENT_RECORD } // Generates kitty keyboard protocol output for a key event. -// https://sw.kovidgoyal.net/kitty/keyboard-protocol/ +// See: https://sw.kovidgoyal.net/kitty/keyboard-protocol/ +// +// Like all Kitty protocol specifications at the time (2026-01-30), it is unfortunately +// defined in a very informal, and sometimes contradictory, narrative specification. +// As such, you'll find that I've heavily "[editorialized]" most citations below. +// It's still a hard read. TerminalInput::OutputType TerminalInput::_makeKittyOutput(const KEY_EVENT_RECORD& key, const DWORD controlKeyState) { const auto virtualKeyCode = key.wVirtualKeyCode; - const auto virtualScanCode = key.wVirtualScanCode; const auto unicodeChar = key.uChar.UnicodeChar; const auto isKeyDown = key.bKeyDown; - - // Swallow lone leading surrogates... - if (til::is_leading_surrogate(unicodeChar)) - { - _leadingSurrogate = unicodeChar; - return _makeNoOutput(); - } - - // ...and combine them with trailing surrogates. - uint32_t fullCodepoint = unicodeChar; - if (_leadingSurrogate != 0 && til::is_trailing_surrogate(unicodeChar)) - { - fullCodepoint = til::combine_surrogates(_leadingSurrogate, unicodeChar); - _leadingSurrogate = 0; - } - else - { - _leadingSurrogate = 0; - } - - // Check if this key matches the last recorded key code (for repeat detection) + uint32_t codepoint = unicodeChar; const auto isRepeat = _lastVirtualKeyCode == virtualKeyCode && isKeyDown; - if (!isKeyDown) + + // First off, some state tracking. { - if (_lastVirtualKeyCode == virtualKeyCode) + // Swallow lone leading surrogates... + if (til::is_leading_surrogate(unicodeChar)) + { + _leadingSurrogate = unicodeChar; + return _makeNoOutput(); + } + // ...and combine them with trailing surrogates. + if (_leadingSurrogate != 0 && til::is_trailing_surrogate(unicodeChar)) + { + codepoint = til::combine_surrogates(_leadingSurrogate, unicodeChar); + } + _leadingSurrogate = 0; + + // Track key state for key repeat detection + if (isKeyDown) + { + _lastVirtualKeyCode = virtualKeyCode; + } + else if (_lastVirtualKeyCode == virtualKeyCode) { _lastVirtualKeyCode = std::nullopt; } } + + const bool shiftPressed = WI_IsFlagSet(controlKeyState, SHIFT_PRESSED); + const bool ctrlPressed = WI_IsAnyFlagSet(controlKeyState, CTRL_PRESSED); + const bool altPressed = WI_IsAnyFlagSet(controlKeyState, ALT_PRESSED); + + // Variables for the legacy encoding (CSI [~ABCDEFHPQS]) + wchar_t legacyFinal = 0; + int32_t legacyParam = 0; + + // Variables for the kitty encoding (CSI u) + // There will be a summary description of these parameters at the end, + // where I'm building the final escape sequence string. + int32_t kittyKeyCode = 0; + int32_t kittyAltKeyCodeShifted = 0; + int32_t kittyAltKeyCodeBase = 0; + int32_t kittyModifiers = 0; + int32_t kittyEventType = 0; + int32_t kittyTextAsCodepoint = 0; + + if (WI_IsFlagSet(_kittyFlags, KittyKeyboardProtocolFlags::DisambiguateEscapeCodes)) + { + // > Turning on this flag will cause the terminal to report the + // > [KEYS] keys using CSI u sequences instead of legacy ones. + + // > The only exceptions [from the Disambiguate Escape Codes specification] + // > are the Enter, Tab and Backspace keys which [...] [use] legacy mode [encoding] [...]. + if ((virtualKeyCode == VK_RETURN || virtualKeyCode == VK_TAB || virtualKeyCode == VK_BACK) && + WI_IsFlagClear(_kittyFlags, KittyKeyboardProtocolFlags::ReportAllKeysAsEscapeCodes)) + { + return MakeUnhandled(); + } + + // > Additionally, all non text keypad keys will be reported [...] with CSI u encoding, [...]. + if (codepoint == 0 && virtualKeyCode >= VK_NUMPAD0 && virtualKeyCode <= VK_DIVIDE) + { + kittyKeyCode = _getKittyKeyCode(key, controlKeyState); + } + // Where [KEYS] mentions: + // > ESC + else if (virtualKeyCode == VK_ESCAPE) + { + kittyKeyCode = 27; // ESCAPE + } + // Where [KEYS] mentions: + // > alt+key, ctrl+key, ctrl+alt+key, shift+alt+key + // + // > Here key is any ASCII key as described in Legacy text keys. [...] + // + // > Legacy text keys: + // > For legacy compatibility, the keys a-z 0-9 ` - = [ ] \ ; ' , . / [...] + else if (controlKeyState == ALT_PRESSED || + controlKeyState == CTRL_PRESSED || + controlKeyState == (CTRL_PRESSED | ALT_PRESSED) || + controlKeyState == (SHIFT_PRESSED | ALT_PRESSED)) + { + const auto hkl = GetKeyboardLayout(GetWindowThreadProcessId(GetForegroundWindow(), nullptr)); + const auto ch = MapVirtualKeyExW(virtualKeyCode, MAPVK_VK_TO_CHAR, hkl); + if ((ch >= '`' && ch <= 'z') || // ` a-z + (ch >= '0' && ch <= '9') || + (ch >= ',' && ch <= '.') || // , - . + (ch >= '[' && ch <= ']') || // [ \ ] + ch == L'=' || ch == L';' || ch == L'\'' || ch == L'/') + { + kittyKeyCode = ch; + } + } + } + + if (WI_IsFlagSet(_kittyFlags, KittyKeyboardProtocolFlags::ReportEventTypes)) + { + // > This [...] causes the terminal to report key repeat and key release events. + if (!isKeyDown) + { + kittyEventType = 3; // release event + } + else if (isRepeat) + { + kittyEventType = 2; // repeat event + } + else + { + kittyEventType = 1; // press event + } + } else { - _lastVirtualKeyCode = virtualKeyCode; - } - - // Note: Disambiguate flag (0x01) is implicitly handled - if we're in this function - // at all (_kittyFlags != 0), then Ctrl+key and Alt+key combos get CSI u encoding. - const auto reportEventTypes = (_kittyFlags & KittyKeyboardProtocolFlags::ReportEventTypes) != 0; - const auto reportAllKeys = (_kittyFlags & KittyKeyboardProtocolFlags::ReportAllKeys) != 0; - const auto reportAlternateKeys = (_kittyFlags & KittyKeyboardProtocolFlags::ReportAlternateKeys) != 0; - const auto reportText = (_kittyFlags & KittyKeyboardProtocolFlags::ReportText) != 0; - - // Without ReportEventTypes, we only handle key down events - if (!isKeyDown && !reportEventTypes) - { - return _makeNoOutput(); - } - - // Get the functional key code, or 0 if this key should use legacy encoding. - const auto functionalKeyCode = _getKittyFunctionalKeyCode(virtualKeyCode, virtualScanCode, controlKeyState); - const auto ctrlIsPressed = WI_IsAnyFlagSet(controlKeyState, CTRL_PRESSED); - const auto altIsPressed = WI_IsAnyFlagSet(controlKeyState, ALT_PRESSED); - - if (!reportAllKeys) - { - // Per spec: "Additionally, with this mode [ReportAllKeys], events for pressing - // modifier keys are reported." So we skip modifier key events without it. - if ((functionalKeyCode >= 57358 && functionalKeyCode <= 57360) || - (functionalKeyCode >= 57441 && functionalKeyCode <= 57450)) + // > Normally only key press events are reported [...]. + if (!isKeyDown) { return _makeNoOutput(); } - - // Legacy encoding for Enter, Tab, and Backspace (spec recovery guarantee). - // These keys use mode-aware legacy sequences unless ReportAllKeys is set, ensuring - // users can type "reset" if an app crashes with the protocol enabled. - // Unlike CSI u (which is mode-independent), legacy encoding must honor LineFeed - // and BackarrowKey modes. Ctrl/Alt combos still use CSI u for disambiguation. - if (virtualKeyCode == VK_RETURN || virtualKeyCode == VK_TAB || virtualKeyCode == VK_BACK) - { - if (!isKeyDown || ctrlIsPressed || altIsPressed) - { - return _makeNoOutput(); - } - - std::wstring str; - switch (virtualKeyCode) - { - case VK_RETURN: - str = _inputMode.test(Mode::LineFeed) ? L"\r\n" : L"\r"; - break; - case VK_TAB: - if (WI_IsFlagSet(controlKeyState, SHIFT_PRESSED)) - { - str = fmt::format(FMT_COMPILE(L"{}Z"), _csi); - } - else - { - str = L"\t"; - } - break; - case VK_BACK: - str = _inputMode.test(Mode::BackarrowKey) ? L"\x08" : L"\x7f"; - break; - default: - break; - } - return MakeOutput(std::move(str)); - } - - // Fast path: For simple text key presses (key down, not a functional key, has a codepoint), - // without Ctrl/Alt modifiers that require disambiguation, and not in reportAllKeys mode, - // we can bypass CSI u encoding and send the character directly. - if (isKeyDown && functionalKeyCode == 0 && fullCodepoint != 0 && !ctrlIsPressed && !altIsPressed) - { - const auto cb = _codepointToBuffer(fullCodepoint); - return MakeOutput({ cb.buf, cb.len }); - } } - const auto isEnhanced = WI_IsFlagSet(controlKeyState, ENHANCED_KEY); - wchar_t legacyFinalChar = 0; - uint32_t legacyParam = 1; - - switch (virtualKeyCode) + if (WI_IsFlagSet(_kittyFlags, KittyKeyboardProtocolFlags::ReportAllKeysAsEscapeCodes)) { - case VK_UP: - if (isEnhanced) + // > This [...] turns on key reporting even for key events that generate text. + // > [...] text will not be sent, instead only key events are sent. + // + // > [...] with this mode, events for pressing modifier keys are reported. + + if (WI_IsFlagSet(_kittyFlags, KittyKeyboardProtocolFlags::ReportAssociatedText)) { - legacyFinalChar = L'A'; + // > This [...] causes key events that generate text to be reported + // > as CSI u escape codes with the text embedded in the escape code. + // + // > Note that this flag is an enhancement to Report all keys as escape codes [...]. + kittyTextAsCodepoint = codepoint; } - break; - case VK_DOWN: - if (isEnhanced) + } + else + { + // The inverse of reporting all keys is that we don't handle keys that we haven't yet handled. + if (kittyKeyCode == 0) { - legacyFinalChar = L'B'; + return MakeUnhandled(); } - break; - case VK_RIGHT: - if (isEnhanced) + + // From a side note (?!) on the "Report event types" section: + // + // > NOTE: The Enter, Tab and Backspace keys will not have release + // > events unless Report all keys as escape codes is also set [...] + if (kittyEventType == 3 && (virtualKeyCode == VK_RETURN || virtualKeyCode == VK_TAB || virtualKeyCode == VK_BACK)) { - legacyFinalChar = L'C'; + return _makeNoOutput(); } - break; - case VK_LEFT: - if (isEnhanced) - { - legacyFinalChar = L'D'; - } - break; - case VK_HOME: - if (isEnhanced) - { - legacyFinalChar = L'H'; - } - break; - case VK_END: - if (isEnhanced) - { - legacyFinalChar = L'F'; - } - break; - case VK_INSERT: - case VK_DELETE: - if (isEnhanced) - { - legacyFinalChar = L'~'; - legacyParam = 2 + (virtualKeyCode - VK_INSERT); - } - break; - case VK_PRIOR: - case VK_NEXT: - if (isEnhanced) - { - legacyFinalChar = L'~'; - legacyParam = 5 + (virtualKeyCode - VK_PRIOR); - } - break; - case VK_F1: - case VK_F2: - case VK_F4: - legacyFinalChar = L'P' + (virtualKeyCode - VK_F1); - break; - case VK_F3: - // Note: F3 cannot use CSI R as that conflicts with Cursor Position Report. - // The kitty spec explicitly removed CSI R for F3. - legacyFinalChar = L'~'; - legacyParam = 13; - break; - case VK_F5: - legacyFinalChar = L'~'; - legacyParam = 15; - break; - case VK_F6: - case VK_F7: - case VK_F8: - case VK_F9: - case VK_F10: - legacyFinalChar = L'~'; - legacyParam = 17 + (virtualKeyCode - VK_F6); - break; - case VK_F11: - case VK_F12: - legacyFinalChar = L'~'; - legacyParam = 23 + (virtualKeyCode - VK_F11); - break; - default: - break; } - // Calculate kitty modifiers early - needed for legacy sequences too - // kitty: shift=1, alt=2, ctrl=4, super=8, hyper=16, meta=32, caps_lock=64, num_lock=128 - uint32_t modifiers = 0; + // > Terminals may choose what they want to do about functional keys that have no legacy encoding. + // + // Specification doesn't specify, so I'm doing it whenever any mode is set. + if (kittyKeyCode == 0) + { + kittyKeyCode = _getKittyKeyCode(key, controlKeyState); + } + if (kittyKeyCode == 0) + { + return MakeUnhandled(); + } + + if (WI_IsFlagSet(_kittyFlags, KittyKeyboardProtocolFlags::ReportAlternateKeys)) + { + // > This [...] causes the terminal to report alternate key values [...] + // > See Key codes for details [...] + // + // > Key codes: + // > The unicode-key-code above is the Unicode codepoint representing the key [...] + // > Note that the codepoint used is always the lower-case [...] version of the key. + + // > Note that [...] only key events represented as escape codes due to the + // > other enhancements in effect will be affected by this enhancement. [...] + if (false) + { + // TODO + } + + // > [...] the terminal can [...] [additionally send] the shifted key and base layout key [...]. + // > The shifted key is [...] the upper-case version of unicode-codepoint, + // > or more technically, the shifted version, in the currently active keyboard layout. + // + // !!NOTE!! that the 2nd line is wrong. On an US layout, Shift+3 is not an "uppercase 3", + // it's "#", which is exactly what kitty sends as the "shifted key" parameter. + // (This is in addition to "unicode-codepoint" never being defined throughout the spec...) + // + // > Note that the shifted key must be present only if shift is also present in the modifiers. + if (shiftPressed) + { + // I'm assuming that codepoint is already the shifted version if shift is pressed. + kittyAltKeyCodeShifted = codepoint; + } + kittyAltKeyCodeBase = _getBaseLayoutCodepoint(virtualKeyCode); + } + + // Modifiers: shift=1, alt=2, ctrl=4, super=8, hyper=16, meta=32, caps_lock=64, num_lock=128 if (WI_IsFlagSet(controlKeyState, SHIFT_PRESSED)) { - modifiers |= 1; + kittyModifiers |= 1; } if (WI_IsAnyFlagSet(controlKeyState, ALT_PRESSED)) { - modifiers |= 2; + kittyModifiers |= 2; } if (WI_IsAnyFlagSet(controlKeyState, CTRL_PRESSED)) { - modifiers |= 4; + kittyModifiers |= 4; } - // Per spec: "Lock modifiers are not reported for text producing keys, to keep them - // usable in legacy programs. To get lock modifiers for all keys use the Report all - // keys as escape codes enhancement." So we report them for functional keys always, - // and for text-producing keys only when ReportAllKeys is set. - if (functionalKeyCode != 0 || reportAllKeys) + // > Lock modifiers are not reported for text producing keys, [...]. + // > To get lock modifiers for all keys use the Report all keys as escape codes enhancement. + if (!_codepointIsText(kittyKeyCode) || WI_IsFlagSet(_kittyFlags, KittyKeyboardProtocolFlags::ReportAllKeysAsEscapeCodes)) { if (WI_IsFlagSet(controlKeyState, CAPSLOCK_ON)) { - modifiers |= 64; + kittyModifiers |= 64; } if (WI_IsFlagSet(controlKeyState, NUMLOCK_ON)) { - modifiers |= 128; + kittyModifiers |= 128; } } - const auto encodedModifiers = 1 + modifiers; - // Determine event type: 1=press, 2=repeat, 3=release - uint32_t eventType = 1; - if (!isKeyDown) - { - eventType = 3; - } - else if (isRepeat) - { - eventType = 2; - } + std::wstring seq; + seq.append(_csi); - // If this is a key that uses legacy CSI sequences, generate it - if (legacyFinalChar != 0) + if (legacyFinal) { - // Format: CSI param ; modifiers ~ or CSI param ; modifiers : event-type ~ - std::wstring seq; - seq.append(_csi); + // > The only exceptions are the Enter, Tab and Backspace keys + // > which still generate the same bytes as in legacy mode [...] + switch (legacyFinal) + { + case '\n': + return MakeOutput(_inputMode.test(Mode::LineFeed) ? L"\r\n" : L"\r"); + case '\t': + if (shiftPressed) + { + return MakeOutput(fmt::format(FMT_COMPILE(L"{}Z"), _csi)); + } + return MakeOutput(L"\t"); + case 0x08: + return MakeOutput(_inputMode.test(Mode::BackarrowKey) ? L"\x08" : L"\x7f"); + default: + break; + } + + // Legacy format: CSI [param] [; modifiers [:event-type]] final if (legacyParam > 1) { fmt::format_to(std::back_inserter(seq), FMT_COMPILE(L"{}"), legacyParam); } - if (encodedModifiers > 1 || (reportEventTypes && eventType > 1)) + if (kittyModifiers != 0 || kittyEventType != 0) { - fmt::format_to(std::back_inserter(seq), FMT_COMPILE(L";{}"), encodedModifiers); - if (reportEventTypes && eventType > 1) + fmt::format_to(std::back_inserter(seq), FMT_COMPILE(L";{}"), 1 + kittyModifiers); + if (kittyEventType != 0) { - fmt::format_to(std::back_inserter(seq), FMT_COMPILE(L":{}"), eventType); + fmt::format_to(std::back_inserter(seq), FMT_COMPILE(L":{}"), kittyEventType); } } - seq.push_back(legacyFinalChar); - return seq; + seq.push_back(legacyFinal); + return MakeOutput(seq); } - // According to kitty protocol: - // > the codepoint used is always the lower-case (or more technically, un-shifted) version of the key - uint32_t keyCode = functionalKeyCode; - if (keyCode == 0) - { - // For alphabetic keys, use the virtual key code converted to lowercase. - // We can't use unicodeChar because when Ctrl is pressed, unicodeChar - // becomes the control character (e.g., Ctrl+C gives unicodeChar=0x03). - if (virtualKeyCode >= 'A' && virtualKeyCode <= 'Z') - { - keyCode = virtualKeyCode + 32; // Convert to lowercase ('A'->'a') - } - // Space needs special handling because Ctrl+Space produces NUL (0). - else if (virtualKeyCode == VK_SPACE) - { - keyCode = L' '; - } - else - { - keyCode = fullCodepoint; + // > CSI unicode-key-code:alternate-key-codes ; modifiers:event-type ; text-as-codepoints u + // + // unicode-key-code: + // > [..] the [lowercase] Unicode codepoint representing the key, as a decimal number. + // + // alternate-key-codes: + // > [...] the terminal can send [...] the shifted key and base layout key, separated by colons. + // shifted key: + // > [...] the shifted key and base layout key, separated by colons. + // > The shifted key is [...] [the result of Shift + unicode-key-code], + // > in the currently active keyboard layout. + // base layout key: + // > [...] the base layout key is the key corresponding to + // > the physical key in the standard PC-101 key layout. + // + // modifiers: + // > Modifiers are encoded as a bit field with: + // > [...] + // For bit encoding see above. + // > [...] the modifier value is encoded as [...] 1 + actual modifiers. + // + // event-type: + // > There are three key event types: press, repeat and release. + // To summarize unnecessary prose: press=1, repeat=2, release=3. + // > If [event-types are requested and] no modifiers are present, + // > the modifiers field must have the value 1 [...]. + // + // text-as-codepoints: + // > [...] the text associated with key events as a sequence of Unicode code points. + // > If multiple code points are present, they must be separated by colons. + // > If no known key is associated with the text the key number 0 must be used. + // > The associated text must not contain control codes [...]. - // For control characters (e.g., Ctrl+[ produces ESC), use ToUnicodeEx - // to get the base character without modifiers. - if (!_codepointIsNonControl(keyCode)) - { - const auto hkl = GetKeyboardLayout(GetWindowThreadProcessId(GetForegroundWindow(), nullptr)); - auto keyState = _getKeyboardState(virtualKeyCode, 0); + fmt::format_to(std::back_inserter(seq), FMT_COMPILE(L"{}"), kittyKeyCode); - // Disable Ctrl and Alt modifiers 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; - - wchar_t buffer[4]; - const auto result = ToUnicodeEx(virtualKeyCode, 0, keyState.data(), buffer, 4, 4, hkl); - - if (result > 0 && result < 4) - { - keyCode = _bufferToCodepoint(&buffer[0]); - } - } - - keyCode = _codepointToLower(keyCode); - - if (!_codepointIsNonControl(keyCode)) - { - return _makeNoOutput(); - } - } - } - - // Add alternate keys if requested (shifted key and base layout key) - uint32_t shiftedKey = 0; - uint32_t baseLayoutKey = 0; - if (reportAlternateKeys && functionalKeyCode == 0) - { - // Shifted key: the uppercase/shifted version of the key - if ((modifiers & 1) != 0 && fullCodepoint != 0 && fullCodepoint != keyCode) - { - shiftedKey = fullCodepoint; - } - - // Base layout key: the key in the standard US PC-101 layout. - static const auto usLayout = LoadKeyboardLayoutW(L"00000409", 0); - if (usLayout != nullptr && virtualKeyCode != 0) - { - auto keyState = _getKeyboardState(virtualKeyCode, 0); // No modifiers for base key - wchar_t baseChar[4]{}; - const auto result = ToUnicodeEx(virtualKeyCode, 0, keyState.data(), baseChar, 4, 4, usLayout); - if (result == 1 && baseChar[0] >= 0x20) - { - // Use lowercase version of the base layout key - auto baseKey = static_cast(baseChar[0]); - if (baseKey >= L'A' && baseKey <= L'Z') - { - baseKey += 32; - } - // Only include if different from keyCode - if (baseKey != keyCode) - { - baseLayoutKey = baseKey; - } - } - } - } - - // CSI unicode-key-code:shifted-key:base-layout-key ; modifiers:event-type ; text-as-codepoints u - - std::wstring seq; - fmt::format_to(std::back_inserter(seq), FMT_COMPILE(L"{}{}"), _csi, keyCode); - - // Append alternate keys to sequence if present - if (shiftedKey != 0 || baseLayoutKey != 0) + if (kittyAltKeyCodeShifted != 0 || kittyAltKeyCodeBase != 0) { seq.push_back(L':'); - if (shiftedKey != 0) + if (kittyAltKeyCodeShifted != 0) { - fmt::format_to(std::back_inserter(seq), FMT_COMPILE(L"{}"), shiftedKey); + fmt::format_to(std::back_inserter(seq), FMT_COMPILE(L"{}"), kittyAltKeyCodeShifted); } - if (baseLayoutKey != 0) + if (kittyAltKeyCodeBase != 0) { - fmt::format_to(std::back_inserter(seq), FMT_COMPILE(L":{}"), baseLayoutKey); + fmt::format_to(std::back_inserter(seq), FMT_COMPILE(L":{}"), kittyAltKeyCodeBase); } } - // Determine if we need to output text-as-codepoints (third field) - // Exclude C0 (< 0x20) and C1 (0x80-0x9F) control codes per spec. - const auto isValidText = fullCodepoint >= 0x20 && (fullCodepoint < 0x80 || fullCodepoint > 0x9F); - const auto needsText = reportText && reportAllKeys && functionalKeyCode == 0 && isValidText && isKeyDown; - - // We need to include modifiers field if: - // - modifiers are non-default (encodedModifiers > 1), OR - // - we need to report non-press event type, OR - // - we need to output text (text is the 3rd field, so we must have 2nd field too) - const auto needsEventType = reportEventTypes && eventType > 1; - if (encodedModifiers > 1 || needsEventType || needsText) + if (kittyModifiers != 0 || kittyEventType != 0 || kittyTextAsCodepoint != 0) { - // Per spec: "If no modifiers are present, the modifiers field must have the value 1" - // when event type sub-field is needed. - fmt::format_to(std::back_inserter(seq), FMT_COMPILE(L";{}"), encodedModifiers); - if (needsEventType) + fmt::format_to(std::back_inserter(seq), FMT_COMPILE(L";{}"), 1 + kittyModifiers); + if (kittyEventType != 0) { - fmt::format_to(std::back_inserter(seq), FMT_COMPILE(L":{}"), eventType); + fmt::format_to(std::back_inserter(seq), FMT_COMPILE(L":{}"), kittyEventType); } - if (needsText) + if (kittyTextAsCodepoint != 0) { - fmt::format_to(std::back_inserter(seq), FMT_COMPILE(L";{}"), fullCodepoint); + fmt::format_to(std::back_inserter(seq), FMT_COMPILE(L";{}"), kittyTextAsCodepoint); } } seq.push_back(L'u'); - return seq; + return MakeOutput(std::move(seq)); } -// See: https://sw.kovidgoyal.net/kitty/keyboard-protocol/#functional-key-definitions -// NOTE: The definition documents keys named as KP_*, which are keypad keys. -uint32_t TerminalInput::_getKittyFunctionalKeyCode(const WORD virtualKeyCode, const WORD virtualScanCode, const DWORD controlKeyState) noexcept +int32_t TerminalInput::_getKittyKeyCode(const KEY_EVENT_RECORD& key, DWORD controlKeyState) noexcept { + const auto virtualKeyCode = key.wVirtualKeyCode; + + if ((virtualKeyCode >= 'A' && virtualKeyCode <= 'Z') || + (virtualKeyCode >= '0' && virtualKeyCode <= '9')) + { + return virtualKeyCode; + } + const auto isEnhanced = WI_IsFlagSet(controlKeyState, ENHANCED_KEY); switch (virtualKeyCode) @@ -1301,7 +1267,7 @@ uint32_t TerminalInput::_getKittyFunctionalKeyCode(const WORD virtualKeyCode, co // Modifier keys case VK_SHIFT: - return virtualScanCode == 0x2A ? 57441 : 57447; // LEFT_SHIFT : RIGHT_SHIFT + return key.wVirtualScanCode == 0x2A ? 57441 : 57447; // LEFT_SHIFT : RIGHT_SHIFT case VK_LSHIFT: return 57441; // LEFT_SHIFT case VK_RSHIFT: @@ -1333,9 +1299,9 @@ std::vector& TerminalInput::_getKittyStack() noexcept return _inAlternateBuffer ? _kittyAltStack : _kittyMainStack; } -bool TerminalInput::_codepointIsNonControl(uint32_t cp) noexcept +bool TerminalInput::_codepointIsText(uint32_t cp) noexcept { - return cp > 0x1f && (cp < 0x7f || cp > 0x9f); + return cp > 0x1f && (cp < 0x7f || cp > 0x9f) && (cp < 57344 || cp > 63743); } TerminalInput::CodepointBuffer TerminalInput::_codepointToBuffer(uint32_t cp) noexcept @@ -1369,14 +1335,49 @@ uint32_t TerminalInput::_bufferToCodepoint(const wchar_t* str) noexcept uint32_t TerminalInput::_codepointToLower(uint32_t cp) noexcept { - auto cb = _codepointToBuffer(cp); - // NOTE: MSDN states that `lpSrcStr == lpDestStr` is valid for LCMAP_LOWERCASE. - const auto len = LCMapStringW(LOCALE_INVARIANT, LCMAP_LOWERCASE, cb.buf, cb.len, cb.buf, gsl::narrow_cast(std::size(cb.buf))); - // NOTE: LCMapStringW returns the length including the null terminator. I'm not checking for it, - // because after decades, LCMapStringW should be reliable enough to return len==0 for OOM. - if (len > 1) + if (cp < 0x80) { - return _bufferToCodepoint(cb.buf); + return til::tolower_ascii(cp); } - return cp; + + auto cb = _codepointToBuffer(cp); + return _bufferToLowerCodepoint(cb.buf, gsl::narrow_cast(std::size(cb.buf))); +} + +uint32_t TerminalInput::_bufferToLowerCodepoint(wchar_t* buf, int cap) noexcept +{ + // NOTE: MSDN states that `lpSrcStr == lpDestStr` is valid for LCMAP_LOWERCASE. + const auto len = LCMapStringW(LOCALE_INVARIANT, LCMAP_LOWERCASE, buf, -1, buf, cap); + // NOTE: LCMapStringW returns the length including the null terminator. + if (len == 0) + { + return 0; + } + return _bufferToCodepoint(buf); +} + +uint32_t TerminalInput::_getBaseLayoutCodepoint(const WORD vkey) noexcept +{ + // > 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 || !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); + + if (result == 0) + { + return 0; + } + + // > [...] pressing the ctrl+ะก key will be ctrl+c in the standard layout. + // > So the terminal should send the base layout key as 99 corresponding to the c key. + // + // Why use many words when few do trick? base-layout = lowercase. + return _bufferToLowerCodepoint(baseChar, gsl::narrow_cast(std::size(baseChar))); } diff --git a/src/terminal/input/terminalInput.hpp b/src/terminal/input/terminalInput.hpp index ce24c2e5b0..5a82a3730a 100644 --- a/src/terminal/input/terminalInput.hpp +++ b/src/terminal/input/terminalInput.hpp @@ -52,11 +52,11 @@ namespace Microsoft::Console::VirtualTerminal struct KittyKeyboardProtocolFlags { static constexpr uint8_t None = 0; - static constexpr uint8_t Disambiguate = 1 << 0; // Disambiguate escape codes - static constexpr uint8_t ReportEventTypes = 1 << 1; // Report event types (press/repeat/release) - static constexpr uint8_t ReportAlternateKeys = 1 << 2; // Report alternate keys - static constexpr uint8_t ReportAllKeys = 1 << 3; // Report all keys as escape codes - static constexpr uint8_t ReportText = 1 << 4; // Report associated text + static constexpr uint8_t DisambiguateEscapeCodes = 1 << 0; + static constexpr uint8_t ReportEventTypes = 1 << 1; + static constexpr uint8_t ReportAlternateKeys = 1 << 2; + static constexpr uint8_t ReportAllKeysAsEscapeCodes = 1 << 3; + static constexpr uint8_t ReportAssociatedText = 1 << 4; static constexpr uint8_t All = (1 << 5) - 1; }; enum class KittyKeyboardProtocolMode : uint8_t @@ -111,8 +111,8 @@ namespace Microsoft::Console::VirtualTerminal bool _forceDisableWin32InputMode{ false }; bool _inAlternateBuffer{ false }; - // Kitty keyboard protocol state - separate stacks for main and alternate screen buffers - static constexpr size_t KittyStackMaxSize = 16; + // Kitty keyboard protocol state + static constexpr size_t KittyStackMaxSize = 8; bool _forceDisableKittyKeyboardProtocol = false; uint8_t _kittyFlags = 0; std::vector _kittyMainStack; @@ -123,19 +123,21 @@ namespace Microsoft::Console::VirtualTerminal void _initKeyboardMap() noexcept; DWORD _trackControlKeyState(const KEY_EVENT_RECORD& key) noexcept; - std::array _getKeyboardState(WORD virtualKeyCode, DWORD controlKeyState) const; + static std::array _getKeyboardState(WORD virtualKeyCode, DWORD controlKeyState); [[nodiscard]] static wchar_t _makeCtrlChar(wchar_t ch); [[nodiscard]] StringType _makeCharOutput(wchar_t ch); [[nodiscard]] static StringType _makeNoOutput() noexcept; void _escapeOutput(StringType& charSequence, bool altIsPressed) const; [[nodiscard]] OutputType _makeWin32Output(const KEY_EVENT_RECORD& key) const; [[nodiscard]] OutputType _makeKittyOutput(const KEY_EVENT_RECORD& key, DWORD controlKeyState); - [[nodiscard]] static uint32_t _getKittyFunctionalKeyCode(WORD virtualKeyCode, WORD virtualScanCode, DWORD controlKeyState) noexcept; + static int32_t _getKittyKeyCode(const KEY_EVENT_RECORD& key, DWORD controlKeyState) noexcept; std::vector& _getKittyStack() noexcept; - static bool _codepointIsNonControl(uint32_t cp) noexcept; + static bool _codepointIsText(uint32_t cp) noexcept; static CodepointBuffer _codepointToBuffer(uint32_t cp) noexcept; 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; #pragma region MouseInputState Management // These methods are defined in mouseInputState.cpp diff --git a/src/terminal/input/ut_kittyKeyboardProtocol.cpp b/src/terminal/input/ut_kittyKeyboardProtocol.cpp new file mode 100644 index 0000000000..f4e6e7ac0b --- /dev/null +++ b/src/terminal/input/ut_kittyKeyboardProtocol.cpp @@ -0,0 +1,1537 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "terminalInput.hpp" +#include + +using namespace WEX::TestExecution; +using namespace WEX::Logging; +using namespace WEX::Common; +using namespace Microsoft::Console::VirtualTerminal; + +// ============================================================================= +// IMPORTANT: Test Design Document for Kitty Keyboard Protocol +// ============================================================================= +// +// This file contains comprehensive unit tests for the Kitty Keyboard Protocol +// as specified at: https://sw.kovidgoyal.net/kitty/keyboard-protocol/ +// +// The tests are organized by the following categories: +// +// 1. Enhancement Flag Combinations (32 combinations for 5 flags) +// - 0b00001 (1) = Disambiguate escape codes +// - 0b00010 (2) = Report event types +// - 0b00100 (4) = Report alternate keys +// - 0b01000 (8) = Report all keys as escape codes +// - 0b10000 (16) = Report associated text +// +// 2. Modifier Combinations (bit field encoding: 1 + actual modifiers) +// - shift=1, alt=2, ctrl=4, super=8, hyper=16, meta=32, caps_lock=64, num_lock=128 +// +// 3. Event Types +// - press (1, default), repeat (2), release (3) +// +// 4. Special Key Behaviors +// - Enter, Tab, Backspace: no release events unless ReportAllKeysAsEscapeCodes +// - Lock modifiers: not reported for text keys unless ReportAllKeysAsEscapeCodes +// +// 5. Key Categories +// - Text-producing keys (a-z, 0-9, symbols) +// - Functional keys (F1-F35, navigation, etc.) +// - Keypad keys +// - Modifier keys +// +// ============================================================================= + +namespace +{ + // Win32 control key state flags + constexpr DWORD RIGHT_ALT_PRESSED = 0x0001; + constexpr DWORD LEFT_ALT_PRESSED = 0x0002; + constexpr DWORD RIGHT_CTRL_PRESSED = 0x0004; + constexpr DWORD LEFT_CTRL_PRESSED = 0x0008; + constexpr DWORD SHIFT_PRESSED = 0x0010; + constexpr DWORD NUMLOCK_ON = 0x0020; + constexpr DWORD SCROLLLOCK_ON = 0x0040; + constexpr DWORD CAPSLOCK_ON = 0x0080; + constexpr DWORD ENHANCED_KEY = 0x0100; + + // Kitty enhancement flags + constexpr uint8_t DisambiguateEscapeCodes = 0b00001; // 1 + constexpr uint8_t ReportEventTypes = 0b00010; // 2 + constexpr uint8_t ReportAlternateKeys = 0b00100; // 4 + constexpr uint8_t ReportAllKeysAsEscapeCodes = 0b01000; // 8 + constexpr uint8_t ReportAssociatedText = 0b10000; // 16 + + // Virtual key codes + constexpr WORD VK_A = 'A'; + constexpr WORD VK_B = 'B'; + constexpr WORD VK_C = 'C'; + constexpr WORD VK_W = 'W'; + constexpr WORD VK_SPACE = 0x20; + constexpr WORD VK_RETURN = 0x0D; + constexpr WORD VK_TAB_KEY = 0x09; + constexpr WORD VK_BACK = 0x08; + constexpr WORD VK_ESCAPE = 0x1B; + constexpr WORD VK_F1 = 0x70; + constexpr WORD VK_F2 = 0x71; + constexpr WORD VK_F3 = 0x72; + constexpr WORD VK_F4 = 0x73; + constexpr WORD VK_F5 = 0x74; + constexpr WORD VK_F12 = 0x7B; + constexpr WORD VK_F13 = 0x7C; + constexpr WORD VK_F24 = 0x87; + constexpr WORD VK_LEFT = 0x25; + constexpr WORD VK_UP = 0x26; + constexpr WORD VK_RIGHT = 0x27; + constexpr WORD VK_DOWN = 0x28; + constexpr WORD VK_HOME = 0x24; + constexpr WORD VK_END = 0x23; + constexpr WORD VK_PRIOR = 0x21; // Page Up + constexpr WORD VK_NEXT = 0x22; // Page Down + constexpr WORD VK_INSERT = 0x2D; + constexpr WORD VK_DELETE = 0x2E; + constexpr WORD VK_NUMPAD0 = 0x60; + constexpr WORD VK_NUMPAD9 = 0x69; + constexpr WORD VK_MULTIPLY = 0x6A; + constexpr WORD VK_ADD = 0x6B; + constexpr WORD VK_SUBTRACT = 0x6D; + constexpr WORD VK_DECIMAL = 0x6E; + constexpr WORD VK_DIVIDE = 0x6F; + constexpr WORD VK_LSHIFT = 0xA0; + constexpr WORD VK_RSHIFT = 0xA1; + constexpr WORD VK_LCONTROL = 0xA2; + constexpr WORD VK_RCONTROL = 0xA3; + constexpr WORD VK_LMENU = 0xA4; + constexpr WORD VK_RMENU = 0xA5; + constexpr WORD VK_CAPITAL = 0x14; // Caps Lock + constexpr WORD VK_NUMLOCK = 0x90; + constexpr WORD VK_SCROLL = 0x91; + + // Helper to create Output from string + TerminalInput::OutputType wrap(const std::wstring& str) + { + return TerminalInput::MakeOutput(str); + } + + // Placeholder for the test harness function that processes key events + // This should call _makeKittyOutput with the given parameters + TerminalInput::OutputType process( + TerminalInput& input, + bool bKeyDown, + uint16_t wVirtualKeyCode, + uint16_t wVirtualScanCode, + wchar_t UnicodeChar, + uint32_t dwControlKeyState) + { + KEY_EVENT_RECORD keyEvent = {}; + keyEvent.bKeyDown = bKeyDown ? TRUE : FALSE; + keyEvent.wRepeatCount = 1; + keyEvent.wVirtualKeyCode = wVirtualKeyCode; + keyEvent.wVirtualScanCode = wVirtualScanCode; + keyEvent.uChar.UnicodeChar = UnicodeChar; + keyEvent.dwControlKeyState = dwControlKeyState; + + INPUT_RECORD record = {}; + record.EventType = KEY_EVENT; + record.Event.KeyEvent = keyEvent; + + return input.HandleKey(record); + } +} + +class KittyKeyboardProtocolTests +{ + TEST_CLASS(KittyKeyboardProtocolTests); + + // ========================================================================= + // SECTION 1: Enhancement Flag Combinations (32 tests) + // Test all 32 combinations of the 5 enhancement flags + // ========================================================================= + + // Flag Combination 0b00000 (0) - No enhancements (legacy mode) + TEST_METHOD(EnhancementFlags_0x00_NoEnhancements_SimpleKeyPress) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(0, KittyKeyboardProtocolMode::Replace); + + // In legacy mode with no kitty flags, 'a' should produce plain text + // This tests that without any enhancements, we fall through to non-kitty handling + auto result = process(input, true, VK_A, 0x1E, L'a', 0); + // Legacy behavior - not CSI u encoded + VERIFY_IS_TRUE(!result.has_value() || std::get(*result) != L"\x1b[97u"); + } + + // Flag Combination 0b00001 (1) - Disambiguate escape codes only + TEST_METHOD(EnhancementFlags_0x01_Disambiguate_EscapeKey) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(DisambiguateEscapeCodes, KittyKeyboardProtocolMode::Replace); + + // Escape key should be encoded as CSI 27 u (disambiguated from ESC byte) + VERIFY_ARE_EQUAL(wrap(L"\x1b[27u"), process(input, true, VK_ESCAPE, 0x01, 0, 0)); + } + + TEST_METHOD(EnhancementFlags_0x01_Disambiguate_AltLetter) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(DisambiguateEscapeCodes, KittyKeyboardProtocolMode::Replace); + + // Alt+a should be CSI 97;3u (3 = 1 + alt modifier 2) + VERIFY_ARE_EQUAL(wrap(L"\x1b[97;3u"), process(input, true, VK_A, 0x1E, L'a', LEFT_ALT_PRESSED)); + } + + TEST_METHOD(EnhancementFlags_0x01_Disambiguate_CtrlLetter) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(DisambiguateEscapeCodes, KittyKeyboardProtocolMode::Replace); + + // Ctrl+c should be CSI 99;5u (5 = 1 + ctrl modifier 4) + VERIFY_ARE_EQUAL(wrap(L"\x1b[99;5u"), process(input, true, VK_C, 0x2E, 0x03, LEFT_CTRL_PRESSED)); + } + + TEST_METHOD(EnhancementFlags_0x01_Disambiguate_CtrlAltLetter) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(DisambiguateEscapeCodes, KittyKeyboardProtocolMode::Replace); + + // Ctrl+Alt+a should be CSI 97;7u (7 = 1 + ctrl 4 + alt 2) + VERIFY_ARE_EQUAL(wrap(L"\x1b[97;7u"), process(input, true, VK_A, 0x1E, 0, LEFT_CTRL_PRESSED | LEFT_ALT_PRESSED)); + } + + TEST_METHOD(EnhancementFlags_0x01_Disambiguate_ShiftAltLetter) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(DisambiguateEscapeCodes, KittyKeyboardProtocolMode::Replace); + + // Shift+Alt+a should be CSI 97;4u (4 = 1 + shift 1 + alt 2) + VERIFY_ARE_EQUAL(wrap(L"\x1b[97;4u"), process(input, true, VK_A, 0x1E, L'A', SHIFT_PRESSED | LEFT_ALT_PRESSED)); + } + + // Flag Combination 0b00010 (2) - Report event types only + TEST_METHOD(EnhancementFlags_0x02_EventTypes_PressEvent) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(ReportEventTypes, KittyKeyboardProtocolMode::Replace); + + // Press event is default (type 1), no event type suffix needed in output + // Key 'a' press with event types should be CSI 97;1:1u or just CSI 97u (press is default) + auto result = process(input, true, VK_A, 0x1E, L'a', 0); + // With only ReportEventTypes, text keys may still produce text + } + + TEST_METHOD(EnhancementFlags_0x02_EventTypes_ReleaseEvent_FunctionalKey) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(ReportEventTypes, KittyKeyboardProtocolMode::Replace); + + // Release event (type 3) for functional keys + // First send press + process(input, true, VK_F1, 0x3B, 0, 0); + // Then send release - should have event type :3 + VERIFY_ARE_EQUAL(wrap(L"\x1b[1;1:3P"), process(input, false, VK_F1, 0x3B, 0, 0)); + } + + // Flag Combination 0b00011 (3) - Disambiguate + Event types + TEST_METHOD(EnhancementFlags_0x03_DisambiguateAndEventTypes_RepeatEvent) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(DisambiguateEscapeCodes | ReportEventTypes, KittyKeyboardProtocolMode::Replace); + + // First press of 'a' + process(input, true, VK_A, 0x1E, L'a', 0); + // Repeat press of 'a' - should have event type :2 + VERIFY_ARE_EQUAL(wrap(L"\x1b[97;1:2u"), process(input, true, VK_A, 0x1E, L'a', 0)); + } + + TEST_METHOD(EnhancementFlags_0x03_DisambiguateAndEventTypes_ReleaseEvent) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(DisambiguateEscapeCodes | ReportEventTypes, KittyKeyboardProtocolMode::Replace); + + // Press then release of 'a' + process(input, true, VK_A, 0x1E, L'a', 0); + VERIFY_ARE_EQUAL(wrap(L"\x1b[97;1:3u"), process(input, false, VK_A, 0x1E, L'a', 0)); + } + + // Flag Combination 0b00100 (4) - Report alternate keys only + TEST_METHOD(EnhancementFlags_0x04_AlternateKeys_ShiftedKey) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(ReportAlternateKeys, KittyKeyboardProtocolMode::Replace); + + // With alternate keys, shift+a should report the shifted key (A = 65) + // Format: CSI unicode-key-code:shifted-key u + // Note: alternate keys only affects keys already encoded as escape codes + auto result = process(input, true, VK_A, 0x1E, L'A', SHIFT_PRESSED); + // Without disambiguate, this may just produce 'A' + } + + // Flag Combination 0b00101 (5) - Disambiguate + Alternate keys + TEST_METHOD(EnhancementFlags_0x05_DisambiguateAndAlternate_ShiftedKey) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(DisambiguateEscapeCodes | ReportAlternateKeys, KittyKeyboardProtocolMode::Replace); + + // Shift+a with alternate keys: CSI 97:65;2u + // 97 = 'a', 65 = 'A' (shifted key), 2 = 1 + shift(1) + VERIFY_ARE_EQUAL(wrap(L"\x1b[97:65;2u"), process(input, true, VK_A, 0x1E, L'A', SHIFT_PRESSED)); + } + + TEST_METHOD(EnhancementFlags_0x05_DisambiguateAndAlternate_BaseLayoutKey) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(DisambiguateEscapeCodes | ReportAlternateKeys, KittyKeyboardProtocolMode::Replace); + + // Ctrl+a with alternate keys should include base layout key + // Format: CSI 97::base-layout u (empty shifted key, only base layout) + VERIFY_ARE_EQUAL(wrap(L"\x1b[97;5u"), process(input, true, VK_A, 0x1E, 0x01, LEFT_CTRL_PRESSED)); + } + + // Flag Combination 0b00110 (6) - Event types + Alternate keys + TEST_METHOD(EnhancementFlags_0x06_EventTypesAndAlternate) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(ReportEventTypes | ReportAlternateKeys, KittyKeyboardProtocolMode::Replace); + + // F1 key release with alternate keys + process(input, true, VK_F1, 0x3B, 0, 0); + VERIFY_ARE_EQUAL(wrap(L"\x1b[1;1:3P"), process(input, false, VK_F1, 0x3B, 0, 0)); + } + + // Flag Combination 0b00111 (7) - Disambiguate + Event types + Alternate keys + TEST_METHOD(EnhancementFlags_0x07_ThreeFlags_ShiftedKeyWithRelease) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(DisambiguateEscapeCodes | ReportEventTypes | ReportAlternateKeys, KittyKeyboardProtocolMode::Replace); + + // Shift+a press: CSI 97:65;2:1u + VERIFY_ARE_EQUAL(wrap(L"\x1b[97:65;2u"), process(input, true, VK_A, 0x1E, L'A', SHIFT_PRESSED)); + // Shift+a release: CSI 97:65;2:3u + VERIFY_ARE_EQUAL(wrap(L"\x1b[97:65;2:3u"), process(input, false, VK_A, 0x1E, L'A', SHIFT_PRESSED)); + } + + // Flag Combination 0b01000 (8) - Report all keys as escape codes + TEST_METHOD(EnhancementFlags_0x08_AllKeysAsEscapeCodes_PlainText) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(ReportAllKeysAsEscapeCodes, KittyKeyboardProtocolMode::Replace); + + // Plain 'a' key should now be encoded as CSI 97u + VERIFY_ARE_EQUAL(wrap(L"\x1b[97u"), process(input, true, VK_A, 0x1E, L'a', 0)); + } + + TEST_METHOD(EnhancementFlags_0x08_AllKeysAsEscapeCodes_EnterKey) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(ReportAllKeysAsEscapeCodes, KittyKeyboardProtocolMode::Replace); + + // Enter key encoded as CSI 13u (not plain CR) + VERIFY_ARE_EQUAL(wrap(L"\x1b[13u"), process(input, true, VK_RETURN, 0x1C, L'\r', 0)); + } + + TEST_METHOD(EnhancementFlags_0x08_AllKeysAsEscapeCodes_TabKey) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(ReportAllKeysAsEscapeCodes, KittyKeyboardProtocolMode::Replace); + + // Tab key encoded as CSI 9u + VERIFY_ARE_EQUAL(wrap(L"\x1b[9u"), process(input, true, VK_TAB_KEY, 0x0F, L'\t', 0)); + } + + TEST_METHOD(EnhancementFlags_0x08_AllKeysAsEscapeCodes_BackspaceKey) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(ReportAllKeysAsEscapeCodes, KittyKeyboardProtocolMode::Replace); + + // Backspace key encoded as CSI 127u + VERIFY_ARE_EQUAL(wrap(L"\x1b[127u"), process(input, true, VK_BACK, 0x0E, 0x7F, 0)); + } + + TEST_METHOD(EnhancementFlags_0x08_AllKeysAsEscapeCodes_ModifierKey) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(ReportAllKeysAsEscapeCodes, KittyKeyboardProtocolMode::Replace); + + // Left Shift key press should be reported as CSI 57441;2u + // 57441 = LEFT_SHIFT functional key code, 2 = 1 + shift(1) + VERIFY_ARE_EQUAL(wrap(L"\x1b[57441;2u"), process(input, true, VK_LSHIFT, 0x2A, 0, SHIFT_PRESSED)); + } + + // Flag Combination 0b01001 (9) - Disambiguate + All keys as escape codes + TEST_METHOD(EnhancementFlags_0x09_DisambiguateAndAllKeys) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(DisambiguateEscapeCodes | ReportAllKeysAsEscapeCodes, KittyKeyboardProtocolMode::Replace); + + // Both flags together - 'a' encoded as CSI 97u + VERIFY_ARE_EQUAL(wrap(L"\x1b[97u"), process(input, true, VK_A, 0x1E, L'a', 0)); + } + + // Flag Combination 0b01010 (10) - Event types + All keys as escape codes + TEST_METHOD(EnhancementFlags_0x0A_EventTypesAndAllKeys_EnterRelease) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(ReportEventTypes | ReportAllKeysAsEscapeCodes, KittyKeyboardProtocolMode::Replace); + + // With AllKeysAsEscapeCodes, Enter DOES report release events + process(input, true, VK_RETURN, 0x1C, L'\r', 0); + VERIFY_ARE_EQUAL(wrap(L"\x1b[13;1:3u"), process(input, false, VK_RETURN, 0x1C, L'\r', 0)); + } + + TEST_METHOD(EnhancementFlags_0x0A_EventTypesAndAllKeys_TabRelease) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(ReportEventTypes | ReportAllKeysAsEscapeCodes, KittyKeyboardProtocolMode::Replace); + + // With AllKeysAsEscapeCodes, Tab DOES report release events + process(input, true, VK_TAB_KEY, 0x0F, L'\t', 0); + VERIFY_ARE_EQUAL(wrap(L"\x1b[9;1:3u"), process(input, false, VK_TAB_KEY, 0x0F, L'\t', 0)); + } + + TEST_METHOD(EnhancementFlags_0x0A_EventTypesAndAllKeys_BackspaceRelease) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(ReportEventTypes | ReportAllKeysAsEscapeCodes, KittyKeyboardProtocolMode::Replace); + + // With AllKeysAsEscapeCodes, Backspace DOES report release events + process(input, true, VK_BACK, 0x0E, 0x7F, 0); + VERIFY_ARE_EQUAL(wrap(L"\x1b[127;1:3u"), process(input, false, VK_BACK, 0x0E, 0x7F, 0)); + } + + // Flag Combination 0b01011 (11) - Disambiguate + Event types + All keys + TEST_METHOD(EnhancementFlags_0x0B_ThreeFlags_PlainKeyRepeat) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(DisambiguateEscapeCodes | ReportEventTypes | ReportAllKeysAsEscapeCodes, KittyKeyboardProtocolMode::Replace); + + // Press then repeat of plain 'a' + process(input, true, VK_A, 0x1E, L'a', 0); + VERIFY_ARE_EQUAL(wrap(L"\x1b[97;1:2u"), process(input, true, VK_A, 0x1E, L'a', 0)); // repeat + } + + // Flag Combination 0b01100 (12) - Alternate keys + All keys as escape codes + TEST_METHOD(EnhancementFlags_0x0C_AlternateAndAllKeys) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(ReportAlternateKeys | ReportAllKeysAsEscapeCodes, KittyKeyboardProtocolMode::Replace); + + // Shift+a with alternate keys: CSI 97:65;2u + VERIFY_ARE_EQUAL(wrap(L"\x1b[97:65;2u"), process(input, true, VK_A, 0x1E, L'A', SHIFT_PRESSED)); + } + + // Flag Combination 0b01101 (13) - Disambiguate + Alternate + All keys + TEST_METHOD(EnhancementFlags_0x0D_ThreeFlags) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(DisambiguateEscapeCodes | ReportAlternateKeys | ReportAllKeysAsEscapeCodes, KittyKeyboardProtocolMode::Replace); + + // Plain 'a' + VERIFY_ARE_EQUAL(wrap(L"\x1b[97u"), process(input, true, VK_A, 0x1E, L'a', 0)); + } + + // Flag Combination 0b01110 (14) - Event types + Alternate + All keys + TEST_METHOD(EnhancementFlags_0x0E_ThreeFlags) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(ReportEventTypes | ReportAlternateKeys | ReportAllKeysAsEscapeCodes, KittyKeyboardProtocolMode::Replace); + + // Shift+a release with alternate keys + process(input, true, VK_A, 0x1E, L'A', SHIFT_PRESSED); + VERIFY_ARE_EQUAL(wrap(L"\x1b[97:65;2:3u"), process(input, false, VK_A, 0x1E, L'A', SHIFT_PRESSED)); + } + + // Flag Combination 0b01111 (15) - Disambiguate + Event types + Alternate + All keys + TEST_METHOD(EnhancementFlags_0x0F_FourFlags) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(DisambiguateEscapeCodes | ReportEventTypes | ReportAlternateKeys | ReportAllKeysAsEscapeCodes, KittyKeyboardProtocolMode::Replace); + + // Full combination test + process(input, true, VK_A, 0x1E, L'a', 0); + VERIFY_ARE_EQUAL(wrap(L"\x1b[97;1:2u"), process(input, true, VK_A, 0x1E, L'a', 0)); // repeat + VERIFY_ARE_EQUAL(wrap(L"\x1b[97;1:3u"), process(input, false, VK_A, 0x1E, L'a', 0)); // release + } + + // Flag Combination 0b10000 (16) - Report associated text only + TEST_METHOD(EnhancementFlags_0x10_AssociatedText_NoEffect) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(ReportAssociatedText, KittyKeyboardProtocolMode::Replace); + + // Associated text without AllKeysAsEscapeCodes is undefined behavior + // The spec says it's undefined if used without ReportAllKeysAsEscapeCodes + auto result = process(input, true, VK_A, 0x1E, L'a', 0); + // Implementation defined behavior + } + + // Flag Combination 0b10001 (17) - Disambiguate + Associated text + TEST_METHOD(EnhancementFlags_0x11_DisambiguateAndText) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(DisambiguateEscapeCodes | ReportAssociatedText, KittyKeyboardProtocolMode::Replace); + + // Still undefined without AllKeysAsEscapeCodes for text keys + auto result = process(input, true, VK_A, 0x1E, L'a', 0); + } + + // Flag Combination 0b10010 (18) - Event types + Associated text + TEST_METHOD(EnhancementFlags_0x12_EventTypesAndText) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(ReportEventTypes | ReportAssociatedText, KittyKeyboardProtocolMode::Replace); + + auto result = process(input, true, VK_F1, 0x3B, 0, 0); + // Functional keys work with event types + } + + // Flag Combination 0b10011 (19) - Disambiguate + Event types + Associated text + TEST_METHOD(EnhancementFlags_0x13_ThreeFlags) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(DisambiguateEscapeCodes | ReportEventTypes | ReportAssociatedText, KittyKeyboardProtocolMode::Replace); + + // Ctrl+a release + process(input, true, VK_A, 0x1E, 0x01, LEFT_CTRL_PRESSED); + VERIFY_ARE_EQUAL(wrap(L"\x1b[97;5:3u"), process(input, false, VK_A, 0x1E, 0x01, LEFT_CTRL_PRESSED)); + } + + // Flag Combination 0b10100 (20) - Alternate keys + Associated text + TEST_METHOD(EnhancementFlags_0x14_AlternateAndText) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(ReportAlternateKeys | ReportAssociatedText, KittyKeyboardProtocolMode::Replace); + + auto result = process(input, true, VK_A, 0x1E, L'a', 0); + } + + // Flag Combination 0b10101 (21) - Disambiguate + Alternate + Associated text + TEST_METHOD(EnhancementFlags_0x15_ThreeFlags) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(DisambiguateEscapeCodes | ReportAlternateKeys | ReportAssociatedText, KittyKeyboardProtocolMode::Replace); + + // Shift+a with alternate and text + auto result = process(input, true, VK_A, 0x1E, L'A', SHIFT_PRESSED); + } + + // Flag Combination 0b10110 (22) - Event types + Alternate + Associated text + TEST_METHOD(EnhancementFlags_0x16_ThreeFlags) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(ReportEventTypes | ReportAlternateKeys | ReportAssociatedText, KittyKeyboardProtocolMode::Replace); + + auto result = process(input, true, VK_F1, 0x3B, 0, 0); + } + + // Flag Combination 0b10111 (23) - Disambiguate + Event types + Alternate + Associated text + TEST_METHOD(EnhancementFlags_0x17_FourFlags) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(DisambiguateEscapeCodes | ReportEventTypes | ReportAlternateKeys | ReportAssociatedText, KittyKeyboardProtocolMode::Replace); + + // Shift+a with full reporting (except AllKeys) + VERIFY_ARE_EQUAL(wrap(L"\x1b[97:65;2u"), process(input, true, VK_A, 0x1E, L'A', SHIFT_PRESSED)); + } + + // Flag Combination 0b11000 (24) - All keys + Associated text + TEST_METHOD(EnhancementFlags_0x18_AllKeysAndText_SimpleKey) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(ReportAllKeysAsEscapeCodes | ReportAssociatedText, KittyKeyboardProtocolMode::Replace); + + // With both flags: CSI 97;;97u (key 97, no modifiers, text 97) + VERIFY_ARE_EQUAL(wrap(L"\x1b[97;;97u"), process(input, true, VK_A, 0x1E, L'a', 0)); + } + + TEST_METHOD(EnhancementFlags_0x18_AllKeysAndText_ShiftKey) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(ReportAllKeysAsEscapeCodes | ReportAssociatedText, KittyKeyboardProtocolMode::Replace); + + // Shift+a: CSI 97;2;65u (key 97, modifier 2, text 'A'=65) + VERIFY_ARE_EQUAL(wrap(L"\x1b[97;2;65u"), process(input, true, VK_A, 0x1E, L'A', SHIFT_PRESSED)); + } + + // Flag Combination 0b11001 (25) - Disambiguate + All keys + Associated text + TEST_METHOD(EnhancementFlags_0x19_ThreeFlags) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(DisambiguateEscapeCodes | ReportAllKeysAsEscapeCodes | ReportAssociatedText, KittyKeyboardProtocolMode::Replace); + + // Same as 0x18 since disambiguate is implied by AllKeys + VERIFY_ARE_EQUAL(wrap(L"\x1b[97;;97u"), process(input, true, VK_A, 0x1E, L'a', 0)); + } + + // Flag Combination 0b11010 (26) - Event types + All keys + Associated text + TEST_METHOD(EnhancementFlags_0x1A_ThreeFlags_KeyRelease) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(ReportEventTypes | ReportAllKeysAsEscapeCodes | ReportAssociatedText, KittyKeyboardProtocolMode::Replace); + + // Press: CSI 97;1:1;97u or CSI 97;;97u (press is default) + process(input, true, VK_A, 0x1E, L'a', 0); + // Release: CSI 97;1:3;97u (no text on release per spec? - implementation may vary) + auto result = process(input, false, VK_A, 0x1E, L'a', 0); + // Text may or may not be present on release + } + + // Flag Combination 0b11011 (27) - Disambiguate + Event types + All keys + Associated text + TEST_METHOD(EnhancementFlags_0x1B_FourFlags) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(DisambiguateEscapeCodes | ReportEventTypes | ReportAllKeysAsEscapeCodes | ReportAssociatedText, KittyKeyboardProtocolMode::Replace); + + // Full tracking with text + VERIFY_ARE_EQUAL(wrap(L"\x1b[97;;97u"), process(input, true, VK_A, 0x1E, L'a', 0)); + } + + // Flag Combination 0b11100 (28) - Alternate + All keys + Associated text + TEST_METHOD(EnhancementFlags_0x1C_ThreeFlags) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(ReportAlternateKeys | ReportAllKeysAsEscapeCodes | ReportAssociatedText, KittyKeyboardProtocolMode::Replace); + + // Shift+a: CSI 97:65;2;65u + VERIFY_ARE_EQUAL(wrap(L"\x1b[97:65;2;65u"), process(input, true, VK_A, 0x1E, L'A', SHIFT_PRESSED)); + } + + // Flag Combination 0b11101 (29) - Disambiguate + Alternate + All keys + Associated text + TEST_METHOD(EnhancementFlags_0x1D_FourFlags) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(DisambiguateEscapeCodes | ReportAlternateKeys | ReportAllKeysAsEscapeCodes | ReportAssociatedText, KittyKeyboardProtocolMode::Replace); + + VERIFY_ARE_EQUAL(wrap(L"\x1b[97:65;2;65u"), process(input, true, VK_A, 0x1E, L'A', SHIFT_PRESSED)); + } + + // Flag Combination 0b11110 (30) - Event types + Alternate + All keys + Associated text + TEST_METHOD(EnhancementFlags_0x1E_FourFlags) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(ReportEventTypes | ReportAlternateKeys | ReportAllKeysAsEscapeCodes | ReportAssociatedText, KittyKeyboardProtocolMode::Replace); + + // Press with repeat + process(input, true, VK_A, 0x1E, L'a', 0); + VERIFY_ARE_EQUAL(wrap(L"\x1b[97;1:2;97u"), process(input, true, VK_A, 0x1E, L'a', 0)); // repeat + } + + // Flag Combination 0b11111 (31) - All flags enabled + TEST_METHOD(EnhancementFlags_0x1F_AllFlags_FullSequence) + { + TerminalInput input; + input.SetKittyKeyboardProtocol( + DisambiguateEscapeCodes | ReportEventTypes | ReportAlternateKeys | + ReportAllKeysAsEscapeCodes | ReportAssociatedText, + KittyKeyboardProtocolMode::Replace); + + // Full sequence: CSI unicode-key-code:alternate-key-codes ; modifiers:event-type ; text-as-codepoints u + // Press 'a': CSI 97;;97u + VERIFY_ARE_EQUAL(wrap(L"\x1b[97;;97u"), process(input, true, VK_A, 0x1E, L'a', 0)); + // Repeat 'a': CSI 97;1:2;97u + VERIFY_ARE_EQUAL(wrap(L"\x1b[97;1:2;97u"), process(input, true, VK_A, 0x1E, L'a', 0)); + // Release 'a': CSI 97;1:3u (no text on release) + VERIFY_ARE_EQUAL(wrap(L"\x1b[97;1:3u"), process(input, false, VK_A, 0x1E, L'a', 0)); + } + + TEST_METHOD(EnhancementFlags_0x1F_AllFlags_ShiftedKey) + { + TerminalInput input; + input.SetKittyKeyboardProtocol( + DisambiguateEscapeCodes | ReportEventTypes | ReportAlternateKeys | + ReportAllKeysAsEscapeCodes | ReportAssociatedText, + KittyKeyboardProtocolMode::Replace); + + // Shift+a: CSI 97:65;2;65u + VERIFY_ARE_EQUAL(wrap(L"\x1b[97:65;2;65u"), process(input, true, VK_A, 0x1E, L'A', SHIFT_PRESSED)); + } + + // ========================================================================= + // SECTION 2: Modifier Combinations + // Test the bit field encoding: modifiers value = 1 + actual modifiers + // ========================================================================= + + TEST_METHOD(Modifiers_Shift_Encoding) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(DisambiguateEscapeCodes, KittyKeyboardProtocolMode::Replace); + + // Shift only: modifier = 1 + 1 = 2 + VERIFY_ARE_EQUAL(wrap(L"\x1b[97;2u"), process(input, true, VK_A, 0x1E, L'A', SHIFT_PRESSED)); + } + + TEST_METHOD(Modifiers_Alt_Encoding) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(DisambiguateEscapeCodes, KittyKeyboardProtocolMode::Replace); + + // Alt only: modifier = 1 + 2 = 3 + VERIFY_ARE_EQUAL(wrap(L"\x1b[97;3u"), process(input, true, VK_A, 0x1E, L'a', LEFT_ALT_PRESSED)); + } + + TEST_METHOD(Modifiers_Ctrl_Encoding) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(DisambiguateEscapeCodes, KittyKeyboardProtocolMode::Replace); + + // Ctrl only: modifier = 1 + 4 = 5 + VERIFY_ARE_EQUAL(wrap(L"\x1b[97;5u"), process(input, true, VK_A, 0x1E, 0x01, LEFT_CTRL_PRESSED)); + } + + TEST_METHOD(Modifiers_ShiftAlt_Encoding) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(DisambiguateEscapeCodes, KittyKeyboardProtocolMode::Replace); + + // Shift+Alt: modifier = 1 + 1 + 2 = 4 + VERIFY_ARE_EQUAL(wrap(L"\x1b[97;4u"), process(input, true, VK_A, 0x1E, L'A', SHIFT_PRESSED | LEFT_ALT_PRESSED)); + } + + TEST_METHOD(Modifiers_ShiftCtrl_Encoding) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(DisambiguateEscapeCodes, KittyKeyboardProtocolMode::Replace); + + // Shift+Ctrl: modifier = 1 + 1 + 4 = 6 + VERIFY_ARE_EQUAL(wrap(L"\x1b[97;6u"), process(input, true, VK_A, 0x1E, 0x01, SHIFT_PRESSED | LEFT_CTRL_PRESSED)); + } + + TEST_METHOD(Modifiers_AltCtrl_Encoding) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(DisambiguateEscapeCodes, KittyKeyboardProtocolMode::Replace); + + // Alt+Ctrl: modifier = 1 + 2 + 4 = 7 + VERIFY_ARE_EQUAL(wrap(L"\x1b[97;7u"), process(input, true, VK_A, 0x1E, 0x01, LEFT_ALT_PRESSED | LEFT_CTRL_PRESSED)); + } + + TEST_METHOD(Modifiers_ShiftAltCtrl_Encoding) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(DisambiguateEscapeCodes, KittyKeyboardProtocolMode::Replace); + + // Shift+Alt+Ctrl: modifier = 1 + 1 + 2 + 4 = 8 + VERIFY_ARE_EQUAL(wrap(L"\x1b[97;8u"), process(input, true, VK_A, 0x1E, 0x01, SHIFT_PRESSED | LEFT_ALT_PRESSED | LEFT_CTRL_PRESSED)); + } + + TEST_METHOD(Modifiers_CapsLock_OnlyWithAllKeys) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(ReportAllKeysAsEscapeCodes, KittyKeyboardProtocolMode::Replace); + + // Caps Lock: modifier = 1 + 64 = 65 + // Lock modifiers only reported with ReportAllKeysAsEscapeCodes + VERIFY_ARE_EQUAL(wrap(L"\x1b[97;65u"), process(input, true, VK_A, 0x1E, L'a', CAPSLOCK_ON)); + } + + TEST_METHOD(Modifiers_NumLock_OnlyWithAllKeys) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(ReportAllKeysAsEscapeCodes, KittyKeyboardProtocolMode::Replace); + + // Num Lock: modifier = 1 + 128 = 129 + VERIFY_ARE_EQUAL(wrap(L"\x1b[97;129u"), process(input, true, VK_A, 0x1E, L'a', NUMLOCK_ON)); + } + + TEST_METHOD(Modifiers_CapsLockAndNumLock_Together) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(ReportAllKeysAsEscapeCodes, KittyKeyboardProtocolMode::Replace); + + // Caps+Num Lock: modifier = 1 + 64 + 128 = 193 + VERIFY_ARE_EQUAL(wrap(L"\x1b[97;193u"), process(input, true, VK_A, 0x1E, L'a', CAPSLOCK_ON | NUMLOCK_ON)); + } + + TEST_METHOD(Modifiers_LocksNotReportedForTextKeys_WithoutAllKeys) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(DisambiguateEscapeCodes, KittyKeyboardProtocolMode::Replace); + + // Without ReportAllKeysAsEscapeCodes, lock modifiers are NOT reported for text keys + // This should NOT include the caps_lock bit + // Plain 'a' with caps lock should still just be CSI 97u (no modifier) + // But actually with Disambiguate, only modified keys are CSI u encoded + // So we use Alt to force encoding + VERIFY_ARE_EQUAL(wrap(L"\x1b[97;3u"), process(input, true, VK_A, 0x1E, L'a', LEFT_ALT_PRESSED | CAPSLOCK_ON)); + // Note: caps_lock bit 64 is NOT included, so it's 3 (1+2) not 67 (1+2+64) + } + + // ========================================================================= + // SECTION 3: Event Types (press, repeat, release) + // ========================================================================= + + TEST_METHOD(EventTypes_Press_IsDefault) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(ReportEventTypes | ReportAllKeysAsEscapeCodes, KittyKeyboardProtocolMode::Replace); + + // Press event (type 1) is default, may be omitted + auto result = process(input, true, VK_A, 0x1E, L'a', 0); + // Could be CSI 97u or CSI 97;1:1u - press is default so type can be omitted + } + + TEST_METHOD(EventTypes_Repeat_Type2) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(ReportEventTypes | ReportAllKeysAsEscapeCodes, KittyKeyboardProtocolMode::Replace); + + // First press + process(input, true, VK_A, 0x1E, L'a', 0); + // Second press without release = repeat (type 2) + VERIFY_ARE_EQUAL(wrap(L"\x1b[97;1:2u"), process(input, true, VK_A, 0x1E, L'a', 0)); + } + + TEST_METHOD(EventTypes_Release_Type3) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(ReportEventTypes | ReportAllKeysAsEscapeCodes, KittyKeyboardProtocolMode::Replace); + + // Press then release + process(input, true, VK_A, 0x1E, L'a', 0); + VERIFY_ARE_EQUAL(wrap(L"\x1b[97;1:3u"), process(input, false, VK_A, 0x1E, L'a', 0)); + } + + TEST_METHOD(EventTypes_ModifierOnRelease_MustBePresent) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(ReportEventTypes | ReportAllKeysAsEscapeCodes, KittyKeyboardProtocolMode::Replace); + + // When modifier key is released, the modifier bit must still be set + // (the release event state includes the key being released) + process(input, true, VK_LSHIFT, 0x2A, 0, SHIFT_PRESSED); + VERIFY_ARE_EQUAL(wrap(L"\x1b[57441;2:3u"), process(input, false, VK_LSHIFT, 0x2A, 0, SHIFT_PRESSED)); + } + + TEST_METHOD(EventTypes_ModifierOnRelease_ResetWhenBothReleased) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(ReportEventTypes | ReportAllKeysAsEscapeCodes, KittyKeyboardProtocolMode::Replace); + + // When both shifts pressed, releasing one keeps shift bit + // Press left shift + process(input, true, VK_LSHIFT, 0x2A, 0, SHIFT_PRESSED); + // Press right shift + process(input, true, VK_RSHIFT, 0x36, 0, SHIFT_PRESSED); + // Release left shift - shift bit still set (right is held) + VERIFY_ARE_EQUAL(wrap(L"\x1b[57441;2:3u"), process(input, false, VK_LSHIFT, 0x2A, 0, SHIFT_PRESSED)); + } + + // ========================================================================= + // SECTION 4: Special Key Behaviors + // Enter, Tab, Backspace have special handling for release events + // ========================================================================= + + TEST_METHOD(SpecialKeys_Enter_NoReleaseWithoutAllKeys) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(DisambiguateEscapeCodes | ReportEventTypes, KittyKeyboardProtocolMode::Replace); + + // Without ReportAllKeysAsEscapeCodes, Enter does NOT report release + process(input, true, VK_RETURN, 0x1C, L'\r', 0); + auto result = process(input, false, VK_RETURN, 0x1C, L'\r', 0); + // Should produce empty/no output on release + VERIFY_IS_TRUE(!result.has_value() || std::get(*result).empty()); + } + + TEST_METHOD(SpecialKeys_Tab_NoReleaseWithoutAllKeys) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(DisambiguateEscapeCodes | ReportEventTypes, KittyKeyboardProtocolMode::Replace); + + // Without ReportAllKeysAsEscapeCodes, Tab does NOT report release + process(input, true, VK_TAB_KEY, 0x0F, L'\t', 0); + auto result = process(input, false, VK_TAB_KEY, 0x0F, L'\t', 0); + VERIFY_IS_TRUE(!result.has_value() || std::get(*result).empty()); + } + + TEST_METHOD(SpecialKeys_Backspace_NoReleaseWithoutAllKeys) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(DisambiguateEscapeCodes | ReportEventTypes, KittyKeyboardProtocolMode::Replace); + + // Without ReportAllKeysAsEscapeCodes, Backspace does NOT report release + process(input, true, VK_BACK, 0x0E, 0x7F, 0); + auto result = process(input, false, VK_BACK, 0x0E, 0x7F, 0); + VERIFY_IS_TRUE(!result.has_value() || std::get(*result).empty()); + } + + TEST_METHOD(SpecialKeys_Escape_Disambiguated) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(DisambiguateEscapeCodes, KittyKeyboardProtocolMode::Replace); + + // Escape key is disambiguated from ESC byte + VERIFY_ARE_EQUAL(wrap(L"\x1b[27u"), process(input, true, VK_ESCAPE, 0x01, 0x1B, 0)); + } + + TEST_METHOD(SpecialKeys_Enter_LegacyBehavior) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(DisambiguateEscapeCodes, KittyKeyboardProtocolMode::Replace); + + // Enter still produces CR in legacy-compatible way when not AllKeys + // Actually with disambiguate it may still be legacy + auto result = process(input, true, VK_RETURN, 0x1C, L'\r', 0); + // Could be 0x0d or CSI 13u depending on implementation + } + + TEST_METHOD(SpecialKeys_Tab_LegacyBehavior) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(DisambiguateEscapeCodes, KittyKeyboardProtocolMode::Replace); + + // Tab still produces HT in legacy-compatible way when not AllKeys + auto result = process(input, true, VK_TAB_KEY, 0x0F, L'\t', 0); + } + + TEST_METHOD(SpecialKeys_Backspace_LegacyBehavior) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(DisambiguateEscapeCodes, KittyKeyboardProtocolMode::Replace); + + // Backspace still produces DEL in legacy-compatible way when not AllKeys + auto result = process(input, true, VK_BACK, 0x0E, 0x7F, 0); + } + + // ========================================================================= + // SECTION 5: Functional Key Definitions + // Test functional keys with their proper CSI codes + // ========================================================================= + + // F1-F4 use SS3 prefix in legacy, CSI with P/Q/R/S final in kitty + TEST_METHOD(FunctionalKeys_F1_Legacy) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(DisambiguateEscapeCodes, KittyKeyboardProtocolMode::Replace); + + // F1 without modifiers uses legacy SS3 P + VERIFY_ARE_EQUAL(wrap(L"\x1bOP"), process(input, true, VK_F1, 0x3B, 0, 0)); + } + + TEST_METHOD(FunctionalKeys_F1_WithModifiers) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(DisambiguateEscapeCodes, KittyKeyboardProtocolMode::Replace); + + // F1 with modifiers uses CSI 1;modifier P + VERIFY_ARE_EQUAL(wrap(L"\x1b[1;2P"), process(input, true, VK_F1, 0x3B, 0, SHIFT_PRESSED)); + } + + TEST_METHOD(FunctionalKeys_F2) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(DisambiguateEscapeCodes, KittyKeyboardProtocolMode::Replace); + + VERIFY_ARE_EQUAL(wrap(L"\x1bOQ"), process(input, true, VK_F2, 0x3C, 0, 0)); + } + + TEST_METHOD(FunctionalKeys_F3) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(DisambiguateEscapeCodes, KittyKeyboardProtocolMode::Replace); + + VERIFY_ARE_EQUAL(wrap(L"\x1bOR"), process(input, true, VK_F3, 0x3D, 0, 0)); + } + + TEST_METHOD(FunctionalKeys_F4) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(DisambiguateEscapeCodes, KittyKeyboardProtocolMode::Replace); + + VERIFY_ARE_EQUAL(wrap(L"\x1bOS"), process(input, true, VK_F4, 0x3E, 0, 0)); + } + + TEST_METHOD(FunctionalKeys_F5) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(DisambiguateEscapeCodes, KittyKeyboardProtocolMode::Replace); + + // F5 uses CSI 15 ~ + VERIFY_ARE_EQUAL(wrap(L"\x1b[15~"), process(input, true, VK_F5, 0x3F, 0, 0)); + } + + TEST_METHOD(FunctionalKeys_F5_WithModifiers) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(DisambiguateEscapeCodes, KittyKeyboardProtocolMode::Replace); + + // F5 with Shift: CSI 15;2 ~ + VERIFY_ARE_EQUAL(wrap(L"\x1b[15;2~"), process(input, true, VK_F5, 0x3F, 0, SHIFT_PRESSED)); + } + + TEST_METHOD(FunctionalKeys_F12) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(DisambiguateEscapeCodes, KittyKeyboardProtocolMode::Replace); + + // F12 uses CSI 24 ~ + VERIFY_ARE_EQUAL(wrap(L"\x1b[24~"), process(input, true, VK_F12, 0x58, 0, 0)); + } + + TEST_METHOD(FunctionalKeys_F13) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(ReportAllKeysAsEscapeCodes, KittyKeyboardProtocolMode::Replace); + + // F13-F35 use CSI u encoding with functional key codes + // F13 = 57376 + VERIFY_ARE_EQUAL(wrap(L"\x1b[57376u"), process(input, true, VK_F13, 0x64, 0, 0)); + } + + TEST_METHOD(FunctionalKeys_F24) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(ReportAllKeysAsEscapeCodes, KittyKeyboardProtocolMode::Replace); + + // F24 = 57387 + VERIFY_ARE_EQUAL(wrap(L"\x1b[57387u"), process(input, true, VK_F24, 0x87, 0, 0)); + } + + // Navigation keys + TEST_METHOD(FunctionalKeys_ArrowUp_Legacy) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(DisambiguateEscapeCodes, KittyKeyboardProtocolMode::Replace); + + // Arrow up: CSI A + VERIFY_ARE_EQUAL(wrap(L"\x1b[A"), process(input, true, VK_UP, 0x48, 0, ENHANCED_KEY)); + } + + TEST_METHOD(FunctionalKeys_ArrowUp_WithModifiers) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(DisambiguateEscapeCodes, KittyKeyboardProtocolMode::Replace); + + // Arrow up with Shift: CSI 1;2 A + VERIFY_ARE_EQUAL(wrap(L"\x1b[1;2A"), process(input, true, VK_UP, 0x48, 0, ENHANCED_KEY | SHIFT_PRESSED)); + } + + TEST_METHOD(FunctionalKeys_ArrowDown) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(DisambiguateEscapeCodes, KittyKeyboardProtocolMode::Replace); + + VERIFY_ARE_EQUAL(wrap(L"\x1b[B"), process(input, true, VK_DOWN, 0x50, 0, ENHANCED_KEY)); + } + + TEST_METHOD(FunctionalKeys_ArrowLeft) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(DisambiguateEscapeCodes, KittyKeyboardProtocolMode::Replace); + + VERIFY_ARE_EQUAL(wrap(L"\x1b[D"), process(input, true, VK_LEFT, 0x4B, 0, ENHANCED_KEY)); + } + + TEST_METHOD(FunctionalKeys_ArrowRight) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(DisambiguateEscapeCodes, KittyKeyboardProtocolMode::Replace); + + VERIFY_ARE_EQUAL(wrap(L"\x1b[C"), process(input, true, VK_RIGHT, 0x4D, 0, ENHANCED_KEY)); + } + + TEST_METHOD(FunctionalKeys_Home) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(DisambiguateEscapeCodes, KittyKeyboardProtocolMode::Replace); + + VERIFY_ARE_EQUAL(wrap(L"\x1b[H"), process(input, true, VK_HOME, 0x47, 0, ENHANCED_KEY)); + } + + TEST_METHOD(FunctionalKeys_End) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(DisambiguateEscapeCodes, KittyKeyboardProtocolMode::Replace); + + VERIFY_ARE_EQUAL(wrap(L"\x1b[F"), process(input, true, VK_END, 0x4F, 0, ENHANCED_KEY)); + } + + TEST_METHOD(FunctionalKeys_Insert) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(DisambiguateEscapeCodes, KittyKeyboardProtocolMode::Replace); + + // Insert: CSI 2 ~ + VERIFY_ARE_EQUAL(wrap(L"\x1b[2~"), process(input, true, VK_INSERT, 0x52, 0, ENHANCED_KEY)); + } + + TEST_METHOD(FunctionalKeys_Delete) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(DisambiguateEscapeCodes, KittyKeyboardProtocolMode::Replace); + + // Delete: CSI 3 ~ + VERIFY_ARE_EQUAL(wrap(L"\x1b[3~"), process(input, true, VK_DELETE, 0x53, 0, ENHANCED_KEY)); + } + + TEST_METHOD(FunctionalKeys_PageUp) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(DisambiguateEscapeCodes, KittyKeyboardProtocolMode::Replace); + + // PageUp: CSI 5 ~ + VERIFY_ARE_EQUAL(wrap(L"\x1b[5~"), process(input, true, VK_PRIOR, 0x49, 0, ENHANCED_KEY)); + } + + TEST_METHOD(FunctionalKeys_PageDown) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(DisambiguateEscapeCodes, KittyKeyboardProtocolMode::Replace); + + // PageDown: CSI 6 ~ + VERIFY_ARE_EQUAL(wrap(L"\x1b[6~"), process(input, true, VK_NEXT, 0x51, 0, ENHANCED_KEY)); + } + + // ========================================================================= + // SECTION 6: Keypad Keys (with ENHANCED_KEY differentiation) + // ========================================================================= + + TEST_METHOD(KeypadKeys_Numpad0_WithAllKeys) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(ReportAllKeysAsEscapeCodes, KittyKeyboardProtocolMode::Replace); + + // KP_0 = 57399 + VERIFY_ARE_EQUAL(wrap(L"\x1b[57399u"), process(input, true, VK_NUMPAD0, 0x52, L'0', 0)); + } + + TEST_METHOD(KeypadKeys_NumpadAdd) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(ReportAllKeysAsEscapeCodes, KittyKeyboardProtocolMode::Replace); + + // KP_ADD = 57413 + VERIFY_ARE_EQUAL(wrap(L"\x1b[57413u"), process(input, true, VK_ADD, 0x4E, L'+', 0)); + } + + TEST_METHOD(KeypadKeys_NumpadSubtract) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(ReportAllKeysAsEscapeCodes, KittyKeyboardProtocolMode::Replace); + + // KP_SUBTRACT = 57412 + VERIFY_ARE_EQUAL(wrap(L"\x1b[57412u"), process(input, true, VK_SUBTRACT, 0x4A, L'-', 0)); + } + + TEST_METHOD(KeypadKeys_NumpadMultiply) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(ReportAllKeysAsEscapeCodes, KittyKeyboardProtocolMode::Replace); + + // KP_MULTIPLY = 57411 + VERIFY_ARE_EQUAL(wrap(L"\x1b[57411u"), process(input, true, VK_MULTIPLY, 0x37, L'*', 0)); + } + + TEST_METHOD(KeypadKeys_NumpadDivide) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(ReportAllKeysAsEscapeCodes, KittyKeyboardProtocolMode::Replace); + + // KP_DIVIDE = 57410 + VERIFY_ARE_EQUAL(wrap(L"\x1b[57410u"), process(input, true, VK_DIVIDE, 0x35, L'/', ENHANCED_KEY)); + } + + TEST_METHOD(KeypadKeys_NumpadDecimal) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(ReportAllKeysAsEscapeCodes, KittyKeyboardProtocolMode::Replace); + + // KP_DECIMAL = 57409 + VERIFY_ARE_EQUAL(wrap(L"\x1b[57409u"), process(input, true, VK_DECIMAL, 0x53, L'.', 0)); + } + + TEST_METHOD(KeypadKeys_NumpadEnter) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(ReportAllKeysAsEscapeCodes, KittyKeyboardProtocolMode::Replace); + + // KP_ENTER = 57414 + // Numpad Enter has ENHANCED_KEY flag + VERIFY_ARE_EQUAL(wrap(L"\x1b[57414u"), process(input, true, VK_RETURN, 0x1C, L'\r', ENHANCED_KEY)); + } + + // Navigation keys on numpad (without NumLock) + TEST_METHOD(KeypadKeys_NumpadHome) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(ReportAllKeysAsEscapeCodes, KittyKeyboardProtocolMode::Replace); + + // KP_HOME = 57423 (Home on numpad without ENHANCED_KEY) + VERIFY_ARE_EQUAL(wrap(L"\x1b[57423u"), process(input, true, VK_HOME, 0x47, 0, 0)); + } + + TEST_METHOD(KeypadKeys_NumpadUp) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(ReportAllKeysAsEscapeCodes, KittyKeyboardProtocolMode::Replace); + + // KP_UP = 57419 (Up on numpad without ENHANCED_KEY) + VERIFY_ARE_EQUAL(wrap(L"\x1b[57419u"), process(input, true, VK_UP, 0x48, 0, 0)); + } + + // ========================================================================= + // SECTION 7: Modifier Keys + // ========================================================================= + + TEST_METHOD(ModifierKeys_LeftShift) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(ReportAllKeysAsEscapeCodes, KittyKeyboardProtocolMode::Replace); + + // LEFT_SHIFT = 57441 + // When pressing shift, the shift modifier bit must be set + VERIFY_ARE_EQUAL(wrap(L"\x1b[57441;2u"), process(input, true, VK_LSHIFT, 0x2A, 0, SHIFT_PRESSED)); + } + + TEST_METHOD(ModifierKeys_RightShift) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(ReportAllKeysAsEscapeCodes, KittyKeyboardProtocolMode::Replace); + + // RIGHT_SHIFT = 57447 + VERIFY_ARE_EQUAL(wrap(L"\x1b[57447;2u"), process(input, true, VK_RSHIFT, 0x36, 0, SHIFT_PRESSED)); + } + + TEST_METHOD(ModifierKeys_LeftControl) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(ReportAllKeysAsEscapeCodes, KittyKeyboardProtocolMode::Replace); + + // LEFT_CONTROL = 57442 + // When pressing ctrl, the ctrl modifier bit must be set + VERIFY_ARE_EQUAL(wrap(L"\x1b[57442;5u"), process(input, true, VK_LCONTROL, 0x1D, 0, LEFT_CTRL_PRESSED)); + } + + TEST_METHOD(ModifierKeys_RightControl) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(ReportAllKeysAsEscapeCodes, KittyKeyboardProtocolMode::Replace); + + // RIGHT_CONTROL = 57448 + VERIFY_ARE_EQUAL(wrap(L"\x1b[57448;5u"), process(input, true, VK_RCONTROL, 0x1D, 0, RIGHT_CTRL_PRESSED | ENHANCED_KEY)); + } + + TEST_METHOD(ModifierKeys_LeftAlt) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(ReportAllKeysAsEscapeCodes, KittyKeyboardProtocolMode::Replace); + + // LEFT_ALT = 57443 + VERIFY_ARE_EQUAL(wrap(L"\x1b[57443;3u"), process(input, true, VK_LMENU, 0x38, 0, LEFT_ALT_PRESSED)); + } + + TEST_METHOD(ModifierKeys_RightAlt) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(ReportAllKeysAsEscapeCodes, KittyKeyboardProtocolMode::Replace); + + // RIGHT_ALT = 57449 + VERIFY_ARE_EQUAL(wrap(L"\x1b[57449;3u"), process(input, true, VK_RMENU, 0x38, 0, RIGHT_ALT_PRESSED | ENHANCED_KEY)); + } + + TEST_METHOD(ModifierKeys_CapsLock) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(ReportAllKeysAsEscapeCodes, KittyKeyboardProtocolMode::Replace); + + // CAPS_LOCK = 57358 + VERIFY_ARE_EQUAL(wrap(L"\x1b[57358u"), process(input, true, VK_CAPITAL, 0x3A, 0, 0)); + } + + TEST_METHOD(ModifierKeys_NumLock) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(ReportAllKeysAsEscapeCodes, KittyKeyboardProtocolMode::Replace); + + // NUM_LOCK = 57360 + VERIFY_ARE_EQUAL(wrap(L"\x1b[57360u"), process(input, true, VK_NUMLOCK, 0x45, 0, ENHANCED_KEY)); + } + + TEST_METHOD(ModifierKeys_ScrollLock) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(ReportAllKeysAsEscapeCodes, KittyKeyboardProtocolMode::Replace); + + // SCROLL_LOCK = 57359 + VERIFY_ARE_EQUAL(wrap(L"\x1b[57359u"), process(input, true, VK_SCROLL, 0x46, 0, 0)); + } + + // ========================================================================= + // SECTION 8: Key Code Encoding (lowercase requirement) + // ========================================================================= + + TEST_METHOD(KeyCodes_AlwaysLowercase) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(DisambiguateEscapeCodes, KittyKeyboardProtocolMode::Replace); + + // Key code must always be lowercase, even with shift + // Shift+a should be CSI 97;2u (not 65) + VERIFY_ARE_EQUAL(wrap(L"\x1b[97;2u"), process(input, true, VK_A, 0x1E, L'A', SHIFT_PRESSED)); + } + + TEST_METHOD(KeyCodes_CtrlShift_StillLowercase) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(DisambiguateEscapeCodes, KittyKeyboardProtocolMode::Replace); + + // Ctrl+Shift+a should still be CSI 97;6u + VERIFY_ARE_EQUAL(wrap(L"\x1b[97;6u"), process(input, true, VK_A, 0x1E, 0x01, SHIFT_PRESSED | LEFT_CTRL_PRESSED)); + } + + // ========================================================================= + // SECTION 9: Text as Codepoints + // ========================================================================= + + TEST_METHOD(TextAsCodepoints_SimpleChar) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(ReportAllKeysAsEscapeCodes | ReportAssociatedText, KittyKeyboardProtocolMode::Replace); + + // 'a' produces text 'a' (97) + VERIFY_ARE_EQUAL(wrap(L"\x1b[97;;97u"), process(input, true, VK_A, 0x1E, L'a', 0)); + } + + TEST_METHOD(TextAsCodepoints_ShiftedChar) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(ReportAllKeysAsEscapeCodes | ReportAssociatedText, KittyKeyboardProtocolMode::Replace); + + // Shift+a produces text 'A' (65) + VERIFY_ARE_EQUAL(wrap(L"\x1b[97;2;65u"), process(input, true, VK_A, 0x1E, L'A', SHIFT_PRESSED)); + } + + TEST_METHOD(TextAsCodepoints_NoTextForNonTextKeys) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(ReportAllKeysAsEscapeCodes | ReportAssociatedText, KittyKeyboardProtocolMode::Replace); + + // Escape doesn't produce text + VERIFY_ARE_EQUAL(wrap(L"\x1b[27u"), process(input, true, VK_ESCAPE, 0x01, 0, 0)); + } + + TEST_METHOD(TextAsCodepoints_NoTextOnRelease) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(ReportEventTypes | ReportAllKeysAsEscapeCodes | ReportAssociatedText, KittyKeyboardProtocolMode::Replace); + + // Text should not be present on release events + process(input, true, VK_A, 0x1E, L'a', 0); + auto releaseResult = process(input, false, VK_A, 0x1E, L'a', 0); + // Release should be CSI 97;1:3u (no text parameter) + VERIFY_ARE_EQUAL(wrap(L"\x1b[97;1:3u"), releaseResult); + } + + // ========================================================================= + // SECTION 10: Protocol Mode Management (Set, Reset, Replace) + // ========================================================================= + + TEST_METHOD(ProtocolMode_Replace) + { + TerminalInput input; + + // Replace mode (1) sets the exact flags + input.SetKittyKeyboardProtocol(DisambiguateEscapeCodes, KittyKeyboardProtocolMode::Replace); + VERIFY_ARE_EQUAL(DisambiguateEscapeCodes, input.GetKittyFlags()); + + // Replace again overwrites + input.SetKittyKeyboardProtocol(ReportEventTypes, KittyKeyboardProtocolMode::Replace); + VERIFY_ARE_EQUAL(ReportEventTypes, input.GetKittyFlags()); + } + + TEST_METHOD(ProtocolMode_Set) + { + TerminalInput input; + + // Start with disambiguate + input.SetKittyKeyboardProtocol(DisambiguateEscapeCodes, KittyKeyboardProtocolMode::Replace); + + // Set mode (2) adds flags without removing existing + input.SetKittyKeyboardProtocol(ReportEventTypes, KittyKeyboardProtocolMode::Set); + VERIFY_ARE_EQUAL(DisambiguateEscapeCodes | ReportEventTypes, input.GetKittyFlags()); + } + + TEST_METHOD(ProtocolMode_Reset) + { + TerminalInput input; + + // Start with multiple flags + input.SetKittyKeyboardProtocol(DisambiguateEscapeCodes | ReportEventTypes | ReportAllKeysAsEscapeCodes, KittyKeyboardProtocolMode::Replace); + + // Reset mode (3) removes specific flags + input.SetKittyKeyboardProtocol(ReportEventTypes, KittyKeyboardProtocolMode::Reset); + VERIFY_ARE_EQUAL(DisambiguateEscapeCodes | ReportAllKeysAsEscapeCodes, input.GetKittyFlags()); + } + + // ========================================================================= + // SECTION 11: Push/Pop Stack Behavior + // ========================================================================= + + TEST_METHOD(Stack_PushPop_Basic) + { + TerminalInput input; + + // Initial state + VERIFY_ARE_EQUAL(0, input.GetKittyFlags()); + + // Push with flags + input.PushKittyFlags(DisambiguateEscapeCodes); + VERIFY_ARE_EQUAL(DisambiguateEscapeCodes, input.GetKittyFlags()); + + // Push another + input.PushKittyFlags(ReportAllKeysAsEscapeCodes); + VERIFY_ARE_EQUAL(ReportAllKeysAsEscapeCodes, input.GetKittyFlags()); + + // Pop once - should restore previous + input.PopKittyFlags(1); + VERIFY_ARE_EQUAL(DisambiguateEscapeCodes, input.GetKittyFlags()); + + // Pop again - should restore initial (0) + input.PopKittyFlags(1); + VERIFY_ARE_EQUAL(0, input.GetKittyFlags()); + } + + TEST_METHOD(Stack_Pop_EmptiesStack_ResetsAllFlags) + { + TerminalInput input; + + input.PushKittyFlags(DisambiguateEscapeCodes); + input.PushKittyFlags(ReportEventTypes); + + // Pop more than stack size - should reset to 0 + input.PopKittyFlags(10); + VERIFY_ARE_EQUAL(0, input.GetKittyFlags()); + } + + TEST_METHOD(Stack_MainAndAlternate_Independent) + { + TerminalInput input; + + // Set flags in main screen + input.SetKittyKeyboardProtocol(DisambiguateEscapeCodes, KittyKeyboardProtocolMode::Replace); + input.PushKittyFlags(ReportEventTypes); + + // Switch to alternate screen - flags should reset for alternate + input.UseAlternateScreenBuffer(); + VERIFY_ARE_EQUAL(0, input.GetKittyFlags()); + + // Set different flags in alternate + input.SetKittyKeyboardProtocol(ReportAllKeysAsEscapeCodes, KittyKeyboardProtocolMode::Replace); + VERIFY_ARE_EQUAL(ReportAllKeysAsEscapeCodes, input.GetKittyFlags()); + + // Switch back to main - should restore main screen state + input.UseMainScreenBuffer(); + VERIFY_ARE_EQUAL(ReportEventTypes, input.GetKittyFlags()); + } + + // ========================================================================= + // SECTION 12: Surrogate Pair Handling + // ========================================================================= + + TEST_METHOD(SurrogatePairs_LeadingSurrogate_Buffered) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(ReportAllKeysAsEscapeCodes | ReportAssociatedText, KittyKeyboardProtocolMode::Replace); + + // Leading surrogate alone should produce no output + auto result = process(input, true, 0, 0, 0xD83D, 0); // Leading surrogate of ๐Ÿ˜€ + VERIFY_IS_TRUE(!result.has_value() || std::get(*result).empty()); + } + + TEST_METHOD(SurrogatePairs_Complete_Emoji) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(ReportAllKeysAsEscapeCodes | ReportAssociatedText, KittyKeyboardProtocolMode::Replace); + + // Leading surrogate (buffered) + process(input, true, 0, 0, 0xD83D, 0); + // Trailing surrogate completes the pair + // ๐Ÿ˜€ = U+1F600 = 128512 + auto result = process(input, true, 0, 0, 0xDE00, 0); + // Should produce CSI 128512;;128512u + VERIFY_ARE_EQUAL(wrap(L"\x1b[128512;;128512u"), result); + } + + // ========================================================================= + // SECTION 13: Edge Cases and Special Scenarios + // ========================================================================= + + TEST_METHOD(EdgeCase_VK_PACKET_PassThrough) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(ReportAllKeysAsEscapeCodes, KittyKeyboardProtocolMode::Replace); + + // VK_PACKET (0xE7) should pass through the unicode char directly + // This is used for synthesized keyboard events + auto result = process(input, true, 0xE7, 0, L'x', 0); + // The implementation may handle this specially + } + + TEST_METHOD(EdgeCase_ZeroVirtualKey_PassThrough) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(ReportAllKeysAsEscapeCodes, KittyKeyboardProtocolMode::Replace); + + // Zero virtual key with a unicode char should pass through + auto result = process(input, true, 0, 0, L'y', 0); + } + + TEST_METHOD(EdgeCase_AutoRepeat_Disabled) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(ReportAllKeysAsEscapeCodes, KittyKeyboardProtocolMode::Replace); + input.SetInputMode(TerminalInput::Mode::AutoRepeat, false); + + // First press + auto first = process(input, true, VK_A, 0x1E, L'a', 0); + VERIFY_IS_TRUE(first.has_value()); + + // Second press (would be repeat) - should be suppressed + auto second = process(input, true, VK_A, 0x1E, L'a', 0); + VERIFY_IS_TRUE(!second.has_value() || std::get(*second).empty()); + } + + TEST_METHOD(EdgeCase_ForceDisableKitty) + { + TerminalInput input; + + // Set flags + input.SetKittyKeyboardProtocol(DisambiguateEscapeCodes, KittyKeyboardProtocolMode::Replace); + VERIFY_ARE_EQUAL(DisambiguateEscapeCodes, input.GetKittyFlags()); + + // Force disable + input.ForceDisableKittyKeyboardProtocol(true); + VERIFY_ARE_EQUAL(0, input.GetKittyFlags()); + + // Attempts to set flags should be ignored + input.SetKittyKeyboardProtocol(ReportAllKeysAsEscapeCodes, KittyKeyboardProtocolMode::Replace); + VERIFY_ARE_EQUAL(0, input.GetKittyFlags()); + } + + TEST_METHOD(EdgeCase_CtrlSpace_NullByte) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(ReportAllKeysAsEscapeCodes | ReportAssociatedText, KittyKeyboardProtocolMode::Replace); + + // Ctrl+Space should produce key with null character + // The kitty key code for space is 32 + auto result = process(input, true, VK_SPACE, 0x39, 0, LEFT_CTRL_PRESSED); + // Control codes (< 0x20) are not included in text per spec + // So this should be CSI 32;5u (no text, since ctrl+space produces 0x00 which is control) + VERIFY_ARE_EQUAL(wrap(L"\x1b[32;5u"), result); + } + + TEST_METHOD(EdgeCase_AltGr_Handling) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(DisambiguateEscapeCodes, KittyKeyboardProtocolMode::Replace); + + // AltGr generates both RIGHT_ALT and LEFT_CTRL + // The implementation should detect this and not treat it as Ctrl+Alt + auto result = process(input, true, VK_A, 0x1E, L'รค', RIGHT_ALT_PRESSED | LEFT_CTRL_PRESSED); + // With AltGr, the character 'รค' should be transmitted if it's a valid char + // The exact handling depends on implementation + } +}; diff --git a/src/tools/kitty-keyboard-test/Cargo.lock b/src/tools/kitty-keyboard-test/Cargo.lock new file mode 100644 index 0000000000..029744b407 --- /dev/null +++ b/src/tools/kitty-keyboard-test/Cargo.lock @@ -0,0 +1,16 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "kitty-keyboard-test" +version = "0.1.0" +dependencies = [ + "libc", +] + +[[package]] +name = "libc" +version = "0.2.180" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" diff --git a/src/tools/kitty-keyboard-test/Cargo.toml b/src/tools/kitty-keyboard-test/Cargo.toml new file mode 100644 index 0000000000..bc33f065e1 --- /dev/null +++ b/src/tools/kitty-keyboard-test/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "kitty-keyboard-test" +version = "0.1.0" +edition = "2021" +description = "Interactive tester for the Kitty keyboard protocol enhancement flags" + +[dependencies] + +[target.'cfg(unix)'.dependencies] +libc = "0.2" diff --git a/src/tools/kitty-keyboard-test/README.md b/src/tools/kitty-keyboard-test/README.md new file mode 100644 index 0000000000..15abcae858 --- /dev/null +++ b/src/tools/kitty-keyboard-test/README.md @@ -0,0 +1,81 @@ +# Kitty Keyboard Protocol Tester + +An interactive tool for testing the [Kitty keyboard protocol](https://sw.kovidgoyal.net/kitty/keyboard-protocol/) enhancement flags. + +## Building + +```sh +cargo build --release +``` + +## Usage + +Run the tool in a terminal that supports the Kitty keyboard protocol: + +```sh +cargo run +``` + +or after building: + +```sh +./target/release/kitty-keyboard-test +``` + +## Controls + +| Key | Action | +|-----|--------| +| `1` | Toggle **Disambiguate escape codes** (0b00001) | +| `2` | Toggle **Report event types** (0b00010) | +| `3` | Toggle **Report alternate keys** (0b00100) | +| `4` | Toggle **Report all keys as escape codes** (0b01000) | +| `5` | Toggle **Report associated text** (0b10000) | +| `q` or `Ctrl+C` | Quit | + +## Enhancement Flags + +1. **Disambiguate escape codes** (bit 0, value 1): Fixes legacy escape code ambiguities. Keys like Esc, Alt+key, Ctrl+key are reported using CSI u sequences. + +2. **Report event types** (bit 1, value 2): Reports key press, repeat, and release events. Without this flag, only press events are reported. + +3. **Report alternate keys** (bit 2, value 4): Reports shifted key and base layout key in addition to the main key code. Useful for shortcut matching across keyboard layouts. + +4. **Report all keys as escape codes** (bit 3, value 8): Even text-producing keys (like regular letters) are reported as escape codes instead of plain text. Required for games and applications that need key events for all keys. + +5. **Report associated text** (bit 4, value 16): When used with flag 4, also reports the text that the key would produce. The text is encoded as Unicode codepoints in the escape sequence. + +## Output Format + +For each key event, the tool displays: +- **Raw bytes**: The actual bytes received from the terminal +- **Escaped string**: A readable representation of the bytes +- **Decoded event**: Human-readable interpretation including key name, modifiers, event type, and any alternate keys or associated text + +## Example Output + +``` +Raw: [0x1b, 0x5b, 0x97, 0x3b, 0x32, 0x3b, 0x41, 0x75] Str: "\x1b[97;2;65u" + โ†’ Key: 'a' (97), Event: press, Modifiers: Shift, Text: "A" +``` + +## Protocol Reference + +- Escape sequence to push keyboard mode: `CSI > flags u` +- Escape sequence to pop keyboard mode: `CSI < u` +- Key event format: `CSI keycode:shifted:base ; modifiers:event ; text u` + +Modifiers are encoded as `1 + modifier_bits`: +- Shift: bit 0 (1) +- Alt: bit 1 (2) +- Ctrl: bit 2 (4) +- Super: bit 3 (8) +- Hyper: bit 4 (16) +- Meta: bit 5 (32) +- CapsLock: bit 6 (64) +- NumLock: bit 7 (128) + +Event types: +- Press: 1 (default if omitted) +- Repeat: 2 +- Release: 3 diff --git a/src/tools/kitty-keyboard-test/src/main.rs b/src/tools/kitty-keyboard-test/src/main.rs new file mode 100644 index 0000000000..69a994df4b --- /dev/null +++ b/src/tools/kitty-keyboard-test/src/main.rs @@ -0,0 +1,902 @@ +//! Interactive tester for the Kitty Keyboard Protocol. +//! +//! This tool allows you to toggle the 5 enhancement flags and see how key events +//! are encoded by the terminal emulator. +//! +//! Shortcuts: +//! 1-5: Toggle enhancement flags +//! q/Ctrl+C: Quit + +use std::fmt::Write as _; + +// Enhancement flags +const FLAG_DISAMBIGUATE: u8 = 0b00001; // 1 +const FLAG_EVENT_TYPES: u8 = 0b00010; // 2 +const FLAG_ALTERNATE_KEYS: u8 = 0b00100; // 4 +const FLAG_ALL_AS_ESCAPES: u8 = 0b01000; // 8 +const FLAG_ASSOCIATED_TEXT: u8 = 0b10000; // 16 + +// Modifier bits (value is encoded as 1 + modifiers in the protocol) +const MOD_SHIFT: u8 = 0b00000001; +const MOD_ALT: u8 = 0b00000010; +const MOD_CTRL: u8 = 0b00000100; +const MOD_SUPER: u8 = 0b00001000; +const MOD_HYPER: u8 = 0b00010000; +const MOD_META: u8 = 0b00100000; +const MOD_CAPS_LOCK: u8 = 0b01000000; +const MOD_NUM_LOCK: u8 = 0b10000000; + +fn main() { + let mut terminal = Terminal::new().expect("Failed to initialize terminal"); + let mut output = String::with_capacity(4096); + + // Detect if terminal supports Kitty keyboard protocol + // Send CSI ? u (query flags) followed by CSI c (DA1) + terminal.write(b"\x1b[?u\x1b[c"); + + let protocol_supported = detect_protocol_support(&mut terminal); + + let mut flags: u8 = 0; + + if protocol_supported { + write_flags(&mut output, flags); + } else { + let _ = write!( + output, + "\x1b[1;33mNote:\x1b[m Terminal does not support Kitty keyboard protocol.\r\n" + ); + let _ = write!( + output, + " Key events will be shown in legacy format only.\r\n\r\n" + ); + } + write_help(&mut output, protocol_supported); + terminal.write(output.as_bytes()); + output.clear(); + + if protocol_supported { + // Push initial flags (0) onto the stack + write_push_keyboard_mode(&mut output, flags); + terminal.write(output.as_bytes()); + output.clear(); + } + + let mut buf = [0u8; 64]; + loop { + let n = terminal.read(&mut buf); + if n == 0 { + continue; + } + + let input = &buf[..n]; + + // Ctrl+C --> Exit + if input == b"\x03" || input == b"\x1b[99;5u" { + break; + } + + if protocol_supported { + let flag_to_toggle = match input { + b"1" | b"\x1b[49u" | b"\x1b[49;;49u" => Some(FLAG_DISAMBIGUATE), + b"2" | b"\x1b[50u" | b"\x1b[50;;50u" => Some(FLAG_EVENT_TYPES), + b"3" | b"\x1b[51u" | b"\x1b[51;;51u" => Some(FLAG_ALTERNATE_KEYS), + b"4" | b"\x1b[52u" | b"\x1b[52;;52u" => Some(FLAG_ALL_AS_ESCAPES), + b"5" | b"\x1b[53u" | b"\x1b[53;;53u" => Some(FLAG_ASSOCIATED_TEXT), + _ => None, + }; + + if let Some(flag) = flag_to_toggle { + flags ^= flag; + write_set_keyboard_mode(&mut output, flags); + write_flags(&mut output, flags); + write_help(&mut output, protocol_supported); + terminal.write(output.as_bytes()); + output.clear(); + continue; + } + } + + write_decoded_input(&mut output, input); + terminal.write(output.as_bytes()); + output.clear(); + } + + if protocol_supported { + write_pop_keyboard_mode(&mut output); + terminal.write(output.as_bytes()); + } +} + +/// Detect Kitty keyboard protocol support by looking for CSI ? u response +/// before the DA1 response (CSI ... c). +fn detect_protocol_support(terminal: &mut Terminal) -> bool { + let mut buf = [0u8; 256]; + let mut response = Vec::new(); + let mut got_kitty_response = false; + + // Read until we see the DA1 response terminator 'c' + loop { + let n = terminal.read(&mut buf); + response.extend_from_slice(&buf[..n]); + + // Parse the accumulated response + let mut i = 0; + while i < response.len() { + if response[i] == 0x1b && i + 1 < response.len() && response[i + 1] == b'[' { + // Found CSI, look for the terminator + if let Some(end) = find_csi_end(&response[i + 2..]) { + let seq_end = i + 2 + end; + let params = &response[i + 2..seq_end]; + let terminator = response[seq_end]; + + if terminator == b'u' && params.starts_with(b"?") { + // CSI ? u - Kitty keyboard query response + got_kitty_response = true; + } else if terminator == b'c' { + // DA1 response - we're done + return got_kitty_response; + } + + i = seq_end + 1; + continue; + } + } + i += 1; + } + } +} + +/// Find the end of CSI parameters (returns index of terminator byte) +fn find_csi_end(data: &[u8]) -> Option { + for (i, &b) in data.iter().enumerate() { + // CSI terminators are in the range 0x40-0x7E + if (0x40..=0x7E).contains(&b) { + return Some(i); + } + // Parameters and intermediates are in 0x20-0x3F range + if !((0x20..=0x3F).contains(&b)) { + return None; + } + } + None +} + +fn write_flags(out: &mut String, flags: u8) { + let _ = write!(out, "\x1b[1mEnhancement Flags:\x1b[m\r\n"); + let _ = write!( + out, + " [{}] \x1b[33m1\x1b[m: Disambiguate escape codes (0b00001)\r\n", + if flags & FLAG_DISAMBIGUATE != 0 { + "\x1b[32mโœ“\x1b[m" + } else { + " " + } + ); + let _ = write!( + out, + " [{}] \x1b[33m2\x1b[m: Report event types (0b00010)\r\n", + if flags & FLAG_EVENT_TYPES != 0 { + "\x1b[32mโœ“\x1b[m" + } else { + " " + } + ); + let _ = write!( + out, + " [{}] \x1b[33m3\x1b[m: Report alternate keys (0b00100)\r\n", + if flags & FLAG_ALTERNATE_KEYS != 0 { + "\x1b[32mโœ“\x1b[m" + } else { + " " + } + ); + let _ = write!( + out, + " [{}] \x1b[33m4\x1b[m: Report all keys as escapes (0b01000)\r\n", + if flags & FLAG_ALL_AS_ESCAPES != 0 { + "\x1b[32mโœ“\x1b[m" + } else { + " " + } + ); + let _ = write!( + out, + " [{}] \x1b[33m5\x1b[m: Report associated text (0b10000)\r\n", + if flags & FLAG_ASSOCIATED_TEXT != 0 { + "\x1b[32mโœ“\x1b[m" + } else { + " " + } + ); + let _ = write!(out, "\r\n"); + let _ = write!( + out, + " \x1b[1mCurrent flags value:\x1b[m \x1b[36m{}\x1b[m (0b{:05b})\r\n", + flags, flags + ); + let _ = write!(out, "\r\n"); +} + +fn write_help(out: &mut String, protocol_supported: bool) { + let _ = write!( + out, + "\x1b[90mโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\x1b[m\r\n" + ); + if protocol_supported { + let _ = write!(out, "\x1b[1mControls:\x1b[m Press \x1b[33m1-5\x1b[m to toggle flags, \x1b[33mCtrl+C\x1b[m to quit\r\n"); + } else { + let _ = write!( + out, + "\x1b[1mControls:\x1b[m Press \x1b[33mCtrl+C\x1b[m to quit\r\n" + ); + } + let _ = write!( + out, + "\x1b[90mโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\x1b[m\r\n" + ); + let _ = write!(out, "\r\n"); + let _ = write!(out, "\x1b[1mKey events:\x1b[m\r\n"); + let _ = write!(out, "\r\n"); +} + +fn write_push_keyboard_mode(out: &mut String, flags: u8) { + // CSI > flags u - Push flags onto the stack + let _ = write!(out, "\x1b[>{}u", flags); +} + +fn write_set_keyboard_mode(out: &mut String, flags: u8) { + // CSI = flags ; 1 u - Set flags (mode 1 = replace all) + let _ = write!(out, "\x1b[={};1u", flags); +} + +fn write_pop_keyboard_mode(out: &mut String) { + // CSI < u - Pop from the stack (restores previous mode) + let _ = write!(out, "\x1b[ { + let _ = write!(out, "\\x1b"); + } + 0x00..=0x1f => { + let _ = write!(out, "\\x{:02x}", b); + } + 0x7f => { + let _ = write!(out, "\\x7f"); + } + _ => { + let _ = write!(out, "{}", b as char); + } + } + } + let _ = write!(out, "\""); + + // Try to decode as Kitty protocol + if let Some(decoded) = decode_kitty_sequence(input) { + let _ = write!(out, "\x1b[m\r\n \x1b[1;32mโ†’ {}\x1b[m", decoded); + } else if let Some(decoded) = decode_legacy_sequence(input) { + let _ = write!( + out, + "\x1b[m\r\n \x1b[1;33mโ†’ {} (legacy)\x1b[m", + decoded + ); + } else if input.len() == 1 && input[0] >= 0x20 && input[0] < 0x7f { + let _ = write!( + out, + "\x1b[m\r\n \x1b[1;34mโ†’ Character: '{}'\x1b[m", + input[0] as char + ); + } else if input.len() == 1 { + if let Some(name) = control_char_name(input[0]) { + let _ = write!(out, "\x1b[m\r\n \x1b[1;35mโ†’ {}\x1b[m", name); + } + } + + let _ = write!(out, "\x1b[m\r\n"); +} + +fn decode_kitty_sequence(input: &[u8]) -> Option { + // Check for CSI ... u format + if input.len() < 3 || input[0] != 0x1b || input[1] != b'[' { + return None; + } + + let rest = &input[2..]; + + // CSI ? flags u - Query response + if rest.starts_with(b"?") && rest.ends_with(b"u") { + let num_str = std::str::from_utf8(&rest[1..rest.len() - 1]).ok()?; + if let Ok(flags) = num_str.parse::() { + return Some(format!("Query response: flags={} (0b{:05b})", flags, flags)); + } + } + + // CSI ... u - Key event + if rest.ends_with(b"u") { + return decode_csi_u_sequence(&rest[..rest.len() - 1]); + } + + // CSI ... ~ - Legacy functional key with possible kitty extensions + if rest.ends_with(b"~") { + return decode_csi_tilde_sequence(&rest[..rest.len() - 1]); + } + + None +} + +/// Parse "modifiers:event_type" parameter, returns (modifiers, event_type) +fn parse_modifiers_and_event(mod_part: Option<&str>) -> (u8, u8) { + if let Some(mod_part) = mod_part { + let mut mod_parts = mod_part.split(':'); + let mods: u8 = mod_parts + .next() + .and_then(|s| s.parse::().ok()) + .unwrap_or(1) + .saturating_sub(1); + let evt: u8 = mod_parts.next().and_then(|s| s.parse().ok()).unwrap_or(1); + (mods, evt) + } else { + (0, 1) + } +} + +/// Format a codepoint as a readable key name +fn format_codepoint(code: u32) -> String { + if let Some(c) = char::from_u32(code) { + if c.is_control() { + format!("{}", code) + } else { + format!("'{}' ({})", c, code) + } + } else { + format!("{}", code) + } +} + +fn decode_csi_u_sequence(params: &[u8]) -> Option { + let params_str = std::str::from_utf8(params).ok()?; + let mut parts = params_str.split(';'); + + // Parse key code (may have alternate keys separated by colons) + let key_part = parts.next()?; + let mut key_codes = key_part.split(':'); + let key_code: u32 = key_codes.next()?.parse().ok()?; + let shifted_key: Option = + key_codes + .next() + .and_then(|s| if s.is_empty() { None } else { s.parse().ok() }); + let base_layout_key: Option = key_codes.next().and_then(|s| s.parse().ok()); + + let (modifiers, event_type) = parse_modifiers_and_event(parts.next()); + + // Parse associated text + let text_codepoints: Option = parts.next().map(|text_part| { + text_part + .split(':') + .filter_map(|s| s.parse::().ok()) + .filter_map(char::from_u32) + .collect() + }); + + // Build result + let key_name = key_code_to_name(key_code); + let event_name = match event_type { + 1 => "press", + 2 => "repeat", + 3 => "release", + _ => "unknown", + }; + + let mut result = format!("Key: {} ({}), Event: {}", key_name, key_code, event_name); + + if modifiers != 0 { + result.push_str(&format!(", Modifiers: {}", modifiers_to_string(modifiers))); + } + + if let Some(shifted) = shifted_key { + result.push_str(&format!(", Shifted: {}", format_codepoint(shifted))); + } + + if let Some(base) = base_layout_key { + result.push_str(&format!(", Base layout: {}", format_codepoint(base))); + } + + if let Some(text) = text_codepoints { + if !text.is_empty() { + result.push_str(&format!(", Text: \"{}\"", text)); + } + } + + Some(result) +} + +fn decode_csi_tilde_sequence(params: &[u8]) -> Option { + let params_str = std::str::from_utf8(params).ok()?; + let mut parts = params_str.split(';'); + + let key_num: u32 = parts.next()?.parse().ok()?; + let (modifiers, event_type) = parse_modifiers_and_event(parts.next()); + + let key_name = match key_num { + 2 => "Insert", + 3 => "Delete", + 5 => "PageUp", + 6 => "PageDown", + 7 => "Home", + 8 => "End", + 11 => "F1", + 12 => "F2", + 13 => "F3", + 14 => "F4", + 15 => "F5", + 17 => "F6", + 18 => "F7", + 19 => "F8", + 20 => "F9", + 21 => "F10", + 23 => "F11", + 24 => "F12", + 29 => "Menu", + _ => return Some(format!("Unknown functional key: {}", key_num)), + }; + + let event_name = match event_type { + 1 => "press", + 2 => "repeat", + 3 => "release", + _ => "unknown", + }; + + let mut result = format!("Key: {}, Event: {}", key_name, event_name); + if modifiers != 0 { + result.push_str(&format!(", Modifiers: {}", modifiers_to_string(modifiers))); + } + + Some(result) +} + +fn decode_legacy_sequence(input: &[u8]) -> Option { + if input.len() < 2 || input[0] != 0x1b { + return None; + } + + // SS3 sequences (ESC O ...) + if input.len() >= 3 && input[1] == b'O' { + let key = match input[2] { + b'A' => "Up", + b'B' => "Down", + b'C' => "Right", + b'D' => "Left", + b'H' => "Home", + b'F' => "End", + b'P' => "F1", + b'Q' => "F2", + b'R' => "F3", + b'S' => "F4", + _ => return None, + }; + return Some(format!("Key: {} (SS3)", key)); + } + + // CSI sequences + if input.len() >= 3 && input[1] == b'[' { + let rest = &input[2..]; + + // CSI letter - simple cursor keys + if rest.len() == 1 { + let key = match rest[0] { + b'A' => "Up", + b'B' => "Down", + b'C' => "Right", + b'D' => "Left", + b'H' => "Home", + b'F' => "End", + b'Z' => "Shift+Tab", + _ => return None, + }; + return Some(format!("Key: {}", key)); + } + + // CSI 1 ; modifier letter + if rest.len() >= 4 && rest[0] == b'1' && rest[1] == b';' { + if let Ok(mod_str) = std::str::from_utf8(&rest[2..rest.len() - 1]) { + if let Ok(mod_val) = mod_str.parse::() { + let modifiers = mod_val.saturating_sub(1); + let key = match rest[rest.len() - 1] { + b'A' => "Up", + b'B' => "Down", + b'C' => "Right", + b'D' => "Left", + b'H' => "Home", + b'F' => "End", + b'P' => "F1", + b'Q' => "F2", + b'S' => "F4", + _ => return None, + }; + return Some(format!( + "Key: {}, Modifiers: {}", + key, + modifiers_to_string(modifiers) + )); + } + } + } + } + + // Alt + key + if input.len() == 2 && input[1] >= 0x20 && input[1] < 0x7f { + return Some(format!("Alt+'{}'", input[1] as char)); + } + + None +} + +fn key_code_to_name(code: u32) -> String { + match code { + 9 => "Tab".to_string(), + 13 => "Enter".to_string(), + 27 => "Escape".to_string(), + 32 => "Space".to_string(), + 127 => "Backspace".to_string(), + + // Functional keys in Private Use Area + 57358 => "CapsLock".to_string(), + 57359 => "ScrollLock".to_string(), + 57360 => "NumLock".to_string(), + 57361 => "PrintScreen".to_string(), + 57362 => "Pause".to_string(), + 57363 => "Menu".to_string(), + + 57376..=57398 => format!("F{}", code - 57376 + 13), // F13-F35 + + 57399..=57408 => format!("KP_{}", code - 57399), // KP_0 - KP_9 + 57409 => "KP_Decimal".to_string(), + 57410 => "KP_Divide".to_string(), + 57411 => "KP_Multiply".to_string(), + 57412 => "KP_Subtract".to_string(), + 57413 => "KP_Add".to_string(), + 57414 => "KP_Enter".to_string(), + 57415 => "KP_Equal".to_string(), + 57416 => "KP_Separator".to_string(), + 57417 => "KP_Left".to_string(), + 57418 => "KP_Right".to_string(), + 57419 => "KP_Up".to_string(), + 57420 => "KP_Down".to_string(), + 57421 => "KP_PageUp".to_string(), + 57422 => "KP_PageDown".to_string(), + 57423 => "KP_Home".to_string(), + 57424 => "KP_End".to_string(), + 57425 => "KP_Insert".to_string(), + 57426 => "KP_Delete".to_string(), + 57427 => "KP_Begin".to_string(), + + 57428 => "MediaPlay".to_string(), + 57429 => "MediaPause".to_string(), + 57430 => "MediaPlayPause".to_string(), + 57431 => "MediaReverse".to_string(), + 57432 => "MediaStop".to_string(), + 57433 => "MediaFastForward".to_string(), + 57434 => "MediaRewind".to_string(), + 57435 => "MediaTrackNext".to_string(), + 57436 => "MediaTrackPrevious".to_string(), + 57437 => "MediaRecord".to_string(), + 57438 => "LowerVolume".to_string(), + 57439 => "RaiseVolume".to_string(), + 57440 => "MuteVolume".to_string(), + + 57441 => "LeftShift".to_string(), + 57442 => "LeftControl".to_string(), + 57443 => "LeftAlt".to_string(), + 57444 => "LeftSuper".to_string(), + 57445 => "LeftHyper".to_string(), + 57446 => "LeftMeta".to_string(), + 57447 => "RightShift".to_string(), + 57448 => "RightControl".to_string(), + 57449 => "RightAlt".to_string(), + 57450 => "RightSuper".to_string(), + 57451 => "RightHyper".to_string(), + 57452 => "RightMeta".to_string(), + 57453 => "IsoLevel3Shift".to_string(), + 57454 => "IsoLevel5Shift".to_string(), + + // Regular characters + c if (0x20..0x7f).contains(&c) => format!("'{}'", char::from_u32(c).unwrap()), + c => { + if let Some(ch) = char::from_u32(c) { + format!("'{}' (U+{:04X})", ch, c) + } else { + format!("U+{:04X}", c) + } + } + } +} + +fn modifiers_to_string(mods: u8) -> String { + let mut parts = Vec::new(); + if mods & MOD_SHIFT != 0 { + parts.push("Shift"); + } + if mods & MOD_ALT != 0 { + parts.push("Alt"); + } + if mods & MOD_CTRL != 0 { + parts.push("Ctrl"); + } + if mods & MOD_SUPER != 0 { + parts.push("Super"); + } + if mods & MOD_HYPER != 0 { + parts.push("Hyper"); + } + if mods & MOD_META != 0 { + parts.push("Meta"); + } + if mods & MOD_CAPS_LOCK != 0 { + parts.push("CapsLock"); + } + if mods & MOD_NUM_LOCK != 0 { + parts.push("NumLock"); + } + if parts.is_empty() { + "None".to_string() + } else { + parts.join("+") + } +} + +fn control_char_name(b: u8) -> Option<&'static str> { + match b { + 0x00 => Some("Ctrl+Space (NUL)"), + 0x01 => Some("Ctrl+A"), + 0x02 => Some("Ctrl+B"), + 0x03 => Some("Ctrl+C"), + 0x04 => Some("Ctrl+D"), + 0x05 => Some("Ctrl+E"), + 0x06 => Some("Ctrl+F"), + 0x07 => Some("Ctrl+G (BEL)"), + 0x08 => Some("Ctrl+H (Backspace)"), + 0x09 => Some("Tab"), + 0x0a => Some("Ctrl+J (Line Feed)"), + 0x0b => Some("Ctrl+K"), + 0x0c => Some("Ctrl+L"), + 0x0d => Some("Enter"), + 0x0e => Some("Ctrl+N"), + 0x0f => Some("Ctrl+O"), + 0x10 => Some("Ctrl+P"), + 0x11 => Some("Ctrl+Q"), + 0x12 => Some("Ctrl+R"), + 0x13 => Some("Ctrl+S"), + 0x14 => Some("Ctrl+T"), + 0x15 => Some("Ctrl+U"), + 0x16 => Some("Ctrl+V"), + 0x17 => Some("Ctrl+W"), + 0x18 => Some("Ctrl+X"), + 0x19 => Some("Ctrl+Y"), + 0x1a => Some("Ctrl+Z"), + 0x1b => Some("Escape"), + 0x1c => Some("Ctrl+\\"), + 0x1d => Some("Ctrl+]"), + 0x1e => Some("Ctrl+^"), + 0x1f => Some("Ctrl+_"), + 0x7f => Some("Backspace (DEL)"), + _ => None, + } +} + +// Platform-specific terminal handling + +#[cfg(unix)] +mod platform { + use std::io; + use std::mem::MaybeUninit; + + const STDIN_FILENO: libc::c_int = 0; + const STDOUT_FILENO: libc::c_int = 1; + + pub struct Terminal { + original_termios: libc::termios, + } + + impl Terminal { + pub fn new() -> io::Result { + let mut termios = MaybeUninit::uninit(); + + unsafe { + if libc::tcgetattr(STDIN_FILENO, termios.as_mut_ptr()) != 0 { + return Err(io::Error::last_os_error()); + } + } + + let original_termios = unsafe { termios.assume_init() }; + let mut raw = original_termios; + + // Set raw mode + raw.c_lflag &= !(libc::ECHO | libc::ICANON | libc::ISIG | libc::IEXTEN); + raw.c_iflag &= !(libc::IXON | libc::ICRNL | libc::BRKINT | libc::INPCK | libc::ISTRIP); + raw.c_oflag &= !libc::OPOST; + + unsafe { + if libc::tcsetattr(STDIN_FILENO, libc::TCSAFLUSH, &raw) != 0 { + return Err(io::Error::last_os_error()); + } + } + + Ok(Terminal { original_termios }) + } + + pub fn read(&mut self, buf: &mut [u8]) -> usize { + unsafe { + let n = libc::read( + STDIN_FILENO, + buf.as_mut_ptr() as *mut libc::c_void, + buf.len(), + ); + if n < 0 { + 0 + } else { + n as usize + } + } + } + + pub fn write(&mut self, buf: &[u8]) { + unsafe { + libc::write( + STDOUT_FILENO, + buf.as_ptr() as *const libc::c_void, + buf.len(), + ); + } + } + } + + impl Drop for Terminal { + fn drop(&mut self) { + unsafe { + libc::tcsetattr(STDIN_FILENO, libc::TCSAFLUSH, &self.original_termios); + } + } + } +} + +#[allow(non_camel_case_types, clippy::upper_case_acronyms)] +#[cfg(windows)] +mod platform { + use std::io; + + type BOOL = i32; + type HANDLE = *mut core::ffi::c_void; + type CONSOLE_MODE = u32; + type STD_HANDLE = u32; + + const STD_INPUT_HANDLE: STD_HANDLE = 0xFFFFFFF6; + const STD_OUTPUT_HANDLE: STD_HANDLE = 0xFFFFFFF5; + const ENABLE_PROCESSED_OUTPUT: CONSOLE_MODE = 1u32; + const ENABLE_WRAP_AT_EOL_OUTPUT: CONSOLE_MODE = 2u32; + const ENABLE_VIRTUAL_TERMINAL_PROCESSING: CONSOLE_MODE = 4u32; + const DISABLE_NEWLINE_AUTO_RETURN: CONSOLE_MODE = 8u32; + const ENABLE_VIRTUAL_TERMINAL_INPUT: CONSOLE_MODE = 512u32; + const CP_UTF8: u32 = 65001; + + unsafe extern "system" { + fn ReadFile( + hfile: HANDLE, + lpbuffer: *mut u8, + nnumberofbytestoread: u32, + lpnumberofbytesread: *mut u32, + lpoverlapped: *mut (), + ) -> BOOL; + + fn WriteFile( + hfile: HANDLE, + lpbuffer: *const u8, + nnumberofbytestowrite: u32, + lpnumberofbyteswritten: *mut u32, + lpoverlapped: *mut (), + ) -> BOOL; + + fn GetStdHandle(nstdhandle: STD_HANDLE) -> HANDLE; + fn GetConsoleMode(hconsolehandle: HANDLE, lpmode: *mut CONSOLE_MODE) -> BOOL; + fn SetConsoleMode(hconsolehandle: HANDLE, dwmode: CONSOLE_MODE) -> BOOL; + fn GetConsoleCP() -> u32; + fn SetConsoleCP(wcodepageid: u32) -> BOOL; + fn GetConsoleOutputCP() -> u32; + fn SetConsoleOutputCP(wcodepageid: u32) -> BOOL; + } + + pub struct Terminal { + stdin_handle: HANDLE, + stdout_handle: HANDLE, + stdin_mode: CONSOLE_MODE, + stdout_mode: CONSOLE_MODE, + stdin_cp: u32, + stdout_cp: u32, + } + + impl Terminal { + pub fn new() -> io::Result { + unsafe { + let stdin_handle = GetStdHandle(STD_INPUT_HANDLE); + let stdout_handle = GetStdHandle(STD_OUTPUT_HANDLE); + + let mut stdin_mode: CONSOLE_MODE = 0; + let mut stdout_mode: CONSOLE_MODE = 0; + GetConsoleMode(stdin_handle, &mut stdin_mode); + GetConsoleMode(stdin_handle, &mut stdout_mode); + + SetConsoleMode(stdin_handle, ENABLE_VIRTUAL_TERMINAL_INPUT); + SetConsoleMode( + stdout_handle, + ENABLE_PROCESSED_OUTPUT + | ENABLE_WRAP_AT_EOL_OUTPUT + | ENABLE_VIRTUAL_TERMINAL_PROCESSING + | DISABLE_NEWLINE_AUTO_RETURN, + ); + + let stdin_cp = GetConsoleCP(); + let stdout_cp = GetConsoleOutputCP(); + SetConsoleCP(CP_UTF8); + SetConsoleOutputCP(CP_UTF8); + + Ok(Terminal { + stdin_handle, + stdout_handle, + stdin_mode, + stdout_mode, + stdin_cp, + stdout_cp, + }) + } + } + + pub fn read(&mut self, buf: &mut [u8]) -> usize { + unsafe { + let mut bytes_read: u32 = 0; + if ReadFile( + self.stdin_handle, + buf.as_mut_ptr() as *mut _, + buf.len() as u32, + &mut bytes_read, + std::ptr::null_mut(), + ) == 0 + { + 0 + } else { + bytes_read as usize + } + } + } + + pub fn write(&mut self, buf: &[u8]) { + unsafe { + let mut bytes_written: u32 = 0; + WriteFile( + self.stdout_handle, + buf.as_ptr() as *const _, + buf.len() as u32, + &mut bytes_written, + std::ptr::null_mut(), + ); + } + } + } + + impl Drop for Terminal { + fn drop(&mut self) { + unsafe { + SetConsoleMode(self.stdin_handle, self.stdin_mode); + SetConsoleMode(self.stdout_handle, self.stdout_mode); + SetConsoleCP(self.stdin_cp); + SetConsoleOutputCP(self.stdout_cp); + } + } + } +} + +use platform::Terminal; diff --git a/src/tools/kitty-keyboard-test/target/.rustc_info.json b/src/tools/kitty-keyboard-test/target/.rustc_info.json new file mode 100644 index 0000000000..6dec59dfe5 --- /dev/null +++ b/src/tools/kitty-keyboard-test/target/.rustc_info.json @@ -0,0 +1 @@ +{"rustc_fingerprint":15155536823543150984,"outputs":{"7971740275564407648":{"success":true,"status":"","code":0,"stdout":"___.exe\nlib___.rlib\n___.dll\n___.dll\n___.lib\n___.dll\nC:\\Users\\lhecker\\.rustup\\toolchains\\nightly-x86_64-pc-windows-msvc\npacked\n___\ndebug_assertions\nemscripten_wasm_eh\nfmt_debug=\"full\"\noverflow_checks\npanic=\"unwind\"\nproc_macro\nrelocation_model=\"pic\"\ntarget_abi=\"\"\ntarget_arch=\"x86_64\"\ntarget_endian=\"little\"\ntarget_env=\"msvc\"\ntarget_family=\"windows\"\ntarget_feature=\"cmpxchg16b\"\ntarget_feature=\"fxsr\"\ntarget_feature=\"lahfsahf\"\ntarget_feature=\"sse\"\ntarget_feature=\"sse2\"\ntarget_feature=\"sse3\"\ntarget_feature=\"x87\"\ntarget_has_atomic\ntarget_has_atomic=\"128\"\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_has_atomic_equal_alignment=\"128\"\ntarget_has_atomic_equal_alignment=\"16\"\ntarget_has_atomic_equal_alignment=\"32\"\ntarget_has_atomic_equal_alignment=\"64\"\ntarget_has_atomic_equal_alignment=\"8\"\ntarget_has_atomic_equal_alignment=\"ptr\"\ntarget_has_atomic_load_store\ntarget_has_atomic_load_store=\"128\"\ntarget_has_atomic_load_store=\"16\"\ntarget_has_atomic_load_store=\"32\"\ntarget_has_atomic_load_store=\"64\"\ntarget_has_atomic_load_store=\"8\"\ntarget_has_atomic_load_store=\"ptr\"\ntarget_has_reliable_f128\ntarget_has_reliable_f16\ntarget_has_reliable_f16_math\ntarget_os=\"windows\"\ntarget_pointer_width=\"64\"\ntarget_thread_local\ntarget_vendor=\"pc\"\nub_checks\nwindows\n","stderr":""},"17747080675513052775":{"success":true,"status":"","code":0,"stdout":"rustc 1.95.0-nightly (873d4682c 2026-01-25)\nbinary: rustc\ncommit-hash: 873d4682c7d285540b8f28bfe637006cef8918a6\ncommit-date: 2026-01-25\nhost: x86_64-pc-windows-msvc\nrelease: 1.95.0-nightly\nLLVM version: 21.1.8\n","stderr":""}},"successes":{}} \ No newline at end of file diff --git a/src/tools/kitty-keyboard-test/target/CACHEDIR.TAG b/src/tools/kitty-keyboard-test/target/CACHEDIR.TAG new file mode 100644 index 0000000000..20d7c319cd --- /dev/null +++ b/src/tools/kitty-keyboard-test/target/CACHEDIR.TAG @@ -0,0 +1,3 @@ +Signature: 8a477f597d28d172789f06886806bc55 +# This file is a cache directory tag created by cargo. +# For information about cache directory tags see https://bford.info/cachedir/ diff --git a/src/tools/kitty-keyboard-test/target/flycheck0/stderr b/src/tools/kitty-keyboard-test/target/flycheck0/stderr new file mode 100644 index 0000000000..7c9dc1c6d2 --- /dev/null +++ b/src/tools/kitty-keyboard-test/target/flycheck0/stderr @@ -0,0 +1,12 @@ + 0.008865700s INFO prepare_target{force=false package_id=kitty-keyboard-test v0.1.0 (C:\Users\lhecker\projects\terminal\src\tools\kitty-keyboard-test) target="kitty-keyboard-test"}: cargo::core::compiler::fingerprint: stale: changed "C:\\Users\\lhecker\\projects\\terminal\\src\\tools\\kitty-keyboard-test\\src\\main.rs" + 0.008885600s INFO prepare_target{force=false package_id=kitty-keyboard-test v0.1.0 (C:\Users\lhecker\projects\terminal\src\tools\kitty-keyboard-test) target="kitty-keyboard-test"}: cargo::core::compiler::fingerprint: (vs) "C:\\Users\\lhecker\\projects\\terminal\\src\\tools\\kitty-keyboard-test\\target\\debug\\.fingerprint\\kitty-keyboard-test-540f39a9b0e2080c\\dep-bin-kitty-keyboard-test" + 0.008890400s INFO prepare_target{force=false package_id=kitty-keyboard-test v0.1.0 (C:\Users\lhecker\projects\terminal\src\tools\kitty-keyboard-test) target="kitty-keyboard-test"}: cargo::core::compiler::fingerprint: FileTime { seconds: 13414356040, nanos: 415027200 } < FileTime { seconds: 13414356217, nanos: 99954000 } + 0.009009300s INFO prepare_target{force=false package_id=kitty-keyboard-test v0.1.0 (C:\Users\lhecker\projects\terminal\src\tools\kitty-keyboard-test) target="kitty-keyboard-test"}: cargo::core::compiler::fingerprint: fingerprint dirty for kitty-keyboard-test v0.1.0 (C:\Users\lhecker\projects\terminal\src\tools\kitty-keyboard-test)/Check { test: false }/TargetInner { name: "kitty-keyboard-test", doc: true, ..: with_path("C:\\Users\\lhecker\\projects\\terminal\\src\\tools\\kitty-keyboard-test\\src\\main.rs", Edition2021) } + 0.009027100s INFO prepare_target{force=false package_id=kitty-keyboard-test v0.1.0 (C:\Users\lhecker\projects\terminal\src\tools\kitty-keyboard-test) target="kitty-keyboard-test"}: cargo::core::compiler::fingerprint: dirty: FsStatusOutdated(StaleItem(ChangedFile { reference: "C:\\Users\\lhecker\\projects\\terminal\\src\\tools\\kitty-keyboard-test\\target\\debug\\.fingerprint\\kitty-keyboard-test-540f39a9b0e2080c\\dep-bin-kitty-keyboard-test", reference_mtime: FileTime { seconds: 13414356040, nanos: 415027200 }, stale: "C:\\Users\\lhecker\\projects\\terminal\\src\\tools\\kitty-keyboard-test\\src\\main.rs", stale_mtime: FileTime { seconds: 13414356217, nanos: 99954000 } })) + 0.009491200s INFO prepare_target{force=false package_id=kitty-keyboard-test v0.1.0 (C:\Users\lhecker\projects\terminal\src\tools\kitty-keyboard-test) target="kitty-keyboard-test"}: cargo::core::compiler::fingerprint: stale: changed "C:\\Users\\lhecker\\projects\\terminal\\src\\tools\\kitty-keyboard-test\\src\\main.rs" + 0.009498900s INFO prepare_target{force=false package_id=kitty-keyboard-test v0.1.0 (C:\Users\lhecker\projects\terminal\src\tools\kitty-keyboard-test) target="kitty-keyboard-test"}: cargo::core::compiler::fingerprint: (vs) "C:\\Users\\lhecker\\projects\\terminal\\src\\tools\\kitty-keyboard-test\\target\\debug\\.fingerprint\\kitty-keyboard-test-bf4513f9cae5b3d2\\dep-test-bin-kitty-keyboard-test" + 0.009502500s INFO prepare_target{force=false package_id=kitty-keyboard-test v0.1.0 (C:\Users\lhecker\projects\terminal\src\tools\kitty-keyboard-test) target="kitty-keyboard-test"}: cargo::core::compiler::fingerprint: FileTime { seconds: 13414356040, nanos: 415027200 } < FileTime { seconds: 13414356217, nanos: 99954000 } + 0.009662200s INFO prepare_target{force=false package_id=kitty-keyboard-test v0.1.0 (C:\Users\lhecker\projects\terminal\src\tools\kitty-keyboard-test) target="kitty-keyboard-test"}: cargo::core::compiler::fingerprint: fingerprint dirty for kitty-keyboard-test v0.1.0 (C:\Users\lhecker\projects\terminal\src\tools\kitty-keyboard-test)/Check { test: true }/TargetInner { name: "kitty-keyboard-test", doc: true, ..: with_path("C:\\Users\\lhecker\\projects\\terminal\\src\\tools\\kitty-keyboard-test\\src\\main.rs", Edition2021) } + 0.009677900s INFO prepare_target{force=false package_id=kitty-keyboard-test v0.1.0 (C:\Users\lhecker\projects\terminal\src\tools\kitty-keyboard-test) target="kitty-keyboard-test"}: cargo::core::compiler::fingerprint: dirty: FsStatusOutdated(StaleItem(ChangedFile { reference: "C:\\Users\\lhecker\\projects\\terminal\\src\\tools\\kitty-keyboard-test\\target\\debug\\.fingerprint\\kitty-keyboard-test-bf4513f9cae5b3d2\\dep-test-bin-kitty-keyboard-test", reference_mtime: FileTime { seconds: 13414356040, nanos: 415027200 }, stale: "C:\\Users\\lhecker\\projects\\terminal\\src\\tools\\kitty-keyboard-test\\src\\main.rs", stale_mtime: FileTime { seconds: 13414356217, nanos: 99954000 } })) + Checking kitty-keyboard-test v0.1.0 (C:\Users\lhecker\projects\terminal\src\tools\kitty-keyboard-test) + Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.08s diff --git a/src/tools/kitty-keyboard-test/target/flycheck0/stdout b/src/tools/kitty-keyboard-test/target/flycheck0/stdout new file mode 100644 index 0000000000..559cdc728f --- /dev/null +++ b/src/tools/kitty-keyboard-test/target/flycheck0/stdout @@ -0,0 +1,3 @@ +{"reason":"compiler-artifact","package_id":"path+file:///C:/Users/lhecker/projects/terminal/src/tools/kitty-keyboard-test#0.1.0","manifest_path":"C:\\Users\\lhecker\\projects\\terminal\\src\\tools\\kitty-keyboard-test\\Cargo.toml","target":{"kind":["bin"],"crate_types":["bin"],"name":"kitty-keyboard-test","src_path":"C:\\Users\\lhecker\\projects\\terminal\\src\\tools\\kitty-keyboard-test\\src\\main.rs","edition":"2021","doc":true,"doctest":false,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["C:\\Users\\lhecker\\projects\\terminal\\src\\tools\\kitty-keyboard-test\\target\\debug\\deps\\libkitty_keyboard_test-540f39a9b0e2080c.rmeta"],"executable":null,"fresh":false} +{"reason":"compiler-artifact","package_id":"path+file:///C:/Users/lhecker/projects/terminal/src/tools/kitty-keyboard-test#0.1.0","manifest_path":"C:\\Users\\lhecker\\projects\\terminal\\src\\tools\\kitty-keyboard-test\\Cargo.toml","target":{"kind":["bin"],"crate_types":["bin"],"name":"kitty-keyboard-test","src_path":"C:\\Users\\lhecker\\projects\\terminal\\src\\tools\\kitty-keyboard-test\\src\\main.rs","edition":"2021","doc":true,"doctest":false,"test":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":true},"features":[],"filenames":["C:\\Users\\lhecker\\projects\\terminal\\src\\tools\\kitty-keyboard-test\\target\\debug\\deps\\libkitty_keyboard_test-bf4513f9cae5b3d2.rmeta"],"executable":null,"fresh":false} +{"reason":"build-finished","success":true}