Files
terminal/src/interactivity/win32/Clipboard.cpp
Dustin L. Howett a9ea60799f When translating clipboard to numpad events, use the *input* CP (#19511)
I looked as far back as I was able to find, and we've used the OutputCP
since at least Windows NT 3.51.

I think it has _never_ been correct.

At issue today is the GB18030-2022 test string, which contains the
following problematic characters:

* `ˊ` `U+02CA` Modifier Letter Acute Accent
* `ˋ` `U+02CB` Modifier Letter Grave Accent
* `˙` `U+02D9` Dot Above
* `–` `U+2013` En Dash

They cannot be pasted into PowerShell 5.1 (PSReadline).

It turns out that when we try to synthesize an input event (Alt down,
numpad press, Alt up **with wchar**) we are using their output codepage
65001. These characters, of course, do not have a single byte encoding
in that codepage... and so we do not generate the numpad portion of the
synthesized event, only the alt down and up parts!

This is totally fine. **However**, there is also a .NET Framework bug
(which was only fixed after they released .NET Core, and rebranded, and
the community stepped in, ...) finally fixed in .NET 9 which used to
result in some Alt KeyUp events being dropped from the queue entirely.

https://github.com/dotnet/runtime/issues/102425

Using the input codepage ensures the right events get synthesized. It
works around the .NET bug.

Technically, padding in those numpad input events is _also more
correct_.

It also scares me, because it has been this way since NT 3.51 or
earlier.
2025-10-30 17:07:02 -05:00

448 lines
15 KiB
C++

// 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
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:
// <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);
if (gci.HasActiveOutputBuffer())
{
gci.GetActiveOutputBuffer().SnapOnInput(0);
}
}
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(&currentChar);
// 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().CP;
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