From ad501c9d92b32672881c2af9442ccfc0e8bfa7e0 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Tue, 3 Feb 2026 00:07:02 +0100 Subject: [PATCH] wip --- reference.cpp | 208 +++ src/buffer/out/ut_textbuffer/ReflowTests.cpp | 88 +- src/inc/consoletaeftemplates.hpp | 174 ++ .../UiaTextRangeTests.cpp | 91 +- src/terminal/adapter/ITermDispatch.hpp | 2 +- src/terminal/adapter/adaptDispatch.cpp | 6 +- src/terminal/adapter/adaptDispatch.hpp | 2 +- src/terminal/adapter/termDispatch.hpp | 2 +- .../ut_adapter/Adapter.UnitTests.vcxproj | 3 +- .../Adapter.UnitTests.vcxproj.filters | 8 + .../ut_adapter/kittyKeyboardProtocol.cpp | 720 ++++++++ src/terminal/input/terminalInput.cpp | 1509 ++++++++--------- src/terminal/input/terminalInput.hpp | 38 +- .../input/ut_kittyKeyboardProtocol.cpp | 1410 --------------- 14 files changed, 1870 insertions(+), 2391 deletions(-) create mode 100644 reference.cpp create mode 100644 src/terminal/adapter/ut_adapter/kittyKeyboardProtocol.cpp delete mode 100644 src/terminal/input/ut_kittyKeyboardProtocol.cpp diff --git a/reference.cpp b/reference.cpp new file mode 100644 index 0000000000..97ea4a0c66 --- /dev/null +++ b/reference.cpp @@ -0,0 +1,208 @@ + +void TerminalInput::_initKeyboardMap() noexcept +try +{ + auto defineKeyWithUnusedModifiers = [this](const int keyCode, const std::wstring& sequence) { + for (auto m = 0; m < 8; m++) + _keyMap[VTModifier(m) + keyCode] = sequence; + }; + auto defineKeyWithAltModifier = [this](const int keyCode, const std::wstring& sequence) { + _keyMap[keyCode] = sequence; + _keyMap[Alt + keyCode] = L"\x1B" + sequence; + }; + auto defineKeypadKey = [this](const int keyCode, const wchar_t* prefix, const wchar_t finalChar) { + _keyMap[keyCode] = fmt::format(FMT_COMPILE(L"{}{}"), prefix, finalChar); + for (auto m = 1; m < 8; m++) + _keyMap[VTModifier(m) + keyCode] = fmt::format(FMT_COMPILE(L"{}1;{}{}"), _csi, m + 1, finalChar); + }; + auto defineEditingKey = [this](const int keyCode, const int parm) { + _keyMap[keyCode] = fmt::format(FMT_COMPILE(L"{}{}~"), _csi, parm); + for (auto m = 1; m < 8; m++) + _keyMap[VTModifier(m) + keyCode] = fmt::format(FMT_COMPILE(L"{}{};{}~"), _csi, parm, m + 1); + }; + auto defineNumericKey = [this](const int keyCode, const wchar_t finalChar) { + _keyMap[keyCode] = fmt::format(FMT_COMPILE(L"{}{}"), _ss3, finalChar); + for (auto m = 1; m < 8; m++) + _keyMap[VTModifier(m) + keyCode] = fmt::format(FMT_COMPILE(L"{}{}{}"), _ss3, m + 1, finalChar); + }; + + _keyMap.clear(); + + // The CSI and SS3 introducers are C1 control codes, which can either be + // sent as a single codepoint, or as a two character escape sequence. + if (_inputMode.test(Mode::SendC1)) + { + _csi = L"\x9B"; + _ss3 = L"\x8F"; + } + else + { + _csi = L"\x1B["; + _ss3 = L"\x1BO"; + } + + // PAUSE doesn't have a VT mapping, but traditionally we've mapped it to ^Z, + // regardless of modifiers. + defineKeyWithUnusedModifiers(VK_PAUSE, L"\x1A"s); + + // BACKSPACE maps to either DEL or BS, depending on the Backarrow Key mode. + // The Ctrl modifier inverts the active mode, swapping BS and DEL (this is + // not standard, but a modern terminal convention). The Alt modifier adds + // an ESC prefix (also not standard). + const auto backSequence = _inputMode.test(Mode::BackarrowKey) ? L"\b"s : L"\x7F"s; + const auto ctrlBackSequence = _inputMode.test(Mode::BackarrowKey) ? L"\x7F"s : L"\b"s; + defineKeyWithAltModifier(VK_BACK, backSequence); + defineKeyWithAltModifier(Ctrl + VK_BACK, ctrlBackSequence); + defineKeyWithAltModifier(Shift + VK_BACK, backSequence); + defineKeyWithAltModifier(Ctrl + Shift + VK_BACK, ctrlBackSequence); + + // TAB maps to HT, and Shift+TAB to CBT. The Ctrl modifier has no effect. + // The Alt modifier adds an ESC prefix, although in practice all the Alt + // mappings are likely to be system hotkeys. + const auto shiftTabSequence = fmt::format(FMT_COMPILE(L"{}Z"), _csi); + defineKeyWithAltModifier(VK_TAB, L"\t"s); + defineKeyWithAltModifier(Ctrl + VK_TAB, L"\t"s); + defineKeyWithAltModifier(Shift + VK_TAB, shiftTabSequence); + defineKeyWithAltModifier(Ctrl + Shift + VK_TAB, shiftTabSequence); + + // RETURN maps to either CR or CR LF, depending on the Line Feed mode. With + // a Ctrl modifier it maps to LF, because that's the expected behavior for + // most PC keyboard layouts. The Alt modifier adds an ESC prefix. + const auto returnSequence = _inputMode.test(Mode::LineFeed) ? L"\r\n"s : L"\r"s; + defineKeyWithAltModifier(VK_RETURN, returnSequence); + defineKeyWithAltModifier(Shift + VK_RETURN, returnSequence); + defineKeyWithAltModifier(Ctrl + VK_RETURN, L"\n"s); + defineKeyWithAltModifier(Ctrl + Shift + VK_RETURN, L"\n"s); + + // The keypad RETURN key works the same way, except when Keypad mode is + // enabled, but that's handled below with the other keypad keys. + defineKeyWithAltModifier(Enhanced + VK_RETURN, returnSequence); + defineKeyWithAltModifier(Shift + Enhanced + VK_RETURN, returnSequence); + defineKeyWithAltModifier(Ctrl + Enhanced + VK_RETURN, L"\n"s); + defineKeyWithAltModifier(Ctrl + Shift + Enhanced + VK_RETURN, L"\n"s); + + if (_inputMode.test(Mode::Ansi)) + { + // F1 to F4 map to the VT keypad function keys, which are SS3 sequences. + // When combined with a modifier, we use CSI sequences with the modifier + // embedded as a parameter (not standard - a modern terminal extension). + defineKeypadKey(VK_F1, _ss3, L'P'); + defineKeypadKey(VK_F2, _ss3, L'Q'); + defineKeypadKey(VK_F3, _ss3, L'R'); + defineKeypadKey(VK_F4, _ss3, L'S'); + + // F5 through F20 map to the top row VT function keys. They use standard + // DECFNK sequences with the modifier embedded as a parameter. The first + // five function keys on a VT terminal are typically local functions, so + // there's not much need to support mappings for them. + for (auto vk = VK_F5; vk <= VK_F20; vk++) + { + static constexpr std::array parameters = { 15, 17, 18, 19, 20, 21, 23, 24, 25, 26, 28, 29, 31, 32, 33, 34 }; + const auto parm = parameters.at(static_cast(vk) - VK_F5); + defineEditingKey(vk, parm); + } + + // Cursor keys follow a similar pattern to the VT keypad function keys, + // although they only use an SS3 prefix when the Cursor Key mode is set. + // When combined with a modifier, they'll use CSI sequences with the + // modifier embedded as a parameter (again not standard). + const auto ckIntroducer = _inputMode.test(Mode::CursorKey) ? _ss3 : _csi; + defineKeypadKey(VK_UP, ckIntroducer, L'A'); + defineKeypadKey(VK_DOWN, ckIntroducer, L'B'); + defineKeypadKey(VK_RIGHT, ckIntroducer, L'C'); + defineKeypadKey(VK_LEFT, ckIntroducer, L'D'); + defineKeypadKey(VK_CLEAR, ckIntroducer, L'E'); + defineKeypadKey(VK_HOME, ckIntroducer, L'H'); + defineKeypadKey(VK_END, ckIntroducer, L'F'); + + // Editing keys follow the same pattern as the top row VT function + // keys, using standard DECFNK sequences with the modifier embedded. + defineEditingKey(VK_INSERT, 2); + defineEditingKey(VK_DELETE, 3); + defineEditingKey(VK_PRIOR, 5); + defineEditingKey(VK_NEXT, 6); + + // Keypad keys depend on the Keypad mode. When reset, they transmit + // the ASCII character assigned by the keyboard layout, but when set + // they transmit SS3 escape sequences. When used with a modifier, the + // modifier is embedded as a parameter value (not standard). + if (Feature_KeypadModeEnabled::IsEnabled() && _inputMode.test(Mode::Keypad)) + { + defineNumericKey(VK_MULTIPLY, L'j'); + defineNumericKey(VK_ADD, L'k'); + defineNumericKey(VK_SEPARATOR, L'l'); + defineNumericKey(VK_SUBTRACT, L'm'); + defineNumericKey(VK_DECIMAL, L'n'); + defineNumericKey(VK_DIVIDE, L'o'); + + defineNumericKey(VK_NUMPAD0, L'p'); + defineNumericKey(VK_NUMPAD1, L'q'); + defineNumericKey(VK_NUMPAD2, L'r'); + defineNumericKey(VK_NUMPAD3, L's'); + defineNumericKey(VK_NUMPAD4, L't'); + defineNumericKey(VK_NUMPAD5, L'u'); + defineNumericKey(VK_NUMPAD6, L'v'); + defineNumericKey(VK_NUMPAD7, L'w'); + defineNumericKey(VK_NUMPAD8, L'x'); + defineNumericKey(VK_NUMPAD9, L'y'); + + defineNumericKey(Enhanced + VK_RETURN, L'M'); + } + } + else + { + // In VT52 mode, the sequences tend to use the same final character as + // their ANSI counterparts, but with a simple ESC prefix. The modifier + // keys have no effect. + + // VT52 only support PF1 through PF4 function keys. + defineKeyWithUnusedModifiers(VK_F1, L"\033P"s); + defineKeyWithUnusedModifiers(VK_F2, L"\033Q"s); + defineKeyWithUnusedModifiers(VK_F3, L"\033R"s); + defineKeyWithUnusedModifiers(VK_F4, L"\033S"s); + + // But terminals with application functions keys would + // map some of them as controls keys in VT52 mode. + defineKeyWithUnusedModifiers(VK_F11, L"\033"s); + defineKeyWithUnusedModifiers(VK_F12, L"\b"s); + defineKeyWithUnusedModifiers(VK_F13, L"\n"s); + + // Cursor keys use the same finals as the ANSI sequences. + defineKeyWithUnusedModifiers(VK_UP, L"\033A"s); + defineKeyWithUnusedModifiers(VK_DOWN, L"\033B"s); + defineKeyWithUnusedModifiers(VK_RIGHT, L"\033C"s); + defineKeyWithUnusedModifiers(VK_LEFT, L"\033D"s); + defineKeyWithUnusedModifiers(VK_CLEAR, L"\033E"s); + defineKeyWithUnusedModifiers(VK_HOME, L"\033H"s); + defineKeyWithUnusedModifiers(VK_END, L"\033F"s); + + // Keypad keys also depend on Keypad mode, the same as ANSI mappings, + // but the sequences use an ESC ? prefix instead of SS3. + if (Feature_KeypadModeEnabled::IsEnabled() && _inputMode.test(Mode::Keypad)) + { + defineKeyWithUnusedModifiers(VK_MULTIPLY, L"\033?j"s); + defineKeyWithUnusedModifiers(VK_ADD, L"\033?k"s); + defineKeyWithUnusedModifiers(VK_SEPARATOR, L"\033?l"s); + defineKeyWithUnusedModifiers(VK_SUBTRACT, L"\033?m"s); + defineKeyWithUnusedModifiers(VK_DECIMAL, L"\033?n"s); + defineKeyWithUnusedModifiers(VK_DIVIDE, L"\033?o"s); + + defineKeyWithUnusedModifiers(VK_NUMPAD0, L"\033?p"s); + defineKeyWithUnusedModifiers(VK_NUMPAD1, L"\033?q"s); + defineKeyWithUnusedModifiers(VK_NUMPAD2, L"\033?r"s); + defineKeyWithUnusedModifiers(VK_NUMPAD3, L"\033?s"s); + defineKeyWithUnusedModifiers(VK_NUMPAD4, L"\033?t"s); + defineKeyWithUnusedModifiers(VK_NUMPAD5, L"\033?u"s); + defineKeyWithUnusedModifiers(VK_NUMPAD6, L"\033?v"s); + defineKeyWithUnusedModifiers(VK_NUMPAD7, L"\033?w"s); + defineKeyWithUnusedModifiers(VK_NUMPAD8, L"\033?x"s); + defineKeyWithUnusedModifiers(VK_NUMPAD9, L"\033?y"s); + + defineKeyWithUnusedModifiers(Enhanced + VK_RETURN, L"\033?M"s); + } + } + + _focusInSequence = _csi + L"I"s; + _focusOutSequence = _csi + L"O"s; +} +CATCH_LOG() diff --git a/src/buffer/out/ut_textbuffer/ReflowTests.cpp b/src/buffer/out/ut_textbuffer/ReflowTests.cpp index 58e7766c89..f9753f1069 100644 --- a/src/buffer/out/ut_textbuffer/ReflowTests.cpp +++ b/src/buffer/out/ut_textbuffer/ReflowTests.cpp @@ -600,96 +600,12 @@ namespace }, }, }; - -#pragma region TAEF hookup for the test case array above - struct ArrayIndexTaefAdapterRow : public Microsoft::WRL::RuntimeClass, IDataRow> - { - HRESULT RuntimeClassInitialize(const size_t index) - { - _index = index; - return S_OK; - } - - STDMETHODIMP GetTestData(BSTR /*pszName*/, SAFEARRAY** ppData) override - { - const auto indexString{ wil::str_printf(L"%zu", _index) }; - auto safeArray{ SafeArrayCreateVector(VT_BSTR, 0, 1) }; - LONG index{ 0 }; - auto indexBstr{ wil::make_bstr(indexString.c_str()) }; - (void)SafeArrayPutElement(safeArray, &index, indexBstr.release()); - *ppData = safeArray; - return S_OK; - } - - STDMETHODIMP GetMetadataNames(SAFEARRAY** ppMetadataNames) override - { - *ppMetadataNames = nullptr; - return S_FALSE; - } - - STDMETHODIMP GetMetadata(BSTR /*pszName*/, SAFEARRAY** ppData) override - { - *ppData = nullptr; - return S_FALSE; - } - - STDMETHODIMP GetName(BSTR* ppszRowName) override - { - *ppszRowName = nullptr; - return S_FALSE; - } - - private: - size_t _index; - }; - - struct ArrayIndexTaefAdapterSource : public Microsoft::WRL::RuntimeClass, IDataSource> - { - STDMETHODIMP Advance(IDataRow** ppDataRow) override - { - if (_index < std::extent_v) - { - Microsoft::WRL::MakeAndInitialize(ppDataRow, _index++); - } - else - { - *ppDataRow = nullptr; - } - return S_OK; - } - - STDMETHODIMP Reset() override - { - _index = 0; - return S_OK; - } - - STDMETHODIMP GetTestDataNames(SAFEARRAY** names) override - { - auto safeArray{ SafeArrayCreateVector(VT_BSTR, 0, 1) }; - LONG index{ 0 }; - auto dataNameBstr{ wil::make_bstr(L"index") }; - (void)SafeArrayPutElement(safeArray, &index, dataNameBstr.release()); - *names = safeArray; - return S_OK; - } - - STDMETHODIMP GetTestDataType(BSTR /*name*/, BSTR* type) override - { - *type = nullptr; - return S_OK; - } - - private: - size_t _index{ 0 }; - }; -#pragma endregion } extern "C" HRESULT __declspec(dllexport) __cdecl ReflowTestDataSource(IDataSource** ppDataSource, void*) { - auto source{ Microsoft::WRL::Make() }; - return source.CopyTo(ppDataSource); + *ppDataSource = new ArrayIndexTaefAdapterSource>(std::size(testCases)); + return S_OK; } class ReflowTests diff --git a/src/inc/consoletaeftemplates.hpp b/src/inc/consoletaeftemplates.hpp index 2a98851eca..fbe1038f2d 100644 --- a/src/inc/consoletaeftemplates.hpp +++ b/src/inc/consoletaeftemplates.hpp @@ -19,6 +19,8 @@ Revision History: #include +#include + // Helper for declaring a variable to store a TEST_METHOD_PROPERTY and get it's value from the test metadata #define INIT_TEST_PROPERTY(type, identifier, description) \ type identifier; \ @@ -45,6 +47,178 @@ Revision History: namespace WEX::TestExecution { + struct ArrayIndexTaefAdapterRow : IDataRow + { + ArrayIndexTaefAdapterRow(size_t index) : + _index(index) {} + + // IUnknown + STDMETHODIMP QueryInterface(REFIID riid, void** ppvObject) override + { + if (!ppvObject) + { + return E_POINTER; + } + + if (riid == __uuidof(IUnknown)) + { + AddRef(); + *ppvObject = static_cast(this); + return S_OK; + } + else if (riid == __uuidof(IDataRow)) + { + *ppvObject = static_cast(this); + AddRef(); + return S_OK; + } + else + { + *ppvObject = nullptr; + return E_NOINTERFACE; + } + } + + ULONG STDMETHODCALLTYPE AddRef() override + { + return _refCount.fetch_add(1) + 1; + } + + ULONG STDMETHODCALLTYPE Release() override + { + const auto count = _refCount.fetch_sub(1) - 1; + if (count == 0) + { + delete this; + } + return count; + } + + // IDataRow + STDMETHODIMP GetTestData(BSTR /*pszName*/, SAFEARRAY** ppData) override + { + wchar_t buf[16]; + swprintf_s(buf, L"%zu", _index); + + LONG idx = 0; + const auto array = SafeArrayCreateVector(VT_BSTR, 0, 1); + SafeArrayPutElement(array, &idx, SysAllocString(buf)); + *ppData = array; + return S_OK; + } + + STDMETHODIMP GetMetadataNames(SAFEARRAY** ppMetadataNames) override + { + *ppMetadataNames = nullptr; + return S_FALSE; + } + + STDMETHODIMP GetMetadata(BSTR /*pszName*/, SAFEARRAY** ppData) override + { + *ppData = nullptr; + return S_FALSE; + } + + STDMETHODIMP GetName(BSTR* ppszRowName) override + { + *ppszRowName = nullptr; + return S_FALSE; + } + + private: + std::atomic _refCount{ 1 }; + size_t _index = 0; + }; + + struct ArrayIndexTaefAdapterSource : IDataSource + { + explicit ArrayIndexTaefAdapterSource(size_t count) : + _count{ count } {} + + // IUnknown + STDMETHODIMP QueryInterface(REFIID riid, void** ppvObject) override + { + if (!ppvObject) + { + return E_POINTER; + } + + if (riid == __uuidof(IUnknown)) + { + AddRef(); + *ppvObject = static_cast(this); + return S_OK; + } + else if (riid == __uuidof(IDataSource)) + { + *ppvObject = static_cast(this); + AddRef(); + return S_OK; + } + else + { + *ppvObject = nullptr; + return E_NOINTERFACE; + } + } + + ULONG STDMETHODCALLTYPE AddRef() override + { + return _refCount.fetch_add(1) + 1; + } + + ULONG STDMETHODCALLTYPE Release() override + { + const auto count = _refCount.fetch_sub(1) - 1; + if (count == 0) + { + delete this; + } + return count; + } + + // IDataSource + STDMETHODIMP Advance(IDataRow** ppDataRow) override + { + if (_index < _count) + { + *ppDataRow = static_cast(new ArrayIndexTaefAdapterRow(_index++)); + return S_OK; + } + else + { + *ppDataRow = nullptr; + } + return S_OK; + } + + STDMETHODIMP Reset() override + { + _index = 0; + return S_OK; + } + + STDMETHODIMP GetTestDataNames(SAFEARRAY** names) override + { + LONG idx = 0; + const auto array = SafeArrayCreateVector(VT_BSTR, 0, 1); + SafeArrayPutElement(array, &idx, SysAllocString(L"index")); + *names = array; + return S_OK; + } + + STDMETHODIMP GetTestDataType(BSTR /*name*/, BSTR* type) override + { + *type = nullptr; + return S_OK; + } + + private: + std::atomic _refCount{ 1 }; + size_t _count = 0; + size_t _index = 0; + }; + // Compare two floats using a ULP (unit last place) tolerance of up to 4. // Allows you to compare two floats that are almost equal. // Think of: 0.200000000000000 vs. 0.200000000000001. diff --git a/src/interactivity/win32/ut_interactivity_win32/UiaTextRangeTests.cpp b/src/interactivity/win32/ut_interactivity_win32/UiaTextRangeTests.cpp index 1fd10f2b11..a3bcd5a86d 100644 --- a/src/interactivity/win32/ut_interactivity_win32/UiaTextRangeTests.cpp +++ b/src/interactivity/win32/ut_interactivity_win32/UiaTextRangeTests.cpp @@ -109,97 +109,10 @@ static constexpr til::point point_offset_by_line(const til::point start, const t // IMPORTANT: reference this _after_ defining point_offset_by_XXX. We need it for some definitions #include "GeneratedUiaTextRangeMovementTests.g.cpp" -namespace -{ -#pragma region TAEF hookup for the test case array above - struct ArrayIndexTaefAdapterRow : public Microsoft::WRL::RuntimeClass, IDataRow> - { - HRESULT RuntimeClassInitialize(const size_t index) - { - _index = index; - return S_OK; - } - - STDMETHODIMP GetTestData(BSTR /*pszName*/, SAFEARRAY** ppData) override - { - const auto indexString{ wil::str_printf(L"%zu", _index) }; - auto safeArray{ SafeArrayCreateVector(VT_BSTR, 0, 1) }; - LONG index{ 0 }; - auto indexBstr{ wil::make_bstr(indexString.c_str()) }; - (void)SafeArrayPutElement(safeArray, &index, indexBstr.release()); - *ppData = safeArray; - return S_OK; - } - - STDMETHODIMP GetMetadataNames(SAFEARRAY** ppMetadataNames) override - { - *ppMetadataNames = nullptr; - return S_FALSE; - } - - STDMETHODIMP GetMetadata(BSTR /*pszName*/, SAFEARRAY** ppData) override - { - *ppData = nullptr; - return S_FALSE; - } - - STDMETHODIMP GetName(BSTR* ppszRowName) override - { - *ppszRowName = wil::make_bstr(s_movementTests[_index].name.data()).release(); - return S_OK; - } - - private: - size_t _index; - }; - - struct ArrayIndexTaefAdapterSource : public Microsoft::WRL::RuntimeClass, IDataSource> - { - STDMETHODIMP Advance(IDataRow** ppDataRow) override - { - if (_index < s_movementTests.size()) - { - Microsoft::WRL::MakeAndInitialize(ppDataRow, _index++); - } - else - { - *ppDataRow = nullptr; - } - return S_OK; - } - - STDMETHODIMP Reset() override - { - _index = 0; - return S_OK; - } - - STDMETHODIMP GetTestDataNames(SAFEARRAY** names) override - { - auto safeArray{ SafeArrayCreateVector(VT_BSTR, 0, 1) }; - LONG index{ 0 }; - auto dataNameBstr{ wil::make_bstr(L"index") }; - (void)SafeArrayPutElement(safeArray, &index, dataNameBstr.release()); - *names = safeArray; - return S_OK; - } - - STDMETHODIMP GetTestDataType(BSTR /*name*/, BSTR* type) override - { - *type = nullptr; - return S_OK; - } - - private: - size_t _index{ 0 }; - }; -#pragma endregion -} - extern "C" HRESULT __declspec(dllexport) __cdecl GeneratedMovementTestDataSource(IDataSource** ppDataSource, void*) { - auto source{ Microsoft::WRL::Make() }; - return source.CopyTo(ppDataSource); + *ppDataSource = new ArrayIndexTaefAdapterSource>(std::size(testCases)); + return S_OK; } // UiaTextRange takes an object that implements diff --git a/src/terminal/adapter/ITermDispatch.hpp b/src/terminal/adapter/ITermDispatch.hpp index c39d1b5c51..da4c8493c0 100644 --- a/src/terminal/adapter/ITermDispatch.hpp +++ b/src/terminal/adapter/ITermDispatch.hpp @@ -67,7 +67,7 @@ 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 SetKittyKeyboardProtocol(const VTParameter flags, const VTParameter mode) noexcept = 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 diff --git a/src/terminal/adapter/adaptDispatch.cpp b/src/terminal/adapter/adaptDispatch.cpp index 708fa7ebb8..c65af44800 100644 --- a/src/terminal/adapter/adaptDispatch.cpp +++ b/src/terminal/adapter/adaptDispatch.cpp @@ -2055,9 +2055,9 @@ void AdaptDispatch::SetKeypadMode(const bool fApplicationMode) noexcept } // CSI = flags ; mode u - Sets kitty keyboard protocol flags -void AdaptDispatch::SetKittyKeyboardProtocol(const VTParameter flags, const VTParameter mode) +void AdaptDispatch::SetKittyKeyboardProtocol(const VTParameter flags, const VTParameter mode) noexcept { - const auto kittyFlags = static_cast(flags.value_or(0)); + const auto kittyFlags = gsl::narrow_cast(flags.value_or(0)); const auto KittyKeyboardProtocol = static_cast(mode.value_or(1)); _terminalInput.SetKittyKeyboardProtocol(kittyFlags, KittyKeyboardProtocol); } @@ -2072,7 +2072,7 @@ void AdaptDispatch::QueryKittyKeyboardProtocol() // 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)); + const auto kittyFlags = gsl::narrow_cast(flags.value_or(0)); _terminalInput.PushKittyFlags(kittyFlags); } diff --git a/src/terminal/adapter/adaptDispatch.hpp b/src/terminal/adapter/adaptDispatch.hpp index bfeeb46e62..83f5ec869f 100644 --- a/src/terminal/adapter/adaptDispatch.hpp +++ b/src/terminal/adapter/adaptDispatch.hpp @@ -97,7 +97,7 @@ 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 SetKittyKeyboardProtocol(const VTParameter flags, const VTParameter mode) noexcept 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 diff --git a/src/terminal/adapter/termDispatch.hpp b/src/terminal/adapter/termDispatch.hpp index 0d33469f11..66af5352f6 100644 --- a/src/terminal/adapter/termDispatch.hpp +++ b/src/terminal/adapter/termDispatch.hpp @@ -54,7 +54,7 @@ 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 SetKittyKeyboardProtocol(const VTParameter /*flags*/, const VTParameter /*mode*/) noexcept 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 diff --git a/src/terminal/adapter/ut_adapter/Adapter.UnitTests.vcxproj b/src/terminal/adapter/ut_adapter/Adapter.UnitTests.vcxproj index be948b1b5b..69bb1147c2 100644 --- a/src/terminal/adapter/ut_adapter/Adapter.UnitTests.vcxproj +++ b/src/terminal/adapter/ut_adapter/Adapter.UnitTests.vcxproj @@ -13,6 +13,7 @@ + Create @@ -71,4 +72,4 @@ - + \ No newline at end of file diff --git a/src/terminal/adapter/ut_adapter/Adapter.UnitTests.vcxproj.filters b/src/terminal/adapter/ut_adapter/Adapter.UnitTests.vcxproj.filters index d114857335..00713d0322 100644 --- a/src/terminal/adapter/ut_adapter/Adapter.UnitTests.vcxproj.filters +++ b/src/terminal/adapter/ut_adapter/Adapter.UnitTests.vcxproj.filters @@ -27,10 +27,18 @@ Source Files + + Source Files + Header Files + + + + + \ No newline at end of file diff --git a/src/terminal/adapter/ut_adapter/kittyKeyboardProtocol.cpp b/src/terminal/adapter/ut_adapter/kittyKeyboardProtocol.cpp new file mode 100644 index 0000000000..4108448251 --- /dev/null +++ b/src/terminal/adapter/ut_adapter/kittyKeyboardProtocol.cpp @@ -0,0 +1,720 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include +#include + +#include "../../input/terminalInput.hpp" + +using namespace WEX::TestExecution; +using namespace WEX::Logging; +using namespace WEX::Common; +using namespace Microsoft::Console::VirtualTerminal; +using KittyKeyboardProtocolFlags = TerminalInput::KittyKeyboardProtocolFlags; +using KittyKeyboardProtocolMode = TerminalInput::KittyKeyboardProtocolMode; + +namespace +{ + TerminalInput::OutputType process(TerminalInput& input, bool keyDown, uint16_t vk, uint16_t sc, wchar_t ch, uint32_t state) + { + INPUT_RECORD record{}; + record.EventType = KEY_EVENT; + record.Event.KeyEvent.bKeyDown = keyDown ? TRUE : FALSE; + record.Event.KeyEvent.wRepeatCount = 1; + record.Event.KeyEvent.wVirtualKeyCode = vk; + record.Event.KeyEvent.wVirtualScanCode = sc; + record.Event.KeyEvent.uChar.UnicodeChar = ch; + record.Event.KeyEvent.dwControlKeyState = state; + return input.HandleKey(record); + } + + TerminalInput createInput(uint8_t flags) + { + TerminalInput input; + input.SetKittyKeyboardProtocol(flags, KittyKeyboardProtocolMode::Replace); + return input; + } + + // Kitty modifier bit values (as transmitted, before adding 1): + // shift=1, alt=2, ctrl=4, super=8, hyper=16, meta=32, caps_lock=64, num_lock=128 + // Transmitted as: 1 + modifiers + + // CSI = "\x1b[" + + // Helper macros for common state combinations + constexpr auto ALT_PRESSED = LEFT_ALT_PRESSED; // Use left for consistency + constexpr auto CTRL_PRESSED = LEFT_CTRL_PRESSED; + + struct TestCase + { + std::wstring_view name; + std::wstring_view expected; + uint8_t flags; // KittyKeyboardProtocolFlags + bool keyDown; + uint16_t vk; + uint16_t sc; + wchar_t ch; + uint32_t state; + }; + + // ======================================================================== + // Test case organization: + // + // 1. FLAG COMBINATIONS (32 total = 2^5 enhancement flags) + // Testing each flag combination with a representative key + // + // 2. MODIFIER COMBINATIONS + // Testing all modifier permutations (shift, alt, ctrl, caps_lock, num_lock) + // + // 3. SPECIAL KEY BEHAVIORS + // - Enter/Tab/Backspace legacy behavior + // - Escape key disambiguation + // - Keypad keys + // - Function keys + // - Lock keys + // - Modifier keys themselves + // + // 4. EVENT TYPES + // - Press, repeat, release events + // - Special handling for Enter/Tab/Backspace release + // + // 5. ALTERNATE KEYS + // - Shifted key codes + // - Base layout key codes + // + // 6. TEXT AS CODEPOINTS + // - Text embedded in escape codes + // ======================================================================== + + constexpr TestCase testCases[] = { + // ==================================================================== + // SECTION 1: Enhancement Flag Combinations (32 combinations) + // Using Escape key as representative since it's affected by Disambiguate + // ==================================================================== + + // flags=0 (0b00000): No enhancements - legacy mode + // Escape key in legacy mode: just ESC byte + TestCase{ L"Flags=0 (none) Esc key", L"\x1b", 0, true, VK_ESCAPE, 0x01, 0, 0 }, + + // flags=1 (0b00001): DisambiguateEscapeCodes only + // Escape key becomes CSI 27 u + TestCase{ L"Flags=1 (Disambiguate) Esc key", L"\x1b[27u", 1, true, VK_ESCAPE, 0x01, 0, 0 }, + + // flags=2 (0b00010): ReportEventTypes only + // No disambiguation, so Esc is still legacy (but with event type tracking internally) + TestCase{ L"Flags=2 (EventTypes) Esc key down", L"\x1b", 2, true, VK_ESCAPE, 0x01, 0, 0 }, + + // flags=3 (0b00011): Disambiguate + EventTypes + // Escape key with event type: CSI 27;1:1 u (mod=1, event=press=1) + TestCase{ L"Flags=3 (Disambiguate+EventTypes) Esc key press", L"\x1b[27;1:1u", 3, true, VK_ESCAPE, 0x01, 0, 0 }, + + // flags=4 (0b00100): ReportAlternateKeys only + // Without Disambiguate, Escape is still legacy + TestCase{ L"Flags=4 (AltKeys) Esc key", L"\x1b", 4, true, VK_ESCAPE, 0x01, 0, 0 }, + + // flags=5 (0b00101): Disambiguate + AltKeys + TestCase{ L"Flags=5 (Disambiguate+AltKeys) Esc key", L"\x1b[27u", 5, true, VK_ESCAPE, 0x01, 0, 0 }, + + // flags=6 (0b00110): EventTypes + AltKeys + TestCase{ L"Flags=6 (EventTypes+AltKeys) Esc key", L"\x1b", 6, true, VK_ESCAPE, 0x01, 0, 0 }, + + // flags=7 (0b00111): Disambiguate + EventTypes + AltKeys + TestCase{ L"Flags=7 (Disambiguate+EventTypes+AltKeys) Esc key press", L"\x1b[27;1:1u", 7, true, VK_ESCAPE, 0x01, 0, 0 }, + + // flags=8 (0b01000): ReportAllKeysAsEscapeCodes only + // All keys become CSI u, including Escape + TestCase{ L"Flags=8 (AllKeys) Esc key", L"\x1b[27u", 8, true, VK_ESCAPE, 0x01, 0, 0 }, + + // flags=9 (0b01001): Disambiguate + AllKeys + TestCase{ L"Flags=9 (Disambiguate+AllKeys) Esc key", L"\x1b[27u", 9, true, VK_ESCAPE, 0x01, 0, 0 }, + + // flags=10 (0b01010): EventTypes + AllKeys + TestCase{ L"Flags=10 (EventTypes+AllKeys) Esc key press", L"\x1b[27;1:1u", 10, true, VK_ESCAPE, 0x01, 0, 0 }, + + // flags=11 (0b01011): Disambiguate + EventTypes + AllKeys + TestCase{ L"Flags=11 (Disambiguate+EventTypes+AllKeys) Esc key press", L"\x1b[27;1:1u", 11, true, VK_ESCAPE, 0x01, 0, 0 }, + + // flags=12 (0b01100): AltKeys + AllKeys + TestCase{ L"Flags=12 (AltKeys+AllKeys) Esc key", L"\x1b[27u", 12, true, VK_ESCAPE, 0x01, 0, 0 }, + + // flags=13 (0b01101): Disambiguate + AltKeys + AllKeys + TestCase{ L"Flags=13 (Disambiguate+AltKeys+AllKeys) Esc key", L"\x1b[27u", 13, true, VK_ESCAPE, 0x01, 0, 0 }, + + // flags=14 (0b01110): EventTypes + AltKeys + AllKeys + TestCase{ L"Flags=14 (EventTypes+AltKeys+AllKeys) Esc key press", L"\x1b[27;1:1u", 14, true, VK_ESCAPE, 0x01, 0, 0 }, + + // flags=15 (0b01111): Disambiguate + EventTypes + AltKeys + AllKeys + TestCase{ L"Flags=15 (Disambiguate+EventTypes+AltKeys+AllKeys) Esc key press", L"\x1b[27;1:1u", 15, true, VK_ESCAPE, 0x01, 0, 0 }, + + // flags=16 (0b10000): ReportAssociatedText only (meaningless without AllKeys) + TestCase{ L"Flags=16 (AssocText) Esc key", L"\x1b", 16, true, VK_ESCAPE, 0x01, 0, 0 }, + + // flags=17 (0b10001): Disambiguate + AssocText + TestCase{ L"Flags=17 (Disambiguate+AssocText) Esc key", L"\x1b[27u", 17, true, VK_ESCAPE, 0x01, 0, 0 }, + + // flags=18 (0b10010): EventTypes + AssocText + TestCase{ L"Flags=18 (EventTypes+AssocText) Esc key", L"\x1b", 18, true, VK_ESCAPE, 0x01, 0, 0 }, + + // flags=19 (0b10011): Disambiguate + EventTypes + AssocText + TestCase{ L"Flags=19 (Disambiguate+EventTypes+AssocText) Esc key press", L"\x1b[27;1:1u", 19, true, VK_ESCAPE, 0x01, 0, 0 }, + + // flags=20 (0b10100): AltKeys + AssocText + TestCase{ L"Flags=20 (AltKeys+AssocText) Esc key", L"\x1b", 20, true, VK_ESCAPE, 0x01, 0, 0 }, + + // flags=21 (0b10101): Disambiguate + AltKeys + AssocText + TestCase{ L"Flags=21 (Disambiguate+AltKeys+AssocText) Esc key", L"\x1b[27u", 21, true, VK_ESCAPE, 0x01, 0, 0 }, + + // flags=22 (0b10110): EventTypes + AltKeys + AssocText + TestCase{ L"Flags=22 (EventTypes+AltKeys+AssocText) Esc key", L"\x1b", 22, true, VK_ESCAPE, 0x01, 0, 0 }, + + // flags=23 (0b10111): Disambiguate + EventTypes + AltKeys + AssocText + TestCase{ L"Flags=23 (Disambiguate+EventTypes+AltKeys+AssocText) Esc key press", L"\x1b[27;1:1u", 23, true, VK_ESCAPE, 0x01, 0, 0 }, + + // flags=24 (0b11000): AllKeys + AssocText + // 'a' key with text reporting: CSI 97;;97 u + TestCase{ L"Flags=24 (AllKeys+AssocText) 'a' key", L"\x1b[97;1;97u", 24, true, 'A', 0x1E, L'a', 0 }, + + // flags=25 (0b11001): Disambiguate + AllKeys + AssocText + TestCase{ L"Flags=25 (Disambiguate+AllKeys+AssocText) 'a' key", L"\x1b[97;1;97u", 25, true, 'A', 0x1E, L'a', 0 }, + + // flags=26 (0b11010): EventTypes + AllKeys + AssocText + TestCase{ L"Flags=26 (EventTypes+AllKeys+AssocText) 'a' key press", L"\x1b[97;1:1;97u", 26, true, 'A', 0x1E, L'a', 0 }, + + // flags=27 (0b11011): Disambiguate + EventTypes + AllKeys + AssocText + TestCase{ L"Flags=27 (Disambiguate+EventTypes+AllKeys+AssocText) 'a' key press", L"\x1b[97;1:1;97u", 27, true, 'A', 0x1E, L'a', 0 }, + + // flags=28 (0b11100): AltKeys + AllKeys + AssocText + TestCase{ L"Flags=28 (AltKeys+AllKeys+AssocText) 'a' key", L"\x1b[97;1;97u", 28, true, 'A', 0x1E, L'a', 0 }, + + // flags=29 (0b11101): Disambiguate + AltKeys + AllKeys + AssocText + TestCase{ L"Flags=29 (Disambiguate+AltKeys+AllKeys+AssocText) 'a' key", L"\x1b[97;1;97u", 29, true, 'A', 0x1E, L'a', 0 }, + + // flags=30 (0b11110): EventTypes + AltKeys + AllKeys + AssocText + TestCase{ L"Flags=30 (EventTypes+AltKeys+AllKeys+AssocText) 'a' key press", L"\x1b[97;1:1;97u", 30, true, 'A', 0x1E, L'a', 0 }, + + // flags=31 (0b11111): All flags enabled + TestCase{ L"Flags=31 (all) 'a' key press", L"\x1b[97;1:1;97u", 31, true, 'A', 0x1E, L'a', 0 }, + + // ==================================================================== + // SECTION 2: Modifier Combinations with Disambiguate (flag=1) + // Testing all modifier permutations with 'a' key + // Kitty modifier encoding: shift=1, alt=2, ctrl=4, caps_lock=64, num_lock=128 + // Transmitted value = 1 + modifiers + // ==================================================================== + + // Alt+'a' -> CSI 97;3 u (mod=1+2=3) + TestCase{ L"Disambiguate: Alt+a", L"\x1b[97;3u", 1, true, 'A', 0x1E, L'a', ALT_PRESSED }, + + // Ctrl+'a' -> CSI 97;5 u (mod=1+4=5) + TestCase{ L"Disambiguate: Ctrl+a", L"\x1b[97;5u", 1, true, 'A', 0x1E, L'\x01', CTRL_PRESSED }, + + // Ctrl+Alt+'a' -> CSI 97;7 u (mod=1+2+4=7) + TestCase{ L"Disambiguate: Ctrl+Alt+a", L"\x1b[97;7u", 1, true, 'A', 0x1E, L'\x01', CTRL_PRESSED | ALT_PRESSED }, + + // Shift+Alt+'a' -> CSI 97;4 u (mod=1+1+2=4) + TestCase{ L"Disambiguate: Shift+Alt+a", L"\x1b[97;4u", 1, true, 'A', 0x1E, L'A', SHIFT_PRESSED | ALT_PRESSED }, + + // ==================================================================== + // SECTION 3: Modifier combinations with AllKeys (flag=8) + // All keys produce CSI u, lock keys are reported + // ==================================================================== + + // No modifiers: 'a' -> CSI 97 u + TestCase{ L"AllKeys: 'a' no mods", L"\x1b[97u", 8, true, 'A', 0x1E, L'a', 0 }, + + // Shift+'a' -> CSI 97;2 u (mod=1+1=2) + TestCase{ L"AllKeys: Shift+a", L"\x1b[97;2u", 8, true, 'A', 0x1E, L'A', SHIFT_PRESSED }, + + // Alt+'a' -> CSI 97;3 u (mod=1+2=3) + TestCase{ L"AllKeys: Alt+a", L"\x1b[97;3u", 8, true, 'A', 0x1E, L'a', ALT_PRESSED }, + + // Ctrl+'a' -> CSI 97;5 u (mod=1+4=5) + TestCase{ L"AllKeys: Ctrl+a", L"\x1b[97;5u", 8, true, 'A', 0x1E, L'\x01', CTRL_PRESSED }, + + // Shift+Alt+'a' -> CSI 97;4 u (mod=1+1+2=4) + TestCase{ L"AllKeys: Shift+Alt+a", L"\x1b[97;4u", 8, true, 'A', 0x1E, L'A', SHIFT_PRESSED | ALT_PRESSED }, + + // Shift+Ctrl+'a' -> CSI 97;6 u (mod=1+1+4=6) + TestCase{ L"AllKeys: Shift+Ctrl+a", L"\x1b[97;6u", 8, true, 'A', 0x1E, L'\x01', SHIFT_PRESSED | CTRL_PRESSED }, + + // Alt+Ctrl+'a' -> CSI 97;7 u (mod=1+2+4=7) + TestCase{ L"AllKeys: Alt+Ctrl+a", L"\x1b[97;7u", 8, true, 'A', 0x1E, L'\x01', ALT_PRESSED | CTRL_PRESSED }, + + // Shift+Alt+Ctrl+'a' -> CSI 97;8 u (mod=1+1+2+4=8) + TestCase{ L"AllKeys: Shift+Alt+Ctrl+a", L"\x1b[97;8u", 8, true, 'A', 0x1E, L'\x01', SHIFT_PRESSED | ALT_PRESSED | CTRL_PRESSED }, + + // CapsLock+'a' -> CSI 97;65 u (mod=1+64=65) + TestCase{ L"AllKeys: CapsLock+a", L"\x1b[97;65u", 8, true, 'A', 0x1E, L'A', CAPSLOCK_ON }, + + // NumLock+'a' -> CSI 97;129 u (mod=1+128=129) + TestCase{ L"AllKeys: NumLock+a", L"\x1b[97;129u", 8, true, 'A', 0x1E, L'a', NUMLOCK_ON }, + + // CapsLock+NumLock+'a' -> CSI 97;193 u (mod=1+64+128=193) + TestCase{ L"AllKeys: CapsLock+NumLock+a", L"\x1b[97;193u", 8, true, 'A', 0x1E, L'A', CAPSLOCK_ON | NUMLOCK_ON }, + + // Shift+CapsLock+'a' -> CSI 97;66 u (mod=1+1+64=66) + TestCase{ L"AllKeys: Shift+CapsLock+a", L"\x1b[97;66u", 8, true, 'A', 0x1E, L'a', SHIFT_PRESSED | CAPSLOCK_ON }, + + // All modifiers: Shift+Alt+Ctrl+CapsLock+NumLock + // mod=1+1+2+4+64+128=200 + TestCase{ L"AllKeys: all mods", L"\x1b[97;200u", 8, true, 'A', 0x1E, L'\x01', SHIFT_PRESSED | ALT_PRESSED | CTRL_PRESSED | CAPSLOCK_ON | NUMLOCK_ON }, + + // ==================================================================== + // SECTION 4: Enter, Tab, Backspace - Legacy behavior exceptions + // Per spec: "The only exceptions are the Enter, Tab and Backspace keys + // which still generate the same bytes as in legacy mode" + // ==================================================================== + + // With Disambiguate only (flag=1), these stay legacy: + // (These should return MakeUnhandled(), causing legacy processing) + // We'll test that they DON'T produce CSI u output + + // With AllKeys (flag=8), they DO get CSI u encoding: + // Enter -> CSI 13 u + TestCase{ L"AllKeys: Enter", L"\x1b[13u", 8, true, VK_RETURN, 0x1C, L'\r', 0 }, + + // Tab -> CSI 9 u + TestCase{ L"AllKeys: Tab", L"\x1b[9u", 8, true, VK_TAB, 0x0F, L'\t', 0 }, + + // Backspace -> CSI 127 u + TestCase{ L"AllKeys: Backspace", L"\x1b[127u", 8, true, VK_BACK, 0x0E, L'\b', 0 }, + + // Enter with Shift -> CSI 13;2 u + TestCase{ L"AllKeys: Shift+Enter", L"\x1b[13;2u", 8, true, VK_RETURN, 0x1C, L'\r', SHIFT_PRESSED }, + + // Tab with Ctrl -> CSI 9;5 u + TestCase{ L"AllKeys: Ctrl+Tab", L"\x1b[9;5u", 8, true, VK_TAB, 0x0F, L'\t', CTRL_PRESSED }, + + // Backspace with Alt -> CSI 127;3 u + TestCase{ L"AllKeys: Alt+Backspace", L"\x1b[127;3u", 8, true, VK_BACK, 0x0E, L'\b', ALT_PRESSED }, + + // ==================================================================== + // SECTION 5: Event Types (flag=2) + // press=1, repeat=2, release=3 + // Format: CSI keycode;mod:event u + // ==================================================================== + + // Key press with Disambiguate+EventTypes (flag=3) + TestCase{ L"EventTypes: Esc press", L"\x1b[27;1:1u", 3, true, VK_ESCAPE, 0x01, 0, 0 }, + + // Key release with Disambiguate+EventTypes (flag=3) + TestCase{ L"EventTypes: Esc release", L"\x1b[27;1:3u", 3, false, VK_ESCAPE, 0x01, 0, 0 }, + + // Key press with AllKeys+EventTypes (flag=10) + TestCase{ L"EventTypes+AllKeys: 'a' press", L"\x1b[97;1:1u", 10, true, 'A', 0x1E, L'a', 0 }, + + // Key release with AllKeys+EventTypes (flag=10) + TestCase{ L"EventTypes+AllKeys: 'a' release", L"\x1b[97;1:3u", 10, false, 'A', 0x1E, L'a', 0 }, + + // Enter/Tab/Backspace release - only with AllKeys+EventTypes + // Without AllKeys, release events for these are suppressed + TestCase{ L"EventTypes+AllKeys: Enter release", L"\x1b[13;1:3u", 10, false, VK_RETURN, 0x1C, L'\r', 0 }, + TestCase{ L"EventTypes+AllKeys: Tab release", L"\x1b[9;1:3u", 10, false, VK_TAB, 0x0F, L'\t', 0 }, + TestCase{ L"EventTypes+AllKeys: Backspace release", L"\x1b[127;1:3u", 10, false, VK_BACK, 0x0E, L'\b', 0 }, + + // Press with modifier: Shift+Esc -> CSI 27;2:1 u + TestCase{ L"EventTypes: Shift+Esc press", L"\x1b[27;2:1u", 3, true, VK_ESCAPE, 0x01, 0, SHIFT_PRESSED }, + + // Release with modifier: Shift+Esc -> CSI 27;2:3 u + TestCase{ L"EventTypes: Shift+Esc release", L"\x1b[27;2:3u", 3, false, VK_ESCAPE, 0x01, 0, SHIFT_PRESSED }, + + // ==================================================================== + // SECTION 6: Keypad Keys + // With Disambiguate, keypad keys get CSI u with special codepoints + // ==================================================================== + + // Keypad 0-9: 57399-57408 + TestCase{ L"Disambiguate: Numpad0", L"\x1b[57399u", 1, true, VK_NUMPAD0, 0x52, L'0', 0 }, + TestCase{ L"Disambiguate: Numpad1", L"\x1b[57400u", 1, true, VK_NUMPAD1, 0x4F, L'1', 0 }, + TestCase{ L"Disambiguate: Numpad5", L"\x1b[57404u", 1, true, VK_NUMPAD5, 0x4C, L'5', 0 }, + TestCase{ L"Disambiguate: Numpad9", L"\x1b[57408u", 1, true, VK_NUMPAD9, 0x49, L'9', 0 }, + + // Keypad operators + TestCase{ L"Disambiguate: Numpad Decimal", L"\x1b[57409u", 1, true, VK_DECIMAL, 0x53, L'.', 0 }, + TestCase{ L"Disambiguate: Numpad Divide", L"\x1b[57410u", 1, true, VK_DIVIDE, 0x35, L'/', ENHANCED_KEY }, + TestCase{ L"Disambiguate: Numpad Multiply", L"\x1b[57411u", 1, true, VK_MULTIPLY, 0x37, L'*', 0 }, + TestCase{ L"Disambiguate: Numpad Subtract", L"\x1b[57412u", 1, true, VK_SUBTRACT, 0x4A, L'-', 0 }, + TestCase{ L"Disambiguate: Numpad Add", L"\x1b[57413u", 1, true, VK_ADD, 0x4E, L'+', 0 }, + + // Keypad with modifiers + TestCase{ L"Disambiguate: Shift+Numpad5", L"\x1b[57404;2u", 1, true, VK_NUMPAD5, 0x4C, L'5', SHIFT_PRESSED }, + TestCase{ L"Disambiguate: Ctrl+Numpad0", L"\x1b[57399;5u", 1, true, VK_NUMPAD0, 0x52, L'0', CTRL_PRESSED }, + + // ==================================================================== + // SECTION 7: Lock Keys and Modifier Keys (with AllKeys flag=8) + // These report their own key codes + // ==================================================================== + + // CapsLock key itself -> CSI 57358 u + TestCase{ L"AllKeys: CapsLock key press", L"\x1b[57358u", 8, true, VK_CAPITAL, 0x3A, 0, 0 }, + + // NumLock key itself -> CSI 57360 u + TestCase{ L"AllKeys: NumLock key press", L"\x1b[57360u", 8, true, VK_NUMLOCK, 0x45, 0, ENHANCED_KEY }, + + // ScrollLock key itself -> CSI 57359 u + TestCase{ L"AllKeys: ScrollLock key press", L"\x1b[57359u", 8, true, VK_SCROLL, 0x46, 0, 0 }, + + // Left Shift key -> CSI 57441 u (with shift modifier set) + TestCase{ L"AllKeys: Left Shift key press", L"\x1b[57441;2u", 8, true, VK_SHIFT, 0x2A, 0, SHIFT_PRESSED }, + + // Right Shift key -> CSI 57447 u + TestCase{ L"AllKeys: Right Shift key press", L"\x1b[57447;2u", 8, true, VK_SHIFT, 0x36, 0, SHIFT_PRESSED }, + + // Left Ctrl key -> CSI 57442 u (with ctrl modifier set) + TestCase{ L"AllKeys: Left Ctrl key press", L"\x1b[57442;5u", 8, true, VK_CONTROL, 0x1D, 0, CTRL_PRESSED }, + + // Right Ctrl key -> CSI 57448 u + TestCase{ L"AllKeys: Right Ctrl key press", L"\x1b[57448;5u", 8, true, VK_CONTROL, 0x1D, 0, CTRL_PRESSED | ENHANCED_KEY }, + + // Left Alt key -> CSI 57443 u (with alt modifier set) + TestCase{ L"AllKeys: Left Alt key press", L"\x1b[57443;3u", 8, true, VK_MENU, 0x38, 0, ALT_PRESSED }, + + // Right Alt key -> CSI 57449 u + TestCase{ L"AllKeys: Right Alt key press", L"\x1b[57449;3u", 8, true, VK_MENU, 0x38, 0, RIGHT_ALT_PRESSED | ENHANCED_KEY }, + + // Left Windows key -> CSI 57444 u (super modifier not available in Win32) + TestCase{ L"AllKeys: Left Win key press", L"\x1b[57444u", 8, true, VK_LWIN, 0x5B, 0, ENHANCED_KEY }, + + // Right Windows key -> CSI 57450 u + TestCase{ L"AllKeys: Right Win key press", L"\x1b[57450u", 8, true, VK_RWIN, 0x5C, 0, ENHANCED_KEY }, + + // ==================================================================== + // SECTION 8: Special Keys with Disambiguate (flag=1) + // ==================================================================== + + // Various special keys that get CSI u encoding + + // Pause key -> CSI 57362 u + TestCase{ L"AllKeys: Pause key", L"\x1b[57362u", 8, true, VK_PAUSE, 0x45, 0, 0 }, + + // PrintScreen key -> CSI 57361 u + TestCase{ L"AllKeys: PrintScreen key", L"\x1b[57361u", 8, true, VK_SNAPSHOT, 0x37, 0, ENHANCED_KEY }, + + // Menu/Apps key -> CSI 57363 u + TestCase{ L"AllKeys: Menu key", L"\x1b[57363u", 8, true, VK_APPS, 0x5D, 0, ENHANCED_KEY }, + + // ==================================================================== + // SECTION 9: Legacy text keys with Disambiguate (flag=1) + // Per spec: "the keys a-z 0-9 ` - = [ ] \ ; ' , . / with modifiers + // alt, ctrl, ctrl+alt, shift+alt" get CSI u encoding + // ==================================================================== + + // Test each punctuation key with Alt + TestCase{ L"Disambiguate: Alt+`", L"\x1b[96;3u", 1, true, VK_OEM_3, 0x29, L'`', ALT_PRESSED }, + TestCase{ L"Disambiguate: Alt+-", L"\x1b[45;3u", 1, true, VK_OEM_MINUS, 0x0C, L'-', ALT_PRESSED }, + TestCase{ L"Disambiguate: Alt+=", L"\x1b[61;3u", 1, true, VK_OEM_PLUS, 0x0D, L'=', ALT_PRESSED }, + TestCase{ L"Disambiguate: Alt+[", L"\x1b[91;3u", 1, true, VK_OEM_4, 0x1A, L'[', ALT_PRESSED }, + TestCase{ L"Disambiguate: Alt+]", L"\x1b[93;3u", 1, true, VK_OEM_6, 0x1B, L']', ALT_PRESSED }, + TestCase{ L"Disambiguate: Alt+\\", L"\x1b[92;3u", 1, true, VK_OEM_5, 0x2B, L'\\', ALT_PRESSED }, + TestCase{ L"Disambiguate: Alt+;", L"\x1b[59;3u", 1, true, VK_OEM_1, 0x27, L';', ALT_PRESSED }, + TestCase{ L"Disambiguate: Alt+'", L"\x1b[39;3u", 1, true, VK_OEM_7, 0x28, L'\'', ALT_PRESSED }, + TestCase{ L"Disambiguate: Alt+,", L"\x1b[44;3u", 1, true, VK_OEM_COMMA, 0x33, L',', ALT_PRESSED }, + TestCase{ L"Disambiguate: Alt+.", L"\x1b[46;3u", 1, true, VK_OEM_PERIOD, 0x34, L'.', ALT_PRESSED }, + TestCase{ L"Disambiguate: Alt+/", L"\x1b[47;3u", 1, true, VK_OEM_2, 0x35, L'/', ALT_PRESSED }, + + // Test numbers with Ctrl + TestCase{ L"Disambiguate: Ctrl+0", L"\x1b[48;5u", 1, true, '0', 0x0B, L'0', CTRL_PRESSED }, + TestCase{ L"Disambiguate: Ctrl+1", L"\x1b[49;5u", 1, true, '1', 0x02, L'1', CTRL_PRESSED }, + TestCase{ L"Disambiguate: Ctrl+9", L"\x1b[57;5u", 1, true, '9', 0x0A, L'9', CTRL_PRESSED }, + + // Test letters with Ctrl+Alt + TestCase{ L"Disambiguate: Ctrl+Alt+a", L"\x1b[97;7u", 1, true, 'A', 0x1E, L'\x01', CTRL_PRESSED | ALT_PRESSED }, + TestCase{ L"Disambiguate: Ctrl+Alt+z", L"\x1b[122;7u", 1, true, 'Z', 0x2C, L'\x1A', CTRL_PRESSED | ALT_PRESSED }, + + // ==================================================================== + // SECTION 10: Navigation keys as keypad (without ENHANCED_KEY) + // When ENHANCED_KEY is not set, navigation keys are from the keypad + // ==================================================================== + + // Home without ENHANCED_KEY -> KP_HOME (57423) + TestCase{ L"AllKeys: Keypad Home", L"\x1b[57423u", 8, true, VK_HOME, 0x47, 0, 0 }, + + // End without ENHANCED_KEY -> KP_END (57424) + TestCase{ L"AllKeys: Keypad End", L"\x1b[57424u", 8, true, VK_END, 0x4F, 0, 0 }, + + // Insert without ENHANCED_KEY -> KP_INSERT (57425) + TestCase{ L"AllKeys: Keypad Insert", L"\x1b[57425u", 8, true, VK_INSERT, 0x52, 0, 0 }, + + // Delete without ENHANCED_KEY -> KP_DELETE (57426) + TestCase{ L"AllKeys: Keypad Delete", L"\x1b[57426u", 8, true, VK_DELETE, 0x53, 0, 0 }, + + // PageUp without ENHANCED_KEY -> KP_PAGE_UP (57421) + TestCase{ L"AllKeys: Keypad PageUp", L"\x1b[57421u", 8, true, VK_PRIOR, 0x49, 0, 0 }, + + // PageDown without ENHANCED_KEY -> KP_PAGE_DOWN (57422) + TestCase{ L"AllKeys: Keypad PageDown", L"\x1b[57422u", 8, true, VK_NEXT, 0x51, 0, 0 }, + + // Arrows without ENHANCED_KEY + TestCase{ L"AllKeys: Keypad Up", L"\x1b[57419u", 8, true, VK_UP, 0x48, 0, 0 }, + TestCase{ L"AllKeys: Keypad Down", L"\x1b[57420u", 8, true, VK_DOWN, 0x50, 0, 0 }, + TestCase{ L"AllKeys: Keypad Left", L"\x1b[57417u", 8, true, VK_LEFT, 0x4B, 0, 0 }, + TestCase{ L"AllKeys: Keypad Right", L"\x1b[57418u", 8, true, VK_RIGHT, 0x4D, 0, 0 }, + + // ==================================================================== + // SECTION 11: Media Keys + // ==================================================================== + + TestCase{ L"AllKeys: Media Play/Pause", L"\x1b[57430u", 8, true, VK_MEDIA_PLAY_PAUSE, 0, 0, 0 }, + TestCase{ L"AllKeys: Media Stop", L"\x1b[57432u", 8, true, VK_MEDIA_STOP, 0, 0, 0 }, + TestCase{ L"AllKeys: Media Next Track", L"\x1b[57435u", 8, true, VK_MEDIA_NEXT_TRACK, 0, 0, 0 }, + TestCase{ L"AllKeys: Media Prev Track", L"\x1b[57436u", 8, true, VK_MEDIA_PREV_TRACK, 0, 0, 0 }, + TestCase{ L"AllKeys: Volume Down", L"\x1b[57438u", 8, true, VK_VOLUME_DOWN, 0, 0, 0 }, + TestCase{ L"AllKeys: Volume Up", L"\x1b[57439u", 8, true, VK_VOLUME_UP, 0, 0, 0 }, + TestCase{ L"AllKeys: Volume Mute", L"\x1b[57440u", 8, true, VK_VOLUME_MUTE, 0, 0, 0 }, + + // ==================================================================== + // SECTION 12: Function Keys (F13-F24) + // F1-F12 use legacy sequences, F13-F24 use CSI u with codes 57376-57387 + // ==================================================================== + + TestCase{ L"AllKeys: F13", L"\x1b[57376u", 8, true, VK_F13, 0x64, 0, 0 }, + TestCase{ L"AllKeys: F14", L"\x1b[57377u", 8, true, VK_F14, 0x65, 0, 0 }, + TestCase{ L"AllKeys: F15", L"\x1b[57378u", 8, true, VK_F15, 0x66, 0, 0 }, + TestCase{ L"AllKeys: F16", L"\x1b[57379u", 8, true, VK_F16, 0x67, 0, 0 }, + TestCase{ L"AllKeys: F17", L"\x1b[57380u", 8, true, VK_F17, 0x68, 0, 0 }, + TestCase{ L"AllKeys: F18", L"\x1b[57381u", 8, true, VK_F18, 0x69, 0, 0 }, + TestCase{ L"AllKeys: F19", L"\x1b[57382u", 8, true, VK_F19, 0x6A, 0, 0 }, + TestCase{ L"AllKeys: F20", L"\x1b[57383u", 8, true, VK_F20, 0x6B, 0, 0 }, + TestCase{ L"AllKeys: F21", L"\x1b[57384u", 8, true, VK_F21, 0x6C, 0, 0 }, + TestCase{ L"AllKeys: F22", L"\x1b[57385u", 8, true, VK_F22, 0x6D, 0, 0 }, + TestCase{ L"AllKeys: F23", L"\x1b[57386u", 8, true, VK_F23, 0x6E, 0, 0 }, + TestCase{ L"AllKeys: F24", L"\x1b[57387u", 8, true, VK_F24, 0x76, 0, 0 }, + + // F13 with modifiers + TestCase{ L"AllKeys: Shift+F13", L"\x1b[57376;2u", 8, true, VK_F13, 0x64, 0, SHIFT_PRESSED }, + TestCase{ L"AllKeys: Ctrl+F13", L"\x1b[57376;5u", 8, true, VK_F13, 0x64, 0, CTRL_PRESSED }, + TestCase{ L"AllKeys: Alt+F13", L"\x1b[57376;3u", 8, true, VK_F13, 0x64, 0, ALT_PRESSED }, + + // ==================================================================== + // SECTION 13: Alternate Keys (ReportAlternateKeys flag = 4) + // Format: CSI keycode:shifted-key:base-layout-key ; modifiers u + // Shifted key is present only when shift modifier is active + // Base layout key is the PC-101 US keyboard equivalent + // ==================================================================== + + // Shift+a with AltKeys flag: 97:65 (a:A) - shifted key is 'A' (65) + // flags = AllKeys(8) + AltKeys(4) = 12 + TestCase{ L"AltKeys+AllKeys: Shift+a", L"\x1b[97:65;2u", 12, true, 'A', 0x1E, L'A', SHIFT_PRESSED }, + + // Shift+1 with AltKeys flag: 49:33 (1:!) - shifted key is '!' (33) + TestCase{ L"AltKeys+AllKeys: Shift+1", L"\x1b[49:33;2u", 12, true, '1', 0x02, L'!', SHIFT_PRESSED }, + + // Shift+[ with AltKeys flag: 91:123 ([:{) - shifted key is '{' (123) + TestCase{ L"AltKeys+AllKeys: Shift+[", L"\x1b[91:123;2u", 12, true, VK_OEM_4, 0x1A, L'{', SHIFT_PRESSED }, + + // Without shift, no shifted key is reported + // 'a' with AltKeys flag (no shift): 97 only, no alternate keys + TestCase{ L"AltKeys+AllKeys: a (no shift)", L"\x1b[97u", 12, true, 'A', 0x1E, L'a', 0 }, + + // ==================================================================== + // SECTION 14: Complex combinations + // Testing multiple flags together with various keys and modifiers + // ==================================================================== + + // AllKeys + EventTypes + CapsLock: 'a' press with CapsLock + // mod=1+64=65, event=press=1 + TestCase{ L"AllKeys+EventTypes: CapsLock+a press", L"\x1b[97;65:1u", 10, true, 'A', 0x1E, L'A', CAPSLOCK_ON }, + + // AllKeys + EventTypes + all modifiers: press + // mod=1+1+2+4+64+128=200, event=1 + TestCase{ L"AllKeys+EventTypes: all mods press", L"\x1b[97;200:1u", 10, true, 'A', 0x1E, L'\x01', SHIFT_PRESSED | ALT_PRESSED | CTRL_PRESSED | CAPSLOCK_ON | NUMLOCK_ON }, + + // AllKeys + EventTypes + all modifiers: release + TestCase{ L"AllKeys+EventTypes: all mods release", L"\x1b[97;200:3u", 10, false, 'A', 0x1E, L'\x01', SHIFT_PRESSED | ALT_PRESSED | CTRL_PRESSED | CAPSLOCK_ON | NUMLOCK_ON }, + + // ==================================================================== + // SECTION 15: Text with associated codepoints (flag=24: AllKeys + AssocText) + // Format: CSI keycode ; modifiers ; text u + // ==================================================================== + + // 'A' (shifted) with AssocText: CSI 97;2;65 u + TestCase{ L"AllKeys+AssocText: Shift+a", L"\x1b[97;2;65u", 24, true, 'A', 0x1E, L'A', SHIFT_PRESSED }, + + // Number with shift (symbol): Shift+1 -> '!' + // CSI 49;2;33 u (49='1', 33='!') + TestCase{ L"AllKeys+AssocText: Shift+1", L"\x1b[49;2;33u", 24, true, '1', 0x02, L'!', SHIFT_PRESSED }, + + // Ctrl+a produces control character (0x01), which should not be in text + // Text field should be omitted for control codes + TestCase{ L"AllKeys+AssocText: Ctrl+a (no text)", L"\x1b[97;5;1u", 24, true, 'A', 0x1E, L'\x01', CTRL_PRESSED }, + + // ==================================================================== + // SECTION 16: Edge cases + // ==================================================================== + + // Keypad Enter (ENHANCED_KEY set) -> KP_ENTER (57414) + TestCase{ L"AllKeys: Keypad Enter", L"\x1b[57414u", 8, true, VK_RETURN, 0x1C, L'\r', ENHANCED_KEY }, + + // Regular Enter vs Keypad Enter distinction + TestCase{ L"AllKeys: Regular Enter", L"\x1b[13u", 8, true, VK_RETURN, 0x1C, L'\r', 0 }, + + // Escape with all basic modifiers + TestCase{ L"AllKeys: Shift+Alt+Ctrl+Esc", L"\x1b[27;8u", 8, true, VK_ESCAPE, 0x01, 0, SHIFT_PRESSED | ALT_PRESSED | CTRL_PRESSED }, + + // Tab with Shift (special legacy: CSI Z, but with AllKeys should be CSI 9;2 u) + TestCase{ L"AllKeys: Shift+Tab", L"\x1b[9;2u", 8, true, VK_TAB, 0x0F, 0, SHIFT_PRESSED }, + }; +} + +extern "C" HRESULT __declspec(dllexport) __cdecl KittyKeyTestDataSource(IDataSource** ppDataSource, void*) +{ + *ppDataSource = new ArrayIndexTaefAdapterSource(std::size(testCases)); + return S_OK; +} + +class KittyKeyboardProtocolTests +{ + TEST_CLASS(KittyKeyboardProtocolTests); + + TEST_METHOD(KeyPressTests) + { + BEGIN_TEST_METHOD_PROPERTIES() + TEST_METHOD_PROPERTY(L"DataSource", L"Export:KittyKeyTestDataSource") + END_TEST_METHOD_PROPERTIES() + + DisableVerifyExceptions disableVerifyExceptions{}; + SetVerifyOutput verifyOutputScope{ VerifyOutputSettings::LogOnlyFailures }; + + size_t i{}; + TestData::TryGetValue(L"index", i); + const auto& tc = testCases[i]; + + Log::Comment(NoThrowString().Format(L"[%zu] Test case \"%.*s\"", i, tc.name.size(), tc.name.data())); + + auto input = createInput(tc.flags); + const auto expected = TerminalInput::MakeOutput(tc.expected); + const auto actual = process(input, tc.keyDown, tc.vk, tc.sc, tc.ch, tc.state); + VERIFY_ARE_EQUAL(expected, actual); + } + + // Repeat events require stateful testing - the same key must be pressed twice + // without a release in between. This cannot be done with the data-driven approach. + TEST_METHOD(KeyRepeatEvents) + { + Log::Comment(L"Testing key repeat event type (event type = 2)"); + + // Use EventTypes flag (2) + AllKeys flag (8) = 10 + constexpr uint8_t flags = 10; + auto input = createInput(flags); + + // First press -> event type 1 (press) + auto result1 = process(input, true, 'A', 0x1E, L'a', 0); + auto expected1 = TerminalInput::MakeOutput(L"\x1b[97;1:1u"); + VERIFY_ARE_EQUAL(expected1, result1, L"First press should be event type 1"); + + // Second press (same key, no release) -> event type 2 (repeat) + auto result2 = process(input, true, 'A', 0x1E, L'a', 0); + auto expected2 = TerminalInput::MakeOutput(L"\x1b[97;1:2u"); + VERIFY_ARE_EQUAL(expected2, result2, L"Second press should be event type 2 (repeat)"); + + // Third press (still same key) -> still event type 2 (repeat) + auto result3 = process(input, true, 'A', 0x1E, L'a', 0); + auto expected3 = TerminalInput::MakeOutput(L"\x1b[97;1:2u"); + VERIFY_ARE_EQUAL(expected3, result3, L"Third press should still be event type 2 (repeat)"); + + // Release -> event type 3 + auto result4 = process(input, false, 'A', 0x1E, L'a', 0); + auto expected4 = TerminalInput::MakeOutput(L"\x1b[97;1:3u"); + VERIFY_ARE_EQUAL(expected4, result4, L"Release should be event type 3"); + + // Next press after release -> event type 1 (press) again + auto result5 = process(input, true, 'A', 0x1E, L'a', 0); + auto expected5 = TerminalInput::MakeOutput(L"\x1b[97;1:1u"); + VERIFY_ARE_EQUAL(expected5, result5, L"Press after release should be event type 1 again"); + } + + // Test repeat events with modifiers + TEST_METHOD(KeyRepeatEventsWithModifiers) + { + Log::Comment(L"Testing key repeat with Shift modifier"); + + constexpr uint8_t flags = 10; // EventTypes + AllKeys + auto input = createInput(flags); + + // First Shift+a press -> event type 1 + auto result1 = process(input, true, 'A', 0x1E, L'A', SHIFT_PRESSED); + auto expected1 = TerminalInput::MakeOutput(L"\x1b[97;2:1u"); + VERIFY_ARE_EQUAL(expected1, result1, L"First Shift+a press should be event type 1"); + + // Repeat Shift+a -> event type 2 + auto result2 = process(input, true, 'A', 0x1E, L'A', SHIFT_PRESSED); + auto expected2 = TerminalInput::MakeOutput(L"\x1b[97;2:2u"); + VERIFY_ARE_EQUAL(expected2, result2, L"Repeat Shift+a should be event type 2"); + } + + // Test that pressing different keys resets repeat detection + TEST_METHOD(KeyRepeatResetOnDifferentKey) + { + Log::Comment(L"Testing that pressing a different key resets repeat detection"); + + constexpr uint8_t flags = 10; // EventTypes + AllKeys + auto input = createInput(flags); + + // Press 'a' + auto result1 = process(input, true, 'A', 0x1E, L'a', 0); + auto expected1 = TerminalInput::MakeOutput(L"\x1b[97;1:1u"); + VERIFY_ARE_EQUAL(expected1, result1, L"First 'a' press should be event type 1"); + + // Press 'b' (different key) -> should be press, not repeat + auto result2 = process(input, true, 'B', 0x30, L'b', 0); + auto expected2 = TerminalInput::MakeOutput(L"\x1b[98;1:1u"); + VERIFY_ARE_EQUAL(expected2, result2, L"'b' press should be event type 1 (not repeat)"); + + // Press 'a' again -> should be press since 'b' was pressed in between + auto result3 = process(input, true, 'A', 0x1E, L'a', 0); + auto expected3 = TerminalInput::MakeOutput(L"\x1b[97;1:1u"); + VERIFY_ARE_EQUAL(expected3, result3, L"'a' press after 'b' should be event type 1 (new press)"); + } + + // Test Enter/Tab/Backspace release suppression without AllKeys + TEST_METHOD(EnterTabBackspaceReleaseWithoutAllKeys) + { + Log::Comment(L"Testing that Enter/Tab/Backspace don't report release without AllKeys flag"); + + // Use Disambiguate + EventTypes (flags = 3), but NOT AllKeys + constexpr uint8_t flags = 3; + auto input = createInput(flags); + + // These keys should NOT produce output for release events + // (they return MakeUnhandled for press too with just Disambiguate, + // but release should produce _makeNoOutput) + + // Note: With flags=3 (no AllKeys), Enter/Tab/Backspace use legacy encoding + // and release events should be suppressed (return no output) + } + + // Test that without EventTypes flag, release events produce no output + TEST_METHOD(ReleaseEventsWithoutEventTypesFlag) + { + Log::Comment(L"Testing that release events produce no output without EventTypes flag"); + + // Use only AllKeys (flag = 8), NOT EventTypes + constexpr uint8_t flags = 8; + auto input = createInput(flags); + + // Press should produce output + auto result1 = process(input, true, 'A', 0x1E, L'a', 0); + auto expected1 = TerminalInput::MakeOutput(L"\x1b[97u"); + VERIFY_ARE_EQUAL(expected1, result1, L"Press should produce output"); + + // Release should produce no output (empty optional) + auto result2 = process(input, false, 'A', 0x1E, L'a', 0); + VERIFY_IS_FALSE(result2.has_value(), L"Release without EventTypes flag should produce no output"); + } + + // Test legacy mode (flags=0) produces MakeUnhandled for regular keys + TEST_METHOD(LegacyModePassthrough) + { + Log::Comment(L"Testing that legacy mode (flags=0) returns MakeUnhandled for regular keys"); + + constexpr uint8_t flags = 0; + auto input = createInput(flags); + + // Regular key 'a' should return MakeUnhandled (falls through to legacy processing) + auto result = process(input, true, 'A', 0x1E, L'a', 0); + auto unhandled = TerminalInput::MakeUnhandled(); + VERIFY_ARE_EQUAL(unhandled, result, L"Regular key in legacy mode should be unhandled"); + } +}; diff --git a/src/terminal/input/terminalInput.cpp b/src/terminal/input/terminalInput.cpp index 7e788994c0..bfdbf47a96 100644 --- a/src/terminal/input/terminalInput.cpp +++ b/src/terminal/input/terminalInput.cpp @@ -8,21 +8,28 @@ #include "../types/inc/IInputEvent.hpp" -using namespace std::string_literals; -using namespace Microsoft::Console::VirtualTerminal; +// Throughout portions of this file will be "KKP>" comments, +// which refer to the kitty keyboard protocol documentation: +// https://sw.kovidgoyal.net/kitty/keyboard-protocol/ +// +// Like other Kitty protocol specifications at the time (2026-01-30), +// it is unfortunately defined in a very informal, narrative specification. +// As such, you'll find that I've heavily "[editorialized]" many citations below. +// It's still a hard read at times. -namespace -{ - // These modifier constants are added to the virtual key code - // to produce a lookup value for determining the appropriate - // VT sequence for a particular modifier + key combination. - constexpr int VTModifier(const int m) { return m << 8; } - constexpr auto Unmodified = VTModifier(0); - constexpr auto Shift = VTModifier(1); - constexpr auto Alt = VTModifier(2); - constexpr auto Ctrl = VTModifier(4); - constexpr auto Enhanced = VTModifier(8); -} +// A significant portion of the following code doesn't care +// about the difference between left/right modifier keys, so this +// Simple Key State bitfield can be used to simplify some expressions. +// +// NOTE: The values for Shift/Alt/Ctrl intentionally match the KKP spec. +// Don't break this unintentionally. +#define SKS_SHIFT 1 +#define SKS_ALT 2 +#define SKS_CTRL 4 +#define SKS_ENHANCED 8 + +using namespace std::string_view_literals; +using namespace Microsoft::Console::VirtualTerminal; TerminalInput::TerminalInput() noexcept { @@ -112,25 +119,25 @@ void TerminalInput::ForceDisableKittyKeyboardProtocol(const bool disable) noexce } } -// Kitty keyboard protocol methods - -void TerminalInput::SetKittyKeyboardProtocol(const uint8_t flags, const KittyKeyboardProtocolMode mode) noexcept +void TerminalInput::SetKittyKeyboardProtocol(uint8_t flags, const KittyKeyboardProtocolMode mode) noexcept { if (_forceDisableKittyKeyboardProtocol) { return; } + flags &= KittyKeyboardProtocolFlags::All; + switch (mode) { case KittyKeyboardProtocolMode::Replace: - _kittyFlags = flags & KittyKeyboardProtocolFlags::All; + _kittyFlags = flags; break; case KittyKeyboardProtocolMode::Set: - _kittyFlags |= (flags & KittyKeyboardProtocolFlags::All); + _kittyFlags |= flags; break; case KittyKeyboardProtocolMode::Reset: - _kittyFlags &= ~(flags & KittyKeyboardProtocolFlags::All); + _kittyFlags &= ~flags; break; } } @@ -140,7 +147,7 @@ uint8_t TerminalInput::GetKittyFlags() const noexcept return _kittyFlags; } -void TerminalInput::PushKittyFlags(const uint8_t flags) noexcept +void TerminalInput::PushKittyFlags(const uint8_t flags) { if (_forceDisableKittyKeyboardProtocol) { @@ -148,7 +155,8 @@ void TerminalInput::PushKittyFlags(const uint8_t flags) noexcept } auto& stack = _getKittyStack(); - // > If a push request is received and the stack is full, the oldest entry from the stack must be evicted. + // KKP> If a push request is received and the stack is full, + // KKP> the oldest entry from the stack must be evicted. if (stack.size() >= KittyStackMaxSize) { stack.erase(stack.begin()); @@ -157,7 +165,7 @@ void TerminalInput::PushKittyFlags(const uint8_t flags) noexcept _kittyFlags = flags & KittyKeyboardProtocolFlags::All; } -void TerminalInput::PopKittyFlags(size_t count) noexcept +void TerminalInput::PopKittyFlags(size_t count) { // NOTE: It's not just an optimization to return early here. if (count == 0) @@ -169,7 +177,7 @@ void TerminalInput::PopKittyFlags(size_t count) noexcept if (count >= stack.size()) { - // > If a pop request is received that empties the stack, all flags are reset. + // KKP> If a pop request is received that empties the stack, all flags are reset. _kittyFlags = 0; stack.clear(); } @@ -223,81 +231,97 @@ 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. + // + // The kitty keyboard protocol takes precedence, because it's cross-platform. 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; - - // Only need to handle key down. See raw key handler (see RawReadWaitRoutine in stream.cpp) - if (!keyEvent.bKeyDown) + uint32_t codepoint = keyEvent.uChar.UnicodeChar; + // Swallow lone leading surrogates... + if (til::is_leading_surrogate(keyEvent.uChar.UnicodeChar)) { - // If this is a release of the last recorded key press, we can reset that. - if (matchingLastKeyPress) - { - _lastVirtualKeyCode = std::nullopt; - } - // If NumLock is on, and this is an Alt release with a unicode char, - // it must be the generated character from an Alt-Numpad composition. - if (WI_IsFlagSet(controlKeyState, NUMLOCK_ON) && virtualKeyCode == VK_MENU && unicodeChar != 0) - { - return MakeOutput({ &unicodeChar, 1 }); - } - // Otherwise we should return an empty string here to prevent unwanted - // characters being transmitted by the release event. + _leadingSurrogate = keyEvent.uChar.UnicodeChar; return _makeNoOutput(); } - - // Unpaired surrogates are no good -> early return. - if (til::is_leading_surrogate(unicodeChar)) + // ...and combine them with trailing surrogates. + if (_leadingSurrogate != 0 && til::is_trailing_surrogate(keyEvent.uChar.UnicodeChar)) { - _leadingSurrogate = unicodeChar; - return _makeNoOutput(); + codepoint = til::combine_surrogates(_leadingSurrogate, keyEvent.uChar.UnicodeChar); } - // Using a scope_exit ensures that a previous leading surrogate is forgotten - // even if the KEY_EVENT that followed didn't end up calling _makeCharOutput. - const auto leadingSurrogateReset = wil::scope_exit([&]() { - _leadingSurrogate = 0; - }); + _leadingSurrogate = 0; - // If this is a VK_PACKET or 0 virtual key, it's likely a synthesized - // keyboard event, so the UnicodeChar is transmitted as is. This must be - // handled before the Auto Repeat test, other we'll end up dropping chars. - if (virtualKeyCode == VK_PACKET || virtualKeyCode == 0) + if (keyEvent.bKeyDown) { - return _makeCharOutput(unicodeChar); + // If this is a VK_PACKET or 0 virtual key, it's likely a synthesized + // keyboard event, so the UnicodeChar is transmitted as is. This must be + // handled before the Auto Repeat test, other we'll end up dropping chars. + if (virtualKeyCode == VK_PACKET || virtualKeyCode == 0) + { + return _makeCharOutput(codepoint); + } + + // If it's a numeric keypad key, and Alt is pressed (but not Ctrl), + // then this is an Alt-Numpad composition, and we should ignore these keys. + // The generated character will be transmitted when the Alt is released. + if (virtualKeyCode >= VK_NUMPAD0 && virtualKeyCode <= VK_NUMPAD9 && + (controlKeyState == LEFT_ALT_PRESSED || controlKeyState == RIGHT_ALT_PRESSED)) + { + return _makeNoOutput(); + } + } + + // Keep track of key repeats. + const auto isKeyRepeat = _lastVirtualKeyCode == virtualKeyCode; + if (keyEvent.bKeyDown) + { + _lastVirtualKeyCode = virtualKeyCode; + } + else if (isKeyRepeat) + { + _lastVirtualKeyCode = std::nullopt; } // If this is a repeat of the last recorded key press, and Auto Repeat Mode // is disabled, then we should suppress this event. - if (matchingLastKeyPress && !_inputMode.test(Mode::AutoRepeat)) + if (isKeyRepeat && !_inputMode.test(Mode::AutoRepeat)) { - // Note that we must return an empty string here to imply that we've handled - // the event; otherwise, the key press can still end up being submitted. return _makeNoOutput(); } - _lastVirtualKeyCode = virtualKeyCode; - // If this is a modifier, it won't produce output, so we can return early. - if (virtualKeyCode >= VK_SHIFT && virtualKeyCode <= VK_MENU) + // There's a bunch of early returns we can place on key-up events, + // before we run our more complex encoding logic below. + if (!keyEvent.bKeyDown) { - return _makeNoOutput(); + // If NumLock is on, and this is an Alt release with a unicode char, + // it must be the generated character from an Alt-Numpad composition. + if (WI_IsFlagSet(controlKeyState, NUMLOCK_ON) && virtualKeyCode == VK_MENU && codepoint != 0) + { + return _makeCharOutput(codepoint); + } + + // KKP> Normally only key press events are reported [...]. + // + // ...or put differently: If ReportEventTypes is disabled, + // and this is a key-up, we can return early. + if (WI_IsFlagClear(_kittyFlags, KittyKeyboardProtocolFlags::ReportEventTypes)) + { + return _makeNoOutput(); + } + + // From a side note (?!) on the "Report event types" section: + // + // KKP> NOTE: The Enter, Tab and Backspace keys will not have release + // KKP> events unless Report all keys as escape codes is also set [...]. + if (WI_IsFlagClear(_kittyFlags, KittyKeyboardProtocolFlags::ReportAllKeysAsEscapeCodes) && + (virtualKeyCode == VK_RETURN || virtualKeyCode == VK_TAB || virtualKeyCode == VK_BACK)) + { + return _makeNoOutput(); + } } // Keyboards that have an AltGr key will generate both a RightAlt key press @@ -309,10 +333,9 @@ TerminalInput::OutputType TerminalInput::HandleKey(const INPUT_RECORD& event) auto leftCtrlIsReallyPressed = WI_IsFlagSet(controlKeyState, LEFT_CTRL_PRESSED); if (WI_AreAllFlagsSet(controlKeyState, LEFT_CTRL_PRESSED | RIGHT_ALT_PRESSED)) { - const auto timeBetweenCtrlAlt = _lastRightAltTime > _lastLeftCtrlTime ? - _lastRightAltTime - _lastLeftCtrlTime : - _lastLeftCtrlTime - _lastRightAltTime; - leftCtrlIsReallyPressed = timeBetweenCtrlAlt > 50; + const auto max = std::max(_lastLeftCtrlTime, _lastRightAltTime); + const auto min = std::min(_lastLeftCtrlTime, _lastRightAltTime); + leftCtrlIsReallyPressed = (max - min) > 50; } const auto ctrlIsPressed = WI_IsAnyFlagSet(controlKeyState, CTRL_PRESSED); @@ -320,51 +343,296 @@ TerminalInput::OutputType TerminalInput::HandleKey(const INPUT_RECORD& event) const auto shiftIsPressed = WI_IsFlagSet(controlKeyState, SHIFT_PRESSED); const auto altIsPressed = WI_IsAnyFlagSet(controlKeyState, ALT_PRESSED); const auto altGrIsPressed = altIsPressed && ctrlIsPressed; + const auto enhanced = WI_IsFlagSet(controlKeyState, ENHANCED_KEY); - // If it's a numeric keypad key, and Alt is pressed (but not Ctrl), then - // this is an Alt-Numpad composition and we should ignore these keys. The - // generated character will be transmitted when the Alt is released. - if (virtualKeyCode >= VK_NUMPAD0 && virtualKeyCode <= VK_NUMPAD9 && altIsPressed && !ctrlIsPressed) + DWORD simpleKeyState = 0; + WI_SetFlagIf(simpleKeyState, SKS_CTRL, ctrlIsReallyPressed); + WI_SetFlagIf(simpleKeyState, SKS_ALT, altIsPressed); + WI_SetFlagIf(simpleKeyState, SKS_SHIFT, shiftIsPressed); + WI_SetFlagIf(simpleKeyState, SKS_ENHANCED, enhanced); + + const auto info = _getKeyEncodingInfo(keyEvent, simpleKeyState); + + // > 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 [...]. + int32_t kittyKeyCode = 0; + int32_t kittyAltKeyCodeShifted = 0; + int32_t kittyAltKeyCodeBase = 0; + int32_t kittyTextAsCodepoint = 0; + + // Get the codepoint that would be generated without modifiers. + // + // _getKeyEncodingInfo() only returns anything for non-text keys. + // So, using the kittyKeyCode == 0 check we filter down to (possible) text keys. + // The logic is also not required if no modifiers are pressed, + // since no modifiers means that we can just use the codepoint as is. + int32_t codepointWithoutModifiers = codepoint; + if (info.kittyKeyCode == 0) { - return _makeNoOutput(); + // We need the current keyboard layout and state to look up the character + // that would be transmitted in that state (via the ToUnicodeEx API). + const auto hkl = GetKeyboardLayout(GetWindowThreadProcessId(GetForegroundWindow(), nullptr)); + auto keyState = _getKeyboardState(virtualKeyCode, controlKeyState); + constexpr UINT flags = 4; // Don't modify the state in the ToUnicodeEx call. + constexpr int bufferSize = 4; + wchar_t buffer[bufferSize]; + + // However, we first need to query the key with the original state, to check + // whether it's a dead key. If that is the case, ToUnicodeEx should return a + // negative number, although in practice it's more likely to return a string + // of length two, with two identical characters. This is because the system + // sees this as a second press of the dead key, which would typically result + // in the combining character representation being transmitted twice. + auto length = ToUnicodeEx(virtualKeyCode, 0, keyState.data(), buffer, bufferSize, flags, hkl); + if (length < 0 || (length == 2 && buffer[0] == buffer[1])) + { + return _makeNoOutput(); + } + + // Once we know it's not a dead key, we run the query again, but with the + // Ctrl and Alt modifiers disabled to obtain the base character mapping. + keyState.at(VK_CONTROL) = keyState.at(VK_LCONTROL) = keyState.at(VK_RCONTROL) = 0; + keyState.at(VK_MENU) = keyState.at(VK_LMENU) = keyState.at(VK_RMENU) = 0; + length = ToUnicodeEx(virtualKeyCode, 0, keyState.data(), buffer, bufferSize, flags, hkl); + if (length == 1 || length == 2) + { + codepointWithoutModifiers = _bufferToCodepoint(buffer); + } } - // The only enhanced key we care about is the Return key, because that - // indicates that it's the key on the numeric keypad, which will transmit - // different escape sequences when the Keypad mode is enabled. - const auto enhancedReturnKey = WI_IsFlagSet(controlKeyState, ENHANCED_KEY) && virtualKeyCode == VK_RETURN; - - // Using the control key state that we calculated above, combined with the - // virtual key code, we've got a unique identifier for the key combination - // that we can lookup in our map of predefined key sequences. - auto keyCombo = virtualKeyCode; - WI_SetFlagIf(keyCombo, Ctrl, ctrlIsReallyPressed); - WI_SetFlagIf(keyCombo, Alt, altIsPressed); - WI_SetFlagIf(keyCombo, Shift, shiftIsPressed); - WI_SetFlagIf(keyCombo, Enhanced, enhancedReturnKey); - const auto keyMatch = _keyMap.find(keyCombo); - if (keyMatch != _keyMap.end()) + if (WI_IsFlagSet(_kittyFlags, KittyKeyboardProtocolFlags::DisambiguateEscapeCodes)) { - return keyMatch->second; + // KKP> Turning on [DisambiguateEscapeCodes] will cause the terminal to + // KKP> report the Esc, alt+key, ctrl+key, ctrl+alt+key, shift+alt+key + // KKP> keys using CSI u sequences instead of legacy ones. + // KKP> Here key is any ASCII key as described in Legacy text keys. [...] + // + // NOTE: The specification fails to mention ctrl+shift+key, but Kitty does handle it. + // So really, it's actually "any modifiers, except for shift+key". + + // KKP> Legacy text keys: + // KKP> For legacy compatibility, the keys a-z 0-9 ` - = [ ] \ ; ' , . / [...] + // + // NOTE: The list of legacy keys doesn't really make any sense. + // My interpretation is that the author meant all printable keys, + // because as before, that's also how Kitty handles it. + + // KKP> Additionally, all non text keypad keys will be reported [...] with CSI u encoding, [...]. + + if (virtualKeyCode == VK_ESCAPE || + (virtualKeyCode >= VK_NUMPAD0 && virtualKeyCode <= VK_DIVIDE) || + ((simpleKeyState & ~SKS_ENHANCED) > SKS_SHIFT && _codepointIsText(codepointWithoutModifiers))) + { + kittyKeyCode = info.kittyKeyCode; + } } + int32_t kittyEventType = 1; + if (WI_IsFlagSet(_kittyFlags, KittyKeyboardProtocolFlags::ReportEventTypes)) + { + // KKP> This [...] causes the terminal to report key repeat and key release events. + if (!keyEvent.bKeyDown) + { + kittyEventType = 3; // release event + } + else if (isKeyRepeat) + { + kittyEventType = 2; // repeat event + } + } + + if (WI_IsFlagSet(_kittyFlags, KittyKeyboardProtocolFlags::ReportAlternateKeys)) + { + // KKP> This [...] causes the terminal to report alternate key values [...] + // KKP> See Key codes for details [...] + // + // KKP> Key codes: + // KKP> The unicode-key-code above is the Unicode codepoint representing the key [...] + // KKP> Note that the codepoint used is always the lower-case [...] version of the key. + + // KKP> Note that [...] only key events represented as escape codes due to the + // KKP> other enhancements in effect will be affected by this enhancement. [...] + + // KKP> [...] the terminal can [...] [additionally send] the shifted key and base layout key [...]. + // KKP> The shifted key is [...] the upper-case version of unicode-codepoint, + // KKP> or more technically, the shifted version, in the currently active keyboard layout. + // + // !!NOTE!! that the 2nd line is wrong. On a 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...) + + // KKP> Note that the shifted key must be present only if shift is also present in the modifiers. + + if ((simpleKeyState & SKS_SHIFT) != 0 && _codepointIsText(codepoint)) + { + // I'm assuming that codepoint is already the shifted version if shift is pressed. + kittyAltKeyCodeShifted = codepoint; + } + + kittyAltKeyCodeBase = _getBaseLayoutCodepoint(virtualKeyCode); + } + + if (WI_IsFlagSet(_kittyFlags, KittyKeyboardProtocolFlags::ReportAllKeysAsEscapeCodes)) + { + // KKP> This [...] turns on key reporting even for key events that generate text. + // KKP> [...] text will not be sent, instead only key events are sent. + // + // KKP> [...] with this mode, events for pressing modifier keys are reported. + kittyKeyCode = info.kittyKeyCode; + if (kittyKeyCode == 0 && _codepointIsText(codepointWithoutModifiers)) + { + kittyKeyCode = codepointWithoutModifiers; + } + + if (WI_IsFlagSet(_kittyFlags, KittyKeyboardProtocolFlags::ReportAssociatedText)) + { + // KKP> This [...] causes key events that generate text to be reported + // KKP> as CSI u escape codes with the text embedded in the escape code. + // + // KKP> Note that this flag is an enhancement to Report all keys as escape codes [...]. + // + // KKP> The associated text must not contain control codes [...]. + if (_codepointIsText(codepoint)) + { + kittyTextAsCodepoint = codepoint; + } + } + } + + // These modifiers apply to all CSI encodings, including KKP. + // As per KKP: shift=1, alt=2, ctrl=4, super=8, hyper=16, meta=32, caps_lock=64, num_lock=128 + int32_t csiModifiers = simpleKeyState & (SKS_SHIFT | SKS_ALT | SKS_CTRL); + // KKP> Lock modifiers are not reported for text producing keys, [...]. + // KKP> To get lock modifiers for all keys use the Report all keys as escape codes enhancement. + if (!_codepointIsText(codepoint) || WI_IsFlagSet(_kittyFlags, KittyKeyboardProtocolFlags::ReportAllKeysAsEscapeCodes)) + { + if (WI_IsFlagSet(controlKeyState, CAPSLOCK_ON)) + { + csiModifiers |= 64; + } + if (WI_IsFlagSet(controlKeyState, NUMLOCK_ON)) + { + csiModifiers |= 128; + } + } + + // > 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. + + std::wstring seq; + + if (kittyKeyCode) + { + seq.append(_csi); + fmt::format_to(std::back_inserter(seq), FMT_COMPILE(L"{}"), kittyKeyCode); + + if (kittyAltKeyCodeShifted != 0 || kittyAltKeyCodeBase != 0) + { + seq.push_back(L':'); + if (kittyAltKeyCodeShifted != 0) + { + fmt::format_to(std::back_inserter(seq), FMT_COMPILE(L"{}"), kittyAltKeyCodeShifted); + } + if (kittyAltKeyCodeBase != 0) + { + fmt::format_to(std::back_inserter(seq), FMT_COMPILE(L":{}"), kittyAltKeyCodeBase); + } + } + } + else if (info.csiParam1) + { + seq.append(_csi); + fmt::format_to(std::back_inserter(seq), FMT_COMPILE(L"{}"), info.csiParam1); + } + + if (csiModifiers != 0 || kittyKeyCode && (kittyEventType != 0 || kittyTextAsCodepoint != 0)) + { + fmt::format_to(std::back_inserter(seq), FMT_COMPILE(L";{}"), 1 + csiModifiers); + if (kittyKeyCode) + { + if (kittyEventType != 0) + { + fmt::format_to(std::back_inserter(seq), FMT_COMPILE(L":{}"), kittyEventType); + } + if (kittyTextAsCodepoint != 0) + { + fmt::format_to(std::back_inserter(seq), FMT_COMPILE(L";{}"), kittyTextAsCodepoint); + } + } + } + + if (kittyKeyCode) + { + seq.push_back(L'u'); + } + else if (info.csiFinal) + { + seq.push_back(info.csiFinal); + } + else if (info.ss3Final) + { + } + else if (!info.plain.empty()) + { + if (info.altPrefix && (simpleKeyState & SKS_ALT)) + { + seq.push_back(L'\x1b'); + } + seq.append(info.plain); + } + + /* // If it's not in the key map, we'll use the UnicodeChar, if provided, // except in the case of Ctrl+Space, which is often mapped incorrectly as // a space character when it's expected to be mapped to NUL. We need to // let that fall through to the standard mapping algorithm below. const auto ctrlSpaceKey = ctrlIsReallyPressed && virtualKeyCode == VK_SPACE; - if (unicodeChar != 0 && !ctrlSpaceKey) + if (codepoint != 0 && !ctrlSpaceKey) { // In the case of an AltGr key, we may still need to apply a Ctrl // modifier to the char, either because both Ctrl keys were pressed, // or we got a LeftCtrl that was distinctly separate from the RightAlt. const auto bothCtrlsArePressed = WI_AreAllFlagsSet(controlKeyState, CTRL_PRESSED); const auto rightAltIsPressed = WI_IsFlagSet(controlKeyState, RIGHT_ALT_PRESSED); + auto wch = codepoint; + if (altGrIsPressed && (bothCtrlsArePressed || (rightAltIsPressed && leftCtrlIsReallyPressed))) { - unicodeChar = _makeCtrlChar(unicodeChar); + wch = _makeCtrlChar(codepoint); } - auto charSequence = _makeCharOutput(unicodeChar); + + auto charSequence = _makeCharOutput(wch); // We may also need to apply an Alt prefix to the char sequence, but // if this is an AltGr key, we only do so if both Alts are pressed. const auto bothAltsArePressed = WI_AreAllFlagsSet(controlKeyState, ALT_PRESSED); @@ -380,43 +648,10 @@ TerminalInput::OutputType TerminalInput::HandleKey(const INPUT_RECORD& event) return _makeNoOutput(); } - // We need the current keyboard layout and state to lookup the character - // that would be transmitted in that state (via the ToUnicodeEx API). - const auto hkl = GetKeyboardLayout(GetWindowThreadProcessId(GetForegroundWindow(), nullptr)); - auto keyState = _getKeyboardState(virtualKeyCode, controlKeyState); - const auto flags = 4u; // Don't modify the state in the ToUnicodeEx call. - const auto bufferSize = 16; - auto buffer = std::array{}; - - // However, we first need to query the key with the original state, to check - // whether it's a dead key. If that is the case, ToUnicodeEx should return a - // negative number, although in practice it's more likely to return a string - // of length two, with two identical characters. This is because the system - // sees this as a second press of the dead key, which would typically result - // in the combining character representation being transmit twice. - auto length = ToUnicodeEx(virtualKeyCode, 0, keyState.data(), buffer.data(), bufferSize, flags, hkl); - if (length < 0 || (length == 2 && buffer.at(0) == buffer.at(1))) - { - return _makeNoOutput(); - } - - // Once we know it's not a dead key, we run the query again, but with the - // Ctrl and Alt modifiers disabled to obtain the base character mapping. - keyState.at(VK_CONTROL) = keyState.at(VK_LCONTROL) = keyState.at(VK_RCONTROL) = 0; - keyState.at(VK_MENU) = keyState.at(VK_LMENU) = keyState.at(VK_RMENU) = 0; - length = ToUnicodeEx(virtualKeyCode, 0, keyState.data(), buffer.data(), bufferSize, flags, hkl); - if (length <= 0) - { - // If we've got nothing usable, we'll just return an empty string. The event - // has technically still been handled, even if it's an unmapped key. - return _makeNoOutput(); - } - - auto charSequence = StringType{ buffer.data(), gsl::narrow_cast(length) }; // Once we've got the base character, we can apply the Ctrl modifier. - if (ctrlIsReallyPressed && charSequence.length() == 1) + if (ctrlIsReallyPressed) { - auto ch = _makeCtrlChar(charSequence.at(0)); + auto ch = _makeCtrlChar(codepoint); // If we haven't found a Ctrl mapping for the key, and it's one of // the alphanumeric keys, we try again using the virtual key code. // On keyboard layouts where the alphanumeric keys are not mapped to @@ -425,11 +660,13 @@ TerminalInput::OutputType TerminalInput::HandleKey(const INPUT_RECORD& event) { ch = _makeCtrlChar(virtualKeyCode); } - charSequence.at(0) = ch; + codepoint = ch; } // If Alt is pressed, that also needs to be applied to the sequence. _escapeOutput(charSequence, altIsPressed); - return charSequence; + */ + + return seq; } TerminalInput::OutputType TerminalInput::HandleFocus(const bool focused) const @@ -445,208 +682,8 @@ TerminalInput::OutputType TerminalInput::HandleFocus(const bool focused) const void TerminalInput::_initKeyboardMap() noexcept try { - auto defineKeyWithUnusedModifiers = [this](const int keyCode, const std::wstring& sequence) { - for (auto m = 0; m < 8; m++) - _keyMap[VTModifier(m) + keyCode] = sequence; - }; - auto defineKeyWithAltModifier = [this](const int keyCode, const std::wstring& sequence) { - _keyMap[keyCode] = sequence; - _keyMap[Alt + keyCode] = L"\x1B" + sequence; - }; - auto defineKeypadKey = [this](const int keyCode, const wchar_t* prefix, const wchar_t finalChar) { - _keyMap[keyCode] = fmt::format(FMT_COMPILE(L"{}{}"), prefix, finalChar); - for (auto m = 1; m < 8; m++) - _keyMap[VTModifier(m) + keyCode] = fmt::format(FMT_COMPILE(L"{}1;{}{}"), _csi, m + 1, finalChar); - }; - auto defineEditingKey = [this](const int keyCode, const int parm) { - _keyMap[keyCode] = fmt::format(FMT_COMPILE(L"{}{}~"), _csi, parm); - for (auto m = 1; m < 8; m++) - _keyMap[VTModifier(m) + keyCode] = fmt::format(FMT_COMPILE(L"{}{};{}~"), _csi, parm, m + 1); - }; - auto defineNumericKey = [this](const int keyCode, const wchar_t finalChar) { - _keyMap[keyCode] = fmt::format(FMT_COMPILE(L"{}{}"), _ss3, finalChar); - for (auto m = 1; m < 8; m++) - _keyMap[VTModifier(m) + keyCode] = fmt::format(FMT_COMPILE(L"{}{}{}"), _ss3, m + 1, finalChar); - }; - - _keyMap.clear(); - - // The CSI and SS3 introducers are C1 control codes, which can either be - // sent as a single codepoint, or as a two character escape sequence. - if (_inputMode.test(Mode::SendC1)) - { - _csi = L"\x9B"; - _ss3 = L"\x8F"; - } - else - { - _csi = L"\x1B["; - _ss3 = L"\x1BO"; - } - - // PAUSE doesn't have a VT mapping, but traditionally we've mapped it to ^Z, - // regardless of modifiers. - defineKeyWithUnusedModifiers(VK_PAUSE, L"\x1A"s); - - // BACKSPACE maps to either DEL or BS, depending on the Backarrow Key mode. - // The Ctrl modifier inverts the active mode, swapping BS and DEL (this is - // not standard, but a modern terminal convention). The Alt modifier adds - // an ESC prefix (also not standard). - const auto backSequence = _inputMode.test(Mode::BackarrowKey) ? L"\b"s : L"\x7F"s; - const auto ctrlBackSequence = _inputMode.test(Mode::BackarrowKey) ? L"\x7F"s : L"\b"s; - defineKeyWithAltModifier(VK_BACK, backSequence); - defineKeyWithAltModifier(Ctrl + VK_BACK, ctrlBackSequence); - defineKeyWithAltModifier(Shift + VK_BACK, backSequence); - defineKeyWithAltModifier(Ctrl + Shift + VK_BACK, ctrlBackSequence); - - // TAB maps to HT, and Shift+TAB to CBT. The Ctrl modifier has no effect. - // The Alt modifier adds an ESC prefix, although in practice all the Alt - // mappings are likely to be system hotkeys. - const auto shiftTabSequence = fmt::format(FMT_COMPILE(L"{}Z"), _csi); - defineKeyWithAltModifier(VK_TAB, L"\t"s); - defineKeyWithAltModifier(Ctrl + VK_TAB, L"\t"s); - defineKeyWithAltModifier(Shift + VK_TAB, shiftTabSequence); - defineKeyWithAltModifier(Ctrl + Shift + VK_TAB, shiftTabSequence); - - // RETURN maps to either CR or CR LF, depending on the Line Feed mode. With - // a Ctrl modifier it maps to LF, because that's the expected behavior for - // most PC keyboard layouts. The Alt modifier adds an ESC prefix. - const auto returnSequence = _inputMode.test(Mode::LineFeed) ? L"\r\n"s : L"\r"s; - defineKeyWithAltModifier(VK_RETURN, returnSequence); - defineKeyWithAltModifier(Shift + VK_RETURN, returnSequence); - defineKeyWithAltModifier(Ctrl + VK_RETURN, L"\n"s); - defineKeyWithAltModifier(Ctrl + Shift + VK_RETURN, L"\n"s); - - // The keypad RETURN key works the same way, except when Keypad mode is - // enabled, but that's handled below with the other keypad keys. - defineKeyWithAltModifier(Enhanced + VK_RETURN, returnSequence); - defineKeyWithAltModifier(Shift + Enhanced + VK_RETURN, returnSequence); - defineKeyWithAltModifier(Ctrl + Enhanced + VK_RETURN, L"\n"s); - defineKeyWithAltModifier(Ctrl + Shift + Enhanced + VK_RETURN, L"\n"s); - - if (_inputMode.test(Mode::Ansi)) - { - // F1 to F4 map to the VT keypad function keys, which are SS3 sequences. - // When combined with a modifier, we use CSI sequences with the modifier - // embedded as a parameter (not standard - a modern terminal extension). - defineKeypadKey(VK_F1, _ss3, L'P'); - defineKeypadKey(VK_F2, _ss3, L'Q'); - defineKeypadKey(VK_F3, _ss3, L'R'); - defineKeypadKey(VK_F4, _ss3, L'S'); - - // F5 through F20 map to the top row VT function keys. They use standard - // DECFNK sequences with the modifier embedded as a parameter. The first - // five function keys on a VT terminal are typically local functions, so - // there's not much need to support mappings for them. - for (auto vk = VK_F5; vk <= VK_F20; vk++) - { - static constexpr std::array parameters = { 15, 17, 18, 19, 20, 21, 23, 24, 25, 26, 28, 29, 31, 32, 33, 34 }; - const auto parm = parameters.at(static_cast(vk) - VK_F5); - defineEditingKey(vk, parm); - } - - // Cursor keys follow a similar pattern to the VT keypad function keys, - // although they only use an SS3 prefix when the Cursor Key mode is set. - // When combined with a modifier, they'll use CSI sequences with the - // modifier embedded as a parameter (again not standard). - const auto ckIntroducer = _inputMode.test(Mode::CursorKey) ? _ss3 : _csi; - defineKeypadKey(VK_UP, ckIntroducer, L'A'); - defineKeypadKey(VK_DOWN, ckIntroducer, L'B'); - defineKeypadKey(VK_RIGHT, ckIntroducer, L'C'); - defineKeypadKey(VK_LEFT, ckIntroducer, L'D'); - defineKeypadKey(VK_CLEAR, ckIntroducer, L'E'); - defineKeypadKey(VK_HOME, ckIntroducer, L'H'); - defineKeypadKey(VK_END, ckIntroducer, L'F'); - - // Editing keys follow the same pattern as the top row VT function - // keys, using standard DECFNK sequences with the modifier embedded. - defineEditingKey(VK_INSERT, 2); - defineEditingKey(VK_DELETE, 3); - defineEditingKey(VK_PRIOR, 5); - defineEditingKey(VK_NEXT, 6); - - // Keypad keys depend on the Keypad mode. When reset, they transmit - // the ASCII character assigned by the keyboard layout, but when set - // they transmit SS3 escape sequences. When used with a modifier, the - // modifier is embedded as a parameter value (not standard). - if (Feature_KeypadModeEnabled::IsEnabled() && _inputMode.test(Mode::Keypad)) - { - defineNumericKey(VK_MULTIPLY, L'j'); - defineNumericKey(VK_ADD, L'k'); - defineNumericKey(VK_SEPARATOR, L'l'); - defineNumericKey(VK_SUBTRACT, L'm'); - defineNumericKey(VK_DECIMAL, L'n'); - defineNumericKey(VK_DIVIDE, L'o'); - - defineNumericKey(VK_NUMPAD0, L'p'); - defineNumericKey(VK_NUMPAD1, L'q'); - defineNumericKey(VK_NUMPAD2, L'r'); - defineNumericKey(VK_NUMPAD3, L's'); - defineNumericKey(VK_NUMPAD4, L't'); - defineNumericKey(VK_NUMPAD5, L'u'); - defineNumericKey(VK_NUMPAD6, L'v'); - defineNumericKey(VK_NUMPAD7, L'w'); - defineNumericKey(VK_NUMPAD8, L'x'); - defineNumericKey(VK_NUMPAD9, L'y'); - - defineNumericKey(Enhanced + VK_RETURN, L'M'); - } - } - else - { - // In VT52 mode, the sequences tend to use the same final character as - // their ANSI counterparts, but with a simple ESC prefix. The modifier - // keys have no effect. - - // VT52 only support PF1 through PF4 function keys. - defineKeyWithUnusedModifiers(VK_F1, L"\033P"s); - defineKeyWithUnusedModifiers(VK_F2, L"\033Q"s); - defineKeyWithUnusedModifiers(VK_F3, L"\033R"s); - defineKeyWithUnusedModifiers(VK_F4, L"\033S"s); - - // But terminals with application functions keys would - // map some of them as controls keys in VT52 mode. - defineKeyWithUnusedModifiers(VK_F11, L"\033"s); - defineKeyWithUnusedModifiers(VK_F12, L"\b"s); - defineKeyWithUnusedModifiers(VK_F13, L"\n"s); - - // Cursor keys use the same finals as the ANSI sequences. - defineKeyWithUnusedModifiers(VK_UP, L"\033A"s); - defineKeyWithUnusedModifiers(VK_DOWN, L"\033B"s); - defineKeyWithUnusedModifiers(VK_RIGHT, L"\033C"s); - defineKeyWithUnusedModifiers(VK_LEFT, L"\033D"s); - defineKeyWithUnusedModifiers(VK_CLEAR, L"\033E"s); - defineKeyWithUnusedModifiers(VK_HOME, L"\033H"s); - defineKeyWithUnusedModifiers(VK_END, L"\033F"s); - - // Keypad keys also depend on Keypad mode, the same as ANSI mappings, - // but the sequences use an ESC ? prefix instead of SS3. - if (Feature_KeypadModeEnabled::IsEnabled() && _inputMode.test(Mode::Keypad)) - { - defineKeyWithUnusedModifiers(VK_MULTIPLY, L"\033?j"s); - defineKeyWithUnusedModifiers(VK_ADD, L"\033?k"s); - defineKeyWithUnusedModifiers(VK_SEPARATOR, L"\033?l"s); - defineKeyWithUnusedModifiers(VK_SUBTRACT, L"\033?m"s); - defineKeyWithUnusedModifiers(VK_DECIMAL, L"\033?n"s); - defineKeyWithUnusedModifiers(VK_DIVIDE, L"\033?o"s); - - defineKeyWithUnusedModifiers(VK_NUMPAD0, L"\033?p"s); - defineKeyWithUnusedModifiers(VK_NUMPAD1, L"\033?q"s); - defineKeyWithUnusedModifiers(VK_NUMPAD2, L"\033?r"s); - defineKeyWithUnusedModifiers(VK_NUMPAD3, L"\033?s"s); - defineKeyWithUnusedModifiers(VK_NUMPAD4, L"\033?t"s); - defineKeyWithUnusedModifiers(VK_NUMPAD5, L"\033?u"s); - defineKeyWithUnusedModifiers(VK_NUMPAD6, L"\033?v"s); - defineKeyWithUnusedModifiers(VK_NUMPAD7, L"\033?w"s); - defineKeyWithUnusedModifiers(VK_NUMPAD8, L"\033?x"s); - defineKeyWithUnusedModifiers(VK_NUMPAD9, L"\033?y"s); - - defineKeyWithUnusedModifiers(Enhanced + VK_RETURN, L"\033?M"s); - } - } - - _focusInSequence = _csi + L"I"s; - _focusOutSequence = _csi + L"O"s; + _focusInSequence = fmt::format(FMT_COMPILE(L"{}I", _csi)); + _focusOutSequence = fmt::format(FMT_COMPILE(L"{}O", _csi)); } CATCH_LOG() @@ -733,17 +770,10 @@ wchar_t TerminalInput::_makeCtrlChar(const wchar_t ch) // If it encounters a surrogate pair, it'll buffer the leading character until a // trailing one has been received and then flush both of them simultaneously. // Surrogate pairs should always be handled as proper pairs after all. -TerminalInput::StringType TerminalInput::_makeCharOutput(const wchar_t ch) +TerminalInput::StringType TerminalInput::_makeCharOutput(const uint32_t cp) { - StringType str; - - if (_leadingSurrogate && til::is_trailing_surrogate(ch)) - { - str.push_back(_leadingSurrogate); - } - - str.push_back(ch); - return str; + const auto buf = _codepointToBuffer(cp); + return { buf.buf, buf.len }; } TerminalInput::StringType TerminalInput::_makeNoOutput() noexcept @@ -788,417 +818,144 @@ TerminalInput::OutputType TerminalInput::_makeWin32Output(const KEY_EVENT_RECORD return fmt::format(FMT_COMPILE(L"{}{};{};{};{};{};{}_"), _csi, vk, sc, uc, kd, cs, rc); } -// Generates kitty keyboard protocol output for a key event. -// 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) +TerminalInput::KeyEncodingInfo TerminalInput::_getKeyEncodingInfo(const KEY_EVENT_RECORD& key, DWORD simpleKeyState) const noexcept { const auto virtualKeyCode = key.wVirtualKeyCode; - const auto unicodeChar = key.uChar.UnicodeChar; - const auto isKeyDown = key.bKeyDown; - uint32_t codepoint = unicodeChar; - const auto isRepeat = _lastVirtualKeyCode == virtualKeyCode && isKeyDown; - - // First off, some state tracking. - { - // 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 - { - // > Normally only key press events are reported [...]. - if (!isKeyDown) - { - return _makeNoOutput(); - } - } - - if (WI_IsFlagSet(_kittyFlags, KittyKeyboardProtocolFlags::ReportAllKeysAsEscapeCodes)) - { - // > 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)) - { - // > 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; - } - } - else - { - // The inverse of reporting all keys is that we don't handle keys that we haven't yet handled. - if (kittyKeyCode == 0) - { - return MakeUnhandled(); - } - - // 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)) - { - return _makeNoOutput(); - } - } - - // > 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)) - { - kittyModifiers |= 1; - } - if (WI_IsAnyFlagSet(controlKeyState, ALT_PRESSED)) - { - kittyModifiers |= 2; - } - if (WI_IsAnyFlagSet(controlKeyState, CTRL_PRESSED)) - { - kittyModifiers |= 4; - } - // > 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)) - { - kittyModifiers |= 64; - } - if (WI_IsFlagSet(controlKeyState, NUMLOCK_ON)) - { - kittyModifiers |= 128; - } - } - - std::wstring seq; - seq.append(_csi); - - if (legacyFinal) - { - // > 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 (kittyModifiers != 0 || kittyEventType != 0) - { - 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":{}"), kittyEventType); - } - } - seq.push_back(legacyFinal); - return MakeOutput(seq); - } - - // > 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 [...]. - - fmt::format_to(std::back_inserter(seq), FMT_COMPILE(L"{}"), kittyKeyCode); - - if (kittyAltKeyCodeShifted != 0 || kittyAltKeyCodeBase != 0) - { - seq.push_back(L':'); - if (kittyAltKeyCodeShifted != 0) - { - fmt::format_to(std::back_inserter(seq), FMT_COMPILE(L"{}"), kittyAltKeyCodeShifted); - } - if (kittyAltKeyCodeBase != 0) - { - fmt::format_to(std::back_inserter(seq), FMT_COMPILE(L":{}"), kittyAltKeyCodeBase); - } - } - - if (kittyModifiers != 0 || kittyEventType != 0 || kittyTextAsCodepoint != 0) - { - 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":{}"), kittyEventType); - } - if (kittyTextAsCodepoint != 0) - { - fmt::format_to(std::back_inserter(seq), FMT_COMPILE(L";{}"), kittyTextAsCodepoint); - } - } - - seq.push_back(L'u'); - return MakeOutput(std::move(seq)); -} - -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); + const auto modified = (simpleKeyState & ~SKS_ENHANCED) != 0; + const auto enhanced = (simpleKeyState & SKS_ENHANCED) != 0; + KeyEncodingInfo info; 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 + // BACKSPACE maps to either DEL or BS, depending on the Backarrow Key mode. + // The Ctrl modifier inverts the active mode, swapping BS and DEL (this is + // not standard, but a modern terminal convention). The Alt modifier adds + // an ESC prefix (also not standard). case VK_BACK: - return 127; // BACKSPACE + info.kittyKeyCode = 127; // BACKSPACE + info.altPrefix = true; + switch (simpleKeyState & ~(SKS_ALT | SKS_SHIFT)) + { + default: + info.plain = _inputMode.test(Mode::BackarrowKey) ? L"\b"sv : L"\x7F"sv; + break; + case SKS_CTRL: + info.plain = _inputMode.test(Mode::BackarrowKey) ? L"\x7f"sv : L"\b"sv; + break; + } + break; + // TAB maps to HT, and Shift+TAB to CBT. The Ctrl modifier has no effect. + // The Alt modifier adds an ESC prefix, although in practice all the Alt + // mappings are likely to be system hotkeys. + case VK_TAB: + info.kittyKeyCode = 9; // TAB + info.altPrefix = true; + switch (simpleKeyState & ~(SKS_ALT | SKS_CTRL)) + { + default: + info.plain = L"\t"sv; + break; + case SKS_SHIFT: + info.csiFinal = L'Z'; + break; + } + break; + // RETURN maps to either CR or CR LF, depending on the Line Feed mode. With + // a Ctrl modifier it maps to LF, because that's the expected behavior for + // most PC keyboard layouts. The Alt modifier adds an ESC prefix. + case VK_RETURN: + info.kittyKeyCode = enhanced ? 57414 : 13; // KP_RETURN : ENTER + // The keypad RETURN key works different. + if (Feature_KeypadModeEnabled::IsEnabled() && _inputMode.test(Mode::Keypad) && enhanced) + { + if (_inputMode.test(Mode::Ansi)) + { + info.ss3Final = L'M'; + } + else + { + info.plain = L"\033?M"sv; + } + } + else + { + info.altPrefix = true; + switch (simpleKeyState & ~(SKS_ALT | SKS_SHIFT | SKS_ENHANCED)) + { + default: + info.plain = _inputMode.test(Mode::LineFeed) ? L"\r\n"sv : L"\r"sv; + break; + case SKS_CTRL: + info.plain = L"\n"sv; + break; + } + } + break; + case VK_ESCAPE: + info.kittyKeyCode = 27; // ESCAPE - // 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 + info.kittyKeyCode = 57358; // CAPS_LOCK + break; case VK_SCROLL: - return 57359; // SCROLL_LOCK + info.kittyKeyCode = 57359; // SCROLL_LOCK + break; case VK_NUMLOCK: - return 57360; // NUM_LOCK - - // Other special keys + info.kittyKeyCode = 57360; // NUM_LOCK + break; case VK_SNAPSHOT: - return 57361; // PRINT_SCREEN + info.kittyKeyCode = 57361; // PRINT_SCREEN + break; + // PAUSE doesn't have a VT mapping, but traditionally we've mapped it to ^Z, + // regardless of modifiers. case VK_PAUSE: - return 57362; // PAUSE + info.kittyKeyCode = 57362; // PAUSE + info.plain = L"\x1A"sv; + break; case VK_APPS: - return 57363; // MENU + info.kittyKeyCode = 57363; // MENU + break; - // Function keys + // F1 to F4 map to the VT keypad function keys, which are SS3 sequences. + // When combined with a modifier, we use CSI sequences with the modifier + // embedded as a parameter (not standard - a modern terminal extension). + // VT52 only supports PF1 through PF4 function keys with ESC prefix. case VK_F1: case VK_F2: case VK_F3: case VK_F4: + if (_inputMode.test(Mode::Ansi)) + { + // KKP> The original version of this specification allowed F3 to + // KKP> be encoded as both CSI R and CSI ~ [and now it doesn't]. + if (virtualKeyCode == VK_F3 && + WI_IsAnyFlagSet( + _kittyFlags, + KittyKeyboardProtocolFlags::DisambiguateEscapeCodes | + KittyKeyboardProtocolFlags::ReportEventTypes | + KittyKeyboardProtocolFlags::ReportAllKeysAsEscapeCodes)) + { + info.csiFinal = L'~'; + info.csiParam1 = 13; + } + else + { + auto& dst = modified ? info.csiFinal : info.ss3Final; + dst = L'P' + (virtualKeyCode - VK_F1); + } + } + else + { + static constexpr std::wstring_view lut[] = { L"\033P", L"\033Q", L"\033R", L"\033S" }; + info.plain = lut[virtualKeyCode - VK_F1]; + } + break; + + // F5 through F20 map to the top row VT function keys. They use standard + // DECFNK sequences with the modifier embedded as a parameter. The first + // five function keys on a VT terminal are typically local functions, so + // there's not much need to support mappings for them. + // In VT52 mode, only F11-F13 are defined (as control keys). case VK_F5: case VK_F6: case VK_F7: @@ -1207,7 +964,6 @@ int32_t TerminalInput::_getKittyKeyCode(const KEY_EVENT_RECORD& key, DWORD contr case VK_F10: case VK_F11: case VK_F12: - return 0; // Use legacy sequences case VK_F13: case VK_F14: case VK_F15: @@ -1216,82 +972,251 @@ int32_t TerminalInput::_getKittyKeyCode(const KEY_EVENT_RECORD& key, DWORD contr case VK_F18: case VK_F19: case VK_F20: + if (virtualKeyCode >= VK_F13) + { + info.kittyKeyCode = 57376 + (virtualKeyCode - VK_F13); + } + if (_inputMode.test(Mode::Ansi)) + { + static constexpr uint8_t lut[] = { 15, 17, 18, 19, 20, 21, 23, 24, 25, 26, 28, 29, 31, 32, 33, 34 }; + info.csiFinal = L'~'; + info.csiParam1 = lut[virtualKeyCode - VK_F5]; + } + else + { + switch (virtualKeyCode) + { + case VK_F11: + info.plain = L"\033"sv; + break; + case VK_F12: + info.plain = L"\b"sv; + break; + case VK_F13: + info.plain = L"\n"sv; + break; + default: + break; + } + } + break; case VK_F21: case VK_F22: case VK_F23: case VK_F24: - return 57376 + (virtualKeyCode - VK_F13); // F13-F24 + info.kittyKeyCode = 57376 + (virtualKeyCode - VK_F13); + break; - // 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 + // Cursor keys follow a similar pattern to the VT keypad function keys, + // although they only use an SS3 prefix when the Cursor Key mode is set. + // When combined with a modifier, they'll use CSI sequences with the + // modifier embedded as a parameter (again not standard). + // In VT52 mode, cursor keys use ESC prefix. + case VK_LEFT: // 0x25 = D + case VK_UP: // 0x26 = A + case VK_RIGHT: // 0x27 = C + case VK_DOWN: // 0x28 = B + { + static constexpr uint8_t lut[] = { 3, 0, 2, 1 }; + const auto idx = lut[virtualKeyCode - VK_LEFT]; + if (!enhanced) + { + // If enhanced is not set, they should indicate keypad arrow keys. + info.kittyKeyCode = 57417 + idx; // KP_LEFT, KP_RIGHT, KP_UP, KP_DOWN + } + if (_inputMode.test(Mode::Ansi)) + { + auto& dst = modified ? info.csiFinal : info.ss3Final; + dst = L'A' + idx; + } + else + { + static constexpr std::wstring_view lut[] = { L"\033D", L"\033A", L"\033C", L"\033B" }; + info.plain = lut[virtualKeyCode - VK_LEFT]; + } + break; + } case VK_CLEAR: - return 57427; // KP_BEGIN + info.kittyKeyCode = 57427; // KP_BEGIN + if (_inputMode.test(Mode::Ansi)) + { + auto& dst = modified ? info.csiFinal : info.ss3Final; + dst = L'E'; + } + else + { + info.plain = L"\033E"sv; + } + break; + case VK_HOME: + if (!enhanced) + { + info.kittyKeyCode = 57423; // KP_HOME + } + if (_inputMode.test(Mode::Ansi)) + { + auto& dst = modified ? info.csiFinal : info.ss3Final; + dst = L'H'; + } + else + { + info.plain = L"\033H"sv; + } + break; + case VK_END: + if (!enhanced) + { + info.kittyKeyCode = 57424; // KP_END + } + if (_inputMode.test(Mode::Ansi)) + { + auto& dst = modified ? info.csiFinal : info.ss3Final; + dst = L'F'; + } + else + { + info.plain = L"\033F"sv; + } + break; + + // Editing keys follow the same pattern as the top row VT function + // keys, using standard DECFNK sequences with the modifier embedded. + // These are not defined in VT52 mode. + case VK_INSERT: // 0x2D = 2 + case VK_DELETE: // 0x2E = 3 + if (!enhanced) + { + info.kittyKeyCode = 57425 + (virtualKeyCode - VK_INSERT); // KP_INSERT, KP_DELETE + } + if (_inputMode.test(Mode::Ansi)) + { + info.csiFinal = L'~'; + info.csiParam1 = 2 + (virtualKeyCode - VK_INSERT); + } + break; + case VK_PRIOR: // 0x21 = 5 + case VK_NEXT: // 0x22 = 6 + if (!enhanced) + { + info.kittyKeyCode = 57421 + (virtualKeyCode - VK_PRIOR); // KP_PAGE_UP, KP_PAGE_DOWN + } + if (_inputMode.test(Mode::Ansi)) + { + info.csiFinal = L'~'; + info.csiParam1 = 5 + (virtualKeyCode - VK_PRIOR); + } + break; + + // Keypad keys depend on the Keypad mode. When reset, they transmit + // the ASCII character assigned by the keyboard layout, but when set + // they transmit SS3 escape sequences. When used with a modifier, the + // modifier is embedded as a parameter value (not standard). + // In VT52 mode, keypad keys use ESC ? prefix instead of SS3. + case VK_NUMPAD0: // 0x60 = p + case VK_NUMPAD1: // 0x61 = q + case VK_NUMPAD2: // 0x62 = r + case VK_NUMPAD3: // 0x63 = s + case VK_NUMPAD4: // 0x64 = t + case VK_NUMPAD5: // 0x65 = u + case VK_NUMPAD6: // 0x66 = v + case VK_NUMPAD7: // 0x67 = w + case VK_NUMPAD8: // 0x68 = x + case VK_NUMPAD9: // 0x69 = y + if (Feature_KeypadModeEnabled::IsEnabled() && _inputMode.test(Mode::Keypad)) + { + if (_inputMode.test(Mode::Ansi)) + { + info.ss3Final = L'p' + (virtualKeyCode - VK_NUMPAD0); + } + else + { + static constexpr std::wstring_view lut[] = { L"\033?p", L"\033?q", L"\033?r", L"\033?s", L"\033?t", L"\033?u", L"\033?v", L"\033?w", L"\033?x", L"\033?y" }; + info.plain = lut[virtualKeyCode - VK_NUMPAD0]; + } + } + break; + case VK_MULTIPLY: // 0x6A = j + case VK_ADD: // 0x6B = k + case VK_SEPARATOR: // 0x6C = l + case VK_SUBTRACT: // 0x6D = m + case VK_DECIMAL: // 0x6E = n + case VK_DIVIDE: // 0x6F = o + if (Feature_KeypadModeEnabled::IsEnabled() && _inputMode.test(Mode::Keypad)) + { + if (_inputMode.test(Mode::Ansi)) + { + info.ss3Final = L'j' + (virtualKeyCode - VK_MULTIPLY); + } + else + { + static constexpr std::wstring_view lut[] = { L"\033?j", L"\033?k", L"\033?l", L"\033?m", L"\033?n", L"\033?o" }; + info.plain = lut[virtualKeyCode - VK_MULTIPLY]; + } + } + break; - // Media keys case VK_MEDIA_PLAY_PAUSE: - return 57430; // MEDIA_PLAY_PAUSE + info.kittyKeyCode = 57430; // MEDIA_PLAY_PAUSE + break; case VK_MEDIA_STOP: - return 57432; // MEDIA_STOP + info.kittyKeyCode = 57432; // MEDIA_STOP + break; case VK_MEDIA_NEXT_TRACK: - return 57435; // MEDIA_TRACK_NEXT + info.kittyKeyCode = 57435; // MEDIA_TRACK_NEXT + break; case VK_MEDIA_PREV_TRACK: - return 57436; // MEDIA_TRACK_PREVIOUS + info.kittyKeyCode = 57436; // MEDIA_TRACK_PREVIOUS + break; case VK_VOLUME_DOWN: - return 57438; // LOWER_VOLUME + info.kittyKeyCode = 57438; // LOWER_VOLUME + break; case VK_VOLUME_UP: - return 57439; // RAISE_VOLUME + info.kittyKeyCode = 57439; // RAISE_VOLUME + break; case VK_VOLUME_MUTE: - return 57440; // MUTE_VOLUME + info.kittyKeyCode = 57440; // MUTE_VOLUME + break; // Modifier keys case VK_SHIFT: - return key.wVirtualScanCode == 0x2A ? 57441 : 57447; // LEFT_SHIFT : RIGHT_SHIFT + info.kittyKeyCode = key.wVirtualScanCode == 0x2A ? 57441 : 57447; // LEFT_SHIFT : RIGHT_SHIFT + break; case VK_LSHIFT: - return 57441; // LEFT_SHIFT + info.kittyKeyCode = 57441; // LEFT_SHIFT + break; case VK_RSHIFT: - return 57447; // RIGHT_SHIFT + info.kittyKeyCode = 57447; // RIGHT_SHIFT + break; case VK_CONTROL: - return isEnhanced ? 57448 : 57442; // RIGHT_CONTROL : LEFT_CONTROL + info.kittyKeyCode = enhanced ? 57448 : 57442; // RIGHT_CONTROL : LEFT_CONTROL + break; case VK_LCONTROL: - return 57442; // LEFT_CONTROL + info.kittyKeyCode = 57442; // LEFT_CONTROL + break; case VK_RCONTROL: - return 57448; // RIGHT_CONTROL + info.kittyKeyCode = 57448; // RIGHT_CONTROL + break; case VK_MENU: - return isEnhanced ? 57449 : 57443; // RIGHT_ALT : LEFT_ALT + info.kittyKeyCode = enhanced ? 57449 : 57443; // RIGHT_ALT : LEFT_ALT + break; case VK_LMENU: - return 57443; // LEFT_ALT + info.kittyKeyCode = 57443; // LEFT_ALT + break; case VK_RMENU: - return 57449; // RIGHT_ALT + info.kittyKeyCode = 57449; // RIGHT_ALT + break; case VK_LWIN: - return 57444; // LEFT_SUPER + info.kittyKeyCode = 57444; // LEFT_SUPER + break; case VK_RWIN: - return 57450; // RIGHT_SUPER + info.kittyKeyCode = 57450; // RIGHT_SUPER + break; default: - return 0; + break; } + + return info; } std::vector& TerminalInput::_getKittyStack() noexcept diff --git a/src/terminal/input/terminalInput.hpp b/src/terminal/input/terminalInput.hpp index 5a82a3730a..7c4ea0f329 100644 --- a/src/terminal/input/terminalInput.hpp +++ b/src/terminal/input/terminalInput.hpp @@ -78,8 +78,8 @@ namespace Microsoft::Console::VirtualTerminal // 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 PushKittyFlags(uint8_t flags); + void PopKittyFlags(size_t count); void ResetKittyKeyboardProtocols() noexcept; #pragma region MouseInput @@ -96,6 +96,31 @@ namespace Microsoft::Console::VirtualTerminal uint16_t len; }; + struct KeyEncodingInfo + { + // If not zero, this value represents the first field in the Kitty + // Keyboard Protocol (KKP) CSI u sequence. If the KKP is requested, + // this field will be preferred over the following fields. + int32_t kittyKeyCode = 0; + + // A non-zero csiFinal value indicates that this key + // should be encoded as `CSI $csiParam1 ; $csiFinal`. + wchar_t csiFinal = 0; + int32_t csiParam1 = 0; + + // A non-zero ss3Final value indicates that this key + // should be encoded as `ESC O $ss3Final`. + wchar_t ss3Final = 0; + + // Any other encoding ends up as a non-zero plain value. + // For instance, the Tab key gets translated to a plain "\t". + std::wstring_view plain; + + // If true, and Alt is pressed, an ESC prefix should be added to + // the final sequence. This only applies to non-KKP encodings. + bool altPrefix = false; + }; + // storage location for the leading surrogate of a utf-16 surrogate pair wchar_t _leadingSurrogate = 0; @@ -118,19 +143,18 @@ namespace Microsoft::Console::VirtualTerminal std::vector _kittyMainStack; std::vector _kittyAltStack; - const wchar_t* _csi = L"\x1B["; - const wchar_t* _ss3 = L"\x1BO"; + static constexpr std::wstring_view _csi{ L"\x1B[" }; + static constexpr std::wstring_view _ss3{ L"\x1BO" }; void _initKeyboardMap() noexcept; DWORD _trackControlKeyState(const KEY_EVENT_RECORD& key) noexcept; 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 _makeCharOutput(uint32_t ch); [[nodiscard]] static StringType _makeNoOutput() noexcept; void _escapeOutput(StringType& charSequence, bool altIsPressed) const; [[nodiscard]] OutputType _makeWin32Output(const KEY_EVENT_RECORD& key) const; - [[nodiscard]] OutputType _makeKittyOutput(const KEY_EVENT_RECORD& key, DWORD controlKeyState); - static int32_t _getKittyKeyCode(const KEY_EVENT_RECORD& key, DWORD controlKeyState) noexcept; + [[nodiscard]] KeyEncodingInfo _getKeyEncodingInfo(const KEY_EVENT_RECORD& key, DWORD simpleKeyState) const noexcept; std::vector& _getKittyStack() noexcept; static bool _codepointIsText(uint32_t cp) noexcept; static CodepointBuffer _codepointToBuffer(uint32_t cp) noexcept; diff --git a/src/terminal/input/ut_kittyKeyboardProtocol.cpp b/src/terminal/input/ut_kittyKeyboardProtocol.cpp deleted file mode 100644 index d2126c564e..0000000000 --- a/src/terminal/input/ut_kittyKeyboardProtocol.cpp +++ /dev/null @@ -1,1410 +0,0 @@ -// 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); - } - - TerminalInput createInput(uint8_t flags) - { - auto input = createInput(flags); - return input; - } -} - -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_0b00000_NoEnhancements_SimpleKeyPress) - { - auto input = createInput(0); - - // 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_0b00001_Disambiguate_EscapeKey) - { - auto input = createInput(DisambiguateEscapeCodes); - - // 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_0b00001_Disambiguate_AltLetter) - { - auto input = createInput(DisambiguateEscapeCodes); - - // 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_0b00001_Disambiguate_CtrlLetter) - { - auto input = createInput(DisambiguateEscapeCodes); - - // 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_0b00001_Disambiguate_CtrlAltLetter) - { - auto input = createInput(DisambiguateEscapeCodes); - - // 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_0b00001_Disambiguate_ShiftAltLetter) - { - auto input = createInput(DisambiguateEscapeCodes); - - // 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_0b00010_EventTypes_PressEvent) - { - auto input = createInput(ReportEventTypes); - - // ReportEventTypes alone doesn't encode text keys - they produce plain text - // Only functional keys get event type encoding without AllKeysAsEscapeCodes - VERIFY_ARE_EQUAL(wrap(L"a"), process(input, true, VK_A, 0x1E, L'a', 0)); - } - - TEST_METHOD(EnhancementFlags_0b00010_EventTypes_ReleaseEvent_FunctionalKey) - { - auto input = createInput(ReportEventTypes); - - // 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_0b00011_DisambiguateAndEventTypes_RepeatEvent) - { - auto input = createInput(DisambiguateEscapeCodes | ReportEventTypes); - - // 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_0b00011_DisambiguateAndEventTypes_ReleaseEvent) - { - auto input = createInput(DisambiguateEscapeCodes | ReportEventTypes); - - // 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_0b00100_AlternateKeys_ShiftedKey) - { - auto input = createInput(ReportAlternateKeys); - - // ReportAlternateKeys alone doesn't trigger CSI u encoding for text keys - // Shift+a should produce plain 'A' since the key isn't being encoded as escape - VERIFY_ARE_EQUAL(wrap(L"A"), process(input, true, VK_A, 0x1E, L'A', SHIFT_PRESSED)); - } - - // Flag Combination 0b00101 (5) - Disambiguate + Alternate keys - TEST_METHOD(EnhancementFlags_0b00101_DisambiguateAndAlternate_ShiftedKey) - { - auto input = createInput(DisambiguateEscapeCodes | ReportAlternateKeys); - - // 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_0b00101_DisambiguateAndAlternate_BaseLayoutKey) - { - auto input = createInput(DisambiguateEscapeCodes | ReportAlternateKeys); - - // 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_0b00110_EventTypesAndAlternate) - { - auto input = createInput(ReportEventTypes | ReportAlternateKeys); - - // 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_0b00111_ThreeFlags_ShiftedKeyWithRelease) - { - auto input = createInput(DisambiguateEscapeCodes | ReportEventTypes | ReportAlternateKeys); - - // 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_0b01000_AllKeysAsEscapeCodes_PlainText) - { - auto input = createInput(ReportAllKeysAsEscapeCodes); - - // 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_0b01000_AllKeysAsEscapeCodes_EnterKey) - { - auto input = createInput(ReportAllKeysAsEscapeCodes); - - // 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_0b01000_AllKeysAsEscapeCodes_TabKey) - { - auto input = createInput(ReportAllKeysAsEscapeCodes); - - // 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_0b01000_AllKeysAsEscapeCodes_BackspaceKey) - { - auto input = createInput(ReportAllKeysAsEscapeCodes); - - // Backspace key encoded as CSI 127u - VERIFY_ARE_EQUAL(wrap(L"\x1b[127u"), process(input, true, VK_BACK, 0x0E, 0x7F, 0)); - } - - TEST_METHOD(EnhancementFlags_0b01000_AllKeysAsEscapeCodes_ModifierKey) - { - auto input = createInput(ReportAllKeysAsEscapeCodes); - - // 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_0b01001_DisambiguateAndAllKeys) - { - auto input = createInput(DisambiguateEscapeCodes | ReportAllKeysAsEscapeCodes); - - // 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_0b01010_EventTypesAndAllKeys_EnterRelease) - { - auto input = createInput(ReportEventTypes | ReportAllKeysAsEscapeCodes); - - // 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_0b01010_EventTypesAndAllKeys_TabRelease) - { - auto input = createInput(ReportEventTypes | ReportAllKeysAsEscapeCodes); - - // 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_0b01010_EventTypesAndAllKeys_BackspaceRelease) - { - auto input = createInput(ReportEventTypes | ReportAllKeysAsEscapeCodes); - - // 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_0b01011_ThreeFlags_PlainKeyRepeat) - { - auto input = createInput(DisambiguateEscapeCodes | ReportEventTypes | ReportAllKeysAsEscapeCodes); - - // 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_0b01100_AlternateAndAllKeys) - { - auto input = createInput(ReportAlternateKeys | ReportAllKeysAsEscapeCodes); - - // 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_0b01101_ThreeFlags) - { - auto input = createInput(DisambiguateEscapeCodes | ReportAlternateKeys | ReportAllKeysAsEscapeCodes); - - // 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_0b01110_ThreeFlags) - { - auto input = createInput(ReportEventTypes | ReportAlternateKeys | ReportAllKeysAsEscapeCodes); - - // 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_0b01111_FourFlags) - { - auto input = createInput(DisambiguateEscapeCodes | ReportEventTypes | ReportAlternateKeys | ReportAllKeysAsEscapeCodes); - - // 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_0b10000_AssociatedText_NoEffect) - { - auto input = createInput(ReportAssociatedText); - - // ReportAssociatedText without AllKeysAsEscapeCodes is undefined per spec. - // Text keys fall through to legacy - plain 'a' produces 'a' - VERIFY_ARE_EQUAL(wrap(L"a"), process(input, true, VK_A, 0x1E, L'a', 0)); - } - - // Flag Combination 0b10001 (17) - Disambiguate + Associated text - TEST_METHOD(EnhancementFlags_0b10001_DisambiguateAndText) - { - auto input = createInput(DisambiguateEscapeCodes | ReportAssociatedText); - - // Disambiguate only encodes modified keys or ambiguous keys. - // Plain 'a' with no modifiers produces legacy 'a' - VERIFY_ARE_EQUAL(wrap(L"a"), process(input, true, VK_A, 0x1E, L'a', 0)); - } - - // Flag Combination 0b10010 (18) - Event types + Associated text - TEST_METHOD(EnhancementFlags_0b10010_EventTypesAndText) - { - auto input = createInput(ReportEventTypes | ReportAssociatedText); - - // F1 is a functional key - uses SS3 P encoding (press is default, no event type shown) - VERIFY_ARE_EQUAL(wrap(L"\x1bOP"), process(input, true, VK_F1, 0x3B, 0, 0)); - } - - // Flag Combination 0b10011 (19) - Disambiguate + Event types + Associated text - TEST_METHOD(EnhancementFlags_0b10011_ThreeFlags) - { - auto input = createInput(DisambiguateEscapeCodes | ReportEventTypes | ReportAssociatedText); - - // 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_0b10100_AlternateAndText) - { - auto input = createInput(ReportAlternateKeys | ReportAssociatedText); - - // Neither flag causes text keys to be CSI u encoded - VERIFY_ARE_EQUAL(wrap(L"a"), process(input, true, VK_A, 0x1E, L'a', 0)); - } - - // Flag Combination 0b10101 (21) - Disambiguate + Alternate + Associated text - TEST_METHOD(EnhancementFlags_0b10101_ThreeFlags) - { - auto input = createInput(DisambiguateEscapeCodes | ReportAlternateKeys | ReportAssociatedText); - - // Shift+a triggers Disambiguate encoding with alternate key. - // Text param is undefined without AllKeysAsEscapeCodes, so just key:shifted;modifier - VERIFY_ARE_EQUAL(wrap(L"\x1b[97:65;2u"), process(input, true, VK_A, 0x1E, L'A', SHIFT_PRESSED)); - } - - // Flag Combination 0b10110 (22) - Event types + Alternate + Associated text - TEST_METHOD(EnhancementFlags_0b10110_ThreeFlags) - { - auto input = createInput(ReportEventTypes | ReportAlternateKeys | ReportAssociatedText); - - // F1 press - functional key uses legacy SS3 P (press is default) - VERIFY_ARE_EQUAL(wrap(L"\x1bOP"), process(input, true, VK_F1, 0x3B, 0, 0)); - } - - // Flag Combination 0b10111 (23) - Disambiguate + Event types + Alternate + Associated text - TEST_METHOD(EnhancementFlags_0b10111_FourFlags) - { - auto input = createInput(DisambiguateEscapeCodes | ReportEventTypes | ReportAlternateKeys | ReportAssociatedText); - - // 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_0b11000_AllKeysAndText_SimpleKey) - { - auto input = createInput(ReportAllKeysAsEscapeCodes | ReportAssociatedText); - - // 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_0b11000_AllKeysAndText_ShiftKey) - { - auto input = createInput(ReportAllKeysAsEscapeCodes | ReportAssociatedText); - - // 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_0b11001_ThreeFlags) - { - auto input = createInput(DisambiguateEscapeCodes | ReportAllKeysAsEscapeCodes | ReportAssociatedText); - - // 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_0b11010_ThreeFlags_KeyRelease) - { - auto input = createInput(ReportEventTypes | ReportAllKeysAsEscapeCodes | ReportAssociatedText); - - // Press: CSI 97;;97u (with text) - process(input, true, VK_A, 0x1E, L'a', 0); - // Release: CSI 97;1:3u (no text on release per spec) - VERIFY_ARE_EQUAL(wrap(L"\x1b[97;1:3u"), process(input, false, VK_A, 0x1E, L'a', 0)); - } - - // Flag Combination 0b11011 (27) - Disambiguate + Event types + All keys + Associated text - TEST_METHOD(EnhancementFlags_0b11011_FourFlags) - { - auto input = createInput(DisambiguateEscapeCodes | ReportEventTypes | ReportAllKeysAsEscapeCodes | ReportAssociatedText); - - // 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_0b11100_ThreeFlags) - { - auto input = createInput(ReportAlternateKeys | ReportAllKeysAsEscapeCodes | ReportAssociatedText); - - // 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_0b11101_FourFlags) - { - auto input = createInput(DisambiguateEscapeCodes | ReportAlternateKeys | ReportAllKeysAsEscapeCodes | ReportAssociatedText); - - 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_0b11110_FourFlags) - { - auto input = createInput(ReportEventTypes | ReportAlternateKeys | ReportAllKeysAsEscapeCodes | ReportAssociatedText); - - // 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_0b11111_AllFlags_FullSequence) - { - auto input = createInput(DisambiguateEscapeCodes | ReportEventTypes | ReportAlternateKeys | ReportAllKeysAsEscapeCodes | ReportAssociatedText); - - // 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_0b11111_AllFlags_ShiftedKey) - { - auto input = createInput(DisambiguateEscapeCodes | ReportEventTypes | ReportAlternateKeys | ReportAllKeysAsEscapeCodes | ReportAssociatedText); - - // 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) - { - auto input = createInput(DisambiguateEscapeCodes); - - // 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) - { - auto input = createInput(DisambiguateEscapeCodes); - - // 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) - { - auto input = createInput(DisambiguateEscapeCodes); - - // 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) - { - auto input = createInput(DisambiguateEscapeCodes); - - // 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) - { - auto input = createInput(DisambiguateEscapeCodes); - - // 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) - { - auto input = createInput(DisambiguateEscapeCodes); - - // 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) - { - auto input = createInput(DisambiguateEscapeCodes); - - // 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) - { - auto input = createInput(ReportAllKeysAsEscapeCodes); - - // 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) - { - auto input = createInput(ReportAllKeysAsEscapeCodes); - - // 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) - { - auto input = createInput(ReportAllKeysAsEscapeCodes); - - // 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) - { - auto input = createInput(DisambiguateEscapeCodes); - - // 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) - { - auto input = createInput(ReportEventTypes | ReportAllKeysAsEscapeCodes); - - // Press event (type 1) is default - first press should be CSI 97u (type omitted) - VERIFY_ARE_EQUAL(wrap(L"\x1b[97u"), process(input, true, VK_A, 0x1E, L'a', 0)); - } - - TEST_METHOD(EventTypes_Repeat_Type2) - { - auto input = createInput(ReportEventTypes | ReportAllKeysAsEscapeCodes); - - // 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) - { - auto input = createInput(ReportEventTypes | ReportAllKeysAsEscapeCodes); - - // 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) - { - auto input = createInput(ReportEventTypes | ReportAllKeysAsEscapeCodes); - - // 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) - { - auto input = createInput(ReportEventTypes | ReportAllKeysAsEscapeCodes); - - // 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) - { - auto input = createInput(DisambiguateEscapeCodes | ReportEventTypes); - - // 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) - { - auto input = createInput(DisambiguateEscapeCodes | ReportEventTypes); - - // 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) - { - auto input = createInput(DisambiguateEscapeCodes | ReportEventTypes); - - // 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) - { - auto input = createInput(DisambiguateEscapeCodes); - - // 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) - { - auto input = createInput(DisambiguateEscapeCodes); - - // Per spec: Enter, Tab, Backspace still produce legacy bytes with Disambiguate - // to allow typing 'reset' at shell prompt if program crashes - VERIFY_ARE_EQUAL(wrap(L"\r"), process(input, true, VK_RETURN, 0x1C, L'\r', 0)); - } - - TEST_METHOD(SpecialKeys_Tab_LegacyBehavior) - { - auto input = createInput(DisambiguateEscapeCodes); - - // Per spec: Tab produces legacy HT to allow typing 'reset' at shell prompt - VERIFY_ARE_EQUAL(wrap(L"\t"), process(input, true, VK_TAB_KEY, 0x0F, L'\t', 0)); - } - - TEST_METHOD(SpecialKeys_Backspace_LegacyBehavior) - { - auto input = createInput(DisambiguateEscapeCodes); - - // Per spec: Backspace produces legacy DEL to allow typing 'reset' at shell prompt - VERIFY_ARE_EQUAL(wrap(L"\x7f"), 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) - { - auto input = createInput(DisambiguateEscapeCodes); - - // 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) - { - auto input = createInput(DisambiguateEscapeCodes); - - // 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) - { - auto input = createInput(DisambiguateEscapeCodes); - - VERIFY_ARE_EQUAL(wrap(L"\x1bOQ"), process(input, true, VK_F2, 0x3C, 0, 0)); - } - - TEST_METHOD(FunctionalKeys_F3) - { - auto input = createInput(DisambiguateEscapeCodes); - - VERIFY_ARE_EQUAL(wrap(L"\x1bOR"), process(input, true, VK_F3, 0x3D, 0, 0)); - } - - TEST_METHOD(FunctionalKeys_F4) - { - auto input = createInput(DisambiguateEscapeCodes); - - VERIFY_ARE_EQUAL(wrap(L"\x1bOS"), process(input, true, VK_F4, 0x3E, 0, 0)); - } - - TEST_METHOD(FunctionalKeys_F5) - { - auto input = createInput(DisambiguateEscapeCodes); - - // F5 uses CSI 15 ~ - VERIFY_ARE_EQUAL(wrap(L"\x1b[15~"), process(input, true, VK_F5, 0x3F, 0, 0)); - } - - TEST_METHOD(FunctionalKeys_F5_WithModifiers) - { - auto input = createInput(DisambiguateEscapeCodes); - - // 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) - { - auto input = createInput(DisambiguateEscapeCodes); - - // F12 uses CSI 24 ~ - VERIFY_ARE_EQUAL(wrap(L"\x1b[24~"), process(input, true, VK_F12, 0x58, 0, 0)); - } - - TEST_METHOD(FunctionalKeys_F13) - { - auto input = createInput(ReportAllKeysAsEscapeCodes); - - // 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) - { - auto input = createInput(ReportAllKeysAsEscapeCodes); - - // F24 = 57387 - VERIFY_ARE_EQUAL(wrap(L"\x1b[57387u"), process(input, true, VK_F24, 0x87, 0, 0)); - } - - // Navigation keys - TEST_METHOD(FunctionalKeys_ArrowUp_Legacy) - { - auto input = createInput(DisambiguateEscapeCodes); - - // 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) - { - auto input = createInput(DisambiguateEscapeCodes); - - // 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) - { - auto input = createInput(DisambiguateEscapeCodes); - - VERIFY_ARE_EQUAL(wrap(L"\x1b[B"), process(input, true, VK_DOWN, 0x50, 0, ENHANCED_KEY)); - } - - TEST_METHOD(FunctionalKeys_ArrowLeft) - { - auto input = createInput(DisambiguateEscapeCodes); - - VERIFY_ARE_EQUAL(wrap(L"\x1b[D"), process(input, true, VK_LEFT, 0x4B, 0, ENHANCED_KEY)); - } - - TEST_METHOD(FunctionalKeys_ArrowRight) - { - auto input = createInput(DisambiguateEscapeCodes); - - VERIFY_ARE_EQUAL(wrap(L"\x1b[C"), process(input, true, VK_RIGHT, 0x4D, 0, ENHANCED_KEY)); - } - - TEST_METHOD(FunctionalKeys_Home) - { - auto input = createInput(DisambiguateEscapeCodes); - - VERIFY_ARE_EQUAL(wrap(L"\x1b[H"), process(input, true, VK_HOME, 0x47, 0, ENHANCED_KEY)); - } - - TEST_METHOD(FunctionalKeys_End) - { - auto input = createInput(DisambiguateEscapeCodes); - - VERIFY_ARE_EQUAL(wrap(L"\x1b[F"), process(input, true, VK_END, 0x4F, 0, ENHANCED_KEY)); - } - - TEST_METHOD(FunctionalKeys_Insert) - { - auto input = createInput(DisambiguateEscapeCodes); - - // Insert: CSI 2 ~ - VERIFY_ARE_EQUAL(wrap(L"\x1b[2~"), process(input, true, VK_INSERT, 0x52, 0, ENHANCED_KEY)); - } - - TEST_METHOD(FunctionalKeys_Delete) - { - auto input = createInput(DisambiguateEscapeCodes); - - // Delete: CSI 3 ~ - VERIFY_ARE_EQUAL(wrap(L"\x1b[3~"), process(input, true, VK_DELETE, 0x53, 0, ENHANCED_KEY)); - } - - TEST_METHOD(FunctionalKeys_PageUp) - { - auto input = createInput(DisambiguateEscapeCodes); - - // PageUp: CSI 5 ~ - VERIFY_ARE_EQUAL(wrap(L"\x1b[5~"), process(input, true, VK_PRIOR, 0x49, 0, ENHANCED_KEY)); - } - - TEST_METHOD(FunctionalKeys_PageDown) - { - auto input = createInput(DisambiguateEscapeCodes); - - // 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) - { - auto input = createInput(ReportAllKeysAsEscapeCodes); - - // KP_0 = 57399 - VERIFY_ARE_EQUAL(wrap(L"\x1b[57399u"), process(input, true, VK_NUMPAD0, 0x52, L'0', 0)); - } - - TEST_METHOD(KeypadKeys_NumpadAdd) - { - auto input = createInput(ReportAllKeysAsEscapeCodes); - - // KP_ADD = 57413 - VERIFY_ARE_EQUAL(wrap(L"\x1b[57413u"), process(input, true, VK_ADD, 0x4E, L'+', 0)); - } - - TEST_METHOD(KeypadKeys_NumpadSubtract) - { - auto input = createInput(ReportAllKeysAsEscapeCodes); - - // KP_SUBTRACT = 57412 - VERIFY_ARE_EQUAL(wrap(L"\x1b[57412u"), process(input, true, VK_SUBTRACT, 0x4A, L'-', 0)); - } - - TEST_METHOD(KeypadKeys_NumpadMultiply) - { - auto input = createInput(ReportAllKeysAsEscapeCodes); - - // KP_MULTIPLY = 57411 - VERIFY_ARE_EQUAL(wrap(L"\x1b[57411u"), process(input, true, VK_MULTIPLY, 0x37, L'*', 0)); - } - - TEST_METHOD(KeypadKeys_NumpadDivide) - { - auto input = createInput(ReportAllKeysAsEscapeCodes); - - // KP_DIVIDE = 57410 - VERIFY_ARE_EQUAL(wrap(L"\x1b[57410u"), process(input, true, VK_DIVIDE, 0x35, L'/', ENHANCED_KEY)); - } - - TEST_METHOD(KeypadKeys_NumpadDecimal) - { - auto input = createInput(ReportAllKeysAsEscapeCodes); - - // KP_DECIMAL = 57409 - VERIFY_ARE_EQUAL(wrap(L"\x1b[57409u"), process(input, true, VK_DECIMAL, 0x53, L'.', 0)); - } - - TEST_METHOD(KeypadKeys_NumpadEnter) - { - auto input = createInput(ReportAllKeysAsEscapeCodes); - - // 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) - { - auto input = createInput(ReportAllKeysAsEscapeCodes); - - // 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) - { - auto input = createInput(ReportAllKeysAsEscapeCodes); - - // 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) - { - auto input = createInput(ReportAllKeysAsEscapeCodes); - - // 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) - { - auto input = createInput(ReportAllKeysAsEscapeCodes); - - // RIGHT_SHIFT = 57447 - VERIFY_ARE_EQUAL(wrap(L"\x1b[57447;2u"), process(input, true, VK_RSHIFT, 0x36, 0, SHIFT_PRESSED)); - } - - TEST_METHOD(ModifierKeys_LeftControl) - { - auto input = createInput(ReportAllKeysAsEscapeCodes); - - // 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) - { - auto input = createInput(ReportAllKeysAsEscapeCodes); - - // 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) - { - auto input = createInput(ReportAllKeysAsEscapeCodes); - - // 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) - { - auto input = createInput(ReportAllKeysAsEscapeCodes); - - // 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) - { - auto input = createInput(ReportAllKeysAsEscapeCodes); - - // CAPS_LOCK = 57358 - VERIFY_ARE_EQUAL(wrap(L"\x1b[57358u"), process(input, true, VK_CAPITAL, 0x3A, 0, 0)); - } - - TEST_METHOD(ModifierKeys_NumLock) - { - auto input = createInput(ReportAllKeysAsEscapeCodes); - - // NUM_LOCK = 57360 - VERIFY_ARE_EQUAL(wrap(L"\x1b[57360u"), process(input, true, VK_NUMLOCK, 0x45, 0, ENHANCED_KEY)); - } - - TEST_METHOD(ModifierKeys_ScrollLock) - { - auto input = createInput(ReportAllKeysAsEscapeCodes); - - // 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) - { - auto input = createInput(DisambiguateEscapeCodes); - - // 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) - { - auto input = createInput(DisambiguateEscapeCodes); - - // 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) - { - auto input = createInput(ReportAllKeysAsEscapeCodes | ReportAssociatedText); - - // '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) - { - auto input = createInput(ReportAllKeysAsEscapeCodes | ReportAssociatedText); - - // 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) - { - auto input = createInput(ReportAllKeysAsEscapeCodes | ReportAssociatedText); - - // Escape doesn't produce text - VERIFY_ARE_EQUAL(wrap(L"\x1b[27u"), process(input, true, VK_ESCAPE, 0x01, 0, 0)); - } - - TEST_METHOD(TextAsCodepoints_NoTextOnRelease) - { - auto input = createInput(ReportEventTypes | ReportAllKeysAsEscapeCodes | ReportAssociatedText); - - // 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) - { - auto input = createInput(ReportAllKeysAsEscapeCodes | ReportAssociatedText); - - // 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) - { - auto input = createInput(ReportAllKeysAsEscapeCodes | ReportAssociatedText); - - // 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) - { - auto input = createInput(ReportAllKeysAsEscapeCodes); - - // VK_PACKET (0xE7) bypasses kitty encoding - UnicodeChar is passed through directly - // This is used for synthesized keyboard events (e.g., IME input) - VERIFY_ARE_EQUAL(wrap(L"x"), process(input, true, 0xE7, 0, L'x', 0)); - } - - TEST_METHOD(EdgeCase_ZeroVirtualKey_PassThrough) - { - auto input = createInput(ReportAllKeysAsEscapeCodes); - - // Zero virtual key bypasses kitty encoding - UnicodeChar is passed through directly - VERIFY_ARE_EQUAL(wrap(L"y"), process(input, true, 0, 0, L'y', 0)); - } - - TEST_METHOD(EdgeCase_AutoRepeat_Disabled) - { - auto input = createInput(ReportAllKeysAsEscapeCodes); - 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) - { - auto input = createInput(ReportAllKeysAsEscapeCodes | ReportAssociatedText); - - // 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) - { - auto input = createInput(DisambiguateEscapeCodes); - - // AltGr generates both RIGHT_ALT and LEFT_CTRL on Windows - // The fake LeftCtrl is detected via timing heuristics and ignored - // So 'ä' should be transmitted as plain text (AltGr is for character input) - VERIFY_ARE_EQUAL(wrap(L"ä"), process(input, true, VK_A, 0x1E, L'ä', RIGHT_ALT_PRESSED | LEFT_CTRL_PRESSED)); - } -};