diff --git a/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs b/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs index 338b9cce9..df5c3871f 100644 --- a/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs @@ -19,11 +19,14 @@ namespace Terminal.Gui { internal class CursesDriver : ConsoleDriver { public override int Cols => Curses.Cols; public override int Rows => Curses.Lines; + public override int Left => 0; public override int Top => 0; public override bool HeightAsBuffer { get; set; } + public override IClipboard Clipboard { get => clipboard; } CursorVisibility? initialCursorVisibility = null; CursorVisibility? currentCursorVisibility = null; + IClipboard clipboard; // Current row, and current col, tracked by Move/AddRune only int ccol, crow; @@ -102,8 +105,8 @@ namespace Terminal.Gui { //Console.Out.Flush (); //Set cursor key to cursor. - Console.Out.Write ("\x1b[?1l"); - Console.Out.Flush (); + //Console.Out.Write ("\x1b[?1l"); + //Console.Out.Flush (); } public override void UpdateScreen () => window.redrawwin (); @@ -714,8 +717,8 @@ namespace Terminal.Gui { try { //Set cursor key to application. - Console.Out.Write ("\x1b[?1h"); - Console.Out.Flush (); + //Console.Out.Write ("\x1b[?1h"); + //Console.Out.Flush (); window = Curses.initscr (); } catch (Exception e) { @@ -745,6 +748,12 @@ namespace Terminal.Gui { break; } + if (RuntimeInformation.IsOSPlatform (OSPlatform.OSX)) { + clipboard = new MacOSXClipboard (); + } else { + clipboard = new CursesClipboard (); + } + Curses.raw (); Curses.noecho (); @@ -1046,4 +1055,139 @@ namespace Terminal.Gui { return true; } } + + class CursesClipboard : IClipboard { + public string GetClipboardData () + { + var tempFileName = System.IO.Path.GetTempFileName (); + try { + // BashRunner.Run ($"xsel -o --clipboard > {tempFileName}"); + BashRunner.Run ($"xclip -o > {tempFileName}"); + return System.IO.File.ReadAllText (tempFileName); + } finally { + System.IO.File.Delete (tempFileName); + } + } + + public void SetClipboardData (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); + } + } + } + + static class BashRunner { + public static string Run (string commandLine) + { + 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}. + 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); + } + } + + static bool DoubleWaitForExit (this System.Diagnostics.Process process) + { + var result = process.WaitForExit (500); + if (result) { + process.WaitForExit (); + } + return result; + } + } + + class MacOSXClipboard : IClipboard { + IntPtr nsString = objc_getClass ("NSString"); + IntPtr nsPasteboard = objc_getClass ("NSPasteboard"); + IntPtr utfTextType; + IntPtr generalPasteboard; + IntPtr initWithUtf8Register = sel_registerName ("initWithUTF8String:"); + IntPtr allocRegister = sel_registerName ("alloc"); + IntPtr setStringRegister = sel_registerName ("setString:forType:"); + IntPtr stringForTypeRegister = sel_registerName ("stringForType:"); + IntPtr utf8Register = sel_registerName ("UTF8String"); + IntPtr nsStringPboardType; + IntPtr generalPasteboardRegister = sel_registerName ("generalPasteboard"); + IntPtr clearContentsRegister = sel_registerName ("clearContents"); + + 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); + } + + public string GetClipboardData () + { + var ptr = objc_msgSend (generalPasteboard, stringForTypeRegister, nsStringPboardType); + var charArray = objc_msgSend (ptr, utf8Register); + return Marshal.PtrToStringAnsi (charArray); + } + + public void SetClipboardData (string text) + { + IntPtr 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) { + objc_msgSend (str, 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); + + [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); + } } diff --git a/Terminal.Gui/ConsoleDrivers/CursesDriver/binding.cs b/Terminal.Gui/ConsoleDrivers/CursesDriver/binding.cs index add2342f6..e3d91b6d9 100644 --- a/Terminal.Gui/ConsoleDrivers/CursesDriver/binding.cs +++ b/Terminal.Gui/ConsoleDrivers/CursesDriver/binding.cs @@ -48,13 +48,13 @@ namespace Unix.Terminal { #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member public partial class Curses { - [StructLayout (LayoutKind.Sequential)] - public struct winsize { - public ushort ws_row; - public ushort ws_col; - public ushort ws_xpixel; /* unused */ - public ushort ws_ypixel; /* unused */ - }; + //[StructLayout (LayoutKind.Sequential)] + //public struct winsize { + // public ushort ws_row; + // public ushort ws_col; + // public ushort ws_xpixel; /* unused */ + // public ushort ws_ypixel; /* unused */ + //}; [StructLayout (LayoutKind.Sequential)] public struct MouseEvent { @@ -77,8 +77,8 @@ namespace Unix.Terminal { [DllImport ("libc")] public extern static int setlocale (int cate, [MarshalAs (UnmanagedType.LPStr)] string locale); - [DllImport ("libc")] - public extern static int ioctl (int fd, int cmd, out winsize argp); + //[DllImport ("libc")] + //public extern static int ioctl (int fd, int cmd, out winsize argp); static void LoadMethods () { @@ -142,11 +142,11 @@ namespace Unix.Terminal { if (l == 1 || l != lines || c != cols) { lines = l; cols = c; - if (l <= 0 || c <= 0) { - Console.Out.Write ($"\x1b[8;50;{c}t"); - Console.Out.Flush (); - return false; - } + //if (l <= 0 || c <= 0) { + // Console.Out.Write ($"\x1b[8;50;{c}t"); + // Console.Out.Flush (); + // return false; + //} return true; } return false; @@ -200,29 +200,29 @@ namespace Unix.Terminal { internal static void console_sharp_get_dims (out int lines, out int cols) { - //lines = Marshal.ReadInt32 (lines_ptr); - //cols = Marshal.ReadInt32 (cols_ptr); + lines = Marshal.ReadInt32 (lines_ptr); + cols = Marshal.ReadInt32 (cols_ptr); - int cmd; - if (UnmanagedLibrary.IsMacOSPlatform) { - cmd = TIOCGWINSZ_MAC; - } else { - cmd = TIOCGWINSZ; - } + //int cmd; + //if (UnmanagedLibrary.IsMacOSPlatform) { + // cmd = TIOCGWINSZ_MAC; + //} else { + // cmd = TIOCGWINSZ; + //} - if (ioctl (1, cmd, out winsize ws) == 0) { - lines = ws.ws_row; - cols = ws.ws_col; + //if (ioctl (1, cmd, out winsize ws) == 0) { + // lines = ws.ws_row; + // cols = ws.ws_col; - if (lines == Lines && cols == Cols) { - return; - } + // if (lines == Lines && cols == Cols) { + // return; + // } - resizeterm (lines, cols); - } else { - lines = Lines; - cols = Cols; - } + // resizeterm (lines, cols); + //} else { + // lines = Lines; + // cols = Cols; + //} } public static Event mousemask (Event newmask, out Event oldmask) @@ -233,7 +233,6 @@ namespace Unix.Terminal { return ret; } - // We encode ESC + char (what Alt-char generates) as 0x2000 + char public const int KeyAlt = 0x2000; @@ -243,6 +242,7 @@ namespace Unix.Terminal { return key & ~KeyAlt; return 0; } + public static int StartColor () => methods.start_color (); public static bool HasColors => methods.has_colors (); public static int InitColorPair (short pair, short foreground, short background) => methods.init_pair (pair, foreground, background); diff --git a/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeConsole.cs b/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeConsole.cs index aba1b0ecd..56f0c4cf2 100644 --- a/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeConsole.cs +++ b/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeConsole.cs @@ -12,10 +12,13 @@ using System.Text; using System.Threading.Tasks; namespace Terminal.Gui { + +#pragma warning disable RCS1138 // Add summary to documentation comment. /// /// /// public static class FakeConsole { +#pragma warning restore RCS1138 // Add summary to documentation comment. // // Summary: @@ -36,10 +39,22 @@ namespace Terminal.Gui { // // T:System.IO.IOException: // Error reading or writing information. +#pragma warning disable RCS1138 // Add summary to documentation comment. + + /// + /// Specifies the initial console width. + /// + public const int WIDTH = 80; + + /// + /// Specifies the initial console height. + /// + public const int HEIGHT = 25; + /// /// /// - public static int WindowWidth { get; set; } = 80; + public static int WindowWidth { get; set; } = WIDTH; // // Summary: // Gets a value that indicates whether output has been redirected from the standard @@ -198,7 +213,7 @@ namespace Terminal.Gui { /// /// /// - public static int BufferHeight { get; set; } = 25; + public static int BufferHeight { get; set; } = HEIGHT; // // Summary: // Gets or sets the width of the buffer area. @@ -220,7 +235,7 @@ namespace Terminal.Gui { /// /// /// - public static int BufferWidth { get; set; } = 80; + public static int BufferWidth { get; set; } = WIDTH; // // Summary: // Gets or sets the height of the console window area. @@ -243,7 +258,7 @@ namespace Terminal.Gui { /// /// /// - public static int WindowHeight { get; set; } = 25; + public static int WindowHeight { get; set; } = HEIGHT; // // Summary: // Gets or sets a value indicating whether the combination of the System.ConsoleModifiers.Control @@ -531,7 +546,7 @@ namespace Terminal.Gui { /// public static void Clear () { - _buffer = new char [WindowWidth, WindowHeight]; + _buffer = new char [BufferWidth, BufferHeight]; SetCursorPosition (0, 0); } @@ -905,7 +920,8 @@ namespace Terminal.Gui { /// public static void SetBufferSize (int width, int height) { - throw new NotImplementedException (); + BufferWidth = width; + BufferHeight = height; } // @@ -939,6 +955,8 @@ namespace Terminal.Gui { { CursorLeft = left; CursorTop = top; + WindowLeft = Math.Max (Math.Min (left, BufferWidth - WindowWidth), 0); + WindowTop = Math.Max (Math.Min (top, BufferHeight - WindowHeight), 0); } // diff --git a/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs b/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs index 5d69fc11f..246f5af5d 100644 --- a/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs @@ -7,8 +7,11 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.InteropServices; using System.Threading; using NStack; +// Alias Console to MockConsole so we don't accidentally use Console +using Console = Terminal.Gui.FakeConsole; namespace Terminal.Gui { /// @@ -16,11 +19,14 @@ namespace Terminal.Gui { /// public class FakeDriver : ConsoleDriver { #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member - int cols, rows; + int cols, rows, left, top; public override int Cols => cols; public override int Rows => rows; + // Only handling left here because not all terminals has a horizontal scroll bar. + public override int Left => 0; public override int Top => 0; public override bool HeightAsBuffer { get; set; } + public override IClipboard Clipboard { get; } // The format is rows, columns and 3 values on the last column: Rune, Attribute and Dirty Flag int [,,] contents; @@ -53,9 +59,13 @@ namespace Terminal.Gui { public FakeDriver () { - cols = FakeConsole.WindowWidth; - rows = FakeConsole.WindowHeight; // - 1; - UpdateOffscreen (); + if (RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) { + Clipboard = new WindowsClipboard (); + } else if (RuntimeInformation.IsOSPlatform (OSPlatform.OSX)) { + Clipboard = new MacOSXClipboard (); + } else { + Clipboard = new CursesClipboard (); + } } bool needMove; @@ -127,6 +137,12 @@ namespace Terminal.Gui { public override void Init (Action terminalResized) { + TerminalResized = terminalResized; + + cols = FakeConsole.WindowWidth = FakeConsole.BufferWidth = FakeConsole.WIDTH; + rows = FakeConsole.WindowHeight = FakeConsole.BufferHeight = FakeConsole.HEIGHT; + UpdateOffscreen (); + Colors.TopLevel = new ColorScheme (); Colors.Base = new ColorScheme (); Colors.Dialog = new ColorScheme (); @@ -190,14 +206,16 @@ namespace Terminal.Gui { public override void UpdateScreen () { - int rows = Rows; + int top = Top; + int left = Left; + int rows = Math.Min (Console.WindowHeight + top, Rows); int cols = Cols; FakeConsole.CursorTop = 0; FakeConsole.CursorLeft = 0; - for (int row = 0; row < rows; row++) { + for (int row = top; row < rows; row++) { dirtyLine [row] = false; - for (int col = 0; col < cols; col++) { + for (int col = left; col < cols; col++) { contents [row, col, 2] = 0; var color = contents [row, col, 1]; if (color != redrawColor) @@ -442,6 +460,94 @@ namespace Terminal.Gui { ProcessInput (new ConsoleKeyInfo (keyChar, key, shift, alt, control)); } + public void SetBufferSize (int width, int height) + { + cols = FakeConsole.WindowWidth = FakeConsole.BufferWidth = width; + rows = FakeConsole.WindowHeight = FakeConsole.BufferHeight = height; + ProcessResize (); + } + + public void SetWindowSize (int width, int height) + { + FakeConsole.WindowWidth = width; + FakeConsole.WindowHeight = height; + if (width > cols || !HeightAsBuffer) { + cols = FakeConsole.BufferWidth = width; + } + if (height > rows || !HeightAsBuffer) { + rows = FakeConsole.BufferHeight = height; + } + ProcessResize (); + } + + public void SetWindowPosition (int left, int top) + { + if (HeightAsBuffer) { + this.left = FakeConsole.WindowLeft = Math.Max (Math.Min (left, Cols - FakeConsole.WindowWidth), 0); + this.top = FakeConsole.WindowTop = Math.Max (Math.Min (top, Rows - Console.WindowHeight), 0); + } else if (this.left > 0 || this.top > 0) { + this.left = FakeConsole.WindowLeft = 0; + this.top = FakeConsole.WindowTop = 0; + } + } + + void ProcessResize () + { + ResizeScreen (); + UpdateOffScreen (); + TerminalResized?.Invoke (); + } + + void ResizeScreen () + { + if (!HeightAsBuffer) { + if (Console.WindowHeight > 0) { + // Can raise an exception while is still resizing. + try { +#pragma warning disable CA1416 + Console.CursorTop = 0; + Console.CursorLeft = 0; + Console.WindowTop = 0; + Console.WindowLeft = 0; +#pragma warning restore CA1416 + } catch (System.IO.IOException) { + return; + } catch (ArgumentOutOfRangeException) { + return; + } + } + } else { + try { +#pragma warning disable CA1416 + Console.WindowLeft = Math.Max (Math.Min (left, Cols - Console.WindowWidth), 0); + Console.WindowTop = Math.Max (Math.Min (top, Rows - Console.WindowHeight), 0); +#pragma warning restore CA1416 + } catch (Exception) { + return; + } + } + + Clip = new Rect (0, 0, Cols, Rows); + + contents = new int [Rows, Cols, 3]; + dirtyLine = new bool [Rows]; + } + + void UpdateOffScreen () + { + // Can raise an exception while is still resizing. + try { + for (int row = 0; row < rows; row++) { + for (int c = 0; c < cols; c++) { + contents [row, c, 0] = ' '; + contents [row, c, 1] = (ushort)Colors.TopLevel.Normal; + contents [row, c, 2] = 0; + dirtyLine [row] = true; + } + } + } catch (IndexOutOfRangeException) { } + } + #region Unused public override void SetColors (ConsoleColor foreground, ConsoleColor background) { diff --git a/Terminal.Gui/ConsoleDrivers/NetDriver.cs b/Terminal.Gui/ConsoleDrivers/NetDriver.cs index cc1117f16..67fc40dd8 100644 --- a/Terminal.Gui/ConsoleDrivers/NetDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/NetDriver.cs @@ -1076,12 +1076,14 @@ namespace Terminal.Gui { int cols, rows, top; public override int Cols => cols; public override int Rows => rows; + public override int Left => 0; public override int Top => top; public override bool HeightAsBuffer { get; set; } public NetWinVTConsole NetWinConsole { get; } public bool IsWinPlatform { get; } public bool AlwaysSetPosition { get; set; } + public override IClipboard Clipboard { get; } int largestWindowHeight; @@ -1093,6 +1095,13 @@ namespace Terminal.Gui { NetWinConsole = new NetWinVTConsole (); } largestWindowHeight = Math.Max (Console.BufferHeight, largestWindowHeight); + if (IsWinPlatform) { + Clipboard = new WindowsClipboard (); + } else if (RuntimeInformation.IsOSPlatform (OSPlatform.OSX)) { + Clipboard = new MacOSXClipboard (); + } else { + Clipboard = new CursesClipboard (); + } } // The format is rows, columns and 3 values on the last column: Rune, Attribute and Dirty Flag diff --git a/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs b/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs index 3d23c796d..bddf89f14 100644 --- a/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs @@ -27,6 +27,7 @@ // using NStack; using System; +using System.ComponentModel; using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; @@ -620,12 +621,16 @@ namespace Terminal.Gui { int cols, rows, top; WindowsConsole winConsole; WindowsConsole.SmallRect damageRegion; + IClipboard clipboard; public override int Cols => cols; public override int Rows => rows; + public override int Left => 0; public override int Top => top; public override bool HeightAsBuffer { get; set; } + public override IClipboard Clipboard => clipboard; + public WindowsConsole WinConsole { get => winConsole; private set => winConsole = value; @@ -639,6 +644,7 @@ namespace Terminal.Gui { public WindowsDriver () { winConsole = new WindowsConsole (); + clipboard = new WindowsClipboard (); } bool winChanging; @@ -1633,4 +1639,137 @@ namespace Terminal.Gui { //} } } + + class WindowsClipboard : IClipboard { + public string GetClipboardData () + { + if (!IsClipboardFormatAvailable (cfUnicodeText)) + return null; + + try { + if (!OpenClipboard (IntPtr.Zero)) + return null; + + IntPtr handle = GetClipboardData (cfUnicodeText); + if (handle == IntPtr.Zero) + return null; + + IntPtr pointer = IntPtr.Zero; + + try { + pointer = GlobalLock (handle); + if (pointer == IntPtr.Zero) + return null; + + int size = GlobalSize (handle); + byte [] buff = new byte [size]; + + Marshal.Copy (pointer, buff, 0, size); + + return System.Text.Encoding.Unicode.GetString (buff) + .TrimEnd ('\0') + .Replace ("\r\n", "\n"); + } finally { + if (pointer != IntPtr.Zero) + GlobalUnlock (handle); + } + } finally { + CloseClipboard (); + } + } + + public void SetClipboardData (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 (); + } + } + + void OpenClipboard () + { + var num = 10; + while (true) { + if (OpenClipboard (default)) { + break; + } + + if (--num == 0) { + ThrowWin32 (); + } + + Thread.Sleep (100); + } + } + + const uint cfUnicodeText = 13; + + void ThrowWin32 () + { + throw new Win32Exception (Marshal.GetLastWin32Error ()); + } + + [DllImport ("User32.dll", SetLastError = true)] + [return: MarshalAs (UnmanagedType.Bool)] + static extern bool IsClipboardFormatAvailable (uint format); + + [DllImport ("kernel32.dll", SetLastError = true)] + static extern int GlobalSize (IntPtr handle); + + [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 (); + + [DllImport ("user32.dll", SetLastError = true)] + static extern IntPtr GetClipboardData (uint uFormat); + } } diff --git a/Terminal.Gui/Core/Clipboard.cs b/Terminal.Gui/Core/Clipboard.cs new file mode 100644 index 000000000..5e029fe29 --- /dev/null +++ b/Terminal.Gui/Core/Clipboard.cs @@ -0,0 +1,32 @@ +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/ConsoleDriver.cs b/Terminal.Gui/Core/ConsoleDriver.cs index fc6451646..9f7594443 100644 --- a/Terminal.Gui/Core/ConsoleDriver.cs +++ b/Terminal.Gui/Core/ConsoleDriver.cs @@ -613,15 +613,27 @@ namespace Terminal.Gui { /// The current number of columns in the terminal. /// public abstract int Cols { get; } + /// /// The current number of rows in the terminal. /// public abstract int Rows { get; } + + /// + /// The current left in the terminal. + /// + public abstract int Left { get; } + /// /// The current top in the terminal. /// public abstract int Top { get; } + /// + /// Get the operation system clipboard. + /// + public abstract IClipboard Clipboard { get; } + /// /// If false height is measured by the window height and thus no scrolling. /// If true then height is measured by the buffer height, enabling scrolling. @@ -1166,4 +1178,20 @@ 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/Terminal.Gui/Views/Clipboard.cs b/Terminal.Gui/Views/Clipboard.cs deleted file mode 100644 index e24e79587..000000000 --- a/Terminal.Gui/Views/Clipboard.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using NStack; - -namespace Terminal.Gui { - /// - /// Provides cut, copy, and paste support for the clipboard. - /// NOTE: Currently not implemented. - /// - public static class Clipboard { - /// - /// - /// - public static ustring Contents { get; set; } - } -} diff --git a/Terminal.Gui/Views/TextView.cs b/Terminal.Gui/Views/TextView.cs index 62d583c42..d533e24d1 100644 --- a/Terminal.Gui/Views/TextView.cs +++ b/Terminal.Gui/Views/TextView.cs @@ -1662,7 +1662,7 @@ namespace Terminal.Gui { int lineRuneCount = line.Count; var col = 0; - Move (0, idxRow); + Move (0, row); for (int idxCol = leftColumn; idxCol < lineRuneCount; idxCol++) { var rune = idxCol >= lineRuneCount ? ' ' : line [idxCol]; var cols = Rune.ColumnWidth (rune); diff --git a/UnitTests/ClipboardTests.cs b/UnitTests/ClipboardTests.cs new file mode 100644 index 000000000..3ee4fc3c9 --- /dev/null +++ b/UnitTests/ClipboardTests.cs @@ -0,0 +1,17 @@ +using Xunit; + +namespace Terminal.Gui.Core { + public class ClipboardTests { + [Fact] + public void Contents_Gets_Sets () + { + Application.Init (new FakeDriver (), new FakeMainLoop (() => FakeConsole.ReadKey (true))); + + var clipText = "This is a clipboard unit test."; + Clipboard.Contents = clipText; + Assert.Equal (clipText, Clipboard.Contents); + + Application.Shutdown (); + } + } +} diff --git a/UnitTests/ConsoleDriverTests.cs b/UnitTests/ConsoleDriverTests.cs index 78d8d6731..f2429e11a 100644 --- a/UnitTests/ConsoleDriverTests.cs +++ b/UnitTests/ConsoleDriverTests.cs @@ -245,5 +245,173 @@ namespace Terminal.Gui.ConsoleDrivers { // Shutdown must be called to safely clean up Application if Init has been called Application.Shutdown (); } + + [Fact] + public void TerminalResized_Simulation () + { + var driver = new FakeDriver (); + Application.Init (driver, new FakeMainLoop (() => FakeConsole.ReadKey (true))); + var wasTerminalResized = false; + Application.Resized = (e) => { + wasTerminalResized = true; + Assert.Equal (120, e.Cols); + Assert.Equal (40, e.Rows); + }; + + Assert.Equal (80, Console.BufferWidth); + Assert.Equal (25, Console.BufferHeight); + + // MockDriver is by default 80x25 + Assert.Equal (Console.BufferWidth, driver.Cols); + Assert.Equal (Console.BufferHeight, driver.Rows); + Assert.False (wasTerminalResized); + + // MockDriver will now be sets to 120x40 + driver.SetBufferSize (120, 40); + Assert.Equal (120, Application.Driver.Cols); + Assert.Equal (40, Application.Driver.Rows); + Assert.True (wasTerminalResized); + + // MockDriver will still be 120x40 + wasTerminalResized = false; + Application.HeightAsBuffer = true; + driver.SetWindowSize (40, 20); + Assert.Equal (120, Application.Driver.Cols); + Assert.Equal (40, Application.Driver.Rows); + Assert.Equal (120, Console.BufferWidth); + Assert.Equal (40, Console.BufferHeight); + Assert.Equal (40, Console.WindowWidth); + Assert.Equal (20, Console.WindowHeight); + Assert.True (wasTerminalResized); + + Application.Shutdown (); + } + + [Fact] + public void HeightAsBuffer_Is_False_Left_And_Top_Is_Always_Zero () + { + var driver = new FakeDriver (); + Application.Init (driver, new FakeMainLoop (() => FakeConsole.ReadKey (true))); + + Assert.False (Application.HeightAsBuffer); + Assert.Equal (0, Console.WindowLeft); + Assert.Equal (0, Console.WindowTop); + + driver.SetWindowPosition (5, 5); + Assert.Equal (0, Console.WindowLeft); + Assert.Equal (0, Console.WindowTop); + + Application.Shutdown (); + } + + [Fact] + public void HeightAsBuffer_Is_True_Left_Cannot_Be_Greater_Than_WindowWidth () + { + var driver = new FakeDriver (); + Application.Init (driver, new FakeMainLoop (() => FakeConsole.ReadKey (true))); + + Application.HeightAsBuffer = true; + Assert.True (Application.HeightAsBuffer); + + driver.SetWindowPosition (81, 25); + Assert.Equal (0, Console.WindowLeft); + Assert.Equal (0, Console.WindowTop); + + Application.Shutdown (); + } + + [Fact] + public void HeightAsBuffer_Is_True_Left_Cannot_Be_Greater_Than_BufferWidth_Minus_WindowWidth () + { + var driver = new FakeDriver (); + Application.Init (driver, new FakeMainLoop (() => FakeConsole.ReadKey (true))); + + Application.HeightAsBuffer = true; + Assert.True (Application.HeightAsBuffer); + + driver.SetWindowPosition (81, 25); + Assert.Equal (0, Console.WindowLeft); + Assert.Equal (0, Console.WindowTop); + + // MockDriver will now be sets to 120x25 + driver.SetBufferSize (120, 25); + Assert.Equal (120, Application.Driver.Cols); + Assert.Equal (25, Application.Driver.Rows); + Assert.Equal (120, Console.BufferWidth); + Assert.Equal (25, Console.BufferHeight); + Assert.Equal (120, Console.WindowWidth); + Assert.Equal (25, Console.WindowHeight); + driver.SetWindowPosition (121, 25); + Assert.Equal (0, Console.WindowLeft); + Assert.Equal (0, Console.WindowTop); + + driver.SetWindowSize (90, 25); + Assert.Equal (120, Application.Driver.Cols); + Assert.Equal (25, Application.Driver.Rows); + Assert.Equal (120, Console.BufferWidth); + Assert.Equal (25, Console.BufferHeight); + Assert.Equal (90, Console.WindowWidth); + Assert.Equal (25, Console.WindowHeight); + driver.SetWindowPosition (121, 25); + Assert.Equal (30, Console.WindowLeft); + Assert.Equal (0, Console.WindowTop); + + Application.Shutdown (); + } + + [Fact] + public void HeightAsBuffer_Is_True_Top_Cannot_Be_Greater_Than_WindowHeight () + { + var driver = new FakeDriver (); + Application.Init (driver, new FakeMainLoop (() => FakeConsole.ReadKey (true))); + + Application.HeightAsBuffer = true; + Assert.True (Application.HeightAsBuffer); + + driver.SetWindowPosition (80, 26); + Assert.Equal (0, Console.WindowLeft); + Assert.Equal (0, Console.WindowTop); + + Application.Shutdown (); + } + + [Fact] + public void HeightAsBuffer_Is_True_Top_Cannot_Be_Greater_Than_BufferHeight_Minus_WindowHeight () + { + var driver = new FakeDriver (); + Application.Init (driver, new FakeMainLoop (() => FakeConsole.ReadKey (true))); + + Application.HeightAsBuffer = true; + Assert.True (Application.HeightAsBuffer); + + driver.SetWindowPosition (80, 26); + Assert.Equal (0, Console.WindowLeft); + Assert.Equal (0, Console.WindowTop); + + // MockDriver will now be sets to 120x25 + driver.SetBufferSize (80, 40); + Assert.Equal (80, Application.Driver.Cols); + Assert.Equal (40, Application.Driver.Rows); + Assert.Equal (80, Console.BufferWidth); + Assert.Equal (40, Console.BufferHeight); + Assert.Equal (80, Console.WindowWidth); + Assert.Equal (40, Console.WindowHeight); + driver.SetWindowPosition (80, 40); + Assert.Equal (0, Console.WindowLeft); + Assert.Equal (0, Console.WindowTop); + + driver.SetWindowSize (80, 20); + Assert.Equal (80, Application.Driver.Cols); + Assert.Equal (40, Application.Driver.Rows); + Assert.Equal (80, Console.BufferWidth); + Assert.Equal (40, Console.BufferHeight); + Assert.Equal (80, Console.WindowWidth); + Assert.Equal (20, Console.WindowHeight); + driver.SetWindowPosition (80, 41); + Assert.Equal (0, Console.WindowLeft); + Assert.Equal (20, Console.WindowTop); + + Application.Shutdown (); + } } } diff --git a/UnitTests/TextFieldTests.cs b/UnitTests/TextFieldTests.cs index e0f179eee..bedcaf4fb 100644 --- a/UnitTests/TextFieldTests.cs +++ b/UnitTests/TextFieldTests.cs @@ -5,7 +5,7 @@ using Xunit; namespace Terminal.Gui.Views { public class TextFieldTests { - // This class enables test functions annoated with the [InitShutdown] attribute + // This class enables test functions annotated with the [InitShutdown] attribute // to have a function called before the test function is called and after. // // This is necessary because a) Application is a singleton and Init/Shutdown must be called @@ -17,8 +17,8 @@ namespace Terminal.Gui.Views { { Application.Init (new FakeDriver (), new FakeMainLoop (() => FakeConsole.ReadKey (true))); - // 1 2 3 - // 01234567890123456789012345678901=32 (Length) + // 1 2 3 + // 01234567890123456789012345678901=32 (Length) TextFieldTests._textField = new TextField ("TAB to jump between text fields."); }