diff --git a/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs b/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs index 9cc00a8bf..bb09ead45 100644 --- a/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs @@ -956,9 +956,10 @@ namespace Terminal.Gui { public static bool Is_WSL_Platform () { if (new CursesClipboard ().IsSupported) { + // If xclip is installed on Linux under WSL, this will return true. return false; } - var (exitCode, result) = BashRunner.Run ("uname -a", runCurses: false); + var (exitCode, result) = ClipboardProcessRunner.Bash ("uname -a", runCurses: false); if (exitCode == 0 && result.Contains ("microsoft") && result.Contains ("WSL")) { return true; } @@ -1273,28 +1274,33 @@ namespace Terminal.Gui { IsSupported = CheckSupport (); } + string xclipPath = string.Empty; public override bool IsSupported { get; } bool CheckSupport () { try { - var (exitCode, result) = BashRunner.Run ("which xclip", runCurses: false); - return (exitCode == 0 && result.FileExists ()); + var (exitCode, result) = ClipboardProcessRunner.Bash ("which xclip", runCurses: false); + if (exitCode == 0 && result.FileExists ()) { + xclipPath = result; + return true; + } } catch (Exception) { // Permissions issue. - return false; } + return false; } protected override string GetClipboardDataImpl () { var tempFileName = System.IO.Path.GetTempFileName (); try { - // BashRunner.Run ($"xsel -o --clipboard > {tempFileName}"); - var (exitCode, result) = BashRunner.Run ($"xclip -selection clipboard -o > {tempFileName}"); + var (exitCode, result) = ClipboardProcessRunner.Bash ($"{xclipPath} -selection clipboard -o > {tempFileName}"); if (exitCode == 0) { return System.IO.File.ReadAllText (tempFileName); } + } catch (Exception e) { + throw new NotSupportedException ($"{xclipPath} -selection clipboard -o failed.", e); } finally { System.IO.File.Delete (tempFileName); } @@ -1303,65 +1309,73 @@ namespace Terminal.Gui { protected override void SetClipboardDataImpl (string 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); - // } - - BashRunner.Run ("xclip -selection clipboard -i", false, text); + try { + ClipboardProcessRunner.Bash ($"{xclipPath} - selection clipboard -i", false, text); + } catch (Exception e) { + throw new NotSupportedException ($"{xclipPath} -selection clipboard -o failed", e); + } } } - static class BashRunner { - public static (int exitCode, string result) Run (string commandLine, bool output = true, string inputText = "", bool runCurses = true) + internal static class ClipboardProcessRunner { + public static (int exitCode, string result) Bash (string commandLine, bool output = true, string inputText = "", bool runCurses = true) { var arguments = $"-c \"{commandLine}\""; + var (exitCode, result) = Process ("bash", arguments, inputText); + if (exitCode == 0) { + if (runCurses && Application.Driver is CursesDriver) { + Curses.raw (); + Curses.noecho (); + } + } + return (exitCode, result.TrimEnd ()); + } + + public static (int exitCode, string result) Process (string cmd, string arguments, string input = null) + { 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", + var output = string.Empty; + + using (Process process = new Process { + StartInfo = new ProcessStartInfo { + FileName = cmd, Arguments = arguments, - RedirectStandardInput = true, RedirectStandardOutput = true, RedirectStandardError = true, + RedirectStandardInput = true, UseShellExecute = false, - CreateNoWindow = false, + CreateNoWindow = true, } }) { process.Start (); - if (output) { - process.StandardInput.Write (inputText); + if (!string.IsNullOrEmpty (input)) { + process.StandardInput.Write (input); + process.StandardInput.Close (); } - process.StandardInput.Close (); + 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: {process.StartInfo.FileName} {arguments}. + + if (!process.WaitForExit (5000)) { + var timeoutError = $@"Process timed out. Command line: {process.StartInfo.FileName} {process.StartInfo.Arguments}. Output: {outputBuilder} Error: {errorBuilder}"; throw new TimeoutException (timeoutError); } - if (process.ExitCode == 0) { - if (runCurses && Application.Driver is CursesDriver) { - Curses.raw (); - Curses.noecho (); - } - return (process.ExitCode, outputBuilder.ToString ().TrimEnd ()); - } - var error = $@"Could not execute process. ExitCode: {process.ExitCode}, Command line: {process.StartInfo.FileName} {arguments}. - Output: {outputBuilder} - Error: {errorBuilder}"; - return (process.ExitCode, error); + if (process.ExitCode > 0) { + output = $@"Process failed to run. Command line: {cmd} {arguments}. + Output: {outputBuilder} + Error: {errorBuilder}"; + } else { + output = outputBuilder.ToString ().TrimEnd (); + } + return (process.ExitCode, output); } } @@ -1380,6 +1394,7 @@ namespace Terminal.Gui { } } + /// /// A clipboard implementation for MacOSX. /// This implementation uses the Mac clipboard API (via P/Invoke) to copy/paste. @@ -1412,12 +1427,12 @@ namespace Terminal.Gui { bool CheckSupport () { - var (exitCode, result) = BashRunner.Run ("which pbcopy"); - if (!result.FileExists ()) { + var (exitCode, result) = ClipboardProcessRunner.Bash ("which pbcopy"); + if (exitCode != 0 || !result.FileExists ()) { return false; } - (exitCode, result) = BashRunner.Run ("which pbpaste"); - return result.FileExists (); + (exitCode, result) = ClipboardProcessRunner.Bash ("which pbpaste"); + return exitCode == 0 && result.FileExists (); } protected override string GetClipboardDataImpl () @@ -1474,83 +1489,45 @@ namespace Terminal.Gui { public override bool IsSupported { get; } - private string powershellCommand = string.Empty; + private string powershellPath = string.Empty; bool CheckSupport () { - if (string.IsNullOrEmpty (powershellCommand)) { + if (string.IsNullOrEmpty (powershellPath)) { // Specify pwsh.exe (not pwsh) to ensure we get the Windows version (invoked via WSL) - var (exitCode, result) = BashRunner.Run ("which pwsh.exe"); + var (exitCode, result) = ClipboardProcessRunner.Bash ("which pwsh.exe"); if (exitCode > 0) { - (exitCode, result) = BashRunner.Run ("which powershell.exe"); + (exitCode, result) = ClipboardProcessRunner.Bash ("which powershell.exe"); } if (exitCode == 0) { - powershellCommand = result; + powershellPath = result; } } - return !string.IsNullOrEmpty (powershellCommand); + return !string.IsNullOrEmpty (powershellPath); } protected override string GetClipboardDataImpl () { - if (!IsSupported) return string.Empty; - using (var powershell = new System.Diagnostics.Process { - StartInfo = new System.Diagnostics.ProcessStartInfo { - RedirectStandardOutput = true, - FileName = powershellCommand, - Arguments = "-noprofile -command \"Get-Clipboard\"", - UseShellExecute = false, - CreateNoWindow = true - } - }) { - powershell.Start (); - if (!powershell.DoubleWaitForExit ()) { - var timeoutError = $@"Process timed out. Command line: bash {powershell.StartInfo.Arguments}. - Output: {powershell.StandardOutput.ReadToEnd ()} - Error: {powershell.StandardError.ReadToEnd ()}"; - throw new Exception (timeoutError); - } - var result = powershell.StandardOutput.ReadToEnd (); - powershell.StandardOutput.Close (); + var (exitCode, output) = ClipboardProcessRunner.Process (powershellPath, "-noprofile -command \"Get-Clipboard\""); + if (exitCode == 0) { if (Application.Driver is CursesDriver) { Curses.raw (); Curses.noecho (); } - if (result.EndsWith ("\r\n")) { - result = result.Substring (0, result.Length - 2); + + if (output.EndsWith ("\r\n")) { + output = output.Substring (0, output.Length - 2); } - return result; + return output; } + return string.Empty; } protected override void SetClipboardDataImpl (string text) { - if (!IsSupported) return; - - using (var powershell = new System.Diagnostics.Process { - StartInfo = new System.Diagnostics.ProcessStartInfo { - RedirectStandardOutput = true, - RedirectStandardError = true, - RedirectStandardInput = true, - FileName = powershellCommand, - Arguments = $"-noprofile -command \"Set-Clipboard -Value \\\"{text}\\\"\"" - } - }) { - powershell.Start (); - powershell.WaitForExit (); - if (!powershell.DoubleWaitForExit ()) { - var timeoutError = $@"Process timed out. Command line: {powershell.StartInfo.FileName} {powershell.StartInfo.Arguments}. - Output: {powershell.StandardOutput.ReadToEnd ()} - Error: {powershell.StandardError.ReadToEnd ()}"; - throw new TimeoutException (timeoutError); - } - if (powershell.ExitCode > 0) { - var setClipboardError = $@"Set-Clipboard failed. Command line: {powershell.StartInfo.FileName} {powershell.StartInfo.Arguments}. - Output: {powershell.StandardOutput.ReadToEnd ()} - Error: {powershell.StandardError.ReadToEnd ()}"; - throw new System.InvalidOperationException (setClipboardError); - } + var (exitCode, output) = ClipboardProcessRunner.Process (powershellPath, $"-noprofile -command \"Set-Clipboard -Value \\\"{text}\\\"\""); + if (exitCode == 0) { if (Application.Driver is CursesDriver) { Curses.raw (); Curses.noecho (); diff --git a/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs b/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs index f7d03594b..95e04af4a 100644 --- a/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs @@ -27,28 +27,7 @@ namespace Terminal.Gui { public override int Top => 0; public override bool HeightAsBuffer { get; set; } private IClipboard clipboard = null; - public override IClipboard Clipboard { - get { - if (clipboard == null) { - if (usingFakeClipboard) { - clipboard = new FakeClipboard (); - } else { - if (RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) { - clipboard = new WindowsClipboard (); - } else if (RuntimeInformation.IsOSPlatform (OSPlatform.OSX)) { - clipboard = new MacOSXClipboard (); - } else { - if (CursesDriver.Is_WSL_Platform ()) { - clipboard = new WSLClipboard (); - } else { - clipboard = new CursesClipboard (); - } - } - } - } - return clipboard; - } - } + public override IClipboard Clipboard => clipboard; // The format is rows, columns and 3 values on the last column: Rune, Attribute and Dirty Flag int [,,] contents; @@ -83,6 +62,21 @@ namespace Terminal.Gui { public FakeDriver (bool useFakeClipboard = true) { usingFakeClipboard = useFakeClipboard; + if (usingFakeClipboard) { + clipboard = new FakeClipboard (); + } else { + if (RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) { + clipboard = new WindowsClipboard (); + } else if (RuntimeInformation.IsOSPlatform (OSPlatform.OSX)) { + clipboard = new MacOSXClipboard (); + } else { + if (CursesDriver.Is_WSL_Platform ()) { + clipboard = new WSLClipboard (); + } else { + clipboard = new CursesClipboard (); + } + } + } } bool needMove; @@ -654,7 +648,7 @@ namespace Terminal.Gui { } #endregion - + public class FakeClipboard : ClipboardBase { public override bool IsSupported => true; diff --git a/Terminal.Gui/Core/Clipboard/ClipboardBase.cs b/Terminal.Gui/Core/Clipboard/ClipboardBase.cs index 9eb17e174..fccbe1519 100644 --- a/Terminal.Gui/Core/Clipboard/ClipboardBase.cs +++ b/Terminal.Gui/Core/Clipboard/ClipboardBase.cs @@ -15,9 +15,10 @@ namespace Terminal.Gui { public abstract bool IsSupported { get; } /// - /// Get the operation system clipboard. + /// Returns the contents of the OS clipboard if possible. /// - /// Thrown if it was not possible to read the clipboard contents + /// The contents of the OS clipboard if successful. + /// Thrown if it was not possible to copy from the OS clipboard. public string GetClipboardData () { try { @@ -28,35 +29,38 @@ namespace Terminal.Gui { } /// - /// Get the operation system clipboard. + /// Returns the contents of the OS clipboard if possible. Implemented by -specific subclasses. /// + /// The contents of the OS clipboard if successful. + /// Thrown if it was not possible to copy from the OS clipboard. protected abstract string GetClipboardDataImpl (); /// - /// Sets the operation system clipboard. + /// Pastes the to the OS clipboard if possible. /// - /// - /// Thrown if it was not possible to set the clipboard contents + /// The text to paste to the OS clipboard. + /// Thrown if it was not possible to paste to the OS clipboard. public void SetClipboardData (string text) { try { SetClipboardDataImpl (text); } catch (Exception ex) { - throw new NotSupportedException ("Failed to write to clipboard.", ex); + throw new NotSupportedException ("Failed to paste to the OS clipboard.", ex); } } /// - /// Sets the operation system clipboard. + /// Pastes the to the OS clipboard if possible. Implemented by -specific subclasses. /// - /// + /// The text to paste to the OS clipboard. + /// Thrown if it was not possible to paste to the OS clipboard. protected abstract void SetClipboardDataImpl (string text); /// - /// Gets the operation system clipboard if possible. + /// Copies the contents of the OS clipboard to if possible. /// - /// Clipboard contents read - /// true if it was possible to read the OS clipboard. + /// The contents of the OS clipboard if successful, if not. + /// the OS clipboard was retrieved, otherwise. public bool TryGetClipboardData (out string result) { // Don't even try to read because environment is not set up. @@ -78,10 +82,10 @@ namespace Terminal.Gui { } /// - /// Sets the operation system clipboard if possible. + /// Pastes the to the OS clipboard if possible. /// - /// - /// True if the clipboard content was set successfully + /// The text to paste to the OS clipboard. + /// the OS clipboard was set, otherwise. public bool TrySetClipboardData (string text) { // Don't even try to set because environment is not set up diff --git a/UnitTests/ClipboardTests.cs b/UnitTests/ClipboardTests.cs index 6ba45d76e..4929eeef6 100644 --- a/UnitTests/ClipboardTests.cs +++ b/UnitTests/ClipboardTests.cs @@ -69,37 +69,6 @@ namespace Terminal.Gui.ConsoleDrivers { } } - private static string RunClipboardProcess (string cmd, string args, string writeText = null) - { - string output = string.Empty; - - using (Process process = new Process { - StartInfo = new ProcessStartInfo { - FileName = cmd, - Arguments = args, - RedirectStandardOutput = true, - RedirectStandardError = true, - RedirectStandardInput = true - } - }) { - process.Start (); - if (string.IsNullOrEmpty (writeText)) { - process.StandardInput.Write (writeText); - process.StandardInput.Close (); - } - process.WaitForExit (); - - if (process.ExitCode > 0) { - var error = $@"RunClipboardProcess failed. Command line: {cmd} {args}. - Output: {process.StandardOutput.ReadToEnd ()} - Error: {process.StandardError.ReadToEnd ()}"; - throw new InvalidOperationException (error); - } - output = process.StandardOutput.ReadToEnd ().TrimEnd (); - process.StandardOutput.Close (); - } - return output; - } [Fact, AutoInitShutdown (useFakeClipboard: false)] public void Contents_Gets_From_OS_Clipboard () @@ -110,18 +79,18 @@ namespace Terminal.Gui.ConsoleDrivers { Application.Iteration += () => { if (RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) { - RunClipboardProcess ("pwsh", $"-command \"Set-Clipboard -Value \\\"{clipText}\\\"\""); + ClipboardProcessRunner.Process ("pwsh", $"-command \"Set-Clipboard -Value \\\"{clipText}\\\"\""); getClipText = Clipboard.Contents.ToString (); } else if (RuntimeInformation.IsOSPlatform (OSPlatform.OSX)) { - RunClipboardProcess ("pbcopy", string.Empty, clipText); + ClipboardProcessRunner.Process ("pbcopy", string.Empty, clipText); getClipText = Clipboard.Contents.ToString (); } else if (RuntimeInformation.IsOSPlatform (OSPlatform.Linux)) { if (Is_WSL_Platform ()) { try { // This runs the WINDOWS version of powershell.exe via WSL. - RunClipboardProcess ("powershell.exe", $"-noprofile -command \"Set-Clipboard -Value \\\"{clipText}\\\"\""); + ClipboardProcessRunner.Process ("powershell.exe", $"-noprofile -command \"Set-Clipboard -Value \\\"{clipText}\\\"\""); } catch { failed = true; } @@ -139,7 +108,7 @@ namespace Terminal.Gui.ConsoleDrivers { } // If we get here, powershell didn't work and xclip exists... - RunClipboardProcess ("bash", $"-c \"xclip -sel clip -i\"", clipText); + ClipboardProcessRunner.Process ("bash", $"-c \"xclip -sel clip -i\"", clipText); if (!failed) { getClipText = Clipboard.Contents.ToString (); @@ -168,27 +137,30 @@ namespace Terminal.Gui.ConsoleDrivers { Clipboard.Contents = clipText; if (RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) { - clipReadText = RunClipboardProcess ("pwsh", "-noprofile -command \"Get-Clipboard\""); + (_, clipReadText) = ClipboardProcessRunner.Process ("pwsh", "-noprofile -command \"Get-Clipboard\""); } else if (RuntimeInformation.IsOSPlatform (OSPlatform.OSX)) { - clipReadText = RunClipboardProcess ("pbpaste", ""); + (_, clipReadText) = ClipboardProcessRunner.Process ("pbpaste", ""); } else if (RuntimeInformation.IsOSPlatform (OSPlatform.Linux)) { + var exitCode = 0; if (Is_WSL_Platform ()) { - try { - clipReadText = RunClipboardProcess ("/opt/microsoft/powershell/7/pwsh", "-noprofile -command \"Get-Clipboard\""); - } catch { - failed = true; + (exitCode, clipReadText) = ClipboardProcessRunner.Process ("powershell.exe", "-noprofile -command \"Get-Clipboard\""); + if (exitCode == 0) { + Application.RequestStop (); + return; } - Application.RequestStop (); + failed = true; } + if (failed = xclipExists () == false) { // xclip doesn't exist then exit. Application.RequestStop (); return; } - clipReadText = RunClipboardProcess ("bash", $"-c \"xclip -sel clip -o\""); + (exitCode, clipReadText) = ClipboardProcessRunner.Process ("bash", $"-c \"xclip -sel clip -o\""); + Assert.Equal (0, exitCode); } Application.RequestStop (); @@ -204,14 +176,14 @@ namespace Terminal.Gui.ConsoleDrivers { bool Is_WSL_Platform () { - var result = RunClipboardProcess ("bash", $"-c \"uname -a\""); + var (_, result) = ClipboardProcessRunner.Process ("bash", $"-c \"uname -a\""); return result.Contains ("microsoft") && result.Contains ("WSL"); } bool xclipExists () { try { - var result = RunClipboardProcess ("bash", $"-c \"which xclip\""); + var (_, result) = ClipboardProcessRunner.Process ("bash", $"-c \"which xclip\""); return result.TrimEnd () != ""; } catch (System.Exception) { return false;