Browse Source

Fixes #983. Improving clipboard with interaction with the OS.

BDisp 4 years ago
parent
commit
f60458cc6b

+ 148 - 4
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);
+	}
 }

+ 38 - 38
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);
-
-			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 (lines == Lines && cols == Cols) {
-					return;
-				}
-
-				resizeterm (lines, cols);
-			} else {
-				lines = Lines;
-				cols = Cols;
-			}
+			lines = Marshal.ReadInt32 (lines_ptr);
+			cols = Marshal.ReadInt32 (cols_ptr);
+
+			//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 (lines == Lines && cols == Cols) {
+			//		return;
+			//	}
+
+			//	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);

+ 24 - 6
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.
 	/// <summary>
 	/// 
 	/// </summary>
 	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.
+
+		/// <summary>
+		/// Specifies the initial console width.
+		/// </summary>
+		public const int WIDTH = 80;
+
+		/// <summary>
+		/// Specifies the initial console height.
+		/// </summary>
+		public const int HEIGHT = 25;
+
 		/// <summary>
 		/// 
 		/// </summary>
-		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 {
 		/// <summary>
 		/// 
 		/// </summary>
-		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 {
 		/// <summary>
 		/// 
 		/// </summary>
-		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 {
 		/// <summary>
 		/// 
 		/// </summary>
-		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 {
 		/// </summary>
 		public static void Clear ()
 		{
-			_buffer = new char [WindowWidth, WindowHeight];
+			_buffer = new char [BufferWidth, BufferHeight];
 			SetCursorPosition (0, 0);
 		}
 
@@ -905,7 +920,8 @@ namespace Terminal.Gui {
 		/// </summary>
 		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);
 		}
 
 		//

+ 113 - 7
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 {
 	/// <summary>
@@ -16,11 +19,14 @@ namespace Terminal.Gui {
 	/// </summary>
 	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)
 		{

+ 9 - 0
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

+ 139 - 0
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);
+	}
 }

+ 32 - 0
Terminal.Gui/Core/Clipboard.cs

@@ -0,0 +1,32 @@
+using NStack;
+using System;
+
+namespace Terminal.Gui {
+	/// <summary>
+	/// Provides cut, copy, and paste support for the clipboard with OS interaction.
+	/// </summary>
+	public static class Clipboard {
+		static ustring contents;
+
+		/// <summary>
+		/// Get or sets the operation system clipboard, otherwise the contents field.
+		/// </summary>
+		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;
+				}
+			}
+		}
+	}
+}

+ 28 - 0
Terminal.Gui/Core/ConsoleDriver.cs

@@ -613,15 +613,27 @@ namespace Terminal.Gui {
 		/// The current number of columns in the terminal.
 		/// </summary>
 		public abstract int Cols { get; }
+
 		/// <summary>
 		/// The current number of rows in the terminal.
 		/// </summary>
 		public abstract int Rows { get; }
+
+		/// <summary>
+		/// The current left in the terminal.
+		/// </summary>
+		public abstract int Left { get; }
+
 		/// <summary>
 		/// The current top in the terminal.
 		/// </summary>
 		public abstract int Top { get; }
 
+		/// <summary>
+		/// Get the operation system clipboard.
+		/// </summary>
+		public abstract IClipboard Clipboard { get; }
+
 		/// <summary>
 		/// 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 {
 		/// <returns>The current attribute.</returns>
 		public abstract Attribute GetAttribute ();
 	}
+
+	/// <summary>
+	/// Definition to interact with the OS clipboard.
+	/// </summary>
+	public interface IClipboard {
+		/// <summary>
+		/// Sets the operation system clipboard.
+		/// </summary>
+		/// <param name="text"></param>
+		void SetClipboardData (string text);
+		/// <summary>
+		/// Get the operation system clipboard.
+		/// </summary>
+		/// <returns></returns>
+		string GetClipboardData ();
+	}
 }

+ 0 - 15
Terminal.Gui/Views/Clipboard.cs

@@ -1,15 +0,0 @@
-using System;
-using NStack;
-
-namespace Terminal.Gui {
-	/// <summary>
-	/// Provides cut, copy, and paste support for the clipboard. 
-	/// NOTE: Currently not implemented.
-	/// </summary>
-	public static class Clipboard {
-		/// <summary>
-		/// 
-		/// </summary>
-		public static ustring Contents { get; set; }
-	}
-}

+ 1 - 1
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);

+ 17 - 0
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 ();
+		}
+	}
+}

+ 168 - 0
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 ();
+		}
 	}
 }

+ 3 - 3
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.");
 			}