// 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: // // 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(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(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(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: // 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. ) 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