From 4abc041eb78902708e1844902e40a88d0bc3d50a Mon Sep 17 00:00:00 2001 From: James Holderness Date: Tue, 10 Jun 2025 21:09:51 +0100 Subject: [PATCH] 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 --- src/host/consoleInformation.cpp | 27 +++++++++++++++++++ src/host/outputStream.cpp | 4 +-- src/host/server.h | 3 +++ src/interactivity/win32/Clipboard.cpp | 16 +++++++++++ .../win32/CustomWindowMessages.h | 2 ++ src/interactivity/win32/clipboard.hpp | 1 + src/interactivity/win32/windowproc.cpp | 9 +++++++ 7 files changed, 60 insertions(+), 2 deletions(-) diff --git a/src/host/consoleInformation.cpp b/src/host/consoleInformation.cpp index d4e5926f6c..7146bee438 100644 --- a/src/host/consoleInformation.cpp +++ b/src/host/consoleInformation.cpp @@ -13,6 +13,7 @@ #include "srvinit.h" #include "../interactivity/inc/ServiceLocator.hpp" +#include "../interactivity/win32/CustomWindowMessages.h" #include "../types/inc/convert.hpp" using Microsoft::Console::Interactivity::ServiceLocator; @@ -179,6 +180,32 @@ void CONSOLE_INFORMATION::SetBracketedPasteMode(const bool enabled) noexcept _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 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: // - Return the active screen buffer of the console. // Arguments: diff --git a/src/host/outputStream.cpp b/src/host/outputStream.cpp index 3e8219a9bb..9107413e7e 100644 --- a/src/host/outputStream.cpp +++ b/src/host/outputStream.cpp @@ -280,9 +280,9 @@ unsigned int ConhostInternalGetSet::GetInputCodePage() const // - content - the text to be copied. // Return Value: // - -void ConhostInternalGetSet::CopyToClipboard(const wil::zwstring_view /*content*/) +void ConhostInternalGetSet::CopyToClipboard(const wil::zwstring_view content) { - // TODO + ServiceLocator::LocateGlobals().getConsoleInformation().CopyTextToClipboard(content); } // Routine Description: diff --git a/src/host/server.h b/src/host/server.h index d62484b3eb..d318808a8c 100644 --- a/src/host/server.h +++ b/src/host/server.h @@ -126,6 +126,8 @@ public: bool GetBracketedPasteMode() const noexcept; void SetBracketedPasteMode(const bool enabled) noexcept; + void CopyTextToClipboard(const std::wstring_view text); + std::optional UsePendingClipboardText(); void SetTitle(const std::wstring_view newTitle); void SetTitlePrefix(const std::wstring_view newTitlePrefix); @@ -160,6 +162,7 @@ private: SCREEN_INFORMATION* pCurrentScreenBuffer = nullptr; COOKED_READ_DATA* _cookedReadData = nullptr; // non-ownership pointer bool _bracketedPasteMode = false; + std::optional _pendingClipboardText; Microsoft::Console::VirtualTerminal::VtIo _vtIo; Microsoft::Console::CursorBlinker _blinker; diff --git a/src/interactivity/win32/Clipboard.cpp b/src/interactivity/win32/Clipboard.cpp index 8c42e91af9..b022947bbd 100644 --- a/src/interactivity/win32/Clipboard.cpp +++ b/src/interactivity/win32/Clipboard.cpp @@ -24,6 +24,22 @@ using namespace Microsoft::Console::Types; #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: // - fAlsoCopyFormatting - Place colored HTML & RTF text onto the clipboard as well as the usual plain text. // Return Value: diff --git a/src/interactivity/win32/CustomWindowMessages.h b/src/interactivity/win32/CustomWindowMessages.h index bf36660249..bcc43fdb7a 100644 --- a/src/interactivity/win32/CustomWindowMessages.h +++ b/src/interactivity/win32/CustomWindowMessages.h @@ -29,4 +29,6 @@ #define CM_SET_KEYBOARD_LAYOUT (WM_USER+19) #endif +#define CM_UPDATE_CLIPBOARD (WM_USER+20) + // clang-format on diff --git a/src/interactivity/win32/clipboard.hpp b/src/interactivity/win32/clipboard.hpp index 09990ab514..54851fa55b 100644 --- a/src/interactivity/win32/clipboard.hpp +++ b/src/interactivity/win32/clipboard.hpp @@ -29,6 +29,7 @@ namespace Microsoft::Console::Interactivity::Win32 public: static Clipboard& Instance(); + void CopyText(const std::wstring& text); void Copy(_In_ const bool fAlsoCopyFormatting = false); void Paste(); void PasteDrop(HDROP drop); diff --git a/src/interactivity/win32/windowproc.cpp b/src/interactivity/win32/windowproc.cpp index c3f1e68c29..3517ddd839 100644 --- a/src/interactivity/win32/windowproc.cpp +++ b/src/interactivity/win32/windowproc.cpp @@ -773,6 +773,15 @@ static constexpr TsfDataProvider s_tsfDataProvider; } #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_UPDATE_REGION: case EVENT_CONSOLE_UPDATE_SIMPLE: