From 7442a7b28205bb393c74f5abd8e7e807a584c48b Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Tue, 24 Jun 2025 16:34:24 +0200 Subject: [PATCH 1/5] Don't trim bracketed pastes set via OSC52 --- .../TerminalApp/AppActionHandlers.cpp | 4 +- src/cascadia/TerminalApp/TerminalPage.cpp | 243 ++++++++++++------ src/cascadia/TerminalApp/TerminalPage.h | 5 +- src/cascadia/TerminalControl/ControlCore.cpp | 107 +------- src/cascadia/TerminalControl/ControlCore.h | 4 +- src/cascadia/TerminalControl/ControlCore.idl | 1 + .../TerminalControl/ControlInteractivity.cpp | 6 +- .../TerminalControl/ControlInteractivity.h | 2 +- .../TerminalControl/ControlInteractivity.idl | 2 +- src/cascadia/TerminalControl/EventArgs.cpp | 1 + src/cascadia/TerminalControl/EventArgs.h | 27 ++ src/cascadia/TerminalControl/EventArgs.idl | 8 + src/cascadia/TerminalControl/TermControl.cpp | 5 +- src/cascadia/TerminalControl/TermControl.h | 7 +- src/cascadia/TerminalControl/TermControl.idl | 3 +- 15 files changed, 234 insertions(+), 191 deletions(-) diff --git a/src/cascadia/TerminalApp/AppActionHandlers.cpp b/src/cascadia/TerminalApp/AppActionHandlers.cpp index 69648929637..777773a9f30 100644 --- a/src/cascadia/TerminalApp/AppActionHandlers.cpp +++ b/src/cascadia/TerminalApp/AppActionHandlers.cpp @@ -548,7 +548,9 @@ namespace winrt::TerminalApp::implementation { if (const auto& realArgs = args.ActionArgs().try_as()) { - const auto handled = _CopyText(realArgs.DismissSelection(), realArgs.SingleLine(), realArgs.WithControlSequences(), realArgs.CopyFormatting()); + const auto copyFormatting = realArgs.CopyFormatting(); + const auto format = copyFormatting ? copyFormatting.Value() : _settings.GlobalSettings().CopyFormatting(); + const auto handled = _CopyText(realArgs.DismissSelection(), realArgs.SingleLine(), realArgs.WithControlSequences(), format); args.Handled(handled); } } diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index 702b4a36ee3..4ec552bc36e 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -57,6 +57,121 @@ namespace winrt using VirtualKeyModifiers = Windows::System::VirtualKeyModifiers; } +namespace clipboard +{ + wil::unique_close_clipboard_call open(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 write(wil::zwstring_view text, std::string_view html, std::string_view rtf) + { + static const auto regular = [](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(); + }; + static const auto registered = [](const wchar_t* format, const void* src, size_t bytes) { + const auto id = RegisterClipboardFormatW(format); + if (!id) + { + LOG_LAST_ERROR(); + return; + } + regular(id, src, bytes); + }; + + EmptyClipboard(); + + if (!text.empty()) + { + // 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. + regular(CF_UNICODETEXT, text.c_str(), (text.size() + 1) * sizeof(wchar_t)); + } + + if (!html.empty()) + { + registered(L"HTML Format", html.data(), html.size()); + } + + if (!rtf.empty()) + { + registered(L"Rich Text Format", rtf.data(), rtf.size()); + } + } + + winrt::hstring read() + { + // 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 {}; + } + + const auto maxLen = GlobalSize(handle) / sizeof(wchar_t); + const auto len = wcsnlen(str, maxLen); + return winrt::hstring{ str, gsl::narrow_cast(len) }; + } + + // 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 {}; + } + + const auto cap = DragQueryFileW(drop, 0, nullptr, 0); + if (cap == 0) + { + return {}; + } + + auto buffer = winrt::impl::hstring_builder{ cap }; + const auto len = DragQueryFileW(drop, 0, buffer.data(), cap + 1); + if (len == 0) + { + return {}; + } + + return buffer.to_hstring(); + } + + return {}; + } +} // namespace clipboard + namespace winrt::TerminalApp::implementation { TerminalPage::TerminalPage(TerminalApp::WindowProperties properties, const TerminalApp::ContentManager& manager) : @@ -1708,10 +1823,9 @@ namespace winrt::TerminalApp::implementation }); term.ShowWindowChanged({ get_weak(), &TerminalPage::_ShowWindowChangedHandler }); - term.SearchMissingCommand({ get_weak(), &TerminalPage::_SearchMissingCommandHandler }); - term.WindowSizeChanged({ get_weak(), &TerminalPage::_WindowSizeChanged }); + term.WriteToClipboard({ get_weak(), &TerminalPage::_copyToClipboard }); // Don't even register for the event if the feature is compiled off. if constexpr (Feature_ShellCompletions::IsEnabled()) @@ -2643,75 +2757,6 @@ namespace winrt::TerminalApp::implementation return dimension; } - static wil::unique_close_clipboard_call _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 }; - } - - static winrt::hstring _extractClipboard() - { - // 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 {}; - } - - const auto maxLen = GlobalSize(handle) / sizeof(wchar_t); - const auto len = wcsnlen(str, maxLen); - return winrt::hstring{ str, gsl::narrow_cast(len) }; - } - - // 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 {}; - } - - const auto cap = DragQueryFileW(drop, 0, nullptr, 0); - if (cap == 0) - { - return {}; - } - - auto buffer = winrt::impl::hstring_builder{ cap }; - const auto len = DragQueryFileW(drop, 0, buffer.data(), cap + 1); - if (len == 0) - { - return {}; - } - - return buffer.to_hstring(); - } - - return {}; - } - // Function Description: // - This function is called when the `TermControl` requests that we send // it the clipboard's content. @@ -2731,24 +2776,40 @@ namespace winrt::TerminalApp::implementation const auto weakThis = get_weak(); const auto dispatcher = Dispatcher(); const auto globalSettings = _settings.GlobalSettings(); + const auto bracketedPaste = eventArgs.BracketedPasteEnabled(); // GetClipboardData might block for up to 30s for delay-rendered contents. co_await winrt::resume_background(); winrt::hstring text; - if (const auto clipboard = _openClipboard(nullptr)) + uint32_t clipboardSequenceNumber = 0; + + if (const auto clipboard = clipboard::open(nullptr)) { - text = _extractClipboard(); + text = clipboard::read(); + clipboardSequenceNumber = GetClipboardSequenceNumber(); + } + else + { + co_return; } - if (globalSettings.TrimPaste()) + if (globalSettings.TrimPaste() && + // If we're using bracketed paste mode, and the clipboard contents originate from us + // (= clipboard sequence number matches the last written one), don't trim the paste. + // Put inversely: Trim the contents if the sequence number changed OR if we're not using bracketed pastes. + (_copyToClipboardSequenceNumber.load(std::memory_order_relaxed) != clipboardSequenceNumber || !bracketedPaste)) { - text = { Utils::TrimPaste(text) }; - if (text.empty()) - { - // Text is all white space, nothing to paste - co_return; - } + // NOTE: This has to be done in 2 steps, because technically speaking we + // may be the only holder of the hstring instance and assigning to it + // will first release the current memory and then assign the new value. + winrt::hstring trimmed{ Utils::TrimPaste(text) }; + text = std::move(trimmed); + } + + if (text.empty()) + { + co_return; } // If the requesting terminal is in bracketed paste mode, then we don't need to warn about a multi-line paste. @@ -2951,7 +3012,7 @@ namespace winrt::TerminalApp::implementation // - formats: dictate which formats need to be copied // Return Value: // - true iff we we able to copy text (if a selection was active) - bool TerminalPage::_CopyText(const bool dismissSelection, const bool singleLine, const bool withControlSequences, const Windows::Foundation::IReference& formats) + bool TerminalPage::_CopyText(const bool dismissSelection, const bool singleLine, const bool withControlSequences, const CopyFormat formats) { if (const auto& control{ _GetActiveControl() }) { @@ -3094,6 +3155,26 @@ namespace winrt::TerminalApp::implementation } } + void TerminalPage::_copyToClipboard(const IInspectable, const WriteToClipboardEventArgs args) + { + const auto plain = args.Plain(); + const auto html = args.Html(); + const auto rtf = args.Rtf(); + + if (auto clipboard = clipboard::open(_hostingHwnd.value_or(nullptr))) + { + clipboard::write( + { plain.data(), plain.size() }, + { reinterpret_cast(html.data()), html.size() }, + { reinterpret_cast(rtf.data()), rtf.size() }); + + // `CloseClipboard` can still bump the sequence number, + // so we need to do this after dropping `clipboard`. + clipboard.reset(); + _copyToClipboardSequenceNumber.store(GetClipboardSequenceNumber(), std::memory_order_relaxed); + } + } + // Method Description: // - Paste text from the Windows Clipboard to the focused terminal void TerminalPage::_PasteText() diff --git a/src/cascadia/TerminalApp/TerminalPage.h b/src/cascadia/TerminalApp/TerminalPage.h index b369bd920eb..d2298d69b75 100644 --- a/src/cascadia/TerminalApp/TerminalPage.h +++ b/src/cascadia/TerminalApp/TerminalPage.h @@ -225,7 +225,7 @@ namespace winrt::TerminalApp::implementation void _UpdateTabIndices(); TerminalApp::TerminalTab _settingsTab{ nullptr }; - + std::atomic _copyToClipboardSequenceNumber{ 0 }; bool _isInFocusMode{ false }; bool _isFullscreen{ false }; bool _isMaximized{ false }; @@ -415,10 +415,11 @@ namespace winrt::TerminalApp::implementation bool _IsUriSupported(const winrt::Windows::Foundation::Uri& parsedUri); void _ShowCouldNotOpenDialog(winrt::hstring reason, winrt::hstring uri); - bool _CopyText(const bool dismissSelection, const bool singleLine, const bool withControlSequences, const Windows::Foundation::IReference& formats); + bool _CopyText(bool dismissSelection, bool singleLine, bool withControlSequences, Microsoft::Terminal::Control::CopyFormat formats); safe_void_coroutine _SetTaskbarProgressHandler(const IInspectable sender, const IInspectable eventArgs); + void _copyToClipboard(IInspectable, Microsoft::Terminal::Control::WriteToClipboardEventArgs args); void _PasteText(); safe_void_coroutine _ControlNoticeRaisedHandler(const IInspectable sender, const Microsoft::Terminal::Control::NoticeEventArgs eventArgs); diff --git a/src/cascadia/TerminalControl/ControlCore.cpp b/src/cascadia/TerminalControl/ControlCore.cpp index 61d699a6cc9..5337bf1a82e 100644 --- a/src/cascadia/TerminalControl/ControlCore.cpp +++ b/src/cascadia/TerminalControl/ControlCore.cpp @@ -104,8 +104,9 @@ namespace winrt::Microsoft::Terminal::Control::implementation // GH#8969: pre-seed working directory to prevent potential races _terminal->SetWorkingDirectory(_settings->StartingDirectory()); - auto pfnCopyToClipboard = [this](auto&& PH1) { _terminalCopyToClipboard(std::forward(PH1)); }; - _terminal->SetCopyToClipboardCallback(pfnCopyToClipboard); + _terminal->SetCopyToClipboardCallback([this](wil::zwstring_view wstr) { + WriteToClipboard.raise(*this, winrt::make(winrt::hstring{ std::wstring_view{ wstr } }, std::string{}, std::string{})); + }); auto pfnWarningBell = [this] { _terminalWarningBell(); }; _terminal->SetWarningBellCallback(pfnWarningBell); @@ -588,7 +589,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation else if (vkey == VK_RETURN && !mods.IsCtrlPressed() && !mods.IsAltPressed()) { // [Shift +] Enter --> copy text - CopySelectionToClipboard(mods.IsShiftPressed(), false, nullptr); + CopySelectionToClipboard(mods.IsShiftPressed(), false, _settings->CopyFormatting()); _terminal->ClearSelection(); _updateSelectionUI(); return true; @@ -1252,89 +1253,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation _updateSelectionUI(); } - static wil::unique_close_clipboard_call _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 }; - } - - static void _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(); - } - - static void _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); - } - - static void copyToClipboard(wil::zwstring_view text, std::string_view html, std::string_view rtf) - { - const auto clipboard = _openClipboard(nullptr); - if (!clipboard) - { - LOG_LAST_ERROR(); - return; - } - - EmptyClipboard(); - - if (!text.empty()) - { - // 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 (!html.empty()) - { - _copyToClipboardRegisteredFormat(L"HTML Format", html.data(), html.size()); - } - - if (!rtf.empty()) - { - _copyToClipboardRegisteredFormat(L"Rich Text Format", rtf.data(), rtf.size()); - } - } - - // Called when the Terminal wants to set something to the clipboard, i.e. - // when an OSC 52 is emitted. - void ControlCore::_terminalCopyToClipboard(wil::zwstring_view wstr) - { - copyToClipboard(wstr, {}, {}); - } - // Method Description: // - Given a copy-able selection, get the selected text from the buffer and send it to the // Windows Clipboard (CascadiaWin32:main.cpp). @@ -1345,7 +1263,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation // if we should defer which formats are copied to the global setting bool ControlCore::CopySelectionToClipboard(bool singleLine, bool withControlSequences, - const Windows::Foundation::IReference& formats) + const CopyFormat formats) { ::Microsoft::Terminal::Core::Terminal::TextCopyData payload; { @@ -1357,19 +1275,20 @@ namespace winrt::Microsoft::Terminal::Control::implementation return false; } - // use action's copyFormatting if it's present, else fallback to globally - // set copyFormatting. - const auto copyFormats = formats != nullptr ? formats.Value() : _settings->CopyFormatting(); - - const auto copyHtml = WI_IsFlagSet(copyFormats, CopyFormat::HTML); - const auto copyRtf = WI_IsFlagSet(copyFormats, CopyFormat::RTF); + const auto copyHtml = WI_IsFlagSet(formats, CopyFormat::HTML); + const auto copyRtf = WI_IsFlagSet(formats, CopyFormat::RTF); // extract text from buffer // RetrieveSelectedTextFromBuffer will lock while it's reading payload = _terminal->RetrieveSelectedTextFromBuffer(singleLine, withControlSequences, copyHtml, copyRtf); } - copyToClipboard(payload.plainText, payload.html, payload.rtf); + WriteToClipboard.raise( + *this, + winrt::make( + winrt::hstring{ payload.plainText }, + std::move(payload.html), + std::move(payload.rtf))); return true; } diff --git a/src/cascadia/TerminalControl/ControlCore.h b/src/cascadia/TerminalControl/ControlCore.h index 2453670d3f8..2f021104e41 100644 --- a/src/cascadia/TerminalControl/ControlCore.h +++ b/src/cascadia/TerminalControl/ControlCore.h @@ -124,7 +124,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation void SendInput(std::wstring_view wstr); void PasteText(const winrt::hstring& hstr); - bool CopySelectionToClipboard(bool singleLine, bool withControlSequences, const Windows::Foundation::IReference& formats); + bool CopySelectionToClipboard(bool singleLine, bool withControlSequences, const CopyFormat formats); void SelectAll(); void ClearSelection(); bool ToggleBlockSelection(); @@ -297,6 +297,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation til::typed_event SearchMissingCommand; til::typed_event<> RefreshQuickFixUI; til::typed_event WindowSizeChanged; + til::typed_event WriteToClipboard; til::typed_event<> CloseTerminalRequested; til::typed_event<> RestartTerminalRequested; @@ -325,7 +326,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation void _sendInputToConnection(std::wstring_view wstr); #pragma region TerminalCoreCallbacks - void _terminalCopyToClipboard(wil::zwstring_view wstr); void _terminalWarningBell(); void _terminalTitleChanged(std::wstring_view wstr); void _terminalScrollPositionChanged(const int viewTop, diff --git a/src/cascadia/TerminalControl/ControlCore.idl b/src/cascadia/TerminalControl/ControlCore.idl index bc704a13426..4e488d9b6f3 100644 --- a/src/cascadia/TerminalControl/ControlCore.idl +++ b/src/cascadia/TerminalControl/ControlCore.idl @@ -194,6 +194,7 @@ namespace Microsoft.Terminal.Control event Windows.Foundation.TypedEventHandler SearchMissingCommand; event Windows.Foundation.TypedEventHandler RefreshQuickFixUI; event Windows.Foundation.TypedEventHandler WindowSizeChanged; + event Windows.Foundation.TypedEventHandler WriteToClipboard; // These events are always called from the UI thread (bugs aside) event Windows.Foundation.TypedEventHandler FontSizeChanged; diff --git a/src/cascadia/TerminalControl/ControlInteractivity.cpp b/src/cascadia/TerminalControl/ControlInteractivity.cpp index f2899121454..930b4cac2ab 100644 --- a/src/cascadia/TerminalControl/ControlInteractivity.cpp +++ b/src/cascadia/TerminalControl/ControlInteractivity.cpp @@ -199,7 +199,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation // if we should defer which formats are copied to the global setting bool ControlInteractivity::CopySelectionToClipboard(bool singleLine, bool withControlSequences, - const Windows::Foundation::IReference& formats) + const CopyFormat formats) { if (_core) { @@ -316,7 +316,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation else { // Try to copy the text and clear the selection - const auto successfulCopy = CopySelectionToClipboard(shiftEnabled, false, nullptr); + const auto successfulCopy = CopySelectionToClipboard(shiftEnabled, false, _core->Settings().CopyFormatting()); _core->ClearSelection(); if (_core->CopyOnSelect() || !successfulCopy) { @@ -461,7 +461,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation // IMPORTANT! // DO NOT clear the selection here! // Otherwise, the selection will be cleared immediately after you make it. - CopySelectionToClipboard(false, false, nullptr); + CopySelectionToClipboard(false, false, _core->Settings().CopyFormatting()); } _singleClickTouchdownPos = std::nullopt; diff --git a/src/cascadia/TerminalControl/ControlInteractivity.h b/src/cascadia/TerminalControl/ControlInteractivity.h index dcf2c811d02..27aea3dee4f 100644 --- a/src/cascadia/TerminalControl/ControlInteractivity.h +++ b/src/cascadia/TerminalControl/ControlInteractivity.h @@ -84,7 +84,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation bool CopySelectionToClipboard(bool singleLine, bool withControlSequences, - const Windows::Foundation::IReference& formats); + const CopyFormat formats); void RequestPasteTextFromClipboard(); void SetEndSelectionPoint(const Core::Point pixelPosition); diff --git a/src/cascadia/TerminalControl/ControlInteractivity.idl b/src/cascadia/TerminalControl/ControlInteractivity.idl index a63726f2d38..bfd94703848 100644 --- a/src/cascadia/TerminalControl/ControlInteractivity.idl +++ b/src/cascadia/TerminalControl/ControlInteractivity.idl @@ -32,7 +32,7 @@ namespace Microsoft.Terminal.Control InteractivityAutomationPeer OnCreateAutomationPeer(); - Boolean CopySelectionToClipboard(Boolean singleLine, Boolean withControlSequences, Windows.Foundation.IReference formats); + Boolean CopySelectionToClipboard(Boolean singleLine, Boolean withControlSequences, CopyFormat formats); void RequestPasteTextFromClipboard(); void SetEndSelectionPoint(Microsoft.Terminal.Core.Point point); diff --git a/src/cascadia/TerminalControl/EventArgs.cpp b/src/cascadia/TerminalControl/EventArgs.cpp index b7e90a7511c..ba5eeb4963e 100644 --- a/src/cascadia/TerminalControl/EventArgs.cpp +++ b/src/cascadia/TerminalControl/EventArgs.cpp @@ -20,3 +20,4 @@ #include "StringSentEventArgs.g.cpp" #include "SearchMissingCommandEventArgs.g.cpp" #include "WindowSizeChangedEventArgs.g.cpp" +#include "WriteToClipboardEventArgs.g.cpp" diff --git a/src/cascadia/TerminalControl/EventArgs.h b/src/cascadia/TerminalControl/EventArgs.h index 526aa2b033f..d2a95fbcc2c 100644 --- a/src/cascadia/TerminalControl/EventArgs.h +++ b/src/cascadia/TerminalControl/EventArgs.h @@ -20,6 +20,7 @@ #include "StringSentEventArgs.g.h" #include "SearchMissingCommandEventArgs.g.h" #include "WindowSizeChangedEventArgs.g.h" +#include "WriteToClipboardEventArgs.g.h" namespace winrt::Microsoft::Terminal::Control::implementation { @@ -238,6 +239,32 @@ namespace winrt::Microsoft::Terminal::Control::implementation WINRT_PROPERTY(int32_t, Width); WINRT_PROPERTY(int32_t, Height); }; + + struct WriteToClipboardEventArgs : WriteToClipboardEventArgsT + { + WriteToClipboardEventArgs(winrt::hstring&& plain, std::string&& html, std::string&& rtf) : + _plain(std::move(plain)), + _html(std::move(html)), + _rtf(std::move(rtf)) + { + } + + winrt::hstring Plain() const noexcept { return _plain; } + winrt::com_array Html() noexcept { return _cast(_html); } + winrt::com_array Rtf() noexcept { return _cast(_rtf); } + + private: + static winrt::com_array _cast(const std::string& str) + { + const auto beg = reinterpret_cast(str.data()); + const auto len = str.size(); + return { beg, beg + len }; + } + + winrt::hstring _plain; + std::string _html; + std::string _rtf; + }; } namespace winrt::Microsoft::Terminal::Control::factory_implementation diff --git a/src/cascadia/TerminalControl/EventArgs.idl b/src/cascadia/TerminalControl/EventArgs.idl index 0cdaee97659..78c4aa67274 100644 --- a/src/cascadia/TerminalControl/EventArgs.idl +++ b/src/cascadia/TerminalControl/EventArgs.idl @@ -6,6 +6,7 @@ namespace Microsoft.Terminal.Control [flags] enum CopyFormat { + None = 0x0, HTML = 0x1, RTF = 0x2, All = 0xffffffff @@ -144,4 +145,11 @@ namespace Microsoft.Terminal.Control Int32 Width; Int32 Height; } + + runtimeclass WriteToClipboardEventArgs + { + String Plain { get; }; // UTF-16, as required by CF_UNICODETEXT + byte[] Html { get; }; // UTF-8, as required by "HTML Format" + byte[] Rtf { get; }; // UTF-8, as required by "Rich Text Format" + } } diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index 67809d4cea9..2a1d362b5a1 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -330,6 +330,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation _revokers.RestartTerminalRequested = _core.RestartTerminalRequested(winrt::auto_revoke, { get_weak(), &TermControl::_bubbleRestartTerminalRequested }); _revokers.SearchMissingCommand = _core.SearchMissingCommand(winrt::auto_revoke, { get_weak(), &TermControl::_bubbleSearchMissingCommand }); _revokers.WindowSizeChanged = _core.WindowSizeChanged(winrt::auto_revoke, { get_weak(), &TermControl::_bubbleWindowSizeChanged }); + _revokers.WriteToClipboard = _core.WriteToClipboard(winrt::auto_revoke, { get_weak(), &TermControl::_bubbleWriteToClipboard }); _revokers.PasteFromClipboard = _interactivity.PasteFromClipboard(winrt::auto_revoke, { get_weak(), &TermControl::_bubblePasteFromClipboard }); @@ -2628,7 +2629,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation // - withControlSequences: if enabled, the copied plain text contains color/style ANSI escape codes from the selection // - formats: which formats to copy (defined by action's CopyFormatting arg). nullptr // if we should defer which formats are copied to the global setting - bool TermControl::CopySelectionToClipboard(bool dismissSelection, bool singleLine, bool withControlSequences, const Windows::Foundation::IReference& formats) + bool TermControl::CopySelectionToClipboard(bool dismissSelection, bool singleLine, bool withControlSequences, const CopyFormat formats) { if (_IsClosing()) { @@ -4147,7 +4148,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation const IInspectable& /*args*/) { // formats = nullptr -> copy all formats - _interactivity.CopySelectionToClipboard(false, false, nullptr); + _interactivity.CopySelectionToClipboard(false, false, _core.Settings().CopyFormatting()); ContextMenu().Hide(); SelectionContextMenu().Hide(); } diff --git a/src/cascadia/TerminalControl/TermControl.h b/src/cascadia/TerminalControl/TermControl.h index 7fe3db1be09..2749d482afc 100644 --- a/src/cascadia/TerminalControl/TermControl.h +++ b/src/cascadia/TerminalControl/TermControl.h @@ -60,7 +60,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation hstring GetProfileName() const; - bool CopySelectionToClipboard(bool dismissSelection, bool singleLine, bool withControlSequences, const Windows::Foundation::IReference& formats); + bool CopySelectionToClipboard(bool dismissSelection, bool singleLine, bool withControlSequences, const CopyFormat formats); void PasteTextFromClipboard(); void SelectAll(); bool ToggleBlockSelection(); @@ -227,8 +227,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation BUBBLED_FORWARDED_TYPED_EVENT(CloseTerminalRequested, IInspectable, IInspectable); BUBBLED_FORWARDED_TYPED_EVENT(CompletionsChanged, IInspectable, Control::CompletionsChangedEventArgs); BUBBLED_FORWARDED_TYPED_EVENT(RestartTerminalRequested, IInspectable, IInspectable); - - BUBBLED_FORWARDED_TYPED_EVENT(PasteFromClipboard, IInspectable, Control::PasteFromClipboardEventArgs); + BUBBLED_FORWARDED_TYPED_EVENT(WriteToClipboard, IInspectable, Control::WriteToClipboardEventArgs); + BUBBLED_FORWARDED_TYPED_EVENT(PasteFromClipboard, IInspectable, Control::PasteFromClipboardEventArgs); // clang-format on @@ -473,6 +473,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation Control::ControlCore::SearchMissingCommand_revoker SearchMissingCommand; Control::ControlCore::RefreshQuickFixUI_revoker RefreshQuickFixUI; Control::ControlCore::WindowSizeChanged_revoker WindowSizeChanged; + Control::ControlCore::WriteToClipboard_revoker WriteToClipboard; // These are set up in _InitializeTerminal Control::ControlCore::RendererWarning_revoker RendererWarning; diff --git a/src/cascadia/TerminalControl/TermControl.idl b/src/cascadia/TerminalControl/TermControl.idl index 9d72977455d..4bef92bcc20 100644 --- a/src/cascadia/TerminalControl/TermControl.idl +++ b/src/cascadia/TerminalControl/TermControl.idl @@ -64,6 +64,7 @@ namespace Microsoft.Terminal.Control event Windows.Foundation.TypedEventHandler ReadOnlyChanged; event Windows.Foundation.TypedEventHandler FocusFollowMouseRequested; event Windows.Foundation.TypedEventHandler WindowSizeChanged; + event Windows.Foundation.TypedEventHandler WriteToClipboard; event Windows.Foundation.TypedEventHandler CompletionsChanged; @@ -87,7 +88,7 @@ namespace Microsoft.Terminal.Control event Windows.Foundation.TypedEventHandler CloseTerminalRequested; event Windows.Foundation.TypedEventHandler RestartTerminalRequested; - Boolean CopySelectionToClipboard(Boolean dismissSelection, Boolean singleLine, Boolean withControlSequences, Windows.Foundation.IReference formats); + Boolean CopySelectionToClipboard(Boolean dismissSelection, Boolean singleLine, Boolean withControlSequences, CopyFormat formats); void PasteTextFromClipboard(); void SelectAll(); Boolean ToggleBlockSelection(); From e4bb5445f6f159e16ab615a176d2d8a9db25e502 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Tue, 29 Jul 2025 01:18:11 +0200 Subject: [PATCH 2/5] Reimplement with Dustin's suggestion --- src/cascadia/TerminalApp/TerminalPage.cpp | 67 +++++++++---------- src/cascadia/TerminalApp/TerminalPage.h | 3 +- src/cascadia/TerminalControl/ControlCore.h | 2 +- src/cascadia/TerminalControl/ControlCore.idl | 2 +- src/cascadia/TerminalControl/EventArgs.cpp | 2 +- src/cascadia/TerminalControl/EventArgs.h | 54 +++++++-------- src/cascadia/TerminalControl/EventArgs.idl | 21 ++++-- src/cascadia/TerminalControl/TermControl.h | 2 +- src/cascadia/TerminalControl/TermControl.idl | 2 +- .../TerminalSettingsEditor/Interaction.cpp | 4 ++ .../TerminalSettingsEditor/Interaction.h | 1 + .../TerminalSettingsEditor/Interaction.idl | 3 + .../TerminalSettingsEditor/Interaction.xaml | 7 +- .../InteractionViewModel.idl | 2 +- .../Resources/en-US/Resources.resw | 20 +++++- .../TerminalSettingsModel/EnumMappings.cpp | 1 + .../TerminalSettingsModel/EnumMappings.h | 1 + .../TerminalSettingsModel/EnumMappings.idl | 1 + .../GlobalAppSettings.idl | 2 +- .../TerminalSettingsModel/MTSMSettings.h | 2 +- .../TerminalSettingsSerializationHelpers.h | 26 +++++++ 21 files changed, 140 insertions(+), 85 deletions(-) diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index 76d9c7dc17f..85bb956c96c 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -1891,7 +1891,7 @@ namespace winrt::TerminalApp::implementation { term.RaiseNotice({ this, &TerminalPage::_ControlNoticeRaisedHandler }); - // Add an event handler when the terminal wants to paste data from the Clipboard. + term.WriteToClipboard({ get_weak(), &TerminalPage::_copyToClipboard }); term.PasteFromClipboard({ this, &TerminalPage::_PasteFromClipboardHandler }); term.OpenHyperlink({ this, &TerminalPage::_OpenHyperlinkHandler }); @@ -1915,7 +1915,6 @@ namespace winrt::TerminalApp::implementation term.ShowWindowChanged({ get_weak(), &TerminalPage::_ShowWindowChangedHandler }); term.SearchMissingCommand({ get_weak(), &TerminalPage::_SearchMissingCommandHandler }); term.WindowSizeChanged({ get_weak(), &TerminalPage::_WindowSizeChanged }); - term.WriteToClipboard({ get_weak(), &TerminalPage::_copyToClipboard }); // Don't even register for the event if the feature is compiled off. if constexpr (Feature_ShellCompletions::IsEnabled()) @@ -2869,29 +2868,14 @@ namespace winrt::TerminalApp::implementation co_await winrt::resume_background(); winrt::hstring text; - uint32_t clipboardSequenceNumber = 0; - if (const auto clipboard = clipboard::open(nullptr)) { text = clipboard::read(); - clipboardSequenceNumber = GetClipboardSequenceNumber(); - } - else - { - co_return; } - if (globalSettings.TrimPaste() && - // If we're using bracketed paste mode, and the clipboard contents originate from us - // (= clipboard sequence number matches the last written one), don't trim the paste. - // Put inversely: Trim the contents if the sequence number changed OR if we're not using bracketed pastes. - (_copyToClipboardSequenceNumber.load(std::memory_order_relaxed) != clipboardSequenceNumber || !bracketedPaste)) + if (!bracketedPaste && globalSettings.TrimPaste()) { - // NOTE: This has to be done in 2 steps, because technically speaking we - // may be the only holder of the hstring instance and assigning to it - // will first release the current memory and then assign the new value. - winrt::hstring trimmed{ Utils::TrimPaste(text) }; - text = std::move(trimmed); + text = winrt::hstring{ Utils::TrimPaste(text) }; } if (text.empty()) @@ -2899,16 +2883,30 @@ namespace winrt::TerminalApp::implementation co_return; } - // If the requesting terminal is in bracketed paste mode, then we don't need to warn about a multi-line paste. - auto warnMultiLine = globalSettings.WarnAboutMultiLinePaste() && !eventArgs.BracketedPasteEnabled(); + bool warnMultiLine = false; + switch (globalSettings.WarnAboutMultiLinePaste()) + { + case WarnAboutMultiLinePaste::Automatic: + // NOTE that this is unsafe, because a shell that doesn't support bracketed paste + // will allow an attacker to enable the mode, not realize that, and then accept + // the paste as if it was a series of legitimate commands. See GH#13014. + warnMultiLine = !bracketedPaste; + break; + case WarnAboutMultiLinePaste::Always: + warnMultiLine = true; + break; + default: + warnMultiLine = false; + break; + } + if (warnMultiLine) { - const auto isNewLineLambda = [](auto c) { return c == L'\n' || c == L'\r'; }; - const auto hasNewLine = std::find_if(text.cbegin(), text.cend(), isNewLineLambda) != text.cend(); - warnMultiLine = hasNewLine; + const std::wstring_view view{ text }; + warnMultiLine = view.find_first_of(L"\r\n") != std::wstring_view::npos; } - constexpr const std::size_t minimumSizeForWarning = 1024 * 5; // 5 KiB + constexpr std::size_t minimumSizeForWarning = 1024 * 5; // 5 KiB const auto warnLargeText = text.size() > minimumSizeForWarning && globalSettings.WarnAboutLargePaste(); if (warnMultiLine || warnLargeText) @@ -2918,7 +2916,7 @@ namespace winrt::TerminalApp::implementation if (const auto strongThis = weakThis.get()) { // We have to initialize the dialog here to be able to change the text of the text block within it - FindName(L"MultiLinePasteDialog").try_as(); + std::ignore = FindName(L"MultiLinePasteDialog"); ClipboardText().Text(text); // The vertical offset on the scrollbar does not reset automatically, so reset it manually @@ -3242,23 +3240,18 @@ namespace winrt::TerminalApp::implementation } } - void TerminalPage::_copyToClipboard(const IInspectable, const WriteToClipboardEventArgs args) + void TerminalPage::_copyToClipboard(const IInspectable, const WriteToClipboardEventArgs args) const { - const auto plain = args.Plain(); - const auto html = args.Html(); - const auto rtf = args.Rtf(); - - if (auto clipboard = clipboard::open(_hostingHwnd.value_or(nullptr))) + if (const auto clipboard = clipboard::open(_hostingHwnd.value_or(nullptr))) { + const auto plain = args.Plain(); + const auto html = args.Html(); + const auto rtf = args.Rtf(); + clipboard::write( { plain.data(), plain.size() }, { reinterpret_cast(html.data()), html.size() }, { reinterpret_cast(rtf.data()), rtf.size() }); - - // `CloseClipboard` can still bump the sequence number, - // so we need to do this after dropping `clipboard`. - clipboard.reset(); - _copyToClipboardSequenceNumber.store(GetClipboardSequenceNumber(), std::memory_order_relaxed); } } diff --git a/src/cascadia/TerminalApp/TerminalPage.h b/src/cascadia/TerminalApp/TerminalPage.h index c31fd669863..ecc6ca224a5 100644 --- a/src/cascadia/TerminalApp/TerminalPage.h +++ b/src/cascadia/TerminalApp/TerminalPage.h @@ -226,7 +226,6 @@ namespace winrt::TerminalApp::implementation void _UpdateTabIndices(); TerminalApp::Tab _settingsTab{ nullptr }; - std::atomic _copyToClipboardSequenceNumber{ 0 }; bool _isInFocusMode{ false }; bool _isFullscreen{ false }; @@ -418,7 +417,7 @@ namespace winrt::TerminalApp::implementation safe_void_coroutine _SetTaskbarProgressHandler(const IInspectable sender, const IInspectable eventArgs); - void _copyToClipboard(IInspectable, Microsoft::Terminal::Control::WriteToClipboardEventArgs args); + void _copyToClipboard(IInspectable, Microsoft::Terminal::Control::WriteToClipboardEventArgs args) const; void _PasteText(); safe_void_coroutine _ControlNoticeRaisedHandler(const IInspectable sender, const Microsoft::Terminal::Control::NoticeEventArgs eventArgs); diff --git a/src/cascadia/TerminalControl/ControlCore.h b/src/cascadia/TerminalControl/ControlCore.h index 2f021104e41..e42162e3661 100644 --- a/src/cascadia/TerminalControl/ControlCore.h +++ b/src/cascadia/TerminalControl/ControlCore.h @@ -277,6 +277,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation til::typed_event FontSizeChanged; til::typed_event TitleChanged; + til::typed_event WriteToClipboard; til::typed_event<> WarningBell; til::typed_event<> TabColorChanged; til::typed_event<> BackgroundColorChanged; @@ -297,7 +298,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation til::typed_event SearchMissingCommand; til::typed_event<> RefreshQuickFixUI; til::typed_event WindowSizeChanged; - til::typed_event WriteToClipboard; til::typed_event<> CloseTerminalRequested; til::typed_event<> RestartTerminalRequested; diff --git a/src/cascadia/TerminalControl/ControlCore.idl b/src/cascadia/TerminalControl/ControlCore.idl index 4e488d9b6f3..a70573d956a 100644 --- a/src/cascadia/TerminalControl/ControlCore.idl +++ b/src/cascadia/TerminalControl/ControlCore.idl @@ -185,6 +185,7 @@ namespace Microsoft.Terminal.Control // These events are called from some background thread event Windows.Foundation.TypedEventHandler TitleChanged; + event Windows.Foundation.TypedEventHandler WriteToClipboard; event Windows.Foundation.TypedEventHandler WarningBell; event Windows.Foundation.TypedEventHandler TabColorChanged; event Windows.Foundation.TypedEventHandler BackgroundColorChanged; @@ -194,7 +195,6 @@ namespace Microsoft.Terminal.Control event Windows.Foundation.TypedEventHandler SearchMissingCommand; event Windows.Foundation.TypedEventHandler RefreshQuickFixUI; event Windows.Foundation.TypedEventHandler WindowSizeChanged; - event Windows.Foundation.TypedEventHandler WriteToClipboard; // These events are always called from the UI thread (bugs aside) event Windows.Foundation.TypedEventHandler FontSizeChanged; diff --git a/src/cascadia/TerminalControl/EventArgs.cpp b/src/cascadia/TerminalControl/EventArgs.cpp index ba5eeb4963e..e5a9fe58765 100644 --- a/src/cascadia/TerminalControl/EventArgs.cpp +++ b/src/cascadia/TerminalControl/EventArgs.cpp @@ -6,6 +6,7 @@ #include "FontSizeChangedArgs.g.cpp" #include "TitleChangedEventArgs.g.cpp" #include "ContextMenuRequestedEventArgs.g.cpp" +#include "WriteToClipboardEventArgs.g.cpp" #include "PasteFromClipboardEventArgs.g.cpp" #include "OpenHyperlinkEventArgs.g.cpp" #include "NoticeEventArgs.g.cpp" @@ -20,4 +21,3 @@ #include "StringSentEventArgs.g.cpp" #include "SearchMissingCommandEventArgs.g.cpp" #include "WindowSizeChangedEventArgs.g.cpp" -#include "WriteToClipboardEventArgs.g.cpp" diff --git a/src/cascadia/TerminalControl/EventArgs.h b/src/cascadia/TerminalControl/EventArgs.h index d2a95fbcc2c..53f1245ef06 100644 --- a/src/cascadia/TerminalControl/EventArgs.h +++ b/src/cascadia/TerminalControl/EventArgs.h @@ -6,6 +6,7 @@ #include "FontSizeChangedArgs.g.h" #include "TitleChangedEventArgs.g.h" #include "ContextMenuRequestedEventArgs.g.h" +#include "WriteToClipboardEventArgs.g.h" #include "PasteFromClipboardEventArgs.g.h" #include "OpenHyperlinkEventArgs.g.h" #include "NoticeEventArgs.g.h" @@ -20,7 +21,6 @@ #include "StringSentEventArgs.g.h" #include "SearchMissingCommandEventArgs.g.h" #include "WindowSizeChangedEventArgs.g.h" -#include "WriteToClipboardEventArgs.g.h" namespace winrt::Microsoft::Terminal::Control::implementation { @@ -57,6 +57,32 @@ namespace winrt::Microsoft::Terminal::Control::implementation WINRT_PROPERTY(winrt::Windows::Foundation::Point, Position); }; + struct WriteToClipboardEventArgs : WriteToClipboardEventArgsT + { + WriteToClipboardEventArgs(winrt::hstring&& plain, std::string&& html, std::string&& rtf) : + _plain(std::move(plain)), + _html(std::move(html)), + _rtf(std::move(rtf)) + { + } + + winrt::hstring Plain() const noexcept { return _plain; } + winrt::com_array Html() noexcept { return _cast(_html); } + winrt::com_array Rtf() noexcept { return _cast(_rtf); } + + private: + static winrt::com_array _cast(const std::string& str) + { + const auto beg = reinterpret_cast(str.data()); + const auto len = str.size(); + return { beg, beg + len }; + } + + winrt::hstring _plain; + std::string _html; + std::string _rtf; + }; + struct PasteFromClipboardEventArgs : public PasteFromClipboardEventArgsT { public: @@ -239,32 +265,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation WINRT_PROPERTY(int32_t, Width); WINRT_PROPERTY(int32_t, Height); }; - - struct WriteToClipboardEventArgs : WriteToClipboardEventArgsT - { - WriteToClipboardEventArgs(winrt::hstring&& plain, std::string&& html, std::string&& rtf) : - _plain(std::move(plain)), - _html(std::move(html)), - _rtf(std::move(rtf)) - { - } - - winrt::hstring Plain() const noexcept { return _plain; } - winrt::com_array Html() noexcept { return _cast(_html); } - winrt::com_array Rtf() noexcept { return _cast(_rtf); } - - private: - static winrt::com_array _cast(const std::string& str) - { - const auto beg = reinterpret_cast(str.data()); - const auto len = str.size(); - return { beg, beg + len }; - } - - winrt::hstring _plain; - std::string _html; - std::string _rtf; - }; } namespace winrt::Microsoft::Terminal::Control::factory_implementation diff --git a/src/cascadia/TerminalControl/EventArgs.idl b/src/cascadia/TerminalControl/EventArgs.idl index 78c4aa67274..d6086fd922d 100644 --- a/src/cascadia/TerminalControl/EventArgs.idl +++ b/src/cascadia/TerminalControl/EventArgs.idl @@ -32,6 +32,13 @@ namespace Microsoft.Terminal.Control AlphanumericHalfWidth, }; + enum WarnAboutMultiLinePaste + { + Automatic, + Always, + Never, + }; + runtimeclass FontSizeChangedArgs { Int32 Width { get; }; @@ -48,6 +55,13 @@ namespace Microsoft.Terminal.Control String Title; } + runtimeclass WriteToClipboardEventArgs + { + String Plain { get; }; // UTF-16, as required by CF_UNICODETEXT + byte[] Html { get; }; // UTF-8, as required by "HTML Format" + byte[] Rtf { get; }; // UTF-8, as required by "Rich Text Format" + } + runtimeclass PasteFromClipboardEventArgs { void HandleClipboardData(String data); @@ -145,11 +159,4 @@ namespace Microsoft.Terminal.Control Int32 Width; Int32 Height; } - - runtimeclass WriteToClipboardEventArgs - { - String Plain { get; }; // UTF-16, as required by CF_UNICODETEXT - byte[] Html { get; }; // UTF-8, as required by "HTML Format" - byte[] Rtf { get; }; // UTF-8, as required by "Rich Text Format" - } } diff --git a/src/cascadia/TerminalControl/TermControl.h b/src/cascadia/TerminalControl/TermControl.h index 214e299bf6a..d522e5b220e 100644 --- a/src/cascadia/TerminalControl/TermControl.h +++ b/src/cascadia/TerminalControl/TermControl.h @@ -463,6 +463,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation Control::ControlCore::UpdateSelectionMarkers_revoker UpdateSelectionMarkers; Control::ControlCore::OpenHyperlink_revoker coreOpenHyperlink; Control::ControlCore::TitleChanged_revoker TitleChanged; + Control::ControlCore::WriteToClipboard_revoker WriteToClipboard; Control::ControlCore::TabColorChanged_revoker TabColorChanged; Control::ControlCore::TaskbarProgressChanged_revoker TaskbarProgressChanged; Control::ControlCore::ConnectionStateChanged_revoker ConnectionStateChanged; @@ -473,7 +474,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation Control::ControlCore::SearchMissingCommand_revoker SearchMissingCommand; Control::ControlCore::RefreshQuickFixUI_revoker RefreshQuickFixUI; Control::ControlCore::WindowSizeChanged_revoker WindowSizeChanged; - Control::ControlCore::WriteToClipboard_revoker WriteToClipboard; // These are set up in _InitializeTerminal Control::ControlCore::RendererWarning_revoker RendererWarning; diff --git a/src/cascadia/TerminalControl/TermControl.idl b/src/cascadia/TerminalControl/TermControl.idl index 4bef92bcc20..2efe9a41066 100644 --- a/src/cascadia/TerminalControl/TermControl.idl +++ b/src/cascadia/TerminalControl/TermControl.idl @@ -53,6 +53,7 @@ namespace Microsoft.Terminal.Control Microsoft.Terminal.Control.IControlSettings Settings { get; }; event Windows.Foundation.TypedEventHandler TitleChanged; + event Windows.Foundation.TypedEventHandler WriteToClipboard; event Windows.Foundation.TypedEventHandler PasteFromClipboard; event Windows.Foundation.TypedEventHandler OpenHyperlink; event Windows.Foundation.TypedEventHandler SetTaskbarProgress; @@ -64,7 +65,6 @@ namespace Microsoft.Terminal.Control event Windows.Foundation.TypedEventHandler ReadOnlyChanged; event Windows.Foundation.TypedEventHandler FocusFollowMouseRequested; event Windows.Foundation.TypedEventHandler WindowSizeChanged; - event Windows.Foundation.TypedEventHandler WriteToClipboard; event Windows.Foundation.TypedEventHandler CompletionsChanged; diff --git a/src/cascadia/TerminalSettingsEditor/Interaction.cpp b/src/cascadia/TerminalSettingsEditor/Interaction.cpp index ed3c55fbda6..24199f0c1b1 100644 --- a/src/cascadia/TerminalSettingsEditor/Interaction.cpp +++ b/src/cascadia/TerminalSettingsEditor/Interaction.cpp @@ -5,6 +5,8 @@ #include "Interaction.h" #include "Interaction.g.cpp" +#include "EnumEntry.h" + using namespace winrt::Windows::UI::Xaml::Navigation; using namespace winrt::Microsoft::Terminal::Settings::Model; @@ -13,6 +15,8 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation Interaction::Interaction() { InitializeComponent(); + + INITIALIZE_BINDABLE_ENUM_SETTING(WarnAboutMultiLinePaste, WarnAboutMultiLinePaste, winrt::Microsoft::Terminal::Control::WarnAboutMultiLinePaste, L"Globals_WarnAboutMultiLinePaste", L"Content"); } void Interaction::OnNavigatedTo(const NavigationEventArgs& e) diff --git a/src/cascadia/TerminalSettingsEditor/Interaction.h b/src/cascadia/TerminalSettingsEditor/Interaction.h index cbb7846c38a..568139fdeed 100644 --- a/src/cascadia/TerminalSettingsEditor/Interaction.h +++ b/src/cascadia/TerminalSettingsEditor/Interaction.h @@ -16,6 +16,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation til::property_changed_event PropertyChanged; WINRT_OBSERVABLE_PROPERTY(Editor::InteractionViewModel, ViewModel, PropertyChanged.raise, nullptr); + GETSET_BINDABLE_ENUM_SETTING(WarnAboutMultiLinePaste, winrt::Microsoft::Terminal::Control::WarnAboutMultiLinePaste, ViewModel().WarnAboutMultiLinePaste); }; } diff --git a/src/cascadia/TerminalSettingsEditor/Interaction.idl b/src/cascadia/TerminalSettingsEditor/Interaction.idl index 840fca4a750..3a41823b87f 100644 --- a/src/cascadia/TerminalSettingsEditor/Interaction.idl +++ b/src/cascadia/TerminalSettingsEditor/Interaction.idl @@ -9,5 +9,8 @@ namespace Microsoft.Terminal.Settings.Editor { Interaction(); InteractionViewModel ViewModel { get; }; + + IInspectable CurrentWarnAboutMultiLinePaste; + Windows.Foundation.Collections.IObservableVector WarnAboutMultiLinePasteList { get; }; } } diff --git a/src/cascadia/TerminalSettingsEditor/Interaction.xaml b/src/cascadia/TerminalSettingsEditor/Interaction.xaml index 84a5019ea6d..fe3f67c6eb3 100644 --- a/src/cascadia/TerminalSettingsEditor/Interaction.xaml +++ b/src/cascadia/TerminalSettingsEditor/Interaction.xaml @@ -142,8 +142,11 @@ - + diff --git a/src/cascadia/TerminalSettingsEditor/InteractionViewModel.idl b/src/cascadia/TerminalSettingsEditor/InteractionViewModel.idl index c178c9f744b..ba77aec55f5 100644 --- a/src/cascadia/TerminalSettingsEditor/InteractionViewModel.idl +++ b/src/cascadia/TerminalSettingsEditor/InteractionViewModel.idl @@ -30,7 +30,7 @@ namespace Microsoft.Terminal.Settings.Editor PERMANENT_OBSERVABLE_PROJECTED_SETTING(Boolean, ConfirmCloseAllTabs); PERMANENT_OBSERVABLE_PROJECTED_SETTING(Boolean, InputServiceWarning); PERMANENT_OBSERVABLE_PROJECTED_SETTING(Boolean, WarnAboutLargePaste); - PERMANENT_OBSERVABLE_PROJECTED_SETTING(Boolean, WarnAboutMultiLinePaste); + PERMANENT_OBSERVABLE_PROJECTED_SETTING(Microsoft.Terminal.Control.WarnAboutMultiLinePaste, WarnAboutMultiLinePaste); PERMANENT_OBSERVABLE_PROJECTED_SETTING(Boolean, EnableColorSelection); } } diff --git a/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw b/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw index 816c799eb11..5c184af53b7 100644 --- a/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw @@ -1924,10 +1924,26 @@ Warn when "Touch Keyboard and Handwriting Panel Service" is disabled - Warn when trying to paste more than 5 KiB of characters + Warn when pasting more than 5 KiB - Warn when trying to paste a "new line" character + Warn when pasting newlines + + + If your shell does not support the "bracketed paste" mode, we recommend setting this to "Always" for security reasons. + "bracketed paste" is a technical term referring to escape sequences. "Always" is a setting, as in "Always VS Never". + + + Only if "bracketed paste" is disabled + "bracketed paste" is a technical term referring to terminal escape sequences. + + + Always + As in: Always VS Never + + + Never + As in: Always VS Never Windows Terminal is running in portable mode. diff --git a/src/cascadia/TerminalSettingsModel/EnumMappings.cpp b/src/cascadia/TerminalSettingsModel/EnumMappings.cpp index 7895bec7d95..6f867db0b65 100644 --- a/src/cascadia/TerminalSettingsModel/EnumMappings.cpp +++ b/src/cascadia/TerminalSettingsModel/EnumMappings.cpp @@ -42,6 +42,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation DEFINE_ENUM_MAP(Microsoft::Terminal::Core::MatchMode, MatchMode); DEFINE_ENUM_MAP(Microsoft::Terminal::Control::GraphicsAPI, GraphicsAPI); DEFINE_ENUM_MAP(Microsoft::Terminal::Control::TextMeasurement, TextMeasurement); + DEFINE_ENUM_MAP(Microsoft::Terminal::Control::WarnAboutMultiLinePaste, WarnAboutMultiLinePaste); // Profile Settings DEFINE_ENUM_MAP(Model::CloseOnExitMode, CloseOnExitMode); diff --git a/src/cascadia/TerminalSettingsModel/EnumMappings.h b/src/cascadia/TerminalSettingsModel/EnumMappings.h index 15fbf50a403..a3412185952 100644 --- a/src/cascadia/TerminalSettingsModel/EnumMappings.h +++ b/src/cascadia/TerminalSettingsModel/EnumMappings.h @@ -38,6 +38,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation static winrt::Windows::Foundation::Collections::IMap MatchMode(); static winrt::Windows::Foundation::Collections::IMap GraphicsAPI(); static winrt::Windows::Foundation::Collections::IMap TextMeasurement(); + static winrt::Windows::Foundation::Collections::IMap WarnAboutMultiLinePaste(); // Profile Settings static winrt::Windows::Foundation::Collections::IMap CloseOnExitMode(); diff --git a/src/cascadia/TerminalSettingsModel/EnumMappings.idl b/src/cascadia/TerminalSettingsModel/EnumMappings.idl index 57735eccd5b..befb2cea5db 100644 --- a/src/cascadia/TerminalSettingsModel/EnumMappings.idl +++ b/src/cascadia/TerminalSettingsModel/EnumMappings.idl @@ -20,6 +20,7 @@ namespace Microsoft.Terminal.Settings.Model static Windows.Foundation.Collections.IMap MatchMode { get; }; static Windows.Foundation.Collections.IMap GraphicsAPI { get; }; static Windows.Foundation.Collections.IMap TextMeasurement { get; }; + static Windows.Foundation.Collections.IMap WarnAboutMultiLinePaste { get; }; // Profile Settings static Windows.Foundation.Collections.IMap CloseOnExitMode { get; }; diff --git a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl index 7e6c1180e4a..e003660f329 100644 --- a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl +++ b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl @@ -70,7 +70,7 @@ namespace Microsoft.Terminal.Settings.Model INHERITABLE_SETTING(Boolean, InputServiceWarning); INHERITABLE_SETTING(Microsoft.Terminal.Control.CopyFormat, CopyFormatting); INHERITABLE_SETTING(Boolean, WarnAboutLargePaste); - INHERITABLE_SETTING(Boolean, WarnAboutMultiLinePaste); + INHERITABLE_SETTING(Microsoft.Terminal.Control.WarnAboutMultiLinePaste, WarnAboutMultiLinePaste); INHERITABLE_SETTING(Boolean, TrimPaste); INHERITABLE_SETTING(LaunchPosition, InitialPosition); INHERITABLE_SETTING(Boolean, CenterOnLaunch); diff --git a/src/cascadia/TerminalSettingsModel/MTSMSettings.h b/src/cascadia/TerminalSettingsModel/MTSMSettings.h index 5fa1fdd59ca..1e548812bd3 100644 --- a/src/cascadia/TerminalSettingsModel/MTSMSettings.h +++ b/src/cascadia/TerminalSettingsModel/MTSMSettings.h @@ -46,7 +46,7 @@ Author(s): X(bool, InputServiceWarning, "warning.inputService", true) \ X(winrt::Microsoft::Terminal::Control::CopyFormat, CopyFormatting, "copyFormatting", 0) \ X(bool, WarnAboutLargePaste, "warning.largePaste", true) \ - X(bool, WarnAboutMultiLinePaste, "warning.multiLinePaste", true) \ + X(winrt::Microsoft::Terminal::Control::WarnAboutMultiLinePaste, WarnAboutMultiLinePaste, "warning.multiLinePaste", winrt::Microsoft::Terminal::Control::WarnAboutMultiLinePaste::Automatic) \ X(Model::LaunchPosition, InitialPosition, "initialPosition", nullptr, nullptr) \ X(bool, CenterOnLaunch, "centerOnLaunch", false) \ X(Model::FirstWindowPreference, FirstWindowPreference, "firstWindowPreference", FirstWindowPreference::DefaultProfile) \ diff --git a/src/cascadia/TerminalSettingsModel/TerminalSettingsSerializationHelpers.h b/src/cascadia/TerminalSettingsModel/TerminalSettingsSerializationHelpers.h index 32a24b11f7e..4376ad406b4 100644 --- a/src/cascadia/TerminalSettingsModel/TerminalSettingsSerializationHelpers.h +++ b/src/cascadia/TerminalSettingsModel/TerminalSettingsSerializationHelpers.h @@ -803,3 +803,29 @@ JSON_ENUM_MAPPER(::winrt::Microsoft::Terminal::Control::PathTranslationStyle) pair_type{ "mingw", ValueType::MinGW }, }; }; + +JSON_ENUM_MAPPER(::winrt::Microsoft::Terminal::Control::WarnAboutMultiLinePaste) +{ + JSON_MAPPINGS(3) = { + pair_type{ "automatic", ValueType::Automatic }, + pair_type{ "always", ValueType::Always }, + pair_type{ "never", ValueType::Never }, + }; + + // Override mapping parser to add boolean parsing + ::winrt::Microsoft::Terminal::Control::WarnAboutMultiLinePaste FromJson(const Json::Value& json) + { + if (json.isBool()) + { + return json.asBool() ? ValueType::Automatic : ValueType::Never; + } + return EnumMapper::FromJson(json); + } + + bool CanConvert(const Json::Value& json) + { + return EnumMapper::CanConvert(json) || json.isBool(); + } + + using EnumMapper::TypeDescription; +}; From 5b746b0570b61aa9eb68803dd4a25c5e2b99d439 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Fri, 8 Aug 2025 19:49:44 +0200 Subject: [PATCH 3/5] Update src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw Co-authored-by: Dustin L. Howett --- .../TerminalSettingsEditor/Resources/en-US/Resources.resw | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw b/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw index 5c184af53b7..7b3ebe85a64 100644 --- a/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw @@ -1930,7 +1930,7 @@ Warn when pasting newlines - If your shell does not support the "bracketed paste" mode, we recommend setting this to "Always" for security reasons. + If your shell does not support "bracketed paste" mode, we recommend setting this to "Always" for security reasons. "bracketed paste" is a technical term referring to escape sequences. "Always" is a setting, as in "Always VS Never". From f1750cff3df45da551164250801397f7b35cd276 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Fri, 8 Aug 2025 21:01:11 +0200 Subject: [PATCH 4/5] Update SerializationTests.cpp --- src/cascadia/UnitTests_SettingsModel/SerializationTests.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/cascadia/UnitTests_SettingsModel/SerializationTests.cpp b/src/cascadia/UnitTests_SettingsModel/SerializationTests.cpp index d3d6586479e..c0c4f3a2e62 100644 --- a/src/cascadia/UnitTests_SettingsModel/SerializationTests.cpp +++ b/src/cascadia/UnitTests_SettingsModel/SerializationTests.cpp @@ -126,7 +126,7 @@ namespace SettingsModelUnitTests "warning.confirmCloseAllTabs" : true, "warning.inputService" : true, "warning.largePaste" : true, - "warning.multiLinePaste" : true, + "warning.multiLinePaste" : "automatic", "actions": [], "keybindings": [] @@ -1296,3 +1296,4 @@ namespace SettingsModelUnitTests VERIFY_ARE_EQUAL(toString(newResult), toString(oldResult)); } } + From ff6f12c8578d98ad69148ad5a83ace7b7e746955 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Fri, 8 Aug 2025 22:16:23 +0200 Subject: [PATCH 5/5] GitHub. why. editor. bad. Severe clubbing needed --- src/cascadia/UnitTests_SettingsModel/SerializationTests.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/cascadia/UnitTests_SettingsModel/SerializationTests.cpp b/src/cascadia/UnitTests_SettingsModel/SerializationTests.cpp index 84ed2bbdf8f..3112f83939e 100644 --- a/src/cascadia/UnitTests_SettingsModel/SerializationTests.cpp +++ b/src/cascadia/UnitTests_SettingsModel/SerializationTests.cpp @@ -1326,4 +1326,3 @@ namespace SettingsModelUnitTests VERIFY_ARE_EQUAL(R"(c:\this_icon_had_better_not_exist.tiff)", newResult["profiles"]["list"][0]["icon"].asString()); } } -