diff --git a/.github/actions/spelling/expect/expect.txt b/.github/actions/spelling/expect/expect.txt
index 6c226753b98..f6ebb617f1a 100644
--- a/.github/actions/spelling/expect/expect.txt
+++ b/.github/actions/spelling/expect/expect.txt
@@ -29,6 +29,7 @@ allocing
alpc
ALTERNATENAME
ALTF
+ALTGR
ALTNUMPAD
ALWAYSTIP
ansicpg
@@ -79,6 +80,7 @@ autoscrolling
Autowrap
AVerify
awch
+AZERTY
azurecr
AZZ
backgrounded
@@ -867,6 +869,8 @@ kinda
KIYEOK
KKP
KLF
+klid
+KLLF
KLMNO
KOK
KPRIORITY
@@ -1133,6 +1137,7 @@ NOSELECTION
NOSENDCHANGING
NOSIZE
NOSNAPSHOT
+NOTELLSHELL
NOTHOUSANDS
NOTICKS
NOTIMEOUTIFNOTHUNG
@@ -1984,8 +1989,8 @@ WRITECONSOLEINPUT
WRITECONSOLEOUTPUT
WRITECONSOLEOUTPUTSTRING
wrkstr
-wrl
WRL
+wrl
wrp
WRunoff
WSLENV
diff --git a/src/terminal/adapter/ut_adapter/Adapter.UnitTests.vcxproj b/src/terminal/adapter/ut_adapter/Adapter.UnitTests.vcxproj
index 69bb1147c25..5e86b478de1 100644
--- a/src/terminal/adapter/ut_adapter/Adapter.UnitTests.vcxproj
+++ b/src/terminal/adapter/ut_adapter/Adapter.UnitTests.vcxproj
@@ -15,12 +15,14 @@
+
Create
+
diff --git a/src/terminal/adapter/ut_adapter/Adapter.UnitTests.vcxproj.filters b/src/terminal/adapter/ut_adapter/Adapter.UnitTests.vcxproj.filters
index 00713d03220..a0f54f93a0b 100644
--- a/src/terminal/adapter/ut_adapter/Adapter.UnitTests.vcxproj.filters
+++ b/src/terminal/adapter/ut_adapter/Adapter.UnitTests.vcxproj.filters
@@ -30,11 +30,17 @@
Source Files
+
+ Source Files
+
Header Files
+
+ Header Files
+
diff --git a/src/terminal/adapter/ut_adapter/TestHook.cpp b/src/terminal/adapter/ut_adapter/TestHook.cpp
new file mode 100644
index 00000000000..50f6fe19569
--- /dev/null
+++ b/src/terminal/adapter/ut_adapter/TestHook.cpp
@@ -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(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 };
+}
diff --git a/src/terminal/adapter/ut_adapter/TestHook.h b/src/terminal/adapter/ut_adapter/TestHook.h
new file mode 100644
index 00000000000..f7e14e25a30
--- /dev/null
+++ b/src/terminal/adapter/ut_adapter/TestHook.h
@@ -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);
+}
diff --git a/src/terminal/adapter/ut_adapter/inputTest.cpp b/src/terminal/adapter/ut_adapter/inputTest.cpp
index d672004cd12..cb221906bf7 100644
--- a/src/terminal/adapter/ut_adapter/inputTest.cpp
+++ b/src/terminal/adapter/ut_adapter/inputTest.cpp
@@ -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(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(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(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;
diff --git a/src/terminal/adapter/ut_adapter/kittyKeyboardProtocol.cpp b/src/terminal/adapter/ut_adapter/kittyKeyboardProtocol.cpp
index 4a46f8d0aac..44f54187016 100644
--- a/src/terminal/adapter/ut_adapter/kittyKeyboardProtocol.cpp
+++ b/src/terminal/adapter/ut_adapter/kittyKeyboardProtocol.cpp
@@ -6,6 +6,7 @@
#include
#include
+#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
}
};
diff --git a/src/terminal/input/TestHook.cpp b/src/terminal/input/TestHook.cpp
new file mode 100644
index 00000000000..01acd44aad4
--- /dev/null
+++ b/src/terminal/input/TestHook.cpp
@@ -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;
+}
diff --git a/src/terminal/input/lib/terminalinput.vcxproj b/src/terminal/input/lib/terminalinput.vcxproj
index 2db4e9fd5c2..cb8a2b57740 100644
--- a/src/terminal/input/lib/terminalinput.vcxproj
+++ b/src/terminal/input/lib/terminalinput.vcxproj
@@ -13,6 +13,7 @@
+
Create
diff --git a/src/terminal/input/mouseInput.cpp b/src/terminal/input/mouseInput.cpp
index 99ba22eccba..3e8116cdeda 100644
--- a/src/terminal/input/mouseInput.cpp
+++ b/src/terminal/input/mouseInput.cpp
@@ -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;
}
diff --git a/src/terminal/input/sources.inc b/src/terminal/input/sources.inc
index cf6c95dd2c5..19de4e70dd3 100644
--- a/src/terminal/input/sources.inc
+++ b/src/terminal/input/sources.inc
@@ -30,6 +30,7 @@ PRECOMPILED_INCLUDE = ..\precomp.h
SOURCES= \
..\terminalInput.cpp \
..\mouseInput.cpp \
+ ..\TestHook.cpp \
INCLUDES = \
$(INCLUDES); \
diff --git a/src/terminal/input/terminalInput.cpp b/src/terminal/input/terminalInput.cpp
index 2c2401cd804..4d7b57e41bd 100644
--- a/src/terminal/input/terminalInput.cpp
+++ b/src/terminal/input/terminalInput.cpp
@@ -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,21 +454,35 @@ 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)
+ 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)
{
- if (WI_IsFlagSet(pressedKeyState, LEFT_CTRL_PRESSED))
- {
- _lastLeftCtrlTime = GetTickCount64();
- }
- if (WI_IsFlagSet(pressedKeyState, RIGHT_ALT_PRESSED))
- {
- _lastRightAltTime = GetTickCount64();
- }
+ 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;
}
- return _lastControlKeyState;
}
uint32_t TerminalInput::_makeCtrlChar(const uint32_t ch) noexcept
@@ -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
+uint32_t TerminalInput::KeyboardHelper::getUnmodifiedKeyboardKey(const SanitizedKeyEvent& key) noexcept
{
- return WI_AreAllFlagsSet(controlKeyState, ALT_PRESSED);
+ return getKeyboardKeyHelper(key, ALT_PRESSED | CTRL_PRESSED, 0);
}
-bool TerminalInput::SanitizedKeyEvent::rightAltPressed() const noexcept
+uint32_t TerminalInput::KeyboardHelper::getKittyBaseKey(const SanitizedKeyEvent& key) noexcept
{
- return WI_IsFlagSet(controlKeyState, RIGHT_ALT_PRESSED);
+ return _codepointToLower(getKeyboardKeyHelper(key, ALT_PRESSED | CTRL_PRESSED | SHIFT_PRESSED | CAPSLOCK_ON, 0));
}
-bool TerminalInput::SanitizedKeyEvent::bothCtrlPressed() const noexcept
+uint32_t TerminalInput::KeyboardHelper::getKittyShiftedKey(const SanitizedKeyEvent& key) noexcept
{
- return WI_AreAllFlagsSet(controlKeyState, CTRL_PRESSED);
+ return getKeyboardKeyHelper(key, ALT_PRESSED | CTRL_PRESSED | CAPSLOCK_ON, SHIFT_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
+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);
- return getKeyboardKey(virtualKey, controlKeyState, nullptr);
-}
+ auto controlKeyState = (key.controlKeyState & ~removeFlags) | addFlags;
-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));
-}
+ // 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;
+ }
-uint32_t TerminalInput::KeyboardHelper::getKittyShiftedKey(const SanitizedKeyEvent& key) noexcept
-{
- const auto virtualKey = key.virtualKey;
- const auto controlKeyState = key.controlKeyState & ~(ALT_PRESSED | CTRL_PRESSED | CAPSLOCK_ON) | SHIFT_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;
-}
diff --git a/src/terminal/input/terminalInput.hpp b/src/terminal/input/terminalInput.hpp
index 80c4be509f8..f9a2eff8f2e 100644
--- a/src/terminal/input/terminalInput.hpp
+++ b/src/terminal/input/terminalInput.hpp
@@ -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 _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;