Skip to content

Commit

Permalink
[RFC] Allow copying ANSI escape code control sequences
Browse files Browse the repository at this point in the history
This is an initial attempt at allowing including control sequences in
text copied from the terminal, for use in tools that subsequently know
how to parse and display that. See microsoft#15703.

At a high level, so far 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.
- Adds basic proof-of-concept functionality in the text buffer to
  generate the relevant control sequences based on the text attributes
  and uses that when the bool is set.

Right now I'm mainly looking for feedback on whether this is something
that might be accepted (is this functionality that's wanted) and whether
this approach broadly seems reasonable. If both of those "checks" pass,
I'll work on polishing this up with completeness, tests, etc.
  • Loading branch information
FuegoFro committed Apr 11, 2024
1 parent 5f3a857 commit 0db340d
Show file tree
Hide file tree
Showing 20 changed files with 147 additions and 23 deletions.
5 changes: 5 additions & 0 deletions doc/cascadia/profiles.schema.json
Expand Up @@ -851,6 +851,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
93 changes: 93 additions & 0 deletions src/buffer/out/textBuffer.cpp
Expand Up @@ -12,9 +12,11 @@
#include "../../types/inc/GlyphWidth.hpp"
#include "../renderer/base/renderer.hpp"
#include "../types/inc/utils.hpp"
#include "../terminal/adapter/DispatchTypes.hpp"

using namespace Microsoft::Console;
using namespace Microsoft::Console::Types;
using namespace Microsoft::Console::VirtualTerminal::DispatchTypes;

using PointTree = interval_tree::IntervalTree<til::point, size_t>;

Expand Down Expand Up @@ -2102,6 +2104,97 @@ std::wstring TextBuffer::GetPlainText(const CopyRequest& req) const
return selectedText;
}

// TODO - Description
std::wstring TextBuffer::GetWithControlSequences(const CopyRequest& req) const
{
if (req.beg > req.end)
{
return {};
}

std::wstring selectedText;
TextAttribute previousAttr;

for (auto iRow = req.beg.y; iRow <= req.end.y; ++iRow)
{
const auto& row = GetRowByOffset(iRow);
const auto [rowBeg, rowEnd, addLineBreak] = _RowCopyHelper(req, iRow, row);
const auto rowBegU16 = gsl::narrow_cast<uint16_t>(rowBeg);
const auto rowEndU16 = gsl::narrow_cast<uint16_t>(rowEnd);
const auto runs = row.Attributes().slice(rowBegU16, rowEndU16).runs();

auto x = rowBegU16;
for (const auto& [attr, length] : runs)
{
const auto nextX = gsl::narrow_cast<uint16_t>(x + length);
// TODO - Rest of the implementation, currently just handling Index16 foreground colors
const auto fg = attr.GetForeground();

if (fg != previousAttr.GetForeground())
{
auto fgCode = GraphicsOptions::ForegroundDefault;

if (!fg.IsDefault() && fg.IsIndex16())
{
switch (fg.GetIndex())
{
case TextColor::DARK_BLACK:
fgCode = GraphicsOptions::ForegroundBlack;
break;
case TextColor::DARK_BLUE:
fgCode = GraphicsOptions::ForegroundBlue;
break;
case TextColor::DARK_GREEN:
fgCode = GraphicsOptions::ForegroundGreen;
break;
case TextColor::DARK_CYAN:
fgCode = GraphicsOptions::ForegroundCyan;
break;
case TextColor::DARK_RED:
fgCode = GraphicsOptions::ForegroundRed;
break;
case TextColor::DARK_MAGENTA:
fgCode = GraphicsOptions::ForegroundMagenta;
break;
case TextColor::DARK_YELLOW:
fgCode = GraphicsOptions::ForegroundYellow;
break;
case TextColor::DARK_WHITE:
fgCode = GraphicsOptions::ForegroundWhite;
break;
}
}
selectedText += L"\x1b[" + std::to_wstring(fgCode) + L"m";
}
//const auto [fg, bg, ul] = GetAttributeColors(attr);
//const auto fgHex = Utils::ColorToHexString(fg);
//const auto bgHex = Utils::ColorToHexString(bg);
//const auto ulHex = Utils::ColorToHexString(ul);
//const auto ulStyle = attr.GetUnderlineStyle();
//const auto isUnderlined = ulStyle != UnderlineStyle::NoUnderline;
//const auto isCrossedOut = attr.IsCrossedOut();
//const auto isOverlined = attr.IsOverlined();
//if (isIntenseBold && attr.IsIntense())
//if (attr.IsItalic())

// text
selectedText += row.GetText(x, nextX);

// advance to next run of text
x = nextX;
previousAttr = attr;
}

if (addLineBreak && iRow != req.end.y)
{
// TODO - Add a reset?
selectedText += L"\r\n";
}
}

return selectedText;
}

// Routine Description:
// - Generates a CF_HTML compliant structure from the selected region of the buffer
// Arguments:
Expand Down
2 changes: 2 additions & 0 deletions src/buffer/out/textBuffer.hpp
Expand Up @@ -271,6 +271,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 Down
2 changes: 1 addition & 1 deletion src/cascadia/TerminalApp/AppActionHandlers.cpp
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
Expand Up @@ -2856,14 +2856,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
Expand Up @@ -401,7 +401,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);

winrt::fire_and_forget _SetTaskbarProgressHandler(const IInspectable sender, const IInspectable eventArgs);

Expand Down
6 changes: 4 additions & 2 deletions src/cascadia/TerminalControl/ControlCore.cpp
Expand Up @@ -568,7 +568,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 @@ -1307,9 +1307,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 @@ -1331,7 +1333,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
Expand Up @@ -121,7 +121,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation

void SendInput(const winrt::hstring& 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
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 @@ -441,7 +443,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
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
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
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
8 changes: 5 additions & 3 deletions src/cascadia/TerminalControl/TermControl.cpp
Expand Up @@ -2217,16 +2217,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 @@ -3697,7 +3698,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation
const IInspectable& /*args*/)
{
// formats = nullptr -> copy all formats
_interactivity.CopySelectionToClipboard(false, nullptr);
// TODO - Maybe use args here???
_interactivity.CopySelectionToClipboard(false, false, nullptr);
ContextMenu().Hide();
SelectionContextMenu().Hide();
}
Expand Down
2 changes: 1 addition & 1 deletion src/cascadia/TerminalControl/TermControl.h
Expand Up @@ -38,7 +38,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
Expand Up @@ -84,7 +84,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.hpp
Expand Up @@ -313,7 +313,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
Expand Up @@ -871,12 +871,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 @@ -894,7 +895,14 @@ Terminal::TextCopyData Terminal::RetrieveSelectedTextFromBuffer(const bool singl
const auto& textBuffer = _activeBuffer();

const auto req = TextBuffer::CopyRequest::FromConfig(textBuffer, _selection->start, _selection->end, singleLine, _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
6 changes: 6 additions & 0 deletions src/cascadia/TerminalSettingsModel/ActionArgs.cpp
Expand Up @@ -206,6 +206,12 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
ss << RS_(L"CopyTextCommandKey").c_str();
}

// TODO - better names if WithControlSequences() is true?
if (WithControlSequences())
{
ss << L", withControlSequences: true";
}

if (!DismissSelection())
{
ss << L", dismissSelection: false";
Expand Down
7 changes: 4 additions & 3 deletions src/cascadia/TerminalSettingsModel/ActionArgs.h
Expand Up @@ -102,9 +102,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

0 comments on commit 0db340d

Please sign in to comment.