From 3dc0672faab0105c345d81f11af7e627b94bdcdf Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Mon, 16 Mar 2020 10:32:01 -0500 Subject: [PATCH] Implement Hard Reset for Terminal (#4909) ## Summary of the Pull Request This _actually_ implements `\033c` ([RIS](https://vt100.net/docs/vt220-rm/chapter4.html)) for the Windows Terminal. I thought I had done this in #4433, but that PR actually only passthrough'd `\x1b[3J`. I didn't realize at the time that #2715 was mostly about hard reset, not erase scrollback. Not only should conpty pass through RIS, but the Terminal should also be prepared to actually handle that sequence. So this PR adds that support as well. ## References * #4433: original PR I thought fixed this. ## PR Checklist * [x] Closes #2715 for real this time * [x] I work here * [x] Tests added/passed * [n/a] Requires documentation to be updated ## Validation Steps Performed Actually tested `printf \033c` in the Terminal this time --- .../TerminalCore/TerminalDispatch.cpp | 98 +++++++++++++++++++ .../TerminalCore/TerminalDispatch.hpp | 3 + .../ConptyRoundtripTests.cpp | 73 +++++++++++++- src/terminal/adapter/adaptDispatch.cpp | 10 +- .../parser/OutputStateMachineEngine.cpp | 7 ++ 5 files changed, 187 insertions(+), 4 deletions(-) diff --git a/src/cascadia/TerminalCore/TerminalDispatch.cpp b/src/cascadia/TerminalCore/TerminalDispatch.cpp index f5d9a02559a..c209e810d25 100644 --- a/src/cascadia/TerminalCore/TerminalDispatch.cpp +++ b/src/cascadia/TerminalCore/TerminalDispatch.cpp @@ -401,3 +401,101 @@ bool TerminalDispatch::_PrivateModeParamsHelper(const DispatchTypes::PrivateMode } return success; } + +bool TerminalDispatch::SoftReset() noexcept +{ + // TODO:GH#1883 much of this method is not yet implemented in the Terminal, + // because the Terminal _doesn't need to_ yet. The terminal is only ever + // connected to conpty, so it doesn't implement most of these things that + // Hard/Soft Reset would reset. As those things are implemented, they should + + // also get cleared here. + // + // This code is left here (from its original form in conhost) as a reminder + // of what needs to be done. + + bool success = CursorVisibility(true); // Cursor enabled. + // if (success) + // { + // success = SetOriginMode(false); // Absolute cursor addressing. + // } + // if (success) + // { + // success = SetAutoWrapMode(true); // Wrap at end of line. + // } + if (success) + { + success = SetCursorKeysMode(false); // Normal characters. + } + if (success) + { + success = SetKeypadMode(false); // Numeric characters. + } + // if (success) + // { + // // Top margin = 1; bottom margin = page length. + // success = _DoSetTopBottomScrollingMargins(0, 0); + // } + // if (success) + // { + // success = DesignateCharset(DispatchTypes::VTCharacterSets::USASCII); // Default Charset + // } + if (success) + { + const auto opt = DispatchTypes::GraphicsOptions::Off; + success = SetGraphicsRendition({ &opt, 1 }); // Normal rendition. + } + // if (success) + // { + // // Reset the saved cursor state. + // // Note that XTerm only resets the main buffer state, but that + // // seems likely to be a bug. Most other terminals reset both. + // _savedCursorState.at(0) = {}; // Main buffer + // _savedCursorState.at(1) = {}; // Alt buffer + // } + + return success; +} + +bool TerminalDispatch::HardReset() noexcept +{ + // TODO:GH#1883 much of this method is not yet implemented in the Terminal, + // because the Terminal _doesn't need to_ yet. The terminal is only ever + // connected to conpty, so it doesn't implement most of these things that + // Hard/Soft Reset would reset. As those things ar implemented, they should + // also get cleared here. + // + // This code is left here (from its original form in conhost) as a reminder + // of what needs to be done. + + // Sets the SGR state to normal - this must be done before EraseInDisplay + // to ensure that it clears with the default background color. + bool success = SoftReset(); + + // Clears the screen - Needs to be done in two operations. + if (success) + { + success = EraseInDisplay(DispatchTypes::EraseType::All); + } + if (success) + { + success = EraseInDisplay(DispatchTypes::EraseType::Scrollback); + } + + // // Set the DECSCNM screen mode back to normal. + // if (success) + // { + // success = SetScreenMode(false); + // } + + // Cursor to 1,1 - the Soft Reset guarantees this is absolute + if (success) + { + success = CursorPosition(1, 1); + } + + // // delete all current tab stops and reapply + // _pConApi->PrivateSetDefaultTabStops(); + + return success; +} diff --git a/src/cascadia/TerminalCore/TerminalDispatch.hpp b/src/cascadia/TerminalCore/TerminalDispatch.hpp index 5321f67dbfa..e3be6ecbe92 100644 --- a/src/cascadia/TerminalCore/TerminalDispatch.hpp +++ b/src/cascadia/TerminalCore/TerminalDispatch.hpp @@ -44,6 +44,9 @@ class TerminalDispatch : public Microsoft::Console::VirtualTerminal::TermDispatc bool SetCursorKeysMode(const bool applicationMode) noexcept override; // DECCKM bool SetKeypadMode(const bool applicationMode) noexcept override; // DECKPAM, DECKPNM + bool SoftReset() noexcept override; // DECSTR + bool HardReset() noexcept override; // RIS + bool EnableVT200MouseMode(const bool enabled) noexcept override; // ?1000 bool EnableUTF8ExtendedMouseMode(const bool enabled) noexcept override; // ?1005 bool EnableSGRExtendedMouseMode(const bool enabled) noexcept override; // ?1006 diff --git a/src/cascadia/UnitTests_TerminalCore/ConptyRoundtripTests.cpp b/src/cascadia/UnitTests_TerminalCore/ConptyRoundtripTests.cpp index 762f1ff7f9a..dae8a248f77 100644 --- a/src/cascadia/UnitTests_TerminalCore/ConptyRoundtripTests.cpp +++ b/src/cascadia/UnitTests_TerminalCore/ConptyRoundtripTests.cpp @@ -158,6 +158,8 @@ class TerminalCoreUnitTests::ConptyRoundtripTests final TEST_METHOD(PassthroughClearScrollback); + TEST_METHOD(PassthroughHardReset); + TEST_METHOD(PassthroughCursorShapeImmediately); TEST_METHOD(TestWrappingALongString); @@ -926,7 +928,7 @@ void ConptyRoundtripTests::PassthroughCursorShapeImmediately() void ConptyRoundtripTests::PassthroughClearScrollback() { Log::Comment(NoThrowString().Format( - L"Write more lines of outout. We should use \r\n to move the cursor")); + L"Write more lines of output than there are lines in the viewport. Clear the scrollback with ^[[3J")); VERIFY_IS_NOT_NULL(_pVtRenderEngine.get()); auto& g = ServiceLocator::LocateGlobals(); @@ -985,7 +987,7 @@ void ConptyRoundtripTests::PassthroughClearScrollback() const auto termSecondView = term->GetViewport(); VERIFY_ARE_EQUAL(0, termSecondView.Top()); - // Verify the top of the Terminal veiwoprt contains the contents of the old viewport + // Verify the top of the Terminal viewport contains the contents of the old viewport for (short y = 0; y < termSecondView.BottomInclusive(); y++) { TestUtils::VerifyExpectedString(termTb, L"X ", { 0, y }); @@ -997,3 +999,70 @@ void ConptyRoundtripTests::PassthroughClearScrollback() TestUtils::VerifyExpectedString(termTb, std::wstring(TerminalViewWidth, L' '), { 0, y }); } } + +void ConptyRoundtripTests::PassthroughHardReset() +{ + // This test is highly similar to PassthroughClearScrollback. + Log::Comment(NoThrowString().Format( + L"Write more lines of output than there are lines in the viewport. Clear everything with ^[c")); + VERIFY_IS_NOT_NULL(_pVtRenderEngine.get()); + + auto& g = ServiceLocator::LocateGlobals(); + auto& renderer = *g.pRender; + auto& gci = g.getConsoleInformation(); + auto& si = gci.GetActiveOutputBuffer(); + auto& hostSm = si.GetStateMachine(); + + auto& termTb = *term->_buffer; + + _flushFirstFrame(); + + _logConpty = true; + + const auto hostView = si.GetViewport(); + const auto end = 2 * hostView.Height(); + for (auto i = 0; i < end; i++) + { + Log::Comment(NoThrowString().Format(L"Writing line %d/%d", i, end)); + expectedOutput.push_back("X"); + if (i < hostView.BottomInclusive()) + { + expectedOutput.push_back("\r\n"); + } + else + { + // After we hit the bottom of the viewport, the newlines come in + // separated for whatever reason. + + expectedOutput.push_back("\r"); + expectedOutput.push_back("\n"); + expectedOutput.push_back(""); + } + + hostSm.ProcessString(L"X\n"); + + VERIFY_SUCCEEDED(renderer.PaintFrame()); + } + + VERIFY_SUCCEEDED(renderer.PaintFrame()); + + // Verify that we've printed height*2 lines of X's to the Terminal + const auto termFirstView = term->GetViewport(); + for (short y = 0; y < 2 * termFirstView.Height(); y++) + { + TestUtils::VerifyExpectedString(termTb, L"X ", { 0, y }); + } + + // Write a Hard Reset VT sequence to the host, it should come through to the Terminal + expectedOutput.push_back("\033c"); + hostSm.ProcessString(L"\033c"); + + const auto termSecondView = term->GetViewport(); + VERIFY_ARE_EQUAL(0, termSecondView.Top()); + + // Verify everything has been cleared out + for (short y = 0; y < termFirstView.BottomInclusive(); y++) + { + TestUtils::VerifyExpectedString(termTb, std::wstring(TerminalViewWidth, L' '), { 0, y }); + } +} diff --git a/src/terminal/adapter/adaptDispatch.cpp b/src/terminal/adapter/adaptDispatch.cpp index 091d0b892c0..e9f3b939771 100644 --- a/src/terminal/adapter/adaptDispatch.cpp +++ b/src/terminal/adapter/adaptDispatch.cpp @@ -1462,6 +1462,8 @@ bool AdaptDispatch::DesignateCharset(const wchar_t wchCharset) noexcept // True if handled successfully. False otherwise. bool AdaptDispatch::SoftReset() { + const bool isPty = _pConApi->IsConsolePty(); + bool success = CursorVisibility(true); // Cursor enabled. if (success) { @@ -1475,11 +1477,15 @@ bool AdaptDispatch::SoftReset() { success = SetCursorKeysMode(false); // Normal characters. } - if (success) + // SetCursorKeysMode will return false if we're in conpty mode, as to + // trigger a passthrough. If that's the case, just power through here. + if (success || isPty) { success = SetKeypadMode(false); // Numeric characters. } - if (success) + // SetKeypadMode will return false if we're in conpty mode, as to trigger a + // passthrough. If that's the case, just power through here. + if (success || isPty) { // Top margin = 1; bottom margin = page length. success = _DoSetTopBottomScrollingMargins(0, 0); diff --git a/src/terminal/parser/OutputStateMachineEngine.cpp b/src/terminal/parser/OutputStateMachineEngine.cpp index be2763a2708..156488f01fa 100644 --- a/src/terminal/parser/OutputStateMachineEngine.cpp +++ b/src/terminal/parser/OutputStateMachineEngine.cpp @@ -274,6 +274,13 @@ bool OutputStateMachineEngine::ActionEscDispatch(const wchar_t wch, } } + // If we were unable to process the string, and there's a TTY attached to us, + // trigger the state machine to flush the string to the terminal. + if (_pfnFlushToTerminal != nullptr && !success) + { + success = _pfnFlushToTerminal(); + } + _ClearLastChar(); return success;