From 67d75725ccbdc716009919b3647573fff1f7556c Mon Sep 17 00:00:00 2001 From: BDisp Date: Sat, 22 May 2021 22:37:04 +0100 Subject: [PATCH 01/10] Fixes #1318. Ensures that the OS clipboard is always sets. --- .../CursesDriver/CursesDriver.cs | 96 ++++++++----- UnitTests/ClipboardTests.cs | 135 +++++++++++++++++- 2 files changed, 195 insertions(+), 36 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs b/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs index df5c3871f..d60099069 100644 --- a/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs @@ -1062,7 +1062,7 @@ namespace Terminal.Gui { var tempFileName = System.IO.Path.GetTempFileName (); try { // BashRunner.Run ($"xsel -o --clipboard > {tempFileName}"); - BashRunner.Run ($"xclip -o > {tempFileName}"); + BashRunner.Run ($"xclip -selection clipboard -o > {tempFileName}"); return System.IO.File.ReadAllText (tempFileName); } finally { System.IO.File.Delete (tempFileName); @@ -1071,52 +1071,78 @@ namespace Terminal.Gui { public void SetClipboardData (string text) { - var tempFileName = System.IO.Path.GetTempFileName (); - System.IO.File.WriteAllText (tempFileName, text); + // var tempFileName = System.IO.Path.GetTempFileName (); + // System.IO.File.WriteAllText (tempFileName, text); + // try { + // // BashRunner.Run ($"cat {tempFileName} | xsel -i --clipboard"); + // BashRunner.Run ($"cat {tempFileName} | xclip -selection clipboard"); + // } finally { + // System.IO.File.Delete (tempFileName); + // } + try { - // BashRunner.Run ($"cat {tempFileName} | xsel -i --clipboard"); - BashRunner.Run ($"cat {tempFileName} | xclip -selection clipboard"); - } finally { - System.IO.File.Delete (tempFileName); + BashRunner.Run ("xclip -selection clipboard -i", false, text); + } catch (Exception ex) { + throw new Exception (ex.Message); } } } static class BashRunner { - public static string Run (string commandLine) + public static string Run (string commandLine, bool output = true, string inputText = "") { - var errorBuilder = new System.Text.StringBuilder (); - var outputBuilder = new System.Text.StringBuilder (); var arguments = $"-c \"{commandLine}\""; - using (var process = new System.Diagnostics.Process { - StartInfo = new System.Diagnostics.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.DoubleWaitForExit ()) { - var timeoutError = $@"Process timed out. Command line: bash {arguments}. + + if (output) { + var errorBuilder = new System.Text.StringBuilder (); + var outputBuilder = new System.Text.StringBuilder (); + + using (var process = new System.Diagnostics.Process { + StartInfo = new System.Diagnostics.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.DoubleWaitForExit ()) { + 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 (timeoutError); + throw new Exception (error); } - if (process.ExitCode == 0) { - return outputBuilder.ToString (); + } else { + using (var process = new System.Diagnostics.Process { + StartInfo = new System.Diagnostics.ProcessStartInfo { + FileName = "bash", + Arguments = arguments, + RedirectStandardInput = true, + UseShellExecute = false, + CreateNoWindow = false + } + }) { + process.Start (); + process.StandardInput.Write (inputText); + process.StandardInput.Close (); + process.WaitForExit (); + return inputText; } - - var error = $@"Could not execute process. Command line: bash {arguments}. - Output: {outputBuilder} - Error: {errorBuilder}"; - throw new Exception (error); } } diff --git a/UnitTests/ClipboardTests.cs b/UnitTests/ClipboardTests.cs index 3ee4fc3c9..13cb097d1 100644 --- a/UnitTests/ClipboardTests.cs +++ b/UnitTests/ClipboardTests.cs @@ -1,4 +1,6 @@ -using Xunit; +using System.Diagnostics; +using System.Runtime.InteropServices; +using Xunit; namespace Terminal.Gui.Core { public class ClipboardTests { @@ -13,5 +15,136 @@ namespace Terminal.Gui.Core { Application.Shutdown (); } + + [Fact] + public void Contents_Gets_From_OS_Clipboard () + { + Application.Init (new FakeDriver (), new FakeMainLoop (() => FakeConsole.ReadKey (true))); + + var clipText = "This is a clipboard unit test to get clipboard from OS."; + + Application.Iteration += () => { + if (RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) { + // using (Process clipExe = new Process { + // StartInfo = new ProcessStartInfo { + // RedirectStandardInput = true, + // FileName = "clip" + // } + // }) { + // clipExe.Start (); + // clipExe.StandardInput.Write (clipText); + // clipExe.StandardInput.Close (); + // var result = clipExe.WaitForExit (500); + // if (result) { + // clipExe.WaitForExit (); + // } + // } + + using (Process pwsh = new Process { + StartInfo = new ProcessStartInfo { + FileName = "powershell", + Arguments = $"-command \"Set-Clipboard -Value \\\"{clipText}\\\"\"" + } + }) { + pwsh.Start (); + pwsh.WaitForExit (); + } + } else if (RuntimeInformation.IsOSPlatform (OSPlatform.OSX)) { + using (Process copy = new Process { + StartInfo = new ProcessStartInfo { + RedirectStandardInput = true, + FileName = "pbcopy" + } + }) { + copy.Start (); + copy.StandardInput.Write (clipText); + copy.StandardInput.Close (); + copy.WaitForExit (); + } + } else if (RuntimeInformation.IsOSPlatform (OSPlatform.Linux)) { + using (Process bash = new Process { + StartInfo = new ProcessStartInfo { + FileName = "bash", + Arguments = $"-c \"xclip -sel clip -i\"", + RedirectStandardInput = true, + } + }) { + bash.Start (); + bash.StandardInput.Write (clipText); + bash.StandardInput.Close (); + bash.WaitForExit (); + } + } + + Application.RequestStop (); + }; + + Application.Run (); + + Assert.Equal (clipText, Clipboard.Contents); + + Application.Shutdown (); + } + + [Fact] + public void Contents_Sets_The_OS_Clipboard () + { + Application.Init (new FakeDriver (), new FakeMainLoop (() => FakeConsole.ReadKey (true))); + + var clipText = "This is a clipboard unit test to set the OS clipboard."; + var clipReadText = ""; + + Application.Iteration += () => { + Clipboard.Contents = clipText; + + if (RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) { + using (Process pwsh = new Process { + StartInfo = new ProcessStartInfo { + RedirectStandardOutput = true, + FileName = "powershell.exe", + Arguments = "-command \"Get-Clipboard\"" + } + }) { + pwsh.Start (); + clipReadText = pwsh.StandardOutput.ReadToEnd ().TrimEnd (); + pwsh.StandardOutput.Close (); + pwsh.WaitForExit (); + } + } else if (RuntimeInformation.IsOSPlatform (OSPlatform.OSX)) { + using (Process paste = new Process { + StartInfo = new ProcessStartInfo { + RedirectStandardOutput = true, + FileName = "pbpaste" + } + }) { + paste.Start (); + clipReadText = paste.StandardOutput.ReadToEnd (); + paste.StandardOutput.Close (); + paste.WaitForExit (); + } + } else if (RuntimeInformation.IsOSPlatform (OSPlatform.Linux)) { + using (Process bash = new Process { + StartInfo = new ProcessStartInfo { + RedirectStandardOutput = true, + FileName = "bash", + Arguments = $"-c \"xclip -sel clip -o\"" + } + }) { + bash.Start (); + clipReadText = bash.StandardOutput.ReadToEnd (); + bash.StandardOutput.Close (); + bash.WaitForExit (); + } + } + + Application.RequestStop (); + }; + + Application.Run (); + + Assert.Equal (clipText, clipReadText); + + Application.Shutdown (); + } } } From 8bdbb12c8a5f90a4cc10f7e33a436448cffacab3 Mon Sep 17 00:00:00 2001 From: BDisp Date: Sun, 23 May 2021 00:47:28 +0100 Subject: [PATCH 02/10] Check if xclip is installed. --- UnitTests/ClipboardTests.cs | 40 +++++++++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/UnitTests/ClipboardTests.cs b/UnitTests/ClipboardTests.cs index 13cb097d1..ebd29fb75 100644 --- a/UnitTests/ClipboardTests.cs +++ b/UnitTests/ClipboardTests.cs @@ -22,6 +22,7 @@ namespace Terminal.Gui.Core { Application.Init (new FakeDriver (), new FakeMainLoop (() => FakeConsole.ReadKey (true))); var clipText = "This is a clipboard unit test to get clipboard from OS."; + var exit = false; Application.Iteration += () => { if (RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) { @@ -62,6 +63,11 @@ namespace Terminal.Gui.Core { copy.WaitForExit (); } } else if (RuntimeInformation.IsOSPlatform (OSPlatform.Linux)) { + if (exit = xclipExists () == false) { + // xclip doesn't exist then exit. + Application.RequestStop (); + } + using (Process bash = new Process { StartInfo = new ProcessStartInfo { FileName = "bash", @@ -81,7 +87,9 @@ namespace Terminal.Gui.Core { Application.Run (); - Assert.Equal (clipText, Clipboard.Contents); + if (!exit) { + Assert.Equal (clipText, Clipboard.Contents); + } Application.Shutdown (); } @@ -93,6 +101,7 @@ namespace Terminal.Gui.Core { var clipText = "This is a clipboard unit test to set the OS clipboard."; var clipReadText = ""; + var exit = false; Application.Iteration += () => { Clipboard.Contents = clipText; @@ -123,6 +132,11 @@ namespace Terminal.Gui.Core { paste.WaitForExit (); } } else if (RuntimeInformation.IsOSPlatform (OSPlatform.Linux)) { + if (exit = xclipExists () == false) { + // xclip doesn't exist then exit. + Application.RequestStop (); + } + using (Process bash = new Process { StartInfo = new ProcessStartInfo { RedirectStandardOutput = true, @@ -142,9 +156,31 @@ namespace Terminal.Gui.Core { Application.Run (); - Assert.Equal (clipText, clipReadText); + if (!exit) { + Assert.Equal (clipText, clipReadText); + } Application.Shutdown (); } + + bool xclipExists () + { + using (Process bash = new Process { + StartInfo = new ProcessStartInfo { + FileName = "bash", + Arguments = $"-c \"which xclip\"", + RedirectStandardOutput = true, + } + }) { + bash.Start (); + bool exist = bash.StandardOutput.ReadToEnd ().TrimEnd () != ""; + bash.StandardOutput.Close (); + bash.WaitForExit (); + if (exist) { + return true; + } + } + return false; + } } } From 87271cc477509e1fbd7b7b6b0bf6f57a8b90971a Mon Sep 17 00:00:00 2001 From: BDisp Date: Tue, 25 May 2021 18:49:30 +0100 Subject: [PATCH 03/10] Implemented the ClipboardBase abstract class as suggested by @tznind. --- .../CursesDriver/CursesDriver.cs | 107 +++++++++++++++-- .../ConsoleDrivers/FakeDriver/FakeDriver.cs | 6 +- Terminal.Gui/ConsoleDrivers/NetDriver.cs | 6 +- Terminal.Gui/ConsoleDrivers/WindowsDriver.cs | 12 +- Terminal.Gui/Core/Clipboard.cs | 32 ------ Terminal.Gui/Core/Clipboard/Clipboard.cs | 67 +++++++++++ Terminal.Gui/Core/Clipboard/ClipboardBase.cs | 100 ++++++++++++++++ Terminal.Gui/Core/Clipboard/IClipboard.cs | 44 +++++++ Terminal.Gui/Core/ConsoleDriver.cs | 16 --- UnitTests/ClipboardTests.cs | 108 ++++++++++++++++++ 10 files changed, 435 insertions(+), 63 deletions(-) delete mode 100644 Terminal.Gui/Core/Clipboard.cs create mode 100644 Terminal.Gui/Core/Clipboard/Clipboard.cs create mode 100644 Terminal.Gui/Core/Clipboard/ClipboardBase.cs create mode 100644 Terminal.Gui/Core/Clipboard/IClipboard.cs diff --git a/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs b/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs index d60099069..f5b32b092 100644 --- a/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs @@ -751,7 +751,11 @@ namespace Terminal.Gui { if (RuntimeInformation.IsOSPlatform (OSPlatform.OSX)) { clipboard = new MacOSXClipboard (); } else { - clipboard = new CursesClipboard (); + if (Is_WSL_Platform ()) { + clipboard = new WSLClipboard (); + } else { + clipboard = new CursesClipboard (); + } } Curses.raw (); @@ -844,6 +848,15 @@ namespace Terminal.Gui { } } + public static bool Is_WSL_Platform () + { + var result = BashRunner.Run ("uname -a"); + if (result.Contains ("microsoft") && result.Contains ("WSL")) { + return true; + } + return false; + } + static int MapColor (Color color) { switch (color) { @@ -1056,8 +1069,19 @@ namespace Terminal.Gui { } } - class CursesClipboard : IClipboard { - public string GetClipboardData () + class CursesClipboard : ClipboardBase { + public override bool IsSupported => CheckSupport (); + + bool CheckSupport () + { + var result = BashRunner.Run ("which xclip"); + if (string.IsNullOrEmpty (result) || result.Contains ("not found")) { + return false; + } + return true; + } + + protected override string GetClipboardDataImpl () { var tempFileName = System.IO.Path.GetTempFileName (); try { @@ -1069,7 +1093,7 @@ namespace Terminal.Gui { } } - public void SetClipboardData (string text) + protected override void SetClipboardDataImpl (string text) { // var tempFileName = System.IO.Path.GetTempFileName (); // System.IO.File.WriteAllText (tempFileName, text); @@ -1083,7 +1107,7 @@ namespace Terminal.Gui { try { BashRunner.Run ("xclip -selection clipboard -i", false, text); } catch (Exception ex) { - throw new Exception (ex.Message); + throw new NotSupportedException ("Write to clipboard failed", ex); } } } @@ -1156,7 +1180,7 @@ namespace Terminal.Gui { } } - class MacOSXClipboard : IClipboard { + class MacOSXClipboard : ClipboardBase { IntPtr nsString = objc_getClass ("NSString"); IntPtr nsPasteboard = objc_getClass ("NSPasteboard"); IntPtr utfTextType; @@ -1170,6 +1194,30 @@ namespace Terminal.Gui { IntPtr generalPasteboardRegister = sel_registerName ("generalPasteboard"); IntPtr clearContentsRegister = sel_registerName ("clearContents"); + public override bool IsSupported => CheckSupport (); + + bool CheckSupport () + { + var result = BashRunner.Run ("which pbcopy"); + if (!FileExists (result)) { + return false; + } + result = BashRunner.Run ("which pbpaste"); + if (!FileExists (result)) { + return false; + } + + bool FileExists (string value) + { + if (string.IsNullOrEmpty (value) || value.Contains ("not found")) { + return false; + } + return true; + } + + return true; + } + public MacOSXClipboard () { utfTextType = objc_msgSend (objc_msgSend (nsString, allocRegister), initWithUtf8Register, "public.utf8-plain-text"); @@ -1177,14 +1225,14 @@ namespace Terminal.Gui { generalPasteboard = objc_msgSend (nsPasteboard, generalPasteboardRegister); } - public string GetClipboardData () + protected override string GetClipboardDataImpl () { var ptr = objc_msgSend (generalPasteboard, stringForTypeRegister, nsStringPboardType); var charArray = objc_msgSend (ptr, utf8Register); return Marshal.PtrToStringAnsi (charArray); } - public void SetClipboardData (string text) + protected override void SetClipboardDataImpl (string text) { IntPtr str = default; try { @@ -1216,4 +1264,47 @@ namespace Terminal.Gui { [DllImport ("/System/Library/Frameworks/AppKit.framework/AppKit")] static extern IntPtr sel_registerName (string selectorName); } + + class WSLClipboard : ClipboardBase { + public override bool IsSupported => CheckSupport (); + + bool CheckSupport () + { + var result = BashRunner.Run ("which powershell.exe"); + if (string.IsNullOrEmpty (result) || result.Contains ("not found")) { + return false; + } + return true; + } + + protected override string GetClipboardDataImpl () + { + using (var powershell = new System.Diagnostics.Process { + StartInfo = new System.Diagnostics.ProcessStartInfo { + RedirectStandardOutput = true, + FileName = "powershell.exe", + Arguments = "-command \"Get-Clipboard\"" + } + }) { + powershell.Start (); + var result = powershell.StandardOutput.ReadToEnd (); + powershell.StandardOutput.Close (); + powershell.WaitForExit (); + return result.TrimEnd (); + } + } + + protected override void SetClipboardDataImpl (string text) + { + using (var powershell = new System.Diagnostics.Process { + StartInfo = new System.Diagnostics.ProcessStartInfo { + FileName = "powershell.exe", + Arguments = $"-command \"Set-Clipboard -Value \\\"{text}\\\"\"" + } + }) { + powershell.Start (); + powershell.WaitForExit (); + } + } + } } diff --git a/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs b/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs index 246f5af5d..0aa48539f 100644 --- a/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs @@ -64,7 +64,11 @@ namespace Terminal.Gui { } else if (RuntimeInformation.IsOSPlatform (OSPlatform.OSX)) { Clipboard = new MacOSXClipboard (); } else { - Clipboard = new CursesClipboard (); + if (CursesDriver.Is_WSL_Platform ()) { + Clipboard = new WSLClipboard (); + } else { + Clipboard = new CursesClipboard (); + } } } diff --git a/Terminal.Gui/ConsoleDrivers/NetDriver.cs b/Terminal.Gui/ConsoleDrivers/NetDriver.cs index 67fc40dd8..ff179c240 100644 --- a/Terminal.Gui/ConsoleDrivers/NetDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/NetDriver.cs @@ -1100,7 +1100,11 @@ namespace Terminal.Gui { } else if (RuntimeInformation.IsOSPlatform (OSPlatform.OSX)) { Clipboard = new MacOSXClipboard (); } else { - Clipboard = new CursesClipboard (); + if (CursesDriver.Is_WSL_Platform ()) { + Clipboard = new WSLClipboard (); + }else{ + Clipboard = new CursesClipboard (); + } } } diff --git a/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs b/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs index bddf89f14..c08a18768 100644 --- a/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs @@ -1640,11 +1640,13 @@ namespace Terminal.Gui { } } - class WindowsClipboard : IClipboard { - public string GetClipboardData () + class WindowsClipboard : ClipboardBase { + public override bool IsSupported => IsClipboardFormatAvailable (cfUnicodeText) ? true : false; + + protected override string GetClipboardDataImpl () { - if (!IsClipboardFormatAvailable (cfUnicodeText)) - return null; + //if (!IsClipboardFormatAvailable (cfUnicodeText)) + // return null; try { if (!OpenClipboard (IntPtr.Zero)) @@ -1678,7 +1680,7 @@ namespace Terminal.Gui { } } - public void SetClipboardData (string text) + protected override void SetClipboardDataImpl (string text) { OpenClipboard (); diff --git a/Terminal.Gui/Core/Clipboard.cs b/Terminal.Gui/Core/Clipboard.cs deleted file mode 100644 index 5e029fe29..000000000 --- a/Terminal.Gui/Core/Clipboard.cs +++ /dev/null @@ -1,32 +0,0 @@ -using NStack; -using System; - -namespace Terminal.Gui { - /// - /// Provides cut, copy, and paste support for the clipboard with OS interaction. - /// - public static class Clipboard { - static ustring contents; - - /// - /// Get or sets the operation system clipboard, otherwise the contents field. - /// - public static ustring Contents { - get { - try { - return Application.Driver.Clipboard.GetClipboardData (); - } catch (Exception) { - return contents; - } - } - set { - try { - Application.Driver.Clipboard.SetClipboardData (value.ToString ()); - contents = value; - } catch (Exception) { - contents = value; - } - } - } - } -} diff --git a/Terminal.Gui/Core/Clipboard/Clipboard.cs b/Terminal.Gui/Core/Clipboard/Clipboard.cs new file mode 100644 index 000000000..e7d570074 --- /dev/null +++ b/Terminal.Gui/Core/Clipboard/Clipboard.cs @@ -0,0 +1,67 @@ +using NStack; +using System; + +namespace Terminal.Gui { + /// + /// Provides cut, copy, and paste support for the clipboard with OS interaction. + /// + public static class Clipboard { + static ustring contents; + + /// + /// Get or sets the operation system clipboard, otherwise the contents field. + /// + public static ustring Contents { + get { + try { + return Application.Driver.Clipboard.GetClipboardData (); + } catch (Exception) { + return contents; + } + } + set { + try { + Application.Driver.Clipboard.SetClipboardData (value.ToString ()); + contents = value; + } catch (Exception) { + contents = value; + } + } + } + + /// + /// Returns true if the environmental dependencies are in place to interact with the OS clipboard. + /// + public static bool IsSupported { get; } = Application.Driver.Clipboard.IsSupported; + + /// + /// Gets the operation system clipboard if possible. + /// + /// Clipboard contents read + /// true if it was possible to read the OS clipboard. + public static bool TryGetClipboardData (out string result) + { + if (Application.Driver.Clipboard.TryGetClipboardData (out result)) { + if (contents != result) { + contents = result; + } + return true; + } + return false; + } + + /// + /// Sets the operation system clipboard if possible. + /// + /// + /// True if the clipboard content was set successfully. + public static bool TrySetClipboardData (string text) + { + if (Application.Driver.Clipboard.TrySetClipboardData (text)) { + contents = text; + return true; + } + return false; + } + } +} diff --git a/Terminal.Gui/Core/Clipboard/ClipboardBase.cs b/Terminal.Gui/Core/Clipboard/ClipboardBase.cs new file mode 100644 index 000000000..db61af80f --- /dev/null +++ b/Terminal.Gui/Core/Clipboard/ClipboardBase.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Terminal.Gui { + /// + /// Shared abstract class to enforce rules from the implementation of the interface. + /// + public abstract class ClipboardBase : IClipboard { + /// + /// Returns true if the environmental dependencies are in place to interact with the OS clipboard + /// + public abstract bool IsSupported { get; } + + /// + /// Get the operation system clipboard. + /// + /// Thrown if it was not possible to read the clipboard contents + public string GetClipboardData () + { + try { + return GetClipboardDataImpl (); + } catch (Exception ex) { + throw new NotSupportedException ("Failed to read clipboard.", ex); + } + } + + /// + /// Get the operation system clipboard. + /// + protected abstract string GetClipboardDataImpl (); + + /// + /// Sets the operation system clipboard. + /// + /// + /// Thrown if it was not possible to set the clipboard contents + public void SetClipboardData (string text) + { + try { + SetClipboardDataImpl (text); + } catch (Exception ex) { + throw new NotSupportedException ("Failed to write to clipboard.", ex); + } + } + + /// + /// Sets the operation system clipboard. + /// + /// + protected abstract void SetClipboardDataImpl (string text); + + /// + /// Gets the operation system clipboard if possible. + /// + /// Clipboard contents read + /// true if it was possible to read the OS clipboard. + public bool TryGetClipboardData (out string result) + { + // Don't even try to read because environment is not set up. + if (!IsSupported) { + result = null; + return false; + } + + try { + result = GetClipboardDataImpl (); + while (result == null) { + result = GetClipboardDataImpl (); + } + return true; + } catch (Exception) { + result = null; + return false; + } + } + + /// + /// Sets the operation system clipboard if possible. + /// + /// + /// True if the clipboard content was set successfully + public bool TrySetClipboardData (string text) + { + // Don't even try to set because environment is not set up + if (!IsSupported) { + return false; + } + + try { + SetClipboardDataImpl (text); + return true; + } catch (Exception) { + return false; + } + } + } +} diff --git a/Terminal.Gui/Core/Clipboard/IClipboard.cs b/Terminal.Gui/Core/Clipboard/IClipboard.cs new file mode 100644 index 000000000..9619d8125 --- /dev/null +++ b/Terminal.Gui/Core/Clipboard/IClipboard.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Terminal.Gui { + /// + /// Definition to interact with the OS clipboard. + /// + public interface IClipboard { + /// + /// Returns true if the environmental dependencies are in place to interact with the OS clipboard. + /// + bool IsSupported { get; } + + /// + /// Get the operation system clipboard. + /// + /// Thrown if it was not possible to read the clipboard contents. + string GetClipboardData (); + + /// + /// Gets the operation system clipboard if possible. + /// + /// Clipboard contents read + /// true if it was possible to read the OS clipboard. + bool TryGetClipboardData (out string result); + + /// + /// Sets the operation system clipboard. + /// + /// + /// Thrown if it was not possible to set the clipboard contents. + void SetClipboardData (string text); + + /// + /// Sets the operation system clipboard if possible. + /// + /// + /// True if the clipboard content was set successfully. + bool TrySetClipboardData (string text); + } +} diff --git a/Terminal.Gui/Core/ConsoleDriver.cs b/Terminal.Gui/Core/ConsoleDriver.cs index 9f7594443..86fc62a6f 100644 --- a/Terminal.Gui/Core/ConsoleDriver.cs +++ b/Terminal.Gui/Core/ConsoleDriver.cs @@ -1178,20 +1178,4 @@ namespace Terminal.Gui { /// The current attribute. public abstract Attribute GetAttribute (); } - - /// - /// Definition to interact with the OS clipboard. - /// - public interface IClipboard { - /// - /// Sets the operation system clipboard. - /// - /// - void SetClipboardData (string text); - /// - /// Get the operation system clipboard. - /// - /// - string GetClipboardData (); - } } diff --git a/UnitTests/ClipboardTests.cs b/UnitTests/ClipboardTests.cs index ebd29fb75..bde15fdfb 100644 --- a/UnitTests/ClipboardTests.cs +++ b/UnitTests/ClipboardTests.cs @@ -11,6 +11,56 @@ namespace Terminal.Gui.Core { var clipText = "This is a clipboard unit test."; Clipboard.Contents = clipText; + + Application.Iteration += () => Application.RequestStop (); + + Application.Run (); + + Assert.Equal (clipText, Clipboard.Contents); + + Application.Shutdown (); + } + + [Fact] + public void IsSupported_Get () + { + Application.Init (new FakeDriver (), new FakeMainLoop (() => FakeConsole.ReadKey (true))); + + Assert.True (Clipboard.IsSupported); + + Application.Shutdown (); + } + + [Fact] + public void TryGetClipboardData_Gets_From_OS_Clipboard () + { + Application.Init (new FakeDriver (), new FakeMainLoop (() => FakeConsole.ReadKey (true))); + + var clipText = "Trying to get from the OS clipboard."; + Clipboard.Contents = clipText; + + Application.Iteration += () => Application.RequestStop (); + + Application.Run (); + + Assert.True (Clipboard.TryGetClipboardData (out string result)); + Assert.Equal (clipText, result); + + Application.Shutdown (); + } + + [Fact] + public void TrySetClipboardData_Sets_The_OS_Clipboard () + { + Application.Init (new FakeDriver (), new FakeMainLoop (() => FakeConsole.ReadKey (true))); + + var clipText = "Trying to set the OS clipboard."; + Assert.True (Clipboard.TrySetClipboardData (clipText)); + + Application.Iteration += () => Application.RequestStop (); + + Application.Run (); + Assert.Equal (clipText, Clipboard.Contents); Application.Shutdown (); @@ -63,9 +113,27 @@ namespace Terminal.Gui.Core { copy.WaitForExit (); } } else if (RuntimeInformation.IsOSPlatform (OSPlatform.Linux)) { + if (Is_WSL_Platform ()) { + try { + using (Process bash = new Process { + StartInfo = new ProcessStartInfo { + FileName = "powershell.exe", + Arguments = $"-command \"Set-Clipboard -Value \\\"{clipText}\\\"\"" + } + }) { + bash.Start (); + bash.WaitForExit (); + } + } catch { + exit = true; + } + Application.RequestStop (); + return; + } if (exit = xclipExists () == false) { // xclip doesn't exist then exit. Application.RequestStop (); + return; } using (Process bash = new Process { @@ -132,6 +200,25 @@ namespace Terminal.Gui.Core { paste.WaitForExit (); } } else if (RuntimeInformation.IsOSPlatform (OSPlatform.Linux)) { + if (Is_WSL_Platform ()) { + try { + using (Process bash = new Process { + StartInfo = new ProcessStartInfo { + RedirectStandardOutput = true, + FileName = "powershell.exe", + Arguments = "-command \"Get-Clipboard\"" + } + }) { + bash.Start (); + clipReadText = bash.StandardOutput.ReadToEnd (); + bash.StandardOutput.Close (); + bash.WaitForExit (); + } + } catch { + exit = true; + } + Application.RequestStop (); + } if (exit = xclipExists () == false) { // xclip doesn't exist then exit. Application.RequestStop (); @@ -163,6 +250,27 @@ namespace Terminal.Gui.Core { Application.Shutdown (); } + bool Is_WSL_Platform () + { + using (Process bash = new Process { + StartInfo = new ProcessStartInfo { + FileName = "bash", + Arguments = $"-c \"uname -a\"", + RedirectStandardOutput = true, + } + }) { + bash.Start (); + var result = bash.StandardOutput.ReadToEnd (); + var isWSL = false; + if (result.Contains ("microsoft") && result.Contains ("WSL")) { + isWSL = true; + } + bash.StandardOutput.Close (); + bash.WaitForExit (); + return isWSL; + } + } + bool xclipExists () { using (Process bash = new Process { From 286461e7c86e6c6fdb51f987210385eddca07d6b Mon Sep 17 00:00:00 2001 From: BDisp Date: Tue, 25 May 2021 19:09:33 +0100 Subject: [PATCH 04/10] Fixes bash permissions issue in Linux. --- .../ConsoleDrivers/CursesDriver/CursesDriver.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs b/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs index f5b32b092..b38cce903 100644 --- a/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs @@ -1074,11 +1074,16 @@ namespace Terminal.Gui { bool CheckSupport () { - var result = BashRunner.Run ("which xclip"); - if (string.IsNullOrEmpty (result) || result.Contains ("not found")) { + try { + var result = BashRunner.Run ("which xclip"); + if (string.IsNullOrEmpty (result) || result.Contains ("not found")) { + return false; + } + return true; + } catch (Exception) { + // Permissions issue. return false; } - return true; } protected override string GetClipboardDataImpl () From 1b7ead0e714c99d23a1ba23d098ed2a2b17d05c0 Mon Sep 17 00:00:00 2001 From: BDisp Date: Tue, 25 May 2021 19:19:14 +0100 Subject: [PATCH 05/10] Fixes more permissions issues. --- UnitTests/ClipboardTests.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/UnitTests/ClipboardTests.cs b/UnitTests/ClipboardTests.cs index bde15fdfb..424cde774 100644 --- a/UnitTests/ClipboardTests.cs +++ b/UnitTests/ClipboardTests.cs @@ -43,8 +43,13 @@ namespace Terminal.Gui.Core { Application.Run (); - Assert.True (Clipboard.TryGetClipboardData (out string result)); - Assert.Equal (clipText, result); + if (Clipboard.IsSupported) { + Assert.True (Clipboard.TryGetClipboardData (out string result)); + Assert.Equal (clipText, result); + } else { + Assert.False (Clipboard.TryGetClipboardData (out string result)); + Assert.NotEqual (clipText, result); + } Application.Shutdown (); } From a5915cd19095d654439ed54b545250d1bf7ce681 Mon Sep 17 00:00:00 2001 From: BDisp Date: Tue, 25 May 2021 19:33:37 +0100 Subject: [PATCH 06/10] Trying fixing more permissions issue. --- UnitTests/ClipboardTests.cs | 44 +++++++++++++++++++++++-------------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/UnitTests/ClipboardTests.cs b/UnitTests/ClipboardTests.cs index 424cde774..e10251e0b 100644 --- a/UnitTests/ClipboardTests.cs +++ b/UnitTests/ClipboardTests.cs @@ -26,7 +26,11 @@ namespace Terminal.Gui.Core { { Application.Init (new FakeDriver (), new FakeMainLoop (() => FakeConsole.ReadKey (true))); - Assert.True (Clipboard.IsSupported); + if (Clipboard.IsSupported) { + Assert.True (Clipboard.IsSupported); + } else { + Assert.False (Clipboard.IsSupported); + } Application.Shutdown (); } @@ -66,7 +70,11 @@ namespace Terminal.Gui.Core { Application.Run (); - Assert.Equal (clipText, Clipboard.Contents); + if (Clipboard.IsSupported) { + Assert.Equal (clipText, Clipboard.Contents); + } else { + Assert.NotEqual (clipText, Clipboard.Contents); + } Application.Shutdown (); } @@ -278,22 +286,26 @@ namespace Terminal.Gui.Core { bool xclipExists () { - using (Process bash = new Process { - StartInfo = new ProcessStartInfo { - FileName = "bash", - Arguments = $"-c \"which xclip\"", - RedirectStandardOutput = true, - } - }) { - bash.Start (); - bool exist = bash.StandardOutput.ReadToEnd ().TrimEnd () != ""; - bash.StandardOutput.Close (); - bash.WaitForExit (); - if (exist) { - return true; + try { + using (Process bash = new Process { + StartInfo = new ProcessStartInfo { + FileName = "bash", + Arguments = $"-c \"which xclip\"", + RedirectStandardOutput = true, + } + }) { + bash.Start (); + bool exist = bash.StandardOutput.ReadToEnd ().TrimEnd () != ""; + bash.StandardOutput.Close (); + bash.WaitForExit (); + if (exist) { + return true; + } } + return false; + } catch (System.Exception) { + return false; } - return false; } } } From 30748df0da6b0b0b10cd1a6d1535c50cbec81e94 Mon Sep 17 00:00:00 2001 From: BDisp Date: Tue, 25 May 2021 19:43:19 +0100 Subject: [PATCH 07/10] Trying even more to fix permissions issue and removed try catch. --- Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs | 6 +----- UnitTests/ClipboardTests.cs | 6 +++++- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs b/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs index b38cce903..54be73cb6 100644 --- a/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs @@ -1109,11 +1109,7 @@ namespace Terminal.Gui { // System.IO.File.Delete (tempFileName); // } - try { - BashRunner.Run ("xclip -selection clipboard -i", false, text); - } catch (Exception ex) { - throw new NotSupportedException ("Write to clipboard failed", ex); - } + BashRunner.Run ("xclip -selection clipboard -i", false, text); } } diff --git a/UnitTests/ClipboardTests.cs b/UnitTests/ClipboardTests.cs index e10251e0b..aa621bdfa 100644 --- a/UnitTests/ClipboardTests.cs +++ b/UnitTests/ClipboardTests.cs @@ -64,7 +64,11 @@ namespace Terminal.Gui.Core { Application.Init (new FakeDriver (), new FakeMainLoop (() => FakeConsole.ReadKey (true))); var clipText = "Trying to set the OS clipboard."; - Assert.True (Clipboard.TrySetClipboardData (clipText)); + if (Clipboard.IsSupported) { + Assert.True (Clipboard.TrySetClipboardData (clipText)); + } else { + Assert.False (Clipboard.TrySetClipboardData (clipText)); + } Application.Iteration += () => Application.RequestStop (); From ce04eeda86ae05aec40154e96614f6313c61f117 Mon Sep 17 00:00:00 2001 From: BDisp Date: Thu, 27 May 2021 17:13:12 +0100 Subject: [PATCH 08/10] Added -noprofile option to PowerShell and the clip.exe. --- .../CursesDriver/CursesDriver.cs | 54 ++++++++++--------- UnitTests/ClipboardTests.cs | 22 ++++++-- 2 files changed, 49 insertions(+), 27 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs b/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs index 54be73cb6..88b876eba 100644 --- a/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs @@ -1076,10 +1076,7 @@ namespace Terminal.Gui { { try { var result = BashRunner.Run ("which xclip"); - if (string.IsNullOrEmpty (result) || result.Contains ("not found")) { - return false; - } - return true; + return BashRunner.FileExists (result); } catch (Exception) { // Permissions issue. return false; @@ -1179,6 +1176,11 @@ namespace Terminal.Gui { } return result; } + + public static bool FileExists (string value) + { + return !string.IsNullOrEmpty (value) && !value.Contains ("not found"); + } } class MacOSXClipboard : ClipboardBase { @@ -1200,23 +1202,11 @@ namespace Terminal.Gui { bool CheckSupport () { var result = BashRunner.Run ("which pbcopy"); - if (!FileExists (result)) { + if (!BashRunner.FileExists (result)) { return false; } result = BashRunner.Run ("which pbpaste"); - if (!FileExists (result)) { - return false; - } - - bool FileExists (string value) - { - if (string.IsNullOrEmpty (value) || value.Contains ("not found")) { - return false; - } - return true; - } - - return true; + return BashRunner.FileExists (result); } public MacOSXClipboard () @@ -1272,10 +1262,14 @@ namespace Terminal.Gui { bool CheckSupport () { var result = BashRunner.Run ("which powershell.exe"); - if (string.IsNullOrEmpty (result) || result.Contains ("not found")) { - return false; - } - return true; + return BashRunner.FileExists (result); + + //var result = BashRunner.Run ("which powershell.exe"); + //if (!BashRunner.FileExists (result)) { + // return false; + //} + //result = BashRunner.Run ("which clip.exe"); + //return BashRunner.FileExists (result); } protected override string GetClipboardDataImpl () @@ -1284,7 +1278,7 @@ namespace Terminal.Gui { StartInfo = new System.Diagnostics.ProcessStartInfo { RedirectStandardOutput = true, FileName = "powershell.exe", - Arguments = "-command \"Get-Clipboard\"" + Arguments = "-noprofile -command \"Get-Clipboard\"" } }) { powershell.Start (); @@ -1300,12 +1294,24 @@ namespace Terminal.Gui { using (var powershell = new System.Diagnostics.Process { StartInfo = new System.Diagnostics.ProcessStartInfo { FileName = "powershell.exe", - Arguments = $"-command \"Set-Clipboard -Value \\\"{text}\\\"\"" + Arguments = $"-noprofile -command \"Set-Clipboard -Value \\\"{text}\\\"\"" } }) { powershell.Start (); powershell.WaitForExit (); } + + //using (var clipExe = new System.Diagnostics.Process { + // StartInfo = new System.Diagnostics.ProcessStartInfo { + // FileName = "clip.exe", + // RedirectStandardInput = true + // } + //}) { + // clipExe.Start (); + // clipExe.StandardInput.Write (text); + // clipExe.StandardInput.Close (); + // clipExe.WaitForExit (); + //} } } } diff --git a/UnitTests/ClipboardTests.cs b/UnitTests/ClipboardTests.cs index aa621bdfa..d12aa852a 100644 --- a/UnitTests/ClipboardTests.cs +++ b/UnitTests/ClipboardTests.cs @@ -135,12 +135,28 @@ namespace Terminal.Gui.Core { using (Process bash = new Process { StartInfo = new ProcessStartInfo { FileName = "powershell.exe", - Arguments = $"-command \"Set-Clipboard -Value \\\"{clipText}\\\"\"" + Arguments = $"-noprofile -command \"Set-Clipboard -Value \\\"{clipText}\\\"\"" } }) { bash.Start (); bash.WaitForExit (); } + + //using (Process clipExe = new Process { + // StartInfo = new ProcessStartInfo { + // RedirectStandardInput = true, + // FileName = "clip.exe" + // } + //}) { + // clipExe.Start (); + // clipExe.StandardInput.Write (clipText); + // clipExe.StandardInput.Close (); + // clipExe.WaitForExit (); + // //var result = clipExe.WaitForExit (500); + // //if (result) { + // // clipExe.WaitForExit (); + // //} + //} } catch { exit = true; } @@ -196,7 +212,7 @@ namespace Terminal.Gui.Core { StartInfo = new ProcessStartInfo { RedirectStandardOutput = true, FileName = "powershell.exe", - Arguments = "-command \"Get-Clipboard\"" + Arguments = "-noprofile -command \"Get-Clipboard\"" } }) { pwsh.Start (); @@ -223,7 +239,7 @@ namespace Terminal.Gui.Core { StartInfo = new ProcessStartInfo { RedirectStandardOutput = true, FileName = "powershell.exe", - Arguments = "-command \"Get-Clipboard\"" + Arguments = "-noprofile -command \"Get-Clipboard\"" } }) { bash.Start (); From 87926a7d3d9555ab70725cee894a439870ddd894 Mon Sep 17 00:00:00 2001 From: BDisp Date: Sat, 29 May 2021 22:10:23 +0100 Subject: [PATCH 09/10] Prevents output to the terminal on Linux. --- UICatalog/UICatalog.cs | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/UICatalog/UICatalog.cs b/UICatalog/UICatalog.cs index 33a1fc725..66f20e687 100644 --- a/UICatalog/UICatalog.cs +++ b/UICatalog/UICatalog.cs @@ -635,19 +635,27 @@ namespace UICatalog { private static void OpenUrl (string url) { try { - Process.Start (url); - } catch { - // hack because of this: https://github.com/dotnet/corefx/issues/10361 if (RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) { url = url.Replace ("&", "^&"); Process.Start (new ProcessStartInfo ("cmd", $"/c start {url}") { CreateNoWindow = true }); } else if (RuntimeInformation.IsOSPlatform (OSPlatform.Linux)) { - Process.Start ("xdg-open", url); + using (var process = new Process { + StartInfo = new ProcessStartInfo { + FileName = "xdg-open", + Arguments = url, + RedirectStandardError = true, + RedirectStandardOutput = true, + CreateNoWindow = true, + UseShellExecute = false + } + }) { + process.Start (); + } } else if (RuntimeInformation.IsOSPlatform (OSPlatform.OSX)) { Process.Start ("open", url); - } else { - throw; } + } catch { + throw; } } } From 0e2446588a1c15c0a5b67127a08c4fbe73c98a0e Mon Sep 17 00:00:00 2001 From: BDisp Date: Sat, 29 May 2021 22:43:39 +0100 Subject: [PATCH 10/10] Prevents read blocking on Mac. --- Terminal.Gui/ConsoleDrivers/CursesDriver/UnixMainLoop.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Terminal.Gui/ConsoleDrivers/CursesDriver/UnixMainLoop.cs b/Terminal.Gui/ConsoleDrivers/CursesDriver/UnixMainLoop.cs index feccf6126..96087cce8 100644 --- a/Terminal.Gui/ConsoleDrivers/CursesDriver/UnixMainLoop.cs +++ b/Terminal.Gui/ConsoleDrivers/CursesDriver/UnixMainLoop.cs @@ -116,7 +116,7 @@ namespace Terminal.Gui { this.mainLoop = mainLoop; pipe (wakeupPipes); AddWatch (wakeupPipes [0], Condition.PollIn, ml => { - read (wakeupPipes [0], ignore, (IntPtr)1); + read (wakeupPipes [0], ignore, (IntPtr)0); return true; }); }