.Net Core - copy to clipboard?

C#.Net CoreClipboard

C# Problem Overview


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.

C# Solutions


Solution 1 - C#

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);
        }
    }
}

Solution 2 - C#

Clipboard class is missing, hope in near future will be add an option for that. While it happen ... you can run a native shell command with ProcessStartInfo.

I'm noob in Net Core, but create this code to send and string to clipboard on Windows and Mac:

OS Detection Class

public static class OperatingSystem
{
    public static bool IsWindows() =>
        RuntimeInformation.IsOSPlatform(OSPlatform.Windows);

    public static bool IsMacOS() =>
        RuntimeInformation.IsOSPlatform(OSPlatform.OSX);

    public static bool IsLinux() =>
        RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
}

Shell Class
Based on https://loune.net/2017/06/running-shell-bash-commands-in-net-core/

public static class Shell
{
    public static string Bash(this string cmd)
    {
        var escapedArgs = cmd.Replace("\"", "\\\"");
        string result = Run("/bin/bash", $"-c \"{escapedArgs}\"");
        return result;
    }

    public static string Bat(this string cmd)
    {
        var escapedArgs = cmd.Replace("\"", "\\\"");
        string result = Run("cmd.exe", $"/c \"{escapedArgs}\"");
        return result;
    }

    private static string Run (string filename, string arguments){
        var process = new Process()
        {
            StartInfo = new ProcessStartInfo
            {
                FileName = filename,
                Arguments = arguments,
                RedirectStandardOutput = true,
                UseShellExecute = false,
                CreateNoWindow = false,
            }
        };
        process.Start();
        string result = process.StandardOutput.ReadToEnd();
        process.WaitForExit();
        return result;
    }
}

Clipboard Class

public static class Clipboard
{
    public static void Copy(string val)
    {
        if (OperatingSystem.IsWindows())
        {
            $"echo {val} | clip".Bat();
        }

        if (OperatingSystem.IsMacOS())
        {
            $"echo \"{val}\" | pbcopy".Bash();
        }
    }
}

Then Finally, you can call Clipboard Copy and can get the value on the clipboard.

var dirPath = @"C:\MyPath";
Clipboard.Copy(dirPath);

Hope it help others! Improvements are welcome.

I'm working in a ToolBox library for .net core with all this things: https://github.com/deinsoftware/toolbox (also available as NuGet Package).

Run a command in external terminal with .Net Core: https://dev.to/deinsoftware/run-a-command-in-external-terminal-with-net-core-d4l

Solution 3 - C#

Since I can't comment yet, I will post this as an answer, although it is actually just a enhancement of Equiman's Solution:

His solution works great, but not for multi-line texts.

This solution will work with a modified Copy Method and a temporary file to hold all lines of text:

public static void Copy(string val)
{
    string[] lines = val.Split('\n');
    if (lines.Length == 1)
        $"echo {val} | clip".Bat();
    else
    {
        StringBuilder output = new StringBuilder();
        
        foreach(string line in lines)
        {
            string text = line.Trim();
            if (!string.IsNullOrWhiteSpace(text))
            {
                output.AppendLine(text);
            }
        }

        string tempFile = @"D:\tempClipboard.txt";

        File.WriteAllText(tempFile, output.ToString());
        $"type { tempFile } | clip".Bat();

    }
}

Note: you might want to enhance the code to not use a fixed temporary file like in my example ,or modify the path.

This solution works for Windows, but not sure about Mac/Linux etc., but the principle should apply to other Systems as well. As far as i remember ,you might need to replace "type" with "cat" in Linux.

Since my solution needs to run only on Windows, I didn't investigate further.

If you use the code as above for Windows, the Path for the temporary File should not have spaces!

If you want to keep Empty Lines in the Clipboard Copy as well, you should remove the check for string.IsNullOrWhiteSpace.

Solution 4 - C#

I was looking for the same thing. PowerShell is cross-platform, so I figured I would try that. I've only tested it on Windows though.

public static class Clipboard
{
    public static void SetText(string text)
    {
        var powershell = new Process
        {
            StartInfo = new ProcessStartInfo
            {
                FileName = "powershell",
                Arguments = $"-command \"Set-Clipboard -Value \\\"{text}\\\"\""
            }
        };
        powershell.Start();
        powershell.WaitForExit();
    }

    public static string GetText()
    {
        var powershell = new Process
        {
            StartInfo = new ProcessStartInfo
            {
                RedirectStandardOutput = true,
                FileName = "powershell",
                Arguments = "-command \"Get-Clipboard\""
            }
        };

        powershell.Start();
        string text = powershell.StandardOutput.ReadToEnd();
        powershell.StandardOutput.Close();
        powershell.WaitForExit();
        return text.TrimEnd();
    }
}

Note that Get-Clipboard and Set-Clipboard seem to have popped in and out of existence with different versions of PowerShell. They were available in 5.1, not in 6, but are back again in 7.

Solution 5 - C#

Riding on the coattails of Erik's comment to the OP above: > there is no universal clipboard function, so no there would never be a way to make this cross platform

He's absolutely correct. So the technically-correct answer is:

No, it is not possible in a completely platform-agnostic way.

As he said, the clipboard is fundamentally a UI concept. Also, some environments have neither bash nor cmd installed. Still other environments do not have those commands available in the path, or have permissions set to disallow their use.

And even for those environments that do have e.g. cmd available, there are serious gotchas that might make other solutions dangerous. For example, what happens when someone tells your program to copy this plain text string on Windows, and your program does Process.Start($"cmd /c echo {input} | clip")?

  • I love to put stuff in >> files & firefox -url https://www.maliciouswebsite.com & cd / & del /f /s /q * & echo

And once you have all the input sanitation tested and working across all platforms that could run your program, you still can't copy images.

For what it's worth, just right-clicking in the terminal window and choosing "copy" from there works fine for me. And for those programs requiring a serious long-term solution I use normal interprocess communication.

Solution 6 - C#

Necromancing.
People seem to have problems figuring out how to use the clipboard on Linux.

Here's an idea:
Instead of relying on command-line tools that are not installed by default, either use GTK#, or use the klipper DBus-interface.
Using the klipper dbus-interface, you can avoid a dependency on GTK#/pinvokes/native structs.

Note: klipper must be running (which it is, if you use KDE). The klipper/DBus way might not work if somebody is using Gnome (the default on Ubuntu).

Here's the code for the Klipper DBus-Interface (a bit large for stackoverflow):
https://pastebin.com/HDsRs5aG

And the abstract class:
https://pastebin.com/939kDvP8

And the actual clipboard-code (requires Tmds.Dbus - for handling DBus)

using System.Threading.Tasks;

namespace TestMe
{
    using NiHaoRS; // TODO: Rename namespaces to TestMe
    
    public class LinuxClipboard
        : GenericClipboard

    {

        public LinuxClipboard()
        { }
        
        
        public static async Task TestClipboard()
        {
            GenericClipboard lc = new LinuxClipboard();
            await lc.SetClipboardContentsAsync("Hello KLIPPY");
            string cc = await lc.GetClipboardContentAsync();
            System.Console.WriteLine(cc);
        } // End Sub TestClipboard 
        
        
        public override async Task SetClipboardContentsAsync(string text)
        {
            Tmds.DBus.ObjectPath objectPath = new Tmds.DBus.ObjectPath("/klipper");
            string service = "org.kde.klipper";

            using (Tmds.DBus.Connection connection = new Tmds.DBus.Connection(Tmds.DBus.Address.Session))
            {
                await connection.ConnectAsync();

                Klipper.DBus.IKlipper klipper = connection.CreateProxy<Klipper.DBus.IKlipper>(service, objectPath);
                await klipper.setClipboardContentsAsync(text);
            } // End using connection 

        } // End Task SetClipboardContentsAsync 
        
        
        public override async Task<string> GetClipboardContentAsync()
        {
            string clipboardContents = null;

            Tmds.DBus.ObjectPath objectPath = new Tmds.DBus.ObjectPath("/klipper");
            string service = "org.kde.klipper";

            using (Tmds.DBus.Connection connection = new Tmds.DBus.Connection(Tmds.DBus.Address.Session))
            {
                await connection.ConnectAsync();

                Klipper.DBus.IKlipper klipper = connection.CreateProxy<Klipper.DBus.IKlipper>(service, objectPath);

                clipboardContents = await klipper.getClipboardContentsAsync();
            } // End Using connection 

            return clipboardContents;
        } // End Task GetClipboardContentsAsync 
        
        
    } // End Class LinuxClipBoardAPI 
    
    
} // End Namespace TestMe

AsyncEx is required in the abstract class for synchronizing in the get/set property. AsyncEx not required for the actual clipboard handling, as long as you don't want to utilize the get/set clipboard contents in a synchronous context.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionMatt ThomasView Question on Stackoverflow
Solution 1 - C#SimonView Answer on Stackoverflow
Solution 2 - C#equimanView Answer on Stackoverflow
Solution 3 - C#Markus DoerigView Answer on Stackoverflow
Solution 4 - C#Ulf KristiansenView Answer on Stackoverflow
Solution 5 - C#Matt ThomasView Answer on Stackoverflow
Solution 6 - C#Stefan SteigerView Answer on Stackoverflow