From 06c5cc2d8ca783c9999652f4d77187aa4383397d Mon Sep 17 00:00:00 2001 From: Jesper Wilfing Date: Fri, 28 Aug 2026 18:33:06 +0200 Subject: [PATCH] Preserve ENHANCED_KEY for direct key events (Alt up, F7 down) Key events that reach us via WindowEmperor::_dispatchSpecialKey (F7 down, Alt up) were forwarded through IDirectKeyListener::OnDirectKeyEvent without the extended-key bit from lParam. TermControl therefore never set ENHANCED_KEY on these events, so releasing e.g. RightAlt produced an INPUT_RECORD that is indistinguishable from a LeftAlt release. Thread an "extended" flag through the OnDirectKeyEvent chain and set EnhancedKey in TermControl, matching what the regular _KeyHandler path derives from CorePhysicalKeyStatus.IsExtendedKey. Adds a TerminalCore unit test verifying that win32-input-mode key events retain ENHANCED_KEY on both press and release of RightAlt. Closes #18120 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01A69HPN9EZs1GJpgrYnjn2F --- src/cascadia/TerminalApp/CommandPalette.cpp | 2 +- src/cascadia/TerminalApp/CommandPalette.h | 2 +- .../TerminalApp/SuggestionsControl.cpp | 2 +- src/cascadia/TerminalApp/SuggestionsControl.h | 2 +- src/cascadia/TerminalApp/TerminalPage.cpp | 2 +- src/cascadia/TerminalApp/TerminalPage.h | 2 +- src/cascadia/TerminalApp/TerminalWindow.cpp | 6 ++--- src/cascadia/TerminalApp/TerminalWindow.h | 2 +- src/cascadia/TerminalControl/TermControl.cpp | 13 ++++++++-- src/cascadia/TerminalControl/TermControl.h | 2 +- src/cascadia/TerminalControl/TermControl.idl | 2 +- src/cascadia/TerminalCore/Terminal.hpp | 2 ++ src/cascadia/UIHelpers/IDirectKeyListener.idl | 2 +- .../UnitTests_TerminalCore/InputTest.cpp | 26 +++++++++++++++++++ src/cascadia/WindowsTerminal/AppHost.cpp | 4 +-- src/cascadia/WindowsTerminal/AppHost.h | 2 +- .../WindowsTerminal/WindowEmperor.cpp | 6 ++++- 17 files changed, 60 insertions(+), 19 deletions(-) diff --git a/src/cascadia/TerminalApp/CommandPalette.cpp b/src/cascadia/TerminalApp/CommandPalette.cpp index 2050bebb2ef..1284fdc2d46 100644 --- a/src/cascadia/TerminalApp/CommandPalette.cpp +++ b/src/cascadia/TerminalApp/CommandPalette.cpp @@ -385,7 +385,7 @@ namespace winrt::TerminalApp::implementation // - Implements the Alt handler // Return value: // - whether the key was handled - bool CommandPalette::OnDirectKeyEvent(const uint32_t vkey, const uint8_t /*scanCode*/, const bool down) + bool CommandPalette::OnDirectKeyEvent(const uint32_t vkey, const uint8_t /*scanCode*/, const bool /*extended*/, const bool down) { auto handled = false; if (_currentMode == CommandPaletteMode::TabSwitchMode) diff --git a/src/cascadia/TerminalApp/CommandPalette.h b/src/cascadia/TerminalApp/CommandPalette.h index 8e621b104dc..3c7d7b44b26 100644 --- a/src/cascadia/TerminalApp/CommandPalette.h +++ b/src/cascadia/TerminalApp/CommandPalette.h @@ -34,7 +34,7 @@ namespace winrt::TerminalApp::implementation void SetTabs(const Windows::Foundation::Collections::IObservableVector& tabs, const Windows::Foundation::Collections::IObservableVector& mruTabs); void SetActionMap(const Microsoft::Terminal::Settings::Model::IActionMapView& actionMap); - bool OnDirectKeyEvent(const uint32_t vkey, const uint8_t scanCode, const bool down); + bool OnDirectKeyEvent(const uint32_t vkey, const uint8_t scanCode, const bool extended, const bool down); void SelectNextItem(const bool moveDown); diff --git a/src/cascadia/TerminalApp/SuggestionsControl.cpp b/src/cascadia/TerminalApp/SuggestionsControl.cpp index 29f3215df63..86e5365b951 100644 --- a/src/cascadia/TerminalApp/SuggestionsControl.cpp +++ b/src/cascadia/TerminalApp/SuggestionsControl.cpp @@ -460,7 +460,7 @@ namespace winrt::TerminalApp::implementation // - Implements the Alt handler // Return value: // - whether the key was handled - bool SuggestionsControl::OnDirectKeyEvent(const uint32_t /*vkey*/, const uint8_t /*scanCode*/, const bool /*down*/) + bool SuggestionsControl::OnDirectKeyEvent(const uint32_t /*vkey*/, const uint8_t /*scanCode*/, const bool /*extended*/, const bool /*down*/) { auto handled = false; return handled; diff --git a/src/cascadia/TerminalApp/SuggestionsControl.h b/src/cascadia/TerminalApp/SuggestionsControl.h index 9ec46e2f3e7..37a1b9f2d89 100644 --- a/src/cascadia/TerminalApp/SuggestionsControl.h +++ b/src/cascadia/TerminalApp/SuggestionsControl.h @@ -25,7 +25,7 @@ namespace winrt::TerminalApp::implementation void SetCommands(const Windows::Foundation::Collections::IVector& actions); - bool OnDirectKeyEvent(const uint32_t vkey, const uint8_t scanCode, const bool down); + bool OnDirectKeyEvent(const uint32_t vkey, const uint8_t scanCode, const bool extended, const bool down); void SelectNextItem(const bool moveDown); diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index 7b7f771012d..d7a448089e5 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -1890,7 +1890,7 @@ namespace winrt::TerminalApp::implementation e.Handled(true); } - bool TerminalPage::OnDirectKeyEvent(const uint32_t vkey, const uint8_t scanCode, const bool down) + bool TerminalPage::OnDirectKeyEvent(const uint32_t vkey, const uint8_t scanCode, const bool /*extended*/, const bool down) { const auto modifiers = _GetPressedModifierKeys(); if (vkey == VK_SPACE && modifiers.IsAltPressed() && down) diff --git a/src/cascadia/TerminalApp/TerminalPage.h b/src/cascadia/TerminalApp/TerminalPage.h index a3e76cb6027..a840befb21c 100644 --- a/src/cascadia/TerminalApp/TerminalPage.h +++ b/src/cascadia/TerminalApp/TerminalPage.h @@ -222,7 +222,7 @@ namespace winrt::TerminalApp::implementation void WindowActivated(const bool activated); bool FocusTab(const winrt::TerminalApp::Tab& tab); - bool OnDirectKeyEvent(const uint32_t vkey, const uint8_t scanCode, const bool down); + bool OnDirectKeyEvent(const uint32_t vkey, const uint8_t scanCode, const bool extended, const bool down); void AttachContent(Windows::Foundation::Collections::IVector args, uint32_t tabIndex); void SendContentToOther(winrt::TerminalApp::RequestReceiveContentArgs args); diff --git a/src/cascadia/TerminalApp/TerminalWindow.cpp b/src/cascadia/TerminalApp/TerminalWindow.cpp index 4e90fccab03..b4884d9326a 100644 --- a/src/cascadia/TerminalApp/TerminalWindow.cpp +++ b/src/cascadia/TerminalApp/TerminalWindow.cpp @@ -910,7 +910,7 @@ namespace winrt::TerminalApp::implementation // - Implements the Alt handler (per GH#6421) // Return value: // - whether the key was handled - bool TerminalWindow::OnDirectKeyEvent(const uint32_t vkey, const uint8_t scanCode, const bool down) + bool TerminalWindow::OnDirectKeyEvent(const uint32_t vkey, const uint8_t scanCode, const bool extended, const bool down) { if (_root) { @@ -926,7 +926,7 @@ namespace winrt::TerminalApp::implementation { if (auto keyListener{ focusedObject.try_as() }) { - if (keyListener.OnDirectKeyEvent(vkey, scanCode, down)) + if (keyListener.OnDirectKeyEvent(vkey, scanCode, extended, down)) { return true; } @@ -954,7 +954,7 @@ namespace winrt::TerminalApp::implementation { if (auto keyListener{ _root.try_as() }) { - return keyListener.OnDirectKeyEvent(vkey, scanCode, down); + return keyListener.OnDirectKeyEvent(vkey, scanCode, extended, down); } } } diff --git a/src/cascadia/TerminalApp/TerminalWindow.h b/src/cascadia/TerminalApp/TerminalWindow.h index 52c390472a3..3ffe1003c70 100644 --- a/src/cascadia/TerminalApp/TerminalWindow.h +++ b/src/cascadia/TerminalApp/TerminalWindow.h @@ -119,7 +119,7 @@ namespace winrt::TerminalApp::implementation hstring Title(); void TitlebarClicked(); - bool OnDirectKeyEvent(const uint32_t vkey, const uint8_t scanCode, const bool down); + bool OnDirectKeyEvent(const uint32_t vkey, const uint8_t scanCode, const bool extended, const bool down); void CloseWindow(); void WindowVisibilityChanged(const bool showOrHide); diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index 1557b968660..00d77386409 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -1535,9 +1535,18 @@ namespace winrt::Microsoft::Terminal::Control::implementation // normally. Namely, the keys we're concerned with are F7 down and Alt up. // Return value: // - Whether the key was handled. - bool TermControl::OnDirectKeyEvent(const uint32_t vkey, const uint8_t scanCode, const bool down) + bool TermControl::OnDirectKeyEvent(const uint32_t vkey, const uint8_t scanCode, const bool extended, const bool down) { - const auto modifiers{ _GetPressedModifierKeys() }; + auto modifiers{ _GetPressedModifierKeys() }; + + // GH#18120: Direct key events bypass the regular _KeyHandler path, + // which gets the extended-key state from CorePhysicalKeyStatus. + // Without this, releasing e.g. RightAlt loses its ENHANCED_KEY flag. + if (extended) + { + modifiers |= ControlKeyStates::EnhancedKey; + } + return _KeyHandler(gsl::narrow_cast(vkey), gsl::narrow_cast(scanCode), modifiers, down); } diff --git a/src/cascadia/TerminalControl/TermControl.h b/src/cascadia/TerminalControl/TermControl.h index 4011c76fc22..8514bf19812 100644 --- a/src/cascadia/TerminalControl/TermControl.h +++ b/src/cascadia/TerminalControl/TermControl.h @@ -144,7 +144,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation bool SearchBoxEditInFocus() const; - bool OnDirectKeyEvent(const uint32_t vkey, const uint8_t scanCode, const bool down); + bool OnDirectKeyEvent(const uint32_t vkey, const uint8_t scanCode, const bool extended, const bool down); bool OnMouseWheel(const Windows::Foundation::Point location, const winrt::Microsoft::Terminal::Core::Point delta, const bool leftButtonDown, const bool midButtonDown, const bool rightButtonDown); diff --git a/src/cascadia/TerminalControl/TermControl.idl b/src/cascadia/TerminalControl/TermControl.idl index faf16be6c84..53234a460d6 100644 --- a/src/cascadia/TerminalControl/TermControl.idl +++ b/src/cascadia/TerminalControl/TermControl.idl @@ -16,7 +16,7 @@ namespace Microsoft.Terminal.Control // WinRT is a trip. [uuid("0ddf4edc-3fda-4dee-97ca-a417ee3dd510")] interface IDirectKeyListener { - Boolean OnDirectKeyEvent(UInt32 vkey, UInt8 scanCode, Boolean down); + Boolean OnDirectKeyEvent(UInt32 vkey, UInt8 scanCode, Boolean extended, Boolean down); } enum CursorDisplayState diff --git a/src/cascadia/TerminalCore/Terminal.hpp b/src/cascadia/TerminalCore/Terminal.hpp index 8f4ca627a2d..9a639d1fc0d 100644 --- a/src/cascadia/TerminalCore/Terminal.hpp +++ b/src/cascadia/TerminalCore/Terminal.hpp @@ -48,6 +48,7 @@ namespace TerminalCoreUnitTests class TerminalBufferTests; class TerminalApiTest; class ScrollTest; + class InputTest; }; #endif @@ -491,5 +492,6 @@ class Microsoft::Terminal::Core::Terminal final : friend class TerminalCoreUnitTests::TerminalBufferTests; friend class TerminalCoreUnitTests::TerminalApiTest; friend class TerminalCoreUnitTests::ScrollTest; + friend class TerminalCoreUnitTests::InputTest; #endif }; diff --git a/src/cascadia/UIHelpers/IDirectKeyListener.idl b/src/cascadia/UIHelpers/IDirectKeyListener.idl index b37c333f2b9..59fb942d6dc 100644 --- a/src/cascadia/UIHelpers/IDirectKeyListener.idl +++ b/src/cascadia/UIHelpers/IDirectKeyListener.idl @@ -5,6 +5,6 @@ namespace Microsoft.Terminal.UI { [uuid("0ddf4edc-3fda-4dee-97ca-a417ee3dd510")] interface IDirectKeyListener { - Boolean OnDirectKeyEvent(UInt32 vkey, UInt8 scanCode, Boolean down); + Boolean OnDirectKeyEvent(UInt32 vkey, UInt8 scanCode, Boolean extended, Boolean down); } } diff --git a/src/cascadia/UnitTests_TerminalCore/InputTest.cpp b/src/cascadia/UnitTests_TerminalCore/InputTest.cpp index 97a8b218905..4a8ca766fa0 100644 --- a/src/cascadia/UnitTests_TerminalCore/InputTest.cpp +++ b/src/cascadia/UnitTests_TerminalCore/InputTest.cpp @@ -30,6 +30,7 @@ namespace TerminalCoreUnitTests TEST_METHOD(AltShiftKey); TEST_METHOD(InvalidKeyEvent); + TEST_METHOD(Win32KeyEventsRetainEnhancedKey); Terminal term{ Terminal::TestDummyMarker{} }; }; @@ -52,4 +53,29 @@ namespace TerminalCoreUnitTests VERIFY_ARE_EQUAL(unhandled(), term.SendKeyEvent(0, 123, {}, true)); VERIFY_ARE_EQUAL(unhandled(), term.SendKeyEvent(255, 123, {}, true)); } + + void InputTest::Win32KeyEventsRetainEnhancedKey() + { + // Tests GH#18120 + // In win32-input-mode, both the press and the release of an extended + // key (e.g. RightAlt) must encode the ENHANCED_KEY flag (0x100) in the + // Cs parameter, so that the client can distinguish RightAlt from LeftAlt. + auto& input = term._getTerminalInput(); + input.SetInputMode(Microsoft::Console::VirtualTerminal::TerminalInput::Mode::Win32, true); + const auto restore = wil::scope_exit([&]() { + input.SetInputMode(Microsoft::Console::VirtualTerminal::TerminalInput::Mode::Win32, false); + }); + + // RightAlt down: VK_MENU (18), scanCode 0x38 (56), + // Cs = RIGHT_ALT_PRESSED | ENHANCED_KEY = 0x0001 | 0x0100 = 257 + const auto down = term.SendKeyEvent(VK_MENU, 0x38, ControlKeyStates::RightAltPressed | ControlKeyStates::EnhancedKey, true); + VERIFY_IS_TRUE(down.has_value()); + VERIFY_ARE_EQUAL(L"\x1b[18;56;0;1;257;1_", *down); + + // RightAlt up: the modifier itself is no longer pressed, + // but the key release must still carry ENHANCED_KEY = 0x0100 = 256. + const auto up = term.SendKeyEvent(VK_MENU, 0x38, ControlKeyStates::EnhancedKey, false); + VERIFY_IS_TRUE(up.has_value()); + VERIFY_ARE_EQUAL(L"\x1b[18;56;0;0;256;1_", *up); + } } diff --git a/src/cascadia/WindowsTerminal/AppHost.cpp b/src/cascadia/WindowsTerminal/AppHost.cpp index 7c6ab35675b..2ef1cece1fe 100644 --- a/src/cascadia/WindowsTerminal/AppHost.cpp +++ b/src/cascadia/WindowsTerminal/AppHost.cpp @@ -84,11 +84,11 @@ AppHost::AppHost(WindowEmperor* manager, const winrt::TerminalApp::AppLogic& log QueryPerformanceCounter(&_lastActivatedTime); } -bool AppHost::OnDirectKeyEvent(const uint32_t vkey, const uint8_t scanCode, const bool down) +bool AppHost::OnDirectKeyEvent(const uint32_t vkey, const uint8_t scanCode, const bool extended, const bool down) { if (_windowLogic) { - return _windowLogic.OnDirectKeyEvent(vkey, scanCode, down); + return _windowLogic.OnDirectKeyEvent(vkey, scanCode, extended, down); } return false; } diff --git a/src/cascadia/WindowsTerminal/AppHost.h b/src/cascadia/WindowsTerminal/AppHost.h index ece07e3ca56..f1edb17a42c 100644 --- a/src/cascadia/WindowsTerminal/AppHost.h +++ b/src/cascadia/WindowsTerminal/AppHost.h @@ -22,7 +22,7 @@ class AppHost : public std::enable_shared_from_this IslandWindow* GetWindow() const noexcept; winrt::TerminalApp::TerminalWindow Logic(); - bool OnDirectKeyEvent(uint32_t vkey, uint8_t scanCode, bool down); + bool OnDirectKeyEvent(uint32_t vkey, uint8_t scanCode, bool extended, bool down); void SetTaskbarProgress(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::Foundation::IInspectable& args); safe_void_coroutine HandleSummon(winrt::TerminalApp::SummonWindowBehavior args) const; void DispatchCommandline(winrt::TerminalApp::CommandlineArgs args); diff --git a/src/cascadia/WindowsTerminal/WindowEmperor.cpp b/src/cascadia/WindowsTerminal/WindowEmperor.cpp index 79a0ff1b871..af58ad953f6 100644 --- a/src/cascadia/WindowsTerminal/WindowEmperor.cpp +++ b/src/cascadia/WindowsTerminal/WindowEmperor.cpp @@ -781,8 +781,12 @@ void WindowEmperor::_dispatchSpecialKey(const MSG& msg) const const auto vkey = gsl::narrow_cast(msg.wParam); const auto scanCode = gsl::narrow_cast(msg.lParam >> 16); + // Bit 24 of lParam indicates an extended key (e.g. RightAlt/RightCtrl). + // We need to forward it, so that the key event can be translated to an + // INPUT_RECORD with the ENHANCED_KEY flag set (GH#18120). + const bool extended = (msg.lParam & 0x01000000) != 0; const bool keyDown = (msg.message & 1) == 0; - window->OnDirectKeyEvent(vkey, scanCode, keyDown); + window->OnDirectKeyEvent(vkey, scanCode, extended, keyDown); } void WindowEmperor::_dispatchCommandline(winrt::TerminalApp::CommandlineArgs args)