From 6268a4779c9b3f49b7f3c30471bda7ae292ac423 Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Thu, 2 Sep 2021 09:59:42 -0500 Subject: [PATCH] Implement and action for manually clearing the Terminal (and conpty) buffer (#10906) ## Summary of the Pull Request ![clear-buffer-000](https://user-images.githubusercontent.com/18356694/127570078-90c6089e-0430-4dfc-bcd4-a0cde20c9167.gif) This adds a new action, `clearBuffer`. It accepts 3 values for the `clear` type: * `"clear": "screen"`: Clear the terminal viewport content. Leaves the scrollback untouched. Moves the cursor row to the top of the viewport (unmodified). * `"clear": "scrollback"`: Clear the scrollback. Leaves the viewport untouched. * `"clear": "all"`: (**default**) Clear the scrollback and the visible viewport. Moves the cursor row to the top of the viewport (unmodified). "Clear Buffer" has also been added to `defaults.json`. ## References * From microsoft/vscode#75141 originally ## PR Checklist * [x] Closes #1193 * [x] Closes #1882 * [x] I work here * [x] Tests added/passed * [ ] Requires documentation to be updated ## Detailed Description of the Pull Request / Additional comments This is a bit tricky, because we need to plumb it all the way through conpty to clear the buffer. If we don't, then conpty will immediately just redraw the screen. So this sends a signal to the attached conpty, and then waits for conpty to draw the updated, cleared, screen back to us. ## Validation Steps Performed * works for each of the three clear types as expected * tests pass. * works even with `ping -t 8.8.8.8` as you'd hope. --- .../TerminalApp/AppActionHandlers.cpp | 16 +++ .../TerminalConnection/ConptyConnection.cpp | 10 ++ .../TerminalConnection/ConptyConnection.h | 1 + .../TerminalConnection/ConptyConnection.idl | 1 + src/cascadia/TerminalControl/ControlCore.cpp | 31 ++++ src/cascadia/TerminalControl/ControlCore.h | 3 + src/cascadia/TerminalControl/ControlCore.idl | 9 ++ src/cascadia/TerminalControl/TermControl.cpp | 4 + src/cascadia/TerminalControl/TermControl.h | 2 + src/cascadia/TerminalControl/TermControl.idl | 2 + .../TerminalSettingsModel/ActionAndArgs.cpp | 2 + .../TerminalSettingsModel/ActionArgs.cpp | 19 +++ .../TerminalSettingsModel/ActionArgs.h | 53 +++++++ .../TerminalSettingsModel/ActionArgs.idl | 8 +- .../AllShortcutActions.h | 2 + .../Resources/en-US/Resources.resw | 12 ++ .../TerminalSettingsSerializationHelpers.h | 10 ++ .../TerminalSettingsModel/defaults.json | 1 + .../UnitTests_Control/ControlCoreTests.cpp | 132 ++++++++++++++++++ .../ConptyRoundtripTests.cpp | 83 +++++++++++ src/host/PtySignalInputThread.cpp | 20 +++ src/host/PtySignalInputThread.hpp | 2 + src/host/getset.cpp | 6 + src/host/getset.h | 1 + src/host/outputStream.cpp | 5 + src/host/outputStream.hpp | 1 + src/host/screenInfo.cpp | 49 +++++++ src/host/screenInfo.hpp | 1 + src/inc/conpty-static.h | 2 + src/inc/conpty.h | 1 + src/terminal/adapter/conGetSet.hpp | 1 + .../adapter/ut_adapter/adapterTest.cpp | 6 + src/winconpty/dll/winconpty.def | 1 + src/winconpty/winconpty.cpp | 38 +++++ src/winconpty/winconpty.h | 2 + 35 files changed, 536 insertions(+), 1 deletion(-) diff --git a/src/cascadia/TerminalApp/AppActionHandlers.cpp b/src/cascadia/TerminalApp/AppActionHandlers.cpp index e7f1cfb95..5638c6bb8 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 d82b6be0a..5c2373fd2 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 8f82a617b..9a2fc3a6e 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 a1cfa9790..2e6cce5c9 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 c5e0cfac5..eac350c2a 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 57052cdd0..a04032ef7 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 154fa869b..84cb83e80 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 e1a2afb37..1c698d8b4 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 521c897fd..9d0f22e33 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 325d813e2..26db0862c 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 02bb1d486..a6094855b 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 99d55d38e..8d2e24d58 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 96dbe07ce..3ae870039 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 3d89466e9..03b36815d 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 e2f5488e4..f198dfc10 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 9ab450753..08d2a1e3d 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 188587f57..674a42fbe 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 ee1cdeb76..9f942d821 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 e57dd8c23..9e8599032 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 1543ea632..fe671a65a 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 df42f45f7..3855a467a 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 d54cf6833..e6c4d8bb4 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 9c3ccde44..36f2ebbd8 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 1fb48e286..e4c539580 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 a70b48afd..bc15c1cdf 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 99889baca..9df19446f 100644 --- a/src/host/outputStream.hpp +++ b/src/host/outputStream.hpp @@ -104,6 +104,7 @@ public: 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 5323cb574..320e1148d 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 3fa091de2..e3b1eed4d 100644 --- a/src/host/screenInfo.hpp +++ b/src/host/screenInfo.hpp @@ -222,6 +222,7 @@ public: 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 bd3ac59ca..052708a6b 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 ff980d7c2..6f51ba942 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 e69ab753f..7576d3801 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 13f9476ae..f65e64059 100644 --- a/src/terminal/adapter/ut_adapter/adapterTest.cpp +++ b/src/terminal/adapter/ut_adapter/adapterTest.cpp @@ -418,6 +418,12 @@ public: 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 1ea7e916a..da8a56535 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 5ef632955..7653ce6bd 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 0916bcd63..f2a7ff429 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);