Skip to content

Commit

Permalink
Allow copying with ANSI escape code control sequences (#17059)
Browse files Browse the repository at this point in the history
## Summary of the Pull Request

This extends the copy command to be able to include control sequences,
for use in tools that subsequently know how to parse and display that.

## References and Relevant Issues

#15703

## Detailed Description of the Pull Request / Additional comments

At a high level, this:
- Expands the `CopyTextArgs` to have a `withControlSequences` bool.
- Plumbs that bool down through many layers to where we actuall get
  data out of the text buffer.
- Modifies the existing `TextBuffer::Serialize` to be more generic
  and renames it to `TextBuffer::ChunkedSerialize`.
- Uses the new `ChunkedSerialize` to generate the data for the copy
  request.

## Validation Steps Performed

To test this I've manually:
- Generated some styled terminal contents, copied it with the control
  sequences, pasted it into a file, `cat`ed the file and seen that it
  looks the same.
- Set `"firstWindowPreference": "persistedWindowLayout"` and
  validated that the contents of windows are saved and
  restored with styling intact.

I also checked that `Invoke-OpenConsoleTests` passed.

## PR Checklist
- [x] Closes #15703
- [ ] Tests added/passed
- [x] Documentation updated
- If checked, please file a pull request on [our docs
repo](https://github.com/MicrosoftDocs/terminal) and link it here:
MicrosoftDocs/terminal#756
- [x] Schema updated (if necessary)
  • Loading branch information
FuegoFro authored Nov 20, 2024
1 parent a607029 commit 282670a
Show file tree
Hide file tree
Showing 21 changed files with 348 additions and 243 deletions.
5 changes: 5 additions & 0 deletions doc/cascadia/profiles.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -885,6 +885,11 @@
"default": false,
"description": "If true, the copied content will be copied as a single line (even if there are hard line breaks present in the text). If false, newlines persist from the selected text."
},
"withControlSequences": {
"type": "boolean",
"default": false,
"description": "If true, copied content will contain ANSI escape code control sequences representing the styling of the content."
},
"dismissSelection": {
"type": "boolean",
"default": true,
Expand Down
510 changes: 292 additions & 218 deletions src/buffer/out/textBuffer.cpp

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion src/buffer/out/textBuffer.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,8 @@ class TextBuffer final

std::wstring GetPlainText(const CopyRequest& req) const;

std::wstring GetWithControlSequences(const CopyRequest& req) const;

std::string GenHTML(const CopyRequest& req,
const int fontHeightPoints,
const std::wstring_view fontFaceName,
Expand All @@ -280,7 +282,7 @@ class TextBuffer final
const bool isIntenseBold,
std::function<std::tuple<COLORREF, COLORREF, COLORREF>(const TextAttribute&)> GetAttributeColors) const noexcept;

void Serialize(const wchar_t* destination) const;
void SerializeToPath(const wchar_t* destination) const;

struct PositionInformation
{
Expand Down Expand Up @@ -332,6 +334,8 @@ class TextBuffer final

std::tuple<til::CoordType, til::CoordType, bool> _RowCopyHelper(const CopyRequest& req, const til::CoordType iRow, const ROW& row) const;

void _SerializeRow(const ROW& row, const til::CoordType startX, const til::CoordType endX, const bool addLineBreak, const bool isLastRow, std::wstring& buffer, std::optional<TextAttribute>& previousTextAttr, bool& delayedLineBreak) const;

static void _AppendRTFText(std::string& contentBuilder, const std::wstring_view& text);

Microsoft::Console::Render::Renderer* _renderer = nullptr;
Expand Down
2 changes: 1 addition & 1 deletion src/cascadia/TerminalApp/AppActionHandlers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -548,7 +548,7 @@ namespace winrt::TerminalApp::implementation
{
if (const auto& realArgs = args.ActionArgs().try_as<CopyTextArgs>())
{
const auto handled = _CopyText(realArgs.DismissSelection(), realArgs.SingleLine(), realArgs.CopyFormatting());
const auto handled = _CopyText(realArgs.DismissSelection(), realArgs.SingleLine(), realArgs.WithControlSequences(), realArgs.CopyFormatting());
args.Handled(handled);
}
}
Expand Down
5 changes: 3 additions & 2 deletions src/cascadia/TerminalApp/TerminalPage.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2974,14 +2974,15 @@ namespace winrt::TerminalApp::implementation
// Arguments:
// - dismissSelection: if not enabled, copying text doesn't dismiss the selection
// - singleLine: if enabled, copy contents as a single line of text
// - withControlSequences: if enabled, the copied plain text contains color/style ANSI escape codes from the selection
// - formats: dictate which formats need to be copied
// Return Value:
// - true iff we we able to copy text (if a selection was active)
bool TerminalPage::_CopyText(const bool dismissSelection, const bool singleLine, const Windows::Foundation::IReference<CopyFormat>& formats)
bool TerminalPage::_CopyText(const bool dismissSelection, const bool singleLine, const bool withControlSequences, const Windows::Foundation::IReference<CopyFormat>& formats)
{
if (const auto& control{ _GetActiveControl() })
{
return control.CopySelectionToClipboard(dismissSelection, singleLine, formats);
return control.CopySelectionToClipboard(dismissSelection, singleLine, withControlSequences, formats);
}
return false;
}
Expand Down
2 changes: 1 addition & 1 deletion src/cascadia/TerminalApp/TerminalPage.h
Original file line number Diff line number Diff line change
Expand Up @@ -429,7 +429,7 @@ namespace winrt::TerminalApp::implementation
bool _IsUriSupported(const winrt::Windows::Foundation::Uri& parsedUri);

void _ShowCouldNotOpenDialog(winrt::hstring reason, winrt::hstring uri);
bool _CopyText(const bool dismissSelection, const bool singleLine, const Windows::Foundation::IReference<Microsoft::Terminal::Control::CopyFormat>& formats);
bool _CopyText(const bool dismissSelection, const bool singleLine, const bool withControlSequences, const Windows::Foundation::IReference<Microsoft::Terminal::Control::CopyFormat>& formats);

safe_void_coroutine _SetTaskbarProgressHandler(const IInspectable sender, const IInspectable eventArgs);

Expand Down
6 changes: 4 additions & 2 deletions src/cascadia/TerminalControl/ControlCore.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -574,7 +574,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
else if (vkey == VK_RETURN && !mods.IsCtrlPressed() && !mods.IsAltPressed())
{
// [Shift +] Enter --> copy text
CopySelectionToClipboard(mods.IsShiftPressed(), nullptr);
CopySelectionToClipboard(mods.IsShiftPressed(), false, nullptr);
_terminal->ClearSelection();
_updateSelectionUI();
return true;
Expand Down Expand Up @@ -1315,9 +1315,11 @@ namespace winrt::Microsoft::Terminal::Control::implementation
// Windows Clipboard (CascadiaWin32:main.cpp).
// Arguments:
// - singleLine: collapse all of the text to one line
// - withControlSequences: if enabled, the copied plain text contains color/style ANSI escape codes from the selection
// - formats: which formats to copy (defined by action's CopyFormatting arg). nullptr
// if we should defer which formats are copied to the global setting
bool ControlCore::CopySelectionToClipboard(bool singleLine,
bool withControlSequences,
const Windows::Foundation::IReference<CopyFormat>& formats)
{
::Microsoft::Terminal::Core::Terminal::TextCopyData payload;
Expand All @@ -1339,7 +1341,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation

// extract text from buffer
// RetrieveSelectedTextFromBuffer will lock while it's reading
payload = _terminal->RetrieveSelectedTextFromBuffer(singleLine, copyHtml, copyRtf);
payload = _terminal->RetrieveSelectedTextFromBuffer(singleLine, withControlSequences, copyHtml, copyRtf);
}

copyToClipboard(payload.plainText, payload.html, payload.rtf);
Expand Down
2 changes: 1 addition & 1 deletion src/cascadia/TerminalControl/ControlCore.h
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation

void SendInput(std::wstring_view wstr);
void PasteText(const winrt::hstring& hstr);
bool CopySelectionToClipboard(bool singleLine, const Windows::Foundation::IReference<CopyFormat>& formats);
bool CopySelectionToClipboard(bool singleLine, bool withControlSequences, const Windows::Foundation::IReference<CopyFormat>& formats);
void SelectAll();
void ClearSelection();
bool ToggleBlockSelection();
Expand Down
8 changes: 5 additions & 3 deletions src/cascadia/TerminalControl/ControlInteractivity.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -194,9 +194,11 @@ namespace winrt::Microsoft::Terminal::Control::implementation
// Windows Clipboard (CascadiaWin32:main.cpp).
// Arguments:
// - singleLine: collapse all of the text to one line
// - withControlSequences: if enabled, the copied plain text contains color/style ANSI escape codes from the selection
// - formats: which formats to copy (defined by action's CopyFormatting arg). nullptr
// if we should defer which formats are copied to the global setting
bool ControlInteractivity::CopySelectionToClipboard(bool singleLine,
bool withControlSequences,
const Windows::Foundation::IReference<CopyFormat>& formats)
{
if (_core)
Expand All @@ -213,7 +215,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
// Mark the current selection as copied
_selectionNeedsToBeCopied = false;

return _core->CopySelectionToClipboard(singleLine, formats);
return _core->CopySelectionToClipboard(singleLine, withControlSequences, formats);
}

return false;
Expand Down Expand Up @@ -312,7 +314,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
else
{
// Try to copy the text and clear the selection
const auto successfulCopy = CopySelectionToClipboard(shiftEnabled, nullptr);
const auto successfulCopy = CopySelectionToClipboard(shiftEnabled, false, nullptr);
_core->ClearSelection();
if (_core->CopyOnSelect() || !successfulCopy)
{
Expand Down Expand Up @@ -445,7 +447,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
// IMPORTANT!
// DO NOT clear the selection here!
// Otherwise, the selection will be cleared immediately after you make it.
CopySelectionToClipboard(false, nullptr);
CopySelectionToClipboard(false, false, nullptr);
}

_singleClickTouchdownPos = std::nullopt;
Expand Down
1 change: 1 addition & 0 deletions src/cascadia/TerminalControl/ControlInteractivity.h
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
#pragma endregion

bool CopySelectionToClipboard(bool singleLine,
bool withControlSequences,
const Windows::Foundation::IReference<CopyFormat>& formats);
void RequestPasteTextFromClipboard();
void SetEndSelectionPoint(const Core::Point pixelPosition);
Expand Down
2 changes: 1 addition & 1 deletion src/cascadia/TerminalControl/ControlInteractivity.idl
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ namespace Microsoft.Terminal.Control

InteractivityAutomationPeer OnCreateAutomationPeer();

Boolean CopySelectionToClipboard(Boolean singleLine, Windows.Foundation.IReference<CopyFormat> formats);
Boolean CopySelectionToClipboard(Boolean singleLine, Boolean withControlSequences, Windows.Foundation.IReference<CopyFormat> formats);
void RequestPasteTextFromClipboard();
void SetEndSelectionPoint(Microsoft.Terminal.Core.Point point);

Expand Down
2 changes: 1 addition & 1 deletion src/cascadia/TerminalControl/HwndTerminal.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ try
const auto lock = publicTerminal->_terminal->LockForWriting();
if (publicTerminal->_terminal->IsSelectionActive())
{
const auto bufferData = publicTerminal->_terminal->RetrieveSelectedTextFromBuffer(false, true, true);
const auto bufferData = publicTerminal->_terminal->RetrieveSelectedTextFromBuffer(false, false, true, true);
LOG_IF_FAILED(publicTerminal->_CopyTextToSystemClipboard(bufferData.plainText, bufferData.html, bufferData.rtf));
publicTerminal->_ClearSelection();
return 0;
Expand Down
7 changes: 4 additions & 3 deletions src/cascadia/TerminalControl/TermControl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2590,16 +2590,17 @@ namespace winrt::Microsoft::Terminal::Control::implementation
// Arguments:
// - dismissSelection: dismiss the text selection after copy
// - singleLine: collapse all of the text to one line
// - withControlSequences: if enabled, the copied plain text contains color/style ANSI escape codes from the selection
// - formats: which formats to copy (defined by action's CopyFormatting arg). nullptr
// if we should defer which formats are copied to the global setting
bool TermControl::CopySelectionToClipboard(bool dismissSelection, bool singleLine, const Windows::Foundation::IReference<CopyFormat>& formats)
bool TermControl::CopySelectionToClipboard(bool dismissSelection, bool singleLine, bool withControlSequences, const Windows::Foundation::IReference<CopyFormat>& formats)
{
if (_IsClosing())
{
return false;
}

const auto successfulCopy = _interactivity.CopySelectionToClipboard(singleLine, formats);
const auto successfulCopy = _interactivity.CopySelectionToClipboard(singleLine, withControlSequences, formats);

if (dismissSelection)
{
Expand Down Expand Up @@ -4162,7 +4163,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
const IInspectable& /*args*/)
{
// formats = nullptr -> copy all formats
_interactivity.CopySelectionToClipboard(false, nullptr);
_interactivity.CopySelectionToClipboard(false, false, nullptr);
ContextMenu().Hide();
SelectionContextMenu().Hide();
}
Expand Down
2 changes: 1 addition & 1 deletion src/cascadia/TerminalControl/TermControl.h
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation

hstring GetProfileName() const;

bool CopySelectionToClipboard(bool dismissSelection, bool singleLine, const Windows::Foundation::IReference<CopyFormat>& formats);
bool CopySelectionToClipboard(bool dismissSelection, bool singleLine, bool withControlSequences, const Windows::Foundation::IReference<CopyFormat>& formats);
void PasteTextFromClipboard();
void SelectAll();
bool ToggleBlockSelection();
Expand Down
2 changes: 1 addition & 1 deletion src/cascadia/TerminalControl/TermControl.idl
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ namespace Microsoft.Terminal.Control
event Windows.Foundation.TypedEventHandler<Object, Object> CloseTerminalRequested;
event Windows.Foundation.TypedEventHandler<Object, Object> RestartTerminalRequested;

Boolean CopySelectionToClipboard(Boolean dismissSelection, Boolean singleLine, Windows.Foundation.IReference<CopyFormat> formats);
Boolean CopySelectionToClipboard(Boolean dismissSelection, Boolean singleLine, Boolean withControlSequences, Windows.Foundation.IReference<CopyFormat> formats);
void PasteTextFromClipboard();
void SelectAll();
Boolean ToggleBlockSelection();
Expand Down
2 changes: 1 addition & 1 deletion src/cascadia/TerminalCore/Terminal.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1570,7 +1570,7 @@ std::wstring Terminal::CurrentCommand() const

void Terminal::SerializeMainBuffer(const wchar_t* destination) const
{
_mainBuffer->Serialize(destination);
_mainBuffer->SerializeToPath(destination);
}

void Terminal::ColorSelection(const TextAttribute& attr, winrt::Microsoft::Terminal::Core::MatchMode matchMode)
Expand Down
2 changes: 1 addition & 1 deletion src/cascadia/TerminalCore/Terminal.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,7 @@ class Microsoft::Terminal::Core::Terminal final :
til::point SelectionEndForRendering() const;
const SelectionEndpoint SelectionEndpointTarget() const noexcept;

TextCopyData RetrieveSelectedTextFromBuffer(const bool singleLine, const bool html = false, const bool rtf = false) const;
TextCopyData RetrieveSelectedTextFromBuffer(const bool singleLine, const bool withControlSequences = false, const bool html = false, const bool rtf = false) const;
#pragma endregion

#ifndef NDEBUG
Expand Down
12 changes: 10 additions & 2 deletions src/cascadia/TerminalCore/TerminalSelection.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -844,12 +844,13 @@ void Terminal::ClearSelection()
// - Optionally, get the highlighted text in HTML and RTF formats
// Arguments:
// - singleLine: collapse all of the text to one line. (Turns off trailing whitespace trimming)
// - withControlSequences: if enabled, the copied plain text contains color/style ANSI escape codes from the selection
// - html: also get text in HTML format
// - rtf: also get text in RTF format
// Return Value:
// - Plain and formatted selected text from buffer. Empty string represents no data for that format.
// - If extended to multiple lines, each line is separated by \r\n
Terminal::TextCopyData Terminal::RetrieveSelectedTextFromBuffer(const bool singleLine, const bool html, const bool rtf) const
Terminal::TextCopyData Terminal::RetrieveSelectedTextFromBuffer(const bool singleLine, const bool withControlSequences, const bool html, const bool rtf) const
{
TextCopyData data;

Expand All @@ -867,7 +868,14 @@ Terminal::TextCopyData Terminal::RetrieveSelectedTextFromBuffer(const bool singl
const auto& textBuffer = _activeBuffer();

const auto req = TextBuffer::CopyRequest::FromConfig(textBuffer, _selection->start, _selection->end, singleLine, _selection->blockSelection, _trimBlockSelection);
data.plainText = textBuffer.GetPlainText(req);
if (withControlSequences)
{
data.plainText = textBuffer.GetWithControlSequences(req);
}
else
{
data.plainText = textBuffer.GetPlainText(req);
}

if (html || rtf)
{
Expand Down
5 changes: 5 additions & 0 deletions src/cascadia/TerminalSettingsModel/ActionArgs.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,11 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
str.append(RS_(L"CopyTextCommandKey"));
}

if (WithControlSequences())
{
str.append(L", withControlSequences: true");
}

if (!DismissSelection())
{
str.append(L", dismissSelection: false");
Expand Down
7 changes: 4 additions & 3 deletions src/cascadia/TerminalSettingsModel/ActionArgs.h
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,10 @@ protected: \
// false, if we don't really care if the parameter is required or not.

////////////////////////////////////////////////////////////////////////////////
#define COPY_TEXT_ARGS(X) \
X(bool, DismissSelection, "dismissSelection", false, true) \
X(bool, SingleLine, "singleLine", false, false) \
#define COPY_TEXT_ARGS(X) \
X(bool, DismissSelection, "dismissSelection", false, true) \
X(bool, SingleLine, "singleLine", false, false) \
X(bool, WithControlSequences, "withControlSequences", false, false) \
X(Windows::Foundation::IReference<Control::CopyFormat>, CopyFormatting, "copyFormatting", false, nullptr)

////////////////////////////////////////////////////////////////////////////////
Expand Down
1 change: 1 addition & 0 deletions src/cascadia/TerminalSettingsModel/ActionArgs.idl
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ namespace Microsoft.Terminal.Settings.Model
CopyTextArgs();
Boolean DismissSelection { get; };
Boolean SingleLine { get; };
Boolean WithControlSequences { get; };
Windows.Foundation.IReference<Microsoft.Terminal.Control.CopyFormat> CopyFormatting { get; };
};

Expand Down

0 comments on commit 282670a

Please sign in to comment.