Files
PowerToys/src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/ClipboardHelper.cs
2025-12-05 10:21:47 -06:00

299 lines
8.4 KiB
C#

// 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<string, uint>[] 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<bool> 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;
}
}
}