From 07792774f69474cdae1c625ecbfcf84485bab0e1 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Thu, 29 Jan 2026 23:58:10 +0100 Subject: [PATCH 1/2] Implement the Kitty Keyboard Protocol --- src/cascadia/TerminalCore/ICoreSettings.idl | 1 + src/cascadia/TerminalCore/Terminal.cpp | 1 + .../TerminalSettings.cpp | 1 + .../TerminalSettingsEditor/ProfileViewModel.h | 1 + .../ProfileViewModel.idl | 1 + .../Profiles_Terminal.xaml | 9 + .../Resources/en-US/Resources.resw | 18 +- .../TerminalSettingsModel/MTSMSettings.h | 1 + .../TerminalSettingsModel/Profile.idl | 1 + src/cascadia/inc/ControlProperties.h | 1 + src/common.build.pre.props | 2 +- src/renderer/atlas/AtlasEngine.cpp | 2 + src/terminal/adapter/ITermDispatch.hpp | 4 + src/terminal/adapter/adaptDispatch.cpp | 29 + src/terminal/adapter/adaptDispatch.hpp | 4 + src/terminal/adapter/termDispatch.hpp | 4 + src/terminal/input/lib/terminalinput.vcxproj | 1 - src/terminal/input/mouseInput.cpp | 2 +- src/terminal/input/mouseInputState.cpp | 30 - src/terminal/input/sources.inc | 1 - src/terminal/input/terminalInput.cpp | 715 +++++++++++++++++- src/terminal/input/terminalInput.hpp | 73 +- .../parser/OutputStateMachineEngine.cpp | 12 + .../parser/OutputStateMachineEngine.hpp | 4 + 24 files changed, 862 insertions(+), 56 deletions(-) delete mode 100644 src/terminal/input/mouseInputState.cpp diff --git a/src/cascadia/TerminalCore/ICoreSettings.idl b/src/cascadia/TerminalCore/ICoreSettings.idl index 0aacc36faa..0f88e0c186 100644 --- a/src/cascadia/TerminalCore/ICoreSettings.idl +++ b/src/cascadia/TerminalCore/ICoreSettings.idl @@ -121,6 +121,7 @@ namespace Microsoft.Terminal.Core String WordDelimiters { get; }; Boolean ForceVTInput { get; }; + Boolean AllowKittyKeyboardMode { get; }; Boolean AllowVtChecksumReport { get; }; Boolean AllowVtClipboardWrite { get; }; Boolean TrimBlockSelection { get; }; diff --git a/src/cascadia/TerminalCore/Terminal.cpp b/src/cascadia/TerminalCore/Terminal.cpp index aec06719d7..4260bf7a7b 100644 --- a/src/cascadia/TerminalCore/Terminal.cpp +++ b/src/cascadia/TerminalCore/Terminal.cpp @@ -98,6 +98,7 @@ void Terminal::UpdateSettings(ICoreSettings settings) } _getTerminalInput().ForceDisableWin32InputMode(settings.ForceVTInput()); + _getTerminalInput().ForceDisableKittyKeyboardProtocol(!settings.AllowKittyKeyboardMode()); if (settings.TabColor() == nullptr) { diff --git a/src/cascadia/TerminalSettingsAppAdapterLib/TerminalSettings.cpp b/src/cascadia/TerminalSettingsAppAdapterLib/TerminalSettings.cpp index 9bc73d5fd7..cba83bb772 100644 --- a/src/cascadia/TerminalSettingsAppAdapterLib/TerminalSettings.cpp +++ b/src/cascadia/TerminalSettingsAppAdapterLib/TerminalSettings.cpp @@ -349,6 +349,7 @@ namespace winrt::Microsoft::Terminal::Settings _ReloadEnvironmentVariables = profile.ReloadEnvironmentVariables(); _RainbowSuggestions = profile.RainbowSuggestions(); _ForceVTInput = profile.ForceVTInput(); + _AllowKittyKeyboardMode = profile.AllowKittyKeyboardMode(); _AllowVtChecksumReport = profile.AllowVtChecksumReport(); _AllowVtClipboardWrite = profile.AllowVtClipboardWrite(); _PathTranslationStyle = profile.PathTranslationStyle(); diff --git a/src/cascadia/TerminalSettingsEditor/ProfileViewModel.h b/src/cascadia/TerminalSettingsEditor/ProfileViewModel.h index 9f85cfee81..dc09a8db51 100644 --- a/src/cascadia/TerminalSettingsEditor/ProfileViewModel.h +++ b/src/cascadia/TerminalSettingsEditor/ProfileViewModel.h @@ -166,6 +166,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation OBSERVABLE_PROJECTED_SETTING(_profile, AutoMarkPrompts); OBSERVABLE_PROJECTED_SETTING(_profile, RepositionCursorWithMouse); OBSERVABLE_PROJECTED_SETTING(_profile, ForceVTInput); + OBSERVABLE_PROJECTED_SETTING(_profile, AllowKittyKeyboardMode); OBSERVABLE_PROJECTED_SETTING(_profile, AllowVtChecksumReport); OBSERVABLE_PROJECTED_SETTING(_profile, AllowVtClipboardWrite); OBSERVABLE_PROJECTED_SETTING(_profile, AnswerbackMessage); diff --git a/src/cascadia/TerminalSettingsEditor/ProfileViewModel.idl b/src/cascadia/TerminalSettingsEditor/ProfileViewModel.idl index 073afdf910..838e7bc3f1 100644 --- a/src/cascadia/TerminalSettingsEditor/ProfileViewModel.idl +++ b/src/cascadia/TerminalSettingsEditor/ProfileViewModel.idl @@ -157,6 +157,7 @@ namespace Microsoft.Terminal.Settings.Editor OBSERVABLE_PROJECTED_PROFILE_SETTING(Boolean, AutoMarkPrompts); OBSERVABLE_PROJECTED_PROFILE_SETTING(Boolean, RepositionCursorWithMouse); OBSERVABLE_PROJECTED_PROFILE_SETTING(Boolean, ForceVTInput); + OBSERVABLE_PROJECTED_PROFILE_SETTING(Boolean, AllowKittyKeyboardMode); OBSERVABLE_PROJECTED_PROFILE_SETTING(Boolean, AllowVtChecksumReport); OBSERVABLE_PROJECTED_PROFILE_SETTING(String, AnswerbackMessage); OBSERVABLE_PROJECTED_PROFILE_SETTING(Boolean, RainbowSuggestions); diff --git a/src/cascadia/TerminalSettingsEditor/Profiles_Terminal.xaml b/src/cascadia/TerminalSettingsEditor/Profiles_Terminal.xaml index dfe5954d4f..feef147108 100644 --- a/src/cascadia/TerminalSettingsEditor/Profiles_Terminal.xaml +++ b/src/cascadia/TerminalSettingsEditor/Profiles_Terminal.xaml @@ -49,6 +49,15 @@ Style="{StaticResource ToggleSwitchInExpanderStyle}" /> + + + + + Use the legacy input encoding Header for a control to toggle legacy input encoding for the terminal. + + Kitty keyboard protocol mode + Header for a control to set the kitty keyboard protocol mode. + + + Sets the baseline flags for the kitty keyboard protocol. Value is a sum of: 1=Disambiguate, 2=Report event types, 4=Report alternate keys, 8=Report all keys, 16=Report text. + Additional description for what the "kitty keyboard mode" setting does. + Allow DECRQCRA (Request Checksum of Rectangular Area) {Locked="DECRQCRA"}{Locked="Request Checksum of Rectangular Area"}Header for a control to toggle support for the DECRQCRA control sequence. @@ -2575,19 +2583,19 @@ An option to choose from for the "path translation" setting. - WSL (C:\ -> /mnt/c) + WSL (C:\ -> /mnt/c) {Locked="WSL","C:\","/mnt/c"} An option to choose from for the "path translation" setting. - Cygwin (C:\ -> /cygdrive/c) + Cygwin (C:\ -> /cygdrive/c) {Locked="Cygwin","C:\","/cygdrive/c"} An option to choose from for the "path translation" setting. - MSYS2 (C:\ -> /c) + MSYS2 (C:\ -> /c) {Locked="MSYS2","C:\","/c"} An option to choose from for the "path translation" setting. - MinGW (C:\ -> C:/) + MinGW (C:\ -> C:/) {Locked="MinGW","C:\","C:/"} An option to choose from for the "path translation" setting. @@ -2701,4 +2709,4 @@ Yes, clear the cache - + \ No newline at end of file diff --git a/src/cascadia/TerminalSettingsModel/MTSMSettings.h b/src/cascadia/TerminalSettingsModel/MTSMSettings.h index 9040f89cbf..c20e49701f 100644 --- a/src/cascadia/TerminalSettingsModel/MTSMSettings.h +++ b/src/cascadia/TerminalSettingsModel/MTSMSettings.h @@ -103,6 +103,7 @@ Author(s): X(bool, ReloadEnvironmentVariables, "compatibility.reloadEnvironmentVariables", true) \ X(bool, RainbowSuggestions, "experimental.rainbowSuggestions", false) \ X(bool, ForceVTInput, "compatibility.input.forceVT", false) \ + X(bool, AllowKittyKeyboardMode, "compatibility.kittyKeyboardMode", true) \ X(bool, AllowVtChecksumReport, "compatibility.allowDECRQCRA", false) \ X(bool, AllowVtClipboardWrite, "compatibility.allowOSC52", true) \ X(bool, AllowKeypadMode, "compatibility.allowDECNKM", false) \ diff --git a/src/cascadia/TerminalSettingsModel/Profile.idl b/src/cascadia/TerminalSettingsModel/Profile.idl index 9d7d93ef99..65de99b913 100644 --- a/src/cascadia/TerminalSettingsModel/Profile.idl +++ b/src/cascadia/TerminalSettingsModel/Profile.idl @@ -88,6 +88,7 @@ namespace Microsoft.Terminal.Settings.Model INHERITABLE_PROFILE_SETTING(Boolean, ReloadEnvironmentVariables); INHERITABLE_PROFILE_SETTING(Boolean, RainbowSuggestions); INHERITABLE_PROFILE_SETTING(Boolean, ForceVTInput); + INHERITABLE_PROFILE_SETTING(Boolean, AllowKittyKeyboardMode); INHERITABLE_PROFILE_SETTING(Boolean, AllowVtChecksumReport); INHERITABLE_PROFILE_SETTING(Boolean, AllowKeypadMode); INHERITABLE_PROFILE_SETTING(Boolean, AllowVtClipboardWrite); diff --git a/src/cascadia/inc/ControlProperties.h b/src/cascadia/inc/ControlProperties.h index ea33ad1b8e..383cb6d584 100644 --- a/src/cascadia/inc/ControlProperties.h +++ b/src/cascadia/inc/ControlProperties.h @@ -49,6 +49,7 @@ X(bool, TrimBlockSelection, true) \ X(bool, SuppressApplicationTitle) \ X(bool, ForceVTInput, false) \ + X(bool, AllowKittyKeyboardMode, true) \ X(winrt::hstring, StartingTitle) \ X(bool, DetectURLs, true) \ X(bool, AutoMarkPrompts) \ diff --git a/src/common.build.pre.props b/src/common.build.pre.props index b3f57bf230..e8084905fc 100644 --- a/src/common.build.pre.props +++ b/src/common.build.pre.props @@ -95,7 +95,7 @@ - v143 + v145 Unicode false x64 diff --git a/src/renderer/atlas/AtlasEngine.cpp b/src/renderer/atlas/AtlasEngine.cpp index af9a92f89a..e2599b420e 100644 --- a/src/renderer/atlas/AtlasEngine.cpp +++ b/src/renderer/atlas/AtlasEngine.cpp @@ -1151,6 +1151,8 @@ void AtlasEngine::_mapComplex(IDWriteFontFace2* mappedFontFace, u32 idx, u32 len const size_t col2 = _api.bufferLineColumn[a.textPosition + i]; const auto fg = colors[col1 << shift]; + // TODO: Instead of aligning each DWrite-cluster to the cell grid, + // we should align each grapheme cluster to the cell grid. const auto expectedAdvance = (col2 - col1) * _p.s->font->cellSize.x; f32 actualAdvance = 0; for (auto j = prevCluster; j < nextCluster; ++j) diff --git a/src/terminal/adapter/ITermDispatch.hpp b/src/terminal/adapter/ITermDispatch.hpp index 5ebb2ff31c..c39d1b5c51 100644 --- a/src/terminal/adapter/ITermDispatch.hpp +++ b/src/terminal/adapter/ITermDispatch.hpp @@ -67,6 +67,10 @@ public: virtual void DeleteColumn(const VTInt distance) = 0; // DECDC virtual void SetKeypadMode(const bool applicationMode) = 0; // DECKPAM, DECKPNM virtual void SetAnsiMode(const bool ansiMode) = 0; // DECANM + virtual void SetKittyKeyboardProtocol(const VTParameter flags, const VTParameter mode) = 0; // CSI = flags ; mode u + virtual void QueryKittyKeyboardProtocol() = 0; // CSI ? u + virtual void PushKittyKeyboardProtocol(const VTParameter flags) = 0; // CSI > flags u + virtual void PopKittyKeyboardProtocol(const VTParameter count) = 0; // CSI < count u virtual void SetTopBottomScrollingMargins(const VTInt topMargin, const VTInt bottomMargin) = 0; // DECSTBM virtual void SetLeftRightScrollingMargins(const VTInt leftMargin, const VTInt rightMargin) = 0; // DECSLRM virtual void EnquireAnswerback() = 0; // ENQ diff --git a/src/terminal/adapter/adaptDispatch.cpp b/src/terminal/adapter/adaptDispatch.cpp index 9de1509bd7..708fa7ebb8 100644 --- a/src/terminal/adapter/adaptDispatch.cpp +++ b/src/terminal/adapter/adaptDispatch.cpp @@ -2054,6 +2054,35 @@ void AdaptDispatch::SetKeypadMode(const bool fApplicationMode) noexcept _terminalInput.SetInputMode(TerminalInput::Mode::Keypad, fApplicationMode); } +// CSI = flags ; mode u - Sets kitty keyboard protocol flags +void AdaptDispatch::SetKittyKeyboardProtocol(const VTParameter flags, const VTParameter mode) +{ + const auto kittyFlags = static_cast(flags.value_or(0)); + const auto KittyKeyboardProtocol = static_cast(mode.value_or(1)); + _terminalInput.SetKittyKeyboardProtocol(kittyFlags, KittyKeyboardProtocol); +} + +// CSI ? u - Queries current kitty keyboard protocol flags +void AdaptDispatch::QueryKittyKeyboardProtocol() +{ + const auto flags = static_cast(_terminalInput.GetKittyFlags()); + _ReturnCsiResponse(fmt::format(FMT_COMPILE(L"?{}u"), flags)); +} + +// CSI > flags u - Pushes current kitty keyboard flags onto the stack and sets new flags +void AdaptDispatch::PushKittyKeyboardProtocol(const VTParameter flags) +{ + const auto kittyFlags = static_cast(flags.value_or(0)); + _terminalInput.PushKittyFlags(kittyFlags); +} + +// CSI < count u - Pops one or more entries from the kitty keyboard stack +void AdaptDispatch::PopKittyKeyboardProtocol(const VTParameter count) +{ + const auto popCount = static_cast(count.value_or(1)); + _terminalInput.PopKittyFlags(popCount); +} + // Routine Description: // - Internal logic for adding or removing lines in the active screen buffer. // This also moves the cursor to the left margin, which is expected behavior for IL and DL. diff --git a/src/terminal/adapter/adaptDispatch.hpp b/src/terminal/adapter/adaptDispatch.hpp index a193a17602..bfeeb46e62 100644 --- a/src/terminal/adapter/adaptDispatch.hpp +++ b/src/terminal/adapter/adaptDispatch.hpp @@ -97,6 +97,10 @@ namespace Microsoft::Console::VirtualTerminal void RequestMode(const DispatchTypes::ModeParams param) override; // DECRQM void SetKeypadMode(const bool applicationMode) noexcept override; // DECKPAM, DECKPNM void SetAnsiMode(const bool ansiMode) override; // DECANM + void SetKittyKeyboardProtocol(const VTParameter flags, const VTParameter mode) override; // Kitty keyboard protocol CSI = flags ; mode u + void QueryKittyKeyboardProtocol() override; // Kitty keyboard protocol CSI ? u + void PushKittyKeyboardProtocol(const VTParameter flags) override; // Kitty keyboard protocol CSI > flags u + void PopKittyKeyboardProtocol(const VTParameter count) override; // Kitty keyboard protocol CSI < count u void SetTopBottomScrollingMargins(const VTInt topMargin, const VTInt bottomMargin) override; // DECSTBM void SetLeftRightScrollingMargins(const VTInt leftMargin, diff --git a/src/terminal/adapter/termDispatch.hpp b/src/terminal/adapter/termDispatch.hpp index 99c9033fee..0d33469f11 100644 --- a/src/terminal/adapter/termDispatch.hpp +++ b/src/terminal/adapter/termDispatch.hpp @@ -54,6 +54,10 @@ public: void DeleteColumn(const VTInt /*distance*/) override {} // DECDC void SetKeypadMode(const bool /*applicationMode*/) override {} // DECKPAM, DECKPNM void SetAnsiMode(const bool /*ansiMode*/) override {} // DECANM + void SetKittyKeyboardProtocol(const VTParameter /*flags*/, const VTParameter /*mode*/) override {} // CSI = flags ; mode u + void QueryKittyKeyboardProtocol() override {} // CSI ? u + void PushKittyKeyboardProtocol(const VTParameter /*flags*/) override {} // CSI > flags u + void PopKittyKeyboardProtocol(const VTParameter /*count*/) override {} // CSI < count u void SetTopBottomScrollingMargins(const VTInt /*topMargin*/, const VTInt /*bottomMargin*/) override {} // DECSTBM void SetLeftRightScrollingMargins(const VTInt /*leftMargin*/, const VTInt /*rightMargin*/) override {} // DECSLRM void EnquireAnswerback() override {} // ENQ diff --git a/src/terminal/input/lib/terminalinput.vcxproj b/src/terminal/input/lib/terminalinput.vcxproj index da082afbfe..2db4e9fd5c 100644 --- a/src/terminal/input/lib/terminalinput.vcxproj +++ b/src/terminal/input/lib/terminalinput.vcxproj @@ -12,7 +12,6 @@ - Create diff --git a/src/terminal/input/mouseInput.cpp b/src/terminal/input/mouseInput.cpp index 19052ad22d..02929b9822 100644 --- a/src/terminal/input/mouseInput.cpp +++ b/src/terminal/input/mouseInput.cpp @@ -487,7 +487,7 @@ TerminalInput::OutputType TerminalInput::_GenerateSGRSequence(const til::point p // True if the alternate buffer is active and alternate scroll mode is enabled and the event is a mouse wheel event. bool TerminalInput::ShouldSendAlternateScroll(const unsigned int button, const short delta) const noexcept { - const auto inAltBuffer{ _mouseInputState.inAlternateBuffer }; + const auto inAltBuffer{ _inAlternateBuffer }; const auto inAltScroll{ _inputMode.test(Mode::AlternateScroll) }; const auto wasMouseWheel{ (button == WM_MOUSEWHEEL || button == WM_MOUSEHWHEEL) && delta != 0 }; return inAltBuffer && inAltScroll && wasMouseWheel; diff --git a/src/terminal/input/mouseInputState.cpp b/src/terminal/input/mouseInputState.cpp deleted file mode 100644 index bf94794c55..0000000000 --- a/src/terminal/input/mouseInputState.cpp +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#include "precomp.h" -#include -#include "terminalInput.hpp" - -using namespace Microsoft::Console::VirtualTerminal; - -// Routine Description: -// - Notify the MouseInput handler that the screen buffer has been swapped to the alternate buffer -// Parameters: -// -// Return value: -// -void TerminalInput::UseAlternateScreenBuffer() noexcept -{ - _mouseInputState.inAlternateBuffer = true; -} - -// Routine Description: -// - Notify the MouseInput handler that the screen buffer has been swapped to the alternate buffer -// Parameters: -// -// Return value: -// -void TerminalInput::UseMainScreenBuffer() noexcept -{ - _mouseInputState.inAlternateBuffer = false; -} diff --git a/src/terminal/input/sources.inc b/src/terminal/input/sources.inc index 8ba6af0433..cf6c95dd2c 100644 --- a/src/terminal/input/sources.inc +++ b/src/terminal/input/sources.inc @@ -30,7 +30,6 @@ PRECOMPILED_INCLUDE = ..\precomp.h SOURCES= \ ..\terminalInput.cpp \ ..\mouseInput.cpp \ - ..\mouseInputState.cpp \ INCLUDES = \ $(INCLUDES); \ diff --git a/src/terminal/input/terminalInput.cpp b/src/terminal/input/terminalInput.cpp index 8f5fb0c820..4654bc918b 100644 --- a/src/terminal/input/terminalInput.cpp +++ b/src/terminal/input/terminalInput.cpp @@ -6,8 +6,6 @@ #include -#include "../../inc/unicode.hpp" -#include "../../interactivity/inc/VtApiRedirection.hpp" #include "../types/inc/IInputEvent.hpp" using namespace std::string_literals; @@ -31,6 +29,16 @@ TerminalInput::TerminalInput() noexcept _initKeyboardMap(); } +void TerminalInput::UseAlternateScreenBuffer() noexcept +{ + _inAlternateBuffer = true; +} + +void TerminalInput::UseMainScreenBuffer() noexcept +{ + _inAlternateBuffer = false; +} + void TerminalInput::SetInputMode(const Mode mode, const bool enabled) noexcept { // If we're changing a tracking mode, we always clear other tracking modes first. @@ -70,6 +78,7 @@ void TerminalInput::ResetInputModes() noexcept _inputMode = { Mode::Ansi, Mode::AutoRepeat, Mode::AlternateScroll }; _mouseInputState.lastPos = { -1, -1 }; _mouseInputState.lastButton = 0; + ResetKittyKeyboardProtocols(); _initKeyboardMap(); } @@ -78,6 +87,86 @@ void TerminalInput::ForceDisableWin32InputMode(const bool win32InputMode) noexce _forceDisableWin32InputMode = win32InputMode; } +void TerminalInput::ForceDisableKittyKeyboardProtocol(const bool disable) noexcept +{ + _forceDisableKittyKeyboardProtocol = disable; + if (disable) + { + _kittyFlags = 0; + } +} + +// Kitty keyboard protocol methods + +void TerminalInput::SetKittyKeyboardProtocol(const uint8_t flags, const KittyKeyboardProtocolMode mode) noexcept +{ + if (_forceDisableKittyKeyboardProtocol) + { + return; + } + + switch (mode) + { + case KittyKeyboardProtocolMode::Replace: + _kittyFlags = flags & KittyKeyboardProtocolFlags::All; + break; + case KittyKeyboardProtocolMode::Set: + _kittyFlags |= (flags & KittyKeyboardProtocolFlags::All); + break; + case KittyKeyboardProtocolMode::Reset: + _kittyFlags &= ~(flags & KittyKeyboardProtocolFlags::All); + break; + } +} + +uint8_t TerminalInput::GetKittyFlags() const noexcept +{ + return _kittyFlags; +} + +void TerminalInput::PushKittyFlags(const uint8_t flags) noexcept +{ + if (_forceDisableKittyKeyboardProtocol) + { + return; + } + + auto& stack = _getKittyStack(); + // Evict oldest entry if stack is full (DoS prevention) + if (stack.size() >= KittyStackMaxSize) + { + stack.erase(stack.begin()); + } + stack.push_back(_kittyFlags); + _kittyFlags = flags & KittyKeyboardProtocolFlags::All; +} + +void TerminalInput::PopKittyFlags(const 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()) + { + stack.clear(); + _kittyFlags = 0; + return; + } + // Pop the requested number of entries, restoring flags from last popped + for (size_t i = 0; i < count; ++i) + { + _kittyFlags = stack.back(); + stack.pop_back(); + } +} + +void TerminalInput::ResetKittyKeyboardProtocols() noexcept +{ + _kittyFlags = 0; + _kittyMainStack.clear(); + _kittyAltStack.clear(); +} + TerminalInput::OutputType TerminalInput::MakeUnhandled() noexcept { return {}; @@ -121,12 +210,20 @@ TerminalInput::OutputType TerminalInput::HandleKey(const INPUT_RECORD& event) } const auto controlKeyState = _trackControlKeyState(keyEvent); + 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) { @@ -669,3 +766,617 @@ TerminalInput::OutputType TerminalInput::_makeWin32Output(const KEY_EVENT_RECORD // Rc: the value of wRepeatCount - any number. If omitted, defaults to '1'. return fmt::format(FMT_COMPILE(L"{}{};{};{};{};{};{}_"), _csi, vk, sc, uc, kd, cs, rc); } + +// Generates kitty keyboard protocol output for a key event. +// https://sw.kovidgoyal.net/kitty/keyboard-protocol/ +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) + const auto isRepeat = _lastVirtualKeyCode == virtualKeyCode && isKeyDown; + if (!isKeyDown) + { + if (_lastVirtualKeyCode == virtualKeyCode) + { + _lastVirtualKeyCode = std::nullopt; + } + } + 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)) + { + 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) + { + case VK_UP: + if (isEnhanced) + { + legacyFinalChar = L'A'; + } + break; + case VK_DOWN: + if (isEnhanced) + { + legacyFinalChar = L'B'; + } + break; + case VK_RIGHT: + if (isEnhanced) + { + legacyFinalChar = L'C'; + } + 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; + if (WI_IsFlagSet(controlKeyState, SHIFT_PRESSED)) + { + modifiers |= 1; + } + if (WI_IsAnyFlagSet(controlKeyState, ALT_PRESSED)) + { + modifiers |= 2; + } + if (WI_IsAnyFlagSet(controlKeyState, CTRL_PRESSED)) + { + modifiers |= 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) + { + if (WI_IsFlagSet(controlKeyState, CAPSLOCK_ON)) + { + modifiers |= 64; + } + if (WI_IsFlagSet(controlKeyState, NUMLOCK_ON)) + { + modifiers |= 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; + } + + // If this is a key that uses legacy CSI sequences, generate it + if (legacyFinalChar != 0) + { + // Format: CSI param ; modifiers ~ or CSI param ; modifiers : event-type ~ + std::wstring seq; + seq.append(_csi); + if (legacyParam > 1) + { + fmt::format_to(std::back_inserter(seq), FMT_COMPILE(L"{}"), legacyParam); + } + if (encodedModifiers > 1 || (reportEventTypes && eventType > 1)) + { + 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":{}"), eventType); + } + } + seq.push_back(legacyFinalChar); + return 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; + + // 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); + + // 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) + { + seq.push_back(L':'); + if (shiftedKey != 0) + { + fmt::format_to(std::back_inserter(seq), FMT_COMPILE(L"{}"), shiftedKey); + } + if (baseLayoutKey != 0) + { + fmt::format_to(std::back_inserter(seq), FMT_COMPILE(L":{}"), baseLayoutKey); + } + } + + // 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) + { + // 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":{}"), eventType); + } + if (needsText) + { + fmt::format_to(std::back_inserter(seq), FMT_COMPILE(L";{}"), fullCodepoint); + } + } + + seq.push_back(L'u'); + return 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 +{ + const auto isEnhanced = WI_IsFlagSet(controlKeyState, ENHANCED_KEY); + + switch (virtualKeyCode) + { + // Special keys with C0 control codes + case VK_ESCAPE: + return 27; // ESCAPE + case VK_RETURN: + return isEnhanced ? 57414 : 13; // KP_RETURN : ENTER + case VK_TAB: + return 9; // TAB + case VK_BACK: + return 127; // BACKSPACE + + // Navigation keys - when ENHANCED_KEY is not set, these are keypad keys + case VK_INSERT: + return isEnhanced ? 0 : 57425; // legacy : KP_INSERT + case VK_DELETE: + return isEnhanced ? 0 : 57426; // legacy : KP_DELETE + case VK_LEFT: + return isEnhanced ? 0 : 57417; // legacy : KP_LEFT + case VK_RIGHT: + return isEnhanced ? 0 : 57418; // legacy : KP_RIGHT + case VK_UP: + return isEnhanced ? 0 : 57419; // legacy : KP_UP + case VK_DOWN: + return isEnhanced ? 0 : 57420; // legacy : KP_DOWN + case VK_PRIOR: + return isEnhanced ? 0 : 57421; // legacy : KP_PAGE_UP + case VK_NEXT: + return isEnhanced ? 0 : 57422; // legacy : KP_PAGE_DOWN + case VK_HOME: + return isEnhanced ? 0 : 57423; // legacy : KP_HOME + case VK_END: + return isEnhanced ? 0 : 57424; // legacy : KP_END + + // Lock keys + case VK_CAPITAL: + return 57358; // CAPS_LOCK + case VK_SCROLL: + return 57359; // SCROLL_LOCK + case VK_NUMLOCK: + return 57360; // NUM_LOCK + + // Other special keys + case VK_SNAPSHOT: + return 57361; // PRINT_SCREEN + case VK_PAUSE: + return 57362; // PAUSE + case VK_APPS: + return 57363; // MENU + + // Function keys + case VK_F1: + case VK_F2: + case VK_F3: + case VK_F4: + case VK_F5: + case VK_F6: + case VK_F7: + case VK_F8: + case VK_F9: + case VK_F10: + case VK_F11: + case VK_F12: + return 0; // Use legacy sequences + case VK_F13: + case VK_F14: + case VK_F15: + case VK_F16: + case VK_F17: + case VK_F18: + case VK_F19: + case VK_F20: + case VK_F21: + case VK_F22: + case VK_F23: + case VK_F24: + return 57376 + (virtualKeyCode - VK_F13); // F13-F24 + + // Keypad keys + case VK_NUMPAD0: + case VK_NUMPAD1: + case VK_NUMPAD2: + case VK_NUMPAD3: + case VK_NUMPAD4: + case VK_NUMPAD5: + case VK_NUMPAD6: + case VK_NUMPAD7: + case VK_NUMPAD8: + case VK_NUMPAD9: + return 57399 + (virtualKeyCode - VK_NUMPAD0); // KP_0-KP_9 + case VK_DECIMAL: + return 57409; // KP_DECIMAL + case VK_DIVIDE: + return 57410; // KP_DIVIDE + case VK_MULTIPLY: + return 57411; // KP_MULTIPLY + case VK_SUBTRACT: + return 57412; // KP_SUBTRACT + case VK_ADD: + return 57413; // KP_ADD + case VK_SEPARATOR: + return 57416; // KP_SEPARATOR + case VK_CLEAR: + return 57427; // KP_BEGIN + + // Media keys + case VK_MEDIA_PLAY_PAUSE: + return 57430; // MEDIA_PLAY_PAUSE + case VK_MEDIA_STOP: + return 57432; // MEDIA_STOP + case VK_MEDIA_NEXT_TRACK: + return 57435; // MEDIA_TRACK_NEXT + case VK_MEDIA_PREV_TRACK: + return 57436; // MEDIA_TRACK_PREVIOUS + case VK_VOLUME_DOWN: + return 57438; // LOWER_VOLUME + case VK_VOLUME_UP: + return 57439; // RAISE_VOLUME + case VK_VOLUME_MUTE: + return 57440; // MUTE_VOLUME + + // Modifier keys + case VK_SHIFT: + return virtualScanCode == 0x2A ? 57441 : 57447; // LEFT_SHIFT : RIGHT_SHIFT + case VK_LSHIFT: + return 57441; // LEFT_SHIFT + case VK_RSHIFT: + return 57447; // RIGHT_SHIFT + case VK_CONTROL: + return isEnhanced ? 57448 : 57442; // RIGHT_CONTROL : LEFT_CONTROL + case VK_LCONTROL: + return 57442; // LEFT_CONTROL + case VK_RCONTROL: + return 57448; // RIGHT_CONTROL + case VK_MENU: + return isEnhanced ? 57449 : 57443; // RIGHT_ALT : LEFT_ALT + case VK_LMENU: + return 57443; // LEFT_ALT + case VK_RMENU: + return 57449; // RIGHT_ALT + case VK_LWIN: + return 57444; // LEFT_SUPER + case VK_RWIN: + return 57450; // RIGHT_SUPER + + default: + return 0; + } +} + +std::vector& TerminalInput::_getKittyStack() noexcept +{ + return _inAlternateBuffer ? _kittyAltStack : _kittyMainStack; +} + +bool TerminalInput::_codepointIsNonControl(uint32_t cp) noexcept +{ + return cp > 0x1f && (cp < 0x7f || cp > 0x9f); +} + +TerminalInput::CodepointBuffer TerminalInput::_codepointToBuffer(uint32_t cp) noexcept +{ + CodepointBuffer cb; + if (cp <= 0xFFFF) + { + cb.buf[0] = static_cast(cp); + cb.buf[1] = 0; + cb.len = 1; + } + else + { + cp -= 0x10000; + cb.buf[0] = static_cast((cp >> 10) + 0xD800); + cb.buf[1] = static_cast((cp & 0x3FF) + 0xDC00); + cb.buf[2] = 0; + cb.len = 2; + } + return cb; +} + +uint32_t TerminalInput::_bufferToCodepoint(const wchar_t* str) noexcept +{ + if (til::is_leading_surrogate(str[0]) && til::is_trailing_surrogate(str[1])) + { + return til::combine_surrogates(str[0], str[1]); + } + return str[0]; +} + +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) + { + return _bufferToCodepoint(cb.buf); + } + return cp; +} diff --git a/src/terminal/input/terminalInput.hpp b/src/terminal/input/terminalInput.hpp index a9abf74f79..ce24c2e5b0 100644 --- a/src/terminal/input/terminalInput.hpp +++ b/src/terminal/input/terminalInput.hpp @@ -47,26 +47,55 @@ namespace Microsoft::Console::VirtualTerminal AlternateScroll }; + // Kitty keyboard protocol progressive enhancement flags + // https://sw.kovidgoyal.net/kitty/keyboard-protocol/ + 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 All = (1 << 5) - 1; + }; + enum class KittyKeyboardProtocolMode : uint8_t + { + Replace = 1, + Set = 2, + Reset = 3, + }; + TerminalInput() noexcept; - void SetInputMode(const Mode mode, const bool enabled) noexcept; - bool GetInputMode(const Mode mode) const noexcept; + void UseAlternateScreenBuffer() noexcept; + void UseMainScreenBuffer() noexcept; + void SetInputMode(Mode mode, bool enabled) noexcept; + bool GetInputMode(Mode mode) const noexcept; void ResetInputModes() noexcept; - void ForceDisableWin32InputMode(const bool win32InputMode) noexcept; + void ForceDisableWin32InputMode(bool win32InputMode) noexcept; + void ForceDisableKittyKeyboardProtocol(bool disable) noexcept; + + // Kitty keyboard protocol methods + void SetKittyKeyboardProtocol(uint8_t flags, KittyKeyboardProtocolMode mode) noexcept; + uint8_t GetKittyFlags() const noexcept; + void PushKittyFlags(uint8_t flags) noexcept; + void PopKittyFlags(size_t count) noexcept; + void ResetKittyKeyboardProtocols() noexcept; #pragma region MouseInput // These methods are defined in mouseInput.cpp bool IsTrackingMouseInput() const noexcept; - bool ShouldSendAlternateScroll(const unsigned int button, const short delta) const noexcept; -#pragma endregion - -#pragma region MouseInputState Management - // These methods are defined in mouseInputState.cpp - void UseAlternateScreenBuffer() noexcept; - void UseMainScreenBuffer() noexcept; + bool ShouldSendAlternateScroll(unsigned int button, short delta) const noexcept; #pragma endregion private: + struct CodepointBuffer + { + wchar_t buf[3]; + uint16_t len; + }; + // storage location for the leading surrogate of a utf-16 surrogate pair wchar_t _leadingSurrogate = 0; @@ -80,24 +109,38 @@ namespace Microsoft::Console::VirtualTerminal til::enumset _inputMode{ Mode::Ansi, Mode::AutoRepeat, Mode::AlternateScroll }; bool _forceDisableWin32InputMode{ false }; + bool _inAlternateBuffer{ false }; + + // Kitty keyboard protocol state - separate stacks for main and alternate screen buffers + static constexpr size_t KittyStackMaxSize = 16; + bool _forceDisableKittyKeyboardProtocol = false; + uint8_t _kittyFlags = 0; + std::vector _kittyMainStack; + std::vector _kittyAltStack; const wchar_t* _csi = L"\x1B["; const wchar_t* _ss3 = L"\x1BO"; void _initKeyboardMap() noexcept; DWORD _trackControlKeyState(const KEY_EVENT_RECORD& key) noexcept; - std::array _getKeyboardState(const WORD virtualKeyCode, const DWORD controlKeyState) const; - [[nodiscard]] static wchar_t _makeCtrlChar(const wchar_t ch); + std::array _getKeyboardState(WORD virtualKeyCode, DWORD controlKeyState) const; + [[nodiscard]] static wchar_t _makeCtrlChar(wchar_t ch); [[nodiscard]] StringType _makeCharOutput(wchar_t ch); [[nodiscard]] static StringType _makeNoOutput() noexcept; - [[nodiscard]] void _escapeOutput(StringType& charSequence, const bool altIsPressed) const; + 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; + std::vector& _getKittyStack() noexcept; + static bool _codepointIsNonControl(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; #pragma region MouseInputState Management // These methods are defined in mouseInputState.cpp struct MouseInputState { - bool inAlternateBuffer{ false }; til::point lastPos{ -1, -1 }; unsigned int lastButton{ 0 }; int accumulatedDelta{ 0 }; @@ -113,7 +156,7 @@ namespace Microsoft::Console::VirtualTerminal [[nodiscard]] OutputType _makeAlternateScrollOutput(unsigned int button, short delta) const; - static constexpr unsigned int s_GetPressedButton(const MouseButtonState state) noexcept; + static constexpr unsigned int s_GetPressedButton(MouseButtonState state) noexcept; #pragma endregion }; } diff --git a/src/terminal/parser/OutputStateMachineEngine.cpp b/src/terminal/parser/OutputStateMachineEngine.cpp index 4febc78ee8..22984909b5 100644 --- a/src/terminal/parser/OutputStateMachineEngine.cpp +++ b/src/terminal/parser/OutputStateMachineEngine.cpp @@ -546,6 +546,18 @@ bool OutputStateMachineEngine::ActionCsiDispatch(const VTID id, const VTParamete case CsiActionCodes::ANSISYSRC_CursorRestore: _dispatch->CursorRestoreState(); break; + case CsiActionCodes::KKP_KittyKeyboardSet: + _dispatch->SetKittyKeyboardProtocol(parameters.at(0), parameters.at(1)); + break; + case CsiActionCodes::KKP_KittyKeyboardQuery: + _dispatch->QueryKittyKeyboardProtocol(); + break; + case CsiActionCodes::KKP_KittyKeyboardPush: + _dispatch->PushKittyKeyboardProtocol(parameters.at(0)); + break; + case CsiActionCodes::KKP_KittyKeyboardPop: + _dispatch->PopKittyKeyboardProtocol(parameters.at(0)); + break; case CsiActionCodes::IL_InsertLine: _dispatch->InsertLine(parameters.at(0)); break; diff --git a/src/terminal/parser/OutputStateMachineEngine.hpp b/src/terminal/parser/OutputStateMachineEngine.hpp index d36789e108..f2a850d7f3 100644 --- a/src/terminal/parser/OutputStateMachineEngine.hpp +++ b/src/terminal/parser/OutputStateMachineEngine.hpp @@ -136,6 +136,10 @@ namespace Microsoft::Console::VirtualTerminal DECSLRM_SetLeftRightMargins = VTID("s"), DTTERM_WindowManipulation = VTID("t"), // NOTE: Overlaps with DECSLPP. Fix when/if implemented. ANSISYSRC_CursorRestore = VTID("u"), + KKP_KittyKeyboardSet = VTID("=u"), + KKP_KittyKeyboardQuery = VTID("?u"), + KKP_KittyKeyboardPush = VTID(">u"), + KKP_KittyKeyboardPop = VTID(" Date: Fri, 30 Jan 2026 00:01:10 +0100 Subject: [PATCH 2/2] Spel --- .github/actions/spelling/expect/expect.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/actions/spelling/expect/expect.txt b/.github/actions/spelling/expect/expect.txt index 0b977d1fcd..65c588c0ab 100644 --- a/.github/actions/spelling/expect/expect.txt +++ b/.github/actions/spelling/expect/expect.txt @@ -866,6 +866,7 @@ KILLACTIVE KILLFOCUS kinda KIYEOK +KKP KLF KLMNO KOK @@ -885,6 +886,7 @@ LBUTTONDOWN LBUTTONUP lcb lci +LCMAP LCONTROL LCTRL lcx