Repository: microsoft/terminal (language: cpp)

Issue:
Add support for DECARM (Auto Repeat Mode)
# Description of the new feature/enhancement

_Auto Repeat Mode_ controls whether a keypress automatically repeats if you keep it held down for long enough. Technically it's a level 1 conformance requirement, and has been supported on every DEC terminal from the VT100 onwards.

My personal use case was for a game I was working on, where holding down a key for too long could end up generating spurious keypresses, and the hero would accidentally throw themselves off the edge of a cliff. Being able to disable the auto repeat makes that less of an issue.

# Proposed technical implementation details (optional)

Every time we receive a keydown event, we record the virtual key code to track that as the last key pressed. If we receive a keyup event the matches that last key code, we reset that field. Then if the auto repeat mode is *reset*, and we receive a keydown event that matches the last key code, we simply ignore it.

This approach works great in conhost, but not so well in Windows Terminal (because of issue #8440). It does work for the arrow keys, though, which is my primary use case. So the question is, would you be happy to accept a PR for this, even if it doesn't fully work in Windows Terminal? Or do we need to fix #8440 first?


Discussion / hints:

Fix patch:
diff --git a/src/terminal/adapter/DispatchTypes.hpp b/src/terminal/adapter/DispatchTypes.hpp
index 5a0bf610eb6..1a89789b905 100644
--- a/src/terminal/adapter/DispatchTypes.hpp
+++ b/src/terminal/adapter/DispatchTypes.hpp
@@ -381,6 +381,7 @@ namespace Microsoft::Console::VirtualTerminal::DispatchTypes
         DECSCNM_ScreenMode = DECPrivateMode(5),
         DECOM_OriginMode = DECPrivateMode(6),
         DECAWM_AutoWrapMode = DECPrivateMode(7),
+        DECARM_AutoRepeatMode = DECPrivateMode(8),
         ATT610_StartCursorBlink = DECPrivateMode(12),
         DECTCEM_TextCursorEnableMode = DECPrivateMode(25),
         XTERM_EnableDECCOLMSupport = DECPrivateMode(40),
diff --git a/src/terminal/adapter/adaptDispatch.cpp b/src/terminal/adapter/adaptDispatch.cpp
index eb6546921a4..5f7da549392 100644
--- a/src/terminal/adapter/adaptDispatch.cpp
+++ b/src/terminal/adapter/adaptDispatch.cpp
@@ -1013,6 +1013,9 @@ bool AdaptDispatch::_ModeParamsHelper(const DispatchTypes::ModeParams param, con
     case DispatchTypes::ModeParams::DECAWM_AutoWrapMode:
         success = SetAutoWrapMode(enable);
         break;
+    case DispatchTypes::ModeParams::DECARM_AutoRepeatMode:
+        success = _SetInputMode(TerminalInput::Mode::AutoRepeat, enable);
+        break;
     case DispatchTypes::ModeParams::ATT610_StartCursorBlink:
         success = EnableCursorBlinking(enable);
         break;
@@ -1864,6 +1867,9 @@ bool AdaptDispatch::HardReset()
     // Reset the Backarrow Key mode
     _SetInputMode(TerminalInput::Mode::BackarrowKey, false);
 
+    // Set the keyboard Auto Repeat mode
+    _SetInputMode(TerminalInput::Mode::AutoRepeat, true);
+
     // Delete all current tab stops and reapply
     _ResetTabStops();
 
diff --git a/src/terminal/adapter/ut_adapter/inputTest.cpp b/src/terminal/adapter/ut_adapter/inputTest.cpp
index f1c9bf2698b..1dd91d89b16 100644
--- a/src/terminal/adapter/ut_adapter/inputTest.cpp
+++ b/src/terminal/adapter/ut_adapter/inputTest.cpp
@@ -44,6 +44,7 @@ class Microsoft::Console::VirtualTerminal::InputTest
     TEST_METHOD(DifferentModifiersTest);
     TEST_METHOD(CtrlNumTest);
     TEST_METHOD(BackarrowKeyModeTest);
+    TEST_METHOD(AutoRepeatModeTest);
 
     wchar_t GetModifierChar(const bool fShift, const bool fAlt, const bool fCtrl)
     {
@@ -851,3 +852,56 @@ void InputTest::BackarrowKeyModeTest()
     s_expectedInput = L"\x1b\x8";
     TestKey(pInput, LEFT_ALT_PRESSED | LEFT_CTRL_PRESSED | SHIFT_PRESSED, vkey);
 }
+
+void InputTest::AutoRepeatModeTest()
+{
+    Log::Comment(L"Starting test...");
+
+    auto receivedChars = std::wstring{};
+    const auto pInput = new TerminalInput([&](auto& inEvents) {
+        for (auto& record : IInputEvent::ToInputRecords(inEvents))
+        {
+            receivedChars.push_back(record.Event.KeyEvent.uChar.UnicodeChar);
+        }
+    });
+
+    const auto repeatKey = [&](auto vKey, auto unicodeChar, int repeatCount) {
+        auto irTest = INPUT_RECORD{ 0 };
+        irTest.EventType = KEY_EVENT;
+        irTest.Event.KeyEvent.wRepeatCount = 1;
+        irTest.Event.KeyEvent.wVirtualKeyCode = vKey;
+        irTest.Event.KeyEvent.uChar.UnicodeChar = unicodeChar;
+        irTest.Event.KeyEvent.bKeyDown = TRUE;
+
+        // We're simulating a key being held down so that it repeats, by sending
+        // multiple KeyDown events followed by a single KeyUp event.
+
+        for (auto i = 0; i < repeatCount; i++)
+        {
+            auto keyDownEvent = IInputEvent::Create(irTest);
+            VERIFY_IS_TRUE(pInput->HandleKey(keyDownEvent.get()));
+        }
+
+        irTest.Event.KeyEvent.bKeyDown = FALSE;
+        auto keyUpEvent = IInputEvent::Create(irTest);
+        VERIFY_IS_FALSE(pInput->HandleKey(keyUpEvent.get()));
+    };
+
+    Log::Comment(L"Sending repeating keypresses with DECARM disabled.");
+
+    pInput->SetInputMode(TerminalInput::Mode::AutoRepeat, false);
+    receivedChars.clear();
+    repeatKey('A', L'a', 5);
+    repeatKey('B', L'b', 5);
+    repeatKey('C', L'c', 5);
+    VERIFY_ARE_EQUAL(L"abc", receivedChars);
+
+    Log::Comment(L"Sending repeating keypresses with DECARM enabled.");
+
+    pInput->SetInputMode(TerminalInput::Mode::AutoRepeat, true);
+    receivedChars.clear();
+    repeatKey('A', L'a', 5);
+    repeatKey('B', L'b', 5);
+    repeatKey('C', L'c', 5);
+    VERIFY_ARE_EQUAL(L"aaaaabbbbbccccc", receivedChars);
+}
diff --git a/src/terminal/input/terminalInput.cpp b/src/terminal/input/terminalInput.cpp
index 19f9104eb44..0e66d9ddfe8 100644
--- a/src/terminal/input/terminalInput.cpp
+++ b/src/terminal/input/terminalInput.cpp
@@ -553,9 +553,24 @@ bool TerminalInput::HandleKey(const IInputEvent* const pInEvent)
     // Only need to handle key down. See raw key handler (see RawReadWaitRoutine in stream.cpp)
     if (!keyEvent.IsKeyDown())
     {
+        // If this is a release of the last recorded key press, we can reset that.
+        if (keyEvent.GetVirtualKeyCode() == _lastVirtualKeyCode)
+        {
+            _lastVirtualKeyCode = 0;
+        }
         return false;
     }
 
+    // If this is a repeat of the last recorded key press, and Auto Repeat Mode
+    // is disabled, then we should suppress this event.
+    if (keyEvent.GetVirtualKeyCode() == _lastVirtualKeyCode && !_inputMode.test(Mode::AutoRepeat))
+    {
+        // Note that we must return true here to say we've handled the event,
+        // otherwise the key press can still end up being submitted.
+        return true;
+    }
+    _lastVirtualKeyCode = keyEvent.GetVirtualKeyCode();
+
     // The VK_BACK key depends on the state of Backarrow Key mode (DECBKM).
     // If the mode is set, we should send BS. If reset, we should send DEL.
     if (keyEvent.GetVirtualKeyCode() == VK_BACK)
diff --git a/src/terminal/input/terminalInput.hpp b/src/terminal/input/terminalInput.hpp
index 80ff4e8c06f..70fc0e6b62e 100644
--- a/src/terminal/input/terminalInput.hpp
+++ b/src/terminal/input/terminalInput.hpp
@@ -39,6 +39,7 @@ namespace Microsoft::Console::VirtualTerminal
         enum class Mode : size_t
         {
             Ansi,
+            AutoRepeat,
             Keypad,
             CursorKey,
             BackarrowKey,
@@ -92,7 +93,9 @@ namespace Microsoft::Console::VirtualTerminal
         // storage location for the leading surrogate of a utf-16 surrogate pair
         std::optional<wchar_t> _leadingSurrogate;
 
-        til::enumset<Mode> _inputMode{ Mode::Ansi };
+        WORD _lastVirtualKeyCode{ 0 };
+
+        til::enumset<Mode> _inputMode{ Mode::Ansi, Mode::AutoRepeat };
         bool _forceDisableWin32InputMode{ false };
 
         void _SendChar(const wchar_t ch);


Test patch: