mirror of
https://github.com/microsoft/terminal.git
synced 2025-12-19 18:11:39 -05:00
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.
448 lines
15 KiB
C++
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(¤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().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
|