瀏覽代碼

Merge remote-tracking branch 'upstream/main' into color-text-view

tznind 4 年之前
父節點
當前提交
2b5c83d3dc
共有 45 個文件被更改,包括 3011 次插入767 次删除
  1. 1 1
      ReactiveExample/ReactiveExample.csproj
  2. 276 8
      Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs
  3. 38 38
      Terminal.Gui/ConsoleDrivers/CursesDriver/binding.cs
  4. 24 6
      Terminal.Gui/ConsoleDrivers/FakeDriver/FakeConsole.cs
  5. 119 9
      Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs
  6. 19 3
      Terminal.Gui/ConsoleDrivers/NetDriver.cs
  7. 141 0
      Terminal.Gui/ConsoleDrivers/WindowsDriver.cs
  8. 11 7
      Terminal.Gui/Core/Application.cs
  9. 67 0
      Terminal.Gui/Core/Clipboard/Clipboard.cs
  10. 100 0
      Terminal.Gui/Core/Clipboard/ClipboardBase.cs
  11. 44 0
      Terminal.Gui/Core/Clipboard/IClipboard.cs
  12. 12 0
      Terminal.Gui/Core/ConsoleDriver.cs
  13. 21 9
      Terminal.Gui/Core/MainLoop.cs
  14. 20 17
      Terminal.Gui/Core/View.cs
  15. 31 22
      Terminal.Gui/Core/Window.cs
  16. 19 2
      Terminal.Gui/Terminal.Gui.csproj
  17. 0 15
      Terminal.Gui/Views/Clipboard.cs
  18. 2 1
      Terminal.Gui/Views/FrameView.cs
  19. 9 0
      Terminal.Gui/Views/Label.cs
  20. 5 3
      Terminal.Gui/Views/ListView.cs
  21. 8 1
      Terminal.Gui/Views/TextField.cs
  22. 1 1
      Terminal.Gui/Views/TextView.cs
  23. 8 2
      UICatalog/Scenarios/LabelsAsButtons.cs
  24. 14 6
      UICatalog/UICatalog.cs
  25. 112 0
      UnitTests/AllViewsTests.cs
  26. 3 0
      UnitTests/ApplicationTests.cs
  27. 32 2
      UnitTests/AssemblyInfo.cs
  28. 147 0
      UnitTests/AttributeTests.cs
  29. 331 0
      UnitTests/ClipboardTests.cs
  30. 186 0
      UnitTests/ConsoleDriverTests.cs
  31. 7 0
      UnitTests/DimTests.cs
  32. 111 20
      UnitTests/GraphViewTests.cs
  33. 97 32
      UnitTests/MainLoopTests.cs
  34. 8 1
      UnitTests/PosTests.cs
  35. 6 0
      UnitTests/ScenarioTests.cs
  36. 60 20
      UnitTests/ScrollBarViewTests.cs
  37. 28 0
      UnitTests/TabViewTests.cs
  38. 482 465
      UnitTests/TableViewTests.cs
  39. 118 8
      UnitTests/TextFieldTests.cs
  40. 6 0
      UnitTests/TextFormatterTests.cs
  41. 28 9
      UnitTests/TextValidateFieldTests.cs
  42. 77 57
      UnitTests/TextViewTests.cs
  43. 15 0
      UnitTests/TreeViewTests.cs
  44. 2 2
      UnitTests/UnitTests.csproj
  45. 165 0
      UnitTests/ViewTests.cs

+ 1 - 1
ReactiveExample/ReactiveExample.csproj

@@ -6,7 +6,7 @@
     <ItemGroup>
         <PackageReference Include="Pharmacist.MsBuild" Version="2.0.8" PrivateAssets="all" />
         <PackageReference Include="Pharmacist.Common" Version="2.0.8" />
-        <PackageReference Include="Terminal.Gui" Version="1.0.0.*" />
+        <PackageReference Include="Terminal.Gui" Version="1.1.1.*" />
         <PackageReference Include="ReactiveUI.Fody" Version="13.3.2" />
         <PackageReference Include="ReactiveUI" Version="13.3.2" />
     </ItemGroup>

+ 276 - 8
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;
@@ -74,10 +77,10 @@ namespace Terminal.Gui {
 		public override void Refresh ()
 		{
 			Curses.refresh ();
-			//if (Curses.CheckWinChange ()) {
-			//	Clip = new Rect (0, 0, Cols, Rows);
-			//	TerminalResized?.Invoke ();
-			//}
+			if (Curses.CheckWinChange ()) {
+				Clip = new Rect (0, 0, Cols, Rows);
+				TerminalResized?.Invoke ();
+			}
 		}
 
 		public override void UpdateCursor () => Refresh ();
@@ -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,16 @@ namespace Terminal.Gui {
 				break;
 			}
 
+			if (RuntimeInformation.IsOSPlatform (OSPlatform.OSX)) {
+				clipboard = new MacOSXClipboard ();
+			} else {
+				if (Is_WSL_Platform ()) {
+					clipboard = new WSLClipboard ();
+				} else {
+					clipboard = new CursesClipboard ();
+				}
+			}
+
 			Curses.raw ();
 			Curses.noecho ();
 
@@ -835,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) {
@@ -1046,4 +1068,250 @@ namespace Terminal.Gui {
 			return true;
 		}
 	}
+
+	class CursesClipboard : ClipboardBase {
+		public override bool IsSupported => CheckSupport ();
+
+		bool CheckSupport ()
+		{
+			try {
+				var result = BashRunner.Run ("which xclip");
+				return BashRunner.FileExists (result);
+			} catch (Exception) {
+				// Permissions issue.
+				return false;
+			}
+		}
+
+		protected override string GetClipboardDataImpl ()
+		{
+			var tempFileName = System.IO.Path.GetTempFileName ();
+			try {
+				// BashRunner.Run ($"xsel -o --clipboard > {tempFileName}");
+				BashRunner.Run ($"xclip -selection clipboard -o > {tempFileName}");
+				return System.IO.File.ReadAllText (tempFileName);
+			} finally {
+				System.IO.File.Delete (tempFileName);
+			}
+		}
+
+		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);
+		}
+	}
+
+	static class BashRunner {
+		public static string Run (string commandLine, bool output = true, string inputText = "")
+		{
+			var arguments = $"-c \"{commandLine}\"";
+
+			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 (error);
+				}
+			} 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;
+				}
+			}
+		}
+
+		static bool DoubleWaitForExit (this System.Diagnostics.Process process)
+		{
+			var result = process.WaitForExit (500);
+			if (result) {
+				process.WaitForExit ();
+			}
+			return result;
+		}
+
+		public static bool FileExists (string value)
+		{
+			return !string.IsNullOrEmpty (value) && !value.Contains ("not found");
+		}
+	}
+
+	class MacOSXClipboard : ClipboardBase {
+		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 override bool IsSupported => CheckSupport ();
+
+		bool CheckSupport ()
+		{
+			var result = BashRunner.Run ("which pbcopy");
+			if (!BashRunner.FileExists (result)) {
+				return false;
+			}
+			result = BashRunner.Run ("which pbpaste");
+			return BashRunner.FileExists (result);
+		}
+
+		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);
+		}
+
+		protected override string GetClipboardDataImpl ()
+		{
+			var ptr = objc_msgSend (generalPasteboard, stringForTypeRegister, nsStringPboardType);
+			var charArray = objc_msgSend (ptr, utf8Register);
+			return Marshal.PtrToStringAnsi (charArray);
+		}
+
+		protected override void SetClipboardDataImpl (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);
+	}
+
+	class WSLClipboard : ClipboardBase {
+		public override bool IsSupported => CheckSupport ();
+
+		bool CheckSupport ()
+		{
+			var result = BashRunner.Run ("which powershell.exe");
+			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 ()
+		{
+			using (var powershell = new System.Diagnostics.Process {
+				StartInfo = new System.Diagnostics.ProcessStartInfo {
+					RedirectStandardOutput = true,
+					FileName = "powershell.exe",
+					Arguments = "-noprofile -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 = $"-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 ();
+			//}
+		}
+	}
 }

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

+ 119 - 9
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,17 @@ 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 {
+				if (CursesDriver.Is_WSL_Platform ()) {
+					Clipboard = new WSLClipboard ();
+				} else {
+					Clipboard = new CursesClipboard ();
+				}
+			}
 		}
 
 		bool needMove;
@@ -127,6 +141,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 +210,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)
@@ -255,10 +277,10 @@ namespace Terminal.Gui {
 		{
 		}
 
-		int currentAttribute;
+		Attribute currentAttribute;
 		public override void SetAttribute (Attribute c)
 		{
-			currentAttribute = c.Value;
+			currentAttribute = c;
 		}
 
 		Key MapKey (ConsoleKeyInfo keyInfo)
@@ -442,6 +464,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)
 		{

+ 19 - 3
Terminal.Gui/ConsoleDrivers/NetDriver.cs

@@ -183,7 +183,8 @@ namespace Terminal.Gui {
 						return;
 					}
 				} else {
-					largestWindowHeight = Math.Max (Console.BufferHeight, largestWindowHeight);
+					//largestWindowHeight = Math.Max (Console.BufferHeight, largestWindowHeight);
+					largestWindowHeight = Console.BufferHeight;
 					if (Console.BufferWidth != consoleDriver.Cols || largestWindowHeight != consoleDriver.Rows
 						|| Console.WindowHeight != lastWindowHeight) {
 						lastWindowHeight = Console.WindowHeight;
@@ -1076,12 +1077,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;
 
@@ -1092,7 +1095,19 @@ namespace Terminal.Gui {
 				IsWinPlatform = true;
 				NetWinConsole = new NetWinVTConsole ();
 			}
-			largestWindowHeight = Math.Max (Console.BufferHeight, largestWindowHeight);
+			//largestWindowHeight = Math.Max (Console.BufferHeight, largestWindowHeight);
+			largestWindowHeight = Console.BufferHeight;
+			if (IsWinPlatform) {
+				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 ();
+				}
+			}
 		}
 
 		// The format is rows, columns and 3 values on the last column: Rune, Attribute and Dirty Flag
@@ -1595,7 +1610,8 @@ namespace Terminal.Gui {
 					Console.WindowHeight);
 				top = 0;
 			} else {
-				largestWindowHeight = Math.Max (Console.BufferHeight, largestWindowHeight);
+				//largestWindowHeight = Math.Max (Console.BufferHeight, largestWindowHeight);
+				largestWindowHeight = Console.BufferHeight;
 				size = new Size (Console.BufferWidth, largestWindowHeight);
 			}
 			cols = size.Width;

+ 141 - 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,139 @@ namespace Terminal.Gui {
 			//}
 		}
 	}
+
+	class WindowsClipboard : ClipboardBase {
+		public override bool IsSupported => IsClipboardFormatAvailable (cfUnicodeText) ? true : false;
+
+		protected override string GetClipboardDataImpl ()
+		{
+			//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 ();
+			}
+		}
+
+		protected override void SetClipboardDataImpl (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);
+	}
 }

+ 11 - 7
Terminal.Gui/Core/Application.cs

@@ -208,14 +208,18 @@ namespace Terminal.Gui {
 		static void Init (Func<Toplevel> topLevelFactory, ConsoleDriver driver = null, IMainLoopDriver mainLoopDriver = null)
 		{
 			if (_initialized && driver == null) return;
-			
+
+			if (_initialized) {
+				throw new InvalidOperationException ("Init must be bracketed by Shutdown");
+			}
+
 			// Used only for start debugging on Unix.
-//#if DEBUG
-//			while (!System.Diagnostics.Debugger.IsAttached) {
-//				System.Threading.Thread.Sleep (100);
-//			}
-//			System.Diagnostics.Debugger.Break ();
-//#endif
+			//#if DEBUG
+			//			while (!System.Diagnostics.Debugger.IsAttached) {
+			//				System.Threading.Thread.Sleep (100);
+			//			}
+			//			System.Diagnostics.Debugger.Break ();
+			//#endif
 
 			// Reset all class variables (Application is a singleton).
 			ResetState ();

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

@@ -0,0 +1,67 @@
+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;
+				}
+			}
+		}
+
+		/// <summary>
+		/// Returns true if the environmental dependencies are in place to interact with the OS clipboard.
+		/// </summary>
+		public static bool IsSupported { get; } = Application.Driver.Clipboard.IsSupported;
+
+		/// <summary>
+		/// Gets the operation system clipboard if possible.
+		/// </summary>
+		/// <param name="result">Clipboard contents read</param>
+		/// <returns>true if it was possible to read the OS clipboard.</returns>
+		public static bool TryGetClipboardData (out string result)
+		{
+			if (Application.Driver.Clipboard.TryGetClipboardData (out result)) {
+				if (contents != result) {
+					contents = result;
+				}
+				return true;
+			}
+			return false;
+		}
+
+		/// <summary>
+		/// Sets the operation system clipboard if possible.
+		/// </summary>
+		/// <param name="text"></param>
+		/// <returns>True if the clipboard content was set successfully.</returns>
+		public static bool TrySetClipboardData (string text)
+		{
+			if (Application.Driver.Clipboard.TrySetClipboardData (text)) {
+				contents = text;
+				return true;
+			}
+			return false;
+		}
+	}
+}

+ 100 - 0
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 {
+	/// <summary>
+	/// Shared abstract class to enforce rules from the implementation of the <see cref="IClipboard"/> interface.
+	/// </summary>
+	public abstract class ClipboardBase : IClipboard {
+		/// <summary>
+		/// Returns true if the environmental dependencies are in place to interact with the OS clipboard
+		/// </summary>
+		public abstract bool IsSupported { get; }
+
+		/// <summary>
+		/// Get the operation system clipboard.
+		/// </summary>
+		/// <exception cref="NotSupportedException">Thrown if it was not possible to read the clipboard contents</exception>
+		public string GetClipboardData ()
+		{
+			try {
+				return GetClipboardDataImpl ();
+			} catch (Exception ex) {
+				throw new NotSupportedException ("Failed to read clipboard.", ex);
+			}
+		}
+
+		/// <summary>
+		/// Get the operation system clipboard.
+		/// </summary>
+		protected abstract string GetClipboardDataImpl ();
+
+		/// <summary>
+		/// Sets the operation system clipboard.
+		/// </summary>
+		/// <param name="text"></param>
+		/// <exception cref="NotSupportedException">Thrown if it was not possible to set the clipboard contents</exception>
+		public void SetClipboardData (string text)
+		{
+			try {
+				SetClipboardDataImpl (text);
+			} catch (Exception ex) {
+				throw new NotSupportedException ("Failed to write to clipboard.", ex);
+			}
+		}
+
+		/// <summary>
+		/// Sets the operation system clipboard.
+		/// </summary>
+		/// <param name="text"></param>
+		protected abstract void SetClipboardDataImpl (string text);
+
+		/// <summary>
+		/// Gets the operation system clipboard if possible.
+		/// </summary>
+		/// <param name="result">Clipboard contents read</param>
+		/// <returns>true if it was possible to read the OS clipboard.</returns>
+		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;
+			}
+		}
+
+		/// <summary>
+		/// Sets the operation system clipboard if possible.
+		/// </summary>
+		/// <param name="text"></param>
+		/// <returns>True if the clipboard content was set successfully</returns>
+		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;
+			}
+		}
+	}
+}

+ 44 - 0
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 {
+	/// <summary>
+	/// Definition to interact with the OS clipboard.
+	/// </summary>
+	public interface IClipboard {
+		/// <summary>
+		/// Returns true if the environmental dependencies are in place to interact with the OS clipboard.
+		/// </summary>
+		bool IsSupported { get; }
+
+		/// <summary>
+		/// Get the operation system clipboard.
+		/// </summary>
+		/// <exception cref="NotSupportedException">Thrown if it was not possible to read the clipboard contents.</exception>
+		string GetClipboardData ();
+
+		/// <summary>
+		/// Gets the operation system clipboard if possible.
+		/// </summary>
+		/// <param name="result">Clipboard contents read</param>
+		/// <returns>true if it was possible to read the OS clipboard.</returns>
+		bool TryGetClipboardData (out string result);
+
+		/// <summary>
+		/// Sets the operation system clipboard.
+		/// </summary>
+		/// <param name="text"></param>
+		/// <exception cref="NotSupportedException">Thrown if it was not possible to set the clipboard contents.</exception>
+		void SetClipboardData (string text);
+
+		/// <summary>
+		/// Sets the operation system clipboard if possible.
+		/// </summary>
+		/// <param name="text"></param>
+		/// <returns>True if the clipboard content was set successfully.</returns>
+		bool TrySetClipboardData (string text);
+	}
+}

+ 12 - 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.

+ 21 - 9
Terminal.Gui/Core/MainLoop.cs

@@ -31,7 +31,7 @@ namespace Terminal.Gui {
 		bool EventsPending (bool wait);
 
 		/// <summary>
-		/// The interation function.
+		/// The iteration function.
 		/// </summary>
 		void MainIteration ();
 	}
@@ -107,22 +107,30 @@ namespace Terminal.Gui {
 		///   Removes an idle handler added with <see cref="AddIdle(Func{bool})"/> from processing.
 		/// </summary>
 		/// <param name="token">A token returned by <see cref="AddIdle(Func{bool})"/></param>
-		public void RemoveIdle (Func<bool> token)
+		/// Returns <c>true</c>if the idle handler is successfully removed; otherwise, <c>false</c>.
+		///  This method also returns <c>false</c> if the idle handler is not found.
+		public bool RemoveIdle (Func<bool> token)
 		{
 			lock (token)
-				idleHandlers.Remove (token);
+				return idleHandlers.Remove (token);
 		}
 
 		void AddTimeout (TimeSpan time, Timeout timeout)
 		{
-			timeouts.Add ((DateTime.UtcNow + time).Ticks, timeout);
+			var k = (DateTime.UtcNow + time).Ticks;
+			lock (timeouts) {
+				while (timeouts.ContainsKey (k)) {
+					k = (DateTime.UtcNow + time).Ticks;
+				}
+			}
+			timeouts.Add (k, timeout);
 		}
 
 		/// <summary>
 		///   Adds a timeout to the mainloop.
 		/// </summary>
 		/// <remarks>
-		///   When time time specified passes, the callback will be invoked.
+		///   When time specified passes, the callback will be invoked.
 		///   If the callback returns true, the timeout will be reset, repeating
 		///   the invocation. If it returns false, the timeout will stop and be removed.
 		///
@@ -147,12 +155,15 @@ namespace Terminal.Gui {
 		/// <remarks>
 		///   The token parameter is the value returned by AddTimeout.
 		/// </remarks>
-		public void RemoveTimeout (object token)
+		/// Returns <c>true</c>if the timeout is successfully removed; otherwise, <c>false</c>.
+		/// This method also returns <c>false</c> if the timeout is not found.
+		public bool RemoveTimeout (object token)
 		{
 			var idx = timeouts.IndexOfValue (token as Timeout);
 			if (idx == -1)
-				return;
+				return false;
 			timeouts.RemoveAt (idx);
+			return true;
 		}
 
 		void RunTimers ()
@@ -160,8 +171,9 @@ namespace Terminal.Gui {
 			long now = DateTime.UtcNow.Ticks;
 			var copy = timeouts;
 			timeouts = new SortedList<long, Timeout> ();
-			foreach (var k in copy.Keys) {
-				var timeout = copy [k];
+			foreach (var t in copy) {
+				var k = t.Key;
+				var timeout = t.Value;
 				if (k < now) {
 					if (timeout.Callback (this))
 						AddTimeout (timeout.Span, timeout);

+ 20 - 17
Terminal.Gui/Core/View.cs

@@ -2098,17 +2098,20 @@ namespace Terminal.Gui {
 
 		bool SetWidthHeight (Rect nBounds)
 		{
-			bool aSize;
+			bool aSize = false;
 			var canSizeW = SetWidth (nBounds.Width, out int rW);
 			var canSizeH = SetHeight (nBounds.Height, out int rH);
-			if (canSizeW && canSizeH) {
+			if (canSizeW) {
 				aSize = true;
-				Bounds = nBounds;
 				width = rW;
+			}
+			if (canSizeH) {
+				aSize = true;
 				height = rH;
+			}
+			if (aSize) {
+				Bounds = new Rect (Bounds.X, Bounds.Y, canSizeW ? rW : Bounds.Width, canSizeH ? rH : Bounds.Height);
 				textFormatter.Size = Bounds.Size;
-			} else {
-				aSize = false;
 			}
 
 			return aSize;
@@ -2266,15 +2269,13 @@ namespace Terminal.Gui {
 			return true;
 		}
 
-		bool CanSetWidth (int desiredWidth, out int resultWidth, out int currentWidth)
+		bool CanSetWidth (int desiredWidth, out int resultWidth)
 		{
 			int w = desiredWidth;
-			currentWidth = Width != null ? Width.Anchor (0) : 0;
 			bool canSetWidth;
 			if (Width is Dim.DimCombine || Width is Dim.DimView || Width is Dim.DimFill) {
 				// It's a Dim.DimCombine and so can't be assigned. Let it have it's width anchored.
 				w = Width.Anchor (w);
-				currentWidth = Width.Anchor (w);
 				canSetWidth = false;
 			} else if (Width is Dim.DimFactor factor) {
 				// Tries to get the SuperView width otherwise the view width.
@@ -2283,7 +2284,6 @@ namespace Terminal.Gui {
 					sw -= Frame.X;
 				}
 				w = Width.Anchor (sw);
-				currentWidth = Width.Anchor (sw);
 				canSetWidth = false;
 			} else {
 				canSetWidth = true;
@@ -2293,15 +2293,13 @@ namespace Terminal.Gui {
 			return canSetWidth;
 		}
 
-		bool CanSetHeight (int desiredHeight, out int resultHeight, out int currentHeight)
+		bool CanSetHeight (int desiredHeight, out int resultHeight)
 		{
 			int h = desiredHeight;
-			currentHeight = Height != null ? Height.Anchor (0) : 0;
 			bool canSetHeight;
 			if (Height is Dim.DimCombine || Height is Dim.DimView || Height is Dim.DimFill) {
 				// It's a Dim.DimCombine and so can't be assigned. Let it have it's height anchored.
 				h = Height.Anchor (h);
-				currentHeight = Height.Anchor (h);
 				canSetHeight = false;
 			} else if (Height is Dim.DimFactor factor) {
 				// Tries to get the SuperView height otherwise the view height.
@@ -2310,7 +2308,6 @@ namespace Terminal.Gui {
 					sh -= Frame.Y;
 				}
 				h = Height.Anchor (sh);
-				currentHeight = Height.Anchor (sh);
 				canSetHeight = false;
 			} else {
 				canSetHeight = true;
@@ -2328,7 +2325,7 @@ namespace Terminal.Gui {
 		/// <returns><c>true</c> if the width can be directly assigned, <c>false</c> otherwise.</returns>
 		public bool SetWidth (int desiredWidth, out int resultWidth)
 		{
-			return CanSetWidth (desiredWidth, out resultWidth, out _);
+			return CanSetWidth (desiredWidth, out resultWidth);
 		}
 
 		/// <summary>
@@ -2339,7 +2336,7 @@ namespace Terminal.Gui {
 		/// <returns><c>true</c> if the height can be directly assigned, <c>false</c> otherwise.</returns>
 		public bool SetHeight (int desiredHeight, out int resultHeight)
 		{
-			return CanSetHeight (desiredHeight, out resultHeight, out _);
+			return CanSetHeight (desiredHeight, out resultHeight);
 		}
 
 		/// <summary>
@@ -2349,7 +2346,10 @@ namespace Terminal.Gui {
 		/// <returns><c>true</c> if the width can be directly assigned, <c>false</c> otherwise.</returns>
 		public bool GetCurrentWidth (out int currentWidth)
 		{
-			return CanSetWidth (0, out _, out currentWidth);
+			SetRelativeLayout (SuperView == null ? Frame : SuperView.Frame);
+			currentWidth = Frame.Width;
+
+			return CanSetWidth (0, out _);
 		}
 
 		/// <summary>
@@ -2359,7 +2359,10 @@ namespace Terminal.Gui {
 		/// <returns><c>true</c> if the height can be directly assigned, <c>false</c> otherwise.</returns>
 		public bool GetCurrentHeight (out int currentHeight)
 		{
-			return CanSetHeight (0, out _, out currentHeight);
+			SetRelativeLayout (SuperView == null ? Frame : SuperView.Frame);
+			currentHeight = Frame.Height;
+
+			return CanSetHeight (0, out _);
 		}
 	}
 }

+ 31 - 22
Terminal.Gui/Core/Window.cs

@@ -23,6 +23,7 @@ namespace Terminal.Gui {
 	public class Window : Toplevel {
 		View contentView;
 		ustring title;
+		int padding;
 
 		/// <summary>
 		/// The title to be displayed for this window.
@@ -52,7 +53,7 @@ namespace Terminal.Gui {
 		/// <param name="frame">Superview-relative rectangle specifying the location and size</param>
 		/// <param name="title">Title</param>
 		/// <remarks>
-		/// This constructor intitalizes a Window with a <see cref="LayoutStyle"/> of <see cref="LayoutStyle.Absolute"/>. Use constructors
+		/// This constructor initializes a Window with a <see cref="LayoutStyle"/> of <see cref="LayoutStyle.Absolute"/>. Use constructors
 		/// that do not take <c>Rect</c> parameters to initialize a Window with <see cref="LayoutStyle.Computed"/>. 
 		/// </remarks>
 		public Window (Rect frame, ustring title = null) : this (frame, title, padding: 0)
@@ -64,7 +65,7 @@ namespace Terminal.Gui {
 		/// </summary>
 		/// <param name="title">Title.</param>
 		/// <remarks>
-		///   This constructor intitalize a View with a <see cref="LayoutStyle"/> of <see cref="LayoutStyle.Computed"/>. 
+		///   This constructor initializes a View with a <see cref="LayoutStyle"/> of <see cref="LayoutStyle.Computed"/>. 
 		///   Use <see cref="View.X"/>, <see cref="View.Y"/>, <see cref="View.Width"/>, and <see cref="View.Height"/> properties to dynamically control the size and location of the view.
 		/// </remarks>
 		public Window (ustring title = null) : this (title, padding: 0)
@@ -76,7 +77,6 @@ namespace Terminal.Gui {
 		/// </summary>
 		public Window () : this (title: null) { }
 
-		int padding;
 		/// <summary>
 		/// Initializes a new instance of the <see cref="Window"/> using <see cref="LayoutStyle.Absolute"/> positioning with the specified frame for its location, with the specified frame padding,
 		/// and an optional title.
@@ -85,40 +85,48 @@ namespace Terminal.Gui {
 		/// <param name="padding">Number of characters to use for padding of the drawn frame.</param>
 		/// <param name="title">Title</param>
 		/// <remarks>
-		/// This constructor intitalizes a Window with a <see cref="LayoutStyle"/> of <see cref="LayoutStyle.Absolute"/>. Use constructors
+		/// This constructor initializes a Window with a <see cref="LayoutStyle"/> of <see cref="LayoutStyle.Absolute"/>. Use constructors
 		/// that do not take <c>Rect</c> parameters to initialize a Window with  <see cref="LayoutStyle"/> of <see cref="LayoutStyle.Computed"/> 
 		/// </remarks>
 		public Window (Rect frame, ustring title = null, int padding = 0) : base (frame)
 		{
-			this.Title = title;
-			int wb = 2 * (1 + padding);
-			this.padding = padding;
-			var cFrame = new Rect (1 + padding, 1 + padding, frame.Width - wb, frame.Height - wb);
-			contentView = new ContentView (cFrame);
-			base.Add (contentView);
+			Initialize (title, frame, padding);
 		}
 
 		/// <summary>
-		/// Initializes a new instance of the <see cref="Window"/> using <see cref="LayoutStyle.Absolute"/> positioning with the specified frame for its location, with the specified frame padding,
+		/// Initializes a new instance of the <see cref="Window"/> using <see cref="LayoutStyle.Computed"/> positioning,
 		/// and an optional title.
 		/// </summary>
 		/// <param name="padding">Number of characters to use for padding of the drawn frame.</param>
 		/// <param name="title">Title.</param>
 		/// <remarks>
-		///   This constructor intitalize a View with a <see cref="LayoutStyle"/> of <see cref="LayoutStyle.Computed"/>. 
+		///   This constructor initializes a View with a <see cref="LayoutStyle"/> of <see cref="LayoutStyle.Computed"/>. 
 		///   Use <see cref="View.X"/>, <see cref="View.Y"/>, <see cref="View.Width"/>, and <see cref="View.Height"/> properties to dynamically control the size and location of the view.
 		/// </remarks>
 		public Window (ustring title = null, int padding = 0) : base ()
 		{
-			this.Title = title;
-			int wb = 1 + padding;
+			Initialize (title, Rect.Empty, padding);
+		}
+
+		void Initialize (ustring title, Rect frame, int padding = 0)
+		{
+			ColorScheme = Colors.Base;
+			Title = title;
+			int wb;
+			if (frame == Rect.Empty) {
+				wb = 1 + padding;
+				contentView = new ContentView () {
+					X = wb,
+					Y = wb,
+					Width = Dim.Fill (wb),
+					Height = Dim.Fill (wb)
+				};
+			} else {
+				wb = 2 * (1 + padding);
+				var cFrame = new Rect (1 + padding, 1 + padding, frame.Width - wb, frame.Height - wb);
+				contentView = new ContentView (cFrame);
+			}
 			this.padding = padding;
-			contentView = new ContentView () {
-				X = wb,
-				Y = wb,
-				Width = Dim.Fill (wb),
-				Height = Dim.Fill (wb)
-			};
 			base.Add (contentView);
 		}
 
@@ -179,7 +187,7 @@ namespace Terminal.Gui {
 
 			var savedClip = ClipToBounds ();
 
-			// Redraw our contenetView
+			// Redraw our contentView
 			// TODO: smartly constrict contentView.Bounds to just be what intersects with the 'bounds' we were passed
 			contentView.Redraw (contentView.Bounds);
 			Driver.Clip = savedClip;
@@ -216,7 +224,8 @@ namespace Terminal.Gui {
 			// a pending mouse event activated.
 
 			int nx, ny;
-			if (!dragPosition.HasValue && mouseEvent.Flags == (MouseFlags.Button1Pressed)) {
+			if (!dragPosition.HasValue && (mouseEvent.Flags == MouseFlags.Button1Pressed
+				|| mouseEvent.Flags == (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition))) {
 				// Only start grabbing if the user clicks on the title bar.
 				if (mouseEvent.Y == 0) {
 					start = new Point (mouseEvent.X, mouseEvent.Y);

+ 19 - 2
Terminal.Gui/Terminal.Gui.csproj

@@ -1,4 +1,4 @@
-<Project Sdk="Microsoft.NET.Sdk">
+<Project Sdk="Microsoft.NET.Sdk">
   <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
     <DebugType></DebugType>
   </PropertyGroup>
@@ -40,9 +40,26 @@
     <Title>Terminal.Gui is a framework for creating console user interfaces</Title>
 
     <PackageReleaseNotes>
+      v1.1.2
+      * testing
+
+      v1.1.1
+      * Fixes #1307 - MainLoop timeouts duplicate keys error.
+
+      v1.1.0
+      * TextView CursorLeft/CursorRight change focus when at start/end #1271
+      * Fixes #1273 - Add direction summaries
+      * Fixes #1276 - Added TextDirection constructor to View and Label and improving AutoSize
+      * Fixes #1272 - Fixes Linux/Mac window sizing
+      * Fixes #1281 - Fixed CursesDriver colors. Added BasicColors scenario
+      * Fixes #1266 - Using Ctrl+PageUp/PageDown to allowing navigate through windows.
+      * TableView - adds last column dividing line #1289
+      * Fixes #1291. Combining two PosAbsolute or two DimAbsolute result on a PosAbsolute or DimAbsolute
+      * Changed Console.WriteLine to ITestOutputHelper in tests #1305
+
       v1.0.0
       * Version 1.0 Release!!! - Thank you to @migueldeicaza, @tig, @bdisp, @tznind, @jmprricone, and many more!
-      
+
       v1.0.0-rc.13
       * NEW CONTROL: GraphView - thanks @tznind!
       * Fixes #1256 - OutConsoleGridView no longer works - ENTER does not work

+ 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; }
-	}
-}

+ 2 - 1
Terminal.Gui/Views/FrameView.cs

@@ -9,6 +9,7 @@
 //  - Does not support IEnumerable
 // Any udpates done here should probably be done in Window as well; TODO: Merge these classes
 
+using System;
 using System.Linq;
 using NStack;
 
@@ -50,7 +51,7 @@ namespace Terminal.Gui {
 		/// <param name="title">Title.</param>
 		public FrameView (Rect frame, ustring title = null) : base (frame)
 		{
-			var cFrame = new Rect (1, 1, frame.Width - 2, frame.Height - 2);
+			var cFrame = new Rect (1, 1, Math.Max (frame.Width - 2, 0), Math.Max (frame.Height - 2, 0));
 			this.title = title;
 			contentView = new ContentView (cFrame);
 			Initialize ();

+ 9 - 0
Terminal.Gui/Views/Label.cs

@@ -23,6 +23,7 @@ namespace Terminal.Gui {
 		/// <inheritdoc/>
 		public Label ()
 		{
+			Initialize ();
 		}
 
 		/// <inheritdoc/>
@@ -33,6 +34,7 @@ namespace Terminal.Gui {
 		/// <inheritdoc/>
 		public Label (ustring text) : base (text)
 		{
+			Initialize ();
 		}
 
 		/// <inheritdoc/>
@@ -43,12 +45,19 @@ namespace Terminal.Gui {
 		/// <inheritdoc/>
 		public Label (int x, int y, ustring text) : base (x, y, text)
 		{
+			Initialize ();
 		}
 
 		/// <inheritdoc/>
 		public Label (ustring text, TextDirection direction)
 			: base (text, direction)
 		{
+			Initialize ();
+		}
+
+		void Initialize ()
+		{
+			AutoSize = true;
 		}
 
 		/// <summary>

+ 5 - 3
Terminal.Gui/Views/ListView.cs

@@ -468,7 +468,7 @@ namespace Terminal.Gui {
 		public virtual bool MovePageDown ()
 		{
 			var n = (selected + Frame.Height);
-			if (n > source.Count)
+			if (n >= source.Count)
 				n = source.Count - 1;
 			if (n != selected) {
 				selected = n;
@@ -557,9 +557,11 @@ namespace Terminal.Gui {
 		/// <returns></returns>
 		public virtual bool MoveEnd ()
 		{
-			if (selected != source.Count - 1) {
+			if (source.Count > 0 && selected != source.Count - 1) {
 				selected = source.Count - 1;
-				top = selected;
+				if (top + selected > Frame.Height - 1) {
+					top = selected;
+				}
 				OnSelectedChanged ();
 				SetNeedsDisplay ();
 			}

+ 8 - 1
Terminal.Gui/Views/TextField.cs

@@ -143,6 +143,9 @@ namespace Terminal.Gui {
 
 				var newText = OnTextChanging (value.Split ("\n") [0]);
 				if (newText.Cancel) {
+					if (point > text.Count) {
+						point = text.Count;
+					}
 					return;
 				}
 				text = TextModel.ToRunes (newText.NewText);
@@ -347,7 +350,11 @@ namespace Terminal.Gui {
 						return true;
 
 					point--;
-					SetText (text.GetRange (0, oldCursorPos - 1).Concat (text.GetRange (oldCursorPos, text.Count - oldCursorPos)));
+					if (oldCursorPos < text.Count) {
+						SetText (text.GetRange (0, oldCursorPos - 1).Concat (text.GetRange (oldCursorPos, text.Count - oldCursorPos)));
+					} else {
+						SetText (text.GetRange (0, oldCursorPos - 1));
+					}
 					Adjust ();
 				} else {
 					DeleteSelectedText ();

+ 1 - 1
Terminal.Gui/Views/TextView.cs

@@ -1691,7 +1691,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);

+ 8 - 2
UICatalog/Scenarios/LabelsAsButtons.cs

@@ -117,8 +117,14 @@ namespace UICatalog {
 				CanFocus = true,
 			};
 			Win.Add (removeLabel);
-			// This in intresting test case because `moveBtn` and below are laid out relative to this one!
-			removeLabel.Clicked += () => Win.Remove (removeLabel);
+			// This in interesting test case because `moveBtn` and below are laid out relative to this one!
+			removeLabel.Clicked += () => {
+				// Now this throw a InvalidOperationException on the TopologicalSort method as is expected.
+				//Win.Remove (removeLabel);
+
+				removeLabel.Visible = false;
+				Win.SetNeedsDisplay ();
+			};
 
 			var computedFrame = new FrameView ("Computed Layout") {
 				X = 0,

+ 14 - 6
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;
 			}
 		}
 	}

+ 112 - 0
UnitTests/AllViewsTests.cs

@@ -0,0 +1,112 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using Xunit;
+using System.IO;
+
+namespace Terminal.Gui.Views {
+	public class AllViewsTests {
+		[Fact]
+		public void AllViews_Tests_All_Constructors ()
+		{
+			Application.Init (new FakeDriver (), new FakeMainLoop (() => FakeConsole.ReadKey (true)));
+
+			foreach (var type in GetAllViewClassesCollection ()) {
+				Assert.True (Constructors_FullTest (type));
+			}
+
+			Application.Shutdown ();
+		}
+
+		public bool Constructors_FullTest (Type type)
+		{
+			foreach (var ctor in type.GetConstructors ()) {
+				if (type.IsGenericType && type.IsTypeDefinition) {
+					List<Type> gTypes = new List<Type> ();
+
+					foreach (var args in type.GetGenericArguments ()) {
+						gTypes.Add (typeof (object));
+					}
+					type = type.MakeGenericType (gTypes.ToArray ());
+
+					Assert.IsType (type, (View)Activator.CreateInstance (type));
+
+				} else {
+					ParameterInfo [] paramsInfo = ctor.GetParameters ();
+					Type paramType;
+					List<object> pTypes = new List<object> ();
+
+					if (type.IsGenericType) {
+						foreach (var args in type.GetGenericArguments ()) {
+							paramType = args.GetType ();
+							if (args.Name == "T") {
+								pTypes.Add (typeof (object));
+							} else {
+								AddArguments (paramType, pTypes);
+							}
+						}
+					}
+
+					foreach (var p in paramsInfo) {
+						paramType = p.ParameterType;
+						if (p.HasDefaultValue) {
+							pTypes.Add (p.DefaultValue);
+						} else {
+							AddArguments (paramType, pTypes);
+						}
+
+					}
+
+					if (type.IsGenericType && !type.IsTypeDefinition) {
+						Assert.IsType (type, (View)Activator.CreateInstance (type));
+					} else {
+						Assert.IsType (type, ctor.Invoke (pTypes.ToArray ()));
+					}
+				}
+			}
+
+			return true;
+		}
+
+		private static void AddArguments (Type paramType, List<object> pTypes)
+		{
+			if (paramType == typeof (Rect)) {
+				pTypes.Add (Rect.Empty);
+			} else if (paramType == typeof (NStack.ustring)) {
+				pTypes.Add (NStack.ustring.Empty);
+			} else if (paramType == typeof (int)) {
+				pTypes.Add (0);
+			} else if (paramType == typeof (bool)) {
+				pTypes.Add (true);
+			} else if (paramType.Name == "IList") {
+				pTypes.Add (new List<object> ());
+			} else if (paramType.Name == "View") {
+				var top = new Toplevel ();
+				var view = new View ();
+				top.Add (view);
+				pTypes.Add (view);
+			} else if (paramType.Name == "View[]") {
+				pTypes.Add (new View [] { });
+			} else if (paramType.Name == "Stream") {
+				pTypes.Add (new MemoryStream ());
+			} else if (paramType.Name == "String") {
+				pTypes.Add (string.Empty);
+			} else if (paramType.Name == "TreeView`1[T]") {
+				pTypes.Add (string.Empty);
+			} else {
+				pTypes.Add (null);
+			}
+		}
+
+		List<Type> GetAllViewClassesCollection ()
+		{
+			List<Type> types = new List<Type> ();
+			foreach (Type type in typeof (View).Assembly.GetTypes ()
+			 .Where (myType => myType.IsClass && !myType.IsAbstract && myType.IsPublic && myType.IsSubclassOf (typeof (View)))) {
+				types.Add (type);
+			}
+			return types;
+		}
+	}
+}

+ 3 - 0
UnitTests/ApplicationTests.cs

@@ -368,6 +368,9 @@ namespace Terminal.Gui.Core {
 			};
 
 			Application.Run (top);
+
+			// Shutdown must be called to safely clean up Application if Init has been called
+			Application.Shutdown ();
 		}
 	}
 }

+ 32 - 2
UnitTests/AssemblyInfo.cs

@@ -1,4 +1,34 @@
-using Xunit;
+using System;
+using System.Diagnostics;
+using System.Reflection;
+using Terminal.Gui;
+using Xunit;
 
 // Since Application is a singleton we can't run tests in parallel
-[assembly: CollectionBehavior (DisableTestParallelization = true)]
+[assembly: CollectionBehavior (DisableTestParallelization = true)]
+
+// This class enables test functions annotaed with the [AutoInitShutdown] attribute to 
+// automatically call Application.Init before called and Application.Shutdown after
+// 
+// This is necessary because a) Application is a singleton and Init/Shutdown must be called
+// as a pair, and b) all unit test functions should be atomic.
+[AttributeUsage (AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
+public class AutoInitShutdown : Xunit.Sdk.BeforeAfterTestAttribute {
+
+	static bool _init = false;
+	public override void Before (MethodInfo methodUnderTest)
+	{
+		if (_init) {
+			throw new InvalidOperationException ("After did not run.");
+		}
+
+		Application.Init (new FakeDriver (), new FakeMainLoop (() => FakeConsole.ReadKey (true)));
+		_init = true;
+	}
+
+	public override void After (MethodInfo methodUnderTest)
+	{
+		Application.Shutdown ();
+		_init = false;
+	}
+}

+ 147 - 0
UnitTests/AttributeTests.cs

@@ -0,0 +1,147 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Terminal.Gui;
+using Xunit;
+
+// Alias Console to MockConsole so we don't accidentally use Console
+using Console = Terminal.Gui.FakeConsole;
+
+namespace Terminal.Gui.ConsoleDrivers {
+	public class AttributeTests {
+		[Fact]
+		public void Constuctors_Constuct ()
+		{
+			var driver = new FakeDriver ();
+			Application.Init (driver, new FakeMainLoop (() => FakeConsole.ReadKey (true)));
+			driver.Init (() => { });
+
+			// Test parameterless constructor
+			var attr = new Attribute ();
+
+			Assert.Equal (default (int), attr.Value);
+			Assert.Equal (default (Color), attr.Foreground);
+			Assert.Equal (default (Color), attr.Background);
+
+			// Test value, foreground, background
+			var value = 42;
+			var fg = new Color ();
+			fg = Color.Red;
+
+			var bg = new Color ();
+			bg = Color.Blue;
+
+			attr = new Attribute (value, fg, bg);
+
+			Assert.Equal (value, attr.Value);
+			Assert.Equal (fg, attr.Foreground);
+			Assert.Equal (bg, attr.Background);
+
+			// value, foreground, background
+			attr = new Attribute (fg, bg);
+
+			Assert.Equal (fg, attr.Foreground);
+			Assert.Equal (bg, attr.Background);
+
+			driver.End ();
+			Application.Shutdown ();
+		}
+
+		[Fact]
+		public void Implicit_Assign ()
+		{
+			var driver = new FakeDriver ();
+			Application.Init (driver, new FakeMainLoop (() => FakeConsole.ReadKey (true)));
+			driver.Init (() => { });
+
+			var attr = new Attribute ();
+
+			var value = 42;
+			var fg = new Color ();
+			fg = Color.Red;
+
+			var bg = new Color ();
+			bg = Color.Blue;
+
+			// Test converstion to int
+			attr = new Attribute (value, fg, bg);
+			int value_implicit = (int)attr.Value;
+			Assert.Equal (value, value_implicit);
+
+			// Test converstion from int
+			attr = value;
+			Assert.Equal (value, attr.Value);
+
+			driver.End ();
+			Application.Shutdown ();
+		}
+
+		[Fact]
+		public void Make_Asserts_IfNotInit ()
+		{
+			var fg = new Color ();
+			fg = Color.Red;
+
+			var bg = new Color ();
+			bg = Color.Blue;
+
+			Assert.Throws<InvalidOperationException> (() => Attribute.Make (fg, bg));
+		}
+
+		[Fact]
+		public void Make_Creates ()
+		{
+			var driver = new FakeDriver ();
+			Application.Init (driver, new FakeMainLoop (() => FakeConsole.ReadKey (true)));
+			driver.Init (() => { });
+
+			var fg = new Color ();
+			fg = Color.Red;
+
+			var bg = new Color ();
+			bg = Color.Blue;
+
+			var attr =  Attribute.Make (fg, bg);
+
+			Assert.Equal (fg, attr.Foreground);
+			Assert.Equal (bg, attr.Background);
+
+			driver.End ();
+			Application.Shutdown ();
+		}
+
+		[Fact]
+		public void Get_Asserts_IfNotInit ()
+		{
+			Assert.Throws<InvalidOperationException> (() => Attribute.Get ());
+		}
+
+		[Fact]
+		public void Get_Gets ()
+		{
+			var driver = new FakeDriver ();
+			Application.Init (driver, new FakeMainLoop (() => FakeConsole.ReadKey (true)));
+			driver.Init (() => { });
+
+			var value = 42;
+			var fg = new Color ();
+			fg = Color.Red;
+
+			var bg = new Color ();
+			bg = Color.Blue;
+
+			var attr = new Attribute (value, fg, bg);
+
+			driver.SetAttribute (attr);
+
+			var ret_attr = Attribute.Get ();
+
+			Assert.Equal (value, ret_attr.Value);
+			Assert.Equal (fg, ret_attr.Foreground);
+			Assert.Equal (bg, ret_attr.Background);
+
+			driver.End ();
+			Application.Shutdown ();
+		}
+	}
+}

+ 331 - 0
UnitTests/ClipboardTests.cs

@@ -0,0 +1,331 @@
+using System.Diagnostics;
+using System.Runtime.InteropServices;
+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;
+
+			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)));
+
+			if (Clipboard.IsSupported) {
+				Assert.True (Clipboard.IsSupported);
+			} else {
+				Assert.False (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 ();
+
+			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 ();
+		}
+
+		[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.";
+			if (Clipboard.IsSupported) {
+				Assert.True (Clipboard.TrySetClipboardData (clipText));
+			} else {
+				Assert.False (Clipboard.TrySetClipboardData (clipText));
+			}
+
+			Application.Iteration += () => Application.RequestStop ();
+
+			Application.Run ();
+
+			if (Clipboard.IsSupported) {
+				Assert.Equal (clipText, Clipboard.Contents);
+			} else {
+				Assert.NotEqual (clipText, Clipboard.Contents);
+			}
+
+			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.";
+			var exit = false;
+
+			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)) {
+					if (Is_WSL_Platform ()) {
+						try {
+							using (Process bash = new Process {
+								StartInfo = new ProcessStartInfo {
+									FileName = "powershell.exe",
+									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;
+						}
+						Application.RequestStop ();
+						return;
+					}
+					if (exit = xclipExists () == false) {
+						// xclip doesn't exist then exit.
+						Application.RequestStop ();
+						return;
+					}
+
+					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 ();
+
+			if (!exit) {
+				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 = "";
+			var exit = false;
+
+			Application.Iteration += () => {
+				Clipboard.Contents = clipText;
+
+				if (RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) {
+					using (Process pwsh = new Process {
+						StartInfo = new ProcessStartInfo {
+							RedirectStandardOutput = true,
+							FileName = "powershell.exe",
+							Arguments = "-noprofile -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)) {
+					if (Is_WSL_Platform ()) {
+						try {
+							using (Process bash = new Process {
+								StartInfo = new ProcessStartInfo {
+									RedirectStandardOutput = true,
+									FileName = "powershell.exe",
+									Arguments = "-noprofile -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 ();
+					}
+
+					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 ();
+
+			if (!exit) {
+				Assert.Equal (clipText, clipReadText);
+			}
+
+			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 ()
+		{
+			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;
+			}
+		}
+	}
+}

+ 186 - 0
UnitTests/ConsoleDriverTests.cs

@@ -23,6 +23,9 @@ namespace Terminal.Gui.ConsoleDrivers {
 			Assert.Equal (Console.BufferWidth, driver.Cols);
 			Assert.Equal (Console.BufferHeight, driver.Rows);
 			driver.End ();
+
+			// Shutdown must be called to safely clean up Application if Init has been called
+			Application.Shutdown ();
 		}
 
 		[Fact]
@@ -46,6 +49,9 @@ namespace Terminal.Gui.ConsoleDrivers {
 			Assert.Equal (0, Console.CursorTop);
 			Assert.Equal (ConsoleColor.Gray, Console.ForegroundColor);
 			Assert.Equal (ConsoleColor.Black, Console.BackgroundColor);
+
+			// Shutdown must be called to safely clean up Application if Init has been called
+			Application.Shutdown ();
 		}
 
 		[Fact]
@@ -67,6 +73,9 @@ namespace Terminal.Gui.ConsoleDrivers {
 			Assert.Equal (ConsoleColor.Gray, Console.ForegroundColor);
 			Assert.Equal (ConsoleColor.Black, Console.BackgroundColor);
 			driver.End ();
+
+			// Shutdown must be called to safely clean up Application if Init has been called
+			Application.Shutdown ();
 		}
 
 		[Fact]
@@ -94,6 +103,9 @@ namespace Terminal.Gui.ConsoleDrivers {
 			Application.Run ();
 
 			Assert.False (wasKeyPressed);
+
+			// Shutdown must be called to safely clean up Application if Init has been called
+			Application.Shutdown ();
 		}
 
 		[Fact]
@@ -133,6 +145,9 @@ namespace Terminal.Gui.ConsoleDrivers {
 			Application.Run ();
 
 			Assert.Equal ("MockKeyPresses", rText);
+
+			// Shutdown must be called to safely clean up Application if Init has been called
+			Application.Shutdown ();
 		}
 
 		[Fact]
@@ -226,6 +241,177 @@ namespace Terminal.Gui.ConsoleDrivers {
 			Application.Run ();
 
 			Assert.Equal (key, lastKey);
+
+			// 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 ();
 		}
 	}
 }

+ 7 - 0
UnitTests/DimTests.cs

@@ -636,6 +636,9 @@ namespace Terminal.Gui.Core {
 			Application.Run (top);
 
 			Assert.Equal (20, count);
+
+			// Shutdown must be called to safely clean up Application if Init has been called
+			Application.Shutdown ();
 		}
 
 		[Fact]
@@ -693,6 +696,10 @@ namespace Terminal.Gui.Core {
 			Application.Run (top);
 
 			Assert.Equal (0, count);
+
+			// Shutdown must be called to safely clean up Application if Init has been called
+			Application.Shutdown ();
+
 		}
 	}
 }

+ 111 - 20
UnitTests/GraphViewTests.cs

@@ -8,6 +8,7 @@ using Point = Terminal.Gui.Point;
 using Attribute = Terminal.Gui.Attribute;
 using System.Text;
 using System.Text.RegularExpressions;
+using Xunit.Abstractions;
 
 namespace Terminal.Gui.Views {
 		
@@ -77,7 +78,7 @@ namespace Terminal.Gui.Views {
 		}
 
 #pragma warning disable xUnit1013 // Public method should be marked as test
-		public static void AssertDriverContentsAre (string expectedLook)
+		public static void AssertDriverContentsAre (string expectedLook, ITestOutputHelper output)
 		{
 #pragma warning restore xUnit1013 // Public method should be marked as test
 
@@ -108,8 +109,8 @@ namespace Terminal.Gui.Views {
 				expectedLook = expectedLook.Replace ("\r\n", "\n");
 				actualLook = actualLook.Replace ("\r\n", "\n");
 
-				Console.WriteLine ("Expected:" + Environment.NewLine + expectedLook);
-				Console.WriteLine ("But Was:" + Environment.NewLine + actualLook);
+				output?.WriteLine ("Expected:" + Environment.NewLine + expectedLook);
+				output?.WriteLine ("But Was:" + Environment.NewLine + actualLook);
 
 				Assert.Equal (expectedLook, actualLook);
 			}
@@ -354,6 +355,9 @@ namespace Terminal.Gui.Views {
 			var ex = Assert.Throws<Exception>(()=>gv.Redraw (gv.Bounds));
 
 			Assert.Equal ("CellSize cannot be 0", ex.Message);
+
+			// Shutdown must be called to safely clean up Application if Init has been called
+			Application.Shutdown ();
 		}
 
 		
@@ -446,6 +450,9 @@ namespace Terminal.Gui.Views {
 			// The screen space the graph will be rendered into should
 			// not overspill the margins
 			Assert.Equal (new Rect (5, 0, 45, 28), graphScreenBounds);
+
+			// Shutdown must be called to safely clean up Application if Init has been called
+			Application.Shutdown ();
 		}
 
 		/// <summary>
@@ -492,6 +499,9 @@ namespace Terminal.Gui.Views {
 			// The screen space the graph will be rendered into should
 			// not overspill the margins
 			Assert.Equal (new Rect (5, 0, 45, 28), graphScreenBounds);
+
+			// Shutdown must be called to safely clean up Application if Init has been called
+			Application.Shutdown ();
 		}
 
 		private class FakeSeries : ISeries {
@@ -514,6 +524,12 @@ namespace Terminal.Gui.Views {
 
 	public class MultiBarSeriesTests{
 
+		readonly ITestOutputHelper output;
+
+		public MultiBarSeriesTests(ITestOutputHelper output)
+		{
+			this.output = output;
+		}
 
 		[Fact]
 		public void MultiBarSeries_BarSpacing(){
@@ -543,6 +559,9 @@ namespace Terminal.Gui.Views {
 			// user passes 1 color only but asks for 5 bars
 			var ex = Assert.Throws<ArgumentException>(()=>new MultiBarSeries(5,7,1,colors));
 			Assert.Equal("Number of colors must match the number of bars (Parameter 'numberOfBarsPerCategory')",ex.Message);
+
+			// Shutdown must be called to safely clean up Application if Init has been called
+			Application.Shutdown ();
 		}
 
 
@@ -563,6 +582,9 @@ namespace Terminal.Gui.Views {
 			Assert.Equal(series.SubSeries.ElementAt(0).OverrideBarColor,colors[0]);
 			Assert.Equal(series.SubSeries.ElementAt(1).OverrideBarColor,colors[1]);
 			Assert.Equal(series.SubSeries.ElementAt(2).OverrideBarColor,colors[2]);
+
+			// Shutdown must be called to safely clean up Application if Init has been called
+			Application.Shutdown ();
 		}
 
 
@@ -643,7 +665,10 @@ namespace Terminal.Gui.Views {
  │  MM  MM  MM
  ┼──┬M──┬M──┬M──────
    heytherebob  ";
-			GraphViewTests.AssertDriverContentsAre (looksLike);
+			GraphViewTests.AssertDriverContentsAre (looksLike, output);
+
+			// Shutdown must be called to safely clean up Application if Init has been called
+			Application.Shutdown ();
 		}
 	}
 
@@ -700,6 +725,9 @@ namespace Terminal.Gui.Views {
 			// Screen position x=2 because bars are drawn every 1f of
 			// graph space and CellSize.X is 0.5f
 			Assert.Contains(2, axisX.LabelPoints);
+
+			// Shutdown must be called to safely clean up Application if Init has been called
+			Application.Shutdown ();
 		}
 
 
@@ -750,9 +778,10 @@ namespace Terminal.Gui.Views {
 			Assert.Equal(0,barSeries.BarScreenEnds[0].Y);
 			Assert.Equal(9,barSeries.BarScreenStarts[1].Y);
 			Assert.Equal(0,barSeries.BarScreenEnds[1].Y);
-		}
-
 
+			// Shutdown must be called to safely clean up Application if Init has been called
+			Application.Shutdown ();
+		}
 
 		[Fact]
 		public void TestOneLongOneShortHorizontalBars_WithOffset(){
@@ -812,6 +841,9 @@ namespace Terminal.Gui.Views {
 			// labels should align with the bars (same screen y axis point)
 			Assert.Contains(4, axisY.LabelPoints);
 			Assert.Contains(1, axisY.LabelPoints);
+
+			// Shutdown must be called to safely clean up Application if Init has been called
+			Application.Shutdown ();
 		}
 
 		private class FakeBarSeries : BarSeries{
@@ -889,6 +921,9 @@ namespace Terminal.Gui.Views {
 
 			Assert.InRange(axis.LabelPoints.Max(),0,49);
 			Assert.InRange(axis.LabelPoints.Min(),0,49);
+
+			// Shutdown must be called to safely clean up Application if Init has been called
+			Application.Shutdown ();
 		}
 
 		[Fact]
@@ -909,6 +944,9 @@ namespace Terminal.Gui.Views {
 
 			Assert.InRange(axis.LabelPoints.Max(),0,49);
 			Assert.InRange(axis.LabelPoints.Min(),0,49);
+
+			// Shutdown must be called to safely clean up Application if Init has been called
+			Application.Shutdown ();
 		}
 
 		[Fact]
@@ -930,6 +968,9 @@ namespace Terminal.Gui.Views {
 			// Axis lables should not be drawn in the margin
 			Assert.InRange(axis.LabelPoints.Max(),5,49);
 			Assert.InRange(axis.LabelPoints.Min(),5,49);
+
+			// Shutdown must be called to safely clean up Application if Init has been called
+			Application.Shutdown ();
 		}
 
 		#endregion
@@ -958,6 +999,9 @@ namespace Terminal.Gui.Views {
 
 			Assert.InRange(axis.LabelPoints.Max(),0,29);
 			Assert.InRange(axis.LabelPoints.Min(),0,29);
+
+			// Shutdown must be called to safely clean up Application if Init has been called
+			Application.Shutdown ();
 		}
 
 		[Fact]
@@ -979,6 +1023,9 @@ namespace Terminal.Gui.Views {
 			// Labels should not be drawn into the axis
 			Assert.InRange(axis.LabelPoints.Max(),0,19);
 			Assert.InRange(axis.LabelPoints.Min(),0,19);
+
+			// Shutdown must be called to safely clean up Application if Init has been called
+			Application.Shutdown ();
 		}
 
 		[Fact]
@@ -999,15 +1046,21 @@ namespace Terminal.Gui.Views {
 
 			Assert.InRange(axis.LabelPoints.Max(),0,29);
 			Assert.InRange(axis.LabelPoints.Min(),0,29);
+
+			// Shutdown must be called to safely clean up Application if Init has been called
+			Application.Shutdown ();
 		}
 
 		#endregion
-
-
-		
 	}
 
 	public class TextAnnotationTests {
+		readonly ITestOutputHelper output;
+
+		public TextAnnotationTests(ITestOutputHelper output)
+		{
+			this.output = output;
+		}
 
 		[Fact]
 		public void TestTextAnnotation_ScreenUnits()
@@ -1029,7 +1082,7 @@ namespace Terminal.Gui.Views {
 0┼┬┬┬┬┬┬┬┬
  0    5";
 
-			GraphViewTests.AssertDriverContentsAre (expected);
+			GraphViewTests.AssertDriverContentsAre (expected, output);
 
 			// user scrolls up one unit of graph space
 			gv.ScrollOffset = new PointF (0, 1f);
@@ -1046,7 +1099,10 @@ namespace Terminal.Gui.Views {
 1┼┬┬┬┬┬┬┬┬
  0    5";
 
-			GraphViewTests.AssertDriverContentsAre (expected);
+			GraphViewTests.AssertDriverContentsAre (expected, output);
+
+			// Shutdown must be called to safely clean up Application if Init has been called
+			Application.Shutdown ();
 		}
 
 
@@ -1070,7 +1126,7 @@ namespace Terminal.Gui.Views {
 0┼┬┬┬┬┬┬┬┬
  0    5";
 
-			GraphViewTests.AssertDriverContentsAre (expected);
+			GraphViewTests.AssertDriverContentsAre (expected, output);
 
 			// user scrolls up one unit of graph space
 			gv.ScrollOffset = new PointF (0, 1f);
@@ -1088,7 +1144,10 @@ namespace Terminal.Gui.Views {
 1┼┬┬┬┬┬┬┬┬
  0    5";
 
-			GraphViewTests.AssertDriverContentsAre (expected);
+			GraphViewTests.AssertDriverContentsAre (expected, output);
+
+			// Shutdown must be called to safely clean up Application if Init has been called
+			Application.Shutdown ();
 		}
 
 		[Fact]
@@ -1115,8 +1174,11 @@ namespace Terminal.Gui.Views {
 0┼┬┬┬┬┬┬┬┬
  0    5";
 
-			GraphViewTests.AssertDriverContentsAre (expected);
+			GraphViewTests.AssertDriverContentsAre (expected, output);
 
+
+			// Shutdown must be called to safely clean up Application if Init has been called
+			Application.Shutdown ();
 		}
 
 
@@ -1141,8 +1203,10 @@ namespace Terminal.Gui.Views {
 0┼┬┬┬┬┬┬┬┬
  0    5";
 
-			GraphViewTests.AssertDriverContentsAre (expected);
+			GraphViewTests.AssertDriverContentsAre (expected, output);
 
+			// Shutdown must be called to safely clean up Application if Init has been called
+			Application.Shutdown ();
 		}
 
 		[Theory]
@@ -1174,12 +1238,21 @@ namespace Terminal.Gui.Views {
 0┼┬┬┬┬┬┬┬┬
  0    5";
 
-			GraphViewTests.AssertDriverContentsAre (expected);
+			GraphViewTests.AssertDriverContentsAre (expected, output);
+
 
+			// Shutdown must be called to safely clean up Application if Init has been called
+			Application.Shutdown ();
 		}
 	}
 
 	public class LegendTests {
+		readonly ITestOutputHelper output;
+
+		public LegendTests(ITestOutputHelper output)
+		{
+			this.output = output;
+		}
 
 		[Fact]
 		public void LegendNormalUsage_WithBorder ()
@@ -1200,8 +1273,11 @@ namespace Terminal.Gui.Views {
 0┼┬┬┬┬┬┬┬┬
  0    5";
 
-			GraphViewTests.AssertDriverContentsAre (expected);
+			GraphViewTests.AssertDriverContentsAre (expected, output);
 
+
+			// Shutdown must be called to safely clean up Application if Init has been called
+			Application.Shutdown ();
 		}
 
 		[Fact]
@@ -1226,12 +1302,21 @@ namespace Terminal.Gui.Views {
 0┼┬┬┬┬┬┬┬┬
  0    5";
 
-			GraphViewTests.AssertDriverContentsAre (expected);
+			GraphViewTests.AssertDriverContentsAre (expected, output);
+
 
+			// Shutdown must be called to safely clean up Application if Init has been called
+			Application.Shutdown ();
 		}
 	}
 
 	public class PathAnnotationTests {
+		readonly ITestOutputHelper output;
+
+		public PathAnnotationTests( ITestOutputHelper output)
+		{
+			this.output = output;
+		}
 
 		[Fact]
 		public void PathAnnotation_Box()
@@ -1259,8 +1344,11 @@ namespace Terminal.Gui.Views {
 0┼┬┬┬┬┬┬┬┬
  0    5";
 
-			GraphViewTests.AssertDriverContentsAre (expected);
+			GraphViewTests.AssertDriverContentsAre (expected, output);
 
+
+			// Shutdown must be called to safely clean up Application if Init has been called
+			Application.Shutdown ();
 		}
 
 		[Fact]
@@ -1288,8 +1376,11 @@ namespace Terminal.Gui.Views {
 0┼┬┬┬┬┬┬┬┬
  0    5";
 
-			GraphViewTests.AssertDriverContentsAre (expected);
+			GraphViewTests.AssertDriverContentsAre (expected,output);
+
 
+			// Shutdown must be called to safely clean up Application if Init has been called
+			Application.Shutdown ();
 		}
 	}
 

+ 97 - 32
UnitTests/MainLoopTests.cs

@@ -5,6 +5,7 @@ using System.Globalization;
 using System.Linq;
 using System.Runtime.InteropServices.ComTypes;
 using System.Threading;
+using System.Threading.Tasks;
 using Terminal.Gui;
 using Xunit;
 using Xunit.Sdk;
@@ -33,28 +34,31 @@ namespace Terminal.Gui.Core {
 			ml.AddIdle (fnTrue);
 			ml.AddIdle (fnFalse);
 
-			ml.RemoveIdle (fnTrue);
+			Assert.True (ml.RemoveIdle (fnTrue));
 
-			// BUGBUG: This doens't throw or indicate an error. Ideally RemoveIdle would either 
-			// trhow an exception in this case, or return an error.
-			ml.RemoveIdle (fnTrue);
+			// BUGBUG: This doesn't throw or indicate an error. Ideally RemoveIdle would either 
+			// throw an exception in this case, or return an error.
+			// No. Only need to return a boolean.
+			Assert.False (ml.RemoveIdle (fnTrue));
 
-			ml.RemoveIdle (fnFalse);
+			Assert.True (ml.RemoveIdle (fnFalse));
 
 			// BUGBUG: This doesn't throw an exception or indicate an error. Ideally RemoveIdle would either 
-			// trhow an exception in this case, or return an error.
-			ml.RemoveIdle (fnFalse);
+			// throw an exception in this case, or return an error.
+			// No. Only need to return a boolean.
+			Assert.False (ml.RemoveIdle (fnFalse));
 
 			// Add again, but with dupe
 			ml.AddIdle (fnTrue);
 			ml.AddIdle (fnTrue);
 
-			ml.RemoveIdle (fnTrue);
-			ml.RemoveIdle (fnTrue);
+			Assert.True (ml.RemoveIdle (fnTrue));
+			Assert.True (ml.RemoveIdle (fnTrue));
 
 			// BUGBUG: This doesn't throw an exception or indicate an error. Ideally RemoveIdle would either 
-			// trhow an exception in this case, or return an error.
-			ml.RemoveIdle (fnTrue);
+			// throw an exception in this case, or return an error.
+			// No. Only need to return a boolean.
+			Assert.False (ml.RemoveIdle (fnTrue));
 		}
 
 		[Fact]
@@ -84,8 +88,7 @@ namespace Terminal.Gui.Core {
 				return true;
 			};
 
-			functionCalled = 0;
-			ml.RemoveIdle (fn);
+			Assert.False (ml.RemoveIdle (fn));
 			ml.MainIteration ();
 			Assert.Equal (0, functionCalled);
 		}
@@ -101,9 +104,8 @@ namespace Terminal.Gui.Core {
 				return true;
 			};
 
-			functionCalled = 0;
 			ml.AddIdle (fn);
-			ml.RemoveIdle (fn);
+			Assert.True (ml.RemoveIdle (fn));
 			ml.MainIteration ();
 			Assert.Equal (0, functionCalled);
 		}
@@ -119,21 +121,21 @@ namespace Terminal.Gui.Core {
 				return true;
 			};
 
-			functionCalled = 0;
 			ml.AddIdle (fn);
 			ml.AddIdle (fn);
 			ml.MainIteration ();
 			Assert.Equal (2, functionCalled);
 
 			functionCalled = 0;
-			ml.RemoveIdle (fn);
+			Assert.True (ml.RemoveIdle (fn));
 			ml.MainIteration ();
 			Assert.Equal (1, functionCalled);
 
 			functionCalled = 0;
-			ml.RemoveIdle (fn);
+			Assert.True (ml.RemoveIdle (fn));
 			ml.MainIteration ();
 			Assert.Equal (0, functionCalled);
+			Assert.False (ml.RemoveIdle (fn));
 		}
 
 		[Fact]
@@ -163,10 +165,11 @@ namespace Terminal.Gui.Core {
 			ml.AddIdle (fnStop);
 			ml.AddIdle (fn1);
 			ml.Run ();
-			ml.RemoveIdle (fnStop);
-			ml.RemoveIdle (fn1);
+			Assert.True (ml.RemoveIdle (fnStop));
+			Assert.False (ml.RemoveIdle (fn1));
 
 			Assert.Equal (10, functionCalled);
+			Assert.Equal (20, stopCount);
 		}
 
 		[Fact]
@@ -194,9 +197,9 @@ namespace Terminal.Gui.Core {
 			ml.AddIdle (fn1);
 			ml.AddIdle (fn1);
 			ml.Run ();
-			ml.RemoveIdle (fnStop);
-			ml.RemoveIdle (fn1);
-			ml.RemoveIdle (fn1);
+			Assert.True (ml.RemoveIdle (fnStop));
+			Assert.False (ml.RemoveIdle (fn1));
+			Assert.False (ml.RemoveIdle (fn1));
 
 			Assert.Equal (2, functionCalled);
 		}
@@ -217,7 +220,7 @@ namespace Terminal.Gui.Core {
 
 			ml.AddIdle (fn);
 			ml.Run ();
-			ml.RemoveIdle (fn);
+			Assert.True (ml.RemoveIdle (fn));
 
 			Assert.Equal (10, functionCalled);
 		}
@@ -237,10 +240,11 @@ namespace Terminal.Gui.Core {
 
 			var token = ml.AddTimeout (TimeSpan.FromMilliseconds (ms), callback);
 
-			ml.RemoveTimeout (token);
+			Assert.True (ml.RemoveTimeout (token));
 
 			// BUGBUG: This should probably fault?
-			ml.RemoveTimeout (token);
+			// Must return a boolean.
+			Assert.False (ml.RemoveTimeout (token));
 		}
 
 		[Fact]
@@ -258,11 +262,72 @@ namespace Terminal.Gui.Core {
 
 			var token = ml.AddTimeout (TimeSpan.FromMilliseconds (ms), callback);
 			ml.Run ();
-			ml.RemoveTimeout (token);
+			Assert.True (ml.RemoveTimeout (token));
 
 			Assert.Equal (1, callbackCount);
 		}
 
+		[Fact]
+		public async Task AddTimer_Duplicate_Keys_Not_Allowed ()
+		{
+			var ml = new MainLoop (new FakeMainLoop (() => FakeConsole.ReadKey (true)));
+			const int ms = 100;
+			object token1 = null, token2 = null;
+
+			var callbackCount = 0;
+			Func<MainLoop, bool> callback = (MainLoop loop) => {
+				callbackCount++;
+				if (callbackCount == 2) {
+					ml.Stop ();
+				}
+				return true;
+			};
+
+			var task1 = new Task (() => token1 = ml.AddTimeout (TimeSpan.FromMilliseconds (ms), callback));
+			var task2 = new Task (() => token2 = ml.AddTimeout (TimeSpan.FromMilliseconds (ms), callback));
+			Assert.Null (token1);
+			Assert.Null (token2);
+			task1.Start ();
+			task2.Start ();
+			ml.Run ();
+			Assert.NotNull (token1);
+			Assert.NotNull (token2);
+			await Task.WhenAll (task1, task2);
+			Assert.True (ml.RemoveTimeout (token1));
+			Assert.True (ml.RemoveTimeout (token2));
+
+			Assert.Equal (2, callbackCount);
+		}
+
+		[Fact]
+		public void AddTimer_In_Parallel_Wont_Throw ()
+		{
+			var ml = new MainLoop (new FakeMainLoop (() => FakeConsole.ReadKey (true)));
+			const int ms = 100;
+			object token1 = null, token2 = null;
+
+			var callbackCount = 0;
+			Func<MainLoop, bool> callback = (MainLoop loop) => {
+				callbackCount++;
+				if (callbackCount == 2) {
+					ml.Stop ();
+				}
+				return true;
+			};
+
+			Parallel.Invoke (
+				() => token1 = ml.AddTimeout (TimeSpan.FromMilliseconds (ms), callback),
+				() => token2 = ml.AddTimeout (TimeSpan.FromMilliseconds (ms), callback)
+			);
+			ml.Run ();
+			Assert.NotNull (token1);
+			Assert.NotNull (token2);
+			Assert.True (ml.RemoveTimeout (token1));
+			Assert.True (ml.RemoveTimeout (token2));
+
+			Assert.Equal (2, callbackCount);
+		}
+
 
 		class MillisecondTolerance : IEqualityComparer<TimeSpan> {
 			int _tolerance = 0;
@@ -293,7 +358,7 @@ namespace Terminal.Gui.Core {
 			// https://github.com/xunit/assert.xunit/pull/25
 			Assert.Equal<TimeSpan> (ms * callbackCount, watch.Elapsed, new MillisecondTolerance (100));
 
-			ml.RemoveTimeout (token);
+			Assert.True (ml.RemoveTimeout (token));
 			Assert.Equal (1, callbackCount);
 		}
 
@@ -321,7 +386,7 @@ namespace Terminal.Gui.Core {
 			// https://github.com/xunit/assert.xunit/pull/25
 			Assert.Equal<TimeSpan> (ms * callbackCount, watch.Elapsed, new MillisecondTolerance (100));
 
-			ml.RemoveTimeout (token);
+			Assert.True (ml.RemoveTimeout (token));
 			Assert.Equal (2, callbackCount);
 		}
 
@@ -349,7 +414,7 @@ namespace Terminal.Gui.Core {
 			};
 
 			var token = ml.AddTimeout (ms, callback);
-			ml.RemoveTimeout (token);
+			Assert.True (ml.RemoveTimeout (token));
 			ml.Run ();
 			Assert.Equal (0, callbackCount);
 		}
@@ -363,7 +428,7 @@ namespace Terminal.Gui.Core {
 			// Force stop if 10 iterations
 			var stopCount = 0;
 			Func<bool> fnStop = () => {
-				Thread.Sleep (10); // Sleep to enable timeer to fire
+				Thread.Sleep (10); // Sleep to enable timer to fire
 				stopCount++;
 				if (stopCount == 10) {
 					ml.Stop ();
@@ -382,7 +447,7 @@ namespace Terminal.Gui.Core {
 			ml.Run ();
 			Assert.Equal (1, callbackCount);
 			Assert.Equal (10, stopCount);
-			ml.RemoveTimeout (token);
+			Assert.False (ml.RemoveTimeout (token));
 		}
 
 		// Invoke Tests

+ 8 - 1
UnitTests/PosTests.cs

@@ -288,6 +288,8 @@ namespace Terminal.Gui.Core {
 			{
 				// Cleanup
 				Application.End (rs);
+				// Shutdown must be called to safely clean up Application if Init has been called
+				Application.Shutdown ();
 			}
 
 			// Test cases:
@@ -326,7 +328,6 @@ namespace Terminal.Gui.Core {
 			rs = Application.Begin (Application.Top);
 			Application.Run ();
 			cleanup (rs);
-
 		}
 
 		[Fact]
@@ -581,6 +582,9 @@ namespace Terminal.Gui.Core {
 			Application.Run (top);
 
 			Assert.Equal (20, count);
+
+			// Shutdown must be called to safely clean up Application if Init has been called
+			Application.Shutdown ();
 		}
 
 		[Fact]
@@ -638,6 +642,9 @@ namespace Terminal.Gui.Core {
 			Application.Run (top);
 
 			Assert.Equal (0, count);
+
+			// Shutdown must be called to safely clean up Application if Init has been called
+			Application.Shutdown ();
 		}
 	}
 }

+ 6 - 0
UnitTests/ScenarioTests.cs

@@ -77,6 +77,9 @@ namespace Terminal.Gui {
 
 				Application.End (rs);
 
+				// Shutdown must be called to safely clean up Application if Init has been called
+				Application.Shutdown ();
+
 				Assert.Equal (0, abortCount);
 				// # of key up events should match # of iterations
 				Assert.Equal (1, iterations);
@@ -140,6 +143,9 @@ namespace Terminal.Gui {
 			// Using variable in the left side of Assert.Equal/NotEqual give error. Must be used literals values.
 			//Assert.Equal (stackSize, iterations);
 
+			// Shutdown must be called to safely clean up Application if Init has been called
+			Application.Shutdown ();
+
 #if DEBUG_IDISPOSABLE
 			foreach (var inst in Responder.Instances) {
 				Assert.True (inst.WasDisposed);

+ 60 - 20
UnitTests/ScrollBarViewTests.cs

@@ -1,8 +1,51 @@
 using System;
+using System.Diagnostics;
+using System.Reflection;
 using Xunit;
 
 namespace Terminal.Gui.Views {
 	public class ScrollBarViewTests {
+
+		// This class enables test functions annoated 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
+		// as a pair, and b) all unit test functions should be atomic.
+		[AttributeUsage (AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
+		public class InitShutdown : Xunit.Sdk.BeforeAfterTestAttribute {
+
+			public override void Before (MethodInfo methodUnderTest)
+			{
+				Debug.WriteLine ($"Before: {methodUnderTest.Name}");
+
+				if (_hostView != null) {
+					throw new InvalidOperationException ("After did not run.");
+				}
+
+				Application.Init (new FakeDriver (), new FakeMainLoop (() => FakeConsole.ReadKey (true)));
+
+				var top = Application.Top;
+
+				ScrollBarViewTests._hostView = new HostView () {
+					Width = Dim.Fill (),
+					Height = Dim.Fill (),
+					Top = 0,
+					Lines = 30,
+					Left = 0,
+					Cols = 100
+				};
+
+				top.Add (ScrollBarViewTests._hostView);
+			}
+
+			public override void After (MethodInfo methodUnderTest)
+			{
+				Debug.WriteLine ($"After: {methodUnderTest.Name}");
+				ScrollBarViewTests._hostView = null;
+				Application.Shutdown ();
+			}
+		}
+
 		public class HostView : View {
 			public int Top { get; set; }
 			public int Lines { get; set; }
@@ -10,27 +53,9 @@ namespace Terminal.Gui.Views {
 			public int Cols { get; set; }
 		}
 
-		private HostView _hostView;
+		private static HostView _hostView;
 		private ScrollBarView _scrollBar;
-		private bool _added;
-
-		public ScrollBarViewTests ()
-		{
-			Application.Init (new FakeDriver (), new FakeMainLoop (() => FakeConsole.ReadKey (true)));
-
-			var top = Application.Top;
-
-			_hostView = new HostView () {
-				Width = Dim.Fill (),
-				Height = Dim.Fill (),
-				Top = 0,
-				Lines = 30,
-				Left = 0,
-				Cols = 100
-			};
-
-			top.Add (_hostView);
-		}
+		private  bool _added;
 
 		private void AddHandlers ()
 		{
@@ -80,6 +105,7 @@ namespace Terminal.Gui.Views {
 		}
 
 		[Fact]
+		[InitShutdown]
 		public void Hosting_A_Null_View_To_A_ScrollBarView_Throws_ArgumentNullException ()
 		{
 			Assert.Throws<ArgumentNullException> ("The host parameter can't be null.",
@@ -89,6 +115,7 @@ namespace Terminal.Gui.Views {
 		}
 
 		[Fact]
+		[InitShutdown]
 		public void Hosting_A_Null_SuperView_View_To_A_ScrollBarView_Throws_ArgumentNullException ()
 		{
 			Assert.Throws<ArgumentNullException> ("The host SuperView parameter can't be null.",
@@ -98,6 +125,7 @@ namespace Terminal.Gui.Views {
 		}
 
 		[Fact]
+		[InitShutdown]
 		public void Hosting_Two_Vertical_ScrollBarView_Throws_ArgumentException ()
 		{
 			var top = new Toplevel ();
@@ -111,6 +139,7 @@ namespace Terminal.Gui.Views {
 		}
 
 		[Fact]
+		[InitShutdown]
 		public void Hosting_Two_Horizontal_ScrollBarView_Throws_ArgumentException ()
 		{
 			var top = new Toplevel ();
@@ -124,6 +153,7 @@ namespace Terminal.Gui.Views {
 		}
 
 		[Fact]
+		[InitShutdown]
 		public void Scrolling_With_Default_Constructor_Do_Not_Scroll ()
 		{
 			var sbv = new ScrollBarView {
@@ -134,6 +164,7 @@ namespace Terminal.Gui.Views {
 		}
 
 		[Fact]
+		[InitShutdown]
 		public void Hosting_A_View_To_A_ScrollBarView ()
 		{
 			RemoveHandlers ();
@@ -159,6 +190,7 @@ namespace Terminal.Gui.Views {
 		}
 
 		[Fact]
+		[InitShutdown]
 		public void ChangedPosition_Update_The_Hosted_View ()
 		{
 			Hosting_A_View_To_A_ScrollBarView ();
@@ -173,6 +205,7 @@ namespace Terminal.Gui.Views {
 		}
 
 		[Fact]
+		[InitShutdown]
 		public void ChangedPosition_Scrolling ()
 		{
 			Hosting_A_View_To_A_ScrollBarView ();
@@ -199,6 +232,7 @@ namespace Terminal.Gui.Views {
 		}
 
 		[Fact]
+		[InitShutdown]
 		public void ChangedPosition_Negative_Value ()
 		{
 			Hosting_A_View_To_A_ScrollBarView ();
@@ -215,6 +249,7 @@ namespace Terminal.Gui.Views {
 		}
 
 		[Fact]
+		[InitShutdown]
 		public void DrawContent_Update_The_ScrollBarView_Position ()
 		{
 			Hosting_A_View_To_A_ScrollBarView ();
@@ -231,6 +266,7 @@ namespace Terminal.Gui.Views {
 		}
 
 		[Fact]
+		[InitShutdown]
 		public void OtherScrollBarView_Not_Null ()
 		{
 			Hosting_A_View_To_A_ScrollBarView ();
@@ -243,6 +279,7 @@ namespace Terminal.Gui.Views {
 		}
 
 		[Fact]
+		[InitShutdown]
 		public void ShowScrollIndicator_Check ()
 		{
 			Hosting_A_View_To_A_ScrollBarView ();
@@ -254,6 +291,7 @@ namespace Terminal.Gui.Views {
 		}
 
 		[Fact]
+		[InitShutdown]
 		public void KeepContentAlwaysInViewport_True ()
 		{
 			Hosting_A_View_To_A_ScrollBarView ();
@@ -293,6 +331,7 @@ namespace Terminal.Gui.Views {
 		}
 
 		[Fact]
+		[InitShutdown]
 		public void KeepContentAlwaysInViewport_False ()
 		{
 			Hosting_A_View_To_A_ScrollBarView ();
@@ -314,6 +353,7 @@ namespace Terminal.Gui.Views {
 		}
 
 		[Fact]
+		[InitShutdown]
 		public void AutoHideScrollBars_Check ()
 		{
 			Hosting_A_View_To_A_ScrollBarView ();

+ 28 - 0
UnitTests/TabViewTests.cs

@@ -38,6 +38,8 @@ namespace Terminal.Gui.Views {
 
 			Assert.Equal (2, tv.Tabs.Count);
 			Assert.Equal (tab2, tv.SelectedTab);
+
+			Application.Shutdown ();
 		}
 
 
@@ -55,6 +57,8 @@ namespace Terminal.Gui.Views {
 
 			Assert.Null (tv.SelectedTab);
 			Assert.Equal (0, tv.TabScrollOffset);
+
+			Application.Shutdown ();
 		}
 
 		[Fact]
@@ -73,6 +77,9 @@ namespace Terminal.Gui.Views {
 			// Asking to show tab2 should automatically move scroll offset accordingly
 			tv.SelectedTab = tab2;
 			Assert.Equal (1, tv.TabScrollOffset);
+
+			// Shutdown must be called to safely clean up Application if Init has been called
+			Application.Shutdown ();
 		}
 
 
@@ -98,6 +105,9 @@ namespace Terminal.Gui.Views {
 			Assert.Equal (1, called);
 			Assert.Equal (tab1, oldTab);
 			Assert.Equal (tab2, newTab);
+
+			// Shutdown must be called to safely clean up Application if Init has been called
+			Application.Shutdown ();
 		}
 		[Fact]
 		public void RemoveTab_ChangesSelection ()
@@ -108,6 +118,9 @@ namespace Terminal.Gui.Views {
 			tv.RemoveTab (tab1);
 
 			Assert.Equal (tab2, tv.SelectedTab);
+
+			// Shutdown must be called to safely clean up Application if Init has been called
+			Application.Shutdown ();
 		}
 
 		[Fact]
@@ -125,6 +138,9 @@ namespace Terminal.Gui.Views {
 			tv.RemoveTab (tab1);
 
 			Assert.Equal (tab2, tv.SelectedTab);
+
+			// Shutdown must be called to safely clean up Application if Init has been called
+			Application.Shutdown ();
 		}
 
 		[Fact]
@@ -137,6 +153,9 @@ namespace Terminal.Gui.Views {
 			tv.RemoveTab (tab2);
 
 			Assert.Null (tv.SelectedTab);
+
+			// Shutdown must be called to safely clean up Application if Init has been called
+			Application.Shutdown ();
 		}
 
 		[Fact]
@@ -171,6 +190,9 @@ namespace Terminal.Gui.Views {
 			// even though we go right 2 indexes the event should only be called once
 			Assert.Equal (1, called);
 			Assert.Equal (tab4, tv.SelectedTab);
+
+			// Shutdown must be called to safely clean up Application if Init has been called
+			Application.Shutdown ();
 		}
 
 		[Fact]
@@ -187,6 +209,9 @@ namespace Terminal.Gui.Views {
 			tv.AddTab (tab1, false);
 
 			Assert.Equal (2, tv.Tabs.Count);
+
+			// Shutdown must be called to safely clean up Application if Init has been called
+			Application.Shutdown ();
 		}
 
 
@@ -205,6 +230,9 @@ namespace Terminal.Gui.Views {
 
 			Assert.Equal (tab1, tv.SelectedTab);
 
+			// Shutdown must be called to safely clean up Application if Init has been called
+			Application.Shutdown ();
+
 		}
 
 		private void InitFakeDriver ()

+ 482 - 465
UnitTests/TableViewTests.cs

@@ -6,501 +6,518 @@ using System.Threading.Tasks;
 using Terminal.Gui;
 using Xunit;
 using System.Globalization;
+using Xunit.Abstractions;
 
 namespace Terminal.Gui.Views {
 
-	public class TableViewTests 
-	{
+	public class TableViewTests {
+		readonly ITestOutputHelper output;
 
-        [Fact]
-        public void EnsureValidScrollOffsets_WithNoCells()
-        {
-            var tableView = new TableView();
+		public TableViewTests (ITestOutputHelper output)
+		{
+			this.output = output;
+		}
+		[Fact]
+		public void EnsureValidScrollOffsets_WithNoCells ()
+		{
+			var tableView = new TableView ();
+
+			Assert.Equal (0, tableView.RowOffset);
+			Assert.Equal (0, tableView.ColumnOffset);
+
+			// Set empty table
+			tableView.Table = new DataTable ();
+
+			// Since table has no rows or columns scroll offset should default to 0
+			tableView.EnsureValidScrollOffsets ();
+			Assert.Equal (0, tableView.RowOffset);
+			Assert.Equal (0, tableView.ColumnOffset);
+		}
+
+
+
+		[Fact]
+		public void EnsureValidScrollOffsets_LoadSmallerTable ()
+		{
+			var tableView = new TableView ();
+			tableView.Bounds = new Rect (0, 0, 25, 10);
+
+			Assert.Equal (0, tableView.RowOffset);
+			Assert.Equal (0, tableView.ColumnOffset);
+
+			// Set big table
+			tableView.Table = BuildTable (25, 50);
+
+			// Scroll down and along
+			tableView.RowOffset = 20;
+			tableView.ColumnOffset = 10;
+
+			tableView.EnsureValidScrollOffsets ();
+
+			// The scroll should be valid at the moment
+			Assert.Equal (20, tableView.RowOffset);
+			Assert.Equal (10, tableView.ColumnOffset);
+
+			// Set small table
+			tableView.Table = BuildTable (2, 2);
+
+			// Setting a small table should automatically trigger fixing the scroll offsets to ensure valid cells
+			Assert.Equal (0, tableView.RowOffset);
+			Assert.Equal (0, tableView.ColumnOffset);
+
+
+			// Trying to set invalid indexes should not be possible
+			tableView.RowOffset = 20;
+			tableView.ColumnOffset = 10;
+
+			Assert.Equal (1, tableView.RowOffset);
+			Assert.Equal (1, tableView.ColumnOffset);
+		}
+
+		[Fact]
+		public void SelectedCellChanged_NotFiredForSameValue ()
+		{
+			var tableView = new TableView () {
+				Table = BuildTable (25, 50)
+			};
+
+			bool called = false;
+			tableView.SelectedCellChanged += (e) => { called = true; };
+
+			Assert.Equal (0, tableView.SelectedColumn);
+			Assert.False (called);
+
+			// Changing value to same as it already was should not raise an event
+			tableView.SelectedColumn = 0;
+
+			Assert.False (called);
 
-            Assert.Equal(0,tableView.RowOffset);
-            Assert.Equal(0,tableView.ColumnOffset);
+			tableView.SelectedColumn = 10;
+			Assert.True (called);
+		}
+
+
+
+		[Fact]
+		public void SelectedCellChanged_SelectedColumnIndexesCorrect ()
+		{
+			var tableView = new TableView () {
+				Table = BuildTable (25, 50)
+			};
+
+			bool called = false;
+			tableView.SelectedCellChanged += (e) => {
+				called = true;
+				Assert.Equal (0, e.OldCol);
+				Assert.Equal (10, e.NewCol);
+			};
+
+			tableView.SelectedColumn = 10;
+			Assert.True (called);
+		}
+
+		[Fact]
+		public void SelectedCellChanged_SelectedRowIndexesCorrect ()
+		{
+			var tableView = new TableView () {
+				Table = BuildTable (25, 50)
+			};
+
+			bool called = false;
+			tableView.SelectedCellChanged += (e) => {
+				called = true;
+				Assert.Equal (0, e.OldRow);
+				Assert.Equal (10, e.NewRow);
+			};
+
+			tableView.SelectedRow = 10;
+			Assert.True (called);
+		}
+
+		[Fact]
+		public void Test_SumColumnWidth_UnicodeLength ()
+		{
+			Assert.Equal (11, "hello there".Sum (c => Rune.ColumnWidth (c)));
+
+			// Creates a string with the peculiar (french?) r symbol
+			String surrogate = "Les Mise" + Char.ConvertFromUtf32 (Int32.Parse ("0301", NumberStyles.HexNumber)) + "rables";
+
+			// The unicode width of this string is shorter than the string length! 
+			Assert.Equal (14, surrogate.Sum (c => Rune.ColumnWidth (c)));
+			Assert.Equal (15, surrogate.Length);
+		}
 
-            // Set empty table
-            tableView.Table = new DataTable();
+		[Fact]
+		public void IsSelected_MultiSelectionOn_Vertical ()
+		{
+			var tableView = new TableView () {
+				Table = BuildTable (25, 50),
+				MultiSelect = true
+			};
+
+			// 3 cell vertical selection
+			tableView.SetSelection (1, 1, false);
+			tableView.SetSelection (1, 3, true);
+
+			Assert.False (tableView.IsSelected (0, 0));
+			Assert.False (tableView.IsSelected (1, 0));
+			Assert.False (tableView.IsSelected (2, 0));
+
+			Assert.False (tableView.IsSelected (0, 1));
+			Assert.True (tableView.IsSelected (1, 1));
+			Assert.False (tableView.IsSelected (2, 1));
+
+			Assert.False (tableView.IsSelected (0, 2));
+			Assert.True (tableView.IsSelected (1, 2));
+			Assert.False (tableView.IsSelected (2, 2));
+
+			Assert.False (tableView.IsSelected (0, 3));
+			Assert.True (tableView.IsSelected (1, 3));
+			Assert.False (tableView.IsSelected (2, 3));
+
+			Assert.False (tableView.IsSelected (0, 4));
+			Assert.False (tableView.IsSelected (1, 4));
+			Assert.False (tableView.IsSelected (2, 4));
+		}
 
-            // Since table has no rows or columns scroll offset should default to 0
-            tableView.EnsureValidScrollOffsets();
-            Assert.Equal(0,tableView.RowOffset);
-            Assert.Equal(0,tableView.ColumnOffset);
-        }
 
+		[Fact]
+		public void IsSelected_MultiSelectionOn_Horizontal ()
+		{
+			var tableView = new TableView () {
+				Table = BuildTable (25, 50),
+				MultiSelect = true
+			};
+
+			// 2 cell horizontal selection
+			tableView.SetSelection (1, 0, false);
+			tableView.SetSelection (2, 0, true);
+
+			Assert.False (tableView.IsSelected (0, 0));
+			Assert.True (tableView.IsSelected (1, 0));
+			Assert.True (tableView.IsSelected (2, 0));
+			Assert.False (tableView.IsSelected (3, 0));
+
+			Assert.False (tableView.IsSelected (0, 1));
+			Assert.False (tableView.IsSelected (1, 1));
+			Assert.False (tableView.IsSelected (2, 1));
+			Assert.False (tableView.IsSelected (3, 1));
+		}
 
 
-        [Fact]
-        public void EnsureValidScrollOffsets_LoadSmallerTable()
-        {
-            var tableView = new TableView();
-            tableView.Bounds = new Rect(0,0,25,10);
 
-            Assert.Equal(0,tableView.RowOffset);
-            Assert.Equal(0,tableView.ColumnOffset);
-
-            // Set big table
-            tableView.Table = BuildTable(25,50);
+		[Fact]
+		public void IsSelected_MultiSelectionOn_BoxSelection ()
+		{
+			var tableView = new TableView () {
+				Table = BuildTable (25, 50),
+				MultiSelect = true
+			};
+
+			// 4 cell horizontal in box 2x2
+			tableView.SetSelection (0, 0, false);
+			tableView.SetSelection (1, 1, true);
+
+			Assert.True (tableView.IsSelected (0, 0));
+			Assert.True (tableView.IsSelected (1, 0));
+			Assert.False (tableView.IsSelected (2, 0));
+
+			Assert.True (tableView.IsSelected (0, 1));
+			Assert.True (tableView.IsSelected (1, 1));
+			Assert.False (tableView.IsSelected (2, 1));
+
+			Assert.False (tableView.IsSelected (0, 2));
+			Assert.False (tableView.IsSelected (1, 2));
+			Assert.False (tableView.IsSelected (2, 2));
+		}
 
-            // Scroll down and along
-            tableView.RowOffset = 20;
-            tableView.ColumnOffset = 10;
-
-            tableView.EnsureValidScrollOffsets();
-
-            // The scroll should be valid at the moment
-            Assert.Equal(20,tableView.RowOffset);
-            Assert.Equal(10,tableView.ColumnOffset);
-
-            // Set small table
-            tableView.Table = BuildTable(2,2);
-
-            // Setting a small table should automatically trigger fixing the scroll offsets to ensure valid cells
-            Assert.Equal(0,tableView.RowOffset);
-            Assert.Equal(0,tableView.ColumnOffset);
-
-
-            // Trying to set invalid indexes should not be possible
-            tableView.RowOffset = 20;
-            tableView.ColumnOffset = 10;
-
-            Assert.Equal(1,tableView.RowOffset);
-            Assert.Equal(1,tableView.ColumnOffset);
-        }
-
-        [Fact]
-        public void SelectedCellChanged_NotFiredForSameValue()
-        {
-            var tableView = new TableView(){
-                Table = BuildTable(25,50)
-            };
-
-            bool called = false;
-            tableView.SelectedCellChanged += (e)=>{called=true;};
-
-            Assert.Equal(0,tableView.SelectedColumn);
-            Assert.False(called);
-            
-            // Changing value to same as it already was should not raise an event
-            tableView.SelectedColumn = 0;
-
-            Assert.False(called);
-
-            tableView.SelectedColumn = 10;
-            Assert.True(called);
-        }
-
-
-
-        [Fact]
-        public void SelectedCellChanged_SelectedColumnIndexesCorrect()
-        {
-            var tableView = new TableView(){
-                Table = BuildTable(25,50)
-            };
-
-            bool called = false;
-            tableView.SelectedCellChanged += (e)=>{
-                called=true;
-                Assert.Equal(0,e.OldCol);
-                Assert.Equal(10,e.NewCol);
-            };
-            
-            tableView.SelectedColumn = 10;
-            Assert.True(called);
-        }
-
-        [Fact]
-        public void SelectedCellChanged_SelectedRowIndexesCorrect()
-        {
-            var tableView = new TableView(){
-                Table = BuildTable(25,50)
-            };
-
-            bool called = false;
-            tableView.SelectedCellChanged += (e)=>{
-                called=true;
-                Assert.Equal(0,e.OldRow);
-                Assert.Equal(10,e.NewRow);
-            };
-            
-            tableView.SelectedRow = 10;
-            Assert.True(called);
-        }
-
-        [Fact]
-        public void Test_SumColumnWidth_UnicodeLength()
-        {
-            Assert.Equal(11,"hello there".Sum(c=>Rune.ColumnWidth(c)));
-
-            // Creates a string with the peculiar (french?) r symbol
-            String surrogate = "Les Mise" + Char.ConvertFromUtf32(Int32.Parse("0301", NumberStyles.HexNumber)) + "rables";
-
-            // The unicode width of this string is shorter than the string length! 
-            Assert.Equal(14,surrogate.Sum(c=>Rune.ColumnWidth(c)));
-            Assert.Equal(15,surrogate.Length);
-        }
-
-        [Fact]
-        public void IsSelected_MultiSelectionOn_Vertical()
-        {
-            var tableView = new TableView(){
-                Table = BuildTable(25,50),
-                MultiSelect = true
-            };
-
-            // 3 cell vertical selection
-            tableView.SetSelection(1,1,false);
-            tableView.SetSelection(1,3,true);
-
-            Assert.False(tableView.IsSelected(0,0));
-            Assert.False(tableView.IsSelected(1,0));
-            Assert.False(tableView.IsSelected(2,0));
-
-            Assert.False(tableView.IsSelected(0,1));
-            Assert.True(tableView.IsSelected(1,1));
-            Assert.False(tableView.IsSelected(2,1));
-
-            Assert.False(tableView.IsSelected(0,2));
-            Assert.True(tableView.IsSelected(1,2));
-            Assert.False(tableView.IsSelected(2,2));
-
-            Assert.False(tableView.IsSelected(0,3));
-            Assert.True(tableView.IsSelected(1,3));
-            Assert.False(tableView.IsSelected(2,3));
-
-            Assert.False(tableView.IsSelected(0,4));
-            Assert.False(tableView.IsSelected(1,4));
-            Assert.False(tableView.IsSelected(2,4));
-        }
-
-
-        [Fact]
-        public void IsSelected_MultiSelectionOn_Horizontal()
-        {
-            var tableView = new TableView(){
-                Table = BuildTable(25,50),
-                MultiSelect = true
-            };
-
-            // 2 cell horizontal selection
-            tableView.SetSelection(1,0,false);
-            tableView.SetSelection(2,0,true);
-
-            Assert.False(tableView.IsSelected(0,0));
-            Assert.True(tableView.IsSelected(1,0));
-            Assert.True(tableView.IsSelected(2,0));
-            Assert.False(tableView.IsSelected(3,0));
-
-            Assert.False(tableView.IsSelected(0,1));
-            Assert.False(tableView.IsSelected(1,1));
-            Assert.False(tableView.IsSelected(2,1));
-            Assert.False(tableView.IsSelected(3,1));
-        }
-
-
-
-        [Fact]
-        public void IsSelected_MultiSelectionOn_BoxSelection()
-        {
-            var tableView = new TableView(){
-                Table = BuildTable(25,50),
-                MultiSelect = true
-            };
-
-            // 4 cell horizontal in box 2x2
-            tableView.SetSelection(0,0,false);
-            tableView.SetSelection(1,1,true);
-
-            Assert.True(tableView.IsSelected(0,0));
-            Assert.True(tableView.IsSelected(1,0));
-            Assert.False(tableView.IsSelected(2,0));
-
-            Assert.True(tableView.IsSelected(0,1));
-            Assert.True(tableView.IsSelected(1,1));
-            Assert.False(tableView.IsSelected(2,1));
-
-            Assert.False(tableView.IsSelected(0,2));
-            Assert.False(tableView.IsSelected(1,2));
-            Assert.False(tableView.IsSelected(2,2));
-        }
-
-        [Fact]
-        public void PageDown_ExcludesHeaders()
-        {
+		[Fact]
+		public void PageDown_ExcludesHeaders ()
+		{
 
 			var driver = new FakeDriver ();
 			Application.Init (driver, new FakeMainLoop (() => FakeConsole.ReadKey (true)));
 			driver.Init (() => { });
 
 
-            var tableView = new TableView(){
-                Table = BuildTable(25,50),
-                MultiSelect = true,
-                Bounds = new Rect(0,0,10,5)
-            };
-
-            // Header should take up 2 lines
-            tableView.Style.ShowHorizontalHeaderOverline = false;
-            tableView.Style.ShowHorizontalHeaderUnderline = true;
-            tableView.Style.AlwaysShowHeaders = false;
+			var tableView = new TableView () {
+				Table = BuildTable (25, 50),
+				MultiSelect = true,
+				Bounds = new Rect (0, 0, 10, 5)
+			};
 
-            Assert.Equal(0,tableView.RowOffset);
+			// Header should take up 2 lines
+			tableView.Style.ShowHorizontalHeaderOverline = false;
+			tableView.Style.ShowHorizontalHeaderUnderline = true;
+			tableView.Style.AlwaysShowHeaders = false;
+
+			Assert.Equal (0, tableView.RowOffset);
+
+			tableView.ProcessKey (new KeyEvent (Key.PageDown, new KeyModifiers ()));
+
+			// window height is 5 rows 2 are header so page down should give 3 new rows
+			Assert.Equal (3, tableView.RowOffset);
+
+			// header is no longer visible so page down should give 5 new rows
+			tableView.ProcessKey (new KeyEvent (Key.PageDown, new KeyModifiers ()));
+
+			Assert.Equal (8, tableView.RowOffset);
+
+			// Shutdown must be called to safely clean up Application if Init has been called
+			Application.Shutdown ();
+		}
 
-            tableView.ProcessKey(new KeyEvent(Key.PageDown,new KeyModifiers()));
-
-            // window height is 5 rows 2 are header so page down should give 3 new rows
-            Assert.Equal(3,tableView.RowOffset);
-
-            // header is no longer visible so page down should give 5 new rows
-            tableView.ProcessKey(new KeyEvent(Key.PageDown,new KeyModifiers()));
-            
-            Assert.Equal(8,tableView.RowOffset);
-        }
-
-        [Fact]
-        public void DeleteRow_SelectAll_AdjustsSelectionToPreventOverrun()
-        {
-            // create a 4 by 4 table
-            var tableView = new TableView(){
-                Table = BuildTable(4,4),
-                MultiSelect = true,
-                Bounds = new Rect(0,0,10,5)
-            };
-
-            tableView.SelectAll();
-            Assert.Equal(16,tableView.GetAllSelectedCells().Count());
-
-            // delete one of the columns
-            tableView.Table.Columns.RemoveAt(2);
-
-            // table should now be 3x4
-            Assert.Equal(12,tableView.GetAllSelectedCells().Count());
-
-            // remove a row
-            tableView.Table.Rows.RemoveAt(1);
-
-            // table should now be 3x3
-            Assert.Equal(9,tableView.GetAllSelectedCells().Count());
-        }
-
-
-        [Fact]
-        public void DeleteRow_SelectLastRow_AdjustsSelectionToPreventOverrun()
-        {
-            // create a 4 by 4 table
-            var tableView = new TableView(){
-                Table = BuildTable(4,4),
-                MultiSelect = true,
-                Bounds = new Rect(0,0,10,5)
-            };
-
-            // select the last row
-            tableView.MultiSelectedRegions.Clear();
-            tableView.MultiSelectedRegions.Push(new TableView.TableSelection (new Point(0,3), new Rect(0,3,4,1)));
-
-            Assert.Equal(4,tableView.GetAllSelectedCells().Count());
-
-            // remove a row
-            tableView.Table.Rows.RemoveAt(0);
-
-            tableView.EnsureValidSelection();
-
-            // since the selection no longer exists it should be removed
-            Assert.Empty(tableView.MultiSelectedRegions);
-        }
-
-        [Theory]
-        [InlineData(true)]
-        [InlineData(false)]
-        public void GetAllSelectedCells_SingleCellSelected_ReturnsOne(bool multiSelect)
-        {
-            var tableView = new TableView(){
-                Table = BuildTable(3,3),
-                MultiSelect = multiSelect,
-                Bounds = new Rect(0,0,10,5)
-            };
-
-            tableView.SetSelection(1,1,false);
-
-            Assert.Single(tableView.GetAllSelectedCells());
-            Assert.Equal(new Point(1,1),tableView.GetAllSelectedCells().Single());
-        }
-
-
-        [Fact]
-        public void GetAllSelectedCells_SquareSelection_ReturnsFour()
-        {
-            var tableView = new TableView(){
-                Table = BuildTable(3,3),
-                MultiSelect = true,
-                Bounds = new Rect(0,0,10,5)
-            };
-
-            // move cursor to 1,1
-            tableView.SetSelection(1,1,false);
-            // spread selection across to 2,2 (e.g. shift+right then shift+down)
-            tableView.SetSelection(2,2,true);
-
-            var selected = tableView.GetAllSelectedCells().ToArray();
-
-            Assert.Equal(4,selected.Length);
-            Assert.Equal(new Point(1,1),selected[0]);
-            Assert.Equal(new Point(2,1),selected[1]);
-            Assert.Equal(new Point(1,2),selected[2]);
-            Assert.Equal(new Point(2,2),selected[3]);
-        }
-        
-
-        [Fact]
-        public void GetAllSelectedCells_SquareSelection_FullRowSelect()
-        {
-            var tableView = new TableView(){
-                Table = BuildTable(3,3),
-                MultiSelect = true,
-                FullRowSelect = true,
-                Bounds = new Rect(0,0,10,5)
-            };
-
-            // move cursor to 1,1
-            tableView.SetSelection(1,1,false);
-            // spread selection across to 2,2 (e.g. shift+right then shift+down)
-            tableView.SetSelection(2,2,true);
-
-            var selected = tableView.GetAllSelectedCells().ToArray();
-
-            Assert.Equal(6,selected.Length);
-            Assert.Equal(new Point(0,1),selected[0]);
-            Assert.Equal(new Point(1,1),selected[1]);
-            Assert.Equal(new Point(2,1),selected[2]);
-            Assert.Equal(new Point(0,2),selected[3]);
-            Assert.Equal(new Point(1,2),selected[4]);
-            Assert.Equal(new Point(2,2),selected[5]);
-        }
-        
-
-        [Fact]
-        public void GetAllSelectedCells_TwoIsolatedSelections_ReturnsSix()
-        {
-            var tableView = new TableView(){
-                Table = BuildTable(20,20),
-                MultiSelect = true,
-                Bounds = new Rect(0,0,10,5)
-            };
-
-            /*  
-                    Sets up disconnected selections like:
-
-                    00000000000
-                    01100000000
-                    01100000000
-                    00000001100
-                    00000000000
-            */
-
-            tableView.MultiSelectedRegions.Clear();
-            tableView.MultiSelectedRegions.Push(new TableView.TableSelection(new Point(1,1),new Rect(1,1,2,2)));
-            tableView.MultiSelectedRegions.Push(new TableView.TableSelection (new Point(7,3),new Rect(7,3,2,1)));
-            
-            tableView.SelectedColumn = 8;
-            tableView.SelectedRow = 3;
-
-            var selected = tableView.GetAllSelectedCells().ToArray();
-
-            Assert.Equal(6,selected.Length);
-
-            Assert.Equal(new Point(1,1),selected[0]);
-            Assert.Equal(new Point(2,1),selected[1]);
-            Assert.Equal(new Point(1,2),selected[2]);
-            Assert.Equal(new Point(2,2),selected[3]);
-            Assert.Equal(new Point(7,3),selected[4]);
-            Assert.Equal(new Point(8,3),selected[5]);
-        }
-
-        [Fact]
-        public void TableView_ExpandLastColumn_True()
-        {
-            var tv = SetUpMiniTable();
-            
-            // the thing we are testing
-            tv.Style.ExpandLastColumn = true;
-
-            tv.Redraw(tv.Bounds);
-            
-            string expected = @"
+		[Fact]
+		public void DeleteRow_SelectAll_AdjustsSelectionToPreventOverrun ()
+		{
+			// create a 4 by 4 table
+			var tableView = new TableView () {
+				Table = BuildTable (4, 4),
+				MultiSelect = true,
+				Bounds = new Rect (0, 0, 10, 5)
+			};
+
+			tableView.SelectAll ();
+			Assert.Equal (16, tableView.GetAllSelectedCells ().Count ());
+
+			// delete one of the columns
+			tableView.Table.Columns.RemoveAt (2);
+
+			// table should now be 3x4
+			Assert.Equal (12, tableView.GetAllSelectedCells ().Count ());
+
+			// remove a row
+			tableView.Table.Rows.RemoveAt (1);
+
+			// table should now be 3x3
+			Assert.Equal (9, tableView.GetAllSelectedCells ().Count ());
+		}
+
+
+		[Fact]
+		public void DeleteRow_SelectLastRow_AdjustsSelectionToPreventOverrun ()
+		{
+			// create a 4 by 4 table
+			var tableView = new TableView () {
+				Table = BuildTable (4, 4),
+				MultiSelect = true,
+				Bounds = new Rect (0, 0, 10, 5)
+			};
+
+			// select the last row
+			tableView.MultiSelectedRegions.Clear ();
+			tableView.MultiSelectedRegions.Push (new TableView.TableSelection (new Point (0, 3), new Rect (0, 3, 4, 1)));
+
+			Assert.Equal (4, tableView.GetAllSelectedCells ().Count ());
+
+			// remove a row
+			tableView.Table.Rows.RemoveAt (0);
+
+			tableView.EnsureValidSelection ();
+
+			// since the selection no longer exists it should be removed
+			Assert.Empty (tableView.MultiSelectedRegions);
+		}
+
+		[Theory]
+		[InlineData (true)]
+		[InlineData (false)]
+		public void GetAllSelectedCells_SingleCellSelected_ReturnsOne (bool multiSelect)
+		{
+			var tableView = new TableView () {
+				Table = BuildTable (3, 3),
+				MultiSelect = multiSelect,
+				Bounds = new Rect (0, 0, 10, 5)
+			};
+
+			tableView.SetSelection (1, 1, false);
+
+			Assert.Single (tableView.GetAllSelectedCells ());
+			Assert.Equal (new Point (1, 1), tableView.GetAllSelectedCells ().Single ());
+		}
+
+
+		[Fact]
+		public void GetAllSelectedCells_SquareSelection_ReturnsFour ()
+		{
+			var tableView = new TableView () {
+				Table = BuildTable (3, 3),
+				MultiSelect = true,
+				Bounds = new Rect (0, 0, 10, 5)
+			};
+
+			// move cursor to 1,1
+			tableView.SetSelection (1, 1, false);
+			// spread selection across to 2,2 (e.g. shift+right then shift+down)
+			tableView.SetSelection (2, 2, true);
+
+			var selected = tableView.GetAllSelectedCells ().ToArray ();
+
+			Assert.Equal (4, selected.Length);
+			Assert.Equal (new Point (1, 1), selected [0]);
+			Assert.Equal (new Point (2, 1), selected [1]);
+			Assert.Equal (new Point (1, 2), selected [2]);
+			Assert.Equal (new Point (2, 2), selected [3]);
+		}
+
+
+		[Fact]
+		public void GetAllSelectedCells_SquareSelection_FullRowSelect ()
+		{
+			var tableView = new TableView () {
+				Table = BuildTable (3, 3),
+				MultiSelect = true,
+				FullRowSelect = true,
+				Bounds = new Rect (0, 0, 10, 5)
+			};
+
+			// move cursor to 1,1
+			tableView.SetSelection (1, 1, false);
+			// spread selection across to 2,2 (e.g. shift+right then shift+down)
+			tableView.SetSelection (2, 2, true);
+
+			var selected = tableView.GetAllSelectedCells ().ToArray ();
+
+			Assert.Equal (6, selected.Length);
+			Assert.Equal (new Point (0, 1), selected [0]);
+			Assert.Equal (new Point (1, 1), selected [1]);
+			Assert.Equal (new Point (2, 1), selected [2]);
+			Assert.Equal (new Point (0, 2), selected [3]);
+			Assert.Equal (new Point (1, 2), selected [4]);
+			Assert.Equal (new Point (2, 2), selected [5]);
+		}
+
+
+		[Fact]
+		public void GetAllSelectedCells_TwoIsolatedSelections_ReturnsSix ()
+		{
+			var tableView = new TableView () {
+				Table = BuildTable (20, 20),
+				MultiSelect = true,
+				Bounds = new Rect (0, 0, 10, 5)
+			};
+
+			/*  
+				Sets up disconnected selections like:
+
+				00000000000
+				01100000000
+				01100000000
+				00000001100
+				00000000000
+			*/
+
+			tableView.MultiSelectedRegions.Clear ();
+			tableView.MultiSelectedRegions.Push (new TableView.TableSelection (new Point (1, 1), new Rect (1, 1, 2, 2)));
+			tableView.MultiSelectedRegions.Push (new TableView.TableSelection (new Point (7, 3), new Rect (7, 3, 2, 1)));
+
+			tableView.SelectedColumn = 8;
+			tableView.SelectedRow = 3;
+
+			var selected = tableView.GetAllSelectedCells ().ToArray ();
+
+			Assert.Equal (6, selected.Length);
+
+			Assert.Equal (new Point (1, 1), selected [0]);
+			Assert.Equal (new Point (2, 1), selected [1]);
+			Assert.Equal (new Point (1, 2), selected [2]);
+			Assert.Equal (new Point (2, 2), selected [3]);
+			Assert.Equal (new Point (7, 3), selected [4]);
+			Assert.Equal (new Point (8, 3), selected [5]);
+		}
+
+		[Fact]
+		public void TableView_ExpandLastColumn_True ()
+		{
+			var tv = SetUpMiniTable ();
+
+			// the thing we are testing
+			tv.Style.ExpandLastColumn = true;
+
+			tv.Redraw (tv.Bounds);
+
+			string expected = @"
 ┌─┬──────┐
 │A│B     │
 ├─┼──────┤
 │1│2     │
 ";
-            GraphViewTests.AssertDriverContentsAre(expected);
-        }
+			GraphViewTests.AssertDriverContentsAre (expected, output);
 
+			// Shutdown must be called to safely clean up Application if Init has been called
+			Application.Shutdown ();
+		}
+
+
+		[Fact]
+		public void TableView_ExpandLastColumn_False ()
+		{
+			var tv = SetUpMiniTable ();
 
-        [Fact]
-        public void TableView_ExpandLastColumn_False()
-        {
-            var tv = SetUpMiniTable();
-            
-            // the thing we are testing
-            tv.Style.ExpandLastColumn = false;
+			// the thing we are testing
+			tv.Style.ExpandLastColumn = false;
 
-            tv.Redraw(tv.Bounds);
-            
-            string expected = @"
+			tv.Redraw (tv.Bounds);
+
+			string expected = @"
 ┌─┬─┬────┐
 │A│B│    │
 ├─┼─┼────┤
 │1│2│    │
 ";
-            GraphViewTests.AssertDriverContentsAre(expected);
-        }
-
-        [Fact]
-        public void TableView_ExpandLastColumn_False_ExactBounds()
-        {
-            var tv = SetUpMiniTable();
-            
-            // the thing we are testing
-            tv.Style.ExpandLastColumn = false;
-            // width exactly matches the max col widths
-            tv.Bounds = new Rect(0,0,5,4);
-
-            tv.Redraw(tv.Bounds);
-            
-            string expected = @"
+			GraphViewTests.AssertDriverContentsAre (expected, output);
+
+			// Shutdown must be called to safely clean up Application if Init has been called
+			Application.Shutdown ();
+		}
+
+		[Fact]
+		public void TableView_ExpandLastColumn_False_ExactBounds ()
+		{
+			var tv = SetUpMiniTable ();
+
+			// the thing we are testing
+			tv.Style.ExpandLastColumn = false;
+			// width exactly matches the max col widths
+			tv.Bounds = new Rect (0, 0, 5, 4);
+
+			tv.Redraw (tv.Bounds);
+
+			string expected = @"
 ┌─┬─┐
 │A│B│
 ├─┼─┤
 │1│2│
 ";
-            GraphViewTests.AssertDriverContentsAre(expected);
-        }
+			GraphViewTests.AssertDriverContentsAre (expected, output);
+
+			// Shutdown must be called to safely clean up Application if Init has been called
+			Application.Shutdown ();
+		}
 
 		private TableView SetUpMiniTable ()
 		{
-			
-            var tv = new TableView();
-            tv.Bounds = new Rect(0,0,10,4);
-
-            var dt = new DataTable();
-            var colA = dt.Columns.Add("A");
-            var colB = dt.Columns.Add("B");
-            dt.Rows.Add(1,2);
-
-            tv.Table = dt;
-            tv.Style.GetOrCreateColumnStyle(colA).MinWidth=1;
-            tv.Style.GetOrCreateColumnStyle(colA).MinWidth=1;
-            tv.Style.GetOrCreateColumnStyle(colB).MaxWidth=1;
-            tv.Style.GetOrCreateColumnStyle(colB).MaxWidth=1;
-
-            GraphViewTests.InitFakeDriver();
-            tv.ColorScheme = new ColorScheme(){
-                Normal = Application.Driver.MakeAttribute(Color.White,Color.Black),
-                HotFocus = Application.Driver.MakeAttribute(Color.White,Color.Black)
-                };
-            return tv;
+
+			var tv = new TableView ();
+			tv.Bounds = new Rect (0, 0, 10, 4);
+
+			var dt = new DataTable ();
+			var colA = dt.Columns.Add ("A");
+			var colB = dt.Columns.Add ("B");
+			dt.Rows.Add (1, 2);
+
+			tv.Table = dt;
+			tv.Style.GetOrCreateColumnStyle (colA).MinWidth = 1;
+			tv.Style.GetOrCreateColumnStyle (colA).MinWidth = 1;
+			tv.Style.GetOrCreateColumnStyle (colB).MaxWidth = 1;
+			tv.Style.GetOrCreateColumnStyle (colB).MaxWidth = 1;
+
+			GraphViewTests.InitFakeDriver ();
+			tv.ColorScheme = new ColorScheme () {
+				Normal = Application.Driver.MakeAttribute (Color.White, Color.Black),
+				HotFocus = Application.Driver.MakeAttribute (Color.White, Color.Black)
+			};
+			return tv;
 		}
 
 		/// <summary>
@@ -509,24 +526,24 @@ namespace Terminal.Gui.Views {
 		/// <param name="cols"></param>
 		/// <param name="rows"></param>
 		/// <returns></returns>
-		public static DataTable BuildTable(int cols, int rows)
+		public static DataTable BuildTable (int cols, int rows)
 		{
-			var dt = new DataTable();
+			var dt = new DataTable ();
 
-			for(int c = 0; c < cols; c++) {
-				dt.Columns.Add("Col"+c);
+			for (int c = 0; c < cols; c++) {
+				dt.Columns.Add ("Col" + c);
 			}
-				
-			for(int r = 0; r < rows; r++) {
-				var newRow = dt.NewRow();
 
-				for(int c = 0; c < cols; c++) {
-					newRow[c] = $"R{r}C{c}";
+			for (int r = 0; r < rows; r++) {
+				var newRow = dt.NewRow ();
+
+				for (int c = 0; c < cols; c++) {
+					newRow [c] = $"R{r}C{c}";
 				}
 
-				dt.Rows.Add(newRow);
+				dt.Rows.Add (newRow);
 			}
-			
+
 			return dt;
 		}
 	}

+ 118 - 8
UnitTests/TextFieldTests.cs

@@ -1,19 +1,38 @@
-using Xunit;
+using System;
+using System.Reflection;
+using Xunit;
 
 namespace Terminal.Gui.Views {
 	public class TextFieldTests {
-		private TextField _textField;
 
-		public TextFieldTests ()
-		{
-			Application.Init (new FakeDriver (), new FakeMainLoop (() => FakeConsole.ReadKey (true)));
+		// 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
+		// as a pair, and b) all unit test functions should be atomic.
+		[AttributeUsage (AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
+		public class InitShutdown : Xunit.Sdk.BeforeAfterTestAttribute {
+
+			public override void Before (MethodInfo methodUnderTest)
+			{
+				Application.Init (new FakeDriver (), new FakeMainLoop (() => FakeConsole.ReadKey (true)));
+
+				//                                                    1         2         3 
+				//                                          01234567890123456789012345678901=32 (Length)
+				TextFieldTests._textField = new TextField ("TAB to jump between text fields.");
+			}
 
-			//                                     1         2         3 
-			//                           01234567890123456789012345678901=32 (Length)
-			_textField = new TextField ("TAB to jump between text fields.");
+			public override void After (MethodInfo methodUnderTest)
+			{
+				TextFieldTests._textField = null;
+				Application.Shutdown ();
+			}
 		}
 
+		private static TextField _textField;
+
 		[Fact]
+		[InitShutdown]
 		public void Changing_SelectedStart_Or_CursorPosition_Update_SelectedLength_And_SelectedText ()
 		{
 			_textField.SelectedStart = 2;
@@ -27,6 +46,7 @@ namespace Terminal.Gui.Views {
 		}
 
 		[Fact]
+		[InitShutdown]
 		public void SelectedStart_With_Value_Less_Than_Minus_One_Changes_To_Minus_One ()
 		{
 			_textField.SelectedStart = -2;
@@ -36,6 +56,7 @@ namespace Terminal.Gui.Views {
 		}
 
 		[Fact]
+		[InitShutdown]
 		public void SelectedStart_With_Value_Greater_Than_Text_Length_Changes_To_Text_Length ()
 		{
 			_textField.CursorPosition = 2;
@@ -46,6 +67,7 @@ namespace Terminal.Gui.Views {
 		}
 
 		[Fact]
+		[InitShutdown]
 		public void SelectedStart_And_CursorPosition_With_Value_Greater_Than_Text_Length_Changes_Both_To_Text_Length ()
 		{
 			_textField.CursorPosition = 33;
@@ -57,6 +79,7 @@ namespace Terminal.Gui.Views {
 		}
 
 		[Fact]
+		[InitShutdown]
 		public void SelectedStart_Greater_Than_CursorPosition_All_Selection_Is_Overwritten_On_Typing ()
 		{
 			_textField.SelectedStart = 19;
@@ -67,6 +90,7 @@ namespace Terminal.Gui.Views {
 		}
 
 		[Fact]
+		[InitShutdown]
 		public void CursorPosition_With_Value_Less_Than_Zero_Changes_To_Zero ()
 		{
 			_textField.CursorPosition = -1;
@@ -76,6 +100,7 @@ namespace Terminal.Gui.Views {
 		}
 
 		[Fact]
+		[InitShutdown]
 		public void CursorPosition_With_Value_Greater_Than_Text_Length_Changes_To_Text_Length ()
 		{
 			_textField.CursorPosition = 33;
@@ -85,6 +110,7 @@ namespace Terminal.Gui.Views {
 		}
 
 		[Fact]
+		[InitShutdown]
 		public void WordForward_With_No_Selection ()
 		{
 			_textField.CursorPosition = 0;
@@ -135,6 +161,7 @@ namespace Terminal.Gui.Views {
 		}
 
 		[Fact]
+		[InitShutdown]
 		public void WordBackward_With_No_Selection ()
 		{
 			_textField.CursorPosition = _textField.Text.Length;
@@ -185,6 +212,7 @@ namespace Terminal.Gui.Views {
 		}
 
 		[Fact]
+		[InitShutdown]
 		public void WordForward_With_Selection ()
 		{
 			_textField.CursorPosition = 0;
@@ -236,6 +264,7 @@ namespace Terminal.Gui.Views {
 		}
 
 		[Fact]
+		[InitShutdown]
 		public void WordBackward_With_Selection ()
 		{
 			_textField.CursorPosition = _textField.Text.Length;
@@ -287,6 +316,7 @@ namespace Terminal.Gui.Views {
 		}
 
 		[Fact]
+		[InitShutdown]
 		public void WordForward_With_The_Same_Values_For_SelectedStart_And_CursorPosition_And_Not_Starting_At_Beginning_Of_The_Text ()
 		{
 			_textField.CursorPosition = 10;
@@ -326,6 +356,7 @@ namespace Terminal.Gui.Views {
 		}
 
 		[Fact]
+		[InitShutdown]
 		public void WordBackward_With_The_Same_Values_For_SelectedStart_And_CursorPosition_And_Not_Starting_At_Beginning_Of_The_Text ()
 		{
 			_textField.CursorPosition = 10;
@@ -359,6 +390,7 @@ namespace Terminal.Gui.Views {
 		}
 
 		[Fact]
+		[InitShutdown]
 		public void WordForward_With_No_Selection_And_With_More_Than_Only_One_Whitespace_And_With_Only_One_Letter ()
 		{
 			//                           1         2         3         4         5    
@@ -436,6 +468,7 @@ namespace Terminal.Gui.Views {
 		}
 
 		[Fact]
+		[InitShutdown]
 		public void WordBackward_With_No_Selection_And_With_More_Than_Only_One_Whitespace_And_With_Only_One_Letter ()
 		{
 			//                           1         2         3         4         5    
@@ -519,6 +552,7 @@ namespace Terminal.Gui.Views {
 		}
 
 		[Fact]
+		[InitShutdown]
 		public void Copy_Or_Cut_Null_If_No_Selection ()
 		{
 			_textField.SelectedStart = -1;
@@ -529,6 +563,7 @@ namespace Terminal.Gui.Views {
 		}
 
 		[Fact]
+		[InitShutdown]
 		public void Copy_Or_Cut_Not_Null_If_Has_Selection ()
 		{
 			_textField.SelectedStart = 20;
@@ -540,6 +575,7 @@ namespace Terminal.Gui.Views {
 		}
 
 		[Fact]
+		[InitShutdown]
 		public void Copy_Or_Cut_And_Paste_With_Selection ()
 		{
 			_textField.SelectedStart = 20;
@@ -556,6 +592,7 @@ namespace Terminal.Gui.Views {
 		}
 
 		[Fact]
+		[InitShutdown]
 		public void Copy_Or_Cut_And_Paste_With_No_Selection ()
 		{
 			_textField.SelectedStart = 20;
@@ -576,6 +613,7 @@ namespace Terminal.Gui.Views {
 		}
 
 		[Fact]
+		[InitShutdown]
 		public void Copy_Or_Cut__Not_Allowed_If_Secret_Is_True ()
 		{
 			_textField.Secret = true;
@@ -593,6 +631,7 @@ namespace Terminal.Gui.Views {
 		}
 
 		[Fact]
+		[InitShutdown]
 		public void Paste_Always_Clear_The_SelectedText ()
 		{
 			_textField.SelectedStart = 20;
@@ -604,6 +643,7 @@ namespace Terminal.Gui.Views {
 		}
 
 		[Fact]
+		[InitShutdown]
 		public void TextChanging_Event ()
 		{
 			bool cancel = true;
@@ -623,6 +663,7 @@ namespace Terminal.Gui.Views {
 		}
 
 		[Fact]
+		[InitShutdown]
 		public void TextChanged_Event ()
 		{
 			_textField.TextChanged += (e) => {
@@ -634,6 +675,7 @@ namespace Terminal.Gui.Views {
 		}
 
 		[Fact]
+		[InitShutdown]
 		public void Used_Is_True_By_Default ()
 		{
 			_textField.CursorPosition = 10;
@@ -649,6 +691,7 @@ namespace Terminal.Gui.Views {
 		}
 
 		[Fact]
+		[InitShutdown]
 		public void Used_Is_False ()
 		{
 			_textField.Used = false;
@@ -663,5 +706,72 @@ namespace Terminal.Gui.Views {
 			_textField.ProcessKey (new KeyEvent ((Key)0x64, new KeyModifiers ())); // d
 			Assert.Equal ("TAB to jumusedtween text fields.", _textField.Text);
 		}
+
+		[Fact]
+		public void ProcessKey_Backspace_From_End ()
+		{
+			var tf = new TextField ("ABC");
+			tf.EnsureFocus ();
+			Assert.Equal ("ABC", tf.Text);
+
+			// now delete the C
+			tf.ProcessKey (new KeyEvent (Key.Backspace, new KeyModifiers ()));
+			Assert.Equal ("AB", tf.Text);
+
+			// then delete the B
+			tf.ProcessKey (new KeyEvent (Key.Backspace, new KeyModifiers ()));
+			Assert.Equal ("A", tf.Text);
+
+			// then delete the A
+			tf.ProcessKey (new KeyEvent (Key.Backspace, new KeyModifiers ()));
+			Assert.Equal ("", tf.Text);
+		}
+
+		[Fact]
+		public void ProcessKey_Backspace_From_Middle ()
+		{
+			var tf = new TextField ("ABC");
+			tf.EnsureFocus ();
+			tf.CursorPosition = 2;
+			Assert.Equal ("ABC", tf.Text);
+
+			// now delete the B
+			tf.ProcessKey (new KeyEvent (Key.Backspace, new KeyModifiers ()));
+			Assert.Equal ("AC", tf.Text);
+
+			// then delete the A
+			tf.ProcessKey (new KeyEvent (Key.Backspace, new KeyModifiers ()));
+			Assert.Equal ("C", tf.Text);
+
+			// then delete nothing
+			tf.ProcessKey (new KeyEvent (Key.Backspace, new KeyModifiers ()));
+			Assert.Equal ("C", tf.Text);
+
+			// now delete the C
+			tf.CursorPosition = 1;
+			tf.ProcessKey (new KeyEvent (Key.Backspace, new KeyModifiers ()));
+			Assert.Equal ("", tf.Text);
+		}
+
+		[Fact]
+		public void Cancel_TextChanging_ThenBackspace ()
+		{
+			var tf = new TextField ();
+			tf.EnsureFocus ();
+			tf.ProcessKey (new KeyEvent (Key.A, new KeyModifiers ()));
+			Assert.Equal ("A", tf.Text);
+
+			// cancel the next keystroke
+			tf.TextChanging += (e) => e.Cancel = e.NewText == "AB";
+			tf.ProcessKey (new KeyEvent (Key.B, new KeyModifiers ()));
+
+			// B was canceled so should just be A
+			Assert.Equal ("A", tf.Text);
+
+			// now delete the A
+			tf.ProcessKey (new KeyEvent (Key.Backspace, new KeyModifiers ()));
+
+			Assert.Equal ("", tf.Text);
+		}
 	}
 }

+ 6 - 0
UnitTests/TextFormatterTests.cs

@@ -2533,6 +2533,9 @@ namespace Terminal.Gui.Core {
 				// After the fix this exception will not be caught.
 				Assert.IsType<IndexOutOfRangeException> (ex);
 			}
+
+			// Shutdown must be called to safely clean up Application if Init has been called
+			Application.Shutdown ();
 		}
 
 		[Fact]
@@ -2561,6 +2564,9 @@ namespace Terminal.Gui.Core {
 				// After the fix this exception will not be caught.
 				Assert.IsType<IndexOutOfRangeException> (ex);
 			}
+
+			// Shutdown must be called to safely clean up Application if Init has been called
+			Application.Shutdown ();
 		}
 	}
 }

+ 28 - 9
UnitTests/TextValidateFieldTests.cs

@@ -1,16 +1,16 @@
-using System.Text.RegularExpressions;
+using System;
+using System.Reflection;
+using System.Text.RegularExpressions;
 using Terminal.Gui.TextValidateProviders;
 
 using Xunit;
 
 namespace Terminal.Gui.Views {
+
 	public class TextValidateField_NET_Provider_Tests {
-		public TextValidateField_NET_Provider_Tests ()
-		{
-			Application.Init (new FakeDriver (), new FakeMainLoop (() => FakeConsole.ReadKey (true)));
-		}
 
 		[Fact]
+		[AutoInitShutdown]
 		public void Initialized_With_Cursor_On_First_Editable_Character ()
 		{
 			//                                                            *
@@ -27,6 +27,7 @@ namespace Terminal.Gui.Views {
 		}
 
 		[Fact]
+		[AutoInitShutdown]
 		public void Input_Ilegal_Character ()
 		{
 			//                                                            *
@@ -44,6 +45,7 @@ namespace Terminal.Gui.Views {
 		}
 
 		[Fact]
+		[AutoInitShutdown]
 		public void Home_Key_First_Editable_Character ()
 		{
 			//                                                            *
@@ -65,6 +67,7 @@ namespace Terminal.Gui.Views {
 		}
 
 		[Fact]
+		[AutoInitShutdown]
 		public void End_Key_Last_Editable_Character ()
 		{
 			//                                                               *
@@ -84,6 +87,7 @@ namespace Terminal.Gui.Views {
 		}
 
 		[Fact]
+		[AutoInitShutdown]
 		public void Right_Key_Stops_In_Last_Editable_Character ()
 		{
 			//                                                               *
@@ -104,6 +108,7 @@ namespace Terminal.Gui.Views {
 		}
 
 		[Fact]
+		[AutoInitShutdown]
 		public void Left_Key_Stops_In_First_Editable_Character ()
 		{
 			//                                                            *
@@ -124,6 +129,7 @@ namespace Terminal.Gui.Views {
 		}
 
 		[Fact]
+		[AutoInitShutdown]
 		public void When_Valid_Is_Valid_True ()
 		{
 			//                                                            ****
@@ -151,6 +157,7 @@ namespace Terminal.Gui.Views {
 		}
 
 		[Fact]
+		[AutoInitShutdown]
 		public void Insert_Skips_Non_Editable_Characters ()
 		{
 			//                                                            ** **
@@ -179,6 +186,7 @@ namespace Terminal.Gui.Views {
 
 
 		[Fact]
+		[AutoInitShutdown]
 		public void Initial_Value_Exact_Valid ()
 		{
 			//                                                            ****
@@ -193,6 +201,7 @@ namespace Terminal.Gui.Views {
 		}
 
 		[Fact]
+		[AutoInitShutdown]
 		public void Initial_Value_Bigger_Than_Mask_Discarded ()
 		{
 			//                                                            ****
@@ -208,6 +217,7 @@ namespace Terminal.Gui.Views {
 		}
 
 		[Fact]
+		[AutoInitShutdown]
 		public void Initial_Value_Smaller_Than_Mask_Accepted ()
 		{
 			//                                                            ****
@@ -223,6 +233,7 @@ namespace Terminal.Gui.Views {
 		}
 
 		[Fact]
+		[AutoInitShutdown]
 		public void Delete_Key_Dosent_Move_Cursor ()
 		{
 			//                                                            ****
@@ -254,6 +265,7 @@ namespace Terminal.Gui.Views {
 		}
 
 		[Fact]
+		[AutoInitShutdown]
 		public void Backspace_Key_Deletes_Previous_Character ()
 		{
 			//                                                            ****
@@ -286,6 +298,7 @@ namespace Terminal.Gui.Views {
 
 
 		[Fact]
+		[AutoInitShutdown]
 		public void Set_Text_After_Initialization ()
 		{
 			//                                                            ****
@@ -302,6 +315,7 @@ namespace Terminal.Gui.Views {
 		}
 
 		[Fact]
+		[AutoInitShutdown]
 		public void Changing_The_Mask_Tries_To_Keep_The_Previous_Text ()
 		{
 			//                                                            ****
@@ -322,6 +336,7 @@ namespace Terminal.Gui.Views {
 		}
 
 		[Fact]
+		[AutoInitShutdown]
 		public void MouseClick_Right_X_Greater_Than_Text_Width_Goes_To_Last_Editable_Position ()
 		{
 			//                                                            ****
@@ -346,12 +361,9 @@ namespace Terminal.Gui.Views {
 	}
 
 	public class TextValidateField_Regex_Provider_Tests {
-		public TextValidateField_Regex_Provider_Tests ()
-		{
-			Application.Init (new FakeDriver (), new FakeMainLoop (() => FakeConsole.ReadKey (true)));
-		}
 
 		[Fact]
+		[AutoInitShutdown]
 		public void Input_Without_Validate_On_Input ()
 		{
 			var field = new TextValidateField (new TextRegexProvider ("^[0-9][0-9][0-9]$") { ValidateOnInput = false }) {
@@ -376,6 +388,7 @@ namespace Terminal.Gui.Views {
 		}
 
 		[Fact]
+		[AutoInitShutdown]
 		public void Input_With_Validate_On_Input_Set_Text ()
 		{
 			var field = new TextValidateField (new TextRegexProvider ("^[0-9][0-9][0-9]$")) {
@@ -399,6 +412,7 @@ namespace Terminal.Gui.Views {
 		}
 
 		[Fact]
+		[AutoInitShutdown]
 		public void Text_With_All_Charset ()
 		{
 			var field = new TextValidateField (new TextRegexProvider ("^[0-9][0-9][0-9]$")) {
@@ -416,6 +430,7 @@ namespace Terminal.Gui.Views {
 		}
 
 		[Fact]
+		[AutoInitShutdown]
 		public void Mask_With_Invalid_Pattern_Exception ()
 		{
 			// Regex Exception
@@ -438,6 +453,7 @@ namespace Terminal.Gui.Views {
 		}
 
 		[Fact]
+		[AutoInitShutdown]
 		public void Home_Key_First_Editable_Character ()
 		{
 			// Range 0 to 1000
@@ -465,6 +481,7 @@ namespace Terminal.Gui.Views {
 		}
 
 		[Fact]
+		[AutoInitShutdown]
 		public void End_Key_End_Of_Input ()
 		{
 			// Exactly 5 numbers
@@ -499,6 +516,7 @@ namespace Terminal.Gui.Views {
 		}
 
 		[Fact]
+		[AutoInitShutdown]
 		public void Right_Key_Stops_At_End_And_Insert ()
 		{
 			var field = new TextValidateField (new TextRegexProvider ("^[0-9][0-9][0-9]$") { ValidateOnInput = false }) {
@@ -523,6 +541,7 @@ namespace Terminal.Gui.Views {
 		}
 
 		[Fact]
+		[AutoInitShutdown]
 		public void Left_Key_Stops_At_Start_And_Insert ()
 		{
 			var field = new TextValidateField (new TextRegexProvider ("^[0-9][0-9][0-9]$") { ValidateOnInput = false }) {

+ 77 - 57
UnitTests/TextViewTests.cs

@@ -1,28 +1,48 @@
 using System;
 using System.Linq;
+using System.Reflection;
 using Xunit;
 
 namespace Terminal.Gui.Views {
 	public class TextViewTests {
-		private TextView _textView;
+		private static TextView _textView;
+
+		// This class enables test functions annoated 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
+		// as a pair, and b) all unit test functions should be atomic.
+		[AttributeUsage (AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
+		public class InitShutdown : Xunit.Sdk.BeforeAfterTestAttribute {
+
+			public override void Before (MethodInfo methodUnderTest)
+			{
+				if (_textView != null) {
+					throw new InvalidOperationException ("After did not run.");
+				}
 
-		public TextViewTests ()
-		{
-			Application.Init (new FakeDriver (), new FakeMainLoop (() => FakeConsole.ReadKey (true)));
-
-			//                   1         2         3 
-			//         01234567890123456789012345678901=32 (Length)
-			var txt = "TAB to jump between text fields.";
-			var buff = new byte [txt.Length];
-			for (int i = 0; i < txt.Length; i++) {
-				buff [i] = (byte)txt [i];
+				Application.Init (new FakeDriver (), new FakeMainLoop (() => FakeConsole.ReadKey (true)));
+
+				//                   1         2         3 
+				//         01234567890123456789012345678901=32 (Length)
+				var txt = "TAB to jump between text fields.";
+				var buff = new byte [txt.Length];
+				for (int i = 0; i < txt.Length; i++) {
+					buff [i] = (byte)txt [i];
+				}
+				var ms = new System.IO.MemoryStream (buff).ToArray ();
+				_textView = new TextView () { Width = 30, Height = 10 };
+				_textView.Text = ms;
+			}
+
+			public override void After (MethodInfo methodUnderTest)
+			{
+				_textView = null;
+				Application.Shutdown ();
 			}
-			var ms = new System.IO.MemoryStream (buff).ToArray ();
-			_textView = new TextView () { Width = 30, Height = 10 };
-			_textView.Text = ms;
 		}
 
-		[Fact]
+		[Fact][InitShutdown]
 		public void Changing_Selection_Or_CursorPosition_Update_SelectedLength_And_SelectedText ()
 		{
 			_textView.SelectionStartColumn = 2;
@@ -38,7 +58,7 @@ namespace Terminal.Gui.Views {
 			Assert.Equal ("B to jump between ", _textView.SelectedText);
 		}
 
-		[Fact]
+		[Fact][InitShutdown]
 		public void Selection_With_Value_Less_Than_Zero_Changes_To_Zero ()
 		{
 			_textView.SelectionStartColumn = -2;
@@ -49,7 +69,7 @@ namespace Terminal.Gui.Views {
 			Assert.Equal ("", _textView.SelectedText);
 		}
 
-		[Fact]
+		[Fact][InitShutdown]
 		public void Selection_With_Value_Greater_Than_Text_Length_Changes_To_Text_Length ()
 		{
 			_textView.CursorPosition = new Point (2, 0);
@@ -61,7 +81,7 @@ namespace Terminal.Gui.Views {
 			Assert.Equal ("B to jump between text fields.", _textView.SelectedText);
 		}
 
-		[Fact]
+		[Fact][InitShutdown]
 		public void Selection_With_Empty_Text ()
 		{
 			_textView = new TextView ();
@@ -74,7 +94,7 @@ namespace Terminal.Gui.Views {
 			Assert.Equal ("", _textView.SelectedText);
 		}
 
-		[Fact]
+		[Fact][InitShutdown]
 		public void Selection_And_CursorPosition_With_Value_Greater_Than_Text_Length_Changes_Both_To_Text_Length ()
 		{
 			_textView.CursorPosition = new Point (33, 2);
@@ -88,7 +108,7 @@ namespace Terminal.Gui.Views {
 			Assert.Equal ("", _textView.SelectedText);
 		}
 
-		[Fact]
+		[Fact][InitShutdown]
 		public void CursorPosition_With_Value_Less_Than_Zero_Changes_To_Zero ()
 		{
 			_textView.CursorPosition = new Point (-1, -1);
@@ -98,7 +118,7 @@ namespace Terminal.Gui.Views {
 			Assert.Equal ("", _textView.SelectedText);
 		}
 
-		[Fact]
+		[Fact][InitShutdown]
 		public void CursorPosition_With_Value_Greater_Than_Text_Length_Changes_To_Text_Length ()
 		{
 			_textView.CursorPosition = new Point (33, 1);
@@ -108,7 +128,7 @@ namespace Terminal.Gui.Views {
 			Assert.Equal ("", _textView.SelectedText);
 		}
 
-		[Fact]
+		[Fact][InitShutdown]
 		public void WordForward_With_No_Selection ()
 		{
 			_textView.CursorPosition = new Point (0, 0);
@@ -170,7 +190,7 @@ namespace Terminal.Gui.Views {
 			}
 		}
 
-		[Fact]
+		[Fact][InitShutdown]
 		public void WordBackward_With_No_Selection ()
 		{
 			_textView.CursorPosition = new Point (_textView.Text.Length, 0);
@@ -232,7 +252,7 @@ namespace Terminal.Gui.Views {
 			}
 		}
 
-		[Fact]
+		[Fact][InitShutdown]
 		public void WordForward_With_Selection ()
 		{
 			_textView.CursorPosition = new Point (0, 0);
@@ -296,7 +316,7 @@ namespace Terminal.Gui.Views {
 			}
 		}
 
-		[Fact]
+		[Fact][InitShutdown]
 		public void WordBackward_With_Selection ()
 		{
 			_textView.CursorPosition = new Point (_textView.Text.Length, 0);
@@ -360,7 +380,7 @@ namespace Terminal.Gui.Views {
 			}
 		}
 
-		[Fact]
+		[Fact][InitShutdown]
 		public void WordForward_With_The_Same_Values_For_SelectedStart_And_CursorPosition_And_Not_Starting_At_Beginning_Of_The_Text ()
 		{
 			_textView.CursorPosition = new Point (10, 0);
@@ -408,7 +428,7 @@ namespace Terminal.Gui.Views {
 			}
 		}
 
-		[Fact]
+		[Fact][InitShutdown]
 		public void WordBackward_With_The_Same_Values_For_SelectedStart_And_CursorPosition_And_Not_Starting_At_Beginning_Of_The_Text ()
 		{
 			_textView.CursorPosition = new Point (10, 0);
@@ -448,7 +468,7 @@ namespace Terminal.Gui.Views {
 			}
 		}
 
-		[Fact]
+		[Fact][InitShutdown]
 		public void WordForward_With_No_Selection_And_With_More_Than_Only_One_Whitespace_And_With_Only_One_Letter ()
 		{
 			//                          1         2         3         4         5    
@@ -545,7 +565,7 @@ namespace Terminal.Gui.Views {
 			}
 		}
 
-		[Fact]
+		[Fact][InitShutdown]
 		public void WordBackward_With_No_Selection_And_With_More_Than_Only_One_Whitespace_And_With_Only_One_Letter ()
 		{
 			//                          1         2         3         4         5    
@@ -650,7 +670,7 @@ namespace Terminal.Gui.Views {
 			}
 		}
 
-		[Fact]
+		[Fact][InitShutdown]
 		public void WordBackward_Multiline_With_Selection ()
 		{
 			//		          4         3          2         1
@@ -764,7 +784,7 @@ namespace Terminal.Gui.Views {
 			}
 		}
 
-		[Fact]
+		[Fact][InitShutdown]
 		public void WordForward_Multiline_With_Selection ()
 		{
 			//			    1         2          3         4
@@ -877,7 +897,7 @@ namespace Terminal.Gui.Views {
 			}
 		}
 
-		[Fact]
+		[Fact][InitShutdown]
 		public void Kill_To_End_Delete_Forwards_And_Copy_To_The_Clipboard ()
 		{
 			_textView.Text = "This is the first line.\nThis is the second line.";
@@ -912,7 +932,7 @@ namespace Terminal.Gui.Views {
 			}
 		}
 
-		[Fact]
+		[Fact][InitShutdown]
 		public void Kill_To_Start_Delete_Backwards_And_Copy_To_The_Clipboard ()
 		{
 			_textView.Text = "This is the first line.\nThis is the second line.";
@@ -948,7 +968,7 @@ namespace Terminal.Gui.Views {
 			}
 		}
 
-		[Fact]
+		[Fact][InitShutdown]
 		public void Kill_Delete_WordForward ()
 		{
 			_textView.Text = "This is the first line.";
@@ -991,7 +1011,7 @@ namespace Terminal.Gui.Views {
 			}
 		}
 
-		[Fact]
+		[Fact][InitShutdown]
 		public void Kill_Delete_WordBackward ()
 		{
 			_textView.Text = "This is the first line.";
@@ -1035,7 +1055,7 @@ namespace Terminal.Gui.Views {
 			}
 		}
 
-		[Fact]
+		[Fact][InitShutdown]
 		public void Kill_Delete_WordForward_Multiline ()
 		{
 			_textView.Text = "This is the first line.\nThis is the second line.";
@@ -1114,7 +1134,7 @@ namespace Terminal.Gui.Views {
 			}
 		}
 
-		[Fact]
+		[Fact][InitShutdown]
 		public void Kill_Delete_WordBackward_Multiline ()
 		{
 			_textView.Text = "This is the first line.\nThis is the second line.";
@@ -1193,7 +1213,7 @@ namespace Terminal.Gui.Views {
 			}
 		}
 
-		[Fact]
+		[Fact][InitShutdown]
 		public void Copy_Or_Cut_Null_If_No_Selection ()
 		{
 			_textView.SelectionStartColumn = 0;
@@ -1204,7 +1224,7 @@ namespace Terminal.Gui.Views {
 			Assert.Equal ("", _textView.SelectedText);
 		}
 
-		[Fact]
+		[Fact][InitShutdown]
 		public void Copy_Or_Cut_Not_Null_If_Has_Selection ()
 		{
 			_textView.SelectionStartColumn = 20;
@@ -1216,7 +1236,7 @@ namespace Terminal.Gui.Views {
 			Assert.Equal ("", _textView.SelectedText);
 		}
 
-		[Fact]
+		[Fact][InitShutdown]
 		public void Copy_Or_Cut_And_Paste_With_Selection ()
 		{
 			_textView.SelectionStartColumn = 20;
@@ -1234,7 +1254,7 @@ namespace Terminal.Gui.Views {
 			Assert.Equal ("TAB to jump between text fields.", _textView.Text);
 		}
 
-		[Fact]
+		[Fact][InitShutdown]
 		public void Copy_Or_Cut_And_Paste_With_No_Selection ()
 		{
 			_textView.SelectionStartColumn = 20;
@@ -1262,7 +1282,7 @@ namespace Terminal.Gui.Views {
 			Assert.Equal ("TAB to jump between texttext fields.", _textView.Text);
 		}
 
-		[Fact]
+		[Fact][InitShutdown]
 		public void Cut_Not_Allowed_If_ReadOnly_Is_True ()
 		{
 			_textView.ReadOnly = true;
@@ -1282,7 +1302,7 @@ namespace Terminal.Gui.Views {
 			Assert.Equal ("", _textView.SelectedText);
 		}
 
-		[Fact]
+		[Fact][InitShutdown]
 		public void Paste_Always_Clear_The_SelectedText ()
 		{
 			_textView.SelectionStartColumn = 20;
@@ -1294,7 +1314,7 @@ namespace Terminal.Gui.Views {
 			Assert.Equal ("", _textView.SelectedText);
 		}
 
-		[Fact]
+		[Fact][InitShutdown]
 		public void TextChanged_Event ()
 		{
 			_textView.TextChanged += () => {
@@ -1308,7 +1328,7 @@ namespace Terminal.Gui.Views {
 			Assert.Equal ("changed", _textView.Text);
 		}
 
-		[Fact]
+		[Fact][InitShutdown]
 		public void Used_Is_True_By_Default ()
 		{
 			_textView.CursorPosition = new Point (10, 0);
@@ -1323,7 +1343,7 @@ namespace Terminal.Gui.Views {
 			Assert.Equal ("TAB to jumusedp between text fields.", _textView.Text);
 		}
 
-		[Fact]
+		[Fact][InitShutdown]
 		public void Used_Is_False ()
 		{
 			_textView.Used = false;
@@ -1339,7 +1359,7 @@ namespace Terminal.Gui.Views {
 			Assert.Equal ("TAB to jumusedtween text fields.", _textView.Text);
 		}
 
-		[Fact]
+		[Fact][InitShutdown]
 		public void Copy_Without_Selection ()
 		{
 			_textView.Text = "This is the first line.\nThis is the second line.\n";
@@ -1357,7 +1377,7 @@ namespace Terminal.Gui.Views {
 			Assert.Equal (new Point (3, 3), _textView.CursorPosition);
 		}
 
-		[Fact]
+		[Fact][InitShutdown]
 		public void TabWidth_Setting_To_Zero_Changes_AllowsTab_To_False_If_True ()
 		{
 			Assert.Equal (4, _textView.TabWidth);
@@ -1375,7 +1395,7 @@ namespace Terminal.Gui.Views {
 			Assert.Equal ("TAB to jump between text fields.", _textView.Text);
 		}
 
-		[Fact]
+		[Fact][InitShutdown]
 		public void AllowsTab_Setting_To_True_Changes_TabWidth_To_Default_If_It_Is_Zero ()
 		{
 			_textView.TabWidth = 0;
@@ -1390,7 +1410,7 @@ namespace Terminal.Gui.Views {
 			Assert.True (_textView.Multiline);
 		}
 
-		[Fact]
+		[Fact][InitShutdown]
 		public void AllowsReturn_Setting_To_True_Changes_Multiline_To_True_If_It_Is_False ()
 		{
 			Assert.True (_textView.AllowsReturn);
@@ -1411,7 +1431,7 @@ namespace Terminal.Gui.Views {
 				"TAB to jump between text fields.", _textView.Text);
 		}
 
-		[Fact]
+		[Fact][InitShutdown]
 		public void Multiline_Setting_Changes_AllowsReturn_And_AllowsTab_And_Height ()
 		{
 			Assert.True (_textView.Multiline);
@@ -1438,7 +1458,7 @@ namespace Terminal.Gui.Views {
 			Assert.Equal ("Dim.Absolute(10)", _textView.Height.ToString ());
 		}
 
-		[Fact]
+		[Fact][InitShutdown]
 		public void Tab_Test_Follow_By_BackTab ()
 		{
 			Application.Top.Add (_textView);
@@ -1473,7 +1493,7 @@ namespace Terminal.Gui.Views {
 			Application.Run ();
 		}
 
-		[Fact]
+		[Fact][InitShutdown]
 		public void BackTab_Test_Follow_By_Tab ()
 		{
 			Application.Top.Add (_textView);
@@ -1515,7 +1535,7 @@ namespace Terminal.Gui.Views {
 			Application.Run ();
 		}
 
-		[Fact]
+		[Fact][InitShutdown]
 		public void Tab_Test_Follow_By_CursorLeft_And_Then_Follow_By_CursorRight ()
 		{
 			Application.Top.Add (_textView);
@@ -1557,7 +1577,7 @@ namespace Terminal.Gui.Views {
 			Application.Run ();
 		}
 
-		[Fact]
+		[Fact][InitShutdown]
 		public void Tab_Test_Follow_By_BackTab_With_Text ()
 		{
 			Application.Top.Add (_textView);
@@ -1592,7 +1612,7 @@ namespace Terminal.Gui.Views {
 			Application.Run ();
 		}
 
-		[Fact]
+		[Fact][InitShutdown]
 		public void Tab_Test_Follow_By_Home_And_Then_Follow_By_End_And_Then_Follow_By_BackTab_With_Text ()
 		{
 			Application.Top.Add (_textView);
@@ -1649,7 +1669,7 @@ namespace Terminal.Gui.Views {
 			Application.Run ();
 		}
 
-		[Fact]
+		[Fact][InitShutdown]
 		public void Tab_Test_Follow_By_CursorLeft_And_Then_Follow_By_CursorRight_With_Text ()
 		{
 			Application.Top.Add (_textView);

+ 15 - 0
UnitTests/TreeViewTests.cs

@@ -117,6 +117,8 @@ namespace Terminal.Gui.Views {
 			tree.Collapse (f);
 			//-+Factory
 			Assert.Equal (9, tree.GetContentWidth (true));
+
+			Application.Shutdown ();
 		}
 
 		[Fact]
@@ -155,6 +157,8 @@ namespace Terminal.Gui.Views {
 			tree.ScrollOffsetVertical = 5;
 			Assert.Equal (0, tree.GetContentWidth (true));
 			Assert.Equal (13, tree.GetContentWidth (false));
+
+			Application.Shutdown ();
 		}
 		/// <summary>
 		/// Tests that <see cref="TreeView.IsExpanded(object)"/> and <see cref="TreeView.Expand(object)"/> behaves correctly when an object cannot be expanded (because it has no children)
@@ -179,6 +183,8 @@ namespace Terminal.Gui.Views {
 			tree.Collapse (c);
 
 			Assert.False (tree.IsExpanded (c));
+
+			Application.Shutdown ();
 		}
 
 		/// <summary>
@@ -497,6 +503,8 @@ namespace Terminal.Gui.Views {
 
 			Assert.True (called);
 			Assert.Same (f, activated);
+
+			Application.Shutdown ();
 		}
 
 
@@ -567,6 +575,7 @@ namespace Terminal.Gui.Views {
 			Assert.True (called);
 			Assert.Same (f, activated);
 
+			Application.Shutdown ();
 		}
 
 		[Fact]
@@ -593,6 +602,8 @@ namespace Terminal.Gui.Views {
 			Assert.True (called);
 			Assert.Same (f, activated);
 			Assert.Same (f, tree.SelectedObject);
+
+			Application.Shutdown ();
 		}
 
 		[Fact]
@@ -622,6 +633,8 @@ namespace Terminal.Gui.Views {
 			Assert.False (called);
 			Assert.Null (activated);
 			Assert.Null (tree.SelectedObject);
+
+			Application.Shutdown ();
 		}
 
 
@@ -657,6 +670,8 @@ namespace Terminal.Gui.Views {
 			Assert.True (called);
 			Assert.Same (car1, activated);
 			Assert.Same (car1, tree.SelectedObject);
+
+			Application.Shutdown ();
 		}
 
 

+ 2 - 2
UnitTests/UnitTests.csproj

@@ -15,8 +15,8 @@
   </PropertyGroup>
 
   <ItemGroup>
-    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
-    <PackageReference Include="ReportGenerator" Version="4.8.8" />
+    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
+    <PackageReference Include="ReportGenerator" Version="4.8.9" />
     <PackageReference Include="System.Collections" Version="4.3.0" />
     <PackageReference Include="xunit" Version="2.4.1" />
     <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">

+ 165 - 0
UnitTests/ViewTests.cs

@@ -1166,6 +1166,171 @@ namespace Terminal.Gui.Views {
 			};
 
 			Application.Run ();
+
+			// Shutdown must be called to safely clean up Application if Init has been called
+			Application.Shutdown ();
+		}
+
+		[Fact]
+		public void SetWidth_CanSetWidth ()
+		{
+			var top = new View () {
+				X = 0,
+				Y = 0,
+				Width = 80,
+			};
+
+			var v = new View () {
+				Width = Dim.Fill ()
+			};
+			top.Add (v);
+
+			Assert.False (v.SetWidth (70, out int rWidth));
+			Assert.Equal (70, rWidth);
+
+			v.Width = Dim.Fill (1);
+			Assert.False (v.SetWidth (70, out rWidth));
+			Assert.Equal (69, rWidth);
+
+			v.Width = null;
+			Assert.True (v.SetWidth (70, out rWidth));
+			Assert.Equal (70, rWidth);
+
+			v.IsInitialized = true;
+			v.Width = Dim.Fill (1);
+			Assert.Throws<ArgumentException> (() => v.Width = 75);
+			v.LayoutStyle = LayoutStyle.Absolute;
+			v.Width = 75;
+			Assert.True (v.SetWidth (60, out rWidth));
+			Assert.Equal (60, rWidth);
+		}
+
+		[Fact]
+		public void SetHeight_CanSetHeight ()
+		{
+			var top = new View () {
+				X = 0,
+				Y = 0,
+				Height = 20
+			};
+
+			var v = new View () {
+				Height = Dim.Fill ()
+			};
+			top.Add (v);
+
+			Assert.False (v.SetHeight (10, out int rHeight));
+			Assert.Equal (10, rHeight);
+
+			v.Height = Dim.Fill (1);
+			Assert.False (v.SetHeight (10, out rHeight));
+			Assert.Equal (9, rHeight);
+
+			v.Height = null;
+			Assert.True (v.SetHeight (10, out rHeight));
+			Assert.Equal (10, rHeight);
+
+			v.IsInitialized = true;
+			v.Height = Dim.Fill (1);
+			Assert.Throws<ArgumentException> (() => v.Height = 15);
+			v.LayoutStyle = LayoutStyle.Absolute;
+			v.Height = 15;
+			Assert.True (v.SetHeight (5, out rHeight));
+			Assert.Equal (5, rHeight);
+		}
+
+		[Fact]
+		public void GetCurrentWidth_CanSetWidth ()
+		{
+			var top = new View () {
+				X = 0,
+				Y = 0,
+				Width = 80,
+			};
+
+			var v = new View () {
+				Width = Dim.Fill ()
+			};
+			top.Add (v);
+
+			Assert.False (v.GetCurrentWidth (out int cWidth));
+			Assert.Equal (80, cWidth);
+
+			v.Width = Dim.Fill (1);
+			Assert.False (v.GetCurrentWidth (out cWidth));
+			Assert.Equal (79, cWidth);
+		}
+
+		[Fact]
+		public void GetCurrentHeight_CanSetHeight ()
+		{
+			var top = new View () {
+				X = 0,
+				Y = 0,
+				Height = 20
+			};
+
+			var v = new View () {
+				Height = Dim.Fill ()
+			};
+			top.Add (v);
+
+			Assert.False (v.GetCurrentHeight (out int cHeight));
+			Assert.Equal (20, cHeight);
+
+			v.Height = Dim.Fill (1);
+			Assert.False (v.GetCurrentHeight (out cHeight));
+			Assert.Equal (19, cHeight);
+		}
+
+		[Fact]
+		public void AutoSize_False_ResizeView_Is_Always_False ()
+		{
+			var label = new Label () { AutoSize = false };
+
+			label.Text = "New text";
+
+			Assert.False (label.AutoSize);
+			Assert.Equal ("{X=0,Y=0,Width=0,Height=0}", label.Bounds.ToString ());
+		}
+
+		[Fact]
+		public void AutoSize_True_ResizeView_With_Dim_Absolute ()
+		{
+			var label = new Label ();
+
+			label.Text = "New text";
+
+			Assert.True (label.AutoSize);
+			Assert.Equal ("{X=0,Y=0,Width=8,Height=1}", label.Bounds.ToString ());
+		}
+
+		[Fact]
+		public void AutoSize_True_ResizeView_With_Dim_Fill ()
+		{
+			var win = new Window (new Rect (0, 0, 30, 80), "");
+			var label = new Label () { Width = Dim.Fill (), Height = Dim.Fill () };
+			win.Add (label);
+
+			label.Text = "New text\nNew line";
+			win.LayoutSubviews ();
+
+			Assert.True (label.AutoSize);
+			Assert.Equal ("{X=0,Y=0,Width=28,Height=78}", label.Bounds.ToString ());
+		}
+
+		[Fact]
+		public void AutoSize_True_SetWidthHeight_With_Dim_Fill_And_Dim_Absolute ()
+		{
+			var win = new Window (new Rect (0, 0, 30, 80), "");
+			var label = new Label () { Width = Dim.Fill () };
+			win.Add (label);
+
+			label.Text = "New text\nNew line";
+			win.LayoutSubviews ();
+
+			Assert.True (label.AutoSize);
+			Assert.Equal ("{X=0,Y=0,Width=28,Height=2}", label.Bounds.ToString ());
 		}
 	}
 }