-
Notifications
You must be signed in to change notification settings - Fork 8.2k
/
Clipboard.cpp
426 lines (360 loc) · 14.3 KB
/
Clipboard.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "precomp.h"
#include "clipboard.hpp"
#include "resource.h"
#include "../../host/dbcs.h"
#include "../../host/scrolling.hpp"
#include "../../host/output.h"
#include "../../types/inc/convert.hpp"
#include "../../types/inc/viewport.hpp"
#include "../inc/conint.h"
#include "../inc/EventSynthesis.hpp"
#include "../inc/ServiceLocator.hpp"
#pragma hdrstop
using namespace Microsoft::Console::Interactivity::Win32;
using namespace Microsoft::Console::Types;
#pragma region Public Methods
// Arguments:
// - fAlsoCopyFormatting - Place colored HTML & RTF text onto the clipboard as well as the usual plain text.
// Return Value:
// <none>
// NOTE: if the registry is set to always copy color data then we will even if fAlsoCopyFormatting is false
void Clipboard::Copy(bool fAlsoCopyFormatting)
{
try
{
// registry settings may tell us to always copy the color/formatting
auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
fAlsoCopyFormatting = fAlsoCopyFormatting || gci.GetCopyColor();
// store selection in clipboard
StoreSelectionToClipboard(fAlsoCopyFormatting);
Selection::Instance().ClearSelection(); // clear selection in console
}
CATCH_LOG();
}
/*++
Perform paste request into old app by pulling out clipboard
contents and writing them to the console's input buffer
--*/
void Clipboard::Paste()
{
const auto clipboard = _openClipboard(ServiceLocator::LocateConsoleWindow()->GetWindowHandle());
if (!clipboard)
{
LOG_LAST_ERROR();
return;
}
// This handles most cases of pasting text as the OS converts most formats to CF_UNICODETEXT automatically.
if (const auto handle = GetClipboardData(CF_UNICODETEXT))
{
const wil::unique_hglobal_locked lock{ handle };
const auto str = static_cast<const wchar_t*>(lock.get());
if (!str)
{
return;
}
// 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.
// --> Use wcsnlen() to determine the actual length.
// NOTE: Some applications don't add a trailing null character. This includes past conhost versions.
const auto maxLen = GlobalSize(handle) / sizeof(wchar_t);
StringPaste(str, wcsnlen(str, maxLen));
return;
}
// We get CF_HDROP when a user copied a file with Ctrl+C in Explorer and pastes that into the terminal (among others).
if (const auto handle = GetClipboardData(CF_HDROP))
{
const wil::unique_hglobal_locked lock{ handle };
const auto drop = static_cast<HDROP>(lock.get());
if (!drop)
{
return;
}
PasteDrop(drop);
}
}
void Clipboard::PasteDrop(HDROP drop)
{
// NOTE: When asking DragQueryFileW for the required capacity it returns a length without trailing \0,
// but then expects a capacity that includes it. If you don't make space for a trailing \0
// then it will silently (!) cut off the end of the string. A somewhat disappointing API design.
const auto expectedLength = DragQueryFileW(drop, 0, nullptr, 0);
if (expectedLength == 0)
{
return;
}
// If the path contains spaces, we'll wrap it in quotes and so this allocates +2 characters ahead of time.
// We'll first make DragQueryFileW copy its contents in the middle and then check if that contains spaces.
// If it does, only then we'll add the quotes at the start and end.
// This is preferable over calling StringPaste 3x (an alternative, simpler approach),
// because the pasted content should be treated as a single atomic unit by the InputBuffer.
const auto buffer = std::make_unique_for_overwrite<wchar_t[]>(expectedLength + 2);
auto str = buffer.get() + 1;
size_t len = expectedLength;
const auto actualLength = DragQueryFileW(drop, 0, str, expectedLength + 1);
if (actualLength != expectedLength)
{
return;
}
if (wmemchr(str, L' ', len))
{
str = buffer.get();
len += 2;
til::at(str, 0) = L'"';
til::at(str, len - 1) = L'"';
}
StringPaste(str, len);
}
Clipboard& Clipboard::Instance()
{
static Clipboard clipboard;
return clipboard;
}
// Routine Description:
// - This routine pastes given Unicode string into the console window.
// Arguments:
// - pData - Unicode string that is pasted to the console window
// - cchData - Size of the Unicode String in characters
// Return Value:
// - None
void Clipboard::StringPaste(_In_reads_(cchData) const wchar_t* const pData,
const size_t cchData)
{
if (pData == nullptr)
{
return;
}
auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
try
{
// Clear any selection or scrolling that may be active.
Selection::Instance().ClearSelection();
Scrolling::s_ClearScroll();
const auto vtInputMode = gci.pInputBuffer->IsInVirtualTerminalInputMode();
const auto bracketedPasteMode = gci.GetBracketedPasteMode();
auto inEvents = TextToKeyEvents(pData, cchData, vtInputMode && bracketedPasteMode);
gci.pInputBuffer->Write(inEvents);
}
catch (...)
{
LOG_HR(wil::ResultFromCaughtException());
}
}
#pragma endregion
#pragma region Private Methods
wil::unique_close_clipboard_call Clipboard::_openClipboard(HWND hwnd)
{
bool success = false;
// OpenClipboard may fail to acquire the internal lock --> retry.
for (DWORD sleep = 10;; sleep *= 2)
{
if (OpenClipboard(hwnd))
{
success = true;
break;
}
// 10 iterations
if (sleep > 10000)
{
break;
}
Sleep(sleep);
}
return wil::unique_close_clipboard_call{ success };
}
void Clipboard::_copyToClipboard(const UINT format, const void* src, const size_t bytes)
{
wil::unique_hglobal handle{ THROW_LAST_ERROR_IF_NULL(GlobalAlloc(GMEM_MOVEABLE, bytes)) };
const auto locked = GlobalLock(handle.get());
memcpy(locked, src, bytes);
GlobalUnlock(handle.get());
THROW_LAST_ERROR_IF_NULL(SetClipboardData(format, handle.get()));
handle.release();
}
void Clipboard::_copyToClipboardRegisteredFormat(const wchar_t* format, const void* src, size_t bytes)
{
const auto id = RegisterClipboardFormatW(format);
if (!id)
{
LOG_LAST_ERROR();
return;
}
_copyToClipboard(id, src, bytes);
}
// Routine Description:
// - converts a wchar_t* into a series of KeyEvents as if it was typed
// from the keyboard
// Arguments:
// - pData - the text to convert
// - cchData - the size of pData, in wchars
// - bracketedPaste - should this be bracketed with paste control sequences
// Return Value:
// - deque of KeyEvents that represent the string passed in
// Note:
// - will throw exception on error
InputEventQueue Clipboard::TextToKeyEvents(_In_reads_(cchData) const wchar_t* const pData,
const size_t cchData,
const bool bracketedPaste)
{
THROW_HR_IF_NULL(E_INVALIDARG, pData);
InputEventQueue keyEvents;
const auto pushControlSequence = [&](const std::wstring_view sequence) {
std::for_each(sequence.begin(), sequence.end(), [&](const auto wch) {
keyEvents.push_back(SynthesizeKeyEvent(true, 1, 0, 0, wch, 0));
keyEvents.push_back(SynthesizeKeyEvent(false, 1, 0, 0, wch, 0));
});
};
// When a bracketed paste is requested, we need to wrap the text with
// control sequences which indicate that the content has been pasted.
if (bracketedPaste)
{
pushControlSequence(L"\x1b[200~");
}
for (size_t i = 0; i < cchData; ++i)
{
auto currentChar = pData[i];
const auto charAllowed = FilterCharacterOnPaste(¤tChar);
// filter out linefeed if it's not the first char and preceded
// by a carriage return
const auto skipLinefeed = (i != 0 &&
currentChar == UNICODE_LINEFEED &&
pData[i - 1] == UNICODE_CARRIAGERETURN);
// filter out escape if bracketed paste mode is enabled
const auto skipEscape = (bracketedPaste && currentChar == UNICODE_ESC);
if (!charAllowed || skipLinefeed || skipEscape)
{
continue;
}
if (currentChar == 0)
{
break;
}
// MSFT:12123975 / WSL GH#2006
// If you paste text with ONLY linefeed line endings (unix style) in wsl,
// then we faithfully pass those along, which the underlying terminal
// interprets as C-j. In nano, C-j is mapped to "Justify text", which
// causes the pasted text to get broken at the width of the terminal.
// This behavior doesn't occur in gnome-terminal, and nothing like it occurs
// in vi or emacs.
// This change doesn't break pasting text into any of those applications
// with CR/LF (Windows) line endings either. That apparently always
// worked right.
if (IsInVirtualTerminalInputMode() && currentChar == UNICODE_LINEFEED)
{
currentChar = UNICODE_CARRIAGERETURN;
}
const auto codepage = ServiceLocator::LocateGlobals().getConsoleInformation().OutputCP;
CharToKeyEvents(currentChar, codepage, keyEvents);
}
if (bracketedPaste)
{
pushControlSequence(L"\x1b[201~");
}
return keyEvents;
}
// Routine Description:
// - Copies the selected area onto the global system clipboard.
// - NOTE: Throws on allocation and other clipboard failures.
// Arguments:
// - copyFormatting - This will also place colored HTML & RTF text onto the clipboard as well as the usual plain text.
// Return Value:
// <none>
void Clipboard::StoreSelectionToClipboard(const bool copyFormatting)
{
std::wstring text;
std::string htmlData, rtfData;
const auto& selection = Selection::Instance();
// See if there is a selection to get
if (!selection.IsAreaSelected())
{
return;
}
const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
const auto& buffer = gci.GetActiveOutputBuffer().GetTextBuffer();
const auto& renderSettings = gci.GetRenderSettings();
const auto GetAttributeColors = [&](const auto& attr) {
const auto [fg, bg] = renderSettings.GetAttributeColors(attr);
const auto ul = renderSettings.GetAttributeUnderlineColor(attr);
return std::tuple{ fg, bg, ul };
};
bool singleLine = false;
if (WI_IsFlagSet(OneCoreSafeGetKeyState(VK_SHIFT), KEY_PRESSED))
{
// When shift is held, put everything in one line
singleLine = true;
}
const auto& [selectionStart, selectionEnd] = selection.GetSelectionAnchors();
const auto req = TextBuffer::CopyRequest::FromConfig(buffer, selectionStart, selectionEnd, singleLine, !selection.IsLineSelection(), false);
text = buffer.GetPlainText(req);
if (copyFormatting)
{
const auto& fontData = gci.GetActiveOutputBuffer().GetCurrentFont();
const auto& fontName = fontData.GetFaceName();
const auto fontSizePt = fontData.GetUnscaledSize().height * 72 / ServiceLocator::LocateGlobals().dpi;
const auto bgColor = renderSettings.GetAttributeColors({}).second;
const auto isIntenseBold = renderSettings.GetRenderMode(::Microsoft::Console::Render::RenderSettings::Mode::IntenseIsBold);
htmlData = buffer.GenHTML(req, fontSizePt, fontName, bgColor, isIntenseBold, GetAttributeColors);
rtfData = buffer.GenRTF(req, fontSizePt, fontName, bgColor, isIntenseBold, GetAttributeColors);
}
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));
if (copyFormatting)
{
_copyToClipboardRegisteredFormat(L"HTML Format", htmlData.data(), htmlData.size());
_copyToClipboardRegisteredFormat(L"Rich Text Format", rtfData.data(), rtfData.size());
}
}
// Returns true if the character should be emitted to the paste stream
// -- in some cases, we will change what character should be emitted, as in the case of "smart quotes"
// Returns false if the character should not be emitted (e.g. <TAB>)
bool Clipboard::FilterCharacterOnPaste(_Inout_ WCHAR* const pwch)
{
const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
auto fAllowChar = true;
if (gci.GetFilterOnPaste() &&
(WI_IsFlagSet(gci.pInputBuffer->InputMode, ENABLE_PROCESSED_INPUT)))
{
switch (*pwch)
{
// swallow tabs to prevent inadvertent tab expansion
case UNICODE_TAB:
{
fAllowChar = false;
break;
}
// Replace Unicode space with standard space
case UNICODE_NBSP:
case UNICODE_NARROW_NBSP:
{
*pwch = UNICODE_SPACE;
break;
}
// Replace "smart quotes" with "dumb ones"
case UNICODE_LEFT_SMARTQUOTE:
case UNICODE_RIGHT_SMARTQUOTE:
{
*pwch = UNICODE_QUOTE;
break;
}
// Replace Unicode dashes with a standard hyphen
case UNICODE_EM_DASH:
case UNICODE_EN_DASH:
{
*pwch = UNICODE_HYPHEN;
break;
}
}
}
return fAllowChar;
}
#pragma endregion