Skip to content

Commit

Permalink
implement cross platform clipboards
Browse files Browse the repository at this point in the history
  • Loading branch information
tomlm committed Jan 9, 2025
1 parent 56c8c1c commit 2d23c1e
Show file tree
Hide file tree
Showing 9 changed files with 540 additions and 31 deletions.
25 changes: 0 additions & 25 deletions src/Consolonia.Core/Infrastructure/ConsoloniaPlatform.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,31 +66,6 @@ public void Initialize()
//.Bind<ISystemDialogImpl>().ToConstant(new GtkSystemDialog())
/*.Bind<IMountedVolumeInfoProvider>().ToConstant(new LinuxMountedVolumeInfoProvider())*/;

if (OperatingSystem.IsWindows())
{
AvaloniaLocator.CurrentMutable.Bind<IClipboard>()
.ToFunc(() =>
{
Assembly assembly = Assembly.Load("Avalonia.Win32");
ArgumentNullException.ThrowIfNull(assembly, "Avalonia.Win32");
Type type = assembly.GetType(assembly.GetName().Name + ".ClipboardImpl");
ArgumentNullException.ThrowIfNull(type, "ClipboardImpl");
var clipboard = Activator.CreateInstance(type) as IClipboard;
ArgumentNullException.ThrowIfNull(clipboard, nameof(clipboard));
return clipboard;
});
}
else if (OperatingSystem.IsMacOS())
{
// TODO: Implement or reuse MacOS clipboard
AvaloniaLocator.CurrentMutable.Bind<IClipboard>().ToConstant(new NaiveClipboard());
}
else if (OperatingSystem.IsLinux())
{
// TODO: Implement or reuse X11 clipboard
AvaloniaLocator.CurrentMutable.Bind<IClipboard>().ToConstant(new NaiveClipboard());
}

}

[DebuggerStepThrough]
Expand Down
15 changes: 10 additions & 5 deletions src/Consolonia.Core/Infrastructure/NaiveClipboard.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,39 +5,44 @@

namespace Consolonia.Core.Infrastructure
{
// TODO: Replace this with avalonia platform implementation
internal class NaiveClipboard : IClipboard
/// <summary>
/// This clipboard only stores memory in the same process, so it is not useful for sharing data between processes.
/// </summary>
public class NaiveClipboard : IClipboard
{
private string _text = String.Empty;

#pragma warning disable CA1822 // Mark members as static
public async Task ClearAsync()
{
await Task.CompletedTask;
_text = String.Empty;
}

public async Task<object> GetDataAsync(string format)
public Task<object> GetDataAsync(string format)
{
throw new NotImplementedException();
}

public async Task<string[]> GetFormatsAsync()
public Task<string[]> GetFormatsAsync()
{
throw new NotImplementedException();
}

public async Task<string> GetTextAsync()
{
await Task.CompletedTask;
return _text;
}

public async Task SetDataObjectAsync(IDataObject data)
public Task SetDataObjectAsync(IDataObject data)
{
throw new NotImplementedException();
}

public async Task SetTextAsync(string text)
{
await Task.CompletedTask;
_text = text;
}
}
Expand Down
114 changes: 114 additions & 0 deletions src/Consolonia.PlatformSupport/Clipboard/ClipboardProcessRunner.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Consolonia.PlatformSupport.Clipboard
{
/// <summary>Provides cut, copy, and paste support for the OS clipboard.</summary>
/// <remarks>
/// <para>On Windows, the <see cref="Clipboard"/> class uses the Windows Clipboard APIs via P/Invoke.</para>
/// <para>
/// On Linux, when not running under Windows Subsystem for Linux (WSL), the <see cref="Clipboard"/> class uses
/// the xclip command line tool. If xclip is not installed, the clipboard will not work.
/// </para>
/// <para>
/// On Linux, when running under Windows Subsystem for Linux (WSL), the <see cref="Clipboard"/> class launches
/// Windows' powershell.exe via WSL interop and uses the "Set-Clipboard" and "Get-Clipboard" Powershell CmdLets.
/// </para>
/// <para>
/// On the Mac, the <see cref="Clipboard"/> class uses the MacO OS X pbcopy and pbpaste command line tools and
/// the Mac clipboard APIs vai P/Invoke.
/// </para>
/// </remarks>

/// <summary>
/// Helper class for console drivers to invoke shell commands to interact with the clipboard.
/// </summary>
internal static class ClipboardProcessRunner
{
public static (int exitCode, string result) Bash(
string commandLine,
string inputText = "",
bool waitForOutput = false
)
{
var arguments = $"-c \"{commandLine}\"";
(int exitCode, string result) = Process("bash", arguments, inputText, waitForOutput);

return (exitCode, result.TrimEnd());
}

public static bool DoubleWaitForExit(this Process process)
{
bool result = process.WaitForExit(500);

if (result)
{
process.WaitForExit();
}

Check warning on line 51 in src/Consolonia.PlatformSupport/Clipboard/ClipboardProcessRunner.cs

View workflow job for this annotation

GitHub Actions / build

"[CA1307] 'string.Contains(string)' has a method overload that takes a 'StringComparison' parameter. Replace this call in 'Consolonia.PlatformSupport.Clipboard.ClipboardProcessRunner.FileExists(string)' with a call to 'string.Contains(string, System.StringComparison)' for clarity of intent." on /home/runner/work/Consolonia/Consolonia/src/Consolonia.PlatformSupport/Clipboard/ClipboardProcessRunner.cs(51,53)

return result;
}

public static bool FileExists(this string value) { return !string.IsNullOrEmpty(value) && !value.Contains("not found"); }

Check failure on line 56 in src/Consolonia.PlatformSupport/Clipboard/ClipboardProcessRunner.cs

View workflow job for this annotation

GitHub Actions / build

'string.Contains(string)' has a method overload that takes a 'StringComparison' parameter. Replace this call in 'Consolonia.PlatformSupport.Clipboard.ClipboardProcessRunner.FileExists(string)' with a call to 'string.Contains(string, System.StringComparison)' for clarity of intent. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1307)

Check failure on line 56 in src/Consolonia.PlatformSupport/Clipboard/ClipboardProcessRunner.cs

View workflow job for this annotation

GitHub Actions / build

'string.Contains(string)' has a method overload that takes a 'StringComparison' parameter. Replace this call in 'Consolonia.PlatformSupport.Clipboard.ClipboardProcessRunner.FileExists(string)' with a call to 'string.Contains(string, System.StringComparison)' for clarity of intent. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1307)

public static (int exitCode, string result) Process(
string cmd,
string arguments,
string input = null,
bool waitForOutput = true
)
{
var output = string.Empty;

using (var process = new Process
{
StartInfo = new()
{
FileName = cmd,
Arguments = arguments,
RedirectStandardOutput = true,
RedirectStandardError = true,
RedirectStandardInput = true,
UseShellExecute = false,
CreateNoWindow = true

Check warning on line 77 in src/Consolonia.PlatformSupport/Clipboard/ClipboardProcessRunner.cs

View workflow job for this annotation

GitHub Actions / build

"[UnusedVariable] Local variable 'eventHandled' is never used" on /home/runner/work/Consolonia/Consolonia/src/Consolonia.PlatformSupport/Clipboard/ClipboardProcessRunner.cs(77,44)
}
})
{
TaskCompletionSource<bool> eventHandled = new();
process.Start();

if (!string.IsNullOrEmpty(input))
{
process.StandardInput.Write(input);
process.StandardInput.Close();
}

if (!process.WaitForExit(5000))
{
var timeoutError =
$@"Process timed out. Command line: {process.StartInfo.FileName} {process.StartInfo.Arguments}.";

throw new TimeoutException(timeoutError);
}

if (waitForOutput && process.StandardOutput.Peek() != -1)
{
output = process.StandardOutput.ReadToEnd();
}

if (process.ExitCode > 0)
{
output = $@"Process failed to run. Command line: {cmd} {arguments}.
Output: {output}
Error: {process.StandardError.ReadToEnd()}";
}

return (process.ExitCode, output);
}
}
}
}
130 changes: 130 additions & 0 deletions src/Consolonia.PlatformSupport/Clipboard/MacOSXClipboard.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Avalonia.Input;
using Avalonia.Input.Platform;

namespace Consolonia.PlatformSupport.Clipboard
{

/// <summary>
/// A clipboard implementation for MacOSX. This implementation uses the Mac clipboard API (via P/Invoke) to
/// copy/paste. The existence of the Mac pbcopy and pbpaste commands is used to determine if copy/paste is supported.
/// </summary>
internal class MacOSXClipboard : IClipboard

Check warning on line 13 in src/Consolonia.PlatformSupport/Clipboard/MacOSXClipboard.cs

View workflow job for this annotation

GitHub Actions / build

"[InconsistentNaming] Name 'MacOSXClipboard' does not match rule 'Types and namespaces'. Suggested name is 'MacOsxClipboard'." on /home/runner/work/Consolonia/Consolonia/src/Consolonia.PlatformSupport/Clipboard/MacOSXClipboard.cs(13,20)
{
private readonly nint _allocRegister = sel_registerName("alloc");
private readonly nint _clearContentsRegister = sel_registerName("clearContents");
private readonly nint _generalPasteboard;
private readonly nint _generalPasteboardRegister = sel_registerName("generalPasteboard");
private readonly nint _initWithUtf8Register = sel_registerName("initWithUTF8String:");
private readonly nint _nsPasteboard = objc_getClass("NSPasteboard");
private readonly nint _nsString = objc_getClass("NSString");
private readonly nint _nsStringPboardType;
private readonly nint _setStringRegister = sel_registerName("setString:forType:");
private readonly nint _stringForTypeRegister = sel_registerName("stringForType:");
private readonly nint _utf8Register = sel_registerName("UTF8String");
private readonly nint _utfTextType;

public MacOSXClipboard()
{
_utfTextType = objc_msgSend(
objc_msgSend(_nsString, _allocRegister),
_initWithUtf8Register,
"public.utf8-plain-text"
);

_nsStringPboardType = objc_msgSend(
objc_msgSend(_nsString, _allocRegister),
_initWithUtf8Register,
"NSStringPboardType"
);
_generalPasteboard = objc_msgSend(_nsPasteboard, _generalPasteboardRegister);
if (!CheckSupport())
throw new System.Exception("clipboard operations are not supported pbcopy and pbpaste are not available on this system.");

Check failure on line 43 in src/Consolonia.PlatformSupport/Clipboard/MacOSXClipboard.cs

View workflow job for this annotation

GitHub Actions / build

Exception type System.Exception is not sufficiently specific (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2201)

Check failure on line 43 in src/Consolonia.PlatformSupport/Clipboard/MacOSXClipboard.cs

View workflow job for this annotation

GitHub Actions / build

Exception type System.Exception is not sufficiently specific (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2201)

Check warning on line 43 in src/Consolonia.PlatformSupport/Clipboard/MacOSXClipboard.cs

View workflow job for this annotation

GitHub Actions / build

"[CA2201] Exception type System.Exception is not sufficiently specific" on /home/runner/work/Consolonia/Consolonia/src/Consolonia.PlatformSupport/Clipboard/MacOSXClipboard.cs(43,23)
}

private static bool CheckSupport()
{
(int exitCode, string result) = ClipboardProcessRunner.Bash("which pbcopy", waitForOutput: true);

if (exitCode != 0 || !result.FileExists())
{
return false;
}

(exitCode, result) = ClipboardProcessRunner.Bash("which pbpaste", waitForOutput: true);

return exitCode == 0 && result.FileExists();
}

[DllImport("/System/Library/Frameworks/AppKit.framework/AppKit", CharSet = CharSet.Unicode)]
private static extern nint objc_getClass(string className);

[DllImport("/System/Library/Frameworks/AppKit.framework/AppKit")]
private static extern nint objc_msgSend(nint receiver, nint selector);

[DllImport("/System/Library/Frameworks/AppKit.framework/AppKit", CharSet = CharSet.Unicode)]
private static extern nint objc_msgSend(nint receiver, nint selector, string arg1);

[DllImport("/System/Library/Frameworks/AppKit.framework/AppKit")]
private static extern nint objc_msgSend(nint receiver, nint selector, nint arg1);

[DllImport("/System/Library/Frameworks/AppKit.framework/AppKit")]
private static extern nint objc_msgSend(nint receiver, nint selector, nint arg1, nint arg2);

[DllImport("/System/Library/Frameworks/AppKit.framework/AppKit", CharSet = CharSet.Unicode)]
private static extern nint sel_registerName(string selectorName);

public async Task<string> GetTextAsync()
{
await Task.CompletedTask;

nint ptr = objc_msgSend(_generalPasteboard, _stringForTypeRegister, _nsStringPboardType);
nint charArray = objc_msgSend(ptr, _utf8Register);

return Marshal.PtrToStringAnsi(charArray);
}

public async Task SetTextAsync(string text)
{
await Task.CompletedTask;

nint str = default;

try
{
str = objc_msgSend(objc_msgSend(_nsString, _allocRegister), _initWithUtf8Register, text);
objc_msgSend(_generalPasteboard, _clearContentsRegister);
objc_msgSend(_generalPasteboard, _setStringRegister, str, _utfTextType);
}
finally
{
if (str != default(nint))
{
objc_msgSend(str, sel_registerName("release"));
}
}
}

public Task ClearAsync()
{
return SetTextAsync(string.Empty);
}

public Task SetDataObjectAsync(IDataObject data)
{
throw new System.NotImplementedException();
}

public Task<string[]> GetFormatsAsync()
{
throw new System.NotImplementedException();
}

public Task<object> GetDataAsync(string format)
{
throw new System.NotImplementedException();
}
}

}
Loading

0 comments on commit 2d23c1e

Please sign in to comment.