diff --git a/src/cascadia/TerminalApp/AppActionHandlers.cpp b/src/cascadia/TerminalApp/AppActionHandlers.cpp index e7f1cfb9566..5638c6bb8f8 100644 --- a/src/cascadia/TerminalApp/AppActionHandlers.cpp +++ b/src/cascadia/TerminalApp/AppActionHandlers.cpp @@ -875,6 +875,22 @@ namespace winrt::TerminalApp::implementation } } + void TerminalPage::_HandleClearBuffer(const IInspectable& /*sender*/, + const ActionEventArgs& args) + { + if (args) + { + if (const auto& realArgs = args.ActionArgs().try_as()) + { + if (const auto termControl{ _GetActiveControl() }) + { + termControl.ClearBuffer(realArgs.Clear()); + args.Handled(true); + } + } + } + } + void TerminalPage::_HandleMultipleActions(const IInspectable& /*sender*/, const ActionEventArgs& args) { diff --git a/src/cascadia/TerminalConnection/ConptyConnection.cpp b/src/cascadia/TerminalConnection/ConptyConnection.cpp index d82b6be0a4f..5c2373fd295 100644 --- a/src/cascadia/TerminalConnection/ConptyConnection.cpp +++ b/src/cascadia/TerminalConnection/ConptyConnection.cpp @@ -513,6 +513,16 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation } } + void ConptyConnection::ClearBuffer() + { + // If we haven't connected yet, then we really don't need to do + // anything. The connection should already start clear! + if (_isConnected()) + { + THROW_IF_FAILED(ConptyClearPseudoConsole(_hPC.get())); + } + } + void ConptyConnection::Close() noexcept try { diff --git a/src/cascadia/TerminalConnection/ConptyConnection.h b/src/cascadia/TerminalConnection/ConptyConnection.h index 8f82a617be3..9a2fc3a6e0f 100644 --- a/src/cascadia/TerminalConnection/ConptyConnection.h +++ b/src/cascadia/TerminalConnection/ConptyConnection.h @@ -35,6 +35,7 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation void WriteInput(hstring const& data); void Resize(uint32_t rows, uint32_t columns); void Close() noexcept; + void ClearBuffer(); winrt::guid Guid() const noexcept; diff --git a/src/cascadia/TerminalConnection/ConptyConnection.idl b/src/cascadia/TerminalConnection/ConptyConnection.idl index a1cfa979084..2e6cce5c9aa 100644 --- a/src/cascadia/TerminalConnection/ConptyConnection.idl +++ b/src/cascadia/TerminalConnection/ConptyConnection.idl @@ -9,6 +9,7 @@ namespace Microsoft.Terminal.TerminalConnection { ConptyConnection(); Guid Guid { get; }; + void ClearBuffer(); static event NewConnectionHandler NewConnection; static void StartInboundListener(); diff --git a/src/cascadia/TerminalControl/ControlCore.cpp b/src/cascadia/TerminalControl/ControlCore.cpp index c5e0cfac584..eac350c2ac3 100644 --- a/src/cascadia/TerminalControl/ControlCore.cpp +++ b/src/cascadia/TerminalControl/ControlCore.cpp @@ -1499,6 +1499,37 @@ namespace winrt::Microsoft::Terminal::Control::implementation _updatePatternLocations->Run(); } + // Method Description: + // - Clear the contents of the buffer. The region cleared is given by + // clearType: + // * Screen: Clear only the contents of the visible viewport, leaving the + // cursor row at the top of the viewport. + // * Scrollback: Clear the contents of the scrollback. + // * All: Do both - clear the visible viewport and the scrollback, leaving + // only the cursor row at the top of the viewport. + // Arguments: + // - clearType: The type of clear to perform. + // Return Value: + // - + void ControlCore::ClearBuffer(Control::ClearBufferType clearType) + { + if (clearType == Control::ClearBufferType::Scrollback || clearType == Control::ClearBufferType::All) + { + _terminal->EraseInDisplay(::Microsoft::Console::VirtualTerminal::DispatchTypes::EraseType::Scrollback); + } + + if (clearType == Control::ClearBufferType::Screen || clearType == Control::ClearBufferType::All) + { + // Send a signal to conpty to clear the buffer. + if (auto conpty{ _connection.try_as() }) + { + // ConPTY will emit sequences to sync up our buffer with its new + // contents. + conpty.ClearBuffer(); + } + } + } + hstring ControlCore::ReadEntireBuffer() const { auto terminalLock = _terminal->LockForWriting(); diff --git a/src/cascadia/TerminalControl/ControlCore.h b/src/cascadia/TerminalControl/ControlCore.h index 57052cdd054..a04032ef78e 100644 --- a/src/cascadia/TerminalControl/ControlCore.h +++ b/src/cascadia/TerminalControl/ControlCore.h @@ -112,6 +112,9 @@ namespace winrt::Microsoft::Terminal::Control::implementation const short wheelDelta, const ::Microsoft::Console::VirtualTerminal::TerminalInput::MouseButtonState state); void UserScrollViewport(const int viewTop); + + void ClearBuffer(Control::ClearBufferType clearType); + #pragma endregion void BlinkAttributeTick(); diff --git a/src/cascadia/TerminalControl/ControlCore.idl b/src/cascadia/TerminalControl/ControlCore.idl index 154fa869b0d..84cb83e80f2 100644 --- a/src/cascadia/TerminalControl/ControlCore.idl +++ b/src/cascadia/TerminalControl/ControlCore.idl @@ -22,6 +22,14 @@ namespace Microsoft.Terminal.Control IsRightButtonDown = 0x4 }; + + enum ClearBufferType + { + Screen, + Scrollback, + All + }; + [default_interface] runtimeclass ControlCore : ICoreState { ControlCore(IControlSettings settings, @@ -49,6 +57,7 @@ namespace Microsoft.Terminal.Control Microsoft.Terminal.Core.ControlKeyStates modifiers); void SendInput(String text); void PasteText(String text); + void ClearBuffer(ClearBufferType clearType); void SetHoveredCell(Microsoft.Terminal.Core.Point terminalPosition); void ClearHoveredCell(); diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index e1a2afb378f..1c698d8b4ab 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -351,6 +351,10 @@ namespace winrt::Microsoft::Terminal::Control::implementation { _core.SendInput(wstr); } + void TermControl::ClearBuffer(Control::ClearBufferType clearType) + { + _core.ClearBuffer(clearType); + } void TermControl::ToggleShaderEffects() { diff --git a/src/cascadia/TerminalControl/TermControl.h b/src/cascadia/TerminalControl/TermControl.h index 521c897fddf..9d0f22e339f 100644 --- a/src/cascadia/TerminalControl/TermControl.h +++ b/src/cascadia/TerminalControl/TermControl.h @@ -63,6 +63,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation til::point GetFontSize() const; void SendInput(const winrt::hstring& input); + void ClearBuffer(Control::ClearBufferType clearType); + void ToggleShaderEffects(); winrt::fire_and_forget RenderEngineSwapChainChanged(IInspectable sender, IInspectable args); diff --git a/src/cascadia/TerminalControl/TermControl.idl b/src/cascadia/TerminalControl/TermControl.idl index 325d813e2f4..26db0862cac 100644 --- a/src/cascadia/TerminalControl/TermControl.idl +++ b/src/cascadia/TerminalControl/TermControl.idl @@ -6,6 +6,7 @@ import "IControlSettings.idl"; import "IDirectKeyListener.idl"; import "EventArgs.idl"; import "ICoreState.idl"; +import "ControlCore.idl"; namespace Microsoft.Terminal.Control { @@ -46,6 +47,7 @@ namespace Microsoft.Terminal.Control Boolean CopySelectionToClipboard(Boolean singleLine, Windows.Foundation.IReference formats); void PasteTextFromClipboard(); + void ClearBuffer(ClearBufferType clearType); void Close(); Windows.Foundation.Size CharacterDimensions { get; }; Windows.Foundation.Size MinimumSize { get; }; diff --git a/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp b/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp index 02bb1d486df..a6094855b17 100644 --- a/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp +++ b/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp @@ -65,6 +65,7 @@ static constexpr std::string_view OpenWindowRenamerKey{ "openWindowRenamer" }; static constexpr std::string_view GlobalSummonKey{ "globalSummon" }; static constexpr std::string_view QuakeModeKey{ "quakeMode" }; static constexpr std::string_view FocusPaneKey{ "focusPane" }; +static constexpr std::string_view ClearBufferKey{ "clearBuffer" }; static constexpr std::string_view MultipleActionsKey{ "multipleActions" }; static constexpr std::string_view ActionKey{ "action" }; @@ -367,6 +368,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation { ShortcutAction::GlobalSummon, L"" }, // Intentionally omitted, must be generated by GenerateName { ShortcutAction::QuakeMode, RS_(L"QuakeModeCommandKey") }, { ShortcutAction::FocusPane, L"" }, // Intentionally omitted, must be generated by GenerateName + { ShortcutAction::ClearBuffer, L"" }, // Intentionally omitted, must be generated by GenerateName { ShortcutAction::MultipleActions, L"" }, // Intentionally omitted, must be generated by GenerateName }; }(); diff --git a/src/cascadia/TerminalSettingsModel/ActionArgs.cpp b/src/cascadia/TerminalSettingsModel/ActionArgs.cpp index 99d55d38e94..8d2e24d5822 100644 --- a/src/cascadia/TerminalSettingsModel/ActionArgs.cpp +++ b/src/cascadia/TerminalSettingsModel/ActionArgs.cpp @@ -34,6 +34,7 @@ #include "RenameWindowArgs.g.cpp" #include "GlobalSummonArgs.g.cpp" #include "FocusPaneArgs.g.cpp" +#include "ClearBufferArgs.g.cpp" #include "MultipleActionsArgs.g.cpp" #include @@ -688,6 +689,24 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation Id()) }; } + winrt::hstring ClearBufferArgs::GenerateName() const + { + // "Clear Buffer" + // "Clear Viewport" + // "Clear Scrollback" + switch (Clear()) + { + case Control::ClearBufferType::All: + return RS_(L"ClearAllCommandKey"); + case Control::ClearBufferType::Screen: + return RS_(L"ClearViewportCommandKey"); + case Control::ClearBufferType::Scrollback: + return RS_(L"ClearScrollbackCommandKey"); + } + + // Return the empty string - the Clear() should be one of these values + return winrt::hstring{ L"" }; + } winrt::hstring MultipleActionsArgs::GenerateName() const { diff --git a/src/cascadia/TerminalSettingsModel/ActionArgs.h b/src/cascadia/TerminalSettingsModel/ActionArgs.h index 96dbe07ce63..3ae8700393d 100644 --- a/src/cascadia/TerminalSettingsModel/ActionArgs.h +++ b/src/cascadia/TerminalSettingsModel/ActionArgs.h @@ -36,6 +36,7 @@ #include "RenameWindowArgs.g.h" #include "GlobalSummonArgs.g.h" #include "FocusPaneArgs.g.h" +#include "ClearBufferArgs.g.h" #include "MultipleActionsArgs.g.h" #include "../../cascadia/inc/cppwinrt_utils.h" @@ -1755,6 +1756,56 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation } }; + struct ClearBufferArgs : public ClearBufferArgsT + { + ClearBufferArgs() = default; + ClearBufferArgs(winrt::Microsoft::Terminal::Control::ClearBufferType clearType) : + _Clear{ clearType } {}; + WINRT_PROPERTY(winrt::Microsoft::Terminal::Control::ClearBufferType, Clear, winrt::Microsoft::Terminal::Control::ClearBufferType::All); + static constexpr std::string_view ClearKey{ "clear" }; + + public: + hstring GenerateName() const; + + bool Equals(const IActionArgs& other) + { + auto otherAsUs = other.try_as(); + if (otherAsUs) + { + return otherAsUs->_Clear == _Clear; + } + return false; + }; + static FromJsonResult FromJson(const Json::Value& json) + { + // LOAD BEARING: Not using make_self here _will_ break you in the future! + auto args = winrt::make_self(); + JsonUtils::GetValueForKey(json, ClearKey, args->_Clear); + return { *args, {} }; + } + static Json::Value ToJson(const IActionArgs& val) + { + if (!val) + { + return {}; + } + Json::Value json{ Json::ValueType::objectValue }; + const auto args{ get_self(val) }; + JsonUtils::SetValueForKey(json, ClearKey, args->_Clear); + return json; + } + IActionArgs Copy() const + { + auto copy{ winrt::make_self() }; + copy->_Clear = _Clear; + return *copy; + } + size_t Hash() const + { + return ::Microsoft::Terminal::Settings::Model::HashUtils::HashProperty(_Clear); + } + }; + struct MultipleActionsArgs : public MultipleActionsArgsT { MultipleActionsArgs() = default; @@ -1787,6 +1838,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation return {}; } Json::Value json{ Json::ValueType::objectValue }; + const auto args{ get_self(val) }; JsonUtils::SetValueForKey(json, ActionsKey, args->_Actions); return json; @@ -1826,5 +1878,6 @@ namespace winrt::Microsoft::Terminal::Settings::Model::factory_implementation BASIC_FACTORY(FocusPaneArgs); BASIC_FACTORY(PrevTabArgs); BASIC_FACTORY(NextTabArgs); + BASIC_FACTORY(ClearBufferArgs); BASIC_FACTORY(MultipleActionsArgs); } diff --git a/src/cascadia/TerminalSettingsModel/ActionArgs.idl b/src/cascadia/TerminalSettingsModel/ActionArgs.idl index 3d89466e93e..03b36815d02 100644 --- a/src/cascadia/TerminalSettingsModel/ActionArgs.idl +++ b/src/cascadia/TerminalSettingsModel/ActionArgs.idl @@ -311,9 +311,15 @@ namespace Microsoft.Terminal.Settings.Model UInt32 Id { get; }; }; + [default_interface] runtimeclass ClearBufferArgs : IActionArgs + { + ClearBufferArgs(Microsoft.Terminal.Control.ClearBufferType clear); + Microsoft.Terminal.Control.ClearBufferType Clear { get; }; + }; + [default_interface] runtimeclass MultipleActionsArgs : IActionArgs { MultipleActionsArgs(); Windows.Foundation.Collections.IVector Actions; - } + }; } diff --git a/src/cascadia/TerminalSettingsModel/AllShortcutActions.h b/src/cascadia/TerminalSettingsModel/AllShortcutActions.h index e2f5488e45b..f198dfc10bd 100644 --- a/src/cascadia/TerminalSettingsModel/AllShortcutActions.h +++ b/src/cascadia/TerminalSettingsModel/AllShortcutActions.h @@ -79,6 +79,7 @@ ON_ALL_ACTIONS(GlobalSummon) \ ON_ALL_ACTIONS(QuakeMode) \ ON_ALL_ACTIONS(FocusPane) \ + ON_ALL_ACTIONS(ClearBuffer) \ ON_ALL_ACTIONS(MultipleActions) #define ALL_SHORTCUT_ACTIONS_WITH_ARGS \ @@ -111,4 +112,5 @@ ON_ALL_ACTIONS_WITH_ARGS(SwitchToTab) \ ON_ALL_ACTIONS_WITH_ARGS(ToggleCommandPalette) \ ON_ALL_ACTIONS_WITH_ARGS(FocusPane) \ + ON_ALL_ACTIONS_WITH_ARGS(ClearBuffer) \ ON_ALL_ACTIONS_WITH_ARGS(MultipleActions) diff --git a/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw b/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw index 9ab4507535d..08d2a1e3d91 100644 --- a/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw @@ -446,6 +446,18 @@ Focus pane {0} {0} will be replaced with a user-specified number + + Clear Buffer + A command to clear the entirety of the Terminal output buffer + + + Clear Viewport + A command to clear the active viewport of the Terminal + + + Clear Scrollback + A command to clear the part of the buffer above the viewport + Microsoft Corporation Paired with `InboxWindowsConsoleName`, this is the application author... which is us: Microsoft. diff --git a/src/cascadia/TerminalSettingsModel/TerminalSettingsSerializationHelpers.h b/src/cascadia/TerminalSettingsModel/TerminalSettingsSerializationHelpers.h index 188587f5708..674a42fbe7e 100644 --- a/src/cascadia/TerminalSettingsModel/TerminalSettingsSerializationHelpers.h +++ b/src/cascadia/TerminalSettingsModel/TerminalSettingsSerializationHelpers.h @@ -472,6 +472,15 @@ JSON_ENUM_MAPPER(::winrt::Microsoft::Terminal::Settings::Model::MonitorBehavior) }; }; +JSON_ENUM_MAPPER(::winrt::Microsoft::Terminal::Control::ClearBufferType) +{ + JSON_MAPPINGS(3) = { + pair_type{ "all", ValueType::All }, + pair_type{ "screen", ValueType::Screen }, + pair_type{ "scrollback", ValueType::Scrollback }, + }; +}; + JSON_FLAG_MAPPER(::winrt::Microsoft::Terminal::Settings::Model::IntenseStyle) { static constexpr std::array mappings = { @@ -479,5 +488,6 @@ JSON_FLAG_MAPPER(::winrt::Microsoft::Terminal::Settings::Model::IntenseStyle) pair_type{ "bold", ValueType::Bold }, pair_type{ "bright", ValueType::Bright }, pair_type{ "all", AllSet }, + }; }; diff --git a/src/cascadia/TerminalSettingsModel/defaults.json b/src/cascadia/TerminalSettingsModel/defaults.json index ee1cdeb76eb..9f942d82134 100644 --- a/src/cascadia/TerminalSettingsModel/defaults.json +++ b/src/cascadia/TerminalSettingsModel/defaults.json @@ -383,6 +383,7 @@ { "command": "scrollUpPage", "keys": "ctrl+shift+pgup" }, { "command": "scrollToTop", "keys": "ctrl+shift+home" }, { "command": "scrollToBottom", "keys": "ctrl+shift+end" }, + { "command": { "action": "clearBuffer", "clear": "all" } }, // Visual Adjustments { "command": { "action": "adjustFontSize", "delta": 1 }, "keys": "ctrl+plus" }, diff --git a/src/cascadia/UnitTests_Control/ControlCoreTests.cpp b/src/cascadia/UnitTests_Control/ControlCoreTests.cpp index e57dd8c23cf..9e8599032bd 100644 --- a/src/cascadia/UnitTests_Control/ControlCoreTests.cpp +++ b/src/cascadia/UnitTests_Control/ControlCoreTests.cpp @@ -6,6 +6,7 @@ #include "../TerminalControl/ControlCore.h" #include "MockControlSettings.h" #include "MockConnection.h" +#include "../UnitTests_TerminalCore/TestUtils.h" using namespace Microsoft::Console; using namespace WEX::Logging; @@ -32,6 +33,10 @@ namespace ControlUnitTests TEST_METHOD(TestFontInitializedInCtor); + TEST_METHOD(TestClearScrollback); + TEST_METHOD(TestClearScreen); + TEST_METHOD(TestClearAll); + TEST_CLASS_SETUP(ModuleSetup) { winrt::init_apartment(winrt::apartment_type::single_threaded); @@ -66,6 +71,15 @@ namespace ControlUnitTests core->_inUnitTests = true; return core; } + + void _standardInit(winrt::com_ptr core) + { + // "Consolas" ends up with an actual size of 9x21 at 96DPI. So + // let's just arbitrarily start with a 270x420px (30x20 chars) window + core->Initialize(270, 420, 1.0); + VERIFY_IS_TRUE(core->_initializedTerminal); + VERIFY_ARE_EQUAL(20, core->_terminal->GetViewport().Height()); + } }; void ControlCoreTests::ComPtrSettings() @@ -202,4 +216,122 @@ namespace ControlUnitTests VERIFY_ARE_EQUAL(L"Impact", std::wstring_view{ core->_actualFont.GetFaceName() }); } + void ControlCoreTests::TestClearScrollback() + { + auto [settings, conn] = _createSettingsAndConnection(); + Log::Comment(L"Create ControlCore object"); + auto core = winrt::make_self(*settings, *conn); + VERIFY_IS_NOT_NULL(core); + _standardInit(core); + + Log::Comment(L"Print 40 rows of 'Foo', and a single row of 'Bar' " + L"(leaving the cursor afer 'Bar')"); + for (int i = 0; i < 40; ++i) + { + conn->WriteInput(L"Foo\r\n"); + } + conn->WriteInput(L"Bar"); + + // We printed that 40 times, but the final \r\n bumped the view down one MORE row. + Log::Comment(L"Check the buffer viewport before the clear"); + VERIFY_ARE_EQUAL(20, core->_terminal->GetViewport().Height()); + VERIFY_ARE_EQUAL(21, core->ScrollOffset()); + VERIFY_ARE_EQUAL(20, core->ViewHeight()); + VERIFY_ARE_EQUAL(41, core->BufferHeight()); + + Log::Comment(L"Clear the buffer"); + core->ClearBuffer(Control::ClearBufferType::Scrollback); + + Log::Comment(L"Check the buffer after the clear"); + VERIFY_ARE_EQUAL(20, core->_terminal->GetViewport().Height()); + VERIFY_ARE_EQUAL(0, core->ScrollOffset()); + VERIFY_ARE_EQUAL(20, core->ViewHeight()); + VERIFY_ARE_EQUAL(20, core->BufferHeight()); + + // In this test, we can't actually check if we cleared the buffer + // contents. ConPTY will handle the actual clearing of the buffer + // contents. We can only ensure that the viewport moved when we did a + // clear scrollback. + // + // The ConptyRoundtripTests test the actual clearing of the contents. + } + void ControlCoreTests::TestClearScreen() + { + auto [settings, conn] = _createSettingsAndConnection(); + Log::Comment(L"Create ControlCore object"); + auto core = winrt::make_self(*settings, *conn); + VERIFY_IS_NOT_NULL(core); + _standardInit(core); + + Log::Comment(L"Print 40 rows of 'Foo', and a single row of 'Bar' " + L"(leaving the cursor afer 'Bar')"); + for (int i = 0; i < 40; ++i) + { + conn->WriteInput(L"Foo\r\n"); + } + conn->WriteInput(L"Bar"); + + // We printed that 40 times, but the final \r\n bumped the view down one MORE row. + Log::Comment(L"Check the buffer viewport before the clear"); + VERIFY_ARE_EQUAL(20, core->_terminal->GetViewport().Height()); + VERIFY_ARE_EQUAL(21, core->ScrollOffset()); + VERIFY_ARE_EQUAL(20, core->ViewHeight()); + VERIFY_ARE_EQUAL(41, core->BufferHeight()); + + Log::Comment(L"Clear the buffer"); + core->ClearBuffer(Control::ClearBufferType::Screen); + + Log::Comment(L"Check the buffer after the clear"); + VERIFY_ARE_EQUAL(20, core->_terminal->GetViewport().Height()); + VERIFY_ARE_EQUAL(21, core->ScrollOffset()); + VERIFY_ARE_EQUAL(20, core->ViewHeight()); + VERIFY_ARE_EQUAL(41, core->BufferHeight()); + + // In this test, we can't actually check if we cleared the buffer + // contents. ConPTY will handle the actual clearing of the buffer + // contents. We can only ensure that the viewport moved when we did a + // clear scrollback. + // + // The ConptyRoundtripTests test the actual clearing of the contents. + } + void ControlCoreTests::TestClearAll() + { + auto [settings, conn] = _createSettingsAndConnection(); + Log::Comment(L"Create ControlCore object"); + auto core = winrt::make_self(*settings, *conn); + VERIFY_IS_NOT_NULL(core); + _standardInit(core); + + Log::Comment(L"Print 40 rows of 'Foo', and a single row of 'Bar' " + L"(leaving the cursor afer 'Bar')"); + for (int i = 0; i < 40; ++i) + { + conn->WriteInput(L"Foo\r\n"); + } + conn->WriteInput(L"Bar"); + + // We printed that 40 times, but the final \r\n bumped the view down one MORE row. + Log::Comment(L"Check the buffer viewport before the clear"); + VERIFY_ARE_EQUAL(20, core->_terminal->GetViewport().Height()); + VERIFY_ARE_EQUAL(21, core->ScrollOffset()); + VERIFY_ARE_EQUAL(20, core->ViewHeight()); + VERIFY_ARE_EQUAL(41, core->BufferHeight()); + + Log::Comment(L"Clear the buffer"); + core->ClearBuffer(Control::ClearBufferType::All); + + Log::Comment(L"Check the buffer after the clear"); + VERIFY_ARE_EQUAL(20, core->_terminal->GetViewport().Height()); + VERIFY_ARE_EQUAL(0, core->ScrollOffset()); + VERIFY_ARE_EQUAL(20, core->ViewHeight()); + VERIFY_ARE_EQUAL(20, core->BufferHeight()); + + // In this test, we can't actually check if we cleared the buffer + // contents. ConPTY will handle the actual clearing of the buffer + // contents. We can only ensure that the viewport moved when we did a + // clear scrollback. + // + // The ConptyRoundtripTests test the actual clearing of the contents. + } + } diff --git a/src/cascadia/UnitTests_TerminalCore/ConptyRoundtripTests.cpp b/src/cascadia/UnitTests_TerminalCore/ConptyRoundtripTests.cpp index 1543ea632e2..fe671a65acd 100644 --- a/src/cascadia/UnitTests_TerminalCore/ConptyRoundtripTests.cpp +++ b/src/cascadia/UnitTests_TerminalCore/ConptyRoundtripTests.cpp @@ -218,10 +218,13 @@ class TerminalCoreUnitTests::ConptyRoundtripTests final TEST_METHOD(ResizeInitializeBufferWithDefaultAttrs); + TEST_METHOD(ClearBufferSignal); + private: bool _writeCallback(const char* const pch, size_t const cch); void _flushFirstFrame(); void _resizeConpty(const unsigned short sx, const unsigned short sy); + void _clearConpty(); [[nodiscard]] std::tuple _performResize(const til::size& newSize); @@ -297,6 +300,12 @@ void ConptyRoundtripTests::_resizeConpty(const unsigned short sx, } } +void ConptyRoundtripTests::_clearConpty() +{ + // Taken verbatim from implementation in PtySignalInputThread::_DoClearBuffer + _pConApi->PrivateClearBuffer(); +} + [[nodiscard]] std::tuple ConptyRoundtripTests::_performResize(const til::size& newSize) { // IMPORTANT! Anyone calling this should make sure that the test is running @@ -3675,3 +3684,77 @@ void ConptyRoundtripTests::HyperlinkIdConsistency() verifyData(hostTb); verifyData(termTb); } + +void ConptyRoundtripTests::ClearBufferSignal() +{ + Log::Comment(L"Write some text to the conpty buffer. Send a ClearBuffer " + L"signal, and check that all but the cursor line is removed " + L"from the host and the terminal."); + auto& g = ServiceLocator::LocateGlobals(); + auto& renderer = *g.pRender; + auto& gci = g.getConsoleInformation(); + auto& si = gci.GetActiveOutputBuffer(); + auto& sm = si.GetStateMachine(); + auto* hostTb = &si.GetTextBuffer(); + auto* termTb = term->_buffer.get(); + + _flushFirstFrame(); + + _checkConptyOutput = false; + _logConpty = true; + + // Print two lines of text: + // |AAAAAAAAAAAAA BBBBBB| + // |BBBBBBBB_ | + // (cursor on the '_') + // A's are in blue-on-green, + // B's are in red-on-yellow + + sm.ProcessString(L"\x1b[?25l"); + sm.ProcessString(L"\x1b[?34;42m"); + sm.ProcessString(std::wstring(50, L'A')); + sm.ProcessString(L" "); + sm.ProcessString(L"\x1b[?31;43m"); + sm.ProcessString(std::wstring(50, L'B')); + sm.ProcessString(L"\x1b[?m"); + sm.ProcessString(L"\x1b[?25h"); + + auto verifyBuffer = [&](const TextBuffer& tb, const til::rectangle viewport, const bool before) { + const short width = viewport.width(); + const short numCharsOnSecondLine = 50 - (width - 51); + auto iter1 = tb.GetCellDataAt({ 0, 0 }); + if (before) + { + TestUtils::VerifySpanOfText(L"A", iter1, 0, 50); + TestUtils::VerifySpanOfText(L" ", iter1, 0, 1); + TestUtils::VerifySpanOfText(L"B", iter1, 0, 50); + COORD expectedCursor{ numCharsOnSecondLine, 1 }; + VERIFY_ARE_EQUAL(expectedCursor, tb.GetCursor().GetPosition()); + } + else + { + TestUtils::VerifySpanOfText(L"B", iter1, 0, numCharsOnSecondLine); + COORD expectedCursor{ numCharsOnSecondLine, 0 }; + VERIFY_ARE_EQUAL(expectedCursor, tb.GetCursor().GetPosition()); + } + }; + + Log::Comment(L"========== Checking the host buffer state (before) =========="); + verifyBuffer(*hostTb, si.GetViewport().ToInclusive(), true); + + Log::Comment(L"Painting the frame"); + VERIFY_SUCCEEDED(renderer.PaintFrame()); + Log::Comment(L"========== Checking the terminal buffer state (before) =========="); + verifyBuffer(*termTb, term->_mutableViewport.ToInclusive(), true); + + Log::Comment(L"========== Clear the ConPTY buffer with the signal =========="); + _clearConpty(); + + Log::Comment(L"========== Checking the host buffer state (after) =========="); + verifyBuffer(*hostTb, si.GetViewport().ToInclusive(), false); + + Log::Comment(L"Painting the frame"); + VERIFY_SUCCEEDED(renderer.PaintFrame()); + Log::Comment(L"========== Checking the terminal buffer state (after) =========="); + verifyBuffer(*termTb, term->_mutableViewport.ToInclusive(), false); +} diff --git a/src/host/PtySignalInputThread.cpp b/src/host/PtySignalInputThread.cpp index df42f45f7a2..3855a467ab5 100644 --- a/src/host/PtySignalInputThread.cpp +++ b/src/host/PtySignalInputThread.cpp @@ -82,6 +82,21 @@ void PtySignalInputThread::ConnectConsole() noexcept { switch (signalId) { + case PtySignal::ClearBuffer: + { + LockConsole(); + auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); + + // If the client app hasn't yet connected, stash the new size in the launchArgs. + // We'll later use the value in launchArgs to set up the console buffer + // We must be under lock here to ensure that someone else doesn't come in + // and set with `ConnectConsole` while we're looking and modifying this. + if (_consoleConnected) + { + _DoClearBuffer(); + } + break; + } case PtySignal::ResizeWindow: { ResizeWindowData resizeMsg = { 0 }; @@ -128,6 +143,11 @@ void PtySignalInputThread::_DoResizeWindow(const ResizeWindowData& data) } } +void PtySignalInputThread::_DoClearBuffer() +{ + _pConApi->PrivateClearBuffer(); +} + // Method Description: // - Retrieves bytes from the file stream and exits or throws errors should the pipe state // be compromised. diff --git a/src/host/PtySignalInputThread.hpp b/src/host/PtySignalInputThread.hpp index d54cf683345..e6c4d8bb4b9 100644 --- a/src/host/PtySignalInputThread.hpp +++ b/src/host/PtySignalInputThread.hpp @@ -35,6 +35,7 @@ namespace Microsoft::Console private: enum class PtySignal : unsigned short { + ClearBuffer = 2, ResizeWindow = 8 }; @@ -47,6 +48,7 @@ namespace Microsoft::Console [[nodiscard]] HRESULT _InputThread(); bool _GetData(_Out_writes_bytes_(cbBuffer) void* const pBuffer, const DWORD cbBuffer); void _DoResizeWindow(const ResizeWindowData& data); + void _DoClearBuffer(); void _Shutdown(); wil::unique_hfile _hFile; diff --git a/src/host/getset.cpp b/src/host/getset.cpp index 9c3ccde4456..36f2ebbd892 100644 --- a/src/host/getset.cpp +++ b/src/host/getset.cpp @@ -1609,6 +1609,12 @@ void DoSrvPrivateEnableAlternateScroll(const bool fEnable) return screenInfo.GetActiveBuffer().VtEraseAll(); } +// See SCREEN_INFORMATION::ClearBuffer's description for details. +[[nodiscard]] HRESULT DoSrvPrivateClearBuffer(SCREEN_INFORMATION& screenInfo) +{ + return screenInfo.GetActiveBuffer().ClearBuffer(); +} + void DoSrvSetCursorStyle(SCREEN_INFORMATION& screenInfo, const CursorType cursorType) { diff --git a/src/host/getset.h b/src/host/getset.h index 1fb48e286a9..e4c5395809a 100644 --- a/src/host/getset.h +++ b/src/host/getset.h @@ -43,6 +43,7 @@ void DoSrvPrivateEnableAnyEventMouseMode(const bool fEnable); void DoSrvPrivateEnableAlternateScroll(const bool fEnable); [[nodiscard]] HRESULT DoSrvPrivateEraseAll(SCREEN_INFORMATION& screenInfo); +[[nodiscard]] HRESULT DoSrvPrivateClearBuffer(SCREEN_INFORMATION& screenInfo); void DoSrvSetCursorStyle(SCREEN_INFORMATION& screenInfo, const CursorType cursorType); diff --git a/src/host/outputStream.cpp b/src/host/outputStream.cpp index a70b48afd0c..bc15c1cdf6c 100644 --- a/src/host/outputStream.cpp +++ b/src/host/outputStream.cpp @@ -545,6 +545,11 @@ bool ConhostInternalGetSet::PrivateEraseAll() return SUCCEEDED(DoSrvPrivateEraseAll(_io.GetActiveOutputBuffer())); } +bool ConhostInternalGetSet::PrivateClearBuffer() +{ + return SUCCEEDED(DoSrvPrivateClearBuffer(_io.GetActiveOutputBuffer())); +} + // Method Description: // - Retrieves the current user default cursor style. // Arguments: diff --git a/src/host/outputStream.hpp b/src/host/outputStream.hpp index 99889baca46..9df19446fb7 100644 --- a/src/host/outputStream.hpp +++ b/src/host/outputStream.hpp @@ -104,6 +104,7 @@ class ConhostInternalGetSet final : public Microsoft::Console::VirtualTerminal:: bool PrivateEnableAnyEventMouseMode(const bool enabled) override; bool PrivateEnableAlternateScroll(const bool enabled) override; bool PrivateEraseAll() override; + bool PrivateClearBuffer() override; bool GetUserDefaultCursorStyle(CursorType& style) override; bool SetCursorStyle(CursorType const style) override; diff --git a/src/host/screenInfo.cpp b/src/host/screenInfo.cpp index 5323cb57414..320e1148dc3 100644 --- a/src/host/screenInfo.cpp +++ b/src/host/screenInfo.cpp @@ -2277,6 +2277,55 @@ void SCREEN_INFORMATION::SetViewport(const Viewport& newViewport, return S_OK; } +// Method Description: +// - Clear the entire contents of the viewport, except for the cursor's row, +// which is moved to the top line of the viewport. +// - This is used exclusively by ConPTY to support GH#1193, GH#1882. This allows +// a terminal to clear the contents of the ConPTY buffer, which is important +// if the user would like to be able to clear the terminal-side buffer. +// Arguments: +// - +// Return Value: +// - S_OK +[[nodiscard]] HRESULT SCREEN_INFORMATION::ClearBuffer() +{ + const COORD oldCursorPos = _textBuffer->GetCursor().GetPosition(); + short sNewTop = oldCursorPos.Y; + const Viewport oldViewport = _viewport; + + short delta = (sNewTop + _viewport.Height()) - (GetBufferSize().Height()); + for (auto i = 0; i < delta; i++) + { + _textBuffer->IncrementCircularBuffer(); + sNewTop--; + } + + const COORD coordNewOrigin = { 0, sNewTop }; + RETURN_IF_FAILED(SetViewportOrigin(true, coordNewOrigin, true)); + + // Place the cursor at the same x coord, on the row that's now the top + RETURN_IF_FAILED(SetCursorPosition(COORD{ oldCursorPos.X, sNewTop }, false)); + + // Update all the rows in the current viewport with the standard erase attributes, + // i.e. the current background color, but with no meta attributes set. + auto fillAttributes = GetAttributes(); + fillAttributes.SetStandardErase(); + + // +1 on the y coord because we don't want to clear the attributes of the + // cursor row, the one we saved. + auto fillPosition = COORD{ 0, _viewport.Top() + 1 }; + auto fillLength = gsl::narrow_cast(_viewport.Height() * GetBufferSize().Width()); + auto fillData = OutputCellIterator{ fillAttributes, fillLength }; + Write(fillData, fillPosition, false); + + _textBuffer->GetRenderTarget().TriggerRedrawAll(); + + // Also reset the line rendition for the erased rows. + _textBuffer->ResetLineRenditionRange(_viewport.Top(), _viewport.BottomExclusive()); + + return S_OK; +} + // Method Description: // - Sets up the Output state machine to be in pty mode. Sequences it doesn't // understand will be written to the pTtyConnection passed in here. diff --git a/src/host/screenInfo.hpp b/src/host/screenInfo.hpp index 3fa091de294..e3b1eed4df2 100644 --- a/src/host/screenInfo.hpp +++ b/src/host/screenInfo.hpp @@ -222,6 +222,7 @@ class SCREEN_INFORMATION : public ConsoleObjectHeader, public Microsoft::Console const TextAttribute& popupAttributes); [[nodiscard]] HRESULT VtEraseAll(); + [[nodiscard]] HRESULT ClearBuffer(); void SetTerminalConnection(_In_ Microsoft::Console::ITerminalOutputConnection* const pTtyConnection); diff --git a/src/inc/conpty-static.h b/src/inc/conpty-static.h index bd3ac59cab9..052708a6bd5 100644 --- a/src/inc/conpty-static.h +++ b/src/inc/conpty-static.h @@ -22,6 +22,8 @@ HRESULT WINAPI ConptyCreatePseudoConsole(COORD size, HANDLE hInput, HANDLE hOutp HRESULT WINAPI ConptyResizePseudoConsole(HPCON hPC, COORD size); +HRESULT WINAPI ConptyClearPseudoConsole(HPCON hPC); + VOID WINAPI ConptyClosePseudoConsole(HPCON hPC); HRESULT WINAPI ConptyPackPseudoConsole(HANDLE hServerProcess, HANDLE hRef, HANDLE hSignal, HPCON* phPC); diff --git a/src/inc/conpty.h b/src/inc/conpty.h index ff980d7c278..6f51ba94246 100644 --- a/src/inc/conpty.h +++ b/src/inc/conpty.h @@ -7,6 +7,7 @@ #include #pragma once +const unsigned int PTY_SIGNAL_CLEAR_WINDOW = 2u; const unsigned int PTY_SIGNAL_RESIZE_WINDOW = 8u; HRESULT CreateConPty(const std::wstring& cmdline, // _In_ diff --git a/src/terminal/adapter/conGetSet.hpp b/src/terminal/adapter/conGetSet.hpp index e69ab753fca..7576d38012b 100644 --- a/src/terminal/adapter/conGetSet.hpp +++ b/src/terminal/adapter/conGetSet.hpp @@ -73,6 +73,7 @@ namespace Microsoft::Console::VirtualTerminal virtual bool PrivateEnableAnyEventMouseMode(const bool enabled) = 0; virtual bool PrivateEnableAlternateScroll(const bool enabled) = 0; virtual bool PrivateEraseAll() = 0; + virtual bool PrivateClearBuffer() = 0; virtual bool GetUserDefaultCursorStyle(CursorType& style) = 0; virtual bool SetCursorStyle(const CursorType style) = 0; virtual bool SetCursorColor(const COLORREF color) = 0; diff --git a/src/terminal/adapter/ut_adapter/adapterTest.cpp b/src/terminal/adapter/ut_adapter/adapterTest.cpp index 13f9476aef3..f65e64059b8 100644 --- a/src/terminal/adapter/ut_adapter/adapterTest.cpp +++ b/src/terminal/adapter/ut_adapter/adapterTest.cpp @@ -418,6 +418,12 @@ class TestGetSet final : public ConGetSet return TRUE; } + bool PrivateClearBuffer() override + { + Log::Comment(L"PrivateClearBuffer MOCK called..."); + return TRUE; + } + bool GetUserDefaultCursorStyle(CursorType& style) override { style = CursorType::Legacy; diff --git a/src/winconpty/dll/winconpty.def b/src/winconpty/dll/winconpty.def index 1ea7e916ae7..da8a5653533 100644 --- a/src/winconpty/dll/winconpty.def +++ b/src/winconpty/dll/winconpty.def @@ -2,3 +2,4 @@ EXPORTS CreatePseudoConsole = ConptyCreatePseudoConsole ResizePseudoConsole = ConptyResizePseudoConsole ClosePseudoConsole = ConptyClosePseudoConsole + ClearPseudoConsole = ConptyClearPseudoConsole diff --git a/src/winconpty/winconpty.cpp b/src/winconpty/winconpty.cpp index 5ef63295517..7653ce6bd5c 100644 --- a/src/winconpty/winconpty.cpp +++ b/src/winconpty/winconpty.cpp @@ -231,6 +231,27 @@ HRESULT _ResizePseudoConsole(_In_ const PseudoConsole* const pPty, _In_ const CO return fSuccess ? S_OK : HRESULT_FROM_WIN32(GetLastError()); } +// Function Description: +// - Clears the conpty +// Arguments: +// - hSignal: A signal pipe as returned by CreateConPty. +// Return Value: +// - S_OK if the call succeeded, else an appropriate HRESULT for failing to +// write the clear message to the pty. +HRESULT _ClearPseudoConsole(_In_ const PseudoConsole* const pPty) +{ + if (pPty == nullptr) + { + return E_INVALIDARG; + } + + unsigned short signalPacket[1]; + signalPacket[0] = PTY_SIGNAL_CLEAR_WINDOW; + + const BOOL fSuccess = WriteFile(pPty->hSignal, signalPacket, sizeof(signalPacket), nullptr, nullptr); + return fSuccess ? S_OK : HRESULT_FROM_WIN32(GetLastError()); +} + // Function Description: // - This closes each of the members of a PseudoConsole. It does not free the // data associated with the PseudoConsole. This is helpful for testing, @@ -385,6 +406,23 @@ extern "C" HRESULT WINAPI ConptyResizePseudoConsole(_In_ HPCON hPC, _In_ COORD s return hr; } +// Function Description: +// - Clear the contents of the conpty buffer, leaving the cursor row at the top +// of the viewport. +// - This is used exclusively by ConPTY to support GH#1193, GH#1882. This allows +// a terminal to clear the contents of the ConPTY buffer, which is important +// if the user would like to be able to clear the terminal-side buffer. +extern "C" HRESULT WINAPI ConptyClearPseudoConsole(_In_ HPCON hPC) +{ + const PseudoConsole* const pPty = (PseudoConsole*)hPC; + HRESULT hr = pPty == nullptr ? E_INVALIDARG : S_OK; + if (SUCCEEDED(hr)) + { + hr = _ClearPseudoConsole(pPty); + } + return hr; +} + // Function Description: // Closes the conpty and all associated state. // Client applications attached to the conpty will also behave as though the diff --git a/src/winconpty/winconpty.h b/src/winconpty/winconpty.h index 0916bcd6330..f2a7ff429ee 100644 --- a/src/winconpty/winconpty.h +++ b/src/winconpty/winconpty.h @@ -17,6 +17,7 @@ typedef struct _PseudoConsole // Signals // These are not defined publicly, but are used for controlling the conpty via // the signal pipe. +#define PTY_SIGNAL_CLEAR_WINDOW (2u) #define PTY_SIGNAL_RESIZE_WINDOW (8u) // CreatePseudoConsole Flags @@ -34,6 +35,7 @@ HRESULT _CreatePseudoConsole(const HANDLE hToken, _Inout_ PseudoConsole* pPty); HRESULT _ResizePseudoConsole(_In_ const PseudoConsole* const pPty, _In_ const COORD size); +HRESULT _ClearPseudoConsole(_In_ const PseudoConsole* const pPty); void _ClosePseudoConsoleMembers(_In_ PseudoConsole* pPty); VOID _ClosePseudoConsole(_In_ PseudoConsole* pPty);