diff --git a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw index f9e8cbf1265..9f50d6718c2 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 849bfcc69d3..0a1f91e4616 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 0bdcf4aaeb1..d598d095556 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 6f415754c38..a3a2a1651b8 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 4952fad4518..b1824d98eaa 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 1f84f821e7d..66026a78dfd 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 3dcf284a7e5..c5e0cfac584 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 3bb321f0b55..57052cdd054 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 fa4746ca404..154fa869b0d 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 3e3d7414ea1..e1a2afb378f 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 e2ee89b827d..521c897fddf 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 1a043e95b5e..325d813e2f4 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(); } }