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
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 09d83fcfc4..0d1c0cfbf5 100644
--- a/src/cascadia/TerminalSettingsEditor/ProfileViewModel.h
+++ b/src/cascadia/TerminalSettingsEditor/ProfileViewModel.h
@@ -145,6 +145,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 fafc7e11da..1fb13853a6 100644
--- a/src/cascadia/TerminalSettingsEditor/ProfileViewModel.idl
+++ b/src/cascadia/TerminalSettingsEditor/ProfileViewModel.idl
@@ -134,6 +134,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 b5b10c2af8..b8701d7132 100644
--- a/src/cascadia/TerminalSettingsEditor/Profiles_Terminal.xaml
+++ b/src/cascadia/TerminalSettingsEditor/Profiles_Terminal.xaml
@@ -51,6 +51,16 @@
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.
@@ -2583,19 +2591,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.
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/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("