Search code examples
c#.net-coreclipboard

.Net Core - copy to clipboard?


Is it possible to copy something to the clipboard using .Net Core (in a platform-agnostic way)?

It seems that the Clipboard class is missing, and P/Invoking isn't an option outside of Windows.

Edit: Unfortunately until now there's been a discrepancy between what my question said and what people heard when they read the question. Based on the comments and answers two things are clear. First, very few care whether the truest kind of "ivory tower" platform agnosticism exists or not. Second, when people post code examples showing how you use the clipboard on different platforms then the technically correct answer ("no, it's not possible") is confusing. So I have struck the parenthetical clause.


Solution

  • This project of mine (https://github.com/SimonCropp/TextCopy) uses a mixed approach of PInvoke and command line invocation. it currently supports

    • Windows with .NET Framework 4.6.1 and up
    • Windows with .NET Core 2.0 and up
    • Windows with Mono 5.0 and up
    • OSX with .NET Core 2.0 and up
    • OSX with Mono 5.20.1 and up
    • Linux with .NET Core 2.0 and up
    • Linux with Mono 5.20.1 and up

    Usage:

    Install-Package TextCopy
    
    TextCopy.ClipboardService.SetText("Text to place in clipboard");
    

    Or just use the actual code

    Windows

    https://github.com/CopyText/TextCopy/blob/master/src/TextCopy/WindowsClipboard.cs

    static class WindowsClipboard
    {
        public static void SetText(string text)
        {
            OpenClipboard();
    
            EmptyClipboard();
            IntPtr hGlobal = default;
            try
            {
                var bytes = (text.Length + 1) * 2;
                hGlobal = Marshal.AllocHGlobal(bytes);
    
                if (hGlobal == default)
                {
                    ThrowWin32();
                }
    
                var target = GlobalLock(hGlobal);
    
                if (target == default)
                {
                    ThrowWin32();
                }
    
                try
                {
                    Marshal.Copy(text.ToCharArray(), 0, target, text.Length);
                }
                finally
                {
                    GlobalUnlock(target);
                }
    
                if (SetClipboardData(cfUnicodeText, hGlobal) == default)
                {
                    ThrowWin32();
                }
    
                hGlobal = default;
            }
            finally
            {
                if (hGlobal != default)
                {
                    Marshal.FreeHGlobal(hGlobal);
                }
    
                CloseClipboard();
            }
        }
    
        public static void OpenClipboard()
        {
            var num = 10;
            while (true)
            {
                if (OpenClipboard(default))
                {
                    break;
                }
    
                if (--num == 0)
                {
                    ThrowWin32();
                }
    
                Thread.Sleep(100);
            }
        }
    
        const uint cfUnicodeText = 13;
    
        static void ThrowWin32()
        {
            throw new Win32Exception(Marshal.GetLastWin32Error());
        }
    
        [DllImport("kernel32.dll", SetLastError = true)]
        static extern IntPtr GlobalLock(IntPtr hMem);
    
        [DllImport("kernel32.dll", SetLastError = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        static extern bool GlobalUnlock(IntPtr hMem);
    
        [DllImport("user32.dll", SetLastError = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        static extern bool OpenClipboard(IntPtr hWndNewOwner);
    
        [DllImport("user32.dll", SetLastError = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        static extern bool CloseClipboard();
    
        [DllImport("user32.dll", SetLastError = true)]
        static extern IntPtr SetClipboardData(uint uFormat, IntPtr data);
    
        [DllImport("user32.dll")]
        static extern bool EmptyClipboard();
    }
    

    macOS

    https://github.com/CopyText/TextCopy/blob/master/src/TextCopy/OsxClipboard.cs

    static class OsxClipboard
    {
        public static void SetText(string text)
        {
            var nsString = objc_getClass("NSString");
            IntPtr str = default;
            IntPtr dataType = default;
            try
            {
                str = objc_msgSend(objc_msgSend(nsString, sel_registerName("alloc")), sel_registerName("initWithUTF8String:"), text);
                dataType = objc_msgSend(objc_msgSend(nsString, sel_registerName("alloc")), sel_registerName("initWithUTF8String:"), NSPasteboardTypeString);
    
                var nsPasteboard = objc_getClass("NSPasteboard");
                var generalPasteboard = objc_msgSend(nsPasteboard, sel_registerName("generalPasteboard"));
    
                objc_msgSend(generalPasteboard, sel_registerName("clearContents"));
                objc_msgSend(generalPasteboard, sel_registerName("setString:forType:"), str, dataType);
            }
            finally
            {
                if (str != default)
                {
                    objc_msgSend(str, sel_registerName("release"));
                }
    
                if (dataType != default)
                {
                    objc_msgSend(dataType, sel_registerName("release"));
                }
            }
        }
    
        [DllImport("/System/Library/Frameworks/AppKit.framework/AppKit")]
        static extern IntPtr objc_getClass(string className);
    
        [DllImport("/System/Library/Frameworks/AppKit.framework/AppKit")]
        static extern IntPtr objc_msgSend(IntPtr receiver, IntPtr selector);
    
        [DllImport("/System/Library/Frameworks/AppKit.framework/AppKit")]
        static extern IntPtr objc_msgSend(IntPtr receiver, IntPtr selector, string arg1);
    
        [DllImport("/System/Library/Frameworks/AppKit.framework/AppKit")]
        static extern IntPtr objc_msgSend(IntPtr receiver, IntPtr selector, IntPtr arg1, IntPtr arg2);
    
        [DllImport("/System/Library/Frameworks/AppKit.framework/AppKit")]
        static extern IntPtr sel_registerName(string selectorName);
    
        const string NSPasteboardTypeString = "public.utf8-plain-text";
    }
    

    Linux

    https://github.com/CopyText/TextCopy/blob/master/src/TextCopy/LinuxClipboard_2.1.cs

    static class LinuxClipboard
    {
        public static void SetText(string text)
        {
            var tempFileName = Path.GetTempFileName();
            File.WriteAllText(tempFileName, text);
            try
            {
                BashRunner.Run($"cat {tempFileName} | xclip");
            }
            finally
            {
                File.Delete(tempFileName);
            }
        }
    
        public static string GetText()
        {
            var tempFileName = Path.GetTempFileName();
            try
            {
                BashRunner.Run($"xclip -o > {tempFileName}");
                return File.ReadAllText(tempFileName);
            }
            finally
            {
                File.Delete(tempFileName);
            }
        }
    }
    
    static class BashRunner
    {
        public static string Run(string commandLine)
        {
            var errorBuilder = new StringBuilder();
            var outputBuilder = new StringBuilder();
            var arguments = $"-c \"{commandLine}\"";
            using (var process = new Process
            {
                StartInfo = new ProcessStartInfo
                {
                    FileName = "bash",
                    Arguments = arguments,
                    RedirectStandardOutput = true,
                    RedirectStandardError = true,
                    UseShellExecute = false,
                    CreateNoWindow = false,
                }
            })
            {
                process.Start();
                process.OutputDataReceived += (sender, args) => { outputBuilder.AppendLine(args.Data); };
                process.BeginOutputReadLine();
                process.ErrorDataReceived += (sender, args) => { errorBuilder.AppendLine(args.Data); };
                process.BeginErrorReadLine();
                if (!process.WaitForExit(500))
                {
                    var timeoutError = $@"Process timed out. Command line: bash {arguments}.
    Output: {outputBuilder}
    Error: {errorBuilder}";
                    throw new Exception(timeoutError);
                }
                if (process.ExitCode == 0)
                {
                    return outputBuilder.ToString();
                }
    
                var error = $@"Could not execute process. Command line: bash {arguments}.
    Output: {outputBuilder}
    Error: {errorBuilder}";
                throw new Exception(error);
            }
        }
    }