Skip to content

Commit 4abc041

Browse files
authored
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
1 parent 155d8a9 commit 4abc041

File tree

7 files changed

+60
-2
lines changed

7 files changed

+60
-2
lines changed

src/host/consoleInformation.cpp

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
#include "srvinit.h"
1414

1515
#include "../interactivity/inc/ServiceLocator.hpp"
16+
#include "../interactivity/win32/CustomWindowMessages.h"
1617
#include "../types/inc/convert.hpp"
1718

1819
using Microsoft::Console::Interactivity::ServiceLocator;
@@ -179,6 +180,32 @@ void CONSOLE_INFORMATION::SetBracketedPasteMode(const bool enabled) noexcept
179180
_bracketedPasteMode = enabled;
180181
}
181182

183+
void CONSOLE_INFORMATION::CopyTextToClipboard(const std::wstring_view text)
184+
{
185+
const auto window = ServiceLocator::LocateConsoleWindow();
186+
if (window)
187+
{
188+
// The clipboard can only be updated from the main GUI thread, so we
189+
// need to post a message to trigger the actual copy operation. But if
190+
// the pending clipboard content is already set, a message would have
191+
// already been posted, so there's no need to post another one.
192+
const auto clipboardMessageSent = _pendingClipboardText.has_value();
193+
_pendingClipboardText = text;
194+
if (!clipboardMessageSent)
195+
{
196+
PostMessageW(window->GetWindowHandle(), CM_UPDATE_CLIPBOARD, 0, 0);
197+
}
198+
}
199+
}
200+
201+
std::optional<std::wstring> CONSOLE_INFORMATION::UsePendingClipboardText()
202+
{
203+
// Once the pending text has been used, we clear the variable to let the
204+
// CopyTextToClipboard method know that the last CM_UPDATE_CLIPBOARD message
205+
// has been processed, and future updates will require another message.
206+
return std::exchange(_pendingClipboardText, {});
207+
}
208+
182209
// Method Description:
183210
// - Return the active screen buffer of the console.
184211
// Arguments:

src/host/outputStream.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -280,9 +280,9 @@ unsigned int ConhostInternalGetSet::GetInputCodePage() const
280280
// - content - the text to be copied.
281281
// Return Value:
282282
// - <none>
283-
void ConhostInternalGetSet::CopyToClipboard(const wil::zwstring_view /*content*/)
283+
void ConhostInternalGetSet::CopyToClipboard(const wil::zwstring_view content)
284284
{
285-
// TODO
285+
ServiceLocator::LocateGlobals().getConsoleInformation().CopyTextToClipboard(content);
286286
}
287287

288288
// Routine Description:

src/host/server.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,8 @@ class CONSOLE_INFORMATION :
126126

127127
bool GetBracketedPasteMode() const noexcept;
128128
void SetBracketedPasteMode(const bool enabled) noexcept;
129+
void CopyTextToClipboard(const std::wstring_view text);
130+
std::optional<std::wstring> UsePendingClipboardText();
129131

130132
void SetTitle(const std::wstring_view newTitle);
131133
void SetTitlePrefix(const std::wstring_view newTitlePrefix);
@@ -160,6 +162,7 @@ class CONSOLE_INFORMATION :
160162
SCREEN_INFORMATION* pCurrentScreenBuffer = nullptr;
161163
COOKED_READ_DATA* _cookedReadData = nullptr; // non-ownership pointer
162164
bool _bracketedPasteMode = false;
165+
std::optional<std::wstring> _pendingClipboardText;
163166

164167
Microsoft::Console::VirtualTerminal::VtIo _vtIo;
165168
Microsoft::Console::CursorBlinker _blinker;

src/interactivity/win32/Clipboard.cpp

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,22 @@ using namespace Microsoft::Console::Types;
2424

2525
#pragma region Public Methods
2626

27+
void Clipboard::CopyText(const std::wstring& text)
28+
{
29+
const auto clipboard = _openClipboard(ServiceLocator::LocateConsoleWindow()->GetWindowHandle());
30+
if (!clipboard)
31+
{
32+
LOG_LAST_ERROR();
33+
return;
34+
}
35+
36+
EmptyClipboard();
37+
// As per: https://learn.microsoft.com/en-us/windows/win32/dataxchg/standard-clipboard-formats
38+
// CF_UNICODETEXT: [...] A null character signals the end of the data.
39+
// --> We add +1 to the length. This works because .c_str() is null-terminated.
40+
_copyToClipboard(CF_UNICODETEXT, text.c_str(), (text.size() + 1) * sizeof(wchar_t));
41+
}
42+
2743
// Arguments:
2844
// - fAlsoCopyFormatting - Place colored HTML & RTF text onto the clipboard as well as the usual plain text.
2945
// Return Value:

src/interactivity/win32/CustomWindowMessages.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,6 @@
2929
#define CM_SET_KEYBOARD_LAYOUT (WM_USER+19)
3030
#endif
3131

32+
#define CM_UPDATE_CLIPBOARD (WM_USER+20)
33+
3234
// clang-format on

src/interactivity/win32/clipboard.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ namespace Microsoft::Console::Interactivity::Win32
2929
public:
3030
static Clipboard& Instance();
3131

32+
void CopyText(const std::wstring& text);
3233
void Copy(_In_ const bool fAlsoCopyFormatting = false);
3334
void Paste();
3435
void PasteDrop(HDROP drop);

src/interactivity/win32/windowproc.cpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -773,6 +773,15 @@ static constexpr TsfDataProvider s_tsfDataProvider;
773773
}
774774
#endif // DBG
775775

776+
case CM_UPDATE_CLIPBOARD:
777+
{
778+
if (const auto clipboardText = gci.UsePendingClipboardText())
779+
{
780+
Clipboard::Instance().CopyText(clipboardText.value());
781+
}
782+
break;
783+
}
784+
776785
case EVENT_CONSOLE_CARET:
777786
case EVENT_CONSOLE_UPDATE_REGION:
778787
case EVENT_CONSOLE_UPDATE_SIMPLE:

0 commit comments

Comments
 (0)