// Copyright (c) Microsoft Corporation // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. using System.Runtime.InteropServices; namespace Microsoft.CommandPalette.Extensions.Toolkit; // shamelessly from https://github.com/PowerShell/PowerShell/blob/master/src/Microsoft.PowerShell.Commands.Management/commands/management/Clipboard.cs public static partial class ClipboardHelper { private static readonly bool? _clipboardSupported = true; // Used if an external clipboard is not available, e.g. if xclip is missing. // This is useful for testing in CI as well. private static string? _internalClipboard; public static string GetText() { if (_clipboardSupported == false) { return _internalClipboard ?? string.Empty; } var tool = string.Empty; var args = string.Empty; var clipboardText = string.Empty; ExecuteOnStaThread(() => GetTextImpl(out clipboardText)); return clipboardText; } public static void SetText(string text) { if (_clipboardSupported == false) { _internalClipboard = text; return; } var tool = string.Empty; var args = string.Empty; ExecuteOnStaThread(() => SetClipboardData(Tuple.Create(text, CF_UNICODETEXT))); return; } public static void SetRtf(string plainText, string rtfText) { if (s_CF_RTF == 0) { s_CF_RTF = RegisterClipboardFormat("Rich Text Format"); } ExecuteOnStaThread(() => SetClipboardData( Tuple.Create(plainText, CF_UNICODETEXT), Tuple.Create(rtfText, s_CF_RTF))); } #pragma warning disable SA1310 // Field names should not contain underscore private const uint GMEM_MOVEABLE = 0x0002; private const uint GMEM_ZEROINIT = 0x0040; #pragma warning restore SA1310 // Field names should not contain underscore private const uint GHND = GMEM_MOVEABLE | GMEM_ZEROINIT; [LibraryImport("kernel32.dll")] private static partial IntPtr GlobalAlloc(uint flags, UIntPtr dwBytes); [LibraryImport("kernel32.dll")] private static partial IntPtr GlobalFree(IntPtr hMem); [LibraryImport("kernel32.dll")] private static partial IntPtr GlobalLock(IntPtr hMem); [LibraryImport("kernel32.dll")] [return: MarshalAs(UnmanagedType.Bool)] private static partial bool GlobalUnlock(IntPtr hMem); [LibraryImport("kernel32.dll", EntryPoint = "RtlMoveMemory")] private static partial void CopyMemory(IntPtr dest, IntPtr src, uint count); [LibraryImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)] private static partial bool IsClipboardFormatAvailable(uint format); [LibraryImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)] private static partial bool OpenClipboard(IntPtr hWndNewOwner); [LibraryImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)] private static partial bool CloseClipboard(); [LibraryImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)] private static partial bool EmptyClipboard(); [LibraryImport("user32.dll")] private static partial IntPtr GetClipboardData(uint format); [LibraryImport("user32.dll")] private static partial IntPtr SetClipboardData(uint format, IntPtr data); [LibraryImport("user32.dll", StringMarshalling = StringMarshalling.Utf16)] private static partial uint RegisterClipboardFormat(string lpszFormat); #pragma warning disable SA1310 // Field names should not contain underscore private const uint CF_TEXT = 1; private const uint CF_UNICODETEXT = 13; #pragma warning disable SA1308 // Variable names should not be prefixed private static uint s_CF_RTF; #pragma warning restore SA1308 // Variable names should not be prefixed #pragma warning restore SA1310 // Field names should not contain underscore private static bool GetTextImpl(out string text) { try { if (IsClipboardFormatAvailable(CF_UNICODETEXT)) { if (OpenClipboard(IntPtr.Zero)) { var data = GetClipboardData(CF_UNICODETEXT); if (data != IntPtr.Zero) { data = GlobalLock(data); text = Marshal.PtrToStringUni(data) ?? string.Empty; GlobalUnlock(data); return true; } } } else if (IsClipboardFormatAvailable(CF_TEXT)) { if (OpenClipboard(IntPtr.Zero)) { var data = GetClipboardData(CF_TEXT); if (data != IntPtr.Zero) { data = GlobalLock(data); text = Marshal.PtrToStringAnsi(data) ?? string.Empty; GlobalUnlock(data); return true; } } } } catch { // Ignore exceptions } finally { CloseClipboard(); } text = string.Empty; return false; } private static bool SetClipboardData(params Tuple[] data) { try { if (!OpenClipboard(IntPtr.Zero)) { return false; } EmptyClipboard(); foreach (var d in data) { if (!SetSingleClipboardData(d.Item1, d.Item2)) { return false; } } } finally { CloseClipboard(); } return true; } private static bool SetSingleClipboardData(string text, uint format) { var hGlobal = IntPtr.Zero; var data = IntPtr.Zero; try { uint bytes; if (format == s_CF_RTF || format == CF_TEXT) { bytes = (uint)(text.Length + 1); data = Marshal.StringToHGlobalAnsi(text); } else if (format == CF_UNICODETEXT) { bytes = (uint)((text.Length + 1) * 2); data = Marshal.StringToHGlobalUni(text); } else { // Not yet supported format. return false; } if (data == IntPtr.Zero) { return false; } hGlobal = GlobalAlloc(GHND, (UIntPtr)bytes); if (hGlobal == IntPtr.Zero) { return false; } var dataCopy = GlobalLock(hGlobal); if (dataCopy == IntPtr.Zero) { return false; } CopyMemory(dataCopy, data, bytes); GlobalUnlock(hGlobal); if (SetClipboardData(format, hGlobal) != IntPtr.Zero) { // The clipboard owns this memory now, so don't free it. hGlobal = IntPtr.Zero; } } catch { // Ignore failures } finally { if (data != IntPtr.Zero) { Marshal.FreeHGlobal(data); } if (hGlobal != IntPtr.Zero) { GlobalFree(hGlobal); } } return true; } private static void ExecuteOnStaThread(Func action) { const int RetryCount = 5; var tries = 0; if (Thread.CurrentThread.GetApartmentState() == ApartmentState.STA) { while (tries++ < RetryCount && !action()) { // wait until RetryCount or action } return; } Exception? exception = null; var thread = new Thread(() => { try { while (tries++ < RetryCount && !action()) { // wait until RetryCount or action } } catch (Exception e) { exception = e; } }); thread.SetApartmentState(ApartmentState.STA); thread.Start(); thread.Join(); if (exception is not null) { throw exception; } } }