Compare commits

...

10 Commits

Author SHA1 Message Date
Leonard Hecker
9354b5c7f6 Merge remote-tracking branch 'origin/main' into dev/lhecker/19977-kkp-altgr 2026-04-28 12:52:27 +02:00
Leonard Hecker
63b6bba433 Fixed SetTerminalInputKeyboardLayout 2026-04-17 20:32:18 +02:00
Leonard Hecker
6e3bb0ead4 Fix compilation failures? 2026-04-17 14:39:27 +02:00
Leonard Hecker
51416799f3 Merge remote-tracking branch 'origin/main' into dev/lhecker/19977-kkp-altgr 2026-04-17 00:30:01 +02:00
Leonard Hecker
552a79f269 SPELL CHECK 2026-04-03 18:22:08 +02:00
Leonard Hecker
0e4d6b8d0d Added test hooks, Use French AZERTY for tests 2026-04-03 18:17:46 +02:00
Leonard Hecker
fcda6e9df1 Fixed TerminalInputModifierKeyTests 2026-04-03 18:17:04 +02:00
Leonard Hecker
f0e34328fd Rethought how AltGr works 2026-04-03 18:16:26 +02:00
Leonard Hecker
e27f76da3f Spel 2026-04-02 13:05:15 +02:00
Leonard Hecker
446f0dffcf Fix various KKP encoding issues 2026-04-02 12:59:42 +02:00
13 changed files with 479 additions and 192 deletions

View File

@@ -29,6 +29,7 @@ allocing
alpc
ALTERNATENAME
ALTF
ALTGR
ALTNUMPAD
ALWAYSTIP
ansicpg
@@ -76,6 +77,7 @@ autoscrolling
Autowrap
AVerify
awch
AZERTY
azurecr
backgrounded
Backgrounder
@@ -840,6 +842,8 @@ kinda
KIYEOK
KKP
KLF
klid
KLLF
KLMNO
KOK
KPRIORITY
@@ -1102,6 +1106,7 @@ NOSELECTION
NOSENDCHANGING
NOSIZE
NOSNAPSHOT
NOTELLSHELL
NOTHOUSANDS
NOTICKS
NOTIMEOUTIFNOTHUNG

View File

@@ -15,12 +15,14 @@
<ClCompile Include="inputTest.cpp" />
<ClCompile Include="kittyKeyboardProtocol.cpp" />
<ClCompile Include="MouseInputTest.cpp" />
<ClCompile Include="TestHook.cpp" />
<ClCompile Include="..\precomp.cpp">
<PrecompiledHeader>Create</PrecompiledHeader>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="..\precomp.h" />
<ClInclude Include="TestHook.h" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\buffer\out\lib\bufferout.vcxproj">

View File

@@ -30,11 +30,17 @@
<ClCompile Include="kittyKeyboardProtocol.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="TestHook.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="..\precomp.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="TestHook.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<Natvis Include="$(SolutionDir)tools\ConsoleTypes.natvis" />

View File

@@ -0,0 +1,132 @@
#include "precomp.h"
#include "TestHook.h"
using namespace TestHook;
thread_local HKL g_keyboardLayout;
extern "C" HKL TestHook_TerminalInput_KeyboardLayout()
{
return g_keyboardLayout;
}
static bool isPreloadedLayout(const wchar_t* klid) noexcept
{
wil::unique_hkey preloadKey;
if (RegOpenKeyExW(HKEY_CURRENT_USER, L"Keyboard Layout\\Preload", 0, KEY_READ, preloadKey.addressof()) != ERROR_SUCCESS)
{
return false;
}
wil::unique_hkey substitutesKey;
RegOpenKeyExW(HKEY_CURRENT_USER, L"Keyboard Layout\\Substitutes", 0, KEY_READ, substitutesKey.addressof());
wchar_t idx[16];
wchar_t layoutId[KL_NAMELENGTH];
for (DWORD i = 0;; i++)
{
DWORD idxLen = ARRAYSIZE(idx);
DWORD layoutIdSize = sizeof(layoutId);
if (RegEnumValueW(preloadKey.get(), i, idx, &idxLen, nullptr, nullptr, reinterpret_cast<BYTE*>(layoutId), &layoutIdSize) != ERROR_SUCCESS)
{
break;
}
// Preload contains base language IDs (e.g. "0000040c").
// The actual layout ID (e.g. "0001040c") may only appear in the Substitutes key.
if (substitutesKey)
{
wchar_t substitute[KL_NAMELENGTH];
DWORD substituteSize = sizeof(substitute);
if (RegGetValueW(substitutesKey.get(), nullptr, layoutId, RRF_RT_REG_SZ, nullptr, substitute, &substituteSize) == ERROR_SUCCESS)
{
memcpy(layoutId, substitute, sizeof(layoutId));
}
}
if (wcscmp(layoutId, klid) == 0)
{
return true;
}
}
return false;
}
void LayoutGuard::_destroy() const noexcept
{
if (g_keyboardLayout == _layout)
{
g_keyboardLayout = nullptr;
}
if (_needsUnload)
{
UnloadKeyboardLayout(_layout);
}
}
LayoutGuard::LayoutGuard(HKL layout, bool needsUnload) noexcept :
_layout{ layout },
_needsUnload{ needsUnload }
{
}
LayoutGuard::~LayoutGuard()
{
_destroy();
}
LayoutGuard::LayoutGuard(LayoutGuard&& other) noexcept :
_layout{ std::exchange(other._layout, nullptr) },
_needsUnload{ std::exchange(other._needsUnload, false) }
{
}
LayoutGuard& LayoutGuard::operator=(LayoutGuard&& other) noexcept
{
if (this != &other)
{
_destroy();
_layout = std::exchange(other._layout, nullptr);
_needsUnload = std::exchange(other._needsUnload, false);
}
return *this;
}
LayoutGuard::operator bool() const noexcept
{
return _layout != nullptr;
}
LayoutGuard::operator HKL() const noexcept
{
return _layout;
}
LayoutGuard TestHook::SetTerminalInputKeyboardLayout(const wchar_t* klid)
{
THROW_HR_IF_MSG(E_UNEXPECTED, g_keyboardLayout != nullptr, "Nested layout test overrides are not supported");
// Check if the layout is installed. LoadKeyboardLayoutW silently returns the
// current active layout if the requested one is missing.
const auto keyPath = fmt::format(FMT_COMPILE(L"SYSTEM\\CurrentControlSet\\Control\\Keyboard Layouts\\{}"), klid);
wil::unique_hkey key;
if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, keyPath.c_str(), 0, KEY_READ, key.addressof()) != ERROR_SUCCESS)
{
return {};
}
const auto layout = LoadKeyboardLayoutW(klid, KLF_NOTELLSHELL);
THROW_LAST_ERROR_IF_NULL(layout);
g_keyboardLayout = layout;
// Unload the layout if it's not one of the user's layouts.
// GetKeyboardLayoutList is unreliable for this purpose, as the keyboard layout API mutates global OS state.
// If a process crashes or exits early without calling UnloadKeyboardLayout all future processes will get it
// returned in their GetKeyboardLayoutList calls. Shell could fix it but alas. So we peek into the registry.
const auto needsUnload = !isPreloadedLayout(klid);
return { layout, needsUnload };
}

View File

@@ -0,0 +1,27 @@
#pragma once
namespace TestHook
{
struct LayoutGuard
{
LayoutGuard() = default;
LayoutGuard(HKL layout, bool needsUnload) noexcept;
~LayoutGuard();
LayoutGuard(const LayoutGuard&) = delete;
LayoutGuard& operator=(const LayoutGuard&) = delete;
LayoutGuard(LayoutGuard&& other) noexcept;
LayoutGuard& operator=(LayoutGuard&& other) noexcept;
explicit operator bool() const noexcept;
operator HKL() const noexcept;
private:
void _destroy() const noexcept;
HKL _layout = nullptr;
bool _needsUnload = false;
};
LayoutGuard SetTerminalInputKeyboardLayout(const wchar_t* klid);
}

View File

@@ -3,6 +3,7 @@
#include "precomp.h"
#include "TestHook.h"
#include "../../../interactivity/inc/VtApiRedirection.hpp"
#include "../../input/terminalInput.hpp"
#include "../types/inc/IInputEvent.hpp"
@@ -308,6 +309,30 @@ void InputTest::TerminalInputModifierKeyTests()
const auto slashVkey = LOBYTE(OneCoreSafeVkKeyScanW(L'/'));
const auto nullVkey = LOBYTE(OneCoreSafeVkKeyScanW(0));
uint8_t keyboardState[256] = {};
wchar_t unicodeBuf[4] = {};
const uint8_t rightAlt = WI_IsFlagSet(uiKeystate, RIGHT_ALT_PRESSED) ? 0x80 : 0;
const uint8_t leftAlt = WI_IsFlagSet(uiKeystate, LEFT_ALT_PRESSED) ? 0x80 : 0;
const uint8_t rightCtrl = WI_IsFlagSet(uiKeystate, RIGHT_CTRL_PRESSED) ? 0x80 : 0;
const uint8_t leftCtrl = WI_IsFlagSet(uiKeystate, LEFT_CTRL_PRESSED) ? 0x80 : 0;
const uint8_t shift = WI_IsFlagSet(uiKeystate, SHIFT_PRESSED) ? 0x80 : 0;
const uint8_t capsLock = WI_IsFlagSet(uiKeystate, CAPSLOCK_ON) ? 0x01 : 0;
keyboardState[VK_SHIFT] = shift;
keyboardState[VK_CONTROL] = leftCtrl | rightCtrl;
keyboardState[VK_MENU] = leftAlt | rightAlt;
keyboardState[VK_CAPITAL] = capsLock;
keyboardState[VK_LSHIFT] = shift;
keyboardState[VK_LCONTROL] = leftCtrl;
keyboardState[VK_RCONTROL] = rightCtrl;
keyboardState[VK_LMENU] = leftAlt;
keyboardState[VK_RMENU] = rightAlt;
const auto anyCtrlPressed = WI_IsAnyFlagSet(uiKeystate, CTRL_PRESSED);
const auto bothCtrlPressed = WI_AreAllFlagsSet(uiKeystate, CTRL_PRESSED);
const auto anyAltPressed = WI_IsAnyFlagSet(uiKeystate, ALT_PRESSED);
const auto bothAltPressed = WI_AreAllFlagsSet(uiKeystate, ALT_PRESSED);
const auto shiftPressed = WI_IsFlagSet(uiKeystate, SHIFT_PRESSED);
Log::Comment(L"Sending every possible VKEY at the input stream for interception during key DOWN.");
for (BYTE vkey = 0; vkey < BYTE_MAX; vkey++)
{
@@ -315,9 +340,17 @@ void InputTest::TerminalInputModifierKeyTests()
auto fExpectedKeyHandled = true;
auto fModifySequence = false;
wchar_t ch = LOWORD(OneCoreSafeMapVirtualKeyW(vkey, MAPVK_VK_TO_CHAR));
if (ControlPressed(uiKeystate))
til::at(keyboardState, vkey) = 0x80; // Momentarily pretend as if the key is set
const auto unicodeLen = ToUnicodeEx(vkey, 0, &keyboardState[0], &unicodeBuf[0], ARRAYSIZE(unicodeBuf), 0b101, nullptr);
til::at(keyboardState, vkey) = 0;
wchar_t ch = unicodeLen == 1 ? unicodeBuf[0] : 0;
const auto altGrPressed = anyAltPressed && anyCtrlPressed && (ch > 0x20 && ch != 0x7f);
const auto ctrlPressed = bothCtrlPressed || (anyCtrlPressed && !altGrPressed);
const auto altPressed = bothAltPressed || (anyAltPressed && !altGrPressed);
if (ctrlPressed)
{
// For Ctrl-/ see DifferentModifiersTest.
if (vkey == VK_DIVIDE || vkey == slashVkey)
@@ -472,28 +505,28 @@ void InputTest::TerminalInputModifierKeyTests()
expected = TerminalInput::MakeOutput({ &ch, 1 });
break;
case VK_RETURN:
if (AltPressed(uiKeystate))
if (altPressed)
{
const auto str = ControlPressed(uiKeystate) ? L"\x1b\n" : L"\x1b\r";
const auto str = ctrlPressed ? L"\x1b\n" : L"\x1b\r";
expected = TerminalInput::MakeOutput(str);
}
else
{
const auto str = ControlPressed(uiKeystate) ? L"\n" : L"\r";
const auto str = ctrlPressed ? L"\n" : L"\r";
expected = TerminalInput::MakeOutput(str);
}
break;
case VK_TAB:
if (AltPressed(uiKeystate))
if (altPressed)
{
// Alt+Tab isn't possible - that's reserved by the system.
continue;
}
else if (ShiftPressed(uiKeystate))
else if (shiftPressed)
{
expected = TerminalInput::MakeOutput(L"\x1b[Z");
}
else if (ControlPressed(uiKeystate))
else
{
expected = TerminalInput::MakeOutput(L"\t");
}
@@ -506,13 +539,19 @@ void InputTest::TerminalInputModifierKeyTests()
case VK_OEM_102:
// OEM keys require special case handling when combined with a Ctrl
// modifier, but otherwise work the same way as regular keys.
if (ControlPressed(uiKeystate))
if (ctrlPressed)
{
continue;
}
[[fallthrough]];
default:
if (ControlPressed(uiKeystate) && (vkey >= '1' && vkey <= '9'))
// Map VK_ESCAPE, etc., to their corresponding character value, if needed.
if (ch == 0)
{
ch = LOWORD(OneCoreSafeMapVirtualKeyW(vkey, MAPVK_VK_TO_CHAR));
}
if (ctrlPressed && (vkey >= '1' && vkey <= '9'))
{
// The C-# keys get translated into very specific control
// characters that don't play nicely with this test. These keys
@@ -531,7 +570,7 @@ void InputTest::TerminalInputModifierKeyTests()
// Alt+Key generates [0x1b, Ctrl+key] into the stream
// Pressing the control key causes all bits but the 5 least
// significant ones to be zeroed out (when using ASCII).
if (AltPressed(uiKeystate) && ControlPressed(uiKeystate) && ch > 0x40 && ch <= 0x5A)
if (altPressed && ctrlPressed && ch > L'@' && ch <= L'~')
{
const wchar_t buffer[2]{ L'\x1b', gsl::narrow_cast<wchar_t>(ch & 0b11111) };
expected = TerminalInput::MakeOutput({ &buffer[0], 2 });
@@ -540,17 +579,25 @@ void InputTest::TerminalInputModifierKeyTests()
}
// Alt+Key generates [0x1b, key] into the stream
if (AltPressed(uiKeystate) && ch != 0)
if (altPressed && ch != 0)
{
const wchar_t buffer[2]{ L'\x1b', ch };
expected = TerminalInput::MakeOutput({ &buffer[0], 2 });
if (ControlPressed(uiKeystate))
if (ctrlPressed)
{
ch = 0;
}
break;
}
// Ctrl+Key masks the key value.
if (ctrlPressed && ch > L'@' && ch <= L'~')
{
const auto b = gsl::narrow_cast<wchar_t>(ch & 0b11111);
expected = TerminalInput::MakeOutput({ &b, 1 });
break;
}
if (ch != 0)
{
expected = TerminalInput::MakeOutput({ &ch, 1 });
@@ -563,11 +610,14 @@ void InputTest::TerminalInputModifierKeyTests()
if (fModifySequence)
{
auto fShift = !!(uiKeystate & SHIFT_PRESSED);
auto fAlt = (uiKeystate & LEFT_ALT_PRESSED) || (uiKeystate & RIGHT_ALT_PRESSED);
auto fCtrl = (uiKeystate & LEFT_CTRL_PRESSED) || (uiKeystate & RIGHT_CTRL_PRESSED);
const auto mod = shiftPressed + (2 * altPressed) + (4 * ctrlPressed);
if (mod == 0)
{
continue;
}
auto& str = expected.value();
str[str.size() - 2] = L'1' + (fShift ? 1 : 0) + (fAlt ? 2 : 0) + (fCtrl ? 4 : 0);
str[str.size() - 2] = static_cast<wchar_t>(L'1' + mod);
}
TestKey(expected, input, uiKeystate, vkey, ch);
@@ -578,13 +628,20 @@ void InputTest::TerminalInputNullKeyTests()
{
using namespace std::string_view_literals;
auto layout = TestHook::SetTerminalInputKeyboardLayout(L"00000409"); // US English
if (!layout)
{
Log::Result(TestResults::Result::Skipped);
return;
}
unsigned int uiKeystate = LEFT_CTRL_PRESSED;
TerminalInput input;
Log::Comment(L"Sending every possible VKEY at the input stream for interception during key DOWN.");
BYTE vkey = LOBYTE(OneCoreSafeVkKeyScanW(0));
BYTE vkey = LOBYTE(VkKeyScanExW(0, layout));
Log::Comment(NoThrowString().Format(L"Testing key, state =0x%x, 0x%x", vkey, uiKeystate));
INPUT_RECORD irTest = { 0 };
@@ -600,7 +657,6 @@ void InputTest::TerminalInputNullKeyTests()
vkey = VK_SPACE;
Log::Comment(NoThrowString().Format(L"Testing key, state =0x%x, 0x%x", vkey, uiKeystate));
irTest.Event.KeyEvent.wVirtualKeyCode = vkey;
irTest.Event.KeyEvent.uChar.UnicodeChar = vkey;
VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"\0"sv), input.HandleKey(irTest), L"Verify key was handled if it should have been.");
uiKeystate = LEFT_CTRL_PRESSED | LEFT_ALT_PRESSED;

View File

@@ -6,6 +6,7 @@
#include <consoletaeftemplates.hpp>
#include <WexTestClass.h>
#include "TestHook.h"
#include "../../input/terminalInput.hpp"
using namespace WEX::TestExecution;
@@ -65,24 +66,24 @@ namespace
constexpr TestCase testCases[] = {
// Core behavior: DisambiguateEscapeCodes (D)
{ L"D Esc", L"\x1b[27u", D, true, VK_ESCAPE, 1, 0, 0 },
{ L"D Ctrl+a", L"\x1b[97;5u", D, true, 'A', 0x1E, L'\x01', Ctrl },
{ L"D Ctrl+Alt+a", L"\x1b[97;7u", D, true, 'A', 0x1E, L'\x01', Ctrl | Alt },
{ L"D Shift+Alt+a", L"\x1b[97;4u", D, true, 'A', 0x1E, L'A', Shift | Alt },
{ L"D Shift+a", L"A", D, true, 'A', 0x1E, L'A', Shift },
{ L"D Ctrl+a", L"\x1b[97;5u", D, true, 'A', 0x10, 0, Ctrl },
{ L"D Ctrl+Alt+a", L"æ", D, true, 'A', 0x10, L'æ', Ctrl | Alt },
{ L"D Shift+Alt+a", L"\x1b[97;4u", D, true, 'A', 0x10, L'A', Shift | Alt },
{ L"D Shift+a", L"A", D, true, 'A', 0x10, L'A', Shift },
// Modifiers with AllKeys (K): all keys use CSI u
{ L"K a", L"\x1b[97u", K, true, 'A', 0x1E, L'a', 0 },
{ L"K Shift+a", L"\x1b[97;2u", K, true, 'A', 0x1E, L'A', Shift },
{ L"K Alt+a", L"\x1b[97;3u", K, true, 'A', 0x1E, L'a', Alt },
{ L"K Ctrl+a", L"\x1b[97;5u", K, true, 'A', 0x1E, L'\x01', Ctrl },
{ L"K Shift+Alt+a", L"\x1b[97;4u", K, true, 'A', 0x1E, L'A', Shift | Alt },
{ L"K Shift+Ctrl+a", L"\x1b[97;6u", K, true, 'A', 0x1E, L'\x01', Shift | Ctrl },
{ L"K Alt+Ctrl+a", L"\x1b[97;7u", K, true, 'A', 0x1E, L'\x01', Alt | Ctrl },
{ L"K Shift+Alt+Ctrl+a", L"\x1b[97;8u", K, true, 'A', 0x1E, L'\x01', Shift | Alt | Ctrl },
{ L"K CapsLock+a", L"\x1b[97;65u", K, true, 'A', 0x1E, L'A', CAPSLOCK_ON },
{ L"K NumLock+a", L"\x1b[97;129u", K, true, 'A', 0x1E, L'a', NUMLOCK_ON },
{ L"K CapsLock+NumLock+a", L"\x1b[97;193u", K, true, 'A', 0x1E, L'A', CAPSLOCK_ON | NUMLOCK_ON },
{ L"K all mods", L"\x1b[97;200u", K, true, 'A', 0x1E, L'\x01', Shift | Alt | Ctrl | CAPSLOCK_ON | NUMLOCK_ON },
{ L"K a", L"\x1b[97u", K, true, 'A', 0x10, L'a', 0 },
{ L"K Shift+a", L"\x1b[97;2u", K, true, 'A', 0x10, L'A', Shift },
{ L"K Alt+a", L"\x1b[97;3u", K, true, 'A', 0x10, L'a', Alt },
{ L"K Ctrl+a", L"\x1b[97;5u", K, true, 'A', 0x10, 0, Ctrl },
{ L"K Shift+Alt+a", L"\x1b[97;4u", K, true, 'A', 0x10, L'A', Shift | Alt },
{ L"K Shift+Ctrl+a", L"\x1b[97;6u", K, true, 'A', 0x10, 0, Shift | Ctrl },
{ L"K Ctrl+Alt+a", L"\x1b[230u", K, true, 'A', 0x10, L'æ', Ctrl | Alt },
{ L"K Shift+Ctrl+Alt+a", L"\x1b[230;2u", K, true, 'A', 0x10, L'Æ', Shift | Ctrl | Alt },
{ L"K CapsLock+a", L"\x1b[97;65u", K, true, 'A', 0x10, L'A', CAPSLOCK_ON },
{ L"K NumLock+a", L"\x1b[97;129u", K, true, 'A', 0x10, L'a', NUMLOCK_ON },
{ L"K CapsLock+NumLock+a", L"\x1b[97;193u", K, true, 'A', 0x10, L'A', CAPSLOCK_ON | NUMLOCK_ON },
{ L"K all mods", L"\x1b[230;194u", K, true, 'A', 0x10, L'Æ', Shift | Ctrl | Alt | CAPSLOCK_ON | NUMLOCK_ON },
// Enter/Tab/Backspace: CSI u with K
{ L"K Enter", L"\x1b[13u", K, true, VK_RETURN, 0x1C, L'\r', 0 },
@@ -96,8 +97,8 @@ namespace
// Event types (D|E, E|K): release sends ;1:3
{ L"D|E Esc press", L"\x1b[27u", D | E, true, VK_ESCAPE, 1, 0, 0 },
{ L"D|E Esc release", L"\x1b[27;1:3u", D | E, false, VK_ESCAPE, 1, 0, 0 },
{ L"E|K a press", L"\x1b[97u", E | K, true, 'A', 0x1E, L'a', 0 },
{ L"E|K a release", L"\x1b[97;1:3u", E | K, false, 'A', 0x1E, L'a', 0 },
{ L"E|K a press", L"\x1b[97u", E | K, true, 'A', 0x10, L'a', 0 },
{ L"E|K a release", L"\x1b[97;1:3u", E | K, false, 'A', 0x10, L'a', 0 },
{ L"E|K Enter release", L"\x1b[13;1:3u", E | K, false, VK_RETURN, 0x1C, L'\r', 0 },
{ L"E|K Tab release", L"\x1b[9;1:3u", E | K, false, VK_TAB, 0x0F, L'\t', 0 },
{ L"E|K Backspace release", L"\x1b[127;1:3u", E | K, false, VK_BACK, 0x0E, L'\b', 0 },
@@ -161,21 +162,21 @@ namespace
{ L"K Shift+F13", L"\x1b[57376;2u", K, true, VK_F13, 0x64, 0, Shift },
// Alternate keys (A|K): shifted key and base layout key
{ L"A|K Shift+a", L"\x1b[97:65;2u", A | K, true, 'A', 0x1E, L'A', Shift },
{ L"A|K Shift+1", L"\x1b[49:33;2u", A | K, true, '1', 0x02, L'!', Shift },
{ L"A|K a (no shift)", L"\x1b[97u", A | K, true, 'A', 0x1E, L'a', 0 },
{ L"A|K Shift+a", L"\x1b[97:65:113;2u", A | K, true, 'A', 0x10, L'A', Shift },
{ L"A|K Shift+1", L"\x1b[224:49:49;2u", A | K, true, '1', 0x02, L'!', Shift },
{ L"A|K a (no shift)", L"\x1b[97::113u", A | K, true, 'A', 0x10, L'a', 0 },
// Associated text (K|T): text codepoint in 3rd param
{ L"K|T Shift+a", L"\x1b[97;2;65u", K | T, true, 'A', 0x1E, L'A', Shift },
{ L"K|T Shift+1", L"\x1b[49;2;33u", K | T, true, '1', 0x02, L'!', Shift },
{ L"K|T Ctrl+a", L"\x1b[97;5u", K | T, true, 'A', 0x1E, L'\x01', Ctrl }, // control char omitted
{ L"K|T Shift+a", L"\x1b[97;2;65u", K | T, true, 'A', 0x10, L'A', Shift },
{ L"K|T Shift+1", L"\x1b[224;2;33u", K | T, true, '1', 0x02, L'!', Shift },
{ L"K|T Ctrl+a", L"\x1b[97;5u", K | T, true, 'A', 0x10, 0, Ctrl }, // control char omitted
// Edge cases
{ L"K Keypad Enter", L"\x1b[57414u", K, true, VK_RETURN, 0x1C, L'\r', ENHANCED_KEY },
{ L"K Regular Enter", L"\x1b[13u", K, true, VK_RETURN, 0x1C, L'\r', 0 },
{ L"K Shift+Alt+Ctrl+Esc", L"\x1b[27;8u", K, true, VK_ESCAPE, 1, 0, Shift | Alt | Ctrl },
{ L"E|K CapsLock+a", L"\x1b[97;65u", E | K, true, 'A', 0x1E, L'A', CAPSLOCK_ON },
{ L"E|K all mods release", L"\x1b[97;200:3u", E | K, false, 'A', 0x1E, L'\x01', Shift | Alt | Ctrl | CAPSLOCK_ON | NUMLOCK_ON },
{ L"K Shift+Ctrl+Alt+Esc", L"\x1b[27;8u", K, true, VK_ESCAPE, 1, 0, Shift | Ctrl | Alt },
{ L"E|K CapsLock+a", L"\x1b[97;65u", E | K, true, 'A', 0x10, L'A', CAPSLOCK_ON },
{ L"E|K all mods release", L"\x1b[230;194:3u", E | K, false, 'A', 0x10, L'Æ', Shift | Ctrl | Alt | CAPSLOCK_ON | NUMLOCK_ON },
// F1-F4 with kitty flags (CSI instead of SS3, F3 special case)
{ L"D F1", L"\x1b[P", D, true, VK_F1, 0x3B, 0, 0 },
@@ -210,20 +211,20 @@ namespace
{ L"E|K Up release", L"\x1b[1;1:3A", E | K, false, VK_UP, 0x48, 0, ENHANCED_KEY },
{ L"E|K Insert release", L"\x1b[2;1:3~", E | K, false, VK_INSERT, 0x52, 0, ENHANCED_KEY },
// Alternate keys with modifiers
{ L"A|K Shift+Ctrl+a", L"\x1b[97:65;6u", A | K, true, 'A', 0x1E, L'\x01', Shift | Ctrl },
{ L"A|K Shift+Ctrl+a", L"\x1b[97:65:113;6u", A | K, true, 'A', 0x10, 0, Shift | Ctrl },
// Associated text with plain key
{ L"K|T a", L"\x1b[97;;97u", K | T, true, 'A', 0x1E, L'a', 0 },
{ L"K|T a", L"\x1b[97;;97u", K | T, true, 'A', 0x10, L'a', 0 },
// Text not reported on release
{ L"E|K|T a release", L"\x1b[97;1:3u", E | K | T, false, 'A', 0x1E, L'a', 0 },
{ L"E|K|T a release", L"\x1b[97;1:3u", E | K | T, false, 'A', 0x10, L'a', 0 },
// Escape has no associated text
{ L"K|T Esc", L"\x1b[27u", K | T, true, VK_ESCAPE, 1, 0, 0 },
// Combined flags: alternate keys with locks
{ L"A|K CapsLock+Shift+a", L"\x1b[97:65;66u", A | K, true, 'A', 0x1E, L'a', CAPSLOCK_ON | Shift },
{ L"A|K CapsLock+Shift+a", L"\x1b[97:65:113;66u", A | K, true, 'A', 0x10, L'a', CAPSLOCK_ON | Shift },
// All flags combined
{ L"A|K|T Shift+a", L"\x1b[97:65;2;65u", A | K | T, true, 'A', 0x1E, L'A', Shift },
{ L"A|K|T Shift+a", L"\x1b[97:65:113;2;65u", A | K | T, true, 'A', 0x10, L'A', Shift },
// Release without EventTypes flag: no output
{ L"K a release (no EventTypes)", L"", K, false, 'A', 0x1E, L'a', 0 },
{ L"K a release (no EventTypes)", L"", K, false, 'A', 0x10, L'a', 0 },
// Enter/Tab/Backspace release without AllKeys: no output
{ L"D|E Enter press", L"\r", D | E, true, VK_RETURN, 0x1C, L'\r', 0 },
@@ -238,8 +239,8 @@ namespace
{ L"E|K CapsLock release (now on)", L"\x1b[57358;65:3u", E | K, false, VK_CAPITAL, 0x3A, 0, CAPSLOCK_ON },
// Associated text filtering
{ L"K|T Shift+a (text)", L"\x1b[97;2;65u", K | T, true, 'A', 0x1E, L'A', Shift },
{ L"K|T Ctrl+a (control char filtered)", L"\x1b[97;5u", K | T, true, 'A', 0x1E, L'\x01', Ctrl },
{ L"K|T Shift+a (text)", L"\x1b[97;2;65u", K | T, true, 'A', 0x10, L'A', Shift },
{ L"K|T Ctrl+a (control char filtered)", L"\x1b[97;5u", K | T, true, 'A', 0x10, 0, Ctrl },
{ L"K|T Esc (no text)", L"\x1b[27u", K | T, true, VK_ESCAPE, 1, 0, 0 },
};
}
@@ -251,8 +252,26 @@ extern "C" HRESULT __declspec(dllexport) __cdecl KittyKeyTestDataSource(IDataSou
class KittyKeyboardProtocolTests
{
TestHook::LayoutGuard layout;
TEST_CLASS(KittyKeyboardProtocolTests);
TEST_CLASS_SETUP(ClassSetup)
{
layout = TestHook::SetTerminalInputKeyboardLayout(L"0001040c"); // French (Standard, AZERTY)
if (!layout)
{
Log::Result(TestResults::Result::Skipped);
}
return true;
}
TEST_CLASS_CLEANUP(ClassCleanup)
{
layout = {};
return true;
}
TEST_METHOD(KeyPressTests)
{
BEGIN_TEST_METHOD_PROPERTIES()
@@ -278,25 +297,25 @@ class KittyKeyboardProtocolTests
TEST_METHOD(KeyRepeatEvents)
{
auto input = createInput(E | K);
VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"\x1b[97u"), process(input, true, 'A', 0x1E, L'a', 0));
VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"\x1b[97;1:2u"), process(input, true, 'A', 0x1E, L'a', 0)); // repeat
VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"\x1b[97;1:2u"), process(input, true, 'A', 0x1E, L'a', 0)); // repeat
VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"\x1b[97;1:3u"), process(input, false, 'A', 0x1E, L'a', 0)); // release
VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"\x1b[97u"), process(input, true, 'A', 0x1E, L'a', 0)); // new press
VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"\x1b[97u"), process(input, true, 'A', 0x10, L'a', 0));
VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"\x1b[97;1:2u"), process(input, true, 'A', 0x10, L'a', 0)); // repeat
VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"\x1b[97;1:2u"), process(input, true, 'A', 0x10, L'a', 0)); // repeat
VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"\x1b[97;1:3u"), process(input, false, 'A', 0x10, L'a', 0)); // release
VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"\x1b[97u"), process(input, true, 'A', 0x10, L'a', 0)); // new press
}
TEST_METHOD(KeyRepeatWithModifiers)
{
auto input = createInput(E | K);
VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"\x1b[97;2u"), process(input, true, 'A', 0x1E, L'A', SHIFT_PRESSED));
VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"\x1b[97;2:2u"), process(input, true, 'A', 0x1E, L'A', SHIFT_PRESSED));
VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"\x1b[97;2u"), process(input, true, 'A', 0x10, L'A', SHIFT_PRESSED));
VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"\x1b[97;2:2u"), process(input, true, 'A', 0x10, L'A', SHIFT_PRESSED));
}
TEST_METHOD(KeyRepeatResetOnDifferentKey)
{
auto input = createInput(E | K);
VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"\x1b[97u"), process(input, true, 'A', 0x1E, L'a', 0));
VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"\x1b[97u"), process(input, true, 'A', 0x10, L'a', 0));
VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"\x1b[98u"), process(input, true, 'B', 0x30, L'b', 0)); // different key
VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"\x1b[97u"), process(input, true, 'A', 0x1E, L'a', 0)); // not repeat
VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"\x1b[97u"), process(input, true, 'A', 0x10, L'a', 0)); // not repeat
}
};

View File

@@ -0,0 +1,14 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "precomp.h"
// This default no-op implementation lives in its own .obj so that the linker
// can skip it when a test DLL supplies its own definition. The classic linking
// model only pulls in .obj files from a .lib if they resolve an otherwise
// unresolved symbol - and nothing else in the test DLL refers to this file.
// See: https://devblogs.microsoft.com/oldnewthing/20250416-00/?p=111077
extern "C" HKL TestHook_TerminalInput_KeyboardLayout()
{
return nullptr;
}

View File

@@ -13,6 +13,7 @@
<ItemGroup>
<ClCompile Include="..\mouseInput.cpp" />
<ClCompile Include="..\terminalInput.cpp" />
<ClCompile Include="..\TestHook.cpp" />
<ClCompile Include="..\precomp.cpp">
<PrecompiledHeader>Create</PrecompiledHeader>
</ClCompile>

View File

@@ -521,6 +521,6 @@ TerminalInput::OutputType TerminalInput::_makeAlternateScrollOutput(const unsign
_encodeRegular(enc, key);
std::wstring str;
_formatEncodingHelper(enc, str);
_formatEncodingHelper(enc, key, str);
return str;
}

View File

@@ -30,6 +30,7 @@ PRECOMPILED_INCLUDE = ..\precomp.h
SOURCES= \
..\terminalInput.cpp \
..\mouseInput.cpp \
..\TestHook.cpp \
INCLUDES = \
$(INCLUDES); \

View File

@@ -277,21 +277,48 @@ TerminalInput::OutputType TerminalInput::HandleKey(const INPUT_RECORD& event)
}
// Keep track of key repeats.
key.keyRepeat = _lastVirtualKeyCode == key.virtualKey;
//
// For modifier keys:
// * Map the vkey to a dwControlKeyState flag
// (_controlKeyStateFromVirtualKey returns 0 for non-modifier keys)
// * Checking whether the flag was already set previously
// For standard keys:
// * Simply check if the last vkey equals the current one
//
// This split helps with international keyboard layouts that use the KLLF_ALTGR flag.
// Those generate interleaved LEFT_CTRL_PRESSED and RIGHT_ALT_PRESSED events,
// which a single _lastVirtualKeyCode field will fail to track.
if (key.keyDown)
{
_lastVirtualKeyCode = key.virtualKey;
if (const auto flags = _controlKeyStateFromVirtualKey(key.virtualKey, key.controlKeyState))
{
key.keyRepeat = (_previousControlKeyState & flags) != 0;
}
else
{
key.keyRepeat = _lastVirtualKeyCode == key.virtualKey;
_lastVirtualKeyCode = key.virtualKey;
}
}
else if (key.keyRepeat)
else
{
_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 (key.keyRepeat && !_inputMode.test(Mode::AutoRepeat))
if (key.keyRepeat)
{
return _makeNoOutput();
if (
// Suppress modifier key events at all times - they aren't reported in any protocol.
(key.virtualKey >= VK_SHIFT && key.virtualKey <= VK_MENU) ||
(key.virtualKey >= VK_LSHIFT && key.virtualKey <= VK_RMENU) ||
(_kittyFlags != 0 ?
// If KKP is enabled, we only report repeats if ReportEventTypes is enabled.
WI_IsFlagClear(_kittyFlags, KittyKeyboardProtocolFlags::ReportEventTypes) :
// Otherwise, it depends on the classic auto-repeat mode setting.
!_inputMode.test(Mode::AutoRepeat)))
{
return _makeNoOutput();
}
}
// There's a bunch of early returns we can place on key-up events,
@@ -331,19 +358,42 @@ TerminalInput::OutputType TerminalInput::HandleKey(const INPUT_RECORD& event)
// be able to detect when the Ctrl key isn't genuine. We do so by tracking
// the time between the Alt and Ctrl key presses, and only consider the Ctrl
// key to really be pressed if the difference is more than 50ms.
key.leftCtrlIsReallyPressed = WI_IsFlagSet(key.controlKeyState, LEFT_CTRL_PRESSED);
auto leftCtrlIsReallyPressed = false;
if (WI_AreAllFlagsSet(key.controlKeyState, LEFT_CTRL_PRESSED | RIGHT_ALT_PRESSED))
{
const auto max = std::max(_lastLeftCtrlTime, _lastRightAltTime);
const auto min = std::min(_lastLeftCtrlTime, _lastRightAltTime);
key.leftCtrlIsReallyPressed = (max - min) > 50;
leftCtrlIsReallyPressed = (max - min) > 50;
}
const auto anyCtrlPressed = WI_IsAnyFlagSet(key.controlKeyState, CTRL_PRESSED);
const auto bothCtrlPressed = WI_AreAllFlagsSet(key.controlKeyState, CTRL_PRESSED);
const auto anyAltPressed = WI_IsAnyFlagSet(key.controlKeyState, ALT_PRESSED);
const auto bothAltPressed = WI_AreAllFlagsSet(key.controlKeyState, ALT_PRESSED);
// We distinguish AltGr+Key / Ctrl+Alt+Key combinations on international keyboard layouts from
// genuine, intentional Ctrl+Alt+Key combinations by checking whether the codepoint is valid.
// Windows should not send a valid codepoint for e.g. Ctrl+Alt+Q on a US ANSI layout,
// so we treat it as a genuine Ctrl+Alt+Q.
//
// However, this isn't universally true and more of a heuristic. Ctrl+Alt+Esc
// for instance results in codepoint=0x1b! As such we restrict to graphical codepoints.
// This should not be considered "Reference Windows Code". It's a personal best guess.
key.altGrPressed = anyAltPressed && anyCtrlPressed && (key.codepoint > 0x20 && key.codepoint != 0x7f);
// Ctrl is a bit tricky to detect, since international keyboards with KLLF_ALTGR will
// send Left-Ctrl + Right-Alt. If both Ctrl keys are pressed it's unambiguous.
// Otherwise, if we haven't guessed this to be an AltGr key, then we can safely
// assume this to be a Ctrl combination as well. Otherwise, we also have our
// timing logic above to guess if the Left-Ctrl key was pressed by a human.
key.ctrlPressed = bothCtrlPressed || (anyCtrlPressed && !key.altGrPressed) || leftCtrlIsReallyPressed;
// Alt is a bit simpler than Ctrl and follows the same pattern.
key.altPressed = bothAltPressed || (anyAltPressed && !key.altGrPressed);
key.shiftPressed = WI_IsFlagSet(key.controlKeyState, SHIFT_PRESSED);
KeyboardHelper kbd;
EncodingHelper enc;
WI_SetFlagIf(enc.csiModifier, CSI_CTRL, key.leftCtrlIsReallyPressed || WI_IsFlagSet(key.controlKeyState, RIGHT_CTRL_PRESSED));
WI_SetFlagIf(enc.csiModifier, CSI_ALT, WI_IsAnyFlagSet(key.controlKeyState, ALT_PRESSED));
WI_SetFlagIf(enc.csiModifier, CSI_SHIFT, WI_IsFlagSet(key.controlKeyState, SHIFT_PRESSED));
WI_SetFlagIf(enc.csiModifier, CSI_CTRL, key.ctrlPressed);
WI_SetFlagIf(enc.csiModifier, CSI_ALT, key.altPressed);
WI_SetFlagIf(enc.csiModifier, CSI_SHIFT, key.shiftPressed);
if (_kittyFlags == 0 || !_encodeKitty(kbd, enc, key))
{
@@ -351,9 +401,9 @@ TerminalInput::OutputType TerminalInput::HandleKey(const INPUT_RECORD& event)
}
std::wstring seq;
if (!_formatEncodingHelper(enc, seq))
if (!_formatEncodingHelper(enc, key, seq))
{
_formatFallback(kbd, enc, key, seq);
_formatFallback(kbd, key, seq);
}
return seq;
}
@@ -390,6 +440,8 @@ void TerminalInput::_initKeyboardMap() noexcept
DWORD TerminalInput::_trackControlKeyState(const KEY_EVENT_RECORD& key) noexcept
{
_previousControlKeyState = _lastControlKeyState;
// First record which key state bits were previously off but are now on.
const auto pressedKeyState = ~_lastControlKeyState & key.dwControlKeyState;
// Then save the new key state so we can determine future state changes.
@@ -402,23 +454,37 @@ DWORD TerminalInput::_trackControlKeyState(const KEY_EVENT_RECORD& key) noexcept
// can be misinterpreted as an Alt+AltGr key combination.
const auto rightAltDown = key.bKeyDown && key.wVirtualKeyCode == VK_MENU && WI_IsFlagSet(key.dwControlKeyState, ENHANCED_KEY);
WI_ClearFlagIf(_lastControlKeyState, RIGHT_ALT_PRESSED, WI_IsFlagSet(pressedKeyState, RIGHT_ALT_PRESSED) && !rightAltDown);
// We also take this opportunity to record the time at which the LeftCtrl
// and RightAlt keys are pressed. This is needed to determine whether the
// Ctrl key was pressed by the user, or fabricated by an AltGr key press.
if (key.bKeyDown)
{
if (WI_IsFlagSet(pressedKeyState, LEFT_CTRL_PRESSED))
{
_lastLeftCtrlTime = GetTickCount64();
}
if (WI_IsFlagSet(pressedKeyState, RIGHT_ALT_PRESSED))
{
_lastRightAltTime = GetTickCount64();
}
}
return _lastControlKeyState;
}
// Maps a modifier virtual key code to its corresponding dwControlKeyState flag.
// Returns 0 for non-modifier keys. For VK_CONTROL and VK_MENU, the ENHANCED_KEY
// bit in controlKeyState disambiguates left vs. right.
DWORD TerminalInput::_controlKeyStateFromVirtualKey(uint16_t vk, uint32_t controlKeyState) noexcept
{
switch (vk)
{
case VK_SHIFT:
case VK_LSHIFT:
case VK_RSHIFT:
return SHIFT_PRESSED;
case VK_CONTROL:
return WI_IsFlagSet(controlKeyState, ENHANCED_KEY) ? RIGHT_CTRL_PRESSED : LEFT_CTRL_PRESSED;
case VK_LCONTROL:
return LEFT_CTRL_PRESSED;
case VK_RCONTROL:
return RIGHT_CTRL_PRESSED;
case VK_MENU:
return WI_IsFlagSet(controlKeyState, ENHANCED_KEY) ? RIGHT_ALT_PRESSED : LEFT_ALT_PRESSED;
case VK_LMENU:
return LEFT_ALT_PRESSED;
case VK_RMENU:
return RIGHT_ALT_PRESSED;
default:
return 0;
}
}
uint32_t TerminalInput::_makeCtrlChar(const uint32_t ch) noexcept
{
if (ch >= L'@' && ch <= L'~')
@@ -623,7 +689,7 @@ bool TerminalInput::_encodeKitty(KeyboardHelper& kbd, EncodingHelper& enc, const
// KKP> Note that the shifted key must be present only if shift is also present in the modifiers.
if (isTextKey(functionalKeyCode) && enc.shiftPressed())
if (isTextKey(functionalKeyCode) && key.shiftPressed)
{
// This is almost identical to our computation of the "base key" for
// ReportAllKeysAsEscapeCodes above, but this time with SHIFT_PRESSED.
@@ -839,9 +905,9 @@ void TerminalInput::_encodeRegular(EncodingHelper& enc, const SanitizedKeyEvent&
// not standard, but a modern terminal convention). The Alt modifier adds
// an ESC prefix (also not standard).
enc.altPrefix = true;
const auto ctrl = (enc.csiModifier & CSI_CTRL) == 0;
const auto ctrl = key.ctrlPressed;
const auto back = _inputMode.test(Mode::BackarrowKey);
enc.plain = ctrl != back ? L"\x7f"sv : L"\b"sv;
enc.plain = ctrl == back ? L"\x7f"sv : L"\b"sv;
break;
}
case VK_TAB:
@@ -849,7 +915,7 @@ void TerminalInput::_encodeRegular(EncodingHelper& enc, const SanitizedKeyEvent&
// The Alt modifier adds an ESC prefix, although in practice all the Alt
// mappings are likely to be system hotkeys.
enc.altPrefix = true;
if ((enc.csiModifier & CSI_SHIFT) == 0)
if (!key.shiftPressed)
{
enc.plain = L"\t"sv;
}
@@ -879,7 +945,7 @@ void TerminalInput::_encodeRegular(EncodingHelper& enc, const SanitizedKeyEvent&
}
else
{
if ((enc.csiModifier & CSI_CTRL) == 0)
if (!key.ctrlPressed)
{
enc.plain = _inputMode.test(Mode::LineFeed) ? L"\r\n"sv : L"\r"sv;
}
@@ -1104,12 +1170,12 @@ void TerminalInput::_encodeRegular(EncodingHelper& enc, const SanitizedKeyEvent&
}
}
bool TerminalInput::_formatEncodingHelper(EncodingHelper& enc, std::wstring& seq) const
bool TerminalInput::_formatEncodingHelper(EncodingHelper& enc, const SanitizedKeyEvent& key, std::wstring& seq) const
{
// NOTE: altPrefix is only ever true for `_fillRegularKeyEncodingInfo` calls,
// and only if one of the 3 conditions below applies.
// In other words, we return with an unmodified `str` if `enc` is unmodified.
if (enc.altPrefix && enc.altPressed() && _inputMode.test(Mode::Ansi))
if (enc.altPrefix && key.altPressed && _inputMode.test(Mode::Ansi))
{
seq.push_back(L'\x1b');
}
@@ -1180,7 +1246,7 @@ bool TerminalInput::_formatEncodingHelper(EncodingHelper& enc, std::wstring& seq
return false;
}
void TerminalInput::_formatFallback(KeyboardHelper& kbd, const EncodingHelper& enc, const SanitizedKeyEvent& key, std::wstring& seq) const
void TerminalInput::_formatFallback(KeyboardHelper& kbd, const SanitizedKeyEvent& key, std::wstring& seq) const
{
// If this is a modifier, it won't produce output, so we can return early.
if (key.virtualKey >= VK_SHIFT && key.virtualKey <= VK_MENU)
@@ -1188,41 +1254,24 @@ void TerminalInput::_formatFallback(KeyboardHelper& kbd, const EncodingHelper& e
return;
}
const auto anyAltPressed = key.anyAltPressed();
auto codepoint = key.codepoint;
// 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 = enc.ctrlPressed() && key.virtualKey == VK_SPACE;
const auto ctrlSpaceKey = key.ctrlPressed && key.virtualKey == VK_SPACE;
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 altGrPressed = key.altGrPressed();
const auto bothAltPressed = key.bothAltPressed();
const auto bothCtrlPressed = key.bothCtrlPressed();
const auto rightAltPressed = key.rightAltPressed();
if (altGrPressed && (bothCtrlPressed || (rightAltPressed && key.leftCtrlIsReallyPressed)))
if (key.ctrlPressed)
{
codepoint = _makeCtrlChar(codepoint);
}
// 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 wantsEscPrefix = altGrPressed ? bothAltPressed : anyAltPressed;
if (wantsEscPrefix && _inputMode.test(Mode::Ansi))
{
seq.push_back(L'\x1b');
}
}
// If we don't have a UnicodeChar, we'll try and determine what the key
// would have transmitted without any Ctrl or Alt modifiers applied. But
// this only makes sense if there were actually modifiers pressed.
else if (anyAltPressed || WI_IsAnyFlagSet(key.controlKeyState, CTRL_PRESSED))
else if (key.altPressed || key.ctrlPressed)
{
// IMPORTANT NOTE: This implicitly, reliably rejects dead keys for us (good!).
//
@@ -1238,14 +1287,8 @@ void TerminalInput::_formatFallback(KeyboardHelper& kbd, const EncodingHelper& e
return;
}
// If Alt is pressed, that also needs to be applied to the sequence.
if (anyAltPressed && _inputMode.test(Mode::Ansi))
{
seq.push_back(L'\x1b');
}
// Once we've got the base character, we can apply the Ctrl modifier.
if (enc.ctrlPressed())
if (key.ctrlPressed)
{
codepoint = _makeCtrlChar(codepoint);
// If we haven't found a Ctrl mapping for the key, and it's one of
@@ -1263,6 +1306,12 @@ void TerminalInput::_formatFallback(KeyboardHelper& kbd, const EncodingHelper& e
return;
}
// If Alt is pressed, that also needs to be applied to the sequence.
if (key.altPressed && _inputMode.test(Mode::Ansi))
{
seq.push_back(L'\x1b');
}
_stringPushCodepoint(seq, codepoint);
}
@@ -1311,9 +1360,8 @@ TerminalInput::CodepointBuffer::CodepointBuffer(uint32_t cp) noexcept
void TerminalInput::CodepointBuffer::convertLowercase() noexcept
{
// NOTE: MSDN states that `lpSrcStr == lpDestStr` is valid for LCMAP_LOWERCASE.
len = LCMapStringW(LOCALE_INVARIANT, LCMAP_LOWERCASE, &buf[0], len, &buf[0], ARRAYSIZE(buf));
// NOTE: LCMapStringW returns the length including the null terminator.
len -= 1;
// NOTE: LCMapStringEx does not null-terminate the output if there's insufficient space. As such we subtract 1 from the buf size.
len = LCMapStringEx(LOCALE_NAME_INVARIANT, LCMAP_LOWERCASE, &buf[0], len, &buf[0], ARRAYSIZE(buf) - 1, nullptr, nullptr, 0);
}
uint32_t TerminalInput::CodepointBuffer::asSingleCodepoint() const noexcept
@@ -1335,49 +1383,33 @@ uint32_t TerminalInput::CodepointBuffer::asSingleCodepoint() const noexcept
return InvalidCodepoint;
}
bool TerminalInput::SanitizedKeyEvent::anyAltPressed() const noexcept
{
return WI_IsAnyFlagSet(controlKeyState, ALT_PRESSED);
}
bool TerminalInput::SanitizedKeyEvent::bothAltPressed() const noexcept
{
return WI_AreAllFlagsSet(controlKeyState, ALT_PRESSED);
}
bool TerminalInput::SanitizedKeyEvent::rightAltPressed() const noexcept
{
return WI_IsFlagSet(controlKeyState, RIGHT_ALT_PRESSED);
}
bool TerminalInput::SanitizedKeyEvent::bothCtrlPressed() const noexcept
{
return WI_AreAllFlagsSet(controlKeyState, CTRL_PRESSED);
}
bool TerminalInput::SanitizedKeyEvent::altGrPressed() const noexcept
{
return WI_IsAnyFlagSet(controlKeyState, ALT_PRESSED) && WI_IsAnyFlagSet(controlKeyState, CTRL_PRESSED);
}
uint32_t TerminalInput::KeyboardHelper::getUnmodifiedKeyboardKey(const SanitizedKeyEvent& key) noexcept
{
const auto virtualKey = key.virtualKey;
const auto controlKeyState = key.controlKeyState & ~(ALT_PRESSED | CTRL_PRESSED);
return getKeyboardKey(virtualKey, controlKeyState, nullptr);
return getKeyboardKeyHelper(key, ALT_PRESSED | CTRL_PRESSED, 0);
}
uint32_t TerminalInput::KeyboardHelper::getKittyBaseKey(const SanitizedKeyEvent& key) noexcept
{
const auto virtualKey = key.virtualKey;
const auto controlKeyState = key.controlKeyState & ~(ALT_PRESSED | CTRL_PRESSED | SHIFT_PRESSED | CAPSLOCK_ON);
return _codepointToLower(getKeyboardKey(virtualKey, controlKeyState, nullptr));
return _codepointToLower(getKeyboardKeyHelper(key, ALT_PRESSED | CTRL_PRESSED | SHIFT_PRESSED | CAPSLOCK_ON, 0));
}
uint32_t TerminalInput::KeyboardHelper::getKittyShiftedKey(const SanitizedKeyEvent& key) noexcept
{
return getKeyboardKeyHelper(key, ALT_PRESSED | CTRL_PRESSED | CAPSLOCK_ON, SHIFT_PRESSED);
}
uint32_t TerminalInput::KeyboardHelper::getKeyboardKeyHelper(const SanitizedKeyEvent& key, DWORD removeFlags, DWORD addFlags) noexcept
{
const auto virtualKey = key.virtualKey;
const auto controlKeyState = key.controlKeyState & ~(ALT_PRESSED | CTRL_PRESSED | CAPSLOCK_ON) | SHIFT_PRESSED;
auto controlKeyState = (key.controlKeyState & ~removeFlags) | addFlags;
// In the context of KKP, AltGr acts more like a keyboard "layer" toggle.
// It's not a modifier that's ever transmitted as-is and instead modifies the actual base key code.
if (key.altGrPressed)
{
controlKeyState |= LEFT_CTRL_PRESSED | RIGHT_ALT_PRESSED;
}
return getKeyboardKey(virtualKey, controlKeyState, nullptr);
}
@@ -1465,9 +1497,21 @@ void TerminalInput::KeyboardHelper::init() noexcept
}
}
// The default no-op implementation lives in TestHook.cpp (its own .obj) so the
// linker can skip it when a test DLL supplies its own definition.
extern "C" HKL TestHook_TerminalInput_KeyboardLayout();
void TerminalInput::KeyboardHelper::initSlow() noexcept
{
_keyboardLayout = GetKeyboardLayout(GetWindowThreadProcessId(GetForegroundWindow(), nullptr));
if (const auto hkl = TestHook_TerminalInput_KeyboardLayout())
{
_keyboardLayout = hkl;
}
else
{
_keyboardLayout = GetKeyboardLayout(GetWindowThreadProcessId(GetForegroundWindow(), nullptr));
}
memset(&_keyboardState[0], 0, sizeof(_keyboardState));
_initialized = true;
}
@@ -1476,18 +1520,3 @@ TerminalInput::EncodingHelper::EncodingHelper() noexcept
{
memset(this, 0, sizeof(*this));
}
bool TerminalInput::EncodingHelper::shiftPressed() const noexcept
{
return csiModifier & CSI_SHIFT;
}
bool TerminalInput::EncodingHelper::altPressed() const noexcept
{
return csiModifier & CSI_ALT;
}
bool TerminalInput::EncodingHelper::ctrlPressed() const noexcept
{
return csiModifier & CSI_CTRL;
}

View File

@@ -109,15 +109,12 @@ namespace Microsoft::Console::VirtualTerminal
uint16_t scanCode = 0;
uint32_t codepoint = 0;
uint32_t controlKeyState = 0;
bool leftCtrlIsReallyPressed = false;
bool keyDown = false;
bool keyRepeat = false;
bool anyAltPressed() const noexcept;
bool bothAltPressed() const noexcept;
bool rightAltPressed() const noexcept;
bool bothCtrlPressed() const noexcept;
bool altGrPressed() const noexcept;
bool altGrPressed = false;
bool ctrlPressed = false;
bool altPressed = false;
bool shiftPressed = false;
};
struct KeyboardHelper
@@ -132,6 +129,7 @@ namespace Microsoft::Console::VirtualTerminal
private:
uint32_t getKeyboardKey(UINT vkey, DWORD controlKeyState, HKL hkl) noexcept;
uint32_t getKeyboardKeyHelper(const SanitizedKeyEvent& key, DWORD removeFlags, DWORD addFlags) noexcept;
void init() noexcept;
void initSlow() noexcept;
@@ -145,11 +143,6 @@ namespace Microsoft::Console::VirtualTerminal
struct EncodingHelper
{
explicit EncodingHelper() noexcept;
bool shiftPressed() const noexcept;
bool altPressed() const noexcept;
bool ctrlPressed() const noexcept;
// The KKP CSI u sequence is a superset of other CSI sequences:
// CSI unicode-key-code:alternate-key-code-shift:alternate-key-code-base ; modifiers:event-type ; text-as-codepoint u
uint32_t csiUnicodeKeyCode;
@@ -180,6 +173,7 @@ namespace Microsoft::Console::VirtualTerminal
std::optional<WORD> _lastVirtualKeyCode;
DWORD _lastControlKeyState = 0;
DWORD _previousControlKeyState = 0;
uint64_t _lastLeftCtrlTime = 0;
uint64_t _lastRightAltTime = 0;
@@ -201,6 +195,7 @@ namespace Microsoft::Console::VirtualTerminal
void _initKeyboardMap() noexcept;
DWORD _trackControlKeyState(const KEY_EVENT_RECORD& key) noexcept;
[[nodiscard]] static DWORD _controlKeyStateFromVirtualKey(uint16_t vk, uint32_t controlKeyState) noexcept;
[[nodiscard]] static uint32_t _makeCtrlChar(uint32_t ch) noexcept;
[[nodiscard]] static StringType _makeCharOutput(uint32_t ch);
[[nodiscard]] static StringType _makeNoOutput() noexcept;
@@ -208,8 +203,8 @@ namespace Microsoft::Console::VirtualTerminal
bool _encodeKitty(KeyboardHelper& kbd, EncodingHelper& enc, const SanitizedKeyEvent& key) noexcept;
static uint32_t _getKittyFunctionalKeyCode(UINT vkey, WORD scanCode, bool enhanced) noexcept;
void _encodeRegular(EncodingHelper& enc, const SanitizedKeyEvent& key) const noexcept;
bool _formatEncodingHelper(EncodingHelper& enc, std::wstring& str) const;
void _formatFallback(KeyboardHelper& kbd, const EncodingHelper& enc, const SanitizedKeyEvent& key, std::wstring& seq) const;
bool _formatEncodingHelper(EncodingHelper& enc, const SanitizedKeyEvent& key, std::wstring& str) const;
void _formatFallback(KeyboardHelper& kbd, const SanitizedKeyEvent& key, std::wstring& seq) const;
static void _stringPushCodepoint(std::wstring& str, uint32_t cp);
static uint32_t _codepointToLower(uint32_t cp) noexcept;