From b77752bb24ad06d8a1f53aa01bb19f37a02e4efc Mon Sep 17 00:00:00 2001 From: donno2048 Date: Mon, 8 Jun 2020 17:31:28 -0500 Subject: [PATCH] Add support for win32-input-mode to conhost, ConPTY, Terminal (#6309) Adds support for `win32-input-mode` to conhost, conpty, and the Windows Terminal. * The shared `terminalInput` class supports sending these sequences when a VT client application requests this mode. * ConPTY supports synthesizing `INPUT_RECORD`s from the input sent to it from a terminal * ConPTY requests this mode immediately on startup (if started with a new flag, `PSEUDOCONSOLE_WIN32_INPUT_MODE`) * The Terminal now supports sending this input as well, when conpty asks for it. Also adds a new ConPTY flag `PSEUDOCONSOLE_WIN32_INPUT_MODE` which requests this functionality from conpty, and the Terminal requests this by default. Also adds `experimental.input.forceVT` as a global setting to let a user opt-out of this behavior, if they don't want it / this ends up breaking horribly. ## Validation Steps Performed * played with this mode in vtpipeterm * played with this mode in Terminal * checked a bunch of scenarios, as outlined in a [comment] on #4999 [comment]: https://github.com/microsoft/terminal/issues/4999#issuecomment-628718631 References #4999: The megathread References #5887: The spec Closes #879 Closes #2865 Closes #530 Closes #3079 Closes #1119 Closes #1694 Closes #3608 Closes #4334 Closes #4446 --- .../PublicTerminalCore/HwndTerminal.cpp | 8 +- .../PublicTerminalCore/HwndTerminal.hpp | 6 +- .../TerminalApp/GlobalAppSettings.cpp | 5 +- src/cascadia/TerminalApp/GlobalAppSettings.h | 1 + .../TerminalConnection/ConptyConnection.cpp | 2 +- src/cascadia/TerminalControl/TermControl.cpp | 60 ++++-- src/cascadia/TerminalControl/TermControl.h | 4 +- src/cascadia/TerminalControl/TermControl.xaml | 1 + src/cascadia/TerminalCore/ITerminalApi.hpp | 1 + src/cascadia/TerminalCore/ITerminalInput.hpp | 2 +- src/cascadia/TerminalCore/Terminal.cpp | 21 ++- src/cascadia/TerminalCore/Terminal.hpp | 3 +- src/cascadia/TerminalCore/TerminalApi.cpp | 6 + .../TerminalCore/TerminalDispatch.cpp | 16 ++ .../TerminalCore/TerminalDispatch.hpp | 2 + .../TerminalSettings/IControlSettings.idl | 5 +- .../TerminalSettings/ICoreSettings.idl | 2 + .../TerminalSettings/terminalsettings.h | 5 +- .../UnitTests_TerminalCore/InputTest.cpp | 3 +- .../UnitTests_TerminalCore/MockTermSettings.h | 2 + .../WpfTerminalControl/NativeMethods.cs | 7 +- .../WpfTerminalControl/TerminalContainer.cs | 6 +- src/host/ConsoleArguments.cpp | 11 ++ src/host/ConsoleArguments.hpp | 3 + src/host/VtIo.cpp | 10 + src/host/VtIo.hpp | 1 + src/host/getset.cpp | 13 ++ src/host/getset.h | 1 + src/host/outputStream.cpp | 14 ++ src/host/outputStream.hpp | 1 + src/inc/conpty-static.h | 1 + src/renderer/vt/VtSequences.cpp | 14 ++ src/renderer/vt/state.cpp | 16 ++ src/renderer/vt/vtrenderer.hpp | 4 + src/terminal/adapter/DispatchTypes.hpp | 3 +- src/terminal/adapter/IInteractDispatch.hpp | 2 +- src/terminal/adapter/ITermDispatch.hpp | 1 + src/terminal/adapter/InteractDispatch.cpp | 15 +- src/terminal/adapter/InteractDispatch.hpp | 2 +- src/terminal/adapter/adaptDispatch.cpp | 24 +++ src/terminal/adapter/adaptDispatch.hpp | 1 + src/terminal/adapter/conGetSet.hpp | 1 + src/terminal/adapter/termDispatch.hpp | 1 + .../adapter/ut_adapter/adapterTest.cpp | 7 + src/terminal/input/terminalInput.cpp | 47 +++++ src/terminal/input/terminalInput.hpp | 12 +- .../parser/InputStateMachineEngine.cpp | 83 +++++++- .../parser/InputStateMachineEngine.hpp | 3 + .../parser/OutputStateMachineEngine.cpp | 1 + .../parser/OutputStateMachineEngine.hpp | 2 +- .../parser/ut_parser/InputEngineTest.cpp | 177 +++++++++++++++++- .../parser/ut_parser/OutputEngineTest.cpp | 11 ++ src/types/inc/IInputEvent.hpp | 30 +++ src/winconpty/winconpty.cpp | 4 +- src/winconpty/winconpty.h | 1 + 55 files changed, 619 insertions(+), 66 deletions(-) diff --git a/src/cascadia/PublicTerminalCore/HwndTerminal.cpp b/src/cascadia/PublicTerminalCore/HwndTerminal.cpp index 7dcc5c39..e5b3b423 100644 --- a/src/cascadia/PublicTerminalCore/HwndTerminal.cpp +++ b/src/cascadia/PublicTerminalCore/HwndTerminal.cpp @@ -557,11 +557,11 @@ catch (...) return false; } -void HwndTerminal::_SendKeyEvent(WORD vkey, WORD scanCode) noexcept +void HwndTerminal::_SendKeyEvent(WORD vkey, WORD scanCode, bool keyDown) noexcept try { const auto flags = getControlKeyState(); - _terminal->SendKeyEvent(vkey, scanCode, flags); + _terminal->SendKeyEvent(vkey, scanCode, flags, keyDown); } CATCH_LOG(); @@ -590,10 +590,10 @@ try } CATCH_LOG(); -void _stdcall TerminalSendKeyEvent(void* terminal, WORD vkey, WORD scanCode) +void _stdcall TerminalSendKeyEvent(void* terminal, WORD vkey, WORD scanCode, bool keyDown) { const auto publicTerminal = static_cast(terminal); - publicTerminal->_SendKeyEvent(vkey, scanCode); + publicTerminal->_SendKeyEvent(vkey, scanCode, keyDown); } void _stdcall TerminalSendCharEvent(void* terminal, wchar_t ch, WORD scanCode) diff --git a/src/cascadia/PublicTerminalCore/HwndTerminal.hpp b/src/cascadia/PublicTerminalCore/HwndTerminal.hpp index cf7ba4f4..90b4cc50 100644 --- a/src/cascadia/PublicTerminalCore/HwndTerminal.hpp +++ b/src/cascadia/PublicTerminalCore/HwndTerminal.hpp @@ -34,7 +34,7 @@ __declspec(dllexport) bool _stdcall TerminalIsSelectionActive(void* terminal); __declspec(dllexport) void _stdcall DestroyTerminal(void* terminal); __declspec(dllexport) void _stdcall TerminalSetTheme(void* terminal, TerminalTheme theme, LPCWSTR fontFamily, short fontSize, int newDpi); __declspec(dllexport) void _stdcall TerminalRegisterWriteCallback(void* terminal, const void __stdcall callback(wchar_t*)); -__declspec(dllexport) void _stdcall TerminalSendKeyEvent(void* terminal, WORD vkey, WORD scanCode); +__declspec(dllexport) void _stdcall TerminalSendKeyEvent(void* terminal, WORD vkey, WORD scanCode, bool keyDown); __declspec(dllexport) void _stdcall TerminalSendCharEvent(void* terminal, wchar_t ch, WORD scanCode); __declspec(dllexport) void _stdcall TerminalBlinkCursor(void* terminal); __declspec(dllexport) void _stdcall TerminalSetCursorVisible(void* terminal, const bool visible); @@ -92,7 +92,7 @@ struct HwndTerminal : ::Microsoft::Console::Types::IControlAccessibilityInfo friend void _stdcall TerminalClearSelection(void* terminal); friend const wchar_t* _stdcall TerminalGetSelection(void* terminal); friend bool _stdcall TerminalIsSelectionActive(void* terminal); - friend void _stdcall TerminalSendKeyEvent(void* terminal, WORD vkey, WORD scanCode); + friend void _stdcall TerminalSendKeyEvent(void* terminal, WORD vkey, WORD scanCode, bool keyDown); friend void _stdcall TerminalSendCharEvent(void* terminal, wchar_t ch, WORD scanCode); friend void _stdcall TerminalSetTheme(void* terminal, TerminalTheme theme, LPCWSTR fontFamily, short fontSize, int newDpi); friend void _stdcall TerminalBlinkCursor(void* terminal); @@ -115,7 +115,7 @@ struct HwndTerminal : ::Microsoft::Console::Types::IControlAccessibilityInfo bool _CanSendVTMouseInput() const noexcept; bool _SendMouseEvent(UINT uMsg, WPARAM wParam, LPARAM lParam) noexcept; - void _SendKeyEvent(WORD vkey, WORD scanCode) noexcept; + void _SendKeyEvent(WORD vkey, WORD scanCode, bool keyDown) noexcept; void _SendCharEvent(wchar_t ch, WORD scanCode) noexcept; // Inherited via IControlAccessibilityInfo diff --git a/src/cascadia/TerminalApp/GlobalAppSettings.cpp b/src/cascadia/TerminalApp/GlobalAppSettings.cpp index 2d3e41cf..d16ba285 100644 --- a/src/cascadia/TerminalApp/GlobalAppSettings.cpp +++ b/src/cascadia/TerminalApp/GlobalAppSettings.cpp @@ -34,12 +34,13 @@ static constexpr std::string_view CopyFormattingKey{ "copyFormatting" }; static constexpr std::string_view LaunchModeKey{ "launchMode" }; static constexpr std::string_view ConfirmCloseAllKey{ "confirmCloseAllTabs" }; static constexpr std::string_view SnapToGridOnResizeKey{ "snapToGridOnResize" }; +static constexpr std::string_view EnableStartupTaskKey{ "startOnUserLogin" }; static constexpr std::string_view DebugFeaturesKey{ "debugFeatures" }; static constexpr std::string_view ForceFullRepaintRenderingKey{ "experimental.rendering.forceFullRepaint" }; static constexpr std::string_view SoftwareRenderingKey{ "experimental.rendering.software" }; -static constexpr std::string_view EnableStartupTaskKey{ "startOnUserLogin" }; +static constexpr std::string_view ForceVTInputKey{ "experimental.input.forceVT" }; // Launch mode values static constexpr std::wstring_view DefaultLaunchModeValue{ L"default" }; @@ -130,6 +131,7 @@ void GlobalAppSettings::ApplyToSettings(TerminalSettings& settings) const noexce settings.CopyOnSelect(_CopyOnSelect); settings.ForceFullRepaintRendering(_ForceFullRepaintRendering); settings.SoftwareRendering(_SoftwareRendering); + settings.ForceVTInput(_ForceVTInput); } // Method Description: @@ -220,6 +222,7 @@ void GlobalAppSettings::LayerJson(const Json::Value& json) JsonUtils::GetBool(json, ForceFullRepaintRenderingKey, _ForceFullRepaintRendering); JsonUtils::GetBool(json, SoftwareRenderingKey, _SoftwareRendering); + JsonUtils::GetBool(json, ForceVTInputKey, _ForceVTInput); // GetBool will only override the current value if the key exists JsonUtils::GetBool(json, DebugFeaturesKey, _DebugFeaturesEnabled); diff --git a/src/cascadia/TerminalApp/GlobalAppSettings.h b/src/cascadia/TerminalApp/GlobalAppSettings.h index 90f3d772..bec6376d 100644 --- a/src/cascadia/TerminalApp/GlobalAppSettings.h +++ b/src/cascadia/TerminalApp/GlobalAppSettings.h @@ -77,6 +77,7 @@ class TerminalApp::GlobalAppSettings final GETSET_PROPERTY(bool, SnapToGridOnResize, true); GETSET_PROPERTY(bool, ForceFullRepaintRendering, false); GETSET_PROPERTY(bool, SoftwareRendering, false); + GETSET_PROPERTY(bool, ForceVTInput, false); GETSET_PROPERTY(bool, DebugFeaturesEnabled); // default value set in constructor GETSET_PROPERTY(bool, StartOnUserLogin, false); diff --git a/src/cascadia/TerminalConnection/ConptyConnection.cpp b/src/cascadia/TerminalConnection/ConptyConnection.cpp index 4734b483..1fe8399b 100644 --- a/src/cascadia/TerminalConnection/ConptyConnection.cpp +++ b/src/cascadia/TerminalConnection/ConptyConnection.cpp @@ -223,7 +223,7 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation try { const COORD dimensions{ gsl::narrow_cast(_initialCols), gsl::narrow_cast(_initialRows) }; - THROW_IF_FAILED(_CreatePseudoConsoleAndPipes(dimensions, PSEUDOCONSOLE_RESIZE_QUIRK, &_inPipe, &_outPipe, &_hPC)); + THROW_IF_FAILED(_CreatePseudoConsoleAndPipes(dimensions, PSEUDOCONSOLE_RESIZE_QUIRK | PSEUDOCONSOLE_WIN32_INPUT_MODE, &_inPipe, &_outPipe, &_hPC)); THROW_IF_FAILED(_LaunchAttachedClient()); _startTime = std::chrono::high_resolution_clock::now(); diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index 0d2530ae..653a2cdf 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -705,7 +705,8 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation if (!handled) { // _TrySendKeyEvent pretends it didn't handle F7 for some unknown reason. - (void)_TrySendKeyEvent(VK_F7, 0, modifiers); + (void)_TrySendKeyEvent(VK_F7, 0, modifiers, true); + (void)_TrySendKeyEvent(VK_F7, 0, modifiers, false); handled = true; } @@ -714,6 +715,17 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation void TermControl::_KeyDownHandler(winrt::Windows::Foundation::IInspectable const& /*sender*/, Input::KeyRoutedEventArgs const& e) + { + _KeyHandler(e, true); + } + + void TermControl::_KeyUpHandler(winrt::Windows::Foundation::IInspectable const& /*sender*/, + Input::KeyRoutedEventArgs const& e) + { + _KeyHandler(e, false); + } + + void TermControl::_KeyHandler(Input::KeyRoutedEventArgs const& e, const bool keyDown) { // If the current focused element is a child element of searchbox, // we do not send this event up to terminal @@ -722,14 +734,15 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation return; } - // mark event as handled and do nothing if... - // - closing - // - key modifier is pressed - // NOTE: for key combos like CTRL + C, two events are fired (one for CTRL, one for 'C'). We care about the 'C' event and then check for key modifiers below. + // Mark the event as handled and do nothing if we're closing, or the key + // was the Windows key. + // + // NOTE: for key combos like CTRL + C, two events are fired (one for + // CTRL, one for 'C'). Since it's possible the terminal is in + // win32-input-mode, then we'll send all these keystrokes to the + // terminal - it's smart enough to ignore the keys it doesn't care + // about. if (_closing || - e.OriginalKey() == VirtualKey::Control || - e.OriginalKey() == VirtualKey::Shift || - e.OriginalKey() == VirtualKey::Menu || e.OriginalKey() == VirtualKey::LeftWindows || e.OriginalKey() == VirtualKey::RightWindows) @@ -743,19 +756,28 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation const auto scanCode = gsl::narrow_cast(e.KeyStatus().ScanCode); bool handled = false; - // Alt-Numpad# input will send us a character once the user releases Alt, so we should be ignoring the individual keydowns. - // The character will be sent through the TSFInputControl. - // See GH#1401 for more details - if (modifiers.IsAltPressed() && (e.OriginalKey() >= VirtualKey::NumberPad0 && e.OriginalKey() <= VirtualKey::NumberPad9)) + // Alt-Numpad# input will send us a character once the user releases + // Alt, so we should be ignoring the individual keydowns. The character + // will be sent through the TSFInputControl. See GH#1401 for more + // details + if (modifiers.IsAltPressed() && + (e.OriginalKey() >= VirtualKey::NumberPad0 && e.OriginalKey() <= VirtualKey::NumberPad9)) { e.Handled(true); return; } - // GH#2235: Terminal::Settings hasn't been modified to differentiate between AltGr and Ctrl+Alt yet. + // GH#2235: Terminal::Settings hasn't been modified to differentiate + // between AltGr and Ctrl+Alt yet. // -> Don't check for key bindings if this is an AltGr key combination. - if (!modifiers.IsAltGrPressed()) + // + // GH#4999: Only process keybindings on the keydown. If we don't check + // this at all, we'll process the keybinding twice. If we only process + // keybindings on the keyUp, then we'll still send the keydown to the + // connected terminal application, and something like ctrl+shift+T will + // emit a ^T to the pipe. + if (!modifiers.IsAltGrPressed() && keyDown) { auto bindings = _settings.KeyBindings(); if (bindings) @@ -771,7 +793,7 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation if (!handled) { - handled = _TrySendKeyEvent(vkey, scanCode, modifiers); + handled = _TrySendKeyEvent(vkey, scanCode, modifiers, keyDown); } // Manually prevent keyboard navigation with tab. We want to send tab to @@ -793,7 +815,11 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation // Arguments: // - vkey: The vkey of the key pressed. // - states: The Microsoft::Terminal::Core::ControlKeyStates representing the modifier key states. - bool TermControl::_TrySendKeyEvent(const WORD vkey, const WORD scanCode, const ControlKeyStates modifiers) + // - keyDown: If true, the key was pressed, otherwise the key was released. + bool TermControl::_TrySendKeyEvent(const WORD vkey, + const WORD scanCode, + const ControlKeyStates modifiers, + const bool keyDown) { // When there is a selection active, escape should clear it and NOT flow through // to the terminal. With any other keypress, it should clear the selection AND @@ -818,7 +844,7 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation // If the terminal translated the key, mark the event as handled. // This will prevent the system from trying to get the character out // of it and sending us a CharacterReceived event. - const auto handled = vkey ? _terminal->SendKeyEvent(vkey, scanCode, modifiers) : true; + const auto handled = vkey ? _terminal->SendKeyEvent(vkey, scanCode, modifiers, keyDown) : true; if (_cursorTimer.has_value()) { diff --git a/src/cascadia/TerminalControl/TermControl.h b/src/cascadia/TerminalControl/TermControl.h index ecb7c52c..3394017e 100644 --- a/src/cascadia/TerminalControl/TermControl.h +++ b/src/cascadia/TerminalControl/TermControl.h @@ -180,6 +180,7 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation void _SetFontSize(int fontSize); void _TappedHandler(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::Input::TappedRoutedEventArgs const& e); void _KeyDownHandler(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::Input::KeyRoutedEventArgs const& e); + void _KeyUpHandler(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::Input::KeyRoutedEventArgs const& e); void _CharacterHandler(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::Input::CharacterReceivedRoutedEventArgs const& e); void _PointerPressedHandler(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::Input::PointerRoutedEventArgs const& e); void _PointerMovedHandler(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::Input::PointerRoutedEventArgs const& e); @@ -218,8 +219,9 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation void _ScrollbarUpdater(Windows::UI::Xaml::Controls::Primitives::ScrollBar scrollbar, const int viewTop, const int viewHeight, const int bufferSize); static Windows::UI::Xaml::Thickness _ParseThicknessFromPadding(const hstring padding); + void _KeyHandler(Windows::UI::Xaml::Input::KeyRoutedEventArgs const& e, const bool keyDown); ::Microsoft::Terminal::Core::ControlKeyStates _GetPressedModifierKeys() const; - bool _TrySendKeyEvent(const WORD vkey, const WORD scanCode, ::Microsoft::Terminal::Core::ControlKeyStates modifiers); + bool _TrySendKeyEvent(const WORD vkey, const WORD scanCode, ::Microsoft::Terminal::Core::ControlKeyStates modifiers, const bool keyDown); bool _TrySendMouseEvent(Windows::UI::Input::PointerPoint const& point); bool _CanSendVTMouseInput(); diff --git a/src/cascadia/TerminalControl/TermControl.xaml b/src/cascadia/TerminalControl/TermControl.xaml index 32609b73..f39e4b1e 100644 --- a/src/cascadia/TerminalControl/TermControl.xaml +++ b/src/cascadia/TerminalControl/TermControl.xaml @@ -19,6 +19,7 @@ Tapped="_TappedHandler" PointerWheelChanged="_MouseWheelHandler" PreviewKeyDown="_KeyDownHandler" + KeyUp="_KeyUpHandler" CharacterReceived="_CharacterHandler" GotFocus="_GotFocusHandler" LostFocus="_LostFocusHandler"> diff --git a/src/cascadia/TerminalCore/ITerminalApi.hpp b/src/cascadia/TerminalCore/ITerminalApi.hpp index 5b1f3d0e..66b4e9a1 100644 --- a/src/cascadia/TerminalCore/ITerminalApi.hpp +++ b/src/cascadia/TerminalCore/ITerminalApi.hpp @@ -49,6 +49,7 @@ namespace Microsoft::Terminal::Core virtual bool SetDefaultForeground(const DWORD color) noexcept = 0; virtual bool SetDefaultBackground(const DWORD color) noexcept = 0; + virtual bool EnableWin32InputMode(const bool win32InputMode) noexcept = 0; virtual bool SetCursorKeysMode(const bool applicationMode) noexcept = 0; virtual bool SetKeypadMode(const bool applicationMode) noexcept = 0; virtual bool EnableVT200MouseMode(const bool enabled) noexcept = 0; diff --git a/src/cascadia/TerminalCore/ITerminalInput.hpp b/src/cascadia/TerminalCore/ITerminalInput.hpp index 5df7a084..1d9785e4 100644 --- a/src/cascadia/TerminalCore/ITerminalInput.hpp +++ b/src/cascadia/TerminalCore/ITerminalInput.hpp @@ -16,7 +16,7 @@ namespace Microsoft::Terminal::Core ITerminalInput& operator=(const ITerminalInput&) = default; ITerminalInput& operator=(ITerminalInput&&) = default; - virtual bool SendKeyEvent(const WORD vkey, const WORD scanCode, const ControlKeyStates states) = 0; + virtual bool SendKeyEvent(const WORD vkey, const WORD scanCode, const ControlKeyStates states, const bool keyDown) = 0; virtual bool SendMouseEvent(const COORD viewportPos, const unsigned int uiButton, const ControlKeyStates states, const short wheelDelta) = 0; virtual bool SendCharEvent(const wchar_t ch, const WORD scanCode, const ControlKeyStates states) = 0; diff --git a/src/cascadia/TerminalCore/Terminal.cpp b/src/cascadia/TerminalCore/Terminal.cpp index 1b20a6e1..62f847aa 100644 --- a/src/cascadia/TerminalCore/Terminal.cpp +++ b/src/cascadia/TerminalCore/Terminal.cpp @@ -145,6 +145,8 @@ void Terminal::UpdateSettings(winrt::Microsoft::Terminal::Settings::ICoreSetting _suppressApplicationTitle = settings.SuppressApplicationTitle(); _startingTitle = settings.StartingTitle(); + _terminalInput->ForceDisableWin32InputMode(settings.ForceVTInput()); + // TODO:MSFT:21327402 - if HistorySize has changed, resize the buffer so we // have a smaller scrollback. We should do this carefully - if the new buffer // size is smaller than where the mutable viewport currently is, we'll want @@ -408,10 +410,14 @@ bool Terminal::IsTrackingMouseInput() const noexcept // - vkey: The vkey of the last pressed key. // - scanCode: The scan code of the last pressed key. // - states: The Microsoft::Terminal::Core::ControlKeyStates representing the modifier key states. +// - keyDown: If true, the key was pressed, otherwise the key was released. // Return Value: // - true if we translated the key event, and it should not be processed any further. // - false if we did not translate the key, and it should be processed into a character. -bool Terminal::SendKeyEvent(const WORD vkey, const WORD scanCode, const ControlKeyStates states) +bool Terminal::SendKeyEvent(const WORD vkey, + const WORD scanCode, + const ControlKeyStates states, + const bool keyDown) { TrySnapOnInput(); _StoreKeyEvent(vkey, scanCode); @@ -452,7 +458,7 @@ bool Terminal::SendKeyEvent(const WORD vkey, const WORD scanCode, const ControlK return false; } - KeyEvent keyEv{ true, 0, vkey, scanCode, ch, states.Value() }; + KeyEvent keyEv{ keyDown, 1, vkey, scanCode, ch, states.Value() }; return _terminalInput->HandleKey(&keyEv); } @@ -513,8 +519,15 @@ bool Terminal::SendCharEvent(const wchar_t ch, const WORD scanCode, const Contro vkey = _VirtualKeyFromCharacter(ch); } - KeyEvent keyEv{ true, 0, vkey, scanCode, ch, states.Value() }; - return _terminalInput->HandleKey(&keyEv); + // Unfortunately, the UI doesn't give us both a character down and a + // character up event, only a character received event. So fake sending both + // to the terminal input translator. Unless it's in win32-input-mode, it'll + // ignore the keyup. + KeyEvent keyDown{ true, 1, vkey, scanCode, ch, states.Value() }; + KeyEvent keyUp{ false, 1, vkey, scanCode, ch, states.Value() }; + const auto handledDown = _terminalInput->HandleKey(&keyDown); + const auto handledUp = _terminalInput->HandleKey(&keyUp); + return handledDown || handledUp; } // Method Description: diff --git a/src/cascadia/TerminalCore/Terminal.hpp b/src/cascadia/TerminalCore/Terminal.hpp index cda60bd4..6f502e37 100644 --- a/src/cascadia/TerminalCore/Terminal.hpp +++ b/src/cascadia/TerminalCore/Terminal.hpp @@ -103,6 +103,7 @@ class Microsoft::Terminal::Core::Terminal final : bool SetDefaultForeground(const COLORREF color) noexcept override; bool SetDefaultBackground(const COLORREF color) noexcept override; + bool EnableWin32InputMode(const bool win32InputMode) noexcept override; bool SetCursorKeysMode(const bool applicationMode) noexcept override; bool SetKeypadMode(const bool applicationMode) noexcept override; bool EnableVT200MouseMode(const bool enabled) noexcept override; @@ -117,7 +118,7 @@ class Microsoft::Terminal::Core::Terminal final : #pragma region ITerminalInput // These methods are defined in Terminal.cpp - bool SendKeyEvent(const WORD vkey, const WORD scanCode, const Microsoft::Terminal::Core::ControlKeyStates states) override; + bool SendKeyEvent(const WORD vkey, const WORD scanCode, const Microsoft::Terminal::Core::ControlKeyStates states, const bool keyDown) override; bool SendMouseEvent(const COORD viewportPos, const unsigned int uiButton, const ControlKeyStates states, const short wheelDelta) override; bool SendCharEvent(const wchar_t ch, const WORD scanCode, const ControlKeyStates states) override; diff --git a/src/cascadia/TerminalCore/TerminalApi.cpp b/src/cascadia/TerminalCore/TerminalApi.cpp index 72abe4e6..c7b68845 100644 --- a/src/cascadia/TerminalCore/TerminalApi.cpp +++ b/src/cascadia/TerminalCore/TerminalApi.cpp @@ -521,6 +521,12 @@ try } CATCH_LOG_RETURN_FALSE() +bool Terminal::EnableWin32InputMode(const bool win32InputMode) noexcept +{ + _terminalInput->ChangeWin32InputMode(win32InputMode); + return true; +} + bool Terminal::SetCursorKeysMode(const bool applicationMode) noexcept { _terminalInput->ChangeCursorKeysMode(applicationMode); diff --git a/src/cascadia/TerminalCore/TerminalDispatch.cpp b/src/cascadia/TerminalCore/TerminalDispatch.cpp index c209e810..eab8627f 100644 --- a/src/cascadia/TerminalCore/TerminalDispatch.cpp +++ b/src/cascadia/TerminalCore/TerminalDispatch.cpp @@ -246,6 +246,19 @@ bool TerminalDispatch::SetCursorKeysMode(const bool applicationMode) noexcept return true; } +// Method Description: +// - win32-input-mode: Enable sending full input records encoded as a string of +// characters to the client application. +// Arguments: +// - win32InputMode - set to true to enable win32-input-mode, false to disable. +// Return Value: +// - True if handled successfully. False otherwise. +bool TerminalDispatch::EnableWin32InputMode(const bool win32Mode) noexcept +{ + _terminalApi.EnableWin32InputMode(win32Mode); + return true; +} + //Routine Description: // Enable VT200 Mouse Mode - Enables/disables the mouse input handler in default tracking mode. //Arguments: @@ -394,6 +407,9 @@ bool TerminalDispatch::_PrivateModeParamsHelper(const DispatchTypes::PrivateMode case DispatchTypes::PrivateModeParams::ATT610_StartCursorBlink: success = EnableCursorBlinking(enable); break; + case DispatchTypes::PrivateModeParams::W32IM_Win32InputMode: + success = EnableWin32InputMode(enable); + break; default: // If no functions to call, overall dispatch was a failure. success = false; diff --git a/src/cascadia/TerminalCore/TerminalDispatch.hpp b/src/cascadia/TerminalCore/TerminalDispatch.hpp index e3be6ecb..4defa9f4 100644 --- a/src/cascadia/TerminalCore/TerminalDispatch.hpp +++ b/src/cascadia/TerminalCore/TerminalDispatch.hpp @@ -18,6 +18,8 @@ class TerminalDispatch : public Microsoft::Console::VirtualTerminal::TermDispatc bool CursorPosition(const size_t line, const size_t column) noexcept override; // CUP + bool EnableWin32InputMode(const bool win32InputMode) noexcept override; // win32-input-mode + bool CursorVisibility(const bool isVisible) noexcept override; // DECTCEM bool EnableCursorBlinking(const bool enable) noexcept override; // ATT610 diff --git a/src/cascadia/TerminalSettings/IControlSettings.idl b/src/cascadia/TerminalSettings/IControlSettings.idl index e8f4a246..47484d99 100644 --- a/src/cascadia/TerminalSettings/IControlSettings.idl +++ b/src/cascadia/TerminalSettings/IControlSettings.idl @@ -52,10 +52,11 @@ namespace Microsoft.Terminal.Settings Windows.UI.Xaml.VerticalAlignment BackgroundImageVerticalAlignment; UInt32 SelectionBackground; + + TextAntialiasingMode AntialiasingMode; + Boolean RetroTerminalEffect; Boolean ForceFullRepaintRendering; Boolean SoftwareRendering; - - TextAntialiasingMode AntialiasingMode; }; } diff --git a/src/cascadia/TerminalSettings/ICoreSettings.idl b/src/cascadia/TerminalSettings/ICoreSettings.idl index bd14d6e5..f1c055cc 100644 --- a/src/cascadia/TerminalSettings/ICoreSettings.idl +++ b/src/cascadia/TerminalSettings/ICoreSettings.idl @@ -33,6 +33,8 @@ namespace Microsoft.Terminal.Settings String StartingTitle; Boolean SuppressApplicationTitle; String WordDelimiters; + + Boolean ForceVTInput; }; } diff --git a/src/cascadia/TerminalSettings/terminalsettings.h b/src/cascadia/TerminalSettings/terminalsettings.h index 60c1c6f5..e32a3682 100644 --- a/src/cascadia/TerminalSettings/terminalsettings.h +++ b/src/cascadia/TerminalSettings/terminalsettings.h @@ -93,11 +93,12 @@ namespace winrt::Microsoft::Terminal::Settings::implementation GETSET_PROPERTY(ScrollbarState, ScrollState, ScrollbarState::Visible); + GETSET_PROPERTY(TextAntialiasingMode, AntialiasingMode, TextAntialiasingMode::Grayscale); + GETSET_PROPERTY(bool, RetroTerminalEffect, false); GETSET_PROPERTY(bool, ForceFullRepaintRendering, false); GETSET_PROPERTY(bool, SoftwareRendering, false); - - GETSET_PROPERTY(TextAntialiasingMode, AntialiasingMode, TextAntialiasingMode::Grayscale); + GETSET_PROPERTY(bool, ForceVTInput, false); #pragma warning(pop) diff --git a/src/cascadia/UnitTests_TerminalCore/InputTest.cpp b/src/cascadia/UnitTests_TerminalCore/InputTest.cpp index f630b7da..037c81db 100644 --- a/src/cascadia/UnitTests_TerminalCore/InputTest.cpp +++ b/src/cascadia/UnitTests_TerminalCore/InputTest.cpp @@ -61,7 +61,8 @@ namespace TerminalCoreUnitTests // Make sure we don't handle Alt+Space. The system will use this to // bring up the system menu for restore, min/maximize, size, move, // close - VERIFY_IS_FALSE(term.SendKeyEvent(L' ', 0, ControlKeyStates::LeftAltPressed)); + VERIFY_IS_FALSE(term.SendKeyEvent(L' ', 0, ControlKeyStates::LeftAltPressed, true)); + VERIFY_IS_FALSE(term.SendKeyEvent(L' ', 0, ControlKeyStates::LeftAltPressed, false)); VERIFY_IS_FALSE(term.SendCharEvent(L' ', 0, ControlKeyStates::LeftAltPressed)); } } diff --git a/src/cascadia/UnitTests_TerminalCore/MockTermSettings.h b/src/cascadia/UnitTests_TerminalCore/MockTermSettings.h index 804275a4..aa668d1a 100644 --- a/src/cascadia/UnitTests_TerminalCore/MockTermSettings.h +++ b/src/cascadia/UnitTests_TerminalCore/MockTermSettings.h @@ -38,6 +38,7 @@ namespace TerminalCoreUnitTests winrt::hstring StartingTitle() { return _startingTitle; } bool SuppressApplicationTitle() { return _suppressApplicationTitle; } uint32_t SelectionBackground() { return COLOR_WHITE; } + bool ForceVTInput() { return false; } // other implemented methods uint32_t GetColorTableEntry(int32_t) const { return 123; } @@ -59,6 +60,7 @@ namespace TerminalCoreUnitTests void StartingTitle(winrt::hstring const& value) { _startingTitle = value; } void SuppressApplicationTitle(bool suppressApplicationTitle) { _suppressApplicationTitle = suppressApplicationTitle; } void SelectionBackground(uint32_t) {} + void ForceVTInput(bool) {} // other unimplemented methods void SetColorTableEntry(int32_t /* index */, uint32_t /* value */) {} diff --git a/src/cascadia/WpfTerminalControl/NativeMethods.cs b/src/cascadia/WpfTerminalControl/NativeMethods.cs index bf17c52b..dd1731ed 100644 --- a/src/cascadia/WpfTerminalControl/NativeMethods.cs +++ b/src/cascadia/WpfTerminalControl/NativeMethods.cs @@ -49,6 +49,11 @@ public enum WindowMessage : int /// WM_KEYDOWN = 0x0100, + /// + /// The WM_KEYUP message is posted to the window with the keyboard focus when a nonsystem key is released. A nonsystem key is a key that is pressed when the ALT key is not pressed, or a keyboard key that is pressed when a window has the keyboard focus. + /// + WM_KEYUP = 0x0101, + /// /// The WM_CHAR message is posted to the window with the keyboard focus when a WM_KEYDOWN message is translated by the TranslateMessage function. The WM_CHAR message contains the character code of the key that was pressed. /// @@ -207,7 +212,7 @@ public enum SetWindowPosFlags : uint public static extern void DestroyTerminal(IntPtr terminal); [DllImport("PublicTerminalCore.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)] - public static extern void TerminalSendKeyEvent(IntPtr terminal, ushort vkey, ushort scanCode); + public static extern void TerminalSendKeyEvent(IntPtr terminal, ushort vkey, ushort scanCode, bool keyDown); [DllImport("PublicTerminalCore.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)] public static extern void TerminalSendCharEvent(IntPtr terminal, char ch, ushort scanCode); diff --git a/src/cascadia/WpfTerminalControl/TerminalContainer.cs b/src/cascadia/WpfTerminalControl/TerminalContainer.cs index 6634bec8..ddd754b0 100644 --- a/src/cascadia/WpfTerminalControl/TerminalContainer.cs +++ b/src/cascadia/WpfTerminalControl/TerminalContainer.cs @@ -235,9 +235,13 @@ private IntPtr TerminalContainer_MessageHook(IntPtr hwnd, int msg, IntPtr wParam case NativeMethods.WindowMessage.WM_KEYDOWN: // WM_KEYDOWN lParam layout documentation: https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-keydown NativeMethods.TerminalSetCursorVisible(this.terminal, true); - NativeMethods.TerminalSendKeyEvent(this.terminal, (ushort)wParam, (ushort)((uint)lParam >> 16)); + NativeMethods.TerminalSendKeyEvent(this.terminal, (ushort)wParam, (ushort)((uint)lParam >> 16), true); this.blinkTimer?.Start(); break; + case NativeMethods.WindowMessage.WM_KEYUP: + // WM_KEYUP lParam layout documentation: https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-keyup + NativeMethods.TerminalSendKeyEvent(this.terminal, (ushort)wParam, (ushort)((uint)lParam >> 16), false); + break; case NativeMethods.WindowMessage.WM_CHAR: // WM_CHAR lParam layout documentation: https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-char NativeMethods.TerminalSendCharEvent(this.terminal, (char)wParam, (ushort)((uint)lParam >> 16)); diff --git a/src/host/ConsoleArguments.cpp b/src/host/ConsoleArguments.cpp index 84daa673..41c07c79 100644 --- a/src/host/ConsoleArguments.cpp +++ b/src/host/ConsoleArguments.cpp @@ -19,6 +19,7 @@ const std::wstring_view ConsoleArguments::WIDTH_ARG = L"--width"; const std::wstring_view ConsoleArguments::HEIGHT_ARG = L"--height"; const std::wstring_view ConsoleArguments::INHERIT_CURSOR_ARG = L"--inheritcursor"; const std::wstring_view ConsoleArguments::RESIZE_QUIRK = L"--resizeQuirk"; +const std::wstring_view ConsoleArguments::WIN32_INPUT_MODE = L"--win32input"; const std::wstring_view ConsoleArguments::FEATURE_ARG = L"--feature"; const std::wstring_view ConsoleArguments::FEATURE_PTY_ARG = L"pty"; @@ -486,6 +487,12 @@ void ConsoleArguments::s_ConsumeArg(_Inout_ std::vector& args, _In s_ConsumeArg(args, i); hr = S_OK; } + else if (arg == WIN32_INPUT_MODE) + { + _win32InputMode = true; + s_ConsumeArg(args, i); + hr = S_OK; + } else if (arg == CLIENT_COMMANDLINE_ARG) { // Everything after this is the explicit commandline @@ -622,6 +629,10 @@ bool ConsoleArguments::IsResizeQuirkEnabled() const { return _resizeQuirk; } +bool ConsoleArguments::IsWin32InputModeEnabled() const +{ + return _win32InputMode; +} // Method Description: // - Tell us to use a different size than the one parsed as the size of the diff --git a/src/host/ConsoleArguments.hpp b/src/host/ConsoleArguments.hpp index 248248ab..e2e7cad2 100644 --- a/src/host/ConsoleArguments.hpp +++ b/src/host/ConsoleArguments.hpp @@ -51,6 +51,7 @@ class ConsoleArguments short GetHeight() const; bool GetInheritCursor() const; bool IsResizeQuirkEnabled() const; + bool IsWin32InputModeEnabled() const; void SetExpectedSize(COORD dimensions) noexcept; @@ -70,6 +71,7 @@ class ConsoleArguments static const std::wstring_view HEIGHT_ARG; static const std::wstring_view INHERIT_CURSOR_ARG; static const std::wstring_view RESIZE_QUIRK; + static const std::wstring_view WIN32_INPUT_MODE; static const std::wstring_view FEATURE_ARG; static const std::wstring_view FEATURE_PTY_ARG; @@ -131,6 +133,7 @@ class ConsoleArguments DWORD _signalHandle; bool _inheritCursor; bool _resizeQuirk{ false }; + bool _win32InputMode{ false }; bool _receivedEarlySizeChange; short _originalWidth; diff --git a/src/host/VtIo.cpp b/src/host/VtIo.cpp index 3fea6aa6..5779fce7 100644 --- a/src/host/VtIo.cpp +++ b/src/host/VtIo.cpp @@ -74,6 +74,7 @@ VtIo::VtIo() : { _lookingForCursorPosition = pArgs->GetInheritCursor(); _resizeQuirk = pArgs->IsResizeQuirkEnabled(); + _win32InputMode = pArgs->IsWin32InputModeEnabled(); // If we were already given VT handles, set up the VT IO engine to use those. if (pArgs->InConptyMode()) @@ -233,6 +234,15 @@ bool VtIo::IsUsingVt() const CATCH_RETURN(); } + // GH#4999 - Send a sequence to the connected terminal to request + // win32-input-mode from them. This will enable the connected terminal to + // send us full INPUT_RECORDs as input. If the terminal doesn't understand + // this sequence, it'll just ignore it. + if (_win32InputMode) + { + LOG_IF_FAILED(_pVtRenderEngine->RequestWin32Input()); + } + // MSFT: 15813316 // If the terminal application wants us to inherit the cursor position, // we're going to emit a VT sequence to ask for the cursor position, then diff --git a/src/host/VtIo.hpp b/src/host/VtIo.hpp index 3ee6654d..e48378d2 100644 --- a/src/host/VtIo.hpp +++ b/src/host/VtIo.hpp @@ -62,6 +62,7 @@ namespace Microsoft::Console::VirtualTerminal std::mutex _shutdownLock; bool _resizeQuirk{ false }; + bool _win32InputMode{ false }; std::unique_ptr _pVtRenderEngine; std::unique_ptr _pVtInputThread; diff --git a/src/host/getset.cpp b/src/host/getset.cpp index ce7c2420..8ee25582 100644 --- a/src/host/getset.cpp +++ b/src/host/getset.cpp @@ -1230,6 +1230,19 @@ void ApiRoutines::GetConsoleDisplayModeImpl(ULONG& flags) noexcept return STATUS_SUCCESS; } +// Function Description: +// - A private API call which enables/disables sending full input records +// encoded as a string of characters to the client application. +// Parameters: +// - win32InputMode - set to true to enable win32-input-mode, false to disable. +// Return value: +// - +void DoSrvPrivateEnableWin32InputMode(const bool win32InputMode) +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + gci.pInputBuffer->GetTerminalInput().ChangeWin32InputMode(win32InputMode); +} + // Routine Description: // - A private API call for changing the screen mode between normal and reverse. // When in reverse screen mode, the background and foreground colors are switched. diff --git a/src/host/getset.h b/src/host/getset.h index c1da1711..e6d43474 100644 --- a/src/host/getset.h +++ b/src/host/getset.h @@ -20,6 +20,7 @@ class SCREEN_INFORMATION; [[nodiscard]] NTSTATUS DoSrvPrivateSetCursorKeysMode(_In_ bool fApplicationMode); [[nodiscard]] NTSTATUS DoSrvPrivateSetKeypadMode(_In_ bool fApplicationMode); +void DoSrvPrivateEnableWin32InputMode(const bool win32InputMode); [[nodiscard]] NTSTATUS DoSrvPrivateSetScreenMode(const bool reverseMode); [[nodiscard]] NTSTATUS DoSrvPrivateSetAutoWrapMode(const bool wrapAtEOL); diff --git a/src/host/outputStream.cpp b/src/host/outputStream.cpp index 8a098f13..a4aa4c57 100644 --- a/src/host/outputStream.cpp +++ b/src/host/outputStream.cpp @@ -239,6 +239,20 @@ bool ConhostInternalGetSet::PrivateSetKeypadMode(const bool fApplicationMode) return NT_SUCCESS(DoSrvPrivateSetKeypadMode(fApplicationMode)); } +// Routine Description: +// - Connects the PrivateEnableWin32InputMode call directly into our Driver Message servicing call inside Conhost.exe +// PrivateEnableWin32InputMode is an internal-only "API" call that the vt commands can execute, +// but it is not represented as a function call on out public API surface. +// Arguments: +// - win32InputMode - set to true to enable win32-input-mode, false to disable. +// Return Value: +// - true always +bool ConhostInternalGetSet::PrivateEnableWin32InputMode(const bool win32InputMode) +{ + DoSrvPrivateEnableWin32InputMode(win32InputMode); + return true; +} + // Routine Description: // - Sets the terminal emulation mode to either ANSI-compatible or VT52. // PrivateSetAnsiMode is an internal-only "API" call that the vt commands can execute, diff --git a/src/host/outputStream.hpp b/src/host/outputStream.hpp index 8d28b75c..a2671396 100644 --- a/src/host/outputStream.hpp +++ b/src/host/outputStream.hpp @@ -73,6 +73,7 @@ class ConhostInternalGetSet final : public Microsoft::Console::VirtualTerminal:: bool PrivateSetCursorKeysMode(const bool applicationMode) override; bool PrivateSetKeypadMode(const bool applicationMode) override; + bool PrivateEnableWin32InputMode(const bool win32InputMode) override; bool PrivateSetAnsiMode(const bool ansiMode) override; bool PrivateSetScreenMode(const bool reverseMode) override; diff --git a/src/inc/conpty-static.h b/src/inc/conpty-static.h index a6561b7b..a88572f3 100644 --- a/src/inc/conpty-static.h +++ b/src/inc/conpty-static.h @@ -16,6 +16,7 @@ extern "C" { #endif #define PSEUDOCONSOLE_RESIZE_QUIRK (2u) +#define PSEUDOCONSOLE_WIN32_INPUT_MODE (4u) HRESULT WINAPI ConptyCreatePseudoConsole(COORD size, HANDLE hInput, HANDLE hOutput, DWORD dwFlags, HPCON* phPC); diff --git a/src/renderer/vt/VtSequences.cpp b/src/renderer/vt/VtSequences.cpp index 0b9c7baa..013a16da 100644 --- a/src/renderer/vt/VtSequences.cpp +++ b/src/renderer/vt/VtSequences.cpp @@ -436,3 +436,17 @@ using namespace Microsoft::Console::Render; { return _Write("\x1b[29m"); } + +// Method Description: +// - Send a sequence to the connected terminal to request win32-input-mode from +// them. This will enable the connected terminal to send us full INPUT_RECORDs +// as input. If the terminal doesn't understand this sequence, it'll just +// ignore it. +// Arguments: +// - +// Return Value: +// - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write. +[[nodiscard]] HRESULT VtEngine::_RequestWin32Input() noexcept +{ + return _Write("\x1b[?9001h"); +} diff --git a/src/renderer/vt/state.cpp b/src/renderer/vt/state.cpp index 099fa487..c03b6574 100644 --- a/src/renderer/vt/state.cpp +++ b/src/renderer/vt/state.cpp @@ -509,3 +509,19 @@ void VtEngine::SetResizeQuirk(const bool resizeQuirk) { return S_OK; } + +// Method Description: +// - Send a sequence to the connected terminal to request win32-input-mode from +// them. This will enable the connected terminal to send us full INPUT_RECORDs +// as input. If the terminal doesn't understand this sequence, it'll just +// ignore it. +// Arguments: +// - +// Return Value: +// - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write. +HRESULT VtEngine::RequestWin32Input() noexcept +{ + RETURN_IF_FAILED(_RequestWin32Input()); + RETURN_IF_FAILED(_Flush()); + return S_OK; +} diff --git a/src/renderer/vt/vtrenderer.hpp b/src/renderer/vt/vtrenderer.hpp index 715cd9ce..613c2048 100644 --- a/src/renderer/vt/vtrenderer.hpp +++ b/src/renderer/vt/vtrenderer.hpp @@ -110,6 +110,8 @@ namespace Microsoft::Console::Render [[nodiscard]] virtual HRESULT ManuallyClearScrollback() noexcept; + [[nodiscard]] HRESULT RequestWin32Input() noexcept; + protected: wil::unique_hfile _hFile; std::string _buffer; @@ -208,6 +210,8 @@ namespace Microsoft::Console::Render [[nodiscard]] HRESULT _RequestCursor() noexcept; + [[nodiscard]] HRESULT _RequestWin32Input() noexcept; + [[nodiscard]] virtual HRESULT _MoveCursor(const COORD coord) noexcept = 0; [[nodiscard]] HRESULT _RgbUpdateDrawingBrushes(const COLORREF colorForeground, const COLORREF colorBackground, diff --git a/src/terminal/adapter/DispatchTypes.hpp b/src/terminal/adapter/DispatchTypes.hpp index 8b41d08c..0c8c9b4f 100644 --- a/src/terminal/adapter/DispatchTypes.hpp +++ b/src/terminal/adapter/DispatchTypes.hpp @@ -96,7 +96,8 @@ namespace Microsoft::Console::VirtualTerminal::DispatchTypes UTF8_EXTENDED_MODE = 1005, SGR_EXTENDED_MODE = 1006, ALTERNATE_SCROLL = 1007, - ASB_AlternateScreenBuffer = 1049 + ASB_AlternateScreenBuffer = 1049, + W32IM_Win32InputMode = 9001 }; namespace CharacterSets diff --git a/src/terminal/adapter/IInteractDispatch.hpp b/src/terminal/adapter/IInteractDispatch.hpp index f1d30628..d8551042 100644 --- a/src/terminal/adapter/IInteractDispatch.hpp +++ b/src/terminal/adapter/IInteractDispatch.hpp @@ -30,7 +30,7 @@ namespace Microsoft::Console::VirtualTerminal virtual bool WriteInput(std::deque>& inputEvents) = 0; - virtual bool WriteCtrlC() = 0; + virtual bool WriteCtrlKey(const KeyEvent& event) = 0; virtual bool WriteString(const std::wstring_view string) = 0; diff --git a/src/terminal/adapter/ITermDispatch.hpp b/src/terminal/adapter/ITermDispatch.hpp index 92049e9b..e6dc5e9d 100644 --- a/src/terminal/adapter/ITermDispatch.hpp +++ b/src/terminal/adapter/ITermDispatch.hpp @@ -53,6 +53,7 @@ class Microsoft::Console::VirtualTerminal::ITermDispatch virtual bool SetColumns(const size_t columns) = 0; // DECCOLM virtual bool SetCursorKeysMode(const bool applicationMode) = 0; // DECCKM virtual bool SetKeypadMode(const bool applicationMode) = 0; // DECKPAM, DECKPNM + virtual bool EnableWin32InputMode(const bool win32InputMode) = 0; // win32-input-mode virtual bool EnableCursorBlinking(const bool enable) = 0; // ATT610 virtual bool SetAnsiMode(const bool ansiMode) = 0; // DECANM virtual bool SetScreenMode(const bool reverseMode) = 0; // DECSCNM diff --git a/src/terminal/adapter/InteractDispatch.cpp b/src/terminal/adapter/InteractDispatch.cpp index 982c3c8c..51f17616 100644 --- a/src/terminal/adapter/InteractDispatch.cpp +++ b/src/terminal/adapter/InteractDispatch.cpp @@ -37,18 +37,17 @@ bool InteractDispatch::WriteInput(std::deque>& inpu } // Method Description: -// - Writes a Ctrl-C event to the host. The host will then decide what to do -// with it, including potentially sending an interrupt to a client -// application. +// - Writes a key event to the host in a fashion that will enable the host to +// process special keys such as Ctrl-C or Ctrl+Break. The host will then +// decide what to do with it, including potentially sending an interrupt to a +// client application. // Arguments: -// +// - event: The key to send to the host. // Return Value: // True if handled successfully. False otherwise. -bool InteractDispatch::WriteCtrlC() +bool InteractDispatch::WriteCtrlKey(const KeyEvent& event) { - KeyEvent keyDown = KeyEvent(true, 1, 'C', 0, UNICODE_ETX, LEFT_CTRL_PRESSED); - KeyEvent keyUp = KeyEvent(false, 1, 'C', 0, UNICODE_ETX, LEFT_CTRL_PRESSED); - return _pConApi->PrivateWriteConsoleControlInput(keyDown) && _pConApi->PrivateWriteConsoleControlInput(keyUp); + return _pConApi->PrivateWriteConsoleControlInput(event); } // Method Description: diff --git a/src/terminal/adapter/InteractDispatch.hpp b/src/terminal/adapter/InteractDispatch.hpp index 482d0eae..da00c237 100644 --- a/src/terminal/adapter/InteractDispatch.hpp +++ b/src/terminal/adapter/InteractDispatch.hpp @@ -26,7 +26,7 @@ namespace Microsoft::Console::VirtualTerminal InteractDispatch(std::unique_ptr pConApi); bool WriteInput(std::deque>& inputEvents) override; - bool WriteCtrlC() override; + bool WriteCtrlKey(const KeyEvent& event) override; bool WriteString(const std::wstring_view string) override; bool WindowManipulation(const DispatchTypes::WindowManipulationType function, const std::basic_string_view parameters) override; // DTTERM_WindowManipulation diff --git a/src/terminal/adapter/adaptDispatch.cpp b/src/terminal/adapter/adaptDispatch.cpp index babdbab7..fe89e839 100644 --- a/src/terminal/adapter/adaptDispatch.cpp +++ b/src/terminal/adapter/adaptDispatch.cpp @@ -1031,6 +1031,9 @@ bool AdaptDispatch::_PrivateModeParamsHelper(const DispatchTypes::PrivateModePar case DispatchTypes::PrivateModeParams::ASB_AlternateScreenBuffer: success = enable ? UseAlternateScreenBuffer() : UseMainScreenBuffer(); break; + case DispatchTypes::PrivateModeParams::W32IM_Win32InputMode: + success = EnableWin32InputMode(enable); + break; default: // If no functions to call, overall dispatch was a failure. success = false; @@ -1102,6 +1105,27 @@ bool AdaptDispatch::SetKeypadMode(const bool fApplicationMode) return success; } +// Method Description: +// - win32-input-mode: Enable sending full input records encoded as a string of +// characters to the client application. +// Arguments: +// - win32InputMode - set to true to enable win32-input-mode, false to disable. +// Return Value: +// - True if handled successfully. False otherwise. +bool AdaptDispatch::EnableWin32InputMode(const bool win32InputMode) +{ + bool success = true; + success = _pConApi->PrivateEnableWin32InputMode(win32InputMode); + + // If we're a conpty, always return false + if (_pConApi->IsConsolePty()) + { + return false; + } + + return success; +} + // - DECCKM - Sets the cursor keys input mode to either Application mode or Normal mode (true, false respectively) // Arguments: // - applicationMode - set to true to enable Application Mode Input, false for Normal Mode Input. diff --git a/src/terminal/adapter/adaptDispatch.hpp b/src/terminal/adapter/adaptDispatch.hpp index fd1dcd7e..64a1960a 100644 --- a/src/terminal/adapter/adaptDispatch.hpp +++ b/src/terminal/adapter/adaptDispatch.hpp @@ -68,6 +68,7 @@ namespace Microsoft::Console::VirtualTerminal bool ResetPrivateModes(const std::basic_string_view params) override; // DECRST bool SetCursorKeysMode(const bool applicationMode) override; // DECCKM bool SetKeypadMode(const bool applicationMode) override; // DECKPAM, DECKPNM + bool EnableWin32InputMode(const bool win32InputMode) override; // win32-input-mode bool EnableCursorBlinking(const bool enable) override; // ATT610 bool SetAnsiMode(const bool ansiMode) override; // DECANM bool SetScreenMode(const bool reverseMode) override; // DECSCNM diff --git a/src/terminal/adapter/conGetSet.hpp b/src/terminal/adapter/conGetSet.hpp index 2a752eca..ddf6c262 100644 --- a/src/terminal/adapter/conGetSet.hpp +++ b/src/terminal/adapter/conGetSet.hpp @@ -45,6 +45,7 @@ namespace Microsoft::Console::VirtualTerminal const SMALL_RECT& window) = 0; virtual bool PrivateSetCursorKeysMode(const bool applicationMode) = 0; virtual bool PrivateSetKeypadMode(const bool applicationMode) = 0; + virtual bool PrivateEnableWin32InputMode(const bool win32InputMode) = 0; virtual bool PrivateSetAnsiMode(const bool ansiMode) = 0; virtual bool PrivateSetScreenMode(const bool reverseMode) = 0; diff --git a/src/terminal/adapter/termDispatch.hpp b/src/terminal/adapter/termDispatch.hpp index f4e5b080..ce0e5ca6 100644 --- a/src/terminal/adapter/termDispatch.hpp +++ b/src/terminal/adapter/termDispatch.hpp @@ -47,6 +47,7 @@ class Microsoft::Console::VirtualTerminal::TermDispatch : public Microsoft::Cons bool SetColumns(const size_t /*columns*/) noexcept override { return false; } // DECCOLM bool SetCursorKeysMode(const bool /*applicationMode*/) noexcept override { return false; } // DECCKM bool SetKeypadMode(const bool /*applicationMode*/) noexcept override { return false; } // DECKPAM, DECKPNM + bool EnableWin32InputMode(const bool /*win32InputMode*/) noexcept override { return false; } // win32-input-mode bool EnableCursorBlinking(const bool /*enable*/) noexcept override { return false; } // ATT610 bool SetAnsiMode(const bool /*ansiMode*/) noexcept override { return false; } // DECANM bool SetScreenMode(const bool /*reverseMode*/) noexcept override { return false; } // DECSCNM diff --git a/src/terminal/adapter/ut_adapter/adapterTest.cpp b/src/terminal/adapter/ut_adapter/adapterTest.cpp index f20a2f2e..e633b099 100644 --- a/src/terminal/adapter/ut_adapter/adapterTest.cpp +++ b/src/terminal/adapter/ut_adapter/adapterTest.cpp @@ -162,6 +162,13 @@ class TestGetSet final : public ConGetSet return _privateSetKeypadModeResult; } + bool PrivateEnableWin32InputMode(const bool /*win32InputMode*/) override + { + Log::Comment(L"PrivateEnableWin32InputMode MOCK called..."); + + return true; + } + bool PrivateSetAnsiMode(const bool ansiMode) override { Log::Comment(L"PrivateSetAnsiMode MOCK called..."); diff --git a/src/terminal/input/terminalInput.cpp b/src/terminal/input/terminalInput.cpp index 64d6e286..7ebabde1 100644 --- a/src/terminal/input/terminalInput.cpp +++ b/src/terminal/input/terminalInput.cpp @@ -267,6 +267,15 @@ void TerminalInput::ChangeCursorKeysMode(const bool applicationMode) noexcept _cursorApplicationMode = applicationMode; } +void TerminalInput::ChangeWin32InputMode(const bool win32InputMode) noexcept +{ + _win32InputMode = win32InputMode; +} +void TerminalInput::ForceDisableWin32InputMode(const bool win32InputMode) noexcept +{ + _forceDisableWin32InputMode = win32InputMode; +} + static const std::basic_string_view _getKeyMapping(const KeyEvent& keyEvent, const bool ansiMode, const bool cursorApplicationMode, @@ -520,6 +529,16 @@ bool TerminalInput::HandleKey(const IInputEvent* const pInEvent) auto keyEvent = *static_cast(pInEvent); + // GH#4999 - If we're in win32-input mode, skip straight to doing that. + // Since this mode handles all types of key events, do nothing else. + // Only do this if win32-input-mode support isn't manually disabled. + if (_win32InputMode && !_forceDisableWin32InputMode) + { + const auto seq = _GenerateWin32KeySequence(keyEvent); + _SendInputSequence(seq); + return true; + } + // Only need to handle key down. See raw key handler (see RawReadWaitRoutine in stream.cpp) if (!keyEvent.IsKeyDown()) { @@ -719,3 +738,31 @@ void TerminalInput::_SendInputSequence(const std::wstring_view sequence) const n } } } + +// Method Description: +// - Synthesize a win32-input-mode sequence for the given keyevent. +// Arguments: +// - key: the KeyEvent to serialize. +// Return Value: +// - the formatted string representation of this key +std::wstring TerminalInput::_GenerateWin32KeySequence(const KeyEvent& key) +{ + // Sequences are formatted as follows: + // + // ^[ [ Vk ; Sc ; Uc ; Kd ; Cs ; Rc _ + // + // Vk: the value of wVirtualKeyCode - any number. If omitted, defaults to '0'. + // Sc: the value of wVirtualScanCode - any number. If omitted, defaults to '0'. + // Uc: the decimal value of UnicodeChar - for example, NUL is "0", LF is + // "10", the character 'A' is "65". If omitted, defaults to '0'. + // Kd: the value of bKeyDown - either a '0' or '1'. If omitted, defaults to '0'. + // Cs: the value of dwControlKeyState - any number. If omitted, defaults to '0'. + // Rc: the value of wRepeatCount - any number. If omitted, defaults to '1'. + return fmt::format(L"\x1b[{};{};{};{};{};{}_", + key.GetVirtualKeyCode(), + key.GetVirtualScanCode(), + static_cast(key.GetCharData()), + key.IsKeyDown() ? 1 : 0, + key.GetActiveModifierKeys(), + key.GetRepeatCount()); +} diff --git a/src/terminal/input/terminalInput.hpp b/src/terminal/input/terminalInput.hpp index 00cf92bb..7b2d9abf 100644 --- a/src/terminal/input/terminalInput.hpp +++ b/src/terminal/input/terminalInput.hpp @@ -38,6 +38,9 @@ namespace Microsoft::Console::VirtualTerminal void ChangeKeypadMode(const bool applicationMode) noexcept; void ChangeCursorKeysMode(const bool applicationMode) noexcept; + void ChangeWin32InputMode(const bool win32InputMode) noexcept; + void ForceDisableWin32InputMode(const bool win32InputMode) noexcept; + #pragma region MouseInput // These methods are defined in mouseInput.cpp bool HandleMouse(const COORD position, @@ -68,14 +71,17 @@ namespace Microsoft::Console::VirtualTerminal // storage location for the leading surrogate of a utf-16 surrogate pair std::optional _leadingSurrogate; - bool _ansiMode = true; - bool _keypadApplicationMode = false; - bool _cursorApplicationMode = false; + bool _ansiMode{ true }; + bool _keypadApplicationMode{ false }; + bool _cursorApplicationMode{ false }; + bool _win32InputMode{ false }; + bool _forceDisableWin32InputMode{ false }; void _SendChar(const wchar_t ch); void _SendNullInputSequence(const DWORD dwControlKeyState) const; void _SendInputSequence(const std::wstring_view sequence) const noexcept; void _SendEscapedInputSequence(const wchar_t wch) const; + static std::wstring _GenerateWin32KeySequence(const KeyEvent& key); #pragma region MouseInputState Management // These methods are defined in mouseInputState.cpp diff --git a/src/terminal/parser/InputStateMachineEngine.cpp b/src/terminal/parser/InputStateMachineEngine.cpp index 5013820d..c364f956 100644 --- a/src/terminal/parser/InputStateMachineEngine.cpp +++ b/src/terminal/parser/InputStateMachineEngine.cpp @@ -129,7 +129,8 @@ bool InputStateMachineEngine::_DoControlCharacter(const wchar_t wch, const bool if (wch == UNICODE_ETX && !writeAlt) { // This is Ctrl+C, which is handled specially by the host. - success = _pDispatch->WriteCtrlC(); + const auto [keyDown, keyUp] = KeyEvent::MakePair(1, 'C', 0, UNICODE_ETX, LEFT_CTRL_PRESSED); + success = _pDispatch->WriteCtrlKey(keyDown) && _pDispatch->WriteCtrlKey(keyUp); } else if (wch >= '\x0' && wch < '\x20') { @@ -365,7 +366,17 @@ bool InputStateMachineEngine::ActionCsiDispatch(const wchar_t wch, const std::basic_string_view intermediates, const std::basic_string_view parameters) { - if (_pDispatch->IsVtInputEnabled() && _pfnFlushToInputQueue) + const auto actionCode = static_cast(wch); + + // GH#4999 - If the client was in VT input mode, but we received a + // win32-input-mode sequence, then _don't_ passthrough the sequence to the + // client. It's impossibly unlikely that the client actually wanted + // win32-input-mode, and if they did, then we'll just translate the + // INPUT_RECORD back to the same sequence we say here later on, when the + // client reads it. + if (_pDispatch->IsVtInputEnabled() && + _pfnFlushToInputQueue && + actionCode != CsiActionCodes::Win32KeyboardInput) { return _pfnFlushToInputQueue(); } @@ -375,6 +386,7 @@ bool InputStateMachineEngine::ActionCsiDispatch(const wchar_t wch, unsigned int function = 0; size_t col = 0; size_t row = 0; + KeyEvent key; // This is all the args after the first arg, and the count of args not including the first one. const auto remainingArgs = parameters.size() > 1 ? parameters.substr(1) : std::basic_string_view{}; @@ -403,7 +415,7 @@ bool InputStateMachineEngine::ActionCsiDispatch(const wchar_t wch, } return success; } - switch (static_cast(wch)) + switch (actionCode) { case CsiActionCodes::Generic: modifierState = _GetGenericKeysModifierState(parameters); @@ -440,6 +452,9 @@ bool InputStateMachineEngine::ActionCsiDispatch(const wchar_t wch, case CsiActionCodes::DTTERM_WindowManipulation: success = _GetWindowManipulationType(parameters, function); break; + case CsiActionCodes::Win32KeyboardInput: + success = _GenerateWin32Key(parameters, key); + break; default: success = false; break; @@ -480,6 +495,14 @@ bool InputStateMachineEngine::ActionCsiDispatch(const wchar_t wch, success = _pDispatch->WindowManipulation(static_cast(function), remainingArgs); break; + case CsiActionCodes::Win32KeyboardInput: + { + // Use WriteCtrlKey here, even for keys that _aren't_ control keys, + // because that will take extra steps to make sure things like + // Ctrl+C, Ctrl+Break are handled correctly. + success = _pDispatch->WriteCtrlKey(key); + break; + } default: success = false; break; @@ -1291,3 +1314,57 @@ bool InputStateMachineEngine::_GetSGRXYPosition(const std::basic_string_view parameters, + KeyEvent& key) +{ + // Sequences are formatted as follows: + // + // ^[ [ Vk ; Sc ; Uc ; Kd ; Cs ; Rc _ + // + // Vk: the value of wVirtualKeyCode - any number. If omitted, defaults to '0'. + // Sc: the value of wVirtualScanCode - any number. If omitted, defaults to '0'. + // Uc: the decimal value of UnicodeChar - for example, NUL is "0", LF is + // "10", the character 'A' is "65". If omitted, defaults to '0'. + // Kd: the value of bKeyDown - either a '0' or '1'. If omitted, defaults to '0'. + // Cs: the value of dwControlKeyState - any number. If omitted, defaults to '0'. + // Rc: the value of wRepeatCount - any number. If omitted, defaults to '1'. + + if (parameters.size() > 6) + { + return false; + } + + key = KeyEvent(); + key.SetRepeatCount(1); + switch (parameters.size()) + { + case 6: + key.SetRepeatCount(::base::saturated_cast(parameters.at(5))); + [[fallthrough]]; + case 5: + key.SetActiveModifierKeys(::base::saturated_cast(parameters.at(4))); + [[fallthrough]]; + case 4: + key.SetKeyDown(static_cast(parameters.at(3))); + [[fallthrough]]; + case 3: + key.SetCharData(static_cast(parameters.at(2))); + [[fallthrough]]; + case 2: + key.SetVirtualScanCode(::base::saturated_cast(parameters.at(1))); + [[fallthrough]]; + case 1: + key.SetVirtualKeyCode(::base::saturated_cast(parameters.at(0))); + break; + } + + return true; +} diff --git a/src/terminal/parser/InputStateMachineEngine.hpp b/src/terminal/parser/InputStateMachineEngine.hpp index 64d5de3a..922b3623 100644 --- a/src/terminal/parser/InputStateMachineEngine.hpp +++ b/src/terminal/parser/InputStateMachineEngine.hpp @@ -73,6 +73,7 @@ namespace Microsoft::Console::VirtualTerminal CSI_F4 = L'S', DTTERM_WindowManipulation = L't', CursorBackTab = L'Z', + Win32KeyboardInput = L'_' }; enum CsiMouseButtonCodes : unsigned short @@ -214,6 +215,8 @@ namespace Microsoft::Console::VirtualTerminal bool _GetWindowManipulationType(const std::basic_string_view parameters, unsigned int& function) const noexcept; + bool _GenerateWin32Key(const std::basic_string_view parameters, KeyEvent& key); + static constexpr size_t DefaultLine = 1; static constexpr size_t DefaultColumn = 1; bool _GetXYPosition(const std::basic_string_view parameters, diff --git a/src/terminal/parser/OutputStateMachineEngine.cpp b/src/terminal/parser/OutputStateMachineEngine.cpp index a3c7a0f4..3f8eae05 100644 --- a/src/terminal/parser/OutputStateMachineEngine.cpp +++ b/src/terminal/parser/OutputStateMachineEngine.cpp @@ -7,6 +7,7 @@ #include "OutputStateMachineEngine.hpp" #include "ascii.hpp" + using namespace Microsoft::Console; using namespace Microsoft::Console::VirtualTerminal; diff --git a/src/terminal/parser/OutputStateMachineEngine.hpp b/src/terminal/parser/OutputStateMachineEngine.hpp index fb41724c..1a0b97e1 100644 --- a/src/terminal/parser/OutputStateMachineEngine.hpp +++ b/src/terminal/parser/OutputStateMachineEngine.hpp @@ -171,7 +171,7 @@ namespace Microsoft::Console::VirtualTerminal SetCursorColor = 12, ResetForegroundColor = 110, // Not implemented ResetBackgroundColor = 111, // Not implemented - ResetCursorColor = 112, + ResetCursorColor = 112 }; static constexpr DispatchTypes::GraphicsOptions DefaultGraphicsOption = DispatchTypes::GraphicsOptions::Off; diff --git a/src/terminal/parser/ut_parser/InputEngineTest.cpp b/src/terminal/parser/ut_parser/InputEngineTest.cpp index c56544db..b631dfa4 100644 --- a/src/terminal/parser/ut_parser/InputEngineTest.cpp +++ b/src/terminal/parser/ut_parser/InputEngineTest.cpp @@ -103,7 +103,18 @@ class TestState void TestInputCallback(std::deque>& inEvents) { auto records = IInputEvent::ToInputRecords(inEvents); - VERIFY_ARE_EQUAL((size_t)1, vExpectedInput.size()); + + // This callback doesn't work super well for the Ctrl+C iteration of the + // C0Test. For ^C, we always send a keydown and a key up event, however, + // both calls to WriteCtrlKey happen in one single call to + // ProcessString, and the test doesn't have a chance to load each key + // into this callback individually. Instead, we'll just skip these + // checks for the second call to WriteInput for this test. + if (_expectSendCtrlC && vExpectedInput.size() == 0) + { + return; + } + VERIFY_ARE_EQUAL(1u, vExpectedInput.size()); bool foundEqual = false; INPUT_RECORD irExpected = vExpectedInput.back(); @@ -268,6 +279,9 @@ class Microsoft::Console::VirtualTerminal::InputEngineTest TEST_METHOD(TestSs3Immediate); TEST_METHOD(TestSs3Param); + TEST_METHOD(TestWin32InputParsing); + TEST_METHOD(TestWin32InputOptionals); + friend class TestInteractDispatch; }; @@ -310,7 +324,8 @@ class Microsoft::Console::VirtualTerminal::TestInteractDispatch final : public I TestInteractDispatch(_In_ std::function>&)> pfn, _In_ TestState* testState); virtual bool WriteInput(_In_ std::deque>& inputEvents) override; - virtual bool WriteCtrlC() override; + + virtual bool WriteCtrlKey(const KeyEvent& event) override; virtual bool WindowManipulation(const DispatchTypes::WindowManipulationType function, const std::basic_string_view parameters) override; // DTTERM_WindowManipulation virtual bool WriteString(const std::wstring_view string) override; @@ -338,14 +353,11 @@ bool TestInteractDispatch::WriteInput(_In_ std::deque_expectSendCtrlC); - KeyEvent keyDown = KeyEvent(true, 1, 'C', 0, UNICODE_ETX, LEFT_CTRL_PRESSED); - KeyEvent keyUp = KeyEvent(false, 1, 'C', 0, UNICODE_ETX, LEFT_CTRL_PRESSED); std::deque> inputEvents; - inputEvents.push_back(std::make_unique(keyDown)); - inputEvents.push_back(std::make_unique(keyUp)); + inputEvents.push_back(std::make_unique(event)); return WriteInput(inputEvents); } @@ -1398,3 +1410,154 @@ void InputEngineTest::TestSs3Param() mach.ProcessCharacter(L'J'); VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Ground); } + +void InputEngineTest::TestWin32InputParsing() +{ + auto pfn = std::bind(&TestState::TestInputCallback, &testState, std::placeholders::_1); + auto dispatch = std::make_unique(pfn, &testState); + auto engine = std::make_unique(std::move(dispatch)); + + { + KeyEvent key{}; + std::vector params{ 1 }; + VERIFY_IS_TRUE(engine->_GenerateWin32Key({ params.data(), params.size() }, key)); + VERIFY_ARE_EQUAL(1, key.GetVirtualKeyCode()); + VERIFY_ARE_EQUAL(0, key.GetVirtualScanCode()); + VERIFY_ARE_EQUAL(L'\0', key.GetCharData()); + VERIFY_ARE_EQUAL(false, key.IsKeyDown()); + VERIFY_ARE_EQUAL(0u, key.GetActiveModifierKeys()); + VERIFY_ARE_EQUAL(1, key.GetRepeatCount()); + } + { + KeyEvent key{}; + std::vector params{ 1, 2 }; + VERIFY_IS_TRUE(engine->_GenerateWin32Key({ params.data(), params.size() }, key)); + VERIFY_ARE_EQUAL(1, key.GetVirtualKeyCode()); + VERIFY_ARE_EQUAL(2, key.GetVirtualScanCode()); + VERIFY_ARE_EQUAL(L'\0', key.GetCharData()); + VERIFY_ARE_EQUAL(false, key.IsKeyDown()); + VERIFY_ARE_EQUAL(0u, key.GetActiveModifierKeys()); + VERIFY_ARE_EQUAL(1, key.GetRepeatCount()); + } + { + KeyEvent key{}; + std::vector params{ 1, 2, 3 }; + VERIFY_IS_TRUE(engine->_GenerateWin32Key({ params.data(), params.size() }, key)); + VERIFY_ARE_EQUAL(1, key.GetVirtualKeyCode()); + VERIFY_ARE_EQUAL(2, key.GetVirtualScanCode()); + VERIFY_ARE_EQUAL(L'\x03', key.GetCharData()); + VERIFY_ARE_EQUAL(false, key.IsKeyDown()); + VERIFY_ARE_EQUAL(0u, key.GetActiveModifierKeys()); + VERIFY_ARE_EQUAL(1, key.GetRepeatCount()); + } + { + KeyEvent key{}; + std::vector params{ 1, 2, 3, 4 }; + VERIFY_IS_TRUE(engine->_GenerateWin32Key({ params.data(), params.size() }, key)); + VERIFY_ARE_EQUAL(1, key.GetVirtualKeyCode()); + VERIFY_ARE_EQUAL(2, key.GetVirtualScanCode()); + VERIFY_ARE_EQUAL(L'\x03', key.GetCharData()); + VERIFY_ARE_EQUAL(true, key.IsKeyDown()); + VERIFY_ARE_EQUAL(0u, key.GetActiveModifierKeys()); + VERIFY_ARE_EQUAL(1, key.GetRepeatCount()); + } + { + KeyEvent key{}; + std::vector params{ 1, 2, 3, 1 }; + VERIFY_IS_TRUE(engine->_GenerateWin32Key({ params.data(), params.size() }, key)); + VERIFY_ARE_EQUAL(1, key.GetVirtualKeyCode()); + VERIFY_ARE_EQUAL(2, key.GetVirtualScanCode()); + VERIFY_ARE_EQUAL(L'\x03', key.GetCharData()); + VERIFY_ARE_EQUAL(true, key.IsKeyDown()); + VERIFY_ARE_EQUAL(0u, key.GetActiveModifierKeys()); + VERIFY_ARE_EQUAL(1, key.GetRepeatCount()); + } + { + KeyEvent key{}; + std::vector params{ 1, 2, 3, 4, 5 }; + VERIFY_IS_TRUE(engine->_GenerateWin32Key({ params.data(), params.size() }, key)); + VERIFY_ARE_EQUAL(1, key.GetVirtualKeyCode()); + VERIFY_ARE_EQUAL(2, key.GetVirtualScanCode()); + VERIFY_ARE_EQUAL(L'\x03', key.GetCharData()); + VERIFY_ARE_EQUAL(true, key.IsKeyDown()); + VERIFY_ARE_EQUAL(0x5u, key.GetActiveModifierKeys()); + VERIFY_ARE_EQUAL(1, key.GetRepeatCount()); + } + { + KeyEvent key{}; + std::vector params{ 1, 2, 3, 4, 5, 6 }; + VERIFY_IS_TRUE(engine->_GenerateWin32Key({ params.data(), params.size() }, key)); + VERIFY_ARE_EQUAL(1, key.GetVirtualKeyCode()); + VERIFY_ARE_EQUAL(2, key.GetVirtualScanCode()); + VERIFY_ARE_EQUAL(L'\x03', key.GetCharData()); + VERIFY_ARE_EQUAL(true, key.IsKeyDown()); + VERIFY_ARE_EQUAL(0x5u, key.GetActiveModifierKeys()); + VERIFY_ARE_EQUAL(6, key.GetRepeatCount()); + } + { + KeyEvent key{}; + std::vector params{ 1, 2, 3, 4, 5, 6, 7 }; + VERIFY_IS_FALSE(engine->_GenerateWin32Key({ params.data(), params.size() }, key)); + } +} + +void InputEngineTest::TestWin32InputOptionals() +{ + // Send a bunch of possible sets of parameters, to see if they all parse correctly. + + BEGIN_TEST_METHOD_PROPERTIES() + TEST_METHOD_PROPERTY(L"Data:provideVirtualKeyCode", L"{false, true}") + TEST_METHOD_PROPERTY(L"Data:provideVirtualScanCode", L"{false, true}") + TEST_METHOD_PROPERTY(L"Data:provideCharData", L"{false, true}") + TEST_METHOD_PROPERTY(L"Data:provideKeyDown", L"{false, true}") + TEST_METHOD_PROPERTY(L"Data:provideActiveModifierKeys", L"{false, true}") + TEST_METHOD_PROPERTY(L"Data:provideRepeatCount", L"{false, true}") + TEST_METHOD_PROPERTY(L"Data:numParams", L"{0, 1, 2, 3, 4, 5, 6}") + END_TEST_METHOD_PROPERTIES(); + + INIT_TEST_PROPERTY(bool, provideVirtualKeyCode, L"If true, pass the VirtualKeyCode param in the list of params. Otherwise, leave it as the default param value (0)"); + INIT_TEST_PROPERTY(bool, provideVirtualScanCode, L"If true, pass the VirtualScanCode param in the list of params. Otherwise, leave it as the default param value (0)"); + INIT_TEST_PROPERTY(bool, provideCharData, L"If true, pass the CharData param in the list of params. Otherwise, leave it as the default param value (0)"); + INIT_TEST_PROPERTY(bool, provideKeyDown, L"If true, pass the KeyDown param in the list of params. Otherwise, leave it as the default param value (0)"); + INIT_TEST_PROPERTY(bool, provideActiveModifierKeys, L"If true, pass the ActiveModifierKeys param in the list of params. Otherwise, leave it as the default param value (0)"); + INIT_TEST_PROPERTY(bool, provideRepeatCount, L"If true, pass the RepeatCount param in the list of params. Otherwise, leave it as the default param value (0)"); + INIT_TEST_PROPERTY(int, numParams, L"Control how many of the params we send"); + + auto pfn = std::bind(&TestState::TestInputCallback, &testState, std::placeholders::_1); + auto dispatch = std::make_unique(pfn, &testState); + auto engine = std::make_unique(std::move(dispatch)); + + { + KeyEvent key{}; + std::vector params{ + ::base::saturated_cast(provideVirtualKeyCode ? 1 : 0), + ::base::saturated_cast(provideVirtualScanCode ? 2 : 0), + ::base::saturated_cast(provideCharData ? 3 : 0), + ::base::saturated_cast(provideKeyDown ? 4 : 0), + ::base::saturated_cast(provideActiveModifierKeys ? 5 : 0), + ::base::saturated_cast(provideRepeatCount ? 6 : 0) + }; + + VERIFY_IS_TRUE(engine->_GenerateWin32Key({ params.data(), static_cast(numParams) }, key)); + VERIFY_ARE_EQUAL((provideVirtualKeyCode && numParams > 0) ? 1 : 0, + key.GetVirtualKeyCode()); + VERIFY_ARE_EQUAL((provideVirtualScanCode && numParams > 1) ? 2 : 0, + key.GetVirtualScanCode()); + VERIFY_ARE_EQUAL((provideCharData && numParams > 2) ? L'\x03' : L'\0', + key.GetCharData()); + VERIFY_ARE_EQUAL((provideKeyDown && numParams > 3) ? true : false, + key.IsKeyDown()); + VERIFY_ARE_EQUAL((provideActiveModifierKeys && numParams > 4) ? 5u : 0u, + key.GetActiveModifierKeys()); + if (numParams == 6) + { + VERIFY_ARE_EQUAL((provideRepeatCount) ? 6 : 0, + key.GetRepeatCount()); + } + else + { + VERIFY_ARE_EQUAL((provideRepeatCount && numParams > 5) ? 6 : 1, + key.GetRepeatCount()); + } + } +} diff --git a/src/terminal/parser/ut_parser/OutputEngineTest.cpp b/src/terminal/parser/ut_parser/OutputEngineTest.cpp index 09c116ab..c5e9a611 100644 --- a/src/terminal/parser/ut_parser/OutputEngineTest.cpp +++ b/src/terminal/parser/ut_parser/OutputEngineTest.cpp @@ -611,6 +611,7 @@ class StatefulDispatch final : public TermDispatch _numTabs{ 0 }, _isDECCOLMAllowed{ false }, _windowWidth{ 80 }, + _win32InputMode{ false }, _options{ s_cMaxOptions, static_cast(s_uiGraphicsCleared) } // fill with cleared option { } @@ -813,6 +814,9 @@ class StatefulDispatch final : public TermDispatch case DispatchTypes::PrivateModeParams::ASB_AlternateScreenBuffer: fSuccess = fEnable ? UseAlternateScreenBuffer() : UseMainScreenBuffer(); break; + case DispatchTypes::PrivateModeParams::W32IM_Win32InputMode: + fSuccess = EnableWin32InputMode(fEnable); + break; default: // If no functions to call, overall dispatch was a failure. fSuccess = false; @@ -854,6 +858,12 @@ class StatefulDispatch final : public TermDispatch return true; } + bool EnableWin32InputMode(const bool enable) noexcept + { + _win32InputMode = enable; + return true; + } + bool EnableCursorBlinking(const bool bEnable) noexcept override { _cursorBlinking = bEnable; @@ -977,6 +987,7 @@ class StatefulDispatch final : public TermDispatch size_t _numTabs; bool _isDECCOLMAllowed; size_t _windowWidth; + bool _win32InputMode; static const size_t s_cMaxOptions = 16; static const size_t s_uiGraphicsCleared = UINT_MAX; diff --git a/src/types/inc/IInputEvent.hpp b/src/types/inc/IInputEvent.hpp index 4d5ebf35..95b54014 100644 --- a/src/types/inc/IInputEvent.hpp +++ b/src/types/inc/IInputEvent.hpp @@ -177,11 +177,41 @@ class KeyEvent : public IInputEvent { } + static std::pair MakePair( + const WORD repeatCount, + const WORD virtualKeyCode, + const WORD virtualScanCode, + const wchar_t charData, + const DWORD activeModifierKeys) + { + return std::make_pair( + { true, + repeatCount, + virtualKeyCode, + virtualScanCode, + charData, + activeModifierKeys }, + { false, + repeatCount, + virtualKeyCode, + virtualScanCode, + charData, + activeModifierKeys }); + } + ~KeyEvent(); KeyEvent(const KeyEvent&) = default; KeyEvent(KeyEvent&&) = default; +// For these two operators, there seems to be a bug in the compiler: +// See https://stackoverflow.com/a/60206505/1481137 +// > C.128 applies only to virtual member functions, but operator= is not +// > virtual in your base class and neither does it have the same signature as +// > in the derived class, so there is no reason for it to apply. +#pragma warning(push) +#pragma warning(disable : 26456) KeyEvent& operator=(const KeyEvent&) & = default; KeyEvent& operator=(KeyEvent&&) & = default; +#pragma warning(pop) INPUT_RECORD ToInputRecord() const noexcept override; InputEventType EventType() const noexcept override; diff --git a/src/winconpty/winconpty.cpp b/src/winconpty/winconpty.cpp index ed250b55..53bebdb0 100644 --- a/src/winconpty/winconpty.cpp +++ b/src/winconpty/winconpty.cpp @@ -83,16 +83,18 @@ HRESULT _CreatePseudoConsole(const HANDLE hToken, RETURN_IF_WIN32_BOOL_FALSE(SetHandleInformation(signalPipeConhostSide.get(), HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT)); // GH4061: Ensure that the path to executable in the format is escaped so C:\Program.exe cannot collide with C:\Program Files - const wchar_t* pwszFormat = L"\"%s\" --headless %s%s--width %hu --height %hu --signal 0x%x --server 0x%x"; + const wchar_t* pwszFormat = L"\"%s\" --headless %s%s%s--width %hu --height %hu --signal 0x%x --server 0x%x"; // This is plenty of space to hold the formatted string wchar_t cmd[MAX_PATH]{}; const BOOL bInheritCursor = (dwFlags & PSEUDOCONSOLE_INHERIT_CURSOR) == PSEUDOCONSOLE_INHERIT_CURSOR; const BOOL bResizeQuirk = (dwFlags & PSEUDOCONSOLE_RESIZE_QUIRK) == PSEUDOCONSOLE_RESIZE_QUIRK; + const BOOL bWin32InputMode = (dwFlags & PSEUDOCONSOLE_WIN32_INPUT_MODE) == PSEUDOCONSOLE_WIN32_INPUT_MODE; swprintf_s(cmd, MAX_PATH, pwszFormat, _ConsoleHostPath(), bInheritCursor ? L"--inheritcursor " : L"", + bWin32InputMode ? L"--win32input " : L"", bResizeQuirk ? L"--resizeQuirk " : L"", size.X, size.Y, diff --git a/src/winconpty/winconpty.h b/src/winconpty/winconpty.h index 53c2279a..0916bcd6 100644 --- a/src/winconpty/winconpty.h +++ b/src/winconpty/winconpty.h @@ -23,6 +23,7 @@ typedef struct _PseudoConsole // The other flag (PSEUDOCONSOLE_INHERIT_CURSOR) is actually defined in consoleapi.h in the OS repo // #define PSEUDOCONSOLE_INHERIT_CURSOR (0x1) #define PSEUDOCONSOLE_RESIZE_QUIRK (0x2) +#define PSEUDOCONSOLE_WIN32_INPUT_MODE (0x4) // Implementations of the various PseudoConsole functions. HRESULT _CreatePseudoConsole(const HANDLE hToken,