Repository: microsoft/terminal (language: cpp)

Issue:
Add support for LNM (Line Feed/New Line Mode)
# Description of the new feature/enhancement

When [`LNM`](https://vt100.net/docs/vt510-rm/LNM.html) is set, the linefeed controls (`LF`, `FF`, and `VT`) will also trigger a carriage return. When reset they, they won't. This is essentially the inverse of our existing console mode, `DISABLE_NEWLINE_AUTO_RETURN`.

However, `LNM` has an additional effect on input. When set, the <kbd>Return</kbd> key generates both a carriage return and a line feed. When reset it only generates the carriage return.

It's quite widely implemented, but I don't think VTE supports it, which suggests it's probably not that widely used. However, it is a requirement for meeting VT level 1 conformance, which is why I would like for us to support it.

# Proposed technical implementation details (optional)

I had originally thought we could map it directly to the `DISABLE_NEWLINE_AUTO_RETURN` mode, since we're already using that to determine how linefeed controls are interpreted. However, when a Windows console app has `DISABLE_NEWLINE_AUTO_RETURN` reset (which is the equivalent of `LNM` being set), we don't typically want the <kbd>Return</kbd> key to behave differently.

So my idea was this: We add a new input mode that specifically handles the `LNM` behavior for the <kbd>Return</kbd> key, which by default is disabled. And we treat the `DISABLE_NEWLINE_AUTO_RETURN` mode as an inverse alias for the `LNM` output behavior (as we already do).

When those two match, we're in a valid VT state, and we can use the `LNM` mode to toggle them both at the same time. But when they're out of sync (which is the default state for a Windows console app), we just act as if the `LNM` mode is not supported, i.e. we don't respond to any attempts to change it, and `DECRQM` reports the mode as unknown.

Does that seem like a reasonable approach to take?


Discussion / hints:

Fix patch:
diff --git a/src/cascadia/TerminalCore/Terminal.cpp b/src/cascadia/TerminalCore/Terminal.cpp
index 99fa8453472..f4b7482cf49 100644
--- a/src/cascadia/TerminalCore/Terminal.cpp
+++ b/src/cascadia/TerminalCore/Terminal.cpp
@@ -236,7 +236,7 @@ void Terminal::EraseScrollback()
 
 bool Terminal::IsXtermBracketedPasteModeEnabled() const noexcept
 {
-    return _bracketedPasteMode;
+    return _systemMode.test(Mode::BracketedPaste);
 }
 
 std::wstring_view Terminal::GetWorkingDirectory() noexcept
diff --git a/src/cascadia/TerminalCore/Terminal.hpp b/src/cascadia/TerminalCore/Terminal.hpp
index d03d927c19b..1c8e36d72b4 100644
--- a/src/cascadia/TerminalCore/Terminal.hpp
+++ b/src/cascadia/TerminalCore/Terminal.hpp
@@ -111,17 +111,14 @@ class Microsoft::Terminal::Core::Terminal final :
     til::rect GetViewport() const noexcept override;
     void SetViewportPosition(const til::point position) noexcept override;
     void SetTextAttributes(const TextAttribute& attrs) noexcept override;
-    void SetAutoWrapMode(const bool wrapAtEOL) noexcept override;
-    bool GetAutoWrapMode() const noexcept override;
+    void SetSystemMode(const Mode mode, const bool enabled) noexcept override;
+    bool GetSystemMode(const Mode mode) const noexcept override;
     void WarningBell() override;
-    bool GetLineFeedMode() const noexcept override;
     void SetWindowTitle(const std::wstring_view title) override;
     CursorType GetUserDefaultCursorStyle() const noexcept override;
     bool ResizeWindow(const til::CoordType width, const til::CoordType height) noexcept override;
     void SetConsoleOutputCP(const unsigned int codepage) noexcept override;
     unsigned int GetConsoleOutputCP() const noexcept override;
-    void SetBracketedPasteMode(const bool enabled) noexcept override;
-    bool GetBracketedPasteMode() const noexcept override;
     void CopyToClipboard(std::wstring_view content) override;
     void SetTaskbarProgress(const ::Microsoft::Console::VirtualTerminal::DispatchTypes::TaskbarState state, const size_t progress) override;
     void SetWorkingDirectory(std::wstring_view uri) override;
@@ -320,10 +317,11 @@ class Microsoft::Terminal::Core::Terminal final :
 
     CursorType _defaultCursorShape = CursorType::Legacy;
 
+    til::enumset<Mode> _systemMode{ Mode::AutoWrap };
+
     bool _snapOnInput = true;
     bool _altGrAliasing = true;
     bool _suppressApplicationTitle = false;
-    bool _bracketedPasteMode = false;
     bool _trimBlockSelection = false;
     bool _autoMarkPrompts = false;
 
diff --git a/src/cascadia/TerminalCore/TerminalApi.cpp b/src/cascadia/TerminalCore/TerminalApi.cpp
index 3c211d69631..28b320f638c 100644
--- a/src/cascadia/TerminalCore/TerminalApi.cpp
+++ b/src/cascadia/TerminalCore/TerminalApi.cpp
@@ -61,15 +61,14 @@ void Terminal::SetTextAttributes(const TextAttribute& attrs) noexcept
     _activeBuffer().SetCurrentAttributes(attrs);
 }
 
-void Terminal::SetAutoWrapMode(const bool /*wrapAtEOL*/) noexcept
+void Terminal::SetSystemMode(const Mode mode, const bool enabled) noexcept
 {
-    // TODO: This will be needed to support DECAWM.
+    _systemMode.set(mode, enabled);
 }
 
-bool Terminal::GetAutoWrapMode() const noexcept
+bool Terminal::GetSystemMode(const Mode mode) const noexcept
 {
-    // TODO: This will be needed to support DECAWM.
-    return true;
+    return _systemMode.test(mode);
 }
 
 void Terminal::WarningBell()
@@ -77,12 +76,6 @@ void Terminal::WarningBell()
     _pfnWarningBell();
 }
 
-bool Terminal::GetLineFeedMode() const noexcept
-{
-    // TODO: This will be needed to support LNM.
-    return false;
-}
-
 void Terminal::SetWindowTitle(const std::wstring_view title)
 {
     if (!_suppressApplicationTitle)
@@ -114,16 +107,6 @@ unsigned int Terminal::GetConsoleOutputCP() const noexcept
     return CP_UTF8;
 }
 
-void Terminal::SetBracketedPasteMode(const bool enabled) noexcept
-{
-    _bracketedPasteMode = enabled;
-}
-
-bool Terminal::GetBracketedPasteMode() const noexcept
-{
-    return _bracketedPasteMode;
-}
-
 void Terminal::CopyToClipboard(std::wstring_view content)
 {
     _pfnCopyToClipboard(content);
diff --git a/src/host/outputStream.cpp b/src/host/outputStream.cpp
index 2bf21f7dbfa..58707827cb2 100644
--- a/src/host/outputStream.cpp
+++ b/src/host/outputStream.cpp
@@ -114,40 +114,49 @@ void ConhostInternalGetSet::SetTextAttributes(const TextAttribute& attrs)
 }
 
 // Routine Description:
-// - Sets the ENABLE_WRAP_AT_EOL_OUTPUT mode. This controls whether the cursor moves
-//     to the beginning of the next row when it reaches the end of the current row.
+// - Sets the state of one of the system modes.
 // Arguments:
-// - wrapAtEOL - set to true to wrap, false to overwrite the last character.
+// - mode - The mode being updated.
+// - enabled - True to enable the mode, false to disable it.
 // Return Value:
 // - <none>
-void ConhostInternalGetSet::SetAutoWrapMode(const bool wrapAtEOL)
+void ConhostInternalGetSet::SetSystemMode(const Mode mode, const bool enabled)
 {
-    auto& outputMode = _io.GetActiveOutputBuffer().OutputMode;
-    WI_UpdateFlag(outputMode, ENABLE_WRAP_AT_EOL_OUTPUT, wrapAtEOL);
+    switch (mode)
+    {
+    case Mode::AutoWrap:
+        WI_UpdateFlag(_io.GetActiveOutputBuffer().OutputMode, ENABLE_WRAP_AT_EOL_OUTPUT, enabled);
+        break;
+    case Mode::LineFeed:
+        WI_UpdateFlag(_io.GetActiveOutputBuffer().OutputMode, DISABLE_NEWLINE_AUTO_RETURN, !enabled);
+        break;
+    case Mode::BracketedPaste:
+        ServiceLocator::LocateGlobals().getConsoleInformation().SetBracketedPasteMode(enabled);
+        break;
+    default:
+        THROW_HR(E_INVALIDARG);
+    }
 }
 
 // Routine Description:
-// - Retrieves the current state of ENABLE_WRAP_AT_EOL_OUTPUT mode.
+// - Retrieves the current state of one of the system modes.
 // Arguments:
-// - <none>
+// - mode - The mode being queried.
 // Return Value:
 // - true if the mode is enabled. false otherwise.
-bool ConhostInternalGetSet::GetAutoWrapMode() const
+bool ConhostInternalGetSet::GetSystemMode(const Mode mode) const
 {
-    const auto outputMode = _io.GetActiveOutputBuffer().OutputMode;
-    return WI_IsFlagSet(outputMode, ENABLE_WRAP_AT_EOL_OUTPUT);
-}
-
-// Method Description:
-// - Retrieves the current Line Feed/New Line (LNM) mode.
-// Arguments:
-// - None
-// Return Value:
-// - true if a line feed also produces a carriage return. false otherwise.
-bool ConhostInternalGetSet::GetLineFeedMode() const
-{
-    auto& screenInfo = _io.GetActiveOutputBuffer();
-    return WI_IsFlagClear(screenInfo.OutputMode, DISABLE_NEWLINE_AUTO_RETURN);
+    switch (mode)
+    {
+    case Mode::AutoWrap:
+        return WI_IsFlagSet(_io.GetActiveOutputBuffer().OutputMode, ENABLE_WRAP_AT_EOL_OUTPUT);
+    case Mode::LineFeed:
+        return WI_IsFlagClear(_io.GetActiveOutputBuffer().OutputMode, DISABLE_NEWLINE_AUTO_RETURN);
+    case Mode::BracketedPaste:
+        return ServiceLocator::LocateGlobals().getConsoleInformation().GetBracketedPasteMode();
+    default:
+        THROW_HR(E_INVALIDARG);
+    }
 }
 
 // Routine Description:
@@ -245,29 +254,6 @@ unsigned int ConhostInternalGetSet::GetConsoleOutputCP() const
     return ServiceLocator::LocateGlobals().getConsoleInformation().OutputCP;
 }
 
-// Routine Description:
-// - Sets the XTerm bracketed paste mode. This controls whether pasted content is
-//     bracketed with control sequences to differentiate it from typed text.
-// Arguments:
-// - enable - set to true to enable bracketing, false to disable.
-// Return Value:
-// - <none>
-void ConhostInternalGetSet::SetBracketedPasteMode(const bool enabled)
-{
-    ServiceLocator::LocateGlobals().getConsoleInformation().SetBracketedPasteMode(enabled);
-}
-
-// Routine Description:
-// - Gets the current state of XTerm bracketed paste mode.
-// Arguments:
-// - <none>
-// Return Value:
-// - true if the mode is enabled, false if not.
-bool ConhostInternalGetSet::GetBracketedPasteMode() const
-{
-    return ServiceLocator::LocateGlobals().getConsoleInformation().GetBracketedPasteMode();
-}
-
 // Routine Description:
 // - Copies the given content to the clipboard.
 // Arguments:
diff --git a/src/host/outputStream.hpp b/src/host/outputStream.hpp
index f555efbcb1a..6e0120cc997 100644
--- a/src/host/outputStream.hpp
+++ b/src/host/outputStream.hpp
@@ -38,13 +38,11 @@ class ConhostInternalGetSet final : public Microsoft::Console::VirtualTerminal::
 
     void SetTextAttributes(const TextAttribute& attrs) override;
 
-    void SetAutoWrapMode(const bool wrapAtEOL) override;
-    bool GetAutoWrapMode() const override;
+    void SetSystemMode(const Mode mode, const bool enabled) override;
+    bool GetSystemMode(const Mode mode) const override;
 
     void WarningBell() override;
 
-    bool GetLineFeedMode() const override;
-
     void SetWindowTitle(const std::wstring_view title) override;
 
     void UseAlternateScreenBuffer() override;
@@ -60,8 +58,6 @@ class ConhostInternalGetSet final : public Microsoft::Console::VirtualTerminal::
     void SetConsoleOutputCP(const unsigned int codepage) override;
     unsigned int GetConsoleOutputCP() const override;
 
-    void SetBracketedPasteMode(const bool enabled) override;
-    bool GetBracketedPasteMode() const override;
     void CopyToClipboard(const std::wstring_view content) override;
     void SetTaskbarProgress(const ::Microsoft::Console::VirtualTerminal::DispatchTypes::TaskbarState state, const size_t progress) override;
     void SetWorkingDirectory(const std::wstring_view uri) override;
diff --git a/src/host/ut_host/ScreenBufferTests.cpp b/src/host/ut_host/ScreenBufferTests.cpp
index 48d4e277190..c6d38ad813b 100644
--- a/src/host/ut_host/ScreenBufferTests.cpp
+++ b/src/host/ut_host/ScreenBufferTests.cpp
@@ -201,6 +201,7 @@ class ScreenBufferTests
 
     TEST_METHOD(ScrollLines256Colors);
 
+    TEST_METHOD(SetLineFeedMode);
     TEST_METHOD(SetScreenMode);
     TEST_METHOD(SetOriginMode);
     TEST_METHOD(SetAutoWrapMode);
@@ -5156,6 +5157,28 @@ void ScreenBufferTests::ScrollLines256Colors()
     }
 }
 
+void ScreenBufferTests::SetLineFeedMode()
+{
+    auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
+    auto& si = gci.GetActiveOutputBuffer();
+    auto& stateMachine = si.GetStateMachine();
+    const auto& terminalInput = gci.GetActiveInputBuffer()->GetTerminalInput();
+
+    // We need to start with newline auto return disabled for LNM to be active.
+    WI_SetFlag(si.OutputMode, DISABLE_NEWLINE_AUTO_RETURN);
+    auto restoreMode = wil::scope_exit([&] { WI_ClearFlag(si.OutputMode, DISABLE_NEWLINE_AUTO_RETURN); });
+
+    Log::Comment(L"When LNM is set, newline auto return and line feed mode are enabled.");
+    stateMachine.ProcessString(L"\x1B[20h");
+    VERIFY_IS_TRUE(WI_IsFlagClear(si.OutputMode, DISABLE_NEWLINE_AUTO_RETURN));
+    VERIFY_IS_TRUE(terminalInput.GetInputMode(TerminalInput::Mode::LineFeed));
+
+    Log::Comment(L"When LNM is reset, newline auto return and line feed mode are disabled.");
+    stateMachine.ProcessString(L"\x1B[20l");
+    VERIFY_IS_FALSE(WI_IsFlagClear(si.OutputMode, DISABLE_NEWLINE_AUTO_RETURN));
+    VERIFY_IS_FALSE(terminalInput.GetInputMode(TerminalInput::Mode::LineFeed));
+}
+
 void ScreenBufferTests::SetScreenMode()
 {
     auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
diff --git a/src/terminal/adapter/DispatchTypes.hpp b/src/terminal/adapter/DispatchTypes.hpp
index b1af645cc4b..27252a87efc 100644
--- a/src/terminal/adapter/DispatchTypes.hpp
+++ b/src/terminal/adapter/DispatchTypes.hpp
@@ -418,6 +418,7 @@ namespace Microsoft::Console::VirtualTerminal::DispatchTypes
     enum ModeParams : VTInt
     {
         IRM_InsertReplaceMode = ANSIStandardMode(4),
+        LNM_LineFeedNewLineMode = ANSIStandardMode(20),
         DECCKM_CursorKeysMode = DECPrivateMode(1),
         DECANM_AnsiMode = DECPrivateMode(2),
         DECCOLM_SetNumberOfColumns = DECPrivateMode(3),
diff --git a/src/terminal/adapter/ITerminalApi.hpp b/src/terminal/adapter/ITerminalApi.hpp
index b0987a22014..cf32f308943 100644
--- a/src/terminal/adapter/ITerminalApi.hpp
+++ b/src/terminal/adapter/ITerminalApi.hpp
@@ -48,11 +48,17 @@ namespace Microsoft::Console::VirtualTerminal
 
         virtual void SetTextAttributes(const TextAttribute& attrs) = 0;
 
-        virtual void SetAutoWrapMode(const bool wrapAtEOL) = 0;
-        virtual bool GetAutoWrapMode() const = 0;
+        enum class Mode : size_t
+        {
+            AutoWrap,
+            LineFeed,
+            BracketedPaste
+        };
+
+        virtual void SetSystemMode(const Mode mode, const bool enabled) = 0;
+        virtual bool GetSystemMode(const Mode mode) const = 0;
 
         virtual void WarningBell() = 0;
-        virtual bool GetLineFeedMode() const = 0;
         virtual void SetWindowTitle(const std::wstring_view title) = 0;
         virtual void UseAlternateScreenBuffer() = 0;
         virtual void UseMainScreenBuffer() = 0;
@@ -64,8 +70,6 @@ namespace Microsoft::Console::VirtualTerminal
         virtual void SetConsoleOutputCP(const unsigned int codepage) = 0;
         virtual unsigned int GetConsoleOutputCP() const = 0;
 
-        virtual void SetBracketedPasteMode(const bool enabled) = 0;
-        virtual bool GetBracketedPasteMode() const = 0;
         virtual void CopyToClipboard(const std::wstring_view content) = 0;
         virtual void SetTaskbarProgress(const DispatchTypes::TaskbarState state, const size_t progress) = 0;
         virtual void SetWorkingDirectory(const std::wstring_view uri) = 0;
diff --git a/src/terminal/adapter/adaptDispatch.cpp b/src/terminal/adapter/adaptDispatch.cpp
index 6a4d7b8d672..d22b24ebfb6 100644
--- a/src/terminal/adapter/adaptDispatch.cpp
+++ b/src/terminal/adapter/adaptDispatch.cpp
@@ -74,7 +74,7 @@ void AdaptDispatch::_WriteToBuffer(const std::wstring_view string)
     auto& textBuffer = _api.GetTextBuffer();
     auto& cursor = textBuffer.GetCursor();
     auto cursorPosition = cursor.GetPosition();
-    const auto wrapAtEOL = _api.GetAutoWrapMode();
+    const auto wrapAtEOL = _api.GetSystemMode(ITerminalApi::Mode::AutoWrap);
     const auto attributes = textBuffer.GetCurrentAttributes();
 
     // Turn off the cursor until we're done, so it isn't refreshed unnecessarily.
@@ -1696,6 +1696,15 @@ bool AdaptDispatch::_ModeParamsHelper(const DispatchTypes::ModeParams param, con
     case DispatchTypes::ModeParams::IRM_InsertReplaceMode:
         _modes.set(Mode::InsertReplace, enable);
         return true;
+    case DispatchTypes::ModeParams::LNM_LineFeedNewLineMode:
+        // VT apps expect that the system and input modes are the same, so if
+        // they become out of sync, we just act as if LNM mode isn't supported.
+        if (_api.GetSystemMode(ITerminalApi::Mode::LineFeed) == _terminalInput.GetInputMode(TerminalInput::Mode::LineFeed))
+        {
+            _api.SetSystemMode(ITerminalApi::Mode::LineFeed, enable);
+            _terminalInput.SetInputMode(TerminalInput::Mode::LineFeed, enable);
+        }
+        return true;
     case DispatchTypes::ModeParams::DECCKM_CursorKeysMode:
         _terminalInput.SetInputMode(TerminalInput::Mode::CursorKey, enable);
         return !_PassThroughInputModes();
@@ -1719,7 +1728,7 @@ bool AdaptDispatch::_ModeParamsHelper(const DispatchTypes::ModeParams param, con
         CursorPosition(1, 1);
         return true;
     case DispatchTypes::ModeParams::DECAWM_AutoWrapMode:
-        _api.SetAutoWrapMode(enable);
+        _api.SetSystemMode(ITerminalApi::Mode::AutoWrap, enable);
         // Resetting DECAWM should also reset the delayed wrap flag.
         if (!enable)
         {
@@ -1771,7 +1780,7 @@ bool AdaptDispatch::_ModeParamsHelper(const DispatchTypes::ModeParams param, con
         _SetAlternateScreenBufferMode(enable);
         return true;
     case DispatchTypes::ModeParams::XTERM_BracketedPasteMode:
-        _api.SetBracketedPasteMode(enable);
+        _api.SetSystemMode(ITerminalApi::Mode::BracketedPaste, enable);
         return !_api.IsConsolePty();
     case DispatchTypes::ModeParams::W32IM_Win32InputMode:
         _terminalInput.SetInputMode(TerminalInput::Mode::Win32, enable);
@@ -1820,6 +1829,14 @@ bool AdaptDispatch::RequestMode(const DispatchTypes::ModeParams param)
     case DispatchTypes::ModeParams::IRM_InsertReplaceMode:
         enabled = _modes.test(Mode::InsertReplace);
         break;
+    case DispatchTypes::ModeParams::LNM_LineFeedNewLineMode:
+        // VT apps expect that the system and input modes are the same, so if
+        // they become out of sync, we just act as if LNM mode isn't supported.
+        if (_api.GetSystemMode(ITerminalApi::Mode::LineFeed) == _terminalInput.GetInputMode(TerminalInput::Mode::LineFeed))
+        {
+            enabled = _terminalInput.GetInputMode(TerminalInput::Mode::LineFeed);
+        }
+        break;
     case DispatchTypes::ModeParams::DECCKM_CursorKeysMode:
         enabled = _terminalInput.GetInputMode(TerminalInput::Mode::CursorKey);
         break;
@@ -1840,7 +1857,7 @@ bool AdaptDispatch::RequestMode(const DispatchTypes::ModeParams param)
         enabled = _modes.test(Mode::Origin);
         break;
     case DispatchTypes::ModeParams::DECAWM_AutoWrapMode:
-        enabled = _api.GetAutoWrapMode();
+        enabled = _api.GetSystemMode(ITerminalApi::Mode::AutoWrap);
         break;
     case DispatchTypes::ModeParams::DECARM_AutoRepeatMode:
         enabled = _terminalInput.GetInputMode(TerminalInput::Mode::AutoRepeat);
@@ -1889,7 +1906,7 @@ bool AdaptDispatch::RequestMode(const DispatchTypes::ModeParams param)
         enabled = _usingAltBuffer;
         break;
     case DispatchTypes::ModeParams::XTERM_BracketedPasteMode:
-        enabled = _api.GetBracketedPasteMode();
+        enabled = _api.GetSystemMode(ITerminalApi::Mode::BracketedPaste);
         break;
     case DispatchTypes::ModeParams::W32IM_Win32InputMode:
         enabled = _terminalInput.GetInputMode(TerminalInput::Mode::Win32);
@@ -2203,7 +2220,7 @@ bool AdaptDispatch::LineFeed(const DispatchTypes::LineFeedType lineFeedType)
     switch (lineFeedType)
     {
     case DispatchTypes::LineFeedType::DependsOnMode:
-        _DoLineFeed(textBuffer, _api.GetLineFeedMode(), false);
+        _DoLineFeed(textBuffer, _api.GetSystemMode(ITerminalApi::Mode::LineFeed), false);
         return true;
     case DispatchTypes::LineFeedType::WithoutReturn:
         _DoLineFeed(textBuffer, false, false);
@@ -2590,7 +2607,7 @@ bool AdaptDispatch::SoftReset()
 {
     _api.GetTextBuffer().GetCursor().SetIsVisible(true); // Cursor enabled.
     _modes.reset(Mode::InsertReplace, Mode::Origin); // Replace mode; Absolute cursor addressing.
-    _api.SetAutoWrapMode(true); // Wrap at end of line.
+    _api.SetSystemMode(ITerminalApi::Mode::AutoWrap, true); // Wrap at end of line.
     _terminalInput.SetInputMode(TerminalInput::Mode::CursorKey, false); // Normal characters.
     _terminalInput.SetInputMode(TerminalInput::Mode::Keypad, false); // Numeric characters.
 
@@ -2663,11 +2680,20 @@ bool AdaptDispatch::HardReset()
     // Cursor to 1,1 - the Soft Reset guarantees this is absolute
     CursorPosition(1, 1);
 
+    // We only reset the system line feed mode if the input mode is set. If it
+    // isn't set, that either means they're both reset, and there's nothing for
+    // us to do, or they're out of sync, which implies the system mode was set
+    // via the console API, so it's not our responsibility.
+    if (_terminalInput.GetInputMode(TerminalInput::Mode::LineFeed))
+    {
+        _api.SetSystemMode(ITerminalApi::Mode::LineFeed, false);
+    }
+
     // Reset input modes to their initial state
     _terminalInput.ResetInputModes();
 
     // Reset bracketed paste mode
-    _api.SetBracketedPasteMode(false);
+    _api.SetSystemMode(ITerminalApi::Mode::BracketedPaste, false);
 
     // Restore cursor blinking mode.
     _api.GetTextBuffer().GetCursor().SetBlinkingAllowed(true);
diff --git a/src/terminal/adapter/ut_adapter/adapterTest.cpp b/src/terminal/adapter/ut_adapter/adapterTest.cpp
index dcb917750d6..7a04ea59166 100644
--- a/src/terminal/adapter/ut_adapter/adapterTest.cpp
+++ b/src/terminal/adapter/ut_adapter/adapterTest.cpp
@@ -96,17 +96,6 @@ class TestGetSet final : public ITerminalApi
         Log::Comment(L"SetViewportPosition MOCK called...");
     }
 
-    void SetAutoWrapMode(const bool /*wrapAtEOL*/) override
-    {
-        Log::Comment(L"SetAutoWrapMode MOCK called...");
-    }
-
-    bool GetAutoWrapMode() const override
-    {
-        Log::Comment(L"GetAutoWrapMode MOCK called...");
-        return true;
-    }
-
     bool IsVtInputEnabled() const override
     {
         return false;
@@ -121,15 +110,21 @@ class TestGetSet final : public ITerminalApi
         _textBuffer->SetCurrentAttributes(attrs);
     }
 
-    void WarningBell() override
+    void SetSystemMode(const Mode mode, const bool enabled)
     {
-        Log::Comment(L"WarningBell MOCK called...");
+        Log::Comment(L"SetSystemMode MOCK called...");
+        _systemMode.set(mode, enabled);
+    }
+
+    bool GetSystemMode(const Mode mode) const
+    {
+        Log::Comment(L"GetSystemMode MOCK called...");
+        return _systemMode.test(mode);
     }
 
-    bool GetLineFeedMode() const override
+    void WarningBell() override
     {
-        Log::Comment(L"GetLineFeedMode MOCK called...");
-        return _getLineFeedModeResult;
+        Log::Comment(L"WarningBell MOCK called...");
     }
 
     void SetWindowTitle(const std::wstring_view title)
@@ -184,17 +179,6 @@ class TestGetSet final : public ITerminalApi
         return _expectedOutputCP;
     }
 
-    void SetBracketedPasteMode(const bool /*enabled*/) override
-    {
-        Log::Comment(L"SetBracketedPasteMode MOCK called...");
-    }
-
-    bool GetBracketedPasteMode() const override
-    {
-        Log::Comment(L"GetBracketedPasteMode MOCK called...");
-        return false;
-    }
-
     void CopyToClipboard(const std::wstring_view /*content*/)
     {
         Log::Comment(L"CopyToClipboard MOCK called...");
@@ -386,7 +370,7 @@ class TestGetSet final : public ITerminalApi
     bool _setTextAttributesResult = false;
     bool _returnResponseResult = false;
 
-    bool _getLineFeedModeResult = false;
+    til::enumset<Mode> _systemMode{ Mode::AutoWrap };
 
     bool _setWindowTitleResult = false;
     std::wstring_view _expectedWindowTitle{};
@@ -1755,7 +1739,39 @@ class AdapterTest
         _testGetSet->ValidateInputEvent(L"\033P0$r\033\\");
     }
 
-    TEST_METHOD(RequestModeTests)
+    TEST_METHOD(RequestStandardModeTests)
+    {
+        // The mode numbers below correspond to the ANSIStandardMode values
+        // in the ModeParams enum in DispatchTypes.hpp.
+
+        BEGIN_TEST_METHOD_PROPERTIES()
+            TEST_METHOD_PROPERTY(L"Data:modeNumber", L"{4, 20}")
+        END_TEST_METHOD_PROPERTIES()
+
+        VTInt modeNumber;
+        VERIFY_SUCCEEDED_RETURN(TestData::TryGetValue(L"modeNumber", modeNumber));
+        const auto mode = DispatchTypes::ANSIStandardMode(modeNumber);
+
+        // DISABLE_
+        Log::Comment(NoThrowString().Format(L"Setting standard mode %d", modeNumber));
+        _testGetSet->PrepData();
+        VERIFY_IS_TRUE(_pDispatch->SetMode(mode));
+        VERIFY_IS_TRUE(_pDispatch->RequestMode(mode));
+
+        wchar_t expectedResponse[20];
+        swprintf_s(expectedResponse, ARRAYSIZE(expectedResponse), L"\x1b[%d;1$y", modeNumber);
+        _testGetSet->ValidateInputEvent(expectedResponse);
+
+        Log::Comment(NoThrowString().Format(L"Resetting standard mode %d", modeNumber));
+        _testGetSet->PrepData();
+        VERIFY_IS_TRUE(_pDispatch->ResetMode(mode));
+        VERIFY_IS_TRUE(_pDispatch->RequestMode(mode));
+
+        swprintf_s(expectedResponse, ARRAYSIZE(expectedResponse), L"\x1b[%d;2$y", modeNumber);
+        _testGetSet->ValidateInputEvent(expectedResponse);
+    }
+
+    TEST_METHOD(RequestPrivateModeTests)
     {
         // The mode numbers below correspond to the DECPrivateMode values
         // in the ModeParams enum in DispatchTypes.hpp. We don't include
@@ -1763,7 +1779,7 @@ class AdapterTest
         // and DECRQM would not then be applicable.
 
         BEGIN_TEST_METHOD_PROPERTIES()
-            TEST_METHOD_PROPERTY(L"Data:modeNumber", L"{1, 3, 5, 6, 8, 12, 25, 40, 66, 67, 1000, 1002, 1003, 1004, 1005, 1006, 1007, 1049, 9001}")
+            TEST_METHOD_PROPERTY(L"Data:modeNumber", L"{1, 3, 5, 6, 7, 8, 12, 25, 40, 66, 67, 1000, 1002, 1003, 1004, 1005, 1006, 1007, 1049, 2004, 9001}")
         END_TEST_METHOD_PROPERTIES()
 
         VTInt modeNumber;
@@ -2262,13 +2278,13 @@ class AdapterTest
         VERIFY_ARE_EQUAL(til::point(0, 1), cursor.GetPosition());
 
         Log::Comment(L"Test 3: Line feed depends on mode, and mode reset.");
-        _testGetSet->_getLineFeedModeResult = false;
+        _testGetSet->_systemMode.reset(ITerminalApi::Mode::LineFeed);
         cursor.SetPosition({ 10, 0 });
         VERIFY_IS_TRUE(_pDispatch->LineFeed(DispatchTypes::LineFeedType::DependsOnMode));
         VERIFY_ARE_EQUAL(til::point(10, 1), cursor.GetPosition());
 
         Log::Comment(L"Test 4: Line feed depends on mode, and mode set.");
-        _testGetSet->_getLineFeedModeResult = true;
+        _testGetSet->_systemMode.set(ITerminalApi::Mode::LineFeed);
         cursor.SetPosition({ 10, 0 });
         VERIFY_IS_TRUE(_pDispatch->LineFeed(DispatchTypes::LineFeedType::DependsOnMode));
         VERIFY_ARE_EQUAL(til::point(0, 1), cursor.GetPosition());
diff --git a/src/terminal/input/terminalInput.cpp b/src/terminal/input/terminalInput.cpp
index 0af36efe603..0e17f178574 100644
--- a/src/terminal/input/terminalInput.cpp
+++ b/src/terminal/input/terminalInput.cpp
@@ -600,6 +600,15 @@ bool TerminalInput::HandleKey(const IInputEvent* const pInEvent)
         return true;
     }
 
+    // When the Line Feed mode is set, a VK_RETURN key should send both CR and LF.
+    // When reset, we fall through to the default behavior, which is to send just
+    // CR, or when the Ctrl modifier is pressed, just LF.
+    if (keyEvent.GetVirtualKeyCode() == VK_RETURN && _inputMode.test(Mode::LineFeed))
+    {
+        _SendInputSequence(L"\r\n");
+        return true;
+    }
+
     // Many keyboard layouts have an AltGr key, which makes widely used characters accessible.
     // For instance on a German keyboard layout "[" is written by pressing AltGr+8.
     // Furthermore Ctrl+Alt is traditionally treated as an alternative way to AltGr by Windows.
diff --git a/src/terminal/input/terminalInput.hpp b/src/terminal/input/terminalInput.hpp
index 41c91072c8b..da88bdad979 100644
--- a/src/terminal/input/terminalInput.hpp
+++ b/src/terminal/input/terminalInput.hpp
@@ -38,6 +38,7 @@ namespace Microsoft::Console::VirtualTerminal
 
         enum class Mode : size_t
         {
+            LineFeed,
             Ansi,
             AutoRepeat,
             Keypad,


Test patch: