Skip to content

Commit

Permalink
Allow exporting terminal buffer into file via tab context menu (#11062)
Browse files Browse the repository at this point in the history
## 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.
  • Loading branch information
Don-Vito authored Aug 31, 2021
1 parent 8d81497 commit c089ae0
Show file tree
Hide file tree
Showing 12 changed files with 129 additions and 0 deletions.
12 changes: 12 additions & 0 deletions src/cascadia/TerminalApp/Resources/en-US/Resources.resw
Original file line number Diff line number Diff line change
Expand Up @@ -685,4 +685,16 @@
<data name="DropPathTabSplit.Text" xml:space="preserve">
<value>Split the window and start in given directory</value>
</data>
<data name="ExportTabText" xml:space="preserve">
<value>Export Text</value>
</data>
<data name="ExportFailure" xml:space="preserve">
<value>Failed to export terminal content</value>
</data>
<data name="ExportSuccess" xml:space="preserve">
<value>Successfully exported terminal content</value>
</data>
<data name="PlainText" xml:space="preserve">
<value>Plain Text</value>
</data>
</root>
52 changes: 52 additions & 0 deletions src/cascadia/TerminalApp/TabManagement.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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<IInitializeWithWindow>()->Initialize(*_hostingHwnd);
savePicker.SuggestedStartLocation(PickerLocationId::Downloads);
const auto fileChoices = single_threaded_vector<hstring>({ 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:
Expand Down
1 change: 1 addition & 0 deletions src/cascadia/TerminalApp/TerminalPage.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
19 changes: 19 additions & 0 deletions src/cascadia/TerminalApp/TerminalTab.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1211,13 +1211,31 @@ 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;
contextMenuFlyout.Items().Append(chooseColorMenuItem);
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
Expand Down Expand Up @@ -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<>);
}
1 change: 1 addition & 0 deletions src/cascadia/TerminalApp/TerminalTab.h
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
3 changes: 3 additions & 0 deletions src/cascadia/TerminalApp/pch.h
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@
#include <winrt/Microsoft.Terminal.TerminalConnection.h>
#include <winrt/Microsoft.Terminal.Settings.Editor.h>
#include <winrt/Microsoft.Terminal.Settings.Model.h>
#include <winrt/Windows.Storage.h>
#include <winrt/Windows.Storage.Provider.h>
#include <winrt/Windows.Storage.Pickers.h>

#include <windows.ui.xaml.media.dxinterop.h>

Expand Down
28 changes: 28 additions & 0 deletions src/cascadia/TerminalControl/ControlCore.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}

}
2 changes: 2 additions & 0 deletions src/cascadia/TerminalControl/ControlCore.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 2 additions & 0 deletions src/cascadia/TerminalControl/ControlCore.idl
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ namespace Microsoft.Terminal.Control
Boolean CursorOn;
void EnablePainting();

String ReadEntireBuffer();

event FontSizeChangedEventArgs FontSizeChanged;

event Windows.Foundation.TypedEventHandler<Object, CopyToClipboardEventArgs> CopyToClipboard;
Expand Down
5 changes: 5 additions & 0 deletions src/cascadia/TerminalControl/TermControl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2586,4 +2586,9 @@ namespace winrt::Microsoft::Terminal::Control::implementation
{
_playWarningBell->Run();
}

hstring TermControl::ReadEntireBuffer() const
{
return _core.ReadEntireBuffer();
}
}
2 changes: 2 additions & 0 deletions src/cascadia/TerminalControl/TermControl.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 2 additions & 0 deletions src/cascadia/TerminalControl/TermControl.idl
Original file line number Diff line number Diff line change
Expand Up @@ -67,5 +67,7 @@ namespace Microsoft.Terminal.Control

Boolean ReadOnly { get; };
void ToggleReadOnly();

String ReadEntireBuffer();
}
}

0 comments on commit c089ae0

Please sign in to comment.