Add support for OSC 52 clipboard copy in conhost (#18949)

This adds support for copying to the clipboard in conhost using the OSC
52 escape sequence, extending the original implementation which was for
Windows Terminal only.

The Windows Terminal implementation was added in PR #5823.

Because the clipboard can't be accessed from a background thread, this
works by saving the content in a global variable, and then posting a
custom message to the main GUI thread, which takes care of the actual
copy operation.

Validation:
I've manually confirmed that tmux copy mode is now able to copy to the
system clipboard.

Closes #18943
This commit is contained in:
James Holderness 2025-06-10 21:09:51 +01:00 committed by GitHub
parent 155d8a9ab2
commit 4abc041eb7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 60 additions and 2 deletions

View File

@ -13,6 +13,7 @@
#include "srvinit.h" #include "srvinit.h"
#include "../interactivity/inc/ServiceLocator.hpp" #include "../interactivity/inc/ServiceLocator.hpp"
#include "../interactivity/win32/CustomWindowMessages.h"
#include "../types/inc/convert.hpp" #include "../types/inc/convert.hpp"
using Microsoft::Console::Interactivity::ServiceLocator; using Microsoft::Console::Interactivity::ServiceLocator;
@ -179,6 +180,32 @@ void CONSOLE_INFORMATION::SetBracketedPasteMode(const bool enabled) noexcept
_bracketedPasteMode = enabled; _bracketedPasteMode = enabled;
} }
void CONSOLE_INFORMATION::CopyTextToClipboard(const std::wstring_view text)
{
const auto window = ServiceLocator::LocateConsoleWindow();
if (window)
{
// The clipboard can only be updated from the main GUI thread, so we
// need to post a message to trigger the actual copy operation. But if
// the pending clipboard content is already set, a message would have
// already been posted, so there's no need to post another one.
const auto clipboardMessageSent = _pendingClipboardText.has_value();
_pendingClipboardText = text;
if (!clipboardMessageSent)
{
PostMessageW(window->GetWindowHandle(), CM_UPDATE_CLIPBOARD, 0, 0);
}
}
}
std::optional<std::wstring> CONSOLE_INFORMATION::UsePendingClipboardText()
{
// Once the pending text has been used, we clear the variable to let the
// CopyTextToClipboard method know that the last CM_UPDATE_CLIPBOARD message
// has been processed, and future updates will require another message.
return std::exchange(_pendingClipboardText, {});
}
// Method Description: // Method Description:
// - Return the active screen buffer of the console. // - Return the active screen buffer of the console.
// Arguments: // Arguments:

View File

@ -280,9 +280,9 @@ unsigned int ConhostInternalGetSet::GetInputCodePage() const
// - content - the text to be copied. // - content - the text to be copied.
// Return Value: // Return Value:
// - <none> // - <none>
void ConhostInternalGetSet::CopyToClipboard(const wil::zwstring_view /*content*/) void ConhostInternalGetSet::CopyToClipboard(const wil::zwstring_view content)
{ {
// TODO ServiceLocator::LocateGlobals().getConsoleInformation().CopyTextToClipboard(content);
} }
// Routine Description: // Routine Description:

View File

@ -126,6 +126,8 @@ public:
bool GetBracketedPasteMode() const noexcept; bool GetBracketedPasteMode() const noexcept;
void SetBracketedPasteMode(const bool enabled) noexcept; void SetBracketedPasteMode(const bool enabled) noexcept;
void CopyTextToClipboard(const std::wstring_view text);
std::optional<std::wstring> UsePendingClipboardText();
void SetTitle(const std::wstring_view newTitle); void SetTitle(const std::wstring_view newTitle);
void SetTitlePrefix(const std::wstring_view newTitlePrefix); void SetTitlePrefix(const std::wstring_view newTitlePrefix);
@ -160,6 +162,7 @@ private:
SCREEN_INFORMATION* pCurrentScreenBuffer = nullptr; SCREEN_INFORMATION* pCurrentScreenBuffer = nullptr;
COOKED_READ_DATA* _cookedReadData = nullptr; // non-ownership pointer COOKED_READ_DATA* _cookedReadData = nullptr; // non-ownership pointer
bool _bracketedPasteMode = false; bool _bracketedPasteMode = false;
std::optional<std::wstring> _pendingClipboardText;
Microsoft::Console::VirtualTerminal::VtIo _vtIo; Microsoft::Console::VirtualTerminal::VtIo _vtIo;
Microsoft::Console::CursorBlinker _blinker; Microsoft::Console::CursorBlinker _blinker;

View File

@ -24,6 +24,22 @@ using namespace Microsoft::Console::Types;
#pragma region Public Methods #pragma region Public Methods
void Clipboard::CopyText(const std::wstring& text)
{
const auto clipboard = _openClipboard(ServiceLocator::LocateConsoleWindow()->GetWindowHandle());
if (!clipboard)
{
LOG_LAST_ERROR();
return;
}
EmptyClipboard();
// As per: https://learn.microsoft.com/en-us/windows/win32/dataxchg/standard-clipboard-formats
// CF_UNICODETEXT: [...] A null character signals the end of the data.
// --> We add +1 to the length. This works because .c_str() is null-terminated.
_copyToClipboard(CF_UNICODETEXT, text.c_str(), (text.size() + 1) * sizeof(wchar_t));
}
// Arguments: // Arguments:
// - fAlsoCopyFormatting - Place colored HTML & RTF text onto the clipboard as well as the usual plain text. // - fAlsoCopyFormatting - Place colored HTML & RTF text onto the clipboard as well as the usual plain text.
// Return Value: // Return Value:

View File

@ -29,4 +29,6 @@
#define CM_SET_KEYBOARD_LAYOUT (WM_USER+19) #define CM_SET_KEYBOARD_LAYOUT (WM_USER+19)
#endif #endif
#define CM_UPDATE_CLIPBOARD (WM_USER+20)
// clang-format on // clang-format on

View File

@ -29,6 +29,7 @@ namespace Microsoft::Console::Interactivity::Win32
public: public:
static Clipboard& Instance(); static Clipboard& Instance();
void CopyText(const std::wstring& text);
void Copy(_In_ const bool fAlsoCopyFormatting = false); void Copy(_In_ const bool fAlsoCopyFormatting = false);
void Paste(); void Paste();
void PasteDrop(HDROP drop); void PasteDrop(HDROP drop);

View File

@ -773,6 +773,15 @@ static constexpr TsfDataProvider s_tsfDataProvider;
} }
#endif // DBG #endif // DBG
case CM_UPDATE_CLIPBOARD:
{
if (const auto clipboardText = gci.UsePendingClipboardText())
{
Clipboard::Instance().CopyText(clipboardText.value());
}
break;
}
case EVENT_CONSOLE_CARET: case EVENT_CONSOLE_CARET:
case EVENT_CONSOLE_UPDATE_REGION: case EVENT_CONSOLE_UPDATE_REGION:
case EVENT_CONSOLE_UPDATE_SIMPLE: case EVENT_CONSOLE_UPDATE_SIMPLE: