From c089ae0c57d9ecc6d6e0f99eeb6cbcb655bd720e Mon Sep 17 00:00:00 2001 From: Don-Vito Date: Tue, 31 Aug 2021 22:36:43 +0300 Subject: [PATCH] Allow exporting terminal buffer into file via tab context menu (#11062) ## Summary of the Pull Request **Naive implementation** of exporting the text buffer of the current pane into a text file triggered from the tab context menu. **Disclaimer: this is not an export of the command history,** but rather just a text buffer dumped into a file when asked explicitly. ## References Should provide partial solution for #642. ## Detailed Description of the Pull Request / Additional comments The logic is following: * Open a file save picker * The location is Downloads folder (should be always accessible) * The suggest name of the file equals to the pane's title * The allowed file formats list contains .txt only * If no file selected stop * Lock terminal * Read all lines till the cursor * Format each line by removing trailing white-spaces and adding CRLF if not wrapped * Asynchronously write to selected file * Show confirmation As the action is relatively fast didn't add a progress bar or any other UX. As the buffer is relatively small, holding it entirely in the memory rather than writing line by line to disk. --- .../Resources/en-US/Resources.resw | 12 +++++ src/cascadia/TerminalApp/TabManagement.cpp | 52 +++++++++++++++++++ src/cascadia/TerminalApp/TerminalPage.h | 1 + src/cascadia/TerminalApp/TerminalTab.cpp | 19 +++++++ src/cascadia/TerminalApp/TerminalTab.h | 1 + src/cascadia/TerminalApp/pch.h | 3 ++ src/cascadia/TerminalControl/ControlCore.cpp | 28 ++++++++++ src/cascadia/TerminalControl/ControlCore.h | 2 + src/cascadia/TerminalControl/ControlCore.idl | 2 + src/cascadia/TerminalControl/TermControl.cpp | 5 ++ src/cascadia/TerminalControl/TermControl.h | 2 + src/cascadia/TerminalControl/TermControl.idl | 2 + 12 files changed, 129 insertions(+) diff --git a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw index f9e8cbf12..9f50d6718 100644 --- a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw @@ -685,4 +685,16 @@ Split the window and start in given directory + + Export Text + + + Failed to export terminal content + + + Successfully exported terminal content + + + Plain Text + diff --git a/src/cascadia/TerminalApp/TabManagement.cpp b/src/cascadia/TerminalApp/TabManagement.cpp index 849bfcc69..0a1f91e46 100644 --- a/src/cascadia/TerminalApp/TabManagement.cpp +++ b/src/cascadia/TerminalApp/TabManagement.cpp @@ -28,6 +28,9 @@ using namespace winrt::Windows::UI::Core; using namespace winrt::Windows::System; using namespace winrt::Windows::ApplicationModel::DataTransfer; using namespace winrt::Windows::UI::Text; +using namespace winrt::Windows::Storage; +using namespace winrt::Windows::Storage::Pickers; +using namespace winrt::Windows::Storage::Provider; using namespace winrt::Microsoft::Terminal; using namespace winrt::Microsoft::Terminal::Control; using namespace winrt::Microsoft::Terminal::TerminalConnection; @@ -171,6 +174,16 @@ namespace winrt::TerminalApp::implementation } }); + newTabImpl->ExportTabRequested([weakTab, weakThis{ get_weak() }]() { + auto page{ weakThis.get() }; + auto tab{ weakTab.get() }; + + if (page && tab) + { + page->_ExportTab(*tab); + } + }); + auto tabViewItem = newTabImpl->TabViewItem(); _tabView.TabItems().Append(tabViewItem); @@ -391,6 +404,45 @@ namespace winrt::TerminalApp::implementation CATCH_LOG(); } + // Method Description: + // - Exports the content of the Terminal Buffer inside the tab + // Arguments: + // - tab: tab to export + winrt::fire_and_forget TerminalPage::_ExportTab(const TerminalTab& tab) + { + try + { + if (const auto control{ tab.GetActiveTerminalControl() }) + { + const FileSavePicker savePicker; + savePicker.as()->Initialize(*_hostingHwnd); + savePicker.SuggestedStartLocation(PickerLocationId::Downloads); + const auto fileChoices = single_threaded_vector({ L".txt" }); + savePicker.FileTypeChoices().Insert(RS_(L"PlainText"), fileChoices); + savePicker.SuggestedFileName(control.Title()); + + const StorageFile file = co_await savePicker.PickSaveFileAsync(); + if (file != nullptr) + { + const auto buffer = control.ReadEntireBuffer(); + CachedFileManager::DeferUpdates(file); + co_await FileIO::WriteTextAsync(file, buffer); + const auto status = co_await CachedFileManager::CompleteUpdatesAsync(file); + switch (status) + { + case FileUpdateStatus::Complete: + case FileUpdateStatus::CompleteAndRenamed: + _ShowControlNoticeDialog(RS_(L"NoticeInfo"), RS_(L"ExportSuccess")); + break; + default: + _ShowControlNoticeDialog(RS_(L"NoticeError"), RS_(L"ExportFailure")); + } + } + } + } + CATCH_LOG(); + } + // Method Description: // - Removes the tab (both TerminalControl and XAML) after prompting for approval // Arguments: diff --git a/src/cascadia/TerminalApp/TerminalPage.h b/src/cascadia/TerminalApp/TerminalPage.h index 0bdcf4aae..d598d0955 100644 --- a/src/cascadia/TerminalApp/TerminalPage.h +++ b/src/cascadia/TerminalApp/TerminalPage.h @@ -220,6 +220,7 @@ namespace winrt::TerminalApp::implementation void _DuplicateTab(const TerminalTab& tab); void _SplitTab(TerminalTab& tab); + winrt::fire_and_forget _ExportTab(const TerminalTab& tab); winrt::Windows::Foundation::IAsyncAction _HandleCloseTabRequested(winrt::TerminalApp::TabBase tab); void _CloseTabAtIndex(uint32_t index); diff --git a/src/cascadia/TerminalApp/TerminalTab.cpp b/src/cascadia/TerminalApp/TerminalTab.cpp index 6f415754c..a3a2a1651 100644 --- a/src/cascadia/TerminalApp/TerminalTab.cpp +++ b/src/cascadia/TerminalApp/TerminalTab.cpp @@ -1211,6 +1211,23 @@ namespace winrt::TerminalApp::implementation splitTabMenuItem.Icon(splitTabSymbol); } + Controls::MenuFlyoutItem exportTabMenuItem; + { + // "Split Tab" + Controls::FontIcon exportTabSymbol; + exportTabSymbol.FontFamily(Media::FontFamily{ L"Segoe MDL2 Assets" }); + exportTabSymbol.Glyph(L"\xE74E"); // Save + + exportTabMenuItem.Click([weakThis](auto&&, auto&&) { + if (auto tab{ weakThis.get() }) + { + tab->_ExportTabRequestedHandlers(); + } + }); + exportTabMenuItem.Text(RS_(L"ExportTabText")); + exportTabMenuItem.Icon(exportTabSymbol); + } + // Build the menu Controls::MenuFlyout contextMenuFlyout; Controls::MenuFlyoutSeparator menuSeparator; @@ -1218,6 +1235,7 @@ namespace winrt::TerminalApp::implementation contextMenuFlyout.Items().Append(renameTabMenuItem); contextMenuFlyout.Items().Append(duplicateTabMenuItem); contextMenuFlyout.Items().Append(splitTabMenuItem); + contextMenuFlyout.Items().Append(exportTabMenuItem); contextMenuFlyout.Items().Append(menuSeparator); // GH#5750 - When the context menu is dismissed with ESC, toss the focus @@ -1592,4 +1610,5 @@ namespace winrt::TerminalApp::implementation DEFINE_EVENT(TerminalTab, TabRaiseVisualBell, _TabRaiseVisualBellHandlers, winrt::delegate<>); DEFINE_EVENT(TerminalTab, DuplicateRequested, _DuplicateRequestedHandlers, winrt::delegate<>); DEFINE_EVENT(TerminalTab, SplitTabRequested, _SplitTabRequestedHandlers, winrt::delegate<>); + DEFINE_EVENT(TerminalTab, ExportTabRequested, _ExportTabRequestedHandlers, winrt::delegate<>); } diff --git a/src/cascadia/TerminalApp/TerminalTab.h b/src/cascadia/TerminalApp/TerminalTab.h index 4952fad45..b1824d98e 100644 --- a/src/cascadia/TerminalApp/TerminalTab.h +++ b/src/cascadia/TerminalApp/TerminalTab.h @@ -103,6 +103,7 @@ namespace winrt::TerminalApp::implementation DECLARE_EVENT(TabRaiseVisualBell, _TabRaiseVisualBellHandlers, winrt::delegate<>); DECLARE_EVENT(DuplicateRequested, _DuplicateRequestedHandlers, winrt::delegate<>); DECLARE_EVENT(SplitTabRequested, _SplitTabRequestedHandlers, winrt::delegate<>); + DECLARE_EVENT(ExportTabRequested, _ExportTabRequestedHandlers, winrt::delegate<>); TYPED_EVENT(TaskbarProgressChanged, IInspectable, IInspectable); private: diff --git a/src/cascadia/TerminalApp/pch.h b/src/cascadia/TerminalApp/pch.h index 1f84f821e..66026a78d 100644 --- a/src/cascadia/TerminalApp/pch.h +++ b/src/cascadia/TerminalApp/pch.h @@ -56,6 +56,9 @@ #include #include #include +#include +#include +#include #include diff --git a/src/cascadia/TerminalControl/ControlCore.cpp b/src/cascadia/TerminalControl/ControlCore.cpp index 3dcf284a7..c5e0cfac5 100644 --- a/src/cascadia/TerminalControl/ControlCore.cpp +++ b/src/cascadia/TerminalControl/ControlCore.cpp @@ -1499,4 +1499,32 @@ namespace winrt::Microsoft::Terminal::Control::implementation _updatePatternLocations->Run(); } + hstring ControlCore::ReadEntireBuffer() const + { + auto terminalLock = _terminal->LockForWriting(); + + const auto& textBuffer = _terminal->GetTextBuffer(); + + std::wstringstream ss; + const auto lastRow = textBuffer.GetLastNonSpaceCharacter().Y; + for (auto rowIndex = 0; rowIndex <= lastRow; rowIndex++) + { + const auto& row = textBuffer.GetRowByOffset(rowIndex); + auto rowText = row.GetText(); + const auto strEnd = rowText.find_last_not_of(UNICODE_SPACE); + if (strEnd != std::string::npos) + { + rowText.erase(strEnd + 1); + ss << rowText; + } + + if (!row.WasWrapForced()) + { + ss << UNICODE_CARRIAGERETURN << UNICODE_LINEFEED; + } + } + + return hstring(ss.str()); + } + } diff --git a/src/cascadia/TerminalControl/ControlCore.h b/src/cascadia/TerminalControl/ControlCore.h index 3bb321f0b..57052cdd0 100644 --- a/src/cascadia/TerminalControl/ControlCore.h +++ b/src/cascadia/TerminalControl/ControlCore.h @@ -144,6 +144,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation bool IsInReadOnlyMode() const; void ToggleReadOnlyMode(); + hstring ReadEntireBuffer() const; + // -------------------------------- WinRT Events --------------------------------- // clang-format off WINRT_CALLBACK(FontSizeChanged, Control::FontSizeChangedEventArgs); diff --git a/src/cascadia/TerminalControl/ControlCore.idl b/src/cascadia/TerminalControl/ControlCore.idl index fa4746ca4..154fa869b 100644 --- a/src/cascadia/TerminalControl/ControlCore.idl +++ b/src/cascadia/TerminalControl/ControlCore.idl @@ -81,6 +81,8 @@ namespace Microsoft.Terminal.Control Boolean CursorOn; void EnablePainting(); + String ReadEntireBuffer(); + event FontSizeChangedEventArgs FontSizeChanged; event Windows.Foundation.TypedEventHandler CopyToClipboard; diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index 3e3d7414e..e1a2afb37 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -2586,4 +2586,9 @@ namespace winrt::Microsoft::Terminal::Control::implementation { _playWarningBell->Run(); } + + hstring TermControl::ReadEntireBuffer() const + { + return _core.ReadEntireBuffer(); + } } diff --git a/src/cascadia/TerminalControl/TermControl.h b/src/cascadia/TerminalControl/TermControl.h index e2ee89b82..521c897fd 100644 --- a/src/cascadia/TerminalControl/TermControl.h +++ b/src/cascadia/TerminalControl/TermControl.h @@ -107,6 +107,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation static unsigned int GetPointerUpdateKind(const winrt::Windows::UI::Input::PointerPoint point); static Windows::UI::Xaml::Thickness ParseThicknessFromPadding(const hstring padding); + hstring ReadEntireBuffer() const; + // -------------------------------- WinRT Events --------------------------------- // clang-format off WINRT_CALLBACK(FontSizeChanged, Control::FontSizeChangedEventArgs); diff --git a/src/cascadia/TerminalControl/TermControl.idl b/src/cascadia/TerminalControl/TermControl.idl index 1a043e95b..325d813e2 100644 --- a/src/cascadia/TerminalControl/TermControl.idl +++ b/src/cascadia/TerminalControl/TermControl.idl @@ -67,5 +67,7 @@ namespace Microsoft.Terminal.Control Boolean ReadOnly { get; }; void ToggleReadOnly(); + + String ReadEntireBuffer(); } }