浏览代码

Merge branch 'v2_develop' of tig:tig/Terminal.Gui into v2_develop

Tig Kindel 1 年之前
父节点
当前提交
136a42df58
共有 100 个文件被更改,包括 26275 次插入22427 次删除
  1. 1733 24
      .editorconfig
  2. 1 1
      .github/workflows/dotnet-core.yml
  3. 12 6
      CONTRIBUTING.md
  4. 66 58
      Example/Example.cs
  5. 200 180
      ReactiveExample/LoginView.cs
  6. 81 70
      ReactiveExample/LoginViewModel.cs
  7. 13 11
      ReactiveExample/Program.cs
  8. 2 2
      ReactiveExample/ReactiveExample.csproj
  9. 55 36
      ReactiveExample/TerminalScheduler.cs
  10. 51 0
      Terminal.Gui/Application.MainLoopSyncContext.cs
  11. 1776 1526
      Terminal.Gui/Application.cs
  12. 201 165
      Terminal.Gui/Clipboard/Clipboard.cs
  13. 119 109
      Terminal.Gui/Clipboard/ClipboardBase.cs
  14. 21 34
      Terminal.Gui/Clipboard/IClipboard.cs
  15. 24 37
      Terminal.Gui/Configuration/AppScope.cs
  16. 106 95
      Terminal.Gui/Configuration/AttributeJsonConverter.cs
  17. 50 50
      Terminal.Gui/Configuration/ColorJsonConverter.cs
  18. 110 99
      Terminal.Gui/Configuration/ColorSchemeJsonConverter.cs
  19. 109 85
      Terminal.Gui/Configuration/ConfigProperty.cs
  20. 592 517
      Terminal.Gui/Configuration/ConfigurationManager.cs
  21. 15 31
      Terminal.Gui/Configuration/ConfigurationManagerEventArgs.cs
  22. 58 41
      Terminal.Gui/Configuration/DictionaryJsonConverter.cs
  23. 164 122
      Terminal.Gui/Configuration/KeyCodeJsonConverter.cs
  24. 12 11
      Terminal.Gui/Configuration/KeyJsonConverter.cs
  25. 136 112
      Terminal.Gui/Configuration/RuneJsonConverter.cs
  26. 72 67
      Terminal.Gui/Configuration/Scope.cs
  27. 223 130
      Terminal.Gui/Configuration/ScopeJsonConverter.cs
  28. 16 22
      Terminal.Gui/Configuration/SerializableConfigurationProperty.cs
  29. 109 103
      Terminal.Gui/Configuration/SettingsScope.cs
  30. 130 164
      Terminal.Gui/Configuration/ThemeManager.cs
  31. 11 11
      Terminal.Gui/Configuration/ThemeScope.cs
  32. 921 1107
      Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs
  33. 2528 1718
      Terminal.Gui/ConsoleDrivers/ConsoleKeyMapping.cs
  34. 256 212
      Terminal.Gui/ConsoleDrivers/CursesDriver/ClipboardImpl.cs
  35. 1007 784
      Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs
  36. 230 214
      Terminal.Gui/ConsoleDrivers/CursesDriver/UnixMainLoop.cs
  37. 303 249
      Terminal.Gui/ConsoleDrivers/CursesDriver/UnmanagedLibrary.cs
  38. 693 583
      Terminal.Gui/ConsoleDrivers/CursesDriver/binding.cs
  39. 165 172
      Terminal.Gui/ConsoleDrivers/CursesDriver/constants.cs
  40. 48 139
      Terminal.Gui/ConsoleDrivers/CursesDriver/handles.cs
  41. 124 113
      Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqReq.cs
  42. 1347 1205
      Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqUtils.cs
  43. 1698 1978
      Terminal.Gui/ConsoleDrivers/FakeDriver/FakeConsole.cs
  44. 527 433
      Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs
  45. 36 41
      Terminal.Gui/ConsoleDrivers/FakeDriver/FakeMainLoop.cs
  46. 1760 1377
      Terminal.Gui/ConsoleDrivers/NetDriver.cs
  47. 843 702
      Terminal.Gui/ConsoleDrivers/WindowsDriver.cs
  48. 56 0
      Terminal.Gui/Drawing/AnsiColorCode.cs
  49. 107 0
      Terminal.Gui/Drawing/Attribute.cs
  50. 35 38
      Terminal.Gui/Drawing/Cell.cs
  51. 80 0
      Terminal.Gui/Drawing/Color.ColorExtensions.cs
  52. 63 0
      Terminal.Gui/Drawing/Color.ColorName.cs
  53. 93 0
      Terminal.Gui/Drawing/Color.ColorParseException.cs
  54. 616 0
      Terminal.Gui/Drawing/Color.Formatting.cs
  55. 97 0
      Terminal.Gui/Drawing/Color.Operators.cs
  56. 283 882
      Terminal.Gui/Drawing/Color.cs
  57. 176 0
      Terminal.Gui/Drawing/ColorScheme.Colors.cs
  58. 100 238
      Terminal.Gui/Drawing/ColorScheme.cs
  59. 439 689
      Terminal.Gui/Drawing/Glyphs.cs
  60. 27 0
      Terminal.Gui/Drawing/ICustomColorFormatter.cs
  61. 20 0
      Terminal.Gui/Drawing/IntersectionDefinition.cs
  62. 19 0
      Terminal.Gui/Drawing/IntersectionRuneType.cs
  63. 28 0
      Terminal.Gui/Drawing/IntersectionType.cs
  64. 846 837
      Terminal.Gui/Drawing/LineCanvas.cs
  65. 48 0
      Terminal.Gui/Drawing/LineStyle.cs
  66. 58 65
      Terminal.Gui/Drawing/Ruler.cs
  67. 194 196
      Terminal.Gui/Drawing/StraightLine.cs
  68. 235 212
      Terminal.Gui/Drawing/StraightLineExtensions.cs
  69. 297 298
      Terminal.Gui/Drawing/Thickness.cs
  70. 12 24
      Terminal.Gui/Drawing/ThicknessEventArgs.cs
  71. 93 116
      Terminal.Gui/FileServices/AllowedType.cs
  72. 167 137
      Terminal.Gui/FileServices/DefaultFileOperations.cs
  73. 19 24
      Terminal.Gui/FileServices/DefaultSearchMatcher.cs
  74. 97 111
      Terminal.Gui/FileServices/FileDialogHistory.cs
  75. 90 84
      Terminal.Gui/FileServices/FileDialogState.cs
  76. 198 228
      Terminal.Gui/FileServices/FileDialogStyle.cs
  77. 107 103
      Terminal.Gui/FileServices/FileSystemInfoStats.cs
  78. 60 80
      Terminal.Gui/FileServices/FileSystemTreeBuilder.cs
  79. 18 27
      Terminal.Gui/FileServices/FilesSelectedEventArgs.cs
  80. 32 41
      Terminal.Gui/FileServices/IFileOperations.cs
  81. 10 20
      Terminal.Gui/FileServices/ISearchMatcher.cs
  82. 263 414
      Terminal.Gui/Input/Command.cs
  83. 14 25
      Terminal.Gui/Input/GrabMouseEventArgs.cs
  84. 966 1044
      Terminal.Gui/Input/Key.cs
  85. 227 253
      Terminal.Gui/Input/KeyBinding.cs
  86. 17 27
      Terminal.Gui/Input/KeyChangedEventArgs.cs
  87. 9 19
      Terminal.Gui/Input/KeystrokeNavigatorEventArgs.cs
  88. 126 175
      Terminal.Gui/Input/Mouse.cs
  89. 28 29
      Terminal.Gui/Input/MouseEventEventArgs.cs
  90. 16 26
      Terminal.Gui/Input/MouseFlagsChangedEventArgs.cs
  91. 9 20
      Terminal.Gui/Input/PointEventArgs.cs
  92. 139 179
      Terminal.Gui/Input/Responder.cs
  93. 171 149
      Terminal.Gui/Input/ShortcutHelper.cs
  94. 5 0
      Terminal.Gui/IterationEventArgs.cs
  95. 383 372
      Terminal.Gui/MainLoop.cs
  96. 47 72
      Terminal.Gui/RunState.cs
  97. 9 21
      Terminal.Gui/RunStateEventArgs.cs
  98. 242 195
      Terminal.Gui/StackExtensions.cs
  99. 25 11
      Terminal.Gui/Terminal.Gui.csproj
  100. 5 0
      Terminal.Gui/Terminal.Gui.csproj.DotSettings

文件差异内容过多而无法显示
+ 1733 - 24
.editorconfig


+ 1 - 1
.github/workflows/dotnet-core.yml

@@ -17,7 +17,7 @@ jobs:
     - name: Setup dotnet
       uses: actions/setup-dotnet@v4
       with:
-        dotnet-version: 8.0
+        dotnet-version: 8.x
         dotnet-quality: 'ga'
 
     - name: Install dependencies

+ 12 - 6
CONTRIBUTING.md

@@ -33,7 +33,7 @@ You now have your own fork and a local repo that references it as `origin`. Your
 
 ### Starting to Make a Change
 
-Ensure your local `develop` branch is up-to-date with `upstream` (`github.com/gui-cs/Terminal.Gui`):
+Ensure your local `develop` (for v1) or `v2_develop` (for v2) branch is up-to-date with `upstream` (`github.com/gui-cs/Terminal.Gui`):
 ```powershell
 cd ./Terminal.Gui
 git checkout develop
@@ -45,12 +45,12 @@ Create a new local branch:
 git checkout -b my_new_branch
 ```
 
-#### Making Changes
+### Making Changes
 Follow all the guidelines below.
 
-* Coding Style
-* Unit Tests
-* Sample Code
+* [Coding Style](#TerminalGui-Coding-Style)
+* [Unit Tests](#Unit-Tests)
+* [Sample Code](#Sample-Code)
 * API Documentation
 * etc...
 
@@ -92,7 +92,13 @@ Follow the template instructions found on Github.
 
 ## Terminal.Gui Coding Style
 
-**Terminal.Gui** follows the [Mono Coding Guidelines](https://www.mono-project.com/community/contributing/coding-guidelines/). [`/.editorconfig`](https://github.com/gui-cs/Terminal.Gui/blob/b0a43ba338adf5ec069066e5a7dff8fea39b41db/.editorconfig) enforces this style in Visual Studio. Use `Ctrl-K-D` in Visual Studio to have it reformat code.
+**Terminal.Gui** uses a derivative of the [Microsoft C# Coding Conventions](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/coding-conventions), with any deviations from those (somewhat older) conventions codified in the .editorconfig for the solution, as well as even more specific definitions in team-shared dotsettings files, used by ReSharper and Rider.\
+Before you commit code, please run the formatting rules on **only the code file(s) you have modified**, in one of the following ways, in order of most preferred to least preferred:
+
+ 1. `Ctrl-E-C` if using ReSharper or Rider
+ 2. Running the free [CleanupCode](https://www.jetbrains.com/help/resharper/CleanupCode.html) tool from JetBrains (this applies the same formatting rules as if you had used ReSharper or Rider, but is free for all users, if you don't have a license for those products)
+     - Run at the command line, from the solution root directory, as: `cleanupcode.exe relative/path/to/your/file.cs`
+ 3. If you are unable to use either of those options, the last resort is to use `Ctrl-K-D` in Visual Studio (with default C# developer key bindings), to apply the subset of the formatting rules that Visual Studio can apply.
 
 ## User Experience Tenets
 

+ 66 - 58
Example/Example.cs

@@ -3,70 +3,78 @@
 
 // A simple Terminal.Gui example in C# - using C# 9.0 Top-level statements
 
+using System;
 using Terminal.Gui;
 
 Application.Run<ExampleWindow> ();
 
-System.Console.WriteLine ($"Username: {((ExampleWindow)Application.Top).usernameText.Text}");
+Console.WriteLine ($"Username: {((ExampleWindow)Application.Top).UserNameText.Text}");
 
 // Before the application exits, reset Terminal.Gui for clean shutdown
 Application.Shutdown ();
 
 // Defines a top-level window with border and title
-public class ExampleWindow : Window {
-	public TextField usernameText;
-	
-	public ExampleWindow ()
-	{
-		Title = $"Example App ({Application.QuitKey} to quit)";
-
-		// Create input components and labels
-		var usernameLabel = new Label () { 
-			Text = "Username:" 
-		};
-
-		usernameText = new TextField ("") {
-			// Position text field adjacent to the label
-			X = Pos.Right (usernameLabel) + 1,
-
-			// Fill remaining horizontal space
-			Width = Dim.Fill (),
-		};
-
-		var passwordLabel = new Label () {
-			Text = "Password:",
-			X = Pos.Left (usernameLabel),
-			Y = Pos.Bottom (usernameLabel) + 1
-		};
-
-		var passwordText = new TextField ("") {
-			Secret = true,
-			// align with the text box above
-			X = Pos.Left (usernameText),
-			Y = Pos.Top (passwordLabel),
-			Width = Dim.Fill (),
-		};
-
-		// Create login button
-		var btnLogin = new Button () {
-			Text = "Login",
-			Y = Pos.Bottom(passwordLabel) + 1,
-			// center the login button horizontally
-			X = Pos.Center (),
-			IsDefault = true,
-		};
-
-		// When login button is clicked display a message popup
-		btnLogin.Clicked += (s,e) => {
-			if (usernameText.Text == "admin" && passwordText.Text == "password") {
-				MessageBox.Query ("Logging In", "Login Successful", "Ok");
-				Application.RequestStop ();
-			} else {
-				MessageBox.ErrorQuery ("Logging In", "Incorrect username or password", "Ok");
-			}
-		};
-
-		// Add the views to the Window
-		Add (usernameLabel, usernameText, passwordLabel, passwordText, btnLogin);
-	}
-}
+public class ExampleWindow : Window
+{
+    public TextField UserNameText;
+
+    public ExampleWindow ()
+    {
+        Title = $"Example App ({Application.QuitKey} to quit)";
+
+        // Create input components and labels
+        var usernameLabel = new Label { Text = "Username:" };
+
+        UserNameText = new TextField
+        {
+            // Position text field adjacent to the label
+            X = Pos.Right (usernameLabel) + 1,
+
+            // Fill remaining horizontal space
+            Width = Dim.Fill ()
+        };
+
+        var passwordLabel = new Label
+        {
+            Text = "Password:", X = Pos.Left (usernameLabel), Y = Pos.Bottom (usernameLabel) + 1
+        };
+
+        var passwordText = new TextField
+        {
+            Secret = true,
+
+            // align with the text box above
+            X = Pos.Left (UserNameText),
+            Y = Pos.Top (passwordLabel),
+            Width = Dim.Fill ()
+        };
+
+        // Create login button
+        var btnLogin = new Button
+        {
+            Text = "Login",
+            Y = Pos.Bottom (passwordLabel) + 1,
+
+            // center the login button horizontally
+            X = Pos.Center (),
+            IsDefault = true
+        };
+
+        // When login button is clicked display a message popup
+        btnLogin.Accept += (s, e) =>
+                            {
+                                if (UserNameText.Text == "admin" && passwordText.Text == "password")
+                                {
+                                    MessageBox.Query ("Logging In", "Login Successful", "Ok");
+                                    Application.RequestStop ();
+                                }
+                                else
+                                {
+                                    MessageBox.ErrorQuery ("Logging In", "Incorrect username or password", "Ok");
+                                }
+                            };
+
+        // Add the views to the Window
+        Add (usernameLabel, UserNameText, passwordLabel, passwordText, btnLogin);
+    }
+}

+ 200 - 180
ReactiveExample/LoginView.cs

@@ -1,185 +1,205 @@
 using System.Reactive.Disposables;
 using System.Reactive.Linq;
-using System.Text;
+using ReactiveMarbles.ObservableEvents;
 using ReactiveUI;
 using Terminal.Gui;
-using ReactiveMarbles.ObservableEvents;
 
-namespace ReactiveExample {
-	public class LoginView : Window, IViewFor<LoginViewModel> {
-		readonly CompositeDisposable _disposable = new CompositeDisposable();
-		
-		public LoginView (LoginViewModel viewModel) : base() {
-			Title = "Reactive Extensions Example";
-			ViewModel = viewModel;
-			var usernameLengthLabel = UsernameLengthLabel (TitleLabel ());
-			var usernameInput = UsernameInput (usernameLengthLabel);
-			var passwordLengthLabel = PasswordLengthLabel (usernameInput);
-			var passwordInput = PasswordInput (passwordLengthLabel);
-			var validationLabel = ValidationLabel (passwordInput);
-			var loginButton = LoginButton (validationLabel);
-			var clearButton = ClearButton (loginButton);
-			LoginProgressLabel (clearButton);
-		}
-		
-		public LoginViewModel ViewModel { get; set; }
-
-		protected override void Dispose (bool disposing) {
-			_disposable.Dispose ();
-			base.Dispose (disposing);
-		}
-
-		Label TitleLabel () {
-			var label = new Label("Login Form");
-			Add (label);
-			return label;
-		}
-
-		TextField UsernameInput (View previous) {
-			var usernameInput = new TextField (ViewModel.Username) {
-				X = Pos.Left(previous),
-				Y = Pos.Top(previous) + 1,
-				Width = 40
-			};
-			ViewModel
-				.WhenAnyValue (x => x.Username)
-				.BindTo (usernameInput, x => x.Text)
-				.DisposeWith (_disposable);
-			usernameInput
-				.Events ()
-				.TextChanged
-				.Select (old => usernameInput.Text)
-				.DistinctUntilChanged ()
-				.BindTo (ViewModel, x => x.Username)
-				.DisposeWith (_disposable);
-			Add (usernameInput);
-			return usernameInput;
-		}
-
-		Label UsernameLengthLabel (View previous) {
-			var usernameLengthLabel = new Label {
-				X = Pos.Left(previous),
-				Y = Pos.Top(previous) + 1,
-				Width = 40
-			};
-			ViewModel
-				.WhenAnyValue (x => x.UsernameLength)
-				.Select (length => $"Username ({length} characters)")
-				.BindTo (usernameLengthLabel, x => x.Text)
-				.DisposeWith (_disposable);
-			Add (usernameLengthLabel);
-			return usernameLengthLabel;
-		}
-
-		TextField PasswordInput (View previous) {
-			var passwordInput = new TextField (ViewModel.Password) {
-				X = Pos.Left(previous),
-				Y = Pos.Top(previous) + 1,
-				Width = 40
-			};
-			ViewModel
-				.WhenAnyValue (x => x.Password)
-				.BindTo (passwordInput, x => x.Text)
-				.DisposeWith (_disposable);
-			passwordInput
-				.Events ()
-				.TextChanged
-				.Select (old => passwordInput.Text)
-				.DistinctUntilChanged ()
-				.BindTo (ViewModel, x => x.Password)
-				.DisposeWith (_disposable);
-			Add (passwordInput);
-			return passwordInput;
-		}
-
-		Label PasswordLengthLabel (View previous) {
-			var passwordLengthLabel = new Label {
-				X = Pos.Left(previous),
-				Y = Pos.Top(previous) + 1,
-				Width = 40
-			};
-			ViewModel
-				.WhenAnyValue (x => x.PasswordLength)
-				.Select (length => $"Password ({length} characters)")
-				.BindTo (passwordLengthLabel, x => x.Text)
-				.DisposeWith (_disposable);
-			Add (passwordLengthLabel);
-			return passwordLengthLabel;
-		}
-
-		Label ValidationLabel (View previous) {
-			var error = "Please, enter user name and password.";
-			var success = "The input is valid!";
-			var validationLabel = new Label(error) {
-				X = Pos.Left(previous),
-				Y = Pos.Top(previous) + 1,
-				Width = 40
-			};
-			ViewModel
-				.WhenAnyValue (x => x.IsValid)	
-				.Select (valid => valid ? success : error)
-				.BindTo (validationLabel, x => x.Text)
-				.DisposeWith (_disposable);
-			ViewModel
-				.WhenAnyValue (x => x.IsValid)	
-				.Select (valid => valid ? Colors.ColorSchemes ["Base"] : Colors.ColorSchemes ["Error"])
-				.BindTo (validationLabel, x => x.ColorScheme)
-				.DisposeWith (_disposable);
-			Add (validationLabel);
-			return validationLabel;
-		}
-
-		Label LoginProgressLabel (View previous) {
-			var progress = "Logging in...";
-			var idle = "Press 'Login' to log in.";
-			var loginProgressLabel = new Label(idle) {
-				X = Pos.Left(previous),
-				Y = Pos.Top(previous) + 1,
-				Width = 40
-			};
-			ViewModel
-				.WhenAnyObservable (x => x.Login.IsExecuting)
-				.Select (executing => executing ? progress : idle)
-				.ObserveOn (RxApp.MainThreadScheduler)
-				.BindTo (loginProgressLabel, x => x.Text)
-				.DisposeWith (_disposable);
-			Add (loginProgressLabel);
-			return loginProgressLabel;
-		}
-
-		Button LoginButton (View previous) {
-			var loginButton = new Button ("Login") {
-				X = Pos.Left(previous),
-				Y = Pos.Top(previous) + 1,
-				Width = 40
-			};
-			loginButton
-				.Events ()
-				.Clicked
-				.InvokeCommand (ViewModel, x => x.Login)
-				.DisposeWith (_disposable);
-			Add (loginButton);
-			return loginButton;
-		}
-
-		Button ClearButton (View previous) {
-			var clearButton = new Button("Clear") {
-				X = Pos.Left(previous),
-				Y = Pos.Top(previous) + 1,
-				Width = 40
-			};
-			clearButton
-				.Events ()
-				.Clicked
-				.InvokeCommand (ViewModel, x => x.Clear)
-				.DisposeWith (_disposable);
-			Add (clearButton);
-			return clearButton;
-		}
-		
-		object IViewFor.ViewModel {
-			get => ViewModel;
-			set => ViewModel = (LoginViewModel) value;
-		}
-	}
-}
+namespace ReactiveExample;
+
+public class LoginView : Window, IViewFor<LoginViewModel>
+{
+    private readonly CompositeDisposable _disposable = new ();
+
+    public LoginView (LoginViewModel viewModel)
+    {
+        Title = $"Reactive Extensions Example - {Application.QuitKey} to Exit";
+        ViewModel = viewModel;
+        Label usernameLengthLabel = UsernameLengthLabel (TitleLabel ());
+        TextField usernameInput = UsernameInput (usernameLengthLabel);
+        Label passwordLengthLabel = PasswordLengthLabel (usernameLengthLabel);
+        TextField passwordInput = PasswordInput (passwordLengthLabel);
+        Label validationLabel = ValidationLabel (passwordInput);
+        Button loginButton = LoginButton (validationLabel);
+        Button clearButton = ClearButton (loginButton);
+        LoginProgressLabel (clearButton);
+    }
+
+    public LoginViewModel ViewModel { get; set; }
+
+    object IViewFor.ViewModel
+    {
+        get => ViewModel;
+        set => ViewModel = (LoginViewModel)value;
+    }
+
+    protected override void Dispose (bool disposing)
+    {
+        _disposable.Dispose ();
+        base.Dispose (disposing);
+    }
+
+    private Button ClearButton (View previous)
+    {
+        var clearButton = new Button
+        {
+            X = Pos.Left (previous), Y = Pos.Top (previous) + 1, Text = "_Clear"
+        };
+
+        clearButton
+            .Events ()
+            .Accept
+            .InvokeCommand (ViewModel, x => x.Clear)
+            .DisposeWith (_disposable);
+        Add (clearButton);
+
+        return clearButton;
+    }
+
+    private Button LoginButton (View previous)
+    {
+        var loginButton = new Button
+        {
+            X = Pos.Left (previous), Y = Pos.Top (previous) + 1, Text = "_Login"
+        };
+
+        loginButton
+            .Events ()
+            .Accept
+            .InvokeCommand (ViewModel, x => x.Login)
+            .DisposeWith (_disposable);
+        Add (loginButton);
+
+        return loginButton;
+    }
+
+    private Label LoginProgressLabel (View previous)
+    {
+        var progress = "Logging in...";
+        var idle = "Press 'Login' to log in.";
+
+        var loginProgressLabel = new Label
+        {
+            AutoSize = false,  X = Pos.Left (previous), Y = Pos.Top (previous) + 1, Width = 40, Height = 1, Text = idle
+        };
+
+        ViewModel
+            .WhenAnyObservable (x => x.Login.IsExecuting)
+            .Select (executing => executing ? progress : idle)
+            .ObserveOn (RxApp.MainThreadScheduler)
+            .BindTo (loginProgressLabel, x => x.Text)
+            .DisposeWith (_disposable);
+        Add (loginProgressLabel);
+
+        return loginProgressLabel;
+    }
+
+    private TextField PasswordInput (View previous)
+    {
+        var passwordInput = new TextField
+        {
+            X = Pos.Right (previous) + 1, Y = Pos.Top (previous), Width = 40, Text = ViewModel.Password
+        };
+
+        ViewModel
+            .WhenAnyValue (x => x.Password)
+            .BindTo (passwordInput, x => x.Text)
+            .DisposeWith (_disposable);
+
+        passwordInput
+            .Events ()
+            .TextChanged
+            .Select (old => passwordInput.Text)
+            .DistinctUntilChanged ()
+            .BindTo (ViewModel, x => x.Password)
+            .DisposeWith (_disposable);
+        Add (passwordInput);
+
+        return passwordInput;
+    }
+
+    private Label PasswordLengthLabel (View previous)
+    {
+        var passwordLengthLabel = new Label { X = Pos.Left (previous), Y = Pos.Top (previous) + 1, };
+
+        ViewModel
+            .WhenAnyValue (x => x.PasswordLength)
+            .Select (length => $"_Password ({length} characters):")
+            .BindTo (passwordLengthLabel, x => x.Text)
+            .DisposeWith (_disposable);
+        Add (passwordLengthLabel);
+
+        return passwordLengthLabel;
+    }
+
+    private Label TitleLabel ()
+    {
+        var label = new Label { Text = "Login Form" };
+        Add (label);
+
+        return label;
+    }
+
+    private TextField UsernameInput (View previous)
+    {
+        var usernameInput = new TextField
+        {
+            X = Pos.Right (previous) + 1, Y = Pos.Top (previous), Width = 40, Text = ViewModel.Username
+        };
+
+        ViewModel
+            .WhenAnyValue (x => x.Username)
+            .BindTo (usernameInput, x => x.Text)
+            .DisposeWith (_disposable);
+
+        usernameInput
+            .Events ()
+            .TextChanged
+            .Select (old => usernameInput.Text)
+            .DistinctUntilChanged ()
+            .BindTo (ViewModel, x => x.Username)
+            .DisposeWith (_disposable);
+        Add (usernameInput);
+
+        return usernameInput;
+    }
+
+    private Label UsernameLengthLabel (View previous)
+    {
+        var usernameLengthLabel = new Label { X = Pos.Left (previous), Y = Pos.Top (previous) + 1 };
+
+        ViewModel
+            .WhenAnyValue (x => x.UsernameLength)
+            .Select (length => $"_Username ({length} characters):")
+            .BindTo (usernameLengthLabel, x => x.Text)
+            .DisposeWith (_disposable);
+        Add (usernameLengthLabel);
+
+        return usernameLengthLabel;
+    }
+
+    private Label ValidationLabel (View previous)
+    {
+        var error = "Please enter a valid user name and password.";
+        var success = "The input is valid!";
+
+        var validationLabel = new Label
+        {
+           X = Pos.Left (previous), Y = Pos.Top (previous) + 1, Text = error
+        };
+
+        ViewModel
+            .WhenAnyValue (x => x.IsValid)
+            .Select (valid => valid ? success : error)
+            .BindTo (validationLabel, x => x.Text)
+            .DisposeWith (_disposable);
+
+        ViewModel
+            .WhenAnyValue (x => x.IsValid)
+            .Select (valid => valid ? Colors.ColorSchemes ["Base"] : Colors.ColorSchemes ["Error"])
+            .BindTo (validationLabel, x => x.ColorScheme)
+            .DisposeWith (_disposable);
+        Add (validationLabel);
+
+        return validationLabel;
+    }
+}

+ 81 - 70
ReactiveExample/LoginViewModel.cs

@@ -1,78 +1,89 @@
 using System;
+using System.ComponentModel;
 using System.Reactive;
 using System.Reactive.Linq;
 using System.Runtime.Serialization;
 using System.Threading.Tasks;
-using System.Text;
 using ReactiveUI;
 using ReactiveUI.Fody.Helpers;
 
-namespace ReactiveExample {
-	//
-	// This view model can be easily shared across different UI frameworks.
-	// For example, if you have a WPF or XF app with view models written
-	// this way, you can easily port your app to Terminal.Gui by implementing
-	// the views with Terminal.Gui classes and ReactiveUI bindings.
-	//
-	// We mark the view model with the [DataContract] attributes and this
-	// allows you to save the view model class to the disk, and then to read
-	// the view model from the disk, making your app state persistent.
-	// See also: https://www.reactiveui.net../docs/handbook/data-persistence/
-	//
-	[DataContract]
-	public class LoginViewModel : ReactiveObject {
-		readonly ObservableAsPropertyHelper<int> _usernameLength;
-		readonly ObservableAsPropertyHelper<int> _passwordLength;
-		readonly ObservableAsPropertyHelper<bool> _isValid;
-		
-		public LoginViewModel () {
-			var canLogin = this.WhenAnyValue (
-				x => x.Username, 
-				x => x.Password,
-				(username, password) =>
-					!string.IsNullOrEmpty (username) &&
-					!string.IsNullOrEmpty (password));
-			
-			_isValid = canLogin.ToProperty (this, x => x.IsValid);
-			Login = ReactiveCommand.CreateFromTask (
-				() => Task.Delay (TimeSpan.FromSeconds (1)),
-				canLogin);
-			
-			_usernameLength = this
-				.WhenAnyValue (x => x.Username)
-				.Select (name => name.Length)
-				.ToProperty (this, x => x.UsernameLength);
-			_passwordLength = this
-				.WhenAnyValue (x => x.Password)
-				.Select (password => password.Length)
-				.ToProperty (this, x => x.PasswordLength);
-			
-			Clear = ReactiveCommand.Create (() => { });
-			Clear.Subscribe (unit => {
-				Username = string.Empty;
-				Password = string.Empty;
-			});
-		}
-		
-		[Reactive, DataMember]
-		public string Username { get; set; } = string.Empty;
-		
-		[Reactive, DataMember]
-		public string Password { get; set; } = string.Empty;
-		
-		[IgnoreDataMember]
-		public int UsernameLength => _usernameLength.Value;
-		
-		[IgnoreDataMember]
-		public int PasswordLength => _passwordLength.Value;
-
-		[IgnoreDataMember]
-		public ReactiveCommand<Unit, Unit> Login { get; }
-		
-		[IgnoreDataMember]
-		public ReactiveCommand<Unit, Unit> Clear { get; }
-		
-		[IgnoreDataMember]
-		public bool IsValid => _isValid.Value;
-	}
-}
+namespace ReactiveExample;
+
+//
+// This view model can be easily shared across different UI frameworks.
+// For example, if you have a WPF or XF app with view models written
+// this way, you can easily port your app to Terminal.Gui by implementing
+// the views with Terminal.Gui classes and ReactiveUI bindings.
+//
+// We mark the view model with the [DataContract] attributes and this
+// allows you to save the view model class to the disk, and then to read
+// the view model from the disk, making your app state persistent.
+// See also: https://www.reactiveui.net../docs/handbook/data-persistence/
+//
+[DataContract]
+public class LoginViewModel : ReactiveObject
+{
+    private readonly ObservableAsPropertyHelper<bool> _isValid;
+    private readonly ObservableAsPropertyHelper<int> _passwordLength;
+    private readonly ObservableAsPropertyHelper<int> _usernameLength;
+
+    public LoginViewModel ()
+    {
+        IObservable<bool> canLogin = this.WhenAnyValue (
+                                                        x => x.Username,
+                                                        x => x.Password,
+                                                        (username, password) =>
+                                                            !string.IsNullOrEmpty (username) && !string.IsNullOrEmpty (password)
+                                                       );
+
+        _isValid = canLogin.ToProperty (this, x => x.IsValid);
+
+        Login = ReactiveCommand.CreateFromTask<CancelEventArgs> (
+                                                                 e => Task.Delay (TimeSpan.FromSeconds (1)),
+                                                                 canLogin
+                                                                );
+
+        _usernameLength = this
+                          .WhenAnyValue (x => x.Username)
+                          .Select (name => name.Length)
+                          .ToProperty (this, x => x.UsernameLength);
+
+        _passwordLength = this
+                          .WhenAnyValue (x => x.Password)
+                          .Select (password => password.Length)
+                          .ToProperty (this, x => x.PasswordLength);
+
+        Clear = ReactiveCommand.Create<CancelEventArgs> (e => { });
+
+        Clear.Subscribe (
+                         unit =>
+                         {
+                             Username = string.Empty;
+                             Password = string.Empty;
+                         }
+                        );
+    }
+
+    [IgnoreDataMember]
+    public ReactiveCommand<CancelEventArgs, Unit> Clear { get; }
+
+    [IgnoreDataMember]
+    public bool IsValid => _isValid.Value;
+
+    [IgnoreDataMember]
+    public ReactiveCommand<CancelEventArgs, Unit> Login { get; }
+
+    [Reactive]
+    [DataMember]
+    public string Password { get; set; } = string.Empty;
+
+    [IgnoreDataMember]
+    public int PasswordLength => _passwordLength.Value;
+
+    [Reactive]
+    [DataMember]
+    public string Username { get; set; } = string.Empty;
+
+    [IgnoreDataMember]
+    public int UsernameLength => _usernameLength.Value;
+}

+ 13 - 11
ReactiveExample/Program.cs

@@ -2,14 +2,16 @@
 using ReactiveUI;
 using Terminal.Gui;
 
-namespace ReactiveExample {
-	public static class Program {
-		static void Main (string [] args) {
-			Application.Init ();
-			RxApp.MainThreadScheduler = TerminalScheduler.Default;
-			RxApp.TaskpoolScheduler = TaskPoolScheduler.Default;
-			Application.Run (new LoginView (new LoginViewModel ()));
-			Application.Shutdown ();
-		}
-	}
-}
+namespace ReactiveExample;
+
+public static class Program
+{
+    private static void Main (string [] args)
+    {
+        Application.Init ();
+        RxApp.MainThreadScheduler = TerminalScheduler.Default;
+        RxApp.TaskpoolScheduler = TaskPoolScheduler.Default;
+        Application.Run (new LoginView (new LoginViewModel ()));
+        Application.Shutdown ();
+    }
+}

+ 2 - 2
ReactiveExample/ReactiveExample.csproj

@@ -11,8 +11,8 @@
     <InformationalVersion>2.0</InformationalVersion>
   </PropertyGroup>
   <ItemGroup>
-    <PackageReference Include="ReactiveUI.Fody" Version="19.5.39" />
-    <PackageReference Include="ReactiveUI" Version="19.5.39" />
+    <PackageReference Include="ReactiveUI.Fody" Version="19.5.41" />
+    <PackageReference Include="ReactiveUI" Version="19.5.41" />
     <PackageReference Include="ReactiveMarbles.ObservableEvents.SourceGenerator" Version="1.3.1" PrivateAssets="all" />
   </ItemGroup>
   <ItemGroup>

+ 55 - 36
ReactiveExample/TerminalScheduler.cs

@@ -3,39 +3,58 @@ using System.Reactive.Concurrency;
 using System.Reactive.Disposables;
 using Terminal.Gui;
 
-namespace ReactiveExample {
-	public class TerminalScheduler : LocalScheduler {
-		public static readonly TerminalScheduler Default = new TerminalScheduler();
-		TerminalScheduler () { }
-
-		public override IDisposable Schedule<TState> (
-			TState state, TimeSpan dueTime,
-			Func<IScheduler, TState, IDisposable> action) {
-			
-			IDisposable PostOnMainLoop() {
-				var composite = new CompositeDisposable(2);
-				var cancellation = new CancellationDisposable();
-				Application.Invoke (() => {
-					if (!cancellation.Token.IsCancellationRequested)
-						composite.Add(action(this, state));
-				});
-				composite.Add(cancellation);
-				return composite;
-			}
-
-			IDisposable PostOnMainLoopAsTimeout () {
-				var composite = new CompositeDisposable (2);
-				var timeout = Application.AddTimeout (dueTime, () => {
-					composite.Add(action (this, state));
-					return false;
-				});
-				composite.Add (Disposable.Create (() => Application.RemoveTimeout (timeout)));
-				return composite;
-			}
-
-			return dueTime == TimeSpan.Zero 
-				? PostOnMainLoop ()
-				: PostOnMainLoopAsTimeout ();
-		}
-	}
-}
+namespace ReactiveExample;
+
+public class TerminalScheduler : LocalScheduler
+{
+    public static readonly TerminalScheduler Default = new ();
+    private TerminalScheduler () { }
+
+    public override IDisposable Schedule<TState> (
+        TState state,
+        TimeSpan dueTime,
+        Func<IScheduler, TState, IDisposable> action
+    )
+    {
+        IDisposable PostOnMainLoop ()
+        {
+            var composite = new CompositeDisposable (2);
+            var cancellation = new CancellationDisposable ();
+
+            Application.Invoke (
+                                () =>
+                                {
+                                    if (!cancellation.Token.IsCancellationRequested)
+                                    {
+                                        composite.Add (action (this, state));
+                                    }
+                                }
+                               );
+            composite.Add (cancellation);
+
+            return composite;
+        }
+
+        IDisposable PostOnMainLoopAsTimeout ()
+        {
+            var composite = new CompositeDisposable (2);
+
+            object timeout = Application.AddTimeout (
+                                                     dueTime,
+                                                     () =>
+                                                     {
+                                                         composite.Add (action (this, state));
+
+                                                         return false;
+                                                     }
+                                                    );
+            composite.Add (Disposable.Create (() => Application.RemoveTimeout (timeout)));
+
+            return composite;
+        }
+
+        return dueTime == TimeSpan.Zero
+                   ? PostOnMainLoop ()
+                   : PostOnMainLoopAsTimeout ();
+    }
+}

+ 51 - 0
Terminal.Gui/Application.MainLoopSyncContext.cs

@@ -0,0 +1,51 @@
+namespace Terminal.Gui;
+
+public static partial class Application
+{
+    /// <summary>
+    ///     provides the sync context set while executing code in Terminal.Gui, to let
+    ///     users use async/await on their code
+    /// </summary>
+    private sealed class MainLoopSyncContext : SynchronizationContext
+    {
+        public override SynchronizationContext CreateCopy () { return new MainLoopSyncContext (); }
+
+        public override void Post (SendOrPostCallback d, object state)
+        {
+            MainLoop.AddIdle (
+                              () =>
+                              {
+                                  d (state);
+
+                                  return false;
+                              }
+                             );
+        }
+
+        //_mainLoop.Driver.Wakeup ();
+        public override void Send (SendOrPostCallback d, object state)
+        {
+            if (Thread.CurrentThread.ManagedThreadId == _mainThreadId)
+            {
+                d (state);
+            }
+            else
+            {
+                var wasExecuted = false;
+
+                Invoke (
+                        () =>
+                        {
+                            d (state);
+                            wasExecuted = true;
+                        }
+                       );
+
+                while (!wasExecuted)
+                {
+                    Thread.Sleep (15);
+                }
+            }
+        }
+    }
+}

+ 1776 - 1526
Terminal.Gui/Application.cs

@@ -1,19 +1,12 @@
-using System;
-using System.Collections.Generic;
-using System.Threading;
-using System.Linq;
 using System.Globalization;
 using System.Reflection;
-using System.IO;
 using System.Text.Json.Serialization;
 
 namespace Terminal.Gui;
 
-/// <summary>
-/// A static, singleton class representing the application. This class is the entry point for the application.
-/// </summary>
+/// <summary>A static, singleton class representing the application. This class is the entry point for the application.</summary>
 /// <example>
-/// <code>
+///     <code>
 /// // A simple Terminal.Gui app that creates a window with a frame and title with 
 /// // 5 rows/columns of padding.
 /// Application.Init();
@@ -28,1525 +21,1782 @@ namespace Terminal.Gui;
 /// Application.Shutdown();
 /// </code>
 /// </example>
-/// <remarks>
-/// TODO: Flush this out.
-/// </remarks>
-public static partial class Application {
-
-	// IMPORTANT: Ensure all property/fields are reset here. See Init_ResetState_Resets_Properties unit test.
-	// Encapsulate all setting of initial state for Application; Having
-	// this in a function like this ensures we don't make mistakes in
-	// guaranteeing that the state of this singleton is deterministic when Init
-	// starts running and after Shutdown returns.
-	internal static void ResetState ()
-	{
-		// Shutdown is the bookend for Init. As such it needs to clean up all resources
-		// Init created. Apps that do any threading will need to code defensively for this.
-		// e.g. see Issue #537
-		foreach (var t in _topLevels) {
-			t.Running = false;
-			t.Dispose ();
-		}
-		_topLevels.Clear ();
-		Current = null;
-		Top?.Dispose ();
-		Top = null;
-
-		// MainLoop stuff
-		MainLoop?.Dispose ();
-		MainLoop = null;
-		_mainThreadId = -1;
-		Iteration = null;
-		EndAfterFirstIteration = false;
-		
-		// Driver stuff
-		if (Driver != null) {
-			Driver.SizeChanged -= Driver_SizeChanged;
-			Driver.KeyDown -= Driver_KeyDown;
-			Driver.KeyUp -= Driver_KeyUp;
-			Driver.MouseEvent -= Driver_MouseEvent;
-			Driver?.End ();
-			Driver = null;
-		}
-		// Don't reset ForceDriver; it needs to be set before Init is called.
-		//ForceDriver = string.Empty;
-		Force16Colors = false;
-		_forceFakeConsole = false;
-		
-		// Run State stuff
-		NotifyNewRunState = null;
-		NotifyStopRunState = null;
-		MouseGrabView = null;
-		_initialized = false;
-
-		// Mouse
-		_mouseEnteredView = null;
-		WantContinuousButtonPressedView = null;
-		MouseEvent = null;
-		GrabbedMouse = null;
-		UnGrabbingMouse = null;
-		GrabbedMouse = null;
-		UnGrabbedMouse = null;
-
-		// Keyboard
-		AlternateBackwardKey = Key.Empty;
-		AlternateForwardKey = Key.Empty;
-		QuitKey = Key.Empty;
-		KeyDown = null;
-		KeyUp = null;
-		SizeChanging = null;
-
-		Colors.Reset ();
-
-		// Reset synchronization context to allow the user to run async/await,
-		// as the main loop has been ended, the synchronization context from 
-		// gui.cs does no longer process any callbacks. See #1084 for more details:
-		// (https://github.com/gui-cs/Terminal.Gui/issues/1084).
-		SynchronizationContext.SetSynchronizationContext (syncContext: null);
-	}
-
-	/// <summary>
-	/// Gets the <see cref="ConsoleDriver"/> that has been selected. See also <see cref="ForceDriver"/>.
-	/// </summary>
-	public static ConsoleDriver Driver { get; internal set; }
-
-	/// <summary>
-	/// Forces the use of the specified driver (one of "fake", "ansi", "curses", "net", or "windows"). If
-	/// not specified, the driver is selected based on the platform.
-	/// </summary>
-	/// <remarks>
-	/// Note, <see cref="Application.Init(ConsoleDriver, string)"/> will override this configuration setting if
-	/// called with either `driver` or `driverName` specified.
-	/// </remarks>
-	[SerializableConfigurationProperty (Scope = typeof (SettingsScope))]
-	public static string ForceDriver { get; set; } = string.Empty;
-
-	/// <summary>
-	/// Gets or sets whether <see cref="Application.Driver"/> will be forced to output only the 16 colors defined in <see cref="ColorName"/>.
-	/// The default is <see langword="false"/>, meaning 24-bit (TrueColor) colors will be output as long as the selected <see cref="ConsoleDriver"/>
-	/// supports TrueColor.
-	/// </summary>
-	[SerializableConfigurationProperty (Scope = typeof (SettingsScope))]
-	public static bool Force16Colors { get; set; } = false;
-
-	// For Unit testing - ignores UseSystemConsole
-	internal static bool _forceFakeConsole;
-
-	static List<CultureInfo> _cachedSupportedCultures;
-
-	/// <summary>
-	/// Gets all cultures supported by the application without the invariant language.
-	/// </summary>
-	public static List<CultureInfo> SupportedCultures => _cachedSupportedCultures;
-
-	internal static List<CultureInfo> GetSupportedCultures ()
-	{
-		var culture = CultureInfo.GetCultures (CultureTypes.AllCultures);
-
-		// Get the assembly
-		var assembly = Assembly.GetExecutingAssembly ();
-
-		//Find the location of the assembly
-		string assemblyLocation = AppDomain.CurrentDomain.BaseDirectory;
-
-		// Find the resource file name of the assembly
-		string resourceFilename = $"{Path.GetFileNameWithoutExtension (assembly.Location)}.resources.dll";
-
-		// Return all culture for which satellite folder found with culture code.
-		return culture.Where (cultureInfo =>
-			Directory.Exists (Path.Combine (assemblyLocation, cultureInfo.Name)) &&
-			File.Exists (Path.Combine (assemblyLocation, cultureInfo.Name, resourceFilename))
-		).ToList ();
-	}
-
-	#region Initialization (Init/Shutdown)
-	/// <summary>
-	/// Initializes a new instance of <see cref="Terminal.Gui"/> Application. 
-	/// </summary>
-	/// <para>
-	/// Call this method once per instance (or after <see cref="Shutdown"/> has been called).
-	/// </para>
-	/// <para>
-	/// This function loads the right <see cref="ConsoleDriver"/> for the platform, 
-	/// Creates a <see cref="Toplevel"/>. and assigns it to <see cref="Top"/>
-	/// </para>
-	/// <para>
-	/// <see cref="Shutdown"/> must be called when the application is closing (typically after <see cref="Run(Func{Exception, bool})"/> has 
-	/// returned) to ensure resources are cleaned up and terminal settings restored.
-	/// </para>
-	/// <para>
-	/// The <see cref="Run{T}(Func{Exception, bool}, ConsoleDriver)"/> function 
-	/// combines <see cref="Init(ConsoleDriver, string)"/> and <see cref="Run(Toplevel, Func{Exception, bool})"/>
-	/// into a single call. An application cam use <see cref="Run{T}(Func{Exception, bool}, ConsoleDriver)"/> 
-	/// without explicitly calling <see cref="Init(ConsoleDriver, string)"/>.
-	/// </para>
-	/// <param name="driver">The <see cref="ConsoleDriver"/> to use. If neither <paramref name="driver"/> or <paramref name="driverName"/> are specified the default driver for the platform will be used.</param>
-	/// <param name="driverName">The short name (e.g. "net", "windows", "ansi", "fake", or "curses") of the <see cref="ConsoleDriver"/> to use. If neither <paramref name="driver"/> or <paramref name="driverName"/> are specified the default driver for the platform will be used.</param>
-	public static void Init (ConsoleDriver driver = null, string driverName = null) => InternalInit (() => new Toplevel (), driver, driverName);
-
-	internal static bool _initialized = false;
-	internal static int _mainThreadId = -1;
-
-	// INTERNAL function for initializing an app with a Toplevel factory object, driver, and mainloop.
-	//
-	// Called from:
-	// 
-	// Init() - When the user wants to use the default Toplevel. calledViaRunT will be false, causing all state to be reset.
-	// Run<T>() - When the user wants to use a custom Toplevel. calledViaRunT will be true, enabling Run<T>() to be called without calling Init first.
-	// Unit Tests - To initialize the app with a custom Toplevel, using the FakeDriver. calledViaRunT will be false, causing all state to be reset.
-	// 
-	// calledViaRunT: If false (default) all state will be reset. If true the state will not be reset.
-	internal static void InternalInit (Func<Toplevel> topLevelFactory, ConsoleDriver driver = null, string driverName = null, bool calledViaRunT = false)
-	{
-		if (_initialized && driver == null) {
-			return;
-		}
-
-		if (_initialized) {
-			throw new InvalidOperationException ("Init has already been called and must be bracketed by Shutdown.");
-		}
-
-		if (!calledViaRunT) {
-			// Reset all class variables (Application is a singleton).
-			ResetState ();
-		}
-
-		// For UnitTests
-		if (driver != null) {
-			Driver = driver;
-		}
-
-		// Start the process of configuration management.
-		// Note that we end up calling LoadConfigurationFromAllSources
-		// multiple times. We need to do this because some settings are only
-		// valid after a Driver is loaded. In this cases we need just 
-		// `Settings` so we can determine which driver to use.
-		Load (true);
-		Apply ();
-
-		// Ignore Configuration for ForceDriver if driverName is specified
-		if (!string.IsNullOrEmpty (driverName)) {
-			ForceDriver = driverName;
-		}
-
-		if (Driver == null) {
-			var p = Environment.OSVersion.Platform;
-			if (string.IsNullOrEmpty (ForceDriver)) {
-				if (p == PlatformID.Win32NT || p == PlatformID.Win32S || p == PlatformID.Win32Windows) {
-					Driver = new WindowsDriver ();
-				} else {
-					Driver = new CursesDriver ();
-				}
-			} else {
-				var drivers = GetDriverTypes ();
-				var driverType = drivers.FirstOrDefault (t => t.Name.ToLower () == ForceDriver.ToLower ());
-				if (driverType != null) {
-					Driver = (ConsoleDriver)Activator.CreateInstance (driverType);
-				} else {
-					throw new ArgumentException ($"Invalid driver name: {ForceDriver}. Valid names are {string.Join (", ", drivers.Select (t => t.Name))}");
-				}
-			}
-		}
-
-		try {
-			MainLoop = Driver.Init ();
-		} catch (InvalidOperationException ex) {
-			// This is a case where the driver is unable to initialize the console.
-			// This can happen if the console is already in use by another process or
-			// if running in unit tests.
-			// In this case, we want to throw a more specific exception.
-			throw new InvalidOperationException ("Unable to initialize the console. This can happen if the console is already in use by another process or in unit tests.", ex);
-		}
-
-		Driver.SizeChanged += (s, args) => OnSizeChanging (args);
-		Driver.KeyDown += (s, args) => OnKeyDown (args);
-		Driver.KeyUp += (s, args) => OnKeyUp (args);
-		Driver.MouseEvent += (s, args) => OnMouseEvent (args);
-
-		SynchronizationContext.SetSynchronizationContext (new MainLoopSyncContext ());
-
-		Top = topLevelFactory ();
-		Current = Top;
-
-		// Ensure Top's layout is up to date.
-		Current.SetRelativeLayout (Driver.Bounds);
-
-		_cachedSupportedCultures = GetSupportedCultures ();
-		_mainThreadId = Thread.CurrentThread.ManagedThreadId;
-		_initialized = true;
-	}
-
-	static void Driver_SizeChanged (object sender, SizeChangedEventArgs e) => OnSizeChanging (e);
-
-	static void Driver_KeyDown (object sender, Key e) => OnKeyDown (e);
-
-	static void Driver_KeyUp (object sender, Key e) => OnKeyUp (e);
-
-	static void Driver_MouseEvent (object sender, MouseEventEventArgs e) => OnMouseEvent (e);
-
-	/// <summary>
-	/// Gets of list of <see cref="ConsoleDriver"/> types that are available.
-	/// </summary>
-	/// <returns></returns>
-	public static List<Type> GetDriverTypes ()
-	{
-		// use reflection to get the list of drivers
-		var driverTypes = new List<Type> ();
-		foreach (var asm in AppDomain.CurrentDomain.GetAssemblies ()) {
-			foreach (var type in asm.GetTypes ()) {
-				if (type.IsSubclassOf (typeof (ConsoleDriver)) && !type.IsAbstract) {
-					driverTypes.Add (type);
-				}
-			}
-		}
-		return driverTypes;
-	}
-
-	/// <summary>
-	/// Shutdown an application initialized with <see cref="Init"/>.
-	/// </summary>
-	/// <remarks>
-	/// Shutdown must be called for every call to <see cref="Init"/> or <see cref="Application.Run(Toplevel, Func{Exception, bool})"/>
-	/// to ensure all resources are cleaned up (Disposed) and terminal settings are restored.
-	/// </remarks>
-	public static void Shutdown ()
-	{
-		ResetState ();
-		PrintJsonErrors ();
-	}
-	#endregion Initialization (Init/Shutdown)
-
-	#region Run (Begin, Run, End, Stop)
-	/// <summary>
-	/// Notify that a new <see cref="RunState"/> was created (<see cref="Begin(Toplevel)"/> was called). The token is created in 
-	/// <see cref="Begin(Toplevel)"/> and this event will be fired before that function exits.
-	/// </summary>
-	/// <remarks>
-	///	If <see cref="EndAfterFirstIteration"/> is <see langword="true"/> callers to
-	///	<see cref="Begin(Toplevel)"/> must also subscribe to <see cref="NotifyStopRunState"/>
-	///	and manually dispose of the <see cref="RunState"/> token when the application is done.
-	/// </remarks>
-	public static event EventHandler<RunStateEventArgs> NotifyNewRunState;
-
-	/// <summary>
-	/// Notify that a existent <see cref="RunState"/> is stopping (<see cref="End(RunState)"/> was called).
-	/// </summary>
-	/// <remarks>
-	///	If <see cref="EndAfterFirstIteration"/> is <see langword="true"/> callers to
-	///	<see cref="Begin(Toplevel)"/> must also subscribe to <see cref="NotifyStopRunState"/>
-	///	and manually dispose of the <see cref="RunState"/> token when the application is done.
-	/// </remarks>
-	public static event EventHandler<ToplevelEventArgs> NotifyStopRunState;
-
-	/// <summary>
-	/// Building block API: Prepares the provided <see cref="Toplevel"/> for execution.
-	/// </summary>
-	/// <returns>The <see cref="RunState"/> handle that needs to be passed to the <see cref="End(RunState)"/> method upon completion.</returns>
-	/// <param name="Toplevel">The <see cref="Toplevel"/> to prepare execution for.</param>
-	/// <remarks>
-	/// This method prepares the provided <see cref="Toplevel"/> for running with the focus,
-	/// it adds this to the list of <see cref="Toplevel"/>s, lays out the Subviews, focuses the first element, and draws the
-	/// <see cref="Toplevel"/> in the screen. This is usually followed by executing
-	/// the <see cref="RunLoop"/> method, and then the <see cref="End(RunState)"/> method upon termination which will
-	///  undo these changes.
-	/// </remarks>
-	public static RunState Begin (Toplevel Toplevel)
-	{
-		if (Toplevel == null) {
-			throw new ArgumentNullException (nameof (Toplevel));
-		} else if (Toplevel.IsOverlappedContainer && OverlappedTop != Toplevel && OverlappedTop != null) {
-			throw new InvalidOperationException ("Only one Overlapped Container is allowed.");
-		}
-
-		// Ensure the mouse is ungrabed.
-		MouseGrabView = null;
-
-		var rs = new RunState (Toplevel);
-
-		// View implements ISupportInitializeNotification which is derived from ISupportInitialize
-		if (!Toplevel.IsInitialized) {
-			Toplevel.BeginInit ();
-			Toplevel.EndInit ();
-		}
-
-		lock (_topLevels) {
-			// If Top was already initialized with Init, and Begin has never been called
-			// Top was not added to the Toplevels Stack. It will thus never get disposed.
-			// Clean it up here:
-			if (Top != null && Toplevel != Top && !_topLevels.Contains (Top)) {
-				Top.Dispose ();
-				Top = null;
-			} else if (Top != null && Toplevel != Top && _topLevels.Contains (Top)) {
-				Top.OnLeave (Toplevel);
-			}
-			// BUGBUG: We should not depend on `Id` internally. 
-			// BUGBUG: It is super unclear what this code does anyway.
-			if (string.IsNullOrEmpty (Toplevel.Id)) {
-				int count = 1;
-				string id = (_topLevels.Count + count).ToString ();
-				while (_topLevels.Count > 0 && _topLevels.FirstOrDefault (x => x.Id == id) != null) {
-					count++;
-					id = (_topLevels.Count + count).ToString ();
-				}
-				Toplevel.Id = (_topLevels.Count + count).ToString ();
-
-				_topLevels.Push (Toplevel);
-			} else {
-				var dup = _topLevels.FirstOrDefault (x => x.Id == Toplevel.Id);
-				if (dup == null) {
-					_topLevels.Push (Toplevel);
-				}
-			}
-
-			if (_topLevels.FindDuplicates (new ToplevelEqualityComparer ()).Count > 0) {
-				throw new ArgumentException ("There are duplicates Toplevels Id's");
-			}
-		}
-		if (Top == null || Toplevel.IsOverlappedContainer) {
-			Top = Toplevel;
-		}
-
-		bool refreshDriver = true;
-		if (OverlappedTop == null || Toplevel.IsOverlappedContainer || Current?.Modal == false && Toplevel.Modal
-		|| Current?.Modal == false && !Toplevel.Modal || Current?.Modal == true && Toplevel.Modal) {
-
-			if (Toplevel.Visible) {
-				Current = Toplevel;
-				SetCurrentOverlappedAsTop ();
-			} else {
-				refreshDriver = false;
-			}
-		} else if (OverlappedTop != null && Toplevel != OverlappedTop && Current?.Modal == true && !_topLevels.Peek ().Modal
-			|| OverlappedTop != null && Toplevel != OverlappedTop && Current?.Running == false) {
-			refreshDriver = false;
-			MoveCurrent (Toplevel);
-		} else {
-			refreshDriver = false;
-			MoveCurrent (Current);
-		}
-
-		//if (Toplevel.LayoutStyle == LayoutStyle.Computed) {
-		Toplevel.SetRelativeLayout (Driver.Bounds);
-		//}
-		Toplevel.LayoutSubviews ();
-		Toplevel.PositionToplevels ();
-		Toplevel.FocusFirst ();
-		if (refreshDriver) {
-			OverlappedTop?.OnChildLoaded (Toplevel);
-			Toplevel.OnLoaded ();
-			Toplevel.SetNeedsDisplay ();
-			Toplevel.Draw ();
-			Toplevel.PositionCursor ();
-			Driver.Refresh ();
-		}
-
-		NotifyNewRunState?.Invoke (Toplevel, new RunStateEventArgs (rs));
-		return rs;
-	}
-
-	/// <summary>
-	/// Runs the application by calling <see cref="Run(Toplevel, Func{Exception, bool})"/> with the value of <see cref="Top"/>.
-	/// </summary>
-	/// <remarks>
-	/// See <see cref="Run(Toplevel, Func{Exception, bool})"/> for more details.
-	/// </remarks>
-	public static void Run (Func<Exception, bool> errorHandler = null) => Run (Top, errorHandler);
-
-	/// <summary>
-	/// Runs the application by calling <see cref="Run(Toplevel, Func{Exception, bool})"/> 
-	/// with a new instance of the specified <see cref="Toplevel"/>-derived class.
-	/// <para>
-	/// Calling <see cref="Init"/> first is not needed as this function will initialize the application.
-	/// </para>
-	/// <para>
-	/// <see cref="Shutdown"/> must be called when the application is closing (typically after Run> has 
-	/// returned) to ensure resources are cleaned up and terminal settings restored.
-	/// </para>
-	/// </summary>
-	/// <remarks>
-	/// See <see cref="Run(Toplevel, Func{Exception, bool})"/> for more details.
-	/// </remarks>
-	/// <param name="errorHandler"></param>
-	/// <param name="driver">The <see cref="ConsoleDriver"/> to use. If not specified the default driver for the
-	/// platform will be used (<see cref="WindowsDriver"/>, <see cref="CursesDriver"/>, or <see cref="NetDriver"/>).
-	/// Must be <see langword="null"/> if <see cref="Init"/> has already been called. 
-	/// </param>
-	public static void Run<T> (Func<Exception, bool> errorHandler = null, ConsoleDriver driver = null) where T : Toplevel, new()
-	{
-		if (_initialized) {
-			if (Driver != null) {
-				// Init() has been called and we have a driver, so just run the app.
-				var top = new T ();
-				var type = top.GetType ().BaseType;
-				while (type != typeof (Toplevel) && type != typeof (object)) {
-					type = type.BaseType;
-				}
-				if (type != typeof (Toplevel)) {
-					throw new ArgumentException ($"{top.GetType ().Name} must be derived from TopLevel");
-				}
-				Run (top, errorHandler);
-			} else {
-				// This code path should be impossible because Init(null, null) will select the platform default driver
-				throw new InvalidOperationException ("Init() completed without a driver being set (this should be impossible); Run<T>() cannot be called.");
-			}
-		} else {
-			// Init() has NOT been called.
-			InternalInit (() => new T (), driver, null, true);
-			Run (Top, errorHandler);
-		}
-	}
-
-	/// <summary>
-	///  Runs the main loop on the given <see cref="Toplevel"/> container.
-	/// </summary>
-	/// <remarks>
-	///  <para>
-	///   This method is used to start processing events
-	///   for the main application, but it is also used to
-	///   run other modal <see cref="View"/>s such as <see cref="Dialog"/> boxes.
-	///  </para>
-	///  <para>
-	///   To make a <see cref="Run(Toplevel, Func{Exception, bool})"/> stop execution, call <see cref="Application.RequestStop"/>.
-	///  </para>
-	///  <para>
-	///   Calling <see cref="Run(Toplevel, Func{Exception, bool})"/> is equivalent to calling <see cref="Begin(Toplevel)"/>,
-	///   followed by <see cref="RunLoop(RunState)"/>, and then calling <see cref="End(RunState)"/>.
-	///  </para>
-	///  <para>
-	///   Alternatively, to have a program control the main loop and 
-	///   process events manually, call <see cref="Begin(Toplevel)"/> to set things up manually and then
-	///   repeatedly call <see cref="RunLoop(RunState)"/> with the wait parameter set to false. By doing this
-	///   the <see cref="RunLoop(RunState)"/> method will only process any pending events, timers, idle handlers and
-	///   then return control immediately.
-	///  </para>
-	///  <para>
-	///   RELEASE builds only: When <paramref name="errorHandler"/> is <see langword="null"/> any exceptions will be rethrown. 
-	///   Otherwise, if <paramref name="errorHandler"/> will be called. If <paramref name="errorHandler"/> 
-	///   returns <see langword="true"/> the <see cref="RunLoop(RunState)"/> will resume; otherwise 
-	///   this method will exit.
-	///  </para>
-	/// </remarks>
-	/// <param name="view">The <see cref="Toplevel"/> to run as a modal.</param>
-	/// <param name="errorHandler">RELEASE builds only: Handler for any unhandled exceptions (resumes when returns true, rethrows when null).</param>
-	public static void Run (Toplevel view, Func<Exception, bool> errorHandler = null)
-	{
-		bool resume = true;
-		while (resume) {
+/// <remarks>TODO: Flush this out.</remarks>
+public static partial class Application
+{
+    // For Unit testing - ignores UseSystemConsole
+    internal static bool _forceFakeConsole;
+
+    /// <summary>Gets the <see cref="ConsoleDriver"/> that has been selected. See also <see cref="ForceDriver"/>.</summary>
+    public static ConsoleDriver Driver { get; internal set; }
+
+    /// <summary>
+    ///     Gets or sets whether <see cref="Application.Driver"/> will be forced to output only the 16 colors defined in
+    ///     <see cref="ColorName"/>. The default is <see langword="false"/>, meaning 24-bit (TrueColor) colors will be output
+    ///     as long as the selected <see cref="ConsoleDriver"/> supports TrueColor.
+    /// </summary>
+    [SerializableConfigurationProperty (Scope = typeof (SettingsScope))]
+    public static bool Force16Colors { get; set; }
+
+    /// <summary>
+    ///     Forces the use of the specified driver (one of "fake", "ansi", "curses", "net", or "windows"). If not
+    ///     specified, the driver is selected based on the platform.
+    /// </summary>
+    /// <remarks>
+    ///     Note, <see cref="Application.Init(ConsoleDriver, string)"/> will override this configuration setting if called
+    ///     with either `driver` or `driverName` specified.
+    /// </remarks>
+    [SerializableConfigurationProperty (Scope = typeof (SettingsScope))]
+    public static string ForceDriver { get; set; } = string.Empty;
+
+    /// <summary>Gets all cultures supported by the application without the invariant language.</summary>
+    public static List<CultureInfo> SupportedCultures { get; private set; }
+
+    internal static List<CultureInfo> GetSupportedCultures ()
+    {
+        CultureInfo [] culture = CultureInfo.GetCultures (CultureTypes.AllCultures);
+
+        // Get the assembly
+        var assembly = Assembly.GetExecutingAssembly ();
+
+        //Find the location of the assembly
+        string assemblyLocation = AppDomain.CurrentDomain.BaseDirectory;
+
+        // Find the resource file name of the assembly
+        var resourceFilename = $"{Path.GetFileNameWithoutExtension (assembly.Location)}.resources.dll";
+
+        // Return all culture for which satellite folder found with culture code.
+        return culture.Where (
+                              cultureInfo =>
+                                  Directory.Exists (Path.Combine (assemblyLocation, cultureInfo.Name))
+                                  && File.Exists (Path.Combine (assemblyLocation, cultureInfo.Name, resourceFilename))
+                             )
+                      .ToList ();
+    }
+
+    // IMPORTANT: Ensure all property/fields are reset here. See Init_ResetState_Resets_Properties unit test.
+    // Encapsulate all setting of initial state for Application; Having
+    // this in a function like this ensures we don't make mistakes in
+    // guaranteeing that the state of this singleton is deterministic when Init
+    // starts running and after Shutdown returns.
+    internal static void ResetState ()
+    {
+        // Shutdown is the bookend for Init. As such it needs to clean up all resources
+        // Init created. Apps that do any threading will need to code defensively for this.
+        // e.g. see Issue #537
+        foreach (Toplevel t in _topLevels)
+        {
+            t.Running = false;
+            t.Dispose ();
+        }
+
+        _topLevels.Clear ();
+        Current = null;
+        Top?.Dispose ();
+        Top = null;
+
+        // MainLoop stuff
+        MainLoop?.Dispose ();
+        MainLoop = null;
+        _mainThreadId = -1;
+        Iteration = null;
+        EndAfterFirstIteration = false;
+
+        // Driver stuff
+        if (Driver is { })
+        {
+            Driver.SizeChanged -= Driver_SizeChanged;
+            Driver.KeyDown -= Driver_KeyDown;
+            Driver.KeyUp -= Driver_KeyUp;
+            Driver.MouseEvent -= Driver_MouseEvent;
+            Driver?.End ();
+            Driver = null;
+        }
+
+        // Don't reset ForceDriver; it needs to be set before Init is called.
+        //ForceDriver = string.Empty;
+        Force16Colors = false;
+        _forceFakeConsole = false;
+
+        // Run State stuff
+        NotifyNewRunState = null;
+        NotifyStopRunState = null;
+        MouseGrabView = null;
+        _initialized = false;
+
+        // Mouse
+        _mouseEnteredView = null;
+        WantContinuousButtonPressedView = null;
+        MouseEvent = null;
+        GrabbedMouse = null;
+        UnGrabbingMouse = null;
+        GrabbedMouse = null;
+        UnGrabbedMouse = null;
+
+        // Keyboard
+        AlternateBackwardKey = Key.Empty;
+        AlternateForwardKey = Key.Empty;
+        QuitKey = Key.Empty;
+        KeyDown = null;
+        KeyUp = null;
+        SizeChanging = null;
+
+        Colors.Reset ();
+
+        // Reset synchronization context to allow the user to run async/await,
+        // as the main loop has been ended, the synchronization context from 
+        // gui.cs does no longer process any callbacks. See #1084 for more details:
+        // (https://github.com/gui-cs/Terminal.Gui/issues/1084).
+        SynchronizationContext.SetSynchronizationContext (null);
+    }
+
+    #region Initialization (Init/Shutdown)
+
+    /// <summary>Initializes a new instance of <see cref="Terminal.Gui"/> Application.</summary>
+    /// <para>Call this method once per instance (or after <see cref="Shutdown"/> has been called).</para>
+    /// <para>
+    ///     This function loads the right <see cref="ConsoleDriver"/> for the platform, Creates a <see cref="Toplevel"/>. and
+    ///     assigns it to <see cref="Top"/>
+    /// </para>
+    /// <para>
+    ///     <see cref="Shutdown"/> must be called when the application is closing (typically after
+    ///     <see cref="Run(Func{Exception, bool})"/> has returned) to ensure resources are cleaned up and terminal settings
+    ///     restored.
+    /// </para>
+    /// <para>
+    ///     The <see cref="Run{T}(Func{Exception, bool}, ConsoleDriver)"/> function combines
+    ///     <see cref="Init(ConsoleDriver, string)"/> and <see cref="Run(Toplevel, Func{Exception, bool})"/> into a single
+    ///     call. An application cam use <see cref="Run{T}(Func{Exception, bool}, ConsoleDriver)"/> without explicitly calling
+    ///     <see cref="Init(ConsoleDriver, string)"/>.
+    /// </para>
+    /// <param name="driver">
+    ///     The <see cref="ConsoleDriver"/> to use. If neither <paramref name="driver"/> or
+    ///     <paramref name="driverName"/> are specified the default driver for the platform will be used.
+    /// </param>
+    /// <param name="driverName">
+    ///     The short name (e.g. "net", "windows", "ansi", "fake", or "curses") of the
+    ///     <see cref="ConsoleDriver"/> to use. If neither <paramref name="driver"/> or <paramref name="driverName"/> are
+    ///     specified the default driver for the platform will be used.
+    /// </param>
+    public static void Init (ConsoleDriver driver = null, string driverName = null) { InternalInit (() => new Toplevel (), driver, driverName); }
+
+    internal static bool _initialized;
+    internal static int _mainThreadId = -1;
+
+    // INTERNAL function for initializing an app with a Toplevel factory object, driver, and mainloop.
+    //
+    // Called from:
+    // 
+    // Init() - When the user wants to use the default Toplevel. calledViaRunT will be false, causing all state to be reset.
+    // Run<T>() - When the user wants to use a custom Toplevel. calledViaRunT will be true, enabling Run<T>() to be called without calling Init first.
+    // Unit Tests - To initialize the app with a custom Toplevel, using the FakeDriver. calledViaRunT will be false, causing all state to be reset.
+    // 
+    // calledViaRunT: If false (default) all state will be reset. If true the state will not be reset.
+    internal static void InternalInit (
+        Func<Toplevel> topLevelFactory,
+        ConsoleDriver driver = null,
+        string driverName = null,
+        bool calledViaRunT = false
+    )
+    {
+        if (_initialized && driver is null)
+        {
+            return;
+        }
+
+        if (_initialized)
+        {
+            throw new InvalidOperationException ("Init has already been called and must be bracketed by Shutdown.");
+        }
+
+        if (!calledViaRunT)
+        {
+            // Reset all class variables (Application is a singleton).
+            ResetState ();
+        }
+
+        // For UnitTests
+        if (driver is { })
+        {
+            Driver = driver;
+        }
+
+        // Start the process of configuration management.
+        // Note that we end up calling LoadConfigurationFromAllSources
+        // multiple times. We need to do this because some settings are only
+        // valid after a Driver is loaded. In this cases we need just 
+        // `Settings` so we can determine which driver to use.
+        Load (true);
+        Apply ();
+
+        // Ignore Configuration for ForceDriver if driverName is specified
+        if (!string.IsNullOrEmpty (driverName))
+        {
+            ForceDriver = driverName;
+        }
+
+        if (Driver is null)
+        {
+            PlatformID p = Environment.OSVersion.Platform;
+
+            if (string.IsNullOrEmpty (ForceDriver))
+            {
+                if (p == PlatformID.Win32NT || p == PlatformID.Win32S || p == PlatformID.Win32Windows)
+                {
+                    Driver = new WindowsDriver ();
+                }
+                else
+                {
+                    Driver = new CursesDriver ();
+                }
+            }
+            else
+            {
+                List<Type> drivers = GetDriverTypes ();
+                Type driverType = drivers.FirstOrDefault (t => t.Name.Equals (ForceDriver, StringComparison.InvariantCultureIgnoreCase));
+
+                if (driverType is { })
+                {
+                    Driver = (ConsoleDriver)Activator.CreateInstance (driverType);
+                }
+                else
+                {
+                    throw new ArgumentException (
+                                                 $"Invalid driver name: {ForceDriver}. Valid names are {string.Join (", ", drivers.Select (t => t.Name))}"
+                                                );
+                }
+            }
+        }
+
+        try
+        {
+            MainLoop = Driver.Init ();
+        }
+        catch (InvalidOperationException ex)
+        {
+            // This is a case where the driver is unable to initialize the console.
+            // This can happen if the console is already in use by another process or
+            // if running in unit tests.
+            // In this case, we want to throw a more specific exception.
+            throw new InvalidOperationException (
+                                                 "Unable to initialize the console. This can happen if the console is already in use by another process or in unit tests.",
+                                                 ex
+                                                );
+        }
+
+        Driver.SizeChanged += (s, args) => OnSizeChanging (args);
+        Driver.KeyDown += (s, args) => OnKeyDown (args);
+        Driver.KeyUp += (s, args) => OnKeyUp (args);
+        Driver.MouseEvent += (s, args) => OnMouseEvent (args);
+
+        SynchronizationContext.SetSynchronizationContext (new MainLoopSyncContext ());
+
+        Top = topLevelFactory ();
+        Current = Top;
+
+        // Ensure Top's layout is up to date.
+        Current.SetRelativeLayout (Driver.Bounds);
+
+        SupportedCultures = GetSupportedCultures ();
+        _mainThreadId = Thread.CurrentThread.ManagedThreadId;
+        _initialized = true;
+    }
+
+    private static void Driver_SizeChanged (object sender, SizeChangedEventArgs e) { OnSizeChanging (e); }
+    private static void Driver_KeyDown (object sender, Key e) { OnKeyDown (e); }
+    private static void Driver_KeyUp (object sender, Key e) { OnKeyUp (e); }
+    private static void Driver_MouseEvent (object sender, MouseEventEventArgs e) { OnMouseEvent (e); }
+
+    /// <summary>Gets of list of <see cref="ConsoleDriver"/> types that are available.</summary>
+    /// <returns></returns>
+    public static List<Type> GetDriverTypes ()
+    {
+        // use reflection to get the list of drivers
+        List<Type> driverTypes = new ();
+
+        foreach (Assembly asm in AppDomain.CurrentDomain.GetAssemblies ())
+        {
+            foreach (Type type in asm.GetTypes ())
+            {
+                if (type.IsSubclassOf (typeof (ConsoleDriver)) && !type.IsAbstract)
+                {
+                    driverTypes.Add (type);
+                }
+            }
+        }
+
+        return driverTypes;
+    }
+
+    /// <summary>Shutdown an application initialized with <see cref="Init"/>.</summary>
+    /// <remarks>
+    ///     Shutdown must be called for every call to <see cref="Init"/> or
+    ///     <see cref="Application.Run(Toplevel, Func{Exception, bool})"/> to ensure all resources are cleaned up (Disposed)
+    ///     and terminal settings are restored.
+    /// </remarks>
+    public static void Shutdown ()
+    {
+        ResetState ();
+        PrintJsonErrors ();
+    }
+
+    #endregion Initialization (Init/Shutdown)
+
+    #region Run (Begin, Run, End, Stop)
+
+    /// <summary>
+    ///     Notify that a new <see cref="RunState"/> was created (<see cref="Begin(Toplevel)"/> was called). The token is
+    ///     created in <see cref="Begin(Toplevel)"/> and this event will be fired before that function exits.
+    /// </summary>
+    /// <remarks>
+    ///     If <see cref="EndAfterFirstIteration"/> is <see langword="true"/> callers to <see cref="Begin(Toplevel)"/>
+    ///     must also subscribe to <see cref="NotifyStopRunState"/> and manually dispose of the <see cref="RunState"/> token
+    ///     when the application is done.
+    /// </remarks>
+    public static event EventHandler<RunStateEventArgs> NotifyNewRunState;
+
+    /// <summary>Notify that a existent <see cref="RunState"/> is stopping (<see cref="End(RunState)"/> was called).</summary>
+    /// <remarks>
+    ///     If <see cref="EndAfterFirstIteration"/> is <see langword="true"/> callers to <see cref="Begin(Toplevel)"/>
+    ///     must also subscribe to <see cref="NotifyStopRunState"/> and manually dispose of the <see cref="RunState"/> token
+    ///     when the application is done.
+    /// </remarks>
+    public static event EventHandler<ToplevelEventArgs> NotifyStopRunState;
+
+    /// <summary>Building block API: Prepares the provided <see cref="Toplevel"/> for execution.</summary>
+    /// <returns>
+    ///     The <see cref="RunState"/> handle that needs to be passed to the <see cref="End(RunState)"/> method upon
+    ///     completion.
+    /// </returns>
+    /// <param name="Toplevel">The <see cref="Toplevel"/> to prepare execution for.</param>
+    /// <remarks>
+    ///     This method prepares the provided <see cref="Toplevel"/> for running with the focus, it adds this to the list
+    ///     of <see cref="Toplevel"/>s, lays out the Subviews, focuses the first element, and draws the <see cref="Toplevel"/>
+    ///     in the screen. This is usually followed by executing the <see cref="RunLoop"/> method, and then the
+    ///     <see cref="End(RunState)"/> method upon termination which will undo these changes.
+    /// </remarks>
+    public static RunState Begin (Toplevel Toplevel)
+    {
+        if (Toplevel is null)
+        {
+            throw new ArgumentNullException (nameof (Toplevel));
+        }
+
+        if (Toplevel.IsOverlappedContainer && OverlappedTop != Toplevel && OverlappedTop is { })
+        {
+            throw new InvalidOperationException ("Only one Overlapped Container is allowed.");
+        }
+
+        // Ensure the mouse is ungrabed.
+        MouseGrabView = null;
+
+        var rs = new RunState (Toplevel);
+
+        // View implements ISupportInitializeNotification which is derived from ISupportInitialize
+        if (!Toplevel.IsInitialized)
+        {
+            Toplevel.BeginInit ();
+            Toplevel.EndInit ();
+        }
+
+        lock (_topLevels)
+        {
+            // If Top was already initialized with Init, and Begin has never been called
+            // Top was not added to the Toplevels Stack. It will thus never get disposed.
+            // Clean it up here:
+            if (Top is { } && Toplevel != Top && !_topLevels.Contains (Top))
+            {
+                Top.Dispose ();
+                Top = null;
+            }
+            else if (Top is { } && Toplevel != Top && _topLevels.Contains (Top))
+            {
+                Top.OnLeave (Toplevel);
+            }
+
+            // BUGBUG: We should not depend on `Id` internally. 
+            // BUGBUG: It is super unclear what this code does anyway.
+            if (string.IsNullOrEmpty (Toplevel.Id))
+            {
+                var count = 1;
+                var id = (_topLevels.Count + count).ToString ();
+
+                while (_topLevels.Count > 0 && _topLevels.FirstOrDefault (x => x.Id == id) is { })
+                {
+                    count++;
+                    id = (_topLevels.Count + count).ToString ();
+                }
+
+                Toplevel.Id = (_topLevels.Count + count).ToString ();
+
+                _topLevels.Push (Toplevel);
+            }
+            else
+            {
+                Toplevel dup = _topLevels.FirstOrDefault (x => x.Id == Toplevel.Id);
+
+                if (dup is null)
+                {
+                    _topLevels.Push (Toplevel);
+                }
+            }
+
+            if (_topLevels.FindDuplicates (new ToplevelEqualityComparer ()).Count > 0)
+            {
+                throw new ArgumentException ("There are duplicates Toplevels Id's");
+            }
+        }
+
+        if (Top is null || Toplevel.IsOverlappedContainer)
+        {
+            Top = Toplevel;
+        }
+
+        var refreshDriver = true;
+
+        if (OverlappedTop == null
+            || Toplevel.IsOverlappedContainer
+            || (Current?.Modal == false && Toplevel.Modal)
+            || (Current?.Modal == false && !Toplevel.Modal)
+            || (Current?.Modal == true && Toplevel.Modal))
+        {
+            if (Toplevel.Visible)
+            {
+                Current = Toplevel;
+                SetCurrentOverlappedAsTop ();
+            }
+            else
+            {
+                refreshDriver = false;
+            }
+        }
+        else if ((OverlappedTop != null
+                  && Toplevel != OverlappedTop
+                  && Current?.Modal == true
+                  && !_topLevels.Peek ().Modal)
+                 || (OverlappedTop is { } && Toplevel != OverlappedTop && Current?.Running == false))
+        {
+            refreshDriver = false;
+            MoveCurrent (Toplevel);
+        }
+        else
+        {
+            refreshDriver = false;
+            MoveCurrent (Current);
+        }
+
+        //if (Toplevel.LayoutStyle == LayoutStyle.Computed) {
+        Toplevel.SetRelativeLayout (Driver.Bounds);
+
+        //}
+        Toplevel.LayoutSubviews ();
+        Toplevel.PositionToplevels ();
+        Toplevel.FocusFirst ();
+
+        if (refreshDriver)
+        {
+            OverlappedTop?.OnChildLoaded (Toplevel);
+            Toplevel.OnLoaded ();
+            Toplevel.SetNeedsDisplay ();
+            Toplevel.Draw ();
+            Toplevel.PositionCursor ();
+            Driver.Refresh ();
+        }
+
+        NotifyNewRunState?.Invoke (Toplevel, new RunStateEventArgs (rs));
+
+        return rs;
+    }
+
+    /// <summary>
+    ///     Runs the application by calling <see cref="Run(Toplevel, Func{Exception, bool})"/> with the value of
+    ///     <see cref="Top"/>.
+    /// </summary>
+    /// <remarks>See <see cref="Run(Toplevel, Func{Exception, bool})"/> for more details.</remarks>
+    public static void Run (Func<Exception, bool> errorHandler = null) { Run (Top, errorHandler); }
+
+    /// <summary>
+    ///     Runs the application by calling <see cref="Run(Toplevel, Func{Exception, bool})"/> with a new instance of the
+    ///     specified <see cref="Toplevel"/>-derived class.
+    ///     <para>Calling <see cref="Init"/> first is not needed as this function will initialize the application.</para>
+    ///     <para>
+    ///         <see cref="Shutdown"/> must be called when the application is closing (typically after Run> has returned) to
+    ///         ensure resources are cleaned up and terminal settings restored.
+    ///     </para>
+    /// </summary>
+    /// <remarks>See <see cref="Run(Toplevel, Func{Exception, bool})"/> for more details.</remarks>
+    /// <param name="errorHandler"></param>
+    /// <param name="driver">
+    ///     The <see cref="ConsoleDriver"/> to use. If not specified the default driver for the platform will
+    ///     be used ( <see cref="WindowsDriver"/>, <see cref="CursesDriver"/>, or <see cref="NetDriver"/>). Must be
+    ///     <see langword="null"/> if <see cref="Init"/> has already been called.
+    /// </param>
+    public static void Run<T> (Func<Exception, bool> errorHandler = null, ConsoleDriver driver = null)
+        where T : Toplevel, new ()
+    {
+        if (_initialized)
+        {
+            if (Driver is { })
+            {
+                // Init() has been called and we have a driver, so just run the app.
+                var top = new T ();
+                Type type = top.GetType ().BaseType;
+
+                while (type != typeof (Toplevel) && type != typeof (object))
+                {
+                    type = type.BaseType;
+                }
+
+                if (type != typeof (Toplevel))
+                {
+                    throw new ArgumentException ($"{top.GetType ().Name} must be derived from TopLevel");
+                }
+
+                Run (top, errorHandler);
+            }
+            else
+            {
+                // This code path should be impossible because Init(null, null) will select the platform default driver
+                throw new InvalidOperationException (
+                                                     "Init() completed without a driver being set (this should be impossible); Run<T>() cannot be called."
+                                                    );
+            }
+        }
+        else
+        {
+            // Init() has NOT been called.
+            InternalInit (() => new T (), driver, null, true);
+            Run (Top, errorHandler);
+        }
+    }
+
+    /// <summary>Runs the main loop on the given <see cref="Toplevel"/> container.</summary>
+    /// <remarks>
+    ///     <para>
+    ///         This method is used to start processing events for the main application, but it is also used to run other
+    ///         modal <see cref="View"/>s such as <see cref="Dialog"/> boxes.
+    ///     </para>
+    ///     <para>
+    ///         To make a <see cref="Run(Toplevel, Func{Exception, bool})"/> stop execution, call
+    ///         <see cref="Application.RequestStop"/>.
+    ///     </para>
+    ///     <para>
+    ///         Calling <see cref="Run(Toplevel, Func{Exception, bool})"/> is equivalent to calling
+    ///         <see cref="Begin(Toplevel)"/>, followed by <see cref="RunLoop(RunState)"/>, and then calling
+    ///         <see cref="End(RunState)"/>.
+    ///     </para>
+    ///     <para>
+    ///         Alternatively, to have a program control the main loop and process events manually, call
+    ///         <see cref="Begin(Toplevel)"/> to set things up manually and then repeatedly call
+    ///         <see cref="RunLoop(RunState)"/> with the wait parameter set to false. By doing this the
+    ///         <see cref="RunLoop(RunState)"/> method will only process any pending events, timers, idle handlers and then
+    ///         return control immediately.
+    ///     </para>
+    ///     <para>
+    ///         RELEASE builds only: When <paramref name="errorHandler"/> is <see langword="null"/> any exceptions will be
+    ///         rethrown. Otherwise, if <paramref name="errorHandler"/> will be called. If <paramref name="errorHandler"/>
+    ///         returns <see langword="true"/> the <see cref="RunLoop(RunState)"/> will resume; otherwise this method will
+    ///         exit.
+    ///     </para>
+    /// </remarks>
+    /// <param name="view">The <see cref="Toplevel"/> to run as a modal.</param>
+    /// <param name="errorHandler">
+    ///     RELEASE builds only: Handler for any unhandled exceptions (resumes when returns true,
+    ///     rethrows when null).
+    /// </param>
+    public static void Run (Toplevel view, Func<Exception, bool> errorHandler = null)
+    {
+        var resume = true;
+
+        while (resume)
+        {
 #if !DEBUG
-				try {
+            try
+            {
 #endif
-			resume = false;
-			var runState = Begin (view);
-			// If EndAfterFirstIteration is true then the user must dispose of the runToken
-			// by using NotifyStopRunState event.
-			RunLoop (runState);
-			if (!EndAfterFirstIteration) {
-				End (runState);
-			}
+            resume = false;
+            RunState runState = Begin (view);
+
+            // If EndAfterFirstIteration is true then the user must dispose of the runToken
+            // by using NotifyStopRunState event.
+            RunLoop (runState);
+
+            if (!EndAfterFirstIteration)
+            {
+                End (runState);
+            }
 #if !DEBUG
-				}
-				catch (Exception error)
-				{
-					if (errorHandler == null)
-					{
-						throw;
-					}
-					resume = errorHandler(error);
-				}
+            }
+            catch (Exception error)
+            {
+                if (errorHandler is null)
+                {
+                    throw;
+                }
+
+                resume = errorHandler (error);
+            }
 #endif
-		}
-	}
-
-	/// <summary>
-	///   Adds a timeout to the application.
-	/// </summary>
-	/// <remarks>
-	///   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.
-	///
-	///   The returned value is a token that can be used to stop the timeout
-	///   by calling <see cref="RemoveTimeout(object)"/>.
-	/// </remarks>
-	public static object AddTimeout (TimeSpan time, Func<bool> callback) => MainLoop?.AddTimeout (time, callback);
-
-	/// <summary>
-	///   Removes a previously scheduled timeout
-	/// </summary>
-	/// <remarks>
-	///   The token parameter is the value returned by <see cref="AddTimeout"/>.
-	/// </remarks>
-	/// 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 static bool RemoveTimeout (object token) => MainLoop?.RemoveTimeout (token) ?? false;
-
-
-	/// <summary>
-	///   Runs <paramref name="action"/> on the thread that is processing events
-	/// </summary>
-	/// <param name="action">the action to be invoked on the main processing thread.</param>
-	public static void Invoke (Action action) => MainLoop?.AddIdle (() => {
-		action ();
-		return false;
-	});
-
-	// TODO: Determine if this is really needed. The only code that calls WakeUp I can find
-	// is ProgressBarStyles and it's not clear it needs to.
-	/// <summary>
-	/// Wakes up the running application that might be waiting on input.
-	/// </summary>
-	public static void Wakeup () => MainLoop?.Wakeup ();
-
-	/// <summary>
-	/// Triggers a refresh of the entire display.
-	/// </summary>
-	public static void Refresh ()
-	{
-		// TODO: Figure out how to remove this call to ClearContents. Refresh should just repaint damaged areas, not clear
-		Driver.ClearContents ();
-		View last = null;
-		foreach (var v in _topLevels.Reverse ()) {
-			if (v.Visible) {
-				v.SetNeedsDisplay ();
-				v.SetSubViewNeedsDisplay ();
-				v.Draw ();
-			}
-			last = v;
-		}
-		last?.PositionCursor ();
-		Driver.Refresh ();
-	}
-
-	/// <summary>
-	///  This event is raised on each iteration of the main loop.
-	/// </summary>
-	/// <remarks>
-	///  See also <see cref="Timeout"/>
-	/// </remarks>
-	public static event EventHandler<IterationEventArgs> Iteration;
-
-	/// <summary>
-	/// The <see cref="MainLoop"/> driver for the application
-	/// </summary>
-	/// <value>The main loop.</value>
-	internal static MainLoop MainLoop { get; private set; }
-
-	/// <summary>
-	/// Set to true to cause <see cref="End"/> to be called after the first iteration.
-	/// Set to false (the default) to cause the application to continue running until Application.RequestStop () is called.
-	/// </summary>
-	public static bool EndAfterFirstIteration { get; set; } = false;
-
-	//
-	// provides the sync context set while executing code in Terminal.Gui, to let
-	// users use async/await on their code
-	//
-	class MainLoopSyncContext : SynchronizationContext {
-		public override SynchronizationContext CreateCopy () => new MainLoopSyncContext ();
-
-		public override void Post (SendOrPostCallback d, object state) => MainLoop.AddIdle (() => {
-			d (state);
-			return false;
-		});
-
-		//_mainLoop.Driver.Wakeup ();
-		public override void Send (SendOrPostCallback d, object state)
-		{
-			if (Thread.CurrentThread.ManagedThreadId == _mainThreadId) {
-				d (state);
-			} else {
-				bool wasExecuted = false;
-				Invoke (() => {
-					d (state);
-					wasExecuted = true;
-				});
-				while (!wasExecuted) {
-					Thread.Sleep (15);
-				}
-			}
-		}
-	}
-
-	/// <summary>
-	///  Building block API: Runs the main loop for the created <see cref="Toplevel"/>.
-	/// </summary>
-	/// <param name="state">The state returned by the <see cref="Begin(Toplevel)"/> method.</param>
-	public static void RunLoop (RunState state)
-	{
-		if (state == null) {
-			throw new ArgumentNullException (nameof (state));
-		}
-		if (state.Toplevel == null) {
-			throw new ObjectDisposedException ("state");
-		}
-
-		bool firstIteration = true;
-		for (state.Toplevel.Running = true; state.Toplevel.Running;) {
-			MainLoop.Running = true;
-			if (EndAfterFirstIteration && !firstIteration) {
-				return;
-			}
-			RunIteration (ref state, ref firstIteration);
-		}
-		MainLoop.Running = false;
-		// Run one last iteration to consume any outstanding input events from Driver
-		// This is important for remaining OnKeyUp events.
-		RunIteration (ref state, ref firstIteration);
-	}
-
-	/// <summary>
-	/// Run one application iteration.
-	/// </summary>
-	/// <param name="state">The state returned by <see cref="Begin(Toplevel)"/>.</param>
-	/// <param name="firstIteration">Set to <see langword="true"/> if this is the first run loop iteration. Upon return,
-	/// it will be set to <see langword="false"/> if at least one iteration happened.</param>
-	public static void RunIteration (ref RunState state, ref bool firstIteration)
-	{
-		if (MainLoop.Running && MainLoop.EventsPending ()) {
-			// Notify Toplevel it's ready
-			if (firstIteration) {
-				state.Toplevel.OnReady ();
-			}
-
-			MainLoop.RunIteration ();
-			Iteration?.Invoke (null, new IterationEventArgs ());
-			EnsureModalOrVisibleAlwaysOnTop (state.Toplevel);
-			if (state.Toplevel != Current) {
-				OverlappedTop?.OnDeactivate (state.Toplevel);
-				state.Toplevel = Current;
-				OverlappedTop?.OnActivate (state.Toplevel);
-				Top.SetSubViewNeedsDisplay ();
-				Refresh ();
-			}
-		}
-
-		firstIteration = false;
-
-		if (state.Toplevel != Top &&
-		(Top.NeedsDisplay || Top.SubViewNeedsDisplay || Top.LayoutNeeded)) {
-			state.Toplevel.SetNeedsDisplay (state.Toplevel.Frame);
-			Top.Draw ();
-			foreach (var top in _topLevels.Reverse ()) {
-				if (top != Top && top != state.Toplevel) {
-					top.SetNeedsDisplay ();
-					top.SetSubViewNeedsDisplay ();
-					top.Draw ();
-				}
-			}
-		}
-		if (_topLevels.Count == 1 && state.Toplevel == Top
-					&& (Driver.Cols != state.Toplevel.Frame.Width || Driver.Rows != state.Toplevel.Frame.Height)
-					&& (state.Toplevel.NeedsDisplay || state.Toplevel.SubViewNeedsDisplay || state.Toplevel.LayoutNeeded)) {
-
-			state.Toplevel.Clear (Driver.Bounds);
-		}
-
-		if (state.Toplevel.NeedsDisplay ||
-		state.Toplevel.SubViewNeedsDisplay ||
-		state.Toplevel.LayoutNeeded ||
-		OverlappedChildNeedsDisplay ()) {
-			state.Toplevel.Draw ();
-			state.Toplevel.PositionCursor ();
-			Driver.Refresh ();
-		} else {
-			Driver.UpdateCursor ();
-		}
-		if (state.Toplevel != Top &&
-		!state.Toplevel.Modal &&
-		(Top.NeedsDisplay || Top.SubViewNeedsDisplay || Top.LayoutNeeded)) {
-			Top.Draw ();
-		}
-	}
-
-	/// <summary>
-	/// Stops running the most recent <see cref="Toplevel"/> or the <paramref name="top"/> if provided.
-	/// </summary>
-	/// <param name="top">The <see cref="Toplevel"/> to stop.</param>
-	/// <remarks>
-	///  <para>
-	///  This will cause <see cref="Application.Run(Func{Exception, bool})"/> to return.
-	///  </para>
-	///  <para>
-	///   Calling <see cref="Application.RequestStop"/> is equivalent to setting the <see cref="Toplevel.Running"/> property 
-	///   on the currently running <see cref="Toplevel"/> to false.
-	///  </para>
-	/// </remarks>
-	public static void RequestStop (Toplevel top = null)
-	{
-		if (OverlappedTop == null || top == null || OverlappedTop == null && top != null) {
-			top = Current;
-		}
-
-		if (OverlappedTop != null && top.IsOverlappedContainer && top?.Running == true
-		&& (Current?.Modal == false || Current?.Modal == true && Current?.Running == false)) {
-
-			OverlappedTop.RequestStop ();
-		} else if (OverlappedTop != null && top != Current && Current?.Running == true && Current?.Modal == true
-			&& top.Modal && top.Running) {
-
-			var ev = new ToplevelClosingEventArgs (Current);
-			Current.OnClosing (ev);
-			if (ev.Cancel) {
-				return;
-			}
-			ev = new ToplevelClosingEventArgs (top);
-			top.OnClosing (ev);
-			if (ev.Cancel) {
-				return;
-			}
-			Current.Running = false;
-			OnNotifyStopRunState (Current);
-			top.Running = false;
-			OnNotifyStopRunState (top);
-		} else if (OverlappedTop != null && top != OverlappedTop && top != Current && Current?.Modal == false
-			&& Current?.Running == true && !top.Running
-			|| OverlappedTop != null && top != OverlappedTop && top != Current && Current?.Modal == false
-			&& Current?.Running == false && !top.Running && _topLevels.ToArray () [1].Running) {
-
-			MoveCurrent (top);
-		} else if (OverlappedTop != null && Current != top && Current?.Running == true && !top.Running
-			&& Current?.Modal == true && top.Modal) {
-			// The Current and the top are both modal so needed to set the Current.Running to false too.
-			Current.Running = false;
-			OnNotifyStopRunState (Current);
-		} else if (OverlappedTop != null && Current == top && OverlappedTop?.Running == true && Current?.Running == true && top.Running
-			&& Current?.Modal == true && top.Modal) {
-			// The OverlappedTop was requested to stop inside a modal Toplevel which is the Current and top,
-			// both are the same, so needed to set the Current.Running to false too.
-			Current.Running = false;
-			OnNotifyStopRunState (Current);
-		} else {
-			Toplevel currentTop;
-			if (top == Current || Current?.Modal == true && !top.Modal) {
-				currentTop = Current;
-			} else {
-				currentTop = top;
-			}
-			if (!currentTop.Running) {
-				return;
-			}
-			var ev = new ToplevelClosingEventArgs (currentTop);
-			currentTop.OnClosing (ev);
-			if (ev.Cancel) {
-				return;
-			}
-			currentTop.Running = false;
-			OnNotifyStopRunState (currentTop);
-		}
-	}
-
-	static void OnNotifyStopRunState (Toplevel top)
-	{
-		if (EndAfterFirstIteration) {
-			NotifyStopRunState?.Invoke (top, new ToplevelEventArgs (top));
-		}
-	}
-
-	/// <summary>
-	/// Building block API: completes the execution of a <see cref="Toplevel"/> that was started with <see cref="Begin(Toplevel)"/> .
-	/// </summary>
-	/// <param name="runState">The <see cref="RunState"/> returned by the <see cref="Begin(Toplevel)"/> method.</param>
-	public static void End (RunState runState)
-	{
-		if (runState == null) {
-			throw new ArgumentNullException (nameof (runState));
-		}
-
-		if (OverlappedTop != null) {
-			OverlappedTop.OnChildUnloaded (runState.Toplevel);
-		} else {
-			runState.Toplevel.OnUnloaded ();
-		}
-
-		// End the RunState.Toplevel 
-		// First, take it off the Toplevel Stack
-		if (_topLevels.Count > 0) {
-			if (_topLevels.Peek () != runState.Toplevel) {
-				// If there the top of the stack is not the RunState.Toplevel then
-				// this call to End is not balanced with the call to Begin that started the RunState
-				throw new ArgumentException ("End must be balanced with calls to Begin");
-			}
-			_topLevels.Pop ();
-		}
-
-		// Notify that it is closing
-		runState.Toplevel?.OnClosed (runState.Toplevel);
-
-		// If there is a OverlappedTop that is not the RunState.Toplevel then runstate.TopLevel 
-		// is a child of MidTop and we should notify the OverlappedTop that it is closing
-		if (OverlappedTop != null && !runState.Toplevel.Modal && runState.Toplevel != OverlappedTop) {
-			OverlappedTop.OnChildClosed (runState.Toplevel);
-		}
-
-		// Set Current and Top to the next TopLevel on the stack
-		if (_topLevels.Count == 0) {
-			Current = null;
-		} else {
-			Current = _topLevels.Peek ();
-			if (_topLevels.Count == 1 && Current == OverlappedTop) {
-				OverlappedTop.OnAllChildClosed ();
-			} else {
-				SetCurrentOverlappedAsTop ();
-				runState.Toplevel.OnLeave (Current);
-				Current.OnEnter (runState.Toplevel);
-			}
-			Refresh ();
-		}
-
-		runState.Toplevel?.Dispose ();
-		runState.Toplevel = null;
-		runState.Dispose ();
-	}
-	#endregion Run (Begin, Run, End)
-
-	#region Toplevel handling
-	/// <summary>
-	/// Holds the stack of TopLevel views.
-	/// </summary>
-	// BUGBUG: Techncally, this is not the full lst of TopLevels. THere be dragons hwre. E.g. see how Toplevel.Id is used. What
-	// about TopLevels that are just a SubView of another View?
-	internal static readonly Stack<Toplevel> _topLevels = new Stack<Toplevel> ();
-
-	/// <summary>
-	/// The <see cref="Toplevel"/> object used for the application on startup (<seealso cref="Application.Top"/>)
-	/// </summary>
-	/// <value>The top.</value>
-	public static Toplevel Top { get; private set; }
-
-	/// <summary>
-	/// The current <see cref="Toplevel"/> object. This is updated when <see cref="Application.Run(Func{Exception, bool})"/> 
-	/// enters and leaves to point to the current <see cref="Toplevel"/> .
-	/// </summary>
-	/// <value>The current.</value>
-	public static Toplevel Current { get; private set; }
-
-	static void EnsureModalOrVisibleAlwaysOnTop (Toplevel Toplevel)
-	{
-		if (!Toplevel.Running || Toplevel == Current && Toplevel.Visible || OverlappedTop == null || _topLevels.Peek ().Modal) {
-			return;
-		}
-
-		foreach (var top in _topLevels.Reverse ()) {
-			if (top.Modal && top != Current) {
-				MoveCurrent (top);
-				return;
-			}
-		}
-		if (!Toplevel.Visible && Toplevel == Current) {
-			OverlappedMoveNext ();
-		}
-	}
-
-	static View FindDeepestTop (Toplevel start, int x, int y, out int resx, out int resy)
-	{
-		var startFrame = start.Frame;
-
-		if (!startFrame.Contains (x, y)) {
-			resx = 0;
-			resy = 0;
-			return null;
-		}
-
-		if (_topLevels != null) {
-			int count = _topLevels.Count;
-			if (count > 0) {
-				int rx = x - startFrame.X;
-				int ry = y - startFrame.Y;
-				foreach (var t in _topLevels) {
-					if (t != Current) {
-						if (t != start && t.Visible && t.Frame.Contains (rx, ry)) {
-							start = t;
-							break;
-						}
-					}
-				}
-			}
-		}
-		resx = x - startFrame.X;
-		resy = y - startFrame.Y;
-		return start;
-	}
-
-	static View FindTopFromView (View view)
-	{
-		var top = view?.SuperView != null && view?.SuperView != Top
-			? view.SuperView : view;
-
-		while (top?.SuperView != null && top?.SuperView != Top) {
-			top = top.SuperView;
-		}
-		return top;
-	}
-
-	// Only return true if the Current has changed.
-	static bool MoveCurrent (Toplevel top)
-	{
-		// The Current is modal and the top is not modal Toplevel then
-		// the Current must be moved above the first not modal Toplevel.
-		if (OverlappedTop != null && top != OverlappedTop && top != Current && Current?.Modal == true && !_topLevels.Peek ().Modal) {
-			lock (_topLevels) {
-				_topLevels.MoveTo (Current, 0, new ToplevelEqualityComparer ());
-			}
-			int index = 0;
-			var savedToplevels = _topLevels.ToArray ();
-			foreach (var t in savedToplevels) {
-				if (!t.Modal && t != Current && t != top && t != savedToplevels [index]) {
-					lock (_topLevels) {
-						_topLevels.MoveTo (top, index, new ToplevelEqualityComparer ());
-					}
-				}
-				index++;
-			}
-			return false;
-		}
-		// The Current and the top are both not running Toplevel then
-		// the top must be moved above the first not running Toplevel.
-		if (OverlappedTop != null && top != OverlappedTop && top != Current && Current?.Running == false && !top.Running) {
-			lock (_topLevels) {
-				_topLevels.MoveTo (Current, 0, new ToplevelEqualityComparer ());
-			}
-			int index = 0;
-			foreach (var t in _topLevels.ToArray ()) {
-				if (!t.Running && t != Current && index > 0) {
-					lock (_topLevels) {
-						_topLevels.MoveTo (top, index - 1, new ToplevelEqualityComparer ());
-					}
-				}
-				index++;
-			}
-			return false;
-		}
-		if (OverlappedTop != null && top?.Modal == true && _topLevels.Peek () != top
-		|| OverlappedTop != null && Current != OverlappedTop && Current?.Modal == false && top == OverlappedTop
-		|| OverlappedTop != null && Current?.Modal == false && top != Current
-		|| OverlappedTop != null && Current?.Modal == true && top == OverlappedTop) {
-			lock (_topLevels) {
-				_topLevels.MoveTo (top, 0, new ToplevelEqualityComparer ());
-				Current = top;
-			}
-		}
-		return true;
-	}
-
-	/// <summary>
-	/// Invoked when the terminal's size changed. The new size of the terminal is provided.
-	/// </summary>
-	/// <remarks>
-	/// Event handlers can set <see cref="SizeChangedEventArgs.Cancel"/> to <see langword="true"/>
-	/// to prevent <see cref="Application"/> from changing it's size to match the new terminal size.
-	/// </remarks>
-	public static event EventHandler<SizeChangedEventArgs> SizeChanging;
-
-	/// <summary>
-	/// Called when the application's size changes. Sets the size of all <see cref="Toplevel"/>s and
-	/// fires the <see cref="SizeChanging"/> event.
-	/// </summary>
-	/// <param name="args">The new size.</param>
-	/// <returns><see lanword="true"/>if the size was changed.</returns>
-	public static bool OnSizeChanging (SizeChangedEventArgs args)
-	{
-		SizeChanging?.Invoke (null, args);
-		if (args.Cancel) {
-			return false;
-		}
-
-		foreach (var t in _topLevels) {
-			t.SetRelativeLayout (new Rect (0, 0, args.Size.Width, args.Size.Height));
-			t.LayoutSubviews ();
-			t.PositionToplevels ();
-			t.OnSizeChanging (new SizeChangedEventArgs (args.Size));
-		}
-		Refresh ();
-		return true;
-	}
-	#endregion Toplevel handling
-
-	#region Mouse handling
-	/// <summary>
-	/// Disable or enable the mouse. The mouse is enabled by default.
-	/// </summary>
-	[SerializableConfigurationProperty (Scope = typeof (SettingsScope))]
-	public static bool IsMouseDisabled { get; set; }
-
-	/// <summary>
-	/// The current <see cref="View"/> object that wants continuous mouse button pressed events.
-	/// </summary>
-	public static View WantContinuousButtonPressedView { get; private set; }
-
-	/// <summary>
-	/// Gets the view that grabbed the mouse (e.g. for dragging). When this is set, all mouse events will be
-	/// routed to this view until the view calls <see cref="UngrabMouse"/> or the mouse is released.
-	/// </summary>
-	public static View MouseGrabView { get; private set; }
-
-	/// <summary>
-	/// Invoked when a view wants to grab the mouse; can be canceled.
-	/// </summary>
-	public static event EventHandler<GrabMouseEventArgs> GrabbingMouse;
-
-	/// <summary>
-	/// Invoked when a view wants un-grab the mouse; can be canceled.
-	/// </summary>
-	public static event EventHandler<GrabMouseEventArgs> UnGrabbingMouse;
-
-	/// <summary>
-	/// Invoked after a view has grabbed the mouse.
-	/// </summary>
-	public static event EventHandler<ViewEventArgs> GrabbedMouse;
-
-	/// <summary>
-	/// Invoked after a view has un-grabbed the mouse.
-	/// </summary>
-	public static event EventHandler<ViewEventArgs> UnGrabbedMouse;
-
-	/// <summary>
-	/// Grabs the mouse, forcing all mouse events to be routed to the specified view until <see cref="UngrabMouse"/> is called.
-	/// </summary>
-	/// <param name="view">View that will receive all mouse events until <see cref="UngrabMouse"/> is invoked.</param>
-	public static void GrabMouse (View view)
-	{
-		if (view == null) {
-			return;
-		}
-		if (!OnGrabbingMouse (view)) {
-			OnGrabbedMouse (view);
-			MouseGrabView = view;
-		}
-	}
-
-	/// <summary>
-	/// Releases the mouse grab, so mouse events will be routed to the view on which the mouse is.
-	/// </summary>
-	public static void UngrabMouse ()
-	{
-		if (MouseGrabView == null) {
-			return;
-		}
-		if (!OnUnGrabbingMouse (MouseGrabView)) {
-			OnUnGrabbedMouse (MouseGrabView);
-			MouseGrabView = null;
-		}
-	}
-
-	static bool OnGrabbingMouse (View view)
-	{
-		if (view == null) {
-			return false;
-		}
-		var evArgs = new GrabMouseEventArgs (view);
-		GrabbingMouse?.Invoke (view, evArgs);
-		return evArgs.Cancel;
-	}
-
-	static bool OnUnGrabbingMouse (View view)
-	{
-		if (view == null) {
-			return false;
-		}
-		var evArgs = new GrabMouseEventArgs (view);
-		UnGrabbingMouse?.Invoke (view, evArgs);
-		return evArgs.Cancel;
-	}
-
-	static void OnGrabbedMouse (View view)
-	{
-		if (view == null) {
-			return;
-		}
-		GrabbedMouse?.Invoke (view, new ViewEventArgs (view));
-	}
-
-	static void OnUnGrabbedMouse (View view)
-	{
-		if (view == null) {
-			return;
-		}
-		UnGrabbedMouse?.Invoke (view, new ViewEventArgs (view));
-	}
-
-	// Used by OnMouseEvent to track the last view that was clicked on.
-	internal static View _mouseEnteredView;
-
-	/// <summary>
-	/// Event fired when a mouse move or click occurs. Coordinates are screen relative.
-	/// </summary>
-	/// <remarks>
-	/// <para>
-	/// Use this event to receive mouse events in screen coordinates. Use <see cref="Responder.MouseEvent"/> to receive
-	/// mouse events relative to a <see cref="View"/>'s bounds.
-	/// </para>
-	/// <para>
-	/// The <see cref="MouseEvent.View"/> will contain the <see cref="View"/> that contains the mouse coordinates.
-	/// </para>
-	/// </remarks>
-	public static event EventHandler<MouseEventEventArgs> MouseEvent;
-
-	/// <summary>
-	/// Called when a mouse event occurs. Fires the <see cref="MouseEvent"/> event.
-	/// </summary>
-	/// <remarks>
-	/// This method can be used to simulate a mouse event, e.g. in unit tests.
-	/// </remarks>
-	/// <param name="a">The mouse event with coordinates relative to the screen.</param>
-	public static void OnMouseEvent (MouseEventEventArgs a)
-	{
-		static bool OutsideRect (Point p, Rect r) => p.X < 0 || p.X > r.Right || p.Y < 0 || p.Y > r.Bottom;
-
-		if (IsMouseDisabled) {
-			return;
-		}
-
-		var view = View.FindDeepestView (Current, a.MouseEvent.X, a.MouseEvent.Y, out int screenX, out int screenY);
-
-		if (view != null && view.WantContinuousButtonPressed) {
-			WantContinuousButtonPressedView = view;
-		} else {
-			WantContinuousButtonPressedView = null;
-		}
-		if (view != null) {
-			a.MouseEvent.View = view;
-		}
-		MouseEvent?.Invoke (null, new MouseEventEventArgs (a.MouseEvent));
-
-		if (a.MouseEvent.Handled) {
-			return;
-		}
-
-		if (MouseGrabView != null) {
-			// If the mouse is grabbed, send the event to the view that grabbed it.
-			// The coordinates are relative to the Bounds of the view that grabbed the mouse.
-			var newxy = MouseGrabView.ScreenToFrame (a.MouseEvent.X, a.MouseEvent.Y);
-			var nme = new MouseEvent () {
-				X = newxy.X,
-				Y = newxy.Y,
-				Flags = a.MouseEvent.Flags,
-				OfX = a.MouseEvent.X - newxy.X,
-				OfY = a.MouseEvent.Y - newxy.Y,
-				View = view
-			};
-			if (OutsideRect (new Point (nme.X, nme.Y), MouseGrabView.Bounds)) {
-				// The mouse has moved outside the bounds of the the view that
-				// grabbed the mouse, so we tell the view that last got 
-				// OnMouseEnter the mouse is leaving
-				// BUGBUG: That sentence makes no sense. Either I'm missing something
-				// or this logic is flawed.
-				_mouseEnteredView?.OnMouseLeave (a.MouseEvent);
-			}
-			//System.Diagnostics.Debug.WriteLine ($"{nme.Flags};{nme.X};{nme.Y};{mouseGrabView}");
-			if (MouseGrabView?.OnMouseEvent (nme) == true) {
-				return;
-			}
-		}
-
-		if ((view == null || view == OverlappedTop) &&
-		Current is { Modal: false } && OverlappedTop != null &&
-		a.MouseEvent.Flags != MouseFlags.ReportMousePosition &&
-		a.MouseEvent.Flags != 0) {
-
-			var top = FindDeepestTop (Top, a.MouseEvent.X, a.MouseEvent.Y, out _, out _);
-			view = View.FindDeepestView (top, a.MouseEvent.X, a.MouseEvent.Y, out screenX, out screenY);
-
-			if (view != null && view != OverlappedTop && top != Current) {
-				MoveCurrent ((Toplevel)top);
-			}
-		}
-
-		bool AdornmentHandledMouseEvent(Adornment frame)
-		{
-			if (frame?.Thickness.Contains (frame.FrameToScreen (), a.MouseEvent.X, a.MouseEvent.Y) ?? false) {
-				var boundsPoint = frame.ScreenToBounds (a.MouseEvent.X, a.MouseEvent.Y);
-				var me = new MouseEvent () {
-					X = boundsPoint.X,
-					Y = boundsPoint.Y,
-					Flags = a.MouseEvent.Flags,
-					OfX = boundsPoint.X,
-					OfY = boundsPoint.Y,
-					View = frame
-				};
-				frame.OnMouseEvent (me);
-				return true;
-			}
-			return false;
-		}
-
-		if (view != null) {
-			// Work inside-out (Padding, Border, Margin)
-			// TODO: Debate whether inside-out or outside-in is the right strategy
-			if (AdornmentHandledMouseEvent(view?.Padding)) {
-				return;
-			}
-			if (AdornmentHandledMouseEvent(view?.Border)) {
-				if (view is Toplevel) {
-					// TODO: This is a temporary hack to work around the fact that 
-					// drag handling is handled in Toplevel (See Issue #2537)
-
-					var me = new MouseEvent () {
-						X = screenX,
-						Y = screenY,
-						Flags = a.MouseEvent.Flags,
-						OfX = screenX,
-						OfY = screenY,
-						View = view
-					};
-
-					if (_mouseEnteredView == null) {
-						_mouseEnteredView = view;
-						view.OnMouseEnter (me);
-					} else if (_mouseEnteredView != view) {
-						_mouseEnteredView.OnMouseLeave (me);
-						view.OnMouseEnter (me);
-						_mouseEnteredView = view;
-					}
-
-					if (!view.WantMousePositionReports && a.MouseEvent.Flags == MouseFlags.ReportMousePosition) {
-						return;
-					}
-
-					WantContinuousButtonPressedView = view.WantContinuousButtonPressed ? view : null;
-
-					if (view.OnMouseEvent (me)) {
-						// Should we bubble up the event, if it is not handled?
-						//return;
-					}
-
-					BringOverlappedTopToFront ();
-				}
-				return;
-			}
-
-			if (AdornmentHandledMouseEvent(view?.Margin)) {
-				return;
-			}
-
-			var bounds = view.BoundsToScreen (view.Bounds);
-			if (bounds.Contains (a.MouseEvent.X, a.MouseEvent.Y)) {
-				var boundsPoint = view.ScreenToBounds (a.MouseEvent.X, a.MouseEvent.Y);
-				var me = new MouseEvent () {
-					X = boundsPoint.X,
-					Y = boundsPoint.Y,
-					Flags = a.MouseEvent.Flags,
-					OfX = boundsPoint.X,
-					OfY = boundsPoint.Y,
-					View = view
-				};
-
-				if (_mouseEnteredView == null) {
-					_mouseEnteredView = view;
-					view.OnMouseEnter (me);
-				} else if (_mouseEnteredView != view) {
-					_mouseEnteredView.OnMouseLeave (me);
-					view.OnMouseEnter (me);
-					_mouseEnteredView = view;
-				}
-
-				if (!view.WantMousePositionReports && a.MouseEvent.Flags == MouseFlags.ReportMousePosition) {
-					return;
-				}
-
-				WantContinuousButtonPressedView = view.WantContinuousButtonPressed ? view : null;
-
-				if (view.OnMouseEvent (me)) {
-					// Should we bubble up the event, if it is not handled?
-					//return;
-				}
-
-				BringOverlappedTopToFront ();
-			}
-		}
-	}
-	#endregion Mouse handling
-
-	#region Keyboard handling
-	static Key _alternateForwardKey = Key.Empty; // Defined in config.json
-
-	/// <summary>
-	/// Alternative key to navigate forwards through views. Ctrl+Tab is the primary key.
-	/// </summary>
-	[SerializableConfigurationProperty (Scope = typeof (SettingsScope))]
-	[JsonConverter (typeof (KeyJsonConverter))]
-	public static Key AlternateForwardKey {
-		get => _alternateForwardKey;
-		set {
-			if (_alternateForwardKey != value) {
-				var oldKey = _alternateForwardKey;
-				_alternateForwardKey = value;
-				OnAlternateForwardKeyChanged (new KeyChangedEventArgs (oldKey, value));
-			}
-		}
-	}
-
-	static void OnAlternateForwardKeyChanged (KeyChangedEventArgs e)
-	{
-		foreach (var top in _topLevels.ToArray ()) {
-			top.OnAlternateForwardKeyChanged (e);
-		}
-	}
-
-	static Key _alternateBackwardKey = Key.Empty; // Defined in config.json
-
-	/// <summary>
-	/// Alternative key to navigate backwards through views. Shift+Ctrl+Tab is the primary key.
-	/// </summary>
-	[SerializableConfigurationProperty (Scope = typeof (SettingsScope))]
-	[JsonConverter (typeof (KeyJsonConverter))]
-	public static Key AlternateBackwardKey {
-		get => _alternateBackwardKey;
-		set {
-			if (_alternateBackwardKey != value) {
-				var oldKey = _alternateBackwardKey;
-				_alternateBackwardKey = value;
-				OnAlternateBackwardKeyChanged (new KeyChangedEventArgs (oldKey, value));
-			}
-		}
-	}
-
-	static void OnAlternateBackwardKeyChanged (KeyChangedEventArgs oldKey)
-	{
-		foreach (var top in _topLevels.ToArray ()) {
-			top.OnAlternateBackwardKeyChanged (oldKey);
-		}
-	}
-
-	static Key _quitKey = Key.Empty; // Defined in config.json
-
-	/// <summary>
-	/// Gets or sets the key to quit the application.
-	/// </summary>
-	[SerializableConfigurationProperty (Scope = typeof (SettingsScope))]
-	[JsonConverter (typeof (KeyJsonConverter))]
-	public static Key QuitKey {
-		get => _quitKey;
-		set {
-			if (_quitKey != value) {
-				var oldKey = _quitKey;
-				_quitKey = value;
-				OnQuitKeyChanged (new KeyChangedEventArgs (oldKey, value));
-			}
-		}
-	}
-
-	static void OnQuitKeyChanged (KeyChangedEventArgs e)
-	{
-		// Duplicate the list so if it changes during enumeration we're safe
-		foreach (var top in _topLevels.ToArray ()) {
-			top.OnQuitKeyChanged (e);
-		}
-	}
-
-	/// <summary>
-	/// Event fired when the user presses a key. Fired by <see cref="OnKeyDown"/>. 
-	/// <para>
-	/// Set <see cref="Key.Handled"/> to <see langword="true"/> to indicate the key was handled and
-	/// to prevent additional processing.
-	/// </para>
-	/// </summary>
-	/// <remarks>
-	/// All drivers support firing the <see cref="KeyDown"/> event. Some drivers (Curses)
-	/// do not support firing the <see cref="KeyDown"/> and <see cref="KeyUp"/> events.
-	/// <para>
-	/// Fired after <see cref="KeyDown"/> and before <see cref="KeyUp"/>.
-	/// </para>
-	/// </remarks>
-	public static event EventHandler<Key> KeyDown;
-
-	/// <summary>
-	/// Called by the <see cref="ConsoleDriver"/> when the user presses a key.
-	/// Fires the <see cref="KeyDown"/> event
-	/// then calls <see cref="View.NewKeyDownEvent"/> on all top level views.
-	/// Called after <see cref="OnKeyDown"/> and before <see cref="OnKeyUp"/>.
-	/// </summary>
-	/// <remarks>
-	/// Can be used to simulate key press events.
-	/// </remarks>
-	/// <param name="keyEvent"></param>
-	/// <returns><see langword="true"/> if the key was handled.</returns>
-	public static bool OnKeyDown (Key keyEvent)
-	{
-		if (!_initialized) {
-			return true;
-		}
-
-		KeyDown?.Invoke (null, keyEvent);
-		if (keyEvent.Handled) {
-			return true;
-		}
-
-		foreach (var topLevel in _topLevels.ToList ()) {
-			if (topLevel.NewKeyDownEvent (keyEvent)) {
-				return true;
-			}
-			if (topLevel.Modal) {
-				break;
-			}
-		}
-
-		// Invoke any Global KeyBindings
-		foreach (var topLevel in _topLevels.ToList ()) {
-			foreach (var view in topLevel.Subviews.Where (v => v.KeyBindings.TryGet (keyEvent.KeyCode, KeyBindingScope.Application, out var _))) {
-				if (view.KeyBindings.TryGet (keyEvent.KeyCode, KeyBindingScope.Application, out var _)) {
-					keyEvent.Scope = KeyBindingScope.Application;
-					bool? handled = view.OnInvokingKeyBindings (keyEvent);
-					if (handled != null && (bool)handled) {
-						return true;
-					}
-				}
-			}
-		}
-
-		return false;
-	}
-
-	/// <summary>
-	/// Event fired when the user releases a key. Fired by <see cref="OnKeyUp"/>.
-	/// <para>
-	/// Set <see cref="Key.Handled"/> to <see langword="true"/> to indicate the key was handled and
-	/// to prevent additional processing.
-	/// </para>
-	/// </summary>
-	/// <remarks>
-	/// All drivers support firing the <see cref="KeyDown"/> event. Some drivers (Curses)
-	/// do not support firing the <see cref="KeyDown"/> and <see cref="KeyUp"/> events.
-	/// <para>
-	/// Fired after <see cref="KeyDown"/>.
-	/// </para>
-	/// </remarks>
-	public static event EventHandler<Key> KeyUp;
-
-	/// <summary>
-	/// Called by the <see cref="ConsoleDriver"/> when the user releases a key.
-	/// Fires the <see cref="KeyUp"/> event
-	/// then calls <see cref="View.NewKeyUpEvent"/> on all top level views.
-	/// Called after <see cref="OnKeyDown"/>.
-	/// </summary>
-	/// <remarks>
-	/// Can be used to simulate key press events.
-	/// </remarks>
-	/// <param name="a"></param>
-	/// <returns><see langword="true"/> if the key was handled.</returns>
-	public static bool OnKeyUp (Key a)
-	{
-		if (!_initialized) {
-			return true;
-		}
-
-		KeyUp?.Invoke (null, a);
-		if (a.Handled) {
-			return true;
-		}
-		foreach (var topLevel in _topLevels.ToList ()) {
-			if (topLevel.NewKeyUpEvent (a)) {
-				return true;
-			}
-			if (topLevel.Modal) {
-				break;
-			}
-		}
-		return false;
-	}
-	#endregion Keyboard handling
-}
-/// <summary>
-/// Event arguments for the <see cref="Application.Iteration"/> event.
-/// </summary>
-public class IterationEventArgs {
+        }
+    }
+
+    /// <summary>Adds a timeout to the application.</summary>
+    /// <remarks>
+    ///     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. The returned value is a
+    ///     token that can be used to stop the timeout by calling <see cref="RemoveTimeout(object)"/>.
+    /// </remarks>
+    public static object AddTimeout (TimeSpan time, Func<bool> callback) { return MainLoop?.AddTimeout (time, callback); }
+
+    /// <summary>Removes a previously scheduled timeout</summary>
+    /// <remarks>The token parameter is the value returned by <see cref="AddTimeout"/>.</remarks>
+    /// 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 static bool RemoveTimeout (object token) { return MainLoop?.RemoveTimeout (token) ?? false; }
+
+    /// <summary>Runs <paramref name="action"/> on the thread that is processing events</summary>
+    /// <param name="action">the action to be invoked on the main processing thread.</param>
+    public static void Invoke (Action action)
+    {
+        MainLoop?.AddIdle (
+                           () =>
+                           {
+                               action ();
+
+                               return false;
+                           }
+                          );
+    }
+
+    // TODO: Determine if this is really needed. The only code that calls WakeUp I can find
+    // is ProgressBarStyles and it's not clear it needs to.
+    /// <summary>Wakes up the running application that might be waiting on input.</summary>
+    public static void Wakeup () { MainLoop?.Wakeup (); }
+
+    /// <summary>Triggers a refresh of the entire display.</summary>
+    public static void Refresh ()
+    {
+        // TODO: Figure out how to remove this call to ClearContents. Refresh should just repaint damaged areas, not clear
+        Driver.ClearContents ();
+        View last = null;
+
+        foreach (Toplevel v in _topLevels.Reverse ())
+        {
+            if (v.Visible)
+            {
+                v.SetNeedsDisplay ();
+                v.SetSubViewNeedsDisplay ();
+                v.Draw ();
+            }
+
+            last = v;
+        }
+
+        last?.PositionCursor ();
+        Driver.Refresh ();
+    }
+
+    /// <summary>This event is raised on each iteration of the main loop.</summary>
+    /// <remarks>See also <see cref="Timeout"/></remarks>
+    public static event EventHandler<IterationEventArgs> Iteration;
+
+    /// <summary>The <see cref="MainLoop"/> driver for the application</summary>
+    /// <value>The main loop.</value>
+    internal static MainLoop MainLoop { get; private set; }
+
+    /// <summary>
+    ///     Set to true to cause <see cref="End"/> to be called after the first iteration. Set to false (the default) to
+    ///     cause the application to continue running until Application.RequestStop () is called.
+    /// </summary>
+    public static bool EndAfterFirstIteration { get; set; }
+
+    /// <summary>Building block API: Runs the main loop for the created <see cref="Toplevel"/>.</summary>
+    /// <param name="state">The state returned by the <see cref="Begin(Toplevel)"/> method.</param>
+    public static void RunLoop (RunState state)
+    {
+        if (state is null)
+        {
+            throw new ArgumentNullException (nameof (state));
+        }
+
+        if (state.Toplevel is null)
+        {
+            throw new ObjectDisposedException ("state");
+        }
+
+        var firstIteration = true;
+
+        for (state.Toplevel.Running = true; state.Toplevel.Running;)
+        {
+            MainLoop.Running = true;
+
+            if (EndAfterFirstIteration && !firstIteration)
+            {
+                return;
+            }
+
+            RunIteration (ref state, ref firstIteration);
+        }
+
+        MainLoop.Running = false;
+
+        // Run one last iteration to consume any outstanding input events from Driver
+        // This is important for remaining OnKeyUp events.
+        RunIteration (ref state, ref firstIteration);
+    }
+
+    /// <summary>Run one application iteration.</summary>
+    /// <param name="state">The state returned by <see cref="Begin(Toplevel)"/>.</param>
+    /// <param name="firstIteration">
+    ///     Set to <see langword="true"/> if this is the first run loop iteration. Upon return, it
+    ///     will be set to <see langword="false"/> if at least one iteration happened.
+    /// </param>
+    public static void RunIteration (ref RunState state, ref bool firstIteration)
+    {
+        if (MainLoop.Running && MainLoop.EventsPending ())
+        {
+            // Notify Toplevel it's ready
+            if (firstIteration)
+            {
+                state.Toplevel.OnReady ();
+            }
+
+            MainLoop.RunIteration ();
+            Iteration?.Invoke (null, new IterationEventArgs ());
+            EnsureModalOrVisibleAlwaysOnTop (state.Toplevel);
+
+            if (state.Toplevel != Current)
+            {
+                OverlappedTop?.OnDeactivate (state.Toplevel);
+                state.Toplevel = Current;
+                OverlappedTop?.OnActivate (state.Toplevel);
+                Top.SetSubViewNeedsDisplay ();
+                Refresh ();
+            }
+        }
+
+        firstIteration = false;
+
+        if (state.Toplevel != Top && (Top.NeedsDisplay || Top.SubViewNeedsDisplay || Top.LayoutNeeded))
+        {
+            state.Toplevel.SetNeedsDisplay (state.Toplevel.Frame);
+            Top.Draw ();
+
+            foreach (Toplevel top in _topLevels.Reverse ())
+            {
+                if (top != Top && top != state.Toplevel)
+                {
+                    top.SetNeedsDisplay ();
+                    top.SetSubViewNeedsDisplay ();
+                    top.Draw ();
+                }
+            }
+        }
+
+        if (_topLevels.Count == 1
+            && state.Toplevel == Top
+            && (Driver.Cols != state.Toplevel.Frame.Width
+                || Driver.Rows != state.Toplevel.Frame.Height)
+            && (state.Toplevel.NeedsDisplay
+                || state.Toplevel.SubViewNeedsDisplay
+                || state.Toplevel.LayoutNeeded))
+        {
+            state.Toplevel.Clear (Driver.Bounds);
+        }
+
+        if (state.Toplevel.NeedsDisplay || state.Toplevel.SubViewNeedsDisplay || state.Toplevel.LayoutNeeded || OverlappedChildNeedsDisplay ())
+        {
+            state.Toplevel.Draw ();
+            state.Toplevel.PositionCursor ();
+            Driver.Refresh ();
+        }
+        else
+        {
+            Driver.UpdateCursor ();
+        }
+
+        if (state.Toplevel != Top && !state.Toplevel.Modal && (Top.NeedsDisplay || Top.SubViewNeedsDisplay || Top.LayoutNeeded))
+        {
+            Top.Draw ();
+        }
+    }
+
+    /// <summary>Stops running the most recent <see cref="Toplevel"/> or the <paramref name="top"/> if provided.</summary>
+    /// <param name="top">The <see cref="Toplevel"/> to stop.</param>
+    /// <remarks>
+    ///     <para>This will cause <see cref="Application.Run(Func{Exception, bool})"/> to return.</para>
+    ///     <para>
+    ///         Calling <see cref="Application.RequestStop"/> is equivalent to setting the <see cref="Toplevel.Running"/>
+    ///         property on the currently running <see cref="Toplevel"/> to false.
+    ///     </para>
+    /// </remarks>
+    public static void RequestStop (Toplevel top = null)
+    {
+        if (OverlappedTop is null || top is null || (OverlappedTop is null && top is { }))
+        {
+            top = Current;
+        }
+
+        if (OverlappedTop != null
+            && top.IsOverlappedContainer
+            && top?.Running == true
+            && (Current?.Modal == false || (Current?.Modal == true && Current?.Running == false)))
+        {
+            OverlappedTop.RequestStop ();
+        }
+        else if (OverlappedTop != null
+                 && top != Current
+                 && Current?.Running == true
+                 && Current?.Modal == true
+                 && top.Modal
+                 && top.Running)
+        {
+            var ev = new ToplevelClosingEventArgs (Current);
+            Current.OnClosing (ev);
+
+            if (ev.Cancel)
+            {
+                return;
+            }
+
+            ev = new ToplevelClosingEventArgs (top);
+            top.OnClosing (ev);
+
+            if (ev.Cancel)
+            {
+                return;
+            }
+
+            Current.Running = false;
+            OnNotifyStopRunState (Current);
+            top.Running = false;
+            OnNotifyStopRunState (top);
+        }
+        else if ((OverlappedTop != null
+                  && top != OverlappedTop
+                  && top != Current
+                  && Current?.Modal == false
+                  && Current?.Running == true
+                  && !top.Running)
+                 || (OverlappedTop != null
+                     && top != OverlappedTop
+                     && top != Current
+                     && Current?.Modal == false
+                     && Current?.Running == false
+                     && !top.Running
+                     && _topLevels.ToArray () [1].Running))
+        {
+            MoveCurrent (top);
+        }
+        else if (OverlappedTop != null
+                 && Current != top
+                 && Current?.Running == true
+                 && !top.Running
+                 && Current?.Modal == true
+                 && top.Modal)
+        {
+            // The Current and the top are both modal so needed to set the Current.Running to false too.
+            Current.Running = false;
+            OnNotifyStopRunState (Current);
+        }
+        else if (OverlappedTop != null
+                 && Current == top
+                 && OverlappedTop?.Running == true
+                 && Current?.Running == true
+                 && top.Running
+                 && Current?.Modal == true
+                 && top.Modal)
+        {
+            // The OverlappedTop was requested to stop inside a modal Toplevel which is the Current and top,
+            // both are the same, so needed to set the Current.Running to false too.
+            Current.Running = false;
+            OnNotifyStopRunState (Current);
+        }
+        else
+        {
+            Toplevel currentTop;
+
+            if (top == Current || (Current?.Modal == true && !top.Modal))
+            {
+                currentTop = Current;
+            }
+            else
+            {
+                currentTop = top;
+            }
+
+            if (!currentTop.Running)
+            {
+                return;
+            }
+
+            var ev = new ToplevelClosingEventArgs (currentTop);
+            currentTop.OnClosing (ev);
+
+            if (ev.Cancel)
+            {
+                return;
+            }
+
+            currentTop.Running = false;
+            OnNotifyStopRunState (currentTop);
+        }
+    }
+
+    private static void OnNotifyStopRunState (Toplevel top)
+    {
+        if (EndAfterFirstIteration)
+        {
+            NotifyStopRunState?.Invoke (top, new ToplevelEventArgs (top));
+        }
+    }
+
+    /// <summary>
+    ///     Building block API: completes the execution of a <see cref="Toplevel"/> that was started with
+    ///     <see cref="Begin(Toplevel)"/> .
+    /// </summary>
+    /// <param name="runState">The <see cref="RunState"/> returned by the <see cref="Begin(Toplevel)"/> method.</param>
+    public static void End (RunState runState)
+    {
+        if (runState is null)
+        {
+            throw new ArgumentNullException (nameof (runState));
+        }
+
+        if (OverlappedTop is { })
+        {
+            OverlappedTop.OnChildUnloaded (runState.Toplevel);
+        }
+        else
+        {
+            runState.Toplevel.OnUnloaded ();
+        }
+
+        // End the RunState.Toplevel 
+        // First, take it off the Toplevel Stack
+        if (_topLevels.Count > 0)
+        {
+            if (_topLevels.Peek () != runState.Toplevel)
+            {
+                // If there the top of the stack is not the RunState.Toplevel then
+                // this call to End is not balanced with the call to Begin that started the RunState
+                throw new ArgumentException ("End must be balanced with calls to Begin");
+            }
+
+            _topLevels.Pop ();
+        }
+
+        // Notify that it is closing
+        runState.Toplevel?.OnClosed (runState.Toplevel);
+
+        // If there is a OverlappedTop that is not the RunState.Toplevel then runstate.TopLevel 
+        // is a child of MidTop and we should notify the OverlappedTop that it is closing
+        if (OverlappedTop is { } && !runState.Toplevel.Modal && runState.Toplevel != OverlappedTop)
+        {
+            OverlappedTop.OnChildClosed (runState.Toplevel);
+        }
+
+        // Set Current and Top to the next TopLevel on the stack
+        if (_topLevels.Count == 0)
+        {
+            Current = null;
+        }
+        else
+        {
+            Current = _topLevels.Peek ();
+
+            if (_topLevels.Count == 1 && Current == OverlappedTop)
+            {
+                OverlappedTop.OnAllChildClosed ();
+            }
+            else
+            {
+                SetCurrentOverlappedAsTop ();
+                runState.Toplevel.OnLeave (Current);
+                Current.OnEnter (runState.Toplevel);
+            }
+
+            Refresh ();
+        }
+
+        runState.Toplevel?.Dispose ();
+        runState.Toplevel = null;
+        runState.Dispose ();
+    }
+
+    #endregion Run (Begin, Run, End)
+
+    #region Toplevel handling
+
+    /// <summary>Holds the stack of TopLevel views.</summary>
+
+    // BUGBUG: Techncally, this is not the full lst of TopLevels. THere be dragons hwre. E.g. see how Toplevel.Id is used. What
+    // about TopLevels that are just a SubView of another View?
+    internal static readonly Stack<Toplevel> _topLevels = new ();
+
+    /// <summary>The <see cref="Toplevel"/> object used for the application on startup (<seealso cref="Application.Top"/>)</summary>
+    /// <value>The top.</value>
+    public static Toplevel Top { get; private set; }
+
+    /// <summary>
+    ///     The current <see cref="Toplevel"/> object. This is updated when
+    ///     <see cref="Application.Run(Func{Exception, bool})"/> enters and leaves to point to the current
+    ///     <see cref="Toplevel"/> .
+    /// </summary>
+    /// <value>The current.</value>
+    public static Toplevel Current { get; private set; }
+
+    private static void EnsureModalOrVisibleAlwaysOnTop (Toplevel Toplevel)
+    {
+        if (!Toplevel.Running
+            || (Toplevel == Current && Toplevel.Visible)
+            || OverlappedTop == null
+            || _topLevels.Peek ().Modal)
+        {
+            return;
+        }
+
+        foreach (Toplevel top in _topLevels.Reverse ())
+        {
+            if (top.Modal && top != Current)
+            {
+                MoveCurrent (top);
+
+                return;
+            }
+        }
+
+        if (!Toplevel.Visible && Toplevel == Current)
+        {
+            OverlappedMoveNext ();
+        }
+    }
+
+    #nullable enable
+    private static Toplevel? FindDeepestTop (Toplevel start, int x, int y)
+    {
+        if (!start.Frame.Contains (x, y))
+        {
+            return null;
+        }
+
+        if (_topLevels is { Count: > 0 })
+        {
+            int rx = x - start.Frame.X;
+            int ry = y - start.Frame.Y;
+
+            foreach (Toplevel t in _topLevels)
+            {
+                if (t != Current)
+                {
+                    if (t != start && t.Visible && t.Frame.Contains (rx, ry))
+                    {
+                        start = t;
+
+                        break;
+                    }
+                }
+            }
+        }
+
+        return start;
+    }
+    #nullable restore
+
+    private static View FindTopFromView (View view)
+    {
+        View top = view?.SuperView is { } && view?.SuperView != Top
+                       ? view.SuperView
+                       : view;
+
+        while (top?.SuperView is { } && top?.SuperView != Top)
+        {
+            top = top.SuperView;
+        }
+
+        return top;
+    }
+
+    #nullable enable
+    // Only return true if the Current has changed.
+    private static bool MoveCurrent (Toplevel? top)
+    {
+        // The Current is modal and the top is not modal Toplevel then
+        // the Current must be moved above the first not modal Toplevel.
+        if (OverlappedTop is { }
+            && top != OverlappedTop
+            && top != Current
+            && Current?.Modal == true
+            && !_topLevels.Peek ().Modal)
+        {
+            lock (_topLevels)
+            {
+                _topLevels.MoveTo (Current, 0, new ToplevelEqualityComparer ());
+            }
+
+            var index = 0;
+            Toplevel [] savedToplevels = _topLevels.ToArray ();
+
+            foreach (Toplevel t in savedToplevels)
+            {
+                if (!t.Modal && t != Current && t != top && t != savedToplevels [index])
+                {
+                    lock (_topLevels)
+                    {
+                        _topLevels.MoveTo (top, index, new ToplevelEqualityComparer ());
+                    }
+                }
+
+                index++;
+            }
+
+            return false;
+        }
+
+        // The Current and the top are both not running Toplevel then
+        // the top must be moved above the first not running Toplevel.
+        if (OverlappedTop is { }
+            && top != OverlappedTop
+            && top != Current
+            && Current?.Running == false
+            && !top.Running)
+        {
+            lock (_topLevels)
+            {
+                _topLevels.MoveTo (Current, 0, new ToplevelEqualityComparer ());
+            }
+
+            var index = 0;
+
+            foreach (Toplevel t in _topLevels.ToArray ())
+            {
+                if (!t.Running && t != Current && index > 0)
+                {
+                    lock (_topLevels)
+                    {
+                        _topLevels.MoveTo (top, index - 1, new ToplevelEqualityComparer ());
+                    }
+                }
+
+                index++;
+            }
+
+            return false;
+        }
+
+        if ((OverlappedTop is { } && top?.Modal == true && _topLevels.Peek () != top)
+            || (OverlappedTop is { } && Current != OverlappedTop && Current?.Modal == false && top == OverlappedTop)
+            || (OverlappedTop is { } && Current?.Modal == false && top != Current)
+            || (OverlappedTop is { } && Current?.Modal == true && top == OverlappedTop))
+        {
+            lock (_topLevels)
+            {
+                _topLevels.MoveTo (top, 0, new ToplevelEqualityComparer ());
+                Current = top;
+            }
+        }
+
+        return true;
+    }
+    #nullable restore
+
+    /// <summary>Invoked when the terminal's size changed. The new size of the terminal is provided.</summary>
+    /// <remarks>
+    ///     Event handlers can set <see cref="SizeChangedEventArgs.Cancel"/> to <see langword="true"/> to prevent
+    ///     <see cref="Application"/> from changing it's size to match the new terminal size.
+    /// </remarks>
+    public static event EventHandler<SizeChangedEventArgs> SizeChanging;
+
+    /// <summary>
+    ///     Called when the application's size changes. Sets the size of all <see cref="Toplevel"/>s and fires the
+    ///     <see cref="SizeChanging"/> event.
+    /// </summary>
+    /// <param name="args">The new size.</param>
+    /// <returns><see lanword="true"/>if the size was changed.</returns>
+    public static bool OnSizeChanging (SizeChangedEventArgs args)
+    {
+        SizeChanging?.Invoke (null, args);
+
+        if (args.Cancel)
+        {
+            return false;
+        }
+
+        foreach (Toplevel t in _topLevels)
+        {
+            t.SetRelativeLayout (Rectangle.Empty with { Size = args.Size });
+            t.LayoutSubviews ();
+            t.PositionToplevels ();
+            t.OnSizeChanging (new SizeChangedEventArgs (args.Size));
+        }
+
+        Refresh ();
+
+        return true;
+    }
+
+    #endregion Toplevel handling
+
+    #region Mouse handling
+
+    /// <summary>Disable or enable the mouse. The mouse is enabled by default.</summary>
+    [SerializableConfigurationProperty (Scope = typeof (SettingsScope))]
+    public static bool IsMouseDisabled { get; set; }
+
+    /// <summary>The current <see cref="View"/> object that wants continuous mouse button pressed events.</summary>
+    public static View WantContinuousButtonPressedView { get; private set; }
+
+    /// <summary>
+    ///     Gets the view that grabbed the mouse (e.g. for dragging). When this is set, all mouse events will be routed to
+    ///     this view until the view calls <see cref="UngrabMouse"/> or the mouse is released.
+    /// </summary>
+    public static View MouseGrabView { get; private set; }
+
+    /// <summary>Invoked when a view wants to grab the mouse; can be canceled.</summary>
+    public static event EventHandler<GrabMouseEventArgs> GrabbingMouse;
+
+    /// <summary>Invoked when a view wants un-grab the mouse; can be canceled.</summary>
+    public static event EventHandler<GrabMouseEventArgs> UnGrabbingMouse;
+
+    /// <summary>Invoked after a view has grabbed the mouse.</summary>
+    public static event EventHandler<ViewEventArgs> GrabbedMouse;
+
+    /// <summary>Invoked after a view has un-grabbed the mouse.</summary>
+    public static event EventHandler<ViewEventArgs> UnGrabbedMouse;
+
+    /// <summary>
+    ///     Grabs the mouse, forcing all mouse events to be routed to the specified view until <see cref="UngrabMouse"/>
+    ///     is called.
+    /// </summary>
+    /// <param name="view">View that will receive all mouse events until <see cref="UngrabMouse"/> is invoked.</param>
+    public static void GrabMouse (View view)
+    {
+        if (view is null)
+        {
+            return;
+        }
+
+        if (!OnGrabbingMouse (view))
+        {
+            OnGrabbedMouse (view);
+            MouseGrabView = view;
+        }
+    }
+
+    /// <summary>Releases the mouse grab, so mouse events will be routed to the view on which the mouse is.</summary>
+    public static void UngrabMouse ()
+    {
+        if (MouseGrabView is null)
+        {
+            return;
+        }
+
+        if (!OnUnGrabbingMouse (MouseGrabView))
+        {
+            OnUnGrabbedMouse (MouseGrabView);
+            MouseGrabView = null;
+        }
+    }
+
+    private static bool OnGrabbingMouse (View view)
+    {
+        if (view is null)
+        {
+            return false;
+        }
+
+        var evArgs = new GrabMouseEventArgs (view);
+        GrabbingMouse?.Invoke (view, evArgs);
+
+        return evArgs.Cancel;
+    }
+
+    private static bool OnUnGrabbingMouse (View view)
+    {
+        if (view is null)
+        {
+            return false;
+        }
+
+        var evArgs = new GrabMouseEventArgs (view);
+        UnGrabbingMouse?.Invoke (view, evArgs);
+
+        return evArgs.Cancel;
+    }
+
+    private static void OnGrabbedMouse (View view)
+    {
+        if (view is null)
+        {
+            return;
+        }
+
+        GrabbedMouse?.Invoke (view, new ViewEventArgs (view));
+    }
+
+    private static void OnUnGrabbedMouse (View view)
+    {
+        if (view is null)
+        {
+            return;
+        }
+
+        UnGrabbedMouse?.Invoke (view, new ViewEventArgs (view));
+    }
+
+    #nullable enable
+    // Used by OnMouseEvent to track the last view that was clicked on.
+    internal static View? _mouseEnteredView;
+
+    /// <summary>Event fired when a mouse move or click occurs. Coordinates are screen relative.</summary>
+    /// <remarks>
+    ///     <para>
+    ///         Use this event to receive mouse events in screen coordinates. Use <see cref="Responder.MouseEvent"/> to
+    ///         receive mouse events relative to a <see cref="View"/>'s bounds.
+    ///     </para>
+    ///     <para>The <see cref="MouseEvent.View"/> will contain the <see cref="View"/> that contains the mouse coordinates.</para>
+    /// </remarks>
+    public static event EventHandler<MouseEventEventArgs> MouseEvent;
+
+    /// <summary>Called when a mouse event occurs. Raises the <see cref="MouseEvent"/> event.</summary>
+    /// <remarks>This method can be used to simulate a mouse event, e.g. in unit tests.</remarks>
+    /// <param name="a">The mouse event with coordinates relative to the screen.</param>
+    internal static void OnMouseEvent (MouseEventEventArgs a)
+    {
+        if (IsMouseDisabled)
+        {
+            return;
+        }
+
+        // TODO: In PR #3273, FindDeepestView will return adornments. Update logic below to fix adornment mouse handling
+        var view = View.FindDeepestView (Current, a.MouseEvent.X, a.MouseEvent.Y);
+
+        if (view is { WantContinuousButtonPressed: true })
+        {
+            WantContinuousButtonPressedView = view;
+        }
+        else
+        {
+            WantContinuousButtonPressedView = null;
+        }
+
+        if (view is { })
+        {
+            a.MouseEvent.View = view;
+        }
+
+        MouseEvent?.Invoke (null, new MouseEventEventArgs (a.MouseEvent));
+
+        if (a.MouseEvent.Handled)
+        {
+            return;
+        }
+
+        if (MouseGrabView is { })
+        {
+            // If the mouse is grabbed, send the event to the view that grabbed it.
+            // The coordinates are relative to the Bounds of the view that grabbed the mouse.
+            Point newxy = MouseGrabView.ScreenToFrame (a.MouseEvent.X, a.MouseEvent.Y);
+
+            var nme = new MouseEvent
+            {
+                X = newxy.X,
+                Y = newxy.Y,
+                Flags = a.MouseEvent.Flags,
+                OfX = a.MouseEvent.X - newxy.X,
+                OfY = a.MouseEvent.Y - newxy.Y,
+                View = view
+            };
+
+            if (MouseGrabView.Bounds.Contains (nme.X, nme.Y) is false)
+            {
+                // The mouse has moved outside the bounds of the view that
+                // grabbed the mouse, so we tell the view that last got 
+                // OnMouseEnter the mouse is leaving
+                // BUGBUG: That sentence makes no sense. Either I'm missing something
+                // or this logic is flawed.
+                _mouseEnteredView?.OnMouseLeave (a.MouseEvent);
+            }
+
+            //System.Diagnostics.Debug.WriteLine ($"{nme.Flags};{nme.X};{nme.Y};{mouseGrabView}");
+            if (MouseGrabView?.OnMouseEvent (nme) == true)
+            {
+                return;
+            }
+        }
+
+        if ((view is null || view == OverlappedTop)
+            && Current is { Modal: false }
+            && OverlappedTop != null
+            && a.MouseEvent.Flags != MouseFlags.ReportMousePosition
+            && a.MouseEvent.Flags != 0)
+        {
+            View? top = FindDeepestTop (Top, a.MouseEvent.X, a.MouseEvent.Y);
+            view = View.FindDeepestView (top, a.MouseEvent.X, a.MouseEvent.Y);
+
+            if (view is { } && view != OverlappedTop && top != Current)
+            {
+                MoveCurrent ((Toplevel)top);
+            }
+        }
+
+        if (view is null)
+        {
+            return;
+        }
+
+        var screen = view.FrameToScreen ();
+
+        // Work inside-out (Padding, Border, Margin)
+        // TODO: Debate whether inside-out or outside-in is the right strategy
+        if (AdornmentHandledMouseEvent (view.Padding, a))
+        {
+            return;
+        }
+
+        if (AdornmentHandledMouseEvent (view.Border, a))
+        {
+            if (view is not Toplevel)
+            {
+                return;
+            }
+
+            // TODO: This is a temporary hack to work around the fact that 
+            // drag handling is handled in Toplevel (See Issue #2537)
+
+            var me = new MouseEvent
+            {
+                X = a.MouseEvent.X - screen.X,
+                Y = a.MouseEvent.Y - screen.Y,
+                Flags = a.MouseEvent.Flags,
+                OfX = a.MouseEvent.X - screen.X,
+                OfY = a.MouseEvent.Y - screen.Y,
+                View = view
+            };
+
+            if (_mouseEnteredView is null)
+            {
+                _mouseEnteredView = view;
+                view.OnMouseEnter (me);
+            }
+            else if (_mouseEnteredView != view)
+            {
+                _mouseEnteredView.OnMouseLeave (me);
+                view.OnMouseEnter (me);
+                _mouseEnteredView = view;
+            }
+
+            if (!view.WantMousePositionReports && a.MouseEvent.Flags == MouseFlags.ReportMousePosition)
+            {
+                return;
+            }
+
+            WantContinuousButtonPressedView = view.WantContinuousButtonPressed ? view : null;
+
+            if (view.OnMouseEvent (me))
+            {
+                // Should we bubble up the event, if it is not handled?
+                //return;
+            }
+
+            BringOverlappedTopToFront ();
+
+            return;
+        }
+
+        if (AdornmentHandledMouseEvent (view?.Margin, a))
+        {
+            return;
+        }
+
+        Rectangle bounds = view.BoundsToScreen (view.Bounds);
+
+        if (bounds.Contains (a.MouseEvent.X, a.MouseEvent.Y))
+        {
+            Point boundsPoint = view.ScreenToBounds (a.MouseEvent.X, a.MouseEvent.Y);
+
+            var me = new MouseEvent
+            {
+                X = boundsPoint.X,
+                Y = boundsPoint.Y,
+                Flags = a.MouseEvent.Flags,
+                OfX = boundsPoint.X,
+                OfY = boundsPoint.Y,
+                View = view
+            };
+
+            if (_mouseEnteredView is null)
+            {
+                _mouseEnteredView = view;
+                view.OnMouseEnter (me);
+            }
+            else if (_mouseEnteredView != view)
+            {
+                _mouseEnteredView.OnMouseLeave (me);
+                view.OnMouseEnter (me);
+                _mouseEnteredView = view;
+            }
+
+            if (!view.WantMousePositionReports && a.MouseEvent.Flags == MouseFlags.ReportMousePosition)
+            {
+                return;
+            }
+
+            WantContinuousButtonPressedView = view.WantContinuousButtonPressed ? view : null;
+
+            if (view.OnMouseEvent (me))
+            {
+                // Should we bubble up the event, if it is not handled?
+                //return;
+            }
+
+            BringOverlappedTopToFront ();
+        }
+
+        return;
+
+        static bool AdornmentHandledMouseEvent (Adornment? frame, MouseEventEventArgs args)
+        {
+            if (frame?.Thickness.Contains (frame.FrameToScreen (), args.MouseEvent.X, args.MouseEvent.Y) is not true)
+            {
+                return false;
+            }
+
+            Point boundsPoint = frame.ScreenToBounds (args.MouseEvent.X, args.MouseEvent.Y);
+
+            var me = new MouseEvent
+            {
+                X = boundsPoint.X,
+                Y = boundsPoint.Y,
+                Flags = args.MouseEvent.Flags,
+                OfX = boundsPoint.X,
+                OfY = boundsPoint.Y,
+                View = frame
+            };
+            frame.OnMouseEvent (me);
+
+            return true;
+        }
+    }
+    #nullable restore
+
+    #endregion Mouse handling
+
+    #region Keyboard handling
+
+    private static Key _alternateForwardKey = Key.Empty; // Defined in config.json
+
+    /// <summary>Alternative key to navigate forwards through views. Ctrl+Tab is the primary key.</summary>
+    [SerializableConfigurationProperty (Scope = typeof (SettingsScope))]
+    [JsonConverter (typeof (KeyJsonConverter))]
+    public static Key AlternateForwardKey
+    {
+        get => _alternateForwardKey;
+        set
+        {
+            if (_alternateForwardKey != value)
+            {
+                Key oldKey = _alternateForwardKey;
+                _alternateForwardKey = value;
+                OnAlternateForwardKeyChanged (new KeyChangedEventArgs (oldKey, value));
+            }
+        }
+    }
+
+    private static void OnAlternateForwardKeyChanged (KeyChangedEventArgs e)
+    {
+        foreach (Toplevel top in _topLevels.ToArray ())
+        {
+            top.OnAlternateForwardKeyChanged (e);
+        }
+    }
+
+    private static Key _alternateBackwardKey = Key.Empty; // Defined in config.json
+
+    /// <summary>Alternative key to navigate backwards through views. Shift+Ctrl+Tab is the primary key.</summary>
+    [SerializableConfigurationProperty (Scope = typeof (SettingsScope))]
+    [JsonConverter (typeof (KeyJsonConverter))]
+    public static Key AlternateBackwardKey
+    {
+        get => _alternateBackwardKey;
+        set
+        {
+            if (_alternateBackwardKey != value)
+            {
+                Key oldKey = _alternateBackwardKey;
+                _alternateBackwardKey = value;
+                OnAlternateBackwardKeyChanged (new KeyChangedEventArgs (oldKey, value));
+            }
+        }
+    }
+
+    private static void OnAlternateBackwardKeyChanged (KeyChangedEventArgs oldKey)
+    {
+        foreach (Toplevel top in _topLevels.ToArray ())
+        {
+            top.OnAlternateBackwardKeyChanged (oldKey);
+        }
+    }
+
+    private static Key _quitKey = Key.Empty; // Defined in config.json
+
+    /// <summary>Gets or sets the key to quit the application.</summary>
+    [SerializableConfigurationProperty (Scope = typeof (SettingsScope))]
+    [JsonConverter (typeof (KeyJsonConverter))]
+    public static Key QuitKey
+    {
+        get => _quitKey;
+        set
+        {
+            if (_quitKey != value)
+            {
+                Key oldKey = _quitKey;
+                _quitKey = value;
+                OnQuitKeyChanged (new KeyChangedEventArgs (oldKey, value));
+            }
+        }
+    }
+
+    private static void OnQuitKeyChanged (KeyChangedEventArgs e)
+    {
+        // Duplicate the list so if it changes during enumeration we're safe
+        foreach (Toplevel top in _topLevels.ToArray ())
+        {
+            top.OnQuitKeyChanged (e);
+        }
+    }
+
+    /// <summary>
+    ///     Event fired when the user presses a key. Fired by <see cref="OnKeyDown"/>.
+    ///     <para>
+    ///         Set <see cref="Key.Handled"/> to <see langword="true"/> to indicate the key was handled and to prevent
+    ///         additional processing.
+    ///     </para>
+    /// </summary>
+    /// <remarks>
+    ///     All drivers support firing the <see cref="KeyDown"/> event. Some drivers (Curses) do not support firing the
+    ///     <see cref="KeyDown"/> and <see cref="KeyUp"/> events.
+    ///     <para>Fired after <see cref="KeyDown"/> and before <see cref="KeyUp"/>.</para>
+    /// </remarks>
+    public static event EventHandler<Key> KeyDown;
+
+    /// <summary>
+    ///     Called by the <see cref="ConsoleDriver"/> when the user presses a key. Fires the <see cref="KeyDown"/> event
+    ///     then calls <see cref="View.NewKeyDownEvent"/> on all top level views. Called after <see cref="OnKeyDown"/> and
+    ///     before <see cref="OnKeyUp"/>.
+    /// </summary>
+    /// <remarks>Can be used to simulate key press events.</remarks>
+    /// <param name="keyEvent"></param>
+    /// <returns><see langword="true"/> if the key was handled.</returns>
+    public static bool OnKeyDown (Key keyEvent)
+    {
+        if (!_initialized)
+        {
+            return true;
+        }
+
+        KeyDown?.Invoke (null, keyEvent);
+
+        if (keyEvent.Handled)
+        {
+            return true;
+        }
+
+        foreach (Toplevel topLevel in _topLevels.ToList ())
+        {
+            if (topLevel.NewKeyDownEvent (keyEvent))
+            {
+                return true;
+            }
+
+            if (topLevel.Modal)
+            {
+                break;
+            }
+        }
+
+        // Invoke any Global KeyBindings
+        foreach (Toplevel topLevel in _topLevels.ToList ())
+        {
+            foreach (View view in topLevel.Subviews.Where (
+                                                           v => v.KeyBindings.TryGet (
+                                                                                      keyEvent,
+                                                                                      KeyBindingScope.Application,
+                                                                                      out KeyBinding _
+                                                                                     )
+                                                          ))
+            {
+                if (view.KeyBindings.TryGet (keyEvent.KeyCode, KeyBindingScope.Application, out KeyBinding _))
+                {
+                    bool? handled = view.OnInvokingKeyBindings (keyEvent);
+
+                    if (handled is { } && (bool)handled)
+                    {
+                        return true;
+                    }
+                }
+            }
+        }
+
+        return false;
+    }
+
+    /// <summary>
+    ///     Event fired when the user releases a key. Fired by <see cref="OnKeyUp"/>.
+    ///     <para>
+    ///         Set <see cref="Key.Handled"/> to <see langword="true"/> to indicate the key was handled and to prevent
+    ///         additional processing.
+    ///     </para>
+    /// </summary>
+    /// <remarks>
+    ///     All drivers support firing the <see cref="KeyDown"/> event. Some drivers (Curses) do not support firing the
+    ///     <see cref="KeyDown"/> and <see cref="KeyUp"/> events.
+    ///     <para>Fired after <see cref="KeyDown"/>.</para>
+    /// </remarks>
+    public static event EventHandler<Key> KeyUp;
+
+    /// <summary>
+    ///     Called by the <see cref="ConsoleDriver"/> when the user releases a key. Fires the <see cref="KeyUp"/> event
+    ///     then calls <see cref="View.NewKeyUpEvent"/> on all top level views. Called after <see cref="OnKeyDown"/>.
+    /// </summary>
+    /// <remarks>Can be used to simulate key press events.</remarks>
+    /// <param name="a"></param>
+    /// <returns><see langword="true"/> if the key was handled.</returns>
+    public static bool OnKeyUp (Key a)
+    {
+        if (!_initialized)
+        {
+            return true;
+        }
+
+        KeyUp?.Invoke (null, a);
+
+        if (a.Handled)
+        {
+            return true;
+        }
+
+        foreach (Toplevel topLevel in _topLevels.ToList ())
+        {
+            if (topLevel.NewKeyUpEvent (a))
+            {
+                return true;
+            }
+
+            if (topLevel.Modal)
+            {
+                break;
+            }
+        }
+
+        return false;
+    }
+
+    #endregion Keyboard handling
 }

+ 201 - 165
Terminal.Gui/Clipboard/Clipboard.cs

@@ -1,174 +1,210 @@
-using System.Text;
-using System;
-using System.Diagnostics;
-using System.Threading.Tasks;
+using System.Diagnostics;
 
 namespace Terminal.Gui;
 
-/// <summary>
-/// Provides cut, copy, and paste support for the OS clipboard.
-/// </summary>
+/// <summary>Provides cut, copy, and paste support for the OS clipboard.</summary>
 /// <remarks>
-/// <para>
-/// On Windows, the <see cref="Clipboard"/> class uses the Windows Clipboard APIs via P/Invoke.
-/// </para>
-/// <para>
-/// On Linux, when not running under Windows Subsystem for Linux (WSL),
-/// the <see cref="Clipboard"/> class uses the xclip command line tool. If xclip is not installed,
-/// the clipboard will not work.
-/// </para>
-/// <para>
-/// On Linux, when running under Windows Subsystem for Linux (WSL),
-/// the <see cref="Clipboard"/> class launches Windows' powershell.exe via WSL interop and uses the
-/// "Set-Clipboard" and "Get-Clipboard" Powershell CmdLets. 
-/// </para>
-/// <para>
-/// On the Mac, the <see cref="Clipboard"/> class uses the MacO OS X pbcopy and pbpaste command line tools
-/// and the Mac clipboard APIs vai P/Invoke.
-/// </para>
+///     <para>On Windows, the <see cref="Clipboard"/> class uses the Windows Clipboard APIs via P/Invoke.</para>
+///     <para>
+///         On Linux, when not running under Windows Subsystem for Linux (WSL), the <see cref="Clipboard"/> class uses
+///         the xclip command line tool. If xclip is not installed, the clipboard will not work.
+///     </para>
+///     <para>
+///         On Linux, when running under Windows Subsystem for Linux (WSL), the <see cref="Clipboard"/> class launches
+///         Windows' powershell.exe via WSL interop and uses the "Set-Clipboard" and "Get-Clipboard" Powershell CmdLets.
+///     </para>
+///     <para>
+///         On the Mac, the <see cref="Clipboard"/> class uses the MacO OS X pbcopy and pbpaste command line tools and
+///         the Mac clipboard APIs vai P/Invoke.
+///     </para>
 /// </remarks>
-public static class Clipboard {
-	static string _contents = string.Empty;
-
-		/// <summary>
-		/// Gets (copies from) or sets (pastes to) the contents of the OS clipboard.
-		/// </summary>
-		public static string Contents {
-			get {
-				try {
-					if (IsSupported) {
-						var clipData = Application.Driver.Clipboard.GetClipboardData ();
-						if (clipData == null) {
-							// throw new InvalidOperationException ($"{Application.Driver.GetType ().Name}.GetClipboardData returned null instead of string.Empty");
-							clipData = string.Empty;
-						}
-						_contents = clipData;
-					}
-				} catch (Exception) {
-					_contents = string.Empty;
-				}
-				return _contents;
-			}
-			set {
-				try {
-					if (IsSupported) {
-						if (value == null) {
-							value = string.Empty;
-						}
-						Application.Driver.Clipboard.SetClipboardData (value);
-					}
-					_contents = value;
-				} catch (Exception) {
-					_contents = value;
-				}
-			}
-		}
-
-	/// <summary>
-	/// Returns true if the environmental dependencies are in place to interact with the OS clipboard.
-	/// </summary>
-	/// <remarks>
-	/// </remarks>
-	public static bool IsSupported { get => Application.Driver.Clipboard.IsSupported; }
-
-	/// <summary>
-	/// Copies the _contents of the OS clipboard to <paramref name="result"/> if possible.
-	/// </summary>
-	/// <param name="result">The _contents of the OS clipboard if successful, <see cref="string.Empty"/> if not.</param>
-	/// <returns><see langword="true"/> the OS clipboard was retrieved, <see langword="false"/> otherwise.</returns>
-	public static bool TryGetClipboardData (out string result)
-	{
-		if (IsSupported && Application.Driver.Clipboard.TryGetClipboardData (out result)) {
-			if (_contents != result) {
-				_contents = result;
-			}
-			return true;
-		}
-		result = string.Empty;
-		return false;
-	}
-
-	/// <summary>
-	/// Pastes the <paramref name="text"/> to the OS clipboard if possible.
-	/// </summary>
-	/// <param name="text">The text to paste to the OS clipboard.</param>
-	/// <returns><see langword="true"/> the OS clipboard was set, <see langword="false"/> otherwise.</returns>
-	public static bool TrySetClipboardData (string text)
-	{
-		if (IsSupported && Application.Driver.Clipboard.TrySetClipboardData (text)) {
-			_contents = text;
-			return true;
-		}
-		return false;
-	}
+public static class Clipboard
+{
+    private static string _contents = string.Empty;
+
+    /// <summary>Gets (copies from) or sets (pastes to) the contents of the OS clipboard.</summary>
+    public static string Contents
+    {
+        get
+        {
+            try
+            {
+                if (IsSupported)
+                {
+                    string clipData = Application.Driver.Clipboard.GetClipboardData ();
+
+                    if (clipData is null)
+                    {
+                        // throw new InvalidOperationException ($"{Application.Driver.GetType ().Name}.GetClipboardData returned null instead of string.Empty");
+                        clipData = string.Empty;
+                    }
+
+                    _contents = clipData;
+                }
+            }
+            catch (Exception)
+            {
+                _contents = string.Empty;
+            }
+
+            return _contents;
+        }
+        set
+        {
+            try
+            {
+                if (IsSupported)
+                {
+                    if (value is null)
+                    {
+                        value = string.Empty;
+                    }
+
+                    Application.Driver.Clipboard.SetClipboardData (value);
+                }
+
+                _contents = value;
+            }
+            catch (Exception)
+            {
+                _contents = value;
+            }
+        }
+    }
+
+    /// <summary>Returns true if the environmental dependencies are in place to interact with the OS clipboard.</summary>
+    /// <remarks></remarks>
+    public static bool IsSupported => Application.Driver.Clipboard.IsSupported;
+
+    /// <summary>Copies the _contents of the OS clipboard to <paramref name="result"/> if possible.</summary>
+    /// <param name="result">The _contents of the OS clipboard if successful, <see cref="string.Empty"/> if not.</param>
+    /// <returns><see langword="true"/> the OS clipboard was retrieved, <see langword="false"/> otherwise.</returns>
+    public static bool TryGetClipboardData (out string result)
+    {
+        if (IsSupported && Application.Driver.Clipboard.TryGetClipboardData (out result))
+        {
+            if (_contents != result)
+            {
+                _contents = result;
+            }
+
+            return true;
+        }
+
+        result = string.Empty;
+
+        return false;
+    }
+
+    /// <summary>Pastes the <paramref name="text"/> to the OS clipboard if possible.</summary>
+    /// <param name="text">The text to paste to the OS clipboard.</param>
+    /// <returns><see langword="true"/> the OS clipboard was set, <see langword="false"/> otherwise.</returns>
+    public static bool TrySetClipboardData (string text)
+    {
+        if (IsSupported && Application.Driver.Clipboard.TrySetClipboardData (text))
+        {
+            _contents = text;
+
+            return true;
+        }
+
+        return false;
+    }
 }
 
 /// <summary>
-/// Helper class for console drivers to invoke shell commands to interact with the clipboard.
-/// Used primarily by CursesDriver, but also used in Unit tests which is why it is in
-/// ConsoleDriver.cs.
+///     Helper class for console drivers to invoke shell commands to interact with the clipboard. Used primarily by
+///     CursesDriver, but also used in Unit tests which is why it is in ConsoleDriver.cs.
 /// </summary>
-internal static class ClipboardProcessRunner {
-	public static (int exitCode, string result) Bash (string commandLine, string inputText = "", bool waitForOutput = false)
-	{
-		var arguments = $"-c \"{commandLine}\"";
-		var (exitCode, result) = Process ("bash", arguments, inputText, waitForOutput);
-
-		return (exitCode, result.TrimEnd ());
-	}
-
-	public static (int exitCode, string result) Process (string cmd, string arguments, string input = null, bool waitForOutput = true)
-	{
-		var output = string.Empty;
-
-		using (Process process = new Process {
-			StartInfo = new ProcessStartInfo {
-				FileName = cmd,
-				Arguments = arguments,
-				RedirectStandardOutput = true,
-				RedirectStandardError = true,
-				RedirectStandardInput = true,
-				UseShellExecute = false,
-				CreateNoWindow = true,
-			}
-		}) {
-			var eventHandled = new TaskCompletionSource<bool> ();
-			process.Start ();
-			if (!string.IsNullOrEmpty (input)) {
-				process.StandardInput.Write (input);
-				process.StandardInput.Close ();
-			}
-
-			if (!process.WaitForExit (5000)) {
-				var timeoutError = $@"Process timed out. Command line: {process.StartInfo.FileName} {process.StartInfo.Arguments}.";
-				throw new TimeoutException (timeoutError);
-			}
-
-			if (waitForOutput && process.StandardOutput.Peek () != -1) {
-				output = process.StandardOutput.ReadToEnd ();
-			}
-
-			if (process.ExitCode > 0) {
-				output = $@"Process failed to run. Command line: {cmd} {arguments}.
-										Output: {output}
-										Error: {process.StandardError.ReadToEnd ()}";
-			}
-
-			return (process.ExitCode, output);
-		}
-	}
-
-	public static bool DoubleWaitForExit (this System.Diagnostics.Process process)
-	{
-		var result = process.WaitForExit (500);
-		if (result) {
-			process.WaitForExit ();
-		}
-		return result;
-	}
-
-	public static bool FileExists (this string value)
-	{
-		return !string.IsNullOrEmpty (value) && !value.Contains ("not found");
-	}
-}
+internal static class ClipboardProcessRunner
+{
+    public static (int exitCode, string result) Bash (
+        string commandLine,
+        string inputText = "",
+        bool waitForOutput = false
+    )
+    {
+        var arguments = $"-c \"{commandLine}\"";
+        (int exitCode, string result) = Process ("bash", arguments, inputText, waitForOutput);
+
+        return (exitCode, result.TrimEnd ());
+    }
+
+    public static bool DoubleWaitForExit (this Process process)
+    {
+        bool result = process.WaitForExit (500);
+
+        if (result)
+        {
+            process.WaitForExit ();
+        }
+
+        return result;
+    }
+
+    public static bool FileExists (this string value) { return !string.IsNullOrEmpty (value) && !value.Contains ("not found"); }
+
+    public static (int exitCode, string result) Process (
+        string cmd,
+        string arguments,
+        string input = null,
+        bool waitForOutput = true
+    )
+    {
+        var output = string.Empty;
+
+        using (var process = new Process
+               {
+                   StartInfo = new ProcessStartInfo
+                   {
+                       FileName = cmd,
+                       Arguments = arguments,
+                       RedirectStandardOutput = true,
+                       RedirectStandardError = true,
+                       RedirectStandardInput = true,
+                       UseShellExecute = false,
+                       CreateNoWindow = true
+                   }
+               })
+        {
+            TaskCompletionSource<bool> eventHandled = new ();
+            process.Start ();
+
+            if (!string.IsNullOrEmpty (input))
+            {
+                process.StandardInput.Write (input);
+                process.StandardInput.Close ();
+            }
+
+            if (!process.WaitForExit (5000))
+            {
+                var timeoutError =
+                    $@"Process timed out. Command line: {process.StartInfo.FileName} {process.StartInfo.Arguments}.";
+
+                throw new TimeoutException (timeoutError);
+            }
+
+            if (waitForOutput && process.StandardOutput.Peek () != -1)
+            {
+                output = process.StandardOutput.ReadToEnd ();
+            }
+
+            if (process.ExitCode > 0)
+            {
+                output = $@"Process failed to run. Command line: {
+                    cmd
+                } {
+                    arguments
+                }.
+										Output: {
+                                            output
+                                        }
+										Error: {
+                                            process.StandardError.ReadToEnd ()
+                                        }";
+            }
+
+            return (process.ExitCode, output);
+        }
+    }
+}

+ 119 - 109
Terminal.Gui/Clipboard/ClipboardBase.cs

@@ -1,110 +1,120 @@
-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>
-		/// Returns the contents of the OS clipboard if possible.
-		/// </summary>
-		/// <returns>The contents of the OS clipboard if successful.</returns>
-		/// <exception cref="NotSupportedException">Thrown if it was not possible to copy from the OS clipboard.</exception>
-		public string GetClipboardData ()
-		{
-			try {
-				var result = GetClipboardDataImpl ();
-				if (result == null) {
-					return string.Empty;
-				}
-				return GetClipboardDataImpl ();
-			} catch (NotSupportedException ex) {
-				throw new NotSupportedException ("Failed to copy from the OS clipboard.", ex);
-			}
-		}
-
-		/// <summary>
-		/// Returns the contents of the OS clipboard if possible. Implemented by <see cref="ConsoleDriver"/>-specific subclasses.
-		/// </summary>
-		/// <returns>The contents of the OS clipboard if successful.</returns>
-		/// <exception cref="NotSupportedException">Thrown if it was not possible to copy from the OS clipboard.</exception>
-		protected abstract string GetClipboardDataImpl ();
-
-		/// <summary>
-		/// Pastes the <paramref name="text"/> to the OS clipboard if possible.
-		/// </summary>
-		/// <param name="text">The text to paste to the OS clipboard.</param>
-		/// <exception cref="NotSupportedException">Thrown if it was not possible to paste to the OS clipboard.</exception>
-		public void SetClipboardData (string text)
-		{
-			if (text == null) {
-				throw new ArgumentNullException (nameof (text));
-			}
-
-			try {
-				SetClipboardDataImpl (text);
-			} catch (NotSupportedException ex) {
-				throw new NotSupportedException ("Failed to paste to the OS clipboard.", ex);
-			}
-		}
-
-		/// <summary>
-		/// Pastes the <paramref name="text"/> to the OS clipboard if possible. Implemented by <see cref="ConsoleDriver"/>-specific subclasses.
-		/// </summary>
-		/// <param name="text">The text to paste to the OS clipboard.</param>
-		/// <exception cref="NotSupportedException">Thrown if it was not possible to paste to the OS clipboard.</exception>
-		protected abstract void SetClipboardDataImpl (string text);
-
-		/// <summary>
-		/// Copies the contents of the OS clipboard to <paramref name="result"/> if possible.
-		/// </summary>
-		/// <param name="result">The contents of the OS clipboard if successful.</param>
-		/// <returns><see langword="true"/> the OS clipboard was retrieved, <see langword="false"/> otherwise.</returns>
-		public bool TryGetClipboardData (out string result)
-		{
-			result = string.Empty;
-			// Don't even try to read because environment is not set up.
-			if (!IsSupported) {
-				return false;
-			}
-
-			try {
-				result = GetClipboardDataImpl ();
-				return true;
-			} catch (NotSupportedException ex) {
-				System.Diagnostics.Debug.WriteLine ($"TryGetClipboardData: {ex.Message}");
-				return false;
-			}
-		}
-
-		/// <summary>
-		/// Pastes the <paramref name="text"/> to the OS clipboard if possible.
-		/// </summary>
-		/// <param name="text">The text to paste to the OS clipboard.</param>
-		/// <returns><see langword="true"/> the OS clipboard was set, <see langword="false"/> otherwise.</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 (NotSupportedException ex) {
-				System.Diagnostics.Debug.WriteLine ($"TrySetClipboardData: {ex.Message}");
-				return false;
-			}
-		}
-	}
+using System.Diagnostics;
+
+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>Returns the contents of the OS clipboard if possible.</summary>
+    /// <returns>The contents of the OS clipboard if successful.</returns>
+    /// <exception cref="NotSupportedException">Thrown if it was not possible to copy from the OS clipboard.</exception>
+    public string GetClipboardData ()
+    {
+        try
+        {
+            string result = GetClipboardDataImpl ();
+
+            if (result is null)
+            {
+                return string.Empty;
+            }
+
+            return GetClipboardDataImpl ();
+        }
+        catch (NotSupportedException ex)
+        {
+            throw new NotSupportedException ("Failed to copy from the OS clipboard.", ex);
+        }
+    }
+
+    /// <summary>Pastes the <paramref name="text"/> to the OS clipboard if possible.</summary>
+    /// <param name="text">The text to paste to the OS clipboard.</param>
+    /// <exception cref="NotSupportedException">Thrown if it was not possible to paste to the OS clipboard.</exception>
+    public void SetClipboardData (string text)
+    {
+        if (text is null)
+        {
+            throw new ArgumentNullException (nameof (text));
+        }
+
+        try
+        {
+            SetClipboardDataImpl (text);
+        }
+        catch (NotSupportedException ex)
+        {
+            throw new NotSupportedException ("Failed to paste to the OS clipboard.", ex);
+        }
+    }
+
+    /// <summary>Copies the contents of the OS clipboard to <paramref name="result"/> if possible.</summary>
+    /// <param name="result">The contents of the OS clipboard if successful.</param>
+    /// <returns><see langword="true"/> the OS clipboard was retrieved, <see langword="false"/> otherwise.</returns>
+    public bool TryGetClipboardData (out string result)
+    {
+        result = string.Empty;
+
+        // Don't even try to read because environment is not set up.
+        if (!IsSupported)
+        {
+            return false;
+        }
+
+        try
+        {
+            result = GetClipboardDataImpl ();
+
+            return true;
+        }
+        catch (NotSupportedException ex)
+        {
+            Debug.WriteLine ($"TryGetClipboardData: {ex.Message}");
+
+            return false;
+        }
+    }
+
+    /// <summary>Pastes the <paramref name="text"/> to the OS clipboard if possible.</summary>
+    /// <param name="text">The text to paste to the OS clipboard.</param>
+    /// <returns><see langword="true"/> the OS clipboard was set, <see langword="false"/> otherwise.</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 (NotSupportedException ex)
+        {
+            Debug.WriteLine ($"TrySetClipboardData: {ex.Message}");
+
+            return false;
+        }
+    }
+
+    /// <summary>
+    ///     Returns the contents of the OS clipboard if possible. Implemented by <see cref="ConsoleDriver"/>-specific
+    ///     subclasses.
+    /// </summary>
+    /// <returns>The contents of the OS clipboard if successful.</returns>
+    /// <exception cref="NotSupportedException">Thrown if it was not possible to copy from the OS clipboard.</exception>
+    protected abstract string GetClipboardDataImpl ();
+
+    /// <summary>
+    ///     Pastes the <paramref name="text"/> to the OS clipboard if possible. Implemented by <see cref="ConsoleDriver"/>
+    ///     -specific subclasses.
+    /// </summary>
+    /// <param name="text">The text to paste to the OS clipboard.</param>
+    /// <exception cref="NotSupportedException">Thrown if it was not possible to paste to the OS clipboard.</exception>
+    protected abstract void SetClipboardDataImpl (string text);
 }

+ 21 - 34
Terminal.Gui/Clipboard/IClipboard.cs

@@ -1,40 +1,27 @@
-using System;
+namespace Terminal.Gui;
 
-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>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>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.
-		/// </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>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 if possible.
-		/// </summary>
-		/// <param name="text"></param>
-		/// <returns>True if the clipboard content was set successfully.</returns>
-		bool TrySetClipboardData (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);
 }

+ 24 - 37
Terminal.Gui/Configuration/AppScope.cs

@@ -1,42 +1,29 @@
-using System;
-using System.Collections;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.Linq;
-using System.Reflection;
+#nullable enable
 using System.Text.Json.Serialization;
-using static Terminal.Gui.ConfigurationManager;
 
-#nullable enable
+namespace Terminal.Gui;
 
-namespace Terminal.Gui; 
-
-/// <summary>
-/// The <see cref="Scope{T}"/> class for application-defined configuration settings.
-/// </summary>
-/// <remarks>
-/// </remarks>
+/// <summary>The <see cref="Scope{T}"/> class for application-defined configuration settings.</summary>
+/// <remarks></remarks>
 /// <example>
-/// <para>
-/// Use the <see cref="SerializableConfigurationProperty"/> attribute to mark properties that should be serialized as part
-/// of application-defined configuration settings.
-/// </para>
-/// <code>
-/// public class MyAppSettings {
-///	[SerializableConfigurationProperty (Scope = typeof (AppScope))]
-///	public static bool? MyProperty { get; set; } = true;
-/// }
-/// </code>
-/// <para>
-/// THe resultant Json will look like this:
-/// </para>
-/// <code>
-///   "AppSettings": {
-///     "MyAppSettings.MyProperty": true,
-///     "UICatalog.ShowStatusBar": true
-///   },
-/// </code>
-/// </example> 
+///     <para>
+///         Use the <see cref="SerializableConfigurationProperty"/> attribute to mark properties that should be
+///         serialized as part of application-defined configuration settings.
+///     </para>
+///     <code>
+///  public class MyAppSettings {
+/// 	[SerializableConfigurationProperty (Scope = typeof (AppScope))]
+/// 	public static bool? MyProperty { get; set; } = true;
+///  }
+///  </code>
+///     <para>THe resultant Json will look like this:</para>
+///     <code>
+///    "AppSettings": {
+///      "MyAppSettings.MyProperty": true,
+///      "UICatalog.ShowStatusBar": true
+///    },
+///  </code>
+/// </example>
 [JsonConverter (typeof (ScopeJsonConverter<AppScope>))]
-public class AppScope : Scope<AppScope> {
-}
+public class AppScope : Scope<AppScope>
+{ }

+ 106 - 95
Terminal.Gui/Configuration/AttributeJsonConverter.cs

@@ -1,97 +1,108 @@
-using System;
-using System.Text.Json;
+using System.Text.Json;
 using System.Text.Json.Serialization;
 
-namespace Terminal.Gui; 
-
-/// <summary>
-/// Json converter fro the <see cref="Attribute"/> class.
-/// </summary>
-class AttributeJsonConverter : JsonConverter<Attribute> {
-	static AttributeJsonConverter _instance;
-
-	/// <summary>
-	/// 
-	/// </summary>
-	public static AttributeJsonConverter Instance {
-		get {
-			if (_instance == null) {
-				_instance = new AttributeJsonConverter ();
-			}
-
-			return _instance;
-		}
-	}
-
-	public override Attribute Read (ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
-	{
-		if (reader.TokenType != JsonTokenType.StartObject) {
-			throw new JsonException ($"Unexpected StartObject token when parsing Attribute: {reader.TokenType}.");
-		}
-
-		var attribute = new Attribute ();
-		Color? foreground = null;
-		Color? background = null;
-		while (reader.Read ()) {
-			if (reader.TokenType == JsonTokenType.EndObject) {
-				if (foreground == null || background == null) {
-					throw new JsonException ("Both Foreground and Background colors must be provided.");
-				}
-				return new Attribute (foreground.Value, background.Value);
-			}
-
-			if (reader.TokenType != JsonTokenType.PropertyName) {
-				throw new JsonException ($"Unexpected token when parsing Attribute: {reader.TokenType}.");
-			}
-
-			var propertyName = reader.GetString ();
-			reader.Read ();
-			var color = $"\"{reader.GetString ()}\"";
-
-			switch (propertyName?.ToLower ()) {
-			case "foreground":
-				foreground = JsonSerializer.Deserialize<Color> (color, options);
-				break;
-			case "background":
-				background = JsonSerializer.Deserialize<Color> (color, options);
-				break;
-			//case "bright":
-			//case "bold":
-			//	attribute.Bright = reader.GetBoolean ();
-			//	break;
-			//case "dim":
-			//	attribute.Dim = reader.GetBoolean ();
-			//	break;
-			//case "underline":
-			//	attribute.Underline = reader.GetBoolean ();
-			//	break;
-			//case "blink":
-			//	attribute.Blink = reader.GetBoolean ();
-			//	break;
-			//case "reverse":
-			//	attribute.Reverse = reader.GetBoolean ();
-			//	break;
-			//case "hidden":
-			//	attribute.Hidden = reader.GetBoolean ();
-			//	break;
-			//case "strike-through":
-			//	attribute.StrikeThrough = reader.GetBoolean ();
-			//	break;				
-			default:
-				throw new JsonException ($"Unknown Attribute property {propertyName}.");
-			}
-		}
-		throw new JsonException ();
-	}
-
-	public override void Write (Utf8JsonWriter writer, Attribute value, JsonSerializerOptions options)
-	{
-		writer.WriteStartObject ();
-		writer.WritePropertyName (nameof (Attribute.Foreground));
-		ColorJsonConverter.Instance.Write (writer, value.Foreground, options);
-		writer.WritePropertyName (nameof (Attribute.Background));
-		ColorJsonConverter.Instance.Write (writer, value.Background, options);
-
-		writer.WriteEndObject ();
-	}
-}
+namespace Terminal.Gui;
+
+/// <summary>Json converter fro the <see cref="Attribute"/> class.</summary>
+internal class AttributeJsonConverter : JsonConverter<Attribute>
+{
+    private static AttributeJsonConverter _instance;
+
+    /// <summary></summary>
+    public static AttributeJsonConverter Instance
+    {
+        get
+        {
+            if (_instance is null)
+            {
+                _instance = new AttributeJsonConverter ();
+            }
+
+            return _instance;
+        }
+    }
+
+    public override Attribute Read (ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+    {
+        if (reader.TokenType != JsonTokenType.StartObject)
+        {
+            throw new JsonException ($"Unexpected StartObject token when parsing Attribute: {reader.TokenType}.");
+        }
+
+        var attribute = new Attribute ();
+        Color? foreground = null;
+        Color? background = null;
+
+        while (reader.Read ())
+        {
+            if (reader.TokenType == JsonTokenType.EndObject)
+            {
+                if (foreground is null || background is null)
+                {
+                    throw new JsonException ("Both Foreground and Background colors must be provided.");
+                }
+
+                return new Attribute (foreground.Value, background.Value);
+            }
+
+            if (reader.TokenType != JsonTokenType.PropertyName)
+            {
+                throw new JsonException ($"Unexpected token when parsing Attribute: {reader.TokenType}.");
+            }
+
+            string propertyName = reader.GetString ();
+            reader.Read ();
+            var color = $"\"{reader.GetString ()}\"";
+
+            switch (propertyName?.ToLower ())
+            {
+                case "foreground":
+                    foreground = JsonSerializer.Deserialize<Color> (color, options);
+
+                    break;
+                case "background":
+                    background = JsonSerializer.Deserialize<Color> (color, options);
+
+                    break;
+
+                //case "bright":
+                //case "bold":
+                //	attribute.Bright = reader.GetBoolean ();
+                //	break;
+                //case "dim":
+                //	attribute.Dim = reader.GetBoolean ();
+                //	break;
+                //case "underline":
+                //	attribute.Underline = reader.GetBoolean ();
+                //	break;
+                //case "blink":
+                //	attribute.Blink = reader.GetBoolean ();
+                //	break;
+                //case "reverse":
+                //	attribute.Reverse = reader.GetBoolean ();
+                //	break;
+                //case "hidden":
+                //	attribute.Hidden = reader.GetBoolean ();
+                //	break;
+                //case "strike-through":
+                //	attribute.StrikeThrough = reader.GetBoolean ();
+                //	break;				
+                default:
+                    throw new JsonException ($"Unknown Attribute property {propertyName}.");
+            }
+        }
+
+        throw new JsonException ();
+    }
+
+    public override void Write (Utf8JsonWriter writer, Attribute value, JsonSerializerOptions options)
+    {
+        writer.WriteStartObject ();
+        writer.WritePropertyName (nameof (Attribute.Foreground));
+        ColorJsonConverter.Instance.Write (writer, value.Foreground, options);
+        writer.WritePropertyName (nameof (Attribute.Background));
+        ColorJsonConverter.Instance.Write (writer, value.Background, options);
+
+        writer.WriteEndObject ();
+    }
+}

+ 50 - 50
Terminal.Gui/Configuration/ColorJsonConverter.cs

@@ -1,52 +1,52 @@
-using System;
-using System.Text.Json.Serialization;
 using System.Text.Json;
-using System.Text.RegularExpressions;
-
-namespace Terminal.Gui {
-	/// <summary>
-	/// Json converter for the <see cref="Color"/> class.
-	/// </summary>
-	internal class ColorJsonConverter : JsonConverter<Color> {
-		private static ColorJsonConverter _instance;
-
-		/// <summary>
-		/// Singleton
-		/// </summary>
-		public static ColorJsonConverter Instance {
-			get {
-				if (_instance == null) {
-					_instance = new ColorJsonConverter ();
-				}
-
-				return _instance;
-			}
-		}
-
-		public override Color Read (ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
-		{
-			// Check if the value is a string
-			if (reader.TokenType == JsonTokenType.String) {
-				// Get the color string
-				var colorString = reader.GetString ();
-
-				// Check if the color string is a color name
-				if (Enum.TryParse (colorString, ignoreCase: true, out ColorName color)) {
-					// Return the parsed color
-					return new Color(color);
-				}
-				if (Color.TryParse (colorString, out Color parsedColor)) {
-					return parsedColor;
-				}
-				throw new JsonException ($"Unexpected color name: {colorString}.");
-			} else {
-				throw new JsonException ($"Unexpected token when parsing Color: {reader.TokenType}");
-			}
-		}
-
-		public override void Write (Utf8JsonWriter writer, Color value, JsonSerializerOptions options)
-		{
-			writer.WriteStringValue (value.ToString());
-		}
-	}
+using System.Text.Json.Serialization;
+
+namespace Terminal.Gui;
+
+/// <summary>Json converter for the <see cref="Color"/> class.</summary>
+internal class ColorJsonConverter : JsonConverter<Color>
+{
+    private static ColorJsonConverter _instance;
+
+    /// <summary>Singleton</summary>
+    public static ColorJsonConverter Instance
+    {
+        get
+        {
+            if (_instance is null)
+            {
+                _instance = new ColorJsonConverter ();
+            }
+
+            return _instance;
+        }
+    }
+
+    public override Color Read (ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+    {
+        // Check if the value is a string
+        if (reader.TokenType == JsonTokenType.String)
+        {
+            // Get the color string
+            ReadOnlySpan<char> colorString = reader.GetString ();
+
+            // Check if the color string is a color name
+            if (Enum.TryParse (colorString, true, out ColorName color))
+            {
+                // Return the parsed color
+                return new Color (in color);
+            }
+
+            if (Color.TryParse (colorString, null, out Color parsedColor))
+            {
+                return parsedColor;
+            }
+
+            throw new JsonException ($"Unexpected color name: {colorString}.");
+        }
+
+        throw new JsonException ($"Unexpected token when parsing Color: {reader.TokenType}");
+    }
+
+    public override void Write (Utf8JsonWriter writer, Color value, JsonSerializerOptions options) { writer.WriteStringValue (value.ToString ()); }
 }

+ 110 - 99
Terminal.Gui/Configuration/ColorSchemeJsonConverter.cs

@@ -1,101 +1,112 @@
-using System;
-using System.Text.Json;
+using System.Text.Json;
 using System.Text.Json.Serialization;
 
-namespace Terminal.Gui {
-	/// <summary>
-	/// Implements a JSON converter for <see cref="ColorScheme"/>. 
-	/// </summary>
-	class ColorSchemeJsonConverter : JsonConverter<ColorScheme> {
-		private static ColorSchemeJsonConverter instance;
-
-		/// <summary>
-		/// Singleton
-		/// </summary>
-		public static ColorSchemeJsonConverter Instance {
-			get {
-				if (instance == null) {
-					instance = new ColorSchemeJsonConverter ();
-				}
-				return instance;
-			}
-		}
-		
-		/// <inheritdoc/>
-		public override ColorScheme Read (ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
-		{
-			if (reader.TokenType != JsonTokenType.StartObject) {
-				throw new JsonException ($"Unexpected StartObject token when parsing ColorScheme: {reader.TokenType}.");
-			}
-
-			Attribute normal = Attribute.Default;
-			Attribute focus = Attribute.Default;
-			Attribute hotNormal = Attribute.Default;
-			Attribute hotFocus = Attribute.Default;
-			Attribute disabled = Attribute.Default;
-
-			while (reader.Read ()) {
-				if (reader.TokenType == JsonTokenType.EndObject) {
-					var colorScheme = new ColorScheme () {
-						Normal = normal,
-						Focus = focus,
-						HotNormal = hotNormal,
-						HotFocus = hotFocus,
-						Disabled = disabled
-					};
-
-					return colorScheme;
-				}
-
-				if (reader.TokenType != JsonTokenType.PropertyName) {
-					throw new JsonException ($"Unexpected token when parsing Attribute: {reader.TokenType}.");
-				}
-
-				var propertyName = reader.GetString ();
-				reader.Read ();
-				var attribute = JsonSerializer.Deserialize<Attribute> (ref reader, options);
-
-				switch (propertyName.ToLower()) {
-				case "normal":
-					normal = attribute;
-					break;
-				case "focus":
-					focus = attribute;
-					break;
-				case "hotnormal":
-					hotNormal = attribute;
-					break;
-				case "hotfocus":
-					hotFocus = attribute;
-					break;
-				case "disabled":
-					disabled = attribute;
-					break;
-				default:
-					throw new JsonException ($"Unrecognized ColorScheme Attribute name: {propertyName}.");
-				}
-			}
-
-			throw new JsonException ();
-		}
-
-		/// <inheritdoc/>
-		public override void Write (Utf8JsonWriter writer, ColorScheme value, JsonSerializerOptions options)
-		{
-			writer.WriteStartObject ();
-
-			writer.WritePropertyName ("Normal");
-			AttributeJsonConverter.Instance.Write (writer, value.Normal, options);
-			writer.WritePropertyName ("Focus");
-			AttributeJsonConverter.Instance.Write (writer, value.Focus, options);
-			writer.WritePropertyName ("HotNormal");
-			AttributeJsonConverter.Instance.Write (writer, value.HotNormal, options);
-			writer.WritePropertyName ("HotFocus");
-			AttributeJsonConverter.Instance.Write (writer, value.HotFocus, options);
-			writer.WritePropertyName ("Disabled");
-			AttributeJsonConverter.Instance.Write (writer, value.Disabled, options);
-
-			writer.WriteEndObject ();
-		}
-	}
-}
+namespace Terminal.Gui;
+
+/// <summary>Implements a JSON converter for <see cref="ColorScheme"/>.</summary>
+internal class ColorSchemeJsonConverter : JsonConverter<ColorScheme>
+{
+    private static ColorSchemeJsonConverter instance;
+
+    /// <summary>Singleton</summary>
+    public static ColorSchemeJsonConverter Instance
+    {
+        get
+        {
+            if (instance is null)
+            {
+                instance = new ColorSchemeJsonConverter ();
+            }
+
+            return instance;
+        }
+    }
+
+    /// <inheritdoc/>
+    public override ColorScheme Read (ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+    {
+        if (reader.TokenType != JsonTokenType.StartObject)
+        {
+            throw new JsonException ($"Unexpected StartObject token when parsing ColorScheme: {reader.TokenType}.");
+        }
+
+        var normal = Attribute.Default;
+        var focus = Attribute.Default;
+        var hotNormal = Attribute.Default;
+        var hotFocus = Attribute.Default;
+        var disabled = Attribute.Default;
+
+        while (reader.Read ())
+        {
+            if (reader.TokenType == JsonTokenType.EndObject)
+            {
+                var colorScheme = new ColorScheme
+                {
+                    Normal = normal,
+                    Focus = focus,
+                    HotNormal = hotNormal,
+                    HotFocus = hotFocus,
+                    Disabled = disabled
+                };
+
+                return colorScheme;
+            }
+
+            if (reader.TokenType != JsonTokenType.PropertyName)
+            {
+                throw new JsonException ($"Unexpected token when parsing Attribute: {reader.TokenType}.");
+            }
+
+            string propertyName = reader.GetString ();
+            reader.Read ();
+            var attribute = JsonSerializer.Deserialize<Attribute> (ref reader, options);
+
+            switch (propertyName.ToLower ())
+            {
+                case "normal":
+                    normal = attribute;
+
+                    break;
+                case "focus":
+                    focus = attribute;
+
+                    break;
+                case "hotnormal":
+                    hotNormal = attribute;
+
+                    break;
+                case "hotfocus":
+                    hotFocus = attribute;
+
+                    break;
+                case "disabled":
+                    disabled = attribute;
+
+                    break;
+                default:
+                    throw new JsonException ($"Unrecognized ColorScheme Attribute name: {propertyName}.");
+            }
+        }
+
+        throw new JsonException ();
+    }
+
+    /// <inheritdoc/>
+    public override void Write (Utf8JsonWriter writer, ColorScheme value, JsonSerializerOptions options)
+    {
+        writer.WriteStartObject ();
+
+        writer.WritePropertyName ("Normal");
+        AttributeJsonConverter.Instance.Write (writer, value.Normal, options);
+        writer.WritePropertyName ("Focus");
+        AttributeJsonConverter.Instance.Write (writer, value.Focus, options);
+        writer.WritePropertyName ("HotNormal");
+        AttributeJsonConverter.Instance.Write (writer, value.HotNormal, options);
+        writer.WritePropertyName ("HotFocus");
+        AttributeJsonConverter.Instance.Write (writer, value.HotFocus, options);
+        writer.WritePropertyName ("Disabled");
+        AttributeJsonConverter.Instance.Write (writer, value.Disabled, options);
+
+        writer.WriteEndObject ();
+    }
+}

+ 109 - 85
Terminal.Gui/Configuration/ConfigProperty.cs

@@ -1,6 +1,4 @@
 #nullable enable
-
-using System;
 using System.Reflection;
 using System.Text.Json;
 using System.Text.Json.Serialization;
@@ -8,99 +6,125 @@ using System.Text.Json.Serialization;
 namespace Terminal.Gui;
 
 /// <summary>
-/// Holds a property's value and the <see cref="PropertyInfo"/> that allows <see cref="ConfigurationManager"/>
-/// to get and set the property's value.
+///     Holds a property's value and the <see cref="PropertyInfo"/> that allows <see cref="ConfigurationManager"/> to
+///     get and set the property's value.
 /// </summary>
 /// <remarks>
-/// Configuration properties must be <see langword="public"/> and <see langword="static"/>
-/// and have the <see cref="SerializableConfigurationProperty"/>
-/// attribute. If the type of the property requires specialized JSON serialization,
-/// a <see cref="JsonConverter"/> must be provided using
-/// the <see cref="JsonConverterAttribute"/> attribute.
+///     Configuration properties must be <see langword="public"/> and <see langword="static"/> and have the
+///     <see cref="SerializableConfigurationProperty"/> attribute. If the type of the property requires specialized JSON
+///     serialization, a <see cref="JsonConverter"/> must be provided using the <see cref="JsonConverterAttribute"/>
+///     attribute.
 /// </remarks>
-public class ConfigProperty {
+public class ConfigProperty
+{
+    /// <summary>Describes the property.</summary>
+    public PropertyInfo? PropertyInfo { get; set; }
+
+    /// <summary>
+    ///     Holds the property's value as it was either read from the class's implementation or from a config file. If the
+    ///     property has not been set (e.g. because no configuration file specified a value), this will be
+    ///     <see langword="null"/>.
+    /// </summary>
+    /// <remarks>
+    ///     On <see langword="set"/>, performs a sparse-copy of the new value to the existing value (only copies elements
+    ///     of the object that are non-null).
+    /// </remarks>
+    public object? PropertyValue { get; set; }
+
+    /// <summary>Applies the <see cref="PropertyValue"/> to the property described by <see cref="PropertyInfo"/>.</summary>
+    /// <returns></returns>
+    public bool Apply ()
+    {
+        if (PropertyValue is { })
+        {
+            try
+            {
+                if (PropertyInfo?.GetValue (null) is { })
+                {
+                    PropertyInfo?.SetValue (null, DeepMemberwiseCopy (PropertyValue, PropertyInfo?.GetValue (null)));
+                }
+            }
+            catch (TargetInvocationException tie)
+            {
+                // Check if there is an inner exception
+                if (tie.InnerException is { })
+                {
+                    // Handle the inner exception separately without catching the outer exception
+                    Exception? innerException = tie.InnerException;
+
+                    // Handle the inner exception here
+                    throw new JsonException (
+                                             $"Error Applying Configuration Change: {innerException.Message}",
+                                             innerException
+                                            );
+                }
 
-	/// <summary>
-	/// Describes the property.
-	/// </summary>
-	public PropertyInfo? PropertyInfo { get; set; }
+                // Handle the outer exception or rethrow it if needed
+                throw new JsonException ($"Error Applying Configuration Change: {tie.Message}", tie);
+            }
+            catch (ArgumentException ae)
+            {
+                throw new JsonException (
+                                         $"Error Applying Configuration Change ({PropertyInfo?.Name}): {ae.Message}",
+                                         ae
+                                        );
+            }
+        }
 
-	/// <summary>
-	/// Holds the property's value as it was either read from the class's implementation or from a config file.
-	/// If the property has not been set (e.g. because no configuration file specified a value),
-	/// this will be <see langword="null"/>.
-	/// </summary>
-	/// <remarks>
-	/// On <see langword="set"/>, performs a sparse-copy of the new value to the existing value (only copies elements of
-	/// the object that are non-null).
-	/// </remarks>
-	public object? PropertyValue { get; set; }
+        return PropertyValue != null;
+    }
 
-	/// <summary>
-	/// Helper to get either the Json property named (specified by [JsonPropertyName(name)]
-	/// or the actual property name.
-	/// </summary>
-	/// <param name="pi"></param>
-	/// <returns></returns>
-	public static string GetJsonPropertyName (PropertyInfo pi)
-	{
-		var jpna = pi.GetCustomAttribute (typeof (JsonPropertyNameAttribute)) as JsonPropertyNameAttribute;
-		return jpna?.Name ?? pi.Name;
-	}
+    /// <summary>
+    ///     Helper to get either the Json property named (specified by [JsonPropertyName(name)] or the actual property
+    ///     name.
+    /// </summary>
+    /// <param name="pi"></param>
+    /// <returns></returns>
+    public static string GetJsonPropertyName (PropertyInfo pi)
+    {
+        var jpna = pi.GetCustomAttribute (typeof (JsonPropertyNameAttribute)) as JsonPropertyNameAttribute;
 
-	internal object? UpdateValueFrom (object source)
-	{
-		if (source == null) {
-			return PropertyValue;
-		}
+        return jpna?.Name ?? pi.Name;
+    }
 
-		var ut = Nullable.GetUnderlyingType (PropertyInfo!.PropertyType);
-		if (source.GetType () != PropertyInfo!.PropertyType && ut != null && source.GetType () != ut) {
-			throw new ArgumentException ($"The source object ({PropertyInfo!.DeclaringType}.{PropertyInfo!.Name}) is not of type {PropertyInfo!.PropertyType}.");
-		}
-		if (PropertyValue != null) {
-			PropertyValue = DeepMemberwiseCopy (source, PropertyValue);
-		} else {
-			PropertyValue = source;
-		}
+    /// <summary>
+    ///     Retrieves (using reflection) the value of the static property described in <see cref="PropertyInfo"/> into
+    ///     <see cref="PropertyValue"/>.
+    /// </summary>
+    /// <returns></returns>
+    public object? RetrieveValue () { return PropertyValue = PropertyInfo!.GetValue (null); }
 
-		return PropertyValue;
-	}
+    internal object? UpdateValueFrom (object source)
+    {
+        if (source is null)
+        {
+            return PropertyValue;
+        }
 
-	/// <summary>
-	/// Retrieves (using reflection) the value of the static property described in <see cref="PropertyInfo"/>
-	/// into <see cref="PropertyValue"/>.
-	/// </summary>
-	/// <returns></returns>
-	public object? RetrieveValue () => PropertyValue = PropertyInfo!.GetValue (null);
+        Type? ut = Nullable.GetUnderlyingType (PropertyInfo!.PropertyType);
 
-	/// <summary>
-	/// Applies the <see cref="PropertyValue"/> to the property described by <see cref="PropertyInfo"/>.
-	/// </summary>
-	/// <returns></returns>
-	public bool Apply ()
-	{
-		if (PropertyValue != null) {
-			try {
-				if (PropertyInfo?.GetValue (null) != null) {
-					PropertyInfo?.SetValue (null, DeepMemberwiseCopy (PropertyValue, PropertyInfo?.GetValue (null)));
-				}
-			} catch (TargetInvocationException tie) {
-				// Check if there is an inner exception
-				if (tie.InnerException != null) {
-					// Handle the inner exception separately without catching the outer exception
-					var innerException = tie.InnerException;
+        if (source.GetType () != PropertyInfo!.PropertyType && ut is { } && source.GetType () != ut)
+        {
+            throw new ArgumentException (
+                                         $"The source object ({
+                                             PropertyInfo!.DeclaringType
+                                         }.{
+                                             PropertyInfo!.Name
+                                         }) is not of type {
+                                             PropertyInfo!.PropertyType
+                                         }."
+                                        );
+        }
 
-					// Handle the inner exception here
-					throw new JsonException ($"Error Applying Configuration Change: {innerException.Message}", innerException);
-				}
+        if (PropertyValue is { })
+        {
+            PropertyValue = DeepMemberwiseCopy (source, PropertyValue);
+        }
+        else
+        {
+            PropertyValue = source;
+        }
 
-				// Handle the outer exception or rethrow it if needed
-				throw new JsonException ($"Error Applying Configuration Change: {tie.Message}", tie);
-			} catch (ArgumentException ae) {
-				throw new JsonException ($"Error Applying Configuration Change ({PropertyInfo?.Name}): {ae.Message}", ae);
-			}
-		}
-		return PropertyValue != null;
-	}
-}
+        return PropertyValue;
+    }
+}

+ 592 - 517
Terminal.Gui/Configuration/ConfigurationManager.cs

@@ -1,532 +1,607 @@
 global using static Terminal.Gui.ConfigurationManager;
 global using CM = Terminal.Gui.ConfigurationManager;
-using System;
 using System.Collections;
-using System.Collections.Generic;
 using System.Diagnostics;
-using System.IO;
-using System.Linq;
 using System.Reflection;
-using System.Text;
 using System.Text.Encodings.Web;
 using System.Text.Json;
 using System.Text.Json.Serialization;
 
-
 #nullable enable
 
 namespace Terminal.Gui;
 
 /// <summary>
-/// Provides settings and configuration management for Terminal.Gui applications.
-/// <para>
-/// Users can set Terminal.Gui settings on a global or per-application basis by providing JSON formatted configuration
-/// files.
-/// The configuration files can be placed in at <c>.tui</c> folder in the user's home directory (e.g.
-/// <c>C:/Users/username/.tui</c>,
-/// or <c>/usr/username/.tui</c>),
-/// the folder where the Terminal.Gui application was launched from (e.g. <c>./.tui</c>), or as a resource
-/// within the Terminal.Gui application's main assembly.
-/// </para>
-/// <para>
-/// Settings are defined in JSON format, according to this schema:
-/// https://gui-cs.github.io/Terminal.Gui/schemas/tui-config-schema.json
-/// </para>
-/// <para>
-/// Settings that will apply to all applications (global settings) reside in files named <c>config.json</c>. Settings
-/// that will apply to a specific Terminal.Gui application reside in files named <c>appname.config.json</c>,
-/// where <c>appname</c> is the assembly name of the application (e.g. <c>UICatalog.config.json</c>).
-/// </para>
-/// Settings are applied using the following precedence (higher precedence settings
-/// overwrite lower precedence settings):
-/// <para>
-/// 1. Application configuration found in the users's home directory (<c>~/.tui/appname.config.json</c>) -- Highest
-/// precedence
-/// </para>
-/// <para>
-/// 2. Application configuration found in the directory the app was launched from (<c>./.tui/appname.config.json</c>).
-/// </para>
-/// <para>
-/// 3. Application configuration found in the applications's resources (<c>Resources/config.json</c>).
-/// </para>
-/// <para>
-/// 4. Global configuration found in the user's home directory (<c>~/.tui/config.json</c>).
-/// </para>
-/// <para>
-/// 5. Global configuration found in the directory the app was launched from (<c>./.tui/config.json</c>).
-/// </para>
-/// <para>
-/// 6. Global configuration in <c>Terminal.Gui.dll</c>'s resources (<c>Terminal.Gui.Resources.config.json</c>) -- Lowest
-/// Precidence.
-/// </para>
+///     Provides settings and configuration management for Terminal.Gui applications.
+///     <para>
+///         Users can set Terminal.Gui settings on a global or per-application basis by providing JSON formatted
+///         configuration files. The configuration files can be placed in at <c>.tui</c> folder in the user's home
+///         directory (e.g. <c>C:/Users/username/.tui</c>, or <c>/usr/username/.tui</c>), the folder where the Terminal.Gui
+///         application was launched from (e.g. <c>./.tui</c> ), or as a resource within the Terminal.Gui application's
+///         main assembly.
+///     </para>
+///     <para>
+///         Settings are defined in JSON format, according to this schema:
+///         https://gui-cs.github.io/Terminal.Gui/schemas/tui-config-schema.json
+///     </para>
+///     <para>
+///         Settings that will apply to all applications (global settings) reside in files named <c>config.json</c>.
+///         Settings that will apply to a specific Terminal.Gui application reside in files named
+///         <c>appname.config.json</c>, where <c>appname</c> is the assembly name of the application (e.g.
+///         <c>UICatalog.config.json</c>).
+///     </para>
+///     Settings are applied using the following precedence (higher precedence settings overwrite lower precedence
+///     settings):
+///     <para>
+///         1. Application configuration found in the users's home directory (<c>~/.tui/appname.config.json</c>) --
+///         Highest precedence
+///     </para>
+///     <para>
+///         2. Application configuration found in the directory the app was launched from (
+///         <c>./.tui/appname.config.json</c>).
+///     </para>
+///     <para>3. Application configuration found in the applications's resources (<c>Resources/config.json</c>).</para>
+///     <para>4. Global configuration found in the user's home directory (<c>~/.tui/config.json</c>).</para>
+///     <para>5. Global configuration found in the directory the app was launched from (<c>./.tui/config.json</c>).</para>
+///     <para>
+///         6. Global configuration in <c>Terminal.Gui.dll</c>'s resources (<c>Terminal.Gui.Resources.config.json</c>) --
+///         Lowest Precidence.
+///     </para>
 /// </summary>
-public static class ConfigurationManager {
-
-	/// <summary>
-	/// Describes the location of the configuration files. The constants can be
-	/// combined (bitwise) to specify multiple locations.
-	/// </summary>
-	[Flags]
-	public enum ConfigLocations {
-		/// <summary>
-		/// No configuration will be loaded.
-		/// </summary>
-		/// <remarks>
-		/// Used for development and testing only. For Terminal,Gui to function properly, at least
-		/// <see cref="DefaultOnly"/> should be set.
-		/// </remarks>
-		None = 0,
-
-		/// <summary>
-		/// Global configuration in <c>Terminal.Gui.dll</c>'s resources (<c>Terminal.Gui.Resources.config.json</c>) -- Lowest
-		/// Precedence.
-		/// </summary>
-		DefaultOnly,
-
-		/// <summary>
-		/// This constant is a combination of all locations
-		/// </summary>
-		All = -1
-
-	}
-
-	static readonly string _configFilename = "config.json";
-
-	internal static readonly JsonSerializerOptions _serializerOptions = new() {
-		ReadCommentHandling = JsonCommentHandling.Skip,
-		PropertyNameCaseInsensitive = true,
-		DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
-		WriteIndented = true,
-		Converters = {
-			// We override the standard Rune converter to support specifying Glyphs in
-			// a flexible way
-			new RuneJsonConverter (),
-			// Override Key to support "Ctrl+Q" format.
-			new KeyJsonConverter ()
-		},
-		// Enables Key to be "Ctrl+Q" vs "Ctrl\u002BQ"
-		Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
-
-	};
-
-	/// <summary>
-	/// A dictionary of all properties in the Terminal.Gui project that are decorated with the
-	/// <see cref="SerializableConfigurationProperty"/> attribute.
-	/// The keys are the property names pre-pended with the class that implements the property (e.g.
-	/// <c>Application.UseSystemConsole</c>).
-	/// The values are instances of <see cref="ConfigProperty"/> which hold the property's value and the
-	/// <see cref="PropertyInfo"/> that allows <see cref="ConfigurationManager"/> to get and set the property's value.
-	/// </summary>
-	/// <remarks>
-	/// Is <see langword="null"/> until <see cref="Initialize"/> is called.
-	/// </remarks>
-	internal static Dictionary<string, ConfigProperty>? _allConfigProperties;
-
-	/// <summary>
-	/// The backing property for <see cref="Settings"/>.
-	/// </summary>
-	/// <remarks>
-	/// Is <see langword="null"/> until <see cref="Reset"/> is called. Gets set to a new instance by
-	/// deserialization (see <see cref="Load"/>).
-	/// </remarks>
-	static SettingsScope? _settings;
-
-	internal static StringBuilder jsonErrors = new ();
-
-	/// <summary>
-	/// The root object of Terminal.Gui configuration settings / JSON schema. Contains only properties with the
-	/// <see cref="SettingsScope"/>
-	/// attribute value.
-	/// </summary>
-	public static SettingsScope? Settings {
-		get {
-			if (_settings == null) {
-				throw new InvalidOperationException ("ConfigurationManager has not been initialized. Call ConfigurationManager.Reset() before accessing the Settings property.");
-			}
-			return _settings;
-		}
-		set => _settings = value!;
-	}
-
-	/// <summary>
-	/// The root object of Terminal.Gui themes manager. Contains only properties with the <see cref="ThemeScope"/>
-	/// attribute value.
-	/// </summary>
-	public static ThemeManager? Themes => ThemeManager.Instance;
-
-	/// <summary>
-	/// Application-specific configuration settings scope.
-	/// </summary>
-	[SerializableConfigurationProperty (Scope = typeof (SettingsScope), OmitClassName = true)] [JsonPropertyName ("AppSettings")]
-	public static AppScope? AppSettings { get; set; }
-
-	/// <summary>
-	/// The set of glyphs used to draw checkboxes, lines, borders, etc...See also
-	/// <seealso cref="Terminal.Gui.GlyphDefinitions"/>.
-	/// </summary>
-	[SerializableConfigurationProperty (Scope = typeof (SettingsScope), OmitClassName = true)] [JsonPropertyName ("Glyphs")]
-	public static GlyphDefinitions Glyphs { get; set; } = new ();
-
-	/// <summary>
-	/// Gets or sets whether the <see cref="ConfigurationManager"/> should throw an exception if it encounters
-	/// an error on deserialization. If <see langword="false"/> (the default), the error is logged and printed to the
-	/// console when <see cref="Application.Shutdown"/> is called.
-	/// </summary>
-	[SerializableConfigurationProperty (Scope = typeof (SettingsScope))]
-	public static bool? ThrowOnJsonErrors { get; set; } = false;
-
-	/// <summary>
-	/// Name of the running application. By default this property is set to the application's assembly name.
-	/// </summary>
-	public static string AppName { get; set; } = Assembly.GetEntryAssembly ()?.FullName?.Split (',') [0]?.Trim ()!;
-
-	/// <summary>
-	/// Gets and sets the locations where <see cref="ConfigurationManager"/> will look for config files.
-	/// The value is <see cref="ConfigLocations.All"/>.
-	/// </summary>
-	public static ConfigLocations Locations { get; set; } = ConfigLocations.All;
-
-	/// <summary>
-	/// Initializes the internal state of ConfigurationManager. Nominally called once as part of application
-	/// startup to initialize global state. Also called from some Unit Tests to ensure correctness (e.g. Reset()).
-	/// </summary>
-	internal static void Initialize ()
-	{
-		_allConfigProperties = new Dictionary<string, ConfigProperty> ();
-		_settings = null;
-
-		var classesWithConfigProps = new Dictionary<string, Type> (StringComparer.InvariantCultureIgnoreCase);
-		// Get Terminal.Gui.dll classes
-
-		var types = from assembly in AppDomain.CurrentDomain.GetAssemblies ()
-			from type in assembly.GetTypes ()
-			where type.GetProperties ().Any (prop => prop.GetCustomAttribute (typeof (SerializableConfigurationProperty)) != null)
-			select type;
-
-		foreach (var classWithConfig in types) {
-			classesWithConfigProps.Add (classWithConfig.Name, classWithConfig);
-		}
-
-		Debug.WriteLine ($"ConfigManager.getConfigProperties found {classesWithConfigProps.Count} classes:");
-		classesWithConfigProps.ToList ().ForEach (x => Debug.WriteLine ($"  Class: {x.Key}"));
-
-		foreach (var p in from c in classesWithConfigProps
-			let props = c.Value.GetProperties (BindingFlags.Instance | BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public).Where (prop =>
-				prop.GetCustomAttribute (typeof (SerializableConfigurationProperty)) is SerializableConfigurationProperty)
-			let enumerable = props
-			from p in enumerable
-			select p) {
-			if (p.GetCustomAttribute (typeof (SerializableConfigurationProperty)) is SerializableConfigurationProperty scp) {
-				if (p.GetGetMethod (true)!.IsStatic) {
-					// If the class name is omitted, JsonPropertyName is allowed. 
-					_allConfigProperties!.Add (scp.OmitClassName ? ConfigProperty.GetJsonPropertyName (p) : $"{p.DeclaringType?.Name}.{p.Name}", new ConfigProperty {
-						PropertyInfo = p,
-						PropertyValue = null
-					});
-				} else {
-					throw new Exception ($"Property {p.Name} in class {p.DeclaringType?.Name} is not static. All SerializableConfigurationProperty properties must be static.");
-				}
-			}
-		}
-
-		_allConfigProperties = _allConfigProperties!.OrderBy (x => x.Key).ToDictionary (x => x.Key, x => x.Value, StringComparer.InvariantCultureIgnoreCase);
-
-		Debug.WriteLine ($"ConfigManager.Initialize found {_allConfigProperties.Count} properties:");
-		//_allConfigProperties.ToList ().ForEach (x => Debug.WriteLine ($"  Property: {x.Key}"));
-
-		AppSettings = new AppScope ();
-	}
-
-	/// <summary>
-	/// Creates a JSON document with the configuration specified.
-	/// </summary>
-	/// <returns></returns>
-	internal static string ToJson ()
-	{
-		Debug.WriteLine ("ConfigurationManager.ToJson()");
-		return JsonSerializer.Serialize (Settings!, _serializerOptions);
-	}
-
-	internal static Stream ToStream ()
-	{
-		var json = JsonSerializer.Serialize (Settings!, _serializerOptions);
-		// turn it into a stream
-		var stream = new MemoryStream ();
-		var writer = new StreamWriter (stream);
-		writer.Write (json);
-		writer.Flush ();
-		stream.Position = 0;
-		return stream;
-	}
-
-	internal static void AddJsonError (string error)
-	{
-		Debug.WriteLine ($"ConfigurationManager: {error}");
-		jsonErrors.AppendLine (error);
-	}
-
-	/// <summary>
-	/// Prints any Json deserialization errors that occurred during deserialization to the console.
-	/// </summary>
-	public static void PrintJsonErrors ()
-	{
-		if (jsonErrors.Length > 0) {
-			Console.WriteLine (@"Terminal.Gui ConfigurationManager encountered the following errors while deserializing configuration files:");
-			Console.WriteLine (jsonErrors.ToString ());
-		}
-	}
-
-	static void ClearJsonErrors () => jsonErrors.Clear ();
-
-	/// <summary>
-	/// Called when the configuration has been updated from a configuration file. Invokes the <see cref="Updated"/>
-	/// event.
-	/// </summary>
-	public static void OnUpdated ()
-	{
-		Debug.WriteLine (@"ConfigurationManager.OnApplied()");
-		Updated?.Invoke (null, new ConfigurationManagerEventArgs ());
-	}
-
-	/// <summary>
-	/// Event fired when the configuration has been updated from a configuration source.
-	/// application.
-	/// </summary>
-	public static event EventHandler<ConfigurationManagerEventArgs>? Updated;
-
-	/// <summary>
-	/// Resets the state of <see cref="ConfigurationManager"/>. Should be called whenever a new app session
-	/// (e.g. in <see cref="Application.Init"/> starts. Called by <see cref="Load"/>
-	/// if the <c>reset</c> parameter is <see langword="true"/>.
-	/// </summary>
-	/// <remarks>
-	/// 
-	/// </remarks>
-	public static void Reset ()
-	{
-		Debug.WriteLine (@"ConfigurationManager.Reset()");
-		if (_allConfigProperties == null) {
-			Initialize ();
-		}
-
-		ClearJsonErrors ();
-
-		Settings = new SettingsScope ();
-		ThemeManager.Reset ();
-		AppSettings = new AppScope ();
-
-		// To enable some unit tests, we only load from resources if the flag is set
-		if (Locations.HasFlag (ConfigLocations.DefaultOnly)) {
-			Settings.UpdateFromResource (typeof (ConfigurationManager).Assembly, $"Terminal.Gui.Resources.{_configFilename}");
-		}
-		Apply ();
-		ThemeManager.Themes? [ThemeManager.SelectedTheme]?.Apply ();
-		AppSettings?.Apply ();
-	}
-
-	/// <summary>
-	/// Retrieves the hard coded default settings from the Terminal.Gui library implementation. Used in development of
-	/// the library to generate the default configuration file. Before calling Application.Init, make sure
-	/// <see cref="Locations"/> is set to <see cref="ConfigLocations.None"/>.
-	/// </summary>
-	/// <remarks>
-	///         <para>
-	///         This method is only really useful when using ConfigurationManagerTests
-	///         to generate the JSON doc that is embedded into Terminal.Gui (during development).
-	///         </para>
-	///         <para>
-	///         WARNING: The <c>Terminal.Gui.Resources.config.json</c> resource has setting definitions (Themes)
-	///         that are NOT generated by this function. If you use this function to regenerate
-	///         <c>Terminal.Gui.Resources.config.json</c>,
-	///         make sure you copy the Theme definitions from the existing <c>Terminal.Gui.Resources.config.json</c> file.
-	///         </para>
-	/// </remarks>
-	internal static void GetHardCodedDefaults ()
-	{
-		if (_allConfigProperties == null) {
-			throw new InvalidOperationException ("Initialize must be called first.");
-		}
-		Settings = new SettingsScope ();
-		ThemeManager.GetHardCodedDefaults ();
-		AppSettings?.RetrieveValues ();
-		foreach (var p in Settings!.Where (cp => cp.Value.PropertyInfo != null)) {
-			Settings! [p.Key].PropertyValue = p.Value.PropertyInfo?.GetValue (null);
-		}
-	}
-
-	/// <summary>
-	/// Applies the configuration settings to the running <see cref="Application"/> instance.
-	/// </summary>
-	public static void Apply ()
-	{
-		var settings = false;
-		var themes = false;
-		var appSettings = false;
-		try {
-			settings = Settings?.Apply () ?? false;
-			themes = !string.IsNullOrEmpty (ThemeManager.SelectedTheme) && (ThemeManager.Themes? [ThemeManager.SelectedTheme]?.Apply () ?? false);
-			appSettings = AppSettings?.Apply () ?? false;
-
-		} catch (JsonException e) {
-			if (ThrowOnJsonErrors ?? false) {
-				throw;
-			} else {
-				AddJsonError ($"Error applying Configuration Change: {e.Message}");
-			}
-		} finally {
-			if (settings || themes || appSettings) {
-				OnApplied ();
-			}
-		}
-	}
-
-	/// <summary>
-	/// Called when an updated configuration has been applied to the
-	/// application. Fires the <see cref="Applied"/> event.
-	/// </summary>
-	public static void OnApplied ()
-	{
-		Debug.WriteLine ("ConfigurationManager.OnApplied()");
-		Applied?.Invoke (null, new ConfigurationManagerEventArgs ());
-
-		// TODO: Refactor ConfigurationManager to not use an event handler for this.
-		// Instead, have it call a method on any class appropriately attributed
-		// to update the cached values. See Issue #2871
-	}
-
-	/// <summary>
-	/// Event fired when an updated configuration has been applied to the
-	/// application.
-	/// </summary>
-	public static event EventHandler<ConfigurationManagerEventArgs>? Applied;
-
-	/// <summary>
-	/// Loads all settings found in the various configuration storage locations to
-	/// the <see cref="ConfigurationManager"/>. Optionally,
-	/// resets all settings attributed with <see cref="SerializableConfigurationProperty"/> to the defaults.
-	/// </summary>
-	/// <remarks>
-	/// Use <see cref="Apply"/> to cause the loaded settings to be applied to the running application.
-	/// </remarks>
-	/// <param name="reset">
-	/// If <see langword="true"/> the state of <see cref="ConfigurationManager"/> will
-	/// be reset to the defaults.
-	/// </param>
-	public static void Load (bool reset = false)
-	{
-		Debug.WriteLine ("ConfigurationManager.Load()");
-
-		if (reset) {
-			Reset ();
-		}
-
-		// LibraryResources is always loaded by Reset
-		if (Locations == ConfigLocations.All) {
-			var embeddedStylesResourceName = Assembly.GetEntryAssembly ()?
-				.GetManifestResourceNames ().FirstOrDefault (x => x.EndsWith (_configFilename));
-			if (string.IsNullOrEmpty (embeddedStylesResourceName)) {
-				embeddedStylesResourceName = _configFilename;
-			}
-
-			Settings = Settings?
-				// Global current directory
-				.Update ($"./.tui/{_configFilename}")?
-				// Global home directory
-				.Update ($"~/.tui/{_configFilename}")?
-				// App resources
-				.UpdateFromResource (Assembly.GetEntryAssembly ()!, embeddedStylesResourceName!)?
-				// App current directory
-				.Update ($"./.tui/{AppName}.{_configFilename}")?
-				// App home directory
-				.Update ($"~/.tui/{AppName}.{_configFilename}");
-		}
-	}
-
-	/// <summary>
-	/// Returns an empty Json document with just the $schema tag.
-	/// </summary>
-	/// <returns></returns>
-	public static string GetEmptyJson ()
-	{
-		var emptyScope = new SettingsScope ();
-		emptyScope.Clear ();
-		return JsonSerializer.Serialize (emptyScope, _serializerOptions);
-	}
-
-	/// <summary>
-	/// System.Text.Json does not support copying a deserialized object to an existing instance.
-	/// To work around this, we implement a 'deep, memberwise copy' method.
-	/// </summary>
-	/// <remarks>
-	/// TOOD: When System.Text.Json implements `PopulateObject` revisit
-	/// https://github.com/dotnet/corefx/issues/37627
-	/// </remarks>
-	/// <param name="source"></param>
-	/// <param name="destination"></param>
-	/// <returns><paramref name="destination"/> updated from <paramref name="source"/></returns>
-	internal static object? DeepMemberwiseCopy (object? source, object? destination)
-	{
-		if (destination == null) {
-			throw new ArgumentNullException (nameof (destination));
-		}
-
-		if (source == null) {
-			return null!;
-		}
-
-		if (source.GetType () == typeof (SettingsScope)) {
-			return ((SettingsScope)destination).Update ((SettingsScope)source);
-		}
-		if (source.GetType () == typeof (ThemeScope)) {
-			return ((ThemeScope)destination).Update ((ThemeScope)source);
-		}
-		if (source.GetType () == typeof (AppScope)) {
-			return ((AppScope)destination).Update ((AppScope)source);
-		}
-
-		// If value type, just use copy constructor.
-		if (source.GetType ().IsValueType || source.GetType () == typeof (string)) {
-			return source;
-		}
-
-		// Dictionary
-		if (source.GetType ().IsGenericType && source.GetType ().GetGenericTypeDefinition ().IsAssignableFrom (typeof (Dictionary<,>))) {
-			foreach (var srcKey in ((IDictionary)source).Keys) {
-				if (srcKey is string) { }
-				if (((IDictionary)destination).Contains (srcKey)) {
-					((IDictionary)destination) [srcKey] = DeepMemberwiseCopy (((IDictionary)source) [srcKey], ((IDictionary)destination) [srcKey]);
-				} else {
-					((IDictionary)destination).Add (srcKey, ((IDictionary)source) [srcKey]);
-				}
-			}
-			return destination;
-		}
-
-		// ALl other object types
-		var sourceProps = source?.GetType ().GetProperties ().Where (x => x.CanRead).ToList ();
-		var destProps = destination?.GetType ().GetProperties ().Where (x => x.CanWrite).ToList ()!;
-		foreach ((var sourceProp, var destProp) in
-			from sourceProp in sourceProps
-			where destProps.Any (x => x.Name == sourceProp.Name)
-			let destProp = destProps.First (x => x.Name == sourceProp.Name)
-			where destProp.CanWrite
-			select (sourceProp, destProp)) {
-
-			var sourceVal = sourceProp.GetValue (source);
-			var destVal = destProp.GetValue (destination);
-			if (sourceVal != null) {
-				try {
-					if (destVal != null) {
-						// Recurse
-						destProp.SetValue (destination, DeepMemberwiseCopy (sourceVal, destVal));
-					} else {
-						destProp.SetValue (destination, sourceVal);
-					}
-				} catch (ArgumentException e) {
-					throw new JsonException ($"Error Applying Configuration Change: {e.Message}", e);
-				}
-			}
-		}
-		return destination!;
-	}
-}
+public static class ConfigurationManager
+{
+    /// <summary>
+    ///     Describes the location of the configuration files. The constants can be combined (bitwise) to specify multiple
+    ///     locations.
+    /// </summary>
+    [Flags]
+    public enum ConfigLocations
+    {
+        /// <summary>No configuration will be loaded.</summary>
+        /// <remarks>
+        ///     Used for development and testing only. For Terminal,Gui to function properly, at least
+        ///     <see cref="DefaultOnly"/> should be set.
+        /// </remarks>
+        None = 0,
+
+        /// <summary>
+        ///     Global configuration in <c>Terminal.Gui.dll</c>'s resources (<c>Terminal.Gui.Resources.config.json</c>) --
+        ///     Lowest Precedence.
+        /// </summary>
+        DefaultOnly,
+
+        /// <summary>This constant is a combination of all locations</summary>
+        All = -1
+    }
+
+    /// <summary>
+    ///     A dictionary of all properties in the Terminal.Gui project that are decorated with the
+    ///     <see cref="SerializableConfigurationProperty"/> attribute. The keys are the property names pre-pended with the
+    ///     class that implements the property (e.g. <c>Application.UseSystemConsole</c>). The values are instances of
+    ///     <see cref="ConfigProperty"/> which hold the property's value and the <see cref="PropertyInfo"/> that allows
+    ///     <see cref="ConfigurationManager"/> to get and set the property's value.
+    /// </summary>
+    /// <remarks>Is <see langword="null"/> until <see cref="Initialize"/> is called.</remarks>
+    internal static Dictionary<string, ConfigProperty>? _allConfigProperties;
+
+    internal static readonly JsonSerializerOptions _serializerOptions = new ()
+    {
+        ReadCommentHandling = JsonCommentHandling.Skip,
+        PropertyNameCaseInsensitive = true,
+        DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
+        WriteIndented = true,
+        Converters =
+        {
+            // We override the standard Rune converter to support specifying Glyphs in
+            // a flexible way
+            new RuneJsonConverter (),
+
+            // Override Key to support "Ctrl+Q" format.
+            new KeyJsonConverter ()
+        },
+
+        // Enables Key to be "Ctrl+Q" vs "Ctrl\u002BQ"
+        Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
+    };
+
+    internal static StringBuilder jsonErrors = new ();
+
+    private static readonly string _configFilename = "config.json";
+
+    /// <summary>The backing property for <see cref="Settings"/>.</summary>
+    /// <remarks>
+    ///     Is <see langword="null"/> until <see cref="Reset"/> is called. Gets set to a new instance by deserialization
+    ///     (see <see cref="Load"/>).
+    /// </remarks>
+    private static SettingsScope? _settings;
+
+    /// <summary>Name of the running application. By default this property is set to the application's assembly name.</summary>
+    public static string AppName { get; set; } = Assembly.GetEntryAssembly ()?.FullName?.Split (',') [0]?.Trim ()!;
+
+    /// <summary>Application-specific configuration settings scope.</summary>
+    [SerializableConfigurationProperty (Scope = typeof (SettingsScope), OmitClassName = true)]
+    [JsonPropertyName ("AppSettings")]
+    public static AppScope? AppSettings { get; set; }
+
+    /// <summary>
+    ///     The set of glyphs used to draw checkboxes, lines, borders, etc...See also
+    ///     <seealso cref="Terminal.Gui.GlyphDefinitions"/>.
+    /// </summary>
+    [SerializableConfigurationProperty (Scope = typeof (SettingsScope), OmitClassName = true)]
+    [JsonPropertyName ("Glyphs")]
+    public static GlyphDefinitions Glyphs { get; set; } = new ();
+
+    /// <summary>
+    ///     Gets and sets the locations where <see cref="ConfigurationManager"/> will look for config files. The value is
+    ///     <see cref="ConfigLocations.All"/>.
+    /// </summary>
+    public static ConfigLocations Locations { get; set; } = ConfigLocations.All;
+
+    /// <summary>
+    ///     The root object of Terminal.Gui configuration settings / JSON schema. Contains only properties with the
+    ///     <see cref="SettingsScope"/> attribute value.
+    /// </summary>
+    public static SettingsScope? Settings
+    {
+        get
+        {
+            if (_settings is null)
+            {
+                throw new InvalidOperationException (
+                                                     "ConfigurationManager has not been initialized. Call ConfigurationManager.Reset() before accessing the Settings property."
+                                                    );
+            }
+
+            return _settings;
+        }
+        set => _settings = value!;
+    }
+
+    /// <summary>
+    ///     The root object of Terminal.Gui themes manager. Contains only properties with the <see cref="ThemeScope"/>
+    ///     attribute value.
+    /// </summary>
+    public static ThemeManager? Themes => ThemeManager.Instance;
+
+    /// <summary>
+    ///     Gets or sets whether the <see cref="ConfigurationManager"/> should throw an exception if it encounters an
+    ///     error on deserialization. If <see langword="false"/> (the default), the error is logged and printed to the console
+    ///     when <see cref="Application.Shutdown"/> is called.
+    /// </summary>
+    [SerializableConfigurationProperty (Scope = typeof (SettingsScope))]
+    public static bool? ThrowOnJsonErrors { get; set; } = false;
+
+    /// <summary>Event fired when an updated configuration has been applied to the application.</summary>
+    public static event EventHandler<ConfigurationManagerEventArgs>? Applied;
+
+    /// <summary>Applies the configuration settings to the running <see cref="Application"/> instance.</summary>
+    public static void Apply ()
+    {
+        var settings = false;
+        var themes = false;
+        var appSettings = false;
+
+        try
+        {
+            settings = Settings?.Apply () ?? false;
+
+            themes = !string.IsNullOrEmpty (ThemeManager.SelectedTheme)
+                     && (ThemeManager.Themes? [ThemeManager.SelectedTheme]?.Apply () ?? false);
+            appSettings = AppSettings?.Apply () ?? false;
+        }
+        catch (JsonException e)
+        {
+            if (ThrowOnJsonErrors ?? false)
+            {
+                throw;
+            }
+            else
+            {
+                AddJsonError ($"Error applying Configuration Change: {e.Message}");
+            }
+        }
+        finally
+        {
+            if (settings || themes || appSettings)
+            {
+                OnApplied ();
+            }
+        }
+    }
+
+    /// <summary>Returns an empty Json document with just the $schema tag.</summary>
+    /// <returns></returns>
+    public static string GetEmptyJson ()
+    {
+        var emptyScope = new SettingsScope ();
+        emptyScope.Clear ();
+
+        return JsonSerializer.Serialize (emptyScope, _serializerOptions);
+    }
+
+    /// <summary>
+    ///     Loads all settings found in the various configuration storage locations to the
+    ///     <see cref="ConfigurationManager"/>. Optionally, resets all settings attributed with
+    ///     <see cref="SerializableConfigurationProperty"/> to the defaults.
+    /// </summary>
+    /// <remarks>Use <see cref="Apply"/> to cause the loaded settings to be applied to the running application.</remarks>
+    /// <param name="reset">
+    ///     If <see langword="true"/> the state of <see cref="ConfigurationManager"/> will be reset to the
+    ///     defaults.
+    /// </param>
+    public static void Load (bool reset = false)
+    {
+        Debug.WriteLine ("ConfigurationManager.Load()");
+
+        if (reset)
+        {
+            Reset ();
+        }
+
+        // LibraryResources is always loaded by Reset
+        if (Locations == ConfigLocations.All)
+        {
+            string? embeddedStylesResourceName = Assembly.GetEntryAssembly ()
+                                                         ?
+                                                         .GetManifestResourceNames ()
+                                                         .FirstOrDefault (x => x.EndsWith (_configFilename));
+
+            if (string.IsNullOrEmpty (embeddedStylesResourceName))
+            {
+                embeddedStylesResourceName = _configFilename;
+            }
+
+            Settings = Settings?
+
+                       // Global current directory
+                       .Update ($"./.tui/{_configFilename}")
+                       ?
+
+                       // Global home directory
+                       .Update ($"~/.tui/{_configFilename}")
+                       ?
+
+                       // App resources
+                       .UpdateFromResource (Assembly.GetEntryAssembly ()!, embeddedStylesResourceName!)
+                       ?
+
+                       // App current directory
+                       .Update ($"./.tui/{AppName}.{_configFilename}")
+                       ?
+
+                       // App home directory
+                       .Update ($"~/.tui/{AppName}.{_configFilename}");
+        }
+    }
+
+    /// <summary>
+    ///     Called when an updated configuration has been applied to the application. Fires the <see cref="Applied"/>
+    ///     event.
+    /// </summary>
+    public static void OnApplied ()
+    {
+        Debug.WriteLine ("ConfigurationManager.OnApplied()");
+        Applied?.Invoke (null, new ConfigurationManagerEventArgs ());
+
+        // TODO: Refactor ConfigurationManager to not use an event handler for this.
+        // Instead, have it call a method on any class appropriately attributed
+        // to update the cached values. See Issue #2871
+    }
+
+    /// <summary>
+    ///     Called when the configuration has been updated from a configuration file. Invokes the <see cref="Updated"/>
+    ///     event.
+    /// </summary>
+    public static void OnUpdated ()
+    {
+        Debug.WriteLine (@"ConfigurationManager.OnApplied()");
+        Updated?.Invoke (null, new ConfigurationManagerEventArgs ());
+    }
+
+    /// <summary>Prints any Json deserialization errors that occurred during deserialization to the console.</summary>
+    public static void PrintJsonErrors ()
+    {
+        if (jsonErrors.Length > 0)
+        {
+            Console.WriteLine (
+                               @"Terminal.Gui ConfigurationManager encountered the following errors while deserializing configuration files:"
+                              );
+            Console.WriteLine (jsonErrors.ToString ());
+        }
+    }
+
+    /// <summary>
+    ///     Resets the state of <see cref="ConfigurationManager"/>. Should be called whenever a new app session (e.g. in
+    ///     <see cref="Application.Init"/> starts. Called by <see cref="Load"/> if the <c>reset</c> parameter is
+    ///     <see langword="true"/>.
+    /// </summary>
+    /// <remarks></remarks>
+    public static void Reset ()
+    {
+        Debug.WriteLine (@"ConfigurationManager.Reset()");
+
+        if (_allConfigProperties is null)
+        {
+            Initialize ();
+        }
+
+        ClearJsonErrors ();
+
+        Settings = new SettingsScope ();
+        ThemeManager.Reset ();
+        AppSettings = new AppScope ();
+
+        // To enable some unit tests, we only load from resources if the flag is set
+        if (Locations.HasFlag (ConfigLocations.DefaultOnly))
+        {
+            Settings.UpdateFromResource (
+                                         typeof (ConfigurationManager).Assembly,
+                                         $"Terminal.Gui.Resources.{_configFilename}"
+                                        );
+        }
+
+        Apply ();
+        ThemeManager.Themes? [ThemeManager.SelectedTheme]?.Apply ();
+        AppSettings?.Apply ();
+    }
+
+    /// <summary>Event fired when the configuration has been updated from a configuration source. application.</summary>
+    public static event EventHandler<ConfigurationManagerEventArgs>? Updated;
+
+    internal static void AddJsonError (string error)
+    {
+        Debug.WriteLine ($"ConfigurationManager: {error}");
+        jsonErrors.AppendLine (error);
+    }
+
+    /// <summary>
+    ///     System.Text.Json does not support copying a deserialized object to an existing instance. To work around this,
+    ///     we implement a 'deep, memberwise copy' method.
+    /// </summary>
+    /// <remarks>TOOD: When System.Text.Json implements `PopulateObject` revisit https://github.com/dotnet/corefx/issues/37627</remarks>
+    /// <param name="source"></param>
+    /// <param name="destination"></param>
+    /// <returns><paramref name="destination"/> updated from <paramref name="source"/></returns>
+    internal static object? DeepMemberwiseCopy (object? source, object? destination)
+    {
+        if (destination is null)
+        {
+            throw new ArgumentNullException (nameof (destination));
+        }
+
+        if (source is null)
+        {
+            return null!;
+        }
+
+        if (source.GetType () == typeof (SettingsScope))
+        {
+            return ((SettingsScope)destination).Update ((SettingsScope)source);
+        }
+
+        if (source.GetType () == typeof (ThemeScope))
+        {
+            return ((ThemeScope)destination).Update ((ThemeScope)source);
+        }
+
+        if (source.GetType () == typeof (AppScope))
+        {
+            return ((AppScope)destination).Update ((AppScope)source);
+        }
+
+        // If value type, just use copy constructor.
+        if (source.GetType ().IsValueType || source.GetType () == typeof (string))
+        {
+            return source;
+        }
+
+        // Dictionary
+        if (source.GetType ().IsGenericType
+            && source.GetType ().GetGenericTypeDefinition ().IsAssignableFrom (typeof (Dictionary<,>)))
+        {
+            foreach (object? srcKey in ((IDictionary)source).Keys)
+            {
+                if (srcKey is string)
+                { }
+
+                if (((IDictionary)destination).Contains (srcKey))
+                {
+                    ((IDictionary)destination) [srcKey] =
+                        DeepMemberwiseCopy (((IDictionary)source) [srcKey], ((IDictionary)destination) [srcKey]);
+                }
+                else
+                {
+                    ((IDictionary)destination).Add (srcKey, ((IDictionary)source) [srcKey]);
+                }
+            }
+
+            return destination;
+        }
+
+        // ALl other object types
+        List<PropertyInfo>? sourceProps = source?.GetType ().GetProperties ().Where (x => x.CanRead).ToList ();
+        List<PropertyInfo>? destProps = destination?.GetType ().GetProperties ().Where (x => x.CanWrite).ToList ()!;
+
+        foreach ((PropertyInfo? sourceProp, PropertyInfo? destProp) in
+                 from sourceProp in sourceProps
+                 where destProps.Any (x => x.Name == sourceProp.Name)
+                 let destProp = destProps.First (x => x.Name == sourceProp.Name)
+                 where destProp.CanWrite
+                 select (sourceProp, destProp))
+        {
+            object? sourceVal = sourceProp.GetValue (source);
+            object? destVal = destProp.GetValue (destination);
+
+            if (sourceVal is { })
+            {
+                try
+                {
+                    if (destVal is { })
+                    {
+                        // Recurse
+                        destProp.SetValue (destination, DeepMemberwiseCopy (sourceVal, destVal));
+                    }
+                    else
+                    {
+                        destProp.SetValue (destination, sourceVal);
+                    }
+                }
+                catch (ArgumentException e)
+                {
+                    throw new JsonException ($"Error Applying Configuration Change: {e.Message}", e);
+                }
+            }
+        }
+
+        return destination!;
+    }
+
+    /// <summary>
+    ///     Retrieves the hard coded default settings from the Terminal.Gui library implementation. Used in development of
+    ///     the library to generate the default configuration file. Before calling Application.Init, make sure
+    ///     <see cref="Locations"/> is set to <see cref="ConfigLocations.None"/>.
+    /// </summary>
+    /// <remarks>
+    ///     <para>
+    ///         This method is only really useful when using ConfigurationManagerTests to generate the JSON doc that is
+    ///         embedded into Terminal.Gui (during development).
+    ///     </para>
+    ///     <para>
+    ///         WARNING: The <c>Terminal.Gui.Resources.config.json</c> resource has setting definitions (Themes) that are NOT
+    ///         generated by this function. If you use this function to regenerate <c>Terminal.Gui.Resources.config.json</c>,
+    ///         make sure you copy the Theme definitions from the existing <c>Terminal.Gui.Resources.config.json</c> file.
+    ///     </para>
+    /// </remarks>
+    internal static void GetHardCodedDefaults ()
+    {
+        if (_allConfigProperties is null)
+        {
+            throw new InvalidOperationException ("Initialize must be called first.");
+        }
+
+        Settings = new SettingsScope ();
+        ThemeManager.GetHardCodedDefaults ();
+        AppSettings?.RetrieveValues ();
+
+        foreach (KeyValuePair<string, ConfigProperty> p in Settings!.Where (cp => cp.Value.PropertyInfo is { }))
+        {
+            Settings! [p.Key].PropertyValue = p.Value.PropertyInfo?.GetValue (null);
+        }
+    }
+
+    /// <summary>
+    ///     Initializes the internal state of ConfigurationManager. Nominally called once as part of application startup
+    ///     to initialize global state. Also called from some Unit Tests to ensure correctness (e.g. Reset()).
+    /// </summary>
+    internal static void Initialize ()
+    {
+        _allConfigProperties = new Dictionary<string, ConfigProperty> ();
+        _settings = null;
+
+        Dictionary<string, Type> classesWithConfigProps = new (StringComparer.InvariantCultureIgnoreCase);
+
+        // Get Terminal.Gui.dll classes
+
+        IEnumerable<Type> types = from assembly in AppDomain.CurrentDomain.GetAssemblies ()
+                                  from type in assembly.GetTypes ()
+                                  where type.GetProperties ()
+                                            .Any (
+                                                  prop => prop.GetCustomAttribute (
+                                                                                   typeof (SerializableConfigurationProperty)
+                                                                                  )
+                                                          != null
+                                                 )
+                                  select type;
+
+        foreach (Type? classWithConfig in types)
+        {
+            classesWithConfigProps.Add (classWithConfig.Name, classWithConfig);
+        }
+
+        Debug.WriteLine ($"ConfigManager.getConfigProperties found {classesWithConfigProps.Count} classes:");
+        classesWithConfigProps.ToList ().ForEach (x => Debug.WriteLine ($"  Class: {x.Key}"));
+
+        foreach (PropertyInfo? p in from c in classesWithConfigProps
+                                    let props = c.Value
+                                                 .GetProperties (
+                                                                 BindingFlags.Instance
+                                                                 | BindingFlags.Static
+                                                                 | BindingFlags.NonPublic
+                                                                 | BindingFlags.Public
+                                                                )
+                                                 .Where (
+                                                         prop =>
+                                                             prop.GetCustomAttribute (
+                                                                                      typeof (SerializableConfigurationProperty)
+                                                                                     ) is
+                                                                 SerializableConfigurationProperty
+                                                        )
+                                    let enumerable = props
+                                    from p in enumerable
+                                    select p)
+        {
+            if (p.GetCustomAttribute (typeof (SerializableConfigurationProperty)) is SerializableConfigurationProperty
+                scp)
+            {
+                if (p.GetGetMethod (true)!.IsStatic)
+                {
+                    // If the class name is omitted, JsonPropertyName is allowed. 
+                    _allConfigProperties!.Add (
+                                               scp.OmitClassName
+                                                   ? ConfigProperty.GetJsonPropertyName (p)
+                                                   : $"{p.DeclaringType?.Name}.{p.Name}",
+                                               new ConfigProperty { PropertyInfo = p, PropertyValue = null }
+                                              );
+                }
+                else
+                {
+                    throw new Exception (
+                                         $"Property {
+                                             p.Name
+                                         } in class {
+                                             p.DeclaringType?.Name
+                                         } is not static. All SerializableConfigurationProperty properties must be static."
+                                        );
+                }
+            }
+        }
+
+        _allConfigProperties = _allConfigProperties!.OrderBy (x => x.Key)
+                                                    .ToDictionary (
+                                                                   x => x.Key,
+                                                                   x => x.Value,
+                                                                   StringComparer.InvariantCultureIgnoreCase
+                                                                  );
+
+        Debug.WriteLine ($"ConfigManager.Initialize found {_allConfigProperties.Count} properties:");
+
+        //_allConfigProperties.ToList ().ForEach (x => Debug.WriteLine ($"  Property: {x.Key}"));
+
+        AppSettings = new AppScope ();
+    }
+
+    /// <summary>Creates a JSON document with the configuration specified.</summary>
+    /// <returns></returns>
+    internal static string ToJson ()
+    {
+        Debug.WriteLine ("ConfigurationManager.ToJson()");
+
+        return JsonSerializer.Serialize (Settings!, _serializerOptions);
+    }
+
+    internal static Stream ToStream ()
+    {
+        string json = JsonSerializer.Serialize (Settings!, _serializerOptions);
+
+        // turn it into a stream
+        var stream = new MemoryStream ();
+        var writer = new StreamWriter (stream);
+        writer.Write (json);
+        writer.Flush ();
+        stream.Position = 0;
+
+        return stream;
+    }
+
+    private static void ClearJsonErrors () { jsonErrors.Clear (); }
+}

+ 15 - 31
Terminal.Gui/Configuration/ConfigurationManagerEventArgs.cs

@@ -1,36 +1,20 @@
-using System;
+#nullable enable
 
-#nullable enable
+namespace Terminal.Gui;
 
-namespace Terminal.Gui {
-	/// <summary>
-	/// Event arguments for the <see cref="ConfigurationManager"/> events.
-	/// </summary>
-	public class ConfigurationManagerEventArgs : EventArgs {
-
-		/// <summary>
-		/// Initializes a new instance of <see cref="ConfigurationManagerEventArgs"/>
-		/// </summary>
-		public ConfigurationManagerEventArgs ()
-		{
-		}
-	}
+/// <summary>Event arguments for the <see cref="ConfigurationManager"/> events.</summary>
+public class ConfigurationManagerEventArgs : EventArgs
+{
+    /// <summary>Initializes a new instance of <see cref="ConfigurationManagerEventArgs"/></summary>
+    public ConfigurationManagerEventArgs () { }
+}
 
-	/// <summary>
-	/// Event arguments for the <see cref="ThemeManager"/> events.
-	/// </summary>
-	public class ThemeManagerEventArgs : EventArgs {
-		/// <summary>
-		/// The name of the new active theme..
-		/// </summary>
-		public string NewTheme { get; set; } = string.Empty;
+/// <summary>Event arguments for the <see cref="ThemeManager"/> events.</summary>
+public class ThemeManagerEventArgs : EventArgs
+{
+    /// <summary>Initializes a new instance of <see cref="ThemeManagerEventArgs"/></summary>
+    public ThemeManagerEventArgs (string newTheme) { NewTheme = newTheme; }
 
-		/// <summary>
-		/// Initializes a new instance of <see cref="ThemeManagerEventArgs"/>
-		/// </summary>
-		public ThemeManagerEventArgs (string newTheme)
-		{
-			NewTheme = newTheme;
-		}
-	}
+    /// <summary>The name of the new active theme..</summary>
+    public string NewTheme { get; set; } = string.Empty;
 }

+ 58 - 41
Terminal.Gui/Configuration/DictionaryJsonConverter.cs

@@ -1,43 +1,60 @@
-using System;
-using System.Collections.Generic;
+using System.Text.Json;
 using System.Text.Json.Serialization;
-using System.Text.Json;
-
-namespace Terminal.Gui {
-	class DictionaryJsonConverter<T> : JsonConverter<Dictionary<string, T>> {
-		public override Dictionary<string, T> Read (ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
-		{
-			if (reader.TokenType != JsonTokenType.StartArray) {
-				throw new JsonException ($"Expected a JSON array (\"[ {{ ... }} ]\"), but got \"{reader.TokenType}\".");
-			}
-
-			var dictionary = new Dictionary<string, T> ();
-			while (reader.Read ()) {
-				if (reader.TokenType == JsonTokenType.StartObject) {
-					reader.Read ();
-					if (reader.TokenType == JsonTokenType.PropertyName) {
-						string key = reader.GetString ();
-						reader.Read ();
-						T value = JsonSerializer.Deserialize<T> (ref reader, options);
-						dictionary.Add (key, value);
-					}
-				} else if (reader.TokenType == JsonTokenType.EndArray)
-					break;
-			}
-			return dictionary;
-		}
-
-		public override void Write (Utf8JsonWriter writer, Dictionary<string, T> value, JsonSerializerOptions options)
-		{
-			writer.WriteStartArray ();
-			foreach (var item in value) {
-				writer.WriteStartObject ();
-				//writer.WriteString (item.Key, item.Key);
-				writer.WritePropertyName (item.Key);
-				JsonSerializer.Serialize (writer, item.Value, options);
-				writer.WriteEndObject ();
-			}
-			writer.WriteEndArray ();
-		}
-	}
+
+namespace Terminal.Gui;
+
+internal class DictionaryJsonConverter<T> : JsonConverter<Dictionary<string, T>>
+{
+    public override Dictionary<string, T> Read (
+        ref Utf8JsonReader reader,
+        Type typeToConvert,
+        JsonSerializerOptions options
+    )
+    {
+        if (reader.TokenType != JsonTokenType.StartArray)
+        {
+            throw new JsonException ($"Expected a JSON array (\"[ {{ ... }} ]\"), but got \"{reader.TokenType}\".");
+        }
+
+        Dictionary<string, T> dictionary = new ();
+
+        while (reader.Read ())
+        {
+            if (reader.TokenType == JsonTokenType.StartObject)
+            {
+                reader.Read ();
+
+                if (reader.TokenType == JsonTokenType.PropertyName)
+                {
+                    string key = reader.GetString ();
+                    reader.Read ();
+                    var value = JsonSerializer.Deserialize<T> (ref reader, options);
+                    dictionary.Add (key, value);
+                }
+            }
+            else if (reader.TokenType == JsonTokenType.EndArray)
+            {
+                break;
+            }
+        }
+
+        return dictionary;
+    }
+
+    public override void Write (Utf8JsonWriter writer, Dictionary<string, T> value, JsonSerializerOptions options)
+    {
+        writer.WriteStartArray ();
+
+        foreach (KeyValuePair<string, T> item in value)
+        {
+            writer.WriteStartObject ();
+
+            //writer.WriteString (item.Key, item.Key);
+            writer.WritePropertyName (item.Key);
+            JsonSerializer.Serialize (writer, item.Value, options);
+            writer.WriteEndObject ();
+        }
+
+        writer.WriteEndArray ();
+    }
 }

+ 164 - 122
Terminal.Gui/Configuration/KeyCodeJsonConverter.cs

@@ -1,126 +1,168 @@
-using System;
-using System.Collections.Generic;
-using System.Text.Json;
+using System.Text.Json;
 using System.Text.Json.Serialization;
 
 namespace Terminal.Gui;
 
-class KeyCodeJsonConverter : JsonConverter<KeyCode> {
-	public override KeyCode Read (ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
-	{
-		if (reader.TokenType == JsonTokenType.StartObject) {
-			KeyCode key = KeyCode.Null;
-			var modifierDict = new Dictionary<string, KeyCode> (comparer: StringComparer.InvariantCultureIgnoreCase) {
-				{ "Shift", KeyCode.ShiftMask },
-				{ "Ctrl", KeyCode.CtrlMask },
-				{ "Alt", KeyCode.AltMask }
-			};
-
-			var modifiers = new List<KeyCode> ();
-
-			while (reader.Read ()) {
-				if (reader.TokenType == JsonTokenType.EndObject) {
-					break;
-				}
-
-				if (reader.TokenType == JsonTokenType.PropertyName) {
-					string propertyName = reader.GetString ();
-					reader.Read ();
-
-					switch (propertyName.ToLowerInvariant ()) {
-					case "key":
-						if (reader.TokenType == JsonTokenType.String) {
-							if (Enum.TryParse (reader.GetString (), false, out key)) {
-								break;
-							}
-
-							// The enum uses "D0..D9" for the number keys
-							if (Enum.TryParse (reader.GetString ().TrimStart ('D', 'd'), false, out key)) {
-								break;
-							}
-
-							if (key == KeyCode.Null) {
-								throw new JsonException ($"The value \"{reader.GetString ()}\" is not a valid Key.");
-							}
-
-						} else if (reader.TokenType == JsonTokenType.Number) {
-							try {
-								key = (KeyCode)reader.GetInt32 ();
-							} catch (InvalidOperationException ioe) {
-								throw new JsonException ($"Error parsing Key value: {ioe.Message}", ioe);
-							} catch (FormatException ioe) {
-								throw new JsonException ($"Error parsing Key value: {ioe.Message}", ioe);
-							}
-							break;
-						}
-						break;
-
-					case "modifiers":
-						if (reader.TokenType == JsonTokenType.StartArray) {
-							while (reader.Read ()) {
-								if (reader.TokenType == JsonTokenType.EndArray) {
-									break;
-								}
-								string mod = reader.GetString ();
-								try {
-									modifiers.Add (modifierDict [mod]);
-								} catch (KeyNotFoundException e) {
-									throw new JsonException ($"The value \"{mod}\" is not a valid modifier.", e);
-								}
-							}
-						} else {
-							throw new JsonException ($"Expected an array of modifiers, but got \"{reader.TokenType}\".");
-						}
-						break;
-
-					default:
-						throw new JsonException ($"Unexpected Key property \"{propertyName}\".");
-					}
-				}
-			}
-
-			foreach (var modifier in modifiers) {
-				key |= modifier;
-			}
-
-			return key;
-		}
-		throw new JsonException ($"Unexpected StartObject token when parsing Key: {reader.TokenType}.");
-	}
-
-	public override void Write (Utf8JsonWriter writer, KeyCode value, JsonSerializerOptions options)
-	{
-		writer.WriteStartObject ();
-
-		string keyName = (value & ~KeyCode.CtrlMask & ~KeyCode.ShiftMask & ~KeyCode.AltMask).ToString ();
-		if (keyName != null) {
-			writer.WriteString ("Key", keyName);
-		} else {
-			writer.WriteNumber ("Key", (uint)(value & ~KeyCode.CtrlMask & ~KeyCode.ShiftMask & ~KeyCode.AltMask));
-		}
-
-		var modifierDict = new Dictionary<string, KeyCode> {
-			{ "Shift", KeyCode.ShiftMask },
-			{ "Ctrl", KeyCode.CtrlMask },
-			{ "Alt", KeyCode.AltMask }
-		};
-
-		var modifiers = new List<string> ();
-		foreach (var pair in modifierDict) {
-			if ((value & pair.Value) == pair.Value) {
-				modifiers.Add (pair.Key);
-			}
-		}
-
-		if (modifiers.Count > 0) {
-			writer.WritePropertyName ("Modifiers");
-			writer.WriteStartArray ();
-			foreach (string modifier in modifiers) {
-				writer.WriteStringValue (modifier);
-			}
-			writer.WriteEndArray ();
-		}
-
-		writer.WriteEndObject ();
-	}
-}
+internal class KeyCodeJsonConverter : JsonConverter<KeyCode>
+{
+    public override KeyCode Read (ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+    {
+        if (reader.TokenType == JsonTokenType.StartObject)
+        {
+            var key = KeyCode.Null;
+
+            Dictionary<string, KeyCode> modifierDict =
+                new (StringComparer.InvariantCultureIgnoreCase)
+                {
+                    { "Shift", KeyCode.ShiftMask }, { "Ctrl", KeyCode.CtrlMask }, { "Alt", KeyCode.AltMask }
+                };
+
+            List<KeyCode> modifiers = new ();
+
+            while (reader.Read ())
+            {
+                if (reader.TokenType == JsonTokenType.EndObject)
+                {
+                    break;
+                }
+
+                if (reader.TokenType == JsonTokenType.PropertyName)
+                {
+                    string propertyName = reader.GetString ();
+                    reader.Read ();
+
+                    switch (propertyName.ToLowerInvariant ())
+                    {
+                        case "key":
+                            if (reader.TokenType == JsonTokenType.String)
+                            {
+                                if (Enum.TryParse (reader.GetString (), false, out key))
+                                {
+                                    break;
+                                }
+
+                                // The enum uses "D0..D9" for the number keys
+                                if (Enum.TryParse (reader.GetString ().TrimStart ('D', 'd'), false, out key))
+                                {
+                                    break;
+                                }
+
+                                if (key == KeyCode.Null)
+                                {
+                                    throw new JsonException (
+                                                             $"The value \"{reader.GetString ()}\" is not a valid Key."
+                                                            );
+                                }
+                            }
+                            else if (reader.TokenType == JsonTokenType.Number)
+                            {
+                                try
+                                {
+                                    key = (KeyCode)reader.GetInt32 ();
+                                }
+                                catch (InvalidOperationException ioe)
+                                {
+                                    throw new JsonException ($"Error parsing Key value: {ioe.Message}", ioe);
+                                }
+                                catch (FormatException ioe)
+                                {
+                                    throw new JsonException ($"Error parsing Key value: {ioe.Message}", ioe);
+                                }
+                            }
+
+                            break;
+
+                        case "modifiers":
+                            if (reader.TokenType == JsonTokenType.StartArray)
+                            {
+                                while (reader.Read ())
+                                {
+                                    if (reader.TokenType == JsonTokenType.EndArray)
+                                    {
+                                        break;
+                                    }
+
+                                    string mod = reader.GetString ();
+
+                                    try
+                                    {
+                                        modifiers.Add (modifierDict [mod]);
+                                    }
+                                    catch (KeyNotFoundException e)
+                                    {
+                                        throw new JsonException ($"The value \"{mod}\" is not a valid modifier.", e);
+                                    }
+                                }
+                            }
+                            else
+                            {
+                                throw new JsonException (
+                                                         $"Expected an array of modifiers, but got \"{reader.TokenType}\"."
+                                                        );
+                            }
+
+                            break;
+
+                        default:
+                            throw new JsonException ($"Unexpected Key property \"{propertyName}\".");
+                    }
+                }
+            }
+
+            foreach (KeyCode modifier in modifiers)
+            {
+                key |= modifier;
+            }
+
+            return key;
+        }
+
+        throw new JsonException ($"Unexpected StartObject token when parsing Key: {reader.TokenType}.");
+    }
+
+    public override void Write (Utf8JsonWriter writer, KeyCode value, JsonSerializerOptions options)
+    {
+        writer.WriteStartObject ();
+
+        var keyName = (value & ~KeyCode.CtrlMask & ~KeyCode.ShiftMask & ~KeyCode.AltMask).ToString ();
+
+        if (keyName is { })
+        {
+            writer.WriteString ("Key", keyName);
+        }
+        else
+        {
+            writer.WriteNumber ("Key", (uint)(value & ~KeyCode.CtrlMask & ~KeyCode.ShiftMask & ~KeyCode.AltMask));
+        }
+
+        Dictionary<string, KeyCode> modifierDict = new ()
+        {
+            { "Shift", KeyCode.ShiftMask }, { "Ctrl", KeyCode.CtrlMask }, { "Alt", KeyCode.AltMask }
+        };
+
+        List<string> modifiers = new ();
+
+        foreach (KeyValuePair<string, KeyCode> pair in modifierDict)
+        {
+            if ((value & pair.Value) == pair.Value)
+            {
+                modifiers.Add (pair.Key);
+            }
+        }
+
+        if (modifiers.Count > 0)
+        {
+            writer.WritePropertyName ("Modifiers");
+            writer.WriteStartArray ();
+
+            foreach (string modifier in modifiers)
+            {
+                writer.WriteStringValue (modifier);
+            }
+
+            writer.WriteEndArray ();
+        }
+
+        writer.WriteEndObject ();
+    }
+}

+ 12 - 11
Terminal.Gui/Configuration/KeyJsonConverter.cs

@@ -1,16 +1,17 @@
-using System;
-using System.Text.Json;
+using System.Text.Json;
 using System.Text.Json.Serialization;
 
 namespace Terminal.Gui;
 
-/// <summary>
-/// Support for <see cref="Key"/> in JSON in the form of "Ctrl-X" or "Alt-Shift-F1".
-/// </summary>
-public class KeyJsonConverter : JsonConverter<Key> {
-	/// <inheritdoc />
-	public override Key Read (ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => Key.TryParse (reader.GetString (), out var key) ? key : Key.Empty;
+/// <summary>Support for <see cref="Key"/> in JSON in the form of "Ctrl-X" or "Alt-Shift-F1".</summary>
+public class KeyJsonConverter : JsonConverter<Key>
+{
+    /// <inheritdoc/>
+    public override Key Read (ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+    {
+        return Key.TryParse (reader.GetString (), out Key key) ? key : Key.Empty;
+    }
 
-	/// <inheritdoc />
-	public override void Write (Utf8JsonWriter writer, Key value, JsonSerializerOptions options) => writer.WriteStringValue (value.ToString ());
-}
+    /// <inheritdoc/>
+    public override void Write (Utf8JsonWriter writer, Key value, JsonSerializerOptions options) { writer.WriteStringValue (value.ToString ()); }
+}

+ 136 - 112
Terminal.Gui/Configuration/RuneJsonConverter.cs

@@ -1,121 +1,145 @@
-using System;
-using System.Globalization;
-using System.Linq;
-using System.Text;
+using System.Globalization;
 using System.Text.Json;
 using System.Text.Json.Serialization;
 using System.Text.RegularExpressions;
 
 namespace Terminal.Gui;
+
 /// <summary>
-/// Json converter for <see cref="Rune"/>. Supports
-/// Json converter for <see cref="Rune"/>. Supports
-/// A string as one of:
-/// - unicode char (e.g. "☑")
-/// - U+hex format (e.g. "U+2611")
-/// - \u format (e.g. "\\u2611")
-/// A number
-/// - The unicode code in decimal
+///     Json converter for <see cref="Rune"/>. Supports Json converter for <see cref="Rune"/>. Supports A string as
+///     one of: - unicode char (e.g. "☑") - U+hex format (e.g. "U+2611") - \u format (e.g. "\\u2611") A number - The
+///     unicode code in decimal
 /// </summary>
-internal class RuneJsonConverter : JsonConverter<Rune> {
-	public override Rune Read (ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
-	{
-		switch (reader.TokenType) {
-		case JsonTokenType.String: {
-				var value = reader.GetString ();
-				int first = RuneExtensions.MaxUnicodeCodePoint + 1;
-				int second = RuneExtensions.MaxUnicodeCodePoint + 1;
-
-				if (value.StartsWith ("U+", StringComparison.OrdinalIgnoreCase) || value.StartsWith ("\\U", StringComparison.OrdinalIgnoreCase)) {
-					// Handle encoded single char, surrogate pair, or combining mark + char
-					var codePoints = Regex.Matches (value, @"(?:\\[uU]\+?|U\+)([0-9A-Fa-f]{1,8})")
-						.Cast<Match> ()
-						.Select (match => uint.Parse (match.Groups [1].Value, NumberStyles.HexNumber))
-						.ToArray ();
-
-					if (codePoints.Length == 0 || codePoints.Length > 2) {
-						throw new JsonException ($"Invalid Rune: {value}.");
-					}
-
-					if (codePoints.Length > 0) {
-						first = (int)codePoints [0];
-					}
-
-					if (codePoints.Length == 2) {
-						second = (int)codePoints [1];
-					}
-				} else {
-					// Handle single character, surrogate pair, or combining mark + char
-					if (value.Length == 0 || value.Length > 2) {
-						throw new JsonException ($"Invalid Rune: {value}.");
-					}
-
-					if (value.Length > 0) {
-						first = value [0];
-					}
-					if (value.Length == 2) {
-						second = value [1];
-					}
-				}
-
-				Rune result;
-				if (second == RuneExtensions.MaxUnicodeCodePoint + 1) {
-					// Single codepoint
-					if (!Rune.TryCreate (first, out result)) {
-						throw new JsonException ($"Invalid Rune: {value}.");
-					}
-					return result;
-				}
-
-				// Surrogate pair?
-				if (Rune.TryCreate ((char)first, (char)second, out result)) {
-					return result;
-				}
-
-				if (!Rune.IsValid (second)) {
-					throw new JsonException ($"The second codepoint is not valid: {second} in ({value})");
-				}
-
-				var cm = new Rune (second);
-				if (!cm.IsCombiningMark ()) {
-					throw new JsonException ($"The second codepoint is not a combining mark: {cm} in ({value})");
-				}
-
-				// not a surrogate pair, so a combining mark + char?
-				var combined = string.Concat ((char)first, (char)second).Normalize ();
-
-				if (!Rune.IsValid (combined [0])) {
-					throw new JsonException ($"Invalid combined Rune ({value})");
-				}
-
-				return new Rune (combined [0]);
-			}
-		case JsonTokenType.Number: {
-				uint num = reader.GetUInt32 ();
-				if (Rune.IsValid (num)) {
-					return new Rune (num);
-				}
-				throw new JsonException ($"Invalid Rune (not a scalar Unicode value): {num}.");
-			}
-		default:
-			throw new JsonException ($"Unexpected token when parsing Rune: {reader.TokenType}.");
-		}
-	}
-
-	public override void Write (Utf8JsonWriter writer, Rune value, JsonSerializerOptions options)
-	{
-		// HACK: Writes a JSON comment in addition to the glyph to ease debugging.
-		// Technically, JSON comments are not valid, but we use relaxed decoding
-		// (ReadCommentHandling = JsonCommentHandling.Skip)
-		//writer.WriteCommentValue ($"(U+{value.Value:X8})");
-		//var printable = value.MakePrintable ();
-		//if (printable == Rune.ReplacementChar) {
-		//	writer.WriteStringValue (value.ToString ());
-		//} else {
-		//	//writer.WriteRawValue ($"\"{value}\"");
-		//}
-
-		writer.WriteNumberValue (value.Value);
-	}
+internal class RuneJsonConverter : JsonConverter<Rune>
+{
+    public override Rune Read (ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+    {
+        switch (reader.TokenType)
+        {
+            case JsonTokenType.String:
+            {
+                string value = reader.GetString ();
+                int first = RuneExtensions.MaxUnicodeCodePoint + 1;
+                int second = RuneExtensions.MaxUnicodeCodePoint + 1;
+
+                if (value.StartsWith ("U+", StringComparison.OrdinalIgnoreCase)
+                    || value.StartsWith ("\\U", StringComparison.OrdinalIgnoreCase))
+                {
+                    // Handle encoded single char, surrogate pair, or combining mark + char
+                    uint [] codePoints = Regex.Matches (value, @"(?:\\[uU]\+?|U\+)([0-9A-Fa-f]{1,8})")
+                                              .Select (
+                                                       match => uint.Parse (
+                                                                            match.Groups [1].Value,
+                                                                            NumberStyles.HexNumber
+                                                                           )
+                                                      )
+                                              .ToArray ();
+
+                    if (codePoints.Length == 0 || codePoints.Length > 2)
+                    {
+                        throw new JsonException ($"Invalid Rune: {value}.");
+                    }
+
+                    if (codePoints.Length > 0)
+                    {
+                        first = (int)codePoints [0];
+                    }
+
+                    if (codePoints.Length == 2)
+                    {
+                        second = (int)codePoints [1];
+                    }
+                }
+                else
+                {
+                    // Handle single character, surrogate pair, or combining mark + char
+                    if (value.Length == 0 || value.Length > 2)
+                    {
+                        throw new JsonException ($"Invalid Rune: {value}.");
+                    }
+
+                    if (value.Length > 0)
+                    {
+                        first = value [0];
+                    }
+
+                    if (value.Length == 2)
+                    {
+                        second = value [1];
+                    }
+                }
+
+                Rune result;
+
+                if (second == RuneExtensions.MaxUnicodeCodePoint + 1)
+                {
+                    // Single codepoint
+                    if (!Rune.TryCreate (first, out result))
+                    {
+                        throw new JsonException ($"Invalid Rune: {value}.");
+                    }
+
+                    return result;
+                }
+
+                // Surrogate pair?
+                if (Rune.TryCreate ((char)first, (char)second, out result))
+                {
+                    return result;
+                }
+
+                if (!Rune.IsValid (second))
+                {
+                    throw new JsonException ($"The second codepoint is not valid: {second} in ({value})");
+                }
+
+                var cm = new Rune (second);
+
+                if (!cm.IsCombiningMark ())
+                {
+                    throw new JsonException ($"The second codepoint is not a combining mark: {cm} in ({value})");
+                }
+
+                // not a surrogate pair, so a combining mark + char?
+                string combined = string.Concat ((char)first, (char)second).Normalize ();
+
+                if (!Rune.IsValid (combined [0]))
+                {
+                    throw new JsonException ($"Invalid combined Rune ({value})");
+                }
+
+                return new Rune (combined [0]);
+            }
+            case JsonTokenType.Number:
+            {
+                uint num = reader.GetUInt32 ();
+
+                if (Rune.IsValid (num))
+                {
+                    return new Rune (num);
+                }
+
+                throw new JsonException ($"Invalid Rune (not a scalar Unicode value): {num}.");
+            }
+            default:
+                throw new JsonException ($"Unexpected token when parsing Rune: {reader.TokenType}.");
+        }
+    }
+
+    public override void Write (Utf8JsonWriter writer, Rune value, JsonSerializerOptions options)
+    {
+        // HACK: Writes a JSON comment in addition to the glyph to ease debugging.
+        // Technically, JSON comments are not valid, but we use relaxed decoding
+        // (ReadCommentHandling = JsonCommentHandling.Skip)
+        //writer.WriteCommentValue ($"(U+{value.Value:X8})");
+        //var printable = value.MakePrintable ();
+        //if (printable == Rune.ReplacementChar) {
+        //	writer.WriteStringValue (value.ToString ());
+        //} else {
+        //	//writer.WriteRawValue ($"\"{value}\"");
+        //}
+
+        writer.WriteNumberValue (value.Value);
+    }
 }
 #pragma warning restore 1591

+ 72 - 67
Terminal.Gui/Configuration/Scope.cs

@@ -1,76 +1,81 @@
-using System;
-using System.Collections;
-using System.Collections.Generic;
-using System.Linq;
+#nullable enable
 using System.Reflection;
-using static Terminal.Gui.ConfigurationManager;
 
-#nullable enable
+namespace Terminal.Gui;
 
-namespace Terminal.Gui {
+/// <summary>
+///     Defines a configuration settings scope. Classes that inherit from this abstract class can be used to define
+///     scopes for configuration settings. Each scope is a JSON object that contains a set of configuration settings.
+/// </summary>
+public class Scope<T> : Dictionary<string, ConfigProperty>
+{ //, IScope<Scope<T>> {
+    /// <summary>Crates a new instance.</summary>
+    public Scope () : base (StringComparer.InvariantCultureIgnoreCase)
+    {
+        foreach (KeyValuePair<string, ConfigProperty> p in GetScopeProperties ())
+        {
+            Add (p.Key, new ConfigProperty { PropertyInfo = p.Value.PropertyInfo, PropertyValue = null });
+        }
+    }
 
-	/// <summary>
-	/// Defines a configuration settings scope. Classes that inherit from this abstract class can be used to define
-	/// scopes for configuration settings. Each scope is a JSON object that contains a set of configuration settings.
-	/// </summary>
-	public class Scope<T> : Dictionary<string, ConfigProperty> { //, IScope<Scope<T>> {
-		/// <summary>
-		/// Crates a new instance.
-		/// </summary>
-		public Scope () : base (StringComparer.InvariantCultureIgnoreCase)
-		{
-			foreach (var p in GetScopeProperties ()) {
-				Add (p.Key, new ConfigProperty () { PropertyInfo = p.Value.PropertyInfo, PropertyValue = null });
-			}
-		}
+    /// <summary>Retrieves the values of the properties of this scope from their corresponding static properties.</summary>
+    public void RetrieveValues ()
+    {
+        foreach (KeyValuePair<string, ConfigProperty> p in this.Where (cp => cp.Value.PropertyInfo is { }))
+        {
+            p.Value.RetrieveValue ();
+        }
+    }
 
-		private IEnumerable<KeyValuePair<string, ConfigProperty>> GetScopeProperties ()
-		{
-			return ConfigurationManager._allConfigProperties!.Where (cp =>
-				(cp.Value.PropertyInfo?.GetCustomAttribute (typeof (SerializableConfigurationProperty))
-				as SerializableConfigurationProperty)?.Scope == GetType ());
-		}
+    /// <summary>Updates this instance from the specified source scope.</summary>
+    /// <param name="source"></param>
+    /// <returns>The updated scope (this).</returns>
+    public Scope<T>? Update (Scope<T> source)
+    {
+        foreach (KeyValuePair<string, ConfigProperty> prop in source)
+        {
+            if (ContainsKey (prop.Key))
+            {
+                this [prop.Key].PropertyValue = this [prop.Key].UpdateValueFrom (prop.Value.PropertyValue!);
+            }
+            else
+            {
+                this [prop.Key].PropertyValue = prop.Value.PropertyValue;
+            }
+        }
 
-		/// <summary>
-		/// Updates this instance from the specified source scope.
-		/// </summary>
-		/// <param name="source"></param>
-		/// <returns>The updated scope (this).</returns>
-		public Scope<T>? Update (Scope<T> source)
-		{
-			foreach (var prop in source) {
-				if (ContainsKey (prop.Key))
-					this [prop.Key].PropertyValue = this [prop.Key].UpdateValueFrom (prop.Value.PropertyValue!);
-				else {
-					this [prop.Key].PropertyValue = prop.Value.PropertyValue;
-				}
-			}
-			return this;
-		}
+        return this;
+    }
 
-		/// <summary>
-		/// Retrieves the values of the properties of this scope from their corresponding static properties.
-		/// </summary>
-		public void RetrieveValues ()
-		{
-			foreach (var p in this.Where (cp => cp.Value.PropertyInfo != null)) {
-				p.Value.RetrieveValue ();
-			}
-		}
+    /// <summary>Applies the values of the properties of this scope to their corresponding static properties.</summary>
+    /// <returns></returns>
+    internal virtual bool Apply ()
+    {
+        var set = false;
 
-		/// <summary>
-		/// Applies the values of the properties of this scope to their corresponding static properties.
-		/// </summary>
-		/// <returns></returns>
-		internal virtual bool Apply ()
-		{
-			bool set = false;
-			foreach (var p in this.Where (t => t.Value != null && t.Value.PropertyValue != null)) {
-				if (p.Value.Apply ()) {
-					set = true;
-				}
-			}
-			return set;
-		}
-	}
+        foreach (KeyValuePair<string, ConfigProperty> p in this.Where (
+                                                                       t => t.Value != null
+                                                                            && t.Value.PropertyValue != null
+                                                                      ))
+        {
+            if (p.Value.Apply ())
+            {
+                set = true;
+            }
+        }
+
+        return set;
+    }
+
+    private IEnumerable<KeyValuePair<string, ConfigProperty>> GetScopeProperties ()
+    {
+        return _allConfigProperties!.Where (
+                                            cp =>
+                                                (cp.Value.PropertyInfo?.GetCustomAttribute (
+                                                                                            typeof (SerializableConfigurationProperty)
+                                                                                           )
+                                                     as SerializableConfigurationProperty)?.Scope
+                                                == GetType ()
+                                           );
+    }
 }

+ 223 - 130
Terminal.Gui/Configuration/ScopeJsonConverter.cs

@@ -1,140 +1,233 @@
-using System;
-using System.Linq;
+#nullable enable
+using System.Diagnostics;
 using System.Reflection;
 using System.Text.Json;
 using System.Text.Json.Serialization;
 
-#nullable enable
-
 namespace Terminal.Gui;
 
 /// <summary>
-/// Converts <see cref="Scope{T}"/> instances to/from JSON. Does all the heavy lifting of reading/writing
-/// config data to/from <see cref="ConfigurationManager"/> JSON documents.
+///     Converts <see cref="Scope{T}"/> instances to/from JSON. Does all the heavy lifting of reading/writing config
+///     data to/from <see cref="ConfigurationManager"/> JSON documents.
 /// </summary>
 /// <typeparam name="scopeT"></typeparam>
-internal class ScopeJsonConverter<scopeT> : JsonConverter<scopeT> where scopeT : Scope<scopeT> {
-	// See: https://stackoverflow.com/questions/60830084/how-to-pass-an-argument-by-reference-using-reflection
-	internal abstract class ReadHelper {
-		public abstract object? Read (ref Utf8JsonReader reader, Type type, JsonSerializerOptions options);
-	}
-
-	internal class ReadHelper<converterT> : ReadHelper {
-		private readonly ReadDelegate _readDelegate;
-		private delegate converterT ReadDelegate (ref Utf8JsonReader reader, Type type, JsonSerializerOptions options);
-		public ReadHelper (object converter)
-			=> _readDelegate = (ReadDelegate)Delegate.CreateDelegate (typeof (ReadDelegate), converter, "Read");
-		public override object? Read (ref Utf8JsonReader reader, Type type, JsonSerializerOptions options)
-			=> _readDelegate.Invoke (ref reader, type, options);
-	}
-
-	public override scopeT Read (ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
-	{
-		if (reader.TokenType != JsonTokenType.StartObject) {
-			throw new JsonException ($"Expected a JSON object (\"{{ \"propName\" : ... }}\"), but got \"{reader.TokenType}\".");
-		}
-
-		var scope = (scopeT)Activator.CreateInstance (typeof (scopeT))!;
-		while (reader.Read ()) {
-			if (reader.TokenType == JsonTokenType.EndObject) {
-				return scope!;
-			}
-			if (reader.TokenType != JsonTokenType.PropertyName) {
-				throw new JsonException ($"Expected a JSON property name, but got \"{reader.TokenType}\".");
-			}
-			var propertyName = reader.GetString ();
-			reader.Read ();
-
-			if (propertyName != null && scope!.TryGetValue (propertyName, out var configProp)) {
-				// This property name was found in the Scope's ScopeProperties dictionary
-				// Figure out if it needs a JsonConverter and if so, create one
-				var propertyType = configProp?.PropertyInfo?.PropertyType!;
-				if (configProp?.PropertyInfo?.GetCustomAttribute (typeof (JsonConverterAttribute)) is JsonConverterAttribute jca) {
-					var converter = Activator.CreateInstance (jca.ConverterType!)!;
-					if (converter.GetType ().BaseType == typeof (JsonConverterFactory)) {
-						var factory = (JsonConverterFactory)converter;
-						if (propertyType != null && factory.CanConvert (propertyType)) {
-							converter = factory.CreateConverter (propertyType, options);
-						}
-					}
-					var readHelper = Activator.CreateInstance ((Type?)typeof (ReadHelper<>).MakeGenericType (typeof (scopeT), propertyType!)!, converter) as ReadHelper;
-					try {
-						scope! [propertyName].PropertyValue = readHelper?.Read (ref reader, propertyType!, options);
-					} catch (NotSupportedException e) {
-						throw new JsonException ($"Error reading property \"{propertyName}\" of type \"{propertyType?.Name}\".", e);
-					}
-				} else {
-					try {
-						scope! [propertyName].PropertyValue = JsonSerializer.Deserialize (ref reader, propertyType!, options);
-					} catch (Exception ex) {
-						System.Diagnostics.Debug.WriteLine ($"scopeT Read: {ex}");
-					}
-				}
-			} else {
-				// It is not a config property. Maybe it's just a property on the Scope with [JsonInclude]
-				// like ScopeSettings.$schema...
-				var property = scope!.GetType ().GetProperties ().Where (p => {
-					var jia = p.GetCustomAttribute (typeof (JsonIncludeAttribute)) as JsonIncludeAttribute;
-					if (jia != null) {
-						var jpna = p.GetCustomAttribute (typeof (JsonPropertyNameAttribute)) as JsonPropertyNameAttribute;
-						if (jpna?.Name == propertyName) {
-							// Bit of a hack, modifying propertyName in an enumerator...
-							propertyName = p.Name;
-							return true;
-						}
-
-						return p.Name == propertyName;
-					}
-					return false;
-				}).FirstOrDefault ();
-
-				if (property != null) {
-					var prop = scope.GetType ().GetProperty (propertyName!)!;
-					prop.SetValue (scope, JsonSerializer.Deserialize (ref reader, prop.PropertyType, options));
-				} else {
-					// Unknown property
-					throw new JsonException ($"Unknown property name \"{propertyName}\".");
-				}
-			}
-		}
-		throw new JsonException ();
-	}
-
-	public override void Write (Utf8JsonWriter writer, scopeT scope, JsonSerializerOptions options)
-	{
-		writer.WriteStartObject ();
-
-		var properties = scope!.GetType ().GetProperties ().Where (p => p.GetCustomAttribute (typeof (JsonIncludeAttribute)) != null);
-		foreach (var p in properties) {
-			writer.WritePropertyName (ConfigProperty.GetJsonPropertyName (p));
-			JsonSerializer.Serialize (writer, scope.GetType ().GetProperty (p.Name)?.GetValue (scope), options);
-		}
-
-		foreach (var p in from p in scope
-					.Where (cp =>
-					cp.Value.PropertyInfo?.GetCustomAttribute (typeof (SerializableConfigurationProperty)) is
-					SerializableConfigurationProperty scp && scp?.Scope == typeof (scopeT))
-					where p.Value.PropertyValue != null
-					select p) {
-
-			writer.WritePropertyName (p.Key);
-			var propertyType = p.Value.PropertyInfo?.PropertyType;
-
-			if (propertyType != null && p.Value.PropertyInfo?.GetCustomAttribute (typeof (JsonConverterAttribute)) is JsonConverterAttribute jca) {
-				var converter = Activator.CreateInstance (jca.ConverterType!)!;
-				if (converter.GetType ().BaseType == typeof (JsonConverterFactory)) {
-					var factory = (JsonConverterFactory)converter;
-					if (factory.CanConvert (propertyType)) {
-						converter = factory.CreateConverter (propertyType, options)!;
-					}
-				}
-				if (p.Value.PropertyValue != null) {
-					converter.GetType ().GetMethod ("Write")?.Invoke (converter, new object [] { writer, p.Value.PropertyValue, options });
-				}
-			} else {
-				JsonSerializer.Serialize (writer, p.Value.PropertyValue, options);
-			}
-		}
-		writer.WriteEndObject ();
-	}
+internal class ScopeJsonConverter<scopeT> : JsonConverter<scopeT> where scopeT : Scope<scopeT>
+{
+    public override scopeT Read (ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+    {
+        if (reader.TokenType != JsonTokenType.StartObject)
+        {
+            throw new JsonException (
+                                     $"Expected a JSON object (\"{{ \"propName\" : ... }}\"), but got \"{reader.TokenType}\"."
+                                    );
+        }
+
+        var scope = (scopeT)Activator.CreateInstance (typeof (scopeT))!;
+
+        while (reader.Read ())
+        {
+            if (reader.TokenType == JsonTokenType.EndObject)
+            {
+                return scope!;
+            }
+
+            if (reader.TokenType != JsonTokenType.PropertyName)
+            {
+                throw new JsonException ($"Expected a JSON property name, but got \"{reader.TokenType}\".");
+            }
+
+            string? propertyName = reader.GetString ();
+            reader.Read ();
+
+            if (propertyName is { } && scope!.TryGetValue (propertyName, out ConfigProperty? configProp))
+            {
+                // This property name was found in the Scope's ScopeProperties dictionary
+                // Figure out if it needs a JsonConverter and if so, create one
+                Type? propertyType = configProp?.PropertyInfo?.PropertyType!;
+
+                if (configProp?.PropertyInfo?.GetCustomAttribute (typeof (JsonConverterAttribute)) is
+                    JsonConverterAttribute jca)
+                {
+                    object? converter = Activator.CreateInstance (jca.ConverterType!)!;
+
+                    if (converter.GetType ().BaseType == typeof (JsonConverterFactory))
+                    {
+                        var factory = (JsonConverterFactory)converter;
+
+                        if (propertyType is { } && factory.CanConvert (propertyType))
+                        {
+                            converter = factory.CreateConverter (propertyType, options);
+                        }
+                    }
+
+                    var readHelper = Activator.CreateInstance (
+                                                               (Type?)typeof (ReadHelper<>).MakeGenericType (
+                                                                    typeof (scopeT),
+                                                                    propertyType!
+                                                                   )!,
+                                                               converter
+                                                              ) as ReadHelper;
+
+                    try
+                    {
+                        scope! [propertyName].PropertyValue = readHelper?.Read (ref reader, propertyType!, options);
+                    }
+                    catch (NotSupportedException e)
+                    {
+                        throw new JsonException (
+                                                 $"Error reading property \"{propertyName}\" of type \"{propertyType?.Name}\".",
+                                                 e
+                                                );
+                    }
+                }
+                else
+                {
+                    try
+                    {
+                        scope! [propertyName].PropertyValue =
+                            JsonSerializer.Deserialize (ref reader, propertyType!, options);
+                    }
+                    catch (Exception ex)
+                    {
+                        Debug.WriteLine ($"scopeT Read: {ex}");
+                    }
+                }
+            }
+            else
+            {
+                // It is not a config property. Maybe it's just a property on the Scope with [JsonInclude]
+                // like ScopeSettings.$schema...
+                PropertyInfo? property = scope!.GetType ()
+                                               .GetProperties ()
+                                               .Where (
+                                                       p =>
+                                                       {
+                                                           var jia =
+                                                               p.GetCustomAttribute (typeof (JsonIncludeAttribute)) as
+                                                                   JsonIncludeAttribute;
+
+                                                           if (jia is { })
+                                                           {
+                                                               var jpna =
+                                                                   p.GetCustomAttribute (
+                                                                                         typeof (JsonPropertyNameAttribute)
+                                                                                        ) as
+                                                                       JsonPropertyNameAttribute;
+
+                                                               if (jpna?.Name == propertyName)
+                                                               {
+                                                                   // Bit of a hack, modifying propertyName in an enumerator...
+                                                                   propertyName = p.Name;
+
+                                                                   return true;
+                                                               }
+
+                                                               return p.Name == propertyName;
+                                                           }
+
+                                                           return false;
+                                                       }
+                                                      )
+                                               .FirstOrDefault ();
+
+                if (property is { })
+                {
+                    PropertyInfo prop = scope.GetType ().GetProperty (propertyName!)!;
+                    prop.SetValue (scope, JsonSerializer.Deserialize (ref reader, prop.PropertyType, options));
+                }
+                else
+                {
+                    // Unknown property
+                    throw new JsonException ($"Unknown property name \"{propertyName}\".");
+                }
+            }
+        }
+
+        throw new JsonException ();
+    }
+
+    public override void Write (Utf8JsonWriter writer, scopeT scope, JsonSerializerOptions options)
+    {
+        writer.WriteStartObject ();
+
+        IEnumerable<PropertyInfo> properties = scope!.GetType ()
+                                                     .GetProperties ()
+                                                     .Where (
+                                                             p => p.GetCustomAttribute (typeof (JsonIncludeAttribute))
+                                                                  != null
+                                                            );
+
+        foreach (PropertyInfo p in properties)
+        {
+            writer.WritePropertyName (ConfigProperty.GetJsonPropertyName (p));
+            JsonSerializer.Serialize (writer, scope.GetType ().GetProperty (p.Name)?.GetValue (scope), options);
+        }
+
+        foreach (KeyValuePair<string, ConfigProperty> p in from p in scope
+                                                               .Where (
+                                                                       cp =>
+                                                                           cp.Value.PropertyInfo?.GetCustomAttribute (
+                                                                                    typeof (
+                                                                                        SerializableConfigurationProperty)
+                                                                                   )
+                                                                               is
+                                                                               SerializableConfigurationProperty scp
+                                                                           && scp?.Scope == typeof (scopeT)
+                                                                      )
+                                                           where p.Value.PropertyValue != null
+                                                           select p)
+        {
+            writer.WritePropertyName (p.Key);
+            Type? propertyType = p.Value.PropertyInfo?.PropertyType;
+
+            if (propertyType != null
+                && p.Value.PropertyInfo?.GetCustomAttribute (typeof (JsonConverterAttribute)) is JsonConverterAttribute
+                    jca)
+            {
+                object converter = Activator.CreateInstance (jca.ConverterType!)!;
+
+                if (converter.GetType ().BaseType == typeof (JsonConverterFactory))
+                {
+                    var factory = (JsonConverterFactory)converter;
+
+                    if (factory.CanConvert (propertyType))
+                    {
+                        converter = factory.CreateConverter (propertyType, options)!;
+                    }
+                }
+
+                if (p.Value.PropertyValue is { })
+                {
+                    converter.GetType ()
+                             .GetMethod ("Write")
+                             ?.Invoke (converter, new [] { writer, p.Value.PropertyValue, options });
+                }
+            }
+            else
+            {
+                JsonSerializer.Serialize (writer, p.Value.PropertyValue, options);
+            }
+        }
+
+        writer.WriteEndObject ();
+    }
+
+    // See: https://stackoverflow.com/questions/60830084/how-to-pass-an-argument-by-reference-using-reflection
+    internal abstract class ReadHelper
+    {
+        public abstract object? Read (ref Utf8JsonReader reader, Type type, JsonSerializerOptions options);
+    }
+
+    internal class ReadHelper<converterT> : ReadHelper
+    {
+        private readonly ReadDelegate _readDelegate;
+        public ReadHelper (object converter) { _readDelegate = (ReadDelegate)Delegate.CreateDelegate (typeof (ReadDelegate), converter, "Read"); }
+
+        public override object? Read (ref Utf8JsonReader reader, Type type, JsonSerializerOptions options)
+        {
+            return _readDelegate.Invoke (ref reader, type, options);
+        }
+
+        private delegate converterT ReadDelegate (ref Utf8JsonReader reader, Type type, JsonSerializerOptions options);
+    }
 }

+ 16 - 22
Terminal.Gui/Configuration/SerializableConfigurationProperty.cs

@@ -1,28 +1,22 @@
-using System;
-
-#nullable enable
+#nullable enable
 
 namespace Terminal.Gui;
 
-/// <summary>
-/// An attribute that can be applied to a property to indicate that it should included in the configuration file.
-/// </summary>
+/// <summary>An attribute that can be applied to a property to indicate that it should included in the configuration file.</summary>
 /// <example>
-/// 	[SerializableConfigurationProperty(Scope = typeof(Configuration.ThemeManager.ThemeScope)), JsonConverter (typeof (JsonStringEnumConverter))]
-///	public static LineStyle DefaultBorderStyle {
-///	...
+///     [SerializableConfigurationProperty(Scope = typeof(Configuration.ThemeManager.ThemeScope)), JsonConverter
+///     (typeof (JsonStringEnumConverter))] public static LineStyle DefaultBorderStyle { ...
 /// </example>
-[AttributeUsage (AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
-public class SerializableConfigurationProperty : System.Attribute {
-	/// <summary>
-	/// Specifies the scope of the property. 
-	/// </summary>
-	public Type? Scope { get; set; }
+[AttributeUsage (AttributeTargets.Property)]
+public class SerializableConfigurationProperty : System.Attribute
+{
+    /// <summary>
+    ///     If <see langword="true"/>, the property will be serialized to the configuration file using only the property
+    ///     name as the key. If <see langword="false"/>, the property will be serialized to the configuration file using the
+    ///     property name pre-pended with the classname (e.g. <c>Application.UseSystemConsole</c>).
+    /// </summary>
+    public bool OmitClassName { get; set; }
 
-	/// <summary>
-	/// If <see langword="true"/>, the property will be serialized to the configuration file using only the property name
-	/// as the key. If <see langword="false"/>, the property will be serialized to the configuration file using the
-	/// property name pre-pended with the classname (e.g. <c>Application.UseSystemConsole</c>).
-	/// </summary>
-	public bool OmitClassName { get; set; }
-}
+    /// <summary>Specifies the scope of the property.</summary>
+    public Type? Scope { get; set; }
+}

+ 109 - 103
Terminal.Gui/Configuration/SettingsScope.cs

@@ -1,20 +1,17 @@
-using System;
-using System.Collections.Generic;
+#nullable enable
 using System.Diagnostics;
-using System.IO;
 using System.Reflection;
 using System.Text.Json;
 using System.Text.Json.Serialization;
 
-#nullable enable
-
 namespace Terminal.Gui;
 
 /// <summary>
-/// The root object of Terminal.Gui configuration settings / JSON schema. Contains only properties 
-/// attributed with  <see cref="SettingsScope"/>.
+///     The root object of Terminal.Gui configuration settings / JSON schema. Contains only properties attributed with
+///     <see cref="SettingsScope"/>.
 /// </summary>
-/// <example><code>
+/// <example>
+///     <code>
 ///  {
 ///    "$schema" : "https://gui-cs.github.io/Terminal.Gui/schemas/tui-config-schema.json",
 ///    "Application.UseSystemConsole" : true,
@@ -22,100 +19,109 @@ namespace Terminal.Gui;
 ///    "Themes": {
 ///    },
 ///  },
-/// </code></example>
-/// <remarks>
-/// </remarks>
+/// </code>
+/// </example>
+/// <remarks></remarks>
 [JsonConverter (typeof (ScopeJsonConverter<SettingsScope>))]
-public class SettingsScope : Scope<SettingsScope> {
-	/// <summary>
-	/// Points to our JSON schema.
-	/// </summary>
-	[JsonInclude, JsonPropertyName ("$schema")]
-	public string Schema { get; set; } = "https://gui-cs.github.io/Terminal.Gui/schemas/tui-config-schema.json";
-
-	/// <summary>
-	/// The list of paths to the configuration files.
-	/// </summary>
-	public List<string> Sources = new List<string> ();
-
-	/// <summary>
-	/// Updates the <see cref="SettingsScope"/> with the settings in a JSON string.
-	/// </summary>
-	/// <param name="stream">Json document to update the settings with.</param>
-	/// <param name="source">The source (filename/resource name) the Json document was read from.</param>
-	public SettingsScope? Update (Stream stream, string source)
-	{
-		// Update the existing settings with the new settings.
-		try {
-			Update (JsonSerializer.Deserialize<SettingsScope> (stream, ConfigurationManager._serializerOptions)!);
-			ConfigurationManager.OnUpdated ();
-			Debug.WriteLine ($"ConfigurationManager: Read configuration from \"{source}\"");
-			Sources.Add (source);
-			return this;
-		} catch (JsonException e) {
-			if (ConfigurationManager.ThrowOnJsonErrors ?? false) {
-				throw;
-			} else {
-				ConfigurationManager.AddJsonError ($"Error deserializing {source}: {e.Message}");
-			}
-		}
-		return this;
-	}
-
-	/// <summary>
-	/// Updates the <see cref="SettingsScope"/> with the settings in a JSON file.
-	/// </summary>
-	/// <param name="filePath"></param>
-	public SettingsScope? Update (string filePath)
-	{
-		var realPath = filePath.Replace ("~", Environment.GetFolderPath (Environment.SpecialFolder.UserProfile));
-		if (!File.Exists (realPath)) {
-			Debug.WriteLine ($"ConfigurationManager: Configuration file \"{realPath}\" does not exist.");
-			Sources.Add (filePath);
-			return this;
-		}
-
-		var stream = File.OpenRead (realPath);
-		var s = Update (stream, filePath);
-		stream.Close ();
-		stream.Dispose ();
-		return s;
-	}
-
-	/// <summary>
-	/// Updates the <see cref="SettingsScope"/> with the settings from a Json resource.
-	/// </summary>
-	/// <param name="assembly"></param>
-	/// <param name="resourceName"></param>
-	public SettingsScope? UpdateFromResource (Assembly assembly, string resourceName)
-	{
-		if (resourceName == null || string.IsNullOrEmpty (resourceName)) {
-			Debug.WriteLine ($"ConfigurationManager: Resource \"{resourceName}\" does not exist in \"{assembly.GetName ().Name}\".");
-			return this;
-		}
-
-		using Stream? stream = assembly.GetManifestResourceStream (resourceName)!;
-		if (stream == null) {
-			Debug.WriteLine ($"ConfigurationManager: Failed to read resource \"{resourceName}\" from \"{assembly.GetName ().Name}\".");
-			return this;
-		}
-
-		return Update (stream, $"resource://[{assembly.GetName ().Name}]/{resourceName}");
-	}
-
-	/// <summary>
-	/// Updates the <see cref="SettingsScope"/> with the settings in a JSON string.
-	/// </summary>
-	/// <param name="json">Json document to update the settings with.</param>
-	/// <param name="source">The source (filename/resource name) the Json document was read from.</param>
-	public SettingsScope? Update (string json, string source)
-	{
-		var stream = new MemoryStream ();
-		var writer = new StreamWriter (stream);
-		writer.Write (json);
-		writer.Flush ();
-		stream.Position = 0;
-
-		return Update (stream, source);
-	}
+public class SettingsScope : Scope<SettingsScope>
+{
+    /// <summary>The list of paths to the configuration files.</summary>
+    public List<string> Sources = new ();
+
+    /// <summary>Points to our JSON schema.</summary>
+    [JsonInclude]
+    [JsonPropertyName ("$schema")]
+    public string Schema { get; set; } = "https://gui-cs.github.io/Terminal.Gui/schemas/tui-config-schema.json";
+
+    /// <summary>Updates the <see cref="SettingsScope"/> with the settings in a JSON string.</summary>
+    /// <param name="stream">Json document to update the settings with.</param>
+    /// <param name="source">The source (filename/resource name) the Json document was read from.</param>
+    public SettingsScope? Update (Stream stream, string source)
+    {
+        // Update the existing settings with the new settings.
+        try
+        {
+            Update (JsonSerializer.Deserialize<SettingsScope> (stream, _serializerOptions)!);
+            OnUpdated ();
+            Debug.WriteLine ($"ConfigurationManager: Read configuration from \"{source}\"");
+            Sources.Add (source);
+
+            return this;
+        }
+        catch (JsonException e)
+        {
+            if (ThrowOnJsonErrors ?? false)
+            {
+                throw;
+            }
+
+            AddJsonError ($"Error deserializing {source}: {e.Message}");
+        }
+
+        return this;
+    }
+
+    /// <summary>Updates the <see cref="SettingsScope"/> with the settings in a JSON file.</summary>
+    /// <param name="filePath"></param>
+    public SettingsScope? Update (string filePath)
+    {
+        string realPath = filePath.Replace ("~", Environment.GetFolderPath (Environment.SpecialFolder.UserProfile));
+
+        if (!File.Exists (realPath))
+        {
+            Debug.WriteLine ($"ConfigurationManager: Configuration file \"{realPath}\" does not exist.");
+            Sources.Add (filePath);
+
+            return this;
+        }
+
+        FileStream stream = File.OpenRead (realPath);
+        SettingsScope? s = Update (stream, filePath);
+        stream.Close ();
+        stream.Dispose ();
+
+        return s;
+    }
+
+    /// <summary>Updates the <see cref="SettingsScope"/> with the settings in a JSON string.</summary>
+    /// <param name="json">Json document to update the settings with.</param>
+    /// <param name="source">The source (filename/resource name) the Json document was read from.</param>
+    public SettingsScope? Update (string json, string source)
+    {
+        var stream = new MemoryStream ();
+        var writer = new StreamWriter (stream);
+        writer.Write (json);
+        writer.Flush ();
+        stream.Position = 0;
+
+        return Update (stream, source);
+    }
+
+    /// <summary>Updates the <see cref="SettingsScope"/> with the settings from a Json resource.</summary>
+    /// <param name="assembly"></param>
+    /// <param name="resourceName"></param>
+    public SettingsScope? UpdateFromResource (Assembly assembly, string resourceName)
+    {
+        if (resourceName is null || string.IsNullOrEmpty (resourceName))
+        {
+            Debug.WriteLine (
+                             $"ConfigurationManager: Resource \"{resourceName}\" does not exist in \"{assembly.GetName ().Name}\"."
+                            );
+
+            return this;
+        }
+
+        using Stream? stream = assembly.GetManifestResourceStream (resourceName)!;
+
+        if (stream is null)
+        {
+            Debug.WriteLine (
+                             $"ConfigurationManager: Failed to read resource \"{resourceName}\" from \"{assembly.GetName ().Name}\"."
+                            );
+
+            return this;
+        }
+
+        return Update (stream, $"resource://[{assembly.GetName ().Name}]/{resourceName}");
+    }
 }

+ 130 - 164
Terminal.Gui/Configuration/ThemeManager.cs

@@ -1,29 +1,22 @@
-using System;
+#nullable enable
 using System.Collections;
-using System.Collections.Generic;
 using System.Diagnostics;
 using System.Text.Json.Serialization;
 
-#nullable enable
+namespace Terminal.Gui;
 
-namespace Terminal.Gui; 
-
-/// <summary>
-/// Contains a dictionary of the <see cref="ThemeManager.Theme"/>s for a Terminal.Gui application.
-/// </summary>
+/// <summary>Contains a dictionary of the <see cref="ThemeManager.Theme"/>s for a Terminal.Gui application.</summary>
 /// <remarks>
-/// <para>
-/// A Theme is a collection of settings that are named. The default theme is named "Default".
-/// </para>
-/// <para>
-/// The <see cref="ThemeManager.Theme"/> property is used to detemrine the currently active theme. 
-/// </para>
+///     <para>A Theme is a collection of settings that are named. The default theme is named "Default".</para>
+///     <para>The <see cref="ThemeManager.Theme"/> property is used to detemrine the currently active theme.</para>
 /// </remarks>
 /// <para>
-/// <see cref="ThemeManager"/> is a singleton class. It is created when the first <see cref="ThemeManager"/> property is accessed.
-/// Accessing <see cref="ThemeManager.Instance"/> is the same as accessing <see cref="ConfigurationManager.Themes"/>.
+///     <see cref="ThemeManager"/> is a singleton class. It is created when the first <see cref="ThemeManager"/> property
+///     is accessed. Accessing <see cref="ThemeManager.Instance"/> is the same as accessing
+///     <see cref="ConfigurationManager.Themes"/>.
 /// </para>
-/// <example><code>
+/// <example>
+///     <code>
 /// 	"Themes": [
 /// 	{
 /// 		"Default": {
@@ -55,153 +48,126 @@ namespace Terminal.Gui;
 /// 			}
 /// 		}
 /// 	}
-/// </code></example> 
-public class ThemeManager : IDictionary<string, ThemeScope> {
-	private static readonly ThemeManager _instance = new ThemeManager ();
-	static ThemeManager () { } // Make sure it's truly lazy
-	private ThemeManager () { } // Prevent instantiation outside
-
-	/// <summary>
-	/// Class is a singleton...
-	/// </summary>
-	public static ThemeManager Instance { get { return _instance; } }
-
-	private static string _theme = string.Empty;
-
-	/// <summary>
-	/// The currently selected theme. This is the internal version; see <see cref="Theme"/>.
-	/// </summary>
-	[JsonInclude, SerializableConfigurationProperty (Scope = typeof (SettingsScope), OmitClassName = true), JsonPropertyName ("Theme")]
-	internal static string SelectedTheme {
-		get => _theme;
-		set {
-			var oldTheme = _theme;
-			_theme = value;
-			if (oldTheme != _theme &&
-				ConfigurationManager.Settings! ["Themes"]?.PropertyValue is Dictionary<string, ThemeScope> themes &&
-				themes.ContainsKey (_theme)) {
-				ConfigurationManager.Settings! ["Theme"].PropertyValue = _theme;
-				Instance.OnThemeChanged (oldTheme);
-			}
-		}
-	}
-
-	/// <summary>
-	/// Gets or sets the currently selected theme. The value is persisted to the "Theme"
-	/// property.
-	/// </summary>
-	[JsonIgnore]
-	public string Theme {
-		get => ThemeManager.SelectedTheme;
-		set {
-			ThemeManager.SelectedTheme = value;
-		}
-	}
-
-	/// <summary>
-	/// Called when the selected theme has changed. Fires the <see cref="ThemeChanged"/> event.
-	/// </summary>
-	internal void OnThemeChanged (string theme)
-	{
-		Debug.WriteLine ($"Themes.OnThemeChanged({theme}) -> {Theme}");
-		ThemeChanged?.Invoke (this, new ThemeManagerEventArgs (theme));
-	}
-
-	/// <summary>
-	/// Event fired he selected theme has changed.
-	/// application.
-	/// </summary>
-	public event EventHandler<ThemeManagerEventArgs>? ThemeChanged;
-
-	/// <summary>
-	/// Holds the <see cref="ThemeScope"/> definitions. 
-	/// </summary>
-	[JsonInclude, JsonConverter (typeof (DictionaryJsonConverter<ThemeScope>))]
-	[SerializableConfigurationProperty (Scope = typeof (SettingsScope), OmitClassName = true)]
-	public static Dictionary<string, ThemeScope>? Themes {
-		get => ConfigurationManager.Settings? ["Themes"]?.PropertyValue as Dictionary<string, ThemeScope>; // themes ?? new Dictionary<string, ThemeScope> ();
-		set {
-			//if (themes == null || value == null) {
-			//	themes = value;
-			//} else {
-			//	themes = (Dictionary<string, ThemeScope>)DeepMemberwiseCopy (value!, themes!)!;
-			//}
-			ConfigurationManager.Settings! ["Themes"].PropertyValue = value;
-		}
-	}
-
-	internal static void Reset ()
-	{
-		Debug.WriteLine ($"Themes.Reset()");
-		Colors.Reset ();
-		Themes?.Clear ();
-		SelectedTheme = string.Empty;
-	}
-
-	internal static void GetHardCodedDefaults ()
-	{
-		Debug.WriteLine ($"Themes.GetHardCodedDefaults()");
-		var theme = new ThemeScope ();
-		theme.RetrieveValues ();
-
-		Themes = new Dictionary<string, ThemeScope> (StringComparer.InvariantCultureIgnoreCase) { { "Default", theme } };
-		SelectedTheme = "Default";
-	}
-
-	#region IDictionary
-#pragma warning disable 1591
+/// </code>
+/// </example>
+public class ThemeManager : IDictionary<string, ThemeScope>
+{
+    private static string _theme = string.Empty;
+    static ThemeManager () { } // Make sure it's truly lazy
+    private ThemeManager () { } // Prevent instantiation outside
+
+    /// <summary>Class is a singleton...</summary>
+    public static ThemeManager Instance { get; } = new ();
+
+    /// <summary>Gets or sets the currently selected theme. The value is persisted to the "Theme" property.</summary>
+    [JsonIgnore]
+    public string Theme
+    {
+        get => SelectedTheme;
+        set => SelectedTheme = value;
+    }
+
+    /// <summary>Holds the <see cref="ThemeScope"/> definitions.</summary>
+    [JsonInclude]
+    [JsonConverter (typeof (DictionaryJsonConverter<ThemeScope>))]
+    [SerializableConfigurationProperty (Scope = typeof (SettingsScope), OmitClassName = true)]
+    public static Dictionary<string, ThemeScope>? Themes
+    {
+        get => Settings? ["Themes"]
+                       ?.PropertyValue as
+                   Dictionary<string, ThemeScope>; // themes ?? new Dictionary<string, ThemeScope> ();
+        set =>
+
+            //if (themes is null || value is null) {
+            //	themes = value;
+            //} else {
+            //	themes = (Dictionary<string, ThemeScope>)DeepMemberwiseCopy (value!, themes!)!;
+            //}
+            Settings! ["Themes"].PropertyValue = value;
+    }
+
+    /// <summary>The currently selected theme. This is the internal version; see <see cref="Theme"/>.</summary>
+    [JsonInclude]
+    [SerializableConfigurationProperty (Scope = typeof (SettingsScope), OmitClassName = true)]
+    [JsonPropertyName ("Theme")]
+    internal static string SelectedTheme
+    {
+        get => _theme;
+        set
+        {
+            string oldTheme = _theme;
+            _theme = value;
+
+            if (oldTheme != _theme && Settings! ["Themes"]?.PropertyValue is Dictionary<string, ThemeScope> themes && themes.ContainsKey (_theme))
+            {
+                Settings! ["Theme"].PropertyValue = _theme;
+                Instance.OnThemeChanged (oldTheme);
+            }
+        }
+    }
+
+    /// <summary>Event fired he selected theme has changed. application.</summary>
+    public event EventHandler<ThemeManagerEventArgs>? ThemeChanged;
+
+    internal static void GetHardCodedDefaults ()
+    {
+        Debug.WriteLine ("Themes.GetHardCodedDefaults()");
+        var theme = new ThemeScope ();
+        theme.RetrieveValues ();
+
+        Themes = new Dictionary<string, ThemeScope> (StringComparer.InvariantCultureIgnoreCase)
+        {
+            { "Default", theme }
+        };
+        SelectedTheme = "Default";
+    }
+
+    /// <summary>Called when the selected theme has changed. Fires the <see cref="ThemeChanged"/> event.</summary>
+    internal void OnThemeChanged (string theme)
+    {
+        Debug.WriteLine ($"Themes.OnThemeChanged({theme}) -> {Theme}");
+        ThemeChanged?.Invoke (this, new ThemeManagerEventArgs (theme));
+    }
+
+    internal static void Reset ()
+    {
+        Debug.WriteLine ("Themes.Reset()");
+        Colors.Reset ();
+        Themes?.Clear ();
+        SelectedTheme = string.Empty;
+    }
+
+    #region IDictionary
 
-	public ICollection<string> Keys => ((IDictionary<string, ThemeScope>)Themes!).Keys;
-	public ICollection<ThemeScope> Values => ((IDictionary<string, ThemeScope>)Themes!).Values;
-	public int Count => ((ICollection<KeyValuePair<string, ThemeScope>>)Themes!).Count;
-	public bool IsReadOnly => ((ICollection<KeyValuePair<string, ThemeScope>>)Themes!).IsReadOnly;
-	public ThemeScope this [string key] { get => ((IDictionary<string, ThemeScope>)Themes!) [key]; set => ((IDictionary<string, ThemeScope>)Themes!) [key] = value; }
-	public void Add (string key, ThemeScope value)
-	{
-		((IDictionary<string, ThemeScope>)Themes!).Add (key, value);
-	}
-	public bool ContainsKey (string key)
-	{
-		return ((IDictionary<string, ThemeScope>)Themes!).ContainsKey (key);
-	}
-	public bool Remove (string key)
-	{
-		return ((IDictionary<string, ThemeScope>)Themes!).Remove (key);
-	}
-	public bool TryGetValue (string key, out ThemeScope value)
-	{
-		return ((IDictionary<string, ThemeScope>)Themes!).TryGetValue (key, out value!);
-	}
-	public void Add (KeyValuePair<string, ThemeScope> item)
-	{
-		((ICollection<KeyValuePair<string, ThemeScope>>)Themes!).Add (item);
-	}
-	public void Clear ()
-	{
-		((ICollection<KeyValuePair<string, ThemeScope>>)Themes!).Clear ();
-	}
-	public bool Contains (KeyValuePair<string, ThemeScope> item)
-	{
-		return ((ICollection<KeyValuePair<string, ThemeScope>>)Themes!).Contains (item);
-	}
-	public void CopyTo (KeyValuePair<string, ThemeScope> [] array, int arrayIndex)
-	{
-		((ICollection<KeyValuePair<string, ThemeScope>>)Themes!).CopyTo (array, arrayIndex);
-	}
-	public bool Remove (KeyValuePair<string, ThemeScope> item)
-	{
-		return ((ICollection<KeyValuePair<string, ThemeScope>>)Themes!).Remove (item);
-	}
-	public IEnumerator<KeyValuePair<string, ThemeScope>> GetEnumerator ()
-	{
-		return ((IEnumerable<KeyValuePair<string, ThemeScope>>)Themes!).GetEnumerator ();
-	}
-
-	IEnumerator IEnumerable.GetEnumerator ()
-	{
-		return ((IEnumerable)Themes!).GetEnumerator ();
-	}
+#pragma warning disable 1591
+    public ICollection<string> Keys => ((IDictionary<string, ThemeScope>)Themes!).Keys;
+    public ICollection<ThemeScope> Values => ((IDictionary<string, ThemeScope>)Themes!).Values;
+    public int Count => ((ICollection<KeyValuePair<string, ThemeScope>>)Themes!).Count;
+    public bool IsReadOnly => ((ICollection<KeyValuePair<string, ThemeScope>>)Themes!).IsReadOnly;
+
+    public ThemeScope this [string key]
+    {
+        get => ((IDictionary<string, ThemeScope>)Themes!) [key];
+        set => ((IDictionary<string, ThemeScope>)Themes!) [key] = value;
+    }
+
+    public void Add (string key, ThemeScope value) { ((IDictionary<string, ThemeScope>)Themes!).Add (key, value); }
+    public bool ContainsKey (string key) { return ((IDictionary<string, ThemeScope>)Themes!).ContainsKey (key); }
+    public bool Remove (string key) { return ((IDictionary<string, ThemeScope>)Themes!).Remove (key); }
+    public bool TryGetValue (string key, out ThemeScope value) { return ((IDictionary<string, ThemeScope>)Themes!).TryGetValue (key, out value!); }
+    public void Add (KeyValuePair<string, ThemeScope> item) { ((ICollection<KeyValuePair<string, ThemeScope>>)Themes!).Add (item); }
+    public void Clear () { ((ICollection<KeyValuePair<string, ThemeScope>>)Themes!).Clear (); }
+    public bool Contains (KeyValuePair<string, ThemeScope> item) { return ((ICollection<KeyValuePair<string, ThemeScope>>)Themes!).Contains (item); }
+
+    public void CopyTo (KeyValuePair<string, ThemeScope> [] array, int arrayIndex)
+    {
+        ((ICollection<KeyValuePair<string, ThemeScope>>)Themes!).CopyTo (array, arrayIndex);
+    }
+
+    public bool Remove (KeyValuePair<string, ThemeScope> item) { return ((ICollection<KeyValuePair<string, ThemeScope>>)Themes!).Remove (item); }
+    public IEnumerator<KeyValuePair<string, ThemeScope>> GetEnumerator () { return ((IEnumerable<KeyValuePair<string, ThemeScope>>)Themes!).GetEnumerator (); }
+    IEnumerator IEnumerable.GetEnumerator () { return ((IEnumerable)Themes!).GetEnumerator (); }
 #pragma warning restore 1591
 
-	#endregion
-}
+    #endregion
+}

+ 11 - 11
Terminal.Gui/Configuration/ThemeScope.cs

@@ -1,18 +1,17 @@
-using System.Text.Json.Serialization;
-
-#nullable enable
+#nullable enable
+using System.Text.Json.Serialization;
 
 namespace Terminal.Gui;
 
 /// <summary>
-/// The root object for a Theme. A Theme is a set of settings that are applied to the running <see cref="Application"/>
-/// as a group.
+///     The root object for a Theme. A Theme is a set of settings that are applied to the running
+///     <see cref="Application"/> as a group.
 /// </summary>
 /// <remarks>
-/// <para>
-/// </para>
+///     <para></para>
 /// </remarks>
-/// <example><code>
+/// <example>
+///     <code>
 /// 	"Default": {
 /// 		"ColorSchemes": [
 /// 		{
@@ -41,7 +40,8 @@ namespace Terminal.Gui;
 /// 
 /// 		}
 /// 	}
-/// </code></example> 
+/// </code>
+/// </example>
 [JsonConverter (typeof (ScopeJsonConverter<ThemeScope>))]
-public class ThemeScope : Scope<ThemeScope> {
-}
+public class ThemeScope : Scope<ThemeScope>
+{ }

+ 921 - 1107
Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs

@@ -1,1126 +1,940 @@
 //
 // ConsoleDriver.cs: Base class for Terminal.Gui ConsoleDriver implementations.
 //
-using System.Text;
-using System;
+
 using System.Diagnostics;
-using System.Linq;
-using Terminal.Gui.ConsoleDrivers;
 
 namespace Terminal.Gui;
 
-/// <summary>
-/// Base class for Terminal.Gui ConsoleDriver implementations.
-/// </summary>
+/// <summary>Base class for Terminal.Gui ConsoleDriver implementations.</summary>
 /// <remarks>
-/// There are currently four implementations:
-/// - <see cref="CursesDriver"/> (for Unix and Mac)
-/// - <see cref="WindowsDriver"/>
-/// - <see cref="NetDriver"/> that uses the .NET Console API
-/// - <see cref="FakeConsole"/> for unit testing.
+///     There are currently four implementations: - <see cref="CursesDriver"/> (for Unix and Mac) -
+///     <see cref="WindowsDriver"/> - <see cref="NetDriver"/> that uses the .NET Console API - <see cref="FakeConsole"/>
+///     for unit testing.
 /// </remarks>
-public abstract class ConsoleDriver {
-	/// <summary>
-	/// Set this to true in any unit tests that attempt to test drivers other than FakeDriver.
-	/// <code>
-	///  public ColorTests ()
-	///  {
-	///    ConsoleDriver.RunningUnitTests = true;
-	///  }
-	/// </code>
-	/// </summary>
-	internal static bool RunningUnitTests { get; set; }
-
-	#region Setup & Teardown
-	/// <summary>
-	/// Initializes the driver
-	/// </summary>
-	/// <returns>Returns an instance of <see cref="MainLoop"/> using the <see cref="IMainLoopDriver"/> for the driver.</returns>
-	internal abstract MainLoop Init ();
-
-	/// <summary>
-	/// Ends the execution of the console driver.
-	/// </summary>
-	internal abstract void End ();
-	#endregion
-
-	/// <summary>
-	/// The event fired when the terminal is resized.
-	/// </summary>
-	public event EventHandler<SizeChangedEventArgs> SizeChanged;
-
-	/// <summary>
-	/// Called when the terminal size changes. Fires the <see cref="SizeChanged"/> event.
-	/// </summary>
-	/// <param name="args"></param>
-	public void OnSizeChanged (SizeChangedEventArgs args) => SizeChanged?.Invoke (this, args);
-
-	/// <summary>
-	/// The number of columns visible in the terminal.
-	/// </summary>
-	public virtual int Cols {
-		get => _cols;
-		internal set {
-			_cols = value;
-			ClearContents ();
-		}
-	}
-
-	/// <summary>
-	/// The number of rows visible in the terminal.
-	/// </summary>
-	public virtual int Rows {
-		get => _rows;
-		internal set {
-			_rows = value;
-			ClearContents ();
-		}
-	}
-
-	/// <summary>
-	/// The leftmost column in the terminal.
-	/// </summary>
-	public virtual int Left { get; internal set; } = 0;
-
-	/// <summary>
-	/// The topmost row in the terminal.
-	/// </summary>
-	public virtual int Top { get; internal set; } = 0;
-
-	/// <summary>
-	/// Get the operating system clipboard.
-	/// </summary>
-	public IClipboard Clipboard { get; internal set; }
-
-	/// <summary>
-	/// The contents of the application output. The driver outputs this buffer to the terminal when <see cref="UpdateScreen"/>
-	/// is called.
-	/// <remarks>
-	/// The format of the array is rows, columns. The first index is the row, the second index is the column.
-	/// </remarks>
-	/// </summary>
-	public Cell [,] Contents { get; internal set; }
-
-	/// <summary>
-	/// Gets the column last set by <see cref="Move"/>. <see cref="Col"/> and <see cref="Row"/>
-	/// are used by <see cref="AddRune(Rune)"/> and <see cref="AddStr"/> to determine where to add content.
-	/// </summary>
-	public int Col { get; internal set; }
-
-	/// <summary>
-	/// Gets the row last set by <see cref="Move"/>. <see cref="Col"/> and <see cref="Row"/>
-	/// are used by <see cref="AddRune(Rune)"/> and <see cref="AddStr"/> to determine where to add content.
-	/// </summary>
-	public int Row { get; internal set; }
-
-	/// <summary>
-	/// Updates <see cref="Col"/> and <see cref="Row"/> to the specified column and row in <see cref="Contents"/>.
-	/// Used by <see cref="AddRune(Rune)"/> and <see cref="AddStr"/> to determine where to add content.
-	/// </summary>
-	/// <remarks>
-	/// <para>
-	/// This does not move the cursor on the screen, it only updates the internal state of the driver.
-	/// </para>
-	/// <para>
-	/// If <paramref name="col"/> or <paramref name="row"/> are negative or beyond  <see cref="Cols"/> and <see cref="Rows"/>,
-	/// the method still sets those properties.
-	/// </para>
-	/// </remarks>
-	/// <param name="col">Column to move to.</param>
-	/// <param name="row">Row to move to.</param>
-	public virtual void Move (int col, int row)
-	{
-		Col = col;
-		Row = row;
-	}
-
-	/// <summary>
-	/// Tests if the specified rune is supported by the driver.
-	/// </summary>
-	/// <param name="rune"></param>
-	/// <returns><see langword="true"/> if the rune can be properly presented; <see langword="false"/> if the driver
-	/// does not support displaying this rune.</returns>
-	public virtual bool IsRuneSupported (Rune rune) => Rune.IsValid (rune.Value);
-
-	/// <summary>
-	/// Adds the specified rune to the display at the current cursor position. 
-	/// </summary>
-	/// <remarks>
-	/// <para>
-	/// When the method returns, <see cref="Col"/> will be incremented by the number of columns <paramref name="rune"/> required,
-	/// even if the new column value is outside of the <see cref="Clip"/> or screen dimensions defined by <see cref="Cols"/>.
-	/// </para>
-	/// <para>
-	/// If <paramref name="rune"/> requires more than one column, and <see cref="Col"/> plus the number of columns needed
-	/// exceeds the <see cref="Clip"/> or screen dimensions, the default Unicode replacement character (U+FFFD) will be added instead.
-	/// </para>
-	/// </remarks>
-	/// <param name="rune">Rune to add.</param>
-	public void AddRune (Rune rune)
-	{
-		int runeWidth = -1;
-		bool validLocation = IsValidLocation (Col, Row);
-		if (validLocation) {
-			rune = rune.MakePrintable ();
-			runeWidth = rune.GetColumns ();
-			if (runeWidth == 0 && rune.IsCombiningMark ()) {
-				// AtlasEngine does not support NON-NORMALIZED combining marks in a way
-				// compatible with the driver architecture. Any CMs (except in the first col)
-				// are correctly combined with the base char, but are ALSO treated as 1 column
-				// width codepoints E.g. `echo "[e`u{0301}`u{0301}]"` will output `[é  ]`.
-				// 
-				// Until this is addressed (see Issue #), we do our best by 
-				// a) Attempting to normalize any CM with the base char to it's left
-				// b) Ignoring any CMs that don't normalize
-				if (Col > 0) {
-					if (Contents [Row, Col - 1].CombiningMarks.Count > 0) {
-						// Just add this mark to the list
-						Contents [Row, Col - 1].CombiningMarks.Add (rune);
-						// Ignore. Don't move to next column (let the driver figure out what to do).
-					} else {
-						// Attempt to normalize the cell to our left combined with this mark
-						string combined = Contents [Row, Col - 1].Rune + rune.ToString ();
-
-						// Normalize to Form C (Canonical Composition)
-						string normalized = combined.Normalize (NormalizationForm.FormC);
-						if (normalized.Length == 1) {
-							// It normalized! We can just set the Cell to the left with the
-							// normalized codepoint 
-							Contents [Row, Col - 1].Rune = (Rune)normalized [0];
-							// Ignore. Don't move to next column because we're already there
-						} else {
-							// It didn't normalize. Add it to the Cell to left's CM list
-							Contents [Row, Col - 1].CombiningMarks.Add (rune);
-							// Ignore. Don't move to next column (let the driver figure out what to do).
-						}
-					}
-					Contents [Row, Col - 1].Attribute = CurrentAttribute;
-					Contents [Row, Col - 1].IsDirty = true;
-				} else {
-					// Most drivers will render a combining mark at col 0 as the mark
-					Contents [Row, Col].Rune = rune;
-					Contents [Row, Col].Attribute = CurrentAttribute;
-					Contents [Row, Col].IsDirty = true;
-					Col++;
-				}
-			} else {
-				Contents [Row, Col].Attribute = CurrentAttribute;
-				Contents [Row, Col].IsDirty = true;
-
-				if (Col > 0) {
-					// Check if cell to left has a wide glyph
-					if (Contents [Row, Col - 1].Rune.GetColumns () > 1) {
-						// Invalidate cell to left
-						Contents [Row, Col - 1].Rune = Rune.ReplacementChar;
-						Contents [Row, Col - 1].IsDirty = true;
-					}
-				}
-
-
-				if (runeWidth < 1) {
-					Contents [Row, Col].Rune = Rune.ReplacementChar;
-
-				} else if (runeWidth == 1) {
-					Contents [Row, Col].Rune = rune;
-					if (Col < Clip.Right - 1) {
-						Contents [Row, Col + 1].IsDirty = true;
-					}
-				} else if (runeWidth == 2) {
-					if (Col == Clip.Right - 1) {
-						// We're at the right edge of the clip, so we can't display a wide character.
-						// TODO: Figure out if it is better to show a replacement character or ' '
-						Contents [Row, Col].Rune = Rune.ReplacementChar;
-					} else {
-						Contents [Row, Col].Rune = rune;
-						if (Col < Clip.Right - 1) {
-							// Invalidate cell to right so that it doesn't get drawn
-							// TODO: Figure out if it is better to show a replacement character or ' '
-							Contents [Row, Col + 1].Rune = Rune.ReplacementChar;
-							Contents [Row, Col + 1].IsDirty = true;
-						}
-					}
-				} else {
-					// This is a non-spacing character, so we don't need to do anything
-					Contents [Row, Col].Rune = (Rune)' ';
-					Contents [Row, Col].IsDirty = false;
-				}
-				_dirtyLines [Row] = true;
-			}
-		}
-
-		if (runeWidth is < 0 or > 0) {
-			Col++;
-		}
-
-		if (runeWidth > 1) {
-			Debug.Assert (runeWidth <= 2);
-			if (validLocation && Col < Clip.Right) {
-				// This is a double-width character, and we are not at the end of the line.
-				// Col now points to the second column of the character. Ensure it doesn't
-				// Get rendered.
-				Contents [Row, Col].IsDirty = false;
-				Contents [Row, Col].Attribute = CurrentAttribute;
-
-				// TODO: Determine if we should wipe this out (for now now)
-				//Contents [Row, Col].Rune = (Rune)' ';
-			}
-			Col++;
-		}
-	}
-
-	/// <summary>
-	/// Adds the specified <see langword="char"/> to the display at the current cursor position. This method
-	/// is a convenience method that calls <see cref="AddRune(Rune)"/> with the <see cref="Rune"/> constructor.
-	/// </summary>
-	/// <param name="c">Character to add.</param>
-	public void AddRune (char c) => AddRune (new Rune (c));
-
-	/// <summary>
-	/// Adds the <paramref name="str"/> to the display at the cursor position.
-	/// </summary>
-	/// <remarks>
-	/// <para>
-	/// When the method returns, <see cref="Col"/> will be incremented by the number of columns <paramref name="str"/> required,
-	/// unless the new column value is outside of the <see cref="Clip"/> or screen dimensions defined by <see cref="Cols"/>.
-	/// </para>
-	/// <para>
-	/// If <paramref name="str"/> requires more columns than are available, the output will be clipped.
-	/// </para>
-	/// </remarks>
-	/// <param name="str">String.</param>
-	public void AddStr (string str)
-	{
-		var runes = str.EnumerateRunes ().ToList ();
-		for (int i = 0; i < runes.Count; i++) {
-			//if (runes [i].IsCombiningMark()) {
-
-			//	// Attempt to normalize
-			//	string combined = runes [i-1] + runes [i].ToString();
-
-			//	// Normalize to Form C (Canonical Composition)
-			//	string normalized = combined.Normalize (NormalizationForm.FormC);
-
-			//	runes [i-]
-			//}
-			AddRune (runes [i]);
-		}
-	}
-
-	Rect _clip;
-
-	/// <summary>
-	/// Tests whether the specified coordinate are valid for drawing. 
-	/// </summary>
-	/// <param name="col">The column.</param>
-	/// <param name="row">The row.</param>
-	/// <returns><see langword="false"/> if the coordinate is outside of the
-	/// screen bounds or outside of <see cref="Clip"/>. <see langword="true"/> otherwise.</returns>
-	public bool IsValidLocation (int col, int row) =>
-		col >= 0 && row >= 0 &&
-		col < Cols && row < Rows &&
-		Clip.Contains (col, row);
-
-	/// <summary>
-	/// Gets or sets the clip rectangle that <see cref="AddRune(Rune)"/> and <see cref="AddStr(string)"/> are 
-	/// subject to.
-	/// </summary>
-	/// <value>The rectangle describing the bounds of <see cref="Clip"/>.</value>
-	public Rect Clip {
-		get => _clip;
-		set => _clip = value;
-	}
-
-	/// <summary>
-	/// Updates the screen to reflect all the changes that have been done to the display buffer
-	/// </summary>
-	public abstract void Refresh ();
-
-	/// <summary>
-	/// Sets the position of the terminal cursor to <see cref="Col"/> and <see cref="Row"/>.
-	/// </summary>
-	public abstract void UpdateCursor ();
-
-	/// <summary>
-	/// Gets the terminal cursor visibility.
-	/// </summary>
-	/// <param name="visibility">The current <see cref="CursorVisibility"/></param>
-	/// <returns><see langword="true"/> upon success</returns>
-	public abstract bool GetCursorVisibility (out CursorVisibility visibility);
-
-	/// <summary>
-	/// Sets the terminal cursor visibility.
-	/// </summary>
-	/// <param name="visibility">The wished <see cref="CursorVisibility"/></param>
-	/// <returns><see langword="true"/> upon success</returns>
-	public abstract bool SetCursorVisibility (CursorVisibility visibility);
-
-	/// <summary>
-	/// Determines if the terminal cursor should be visible or not and sets it accordingly.
-	/// </summary>
-	/// <returns><see langword="true"/> upon success</returns>
-	public abstract bool EnsureCursorVisibility ();
-
-	// As performance is a concern, we keep track of the dirty lines and only refresh those.
-	// This is in addition to the dirty flag on each cell.
-	internal bool [] _dirtyLines;
-
-	/// <summary>
-	/// Clears the <see cref="Contents"/> of the driver.
-	/// </summary>
-	public void ClearContents ()
-	{
-		// TODO: This method is really "Clear Contents" now and should not be abstract (or virtual)
-		Contents = new Cell [Rows, Cols];
-		Clip = new Rect (0, 0, Cols, Rows);
-		_dirtyLines = new bool [Rows];
-
-		lock (Contents) {
-			// 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] = new Cell () {
-							Rune = (Rune)' ',
-							Attribute = new Attribute (Color.White, Color.Black),
-							IsDirty = true
-						};
-						_dirtyLines [row] = true;
-					}
-				}
-			} catch (IndexOutOfRangeException) { }
-		}
-	}
-
-	/// <summary>
-	/// Redraws the physical screen with the contents that have been queued up via any of the printing commands.
-	/// </summary>
-	public abstract void UpdateScreen ();
-
-	#region Color Handling
-	/// <summary>
-	/// Gets whether the <see cref="ConsoleDriver"/> supports TrueColor output.
-	/// </summary>
-	public virtual bool SupportsTrueColor => true;
-
-	/// <summary>
-	/// Gets or sets whether the <see cref="ConsoleDriver"/> should use 16 colors instead of the default TrueColors. See <see cref="Application.Force16Colors"/>
-	/// to change this setting via <see cref="ConfigurationManager"/>.
-	/// </summary>
-	/// <remarks>
-	/// <para>
-	/// Will be forced to <see langword="true"/> if <see cref="ConsoleDriver.SupportsTrueColor"/> is  <see langword="false"/>, indicating
-	/// that the <see cref="ConsoleDriver"/> cannot support TrueColor.
-	/// </para>
-	/// </remarks>
-	internal virtual bool Force16Colors {
-		get => Application.Force16Colors || !SupportsTrueColor;
-		set => Application.Force16Colors = value || !SupportsTrueColor;
-	}
-
-	Attribute _currentAttribute;
-	int _cols;
-	int _rows;
-
-	/// <summary>
-	/// The <see cref="Attribute"/> that will be used for the next <see cref="AddRune(Rune)"/> or <see cref="AddStr"/> call.
-	/// </summary>
-	public Attribute CurrentAttribute {
-		get => _currentAttribute;
-		set {
-			if (Application.Driver != null) {
-				_currentAttribute = new Attribute (value.Foreground, value.Background);
-				return;
-			}
-
-			_currentAttribute = value;
-		}
-	}
-
-	/// <summary>
-	/// Selects the specified attribute as the attribute to use for future calls to AddRune and AddString.
-	/// </summary>
-	/// <remarks>
-	/// Implementations should call <c>base.SetAttribute(c)</c>.
-	/// </remarks>
-	/// <param name="c">C.</param>
-	public Attribute SetAttribute (Attribute c)
-	{
-		var prevAttribute = CurrentAttribute;
-		CurrentAttribute = c;
-		return prevAttribute;
-	}
-
-	/// <summary>
-	/// Gets the current <see cref="Attribute"/>.
-	/// </summary>
-	/// <returns>The current attribute.</returns>
-	public Attribute GetAttribute () => CurrentAttribute;
-
-	// TODO: This is only overridden by CursesDriver. Once CursesDriver supports 24-bit color, this virtual method can be
-	// removed (and Attribute can lose the platformColor property).
-	/// <summary>
-	/// Makes an <see cref="Attribute"/>.
-	/// </summary>
-	/// <param name="foreground">The foreground color.</param>
-	/// <param name="background">The background color.</param>
-	/// <returns>The attribute for the foreground and background colors.</returns>
-	public virtual Attribute MakeColor (Color foreground, Color background) =>
-		// Encode the colors into the int value.
-		new (
-			-1, // only used by cursesdriver!
-			foreground,
-			background
-		);
-	#endregion
-
-	#region Mouse and Keyboard
-	/// <summary>
-	/// Event fired when a key is pressed down. This is a precursor to <see cref="KeyUp"/>.
-	/// </summary>
-	public event EventHandler<Key> KeyDown;
-
-	/// <summary>
-	/// Called when a key is pressed down. Fires the <see cref="KeyDown"/> event. This is a precursor to <see cref="OnKeyUp"/>.
-	/// </summary>
-	/// <param name="a"></param>
-	public void OnKeyDown (Key a) => KeyDown?.Invoke (this, a);
-
-	/// <summary>
-	/// Event fired when a key is released. 
-	/// </summary>
-	/// <remarks>
-	/// Drivers that do not support key release events will fire this event after <see cref="KeyDown"/> processing is complete.
-	/// </remarks>
-	public event EventHandler<Key> KeyUp;
-
-	/// <summary>
-	/// Called when a key is released. Fires the <see cref="KeyUp"/> event.
-	/// </summary>
-	/// <remarks>
-	/// Drivers that do not support key release events will calls this method after <see cref="OnKeyDown"/> processing is complete.
-	/// </remarks>
-	/// <param name="a"></param>
-	public void OnKeyUp (Key a) => KeyUp?.Invoke (this, a);
-
-	/// <summary>
-	/// Event fired when a mouse event occurs.
-	/// </summary>
-	public event EventHandler<MouseEventEventArgs> MouseEvent;
-
-	/// <summary>
-	/// Called when a mouse event occurs. Fires the <see cref="MouseEvent"/> event.
-	/// </summary>
-	/// <param name="a"></param>
-	public void OnMouseEvent (MouseEventEventArgs a) => MouseEvent?.Invoke (this, a);
-
-	/// <summary>
-	/// Simulates a key press.
-	/// </summary>
-	/// <param name="keyChar">The key character.</param>
-	/// <param name="key">The key.</param>
-	/// <param name="shift">If <see langword="true"/> simulates the Shift key being pressed.</param>
-	/// <param name="alt">If <see langword="true"/> simulates the Alt key being pressed.</param>
-	/// <param name="ctrl">If <see langword="true"/> simulates the Ctrl key being pressed.</param>
-	public abstract void SendKeys (char keyChar, ConsoleKey key, bool shift, bool alt, bool ctrl);
-	#endregion
-
-	/// <summary>
-	/// Enables diagnostic functions
-	/// </summary>
-	[Flags]
-	public enum DiagnosticFlags : uint {
-		/// <summary>
-		/// All diagnostics off
-		/// </summary>
-		Off = 0b_0000_0000,
-
-		/// <summary>
-		/// When enabled, <see cref="View.OnDrawAdornments"/> will draw a 
-		/// ruler in the frame for any side with a padding value greater than 0.
-		/// </summary>
-		FrameRuler = 0b_0000_0001,
-
-		/// <summary>
-		/// When enabled, <see cref="View.OnDrawAdornments"/> will draw a 
-		/// 'L', 'R', 'T', and 'B' when clearing <see cref="Thickness"/>'s instead of ' '.
-		/// </summary>
-		FramePadding = 0b_0000_0010
-	}
-
-	/// <summary>
-	/// Set flags to enable/disable <see cref="ConsoleDriver"/> diagnostics.
-	/// </summary>
-	public static DiagnosticFlags Diagnostics { get; set; }
-
-	/// <summary>
-	/// Gets the dimensions of the terminal.
-	/// </summary>
-	public Rect Bounds => new Rect (0, 0, Cols, Rows);
-
-	/// <summary>
-	/// Suspends the application (e.g. on Linux via SIGTSTP) and upon resume, resets the console driver.
-	/// </summary>
-	/// <remarks>This is only implemented in <see cref="CursesDriver"/>.</remarks>
-	public abstract void Suspend ();
-
-	// TODO: Move FillRect to ./Drawing	
-	/// <summary>
-	/// Fills the specified rectangle with the specified rune.
-	/// </summary>
-	/// <param name="rect"></param>
-	/// <param name="rune"></param>
-	public void FillRect (Rect rect, Rune rune = default)
-	{
-		for (int r = rect.Y; r < rect.Y + rect.Height; r++) {
-			for (int c = rect.X; c < rect.X + rect.Width; c++) {
-				Application.Driver.Move (c, r);
-				Application.Driver.AddRune (rune == default ? new Rune (' ') : rune);
-			}
-		}
-	}
-
-	/// <summary>
-	/// Fills the specified rectangle with the specified <see langword="char"/>. This method
-	/// is a convenience method that calls <see cref="FillRect(Rect, Rune)"/>.
-	/// </summary>
-	/// <param name="rect"></param>
-	/// <param name="c"></param>
-	public void FillRect (Rect rect, char c) => FillRect (rect, new Rune (c));
-
-	/// <summary>
-	/// Returns the name of the driver and relevant library version information.
-	/// </summary>
-	/// <returns></returns>
-	public virtual string GetVersionInfo () => GetType ().Name;
+public abstract class ConsoleDriver
+{
+    /// <summary>Enables diagnostic functions</summary>
+    [Flags]
+    public enum DiagnosticFlags : uint
+    {
+        /// <summary>All diagnostics off</summary>
+        Off = 0b_0000_0000,
+
+        /// <summary>
+        ///     When enabled, <see cref="View.OnDrawAdornments"/> will draw a ruler in the frame for any side with a padding
+        ///     value greater than 0.
+        /// </summary>
+        FrameRuler = 0b_0000_0001,
+
+        /// <summary>
+        ///     When enabled, <see cref="View.OnDrawAdornments"/> will draw a 'L', 'R', 'T', and 'B' when clearing
+        ///     <see cref="Thickness"/>'s instead of ' '.
+        /// </summary>
+        FramePadding = 0b_0000_0010
+    }
+
+    // As performance is a concern, we keep track of the dirty lines and only refresh those.
+    // This is in addition to the dirty flag on each cell.
+    internal bool [] _dirtyLines;
+
+    /// <summary>Gets the dimensions of the terminal.</summary>
+    public Rectangle Bounds => new (0, 0, Cols, Rows);
+
+    /// <summary>
+    ///     Gets or sets the clip rectangle that <see cref="AddRune(Rune)"/> and <see cref="AddStr(string)"/> are subject
+    ///     to.
+    /// </summary>
+    /// <value>The rectangle describing the bounds of <see cref="Clip"/>.</value>
+    public Rectangle Clip { get; set; }
+
+    /// <summary>Get the operating system clipboard.</summary>
+    public IClipboard Clipboard { get; internal set; }
+
+    /// <summary>
+    ///     Gets the column last set by <see cref="Move"/>. <see cref="Col"/> and <see cref="Row"/> are used by
+    ///     <see cref="AddRune(Rune)"/> and <see cref="AddStr"/> to determine where to add content.
+    /// </summary>
+    public int Col { get; internal set; }
+
+    /// <summary>The number of columns visible in the terminal.</summary>
+    public virtual int Cols
+    {
+        get => _cols;
+        internal set
+        {
+            _cols = value;
+            ClearContents ();
+        }
+    }
+
+    /// <summary>
+    ///     The contents of the application output. The driver outputs this buffer to the terminal when
+    ///     <see cref="UpdateScreen"/> is called.
+    ///     <remarks>The format of the array is rows, columns. The first index is the row, the second index is the column.</remarks>
+    /// </summary>
+    public Cell [,] Contents { get; internal set; }
+
+    /// <summary>Set flags to enable/disable <see cref="ConsoleDriver"/> diagnostics.</summary>
+    public static DiagnosticFlags Diagnostics { get; set; }
+
+    /// <summary>The leftmost column in the terminal.</summary>
+    public virtual int Left { get; internal set; } = 0;
+
+    /// <summary>
+    ///     Gets the row last set by <see cref="Move"/>. <see cref="Col"/> and <see cref="Row"/> are used by
+    ///     <see cref="AddRune(Rune)"/> and <see cref="AddStr"/> to determine where to add content.
+    /// </summary>
+    public int Row { get; internal set; }
+
+    /// <summary>The number of rows visible in the terminal.</summary>
+    public virtual int Rows
+    {
+        get => _rows;
+        internal set
+        {
+            _rows = value;
+            ClearContents ();
+        }
+    }
+
+    /// <summary>The topmost row in the terminal.</summary>
+    public virtual int Top { get; internal set; } = 0;
+
+    /// <summary>
+    ///     Set this to true in any unit tests that attempt to test drivers other than FakeDriver.
+    ///     <code>
+    ///  public ColorTests ()
+    ///  {
+    ///    ConsoleDriver.RunningUnitTests = true;
+    ///  }
+    /// </code>
+    /// </summary>
+    internal static bool RunningUnitTests { get; set; }
+
+    /// <summary>Adds the specified rune to the display at the current cursor position.</summary>
+    /// <remarks>
+    ///     <para>
+    ///         When the method returns, <see cref="Col"/> will be incremented by the number of columns
+    ///         <paramref name="rune"/> required, even if the new column value is outside of the <see cref="Clip"/> or screen
+    ///         dimensions defined by <see cref="Cols"/>.
+    ///     </para>
+    ///     <para>
+    ///         If <paramref name="rune"/> requires more than one column, and <see cref="Col"/> plus the number of columns
+    ///         needed exceeds the <see cref="Clip"/> or screen dimensions, the default Unicode replacement character (U+FFFD)
+    ///         will be added instead.
+    ///     </para>
+    /// </remarks>
+    /// <param name="rune">Rune to add.</param>
+    public void AddRune (Rune rune)
+    {
+        int runeWidth = -1;
+        bool validLocation = IsValidLocation (Col, Row);
+
+        if (validLocation)
+        {
+            rune = rune.MakePrintable ();
+            runeWidth = rune.GetColumns ();
+
+            if (runeWidth == 0 && rune.IsCombiningMark ())
+            {
+                // AtlasEngine does not support NON-NORMALIZED combining marks in a way
+                // compatible with the driver architecture. Any CMs (except in the first col)
+                // are correctly combined with the base char, but are ALSO treated as 1 column
+                // width codepoints E.g. `echo "[e`u{0301}`u{0301}]"` will output `[é  ]`.
+                // 
+                // Until this is addressed (see Issue #), we do our best by 
+                // a) Attempting to normalize any CM with the base char to it's left
+                // b) Ignoring any CMs that don't normalize
+                if (Col > 0)
+                {
+                    if (Contents [Row, Col - 1].CombiningMarks.Count > 0)
+                    {
+                        // Just add this mark to the list
+                        Contents [Row, Col - 1].CombiningMarks.Add (rune);
+
+                        // Ignore. Don't move to next column (let the driver figure out what to do).
+                    }
+                    else
+                    {
+                        // Attempt to normalize the cell to our left combined with this mark
+                        string combined = Contents [Row, Col - 1].Rune + rune.ToString ();
+
+                        // Normalize to Form C (Canonical Composition)
+                        string normalized = combined.Normalize (NormalizationForm.FormC);
+
+                        if (normalized.Length == 1)
+                        {
+                            // It normalized! We can just set the Cell to the left with the
+                            // normalized codepoint 
+                            Contents [Row, Col - 1].Rune = (Rune)normalized [0];
+
+                            // Ignore. Don't move to next column because we're already there
+                        }
+                        else
+                        {
+                            // It didn't normalize. Add it to the Cell to left's CM list
+                            Contents [Row, Col - 1].CombiningMarks.Add (rune);
+
+                            // Ignore. Don't move to next column (let the driver figure out what to do).
+                        }
+                    }
+
+                    Contents [Row, Col - 1].Attribute = CurrentAttribute;
+                    Contents [Row, Col - 1].IsDirty = true;
+                }
+                else
+                {
+                    // Most drivers will render a combining mark at col 0 as the mark
+                    Contents [Row, Col].Rune = rune;
+                    Contents [Row, Col].Attribute = CurrentAttribute;
+                    Contents [Row, Col].IsDirty = true;
+                    Col++;
+                }
+            }
+            else
+            {
+                Contents [Row, Col].Attribute = CurrentAttribute;
+                Contents [Row, Col].IsDirty = true;
+
+                if (Col > 0)
+                {
+                    // Check if cell to left has a wide glyph
+                    if (Contents [Row, Col - 1].Rune.GetColumns () > 1)
+                    {
+                        // Invalidate cell to left
+                        Contents [Row, Col - 1].Rune = Rune.ReplacementChar;
+                        Contents [Row, Col - 1].IsDirty = true;
+                    }
+                }
+
+                if (runeWidth < 1)
+                {
+                    Contents [Row, Col].Rune = Rune.ReplacementChar;
+                }
+                else if (runeWidth == 1)
+                {
+                    Contents [Row, Col].Rune = rune;
+
+                    if (Col < Clip.Right - 1)
+                    {
+                        Contents [Row, Col + 1].IsDirty = true;
+                    }
+                }
+                else if (runeWidth == 2)
+                {
+                    if (Col == Clip.Right - 1)
+                    {
+                        // We're at the right edge of the clip, so we can't display a wide character.
+                        // TODO: Figure out if it is better to show a replacement character or ' '
+                        Contents [Row, Col].Rune = Rune.ReplacementChar;
+                    }
+                    else
+                    {
+                        Contents [Row, Col].Rune = rune;
+
+                        if (Col < Clip.Right - 1)
+                        {
+                            // Invalidate cell to right so that it doesn't get drawn
+                            // TODO: Figure out if it is better to show a replacement character or ' '
+                            Contents [Row, Col + 1].Rune = Rune.ReplacementChar;
+                            Contents [Row, Col + 1].IsDirty = true;
+                        }
+                    }
+                }
+                else
+                {
+                    // This is a non-spacing character, so we don't need to do anything
+                    Contents [Row, Col].Rune = (Rune)' ';
+                    Contents [Row, Col].IsDirty = false;
+                }
+
+                _dirtyLines [Row] = true;
+            }
+        }
+
+        if (runeWidth is < 0 or > 0)
+        {
+            Col++;
+        }
+
+        if (runeWidth > 1)
+        {
+            Debug.Assert (runeWidth <= 2);
+
+            if (validLocation && Col < Clip.Right)
+            {
+                // This is a double-width character, and we are not at the end of the line.
+                // Col now points to the second column of the character. Ensure it doesn't
+                // Get rendered.
+                Contents [Row, Col].IsDirty = false;
+                Contents [Row, Col].Attribute = CurrentAttribute;
+
+                // TODO: Determine if we should wipe this out (for now now)
+                //Contents [Row, Col].Rune = (Rune)' ';
+            }
+
+            Col++;
+        }
+    }
+
+    /// <summary>
+    ///     Adds the specified <see langword="char"/> to the display at the current cursor position. This method is a
+    ///     convenience method that calls <see cref="AddRune(Rune)"/> with the <see cref="Rune"/> constructor.
+    /// </summary>
+    /// <param name="c">Character to add.</param>
+    public void AddRune (char c) { AddRune (new Rune (c)); }
+
+    /// <summary>Adds the <paramref name="str"/> to the display at the cursor position.</summary>
+    /// <remarks>
+    ///     <para>
+    ///         When the method returns, <see cref="Col"/> will be incremented by the number of columns
+    ///         <paramref name="str"/> required, unless the new column value is outside of the <see cref="Clip"/> or screen
+    ///         dimensions defined by <see cref="Cols"/>.
+    ///     </para>
+    ///     <para>If <paramref name="str"/> requires more columns than are available, the output will be clipped.</para>
+    /// </remarks>
+    /// <param name="str">String.</param>
+    public void AddStr (string str)
+    {
+        List<Rune> runes = str.EnumerateRunes ().ToList ();
+
+        for (var i = 0; i < runes.Count; i++)
+        {
+            //if (runes [i].IsCombiningMark()) {
+
+            //	// Attempt to normalize
+            //	string combined = runes [i-1] + runes [i].ToString();
+
+            //	// Normalize to Form C (Canonical Composition)
+            //	string normalized = combined.Normalize (NormalizationForm.FormC);
+
+            //	runes [i-]
+            //}
+            AddRune (runes [i]);
+        }
+    }
+
+    /// <summary>Clears the <see cref="Contents"/> of the driver.</summary>
+    public void ClearContents ()
+    {
+        // TODO: This method is really "Clear Contents" now and should not be abstract (or virtual)
+        Contents = new Cell [Rows, Cols];
+        //CONCURRENCY: Unsynchronized access to Clip isn't safe.
+        Clip = new (0, 0, Cols, Rows);
+        _dirtyLines = new bool [Rows];
+
+        lock (Contents)
+        {
+            // Can raise an exception while is still resizing.
+            try
+            {
+                for (var row = 0; row < Rows; row++)
+                {
+                    for (var c = 0; c < Cols; c++)
+                    {
+                        Contents [row, c] = new Cell
+                        {
+                            Rune = (Rune)' ', Attribute = new Attribute (Color.White, Color.Black), IsDirty = true
+                        };
+                        _dirtyLines [row] = true;
+                    }
+                }
+            }
+            catch (IndexOutOfRangeException)
+            { }
+        }
+    }
+
+    /// <summary>Determines if the terminal cursor should be visible or not and sets it accordingly.</summary>
+    /// <returns><see langword="true"/> upon success</returns>
+    public abstract bool EnsureCursorVisibility ();
+
+    // TODO: Move FillRect to ./Drawing	
+    /// <summary>Fills the specified rectangle with the specified rune.</summary>
+    /// <param name="rect"></param>
+    /// <param name="rune"></param>
+    public void FillRect (Rectangle rect, Rune rune = default)
+    {
+        for (int r = rect.Y; r < rect.Y + rect.Height; r++)
+        {
+            for (int c = rect.X; c < rect.X + rect.Width; c++)
+            {
+                Application.Driver.Move (c, r);
+                Application.Driver.AddRune (rune == default (Rune) ? new Rune (' ') : rune);
+            }
+        }
+    }
+
+    /// <summary>
+    ///     Fills the specified rectangle with the specified <see langword="char"/>. This method is a convenience method
+    ///     that calls <see cref="FillRect(Rectangle, Rune)"/>.
+    /// </summary>
+    /// <param name="rect"></param>
+    /// <param name="c"></param>
+    public void FillRect (Rectangle rect, char c) { FillRect (rect, new Rune (c)); }
+
+    /// <summary>Gets the terminal cursor visibility.</summary>
+    /// <param name="visibility">The current <see cref="CursorVisibility"/></param>
+    /// <returns><see langword="true"/> upon success</returns>
+    public abstract bool GetCursorVisibility (out CursorVisibility visibility);
+
+    /// <summary>Returns the name of the driver and relevant library version information.</summary>
+    /// <returns></returns>
+    public virtual string GetVersionInfo () { return GetType ().Name; }
+
+    /// <summary>Tests if the specified rune is supported by the driver.</summary>
+    /// <param name="rune"></param>
+    /// <returns>
+    ///     <see langword="true"/> if the rune can be properly presented; <see langword="false"/> if the driver does not
+    ///     support displaying this rune.
+    /// </returns>
+    public virtual bool IsRuneSupported (Rune rune) { return Rune.IsValid (rune.Value); }
+
+    /// <summary>Tests whether the specified coordinate are valid for drawing.</summary>
+    /// <param name="col">The column.</param>
+    /// <param name="row">The row.</param>
+    /// <returns>
+    ///     <see langword="false"/> if the coordinate is outside of the screen bounds or outside of <see cref="Clip"/>.
+    ///     <see langword="true"/> otherwise.
+    /// </returns>
+    public bool IsValidLocation (int col, int row) { return col >= 0 && row >= 0 && col < Cols && row < Rows && Clip.Contains (col, row); }
+
+    /// <summary>
+    ///     Updates <see cref="Col"/> and <see cref="Row"/> to the specified column and row in <see cref="Contents"/>.
+    ///     Used by <see cref="AddRune(Rune)"/> and <see cref="AddStr"/> to determine where to add content.
+    /// </summary>
+    /// <remarks>
+    ///     <para>This does not move the cursor on the screen, it only updates the internal state of the driver.</para>
+    ///     <para>
+    ///         If <paramref name="col"/> or <paramref name="row"/> are negative or beyond  <see cref="Cols"/> and
+    ///         <see cref="Rows"/>, the method still sets those properties.
+    ///     </para>
+    /// </remarks>
+    /// <param name="col">Column to move to.</param>
+    /// <param name="row">Row to move to.</param>
+    public virtual void Move (int col, int row)
+    {
+        Col = col;
+        Row = row;
+    }
+
+    /// <summary>Called when the terminal size changes. Fires the <see cref="SizeChanged"/> event.</summary>
+    /// <param name="args"></param>
+    public void OnSizeChanged (SizeChangedEventArgs args) { SizeChanged?.Invoke (this, args); }
+
+    /// <summary>Updates the screen to reflect all the changes that have been done to the display buffer</summary>
+    public abstract void Refresh ();
+
+    /// <summary>Sets the terminal cursor visibility.</summary>
+    /// <param name="visibility">The wished <see cref="CursorVisibility"/></param>
+    /// <returns><see langword="true"/> upon success</returns>
+    public abstract bool SetCursorVisibility (CursorVisibility visibility);
+
+    /// <summary>The event fired when the terminal is resized.</summary>
+    public event EventHandler<SizeChangedEventArgs> SizeChanged;
+
+    /// <summary>Suspends the application (e.g. on Linux via SIGTSTP) and upon resume, resets the console driver.</summary>
+    /// <remarks>This is only implemented in <see cref="CursesDriver"/>.</remarks>
+    public abstract void Suspend ();
+
+    /// <summary>Sets the position of the terminal cursor to <see cref="Col"/> and <see cref="Row"/>.</summary>
+    public abstract void UpdateCursor ();
+
+    /// <summary>Redraws the physical screen with the contents that have been queued up via any of the printing commands.</summary>
+    public abstract void UpdateScreen ();
+
+    #region Setup & Teardown
+
+    /// <summary>Initializes the driver</summary>
+    /// <returns>Returns an instance of <see cref="MainLoop"/> using the <see cref="IMainLoopDriver"/> for the driver.</returns>
+    internal abstract MainLoop Init ();
+
+    /// <summary>Ends the execution of the console driver.</summary>
+    internal abstract void End ();
+
+    #endregion
+
+    #region Color Handling
+
+    /// <summary>Gets whether the <see cref="ConsoleDriver"/> supports TrueColor output.</summary>
+    public virtual bool SupportsTrueColor => true;
+
+    /// <summary>
+    ///     Gets or sets whether the <see cref="ConsoleDriver"/> should use 16 colors instead of the default TrueColors.
+    ///     See <see cref="Application.Force16Colors"/> to change this setting via <see cref="ConfigurationManager"/>.
+    /// </summary>
+    /// <remarks>
+    ///     <para>
+    ///         Will be forced to <see langword="true"/> if <see cref="ConsoleDriver.SupportsTrueColor"/> is
+    ///         <see langword="false"/>, indicating that the <see cref="ConsoleDriver"/> cannot support TrueColor.
+    ///     </para>
+    /// </remarks>
+    internal virtual bool Force16Colors
+    {
+        get => Application.Force16Colors || !SupportsTrueColor;
+        set => Application.Force16Colors = value || !SupportsTrueColor;
+    }
+
+    private Attribute _currentAttribute;
+    private int _cols;
+    private int _rows;
+
+    /// <summary>
+    ///     The <see cref="Attribute"/> that will be used for the next <see cref="AddRune(Rune)"/> or <see cref="AddStr"/>
+    ///     call.
+    /// </summary>
+    public Attribute CurrentAttribute
+    {
+        get => _currentAttribute;
+        set
+        {
+            if (Application.Driver is { })
+            {
+                _currentAttribute = new Attribute (value.Foreground, value.Background);
+
+                return;
+            }
+
+            _currentAttribute = value;
+        }
+    }
+
+    /// <summary>Selects the specified attribute as the attribute to use for future calls to AddRune and AddString.</summary>
+    /// <remarks>Implementations should call <c>base.SetAttribute(c)</c>.</remarks>
+    /// <param name="c">C.</param>
+    public Attribute SetAttribute (Attribute c)
+    {
+        Attribute prevAttribute = CurrentAttribute;
+        CurrentAttribute = c;
+
+        return prevAttribute;
+    }
+
+    /// <summary>Gets the current <see cref="Attribute"/>.</summary>
+    /// <returns>The current attribute.</returns>
+    public Attribute GetAttribute () { return CurrentAttribute; }
+
+    // TODO: This is only overridden by CursesDriver. Once CursesDriver supports 24-bit color, this virtual method can be
+    // removed (and Attribute can lose the platformColor property).
+    /// <summary>Makes an <see cref="Attribute"/>.</summary>
+    /// <param name="foreground">The foreground color.</param>
+    /// <param name="background">The background color.</param>
+    /// <returns>The attribute for the foreground and background colors.</returns>
+    public virtual Attribute MakeColor (in Color foreground, in Color background)
+    {
+        // Encode the colors into the int value.
+        return new Attribute (
+                              -1, // only used by cursesdriver!
+                              foreground,
+                              background
+                             );
+    }
+
+    #endregion
+
+    #region Mouse and Keyboard
+
+    /// <summary>Event fired when a key is pressed down. This is a precursor to <see cref="KeyUp"/>.</summary>
+    public event EventHandler<Key> KeyDown;
+
+    /// <summary>
+    ///     Called when a key is pressed down. Fires the <see cref="KeyDown"/> event. This is a precursor to
+    ///     <see cref="OnKeyUp"/>.
+    /// </summary>
+    /// <param name="a"></param>
+    public void OnKeyDown (Key a) { KeyDown?.Invoke (this, a); }
+
+    /// <summary>Event fired when a key is released.</summary>
+    /// <remarks>
+    ///     Drivers that do not support key release events will fire this event after <see cref="KeyDown"/> processing is
+    ///     complete.
+    /// </remarks>
+    public event EventHandler<Key> KeyUp;
+
+    /// <summary>Called when a key is released. Fires the <see cref="KeyUp"/> event.</summary>
+    /// <remarks>
+    ///     Drivers that do not support key release events will calls this method after <see cref="OnKeyDown"/> processing
+    ///     is complete.
+    /// </remarks>
+    /// <param name="a"></param>
+    public void OnKeyUp (Key a) { KeyUp?.Invoke (this, a); }
+
+    /// <summary>Event fired when a mouse event occurs.</summary>
+    public event EventHandler<MouseEventEventArgs> MouseEvent;
+
+    /// <summary>Called when a mouse event occurs. Fires the <see cref="MouseEvent"/> event.</summary>
+    /// <param name="a"></param>
+    public void OnMouseEvent (MouseEventEventArgs a) { MouseEvent?.Invoke (this, a); }
+
+    /// <summary>Simulates a key press.</summary>
+    /// <param name="keyChar">The key character.</param>
+    /// <param name="key">The key.</param>
+    /// <param name="shift">If <see langword="true"/> simulates the Shift key being pressed.</param>
+    /// <param name="alt">If <see langword="true"/> simulates the Alt key being pressed.</param>
+    /// <param name="ctrl">If <see langword="true"/> simulates the Ctrl key being pressed.</param>
+    public abstract void SendKeys (char keyChar, ConsoleKey key, bool shift, bool alt, bool ctrl);
+
+    #endregion
 }
 
-/// <summary>
-/// Terminal Cursor Visibility settings.
-/// </summary>
+/// <summary>Terminal Cursor Visibility settings.</summary>
 /// <remarks>
-/// Hex value are set as 0xAABBCCDD where :
-///
-///     AA stand for the TERMINFO DECSUSR parameter value to be used under Linux and MacOS
-///     BB stand for the NCurses curs_set parameter value to be used under Linux and MacOS
-///     CC stand for the CONSOLE_CURSOR_INFO.bVisible parameter value to be used under Windows
-///     DD stand for the CONSOLE_CURSOR_INFO.dwSize parameter value to be used under Windows
-///</remarks>
-public enum CursorVisibility {
-	/// <summary>
-	///	Cursor caret has default
-	/// </summary>
-	/// <remarks>Works under Xterm-like terminal otherwise this is equivalent to <see ref="Underscore"/>. This default directly depends of the XTerm user configuration settings so it could be Block, I-Beam, Underline with possible blinking.</remarks>
-	Default = 0x00010119,
-
-	/// <summary>
-	///	Cursor caret is hidden
-	/// </summary>
-	Invisible = 0x03000019,
-
-	/// <summary>
-	///	Cursor caret is normally shown as a blinking underline bar _
-	/// </summary>
-	Underline = 0x03010119,
-
-	/// <summary>
-	///	Cursor caret is normally shown as a underline bar _
-	/// </summary>
-	/// <remarks>Under Windows, this is equivalent to <see ref="UnderscoreBlinking"/></remarks>
-	UnderlineFix = 0x04010119,
-
-	/// <summary>
-	///	Cursor caret is displayed a blinking vertical bar |
-	/// </summary>
-	/// <remarks>Works under Xterm-like terminal otherwise this is equivalent to <see ref="Underscore"/></remarks>
-	Vertical = 0x05010119,
-
-	/// <summary>
-	///	Cursor caret is displayed a blinking vertical bar |
-	/// </summary>
-	/// <remarks>Works under Xterm-like terminal otherwise this is equivalent to <see ref="Underscore"/></remarks>
-	VerticalFix = 0x06010119,
-
-	/// <summary>
-	///	Cursor caret is displayed as a blinking block ▉
-	/// </summary>
-	Box = 0x01020164,
-
-	/// <summary>
-	///	Cursor caret is displayed a block ▉
-	/// </summary>
-	/// <remarks>Works under Xterm-like terminal otherwise this is equivalent to <see ref="Block"/></remarks>
-	BoxFix = 0x02020164
+///     Hex value are set as 0xAABBCCDD where : AA stand for the TERMINFO DECSUSR parameter value to be used under
+///     Linux and MacOS BB stand for the NCurses curs_set parameter value to be used under Linux and MacOS CC stand for the
+///     CONSOLE_CURSOR_INFO.bVisible parameter value to be used under Windows DD stand for the CONSOLE_CURSOR_INFO.dwSize
+///     parameter value to be used under Windows
+/// </remarks>
+public enum CursorVisibility
+{
+    /// <summary>Cursor caret has default</summary>
+    /// <remarks>
+    ///     Works under Xterm-like terminal otherwise this is equivalent to <see ref="Underscore"/>. This default directly
+    ///     depends of the XTerm user configuration settings so it could be Block, I-Beam, Underline with possible blinking.
+    /// </remarks>
+    Default = 0x00010119,
+
+    /// <summary>Cursor caret is hidden</summary>
+    Invisible = 0x03000019,
+
+    /// <summary>Cursor caret is normally shown as a blinking underline bar _</summary>
+    Underline = 0x03010119,
+
+    /// <summary>Cursor caret is normally shown as a underline bar _</summary>
+    /// <remarks>Under Windows, this is equivalent to <see ref="UnderscoreBlinking"/></remarks>
+    UnderlineFix = 0x04010119,
+
+    /// <summary>Cursor caret is displayed a blinking vertical bar |</summary>
+    /// <remarks>Works under Xterm-like terminal otherwise this is equivalent to <see ref="Underscore"/></remarks>
+    Vertical = 0x05010119,
+
+    /// <summary>Cursor caret is displayed a blinking vertical bar |</summary>
+    /// <remarks>Works under Xterm-like terminal otherwise this is equivalent to <see ref="Underscore"/></remarks>
+    VerticalFix = 0x06010119,
+
+    /// <summary>Cursor caret is displayed as a blinking block ▉</summary>
+    Box = 0x01020164,
+
+    /// <summary>Cursor caret is displayed a block ▉</summary>
+    /// <remarks>Works under Xterm-like terminal otherwise this is equivalent to <see ref="Block"/></remarks>
+    BoxFix = 0x02020164
 }
 
 /// <summary>
-/// The <see cref="KeyCode"/> enumeration encodes key information from <see cref="ConsoleDriver"/>s and provides a consistent
-/// way for application code to specify keys and receive key events. 
-/// <para>
-/// The <see cref="Key"/> class provides a higher-level abstraction, with helper methods and properties for common
-/// operations. For example, <see cref="Key.IsAlt"/> and <see cref="Key.IsCtrl"/> provide a convenient way
-/// to check whether the Alt or Ctrl modifier keys were pressed when a key was pressed.
-/// </para>
+///     The <see cref="KeyCode"/> enumeration encodes key information from <see cref="ConsoleDriver"/>s and provides a
+///     consistent way for application code to specify keys and receive key events.
+///     <para>
+///         The <see cref="Key"/> class provides a higher-level abstraction, with helper methods and properties for
+///         common operations. For example, <see cref="Key.IsAlt"/> and <see cref="Key.IsCtrl"/> provide a convenient way
+///         to check whether the Alt or Ctrl modifier keys were pressed when a key was pressed.
+///     </para>
 /// </summary>
 /// <remarks>
-/// <para>
-/// Lowercase alpha keys are encoded as values between 65 and 90 corresponding to the un-shifted A to Z keys on a keyboard. Enum values
-/// are provided for these (e.g. <see cref="KeyCode.A"/>, <see cref="KeyCode.B"/>, etc.). Even though the values are the same as the ASCII
-/// values for uppercase characters, these enum values represent *lowercase*, un-shifted characters.
-/// </para>
-/// <para>
-/// Numeric keys are the values between 48 and 57 corresponding to 0 to 9 (e.g. <see cref="KeyCode.D0"/>, <see cref="KeyCode.D1"/>, etc.).
-/// </para>
-/// <para>
-/// The shift modifiers (<see cref="KeyCode.ShiftMask"/>, <see cref="KeyCode.CtrlMask"/>, and <see cref="KeyCode.AltMask"/>) can be combined (with logical or)
-/// with the other key codes to represent shifted keys. For example, the <see cref="KeyCode.A"/> enum value represents the un-shifted 'a' key, while
-/// <see cref="KeyCode.ShiftMask"/> | <see cref="KeyCode.A"/> represents the 'A' key (shifted 'a' key). Likewise, <see cref="KeyCode.AltMask"/> | <see cref="KeyCode.A"/>
-/// represents the 'Alt+A' key combination.
-/// </para>
-/// <para>
-/// All other keys that produce a printable character are encoded as the Unicode value of the character. For example, the <see cref="KeyCode"/>
-/// for the '!' character is 33, which is the Unicode value for '!'. Likewise, `â` is 226, `Â` is 194, etc.
-/// </para>
-/// <para>
-/// If the <see cref="SpecialMask"/> is set, then the value is that of the special mask,
-/// otherwise, the value is the one of the lower bits (as extracted by <see cref="CharMask"/>).
-/// </para>
+///     <para>
+///         Lowercase alpha keys are encoded as values between 65 and 90 corresponding to the un-shifted A to Z keys on a
+///         keyboard. Enum values are provided for these (e.g. <see cref="KeyCode.A"/>, <see cref="KeyCode.B"/>, etc.).
+///         Even though the values are the same as the ASCII values for uppercase characters, these enum values represent
+///         *lowercase*, un-shifted characters.
+///     </para>
+///     <para>
+///         Numeric keys are the values between 48 and 57 corresponding to 0 to 9 (e.g. <see cref="KeyCode.D0"/>,
+///         <see cref="KeyCode.D1"/>, etc.).
+///     </para>
+///     <para>
+///         The shift modifiers (<see cref="KeyCode.ShiftMask"/>, <see cref="KeyCode.CtrlMask"/>, and
+///         <see cref="KeyCode.AltMask"/>) can be combined (with logical or) with the other key codes to represent shifted
+///         keys. For example, the <see cref="KeyCode.A"/> enum value represents the un-shifted 'a' key, while
+///         <see cref="KeyCode.ShiftMask"/> | <see cref="KeyCode.A"/> represents the 'A' key (shifted 'a' key). Likewise,
+///         <see cref="KeyCode.AltMask"/> | <see cref="KeyCode.A"/> represents the 'Alt+A' key combination.
+///     </para>
+///     <para>
+///         All other keys that produce a printable character are encoded as the Unicode value of the character. For
+///         example, the <see cref="KeyCode"/> for the '!' character is 33, which is the Unicode value for '!'. Likewise,
+///         `â` is 226, `Â` is 194, etc.
+///     </para>
+///     <para>
+///         If the <see cref="SpecialMask"/> is set, then the value is that of the special mask, otherwise, the value is
+///         the one of the lower bits (as extracted by <see cref="CharMask"/>).
+///     </para>
 /// </remarks>
 [Flags]
-public enum KeyCode : uint {
-	/// <summary>
-	/// Mask that indicates that the key is a unicode codepoint. Values outside this range
-	/// indicate the key has shift modifiers or is a special key like function keys, arrows keys and so on.
-	/// </summary>
-	CharMask = 0x_f_ffff,
-
-	/// <summary>
-	/// If the <see cref="SpecialMask"/> is set, then the value is that of the special mask,
-	/// otherwise, the value is in the the lower bits (as extracted by <see cref="CharMask"/>).
-	/// </summary>
-	SpecialMask = 0x_fff0_0000,
-
-	/// <summary>
-	/// When this value is set, the Key encodes the sequence Shift-KeyValue.
-	/// The actual value must be extracted by removing the ShiftMask.
-	/// </summary>
-	ShiftMask = 0x_1000_0000,
-
-	/// <summary>
-	/// When this value is set, the Key encodes the sequence Alt-KeyValue.
-	/// The actual value must be extracted by removing the AltMask.
-	/// </summary>
-	AltMask = 0x_8000_0000,
-
-	/// <summary>
-	/// When this value is set, the Key encodes the sequence Ctrl-KeyValue.
-	/// The actual value must be extracted by removing the CtrlMask.
-	/// </summary>
-	CtrlMask = 0x_4000_0000,
-
-	/// <summary>
-	/// The key code representing an invalid or empty key.
-	/// </summary>
-	Null = 0,
-
-	/// <summary>
-	/// Backspace key.
-	/// </summary>
-	Backspace = 8,
-
-	/// <summary>
-	/// The key code for the tab key (forwards tab key).
-	/// </summary>
-	Tab = 9,
-
-	/// <summary>
-	/// The key code for the return key.
-	/// </summary>
-	Enter = ConsoleKey.Enter,
-
-	/// <summary>
-	/// The key code for the clear key.
-	/// </summary>
-	Clear = 12,
-
-	/// <summary>
-	/// The key code for the escape key.
-	/// </summary>
-	Esc = 27,
-
-	/// <summary>
-	/// The key code for the space bar key.
-	/// </summary>
-	Space = 32,
-
-	/// <summary>
-	/// Digit 0.
-	/// </summary>
-	D0 = 48,
-
-	/// <summary>
-	/// Digit 1.
-	/// </summary>
-	D1,
-
-	/// <summary>
-	/// Digit 2.
-	/// </summary>
-	D2,
-
-	/// <summary>
-	/// Digit 3.
-	/// </summary>
-	D3,
-
-	/// <summary>
-	/// Digit 4.
-	/// </summary>
-	D4,
-
-	/// <summary>
-	/// Digit 5.
-	/// </summary>
-	D5,
-
-	/// <summary>
-	/// Digit 6.
-	/// </summary>
-	D6,
-
-	/// <summary>
-	/// Digit 7.
-	/// </summary>
-	D7,
-
-	/// <summary>
-	/// Digit 8.
-	/// </summary>
-	D8,
-
-	/// <summary>
-	/// Digit 9.
-	/// </summary>
-	D9,
-
-	/// <summary>
-	/// The key code for the A key
-	/// </summary>
-	A = 65,
-
-	/// <summary>
-	/// The key code for the B key
-	/// </summary>
-	B,
-
-	/// <summary>
-	/// The key code for the C key
-	/// </summary>
-	C,
-
-	/// <summary>
-	/// The key code for the D key
-	/// </summary>
-	D,
-
-	/// <summary>
-	/// The key code for the E key
-	/// </summary>
-	E,
-
-	/// <summary>
-	/// The key code for the F key
-	/// </summary>
-	F,
-
-	/// <summary>
-	/// The key code for the G key
-	/// </summary>
-	G,
-
-	/// <summary>
-	/// The key code for the H key
-	/// </summary>
-	H,
-
-	/// <summary>
-	/// The key code for the I key
-	/// </summary>
-	I,
-
-	/// <summary>
-	/// The key code for the J key
-	/// </summary>
-	J,
-
-	/// <summary>
-	/// The key code for the K key
-	/// </summary>
-	K,
-
-	/// <summary>
-	/// The key code for the L key
-	/// </summary>
-	L,
-
-	/// <summary>
-	/// The key code for the M key
-	/// </summary>
-	M,
-
-	/// <summary>
-	/// The key code for the N key
-	/// </summary>
-	N,
-
-	/// <summary>
-	/// The key code for the O key
-	/// </summary>
-	O,
-
-	/// <summary>
-	/// The key code for the P key
-	/// </summary>
-	P,
-
-	/// <summary>
-	/// The key code for the Q key
-	/// </summary>
-	Q,
-
-	/// <summary>
-	/// The key code for the R key
-	/// </summary>
-	R,
-
-	/// <summary>
-	/// The key code for the S key
-	/// </summary>
-	S,
-
-	/// <summary>
-	/// The key code for the T key
-	/// </summary>
-	T,
-
-	/// <summary>
-	/// The key code for the U key
-	/// </summary>
-	U,
-
-	/// <summary>
-	/// The key code for the V key
-	/// </summary>
-	V,
-
-	/// <summary>
-	/// The key code for the W key
-	/// </summary>
-	W,
-
-	/// <summary>
-	/// The key code for the X key
-	/// </summary>
-	X,
-
-	/// <summary>
-	/// The key code for the Y key
-	/// </summary>
-	Y,
-
-	/// <summary>
-	/// The key code for the Z key
-	/// </summary>
-	Z,
-
-	///// <summary>
-	///// The key code for the Delete key.
-	///// </summary>
-	//Delete = 127,
-
-	// --- Special keys ---
-	// The values below are common non-alphanum keys. Their values are 
-	// based on the .NET ConsoleKey values, which, in-turn are based on the
-	// VK_ values from the Windows API. 
-	// We add MaxCodePoint to avoid conflicts with the Unicode values.
-
-	/// <summary>
-	/// The maximum Unicode codepoint value. Used to encode the non-alphanumeric control
-	/// keys. 
-	/// </summary>
-	MaxCodePoint = 0x10FFFF,
-
-	/// <summary>
-	/// Cursor up key
-	/// </summary>
-	CursorUp = MaxCodePoint + ConsoleKey.UpArrow,
-
-	/// <summary>
-	/// Cursor down key.
-	/// </summary>
-	CursorDown = MaxCodePoint + ConsoleKey.DownArrow,
-
-	/// <summary>
-	/// Cursor left key.
-	/// </summary>
-	CursorLeft = MaxCodePoint + ConsoleKey.LeftArrow,
-
-	/// <summary>
-	/// Cursor right key.
-	/// </summary>
-	CursorRight = MaxCodePoint + ConsoleKey.RightArrow,
-
-	/// <summary>
-	/// Page Up key.
-	/// </summary>
-	PageUp = MaxCodePoint + ConsoleKey.PageUp,
-
-	/// <summary>
-	/// Page Down key.
-	/// </summary>
-	PageDown = MaxCodePoint + ConsoleKey.PageDown,
-
-	/// <summary>
-	/// Home key.
-	/// </summary>
-	Home = MaxCodePoint + ConsoleKey.Home,
-
-	/// <summary>
-	/// End key.
-	/// </summary>
-	End = MaxCodePoint + ConsoleKey.End,
-
-	/// <summary>
-	/// Insert (INS) key.
-	/// </summary>
-	Insert = MaxCodePoint + ConsoleKey.Insert,
-
-	/// <summary>
-	/// Delete (DEL) key.
-	/// </summary>
-	Delete = MaxCodePoint + ConsoleKey.Delete,
-
-	/// <summary>
-	/// Print screen character key.
-	/// </summary>
-	PrintScreen = MaxCodePoint + ConsoleKey.PrintScreen,
-
-	/// <summary>
-	/// F1 key.
-	/// </summary>
-	F1 = MaxCodePoint + ConsoleKey.F1,
-
-	/// <summary>
-	/// F2 key.
-	/// </summary>
-	F2 = MaxCodePoint + ConsoleKey.F2,
-
-	/// <summary>
-	/// F3 key.
-	/// </summary>
-	F3 = MaxCodePoint + ConsoleKey.F3,
-
-	/// <summary>
-	/// F4 key.
-	/// </summary>
-	F4 = MaxCodePoint + ConsoleKey.F4,
-
-	/// <summary>
-	/// F5 key.
-	/// </summary>
-	F5 = MaxCodePoint + ConsoleKey.F5,
-
-	/// <summary>
-	/// F6 key.
-	/// </summary>
-	F6 = MaxCodePoint + ConsoleKey.F6,
-
-	/// <summary>
-	/// F7 key.
-	/// </summary>
-	F7 = MaxCodePoint + ConsoleKey.F7,
-
-	/// <summary>
-	/// F8 key.
-	/// </summary>
-	F8 = MaxCodePoint + ConsoleKey.F8,
-
-	/// <summary>
-	/// F9 key.
-	/// </summary>
-	F9 = MaxCodePoint + ConsoleKey.F9,
-
-	/// <summary>
-	/// F10 key.
-	/// </summary>
-	F10 = MaxCodePoint + ConsoleKey.F10,
-
-	/// <summary>
-	/// F11 key.
-	/// </summary>
-	F11 = MaxCodePoint + ConsoleKey.F11,
-
-	/// <summary>
-	/// F12 key.
-	/// </summary>
-	F12 = MaxCodePoint + ConsoleKey.F12,
-
-	/// <summary>
-	/// F13 key.
-	/// </summary>
-	F13 = MaxCodePoint + ConsoleKey.F13,
-
-	/// <summary>
-	/// F14 key.
-	/// </summary>
-	F14 = MaxCodePoint + ConsoleKey.F14,
-
-	/// <summary>
-	/// F15 key.
-	/// </summary>
-	F15 = MaxCodePoint + ConsoleKey.F15,
-
-	/// <summary>
-	/// F16 key.
-	/// </summary>
-	F16 = MaxCodePoint + ConsoleKey.F16,
-
-	/// <summary>
-	/// F17 key.
-	/// </summary>
-	F17 = MaxCodePoint + ConsoleKey.F17,
-
-	/// <summary>
-	/// F18 key.
-	/// </summary>
-	F18 = MaxCodePoint + ConsoleKey.F18,
-
-	/// <summary>
-	/// F19 key.
-	/// </summary>
-	F19 = MaxCodePoint + ConsoleKey.F19,
-
-	/// <summary>
-	/// F20 key.
-	/// </summary>
-	F20 = MaxCodePoint + ConsoleKey.F20,
-
-	/// <summary>
-	/// F21 key.
-	/// </summary>
-	F21 = MaxCodePoint + ConsoleKey.F21,
-
-	/// <summary>
-	/// F22 key.
-	/// </summary>
-	F22 = MaxCodePoint + ConsoleKey.F22,
-
-	/// <summary>
-	/// F23 key.
-	/// </summary>
-	F23 = MaxCodePoint + ConsoleKey.F23,
-
-	/// <summary>
-	/// F24 key.
-	/// </summary>
-	F24 = MaxCodePoint + ConsoleKey.F24,
-}
+public enum KeyCode : uint
+{
+    /// <summary>
+    ///     Mask that indicates that the key is a unicode codepoint. Values outside this range indicate the key has shift
+    ///     modifiers or is a special key like function keys, arrows keys and so on.
+    /// </summary>
+    CharMask = 0x_f_ffff,
+
+    /// <summary>
+    ///     If the <see cref="SpecialMask"/> is set, then the value is that of the special mask, otherwise, the value is
+    ///     in the the lower bits (as extracted by <see cref="CharMask"/>).
+    /// </summary>
+    SpecialMask = 0x_fff0_0000,
+
+    /// <summary>
+    ///     When this value is set, the Key encodes the sequence Shift-KeyValue. The actual value must be extracted by
+    ///     removing the ShiftMask.
+    /// </summary>
+    ShiftMask = 0x_1000_0000,
+
+    /// <summary>
+    ///     When this value is set, the Key encodes the sequence Alt-KeyValue. The actual value must be extracted by
+    ///     removing the AltMask.
+    /// </summary>
+    AltMask = 0x_8000_0000,
+
+    /// <summary>
+    ///     When this value is set, the Key encodes the sequence Ctrl-KeyValue. The actual value must be extracted by
+    ///     removing the CtrlMask.
+    /// </summary>
+    CtrlMask = 0x_4000_0000,
+
+    /// <summary>The key code representing an invalid or empty key.</summary>
+    Null = 0,
+
+    /// <summary>Backspace key.</summary>
+    Backspace = 8,
+
+    /// <summary>The key code for the tab key (forwards tab key).</summary>
+    Tab = 9,
+
+    /// <summary>The key code for the return key.</summary>
+    Enter = ConsoleKey.Enter,
+
+    /// <summary>The key code for the clear key.</summary>
+    Clear = 12,
+
+    /// <summary>The key code for the escape key.</summary>
+    Esc = 27,
+
+    /// <summary>The key code for the space bar key.</summary>
+    Space = 32,
+
+    /// <summary>Digit 0.</summary>
+    D0 = 48,
+
+    /// <summary>Digit 1.</summary>
+    D1,
+
+    /// <summary>Digit 2.</summary>
+    D2,
+
+    /// <summary>Digit 3.</summary>
+    D3,
+
+    /// <summary>Digit 4.</summary>
+    D4,
+
+    /// <summary>Digit 5.</summary>
+    D5,
+
+    /// <summary>Digit 6.</summary>
+    D6,
+
+    /// <summary>Digit 7.</summary>
+    D7,
+
+    /// <summary>Digit 8.</summary>
+    D8,
+
+    /// <summary>Digit 9.</summary>
+    D9,
+
+    /// <summary>The key code for the A key</summary>
+    A = 65,
+
+    /// <summary>The key code for the B key</summary>
+    B,
+
+    /// <summary>The key code for the C key</summary>
+    C,
+
+    /// <summary>The key code for the D key</summary>
+    D,
+
+    /// <summary>The key code for the E key</summary>
+    E,
+
+    /// <summary>The key code for the F key</summary>
+    F,
+
+    /// <summary>The key code for the G key</summary>
+    G,
+
+    /// <summary>The key code for the H key</summary>
+    H,
+
+    /// <summary>The key code for the I key</summary>
+    I,
+
+    /// <summary>The key code for the J key</summary>
+    J,
+
+    /// <summary>The key code for the K key</summary>
+    K,
+
+    /// <summary>The key code for the L key</summary>
+    L,
+
+    /// <summary>The key code for the M key</summary>
+    M,
+
+    /// <summary>The key code for the N key</summary>
+    N,
+
+    /// <summary>The key code for the O key</summary>
+    O,
+
+    /// <summary>The key code for the P key</summary>
+    P,
+
+    /// <summary>The key code for the Q key</summary>
+    Q,
+
+    /// <summary>The key code for the R key</summary>
+    R,
+
+    /// <summary>The key code for the S key</summary>
+    S,
+
+    /// <summary>The key code for the T key</summary>
+    T,
+
+    /// <summary>The key code for the U key</summary>
+    U,
+
+    /// <summary>The key code for the V key</summary>
+    V,
+
+    /// <summary>The key code for the W key</summary>
+    W,
+
+    /// <summary>The key code for the X key</summary>
+    X,
+
+    /// <summary>The key code for the Y key</summary>
+    Y,
+
+    /// <summary>The key code for the Z key</summary>
+    Z,
+
+    ///// <summary>
+    ///// The key code for the Delete key.
+    ///// </summary>
+    //Delete = 127,
+
+    // --- Special keys ---
+    // The values below are common non-alphanum keys. Their values are 
+    // based on the .NET ConsoleKey values, which, in-turn are based on the
+    // VK_ values from the Windows API. 
+    // We add MaxCodePoint to avoid conflicts with the Unicode values.
+
+    /// <summary>The maximum Unicode codepoint value. Used to encode the non-alphanumeric control keys.</summary>
+    MaxCodePoint = 0x10FFFF,
+
+    /// <summary>Cursor up key</summary>
+    CursorUp = MaxCodePoint + ConsoleKey.UpArrow,
+
+    /// <summary>Cursor down key.</summary>
+    CursorDown = MaxCodePoint + ConsoleKey.DownArrow,
+
+    /// <summary>Cursor left key.</summary>
+    CursorLeft = MaxCodePoint + ConsoleKey.LeftArrow,
+
+    /// <summary>Cursor right key.</summary>
+    CursorRight = MaxCodePoint + ConsoleKey.RightArrow,
+
+    /// <summary>Page Up key.</summary>
+    PageUp = MaxCodePoint + ConsoleKey.PageUp,
+
+    /// <summary>Page Down key.</summary>
+    PageDown = MaxCodePoint + ConsoleKey.PageDown,
+
+    /// <summary>Home key.</summary>
+    Home = MaxCodePoint + ConsoleKey.Home,
+
+    /// <summary>End key.</summary>
+    End = MaxCodePoint + ConsoleKey.End,
+
+    /// <summary>Insert (INS) key.</summary>
+    Insert = MaxCodePoint + ConsoleKey.Insert,
+
+    /// <summary>Delete (DEL) key.</summary>
+    Delete = MaxCodePoint + ConsoleKey.Delete,
+
+    /// <summary>Print screen character key.</summary>
+    PrintScreen = MaxCodePoint + ConsoleKey.PrintScreen,
+
+    /// <summary>F1 key.</summary>
+    F1 = MaxCodePoint + ConsoleKey.F1,
+
+    /// <summary>F2 key.</summary>
+    F2 = MaxCodePoint + ConsoleKey.F2,
+
+    /// <summary>F3 key.</summary>
+    F3 = MaxCodePoint + ConsoleKey.F3,
+
+    /// <summary>F4 key.</summary>
+    F4 = MaxCodePoint + ConsoleKey.F4,
+
+    /// <summary>F5 key.</summary>
+    F5 = MaxCodePoint + ConsoleKey.F5,
+
+    /// <summary>F6 key.</summary>
+    F6 = MaxCodePoint + ConsoleKey.F6,
+
+    /// <summary>F7 key.</summary>
+    F7 = MaxCodePoint + ConsoleKey.F7,
+
+    /// <summary>F8 key.</summary>
+    F8 = MaxCodePoint + ConsoleKey.F8,
+
+    /// <summary>F9 key.</summary>
+    F9 = MaxCodePoint + ConsoleKey.F9,
+
+    /// <summary>F10 key.</summary>
+    F10 = MaxCodePoint + ConsoleKey.F10,
+
+    /// <summary>F11 key.</summary>
+    F11 = MaxCodePoint + ConsoleKey.F11,
+
+    /// <summary>F12 key.</summary>
+    F12 = MaxCodePoint + ConsoleKey.F12,
+
+    /// <summary>F13 key.</summary>
+    F13 = MaxCodePoint + ConsoleKey.F13,
+
+    /// <summary>F14 key.</summary>
+    F14 = MaxCodePoint + ConsoleKey.F14,
+
+    /// <summary>F15 key.</summary>
+    F15 = MaxCodePoint + ConsoleKey.F15,
+
+    /// <summary>F16 key.</summary>
+    F16 = MaxCodePoint + ConsoleKey.F16,
+
+    /// <summary>F17 key.</summary>
+    F17 = MaxCodePoint + ConsoleKey.F17,
+
+    /// <summary>F18 key.</summary>
+    F18 = MaxCodePoint + ConsoleKey.F18,
+
+    /// <summary>F19 key.</summary>
+    F19 = MaxCodePoint + ConsoleKey.F19,
+
+    /// <summary>F20 key.</summary>
+    F20 = MaxCodePoint + ConsoleKey.F20,
+
+    /// <summary>F21 key.</summary>
+    F21 = MaxCodePoint + ConsoleKey.F21,
+
+    /// <summary>F22 key.</summary>
+    F22 = MaxCodePoint + ConsoleKey.F22,
+
+    /// <summary>F23 key.</summary>
+    F23 = MaxCodePoint + ConsoleKey.F23,
+
+    /// <summary>F24 key.</summary>
+    F24 = MaxCodePoint + ConsoleKey.F24
+}

+ 2528 - 1718
Terminal.Gui/ConsoleDrivers/ConsoleKeyMapping.cs

@@ -1,1726 +1,2536 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.Linq;
+using System.Globalization;
 using System.Runtime.InteropServices;
-using System.Text;
 
 namespace Terminal.Gui.ConsoleDrivers;
 
-/// <summary>
-/// Helper class to handle the scan code and virtual key from a <see cref="ConsoleKey"/>.
-/// </summary>
-public static class ConsoleKeyMapping {
-
+/// <summary>Helper class to handle the scan code and virtual key from a <see cref="ConsoleKey"/>.</summary>
+public static class ConsoleKeyMapping
+{
 #if !WT_ISSUE_8871_FIXED // https://github.com/microsoft/terminal/issues/8871
-	/// <summary>
-	/// Translates (maps) a virtual-key code into a scan code or character value, or translates a scan code into a virtual-key code.
-	/// </summary>
-	/// <param name="vk"></param>
-	/// <param name="uMapType">
-	/// If MAPVK_VK_TO_CHAR (2) - The uCode parameter is a virtual-key code and is translated into an un-shifted
-	/// character value in the low order word of the return value. 
-	/// </param>
-	/// <param name="dwhkl"></param>
-	/// <returns>An un-shifted character value in the low order word of the return value. Dead keys (diacritics)
-	/// are indicated by setting the top bit of the return value. If there is no translation,
-	/// the function returns 0. See Remarks.</returns>
-	[DllImport ("user32.dll", EntryPoint = "MapVirtualKeyExW", CharSet = CharSet.Unicode)]
-	extern static uint MapVirtualKeyEx (VK vk, uint uMapType, IntPtr dwhkl);
-
-	/// <summary>
-	/// Retrieves the active input locale identifier (formerly called the keyboard layout).
-	/// </summary>
-	/// <param name="idThread">0 for current thread</param>
-	/// <returns>The return value is the input locale identifier for the thread.
-	/// The low word contains a Language Identifier for the input language
-	/// and the high word contains a device handle to the physical layout of the keyboard.
-	/// </returns>
-	[DllImport ("user32.dll", EntryPoint = "GetKeyboardLayout", CharSet = CharSet.Unicode)]
-	extern static IntPtr GetKeyboardLayout (IntPtr idThread);
-
-	//[DllImport ("user32.dll", EntryPoint = "GetKeyboardLayoutNameW", CharSet = CharSet.Unicode)]
-	//extern static uint GetKeyboardLayoutName (uint idThread);
-
-	[DllImport ("user32.dll")]
-	extern static IntPtr GetForegroundWindow ();
-
-	[DllImport ("user32.dll")]
-	extern static IntPtr GetWindowThreadProcessId (IntPtr hWnd, IntPtr ProcessId);
-
-	/// <summary>
-	/// Translates the specified virtual-key code and keyboard state to the corresponding Unicode character or characters using
-	/// the Win32 API MapVirtualKey.
-	/// </summary>
-	/// <param name="vk"></param>
-	/// <returns>An un-shifted character value in the low order word of the return value. Dead keys (diacritics)
-	/// are indicated by setting the top bit of the return value. If there is no translation,
-	/// the function returns 0.</returns>
-	public static uint MapVKtoChar (VK vk)
-	{
-		if (Environment.OSVersion.Platform != PlatformID.Win32NT) {
-			return 0;
-		}
-		var tid = GetWindowThreadProcessId (GetForegroundWindow (), 0);
-		var hkl = GetKeyboardLayout (tid);
-		return MapVirtualKeyEx (vk, 2, hkl);
-	}
+    /// <summary>
+    ///     Translates (maps) a virtual-key code into a scan code or character value, or translates a scan code into a
+    ///     virtual-key code.
+    /// </summary>
+    /// <param name="vk"></param>
+    /// <param name="uMapType">
+    ///     If MAPVK_VK_TO_CHAR (2) - The uCode parameter is a virtual-key code and is translated into an
+    ///     un-shifted character value in the low order word of the return value.
+    /// </param>
+    /// <param name="dwhkl"></param>
+    /// <returns>
+    ///     An un-shifted character value in the low order word of the return value. Dead keys (diacritics) are indicated
+    ///     by setting the top bit of the return value. If there is no translation, the function returns 0. See Remarks.
+    /// </returns>
+    [DllImport ("user32.dll", EntryPoint = "MapVirtualKeyExW", CharSet = CharSet.Unicode)]
+    private static extern uint MapVirtualKeyEx (VK vk, uint uMapType, nint dwhkl);
+
+    /// <summary>Retrieves the active input locale identifier (formerly called the keyboard layout).</summary>
+    /// <param name="idThread">0 for current thread</param>
+    /// <returns>
+    ///     The return value is the input locale identifier for the thread. The low word contains a Language Identifier
+    ///     for the input language and the high word contains a device handle to the physical layout of the keyboard.
+    /// </returns>
+    [DllImport ("user32.dll", EntryPoint = "GetKeyboardLayout", CharSet = CharSet.Unicode)]
+    private static extern nint GetKeyboardLayout (nint idThread);
+
+    //[DllImport ("user32.dll", EntryPoint = "GetKeyboardLayoutNameW", CharSet = CharSet.Unicode)]
+    //extern static uint GetKeyboardLayoutName (uint idThread);
+    [DllImport ("user32.dll")]
+    private static extern nint GetForegroundWindow ();
+
+    [DllImport ("user32.dll")]
+    private static extern nint GetWindowThreadProcessId (nint hWnd, nint ProcessId);
+
+    /// <summary>
+    ///     Translates the specified virtual-key code and keyboard state to the corresponding Unicode character or
+    ///     characters using the Win32 API MapVirtualKey.
+    /// </summary>
+    /// <param name="vk"></param>
+    /// <returns>
+    ///     An un-shifted character value in the low order word of the return value. Dead keys (diacritics) are indicated
+    ///     by setting the top bit of the return value. If there is no translation, the function returns 0.
+    /// </returns>
+    public static uint MapVKtoChar (VK vk)
+    {
+        if (Environment.OSVersion.Platform != PlatformID.Win32NT)
+        {
+            return 0;
+        }
+
+        nint tid = GetWindowThreadProcessId (GetForegroundWindow (), 0);
+        nint hkl = GetKeyboardLayout (tid);
+
+        return MapVirtualKeyEx (vk, 2, hkl);
+    }
 #else
-	/// <summary>
-	/// Translates (maps) a virtual-key code into a scan code or character value, or translates a scan code into a virtual-key code.
-	/// </summary>
-	/// <param name="vk"></param>
-	/// <param name="uMapType">
-	/// If MAPVK_VK_TO_CHAR (2) - The uCode parameter is a virtual-key code and is translated into an unshifted
-	/// character value in the low order word of the return value. 
-	/// </param>
-	/// <returns>An unshifted character value in the low order word of the return value. Dead keys (diacritics)
-	/// are indicated by setting the top bit of the return value. If there is no translation,
-	/// the function returns 0. See Remarks.</returns>
-	[DllImport ("user32.dll", EntryPoint = "MapVirtualKeyW", CharSet = CharSet.Unicode)]
-	extern static uint MapVirtualKey (VK vk, uint uMapType = 2);
-
-	uint MapVKtoChar (VK vk) => MapVirtualKeyToCharEx (vk);
+    /// <summary>
+    /// Translates (maps) a virtual-key code into a scan code or character value, or translates a scan code into a virtual-key code.
+    /// </summary>
+    /// <param name="vk"></param>
+    /// <param name="uMapType">
+    /// If MAPVK_VK_TO_CHAR (2) - The uCode parameter is a virtual-key code and is translated into an unshifted
+    /// character value in the low order word of the return value. 
+    /// </param>
+    /// <returns>An unshifted character value in the low order word of the return value. Dead keys (diacritics)
+    /// are indicated by setting the top bit of the return value. If there is no translation,
+    /// the function returns 0. See Remarks.</returns>
+    [DllImport ("user32.dll", EntryPoint = "MapVirtualKeyW", CharSet = CharSet.Unicode)]
+    extern static uint MapVirtualKey (VK vk, uint uMapType = 2);
+
+    uint MapVKtoChar (VK vk) => MapVirtualKeyToCharEx (vk);
 #endif
-	/// <summary>
-	/// Retrieves the name of the active input locale identifier (formerly called the keyboard layout) for the calling thread.
-	/// </summary>
-	/// <param name="pwszKLID"></param>
-	/// <returns></returns>
-	[DllImport ("user32.dll")]
-	extern static bool GetKeyboardLayoutName ([Out] StringBuilder pwszKLID);
-
-	/// <summary>
-	/// Retrieves the name of the active input locale identifier (formerly called the keyboard layout) for the calling thread.
-	/// </summary>
-	/// <returns></returns>
-	public static string GetKeyboardLayoutName ()
-	{
-		if (Environment.OSVersion.Platform != PlatformID.Win32NT) {
-			return "none";
-		}
-
-		StringBuilder klidSB = new StringBuilder ();
-		GetKeyboardLayoutName (klidSB);
-		return klidSB.ToString ();
-	}
-
-	class ScanCodeMapping : IEquatable<ScanCodeMapping> {
-		public uint ScanCode;
-		public VK VirtualKey;
-		public ConsoleModifiers Modifiers;
-		public uint UnicodeChar;
-
-		public ScanCodeMapping (uint scanCode, VK virtualKey, ConsoleModifiers modifiers, uint unicodeChar)
-		{
-			ScanCode = scanCode;
-			VirtualKey = virtualKey;
-			Modifiers = modifiers;
-			UnicodeChar = unicodeChar;
-		}
-
-		public bool Equals (ScanCodeMapping other)
-		{
-			return ScanCode.Equals (other.ScanCode) &&
-				VirtualKey.Equals (other.VirtualKey) &&
-				Modifiers.Equals (other.Modifiers) &&
-				UnicodeChar.Equals (other.UnicodeChar);
-		}
-	}
-
-	static ConsoleModifiers GetModifiers (ConsoleModifiers modifiers)
-	{
-		if (modifiers.HasFlag (ConsoleModifiers.Shift)
-		&& !modifiers.HasFlag (ConsoleModifiers.Alt)
-		&& !modifiers.HasFlag (ConsoleModifiers.Control)) {
-			return ConsoleModifiers.Shift;
-		} else if (modifiers == (ConsoleModifiers.Alt | ConsoleModifiers.Control)) {
-			return modifiers;
-		}
-
-		return 0;
-	}
-
-	static ScanCodeMapping GetScanCode (string propName, uint keyValue, ConsoleModifiers modifiers)
-	{
-		switch (propName) {
-		case "UnicodeChar":
-			var sCode = _scanCodes.FirstOrDefault ((e) => e.UnicodeChar == keyValue && e.Modifiers == modifiers);
-			if (sCode == null && modifiers == (ConsoleModifiers.Alt | ConsoleModifiers.Control)) {
-				return _scanCodes.FirstOrDefault ((e) => e.UnicodeChar == keyValue && e.Modifiers == 0);
-			}
-			return sCode;
-		case "VirtualKey":
-			sCode = _scanCodes.FirstOrDefault ((e) => e.VirtualKey == (VK)keyValue && e.Modifiers == modifiers);
-			if (sCode == null && modifiers == (ConsoleModifiers.Alt | ConsoleModifiers.Control)) {
-				return _scanCodes.FirstOrDefault ((e) => e.VirtualKey == (VK)keyValue && e.Modifiers == 0);
-			}
-			return sCode;
-		}
-
-		return null;
-	}
-
-	// BUGBUG: This API is not correct. It is only used by WindowsDriver in VKPacket scenarios
-	/// <summary>
-	/// Get the scan code from a <see cref="ConsoleKeyInfo"/>.
-	/// </summary>
-	/// <param name="consoleKeyInfo">The console key info.</param>
-	/// <returns>The value if apply.</returns>
-	public static uint GetScanCodeFromConsoleKeyInfo (ConsoleKeyInfo consoleKeyInfo)
-	{
-		var mod = GetModifiers (consoleKeyInfo.Modifiers);
-		ScanCodeMapping scode = GetScanCode ("VirtualKey", (uint)consoleKeyInfo.Key, mod);
-		if (scode != null) {
-			return scode.ScanCode;
-		}
-
-		return 0;
-	}
-
-	// BUGBUG: This API is not correct. It is only used by FakeDriver and VkeyPacketSimulator
-	/// <summary>
-	/// Gets the <see cref="ConsoleKeyInfo"/> from the provided <see cref="KeyCode"/>.
-	/// </summary>
-	/// <param name="key">The key code.</param>
-	/// <returns>The console key info.</returns>
-	public static ConsoleKeyInfo GetConsoleKeyInfoFromKeyCode (KeyCode key)
-	{
-		var modifiers = MapToConsoleModifiers (key);
-		var keyValue = MapKeyCodeToConsoleKey (key, out bool isConsoleKey);
-		if (isConsoleKey) {
-			var mod = GetModifiers (modifiers);
-			var scode = GetScanCode ("VirtualKey", (uint)keyValue, mod);
-			if (scode != null) {
-				return new ConsoleKeyInfo ((char)scode.UnicodeChar, (ConsoleKey)scode.VirtualKey, modifiers.HasFlag (ConsoleModifiers.Shift),
-					modifiers.HasFlag (ConsoleModifiers.Alt), modifiers.HasFlag (ConsoleModifiers.Control));
-			}
-		} else {
-			var keyChar = GetKeyCharFromUnicodeChar ((uint)keyValue, modifiers, out uint consoleKey, out _, isConsoleKey);
-			if (consoleKey != 0) {
-				return new ConsoleKeyInfo ((char)keyChar, (ConsoleKey)consoleKey, modifiers.HasFlag (ConsoleModifiers.Shift),
-					modifiers.HasFlag (ConsoleModifiers.Alt), modifiers.HasFlag (ConsoleModifiers.Control));
-			}
-		}
-
-		return new ConsoleKeyInfo ((char)keyValue, ConsoleKey.None, modifiers.HasFlag (ConsoleModifiers.Shift),
-			modifiers.HasFlag (ConsoleModifiers.Alt), modifiers.HasFlag (ConsoleModifiers.Control));
-	}
-
-	/// <summary>
-	/// Map existing <see cref="KeyCode"/> modifiers to <see cref="ConsoleModifiers"/>.
-	/// </summary>
-	/// <param name="key">The key code.</param>
-	/// <returns>The console modifiers.</returns>
-	public static ConsoleModifiers MapToConsoleModifiers (KeyCode key)
-	{
-		var modifiers = new ConsoleModifiers ();
-		if (key.HasFlag (KeyCode.ShiftMask)) {
-			modifiers |= ConsoleModifiers.Shift;
-		}
-		if (key.HasFlag (KeyCode.AltMask)) {
-			modifiers |= ConsoleModifiers.Alt;
-		}
-		if (key.HasFlag (KeyCode.CtrlMask)) {
-			modifiers |= ConsoleModifiers.Control;
-		}
-
-		return modifiers;
-	}
-
-	/// <summary>
-	/// Gets <see cref="ConsoleModifiers"/> from <see cref="bool"/> modifiers.
-	/// </summary>
-	/// <param name="shift">The shift key.</param>
-	/// <param name="alt">The alt key.</param>
-	/// <param name="control">The control key.</param>
-	/// <returns>The console modifiers.</returns>
-	public static ConsoleModifiers GetModifiers (bool shift, bool alt, bool control)
-	{
-		var modifiers = new ConsoleModifiers ();
-		if (shift) {
-			modifiers |= ConsoleModifiers.Shift;
-		}
-		if (alt) {
-			modifiers |= ConsoleModifiers.Alt;
-		}
-		if (control) {
-			modifiers |= ConsoleModifiers.Control;
-		}
-
-		return modifiers;
-	}
-
-	/// <summary>
-	/// Get the <see cref="ConsoleKeyInfo"/> from a unicode character and modifiers (e.g. (Key)'a' and (Key)Key.CtrlMask).
-	/// </summary>
-	/// <param name="keyValue">The key as a unicode codepoint.</param>
-	/// <param name="modifiers">The modifier keys.</param>
-	/// <param name="scanCode">The resulting scan code.</param>
-	/// <returns>The <see cref="ConsoleKeyInfo"/>.</returns>
-	static ConsoleKeyInfo GetConsoleKeyInfoFromKeyChar (uint keyValue, ConsoleModifiers modifiers, out uint scanCode)
-	{
-		scanCode = 0;
-		if (keyValue == 0) {
-			return new ConsoleKeyInfo ((char)keyValue, ConsoleKey.None, modifiers.HasFlag (ConsoleModifiers.Shift),
-				modifiers.HasFlag (ConsoleModifiers.Alt), modifiers.HasFlag (ConsoleModifiers.Control));
-		}
-
-		uint outputChar = keyValue;
-		uint consoleKey;
-		if (keyValue > byte.MaxValue) {
-			var sCode = _scanCodes.FirstOrDefault ((e) => e.UnicodeChar == keyValue);
-			if (sCode == null) {
-				consoleKey = (byte)(keyValue & byte.MaxValue);
-				sCode = _scanCodes.FirstOrDefault ((e) => e.VirtualKey == (VK)consoleKey);
-				if (sCode == null) {
-					consoleKey = 0;
-					outputChar = keyValue;
-				} else {
-					outputChar = (char)(keyValue >> 8);
-				}
-			} else {
-				consoleKey = (byte)sCode.VirtualKey;
-				outputChar = keyValue;
-			}
-		} else {
-			consoleKey = (byte)keyValue;
-			outputChar = '\0';
-		}
-
-		return new ConsoleKeyInfo ((char)outputChar, (ConsoleKey)consoleKey, modifiers.HasFlag (ConsoleModifiers.Shift),
-			modifiers.HasFlag (ConsoleModifiers.Alt), modifiers.HasFlag (ConsoleModifiers.Control));
-	}
-
-	// Used only by unit tests
-	internal static uint GetKeyChar (uint keyValue, ConsoleModifiers modifiers)
-	{
-		if (modifiers == ConsoleModifiers.Shift && keyValue - 32 is >= 'A' and <= 'Z') {
-			return keyValue - 32;
-		} else if (modifiers == ConsoleModifiers.None && keyValue is >= 'A' and <= 'Z') {
-			return keyValue + 32;
-		}
-
-		if (modifiers == ConsoleModifiers.Shift && keyValue - 32 is >= 'À' and <= 'Ý') {
-			return keyValue - 32;
-		} else if (modifiers == ConsoleModifiers.None && keyValue is >= 'À' and <= 'Ý') {
-			return keyValue + 32;
-		}
-
-		if (modifiers.HasFlag (ConsoleModifiers.Shift) && keyValue is '0') {
-			return keyValue + 13;
-		} else if (modifiers == ConsoleModifiers.None && keyValue - 13 is '0') {
-			return keyValue - 13;
-		}
-
-		if (modifiers.HasFlag (ConsoleModifiers.Shift) && keyValue is >= '1' and <= '9' and not '7') {
-			return keyValue - 16;
-		} else if (modifiers == ConsoleModifiers.None && keyValue + 16 is >= '1' and <= '9' and not '7') {
-			return keyValue + 16;
-		}
-
-		if (modifiers.HasFlag (ConsoleModifiers.Shift) && keyValue is '7') {
-			return keyValue - 8;
-		} else if (modifiers == ConsoleModifiers.None && keyValue + 8 is '7') {
-			return keyValue + 8;
-		}
-
-		if (modifiers.HasFlag (ConsoleModifiers.Shift) && keyValue is '\'') {
-			return keyValue + 24;
-		} else if (modifiers == ConsoleModifiers.None && keyValue - 24 is '\'') {
-			return keyValue - 24;
-		}
-
-		if (modifiers.HasFlag (ConsoleModifiers.Shift) && keyValue is '«') {
-			return keyValue + 16;
-		} else if (modifiers == ConsoleModifiers.None && keyValue - 16 is '«') {
-			return keyValue - 16;
-		}
-
-		if (modifiers.HasFlag (ConsoleModifiers.Shift) && keyValue is '\\') {
-			return keyValue + 32;
-		} else if (modifiers == ConsoleModifiers.None && keyValue - 32 is '\\') {
-			return keyValue - 32;
-		}
-
-		if (modifiers.HasFlag (ConsoleModifiers.Shift) && keyValue is '+') {
-			return keyValue - 1;
-		} else if (modifiers == ConsoleModifiers.None && keyValue + 1 is '+') {
-			return keyValue + 1;
-		}
-
-		if (modifiers.HasFlag (ConsoleModifiers.Shift) && keyValue is '´') {
-			return keyValue - 84;
-		} else if (modifiers == ConsoleModifiers.None && keyValue + 84 is '´') {
-			return keyValue + 84;
-		}
-
-		if (modifiers.HasFlag (ConsoleModifiers.Shift) && keyValue is 'º') {
-			return keyValue - 16;
-		} else if (modifiers == ConsoleModifiers.None && keyValue + 16 is 'º') {
-			return keyValue + 16;
-		}
-
-		if (modifiers.HasFlag (ConsoleModifiers.Shift) && keyValue is '~') {
-			return keyValue - 32;
-		} else if (modifiers == ConsoleModifiers.None && keyValue + 32 is '~') {
-			return keyValue + 32;
-		}
-
-		if (modifiers.HasFlag (ConsoleModifiers.Shift) && keyValue is '<') {
-			return keyValue + 2;
-		} else if (modifiers == ConsoleModifiers.None && keyValue - 2 is '<') {
-			return keyValue - 2;
-		}
-
-		if (modifiers.HasFlag (ConsoleModifiers.Shift) && keyValue is ',') {
-			return keyValue + 15;
-		} else if (modifiers == ConsoleModifiers.None && keyValue - 15 is ',') {
-			return keyValue - 15;
-		}
-		if (modifiers.HasFlag (ConsoleModifiers.Shift) && keyValue is '.') {
-			return keyValue + 12;
-		} else if (modifiers == ConsoleModifiers.None && keyValue - 12 is '.') {
-			return keyValue - 12;
-		}
-
-		if (modifiers.HasFlag (ConsoleModifiers.Shift) && keyValue is '-') {
-			return keyValue + 50;
-		} else if (modifiers == ConsoleModifiers.None && keyValue - 50 is '-') {
-			return keyValue - 50;
-		}
-
-		return keyValue;
-	}
-
-	/// <summary>
-	/// Get the output character from the <see cref="GetConsoleKeyInfoFromKeyCode"/>, with the correct <see cref="ConsoleKey"/>
-	/// and the scan code used on <see cref="WindowsDriver"/>.
-	/// </summary>
-	/// <param name="unicodeChar">The unicode character.</param>
-	/// <param name="modifiers">The modifiers keys.</param>
-	/// <param name="consoleKey">The resulting console key.</param>
-	/// <param name="scanCode">The resulting scan code.</param>
-	/// <param name="isConsoleKey">Indicates if the <paramref name="unicodeChar"/> is a <see cref="ConsoleKey"/>.</param>
-	/// <returns>The output character or the <paramref name="consoleKey"/>.</returns>
-	/// <remarks>This is only used by the <see cref="GetConsoleKeyInfoFromKeyCode"/> and by unit tests.</remarks>
-	internal static uint GetKeyCharFromUnicodeChar (uint unicodeChar, ConsoleModifiers modifiers, out uint consoleKey, out uint scanCode, bool isConsoleKey = false)
-	{
-		uint decodedChar = unicodeChar >> 8 == 0xff ? unicodeChar & 0xff : unicodeChar;
-		uint keyChar = decodedChar;
-		consoleKey = 0;
-		var mod = GetModifiers (modifiers);
-		scanCode = 0;
-		ScanCodeMapping scode = null;
-		if (unicodeChar != 0 && unicodeChar >> 8 != 0xff && isConsoleKey) {
-			scode = GetScanCode ("VirtualKey", decodedChar, mod);
-		}
-		if (isConsoleKey && scode != null) {
-			consoleKey = (uint)scode.VirtualKey;
-			keyChar = scode.UnicodeChar;
-			scanCode = scode.ScanCode;
-		}
-		if (scode == null) {
-			scode = unicodeChar != 0 ? GetScanCode ("UnicodeChar", decodedChar, mod) : null;
-			if (scode != null) {
-				consoleKey = (uint)scode.VirtualKey;
-				keyChar = scode.UnicodeChar;
-				scanCode = scode.ScanCode;
-			}
-		}
-		if (decodedChar != 0 && scanCode == 0 && char.IsLetter ((char)decodedChar)) {
-			string stFormD = ((char)decodedChar).ToString ().Normalize (System.Text.NormalizationForm.FormD);
-			for (int i = 0; i < stFormD.Length; i++) {
-				var uc = CharUnicodeInfo.GetUnicodeCategory (stFormD [i]);
-				if (uc != UnicodeCategory.NonSpacingMark && uc != UnicodeCategory.OtherLetter) {
-					consoleKey = char.ToUpper (stFormD [i]);
-					scode = GetScanCode ("VirtualKey", char.ToUpper (stFormD [i]), 0);
-					if (scode != null) {
-						scanCode = scode.ScanCode;
-					}
-				}
-			}
-		}
-		if (keyChar < 255 && consoleKey == 0 && scanCode == 0) {
-			scode = GetScanCode ("VirtualKey", keyChar, mod);
-			if (scode != null) {
-				consoleKey = (uint)scode.VirtualKey;
-				keyChar = scode.UnicodeChar;
-				scanCode = scode.ScanCode;
-			}
-		}
-
-		return keyChar;
-	}
-
-	/// <summary>
-	/// Maps a unicode character (e.g. (Key)'a') to a uint representing a <see cref="ConsoleKey"/>.
-	/// </summary>
-	/// <param name="keyValue">The key value.</param>
-	/// <param name="isConsoleKey">Indicates if the <paramref name="keyValue"/> is a <see cref="ConsoleKey"/>.
-	/// <see langword="true"/> means the return value is in the ConsoleKey enum.
-	/// <see langword="false"/> means the return value can be mapped to a valid unicode character.
-	/// </param>
-	/// <returns>The <see cref="ConsoleKey"/> or the <paramref name="keyValue"/>.</returns>
-	/// <remarks>This is only used by the <see cref="GetConsoleKeyInfoFromKeyCode"/> and by unit tests.</remarks>
-	internal static uint MapKeyCodeToConsoleKey (KeyCode keyValue, out bool isConsoleKey)
-	{
-		isConsoleKey = true;
-		keyValue = keyValue & ~KeyCode.CtrlMask & ~KeyCode.ShiftMask & ~KeyCode.AltMask;
-
-		switch (keyValue) {
-		case KeyCode.Enter:
-			return (uint)ConsoleKey.Enter;
-		case KeyCode.CursorUp:
-			return (uint)ConsoleKey.UpArrow;
-		case KeyCode.CursorDown:
-			return (uint)ConsoleKey.DownArrow;
-		case KeyCode.CursorLeft:
-			return (uint)ConsoleKey.LeftArrow;
-		case KeyCode.CursorRight:
-			return (uint)ConsoleKey.RightArrow;
-		case KeyCode.PageUp:
-			return (uint)ConsoleKey.PageUp;
-		case KeyCode.PageDown:
-			return (uint)ConsoleKey.PageDown;
-		case KeyCode.Home:
-			return (uint)ConsoleKey.Home;
-		case KeyCode.End:
-			return (uint)ConsoleKey.End;
-		case KeyCode.Insert:
-			return (uint)ConsoleKey.Insert;
-		case KeyCode.Delete:
-			return (uint)ConsoleKey.Delete;
-		case KeyCode.F1:
-			return (uint)ConsoleKey.F1;
-		case KeyCode.F2:
-			return (uint)ConsoleKey.F2;
-		case KeyCode.F3:
-			return (uint)ConsoleKey.F3;
-		case KeyCode.F4:
-			return (uint)ConsoleKey.F4;
-		case KeyCode.F5:
-			return (uint)ConsoleKey.F5;
-		case KeyCode.F6:
-			return (uint)ConsoleKey.F6;
-		case KeyCode.F7:
-			return (uint)ConsoleKey.F7;
-		case KeyCode.F8:
-			return (uint)ConsoleKey.F8;
-		case KeyCode.F9:
-			return (uint)ConsoleKey.F9;
-		case KeyCode.F10:
-			return (uint)ConsoleKey.F10;
-		case KeyCode.F11:
-			return (uint)ConsoleKey.F11;
-		case KeyCode.F12:
-			return (uint)ConsoleKey.F12;
-		case KeyCode.F13:
-			return (uint)ConsoleKey.F13;
-		case KeyCode.F14:
-			return (uint)ConsoleKey.F14;
-		case KeyCode.F15:
-			return (uint)ConsoleKey.F15;
-		case KeyCode.F16:
-			return (uint)ConsoleKey.F16;
-		case KeyCode.F17:
-			return (uint)ConsoleKey.F17;
-		case KeyCode.F18:
-			return (uint)ConsoleKey.F18;
-		case KeyCode.F19:
-			return (uint)ConsoleKey.F19;
-		case KeyCode.F20:
-			return (uint)ConsoleKey.F20;
-		case KeyCode.F21:
-			return (uint)ConsoleKey.F21;
-		case KeyCode.F22:
-			return (uint)ConsoleKey.F22;
-		case KeyCode.F23:
-			return (uint)ConsoleKey.F23;
-		case KeyCode.F24:
-			return (uint)ConsoleKey.F24;
-		case KeyCode.Tab | KeyCode.ShiftMask:
-			return (uint)ConsoleKey.Tab;
-		}
-
-		isConsoleKey = false;
-		return (uint)keyValue;
-	}
-
-	/// <summary>
-	/// Maps a <see cref="ConsoleKeyInfo"/> to a <see cref="KeyCode"/>.
-	/// </summary>
-	/// <param name="consoleKeyInfo">The console key.</param>
-	/// <returns>The <see cref="KeyCode"/> or the <paramref name="consoleKeyInfo"/>.</returns>
-	public static KeyCode MapConsoleKeyInfoToKeyCode (ConsoleKeyInfo consoleKeyInfo)
-	{
-		KeyCode keyCode;
-
-		switch (consoleKeyInfo.Key) {
-		case ConsoleKey.Enter:
-			keyCode = KeyCode.Enter;
-			break;
-		case ConsoleKey.Delete:
-			keyCode = KeyCode.Delete;
-			break;
-		case ConsoleKey.UpArrow:
-			keyCode = KeyCode.CursorUp;
-			break;
-		case ConsoleKey.DownArrow:
-			keyCode = KeyCode.CursorDown;
-			break;
-		case ConsoleKey.LeftArrow:
-			keyCode = KeyCode.CursorLeft;
-			break;
-		case ConsoleKey.RightArrow:
-			keyCode = KeyCode.CursorRight;
-			break;
-		case ConsoleKey.PageUp:
-			keyCode = KeyCode.PageUp;
-			break;
-		case ConsoleKey.PageDown:
-			keyCode = KeyCode.PageDown;
-			break;
-		case ConsoleKey.Home:
-			keyCode = KeyCode.Home;
-			break;
-		case ConsoleKey.End:
-			keyCode = KeyCode.End;
-			break;
-		case ConsoleKey.Insert:
-			keyCode = KeyCode.Insert;
-			break;
-		case ConsoleKey.F1:
-			keyCode = KeyCode.F1;
-			break;
-		case ConsoleKey.F2:
-			keyCode = KeyCode.F2;
-			break;
-		case ConsoleKey.F3:
-			keyCode = KeyCode.F3;
-			break;
-		case ConsoleKey.F4:
-			keyCode = KeyCode.F4;
-			break;
-		case ConsoleKey.F5:
-			keyCode = KeyCode.F5;
-			break;
-		case ConsoleKey.F6:
-			keyCode = KeyCode.F6;
-			break;
-		case ConsoleKey.F7:
-			keyCode = KeyCode.F7;
-			break;
-		case ConsoleKey.F8:
-			keyCode = KeyCode.F8;
-			break;
-		case ConsoleKey.F9:
-			keyCode = KeyCode.F9;
-			break;
-		case ConsoleKey.F10:
-			keyCode = KeyCode.F10;
-			break;
-		case ConsoleKey.F11:
-			keyCode = KeyCode.F11;
-			break;
-		case ConsoleKey.F12:
-			keyCode = KeyCode.F12;
-			break;
-		case ConsoleKey.F13:
-			keyCode = KeyCode.F13;
-			break;
-		case ConsoleKey.F14:
-			keyCode = KeyCode.F14;
-			break;
-		case ConsoleKey.F15:
-			keyCode = KeyCode.F15;
-			break;
-		case ConsoleKey.F16:
-			keyCode = KeyCode.F16;
-			break;
-		case ConsoleKey.F17:
-			keyCode = KeyCode.F17;
-			break;
-		case ConsoleKey.F18:
-			keyCode = KeyCode.F18;
-			break;
-		case ConsoleKey.F19:
-			keyCode = KeyCode.F19;
-			break;
-		case ConsoleKey.F20:
-			keyCode = KeyCode.F20;
-			break;
-		case ConsoleKey.F21:
-			keyCode = KeyCode.F21;
-			break;
-		case ConsoleKey.F22:
-			keyCode = KeyCode.F22;
-			break;
-		case ConsoleKey.F23:
-			keyCode = KeyCode.F23;
-			break;
-		case ConsoleKey.F24:
-			keyCode = KeyCode.F24;
-			break;
-		case ConsoleKey.Tab:
-			keyCode = KeyCode.Tab;
-			break;
-		default:
-			keyCode = (KeyCode)consoleKeyInfo.KeyChar;
-			break;
-		}
-		keyCode |= MapToKeyCodeModifiers (consoleKeyInfo.Modifiers, keyCode);
-
-		return keyCode;
-	}
-
-	/// <summary>
-	/// Maps a <see cref="ConsoleKeyInfo"/> to a <see cref="KeyCode"/>.
-	/// </summary>
-	/// <param name="modifiers">The console modifiers.</param>
-	/// <param name="key">The key code.</param>
-	/// <returns>The <see cref="KeyCode"/> with <see cref="ConsoleModifiers"/> or the <paramref name="key"/></returns>
-	public static KeyCode MapToKeyCodeModifiers (ConsoleModifiers modifiers, KeyCode key)
-	{
-		var keyMod = new KeyCode ();
-		if ((modifiers & ConsoleModifiers.Shift) != 0) {
-			keyMod = KeyCode.ShiftMask;
-		}
-		if ((modifiers & ConsoleModifiers.Control) != 0) {
-			keyMod |= KeyCode.CtrlMask;
-		}
-		if ((modifiers & ConsoleModifiers.Alt) != 0) {
-			keyMod |= KeyCode.AltMask;
-		}
-
-		return keyMod != KeyCode.Null ? keyMod | key : key;
-	}
-
-	/// <summary>
-	/// Generated from winuser.h. See https://learn.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes
-	/// </summary>
-	public enum VK : ushort {
-		/// <summary>
-		/// Left mouse button.
-		/// </summary>
-		LBUTTON = 0x01,
-
-		/// <summary>
-		/// Right mouse button.
-		/// </summary>
-		RBUTTON = 0x02,
-
-		/// <summary>
-		/// Control-break processing.
-		/// </summary>
-		CANCEL = 0x03,
-
-		/// <summary>
-		/// Middle mouse button (three-button mouse).
-		/// </summary>
-		MBUTTON = 0x04,
-
-		/// <summary>
-		/// X1 mouse button.
-		/// </summary>
-		XBUTTON1 = 0x05,
-
-		/// <summary>
-		/// X2 mouse button.
-		/// </summary>
-		XBUTTON2 = 0x06,
-
-		/// <summary>
-		/// BACKSPACE key.
-		/// </summary>
-		BACK = 0x08,
-
-		/// <summary>
-		/// TAB key.
-		/// </summary>
-		TAB = 0x09,
-
-		/// <summary>
-		/// CLEAR key.
-		/// </summary>
-		CLEAR = 0x0C,
-
-		/// <summary>
-		/// ENTER key.
-		/// </summary>
-		RETURN = 0x0D,
-
-		/// <summary>
-		/// SHIFT key.
-		/// </summary>
-		SHIFT = 0x10,
-
-		/// <summary>
-		/// CTRL key.
-		/// </summary>
-		CONTROL = 0x11,
-
-		/// <summary>
-		/// ALT key.
-		/// </summary>
-		MENU = 0x12,
-
-		/// <summary>
-		/// PAUSE key.
-		/// </summary>
-		PAUSE = 0x13,
-
-		/// <summary>
-		/// CAPS LOCK key.
-		/// </summary>
-		CAPITAL = 0x14,
-
-		/// <summary>
-		/// IME Kana mode.
-		/// </summary>
-		KANA = 0x15,
-
-		/// <summary>
-		/// IME Hangul mode.
-		/// </summary>
-		HANGUL = 0x15,
-
-		/// <summary>
-		/// IME Junja mode.
-		/// </summary>
-		JUNJA = 0x17,
-
-		/// <summary>
-		/// IME final mode.
-		/// </summary>
-		FINAL = 0x18,
-
-		/// <summary>
-		/// IME Hanja mode.
-		/// </summary>
-		HANJA = 0x19,
-
-		/// <summary>
-		/// IME Kanji mode.
-		/// </summary>
-		KANJI = 0x19,
-
-		/// <summary>
-		/// ESC key.
-		/// </summary>
-		ESCAPE = 0x1B,
-
-		/// <summary>
-		/// IME convert.
-		/// </summary>
-		CONVERT = 0x1C,
-
-		/// <summary>
-		/// IME nonconvert.
-		/// </summary>
-		NONCONVERT = 0x1D,
-
-		/// <summary>
-		/// IME accept.
-		/// </summary>
-		ACCEPT = 0x1E,
-
-		/// <summary>
-		/// IME mode change request.
-		/// </summary>
-		MODECHANGE = 0x1F,
-
-		/// <summary>
-		/// SPACEBAR.
-		/// </summary>
-		SPACE = 0x20,
-
-		/// <summary>
-		/// PAGE UP key.
-		/// </summary>
-		PRIOR = 0x21,
-
-		/// <summary>
-		/// PAGE DOWN key.
-		/// </summary>
-		NEXT = 0x22,
-
-		/// <summary>
-		/// END key.
-		/// </summary>
-		END = 0x23,
-
-		/// <summary>
-		/// HOME key.
-		/// </summary>
-		HOME = 0x24,
-
-		/// <summary>
-		/// LEFT ARROW key.
-		/// </summary>
-		LEFT = 0x25,
-
-		/// <summary>
-		/// UP ARROW key.
-		/// </summary>
-		UP = 0x26,
-
-		/// <summary>
-		/// RIGHT ARROW key.
-		/// </summary>
-		RIGHT = 0x27,
-
-		/// <summary>
-		/// DOWN ARROW key.
-		/// </summary>
-		DOWN = 0x28,
-
-		/// <summary>
-		/// SELECT key.
-		/// </summary>
-		SELECT = 0x29,
-
-		/// <summary>
-		/// PRINT key.
-		/// </summary>
-		PRINT = 0x2A,
-
-		/// <summary>
-		/// EXECUTE key
-		/// </summary>
-		EXECUTE = 0x2B,
-
-		/// <summary>
-		/// PRINT SCREEN key
-		/// </summary>
-		SNAPSHOT = 0x2C,
-
-		/// <summary>
-		/// INS key
-		/// </summary>
-		INSERT = 0x2D,
-
-		/// <summary>
-		/// DEL key
-		/// </summary>
-		DELETE = 0x2E,
-
-		/// <summary>
-		/// HELP key
-		/// </summary>
-		HELP = 0x2F,
-
-		/// <summary>
-		/// Left Windows key (Natural keyboard)
-		/// </summary>
-		LWIN = 0x5B,
-
-		/// <summary>
-		/// Right Windows key (Natural keyboard)
-		/// </summary>
-		RWIN = 0x5C,
-
-		/// <summary>
-		/// Applications key (Natural keyboard)
-		/// </summary>
-		APPS = 0x5D,
-
-		/// <summary>
-		/// Computer Sleep key
-		/// </summary>
-		SLEEP = 0x5F,
-
-		/// <summary>
-		/// Numeric keypad 0 key
-		/// </summary>
-		NUMPAD0 = 0x60,
-
-		/// <summary>
-		/// Numeric keypad 1 key
-		/// </summary>
-		NUMPAD1 = 0x61,
-
-		/// <summary>
-		/// Numeric keypad 2 key
-		/// </summary>
-		NUMPAD2 = 0x62,
-
-		/// <summary>
-		/// Numeric keypad 3 key
-		/// </summary>
-		NUMPAD3 = 0x63,
-
-		/// <summary>
-		/// Numeric keypad 4 key
-		/// </summary>
-		NUMPAD4 = 0x64,
-
-		/// <summary>
-		/// Numeric keypad 5 key
-		/// </summary>
-		NUMPAD5 = 0x65,
-
-		/// <summary>
-		/// Numeric keypad 6 key
-		/// </summary>
-		NUMPAD6 = 0x66,
-
-		/// <summary>
-		/// Numeric keypad 7 key
-		/// </summary>
-		NUMPAD7 = 0x67,
-
-		/// <summary>
-		/// Numeric keypad 8 key
-		/// </summary>
-		NUMPAD8 = 0x68,
-
-		/// <summary>
-		/// Numeric keypad 9 key
-		/// </summary>
-		NUMPAD9 = 0x69,
-
-		/// <summary>
-		/// Multiply key
-		/// </summary>
-		MULTIPLY = 0x6A,
-
-		/// <summary>
-		/// Add key
-		/// </summary>
-		ADD = 0x6B,
-
-		/// <summary>
-		/// Separator key
-		/// </summary>
-		SEPARATOR = 0x6C,
-
-		/// <summary>
-		/// Subtract key
-		/// </summary>
-		SUBTRACT = 0x6D,
-
-		/// <summary>
-		/// Decimal key
-		/// </summary>
-		DECIMAL = 0x6E,
-
-		/// <summary>
-		/// Divide key
-		/// </summary>
-		DIVIDE = 0x6F,
-
-		/// <summary>
-		/// F1 key
-		/// </summary>
-		F1 = 0x70,
-
-		/// <summary>
-		/// F2 key
-		/// </summary>
-		F2 = 0x71,
-
-		/// <summary>
-		/// F3 key
-		/// </summary>
-		F3 = 0x72,
-
-		/// <summary>
-		/// F4 key
-		/// </summary>
-		F4 = 0x73,
-
-		/// <summary>
-		/// F5 key
-		/// </summary>
-		F5 = 0x74,
-
-		/// <summary>
-		/// F6 key
-		/// </summary>
-		F6 = 0x75,
-
-		/// <summary>
-		/// F7 key
-		/// </summary>
-		F7 = 0x76,
-
-		/// <summary>
-		/// F8 key
-		/// </summary>
-		F8 = 0x77,
-
-		/// <summary>
-		/// F9 key
-		/// </summary>
-		F9 = 0x78,
-
-		/// <summary>
-		/// F10 key
-		/// </summary>
-		F10 = 0x79,
-
-		/// <summary>
-		/// F11 key
-		/// </summary>
-		F11 = 0x7A,
-
-		/// <summary>
-		/// F12 key
-		/// </summary>
-		F12 = 0x7B,
-
-		/// <summary>
-		/// F13 key
-		/// </summary>
-		F13 = 0x7C,
-
-		/// <summary>
-		/// F14 key
-		/// </summary>
-		F14 = 0x7D,
-
-		/// <summary>
-		/// F15 key
-		/// </summary>
-		F15 = 0x7E,
-
-		/// <summary>
-		/// F16 key
-		/// </summary>
-		F16 = 0x7F,
-
-		/// <summary>
-		/// F17 key
-		/// </summary>
-		F17 = 0x80,
-
-		/// <summary>
-		/// F18 key
-		/// </summary>
-		F18 = 0x81,
-
-		/// <summary>
-		/// F19 key
-		/// </summary>
-		F19 = 0x82,
-
-		/// <summary>
-		/// F20 key
-		/// </summary>
-		F20 = 0x83,
-
-		/// <summary>
-		/// F21 key
-		/// </summary>
-		F21 = 0x84,
-
-		/// <summary>
-		/// F22 key
-		/// </summary>
-		F22 = 0x85,
-
-		/// <summary>
-		/// F23 key
-		/// </summary>
-		F23 = 0x86,
-
-		/// <summary>
-		/// F24 key
-		/// </summary>
-		F24 = 0x87,
-
-		/// <summary>
-		/// NUM LOCK key
-		/// </summary>
-		NUMLOCK = 0x90,
-
-		/// <summary>
-		/// SCROLL LOCK key
-		/// </summary>
-		SCROLL = 0x91,
-
-		/// <summary>
-		/// NEC PC-9800 kbd definition: '=' key on numpad
-		/// </summary>
-		OEM_NEC_EQUAL = 0x92,
-
-		/// <summary>
-		/// Fujitsu/OASYS kbd definition: 'Dictionary' key
-		/// </summary>
-		OEM_FJ_JISHO = 0x92,
-
-		/// <summary>
-		/// Fujitsu/OASYS kbd definition: 'Unregister word' key
-		/// </summary>
-		OEM_FJ_MASSHOU = 0x93,
-
-		/// <summary>
-		/// Fujitsu/OASYS kbd definition: 'Register word' key
-		/// </summary>
-		OEM_FJ_TOUROKU = 0x94,
-
-		/// <summary>
-		/// Fujitsu/OASYS kbd definition: 'Left OYAYUBI' key
-		/// </summary>
-		OEM_FJ_LOYA = 0x95,
-
-		/// <summary>
-		/// Fujitsu/OASYS kbd definition: 'Right OYAYUBI' key
-		/// </summary>
-		OEM_FJ_ROYA = 0x96,
-
-		/// <summary>
-		/// Left SHIFT key
-		/// </summary>
-		LSHIFT = 0xA0,
-
-		/// <summary>
-		/// Right SHIFT key
-		/// </summary>
-		RSHIFT = 0xA1,
-
-		/// <summary>
-		/// Left CONTROL key
-		/// </summary>
-		LCONTROL = 0xA2,
-
-		/// <summary>
-		/// Right CONTROL key
-		/// </summary>
-		RCONTROL = 0xA3,
-
-		/// <summary>
-		/// Left MENU key (Left Alt key)
-		/// </summary>
-		LMENU = 0xA4,
-
-		/// <summary>
-		/// Right MENU key (Right Alt key)
-		/// </summary>
-		RMENU = 0xA5,
-
-		/// <summary>
-		/// Browser Back key
-		/// </summary>
-		BROWSER_BACK = 0xA6,
-
-		/// <summary>
-		/// Browser Forward key
-		/// </summary>
-		BROWSER_FORWARD = 0xA7,
-
-		/// <summary>
-		/// Browser Refresh key
-		/// </summary>
-		BROWSER_REFRESH = 0xA8,
-
-		/// <summary>
-		/// Browser Stop key
-		/// </summary>
-		BROWSER_STOP = 0xA9,
-
-		/// <summary>
-		/// Browser Search key
-		/// </summary>
-		BROWSER_SEARCH = 0xAA,
-
-		/// <summary>
-		/// Browser Favorites key
-		/// </summary>
-		BROWSER_FAVORITES = 0xAB,
-
-		/// <summary>
-		/// Browser Home key
-		/// </summary>
-		BROWSER_HOME = 0xAC,
-
-		/// <summary>
-		/// Volume Mute key
-		/// </summary>
-		VOLUME_MUTE = 0xAD,
-
-		/// <summary>
-		/// Volume Down key
-		/// </summary>
-		VOLUME_DOWN = 0xAE,
-
-		/// <summary>
-		/// Volume Up key
-		/// </summary>
-		VOLUME_UP = 0xAF,
-
-		/// <summary>
-		/// Next Track key
-		/// </summary>
-		MEDIA_NEXT_TRACK = 0xB0,
-
-		/// <summary>
-		/// Previous Track key
-		/// </summary>
-		MEDIA_PREV_TRACK = 0xB1,
-
-		/// <summary>
-		/// Stop Media key
-		/// </summary>
-		MEDIA_STOP = 0xB2,
-
-		/// <summary>
-		/// Play/Pause Media key
-		/// </summary>
-		MEDIA_PLAY_PAUSE = 0xB3,
-
-		/// <summary>
-		/// Start Mail key
-		/// </summary>
-		LAUNCH_MAIL = 0xB4,
-
-		/// <summary>
-		/// Select Media key
-		/// </summary>
-		LAUNCH_MEDIA_SELECT = 0xB5,
-
-		/// <summary>
-		/// Start Application 1 key
-		/// </summary>
-		LAUNCH_APP1 = 0xB6,
-
-		/// <summary>
-		/// Start Application 2 key
-		/// </summary>
-		LAUNCH_APP2 = 0xB7,
-
-		/// <summary>
-		/// Used for miscellaneous characters; it can vary by keyboard. For the US standard keyboard, the ';:' key
-		/// </summary>
-		OEM_1 = 0xBA,
-
-		/// <summary>
-		/// For any country/region, the '+' key
-		/// </summary>
-		OEM_PLUS = 0xBB,
-
-		/// <summary>
-		/// For any country/region, the ',' key
-		/// </summary>
-		OEM_COMMA = 0xBC,
-
-		/// <summary>
-		/// For any country/region, the '-' key
-		/// </summary>
-		OEM_MINUS = 0xBD,
-
-		/// <summary>
-		/// For any country/region, the '.' key
-		/// </summary>
-		OEM_PERIOD = 0xBE,
-
-		/// <summary>
-		/// Used for miscellaneous characters; it can vary by keyboard. For the US standard keyboard, the '/?' key
-		/// </summary>
-		OEM_2 = 0xBF,
-
-		/// <summary>
-		/// Used for miscellaneous characters; it can vary by keyboard. For the US standard keyboard, the '`~' key
-		/// </summary>
-		OEM_3 = 0xC0,
-
-		/// <summary>
-		/// Used for miscellaneous characters; it can vary by keyboard. For the US standard keyboard, the '[{' key
-		/// </summary>
-		OEM_4 = 0xDB,
-
-		/// <summary>
-		/// Used for miscellaneous characters; it can vary by keyboard. For the US standard keyboard, the '\|' key
-		/// </summary>
-		OEM_5 = 0xDC,
-
-		/// <summary>
-		/// Used for miscellaneous characters; it can vary by keyboard. For the US standard keyboard, the ']}' key
-		/// </summary>
-		OEM_6 = 0xDD,
-
-		/// <summary>
-		/// Used for miscellaneous characters; it can vary by keyboard. For the US standard keyboard, the 'single-quote/double-quote' key
-		/// </summary>
-		OEM_7 = 0xDE,
-
-		/// <summary>
-		/// Used for miscellaneous characters; it can vary by keyboard.
-		/// </summary>
-		OEM_8 = 0xDF,
-
-		/// <summary>
-		/// 'AX' key on Japanese AX kbd
-		/// </summary>
-		OEM_AX = 0xE1,
-
-		/// <summary>
-		/// Either the angle bracket key or the backslash key on the RT 102-key keyboard
-		/// </summary>
-		OEM_102 = 0xE2,
-
-		/// <summary>
-		/// Help key on ICO
-		/// </summary>
-		ICO_HELP = 0xE3,
-
-		/// <summary>
-		/// 00 key on ICO
-		/// </summary>
-		ICO_00 = 0xE4,
-
-		/// <summary>
-		/// Process key
-		/// </summary>
-		PROCESSKEY = 0xE5,
-
-		/// <summary>
-		/// Clear key on ICO
-		/// </summary>
-		ICO_CLEAR = 0xE6,
-
-		/// <summary>
-		/// Packet key to be used to pass Unicode characters as if they were keystrokes
-		/// </summary>
-		PACKET = 0xE7,
-
-		/// <summary>
-		/// Reset key
-		/// </summary>
-		OEM_RESET = 0xE9,
-
-		/// <summary>
-		/// Jump key
-		/// </summary>
-		OEM_JUMP = 0xEA,
-
-		/// <summary>
-		/// PA1 key
-		/// </summary>
-		OEM_PA1 = 0xEB,
-
-		/// <summary>
-		/// PA2 key
-		/// </summary>
-		OEM_PA2 = 0xEC,
-
-		/// <summary>
-		/// PA3 key
-		/// </summary>
-		OEM_PA3 = 0xED,
-
-		/// <summary>
-		/// WsCtrl key
-		/// </summary>
-		OEM_WSCTRL = 0xEE,
-
-		/// <summary>
-		/// CuSel key
-		/// </summary>
-		OEM_CUSEL = 0xEF,
-
-		/// <summary>
-		/// Attn key
-		/// </summary>
-		OEM_ATTN = 0xF0,
-
-		/// <summary>
-		/// Finish key
-		/// </summary>
-		OEM_FINISH = 0xF1,
-
-		/// <summary>
-		/// Copy key
-		/// </summary>
-		OEM_COPY = 0xF2,
-
-		/// <summary>
-		/// Auto key
-		/// </summary>
-		OEM_AUTO = 0xF3,
-
-		/// <summary>
-		/// Enlw key
-		/// </summary>
-		OEM_ENLW = 0xF4,
-
-		/// <summary>
-		/// BackTab key
-		/// </summary>
-		OEM_BACKTAB = 0xF5,
-
-		/// <summary>
-		/// Attn key
-		/// </summary>
-		ATTN = 0xF6,
-
-		/// <summary>
-		/// CrSel key
-		/// </summary>
-		CRSEL = 0xF7,
-
-		/// <summary>
-		/// ExSel key
-		/// </summary>
-		EXSEL = 0xF8,
-
-		/// <summary>
-		/// Erase EOF key
-		/// </summary>
-		EREOF = 0xF9,
-
-		/// <summary>
-		/// Play key
-		/// </summary>
-		PLAY = 0xFA,
-
-		/// <summary>
-		/// Zoom key
-		/// </summary>
-		ZOOM = 0xFB,
-
-		/// <summary>
-		/// Reserved
-		/// </summary>
-		NONAME = 0xFC,
-
-		/// <summary>
-		/// PA1 key
-		/// </summary>
-		PA1 = 0xFD,
-
-		/// <summary>
-		/// Clear key
-		/// </summary>
-		OEM_CLEAR = 0xFE
-	}
-
-	// BUGBUG: This database makes no sense. It is not possible to map a VK code to a character without knowing the keyboard layout
-	//         It should be deleted.
-	static HashSet<ScanCodeMapping> _scanCodes = new HashSet<ScanCodeMapping> {
-		new (1, VK.ESCAPE, 0, '\u001B'), // Escape
-		new (1, VK.ESCAPE, ConsoleModifiers.Shift, '\u001B'),
-		new (2, (VK)'1', 0, '1'), // D1
-		new (2, (VK)'1', ConsoleModifiers.Shift, '!'),
-		new (3, (VK)'2', 0, '2'), // D2
-		new (3, (VK)'2', ConsoleModifiers.Shift, '\"'), // BUGBUG: This is true for Portugese keyboard, but not ENG (@) or DEU (")
-		new (3, (VK)'2', ConsoleModifiers.Alt | ConsoleModifiers.Control, '@'),
-		new (4, (VK)'3', 0, '3'), // D3
-		new (4, (VK)'3', ConsoleModifiers.Shift, '#'),
-		new (4, (VK)'3', ConsoleModifiers.Alt | ConsoleModifiers.Control, '£'),
-		new (5, (VK)'4', 0, '4'), // D4
-		new (5, (VK)'4', ConsoleModifiers.Shift, '$'),
-		new (5, (VK)'4', ConsoleModifiers.Alt | ConsoleModifiers.Control, '§'),
-		new (6, (VK)'5', 0, '5'), // D5
-		new (6, (VK)'5', ConsoleModifiers.Shift, '%'),
-		new (6, (VK)'5', ConsoleModifiers.Alt | ConsoleModifiers.Control, '€'),
-		new (7, (VK)'6', 0, '6'), // D6
-		new (7, (VK)'6', ConsoleModifiers.Shift, '&'),
-		new (8, (VK)'7', 0, '7'), // D7
-		new (8, (VK)'7', ConsoleModifiers.Shift, '/'),
-		new (8, (VK)'7', ConsoleModifiers.Alt | ConsoleModifiers.Control, '{'),
-		new (9, (VK)'8', 0, '8'), // D8
-		new (9, (VK)'8', ConsoleModifiers.Shift, '('),
-		new (9, (VK)'8', ConsoleModifiers.Alt | ConsoleModifiers.Control, '['),
-		new (10, (VK)'9', 0, '9'), // D9
-		new (10, (VK)'9', ConsoleModifiers.Shift, ')'),
-		new (10, (VK)'9', ConsoleModifiers.Alt | ConsoleModifiers.Control, ']'),
-		new (11, (VK)'0', 0, '0'), // D0
-		new (11, (VK)'0', ConsoleModifiers.Shift, '='),
-		new (11, (VK)'0', ConsoleModifiers.Alt | ConsoleModifiers.Control, '}'),
-		new (12, VK.OEM_4, 0, '\''), // Oem4
-		new (12, VK.OEM_4, ConsoleModifiers.Shift, '?'),
-		new (13, VK.OEM_6, 0, '+'), // Oem6
-		new (13, VK.OEM_6, ConsoleModifiers.Shift, '*'),
-		new (14, VK.BACK, 0, '\u0008'), // Backspace
-		new (14, VK.BACK, ConsoleModifiers.Shift, '\u0008'),
-		new (15, VK.TAB, 0, '\u0009'), // Tab
-		new (15, VK.TAB, ConsoleModifiers.Shift, '\u000F'),
-		new (16, (VK)'Q', 0, 'q'), // Q
-		new (16, (VK)'Q', ConsoleModifiers.Shift, 'Q'),
-		new (17, (VK)'W', 0, 'w'), // W
-		new (17, (VK)'W', ConsoleModifiers.Shift, 'W'),
-		new (18, (VK)'E', 0, 'e'), // E
-		new (18, (VK)'E', ConsoleModifiers.Shift, 'E'),
-		new (19, (VK)'R', 0, 'r'), // R
-		new (19, (VK)'R', ConsoleModifiers.Shift, 'R'),
-		new (20, (VK)'T', 0, 't'), // T
-		new (20, (VK)'T', ConsoleModifiers.Shift, 'T'),
-		new (21, (VK)'Y', 0, 'y'), // Y
-		new (21, (VK)'Y', ConsoleModifiers.Shift, 'Y'),
-		new (22, (VK)'U', 0, 'u'), // U
-		new (22, (VK)'U', ConsoleModifiers.Shift, 'U'),
-		new (23, (VK)'I', 0, 'i'), // I
-		new (23, (VK)'I', ConsoleModifiers.Shift, 'I'),
-		new (24, (VK)'O', 0, 'o'), // O
-		new (24, (VK)'O', ConsoleModifiers.Shift, 'O'),
-		new (25, (VK)'P', 0, 'p'), // P
-		new (25, (VK)'P', ConsoleModifiers.Shift, 'P'),
-		new (26, VK.OEM_PLUS, 0, '+'), // OemPlus
-		new (26, VK.OEM_PLUS, ConsoleModifiers.Shift, '*'),
-		new (26, VK.OEM_PLUS, ConsoleModifiers.Alt | ConsoleModifiers.Control, '¨'),
-		new (27, VK.OEM_1, 0, '´'), // Oem1
-		new (27, VK.OEM_1, ConsoleModifiers.Shift, '`'),
-		new (28, VK.RETURN, 0, '\u000D'), // Enter
-		new (28, VK.RETURN, ConsoleModifiers.Shift, '\u000D'),
-		new (29, VK.CONTROL, 0, '\0'), // Control
-		new (29, VK.CONTROL, ConsoleModifiers.Shift, '\0'),
-		new (30, (VK)'A', 0, 'a'), // A
-		new (30, (VK)'A', ConsoleModifiers.Shift, 'A'),
-		new (31, (VK)'S', 0, 's'), // S
-		new (31, (VK)'S', ConsoleModifiers.Shift, 'S'),
-		new (32, (VK)'D', 0, 'd'), // D
-		new (32, (VK)'D', ConsoleModifiers.Shift, 'D'),
-		new (33, (VK)'F', 0, 'f'), // F
-		new (33, (VK)'F', ConsoleModifiers.Shift, 'F'),
-		new (34, (VK)'G', 0, 'g'), // G
-		new (34, (VK)'G', ConsoleModifiers.Shift, 'G'),
-		new (35, (VK)'H', 0, 'h'), // H
-		new (35, (VK)'H', ConsoleModifiers.Shift, 'H'),
-		new (36, (VK)'J', 0, 'j'), // J
-		new (36, (VK)'J', ConsoleModifiers.Shift, 'J'),
-		new (37, (VK)'K', 0, 'k'), // K
-		new (37, (VK)'K', ConsoleModifiers.Shift, 'K'),
-		new (38, (VK)'L', 0, 'l'), // L
-		new (38, (VK)'L', ConsoleModifiers.Shift, 'L'),
-		new (39, VK.OEM_3, 0, '`'), // Oem3 (Backtick/Grave)
-		new (39, VK.OEM_3, ConsoleModifiers.Shift, '~'),
-		new (40, VK.OEM_7, 0, '\''), // Oem7 (Single Quote)
-		new (40, VK.OEM_7, ConsoleModifiers.Shift, '\"'),
-		new (41, VK.OEM_5, 0, '\\'), // Oem5 (Backslash)
-		new (41, VK.OEM_5, ConsoleModifiers.Shift, '|'),
-		new (42, VK.LSHIFT, 0, '\0'), // Left Shift
-		new (42, VK.LSHIFT, ConsoleModifiers.Shift, '\0'),
-		new (43, VK.OEM_2, 0, '/'), // Oem2 (Forward Slash)
-		new (43, VK.OEM_2, ConsoleModifiers.Shift, '?'),
-		new (44, (VK)'Z', 0, 'z'), // Z
-		new (44, (VK)'Z', ConsoleModifiers.Shift, 'Z'),
-		new (45, (VK)'X', 0, 'x'), // X
-		new (45, (VK)'X', ConsoleModifiers.Shift, 'X'),
-		new (46, (VK)'C', 0, 'c'), // C
-		new (46, (VK)'C', ConsoleModifiers.Shift, 'C'),
-		new (47, (VK)'V', 0, 'v'), // V
-		new (47, (VK)'V', ConsoleModifiers.Shift, 'V'),
-		new (48, (VK)'B', 0, 'b'), // B
-		new (48, (VK)'B', ConsoleModifiers.Shift, 'B'),
-		new (49, (VK)'N', 0, 'n'), // N
-		new (49, (VK)'N', ConsoleModifiers.Shift, 'N'),
-		new (50, (VK)'M', 0, 'm'), // M
-		new (50, (VK)'M', ConsoleModifiers.Shift, 'M'),
-		new (51, VK.OEM_COMMA, 0, ','), // OemComma
-		new (51, VK.OEM_COMMA, ConsoleModifiers.Shift, '<'),
-		new (52, VK.OEM_PERIOD, 0, '.'), // OemPeriod
-		new (52, VK.OEM_PERIOD, ConsoleModifiers.Shift, '>'),
-		new (53, VK.OEM_MINUS, 0, '-'), // OemMinus
-		new (53, VK.OEM_MINUS, ConsoleModifiers.Shift, '_'),
-		new (54, VK.RSHIFT, 0, '\0'), // Right Shift
-		new (54, VK.RSHIFT, ConsoleModifiers.Shift, '\0'),
-		new (55, VK.PRINT, 0, '\0'), // Print Screen
-		new (55, VK.PRINT, ConsoleModifiers.Shift, '\0'),
-		new (56, VK.LMENU, 0, '\0'), // Alt
-		new (56, VK.LMENU, ConsoleModifiers.Shift, '\0'),
-		new (57, VK.SPACE, 0, ' '), // Spacebar
-		new (57, VK.SPACE, ConsoleModifiers.Shift, ' '),
-		new (58, VK.CAPITAL, 0, '\0'), // Caps Lock
-		new (58, VK.CAPITAL, ConsoleModifiers.Shift, '\0'),
-		new (59, VK.F1, 0, '\0'), // F1
-		new (59, VK.F1, ConsoleModifiers.Shift, '\0'),
-		new (60, VK.F2, 0, '\0'), // F2
-		new (60, VK.F2, ConsoleModifiers.Shift, '\0'),
-		new (61, VK.F3, 0, '\0'), // F3
-		new (61, VK.F3, ConsoleModifiers.Shift, '\0'),
-		new (62, VK.F4, 0, '\0'), // F4
-		new (62, VK.F4, ConsoleModifiers.Shift, '\0'),
-		new (63, VK.F5, 0, '\0'), // F5
-		new (63, VK.F5, ConsoleModifiers.Shift, '\0'),
-		new (64, VK.F6, 0, '\0'), // F6
-		new (64, VK.F6, ConsoleModifiers.Shift, '\0'),
-		new (65, VK.F7, 0, '\0'), // F7
-		new (65, VK.F7, ConsoleModifiers.Shift, '\0'),
-		new (66, VK.F8, 0, '\0'), // F8
-		new (66, VK.F8, ConsoleModifiers.Shift, '\0'),
-		new (67, VK.F9, 0, '\0'), // F9
-		new (67, VK.F9, ConsoleModifiers.Shift, '\0'),
-		new (68, VK.F10, 0, '\0'), // F10
-		new (68, VK.F10, ConsoleModifiers.Shift, '\0'),
-		new (69, VK.NUMLOCK, 0, '\0'), // Num Lock
-		new (69, VK.NUMLOCK, ConsoleModifiers.Shift, '\0'),
-		new (70, VK.SCROLL, 0, '\0'), // Scroll Lock
-		new (70, VK.SCROLL, ConsoleModifiers.Shift, '\0'),
-		new (71, VK.HOME, 0, '\0'), // Home
-		new (71, VK.HOME, ConsoleModifiers.Shift, '\0'),
-		new (72, VK.UP, 0, '\0'), // Up Arrow
-		new (72, VK.UP, ConsoleModifiers.Shift, '\0'),
-		new (73, VK.PRIOR, 0, '\0'), // Page Up
-		new (73, VK.PRIOR, ConsoleModifiers.Shift, '\0'),
-		new (74, VK.SUBTRACT, 0, '-'), // Subtract (Num Pad '-')
-		new (74, VK.SUBTRACT, ConsoleModifiers.Shift, '-'),
-		new (75, VK.LEFT, 0, '\0'), // Left Arrow
-		new (75, VK.LEFT, ConsoleModifiers.Shift, '\0'),
-		new (76, VK.CLEAR, 0, '\0'), // Center key (Num Pad 5 with Num Lock off)
-		new (76, VK.CLEAR, ConsoleModifiers.Shift, '\0'),
-		new (77, VK.RIGHT, 0, '\0'), // Right Arrow
-		new (77, VK.RIGHT, ConsoleModifiers.Shift, '\0'),
-		new (78, VK.ADD, 0, '+'), // Add (Num Pad '+')
-		new (78, VK.ADD, ConsoleModifiers.Shift, '+'),
-		new (79, VK.END, 0, '\0'), // End
-		new (79, VK.END, ConsoleModifiers.Shift, '\0'),
-		new (80, VK.DOWN, 0, '\0'), // Down Arrow
-		new (80, VK.DOWN, ConsoleModifiers.Shift, '\0'),
-		new (81, VK.NEXT, 0, '\0'), // Page Down
-		new (81, VK.NEXT, ConsoleModifiers.Shift, '\0'),
-		new (82, VK.INSERT, 0, '\0'), // Insert
-		new (82, VK.INSERT, ConsoleModifiers.Shift, '\0'),
-		new (83, VK.DELETE, 0, '\0'), // Delete
-		new (83, VK.DELETE, ConsoleModifiers.Shift, '\0'),
-		new (86, VK.OEM_102, 0, '<'), // OEM 102 (Typically '<' or '|' key next to Left Shift)
-		new (86, VK.OEM_102, ConsoleModifiers.Shift, '>'),
-		new (87, VK.F11, 0, '\0'), // F11
-		new (87, VK.F11, ConsoleModifiers.Shift, '\0'),
-		new (88, VK.F12, 0, '\0'), // F12
-		new (88, VK.F12, ConsoleModifiers.Shift, '\0')
-	};
-
-	/// <summary>
-	/// Decode a <see cref="ConsoleKeyInfo"/> that is using <see cref="ConsoleKey.Packet"/>.
-	/// </summary>
-	/// <param name="consoleKeyInfo">The console key info.</param>
-	/// <returns>The decoded <see cref="ConsoleKeyInfo"/> or the <paramref name="consoleKeyInfo"/>.</returns>
-	/// <remarks>If it's a <see cref="ConsoleKey.Packet"/> the <see cref="ConsoleKeyInfo.KeyChar"/> may be
-	/// a <see cref="ConsoleKeyInfo.Key"/> or a <see cref="ConsoleKeyInfo.KeyChar"/> value.
-	/// </remarks>
-	public static ConsoleKeyInfo DecodeVKPacketToKConsoleKeyInfo (ConsoleKeyInfo consoleKeyInfo)
-	{
-		if (consoleKeyInfo.Key != ConsoleKey.Packet) {
-			return consoleKeyInfo;
-		}
-
-		return GetConsoleKeyInfoFromKeyChar (consoleKeyInfo.KeyChar, consoleKeyInfo.Modifiers, out _);
-	}
-
-	/// <summary>
-	/// Encode the <see cref="ConsoleKeyInfo.KeyChar"/> with the <see cref="ConsoleKeyInfo.Key"/>
-	/// if the first a byte length, otherwise only the KeyChar is considered and searched on the database.
-	/// </summary>
-	/// <param name="consoleKeyInfo">The console key info.</param>
-	/// <returns>The encoded KeyChar with the Key if both can be shifted, otherwise only the KeyChar.</returns>
-	/// <remarks>This is useful to use with the <see cref="ConsoleKey.Packet"/>.</remarks>
-	public static char EncodeKeyCharForVKPacket (ConsoleKeyInfo consoleKeyInfo)
-	{
-		char keyChar = consoleKeyInfo.KeyChar;
-		ConsoleKey consoleKey = consoleKeyInfo.Key;
-		if (keyChar != 0 && consoleKeyInfo.KeyChar < byte.MaxValue && consoleKey == ConsoleKey.None) {
-			// try to get the ConsoleKey
-			var scode = _scanCodes.FirstOrDefault ((e) => e.UnicodeChar == keyChar);
-			if (scode != null) {
-				consoleKey = (ConsoleKey)scode.VirtualKey;
-			}
-		}
-		if (keyChar < byte.MaxValue && consoleKey != ConsoleKey.None) {
-			keyChar = (char)(consoleKeyInfo.KeyChar << 8 | (byte)consoleKey);
-		}
-
-		return keyChar;
-	}
+    /// <summary>
+    ///     Retrieves the name of the active input locale identifier (formerly called the keyboard layout) for the calling
+    ///     thread.
+    /// </summary>
+    /// <param name="pwszKLID"></param>
+    /// <returns></returns>
+    [DllImport ("user32.dll")]
+    private static extern bool GetKeyboardLayoutName ([Out] StringBuilder pwszKLID);
+
+    /// <summary>
+    ///     Retrieves the name of the active input locale identifier (formerly called the keyboard layout) for the calling
+    ///     thread.
+    /// </summary>
+    /// <returns></returns>
+    public static string GetKeyboardLayoutName ()
+    {
+        if (Environment.OSVersion.Platform != PlatformID.Win32NT)
+        {
+            return "none";
+        }
+
+        var klidSB = new StringBuilder ();
+        GetKeyboardLayoutName (klidSB);
+
+        return klidSB.ToString ();
+    }
+
+    private class ScanCodeMapping : IEquatable<ScanCodeMapping>
+    {
+        public readonly ConsoleModifiers Modifiers;
+        public readonly uint ScanCode;
+        public readonly uint UnicodeChar;
+        public readonly VK VirtualKey;
+
+        public ScanCodeMapping (uint scanCode, VK virtualKey, ConsoleModifiers modifiers, uint unicodeChar)
+        {
+            ScanCode = scanCode;
+            VirtualKey = virtualKey;
+            Modifiers = modifiers;
+            UnicodeChar = unicodeChar;
+        }
+
+        public bool Equals (ScanCodeMapping other)
+        {
+            return ScanCode.Equals (other.ScanCode)
+                   && VirtualKey.Equals (other.VirtualKey)
+                   && Modifiers.Equals (other.Modifiers)
+                   && UnicodeChar.Equals (other.UnicodeChar);
+        }
+    }
+
+    private static ConsoleModifiers GetModifiers (ConsoleModifiers modifiers)
+    {
+        if (modifiers.HasFlag (ConsoleModifiers.Shift)
+            && !modifiers.HasFlag (ConsoleModifiers.Alt)
+            && !modifiers.HasFlag (ConsoleModifiers.Control))
+        {
+            return ConsoleModifiers.Shift;
+        }
+
+        if (modifiers == (ConsoleModifiers.Alt | ConsoleModifiers.Control))
+        {
+            return modifiers;
+        }
+
+        return 0;
+    }
+
+    private static ScanCodeMapping GetScanCode (string propName, uint keyValue, ConsoleModifiers modifiers)
+    {
+        switch (propName)
+        {
+            case "UnicodeChar":
+                ScanCodeMapping sCode =
+                    _scanCodes.FirstOrDefault (e => e.UnicodeChar == keyValue && e.Modifiers == modifiers);
+
+                if (sCode is null && modifiers == (ConsoleModifiers.Alt | ConsoleModifiers.Control))
+                {
+                    return _scanCodes.FirstOrDefault (e => e.UnicodeChar == keyValue && e.Modifiers == 0);
+                }
+
+                return sCode;
+            case "VirtualKey":
+                sCode = _scanCodes.FirstOrDefault (e => e.VirtualKey == (VK)keyValue && e.Modifiers == modifiers);
+
+                if (sCode is null && modifiers == (ConsoleModifiers.Alt | ConsoleModifiers.Control))
+                {
+                    return _scanCodes.FirstOrDefault (e => e.VirtualKey == (VK)keyValue && e.Modifiers == 0);
+                }
+
+                return sCode;
+        }
+
+        return null;
+    }
+
+    // BUGBUG: This API is not correct. It is only used by WindowsDriver in VKPacket scenarios
+    /// <summary>Get the scan code from a <see cref="ConsoleKeyInfo"/>.</summary>
+    /// <param name="consoleKeyInfo">The console key info.</param>
+    /// <returns>The value if apply.</returns>
+    public static uint GetScanCodeFromConsoleKeyInfo (ConsoleKeyInfo consoleKeyInfo)
+    {
+        ConsoleModifiers mod = GetModifiers (consoleKeyInfo.Modifiers);
+        ScanCodeMapping scode = GetScanCode ("VirtualKey", (uint)consoleKeyInfo.Key, mod);
+
+        if (scode is { })
+        {
+            return scode.ScanCode;
+        }
+
+        return 0;
+    }
+
+    // BUGBUG: This API is not correct. It is only used by FakeDriver and VkeyPacketSimulator
+    /// <summary>Gets the <see cref="ConsoleKeyInfo"/> from the provided <see cref="KeyCode"/>.</summary>
+    /// <param name="key">The key code.</param>
+    /// <returns>The console key info.</returns>
+    public static ConsoleKeyInfo GetConsoleKeyInfoFromKeyCode (KeyCode key)
+    {
+        ConsoleModifiers modifiers = MapToConsoleModifiers (key);
+        uint keyValue = MapKeyCodeToConsoleKey (key, out bool isConsoleKey);
+
+        if (isConsoleKey)
+        {
+            ConsoleModifiers mod = GetModifiers (modifiers);
+            ScanCodeMapping scode = GetScanCode ("VirtualKey", keyValue, mod);
+
+            if (scode is { })
+            {
+                return new ConsoleKeyInfo (
+                                           (char)scode.UnicodeChar,
+                                           (ConsoleKey)scode.VirtualKey,
+                                           modifiers.HasFlag (ConsoleModifiers.Shift),
+                                           modifiers.HasFlag (ConsoleModifiers.Alt),
+                                           modifiers.HasFlag (ConsoleModifiers.Control)
+                                          );
+            }
+        }
+        else
+        {
+            uint keyChar = GetKeyCharFromUnicodeChar (keyValue, modifiers, out uint consoleKey, out _, isConsoleKey);
+
+            if (consoleKey != 0)
+            {
+                return new ConsoleKeyInfo (
+                                           (char)keyChar,
+                                           (ConsoleKey)consoleKey,
+                                           modifiers.HasFlag (ConsoleModifiers.Shift),
+                                           modifiers.HasFlag (ConsoleModifiers.Alt),
+                                           modifiers.HasFlag (ConsoleModifiers.Control)
+                                          );
+            }
+        }
+
+        return new ConsoleKeyInfo (
+                                   (char)keyValue,
+                                   ConsoleKey.None,
+                                   modifiers.HasFlag (ConsoleModifiers.Shift),
+                                   modifiers.HasFlag (ConsoleModifiers.Alt),
+                                   modifiers.HasFlag (ConsoleModifiers.Control)
+                                  );
+    }
+
+    /// <summary>Map existing <see cref="KeyCode"/> modifiers to <see cref="ConsoleModifiers"/>.</summary>
+    /// <param name="key">The key code.</param>
+    /// <returns>The console modifiers.</returns>
+    public static ConsoleModifiers MapToConsoleModifiers (KeyCode key)
+    {
+        var modifiers = new ConsoleModifiers ();
+
+        if (key.HasFlag (KeyCode.ShiftMask))
+        {
+            modifiers |= ConsoleModifiers.Shift;
+        }
+
+        if (key.HasFlag (KeyCode.AltMask))
+        {
+            modifiers |= ConsoleModifiers.Alt;
+        }
+
+        if (key.HasFlag (KeyCode.CtrlMask))
+        {
+            modifiers |= ConsoleModifiers.Control;
+        }
+
+        return modifiers;
+    }
+
+    /// <summary>Gets <see cref="ConsoleModifiers"/> from <see cref="bool"/> modifiers.</summary>
+    /// <param name="shift">The shift key.</param>
+    /// <param name="alt">The alt key.</param>
+    /// <param name="control">The control key.</param>
+    /// <returns>The console modifiers.</returns>
+    public static ConsoleModifiers GetModifiers (bool shift, bool alt, bool control)
+    {
+        var modifiers = new ConsoleModifiers ();
+
+        if (shift)
+        {
+            modifiers |= ConsoleModifiers.Shift;
+        }
+
+        if (alt)
+        {
+            modifiers |= ConsoleModifiers.Alt;
+        }
+
+        if (control)
+        {
+            modifiers |= ConsoleModifiers.Control;
+        }
+
+        return modifiers;
+    }
+
+    /// <summary>
+    ///     Get the <see cref="ConsoleKeyInfo"/> from a unicode character and modifiers (e.g. (Key)'a' and
+    ///     (Key)Key.CtrlMask).
+    /// </summary>
+    /// <param name="keyValue">The key as a unicode codepoint.</param>
+    /// <param name="modifiers">The modifier keys.</param>
+    /// <param name="scanCode">The resulting scan code.</param>
+    /// <returns>The <see cref="ConsoleKeyInfo"/>.</returns>
+    private static ConsoleKeyInfo GetConsoleKeyInfoFromKeyChar (
+        uint keyValue,
+        ConsoleModifiers modifiers,
+        out uint scanCode
+    )
+    {
+        scanCode = 0;
+
+        if (keyValue == 0)
+        {
+            return new ConsoleKeyInfo (
+                                       (char)keyValue,
+                                       ConsoleKey.None,
+                                       modifiers.HasFlag (ConsoleModifiers.Shift),
+                                       modifiers.HasFlag (ConsoleModifiers.Alt),
+                                       modifiers.HasFlag (ConsoleModifiers.Control)
+                                      );
+        }
+
+        uint outputChar = keyValue;
+        uint consoleKey;
+
+        if (keyValue > byte.MaxValue)
+        {
+            ScanCodeMapping sCode = _scanCodes.FirstOrDefault (e => e.UnicodeChar == keyValue);
+
+            if (sCode is null)
+            {
+                consoleKey = (byte)(keyValue & byte.MaxValue);
+                sCode = _scanCodes.FirstOrDefault (e => e.VirtualKey == (VK)consoleKey);
+
+                if (sCode is null)
+                {
+                    consoleKey = 0;
+                    outputChar = keyValue;
+                }
+                else
+                {
+                    outputChar = (char)(keyValue >> 8);
+                }
+            }
+            else
+            {
+                consoleKey = (byte)sCode.VirtualKey;
+                outputChar = keyValue;
+            }
+        }
+        else
+        {
+            consoleKey = (byte)keyValue;
+            outputChar = '\0';
+        }
+
+        return new ConsoleKeyInfo (
+                                   (char)outputChar,
+                                   (ConsoleKey)consoleKey,
+                                   modifiers.HasFlag (ConsoleModifiers.Shift),
+                                   modifiers.HasFlag (ConsoleModifiers.Alt),
+                                   modifiers.HasFlag (ConsoleModifiers.Control)
+                                  );
+    }
+
+    // Used only by unit tests
+    internal static uint GetKeyChar (uint keyValue, ConsoleModifiers modifiers)
+    {
+        if (modifiers == ConsoleModifiers.Shift && keyValue - 32 is >= 'A' and <= 'Z')
+        {
+            return keyValue - 32;
+        }
+
+        if (modifiers == ConsoleModifiers.None && keyValue is >= 'A' and <= 'Z')
+        {
+            return keyValue + 32;
+        }
+
+        if (modifiers == ConsoleModifiers.Shift && keyValue - 32 is >= 'À' and <= 'Ý')
+        {
+            return keyValue - 32;
+        }
+
+        if (modifiers == ConsoleModifiers.None && keyValue is >= 'À' and <= 'Ý')
+        {
+            return keyValue + 32;
+        }
+
+        if (modifiers.HasFlag (ConsoleModifiers.Shift) && keyValue is '0')
+        {
+            return keyValue + 13;
+        }
+
+        if (modifiers == ConsoleModifiers.None && keyValue - 13 is '0')
+        {
+            return keyValue - 13;
+        }
+
+        if (modifiers.HasFlag (ConsoleModifiers.Shift) && keyValue is >= '1' and <= '9' and not '7')
+        {
+            return keyValue - 16;
+        }
+
+        if (modifiers == ConsoleModifiers.None && keyValue + 16 is >= '1' and <= '9' and not '7')
+        {
+            return keyValue + 16;
+        }
+
+        if (modifiers.HasFlag (ConsoleModifiers.Shift) && keyValue is '7')
+        {
+            return keyValue - 8;
+        }
+
+        if (modifiers == ConsoleModifiers.None && keyValue + 8 is '7')
+        {
+            return keyValue + 8;
+        }
+
+        if (modifiers.HasFlag (ConsoleModifiers.Shift) && keyValue is '\'')
+        {
+            return keyValue + 24;
+        }
+
+        if (modifiers == ConsoleModifiers.None && keyValue - 24 is '\'')
+        {
+            return keyValue - 24;
+        }
+
+        if (modifiers.HasFlag (ConsoleModifiers.Shift) && keyValue is '«')
+        {
+            return keyValue + 16;
+        }
+
+        if (modifiers == ConsoleModifiers.None && keyValue - 16 is '«')
+        {
+            return keyValue - 16;
+        }
+
+        if (modifiers.HasFlag (ConsoleModifiers.Shift) && keyValue is '\\')
+        {
+            return keyValue + 32;
+        }
+
+        if (modifiers == ConsoleModifiers.None && keyValue - 32 is '\\')
+        {
+            return keyValue - 32;
+        }
+
+        if (modifiers.HasFlag (ConsoleModifiers.Shift) && keyValue is '+')
+        {
+            return keyValue - 1;
+        }
+
+        if (modifiers == ConsoleModifiers.None && keyValue + 1 is '+')
+        {
+            return keyValue + 1;
+        }
+
+        if (modifiers.HasFlag (ConsoleModifiers.Shift) && keyValue is '´')
+        {
+            return keyValue - 84;
+        }
+
+        if (modifiers == ConsoleModifiers.None && keyValue + 84 is '´')
+        {
+            return keyValue + 84;
+        }
+
+        if (modifiers.HasFlag (ConsoleModifiers.Shift) && keyValue is 'º')
+        {
+            return keyValue - 16;
+        }
+
+        if (modifiers == ConsoleModifiers.None && keyValue + 16 is 'º')
+        {
+            return keyValue + 16;
+        }
+
+        if (modifiers.HasFlag (ConsoleModifiers.Shift) && keyValue is '~')
+        {
+            return keyValue - 32;
+        }
+
+        if (modifiers == ConsoleModifiers.None && keyValue + 32 is '~')
+        {
+            return keyValue + 32;
+        }
+
+        if (modifiers.HasFlag (ConsoleModifiers.Shift) && keyValue is '<')
+        {
+            return keyValue + 2;
+        }
+
+        if (modifiers == ConsoleModifiers.None && keyValue - 2 is '<')
+        {
+            return keyValue - 2;
+        }
+
+        if (modifiers.HasFlag (ConsoleModifiers.Shift) && keyValue is ',')
+        {
+            return keyValue + 15;
+        }
+
+        if (modifiers == ConsoleModifiers.None && keyValue - 15 is ',')
+        {
+            return keyValue - 15;
+        }
+
+        if (modifiers.HasFlag (ConsoleModifiers.Shift) && keyValue is '.')
+        {
+            return keyValue + 12;
+        }
+
+        if (modifiers == ConsoleModifiers.None && keyValue - 12 is '.')
+        {
+            return keyValue - 12;
+        }
+
+        if (modifiers.HasFlag (ConsoleModifiers.Shift) && keyValue is '-')
+        {
+            return keyValue + 50;
+        }
+
+        if (modifiers == ConsoleModifiers.None && keyValue - 50 is '-')
+        {
+            return keyValue - 50;
+        }
+
+        return keyValue;
+    }
+
+    /// <summary>
+    ///     Get the output character from the <see cref="GetConsoleKeyInfoFromKeyCode"/>, with the correct
+    ///     <see cref="ConsoleKey"/> and the scan code used on <see cref="WindowsDriver"/>.
+    /// </summary>
+    /// <param name="unicodeChar">The unicode character.</param>
+    /// <param name="modifiers">The modifiers keys.</param>
+    /// <param name="consoleKey">The resulting console key.</param>
+    /// <param name="scanCode">The resulting scan code.</param>
+    /// <param name="isConsoleKey">Indicates if the <paramref name="unicodeChar"/> is a <see cref="ConsoleKey"/>.</param>
+    /// <returns>The output character or the <paramref name="consoleKey"/>.</returns>
+    /// <remarks>This is only used by the <see cref="GetConsoleKeyInfoFromKeyCode"/> and by unit tests.</remarks>
+    internal static uint GetKeyCharFromUnicodeChar (
+        uint unicodeChar,
+        ConsoleModifiers modifiers,
+        out uint consoleKey,
+        out uint scanCode,
+        bool isConsoleKey = false
+    )
+    {
+        uint decodedChar = unicodeChar >> 8 == 0xff ? unicodeChar & 0xff : unicodeChar;
+        uint keyChar = decodedChar;
+        consoleKey = 0;
+        ConsoleModifiers mod = GetModifiers (modifiers);
+        scanCode = 0;
+        ScanCodeMapping scode = null;
+
+        if (unicodeChar != 0 && unicodeChar >> 8 != 0xff && isConsoleKey)
+        {
+            scode = GetScanCode ("VirtualKey", decodedChar, mod);
+        }
+
+        if (isConsoleKey && scode is { })
+        {
+            consoleKey = (uint)scode.VirtualKey;
+            keyChar = scode.UnicodeChar;
+            scanCode = scode.ScanCode;
+        }
+
+        if (scode is null)
+        {
+            scode = unicodeChar != 0 ? GetScanCode ("UnicodeChar", decodedChar, mod) : null;
+
+            if (scode is { })
+            {
+                consoleKey = (uint)scode.VirtualKey;
+                keyChar = scode.UnicodeChar;
+                scanCode = scode.ScanCode;
+            }
+        }
+
+        if (decodedChar != 0 && scanCode == 0 && char.IsLetter ((char)decodedChar))
+        {
+            string stFormD = ((char)decodedChar).ToString ().Normalize (NormalizationForm.FormD);
+
+            for (var i = 0; i < stFormD.Length; i++)
+            {
+                UnicodeCategory uc = CharUnicodeInfo.GetUnicodeCategory (stFormD [i]);
+
+                if (uc != UnicodeCategory.NonSpacingMark && uc != UnicodeCategory.OtherLetter)
+                {
+                    consoleKey = char.ToUpper (stFormD [i]);
+                    scode = GetScanCode ("VirtualKey", char.ToUpper (stFormD [i]), 0);
+
+                    if (scode is { })
+                    {
+                        scanCode = scode.ScanCode;
+                    }
+                }
+            }
+        }
+
+        if (keyChar < 255 && consoleKey == 0 && scanCode == 0)
+        {
+            scode = GetScanCode ("VirtualKey", keyChar, mod);
+
+            if (scode is { })
+            {
+                consoleKey = (uint)scode.VirtualKey;
+                keyChar = scode.UnicodeChar;
+                scanCode = scode.ScanCode;
+            }
+        }
+
+        return keyChar;
+    }
+
+    /// <summary>Maps a unicode character (e.g. (Key)'a') to a uint representing a <see cref="ConsoleKey"/>.</summary>
+    /// <param name="keyValue">The key value.</param>
+    /// <param name="isConsoleKey">
+    ///     Indicates if the <paramref name="keyValue"/> is a <see cref="ConsoleKey"/>.
+    ///     <see langword="true"/> means the return value is in the ConsoleKey enum. <see langword="false"/> means the return
+    ///     value can be mapped to a valid unicode character.
+    /// </param>
+    /// <returns>The <see cref="ConsoleKey"/> or the <paramref name="keyValue"/>.</returns>
+    /// <remarks>This is only used by the <see cref="GetConsoleKeyInfoFromKeyCode"/> and by unit tests.</remarks>
+    internal static uint MapKeyCodeToConsoleKey (KeyCode keyValue, out bool isConsoleKey)
+    {
+        isConsoleKey = true;
+        keyValue = keyValue & ~KeyCode.CtrlMask & ~KeyCode.ShiftMask & ~KeyCode.AltMask;
+
+        switch (keyValue)
+        {
+            case KeyCode.Enter:
+                return (uint)ConsoleKey.Enter;
+            case KeyCode.CursorUp:
+                return (uint)ConsoleKey.UpArrow;
+            case KeyCode.CursorDown:
+                return (uint)ConsoleKey.DownArrow;
+            case KeyCode.CursorLeft:
+                return (uint)ConsoleKey.LeftArrow;
+            case KeyCode.CursorRight:
+                return (uint)ConsoleKey.RightArrow;
+            case KeyCode.PageUp:
+                return (uint)ConsoleKey.PageUp;
+            case KeyCode.PageDown:
+                return (uint)ConsoleKey.PageDown;
+            case KeyCode.Home:
+                return (uint)ConsoleKey.Home;
+            case KeyCode.End:
+                return (uint)ConsoleKey.End;
+            case KeyCode.Insert:
+                return (uint)ConsoleKey.Insert;
+            case KeyCode.Delete:
+                return (uint)ConsoleKey.Delete;
+            case KeyCode.F1:
+                return (uint)ConsoleKey.F1;
+            case KeyCode.F2:
+                return (uint)ConsoleKey.F2;
+            case KeyCode.F3:
+                return (uint)ConsoleKey.F3;
+            case KeyCode.F4:
+                return (uint)ConsoleKey.F4;
+            case KeyCode.F5:
+                return (uint)ConsoleKey.F5;
+            case KeyCode.F6:
+                return (uint)ConsoleKey.F6;
+            case KeyCode.F7:
+                return (uint)ConsoleKey.F7;
+            case KeyCode.F8:
+                return (uint)ConsoleKey.F8;
+            case KeyCode.F9:
+                return (uint)ConsoleKey.F9;
+            case KeyCode.F10:
+                return (uint)ConsoleKey.F10;
+            case KeyCode.F11:
+                return (uint)ConsoleKey.F11;
+            case KeyCode.F12:
+                return (uint)ConsoleKey.F12;
+            case KeyCode.F13:
+                return (uint)ConsoleKey.F13;
+            case KeyCode.F14:
+                return (uint)ConsoleKey.F14;
+            case KeyCode.F15:
+                return (uint)ConsoleKey.F15;
+            case KeyCode.F16:
+                return (uint)ConsoleKey.F16;
+            case KeyCode.F17:
+                return (uint)ConsoleKey.F17;
+            case KeyCode.F18:
+                return (uint)ConsoleKey.F18;
+            case KeyCode.F19:
+                return (uint)ConsoleKey.F19;
+            case KeyCode.F20:
+                return (uint)ConsoleKey.F20;
+            case KeyCode.F21:
+                return (uint)ConsoleKey.F21;
+            case KeyCode.F22:
+                return (uint)ConsoleKey.F22;
+            case KeyCode.F23:
+                return (uint)ConsoleKey.F23;
+            case KeyCode.F24:
+                return (uint)ConsoleKey.F24;
+            case KeyCode.Tab | KeyCode.ShiftMask:
+                return (uint)ConsoleKey.Tab;
+        }
+
+        isConsoleKey = false;
+
+        return (uint)keyValue;
+    }
+
+    /// <summary>Maps a <see cref="ConsoleKeyInfo"/> to a <see cref="KeyCode"/>.</summary>
+    /// <param name="consoleKeyInfo">The console key.</param>
+    /// <returns>The <see cref="KeyCode"/> or the <paramref name="consoleKeyInfo"/>.</returns>
+    public static KeyCode MapConsoleKeyInfoToKeyCode (ConsoleKeyInfo consoleKeyInfo)
+    {
+        KeyCode keyCode;
+
+        switch (consoleKeyInfo.Key)
+        {
+            case ConsoleKey.Enter:
+                keyCode = KeyCode.Enter;
+
+                break;
+            case ConsoleKey.Delete:
+                keyCode = KeyCode.Delete;
+
+                break;
+            case ConsoleKey.UpArrow:
+                keyCode = KeyCode.CursorUp;
+
+                break;
+            case ConsoleKey.DownArrow:
+                keyCode = KeyCode.CursorDown;
+
+                break;
+            case ConsoleKey.LeftArrow:
+                keyCode = KeyCode.CursorLeft;
+
+                break;
+            case ConsoleKey.RightArrow:
+                keyCode = KeyCode.CursorRight;
+
+                break;
+            case ConsoleKey.PageUp:
+                keyCode = KeyCode.PageUp;
+
+                break;
+            case ConsoleKey.PageDown:
+                keyCode = KeyCode.PageDown;
+
+                break;
+            case ConsoleKey.Home:
+                keyCode = KeyCode.Home;
+
+                break;
+            case ConsoleKey.End:
+                keyCode = KeyCode.End;
+
+                break;
+            case ConsoleKey.Insert:
+                keyCode = KeyCode.Insert;
+
+                break;
+            case ConsoleKey.F1:
+                keyCode = KeyCode.F1;
+
+                break;
+            case ConsoleKey.F2:
+                keyCode = KeyCode.F2;
+
+                break;
+            case ConsoleKey.F3:
+                keyCode = KeyCode.F3;
+
+                break;
+            case ConsoleKey.F4:
+                keyCode = KeyCode.F4;
+
+                break;
+            case ConsoleKey.F5:
+                keyCode = KeyCode.F5;
+
+                break;
+            case ConsoleKey.F6:
+                keyCode = KeyCode.F6;
+
+                break;
+            case ConsoleKey.F7:
+                keyCode = KeyCode.F7;
+
+                break;
+            case ConsoleKey.F8:
+                keyCode = KeyCode.F8;
+
+                break;
+            case ConsoleKey.F9:
+                keyCode = KeyCode.F9;
+
+                break;
+            case ConsoleKey.F10:
+                keyCode = KeyCode.F10;
+
+                break;
+            case ConsoleKey.F11:
+                keyCode = KeyCode.F11;
+
+                break;
+            case ConsoleKey.F12:
+                keyCode = KeyCode.F12;
+
+                break;
+            case ConsoleKey.F13:
+                keyCode = KeyCode.F13;
+
+                break;
+            case ConsoleKey.F14:
+                keyCode = KeyCode.F14;
+
+                break;
+            case ConsoleKey.F15:
+                keyCode = KeyCode.F15;
+
+                break;
+            case ConsoleKey.F16:
+                keyCode = KeyCode.F16;
+
+                break;
+            case ConsoleKey.F17:
+                keyCode = KeyCode.F17;
+
+                break;
+            case ConsoleKey.F18:
+                keyCode = KeyCode.F18;
+
+                break;
+            case ConsoleKey.F19:
+                keyCode = KeyCode.F19;
+
+                break;
+            case ConsoleKey.F20:
+                keyCode = KeyCode.F20;
+
+                break;
+            case ConsoleKey.F21:
+                keyCode = KeyCode.F21;
+
+                break;
+            case ConsoleKey.F22:
+                keyCode = KeyCode.F22;
+
+                break;
+            case ConsoleKey.F23:
+                keyCode = KeyCode.F23;
+
+                break;
+            case ConsoleKey.F24:
+                keyCode = KeyCode.F24;
+
+                break;
+            case ConsoleKey.Tab:
+                keyCode = KeyCode.Tab;
+
+                break;
+            default:
+                keyCode = (KeyCode)consoleKeyInfo.KeyChar;
+
+                break;
+        }
+
+        keyCode |= MapToKeyCodeModifiers (consoleKeyInfo.Modifiers, keyCode);
+
+        return keyCode;
+    }
+
+    /// <summary>Maps a <see cref="ConsoleKeyInfo"/> to a <see cref="KeyCode"/>.</summary>
+    /// <param name="modifiers">The console modifiers.</param>
+    /// <param name="key">The key code.</param>
+    /// <returns>The <see cref="KeyCode"/> with <see cref="ConsoleModifiers"/> or the <paramref name="key"/></returns>
+    public static KeyCode MapToKeyCodeModifiers (ConsoleModifiers modifiers, KeyCode key)
+    {
+        var keyMod = new KeyCode ();
+
+        if ((modifiers & ConsoleModifiers.Shift) != 0)
+        {
+            keyMod = KeyCode.ShiftMask;
+        }
+
+        if ((modifiers & ConsoleModifiers.Control) != 0)
+        {
+            keyMod |= KeyCode.CtrlMask;
+        }
+
+        if ((modifiers & ConsoleModifiers.Alt) != 0)
+        {
+            keyMod |= KeyCode.AltMask;
+        }
+
+        return keyMod != KeyCode.Null ? keyMod | key : key;
+    }
+
+    /// <summary>Generated from winuser.h. See https://learn.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes</summary>
+    public enum VK : ushort
+    {
+        /// <summary>Left mouse button.</summary>
+        LBUTTON = 0x01,
+
+        /// <summary>Right mouse button.</summary>
+        RBUTTON = 0x02,
+
+        /// <summary>Control-break processing.</summary>
+        CANCEL = 0x03,
+
+        /// <summary>Middle mouse button (three-button mouse).</summary>
+        MBUTTON = 0x04,
+
+        /// <summary>X1 mouse button.</summary>
+        XBUTTON1 = 0x05,
+
+        /// <summary>X2 mouse button.</summary>
+        XBUTTON2 = 0x06,
+
+        /// <summary>BACKSPACE key.</summary>
+        BACK = 0x08,
+
+        /// <summary>TAB key.</summary>
+        TAB = 0x09,
+
+        /// <summary>CLEAR key.</summary>
+        CLEAR = 0x0C,
+
+        /// <summary>ENTER key.</summary>
+        RETURN = 0x0D,
+
+        /// <summary>SHIFT key.</summary>
+        SHIFT = 0x10,
+
+        /// <summary>CTRL key.</summary>
+        CONTROL = 0x11,
+
+        /// <summary>ALT key.</summary>
+        MENU = 0x12,
+
+        /// <summary>PAUSE key.</summary>
+        PAUSE = 0x13,
+
+        /// <summary>CAPS LOCK key.</summary>
+        CAPITAL = 0x14,
+
+        /// <summary>IME Kana mode.</summary>
+        KANA = 0x15,
+
+        /// <summary>IME Hangul mode.</summary>
+        HANGUL = 0x15,
+
+        /// <summary>IME Junja mode.</summary>
+        JUNJA = 0x17,
+
+        /// <summary>IME final mode.</summary>
+        FINAL = 0x18,
+
+        /// <summary>IME Hanja mode.</summary>
+        HANJA = 0x19,
+
+        /// <summary>IME Kanji mode.</summary>
+        KANJI = 0x19,
+
+        /// <summary>ESC key.</summary>
+        ESCAPE = 0x1B,
+
+        /// <summary>IME convert.</summary>
+        CONVERT = 0x1C,
+
+        /// <summary>IME nonconvert.</summary>
+        NONCONVERT = 0x1D,
+
+        /// <summary>IME accept.</summary>
+        ACCEPT = 0x1E,
+
+        /// <summary>IME mode change request.</summary>
+        MODECHANGE = 0x1F,
+
+        /// <summary>SPACEBAR.</summary>
+        SPACE = 0x20,
+
+        /// <summary>PAGE UP key.</summary>
+        PRIOR = 0x21,
+
+        /// <summary>PAGE DOWN key.</summary>
+        NEXT = 0x22,
+
+        /// <summary>END key.</summary>
+        END = 0x23,
+
+        /// <summary>HOME key.</summary>
+        HOME = 0x24,
+
+        /// <summary>LEFT ARROW key.</summary>
+        LEFT = 0x25,
+
+        /// <summary>UP ARROW key.</summary>
+        UP = 0x26,
+
+        /// <summary>RIGHT ARROW key.</summary>
+        RIGHT = 0x27,
+
+        /// <summary>DOWN ARROW key.</summary>
+        DOWN = 0x28,
+
+        /// <summary>SELECT key.</summary>
+        SELECT = 0x29,
+
+        /// <summary>PRINT key.</summary>
+        PRINT = 0x2A,
+
+        /// <summary>EXECUTE key</summary>
+        EXECUTE = 0x2B,
+
+        /// <summary>PRINT SCREEN key</summary>
+        SNAPSHOT = 0x2C,
+
+        /// <summary>INS key</summary>
+        INSERT = 0x2D,
+
+        /// <summary>DEL key</summary>
+        DELETE = 0x2E,
+
+        /// <summary>HELP key</summary>
+        HELP = 0x2F,
+
+        /// <summary>Left Windows key (Natural keyboard)</summary>
+        LWIN = 0x5B,
+
+        /// <summary>Right Windows key (Natural keyboard)</summary>
+        RWIN = 0x5C,
+
+        /// <summary>Applications key (Natural keyboard)</summary>
+        APPS = 0x5D,
+
+        /// <summary>Computer Sleep key</summary>
+        SLEEP = 0x5F,
+
+        /// <summary>Numeric keypad 0 key</summary>
+        NUMPAD0 = 0x60,
+
+        /// <summary>Numeric keypad 1 key</summary>
+        NUMPAD1 = 0x61,
+
+        /// <summary>Numeric keypad 2 key</summary>
+        NUMPAD2 = 0x62,
+
+        /// <summary>Numeric keypad 3 key</summary>
+        NUMPAD3 = 0x63,
+
+        /// <summary>Numeric keypad 4 key</summary>
+        NUMPAD4 = 0x64,
+
+        /// <summary>Numeric keypad 5 key</summary>
+        NUMPAD5 = 0x65,
+
+        /// <summary>Numeric keypad 6 key</summary>
+        NUMPAD6 = 0x66,
+
+        /// <summary>Numeric keypad 7 key</summary>
+        NUMPAD7 = 0x67,
+
+        /// <summary>Numeric keypad 8 key</summary>
+        NUMPAD8 = 0x68,
+
+        /// <summary>Numeric keypad 9 key</summary>
+        NUMPAD9 = 0x69,
+
+        /// <summary>Multiply key</summary>
+        MULTIPLY = 0x6A,
+
+        /// <summary>Add key</summary>
+        ADD = 0x6B,
+
+        /// <summary>Separator key</summary>
+        SEPARATOR = 0x6C,
+
+        /// <summary>Subtract key</summary>
+        SUBTRACT = 0x6D,
+
+        /// <summary>Decimal key</summary>
+        DECIMAL = 0x6E,
+
+        /// <summary>Divide key</summary>
+        DIVIDE = 0x6F,
+
+        /// <summary>F1 key</summary>
+        F1 = 0x70,
+
+        /// <summary>F2 key</summary>
+        F2 = 0x71,
+
+        /// <summary>F3 key</summary>
+        F3 = 0x72,
+
+        /// <summary>F4 key</summary>
+        F4 = 0x73,
+
+        /// <summary>F5 key</summary>
+        F5 = 0x74,
+
+        /// <summary>F6 key</summary>
+        F6 = 0x75,
+
+        /// <summary>F7 key</summary>
+        F7 = 0x76,
+
+        /// <summary>F8 key</summary>
+        F8 = 0x77,
+
+        /// <summary>F9 key</summary>
+        F9 = 0x78,
+
+        /// <summary>F10 key</summary>
+        F10 = 0x79,
+
+        /// <summary>F11 key</summary>
+        F11 = 0x7A,
+
+        /// <summary>F12 key</summary>
+        F12 = 0x7B,
+
+        /// <summary>F13 key</summary>
+        F13 = 0x7C,
+
+        /// <summary>F14 key</summary>
+        F14 = 0x7D,
+
+        /// <summary>F15 key</summary>
+        F15 = 0x7E,
+
+        /// <summary>F16 key</summary>
+        F16 = 0x7F,
+
+        /// <summary>F17 key</summary>
+        F17 = 0x80,
+
+        /// <summary>F18 key</summary>
+        F18 = 0x81,
+
+        /// <summary>F19 key</summary>
+        F19 = 0x82,
+
+        /// <summary>F20 key</summary>
+        F20 = 0x83,
+
+        /// <summary>F21 key</summary>
+        F21 = 0x84,
+
+        /// <summary>F22 key</summary>
+        F22 = 0x85,
+
+        /// <summary>F23 key</summary>
+        F23 = 0x86,
+
+        /// <summary>F24 key</summary>
+        F24 = 0x87,
+
+        /// <summary>NUM LOCK key</summary>
+        NUMLOCK = 0x90,
+
+        /// <summary>SCROLL LOCK key</summary>
+        SCROLL = 0x91,
+
+        /// <summary>NEC PC-9800 kbd definition: '=' key on numpad</summary>
+        OEM_NEC_EQUAL = 0x92,
+
+        /// <summary>Fujitsu/OASYS kbd definition: 'Dictionary' key</summary>
+        OEM_FJ_JISHO = 0x92,
+
+        /// <summary>Fujitsu/OASYS kbd definition: 'Unregister word' key</summary>
+        OEM_FJ_MASSHOU = 0x93,
+
+        /// <summary>Fujitsu/OASYS kbd definition: 'Register word' key</summary>
+        OEM_FJ_TOUROKU = 0x94,
+
+        /// <summary>Fujitsu/OASYS kbd definition: 'Left OYAYUBI' key</summary>
+        OEM_FJ_LOYA = 0x95,
+
+        /// <summary>Fujitsu/OASYS kbd definition: 'Right OYAYUBI' key</summary>
+        OEM_FJ_ROYA = 0x96,
+
+        /// <summary>Left SHIFT key</summary>
+        LSHIFT = 0xA0,
+
+        /// <summary>Right SHIFT key</summary>
+        RSHIFT = 0xA1,
+
+        /// <summary>Left CONTROL key</summary>
+        LCONTROL = 0xA2,
+
+        /// <summary>Right CONTROL key</summary>
+        RCONTROL = 0xA3,
+
+        /// <summary>Left MENU key (Left Alt key)</summary>
+        LMENU = 0xA4,
+
+        /// <summary>Right MENU key (Right Alt key)</summary>
+        RMENU = 0xA5,
+
+        /// <summary>Browser Back key</summary>
+        BROWSER_BACK = 0xA6,
+
+        /// <summary>Browser Forward key</summary>
+        BROWSER_FORWARD = 0xA7,
+
+        /// <summary>Browser Refresh key</summary>
+        BROWSER_REFRESH = 0xA8,
+
+        /// <summary>Browser Stop key</summary>
+        BROWSER_STOP = 0xA9,
+
+        /// <summary>Browser Search key</summary>
+        BROWSER_SEARCH = 0xAA,
+
+        /// <summary>Browser Favorites key</summary>
+        BROWSER_FAVORITES = 0xAB,
+
+        /// <summary>Browser Home key</summary>
+        BROWSER_HOME = 0xAC,
+
+        /// <summary>Volume Mute key</summary>
+        VOLUME_MUTE = 0xAD,
+
+        /// <summary>Volume Down key</summary>
+        VOLUME_DOWN = 0xAE,
+
+        /// <summary>Volume Up key</summary>
+        VOLUME_UP = 0xAF,
+
+        /// <summary>Next Track key</summary>
+        MEDIA_NEXT_TRACK = 0xB0,
+
+        /// <summary>Previous Track key</summary>
+        MEDIA_PREV_TRACK = 0xB1,
+
+        /// <summary>Stop Media key</summary>
+        MEDIA_STOP = 0xB2,
+
+        /// <summary>Play/Pause Media key</summary>
+        MEDIA_PLAY_PAUSE = 0xB3,
+
+        /// <summary>Start Mail key</summary>
+        LAUNCH_MAIL = 0xB4,
+
+        /// <summary>Select Media key</summary>
+        LAUNCH_MEDIA_SELECT = 0xB5,
+
+        /// <summary>Start Application 1 key</summary>
+        LAUNCH_APP1 = 0xB6,
+
+        /// <summary>Start Application 2 key</summary>
+        LAUNCH_APP2 = 0xB7,
+
+        /// <summary>Used for miscellaneous characters; it can vary by keyboard. For the US standard keyboard, the ';:' key</summary>
+        OEM_1 = 0xBA,
+
+        /// <summary>For any country/region, the '+' key</summary>
+        OEM_PLUS = 0xBB,
+
+        /// <summary>For any country/region, the ',' key</summary>
+        OEM_COMMA = 0xBC,
+
+        /// <summary>For any country/region, the '-' key</summary>
+        OEM_MINUS = 0xBD,
+
+        /// <summary>For any country/region, the '.' key</summary>
+        OEM_PERIOD = 0xBE,
+
+        /// <summary>Used for miscellaneous characters; it can vary by keyboard. For the US standard keyboard, the '/?' key</summary>
+        OEM_2 = 0xBF,
+
+        /// <summary>Used for miscellaneous characters; it can vary by keyboard. For the US standard keyboard, the '`~' key</summary>
+        OEM_3 = 0xC0,
+
+        /// <summary>Used for miscellaneous characters; it can vary by keyboard. For the US standard keyboard, the '[{' key</summary>
+        OEM_4 = 0xDB,
+
+        /// <summary>Used for miscellaneous characters; it can vary by keyboard. For the US standard keyboard, the '\|' key</summary>
+        OEM_5 = 0xDC,
+
+        /// <summary>Used for miscellaneous characters; it can vary by keyboard. For the US standard keyboard, the ']}' key</summary>
+        OEM_6 = 0xDD,
+
+        /// <summary>
+        ///     Used for miscellaneous characters; it can vary by keyboard. For the US standard keyboard, the
+        ///     'single-quote/double-quote' key
+        /// </summary>
+        OEM_7 = 0xDE,
+
+        /// <summary>Used for miscellaneous characters; it can vary by keyboard.</summary>
+        OEM_8 = 0xDF,
+
+        /// <summary>'AX' key on Japanese AX kbd</summary>
+        OEM_AX = 0xE1,
+
+        /// <summary>Either the angle bracket key or the backslash key on the RT 102-key keyboard</summary>
+        OEM_102 = 0xE2,
+
+        /// <summary>Help key on ICO</summary>
+        ICO_HELP = 0xE3,
+
+        /// <summary>00 key on ICO</summary>
+        ICO_00 = 0xE4,
+
+        /// <summary>Process key</summary>
+        PROCESSKEY = 0xE5,
+
+        /// <summary>Clear key on ICO</summary>
+        ICO_CLEAR = 0xE6,
+
+        /// <summary>Packet key to be used to pass Unicode characters as if they were keystrokes</summary>
+        PACKET = 0xE7,
+
+        /// <summary>Reset key</summary>
+        OEM_RESET = 0xE9,
+
+        /// <summary>Jump key</summary>
+        OEM_JUMP = 0xEA,
+
+        /// <summary>PA1 key</summary>
+        OEM_PA1 = 0xEB,
+
+        /// <summary>PA2 key</summary>
+        OEM_PA2 = 0xEC,
+
+        /// <summary>PA3 key</summary>
+        OEM_PA3 = 0xED,
+
+        /// <summary>WsCtrl key</summary>
+        OEM_WSCTRL = 0xEE,
+
+        /// <summary>CuSel key</summary>
+        OEM_CUSEL = 0xEF,
+
+        /// <summary>Attn key</summary>
+        OEM_ATTN = 0xF0,
+
+        /// <summary>Finish key</summary>
+        OEM_FINISH = 0xF1,
+
+        /// <summary>Copy key</summary>
+        OEM_COPY = 0xF2,
+
+        /// <summary>Auto key</summary>
+        OEM_AUTO = 0xF3,
+
+        /// <summary>Enlw key</summary>
+        OEM_ENLW = 0xF4,
+
+        /// <summary>BackTab key</summary>
+        OEM_BACKTAB = 0xF5,
+
+        /// <summary>Attn key</summary>
+        ATTN = 0xF6,
+
+        /// <summary>CrSel key</summary>
+        CRSEL = 0xF7,
+
+        /// <summary>ExSel key</summary>
+        EXSEL = 0xF8,
+
+        /// <summary>Erase EOF key</summary>
+        EREOF = 0xF9,
+
+        /// <summary>Play key</summary>
+        PLAY = 0xFA,
+
+        /// <summary>Zoom key</summary>
+        ZOOM = 0xFB,
+
+        /// <summary>Reserved</summary>
+        NONAME = 0xFC,
+
+        /// <summary>PA1 key</summary>
+        PA1 = 0xFD,
+
+        /// <summary>Clear key</summary>
+        OEM_CLEAR = 0xFE
+    }
+
+    // BUGBUG: This database makes no sense. It is not possible to map a VK code to a character without knowing the keyboard layout
+    //         It should be deleted.
+    private static readonly HashSet<ScanCodeMapping> _scanCodes = new ()
+    {
+        new ScanCodeMapping (
+                             1,
+                             VK.ESCAPE,
+                             0,
+                             '\u001B'
+                            ), // Escape
+        new ScanCodeMapping (
+                             1,
+                             VK.ESCAPE,
+                             ConsoleModifiers.Shift,
+                             '\u001B'
+                            ),
+        new ScanCodeMapping (
+                             2,
+                             (VK)'1',
+                             0,
+                             '1'
+                            ), // D1
+        new ScanCodeMapping (
+                             2,
+                             (VK)'1',
+                             ConsoleModifiers.Shift,
+                             '!'
+                            ),
+        new ScanCodeMapping (
+                             3,
+                             (VK)'2',
+                             0,
+                             '2'
+                            ), // D2
+        new ScanCodeMapping (
+                             3,
+                             (VK)'2',
+                             ConsoleModifiers.Shift,
+                             '\"'
+                            ), // BUGBUG: This is true for Portugese keyboard, but not ENG (@) or DEU (")
+        new ScanCodeMapping (
+                             3,
+                             (VK)'2',
+                             ConsoleModifiers.Alt
+                             | ConsoleModifiers.Control,
+                             '@'
+                            ),
+        new ScanCodeMapping (
+                             4,
+                             (VK)'3',
+                             0,
+                             '3'
+                            ), // D3
+        new ScanCodeMapping (
+                             4,
+                             (VK)'3',
+                             ConsoleModifiers.Shift,
+                             '#'
+                            ),
+        new ScanCodeMapping (
+                             4,
+                             (VK)'3',
+                             ConsoleModifiers.Alt
+                             | ConsoleModifiers.Control,
+                             '£'
+                            ),
+        new ScanCodeMapping (
+                             5,
+                             (VK)'4',
+                             0,
+                             '4'
+                            ), // D4
+        new ScanCodeMapping (
+                             5,
+                             (VK)'4',
+                             ConsoleModifiers.Shift,
+                             '$'
+                            ),
+        new ScanCodeMapping (
+                             5,
+                             (VK)'4',
+                             ConsoleModifiers.Alt
+                             | ConsoleModifiers.Control,
+                             '§'
+                            ),
+        new ScanCodeMapping (
+                             6,
+                             (VK)'5',
+                             0,
+                             '5'
+                            ), // D5
+        new ScanCodeMapping (
+                             6,
+                             (VK)'5',
+                             ConsoleModifiers.Shift,
+                             '%'
+                            ),
+        new ScanCodeMapping (
+                             6,
+                             (VK)'5',
+                             ConsoleModifiers.Alt
+                             | ConsoleModifiers.Control,
+                             '€'
+                            ),
+        new ScanCodeMapping (
+                             7,
+                             (VK)'6',
+                             0,
+                             '6'
+                            ), // D6
+        new ScanCodeMapping (
+                             7,
+                             (VK)'6',
+                             ConsoleModifiers.Shift,
+                             '&'
+                            ),
+        new ScanCodeMapping (
+                             8,
+                             (VK)'7',
+                             0,
+                             '7'
+                            ), // D7
+        new ScanCodeMapping (
+                             8,
+                             (VK)'7',
+                             ConsoleModifiers.Shift,
+                             '/'
+                            ),
+        new ScanCodeMapping (
+                             8,
+                             (VK)'7',
+                             ConsoleModifiers.Alt
+                             | ConsoleModifiers.Control,
+                             '{'
+                            ),
+        new ScanCodeMapping (
+                             9,
+                             (VK)'8',
+                             0,
+                             '8'
+                            ), // D8
+        new ScanCodeMapping (
+                             9,
+                             (VK)'8',
+                             ConsoleModifiers.Shift,
+                             '('
+                            ),
+        new ScanCodeMapping (
+                             9,
+                             (VK)'8',
+                             ConsoleModifiers.Alt
+                             | ConsoleModifiers.Control,
+                             '['
+                            ),
+        new ScanCodeMapping (
+                             10,
+                             (VK)'9',
+                             0,
+                             '9'
+                            ), // D9
+        new ScanCodeMapping (
+                             10,
+                             (VK)'9',
+                             ConsoleModifiers.Shift,
+                             ')'
+                            ),
+        new ScanCodeMapping (
+                             10,
+                             (VK)'9',
+                             ConsoleModifiers.Alt
+                             | ConsoleModifiers.Control,
+                             ']'
+                            ),
+        new ScanCodeMapping (
+                             11,
+                             (VK)'0',
+                             0,
+                             '0'
+                            ), // D0
+        new ScanCodeMapping (
+                             11,
+                             (VK)'0',
+                             ConsoleModifiers.Shift,
+                             '='
+                            ),
+        new ScanCodeMapping (
+                             11,
+                             (VK)'0',
+                             ConsoleModifiers.Alt
+                             | ConsoleModifiers.Control,
+                             '}'
+                            ),
+        new ScanCodeMapping (
+                             12,
+                             VK.OEM_4,
+                             0,
+                             '\''
+                            ), // Oem4
+        new ScanCodeMapping (
+                             12,
+                             VK.OEM_4,
+                             ConsoleModifiers.Shift,
+                             '?'
+                            ),
+        new ScanCodeMapping (
+                             13,
+                             VK.OEM_6,
+                             0,
+                             '+'
+                            ), // Oem6
+        new ScanCodeMapping (
+                             13,
+                             VK.OEM_6,
+                             ConsoleModifiers.Shift,
+                             '*'
+                            ),
+        new ScanCodeMapping (
+                             14,
+                             VK.BACK,
+                             0,
+                             '\u0008'
+                            ), // Backspace
+        new ScanCodeMapping (
+                             14,
+                             VK.BACK,
+                             ConsoleModifiers.Shift,
+                             '\u0008'
+                            ),
+        new ScanCodeMapping (
+                             15,
+                             VK.TAB,
+                             0,
+                             '\u0009'
+                            ), // Tab
+        new ScanCodeMapping (
+                             15,
+                             VK.TAB,
+                             ConsoleModifiers.Shift,
+                             '\u000F'
+                            ),
+        new ScanCodeMapping (
+                             16,
+                             (VK)'Q',
+                             0,
+                             'q'
+                            ), // Q
+        new ScanCodeMapping (
+                             16,
+                             (VK)'Q',
+                             ConsoleModifiers.Shift,
+                             'Q'
+                            ),
+        new ScanCodeMapping (
+                             17,
+                             (VK)'W',
+                             0,
+                             'w'
+                            ), // W
+        new ScanCodeMapping (
+                             17,
+                             (VK)'W',
+                             ConsoleModifiers.Shift,
+                             'W'
+                            ),
+        new ScanCodeMapping (
+                             18,
+                             (VK)'E',
+                             0,
+                             'e'
+                            ), // E
+        new ScanCodeMapping (
+                             18,
+                             (VK)'E',
+                             ConsoleModifiers.Shift,
+                             'E'
+                            ),
+        new ScanCodeMapping (
+                             19,
+                             (VK)'R',
+                             0,
+                             'r'
+                            ), // R
+        new ScanCodeMapping (
+                             19,
+                             (VK)'R',
+                             ConsoleModifiers.Shift,
+                             'R'
+                            ),
+        new ScanCodeMapping (
+                             20,
+                             (VK)'T',
+                             0,
+                             't'
+                            ), // T
+        new ScanCodeMapping (
+                             20,
+                             (VK)'T',
+                             ConsoleModifiers.Shift,
+                             'T'
+                            ),
+        new ScanCodeMapping (
+                             21,
+                             (VK)'Y',
+                             0,
+                             'y'
+                            ), // Y
+        new ScanCodeMapping (
+                             21,
+                             (VK)'Y',
+                             ConsoleModifiers.Shift,
+                             'Y'
+                            ),
+        new ScanCodeMapping (
+                             22,
+                             (VK)'U',
+                             0,
+                             'u'
+                            ), // U
+        new ScanCodeMapping (
+                             22,
+                             (VK)'U',
+                             ConsoleModifiers.Shift,
+                             'U'
+                            ),
+        new ScanCodeMapping (
+                             23,
+                             (VK)'I',
+                             0,
+                             'i'
+                            ), // I
+        new ScanCodeMapping (
+                             23,
+                             (VK)'I',
+                             ConsoleModifiers.Shift,
+                             'I'
+                            ),
+        new ScanCodeMapping (
+                             24,
+                             (VK)'O',
+                             0,
+                             'o'
+                            ), // O
+        new ScanCodeMapping (
+                             24,
+                             (VK)'O',
+                             ConsoleModifiers.Shift,
+                             'O'
+                            ),
+        new ScanCodeMapping (
+                             25,
+                             (VK)'P',
+                             0,
+                             'p'
+                            ), // P
+        new ScanCodeMapping (
+                             25,
+                             (VK)'P',
+                             ConsoleModifiers.Shift,
+                             'P'
+                            ),
+        new ScanCodeMapping (
+                             26,
+                             VK.OEM_PLUS,
+                             0,
+                             '+'
+                            ), // OemPlus
+        new ScanCodeMapping (
+                             26,
+                             VK.OEM_PLUS,
+                             ConsoleModifiers.Shift,
+                             '*'
+                            ),
+        new ScanCodeMapping (
+                             26,
+                             VK.OEM_PLUS,
+                             ConsoleModifiers.Alt
+                             | ConsoleModifiers.Control,
+                             '¨'
+                            ),
+        new ScanCodeMapping (
+                             27,
+                             VK.OEM_1,
+                             0,
+                             '´'
+                            ), // Oem1
+        new ScanCodeMapping (
+                             27,
+                             VK.OEM_1,
+                             ConsoleModifiers.Shift,
+                             '`'
+                            ),
+        new ScanCodeMapping (
+                             28,
+                             VK.RETURN,
+                             0,
+                             '\u000D'
+                            ), // Enter
+        new ScanCodeMapping (
+                             28,
+                             VK.RETURN,
+                             ConsoleModifiers.Shift,
+                             '\u000D'
+                            ),
+        new ScanCodeMapping (
+                             29,
+                             VK.CONTROL,
+                             0,
+                             '\0'
+                            ), // Control
+        new ScanCodeMapping (
+                             29,
+                             VK.CONTROL,
+                             ConsoleModifiers.Shift,
+                             '\0'
+                            ),
+        new ScanCodeMapping (
+                             30,
+                             (VK)'A',
+                             0,
+                             'a'
+                            ), // A
+        new ScanCodeMapping (
+                             30,
+                             (VK)'A',
+                             ConsoleModifiers.Shift,
+                             'A'
+                            ),
+        new ScanCodeMapping (
+                             31,
+                             (VK)'S',
+                             0,
+                             's'
+                            ), // S
+        new ScanCodeMapping (
+                             31,
+                             (VK)'S',
+                             ConsoleModifiers.Shift,
+                             'S'
+                            ),
+        new ScanCodeMapping (
+                             32,
+                             (VK)'D',
+                             0,
+                             'd'
+                            ), // D
+        new ScanCodeMapping (
+                             32,
+                             (VK)'D',
+                             ConsoleModifiers.Shift,
+                             'D'
+                            ),
+        new ScanCodeMapping (
+                             33,
+                             (VK)'F',
+                             0,
+                             'f'
+                            ), // F
+        new ScanCodeMapping (
+                             33,
+                             (VK)'F',
+                             ConsoleModifiers.Shift,
+                             'F'
+                            ),
+        new ScanCodeMapping (
+                             34,
+                             (VK)'G',
+                             0,
+                             'g'
+                            ), // G
+        new ScanCodeMapping (
+                             34,
+                             (VK)'G',
+                             ConsoleModifiers.Shift,
+                             'G'
+                            ),
+        new ScanCodeMapping (
+                             35,
+                             (VK)'H',
+                             0,
+                             'h'
+                            ), // H
+        new ScanCodeMapping (
+                             35,
+                             (VK)'H',
+                             ConsoleModifiers.Shift,
+                             'H'
+                            ),
+        new ScanCodeMapping (
+                             36,
+                             (VK)'J',
+                             0,
+                             'j'
+                            ), // J
+        new ScanCodeMapping (
+                             36,
+                             (VK)'J',
+                             ConsoleModifiers.Shift,
+                             'J'
+                            ),
+        new ScanCodeMapping (
+                             37,
+                             (VK)'K',
+                             0,
+                             'k'
+                            ), // K
+        new ScanCodeMapping (
+                             37,
+                             (VK)'K',
+                             ConsoleModifiers.Shift,
+                             'K'
+                            ),
+        new ScanCodeMapping (
+                             38,
+                             (VK)'L',
+                             0,
+                             'l'
+                            ), // L
+        new ScanCodeMapping (
+                             38,
+                             (VK)'L',
+                             ConsoleModifiers.Shift,
+                             'L'
+                            ),
+        new ScanCodeMapping (
+                             39,
+                             VK.OEM_3,
+                             0,
+                             '`'
+                            ), // Oem3 (Backtick/Grave)
+        new ScanCodeMapping (
+                             39,
+                             VK.OEM_3,
+                             ConsoleModifiers.Shift,
+                             '~'
+                            ),
+        new ScanCodeMapping (
+                             40,
+                             VK.OEM_7,
+                             0,
+                             '\''
+                            ), // Oem7 (Single Quote)
+        new ScanCodeMapping (
+                             40,
+                             VK.OEM_7,
+                             ConsoleModifiers.Shift,
+                             '\"'
+                            ),
+        new ScanCodeMapping (
+                             41,
+                             VK.OEM_5,
+                             0,
+                             '\\'
+                            ), // Oem5 (Backslash)
+        new ScanCodeMapping (
+                             41,
+                             VK.OEM_5,
+                             ConsoleModifiers.Shift,
+                             '|'
+                            ),
+        new ScanCodeMapping (
+                             42,
+                             VK.LSHIFT,
+                             0,
+                             '\0'
+                            ), // Left Shift
+        new ScanCodeMapping (
+                             42,
+                             VK.LSHIFT,
+                             ConsoleModifiers.Shift,
+                             '\0'
+                            ),
+        new ScanCodeMapping (
+                             43,
+                             VK.OEM_2,
+                             0,
+                             '/'
+                            ), // Oem2 (Forward Slash)
+        new ScanCodeMapping (
+                             43,
+                             VK.OEM_2,
+                             ConsoleModifiers.Shift,
+                             '?'
+                            ),
+        new ScanCodeMapping (
+                             44,
+                             (VK)'Z',
+                             0,
+                             'z'
+                            ), // Z
+        new ScanCodeMapping (
+                             44,
+                             (VK)'Z',
+                             ConsoleModifiers.Shift,
+                             'Z'
+                            ),
+        new ScanCodeMapping (
+                             45,
+                             (VK)'X',
+                             0,
+                             'x'
+                            ), // X
+        new ScanCodeMapping (
+                             45,
+                             (VK)'X',
+                             ConsoleModifiers.Shift,
+                             'X'
+                            ),
+        new ScanCodeMapping (
+                             46,
+                             (VK)'C',
+                             0,
+                             'c'
+                            ), // C
+        new ScanCodeMapping (
+                             46,
+                             (VK)'C',
+                             ConsoleModifiers.Shift,
+                             'C'
+                            ),
+        new ScanCodeMapping (
+                             47,
+                             (VK)'V',
+                             0,
+                             'v'
+                            ), // V
+        new ScanCodeMapping (
+                             47,
+                             (VK)'V',
+                             ConsoleModifiers.Shift,
+                             'V'
+                            ),
+        new ScanCodeMapping (
+                             48,
+                             (VK)'B',
+                             0,
+                             'b'
+                            ), // B
+        new ScanCodeMapping (
+                             48,
+                             (VK)'B',
+                             ConsoleModifiers.Shift,
+                             'B'
+                            ),
+        new ScanCodeMapping (
+                             49,
+                             (VK)'N',
+                             0,
+                             'n'
+                            ), // N
+        new ScanCodeMapping (
+                             49,
+                             (VK)'N',
+                             ConsoleModifiers.Shift,
+                             'N'
+                            ),
+        new ScanCodeMapping (
+                             50,
+                             (VK)'M',
+                             0,
+                             'm'
+                            ), // M
+        new ScanCodeMapping (
+                             50,
+                             (VK)'M',
+                             ConsoleModifiers.Shift,
+                             'M'
+                            ),
+        new ScanCodeMapping (
+                             51,
+                             VK.OEM_COMMA,
+                             0,
+                             ','
+                            ), // OemComma
+        new ScanCodeMapping (
+                             51,
+                             VK.OEM_COMMA,
+                             ConsoleModifiers.Shift,
+                             '<'
+                            ),
+        new ScanCodeMapping (
+                             52,
+                             VK.OEM_PERIOD,
+                             0,
+                             '.'
+                            ), // OemPeriod
+        new ScanCodeMapping (
+                             52,
+                             VK.OEM_PERIOD,
+                             ConsoleModifiers.Shift,
+                             '>'
+                            ),
+        new ScanCodeMapping (
+                             53,
+                             VK.OEM_MINUS,
+                             0,
+                             '-'
+                            ), // OemMinus
+        new ScanCodeMapping (
+                             53,
+                             VK.OEM_MINUS,
+                             ConsoleModifiers.Shift,
+                             '_'
+                            ),
+        new ScanCodeMapping (
+                             54,
+                             VK.RSHIFT,
+                             0,
+                             '\0'
+                            ), // Right Shift
+        new ScanCodeMapping (
+                             54,
+                             VK.RSHIFT,
+                             ConsoleModifiers.Shift,
+                             '\0'
+                            ),
+        new ScanCodeMapping (
+                             55,
+                             VK.PRINT,
+                             0,
+                             '\0'
+                            ), // Print Screen
+        new ScanCodeMapping (
+                             55,
+                             VK.PRINT,
+                             ConsoleModifiers.Shift,
+                             '\0'
+                            ),
+        new ScanCodeMapping (
+                             56,
+                             VK.LMENU,
+                             0,
+                             '\0'
+                            ), // Alt
+        new ScanCodeMapping (
+                             56,
+                             VK.LMENU,
+                             ConsoleModifiers.Shift,
+                             '\0'
+                            ),
+        new ScanCodeMapping (
+                             57,
+                             VK.SPACE,
+                             0,
+                             ' '
+                            ), // Spacebar
+        new ScanCodeMapping (
+                             57,
+                             VK.SPACE,
+                             ConsoleModifiers.Shift,
+                             ' '
+                            ),
+        new ScanCodeMapping (
+                             58,
+                             VK.CAPITAL,
+                             0,
+                             '\0'
+                            ), // Caps Lock
+        new ScanCodeMapping (
+                             58,
+                             VK.CAPITAL,
+                             ConsoleModifiers.Shift,
+                             '\0'
+                            ),
+        new ScanCodeMapping (
+                             59,
+                             VK.F1,
+                             0,
+                             '\0'
+                            ), // F1
+        new ScanCodeMapping (
+                             59,
+                             VK.F1,
+                             ConsoleModifiers.Shift,
+                             '\0'
+                            ),
+        new ScanCodeMapping (
+                             60,
+                             VK.F2,
+                             0,
+                             '\0'
+                            ), // F2
+        new ScanCodeMapping (
+                             60,
+                             VK.F2,
+                             ConsoleModifiers.Shift,
+                             '\0'
+                            ),
+        new ScanCodeMapping (
+                             61,
+                             VK.F3,
+                             0,
+                             '\0'
+                            ), // F3
+        new ScanCodeMapping (
+                             61,
+                             VK.F3,
+                             ConsoleModifiers.Shift,
+                             '\0'
+                            ),
+        new ScanCodeMapping (
+                             62,
+                             VK.F4,
+                             0,
+                             '\0'
+                            ), // F4
+        new ScanCodeMapping (
+                             62,
+                             VK.F4,
+                             ConsoleModifiers.Shift,
+                             '\0'
+                            ),
+        new ScanCodeMapping (
+                             63,
+                             VK.F5,
+                             0,
+                             '\0'
+                            ), // F5
+        new ScanCodeMapping (
+                             63,
+                             VK.F5,
+                             ConsoleModifiers.Shift,
+                             '\0'
+                            ),
+        new ScanCodeMapping (
+                             64,
+                             VK.F6,
+                             0,
+                             '\0'
+                            ), // F6
+        new ScanCodeMapping (
+                             64,
+                             VK.F6,
+                             ConsoleModifiers.Shift,
+                             '\0'
+                            ),
+        new ScanCodeMapping (
+                             65,
+                             VK.F7,
+                             0,
+                             '\0'
+                            ), // F7
+        new ScanCodeMapping (
+                             65,
+                             VK.F7,
+                             ConsoleModifiers.Shift,
+                             '\0'
+                            ),
+        new ScanCodeMapping (
+                             66,
+                             VK.F8,
+                             0,
+                             '\0'
+                            ), // F8
+        new ScanCodeMapping (
+                             66,
+                             VK.F8,
+                             ConsoleModifiers.Shift,
+                             '\0'
+                            ),
+        new ScanCodeMapping (
+                             67,
+                             VK.F9,
+                             0,
+                             '\0'
+                            ), // F9
+        new ScanCodeMapping (
+                             67,
+                             VK.F9,
+                             ConsoleModifiers.Shift,
+                             '\0'
+                            ),
+        new ScanCodeMapping (
+                             68,
+                             VK.F10,
+                             0,
+                             '\0'
+                            ), // F10
+        new ScanCodeMapping (
+                             68,
+                             VK.F10,
+                             ConsoleModifiers.Shift,
+                             '\0'
+                            ),
+        new ScanCodeMapping (
+                             69,
+                             VK.NUMLOCK,
+                             0,
+                             '\0'
+                            ), // Num Lock
+        new ScanCodeMapping (
+                             69,
+                             VK.NUMLOCK,
+                             ConsoleModifiers.Shift,
+                             '\0'
+                            ),
+        new ScanCodeMapping (
+                             70,
+                             VK.SCROLL,
+                             0,
+                             '\0'
+                            ), // Scroll Lock
+        new ScanCodeMapping (
+                             70,
+                             VK.SCROLL,
+                             ConsoleModifiers.Shift,
+                             '\0'
+                            ),
+        new ScanCodeMapping (
+                             71,
+                             VK.HOME,
+                             0,
+                             '\0'
+                            ), // Home
+        new ScanCodeMapping (
+                             71,
+                             VK.HOME,
+                             ConsoleModifiers.Shift,
+                             '\0'
+                            ),
+        new ScanCodeMapping (
+                             72,
+                             VK.UP,
+                             0,
+                             '\0'
+                            ), // Up Arrow
+        new ScanCodeMapping (
+                             72,
+                             VK.UP,
+                             ConsoleModifiers.Shift,
+                             '\0'
+                            ),
+        new ScanCodeMapping (
+                             73,
+                             VK.PRIOR,
+                             0,
+                             '\0'
+                            ), // Page Up
+        new ScanCodeMapping (
+                             73,
+                             VK.PRIOR,
+                             ConsoleModifiers.Shift,
+                             '\0'
+                            ),
+        new ScanCodeMapping (
+                             74,
+                             VK.SUBTRACT,
+                             0,
+                             '-'
+                            ), // Subtract (Num Pad '-')
+        new ScanCodeMapping (
+                             74,
+                             VK.SUBTRACT,
+                             ConsoleModifiers.Shift,
+                             '-'
+                            ),
+        new ScanCodeMapping (
+                             75,
+                             VK.LEFT,
+                             0,
+                             '\0'
+                            ), // Left Arrow
+        new ScanCodeMapping (
+                             75,
+                             VK.LEFT,
+                             ConsoleModifiers.Shift,
+                             '\0'
+                            ),
+        new ScanCodeMapping (
+                             76,
+                             VK.CLEAR,
+                             0,
+                             '\0'
+                            ), // Center key (Num Pad 5 with Num Lock off)
+        new ScanCodeMapping (
+                             76,
+                             VK.CLEAR,
+                             ConsoleModifiers.Shift,
+                             '\0'
+                            ),
+        new ScanCodeMapping (
+                             77,
+                             VK.RIGHT,
+                             0,
+                             '\0'
+                            ), // Right Arrow
+        new ScanCodeMapping (
+                             77,
+                             VK.RIGHT,
+                             ConsoleModifiers.Shift,
+                             '\0'
+                            ),
+        new ScanCodeMapping (
+                             78,
+                             VK.ADD,
+                             0,
+                             '+'
+                            ), // Add (Num Pad '+')
+        new ScanCodeMapping (
+                             78,
+                             VK.ADD,
+                             ConsoleModifiers.Shift,
+                             '+'
+                            ),
+        new ScanCodeMapping (
+                             79,
+                             VK.END,
+                             0,
+                             '\0'
+                            ), // End
+        new ScanCodeMapping (
+                             79,
+                             VK.END,
+                             ConsoleModifiers.Shift,
+                             '\0'
+                            ),
+        new ScanCodeMapping (
+                             80,
+                             VK.DOWN,
+                             0,
+                             '\0'
+                            ), // Down Arrow
+        new ScanCodeMapping (
+                             80,
+                             VK.DOWN,
+                             ConsoleModifiers.Shift,
+                             '\0'
+                            ),
+        new ScanCodeMapping (
+                             81,
+                             VK.NEXT,
+                             0,
+                             '\0'
+                            ), // Page Down
+        new ScanCodeMapping (
+                             81,
+                             VK.NEXT,
+                             ConsoleModifiers.Shift,
+                             '\0'
+                            ),
+        new ScanCodeMapping (
+                             82,
+                             VK.INSERT,
+                             0,
+                             '\0'
+                            ), // Insert
+        new ScanCodeMapping (
+                             82,
+                             VK.INSERT,
+                             ConsoleModifiers.Shift,
+                             '\0'
+                            ),
+        new ScanCodeMapping (
+                             83,
+                             VK.DELETE,
+                             0,
+                             '\0'
+                            ), // Delete
+        new ScanCodeMapping (
+                             83,
+                             VK.DELETE,
+                             ConsoleModifiers.Shift,
+                             '\0'
+                            ),
+        new ScanCodeMapping (
+                             86,
+                             VK.OEM_102,
+                             0,
+                             '<'
+                            ), // OEM 102 (Typically '<' or '|' key next to Left Shift)
+        new ScanCodeMapping (
+                             86,
+                             VK.OEM_102,
+                             ConsoleModifiers.Shift,
+                             '>'
+                            ),
+        new ScanCodeMapping (
+                             87,
+                             VK.F11,
+                             0,
+                             '\0'
+                            ), // F11
+        new ScanCodeMapping (
+                             87,
+                             VK.F11,
+                             ConsoleModifiers.Shift,
+                             '\0'
+                            ),
+        new ScanCodeMapping (
+                             88,
+                             VK.F12,
+                             0,
+                             '\0'
+                            ), // F12
+        new ScanCodeMapping (
+                             88,
+                             VK.F12,
+                             ConsoleModifiers.Shift,
+                             '\0'
+                            )
+    };
+
+    /// <summary>Decode a <see cref="ConsoleKeyInfo"/> that is using <see cref="ConsoleKey.Packet"/>.</summary>
+    /// <param name="consoleKeyInfo">The console key info.</param>
+    /// <returns>The decoded <see cref="ConsoleKeyInfo"/> or the <paramref name="consoleKeyInfo"/>.</returns>
+    /// <remarks>
+    ///     If it's a <see cref="ConsoleKey.Packet"/> the <see cref="ConsoleKeyInfo.KeyChar"/> may be a
+    ///     <see cref="ConsoleKeyInfo.Key"/> or a <see cref="ConsoleKeyInfo.KeyChar"/> value.
+    /// </remarks>
+    public static ConsoleKeyInfo DecodeVKPacketToKConsoleKeyInfo (ConsoleKeyInfo consoleKeyInfo)
+    {
+        if (consoleKeyInfo.Key != ConsoleKey.Packet)
+        {
+            return consoleKeyInfo;
+        }
+
+        return GetConsoleKeyInfoFromKeyChar (consoleKeyInfo.KeyChar, consoleKeyInfo.Modifiers, out _);
+    }
+
+    /// <summary>
+    ///     Encode the <see cref="ConsoleKeyInfo.KeyChar"/> with the <see cref="ConsoleKeyInfo.Key"/> if the first a byte
+    ///     length, otherwise only the KeyChar is considered and searched on the database.
+    /// </summary>
+    /// <param name="consoleKeyInfo">The console key info.</param>
+    /// <returns>The encoded KeyChar with the Key if both can be shifted, otherwise only the KeyChar.</returns>
+    /// <remarks>This is useful to use with the <see cref="ConsoleKey.Packet"/>.</remarks>
+    public static char EncodeKeyCharForVKPacket (ConsoleKeyInfo consoleKeyInfo)
+    {
+        char keyChar = consoleKeyInfo.KeyChar;
+        ConsoleKey consoleKey = consoleKeyInfo.Key;
+
+        if (keyChar != 0 && consoleKeyInfo.KeyChar < byte.MaxValue && consoleKey == ConsoleKey.None)
+        {
+            // try to get the ConsoleKey
+            ScanCodeMapping scode = _scanCodes.FirstOrDefault (e => e.UnicodeChar == keyChar);
+
+            if (scode is { })
+            {
+                consoleKey = (ConsoleKey)scode.VirtualKey;
+            }
+        }
+
+        if (keyChar < byte.MaxValue && consoleKey != ConsoleKey.None)
+        {
+            keyChar = (char)((consoleKeyInfo.KeyChar << 8) | (byte)consoleKey);
+        }
+
+        return keyChar;
+    }
 }

+ 256 - 212
Terminal.Gui/ConsoleDrivers/CursesDriver/ClipboardImpl.cs

@@ -1,225 +1,269 @@
-using System;
-using System.Runtime.InteropServices;
+using System.Runtime.InteropServices;
 using Unix.Terminal;
 
 namespace Terminal.Gui;
 
-/// <summary>
-///  A clipboard implementation for Linux.
-///  This implementation uses the xclip command to access the clipboard.
-/// </summary>	
-/// <remarks>
-/// If xclip is not installed, this implementation will not work.
-/// </remarks>
-class CursesClipboard : ClipboardBase {
-	public CursesClipboard ()
-	{
-		IsSupported = CheckSupport ();
-	}
-
-	string _xclipPath = string.Empty;
-	public override bool IsSupported { get; }
-
-	bool CheckSupport ()
-	{
+/// <summary>A clipboard implementation for Linux. This implementation uses the xclip command to access the clipboard.</summary>
+/// <remarks>If xclip is not installed, this implementation will not work.</remarks>
+internal class CursesClipboard : ClipboardBase
+{
+    private string _xclipPath = string.Empty;
+    public CursesClipboard () { IsSupported = CheckSupport (); }
+    public override bool IsSupported { get; }
+
+    protected override string GetClipboardDataImpl ()
+    {
+        string tempFileName = Path.GetTempFileName ();
+        var xclipargs = "-selection clipboard -o";
+
+        try
+        {
+            (int exitCode, string result) =
+                ClipboardProcessRunner.Bash ($"{_xclipPath} {xclipargs} > {tempFileName}", waitForOutput: false);
+
+            if (exitCode == 0)
+            {
+                if (Application.Driver is CursesDriver)
+                {
+                    Curses.raw ();
+                    Curses.noecho ();
+                }
+
+                return File.ReadAllText (tempFileName);
+            }
+        }
+        catch (Exception e)
+        {
+            throw new NotSupportedException ($"\"{_xclipPath} {xclipargs}\" failed.", e);
+        }
+        finally
+        {
+            File.Delete (tempFileName);
+        }
+
+        return string.Empty;
+    }
+
+    protected override void SetClipboardDataImpl (string text)
+    {
+        var xclipargs = "-selection clipboard -i";
+
+        try
+        {
+            (int exitCode, _) = ClipboardProcessRunner.Bash ($"{_xclipPath} {xclipargs}", text);
+
+            if (exitCode == 0 && Application.Driver is CursesDriver)
+            {
+                Curses.raw ();
+                Curses.noecho ();
+            }
+        }
+        catch (Exception e)
+        {
+            throw new NotSupportedException ($"\"{_xclipPath} {xclipargs} < {text}\" failed", e);
+        }
+    }
+
+    private bool CheckSupport ()
+    {
 #pragma warning disable RCS1075 // Avoid empty catch clause that catches System.Exception.
-		try {
-			var (exitCode, result) = ClipboardProcessRunner.Bash ("which xclip", waitForOutput: true);
-			if (exitCode == 0 && result.FileExists ()) {
-				_xclipPath = result;
-				return true;
-			}
-		} catch (Exception) {
-			// Permissions issue.
-		}
+        try
+        {
+            (int exitCode, string result) = ClipboardProcessRunner.Bash ("which xclip", waitForOutput: true);
+
+            if (exitCode == 0 && result.FileExists ())
+            {
+                _xclipPath = result;
+
+                return true;
+            }
+        }
+        catch (Exception)
+        {
+            // Permissions issue.
+        }
 #pragma warning restore RCS1075 // Avoid empty catch clause that catches System.Exception.
-		return false;
-	}
-
-	protected override string GetClipboardDataImpl ()
-	{
-		var tempFileName = System.IO.Path.GetTempFileName ();
-		var xclipargs = "-selection clipboard -o";
-
-		try {
-			var (exitCode, result) = ClipboardProcessRunner.Bash ($"{_xclipPath} {xclipargs} > {tempFileName}", waitForOutput: false);
-			if (exitCode == 0) {
-				if (Application.Driver is CursesDriver) {
-					Curses.raw ();
-					Curses.noecho ();
-				}
-				return System.IO.File.ReadAllText (tempFileName);
-			}
-		} catch (Exception e) {
-			throw new NotSupportedException ($"\"{_xclipPath} {xclipargs}\" failed.", e);
-		} finally {
-			System.IO.File.Delete (tempFileName);
-		}
-		return string.Empty;
-	}
-
-	protected override void SetClipboardDataImpl (string text)
-	{
-		var xclipargs = "-selection clipboard -i";
-		try {
-			var (exitCode, _) = ClipboardProcessRunner.Bash ($"{_xclipPath} {xclipargs}", text, waitForOutput: false);
-			if (exitCode == 0 && Application.Driver is CursesDriver) {
-				Curses.raw ();
-				Curses.noecho ();
-			}
-		} catch (Exception e) {
-			throw new NotSupportedException ($"\"{_xclipPath} {xclipargs} < {text}\" failed", e);
-		}
-	}
+        return false;
+    }
 }
+
 /// <summary>
-///  A clipboard implementation for MacOSX. 
-///  This implementation uses the Mac clipboard API (via P/Invoke) to copy/paste.
-///  The existance of the Mac pbcopy and pbpaste commands 
-///  is used to determine if copy/paste is supported.
-/// </summary>	
-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 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);
-		IsSupported = CheckSupport ();
-	}
-
-	public override bool IsSupported { get; }
-
-	bool CheckSupport ()
-	{
-		var (exitCode, result) = ClipboardProcessRunner.Bash ("which pbcopy", waitForOutput: true);
-		if (exitCode != 0 || !result.FileExists ()) {
-			return false;
-		}
-		(exitCode, result) = ClipboardProcessRunner.Bash ("which pbpaste", waitForOutput: true);
-		return exitCode == 0 && result.FileExists ();
-	}
-
-	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);
+///     A clipboard implementation for MacOSX. This implementation uses the Mac clipboard API (via P/Invoke) to
+///     copy/paste. The existance of the Mac pbcopy and pbpaste commands is used to determine if copy/paste is supported.
+/// </summary>
+internal class MacOSXClipboard : ClipboardBase
+{
+    private readonly nint _allocRegister = sel_registerName ("alloc");
+    private readonly nint _clearContentsRegister = sel_registerName ("clearContents");
+    private readonly nint _generalPasteboard;
+    private readonly nint _generalPasteboardRegister = sel_registerName ("generalPasteboard");
+    private readonly nint _initWithUtf8Register = sel_registerName ("initWithUTF8String:");
+    private readonly nint _nsPasteboard = objc_getClass ("NSPasteboard");
+    private readonly nint _nsString = objc_getClass ("NSString");
+    private readonly nint _nsStringPboardType;
+    private readonly nint _setStringRegister = sel_registerName ("setString:forType:");
+    private readonly nint _stringForTypeRegister = sel_registerName ("stringForType:");
+    private readonly nint _utf8Register = sel_registerName ("UTF8String");
+    private readonly nint _utfTextType;
+
+    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);
+        IsSupported = CheckSupport ();
+    }
+
+    public override bool IsSupported { get; }
+
+    protected override string GetClipboardDataImpl ()
+    {
+        nint ptr = objc_msgSend (_generalPasteboard, _stringForTypeRegister, _nsStringPboardType);
+        nint charArray = objc_msgSend (ptr, _utf8Register);
+
+        return Marshal.PtrToStringAnsi (charArray);
+    }
+
+    protected override void SetClipboardDataImpl (string text)
+    {
+        nint 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 (nint))
+            {
+                objc_msgSend (str, sel_registerName ("release"));
+            }
+        }
+    }
+
+    private bool CheckSupport ()
+    {
+        (int exitCode, string result) = ClipboardProcessRunner.Bash ("which pbcopy", waitForOutput: true);
+
+        if (exitCode != 0 || !result.FileExists ())
+        {
+            return false;
+        }
+
+        (exitCode, result) = ClipboardProcessRunner.Bash ("which pbpaste", waitForOutput: true);
+
+        return exitCode == 0 && result.FileExists ();
+    }
+
+    [DllImport ("/System/Library/Frameworks/AppKit.framework/AppKit")]
+    private static extern nint objc_getClass (string className);
+
+    [DllImport ("/System/Library/Frameworks/AppKit.framework/AppKit")]
+    private static extern nint objc_msgSend (nint receiver, nint selector);
+
+    [DllImport ("/System/Library/Frameworks/AppKit.framework/AppKit")]
+    private static extern nint objc_msgSend (nint receiver, nint selector, string arg1);
+
+    [DllImport ("/System/Library/Frameworks/AppKit.framework/AppKit")]
+    private static extern nint objc_msgSend (nint receiver, nint selector, nint arg1);
+
+    [DllImport ("/System/Library/Frameworks/AppKit.framework/AppKit")]
+    private static extern nint objc_msgSend (nint receiver, nint selector, nint arg1, nint arg2);
+
+    [DllImport ("/System/Library/Frameworks/AppKit.framework/AppKit")]
+    private static extern nint sel_registerName (string selectorName);
 }
 
 /// <summary>
-///  A clipboard implementation for Linux, when running under WSL. 
-///  This implementation uses the Windows clipboard to store the data, and uses Windows'
-///  powershell.exe (launched via WSL interop services) to set/get the Windows
-///  clipboard. 
+///     A clipboard implementation for Linux, when running under WSL. This implementation uses the Windows clipboard
+///     to store the data, and uses Windows' powershell.exe (launched via WSL interop services) to set/get the Windows
+///     clipboard.
 /// </summary>
-class WSLClipboard : ClipboardBase {
-	public WSLClipboard ()
-	{
-	}
-
-	public override bool IsSupported {
-		get {
-			return CheckSupport ();
-		}
-	}
-
-	private static string _powershellPath = string.Empty;
-
-	bool CheckSupport ()
-	{
-		if (string.IsNullOrEmpty (_powershellPath)) {
-			// Specify pwsh.exe (not pwsh) to ensure we get the Windows version (invoked via WSL)
-			var (exitCode, result) = ClipboardProcessRunner.Bash ("which pwsh.exe", waitForOutput: true);
-			if (exitCode > 0) {
-				(exitCode, result) = ClipboardProcessRunner.Bash ("which powershell.exe", waitForOutput: true);
-			}
-
-			if (exitCode == 0) {
-				_powershellPath = result;
-			}
-		}
-		return !string.IsNullOrEmpty (_powershellPath);
-	}
-
-	protected override string GetClipboardDataImpl ()
-	{
-		if (!IsSupported) {
-			return string.Empty;
-		}
-
-		var (exitCode, output) = ClipboardProcessRunner.Process (_powershellPath, "-noprofile -command \"Get-Clipboard\"");
-		if (exitCode == 0) {
-			if (Application.Driver is CursesDriver) {
-				Curses.raw ();
-				Curses.noecho ();
-			}
-
-			if (output.EndsWith ("\r\n")) {
-				output = output.Substring (0, output.Length - 2);
-			}
-			return output;
-		}
-		return string.Empty;
-	}
-
-	protected override void SetClipboardDataImpl (string text)
-	{
-		if (!IsSupported) {
-			return;
-		}
-
-		var (exitCode, output) = ClipboardProcessRunner.Process (_powershellPath, $"-noprofile -command \"Set-Clipboard -Value \\\"{text}\\\"\"");
-		if (exitCode == 0) {
-			if (Application.Driver is CursesDriver) {
-				Curses.raw ();
-				Curses.noecho ();
-			}
-		}
-	}
+internal class WSLClipboard : ClipboardBase
+{
+    private static string _powershellPath = string.Empty;
+    public override bool IsSupported => CheckSupport ();
+
+    protected override string GetClipboardDataImpl ()
+    {
+        if (!IsSupported)
+        {
+            return string.Empty;
+        }
+
+        (int exitCode, string output) =
+            ClipboardProcessRunner.Process (_powershellPath, "-noprofile -command \"Get-Clipboard\"");
+
+        if (exitCode == 0)
+        {
+            if (Application.Driver is CursesDriver)
+            {
+                Curses.raw ();
+                Curses.noecho ();
+            }
+
+            if (output.EndsWith ("\r\n"))
+            {
+                output = output.Substring (0, output.Length - 2);
+            }
+
+            return output;
+        }
+
+        return string.Empty;
+    }
+
+    protected override void SetClipboardDataImpl (string text)
+    {
+        if (!IsSupported)
+        {
+            return;
+        }
+
+        (int exitCode, string output) = ClipboardProcessRunner.Process (
+                                                                        _powershellPath,
+                                                                        $"-noprofile -command \"Set-Clipboard -Value \\\"{text}\\\"\""
+                                                                       );
+
+        if (exitCode == 0)
+        {
+            if (Application.Driver is CursesDriver)
+            {
+                Curses.raw ();
+                Curses.noecho ();
+            }
+        }
+    }
+
+    private bool CheckSupport ()
+    {
+        if (string.IsNullOrEmpty (_powershellPath))
+        {
+            // Specify pwsh.exe (not pwsh) to ensure we get the Windows version (invoked via WSL)
+            (int exitCode, string result) = ClipboardProcessRunner.Bash ("which pwsh.exe", waitForOutput: true);
+
+            if (exitCode > 0)
+            {
+                (exitCode, result) = ClipboardProcessRunner.Bash ("which powershell.exe", waitForOutput: true);
+            }
+
+            if (exitCode == 0)
+            {
+                _powershellPath = result;
+            }
+        }
+
+        return !string.IsNullOrEmpty (_powershellPath);
+    }
 }

+ 1007 - 784
Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs

@@ -1,795 +1,1018 @@
 //
 // Driver.cs: Curses-based Driver
 //
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
+
 using System.Runtime.InteropServices;
-using System.Text;
 using Terminal.Gui.ConsoleDrivers;
 using Unix.Terminal;
 
 namespace Terminal.Gui;
 
-/// <summary>
-/// This is the Curses driver for the gui.cs/Terminal framework.
-/// </summary>
-class CursesDriver : ConsoleDriver {
-	public override int Cols {
-		get => Curses.Cols;
-		internal set {
-			Curses.Cols = value;
-			ClearContents();
-		}
-	}
-
-	public override int Rows {
-		get => Curses.Lines;
-		internal set {
-			Curses.Lines = value;
-			ClearContents();
-		}
-	}
-
-	CursorVisibility? _initialCursorVisibility = null;
-	CursorVisibility? _currentCursorVisibility = null;
-
-	public override string GetVersionInfo () => $"{Curses.curses_version ()}";
-	UnixMainLoop _mainLoopDriver = null;
-
-	public override bool SupportsTrueColor => false;
-
-	object _processInputToken;
-
-	internal override MainLoop Init ()
-	{
-		_mainLoopDriver = new UnixMainLoop (this);
-		if (!RunningUnitTests) {
-
-			_window = Curses.initscr ();
-			Curses.set_escdelay (10);
-
-			// Ensures that all procedures are performed at some previous closing.
-			Curses.doupdate ();
-
-			// 
-			// We are setting Invisible as default so we could ignore XTerm DECSUSR setting
-			//
-			switch (Curses.curs_set (0)) {
-			case 0:
-				_currentCursorVisibility = _initialCursorVisibility = CursorVisibility.Invisible;
-				break;
-
-			case 1:
-				_currentCursorVisibility = _initialCursorVisibility = CursorVisibility.Underline;
-				Curses.curs_set (1);
-				break;
-
-			case 2:
-				_currentCursorVisibility = _initialCursorVisibility = CursorVisibility.Box;
-				Curses.curs_set (2);
-				break;
-
-			default:
-				_currentCursorVisibility = _initialCursorVisibility = null;
-				break;
-			}
-			if (!Curses.HasColors) {
-				throw new InvalidOperationException ("V2 - This should never happen. File an Issue if it does.");
-			}
-
-			Curses.raw ();
-			Curses.noecho ();
-
-			Curses.Window.Standard.keypad (true);
-
-			Curses.StartColor ();
-			Curses.UseDefaultColors ();
-
-			if (!RunningUnitTests) {
-				Curses.timeout (0);
-			}
-
-			_processInputToken = _mainLoopDriver?.AddWatch (0, UnixMainLoop.Condition.PollIn, x => {
-				ProcessInput ();
-				return true;
-			});
-		}
-
-		CurrentAttribute = new Attribute (ColorName.White, ColorName.Black);
-
-		if (Environment.OSVersion.Platform == PlatformID.Win32NT) {
-			Clipboard = new FakeDriver.FakeClipboard ();
-		} else {
-			if (RuntimeInformation.IsOSPlatform (OSPlatform.OSX)) {
-				Clipboard = new MacOSXClipboard ();
-			} else {
-				if (Is_WSL_Platform ()) {
-					Clipboard = new WSLClipboard ();
-				} else {
-					Clipboard = new CursesClipboard ();
-				}
-			}
-		}
-
-		ClearContents ();
-		StartReportingMouseMoves ();
-
-		if (!RunningUnitTests) {
-			Curses.CheckWinChange ();
-			Curses.refresh ();
-		}
-		return new MainLoop (_mainLoopDriver);
-	}
-
-	public override void Move (int col, int row)
-	{
-		base.Move (col, row);
-
-		if (RunningUnitTests) {
-			return;
-		}
-
-		if (IsValidLocation (col, row)) {
-			Curses.move (row, col);
-		} else {
-			// Not a valid location (outside screen or clip region)
-			// Move within the clip region, then AddRune will actually move to Col, Row
-			Curses.move (Clip.Y, Clip.X);
-		}
-	}
-
-	public override bool IsRuneSupported (Rune rune) =>
-		// See Issue #2615 - CursesDriver is broken with non-BMP characters
-		base.IsRuneSupported (rune) && rune.IsBmp;
-
-	public override void Refresh ()
-	{
-		UpdateScreen ();
-		UpdateCursor ();
-	}
-
-	internal void ProcessWinChange ()
-	{
-		if (!RunningUnitTests && Curses.CheckWinChange ()) {
-			ClearContents ();
-			OnSizeChanged (new SizeChangedEventArgs (new Size (Cols, Rows)));
-		}
-	}
-
-	#region Color Handling
-	/// <summary>
-	/// Creates an Attribute from the provided curses-based foreground and background color numbers
-	/// </summary>
-	/// <param name="foreground">Contains the curses color number for the foreground (color, plus any attributes)</param>
-	/// <param name="background">Contains the curses color number for the background (color, plus any attributes)</param>
-	/// <returns></returns>
-	static Attribute MakeColor (short foreground, short background)
-	{
-		short v = (short)((int)foreground | background << 4);
-
-		// TODO: for TrueColor - Use InitExtendedPair
-		Curses.InitColorPair (v, foreground, background);
-		return new Attribute (
-			Curses.ColorPair (v),
-			CursesColorNumberToColorName (foreground),
-			CursesColorNumberToColorName (background));
-	}
-
-	/// <inheritdoc/>
-	/// <remarks>
-	/// In the CursesDriver, colors are encoded as an int. 
-	/// The foreground color is stored in the most significant 4 bits, 
-	/// and the background color is stored in the least significant 4 bits.
-	/// The Terminal.GUi Color values are converted to curses color encoding before being encoded.
-	/// </remarks>
-	public override Attribute MakeColor (Color foreground, Color background)
-	{
-		if (!RunningUnitTests) {
-			return MakeColor (ColorNameToCursesColorNumber (foreground.ColorName), ColorNameToCursesColorNumber (background.ColorName));
-		} else {
-			return new Attribute (
-				0,
-				foreground,
-				background);
-		}
-	}
-
-	static short ColorNameToCursesColorNumber (ColorName color)
-	{
-		switch (color) {
-		case ColorName.Black:
-			return Curses.COLOR_BLACK;
-		case ColorName.Blue:
-			return Curses.COLOR_BLUE;
-		case ColorName.Green:
-			return Curses.COLOR_GREEN;
-		case ColorName.Cyan:
-			return Curses.COLOR_CYAN;
-		case ColorName.Red:
-			return Curses.COLOR_RED;
-		case ColorName.Magenta:
-			return Curses.COLOR_MAGENTA;
-		case ColorName.Yellow:
-			return Curses.COLOR_YELLOW;
-		case ColorName.Gray:
-			return Curses.COLOR_WHITE;
-		case ColorName.DarkGray:
-			return Curses.COLOR_GRAY;
-		case ColorName.BrightBlue:
-			return Curses.COLOR_BLUE | Curses.COLOR_GRAY;
-		case ColorName.BrightGreen:
-			return Curses.COLOR_GREEN | Curses.COLOR_GRAY;
-		case ColorName.BrightCyan:
-			return Curses.COLOR_CYAN | Curses.COLOR_GRAY;
-		case ColorName.BrightRed:
-			return Curses.COLOR_RED | Curses.COLOR_GRAY;
-		case ColorName.BrightMagenta:
-			return Curses.COLOR_MAGENTA | Curses.COLOR_GRAY;
-		case ColorName.BrightYellow:
-			return Curses.COLOR_YELLOW | Curses.COLOR_GRAY;
-		case ColorName.White:
-			return Curses.COLOR_WHITE | Curses.COLOR_GRAY;
-		}
-		throw new ArgumentException ("Invalid color code");
-	}
-
-	static ColorName CursesColorNumberToColorName (short color)
-	{
-		switch (color) {
-		case Curses.COLOR_BLACK:
-			return ColorName.Black;
-		case Curses.COLOR_BLUE:
-			return ColorName.Blue;
-		case Curses.COLOR_GREEN:
-			return ColorName.Green;
-		case Curses.COLOR_CYAN:
-			return ColorName.Cyan;
-		case Curses.COLOR_RED:
-			return ColorName.Red;
-		case Curses.COLOR_MAGENTA:
-			return ColorName.Magenta;
-		case Curses.COLOR_YELLOW:
-			return ColorName.Yellow;
-		case Curses.COLOR_WHITE:
-			return ColorName.Gray;
-		case Curses.COLOR_GRAY:
-			return ColorName.DarkGray;
-		case Curses.COLOR_BLUE | Curses.COLOR_GRAY:
-			return ColorName.BrightBlue;
-		case Curses.COLOR_GREEN | Curses.COLOR_GRAY:
-			return ColorName.BrightGreen;
-		case Curses.COLOR_CYAN | Curses.COLOR_GRAY:
-			return ColorName.BrightCyan;
-		case Curses.COLOR_RED | Curses.COLOR_GRAY:
-			return ColorName.BrightRed;
-		case Curses.COLOR_MAGENTA | Curses.COLOR_GRAY:
-			return ColorName.BrightMagenta;
-		case Curses.COLOR_YELLOW | Curses.COLOR_GRAY:
-			return ColorName.BrightYellow;
-		case Curses.COLOR_WHITE | Curses.COLOR_GRAY:
-			return ColorName.White;
-		}
-		throw new ArgumentException ("Invalid curses color code");
-	}
-	#endregion
-
-	public override void UpdateCursor ()
-	{
-		EnsureCursorVisibility ();
-
-		if (!RunningUnitTests && Col >= 0 && Col < Cols && Row >= 0 && Row < Rows) {
-			Curses.move (Row, Col);
-		}
-	}
-
-	internal override void End ()
-	{
-		StopReportingMouseMoves ();
-		SetCursorVisibility (CursorVisibility.Default);
-
-		if (_mainLoopDriver != null) {
-			_mainLoopDriver.RemoveWatch (_processInputToken);
-		}
-
-		if (RunningUnitTests) {
-			return;
-		}
-		// throws away any typeahead that has been typed by
-		// the user and has not yet been read by the program.
-		Curses.flushinp ();
-
-		Curses.endwin ();
-	}
-
-	public override void UpdateScreen ()
-	{
-		for (int row = 0; row < Rows; row++) {
-			if (!_dirtyLines [row]) {
-				continue;
-			}
-			_dirtyLines [row] = false;
-
-			for (int col = 0; col < Cols; col++) {
-				if (Contents [row, col].IsDirty == false) {
-					continue;
-				}
-				if (RunningUnitTests) {
-					// In unit tests, we don't want to actually write to the screen.
-					continue;
-				}
-				Curses.attrset (Contents [row, col].Attribute.GetValueOrDefault ().PlatformColor);
-
-				var rune = Contents [row, col].Rune;
-				if (rune.IsBmp) {
-					// BUGBUG: CursesDriver doesn't render CharMap correctly for wide chars (and other Unicode) - Curses is doing something funky with glyphs that report GetColums() of 1 yet are rendered wide. E.g. 0x2064 (invisible times) is reported as 1 column but is rendered as 2. WindowsDriver & NetDriver correctly render this as 1 column, overlapping the next cell.
-					if (rune.GetColumns () < 2) {
-						Curses.mvaddch (row, col, rune.Value);
-					} else /*if (col + 1 < Cols)*/ {
-						Curses.mvaddwstr (row, col, rune.ToString ());
-					}
-
-				} else {
-					Curses.mvaddwstr (row, col, rune.ToString ());
-					if (rune.GetColumns () > 1 && col + 1 < Cols) {
-						// TODO: This is a hack to deal with non-BMP and wide characters.
-						//col++;
-						Curses.mvaddch (row, ++col, '*');
-					}
-				}
-			}
-		}
-
-		if (!RunningUnitTests) {
-			Curses.move (Row, Col);
-			_window.wrefresh ();
-		}
-	}
-
-	public Curses.Window _window;
-
-	static KeyCode MapCursesKey (int cursesKey)
-	{
-		switch (cursesKey) {
-		case Curses.KeyF1: return KeyCode.F1;
-		case Curses.KeyF2: return KeyCode.F2;
-		case Curses.KeyF3: return KeyCode.F3;
-		case Curses.KeyF4: return KeyCode.F4;
-		case Curses.KeyF5: return KeyCode.F5;
-		case Curses.KeyF6: return KeyCode.F6;
-		case Curses.KeyF7: return KeyCode.F7;
-		case Curses.KeyF8: return KeyCode.F8;
-		case Curses.KeyF9: return KeyCode.F9;
-		case Curses.KeyF10: return KeyCode.F10;
-		case Curses.KeyF11: return KeyCode.F11;
-		case Curses.KeyF12: return KeyCode.F12;
-		case Curses.KeyUp: return KeyCode.CursorUp;
-		case Curses.KeyDown: return KeyCode.CursorDown;
-		case Curses.KeyLeft: return KeyCode.CursorLeft;
-		case Curses.KeyRight: return KeyCode.CursorRight;
-		case Curses.KeyHome: return KeyCode.Home;
-		case Curses.KeyEnd: return KeyCode.End;
-		case Curses.KeyNPage: return KeyCode.PageDown;
-		case Curses.KeyPPage: return KeyCode.PageUp;
-		case Curses.KeyDeleteChar: return KeyCode.Delete;
-		case Curses.KeyInsertChar: return KeyCode.Insert;
-		case Curses.KeyTab: return KeyCode.Tab;
-		case Curses.KeyBackTab: return KeyCode.Tab | KeyCode.ShiftMask;
-		case Curses.KeyBackspace: return KeyCode.Backspace;
-		case Curses.ShiftKeyUp: return KeyCode.CursorUp | KeyCode.ShiftMask;
-		case Curses.ShiftKeyDown: return KeyCode.CursorDown | KeyCode.ShiftMask;
-		case Curses.ShiftKeyLeft: return KeyCode.CursorLeft | KeyCode.ShiftMask;
-		case Curses.ShiftKeyRight: return KeyCode.CursorRight | KeyCode.ShiftMask;
-		case Curses.ShiftKeyHome: return KeyCode.Home | KeyCode.ShiftMask;
-		case Curses.ShiftKeyEnd: return KeyCode.End | KeyCode.ShiftMask;
-		case Curses.ShiftKeyNPage: return KeyCode.PageDown | KeyCode.ShiftMask;
-		case Curses.ShiftKeyPPage: return KeyCode.PageUp | KeyCode.ShiftMask;
-		case Curses.AltKeyUp: return KeyCode.CursorUp | KeyCode.AltMask;
-		case Curses.AltKeyDown: return KeyCode.CursorDown | KeyCode.AltMask;
-		case Curses.AltKeyLeft: return KeyCode.CursorLeft | KeyCode.AltMask;
-		case Curses.AltKeyRight: return KeyCode.CursorRight | KeyCode.AltMask;
-		case Curses.AltKeyHome: return KeyCode.Home | KeyCode.AltMask;
-		case Curses.AltKeyEnd: return KeyCode.End | KeyCode.AltMask;
-		case Curses.AltKeyNPage: return KeyCode.PageDown | KeyCode.AltMask;
-		case Curses.AltKeyPPage: return KeyCode.PageUp | KeyCode.AltMask;
-		case Curses.CtrlKeyUp: return KeyCode.CursorUp | KeyCode.CtrlMask;
-		case Curses.CtrlKeyDown: return KeyCode.CursorDown | KeyCode.CtrlMask;
-		case Curses.CtrlKeyLeft: return KeyCode.CursorLeft | KeyCode.CtrlMask;
-		case Curses.CtrlKeyRight: return KeyCode.CursorRight | KeyCode.CtrlMask;
-		case Curses.CtrlKeyHome: return KeyCode.Home | KeyCode.CtrlMask;
-		case Curses.CtrlKeyEnd: return KeyCode.End | KeyCode.CtrlMask;
-		case Curses.CtrlKeyNPage: return KeyCode.PageDown | KeyCode.CtrlMask;
-		case Curses.CtrlKeyPPage: return KeyCode.PageUp | KeyCode.CtrlMask;
-		case Curses.ShiftCtrlKeyUp: return KeyCode.CursorUp | KeyCode.ShiftMask | KeyCode.CtrlMask;
-		case Curses.ShiftCtrlKeyDown: return KeyCode.CursorDown | KeyCode.ShiftMask | KeyCode.CtrlMask;
-		case Curses.ShiftCtrlKeyLeft: return KeyCode.CursorLeft | KeyCode.ShiftMask | KeyCode.CtrlMask;
-		case Curses.ShiftCtrlKeyRight: return KeyCode.CursorRight | KeyCode.ShiftMask | KeyCode.CtrlMask;
-		case Curses.ShiftCtrlKeyHome: return KeyCode.Home | KeyCode.ShiftMask | KeyCode.CtrlMask;
-		case Curses.ShiftCtrlKeyEnd: return KeyCode.End | KeyCode.ShiftMask | KeyCode.CtrlMask;
-		case Curses.ShiftCtrlKeyNPage: return KeyCode.PageDown | KeyCode.ShiftMask | KeyCode.CtrlMask;
-		case Curses.ShiftCtrlKeyPPage: return KeyCode.PageUp | KeyCode.ShiftMask | KeyCode.CtrlMask;
-		case Curses.ShiftAltKeyUp: return KeyCode.CursorUp | KeyCode.ShiftMask | KeyCode.AltMask;
-		case Curses.ShiftAltKeyDown: return KeyCode.CursorDown | KeyCode.ShiftMask | KeyCode.AltMask;
-		case Curses.ShiftAltKeyLeft: return KeyCode.CursorLeft | KeyCode.ShiftMask | KeyCode.AltMask;
-		case Curses.ShiftAltKeyRight: return KeyCode.CursorRight | KeyCode.ShiftMask | KeyCode.AltMask;
-		case Curses.ShiftAltKeyNPage: return KeyCode.PageDown | KeyCode.ShiftMask | KeyCode.AltMask;
-		case Curses.ShiftAltKeyPPage: return KeyCode.PageUp | KeyCode.ShiftMask | KeyCode.AltMask;
-		case Curses.ShiftAltKeyHome: return KeyCode.Home | KeyCode.ShiftMask | KeyCode.AltMask;
-		case Curses.ShiftAltKeyEnd: return KeyCode.End | KeyCode.ShiftMask | KeyCode.AltMask;
-		case Curses.AltCtrlKeyNPage: return KeyCode.PageDown | KeyCode.AltMask | KeyCode.CtrlMask;
-		case Curses.AltCtrlKeyPPage: return KeyCode.PageUp | KeyCode.AltMask | KeyCode.CtrlMask;
-		case Curses.AltCtrlKeyHome: return KeyCode.Home | KeyCode.AltMask | KeyCode.CtrlMask;
-		case Curses.AltCtrlKeyEnd: return KeyCode.End | KeyCode.AltMask | KeyCode.CtrlMask;
-		default: return KeyCode.Null;
-		}
-	}
-
-	internal void ProcessInput ()
-	{
-		int wch;
-		int code = Curses.get_wch (out wch);
-		//System.Diagnostics.Debug.WriteLine ($"code: {code}; wch: {wch}");
-		if (code == Curses.ERR) {
-			return;
-		}
-		var k = KeyCode.Null;
-
-		if (code == Curses.KEY_CODE_YES) {
-			while (code == Curses.KEY_CODE_YES && wch == Curses.KeyResize) {
-				ProcessWinChange ();
-				code = Curses.get_wch (out wch);
-			}
-			if (wch == 0) {
-				return;
-			}
-			if (wch == Curses.KeyMouse) {
-				int wch2 = wch;
-
-				while (wch2 == Curses.KeyMouse) {
-					Key kea = null;
-					var cki = new ConsoleKeyInfo [] {
-						new ((char)KeyCode.Esc, 0, false, false, false),
-						new ('[', 0, false, false, false),
-						new ('<', 0, false, false, false)
-					};
-					code = 0;
-					HandleEscSeqResponse (ref code, ref k, ref wch2, ref kea, ref cki);
-				}
-				return;
-			}
-			k = MapCursesKey (wch);
-			if (wch >= 277 && wch <= 288) {
-				// Shift+(F1 - F12)
-				wch -= 12;
-				k = KeyCode.ShiftMask | MapCursesKey (wch);
-			} else if (wch >= 289 && wch <= 300) {
-				// Ctrl+(F1 - F12)
-				wch -= 24;
-				k = KeyCode.CtrlMask | MapCursesKey (wch);
-			} else if (wch >= 301 && wch <= 312) {
-				// Ctrl+Shift+(F1 - F12)
-				wch -= 36;
-				k = KeyCode.CtrlMask | KeyCode.ShiftMask | MapCursesKey (wch);
-			} else if (wch >= 313 && wch <= 324) {
-				// Alt+(F1 - F12)
-				wch -= 48;
-				k = KeyCode.AltMask | MapCursesKey (wch);
-			} else if (wch >= 325 && wch <= 327) {
-				// Shift+Alt+(F1 - F3)
-				wch -= 60;
-				k = KeyCode.ShiftMask | KeyCode.AltMask | MapCursesKey (wch);
-			}
-			OnKeyDown (new Key (k));
-			OnKeyUp (new Key (k));
-			return;
-		}
-
-		// Special handling for ESC, we want to try to catch ESC+letter to simulate alt-letter as well as Alt-Fkey
-		if (wch == 27) {
-			Curses.timeout (10);
-
-			code = Curses.get_wch (out int wch2);
-
-			if (code == Curses.KEY_CODE_YES) {
-				k = KeyCode.AltMask | MapCursesKey (wch);
-			}
-			Key key = null;
-			if (code == 0) {
-
-				// The ESC-number handling, debatable.
-				// Simulates the AltMask itself by pressing Alt + Space.
-				if (wch2 == (int)KeyCode.Space) {
-					k = KeyCode.AltMask;
-				} else if (wch2 - (int)KeyCode.Space >= (uint)KeyCode.A && wch2 - (int)KeyCode.Space <= (uint)KeyCode.Z) {
-					k = (KeyCode)((uint)KeyCode.AltMask + (wch2 - (int)KeyCode.Space));
-				} else if (wch2 >= (uint)KeyCode.A - 64 && wch2 <= (uint)KeyCode.Z - 64) {
-					k = (KeyCode)((uint)(KeyCode.AltMask | KeyCode.CtrlMask) + (wch2 + 64));
-				} else if (wch2 >= (uint)KeyCode.D0 && wch2 <= (uint)KeyCode.D9) {
-					k = (KeyCode)((uint)KeyCode.AltMask + (uint)KeyCode.D0 + (wch2 - (uint)KeyCode.D0));
-				} else if (wch2 == Curses.KeyCSI) {
-					var cki = new ConsoleKeyInfo [] {
-						new ((char)KeyCode.Esc, 0, false, false, false),
-						new ('[', 0, false, false, false)
-					};
-					HandleEscSeqResponse (ref code, ref k, ref wch2, ref key, ref cki);
-					return;
-				} else {
-					// Unfortunately there are no way to differentiate Ctrl+Alt+alfa and Ctrl+Shift+Alt+alfa.
-					if (((KeyCode)wch2 & KeyCode.CtrlMask) != 0) {
-						k = (KeyCode)((uint)KeyCode.CtrlMask + (wch2 & ~(int)KeyCode.CtrlMask));
-					}
-					if (wch2 == 0) {
-						k = KeyCode.CtrlMask | KeyCode.AltMask | KeyCode.Space;
-					} else if (wch >= (uint)KeyCode.A && wch <= (uint)KeyCode.Z) {
-						k = KeyCode.ShiftMask | KeyCode.AltMask | KeyCode.Space;
-					} else if (wch2 < 256) {
-						k = (KeyCode)wch2;// | KeyCode.AltMask;
-					} else {
-						k = (KeyCode)((uint)(KeyCode.AltMask | KeyCode.CtrlMask) + wch2);
-					}
-				} 
-				key = new Key (k);
-			} else {
-				key = new Key (KeyCode.Esc);
-			}
-			OnKeyDown (key);
-			OnKeyUp (key);
-		} else if (wch == Curses.KeyTab) {
-			k = MapCursesKey (wch);
-			OnKeyDown (new Key (k));
-			OnKeyUp (new Key (k));
-		} else {
-			// Unfortunately there are no way to differentiate Ctrl+alfa and Ctrl+Shift+alfa.
-			k = (KeyCode)wch;
-			if (wch == 0) {
-				k = KeyCode.CtrlMask | KeyCode.Space;
-			} else if (wch >= (uint)KeyCode.A - 64 && wch <= (uint)KeyCode.Z - 64) {
-				if ((KeyCode)(wch + 64) != KeyCode.J) {
-					k = KeyCode.CtrlMask | (KeyCode)(wch + 64);
-				}
-			} else if (wch >= (uint)KeyCode.A && wch <= (uint)KeyCode.Z) {
-				k = (KeyCode)wch | KeyCode.ShiftMask;
-			} 
-			
-			if (wch == '\n' || wch == '\r') {
-				k = KeyCode.Enter;
-			}
-			OnKeyDown (new Key (k));
-			OnKeyUp (new Key (k));
-		}
-	}
-
-	void HandleEscSeqResponse (ref int code, ref KeyCode k, ref int wch2, ref Key keyEventArgs, ref ConsoleKeyInfo [] cki)
-	{
-		ConsoleKey ck = 0;
-		ConsoleModifiers mod = 0;
-		while (code == 0) {
-			code = Curses.get_wch (out wch2);
-			var consoleKeyInfo = new ConsoleKeyInfo ((char)wch2, 0, false, false, false);
-			if (wch2 == 0 || wch2 == 27 || wch2 == Curses.KeyMouse) {
-				EscSeqUtils.DecodeEscSeq (null, ref consoleKeyInfo, ref ck, cki, ref mod, out _, out _, out _, out _, out bool isKeyMouse, out var mouseFlags, out var pos, out _, ProcessMouseEvent);
-				if (isKeyMouse) {
-					foreach (var mf in mouseFlags) {
-						ProcessMouseEvent (mf, pos);
-					}
-					cki = null;
-					if (wch2 == 27) {
-						cki = EscSeqUtils.ResizeArray (new ConsoleKeyInfo ((char)KeyCode.Esc, 0,
-							false, false, false), cki);
-					}
-				} else {
-					k = ConsoleKeyMapping.MapConsoleKeyInfoToKeyCode (consoleKeyInfo);
-					keyEventArgs = new Key (k);
-					OnKeyDown (keyEventArgs);
-				}
-			} else {
-				cki = EscSeqUtils.ResizeArray (consoleKeyInfo, cki);
-			}
-		}
-	}
-
-	MouseFlags _lastMouseFlags;
-
-	void ProcessMouseEvent (MouseFlags mouseFlag, Point pos)
-	{
-		bool WasButtonReleased (MouseFlags flag) => flag.HasFlag (MouseFlags.Button1Released) ||
-							flag.HasFlag (MouseFlags.Button2Released) ||
-							flag.HasFlag (MouseFlags.Button3Released) ||
-							flag.HasFlag (MouseFlags.Button4Released);
-
-		bool IsButtonNotPressed (MouseFlags flag) => !flag.HasFlag (MouseFlags.Button1Pressed) &&
-								!flag.HasFlag (MouseFlags.Button2Pressed) &&
-								!flag.HasFlag (MouseFlags.Button3Pressed) &&
-								!flag.HasFlag (MouseFlags.Button4Pressed);
-
-		bool IsButtonClickedOrDoubleClicked (MouseFlags flag) => flag.HasFlag (MouseFlags.Button1Clicked) ||
-									flag.HasFlag (MouseFlags.Button2Clicked) ||
-									flag.HasFlag (MouseFlags.Button3Clicked) ||
-									flag.HasFlag (MouseFlags.Button4Clicked) ||
-									flag.HasFlag (MouseFlags.Button1DoubleClicked) ||
-									flag.HasFlag (MouseFlags.Button2DoubleClicked) ||
-									flag.HasFlag (MouseFlags.Button3DoubleClicked) ||
-									flag.HasFlag (MouseFlags.Button4DoubleClicked);
-
-		if (WasButtonReleased (mouseFlag) && IsButtonNotPressed (_lastMouseFlags) ||
-		IsButtonClickedOrDoubleClicked (mouseFlag) && _lastMouseFlags == 0) {
-			return;
-		}
-
-		_lastMouseFlags = mouseFlag;
-
-		var me = new MouseEvent () {
-			Flags = mouseFlag,
-			X = pos.X,
-			Y = pos.Y
-		};
-		OnMouseEvent (new MouseEventEventArgs (me));
-	}
-
-
-	public static bool Is_WSL_Platform ()
-	{
-		// xclip does not work on WSL, so we need to use the Windows clipboard vis Powershell
-		//if (new CursesClipboard ().IsSupported) {
-		//	// If xclip is installed on Linux under WSL, this will return true.
-		//	return false;
-		//}
-		(int exitCode, string result) = ClipboardProcessRunner.Bash ("uname -a", waitForOutput: true);
-		if (exitCode == 0 && result.Contains ("microsoft") && result.Contains ("WSL")) {
-			return true;
-		}
-		return false;
-	}
-
-	public override void Suspend ()
-	{
-		StopReportingMouseMoves ();
-		if (!RunningUnitTests) {
-			Platform.Suspend ();
-			Curses.Window.Standard.redrawwin ();
-			Curses.refresh ();
-		}
-		StartReportingMouseMoves ();
-	}
-
-	public void StartReportingMouseMoves ()
-	{
-		if (!RunningUnitTests) {
-			Console.Out.Write (EscSeqUtils.CSI_EnableMouseEvents);
-		}
-	}
-
-	public void StopReportingMouseMoves ()
-	{
-		if (!RunningUnitTests) {
-			Console.Out.Write (EscSeqUtils.CSI_DisableMouseEvents);
-		}
-	}
-
-	/// <inheritdoc/>
-	public override bool GetCursorVisibility (out CursorVisibility visibility)
-	{
-		visibility = CursorVisibility.Invisible;
-
-		if (!_currentCursorVisibility.HasValue) {
-			return false;
-		}
-
-		visibility = _currentCursorVisibility.Value;
-
-		return true;
-	}
-
-	/// <inheritdoc/>
-	public override bool SetCursorVisibility (CursorVisibility visibility)
-	{
-		if (_initialCursorVisibility.HasValue == false) {
-			return false;
-		}
-
-		if (!RunningUnitTests) {
-			Curses.curs_set ((int)visibility >> 16 & 0x000000FF);
-		}
-
-		if (visibility != CursorVisibility.Invisible) {
-			Console.Out.Write (EscSeqUtils.CSI_SetCursorStyle ((EscSeqUtils.DECSCUSR_Style)((int)visibility >> 24 & 0xFF)));
-		}
-
-		_currentCursorVisibility = visibility;
-
-		return true;
-	}
-
-	/// <inheritdoc/>
-	public override bool EnsureCursorVisibility () => false;
-
-	public override void SendKeys (char keyChar, ConsoleKey consoleKey, bool shift, bool alt, bool control)
-	{
-		KeyCode key;
-
-		if (consoleKey == ConsoleKey.Packet) {
-			var mod = new ConsoleModifiers ();
-			if (shift) {
-				mod |= ConsoleModifiers.Shift;
-			}
-			if (alt) {
-				mod |= ConsoleModifiers.Alt;
-			}
-			if (control) {
-				mod |= ConsoleModifiers.Control;
-			}
-			var cKeyInfo = new ConsoleKeyInfo (keyChar, consoleKey, shift, alt, control);
-			cKeyInfo = ConsoleKeyMapping.DecodeVKPacketToKConsoleKeyInfo (cKeyInfo);
-			key = ConsoleKeyMapping.MapConsoleKeyInfoToKeyCode (cKeyInfo);
-		} else {
-			key = (KeyCode)keyChar;
-		}
-
-		OnKeyDown (new Key (key));
-		OnKeyUp (new Key (key));
-		//OnKeyPressed (new KeyEventArgsEventArgs (key));
-	}
+/// <summary>This is the Curses driver for the gui.cs/Terminal framework.</summary>
+internal class CursesDriver : ConsoleDriver
+{
+    public Curses.Window _window;
+    private CursorVisibility? _currentCursorVisibility;
+    private CursorVisibility? _initialCursorVisibility;
+    private MouseFlags _lastMouseFlags;
+    private UnixMainLoop _mainLoopDriver;
+    private object _processInputToken;
+
+    public override int Cols
+    {
+        get => Curses.Cols;
+        internal set
+        {
+            Curses.Cols = value;
+            ClearContents ();
+        }
+    }
+
+    public override int Rows
+    {
+        get => Curses.Lines;
+        internal set
+        {
+            Curses.Lines = value;
+            ClearContents ();
+        }
+    }
+
+    public override bool SupportsTrueColor => false;
+
+    /// <inheritdoc/>
+    public override bool EnsureCursorVisibility () { return false; }
+
+    /// <inheritdoc/>
+    public override bool GetCursorVisibility (out CursorVisibility visibility)
+    {
+        visibility = CursorVisibility.Invisible;
+
+        if (!_currentCursorVisibility.HasValue)
+        {
+            return false;
+        }
+
+        visibility = _currentCursorVisibility.Value;
+
+        return true;
+    }
+
+    public override string GetVersionInfo () { return $"{Curses.curses_version ()}"; }
+
+    public static bool Is_WSL_Platform ()
+    {
+        // xclip does not work on WSL, so we need to use the Windows clipboard vis Powershell
+        //if (new CursesClipboard ().IsSupported) {
+        //	// If xclip is installed on Linux under WSL, this will return true.
+        //	return false;
+        //}
+        (int exitCode, string result) = ClipboardProcessRunner.Bash ("uname -a", waitForOutput: true);
+
+        if (exitCode == 0 && result.Contains ("microsoft") && result.Contains ("WSL"))
+        {
+            return true;
+        }
+
+        return false;
+    }
+
+    public override bool IsRuneSupported (Rune rune)
+    {
+        // See Issue #2615 - CursesDriver is broken with non-BMP characters
+        return base.IsRuneSupported (rune) && rune.IsBmp;
+    }
+
+    public override void Move (int col, int row)
+    {
+        base.Move (col, row);
+
+        if (RunningUnitTests)
+        {
+            return;
+        }
+
+        if (IsValidLocation (col, row))
+        {
+            Curses.move (row, col);
+        }
+        else
+        {
+            // Not a valid location (outside screen or clip region)
+            // Move within the clip region, then AddRune will actually move to Col, Row
+            Curses.move (Clip.Y, Clip.X);
+        }
+    }
+
+    public override void Refresh ()
+    {
+        UpdateScreen ();
+        UpdateCursor ();
+    }
+
+    public override void SendKeys (char keyChar, ConsoleKey consoleKey, bool shift, bool alt, bool control)
+    {
+        KeyCode key;
+
+        if (consoleKey == ConsoleKey.Packet)
+        {
+            var mod = new ConsoleModifiers ();
+
+            if (shift)
+            {
+                mod |= ConsoleModifiers.Shift;
+            }
+
+            if (alt)
+            {
+                mod |= ConsoleModifiers.Alt;
+            }
+
+            if (control)
+            {
+                mod |= ConsoleModifiers.Control;
+            }
+
+            var cKeyInfo = new ConsoleKeyInfo (keyChar, consoleKey, shift, alt, control);
+            cKeyInfo = ConsoleKeyMapping.DecodeVKPacketToKConsoleKeyInfo (cKeyInfo);
+            key = ConsoleKeyMapping.MapConsoleKeyInfoToKeyCode (cKeyInfo);
+        }
+        else
+        {
+            key = (KeyCode)keyChar;
+        }
+
+        OnKeyDown (new Key (key));
+        OnKeyUp (new Key (key));
+
+        //OnKeyPressed (new KeyEventArgsEventArgs (key));
+    }
+
+    /// <inheritdoc/>
+    public override bool SetCursorVisibility (CursorVisibility visibility)
+    {
+        if (_initialCursorVisibility.HasValue == false)
+        {
+            return false;
+        }
+
+        if (!RunningUnitTests)
+        {
+            Curses.curs_set (((int)visibility >> 16) & 0x000000FF);
+        }
+
+        if (visibility != CursorVisibility.Invisible)
+        {
+            Console.Out.Write (
+                               EscSeqUtils.CSI_SetCursorStyle (
+                                                               (EscSeqUtils.DECSCUSR_Style)(((int)visibility >> 24)
+                                                                                            & 0xFF)
+                                                              )
+                              );
+        }
+
+        _currentCursorVisibility = visibility;
+
+        return true;
+    }
+
+    public void StartReportingMouseMoves ()
+    {
+        if (!RunningUnitTests)
+        {
+            Console.Out.Write (EscSeqUtils.CSI_EnableMouseEvents);
+        }
+    }
+
+    public void StopReportingMouseMoves ()
+    {
+        if (!RunningUnitTests)
+        {
+            Console.Out.Write (EscSeqUtils.CSI_DisableMouseEvents);
+        }
+    }
+
+    public override void Suspend ()
+    {
+        StopReportingMouseMoves ();
+
+        if (!RunningUnitTests)
+        {
+            Platform.Suspend ();
+            Curses.Window.Standard.redrawwin ();
+            Curses.refresh ();
+        }
+
+        StartReportingMouseMoves ();
+    }
+
+    public override void UpdateCursor ()
+    {
+        EnsureCursorVisibility ();
+
+        if (!RunningUnitTests && Col >= 0 && Col < Cols && Row >= 0 && Row < Rows)
+        {
+            Curses.move (Row, Col);
+        }
+    }
+
+    public override void UpdateScreen ()
+    {
+        for (var row = 0; row < Rows; row++)
+        {
+            if (!_dirtyLines [row])
+            {
+                continue;
+            }
+
+            _dirtyLines [row] = false;
+
+            for (var col = 0; col < Cols; col++)
+            {
+                if (Contents [row, col].IsDirty == false)
+                {
+                    continue;
+                }
+
+                if (RunningUnitTests)
+                {
+                    // In unit tests, we don't want to actually write to the screen.
+                    continue;
+                }
+
+                Curses.attrset (Contents [row, col].Attribute.GetValueOrDefault ().PlatformColor);
+
+                Rune rune = Contents [row, col].Rune;
+
+                if (rune.IsBmp)
+                {
+                    // BUGBUG: CursesDriver doesn't render CharMap correctly for wide chars (and other Unicode) - Curses is doing something funky with glyphs that report GetColums() of 1 yet are rendered wide. E.g. 0x2064 (invisible times) is reported as 1 column but is rendered as 2. WindowsDriver & NetDriver correctly render this as 1 column, overlapping the next cell.
+                    if (rune.GetColumns () < 2)
+                    {
+                        Curses.mvaddch (row, col, rune.Value);
+                    }
+                    else /*if (col + 1 < Cols)*/
+                    {
+                        Curses.mvaddwstr (row, col, rune.ToString ());
+                    }
+                }
+                else
+                {
+                    Curses.mvaddwstr (row, col, rune.ToString ());
+
+                    if (rune.GetColumns () > 1 && col + 1 < Cols)
+                    {
+                        // TODO: This is a hack to deal with non-BMP and wide characters.
+                        //col++;
+                        Curses.mvaddch (row, ++col, '*');
+                    }
+                }
+            }
+        }
+
+        if (!RunningUnitTests)
+        {
+            Curses.move (Row, Col);
+            _window.wrefresh ();
+        }
+    }
+
+    internal override void End ()
+    {
+        StopReportingMouseMoves ();
+        SetCursorVisibility (CursorVisibility.Default);
+
+        if (_mainLoopDriver is { })
+        {
+            _mainLoopDriver.RemoveWatch (_processInputToken);
+        }
+
+        if (RunningUnitTests)
+        {
+            return;
+        }
+
+        // throws away any typeahead that has been typed by
+        // the user and has not yet been read by the program.
+        Curses.flushinp ();
+
+        Curses.endwin ();
+    }
+
+    internal override MainLoop Init ()
+    {
+        _mainLoopDriver = new UnixMainLoop (this);
+
+        if (!RunningUnitTests)
+        {
+            _window = Curses.initscr ();
+            Curses.set_escdelay (10);
+
+            // Ensures that all procedures are performed at some previous closing.
+            Curses.doupdate ();
+
+            // 
+            // We are setting Invisible as default so we could ignore XTerm DECSUSR setting
+            //
+            switch (Curses.curs_set (0))
+            {
+                case 0:
+                    _currentCursorVisibility = _initialCursorVisibility = CursorVisibility.Invisible;
+
+                    break;
+
+                case 1:
+                    _currentCursorVisibility = _initialCursorVisibility = CursorVisibility.Underline;
+                    Curses.curs_set (1);
+
+                    break;
+
+                case 2:
+                    _currentCursorVisibility = _initialCursorVisibility = CursorVisibility.Box;
+                    Curses.curs_set (2);
+
+                    break;
+
+                default:
+                    _currentCursorVisibility = _initialCursorVisibility = null;
+
+                    break;
+            }
+
+            if (!Curses.HasColors)
+            {
+                throw new InvalidOperationException ("V2 - This should never happen. File an Issue if it does.");
+            }
+
+            Curses.raw ();
+            Curses.noecho ();
+
+            Curses.Window.Standard.keypad (true);
+
+            Curses.StartColor ();
+            Curses.UseDefaultColors ();
+
+            if (!RunningUnitTests)
+            {
+                Curses.timeout (0);
+            }
+
+            _processInputToken = _mainLoopDriver?.AddWatch (
+                                                            0,
+                                                            UnixMainLoop.Condition.PollIn,
+                                                            x =>
+                                                            {
+                                                                ProcessInput ();
+
+                                                                return true;
+                                                            }
+                                                           );
+        }
+
+        CurrentAttribute = new Attribute (ColorName.White, ColorName.Black);
+
+        if (Environment.OSVersion.Platform == PlatformID.Win32NT)
+        {
+            Clipboard = new FakeDriver.FakeClipboard ();
+        }
+        else
+        {
+            if (RuntimeInformation.IsOSPlatform (OSPlatform.OSX))
+            {
+                Clipboard = new MacOSXClipboard ();
+            }
+            else
+            {
+                if (Is_WSL_Platform ())
+                {
+                    Clipboard = new WSLClipboard ();
+                }
+                else
+                {
+                    Clipboard = new CursesClipboard ();
+                }
+            }
+        }
+
+        ClearContents ();
+        StartReportingMouseMoves ();
+
+        if (!RunningUnitTests)
+        {
+            Curses.CheckWinChange ();
+            Curses.refresh ();
+        }
+
+        return new MainLoop (_mainLoopDriver);
+    }
+
+    internal void ProcessInput ()
+    {
+        int wch;
+        int code = Curses.get_wch (out wch);
+
+        //System.Diagnostics.Debug.WriteLine ($"code: {code}; wch: {wch}");
+        if (code == Curses.ERR)
+        {
+            return;
+        }
+
+        var k = KeyCode.Null;
+
+        if (code == Curses.KEY_CODE_YES)
+        {
+            while (code == Curses.KEY_CODE_YES && wch == Curses.KeyResize)
+            {
+                ProcessWinChange ();
+                code = Curses.get_wch (out wch);
+            }
+
+            if (wch == 0)
+            {
+                return;
+            }
+
+            if (wch == Curses.KeyMouse)
+            {
+                int wch2 = wch;
+
+                while (wch2 == Curses.KeyMouse)
+                {
+                    Key kea = null;
+
+                    ConsoleKeyInfo [] cki =
+                    {
+                        new ((char)KeyCode.Esc, 0, false, false, false),
+                        new ('[', 0, false, false, false),
+                        new ('<', 0, false, false, false)
+                    };
+                    code = 0;
+                    HandleEscSeqResponse (ref code, ref k, ref wch2, ref kea, ref cki);
+                }
+
+                return;
+            }
+
+            k = MapCursesKey (wch);
+
+            if (wch >= 277 && wch <= 288)
+            {
+                // Shift+(F1 - F12)
+                wch -= 12;
+                k = KeyCode.ShiftMask | MapCursesKey (wch);
+            }
+            else if (wch >= 289 && wch <= 300)
+            {
+                // Ctrl+(F1 - F12)
+                wch -= 24;
+                k = KeyCode.CtrlMask | MapCursesKey (wch);
+            }
+            else if (wch >= 301 && wch <= 312)
+            {
+                // Ctrl+Shift+(F1 - F12)
+                wch -= 36;
+                k = KeyCode.CtrlMask | KeyCode.ShiftMask | MapCursesKey (wch);
+            }
+            else if (wch >= 313 && wch <= 324)
+            {
+                // Alt+(F1 - F12)
+                wch -= 48;
+                k = KeyCode.AltMask | MapCursesKey (wch);
+            }
+            else if (wch >= 325 && wch <= 327)
+            {
+                // Shift+Alt+(F1 - F3)
+                wch -= 60;
+                k = KeyCode.ShiftMask | KeyCode.AltMask | MapCursesKey (wch);
+            }
+
+            OnKeyDown (new Key (k));
+            OnKeyUp (new Key (k));
+
+            return;
+        }
+
+        // Special handling for ESC, we want to try to catch ESC+letter to simulate alt-letter as well as Alt-Fkey
+        if (wch == 27)
+        {
+            Curses.timeout (10);
+
+            code = Curses.get_wch (out int wch2);
+
+            if (code == Curses.KEY_CODE_YES)
+            {
+                k = KeyCode.AltMask | MapCursesKey (wch);
+            }
+
+            Key key = null;
+
+            if (code == 0)
+            {
+                // The ESC-number handling, debatable.
+                // Simulates the AltMask itself by pressing Alt + Space.
+                if (wch2 == (int)KeyCode.Space)
+                {
+                    k = KeyCode.AltMask;
+                }
+                else if (wch2 - (int)KeyCode.Space >= (uint)KeyCode.A
+                         && wch2 - (int)KeyCode.Space <= (uint)KeyCode.Z)
+                {
+                    k = (KeyCode)((uint)KeyCode.AltMask + (wch2 - (int)KeyCode.Space));
+                }
+                else if (wch2 >= (uint)KeyCode.A - 64 && wch2 <= (uint)KeyCode.Z - 64)
+                {
+                    k = (KeyCode)((uint)(KeyCode.AltMask | KeyCode.CtrlMask) + (wch2 + 64));
+                }
+                else if (wch2 >= (uint)KeyCode.D0 && wch2 <= (uint)KeyCode.D9)
+                {
+                    k = (KeyCode)((uint)KeyCode.AltMask + (uint)KeyCode.D0 + (wch2 - (uint)KeyCode.D0));
+                }
+                else if (wch2 == Curses.KeyCSI)
+                {
+                    ConsoleKeyInfo [] cki =
+                    {
+                        new ((char)KeyCode.Esc, 0, false, false, false), new ('[', 0, false, false, false)
+                    };
+                    HandleEscSeqResponse (ref code, ref k, ref wch2, ref key, ref cki);
+
+                    return;
+                }
+                else
+                {
+                    // Unfortunately there are no way to differentiate Ctrl+Alt+alfa and Ctrl+Shift+Alt+alfa.
+                    if (((KeyCode)wch2 & KeyCode.CtrlMask) != 0)
+                    {
+                        k = (KeyCode)((uint)KeyCode.CtrlMask + (wch2 & ~(int)KeyCode.CtrlMask));
+                    }
+
+                    if (wch2 == 0)
+                    {
+                        k = KeyCode.CtrlMask | KeyCode.AltMask | KeyCode.Space;
+                    }
+                    else if (wch >= (uint)KeyCode.A && wch <= (uint)KeyCode.Z)
+                    {
+                        k = KeyCode.ShiftMask | KeyCode.AltMask | KeyCode.Space;
+                    }
+                    else if (wch2 < 256)
+                    {
+                        k = (KeyCode)wch2; // | KeyCode.AltMask;
+                    }
+                    else
+                    {
+                        k = (KeyCode)((uint)(KeyCode.AltMask | KeyCode.CtrlMask) + wch2);
+                    }
+                }
+
+                key = new Key (k);
+            }
+            else
+            {
+                key = Key.Esc;
+            }
+
+            OnKeyDown (key);
+            OnKeyUp (key);
+        }
+        else if (wch == Curses.KeyTab)
+        {
+            k = MapCursesKey (wch);
+            OnKeyDown (new Key (k));
+            OnKeyUp (new Key (k));
+        }
+        else
+        {
+            // Unfortunately there are no way to differentiate Ctrl+alfa and Ctrl+Shift+alfa.
+            k = (KeyCode)wch;
+
+            if (wch == 0)
+            {
+                k = KeyCode.CtrlMask | KeyCode.Space;
+            }
+            else if (wch >= (uint)KeyCode.A - 64 && wch <= (uint)KeyCode.Z - 64)
+            {
+                if ((KeyCode)(wch + 64) != KeyCode.J)
+                {
+                    k = KeyCode.CtrlMask | (KeyCode)(wch + 64);
+                }
+            }
+            else if (wch >= (uint)KeyCode.A && wch <= (uint)KeyCode.Z)
+            {
+                k = (KeyCode)wch | KeyCode.ShiftMask;
+            }
+
+            if (wch == '\n' || wch == '\r')
+            {
+                k = KeyCode.Enter;
+            }
+
+            OnKeyDown (new Key (k));
+            OnKeyUp (new Key (k));
+        }
+    }
+
+    internal void ProcessWinChange ()
+    {
+        if (!RunningUnitTests && Curses.CheckWinChange ())
+        {
+            ClearContents ();
+            OnSizeChanged (new SizeChangedEventArgs (new (Cols, Rows)));
+        }
+    }
+
+    private void HandleEscSeqResponse (
+        ref int code,
+        ref KeyCode k,
+        ref int wch2,
+        ref Key keyEventArgs,
+        ref ConsoleKeyInfo [] cki
+    )
+    {
+        ConsoleKey ck = 0;
+        ConsoleModifiers mod = 0;
+
+        while (code == 0)
+        {
+            code = Curses.get_wch (out wch2);
+            var consoleKeyInfo = new ConsoleKeyInfo ((char)wch2, 0, false, false, false);
+
+            if (wch2 == 0 || wch2 == 27 || wch2 == Curses.KeyMouse)
+            {
+                EscSeqUtils.DecodeEscSeq (
+                                          null,
+                                          ref consoleKeyInfo,
+                                          ref ck,
+                                          cki,
+                                          ref mod,
+                                          out _,
+                                          out _,
+                                          out _,
+                                          out _,
+                                          out bool isKeyMouse,
+                                          out List<MouseFlags> mouseFlags,
+                                          out Point pos,
+                                          out _,
+                                          ProcessMouseEvent
+                                         );
+
+                if (isKeyMouse)
+                {
+                    foreach (MouseFlags mf in mouseFlags)
+                    {
+                        ProcessMouseEvent (mf, pos);
+                    }
+
+                    cki = null;
+
+                    if (wch2 == 27)
+                    {
+                        cki = EscSeqUtils.ResizeArray (
+                                                       new ConsoleKeyInfo (
+                                                                           (char)KeyCode.Esc,
+                                                                           0,
+                                                                           false,
+                                                                           false,
+                                                                           false
+                                                                          ),
+                                                       cki
+                                                      );
+                    }
+                }
+                else
+                {
+                    k = ConsoleKeyMapping.MapConsoleKeyInfoToKeyCode (consoleKeyInfo);
+                    keyEventArgs = new Key (k);
+                    OnKeyDown (keyEventArgs);
+                }
+            }
+            else
+            {
+                cki = EscSeqUtils.ResizeArray (consoleKeyInfo, cki);
+            }
+        }
+    }
+
+    private static KeyCode MapCursesKey (int cursesKey)
+    {
+        switch (cursesKey)
+        {
+            case Curses.KeyF1: return KeyCode.F1;
+            case Curses.KeyF2: return KeyCode.F2;
+            case Curses.KeyF3: return KeyCode.F3;
+            case Curses.KeyF4: return KeyCode.F4;
+            case Curses.KeyF5: return KeyCode.F5;
+            case Curses.KeyF6: return KeyCode.F6;
+            case Curses.KeyF7: return KeyCode.F7;
+            case Curses.KeyF8: return KeyCode.F8;
+            case Curses.KeyF9: return KeyCode.F9;
+            case Curses.KeyF10: return KeyCode.F10;
+            case Curses.KeyF11: return KeyCode.F11;
+            case Curses.KeyF12: return KeyCode.F12;
+            case Curses.KeyUp: return KeyCode.CursorUp;
+            case Curses.KeyDown: return KeyCode.CursorDown;
+            case Curses.KeyLeft: return KeyCode.CursorLeft;
+            case Curses.KeyRight: return KeyCode.CursorRight;
+            case Curses.KeyHome: return KeyCode.Home;
+            case Curses.KeyEnd: return KeyCode.End;
+            case Curses.KeyNPage: return KeyCode.PageDown;
+            case Curses.KeyPPage: return KeyCode.PageUp;
+            case Curses.KeyDeleteChar: return KeyCode.Delete;
+            case Curses.KeyInsertChar: return KeyCode.Insert;
+            case Curses.KeyTab: return KeyCode.Tab;
+            case Curses.KeyBackTab: return KeyCode.Tab | KeyCode.ShiftMask;
+            case Curses.KeyBackspace: return KeyCode.Backspace;
+            case Curses.ShiftKeyUp: return KeyCode.CursorUp | KeyCode.ShiftMask;
+            case Curses.ShiftKeyDown: return KeyCode.CursorDown | KeyCode.ShiftMask;
+            case Curses.ShiftKeyLeft: return KeyCode.CursorLeft | KeyCode.ShiftMask;
+            case Curses.ShiftKeyRight: return KeyCode.CursorRight | KeyCode.ShiftMask;
+            case Curses.ShiftKeyHome: return KeyCode.Home | KeyCode.ShiftMask;
+            case Curses.ShiftKeyEnd: return KeyCode.End | KeyCode.ShiftMask;
+            case Curses.ShiftKeyNPage: return KeyCode.PageDown | KeyCode.ShiftMask;
+            case Curses.ShiftKeyPPage: return KeyCode.PageUp | KeyCode.ShiftMask;
+            case Curses.AltKeyUp: return KeyCode.CursorUp | KeyCode.AltMask;
+            case Curses.AltKeyDown: return KeyCode.CursorDown | KeyCode.AltMask;
+            case Curses.AltKeyLeft: return KeyCode.CursorLeft | KeyCode.AltMask;
+            case Curses.AltKeyRight: return KeyCode.CursorRight | KeyCode.AltMask;
+            case Curses.AltKeyHome: return KeyCode.Home | KeyCode.AltMask;
+            case Curses.AltKeyEnd: return KeyCode.End | KeyCode.AltMask;
+            case Curses.AltKeyNPage: return KeyCode.PageDown | KeyCode.AltMask;
+            case Curses.AltKeyPPage: return KeyCode.PageUp | KeyCode.AltMask;
+            case Curses.CtrlKeyUp: return KeyCode.CursorUp | KeyCode.CtrlMask;
+            case Curses.CtrlKeyDown: return KeyCode.CursorDown | KeyCode.CtrlMask;
+            case Curses.CtrlKeyLeft: return KeyCode.CursorLeft | KeyCode.CtrlMask;
+            case Curses.CtrlKeyRight: return KeyCode.CursorRight | KeyCode.CtrlMask;
+            case Curses.CtrlKeyHome: return KeyCode.Home | KeyCode.CtrlMask;
+            case Curses.CtrlKeyEnd: return KeyCode.End | KeyCode.CtrlMask;
+            case Curses.CtrlKeyNPage: return KeyCode.PageDown | KeyCode.CtrlMask;
+            case Curses.CtrlKeyPPage: return KeyCode.PageUp | KeyCode.CtrlMask;
+            case Curses.ShiftCtrlKeyUp: return KeyCode.CursorUp | KeyCode.ShiftMask | KeyCode.CtrlMask;
+            case Curses.ShiftCtrlKeyDown: return KeyCode.CursorDown | KeyCode.ShiftMask | KeyCode.CtrlMask;
+            case Curses.ShiftCtrlKeyLeft: return KeyCode.CursorLeft | KeyCode.ShiftMask | KeyCode.CtrlMask;
+            case Curses.ShiftCtrlKeyRight: return KeyCode.CursorRight | KeyCode.ShiftMask | KeyCode.CtrlMask;
+            case Curses.ShiftCtrlKeyHome: return KeyCode.Home | KeyCode.ShiftMask | KeyCode.CtrlMask;
+            case Curses.ShiftCtrlKeyEnd: return KeyCode.End | KeyCode.ShiftMask | KeyCode.CtrlMask;
+            case Curses.ShiftCtrlKeyNPage: return KeyCode.PageDown | KeyCode.ShiftMask | KeyCode.CtrlMask;
+            case Curses.ShiftCtrlKeyPPage: return KeyCode.PageUp | KeyCode.ShiftMask | KeyCode.CtrlMask;
+            case Curses.ShiftAltKeyUp: return KeyCode.CursorUp | KeyCode.ShiftMask | KeyCode.AltMask;
+            case Curses.ShiftAltKeyDown: return KeyCode.CursorDown | KeyCode.ShiftMask | KeyCode.AltMask;
+            case Curses.ShiftAltKeyLeft: return KeyCode.CursorLeft | KeyCode.ShiftMask | KeyCode.AltMask;
+            case Curses.ShiftAltKeyRight: return KeyCode.CursorRight | KeyCode.ShiftMask | KeyCode.AltMask;
+            case Curses.ShiftAltKeyNPage: return KeyCode.PageDown | KeyCode.ShiftMask | KeyCode.AltMask;
+            case Curses.ShiftAltKeyPPage: return KeyCode.PageUp | KeyCode.ShiftMask | KeyCode.AltMask;
+            case Curses.ShiftAltKeyHome: return KeyCode.Home | KeyCode.ShiftMask | KeyCode.AltMask;
+            case Curses.ShiftAltKeyEnd: return KeyCode.End | KeyCode.ShiftMask | KeyCode.AltMask;
+            case Curses.AltCtrlKeyNPage: return KeyCode.PageDown | KeyCode.AltMask | KeyCode.CtrlMask;
+            case Curses.AltCtrlKeyPPage: return KeyCode.PageUp | KeyCode.AltMask | KeyCode.CtrlMask;
+            case Curses.AltCtrlKeyHome: return KeyCode.Home | KeyCode.AltMask | KeyCode.CtrlMask;
+            case Curses.AltCtrlKeyEnd: return KeyCode.End | KeyCode.AltMask | KeyCode.CtrlMask;
+            default: return KeyCode.Null;
+        }
+    }
+
+    private void ProcessMouseEvent (MouseFlags mouseFlag, Point pos)
+    {
+        bool WasButtonReleased (MouseFlags flag)
+        {
+            return flag.HasFlag (MouseFlags.Button1Released)
+                   || flag.HasFlag (MouseFlags.Button2Released)
+                   || flag.HasFlag (MouseFlags.Button3Released)
+                   || flag.HasFlag (MouseFlags.Button4Released);
+        }
+
+        bool IsButtonNotPressed (MouseFlags flag)
+        {
+            return !flag.HasFlag (MouseFlags.Button1Pressed)
+                   && !flag.HasFlag (MouseFlags.Button2Pressed)
+                   && !flag.HasFlag (MouseFlags.Button3Pressed)
+                   && !flag.HasFlag (MouseFlags.Button4Pressed);
+        }
+
+        bool IsButtonClickedOrDoubleClicked (MouseFlags flag)
+        {
+            return flag.HasFlag (MouseFlags.Button1Clicked)
+                   || flag.HasFlag (MouseFlags.Button2Clicked)
+                   || flag.HasFlag (MouseFlags.Button3Clicked)
+                   || flag.HasFlag (MouseFlags.Button4Clicked)
+                   || flag.HasFlag (MouseFlags.Button1DoubleClicked)
+                   || flag.HasFlag (MouseFlags.Button2DoubleClicked)
+                   || flag.HasFlag (MouseFlags.Button3DoubleClicked)
+                   || flag.HasFlag (MouseFlags.Button4DoubleClicked);
+        }
+
+        if ((WasButtonReleased (mouseFlag) && IsButtonNotPressed (_lastMouseFlags)) || (IsButtonClickedOrDoubleClicked (mouseFlag) && _lastMouseFlags == 0))
+        {
+            return;
+        }
+
+        _lastMouseFlags = mouseFlag;
+
+        var me = new MouseEvent { Flags = mouseFlag, X = pos.X, Y = pos.Y };
+        OnMouseEvent (new MouseEventEventArgs (me));
+    }
+
+    #region Color Handling
+
+    /// <summary>Creates an Attribute from the provided curses-based foreground and background color numbers</summary>
+    /// <param name="foreground">Contains the curses color number for the foreground (color, plus any attributes)</param>
+    /// <param name="background">Contains the curses color number for the background (color, plus any attributes)</param>
+    /// <returns></returns>
+    private static Attribute MakeColor (short foreground, short background)
+    {
+        var v = (short)(foreground | (background << 4));
+
+        // TODO: for TrueColor - Use InitExtendedPair
+        Curses.InitColorPair (v, foreground, background);
+
+        return new Attribute (
+                              Curses.ColorPair (v),
+                              CursesColorNumberToColorName (foreground),
+                              CursesColorNumberToColorName (background)
+                             );
+    }
+
+    /// <inheritdoc/>
+    /// <remarks>
+    ///     In the CursesDriver, colors are encoded as an int. The foreground color is stored in the most significant 4
+    ///     bits, and the background color is stored in the least significant 4 bits. The Terminal.GUi Color values are
+    ///     converted to curses color encoding before being encoded.
+    /// </remarks>
+    public override Attribute MakeColor (in Color foreground, in Color background)
+    {
+        if (!RunningUnitTests)
+        {
+            return MakeColor (
+                              ColorNameToCursesColorNumber (foreground.GetClosestNamedColor ()),
+                              ColorNameToCursesColorNumber (background.GetClosestNamedColor ())
+                             );
+        }
+
+        return new Attribute (
+                              0,
+                              foreground,
+                              background
+                             );
+    }
+
+    private static short ColorNameToCursesColorNumber (ColorName color)
+    {
+        switch (color)
+        {
+            case ColorName.Black:
+                return Curses.COLOR_BLACK;
+            case ColorName.Blue:
+                return Curses.COLOR_BLUE;
+            case ColorName.Green:
+                return Curses.COLOR_GREEN;
+            case ColorName.Cyan:
+                return Curses.COLOR_CYAN;
+            case ColorName.Red:
+                return Curses.COLOR_RED;
+            case ColorName.Magenta:
+                return Curses.COLOR_MAGENTA;
+            case ColorName.Yellow:
+                return Curses.COLOR_YELLOW;
+            case ColorName.Gray:
+                return Curses.COLOR_WHITE;
+            case ColorName.DarkGray:
+                return Curses.COLOR_GRAY;
+            case ColorName.BrightBlue:
+                return Curses.COLOR_BLUE | Curses.COLOR_GRAY;
+            case ColorName.BrightGreen:
+                return Curses.COLOR_GREEN | Curses.COLOR_GRAY;
+            case ColorName.BrightCyan:
+                return Curses.COLOR_CYAN | Curses.COLOR_GRAY;
+            case ColorName.BrightRed:
+                return Curses.COLOR_RED | Curses.COLOR_GRAY;
+            case ColorName.BrightMagenta:
+                return Curses.COLOR_MAGENTA | Curses.COLOR_GRAY;
+            case ColorName.BrightYellow:
+                return Curses.COLOR_YELLOW | Curses.COLOR_GRAY;
+            case ColorName.White:
+                return Curses.COLOR_WHITE | Curses.COLOR_GRAY;
+        }
+
+        throw new ArgumentException ("Invalid color code");
+    }
+
+    private static ColorName CursesColorNumberToColorName (short color)
+    {
+        switch (color)
+        {
+            case Curses.COLOR_BLACK:
+                return ColorName.Black;
+            case Curses.COLOR_BLUE:
+                return ColorName.Blue;
+            case Curses.COLOR_GREEN:
+                return ColorName.Green;
+            case Curses.COLOR_CYAN:
+                return ColorName.Cyan;
+            case Curses.COLOR_RED:
+                return ColorName.Red;
+            case Curses.COLOR_MAGENTA:
+                return ColorName.Magenta;
+            case Curses.COLOR_YELLOW:
+                return ColorName.Yellow;
+            case Curses.COLOR_WHITE:
+                return ColorName.Gray;
+            case Curses.COLOR_GRAY:
+                return ColorName.DarkGray;
+            case Curses.COLOR_BLUE | Curses.COLOR_GRAY:
+                return ColorName.BrightBlue;
+            case Curses.COLOR_GREEN | Curses.COLOR_GRAY:
+                return ColorName.BrightGreen;
+            case Curses.COLOR_CYAN | Curses.COLOR_GRAY:
+                return ColorName.BrightCyan;
+            case Curses.COLOR_RED | Curses.COLOR_GRAY:
+                return ColorName.BrightRed;
+            case Curses.COLOR_MAGENTA | Curses.COLOR_GRAY:
+                return ColorName.BrightMagenta;
+            case Curses.COLOR_YELLOW | Curses.COLOR_GRAY:
+                return ColorName.BrightYellow;
+            case Curses.COLOR_WHITE | Curses.COLOR_GRAY:
+                return ColorName.White;
+        }
+
+        throw new ArgumentException ("Invalid curses color code");
+    }
+
+    #endregion
 }
 
-static class Platform {
-	[DllImport ("libc")]
-	extern static int uname (IntPtr buf);
-
-	[DllImport ("libc")]
-	extern static int killpg (int pgrp, int pid);
-
-	static int _suspendSignal;
-
-	static int GetSuspendSignal ()
-	{
-		if (_suspendSignal != 0) {
-			return _suspendSignal;
-		}
-
-		IntPtr buf = Marshal.AllocHGlobal (8192);
-		if (uname (buf) != 0) {
-			Marshal.FreeHGlobal (buf);
-			_suspendSignal = -1;
-			return _suspendSignal;
-		}
-		try {
-			switch (Marshal.PtrToStringAnsi (buf)) {
-			case "Darwin":
-			case "DragonFly":
-			case "FreeBSD":
-			case "NetBSD":
-			case "OpenBSD":
-				_suspendSignal = 18;
-				break;
-			case "Linux":
-				// TODO: should fetch the machine name and
-				// if it is MIPS return 24
-				_suspendSignal = 20;
-				break;
-			case "Solaris":
-				_suspendSignal = 24;
-				break;
-			default:
-				_suspendSignal = -1;
-				break;
-			}
-			return _suspendSignal;
-		} finally {
-			Marshal.FreeHGlobal (buf);
-		}
-	}
-
-	/// <summary>
-	/// Suspends the process by sending SIGTSTP to itself
-	/// </summary>
-	/// <returns>The suspend.</returns>
-	public static bool Suspend ()
-	{
-		int signal = GetSuspendSignal ();
-		if (signal == -1) {
-			return false;
-		}
-		killpg (0, signal);
-		return true;
-	}
-}
+internal static class Platform
+{
+    private static int _suspendSignal;
+
+    /// <summary>Suspends the process by sending SIGTSTP to itself</summary>
+    /// <returns>The suspend.</returns>
+    public static bool Suspend ()
+    {
+        int signal = GetSuspendSignal ();
+
+        if (signal == -1)
+        {
+            return false;
+        }
+
+        killpg (0, signal);
+
+        return true;
+    }
+
+    private static int GetSuspendSignal ()
+    {
+        if (_suspendSignal != 0)
+        {
+            return _suspendSignal;
+        }
+
+        nint buf = Marshal.AllocHGlobal (8192);
+
+        if (uname (buf) != 0)
+        {
+            Marshal.FreeHGlobal (buf);
+            _suspendSignal = -1;
+
+            return _suspendSignal;
+        }
+
+        try
+        {
+            switch (Marshal.PtrToStringAnsi (buf))
+            {
+                case "Darwin":
+                case "DragonFly":
+                case "FreeBSD":
+                case "NetBSD":
+                case "OpenBSD":
+                    _suspendSignal = 18;
+
+                    break;
+                case "Linux":
+                    // TODO: should fetch the machine name and
+                    // if it is MIPS return 24
+                    _suspendSignal = 20;
+
+                    break;
+                case "Solaris":
+                    _suspendSignal = 24;
+
+                    break;
+                default:
+                    _suspendSignal = -1;
+
+                    break;
+            }
+
+            return _suspendSignal;
+        }
+        finally
+        {
+            Marshal.FreeHGlobal (buf);
+        }
+    }
+
+    [DllImport ("libc")]
+    private static extern int killpg (int pgrp, int pid);
+
+    [DllImport ("libc")]
+    private static extern int uname (nint buf);
+}

+ 230 - 214
Terminal.Gui/ConsoleDrivers/CursesDriver/UnixMainLoop.cs

@@ -1,219 +1,235 @@
 //
 // mainloop.cs: Linux/Curses MainLoop implementation.
 //
-using System;
-using System.Collections.Generic;
+
 using System.Runtime.InteropServices;
-using static Terminal.Gui.NetEvents;
-
-namespace Terminal.Gui {
-	/// <summary>
-	/// Unix main loop, suitable for using on Posix systems
-	/// </summary>
-	/// <remarks>
-	/// In addition to the general functions of the MainLoop, the Unix version
-	/// can watch file descriptors using the AddWatch methods.
-	/// </remarks>
-	internal class UnixMainLoop : IMainLoopDriver {
-		private CursesDriver _cursesDriver;
-		public UnixMainLoop (ConsoleDriver consoleDriver = null)
-		{
-			// UnixDriver doesn't use the consoleDriver parameter, but the WindowsDriver does.
-			_cursesDriver = (CursesDriver)Application.Driver;
-		}
-
-		public const int KEY_RESIZE = unchecked((int)0xffffffffffffffff);
-
-		[StructLayout (LayoutKind.Sequential)]
-		struct Pollfd {
-			public int fd;
-			public short events, revents;
-		}
-
-		/// <summary>
-		///	Condition on which to wake up from file descriptor activity.  These match the Linux/BSD poll definitions.
-		/// </summary>
-		[Flags]
-		public enum Condition : short {
-			/// <summary>
-			/// There is data to read
-			/// </summary>
-			PollIn = 1,
-			/// <summary>
-			/// Writing to the specified descriptor will not block
-			/// </summary>
-			PollOut = 4,
-			/// <summary>
-			/// There is urgent data to read
-			/// </summary>
-			PollPri = 2,
-			/// <summary>
-			///  Error condition on output
-			/// </summary>
-			PollErr = 8,
-			/// <summary>
-			/// Hang-up on output
-			/// </summary>
-			PollHup = 16,
-			/// <summary>
-			/// File descriptor is not open.
-			/// </summary>
-			PollNval = 32
-		}
-
-		class Watch {
-			public int File;
-			public Condition Condition;
-			public Func<MainLoop, bool> Callback;
-		}
-
-		readonly Dictionary<int, Watch> _descriptorWatchers = new Dictionary<int, Watch> ();
-
-		[DllImport ("libc")]
-		extern static int poll ([In, Out] Pollfd [] ufds, uint nfds, int timeout);
-
-		[DllImport ("libc")]
-		extern static int pipe ([In, Out] int [] pipes);
-
-		[DllImport ("libc")]
-		extern static int read (int fd, IntPtr buf, IntPtr n);
-
-		[DllImport ("libc")]
-		extern static int write (int fd, IntPtr buf, IntPtr n);
-
-		Pollfd [] _pollMap;
-		bool _pollDirty = true;
-		readonly int [] _wakeUpPipes = new int [2];
-		static readonly IntPtr _ignore = Marshal.AllocHGlobal (1);
-		MainLoop _mainLoop;
-		bool _winChanged;
-
-		void IMainLoopDriver.Wakeup ()
-		{
-			if (!ConsoleDriver.RunningUnitTests) {
-				write (_wakeUpPipes [1], _ignore, (IntPtr)1);
-			}
-		}
-
-		void IMainLoopDriver.Setup (MainLoop mainLoop)
-		{
-			this._mainLoop = mainLoop;
-			if (ConsoleDriver.RunningUnitTests) {
-				return;
-			}
-
-			try {
-				pipe (_wakeUpPipes);
-				AddWatch (_wakeUpPipes [0], Condition.PollIn, ml => {
-					read (_wakeUpPipes [0], _ignore, (IntPtr)1);
-					return true;
-				});
-			} catch (DllNotFoundException e) {
-				throw new NotSupportedException ("libc not found", e);
-			}
-		}
-
-		/// <summary>
-		///	Removes an active watch from the mainloop.
-		/// </summary>
-		/// <remarks>
-		///	The token parameter is the value returned from AddWatch
-		/// </remarks>
-		internal void RemoveWatch (object token)
-		{
-			if (!ConsoleDriver.RunningUnitTests) {
-				if (token is not Watch watch) {
-					return;
-				}
-				_descriptorWatchers.Remove (watch.File);
-			}
-		}
-
-		/// <summary>
-		///  Watches a file descriptor for activity.
-		/// </summary>
-		/// <remarks>
-		///  When the condition is met, the provided callback
-		///  is invoked.  If the callback returns false, the
-		///  watch is automatically removed.
-		///
-		///  The return value is a token that represents this watch, you can
-		///  use this token to remove the watch by calling RemoveWatch.
-		/// </remarks>
-		internal object AddWatch (int fileDescriptor, Condition condition, Func<MainLoop, bool> callback)
-		{
-			if (callback == null) {
-				throw new ArgumentNullException (nameof (callback));
-			}
-
-			var watch = new Watch () { Condition = condition, Callback = callback, File = fileDescriptor };
-			_descriptorWatchers [fileDescriptor] = watch;
-			_pollDirty = true;
-			return watch;
-		}
-
-		void UpdatePollMap ()
-		{
-			if (!_pollDirty) {
-				return;
-			}
-			_pollDirty = false;
-
-			_pollMap = new Pollfd [_descriptorWatchers.Count];
-			var i = 0;
-			foreach (var fd in _descriptorWatchers.Keys) {
-				_pollMap [i].fd = fd;
-				_pollMap [i].events = (short)_descriptorWatchers [fd].Condition;
-				i++;
-			}
-		}
-
-		bool IMainLoopDriver.EventsPending ()
-		{
-			UpdatePollMap ();
-
-			var checkTimersResult = _mainLoop.CheckTimersAndIdleHandlers (out var pollTimeout);
-
-			var n = poll (_pollMap, (uint)_pollMap.Length, pollTimeout);
-
-			if (n == KEY_RESIZE) {
-				_winChanged = true;
-			}
-
-			return checkTimersResult || n >= KEY_RESIZE;
-		}
-
-		void IMainLoopDriver.Iteration ()
-		{
-			if (_winChanged) {
-				_winChanged = false;
-				_cursesDriver.ProcessInput ();
-				// This is needed on the mac. See https://github.com/gui-cs/Terminal.Gui/pull/2922#discussion_r1365992426
-				_cursesDriver.ProcessWinChange ();
-			}
-			if (_pollMap == null) return;
-			foreach (var p in _pollMap) {
-				Watch watch;
-
-				if (p.revents == 0) {
-					continue;
-				}
-
-				if (!_descriptorWatchers.TryGetValue (p.fd, out watch)) {
-					continue;
-				}
-
-				if (!watch.Callback (this._mainLoop)) {
-					_descriptorWatchers.Remove (p.fd);
-				}
-			}
-		}
-
-		void IMainLoopDriver.TearDown ()
-		{
-			_descriptorWatchers?.Clear ();
-
-			_mainLoop = null;
-		}
-	}
+
+namespace Terminal.Gui;
+
+/// <summary>Unix main loop, suitable for using on Posix systems</summary>
+/// <remarks>
+///     In addition to the general functions of the MainLoop, the Unix version can watch file descriptors using the
+///     AddWatch methods.
+/// </remarks>
+internal class UnixMainLoop : IMainLoopDriver
+{
+    /// <summary>Condition on which to wake up from file descriptor activity.  These match the Linux/BSD poll definitions.</summary>
+    [Flags]
+    public enum Condition : short
+    {
+        /// <summary>There is data to read</summary>
+        PollIn = 1,
+
+        /// <summary>Writing to the specified descriptor will not block</summary>
+        PollOut = 4,
+
+        /// <summary>There is urgent data to read</summary>
+        PollPri = 2,
+
+        /// <summary>Error condition on output</summary>
+        PollErr = 8,
+
+        /// <summary>Hang-up on output</summary>
+        PollHup = 16,
+
+        /// <summary>File descriptor is not open.</summary>
+        PollNval = 32
+    }
+
+    public const int KEY_RESIZE = unchecked ((int)0xffffffffffffffff);
+    private static readonly nint _ignore = Marshal.AllocHGlobal (1);
+
+    private readonly CursesDriver _cursesDriver;
+    private readonly Dictionary<int, Watch> _descriptorWatchers = new ();
+    private readonly int [] _wakeUpPipes = new int [2];
+    private MainLoop _mainLoop;
+    private bool _pollDirty = true;
+    private Pollfd [] _pollMap;
+    private bool _winChanged;
+
+    public UnixMainLoop (ConsoleDriver consoleDriver = null)
+    {
+        // UnixDriver doesn't use the consoleDriver parameter, but the WindowsDriver does.
+        _cursesDriver = (CursesDriver)Application.Driver;
+    }
+
+    void IMainLoopDriver.Wakeup ()
+    {
+        if (!ConsoleDriver.RunningUnitTests)
+        {
+            write (_wakeUpPipes [1], _ignore, 1);
+        }
+    }
+
+    void IMainLoopDriver.Setup (MainLoop mainLoop)
+    {
+        _mainLoop = mainLoop;
+
+        if (ConsoleDriver.RunningUnitTests)
+        {
+            return;
+        }
+
+        try
+        {
+            pipe (_wakeUpPipes);
+
+            AddWatch (
+                      _wakeUpPipes [0],
+                      Condition.PollIn,
+                      ml =>
+                      {
+                          read (_wakeUpPipes [0], _ignore, 1);
+
+                          return true;
+                      }
+                     );
+        }
+        catch (DllNotFoundException e)
+        {
+            throw new NotSupportedException ("libc not found", e);
+        }
+    }
+
+    bool IMainLoopDriver.EventsPending ()
+    {
+        UpdatePollMap ();
+
+        bool checkTimersResult = _mainLoop.CheckTimersAndIdleHandlers (out int pollTimeout);
+
+        int n = poll (_pollMap, (uint)_pollMap.Length, pollTimeout);
+
+        if (n == KEY_RESIZE)
+        {
+            _winChanged = true;
+        }
+
+        return checkTimersResult || n >= KEY_RESIZE;
+    }
+
+    void IMainLoopDriver.Iteration ()
+    {
+        if (_winChanged)
+        {
+            _winChanged = false;
+            _cursesDriver.ProcessInput ();
+
+            // This is needed on the mac. See https://github.com/gui-cs/Terminal.Gui/pull/2922#discussion_r1365992426
+            _cursesDriver.ProcessWinChange ();
+        }
+
+        if (_pollMap is null)
+        {
+            return;
+        }
+
+        foreach (Pollfd p in _pollMap)
+        {
+            Watch watch;
+
+            if (p.revents == 0)
+            {
+                continue;
+            }
+
+            if (!_descriptorWatchers.TryGetValue (p.fd, out watch))
+            {
+                continue;
+            }
+
+            if (!watch.Callback (_mainLoop))
+            {
+                _descriptorWatchers.Remove (p.fd);
+            }
+        }
+    }
+
+    void IMainLoopDriver.TearDown ()
+    {
+        _descriptorWatchers?.Clear ();
+
+        _mainLoop = null;
+    }
+
+    /// <summary>Watches a file descriptor for activity.</summary>
+    /// <remarks>
+    ///     When the condition is met, the provided callback is invoked.  If the callback returns false, the watch is
+    ///     automatically removed. The return value is a token that represents this watch, you can use this token to remove the
+    ///     watch by calling RemoveWatch.
+    /// </remarks>
+    internal object AddWatch (int fileDescriptor, Condition condition, Func<MainLoop, bool> callback)
+    {
+        if (callback is null)
+        {
+            throw new ArgumentNullException (nameof (callback));
+        }
+
+        var watch = new Watch { Condition = condition, Callback = callback, File = fileDescriptor };
+        _descriptorWatchers [fileDescriptor] = watch;
+        _pollDirty = true;
+
+        return watch;
+    }
+
+    /// <summary>Removes an active watch from the mainloop.</summary>
+    /// <remarks>The token parameter is the value returned from AddWatch</remarks>
+    internal void RemoveWatch (object token)
+    {
+        if (!ConsoleDriver.RunningUnitTests)
+        {
+            if (token is not Watch watch)
+            {
+                return;
+            }
+
+            _descriptorWatchers.Remove (watch.File);
+        }
+    }
+
+    [DllImport ("libc")]
+    private static extern int pipe ([In] [Out] int [] pipes);
+
+    [DllImport ("libc")]
+    private static extern int poll ([In] [Out] Pollfd [] ufds, uint nfds, int timeout);
+
+    [DllImport ("libc")]
+    private static extern int read (int fd, nint buf, nint n);
+
+    private void UpdatePollMap ()
+    {
+        if (!_pollDirty)
+        {
+            return;
+        }
+
+        _pollDirty = false;
+
+        _pollMap = new Pollfd [_descriptorWatchers.Count];
+        var i = 0;
+
+        foreach (int fd in _descriptorWatchers.Keys)
+        {
+            _pollMap [i].fd = fd;
+            _pollMap [i].events = (short)_descriptorWatchers [fd].Condition;
+            i++;
+        }
+    }
+
+    [DllImport ("libc")]
+    private static extern int write (int fd, nint buf, nint n);
+
+    [StructLayout (LayoutKind.Sequential)]
+    private struct Pollfd
+    {
+        public int fd;
+        public short events;
+        public readonly short revents;
+    }
+
+    private class Watch
+    {
+        public Func<MainLoop, bool> Callback;
+        public Condition Condition;
+        public int File;
+    }
 }

+ 303 - 249
Terminal.Gui/ConsoleDrivers/CursesDriver/UnmanagedLibrary.cs

@@ -1,5 +1,3 @@
-
-
 // Copyright 2015 gRPC authors.
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,265 +11,321 @@
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
 // limitations under the License.
-#define GUICS
 
-using System;
-using System.IO;
-using System.Reflection;
+#define GUICS
 using System.Runtime.InteropServices;
-using System.Threading;
-
-namespace Unix.Terminal {
-	/// <summary>
-	/// Represents a dynamically loaded unmanaged library in a (partially) platform independent manner.
-	/// First, the native library is loaded using dlopen (on Unix systems) or using LoadLibrary (on Windows).
-	/// dlsym or GetProcAddress are then used to obtain symbol addresses. <c>Marshal.GetDelegateForFunctionPointer</c>
-	/// transforms the addresses into delegates to native methods.
-	/// See http://stackoverflow.com/questions/13461989/p-invoke-to-dynamically-loaded-library-on-mono.
-	/// </summary>
-	internal class UnmanagedLibrary {
-		const string UnityEngineApplicationClassName = "UnityEngine.Application, UnityEngine";
-		const string XamarinAndroidObjectClassName = "Java.Lang.Object, Mono.Android";
-		const string XamarinIOSObjectClassName = "Foundation.NSObject, Xamarin.iOS";
-		static bool IsWindows, IsLinux, IsMacOS;
-		static bool Is64Bit;
+
+namespace Unix.Terminal;
+
+/// <summary>
+///     Represents a dynamically loaded unmanaged library in a (partially) platform independent manner. First, the
+///     native library is loaded using dlopen (on Unix systems) or using LoadLibrary (on Windows). dlsym or GetProcAddress
+///     are then used to obtain symbol addresses. <c>Marshal.GetDelegateForFunctionPointer</c> transforms the addresses
+///     into delegates to native methods. See
+///     http://stackoverflow.com/questions/13461989/p-invoke-to-dynamically-loaded-library-on-mono.
+/// </summary>
+internal class UnmanagedLibrary
+{
+    private const string UnityEngineApplicationClassName = "UnityEngine.Application, UnityEngine";
+    private const string XamarinAndroidObjectClassName = "Java.Lang.Object, Mono.Android";
+    private const string XamarinIOSObjectClassName = "Foundation.NSObject, Xamarin.iOS";
+    private static readonly bool IsWindows;
+    private static readonly bool IsLinux;
+    private static readonly bool Is64Bit;
 #if GUICS
-		static bool IsMono;
+    private static readonly bool IsMono;
 #else
 		static bool IsMono, IsUnity, IsXamarinIOS, IsXamarinAndroid, IsXamarin;
 #endif
-		static bool IsNetCore;
-
-		public static bool IsMacOSPlatform => IsMacOS;
-
-		[DllImport ("libc")]
-		static extern int uname (IntPtr buf);
-
-		static string GetUname ()
-		{
-			var buffer = Marshal.AllocHGlobal (8192);
-			try {
-				if (uname (buffer) == 0) {
-					return Marshal.PtrToStringAnsi (buffer);
-				}
-				return string.Empty;
-			} catch {
-				return string.Empty;
-			} finally {
-				if (buffer != IntPtr.Zero) {
-					Marshal.FreeHGlobal (buffer);
-				}
-			}
-		}
-
-		static UnmanagedLibrary ()
-		{
-			var platform = Environment.OSVersion.Platform;
-
-			IsMacOS = (platform == PlatformID.Unix && GetUname () == "Darwin");
-			IsLinux = (platform == PlatformID.Unix && !IsMacOS);
-			IsWindows = (platform == PlatformID.Win32NT || platform == PlatformID.Win32S || platform == PlatformID.Win32Windows);
-			Is64Bit = Marshal.SizeOf (typeof (IntPtr)) == 8;
-			IsMono = Type.GetType ("Mono.Runtime") != null;
-			if (!IsMono) {
-				IsNetCore = Type.GetType ("System.MathF") != null;
-			}
+    private static bool IsNetCore;
+    public static bool IsMacOSPlatform { get; }
+
+    [DllImport ("libc")]
+    private static extern int uname (nint buf);
+
+    private static string GetUname ()
+    {
+        nint buffer = Marshal.AllocHGlobal (8192);
+
+        try
+        {
+            if (uname (buffer) == 0)
+            {
+                return Marshal.PtrToStringAnsi (buffer);
+            }
+
+            return string.Empty;
+        }
+        catch
+        {
+            return string.Empty;
+        }
+        finally
+        {
+            if (buffer != nint.Zero)
+            {
+                Marshal.FreeHGlobal (buffer);
+            }
+        }
+    }
+
+    static UnmanagedLibrary ()
+    {
+        PlatformID platform = Environment.OSVersion.Platform;
+
+        IsMacOSPlatform = platform == PlatformID.Unix && GetUname () == "Darwin";
+        IsLinux = platform == PlatformID.Unix && !IsMacOSPlatform;
+
+        IsWindows = platform == PlatformID.Win32NT
+                    || platform == PlatformID.Win32S
+                    || platform == PlatformID.Win32Windows;
+        Is64Bit = Marshal.SizeOf (typeof (nint)) == 8;
+        IsMono = Type.GetType ("Mono.Runtime") != null;
+
+        if (!IsMono)
+        {
+            IsNetCore = Type.GetType ("System.MathF") != null;
+        }
 #if GUICS
-			//IsUnity = IsXamarinIOS = IsXamarinAndroid = IsXamarin = false;
+
+        //IsUnity = IsXamarinIOS = IsXamarinAndroid = IsXamarin = false;
 #else
 			IsUnity = Type.GetType (UnityEngineApplicationClassName) != null;
 			IsXamarinIOS = Type.GetType (XamarinIOSObjectClassName) != null;
 			IsXamarinAndroid = Type.GetType (XamarinAndroidObjectClassName) != null;
 			IsXamarin = IsXamarinIOS || IsXamarinAndroid;
 #endif
+    }
+
+    // flags for dlopen
+    private const int RTLD_LAZY = 1;
+    private const int RTLD_GLOBAL = 8;
+    public readonly string LibraryPath;
+    public nint NativeLibraryHandle { get; }
+
+    //
+    // if isFullPath is set to true, the provided array of libraries are full paths
+    // and are tested for the file existing, otherwise the file is merely the name
+    // of the shared library that we pass to dlopen
+    //
+    public UnmanagedLibrary (string [] libraryPathAlternatives, bool isFullPath)
+    {
+        if (isFullPath)
+        {
+            LibraryPath = FirstValidLibraryPath (libraryPathAlternatives);
+            NativeLibraryHandle = PlatformSpecificLoadLibrary (LibraryPath);
+        }
+        else
+        {
+            foreach (string lib in libraryPathAlternatives)
+            {
+                NativeLibraryHandle = PlatformSpecificLoadLibrary (lib);
+
+                if (NativeLibraryHandle != nint.Zero)
+                {
+                    LibraryPath = lib;
+
+                    break;
+                }
+            }
+        }
+
+        if (NativeLibraryHandle == nint.Zero)
+        {
+            throw new IOException ($"Error loading native library \"{string.Join (", ", libraryPathAlternatives)}\"");
+        }
+    }
+
+    /// <summary>Loads symbol in a platform specific way.</summary>
+    /// <param name="symbolName"></param>
+    /// <returns></returns>
+    public nint LoadSymbol (string symbolName)
+    {
+        if (IsWindows)
+        {
+            // See http://stackoverflow.com/questions/10473310 for background on this.
+            if (Is64Bit)
+            {
+                return Windows.GetProcAddress (NativeLibraryHandle, symbolName);
+            }
+
+            // Yes, we could potentially predict the size... but it's a lot simpler to just try
+            // all the candidates. Most functions have a suffix of @0, @4 or @8 so we won't be trying
+            // many options - and if it takes a little bit longer to fail if we've really got the wrong
+            // library, that's not a big problem. This is only called once per function in the native library.
+            symbolName = "_" + symbolName + "@";
+
+            for (var stackSize = 0; stackSize < 128; stackSize += 4)
+            {
+                nint candidate = Windows.GetProcAddress (NativeLibraryHandle, symbolName + stackSize);
+
+                if (candidate != nint.Zero)
+                {
+                    return candidate;
+                }
+            }
+
+            // Fail.
+            return nint.Zero;
+        }
+
+        if (IsLinux)
+        {
+            if (IsMono)
+            {
+                return Mono.dlsym (NativeLibraryHandle, symbolName);
+            }
+
+            if (IsNetCore)
+            {
+                return CoreCLR.dlsym (NativeLibraryHandle, symbolName);
+            }
+
+            return Linux.dlsym (NativeLibraryHandle, symbolName);
+        }
+
+        if (IsMacOSPlatform)
+        {
+            return MacOSX.dlsym (NativeLibraryHandle, symbolName);
+        }
+
+        throw new InvalidOperationException ("Unsupported platform.");
+    }
+
+    public T GetNativeMethodDelegate<T> (string methodName)
+        where T : class
+    {
+        nint ptr = LoadSymbol (methodName);
+
+        if (ptr == nint.Zero)
+        {
+            throw new MissingMethodException (string.Format ("The native method \"{0}\" does not exist", methodName));
+        }
+
+        return Marshal.GetDelegateForFunctionPointer<T> (ptr); // non-generic version is obsolete
+    }
+
+    /// <summary>Loads library in a platform specific way.</summary>
+    private static nint PlatformSpecificLoadLibrary (string libraryPath)
+    {
+        if (IsWindows)
+        {
+            return Windows.LoadLibrary (libraryPath);
+        }
+
+        if (IsLinux)
+        {
+            if (IsMono)
+            {
+                return Mono.dlopen (libraryPath, RTLD_GLOBAL + RTLD_LAZY);
+            }
+
+            if (IsNetCore)
+            {
+                try
+                {
+                    return CoreCLR.dlopen (libraryPath, RTLD_GLOBAL + RTLD_LAZY);
+                }
+                catch (Exception)
+                {
+                    IsNetCore = false;
+                }
+            }
+
+            return Linux.dlopen (libraryPath, RTLD_GLOBAL + RTLD_LAZY);
+        }
+
+        if (IsMacOSPlatform)
+        {
+            return MacOSX.dlopen (libraryPath, RTLD_GLOBAL + RTLD_LAZY);
+        }
+
+        throw new InvalidOperationException ("Unsupported platform.");
+    }
+
+    private static string FirstValidLibraryPath (string [] libraryPathAlternatives)
+    {
+        foreach (string path in libraryPathAlternatives)
+        {
+            if (File.Exists (path))
+            {
+                return path;
+            }
+        }
+
+        throw new FileNotFoundException (
+                                         string.Format (
+                                                        "Error loading native library. Not found in any of the possible locations: {0}",
+                                                        string.Join (",", libraryPathAlternatives)
+                                                       )
+                                        );
+    }
+
+    private static class Windows
+    {
+        [DllImport ("kernel32.dll")]
+        internal static extern nint GetProcAddress (nint hModule, string procName);
+
+        [DllImport ("kernel32.dll")]
+        internal static extern nint LoadLibrary (string filename);
+    }
+
+    private static class Linux
+    {
+        [DllImport ("libdl.so")]
+        internal static extern nint dlopen (string filename, int flags);
+
+        [DllImport ("libdl.so")]
+        internal static extern nint dlsym (nint handle, string symbol);
+    }
+
+    private static class MacOSX
+    {
+        [DllImport ("libSystem.dylib")]
+        internal static extern nint dlopen (string filename, int flags);
+
+        [DllImport ("libSystem.dylib")]
+        internal static extern nint dlsym (nint handle, string symbol);
+    }
+
+    /// <summary>
+    ///     On Linux systems, using using dlopen and dlsym results in DllNotFoundException("libdl.so not found") if
+    ///     libc6-dev is not installed. As a workaround, we load symbols for dlopen and dlsym from the current process as on
+    ///     Linux Mono sure is linked against these symbols.
+    /// </summary>
+    private static class Mono
+    {
+        [DllImport ("__Internal")]
+        internal static extern nint dlopen (string filename, int flags);
+
+        [DllImport ("__Internal")]
+        internal static extern nint dlsym (nint handle, string symbol);
+    }
+
+    /// <summary>
+    ///     Similarly as for Mono on Linux, we load symbols for dlopen and dlsym from the "libcoreclr.so", to avoid the
+    ///     dependency on libc-dev Linux.
+    /// </summary>
+    private static class CoreCLR
+    {
+        // Custom resolver to support true single-file apps
+        // (those which run directly from bundle; in-memory).
+        //	 -1 on Unix means self-referencing binary (libcoreclr.so)
+        //	 0 means fallback to CoreCLR's internal resolution
+        // Note: meaning of -1 stay the same even for non-single-file form factors.
+        static CoreCLR ()
+        {
+            NativeLibrary.SetDllImportResolver (
+                                                typeof (CoreCLR).Assembly,
+                                                (libraryName, assembly, searchPath) =>
+                                                    libraryName == "libcoreclr.so" ? -1 : nint.Zero
+                                               );
+        }
+
+        [DllImport ("libcoreclr.so")]
+        internal static extern nint dlopen (string filename, int flags);
 
-		}
-
-		// flags for dlopen
-		const int RTLD_LAZY = 1;
-		const int RTLD_GLOBAL = 8;
-
-		public readonly string LibraryPath;
-		readonly IntPtr handle;
-
-		public IntPtr NativeLibraryHandle => handle;
-
-		//
-		// if isFullPath is set to true, the provided array of libraries are full paths
-		// and are tested for the file existing, otherwise the file is merely the name
-		// of the shared library that we pass to dlopen
-		//
-		public UnmanagedLibrary (string [] libraryPathAlternatives, bool isFullPath)
-		{
-			if (isFullPath) {
-				this.LibraryPath = FirstValidLibraryPath (libraryPathAlternatives);
-				this.handle = PlatformSpecificLoadLibrary (this.LibraryPath);
-			} else {
-				foreach (var lib in libraryPathAlternatives) {
-					this.handle = PlatformSpecificLoadLibrary (lib);
-					if (this.handle != IntPtr.Zero) {
-						this.LibraryPath = lib;
-						break;
-					}
-				}
-			}
-
-			if (this.handle == IntPtr.Zero) {
-				throw new IOException ($"Error loading native library \"{string.Join (", ", libraryPathAlternatives)}\"");
-			}
-		}
-
-		/// <summary>
-		/// Loads symbol in a platform specific way.
-		/// </summary>
-		/// <param name="symbolName"></param>
-		/// <returns></returns>
-		public IntPtr LoadSymbol (string symbolName)
-		{
-			if (IsWindows) {
-				// See http://stackoverflow.com/questions/10473310 for background on this.
-				if (Is64Bit) {
-					return Windows.GetProcAddress (this.handle, symbolName);
-				} else {
-					// Yes, we could potentially predict the size... but it's a lot simpler to just try
-					// all the candidates. Most functions have a suffix of @0, @4 or @8 so we won't be trying
-					// many options - and if it takes a little bit longer to fail if we've really got the wrong
-					// library, that's not a big problem. This is only called once per function in the native library.
-					symbolName = "_" + symbolName + "@";
-					for (int stackSize = 0; stackSize < 128; stackSize += 4) {
-						IntPtr candidate = Windows.GetProcAddress (this.handle, symbolName + stackSize);
-						if (candidate != IntPtr.Zero) {
-							return candidate;
-						}
-					}
-					// Fail.
-					return IntPtr.Zero;
-				}
-			}
-			if (IsLinux) {
-				if (IsMono) {
-					return Mono.dlsym (this.handle, symbolName);
-				}
-				if (IsNetCore) {
-					return CoreCLR.dlsym (this.handle, symbolName);
-				}
-				return Linux.dlsym (this.handle, symbolName);
-			}
-			if (IsMacOS) {
-				return MacOSX.dlsym (this.handle, symbolName);
-			}
-			throw new InvalidOperationException ("Unsupported platform.");
-		}
-
-		public T GetNativeMethodDelegate<T> (string methodName)
-			where T : class
-		{
-			var ptr = LoadSymbol (methodName);
-			if (ptr == IntPtr.Zero) {
-				throw new MissingMethodException (string.Format ("The native method \"{0}\" does not exist", methodName));
-			}
-			return Marshal.GetDelegateForFunctionPointer<T> (ptr);  // non-generic version is obsolete
-		}
-
-		/// <summary>
-		/// Loads library in a platform specific way.
-		/// </summary>
-		static IntPtr PlatformSpecificLoadLibrary (string libraryPath)
-		{
-			if (IsWindows) {
-				return Windows.LoadLibrary (libraryPath);
-			}
-			if (IsLinux) {
-				if (IsMono) {
-					return Mono.dlopen (libraryPath, RTLD_GLOBAL + RTLD_LAZY);
-				}
-				if (IsNetCore) {
-					try {
-						return CoreCLR.dlopen (libraryPath, RTLD_GLOBAL + RTLD_LAZY);
-					} catch (Exception) {
-
-						IsNetCore = false;
-					}
-				}
-				return Linux.dlopen (libraryPath, RTLD_GLOBAL + RTLD_LAZY);
-			}
-			if (IsMacOS) {
-				return MacOSX.dlopen (libraryPath, RTLD_GLOBAL + RTLD_LAZY);
-			}
-			throw new InvalidOperationException ("Unsupported platform.");
-		}
-
-		static string FirstValidLibraryPath (string [] libraryPathAlternatives)
-		{
-			foreach (var path in libraryPathAlternatives) {
-				if (File.Exists (path)) {
-					return path;
-				}
-			}
-			throw new FileNotFoundException (
-				String.Format ("Error loading native library. Not found in any of the possible locations: {0}",
-				string.Join (",", libraryPathAlternatives)));
-		}
-
-		static class Windows {
-			[DllImport ("kernel32.dll")]
-			internal static extern IntPtr LoadLibrary (string filename);
-
-			[DllImport ("kernel32.dll")]
-			internal static extern IntPtr GetProcAddress (IntPtr hModule, string procName);
-		}
-
-		static class Linux {
-			[DllImport ("libdl.so")]
-			internal static extern IntPtr dlopen (string filename, int flags);
-
-			[DllImport ("libdl.so")]
-			internal static extern IntPtr dlsym (IntPtr handle, string symbol);
-		}
-
-		static class MacOSX {
-			[DllImport ("libSystem.dylib")]
-			internal static extern IntPtr dlopen (string filename, int flags);
-
-			[DllImport ("libSystem.dylib")]
-			internal static extern IntPtr dlsym (IntPtr handle, string symbol);
-		}
-
-		/// <summary>
-		/// On Linux systems, using using dlopen and dlsym results in
-		/// DllNotFoundException("libdl.so not found") if libc6-dev
-		/// is not installed. As a workaround, we load symbols for
-		/// dlopen and dlsym from the current process as on Linux
-		/// Mono sure is linked against these symbols.
-		/// </summary>
-		static class Mono {
-			[DllImport ("__Internal")]
-			internal static extern IntPtr dlopen (string filename, int flags);
-
-			[DllImport ("__Internal")]
-			internal static extern IntPtr dlsym (IntPtr handle, string symbol);
-		}
-
-		/// <summary>
-		/// Similarly as for Mono on Linux, we load symbols for
-		/// dlopen and dlsym from the "libcoreclr.so",
-		/// to avoid the dependency on libc-dev Linux.
-		/// </summary>
-		static class CoreCLR {
-			// Custom resolver to support true single-file apps
-			// (those which run directly from bundle; in-memory).
-			//	 -1 on Unix means self-referencing binary (libcoreclr.so)
-			//	 0 means fallback to CoreCLR's internal resolution
-			// Note: meaning of -1 stay the same even for non-single-file form factors.
-			static CoreCLR() =>  NativeLibrary.SetDllImportResolver(typeof(CoreCLR).Assembly,
-				(string libraryName, Assembly assembly, DllImportSearchPath? searchPath) =>
-					libraryName == "libcoreclr.so" ? (IntPtr)(-1) : IntPtr.Zero);
-
-			[DllImport ("libcoreclr.so")]
-			internal static extern IntPtr dlopen (string filename, int flags);
-
-			[DllImport ("libcoreclr.so")]
-			internal static extern IntPtr dlsym (IntPtr handle, string symbol);
-		}
-	}
+        [DllImport ("libcoreclr.so")]
+        internal static extern nint dlsym (nint handle, string symbol);
+    }
 }

+ 693 - 583
Terminal.Gui/ConsoleDrivers/CursesDriver/binding.cs

@@ -41,597 +41,707 @@
 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 //
-using System;
+
 using System.Runtime.InteropServices;
 using Terminal.Gui;
 
-namespace Unix.Terminal {
+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 MouseEvent {
-			public short ID;
-			public int X, Y, Z;
-			public Event ButtonState;
-		}
-
-		static int lines, cols;
-		static Window main_window;
-		static IntPtr curses_handle, curscr_ptr, lines_ptr, cols_ptr;
-
-		// If true, uses the DllImport into "ncurses", otherwise "libncursesw.so.5"
-		//static bool use_naked_driver;
-
-		static UnmanagedLibrary curses_library;
-
-		static NativeMethods methods;
-
-		[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);
-
-		static void LoadMethods ()
-		{
-			var libs = UnmanagedLibrary.IsMacOSPlatform ? new string [] { "libncurses.dylib" } : new string [] { "libncursesw.so.6", "libncursesw.so.5" };
-			var attempts = 1;
-			while (true) {
-				try {
-					curses_library = new UnmanagedLibrary (libs, false);
-					methods = new NativeMethods (curses_library);
-					break;
-				} catch (Exception ex) {
-
-					if (attempts == 1) {
-						attempts++;
-						var (exitCode, result) = ClipboardProcessRunner.Bash ("cat /etc/os-release", waitForOutput: true);
-						if (exitCode == 0 && result.Contains ("opensuse")) {
-							libs [0] = "libncursesw.so.5";
-						}
-					} else {
-						throw ex.GetBaseException ();
-					}
-				}
-			}
-		}
-
-		static void FindNCurses ()
-		{
-			LoadMethods ();
-			curses_handle = methods.UnmanagedLibrary.NativeLibraryHandle;
-
-			stdscr = read_static_ptr ("stdscr");
-			curscr_ptr = get_ptr ("curscr");
-			lines_ptr = get_ptr ("LINES");
-			cols_ptr = get_ptr ("COLS");
-		}
-
-		static public string curses_version ()
-		{
-			var v = methods.curses_version ();
-			return $"{Marshal.PtrToStringAnsi (v)}, {curses_library.LibraryPath}";
-
-		}
-
-		static public Window initscr ()
-		{
-			setlocale (LC_ALL, "");
-			FindNCurses ();
-
-			// Prevents the terminal from being locked after exiting.
-			reset_shell_mode ();
-
-			main_window = new Window (methods.initscr ());
-			try {
-				console_sharp_get_dims (out lines, out cols);
-			} catch (DllNotFoundException) {
-				endwin ();
-				Console.Error.WriteLine ("Unable to find the @MONO_CURSES@ native library\n" +
-							 "this is different than the managed mono-curses.dll\n\n" +
-							 "Typically you need to install to a LD_LIBRARY_PATH directory\n" +
-							 "or DYLD_LIBRARY_PATH directory or run /sbin/ldconfig");
-				Environment.Exit (1);
-			}
-
-			//Console.Error.WriteLine ($"using curses {Curses.curses_version ()}");
-
-			return main_window;
-		}
-
-		public static int Lines {
-			get {
-				return lines;
-			}
-			internal set {
-				// For unit tests
-				lines = value;
-			}
-		}
-
-		public static int Cols {
-			get {
-				return cols;
-			}
-			internal set {
-				// For unit tests
-				cols = value;
-			}
-		}
-
-		//
-		// Returns true if the window changed since the last invocation, as a
-		// side effect, the Lines and Cols properties are updated
-		//
-		public static bool CheckWinChange ()
-		{
-			int l, c;
-
-			console_sharp_get_dims (out l, out c);
-
-			if (l < 1) {
-				l = 1;
-			}
-			if (l != lines || c != cols) {
-				lines = l;
-				cols = c;
-				return true;
-			}
-			return false;
-		}
-
-		public static int addstr (string format, params object [] args)
-		{
-			var s = string.Format (format, args);
-			return addwstr (s);
-		}
-
-		static char [] r = new char [1];
-
-		//
-		// Have to wrap the native addch, as it can not
-		// display unicode characters, we have to use addstr
-		// for that.   but we need addch to render special ACS
-		// characters
-		//
-		public static int addch (int ch)
-		{
-			if (ch < 127 || ch > 0xffff)
-				return methods.addch (ch);
-			char c = (char)ch;
-			return addwstr (new String (c, 1));
-		}
-
-		public static int mvaddch (int y, int x, int ch)
-		{
-			if (ch < 127 || ch > 0xffff)
-				return methods.mvaddch (y, x, ch);
-			char c = (char)ch;
-			return mvaddwstr (y, x, new String (c, 1));
-		}
-
-		static IntPtr stdscr;
-
-		static IntPtr get_ptr (string key)
-		{
-			var ptr = curses_library.LoadSymbol (key);
-
-			if (ptr == IntPtr.Zero)
-				throw new Exception ("Could not load the key " + key);
-			return ptr;
-		}
-
-		internal static IntPtr read_static_ptr (string key)
-		{
-			var ptr = get_ptr (key);
-			return Marshal.ReadIntPtr (ptr);
-		}
-
-		internal static IntPtr console_sharp_get_stdscr () => stdscr;
-
-		internal static IntPtr console_sharp_get_curscr ()
-		{
-			return Marshal.ReadIntPtr (curscr_ptr);
-		}
-
-		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;
-			//}
-		}
-
-		public static Event mousemask (Event newmask, out Event oldmask)
-		{
-			IntPtr e;
-			var ret = (Event)(methods.mousemask ((IntPtr)newmask, out e));
-			oldmask = (Event)e;
-			return ret;
-		}
-
-		// We encode ESC + char (what Alt-char generates) as 0x2000 + char
-		public const int KeyAlt = 0x2000;
-
-		static public int IsAlt (int key)
-		{
-			if ((key & KeyAlt) != 0)
-				return key & ~KeyAlt;
-			return 0;
-		}
-
-		public static int StartColor () => methods.start_color ();
-		public static bool HasColors => methods.has_colors ();
-		/// <summary>
-		/// The init_pair routine changes the definition of a color-pair.It takes
-		/// three arguments: the number of the color-pair to be changed, the  fore-
-		/// ground color number, and the background color number.For portable ap-
-		/// plications:
-		/// 
-		/// o The first argument must be a legal color pair  value.If  default
-		/// colors are used (see use_default_colors(3x)) the upper limit is ad-
-		/// justed to allow for extra pairs which use a default color in  fore-
-		/// ground and/or background.
-		/// 
-		/// o The second and third arguments must be legal color values.
-		/// 
-		/// If the  color-pair was previously initialized, the screen is refreshed
-		/// and all occurrences of that color-pair are changed to the new defini-
-		/// tion.
-		/// 
-		/// As an  extension,  ncurses allows you to set color pair 0 via the as-
-		/// sume_default_colors (3x) routine, or to specify the use of default  col-
-		/// ors (color number  -1) if you first invoke the use_default_colors (3x)
-		/// routine.
-		/// </summary>
-		/// <param name="pair"></param>
-		/// <param name="foreground"></param>
-		/// <param name="background"></param>
-		/// <returns></returns>
-		public static int InitColorPair (short pair, short foreground, short background) => methods.init_pair (pair, foreground, background);
-		// TODO: Upgrade to ncurses 6.1 and use the extended version
-		//public static int InitExtendedPair (int pair, int foreground, int background) => methods.init_extended_pair (pair, foreground, background);
-		public static int UseDefaultColors () => methods.use_default_colors ();
-		public static int ColorPairs => methods.COLOR_PAIRS ();
-
-		//
-		// The proxy methods to call into each version
-		//
-		static public int endwin () => methods.endwin ();
-		static public bool isendwin () => methods.isendwin ();
-		static public int cbreak () => methods.cbreak ();
-		static public int nocbreak () => methods.nocbreak ();
-		static public int echo () => methods.echo ();
-		static public int noecho () => methods.noecho ();
-		static public int halfdelay (int t) => methods.halfdelay (t);
-		static public int raw () => methods.raw ();
-		static public int noraw () => methods.noraw ();
-		static public void noqiflush () => methods.noqiflush ();
-		static public void qiflush () => methods.qiflush ();
-		static public int typeahead (IntPtr fd) => methods.typeahead (fd);
-		static public int timeout (int delay) => methods.timeout (delay);
-		static public int wtimeout (IntPtr win, int delay) => methods.wtimeout (win, delay);
-		static public int notimeout (IntPtr win, bool bf) => methods.notimeout (win, bf);
-		static public int keypad (IntPtr win, bool bf) => methods.keypad (win, bf);
-		static public int meta (IntPtr win, bool bf) => methods.meta (win, bf);
-		static public int intrflush (IntPtr win, bool bf) => methods.intrflush (win, bf);
-		static public int clearok (IntPtr win, bool bf) => methods.clearok (win, bf);
-		static public int idlok (IntPtr win, bool bf) => methods.idlok (win, bf);
-		static public void idcok (IntPtr win, bool bf) => methods.idcok (win, bf);
-		static public void immedok (IntPtr win, bool bf) => methods.immedok (win, bf);
-		static public int leaveok (IntPtr win, bool bf) => methods.leaveok (win, bf);
-		static public int wsetscrreg (IntPtr win, int top, int bot) => methods.wsetscrreg (win, top, bot);
-		static public int scrollok (IntPtr win, bool bf) => methods.scrollok (win, bf);
-		static public int nl () => methods.nl ();
-		static public int nonl () => methods.nonl ();
-		static public int setscrreg (int top, int bot) => methods.setscrreg (top, bot);
-		static public int refresh () => methods.refresh ();
-		static public int doupdate () => methods.doupdate ();
-		static public int wrefresh (IntPtr win) => methods.wrefresh (win);
-		static public int redrawwin (IntPtr win) => methods.redrawwin (win);
-		//static public int wredrawwin (IntPtr win, int beg_line, int num_lines) => methods.wredrawwin (win, beg_line, num_lines);
-		static public int wnoutrefresh (IntPtr win) => methods.wnoutrefresh (win);
-		static public int move (int line, int col) => methods.move (line, col);
-		static public int curs_set (int visibility) => methods.curs_set (visibility);
-		//static public int addch (int ch) => methods.addch (ch);
-		static public int echochar (int ch) => methods.echochar (ch);
-		static public int addwstr (string s) => methods.addwstr (s);
-		static public int mvaddwstr (int y, int x, string s) => methods.mvaddwstr (y, x, s);
-		static public int wmove (IntPtr win, int line, int col) => methods.wmove (win, line, col);
-		static public int waddch (IntPtr win, int ch) => methods.waddch (win, ch);
-		//static public int wechochar (IntPtr win, int ch) => methods.wechochar (win, ch);
-		static public int attron (int attrs) => methods.attron (attrs);
-		static public int attroff (int attrs) => methods.attroff (attrs);
-		static public int attrset (int attrs) => methods.attrset (attrs);
-		static public int getch () => methods.getch ();
-		static public int get_wch (out int sequence) => methods.get_wch (out sequence);
-		static public int ungetch (int ch) => methods.ungetch (ch);
-		static public int mvgetch (int y, int x) => methods.mvgetch (y, x);
-		static public bool has_colors () => methods.has_colors ();
-		static public int start_color () => methods.start_color ();
-		static public int init_pair (short pair, short f, short b) => methods.init_pair (pair, f, b);
-		static public int use_default_colors () => methods.use_default_colors ();
-		static public int COLOR_PAIRS () => methods.COLOR_PAIRS ();
-		static public uint getmouse (out MouseEvent ev) => methods.getmouse (out ev);
-		static public uint ungetmouse (ref MouseEvent ev) => methods.ungetmouse (ref ev);
-		static public int mouseinterval (int interval) => methods.mouseinterval (interval);
-		static public bool is_term_resized (int lines, int columns) => methods.is_term_resized (lines, columns);
-		static public int resize_term (int lines, int columns) => methods.resize_term (lines, columns);
-		static public int resizeterm (int lines, int columns) => methods.resizeterm (lines, columns);
-		static public void use_env (bool f) => methods.use_env (f);
-		static public int flushinp () => methods.flushinp ();
-		static public int def_prog_mode () => methods.def_prog_mode ();
-		static public int def_shell_mode () => methods.def_shell_mode ();
-		static public int reset_prog_mode () => methods.reset_prog_mode ();
-		static public int reset_shell_mode () => methods.reset_shell_mode ();
-		static public int savetty () => methods.savetty ();
-		static public int resetty () => methods.resetty ();
-		static public int set_escdelay (int size) => methods.set_escdelay (size);
-	}
+public partial class Curses
+{
+    // We encode ESC + char (what Alt-char generates) as 0x2000 + char
+    public const int KeyAlt = 0x2000;
+    private static nint curses_handle, curscr_ptr, lines_ptr, cols_ptr;
+
+    // If true, uses the DllImport into "ncurses", otherwise "libncursesw.so.5"
+    //static bool use_naked_driver;
+    private static UnmanagedLibrary curses_library;
+    private static int lines, cols;
+    private static Window main_window;
+    private static NativeMethods methods;
+    private static char [] r = new char [1];
+    private static nint stdscr;
+    public static int ColorPairs => methods.COLOR_PAIRS ();
+
+    public static int Cols
+    {
+        get => cols;
+        internal set =>
+
+            // For unit tests
+            cols = value;
+    }
+
+    public static bool HasColors => methods.has_colors ();
+
+    public static int Lines
+    {
+        get => lines;
+        internal set =>
+
+            // For unit tests
+            lines = value;
+    }
+
+    //
+    // Have to wrap the native addch, as it can not
+    // display unicode characters, we have to use addstr
+    // for that.   but we need addch to render special ACS
+    // characters
+    //
+    public static int addch (int ch)
+    {
+        if (ch < 127 || ch > 0xffff)
+        {
+            return methods.addch (ch);
+        }
+
+        var c = (char)ch;
+
+        return addwstr (new string (c, 1));
+    }
+
+    public static int addstr (string format, params object [] args)
+    {
+        string s = string.Format (format, args);
+
+        return addwstr (s);
+    }
+
+    public static int addwstr (string s) { return methods.addwstr (s); }
+    public static int attroff (int attrs) { return methods.attroff (attrs); }
+
+    //static public int wechochar (IntPtr win, int ch) => methods.wechochar (win, ch);
+    public static int attron (int attrs) { return methods.attron (attrs); }
+    public static int attrset (int attrs) { return methods.attrset (attrs); }
+    public static int cbreak () { return methods.cbreak (); }
+
+    //
+    // Returns true if the window changed since the last invocation, as a
+    // side effect, the Lines and Cols properties are updated
+    //
+    public static bool CheckWinChange ()
+    {
+        int l, c;
+
+        console_sharp_get_dims (out l, out c);
+
+        if (l < 1)
+        {
+            l = 1;
+        }
+
+        if (l != lines || c != cols)
+        {
+            lines = l;
+            cols = c;
+
+            return true;
+        }
+
+        return false;
+    }
+
+    public static int clearok (nint win, bool bf) { return methods.clearok (win, bf); }
+    public static int COLOR_PAIRS () { return methods.COLOR_PAIRS (); }
+    public static int curs_set (int visibility) { return methods.curs_set (visibility); }
+
+    public static string curses_version ()
+    {
+        nint v = methods.curses_version ();
+
+        return $"{Marshal.PtrToStringAnsi (v)}, {curses_library.LibraryPath}";
+    }
+
+    public static int def_prog_mode () { return methods.def_prog_mode (); }
+    public static int def_shell_mode () { return methods.def_shell_mode (); }
+    public static int doupdate () { return methods.doupdate (); }
+    public static int echo () { return methods.echo (); }
+
+    //static public int addch (int ch) => methods.addch (ch);
+    public static int echochar (int ch) { return methods.echochar (ch); }
+
+    //
+    // The proxy methods to call into each version
+    //
+    public static int endwin () { return methods.endwin (); }
+    public static int flushinp () { return methods.flushinp (); }
+    public static int get_wch (out int sequence) { return methods.get_wch (out sequence); }
+    public static int getch () { return methods.getch (); }
+    public static uint getmouse (out MouseEvent ev) { return methods.getmouse (out ev); }
+    public static int halfdelay (int t) { return methods.halfdelay (t); }
+    public static bool has_colors () { return methods.has_colors (); }
+    public static void idcok (nint win, bool bf) { methods.idcok (win, bf); }
+    public static int idlok (nint win, bool bf) { return methods.idlok (win, bf); }
+    public static void immedok (nint win, bool bf) { methods.immedok (win, bf); }
+    public static int init_pair (short pair, short f, short b) { return methods.init_pair (pair, f, b); }
+
+    /// <summary>
+    ///     The init_pair routine changes the definition of a color-pair.It takes three arguments: the number of the
+    ///     color-pair to be changed, the  fore- ground color number, and the background color number.For portable ap-
+    ///     plications: o The first argument must be a legal color pair  value.If  default colors are used (see
+    ///     use_default_colors(3x)) the upper limit is ad- justed to allow for extra pairs which use a default color in  fore-
+    ///     ground and/or background. o The second and third arguments must be legal color values. If the  color-pair was
+    ///     previously initialized, the screen is refreshed and all occurrences of that color-pair are changed to the new
+    ///     defini- tion. As an  extension,  ncurses allows you to set color pair 0 via the as- sume_default_colors (3x)
+    ///     routine, or to specify the use of default  col- ors (color number  -1) if you first invoke the use_default_colors
+    ///     (3x) routine.
+    /// </summary>
+    /// <param name="pair"></param>
+    /// <param name="foreground"></param>
+    /// <param name="background"></param>
+    /// <returns></returns>
+    public static int InitColorPair (short pair, short foreground, short background) { return methods.init_pair (pair, foreground, background); }
+
+    public static Window initscr ()
+    {
+        setlocale (LC_ALL, "");
+        FindNCurses ();
+
+        // Prevents the terminal from being locked after exiting.
+        reset_shell_mode ();
+
+        main_window = new Window (methods.initscr ());
+
+        try
+        {
+            console_sharp_get_dims (out lines, out cols);
+        }
+        catch (DllNotFoundException)
+        {
+            endwin ();
+
+            Console.Error.WriteLine (
+                                     "Unable to find the @MONO_CURSES@ native library\n"
+                                     + "this is different than the managed mono-curses.dll\n\n"
+                                     + "Typically you need to install to a LD_LIBRARY_PATH directory\n"
+                                     + "or DYLD_LIBRARY_PATH directory or run /sbin/ldconfig"
+                                    );
+            Environment.Exit (1);
+        }
+
+        //Console.Error.WriteLine ($"using curses {Curses.curses_version ()}");
+
+        return main_window;
+    }
+
+    public static int intrflush (nint win, bool bf) { return methods.intrflush (win, bf); }
+    public static bool is_term_resized (int lines, int columns) { return methods.is_term_resized (lines, columns); }
+
+    public static int IsAlt (int key)
+    {
+        if ((key & KeyAlt) != 0)
+        {
+            return key & ~KeyAlt;
+        }
+
+        return 0;
+    }
+
+    public static bool isendwin () { return methods.isendwin (); }
+    public static int keypad (nint win, bool bf) { return methods.keypad (win, bf); }
+    public static int leaveok (nint win, bool bf) { return methods.leaveok (win, bf); }
+    public static int meta (nint win, bool bf) { return methods.meta (win, bf); }
+    public static int mouseinterval (int interval) { return methods.mouseinterval (interval); }
+
+    public static Event mousemask (Event newmask, out Event oldmask)
+    {
+        nint e;
+        var ret = (Event)methods.mousemask ((nint)newmask, out e);
+        oldmask = (Event)e;
+
+        return ret;
+    }
+
+    public static int move (int line, int col) { return methods.move (line, col); }
+
+    public static int mvaddch (int y, int x, int ch)
+    {
+        if (ch < 127 || ch > 0xffff)
+        {
+            return methods.mvaddch (y, x, ch);
+        }
+
+        var c = (char)ch;
+
+        return mvaddwstr (y, x, new string (c, 1));
+    }
+
+    public static int mvaddwstr (int y, int x, string s) { return methods.mvaddwstr (y, x, s); }
+    public static int mvgetch (int y, int x) { return methods.mvgetch (y, x); }
+    public static int nl () { return methods.nl (); }
+    public static int nocbreak () { return methods.nocbreak (); }
+    public static int noecho () { return methods.noecho (); }
+    public static int nonl () { return methods.nonl (); }
+    public static void noqiflush () { methods.noqiflush (); }
+    public static int noraw () { return methods.noraw (); }
+    public static int notimeout (nint win, bool bf) { return methods.notimeout (win, bf); }
+    public static void qiflush () { methods.qiflush (); }
+    public static int raw () { return methods.raw (); }
+    public static int redrawwin (nint win) { return methods.redrawwin (win); }
+    public static int refresh () { return methods.refresh (); }
+    public static int reset_prog_mode () { return methods.reset_prog_mode (); }
+    public static int reset_shell_mode () { return methods.reset_shell_mode (); }
+    public static int resetty () { return methods.resetty (); }
+    public static int resize_term (int lines, int columns) { return methods.resize_term (lines, columns); }
+    public static int resizeterm (int lines, int columns) { return methods.resizeterm (lines, columns); }
+    public static int savetty () { return methods.savetty (); }
+    public static int scrollok (nint win, bool bf) { return methods.scrollok (win, bf); }
+    public static int set_escdelay (int size) { return methods.set_escdelay (size); }
+
+    [DllImport ("libc")]
+    public static extern int setlocale (int cate, [MarshalAs (UnmanagedType.LPStr)] string locale);
+
+    public static int setscrreg (int top, int bot) { return methods.setscrreg (top, bot); }
+    public static int start_color () { return methods.start_color (); }
+    public static int StartColor () { return methods.start_color (); }
+    public static int timeout (int delay) { return methods.timeout (delay); }
+    public static int typeahead (nint fd) { return methods.typeahead (fd); }
+    public static int ungetch (int ch) { return methods.ungetch (ch); }
+    public static uint ungetmouse (ref MouseEvent ev) { return methods.ungetmouse (ref ev); }
+    public static int use_default_colors () { return methods.use_default_colors (); }
+    public static void use_env (bool f) { methods.use_env (f); }
+
+    // TODO: Upgrade to ncurses 6.1 and use the extended version
+    //public static int InitExtendedPair (int pair, int foreground, int background) => methods.init_extended_pair (pair, foreground, background);
+    public static int UseDefaultColors () { return methods.use_default_colors (); }
+    public static int waddch (nint win, int ch) { return methods.waddch (win, ch); }
+    public static int wmove (nint win, int line, int col) { return methods.wmove (win, line, col); }
+
+    //static public int wredrawwin (IntPtr win, int beg_line, int num_lines) => methods.wredrawwin (win, beg_line, num_lines);
+    public static int wnoutrefresh (nint win) { return methods.wnoutrefresh (win); }
+    public static int wrefresh (nint win) { return methods.wrefresh (win); }
+    public static int wsetscrreg (nint win, int top, int bot) { return methods.wsetscrreg (win, top, bot); }
+    public static int wtimeout (nint win, int delay) { return methods.wtimeout (win, delay); }
+    internal static nint console_sharp_get_curscr () { return Marshal.ReadIntPtr (curscr_ptr); }
+
+    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;
+        //}
+    }
+
+    internal static nint console_sharp_get_stdscr () { return stdscr; }
+
+    internal static nint read_static_ptr (string key)
+    {
+        nint ptr = get_ptr (key);
+
+        return Marshal.ReadIntPtr (ptr);
+    }
+
+    private static void FindNCurses ()
+    {
+        LoadMethods ();
+        curses_handle = methods.UnmanagedLibrary.NativeLibraryHandle;
+
+        stdscr = read_static_ptr ("stdscr");
+        curscr_ptr = get_ptr ("curscr");
+        lines_ptr = get_ptr ("LINES");
+        cols_ptr = get_ptr ("COLS");
+    }
+
+    private static nint get_ptr (string key)
+    {
+        nint ptr = curses_library.LoadSymbol (key);
+
+        if (ptr == nint.Zero)
+        {
+            throw new Exception ("Could not load the key " + key);
+        }
+
+        return ptr;
+    }
+
+    //[DllImport ("libc")]
+    //public extern static int ioctl (int fd, int cmd, out winsize argp);
+
+    private static void LoadMethods ()
+    {
+        string [] libs = UnmanagedLibrary.IsMacOSPlatform
+                             ? new [] { "libncurses.dylib" }
+                             : new [] { "libncursesw.so.6", "libncursesw.so.5" };
+        var attempts = 1;
+
+        while (true)
+        {
+            try
+            {
+                curses_library = new UnmanagedLibrary (libs, false);
+                methods = new NativeMethods (curses_library);
+
+                break;
+            }
+            catch (Exception ex)
+            {
+                if (attempts == 1)
+                {
+                    attempts++;
+
+                    (int exitCode, string result) =
+                        ClipboardProcessRunner.Bash ("cat /etc/os-release", waitForOutput: true);
+
+                    if (exitCode == 0 && result.Contains ("opensuse"))
+                    {
+                        libs [0] = "libncursesw.so.5";
+                    }
+                }
+                else
+                {
+                    throw ex.GetBaseException ();
+                }
+            }
+        }
+    }
+
+    //[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
+    {
+        public short ID;
+        public int X, Y, Z;
+        public Event ButtonState;
+    }
+}
 
 #pragma warning disable RCS1102 // Make class static.'
-	internal class Delegates {
+internal class Delegates
+{
 #pragma warning restore RCS1102 // Make class static.
 #pragma warning disable CS8981 // The type name only contains lower-cased ascii characters. Such names may become reserved for the language.
-		public delegate IntPtr initscr ();
-		public delegate int endwin ();
-		public delegate bool isendwin ();
-		public delegate int cbreak ();
-		public delegate int nocbreak ();
-		public delegate int echo ();
-		public delegate int noecho ();
-		public delegate int halfdelay (int t);
-		public delegate int raw ();
-		public delegate int noraw ();
-		public delegate void noqiflush ();
-		public delegate void qiflush ();
-		public delegate int typeahead (IntPtr fd);
-		public delegate int timeout (int delay);
-		public delegate int wtimeout (IntPtr win, int delay);
-		public delegate int notimeout (IntPtr win, bool bf);
-		public delegate int keypad (IntPtr win, bool bf);
-		public delegate int meta (IntPtr win, bool bf);
-		public delegate int intrflush (IntPtr win, bool bf);
-		public delegate int clearok (IntPtr win, bool bf);
-		public delegate int idlok (IntPtr win, bool bf);
-		public delegate void idcok (IntPtr win, bool bf);
-		public delegate void immedok (IntPtr win, bool bf);
-		public delegate int leaveok (IntPtr win, bool bf);
-		public delegate int wsetscrreg (IntPtr win, int top, int bot);
-		public delegate int scrollok (IntPtr win, bool bf);
-		public delegate int nl ();
-		public delegate int nonl ();
-		public delegate int setscrreg (int top, int bot);
-		public delegate int refresh ();
-		public delegate int doupdate ();
-		public delegate int wrefresh (IntPtr win);
-		public delegate int redrawwin (IntPtr win);
-		//public delegate int wredrawwin (IntPtr win, int beg_line, int num_lines);
-		public delegate int wnoutrefresh (IntPtr win);
-		public delegate int move (int line, int col);
-		public delegate int curs_set (int visibility);
-		public delegate int addch (int ch);
-		public delegate int echochar (int ch);
-		public delegate int mvaddch (int y, int x, int ch);
-		public delegate int addwstr ([MarshalAs (UnmanagedType.LPWStr)] string s);
-		public delegate int mvaddwstr (int y, int x, [MarshalAs (UnmanagedType.LPWStr)] string s);
-		public delegate int wmove (IntPtr win, int line, int col);
-		public delegate int waddch (IntPtr win, int ch);
-		public delegate int attron (int attrs);
-		public delegate int attroff (int attrs);
-		public delegate int attrset (int attrs);
-		public delegate int getch ();
-		public delegate int get_wch (out int sequence);
-		public delegate int ungetch (int ch);
-		public delegate int mvgetch (int y, int x);
-		public delegate bool has_colors ();
-		public delegate int start_color ();
-		public delegate int init_pair (short pair, short f, short b);
-		public delegate int use_default_colors ();
-		public delegate int COLOR_PAIRS ();
-		public delegate uint getmouse (out Curses.MouseEvent ev);
-		public delegate uint ungetmouse (ref Curses.MouseEvent ev);
-		public delegate int mouseinterval (int interval);
-		public delegate IntPtr mousemask (IntPtr newmask, out IntPtr oldMask);
-		public delegate bool is_term_resized (int lines, int columns);
-		public delegate int resize_term (int lines, int columns);
-		public delegate int resizeterm (int lines, int columns);
-		public delegate void use_env (bool f);
-		public delegate int flushinp ();
-		public delegate int def_prog_mode ();
-		public delegate int def_shell_mode ();
-		public delegate int reset_prog_mode ();
-		public delegate int reset_shell_mode ();
-		public delegate int savetty ();
-		public delegate int resetty ();
-		public delegate int set_escdelay (int size);
-		public delegate IntPtr curses_version ();
-	}
-
-	internal class NativeMethods {
-		public readonly Delegates.initscr initscr;
-		public readonly Delegates.endwin endwin;
-		public readonly Delegates.isendwin isendwin;
-		public readonly Delegates.cbreak cbreak;
-		public readonly Delegates.nocbreak nocbreak;
-		public readonly Delegates.echo echo;
-		public readonly Delegates.noecho noecho;
-		public readonly Delegates.halfdelay halfdelay;
-		public readonly Delegates.raw raw;
-		public readonly Delegates.noraw noraw;
-		public readonly Delegates.noqiflush noqiflush;
-		public readonly Delegates.qiflush qiflush;
-		public readonly Delegates.typeahead typeahead;
-		public readonly Delegates.timeout timeout;
-		public readonly Delegates.wtimeout wtimeout;
-		public readonly Delegates.notimeout notimeout;
-		public readonly Delegates.keypad keypad;
-		public readonly Delegates.meta meta;
-		public readonly Delegates.intrflush intrflush;
-		public readonly Delegates.clearok clearok;
-		public readonly Delegates.idlok idlok;
-		public readonly Delegates.idcok idcok;
-		public readonly Delegates.immedok immedok;
-		public readonly Delegates.leaveok leaveok;
-		public readonly Delegates.wsetscrreg wsetscrreg;
-		public readonly Delegates.scrollok scrollok;
-		public readonly Delegates.nl nl;
-		public readonly Delegates.nonl nonl;
-		public readonly Delegates.setscrreg setscrreg;
-		public readonly Delegates.refresh refresh;
-		public readonly Delegates.doupdate doupdate;
-		public readonly Delegates.wrefresh wrefresh;
-		public readonly Delegates.redrawwin redrawwin;
-		//public readonly Delegates.wredrawwin wredrawwin;
-		public readonly Delegates.wnoutrefresh wnoutrefresh;
-		public readonly Delegates.move move;
-		public readonly Delegates.curs_set curs_set;
-		public readonly Delegates.addch addch;
-		public readonly Delegates.echochar echochar;
-		public readonly Delegates.mvaddch mvaddch;
-		public readonly Delegates.addwstr addwstr;
-		public readonly Delegates.mvaddwstr mvaddwstr;
-		public readonly Delegates.wmove wmove;
-		public readonly Delegates.waddch waddch;
-		//public readonly Delegates.wechochar wechochar;
-		public readonly Delegates.attron attron;
-		public readonly Delegates.attroff attroff;
-		public readonly Delegates.attrset attrset;
-		public readonly Delegates.getch getch;
-		public readonly Delegates.get_wch get_wch;
-		public readonly Delegates.ungetch ungetch;
-		public readonly Delegates.mvgetch mvgetch;
-		public readonly Delegates.has_colors has_colors;
-		public readonly Delegates.start_color start_color;
-		public readonly Delegates.init_pair init_pair;
-		public readonly Delegates.use_default_colors use_default_colors;
-		public readonly Delegates.COLOR_PAIRS COLOR_PAIRS;
-		public readonly Delegates.getmouse getmouse;
-		public readonly Delegates.ungetmouse ungetmouse;
-		public readonly Delegates.mouseinterval mouseinterval;
-		public readonly Delegates.mousemask mousemask;
-		public readonly Delegates.is_term_resized is_term_resized;
-		public readonly Delegates.resize_term resize_term;
-		public readonly Delegates.resizeterm resizeterm;
-		public readonly Delegates.use_env use_env;
-		public readonly Delegates.flushinp flushinp;
-		public readonly Delegates.def_prog_mode def_prog_mode;
-		public readonly Delegates.def_shell_mode def_shell_mode;
-		public readonly Delegates.reset_prog_mode reset_prog_mode;
-		public readonly Delegates.reset_shell_mode reset_shell_mode;
-		public readonly Delegates.savetty savetty;
-		public readonly Delegates.resetty resetty;
-		public readonly Delegates.set_escdelay set_escdelay;
-		public readonly Delegates.curses_version curses_version;
-		public UnmanagedLibrary UnmanagedLibrary;
-
-		public NativeMethods (UnmanagedLibrary lib)
-		{
-			this.UnmanagedLibrary = lib;
-			initscr = lib.GetNativeMethodDelegate<Delegates.initscr> ("initscr");
-			endwin = lib.GetNativeMethodDelegate<Delegates.endwin> ("endwin");
-			isendwin = lib.GetNativeMethodDelegate<Delegates.isendwin> ("isendwin");
-			cbreak = lib.GetNativeMethodDelegate<Delegates.cbreak> ("cbreak");
-			nocbreak = lib.GetNativeMethodDelegate<Delegates.nocbreak> ("nocbreak");
-			echo = lib.GetNativeMethodDelegate<Delegates.echo> ("echo");
-			noecho = lib.GetNativeMethodDelegate<Delegates.noecho> ("noecho");
-			halfdelay = lib.GetNativeMethodDelegate<Delegates.halfdelay> ("halfdelay");
-			raw = lib.GetNativeMethodDelegate<Delegates.raw> ("raw");
-			noraw = lib.GetNativeMethodDelegate<Delegates.noraw> ("noraw");
-			noqiflush = lib.GetNativeMethodDelegate<Delegates.noqiflush> ("noqiflush");
-			qiflush = lib.GetNativeMethodDelegate<Delegates.qiflush> ("qiflush");
-			typeahead = lib.GetNativeMethodDelegate<Delegates.typeahead> ("typeahead");
-			timeout = lib.GetNativeMethodDelegate<Delegates.timeout> ("timeout");
-			wtimeout = lib.GetNativeMethodDelegate<Delegates.wtimeout> ("wtimeout");
-			notimeout = lib.GetNativeMethodDelegate<Delegates.notimeout> ("notimeout");
-			keypad = lib.GetNativeMethodDelegate<Delegates.keypad> ("keypad");
-			meta = lib.GetNativeMethodDelegate<Delegates.meta> ("meta");
-			intrflush = lib.GetNativeMethodDelegate<Delegates.intrflush> ("intrflush");
-			clearok = lib.GetNativeMethodDelegate<Delegates.clearok> ("clearok");
-			idlok = lib.GetNativeMethodDelegate<Delegates.idlok> ("idlok");
-			idcok = lib.GetNativeMethodDelegate<Delegates.idcok> ("idcok");
-			immedok = lib.GetNativeMethodDelegate<Delegates.immedok> ("immedok");
-			leaveok = lib.GetNativeMethodDelegate<Delegates.leaveok> ("leaveok");
-			wsetscrreg = lib.GetNativeMethodDelegate<Delegates.wsetscrreg> ("wsetscrreg");
-			scrollok = lib.GetNativeMethodDelegate<Delegates.scrollok> ("scrollok");
-			nl = lib.GetNativeMethodDelegate<Delegates.nl> ("nl");
-			nonl = lib.GetNativeMethodDelegate<Delegates.nonl> ("nonl");
-			setscrreg = lib.GetNativeMethodDelegate<Delegates.setscrreg> ("setscrreg");
-			refresh = lib.GetNativeMethodDelegate<Delegates.refresh> ("refresh");
-			doupdate = lib.GetNativeMethodDelegate<Delegates.doupdate> ("doupdate");
-			wrefresh = lib.GetNativeMethodDelegate<Delegates.wrefresh> ("wrefresh");
-			redrawwin = lib.GetNativeMethodDelegate<Delegates.redrawwin> ("redrawwin");
-			//wredrawwin = lib.GetNativeMethodDelegate<Delegates.wredrawwin> ("wredrawwin");
-			wnoutrefresh = lib.GetNativeMethodDelegate<Delegates.wnoutrefresh> ("wnoutrefresh");
-			move = lib.GetNativeMethodDelegate<Delegates.move> ("move");
-			curs_set = lib.GetNativeMethodDelegate<Delegates.curs_set> ("curs_set");
-			addch = lib.GetNativeMethodDelegate<Delegates.addch> ("addch");
-			echochar = lib.GetNativeMethodDelegate<Delegates.echochar> ("echochar");
-			mvaddch = lib.GetNativeMethodDelegate<Delegates.mvaddch> ("mvaddch");
-			addwstr = lib.GetNativeMethodDelegate<Delegates.addwstr> ("addwstr");
-			mvaddwstr = lib.GetNativeMethodDelegate<Delegates.mvaddwstr> ("mvaddwstr");
-			wmove = lib.GetNativeMethodDelegate<Delegates.wmove> ("wmove");
-			waddch = lib.GetNativeMethodDelegate<Delegates.waddch> ("waddch");
-			attron = lib.GetNativeMethodDelegate<Delegates.attron> ("attron");
-			attroff = lib.GetNativeMethodDelegate<Delegates.attroff> ("attroff");
-			attrset = lib.GetNativeMethodDelegate<Delegates.attrset> ("attrset");
-			getch = lib.GetNativeMethodDelegate<Delegates.getch> ("getch");
-			get_wch = lib.GetNativeMethodDelegate<Delegates.get_wch> ("get_wch");
-			ungetch = lib.GetNativeMethodDelegate<Delegates.ungetch> ("ungetch");
-			mvgetch = lib.GetNativeMethodDelegate<Delegates.mvgetch> ("mvgetch");
-			has_colors = lib.GetNativeMethodDelegate<Delegates.has_colors> ("has_colors");
-			start_color = lib.GetNativeMethodDelegate<Delegates.start_color> ("start_color");
-			init_pair = lib.GetNativeMethodDelegate<Delegates.init_pair> ("init_pair");
-			use_default_colors = lib.GetNativeMethodDelegate<Delegates.use_default_colors> ("use_default_colors");
-			COLOR_PAIRS = lib.GetNativeMethodDelegate<Delegates.COLOR_PAIRS> ("COLOR_PAIRS");
-			getmouse = lib.GetNativeMethodDelegate<Delegates.getmouse> ("getmouse");
-			ungetmouse = lib.GetNativeMethodDelegate<Delegates.ungetmouse> ("ungetmouse");
-			mouseinterval = lib.GetNativeMethodDelegate<Delegates.mouseinterval> ("mouseinterval");
-			mousemask = lib.GetNativeMethodDelegate<Delegates.mousemask> ("mousemask");
-			is_term_resized = lib.GetNativeMethodDelegate<Delegates.is_term_resized> ("is_term_resized");
-			resize_term = lib.GetNativeMethodDelegate<Delegates.resize_term> ("resize_term");
-			resizeterm = lib.GetNativeMethodDelegate<Delegates.resizeterm> ("resizeterm");
-			use_env = lib.GetNativeMethodDelegate<Delegates.use_env> ("use_env");
-			flushinp = lib.GetNativeMethodDelegate<Delegates.flushinp> ("flushinp");
-			def_prog_mode = lib.GetNativeMethodDelegate<Delegates.def_prog_mode> ("def_prog_mode");
-			def_shell_mode = lib.GetNativeMethodDelegate<Delegates.def_shell_mode> ("def_shell_mode");
-			reset_prog_mode = lib.GetNativeMethodDelegate<Delegates.reset_prog_mode> ("reset_prog_mode");
-			reset_shell_mode = lib.GetNativeMethodDelegate<Delegates.reset_shell_mode> ("reset_shell_mode");
-			savetty = lib.GetNativeMethodDelegate<Delegates.savetty> ("savetty");
-			resetty = lib.GetNativeMethodDelegate<Delegates.resetty> ("resetty");
-			set_escdelay = lib.GetNativeMethodDelegate<Delegates.set_escdelay> ("set_escdelay");
-			curses_version = lib.GetNativeMethodDelegate<Delegates.curses_version> ("curses_version");
-		}
-	}
-#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member
-#pragma warning restore CS8981 // The type name only contains lower-cased ascii characters. Such names may become reserved for the language.
+    public delegate nint initscr ();
+
+    public delegate int endwin ();
+
+    public delegate bool isendwin ();
+
+    public delegate int cbreak ();
+
+    public delegate int nocbreak ();
+
+    public delegate int echo ();
+
+    public delegate int noecho ();
+
+    public delegate int halfdelay (int t);
+
+    public delegate int raw ();
+
+    public delegate int noraw ();
+
+    public delegate void noqiflush ();
+
+    public delegate void qiflush ();
+
+    public delegate int typeahead (nint fd);
+
+    public delegate int timeout (int delay);
+
+    public delegate int wtimeout (nint win, int delay);
+
+    public delegate int notimeout (nint win, bool bf);
+
+    public delegate int keypad (nint win, bool bf);
+
+    public delegate int meta (nint win, bool bf);
+
+    public delegate int intrflush (nint win, bool bf);
+
+    public delegate int clearok (nint win, bool bf);
+
+    public delegate int idlok (nint win, bool bf);
+
+    public delegate void idcok (nint win, bool bf);
+
+    public delegate void immedok (nint win, bool bf);
+
+    public delegate int leaveok (nint win, bool bf);
+
+    public delegate int wsetscrreg (nint win, int top, int bot);
+
+    public delegate int scrollok (nint win, bool bf);
+
+    public delegate int nl ();
+
+    public delegate int nonl ();
+
+    public delegate int setscrreg (int top, int bot);
+
+    public delegate int refresh ();
+
+    public delegate int doupdate ();
+
+    public delegate int wrefresh (nint win);
+
+    public delegate int redrawwin (nint win);
+
+    //public delegate int wredrawwin (IntPtr win, int beg_line, int num_lines);
+    public delegate int wnoutrefresh (nint win);
+
+    public delegate int move (int line, int col);
+
+    public delegate int curs_set (int visibility);
 
+    public delegate int addch (int ch);
+
+    public delegate int echochar (int ch);
+
+    public delegate int mvaddch (int y, int x, int ch);
+
+    public delegate int addwstr ([MarshalAs (UnmanagedType.LPWStr)] string s);
+
+    public delegate int mvaddwstr (int y, int x, [MarshalAs (UnmanagedType.LPWStr)] string s);
+
+    public delegate int wmove (nint win, int line, int col);
+
+    public delegate int waddch (nint win, int ch);
+
+    public delegate int attron (int attrs);
+
+    public delegate int attroff (int attrs);
+
+    public delegate int attrset (int attrs);
+
+    public delegate int getch ();
+
+    public delegate int get_wch (out int sequence);
+
+    public delegate int ungetch (int ch);
+
+    public delegate int mvgetch (int y, int x);
+
+    public delegate bool has_colors ();
+
+    public delegate int start_color ();
+
+    public delegate int init_pair (short pair, short f, short b);
+
+    public delegate int use_default_colors ();
+
+    public delegate int COLOR_PAIRS ();
+
+    public delegate uint getmouse (out Curses.MouseEvent ev);
+
+    public delegate uint ungetmouse (ref Curses.MouseEvent ev);
+
+    public delegate int mouseinterval (int interval);
+
+    public delegate nint mousemask (nint newmask, out nint oldMask);
+
+    public delegate bool is_term_resized (int lines, int columns);
+
+    public delegate int resize_term (int lines, int columns);
+
+    public delegate int resizeterm (int lines, int columns);
+
+    public delegate void use_env (bool f);
+
+    public delegate int flushinp ();
+
+    public delegate int def_prog_mode ();
+
+    public delegate int def_shell_mode ();
+
+    public delegate int reset_prog_mode ();
+
+    public delegate int reset_shell_mode ();
+
+    public delegate int savetty ();
+
+    public delegate int resetty ();
+
+    public delegate int set_escdelay (int size);
+
+    public delegate nint curses_version ();
 }
+
+internal class NativeMethods
+{
+    public readonly Delegates.addch addch;
+    public readonly Delegates.addwstr addwstr;
+    public readonly Delegates.attroff attroff;
+
+    //public readonly Delegates.wechochar wechochar;
+    public readonly Delegates.attron attron;
+    public readonly Delegates.attrset attrset;
+    public readonly Delegates.cbreak cbreak;
+    public readonly Delegates.clearok clearok;
+    public readonly Delegates.COLOR_PAIRS COLOR_PAIRS;
+    public readonly Delegates.curs_set curs_set;
+    public readonly Delegates.curses_version curses_version;
+    public readonly Delegates.def_prog_mode def_prog_mode;
+    public readonly Delegates.def_shell_mode def_shell_mode;
+    public readonly Delegates.doupdate doupdate;
+    public readonly Delegates.echo echo;
+    public readonly Delegates.echochar echochar;
+    public readonly Delegates.endwin endwin;
+    public readonly Delegates.flushinp flushinp;
+    public readonly Delegates.get_wch get_wch;
+    public readonly Delegates.getch getch;
+    public readonly Delegates.getmouse getmouse;
+    public readonly Delegates.halfdelay halfdelay;
+    public readonly Delegates.has_colors has_colors;
+    public readonly Delegates.idcok idcok;
+    public readonly Delegates.idlok idlok;
+    public readonly Delegates.immedok immedok;
+    public readonly Delegates.init_pair init_pair;
+    public readonly Delegates.initscr initscr;
+    public readonly Delegates.intrflush intrflush;
+    public readonly Delegates.is_term_resized is_term_resized;
+    public readonly Delegates.isendwin isendwin;
+    public readonly Delegates.keypad keypad;
+    public readonly Delegates.leaveok leaveok;
+    public readonly Delegates.meta meta;
+    public readonly Delegates.mouseinterval mouseinterval;
+    public readonly Delegates.mousemask mousemask;
+    public readonly Delegates.move move;
+    public readonly Delegates.mvaddch mvaddch;
+    public readonly Delegates.mvaddwstr mvaddwstr;
+    public readonly Delegates.mvgetch mvgetch;
+    public readonly Delegates.nl nl;
+    public readonly Delegates.nocbreak nocbreak;
+    public readonly Delegates.noecho noecho;
+    public readonly Delegates.nonl nonl;
+    public readonly Delegates.noqiflush noqiflush;
+    public readonly Delegates.noraw noraw;
+    public readonly Delegates.notimeout notimeout;
+    public readonly Delegates.qiflush qiflush;
+    public readonly Delegates.raw raw;
+    public readonly Delegates.redrawwin redrawwin;
+    public readonly Delegates.refresh refresh;
+    public readonly Delegates.reset_prog_mode reset_prog_mode;
+    public readonly Delegates.reset_shell_mode reset_shell_mode;
+    public readonly Delegates.resetty resetty;
+    public readonly Delegates.resize_term resize_term;
+    public readonly Delegates.resizeterm resizeterm;
+    public readonly Delegates.savetty savetty;
+    public readonly Delegates.scrollok scrollok;
+    public readonly Delegates.set_escdelay set_escdelay;
+    public readonly Delegates.setscrreg setscrreg;
+    public readonly Delegates.start_color start_color;
+    public readonly Delegates.timeout timeout;
+    public readonly Delegates.typeahead typeahead;
+    public readonly Delegates.ungetch ungetch;
+    public readonly Delegates.ungetmouse ungetmouse;
+    public readonly Delegates.use_default_colors use_default_colors;
+    public readonly Delegates.use_env use_env;
+    public readonly Delegates.waddch waddch;
+    public readonly Delegates.wmove wmove;
+
+    //public readonly Delegates.wredrawwin wredrawwin;
+    public readonly Delegates.wnoutrefresh wnoutrefresh;
+    public readonly Delegates.wrefresh wrefresh;
+    public readonly Delegates.wsetscrreg wsetscrreg;
+    public readonly Delegates.wtimeout wtimeout;
+    public UnmanagedLibrary UnmanagedLibrary;
+
+    public NativeMethods (UnmanagedLibrary lib)
+    {
+        UnmanagedLibrary = lib;
+        initscr = lib.GetNativeMethodDelegate<Delegates.initscr> ("initscr");
+        endwin = lib.GetNativeMethodDelegate<Delegates.endwin> ("endwin");
+        isendwin = lib.GetNativeMethodDelegate<Delegates.isendwin> ("isendwin");
+        cbreak = lib.GetNativeMethodDelegate<Delegates.cbreak> ("cbreak");
+        nocbreak = lib.GetNativeMethodDelegate<Delegates.nocbreak> ("nocbreak");
+        echo = lib.GetNativeMethodDelegate<Delegates.echo> ("echo");
+        noecho = lib.GetNativeMethodDelegate<Delegates.noecho> ("noecho");
+        halfdelay = lib.GetNativeMethodDelegate<Delegates.halfdelay> ("halfdelay");
+        raw = lib.GetNativeMethodDelegate<Delegates.raw> ("raw");
+        noraw = lib.GetNativeMethodDelegate<Delegates.noraw> ("noraw");
+        noqiflush = lib.GetNativeMethodDelegate<Delegates.noqiflush> ("noqiflush");
+        qiflush = lib.GetNativeMethodDelegate<Delegates.qiflush> ("qiflush");
+        typeahead = lib.GetNativeMethodDelegate<Delegates.typeahead> ("typeahead");
+        timeout = lib.GetNativeMethodDelegate<Delegates.timeout> ("timeout");
+        wtimeout = lib.GetNativeMethodDelegate<Delegates.wtimeout> ("wtimeout");
+        notimeout = lib.GetNativeMethodDelegate<Delegates.notimeout> ("notimeout");
+        keypad = lib.GetNativeMethodDelegate<Delegates.keypad> ("keypad");
+        meta = lib.GetNativeMethodDelegate<Delegates.meta> ("meta");
+        intrflush = lib.GetNativeMethodDelegate<Delegates.intrflush> ("intrflush");
+        clearok = lib.GetNativeMethodDelegate<Delegates.clearok> ("clearok");
+        idlok = lib.GetNativeMethodDelegate<Delegates.idlok> ("idlok");
+        idcok = lib.GetNativeMethodDelegate<Delegates.idcok> ("idcok");
+        immedok = lib.GetNativeMethodDelegate<Delegates.immedok> ("immedok");
+        leaveok = lib.GetNativeMethodDelegate<Delegates.leaveok> ("leaveok");
+        wsetscrreg = lib.GetNativeMethodDelegate<Delegates.wsetscrreg> ("wsetscrreg");
+        scrollok = lib.GetNativeMethodDelegate<Delegates.scrollok> ("scrollok");
+        nl = lib.GetNativeMethodDelegate<Delegates.nl> ("nl");
+        nonl = lib.GetNativeMethodDelegate<Delegates.nonl> ("nonl");
+        setscrreg = lib.GetNativeMethodDelegate<Delegates.setscrreg> ("setscrreg");
+        refresh = lib.GetNativeMethodDelegate<Delegates.refresh> ("refresh");
+        doupdate = lib.GetNativeMethodDelegate<Delegates.doupdate> ("doupdate");
+        wrefresh = lib.GetNativeMethodDelegate<Delegates.wrefresh> ("wrefresh");
+        redrawwin = lib.GetNativeMethodDelegate<Delegates.redrawwin> ("redrawwin");
+
+        //wredrawwin = lib.GetNativeMethodDelegate<Delegates.wredrawwin> ("wredrawwin");
+        wnoutrefresh = lib.GetNativeMethodDelegate<Delegates.wnoutrefresh> ("wnoutrefresh");
+        move = lib.GetNativeMethodDelegate<Delegates.move> ("move");
+        curs_set = lib.GetNativeMethodDelegate<Delegates.curs_set> ("curs_set");
+        addch = lib.GetNativeMethodDelegate<Delegates.addch> ("addch");
+        echochar = lib.GetNativeMethodDelegate<Delegates.echochar> ("echochar");
+        mvaddch = lib.GetNativeMethodDelegate<Delegates.mvaddch> ("mvaddch");
+        addwstr = lib.GetNativeMethodDelegate<Delegates.addwstr> ("addwstr");
+        mvaddwstr = lib.GetNativeMethodDelegate<Delegates.mvaddwstr> ("mvaddwstr");
+        wmove = lib.GetNativeMethodDelegate<Delegates.wmove> ("wmove");
+        waddch = lib.GetNativeMethodDelegate<Delegates.waddch> ("waddch");
+        attron = lib.GetNativeMethodDelegate<Delegates.attron> ("attron");
+        attroff = lib.GetNativeMethodDelegate<Delegates.attroff> ("attroff");
+        attrset = lib.GetNativeMethodDelegate<Delegates.attrset> ("attrset");
+        getch = lib.GetNativeMethodDelegate<Delegates.getch> ("getch");
+        get_wch = lib.GetNativeMethodDelegate<Delegates.get_wch> ("get_wch");
+        ungetch = lib.GetNativeMethodDelegate<Delegates.ungetch> ("ungetch");
+        mvgetch = lib.GetNativeMethodDelegate<Delegates.mvgetch> ("mvgetch");
+        has_colors = lib.GetNativeMethodDelegate<Delegates.has_colors> ("has_colors");
+        start_color = lib.GetNativeMethodDelegate<Delegates.start_color> ("start_color");
+        init_pair = lib.GetNativeMethodDelegate<Delegates.init_pair> ("init_pair");
+        use_default_colors = lib.GetNativeMethodDelegate<Delegates.use_default_colors> ("use_default_colors");
+        COLOR_PAIRS = lib.GetNativeMethodDelegate<Delegates.COLOR_PAIRS> ("COLOR_PAIRS");
+        getmouse = lib.GetNativeMethodDelegate<Delegates.getmouse> ("getmouse");
+        ungetmouse = lib.GetNativeMethodDelegate<Delegates.ungetmouse> ("ungetmouse");
+        mouseinterval = lib.GetNativeMethodDelegate<Delegates.mouseinterval> ("mouseinterval");
+        mousemask = lib.GetNativeMethodDelegate<Delegates.mousemask> ("mousemask");
+        is_term_resized = lib.GetNativeMethodDelegate<Delegates.is_term_resized> ("is_term_resized");
+        resize_term = lib.GetNativeMethodDelegate<Delegates.resize_term> ("resize_term");
+        resizeterm = lib.GetNativeMethodDelegate<Delegates.resizeterm> ("resizeterm");
+        use_env = lib.GetNativeMethodDelegate<Delegates.use_env> ("use_env");
+        flushinp = lib.GetNativeMethodDelegate<Delegates.flushinp> ("flushinp");
+        def_prog_mode = lib.GetNativeMethodDelegate<Delegates.def_prog_mode> ("def_prog_mode");
+        def_shell_mode = lib.GetNativeMethodDelegate<Delegates.def_shell_mode> ("def_shell_mode");
+        reset_prog_mode = lib.GetNativeMethodDelegate<Delegates.reset_prog_mode> ("reset_prog_mode");
+        reset_shell_mode = lib.GetNativeMethodDelegate<Delegates.reset_shell_mode> ("reset_shell_mode");
+        savetty = lib.GetNativeMethodDelegate<Delegates.savetty> ("savetty");
+        resetty = lib.GetNativeMethodDelegate<Delegates.resetty> ("resetty");
+        set_escdelay = lib.GetNativeMethodDelegate<Delegates.set_escdelay> ("set_escdelay");
+        curses_version = lib.GetNativeMethodDelegate<Delegates.curses_version> ("curses_version");
+    }
+}
+#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member
+#pragma warning restore CS8981 // The type name only contains lower-cased ascii characters. Such names may become reserved for the language.

+ 165 - 172
Terminal.Gui/ConsoleDrivers/CursesDriver/constants.cs

@@ -4,182 +4,175 @@
 
 //#define XTERM1006
 
-using System;
+using System.Runtime.InteropServices;
 
-namespace Unix.Terminal {
+namespace Unix.Terminal;
 #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
-	public partial class Curses {
-		public const int A_NORMAL = unchecked((int)0x0);
-		public const int A_STANDOUT = unchecked((int)0x10000);
-		public const int A_UNDERLINE = unchecked((int)0x20000);
-		public const int A_REVERSE = unchecked((int)0x40000);
-		public const int A_BLINK = unchecked((int)0x80000);
-		public const int A_DIM = unchecked((int)0x100000);
-		public const int A_BOLD = unchecked((int)0x200000);
-		public const int A_PROTECT = unchecked((int)0x1000000);
-		public const int A_INVIS = unchecked((int)0x800000);
-		public const int ACS_LLCORNER = unchecked((int)0x40006d);
-		public const int ACS_LRCORNER = unchecked((int)0x40006a);
-		public const int ACS_HLINE = unchecked((int)0x400071);
-		public const int ACS_ULCORNER = unchecked((int)0x40006c);
-		public const int ACS_URCORNER = unchecked((int)0x40006b);
-		public const int ACS_VLINE = unchecked((int)0x400078);
-		public const int ACS_LTEE = unchecked((int)0x400074);
-		public const int ACS_RTEE = unchecked((int)0x400075);
-		public const int ACS_BTEE = unchecked((int)0x400076);
-		public const int ACS_TTEE = unchecked((int)0x400077);
-		public const int ACS_PLUS = unchecked((int)0x40006e);
-		public const int ACS_S1 = unchecked((int)0x40006f);
-		public const int ACS_S9 = unchecked((int)0x400073);
-		public const int ACS_DIAMOND = unchecked((int)0x400060);
-		public const int ACS_CKBOARD = unchecked((int)0x400061);
-		public const int ACS_DEGREE = unchecked((int)0x400066);
-		public const int ACS_PLMINUS = unchecked((int)0x400067);
-		public const int ACS_BULLET = unchecked((int)0x40007e);
-		public const int ACS_LARROW = unchecked((int)0x40002c);
-		public const int ACS_RARROW = unchecked((int)0x40002b);
-		public const int ACS_DARROW = unchecked((int)0x40002e);
-		public const int ACS_UARROW = unchecked((int)0x40002d);
-		public const int ACS_BOARD = unchecked((int)0x400068);
-		public const int ACS_LANTERN = unchecked((int)0x400069);
-		public const int ACS_BLOCK = unchecked((int)0x400030);
-		public const int COLOR_BLACK = unchecked((int)0x0);
-		public const int COLOR_RED = unchecked((int)0x1);
-		public const int COLOR_GREEN = unchecked((int)0x2);
-		public const int COLOR_YELLOW = unchecked((int)0x3);
-		public const int COLOR_BLUE = unchecked((int)0x4);
-		public const int COLOR_MAGENTA = unchecked((int)0x5);
-		public const int COLOR_CYAN = unchecked((int)0x6);
-		public const int COLOR_WHITE = unchecked((int)0x7);
-		public const int COLOR_GRAY = unchecked((int)0x8);
-		public const int KEY_CODE_YES = unchecked((int)0x100);
-		public const int ERR = unchecked((int)0xffffffff);
-		public const int TIOCGWINSZ  = unchecked((int)0x5413);
-		public const int TIOCGWINSZ_MAC  = unchecked((int)0x40087468);
-
-		[Flags]
-		public enum Event : long {
-			Button1Pressed = unchecked((int)0x2),
-			Button1Released = unchecked((int)0x1),
-			Button1Clicked = unchecked((int)0x4),
-			Button1DoubleClicked = unchecked((int)0x8),
-			Button1TripleClicked = unchecked((int)0x10),
-			Button2Pressed = unchecked((int)0x40),
-			Button2Released = unchecked((int)0x20),
-			Button2Clicked = unchecked((int)0x80),
-			Button2DoubleClicked = unchecked((int)0x100),
-			Button2TripleClicked = unchecked((int)0x200),
-			Button3Pressed = unchecked((int)0x800),
-			Button3Released = unchecked((int)0x400),
-			Button3Clicked = unchecked((int)0x1000),
-			Button3DoubleClicked = unchecked((int)0x2000),
-			Button3TripleClicked = unchecked((int)0x4000),
-			ButtonWheeledUp = unchecked((int)0x10000),
-			ButtonWheeledDown = unchecked((int)0x200000),
-			Button4Pressed = unchecked((int)0x80000),
-			Button4Released = unchecked((int)0x40000),
-			Button4Clicked = unchecked((int)0x100000),
-			Button4DoubleClicked = unchecked((int)0x20000),
-			Button4TripleClicked = unchecked((int)0x400000),
-			ButtonShift = unchecked((int)0x4000000),
-			ButtonCtrl = unchecked((int)0x2000000),
-			ButtonAlt = unchecked((int)0x8000000),
-			ReportMousePosition = unchecked((int)0x10000000),
-			AllEvents = unchecked((int)0x7ffffff),
-		}
+public partial class Curses
+{
+    public const int A_NORMAL = 0x0;
+    public const int A_STANDOUT = 0x10000;
+    public const int A_UNDERLINE = 0x20000;
+    public const int A_REVERSE = 0x40000;
+    public const int A_BLINK = 0x80000;
+    public const int A_DIM = 0x100000;
+    public const int A_BOLD = 0x200000;
+    public const int A_PROTECT = 0x1000000;
+    public const int A_INVIS = 0x800000;
+    public const int ACS_LLCORNER = 0x40006d;
+    public const int ACS_LRCORNER = 0x40006a;
+    public const int ACS_HLINE = 0x400071;
+    public const int ACS_ULCORNER = 0x40006c;
+    public const int ACS_URCORNER = 0x40006b;
+    public const int ACS_VLINE = 0x400078;
+    public const int ACS_LTEE = 0x400074;
+    public const int ACS_RTEE = 0x400075;
+    public const int ACS_BTEE = 0x400076;
+    public const int ACS_TTEE = 0x400077;
+    public const int ACS_PLUS = 0x40006e;
+    public const int ACS_S1 = 0x40006f;
+    public const int ACS_S9 = 0x400073;
+    public const int ACS_DIAMOND = 0x400060;
+    public const int ACS_CKBOARD = 0x400061;
+    public const int ACS_DEGREE = 0x400066;
+    public const int ACS_PLMINUS = 0x400067;
+    public const int ACS_BULLET = 0x40007e;
+    public const int ACS_LARROW = 0x40002c;
+    public const int ACS_RARROW = 0x40002b;
+    public const int ACS_DARROW = 0x40002e;
+    public const int ACS_UARROW = 0x40002d;
+    public const int ACS_BOARD = 0x400068;
+    public const int ACS_LANTERN = 0x400069;
+    public const int ACS_BLOCK = 0x400030;
+    public const int COLOR_BLACK = 0x0;
+    public const int COLOR_RED = 0x1;
+    public const int COLOR_GREEN = 0x2;
+    public const int COLOR_YELLOW = 0x3;
+    public const int COLOR_BLUE = 0x4;
+    public const int COLOR_MAGENTA = 0x5;
+    public const int COLOR_CYAN = 0x6;
+    public const int COLOR_WHITE = 0x7;
+    public const int COLOR_GRAY = 0x8;
+    public const int KEY_CODE_YES = 0x100;
+    public const int ERR = unchecked ((int)0xffffffff);
+    public const int TIOCGWINSZ = 0x5413;
+    public const int TIOCGWINSZ_MAC = 0x40087468;
+    [Flags]
+    public enum Event : long
+    {
+        Button1Pressed = 0x2,
+        Button1Released = 0x1,
+        Button1Clicked = 0x4,
+        Button1DoubleClicked = 0x8,
+        Button1TripleClicked = 0x10,
+        Button2Pressed = 0x40,
+        Button2Released = 0x20,
+        Button2Clicked = 0x80,
+        Button2DoubleClicked = 0x100,
+        Button2TripleClicked = 0x200,
+        Button3Pressed = 0x800,
+        Button3Released = 0x400,
+        Button3Clicked = 0x1000,
+        Button3DoubleClicked = 0x2000,
+        Button3TripleClicked = 0x4000,
+        ButtonWheeledUp = 0x10000,
+        ButtonWheeledDown = 0x200000,
+        Button4Pressed = 0x80000,
+        Button4Released = 0x40000,
+        Button4Clicked = 0x100000,
+        Button4DoubleClicked = 0x20000,
+        Button4TripleClicked = 0x400000,
+        ButtonShift = 0x4000000,
+        ButtonCtrl = 0x2000000,
+        ButtonAlt = 0x8000000,
+        ReportMousePosition = 0x10000000,
+        AllEvents = 0x7ffffff
+    }
 #if XTERM1006
-		public const int LeftRightUpNPagePPage= unchecked((int)0x8);
-		public const int DownEnd = unchecked((int)0x6);
-		public const int Home = unchecked((int)0x7);
+    public const int LeftRightUpNPagePPage = unchecked ((int)0x8);
+    public const int DownEnd = unchecked ((int)0x6);
+    public const int Home = unchecked ((int)0x7);
 #else
-		public const int LeftRightUpNPagePPage = unchecked((int)0x0);
-		public const int DownEnd = unchecked((int)0x0);
-		public const int Home = unchecked((int)0x0);
+    public const int LeftRightUpNPagePPage = 0x0;
+    public const int DownEnd = 0x0;
+    public const int Home = 0x0;
 #endif
-		public const int KeyBackspace = unchecked((int)0x107);
-		public const int KeyUp = unchecked((int)0x103);
-		public const int KeyDown = unchecked((int)0x102);
-		public const int KeyLeft = unchecked((int)0x104);
-		public const int KeyRight = unchecked((int)0x105);
-		public const int KeyNPage = unchecked((int)0x152);
-		public const int KeyPPage = unchecked((int)0x153);
-		public const int KeyHome = unchecked((int)0x106);
-		public const int KeyMouse = unchecked((int)0x199);
-		public const int KeyCSI = unchecked((int)0x5b);
-		public const int KeyEnd = unchecked((int)0x168);
-		public const int KeyDeleteChar = unchecked((int)0x14a);
-		public const int KeyInsertChar = unchecked((int)0x14b);
-		public const int KeyTab = unchecked((int)0x009);
-		public const int KeyBackTab = unchecked((int)0x161);
-		public const int KeyF1 = unchecked((int)0x109);
-		public const int KeyF2 = unchecked((int)0x10a);
-		public const int KeyF3 = unchecked((int)0x10b);
-		public const int KeyF4 = unchecked((int)0x10c);
-		public const int KeyF5 = unchecked((int)0x10d);
-		public const int KeyF6 = unchecked((int)0x10e);
-		public const int KeyF7 = unchecked((int)0x10f);
-		public const int KeyF8 = unchecked((int)0x110);
-		public const int KeyF9 = unchecked((int)0x111);
-		public const int KeyF10 = unchecked((int)0x112);
-		public const int KeyF11 = unchecked((int)0x113);
-		public const int KeyF12 = unchecked((int)0x114);
-		public const int KeyResize = unchecked((int)0x19a);
-		public const int ShiftKeyUp = unchecked((int)0x151);
-		public const int ShiftKeyDown = unchecked((int)0x150);
-		public const int ShiftKeyLeft = unchecked((int)0x189);
-		public const int ShiftKeyRight = unchecked((int)0x192);
-		public const int ShiftKeyNPage = unchecked((int)0x18c);
-		public const int ShiftKeyPPage = unchecked((int)0x18e);
-		public const int ShiftKeyHome = unchecked((int)0x187);
-		public const int ShiftKeyEnd = unchecked((int)0x182);
-		public const int AltKeyUp = unchecked((int)0x234 + LeftRightUpNPagePPage);
-		public const int AltKeyDown = unchecked((int)0x20b + DownEnd);
-		public const int AltKeyLeft = unchecked((int)0x21f + LeftRightUpNPagePPage);
-		public const int AltKeyRight = unchecked((int)0x22e + LeftRightUpNPagePPage);
-		public const int AltKeyNPage = unchecked((int)0x224 + LeftRightUpNPagePPage);
-		public const int AltKeyPPage = unchecked((int)0x229 + LeftRightUpNPagePPage);
-		public const int AltKeyHome = unchecked((int)0x215 + Home);
-		public const int AltKeyEnd = unchecked((int)0x210 + DownEnd);
-		public const int CtrlKeyUp = unchecked((int)0x236 + LeftRightUpNPagePPage);
-		public const int CtrlKeyDown = unchecked((int)0x20d + DownEnd);
-		public const int CtrlKeyLeft = unchecked((int)0x221 + LeftRightUpNPagePPage);
-		public const int CtrlKeyRight = unchecked((int)0x230 + LeftRightUpNPagePPage);
-		public const int CtrlKeyNPage = unchecked((int)0x226 + LeftRightUpNPagePPage);
-		public const int CtrlKeyPPage = unchecked((int)0x22b + LeftRightUpNPagePPage);
-		public const int CtrlKeyHome = unchecked((int)0x217 + Home);
-		public const int CtrlKeyEnd = unchecked((int)0x212 + DownEnd);
-		public const int ShiftCtrlKeyUp = unchecked((int)0x237 + LeftRightUpNPagePPage);
-		public const int ShiftCtrlKeyDown = unchecked((int)0x20e + DownEnd);
-		public const int ShiftCtrlKeyLeft = unchecked((int)0x222 + LeftRightUpNPagePPage);
-		public const int ShiftCtrlKeyRight = unchecked((int)0x231 + LeftRightUpNPagePPage);
-		public const int ShiftCtrlKeyNPage = unchecked((int)0x227 + LeftRightUpNPagePPage);
-		public const int ShiftCtrlKeyPPage = unchecked((int)0x22c + LeftRightUpNPagePPage);
-		public const int ShiftCtrlKeyHome = unchecked((int)0x218 + Home);
-		public const int ShiftCtrlKeyEnd = unchecked((int)0x213 + DownEnd);
-		public const int ShiftAltKeyUp = unchecked((int)0x235 + LeftRightUpNPagePPage);
-		public const int ShiftAltKeyDown = unchecked((int)0x20c + DownEnd);
-		public const int ShiftAltKeyLeft = unchecked((int)0x220 + LeftRightUpNPagePPage);
-		public const int ShiftAltKeyRight = unchecked((int)0x22f + LeftRightUpNPagePPage);
-		public const int ShiftAltKeyNPage = unchecked((int)0x225 + LeftRightUpNPagePPage);
-		public const int ShiftAltKeyPPage = unchecked((int)0x22a + LeftRightUpNPagePPage);
-		public const int ShiftAltKeyHome = unchecked((int)0x216 + Home);
-		public const int ShiftAltKeyEnd = unchecked((int)0x211 + DownEnd);
-		public const int AltCtrlKeyNPage = unchecked((int)0x228 + LeftRightUpNPagePPage);
-		public const int AltCtrlKeyPPage = unchecked((int)0x22d + LeftRightUpNPagePPage);
-		public const int AltCtrlKeyHome = unchecked((int)0x219 + Home);
-		public const int AltCtrlKeyEnd = unchecked((int)0x214 + DownEnd);
-
-		// see #949
-		static public int LC_ALL { get; private set; }
-		static Curses ()
-		{
-			LC_ALL = System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform (System.Runtime.InteropServices.OSPlatform.OSX) ? 0 : 6;
-		}
+    public const int KeyBackspace = 0x107;
+    public const int KeyUp = 0x103;
+    public const int KeyDown = 0x102;
+    public const int KeyLeft = 0x104;
+    public const int KeyRight = 0x105;
+    public const int KeyNPage = 0x152;
+    public const int KeyPPage = 0x153;
+    public const int KeyHome = 0x106;
+    public const int KeyMouse = 0x199;
+    public const int KeyCSI = 0x5b;
+    public const int KeyEnd = 0x168;
+    public const int KeyDeleteChar = 0x14a;
+    public const int KeyInsertChar = 0x14b;
+    public const int KeyTab = 0x009;
+    public const int KeyBackTab = 0x161;
+    public const int KeyF1 = 0x109;
+    public const int KeyF2 = 0x10a;
+    public const int KeyF3 = 0x10b;
+    public const int KeyF4 = 0x10c;
+    public const int KeyF5 = 0x10d;
+    public const int KeyF6 = 0x10e;
+    public const int KeyF7 = 0x10f;
+    public const int KeyF8 = 0x110;
+    public const int KeyF9 = 0x111;
+    public const int KeyF10 = 0x112;
+    public const int KeyF11 = 0x113;
+    public const int KeyF12 = 0x114;
+    public const int KeyResize = 0x19a;
+    public const int ShiftKeyUp = 0x151;
+    public const int ShiftKeyDown = 0x150;
+    public const int ShiftKeyLeft = 0x189;
+    public const int ShiftKeyRight = 0x192;
+    public const int ShiftKeyNPage = 0x18c;
+    public const int ShiftKeyPPage = 0x18e;
+    public const int ShiftKeyHome = 0x187;
+    public const int ShiftKeyEnd = 0x182;
+    public const int AltKeyUp = unchecked (0x234 + LeftRightUpNPagePPage);
+    public const int AltKeyDown = unchecked (0x20b + DownEnd);
+    public const int AltKeyLeft = unchecked (0x21f + LeftRightUpNPagePPage);
+    public const int AltKeyRight = unchecked (0x22e + LeftRightUpNPagePPage);
+    public const int AltKeyNPage = unchecked (0x224 + LeftRightUpNPagePPage);
+    public const int AltKeyPPage = unchecked (0x229 + LeftRightUpNPagePPage);
+    public const int AltKeyHome = unchecked (0x215 + Home);
+    public const int AltKeyEnd = unchecked (0x210 + DownEnd);
+    public const int CtrlKeyUp = unchecked (0x236 + LeftRightUpNPagePPage);
+    public const int CtrlKeyDown = unchecked (0x20d + DownEnd);
+    public const int CtrlKeyLeft = unchecked (0x221 + LeftRightUpNPagePPage);
+    public const int CtrlKeyRight = unchecked (0x230 + LeftRightUpNPagePPage);
+    public const int CtrlKeyNPage = unchecked (0x226 + LeftRightUpNPagePPage);
+    public const int CtrlKeyPPage = unchecked (0x22b + LeftRightUpNPagePPage);
+    public const int CtrlKeyHome = unchecked (0x217 + Home);
+    public const int CtrlKeyEnd = unchecked (0x212 + DownEnd);
+    public const int ShiftCtrlKeyUp = unchecked (0x237 + LeftRightUpNPagePPage);
+    public const int ShiftCtrlKeyDown = unchecked (0x20e + DownEnd);
+    public const int ShiftCtrlKeyLeft = unchecked (0x222 + LeftRightUpNPagePPage);
+    public const int ShiftCtrlKeyRight = unchecked (0x231 + LeftRightUpNPagePPage);
+    public const int ShiftCtrlKeyNPage = unchecked (0x227 + LeftRightUpNPagePPage);
+    public const int ShiftCtrlKeyPPage = unchecked (0x22c + LeftRightUpNPagePPage);
+    public const int ShiftCtrlKeyHome = unchecked (0x218 + Home);
+    public const int ShiftCtrlKeyEnd = unchecked (0x213 + DownEnd);
+    public const int ShiftAltKeyUp = unchecked (0x235 + LeftRightUpNPagePPage);
+    public const int ShiftAltKeyDown = unchecked (0x20c + DownEnd);
+    public const int ShiftAltKeyLeft = unchecked (0x220 + LeftRightUpNPagePPage);
+    public const int ShiftAltKeyRight = unchecked (0x22f + LeftRightUpNPagePPage);
+    public const int ShiftAltKeyNPage = unchecked (0x225 + LeftRightUpNPagePPage);
+    public const int ShiftAltKeyPPage = unchecked (0x22a + LeftRightUpNPagePPage);
+    public const int ShiftAltKeyHome = unchecked (0x216 + Home);
+    public const int ShiftAltKeyEnd = unchecked (0x211 + DownEnd);
+    public const int AltCtrlKeyNPage = unchecked (0x228 + LeftRightUpNPagePPage);
+    public const int AltCtrlKeyPPage = unchecked (0x22d + LeftRightUpNPagePPage);
+    public const int AltCtrlKeyHome = unchecked (0x219 + Home);
+    public const int AltCtrlKeyEnd = unchecked (0x214 + DownEnd);
 
-		static public int ColorPair (int n)
-		{
-			return 0 + n * 256;
-		}
-	}
-#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member
+    // see #949
+    public static int LC_ALL { get; }
+    static Curses () { LC_ALL = RuntimeInformation.IsOSPlatform (OSPlatform.OSX) ? 0 : 6; }
+    public static int ColorPair (int n) { return 0 + n * 256; }
 }
+#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member

+ 48 - 139
Terminal.Gui/ConsoleDrivers/CursesDriver/handles.cs

@@ -25,153 +25,62 @@
 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 //
-using System;
 
-namespace Unix.Terminal {
+namespace Unix.Terminal;
 #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
-	public partial class Curses {
-		public class Window {
-			public readonly IntPtr Handle;
-			static Window curscr;
-			static Window stdscr;
-
-			static Window ()
-			{
-				Curses.initscr ();
-				stdscr = new Window (Curses.console_sharp_get_stdscr ());
-				curscr = new Window (Curses.console_sharp_get_curscr ());
-			}
-
-			internal Window (IntPtr handle)
-			{
-				Handle = handle;
-			}
-
-			static public Window Standard {
-				get {
-					return stdscr;
-				}
-			}
-
-			static public Window Current {
-				get {
-					return curscr;
-				}
-			}
-
-			public int wtimeout (int delay)
-			{
-				return Curses.wtimeout (Handle, delay);
-			}
-
-			public int notimeout (bool bf)
-			{
-				return Curses.notimeout (Handle, bf);
-			}
-
-			public int keypad (bool bf)
-			{
-				return Curses.keypad (Handle, bf);
-			}
-
-			public int meta (bool bf)
-			{
-				return Curses.meta (Handle, bf);
-			}
-
-			public int intrflush (bool bf)
-			{
-				return Curses.intrflush (Handle, bf);
-			}
-
-			public int clearok (bool bf)
-			{
-				return Curses.clearok (Handle, bf);
-			}
-
-			public int idlok (bool bf)
-			{
-				return Curses.idlok (Handle, bf);
-			}
-
-			public void idcok (bool bf)
-			{
-				Curses.idcok (Handle, bf);
-			}
-
-			public void immedok (bool bf)
-			{
-				Curses.immedok (Handle, bf);
-			}
-
-			public int leaveok (bool bf)
-			{
-				return Curses.leaveok (Handle, bf);
-			}
-
-			public int setscrreg (int top, int bot)
-			{
-				return Curses.wsetscrreg (Handle, top, bot);
-			}
-
-			public int scrollok (bool bf)
-			{
-				return Curses.scrollok (Handle, bf);
-			}
-
-			public int wrefresh ()
-			{
-				return Curses.wrefresh (Handle);
-			}
-
-			public int redrawwin ()
-			{
-				return Curses.redrawwin (Handle);
-			}
-
+public partial class Curses
+{
+    public class Window
+    {
+        public readonly nint Handle;
+
+        static Window ()
+        {
+            initscr ();
+            Standard = new Window (console_sharp_get_stdscr ());
+            Current = new Window (console_sharp_get_curscr ());
+        }
+
+        internal Window (nint handle) { Handle = handle; }
+        public static Window Standard { get; }
+        public static Window Current { get; }
+        public int wtimeout (int delay) { return Curses.wtimeout (Handle, delay); }
+        public int notimeout (bool bf) { return Curses.notimeout (Handle, bf); }
+        public int keypad (bool bf) { return Curses.keypad (Handle, bf); }
+        public int meta (bool bf) { return Curses.meta (Handle, bf); }
+        public int intrflush (bool bf) { return Curses.intrflush (Handle, bf); }
+        public int clearok (bool bf) { return Curses.clearok (Handle, bf); }
+        public int idlok (bool bf) { return Curses.idlok (Handle, bf); }
+        public void idcok (bool bf) { Curses.idcok (Handle, bf); }
+        public void immedok (bool bf) { Curses.immedok (Handle, bf); }
+        public int leaveok (bool bf) { return Curses.leaveok (Handle, bf); }
+        public int setscrreg (int top, int bot) { return wsetscrreg (Handle, top, bot); }
+        public int scrollok (bool bf) { return Curses.scrollok (Handle, bf); }
+        public int wrefresh () { return Curses.wrefresh (Handle); }
+        public int redrawwin () { return Curses.redrawwin (Handle); }
 #if false
 			public int wredrawwin (int beg_line, int num_lines)
 			{
 				return Curses.wredrawwin (Handle, beg_line, num_lines);
 			}
 #endif
-			public int wnoutrefresh ()
-			{
-				return Curses.wnoutrefresh (Handle);
-			}
-	
-			public int move (int line, int col)
-			{
-				return Curses.wmove (Handle, line, col);
-			}
-	
-			public int addch (char ch)
-			{
-				return Curses.waddch (Handle, ch);
-			}
-
-			//public int echochar (char ch)
-			//{
-			//	return Curses.wechochar (Handle, ch);
-			//}
-
-			public int refresh ()
-			{
-				return Curses.wrefresh (Handle);
-			}
-		}
-	
-	 	// Currently unused, to do later
-	 	internal class Screen {
-	 		public readonly IntPtr Handle;
-	 		
-	 		internal Screen (IntPtr handle)
-	 		{
-	 			Handle = handle;
-	 		}
-	 	}
+        public int wnoutrefresh () { return Curses.wnoutrefresh (Handle); }
+        public int move (int line, int col) { return wmove (Handle, line, col); }
+        public int addch (char ch) { return waddch (Handle, ch); }
+
+        //public int echochar (char ch)
+        //{
+        //	return Curses.wechochar (Handle, ch);
+        //}
+        public int refresh () { return Curses.wrefresh (Handle); }
+    }
+
+    // Currently unused, to do later
+    internal class Screen
+    {
+        public readonly nint Handle;
+        internal Screen (nint handle) { Handle = handle; }
+    }
 
 #pragma warning restore CS1591 // Missing XML comment for publicly visible type or member
-	}
-
 }

+ 124 - 113
Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqReq.cs

@@ -1,114 +1,125 @@
-using System;
-using System.Collections.Generic;
-
-namespace Terminal.Gui {
-	/// <summary>
-	/// Represents the status of an ANSI escape sequence request made to the terminal using <see cref="EscSeqRequests"/>.
-	/// </summary>
-	/// <remarks>
-	/// </remarks>
-	public class EscSeqReqStatus {
-		/// <summary>
-		/// Gets the Escape Sequence Termintor (e.g. ESC[8t ... t is the terminator).
-		/// </summary>
-		public string Terminator { get; }
-		/// <summary>
-		/// Gets the number of requests.
-		/// </summary>
-		public int NumRequests { get; }
-		/// <summary>
-		/// Gets the number of unfinished requests.
-		/// </summary>
-		public int NumOutstanding { get; set; }
-
-		/// <summary>
-		/// Creates a new state of escape sequence request.
-		/// </summary>
-		/// <param name="terminator">The terminator.</param>
-		/// <param name="numReq">The number of requests.</param>
-		public EscSeqReqStatus (string terminator, int numReq)
-		{
-			Terminator = terminator;
-			NumRequests = NumOutstanding = numReq;
-		}
-	}
-
-	// TODO: This class is a singleton. It should use the singleton pattern.
-	/// <summary>
-	/// Manages ANSI Escape Sequence requests and responses. The list of <see cref="EscSeqReqStatus"/> contains the status of the request.
-	/// Each request is identified by the terminator (e.g. ESC[8t ... t is the terminator).
-	/// </summary>
-	public class EscSeqRequests {
-		/// <summary>
-		/// Gets the <see cref="EscSeqReqStatus"/> list.
-		/// </summary>
-		public List<EscSeqReqStatus> Statuses { get; } = new List<EscSeqReqStatus> ();
-
-		/// <summary>
-		/// Adds a new request for the ANSI Escape Sequence defined by <paramref name="terminator"/>. Adds a <see cref="EscSeqReqStatus"/>
-		/// instance to <see cref="Statuses"/> list.
-		/// </summary>
-		/// <param name="terminator">The terminator.</param>
-		/// <param name="numReq">The number of requests.</param>
-		public void Add (string terminator, int numReq = 1)
-		{
-			lock (Statuses) {
-				var found = Statuses.Find (x => x.Terminator == terminator);
-				if (found == null) {
-					Statuses.Add (new EscSeqReqStatus (terminator, numReq));
-				} else if (found != null && found.NumOutstanding < found.NumRequests) {
-					found.NumOutstanding = Math.Min (found.NumOutstanding + numReq, found.NumRequests);
-				}
-			}
-		}
-
-		/// <summary>
-		/// Removes a request defined by <paramref name="terminator"/>. If a matching <see cref="EscSeqReqStatus"/> is found
-		/// and the number of outstanding
-		/// requests is greater than 0, the number of outstanding requests is decremented. If the number of outstanding requests
-		/// is 0, the <see cref="EscSeqReqStatus"/> is removed from <see cref="Statuses"/>.
-		/// </summary>
-		/// <param name="terminator">The terminating string.</param>
-		public void Remove (string terminator)
-		{
-			lock (Statuses) {
-				var found = Statuses.Find (x => x.Terminator == terminator);
-				if (found == null) {
-					return;
-				}
-				if (found != null && found.NumOutstanding == 0) {
-					Statuses.Remove (found);
-				} else if (found != null && found.NumOutstanding > 0) {
-					found.NumOutstanding--;
-					if (found.NumOutstanding == 0) {
-						Statuses.Remove (found);
-					}
-				}
-			}
-		}
-
-		/// <summary>
-		/// Indicates if a <see cref="EscSeqReqStatus"/> with the <paramref name="terminator"/> exists
-		/// in the <see cref="Statuses"/> list.
-		/// </summary>
-		/// <param name="terminator"></param>
-		/// <returns><see langword="true"/> if exist, <see langword="false"/> otherwise.</returns>
-		public bool HasResponse (string terminator)
-		{
-			lock (Statuses) {
-				var found = Statuses.Find (x => x.Terminator == terminator);
-				if (found == null) {
-					return false;
-				}
-				if (found != null && found.NumOutstanding > 0) {
-					return true;
-				} else {
-					// BUGBUG: Why does an API that returns a bool remove the entry from the list?
-					// NetDriver and Unit tests never exercise this line of code. Maybe Curses does?
-					Statuses.Remove (found);
-				}
-				return false;
-			}
-		}
-	}
+namespace Terminal.Gui;
+
+/// <summary>
+///     Represents the status of an ANSI escape sequence request made to the terminal using
+///     <see cref="EscSeqRequests"/>.
+/// </summary>
+/// <remarks></remarks>
+public class EscSeqReqStatus
+{
+    /// <summary>Creates a new state of escape sequence request.</summary>
+    /// <param name="terminator">The terminator.</param>
+    /// <param name="numReq">The number of requests.</param>
+    public EscSeqReqStatus (string terminator, int numReq)
+    {
+        Terminator = terminator;
+        NumRequests = NumOutstanding = numReq;
+    }
+
+    /// <summary>Gets the number of unfinished requests.</summary>
+    public int NumOutstanding { get; set; }
+
+    /// <summary>Gets the number of requests.</summary>
+    public int NumRequests { get; }
+
+    /// <summary>Gets the Escape Sequence Termintor (e.g. ESC[8t ... t is the terminator).</summary>
+    public string Terminator { get; }
+}
+
+// TODO: This class is a singleton. It should use the singleton pattern.
+/// <summary>
+///     Manages ANSI Escape Sequence requests and responses. The list of <see cref="EscSeqReqStatus"/> contains the
+///     status of the request. Each request is identified by the terminator (e.g. ESC[8t ... t is the terminator).
+/// </summary>
+public class EscSeqRequests
+{
+    /// <summary>Gets the <see cref="EscSeqReqStatus"/> list.</summary>
+    public List<EscSeqReqStatus> Statuses { get; } = new ();
+
+    /// <summary>
+    ///     Adds a new request for the ANSI Escape Sequence defined by <paramref name="terminator"/>. Adds a
+    ///     <see cref="EscSeqReqStatus"/> instance to <see cref="Statuses"/> list.
+    /// </summary>
+    /// <param name="terminator">The terminator.</param>
+    /// <param name="numReq">The number of requests.</param>
+    public void Add (string terminator, int numReq = 1)
+    {
+        lock (Statuses)
+        {
+            EscSeqReqStatus found = Statuses.Find (x => x.Terminator == terminator);
+
+            if (found is null)
+            {
+                Statuses.Add (new EscSeqReqStatus (terminator, numReq));
+            }
+            else if (found is { } && found.NumOutstanding < found.NumRequests)
+            {
+                found.NumOutstanding = Math.Min (found.NumOutstanding + numReq, found.NumRequests);
+            }
+        }
+    }
+
+    /// <summary>
+    ///     Indicates if a <see cref="EscSeqReqStatus"/> with the <paramref name="terminator"/> exists in the
+    ///     <see cref="Statuses"/> list.
+    /// </summary>
+    /// <param name="terminator"></param>
+    /// <returns><see langword="true"/> if exist, <see langword="false"/> otherwise.</returns>
+    public bool HasResponse (string terminator)
+    {
+        lock (Statuses)
+        {
+            EscSeqReqStatus found = Statuses.Find (x => x.Terminator == terminator);
+
+            if (found is null)
+            {
+                return false;
+            }
+
+            if (found is { } && found.NumOutstanding > 0)
+            {
+                return true;
+            }
+
+            // BUGBUG: Why does an API that returns a bool remove the entry from the list?
+            // NetDriver and Unit tests never exercise this line of code. Maybe Curses does?
+            Statuses.Remove (found);
+
+            return false;
+        }
+    }
+
+    /// <summary>
+    ///     Removes a request defined by <paramref name="terminator"/>. If a matching <see cref="EscSeqReqStatus"/> is
+    ///     found and the number of outstanding requests is greater than 0, the number of outstanding requests is decremented.
+    ///     If the number of outstanding requests is 0, the <see cref="EscSeqReqStatus"/> is removed from
+    ///     <see cref="Statuses"/>.
+    /// </summary>
+    /// <param name="terminator">The terminating string.</param>
+    public void Remove (string terminator)
+    {
+        lock (Statuses)
+        {
+            EscSeqReqStatus found = Statuses.Find (x => x.Terminator == terminator);
+
+            if (found is null)
+            {
+                return;
+            }
+
+            if (found is { } && found.NumOutstanding == 0)
+            {
+                Statuses.Remove (found);
+            }
+            else if (found is { } && found.NumOutstanding > 0)
+            {
+                found.NumOutstanding--;
+
+                if (found.NumOutstanding == 0)
+                {
+                    Statuses.Remove (found);
+                }
+            }
+        }
+    }
 }

+ 1347 - 1205
Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqUtils.cs

@@ -1,1211 +1,1353 @@
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.Linq;
-using System.Management;
-using System.Runtime.InteropServices;
-using System.Threading.Tasks;
-using static Unix.Terminal.Curses;
-
 namespace Terminal.Gui;
+
 /// <summary>
-/// Provides a platform-independent API for managing ANSI escape sequences.
+///     Provides a platform-independent API for managing ANSI escape sequences.
 /// </summary>
 /// <remarks>
-/// Useful resources:
-/// * https://learn.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences
-/// * https://invisible-island.net/xterm/ctlseqs/ctlseqs.html
-/// * https://vt100.net/
+///     Useful resources:
+///     * https://learn.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences
+///     * https://invisible-island.net/xterm/ctlseqs/ctlseqs.html
+///     * https://vt100.net/
 /// </remarks>
-public static class EscSeqUtils {
-	/// <summary>
-	/// Escape key code (ASCII 27/0x1B).
-	/// </summary>
-	public static readonly char KeyEsc = (char)KeyCode.Esc;
-
-	/// <summary>
-	/// ESC [ - The CSI (Control Sequence Introducer).
-	/// </summary>
-	public static readonly string CSI = $"{KeyEsc}[";
-
-	/// <summary>
-	/// ESC [ ? 1003 h - Enable  mouse event tracking.
-	/// </summary>
-	public static readonly string CSI_EnableAnyEventMouse = CSI + "?1003h";
-
-	/// <summary>
-	/// ESC [ ? 1006 h - Enable SGR (Select Graphic Rendition).
-	/// </summary>
-	public static readonly string CSI_EnableSgrExtModeMouse = CSI + "?1006h";
-
-	/// <summary>
-	/// ESC [ ? 1015 h - Enable URXVT (Unicode Extended Virtual Terminal).
-	/// </summary>
-	public static readonly string CSI_EnableUrxvtExtModeMouse = CSI + "?1015h";
-
-	/// <summary>
-	/// ESC [ ? 1003 l - Disable any mouse event tracking.
-	/// </summary>
-	public static readonly string CSI_DisableAnyEventMouse = CSI + "?1003l";
-
-	/// <summary>
-	/// ESC [ ? 1006 l - Disable SGR (Select Graphic Rendition).
-	/// </summary>
-	public static readonly string CSI_DisableSgrExtModeMouse = CSI + "?1006l";
-
-	/// <summary>
-	/// ESC [ ? 1015 l - Disable URXVT (Unicode Extended Virtual Terminal).
-	/// </summary>
-	public static readonly string CSI_DisableUrxvtExtModeMouse = CSI + "?1015l";
-
-	/// <summary>
-	/// Control sequence for enabling mouse events.
-	/// </summary>
-	public static string CSI_EnableMouseEvents { get; set; } =
-		CSI_EnableAnyEventMouse + CSI_EnableUrxvtExtModeMouse + CSI_EnableSgrExtModeMouse;
-
-	/// <summary>
-	/// Control sequence for disabling mouse events.
-	/// </summary>
-	public static string CSI_DisableMouseEvents { get; set; } =
-		CSI_DisableAnyEventMouse + CSI_DisableUrxvtExtModeMouse + CSI_DisableSgrExtModeMouse;
-
-	/// <summary>
-	/// ESC [ ? 1047 h - Activate xterm alternative buffer (no backscroll)
-	/// </summary>
-	/// <remarks>
-	/// From https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Functions-using-CSI-_-ordered-by-the-final-character_s_
-	/// Use Alternate Screen Buffer, xterm. 
-	/// </remarks>
-	public static readonly string CSI_ActivateAltBufferNoBackscroll = CSI + "?1047h";
-
-	/// <summary>
-	/// ESC [ ? 1047 l - Restore xterm working buffer (with backscroll)
-	/// </summary>
-	/// <remarks>
-	/// From https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Functions-using-CSI-_-ordered-by-the-final-character_s_
-	/// Use Normal Screen Buffer, xterm.  Clear the screen first if in the Alternate Screen Buffer.
-	/// </remarks>
-	public static readonly string CSI_RestoreAltBufferWithBackscroll = CSI + "?1047l";
-
-	/// <summary>
-	/// ESC [ ? 1049 h - Save cursor position and activate xterm alternative buffer (no backscroll)
-	/// </summary>
-	/// <remarks>
-	/// From https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Functions-using-CSI-_-ordered-by-the-final-character_s_
-	/// Save cursor as in DECSC, xterm. After saving the cursor, switch to the Alternate Screen Buffer,
-	/// clearing it first.
-	/// This control combines the effects of the 1047 and 1048 modes.
-	/// Use this with terminfo-based applications rather than the 47 mode.
-	/// </remarks>
-	public static readonly string CSI_SaveCursorAndActivateAltBufferNoBackscroll = CSI + "?1049h";
-
-	/// <summary>
-	/// ESC [ ? 1049 l - Restore cursor position and restore xterm working buffer (with backscroll)
-	/// </summary>
-	/// <remarks>
-	/// From https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Functions-using-CSI-_-ordered-by-the-final-character_s_
-	/// Use Normal Screen Buffer and restore cursor as in DECRC, xterm.
-	/// resource.This combines the effects of the 1047 and 1048  modes.
-	/// </remarks>
-	public static readonly string CSI_RestoreCursorAndRestoreAltBufferWithBackscroll = CSI + "?1049l";
-
-	/// <summary>
-	/// Options for ANSI ESC "[xJ" - Clears part of the screen.
-	/// </summary>
-	public enum ClearScreenOptions {
-		/// <summary>
-		/// If n is 0 (or missing), clear from cursor to end of screen.
-		/// </summary>
-		CursorToEndOfScreen = 0,
-		/// <summary>
-		/// If n is 1, clear from cursor to beginning of the screen.
-		/// </summary>
-		CursorToBeginningOfScreen = 1,
-		/// <summary>
-		/// If n is 2, clear entire screen (and moves cursor to upper left on DOS ANSI.SYS).
-		/// </summary>
-		EntireScreen = 2,
-		/// <summary>
-		/// If n is 3, clear entire screen and delete all lines saved in the scrollback buffer
-		/// </summary>
-		EntireScreenAndScrollbackBuffer = 3
-	}
-
-	/// <summary>
-	/// ESC [ x J - Clears part of the screen. See <see cref="ClearScreenOptions"/>.
-	/// </summary>
-	/// <param name="option"></param>
-	/// <returns></returns>
-	public static string CSI_ClearScreen (ClearScreenOptions option) => $"{CSI}{(int)option}J";
-
-	#region Cursor
-	//ESC [ M - RI Reverse Index – Performs the reverse operation of \n, moves cursor up one line, maintains horizontal position, scrolls buffer if necessary*
-
-	/// <summary>
-	/// ESC [ 7 - Save Cursor Position in Memory**
-	/// </summary>
-	public static readonly string CSI_SaveCursorPosition = CSI + "7";
-
-	/// <summary>
-	/// ESC [ 8 - DECSR Restore Cursor Position from Memory**
-	/// </summary>
-	public static readonly string CSI_RestoreCursorPosition = CSI + "8";
-
-	/// <summary>
-	/// ESC [ 8 ; height ; width t - Set Terminal Window Size
-	/// https://terminalguide.namepad.de/seq/csi_st-8/
-	/// </summary>
-	public static string CSI_SetTerminalWindowSize (int height, int width) => $"{CSI}8;{height};{width}t";
-
-	//ESC [ < n > A - CUU - Cursor Up       Cursor up by < n >
-	//ESC [ < n > B - CUD - Cursor Down     Cursor down by < n >
-	//ESC [ < n > C - CUF - Cursor Forward  Cursor forward (Right) by < n >
-	//ESC [ < n > D - CUB - Cursor Backward Cursor backward (Left) by < n >
-	//ESC [ < n > E - CNL - Cursor Next Line - Cursor down < n > lines from current position
-	//ESC [ < n > F - CPL - Cursor Previous Line    Cursor up < n > lines from current position
-	//ESC [ < n > G - CHA - Cursor Horizontal Absolute      Cursor moves to < n > th position horizontally in the current line
-	//ESC [ < n > d - VPA - Vertical Line Position Absolute Cursor moves to the < n > th position vertically in the current column
-
-	/// <summary>
-	/// ESC [ y ; x H - CUP Cursor Position - Cursor moves to x ; y coordinate within the viewport, where x is the column of the y line
-	/// </summary>
-	/// <param name="row">Origin is (1,1).</param>
-	/// <param name="col">Origin is (1,1).</param>
-	/// <returns></returns>
-	public static string CSI_SetCursorPosition (int row, int col) => $"{CSI}{row};{col}H";
-
-
-	//ESC [ <y> ; <x> f - HVP     Horizontal Vertical Position* Cursor moves to<x>; <y> coordinate within the viewport, where <x> is the column of the<y> line
-	//ESC [ s - ANSISYSSC       Save Cursor – Ansi.sys emulation	**With no parameters, performs a save cursor operation like DECSC
-	//ESC [ u - ANSISYSRC       Restore Cursor – Ansi.sys emulation	**With no parameters, performs a restore cursor operation like DECRC
-	//ESC [ ? 12 h - ATT160  Text Cursor Enable Blinking     Start the cursor blinking
-	//ESC [ ? 12 l - ATT160  Text Cursor Disable Blinking    Stop blinking the cursor
-	/// <summary>
-	/// ESC [ ? 25 h - DECTCEM Text Cursor Enable Mode Show    Show the cursor
-	/// </summary>
-	public static readonly string CSI_ShowCursor = CSI + "?25h";
-
-	/// <summary>
-	/// ESC [ ? 25 l - DECTCEM Text Cursor Enable Mode Hide    Hide the cursor
-	/// </summary>
-	public static readonly string CSI_HideCursor = CSI + "?25l";
-	//ESC [ ? 12 h - ATT160  Text Cursor Enable Blinking     Start the cursor blinking
-	//ESC [ ? 12 l - ATT160  Text Cursor Disable Blinking    Stop blinking the cursor
-	//ESC [ ? 25 h - DECTCEM Text Cursor Enable Mode Show    Show the cursor
-	//ESC [ ? 25 l - DECTCEM Text Cursor Enable Mode Hide    Hide the cursor
-
-	/// <summary>
-	/// Styles for ANSI ESC "[x q" - Set Cursor Style
-	/// </summary>
-	public enum DECSCUSR_Style {
-		/// <summary>
-		/// DECSCUSR - User Shape - Default cursor shape configured by the user
-		/// </summary>
-		UserShape = 0,
-		/// <summary>
-		/// DECSCUSR - Blinking Block - Blinking block cursor shape
-		/// </summary>
-		BlinkingBlock = 1,
-		/// <summary>
-		/// DECSCUSR - Steady Block - Steady block cursor shape
-		/// </summary>
-		SteadyBlock = 2,
-		/// <summary>
-		/// DECSCUSR - Blinking Underline - Blinking underline cursor shape
-		/// </summary>
-		BlinkingUnderline = 3,
-		/// <summary>
-		/// DECSCUSR - Steady Underline - Steady underline cursor shape
-		/// </summary>
-		SteadyUnderline = 4,
-		/// <summary>
-		/// DECSCUSR - Blinking Bar - Blinking bar cursor shape
-		/// </summary>
-		BlinkingBar = 5,
-		/// <summary>
-		/// DECSCUSR - Steady Bar - Steady bar cursor shape
-		/// </summary>
-		SteadyBar = 6
-	}
-
-	/// <summary>
-	/// ESC [ n SP q - Select Cursor Style (DECSCUSR)
-	/// https://terminalguide.namepad.de/seq/csi_sq_t_space/ 
-	/// </summary>
-	/// <param name="style"></param>
-	/// <returns></returns>
-	public static string CSI_SetCursorStyle (DECSCUSR_Style style) => $"{CSI}{(int)style} q";
-
-	#endregion
-
-	#region Colors
-	/// <summary>
-	/// ESC [ (n) m - SGR - Set Graphics Rendition - Set the format of the screen and text as specified by (n)
-	/// This command is special in that the (n) position can accept between 0 and 16 parameters separated by semicolons.
-	/// When no parameters are specified, it is treated the same as a single 0 parameter.
-	/// https://terminalguide.namepad.de/seq/csi_sm/
-	/// </summary>
-	public static string CSI_SetGraphicsRendition (params int [] parameters) => $"{CSI}{string.Join (";", parameters)}m";
-
-	/// <summary>
-	/// ESC [ (n) m - Uses <see cref="CSI_SetGraphicsRendition(int[])" /> to set the foreground color.
-	/// </summary>
-	/// <param name="code">One of the 16 color codes.</param>
-	/// <returns></returns>
-	public static string CSI_SetForegroundColor (AnsiColorCode code) => CSI_SetGraphicsRendition ((int)code);
-
-	/// <summary>
-	/// ESC [ (n) m - Uses <see cref="CSI_SetGraphicsRendition(int[])" /> to set the background color.
-	/// </summary>
-	/// <param name="code">One of the 16 color codes.</param>
-	/// <returns></returns>
-	public static string CSI_SetBackgroundColor (AnsiColorCode code) => CSI_SetGraphicsRendition ((int)code+10);
-
-	/// <summary>
-	/// ESC[38;5;{id}m - Set foreground color (256 colors)
-	/// </summary>
-	public static string CSI_SetForegroundColor256 (int color) => $"{CSI}38;5;{color}m";
-
-	/// <summary>
-	/// ESC[48;5;{id}m - Set background color (256 colors)
-	/// </summary>
-	public static string CSI_SetBackgroundColor256 (int color) => $"{CSI}48;5;{color}m";
-
-	/// <summary>
-	/// ESC[38;2;{r};{g};{b}m	Set foreground color as RGB.
-	/// </summary>
-	public static string CSI_SetForegroundColorRGB (int r, int g, int b) => $"{CSI}38;2;{r};{g};{b}m";
-
-	/// <summary>
-	/// ESC[48;2;{r};{g};{b}m	Set background color as RGB.
-	/// </summary>
-	public static string CSI_SetBackgroundColorRGB (int r, int g, int b) => $"{CSI}48;2;{r};{g};{b}m";
-
-	#endregion
-
-	#region Requests
-	/// <summary>
-	/// ESC [ ? 6 n - Request Cursor Position Report (?) (DECXCPR)
-	/// https://terminalguide.namepad.de/seq/csi_sn__p-6/
-	/// </summary>
-	public static readonly string CSI_RequestCursorPositionReport = CSI + "?6n";
-
-	/// <summary>
-	/// The terminal reply to <see cref="CSI_RequestCursorPositionReport"/>. ESC [ ? (y) ; (x) R
-	/// </summary>
-	public const string CSI_RequestCursorPositionReport_Terminator = "R";
-
-	/// <summary>
-	/// ESC [ 0 c - Send Device Attributes (Primary DA)
-	///
-	/// https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Application-Program-Command-functions
-	/// https://www.xfree86.org/current/ctlseqs.html
-	/// Windows Terminal v1.17 and below emits “\x1b[?1;0c”, indicating "VT101 with No Options".
-	/// Windows Terminal v1.18+ emits: \x1b[?61;6;7;22;23;24;28;32;42c"
-	/// See https://github.com/microsoft/terminal/pull/14906
-	///
-	/// 61 - The device conforms to level 1 of the character cell display architecture
-	/// (See https://github.com/microsoft/terminal/issues/15693#issuecomment-1633304497)
-	/// 6 = Selective erase
-	/// 7 = Soft fonts
-	/// 22 = Color text
-	/// 23 = Greek character sets
-	/// 24 = Turkish character sets
-	/// 28 = Rectangular area operations
-	/// 32 = Text macros
-	/// 42 = ISO Latin-2 character set
-	/// 
-	/// </summary>
-	public static readonly string CSI_SendDeviceAttributes = CSI + "0c";
-
-	/// <summary>
-	/// ESC [ > 0 c - Send Device Attributes (Secondary DA)
-	/// Windows Terminal v1.18+ emits: "\x1b[>0;10;1c" (vt100, firmware version 1.0, vt220)
-	/// </summary>
-	public static readonly string CSI_SendDeviceAttributes2 = CSI + ">0c";
-
-	/// <summary>
-	/// The terminator indicating a reply to <see cref="CSI_SendDeviceAttributes"/> or <see cref="CSI_SendDeviceAttributes2"/>
-	/// 
-	/// </summary>
-	public const string CSI_ReportDeviceAttributes_Terminator = "c";
-
-	/// <summary>
-	/// CSI 1 8 t  | yes | yes |  yes  | report window size in chars
-	/// https://terminalguide.namepad.de/seq/csi_st-18/
-	/// </summary>
-	public static readonly string CSI_ReportTerminalSizeInChars = CSI + "18t";
-
-	/// <summary>
-	/// The terminator indicating a reply to <see cref="CSI_ReportTerminalSizeInChars"/> : ESC [ 8 ; height ; width t
-	/// </summary>
-	public const string CSI_ReportTerminalSizeInChars_Terminator = "t";
-
-	/// <summary>
-	/// The value of the response to <see cref="CSI_ReportTerminalSizeInChars"/> indicating value 1 and 2 are the terminal size in chars.
-	/// </summary>
-	public const string CSI_ReportTerminalSizeInChars_ResponseValue = "8";
-	#endregion
-
-	/// <summary>
-	/// Ensures a console key is mapped to one that works correctly with ANSI escape sequences.
-	/// </summary>
-	/// <param name="consoleKeyInfo">The <see cref="ConsoleKeyInfo"/>.</param>
-	/// <returns>The <see cref="ConsoleKeyInfo"/> modified.</returns>
-	public static ConsoleKeyInfo MapConsoleKeyInfo (ConsoleKeyInfo consoleKeyInfo)
-	{
-		ConsoleKeyInfo newConsoleKeyInfo = consoleKeyInfo;
-		ConsoleKey key;
-		var keyChar = consoleKeyInfo.KeyChar;
-		switch ((uint)keyChar) {
-		case 0:
-			if (consoleKeyInfo.Key == (ConsoleKey)64) {    // Ctrl+Space in Windows.
-				newConsoleKeyInfo = new ConsoleKeyInfo (' ', ConsoleKey.Spacebar,
-					(consoleKeyInfo.Modifiers & ConsoleModifiers.Shift) != 0,
-					(consoleKeyInfo.Modifiers & ConsoleModifiers.Alt) != 0,
-					(consoleKeyInfo.Modifiers & ConsoleModifiers.Control) != 0);
-			}
-			break;
-		case uint n when n > 0 && n <= KeyEsc:
-			if (consoleKeyInfo.Key == 0 && consoleKeyInfo.KeyChar == '\r') {
-				key = ConsoleKey.Enter;
-				newConsoleKeyInfo = new ConsoleKeyInfo (consoleKeyInfo.KeyChar,
-					key,
-					(consoleKeyInfo.Modifiers & ConsoleModifiers.Shift) != 0,
-					(consoleKeyInfo.Modifiers & ConsoleModifiers.Alt) != 0,
-					(consoleKeyInfo.Modifiers & ConsoleModifiers.Control) != 0);
-			} else if (consoleKeyInfo.Key == 0) {
-				key = (ConsoleKey)(char)(consoleKeyInfo.KeyChar + (uint)ConsoleKey.A - 1);
-				newConsoleKeyInfo = new ConsoleKeyInfo ((char)key,
-					key,
-					(consoleKeyInfo.Modifiers & ConsoleModifiers.Shift) != 0,
-					(consoleKeyInfo.Modifiers & ConsoleModifiers.Alt) != 0,
-					true);
-			}
-			break;
-		case 127: // DEL
-			newConsoleKeyInfo = new ConsoleKeyInfo (consoleKeyInfo.KeyChar, ConsoleKey.Backspace,
-				(consoleKeyInfo.Modifiers & ConsoleModifiers.Shift) != 0,
-				(consoleKeyInfo.Modifiers & ConsoleModifiers.Alt) != 0,
-				(consoleKeyInfo.Modifiers & ConsoleModifiers.Control) != 0);
-			break;
-		default:
-			newConsoleKeyInfo = consoleKeyInfo;
-			break;
-		}
-
-		return newConsoleKeyInfo;
-	}
-
-	/// <summary>
-	/// A helper to resize the <see cref="ConsoleKeyInfo"/> as needed.
-	/// </summary>
-	/// <param name="consoleKeyInfo">The <see cref="ConsoleKeyInfo"/>.</param>
-	/// <param name="cki">The <see cref="ConsoleKeyInfo"/> array to resize.</param>
-	/// <returns>The <see cref="ConsoleKeyInfo"/> resized.</returns>
-	public static ConsoleKeyInfo [] ResizeArray (ConsoleKeyInfo consoleKeyInfo, ConsoleKeyInfo [] cki)
-	{
-		Array.Resize (ref cki, cki == null ? 1 : cki.Length + 1);
-		cki [cki.Length - 1] = consoleKeyInfo;
-		return cki;
-	}
-
-	/// <summary>
-	/// Decodes an ANSI escape sequence.
-	/// </summary>
-	/// <param name="escSeqRequests">The <see cref="EscSeqRequests"/> which may contain a request.</param>
-	/// <param name="newConsoleKeyInfo">The <see cref="ConsoleKeyInfo"/> which may changes.</param>
-	/// <param name="key">The <see cref="ConsoleKey"/> which may changes.</param>
-	/// <param name="cki">The <see cref="ConsoleKeyInfo"/> array.</param>
-	/// <param name="mod">The <see cref="ConsoleModifiers"/> which may changes.</param>
-	/// <param name="c1Control">The control returned by the <see cref="GetC1ControlChar(char)"/> method.</param>
-	/// <param name="code">The code returned by the <see cref="GetEscapeResult(char[])"/> method.</param>
-	/// <param name="values">The values returned by the <see cref="GetEscapeResult(char[])"/> method.</param>
-	/// <param name="terminator">The terminator returned by the <see cref="GetEscapeResult(char[])"/> method.</param>
-	/// <param name="isMouse">Indicates if the escape sequence is a mouse event.</param>
-	/// <param name="buttonState">The <see cref="MouseFlags"/> button state.</param>
-	/// <param name="pos">The <see cref="MouseFlags"/> position.</param>
-	/// <param name="isResponse">Indicates if the escape sequence is a response to a request.</param>
-	/// <param name="continuousButtonPressedHandler">The handler that will process the event.</param>
-	public static void DecodeEscSeq (EscSeqRequests escSeqRequests, ref ConsoleKeyInfo newConsoleKeyInfo, ref ConsoleKey key, ConsoleKeyInfo [] cki, ref ConsoleModifiers mod, out string c1Control, out string code, out string [] values, out string terminator, out bool isMouse, out List<MouseFlags> buttonState, out Point pos, out bool isResponse, Action<MouseFlags, Point> continuousButtonPressedHandler)
-	{
-		char [] kChars = GetKeyCharArray (cki);
-		(c1Control, code, values, terminator) = GetEscapeResult (kChars);
-		isMouse = false;
-		buttonState = new List<MouseFlags> () { 0 };
-		pos = default;
-		isResponse = false;
-		switch (c1Control) {
-		case "ESC":
-			if (values == null && string.IsNullOrEmpty (terminator)) {
-				key = ConsoleKey.Escape;
-				newConsoleKeyInfo = new ConsoleKeyInfo (cki [0].KeyChar, key,
-					(mod & ConsoleModifiers.Shift) != 0,
-					(mod & ConsoleModifiers.Alt) != 0,
-					(mod & ConsoleModifiers.Control) != 0);
-			} else if ((uint)cki [1].KeyChar >= 1 && (uint)cki [1].KeyChar <= 26) {
-				key = (ConsoleKey)(char)(cki [1].KeyChar + (uint)ConsoleKey.A - 1);
-				newConsoleKeyInfo = new ConsoleKeyInfo (cki [1].KeyChar,
-					key,
-					false,
-					true,
-					true);
-			} else {
-				if (cki [1].KeyChar >= 97 && cki [1].KeyChar <= 122) {
-					key = (ConsoleKey)cki [1].KeyChar.ToString ().ToUpper () [0];
-				} else {
-					key = (ConsoleKey)cki [1].KeyChar;
-				}
-				newConsoleKeyInfo = new ConsoleKeyInfo ((char)key,
-					(ConsoleKey)Math.Min ((uint)key, 255),
-					false,
-					true,
-					false);
-			}
-			break;
-		case "SS3":
-			key = GetConsoleKey (terminator [0], values [0], ref mod);
-			newConsoleKeyInfo = new ConsoleKeyInfo ('\0',
-				key,
-				(mod & ConsoleModifiers.Shift) != 0,
-				(mod & ConsoleModifiers.Alt) != 0,
-				(mod & ConsoleModifiers.Control) != 0);
-			break;
-		case "CSI":
-			if (!string.IsNullOrEmpty (code) && code == "<") {
-				GetMouse (cki, out buttonState, out pos, continuousButtonPressedHandler);
-				isMouse = true;
-				return;
-			} else if (escSeqRequests != null && escSeqRequests.HasResponse (terminator)) {
-				isResponse = true;
-				escSeqRequests.Remove (terminator);
-				return;
-			}
-			if (!string.IsNullOrEmpty (terminator)) {
-				key = GetConsoleKey (terminator [0], values [0], ref mod);
-				if (key != 0 && values.Length > 1) {
-					mod |= GetConsoleModifiers (values [1]);
-				}
-				newConsoleKeyInfo = new ConsoleKeyInfo ('\0',
-					key,
-					(mod & ConsoleModifiers.Shift) != 0,
-					(mod & ConsoleModifiers.Alt) != 0,
-					(mod & ConsoleModifiers.Control) != 0);
-			} else {
-				// BUGBUG: See https://github.com/gui-cs/Terminal.Gui/issues/2803
-				// This is caused by NetDriver depending on Console.KeyAvailable?
-				throw new InvalidOperationException ($"CSI response, but there's no terminator");
-				//newConsoleKeyInfo = new ConsoleKeyInfo ('\0',
-				//	key,
-				//	(mod & ConsoleModifiers.Shift) != 0,
-				//	(mod & ConsoleModifiers.Alt) != 0,
-				//	(mod & ConsoleModifiers.Control) != 0);
-			}
-			break;
-		}
-	}
-
-	/// <summary>
-	/// Gets all the needed information about a escape sequence.
-	/// </summary>
-	/// <param name="kChar">The array with all chars.</param>
-	/// <returns>
-	/// The c1Control returned by <see cref="GetC1ControlChar(char)"/>, code, values and terminating.
-	/// </returns>
-	public static (string c1Control, string code, string [] values, string terminating) GetEscapeResult (char [] kChar)
-	{
-		if (kChar == null || kChar.Length == 0) {
-			return (null, null, null, null);
-		}
-		if (kChar [0] != KeyEsc) {
-			throw new InvalidOperationException ("Invalid escape character!");
-		}
-		if (kChar.Length == 1) {
-			return ("ESC", null, null, null);
-		}
-		if (kChar.Length == 2) {
-			return ("ESC", null, null, kChar [1].ToString ());
-		}
-		string c1Control = GetC1ControlChar (kChar [1]);
-		string code = null;
-		int nSep = kChar.Count (x => x == ';') + 1;
-		string [] values = new string [nSep];
-		int valueIdx = 0;
-		string terminating = "";
-		for (int i = 2; i < kChar.Length; i++) {
-			var c = kChar [i];
-			if (char.IsDigit (c)) {
-				values [valueIdx] += c.ToString ();
-			} else if (c == ';') {
-				valueIdx++;
-			} else if (valueIdx == nSep - 1 || i == kChar.Length - 1) {
-				terminating += c.ToString ();
-			} else {
-				code += c.ToString ();
-			}
-		}
-
-		return (c1Control, code, values, terminating);
-	}
-
-	/// <summary>
-	/// Gets the c1Control used in the called escape sequence.
-	/// </summary>
-	/// <param name="c">The char used.</param>
-	/// <returns>The c1Control.</returns>
-	public static string GetC1ControlChar (char c)
-	{
-		// These control characters are used in the vtXXX emulation.
-		switch (c) {
-		case 'D':
-			return "IND"; // Index
-		case 'E':
-			return "NEL"; // Next Line
-		case 'H':
-			return "HTS"; // Tab Set
-		case 'M':
-			return "RI"; // Reverse Index
-		case 'N':
-			return "SS2"; // Single Shift Select of G2 Character Set: affects next character only
-		case 'O':
-			return "SS3"; // Single Shift Select of G3 Character Set: affects next character only
-		case 'P':
-			return "DCS"; // Device Control String
-		case 'V':
-			return "SPA"; // Start of Guarded Area
-		case 'W':
-			return "EPA"; // End of Guarded Area
-		case 'X':
-			return "SOS"; // Start of String
-		case 'Z':
-			return "DECID"; // Return Terminal ID Obsolete form of CSI c (DA)
-		case '[':
-			return "CSI"; // Control Sequence Introducer
-		case '\\':
-			return "ST"; // String Terminator
-		case ']':
-			return "OSC"; // Operating System Command
-		case '^':
-			return "PM"; // Privacy Message
-		case '_':
-			return "APC"; // Application Program Command
-		default:
-			return ""; // Not supported
-		}
-	}
-
-	/// <summary>
-	/// Gets the <see cref="ConsoleModifiers"/> from the value.
-	/// </summary>
-	/// <param name="value">The value.</param>
-	/// <returns>The <see cref="ConsoleModifiers"/> or zero.</returns>
-	public static ConsoleModifiers GetConsoleModifiers (string value)
-	{
-		switch (value) {
-		case "2":
-			return ConsoleModifiers.Shift;
-		case "3":
-			return ConsoleModifiers.Alt;
-		case "4":
-			return ConsoleModifiers.Shift | ConsoleModifiers.Alt;
-		case "5":
-			return ConsoleModifiers.Control;
-		case "6":
-			return ConsoleModifiers.Shift | ConsoleModifiers.Control;
-		case "7":
-			return ConsoleModifiers.Alt | ConsoleModifiers.Control;
-		case "8":
-			return ConsoleModifiers.Shift | ConsoleModifiers.Alt | ConsoleModifiers.Control;
-		default:
-			return 0;
-		}
-	}
-
-	/// <summary>
-	/// Gets the <see cref="ConsoleKey"/> depending on terminating and value.
-	/// </summary>
-	/// <param name="terminator">The terminator indicating a reply to <see cref="CSI_SendDeviceAttributes"/> or <see cref="CSI_SendDeviceAttributes2"/>.</param>
-	/// <param name="value">The value.</param>
-	/// <param name="mod">The <see cref="ConsoleModifiers"/> which may changes.</param>
-	/// <returns>The <see cref="ConsoleKey"/> and probably the <see cref="ConsoleModifiers"/>.</returns>
-	public static ConsoleKey GetConsoleKey (char terminator, string value, ref ConsoleModifiers mod)
-	{
-		ConsoleKey key;
-		switch (terminator) {
-		case 'A':
-			key = ConsoleKey.UpArrow;
-			break;
-		case 'B':
-			key = ConsoleKey.DownArrow;
-			break;
-		case 'C':
-			key = ConsoleKey.RightArrow;
-			break;
-		case 'D':
-			key = ConsoleKey.LeftArrow;
-			break;
-		case 'F':
-			key = ConsoleKey.End;
-			break;
-		case 'H':
-			key = ConsoleKey.Home;
-			break;
-		case 'P':
-			key = ConsoleKey.F1;
-			break;
-		case 'Q':
-			key = ConsoleKey.F2;
-			break;
-		case 'R':
-			key = ConsoleKey.F3;
-			break;
-		case 'S':
-			key = ConsoleKey.F4;
-			break;
-		case 'Z':
-			key = ConsoleKey.Tab;
-			mod |= ConsoleModifiers.Shift;
-			break;
-		case '~':
-			switch (value) {
-			case "2":
-				key = ConsoleKey.Insert;
-				break;
-			case "3":
-				key = ConsoleKey.Delete;
-				break;
-			case "5":
-				key = ConsoleKey.PageUp;
-				break;
-			case "6":
-				key = ConsoleKey.PageDown;
-				break;
-			case "15":
-				key = ConsoleKey.F5;
-				break;
-			case "17":
-				key = ConsoleKey.F6;
-				break;
-			case "18":
-				key = ConsoleKey.F7;
-				break;
-			case "19":
-				key = ConsoleKey.F8;
-				break;
-			case "20":
-				key = ConsoleKey.F9;
-				break;
-			case "21":
-				key = ConsoleKey.F10;
-				break;
-			case "23":
-				key = ConsoleKey.F11;
-				break;
-			case "24":
-				key = ConsoleKey.F12;
-				break;
-			default:
-				key = 0;
-				break;
-			}
-			break;
-		default:
-			key = 0;
-			break;
-		}
-
-		return key;
-	}
-
-	/// <summary>
-	/// A helper to get only the <see cref="ConsoleKeyInfo.KeyChar"/> from the <see cref="ConsoleKeyInfo"/> array.
-	/// </summary>
-	/// <param name="cki"></param>
-	/// <returns>The char array of the escape sequence.</returns>
-	public static char [] GetKeyCharArray (ConsoleKeyInfo [] cki)
-	{
-		char [] kChar = new char [] { };
-		var length = 0;
-		foreach (var kc in cki) {
-			length++;
-			Array.Resize (ref kChar, length);
-			kChar [length - 1] = kc.KeyChar;
-		}
-
-		return kChar;
-	}
-
-	private static MouseFlags? lastMouseButtonPressed;
-	//private static MouseFlags? lastMouseButtonReleased;
-	private static bool isButtonPressed;
-	//private static bool isButtonReleased;
-	private static bool isButtonClicked;
-	private static bool isButtonDoubleClicked;
-	private static bool isButtonTripleClicked;
-	private static Point point;
-
-	/// <summary>
-	/// Gets the <see cref="MouseFlags"/> mouse button flags and the position.
-	/// </summary>
-	/// <param name="cki">The <see cref="ConsoleKeyInfo"/> array.</param>
-	/// <param name="mouseFlags">The mouse button flags.</param>
-	/// <param name="pos">The mouse position.</param>
-	/// <param name="continuousButtonPressedHandler">The handler that will process the event.</param>
-	public static void GetMouse (ConsoleKeyInfo [] cki, out List<MouseFlags> mouseFlags, out Point pos, Action<MouseFlags, Point> continuousButtonPressedHandler)
-	{
-		MouseFlags buttonState = 0;
-		pos = new Point ();
-		int buttonCode = 0;
-		bool foundButtonCode = false;
-		int foundPoint = 0;
-		string value = "";
-		var kChar = GetKeyCharArray (cki);
-		//System.Diagnostics.Debug.WriteLine ($"kChar: {new string (kChar)}");
-		for (int i = 0; i < kChar.Length; i++) {
-			var c = kChar [i];
-			if (c == '<') {
-				foundButtonCode = true;
-			} else if (foundButtonCode && c != ';') {
-				value += c.ToString ();
-			} else if (c == ';') {
-				if (foundButtonCode) {
-					foundButtonCode = false;
-					buttonCode = int.Parse (value);
-				}
-				if (foundPoint == 1) {
-					pos.X = int.Parse (value) - 1;
-				}
-				value = "";
-				foundPoint++;
-			} else if (foundPoint > 0 && c != 'm' && c != 'M') {
-				value += c.ToString ();
-			} else if (c == 'm' || c == 'M') {
-				//pos.Y = int.Parse (value) + Console.WindowTop - 1;
-				pos.Y = int.Parse (value) - 1;
-
-				switch (buttonCode) {
-				case 0:
-				case 8:
-				case 16:
-				case 24:
-				case 32:
-				case 36:
-				case 40:
-				case 48:
-				case 56:
-					buttonState = c == 'M' ? MouseFlags.Button1Pressed
-						: MouseFlags.Button1Released;
-					break;
-				case 1:
-				case 9:
-				case 17:
-				case 25:
-				case 33:
-				case 37:
-				case 41:
-				case 45:
-				case 49:
-				case 53:
-				case 57:
-				case 61:
-					buttonState = c == 'M' ? MouseFlags.Button2Pressed
-						: MouseFlags.Button2Released;
-					break;
-				case 2:
-				case 10:
-				case 14:
-				case 18:
-				case 22:
-				case 26:
-				case 30:
-				case 34:
-				case 42:
-				case 46:
-				case 50:
-				case 54:
-				case 58:
-				case 62:
-					buttonState = c == 'M' ? MouseFlags.Button3Pressed
-						: MouseFlags.Button3Released;
-					break;
-				case 35:
-				//// Needed for Windows OS
-				//if (isButtonPressed && c == 'm'
-				//	&& (lastMouseEvent.ButtonState == MouseFlags.Button1Pressed
-				//	|| lastMouseEvent.ButtonState == MouseFlags.Button2Pressed
-				//	|| lastMouseEvent.ButtonState == MouseFlags.Button3Pressed)) {
-
-				//	switch (lastMouseEvent.ButtonState) {
-				//	case MouseFlags.Button1Pressed:
-				//		buttonState = MouseFlags.Button1Released;
-				//		break;
-				//	case MouseFlags.Button2Pressed:
-				//		buttonState = MouseFlags.Button2Released;
-				//		break;
-				//	case MouseFlags.Button3Pressed:
-				//		buttonState = MouseFlags.Button3Released;
-				//		break;
-				//	}
-				//} else {
-				//	buttonState = MouseFlags.ReportMousePosition;
-				//}
-				//break;
-				case 39:
-				case 43:
-				case 47:
-				case 51:
-				case 55:
-				case 59:
-				case 63:
-					buttonState = MouseFlags.ReportMousePosition;
-					break;
-				case 64:
-					buttonState = MouseFlags.WheeledUp;
-					break;
-				case 65:
-					buttonState = MouseFlags.WheeledDown;
-					break;
-				case 68:
-				case 72:
-				case 80:
-					buttonState = MouseFlags.WheeledLeft;       // Shift/Ctrl+WheeledUp
-					break;
-				case 69:
-				case 73:
-				case 81:
-					buttonState = MouseFlags.WheeledRight;      // Shift/Ctrl+WheeledDown
-					break;
-				}
-				// Modifiers.
-				switch (buttonCode) {
-				case 8:
-				case 9:
-				case 10:
-				case 43:
-					buttonState |= MouseFlags.ButtonAlt;
-					break;
-				case 14:
-				case 47:
-					buttonState |= MouseFlags.ButtonAlt | MouseFlags.ButtonShift;
-					break;
-				case 16:
-				case 17:
-				case 18:
-				case 51:
-					buttonState |= MouseFlags.ButtonCtrl;
-					break;
-				case 22:
-				case 55:
-					buttonState |= MouseFlags.ButtonCtrl | MouseFlags.ButtonShift;
-					break;
-				case 24:
-				case 25:
-				case 26:
-				case 59:
-					buttonState |= MouseFlags.ButtonCtrl | MouseFlags.ButtonAlt;
-					break;
-				case 30:
-				case 63:
-					buttonState |= MouseFlags.ButtonCtrl | MouseFlags.ButtonShift | MouseFlags.ButtonAlt;
-					break;
-				case 32:
-				case 33:
-				case 34:
-					buttonState |= MouseFlags.ReportMousePosition;
-					break;
-				case 36:
-				case 37:
-					buttonState |= MouseFlags.ReportMousePosition | MouseFlags.ButtonShift;
-					break;
-				case 39:
-				case 68:
-				case 69:
-					buttonState |= MouseFlags.ButtonShift;
-					break;
-				case 40:
-				case 41:
-				case 42:
-					buttonState |= MouseFlags.ReportMousePosition | MouseFlags.ButtonAlt;
-					break;
-				case 45:
-				case 46:
-					buttonState |= MouseFlags.ReportMousePosition | MouseFlags.ButtonAlt | MouseFlags.ButtonShift;
-					break;
-				case 48:
-				case 49:
-				case 50:
-					buttonState |= MouseFlags.ReportMousePosition | MouseFlags.ButtonCtrl;
-					break;
-				case 53:
-				case 54:
-					buttonState |= MouseFlags.ReportMousePosition | MouseFlags.ButtonCtrl | MouseFlags.ButtonShift;
-					break;
-				case 56:
-				case 57:
-				case 58:
-					buttonState |= MouseFlags.ReportMousePosition | MouseFlags.ButtonCtrl | MouseFlags.ButtonAlt;
-					break;
-				case 61:
-				case 62:
-					buttonState |= MouseFlags.ReportMousePosition | MouseFlags.ButtonCtrl | MouseFlags.ButtonShift | MouseFlags.ButtonAlt;
-					break;
-				}
-			}
-		}
-
-		mouseFlags = new List<MouseFlags> () { MouseFlags.AllEvents };
-
-		if (lastMouseButtonPressed != null && !isButtonPressed && !buttonState.HasFlag (MouseFlags.ReportMousePosition)
-			&& !buttonState.HasFlag (MouseFlags.Button1Released)
-			&& !buttonState.HasFlag (MouseFlags.Button2Released)
-			&& !buttonState.HasFlag (MouseFlags.Button3Released)
-			&& !buttonState.HasFlag (MouseFlags.Button4Released)) {
-
-			lastMouseButtonPressed = null;
-			isButtonPressed = false;
-		}
-
-		if (!isButtonClicked && !isButtonDoubleClicked && ((buttonState == MouseFlags.Button1Pressed || buttonState == MouseFlags.Button2Pressed ||
-			  buttonState == MouseFlags.Button3Pressed || buttonState == MouseFlags.Button4Pressed) && lastMouseButtonPressed == null) ||
-			  isButtonPressed && lastMouseButtonPressed != null && buttonState.HasFlag (MouseFlags.ReportMousePosition)) {
-
-			mouseFlags [0] = buttonState;
-			lastMouseButtonPressed = buttonState;
-			isButtonPressed = true;
-
-			if ((mouseFlags [0] & MouseFlags.ReportMousePosition) == 0) {
-				point = new Point () {
-					X = pos.X,
-					Y = pos.Y
-				};
-
-				Application.MainLoop.AddIdle (() => {
-					Task.Run (async () => await ProcessContinuousButtonPressedAsync (buttonState, continuousButtonPressedHandler));
-					return false;
-				});
-			} else if (mouseFlags [0] == MouseFlags.ReportMousePosition) {
-				isButtonPressed = false;
-			}
-
-		} else if (isButtonDoubleClicked && (buttonState == MouseFlags.Button1Pressed || buttonState == MouseFlags.Button2Pressed ||
-			buttonState == MouseFlags.Button3Pressed || buttonState == MouseFlags.Button4Pressed)) {
-
-			mouseFlags [0] = GetButtonTripleClicked (buttonState);
-			isButtonDoubleClicked = false;
-			isButtonTripleClicked = true;
-
-		} else if (isButtonClicked && (buttonState == MouseFlags.Button1Pressed || buttonState == MouseFlags.Button2Pressed ||
-			buttonState == MouseFlags.Button3Pressed || buttonState == MouseFlags.Button4Pressed)) {
-
-			mouseFlags [0] = GetButtonDoubleClicked (buttonState);
-			isButtonClicked = false;
-			isButtonDoubleClicked = true;
-			Application.MainLoop.AddIdle (() => {
-				Task.Run (async () => await ProcessButtonDoubleClickedAsync ());
-				return false;
-			});
-
-		}
-		//else if (isButtonReleased && !isButtonClicked && buttonState == MouseFlags.ReportMousePosition) {
-		//	mouseFlag [0] = GetButtonClicked ((MouseFlags)lastMouseButtonReleased);
-		//	lastMouseButtonReleased = null;
-		//	isButtonReleased = false;
-		//	isButtonClicked = true;
-		//	Application.MainLoop.AddIdle (() => {
-		//		Task.Run (async () => await ProcessButtonClickedAsync ());
-		//		return false;
-		//	});
-
-		//} 
-		else if (!isButtonClicked && !isButtonDoubleClicked && (buttonState == MouseFlags.Button1Released || buttonState == MouseFlags.Button2Released ||
-			  buttonState == MouseFlags.Button3Released || buttonState == MouseFlags.Button4Released)) {
-
-			mouseFlags [0] = buttonState;
-			isButtonPressed = false;
-
-			if (isButtonTripleClicked) {
-				isButtonTripleClicked = false;
-			} else if (pos.X == point.X && pos.Y == point.Y) {
-				mouseFlags.Add (GetButtonClicked (buttonState));
-				isButtonClicked = true;
-				Application.MainLoop.AddIdle (() => {
-					Task.Run (async () => await ProcessButtonClickedAsync ());
-					return false;
-				});
-			}
-
-			point = pos;
-
-			//if ((lastMouseButtonPressed & MouseFlags.ReportMousePosition) == 0) {
-			//	lastMouseButtonReleased = buttonState;
-			//	isButtonPressed = false;
-			//	isButtonReleased = true;
-			//} else {
-			//	lastMouseButtonPressed = null;
-			//	isButtonPressed = false;
-			//}
-
-		} else if (buttonState == MouseFlags.WheeledUp) {
-
-			mouseFlags [0] = MouseFlags.WheeledUp;
-
-		} else if (buttonState == MouseFlags.WheeledDown) {
-
-			mouseFlags [0] = MouseFlags.WheeledDown;
-
-		} else if (buttonState == MouseFlags.WheeledLeft) {
-
-			mouseFlags [0] = MouseFlags.WheeledLeft;
-
-		} else if (buttonState == MouseFlags.WheeledRight) {
-
-			mouseFlags [0] = MouseFlags.WheeledRight;
-
-		} else if (buttonState == MouseFlags.ReportMousePosition) {
-			mouseFlags [0] = MouseFlags.ReportMousePosition;
-
-		} else {
-			mouseFlags [0] = buttonState;
-			//foreach (var flag in buttonState.GetUniqueFlags()) {
-			//	mouseFlag [0] |= flag;
-			//}
-		}
-
-		mouseFlags [0] = SetControlKeyStates (buttonState, mouseFlags [0]);
-		//buttonState = mouseFlags;
-
-		//System.Diagnostics.Debug.WriteLine ($"buttonState: {buttonState} X: {pos.X} Y: {pos.Y}");
-		//foreach (var mf in mouseFlags) {
-		//	System.Diagnostics.Debug.WriteLine ($"mouseFlags: {mf} X: {pos.X} Y: {pos.Y}");
-		//}
-	}
-
-	private static async Task ProcessContinuousButtonPressedAsync (MouseFlags mouseFlag, Action<MouseFlags, Point> continuousButtonPressedHandler)
-	{
-		while (isButtonPressed) {
-			await Task.Delay (100);
-			//var me = new MouseEvent () {
-			//	X = point.X,
-			//	Y = point.Y,
-			//	Flags = mouseFlag
-			//};
-
-			var view = Application.WantContinuousButtonPressedView;
-			if (view == null)
-				break;
-			if (isButtonPressed && lastMouseButtonPressed != null && (mouseFlag & MouseFlags.ReportMousePosition) == 0) {
-				Application.Invoke (() => continuousButtonPressedHandler (mouseFlag, point));
-			}
-		}
-	}
-
-	private static async Task ProcessButtonClickedAsync ()
-	{
-		await Task.Delay (300);
-		isButtonClicked = false;
-	}
-
-	private static async Task ProcessButtonDoubleClickedAsync ()
-	{
-		await Task.Delay (300);
-		isButtonDoubleClicked = false;
-	}
-
-	private static MouseFlags GetButtonClicked (MouseFlags mouseFlag)
-	{
-		MouseFlags mf = default;
-		switch (mouseFlag) {
-		case MouseFlags.Button1Released:
-			mf = MouseFlags.Button1Clicked;
-			break;
-
-		case MouseFlags.Button2Released:
-			mf = MouseFlags.Button2Clicked;
-			break;
-
-		case MouseFlags.Button3Released:
-			mf = MouseFlags.Button3Clicked;
-			break;
-		}
-		return mf;
-	}
-
-	private static MouseFlags GetButtonDoubleClicked (MouseFlags mouseFlag)
-	{
-		MouseFlags mf = default;
-		switch (mouseFlag) {
-		case MouseFlags.Button1Pressed:
-			mf = MouseFlags.Button1DoubleClicked;
-			break;
-
-		case MouseFlags.Button2Pressed:
-			mf = MouseFlags.Button2DoubleClicked;
-			break;
-
-		case MouseFlags.Button3Pressed:
-			mf = MouseFlags.Button3DoubleClicked;
-			break;
-		}
-		return mf;
-	}
-
-	private static MouseFlags GetButtonTripleClicked (MouseFlags mouseFlag)
-	{
-		MouseFlags mf = default;
-		switch (mouseFlag) {
-		case MouseFlags.Button1Pressed:
-			mf = MouseFlags.Button1TripleClicked;
-			break;
-
-		case MouseFlags.Button2Pressed:
-			mf = MouseFlags.Button2TripleClicked;
-			break;
-
-		case MouseFlags.Button3Pressed:
-			mf = MouseFlags.Button3TripleClicked;
-			break;
-		}
-		return mf;
-	}
-
-	private static MouseFlags SetControlKeyStates (MouseFlags buttonState, MouseFlags mouseFlag)
-	{
-		if ((buttonState & MouseFlags.ButtonCtrl) != 0 && (mouseFlag & MouseFlags.ButtonCtrl) == 0)
-			mouseFlag |= MouseFlags.ButtonCtrl;
-
-		if ((buttonState & MouseFlags.ButtonShift) != 0 && (mouseFlag & MouseFlags.ButtonShift) == 0)
-			mouseFlag |= MouseFlags.ButtonShift;
-
-		if ((buttonState & MouseFlags.ButtonAlt) != 0 && (mouseFlag & MouseFlags.ButtonAlt) == 0)
-			mouseFlag |= MouseFlags.ButtonAlt;
-		return mouseFlag;
-	}
-
-	// TODO: Move this out of here and into ConsoleDriver or somewhere else.
-	/// <summary>
-	/// Get the terminal that holds the console driver.
-	/// </summary>
-	/// <param name="process">The process.</param>
-	/// <returns>If supported the executable console process, null otherwise.</returns>
-	public static Process GetParentProcess (Process process)
-	{
-		if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) {
-			return null;
-		}
-
-		string query = "SELECT ParentProcessId FROM Win32_Process WHERE ProcessId = " + process.Id;
-		using (ManagementObjectSearcher mos = new ManagementObjectSearcher (query)) {
-			foreach (ManagementObject mo in mos.Get ()) {
-				if (mo ["ParentProcessId"] != null) {
-					try {
-						var id = Convert.ToInt32 (mo ["ParentProcessId"]);
-						return Process.GetProcessById (id);
-					} catch {
-					}
-				}
-			}
-		}
-		return null;
-	}
+public static class EscSeqUtils
+{
+    /// <summary>
+    ///     Options for ANSI ESC "[xJ" - Clears part of the screen.
+    /// </summary>
+    public enum ClearScreenOptions
+    {
+        /// <summary>
+        ///     If n is 0 (or missing), clear from cursor to end of screen.
+        /// </summary>
+        CursorToEndOfScreen = 0,
+
+        /// <summary>
+        ///     If n is 1, clear from cursor to beginning of the screen.
+        /// </summary>
+        CursorToBeginningOfScreen = 1,
+
+        /// <summary>
+        ///     If n is 2, clear entire screen (and moves cursor to upper left on DOS ANSI.SYS).
+        /// </summary>
+        EntireScreen = 2,
+
+        /// <summary>
+        ///     If n is 3, clear entire screen and delete all lines saved in the scrollback buffer
+        /// </summary>
+        EntireScreenAndScrollbackBuffer = 3
+    }
+
+    /// <summary>
+    ///     Escape key code (ASCII 27/0x1B).
+    /// </summary>
+    public const char KeyEsc = (char)KeyCode.Esc;
+
+    /// <summary>
+    ///     ESC [ - The CSI (Control Sequence Introducer).
+    /// </summary>
+    public const string CSI = "\u001B[";
+
+    /// <summary>
+    ///     ESC [ ? 1047 h - Activate xterm alternative buffer (no backscroll)
+    /// </summary>
+    /// <remarks>
+    ///     From
+    ///     https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Functions-using-CSI-_-ordered-by-the-final-character_s_
+    ///     Use Alternate Screen Buffer, xterm.
+    /// </remarks>
+    public static readonly string CSI_ActivateAltBufferNoBackscroll = CSI + "?1047h";
+
+    /// <summary>
+    ///     ESC [ ? 1003 l - Disable any mouse event tracking.
+    /// </summary>
+    public static readonly string CSI_DisableAnyEventMouse = CSI + "?1003l";
+
+    /// <summary>
+    ///     ESC [ ? 1006 l - Disable SGR (Select Graphic Rendition).
+    /// </summary>
+    public static readonly string CSI_DisableSgrExtModeMouse = CSI + "?1006l";
+
+    /// <summary>
+    ///     ESC [ ? 1015 l - Disable URXVT (Unicode Extended Virtual Terminal).
+    /// </summary>
+    public static readonly string CSI_DisableUrxvtExtModeMouse = CSI + "?1015l";
+
+    /// <summary>
+    ///     ESC [ ? 1003 h - Enable  mouse event tracking.
+    /// </summary>
+    public static readonly string CSI_EnableAnyEventMouse = CSI + "?1003h";
+
+    /// <summary>
+    ///     ESC [ ? 1006 h - Enable SGR (Select Graphic Rendition).
+    /// </summary>
+    public static readonly string CSI_EnableSgrExtModeMouse = CSI + "?1006h";
+
+    /// <summary>
+    ///     ESC [ ? 1015 h - Enable URXVT (Unicode Extended Virtual Terminal).
+    /// </summary>
+    public static readonly string CSI_EnableUrxvtExtModeMouse = CSI + "?1015h";
+
+    /// <summary>
+    ///     ESC [ ? 1047 l - Restore xterm working buffer (with backscroll)
+    /// </summary>
+    /// <remarks>
+    ///     From
+    ///     https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Functions-using-CSI-_-ordered-by-the-final-character_s_
+    ///     Use Normal Screen Buffer, xterm.  Clear the screen first if in the Alternate Screen Buffer.
+    /// </remarks>
+    public static readonly string CSI_RestoreAltBufferWithBackscroll = CSI + "?1047l";
+
+    /// <summary>
+    ///     ESC [ ? 1049 l - Restore cursor position and restore xterm working buffer (with backscroll)
+    /// </summary>
+    /// <remarks>
+    ///     From
+    ///     https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Functions-using-CSI-_-ordered-by-the-final-character_s_
+    ///     Use Normal Screen Buffer and restore cursor as in DECRC, xterm.
+    ///     resource.This combines the effects of the 1047 and 1048  modes.
+    /// </remarks>
+    public static readonly string CSI_RestoreCursorAndRestoreAltBufferWithBackscroll = CSI + "?1049l";
+
+    /// <summary>
+    ///     ESC [ ? 1049 h - Save cursor position and activate xterm alternative buffer (no backscroll)
+    /// </summary>
+    /// <remarks>
+    ///     From
+    ///     https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Functions-using-CSI-_-ordered-by-the-final-character_s_
+    ///     Save cursor as in DECSC, xterm. After saving the cursor, switch to the Alternate Screen Buffer,
+    ///     clearing it first.
+    ///     This control combines the effects of the 1047 and 1048 modes.
+    ///     Use this with terminfo-based applications rather than the 47 mode.
+    /// </remarks>
+    public static readonly string CSI_SaveCursorAndActivateAltBufferNoBackscroll = CSI + "?1049h";
+
+    //private static bool isButtonReleased;
+    private static bool isButtonClicked;
+
+    private static bool isButtonDoubleClicked;
+
+    //private static MouseFlags? lastMouseButtonReleased;
+    // QUESTION: What's the difference between isButtonClicked and isButtonPressed?
+    // Some clarity or comments would be handy, here.
+    // It also seems like some enforcement of valid states might be a good idea.
+    private static bool isButtonPressed;
+    private static bool isButtonTripleClicked;
+
+    private static MouseFlags? lastMouseButtonPressed;
+    private static Point? point;
+
+    /// <summary>
+    ///     Control sequence for disabling mouse events.
+    /// </summary>
+    public static string CSI_DisableMouseEvents { get; set; } =
+        CSI_DisableAnyEventMouse + CSI_DisableUrxvtExtModeMouse + CSI_DisableSgrExtModeMouse;
+
+    /// <summary>
+    ///     Control sequence for enabling mouse events.
+    /// </summary>
+    public static string CSI_EnableMouseEvents { get; set; } =
+        CSI_EnableAnyEventMouse + CSI_EnableUrxvtExtModeMouse + CSI_EnableSgrExtModeMouse;
+
+    /// <summary>
+    ///     ESC [ x J - Clears part of the screen. See <see cref="ClearScreenOptions"/>.
+    /// </summary>
+    /// <param name="option"></param>
+    /// <returns></returns>
+    public static string CSI_ClearScreen (ClearScreenOptions option) { return $"{CSI}{(int)option}J"; }
+
+    /// <summary>
+    ///     Decodes an ANSI escape sequence.
+    /// </summary>
+    /// <param name="escSeqRequests">The <see cref="EscSeqRequests"/> which may contain a request.</param>
+    /// <param name="newConsoleKeyInfo">The <see cref="ConsoleKeyInfo"/> which may changes.</param>
+    /// <param name="key">The <see cref="ConsoleKey"/> which may changes.</param>
+    /// <param name="cki">The <see cref="ConsoleKeyInfo"/> array.</param>
+    /// <param name="mod">The <see cref="ConsoleModifiers"/> which may changes.</param>
+    /// <param name="c1Control">The control returned by the <see cref="GetC1ControlChar(char)"/> method.</param>
+    /// <param name="code">The code returned by the <see cref="GetEscapeResult(char[])"/> method.</param>
+    /// <param name="values">The values returned by the <see cref="GetEscapeResult(char[])"/> method.</param>
+    /// <param name="terminator">The terminator returned by the <see cref="GetEscapeResult(char[])"/> method.</param>
+    /// <param name="isMouse">Indicates if the escape sequence is a mouse event.</param>
+    /// <param name="buttonState">The <see cref="MouseFlags"/> button state.</param>
+    /// <param name="pos">The <see cref="MouseFlags"/> position.</param>
+    /// <param name="isResponse">Indicates if the escape sequence is a response to a request.</param>
+    /// <param name="continuousButtonPressedHandler">The handler that will process the event.</param>
+    public static void DecodeEscSeq (
+        EscSeqRequests escSeqRequests,
+        ref ConsoleKeyInfo newConsoleKeyInfo,
+        ref ConsoleKey key,
+        ConsoleKeyInfo [] cki,
+        ref ConsoleModifiers mod,
+        out string c1Control,
+        out string code,
+        out string [] values,
+        out string terminator,
+        out bool isMouse,
+        out List<MouseFlags> buttonState,
+        out Point pos,
+        out bool isResponse,
+        Action<MouseFlags, Point> continuousButtonPressedHandler
+    )
+    {
+        char [] kChars = GetKeyCharArray (cki);
+        (c1Control, code, values, terminator) = GetEscapeResult (kChars);
+        isMouse = false;
+        buttonState = new List<MouseFlags> { 0 };
+        pos = default (Point);
+        isResponse = false;
+
+        switch (c1Control)
+        {
+            case "ESC":
+                if (values is null && string.IsNullOrEmpty (terminator))
+                {
+                    key = ConsoleKey.Escape;
+
+                    newConsoleKeyInfo = new ConsoleKeyInfo (
+                                                            cki [0].KeyChar,
+                                                            key,
+                                                            (mod & ConsoleModifiers.Shift) != 0,
+                                                            (mod & ConsoleModifiers.Alt) != 0,
+                                                            (mod & ConsoleModifiers.Control) != 0);
+                }
+                else if ((uint)cki [1].KeyChar >= 1 && (uint)cki [1].KeyChar <= 26)
+                {
+                    key = (ConsoleKey)(char)(cki [1].KeyChar + (uint)ConsoleKey.A - 1);
+
+                    newConsoleKeyInfo = new ConsoleKeyInfo (
+                                                            cki [1].KeyChar,
+                                                            key,
+                                                            false,
+                                                            true,
+                                                            true);
+                }
+                else
+                {
+                    if (cki [1].KeyChar >= 97 && cki [1].KeyChar <= 122)
+                    {
+                        key = (ConsoleKey)cki [1].KeyChar.ToString ().ToUpper () [0];
+                    }
+                    else
+                    {
+                        key = (ConsoleKey)cki [1].KeyChar;
+                    }
+
+                    newConsoleKeyInfo = new ConsoleKeyInfo (
+                                                            (char)key,
+                                                            (ConsoleKey)Math.Min ((uint)key, 255),
+                                                            false,
+                                                            true,
+                                                            false);
+                }
+
+                break;
+            case "SS3":
+                key = GetConsoleKey (terminator [0], values [0], ref mod);
+
+                newConsoleKeyInfo = new ConsoleKeyInfo (
+                                                        '\0',
+                                                        key,
+                                                        (mod & ConsoleModifiers.Shift) != 0,
+                                                        (mod & ConsoleModifiers.Alt) != 0,
+                                                        (mod & ConsoleModifiers.Control) != 0);
+
+                break;
+            case "CSI":
+                if (!string.IsNullOrEmpty (code) && code == "<")
+                {
+                    GetMouse (cki, out buttonState, out pos, continuousButtonPressedHandler);
+                    isMouse = true;
+
+                    return;
+                }
+
+                if (escSeqRequests is { } && escSeqRequests.HasResponse (terminator))
+                {
+                    isResponse = true;
+                    escSeqRequests.Remove (terminator);
+
+                    return;
+                }
+
+                if (!string.IsNullOrEmpty (terminator))
+                {
+                    key = GetConsoleKey (terminator [0], values [0], ref mod);
+
+                    if (key != 0 && values.Length > 1)
+                    {
+                        mod |= GetConsoleModifiers (values [1]);
+                    }
+
+                    newConsoleKeyInfo = new ConsoleKeyInfo (
+                                                            '\0',
+                                                            key,
+                                                            (mod & ConsoleModifiers.Shift) != 0,
+                                                            (mod & ConsoleModifiers.Alt) != 0,
+                                                            (mod & ConsoleModifiers.Control) != 0);
+                }
+                else
+                {
+                    // BUGBUG: See https://github.com/gui-cs/Terminal.Gui/issues/2803
+                    // This is caused by NetDriver depending on Console.KeyAvailable?
+                    throw new InvalidOperationException ("CSI response, but there's no terminator");
+
+                    //newConsoleKeyInfo = new ConsoleKeyInfo ('\0',
+                    //	key,
+                    //	(mod & ConsoleModifiers.Shift) != 0,
+                    //	(mod & ConsoleModifiers.Alt) != 0,
+                    //	(mod & ConsoleModifiers.Control) != 0);
+                }
+
+                break;
+        }
+    }
+
+    #nullable enable
+    /// <summary>
+    ///     Gets the c1Control used in the called escape sequence.
+    /// </summary>
+    /// <param name="c">The char used.</param>
+    /// <returns>The c1Control.</returns>
+    [Pure]
+    public static string GetC1ControlChar (in char c)
+    {
+        // These control characters are used in the vtXXX emulation.
+        return c switch
+               {
+                   'D' => "IND", // Index
+                   'E' => "NEL", // Next Line
+                   'H' => "HTS", // Tab Set
+                   'M' => "RI", // Reverse Index
+                   'N' => "SS2", // Single Shift Select of G2 Character Set: affects next character only
+                   'O' => "SS3", // Single Shift Select of G3 Character Set: affects next character only
+                   'P' => "DCS", // Device Control String
+                   'V' => "SPA", // Start of Guarded Area
+                   'W' => "EPA", // End of Guarded Area
+                   'X' => "SOS", // Start of String
+                   'Z' => "DECID", // Return Terminal ID Obsolete form of CSI c (DA)
+                   '[' => "CSI", // Control Sequence Introducer
+                   '\\' => "ST", // String Terminator
+                   ']' => "OSC", // Operating System Command
+                   '^' => "PM", // Privacy Message
+                   '_' => "APC", // Application Program Command
+                   _ => string.Empty
+               };
+    }
+
+    /// <summary>
+    ///     Gets the <see cref="ConsoleKey"/> depending on terminating and value.
+    /// </summary>
+    /// <param name="terminator">
+    ///     The terminator indicating a reply to <see cref="CSI_SendDeviceAttributes"/> or
+    ///     <see cref="CSI_SendDeviceAttributes2"/>.
+    /// </param>
+    /// <param name="value">The value.</param>
+    /// <param name="mod">The <see cref="ConsoleModifiers"/> which may changes.</param>
+    /// <returns>The <see cref="ConsoleKey"/> and probably the <see cref="ConsoleModifiers"/>.</returns>
+    public static ConsoleKey GetConsoleKey (char terminator, string? value, ref ConsoleModifiers mod)
+    {
+        if (terminator == 'Z')
+        {
+            mod |= ConsoleModifiers.Shift;
+        }
+
+        return (terminator, value) switch
+               {
+                   ('A', _) => ConsoleKey.UpArrow,
+                   ('B', _) => ConsoleKey.DownArrow,
+                   ('C', _) => ConsoleKey.RightArrow,
+                   ('D', _) => ConsoleKey.LeftArrow,
+                   ('F', _) => ConsoleKey.End,
+                   ('H', _) => ConsoleKey.Home,
+                   ('P', _) => ConsoleKey.F1,
+                   ('Q', _) => ConsoleKey.F2,
+                   ('R', _) => ConsoleKey.F3,
+                   ('S', _) => ConsoleKey.F4,
+                   ('Z', _) => ConsoleKey.Tab,
+                   ('~', "2") => ConsoleKey.Insert,
+                   ('~', "3") => ConsoleKey.Delete,
+                   ('~', "5") => ConsoleKey.PageUp,
+                   ('~', "6") => ConsoleKey.PageDown,
+                   ('~', "15") => ConsoleKey.F5,
+                   ('~', "17") => ConsoleKey.F6,
+                   ('~', "18") => ConsoleKey.F7,
+                   ('~', "19") => ConsoleKey.F8,
+                   ('~', "20") => ConsoleKey.F9,
+                   ('~', "21") => ConsoleKey.F10,
+                   ('~', "23") => ConsoleKey.F11,
+                   ('~', "24") => ConsoleKey.F12,
+                   (_, _) => 0
+               };
+    }
+
+    /// <summary>
+    ///     Gets the <see cref="ConsoleModifiers"/> from the value.
+    /// </summary>
+    /// <param name="value">The value.</param>
+    /// <returns>The <see cref="ConsoleModifiers"/> or zero.</returns>
+    public static ConsoleModifiers GetConsoleModifiers (string? value)
+    {
+        return value switch
+               {
+                   "2" => ConsoleModifiers.Shift,
+                   "3" => ConsoleModifiers.Alt,
+                   "4" => ConsoleModifiers.Shift | ConsoleModifiers.Alt,
+                   "5" => ConsoleModifiers.Control,
+                   "6" => ConsoleModifiers.Shift | ConsoleModifiers.Control,
+                   "7" => ConsoleModifiers.Alt | ConsoleModifiers.Control,
+                   "8" => ConsoleModifiers.Shift | ConsoleModifiers.Alt | ConsoleModifiers.Control,
+                   _ => 0
+               };
+    }
+    #nullable restore
+
+    /// <summary>
+    ///     Gets all the needed information about a escape sequence.
+    /// </summary>
+    /// <param name="kChar">The array with all chars.</param>
+    /// <returns>
+    ///     The c1Control returned by <see cref="GetC1ControlChar(char)"/>, code, values and terminating.
+    /// </returns>
+    public static (string c1Control, string code, string [] values, string terminating) GetEscapeResult (char [] kChar)
+    {
+        if (kChar is null || kChar.Length == 0)
+        {
+            return (null, null, null, null);
+        }
+
+        if (kChar [0] != KeyEsc)
+        {
+            throw new InvalidOperationException ("Invalid escape character!");
+        }
+
+        if (kChar.Length == 1)
+        {
+            return ("ESC", null, null, null);
+        }
+
+        if (kChar.Length == 2)
+        {
+            return ("ESC", null, null, kChar [1].ToString ());
+        }
+
+        string c1Control = GetC1ControlChar (kChar [1]);
+        string code = null;
+        int nSep = kChar.Count (static x => x == ';') + 1;
+        var values = new string [nSep];
+        var valueIdx = 0;
+        var terminating = string.Empty;
+
+        for (var i = 2; i < kChar.Length; i++)
+        {
+            char c = kChar [i];
+
+            if (char.IsDigit (c))
+            {
+                // PERF: Ouch
+                values [valueIdx] += c.ToString ();
+            }
+            else if (c == ';')
+            {
+                valueIdx++;
+            }
+            else if (valueIdx == nSep - 1 || i == kChar.Length - 1)
+            {
+                // PERF: Ouch
+                terminating += c.ToString ();
+            }
+            else
+            {
+                // PERF: Ouch
+                code += c.ToString ();
+            }
+        }
+
+        return (c1Control, code, values, terminating);
+    }
+
+    /// <summary>
+    ///     A helper to get only the <see cref="ConsoleKeyInfo.KeyChar"/> from the <see cref="ConsoleKeyInfo"/> array.
+    /// </summary>
+    /// <param name="cki"></param>
+    /// <returns>The char array of the escape sequence.</returns>
+    // PERF: This is expensive
+    public static char [] GetKeyCharArray (ConsoleKeyInfo [] cki)
+    {
+        char [] kChar = { };
+        var length = 0;
+
+        foreach (ConsoleKeyInfo kc in cki)
+        {
+            length++;
+            Array.Resize (ref kChar, length);
+            kChar [length - 1] = kc.KeyChar;
+        }
+
+        return kChar;
+    }
+
+    /// <summary>
+    ///     Gets the <see cref="MouseFlags"/> mouse button flags and the position.
+    /// </summary>
+    /// <param name="cki">The <see cref="ConsoleKeyInfo"/> array.</param>
+    /// <param name="mouseFlags">The mouse button flags.</param>
+    /// <param name="pos">The mouse position.</param>
+    /// <param name="continuousButtonPressedHandler">The handler that will process the event.</param>
+    public static void GetMouse (
+        ConsoleKeyInfo [] cki,
+        out List<MouseFlags> mouseFlags,
+        out Point pos,
+        Action<MouseFlags, Point> continuousButtonPressedHandler
+    )
+    {
+        MouseFlags buttonState = 0;
+        pos = Point.Empty;
+        var buttonCode = 0;
+        var foundButtonCode = false;
+        var foundPoint = 0;
+        string value = string.Empty;
+        char [] kChar = GetKeyCharArray (cki);
+
+        // PERF: This loop could benefit from use of Spans and other strategies to avoid copies.
+        //System.Diagnostics.Debug.WriteLine ($"kChar: {new string (kChar)}");
+        for (var i = 0; i < kChar.Length; i++)
+        {
+            // PERF: Copy
+            char c = kChar [i];
+
+            if (c == '<')
+            {
+                foundButtonCode = true;
+            }
+            else if (foundButtonCode && c != ';')
+            {
+                // PERF: Ouch
+                value += c.ToString ();
+            }
+            else if (c == ';')
+            {
+                if (foundButtonCode)
+                {
+                    foundButtonCode = false;
+                    buttonCode = int.Parse (value);
+                }
+
+                if (foundPoint == 1)
+                {
+                    pos.X = int.Parse (value) - 1;
+                }
+
+                value = string.Empty;
+                foundPoint++;
+            }
+            else if (foundPoint > 0 && c != 'm' && c != 'M')
+            {
+                value += c.ToString ();
+            }
+            else if (c == 'm' || c == 'M')
+            {
+                //pos.Y = int.Parse (value) + Console.WindowTop - 1;
+                pos.Y = int.Parse (value) - 1;
+
+                switch (buttonCode)
+                {
+                    case 0:
+                    case 8:
+                    case 16:
+                    case 24:
+                    case 32:
+                    case 36:
+                    case 40:
+                    case 48:
+                    case 56:
+                        buttonState = c == 'M'
+                                          ? MouseFlags.Button1Pressed
+                                          : MouseFlags.Button1Released;
+
+                        break;
+                    case 1:
+                    case 9:
+                    case 17:
+                    case 25:
+                    case 33:
+                    case 37:
+                    case 41:
+                    case 45:
+                    case 49:
+                    case 53:
+                    case 57:
+                    case 61:
+                        buttonState = c == 'M'
+                                          ? MouseFlags.Button2Pressed
+                                          : MouseFlags.Button2Released;
+
+                        break;
+                    case 2:
+                    case 10:
+                    case 14:
+                    case 18:
+                    case 22:
+                    case 26:
+                    case 30:
+                    case 34:
+                    case 42:
+                    case 46:
+                    case 50:
+                    case 54:
+                    case 58:
+                    case 62:
+                        buttonState = c == 'M'
+                                          ? MouseFlags.Button3Pressed
+                                          : MouseFlags.Button3Released;
+
+                        break;
+                    case 35:
+                    //// Needed for Windows OS
+                    //if (isButtonPressed && c == 'm'
+                    //	&& (lastMouseEvent.ButtonState == MouseFlags.Button1Pressed
+                    //	|| lastMouseEvent.ButtonState == MouseFlags.Button2Pressed
+                    //	|| lastMouseEvent.ButtonState == MouseFlags.Button3Pressed)) {
+
+                    //	switch (lastMouseEvent.ButtonState) {
+                    //	case MouseFlags.Button1Pressed:
+                    //		buttonState = MouseFlags.Button1Released;
+                    //		break;
+                    //	case MouseFlags.Button2Pressed:
+                    //		buttonState = MouseFlags.Button2Released;
+                    //		break;
+                    //	case MouseFlags.Button3Pressed:
+                    //		buttonState = MouseFlags.Button3Released;
+                    //		break;
+                    //	}
+                    //} else {
+                    //	buttonState = MouseFlags.ReportMousePosition;
+                    //}
+                    //break;
+                    case 39:
+                    case 43:
+                    case 47:
+                    case 51:
+                    case 55:
+                    case 59:
+                    case 63:
+                        buttonState = MouseFlags.ReportMousePosition;
+
+                        break;
+                    case 64:
+                        buttonState = MouseFlags.WheeledUp;
+
+                        break;
+                    case 65:
+                        buttonState = MouseFlags.WheeledDown;
+
+                        break;
+                    case 68:
+                    case 72:
+                    case 80:
+                        buttonState = MouseFlags.WheeledLeft; // Shift/Ctrl+WheeledUp
+
+                        break;
+                    case 69:
+                    case 73:
+                    case 81:
+                        buttonState = MouseFlags.WheeledRight; // Shift/Ctrl+WheeledDown
+
+                        break;
+                }
+
+                // Modifiers.
+                switch (buttonCode)
+                {
+                    case 8:
+                    case 9:
+                    case 10:
+                    case 43:
+                        buttonState |= MouseFlags.ButtonAlt;
+
+                        break;
+                    case 14:
+                    case 47:
+                        buttonState |= MouseFlags.ButtonAlt | MouseFlags.ButtonShift;
+
+                        break;
+                    case 16:
+                    case 17:
+                    case 18:
+                    case 51:
+                        buttonState |= MouseFlags.ButtonCtrl;
+
+                        break;
+                    case 22:
+                    case 55:
+                        buttonState |= MouseFlags.ButtonCtrl | MouseFlags.ButtonShift;
+
+                        break;
+                    case 24:
+                    case 25:
+                    case 26:
+                    case 59:
+                        buttonState |= MouseFlags.ButtonCtrl | MouseFlags.ButtonAlt;
+
+                        break;
+                    case 30:
+                    case 63:
+                        buttonState |= MouseFlags.ButtonCtrl | MouseFlags.ButtonShift | MouseFlags.ButtonAlt;
+
+                        break;
+                    case 32:
+                    case 33:
+                    case 34:
+                        buttonState |= MouseFlags.ReportMousePosition;
+
+                        break;
+                    case 36:
+                    case 37:
+                        buttonState |= MouseFlags.ReportMousePosition | MouseFlags.ButtonShift;
+
+                        break;
+                    case 39:
+                    case 68:
+                    case 69:
+                        buttonState |= MouseFlags.ButtonShift;
+
+                        break;
+                    case 40:
+                    case 41:
+                    case 42:
+                        buttonState |= MouseFlags.ReportMousePosition | MouseFlags.ButtonAlt;
+
+                        break;
+                    case 45:
+                    case 46:
+                        buttonState |= MouseFlags.ReportMousePosition | MouseFlags.ButtonAlt | MouseFlags.ButtonShift;
+
+                        break;
+                    case 48:
+                    case 49:
+                    case 50:
+                        buttonState |= MouseFlags.ReportMousePosition | MouseFlags.ButtonCtrl;
+
+                        break;
+                    case 53:
+                    case 54:
+                        buttonState |= MouseFlags.ReportMousePosition | MouseFlags.ButtonCtrl | MouseFlags.ButtonShift;
+
+                        break;
+                    case 56:
+                    case 57:
+                    case 58:
+                        buttonState |= MouseFlags.ReportMousePosition | MouseFlags.ButtonCtrl | MouseFlags.ButtonAlt;
+
+                        break;
+                    case 61:
+                    case 62:
+                        buttonState |= MouseFlags.ReportMousePosition | MouseFlags.ButtonCtrl | MouseFlags.ButtonShift | MouseFlags.ButtonAlt;
+
+                        break;
+                }
+            }
+        }
+
+        mouseFlags = [MouseFlags.AllEvents];
+
+        if (lastMouseButtonPressed != null
+            && !isButtonPressed
+            && !buttonState.HasFlag (MouseFlags.ReportMousePosition)
+            && !buttonState.HasFlag (MouseFlags.Button1Released)
+            && !buttonState.HasFlag (MouseFlags.Button2Released)
+            && !buttonState.HasFlag (MouseFlags.Button3Released)
+            && !buttonState.HasFlag (MouseFlags.Button4Released))
+        {
+            lastMouseButtonPressed = null;
+            isButtonPressed = false;
+        }
+
+        if ((!isButtonClicked
+             && !isButtonDoubleClicked
+             && (buttonState == MouseFlags.Button1Pressed
+                 || buttonState == MouseFlags.Button2Pressed
+                 || buttonState == MouseFlags.Button3Pressed
+                 || buttonState == MouseFlags.Button4Pressed)
+             && lastMouseButtonPressed is null)
+            || (isButtonPressed && lastMouseButtonPressed is { } && buttonState.HasFlag (MouseFlags.ReportMousePosition)))
+        {
+            mouseFlags [0] = buttonState;
+            lastMouseButtonPressed = buttonState;
+            isButtonPressed = true;
+
+            point ??= pos;
+
+            if ((mouseFlags [0] & MouseFlags.ReportMousePosition) == 0)
+            {
+                Application.MainLoop.AddIdle (
+                                              () =>
+                                              {
+                                                  // INTENT: What's this trying to do?
+                                                  // The task itself is not awaited.
+                                                  Task.Run (
+                                                            async () => await ProcessContinuousButtonPressedAsync (
+                                                                         buttonState,
+                                                                         continuousButtonPressedHandler));
+
+                                                  return false;
+                                              });
+            }
+            else if (mouseFlags [0].HasFlag (MouseFlags.ReportMousePosition))
+            {
+                point = pos;
+
+                // The isButtonPressed must always be true, otherwise we can lose the feature
+                // If mouse flags has ReportMousePosition this feature won't run
+                // but is always prepared with the new location
+                //isButtonPressed = false;
+            }
+        }
+        else if (isButtonDoubleClicked
+                 && (buttonState == MouseFlags.Button1Pressed
+                     || buttonState == MouseFlags.Button2Pressed
+                     || buttonState == MouseFlags.Button3Pressed
+                     || buttonState == MouseFlags.Button4Pressed))
+        {
+            mouseFlags [0] = GetButtonTripleClicked (buttonState);
+            isButtonDoubleClicked = false;
+            isButtonTripleClicked = true;
+        }
+        else if (isButtonClicked
+                 && (buttonState == MouseFlags.Button1Pressed
+                     || buttonState == MouseFlags.Button2Pressed
+                     || buttonState == MouseFlags.Button3Pressed
+                     || buttonState == MouseFlags.Button4Pressed))
+        {
+            mouseFlags [0] = GetButtonDoubleClicked (buttonState);
+            isButtonClicked = false;
+            isButtonDoubleClicked = true;
+
+            Application.MainLoop.AddIdle (
+                                          () =>
+                                          {
+                                              Task.Run (async () => await ProcessButtonDoubleClickedAsync ());
+
+                                              return false;
+                                          });
+        }
+
+        //else if (isButtonReleased && !isButtonClicked && buttonState == MouseFlags.ReportMousePosition) {
+        //	mouseFlag [0] = GetButtonClicked ((MouseFlags)lastMouseButtonReleased);
+        //	lastMouseButtonReleased = null;
+        //	isButtonReleased = false;
+        //	isButtonClicked = true;
+        //	Application.MainLoop.AddIdle (() => {
+        //		Task.Run (async () => await ProcessButtonClickedAsync ());
+        //		return false;
+        //	});
+
+        //} 
+        else if (!isButtonClicked
+                 && !isButtonDoubleClicked
+                 && (buttonState == MouseFlags.Button1Released
+                     || buttonState == MouseFlags.Button2Released
+                     || buttonState == MouseFlags.Button3Released
+                     || buttonState == MouseFlags.Button4Released))
+        {
+            mouseFlags [0] = buttonState;
+            isButtonPressed = false;
+
+            if (isButtonTripleClicked)
+            {
+                isButtonTripleClicked = false;
+            }
+            else if (pos.X == point?.X && pos.Y == point?.Y)
+            {
+                mouseFlags.Add (GetButtonClicked (buttonState));
+                isButtonClicked = true;
+
+                Application.MainLoop.AddIdle (
+                                              () =>
+                                              {
+                                                  Task.Run (async () => await ProcessButtonClickedAsync ());
+
+                                                  return false;
+                                              });
+            }
+
+            point = pos;
+
+            //if ((lastMouseButtonPressed & MouseFlags.ReportMousePosition) == 0) {
+            //	lastMouseButtonReleased = buttonState;
+            //	isButtonPressed = false;
+            //	isButtonReleased = true;
+            //} else {
+            //	lastMouseButtonPressed = null;
+            //	isButtonPressed = false;
+            //}
+        }
+        else if (buttonState == MouseFlags.WheeledUp)
+        {
+            mouseFlags [0] = MouseFlags.WheeledUp;
+        }
+        else if (buttonState == MouseFlags.WheeledDown)
+        {
+            mouseFlags [0] = MouseFlags.WheeledDown;
+        }
+        else if (buttonState == MouseFlags.WheeledLeft)
+        {
+            mouseFlags [0] = MouseFlags.WheeledLeft;
+        }
+        else if (buttonState == MouseFlags.WheeledRight)
+        {
+            mouseFlags [0] = MouseFlags.WheeledRight;
+        }
+        else if (buttonState == MouseFlags.ReportMousePosition)
+        {
+            mouseFlags [0] = MouseFlags.ReportMousePosition;
+        }
+        else
+        {
+            mouseFlags [0] = buttonState;
+
+            //foreach (var flag in buttonState.GetUniqueFlags()) {
+            //	mouseFlag [0] |= flag;
+            //}
+        }
+
+        mouseFlags [0] = SetControlKeyStates (buttonState, mouseFlags [0]);
+
+        //buttonState = mouseFlags;
+
+        //System.Diagnostics.Debug.WriteLine ($"buttonState: {buttonState} X: {pos.X} Y: {pos.Y}");
+        //foreach (var mf in mouseFlags) {
+        //	System.Diagnostics.Debug.WriteLine ($"mouseFlags: {mf} X: {pos.X} Y: {pos.Y}");
+        //}
+    }
+
+    /// <summary>
+    ///     Ensures a console key is mapped to one that works correctly with ANSI escape sequences.
+    /// </summary>
+    /// <param name="consoleKeyInfo">The <see cref="ConsoleKeyInfo"/>.</param>
+    /// <returns>The <see cref="ConsoleKeyInfo"/> modified.</returns>
+    public static ConsoleKeyInfo MapConsoleKeyInfo (ConsoleKeyInfo consoleKeyInfo)
+    {
+        ConsoleKeyInfo newConsoleKeyInfo = consoleKeyInfo;
+        ConsoleKey key;
+        char keyChar = consoleKeyInfo.KeyChar;
+
+        switch ((uint)keyChar)
+        {
+            case 0:
+                if (consoleKeyInfo.Key == (ConsoleKey)64)
+                { // Ctrl+Space in Windows.
+                    newConsoleKeyInfo = new ConsoleKeyInfo (
+                                                            ' ',
+                                                            ConsoleKey.Spacebar,
+                                                            (consoleKeyInfo.Modifiers & ConsoleModifiers.Shift) != 0,
+                                                            (consoleKeyInfo.Modifiers & ConsoleModifiers.Alt) != 0,
+                                                            (consoleKeyInfo.Modifiers & ConsoleModifiers.Control) != 0);
+                }
+
+                break;
+            case uint n when n > 0 && n <= KeyEsc:
+                if (consoleKeyInfo.Key == 0 && consoleKeyInfo.KeyChar == '\r')
+                {
+                    key = ConsoleKey.Enter;
+
+                    newConsoleKeyInfo = new ConsoleKeyInfo (
+                                                            consoleKeyInfo.KeyChar,
+                                                            key,
+                                                            (consoleKeyInfo.Modifiers & ConsoleModifiers.Shift) != 0,
+                                                            (consoleKeyInfo.Modifiers & ConsoleModifiers.Alt) != 0,
+                                                            (consoleKeyInfo.Modifiers & ConsoleModifiers.Control) != 0);
+                }
+                else if (consoleKeyInfo.Key == 0)
+                {
+                    key = (ConsoleKey)(char)(consoleKeyInfo.KeyChar + (uint)ConsoleKey.A - 1);
+
+                    newConsoleKeyInfo = new ConsoleKeyInfo (
+                                                            (char)key,
+                                                            key,
+                                                            (consoleKeyInfo.Modifiers & ConsoleModifiers.Shift) != 0,
+                                                            (consoleKeyInfo.Modifiers & ConsoleModifiers.Alt) != 0,
+                                                            true);
+                }
+
+                break;
+            case 127: // DEL
+                newConsoleKeyInfo = new ConsoleKeyInfo (
+                                                        consoleKeyInfo.KeyChar,
+                                                        ConsoleKey.Backspace,
+                                                        (consoleKeyInfo.Modifiers & ConsoleModifiers.Shift) != 0,
+                                                        (consoleKeyInfo.Modifiers & ConsoleModifiers.Alt) != 0,
+                                                        (consoleKeyInfo.Modifiers & ConsoleModifiers.Control) != 0);
+
+                break;
+            default:
+                newConsoleKeyInfo = consoleKeyInfo;
+
+                break;
+        }
+
+        return newConsoleKeyInfo;
+    }
+
+    /// <summary>
+    ///     A helper to resize the <see cref="ConsoleKeyInfo"/> as needed.
+    /// </summary>
+    /// <param name="consoleKeyInfo">The <see cref="ConsoleKeyInfo"/>.</param>
+    /// <param name="cki">The <see cref="ConsoleKeyInfo"/> array to resize.</param>
+    /// <returns>The <see cref="ConsoleKeyInfo"/> resized.</returns>
+    public static ConsoleKeyInfo [] ResizeArray (ConsoleKeyInfo consoleKeyInfo, ConsoleKeyInfo [] cki)
+    {
+        Array.Resize (ref cki, cki is null ? 1 : cki.Length + 1);
+        cki [cki.Length - 1] = consoleKeyInfo;
+
+        return cki;
+    }
+
+    private static MouseFlags GetButtonClicked (MouseFlags mouseFlag)
+    {
+        MouseFlags mf = default;
+
+        switch (mouseFlag)
+        {
+            case MouseFlags.Button1Released:
+                mf = MouseFlags.Button1Clicked;
+
+                break;
+
+            case MouseFlags.Button2Released:
+                mf = MouseFlags.Button2Clicked;
+
+                break;
+
+            case MouseFlags.Button3Released:
+                mf = MouseFlags.Button3Clicked;
+
+                break;
+        }
+
+        return mf;
+    }
+
+    private static MouseFlags GetButtonDoubleClicked (MouseFlags mouseFlag)
+    {
+        MouseFlags mf = default;
+
+        switch (mouseFlag)
+        {
+            case MouseFlags.Button1Pressed:
+                mf = MouseFlags.Button1DoubleClicked;
+
+                break;
+
+            case MouseFlags.Button2Pressed:
+                mf = MouseFlags.Button2DoubleClicked;
+
+                break;
+
+            case MouseFlags.Button3Pressed:
+                mf = MouseFlags.Button3DoubleClicked;
+
+                break;
+        }
+
+        return mf;
+    }
+
+    private static MouseFlags GetButtonTripleClicked (MouseFlags mouseFlag)
+    {
+        MouseFlags mf = default;
+
+        switch (mouseFlag)
+        {
+            case MouseFlags.Button1Pressed:
+                mf = MouseFlags.Button1TripleClicked;
+
+                break;
+
+            case MouseFlags.Button2Pressed:
+                mf = MouseFlags.Button2TripleClicked;
+
+                break;
+
+            case MouseFlags.Button3Pressed:
+                mf = MouseFlags.Button3TripleClicked;
+
+                break;
+        }
+
+        return mf;
+    }
+
+    private static async Task ProcessButtonClickedAsync ()
+    {
+        await Task.Delay (300);
+        isButtonClicked = false;
+    }
+
+    private static async Task ProcessButtonDoubleClickedAsync ()
+    {
+        await Task.Delay (300);
+        isButtonDoubleClicked = false;
+    }
+
+    private static async Task ProcessContinuousButtonPressedAsync (MouseFlags mouseFlag, Action<MouseFlags, Point> continuousButtonPressedHandler)
+    {
+        // PERF: Pause and poll in a hot loop.
+        // This should be replaced with event dispatch and a synchronization primitive such as AutoResetEvent.
+        // Will make a massive difference in responsiveness.
+        while (isButtonPressed)
+        {
+            await Task.Delay (100);
+
+            View view = Application.WantContinuousButtonPressedView;
+
+            if (view is null)
+            {
+                break;
+            }
+
+            if (isButtonPressed && lastMouseButtonPressed is { } && (mouseFlag & MouseFlags.ReportMousePosition) == 0)
+            {
+                Application.Invoke (() => continuousButtonPressedHandler (mouseFlag, point ?? Point.Empty));
+            }
+        }
+    }
+
+    private static MouseFlags SetControlKeyStates (MouseFlags buttonState, MouseFlags mouseFlag)
+    {
+        if ((buttonState & MouseFlags.ButtonCtrl) != 0 && (mouseFlag & MouseFlags.ButtonCtrl) == 0)
+        {
+            mouseFlag |= MouseFlags.ButtonCtrl;
+        }
+
+        if ((buttonState & MouseFlags.ButtonShift) != 0 && (mouseFlag & MouseFlags.ButtonShift) == 0)
+        {
+            mouseFlag |= MouseFlags.ButtonShift;
+        }
+
+        if ((buttonState & MouseFlags.ButtonAlt) != 0 && (mouseFlag & MouseFlags.ButtonAlt) == 0)
+        {
+            mouseFlag |= MouseFlags.ButtonAlt;
+        }
+
+        return mouseFlag;
+    }
+
+    #region Cursor
+
+    //ESC [ M - RI Reverse Index – Performs the reverse operation of \n, moves cursor up one line, maintains horizontal position, scrolls buffer if necessary*
+
+    /// <summary>
+    ///     ESC [ 7 - Save Cursor Position in Memory**
+    /// </summary>
+    public static readonly string CSI_SaveCursorPosition = CSI + "7";
+
+    /// <summary>
+    ///     ESC [ 8 - DECSR Restore Cursor Position from Memory**
+    /// </summary>
+    public static readonly string CSI_RestoreCursorPosition = CSI + "8";
+
+    /// <summary>
+    ///     ESC [ 8 ; height ; width t - Set Terminal Window Size
+    ///     https://terminalguide.namepad.de/seq/csi_st-8/
+    /// </summary>
+    public static string CSI_SetTerminalWindowSize (int height, int width) { return $"{CSI}8;{height};{width}t"; }
+
+    //ESC [ < n > A - CUU - Cursor Up       Cursor up by < n >
+    //ESC [ < n > B - CUD - Cursor Down     Cursor down by < n >
+    //ESC [ < n > C - CUF - Cursor Forward  Cursor forward (Right) by < n >
+    //ESC [ < n > D - CUB - Cursor Backward Cursor backward (Left) by < n >
+    //ESC [ < n > E - CNL - Cursor Next Line - Cursor down < n > lines from current position
+    //ESC [ < n > F - CPL - Cursor Previous Line    Cursor up < n > lines from current position
+    //ESC [ < n > G - CHA - Cursor Horizontal Absolute      Cursor moves to < n > th position horizontally in the current line
+    //ESC [ < n > d - VPA - Vertical Line Position Absolute Cursor moves to the < n > th position vertically in the current column
+
+    /// <summary>
+    ///     ESC [ y ; x H - CUP Cursor Position - Cursor moves to x ; y coordinate within the viewport, where x is the column
+    ///     of the y line
+    /// </summary>
+    /// <param name="row">Origin is (1,1).</param>
+    /// <param name="col">Origin is (1,1).</param>
+    /// <returns></returns>
+    public static string CSI_SetCursorPosition (int row, int col) { return $"{CSI}{row};{col}H"; }
+
+    //ESC [ <y> ; <x> f - HVP     Horizontal Vertical Position* Cursor moves to<x>; <y> coordinate within the viewport, where <x> is the column of the<y> line
+    //ESC [ s - ANSISYSSC       Save Cursor – Ansi.sys emulation	**With no parameters, performs a save cursor operation like DECSC
+    //ESC [ u - ANSISYSRC       Restore Cursor – Ansi.sys emulation	**With no parameters, performs a restore cursor operation like DECRC
+    //ESC [ ? 12 h - ATT160  Text Cursor Enable Blinking     Start the cursor blinking
+    //ESC [ ? 12 l - ATT160  Text Cursor Disable Blinking    Stop blinking the cursor
+    /// <summary>
+    ///     ESC [ ? 25 h - DECTCEM Text Cursor Enable Mode Show    Show the cursor
+    /// </summary>
+    public static readonly string CSI_ShowCursor = CSI + "?25h";
+
+    /// <summary>
+    ///     ESC [ ? 25 l - DECTCEM Text Cursor Enable Mode Hide    Hide the cursor
+    /// </summary>
+    public static readonly string CSI_HideCursor = CSI + "?25l";
+
+    //ESC [ ? 12 h - ATT160  Text Cursor Enable Blinking     Start the cursor blinking
+    //ESC [ ? 12 l - ATT160  Text Cursor Disable Blinking    Stop blinking the cursor
+    //ESC [ ? 25 h - DECTCEM Text Cursor Enable Mode Show    Show the cursor
+    //ESC [ ? 25 l - DECTCEM Text Cursor Enable Mode Hide    Hide the cursor
+
+    /// <summary>
+    ///     Styles for ANSI ESC "[x q" - Set Cursor Style
+    /// </summary>
+    public enum DECSCUSR_Style
+    {
+        /// <summary>
+        ///     DECSCUSR - User Shape - Default cursor shape configured by the user
+        /// </summary>
+        UserShape = 0,
+
+        /// <summary>
+        ///     DECSCUSR - Blinking Block - Blinking block cursor shape
+        /// </summary>
+        BlinkingBlock = 1,
+
+        /// <summary>
+        ///     DECSCUSR - Steady Block - Steady block cursor shape
+        /// </summary>
+        SteadyBlock = 2,
+
+        /// <summary>
+        ///     DECSCUSR - Blinking Underline - Blinking underline cursor shape
+        /// </summary>
+        BlinkingUnderline = 3,
+
+        /// <summary>
+        ///     DECSCUSR - Steady Underline - Steady underline cursor shape
+        /// </summary>
+        SteadyUnderline = 4,
+
+        /// <summary>
+        ///     DECSCUSR - Blinking Bar - Blinking bar cursor shape
+        /// </summary>
+        BlinkingBar = 5,
+
+        /// <summary>
+        ///     DECSCUSR - Steady Bar - Steady bar cursor shape
+        /// </summary>
+        SteadyBar = 6
+    }
+
+    /// <summary>
+    ///     ESC [ n SP q - Select Cursor Style (DECSCUSR)
+    ///     https://terminalguide.namepad.de/seq/csi_sq_t_space/
+    /// </summary>
+    /// <param name="style"></param>
+    /// <returns></returns>
+    public static string CSI_SetCursorStyle (DECSCUSR_Style style) { return $"{CSI}{(int)style} q"; }
+
+    #endregion
+
+    #region Colors
+
+    /// <summary>
+    ///     ESC [ (n) m - SGR - Set Graphics Rendition - Set the format of the screen and text as specified by (n)
+    ///     This command is special in that the (n) position can accept between 0 and 16 parameters separated by semicolons.
+    ///     When no parameters are specified, it is treated the same as a single 0 parameter.
+    ///     https://terminalguide.namepad.de/seq/csi_sm/
+    /// </summary>
+    public static string CSI_SetGraphicsRendition (params int [] parameters) { return $"{CSI}{string.Join (";", parameters)}m"; }
+
+    /// <summary>
+    ///     ESC [ (n) m - Uses <see cref="CSI_SetGraphicsRendition(int[])"/> to set the foreground color.
+    /// </summary>
+    /// <param name="code">One of the 16 color codes.</param>
+    /// <returns></returns>
+    public static string CSI_SetForegroundColor (AnsiColorCode code) { return CSI_SetGraphicsRendition ((int)code); }
+
+    /// <summary>
+    ///     ESC [ (n) m - Uses <see cref="CSI_SetGraphicsRendition(int[])"/> to set the background color.
+    /// </summary>
+    /// <param name="code">One of the 16 color codes.</param>
+    /// <returns></returns>
+    public static string CSI_SetBackgroundColor (AnsiColorCode code) { return CSI_SetGraphicsRendition ((int)code + 10); }
+
+    /// <summary>
+    ///     ESC[38;5;{id}m - Set foreground color (256 colors)
+    /// </summary>
+    public static string CSI_SetForegroundColor256 (int color) { return $"{CSI}38;5;{color}m"; }
+
+    /// <summary>
+    ///     ESC[48;5;{id}m - Set background color (256 colors)
+    /// </summary>
+    public static string CSI_SetBackgroundColor256 (int color) { return $"{CSI}48;5;{color}m"; }
+
+    /// <summary>
+    ///     ESC[38;2;{r};{g};{b}m	Set foreground color as RGB.
+    /// </summary>
+    public static string CSI_SetForegroundColorRGB (int r, int g, int b) { return $"{CSI}38;2;{r};{g};{b}m"; }
+
+    /// <summary>
+    ///     ESC[48;2;{r};{g};{b}m	Set background color as RGB.
+    /// </summary>
+    public static string CSI_SetBackgroundColorRGB (int r, int g, int b) { return $"{CSI}48;2;{r};{g};{b}m"; }
+
+    #endregion
+
+    #region Requests
+
+    /// <summary>
+    ///     ESC [ ? 6 n - Request Cursor Position Report (?) (DECXCPR)
+    ///     https://terminalguide.namepad.de/seq/csi_sn__p-6/
+    /// </summary>
+    public static readonly string CSI_RequestCursorPositionReport = CSI + "?6n";
+
+    /// <summary>
+    ///     The terminal reply to <see cref="CSI_RequestCursorPositionReport"/>. ESC [ ? (y) ; (x) R
+    /// </summary>
+    public const string CSI_RequestCursorPositionReport_Terminator = "R";
+
+    /// <summary>
+    ///     ESC [ 0 c - Send Device Attributes (Primary DA)
+    ///     https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Application-Program-Command-functions
+    ///     https://www.xfree86.org/current/ctlseqs.html
+    ///     Windows Terminal v1.17 and below emits “\x1b[?1;0c”, indicating "VT101 with No Options".
+    ///     Windows Terminal v1.18+ emits: \x1b[?61;6;7;22;23;24;28;32;42c"
+    ///     See https://github.com/microsoft/terminal/pull/14906
+    ///     61 - The device conforms to level 1 of the character cell display architecture
+    ///     (See https://github.com/microsoft/terminal/issues/15693#issuecomment-1633304497)
+    ///     6 = Selective erase
+    ///     7 = Soft fonts
+    ///     22 = Color text
+    ///     23 = Greek character sets
+    ///     24 = Turkish character sets
+    ///     28 = Rectangular area operations
+    ///     32 = Text macros
+    ///     42 = ISO Latin-2 character set
+    /// </summary>
+    public static readonly string CSI_SendDeviceAttributes = CSI + "0c";
+
+    /// <summary>
+    ///     ESC [ > 0 c - Send Device Attributes (Secondary DA)
+    ///     Windows Terminal v1.18+ emits: "\x1b[>0;10;1c" (vt100, firmware version 1.0, vt220)
+    /// </summary>
+    public static readonly string CSI_SendDeviceAttributes2 = CSI + ">0c";
+
+    /// <summary>
+    ///     The terminator indicating a reply to <see cref="CSI_SendDeviceAttributes"/> or
+    ///     <see cref="CSI_SendDeviceAttributes2"/>
+    /// </summary>
+    public const string CSI_ReportDeviceAttributes_Terminator = "c";
+
+    /// <summary>
+    ///     CSI 1 8 t  | yes | yes |  yes  | report window size in chars
+    ///     https://terminalguide.namepad.de/seq/csi_st-18/
+    /// </summary>
+    public static readonly string CSI_ReportTerminalSizeInChars = CSI + "18t";
+
+    /// <summary>
+    ///     The terminator indicating a reply to <see cref="CSI_ReportTerminalSizeInChars"/> : ESC [ 8 ; height ; width t
+    /// </summary>
+    public const string CSI_ReportTerminalSizeInChars_Terminator = "t";
+
+    /// <summary>
+    ///     The value of the response to <see cref="CSI_ReportTerminalSizeInChars"/> indicating value 1 and 2 are the terminal
+    ///     size in chars.
+    /// </summary>
+    public const string CSI_ReportTerminalSizeInChars_ResponseValue = "8";
+
+    #endregion
 }

+ 1698 - 1978
Terminal.Gui/ConsoleDrivers/FakeDriver/FakeConsole.cs

@@ -1,1990 +1,1710 @@
 //
 // FakeConsole.cs: A fake .NET Windows Console API implementation for unit tests.
 //
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Text;
+
 using Terminal.Gui.ConsoleDrivers;
-using Rune = System.Text.Rune;
 
 namespace Terminal.Gui;
 
 #pragma warning disable RCS1138 // Add summary to documentation comment.
-/// <summary>
-/// 
-/// </summary>
-public static class FakeConsole {
+/// <summary></summary>
+public static class FakeConsole
+{
 #pragma warning restore RCS1138 // Add summary to documentation comment.
 
-	//
-	// Summary:
-	//	Gets or sets the width of the console window.
-	//
-	// Returns:
-	//	The width of the console window measured in columns.
-	//
-	// Exceptions:
-	//	T:System.ArgumentOutOfRangeException:
-	//	The value of the System.Console.WindowWidth property or the value of the System.Console.WindowHeight
-	//	property is less than or equal to 0.-or-The value of the System.Console.WindowHeight
-	//	property plus the value of the System.Console.WindowTop property is greater than
-	//	or equal to System.Int16.MaxValue.-or-The value of the System.Console.WindowWidth
-	//	property or the value of the System.Console.WindowHeight property is greater
-	//	than the largest possible window width or height for the current screen resolution
-	//	and console font.
-	//
-	//	T:System.IO.IOException:
-	//	Error reading or writing information.
+    //
+    // Summary:
+    //	Gets or sets the width of the console window.
+    //
+    // Returns:
+    //	The width of the console window measured in columns.
+    //
+    // Exceptions:
+    //	T:System.ArgumentOutOfRangeException:
+    //	The value of the System.Console.WindowWidth property or the value of the System.Console.WindowHeight
+    //	property is less than or equal to 0.-or-The value of the System.Console.WindowHeight
+    //	property plus the value of the System.Console.WindowTop property is greater than
+    //	or equal to System.Int16.MaxValue.-or-The value of the System.Console.WindowWidth
+    //	property or the value of the System.Console.WindowHeight property is greater
+    //	than the largest possible window width or height for the current screen resolution
+    //	and console font.
+    //
+    //	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; } = WIDTH;
-	//
-	// Summary:
-	//	Gets a value that indicates whether output has been redirected from the standard
-	//	output stream.
-	//
-	// Returns:
-	//	true if output is redirected; otherwise, false.
-	/// <summary>
-	/// 
-	/// </summary>
-	public static bool IsOutputRedirected { get; }
-	//
-	// Summary:
-	//	Gets a value that indicates whether the error output stream has been redirected
-	//	from the standard error stream.
-	//
-	// Returns:
-	//	true if error output is redirected; otherwise, false.
-	/// <summary>
-	/// 
-	/// </summary>
-	public static bool IsErrorRedirected { get; }
-	//
-	// Summary:
-	//	Gets the standard input stream.
-	//
-	// Returns:
-	//	A System.IO.TextReader that represents the standard input stream.
-	/// <summary>
-	/// 
-	/// </summary>
-	public static TextReader In { get; }
-	//
-	// Summary:
-	//	Gets the standard output stream.
-	//
-	// Returns:
-	//	A System.IO.TextWriter that represents the standard output stream.
-	/// <summary>
-	/// 
-	/// </summary>
-	public static TextWriter Out { get; }
-	//
-	// Summary:
-	//	Gets the standard error output stream.
-	//
-	// Returns:
-	//	A System.IO.TextWriter that represents the standard error output stream.
-	/// <summary>
-	/// 
-	/// </summary>
-	public static TextWriter Error { get; }
-	//
-	// Summary:
-	//	Gets or sets the encoding the console uses to read input.
-	//
-	// Returns:
-	//	The encoding used to read console input.
-	//
-	// Exceptions:
-	//	T:System.ArgumentNullException:
-	//	The property value in a set operation is null.
-	//
-	//	T:System.IO.IOException:
-	//	An error occurred during the execution of this operation.
-	//
-	//	T:System.Security.SecurityException:
-	//	Your application does not have permission to perform this operation.
-	/// <summary>
-	/// 
-	/// </summary>
-	public static Encoding InputEncoding { get; set; }
-	//
-	// Summary:
-	//	Gets or sets the encoding the console uses to write output.
-	//
-	// Returns:
-	//	The encoding used to write console output.
-	//
-	// Exceptions:
-	//	T:System.ArgumentNullException:
-	//	The property value in a set operation is null.
-	//
-	//	T:System.IO.IOException:
-	//	An error occurred during the execution of this operation.
-	//
-	//	T:System.Security.SecurityException:
-	//	Your application does not have permission to perform this operation.
-	/// <summary>
-	/// 
-	/// </summary>
-	public static Encoding OutputEncoding { get; set; }
-	//
-	// Summary:
-	//	Gets or sets the background color of the console.
-	//
-	// Returns:
-	//	A value that specifies the background color of the console; that is, the color
-	//	that appears behind each character. The default is black.
-	//
-	// Exceptions:
-	//	T:System.ArgumentException:
-	//	The color specified in a set operation is not a valid member of System.ConsoleColor.
-	//
-	//	T:System.Security.SecurityException:
-	//	The user does not have permission to perform this action.
-	//
-	//	T:System.IO.IOException:
-	//	An I/O error occurred.
-
-	static ConsoleColor _defaultBackgroundColor = ConsoleColor.Black;
-
-	/// <summary>
-	/// 
-	/// </summary>
-	public static ConsoleColor BackgroundColor { get; set; } = _defaultBackgroundColor;
-
-	//
-	// Summary:
-	//	Gets or sets the foreground color of the console.
-	//
-	// Returns:
-	//	A System.ConsoleColor that specifies the foreground color of the console; that
-	//	is, the color of each character that is displayed. The default is gray.
-	//
-	// Exceptions:
-	//	T:System.ArgumentException:
-	//	The color specified in a set operation is not a valid member of System.ConsoleColor.
-	//
-	//	T:System.Security.SecurityException:
-	//	The user does not have permission to perform this action.
-	//
-	//	T:System.IO.IOException:
-	//	An I/O error occurred.
-
-	static ConsoleColor _defaultForegroundColor = ConsoleColor.Gray;
-
-	/// <summary>
-	/// 
-	/// </summary>
-	public static ConsoleColor ForegroundColor { get; set; } = _defaultForegroundColor;
-	//
-	// Summary:
-	//	Gets or sets the height of the buffer area.
-	//
-	// Returns:
-	//	The current height, in rows, of the buffer area.
-	//
-	// Exceptions:
-	//	T:System.ArgumentOutOfRangeException:
-	//	The value in a set operation is less than or equal to zero.-or- The value in
-	//	a set operation is greater than or equal to System.Int16.MaxValue.-or- The value
-	//	in a set operation is less than System.Console.WindowTop + System.Console.WindowHeight.
-	//
-	//	T:System.Security.SecurityException:
-	//	The user does not have permission to perform this action.
-	//
-	//	T:System.IO.IOException:
-	//	An I/O error occurred.
-	/// <summary>
-	/// 
-	/// </summary>
-	public static int BufferHeight { get; set; } = HEIGHT;
-	//
-	// Summary:
-	//	Gets or sets the width of the buffer area.
-	//
-	// Returns:
-	//	The current width, in columns, of the buffer area.
-	//
-	// Exceptions:
-	//	T:System.ArgumentOutOfRangeException:
-	//	The value in a set operation is less than or equal to zero.-or- The value in
-	//	a set operation is greater than or equal to System.Int16.MaxValue.-or- The value
-	//	in a set operation is less than System.Console.WindowLeft + System.Console.WindowWidth.
-	//
-	//	T:System.Security.SecurityException:
-	//	The user does not have permission to perform this action.
-	//
-	//	T:System.IO.IOException:
-	//	An I/O error occurred.
-	/// <summary>
-	/// 
-	/// </summary>
-	public static int BufferWidth { get; set; } = WIDTH;
-
-	//
-	// Summary:
-	//	Gets or sets the height of the console window area.
-	//
-	// Returns:
-	//	The height of the console window measured in rows.
-	//
-	// Exceptions:
-	//	T:System.ArgumentOutOfRangeException:
-	//	The value of the System.Console.WindowWidth property or the value of the System.Console.WindowHeight
-	//	property is less than or equal to 0.-or-The value of the System.Console.WindowHeight
-	//	property plus the value of the System.Console.WindowTop property is greater than
-	//	or equal to System.Int16.MaxValue.-or-The value of the System.Console.WindowWidth
-	//	property or the value of the System.Console.WindowHeight property is greater
-	//	than the largest possible window width or height for the current screen resolution
-	//	and console font.
-	//
-	//	T:System.IO.IOException:
-	//	Error reading or writing information.
-	/// <summary>
-	/// 
-	/// </summary>
-	public static int WindowHeight { get; set; } = HEIGHT;
-	//
-	// Summary:
-	//	Gets or sets a value indicating whether the combination of the System.ConsoleModifiers.Control
-	//	modifier key and System.ConsoleKey.C console key (Ctrl+C) is treated as ordinary
-	//	input or as an interruption that is handled by the operating system.
-	//
-	// Returns:
-	//	true if Ctrl+C is treated as ordinary input; otherwise, false.
-	//
-	// Exceptions:
-	//	T:System.IO.IOException:
-	//	Unable to get or set the input mode of the console input buffer.
-	/// <summary>
-	/// 
-	/// </summary>
-	public static bool TreatControlCAsInput { get; set; }
-	//
-	// Summary:
-	//	Gets the largest possible number of console window columns, based on the current
-	//	font and screen resolution.
-	//
-	// Returns:
-	//	The width of the largest possible console window measured in columns.
-	/// <summary>
-	/// 
-	/// </summary>
-	public static int LargestWindowWidth { get; }
-	//
-	// Summary:
-	//	Gets the largest possible number of console window rows, based on the current
-	//	font and screen resolution.
-	//
-	// Returns:
-	//	The height of the largest possible console window measured in rows.
-	/// <summary>
-	/// 
-	/// </summary>
-	public static int LargestWindowHeight { get; }
-	//
-	// Summary:
-	//	Gets or sets the leftmost position of the console window area relative to the
-	//	screen buffer.
-	//
-	// Returns:
-	//	The leftmost console window position measured in columns.
-	//
-	// Exceptions:
-	//	T:System.ArgumentOutOfRangeException:
-	//	In a set operation, the value to be assigned is less than zero.-or-As a result
-	//	of the assignment, System.Console.WindowLeft plus System.Console.WindowWidth
-	//	would exceed System.Console.BufferWidth.
-	//
-	//	T:System.IO.IOException:
-	//	Error reading or writing information.
-	/// <summary>
-	/// 
-	/// </summary>
-	public static int WindowLeft { get; set; }
-	//
-	// Summary:
-	//	Gets or sets the top position of the console window area relative to the screen
-	//	buffer.
-	//
-	// Returns:
-	//	The uppermost console window position measured in rows.
-	//
-	// Exceptions:
-	//	T:System.ArgumentOutOfRangeException:
-	//	In a set operation, the value to be assigned is less than zero.-or-As a result
-	//	of the assignment, System.Console.WindowTop plus System.Console.WindowHeight
-	//	would exceed System.Console.BufferHeight.
-	//
-	//	T:System.IO.IOException:
-	//	Error reading or writing information.
-	/// <summary>
-	/// 
-	/// </summary>
-	public static int WindowTop { get; set; }
-	//
-	// Summary:
-	//	Gets or sets the column position of the cursor within the buffer area.
-	//
-	// Returns:
-	//	The current position, in columns, of the cursor.
-	//
-	// Exceptions:
-	//	T:System.ArgumentOutOfRangeException:
-	//	The value in a set operation is less than zero.-or- The value in a set operation
-	//	is greater than or equal to System.Console.BufferWidth.
-	//
-	//	T:System.Security.SecurityException:
-	//	The user does not have permission to perform this action.
-	//
-	//	T:System.IO.IOException:
-	//	An I/O error occurred.
-	/// <summary>
-	/// 
-	/// </summary>
-	public static int CursorLeft { get; set; }
-	//
-	// Summary:
-	//	Gets or sets the row position of the cursor within the buffer area.
-	//
-	// Returns:
-	//	The current position, in rows, of the cursor.
-	//
-	// Exceptions:
-	//	T:System.ArgumentOutOfRangeException:
-	//	The value in a set operation is less than zero.-or- The value in a set operation
-	//	is greater than or equal to System.Console.BufferHeight.
-	//
-	//	T:System.Security.SecurityException:
-	//	The user does not have permission to perform this action.
-	//
-	//	T:System.IO.IOException:
-	//	An I/O error occurred.
-	/// <summary>
-	/// 
-	/// </summary>
-	public static int CursorTop { get; set; }
-	//
-	// Summary:
-	//	Gets or sets the height of the cursor within a character cell.
-	//
-	// Returns:
-	//	The size of the cursor expressed as a percentage of the height of a character
-	//	cell. The property value ranges from 1 to 100.
-	//
-	// Exceptions:
-	//	T:System.ArgumentOutOfRangeException:
-	//	The value specified in a set operation is less than 1 or greater than 100.
-	//
-	//	T:System.Security.SecurityException:
-	//	The user does not have permission to perform this action.
-	//
-	//	T:System.IO.IOException:
-	//	An I/O error occurred.
-	/// <summary>
-	/// 
-	/// </summary>
-	public static int CursorSize { get; set; }
-	//
-	// Summary:
-	//	Gets or sets a value indicating whether the cursor is visible.
-	//
-	// Returns:
-	//	true if the cursor is visible; otherwise, false.
-	//
-	// Exceptions:
-	//	T:System.Security.SecurityException:
-	//	The user does not have permission to perform this action.
-	//
-	//	T:System.IO.IOException:
-	//	An I/O error occurred.
-	/// <summary>
-	/// 
-	/// </summary>
-	public static bool CursorVisible { get; set; }
-	//
-	// Summary:
-	//	Gets or sets the title to display in the console title bar.
-	//
-	// Returns:
-	//	The string to be displayed in the title bar of the console. The maximum length
-	//	of the title string is 24500 characters.
-	//
-	// Exceptions:
-	//	T:System.InvalidOperationException:
-	//	In a get operation, the retrieved title is longer than 24500 characters.
-	//
-	//	T:System.ArgumentOutOfRangeException:
-	//	In a set operation, the specified title is longer than 24500 characters.
-	//
-	//	T:System.ArgumentNullException:
-	//	In a set operation, the specified title is null.
-	//
-	//	T:System.IO.IOException:
-	//	An I/O error occurred.
-	/// <summary>
-	/// 
-	/// </summary>
-	public static string Title { get; set; }
-	//
-	// Summary:
-	//	Gets a value indicating whether a key press is available in the input stream.
-	//
-	// Returns:
-	//	true if a key press is available; otherwise, false.
-	//
-	// Exceptions:
-	//	T:System.IO.IOException:
-	//	An I/O error occurred.
-	//
-	//	T:System.InvalidOperationException:
-	//	Standard input is redirected to a file instead of the keyboard.
-	/// <summary>
-	/// 
-	/// </summary>
-	public static bool KeyAvailable { get; }
-	//
-	// Summary:
-	//	Gets a value that indicates whether input has been redirected from the standard
-	//	input stream.
-	//
-	// Returns:
-	//	true if input is redirected; otherwise, false.
-	/// <summary>
-	/// 
-	/// </summary>
-	public static bool IsInputRedirected { get; }
-
-	//
-	// Summary:
-	//	Plays the sound of a beep through the console speaker.
-	//
-	// Exceptions:
-	//	T:System.Security.HostProtectionException:
-	//	This method was executed on a server, such as SQL Server, that does not permit
-	//	access to a user interface.
-	/// <summary>
-	/// 
-	/// </summary>
-	public static void Beep ()
-	{
-		throw new NotImplementedException ();
-	}
-	//
-	// Summary:
-	//	Plays the sound of a beep of a specified frequency and duration through the console
-	//	speaker.
-	//
-	// Parameters:
-	//	frequency:
-	//	The frequency of the beep, ranging from 37 to 32767 hertz.
-	//
-	//	duration:
-	//	The duration of the beep measured in milliseconds.
-	//
-	// Exceptions:
-	//	T:System.ArgumentOutOfRangeException:
-	//	frequency is less than 37 or more than 32767 hertz.-or- duration is less than
-	//	or equal to zero.
-	//
-	//	T:System.Security.HostProtectionException:
-	//	This method was executed on a server, such as SQL Server, that does not permit
-	//	access to the console.
-	/// <summary>
-	/// 
-	/// </summary>
-	public static void Beep (int frequency, int duration)
-	{
-		throw new NotImplementedException ();
-	}
-	//
-	// Summary:
-	//	Clears the console buffer and corresponding console window of display information.
-	//
-	// Exceptions:
-	//	T:System.IO.IOException:
-	//	An I/O error occurred.
-
-	static char [,] _buffer = new char [WindowWidth, WindowHeight];
-
-	/// <summary>
-	/// 
-	/// </summary>
-	public static void Clear ()
-	{
-		_buffer = new char [BufferWidth, BufferHeight];
-		SetCursorPosition (0, 0);
-	}
-
-	//
-	// Summary:
-	//	Copies a specified source area of the screen buffer to a specified destination
-	//	area.
-	//
-	// Parameters:
-	//	sourceLeft:
-	//	The leftmost column of the source area.
-	//
-	//	sourceTop:
-	//	The topmost row of the source area.
-	//
-	//	sourceWidth:
-	//	The number of columns in the source area.
-	//
-	//	sourceHeight:
-	//	The number of rows in the source area.
-	//
-	//	targetLeft:
-	//	The leftmost column of the destination area.
-	//
-	//	targetTop:
-	//	The topmost row of the destination area.
-	//
-	// Exceptions:
-	//	T:System.ArgumentOutOfRangeException:
-	//	One or more of the parameters is less than zero.-or- sourceLeft or targetLeft
-	//	is greater than or equal to System.Console.BufferWidth.-or- sourceTop or targetTop
-	//	is greater than or equal to System.Console.BufferHeight.-or- sourceTop + sourceHeight
-	//	is greater than or equal to System.Console.BufferHeight.-or- sourceLeft + sourceWidth
-	//	is greater than or equal to System.Console.BufferWidth.
-	//
-	//	T:System.Security.SecurityException:
-	//	The user does not have permission to perform this action.
-	//
-	//	T:System.IO.IOException:
-	//	An I/O error occurred.
-	/// <summary>
-	/// 
-	/// </summary>
-	public static void MoveBufferArea (int sourceLeft, int sourceTop, int sourceWidth, int sourceHeight, int targetLeft, int targetTop)
-	{
-		throw new NotImplementedException ();
-	}
-
-	//
-	// Summary:
-	//	Copies a specified source area of the screen buffer to a specified destination
-	//	area.
-	//
-	// Parameters:
-	//	sourceLeft:
-	//	The leftmost column of the source area.
-	//
-	//	sourceTop:
-	//	The topmost row of the source area.
-	//
-	//	sourceWidth:
-	//	The number of columns in the source area.
-	//
-	//	sourceHeight:
-	//	The number of rows in the source area.
-	//
-	//	targetLeft:
-	//	The leftmost column of the destination area.
-	//
-	//	targetTop:
-	//	The topmost row of the destination area.
-	//
-	//	sourceChar:
-	//	The character used to fill the source area.
-	//
-	//	sourceForeColor:
-	//	The foreground color used to fill the source area.
-	//
-	//	sourceBackColor:
-	//	The background color used to fill the source area.
-	//
-	// Exceptions:
-	//	T:System.ArgumentOutOfRangeException:
-	//	One or more of the parameters is less than zero.-or- sourceLeft or targetLeft
-	//	is greater than or equal to System.Console.BufferWidth.-or- sourceTop or targetTop
-	//	is greater than or equal to System.Console.BufferHeight.-or- sourceTop + sourceHeight
-	//	is greater than or equal to System.Console.BufferHeight.-or- sourceLeft + sourceWidth
-	//	is greater than or equal to System.Console.BufferWidth.
-	//
-	//	T:System.ArgumentException:
-	//	One or both of the color parameters is not a member of the System.ConsoleColor
-	//	enumeration.
-	//
-	//	T:System.Security.SecurityException:
-	//	The user does not have permission to perform this action.
-	//
-	//	T:System.IO.IOException:
-	//	An I/O error occurred.
-	//[SecuritySafeCritical]
-	/// <summary>
-	/// 
-	/// </summary>
-	public static void MoveBufferArea (int sourceLeft, int sourceTop, int sourceWidth, int sourceHeight, int targetLeft, int targetTop, char sourceChar, ConsoleColor sourceForeColor, ConsoleColor sourceBackColor)
-	{
-		throw new NotImplementedException ();
-	}
-
-	//
-	// Summary:
-	//	Acquires the standard error stream.
-	//
-	// Returns:
-	//	The standard error stream.
-	/// <summary>
-	/// 
-	/// </summary>
-	public static Stream OpenStandardError ()
-	{
-		throw new NotImplementedException ();
-	}
-
-	//
-	// Summary:
-	//	Acquires the standard error stream, which is set to a specified buffer size.
-	//
-	// Parameters:
-	//	bufferSize:
-	//	The internal stream buffer size.
-	//
-	// Returns:
-	//	The standard error stream.
-	//
-	// Exceptions:
-	//	T:System.ArgumentOutOfRangeException:
-	//	bufferSize is less than or equal to zero.
-	/// <summary>
-	/// 
-	/// </summary>
-	public static Stream OpenStandardError (int bufferSize)
-	{
-		throw new NotImplementedException ();
-	}
-
-	//
-	// Summary:
-	//	Acquires the standard input stream, which is set to a specified buffer size.
-	//
-	// Parameters:
-	//	bufferSize:
-	//	The internal stream buffer size.
-	//
-	// Returns:
-	//	The standard input stream.
-	//
-	// Exceptions:
-	//	T:System.ArgumentOutOfRangeException:
-	//	bufferSize is less than or equal to zero.
-	/// <summary>
-	/// 
-	/// </summary>
-	public static Stream OpenStandardInput (int bufferSize)
-	{
-		throw new NotImplementedException ();
-	}
-
-	//
-	// Summary:
-	//	Acquires the standard input stream.
-	//
-	// Returns:
-	//	The standard input stream.
-	/// <summary>
-	/// 
-	/// </summary>
-	public static Stream OpenStandardInput ()
-	{
-		throw new NotImplementedException ();
-	}
-
-	//
-	// Summary:
-	//	Acquires the standard output stream, which is set to a specified buffer size.
-	//
-	// Parameters:
-	//	bufferSize:
-	//	The internal stream buffer size.
-	//
-	// Returns:
-	//	The standard output stream.
-	//
-	// Exceptions:
-	//	T:System.ArgumentOutOfRangeException:
-	//	bufferSize is less than or equal to zero.
-	/// <summary>
-	/// 
-	/// </summary>
-	public static Stream OpenStandardOutput (int bufferSize)
-	{
-		throw new NotImplementedException ();
-	}
-
-	//
-	// Summary:
-	//	Acquires the standard output stream.
-	//
-	// Returns:
-	//	The standard output stream.
-	/// <summary>
-	/// 
-	/// </summary>
-	public static Stream OpenStandardOutput ()
-	{
-		throw new NotImplementedException ();
-	}
-
-	//
-	// Summary:
-	//	Reads the next character from the standard input stream.
-	//
-	// Returns:
-	//	The next character from the input stream, or negative one (-1) if there are currently
-	//	no more characters to be read.
-	//
-	// Exceptions:
-	//	T:System.IO.IOException:
-	//	An I/O error occurred.
-	/// <summary>
-	/// 
-	/// </summary>
-	public static int Read ()
-	{
-		throw new NotImplementedException ();
-	}
-
-	//
-	// Summary:
-	//	Obtains the next character or function key pressed by the user. The pressed key
-	//	is optionally displayed in the console window.
-	//
-	// Parameters:
-	//	intercept:
-	//	Determines whether to display the pressed key in the console window. true to
-	//	not display the pressed key; otherwise, false.
-	//
-	// Returns:
-	//	An object that describes the System.ConsoleKey constant and Unicode character,
-	//	if any, that correspond to the pressed console key. The System.ConsoleKeyInfo
-	//	object also describes, in a bitwise combination of System.ConsoleModifiers values,
-	//	whether one or more Shift, Alt, or Ctrl modifier keys was pressed simultaneously
-	//	with the console key.
-	//
-	// Exceptions:
-	//	T:System.InvalidOperationException:
-	//	The System.Console.In property is redirected from some stream other than the
-	//	console.
-	//[SecuritySafeCritical]
-
-	/// <summary>
-	/// A stack of keypresses to return when ReadKey is called.
-	/// </summary>
-	public static Stack<ConsoleKeyInfo> MockKeyPresses = new Stack<ConsoleKeyInfo> ();
-
-	/// <summary>
-	///  Helper to push a <see cref="KeyCode"/> onto <see cref="MockKeyPresses"/>.
-	/// </summary>
-	/// <param name="key"></param>
-	public static void PushMockKeyPress (KeyCode key)
-	{
-		MockKeyPresses.Push (new ConsoleKeyInfo (
-			(char)(key & ~KeyCode.CtrlMask & ~KeyCode.ShiftMask & ~KeyCode.AltMask),
-			ConsoleKeyMapping.GetConsoleKeyInfoFromKeyCode (key).Key,
-			key.HasFlag (KeyCode.ShiftMask),
-			key.HasFlag (KeyCode.AltMask),
-			key.HasFlag (KeyCode.CtrlMask)));
-	}
-
-	//
-	// Summary:
-	//	Obtains the next character or function key pressed by the user. The pressed key
-	//	is displayed in the console window.
-	//
-	// Returns:
-	//	An object that describes the System.ConsoleKey constant and Unicode character,
-	//	if any, that correspond to the pressed console key. The System.ConsoleKeyInfo
-	//	object also describes, in a bitwise combination of System.ConsoleModifiers values,
-	//	whether one or more Shift, Alt, or Ctrl modifier keys was pressed simultaneously
-	//	with the console key.
-	//
-	// Exceptions:
-	//	T:System.InvalidOperationException:
-	//	The System.Console.In property is redirected from some stream other than the
-	//	console.
-	/// <summary>
-	/// 
-	/// </summary>
-	public static ConsoleKeyInfo ReadKey ()
-	{
-		throw new NotImplementedException ();
-	}
-
-	//
-	// Summary:
-	//	Reads the next line of characters from the standard input stream.
-	//
-	// Returns:
-	//	The next line of characters from the input stream, or null if no more lines are
-	//	available.
-	//
-	// Exceptions:
-	//	T:System.IO.IOException:
-	//	An I/O error occurred.
-	//
-	//	T:System.OutOfMemoryException:
-	//	There is insufficient memory to allocate a buffer for the returned string.
-	//
-	//	T:System.ArgumentOutOfRangeException:
-	//	The number of characters in the next line of characters is greater than System.Int32.MaxValue.
-	/// <summary>
-	/// 
-	/// </summary>
-	public static string ReadLine ()
-	{
-		throw new NotImplementedException ();
-	}
-
-	//
-	// Summary:
-	//	Sets the foreground and background console colors to their defaults.
-	//
-	// Exceptions:
-	//	T:System.Security.SecurityException:
-	//	The user does not have permission to perform this action.
-	//
-	//	T:System.IO.IOException:
-	//	An I/O error occurred.
-	//[SecuritySafeCritical]
-	/// <summary>
-	/// 
-	/// </summary>
-	public static void ResetColor ()
-	{
-		BackgroundColor = _defaultBackgroundColor;
-		ForegroundColor = _defaultForegroundColor;
-	}
-
-	//
-	// Summary:
-	//	Sets the height and width of the screen buffer area to the specified values.
-	//
-	// Parameters:
-	//	width:
-	//	The width of the buffer area measured in columns.
-	//
-	//	height:
-	//	The height of the buffer area measured in rows.
-	//
-	// Exceptions:
-	//	T:System.ArgumentOutOfRangeException:
-	//	height or width is less than or equal to zero.-or- height or width is greater
-	//	than or equal to System.Int16.MaxValue.-or- width is less than System.Console.WindowLeft
-	//	+ System.Console.WindowWidth.-or- height is less than System.Console.WindowTop
-	//	+ System.Console.WindowHeight.
-	//
-	//	T:System.Security.SecurityException:
-	//	The user does not have permission to perform this action.
-	//
-	//	T:System.IO.IOException:
-	//	An I/O error occurred.
-	//[SecuritySafeCritical]
-	/// <summary>
-	/// 
-	/// </summary>
-	public static void SetBufferSize (int width, int height)
-	{
-		BufferWidth = width;
-		BufferHeight = height;
-		_buffer = new char [BufferWidth, BufferHeight];
-	}
-
-	//
-	// Summary:
-	//	Sets the position of the cursor.
-	//
-	// Parameters:
-	//	left:
-	//	The column position of the cursor. Columns are numbered from left to right starting
-	//	at 0.
-	//
-	//	top:
-	//	The row position of the cursor. Rows are numbered from top to bottom starting
-	//	at 0.
-	//
-	// Exceptions:
-	//	T:System.ArgumentOutOfRangeException:
-	//	left or top is less than zero.-or- left is greater than or equal to System.Console.BufferWidth.-or-
-	//	top is greater than or equal to System.Console.BufferHeight.
-	//
-	//	T:System.Security.SecurityException:
-	//	The user does not have permission to perform this action.
-	//
-	//	T:System.IO.IOException:
-	//	An I/O error occurred.
-	//[SecuritySafeCritical]
-	/// <summary>
-	/// 
-	/// </summary>
-	public static void SetCursorPosition (int left, int top)
-	{
-		CursorLeft = left;
-		CursorTop = top;
-		WindowLeft = Math.Max (Math.Min (left, BufferWidth - WindowWidth), 0);
-		WindowTop = Math.Max (Math.Min (top, BufferHeight - WindowHeight), 0);
-	}
-
-	//
-	// Summary:
-	//	Sets the System.Console.Error property to the specified System.IO.TextWriter
-	//	object.
-	//
-	// Parameters:
-	//	newError:
-	//	A stream that is the new standard error output.
-	//
-	// Exceptions:
-	//	T:System.ArgumentNullException:
-	//	newError is null.
-	//
-	//	T:System.Security.SecurityException:
-	//	The caller does not have the required permission.
-	//[SecuritySafeCritical]
-	/// <summary>
-	/// 
-	/// </summary>
-	public static void SetError (TextWriter newError)
-	{
-		throw new NotImplementedException ();
-	}
-
-	//
-	// Summary:
-	//	Sets the System.Console.In property to the specified System.IO.TextReader object.
-	//
-	// Parameters:
-	//	newIn:
-	//	A stream that is the new standard input.
-	//
-	// Exceptions:
-	//	T:System.ArgumentNullException:
-	//	newIn is null.
-	//
-	//	T:System.Security.SecurityException:
-	//	The caller does not have the required permission.
-	//[SecuritySafeCritical]
-	/// <summary>
-	/// 
-	/// </summary>
-	public static void SetIn (TextReader newIn)
-	{
-		throw new NotImplementedException ();
-	}
-
-	//
-	// Summary:
-	//	Sets the System.Console.Out property to the specified System.IO.TextWriter object.
-	//
-	// Parameters:
-	//	newOut:
-	//	A stream that is the new standard output.
-	//
-	// Exceptions:
-	//	T:System.ArgumentNullException:
-	//	newOut is null.
-	//
-	//	T:System.Security.SecurityException:
-	//	The caller does not have the required permission.
-	//[SecuritySafeCritical]
-	/// <summary>
-	/// 
-	/// </summary>
-	/// <param name="newOut"></param>
-	public static void SetOut (TextWriter newOut)
-	{
-		throw new NotImplementedException ();
-	}
-
-	//
-	// Summary:
-	//	Sets the position of the console window relative to the screen buffer.
-	//
-	// Parameters:
-	//	left:
-	//	The column position of the upper left corner of the console window.
-	//
-	//	top:
-	//	The row position of the upper left corner of the console window.
-	//
-	// Exceptions:
-	//	T:System.ArgumentOutOfRangeException:
-	//	left or top is less than zero.-or- left + System.Console.WindowWidth is greater
-	//	than System.Console.BufferWidth.-or- top + System.Console.WindowHeight is greater
-	//	than System.Console.BufferHeight.
-	//
-	//	T:System.Security.SecurityException:
-	//	The user does not have permission to perform this action.
-	//
-	//	T:System.IO.IOException:
-	//	An I/O error occurred.
-	//[SecuritySafeCritical]
-	/// <summary>
-	/// 
-	/// </summary>
-	/// <param name="left"></param>
-	/// <param name="top"></param>
-	public static void SetWindowPosition (int left, int top)
-	{
-		WindowLeft = left;
-		WindowTop = top;
-	}
-
-	//
-	// Summary:
-	//	Sets the height and width of the console window to the specified values.
-	//
-	// Parameters:
-	//	width:
-	//	The width of the console window measured in columns.
-	//
-	//	height:
-	//	The height of the console window measured in rows.
-	//
-	// Exceptions:
-	//	T:System.ArgumentOutOfRangeException:
-	//	width or height is less than or equal to zero.-or- width plus System.Console.WindowLeft
-	//	or height plus System.Console.WindowTop is greater than or equal to System.Int16.MaxValue.
-	//	-or- width or height is greater than the largest possible window width or height
-	//	for the current screen resolution and console font.
-	//
-	//	T:System.Security.SecurityException:
-	//	The user does not have permission to perform this action.
-	//
-	//	T:System.IO.IOException:
-	//	An I/O error occurred.
-	//[SecuritySafeCritical]
-	/// <summary>
-	/// 
-	/// </summary>
-	/// <param name="width"></param>
-	/// <param name="height"></param>
-	public static void SetWindowSize (int width, int height)
-	{
-		WindowWidth = width;
-		WindowHeight = height;
-	}
-
-	//
-	// Summary:
-	//	Writes the specified string value to the standard output stream.
-	//
-	// Parameters:
-	//	value:
-	//	The value to write.
-	//
-	// Exceptions:
-	//	T:System.IO.IOException:
-	//	An I/O error occurred.
-	/// <summary>
-	/// 
-	/// </summary>
-	/// <param name="value"></param>
-	public static void Write (string value)
-	{
-		throw new NotImplementedException ();
-	}
-
-	//
-	// Summary:
-	//	Writes the text representation of the specified object to the standard output
-	//	stream.
-	//
-	// Parameters:
-	//	value:
-	//	The value to write, or null.
-	//
-	// Exceptions:
-	//	T:System.IO.IOException:
-	//	An I/O error occurred.
-	/// <summary>
-	/// 
-	/// </summary>
-	/// <param name="value"></param>
-	public static void Write (object value)
-	{
-		if (value is Rune rune) {
-			Write ((char)rune.Value);
-		} else {
-			throw new NotImplementedException ();
-		}
-	}
-
-	//
-	// Summary:
-	//	Writes the text representation of the specified 64-bit unsigned integer value
-	//	to the standard output stream.
-	//
-	// Parameters:
-	//	value:
-	//	The value to write.
-	//
-	// Exceptions:
-	//	T:System.IO.IOException:
-	//	An I/O error occurred.
-	//[CLSCompliant (false)]
-	/// <summary>
-	/// 
-	/// </summary>
-	/// <param name="value"></param>
-	public static void Write (ulong value)
-	{
-		throw new NotImplementedException ();
-	}
-
-	//
-	// Summary:
-	//	Writes the text representation of the specified 64-bit signed integer value to
-	//	the standard output stream.
-	//
-	// Parameters:
-	//	value:
-	//	The value to write.
-	//
-	// Exceptions:
-	//	T:System.IO.IOException:
-	//	An I/O error occurred.
-	/// <summary>
-	/// 
-	/// </summary>
-	/// <param name="value"></param>
-	public static void Write (long value)
-	{
-		throw new NotImplementedException ();
-	}
-
-	//
-	// Summary:
-	//	Writes the text representation of the specified objects to the standard output
-	//	stream using the specified format information.
-	//
-	// Parameters:
-	//	format:
-	//	A composite format string (see Remarks).
-	//
-	//	arg0:
-	//	The first object to write using format.
-	//
-	//	arg1:
-	//	The second object to write using format.
-	//
-	// Exceptions:
-	//	T:System.IO.IOException:
-	//	An I/O error occurred.
-	//
-	//	T:System.ArgumentNullException:
-	//	format is null.
-	//
-	//	T:System.FormatException:
-	//	The format specification in format is invalid.
-	/// <summary>
-	/// 
-	/// </summary>
-	/// <param name="format"></param>
-	/// <param name="arg0"></param>
-	/// <param name="arg1"></param>
-	public static void Write (string format, object arg0, object arg1)
-	{
-		throw new NotImplementedException ();
-	}
-
-	//
-	// Summary:
-	//	Writes the text representation of the specified 32-bit signed integer value to
-	//	the standard output stream.
-	//
-	// Parameters:
-	//	value:
-	//	The value to write.
-	//
-	// Exceptions:
-	//	T:System.IO.IOException:
-	//	An I/O error occurred.
-	/// <summary>
-	/// 
-	/// </summary>
-	/// <param name="value"></param>
-	public static void Write (int value)
-	{
-		throw new NotImplementedException ();
-	}
-
-	//
-	// Summary:
-	//	Writes the text representation of the specified object to the standard output
-	//	stream using the specified format information.
-	//
-	// Parameters:
-	//	format:
-	//	A composite format string (see Remarks).
-	//
-	//	arg0:
-	//	An object to write using format.
-	//
-	// Exceptions:
-	//	T:System.IO.IOException:
-	//	An I/O error occurred.
-	//
-	//	T:System.ArgumentNullException:
-	//	format is null.
-	//
-	//	T:System.FormatException:
-	//	The format specification in format is invalid.
-	/// <summary>
-	/// 
-	/// </summary>
-	/// <param name="format"></param>
-	/// <param name="arg0"></param>
-	public static void Write (string format, object arg0)
-	{
-		throw new NotImplementedException ();
-	}
-
-	//
-	// Summary:
-	//	Writes the text representation of the specified 32-bit unsigned integer value
-	//	to the standard output stream.
-	//
-	// Parameters:
-	//	value:
-	//	The value to write.
-	//
-	// Exceptions:
-	//	T:System.IO.IOException:
-	//	An I/O error occurred.
-	//[CLSCompliant (false)]
-	/// <summary>
-	/// 
-	/// </summary>
-	/// <param name="value"></param>
-	public static void Write (uint value)
-	{
-		throw new NotImplementedException ();
-	}
-
-	//[CLSCompliant (false)]
-	/// <summary>
-	/// 
-	/// </summary>
-	/// <param name="format"></param>
-	/// <param name="arg0"></param>
-	/// <param name="arg1"></param>
-	/// <param name="arg2"></param>
-	/// <param name="arg3"></param>
-	public static void Write (string format, object arg0, object arg1, object arg2, object arg3)
-	{
-		throw new NotImplementedException ();
-	}
-
-	//
-	// Summary:
-	//	Writes the text representation of the specified array of objects to the standard
-	//	output stream using the specified format information.
-	//
-	// Parameters:
-	//	format:
-	//	A composite format string (see Remarks).
-	//
-	//	arg:
-	//	An array of objects to write using format.
-	//
-	// Exceptions:
-	//	T:System.IO.IOException:
-	//	An I/O error occurred.
-	//
-	//	T:System.ArgumentNullException:
-	//	format or arg is null.
-	//
-	//	T:System.FormatException:
-	//	The format specification in format is invalid.
-	/// <summary>
-	/// 
-	/// </summary>
-	/// <param name="format"></param>
-	/// <param name="arg"></param>
-	public static void Write (string format, params object [] arg)
-	{
-		throw new NotImplementedException ();
-	}
-
-	//
-	// Summary:
-	//	Writes the text representation of the specified Boolean value to the standard
-	//	output stream.
-	//
-	// Parameters:
-	//	value:
-	//	The value to write.
-	//
-	// Exceptions:
-	//	T:System.IO.IOException:
-	//	An I/O error occurred.
-	/// <summary>
-	/// 
-	/// </summary>
-	/// <param name="value"></param>
-	public static void Write (bool value)
-	{
-		throw new NotImplementedException ();
-	}
-
-	//
-	// Summary:
-	//	Writes the specified Unicode character value to the standard output stream.
-	//
-	// Parameters:
-	//	value:
-	//	The value to write.
-	//
-	// Exceptions:
-	//	T:System.IO.IOException:
-	//	An I/O error occurred.
-	/// <summary>
-	/// 
-	/// </summary>
-	/// <param name="value"></param>
-	public static void Write (char value)
-	{
-		_buffer [CursorLeft, CursorTop] = value;
-	}
-
-	//
-	// Summary:
-	//	Writes the specified array of Unicode characters to the standard output stream.
-	//
-	// Parameters:
-	//	buffer:
-	//	A Unicode character array.
-	//
-	// Exceptions:
-	//	T:System.IO.IOException:
-	//	An I/O error occurred.
-	/// <summary>
-	/// 
-	/// </summary>
-	/// <param name="buffer"></param>
-	public static void Write (char [] buffer)
-	{
-		_buffer [CursorLeft, CursorTop] = (char)0;
-		foreach (var ch in buffer) {
-			_buffer [CursorLeft, CursorTop] += ch;
-		}
-	}
-
-	//
-	// Summary:
-	//	Writes the specified subarray of Unicode characters to the standard output stream.
-	//
-	// Parameters:
-	//	buffer:
-	//	An array of Unicode characters.
-	//
-	//	index:
-	//	The starting position in buffer.
-	//
-	//	count:
-	//	The number of characters to write.
-	//
-	// Exceptions:
-	//	T:System.ArgumentNullException:
-	//	buffer is null.
-	//
-	//	T:System.ArgumentOutOfRangeException:
-	//	index or count is less than zero.
-	//
-	//	T:System.ArgumentException:
-	//	index plus count specify a position that is not within buffer.
-	//
-	//	T:System.IO.IOException:
-	//	An I/O error occurred.
-	/// <summary>
-	/// 
-	/// </summary>
-	/// <param name="buffer"></param>
-	/// <param name="index"></param>
-	/// <param name="count"></param>
-	public static void Write (char [] buffer, int index, int count)
-	{
-		throw new NotImplementedException ();
-	}
-
-	//
-	// Summary:
-	//	Writes the text representation of the specified objects to the standard output
-	//	stream using the specified format information.
-	//
-	// Parameters:
-	//	format:
-	//	A composite format string (see Remarks).
-	//
-	//	arg0:
-	//	The first object to write using format.
-	//
-	//	arg1:
-	//	The second object to write using format.
-	//
-	//	arg2:
-	//	The third object to write using format.
-	//
-	// Exceptions:
-	//	T:System.IO.IOException:
-	//	An I/O error occurred.
-	//
-	//	T:System.ArgumentNullException:
-	//	format is null.
-	//
-	//	T:System.FormatException:
-	//	The format specification in format is invalid.
-	/// <summary>
-	/// 
-	/// </summary>
-	/// <param name="format"></param>
-	/// <param name="arg0"></param>
-	/// <param name="arg1"></param>
-	/// <param name="arg2"></param>
-	public static void Write (string format, object arg0, object arg1, object arg2)
-	{
-		throw new NotImplementedException ();
-	}
-
-	//
-	// Summary:
-	//	Writes the text representation of the specified System.Decimal value to the standard
-	//	output stream.
-	//
-	// Parameters:
-	//	value:
-	//	The value to write.
-	//
-	// Exceptions:
-	//	T:System.IO.IOException:
-	//	An I/O error occurred.
-	/// <summary>
-	/// 
-	/// </summary>
-	/// <param name="value"></param>
-	public static void Write (decimal value)
-	{
-		throw new NotImplementedException ();
-	}
-
-	//
-	// Summary:
-	//	Writes the text representation of the specified single-precision floating-point
-	//	value to the standard output stream.
-	//
-	// Parameters:
-	//	value:
-	//	The value to write.
-	//
-	// Exceptions:
-	//	T:System.IO.IOException:
-	//	An I/O error occurred.
-	/// <summary>
-	/// 
-	/// </summary>
-	/// <param name="value"></param>
-	public static void Write (float value)
-	{
-		throw new NotImplementedException ();
-	}
-
-	//
-	// Summary:
-	//	Writes the text representation of the specified double-precision floating-point
-	//	value to the standard output stream.
-	//
-	// Parameters:
-	//	value:
-	//	The value to write.
-	//
-	// Exceptions:
-	//	T:System.IO.IOException:
-	//	An I/O error occurred.
-	/// <summary>
-	/// 
-	/// </summary>
-	/// <param name="value"></param>
-	public static void Write (double value)
-	{
-		throw new NotImplementedException ();
-	}
-
-	//
-	// Summary:
-	//	Writes the current line terminator to the standard output stream.
-	//
-	// Exceptions:
-	//	T:System.IO.IOException:
-	//	An I/O error occurred.
-	/// <summary>
-	/// 
-	/// </summary>
-	public static void WriteLine ()
-	{
-		throw new NotImplementedException ();
-	}
-
-	//
-	// Summary:
-	//	Writes the text representation of the specified single-precision floating-point
-	//	value, followed by the current line terminator, to the standard output stream.
-	//
-	// Parameters:
-	//	value:
-	//	The value to write.
-	//
-	// Exceptions:
-	//	T:System.IO.IOException:
-	//	An I/O error occurred.
-	/// <summary>
-	/// 
-	/// </summary>
-	/// <param name="value"></param>
-	public static void WriteLine (float value)
-	{
-		throw new NotImplementedException ();
-	}
-
-	//
-	// Summary:
-	//	Writes the text representation of the specified 32-bit signed integer value,
-	//	followed by the current line terminator, to the standard output stream.
-	//
-	// Parameters:
-	//	value:
-	//	The value to write.
-	//
-	// Exceptions:
-	//	T:System.IO.IOException:
-	//	An I/O error occurred.
-	/// <summary>
-	/// 
-	/// </summary>
-	/// <param name="value"></param>
-	public static void WriteLine (int value)
-	{
-		throw new NotImplementedException ();
-	}
-
-	//
-	// Summary:
-	//	Writes the text representation of the specified 32-bit unsigned integer value,
-	//	followed by the current line terminator, to the standard output stream.
-	//
-	// Parameters:
-	//	value:
-	//	The value to write.
-	//
-	// Exceptions:
-	//	T:System.IO.IOException:
-	//	An I/O error occurred.
-	//[CLSCompliant (false)]
-	/// <summary>
-	/// 
-	/// </summary>
-	/// <param name="value"></param>
-	public static void WriteLine (uint value)
-	{
-		throw new NotImplementedException ();
-	}
-
-	//
-	// Summary:
-	//	Writes the text representation of the specified 64-bit signed integer value,
-	//	followed by the current line terminator, to the standard output stream.
-	//
-	// Parameters:
-	//	value:
-	//	The value to write.
-	//
-	// Exceptions:
-	//	T:System.IO.IOException:
-	//	An I/O error occurred.
-	/// <summary>
-	/// 
-	/// </summary>
-	/// <param name="value"></param>
-	public static void WriteLine (long value)
-	{
-		throw new NotImplementedException ();
-	}
-
-	//
-	// Summary:
-	//	Writes the text representation of the specified 64-bit unsigned integer value,
-	//	followed by the current line terminator, to the standard output stream.
-	//
-	// Parameters:
-	//	value:
-	//	The value to write.
-	//
-	// Exceptions:
-	//	T:System.IO.IOException:
-	//	An I/O error occurred.
-	//[CLSCompliant (false)]
-	/// <summary>
-	/// 
-	/// </summary>
-	/// <param name="value"></param>
-	public static void WriteLine (ulong value)
-	{
-		throw new NotImplementedException ();
-	}
-
-	//
-	// Summary:
-	//	Writes the text representation of the specified object, followed by the current
-	//	line terminator, to the standard output stream.
-	//
-	// Parameters:
-	//	value:
-	//	The value to write.
-	//
-	// Exceptions:
-	//	T:System.IO.IOException:
-	//	An I/O error occurred.
-	/// <summary>
-	/// 
-	/// </summary>
-	/// <param name="value"></param>
-	public static void WriteLine (object value)
-	{
-		throw new NotImplementedException ();
-	}
-
-	//
-	// Summary:
-	//	Writes the specified string value, followed by the current line terminator, to
-	//	the standard output stream.
-	//
-	// Parameters:
-	//	value:
-	//	The value to write.
-	//
-	// Exceptions:
-	//	T:System.IO.IOException:
-	//	An I/O error occurred.
-	/// <summary>
-	/// 
-	/// </summary>
-	/// <param name="value"></param>
-	public static void WriteLine (string value)
-	{
-		throw new NotImplementedException ();
-	}
-
-	//
-	// Summary:
-	//	Writes the text representation of the specified object, followed by the current
-	//	line terminator, to the standard output stream using the specified format information.
-	//
-	// Parameters:
-	//	format:
-	//	A composite format string (see Remarks).
-	//
-	//	arg0:
-	//	An object to write using format.
-	//
-	// Exceptions:
-	//	T:System.IO.IOException:
-	//	An I/O error occurred.
-	//
-	//	T:System.ArgumentNullException:
-	//	format is null.
-	//
-	//	T:System.FormatException:
-	//	The format specification in format is invalid.
-	/// <summary>
-	/// 
-	/// </summary>
-	/// <param name="format"></param>
-	/// <param name="arg0"></param>
-	public static void WriteLine (string format, object arg0)
-	{
-		throw new NotImplementedException ();
-	}
-
-	//
-	// Summary:
-	//	Writes the text representation of the specified objects, followed by the current
-	//	line terminator, to the standard output stream using the specified format information.
-	//
-	// Parameters:
-	//	format:
-	//	A composite format string (see Remarks).
-	//
-	//	arg0:
-	//	The first object to write using format.
-	//
-	//	arg1:
-	//	The second object to write using format.
-	//
-	//	arg2:
-	//	The third object to write using format.
-	//
-	// Exceptions:
-	//	T:System.IO.IOException:
-	//	An I/O error occurred.
-	//
-	//	T:System.ArgumentNullException:
-	//	format is null.
-	//
-	//	T:System.FormatException:
-	//	The format specification in format is invalid.
-	/// <summary>
-	/// 
-	/// </summary>
-	/// <param name="format"></param>
-	/// <param name="arg0"></param>
-	/// <param name="arg1"></param>
-	/// <param name="arg2"></param>
-	public static void WriteLine (string format, object arg0, object arg1, object arg2)
-	{
-		throw new NotImplementedException ();
-	}
-
-	//[CLSCompliant (false)]
-	/// <summary>
-	/// 
-	/// </summary>
-	/// <param name="format"></param>
-	/// <param name="arg0"></param>
-	/// <param name="arg1"></param>
-	/// <param name="arg2"></param>
-	/// <param name="arg3"></param>
-	public static void WriteLine (string format, object arg0, object arg1, object arg2, object arg3)
-	{
-		throw new NotImplementedException ();
-	}
-
-	//
-	// Summary:
-	//	Writes the text representation of the specified array of objects, followed by
-	//	the current line terminator, to the standard output stream using the specified
-	//	format information.
-	//
-	// Parameters:
-	//	format:
-	//	A composite format string (see Remarks).
-	//
-	//	arg:
-	//	An array of objects to write using format.
-	//
-	// Exceptions:
-	//	T:System.IO.IOException:
-	//	An I/O error occurred.
-	//
-	//	T:System.ArgumentNullException:
-	//	format or arg is null.
-	//
-	//	T:System.FormatException:
-	//	The format specification in format is invalid.
-	/// <summary>
-	/// 
-	/// </summary>
-	/// <param name="format"></param>
-	/// <param name="arg"></param>
-	public static void WriteLine (string format, params object [] arg)
-	{
-		throw new NotImplementedException ();
-	}
-
-	//
-	// Summary:
-	//	Writes the specified subarray of Unicode characters, followed by the current
-	//	line terminator, to the standard output stream.
-	//
-	// Parameters:
-	//	buffer:
-	//	An array of Unicode characters.
-	//
-	//	index:
-	//	The starting position in buffer.
-	//
-	//	count:
-	//	The number of characters to write.
-	//
-	// Exceptions:
-	//	T:System.ArgumentNullException:
-	//	buffer is null.
-	//
-	//	T:System.ArgumentOutOfRangeException:
-	//	index or count is less than zero.
-	//
-	//	T:System.ArgumentException:
-	//	index plus count specify a position that is not within buffer.
-	//
-	//	T:System.IO.IOException:
-	//	An I/O error occurred.
-	/// <summary>
-	/// 
-	/// </summary>
-	/// <param name="buffer"></param>
-	/// <param name="index"></param>
-	/// <param name="count"></param>
-	public static void WriteLine (char [] buffer, int index, int count)
-	{
-		throw new NotImplementedException ();
-	}
-
-	//
-	// Summary:
-	//	Writes the text representation of the specified System.Decimal value, followed
-	//	by the current line terminator, to the standard output stream.
-	//
-	// Parameters:
-	//	value:
-	//	The value to write.
-	//
-	// Exceptions:
-	//	T:System.IO.IOException:
-	//	An I/O error occurred.
-	/// <summary>
-	/// 
-	/// </summary>
-	/// <param name="value"></param>
-	public static void WriteLine (decimal value)
-	{
-		throw new NotImplementedException ();
-	}
-
-	//
-	// Summary:
-	//	Writes the specified array of Unicode characters, followed by the current line
-	//	terminator, to the standard output stream.
-	//
-	// Parameters:
-	//	buffer:
-	//	A Unicode character array.
-	//
-	// Exceptions:
-	//	T:System.IO.IOException:
-	//	An I/O error occurred.
-	/// <summary>
-	/// 
-	/// </summary>
-	/// <param name="buffer"></param>
-	public static void WriteLine (char [] buffer)
-	{
-		throw new NotImplementedException ();
-	}
-
-	//
-	// Summary:
-	//	Writes the specified Unicode character, followed by the current line terminator,
-	//	value to the standard output stream.
-	//
-	// Parameters:
-	//	value:
-	//	The value to write.
-	//
-	// Exceptions:
-	//	T:System.IO.IOException:
-	//	An I/O error occurred.
-	/// <summary>
-	/// 
-	/// </summary>
-	/// <param name="value"></param>
-	public static void WriteLine (char value)
-	{
-		throw new NotImplementedException ();
-	}
-
-	//
-	// Summary:
-	//	Writes the text representation of the specified Boolean value, followed by the
-	//	current line terminator, to the standard output stream.
-	//
-	// Parameters:
-	//	value:
-	//	The value to write.
-	//
-	// Exceptions:
-	//	T:System.IO.IOException:
-	//	An I/O error occurred.
-	/// <summary>
-	/// 
-	/// </summary>
-	/// <param name="value"></param>
-	public static void WriteLine (bool value)
-	{
-		throw new NotImplementedException ();
-	}
-
-	//
-	// Summary:
-	//	Writes the text representation of the specified objects, followed by the current
-	//	line terminator, to the standard output stream using the specified format information.
-	//
-	// Parameters:
-	//	format:
-	//	A composite format string (see Remarks).
-	//
-	//	arg0:
-	//	The first object to write using format.
-	//
-	//	arg1:
-	//	The second object to write using format.
-	//
-	// Exceptions:
-	//	T:System.IO.IOException:
-	//	An I/O error occurred.
-	//
-	//	T:System.ArgumentNullException:
-	//	format is null.
-	//
-	//	T:System.FormatException:
-	//	The format specification in format is invalid.
-	/// <summary>
-	/// 
-	/// </summary>
-	/// <param name="format"></param>
-	/// <param name="arg0"></param>
-	/// <param name="arg1"></param>
-	public static void WriteLine (string format, object arg0, object arg1)
-	{
-		throw new NotImplementedException ();
-	}
-
-	//
-	// Summary:
-	//	Writes the text representation of the specified double-precision floating-point
-	//	value, followed by the current line terminator, to the standard output stream.
-	//
-	// Parameters:
-	//	value:
-	//	The value to write.
-	//
-	// Exceptions:
-	//	T:System.IO.IOException:
-	//	An I/O error occurred.
-	/// <summary>
-	/// 
-	/// </summary>
-	/// <param name="value"></param>
-	public static void WriteLine (double value)
-	{
-		throw new NotImplementedException ();
-	}
-
-}
+    /// <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; } = WIDTH;
+
+    //
+    // Summary:
+    //	Gets a value that indicates whether output has been redirected from the standard
+    //	output stream.
+    //
+    // Returns:
+    //	true if output is redirected; otherwise, false.
+    /// <summary></summary>
+    public static bool IsOutputRedirected { get; }
+
+    //
+    // Summary:
+    //	Gets a value that indicates whether the error output stream has been redirected
+    //	from the standard error stream.
+    //
+    // Returns:
+    //	true if error output is redirected; otherwise, false.
+    /// <summary></summary>
+    public static bool IsErrorRedirected { get; }
+
+    //
+    // Summary:
+    //	Gets the standard input stream.
+    //
+    // Returns:
+    //	A System.IO.TextReader that represents the standard input stream.
+    /// <summary></summary>
+    public static TextReader In { get; }
+
+    //
+    // Summary:
+    //	Gets the standard output stream.
+    //
+    // Returns:
+    //	A System.IO.TextWriter that represents the standard output stream.
+    /// <summary></summary>
+    public static TextWriter Out { get; }
+
+    //
+    // Summary:
+    //	Gets the standard error output stream.
+    //
+    // Returns:
+    //	A System.IO.TextWriter that represents the standard error output stream.
+    /// <summary></summary>
+    public static TextWriter Error { get; }
+
+    //
+    // Summary:
+    //	Gets or sets the encoding the console uses to read input.
+    //
+    // Returns:
+    //	The encoding used to read console input.
+    //
+    // Exceptions:
+    //	T:System.ArgumentNullException:
+    //	The property value in a set operation is null.
+    //
+    //	T:System.IO.IOException:
+    //	An error occurred during the execution of this operation.
+    //
+    //	T:System.Security.SecurityException:
+    //	Your application does not have permission to perform this operation.
+    /// <summary></summary>
+    public static Encoding InputEncoding { get; set; }
+
+    //
+    // Summary:
+    //	Gets or sets the encoding the console uses to write output.
+    //
+    // Returns:
+    //	The encoding used to write console output.
+    //
+    // Exceptions:
+    //	T:System.ArgumentNullException:
+    //	The property value in a set operation is null.
+    //
+    //	T:System.IO.IOException:
+    //	An error occurred during the execution of this operation.
+    //
+    //	T:System.Security.SecurityException:
+    //	Your application does not have permission to perform this operation.
+    /// <summary></summary>
+    public static Encoding OutputEncoding { get; set; }
+
+    //
+    // Summary:
+    //	Gets or sets the background color of the console.
+    //
+    // Returns:
+    //	A value that specifies the background color of the console; that is, the color
+    //	that appears behind each character. The default is black.
+    //
+    // Exceptions:
+    //	T:System.ArgumentException:
+    //	The color specified in a set operation is not a valid member of System.ConsoleColor.
+    //
+    //	T:System.Security.SecurityException:
+    //	The user does not have permission to perform this action.
+    //
+    //	T:System.IO.IOException:
+    //	An I/O error occurred.
+    private static readonly ConsoleColor _defaultBackgroundColor = ConsoleColor.Black;
+
+    /// <summary></summary>
+    public static ConsoleColor BackgroundColor { get; set; } = _defaultBackgroundColor;
+
+    //
+    // Summary:
+    //	Gets or sets the foreground color of the console.
+    //
+    // Returns:
+    //	A System.ConsoleColor that specifies the foreground color of the console; that
+    //	is, the color of each character that is displayed. The default is gray.
+    //
+    // Exceptions:
+    //	T:System.ArgumentException:
+    //	The color specified in a set operation is not a valid member of System.ConsoleColor.
+    //
+    //	T:System.Security.SecurityException:
+    //	The user does not have permission to perform this action.
+    //
+    //	T:System.IO.IOException:
+    //	An I/O error occurred.
+    private static readonly ConsoleColor _defaultForegroundColor = ConsoleColor.Gray;
+
+    /// <summary></summary>
+    public static ConsoleColor ForegroundColor { get; set; } = _defaultForegroundColor;
+
+    //
+    // Summary:
+    //	Gets or sets the height of the buffer area.
+    //
+    // Returns:
+    //	The current height, in rows, of the buffer area.
+    //
+    // Exceptions:
+    //	T:System.ArgumentOutOfRangeException:
+    //	The value in a set operation is less than or equal to zero.-or- The value in
+    //	a set operation is greater than or equal to System.Int16.MaxValue.-or- The value
+    //	in a set operation is less than System.Console.WindowTop + System.Console.WindowHeight.
+    //
+    //	T:System.Security.SecurityException:
+    //	The user does not have permission to perform this action.
+    //
+    //	T:System.IO.IOException:
+    //	An I/O error occurred.
+    /// <summary></summary>
+    public static int BufferHeight { get; set; } = HEIGHT;
+
+    //
+    // Summary:
+    //	Gets or sets the width of the buffer area.
+    //
+    // Returns:
+    //	The current width, in columns, of the buffer area.
+    //
+    // Exceptions:
+    //	T:System.ArgumentOutOfRangeException:
+    //	The value in a set operation is less than or equal to zero.-or- The value in
+    //	a set operation is greater than or equal to System.Int16.MaxValue.-or- The value
+    //	in a set operation is less than System.Console.WindowLeft + System.Console.WindowWidth.
+    //
+    //	T:System.Security.SecurityException:
+    //	The user does not have permission to perform this action.
+    //
+    //	T:System.IO.IOException:
+    //	An I/O error occurred.
+    /// <summary></summary>
+    public static int BufferWidth { get; set; } = WIDTH;
+
+    //
+    // Summary:
+    //	Gets or sets the height of the console window area.
+    //
+    // Returns:
+    //	The height of the console window measured in rows.
+    //
+    // Exceptions:
+    //	T:System.ArgumentOutOfRangeException:
+    //	The value of the System.Console.WindowWidth property or the value of the System.Console.WindowHeight
+    //	property is less than or equal to 0.-or-The value of the System.Console.WindowHeight
+    //	property plus the value of the System.Console.WindowTop property is greater than
+    //	or equal to System.Int16.MaxValue.-or-The value of the System.Console.WindowWidth
+    //	property or the value of the System.Console.WindowHeight property is greater
+    //	than the largest possible window width or height for the current screen resolution
+    //	and console font.
+    //
+    //	T:System.IO.IOException:
+    //	Error reading or writing information.
+    /// <summary></summary>
+    public static int WindowHeight { get; set; } = HEIGHT;
+
+    //
+    // Summary:
+    //	Gets or sets a value indicating whether the combination of the System.ConsoleModifiers.Control
+    //	modifier key and System.ConsoleKey.C console key (Ctrl+C) is treated as ordinary
+    //	input or as an interruption that is handled by the operating system.
+    //
+    // Returns:
+    //	true if Ctrl+C is treated as ordinary input; otherwise, false.
+    //
+    // Exceptions:
+    //	T:System.IO.IOException:
+    //	Unable to get or set the input mode of the console input buffer.
+    /// <summary></summary>
+    public static bool TreatControlCAsInput { get; set; }
+
+    //
+    // Summary:
+    //	Gets the largest possible number of console window columns, based on the current
+    //	font and screen resolution.
+    //
+    // Returns:
+    //	The width of the largest possible console window measured in columns.
+    /// <summary></summary>
+    public static int LargestWindowWidth { get; }
+
+    //
+    // Summary:
+    //	Gets the largest possible number of console window rows, based on the current
+    //	font and screen resolution.
+    //
+    // Returns:
+    //	The height of the largest possible console window measured in rows.
+    /// <summary></summary>
+    public static int LargestWindowHeight { get; }
+
+    //
+    // Summary:
+    //	Gets or sets the leftmost position of the console window area relative to the
+    //	screen buffer.
+    //
+    // Returns:
+    //	The leftmost console window position measured in columns.
+    //
+    // Exceptions:
+    //	T:System.ArgumentOutOfRangeException:
+    //	In a set operation, the value to be assigned is less than zero.-or-As a result
+    //	of the assignment, System.Console.WindowLeft plus System.Console.WindowWidth
+    //	would exceed System.Console.BufferWidth.
+    //
+    //	T:System.IO.IOException:
+    //	Error reading or writing information.
+    /// <summary></summary>
+    public static int WindowLeft { get; set; }
+
+    //
+    // Summary:
+    //	Gets or sets the top position of the console window area relative to the screen
+    //	buffer.
+    //
+    // Returns:
+    //	The uppermost console window position measured in rows.
+    //
+    // Exceptions:
+    //	T:System.ArgumentOutOfRangeException:
+    //	In a set operation, the value to be assigned is less than zero.-or-As a result
+    //	of the assignment, System.Console.WindowTop plus System.Console.WindowHeight
+    //	would exceed System.Console.BufferHeight.
+    //
+    //	T:System.IO.IOException:
+    //	Error reading or writing information.
+    /// <summary></summary>
+    public static int WindowTop { get; set; }
+
+    //
+    // Summary:
+    //	Gets or sets the column position of the cursor within the buffer area.
+    //
+    // Returns:
+    //	The current position, in columns, of the cursor.
+    //
+    // Exceptions:
+    //	T:System.ArgumentOutOfRangeException:
+    //	The value in a set operation is less than zero.-or- The value in a set operation
+    //	is greater than or equal to System.Console.BufferWidth.
+    //
+    //	T:System.Security.SecurityException:
+    //	The user does not have permission to perform this action.
+    //
+    //	T:System.IO.IOException:
+    //	An I/O error occurred.
+    /// <summary></summary>
+    public static int CursorLeft { get; set; }
+
+    //
+    // Summary:
+    //	Gets or sets the row position of the cursor within the buffer area.
+    //
+    // Returns:
+    //	The current position, in rows, of the cursor.
+    //
+    // Exceptions:
+    //	T:System.ArgumentOutOfRangeException:
+    //	The value in a set operation is less than zero.-or- The value in a set operation
+    //	is greater than or equal to System.Console.BufferHeight.
+    //
+    //	T:System.Security.SecurityException:
+    //	The user does not have permission to perform this action.
+    //
+    //	T:System.IO.IOException:
+    //	An I/O error occurred.
+    /// <summary></summary>
+    public static int CursorTop { get; set; }
+
+    //
+    // Summary:
+    //	Gets or sets the height of the cursor within a character cell.
+    //
+    // Returns:
+    //	The size of the cursor expressed as a percentage of the height of a character
+    //	cell. The property value ranges from 1 to 100.
+    //
+    // Exceptions:
+    //	T:System.ArgumentOutOfRangeException:
+    //	The value specified in a set operation is less than 1 or greater than 100.
+    //
+    //	T:System.Security.SecurityException:
+    //	The user does not have permission to perform this action.
+    //
+    //	T:System.IO.IOException:
+    //	An I/O error occurred.
+    /// <summary></summary>
+    public static int CursorSize { get; set; }
+
+    //
+    // Summary:
+    //	Gets or sets a value indicating whether the cursor is visible.
+    //
+    // Returns:
+    //	true if the cursor is visible; otherwise, false.
+    //
+    // Exceptions:
+    //	T:System.Security.SecurityException:
+    //	The user does not have permission to perform this action.
+    //
+    //	T:System.IO.IOException:
+    //	An I/O error occurred.
+    /// <summary></summary>
+    public static bool CursorVisible { get; set; }
+
+    //
+    // Summary:
+    //	Gets or sets the title to display in the console title bar.
+    //
+    // Returns:
+    //	The string to be displayed in the title bar of the console. The maximum length
+    //	of the title string is 24500 characters.
+    //
+    // Exceptions:
+    //	T:System.InvalidOperationException:
+    //	In a get operation, the retrieved title is longer than 24500 characters.
+    //
+    //	T:System.ArgumentOutOfRangeException:
+    //	In a set operation, the specified title is longer than 24500 characters.
+    //
+    //	T:System.ArgumentNullException:
+    //	In a set operation, the specified title is null.
+    //
+    //	T:System.IO.IOException:
+    //	An I/O error occurred.
+    /// <summary></summary>
+    public static string Title { get; set; }
+
+    //
+    // Summary:
+    //	Gets a value indicating whether a key press is available in the input stream.
+    //
+    // Returns:
+    //	true if a key press is available; otherwise, false.
+    //
+    // Exceptions:
+    //	T:System.IO.IOException:
+    //	An I/O error occurred.
+    //
+    //	T:System.InvalidOperationException:
+    //	Standard input is redirected to a file instead of the keyboard.
+    /// <summary></summary>
+    public static bool KeyAvailable { get; }
+
+    //
+    // Summary:
+    //	Gets a value that indicates whether input has been redirected from the standard
+    //	input stream.
+    //
+    // Returns:
+    //	true if input is redirected; otherwise, false.
+    /// <summary></summary>
+    public static bool IsInputRedirected { get; }
+
+    //
+    // Summary:
+    //	Plays the sound of a beep through the console speaker.
+    //
+    // Exceptions:
+    //	T:System.Security.HostProtectionException:
+    //	This method was executed on a server, such as SQL Server, that does not permit
+    //	access to a user interface.
+    /// <summary></summary>
+    public static void Beep () { throw new NotImplementedException (); }
+
+    //
+    // Summary:
+    //	Plays the sound of a beep of a specified frequency and duration through the console
+    //	speaker.
+    //
+    // Parameters:
+    //	frequency:
+    //	The frequency of the beep, ranging from 37 to 32767 hertz.
+    //
+    //	duration:
+    //	The duration of the beep measured in milliseconds.
+    //
+    // Exceptions:
+    //	T:System.ArgumentOutOfRangeException:
+    //	frequency is less than 37 or more than 32767 hertz.-or- duration is less than
+    //	or equal to zero.
+    //
+    //	T:System.Security.HostProtectionException:
+    //	This method was executed on a server, such as SQL Server, that does not permit
+    //	access to the console.
+    /// <summary></summary>
+    public static void Beep (int frequency, int duration) { throw new NotImplementedException (); }
+
+    //
+    // Summary:
+    //	Clears the console buffer and corresponding console window of display information.
+    //
+    // Exceptions:
+    //	T:System.IO.IOException:
+    //	An I/O error occurred.
+    private static char [,] _buffer = new char [WindowWidth, WindowHeight];
+
+    /// <summary></summary>
+    public static void Clear ()
+    {
+        _buffer = new char [BufferWidth, BufferHeight];
+        SetCursorPosition (0, 0);
+    }
+
+    //
+    // Summary:
+    //	Copies a specified source area of the screen buffer to a specified destination
+    //	area.
+    //
+    // Parameters:
+    //	sourceLeft:
+    //	The leftmost column of the source area.
+    //
+    //	sourceTop:
+    //	The topmost row of the source area.
+    //
+    //	sourceWidth:
+    //	The number of columns in the source area.
+    //
+    //	sourceHeight:
+    //	The number of rows in the source area.
+    //
+    //	targetLeft:
+    //	The leftmost column of the destination area.
+    //
+    //	targetTop:
+    //	The topmost row of the destination area.
+    //
+    // Exceptions:
+    //	T:System.ArgumentOutOfRangeException:
+    //	One or more of the parameters is less than zero.-or- sourceLeft or targetLeft
+    //	is greater than or equal to System.Console.BufferWidth.-or- sourceTop or targetTop
+    //	is greater than or equal to System.Console.BufferHeight.-or- sourceTop + sourceHeight
+    //	is greater than or equal to System.Console.BufferHeight.-or- sourceLeft + sourceWidth
+    //	is greater than or equal to System.Console.BufferWidth.
+    //
+    //	T:System.Security.SecurityException:
+    //	The user does not have permission to perform this action.
+    //
+    //	T:System.IO.IOException:
+    //	An I/O error occurred.
+    /// <summary></summary>
+    public static void MoveBufferArea (
+        int sourceLeft,
+        int sourceTop,
+        int sourceWidth,
+        int sourceHeight,
+        int targetLeft,
+        int targetTop
+    )
+    {
+        throw new NotImplementedException ();
+    }
+
+    //
+    // Summary:
+    //	Copies a specified source area of the screen buffer to a specified destination
+    //	area.
+    //
+    // Parameters:
+    //	sourceLeft:
+    //	The leftmost column of the source area.
+    //
+    //	sourceTop:
+    //	The topmost row of the source area.
+    //
+    //	sourceWidth:
+    //	The number of columns in the source area.
+    //
+    //	sourceHeight:
+    //	The number of rows in the source area.
+    //
+    //	targetLeft:
+    //	The leftmost column of the destination area.
+    //
+    //	targetTop:
+    //	The topmost row of the destination area.
+    //
+    //	sourceChar:
+    //	The character used to fill the source area.
+    //
+    //	sourceForeColor:
+    //	The foreground color used to fill the source area.
+    //
+    //	sourceBackColor:
+    //	The background color used to fill the source area.
+    //
+    // Exceptions:
+    //	T:System.ArgumentOutOfRangeException:
+    //	One or more of the parameters is less than zero.-or- sourceLeft or targetLeft
+    //	is greater than or equal to System.Console.BufferWidth.-or- sourceTop or targetTop
+    //	is greater than or equal to System.Console.BufferHeight.-or- sourceTop + sourceHeight
+    //	is greater than or equal to System.Console.BufferHeight.-or- sourceLeft + sourceWidth
+    //	is greater than or equal to System.Console.BufferWidth.
+    //
+    //	T:System.ArgumentException:
+    //	One or both of the color parameters is not a member of the System.ConsoleColor
+    //	enumeration.
+    //
+    //	T:System.Security.SecurityException:
+    //	The user does not have permission to perform this action.
+    //
+    //	T:System.IO.IOException:
+    //	An I/O error occurred.
+    //[SecuritySafeCritical]
+    /// <summary></summary>
+    public static void MoveBufferArea (
+        int sourceLeft,
+        int sourceTop,
+        int sourceWidth,
+        int sourceHeight,
+        int targetLeft,
+        int targetTop,
+        char sourceChar,
+        ConsoleColor sourceForeColor,
+        ConsoleColor sourceBackColor
+    )
+    {
+        throw new NotImplementedException ();
+    }
+
+    //
+    // Summary:
+    //	Acquires the standard error stream.
+    //
+    // Returns:
+    //	The standard error stream.
+    /// <summary></summary>
+    public static Stream OpenStandardError () { throw new NotImplementedException (); }
+
+    //
+    // Summary:
+    //	Acquires the standard error stream, which is set to a specified buffer size.
+    //
+    // Parameters:
+    //	bufferSize:
+    //	The internal stream buffer size.
+    //
+    // Returns:
+    //	The standard error stream.
+    //
+    // Exceptions:
+    //	T:System.ArgumentOutOfRangeException:
+    //	bufferSize is less than or equal to zero.
+    /// <summary></summary>
+    public static Stream OpenStandardError (int bufferSize) { throw new NotImplementedException (); }
+
+    //
+    // Summary:
+    //	Acquires the standard input stream, which is set to a specified buffer size.
+    //
+    // Parameters:
+    //	bufferSize:
+    //	The internal stream buffer size.
+    //
+    // Returns:
+    //	The standard input stream.
+    //
+    // Exceptions:
+    //	T:System.ArgumentOutOfRangeException:
+    //	bufferSize is less than or equal to zero.
+    /// <summary></summary>
+    public static Stream OpenStandardInput (int bufferSize) { throw new NotImplementedException (); }
+
+    //
+    // Summary:
+    //	Acquires the standard input stream.
+    //
+    // Returns:
+    //	The standard input stream.
+    /// <summary></summary>
+    public static Stream OpenStandardInput () { throw new NotImplementedException (); }
+
+    //
+    // Summary:
+    //	Acquires the standard output stream, which is set to a specified buffer size.
+    //
+    // Parameters:
+    //	bufferSize:
+    //	The internal stream buffer size.
+    //
+    // Returns:
+    //	The standard output stream.
+    //
+    // Exceptions:
+    //	T:System.ArgumentOutOfRangeException:
+    //	bufferSize is less than or equal to zero.
+    /// <summary></summary>
+    public static Stream OpenStandardOutput (int bufferSize) { throw new NotImplementedException (); }
+
+    //
+    // Summary:
+    //	Acquires the standard output stream.
+    //
+    // Returns:
+    //	The standard output stream.
+    /// <summary></summary>
+    public static Stream OpenStandardOutput () { throw new NotImplementedException (); }
+
+    //
+    // Summary:
+    //	Reads the next character from the standard input stream.
+    //
+    // Returns:
+    //	The next character from the input stream, or negative one (-1) if there are currently
+    //	no more characters to be read.
+    //
+    // Exceptions:
+    //	T:System.IO.IOException:
+    //	An I/O error occurred.
+    /// <summary></summary>
+    public static int Read () { throw new NotImplementedException (); }
+
+    //
+    // Summary:
+    //	Obtains the next character or function key pressed by the user. The pressed key
+    //	is optionally displayed in the console window.
+    //
+    // Parameters:
+    //	intercept:
+    //	Determines whether to display the pressed key in the console window. true to
+    //	not display the pressed key; otherwise, false.
+    //
+    // Returns:
+    //	An object that describes the System.ConsoleKey constant and Unicode character,
+    //	if any, that correspond to the pressed console key. The System.ConsoleKeyInfo
+    //	object also describes, in a bitwise combination of System.ConsoleModifiers values,
+    //	whether one or more Shift, Alt, or Ctrl modifier keys was pressed simultaneously
+    //	with the console key.
+    //
+    // Exceptions:
+    //	T:System.InvalidOperationException:
+    //	The System.Console.In property is redirected from some stream other than the
+    //	console.
+    //[SecuritySafeCritical]
+
+    /// <summary>A stack of keypresses to return when ReadKey is called.</summary>
+    public static Stack<ConsoleKeyInfo> MockKeyPresses = new ();
+
+    /// <summary>Helper to push a <see cref="KeyCode"/> onto <see cref="MockKeyPresses"/>.</summary>
+    /// <param name="key"></param>
+    public static void PushMockKeyPress (KeyCode key)
+    {
+        MockKeyPresses.Push (
+                             new ConsoleKeyInfo (
+                                                 (char)(key
+                                                        & ~KeyCode.CtrlMask
+                                                        & ~KeyCode.ShiftMask
+                                                        & ~KeyCode.AltMask),
+                                                 ConsoleKeyMapping.GetConsoleKeyInfoFromKeyCode (key).Key,
+                                                 key.HasFlag (KeyCode.ShiftMask),
+                                                 key.HasFlag (KeyCode.AltMask),
+                                                 key.HasFlag (KeyCode.CtrlMask)
+                                                )
+                            );
+    }
+
+    //
+    // Summary:
+    //	Obtains the next character or function key pressed by the user. The pressed key
+    //	is displayed in the console window.
+    //
+    // Returns:
+    //	An object that describes the System.ConsoleKey constant and Unicode character,
+    //	if any, that correspond to the pressed console key. The System.ConsoleKeyInfo
+    //	object also describes, in a bitwise combination of System.ConsoleModifiers values,
+    //	whether one or more Shift, Alt, or Ctrl modifier keys was pressed simultaneously
+    //	with the console key.
+    //
+    // Exceptions:
+    //	T:System.InvalidOperationException:
+    //	The System.Console.In property is redirected from some stream other than the
+    //	console.
+    /// <summary></summary>
+    public static ConsoleKeyInfo ReadKey () { throw new NotImplementedException (); }
+
+    //
+    // Summary:
+    //	Reads the next line of characters from the standard input stream.
+    //
+    // Returns:
+    //	The next line of characters from the input stream, or null if no more lines are
+    //	available.
+    //
+    // Exceptions:
+    //	T:System.IO.IOException:
+    //	An I/O error occurred.
+    //
+    //	T:System.OutOfMemoryException:
+    //	There is insufficient memory to allocate a buffer for the returned string.
+    //
+    //	T:System.ArgumentOutOfRangeException:
+    //	The number of characters in the next line of characters is greater than System.Int32.MaxValue.
+    /// <summary></summary>
+    public static string ReadLine () { throw new NotImplementedException (); }
+
+    //
+    // Summary:
+    //	Sets the foreground and background console colors to their defaults.
+    //
+    // Exceptions:
+    //	T:System.Security.SecurityException:
+    //	The user does not have permission to perform this action.
+    //
+    //	T:System.IO.IOException:
+    //	An I/O error occurred.
+    //[SecuritySafeCritical]
+    /// <summary></summary>
+    public static void ResetColor ()
+    {
+        BackgroundColor = _defaultBackgroundColor;
+        ForegroundColor = _defaultForegroundColor;
+    }
+
+    //
+    // Summary:
+    //	Sets the height and width of the screen buffer area to the specified values.
+    //
+    // Parameters:
+    //	width:
+    //	The width of the buffer area measured in columns.
+    //
+    //	height:
+    //	The height of the buffer area measured in rows.
+    //
+    // Exceptions:
+    //	T:System.ArgumentOutOfRangeException:
+    //	height or width is less than or equal to zero.-or- height or width is greater
+    //	than or equal to System.Int16.MaxValue.-or- width is less than System.Console.WindowLeft
+    //	+ System.Console.WindowWidth.-or- height is less than System.Console.WindowTop
+    //	+ System.Console.WindowHeight.
+    //
+    //	T:System.Security.SecurityException:
+    //	The user does not have permission to perform this action.
+    //
+    //	T:System.IO.IOException:
+    //	An I/O error occurred.
+    //[SecuritySafeCritical]
+    /// <summary></summary>
+    public static void SetBufferSize (int width, int height)
+    {
+        BufferWidth = width;
+        BufferHeight = height;
+        _buffer = new char [BufferWidth, BufferHeight];
+    }
+
+    //
+    // Summary:
+    //	Sets the position of the cursor.
+    //
+    // Parameters:
+    //	left:
+    //	The column position of the cursor. Columns are numbered from left to right starting
+    //	at 0.
+    //
+    //	top:
+    //	The row position of the cursor. Rows are numbered from top to bottom starting
+    //	at 0.
+    //
+    // Exceptions:
+    //	T:System.ArgumentOutOfRangeException:
+    //	left or top is less than zero.-or- left is greater than or equal to System.Console.BufferWidth.-or-
+    //	top is greater than or equal to System.Console.BufferHeight.
+    //
+    //	T:System.Security.SecurityException:
+    //	The user does not have permission to perform this action.
+    //
+    //	T:System.IO.IOException:
+    //	An I/O error occurred.
+    //[SecuritySafeCritical]
+    /// <summary></summary>
+    public static void SetCursorPosition (int left, int top)
+    {
+        CursorLeft = left;
+        CursorTop = top;
+        WindowLeft = Math.Max (Math.Min (left, BufferWidth - WindowWidth), 0);
+        WindowTop = Math.Max (Math.Min (top, BufferHeight - WindowHeight), 0);
+    }
+
+    //
+    // Summary:
+    //	Sets the System.Console.Error property to the specified System.IO.TextWriter
+    //	object.
+    //
+    // Parameters:
+    //	newError:
+    //	A stream that is the new standard error output.
+    //
+    // Exceptions:
+    //	T:System.ArgumentNullException:
+    //	newError is null.
+    //
+    //	T:System.Security.SecurityException:
+    //	The caller does not have the required permission.
+    //[SecuritySafeCritical]
+    /// <summary></summary>
+    public static void SetError (TextWriter newError) { throw new NotImplementedException (); }
+
+    //
+    // Summary:
+    //	Sets the System.Console.In property to the specified System.IO.TextReader object.
+    //
+    // Parameters:
+    //	newIn:
+    //	A stream that is the new standard input.
+    //
+    // Exceptions:
+    //	T:System.ArgumentNullException:
+    //	newIn is null.
+    //
+    //	T:System.Security.SecurityException:
+    //	The caller does not have the required permission.
+    //[SecuritySafeCritical]
+    /// <summary></summary>
+    public static void SetIn (TextReader newIn) { throw new NotImplementedException (); }
+
+    //
+    // Summary:
+    //	Sets the System.Console.Out property to the specified System.IO.TextWriter object.
+    //
+    // Parameters:
+    //	newOut:
+    //	A stream that is the new standard output.
+    //
+    // Exceptions:
+    //	T:System.ArgumentNullException:
+    //	newOut is null.
+    //
+    //	T:System.Security.SecurityException:
+    //	The caller does not have the required permission.
+    //[SecuritySafeCritical]
+    /// <summary></summary>
+    /// <param name="newOut"></param>
+    public static void SetOut (TextWriter newOut) { throw new NotImplementedException (); }
+
+    //
+    // Summary:
+    //	Sets the position of the console window relative to the screen buffer.
+    //
+    // Parameters:
+    //	left:
+    //	The column position of the upper left corner of the console window.
+    //
+    //	top:
+    //	The row position of the upper left corner of the console window.
+    //
+    // Exceptions:
+    //	T:System.ArgumentOutOfRangeException:
+    //	left or top is less than zero.-or- left + System.Console.WindowWidth is greater
+    //	than System.Console.BufferWidth.-or- top + System.Console.WindowHeight is greater
+    //	than System.Console.BufferHeight.
+    //
+    //	T:System.Security.SecurityException:
+    //	The user does not have permission to perform this action.
+    //
+    //	T:System.IO.IOException:
+    //	An I/O error occurred.
+    //[SecuritySafeCritical]
+    /// <summary></summary>
+    /// <param name="left"></param>
+    /// <param name="top"></param>
+    public static void SetWindowPosition (int left, int top)
+    {
+        WindowLeft = left;
+        WindowTop = top;
+    }
+
+    //
+    // Summary:
+    //	Sets the height and width of the console window to the specified values.
+    //
+    // Parameters:
+    //	width:
+    //	The width of the console window measured in columns.
+    //
+    //	height:
+    //	The height of the console window measured in rows.
+    //
+    // Exceptions:
+    //	T:System.ArgumentOutOfRangeException:
+    //	width or height is less than or equal to zero.-or- width plus System.Console.WindowLeft
+    //	or height plus System.Console.WindowTop is greater than or equal to System.Int16.MaxValue.
+    //	-or- width or height is greater than the largest possible window width or height
+    //	for the current screen resolution and console font.
+    //
+    //	T:System.Security.SecurityException:
+    //	The user does not have permission to perform this action.
+    //
+    //	T:System.IO.IOException:
+    //	An I/O error occurred.
+    //[SecuritySafeCritical]
+    /// <summary></summary>
+    /// <param name="width"></param>
+    /// <param name="height"></param>
+    public static void SetWindowSize (int width, int height)
+    {
+        WindowWidth = width;
+        WindowHeight = height;
+    }
+
+    //
+    // Summary:
+    //	Writes the specified string value to the standard output stream.
+    //
+    // Parameters:
+    //	value:
+    //	The value to write.
+    //
+    // Exceptions:
+    //	T:System.IO.IOException:
+    //	An I/O error occurred.
+    /// <summary></summary>
+    /// <param name="value"></param>
+    public static void Write (string value) { throw new NotImplementedException (); }
+
+    //
+    // Summary:
+    //	Writes the text representation of the specified object to the standard output
+    //	stream.
+    //
+    // Parameters:
+    //	value:
+    //	The value to write, or null.
+    //
+    // Exceptions:
+    //	T:System.IO.IOException:
+    //	An I/O error occurred.
+    /// <summary></summary>
+    /// <param name="value"></param>
+    public static void Write (object value)
+    {
+        if (value is Rune rune)
+        {
+            Write ((char)rune.Value);
+        }
+        else
+        {
+            throw new NotImplementedException ();
+        }
+    }
+
+    //
+    // Summary:
+    //	Writes the text representation of the specified 64-bit unsigned integer value
+    //	to the standard output stream.
+    //
+    // Parameters:
+    //	value:
+    //	The value to write.
+    //
+    // Exceptions:
+    //	T:System.IO.IOException:
+    //	An I/O error occurred.
+    //[CLSCompliant (false)]
+    /// <summary></summary>
+    /// <param name="value"></param>
+    public static void Write (ulong value) { throw new NotImplementedException (); }
+
+    //
+    // Summary:
+    //	Writes the text representation of the specified 64-bit signed integer value to
+    //	the standard output stream.
+    //
+    // Parameters:
+    //	value:
+    //	The value to write.
+    //
+    // Exceptions:
+    //	T:System.IO.IOException:
+    //	An I/O error occurred.
+    /// <summary></summary>
+    /// <param name="value"></param>
+    public static void Write (long value) { throw new NotImplementedException (); }
+
+    //
+    // Summary:
+    //	Writes the text representation of the specified objects to the standard output
+    //	stream using the specified format information.
+    //
+    // Parameters:
+    //	format:
+    //	A composite format string (see Remarks).
+    //
+    //	arg0:
+    //	The first object to write using format.
+    //
+    //	arg1:
+    //	The second object to write using format.
+    //
+    // Exceptions:
+    //	T:System.IO.IOException:
+    //	An I/O error occurred.
+    //
+    //	T:System.ArgumentNullException:
+    //	format is null.
+    //
+    //	T:System.FormatException:
+    //	The format specification in format is invalid.
+    /// <summary></summary>
+    /// <param name="format"></param>
+    /// <param name="arg0"></param>
+    /// <param name="arg1"></param>
+    public static void Write (string format, object arg0, object arg1) { throw new NotImplementedException (); }
+
+    //
+    // Summary:
+    //	Writes the text representation of the specified 32-bit signed integer value to
+    //	the standard output stream.
+    //
+    // Parameters:
+    //	value:
+    //	The value to write.
+    //
+    // Exceptions:
+    //	T:System.IO.IOException:
+    //	An I/O error occurred.
+    /// <summary></summary>
+    /// <param name="value"></param>
+    public static void Write (int value) { throw new NotImplementedException (); }
+
+    //
+    // Summary:
+    //	Writes the text representation of the specified object to the standard output
+    //	stream using the specified format information.
+    //
+    // Parameters:
+    //	format:
+    //	A composite format string (see Remarks).
+    //
+    //	arg0:
+    //	An object to write using format.
+    //
+    // Exceptions:
+    //	T:System.IO.IOException:
+    //	An I/O error occurred.
+    //
+    //	T:System.ArgumentNullException:
+    //	format is null.
+    //
+    //	T:System.FormatException:
+    //	The format specification in format is invalid.
+    /// <summary></summary>
+    /// <param name="format"></param>
+    /// <param name="arg0"></param>
+    public static void Write (string format, object arg0) { throw new NotImplementedException (); }
+
+    //
+    // Summary:
+    //	Writes the text representation of the specified 32-bit unsigned integer value
+    //	to the standard output stream.
+    //
+    // Parameters:
+    //	value:
+    //	The value to write.
+    //
+    // Exceptions:
+    //	T:System.IO.IOException:
+    //	An I/O error occurred.
+    //[CLSCompliant (false)]
+    /// <summary></summary>
+    /// <param name="value"></param>
+    public static void Write (uint value) { throw new NotImplementedException (); }
+
+    //[CLSCompliant (false)]
+    /// <summary></summary>
+    /// <param name="format"></param>
+    /// <param name="arg0"></param>
+    /// <param name="arg1"></param>
+    /// <param name="arg2"></param>
+    /// <param name="arg3"></param>
+    public static void Write (string format, object arg0, object arg1, object arg2, object arg3) { throw new NotImplementedException (); }
+
+    //
+    // Summary:
+    //	Writes the text representation of the specified array of objects to the standard
+    //	output stream using the specified format information.
+    //
+    // Parameters:
+    //	format:
+    //	A composite format string (see Remarks).
+    //
+    //	arg:
+    //	An array of objects to write using format.
+    //
+    // Exceptions:
+    //	T:System.IO.IOException:
+    //	An I/O error occurred.
+    //
+    //	T:System.ArgumentNullException:
+    //	format or arg is null.
+    //
+    //	T:System.FormatException:
+    //	The format specification in format is invalid.
+    /// <summary></summary>
+    /// <param name="format"></param>
+    /// <param name="arg"></param>
+    public static void Write (string format, params object [] arg) { throw new NotImplementedException (); }
+
+    //
+    // Summary:
+    //	Writes the text representation of the specified Boolean value to the standard
+    //	output stream.
+    //
+    // Parameters:
+    //	value:
+    //	The value to write.
+    //
+    // Exceptions:
+    //	T:System.IO.IOException:
+    //	An I/O error occurred.
+    /// <summary></summary>
+    /// <param name="value"></param>
+    public static void Write (bool value) { throw new NotImplementedException (); }
+
+    //
+    // Summary:
+    //	Writes the specified Unicode character value to the standard output stream.
+    //
+    // Parameters:
+    //	value:
+    //	The value to write.
+    //
+    // Exceptions:
+    //	T:System.IO.IOException:
+    //	An I/O error occurred.
+    /// <summary></summary>
+    /// <param name="value"></param>
+    public static void Write (char value) { _buffer [CursorLeft, CursorTop] = value; }
+
+    //
+    // Summary:
+    //	Writes the specified array of Unicode characters to the standard output stream.
+    //
+    // Parameters:
+    //	buffer:
+    //	A Unicode character array.
+    //
+    // Exceptions:
+    //	T:System.IO.IOException:
+    //	An I/O error occurred.
+    /// <summary></summary>
+    /// <param name="buffer"></param>
+    public static void Write (char [] buffer)
+    {
+        _buffer [CursorLeft, CursorTop] = (char)0;
+
+        foreach (char ch in buffer)
+        {
+            _buffer [CursorLeft, CursorTop] += ch;
+        }
+    }
+
+    //
+    // Summary:
+    //	Writes the specified subarray of Unicode characters to the standard output stream.
+    //
+    // Parameters:
+    //	buffer:
+    //	An array of Unicode characters.
+    //
+    //	index:
+    //	The starting position in buffer.
+    //
+    //	count:
+    //	The number of characters to write.
+    //
+    // Exceptions:
+    //	T:System.ArgumentNullException:
+    //	buffer is null.
+    //
+    //	T:System.ArgumentOutOfRangeException:
+    //	index or count is less than zero.
+    //
+    //	T:System.ArgumentException:
+    //	index plus count specify a position that is not within buffer.
+    //
+    //	T:System.IO.IOException:
+    //	An I/O error occurred.
+    /// <summary></summary>
+    /// <param name="buffer"></param>
+    /// <param name="index"></param>
+    /// <param name="count"></param>
+    public static void Write (char [] buffer, int index, int count) { throw new NotImplementedException (); }
+
+    //
+    // Summary:
+    //	Writes the text representation of the specified objects to the standard output
+    //	stream using the specified format information.
+    //
+    // Parameters:
+    //	format:
+    //	A composite format string (see Remarks).
+    //
+    //	arg0:
+    //	The first object to write using format.
+    //
+    //	arg1:
+    //	The second object to write using format.
+    //
+    //	arg2:
+    //	The third object to write using format.
+    //
+    // Exceptions:
+    //	T:System.IO.IOException:
+    //	An I/O error occurred.
+    //
+    //	T:System.ArgumentNullException:
+    //	format is null.
+    //
+    //	T:System.FormatException:
+    //	The format specification in format is invalid.
+    /// <summary></summary>
+    /// <param name="format"></param>
+    /// <param name="arg0"></param>
+    /// <param name="arg1"></param>
+    /// <param name="arg2"></param>
+    public static void Write (string format, object arg0, object arg1, object arg2) { throw new NotImplementedException (); }
+
+    //
+    // Summary:
+    //	Writes the text representation of the specified System.Decimal value to the standard
+    //	output stream.
+    //
+    // Parameters:
+    //	value:
+    //	The value to write.
+    //
+    // Exceptions:
+    //	T:System.IO.IOException:
+    //	An I/O error occurred.
+    /// <summary></summary>
+    /// <param name="value"></param>
+    public static void Write (decimal value) { throw new NotImplementedException (); }
+
+    //
+    // Summary:
+    //	Writes the text representation of the specified single-precision floating-point
+    //	value to the standard output stream.
+    //
+    // Parameters:
+    //	value:
+    //	The value to write.
+    //
+    // Exceptions:
+    //	T:System.IO.IOException:
+    //	An I/O error occurred.
+    /// <summary></summary>
+    /// <param name="value"></param>
+    public static void Write (float value) { throw new NotImplementedException (); }
+
+    //
+    // Summary:
+    //	Writes the text representation of the specified double-precision floating-point
+    //	value to the standard output stream.
+    //
+    // Parameters:
+    //	value:
+    //	The value to write.
+    //
+    // Exceptions:
+    //	T:System.IO.IOException:
+    //	An I/O error occurred.
+    /// <summary></summary>
+    /// <param name="value"></param>
+    public static void Write (double value) { throw new NotImplementedException (); }
+
+    //
+    // Summary:
+    //	Writes the current line terminator to the standard output stream.
+    //
+    // Exceptions:
+    //	T:System.IO.IOException:
+    //	An I/O error occurred.
+    /// <summary></summary>
+    public static void WriteLine () { throw new NotImplementedException (); }
+
+    //
+    // Summary:
+    //	Writes the text representation of the specified single-precision floating-point
+    //	value, followed by the current line terminator, to the standard output stream.
+    //
+    // Parameters:
+    //	value:
+    //	The value to write.
+    //
+    // Exceptions:
+    //	T:System.IO.IOException:
+    //	An I/O error occurred.
+    /// <summary></summary>
+    /// <param name="value"></param>
+    public static void WriteLine (float value) { throw new NotImplementedException (); }
+
+    //
+    // Summary:
+    //	Writes the text representation of the specified 32-bit signed integer value,
+    //	followed by the current line terminator, to the standard output stream.
+    //
+    // Parameters:
+    //	value:
+    //	The value to write.
+    //
+    // Exceptions:
+    //	T:System.IO.IOException:
+    //	An I/O error occurred.
+    /// <summary></summary>
+    /// <param name="value"></param>
+    public static void WriteLine (int value) { throw new NotImplementedException (); }
+
+    //
+    // Summary:
+    //	Writes the text representation of the specified 32-bit unsigned integer value,
+    //	followed by the current line terminator, to the standard output stream.
+    //
+    // Parameters:
+    //	value:
+    //	The value to write.
+    //
+    // Exceptions:
+    //	T:System.IO.IOException:
+    //	An I/O error occurred.
+    //[CLSCompliant (false)]
+    /// <summary></summary>
+    /// <param name="value"></param>
+    public static void WriteLine (uint value) { throw new NotImplementedException (); }
+
+    //
+    // Summary:
+    //	Writes the text representation of the specified 64-bit signed integer value,
+    //	followed by the current line terminator, to the standard output stream.
+    //
+    // Parameters:
+    //	value:
+    //	The value to write.
+    //
+    // Exceptions:
+    //	T:System.IO.IOException:
+    //	An I/O error occurred.
+    /// <summary></summary>
+    /// <param name="value"></param>
+    public static void WriteLine (long value) { throw new NotImplementedException (); }
+
+    //
+    // Summary:
+    //	Writes the text representation of the specified 64-bit unsigned integer value,
+    //	followed by the current line terminator, to the standard output stream.
+    //
+    // Parameters:
+    //	value:
+    //	The value to write.
+    //
+    // Exceptions:
+    //	T:System.IO.IOException:
+    //	An I/O error occurred.
+    //[CLSCompliant (false)]
+    /// <summary></summary>
+    /// <param name="value"></param>
+    public static void WriteLine (ulong value) { throw new NotImplementedException (); }
+
+    //
+    // Summary:
+    //	Writes the text representation of the specified object, followed by the current
+    //	line terminator, to the standard output stream.
+    //
+    // Parameters:
+    //	value:
+    //	The value to write.
+    //
+    // Exceptions:
+    //	T:System.IO.IOException:
+    //	An I/O error occurred.
+    /// <summary></summary>
+    /// <param name="value"></param>
+    public static void WriteLine (object value) { throw new NotImplementedException (); }
+
+    //
+    // Summary:
+    //	Writes the specified string value, followed by the current line terminator, to
+    //	the standard output stream.
+    //
+    // Parameters:
+    //	value:
+    //	The value to write.
+    //
+    // Exceptions:
+    //	T:System.IO.IOException:
+    //	An I/O error occurred.
+    /// <summary></summary>
+    /// <param name="value"></param>
+    public static void WriteLine (string value) { throw new NotImplementedException (); }
+
+    //
+    // Summary:
+    //	Writes the text representation of the specified object, followed by the current
+    //	line terminator, to the standard output stream using the specified format information.
+    //
+    // Parameters:
+    //	format:
+    //	A composite format string (see Remarks).
+    //
+    //	arg0:
+    //	An object to write using format.
+    //
+    // Exceptions:
+    //	T:System.IO.IOException:
+    //	An I/O error occurred.
+    //
+    //	T:System.ArgumentNullException:
+    //	format is null.
+    //
+    //	T:System.FormatException:
+    //	The format specification in format is invalid.
+    /// <summary></summary>
+    /// <param name="format"></param>
+    /// <param name="arg0"></param>
+    public static void WriteLine (string format, object arg0) { throw new NotImplementedException (); }
+
+    //
+    // Summary:
+    //	Writes the text representation of the specified objects, followed by the current
+    //	line terminator, to the standard output stream using the specified format information.
+    //
+    // Parameters:
+    //	format:
+    //	A composite format string (see Remarks).
+    //
+    //	arg0:
+    //	The first object to write using format.
+    //
+    //	arg1:
+    //	The second object to write using format.
+    //
+    //	arg2:
+    //	The third object to write using format.
+    //
+    // Exceptions:
+    //	T:System.IO.IOException:
+    //	An I/O error occurred.
+    //
+    //	T:System.ArgumentNullException:
+    //	format is null.
+    //
+    //	T:System.FormatException:
+    //	The format specification in format is invalid.
+    /// <summary></summary>
+    /// <param name="format"></param>
+    /// <param name="arg0"></param>
+    /// <param name="arg1"></param>
+    /// <param name="arg2"></param>
+    public static void WriteLine (string format, object arg0, object arg1, object arg2) { throw new NotImplementedException (); }
+
+    //[CLSCompliant (false)]
+    /// <summary></summary>
+    /// <param name="format"></param>
+    /// <param name="arg0"></param>
+    /// <param name="arg1"></param>
+    /// <param name="arg2"></param>
+    /// <param name="arg3"></param>
+    public static void WriteLine (string format, object arg0, object arg1, object arg2, object arg3) { throw new NotImplementedException (); }
+
+    //
+    // Summary:
+    //	Writes the text representation of the specified array of objects, followed by
+    //	the current line terminator, to the standard output stream using the specified
+    //	format information.
+    //
+    // Parameters:
+    //	format:
+    //	A composite format string (see Remarks).
+    //
+    //	arg:
+    //	An array of objects to write using format.
+    //
+    // Exceptions:
+    //	T:System.IO.IOException:
+    //	An I/O error occurred.
+    //
+    //	T:System.ArgumentNullException:
+    //	format or arg is null.
+    //
+    //	T:System.FormatException:
+    //	The format specification in format is invalid.
+    /// <summary></summary>
+    /// <param name="format"></param>
+    /// <param name="arg"></param>
+    public static void WriteLine (string format, params object [] arg) { throw new NotImplementedException (); }
+
+    //
+    // Summary:
+    //	Writes the specified subarray of Unicode characters, followed by the current
+    //	line terminator, to the standard output stream.
+    //
+    // Parameters:
+    //	buffer:
+    //	An array of Unicode characters.
+    //
+    //	index:
+    //	The starting position in buffer.
+    //
+    //	count:
+    //	The number of characters to write.
+    //
+    // Exceptions:
+    //	T:System.ArgumentNullException:
+    //	buffer is null.
+    //
+    //	T:System.ArgumentOutOfRangeException:
+    //	index or count is less than zero.
+    //
+    //	T:System.ArgumentException:
+    //	index plus count specify a position that is not within buffer.
+    //
+    //	T:System.IO.IOException:
+    //	An I/O error occurred.
+    /// <summary></summary>
+    /// <param name="buffer"></param>
+    /// <param name="index"></param>
+    /// <param name="count"></param>
+    public static void WriteLine (char [] buffer, int index, int count) { throw new NotImplementedException (); }
+
+    //
+    // Summary:
+    //	Writes the text representation of the specified System.Decimal value, followed
+    //	by the current line terminator, to the standard output stream.
+    //
+    // Parameters:
+    //	value:
+    //	The value to write.
+    //
+    // Exceptions:
+    //	T:System.IO.IOException:
+    //	An I/O error occurred.
+    /// <summary></summary>
+    /// <param name="value"></param>
+    public static void WriteLine (decimal value) { throw new NotImplementedException (); }
+
+    //
+    // Summary:
+    //	Writes the specified array of Unicode characters, followed by the current line
+    //	terminator, to the standard output stream.
+    //
+    // Parameters:
+    //	buffer:
+    //	A Unicode character array.
+    //
+    // Exceptions:
+    //	T:System.IO.IOException:
+    //	An I/O error occurred.
+    /// <summary></summary>
+    /// <param name="buffer"></param>
+    public static void WriteLine (char [] buffer) { throw new NotImplementedException (); }
+
+    //
+    // Summary:
+    //	Writes the specified Unicode character, followed by the current line terminator,
+    //	value to the standard output stream.
+    //
+    // Parameters:
+    //	value:
+    //	The value to write.
+    //
+    // Exceptions:
+    //	T:System.IO.IOException:
+    //	An I/O error occurred.
+    /// <summary></summary>
+    /// <param name="value"></param>
+    public static void WriteLine (char value) { throw new NotImplementedException (); }
+
+    //
+    // Summary:
+    //	Writes the text representation of the specified Boolean value, followed by the
+    //	current line terminator, to the standard output stream.
+    //
+    // Parameters:
+    //	value:
+    //	The value to write.
+    //
+    // Exceptions:
+    //	T:System.IO.IOException:
+    //	An I/O error occurred.
+    /// <summary></summary>
+    /// <param name="value"></param>
+    public static void WriteLine (bool value) { throw new NotImplementedException (); }
+
+    //
+    // Summary:
+    //	Writes the text representation of the specified objects, followed by the current
+    //	line terminator, to the standard output stream using the specified format information.
+    //
+    // Parameters:
+    //	format:
+    //	A composite format string (see Remarks).
+    //
+    //	arg0:
+    //	The first object to write using format.
+    //
+    //	arg1:
+    //	The second object to write using format.
+    //
+    // Exceptions:
+    //	T:System.IO.IOException:
+    //	An I/O error occurred.
+    //
+    //	T:System.ArgumentNullException:
+    //	format is null.
+    //
+    //	T:System.FormatException:
+    //	The format specification in format is invalid.
+    /// <summary></summary>
+    /// <param name="format"></param>
+    /// <param name="arg0"></param>
+    /// <param name="arg1"></param>
+    public static void WriteLine (string format, object arg0, object arg1) { throw new NotImplementedException (); }
+
+    //
+    // Summary:
+    //	Writes the text representation of the specified double-precision floating-point
+    //	value, followed by the current line terminator, to the standard output stream.
+    //
+    // Parameters:
+    //	value:
+    //	The value to write.
+    //
+    // Exceptions:
+    //	T:System.IO.IOException:
+    //	An I/O error occurred.
+    /// <summary></summary>
+    /// <param name="value"></param>
+    public static void WriteLine (double value) { throw new NotImplementedException (); }
+}

+ 527 - 433
Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs

@@ -1,448 +1,542 @@
 //
 // FakeDriver.cs: A fake ConsoleDriver for unit tests. 
 //
-using System;
+
 using System.Diagnostics;
 using System.Runtime.InteropServices;
-using System.Text;
 using Terminal.Gui.ConsoleDrivers;
 
 // Alias Console to MockConsole so we don't accidentally use Console
-using Console = Terminal.Gui.FakeConsole;
 
 namespace Terminal.Gui;
 
-/// <summary>
-/// Implements a mock ConsoleDriver for unit testing
-/// </summary>
-public class FakeDriver : ConsoleDriver {
+/// <summary>Implements a mock ConsoleDriver for unit testing</summary>
+public class FakeDriver : ConsoleDriver
+{
 #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
 
-	public class Behaviors {
-		public bool UseFakeClipboard { get; internal set; }
-
-		public bool FakeClipboardAlwaysThrowsNotSupportedException { get; internal set; }
-
-		public bool FakeClipboardIsSupportedAlwaysFalse { get; internal set; }
-
-		public Behaviors (bool useFakeClipboard = false, bool fakeClipboardAlwaysThrowsNotSupportedException = false, bool fakeClipboardIsSupportedAlwaysTrue = false)
-		{
-			UseFakeClipboard = useFakeClipboard;
-			FakeClipboardAlwaysThrowsNotSupportedException = fakeClipboardAlwaysThrowsNotSupportedException;
-			FakeClipboardIsSupportedAlwaysFalse = fakeClipboardIsSupportedAlwaysTrue;
-
-			// double check usage is correct
-			Debug.Assert (useFakeClipboard == false && fakeClipboardAlwaysThrowsNotSupportedException == false);
-			Debug.Assert (useFakeClipboard == false && fakeClipboardIsSupportedAlwaysTrue == false);
-		}
-	}
-
-	public static FakeDriver.Behaviors FakeBehaviors = new Behaviors ();
-
-	public override bool SupportsTrueColor => false;
-
-	public FakeDriver ()
-	{
-		Cols = FakeConsole.WindowWidth = FakeConsole.BufferWidth = FakeConsole.WIDTH;
-		Rows = FakeConsole.WindowHeight = FakeConsole.BufferHeight = FakeConsole.HEIGHT;
-		if (FakeBehaviors.UseFakeClipboard) {
-			Clipboard = new FakeClipboard (FakeBehaviors.FakeClipboardAlwaysThrowsNotSupportedException, FakeBehaviors.FakeClipboardIsSupportedAlwaysFalse);
-		} else {
-			if (RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) {
-				Clipboard = new WindowsClipboard ();
-			} else if (RuntimeInformation.IsOSPlatform (OSPlatform.OSX)) {
-				Clipboard = new MacOSXClipboard ();
-			} else {
-				if (CursesDriver.Is_WSL_Platform ()) {
-					Clipboard = new WSLClipboard ();
-				} else {
-					Clipboard = new CursesClipboard ();
-				}
-			}
-		}
-	}
-
-	internal override void End ()
-	{
-		FakeConsole.ResetColor ();
-		FakeConsole.Clear ();
-	}
-
-	FakeMainLoop _mainLoopDriver = null;
-
-	internal override MainLoop Init ()
-	{
-		FakeConsole.MockKeyPresses.Clear ();
-
-		Cols = FakeConsole.WindowWidth = FakeConsole.BufferWidth = FakeConsole.WIDTH;
-		Rows = FakeConsole.WindowHeight = FakeConsole.BufferHeight = FakeConsole.HEIGHT;
-		FakeConsole.Clear ();
-		ResizeScreen ();
-		CurrentAttribute = new Attribute (Color.White, Color.Black);
-		ClearContents ();
-
-		_mainLoopDriver = new FakeMainLoop (this);
-		_mainLoopDriver.MockKeyPressed = MockKeyPressedHandler;
-		return new MainLoop (_mainLoopDriver);
-	}
-
-
-	public override void UpdateScreen ()
-	{
-		var savedRow = FakeConsole.CursorTop;
-		var savedCol = FakeConsole.CursorLeft;
-		var savedCursorVisible = FakeConsole.CursorVisible;
-
-		var top = 0;
-		var left = 0;
-		var rows = Rows;
-		var cols = Cols;
-		System.Text.StringBuilder output = new System.Text.StringBuilder ();
-		Attribute redrawAttr = new Attribute ();
-		var lastCol = -1;
-
-		for (var row = top; row < rows; row++) {
-			if (!_dirtyLines [row]) {
-				continue;
-			}
-
-			FakeConsole.CursorTop = row;
-			FakeConsole.CursorLeft = 0;
-
-			_dirtyLines [row] = false;
-			output.Clear ();
-			for (var col = left; col < cols; col++) {
-				lastCol = -1;
-				var outputWidth = 0;
-				for (; col < cols; col++) {
-					if (!Contents [row, col].IsDirty) {
-						if (output.Length > 0) {
-							WriteToConsole (output, ref lastCol, row, ref outputWidth);
-						} else if (lastCol == -1) {
-							lastCol = col;
-						}
-						if (lastCol + 1 < cols)
-							lastCol++;
-						continue;
-					}
-
-					if (lastCol == -1) {
-						lastCol = col;
-					}
-
-					Attribute attr = Contents [row, col].Attribute.Value;
-					// Performance: Only send the escape sequence if the attribute has changed.
-					if (attr != redrawAttr) {
-						redrawAttr = attr;
-						FakeConsole.ForegroundColor = (ConsoleColor)attr.Foreground.ColorName;
-						FakeConsole.BackgroundColor = (ConsoleColor)attr.Background.ColorName;
-					}
-					outputWidth++;
-					var rune = (Rune)Contents [row, col].Rune;
-					output.Append (rune.ToString ());
-					if (rune.IsSurrogatePair () && rune.GetColumns () < 2) {
-						WriteToConsole (output, ref lastCol, row, ref outputWidth);
-						FakeConsole.CursorLeft--;
-					}
-					Contents [row, col].IsDirty = false;
-				}
-			}
-			if (output.Length > 0) {
-				FakeConsole.CursorTop = row;
-				FakeConsole.CursorLeft = lastCol;
-
-				foreach (var c in output.ToString ()) {
-					FakeConsole.Write (c);
-				}
-			}
-		}
-		FakeConsole.CursorTop = 0;
-		FakeConsole.CursorLeft = 0;
-
-		//SetCursorVisibility (savedVisibitity);
-
-		void WriteToConsole (StringBuilder output, ref int lastCol, int row, ref int outputWidth)
-		{
-			FakeConsole.CursorTop = row;
-			FakeConsole.CursorLeft = lastCol;
-			foreach (var c in output.ToString ()) {
-				FakeConsole.Write (c);
-			}
-
-			output.Clear ();
-			lastCol += outputWidth;
-			outputWidth = 0;
-		}
-
-		FakeConsole.CursorTop = savedRow;
-		FakeConsole.CursorLeft = savedCol;
-		FakeConsole.CursorVisible = savedCursorVisible;
-	}
-
-	public override void Refresh ()
-	{
-		UpdateScreen ();
-		UpdateCursor ();
-	}
-
-	#region Color Handling
-	///// <remarks>
-	///// In the FakeDriver, colors are encoded as an int; same as NetDriver
-	///// However, the foreground color is stored in the most significant 16 bits, 
-	///// and the background color is stored in the least significant 16 bits.
-	///// </remarks>
-	//public override Attribute MakeColor (Color foreground, Color background)
-	//{
-	//	// Encode the colors into the int value.
-	//	return new Attribute (
-	//		platformColor: 0,//((((int)foreground.ColorName) & 0xffff) << 16) | (((int)background.ColorName) & 0xffff),
-	//		foreground: foreground,
-	//		background: background
-	//	);
-	//}
-	#endregion
-
-
-	KeyCode MapKey (ConsoleKeyInfo keyInfo)
-	{
-		switch (keyInfo.Key) {
-		case ConsoleKey.Escape:
-			return ConsoleKeyMapping.MapToKeyCodeModifiers (keyInfo.Modifiers, KeyCode.Esc);
-		case ConsoleKey.Tab:
-			return ConsoleKeyMapping.MapToKeyCodeModifiers (keyInfo.Modifiers, KeyCode.Tab);
-		case ConsoleKey.Clear:
-			return ConsoleKeyMapping.MapToKeyCodeModifiers (keyInfo.Modifiers, KeyCode.Clear);
-		case ConsoleKey.Home:
-			return ConsoleKeyMapping.MapToKeyCodeModifiers (keyInfo.Modifiers, KeyCode.Home);
-		case ConsoleKey.End:
-			return ConsoleKeyMapping.MapToKeyCodeModifiers (keyInfo.Modifiers, KeyCode.End);
-		case ConsoleKey.LeftArrow:
-			return ConsoleKeyMapping.MapToKeyCodeModifiers (keyInfo.Modifiers, KeyCode.CursorLeft);
-		case ConsoleKey.RightArrow:
-			return ConsoleKeyMapping.MapToKeyCodeModifiers (keyInfo.Modifiers, KeyCode.CursorRight);
-		case ConsoleKey.UpArrow:
-			return ConsoleKeyMapping.MapToKeyCodeModifiers (keyInfo.Modifiers, KeyCode.CursorUp);
-		case ConsoleKey.DownArrow:
-			return ConsoleKeyMapping.MapToKeyCodeModifiers (keyInfo.Modifiers, KeyCode.CursorDown);
-		case ConsoleKey.PageUp:
-			return ConsoleKeyMapping.MapToKeyCodeModifiers (keyInfo.Modifiers, KeyCode.PageUp);
-		case ConsoleKey.PageDown:
-			return ConsoleKeyMapping.MapToKeyCodeModifiers (keyInfo.Modifiers, KeyCode.PageDown);
-		case ConsoleKey.Enter:
-			return ConsoleKeyMapping.MapToKeyCodeModifiers (keyInfo.Modifiers, KeyCode.Enter);
-		case ConsoleKey.Spacebar:
-			return ConsoleKeyMapping.MapToKeyCodeModifiers (keyInfo.Modifiers, keyInfo.KeyChar == 0 ? KeyCode.Space : (KeyCode)keyInfo.KeyChar);
-		case ConsoleKey.Backspace:
-			return ConsoleKeyMapping.MapToKeyCodeModifiers (keyInfo.Modifiers, KeyCode.Backspace);
-		case ConsoleKey.Delete:
-			return ConsoleKeyMapping.MapToKeyCodeModifiers (keyInfo.Modifiers, KeyCode.Delete);
-		case ConsoleKey.Insert:
-			return ConsoleKeyMapping.MapToKeyCodeModifiers (keyInfo.Modifiers, KeyCode.Insert);
-		case ConsoleKey.PrintScreen:
-			return ConsoleKeyMapping.MapToKeyCodeModifiers (keyInfo.Modifiers, KeyCode.PrintScreen);
-
-		case ConsoleKey.Oem1:
-		case ConsoleKey.Oem2:
-		case ConsoleKey.Oem3:
-		case ConsoleKey.Oem4:
-		case ConsoleKey.Oem5:
-		case ConsoleKey.Oem6:
-		case ConsoleKey.Oem7:
-		case ConsoleKey.Oem8:
-		case ConsoleKey.Oem102:
-		case ConsoleKey.OemPeriod:
-		case ConsoleKey.OemComma:
-		case ConsoleKey.OemPlus:
-		case ConsoleKey.OemMinus:
-			if (keyInfo.KeyChar == 0) {
-				return KeyCode.Null;
-			}
-
-			return ConsoleKeyMapping.MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)((uint)keyInfo.KeyChar));
-		}
-
-		var key = keyInfo.Key;
-		if (key >= ConsoleKey.A && key <= ConsoleKey.Z) {
-			var delta = key - ConsoleKey.A;
-			if (keyInfo.KeyChar != (uint)key) {
-				return ConsoleKeyMapping.MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)keyInfo.KeyChar);
-			}
-			if (keyInfo.Modifiers.HasFlag (ConsoleModifiers.Control)
-			|| keyInfo.Modifiers.HasFlag (ConsoleModifiers.Alt)
-			|| keyInfo.Modifiers.HasFlag (ConsoleModifiers.Shift)) {
-				return ConsoleKeyMapping.MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)((uint)KeyCode.A + delta));
-			}
-			var alphaBase = ((keyInfo.Modifiers != ConsoleModifiers.Shift)) ? 'A' : 'a';
-			return (KeyCode)((uint)alphaBase + delta);
-		}
-
-		return ConsoleKeyMapping.MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)((uint)keyInfo.KeyChar));
-	}
-
-	private CursorVisibility _savedCursorVisibility;
-
-	void MockKeyPressedHandler (ConsoleKeyInfo consoleKeyInfo)
-	{
-		if (consoleKeyInfo.Key == ConsoleKey.Packet) {
-			consoleKeyInfo = ConsoleKeyMapping.DecodeVKPacketToKConsoleKeyInfo (consoleKeyInfo);
-		}
-
-		var map = MapKey (consoleKeyInfo);
-		OnKeyDown (new Key (map));
-		OnKeyUp (new Key (map));
-		//OnKeyPressed (new KeyEventArgs (map));
-	}
-
-	/// <inheritdoc/>
-	public override bool GetCursorVisibility (out CursorVisibility visibility)
-	{
-		visibility = FakeConsole.CursorVisible
-			? CursorVisibility.Default
-			: CursorVisibility.Invisible;
-
-		return FakeConsole.CursorVisible;
-	}
-
-	/// <inheritdoc/>
-	public override bool SetCursorVisibility (CursorVisibility visibility)
-	{
-		_savedCursorVisibility = visibility;
-		return FakeConsole.CursorVisible = visibility == CursorVisibility.Default;
-	}
-
-	/// <inheritdoc/>
-	public override bool EnsureCursorVisibility ()
-	{
-		if (!(Col >= 0 && Row >= 0 && Col < Cols && Row < Rows)) {
-			GetCursorVisibility (out CursorVisibility cursorVisibility);
-			_savedCursorVisibility = cursorVisibility;
-			SetCursorVisibility (CursorVisibility.Invisible);
-			return false;
-		}
-
-		SetCursorVisibility (_savedCursorVisibility);
-		return FakeConsole.CursorVisible;
-	}
-
-	public override void SendKeys (char keyChar, ConsoleKey key, bool shift, bool alt, bool control)
-	{
-		MockKeyPressedHandler (new ConsoleKeyInfo (keyChar, key, shift, alt, control));
-	}
-
-	public void SetBufferSize (int width, int height)
-	{
-		FakeConsole.SetBufferSize (width, height);
-		Cols = width;
-		Rows = height;
-		SetWindowSize (width, height);
-		ProcessResize ();
-	}
-
-	public void SetWindowSize (int width, int height)
-	{
-		FakeConsole.SetWindowSize (width, height);
-		if (width != Cols || height != Rows) {
-			SetBufferSize (width, height);
-			Cols = width;
-			Rows = height;
-		}
-		ProcessResize ();
-	}
-
-	public void SetWindowPosition (int left, int top)
-	{
-		if (Left > 0 || Top > 0) {
-			Left = 0;
-			Top = 0;
-		}
-		FakeConsole.SetWindowPosition (Left, Top);
-	}
-
-	void ProcessResize ()
-	{
-		ResizeScreen ();
-		ClearContents ();
-		OnSizeChanged (new SizeChangedEventArgs (new Size (Cols, Rows)));
-	}
-
-	public virtual void ResizeScreen ()
-	{
-		if (FakeConsole.WindowHeight > 0) {
-			// Can raise an exception while is still resizing.
-			try {
-				FakeConsole.CursorTop = 0;
-				FakeConsole.CursorLeft = 0;
-				FakeConsole.WindowTop = 0;
-				FakeConsole.WindowLeft = 0;
-			} catch (System.IO.IOException) {
-				return;
-			} catch (ArgumentOutOfRangeException) {
-				return;
-			}
-		}
-
-		Clip = new Rect (0, 0, Cols, Rows);
-	}
-
-	public override void UpdateCursor ()
-	{
-		if (!EnsureCursorVisibility ()) {
-			return;
-		}
-
-		// Prevents the exception of size changing during resizing.
-		try {
-			// BUGBUG: Why is this using BufferWidth/Height and now Cols/Rows?
-			if (Col >= 0 && Col < FakeConsole.BufferWidth && Row >= 0 && Row < FakeConsole.BufferHeight) {
-				FakeConsole.SetCursorPosition (Col, Row);
-			}
-		} catch (System.IO.IOException) { } catch (ArgumentOutOfRangeException) { }
-	}
-
-	#region Not Implemented
-	public override void Suspend ()
-	{
-		return;
-		//throw new NotImplementedException ();
-	}
-	#endregion
-
-	public class FakeClipboard : ClipboardBase {
-		public Exception FakeException = null;
-
-		string _contents = string.Empty;
-
-		bool _isSupportedAlwaysFalse = false;
-
-		public override bool IsSupported => !_isSupportedAlwaysFalse;
-
-		public FakeClipboard (bool fakeClipboardThrowsNotSupportedException = false, bool isSupportedAlwaysFalse = false)
-		{
-			_isSupportedAlwaysFalse = isSupportedAlwaysFalse;
-			if (fakeClipboardThrowsNotSupportedException) {
-				FakeException = new NotSupportedException ("Fake clipboard exception");
-			}
-		}
-
-		protected override string GetClipboardDataImpl ()
-		{
-			if (FakeException != null) {
-				throw FakeException;
-			}
-			return _contents;
-		}
-
-		protected override void SetClipboardDataImpl (string text)
-		{
-			if (text == null) {
-				throw new ArgumentNullException (nameof (text));
-			}
-			if (FakeException != null) {
-				throw FakeException;
-			}
-			_contents = text;
-		}
-	}
+    public class Behaviors
+    {
+        public Behaviors (
+            bool useFakeClipboard = false,
+            bool fakeClipboardAlwaysThrowsNotSupportedException = false,
+            bool fakeClipboardIsSupportedAlwaysTrue = false
+        )
+        {
+            UseFakeClipboard = useFakeClipboard;
+            FakeClipboardAlwaysThrowsNotSupportedException = fakeClipboardAlwaysThrowsNotSupportedException;
+            FakeClipboardIsSupportedAlwaysFalse = fakeClipboardIsSupportedAlwaysTrue;
+
+            // double check usage is correct
+            Debug.Assert (useFakeClipboard == false && fakeClipboardAlwaysThrowsNotSupportedException == false);
+            Debug.Assert (useFakeClipboard == false && fakeClipboardIsSupportedAlwaysTrue == false);
+        }
+
+        public bool FakeClipboardAlwaysThrowsNotSupportedException { get; internal set; }
+        public bool FakeClipboardIsSupportedAlwaysFalse { get; internal set; }
+        public bool UseFakeClipboard { get; internal set; }
+    }
+
+    public static Behaviors FakeBehaviors = new ();
+    public override bool SupportsTrueColor => false;
+
+    public FakeDriver ()
+    {
+        Cols = FakeConsole.WindowWidth = FakeConsole.BufferWidth = FakeConsole.WIDTH;
+        Rows = FakeConsole.WindowHeight = FakeConsole.BufferHeight = FakeConsole.HEIGHT;
+
+        if (FakeBehaviors.UseFakeClipboard)
+        {
+            Clipboard = new FakeClipboard (
+                                           FakeBehaviors.FakeClipboardAlwaysThrowsNotSupportedException,
+                                           FakeBehaviors.FakeClipboardIsSupportedAlwaysFalse
+                                          );
+        }
+        else
+        {
+            if (RuntimeInformation.IsOSPlatform (OSPlatform.Windows))
+            {
+                Clipboard = new WindowsClipboard ();
+            }
+            else if (RuntimeInformation.IsOSPlatform (OSPlatform.OSX))
+            {
+                Clipboard = new MacOSXClipboard ();
+            }
+            else
+            {
+                if (CursesDriver.Is_WSL_Platform ())
+                {
+                    Clipboard = new WSLClipboard ();
+                }
+                else
+                {
+                    Clipboard = new CursesClipboard ();
+                }
+            }
+        }
+    }
+
+    internal override void End ()
+    {
+        FakeConsole.ResetColor ();
+        FakeConsole.Clear ();
+    }
+
+    private FakeMainLoop _mainLoopDriver;
+
+    internal override MainLoop Init ()
+    {
+        FakeConsole.MockKeyPresses.Clear ();
+
+        Cols = FakeConsole.WindowWidth = FakeConsole.BufferWidth = FakeConsole.WIDTH;
+        Rows = FakeConsole.WindowHeight = FakeConsole.BufferHeight = FakeConsole.HEIGHT;
+        FakeConsole.Clear ();
+        ResizeScreen ();
+        CurrentAttribute = new Attribute (Color.White, Color.Black);
+        ClearContents ();
+
+        _mainLoopDriver = new FakeMainLoop (this);
+        _mainLoopDriver.MockKeyPressed = MockKeyPressedHandler;
+
+        return new MainLoop (_mainLoopDriver);
+    }
+
+    public override void UpdateScreen ()
+    {
+        int savedRow = FakeConsole.CursorTop;
+        int savedCol = FakeConsole.CursorLeft;
+        bool savedCursorVisible = FakeConsole.CursorVisible;
+
+        var top = 0;
+        var left = 0;
+        int rows = Rows;
+        int cols = Cols;
+        var output = new StringBuilder ();
+        var redrawAttr = new Attribute ();
+        int lastCol = -1;
+
+        for (int row = top; row < rows; row++)
+        {
+            if (!_dirtyLines [row])
+            {
+                continue;
+            }
+
+            FakeConsole.CursorTop = row;
+            FakeConsole.CursorLeft = 0;
+
+            _dirtyLines [row] = false;
+            output.Clear ();
+
+            for (int col = left; col < cols; col++)
+            {
+                lastCol = -1;
+                var outputWidth = 0;
+
+                for (; col < cols; col++)
+                {
+                    if (!Contents [row, col].IsDirty)
+                    {
+                        if (output.Length > 0)
+                        {
+                            WriteToConsole (output, ref lastCol, row, ref outputWidth);
+                        }
+                        else if (lastCol == -1)
+                        {
+                            lastCol = col;
+                        }
+
+                        if (lastCol + 1 < cols)
+                        {
+                            lastCol++;
+                        }
+
+                        continue;
+                    }
+
+                    if (lastCol == -1)
+                    {
+                        lastCol = col;
+                    }
+
+                    Attribute attr = Contents [row, col].Attribute.Value;
+
+                    // Performance: Only send the escape sequence if the attribute has changed.
+                    if (attr != redrawAttr)
+                    {
+                        redrawAttr = attr;
+                        FakeConsole.ForegroundColor = (ConsoleColor)attr.Foreground.GetClosestNamedColor ();
+                        FakeConsole.BackgroundColor = (ConsoleColor)attr.Background.GetClosestNamedColor ();
+                    }
+
+                    outputWidth++;
+                    Rune rune = Contents [row, col].Rune;
+                    output.Append (rune.ToString ());
+
+                    if (rune.IsSurrogatePair () && rune.GetColumns () < 2)
+                    {
+                        WriteToConsole (output, ref lastCol, row, ref outputWidth);
+                        FakeConsole.CursorLeft--;
+                    }
+
+                    Contents [row, col].IsDirty = false;
+                }
+            }
+
+            if (output.Length > 0)
+            {
+                FakeConsole.CursorTop = row;
+                FakeConsole.CursorLeft = lastCol;
+
+                foreach (char c in output.ToString ())
+                {
+                    FakeConsole.Write (c);
+                }
+            }
+        }
+
+        FakeConsole.CursorTop = 0;
+        FakeConsole.CursorLeft = 0;
+
+        //SetCursorVisibility (savedVisibitity);
+
+        void WriteToConsole (StringBuilder output, ref int lastCol, int row, ref int outputWidth)
+        {
+            FakeConsole.CursorTop = row;
+            FakeConsole.CursorLeft = lastCol;
+
+            foreach (char c in output.ToString ())
+            {
+                FakeConsole.Write (c);
+            }
+
+            output.Clear ();
+            lastCol += outputWidth;
+            outputWidth = 0;
+        }
+
+        FakeConsole.CursorTop = savedRow;
+        FakeConsole.CursorLeft = savedCol;
+        FakeConsole.CursorVisible = savedCursorVisible;
+    }
+
+    public override void Refresh ()
+    {
+        UpdateScreen ();
+        UpdateCursor ();
+    }
+
+    #region Color Handling
+
+    ///// <remarks>
+    ///// In the FakeDriver, colors are encoded as an int; same as NetDriver
+    ///// However, the foreground color is stored in the most significant 16 bits, 
+    ///// and the background color is stored in the least significant 16 bits.
+    ///// </remarks>
+    //public override Attribute MakeColor (Color foreground, Color background)
+    //{
+    //	// Encode the colors into the int value.
+    //	return new Attribute (
+    //		platformColor: 0,//((((int)foreground.ColorName) & 0xffff) << 16) | (((int)background.ColorName) & 0xffff),
+    //		foreground: foreground,
+    //		background: background
+    //	);
+    //}
+
+    #endregion
+
+    private KeyCode MapKey (ConsoleKeyInfo keyInfo)
+    {
+        switch (keyInfo.Key)
+        {
+            case ConsoleKey.Escape:
+                return ConsoleKeyMapping.MapToKeyCodeModifiers (keyInfo.Modifiers, KeyCode.Esc);
+            case ConsoleKey.Tab:
+                return ConsoleKeyMapping.MapToKeyCodeModifiers (keyInfo.Modifiers, KeyCode.Tab);
+            case ConsoleKey.Clear:
+                return ConsoleKeyMapping.MapToKeyCodeModifiers (keyInfo.Modifiers, KeyCode.Clear);
+            case ConsoleKey.Home:
+                return ConsoleKeyMapping.MapToKeyCodeModifiers (keyInfo.Modifiers, KeyCode.Home);
+            case ConsoleKey.End:
+                return ConsoleKeyMapping.MapToKeyCodeModifiers (keyInfo.Modifiers, KeyCode.End);
+            case ConsoleKey.LeftArrow:
+                return ConsoleKeyMapping.MapToKeyCodeModifiers (keyInfo.Modifiers, KeyCode.CursorLeft);
+            case ConsoleKey.RightArrow:
+                return ConsoleKeyMapping.MapToKeyCodeModifiers (keyInfo.Modifiers, KeyCode.CursorRight);
+            case ConsoleKey.UpArrow:
+                return ConsoleKeyMapping.MapToKeyCodeModifiers (keyInfo.Modifiers, KeyCode.CursorUp);
+            case ConsoleKey.DownArrow:
+                return ConsoleKeyMapping.MapToKeyCodeModifiers (keyInfo.Modifiers, KeyCode.CursorDown);
+            case ConsoleKey.PageUp:
+                return ConsoleKeyMapping.MapToKeyCodeModifiers (keyInfo.Modifiers, KeyCode.PageUp);
+            case ConsoleKey.PageDown:
+                return ConsoleKeyMapping.MapToKeyCodeModifiers (keyInfo.Modifiers, KeyCode.PageDown);
+            case ConsoleKey.Enter:
+                return ConsoleKeyMapping.MapToKeyCodeModifiers (keyInfo.Modifiers, KeyCode.Enter);
+            case ConsoleKey.Spacebar:
+                return ConsoleKeyMapping.MapToKeyCodeModifiers (
+                                                                keyInfo.Modifiers,
+                                                                keyInfo.KeyChar == 0
+                                                                    ? KeyCode.Space
+                                                                    : (KeyCode)keyInfo.KeyChar
+                                                               );
+            case ConsoleKey.Backspace:
+                return ConsoleKeyMapping.MapToKeyCodeModifiers (keyInfo.Modifiers, KeyCode.Backspace);
+            case ConsoleKey.Delete:
+                return ConsoleKeyMapping.MapToKeyCodeModifiers (keyInfo.Modifiers, KeyCode.Delete);
+            case ConsoleKey.Insert:
+                return ConsoleKeyMapping.MapToKeyCodeModifiers (keyInfo.Modifiers, KeyCode.Insert);
+            case ConsoleKey.PrintScreen:
+                return ConsoleKeyMapping.MapToKeyCodeModifiers (keyInfo.Modifiers, KeyCode.PrintScreen);
+
+            case ConsoleKey.Oem1:
+            case ConsoleKey.Oem2:
+            case ConsoleKey.Oem3:
+            case ConsoleKey.Oem4:
+            case ConsoleKey.Oem5:
+            case ConsoleKey.Oem6:
+            case ConsoleKey.Oem7:
+            case ConsoleKey.Oem8:
+            case ConsoleKey.Oem102:
+            case ConsoleKey.OemPeriod:
+            case ConsoleKey.OemComma:
+            case ConsoleKey.OemPlus:
+            case ConsoleKey.OemMinus:
+                if (keyInfo.KeyChar == 0)
+                {
+                    return KeyCode.Null;
+                }
+
+                return ConsoleKeyMapping.MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)keyInfo.KeyChar);
+        }
+
+        ConsoleKey key = keyInfo.Key;
+
+        if (key >= ConsoleKey.A && key <= ConsoleKey.Z)
+        {
+            int delta = key - ConsoleKey.A;
+
+            if (keyInfo.KeyChar != (uint)key)
+            {
+                return ConsoleKeyMapping.MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)keyInfo.Key);
+            }
+
+            if (keyInfo.Modifiers.HasFlag (ConsoleModifiers.Control)
+                || keyInfo.Modifiers.HasFlag (ConsoleModifiers.Alt)
+                || keyInfo.Modifiers.HasFlag (ConsoleModifiers.Shift))
+            {
+                return ConsoleKeyMapping.MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)((uint)KeyCode.A + delta));
+            }
+
+            char alphaBase = keyInfo.Modifiers != ConsoleModifiers.Shift ? 'A' : 'a';
+
+            return (KeyCode)((uint)alphaBase + delta);
+        }
+
+        return ConsoleKeyMapping.MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)keyInfo.KeyChar);
+    }
+
+    private CursorVisibility _savedCursorVisibility;
+
+    private void MockKeyPressedHandler (ConsoleKeyInfo consoleKeyInfo)
+    {
+        if (consoleKeyInfo.Key == ConsoleKey.Packet)
+        {
+            consoleKeyInfo = ConsoleKeyMapping.DecodeVKPacketToKConsoleKeyInfo (consoleKeyInfo);
+        }
+
+        KeyCode map = MapKey (consoleKeyInfo);
+        OnKeyDown (new Key (map));
+        OnKeyUp (new Key (map));
+
+        //OnKeyPressed (new KeyEventArgs (map));
+    }
+
+    /// <inheritdoc/>
+    public override bool GetCursorVisibility (out CursorVisibility visibility)
+    {
+        visibility = FakeConsole.CursorVisible
+                         ? CursorVisibility.Default
+                         : CursorVisibility.Invisible;
+
+        return FakeConsole.CursorVisible;
+    }
+
+    /// <inheritdoc/>
+    public override bool SetCursorVisibility (CursorVisibility visibility)
+    {
+        _savedCursorVisibility = visibility;
+
+        return FakeConsole.CursorVisible = visibility == CursorVisibility.Default;
+    }
+
+    /// <inheritdoc/>
+    public override bool EnsureCursorVisibility ()
+    {
+        if (!(Col >= 0 && Row >= 0 && Col < Cols && Row < Rows))
+        {
+            GetCursorVisibility (out CursorVisibility cursorVisibility);
+            _savedCursorVisibility = cursorVisibility;
+            SetCursorVisibility (CursorVisibility.Invisible);
+
+            return false;
+        }
+
+        SetCursorVisibility (_savedCursorVisibility);
+
+        return FakeConsole.CursorVisible;
+    }
+
+    public override void SendKeys (char keyChar, ConsoleKey key, bool shift, bool alt, bool control)
+    {
+        MockKeyPressedHandler (new ConsoleKeyInfo (keyChar, key, shift, alt, control));
+    }
+
+    public void SetBufferSize (int width, int height)
+    {
+        FakeConsole.SetBufferSize (width, height);
+        Cols = width;
+        Rows = height;
+        SetWindowSize (width, height);
+        ProcessResize ();
+    }
+
+    public void SetWindowSize (int width, int height)
+    {
+        FakeConsole.SetWindowSize (width, height);
+
+        if (width != Cols || height != Rows)
+        {
+            SetBufferSize (width, height);
+            Cols = width;
+            Rows = height;
+        }
+
+        ProcessResize ();
+    }
+
+    public void SetWindowPosition (int left, int top)
+    {
+        if (Left > 0 || Top > 0)
+        {
+            Left = 0;
+            Top = 0;
+        }
+
+        FakeConsole.SetWindowPosition (Left, Top);
+    }
+
+    private void ProcessResize ()
+    {
+        ResizeScreen ();
+        ClearContents ();
+        OnSizeChanged (new SizeChangedEventArgs (new (Cols, Rows)));
+    }
+
+    public virtual void ResizeScreen ()
+    {
+        if (FakeConsole.WindowHeight > 0)
+        {
+            // Can raise an exception while is still resizing.
+            try
+            {
+                FakeConsole.CursorTop = 0;
+                FakeConsole.CursorLeft = 0;
+                FakeConsole.WindowTop = 0;
+                FakeConsole.WindowLeft = 0;
+            }
+            catch (IOException)
+            {
+                return;
+            }
+            catch (ArgumentOutOfRangeException)
+            {
+                return;
+            }
+        }
+
+        // CONCURRENCY: Unsynchronized access to Clip is not safe.
+        Clip = new (0, 0, Cols, Rows);
+    }
+
+    public override void UpdateCursor ()
+    {
+        if (!EnsureCursorVisibility ())
+        {
+            return;
+        }
+
+        // Prevents the exception of size changing during resizing.
+        try
+        {
+            // BUGBUG: Why is this using BufferWidth/Height and now Cols/Rows?
+            if (Col >= 0 && Col < FakeConsole.BufferWidth && Row >= 0 && Row < FakeConsole.BufferHeight)
+            {
+                FakeConsole.SetCursorPosition (Col, Row);
+            }
+        }
+        catch (IOException)
+        { }
+        catch (ArgumentOutOfRangeException)
+        { }
+    }
+
+    #region Not Implemented
+
+    public override void Suspend ()
+    {
+        //throw new NotImplementedException ();
+    }
+
+    #endregion
+
+    public class FakeClipboard : ClipboardBase
+    {
+        public Exception FakeException;
+
+        private readonly bool _isSupportedAlwaysFalse;
+        private string _contents = string.Empty;
+
+        public FakeClipboard (
+            bool fakeClipboardThrowsNotSupportedException = false,
+            bool isSupportedAlwaysFalse = false
+        )
+        {
+            _isSupportedAlwaysFalse = isSupportedAlwaysFalse;
+
+            if (fakeClipboardThrowsNotSupportedException)
+            {
+                FakeException = new NotSupportedException ("Fake clipboard exception");
+            }
+        }
+
+        public override bool IsSupported => !_isSupportedAlwaysFalse;
+
+        protected override string GetClipboardDataImpl ()
+        {
+            if (FakeException is { })
+            {
+                throw FakeException;
+            }
+
+            return _contents;
+        }
+
+        protected override void SetClipboardDataImpl (string text)
+        {
+            if (text is null)
+            {
+                throw new ArgumentNullException (nameof (text));
+            }
+
+            if (FakeException is { })
+            {
+                throw FakeException;
+            }
+
+            _contents = text;
+        }
+    }
 
 #pragma warning restore CS1591 // Missing XML comment for publicly visible type or member
-}
+}

+ 36 - 41
Terminal.Gui/ConsoleDrivers/FakeDriver/FakeMainLoop.cs

@@ -1,42 +1,37 @@
-using System;
-using static Terminal.Gui.NetEvents;
-
-namespace Terminal.Gui;
-
-internal class FakeMainLoop : IMainLoopDriver {
-
-	public Action<ConsoleKeyInfo> MockKeyPressed;
-
-	public FakeMainLoop (ConsoleDriver consoleDriver = null)
-	{
-		// No implementation needed for FakeMainLoop
-	}
-
-	public void Setup (MainLoop mainLoop)
-	{
-		// No implementation needed for FakeMainLoop
-	}
-
-	public void Wakeup ()
-	{
-		// No implementation needed for FakeMainLoop
-	}
-
-	public bool EventsPending ()
-	{
-		// Always return true for FakeMainLoop
-		return true;
-	}
-
-	public void Iteration ()
-	{
-		if (FakeConsole.MockKeyPresses.Count > 0) {
-			MockKeyPressed?.Invoke (FakeConsole.MockKeyPresses.Pop ());
-		}
-	}
-
-	public void TearDown ()
-	{
-	}
+namespace Terminal.Gui;
+
+internal class FakeMainLoop : IMainLoopDriver
+{
+    public Action<ConsoleKeyInfo> MockKeyPressed;
+
+    public FakeMainLoop (ConsoleDriver consoleDriver = null)
+    {
+        // No implementation needed for FakeMainLoop
+    }
+
+    public void Setup (MainLoop mainLoop)
+    {
+        // No implementation needed for FakeMainLoop
+    }
+
+    public void Wakeup ()
+    {
+        // No implementation needed for FakeMainLoop
+    }
+
+    public bool EventsPending ()
+    {
+        // Always return true for FakeMainLoop
+        return true;
+    }
+
+    public void Iteration ()
+    {
+        if (FakeConsole.MockKeyPresses.Count > 0)
+        {
+            MockKeyPressed?.Invoke (FakeConsole.MockKeyPresses.Pop ());
+        }
+    }
+
+    public void TearDown () { }
 }
-

+ 1760 - 1377
Terminal.Gui/ConsoleDrivers/NetDriver.cs

@@ -1,1401 +1,1784 @@
 //
 // NetDriver.cs: The System.Console-based .NET driver, works on Windows and Unix, but is not particularly efficient.
 //
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.IO;
-using System.Linq;
+
 using System.Runtime.InteropServices;
-using System.Text;
-using System.Threading;
-using System.Threading.Tasks;
 using static Terminal.Gui.ConsoleDrivers.ConsoleKeyMapping;
 using static Terminal.Gui.NetEvents;
-using static Terminal.Gui.WindowsConsole;
 
 namespace Terminal.Gui;
 
-class NetWinVTConsole {
-	IntPtr _inputHandle, _outputHandle, _errorHandle;
-	uint _originalInputConsoleMode, _originalOutputConsoleMode, _originalErrorConsoleMode;
-
-	public NetWinVTConsole ()
-	{
-		_inputHandle = GetStdHandle (STD_INPUT_HANDLE);
-		if (!GetConsoleMode (_inputHandle, out uint mode)) {
-			throw new ApplicationException ($"Failed to get input console mode, error code: {GetLastError ()}.");
-		}
-		_originalInputConsoleMode = mode;
-		if ((mode & ENABLE_VIRTUAL_TERMINAL_INPUT) < ENABLE_VIRTUAL_TERMINAL_INPUT) {
-			mode |= ENABLE_VIRTUAL_TERMINAL_INPUT;
-			if (!SetConsoleMode (_inputHandle, mode)) {
-				throw new ApplicationException ($"Failed to set input console mode, error code: {GetLastError ()}.");
-			}
-		}
-
-		_outputHandle = GetStdHandle (STD_OUTPUT_HANDLE);
-		if (!GetConsoleMode (_outputHandle, out mode)) {
-			throw new ApplicationException ($"Failed to get output console mode, error code: {GetLastError ()}.");
-		}
-		_originalOutputConsoleMode = mode;
-		if ((mode & (ENABLE_VIRTUAL_TERMINAL_PROCESSING | DISABLE_NEWLINE_AUTO_RETURN)) < DISABLE_NEWLINE_AUTO_RETURN) {
-			mode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING | DISABLE_NEWLINE_AUTO_RETURN;
-			if (!SetConsoleMode (_outputHandle, mode)) {
-				throw new ApplicationException ($"Failed to set output console mode, error code: {GetLastError ()}.");
-			}
-		}
-
-		_errorHandle = GetStdHandle (STD_ERROR_HANDLE);
-		if (!GetConsoleMode (_errorHandle, out mode)) {
-			throw new ApplicationException ($"Failed to get error console mode, error code: {GetLastError ()}.");
-		}
-		_originalErrorConsoleMode = mode;
-		if ((mode & DISABLE_NEWLINE_AUTO_RETURN) < DISABLE_NEWLINE_AUTO_RETURN) {
-			mode |= DISABLE_NEWLINE_AUTO_RETURN;
-			if (!SetConsoleMode (_errorHandle, mode)) {
-				throw new ApplicationException ($"Failed to set error console mode, error code: {GetLastError ()}.");
-			}
-		}
-	}
-
-	public void Cleanup ()
-	{
-		if (!SetConsoleMode (_inputHandle, _originalInputConsoleMode)) {
-			throw new ApplicationException ($"Failed to restore input console mode, error code: {GetLastError ()}.");
-		}
-		if (!SetConsoleMode (_outputHandle, _originalOutputConsoleMode)) {
-			throw new ApplicationException ($"Failed to restore output console mode, error code: {GetLastError ()}.");
-		}
-		if (!SetConsoleMode (_errorHandle, _originalErrorConsoleMode)) {
-			throw new ApplicationException ($"Failed to restore error console mode, error code: {GetLastError ()}.");
-		}
-	}
-
-	const int STD_INPUT_HANDLE = -10;
-	const int STD_OUTPUT_HANDLE = -11;
-	const int STD_ERROR_HANDLE = -12;
-
-	// Input modes.
-	const uint ENABLE_PROCESSED_INPUT = 1;
-	const uint ENABLE_LINE_INPUT = 2;
-	const uint ENABLE_ECHO_INPUT = 4;
-	const uint ENABLE_WINDOW_INPUT = 8;
-	const uint ENABLE_MOUSE_INPUT = 16;
-	const uint ENABLE_INSERT_MODE = 32;
-	const uint ENABLE_QUICK_EDIT_MODE = 64;
-	const uint ENABLE_EXTENDED_FLAGS = 128;
-	const uint ENABLE_VIRTUAL_TERMINAL_INPUT = 512;
-
-	// Output modes.
-	const uint ENABLE_PROCESSED_OUTPUT = 1;
-	const uint ENABLE_WRAP_AT_EOL_OUTPUT = 2;
-	const uint ENABLE_VIRTUAL_TERMINAL_PROCESSING = 4;
-	const uint DISABLE_NEWLINE_AUTO_RETURN = 8;
-	const uint ENABLE_LVB_GRID_WORLDWIDE = 10;
-
-	[DllImport ("kernel32.dll", SetLastError = true)]
-	extern static IntPtr GetStdHandle (int nStdHandle);
-
-	[DllImport ("kernel32.dll")]
-	extern static bool GetConsoleMode (IntPtr hConsoleHandle, out uint lpMode);
-
-	[DllImport ("kernel32.dll")]
-	extern static bool SetConsoleMode (IntPtr hConsoleHandle, uint dwMode);
-
-	[DllImport ("kernel32.dll")]
-	extern static uint GetLastError ();
+internal class NetWinVTConsole
+{
+    private const uint DISABLE_NEWLINE_AUTO_RETURN = 8;
+    private const uint ENABLE_ECHO_INPUT = 4;
+    private const uint ENABLE_EXTENDED_FLAGS = 128;
+    private const uint ENABLE_INSERT_MODE = 32;
+    private const uint ENABLE_LINE_INPUT = 2;
+    private const uint ENABLE_LVB_GRID_WORLDWIDE = 10;
+    private const uint ENABLE_MOUSE_INPUT = 16;
+
+    // Input modes.
+    private const uint ENABLE_PROCESSED_INPUT = 1;
+
+    // Output modes.
+    private const uint ENABLE_PROCESSED_OUTPUT = 1;
+    private const uint ENABLE_QUICK_EDIT_MODE = 64;
+    private const uint ENABLE_VIRTUAL_TERMINAL_INPUT = 512;
+    private const uint ENABLE_VIRTUAL_TERMINAL_PROCESSING = 4;
+    private const uint ENABLE_WINDOW_INPUT = 8;
+    private const uint ENABLE_WRAP_AT_EOL_OUTPUT = 2;
+    private const int STD_ERROR_HANDLE = -12;
+    private const int STD_INPUT_HANDLE = -10;
+    private const int STD_OUTPUT_HANDLE = -11;
+
+    private readonly nint _errorHandle;
+    private readonly nint _inputHandle;
+    private readonly uint _originalErrorConsoleMode;
+    private readonly uint _originalInputConsoleMode;
+    private readonly uint _originalOutputConsoleMode;
+    private readonly nint _outputHandle;
+
+    public NetWinVTConsole ()
+    {
+        _inputHandle = GetStdHandle (STD_INPUT_HANDLE);
+
+        if (!GetConsoleMode (_inputHandle, out uint mode))
+        {
+            throw new ApplicationException ($"Failed to get input console mode, error code: {GetLastError ()}.");
+        }
+
+        _originalInputConsoleMode = mode;
+
+        if ((mode & ENABLE_VIRTUAL_TERMINAL_INPUT) < ENABLE_VIRTUAL_TERMINAL_INPUT)
+        {
+            mode |= ENABLE_VIRTUAL_TERMINAL_INPUT;
+
+            if (!SetConsoleMode (_inputHandle, mode))
+            {
+                throw new ApplicationException ($"Failed to set input console mode, error code: {GetLastError ()}.");
+            }
+        }
+
+        _outputHandle = GetStdHandle (STD_OUTPUT_HANDLE);
+
+        if (!GetConsoleMode (_outputHandle, out mode))
+        {
+            throw new ApplicationException ($"Failed to get output console mode, error code: {GetLastError ()}.");
+        }
+
+        _originalOutputConsoleMode = mode;
+
+        if ((mode & (ENABLE_VIRTUAL_TERMINAL_PROCESSING | DISABLE_NEWLINE_AUTO_RETURN)) < DISABLE_NEWLINE_AUTO_RETURN)
+        {
+            mode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING | DISABLE_NEWLINE_AUTO_RETURN;
+
+            if (!SetConsoleMode (_outputHandle, mode))
+            {
+                throw new ApplicationException ($"Failed to set output console mode, error code: {GetLastError ()}.");
+            }
+        }
+
+        _errorHandle = GetStdHandle (STD_ERROR_HANDLE);
+
+        if (!GetConsoleMode (_errorHandle, out mode))
+        {
+            throw new ApplicationException ($"Failed to get error console mode, error code: {GetLastError ()}.");
+        }
+
+        _originalErrorConsoleMode = mode;
+
+        if ((mode & DISABLE_NEWLINE_AUTO_RETURN) < DISABLE_NEWLINE_AUTO_RETURN)
+        {
+            mode |= DISABLE_NEWLINE_AUTO_RETURN;
+
+            if (!SetConsoleMode (_errorHandle, mode))
+            {
+                throw new ApplicationException ($"Failed to set error console mode, error code: {GetLastError ()}.");
+            }
+        }
+    }
+
+    public void Cleanup ()
+    {
+        if (!SetConsoleMode (_inputHandle, _originalInputConsoleMode))
+        {
+            throw new ApplicationException ($"Failed to restore input console mode, error code: {GetLastError ()}.");
+        }
+
+        if (!SetConsoleMode (_outputHandle, _originalOutputConsoleMode))
+        {
+            throw new ApplicationException ($"Failed to restore output console mode, error code: {GetLastError ()}.");
+        }
+
+        if (!SetConsoleMode (_errorHandle, _originalErrorConsoleMode))
+        {
+            throw new ApplicationException ($"Failed to restore error console mode, error code: {GetLastError ()}.");
+        }
+    }
+
+    [DllImport ("kernel32.dll")]
+    private static extern bool GetConsoleMode (nint hConsoleHandle, out uint lpMode);
+
+    [DllImport ("kernel32.dll")]
+    private static extern uint GetLastError ();
+
+    [DllImport ("kernel32.dll", SetLastError = true)]
+    private static extern nint GetStdHandle (int nStdHandle);
+
+    [DllImport ("kernel32.dll")]
+    private static extern bool SetConsoleMode (nint hConsoleHandle, uint dwMode);
 }
 
-class NetEvents : IDisposable {
-	readonly ManualResetEventSlim _inputReady = new (false);
-	CancellationTokenSource _inputReadyCancellationTokenSource;
-
-	readonly ManualResetEventSlim _waitForStart = new (false);
-	//CancellationTokenSource _waitForStartCancellationTokenSource;
-
-	readonly ManualResetEventSlim _winChange = new (false);
-
-	readonly Queue<InputResult?> _inputQueue = new ();
-
-	readonly ConsoleDriver _consoleDriver;
-	ConsoleKeyInfo [] _cki;
-	bool _isEscSeq;
-
+internal class NetEvents : IDisposable
+{
+    private readonly ManualResetEventSlim _inputReady = new (false);
+    private CancellationTokenSource _inputReadyCancellationTokenSource;
+    private readonly ManualResetEventSlim _waitForStart = new (false);
+
+    //CancellationTokenSource _waitForStartCancellationTokenSource;
+    private readonly ManualResetEventSlim _winChange = new (false);
+    private readonly Queue<InputResult?> _inputQueue = new ();
+    private readonly ConsoleDriver _consoleDriver;
+    private ConsoleKeyInfo [] _cki;
+    private bool _isEscSeq;
 #if PROCESS_REQUEST
-		bool _neededProcessRequest;
+    bool _neededProcessRequest;
 #endif
-	public EscSeqRequests EscSeqRequests { get; } = new ();
-
-	public NetEvents (ConsoleDriver consoleDriver)
-	{
-		_consoleDriver = consoleDriver ?? throw new ArgumentNullException (nameof (consoleDriver));
-		_inputReadyCancellationTokenSource = new CancellationTokenSource ();
-
-		Task.Run (ProcessInputQueue, _inputReadyCancellationTokenSource.Token);
-
-		Task.Run (CheckWindowSizeChange, _inputReadyCancellationTokenSource.Token);
-	}
-
-	public InputResult? DequeueInput ()
-	{
-		while (_inputReadyCancellationTokenSource != null && !_inputReadyCancellationTokenSource.Token.IsCancellationRequested) {
-			_waitForStart.Set ();
-			_winChange.Set ();
-
-			try {
-				if (!_inputReadyCancellationTokenSource.Token.IsCancellationRequested) {
-					if (_inputQueue.Count == 0) {
-						_inputReady.Wait (_inputReadyCancellationTokenSource.Token);
-					}
-				}
-
-			} catch (OperationCanceledException) {
-				return null;
-			} finally {
-				_inputReady.Reset ();
-			}
+    public EscSeqRequests EscSeqRequests { get; } = new ();
+
+    public NetEvents (ConsoleDriver consoleDriver)
+    {
+        _consoleDriver = consoleDriver ?? throw new ArgumentNullException (nameof (consoleDriver));
+        _inputReadyCancellationTokenSource = new CancellationTokenSource ();
+
+        Task.Run (ProcessInputQueue, _inputReadyCancellationTokenSource.Token);
+
+        Task.Run (CheckWindowSizeChange, _inputReadyCancellationTokenSource.Token);
+    }
+
+    public InputResult? DequeueInput ()
+    {
+        while (_inputReadyCancellationTokenSource != null
+               && !_inputReadyCancellationTokenSource.Token.IsCancellationRequested)
+        {
+            _waitForStart.Set ();
+            _winChange.Set ();
+
+            try
+            {
+                if (!_inputReadyCancellationTokenSource.Token.IsCancellationRequested)
+                {
+                    if (_inputQueue.Count == 0)
+                    {
+                        _inputReady.Wait (_inputReadyCancellationTokenSource.Token);
+                    }
+                }
+            }
+            catch (OperationCanceledException)
+            {
+                return null;
+            }
+            finally
+            {
+                _inputReady.Reset ();
+            }
 
 #if PROCESS_REQUEST
-				_neededProcessRequest = false;
+            _neededProcessRequest = false;
 #endif
-			if (_inputQueue.Count > 0) {
-				return _inputQueue.Dequeue ();
-			}
-		}
-		return null;
-	}
-
-	static ConsoleKeyInfo ReadConsoleKeyInfo (CancellationToken cancellationToken, bool intercept = true)
-	{
-		// if there is a key available, return it without waiting
-		//  (or dispatching work to the thread queue)
-		if (Console.KeyAvailable) {
-			return Console.ReadKey (intercept);
-		}
-
-		while (!cancellationToken.IsCancellationRequested) {
-			Task.Delay (100);
-			if (Console.KeyAvailable) {
-				return Console.ReadKey (intercept);
-			}
-		}
-		cancellationToken.ThrowIfCancellationRequested ();
-		return default;
-	}
-
-	void ProcessInputQueue ()
-	{
-		while (!_inputReadyCancellationTokenSource.Token.IsCancellationRequested) {
-
-			try {
-				_waitForStart.Wait (_inputReadyCancellationTokenSource.Token);
-			} catch (OperationCanceledException) {
-
-				return;
-			}
-			_waitForStart.Reset ();
-
-			if (_inputQueue.Count == 0) {
-				ConsoleKey key = 0;
-				ConsoleModifiers mod = 0;
-				ConsoleKeyInfo newConsoleKeyInfo = default;
-
-				while (true) {
-					if (_inputReadyCancellationTokenSource.Token.IsCancellationRequested) {
-						return;
-					}
-					ConsoleKeyInfo consoleKeyInfo;
-					try {
-						consoleKeyInfo = ReadConsoleKeyInfo (_inputReadyCancellationTokenSource.Token, true);
-					} catch (OperationCanceledException) {
-						return;
-					}
-					if (consoleKeyInfo.KeyChar == (char)KeyCode.Esc && !_isEscSeq
-					|| consoleKeyInfo.KeyChar != (char)KeyCode.Esc && _isEscSeq) {
-
-						if (_cki == null && consoleKeyInfo.KeyChar != (char)KeyCode.Esc && _isEscSeq) {
-							_cki = EscSeqUtils.ResizeArray (new ConsoleKeyInfo ((char)KeyCode.Esc, 0,
-								false, false, false), _cki);
-						}
-						_isEscSeq = true;
-						newConsoleKeyInfo = consoleKeyInfo;
-						_cki = EscSeqUtils.ResizeArray (consoleKeyInfo, _cki);
-						if (Console.KeyAvailable) {
-							continue;
-						}
-						ProcessRequestResponse (ref newConsoleKeyInfo, ref key, _cki, ref mod);
-						_cki = null;
-						_isEscSeq = false;
-						break;
-					} else if (consoleKeyInfo.KeyChar == (char)KeyCode.Esc && _isEscSeq && _cki != null) {
-						ProcessRequestResponse (ref newConsoleKeyInfo, ref key, _cki, ref mod);
-						_cki = null;
-						if (Console.KeyAvailable) {
-							_cki = EscSeqUtils.ResizeArray (consoleKeyInfo, _cki);
-						} else {
-							ProcessMapConsoleKeyInfo (consoleKeyInfo);
-						}
-						break;
-					} else {
-						ProcessMapConsoleKeyInfo (consoleKeyInfo);
-						break;
-					}
-				}
-			}
-
-			_inputReady.Set ();
-		}
-
-		void ProcessMapConsoleKeyInfo (ConsoleKeyInfo consoleKeyInfo)
-		{
-			_inputQueue.Enqueue (new InputResult {
-				EventType = EventType.Key,
-				ConsoleKeyInfo = EscSeqUtils.MapConsoleKeyInfo (consoleKeyInfo)
-			});
-			_isEscSeq = false;
-		}
-	}
-
-	void CheckWindowSizeChange ()
-	{
-		void RequestWindowSize (CancellationToken cancellationToken)
-		{
-			while (!cancellationToken.IsCancellationRequested) {
-				// Wait for a while then check if screen has changed sizes
-				Task.Delay (500, cancellationToken);
-
-				int buffHeight, buffWidth;
-				if (((NetDriver)_consoleDriver).IsWinPlatform) {
-					buffHeight = Math.Max (Console.BufferHeight, 0);
-					buffWidth = Math.Max (Console.BufferWidth, 0);
-				} else {
-					buffHeight = _consoleDriver.Rows;
-					buffWidth = _consoleDriver.Cols;
-				}
-				if (EnqueueWindowSizeEvent (
-					Math.Max (Console.WindowHeight, 0),
-					Math.Max (Console.WindowWidth, 0),
-					buffHeight,
-					buffWidth)) {
-
-					return;
-				}
-			}
-			cancellationToken.ThrowIfCancellationRequested ();
-		}
-
-		while (true) {
-			if (_inputReadyCancellationTokenSource.IsCancellationRequested) {
-				return;
-			}
-			_winChange.Wait (_inputReadyCancellationTokenSource.Token);
-			_winChange.Reset ();
-			try {
-				RequestWindowSize (_inputReadyCancellationTokenSource.Token);
-			} catch (OperationCanceledException) {
-				return;
-			}
-			_inputReady.Set ();
-		}
-	}
-
-	/// <summary>
-	/// Enqueue a window size event if the window size has changed.
-	/// </summary>
-	/// <param name="winHeight"></param>
-	/// <param name="winWidth"></param>
-	/// <param name="buffHeight"></param>
-	/// <param name="buffWidth"></param>
-	/// <returns></returns>
-	bool EnqueueWindowSizeEvent (int winHeight, int winWidth, int buffHeight, int buffWidth)
-	{
-		if (winWidth == _consoleDriver.Cols && winHeight == _consoleDriver.Rows) {
-			return false;
-		}
-		int w = Math.Max (winWidth, 0);
-		int h = Math.Max (winHeight, 0);
-		_inputQueue.Enqueue (new InputResult () {
-			EventType = EventType.WindowSize,
-			WindowSizeEvent = new WindowSizeEvent () {
-				Size = new Size (w, h)
-			}
-		});
-		return true;
-	}
-
-	// Process a CSI sequence received by the driver (key pressed, mouse event, or request/response event)
-	void ProcessRequestResponse (ref ConsoleKeyInfo newConsoleKeyInfo, ref ConsoleKey key, ConsoleKeyInfo [] cki, ref ConsoleModifiers mod)
-	{
-		// isMouse is true if it's CSI<, false otherwise
-		EscSeqUtils.DecodeEscSeq (EscSeqRequests, ref newConsoleKeyInfo, ref key, cki, ref mod,
-			out string c1Control, out string code, out string [] values, out string terminating,
-			out bool isMouse, out var mouseFlags,
-			out var pos, out bool isReq,
-			(f, p) => HandleMouseEvent (MapMouseFlags (f), p));
-
-		if (isMouse) {
-			foreach (var mf in mouseFlags) {
-				HandleMouseEvent (MapMouseFlags (mf), pos);
-			}
-			return;
-		} else if (isReq) {
-			HandleRequestResponseEvent (c1Control, code, values, terminating);
-			return;
-		}
-		HandleKeyboardEvent (newConsoleKeyInfo);
-	}
-
-	MouseButtonState MapMouseFlags (MouseFlags mouseFlags)
-	{
-		MouseButtonState mbs = default;
-		foreach (object flag in Enum.GetValues (mouseFlags.GetType ())) {
-			if (mouseFlags.HasFlag ((MouseFlags)flag)) {
-				switch (flag) {
-				case MouseFlags.Button1Pressed:
-					mbs |= MouseButtonState.Button1Pressed;
-					break;
-				case MouseFlags.Button1Released:
-					mbs |= MouseButtonState.Button1Released;
-					break;
-				case MouseFlags.Button1Clicked:
-					mbs |= MouseButtonState.Button1Clicked;
-					break;
-				case MouseFlags.Button1DoubleClicked:
-					mbs |= MouseButtonState.Button1DoubleClicked;
-					break;
-				case MouseFlags.Button1TripleClicked:
-					mbs |= MouseButtonState.Button1TripleClicked;
-					break;
-				case MouseFlags.Button2Pressed:
-					mbs |= MouseButtonState.Button2Pressed;
-					break;
-				case MouseFlags.Button2Released:
-					mbs |= MouseButtonState.Button2Released;
-					break;
-				case MouseFlags.Button2Clicked:
-					mbs |= MouseButtonState.Button2Clicked;
-					break;
-				case MouseFlags.Button2DoubleClicked:
-					mbs |= MouseButtonState.Button2DoubleClicked;
-					break;
-				case MouseFlags.Button2TripleClicked:
-					mbs |= MouseButtonState.Button2TripleClicked;
-					break;
-				case MouseFlags.Button3Pressed:
-					mbs |= MouseButtonState.Button3Pressed;
-					break;
-				case MouseFlags.Button3Released:
-					mbs |= MouseButtonState.Button3Released;
-					break;
-				case MouseFlags.Button3Clicked:
-					mbs |= MouseButtonState.Button3Clicked;
-					break;
-				case MouseFlags.Button3DoubleClicked:
-					mbs |= MouseButtonState.Button3DoubleClicked;
-					break;
-				case MouseFlags.Button3TripleClicked:
-					mbs |= MouseButtonState.Button3TripleClicked;
-					break;
-				case MouseFlags.WheeledUp:
-					mbs |= MouseButtonState.ButtonWheeledUp;
-					break;
-				case MouseFlags.WheeledDown:
-					mbs |= MouseButtonState.ButtonWheeledDown;
-					break;
-				case MouseFlags.WheeledLeft:
-					mbs |= MouseButtonState.ButtonWheeledLeft;
-					break;
-				case MouseFlags.WheeledRight:
-					mbs |= MouseButtonState.ButtonWheeledRight;
-					break;
-				case MouseFlags.Button4Pressed:
-					mbs |= MouseButtonState.Button4Pressed;
-					break;
-				case MouseFlags.Button4Released:
-					mbs |= MouseButtonState.Button4Released;
-					break;
-				case MouseFlags.Button4Clicked:
-					mbs |= MouseButtonState.Button4Clicked;
-					break;
-				case MouseFlags.Button4DoubleClicked:
-					mbs |= MouseButtonState.Button4DoubleClicked;
-					break;
-				case MouseFlags.Button4TripleClicked:
-					mbs |= MouseButtonState.Button4TripleClicked;
-					break;
-				case MouseFlags.ButtonShift:
-					mbs |= MouseButtonState.ButtonShift;
-					break;
-				case MouseFlags.ButtonCtrl:
-					mbs |= MouseButtonState.ButtonCtrl;
-					break;
-				case MouseFlags.ButtonAlt:
-					mbs |= MouseButtonState.ButtonAlt;
-					break;
-				case MouseFlags.ReportMousePosition:
-					mbs |= MouseButtonState.ReportMousePosition;
-					break;
-				case MouseFlags.AllEvents:
-					mbs |= MouseButtonState.AllEvents;
-					break;
-				}
-			}
-		}
-		return mbs;
-	}
-
-	Point _lastCursorPosition;
-
-	void HandleRequestResponseEvent (string c1Control, string code, string [] values, string terminating)
-	{
-		switch (terminating) {
-		// BUGBUG: I can't find where we send a request for cursor position (ESC[?6n), so I'm not sure if this is needed.
-		case EscSeqUtils.CSI_RequestCursorPositionReport_Terminator:
-			var point = new Point {
-				X = int.Parse (values [1]) - 1,
-				Y = int.Parse (values [0]) - 1
-			};
-			if (_lastCursorPosition.Y != point.Y) {
-				_lastCursorPosition = point;
-				var eventType = EventType.WindowPosition;
-				var winPositionEv = new WindowPositionEvent () {
-					CursorPosition = point
-				};
-				_inputQueue.Enqueue (new InputResult () {
-					EventType = eventType,
-					WindowPositionEvent = winPositionEv
-				});
-			} else {
-				return;
-			}
-			break;
-
-		case EscSeqUtils.CSI_ReportTerminalSizeInChars_Terminator:
-			switch (values [0]) {
-			case EscSeqUtils.CSI_ReportTerminalSizeInChars_ResponseValue:
-				EnqueueWindowSizeEvent (
-					Math.Max (int.Parse (values [1]), 0),
-					Math.Max (int.Parse (values [2]), 0),
-					Math.Max (int.Parse (values [1]), 0),
-					Math.Max (int.Parse (values [2]), 0));
-				break;
-			default:
-				EnqueueRequestResponseEvent (c1Control, code, values, terminating);
-				break;
-			}
-			break;
-		default:
-			EnqueueRequestResponseEvent (c1Control, code, values, terminating);
-			break;
-		}
-
-		_inputReady.Set ();
-	}
-
-	void EnqueueRequestResponseEvent (string c1Control, string code, string [] values, string terminating)
-	{
-		var eventType = EventType.RequestResponse;
-		var requestRespEv = new RequestResponseEvent () {
-			ResultTuple = (c1Control, code, values, terminating)
-		};
-		_inputQueue.Enqueue (new InputResult () {
-			EventType = eventType,
-			RequestResponseEvent = requestRespEv
-		});
-	}
-
-	void HandleMouseEvent (MouseButtonState buttonState, Point pos)
-	{
-		var mouseEvent = new MouseEvent () {
-			Position = pos,
-			ButtonState = buttonState
-		};
-
-		_inputQueue.Enqueue (new InputResult () {
-			EventType = EventType.Mouse,
-			MouseEvent = mouseEvent
-		});
-
-		_inputReady.Set ();
-	}
-
-	public enum EventType {
-		Key = 1,
-		Mouse = 2,
-		WindowSize = 3,
-		WindowPosition = 4,
-		RequestResponse = 5
-	}
-
-	[Flags]
-	public enum MouseButtonState {
-		Button1Pressed = 0x1,
-		Button1Released = 0x2,
-		Button1Clicked = 0x4,
-		Button1DoubleClicked = 0x8,
-		Button1TripleClicked = 0x10,
-		Button2Pressed = 0x20,
-		Button2Released = 0x40,
-		Button2Clicked = 0x80,
-		Button2DoubleClicked = 0x100,
-		Button2TripleClicked = 0x200,
-		Button3Pressed = 0x400,
-		Button3Released = 0x800,
-		Button3Clicked = 0x1000,
-		Button3DoubleClicked = 0x2000,
-		Button3TripleClicked = 0x4000,
-		ButtonWheeledUp = 0x8000,
-		ButtonWheeledDown = 0x10000,
-		ButtonWheeledLeft = 0x20000,
-		ButtonWheeledRight = 0x40000,
-		Button4Pressed = 0x80000,
-		Button4Released = 0x100000,
-		Button4Clicked = 0x200000,
-		Button4DoubleClicked = 0x400000,
-		Button4TripleClicked = 0x800000,
-		ButtonShift = 0x1000000,
-		ButtonCtrl = 0x2000000,
-		ButtonAlt = 0x4000000,
-		ReportMousePosition = 0x8000000,
-		AllEvents = -1
-	}
-
-	public struct MouseEvent {
-		public Point Position;
-		public MouseButtonState ButtonState;
-	}
-
-	public struct WindowSizeEvent {
-		public Size Size;
-	}
-
-	public struct WindowPositionEvent {
-		public int Top;
-		public int Left;
-		public Point CursorPosition;
-	}
-
-	public struct RequestResponseEvent {
-		public (string c1Control, string code, string [] values, string terminating) ResultTuple;
-	}
-
-	public struct InputResult {
-		public EventType EventType;
-		public ConsoleKeyInfo ConsoleKeyInfo;
-		public MouseEvent MouseEvent;
-		public WindowSizeEvent WindowSizeEvent;
-		public WindowPositionEvent WindowPositionEvent;
-		public RequestResponseEvent RequestResponseEvent;
-
-		public override readonly string ToString ()
-		{
-			return EventType switch {
-				EventType.Key => ToString (ConsoleKeyInfo),
-				EventType.Mouse => MouseEvent.ToString (),
-				//EventType.WindowSize => WindowSize.ToString (),
-				//EventType.RequestResponse => RequestResponse.ToString (),
-				_ => "Unknown event type: " + EventType
-			};
-		}
-
-		/// <summary>
-		/// Prints a ConsoleKeyInfoEx structure
-		/// </summary>
-		/// <param name="cki"></param>
-		/// <returns></returns>
-		public readonly string ToString (ConsoleKeyInfo cki)
-		{
-			var ke = new Key ((KeyCode)cki.KeyChar);
-			var sb = new StringBuilder ();
-			sb.Append ($"Key: {(KeyCode)cki.Key} ({cki.Key})");
-			sb.Append ((cki.Modifiers & ConsoleModifiers.Shift) != 0 ? " | Shift" : string.Empty);
-			sb.Append ((cki.Modifiers & ConsoleModifiers.Control) != 0 ? " | Control" : string.Empty);
-			sb.Append ((cki.Modifiers & ConsoleModifiers.Alt) != 0 ? " | Alt" : string.Empty);
-			sb.Append ($", KeyChar: {ke.AsRune.MakePrintable ()} ({(uint)cki.KeyChar}) ");
-			var s = sb.ToString ().TrimEnd (',').TrimEnd (' ');
-			return $"[ConsoleKeyInfo({s})]";
-		}
-	}
-
-	void HandleKeyboardEvent (ConsoleKeyInfo cki)
-	{
-		var inputResult = new InputResult {
-			EventType = EventType.Key,
-			ConsoleKeyInfo = cki
-		};
-
-		_inputQueue.Enqueue (inputResult);
-	}
-
-	public void Dispose ()
-	{
-		_inputReadyCancellationTokenSource?.Cancel ();
-		_inputReadyCancellationTokenSource?.Dispose ();
-		_inputReadyCancellationTokenSource = null;
-
-		try {
-			// throws away any typeahead that has been typed by
-			// the user and has not yet been read by the program.
-			while (Console.KeyAvailable) {
-				Console.ReadKey (true);
-			}
-		} catch (InvalidOperationException) {
-			// Ignore - Console input has already been closed
-		}
-	}
+            if (_inputQueue.Count > 0)
+            {
+                return _inputQueue.Dequeue ();
+            }
+        }
+
+        return null;
+    }
+
+    private static ConsoleKeyInfo ReadConsoleKeyInfo (CancellationToken cancellationToken, bool intercept = true)
+    {
+        // if there is a key available, return it without waiting
+        //  (or dispatching work to the thread queue)
+        if (Console.KeyAvailable)
+        {
+            return Console.ReadKey (intercept);
+        }
+
+        while (!cancellationToken.IsCancellationRequested)
+        {
+            Task.Delay (100);
+
+            if (Console.KeyAvailable)
+            {
+                return Console.ReadKey (intercept);
+            }
+        }
+
+        cancellationToken.ThrowIfCancellationRequested ();
+
+        return default (ConsoleKeyInfo);
+    }
+
+    private void ProcessInputQueue ()
+    {
+        while (!_inputReadyCancellationTokenSource.Token.IsCancellationRequested)
+        {
+            try
+            {
+                _waitForStart.Wait (_inputReadyCancellationTokenSource.Token);
+            }
+            catch (OperationCanceledException)
+            {
+                return;
+            }
+
+            _waitForStart.Reset ();
+
+            if (_inputQueue.Count == 0)
+            {
+                ConsoleKey key = 0;
+                ConsoleModifiers mod = 0;
+                ConsoleKeyInfo newConsoleKeyInfo = default;
+
+                while (true)
+                {
+                    if (_inputReadyCancellationTokenSource.Token.IsCancellationRequested)
+                    {
+                        return;
+                    }
+
+                    ConsoleKeyInfo consoleKeyInfo;
+
+                    try
+                    {
+                        consoleKeyInfo = ReadConsoleKeyInfo (_inputReadyCancellationTokenSource.Token);
+                    }
+                    catch (OperationCanceledException)
+                    {
+                        return;
+                    }
+
+                    if ((consoleKeyInfo.KeyChar == (char)KeyCode.Esc && !_isEscSeq)
+                        || (consoleKeyInfo.KeyChar != (char)KeyCode.Esc && _isEscSeq))
+                    {
+                        if (_cki is null && consoleKeyInfo.KeyChar != (char)KeyCode.Esc && _isEscSeq)
+                        {
+                            _cki = EscSeqUtils.ResizeArray (
+                                                            new ConsoleKeyInfo (
+                                                                                (char)KeyCode.Esc,
+                                                                                0,
+                                                                                false,
+                                                                                false,
+                                                                                false
+                                                                               ),
+                                                            _cki
+                                                           );
+                        }
+
+                        _isEscSeq = true;
+                        newConsoleKeyInfo = consoleKeyInfo;
+                        _cki = EscSeqUtils.ResizeArray (consoleKeyInfo, _cki);
+
+                        if (Console.KeyAvailable)
+                        {
+                            continue;
+                        }
+
+                        ProcessRequestResponse (ref newConsoleKeyInfo, ref key, _cki, ref mod);
+                        _cki = null;
+                        _isEscSeq = false;
+
+                        break;
+                    }
+
+                    if (consoleKeyInfo.KeyChar == (char)KeyCode.Esc && _isEscSeq && _cki is { })
+                    {
+                        ProcessRequestResponse (ref newConsoleKeyInfo, ref key, _cki, ref mod);
+                        _cki = null;
+
+                        if (Console.KeyAvailable)
+                        {
+                            _cki = EscSeqUtils.ResizeArray (consoleKeyInfo, _cki);
+                        }
+                        else
+                        {
+                            ProcessMapConsoleKeyInfo (consoleKeyInfo);
+                        }
+
+                        break;
+                    }
+
+                    ProcessMapConsoleKeyInfo (consoleKeyInfo);
+
+                    break;
+                }
+            }
+
+            _inputReady.Set ();
+        }
+
+        void ProcessMapConsoleKeyInfo (ConsoleKeyInfo consoleKeyInfo)
+        {
+            _inputQueue.Enqueue (
+                                 new InputResult
+                                 {
+                                     EventType = EventType.Key, ConsoleKeyInfo = EscSeqUtils.MapConsoleKeyInfo (consoleKeyInfo)
+                                 }
+                                );
+            _isEscSeq = false;
+        }
+    }
+
+    private void CheckWindowSizeChange ()
+    {
+        void RequestWindowSize (CancellationToken cancellationToken)
+        {
+            while (!cancellationToken.IsCancellationRequested)
+            {
+                // Wait for a while then check if screen has changed sizes
+                Task.Delay (500, cancellationToken);
+
+                int buffHeight, buffWidth;
+
+                if (((NetDriver)_consoleDriver).IsWinPlatform)
+                {
+                    buffHeight = Math.Max (Console.BufferHeight, 0);
+                    buffWidth = Math.Max (Console.BufferWidth, 0);
+                }
+                else
+                {
+                    buffHeight = _consoleDriver.Rows;
+                    buffWidth = _consoleDriver.Cols;
+                }
+
+                if (EnqueueWindowSizeEvent (
+                                            Math.Max (Console.WindowHeight, 0),
+                                            Math.Max (Console.WindowWidth, 0),
+                                            buffHeight,
+                                            buffWidth
+                                           ))
+                {
+                    return;
+                }
+            }
+
+            cancellationToken.ThrowIfCancellationRequested ();
+        }
+
+        while (true)
+        {
+            if (_inputReadyCancellationTokenSource.IsCancellationRequested)
+            {
+                return;
+            }
+
+            _winChange.Wait (_inputReadyCancellationTokenSource.Token);
+            _winChange.Reset ();
+
+            try
+            {
+                RequestWindowSize (_inputReadyCancellationTokenSource.Token);
+            }
+            catch (OperationCanceledException)
+            {
+                return;
+            }
+
+            _inputReady.Set ();
+        }
+    }
+
+    /// <summary>Enqueue a window size event if the window size has changed.</summary>
+    /// <param name="winHeight"></param>
+    /// <param name="winWidth"></param>
+    /// <param name="buffHeight"></param>
+    /// <param name="buffWidth"></param>
+    /// <returns></returns>
+    private bool EnqueueWindowSizeEvent (int winHeight, int winWidth, int buffHeight, int buffWidth)
+    {
+        if (winWidth == _consoleDriver.Cols && winHeight == _consoleDriver.Rows)
+        {
+            return false;
+        }
+
+        int w = Math.Max (winWidth, 0);
+        int h = Math.Max (winHeight, 0);
+
+        _inputQueue.Enqueue (
+                             new InputResult
+                             {
+                                 EventType = EventType.WindowSize, WindowSizeEvent = new WindowSizeEvent { Size = new (w, h) }
+                             }
+                            );
+
+        return true;
+    }
+
+    // Process a CSI sequence received by the driver (key pressed, mouse event, or request/response event)
+    private void ProcessRequestResponse (
+        ref ConsoleKeyInfo newConsoleKeyInfo,
+        ref ConsoleKey key,
+        ConsoleKeyInfo [] cki,
+        ref ConsoleModifiers mod
+    )
+    {
+        // isMouse is true if it's CSI<, false otherwise
+        EscSeqUtils.DecodeEscSeq (
+                                  EscSeqRequests,
+                                  ref newConsoleKeyInfo,
+                                  ref key,
+                                  cki,
+                                  ref mod,
+                                  out string c1Control,
+                                  out string code,
+                                  out string [] values,
+                                  out string terminating,
+                                  out bool isMouse,
+                                  out List<MouseFlags> mouseFlags,
+                                  out Point pos,
+                                  out bool isReq,
+                                  (f, p) => HandleMouseEvent (MapMouseFlags (f), p)
+                                 );
+
+        if (isMouse)
+        {
+            foreach (MouseFlags mf in mouseFlags)
+            {
+                HandleMouseEvent (MapMouseFlags (mf), pos);
+            }
+
+            return;
+        }
+
+        if (isReq)
+        {
+            HandleRequestResponseEvent (c1Control, code, values, terminating);
+
+            return;
+        }
+
+        HandleKeyboardEvent (newConsoleKeyInfo);
+    }
+
+    private MouseButtonState MapMouseFlags (MouseFlags mouseFlags)
+    {
+        MouseButtonState mbs = default;
+
+        foreach (object flag in Enum.GetValues (mouseFlags.GetType ()))
+        {
+            if (mouseFlags.HasFlag ((MouseFlags)flag))
+            {
+                switch (flag)
+                {
+                    case MouseFlags.Button1Pressed:
+                        mbs |= MouseButtonState.Button1Pressed;
+
+                        break;
+                    case MouseFlags.Button1Released:
+                        mbs |= MouseButtonState.Button1Released;
+
+                        break;
+                    case MouseFlags.Button1Clicked:
+                        mbs |= MouseButtonState.Button1Clicked;
+
+                        break;
+                    case MouseFlags.Button1DoubleClicked:
+                        mbs |= MouseButtonState.Button1DoubleClicked;
+
+                        break;
+                    case MouseFlags.Button1TripleClicked:
+                        mbs |= MouseButtonState.Button1TripleClicked;
+
+                        break;
+                    case MouseFlags.Button2Pressed:
+                        mbs |= MouseButtonState.Button2Pressed;
+
+                        break;
+                    case MouseFlags.Button2Released:
+                        mbs |= MouseButtonState.Button2Released;
+
+                        break;
+                    case MouseFlags.Button2Clicked:
+                        mbs |= MouseButtonState.Button2Clicked;
+
+                        break;
+                    case MouseFlags.Button2DoubleClicked:
+                        mbs |= MouseButtonState.Button2DoubleClicked;
+
+                        break;
+                    case MouseFlags.Button2TripleClicked:
+                        mbs |= MouseButtonState.Button2TripleClicked;
+
+                        break;
+                    case MouseFlags.Button3Pressed:
+                        mbs |= MouseButtonState.Button3Pressed;
+
+                        break;
+                    case MouseFlags.Button3Released:
+                        mbs |= MouseButtonState.Button3Released;
+
+                        break;
+                    case MouseFlags.Button3Clicked:
+                        mbs |= MouseButtonState.Button3Clicked;
+
+                        break;
+                    case MouseFlags.Button3DoubleClicked:
+                        mbs |= MouseButtonState.Button3DoubleClicked;
+
+                        break;
+                    case MouseFlags.Button3TripleClicked:
+                        mbs |= MouseButtonState.Button3TripleClicked;
+
+                        break;
+                    case MouseFlags.WheeledUp:
+                        mbs |= MouseButtonState.ButtonWheeledUp;
+
+                        break;
+                    case MouseFlags.WheeledDown:
+                        mbs |= MouseButtonState.ButtonWheeledDown;
+
+                        break;
+                    case MouseFlags.WheeledLeft:
+                        mbs |= MouseButtonState.ButtonWheeledLeft;
+
+                        break;
+                    case MouseFlags.WheeledRight:
+                        mbs |= MouseButtonState.ButtonWheeledRight;
+
+                        break;
+                    case MouseFlags.Button4Pressed:
+                        mbs |= MouseButtonState.Button4Pressed;
+
+                        break;
+                    case MouseFlags.Button4Released:
+                        mbs |= MouseButtonState.Button4Released;
+
+                        break;
+                    case MouseFlags.Button4Clicked:
+                        mbs |= MouseButtonState.Button4Clicked;
+
+                        break;
+                    case MouseFlags.Button4DoubleClicked:
+                        mbs |= MouseButtonState.Button4DoubleClicked;
+
+                        break;
+                    case MouseFlags.Button4TripleClicked:
+                        mbs |= MouseButtonState.Button4TripleClicked;
+
+                        break;
+                    case MouseFlags.ButtonShift:
+                        mbs |= MouseButtonState.ButtonShift;
+
+                        break;
+                    case MouseFlags.ButtonCtrl:
+                        mbs |= MouseButtonState.ButtonCtrl;
+
+                        break;
+                    case MouseFlags.ButtonAlt:
+                        mbs |= MouseButtonState.ButtonAlt;
+
+                        break;
+                    case MouseFlags.ReportMousePosition:
+                        mbs |= MouseButtonState.ReportMousePosition;
+
+                        break;
+                    case MouseFlags.AllEvents:
+                        mbs |= MouseButtonState.AllEvents;
+
+                        break;
+                }
+            }
+        }
+
+        return mbs;
+    }
+
+    private Point _lastCursorPosition;
+
+    private void HandleRequestResponseEvent (string c1Control, string code, string [] values, string terminating)
+    {
+        switch (terminating)
+        {
+            // BUGBUG: I can't find where we send a request for cursor position (ESC[?6n), so I'm not sure if this is needed.
+            case EscSeqUtils.CSI_RequestCursorPositionReport_Terminator:
+                var point = new Point { X = int.Parse (values [1]) - 1, Y = int.Parse (values [0]) - 1 };
+
+                if (_lastCursorPosition.Y != point.Y)
+                {
+                    _lastCursorPosition = point;
+                    var eventType = EventType.WindowPosition;
+                    var winPositionEv = new WindowPositionEvent { CursorPosition = point };
+
+                    _inputQueue.Enqueue (
+                                         new InputResult { EventType = eventType, WindowPositionEvent = winPositionEv }
+                                        );
+                }
+                else
+                {
+                    return;
+                }
+
+                break;
+
+            case EscSeqUtils.CSI_ReportTerminalSizeInChars_Terminator:
+                switch (values [0])
+                {
+                    case EscSeqUtils.CSI_ReportTerminalSizeInChars_ResponseValue:
+                        EnqueueWindowSizeEvent (
+                                                Math.Max (int.Parse (values [1]), 0),
+                                                Math.Max (int.Parse (values [2]), 0),
+                                                Math.Max (int.Parse (values [1]), 0),
+                                                Math.Max (int.Parse (values [2]), 0)
+                                               );
+
+                        break;
+                    default:
+                        EnqueueRequestResponseEvent (c1Control, code, values, terminating);
+
+                        break;
+                }
+
+                break;
+            default:
+                EnqueueRequestResponseEvent (c1Control, code, values, terminating);
+
+                break;
+        }
+
+        _inputReady.Set ();
+    }
+
+    private void EnqueueRequestResponseEvent (string c1Control, string code, string [] values, string terminating)
+    {
+        var eventType = EventType.RequestResponse;
+        var requestRespEv = new RequestResponseEvent { ResultTuple = (c1Control, code, values, terminating) };
+
+        _inputQueue.Enqueue (
+                             new InputResult { EventType = eventType, RequestResponseEvent = requestRespEv }
+                            );
+    }
+
+    private void HandleMouseEvent (MouseButtonState buttonState, Point pos)
+    {
+        var mouseEvent = new MouseEvent { Position = pos, ButtonState = buttonState };
+
+        _inputQueue.Enqueue (
+                             new InputResult { EventType = EventType.Mouse, MouseEvent = mouseEvent }
+                            );
+
+        _inputReady.Set ();
+    }
+
+    public enum EventType
+    {
+        Key = 1,
+        Mouse = 2,
+        WindowSize = 3,
+        WindowPosition = 4,
+        RequestResponse = 5
+    }
+
+    [Flags]
+    public enum MouseButtonState
+    {
+        Button1Pressed = 0x1,
+        Button1Released = 0x2,
+        Button1Clicked = 0x4,
+        Button1DoubleClicked = 0x8,
+        Button1TripleClicked = 0x10,
+        Button2Pressed = 0x20,
+        Button2Released = 0x40,
+        Button2Clicked = 0x80,
+        Button2DoubleClicked = 0x100,
+        Button2TripleClicked = 0x200,
+        Button3Pressed = 0x400,
+        Button3Released = 0x800,
+        Button3Clicked = 0x1000,
+        Button3DoubleClicked = 0x2000,
+        Button3TripleClicked = 0x4000,
+        ButtonWheeledUp = 0x8000,
+        ButtonWheeledDown = 0x10000,
+        ButtonWheeledLeft = 0x20000,
+        ButtonWheeledRight = 0x40000,
+        Button4Pressed = 0x80000,
+        Button4Released = 0x100000,
+        Button4Clicked = 0x200000,
+        Button4DoubleClicked = 0x400000,
+        Button4TripleClicked = 0x800000,
+        ButtonShift = 0x1000000,
+        ButtonCtrl = 0x2000000,
+        ButtonAlt = 0x4000000,
+        ReportMousePosition = 0x8000000,
+        AllEvents = -1
+    }
+
+    public struct MouseEvent
+    {
+        public Point Position;
+        public MouseButtonState ButtonState;
+    }
+
+    public struct WindowSizeEvent
+    {
+        public Size Size;
+    }
+
+    public struct WindowPositionEvent
+    {
+        public int Top;
+        public int Left;
+        public Point CursorPosition;
+    }
+
+    public struct RequestResponseEvent
+    {
+        public (string c1Control, string code, string [] values, string terminating) ResultTuple;
+    }
+
+    public struct InputResult
+    {
+        public EventType EventType;
+        public ConsoleKeyInfo ConsoleKeyInfo;
+        public MouseEvent MouseEvent;
+        public WindowSizeEvent WindowSizeEvent;
+        public WindowPositionEvent WindowPositionEvent;
+        public RequestResponseEvent RequestResponseEvent;
+
+        public readonly override string ToString ()
+        {
+            return EventType switch
+                   {
+                       EventType.Key => ToString (ConsoleKeyInfo),
+                       EventType.Mouse => MouseEvent.ToString (),
+
+                       //EventType.WindowSize => WindowSize.ToString (),
+                       //EventType.RequestResponse => RequestResponse.ToString (),
+                       _ => "Unknown event type: " + EventType
+                   };
+        }
+
+        /// <summary>Prints a ConsoleKeyInfoEx structure</summary>
+        /// <param name="cki"></param>
+        /// <returns></returns>
+        public readonly string ToString (ConsoleKeyInfo cki)
+        {
+            var ke = new Key ((KeyCode)cki.KeyChar);
+            var sb = new StringBuilder ();
+            sb.Append ($"Key: {(KeyCode)cki.Key} ({cki.Key})");
+            sb.Append ((cki.Modifiers & ConsoleModifiers.Shift) != 0 ? " | Shift" : string.Empty);
+            sb.Append ((cki.Modifiers & ConsoleModifiers.Control) != 0 ? " | Control" : string.Empty);
+            sb.Append ((cki.Modifiers & ConsoleModifiers.Alt) != 0 ? " | Alt" : string.Empty);
+            sb.Append ($", KeyChar: {ke.AsRune.MakePrintable ()} ({(uint)cki.KeyChar}) ");
+            string s = sb.ToString ().TrimEnd (',').TrimEnd (' ');
+
+            return $"[ConsoleKeyInfo({s})]";
+        }
+    }
+
+    private void HandleKeyboardEvent (ConsoleKeyInfo cki)
+    {
+        var inputResult = new InputResult { EventType = EventType.Key, ConsoleKeyInfo = cki };
+
+        _inputQueue.Enqueue (inputResult);
+    }
+
+    public void Dispose ()
+    {
+        _inputReadyCancellationTokenSource?.Cancel ();
+        _inputReadyCancellationTokenSource?.Dispose ();
+        _inputReadyCancellationTokenSource = null;
+
+        try
+        {
+            // throws away any typeahead that has been typed by
+            // the user and has not yet been read by the program.
+            while (Console.KeyAvailable)
+            {
+                Console.ReadKey (true);
+            }
+        }
+        catch (InvalidOperationException)
+        {
+            // Ignore - Console input has already been closed
+        }
+    }
 }
 
-class NetDriver : ConsoleDriver {
-	const int COLOR_BLACK = 30;
-	const int COLOR_RED = 31;
-	const int COLOR_GREEN = 32;
-	const int COLOR_YELLOW = 33;
-	const int COLOR_BLUE = 34;
-	const int COLOR_MAGENTA = 35;
-	const int COLOR_CYAN = 36;
-	const int COLOR_WHITE = 37;
-	const int COLOR_BRIGHT_BLACK = 90;
-	const int COLOR_BRIGHT_RED = 91;
-	const int COLOR_BRIGHT_GREEN = 92;
-	const int COLOR_BRIGHT_YELLOW = 93;
-	const int COLOR_BRIGHT_BLUE = 94;
-	const int COLOR_BRIGHT_MAGENTA = 95;
-	const int COLOR_BRIGHT_CYAN = 96;
-	const int COLOR_BRIGHT_WHITE = 97;
-
-	NetMainLoop _mainLoopDriver = null;
-
-	public override bool SupportsTrueColor => Environment.OSVersion.Platform == PlatformID.Unix || IsWinPlatform && Environment.OSVersion.Version.Build >= 14931;
-
-	public NetWinVTConsole NetWinConsole { get; private set; }
-
-	public bool IsWinPlatform { get; private set; }
-
-	internal override MainLoop Init ()
-	{
-		var p = Environment.OSVersion.Platform;
-		if (p == PlatformID.Win32NT || p == PlatformID.Win32S || p == PlatformID.Win32Windows) {
-			IsWinPlatform = true;
-			try {
-				NetWinConsole = new NetWinVTConsole ();
-			} catch (ApplicationException) {
-				// Likely running as a unit test, or in a non-interactive session.
-			}
-		}
-		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 ();
-			}
-		}
-
-		if (!RunningUnitTests) {
-			Console.TreatControlCAsInput = true;
-
-			Cols = Console.WindowWidth;
-			Rows = Console.WindowHeight;
-
-			//Enable alternative screen buffer.
-			Console.Out.Write (EscSeqUtils.CSI_SaveCursorAndActivateAltBufferNoBackscroll);
-
-			//Set cursor key to application.
-			Console.Out.Write (EscSeqUtils.CSI_HideCursor);
-
-		} else {
-			// We are being run in an environment that does not support a console
-			// such as a unit test, or a pipe.
-			Cols = 80;
-			Rows = 24;
-		}
-
-		ResizeScreen ();
-		ClearContents ();
-		CurrentAttribute = new Attribute (Color.White, Color.Black);
-
-		StartReportingMouseMoves ();
-
-		_mainLoopDriver = new NetMainLoop (this);
-		_mainLoopDriver.ProcessInput = ProcessInput;
-		return new MainLoop (_mainLoopDriver);
-	}
-
-	internal override void End ()
-	{
-		if (IsWinPlatform) {
-			NetWinConsole?.Cleanup ();
-		}
-
-		StopReportingMouseMoves ();
-
-		if (!RunningUnitTests) {
-			Console.ResetColor ();
-
-			//Disable alternative screen buffer.
-			Console.Out.Write (EscSeqUtils.CSI_RestoreCursorAndRestoreAltBufferWithBackscroll);
-
-			//Set cursor key to cursor.
-			Console.Out.Write (EscSeqUtils.CSI_ShowCursor);
-			Console.Out.Close ();
-		}
-	}
-
-
-	#region Size and Position Handling
-	volatile bool _winSizeChanging;
-
-	void SetWindowPosition (int col, int row)
-	{
-		if (!RunningUnitTests) {
-			Top = Console.WindowTop;
-			Left = Console.WindowLeft;
-		} else {
-			Top = row;
-			Left = col;
-		}
-	}
-
-	public virtual void ResizeScreen ()
-	{
-		// Not supported on Unix.
-		if (IsWinPlatform) {
-			// Can raise an exception while is still resizing.
-			try {
+internal class NetDriver : ConsoleDriver
+{
+    private const int COLOR_BLACK = 30;
+    private const int COLOR_BLUE = 34;
+    private const int COLOR_BRIGHT_BLACK = 90;
+    private const int COLOR_BRIGHT_BLUE = 94;
+    private const int COLOR_BRIGHT_CYAN = 96;
+    private const int COLOR_BRIGHT_GREEN = 92;
+    private const int COLOR_BRIGHT_MAGENTA = 95;
+    private const int COLOR_BRIGHT_RED = 91;
+    private const int COLOR_BRIGHT_WHITE = 97;
+    private const int COLOR_BRIGHT_YELLOW = 93;
+    private const int COLOR_CYAN = 36;
+    private const int COLOR_GREEN = 32;
+    private const int COLOR_MAGENTA = 35;
+    private const int COLOR_RED = 31;
+    private const int COLOR_WHITE = 37;
+    private const int COLOR_YELLOW = 33;
+    private NetMainLoop _mainLoopDriver;
+    public bool IsWinPlatform { get; private set; }
+    public NetWinVTConsole NetWinConsole { get; private set; }
+
+    public override bool SupportsTrueColor => Environment.OSVersion.Platform == PlatformID.Unix
+                                              || (IsWinPlatform && Environment.OSVersion.Version.Build >= 14931);
+
+    public override void Refresh ()
+    {
+        UpdateScreen ();
+        UpdateCursor ();
+    }
+
+    public override void SendKeys (char keyChar, ConsoleKey key, bool shift, bool alt, bool control)
+    {
+        var input = new InputResult
+        {
+            EventType = EventType.Key, ConsoleKeyInfo = new ConsoleKeyInfo (keyChar, key, shift, alt, control)
+        };
+
+        try
+        {
+            ProcessInput (input);
+        }
+        catch (OverflowException)
+        { }
+    }
+
+    #region Not Implemented
+
+    public override void Suspend () { throw new NotImplementedException (); }
+
+    #endregion
+
+    public override void UpdateScreen ()
+    {
+        if (RunningUnitTests
+            || _winSizeChanging
+            || Console.WindowHeight < 1
+            || Contents.Length != Rows * Cols
+            || Rows != Console.WindowHeight)
+        {
+            return;
+        }
+
+        var top = 0;
+        var left = 0;
+        int rows = Rows;
+        int cols = Cols;
+        var output = new StringBuilder ();
+        var redrawAttr = new Attribute ();
+        int lastCol = -1;
+
+        CursorVisibility? savedVisibitity = _cachedCursorVisibility;
+        SetCursorVisibility (CursorVisibility.Invisible);
+
+        for (int row = top; row < rows; row++)
+        {
+            if (Console.WindowHeight < 1)
+            {
+                return;
+            }
+
+            if (!_dirtyLines [row])
+            {
+                continue;
+            }
+
+            if (!SetCursorPosition (0, row))
+            {
+                return;
+            }
+
+            _dirtyLines [row] = false;
+            output.Clear ();
+
+            for (int col = left; col < cols; col++)
+            {
+                lastCol = -1;
+                var outputWidth = 0;
+
+                for (; col < cols; col++)
+                {
+                    if (!Contents [row, col].IsDirty)
+                    {
+                        if (output.Length > 0)
+                        {
+                            WriteToConsole (output, ref lastCol, row, ref outputWidth);
+                        }
+                        else if (lastCol == -1)
+                        {
+                            lastCol = col;
+                        }
+
+                        if (lastCol + 1 < cols)
+                        {
+                            lastCol++;
+                        }
+
+                        continue;
+                    }
+
+                    if (lastCol == -1)
+                    {
+                        lastCol = col;
+                    }
+
+                    Attribute attr = Contents [row, col].Attribute.Value;
+
+                    // Performance: Only send the escape sequence if the attribute has changed.
+                    if (attr != redrawAttr)
+                    {
+                        redrawAttr = attr;
+
+                        if (Force16Colors)
+                        {
+                            output.Append (
+                                           EscSeqUtils.CSI_SetGraphicsRendition (
+                                                                                 MapColors (
+                                                                                            (ConsoleColor)attr.Background.GetClosestNamedColor (),
+                                                                                            false
+                                                                                           ),
+                                                                                 MapColors ((ConsoleColor)attr.Foreground.GetClosestNamedColor ())
+                                                                                )
+                                          );
+                        }
+                        else
+                        {
+                            output.Append (
+                                           EscSeqUtils.CSI_SetForegroundColorRGB (
+                                                                                  attr.Foreground.R,
+                                                                                  attr.Foreground.G,
+                                                                                  attr.Foreground.B
+                                                                                 )
+                                          );
+
+                            output.Append (
+                                           EscSeqUtils.CSI_SetBackgroundColorRGB (
+                                                                                  attr.Background.R,
+                                                                                  attr.Background.G,
+                                                                                  attr.Background.B
+                                                                                 )
+                                          );
+                        }
+                    }
+
+                    outputWidth++;
+                    Rune rune = Contents [row, col].Rune;
+                    output.Append (rune);
+
+                    if (Contents [row, col].CombiningMarks.Count > 0)
+                    {
+                        // AtlasEngine does not support NON-NORMALIZED combining marks in a way
+                        // compatible with the driver architecture. Any CMs (except in the first col)
+                        // are correctly combined with the base char, but are ALSO treated as 1 column
+                        // width codepoints E.g. `echo "[e`u{0301}`u{0301}]"` will output `[é  ]`.
+                        // 
+                        // For now, we just ignore the list of CMs.
+                        //foreach (var combMark in Contents [row, col].CombiningMarks) {
+                        //	output.Append (combMark);
+                        //}
+                        // WriteToConsole (output, ref lastCol, row, ref outputWidth);
+                    }
+                    else if (rune.IsSurrogatePair () && rune.GetColumns () < 2)
+                    {
+                        WriteToConsole (output, ref lastCol, row, ref outputWidth);
+                        SetCursorPosition (col - 1, row);
+                    }
+
+                    Contents [row, col].IsDirty = false;
+                }
+            }
+
+            if (output.Length > 0)
+            {
+                SetCursorPosition (lastCol, row);
+                Console.Write (output);
+            }
+        }
+
+        SetCursorPosition (0, 0);
+
+        _cachedCursorVisibility = savedVisibitity;
+
+        void WriteToConsole (StringBuilder output, ref int lastCol, int row, ref int outputWidth)
+        {
+            SetCursorPosition (lastCol, row);
+            Console.Write (output);
+            output.Clear ();
+            lastCol += outputWidth;
+            outputWidth = 0;
+        }
+    }
+
+    internal override void End ()
+    {
+        if (IsWinPlatform)
+        {
+            NetWinConsole?.Cleanup ();
+        }
+
+        StopReportingMouseMoves ();
+
+        if (!RunningUnitTests)
+        {
+            Console.ResetColor ();
+
+            //Disable alternative screen buffer.
+            Console.Out.Write (EscSeqUtils.CSI_RestoreCursorAndRestoreAltBufferWithBackscroll);
+
+            //Set cursor key to cursor.
+            Console.Out.Write (EscSeqUtils.CSI_ShowCursor);
+            Console.Out.Close ();
+        }
+    }
+
+    internal override MainLoop Init ()
+    {
+        PlatformID p = Environment.OSVersion.Platform;
+
+        if (p == PlatformID.Win32NT || p == PlatformID.Win32S || p == PlatformID.Win32Windows)
+        {
+            IsWinPlatform = true;
+
+            try
+            {
+                NetWinConsole = new NetWinVTConsole ();
+            }
+            catch (ApplicationException)
+            {
+                // Likely running as a unit test, or in a non-interactive session.
+            }
+        }
+
+        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 ();
+            }
+        }
+
+        if (!RunningUnitTests)
+        {
+            Console.TreatControlCAsInput = true;
+
+            Cols = Console.WindowWidth;
+            Rows = Console.WindowHeight;
+
+            //Enable alternative screen buffer.
+            Console.Out.Write (EscSeqUtils.CSI_SaveCursorAndActivateAltBufferNoBackscroll);
+
+            //Set cursor key to application.
+            Console.Out.Write (EscSeqUtils.CSI_HideCursor);
+        }
+        else
+        {
+            // We are being run in an environment that does not support a console
+            // such as a unit test, or a pipe.
+            Cols = 80;
+            Rows = 24;
+        }
+
+        ResizeScreen ();
+        ClearContents ();
+        CurrentAttribute = new Attribute (Color.White, Color.Black);
+
+        StartReportingMouseMoves ();
+
+        _mainLoopDriver = new NetMainLoop (this);
+        _mainLoopDriver.ProcessInput = ProcessInput;
+
+        return new MainLoop (_mainLoopDriver);
+    }
+
+    private void ProcessInput (InputResult inputEvent)
+    {
+        switch (inputEvent.EventType)
+        {
+            case EventType.Key:
+                ConsoleKeyInfo consoleKeyInfo = inputEvent.ConsoleKeyInfo;
+
+                //if (consoleKeyInfo.Key == ConsoleKey.Packet) {
+                //	consoleKeyInfo = FromVKPacketToKConsoleKeyInfo (consoleKeyInfo);
+                //}
+
+                //Debug.WriteLine ($"event: {inputEvent}");
+
+                KeyCode map = MapKey (consoleKeyInfo);
+
+                if (map == KeyCode.Null)
+                {
+                    break;
+                }
+
+                OnKeyDown (new Key (map));
+                OnKeyUp (new Key (map));
+
+                break;
+            case EventType.Mouse:
+                OnMouseEvent (new MouseEventEventArgs (ToDriverMouse (inputEvent.MouseEvent)));
+
+                break;
+            case EventType.WindowSize:
+                _winSizeChanging = true;
+                Top = 0;
+                Left = 0;
+                Cols = inputEvent.WindowSizeEvent.Size.Width;
+                Rows = Math.Max (inputEvent.WindowSizeEvent.Size.Height, 0);
+                ;
+                ResizeScreen ();
+                ClearContents ();
+                _winSizeChanging = false;
+                OnSizeChanged (new SizeChangedEventArgs (new (Cols, Rows)));
+
+                break;
+            case EventType.RequestResponse:
+                break;
+            case EventType.WindowPosition:
+                break;
+            default:
+                throw new ArgumentOutOfRangeException ();
+        }
+    }
+
+    #region Size and Position Handling
+
+    private volatile bool _winSizeChanging;
+
+    private void SetWindowPosition (int col, int row)
+    {
+        if (!RunningUnitTests)
+        {
+            Top = Console.WindowTop;
+            Left = Console.WindowLeft;
+        }
+        else
+        {
+            Top = row;
+            Left = col;
+        }
+    }
+
+    public virtual void ResizeScreen ()
+    {
+        // Not supported on Unix.
+        if (IsWinPlatform)
+        {
+            // Can raise an exception while is still resizing.
+            try
+            {
 #pragma warning disable CA1416
-				if (Console.WindowHeight > 0) {
-					Console.CursorTop = 0;
-					Console.CursorLeft = 0;
-					Console.WindowTop = 0;
-					Console.WindowLeft = 0;
-					if (Console.WindowHeight > Rows) {
-						Console.SetWindowSize (Cols, Rows);
-					}
-					Console.SetBufferSize (Cols, Rows);
-				}
+                if (Console.WindowHeight > 0)
+                {
+                    Console.CursorTop = 0;
+                    Console.CursorLeft = 0;
+                    Console.WindowTop = 0;
+                    Console.WindowLeft = 0;
+
+                    if (Console.WindowHeight > Rows)
+                    {
+                        Console.SetWindowSize (Cols, Rows);
+                    }
+
+                    Console.SetBufferSize (Cols, Rows);
+                }
 #pragma warning restore CA1416
-			} catch (IOException) {
-				Clip = new Rect (0, 0, Cols, Rows);
-			} catch (ArgumentOutOfRangeException) {
-				Clip = new Rect (0, 0, Cols, Rows);
-			}
-		} else {
-			Console.Out.Write (EscSeqUtils.CSI_SetTerminalWindowSize (Rows, Cols));
-		}
-
-
-		Clip = new Rect (0, 0, Cols, Rows);
-	}
-	#endregion
-
-	public override void Refresh ()
-	{
-		UpdateScreen ();
-		UpdateCursor ();
-	}
-
-	public override void UpdateScreen ()
-	{
-		if (RunningUnitTests || _winSizeChanging || Console.WindowHeight < 1 || Contents.Length != Rows * Cols || Rows != Console.WindowHeight) {
-			return;
-		}
-
-		int top = 0;
-		int left = 0;
-		int rows = Rows;
-		int cols = Cols;
-		var output = new StringBuilder ();
-		var redrawAttr = new Attribute ();
-		int lastCol = -1;
-
-		var savedVisibitity = _cachedCursorVisibility;
-		SetCursorVisibility (CursorVisibility.Invisible);
-
-		for (int row = top; row < rows; row++) {
-			if (Console.WindowHeight < 1) {
-				return;
-			}
-			if (!_dirtyLines [row]) {
-				continue;
-			}
-			if (!SetCursorPosition (0, row)) {
-				return;
-			}
-			_dirtyLines [row] = false;
-			output.Clear ();
-			for (int col = left; col < cols; col++) {
-				lastCol = -1;
-				int outputWidth = 0;
-				for (; col < cols; col++) {
-					if (!Contents [row, col].IsDirty) {
-						if (output.Length > 0) {
-							WriteToConsole (output, ref lastCol, row, ref outputWidth);
-						} else if (lastCol == -1) {
-							lastCol = col;
-						}
-						if (lastCol + 1 < cols)
-							lastCol++;
-						continue;
-					}
-
-					if (lastCol == -1) {
-						lastCol = col;
-					}
-
-					var attr = Contents [row, col].Attribute.Value;
-					// Performance: Only send the escape sequence if the attribute has changed.
-					if (attr != redrawAttr) {
-						redrawAttr = attr;
-
-						if (Force16Colors) {
-							output.Append (EscSeqUtils.CSI_SetGraphicsRendition (
-								MapColors ((ConsoleColor)attr.Background.ColorName, false), MapColors ((ConsoleColor)attr.Foreground.ColorName, true)));
-						} else {
-							output.Append (EscSeqUtils.CSI_SetForegroundColorRGB (attr.Foreground.R, attr.Foreground.G, attr.Foreground.B));
-							output.Append (EscSeqUtils.CSI_SetBackgroundColorRGB (attr.Background.R, attr.Background.G, attr.Background.B));
-						}
-
-					}
-					outputWidth++;
-					var rune = (Rune)Contents [row, col].Rune;
-					output.Append (rune);
-					if (Contents [row, col].CombiningMarks.Count > 0) {
-						// AtlasEngine does not support NON-NORMALIZED combining marks in a way
-						// compatible with the driver architecture. Any CMs (except in the first col)
-						// are correctly combined with the base char, but are ALSO treated as 1 column
-						// width codepoints E.g. `echo "[e`u{0301}`u{0301}]"` will output `[é  ]`.
-						// 
-						// For now, we just ignore the list of CMs.
-						//foreach (var combMark in Contents [row, col].CombiningMarks) {
-						//	output.Append (combMark);
-						//}
-						// WriteToConsole (output, ref lastCol, row, ref outputWidth);
-					} else if (rune.IsSurrogatePair () && rune.GetColumns () < 2) {
-						WriteToConsole (output, ref lastCol, row, ref outputWidth);
-						SetCursorPosition (col - 1, row);
-					}
-					Contents [row, col].IsDirty = false;
-				}
-			}
-			if (output.Length > 0) {
-				SetCursorPosition (lastCol, row);
-				Console.Write (output);
-			}
-		}
-		SetCursorPosition (0, 0);
-
-		_cachedCursorVisibility = savedVisibitity;
-
-		void WriteToConsole (StringBuilder output, ref int lastCol, int row, ref int outputWidth)
-		{
-			SetCursorPosition (lastCol, row);
-			Console.Write (output);
-			output.Clear ();
-			lastCol += outputWidth;
-			outputWidth = 0;
-		}
-	}
-
-	#region Color Handling
-	// Cache the list of ConsoleColor values.
-	static readonly HashSet<int> ConsoleColorValues = new (
-		Enum.GetValues (typeof (ConsoleColor)).OfType<ConsoleColor> ().Select (c => (int)c)
-	);
-
-	// Dictionary for mapping ConsoleColor values to the values used by System.Net.Console.
-	static Dictionary<ConsoleColor, int> colorMap = new () {
-		{ ConsoleColor.Black, COLOR_BLACK },
-		{ ConsoleColor.DarkBlue, COLOR_BLUE },
-		{ ConsoleColor.DarkGreen, COLOR_GREEN },
-		{ ConsoleColor.DarkCyan, COLOR_CYAN },
-		{ ConsoleColor.DarkRed, COLOR_RED },
-		{ ConsoleColor.DarkMagenta, COLOR_MAGENTA },
-		{ ConsoleColor.DarkYellow, COLOR_YELLOW },
-		{ ConsoleColor.Gray, COLOR_WHITE },
-		{ ConsoleColor.DarkGray, COLOR_BRIGHT_BLACK },
-		{ ConsoleColor.Blue, COLOR_BRIGHT_BLUE },
-		{ ConsoleColor.Green, COLOR_BRIGHT_GREEN },
-		{ ConsoleColor.Cyan, COLOR_BRIGHT_CYAN },
-		{ ConsoleColor.Red, COLOR_BRIGHT_RED },
-		{ ConsoleColor.Magenta, COLOR_BRIGHT_MAGENTA },
-		{ ConsoleColor.Yellow, COLOR_BRIGHT_YELLOW },
-		{ ConsoleColor.White, COLOR_BRIGHT_WHITE }
-	};
-
-	// Map a ConsoleColor to a platform dependent value.
-	int MapColors (ConsoleColor color, bool isForeground = true) => colorMap.TryGetValue (color, out int colorValue) ? colorValue + (isForeground ? 0 : 10) : 0;
-
-	///// <remarks>
-	///// In the NetDriver, colors are encoded as an int. 
-	///// However, the foreground color is stored in the most significant 16 bits, 
-	///// and the background color is stored in the least significant 16 bits.
-	///// </remarks>
-	//public override Attribute MakeColor (Color foreground, Color background)
-	//{
-	//	// Encode the colors into the int value.
-	//	return new Attribute (
-	//		platformColor: ((((int)foreground.ColorName) & 0xffff) << 16) | (((int)background.ColorName) & 0xffff),
-	//		foreground: foreground,
-	//		background: background
-	//	);
-	//}
-	#endregion
-
-	#region Cursor Handling
-	bool SetCursorPosition (int col, int row)
-	{
-		if (IsWinPlatform) {
-			// Could happens that the windows is still resizing and the col is bigger than Console.WindowWidth.
-			try {
-				Console.SetCursorPosition (col, row);
-				return true;
-			} catch (Exception) {
-				return false;
-			}
-		} else {
-			// + 1 is needed because non-Windows is based on 1 instead of 0 and
-			// Console.CursorTop/CursorLeft isn't reliable.
-			Console.Out.Write (EscSeqUtils.CSI_SetCursorPosition (row + 1, col + 1));
-			return true;
-		}
-	}
-
-	CursorVisibility? _cachedCursorVisibility;
-
-	public override void UpdateCursor ()
-	{
-		EnsureCursorVisibility ();
-
-		if (Col >= 0 && Col < Cols && Row >= 0 && Row < Rows) {
-			SetCursorPosition (Col, Row);
-			SetWindowPosition (0, Row);
-		}
-	}
-
-	public override bool GetCursorVisibility (out CursorVisibility visibility)
-	{
-		visibility = _cachedCursorVisibility ?? CursorVisibility.Default;
-		return visibility == CursorVisibility.Default;
-	}
-
-	public override bool SetCursorVisibility (CursorVisibility visibility)
-	{
-		_cachedCursorVisibility = visibility;
-		bool isVisible = RunningUnitTests ? visibility == CursorVisibility.Default : Console.CursorVisible = visibility == CursorVisibility.Default;
-		Console.Out.Write (isVisible ? EscSeqUtils.CSI_ShowCursor : EscSeqUtils.CSI_HideCursor);
-		return isVisible;
-	}
-
-	public override bool EnsureCursorVisibility ()
-	{
-		if (!(Col >= 0 && Row >= 0 && Col < Cols && Row < Rows)) {
-			GetCursorVisibility (out var cursorVisibility);
-			_cachedCursorVisibility = cursorVisibility;
-			SetCursorVisibility (CursorVisibility.Invisible);
-			return false;
-		}
-
-		SetCursorVisibility (_cachedCursorVisibility ?? CursorVisibility.Default);
-		return _cachedCursorVisibility == CursorVisibility.Default;
-	}
-	#endregion
-
-	#region Mouse Handling
-	public void StartReportingMouseMoves ()
-	{
-		if (!RunningUnitTests) {
-			Console.Out.Write (EscSeqUtils.CSI_EnableMouseEvents);
-		}
-	}
-
-	public void StopReportingMouseMoves ()
-	{
-		if (!RunningUnitTests) {
-			Console.Out.Write (EscSeqUtils.CSI_DisableMouseEvents);
-		}
-	}
-
-	MouseEvent ToDriverMouse (NetEvents.MouseEvent me)
-	{
-		//System.Diagnostics.Debug.WriteLine ($"X: {me.Position.X}; Y: {me.Position.Y}; ButtonState: {me.ButtonState}");
-
-		MouseFlags mouseFlag = 0;
-
-		if ((me.ButtonState & MouseButtonState.Button1Pressed) != 0) {
-			mouseFlag |= MouseFlags.Button1Pressed;
-		}
-		if ((me.ButtonState & MouseButtonState.Button1Released) != 0) {
-			mouseFlag |= MouseFlags.Button1Released;
-		}
-		if ((me.ButtonState & MouseButtonState.Button1Clicked) != 0) {
-			mouseFlag |= MouseFlags.Button1Clicked;
-		}
-		if ((me.ButtonState & MouseButtonState.Button1DoubleClicked) != 0) {
-			mouseFlag |= MouseFlags.Button1DoubleClicked;
-		}
-		if ((me.ButtonState & MouseButtonState.Button1TripleClicked) != 0) {
-			mouseFlag |= MouseFlags.Button1TripleClicked;
-		}
-		if ((me.ButtonState & MouseButtonState.Button2Pressed) != 0) {
-			mouseFlag |= MouseFlags.Button2Pressed;
-		}
-		if ((me.ButtonState & MouseButtonState.Button2Released) != 0) {
-			mouseFlag |= MouseFlags.Button2Released;
-		}
-		if ((me.ButtonState & MouseButtonState.Button2Clicked) != 0) {
-			mouseFlag |= MouseFlags.Button2Clicked;
-		}
-		if ((me.ButtonState & MouseButtonState.Button2DoubleClicked) != 0) {
-			mouseFlag |= MouseFlags.Button2DoubleClicked;
-		}
-		if ((me.ButtonState & MouseButtonState.Button2TripleClicked) != 0) {
-			mouseFlag |= MouseFlags.Button2TripleClicked;
-		}
-		if ((me.ButtonState & MouseButtonState.Button3Pressed) != 0) {
-			mouseFlag |= MouseFlags.Button3Pressed;
-		}
-		if ((me.ButtonState & MouseButtonState.Button3Released) != 0) {
-			mouseFlag |= MouseFlags.Button3Released;
-		}
-		if ((me.ButtonState & MouseButtonState.Button3Clicked) != 0) {
-			mouseFlag |= MouseFlags.Button3Clicked;
-		}
-		if ((me.ButtonState & MouseButtonState.Button3DoubleClicked) != 0) {
-			mouseFlag |= MouseFlags.Button3DoubleClicked;
-		}
-		if ((me.ButtonState & MouseButtonState.Button3TripleClicked) != 0) {
-			mouseFlag |= MouseFlags.Button3TripleClicked;
-		}
-		if ((me.ButtonState & MouseButtonState.ButtonWheeledUp) != 0) {
-			mouseFlag |= MouseFlags.WheeledUp;
-		}
-		if ((me.ButtonState & MouseButtonState.ButtonWheeledDown) != 0) {
-			mouseFlag |= MouseFlags.WheeledDown;
-		}
-		if ((me.ButtonState & MouseButtonState.ButtonWheeledLeft) != 0) {
-			mouseFlag |= MouseFlags.WheeledLeft;
-		}
-		if ((me.ButtonState & MouseButtonState.ButtonWheeledRight) != 0) {
-			mouseFlag |= MouseFlags.WheeledRight;
-		}
-		if ((me.ButtonState & MouseButtonState.Button4Pressed) != 0) {
-			mouseFlag |= MouseFlags.Button4Pressed;
-		}
-		if ((me.ButtonState & MouseButtonState.Button4Released) != 0) {
-			mouseFlag |= MouseFlags.Button4Released;
-		}
-		if ((me.ButtonState & MouseButtonState.Button4Clicked) != 0) {
-			mouseFlag |= MouseFlags.Button4Clicked;
-		}
-		if ((me.ButtonState & MouseButtonState.Button4DoubleClicked) != 0) {
-			mouseFlag |= MouseFlags.Button4DoubleClicked;
-		}
-		if ((me.ButtonState & MouseButtonState.Button4TripleClicked) != 0) {
-			mouseFlag |= MouseFlags.Button4TripleClicked;
-		}
-		if ((me.ButtonState & MouseButtonState.ReportMousePosition) != 0) {
-			mouseFlag |= MouseFlags.ReportMousePosition;
-		}
-		if ((me.ButtonState & MouseButtonState.ButtonShift) != 0) {
-			mouseFlag |= MouseFlags.ButtonShift;
-		}
-		if ((me.ButtonState & MouseButtonState.ButtonCtrl) != 0) {
-			mouseFlag |= MouseFlags.ButtonCtrl;
-		}
-		if ((me.ButtonState & MouseButtonState.ButtonAlt) != 0) {
-			mouseFlag |= MouseFlags.ButtonAlt;
-		}
-
-		return new MouseEvent () {
-			X = me.Position.X,
-			Y = me.Position.Y,
-			Flags = mouseFlag
-		};
-	}
-	#endregion Mouse Handling
-
-	#region Keyboard Handling
-	ConsoleKeyInfo FromVKPacketToKConsoleKeyInfo (ConsoleKeyInfo consoleKeyInfo)
-	{
-		if (consoleKeyInfo.Key != ConsoleKey.Packet) {
-			return consoleKeyInfo;
-		}
-
-		var mod = consoleKeyInfo.Modifiers;
-		bool shift = (mod & ConsoleModifiers.Shift) != 0;
-		bool alt = (mod & ConsoleModifiers.Alt) != 0;
-		bool control = (mod & ConsoleModifiers.Control) != 0;
-
-		var cKeyInfo = DecodeVKPacketToKConsoleKeyInfo (consoleKeyInfo);
-
-		return new ConsoleKeyInfo (cKeyInfo.KeyChar, cKeyInfo.Key, shift, alt, control);
-	}
-
-	KeyCode MapKey (ConsoleKeyInfo keyInfo)
-	{
-		switch (keyInfo.Key) {
-		case ConsoleKey.OemPeriod:
-		case ConsoleKey.OemComma:
-		case ConsoleKey.OemPlus:
-		case ConsoleKey.OemMinus:
-		case ConsoleKey.Packet:
-		case ConsoleKey.Oem1:
-		case ConsoleKey.Oem2:
-		case ConsoleKey.Oem3:
-		case ConsoleKey.Oem4:
-		case ConsoleKey.Oem5:
-		case ConsoleKey.Oem6:
-		case ConsoleKey.Oem7:
-		case ConsoleKey.Oem8:
-		case ConsoleKey.Oem102:
-			if (keyInfo.KeyChar == 0) {
-				// If the keyChar is 0, keyInfo.Key value is not a printable character. 
-
-				return KeyCode.Null;// MapToKeyCodeModifiers (keyInfo.Modifiers, KeyCode)keyInfo.Key);
-			} else {
-				if (keyInfo.Modifiers != ConsoleModifiers.Shift) {
-					// If Shift wasn't down we don't need to do anything but return the keyInfo.KeyChar
-					return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)(keyInfo.KeyChar));
-				}
-
-				// Strip off Shift - We got here because they KeyChar from Windows is the shifted char (e.g. "Ç")
-				// and passing on Shift would be redundant.
-				return MapToKeyCodeModifiers (keyInfo.Modifiers & ~ConsoleModifiers.Shift, (KeyCode)keyInfo.KeyChar);
-			}
-		}
-
-		var key = keyInfo.Key;
-		// A..Z are special cased:
-		// - Alone, they represent lowercase a...z
-		// - With ShiftMask they are A..Z
-		// - If CapsLock is on the above is reversed.
-		// - If Alt and/or Ctrl are present, treat as upper case
-		if (keyInfo.Key is >= ConsoleKey.A and <= ConsoleKey.Z) {
-			if (keyInfo.Modifiers.HasFlag (ConsoleModifiers.Alt) || keyInfo.Modifiers.HasFlag (ConsoleModifiers.Control)) {
-				return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)(uint)keyInfo.Key);
-			}
-
-			if (keyInfo.Modifiers == ConsoleModifiers.Shift) {
-				// If ShiftMask is on  add the ShiftMask
-				if (char.IsUpper (keyInfo.KeyChar)) {
-					return (KeyCode)((uint)keyInfo.Key) | KeyCode.ShiftMask;
-				}
-			}
-			return (KeyCode)(uint)keyInfo.KeyChar;
-		}
-
-		// Handle control keys whose VK codes match the related ASCII value (those below ASCII 33) like ESC
-		if (keyInfo.Key != ConsoleKey.None && Enum.IsDefined (typeof (KeyCode), (uint)keyInfo.Key)) {
-			return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)(keyInfo.Key));
-		}
-
-		// Handle control keys (e.g. CursorUp)
-		if (keyInfo.Key != ConsoleKey.None && Enum.IsDefined (typeof (KeyCode), ((uint)keyInfo.Key + (uint)KeyCode.MaxCodePoint))) {
-			return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)((uint)keyInfo.Key + (uint)KeyCode.MaxCodePoint));
-		}
-
-
-		return (KeyCode)(uint)keyInfo.KeyChar;
-	}
-	#endregion Keyboard Handling
-
-	void ProcessInput (InputResult inputEvent)
-	{
-		switch (inputEvent.EventType) {
-		case NetEvents.EventType.Key:
-			var consoleKeyInfo = inputEvent.ConsoleKeyInfo;
-			//if (consoleKeyInfo.Key == ConsoleKey.Packet) {
-			//	consoleKeyInfo = FromVKPacketToKConsoleKeyInfo (consoleKeyInfo);
-			//}
-
-			//Debug.WriteLine ($"event: {inputEvent}");
-
-			var map = MapKey (consoleKeyInfo);
-
-			if (map == KeyCode.Null) {
-				break;
-			}
-
-			OnKeyDown (new Key (map));
-			OnKeyUp (new Key (map));
-			break;
-		case NetEvents.EventType.Mouse:
-			OnMouseEvent (new MouseEventEventArgs (ToDriverMouse (inputEvent.MouseEvent)));
-			break;
-		case NetEvents.EventType.WindowSize:
-			_winSizeChanging = true;
-			Top = 0;
-			Left = 0;
-			Cols = inputEvent.WindowSizeEvent.Size.Width;
-			Rows = Math.Max (inputEvent.WindowSizeEvent.Size.Height, 0);
-			;
-			ResizeScreen ();
-			ClearContents ();
-			_winSizeChanging = false;
-			OnSizeChanged (new SizeChangedEventArgs (new Size (Cols, Rows)));
-			break;
-		case NetEvents.EventType.RequestResponse:
-			break;
-		case NetEvents.EventType.WindowPosition:
-			break;
-		default:
-			throw new ArgumentOutOfRangeException ();
-		}
-	}
-
-	public override void SendKeys (char keyChar, ConsoleKey key, bool shift, bool alt, bool control)
-	{
-		var input = new InputResult {
-			EventType = NetEvents.EventType.Key,
-			ConsoleKeyInfo = new ConsoleKeyInfo (keyChar, key, shift, alt, control)
-		};
-
-		try {
-			ProcessInput (input);
-		} catch (OverflowException) { }
-	}
-
-
-	#region Not Implemented
-	public override void Suspend () => throw new NotImplementedException ();
-	#endregion
+            }
+            // INTENT: Why are these eating the exceptions?
+            // Comments would be good here.
+            catch (IOException)
+            {
+                // CONCURRENCY: Unsynchronized access to Clip is not safe.
+                Clip = new (0, 0, Cols, Rows);
+            }
+            catch (ArgumentOutOfRangeException)
+            {
+                // CONCURRENCY: Unsynchronized access to Clip is not safe.
+                Clip = new (0, 0, Cols, Rows);
+            }
+        }
+        else
+        {
+            Console.Out.Write (EscSeqUtils.CSI_SetTerminalWindowSize (Rows, Cols));
+        }
+
+        // CONCURRENCY: Unsynchronized access to Clip is not safe.
+        Clip = new (0, 0, Cols, Rows);
+    }
+
+    #endregion
+
+    #region Color Handling
+
+    // Cache the list of ConsoleColor values.
+    private static readonly HashSet<int> ConsoleColorValues = new (
+                                                                   Enum.GetValues (typeof (ConsoleColor))
+                                                                       .OfType<ConsoleColor> ()
+                                                                       .Select (c => (int)c)
+                                                                  );
+
+    // Dictionary for mapping ConsoleColor values to the values used by System.Net.Console.
+    private static readonly Dictionary<ConsoleColor, int> colorMap = new ()
+    {
+        { ConsoleColor.Black, COLOR_BLACK },
+        { ConsoleColor.DarkBlue, COLOR_BLUE },
+        { ConsoleColor.DarkGreen, COLOR_GREEN },
+        { ConsoleColor.DarkCyan, COLOR_CYAN },
+        { ConsoleColor.DarkRed, COLOR_RED },
+        { ConsoleColor.DarkMagenta, COLOR_MAGENTA },
+        { ConsoleColor.DarkYellow, COLOR_YELLOW },
+        { ConsoleColor.Gray, COLOR_WHITE },
+        { ConsoleColor.DarkGray, COLOR_BRIGHT_BLACK },
+        { ConsoleColor.Blue, COLOR_BRIGHT_BLUE },
+        { ConsoleColor.Green, COLOR_BRIGHT_GREEN },
+        { ConsoleColor.Cyan, COLOR_BRIGHT_CYAN },
+        { ConsoleColor.Red, COLOR_BRIGHT_RED },
+        { ConsoleColor.Magenta, COLOR_BRIGHT_MAGENTA },
+        { ConsoleColor.Yellow, COLOR_BRIGHT_YELLOW },
+        { ConsoleColor.White, COLOR_BRIGHT_WHITE }
+    };
+
+    // Map a ConsoleColor to a platform dependent value.
+    private int MapColors (ConsoleColor color, bool isForeground = true)
+    {
+        return colorMap.TryGetValue (color, out int colorValue) ? colorValue + (isForeground ? 0 : 10) : 0;
+    }
+
+    ///// <remarks>
+    ///// In the NetDriver, colors are encoded as an int. 
+    ///// However, the foreground color is stored in the most significant 16 bits, 
+    ///// and the background color is stored in the least significant 16 bits.
+    ///// </remarks>
+    //public override Attribute MakeColor (Color foreground, Color background)
+    //{
+    //	// Encode the colors into the int value.
+    //	return new Attribute (
+    //		platformColor: ((((int)foreground.ColorName) & 0xffff) << 16) | (((int)background.ColorName) & 0xffff),
+    //		foreground: foreground,
+    //		background: background
+    //	);
+    //}
+
+    #endregion
+
+    #region Cursor Handling
+
+    private bool SetCursorPosition (int col, int row)
+    {
+        if (IsWinPlatform)
+        {
+            // Could happens that the windows is still resizing and the col is bigger than Console.WindowWidth.
+            try
+            {
+                Console.SetCursorPosition (col, row);
+
+                return true;
+            }
+            catch (Exception)
+            {
+                return false;
+            }
+        }
+
+        // + 1 is needed because non-Windows is based on 1 instead of 0 and
+        // Console.CursorTop/CursorLeft isn't reliable.
+        Console.Out.Write (EscSeqUtils.CSI_SetCursorPosition (row + 1, col + 1));
+
+        return true;
+    }
+
+    private CursorVisibility? _cachedCursorVisibility;
+
+    public override void UpdateCursor ()
+    {
+        EnsureCursorVisibility ();
+
+        if (Col >= 0 && Col < Cols && Row >= 0 && Row < Rows)
+        {
+            SetCursorPosition (Col, Row);
+            SetWindowPosition (0, Row);
+        }
+    }
+
+    public override bool GetCursorVisibility (out CursorVisibility visibility)
+    {
+        visibility = _cachedCursorVisibility ?? CursorVisibility.Default;
+
+        return visibility == CursorVisibility.Default;
+    }
+
+    public override bool SetCursorVisibility (CursorVisibility visibility)
+    {
+        _cachedCursorVisibility = visibility;
+
+        bool isVisible = RunningUnitTests
+                             ? visibility == CursorVisibility.Default
+                             : Console.CursorVisible = visibility == CursorVisibility.Default;
+        Console.Out.Write (isVisible ? EscSeqUtils.CSI_ShowCursor : EscSeqUtils.CSI_HideCursor);
+
+        return isVisible;
+    }
+
+    public override bool EnsureCursorVisibility ()
+    {
+        if (!(Col >= 0 && Row >= 0 && Col < Cols && Row < Rows))
+        {
+            GetCursorVisibility (out CursorVisibility cursorVisibility);
+            _cachedCursorVisibility = cursorVisibility;
+            SetCursorVisibility (CursorVisibility.Invisible);
+
+            return false;
+        }
+
+        SetCursorVisibility (_cachedCursorVisibility ?? CursorVisibility.Default);
+
+        return _cachedCursorVisibility == CursorVisibility.Default;
+    }
+
+    #endregion
+
+    #region Mouse Handling
+
+    public void StartReportingMouseMoves ()
+    {
+        if (!RunningUnitTests)
+        {
+            Console.Out.Write (EscSeqUtils.CSI_EnableMouseEvents);
+        }
+    }
+
+    public void StopReportingMouseMoves ()
+    {
+        if (!RunningUnitTests)
+        {
+            Console.Out.Write (EscSeqUtils.CSI_DisableMouseEvents);
+        }
+    }
+
+    private MouseEvent ToDriverMouse (NetEvents.MouseEvent me)
+    {
+        //System.Diagnostics.Debug.WriteLine ($"X: {me.Position.X}; Y: {me.Position.Y}; ButtonState: {me.ButtonState}");
+
+        MouseFlags mouseFlag = 0;
+
+        if ((me.ButtonState & MouseButtonState.Button1Pressed) != 0)
+        {
+            mouseFlag |= MouseFlags.Button1Pressed;
+        }
+
+        if ((me.ButtonState & MouseButtonState.Button1Released) != 0)
+        {
+            mouseFlag |= MouseFlags.Button1Released;
+        }
+
+        if ((me.ButtonState & MouseButtonState.Button1Clicked) != 0)
+        {
+            mouseFlag |= MouseFlags.Button1Clicked;
+        }
+
+        if ((me.ButtonState & MouseButtonState.Button1DoubleClicked) != 0)
+        {
+            mouseFlag |= MouseFlags.Button1DoubleClicked;
+        }
+
+        if ((me.ButtonState & MouseButtonState.Button1TripleClicked) != 0)
+        {
+            mouseFlag |= MouseFlags.Button1TripleClicked;
+        }
+
+        if ((me.ButtonState & MouseButtonState.Button2Pressed) != 0)
+        {
+            mouseFlag |= MouseFlags.Button2Pressed;
+        }
+
+        if ((me.ButtonState & MouseButtonState.Button2Released) != 0)
+        {
+            mouseFlag |= MouseFlags.Button2Released;
+        }
+
+        if ((me.ButtonState & MouseButtonState.Button2Clicked) != 0)
+        {
+            mouseFlag |= MouseFlags.Button2Clicked;
+        }
+
+        if ((me.ButtonState & MouseButtonState.Button2DoubleClicked) != 0)
+        {
+            mouseFlag |= MouseFlags.Button2DoubleClicked;
+        }
+
+        if ((me.ButtonState & MouseButtonState.Button2TripleClicked) != 0)
+        {
+            mouseFlag |= MouseFlags.Button2TripleClicked;
+        }
+
+        if ((me.ButtonState & MouseButtonState.Button3Pressed) != 0)
+        {
+            mouseFlag |= MouseFlags.Button3Pressed;
+        }
+
+        if ((me.ButtonState & MouseButtonState.Button3Released) != 0)
+        {
+            mouseFlag |= MouseFlags.Button3Released;
+        }
+
+        if ((me.ButtonState & MouseButtonState.Button3Clicked) != 0)
+        {
+            mouseFlag |= MouseFlags.Button3Clicked;
+        }
+
+        if ((me.ButtonState & MouseButtonState.Button3DoubleClicked) != 0)
+        {
+            mouseFlag |= MouseFlags.Button3DoubleClicked;
+        }
+
+        if ((me.ButtonState & MouseButtonState.Button3TripleClicked) != 0)
+        {
+            mouseFlag |= MouseFlags.Button3TripleClicked;
+        }
+
+        if ((me.ButtonState & MouseButtonState.ButtonWheeledUp) != 0)
+        {
+            mouseFlag |= MouseFlags.WheeledUp;
+        }
+
+        if ((me.ButtonState & MouseButtonState.ButtonWheeledDown) != 0)
+        {
+            mouseFlag |= MouseFlags.WheeledDown;
+        }
+
+        if ((me.ButtonState & MouseButtonState.ButtonWheeledLeft) != 0)
+        {
+            mouseFlag |= MouseFlags.WheeledLeft;
+        }
+
+        if ((me.ButtonState & MouseButtonState.ButtonWheeledRight) != 0)
+        {
+            mouseFlag |= MouseFlags.WheeledRight;
+        }
+
+        if ((me.ButtonState & MouseButtonState.Button4Pressed) != 0)
+        {
+            mouseFlag |= MouseFlags.Button4Pressed;
+        }
+
+        if ((me.ButtonState & MouseButtonState.Button4Released) != 0)
+        {
+            mouseFlag |= MouseFlags.Button4Released;
+        }
+
+        if ((me.ButtonState & MouseButtonState.Button4Clicked) != 0)
+        {
+            mouseFlag |= MouseFlags.Button4Clicked;
+        }
+
+        if ((me.ButtonState & MouseButtonState.Button4DoubleClicked) != 0)
+        {
+            mouseFlag |= MouseFlags.Button4DoubleClicked;
+        }
+
+        if ((me.ButtonState & MouseButtonState.Button4TripleClicked) != 0)
+        {
+            mouseFlag |= MouseFlags.Button4TripleClicked;
+        }
+
+        if ((me.ButtonState & MouseButtonState.ReportMousePosition) != 0)
+        {
+            mouseFlag |= MouseFlags.ReportMousePosition;
+        }
+
+        if ((me.ButtonState & MouseButtonState.ButtonShift) != 0)
+        {
+            mouseFlag |= MouseFlags.ButtonShift;
+        }
+
+        if ((me.ButtonState & MouseButtonState.ButtonCtrl) != 0)
+        {
+            mouseFlag |= MouseFlags.ButtonCtrl;
+        }
+
+        if ((me.ButtonState & MouseButtonState.ButtonAlt) != 0)
+        {
+            mouseFlag |= MouseFlags.ButtonAlt;
+        }
+
+        return new MouseEvent { X = me.Position.X, Y = me.Position.Y, Flags = mouseFlag };
+    }
+
+    #endregion Mouse Handling
+
+    #region Keyboard Handling
+
+    private ConsoleKeyInfo FromVKPacketToKConsoleKeyInfo (ConsoleKeyInfo consoleKeyInfo)
+    {
+        if (consoleKeyInfo.Key != ConsoleKey.Packet)
+        {
+            return consoleKeyInfo;
+        }
+
+        ConsoleModifiers mod = consoleKeyInfo.Modifiers;
+        bool shift = (mod & ConsoleModifiers.Shift) != 0;
+        bool alt = (mod & ConsoleModifiers.Alt) != 0;
+        bool control = (mod & ConsoleModifiers.Control) != 0;
+
+        ConsoleKeyInfo cKeyInfo = DecodeVKPacketToKConsoleKeyInfo (consoleKeyInfo);
+
+        return new ConsoleKeyInfo (cKeyInfo.KeyChar, cKeyInfo.Key, shift, alt, control);
+    }
+
+    private KeyCode MapKey (ConsoleKeyInfo keyInfo)
+    {
+        switch (keyInfo.Key)
+        {
+            case ConsoleKey.OemPeriod:
+            case ConsoleKey.OemComma:
+            case ConsoleKey.OemPlus:
+            case ConsoleKey.OemMinus:
+            case ConsoleKey.Packet:
+            case ConsoleKey.Oem1:
+            case ConsoleKey.Oem2:
+            case ConsoleKey.Oem3:
+            case ConsoleKey.Oem4:
+            case ConsoleKey.Oem5:
+            case ConsoleKey.Oem6:
+            case ConsoleKey.Oem7:
+            case ConsoleKey.Oem8:
+            case ConsoleKey.Oem102:
+                if (keyInfo.KeyChar == 0)
+                {
+                    // If the keyChar is 0, keyInfo.Key value is not a printable character. 
+
+                    return KeyCode.Null; // MapToKeyCodeModifiers (keyInfo.Modifiers, KeyCode)keyInfo.Key);
+                }
+
+                if (keyInfo.Modifiers != ConsoleModifiers.Shift)
+                {
+                    // If Shift wasn't down we don't need to do anything but return the keyInfo.KeyChar
+                    return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)keyInfo.KeyChar);
+                }
+
+                // Strip off Shift - We got here because they KeyChar from Windows is the shifted char (e.g. "Ç")
+                // and passing on Shift would be redundant.
+                return MapToKeyCodeModifiers (keyInfo.Modifiers & ~ConsoleModifiers.Shift, (KeyCode)keyInfo.KeyChar);
+        }
+
+        ConsoleKey key = keyInfo.Key;
+
+        // A..Z are special cased:
+        // - Alone, they represent lowercase a...z
+        // - With ShiftMask they are A..Z
+        // - If CapsLock is on the above is reversed.
+        // - If Alt and/or Ctrl are present, treat as upper case
+        if (keyInfo.Key is >= ConsoleKey.A and <= ConsoleKey.Z)
+        {
+            if (keyInfo.Modifiers.HasFlag (ConsoleModifiers.Alt)
+                || keyInfo.Modifiers.HasFlag (ConsoleModifiers.Control))
+            {
+                return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)(uint)keyInfo.Key);
+            }
+
+            if (keyInfo.Modifiers == ConsoleModifiers.Shift)
+            {
+                // If ShiftMask is on  add the ShiftMask
+                if (char.IsUpper (keyInfo.KeyChar))
+                {
+                    return (KeyCode)(uint)keyInfo.Key | KeyCode.ShiftMask;
+                }
+            }
+
+            return (KeyCode)keyInfo.KeyChar;
+        }
+
+        // Handle control keys whose VK codes match the related ASCII value (those below ASCII 33) like ESC
+        if (keyInfo.Key != ConsoleKey.None && Enum.IsDefined (typeof (KeyCode), (uint)keyInfo.Key))
+        {
+            return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)keyInfo.Key);
+        }
+
+        // Handle control keys (e.g. CursorUp)
+        if (keyInfo.Key != ConsoleKey.None
+            && Enum.IsDefined (typeof (KeyCode), (uint)keyInfo.Key + (uint)KeyCode.MaxCodePoint))
+        {
+            return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)((uint)keyInfo.Key + (uint)KeyCode.MaxCodePoint));
+        }
+
+        return (KeyCode)keyInfo.KeyChar;
+    }
+
+    #endregion Keyboard Handling
 }
 
 /// <summary>
-/// Mainloop intended to be used with the .NET System.Console API, and can
-/// be used on Windows and Unix, it is cross platform but lacks things like
-/// file descriptor monitoring.
+///     Mainloop intended to be used with the .NET System.Console API, and can be used on Windows and Unix, it is
+///     cross platform but lacks things like file descriptor monitoring.
 /// </summary>
-/// <remarks>
-/// This implementation is used for NetDriver.
-/// </remarks>
-class NetMainLoop : IMainLoopDriver {
-	readonly ManualResetEventSlim _eventReady = new (false);
-	readonly ManualResetEventSlim _waitForProbe = new (false);
-	readonly Queue<InputResult?> _resultQueue = new ();
-	MainLoop _mainLoop;
-	CancellationTokenSource _eventReadyTokenSource = new ();
-	readonly CancellationTokenSource _inputHandlerTokenSource = new ();
-	internal NetEvents _netEvents;
-
-	/// <summary>
-	/// Invoked when a Key is pressed.
-	/// </summary>
-	internal Action<InputResult> ProcessInput;
-
-	/// <summary>
-	/// Initializes the class with the console driver.
-	/// </summary>
-	/// <remarks>
-	///   Passing a consoleDriver is provided to capture windows resizing.
-	/// </remarks>
-	/// <param name="consoleDriver">The console driver used by this Net main loop.</param>
-	/// <exception cref="ArgumentNullException"></exception>
-	public NetMainLoop (ConsoleDriver consoleDriver = null)
-	{
-		if (consoleDriver == null) {
-			throw new ArgumentNullException (nameof (consoleDriver));
-		}
-		_netEvents = new NetEvents (consoleDriver);
-	}
-
-	void NetInputHandler ()
-	{
-		while (_mainLoop != null) {
-			try {
-				if (!_inputHandlerTokenSource.IsCancellationRequested) {
-					_waitForProbe.Wait (_inputHandlerTokenSource.Token);
-				}
-
-			} catch (OperationCanceledException) {
-				return;
-			} finally {
-				if (_waitForProbe.IsSet) {
-					_waitForProbe.Reset ();
-				}
-			}
-
-			if (_inputHandlerTokenSource.IsCancellationRequested) {
-				return;
-			}
-			if (_resultQueue.Count == 0) {
-				_resultQueue.Enqueue (_netEvents.DequeueInput ());
-			}
-			try {
-				while (_resultQueue.Peek () == null) {
-					_resultQueue.Dequeue ();
-				}
-				if (_resultQueue.Count > 0) {
-					_eventReady.Set ();
-				}
-			} catch (InvalidOperationException) {
-				// Ignore
-			}
-		}
-	}
-
-	void IMainLoopDriver.Setup (MainLoop mainLoop)
-	{
-		_mainLoop = mainLoop;
-		Task.Run (NetInputHandler, _inputHandlerTokenSource.Token);
-	}
-
-	void IMainLoopDriver.Wakeup () => _eventReady.Set ();
-
-	bool IMainLoopDriver.EventsPending ()
-	{
-		_waitForProbe.Set ();
-
-		if (_mainLoop.CheckTimersAndIdleHandlers (out int waitTimeout)) {
-			return true;
-		}
-
-		try {
-			if (!_eventReadyTokenSource.IsCancellationRequested) {
-				// Note: ManualResetEventSlim.Wait will wait indefinitely if the timeout is -1. The timeout is -1 when there
-				// are no timers, but there IS an idle handler waiting.
-				_eventReady.Wait (waitTimeout, _eventReadyTokenSource.Token);
-			}
-		} catch (OperationCanceledException) {
-			return true;
-		} finally {
-			_eventReady.Reset ();
-		}
-
-		if (!_eventReadyTokenSource.IsCancellationRequested) {
-			return _resultQueue.Count > 0 || _mainLoop.CheckTimersAndIdleHandlers (out _);
-		}
-
-		_eventReadyTokenSource.Dispose ();
-		_eventReadyTokenSource = new CancellationTokenSource ();
-		return true;
-	}
-
-	void IMainLoopDriver.Iteration ()
-	{
-		while (_resultQueue.Count > 0) {
-			ProcessInput?.Invoke (_resultQueue.Dequeue ().Value);
-		}
-	}
-
-	void IMainLoopDriver.TearDown ()
-	{
-		_inputHandlerTokenSource?.Cancel ();
-		_inputHandlerTokenSource?.Dispose ();
-		_eventReadyTokenSource?.Cancel ();
-		_eventReadyTokenSource?.Dispose ();
-
-		_eventReady?.Dispose ();
-
-		_resultQueue?.Clear ();
-		_waitForProbe?.Dispose ();
-		_netEvents?.Dispose ();
-		_netEvents = null;
-
-		_mainLoop = null;
-	}
-}
+/// <remarks>This implementation is used for NetDriver.</remarks>
+internal class NetMainLoop : IMainLoopDriver
+{
+    internal NetEvents _netEvents;
+
+    /// <summary>Invoked when a Key is pressed.</summary>
+    internal Action<InputResult> ProcessInput;
+
+    private readonly ManualResetEventSlim _eventReady = new (false);
+    private readonly CancellationTokenSource _inputHandlerTokenSource = new ();
+    private readonly Queue<InputResult?> _resultQueue = new ();
+    private readonly ManualResetEventSlim _waitForProbe = new (false);
+    private CancellationTokenSource _eventReadyTokenSource = new ();
+    private MainLoop _mainLoop;
+
+    /// <summary>Initializes the class with the console driver.</summary>
+    /// <remarks>Passing a consoleDriver is provided to capture windows resizing.</remarks>
+    /// <param name="consoleDriver">The console driver used by this Net main loop.</param>
+    /// <exception cref="ArgumentNullException"></exception>
+    public NetMainLoop (ConsoleDriver consoleDriver = null)
+    {
+        if (consoleDriver is null)
+        {
+            throw new ArgumentNullException (nameof (consoleDriver));
+        }
+
+        _netEvents = new NetEvents (consoleDriver);
+    }
+
+    void IMainLoopDriver.Setup (MainLoop mainLoop)
+    {
+        _mainLoop = mainLoop;
+        Task.Run (NetInputHandler, _inputHandlerTokenSource.Token);
+    }
+
+    void IMainLoopDriver.Wakeup () { _eventReady.Set (); }
+
+    bool IMainLoopDriver.EventsPending ()
+    {
+        _waitForProbe.Set ();
+
+        if (_mainLoop.CheckTimersAndIdleHandlers (out int waitTimeout))
+        {
+            return true;
+        }
+
+        try
+        {
+            if (!_eventReadyTokenSource.IsCancellationRequested)
+            {
+                // Note: ManualResetEventSlim.Wait will wait indefinitely if the timeout is -1. The timeout is -1 when there
+                // are no timers, but there IS an idle handler waiting.
+                _eventReady.Wait (waitTimeout, _eventReadyTokenSource.Token);
+            }
+        }
+        catch (OperationCanceledException)
+        {
+            return true;
+        }
+        finally
+        {
+            _eventReady.Reset ();
+        }
+
+        if (!_eventReadyTokenSource.IsCancellationRequested)
+        {
+            return _resultQueue.Count > 0 || _mainLoop.CheckTimersAndIdleHandlers (out _);
+        }
+
+        _eventReadyTokenSource.Dispose ();
+        _eventReadyTokenSource = new CancellationTokenSource ();
+
+        return true;
+    }
+
+    void IMainLoopDriver.Iteration ()
+    {
+        while (_resultQueue.Count > 0)
+        {
+            ProcessInput?.Invoke (_resultQueue.Dequeue ().Value);
+        }
+    }
+
+    void IMainLoopDriver.TearDown ()
+    {
+        _inputHandlerTokenSource?.Cancel ();
+        _inputHandlerTokenSource?.Dispose ();
+        _eventReadyTokenSource?.Cancel ();
+        _eventReadyTokenSource?.Dispose ();
+
+        _eventReady?.Dispose ();
+
+        _resultQueue?.Clear ();
+        _waitForProbe?.Dispose ();
+        _netEvents?.Dispose ();
+        _netEvents = null;
+
+        _mainLoop = null;
+    }
+
+    private void NetInputHandler ()
+    {
+        while (_mainLoop is { })
+        {
+            try
+            {
+                if (!_inputHandlerTokenSource.IsCancellationRequested)
+                {
+                    _waitForProbe.Wait (_inputHandlerTokenSource.Token);
+                }
+            }
+            catch (OperationCanceledException)
+            {
+                return;
+            }
+            finally
+            {
+                if (_waitForProbe.IsSet)
+                {
+                    _waitForProbe.Reset ();
+                }
+            }
+
+            if (_inputHandlerTokenSource.IsCancellationRequested)
+            {
+                return;
+            }
+
+            if (_resultQueue.Count == 0)
+            {
+                _resultQueue.Enqueue (_netEvents.DequeueInput ());
+            }
+
+            try
+            {
+                while (_resultQueue.Peek () is null)
+                {
+                    _resultQueue.Dequeue ();
+                }
+
+                if (_resultQueue.Count > 0)
+                {
+                    _eventReady.Set ();
+                }
+            }
+            catch (InvalidOperationException)
+            {
+                // Ignore
+            }
+        }
+    }
+}

文件差异内容过多而无法显示
+ 843 - 702
Terminal.Gui/ConsoleDrivers/WindowsDriver.cs


+ 56 - 0
Terminal.Gui/Drawing/AnsiColorCode.cs

@@ -0,0 +1,56 @@
+namespace Terminal.Gui;
+
+/// <summary>
+///     The 16 foreground color codes used by ANSI Esc sequences for 256 color terminals. Add 10 to these values for
+///     background color.
+/// </summary>
+public enum AnsiColorCode
+{
+    /// <summary>The ANSI color code for Black.</summary>
+    BLACK = 30,
+
+    /// <summary>The ANSI color code for Red.</summary>
+    RED = 31,
+
+    /// <summary>The ANSI color code for Green.</summary>
+    GREEN = 32,
+
+    /// <summary>The ANSI color code for Yellow.</summary>
+    YELLOW = 33,
+
+    /// <summary>The ANSI color code for Blue.</summary>
+    BLUE = 34,
+
+    /// <summary>The ANSI color code for Magenta.</summary>
+    MAGENTA = 35,
+
+    /// <summary>The ANSI color code for Cyan.</summary>
+    CYAN = 36,
+
+    /// <summary>The ANSI color code for White.</summary>
+    WHITE = 37,
+
+    /// <summary>The ANSI color code for Bright Black.</summary>
+    BRIGHT_BLACK = 90,
+
+    /// <summary>The ANSI color code for Bright Red.</summary>
+    BRIGHT_RED = 91,
+
+    /// <summary>The ANSI color code for Bright Green.</summary>
+    BRIGHT_GREEN = 92,
+
+    /// <summary>The ANSI color code for Bright Yellow.</summary>
+    BRIGHT_YELLOW = 93,
+
+    /// <summary>The ANSI color code for Bright Blue.</summary>
+    BRIGHT_BLUE = 94,
+
+    /// <summary>The ANSI color code for Bright Magenta.</summary>
+    BRIGHT_MAGENTA = 95,
+
+    /// <summary>The ANSI color code for Bright Cyan.</summary>
+    BRIGHT_CYAN = 96,
+
+    /// <summary>The ANSI color code for Bright White.</summary>
+    BRIGHT_WHITE = 97
+}

+ 107 - 0
Terminal.Gui/Drawing/Attribute.cs

@@ -0,0 +1,107 @@
+#nullable enable
+using System.Numerics;
+using System.Text.Json.Serialization;
+
+namespace Terminal.Gui;
+
+/// <summary>Attributes represent how text is styled when displayed in the terminal.</summary>
+/// <remarks>
+///     <see cref="Attribute"/> provides a platform independent representation of colors (and someday other forms of
+///     text styling). They encode both the foreground and the background color and are used in the
+///     <see cref="ColorScheme"/> class to define color schemes that can be used in an application.
+/// </remarks>
+[JsonConverter (typeof (AttributeJsonConverter))]
+public readonly record struct Attribute : IEqualityOperators<Attribute, Attribute, bool>
+{
+    /// <summary>Default empty attribute.</summary>
+    [JsonIgnore]
+    public static Attribute Default => new (Color.White, ColorName.Black);
+
+    /// <summary>The <see cref="ConsoleDriver"/>-specific color value.</summary>
+    [JsonIgnore (Condition = JsonIgnoreCondition.Always)]
+    internal int PlatformColor { get; init; }
+
+    /// <summary>The foreground color.</summary>
+    [JsonConverter (typeof (ColorJsonConverter))]
+    public Color Foreground { get; }
+
+    /// <summary>The background color.</summary>
+    [JsonConverter (typeof (ColorJsonConverter))]
+    public Color Background { get; }
+
+    /// <summary>Initializes a new instance with default values.</summary>
+    public Attribute ()
+    {
+        this = Default with { PlatformColor = -1 };
+    }
+
+    /// <summary>Initializes a new instance from an existing instance.</summary>
+    public Attribute (in Attribute attr)
+    {
+        this = attr with { PlatformColor = -1 };
+    }
+
+    /// <summary>Initializes a new instance of the <see cref="Attribute"/> struct.</summary>
+    /// <param name="platformColor">platform-dependent color value.</param>
+    /// <param name="foreground">Foreground</param>
+    /// <param name="background">Background</param>
+    internal Attribute (int platformColor, Color foreground, Color background)
+    {
+        Foreground = foreground;
+        Background = background;
+        PlatformColor = platformColor;
+    }
+
+    /// <summary>Initializes a new instance of the <see cref="Attribute"/> struct.</summary>
+    /// <param name="foreground">Foreground</param>
+    /// <param name="background">Background</param>
+    public Attribute (in Color foreground, in Color background)
+    {
+        Foreground = foreground;
+        Background = background;
+
+        // TODO: Once CursesDriver supports true color all the PlatformColor stuff goes away
+        PlatformColor = Application.Driver?.MakeColor(in foreground, in background).PlatformColor ?? -1;
+    }
+
+    /// <summary>
+    ///     Initializes a new instance with a <see cref="ColorName"/> value. Both <see cref="Foreground"/> and
+    ///     <see cref="Background"/> will be set to the specified color.
+    /// </summary>
+    /// <param name="colorName">Value.</param>
+    internal Attribute (in ColorName colorName) : this (in colorName, in colorName) { }
+
+    /// <summary>Initializes a new instance of the <see cref="Attribute"/> struct.</summary>
+    /// <param name="foregroundName">Foreground</param>
+    /// <param name="backgroundName">Background</param>
+    public Attribute (in ColorName foregroundName, in ColorName backgroundName)
+        : this (new Color (in foregroundName), new Color (in backgroundName))
+    { }
+
+    /// <summary>Initializes a new instance of the <see cref="Attribute"/> struct.</summary>
+    /// <param name="foregroundName">Foreground</param>
+    /// <param name="background">Background</param>
+    public Attribute (in ColorName foregroundName, in Color background) : this (new Color (in foregroundName), in background) { }
+
+    /// <summary>Initializes a new instance of the <see cref="Attribute"/> struct.</summary>
+    /// <param name="foreground">Foreground</param>
+    /// <param name="backgroundName">Background</param>
+    public Attribute (in Color foreground, in ColorName backgroundName) : this (in foreground, new Color (in backgroundName)) { }
+
+    /// <summary>
+    ///     Initializes a new instance of the <see cref="Attribute"/> struct with the same colors for the foreground and
+    ///     background.
+    /// </summary>
+    /// <param name="color">The color.</param>
+    public Attribute (in Color color) : this (color, color) { }
+
+    /// <inheritdoc/>
+    public override int GetHashCode () { return HashCode.Combine (PlatformColor, Foreground, Background); }
+
+    /// <inheritdoc/>
+    public override string ToString ()
+    {
+        // Note: Unit tests are dependent on this format
+        return $"[{Foreground},{Background}]";
+    }
+}

+ 35 - 38
Terminal.Gui/Drawing/Cell.cs

@@ -1,46 +1,43 @@
-using System.Collections.Generic;
-using System.Text;
-
-
-namespace Terminal.Gui;
+namespace Terminal.Gui;
 
 /// <summary>
-/// Represents a single row/column in a Terminal.Gui rendering surface
-/// (e.g. <see cref="LineCanvas"/> and <see cref="ConsoleDriver"/>).
+///     Represents a single row/column in a Terminal.Gui rendering surface (e.g. <see cref="LineCanvas"/> and
+///     <see cref="ConsoleDriver"/>).
 /// </summary>
-public class Cell {
-	Rune _rune;
-	/// <summary>
-	/// The character to display. If <see cref="Rune"/> is <see langword="null"/>, then <see cref="Rune"/> is ignored.
-	/// </summary>
-	public Rune Rune {
-		get => _rune;
-		set {
-			CombiningMarks.Clear ();
-			_rune = value;
-		}
-	}
+public class Cell
+{
+    private Rune _rune;
+
+    /// <summary>The attributes to use when drawing the Glyph.</summary>
+    public Attribute? Attribute { get; set; }
 
-	/// <summary>
-	/// The combining marks for <see cref="Rune"/> that when combined makes this Cell a combining sequence.
-	/// If <see cref="CombiningMarks"/> empty, then <see cref="CombiningMarks"/> is ignored.
-	/// </summary>
-	/// <remarks>
-	/// Only valid in the rare case where <see cref="Rune"/> is a combining sequence that could not be normalized to a single Rune.
-	/// </remarks>
-	internal List<Rune> CombiningMarks { get; } = new List<Rune> ();
+    /// <summary>
+    ///     Gets or sets a value indicating whether this <see cref="T:Terminal.Gui.Cell"/> has been modified since the
+    ///     last time it was drawn.
+    /// </summary>
+    public bool IsDirty { get; set; }
 
-	/// <summary>
-	/// The attributes to use when drawing the Glyph.
-	/// </summary>
-	public Attribute? Attribute { get; set; }
+    /// <summary>The character to display. If <see cref="Rune"/> is <see langword="null"/>, then <see cref="Rune"/> is ignored.</summary>
+    public Rune Rune
+    {
+        get => _rune;
+        set
+        {
+            CombiningMarks.Clear ();
+            _rune = value;
+        }
+    }
 
-	/// <summary>
-	/// Gets or sets a value indicating whether this <see cref="T:Terminal.Gui.Cell"/> has
-	/// been modified since the last time it was drawn.
-	/// </summary>
-	public bool IsDirty { get; set; }
+    /// <summary>
+    ///     The combining marks for <see cref="Rune"/> that when combined makes this Cell a combining sequence. If
+    ///     <see cref="CombiningMarks"/> empty, then <see cref="CombiningMarks"/> is ignored.
+    /// </summary>
+    /// <remarks>
+    ///     Only valid in the rare case where <see cref="Rune"/> is a combining sequence that could not be normalized to a
+    ///     single Rune.
+    /// </remarks>
+    internal List<Rune> CombiningMarks { get; } = new ();
 
-	/// <inheritdoc />
-	public override string ToString () => $"[{Rune}, {Attribute}]";
+    /// <inheritdoc/>
+    public override string ToString () { return $"[{Rune}, {Attribute}]"; }
 }

+ 80 - 0
Terminal.Gui/Drawing/Color.ColorExtensions.cs

@@ -0,0 +1,80 @@
+using System.Collections.Frozen;
+
+namespace Terminal.Gui;
+
+internal static class ColorExtensions
+{
+    private static FrozenDictionary<Color, ColorName> colorToNameMap;
+
+    static ColorExtensions ()
+    {
+        Dictionary<ColorName, AnsiColorCode> nameToCodeMap = new ()
+        {
+            { ColorName.Black, AnsiColorCode.BLACK },
+            { ColorName.Blue, AnsiColorCode.BLUE },
+            { ColorName.Green, AnsiColorCode.GREEN },
+            { ColorName.Cyan, AnsiColorCode.CYAN },
+            { ColorName.Red, AnsiColorCode.RED },
+            { ColorName.Magenta, AnsiColorCode.MAGENTA },
+            { ColorName.Yellow, AnsiColorCode.YELLOW },
+            { ColorName.Gray, AnsiColorCode.WHITE },
+            { ColorName.DarkGray, AnsiColorCode.BRIGHT_BLACK },
+            { ColorName.BrightBlue, AnsiColorCode.BRIGHT_BLUE },
+            { ColorName.BrightGreen, AnsiColorCode.BRIGHT_GREEN },
+            { ColorName.BrightCyan, AnsiColorCode.BRIGHT_CYAN },
+            { ColorName.BrightRed, AnsiColorCode.BRIGHT_RED },
+            { ColorName.BrightMagenta, AnsiColorCode.BRIGHT_MAGENTA },
+            { ColorName.BrightYellow, AnsiColorCode.BRIGHT_YELLOW },
+            { ColorName.White, AnsiColorCode.BRIGHT_WHITE }
+        };
+        ColorNameToAnsiColorMap = nameToCodeMap.ToFrozenDictionary ();
+
+        ColorToNameMap = new Dictionary<Color, ColorName>
+        {
+            // using "Windows 10 Console/PowerShell 6" here: https://i.stack.imgur.com/9UVnC.png
+            // See also: https://en.wikipedia.org/wiki/ANSI_escape_code
+            { new Color (12, 12, 12), ColorName.Black },
+            { new Color (0, 55, 218), ColorName.Blue },
+            { new Color (19, 161, 14), ColorName.Green },
+            { new Color (58, 150, 221), ColorName.Cyan },
+            { new Color (197, 15, 31), ColorName.Red },
+            { new Color (136, 23, 152), ColorName.Magenta },
+            { new Color (128, 64, 32), ColorName.Yellow },
+            { new Color (204, 204, 204), ColorName.Gray },
+            { new Color (118, 118, 118), ColorName.DarkGray },
+            { new Color (59, 120, 255), ColorName.BrightBlue },
+            { new Color (22, 198, 12), ColorName.BrightGreen },
+            { new Color (97, 214, 214), ColorName.BrightCyan },
+            { new Color (231, 72, 86), ColorName.BrightRed },
+            { new Color (180, 0, 158), ColorName.BrightMagenta },
+            { new Color (249, 241, 165), ColorName.BrightYellow },
+            { new Color (242, 242, 242), ColorName.White }
+        }.ToFrozenDictionary ();
+    }
+
+    /// <summary>Defines the 16 legacy color names and their corresponding ANSI color codes.</summary>
+    internal static FrozenDictionary<ColorName, AnsiColorCode> ColorNameToAnsiColorMap { get; }
+
+    /// <summary>Reverse mapping for <see cref="ColorToNameMap"/>.</summary>
+    internal static FrozenDictionary<ColorName, Color> ColorNameToColorMap { get; private set; }
+
+    /// <summary>
+    ///     Gets or sets a <see cref="FrozenDictionary{TKey,TValue}"/> that maps legacy 16-color values to the
+    ///     corresponding <see cref="ColorName"/>.
+    /// </summary>
+    /// <remarks>
+    ///     Setter should be called as infrequently as possible, as <see cref="FrozenDictionary{TKey,TValue}"/> is
+    ///     expensive to create.
+    /// </remarks>
+    internal static FrozenDictionary<Color, ColorName> ColorToNameMap
+    {
+        get => colorToNameMap;
+        set
+        {
+            colorToNameMap = value;
+
+            //Also be sure to set the reverse mapping
+            ColorNameToColorMap = value.ToFrozenDictionary (static kvp => kvp.Value, static kvp => kvp.Key);
+        }
+    }
+}

+ 63 - 0
Terminal.Gui/Drawing/Color.ColorName.cs

@@ -0,0 +1,63 @@
+namespace Terminal.Gui;
+
+/// <summary>
+///     Defines the 16 legacy color names and values that can be used to set the foreground and background colors in
+///     Terminal.Gui apps. Used with <see cref="Color"/>.
+/// </summary>
+/// <remarks>
+///     <para>These colors match the 16 colors defined for ANSI escape sequences for 4-bit (16) colors.</para>
+///     <para>
+///         For terminals that support 24-bit color (TrueColor), the RGB values for each of these colors can be
+///         configured using the <see cref="Color.Colors"/> property.
+///     </para>
+/// </remarks>
+public enum ColorName
+{
+    /// <summary>The black color. ANSI escape sequence: <c>\u001b[30m</c>.</summary>
+    Black,
+
+    /// <summary>The blue color. ANSI escape sequence: <c>\u001b[34m</c>.</summary>
+    Blue,
+
+    /// <summary>The green color. ANSI escape sequence: <c>\u001b[32m</c>.</summary>
+    Green,
+
+    /// <summary>The cyan color. ANSI escape sequence: <c>\u001b[36m</c>.</summary>
+    Cyan,
+
+    /// <summary>The red color. ANSI escape sequence: <c>\u001b[31m</c>.</summary>
+    Red,
+
+    /// <summary>The magenta color. ANSI escape sequence: <c>\u001b[35m</c>.</summary>
+    Magenta,
+
+    /// <summary>The yellow color (also known as Brown). ANSI escape sequence: <c>\u001b[33m</c>.</summary>
+    Yellow,
+
+    /// <summary>The gray color (also known as White). ANSI escape sequence: <c>\u001b[37m</c>.</summary>
+    Gray,
+
+    /// <summary>The dark gray color (also known as Bright Black). ANSI escape sequence: <c>\u001b[30;1m</c>.</summary>
+    DarkGray,
+
+    /// <summary>The bright blue color. ANSI escape sequence: <c>\u001b[34;1m</c>.</summary>
+    BrightBlue,
+
+    /// <summary>The bright green color. ANSI escape sequence: <c>\u001b[32;1m</c>.</summary>
+    BrightGreen,
+
+    /// <summary>The bright cyan color. ANSI escape sequence: <c>\u001b[36;1m</c>.</summary>
+    BrightCyan,
+
+    /// <summary>The bright red color. ANSI escape sequence: <c>\u001b[31;1m</c>.</summary>
+    BrightRed,
+
+    /// <summary>The bright magenta color. ANSI escape sequence: <c>\u001b[35;1m</c>.</summary>
+    BrightMagenta,
+
+    /// <summary>The bright yellow color. ANSI escape sequence: <c>\u001b[33;1m</c>.</summary>
+    BrightYellow,
+
+    /// <summary>The White color (also known as Bright White). ANSI escape sequence: <c>\u001b[37;1m</c>.</summary>
+    White
+}

+ 93 - 0
Terminal.Gui/Drawing/Color.ColorParseException.cs

@@ -0,0 +1,93 @@
+#nullable enable
+using System.Diagnostics.CodeAnalysis;
+
+namespace Terminal.Gui;
+
+/// <summary>An exception thrown when something goes wrong when trying to parse a <see cref="Color"/>.</summary>
+/// <remarks>Contains additional information to help locate the problem. <br/> Not intended to be thrown by consumers.</remarks>
+public sealed class ColorParseException : FormatException
+{
+    internal const string DefaultMessage = "Failed to parse text as Color.";
+
+    internal ColorParseException (string colorString, string? message, Exception? innerException = null) :
+        base (message ?? DefaultMessage, innerException)
+    {
+        ColorString = colorString;
+    }
+
+    internal ColorParseException (string colorString, string? message = DefaultMessage) : base (message) { ColorString = colorString; }
+
+    /// <summary>Creates a new instance of a <see cref="ColorParseException"/> populated with the provided values.</summary>
+    /// <param name="colorString">The text that caused this exception, as a <see langword="string"/>.</param>
+    /// <param name="badValue">The specific value in <paramref name="colorString"/> that caused this exception.</param>
+    /// <param name="badValueName">The name of the value (red, green, blue, alpha) that <paramref name="badValue"/> represents.</param>
+    /// <param name="reason">The reason that <paramref name="badValue"/> failed to parse.</param>
+    internal ColorParseException (
+        in ReadOnlySpan<char> colorString,
+        string reason,
+        in ReadOnlySpan<char> badValue = default,
+        in ReadOnlySpan<char> badValueName = default
+    ) : base (DefaultMessage)
+    {
+        ColorString = colorString.ToString ();
+        BadValue = badValue.ToString ();
+        BadValueName = badValueName.ToString ();
+        Reason = reason;
+    }
+
+    /// <summary>Creates a new instance of a <see cref="ColorParseException"/> populated with the provided values.</summary>
+    /// <param name="colorString">The text that caused this exception, as a <see langword="string"/>.</param>
+    /// <param name="badValue">The specific value in <paramref name="colorString"/> that caused this exception.</param>
+    /// <param name="badValueName">The name of the value (red, green, blue, alpha) that <paramref name="badValue"/> represents.</param>
+    /// <param name="reason">The reason that <paramref name="badValue"/> failed to parse.</param>
+    internal ColorParseException (
+        in ReadOnlySpan<char> colorString,
+        string? badValue = null,
+        string? badValueName = null,
+        string? reason = null
+    ) : base (DefaultMessage)
+    {
+        ColorString = colorString.ToString ();
+        BadValue = badValue;
+        BadValueName = badValueName;
+        Reason = reason;
+    }
+
+    /// <summary>Gets the substring of <see cref="ColorString"/> caused this exception, as a <see langword="string"/></summary>
+    /// <remarks>May be null or empty - only set if known.</remarks>
+    public string? BadValue { get; }
+
+    /// <summary>Gets the name of the color component corresponding to <see cref="BadValue"/>, if known.</summary>
+    /// <remarks>May be null or empty - only set if known.</remarks>
+    public string? BadValueName { get; }
+
+    /// <summary>Gets the text that failed to parse, as a <see langword="string"/></summary>
+    /// <remarks>Is marked <see langword="required"/>, so must be set by a constructor or initializer.</remarks>
+    public string ColorString { get; }
+
+    /// <summary>Gets the reason that <see cref="BadValue"/> failed to parse, if known.</summary>
+    /// <remarks>May be null or empty - only set if known.</remarks>
+    public string? Reason { get; }
+
+    [DoesNotReturn]
+    internal static void Throw (
+        in ReadOnlySpan<char> colorString,
+        string reason,
+        in ReadOnlySpan<char> badValue = default,
+        in ReadOnlySpan<char> badValueName = default
+    )
+    {
+        throw new ColorParseException (in colorString, reason, in badValue, in badValueName);
+    }
+
+    [DoesNotReturn]
+    internal static void ThrowIfNotAsciiDigits (
+        in ReadOnlySpan<char> valueText,
+        string reason,
+        in ReadOnlySpan<char> badValue = default,
+        in ReadOnlySpan<char> badValueName = default
+    )
+    {
+        throw new ColorParseException (in valueText, reason, in badValue, in badValueName);
+    }
+}

+ 616 - 0
Terminal.Gui/Drawing/Color.Formatting.cs

@@ -0,0 +1,616 @@
+#nullable enable
+using System.Diagnostics.CodeAnalysis;
+using System.Diagnostics.Contracts;
+using System.Globalization;
+using System.Runtime.CompilerServices;
+
+namespace Terminal.Gui;
+
+public readonly partial record struct Color
+{
+    /// <inheritdoc cref="object.ToString"/>
+    /// <summary>
+    ///     Returns a <see langword="string"/> representation of the current <see cref="Color"/> value, according to the
+    ///     provided <paramref name="formatString"/> and optional <paramref name="formatProvider"/>.
+    /// </summary>
+    /// <param name="formatString">
+    ///     A format string that will be passed to
+    ///     <see cref="string.Format(System.IFormatProvider?,string,object?[])"/>.
+    ///     <para/>
+    ///     See remarks for parameters passed to that method.
+    /// </param>
+    /// <param name="formatProvider">
+    ///     An optional <see cref="IFormatProvider"/> to use when formatting the <see cref="Color"/>
+    ///     using custom format strings not specified for this method. Provides this instance as <see cref="Argb"/>. <br/> If
+    ///     this parameter is not null, the specified <see cref="IFormatProvider"/> will be used instead of the custom
+    ///     formatting provided by the <see cref="Color"/> type.
+    ///     <para/>
+    ///     See remarks for defined format strings.
+    /// </param>
+    /// <remarks>
+    ///     Pre-defined format strings for this method, if a custom <paramref name="formatProvider"/> is not supplied are:
+    ///     <list type="bullet">
+    ///         <listheader>
+    ///             <term>Value</term> <description>Result</description>
+    ///         </listheader>
+    ///         <item>
+    ///             <term>g or null or empty string</term>
+    ///             <description>
+    ///                 General/default format - Returns a named <see cref="Color"/> if there is a match, or a
+    ///                 24-bit/3-byte/6-hex digit string in "#RRGGBB" format.
+    ///             </description>
+    ///         </item>
+    ///         <item>
+    ///             <term>G</term>
+    ///             <description>
+    ///                 Extended general format - Returns a named <see cref="Color"/> if there is a match, or a
+    ///                 32-bit/4-byte/8-hex digit string in "#AARRGGBB" format.
+    ///             </description>
+    ///         </item>
+    ///         <item>
+    ///             <term>d</term>
+    ///             <description>
+    ///                 Decimal format - Returns a 3-component decimal representation of the <see cref="Color"/> in
+    ///                 "rgb(R,G,B)" format.
+    ///             </description>
+    ///         </item>
+    ///         <item>
+    ///             <term>D</term>
+    ///             <description>
+    ///                 Extended decimal format - Returns a 4-component decimal representation of the
+    ///                 <see cref="Color"/> in "rgba(R,G,B,A)" format.
+    ///             </description>
+    ///         </item>
+    ///     </list>
+    ///     <para>
+    ///         If <paramref name="formatProvider"/> is provided and is a non-null <see cref="ICustomColorFormatter"/>, the
+    ///         following behaviors are available, for the specified values of <paramref name="formatString"/>:
+    ///         <list type="bullet">
+    ///             <listheader>
+    ///                 <term>Value</term> <description>Result</description>
+    ///             </listheader>
+    ///             <item>
+    ///                 <term>null or empty string</term>
+    ///                 <description>
+    ///                     Calls <see cref="ICustomColorFormatter.Format(string?,byte,byte,byte,byte)"/> on the
+    ///                     provided <paramref name="formatProvider"/> with the null string, and <see cref="R"/>,
+    ///                     <see cref="G"/>, <see cref="B"/>, and <see cref="A"/> as typed arguments of type <see cref="Byte"/>
+    ///                     .
+    ///                 </description>
+    ///             </item>
+    ///             <item>
+    ///                 <term>All other values</term>
+    ///                 <description>
+    ///                     Calls <see cref="string.Format{TArg0}"/> with the provided
+    ///                     <paramref name="formatProvider"/> and <paramref name="formatString"/> (parsed as a
+    ///                     <see cref="CompositeFormat"/>), with the value of <see cref="Argb"/> as the sole
+    ///                     <see langword="uint"/>-typed argument.
+    ///                 </description>
+    ///             </item>
+    ///         </list>
+    ///     </para>
+    /// </remarks>
+    [SkipLocalsInit]
+    public string ToString (
+        [StringSyntax (StringSyntaxAttribute.CompositeFormat)] string? formatString,
+        IFormatProvider? formatProvider = null
+    )
+    {
+        return (formatString, formatProvider) switch
+               {
+                   // Null or empty string and null formatProvider - Revert to 'g' case behavior
+                   (null or { Length: 0 }, null) => ToString (),
+
+                   // Null or empty string and formatProvider is an ICustomColorFormatter - Output according to the given ICustomColorFormatted, with R, G, B, and A as typed arguments
+                   (null or { Length: 0 }, ICustomColorFormatter ccf) => ccf.Format (null, R, G, B, A),
+
+                   // Null or empty string and formatProvider is otherwise non-null but not the invariant culture - Output according to string.Format with the given IFormatProvider and R, G, B, and A as boxed arguments, with string.Empty as the format string
+                   (null or { Length: 0 }, { }) when !Equals (formatProvider, CultureInfo.InvariantCulture) =>
+                       string.Format (formatProvider, formatString ?? string.Empty, R, G, B, A),
+
+                   // Null or empty string and formatProvider is the invariant culture - Output according to string.Format with the given IFormatProvider and R, G, B, and A as boxed arguments, with string.Empty as the format string
+                   (null or { Length: 0 }, { }) when Equals (formatProvider, CultureInfo.InvariantCulture) =>
+                       $"#{R:X2}{G:X2}{B:X2}",
+
+                   // Non-null string and non-null formatProvider - let formatProvider handle it and give it R, G, B, and A
+                   ({ }, { }) => string.Format (formatProvider, CompositeFormat.Parse (formatString), R, G, B, A),
+
+                   // g format string and null formatProvider - Output as 24-bit hex according to invariant culture rules from R, G, and B
+                   (['g'], null) => ToString (),
+
+                   // G format string and null formatProvider - Output as 32-bit hex according to invariant culture rules from Argb
+                   (['G'], null) => $"#{A:X2}{R:X2}{G:X2}{B:X2}",
+
+                   // d format string and null formatProvider - Output as 24-bit decimal rgb(r,g,b) according to invariant culture rules from R, G, and B
+                   (['d'], null) => $"rgb({R:D},{G:D},{B:D})",
+
+                   // D format string and null formatProvider - Output as 32-bit decimal rgba(r,g,b,a) according to invariant culture rules from R, G, B, and A. Non-standard: a is a decimal byte value.
+                   (['D'], null) => $"rgba({R:D},{G:D},{B:D},{A:D})",
+
+                   // All other cases (formatString is not null here) - Delegate to formatProvider, first, and otherwise to invariant culture, and try to format the provided string from the channels
+                   ({ }, _) => string.Format (
+                                              formatProvider ?? CultureInfo.InvariantCulture,
+                                              CompositeFormat.Parse (formatString),
+                                              R,
+                                              G,
+                                              B,
+                                              A
+                                             ),
+                   _ => throw new InvalidOperationException (
+                                                             $"Unable to create string from Color with value {Argb}, using format string {formatString}"
+                                                            )
+               }
+               ?? throw new InvalidOperationException (
+                                                       $"Unable to create string from Color with value {Argb}, using format string {formatString}"
+                                                      );
+    }
+
+    /// <inheritdoc/>
+    /// <remarks>
+    ///     <para>
+    ///         This method should be used only when absolutely necessary, because it <b>always</b> has more overhead than
+    ///         <see cref="ToString(string?,System.IFormatProvider?)"/>, as this method results in an intermediate allocation
+    ///         of one or more instances of <see langword="string"/> and a copy of that string to
+    ///         <paramref name="destination"/> if formatting was successful. <br/> When possible, use
+    ///         <see cref="ToString(string?,System.IFormatProvider?)"/>, which attempts to avoid intermediate allocations.
+    ///     </para>
+    ///     <para>
+    ///         This method only returns <see langword="true"/> and with its output written to <paramref name="destination"/>
+    ///         if the formatted string, <i>in its entirety</i>, will fit in <paramref name="destination"/>. If the resulting
+    ///         formatted string is too large to fit in <paramref name="destination"/>, the result will be false and
+    ///         <paramref name="destination"/> will be unaltered.
+    ///     </para>
+    ///     <para>
+    ///         The resulting formatted string may be <b>shorter</b> than <paramref name="destination"/>. When this method
+    ///         returns <see langword="true"/>, use <paramref name="charsWritten"/> when handling the value of
+    ///         <paramref name="destination"/>.
+    ///     </para>
+    /// </remarks>
+    [Pure]
+    [SkipLocalsInit]
+    public bool TryFormat (
+        Span<char> destination,
+        out int charsWritten,
+        ReadOnlySpan<char> format,
+        IFormatProvider? provider
+    )
+    {
+        // TODO: This can probably avoid a string allocation with a little more work
+        try
+        {
+            string formattedString = ToString (format.ToString (), provider);
+
+            if (formattedString.Length <= destination.Length)
+            {
+                formattedString.CopyTo (destination);
+                charsWritten = formattedString.Length;
+
+                return true;
+            }
+        }
+        catch
+        {
+            destination.Clear ();
+            charsWritten = 0;
+
+            return false;
+        }
+
+        destination.Clear ();
+        charsWritten = 0;
+
+        return false;
+    }
+
+    /// <summary>Converts the provided <see langword="string"/> to a new <see cref="Color"/> value.</summary>
+    /// <param name="text">
+    ///     The text to analyze. Formats supported are "#RGB", "#RRGGBB", "#ARGB", "#AARRGGBB", "rgb(r,g,b)",
+    ///     "rgb(r,g,b,a)", "rgba(r,g,b)", "rgba(r,g,b,a)", and any of the <see cref="Gui.ColorName"/> string values.
+    /// </param>
+    /// <param name="formatProvider">
+    ///     If specified and not <see langword="null"/>, will be passed to
+    ///     <see cref="Parse(System.ReadOnlySpan{char},System.IFormatProvider?)"/>.
+    /// </param>
+    /// <returns>A <see cref="Color"/> value equivalent to <paramref name="text"/>, if parsing was successful.</returns>
+    /// <remarks>While <see cref="Color"/> supports the alpha channel <see cref="A"/>, Terminal.Gui does not.</remarks>
+    /// <exception cref="ArgumentNullException">If <paramref name="text"/> is <see langword="null"/>.</exception>
+    /// <exception cref="ArgumentException">
+    ///     If <paramref name="text"/> is an empty string or consists of only whitespace
+    ///     characters.
+    /// </exception>
+    /// <exception cref="ColorParseException">
+    ///     If thrown by
+    ///     <see cref="Parse(System.ReadOnlySpan{char},System.IFormatProvider?)"/>.
+    /// </exception>
+    [Pure]
+    [SkipLocalsInit]
+    public static Color Parse (string? text, IFormatProvider? formatProvider = null)
+    {
+        ArgumentException.ThrowIfNullOrWhiteSpace (text, nameof (text));
+
+        if (text is { Length: < 3 } && formatProvider is null)
+        {
+            throw new ColorParseException (
+                                           text,
+                                           reason: "Provided text is too short to be any known color format.",
+                                           badValue: text
+                                          );
+        }
+
+        return Parse (text.AsSpan (), formatProvider ?? CultureInfo.InvariantCulture);
+    }
+
+    /// <summary>
+    ///     Converts the provided <see cref="ReadOnlySpan{T}"/> of <see langword="char"/> to a new <see cref="Color"/>
+    ///     value.
+    /// </summary>
+    /// <param name="text">
+    ///     The text to analyze. Formats supported are "#RGB", "#RRGGBB", "#RGBA", "#AARRGGBB", "rgb(r,g,b)",
+    ///     "rgb(r,g,b,a)", "rgba(r,g,b)", "rgba(r,g,b,a)", and any of the <see cref="Gui.ColorName"/> string values.
+    /// </param>
+    /// <param name="formatProvider">
+    ///     Optional <see cref="IFormatProvider"/> to provide parsing services for the input text.
+    ///     <br/> Defaults to <see cref="CultureInfo.InvariantCulture"/> if <see langword="null"/>. <br/> If not null, must
+    ///     implement <see cref="ICustomColorFormatter"/> or will be ignored and <see cref="CultureInfo.InvariantCulture"/>
+    ///     will be used.
+    /// </param>
+    /// <returns>A <see cref="Color"/> value equivalent to <paramref name="text"/>, if parsing was successful.</returns>
+    /// <remarks>While <see cref="Color"/> supports the alpha channel <see cref="A"/>, Terminal.Gui does not.</remarks>
+    /// <exception cref="ArgumentException">
+    ///     with an inner <see cref="FormatException"/> if <paramref name="text"/> was unable
+    ///     to be successfully parsed as a <see cref="Color"/>, for any reason.
+    /// </exception>
+    [Pure]
+    [SkipLocalsInit]
+    public static Color Parse (ReadOnlySpan<char> text, IFormatProvider? formatProvider = null)
+    {
+        return text switch
+               {
+                   // Null string or empty span provided
+                   { IsEmpty: true } when formatProvider is null => throw new ColorParseException (
+                                                                                                   in text,
+                                                                                                   "The text provided was null or empty.",
+                                                                                                   in text
+                                                                                                  ),
+
+                   // A valid ICustomColorFormatter was specified and the text wasn't null or empty
+                   { IsEmpty: false } when formatProvider is ICustomColorFormatter f => f.Parse (text),
+
+                   // Input string is only whitespace
+                   { Length: > 0 } when text.IsWhiteSpace () => throw new ColorParseException (
+                                                                                               in text,
+                                                                                               "The text provided consisted of only whitespace characters.",
+                                                                                               in text
+                                                                                              ),
+
+                   // Any string too short to possibly be any supported format.
+                   { Length: > 0 and < 4 } => throw new ColorParseException (
+                                                                             in text,
+                                                                             "Text was too short to be any possible supported format.",
+                                                                             in text
+                                                                            ),
+
+                   // The various hexadecimal cases
+                   ['#', ..] hexString => hexString switch
+                                          {
+                                              // #RGB
+                                              ['#', var rChar, var gChar, var bChar] chars when chars [1..]
+                                                      .IsAllAsciiHexDigits () =>
+                                                  new Color (
+                                                             byte.Parse ([rChar, rChar], NumberStyles.HexNumber),
+                                                             byte.Parse ([gChar, gChar], NumberStyles.HexNumber),
+                                                             byte.Parse ([bChar, bChar], NumberStyles.HexNumber)
+                                                            ),
+
+                                              // #ARGB
+                                              ['#', var aChar, var rChar, var gChar, var bChar] chars when chars [1..]
+                                                      .IsAllAsciiHexDigits () =>
+                                                  new Color (
+                                                             byte.Parse ([rChar, rChar], NumberStyles.HexNumber),
+                                                             byte.Parse ([gChar, gChar], NumberStyles.HexNumber),
+                                                             byte.Parse ([bChar, bChar], NumberStyles.HexNumber),
+                                                             byte.Parse ([aChar, aChar], NumberStyles.HexNumber)
+                                                            ),
+
+                                              // #RRGGBB
+                                              [
+                                                      '#', var r1Char, var r2Char, var g1Char, var g2Char, var b1Char,
+                                                      var b2Char
+                                                  ] chars when chars [1..].IsAllAsciiHexDigits () =>
+                                                  new Color (
+                                                             byte.Parse ([r1Char, r2Char], NumberStyles.HexNumber),
+                                                             byte.Parse ([g1Char, g2Char], NumberStyles.HexNumber),
+                                                             byte.Parse ([b1Char, b2Char], NumberStyles.HexNumber)
+                                                            ),
+
+                                              // #AARRGGBB
+                                              [
+                                                      '#', var a1Char, var a2Char, var r1Char, var r2Char, var g1Char,
+                                                      var g2Char, var b1Char, var b2Char
+                                                  ] chars when chars [1..].IsAllAsciiHexDigits () =>
+                                                  new Color (
+                                                             byte.Parse ([r1Char, r2Char], NumberStyles.HexNumber),
+                                                             byte.Parse ([g1Char, g2Char], NumberStyles.HexNumber),
+                                                             byte.Parse ([b1Char, b2Char], NumberStyles.HexNumber),
+                                                             byte.Parse ([a1Char, a2Char], NumberStyles.HexNumber)
+                                                            ),
+                                              _ => throw new ColorParseException (
+                                                                                  in hexString,
+                                                                                  $"Color hex string {hexString} was not in a supported format",
+                                                                                  in hexString
+                                                                                 )
+                                          },
+
+                   // rgb(r,g,b) or rgb(r,g,b,a)
+                   ['r', 'g', 'b', '(', .., ')'] => ParseRgbaFormat (in text, 4),
+
+                   // rgba(r,g,b,a) or rgba(r,g,b)
+                   ['r', 'g', 'b', 'a', '(', .., ')'] => ParseRgbaFormat (in text, 5),
+
+                   // Attempt to parse as a named color from the ColorName enum
+                   { } when char.IsLetter (text [0]) && Enum.TryParse (text, true, out ColorName colorName) =>
+                       new Color (colorName),
+
+                   // Any other input
+                   _ => throw new ColorParseException (in text, "Text did not match any expected format.", in text, [])
+               };
+
+        [Pure]
+        [SkipLocalsInit]
+        static Color ParseRgbaFormat (in ReadOnlySpan<char> originalString, in int startIndex)
+        {
+            ReadOnlySpan<char> valuesSubstring = originalString [startIndex..^1];
+            Span<Range> valueRanges = stackalloc Range [4];
+
+            int rangeCount = valuesSubstring.Split (
+                                                    valueRanges,
+                                                    ',',
+                                                    StringSplitOptions.RemoveEmptyEntries
+                                                    | StringSplitOptions.TrimEntries
+                                                   );
+
+            switch (rangeCount)
+            {
+                case 3:
+                {
+                    // rgba(r,g,b)
+                    ParseRgbValues (
+                                    in valuesSubstring,
+                                    in valueRanges,
+                                    in originalString,
+                                    out ReadOnlySpan<char> rSpan,
+                                    out ReadOnlySpan<char> gSpan,
+                                    out ReadOnlySpan<char> bSpan
+                                   );
+
+                    return new Color (int.Parse (rSpan), int.Parse (gSpan), int.Parse (bSpan));
+                }
+                case 4:
+                {
+                    // rgba(r,g,b,a)
+                    ParseRgbValues (
+                                    in valuesSubstring,
+                                    in valueRanges,
+                                    in originalString,
+                                    out ReadOnlySpan<char> rSpan,
+                                    out ReadOnlySpan<char> gSpan,
+                                    out ReadOnlySpan<char> bSpan
+                                   );
+                    ReadOnlySpan<char> aSpan = valuesSubstring [valueRanges [3]];
+
+                    if (!aSpan.IsAllAsciiDigits ())
+                    {
+                        throw new ColorParseException (
+                                                       in originalString,
+                                                       "Value was not composed entirely of decimal digits.",
+                                                       in aSpan,
+                                                       nameof (A)
+                                                      );
+                    }
+
+                    return new Color (int.Parse (rSpan), int.Parse (gSpan), int.Parse (bSpan), int.Parse (aSpan));
+                }
+                default:
+                    throw new ColorParseException (
+                                                   in originalString,
+                                                   $"Wrong number of values. Expected 3 or 4 decimal integers. Got {rangeCount}.",
+                                                   in originalString
+                                                  );
+            }
+
+            [Pure]
+            [SkipLocalsInit]
+            static void ParseRgbValues (
+                in ReadOnlySpan<char> valuesString,
+                in Span<Range> valueComponentRanges,
+                in ReadOnlySpan<char> originalString,
+                out ReadOnlySpan<char> rSpan,
+                out ReadOnlySpan<char> gSpan,
+                out ReadOnlySpan<char> bSpan
+            )
+            {
+                rSpan = valuesString [valueComponentRanges [0]];
+
+                if (!rSpan.IsAllAsciiDigits ())
+                {
+                    throw new ColorParseException (
+                                                   in originalString,
+                                                   "Value was not composed entirely of decimal digits.",
+                                                   in rSpan,
+                                                   nameof (R)
+                                                  );
+                }
+
+                gSpan = valuesString [valueComponentRanges [1]];
+
+                if (!gSpan.IsAllAsciiDigits ())
+                {
+                    throw new ColorParseException (
+                                                   in originalString,
+                                                   "Value was not composed entirely of decimal digits.",
+                                                   in gSpan,
+                                                   nameof (G)
+                                                  );
+                }
+
+                bSpan = valuesString [valueComponentRanges [2]];
+
+                if (!bSpan.IsAllAsciiDigits ())
+                {
+                    throw new ColorParseException (
+                                                   in originalString,
+                                                   "Value was not composed entirely of decimal digits.",
+                                                   in bSpan,
+                                                   nameof (B)
+                                                  );
+                }
+            }
+        }
+    }
+
+    /// <summary>Converts the provided <see langword="string"/> to a new <see cref="Color"/> value.</summary>
+    /// <param name="text">
+    ///     The text to analyze. Formats supported are "#RGB", "#RRGGBB", "#ARGB", "#AARRGGBB", "rgb(r,g,b)",
+    ///     "rgb(r,g,b,a)", "rgba(r,g,b)", "rgba(r,g,b,a)", and any of the <see cref="GetClosestNamedColor (Color)"/> string
+    ///     values.
+    /// </param>
+    /// <param name="formatProvider">
+    ///     Optional <see cref="IFormatProvider"/> to provide formatting services for the input text.
+    ///     <br/> Defaults to <see cref="CultureInfo.InvariantCulture"/> if <see langword="null"/>.
+    /// </param>
+    /// <param name="result">
+    ///     The parsed value, if successful, or <see langword="default"/>(<see cref="Color"/>), if
+    ///     unsuccessful.
+    /// </param>
+    /// <returns>A <see langword="bool"/> value indicating whether parsing was successful.</returns>
+    /// <remarks>While <see cref="Color"/> supports the alpha channel <see cref="A"/>, Terminal.Gui does not.</remarks>
+    [Pure]
+    [SkipLocalsInit]
+    public static bool TryParse (string? text, IFormatProvider? formatProvider, out Color result)
+    {
+        return TryParse (
+                         text.AsSpan (),
+                         formatProvider ?? CultureInfo.InvariantCulture,
+                         out result
+                        );
+    }
+
+    /// <summary>
+    ///     Converts the provided <see cref="ReadOnlySpan{T}"/> of <see langword="char"/> to a new <see cref="Color"/>
+    ///     value.
+    /// </summary>
+    /// <param name="text">
+    ///     The text to analyze. Formats supported are "#RGB", "#RRGGBB", "#ARGB", "#AARRGGBB", "rgb(r,g,b)",
+    ///     "rgb(r,g,b,a)", "rgba(r,g,b)", "rgba(r,g,b,a)", and any of the <see cref="GetClosestNamedColor (Color)"/> string
+    ///     values.
+    /// </param>
+    /// <param name="formatProvider">
+    ///     If specified and not <see langword="null"/>, will be passed to
+    ///     <see cref="Parse(System.ReadOnlySpan{char},System.IFormatProvider?)"/>.
+    /// </param>
+    /// <param name="color">
+    ///     The parsed value, if successful, or <see langword="default"/>(<see cref="Color"/>), if
+    ///     unsuccessful.
+    /// </param>
+    /// <returns>A <see langword="bool"/> value indicating whether parsing was successful.</returns>
+    /// <remarks>While <see cref="Color"/> supports the alpha channel <see cref="A"/>, Terminal.Gui does not.</remarks>
+    [Pure]
+    [SkipLocalsInit]
+    public static bool TryParse (ReadOnlySpan<char> text, IFormatProvider? formatProvider, out Color color)
+    {
+        try
+        {
+            Color c = Parse (text, formatProvider);
+            color = c;
+
+            return true;
+        }
+        catch (ColorParseException)
+        {
+            color = default (Color);
+
+            return false;
+        }
+    }
+
+    /// <inheritdoc/>
+    /// <remarks>
+    ///     Use of this method involves a stack allocation of <paramref name="utf8Destination"/>.Length * 2 bytes. Use of
+    ///     the overload taking a char span is recommended.
+    /// </remarks>
+    [SkipLocalsInit]
+    public bool TryFormat (
+        Span<byte> utf8Destination,
+        out int bytesWritten,
+        ReadOnlySpan<char> format,
+        IFormatProvider? provider
+    )
+    {
+        Span<char> charDestination = stackalloc char [utf8Destination.Length * 2];
+
+        if (TryFormat (charDestination, out int charsWritten, format, provider))
+        {
+            Encoding.UTF8.GetBytes (charDestination, utf8Destination);
+            bytesWritten = charsWritten / 2;
+
+            return true;
+        }
+
+        utf8Destination.Clear ();
+        bytesWritten = 0;
+
+        return false;
+    }
+
+    /// <inheritdoc/>
+    [Pure]
+    [SkipLocalsInit]
+    public static Color Parse (ReadOnlySpan<byte> utf8Text, IFormatProvider? provider) { return Parse (Encoding.UTF8.GetString (utf8Text), provider); }
+
+    /// <inheritdoc/>
+    [Pure]
+    [SkipLocalsInit]
+    public static bool TryParse (ReadOnlySpan<byte> utf8Text, IFormatProvider? provider, out Color result)
+    {
+        return TryParse (Encoding.UTF8.GetString (utf8Text), provider, out result);
+    }
+
+    /// <summary>Converts the color to a string representation.</summary>
+    /// <remarks>
+    ///     <para>If the color is a named color, the name is returned. Otherwise, the color is returned as a hex string.</para>
+    ///     <para><see cref="A"/> (Alpha channel) is ignored and the returned string will not include it for this overload.</para>
+    /// </remarks>
+    /// <returns>The string representation of this value in #RRGGBB format.</returns>
+    [Pure]
+    [SkipLocalsInit]
+    public override string ToString ()
+    {
+        // If Values has an exact match with a named color (in _colorNames), use that.
+        return ColorExtensions.ColorToNameMap.TryGetValue (this, out ColorName colorName)
+                   ? Enum.GetName (typeof (ColorName), colorName) ?? $"#{R:X2}{G:X2}{B:X2}"
+                   : // Otherwise return as an RGB hex value.
+                   $"#{R:X2}{G:X2}{B:X2}";
+    }
+
+    /// <summary>Converts the provided string to a new <see cref="Color"/> instance.</summary>
+    /// <param name="text">
+    ///     The text to analyze. Formats supported are "#RGB", "#RRGGBB", "#ARGB", "#AARRGGBB", "rgb(r,g,b)",
+    ///     "rgb(r,g,b,a)", "rgba(r,g,b)", "rgba(r,g,b,a)", and any of the <see cref="Gui.ColorName"/> string values.
+    /// </param>
+    /// <param name="color">The parsed value.</param>
+    /// <returns>A boolean value indicating whether parsing was successful.</returns>
+    /// <remarks>While <see cref="Color"/> supports the alpha channel <see cref="A"/>, Terminal.Gui does not.</remarks>
+    public static bool TryParse (string text, [NotNullWhen (true)] out Color? color)
+    {
+        if (TryParse (text.AsSpan (), null, out Color c))
+        {
+            color = c;
+
+            return true;
+        }
+
+        color = null;
+
+        return false;
+    }
+}

+ 97 - 0
Terminal.Gui/Drawing/Color.Operators.cs

@@ -0,0 +1,97 @@
+#nullable enable
+using System.Diagnostics.Contracts;
+using System.Numerics;
+
+namespace Terminal.Gui;
+
+public readonly partial record struct Color
+{
+    /// <inheritdoc/>
+    /// <returns>
+    ///     A <see cref="Color"/> <see langword="struct"/> with all values set to <see cref="byte.MaxValue"/>, meaning
+    ///     white.
+    /// </returns>
+    public static Color MaxValue => new (uint.MaxValue);
+
+    /// <inheritdoc/>
+    /// <returns>A <see cref="Color"/> <see langword="struct"/> with all values set to zero.</returns>
+    /// <remarks>
+    ///     Though this returns a <see cref="Color"/> with <see cref="A"/>, <see cref="R"/>, <see cref="G"/>, and
+    ///     <see cref="B"/> all set to zero, Terminal.Gui will treat it as black, because the alpha channel is not supported.
+    /// </remarks>
+    public static Color MinValue => new (uint.MinValue);
+
+    /// <inheritdoc/>
+    [Pure]
+    public override int GetHashCode () { return Rgba.GetHashCode (); }
+
+    /// <summary>
+    ///     Implicit conversion from <see cref="Color"/> to <see cref="Vector3"/> via
+    ///     <see cref="Vector3(float,float,float)"/> where ( <see cref="Vector3.X"/>, <see cref="Vector3.Y"/>,
+    ///     <see cref="Vector3.Z"/>) is (R,G,B).
+    /// </summary>
+    /// <remarks>
+    ///     This cast is narrowing and drops the alpha channel.
+    ///     <para/>
+    ///     Use <see cref="implicit operator Vector4(Color)"/> to maintain full value.
+    /// </remarks>
+    [Pure]
+    public static explicit operator Vector3 (Color color) { return new Vector3 (color.R, color.G, color.B); }
+
+    /// <summary>
+    ///     Implicit conversion from <see langword="int"/> to <see cref="Color"/>, via the <see cref="Color(int)"/>
+    ///     costructor.
+    /// </summary>
+    [Pure]
+    public static implicit operator Color (int rgba) { return new Color (rgba); }
+
+    /// <summary>
+    ///     Implicit conversion from <see langword="uint"/> to <see cref="Color"/>, via the <see cref="Color(uint)"/>
+    ///     costructor.
+    /// </summary>
+    [Pure]
+    public static implicit operator Color (uint u) { return new Color (u); }
+
+    /// <summary>
+    ///     Implicit conversion from <see cref="GetClosestNamedColor (Color)"/> to <see cref="Color"/> via lookup from
+    ///     <see cref="ColorExtensions.ColorNameToColorMap"/>.
+    /// </summary>
+    [Pure]
+    public static implicit operator Color (ColorName colorName) { return ColorExtensions.ColorNameToColorMap [colorName]; }
+
+    /// <summary>
+    ///     Implicit conversion from <see cref="Vector4"/> to <see cref="Color"/>, where (<see cref="Vector4.X"/>,
+    ///     <see cref="Vector4.Y"/>, <see cref="Vector4.Z"/>, <see cref="Vector4.W"/>) is (<see cref="A"/>,<see cref="R"/>,
+    ///     <see cref="G"/>,<see cref="B"/>), via <see cref="Color(int,int,int,int)"/>.
+    /// </summary>
+    [Pure]
+    public static implicit operator Color (Vector4 v) { return new Color ((byte)v.X, (byte)v.Y, (byte)v.Z, (byte)v.W); }
+
+    /// <summary>
+    ///     Implicit conversion from <see cref="Vector3"/>, where <see cref="Vector3.X"/> = <see cref="R"/>,
+    ///     <see cref="Vector3.Y"/> = <see cref="G"/>, and <see cref="Vector3.Z"/> = <see cref="B"/>.
+    /// </summary>
+    [Pure]
+    public static implicit operator Color (Vector3 v) { return new Color ((byte)v.X, (byte)v.Y, (byte)v.Z); }
+
+    /// <summary>
+    ///     Implicit conversion from <see cref="Color"/> to <see langword="int"/> by returning the value of the
+    ///     <see cref="Rgba"/> field.
+    /// </summary>
+    [Pure]
+    public static implicit operator int (Color color) { return color.Rgba; }
+
+    /// <summary>
+    ///     Implicit conversion from <see cref="Color"/> to <see langword="uint"/> by returning the value of the
+    ///     <see cref="Argb"/> field.
+    /// </summary>
+    [Pure]
+    public static implicit operator uint (Color color) { return color.Argb; }
+
+    /// <summary>
+    ///     Implicit conversion to <see cref="Vector3"/>, where <see cref="Vector3.X"/> = <see cref="R"/>,
+    ///     <see cref="Vector3.Y"/> = <see cref="G"/>, and <see cref="Vector3.Z"/> = <see cref="B"/>.
+    /// </summary>
+    [Pure]
+    public static implicit operator Vector4 (Color color) { return new Vector4 (color.R, color.G, color.B, color.A); }
+}

+ 283 - 882
Terminal.Gui/Drawing/Color.cs

@@ -1,890 +1,291 @@
-global using Attribute = Terminal.Gui.Attribute;
-using System;
-using System.Collections.Generic;
-using System.Collections.Immutable;
-using System.Diagnostics.CodeAnalysis;
-using System.Linq;
+#nullable enable
+using System.Collections.Frozen;
+using System.Diagnostics.Contracts;
+using System.Globalization;
+using System.Numerics;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
 using System.Text.Json.Serialization;
-using System.Text.RegularExpressions;
 
 namespace Terminal.Gui;
 
 /// <summary>
-/// Defines the 16 legacy color names and values that can be used to set the
-/// foreground and background colors in Terminal.Gui apps. Used with <see cref="Color"/>.
-/// </summary>
-/// <remarks>
-///         <para>
-///         These colors match the 16 colors defined for ANSI escape sequences for 4-bit (16) colors.
-///         </para>
-///         <para>
-///         For terminals that support 24-bit color (TrueColor), the RGB values for each of these colors can be configured
-///         using the
-///         <see cref="Color.Colors"/> property.
-///         </para>
-/// </remarks>
-public enum ColorName {
-	/// <summary>
-	/// The black color. ANSI escape sequence: <c>\u001b[30m</c>.
-	/// </summary>
-	Black,
-
-	/// <summary>
-	/// The blue color. ANSI escape sequence: <c>\u001b[34m</c>.
-	/// </summary>
-	Blue,
-
-	/// <summary>
-	/// The green color. ANSI escape sequence: <c>\u001b[32m</c>.
-	/// </summary>
-	Green,
-
-	/// <summary>
-	/// The cyan color. ANSI escape sequence: <c>\u001b[36m</c>.
-	/// </summary>
-	Cyan,
-
-	/// <summary>
-	/// The red color. ANSI escape sequence: <c>\u001b[31m</c>.
-	/// </summary>
-	Red,
-
-	/// <summary>
-	/// The magenta color. ANSI escape sequence: <c>\u001b[35m</c>.
-	/// </summary>
-	Magenta,
-
-	/// <summary>
-	/// The yellow color (also known as Brown). ANSI escape sequence: <c>\u001b[33m</c>.
-	/// </summary>
-	Yellow,
-
-	/// <summary>
-	/// The gray color (also known as White). ANSI escape sequence: <c>\u001b[37m</c>.
-	/// </summary>
-	Gray,
-
-	/// <summary>
-	/// The dark gray color (also known as Bright Black). ANSI escape sequence: <c>\u001b[30;1m</c>.
-	/// </summary>
-	DarkGray,
-
-	/// <summary>
-	/// The bright blue color. ANSI escape sequence: <c>\u001b[34;1m</c>.
-	/// </summary>
-	BrightBlue,
-
-	/// <summary>
-	/// The bright green color. ANSI escape sequence: <c>\u001b[32;1m</c>.
-	/// </summary>
-	BrightGreen,
-
-	/// <summary>
-	/// The bright cyan color. ANSI escape sequence: <c>\u001b[36;1m</c>.
-	/// </summary>
-	BrightCyan,
-
-	/// <summary>
-	/// The bright red color. ANSI escape sequence: <c>\u001b[31;1m</c>.
-	/// </summary>
-	BrightRed,
-
-	/// <summary>
-	/// The bright magenta color. ANSI escape sequence: <c>\u001b[35;1m</c>.
-	/// </summary>
-	BrightMagenta,
-
-	/// <summary>
-	/// The bright yellow color. ANSI escape sequence: <c>\u001b[33;1m</c>.
-	/// </summary>
-	BrightYellow,
-
-	/// <summary>
-	/// The White color (also known as Bright White). ANSI escape sequence: <c>\u001b[37;1m</c>.
-	/// </summary>
-	White
-}
-
-/// <summary>
-/// The 16 foreground color codes used by ANSI Esc sequences for 256 color terminals. Add 10 to these values for background
-/// color.
-/// </summary>
-public enum AnsiColorCode {
-	/// <summary>
-	/// The ANSI color code for Black.
-	/// </summary>
-	BLACK = 30,
-
-	/// <summary>
-	/// The ANSI color code for Red.
-	/// </summary>
-	RED = 31,
-
-	/// <summary>
-	/// The ANSI color code for Green.
-	/// </summary>
-	GREEN = 32,
-
-	/// <summary>
-	/// The ANSI color code for Yellow.
-	/// </summary>
-	YELLOW = 33,
-
-	/// <summary>
-	/// The ANSI color code for Blue.
-	/// </summary>
-	BLUE = 34,
-
-	/// <summary>
-	/// The ANSI color code for Magenta.
-	/// </summary>
-	MAGENTA = 35,
-
-	/// <summary>
-	/// The ANSI color code for Cyan.
-	/// </summary>
-	CYAN = 36,
-
-	/// <summary>
-	/// The ANSI color code for White.
-	/// </summary>
-	WHITE = 37,
-
-	/// <summary>
-	/// The ANSI color code for Bright Black.
-	/// </summary>
-	BRIGHT_BLACK = 90,
-
-	/// <summary>
-	/// The ANSI color code for Bright Red.
-	/// </summary>
-	BRIGHT_RED = 91,
-
-	/// <summary>
-	/// The ANSI color code for Bright Green.
-	/// </summary>
-	BRIGHT_GREEN = 92,
-
-	/// <summary>
-	/// The ANSI color code for Bright Yellow.
-	/// </summary>
-	BRIGHT_YELLOW = 93,
-
-	/// <summary>
-	/// The ANSI color code for Bright Blue.
-	/// </summary>
-	BRIGHT_BLUE = 94,
-
-	/// <summary>
-	/// The ANSI color code for Bright Magenta.
-	/// </summary>
-	BRIGHT_MAGENTA = 95,
-
-	/// <summary>
-	/// The ANSI color code for Bright Cyan.
-	/// </summary>
-	BRIGHT_CYAN = 96,
-
-	/// <summary>
-	/// The ANSI color code for Bright White.
-	/// </summary>
-	BRIGHT_WHITE = 97
-}
-
-/// <summary>
-/// Represents a 24-bit color. Provides automatic mapping between the legacy 4-bit (16 color) system and 24-bit colors (see
-/// <see cref="ColorName"/>). Used with <see cref="Attribute"/>.
+///     Represents a 24-bit color encoded in ARGB32 format.
+///     <para/>
 /// </summary>
+/// <seealso cref="Attribute"/>
+/// <seealso cref="ColorExtensions"/>
+/// <seealso cref="ColorName"/>
 [JsonConverter (typeof (ColorJsonConverter))]
-public readonly struct Color : IEquatable<Color> {
-
-	// TODO: Make this map configurable via ConfigurationManager
-	// TODO: This does not need to be a Dictionary, but can be an 16 element array.
-	/// <summary>
-	/// Maps legacy 16-color values to the corresponding 24-bit RGB value.
-	/// </summary>
-	internal static ImmutableDictionary<Color, ColorName> _colorToNameMap = new Dictionary<Color, ColorName> {
-		// using "Windows 10 Console/PowerShell 6" here: https://i.stack.imgur.com/9UVnC.png
-		// See also: https://en.wikipedia.org/wiki/ANSI_escape_code
-		{ new Color (12, 12, 12), ColorName.Black },
-		{ new Color (0, 55, 218), ColorName.Blue },
-		{ new Color (19, 161, 14), ColorName.Green },
-		{ new Color (58, 150, 221), ColorName.Cyan },
-		{ new Color (197, 15, 31), ColorName.Red },
-		{ new Color (136, 23, 152), ColorName.Magenta },
-		{ new Color (128, 64, 32), ColorName.Yellow },
-		{ new Color (204, 204, 204), ColorName.Gray },
-		{ new Color (118, 118, 118), ColorName.DarkGray },
-		{ new Color (59, 120, 255), ColorName.BrightBlue },
-		{ new Color (22, 198, 12), ColorName.BrightGreen },
-		{ new Color (97, 214, 214), ColorName.BrightCyan },
-		{ new Color (231, 72, 86), ColorName.BrightRed },
-		{ new Color (180, 0, 158), ColorName.BrightMagenta },
-		{ new Color (249, 241, 165), ColorName.BrightYellow },
-		{ new Color (242, 242, 242), ColorName.White }
-	}.ToImmutableDictionary ();
-
-
-	/// <summary>
-	/// Defines the 16 legacy color names and values that can be used to set the
-	/// </summary>
-	internal static ImmutableDictionary<ColorName, AnsiColorCode> _colorNameToAnsiColorMap = new Dictionary<ColorName, AnsiColorCode> {
-		{ ColorName.Black, AnsiColorCode.BLACK },
-		{ ColorName.Blue, AnsiColorCode.BLUE },
-		{ ColorName.Green, AnsiColorCode.GREEN },
-		{ ColorName.Cyan, AnsiColorCode.CYAN },
-		{ ColorName.Red, AnsiColorCode.RED },
-		{ ColorName.Magenta, AnsiColorCode.MAGENTA },
-		{ ColorName.Yellow, AnsiColorCode.YELLOW },
-		{ ColorName.Gray, AnsiColorCode.WHITE },
-		{ ColorName.DarkGray, AnsiColorCode.BRIGHT_BLACK },
-		{ ColorName.BrightBlue, AnsiColorCode.BRIGHT_BLUE },
-		{ ColorName.BrightGreen, AnsiColorCode.BRIGHT_GREEN },
-		{ ColorName.BrightCyan, AnsiColorCode.BRIGHT_CYAN },
-		{ ColorName.BrightRed, AnsiColorCode.BRIGHT_RED },
-		{ ColorName.BrightMagenta, AnsiColorCode.BRIGHT_MAGENTA },
-		{ ColorName.BrightYellow, AnsiColorCode.BRIGHT_YELLOW },
-		{ ColorName.White, AnsiColorCode.BRIGHT_WHITE }
-	}.ToImmutableDictionary ();
-
-	/// <summary>
-	/// Initializes a new instance of the <see cref="Color"/> class.
-	/// </summary>
-	/// <param name="red">The red 8-bits.</param>
-	/// <param name="green">The green 8-bits.</param>
-	/// <param name="blue">The blue 8-bits.</param>
-	/// <param name="alpha">Optional; defaults to 0xFF. The Alpha channel is not supported by Terminal.Gui.</param>
-	public Color (int red, int green, int blue, int alpha = 0xFF)
-	{
-		R = red;
-		G = green;
-		B = blue;
-		A = alpha;
-	}
-
-	/// <summary>
-	/// Initializes a new instance of the <see cref="Color"/> class with an encoded 24-bit color value.
-	/// </summary>
-	/// <param name="rgba">The encoded 24-bit color value (see <see cref="Rgba"/>).</param>
-	public Color (int rgba)
-	{
-		A = (byte)(rgba >> 24 & 0xFF);
-		R = (byte)(rgba >> 16 & 0xFF);
-		G = (byte)(rgba >> 8 & 0xFF);
-		B = (byte)(rgba & 0xFF);
-	}
-
-	/// <summary>
-	/// Initializes a new instance of the <see cref="Color"/> color from a legacy 16-color value.
-	/// </summary>
-	/// <param name="colorName">The 16-color value.</param>
-	public Color (ColorName colorName)
-	{
-		var c = FromColorName (colorName);
-		R = c.R;
-		G = c.G;
-		B = c.B;
-		A = c.A;
-	}
-
-	/// <summary>
-	/// Initializes a new instance of the <see cref="Color"/> color from string. See <see cref="TryParse(string, out Color)"/>
-	/// for details.
-	/// </summary>
-	/// <param name="colorString"></param>
-	/// <exception cref="Exception"></exception>
-	public Color (string colorString)
-	{
-		if (!TryParse (colorString, out var c)) {
-			throw new ArgumentOutOfRangeException (nameof (colorString));
-		}
-		R = c.R;
-		G = c.G;
-		B = c.B;
-		A = c.A;
-	}
-
-	/// <summary>
-	/// Initializes a new instance of the <see cref="Color"/>.
-	/// </summary>
-	public Color ()
-	{
-		R = 0;
-		G = 0;
-		B = 0;
-		A = 0xFF;
-	}
-
-	/// <summary>
-	/// Red color component.
-	/// </summary>
-	public int R { get; }
-
-	/// <summary>
-	/// Green color component.
-	/// </summary>
-	public int G { get; }
-
-	/// <summary>
-	/// Blue color component.
-	/// </summary>
-	public int B { get; }
-
-	/// <summary>
-	/// Alpha color component.
-	/// </summary>
-	/// <remarks>
-	/// The Alpha channel is not supported by Terminal.Gui.
-	/// </remarks>
-	public int A { get; } // Not currently supported; here for completeness.
-
-	/// <summary>
-	/// Gets or sets the color value encoded as ARGB32.
-	/// <code>
-	/// (&lt;see cref="A"/&gt; &lt;&lt; 24) | (&lt;see cref="R"/&gt; &lt;&lt; 16) | (&lt;see cref="G"/&gt; &lt;&lt; 8) | &lt;see cref="B"/&gt;
-	/// </code>
-	/// </summary>
-	[JsonIgnore]
-	public int Rgba => A << 24 | R << 16 | G << 8 | B;
-
-	/// <summary>
-	/// Gets or sets the 24-bit color value for each of the legacy 16-color values.
-	/// </summary>
-	[SerializableConfigurationProperty (Scope = typeof (SettingsScope), OmitClassName = true)]
-	public static Dictionary<ColorName, string> Colors {
-		get =>
-			// Transform _colorToNameMap into a Dictionary<ColorNames,string>
-			_colorToNameMap.ToDictionary (kvp => kvp.Value, kvp => $"#{kvp.Key.R:X2}{kvp.Key.G:X2}{kvp.Key.B:X2}");
-		set {
-			// Transform Dictionary<ColorNames,string> into _colorToNameMap
-			var newMap = value.ToDictionary (kvp => new Color (kvp.Value), kvp => {
-				if (Enum.TryParse<ColorName> (kvp.Key.ToString (), true, out var colorName)) {
-					return colorName;
-				}
-				throw new ArgumentException ($"Invalid color name: {kvp.Key}");
-			});
-			_colorToNameMap = newMap.ToImmutableDictionary ();
-		}
-	}
-
-	/// <summary>
-	/// Gets the <see cref="Color"/> using a legacy 16-color <see cref="Gui.ColorName"/> value.
-	/// <see langword="get"/> will return the closest 16 color match to the true color when no exact value is found.
-	/// </summary>
-	/// <remarks>
-	/// Get returns the <see cref="ColorName"/> of the closest 24-bit color value. Set sets the RGB value using a hard-coded
-	/// map.
-	/// </remarks>
-	[JsonIgnore]
-	public ColorName ColorName => FindClosestColor (this);
-
-	/// <summary>
-	/// Gets the <see cref="Color"/> using a legacy 16-color <see cref="Gui.ColorName"/> value.
-	/// <see langword="get"/> will return the closest 16 color match to the true color when no exact value is found.
-	/// </summary>
-	/// <remarks>
-	/// Get returns the <see cref="ColorName"/> of the closest 24-bit color value. Set sets the RGB value using a hard-coded
-	/// map.
-	/// </remarks>
-	[JsonIgnore]
-	public AnsiColorCode AnsiColorCode => _colorNameToAnsiColorMap [ColorName];
-
-	/// <summary>
-	/// Converts a legacy <see cref="Gui.ColorName"/> to a 24-bit <see cref="Color"/>.
-	/// </summary>
-	/// <param name="colorName">The <see cref="Color"/> to convert.</param>
-	/// <returns></returns>
-	static Color FromColorName (ColorName colorName) => _colorToNameMap.FirstOrDefault (x => x.Value == colorName).Key;
-
-	// Iterates through the entries in the _colorNames dictionary, calculates the
-	// Euclidean distance between the input color and each dictionary color in RGB space,
-	// and keeps track of the closest entry found so far. The function returns a KeyValuePair
-	// representing the closest color entry and its associated color name.
-	internal static ColorName FindClosestColor (Color inputColor)
-	{
-		var closestColor = ColorName.Black; // Default to Black
-		var closestDistance = double.MaxValue;
-
-		foreach (var colorEntry in _colorToNameMap) {
-			var distance = CalculateColorDistance (inputColor, colorEntry.Key);
-			if (distance < closestDistance) {
-				closestDistance = distance;
-				closestColor = colorEntry.Value;
-			}
-		}
-
-		return closestColor;
-	}
-
-	static double CalculateColorDistance (Color color1, Color color2)
-	{
-		// Calculate the Euclidean distance between two colors
-		var deltaR = color1.R - (double)color2.R;
-		var deltaG = color1.G - (double)color2.G;
-		var deltaB = color1.B - (double)color2.B;
-
-		return Math.Sqrt (deltaR * deltaR + deltaG * deltaG + deltaB * deltaB);
-	}
-
-	/// <summary>
-	/// Converts the provided string to a new <see cref="Color"/> instance.
-	/// </summary>
-	/// <param name="text">
-	/// The text to analyze. Formats supported are
-	/// "#RGB", "#RRGGBB", "#RGBA", "#RRGGBBAA", "rgb(r,g,b)", "rgb(r,g,b,a)", and any of the
-	/// <see cref="Gui.ColorName"/>.
-	/// </param>
-	/// <param name="color">The parsed value.</param>
-	/// <returns>A boolean value indicating whether parsing was successful.</returns>
-	/// <remarks>
-	/// While <see cref="Color"/> supports the alpha channel <see cref="A"/>, Terminal.Gui does not.
-	/// </remarks>
-	public static bool TryParse (string text, [NotNullWhen (true)] out Color color)
-	{
-		// empty color
-		if (string.IsNullOrEmpty (text)) {
-			color = new Color ();
-			return false;
-		}
-
-		// #RRGGBB, #RGB
-		if (text [0] == '#' && text.Length is 7 or 4) {
-			if (text.Length == 7) {
-				var r = Convert.ToInt32 (text.Substring (1, 2), 16);
-				var g = Convert.ToInt32 (text.Substring (3, 2), 16);
-				var b = Convert.ToInt32 (text.Substring (5, 2), 16);
-				color = new Color (r, g, b);
-			} else {
-				var rText = char.ToString (text [1]);
-				var gText = char.ToString (text [2]);
-				var bText = char.ToString (text [3]);
-
-				var r = Convert.ToInt32 (rText + rText, 16);
-				var g = Convert.ToInt32 (gText + gText, 16);
-				var b = Convert.ToInt32 (bText + bText, 16);
-				color = new Color (r, g, b);
-			}
-			return true;
-		}
-
-		// #RRGGBB, #RGBA
-		if (text [0] == '#' && text.Length is 8 or 5) {
-			if (text.Length == 7) {
-				var r = Convert.ToInt32 (text.Substring (1, 2), 16);
-				var g = Convert.ToInt32 (text.Substring (3, 2), 16);
-				var b = Convert.ToInt32 (text.Substring (5, 2), 16);
-				var a = Convert.ToInt32 (text.Substring (7, 2), 16);
-				color = new Color (a, r, g, b);
-			} else {
-				var rText = char.ToString (text [1]);
-				var gText = char.ToString (text [2]);
-				var bText = char.ToString (text [3]);
-				var aText = char.ToString (text [4]);
-
-				var r = Convert.ToInt32 (aText + aText, 16);
-				var g = Convert.ToInt32 (rText + rText, 16);
-				var b = Convert.ToInt32 (gText + gText, 16);
-				var a = Convert.ToInt32 (bText + bText, 16);
-				color = new Color (r, g, b, a);
-			}
-			return true;
-		}
-
-		// rgb(r,g,b)
-		var match = Regex.Match (text, @"rgb\((\d+),(\d+),(\d+)\)");
-		if (match.Success) {
-			var r = int.Parse (match.Groups [1].Value);
-			var g = int.Parse (match.Groups [2].Value);
-			var b = int.Parse (match.Groups [3].Value);
-			color = new Color (r, g, b);
-			return true;
-		}
-
-		// rgb(r,g,b,a)
-		match = Regex.Match (text, @"rgb\((\d+),(\d+),(\d+),(\d+)\)");
-		if (match.Success) {
-			var r = int.Parse (match.Groups [1].Value);
-			var g = int.Parse (match.Groups [2].Value);
-			var b = int.Parse (match.Groups [3].Value);
-			var a = int.Parse (match.Groups [4].Value);
-			color = new Color (r, g, b, a);
-			return true;
-		}
-
-		if (Enum.TryParse<ColorName> (text, true, out var colorName)) {
-			color = new Color (colorName);
-			return true;
-		}
-
-		color = new Color ();
-		return false;
-	}
-
-	/// <summary>
-	/// Converts the color to a string representation.
-	/// </summary>
-	/// <remarks>
-	///         <para>
-	///         If the color is a named color, the name is returned. Otherwise, the color is returned as a hex string.
-	///         </para>
-	///         <para>
-	///         <see cref="A"/> (Alpha channel) is ignored and the returned string will not include it.
-	///         </para>
-	/// </remarks>
-	/// <returns></returns>
-	public override string ToString ()
-	{
-		// If Values has an exact match with a named color (in _colorNames), use that.
-		if (_colorToNameMap.TryGetValue (this, out var colorName)) {
-			return Enum.GetName (typeof (ColorName), colorName);
-		}
-		// Otherwise return as an RGB hex value.
-		return $"#{R:X2}{G:X2}{B:X2}";
-	}
-
-	#region Legacy Color Names
-	/// <summary>
-	/// The black color.
-	/// </summary>
-	public const ColorName Black = ColorName.Black;
-
-	/// <summary>
-	/// The blue color.
-	/// </summary>
-	public const ColorName Blue = ColorName.Blue;
-
-	/// <summary>
-	/// The green color.
-	/// </summary>
-	public const ColorName Green = ColorName.Green;
-
-	/// <summary>
-	/// The cyan color.
-	/// </summary>
-	public const ColorName Cyan = ColorName.Cyan;
-
-	/// <summary>
-	/// The red color.
-	/// </summary>
-	public const ColorName Red = ColorName.Red;
-
-	/// <summary>
-	/// The magenta color.
-	/// </summary>
-	public const ColorName Magenta = ColorName.Magenta;
-
-	/// <summary>
-	/// The yellow color.
-	/// </summary>
-	public const ColorName Yellow = ColorName.Yellow;
-
-	/// <summary>
-	/// The gray color.
-	/// </summary>
-	public const ColorName Gray = ColorName.Gray;
-
-	/// <summary>
-	/// The dark gray color.
-	/// </summary>
-	public const ColorName DarkGray = ColorName.DarkGray;
-
-	/// <summary>
-	/// The bright bBlue color.
-	/// </summary>
-	public const ColorName BrightBlue = ColorName.BrightBlue;
-
-	/// <summary>
-	/// The bright green color.
-	/// </summary>
-	public const ColorName BrightGreen = ColorName.BrightGreen;
-
-	/// <summary>
-	/// The bright cyan color.
-	/// </summary>
-	public const ColorName BrightCyan = ColorName.BrightCyan;
-
-	/// <summary>
-	/// The bright red color.
-	/// </summary>
-	public const ColorName BrightRed = ColorName.BrightRed;
-
-	/// <summary>
-	/// The bright magenta color.
-	/// </summary>
-	public const ColorName BrightMagenta = ColorName.BrightMagenta;
-
-	/// <summary>
-	/// The bright yellow color.
-	/// </summary>
-	public const ColorName BrightYellow = ColorName.BrightYellow;
-
-	/// <summary>
-	/// The White color.
-	/// </summary>
-	public const ColorName White = ColorName.White;
-	#endregion
-
-	// TODO: Verify implict/explicit are correct for below
-	#region Operators
-	/// <summary>
-	/// Cast from int.
-	/// </summary>
-	/// <param name="rgba"></param>
-	public static implicit operator Color (int rgba) => new (rgba);
-
-	/// <summary>
-	/// Cast to int. 
-	/// </summary>
-	/// <param name="color"></param>
-	public static implicit operator int (Color color) => color.Rgba;
-
-	/// <summary>
-	/// Cast from <see cref="Gui.ColorName"/>. May fail if the color is not a named color.
-	/// </summary>
-	/// <param name="colorName"></param>
-	public static explicit operator Color (ColorName colorName) => new (colorName);
-
-	/// <summary>
-	/// Cast to <see cref="Gui.ColorName"/>. May fail if the color is not a named color.
-	/// </summary>
-	/// <param name="color"></param>
-	public static explicit operator ColorName (Color color) => color.ColorName;
-
-	/// <summary>
-	/// Equality operator for two <see cref="Color"/> objects..
-	/// </summary>
-	/// <param name="left"></param>
-	/// <param name="right"></param>
-	/// <returns></returns>
-	public static bool operator == (Color left, Color right) => left.Equals (right);
-
-	/// <summary>
-	/// Inequality operator for two <see cref="Color"/> objects.
-	/// </summary>
-	/// <param name="left"></param>
-	/// <param name="right"></param>
-	/// <returns></returns>
-	public static bool operator != (Color left, Color right) => !left.Equals (right);
-
-	/// <summary>
-	/// Equality operator for <see cref="Color"/> and <see cref="Gui.ColorName"/> objects.
-	/// </summary>
-	/// <param name="left"></param>
-	/// <param name="right"></param>
-	/// <returns></returns>
-	public static bool operator == (ColorName left, Color right) => left == right.ColorName;
-
-	/// <summary>
-	/// Inequality operator for <see cref="Color"/> and <see cref="Gui.ColorName"/> objects.
-	/// </summary>
-	/// <param name="left"></param>
-	/// <param name="right"></param>
-	/// <returns></returns>
-	public static bool operator != (ColorName left, Color right) => left != right.ColorName;
-
-	/// <summary>
-	/// Equality operator for <see cref="Color"/> and <see cref="Gui.ColorName"/> objects.
-	/// </summary>
-	/// <param name="left"></param>
-	/// <param name="right"></param>
-	/// <returns></returns>
-	public static bool operator == (Color left, ColorName right) => left.ColorName == right;
-
-	/// <summary>
-	/// Inequality operator for <see cref="Color"/> and <see cref="Gui.ColorName"/> objects.
-	/// </summary>
-	/// <param name="left"></param>
-	/// <param name="right"></param>
-	/// <returns></returns>
-	public static bool operator != (Color left, ColorName right) => left.ColorName != right;
-
-
-	/// <inheritdoc/>
-	public override bool Equals (object obj) => obj is Color other && Equals (other);
-
-	/// <inheritdoc/>
-	public bool Equals (Color other) => R == other.R &&
-	                                    G == other.G &&
-	                                    B == other.B &&
-	                                    A == other.A;
-
-	/// <inheritdoc/>
-	public override int GetHashCode () => HashCode.Combine (R, G, B, A);
-	#endregion
-}
-
-/// <summary>
-/// Attributes represent how text is styled when displayed in the terminal.
-/// </summary>
-/// <remarks>
-/// <see cref="Attribute"/> provides a platform independent representation of colors (and someday other forms of text
-/// styling).
-/// They encode both the foreground and the background color and are used in the <see cref="ColorScheme"/>
-/// class to define color schemes that can be used in an application.
-/// </remarks>
-[JsonConverter (typeof (AttributeJsonConverter))]
-public readonly struct Attribute : IEquatable<Attribute> {
-	/// <summary>
-	/// Default empty attribute.
-	/// </summary>
-	public static readonly Attribute Default = new (Color.White, Color.Black);
-
-	/// <summary>
-	/// The <see cref="ConsoleDriver"/>-specific color value.
-	/// </summary>
-	[JsonIgnore (Condition = JsonIgnoreCondition.Always)]
-	internal int PlatformColor { get; }
-
-	/// <summary>
-	/// The foreground color.
-	/// </summary>
-	[JsonConverter (typeof (ColorJsonConverter))]
-	public Color Foreground { get; }
-
-	/// <summary>
-	/// The background color.
-	/// </summary>
-	[JsonConverter (typeof (ColorJsonConverter))]
-	public Color Background { get; }
-
-	/// <summary>
-	/// Initializes a new instance with default values.
-	/// </summary>
-	public Attribute ()
-	{
-		PlatformColor = -1;
-		Foreground = new Color (Default.Foreground.ColorName);
-		Background = new Color (Default.Background.ColorName);
-	}
-
-	/// <summary>
-	/// Initializes a new instance from an existing instance.
-	/// </summary>
-	public Attribute (Attribute attr)
-	{
-		PlatformColor = -1;
-		Foreground = new Color (attr.Foreground.ColorName);
-		Background = new Color (attr.Background.ColorName);
-	}
-
-	/// <summary>
-	/// Initializes a new instance with platform specific color value.
-	/// </summary>
-	/// <param name="platformColor">Value.</param>
-	internal Attribute (int platformColor)
-	{
-		PlatformColor = platformColor;
-		Foreground = new Color (Default.Foreground.ColorName);
-		Background = new Color (Default.Background.ColorName);
-	}
-
-	/// <summary>
-	/// Initializes a new instance of the <see cref="Attribute"/> struct.
-	/// </summary>
-	/// <param name="platformColor">platform-dependent color value.</param>
-	/// <param name="foreground">Foreground</param>
-	/// <param name="background">Background</param>
-	internal Attribute (int platformColor, Color foreground, Color background)
-	{
-		Foreground = foreground;
-		Background = background;
-		PlatformColor = platformColor;
-	}
-
-	/// <summary>
-	/// Initializes a new instance of the <see cref="Attribute"/> struct.
-	/// </summary>
-	/// <param name="platformColor">platform-dependent color value.</param>
-	/// <param name="foreground">Foreground</param>
-	/// <param name="background">Background</param>
-	internal Attribute (int platformColor, ColorName foreground, ColorName background) : this (platformColor, new Color (foreground), new Color (background)) { }
-
-	/// <summary>
-	/// Initializes a new instance of the <see cref="Attribute"/> struct.
-	/// </summary>
-	/// <param name="foreground">Foreground</param>
-	/// <param name="background">Background</param>
-	public Attribute (Color foreground, Color background)
-	{
-		Foreground = foreground;
-		Background = background;
-
-		// TODO: Once CursesDriver supports truecolor all the PlatformColor stuff goes away
-		if (Application.Driver == null) {
-			PlatformColor = -1;
-			return;
-		}
-
-		var make = Application.Driver.MakeColor (foreground, background);
-		PlatformColor = make.PlatformColor;
-	}
-
-	/// <summary>
-	/// Initializes a new instance with a <see cref="ColorName"/> value. Both <see cref="Foreground"/> and
-	/// <see cref="Background"/> will be set to the specified color.
-	/// </summary>
-	/// <param name="colorName">Value.</param>
-	internal Attribute (ColorName colorName) : this (colorName, colorName) { }
-
-	/// <summary>
-	/// Initializes a new instance of the <see cref="Attribute"/> struct.
-	/// </summary>
-	/// <param name="foregroundName">Foreground</param>
-	/// <param name="backgroundName">Background</param>
-	public Attribute (ColorName foregroundName, ColorName backgroundName) : this (new Color (foregroundName), new Color (backgroundName)) { }
-
-
-	/// <summary>
-	/// Initializes a new instance of the <see cref="Attribute"/> struct.
-	/// </summary>
-	/// <param name="foregroundName">Foreground</param>
-	/// <param name="background">Background</param>
-	public Attribute (ColorName foregroundName, Color background) : this (new Color (foregroundName), background) { }
-
-	/// <summary>
-	/// Initializes a new instance of the <see cref="Attribute"/> struct.
-	/// </summary>
-	/// <param name="foreground">Foreground</param>
-	/// <param name="backgroundName">Background</param>
-	public Attribute (Color foreground, ColorName backgroundName) : this (foreground, new Color (backgroundName)) { }
-
-	/// <summary>
-	/// Initializes a new instance of the <see cref="Attribute"/> struct
-	/// with the same colors for the foreground and background.
-	/// </summary>
-	/// <param name="color">The color.</param>
-	public Attribute (Color color) : this (color, color) { }
-
-
-	/// <summary>
-	/// Compares two attributes for equality.
-	/// </summary>
-	/// <param name="left"></param>
-	/// <param name="right"></param>
-	/// <returns></returns>
-	public static bool operator == (Attribute left, Attribute right) => left.Equals (right);
-
-	/// <summary>
-	/// Compares two attributes for inequality.
-	/// </summary>
-	/// <param name="left"></param>
-	/// <param name="right"></param>
-	/// <returns></returns>
-	public static bool operator != (Attribute left, Attribute right) => !(left == right);
-
-	/// <inheritdoc/>
-	public override bool Equals (object obj) => obj is Attribute other && Equals (other);
-
-	/// <inheritdoc/>
-	public bool Equals (Attribute other) => PlatformColor == other.PlatformColor &&
-	                                        Foreground == other.Foreground &&
-	                                        Background == other.Background;
-
-	/// <inheritdoc/>
-	public override int GetHashCode () => HashCode.Combine (PlatformColor, Foreground, Background);
-
-	/// <inheritdoc/>
-	public override string ToString () =>
-		// Note: Unit tests are dependent on this format
-		$"[{Foreground},{Background}]";
+[StructLayout (LayoutKind.Explicit)]
+public readonly partial record struct Color : ISpanParsable<Color>, IUtf8SpanParsable<Color>, ISpanFormattable,
+                                              IUtf8SpanFormattable, IMinMaxValue<Color>
+{
+    /// <summary>The value of the alpha channel component</summary>
+    /// <remarks>
+    ///     The alpha channel is not currently supported, so the value of the alpha channel bits will not affect
+    ///     rendering.
+    /// </remarks>
+    [JsonIgnore]
+    [field: FieldOffset (3)]
+    public readonly byte A;
+
+    /// <summary>The value of this <see cref="Color"/> as a <see langword="uint"/> in ARGB32 format.</summary>
+    /// <remarks>
+    ///     The alpha channel is not currently supported, so the value of the alpha channel bits will not affect
+    ///     rendering.
+    /// </remarks>
+    [JsonIgnore]
+    [field: FieldOffset (0)]
+    public readonly uint Argb;
+
+    /// <summary>The value of the blue color component.</summary>
+    [JsonIgnore]
+    [field: FieldOffset (0)]
+    public readonly byte B;
+
+    /// <summary>The value of the green color component.</summary>
+    [JsonIgnore]
+    [field: FieldOffset (1)]
+    public readonly byte G;
+
+    /// <summary>The value of the red color component.</summary>
+    [JsonIgnore]
+    [field: FieldOffset (2)]
+    public readonly byte R;
+
+    /// <summary>The value of this <see cref="Color"/> encoded as a signed 32-bit integer in ARGB32 format.</summary>
+    [JsonIgnore]
+    [field: FieldOffset (0)]
+    public readonly int Rgba;
+
+    /// <summary>
+    ///     Initializes a new instance of the <see cref="Color"/> <see langword="struct"/> using the supplied component
+    ///     values.
+    /// </summary>
+    /// <param name="red">The red 8-bits.</param>
+    /// <param name="green">The green 8-bits.</param>
+    /// <param name="blue">The blue 8-bits.</param>
+    /// <param name="alpha">Optional; defaults to 0xFF. The Alpha channel is not supported by Terminal.Gui.</param>
+    /// <remarks>Alpha channel is not currently supported by Terminal.Gui.</remarks>
+    /// <exception cref="OverflowException">If the value of any parameter is greater than <see cref="byte.MaxValue"/>.</exception>
+    /// <exception cref="ArgumentOutOfRangeException">If the value of any parameter is negative.</exception>
+    public Color (int red = 0, int green = 0, int blue = 0, int alpha = byte.MaxValue)
+    {
+        ArgumentOutOfRangeException.ThrowIfNegative (red, nameof (red));
+        ArgumentOutOfRangeException.ThrowIfNegative (green, nameof (green));
+        ArgumentOutOfRangeException.ThrowIfNegative (blue, nameof (blue));
+        ArgumentOutOfRangeException.ThrowIfNegative (alpha, nameof (alpha));
+
+        A = Convert.ToByte (alpha);
+        R = Convert.ToByte (red);
+        G = Convert.ToByte (green);
+        B = Convert.ToByte (blue);
+    }
+
+    /// <summary>
+    ///     Initializes a new instance of the <see cref="Color"/> class with an encoded signed 32-bit color value in
+    ///     ARGB32 format.
+    /// </summary>
+    /// <param name="rgba">The encoded 32-bit color value (see <see cref="Rgba"/>).</param>
+    /// <remarks>
+    ///     The alpha channel is not currently supported, so the value of the alpha channel bits will not affect
+    ///     rendering.
+    /// </remarks>
+    public Color (int rgba) { Rgba = rgba; }
+
+    /// <summary>
+    ///     Initializes a new instance of the <see cref="Color"/> class with an encoded unsigned 32-bit color value in
+    ///     ARGB32 format.
+    /// </summary>
+    /// <param name="argb">The encoded unsigned 32-bit color value (see <see cref="Argb"/>).</param>
+    /// <remarks>
+    ///     The alpha channel is not currently supported, so the value of the alpha channel bits will not affect
+    ///     rendering.
+    /// </remarks>
+    public Color (uint argb) { Argb = argb; }
+
+    /// <summary>Initializes a new instance of the <see cref="Color"/> color from a legacy 16-color named value.</summary>
+    /// <param name="colorName">The 16-color value.</param>
+    public Color (in ColorName colorName) { this = ColorExtensions.ColorNameToColorMap [colorName]; }
+
+    /// <summary>
+    ///     Initializes a new instance of the <see cref="Color"/> color from string. See
+    ///     <see cref="TryParse(string, out Color?)"/> for details.
+    /// </summary>
+    /// <param name="colorString"></param>
+    /// <exception cref="ArgumentNullException">If <paramref name="colorString"/> is <see langword="null"/>.</exception>
+    /// <exception cref="ArgumentException">
+    ///     If <paramref name="colorString"/> is an empty string or consists of only whitespace
+    ///     characters.
+    /// </exception>
+    /// <exception cref="ColorParseException">If thrown by <see cref="Parse(string?,System.IFormatProvider?)"/></exception>
+    public Color (string colorString)
+    {
+        ArgumentException.ThrowIfNullOrWhiteSpace (colorString, nameof (colorString));
+        this = Parse (colorString, CultureInfo.InvariantCulture);
+    }
+
+    /// <summary>Initializes a new instance of the <see cref="Color"/> with all channels set to 0.</summary>
+    public Color () { Argb = 0u; }
+
+    /// <summary>Gets or sets the 3-byte/6-character hexadecimal value for each of the legacy 16-color values.</summary>
+    [SerializableConfigurationProperty (Scope = typeof (SettingsScope), OmitClassName = true)]
+    public static Dictionary<ColorName, string> Colors
+    {
+        get =>
+
+            // Transform _colorToNameMap into a Dictionary<ColorNames,string>
+            ColorExtensions.ColorToNameMap.ToDictionary (static kvp => kvp.Value, static kvp => kvp.Key.ToString ("g"));
+        set
+        {
+            // Transform Dictionary<ColorNames,string> into _colorToNameMap
+            ColorExtensions.ColorToNameMap = value.ToFrozenDictionary (GetColorToNameMapKey, GetColorToNameMapValue);
+
+            return;
+
+            static Color GetColorToNameMapKey (KeyValuePair<ColorName, string> kvp) { return new Color (kvp.Value); }
+
+            static ColorName GetColorToNameMapValue (KeyValuePair<ColorName, string> kvp)
+            {
+                return Enum.TryParse (kvp.Key.ToString (), true, out ColorName colorName)
+                           ? colorName
+                           : throw new ArgumentException ($"Invalid color name: {kvp.Key}");
+            }
+        }
+    }
+
+    /// <summary>
+    ///     Gets the <see cref="Color"/> using a legacy 16-color <see cref="ColorName"/> value. <see langword="get"/> will
+    ///     return the closest 16 color match to the true color when no exact value is found.
+    /// </summary>
+    /// <remarks>
+    ///     Get returns the <see cref="GetClosestNamedColor (Color)"/> of the closest 24-bit color value. Set sets the RGB
+    ///     value using a hard-coded map.
+    /// </remarks>
+    public AnsiColorCode GetAnsiColorCode () { return ColorExtensions.ColorNameToAnsiColorMap [GetClosestNamedColor ()]; }
+
+    /// <summary>
+    ///     Gets the <see cref="Color"/> using a legacy 16-color <see cref="Gui.ColorName"/> value. <see langword="get"/>
+    ///     will return the closest 16 color match to the true color when no exact value is found.
+    /// </summary>
+    /// <remarks>
+    ///     Get returns the <see cref="GetClosestNamedColor (Color)"/> of the closest 24-bit color value. Set sets the RGB
+    ///     value using a hard-coded map.
+    /// </remarks>
+    public ColorName GetClosestNamedColor () { return GetClosestNamedColor (this); }
+
+    /// <summary>
+    ///     Determines if the closest named <see cref="Color"/> to <see langword="this"/> is the provided
+    ///     <paramref name="namedColor"/>.
+    /// </summary>
+    /// <param name="namedColor">
+    ///     The <see cref="GetClosestNamedColor (Color)"/> to check if this <see cref="Color"/> is closer
+    ///     to than any other configured named color.
+    /// </param>
+    /// <returns>
+    ///     <see langword="true"/> if the closest named color is the provided value. <br/> <see langword="false"/> if any
+    ///     other named color is closer to this <see cref="Color"/> than <paramref name="namedColor"/>.
+    /// </returns>
+    /// <remarks>
+    ///     If <see langword="this"/> is equidistant from two named colors, the result of this method is not guaranteed to
+    ///     be determinate.
+    /// </remarks>
+    [Pure]
+    [MethodImpl (MethodImplOptions.AggressiveInlining)]
+    public bool IsClosestToNamedColor (in ColorName namedColor) { return GetClosestNamedColor () == namedColor; }
+
+    /// <summary>
+    ///     Determines if the closest named <see cref="Color"/> to <paramref name="color"/>/> is the provided
+    ///     <paramref name="namedColor"/>.
+    /// </summary>
+    /// <param name="color">
+    ///     The color to test against the <see cref="GetClosestNamedColor (Color)"/> value in
+    ///     <paramref name="namedColor"/>.
+    /// </param>
+    /// <param name="namedColor">
+    ///     The <see cref="GetClosestNamedColor (Color)"/> to check if this <see cref="Color"/> is closer
+    ///     to than any other configured named color.
+    /// </param>
+    /// <returns>
+    ///     <see langword="true"/> if the closest named color to <paramref name="color"/> is the provided value. <br/>
+    ///     <see langword="false"/> if any other named color is closer to <paramref name="color"/> than
+    ///     <paramref name="namedColor"/>.
+    /// </returns>
+    /// <remarks>
+    ///     If <paramref name="color"/> is equidistant from two named colors, the result of this method is not guaranteed
+    ///     to be determinate.
+    /// </remarks>
+    [Pure]
+    [MethodImpl (MethodImplOptions.AggressiveInlining)]
+    public static bool IsColorClosestToNamedColor (in Color color, in ColorName namedColor) { return color.IsClosestToNamedColor (in namedColor); }
+
+    /// <summary>Gets the "closest" named color to this <see cref="Color"/> value.</summary>
+    /// <param name="inputColor"></param>
+    /// <remarks>
+    ///     Distance is defined here as the Euclidean distance between each color interpreted as a <see cref="Vector3"/>.
+    ///     <para/>
+    ///     The order of the values in the passed Vector3 must be
+    /// </remarks>
+    /// <returns></returns>
+    [SkipLocalsInit]
+    internal static ColorName GetClosestNamedColor (Color inputColor)
+    {
+        return ColorExtensions.ColorToNameMap.MinBy (pair => CalculateColorDistance (inputColor, pair.Key)).Value;
+    }
+
+    [SkipLocalsInit]
+    private static float CalculateColorDistance (in Vector4 color1, in Vector4 color2) { return Vector4.Distance (color1, color2); }
+
+    #region Legacy Color Names
+
+    /// <summary>The black color.</summary>
+    public const ColorName Black = ColorName.Black;
+
+    /// <summary>The blue color.</summary>
+    public const ColorName Blue = ColorName.Blue;
+
+    /// <summary>The green color.</summary>
+    public const ColorName Green = ColorName.Green;
+
+    /// <summary>The cyan color.</summary>
+    public const ColorName Cyan = ColorName.Cyan;
+
+    /// <summary>The red color.</summary>
+    public const ColorName Red = ColorName.Red;
+
+    /// <summary>The magenta color.</summary>
+    public const ColorName Magenta = ColorName.Magenta;
+
+    /// <summary>The yellow color.</summary>
+    public const ColorName Yellow = ColorName.Yellow;
+
+    /// <summary>The gray color.</summary>
+    public const ColorName Gray = ColorName.Gray;
+
+    /// <summary>The dark gray color.</summary>
+    public const ColorName DarkGray = ColorName.DarkGray;
+
+    /// <summary>The bright bBlue color.</summary>
+    public const ColorName BrightBlue = ColorName.BrightBlue;
+
+    /// <summary>The bright green color.</summary>
+    public const ColorName BrightGreen = ColorName.BrightGreen;
+
+    /// <summary>The bright cyan color.</summary>
+    public const ColorName BrightCyan = ColorName.BrightCyan;
+
+    /// <summary>The bright red color.</summary>
+    public const ColorName BrightRed = ColorName.BrightRed;
+
+    /// <summary>The bright magenta color.</summary>
+    public const ColorName BrightMagenta = ColorName.BrightMagenta;
+
+    /// <summary>The bright yellow color.</summary>
+    public const ColorName BrightYellow = ColorName.BrightYellow;
+
+    /// <summary>The White color.</summary>
+    public const ColorName White = ColorName.White;
+
+    #endregion
 }

+ 176 - 0
Terminal.Gui/Drawing/ColorScheme.Colors.cs

@@ -0,0 +1,176 @@
+#nullable enable
+using System.Collections;
+using System.Collections.Specialized;
+using System.Text.Json.Serialization;
+
+namespace Terminal.Gui;
+
+/// <summary>
+///     Holds the <see cref="ColorScheme"/>s that define the <see cref="Attribute"/>s that are used by views to render
+///     themselves.
+/// </summary>
+public sealed class Colors : INotifyCollectionChanged, IDictionary<string, ColorScheme?>
+{
+    static Colors ()
+    {
+        ColorSchemes = new (5, StringComparer.InvariantCultureIgnoreCase);
+        Reset ();
+    }
+
+    /// <summary>Gets a dictionary of defined <see cref="ColorScheme"/> objects.</summary>
+    /// <remarks>
+    ///     <para>
+    ///         The <see cref="ColorSchemes"/> dictionary includes the following keys, by default:
+    ///         <list type="table">
+    ///             <listheader>
+    ///                 <term>Built-in Color Scheme</term> <description>Description</description>
+    ///             </listheader>
+    ///             <item>
+    ///                 <term>Base</term> <description>The base color scheme used for most Views.</description>
+    ///             </item>
+    ///             <item>
+    ///                 <term>TopLevel</term>
+    ///                 <description>The application Toplevel color scheme; used for the <see cref="Toplevel"/> View.</description>
+    ///             </item>
+    ///             <item>
+    ///                 <term>Dialog</term>
+    ///                 <description>
+    ///                     The dialog color scheme; used for <see cref="Dialog"/>, <see cref="MessageBox"/>, and
+    ///                     other views dialog-like views.
+    ///                 </description>
+    ///             </item>
+    ///             <item>
+    ///                 <term>Menu</term>
+    ///                 <description>
+    ///                     The menu color scheme; used for <see cref="MenuBar"/>, <see cref="ContextMenu"/>, and
+    ///                     <see cref="StatusBar"/>.
+    ///                 </description>
+    ///             </item>
+    ///             <item>
+    ///                 <term>Error</term>
+    ///                 <description>
+    ///                     The color scheme for showing errors, such as in
+    ///                     <see cref="MessageBox.ErrorQuery(string, string, string[])"/>.
+    ///                 </description>
+    ///             </item>
+    ///         </list>
+    ///     </para>
+    ///     <para>Changing the values of an entry in this dictionary will affect all views that use the scheme.</para>
+    ///     <para>
+    ///         <see cref="ConfigurationManager"/> can be used to override the default values for these schemes and add
+    ///         additional schemes. See <see cref="ConfigurationManager.Themes"/>.
+    ///     </para>
+    /// </remarks>
+    [SerializableConfigurationProperty (Scope = typeof (ThemeScope), OmitClassName = true)]
+    [JsonConverter (typeof (DictionaryJsonConverter<ColorScheme?>))]
+    [UsedImplicitly]
+    public static Dictionary<string, ColorScheme?> ColorSchemes { get; private set; }
+
+    /// <inheritdoc/>
+    public IEnumerator<KeyValuePair<string, ColorScheme?>> GetEnumerator () { return ColorSchemes.GetEnumerator (); }
+
+    /// <inheritdoc/>
+    IEnumerator IEnumerable.GetEnumerator () { return GetEnumerator (); }
+
+    /// <inheritdoc/>
+    public void Add (KeyValuePair<string, ColorScheme?> item)
+    {
+        ColorSchemes.Add (item.Key, item.Value);
+        CollectionChanged?.Invoke (this, new (NotifyCollectionChangedAction.Add, item));
+    }
+
+    /// <inheritdoc/>
+    public void Clear ()
+    {
+        ColorSchemes.Clear ();
+        CollectionChanged?.Invoke (this, new (NotifyCollectionChangedAction.Reset));
+    }
+
+    /// <inheritdoc/>
+    public bool Contains (KeyValuePair<string, ColorScheme?> item) { return ColorSchemes.Contains (item); }
+
+    /// <inheritdoc/>
+    public void CopyTo (KeyValuePair<string, ColorScheme?> [] array, int arrayIndex) { ((ICollection)ColorSchemes).CopyTo (array, arrayIndex); }
+
+    /// <inheritdoc/>
+    public bool Remove (KeyValuePair<string, ColorScheme?> item)
+    {
+        if (ColorSchemes.Remove (item.Key))
+        {
+            CollectionChanged?.Invoke (this, new (NotifyCollectionChangedAction.Remove, item));
+
+            return true;
+        }
+
+        return false;
+    }
+
+    /// <inheritdoc/>
+    public int Count => ColorSchemes.Count;
+
+    /// <inheritdoc/>
+    public bool IsReadOnly => false;
+
+    /// <inheritdoc/>
+    public void Add (string key, ColorScheme? value) { Add (new (key, value)); }
+
+    /// <inheritdoc/>
+    public bool ContainsKey (string key) { return ColorSchemes.ContainsKey (key); }
+
+    /// <inheritdoc/>
+    public bool Remove (string key)
+    {
+        if (ColorSchemes.Remove (key))
+        {
+            CollectionChanged?.Invoke (this, new (NotifyCollectionChangedAction.Remove, key));
+
+            return true;
+        }
+
+        return false;
+    }
+
+    /// <inheritdoc/>
+    public bool TryGetValue (string key, out ColorScheme? value) { return ColorSchemes.TryGetValue (key, out value); }
+
+    /// <inheritdoc/>
+    public ColorScheme? this [string key]
+    {
+        get => ColorSchemes [key];
+        set
+        {
+            if (ColorSchemes.TryAdd (key, value))
+            {
+                CollectionChanged?.Invoke (this, new (NotifyCollectionChangedAction.Add, new KeyValuePair<string, ColorScheme?> (key, value)));
+            }
+            else
+            {
+                ColorScheme? oldValue = ColorSchemes [key];
+                ColorSchemes [key] = value;
+                CollectionChanged?.Invoke (this, new (NotifyCollectionChangedAction.Replace, value, oldValue));
+            }
+        }
+    }
+
+    /// <inheritdoc/>
+    public ICollection<string> Keys => ColorSchemes.Keys;
+
+    /// <inheritdoc/>
+    public ICollection<ColorScheme?> Values => ColorSchemes.Values;
+
+    /// <inheritdoc/>
+    public event NotifyCollectionChangedEventHandler? CollectionChanged;
+
+    /// <summary>Resets the <see cref="ColorSchemes"/> dictionary to the default values.</summary>
+    public static Dictionary<string, ColorScheme?> Reset ()
+    {
+        ColorSchemes.Clear ();
+        ColorSchemes.Add ("TopLevel", new ());
+        ColorSchemes.Add ("Base", new ());
+        ColorSchemes.Add ("Dialog", new ());
+        ColorSchemes.Add ("Menu", new ());
+        ColorSchemes.Add ("Error", new ());
+
+        return ColorSchemes;
+    }
+}

+ 100 - 238
Terminal.Gui/Drawing/ColorScheme.cs

@@ -1,246 +1,108 @@
-using System;
-using System.Collections.Generic;
+#nullable enable
+using System.Numerics;
 using System.Text.Json.Serialization;
 
 namespace Terminal.Gui;
 
-/// <summary>
-/// Defines a standard set of <see cref="Attribute"/>s for common visible elements in a <see cref="View"/>.
-/// </summary>
+/// <summary>Defines a standard set of <see cref="Attribute"/>s for common visible elements in a <see cref="View"/>.</summary>
 /// <remarks>
-/// <para>
-/// ColorScheme objects are immutable. Once constructed, the properties cannot be changed.
-/// To change a ColorScheme, create a new one with the desired values,
-/// using the <see cref="ColorScheme(ColorScheme)"/> constructor.
-/// </para>
-/// <para>
-/// See also: <see cref="Colors.ColorSchemes"/>.
-/// </para>
+///     <para>
+///         ColorScheme objects are immutable. Once constructed, the properties cannot be changed. To change a
+///         ColorScheme, create a new one with the desired values, using the <see cref="ColorScheme(ColorScheme)"/>
+///         constructor.
+///     </para>
 /// </remarks>
 [JsonConverter (typeof (ColorSchemeJsonConverter))]
-public class ColorScheme : IEquatable<ColorScheme> {
-	readonly Attribute _disabled = Attribute.Default;
-	readonly Attribute _focus = Attribute.Default;
-	readonly Attribute _hotFocus = Attribute.Default;
-	readonly Attribute _hotNormal = Attribute.Default;
-	readonly Attribute _normal = Attribute.Default;
-
-	/// <summary>
-	/// Creates a new instance set to the default colors (see <see cref="Attribute.Default"/>).
-	/// </summary>
-	public ColorScheme () : this (Attribute.Default) { }
-
-	/// <summary>
-	/// Creates a new instance, initialized with the values from <paramref name="scheme"/>.
-	/// </summary>
-	/// <param name="scheme">The scheme to initialize the new instance with.</param>
-	public ColorScheme (ColorScheme scheme)
-	{
-		if (scheme == null) {
-			throw new ArgumentNullException (nameof (scheme));
-		}
-		_normal = scheme.Normal;
-		_focus = scheme.Focus;
-		_hotNormal = scheme.HotNormal;
-		_disabled = scheme.Disabled;
-		_hotFocus = scheme.HotFocus;
-	}
-
-	/// <summary>
-	/// Creates a new instance, initialized with the values from <paramref name="attribute"/>.
-	/// </summary>
-	/// <param name="attribute">The attribute to initialize the new instance with.</param>
-	public ColorScheme (Attribute attribute)
-	{
-		_normal = attribute;
-		_focus = attribute;
-		_hotNormal = attribute;
-		_disabled = attribute;
-		_hotFocus = attribute;
-	}
-
-	/// <summary>
-	/// The foreground and background color for text when the view is not focused, hot, or disabled.
-	/// </summary>
-	public Attribute Normal {
-		get => _normal;
-		init => _normal = value;
-	}
-
-	/// <summary>
-	/// The foreground and background color for text when the view has the focus.
-	/// </summary>
-	public Attribute Focus {
-		get => _focus;
-		init => _focus = value;
-	}
-
-	/// <summary>
-	/// The foreground and background color for text in a non-focused view that indicates a <see cref="View.HotKey"/>.
-	/// </summary>
-	public Attribute HotNormal {
-		get => _hotNormal;
-		init => _hotNormal = value;
-	}
-
-	/// <summary>
-	/// The foreground and background color for for text in a focused view that indicates a <see cref="View.HotKey"/>.
-	/// </summary>
-	public Attribute HotFocus {
-		get => _hotFocus;
-		init => _hotFocus = value;
-	}
-
-	/// <summary>
-	/// The default foreground and background color for text when the view is disabled.
-	/// </summary>
-	public Attribute Disabled {
-		get => _disabled;
-		init => _disabled = value;
-	}
-
-	/// <summary>
-	/// Compares two <see cref="ColorScheme"/> objects for equality.
-	/// </summary>
-	/// <param name="other"></param>
-	/// <returns>true if the two objects are equal</returns>
-	public bool Equals (ColorScheme other) => other != null &&
-	                                          EqualityComparer<Attribute>.Default.Equals (_normal, other._normal) &&
-	                                          EqualityComparer<Attribute>.Default.Equals (_focus, other._focus) &&
-	                                          EqualityComparer<Attribute>.Default.Equals (_hotNormal, other._hotNormal) &&
-	                                          EqualityComparer<Attribute>.Default.Equals (_hotFocus, other._hotFocus) &&
-	                                          EqualityComparer<Attribute>.Default.Equals (_disabled, other._disabled);
-
-	/// <summary>
-	/// Compares two <see cref="ColorScheme"/> objects for equality.
-	/// </summary>
-	/// <param name="obj"></param>
-	/// <returns>true if the two objects are equal</returns>
-	public override bool Equals (object obj) => Equals (obj is ColorScheme ? (ColorScheme)obj : default);
-
-	/// <summary>
-	/// Returns a hashcode for this instance.
-	/// </summary>
-	/// <returns>hashcode for this instance</returns>
-	public override int GetHashCode ()
-	{
-		var hashCode = -1242460230;
-		hashCode = hashCode * -1521134295 + _normal.GetHashCode ();
-		hashCode = hashCode * -1521134295 + _focus.GetHashCode ();
-		hashCode = hashCode * -1521134295 + _hotNormal.GetHashCode ();
-		hashCode = hashCode * -1521134295 + _hotFocus.GetHashCode ();
-		hashCode = hashCode * -1521134295 + _disabled.GetHashCode ();
-		return hashCode;
-	}
-
-	/// <summary>
-	/// Compares two <see cref="ColorScheme"/> objects for equality.
-	/// </summary>
-	/// <param name="left"></param>
-	/// <param name="right"></param>
-	/// <returns><c>true</c> if the two objects are equivalent</returns>
-	public static bool operator == (ColorScheme left, ColorScheme right) => EqualityComparer<ColorScheme>.Default.Equals (left, right);
-
-	/// <summary>
-	/// Compares two <see cref="ColorScheme"/> objects for inequality.
-	/// </summary>
-	/// <param name="left"></param>
-	/// <param name="right"></param>
-	/// <returns><c>true</c> if the two objects are not equivalent</returns>
-	public static bool operator != (ColorScheme left, ColorScheme right) => !(left == right);
-}
-
-/// <summary>
-/// Holds the <see cref="ColorScheme"/>s that define the <see cref="Attribute"/>s that are used by views to render themselves.
-/// </summary>
-public static class Colors {
-	static Colors () => Reset ();
-	/// <summary>
-	/// Gets a dictionary of defined <see cref="ColorScheme"/> objects.
-	/// </summary>
-	/// <remarks>
-	/// <para>
-	/// The <see cref="ColorSchemes"/> dictionary includes the following keys, by default:
-	/// <list type="table">
-	/// <listheader>
-	///         <term>Built-in Color Scheme</term>
-	///         <description>Description</description>
-	/// </listheader>
-	/// <item>
-	///         <term>
-	///         Base
-	///         </term>
-	///         <description>
-	///         The base color scheme used for most Views.
-	///         </description>
-	/// </item>
-	/// <item>
-	///         <term>
-	///         TopLevel
-	///         </term>
-	///         <description>
-	///         The application Toplevel color scheme; used for the <see cref="Toplevel"/> View.
-	///         </description>
-	/// </item>
-	/// <item>
-	///         <term>
-	///         Dialog
-	///         </term>
-	///         <description>
-	///         The dialog color scheme; used for <see cref="Dialog"/>, <see cref="MessageBox"/>, and other views dialog-like views.
-	///         </description>
-	/// </item> 
-	/// <item>
-	///         <term>
-	///         Menu
-	///         </term>
-	///         <description>
-	///         The menu color scheme; used for <see cref="MenuBar"/>, <see cref="ContextMenu"/>, and <see cref="StatusBar"/>. 
-	///         </description>
-	/// </item>
-	/// <item>
-	///         <term>
-	///         Error
-	///         </term>
-	///         <description>
-	///         The color scheme for showing errors, such as in <see cref="MessageBox.ErrorQuery(string, string, string[])"/>. 
-	///         </description>
-	/// </item>
-	/// </list>
-	/// </para>
-	/// <para>
-	/// Changing the values of an entry in this dictionary will affect all views that use the scheme.
-	/// </para>
-	/// <para>
-	/// <see cref="ConfigurationManager"/> can be used to override the default values for these schemes and add additional schemes.
-	/// See <see cref="ConfigurationManager.Themes"/>.
-	/// </para>
-	/// </remarks>
-	[SerializableConfigurationProperty (Scope = typeof (ThemeScope), OmitClassName = true)]
-	[JsonConverter (typeof (DictionaryJsonConverter<ColorScheme>))]
-	public static Dictionary<string, ColorScheme> ColorSchemes { get; private set; } // Serialization requires this to have a setter (private set;)
-
-	/// <summary>
-	/// Resets the <see cref="ColorSchemes"/> dictionary to the default values.
-	/// </summary>
-	public static Dictionary<string, ColorScheme> Reset () =>
-		ColorSchemes = new Dictionary<string, ColorScheme> (comparer: new SchemeNameComparerIgnoreCase ()) {
-			{ "TopLevel", new ColorScheme () },
-			{ "Base", new ColorScheme () },
-			{ "Dialog", new ColorScheme () },
-			{ "Menu", new ColorScheme () },
-			{ "Error", new ColorScheme () },
-		};
-
-	class SchemeNameComparerIgnoreCase : IEqualityComparer<string> {
-		public bool Equals (string x, string y)
-		{
-			if (x != null && y != null) {
-				return string.Equals (x, y, StringComparison.InvariantCultureIgnoreCase);
-			}
-			return false;
-		}
-
-		public int GetHashCode (string obj) => obj.ToLowerInvariant ().GetHashCode ();
-	}
+public record ColorScheme : IEqualityOperators<ColorScheme, ColorScheme, bool>
+{
+    private readonly Attribute _disabled;
+    private readonly Attribute _focus;
+    private readonly Attribute _hotFocus;
+    private readonly Attribute _hotNormal;
+    private readonly Attribute _normal;
+
+    /// <summary>Creates a new instance set to the default colors (see <see cref="Attribute.Default"/>).</summary>
+    public ColorScheme () : this (Attribute.Default) { }
+
+    /// <summary>Creates a new instance, initialized with the values from <paramref name="scheme"/>.</summary>
+    /// <param name="scheme">The scheme to initialize the new instance with.</param>
+    public ColorScheme (ColorScheme? scheme)
+    {
+        ArgumentNullException.ThrowIfNull (scheme);
+
+        _normal = scheme.Normal;
+        _focus = scheme.Focus;
+        _hotNormal = scheme.HotNormal;
+        _disabled = scheme.Disabled;
+        _hotFocus = scheme.HotFocus;
+    }
+
+    /// <summary>Creates a new instance, initialized with the values from <paramref name="attribute"/>.</summary>
+    /// <param name="attribute">The attribute to initialize the new instance with.</param>
+    public ColorScheme (Attribute attribute)
+    {
+        _normal = attribute;
+        _focus = attribute;
+        _hotNormal = attribute;
+        _disabled = attribute;
+        _hotFocus = attribute;
+    }
+
+    /// <summary>The default foreground and background color for text when the view is disabled.</summary>
+    public Attribute Disabled
+    {
+        get => _disabled;
+        init => _disabled = value;
+    }
+
+    /// <summary>The foreground and background color for text when the view has the focus.</summary>
+    public Attribute Focus
+    {
+        get => _focus;
+        init => _focus = value;
+    }
+
+    /// <summary>The foreground and background color for for text in a focused view that indicates a <see cref="View.HotKey"/>.</summary>
+    public Attribute HotFocus
+    {
+        get => _hotFocus;
+        init => _hotFocus = value;
+    }
+
+    /// <summary>The foreground and background color for text in a non-focused view that indicates a <see cref="View.HotKey"/>.</summary>
+    public Attribute HotNormal
+    {
+        get => _hotNormal;
+        init => _hotNormal = value;
+    }
+
+    /// <summary>The foreground and background color for text when the view is not focused, hot, or disabled.</summary>
+    public Attribute Normal
+    {
+        get => _normal;
+        init => _normal = value;
+    }
+
+    /// <summary>Compares two <see cref="ColorScheme"/> objects for equality.</summary>
+    /// <param name="other"></param>
+    /// <returns>true if the two objects are equal</returns>
+    public virtual bool Equals (ColorScheme? other)
+    {
+        return other is { }
+               && EqualityComparer<Attribute>.Default.Equals (_normal, other._normal)
+               && EqualityComparer<Attribute>.Default.Equals (_focus, other._focus)
+               && EqualityComparer<Attribute>.Default.Equals (_hotNormal, other._hotNormal)
+               && EqualityComparer<Attribute>.Default.Equals (_hotFocus, other._hotFocus)
+               && EqualityComparer<Attribute>.Default.Equals (_disabled, other._disabled);
+    }
+
+    /// <summary>Returns a hashcode for this instance.</summary>
+    /// <returns>hashcode for this instance</returns>
+    public override int GetHashCode ()
+    {
+        return HashCode.Combine (_normal, _focus, _hotNormal, _hotFocus, _disabled);
+    }
+
+    /// <inheritdoc/>
+    public override string ToString () { return $"Normal: {Normal}; Focus: {Focus}; HotNormal: {HotNormal}; HotFocus: {HotFocus}; Disabled: {Disabled}"; }
 }

+ 439 - 689
Terminal.Gui/Drawing/Glyphs.cs

@@ -1,690 +1,440 @@
-using static Terminal.Gui.ConfigurationManager;
-using System.Text.Json.Serialization;
-using System.Text;
-
-namespace Terminal.Gui {
-	/// <summary>
-	/// Defines the standard set of glyphs used to draw checkboxes, lines, borders, etc...
-	/// </summary>
-	/// <remarks>
-	/// <para>
-	/// Access with <see cref="CM.Glyphs"/> (which is a global using alias for <see cref="ConfigurationManager.Glyphs"/>).
-	/// </para>
-	/// <para>
-	/// The default glyphs can be changed via the <see cref="ConfigurationManager"/>. Within a <c>config.json</c> file 
-	/// The Json property name is the property name prefixed with "Glyphs.". 
-	/// </para>
-	/// <para>
-	/// The JSon property can be either a decimal number or a string. The string may be one of:
-	/// - A unicode char (e.g. "☑")
-	/// - A hex value in U+ format (e.g. "U+2611")
-	/// - A hex value in UTF-16 format (e.g. "\\u2611")
-	/// </para>
-	/// </remarks>
-	public class GlyphDefinitions {
-		#region ----------------- Single Glyphs -----------------
-
-		/// <summary>
-		/// Checked indicator (e.g. for <see cref="ListView"/> and <see cref="CheckBox"/>).
-		/// </summary>
-		public Rune Checked { get; set; } = (Rune)'☑';
-
-		/// <summary>
-		/// Not Checked indicator (e.g. for <see cref="ListView"/> and <see cref="CheckBox"/>).
-		/// </summary>
-		public Rune UnChecked { get; set; } = (Rune)'☐';
-
-		/// <summary>
-		/// Null Checked indicator (e.g. for <see cref="ListView"/> and <see cref="CheckBox"/>).
-		/// </summary>
-		public Rune NullChecked { get; set; } = (Rune)'☒';
-
-		/// <summary>
-		/// Selected indicator  (e.g. for <see cref="ListView"/> and <see cref="RadioGroup"/>).
-		/// </summary>
-		public Rune Selected { get; set; } = (Rune)'◉';
-
-		/// <summary>
-		/// Not Selected indicator (e.g. for <see cref="ListView"/> and <see cref="RadioGroup"/>).
-		/// </summary>
-		public Rune UnSelected { get; set; } = (Rune)'○';
-
-		/// <summary>
-		/// Horizontal arrow.
-		/// </summary>
-		public Rune RightArrow { get; set; } = (Rune)'►';
-
-		/// <summary>
-		/// Left arrow.
-		/// </summary>
-		public Rune LeftArrow { get; set; } = (Rune)'◄';
-
-		/// <summary>
-		/// Down arrow.
-		/// </summary>
-		public Rune DownArrow { get; set; } = (Rune)'▼';
-
-		/// <summary>
-		/// Vertical arrow.
-		/// </summary>
-		public Rune UpArrow { get; set; } = (Rune)'▲';
-
-		/// <summary>
-		/// Left default indicator (e.g. for <see cref="Button"/>.
-		/// </summary>
-		public Rune LeftDefaultIndicator { get; set; } = (Rune)'►';
-
-		/// <summary>
-		/// Horizontal default indicator (e.g. for <see cref="Button"/>.
-		/// </summary>
-		public Rune RightDefaultIndicator { get; set; } = (Rune)'◄';
-
-		/// <summary>
-		/// Left Bracket (e.g. for <see cref="Button"/>. Default is (U+005B) - [.
-		/// </summary>
-		public Rune LeftBracket { get; set; } = (Rune)'⟦';
-
-		/// <summary>
-		/// Horizontal Bracket (e.g. for <see cref="Button"/>. Default is (U+005D) - ].
-		/// </summary>
-		public Rune RightBracket { get; set; } = (Rune)'⟧';
-
-		/// <summary>
-		/// Half block meter segment (e.g. for <see cref="ProgressBar"/>).
-		/// </summary>
-		public Rune BlocksMeterSegment { get; set; } = (Rune)'▌';
-
-		/// <summary>
-		/// Continuous block meter segment (e.g. for <see cref="ProgressBar"/>).
-		/// </summary>
-		public Rune ContinuousMeterSegment { get; set; } = (Rune)'█';
-
-		/// <summary>
-		/// Stipple pattern (e.g. for <see cref="ScrollBarView"/>). Default is Light Shade (U+2591) - ░.
-		/// </summary>
-		public Rune Stipple { get; set; } = (Rune)'░';
-
-		/// <summary>
-		/// Diamond (e.g. for <see cref="ScrollBarView"/>. Default is Lozenge (U+25CA) - ◊.
-		/// </summary>
-		public Rune Diamond { get; set; } = (Rune)'◊';
-
-		/// <summary>
-		/// Close. Default is Heavy Ballot X (U+2718) - ✘.
-		/// </summary>
-		public Rune Close { get; set; } = (Rune)'✘';
-
-		/// <summary>
-		/// Minimize. Default is Lower Horizontal Shadowed White Circle (U+274F) - ❏.
-		/// </summary>
-		public Rune Minimize { get; set; } = (Rune)'❏';
-
-		/// <summary>
-		/// Maximize. Default is Upper Horizontal Shadowed White Circle (U+273D) - ✽.
-		/// </summary>
-		public Rune Maximize { get; set; } = (Rune)'✽';
-
-		/// <summary>
-		/// Dot. Default is (U+2219) - ∙.
-		/// </summary>
-		public Rune Dot { get; set; } = (Rune)'∙';
-
-		/// <summary>
-		/// Black Circle . Default is (U+025cf) - ●.
-		/// </summary>
-		public Rune BlackCircle { get; set; } = (Rune)'●'; // Black Circle - ● U+025cf
-
-		/// <summary>
-		/// Expand (e.g. for <see cref="TreeView"/>.
-		/// </summary>
-		public Rune Expand { get; set; } = (Rune)'+';
-
-		/// <summary>
-		/// Expand (e.g. for <see cref="TreeView"/>.
-		/// </summary>
-		public Rune Collapse { get; set; } = (Rune)'-';
-
-		/// <summary>
-		/// Identical To (U+226)
-		/// </summary>
-		public Rune IdenticalTo { get; set; } = (Rune)'≡';
-
-		/// <summary>
-		/// Apple (non-BMP). Because snek. And because it's an example of a non-BMP surrogate pair. See Issue #2610.
-		/// </summary>
-		public Rune Apple { get; set; } = "🍎".ToRunes () [0]; // nonBMP
-
-		/// <summary>
-		/// Apple (BMP). Because snek. See Issue #2610.
-		/// </summary>
-		public Rune AppleBMP { get; set; } = (Rune)'❦';
-
-		///// <summary>
-		///// A nonprintable (low surrogate) that should fail to ctor.
-		///// </summary>
-		//public Rune InvalidGlyph { get; set; } = (Rune)'\ud83d';
-
-		#endregion
-
-		/// <summary>
-		/// Folder icon.  Defaults to ꤉ (Kayah Li Digit Nine)
-		/// </summary>
-		public Rune Folder { get; set; } = (Rune)'꤉';
-
-		/// <summary>
-		/// File icon.  Defaults to ☰ (Trigram For Heaven)
-		/// </summary>
-		public Rune File { get; set; } = (Rune)'☰';
-
-		/// <summary>
-		/// Horizontal Ellipsis - … U+2026
-		/// </summary>
-		public Rune HorizontalEllipsis { get; set; } = (Rune)'…';
-
-		/// <summary>
-		/// Vertical Four Dots - ⁞ U+205e
-		/// </summary>
-		public Rune VerticalFourDots { get; set; } = (Rune)'⁞';
-
-		#region ----------------- Lines -----------------
-		/// <summary>
-		/// Box Drawings Horizontal Line - Light (U+2500) - ─
-		/// </summary>
-		public Rune HLine { get; set; } = (Rune)'─';
-
-		/// <summary>
-		/// Box Drawings Vertical Line - Light (U+2502) - │
-		/// </summary>
-		public Rune VLine { get; set; } = (Rune)'│';
-
-		/// <summary>
-		/// Box Drawings Double Horizontal (U+2550) - ═
-		/// </summary>
-		public Rune HLineDbl { get; set; } = (Rune)'═';
-
-		/// <summary>
-		/// Box Drawings Double Vertical (U+2551) - ║
-		/// </summary>
-		public Rune VLineDbl { get; set; } = (Rune)'║';
-
-		/// <summary>
-		/// Box Drawings Heavy Double Dash Horizontal (U+254D) - ╍
-		/// </summary>
-		public Rune HLineHvDa2 { get; set; } = (Rune)'╍';
-
-		/// <summary>
-		/// Box Drawings Heavy Triple Dash Vertical (U+2507) - ┇
-		/// </summary>
-		public Rune VLineHvDa3 { get; set; } = (Rune)'┇';
-
-		/// <summary>
-		/// Box Drawings Heavy Triple Dash Horizontal (U+2505) - ┅
-		/// </summary>
-		public Rune HLineHvDa3 { get; set; } = (Rune)'┅';
-
-		/// <summary>
-		/// Box Drawings Heavy Quadruple Dash Horizontal (U+2509) - ┉
-		/// </summary>
-		public Rune HLineHvDa4 { get; set; } = (Rune)'┉';
-
-		/// <summary>
-		/// Box Drawings Heavy Double Dash Vertical (U+254F) - ╏
-		/// </summary>
-		public Rune VLineHvDa2 { get; set; } = (Rune)'╏';
-
-		/// <summary>
-		/// Box Drawings Heavy Quadruple Dash Vertical (U+250B) - ┋
-		/// </summary>
-		public Rune VLineHvDa4 { get; set; } = (Rune)'┋';
-
-		/// <summary>
-		/// Box Drawings Light Double Dash Horizontal (U+254C) - ╌
-		/// </summary>
-		public Rune HLineDa2 { get; set; } = (Rune)'╌';
-
-		/// <summary>
-		/// Box Drawings Light Triple Dash Vertical (U+2506) - ┆
-		/// </summary>
-		public Rune VLineDa3 { get; set; } = (Rune)'┆';
-
-		/// <summary>
-		/// Box Drawings Light Triple Dash Horizontal (U+2504) - ┄
-		/// </summary>
-		public Rune HLineDa3 { get; set; } = (Rune)'┄';
-
-		/// <summary>
-		/// Box Drawings Light Quadruple Dash Horizontal (U+2508) - ┈
-		/// </summary>
-		public Rune HLineDa4 { get; set; } = (Rune)'┈';
-
-		/// <summary>
-		/// Box Drawings Light Double Dash Vertical (U+254E) - ╎
-		/// </summary>
-		public Rune VLineDa2 { get; set; } = (Rune)'╎';
-
-		/// <summary>
-		/// Box Drawings Light Quadruple Dash Vertical (U+250A) - ┊
-		/// </summary>
-		public Rune VLineDa4 { get; set; } = (Rune)'┊';
-
-		/// <summary>
-		/// Box Drawings Heavy Horizontal (U+2501) - ━
-		/// </summary>
-		public Rune HLineHv { get; set; } = (Rune)'━';
-
-		/// <summary>
-		/// Box Drawings Heavy Vertical (U+2503) - ┃
-		/// </summary>
-		public Rune VLineHv { get; set; } = (Rune)'┃';
-
-		/// <summary>
-		/// Box Drawings Light Left (U+2574) - ╴
-		/// </summary>
-		public Rune HalfLeftLine { get; set; } = (Rune)'╴';
-
-		/// <summary>
-		/// Box Drawings Light Vertical (U+2575) - ╵
-		/// </summary>
-		public Rune HalfTopLine { get; set; } = (Rune)'╵';
-
-		/// <summary>
-		/// Box Drawings Light Horizontal (U+2576) - ╶
-		/// </summary>
-		public Rune HalfRightLine { get; set; } = (Rune)'╶';
-
-		/// <summary>
-		/// Box Drawings Light Down (U+2577) - ╷
-		/// </summary>
-		public Rune HalfBottomLine { get; set; } = (Rune)'╷';
-
-		/// <summary>
-		/// Box Drawings Heavy Left (U+2578) - ╸
-		/// </summary>
-		public Rune HalfLeftLineHv { get; set; } = (Rune)'╸';
-
-		/// <summary>
-		/// Box Drawings Heavy Vertical (U+2579) - ╹
-		/// </summary>
-		public Rune HalfTopLineHv { get; set; } = (Rune)'╹';
-
-		/// <summary>
-		/// Box Drawings Heavy Horizontal (U+257A) - ╺
-		/// </summary>
-		public Rune HalfRightLineHv { get; set; } = (Rune)'╺';
-
-		/// <summary>
-		/// Box Drawings Light Vertical and Horizontal (U+257B) - ╻
-		/// </summary>
-		public Rune HalfBottomLineLt { get; set; } = (Rune)'╻';
-
-		/// <summary>
-		/// Box Drawings Light Horizontal and Heavy Horizontal (U+257C) - ╼
-		/// </summary>
-		public Rune RightSideLineLtHv { get; set; } = (Rune)'╼';
-
-		/// <summary>
-		/// Box Drawings Light Vertical and Heavy Horizontal (U+257D) - ╽
-		/// </summary>
-		public Rune BottomSideLineLtHv { get; set; } = (Rune)'╽';
-
-		/// <summary>
-		/// Box Drawings Heavy Left and Light Horizontal (U+257E) - ╾
-		/// </summary>
-		public Rune LeftSideLineHvLt { get; set; } = (Rune)'╾';
-
-		/// <summary>
-		/// Box Drawings Heavy Vertical and Light Horizontal (U+257F) - ╿
-		/// </summary>
-		public Rune TopSideLineHvLt { get; set; } = (Rune)'╿';
-		#endregion
-
-		#region ----------------- Upper Left Corners -----------------
-		/// <summary>
-		/// Box Drawings Upper Left Corner - Light Vertical and Light Horizontal (U+250C) - ┌
-		/// </summary>
-		public Rune ULCorner { get; set; } = (Rune)'┌';
-
-		/// <summary>
-		/// Box Drawings Upper Left Corner -  Double (U+2554) - ╔
-		/// </summary>
-		public Rune ULCornerDbl { get; set; } = (Rune)'╔';
-
-		/// <summary>
-		/// Box Drawings Upper Left Corner - Light Arc Down and Horizontal (U+256D) - ╭
-		/// </summary>
-		public Rune ULCornerR { get; set; } = (Rune)'╭';
-
-		/// <summary>
-		/// Box Drawings Heavy Down and Horizontal (U+250F) - ┏
-		/// </summary>
-		public Rune ULCornerHv { get; set; } = (Rune)'┏';
-
-		/// <summary>
-		/// Box Drawings Down Heavy and Horizontal Light (U+251E) - ┎
-		/// </summary>
-		public Rune ULCornerHvLt { get; set; } = (Rune)'┎';
-
-		/// <summary>
-		/// Box Drawings Down Light and Horizontal Heavy (U+250D) - ┎
-		/// </summary>
-		public Rune ULCornerLtHv { get; set; } = (Rune)'┍';
-
-		/// <summary>
-		/// Box Drawings Double Down and Single Horizontal (U+2553) - ╓
-		/// </summary>
-		public Rune ULCornerDblSingle { get; set; } = (Rune)'╓';
-
-		/// <summary>
-		/// Box Drawings Single Down and Double Horizontal (U+2552) - ╒
-		/// </summary>
-		public Rune ULCornerSingleDbl { get; set; } = (Rune)'╒';
-		#endregion
-
-		#region ----------------- Lower Left Corners -----------------
-		/// <summary>
-		/// Box Drawings Lower Left Corner - Light Vertical and Light Horizontal (U+2514) - └
-		/// </summary>
-		public Rune LLCorner { get; set; } = (Rune)'└';
-
-		/// <summary>
-		/// Box Drawings Heavy Vertical and Horizontal (U+2517) - ┗
-		/// </summary>
-		public Rune LLCornerHv { get; set; } = (Rune)'┗';
-
-		/// <summary>
-		/// Box Drawings Heavy Vertical and Horizontal Light (U+2516) - ┖
-		/// </summary>
-		public Rune LLCornerHvLt { get; set; } = (Rune)'┖';
-
-		/// <summary>
-		/// Box Drawings Vertical Light and Horizontal Heavy (U+2511) - ┕
-		/// </summary>
-		public Rune LLCornerLtHv { get; set; } = (Rune)'┕';
-
-		/// <summary>
-		/// Box Drawings Double Vertical and Double Left (U+255A) - ╚
-		/// </summary>
-		public Rune LLCornerDbl { get; set; } = (Rune)'╚';
-
-		/// <summary>
-		/// Box Drawings Single Vertical and Double Left (U+2558) - ╘
-		/// </summary>
-		public Rune LLCornerSingleDbl { get; set; } = (Rune)'╘';
-
-		/// <summary>
-		/// Box Drawings Double Down and Single Left (U+2559) - ╙
-		/// </summary>
-		public Rune LLCornerDblSingle { get; set; } = (Rune)'╙';
-
-		/// <summary>
-		/// Box Drawings Upper Left Corner - Light Arc Down and Left (U+2570) - ╰
-		/// </summary>
-		public Rune LLCornerR { get; set; } = (Rune)'╰';
-
-		#endregion
-
-		#region ----------------- Upper Right Corners -----------------
-		/// <summary>
-		/// Box Drawings Upper Horizontal Corner - Light Vertical and Light Horizontal (U+2510) - ┐
-		/// </summary>
-		public Rune URCorner { get; set; } = (Rune)'┐';
-
-		/// <summary>
-		/// Box Drawings Upper Horizontal Corner - Double Vertical and Double Horizontal (U+2557) - ╗
-		/// </summary>
-		public Rune URCornerDbl { get; set; } = (Rune)'╗';
-
-		/// <summary>
-		/// Box Drawings Upper Horizontal Corner - Light Arc Vertical and Horizontal (U+256E) - ╮
-		/// </summary>
-		public Rune URCornerR { get; set; } = (Rune)'╮';
-
-		/// <summary>
-		/// Box Drawings Heavy Down and Left (U+2513) - ┓
-		/// </summary>
-		public Rune URCornerHv { get; set; } = (Rune)'┓';
-
-		/// <summary>
-		/// Box Drawings Heavy Vertical and Left Down Light (U+2511) - ┑
-		/// </summary>
-		public Rune URCornerHvLt { get; set; } = (Rune)'┑';
-
-		/// <summary>
-		/// Box Drawings Down Light and Horizontal Heavy (U+2514) - ┒
-		/// </summary>
-		public Rune URCornerLtHv { get; set; } = (Rune)'┒';
-
-		/// <summary>
-		/// Box Drawings Double Vertical and Single Left (U+2556) - ╖
-		/// </summary>
-		public Rune URCornerDblSingle { get; set; } = (Rune)'╖';
-
-		/// <summary>
-		/// Box Drawings Single Vertical and Double Left (U+2555) - ╕
-		/// </summary>
-		public Rune URCornerSingleDbl { get; set; } = (Rune)'╕';
-		#endregion
-
-		#region ----------------- Lower Right Corners -----------------
-		/// <summary>
-		/// Box Drawings Lower Right Corner - Light (U+2518) - ┘
-		/// </summary>
-		public Rune LRCorner { get; set; } = (Rune)'┘';
-
-		/// <summary>
-		/// Box Drawings Lower Right Corner - Double (U+255D) - ╝
-		/// </summary>
-		public Rune LRCornerDbl { get; set; } = (Rune)'╝';
-
-		/// <summary>
-		/// Box Drawings Lower Right Corner - Rounded (U+256F) - ╯
-		/// </summary>
-		public Rune LRCornerR { get; set; } = (Rune)'╯';
-
-		/// <summary>
-		/// Box Drawings Lower Right Corner - Heavy (U+251B) - ┛
-		/// </summary>
-		public Rune LRCornerHv { get; set; } = (Rune)'┛';
-
-		/// <summary>
-		/// Box Drawings Lower Right Corner - Double Vertical and Single Horizontal (U+255C) - ╜
-		/// </summary>
-		public Rune LRCornerDblSingle { get; set; } = (Rune)'╜';
-
-		/// <summary>
-		/// Box Drawings Lower Right Corner - Single Vertical and Double Horizontal (U+255B) - ╛
-		/// </summary>
-		public Rune LRCornerSingleDbl { get; set; } = (Rune)'╛';
-
-		/// <summary>
-		/// Box Drawings Lower Right Corner - Light Vertical and Heavy Horizontal (U+2519) - ┙
-		/// </summary>
-		public Rune LRCornerLtHv { get; set; } = (Rune)'┙';
-
-		/// <summary>
-		/// Box Drawings Lower Right Corner - Heavy Vertical and Light Horizontal (U+251A) - ┚
-		/// </summary>
-		public Rune LRCornerHvLt { get; set; } = (Rune)'┚';
-		#endregion
-
-		#region ----------------- Tees -----------------
-		/// <summary>
-		/// Box Drawings Left Tee - Single Vertical and Single Horizontal (U+251C) - ├
-		/// </summary>
-		public Rune LeftTee { get; set; } = (Rune)'├';
-
-		/// <summary>
-		/// Box Drawings Left Tee - Single Vertical and Double Horizontal (U+255E) - ╞
-		/// </summary>
-		public Rune LeftTeeDblH { get; set; } = (Rune)'╞';
-
-		/// <summary>
-		/// Box Drawings Left Tee - Double Vertical and Single Horizontal (U+255F) - ╟
-		/// </summary>
-		public Rune LeftTeeDblV { get; set; } = (Rune)'╟';
-
-		/// <summary>
-		/// Box Drawings Left Tee - Double Vertical and Double Horizontal (U+2560) - ╠
-		/// </summary>
-		public Rune LeftTeeDbl { get; set; } = (Rune)'╠';
-
-		/// <summary>
-		/// Box Drawings Left Tee - Heavy Horizontal and Light Vertical (U+2523) - ┝
-		/// </summary>
-		public Rune LeftTeeHvH { get; set; } = (Rune)'┝';
-
-		/// <summary>
-		/// Box Drawings Left Tee - Light Horizontal and Heavy Vertical (U+252B) - ┠
-		/// </summary>
-		public Rune LeftTeeHvV { get; set; } = (Rune)'┠';
-
-		/// <summary>
-		/// Box Drawings Left Tee - Heavy Vertical and Heavy Horizontal (U+2527) - ┣
-		/// </summary>
-		public Rune LeftTeeHvDblH { get; set; } = (Rune)'┣';
-
-		/// <summary>
-		/// Box Drawings Righ Tee - Single Vertical and Single Horizontal (U+2524) - ┤
-		/// </summary>
-		public Rune RightTee { get; set; } = (Rune)'┤';
-
-		/// <summary>
-		/// Box Drawings Right Tee - Single Vertical and Double Horizontal (U+2561) - ╡
-		/// </summary>
-		public Rune RightTeeDblH { get; set; } = (Rune)'╡';
-
-		/// <summary>
-		/// Box Drawings Right Tee - Double Vertical and Single Horizontal (U+2562) - ╢
-		/// </summary>
-		public Rune RightTeeDblV { get; set; } = (Rune)'╢';
-
-		/// <summary>
-		/// Box Drawings Right Tee - Double Vertical and Double Horizontal (U+2563) - ╣
-		/// </summary>
-		public Rune RightTeeDbl { get; set; } = (Rune)'╣';
-
-		/// <summary>
-		/// Box Drawings Right Tee - Heavy Horizontal and Light Vertical (U+2528) - ┥
-		/// </summary>
-		public Rune RightTeeHvH { get; set; } = (Rune)'┥';
-
-		/// <summary>
-		/// Box Drawings Right Tee - Light Horizontal and Heavy Vertical (U+2530) - ┨
-		/// </summary>
-		public Rune RightTeeHvV { get; set; } = (Rune)'┨';
-
-		/// <summary>
-		/// Box Drawings Right Tee - Heavy Vertical and Heavy Horizontal (U+252C) - ┫
-		/// </summary>
-		public Rune RightTeeHvDblH { get; set; } = (Rune)'┫';
-
-		/// <summary>
-		/// Box Drawings Top Tee - Single Vertical and Single Horizontal (U+252C) - ┬
-		/// </summary>
-		public Rune TopTee { get; set; } = (Rune)'┬';
-
-		/// <summary>
-		/// Box Drawings Top Tee - Single Vertical and Double Horizontal (U+2564) - ╤
-		/// </summary>
-		public Rune TopTeeDblH { get; set; } = (Rune)'╤';
-
-		/// <summary>
-		/// Box Drawings Top Tee - Double Vertical and Single Horizontal  (U+2565) - ╥
-		/// </summary>
-		public Rune TopTeeDblV { get; set; } = (Rune)'╥';
-
-		/// <summary>
-		/// Box Drawings Top Tee - Double Vertical and Double Horizontal (U+2566) - ╦
-		/// </summary>
-		public Rune TopTeeDbl { get; set; } = (Rune)'╦';
-
-		/// <summary>
-		/// Box Drawings Top Tee - Heavy Horizontal and Light Vertical (U+252F) - ┯
-		/// </summary>
-		public Rune TopTeeHvH { get; set; } = (Rune)'┯';
-
-		/// <summary>
-		/// Box Drawings Top Tee - Light Horizontal and Heavy Vertical (U+2537) - ┰
-		/// </summary>
-		public Rune TopTeeHvV { get; set; } = (Rune)'┰';
-
-		/// <summary>
-		/// Box Drawings Top Tee - Heavy Vertical and Heavy Horizontal (U+2533) - ┳
-		/// </summary>
-		public Rune TopTeeHvDblH { get; set; } = (Rune)'┳';
-
-		/// <summary>
-		/// Box Drawings Bottom Tee - Single Vertical and Single Horizontal (U+2534) - ┴
-		/// </summary>
-		public Rune BottomTee { get; set; } = (Rune)'┴';
-
-		/// <summary>
-		/// Box Drawings Bottom Tee - Single Vertical and Double Horizontal (U+2567) - ╧
-		/// </summary>
-		public Rune BottomTeeDblH { get; set; } = (Rune)'╧';
-
-		/// <summary>
-		/// Box Drawings Bottom Tee - Double Vertical and Single Horizontal (U+2568) - ╨
-		/// </summary>
-		public Rune BottomTeeDblV { get; set; } = (Rune)'╨';
-
-		/// <summary>
-		/// Box Drawings Bottom Tee - Double Vertical and Double Horizontal (U+2569) - ╩
-		/// </summary>
-		public Rune BottomTeeDbl { get; set; } = (Rune)'╩';
-
-		/// <summary>
-		/// Box Drawings Bottom Tee - Heavy Horizontal and Light Vertical (U+2535) - ┷
-		/// </summary>
-		public Rune BottomTeeHvH { get; set; } = (Rune)'┷';
-
-		/// <summary>
-		/// Box Drawings Bottom Tee - Light Horizontal and Heavy Vertical (U+253D) - ┸
-		/// </summary>
-		public Rune BottomTeeHvV { get; set; } = (Rune)'┸';
-
-		/// <summary>
-		/// Box Drawings Bottom Tee - Heavy Vertical and Heavy Horizontal (U+2539) - ┻
-		/// </summary>
-		public Rune BottomTeeHvDblH { get; set; } = (Rune)'┻';
-
-		#endregion
-
-		#region ----------------- Crosses -----------------
-		/// <summary>
-		/// Box Drawings Cross - Single Vertical and Single Horizontal (U+253C) - ┼
-		/// </summary>
-		public Rune Cross { get; set; } = (Rune)'┼';
-
-		/// <summary>
-		/// Box Drawings Cross - Single Vertical and Double Horizontal (U+256A) - ╪
-		/// </summary>
-		public Rune CrossDblH { get; set; } = (Rune)'╪';
-
-		/// <summary>
-		/// Box Drawings Cross - Double Vertical and Single Horizontal (U+256B) - ╫
-		/// </summary>
-		public Rune CrossDblV { get; set; } = (Rune)'╫';
-
-		/// <summary>
-		/// Box Drawings Cross - Double Vertical and Double Horizontal (U+256C) - ╬
-		/// </summary>
-		public Rune CrossDbl { get; set; } = (Rune)'╬';
-
-		/// <summary>
-		/// Box Drawings Cross - Heavy Horizontal and Light Vertical (U+253F) - ┿
-		/// </summary>
-		public Rune CrossHvH { get; set; } = (Rune)'┿';
-
-		/// <summary>
-		/// Box Drawings Cross - Light Horizontal and Heavy Vertical (U+2541) - ╂
-		/// </summary>
-		public Rune CrossHvV { get; set; } = (Rune)'╂';
-
-		/// <summary>
-		/// Box Drawings Cross - Heavy Vertical and Heavy Horizontal (U+254B) - ╋
-		/// </summary>
-		public Rune CrossHv { get; set; } = (Rune)'╋';
-		#endregion
-	}
+namespace Terminal.Gui;
+
+/// <summary>Defines the standard set of glyphs used to draw checkboxes, lines, borders, etc...</summary>
+/// <remarks>
+///     <para>
+///         Access with <see cref="CM.Glyphs"/> (which is a global using alias for
+///         <see cref="ConfigurationManager.Glyphs"/>).
+///     </para>
+///     <para>
+///         The default glyphs can be changed via the <see cref="ConfigurationManager"/>. Within a <c>config.json</c>
+///         file The Json property name is the property name prefixed with "Glyphs.".
+///     </para>
+///     <para>
+///         The JSon property can be either a decimal number or a string. The string may be one of: - A unicode char
+///         (e.g. "☑") - A hex value in U+ format (e.g. "U+2611") - A hex value in UTF-16 format (e.g. "\\u2611")
+///     </para>
+/// </remarks>
+public class GlyphDefinitions
+{
+    /// <summary>File icon.  Defaults to ☰ (Trigram For Heaven)</summary>
+    public Rune File { get; set; } = (Rune)'☰';
+
+    /// <summary>Folder icon.  Defaults to ꤉ (Kayah Li Digit Nine)</summary>
+    public Rune Folder { get; set; } = (Rune)'꤉';
+
+    /// <summary>Horizontal Ellipsis - … U+2026</summary>
+    public Rune HorizontalEllipsis { get; set; } = (Rune)'…';
+
+    /// <summary>Vertical Four Dots - ⁞ U+205e</summary>
+    public Rune VerticalFourDots { get; set; } = (Rune)'⁞';
+
+    #region ----------------- Single Glyphs -----------------
+
+    /// <summary>Checked indicator (e.g. for <see cref="ListView"/> and <see cref="CheckBox"/>).</summary>
+    public Rune Checked { get; set; } = (Rune)'☑';
+
+    /// <summary>Not Checked indicator (e.g. for <see cref="ListView"/> and <see cref="CheckBox"/>).</summary>
+    public Rune UnChecked { get; set; } = (Rune)'☐';
+
+    /// <summary>Null Checked indicator (e.g. for <see cref="ListView"/> and <see cref="CheckBox"/>).</summary>
+    public Rune NullChecked { get; set; } = (Rune)'☒';
+
+    /// <summary>Selected indicator  (e.g. for <see cref="ListView"/> and <see cref="RadioGroup"/>).</summary>
+    public Rune Selected { get; set; } = (Rune)'◉';
+
+    /// <summary>Not Selected indicator (e.g. for <see cref="ListView"/> and <see cref="RadioGroup"/>).</summary>
+    public Rune UnSelected { get; set; } = (Rune)'○';
+
+    /// <summary>Horizontal arrow.</summary>
+    public Rune RightArrow { get; set; } = (Rune)'►';
+
+    /// <summary>Left arrow.</summary>
+    public Rune LeftArrow { get; set; } = (Rune)'◄';
+
+    /// <summary>Down arrow.</summary>
+    public Rune DownArrow { get; set; } = (Rune)'▼';
+
+    /// <summary>Vertical arrow.</summary>
+    public Rune UpArrow { get; set; } = (Rune)'▲';
+
+    /// <summary>Left default indicator (e.g. for <see cref="Button"/>.</summary>
+    public Rune LeftDefaultIndicator { get; set; } = (Rune)'►';
+
+    /// <summary>Horizontal default indicator (e.g. for <see cref="Button"/>.</summary>
+    public Rune RightDefaultIndicator { get; set; } = (Rune)'◄';
+
+    /// <summary>Left Bracket (e.g. for <see cref="Button"/>. Default is (U+005B) - [.</summary>
+    public Rune LeftBracket { get; set; } = (Rune)'⟦';
+
+    /// <summary>Horizontal Bracket (e.g. for <see cref="Button"/>. Default is (U+005D) - ].</summary>
+    public Rune RightBracket { get; set; } = (Rune)'⟧';
+
+    /// <summary>Half block meter segment (e.g. for <see cref="ProgressBar"/>).</summary>
+    public Rune BlocksMeterSegment { get; set; } = (Rune)'▌';
+
+    /// <summary>Continuous block meter segment (e.g. for <see cref="ProgressBar"/>).</summary>
+    public Rune ContinuousMeterSegment { get; set; } = (Rune)'█';
+
+    /// <summary>Stipple pattern (e.g. for <see cref="ScrollBarView"/>). Default is Light Shade (U+2591) - ░.</summary>
+    public Rune Stipple { get; set; } = (Rune)'░';
+
+    /// <summary>Diamond (e.g. for <see cref="ScrollBarView"/>. Default is Lozenge (U+25CA) - ◊.</summary>
+    public Rune Diamond { get; set; } = (Rune)'◊';
+
+    /// <summary>Close. Default is Heavy Ballot X (U+2718) - ✘.</summary>
+    public Rune Close { get; set; } = (Rune)'✘';
+
+    /// <summary>Minimize. Default is Lower Horizontal Shadowed White Circle (U+274F) - ❏.</summary>
+    public Rune Minimize { get; set; } = (Rune)'❏';
+
+    /// <summary>Maximize. Default is Upper Horizontal Shadowed White Circle (U+273D) - ✽.</summary>
+    public Rune Maximize { get; set; } = (Rune)'✽';
+
+    /// <summary>Dot. Default is (U+2219) - ∙.</summary>
+    public Rune Dot { get; set; } = (Rune)'∙';
+
+    /// <summary>Black Circle . Default is (U+025cf) - ●.</summary>
+    public Rune BlackCircle { get; set; } = (Rune)'●'; // Black Circle - ● U+025cf
+
+    /// <summary>Expand (e.g. for <see cref="TreeView"/>.</summary>
+    public Rune Expand { get; set; } = (Rune)'+';
+
+    /// <summary>Expand (e.g. for <see cref="TreeView"/>.</summary>
+    public Rune Collapse { get; set; } = (Rune)'-';
+
+    /// <summary>Identical To (U+226)</summary>
+    public Rune IdenticalTo { get; set; } = (Rune)'≡';
+
+    /// <summary>Apple (non-BMP). Because snek. And because it's an example of a non-BMP surrogate pair. See Issue #2610.</summary>
+    public Rune Apple { get; set; } = "🍎".ToRunes () [0]; // nonBMP
+
+    /// <summary>Apple (BMP). Because snek. See Issue #2610.</summary>
+    public Rune AppleBMP { get; set; } = (Rune)'❦';
+
+    ///// <summary>
+    ///// A nonprintable (low surrogate) that should fail to ctor.
+    ///// </summary>
+    //public Rune InvalidGlyph { get; set; } = (Rune)'\ud83d';
+
+    #endregion
+
+    #region ----------------- Lines -----------------
+
+    /// <summary>Box Drawings Horizontal Line - Light (U+2500) - ─</summary>
+    public Rune HLine { get; set; } = (Rune)'─';
+
+    /// <summary>Box Drawings Vertical Line - Light (U+2502) - │</summary>
+    public Rune VLine { get; set; } = (Rune)'│';
+
+    /// <summary>Box Drawings Double Horizontal (U+2550) - ═</summary>
+    public Rune HLineDbl { get; set; } = (Rune)'═';
+
+    /// <summary>Box Drawings Double Vertical (U+2551) - ║</summary>
+    public Rune VLineDbl { get; set; } = (Rune)'║';
+
+    /// <summary>Box Drawings Heavy Double Dash Horizontal (U+254D) - ╍</summary>
+    public Rune HLineHvDa2 { get; set; } = (Rune)'╍';
+
+    /// <summary>Box Drawings Heavy Triple Dash Vertical (U+2507) - ┇</summary>
+    public Rune VLineHvDa3 { get; set; } = (Rune)'┇';
+
+    /// <summary>Box Drawings Heavy Triple Dash Horizontal (U+2505) - ┅</summary>
+    public Rune HLineHvDa3 { get; set; } = (Rune)'┅';
+
+    /// <summary>Box Drawings Heavy Quadruple Dash Horizontal (U+2509) - ┉</summary>
+    public Rune HLineHvDa4 { get; set; } = (Rune)'┉';
+
+    /// <summary>Box Drawings Heavy Double Dash Vertical (U+254F) - ╏</summary>
+    public Rune VLineHvDa2 { get; set; } = (Rune)'╏';
+
+    /// <summary>Box Drawings Heavy Quadruple Dash Vertical (U+250B) - ┋</summary>
+    public Rune VLineHvDa4 { get; set; } = (Rune)'┋';
+
+    /// <summary>Box Drawings Light Double Dash Horizontal (U+254C) - ╌</summary>
+    public Rune HLineDa2 { get; set; } = (Rune)'╌';
+
+    /// <summary>Box Drawings Light Triple Dash Vertical (U+2506) - ┆</summary>
+    public Rune VLineDa3 { get; set; } = (Rune)'┆';
+
+    /// <summary>Box Drawings Light Triple Dash Horizontal (U+2504) - ┄</summary>
+    public Rune HLineDa3 { get; set; } = (Rune)'┄';
+
+    /// <summary>Box Drawings Light Quadruple Dash Horizontal (U+2508) - ┈</summary>
+    public Rune HLineDa4 { get; set; } = (Rune)'┈';
+
+    /// <summary>Box Drawings Light Double Dash Vertical (U+254E) - ╎</summary>
+    public Rune VLineDa2 { get; set; } = (Rune)'╎';
+
+    /// <summary>Box Drawings Light Quadruple Dash Vertical (U+250A) - ┊</summary>
+    public Rune VLineDa4 { get; set; } = (Rune)'┊';
+
+    /// <summary>Box Drawings Heavy Horizontal (U+2501) - ━</summary>
+    public Rune HLineHv { get; set; } = (Rune)'━';
+
+    /// <summary>Box Drawings Heavy Vertical (U+2503) - ┃</summary>
+    public Rune VLineHv { get; set; } = (Rune)'┃';
+
+    /// <summary>Box Drawings Light Left (U+2574) - ╴</summary>
+    public Rune HalfLeftLine { get; set; } = (Rune)'╴';
+
+    /// <summary>Box Drawings Light Vertical (U+2575) - ╵</summary>
+    public Rune HalfTopLine { get; set; } = (Rune)'╵';
+
+    /// <summary>Box Drawings Light Horizontal (U+2576) - ╶</summary>
+    public Rune HalfRightLine { get; set; } = (Rune)'╶';
+
+    /// <summary>Box Drawings Light Down (U+2577) - ╷</summary>
+    public Rune HalfBottomLine { get; set; } = (Rune)'╷';
+
+    /// <summary>Box Drawings Heavy Left (U+2578) - ╸</summary>
+    public Rune HalfLeftLineHv { get; set; } = (Rune)'╸';
+
+    /// <summary>Box Drawings Heavy Vertical (U+2579) - ╹</summary>
+    public Rune HalfTopLineHv { get; set; } = (Rune)'╹';
+
+    /// <summary>Box Drawings Heavy Horizontal (U+257A) - ╺</summary>
+    public Rune HalfRightLineHv { get; set; } = (Rune)'╺';
+
+    /// <summary>Box Drawings Light Vertical and Horizontal (U+257B) - ╻</summary>
+    public Rune HalfBottomLineLt { get; set; } = (Rune)'╻';
+
+    /// <summary>Box Drawings Light Horizontal and Heavy Horizontal (U+257C) - ╼</summary>
+    public Rune RightSideLineLtHv { get; set; } = (Rune)'╼';
+
+    /// <summary>Box Drawings Light Vertical and Heavy Horizontal (U+257D) - ╽</summary>
+    public Rune BottomSideLineLtHv { get; set; } = (Rune)'╽';
+
+    /// <summary>Box Drawings Heavy Left and Light Horizontal (U+257E) - ╾</summary>
+    public Rune LeftSideLineHvLt { get; set; } = (Rune)'╾';
+
+    /// <summary>Box Drawings Heavy Vertical and Light Horizontal (U+257F) - ╿</summary>
+    public Rune TopSideLineHvLt { get; set; } = (Rune)'╿';
+
+    #endregion
+
+    #region ----------------- Upper Left Corners -----------------
+
+    /// <summary>Box Drawings Upper Left Corner - Light Vertical and Light Horizontal (U+250C) - ┌</summary>
+    public Rune ULCorner { get; set; } = (Rune)'┌';
+
+    /// <summary>Box Drawings Upper Left Corner -  Double (U+2554) - ╔</summary>
+    public Rune ULCornerDbl { get; set; } = (Rune)'╔';
+
+    /// <summary>Box Drawings Upper Left Corner - Light Arc Down and Horizontal (U+256D) - ╭</summary>
+    public Rune ULCornerR { get; set; } = (Rune)'╭';
+
+    /// <summary>Box Drawings Heavy Down and Horizontal (U+250F) - ┏</summary>
+    public Rune ULCornerHv { get; set; } = (Rune)'┏';
+
+    /// <summary>Box Drawings Down Heavy and Horizontal Light (U+251E) - ┎</summary>
+    public Rune ULCornerHvLt { get; set; } = (Rune)'┎';
+
+    /// <summary>Box Drawings Down Light and Horizontal Heavy (U+250D) - ┎</summary>
+    public Rune ULCornerLtHv { get; set; } = (Rune)'┍';
+
+    /// <summary>Box Drawings Double Down and Single Horizontal (U+2553) - ╓</summary>
+    public Rune ULCornerDblSingle { get; set; } = (Rune)'╓';
+
+    /// <summary>Box Drawings Single Down and Double Horizontal (U+2552) - ╒</summary>
+    public Rune ULCornerSingleDbl { get; set; } = (Rune)'╒';
+
+    #endregion
+
+    #region ----------------- Lower Left Corners -----------------
+
+    /// <summary>Box Drawings Lower Left Corner - Light Vertical and Light Horizontal (U+2514) - └</summary>
+    public Rune LLCorner { get; set; } = (Rune)'└';
+
+    /// <summary>Box Drawings Heavy Vertical and Horizontal (U+2517) - ┗</summary>
+    public Rune LLCornerHv { get; set; } = (Rune)'┗';
+
+    /// <summary>Box Drawings Heavy Vertical and Horizontal Light (U+2516) - ┖</summary>
+    public Rune LLCornerHvLt { get; set; } = (Rune)'┖';
+
+    /// <summary>Box Drawings Vertical Light and Horizontal Heavy (U+2511) - ┕</summary>
+    public Rune LLCornerLtHv { get; set; } = (Rune)'┕';
+
+    /// <summary>Box Drawings Double Vertical and Double Left (U+255A) - ╚</summary>
+    public Rune LLCornerDbl { get; set; } = (Rune)'╚';
+
+    /// <summary>Box Drawings Single Vertical and Double Left (U+2558) - ╘</summary>
+    public Rune LLCornerSingleDbl { get; set; } = (Rune)'╘';
+
+    /// <summary>Box Drawings Double Down and Single Left (U+2559) - ╙</summary>
+    public Rune LLCornerDblSingle { get; set; } = (Rune)'╙';
+
+    /// <summary>Box Drawings Upper Left Corner - Light Arc Down and Left (U+2570) - ╰</summary>
+    public Rune LLCornerR { get; set; } = (Rune)'╰';
+
+    #endregion
+
+    #region ----------------- Upper Right Corners -----------------
+
+    /// <summary>Box Drawings Upper Horizontal Corner - Light Vertical and Light Horizontal (U+2510) - ┐</summary>
+    public Rune URCorner { get; set; } = (Rune)'┐';
+
+    /// <summary>Box Drawings Upper Horizontal Corner - Double Vertical and Double Horizontal (U+2557) - ╗</summary>
+    public Rune URCornerDbl { get; set; } = (Rune)'╗';
+
+    /// <summary>Box Drawings Upper Horizontal Corner - Light Arc Vertical and Horizontal (U+256E) - ╮</summary>
+    public Rune URCornerR { get; set; } = (Rune)'╮';
+
+    /// <summary>Box Drawings Heavy Down and Left (U+2513) - ┓</summary>
+    public Rune URCornerHv { get; set; } = (Rune)'┓';
+
+    /// <summary>Box Drawings Heavy Vertical and Left Down Light (U+2511) - ┑</summary>
+    public Rune URCornerHvLt { get; set; } = (Rune)'┑';
+
+    /// <summary>Box Drawings Down Light and Horizontal Heavy (U+2514) - ┒</summary>
+    public Rune URCornerLtHv { get; set; } = (Rune)'┒';
+
+    /// <summary>Box Drawings Double Vertical and Single Left (U+2556) - ╖</summary>
+    public Rune URCornerDblSingle { get; set; } = (Rune)'╖';
+
+    /// <summary>Box Drawings Single Vertical and Double Left (U+2555) - ╕</summary>
+    public Rune URCornerSingleDbl { get; set; } = (Rune)'╕';
+
+    #endregion
+
+    #region ----------------- Lower Right Corners -----------------
+
+    /// <summary>Box Drawings Lower Right Corner - Light (U+2518) - ┘</summary>
+    public Rune LRCorner { get; set; } = (Rune)'┘';
+
+    /// <summary>Box Drawings Lower Right Corner - Double (U+255D) - ╝</summary>
+    public Rune LRCornerDbl { get; set; } = (Rune)'╝';
+
+    /// <summary>Box Drawings Lower Right Corner - Rounded (U+256F) - ╯</summary>
+    public Rune LRCornerR { get; set; } = (Rune)'╯';
+
+    /// <summary>Box Drawings Lower Right Corner - Heavy (U+251B) - ┛</summary>
+    public Rune LRCornerHv { get; set; } = (Rune)'┛';
+
+    /// <summary>Box Drawings Lower Right Corner - Double Vertical and Single Horizontal (U+255C) - ╜</summary>
+    public Rune LRCornerDblSingle { get; set; } = (Rune)'╜';
+
+    /// <summary>Box Drawings Lower Right Corner - Single Vertical and Double Horizontal (U+255B) - ╛</summary>
+    public Rune LRCornerSingleDbl { get; set; } = (Rune)'╛';
+
+    /// <summary>Box Drawings Lower Right Corner - Light Vertical and Heavy Horizontal (U+2519) - ┙</summary>
+    public Rune LRCornerLtHv { get; set; } = (Rune)'┙';
+
+    /// <summary>Box Drawings Lower Right Corner - Heavy Vertical and Light Horizontal (U+251A) - ┚</summary>
+    public Rune LRCornerHvLt { get; set; } = (Rune)'┚';
+
+    #endregion
+
+    #region ----------------- Tees -----------------
+
+    /// <summary>Box Drawings Left Tee - Single Vertical and Single Horizontal (U+251C) - ├</summary>
+    public Rune LeftTee { get; set; } = (Rune)'├';
+
+    /// <summary>Box Drawings Left Tee - Single Vertical and Double Horizontal (U+255E) - ╞</summary>
+    public Rune LeftTeeDblH { get; set; } = (Rune)'╞';
+
+    /// <summary>Box Drawings Left Tee - Double Vertical and Single Horizontal (U+255F) - ╟</summary>
+    public Rune LeftTeeDblV { get; set; } = (Rune)'╟';
+
+    /// <summary>Box Drawings Left Tee - Double Vertical and Double Horizontal (U+2560) - ╠</summary>
+    public Rune LeftTeeDbl { get; set; } = (Rune)'╠';
+
+    /// <summary>Box Drawings Left Tee - Heavy Horizontal and Light Vertical (U+2523) - ┝</summary>
+    public Rune LeftTeeHvH { get; set; } = (Rune)'┝';
+
+    /// <summary>Box Drawings Left Tee - Light Horizontal and Heavy Vertical (U+252B) - ┠</summary>
+    public Rune LeftTeeHvV { get; set; } = (Rune)'┠';
+
+    /// <summary>Box Drawings Left Tee - Heavy Vertical and Heavy Horizontal (U+2527) - ┣</summary>
+    public Rune LeftTeeHvDblH { get; set; } = (Rune)'┣';
+
+    /// <summary>Box Drawings Righ Tee - Single Vertical and Single Horizontal (U+2524) - ┤</summary>
+    public Rune RightTee { get; set; } = (Rune)'┤';
+
+    /// <summary>Box Drawings Right Tee - Single Vertical and Double Horizontal (U+2561) - ╡</summary>
+    public Rune RightTeeDblH { get; set; } = (Rune)'╡';
+
+    /// <summary>Box Drawings Right Tee - Double Vertical and Single Horizontal (U+2562) - ╢</summary>
+    public Rune RightTeeDblV { get; set; } = (Rune)'╢';
+
+    /// <summary>Box Drawings Right Tee - Double Vertical and Double Horizontal (U+2563) - ╣</summary>
+    public Rune RightTeeDbl { get; set; } = (Rune)'╣';
+
+    /// <summary>Box Drawings Right Tee - Heavy Horizontal and Light Vertical (U+2528) - ┥</summary>
+    public Rune RightTeeHvH { get; set; } = (Rune)'┥';
+
+    /// <summary>Box Drawings Right Tee - Light Horizontal and Heavy Vertical (U+2530) - ┨</summary>
+    public Rune RightTeeHvV { get; set; } = (Rune)'┨';
+
+    /// <summary>Box Drawings Right Tee - Heavy Vertical and Heavy Horizontal (U+252C) - ┫</summary>
+    public Rune RightTeeHvDblH { get; set; } = (Rune)'┫';
+
+    /// <summary>Box Drawings Top Tee - Single Vertical and Single Horizontal (U+252C) - ┬</summary>
+    public Rune TopTee { get; set; } = (Rune)'┬';
+
+    /// <summary>Box Drawings Top Tee - Single Vertical and Double Horizontal (U+2564) - ╤</summary>
+    public Rune TopTeeDblH { get; set; } = (Rune)'╤';
+
+    /// <summary>Box Drawings Top Tee - Double Vertical and Single Horizontal  (U+2565) - ╥</summary>
+    public Rune TopTeeDblV { get; set; } = (Rune)'╥';
+
+    /// <summary>Box Drawings Top Tee - Double Vertical and Double Horizontal (U+2566) - ╦</summary>
+    public Rune TopTeeDbl { get; set; } = (Rune)'╦';
+
+    /// <summary>Box Drawings Top Tee - Heavy Horizontal and Light Vertical (U+252F) - ┯</summary>
+    public Rune TopTeeHvH { get; set; } = (Rune)'┯';
+
+    /// <summary>Box Drawings Top Tee - Light Horizontal and Heavy Vertical (U+2537) - ┰</summary>
+    public Rune TopTeeHvV { get; set; } = (Rune)'┰';
+
+    /// <summary>Box Drawings Top Tee - Heavy Vertical and Heavy Horizontal (U+2533) - ┳</summary>
+    public Rune TopTeeHvDblH { get; set; } = (Rune)'┳';
+
+    /// <summary>Box Drawings Bottom Tee - Single Vertical and Single Horizontal (U+2534) - ┴</summary>
+    public Rune BottomTee { get; set; } = (Rune)'┴';
+
+    /// <summary>Box Drawings Bottom Tee - Single Vertical and Double Horizontal (U+2567) - ╧</summary>
+    public Rune BottomTeeDblH { get; set; } = (Rune)'╧';
+
+    /// <summary>Box Drawings Bottom Tee - Double Vertical and Single Horizontal (U+2568) - ╨</summary>
+    public Rune BottomTeeDblV { get; set; } = (Rune)'╨';
+
+    /// <summary>Box Drawings Bottom Tee - Double Vertical and Double Horizontal (U+2569) - ╩</summary>
+    public Rune BottomTeeDbl { get; set; } = (Rune)'╩';
+
+    /// <summary>Box Drawings Bottom Tee - Heavy Horizontal and Light Vertical (U+2535) - ┷</summary>
+    public Rune BottomTeeHvH { get; set; } = (Rune)'┷';
+
+    /// <summary>Box Drawings Bottom Tee - Light Horizontal and Heavy Vertical (U+253D) - ┸</summary>
+    public Rune BottomTeeHvV { get; set; } = (Rune)'┸';
+
+    /// <summary>Box Drawings Bottom Tee - Heavy Vertical and Heavy Horizontal (U+2539) - ┻</summary>
+    public Rune BottomTeeHvDblH { get; set; } = (Rune)'┻';
+
+    #endregion
+
+    #region ----------------- Crosses -----------------
+
+    /// <summary>Box Drawings Cross - Single Vertical and Single Horizontal (U+253C) - ┼</summary>
+    public Rune Cross { get; set; } = (Rune)'┼';
+
+    /// <summary>Box Drawings Cross - Single Vertical and Double Horizontal (U+256A) - ╪</summary>
+    public Rune CrossDblH { get; set; } = (Rune)'╪';
+
+    /// <summary>Box Drawings Cross - Double Vertical and Single Horizontal (U+256B) - ╫</summary>
+    public Rune CrossDblV { get; set; } = (Rune)'╫';
+
+    /// <summary>Box Drawings Cross - Double Vertical and Double Horizontal (U+256C) - ╬</summary>
+    public Rune CrossDbl { get; set; } = (Rune)'╬';
+
+    /// <summary>Box Drawings Cross - Heavy Horizontal and Light Vertical (U+253F) - ┿</summary>
+    public Rune CrossHvH { get; set; } = (Rune)'┿';
+
+    /// <summary>Box Drawings Cross - Light Horizontal and Heavy Vertical (U+2541) - ╂</summary>
+    public Rune CrossHvV { get; set; } = (Rune)'╂';
+
+    /// <summary>Box Drawings Cross - Heavy Vertical and Heavy Horizontal (U+254B) - ╋</summary>
+    public Rune CrossHv { get; set; } = (Rune)'╋';
+
+    #endregion
 }

+ 27 - 0
Terminal.Gui/Drawing/ICustomColorFormatter.cs

@@ -0,0 +1,27 @@
+#nullable enable
+namespace Terminal.Gui;
+
+/// <summary>An interface to support custom formatting and parsing of <see cref="Color"/> values.</summary>
+public interface ICustomColorFormatter : IFormatProvider, ICustomFormatter
+{
+    /// <summary>
+    ///     A method that returns a <see langword="string"/> based on the <paramref name="formatString"/> specified and
+    ///     the byte parameters <paramref name="r"/>, <paramref name="g"/>, <paramref name="b"/>, and <paramref name="a"/>,
+    ///     which are provided by <see cref="Color"/>
+    /// </summary>
+    /// <param name="formatString"></param>
+    /// <param name="r"></param>
+    /// <param name="g"></param>
+    /// <param name="b"></param>
+    /// <param name="a"></param>
+    /// <returns></returns>
+    string Format (string? formatString, byte r, byte g, byte b, byte a);
+
+    /// <summary>A method that returns a <see cref="Color"/> value based on the <paramref name="text"/> specified.</summary>
+    /// <param name="text">
+    ///     A string or other <see cref="ReadOnlySpan{T}"/> of <see langword="char"/> to parse as a
+    ///     <see cref="Color"/>.
+    /// </param>
+    /// <returns>A <see cref="Color"/> value equivalent to <paramref name="text"/>.</returns>
+    Color Parse (ReadOnlySpan<char> text);
+}

+ 20 - 0
Terminal.Gui/Drawing/IntersectionDefinition.cs

@@ -0,0 +1,20 @@
+namespace Terminal.Gui;
+
+internal class IntersectionDefinition
+{
+    internal IntersectionDefinition (Point point, IntersectionType type, StraightLine line)
+    {
+        Point = point;
+        Type = type;
+        Line = line;
+    }
+
+    /// <summary>The line that intersects <see cref="Point"/></summary>
+    internal StraightLine Line { get; }
+
+    /// <summary>The point at which the intersection happens</summary>
+    internal Point Point { get; }
+
+    /// <summary>Defines how <see cref="Line"/> position relates to <see cref="Point"/>.</summary>
+    internal IntersectionType Type { get; }
+}

+ 19 - 0
Terminal.Gui/Drawing/IntersectionRuneType.cs

@@ -0,0 +1,19 @@
+namespace Terminal.Gui;
+
+/// <summary>The type of Rune that we will use before considering double width, curved borders etc</summary>
+internal enum IntersectionRuneType
+{
+    None,
+    Dot,
+    ULCorner,
+    URCorner,
+    LLCorner,
+    LRCorner,
+    TopTee,
+    BottomTee,
+    RightTee,
+    LeftTee,
+    Cross,
+    HLine,
+    VLine
+}

+ 28 - 0
Terminal.Gui/Drawing/IntersectionType.cs

@@ -0,0 +1,28 @@
+namespace Terminal.Gui;
+
+internal enum IntersectionType
+{
+    /// <summary>There is no intersection</summary>
+    None,
+
+    /// <summary>A line passes directly over this point traveling along the horizontal axis</summary>
+    PassOverHorizontal,
+
+    /// <summary>A line passes directly over this point traveling along the vertical axis</summary>
+    PassOverVertical,
+
+    /// <summary>A line starts at this point and is traveling up</summary>
+    StartUp,
+
+    /// <summary>A line starts at this point and is traveling right</summary>
+    StartRight,
+
+    /// <summary>A line starts at this point and is traveling down</summary>
+    StartDown,
+
+    /// <summary>A line starts at this point and is traveling left</summary>
+    StartLeft,
+
+    /// <summary>A line exists at this point who has 0 length</summary>
+    Dot
+}

+ 846 - 837
Terminal.Gui/Drawing/LineCanvas.cs

@@ -1,837 +1,846 @@
-#nullable enable
-using System;
-using System.Collections;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-namespace Terminal.Gui {
-
-	/// <summary>
-	/// Defines the style of lines for a <see cref="LineCanvas"/>.
-	/// </summary>
-	public enum LineStyle {
-		/// <summary>
-		/// No border is drawn.
-		/// </summary>
-		None,
-		/// <summary>
-		/// The border is drawn using thin line CM.Glyphs.
-		/// </summary>
-		Single,
-		/// <summary>
-		/// The border is drawn using thin line glyphs with dashed (double and triple) straight lines.
-		/// </summary>
-		Dashed,
-		/// <summary>
-		/// The border is drawn using thin line glyphs with short dashed (triple and quadruple) straight lines.
-		/// </summary>
-		Dotted,
-		/// <summary>
-		/// The border is drawn using thin double line CM.Glyphs.
-		/// </summary>
-		Double,
-		/// <summary>
-		/// The border is drawn using heavy line CM.Glyphs.
-		/// </summary>
-		Heavy,
-		/// <summary>
-		/// The border is drawn using heavy line glyphs with dashed (double and triple) straight lines.
-		/// </summary>
-		HeavyDashed,
-		/// <summary>
-		/// The border is drawn using heavy line glyphs with short dashed (triple and quadruple) straight lines.
-		/// </summary>
-		HeavyDotted,
-		/// <summary>
-		/// The border is drawn using thin line glyphs with rounded corners.
-		/// </summary>
-		Rounded,
-		/// <summary>
-		/// The border is drawn using thin line glyphs with rounded corners and dashed (double and triple) straight lines.
-		/// </summary>
-		RoundedDashed,
-		/// <summary>
-		/// The border is drawn using thin line glyphs with rounded corners and short dashed (triple and quadruple) straight lines.
-		/// </summary>
-		RoundedDotted,
-		// TODO: Support Ruler
-		///// <summary> 
-		///// The border is drawn as a diagnostic ruler ("|123456789...").
-		///// </summary>
-		//Ruler
-	}
-
-	/// <summary>
-	/// Facilitates box drawing and line intersection detection
-	/// and rendering.  Does not support diagonal lines.
-	/// </summary>
-	public class LineCanvas : IDisposable {
-		/// <summary>
-		/// Creates a new instance.
-		/// </summary>
-		public LineCanvas()
-		{
-			// TODO: Refactor ConfigurationManager to not use an event handler for this.
-			// Instead, have it call a method on any class appropriately attributed
-			// to update the cached values. See Issue #2871
-			ConfigurationManager.Applied += ConfigurationManager_Applied;
-		}
-
-		/// <summary>
-		/// Creates a new instance with the given <paramref name="lines"/>.
-		/// </summary>
-		/// <param name="lines">Initial lines for the canvas.</param>
-		public LineCanvas (IEnumerable<StraightLine> lines) : this()
-		{
-			_lines = lines.ToList();
-		}
-
-		private void ConfigurationManager_Applied (object? sender, ConfigurationManagerEventArgs e)
-		{
-			foreach (var irr in runeResolvers) {
-				irr.Value.SetGlyphs ();
-			}
-		}
-
-		private List<StraightLine> _lines = new List<StraightLine> ();
-
-		/// <summary>
-		/// Gets the lines in the canvas.
-		/// </summary>
-		public IReadOnlyCollection<StraightLine> Lines { get { return _lines.AsReadOnly(); } }
-
-		Dictionary<IntersectionRuneType, IntersectionRuneResolver> runeResolvers = new Dictionary<IntersectionRuneType, IntersectionRuneResolver> {
-			{IntersectionRuneType.ULCorner,new ULIntersectionRuneResolver()},
-			{IntersectionRuneType.URCorner,new URIntersectionRuneResolver()},
-			{IntersectionRuneType.LLCorner,new LLIntersectionRuneResolver()},
-			{IntersectionRuneType.LRCorner,new LRIntersectionRuneResolver()},
-
-			{IntersectionRuneType.TopTee,new TopTeeIntersectionRuneResolver()},
-			{IntersectionRuneType.LeftTee,new LeftTeeIntersectionRuneResolver()},
-			{IntersectionRuneType.RightTee,new RightTeeIntersectionRuneResolver()},
-			{IntersectionRuneType.BottomTee,new BottomTeeIntersectionRuneResolver()},
-
-			{IntersectionRuneType.Cross,new CrossIntersectionRuneResolver()},
-			// TODO: Add other resolvers
-		};
-
-		/// <summary>
-		/// <para>
-		/// Adds a new <paramref name="length"/> long line to the canvas starting at <paramref name="start"/>.
-		/// </para>
-		/// <para>
-		/// Use positive <paramref name="length"/> for the line to extend Right and negative for Left
-		/// when <see cref="Orientation"/> is <see cref="Orientation.Horizontal"/>.
-		/// </para>
-		/// <para>
-		/// Use positive <paramref name="length"/> for the line to extend Down and negative for Up
-		/// when <see cref="Orientation"/> is <see cref="Orientation.Vertical"/>.
-		/// </para>
-		/// </summary>
-		/// <param name="start">Starting point.</param>
-		/// <param name="length">The length of line. 0 for an intersection (cross or T). Positive for Down/Right. Negative for Up/Left.</param>
-		/// <param name="orientation">The direction of the line.</param>
-		/// <param name="style">The style of line to use</param>
-		/// <param name="attribute"></param>
-		public void AddLine (Point start, int length, Orientation orientation, LineStyle style, Attribute? attribute = default)
-		{
-			_cachedBounds = Rect.Empty;
-			_lines.Add (new StraightLine (start, length, orientation, style, attribute));
-		}
-
-		/// <summary>
-		/// Adds a new line to the canvas
-		/// </summary>
-		/// <param name="line"></param>
-		public void AddLine (StraightLine line)
-		{
-			_cachedBounds = Rect.Empty;
-			_lines.Add (line);
-		}
-
-		/// <summary>
-		/// Removes the last line added to the canvas
-		/// </summary>
-		/// <returns></returns>
-		public StraightLine RemoveLastLine()
-		{
-			var l = _lines.LastOrDefault ();
-			if(l != null) {
-				_lines.Remove(l);
-			}
-
-			return l!;
-		}
-
-		/// <summary>
-		/// Clears all lines from the LineCanvas.
-		/// </summary>
-		public void Clear ()
-		{
-			_cachedBounds = Rect.Empty;
-			_lines.Clear ();
-		}
-
-		/// <summary>
-		/// Clears any cached states from the canvas
-		/// Call this method if you make changes to lines
-		/// that have already been added.
-		/// </summary>
-		public void ClearCache ()
-		{
-			_cachedBounds = Rect.Empty;
-		}
-		private Rect _cachedBounds;
-
-		/// <summary>
-		/// Gets the rectangle that describes the bounds of the canvas. Location is the coordinates of the 
-		/// line that is furthest left/top and Size is defined by the line that extends the furthest
-		/// right/bottom.
-		/// </summary>
-		public Rect Bounds {
-			get {
-				if (_cachedBounds.IsEmpty) {
-					if (_lines.Count == 0) {
-						return _cachedBounds;
-					}
-
-					Rect bounds = _lines [0].Bounds;
-
-					for (var i = 1; i < _lines.Count; i++) {
-						var line = _lines [i];
-						var lineBounds = line.Bounds;
-						bounds = Rect.Union (bounds, lineBounds);
-					}
-
-					if (bounds.Width == 0) {
-						bounds.Width = 1;
-					}
-
-					if (bounds.Height == 0) {
-						bounds.Height = 1;
-					}
-					_cachedBounds = new Rect (bounds.X, bounds.Y, bounds.Width, bounds.Height);
-				}
-
-				return _cachedBounds;
-			}
-		}
-
-		// TODO: Unless there's an obvious use case for this API we should delete it in favor of the
-		// simpler version that doensn't take an area.
-		/// <summary>
-		/// Evaluates the lines that have been added to the canvas and returns a map containing
-		/// the glyphs and their locations. The glyphs are the characters that should be rendered
-		/// so that all lines connect up with the appropriate intersection symbols. 
-		/// </summary>
-		/// <param name="inArea">A rectangle to constrain the search by.</param>
-		/// <returns>A map of the points within the canvas that intersect with <paramref name="inArea"/>.</returns>
-		public Dictionary<Point, Rune> GetMap (Rect inArea)
-		{
-			var map = new Dictionary<Point, Rune> ();
-
-			// walk through each pixel of the bitmap
-			for (int y = inArea.Y; y < inArea.Y + inArea.Height; y++) {
-				for (int x = inArea.X; x < inArea.X + inArea.Width; x++) {
-
-					var intersects = _lines
-						.Select (l => l.Intersects (x, y))
-						.Where (i => i != null)
-						.ToArray ();
-
-					var rune = GetRuneForIntersects (Application.Driver, intersects);
-
-					if (rune != null) {
-						map.Add (new Point (x, y), rune.Value);
-					}
-				}
-			}
-
-			return map;
-		}
-
-		/// <summary>
-		/// Evaluates the lines that have been added to the canvas and returns a map containing
-		/// the glyphs and their locations. The glyphs are the characters that should be rendered
-		/// so that all lines connect up with the appropriate intersection symbols. 
-		/// </summary>
-		/// <returns>A map of all the points within the canvas.</returns>
-		public Dictionary<Point, Cell> GetCellMap ()
-		{
-			var map = new Dictionary<Point, Cell> ();
-
-			// walk through each pixel of the bitmap
-			for (int y = Bounds.Y; y < Bounds.Y + Bounds.Height; y++) {
-				for (int x = Bounds.X; x < Bounds.X + Bounds.Width; x++) {
-
-					var intersects = _lines
-						.Select (l => l.Intersects (x, y))
-						.Where (i => i != null)
-						.ToArray ();
-
-					var cell = GetCellForIntersects (Application.Driver, intersects);
-
-					if (cell != null) {
-						map.Add (new Point (x, y), cell);
-					}
-				}
-			}
-
-			return map;
-		}
-
-		/// <summary>
-		/// Evaluates the lines that have been added to the canvas and returns a map containing
-		/// the glyphs and their locations. The glyphs are the characters that should be rendered
-		/// so that all lines connect up with the appropriate intersection symbols. 
-		/// </summary>
-		/// <returns>A map of all the points within the canvas.</returns>
-		public Dictionary<Point, Rune> GetMap () => GetMap (Bounds);
-
-		/// <summary>
-		/// Returns the contents of the line canvas rendered to a string. The string
-		/// will include all columns and rows, even if <see cref="Bounds"/> has negative coordinates. 
-		/// For example, if the canvas contains a single line that starts at (-1,-1) with a length of 2, the
-		/// rendered string will have a length of 2.
-		/// </summary>
-		/// <returns>The canvas rendered to a string.</returns>
-		public override string ToString ()
-		{
-			if (Bounds.IsEmpty) {
-				return string.Empty;
-			}
-
-			// Generate the rune map for the entire canvas
-			var runeMap = GetMap ();
-
-			// Create the rune canvas
-			Rune [,] canvas = new Rune [Bounds.Height, Bounds.Width];
-
-			// Copy the rune map to the canvas, adjusting for any negative coordinates
-			foreach (var kvp in runeMap) {
-				int x = kvp.Key.X - Bounds.X;
-				int y = kvp.Key.Y - Bounds.Y;
-				canvas [y, x] = kvp.Value;
-			}
-
-			// Convert the canvas to a string
-			StringBuilder sb = new StringBuilder ();
-			for (int y = 0; y < canvas.GetLength (0); y++) {
-				for (int x = 0; x < canvas.GetLength (1); x++) {
-					Rune r = canvas [y, x];
-					sb.Append (r.Value == 0 ? ' ' : r.ToString ());
-				}
-				if (y < canvas.GetLength (0) - 1) {
-					sb.AppendLine ();
-				}
-			}
-
-			return sb.ToString ();
-		}
-
-		private abstract class IntersectionRuneResolver {
-			internal Rune _round;
-			internal Rune _doubleH;
-			internal Rune _doubleV;
-			internal Rune _doubleBoth;
-			internal Rune _thickH;
-			internal Rune _thickV;
-			internal Rune _thickBoth;
-			internal Rune _normal;
-
-			public IntersectionRuneResolver()
-			{
-				SetGlyphs ();
-			}
-			
-			/// <summary>
-			/// Sets the glyphs used. Call this method after construction and any time 
-			/// ConfigurationManager has updated the settings.
-			/// </summary>
-			public abstract void SetGlyphs ();
-
-			public Rune? GetRuneForIntersects (ConsoleDriver driver, IntersectionDefinition? [] intersects)
-			{
-				var useRounded = intersects.Any (i => i?.Line.Length != 0 && (
-					i?.Line.Style == LineStyle.Rounded || i?.Line.Style == LineStyle.RoundedDashed || i?.Line.Style == LineStyle.RoundedDotted));
-
-				// Note that there aren't any glyphs for intersections of double lines with heavy lines
-
-				bool doubleHorizontal = intersects.Any (l => l?.Line.Orientation == Orientation.Horizontal && l.Line.Style == LineStyle.Double);
-				bool doubleVertical = intersects.Any (l => l?.Line.Orientation == Orientation.Vertical && l.Line.Style == LineStyle.Double);
-
-				bool thickHorizontal = intersects.Any (l => l?.Line.Orientation == Orientation.Horizontal && (
-					l.Line.Style == LineStyle.Heavy || l.Line.Style == LineStyle.HeavyDashed || l.Line.Style == LineStyle.HeavyDotted));
-				bool thickVertical = intersects.Any (l => l?.Line.Orientation == Orientation.Vertical && (
-					l.Line.Style == LineStyle.Heavy || l.Line.Style == LineStyle.HeavyDashed || l.Line.Style == LineStyle.HeavyDotted));
-
-				if (doubleHorizontal) {
-					return doubleVertical ? _doubleBoth : _doubleH;
-				}
-				if (doubleVertical) {
-					return _doubleV;
-				}
-
-				if (thickHorizontal) {
-					return thickVertical ? _thickBoth : _thickH;
-				}
-				if (thickVertical) {
-					return _thickV;
-				}
-
-				return useRounded ? _round : _normal;
-			}
-		}
-
-		private class ULIntersectionRuneResolver : IntersectionRuneResolver {
-			public override void SetGlyphs ()
-			{
-				_round = CM.Glyphs.ULCornerR;
-				_doubleH = CM.Glyphs.ULCornerSingleDbl;
-				_doubleV = CM.Glyphs.ULCornerDblSingle;
-				_doubleBoth = CM.Glyphs.ULCornerDbl;
-				_thickH = CM.Glyphs.ULCornerLtHv;
-				_thickV = CM.Glyphs.ULCornerHvLt;
-				_thickBoth = CM.Glyphs.ULCornerHv;
-				_normal = CM.Glyphs.ULCorner;
-			}
-		}
-		private class URIntersectionRuneResolver : IntersectionRuneResolver {
-			public override void SetGlyphs ()
-			{
-				_round = CM.Glyphs.URCornerR;
-				_doubleH = CM.Glyphs.URCornerSingleDbl;
-				_doubleV = CM.Glyphs.URCornerDblSingle;
-				_doubleBoth = CM.Glyphs.URCornerDbl;
-				_thickH = CM.Glyphs.URCornerHvLt;
-				_thickV = CM.Glyphs.URCornerLtHv;
-				_thickBoth = CM.Glyphs.URCornerHv;
-				_normal = CM.Glyphs.URCorner;
-			}
-		}
-		private class LLIntersectionRuneResolver : IntersectionRuneResolver {
-			public override void SetGlyphs ()
-			{
-				_round = CM.Glyphs.LLCornerR;
-				_doubleH = CM.Glyphs.LLCornerSingleDbl;
-				_doubleV = CM.Glyphs.LLCornerDblSingle;
-				_doubleBoth = CM.Glyphs.LLCornerDbl;
-				_thickH = CM.Glyphs.LLCornerLtHv;
-				_thickV = CM.Glyphs.LLCornerHvLt;
-				_thickBoth = CM.Glyphs.LLCornerHv;
-				_normal = CM.Glyphs.LLCorner;
-			}
-
-		}
-		private class LRIntersectionRuneResolver : IntersectionRuneResolver {
-			public override void SetGlyphs ()
-			{
-				_round = CM.Glyphs.LRCornerR;
-				_doubleH = CM.Glyphs.LRCornerSingleDbl;
-				_doubleV = CM.Glyphs.LRCornerDblSingle;
-				_doubleBoth = CM.Glyphs.LRCornerDbl;
-				_thickH = CM.Glyphs.LRCornerLtHv;
-				_thickV = CM.Glyphs.LRCornerHvLt;
-				_thickBoth = CM.Glyphs.LRCornerHv;
-				_normal = CM.Glyphs.LRCorner;
-			}
-		}
-
-		private class TopTeeIntersectionRuneResolver : IntersectionRuneResolver {
-			public override void SetGlyphs ()
-			{
-				_round = CM.Glyphs.TopTee;
-				_doubleH = CM.Glyphs.TopTeeDblH;
-				_doubleV = CM.Glyphs.TopTeeDblV;
-				_doubleBoth = CM.Glyphs.TopTeeDbl;
-				_thickH = CM.Glyphs.TopTeeHvH;
-				_thickV = CM.Glyphs.TopTeeHvV;
-				_thickBoth = CM.Glyphs.TopTeeHvDblH;
-				_normal = CM.Glyphs.TopTee;
-			}
-		}
-		private class LeftTeeIntersectionRuneResolver : IntersectionRuneResolver {
-			public override void SetGlyphs ()
-			{
-				_round = CM.Glyphs.LeftTee;
-				_doubleH = CM.Glyphs.LeftTeeDblH;
-				_doubleV = CM.Glyphs.LeftTeeDblV;
-				_doubleBoth = CM.Glyphs.LeftTeeDbl;
-				_thickH = CM.Glyphs.LeftTeeHvH;
-				_thickV = CM.Glyphs.LeftTeeHvV;
-				_thickBoth = CM.Glyphs.LeftTeeHvDblH;
-				_normal = CM.Glyphs.LeftTee;
-			}
-		}
-		private class RightTeeIntersectionRuneResolver : IntersectionRuneResolver {
-			public override void SetGlyphs ()
-			{
-				_round = CM.Glyphs.RightTee;
-				_doubleH = CM.Glyphs.RightTeeDblH;
-				_doubleV = CM.Glyphs.RightTeeDblV;
-				_doubleBoth = CM.Glyphs.RightTeeDbl;
-				_thickH = CM.Glyphs.RightTeeHvH;
-				_thickV = CM.Glyphs.RightTeeHvV;
-				_thickBoth = CM.Glyphs.RightTeeHvDblH;
-				_normal = CM.Glyphs.RightTee;
-			}
-		}
-		private class BottomTeeIntersectionRuneResolver : IntersectionRuneResolver {
-			public override void SetGlyphs ()
-			{
-				_round = CM.Glyphs.BottomTee;
-				_doubleH = CM.Glyphs.BottomTeeDblH;
-				_doubleV = CM.Glyphs.BottomTeeDblV;
-				_doubleBoth = CM.Glyphs.BottomTeeDbl;
-				_thickH = CM.Glyphs.BottomTeeHvH;
-				_thickV = CM.Glyphs.BottomTeeHvV;
-				_thickBoth = CM.Glyphs.BottomTeeHvDblH;
-				_normal = CM.Glyphs.BottomTee;
-			}
-		}
-		private class CrossIntersectionRuneResolver : IntersectionRuneResolver {
-			public override void SetGlyphs ()
-			{
-				_round = CM.Glyphs.Cross;
-				_doubleH = CM.Glyphs.CrossDblH;
-				_doubleV = CM.Glyphs.CrossDblV;
-				_doubleBoth = CM.Glyphs.CrossDbl;
-				_thickH = CM.Glyphs.CrossHvH;
-				_thickV = CM.Glyphs.CrossHvV;
-				_thickBoth = CM.Glyphs.CrossHv;
-				_normal = CM.Glyphs.Cross;
-			}
-		}
-
-		private Rune? GetRuneForIntersects (ConsoleDriver driver, IntersectionDefinition? [] intersects)
-		{
-			if (!intersects.Any ()) {
-				return null;
-			}
-
-			var runeType = GetRuneTypeForIntersects (intersects);
-
-			if (runeResolvers.TryGetValue (runeType, out var resolver)) {
-				return resolver.GetRuneForIntersects (driver, intersects);
-			}
-
-			// TODO: Remove these once we have all of the below ported to IntersectionRuneResolvers
-			var useDouble = intersects.Any (i => i?.Line.Style == LineStyle.Double);
-			var useDashed = intersects.Any (i => i?.Line.Style == LineStyle.Dashed || i?.Line.Style == LineStyle.RoundedDashed);
-			var useDotted = intersects.Any (i => i?.Line.Style == LineStyle.Dotted || i?.Line.Style == LineStyle.RoundedDotted);
-			// horiz and vert lines same as Single for Rounded
-			var useThick = intersects.Any (i => i?.Line.Style == LineStyle.Heavy);
-			var useThickDashed = intersects.Any (i => i?.Line.Style == LineStyle.HeavyDashed);
-			var useThickDotted = intersects.Any (i => i?.Line.Style == LineStyle.HeavyDotted);
-			// TODO: Support ruler
-			//var useRuler = intersects.Any (i => i.Line.Style == LineStyle.Ruler && i.Line.Length != 0);
-
-			// TODO: maybe make these resolvers too for simplicity?
-			switch (runeType) {
-			case IntersectionRuneType.None:
-				return null;
-			case IntersectionRuneType.Dot:
-				return (Rune)CM.Glyphs.Dot;
-			case IntersectionRuneType.HLine:
-				if (useDouble) {
-					return CM.Glyphs.HLineDbl;
-				}
-				if (useDashed) {
-					return CM.Glyphs.HLineDa2;
-				}
-				if (useDotted) {
-					return CM.Glyphs.HLineDa3;
-				}
-				return useThick ? CM.Glyphs.HLineHv : (useThickDashed ? CM.Glyphs.HLineHvDa2 : (useThickDotted ? CM.Glyphs.HLineHvDa3 : CM.Glyphs.HLine));
-			case IntersectionRuneType.VLine:
-				if (useDouble) {
-					return CM.Glyphs.VLineDbl;
-				}
-				if (useDashed) {
-					return CM.Glyphs.VLineDa3;
-				}
-				if (useDotted) {
-					return CM.Glyphs.VLineDa4;
-				}
-				return useThick ? CM.Glyphs.VLineHv : (useThickDashed ? CM.Glyphs.VLineHvDa3 : (useThickDotted ? CM.Glyphs.VLineHvDa4 : CM.Glyphs.VLine));
-
-			default: throw new Exception ("Could not find resolver or switch case for " + nameof (runeType) + ":" + runeType);
-			}
-		}
-
-		private Attribute? GetAttributeForIntersects (IntersectionDefinition? [] intersects) =>intersects [0]!.Line.Attribute;
-
-		private Cell? GetCellForIntersects (ConsoleDriver driver, IntersectionDefinition? [] intersects)
-		{
-			if (!intersects.Any ()) {
-				return null;
-			}
-
-			var cell = new Cell ();
-			var rune = GetRuneForIntersects (driver, intersects);
-			if (rune.HasValue) {
-				cell.Rune = rune.Value;
-			}
-			cell.Attribute = GetAttributeForIntersects (intersects);
-			return cell;
-		}
-
-		private IntersectionRuneType GetRuneTypeForIntersects (IntersectionDefinition? [] intersects)
-		{
-			var set = new HashSet<IntersectionType> (intersects.Select (i => i!.Type));
-
-			#region Cross Conditions
-			if (Has (set,
-				IntersectionType.PassOverHorizontal,
-				IntersectionType.PassOverVertical
-				)) {
-				return IntersectionRuneType.Cross;
-			}
-
-			if (Has (set,
-				IntersectionType.PassOverVertical,
-				IntersectionType.StartLeft,
-				IntersectionType.StartRight
-				)) {
-				return IntersectionRuneType.Cross;
-			}
-
-			if (Has (set,
-				IntersectionType.PassOverHorizontal,
-				IntersectionType.StartUp,
-				IntersectionType.StartDown
-				)) {
-				return IntersectionRuneType.Cross;
-			}
-
-			if (Has (set,
-				IntersectionType.StartLeft,
-				IntersectionType.StartRight,
-				IntersectionType.StartUp,
-				IntersectionType.StartDown)) {
-				return IntersectionRuneType.Cross;
-			}
-			#endregion
-
-			#region Corner Conditions
-			if (Exactly (set,
-				IntersectionType.StartRight,
-				IntersectionType.StartDown)) {
-				return IntersectionRuneType.ULCorner;
-			}
-
-			if (Exactly (set,
-				IntersectionType.StartLeft,
-				IntersectionType.StartDown)) {
-				return IntersectionRuneType.URCorner;
-			}
-
-			if (Exactly (set,
-				IntersectionType.StartUp,
-				IntersectionType.StartLeft)) {
-				return IntersectionRuneType.LRCorner;
-			}
-
-			if (Exactly (set,
-				IntersectionType.StartUp,
-				IntersectionType.StartRight)) {
-				return IntersectionRuneType.LLCorner;
-			}
-			#endregion Corner Conditions
-
-			#region T Conditions
-			if (Has (set,
-				IntersectionType.PassOverHorizontal,
-				IntersectionType.StartDown)) {
-				return IntersectionRuneType.TopTee;
-			}
-			if (Has (set,
-				IntersectionType.StartRight,
-				IntersectionType.StartLeft,
-				IntersectionType.StartDown)) {
-				return IntersectionRuneType.TopTee;
-			}
-
-			if (Has (set,
-				IntersectionType.PassOverHorizontal,
-				IntersectionType.StartUp)) {
-				return IntersectionRuneType.BottomTee;
-			}
-			if (Has (set,
-				IntersectionType.StartRight,
-				IntersectionType.StartLeft,
-				IntersectionType.StartUp)) {
-				return IntersectionRuneType.BottomTee;
-			}
-
-			if (Has (set,
-				IntersectionType.PassOverVertical,
-				IntersectionType.StartRight)) {
-				return IntersectionRuneType.LeftTee;
-			}
-			if (Has (set,
-				IntersectionType.StartRight,
-				IntersectionType.StartDown,
-				IntersectionType.StartUp)) {
-				return IntersectionRuneType.LeftTee;
-			}
-
-			if (Has (set,
-				IntersectionType.PassOverVertical,
-				IntersectionType.StartLeft)) {
-				return IntersectionRuneType.RightTee;
-			}
-			if (Has (set,
-				IntersectionType.StartLeft,
-				IntersectionType.StartDown,
-				IntersectionType.StartUp)) {
-				return IntersectionRuneType.RightTee;
-			}
-			#endregion
-
-			if (All (intersects, Orientation.Horizontal)) {
-				return IntersectionRuneType.HLine;
-			}
-
-			if (All (intersects, Orientation.Vertical)) {
-				return IntersectionRuneType.VLine;
-			}
-
-			return IntersectionRuneType.Dot;
-		}
-
-		private bool All (IntersectionDefinition? [] intersects, Orientation orientation)
-		{
-			return intersects.All (i => i!.Line.Orientation == orientation);
-		}
-
-		/// <summary>
-		/// Returns true if the <paramref name="intersects"/> collection has all the <paramref name="types"/>
-		/// specified (i.e. AND).
-		/// </summary>
-		/// <param name="intersects"></param>
-		/// <param name="types"></param>
-		/// <returns></returns>
-		private bool Has (HashSet<IntersectionType> intersects, params IntersectionType [] types)
-		{
-			return types.All (t => intersects.Contains (t));
-		}
-
-		/// <summary>
-		/// Returns true if all requested <paramref name="types"/> appear in <paramref name="intersects"/>
-		/// and there are no additional <see cref="IntersectionRuneType"/>
-		/// </summary>
-		/// <param name="intersects"></param>
-		/// <param name="types"></param>
-		/// <returns></returns>
-		private bool Exactly (HashSet<IntersectionType> intersects, params IntersectionType [] types)
-		{
-			return intersects.SetEquals (types);
-		}
-
-		/// <summary>
-		/// Merges one line canvas into this one.
-		/// </summary>
-		/// <param name="lineCanvas"></param>
-		public void Merge (LineCanvas lineCanvas)
-		{
-			foreach (var line in lineCanvas._lines) {
-				AddLine (line);
-			}
-		}
-		
-		/// <inheritdoc />
-		public void Dispose ()
-		{
-			ConfigurationManager.Applied -= ConfigurationManager_Applied;
-		}
-	}
-	internal class IntersectionDefinition {
-		/// <summary>
-		/// The point at which the intersection happens
-		/// </summary>
-		internal Point Point { get; }
-
-		/// <summary>
-		/// Defines how <see cref="Line"/> position relates
-		/// to <see cref="Point"/>.
-		/// </summary>
-		internal IntersectionType Type { get; }
-
-		/// <summary>
-		/// The line that intersects <see cref="Point"/>
-		/// </summary>
-		internal StraightLine Line { get; }
-
-		internal IntersectionDefinition (Point point, IntersectionType type, StraightLine line)
-		{
-			Point = point;
-			Type = type;
-			Line = line;
-		}
-	}
-
-	/// <summary>
-	/// The type of Rune that we will use before considering
-	/// double width, curved borders etc
-	/// </summary>
-	internal enum IntersectionRuneType {
-		None,
-		Dot,
-		ULCorner,
-		URCorner,
-		LLCorner,
-		LRCorner,
-		TopTee,
-		BottomTee,
-		RightTee,
-		LeftTee,
-		Cross,
-		HLine,
-		VLine,
-	}
-
-	internal enum IntersectionType {
-		/// <summary>
-		/// There is no intersection
-		/// </summary>
-		None,
-
-		/// <summary>
-		///  A line passes directly over this point traveling along
-		///  the horizontal axis
-		/// </summary>
-		PassOverHorizontal,
-
-		/// <summary>
-		///  A line passes directly over this point traveling along
-		///  the vertical axis
-		/// </summary>
-		PassOverVertical,
-
-		/// <summary>
-		/// A line starts at this point and is traveling up
-		/// </summary>
-		StartUp,
-
-		/// <summary>
-		/// A line starts at this point and is traveling right
-		/// </summary>
-		StartRight,
-
-		/// <summary>
-		/// A line starts at this point and is traveling down
-		/// </summary>
-		StartDown,
-
-		/// <summary>
-		/// A line starts at this point and is traveling left
-		/// </summary>
-		StartLeft,
-
-		/// <summary>
-		/// A line exists at this point who has 0 length
-		/// </summary>
-		Dot
-	}
-}
+#nullable enable
+namespace Terminal.Gui;
+
+/// <summary>Facilitates box drawing and line intersection detection and rendering.  Does not support diagonal lines.</summary>
+public class LineCanvas : IDisposable
+{
+    private readonly List<StraightLine> _lines = [];
+
+    private readonly Dictionary<IntersectionRuneType, IntersectionRuneResolver> _runeResolvers = new ()
+    {
+        {
+            IntersectionRuneType.ULCorner,
+            new ULIntersectionRuneResolver ()
+        },
+        {
+            IntersectionRuneType.URCorner,
+            new URIntersectionRuneResolver ()
+        },
+        {
+            IntersectionRuneType.LLCorner,
+            new LLIntersectionRuneResolver ()
+        },
+        {
+            IntersectionRuneType.LRCorner,
+            new LRIntersectionRuneResolver ()
+        },
+        {
+            IntersectionRuneType.TopTee,
+            new TopTeeIntersectionRuneResolver ()
+        },
+        {
+            IntersectionRuneType.LeftTee,
+            new LeftTeeIntersectionRuneResolver ()
+        },
+        {
+            IntersectionRuneType.RightTee,
+            new RightTeeIntersectionRuneResolver ()
+        },
+        {
+            IntersectionRuneType.BottomTee,
+            new BottomTeeIntersectionRuneResolver ()
+        },
+        {
+            IntersectionRuneType.Cross,
+            new CrossIntersectionRuneResolver ()
+        }
+
+        // TODO: Add other resolvers
+    };
+
+    private Rectangle _cachedBounds;
+
+    /// <summary>Creates a new instance.</summary>
+    public LineCanvas ()
+    {
+        // TODO: Refactor ConfigurationManager to not use an event handler for this.
+        // Instead, have it call a method on any class appropriately attributed
+        // to update the cached values. See Issue #2871
+        Applied += ConfigurationManager_Applied;
+    }
+
+    /// <summary>Creates a new instance with the given <paramref name="lines"/>.</summary>
+    /// <param name="lines">Initial lines for the canvas.</param>
+    public LineCanvas (IEnumerable<StraightLine> lines) : this () { _lines = lines.ToList (); }
+
+    /// <summary>
+    ///     Gets the rectangle that describes the bounds of the canvas. Location is the coordinates of the line that is
+    ///     furthest left/top and Size is defined by the line that extends the furthest right/bottom.
+    /// </summary>
+    public Rectangle Bounds
+    {
+        get
+        {
+            if (_cachedBounds.IsEmpty)
+            {
+                if (_lines.Count == 0)
+                {
+                    return _cachedBounds;
+                }
+
+                Rectangle bounds = _lines [0].Bounds;
+
+                for (var i = 1; i < _lines.Count; i++)
+                {
+                    bounds = Rectangle.Union (bounds, _lines [i].Bounds);
+                }
+
+                if (bounds is {Width: 0} or {Height: 0})
+                {
+                    bounds = bounds with
+                    {
+                        Width = Math.Clamp (bounds.Width, 1, short.MaxValue),
+                        Height = Math.Clamp (bounds.Height, 1, short.MaxValue)
+                    };
+                }
+
+                _cachedBounds = bounds;
+            }
+
+            return _cachedBounds;
+        }
+    }
+
+    /// <summary>Gets the lines in the canvas.</summary>
+    public IReadOnlyCollection<StraightLine> Lines => _lines.AsReadOnly ();
+
+    /// <inheritdoc/>
+    public void Dispose () { Applied -= ConfigurationManager_Applied; }
+
+    /// <summary>
+    ///     <para>Adds a new <paramref name="length"/> long line to the canvas starting at <paramref name="start"/>.</para>
+    ///     <para>
+    ///         Use positive <paramref name="length"/> for the line to extend Right and negative for Left when
+    ///         <see cref="Orientation"/> is <see cref="Orientation.Horizontal"/>.
+    ///     </para>
+    ///     <para>
+    ///         Use positive <paramref name="length"/> for the line to extend Down and negative for Up when
+    ///         <see cref="Orientation"/> is <see cref="Orientation.Vertical"/>.
+    ///     </para>
+    /// </summary>
+    /// <param name="start">Starting point.</param>
+    /// <param name="length">
+    ///     The length of line. 0 for an intersection (cross or T). Positive for Down/Right. Negative for
+    ///     Up/Left.
+    /// </param>
+    /// <param name="orientation">The direction of the line.</param>
+    /// <param name="style">The style of line to use</param>
+    /// <param name="attribute"></param>
+    public void AddLine (
+        Point start,
+        int length,
+        Orientation orientation,
+        LineStyle style,
+        Attribute? attribute = default
+    )
+    {
+        _cachedBounds = Rectangle.Empty;
+        _lines.Add (new StraightLine (start, length, orientation, style, attribute));
+    }
+
+    /// <summary>Adds a new line to the canvas</summary>
+    /// <param name="line"></param>
+    public void AddLine (StraightLine line)
+    {
+        _cachedBounds = Rectangle.Empty;
+        _lines.Add (line);
+    }
+
+    /// <summary>Clears all lines from the LineCanvas.</summary>
+    public void Clear ()
+    {
+        _cachedBounds = Rectangle.Empty;
+        _lines.Clear ();
+    }
+
+    /// <summary>
+    ///     Clears any cached states from the canvas Call this method if you make changes to lines that have already been
+    ///     added.
+    /// </summary>
+    public void ClearCache () { _cachedBounds = Rectangle.Empty; }
+
+    /// <summary>
+    ///     Evaluates the lines that have been added to the canvas and returns a map containing the glyphs and their
+    ///     locations. The glyphs are the characters that should be rendered so that all lines connect up with the appropriate
+    ///     intersection symbols.
+    /// </summary>
+    /// <returns>A map of all the points within the canvas.</returns>
+    public Dictionary<Point, Cell> GetCellMap ()
+    {
+        Dictionary<Point, Cell> map = new ();
+
+        // walk through each pixel of the bitmap
+        for (int y = Bounds.Y; y < Bounds.Y + Bounds.Height; y++)
+        {
+            for (int x = Bounds.X; x < Bounds.X + Bounds.Width; x++)
+            {
+                IntersectionDefinition? [] intersects = _lines
+                                                        .Select (l => l.Intersects (x, y))
+                                                        .Where (i => i is { })
+                                                        .ToArray ();
+
+                Cell? cell = GetCellForIntersects (Application.Driver, intersects);
+
+                if (cell is { })
+                {
+                    map.Add (new Point (x, y), cell);
+                }
+            }
+        }
+
+        return map;
+    }
+
+    // TODO: Unless there's an obvious use case for this API we should delete it in favor of the
+    // simpler version that doensn't take an area.
+    /// <summary>
+    ///     Evaluates the lines that have been added to the canvas and returns a map containing the glyphs and their
+    ///     locations. The glyphs are the characters that should be rendered so that all lines connect up with the appropriate
+    ///     intersection symbols.
+    /// </summary>
+    /// <param name="inArea">A rectangle to constrain the search by.</param>
+    /// <returns>A map of the points within the canvas that intersect with <paramref name="inArea"/>.</returns>
+    public Dictionary<Point, Rune> GetMap (Rectangle inArea)
+    {
+        Dictionary<Point, Rune> map = new ();
+
+        // walk through each pixel of the bitmap
+        for (int y = inArea.Y; y < inArea.Y + inArea.Height; y++)
+        {
+            for (int x = inArea.X; x < inArea.X + inArea.Width; x++)
+            {
+                IntersectionDefinition? [] intersects = _lines
+                                                        .Select (l => l.Intersects (x, y))
+                                                        .Where (i => i is { })
+                                                        .ToArray ();
+
+                Rune? rune = GetRuneForIntersects (Application.Driver, intersects);
+
+                if (rune is { })
+                {
+                    map.Add (new Point (x, y), rune.Value);
+                }
+            }
+        }
+
+        return map;
+    }
+
+    /// <summary>
+    ///     Evaluates the lines that have been added to the canvas and returns a map containing the glyphs and their
+    ///     locations. The glyphs are the characters that should be rendered so that all lines connect up with the appropriate
+    ///     intersection symbols.
+    /// </summary>
+    /// <returns>A map of all the points within the canvas.</returns>
+    public Dictionary<Point, Rune> GetMap () { return GetMap (Bounds); }
+
+    /// <summary>Merges one line canvas into this one.</summary>
+    /// <param name="lineCanvas"></param>
+    public void Merge (LineCanvas lineCanvas)
+    {
+        foreach (StraightLine line in lineCanvas._lines)
+        {
+            AddLine (line);
+        }
+    }
+
+    /// <summary>Removes the last line added to the canvas</summary>
+    /// <returns></returns>
+    public StraightLine RemoveLastLine ()
+    {
+        StraightLine? l = _lines.LastOrDefault ();
+
+        if (l is { })
+        {
+            _lines.Remove (l);
+        }
+
+        return l!;
+    }
+
+    /// <summary>
+    ///     Returns the contents of the line canvas rendered to a string. The string will include all columns and rows,
+    ///     even if <see cref="Bounds"/> has negative coordinates. For example, if the canvas contains a single line that
+    ///     starts at (-1,-1) with a length of 2, the rendered string will have a length of 2.
+    /// </summary>
+    /// <returns>The canvas rendered to a string.</returns>
+    public override string ToString ()
+    {
+        if (Bounds.IsEmpty)
+        {
+            return string.Empty;
+        }
+
+        // Generate the rune map for the entire canvas
+        Dictionary<Point, Rune> runeMap = GetMap ();
+
+        // Create the rune canvas
+        Rune [,] canvas = new Rune [Bounds.Height, Bounds.Width];
+
+        // Copy the rune map to the canvas, adjusting for any negative coordinates
+        foreach (KeyValuePair<Point, Rune> kvp in runeMap)
+        {
+            int x = kvp.Key.X - Bounds.X;
+            int y = kvp.Key.Y - Bounds.Y;
+            canvas [y, x] = kvp.Value;
+        }
+
+        // Convert the canvas to a string
+        var sb = new StringBuilder ();
+
+        for (var y = 0; y < canvas.GetLength (0); y++)
+        {
+            for (var x = 0; x < canvas.GetLength (1); x++)
+            {
+                Rune r = canvas [y, x];
+                sb.Append (r.Value == 0 ? ' ' : r.ToString ());
+            }
+
+            if (y < canvas.GetLength (0) - 1)
+            {
+                sb.AppendLine ();
+            }
+        }
+
+        return sb.ToString ();
+    }
+
+    private bool All (IntersectionDefinition? [] intersects, Orientation orientation) { return intersects.All (i => i!.Line.Orientation == orientation); }
+
+    private void ConfigurationManager_Applied (object? sender, ConfigurationManagerEventArgs e)
+    {
+        foreach (KeyValuePair<IntersectionRuneType, IntersectionRuneResolver> irr in _runeResolvers)
+        {
+            irr.Value.SetGlyphs ();
+        }
+    }
+
+    /// <summary>
+    ///     Returns true if all requested <paramref name="types"/> appear in <paramref name="intersects"/> and there are
+    ///     no additional <see cref="IntersectionRuneType"/>
+    /// </summary>
+    /// <param name="intersects"></param>
+    /// <param name="types"></param>
+    /// <returns></returns>
+    private bool Exactly (HashSet<IntersectionType> intersects, params IntersectionType [] types) { return intersects.SetEquals (types); }
+
+    private Attribute? GetAttributeForIntersects (IntersectionDefinition? [] intersects) { return intersects [0]!.Line.Attribute; }
+
+    private Cell? GetCellForIntersects (ConsoleDriver driver, IntersectionDefinition? [] intersects)
+    {
+        if (!intersects.Any ())
+        {
+            return null;
+        }
+
+        var cell = new Cell ();
+        Rune? rune = GetRuneForIntersects (driver, intersects);
+
+        if (rune.HasValue)
+        {
+            cell.Rune = rune.Value;
+        }
+
+        cell.Attribute = GetAttributeForIntersects (intersects);
+
+        return cell;
+    }
+
+    private Rune? GetRuneForIntersects (ConsoleDriver driver, IntersectionDefinition? [] intersects)
+    {
+        if (!intersects.Any ())
+        {
+            return null;
+        }
+
+        IntersectionRuneType runeType = GetRuneTypeForIntersects (intersects);
+
+        if (_runeResolvers.TryGetValue (runeType, out IntersectionRuneResolver? resolver))
+        {
+            return resolver.GetRuneForIntersects (driver, intersects);
+        }
+
+        // TODO: Remove these once we have all of the below ported to IntersectionRuneResolvers
+        bool useDouble = intersects.Any (i => i?.Line.Style == LineStyle.Double);
+
+        bool useDashed = intersects.Any (
+                                         i => i?.Line.Style == LineStyle.Dashed
+                                              || i?.Line.Style == LineStyle.RoundedDashed
+                                        );
+
+        bool useDotted = intersects.Any (
+                                         i => i?.Line.Style == LineStyle.Dotted
+                                              || i?.Line.Style == LineStyle.RoundedDotted
+                                        );
+
+        // horiz and vert lines same as Single for Rounded
+        bool useThick = intersects.Any (i => i?.Line.Style == LineStyle.Heavy);
+        bool useThickDashed = intersects.Any (i => i?.Line.Style == LineStyle.HeavyDashed);
+        bool useThickDotted = intersects.Any (i => i?.Line.Style == LineStyle.HeavyDotted);
+
+        // TODO: Support ruler
+        //var useRuler = intersects.Any (i => i.Line.Style == LineStyle.Ruler && i.Line.Length != 0);
+
+        // TODO: maybe make these resolvers too for simplicity?
+        switch (runeType)
+        {
+            case IntersectionRuneType.None:
+                return null;
+            case IntersectionRuneType.Dot:
+                return Glyphs.Dot;
+            case IntersectionRuneType.HLine:
+                if (useDouble)
+                {
+                    return Glyphs.HLineDbl;
+                }
+
+                if (useDashed)
+                {
+                    return Glyphs.HLineDa2;
+                }
+
+                if (useDotted)
+                {
+                    return Glyphs.HLineDa3;
+                }
+
+                return useThick ? Glyphs.HLineHv :
+                       useThickDashed ? Glyphs.HLineHvDa2 :
+                       useThickDotted ? Glyphs.HLineHvDa3 : Glyphs.HLine;
+            case IntersectionRuneType.VLine:
+                if (useDouble)
+                {
+                    return Glyphs.VLineDbl;
+                }
+
+                if (useDashed)
+                {
+                    return Glyphs.VLineDa3;
+                }
+
+                if (useDotted)
+                {
+                    return Glyphs.VLineDa4;
+                }
+
+                return useThick ? Glyphs.VLineHv :
+                       useThickDashed ? Glyphs.VLineHvDa3 :
+                       useThickDotted ? Glyphs.VLineHvDa4 : Glyphs.VLine;
+
+            default:
+                throw new Exception (
+                                     "Could not find resolver or switch case for "
+                                     + nameof (runeType)
+                                     + ":"
+                                     + runeType
+                                    );
+        }
+    }
+
+    private IntersectionRuneType GetRuneTypeForIntersects (IntersectionDefinition? [] intersects)
+    {
+        HashSet<IntersectionType> set = new (intersects.Select (i => i!.Type));
+
+        #region Cross Conditions
+
+        if (Has (
+                 set,
+                 IntersectionType.PassOverHorizontal,
+                 IntersectionType.PassOverVertical
+                ))
+        {
+            return IntersectionRuneType.Cross;
+        }
+
+        if (Has (
+                 set,
+                 IntersectionType.PassOverVertical,
+                 IntersectionType.StartLeft,
+                 IntersectionType.StartRight
+                ))
+        {
+            return IntersectionRuneType.Cross;
+        }
+
+        if (Has (
+                 set,
+                 IntersectionType.PassOverHorizontal,
+                 IntersectionType.StartUp,
+                 IntersectionType.StartDown
+                ))
+        {
+            return IntersectionRuneType.Cross;
+        }
+
+        if (Has (
+                 set,
+                 IntersectionType.StartLeft,
+                 IntersectionType.StartRight,
+                 IntersectionType.StartUp,
+                 IntersectionType.StartDown
+                ))
+        {
+            return IntersectionRuneType.Cross;
+        }
+
+        #endregion
+
+        #region Corner Conditions
+
+        if (Exactly (
+                     set,
+                     IntersectionType.StartRight,
+                     IntersectionType.StartDown
+                    ))
+        {
+            return IntersectionRuneType.ULCorner;
+        }
+
+        if (Exactly (
+                     set,
+                     IntersectionType.StartLeft,
+                     IntersectionType.StartDown
+                    ))
+        {
+            return IntersectionRuneType.URCorner;
+        }
+
+        if (Exactly (
+                     set,
+                     IntersectionType.StartUp,
+                     IntersectionType.StartLeft
+                    ))
+        {
+            return IntersectionRuneType.LRCorner;
+        }
+
+        if (Exactly (
+                     set,
+                     IntersectionType.StartUp,
+                     IntersectionType.StartRight
+                    ))
+        {
+            return IntersectionRuneType.LLCorner;
+        }
+
+        #endregion Corner Conditions
+
+        #region T Conditions
+
+        if (Has (
+                 set,
+                 IntersectionType.PassOverHorizontal,
+                 IntersectionType.StartDown
+                ))
+        {
+            return IntersectionRuneType.TopTee;
+        }
+
+        if (Has (
+                 set,
+                 IntersectionType.StartRight,
+                 IntersectionType.StartLeft,
+                 IntersectionType.StartDown
+                ))
+        {
+            return IntersectionRuneType.TopTee;
+        }
+
+        if (Has (
+                 set,
+                 IntersectionType.PassOverHorizontal,
+                 IntersectionType.StartUp
+                ))
+        {
+            return IntersectionRuneType.BottomTee;
+        }
+
+        if (Has (
+                 set,
+                 IntersectionType.StartRight,
+                 IntersectionType.StartLeft,
+                 IntersectionType.StartUp
+                ))
+        {
+            return IntersectionRuneType.BottomTee;
+        }
+
+        if (Has (
+                 set,
+                 IntersectionType.PassOverVertical,
+                 IntersectionType.StartRight
+                ))
+        {
+            return IntersectionRuneType.LeftTee;
+        }
+
+        if (Has (
+                 set,
+                 IntersectionType.StartRight,
+                 IntersectionType.StartDown,
+                 IntersectionType.StartUp
+                ))
+        {
+            return IntersectionRuneType.LeftTee;
+        }
+
+        if (Has (
+                 set,
+                 IntersectionType.PassOverVertical,
+                 IntersectionType.StartLeft
+                ))
+        {
+            return IntersectionRuneType.RightTee;
+        }
+
+        if (Has (
+                 set,
+                 IntersectionType.StartLeft,
+                 IntersectionType.StartDown,
+                 IntersectionType.StartUp
+                ))
+        {
+            return IntersectionRuneType.RightTee;
+        }
+
+        #endregion
+
+        if (All (intersects, Orientation.Horizontal))
+        {
+            return IntersectionRuneType.HLine;
+        }
+
+        if (All (intersects, Orientation.Vertical))
+        {
+            return IntersectionRuneType.VLine;
+        }
+
+        return IntersectionRuneType.Dot;
+    }
+
+    /// <summary>
+    ///     Returns true if the <paramref name="intersects"/> collection has all the <paramref name="types"/> specified
+    ///     (i.e. AND).
+    /// </summary>
+    /// <param name="intersects"></param>
+    /// <param name="types"></param>
+    /// <returns></returns>
+    private bool Has (HashSet<IntersectionType> intersects, params IntersectionType [] types) { return types.All (t => intersects.Contains (t)); }
+
+    private class BottomTeeIntersectionRuneResolver : IntersectionRuneResolver
+    {
+        public override void SetGlyphs ()
+        {
+            _round = Glyphs.BottomTee;
+            _doubleH = Glyphs.BottomTeeDblH;
+            _doubleV = Glyphs.BottomTeeDblV;
+            _doubleBoth = Glyphs.BottomTeeDbl;
+            _thickH = Glyphs.BottomTeeHvH;
+            _thickV = Glyphs.BottomTeeHvV;
+            _thickBoth = Glyphs.BottomTeeHvDblH;
+            _normal = Glyphs.BottomTee;
+        }
+    }
+
+    private class CrossIntersectionRuneResolver : IntersectionRuneResolver
+    {
+        public override void SetGlyphs ()
+        {
+            _round = Glyphs.Cross;
+            _doubleH = Glyphs.CrossDblH;
+            _doubleV = Glyphs.CrossDblV;
+            _doubleBoth = Glyphs.CrossDbl;
+            _thickH = Glyphs.CrossHvH;
+            _thickV = Glyphs.CrossHvV;
+            _thickBoth = Glyphs.CrossHv;
+            _normal = Glyphs.Cross;
+        }
+    }
+
+    private abstract class IntersectionRuneResolver
+    {
+        internal Rune _doubleBoth;
+        internal Rune _doubleH;
+        internal Rune _doubleV;
+        internal Rune _normal;
+        internal Rune _round;
+        internal Rune _thickBoth;
+        internal Rune _thickH;
+        internal Rune _thickV;
+        public IntersectionRuneResolver () { SetGlyphs (); }
+
+        public Rune? GetRuneForIntersects (ConsoleDriver driver, IntersectionDefinition? [] intersects)
+        {
+            bool useRounded = intersects.Any (
+                                              i => i?.Line.Length != 0
+                                                   && (
+                                                          i?.Line.Style == LineStyle.Rounded
+                                                          || i?.Line.Style
+                                                          == LineStyle.RoundedDashed
+                                                          || i?.Line.Style
+                                                          == LineStyle.RoundedDotted)
+                                             );
+
+            // Note that there aren't any glyphs for intersections of double lines with heavy lines
+
+            bool doubleHorizontal = intersects.Any (
+                                                    l => l?.Line.Orientation == Orientation.Horizontal
+                                                         && l.Line.Style == LineStyle.Double
+                                                   );
+
+            bool doubleVertical = intersects.Any (
+                                                  l => l?.Line.Orientation == Orientation.Vertical
+                                                       && l.Line.Style == LineStyle.Double
+                                                 );
+
+            bool thickHorizontal = intersects.Any (
+                                                   l => l?.Line.Orientation == Orientation.Horizontal
+                                                        && (
+                                                               l.Line.Style == LineStyle.Heavy
+                                                               || l.Line.Style == LineStyle.HeavyDashed
+                                                               || l.Line.Style == LineStyle.HeavyDotted)
+                                                  );
+
+            bool thickVertical = intersects.Any (
+                                                 l => l?.Line.Orientation == Orientation.Vertical
+                                                      && (
+                                                             l.Line.Style == LineStyle.Heavy
+                                                             || l.Line.Style == LineStyle.HeavyDashed
+                                                             || l.Line.Style == LineStyle.HeavyDotted)
+                                                );
+
+            if (doubleHorizontal)
+            {
+                return doubleVertical ? _doubleBoth : _doubleH;
+            }
+
+            if (doubleVertical)
+            {
+                return _doubleV;
+            }
+
+            if (thickHorizontal)
+            {
+                return thickVertical ? _thickBoth : _thickH;
+            }
+
+            if (thickVertical)
+            {
+                return _thickV;
+            }
+
+            return useRounded ? _round : _normal;
+        }
+
+        /// <summary>
+        ///     Sets the glyphs used. Call this method after construction and any time ConfigurationManager has updated the
+        ///     settings.
+        /// </summary>
+        public abstract void SetGlyphs ();
+    }
+
+    private class LeftTeeIntersectionRuneResolver : IntersectionRuneResolver
+    {
+        public override void SetGlyphs ()
+        {
+            _round = Glyphs.LeftTee;
+            _doubleH = Glyphs.LeftTeeDblH;
+            _doubleV = Glyphs.LeftTeeDblV;
+            _doubleBoth = Glyphs.LeftTeeDbl;
+            _thickH = Glyphs.LeftTeeHvH;
+            _thickV = Glyphs.LeftTeeHvV;
+            _thickBoth = Glyphs.LeftTeeHvDblH;
+            _normal = Glyphs.LeftTee;
+        }
+    }
+
+    private class LLIntersectionRuneResolver : IntersectionRuneResolver
+    {
+        public override void SetGlyphs ()
+        {
+            _round = Glyphs.LLCornerR;
+            _doubleH = Glyphs.LLCornerSingleDbl;
+            _doubleV = Glyphs.LLCornerDblSingle;
+            _doubleBoth = Glyphs.LLCornerDbl;
+            _thickH = Glyphs.LLCornerLtHv;
+            _thickV = Glyphs.LLCornerHvLt;
+            _thickBoth = Glyphs.LLCornerHv;
+            _normal = Glyphs.LLCorner;
+        }
+    }
+
+    private class LRIntersectionRuneResolver : IntersectionRuneResolver
+    {
+        public override void SetGlyphs ()
+        {
+            _round = Glyphs.LRCornerR;
+            _doubleH = Glyphs.LRCornerSingleDbl;
+            _doubleV = Glyphs.LRCornerDblSingle;
+            _doubleBoth = Glyphs.LRCornerDbl;
+            _thickH = Glyphs.LRCornerLtHv;
+            _thickV = Glyphs.LRCornerHvLt;
+            _thickBoth = Glyphs.LRCornerHv;
+            _normal = Glyphs.LRCorner;
+        }
+    }
+
+    private class RightTeeIntersectionRuneResolver : IntersectionRuneResolver
+    {
+        public override void SetGlyphs ()
+        {
+            _round = Glyphs.RightTee;
+            _doubleH = Glyphs.RightTeeDblH;
+            _doubleV = Glyphs.RightTeeDblV;
+            _doubleBoth = Glyphs.RightTeeDbl;
+            _thickH = Glyphs.RightTeeHvH;
+            _thickV = Glyphs.RightTeeHvV;
+            _thickBoth = Glyphs.RightTeeHvDblH;
+            _normal = Glyphs.RightTee;
+        }
+    }
+
+    private class TopTeeIntersectionRuneResolver : IntersectionRuneResolver
+    {
+        public override void SetGlyphs ()
+        {
+            _round = Glyphs.TopTee;
+            _doubleH = Glyphs.TopTeeDblH;
+            _doubleV = Glyphs.TopTeeDblV;
+            _doubleBoth = Glyphs.TopTeeDbl;
+            _thickH = Glyphs.TopTeeHvH;
+            _thickV = Glyphs.TopTeeHvV;
+            _thickBoth = Glyphs.TopTeeHvDblH;
+            _normal = Glyphs.TopTee;
+        }
+    }
+
+    private class ULIntersectionRuneResolver : IntersectionRuneResolver
+    {
+        public override void SetGlyphs ()
+        {
+            _round = Glyphs.ULCornerR;
+            _doubleH = Glyphs.ULCornerSingleDbl;
+            _doubleV = Glyphs.ULCornerDblSingle;
+            _doubleBoth = Glyphs.ULCornerDbl;
+            _thickH = Glyphs.ULCornerLtHv;
+            _thickV = Glyphs.ULCornerHvLt;
+            _thickBoth = Glyphs.ULCornerHv;
+            _normal = Glyphs.ULCorner;
+        }
+    }
+
+    private class URIntersectionRuneResolver : IntersectionRuneResolver
+    {
+        public override void SetGlyphs ()
+        {
+            _round = Glyphs.URCornerR;
+            _doubleH = Glyphs.URCornerSingleDbl;
+            _doubleV = Glyphs.URCornerDblSingle;
+            _doubleBoth = Glyphs.URCornerDbl;
+            _thickH = Glyphs.URCornerHvLt;
+            _thickV = Glyphs.URCornerLtHv;
+            _thickBoth = Glyphs.URCornerHv;
+            _normal = Glyphs.URCorner;
+        }
+    }
+}

+ 48 - 0
Terminal.Gui/Drawing/LineStyle.cs

@@ -0,0 +1,48 @@
+#nullable enable
+namespace Terminal.Gui;
+
+/// <summary>Defines the style of lines for a <see cref="LineCanvas"/>.</summary>
+public enum LineStyle
+{
+    /// <summary>No border is drawn.</summary>
+    None,
+
+    /// <summary>The border is drawn using thin line CM.Glyphs.</summary>
+    Single,
+
+    /// <summary>The border is drawn using thin line glyphs with dashed (double and triple) straight lines.</summary>
+    Dashed,
+
+    /// <summary>The border is drawn using thin line glyphs with short dashed (triple and quadruple) straight lines.</summary>
+    Dotted,
+
+    /// <summary>The border is drawn using thin double line CM.Glyphs.</summary>
+    Double,
+
+    /// <summary>The border is drawn using heavy line CM.Glyphs.</summary>
+    Heavy,
+
+    /// <summary>The border is drawn using heavy line glyphs with dashed (double and triple) straight lines.</summary>
+    HeavyDashed,
+
+    /// <summary>The border is drawn using heavy line glyphs with short dashed (triple and quadruple) straight lines.</summary>
+    HeavyDotted,
+
+    /// <summary>The border is drawn using thin line glyphs with rounded corners.</summary>
+    Rounded,
+
+    /// <summary>The border is drawn using thin line glyphs with rounded corners and dashed (double and triple) straight lines.</summary>
+    RoundedDashed,
+
+    /// <summary>
+    ///     The border is drawn using thin line glyphs with rounded corners and short dashed (triple and quadruple)
+    ///     straight lines.
+    /// </summary>
+    RoundedDotted
+
+    // TODO: Support Ruler
+    ///// <summary> 
+    ///// The border is drawn as a diagnostic ruler ("|123456789...").
+    ///// </summary>
+    //Ruler
+}

+ 58 - 65
Terminal.Gui/Drawing/Ruler.cs

@@ -1,65 +1,58 @@
-using System;
-using System.Collections.Generic;
-using System.Data;
-using System.Text;
-using System.Text.Json.Serialization;
-
-namespace Terminal.Gui {
-	/// <summary>
-	/// Draws a ruler on the screen.
-	/// </summary>
-	/// <remarks>
-	/// <para>
-	/// </para>
-	/// </remarks>
-	public class Ruler {
-
-		/// <summary>
-		/// Gets or sets whether the ruler is drawn horizontally or vertically. The default is horizontally.
-		/// </summary>
-		public Orientation Orientation { get; set; }
-
-		/// <summary>
-		/// Gets or sets the length of the ruler. The default is 0.
-		/// </summary>
-		public int Length { get; set; }
-
-		/// <summary>
-		/// Gets or sets the foreground and background color to use.
-		/// </summary>
-		public Attribute Attribute { get; set; } = new Attribute ();
-
-		string _hTemplate { get; } = "|123456789";
-		string _vTemplate { get; } = "-123456789";
-
-		/// <summary>
-		/// Draws the <see cref="Ruler"/>. 
-		/// </summary>
-		/// <param name="location">The location to start drawing the ruler, in screen-relative coordinates.</param>
-		/// <param name="start">The start value of the ruler.</param>
-		public void Draw (Point location, int start = 0)
-		{
-			if (start < 0) {
-				throw new ArgumentException ("start must be greater than or equal to 0");
-			}
-
-			if (Length < 1) {
-				return;
-			}
-
-			if (Orientation == Orientation.Horizontal) {
-				var hrule = _hTemplate.Repeat ((int)Math.Ceiling ((double)Length + 2 / (double)_hTemplate.Length)) [start..(Length + start)];
-				// Top
-				Application.Driver.Move (location.X, location.Y);
-				Application.Driver.AddStr (hrule);
-
-			} else {
-				var vrule = _vTemplate.Repeat ((int)Math.Ceiling ((double)(Length + 2) / (double)_vTemplate.Length)) [start..(Length + start)];
-				for (var r = location.Y; r < location.Y + Length; r++) {
-					Application.Driver.Move (location.X, r);
-					Application.Driver.AddRune ((Rune)vrule [r - location.Y]);
-				}
-			}
-		}
-	}
-}
+namespace Terminal.Gui;
+
+/// <summary>Draws a ruler on the screen.</summary>
+/// <remarks>
+///     <para></para>
+/// </remarks>
+public class Ruler
+{
+    /// <summary>Gets or sets the foreground and background color to use.</summary>
+    public Attribute Attribute { get; set; } = new ();
+
+    /// <summary>Gets or sets the length of the ruler. The default is 0.</summary>
+    public int Length { get; set; }
+
+    /// <summary>Gets or sets whether the ruler is drawn horizontally or vertically. The default is horizontally.</summary>
+    public Orientation Orientation { get; set; }
+
+    private string _hTemplate { get; } = "|123456789";
+    private string _vTemplate { get; } = "-123456789";
+
+    /// <summary>Draws the <see cref="Ruler"/>.</summary>
+    /// <param name="location">The location to start drawing the ruler, in screen-relative coordinates.</param>
+    /// <param name="start">The start value of the ruler.</param>
+    public void Draw (Point location, int start = 0)
+    {
+        if (start < 0)
+        {
+            throw new ArgumentException ("start must be greater than or equal to 0");
+        }
+
+        if (Length < 1)
+        {
+            return;
+        }
+
+        if (Orientation == Orientation.Horizontal)
+        {
+            string hrule =
+                _hTemplate.Repeat ((int)Math.Ceiling (Length + 2 / (double)_hTemplate.Length)) [start..(Length + start)];
+
+            // Top
+            Application.Driver.Move (location.X, location.Y);
+            Application.Driver.AddStr (hrule);
+        }
+        else
+        {
+            string vrule =
+                _vTemplate.Repeat ((int)Math.Ceiling ((Length + 2) / (double)_vTemplate.Length))
+                    [start..(Length + start)];
+
+            for (int r = location.Y; r < location.Y + Length; r++)
+            {
+                Application.Driver.Move (location.X, r);
+                Application.Driver.AddRune ((Rune)vrule [r - location.Y]);
+            }
+        }
+    }
+}

+ 194 - 196
Terminal.Gui/Drawing/StraightLine.cs

@@ -1,198 +1,196 @@
-using System;
-
-namespace Terminal.Gui {
+namespace Terminal.Gui;
 #nullable enable
-	// TODO: Add events that notify when StraightLine changes to enable dynamic layout
-	/// <summary>
-	/// A line between two points on a horizontal or vertical <see cref="Orientation"/>
-	/// and a given style/color.
-	/// </summary>
-	public class StraightLine {
-
-		/// <summary>
-		/// Gets or sets where the line begins.
-		/// </summary>
-		public Point Start { get; set; }
-
-		/// <summary>
-		/// Gets or sets the length of the line.
-		/// </summary>
-		public int Length { get; set; }
-
-		/// <summary>
-		/// Gets or sets the orientation (horizontal or vertical) of the line.
-		/// </summary>
-		public Orientation Orientation { get; set; }
-
-		/// <summary>
-		/// Gets or sets the line style of the line (e.g. dotted, double).
-		/// </summary>
-		public LineStyle Style { get; set; }
-
-		/// <summary>
-		/// Gets or sets the color of the line.
-		/// </summary>
-		public Attribute? Attribute { get; set; }
-
-		/// <summary>
-		/// Creates a new instance of the <see cref="StraightLine"/> class.
-		/// </summary>
-		/// <param name="start"></param>
-		/// <param name="length"></param>
-		/// <param name="orientation"></param>
-		/// <param name="style"></param>
-		/// <param name="attribute"></param>
-		public StraightLine (Point start, int length, Orientation orientation, LineStyle style, Attribute? attribute = default)
-		{
-			this.Start = start;
-			this.Length = length;
-			this.Orientation = orientation;
-			this.Style = style;
-			this.Attribute = attribute;
-		}
-
-		internal IntersectionDefinition? Intersects (int x, int y)
-		{
-			switch (Orientation) {
-			case Orientation.Horizontal: return IntersectsHorizontally (x, y);
-			case Orientation.Vertical: return IntersectsVertically (x, y);
-			default: throw new ArgumentOutOfRangeException (nameof (Orientation));
-			}
-
-		}
-
-		private IntersectionDefinition? IntersectsHorizontally (int x, int y)
-		{
-			if (Start.Y != y) {
-				return null;
-			} else {
-				if (StartsAt (x, y)) {
-
-					return new IntersectionDefinition (
-						Start,
-						GetTypeByLength (IntersectionType.StartLeft, IntersectionType.PassOverHorizontal, IntersectionType.StartRight),
-						this
-						);
-
-				}
-
-				if (EndsAt (x, y)) {
-
-					return new IntersectionDefinition (
-						Start,
-						Length < 0 ? IntersectionType.StartRight : IntersectionType.StartLeft,
-						this
-						);
-
-				} else {
-					var xmin = Math.Min (Start.X, Start.X + Length);
-					var xmax = Math.Max (Start.X, Start.X + Length);
-
-					if (xmin < x && xmax > x) {
-						return new IntersectionDefinition (
-						new Point (x, y),
-						IntersectionType.PassOverHorizontal,
-						this
-						);
-					}
-				}
-
-				return null;
-			}
-		}
-
-		private IntersectionDefinition? IntersectsVertically (int x, int y)
-		{
-			if (Start.X != x) {
-				return null;
-			} else {
-				if (StartsAt (x, y)) {
-
-					return new IntersectionDefinition (
-						Start,
-						GetTypeByLength (IntersectionType.StartUp, IntersectionType.PassOverVertical, IntersectionType.StartDown),
-						this
-						);
-
-				}
-
-				if (EndsAt (x, y)) {
-
-					return new IntersectionDefinition (
-						Start,
-						Length < 0 ? IntersectionType.StartDown : IntersectionType.StartUp,
-						this
-						);
-
-				} else {
-					var ymin = Math.Min (Start.Y, Start.Y + Length);
-					var ymax = Math.Max (Start.Y, Start.Y + Length);
-
-					if (ymin < y && ymax > y) {
-						return new IntersectionDefinition (
-						new Point (x, y),
-						IntersectionType.PassOverVertical,
-						this
-						);
-					}
-				}
-
-				return null;
-			}
-		}
-
-		private IntersectionType GetTypeByLength (IntersectionType typeWhenNegative, IntersectionType typeWhenZero, IntersectionType typeWhenPositive)
-		{
-			if (Length == 0) {
-				return typeWhenZero;
-			}
-
-			return Length < 0 ? typeWhenNegative : typeWhenPositive;
-		}
-
-		private bool EndsAt (int x, int y)
-		{
-			var sub = (Length == 0) ? 0 : (Length > 0) ? 1 : -1;
-			if (Orientation == Orientation.Horizontal) {
-				return Start.X + Length - sub == x && Start.Y == y;
-			}
-
-			return Start.X == x && Start.Y + Length - sub == y;
-		}
-
-		private bool StartsAt (int x, int y)
-		{
-			return Start.X == x && Start.Y == y;
-		}
-
-		/// <summary>
-		/// Gets the rectangle that describes the bounds of the canvas. Location is the coordinates of the 
-		/// line that is furthest left/top and Size is defined by the line that extends the furthest
-		/// right/bottom.
-		/// </summary>
-		internal Rect Bounds {
-			get {
-
-				// 0 and 1/-1 Length means a size (width or height) of 1
-				var size = Math.Max (1, Math.Abs (Length));
-
-				// How much to offset x or y to get the start of the line
-				var offset = Math.Abs (Length < 0 ? Length + 1 : 0);
-				var x = Start.X - (Orientation == Orientation.Horizontal ? offset : 0);
-				var y = Start.Y - (Orientation == Orientation.Vertical ? offset : 0);
-				var width = Orientation == Orientation.Horizontal ? size : 1;
-				var height = Orientation == Orientation.Vertical ? size : 1;
-
-				return new Rect (x, y, width, height);
-			}
-		}
-
-		/// <summary>
-		/// Formats the Line as a string in (Start.X,Start.Y,Length,Orientation) notation.
-		/// </summary>
-		public override string ToString ()
-		{
-			return $"({Start.X},{Start.Y},{Length},{Orientation})";
-		}
-	}
+
+// TODO: Add events that notify when StraightLine changes to enable dynamic layout
+/// <summary>A line between two points on a horizontal or vertical <see cref="Orientation"/> and a given style/color.</summary>
+public class StraightLine
+{
+    /// <summary>Creates a new instance of the <see cref="StraightLine"/> class.</summary>
+    /// <param name="start"></param>
+    /// <param name="length"></param>
+    /// <param name="orientation"></param>
+    /// <param name="style"></param>
+    /// <param name="attribute"></param>
+    public StraightLine (
+        Point start,
+        int length,
+        Orientation orientation,
+        LineStyle style,
+        Attribute? attribute = default
+    )
+    {
+        Start = start;
+        Length = length;
+        Orientation = orientation;
+        Style = style;
+        Attribute = attribute;
+    }
+
+    /// <summary>Gets or sets the color of the line.</summary>
+    public Attribute? Attribute { get; set; }
+
+    /// <summary>Gets or sets the length of the line.</summary>
+    public int Length { get; set; }
+
+    /// <summary>Gets or sets the orientation (horizontal or vertical) of the line.</summary>
+    public Orientation Orientation { get; set; }
+
+    /// <summary>Gets or sets where the line begins.</summary>
+    public Point Start { get; set; }
+
+    /// <summary>Gets or sets the line style of the line (e.g. dotted, double).</summary>
+    public LineStyle Style { get; set; }
+
+    /// <summary>
+    ///     Gets the rectangle that describes the bounds of the canvas. Location is the coordinates of the line that is
+    ///     furthest left/top and Size is defined by the line that extends the furthest right/bottom.
+    /// </summary>
+    // PERF: Probably better to store the rectangle rather than make a new one on every single access to Bounds.
+    internal Rectangle Bounds
+    {
+        get
+        {
+            // 0 and 1/-1 Length means a size (width or height) of 1
+            int size = Math.Max (1, Math.Abs (Length));
+
+            // How much to offset x or y to get the start of the line
+            int offset = Math.Abs (Length < 0 ? Length + 1 : 0);
+            int x = Start.X - (Orientation == Orientation.Horizontal ? offset : 0);
+            int y = Start.Y - (Orientation == Orientation.Vertical ? offset : 0);
+            int width = Orientation == Orientation.Horizontal ? size : 1;
+            int height = Orientation == Orientation.Vertical ? size : 1;
+
+            return new (x, y, width, height);
+        }
+    }
+
+    /// <summary>Formats the Line as a string in (Start.X,Start.Y,Length,Orientation) notation.</summary>
+    public override string ToString () { return $"({Start.X},{Start.Y},{Length},{Orientation})"; }
+
+    internal IntersectionDefinition? Intersects (int x, int y)
+    {
+        switch (Orientation)
+        {
+            case Orientation.Horizontal: return IntersectsHorizontally (x, y);
+            case Orientation.Vertical: return IntersectsVertically (x, y);
+            default: throw new ArgumentOutOfRangeException (nameof (Orientation));
+        }
+    }
+
+    private bool EndsAt (int x, int y)
+    {
+        int sub = Length == 0 ? 0 :
+                  Length > 0 ? 1 : -1;
+
+        if (Orientation == Orientation.Horizontal)
+        {
+            return Start.X + Length - sub == x && Start.Y == y;
+        }
+
+        return Start.X == x && Start.Y + Length - sub == y;
+    }
+
+    private IntersectionType GetTypeByLength (
+        IntersectionType typeWhenNegative,
+        IntersectionType typeWhenZero,
+        IntersectionType typeWhenPositive
+    )
+    {
+        if (Length == 0)
+        {
+            return typeWhenZero;
+        }
+
+        return Length < 0 ? typeWhenNegative : typeWhenPositive;
+    }
+
+    private IntersectionDefinition? IntersectsHorizontally (int x, int y)
+    {
+        if (Start.Y != y)
+        {
+            return null;
+        }
+
+        if (StartsAt (x, y))
+        {
+            return new IntersectionDefinition (
+                                               Start,
+                                               GetTypeByLength (
+                                                                IntersectionType.StartLeft,
+                                                                IntersectionType.PassOverHorizontal,
+                                                                IntersectionType.StartRight
+                                                               ),
+                                               this
+                                              );
+        }
+
+        if (EndsAt (x, y))
+        {
+            return new IntersectionDefinition (
+                                               Start,
+                                               Length < 0 ? IntersectionType.StartRight : IntersectionType.StartLeft,
+                                               this
+                                              );
+        }
+
+        int xmin = Math.Min (Start.X, Start.X + Length);
+        int xmax = Math.Max (Start.X, Start.X + Length);
+
+        if (xmin < x && xmax > x)
+        {
+            return new IntersectionDefinition (
+                                               new Point (x, y),
+                                               IntersectionType.PassOverHorizontal,
+                                               this
+                                              );
+        }
+
+        return null;
+    }
+
+    private IntersectionDefinition? IntersectsVertically (int x, int y)
+    {
+        if (Start.X != x)
+        {
+            return null;
+        }
+
+        if (StartsAt (x, y))
+        {
+            return new IntersectionDefinition (
+                                               Start,
+                                               GetTypeByLength (
+                                                                IntersectionType.StartUp,
+                                                                IntersectionType.PassOverVertical,
+                                                                IntersectionType.StartDown
+                                                               ),
+                                               this
+                                              );
+        }
+
+        if (EndsAt (x, y))
+        {
+            return new IntersectionDefinition (
+                                               Start,
+                                               Length < 0 ? IntersectionType.StartDown : IntersectionType.StartUp,
+                                               this
+                                              );
+        }
+
+        int ymin = Math.Min (Start.Y, Start.Y + Length);
+        int ymax = Math.Max (Start.Y, Start.Y + Length);
+
+        if (ymin < y && ymax > y)
+        {
+            return new IntersectionDefinition (
+                                               new Point (x, y),
+                                               IntersectionType.PassOverVertical,
+                                               this
+                                              );
+        }
+
+        return null;
+    }
+
+    private bool StartsAt (int x, int y) { return Start.X == x && Start.Y == y; }
 }

+ 235 - 212
Terminal.Gui/Drawing/StraightLineExtensions.cs

@@ -1,213 +1,236 @@
-using System;
-using System.Collections.Generic;
-
-namespace Terminal.Gui {
-
-	/// <summary>
-	/// Extension methods for <see cref="StraightLine"/> (including collections).
-	/// </summary>
-	public static class StraightLineExtensions {
-		/// <summary>
-		/// Splits or removes all lines in the <paramref name="collection"/> such that none cover the given
-		/// exclusion area.
-		/// </summary>
-		/// <param name="collection">Lines to adjust</param>
-		/// <param name="start">First point to remove from collection</param>
-		/// <param name="length">The number of sequential points to exclude</param>
-		/// <param name="orientation">Orientation of the exclusion line</param>
-		/// <returns></returns>
-		public static IEnumerable<StraightLine> Exclude (this IEnumerable<StraightLine> collection, Point start, int length, Orientation orientation)
-		{
-			var toReturn = new List<StraightLine> ();
-			if (length == 0) {
-				return collection;
-			}
-
-			foreach (var l in collection) {
-
-				if(l.Length == 0) {
-					toReturn.Add (l);
-					continue;
-				}
-
-				// lines are parallel.  For any straight line one axis (x or y) is constant
-				// e.g. Horizontal lines have constant y
-				int econstPoint = orientation == Orientation.Horizontal ? start.Y : start.X;
-				int lconstPoint = l.Orientation == Orientation.Horizontal ? l.Start.Y : l.Start.X;
-
-				// For the varying axis what is the max/mins
-				// i.e. points on horizontal lines vary by x, vertical lines vary by y
-				int eDiffMin = GetLineStartOnDiffAxis (start, length, orientation);
-				int eDiffMax = GetLineEndOnDiffAxis (start, length, orientation);
-				int lDiffMin = GetLineStartOnDiffAxis (l.Start, l.Length, l.Orientation);
-				int lDiffMax = GetLineEndOnDiffAxis (l.Start, l.Length, l.Orientation);
-
-				// line is parallel to exclusion
-				if (l.Orientation == orientation) {
-
-					// Do the parallel lines share constant plane
-					if (econstPoint != lconstPoint) {
-
-						// No, so no way they overlap
-						toReturn.Add (l);
-					} else {
-
-						
-
-						if (lDiffMax < eDiffMin) {
-							// Line ends before exclusion starts
-							toReturn.Add (l);
-						} else if (lDiffMin > eDiffMax) {
-							// Line starts after exclusion ends
-							toReturn.Add (l);
-						} else {
-							//lines overlap!
-
-							// Is there a bit we can keep on the left?
-							if (lDiffMin < eDiffMin) {
-								// Create line up to exclusion point
-								int from = lDiffMin;
-								int len = eDiffMin - lDiffMin;
-
-								if (len > 0) {
-									toReturn.Add (CreateLineFromDiff (l, from, len));
-								}
-							}
-
-							// Is there a bit we can keep on the right?
-							if (lDiffMax > eDiffMax) {
-								// Create line up to exclusion point
-								int from = eDiffMax + 1;
-								int len = lDiffMax - eDiffMax;
-
-								if (len > 0) {
-
-									// A single line with length 1 and -1 are the same (fills only the single cell)
-									// They differ only in how they join to other lines (i.e. to create corners)
-									// Using negative for the later half of the line ensures line joins in a way
-									// consistent with its pre-snipped state.
-									if (len == 1) {
-										len = -1;
-									}
-										
-									toReturn.Add (CreateLineFromDiff (l, from, len));
-								}
-							}
-						}
-					}
-
-				} else {
-					// line is perpendicular to exclusion
-
-					// Does the constant plane of the exclusion appear within the differing plane of the line?
-					if(econstPoint >= lDiffMin && econstPoint <= lDiffMax) {
-						// Yes, e.g. Vertical exclusion's x is within xmin/xmax of the horizontal line
-
-						// Vice versa must also be true
-						// for example there is no intersection if the vertical exclusion line does not
-						// stretch down far enough to reach the line
-						if(lconstPoint >= eDiffMin && lconstPoint <= eDiffMax) {
-
-							// Perpendicular intersection occurs here
-							var intersection = l.Orientation == Orientation.Horizontal ?
-								new Point (econstPoint,lconstPoint) :
-								new Point (lconstPoint,econstPoint);
-
-							// To snip out this single point we will use a recursive call
-							// snipping 1 length along the orientation of l (i.e. parallel)
-							toReturn.AddRange (new [] { l }.Exclude (intersection, 1, l.Orientation));
-						}
-						else {
-							// No intersection
-							toReturn.Add (l);
-						}
-
-					}
-					else {
-						// Lines do not intersect
-						toReturn.Add (l);
-					}
-
-				}
-			}
-
-			return toReturn;
-		}
-
-		/// <summary>
-		/// <para>Calculates the single digit point where a line starts on the differing axis
-		/// i.e. the minimum (controlling for negative lengths).</para>
-		/// <para>
-		/// For lines with <see cref="Orientation.Horizontal"/> this is an x coordinate.
-		/// For lines that are <see cref="Orientation.Vertical"/> this is a y coordinate.
-		/// </para>
-		/// </summary>
-		/// <param name="start">Where the line starts</param>
-		/// <param name="length">Length of the line</param>
-		/// <param name="orientation">Orientation of the line</param>
-		/// <returns>The minimum x or y (whichever is differing) point on the line, controlling for negative lengths. </returns>
-		private static int GetLineStartOnDiffAxis (Point start, int length, Orientation orientation)
-		{
-			if(length == 0) {
-				throw new ArgumentException ("0 length lines are not supported", nameof (length));
-			}
-
-			var sub = length > 0 ? 1 : -1;
-
-			if (orientation == Orientation.Vertical) {
-				// Points on line differ by y
-				return Math.Min (start.Y + length - sub, start.Y);
-			}
-
-			// Points on line differ by x
-			return Math.Min (start.X + length - sub, start.X);
-		}
-
-		/// <summary>
-		/// <para>Calculates the single digit point where a line ends on the differing axis
-		/// i.e. the maximum (controlling for negative lengths).</para>
-		/// <para>
-		/// For lines with <see cref="Orientation.Horizontal"/> this is an x coordinate.
-		/// For lines that are <see cref="Orientation.Vertical"/> this is a y coordinate.
-		/// </para>
-		/// </summary>
-		/// <param name="start">Where the line starts</param>
-		/// <param name="length">Length of the line</param>
-		/// <param name="orientation">Orientation of the line</param>
-		/// <returns>The maximum x or y (whichever is differing) point on the line, controlling for negative lengths. </returns>
-		private static int GetLineEndOnDiffAxis (Point start, int length, Orientation orientation)
-		{
-			if (length == 0) {
-				throw new ArgumentException ("0 length lines are not supported", nameof (length));
-			}
-
-			var sub = length > 0 ? 1 : -1;
-
-			if (orientation == Orientation.Vertical) {
-				// Points on line differ by y
-				return Math.Max (start.Y + length - sub, start.Y);
-			}
-
-			// Points on line differ by x
-			return Math.Max (start.X + length - sub, start.X);
-		}
-
-		/// <summary>
-		/// Creates a new line which is part of <paramref name="l"/> from the point on the varying
-		/// axis <paramref name="from"/> to <paramref name="length"/>.  Horizontal lines have points that
-		/// vary by x while vertical lines have points that vary by y
-		/// </summary>
-		/// <param name="l">Line to create sub part from</param>
-		/// <param name="from">Point on varying axis to start at</param>
-		/// <param name="length">Length of line to return</param>
-		/// <returns>The new line</returns>
-		private static StraightLine CreateLineFromDiff (StraightLine l, int from, int length)
-		{
-			var start = new Point (
-				l.Orientation == Orientation.Horizontal ? from : l.Start.X,
-				l.Orientation == Orientation.Horizontal ? l.Start.Y : from);
-
-			return new StraightLine (start, length, l.Orientation, l.Style, l.Attribute);
-		}
-	}
+namespace Terminal.Gui;
+
+/// <summary>Extension methods for <see cref="StraightLine"/> (including collections).</summary>
+public static class StraightLineExtensions
+{
+    /// <summary>
+    ///     Splits or removes all lines in the <paramref name="collection"/> such that none cover the given exclusion
+    ///     area.
+    /// </summary>
+    /// <param name="collection">Lines to adjust</param>
+    /// <param name="start">First point to remove from collection</param>
+    /// <param name="length">The number of sequential points to exclude</param>
+    /// <param name="orientation">Orientation of the exclusion line</param>
+    /// <returns></returns>
+    public static IEnumerable<StraightLine> Exclude (
+        this IEnumerable<StraightLine> collection,
+        Point start,
+        int length,
+        Orientation orientation
+    )
+    {
+        List<StraightLine> toReturn = new ();
+
+        if (length == 0)
+        {
+            return collection;
+        }
+
+        foreach (StraightLine l in collection)
+        {
+            if (l.Length == 0)
+            {
+                toReturn.Add (l);
+
+                continue;
+            }
+
+            // lines are parallel.  For any straight line one axis (x or y) is constant
+            // e.g. Horizontal lines have constant y
+            int econstPoint = orientation == Orientation.Horizontal ? start.Y : start.X;
+            int lconstPoint = l.Orientation == Orientation.Horizontal ? l.Start.Y : l.Start.X;
+
+            // For the varying axis what is the max/mins
+            // i.e. points on horizontal lines vary by x, vertical lines vary by y
+            int eDiffMin = GetLineStartOnDiffAxis (start, length, orientation);
+            int eDiffMax = GetLineEndOnDiffAxis (start, length, orientation);
+            int lDiffMin = GetLineStartOnDiffAxis (l.Start, l.Length, l.Orientation);
+            int lDiffMax = GetLineEndOnDiffAxis (l.Start, l.Length, l.Orientation);
+
+            // line is parallel to exclusion
+            if (l.Orientation == orientation)
+            {
+                // Do the parallel lines share constant plane
+                if (econstPoint != lconstPoint)
+                {
+                    // No, so no way they overlap
+                    toReturn.Add (l);
+                }
+                else
+                {
+                    if (lDiffMax < eDiffMin)
+                    {
+                        // Line ends before exclusion starts
+                        toReturn.Add (l);
+                    }
+                    else if (lDiffMin > eDiffMax)
+                    {
+                        // Line starts after exclusion ends
+                        toReturn.Add (l);
+                    }
+                    else
+                    {
+                        //lines overlap!
+
+                        // Is there a bit we can keep on the left?
+                        if (lDiffMin < eDiffMin)
+                        {
+                            // Create line up to exclusion point
+                            int from = lDiffMin;
+                            int len = eDiffMin - lDiffMin;
+
+                            if (len > 0)
+                            {
+                                toReturn.Add (CreateLineFromDiff (l, from, len));
+                            }
+                        }
+
+                        // Is there a bit we can keep on the right?
+                        if (lDiffMax > eDiffMax)
+                        {
+                            // Create line up to exclusion point
+                            int from = eDiffMax + 1;
+                            int len = lDiffMax - eDiffMax;
+
+                            if (len > 0)
+                            {
+                                // A single line with length 1 and -1 are the same (fills only the single cell)
+                                // They differ only in how they join to other lines (i.e. to create corners)
+                                // Using negative for the later half of the line ensures line joins in a way
+                                // consistent with its pre-snipped state.
+                                if (len == 1)
+                                {
+                                    len = -1;
+                                }
+
+                                toReturn.Add (CreateLineFromDiff (l, from, len));
+                            }
+                        }
+                    }
+                }
+            }
+            else
+            {
+                // line is perpendicular to exclusion
+
+                // Does the constant plane of the exclusion appear within the differing plane of the line?
+                if (econstPoint >= lDiffMin && econstPoint <= lDiffMax)
+                {
+                    // Yes, e.g. Vertical exclusion's x is within xmin/xmax of the horizontal line
+
+                    // Vice versa must also be true
+                    // for example there is no intersection if the vertical exclusion line does not
+                    // stretch down far enough to reach the line
+                    if (lconstPoint >= eDiffMin && lconstPoint <= eDiffMax)
+                    {
+                        // Perpendicular intersection occurs here
+                        Point intersection = l.Orientation == Orientation.Horizontal
+                                                 ? new Point (econstPoint, lconstPoint)
+                                                 : new Point (lconstPoint, econstPoint);
+
+                        // To snip out this single point we will use a recursive call
+                        // snipping 1 length along the orientation of l (i.e. parallel)
+                        toReturn.AddRange (new [] { l }.Exclude (intersection, 1, l.Orientation));
+                    }
+                    else
+                    {
+                        // No intersection
+                        toReturn.Add (l);
+                    }
+                }
+                else
+                {
+                    // Lines do not intersect
+                    toReturn.Add (l);
+                }
+            }
+        }
+
+        return toReturn;
+    }
+
+    /// <summary>
+    ///     Creates a new line which is part of <paramref name="l"/> from the point on the varying axis
+    ///     <paramref name="from"/> to <paramref name="length"/>.  Horizontal lines have points that vary by x while vertical
+    ///     lines have points that vary by y
+    /// </summary>
+    /// <param name="l">Line to create sub part from</param>
+    /// <param name="from">Point on varying axis to start at</param>
+    /// <param name="length">Length of line to return</param>
+    /// <returns>The new line</returns>
+    private static StraightLine CreateLineFromDiff (StraightLine l, int from, int length)
+    {
+        var start = new Point (
+                               l.Orientation == Orientation.Horizontal ? from : l.Start.X,
+                               l.Orientation == Orientation.Horizontal ? l.Start.Y : from
+                              );
+
+        return new StraightLine (start, length, l.Orientation, l.Style, l.Attribute);
+    }
+
+    /// <summary>
+    ///     <para>
+    ///         Calculates the single digit point where a line ends on the differing axis i.e. the maximum (controlling for
+    ///         negative lengths).
+    ///     </para>
+    ///     <para>
+    ///         For lines with <see cref="Orientation.Horizontal"/> this is an x coordinate. For lines that are
+    ///         <see cref="Orientation.Vertical"/> this is a y coordinate.
+    ///     </para>
+    /// </summary>
+    /// <param name="start">Where the line starts</param>
+    /// <param name="length">Length of the line</param>
+    /// <param name="orientation">Orientation of the line</param>
+    /// <returns>The maximum x or y (whichever is differing) point on the line, controlling for negative lengths. </returns>
+    private static int GetLineEndOnDiffAxis (Point start, int length, Orientation orientation)
+    {
+        if (length == 0)
+        {
+            throw new ArgumentException ("0 length lines are not supported", nameof (length));
+        }
+
+        int sub = length > 0 ? 1 : -1;
+
+        if (orientation == Orientation.Vertical)
+        {
+            // Points on line differ by y
+            return Math.Max (start.Y + length - sub, start.Y);
+        }
+
+        // Points on line differ by x
+        return Math.Max (start.X + length - sub, start.X);
+    }
+
+    /// <summary>
+    ///     <para>
+    ///         Calculates the single digit point where a line starts on the differing axis i.e. the minimum (controlling for
+    ///         negative lengths).
+    ///     </para>
+    ///     <para>
+    ///         For lines with <see cref="Orientation.Horizontal"/> this is an x coordinate. For lines that are
+    ///         <see cref="Orientation.Vertical"/> this is a y coordinate.
+    ///     </para>
+    /// </summary>
+    /// <param name="start">Where the line starts</param>
+    /// <param name="length">Length of the line</param>
+    /// <param name="orientation">Orientation of the line</param>
+    /// <returns>The minimum x or y (whichever is differing) point on the line, controlling for negative lengths. </returns>
+    private static int GetLineStartOnDiffAxis (Point start, int length, Orientation orientation)
+    {
+        if (length == 0)
+        {
+            throw new ArgumentException ("0 length lines are not supported", nameof (length));
+        }
+
+        int sub = length > 0 ? 1 : -1;
+
+        if (orientation == Orientation.Vertical)
+        {
+            // Points on line differ by y
+            return Math.Min (start.Y + length - sub, start.Y);
+        }
+
+        // Points on line differ by x
+        return Math.Min (start.X + length - sub, start.X);
+    }
 }

+ 297 - 298
Terminal.Gui/Drawing/Thickness.cs

@@ -1,298 +1,297 @@
-using System;
-using System.Collections.Generic;
-using System.Text;
-using System.Text.Json.Serialization;
-using Terminal.Gui;
-
-namespace Terminal.Gui {
-	/// <summary>
-	/// Describes the thickness of a frame around a rectangle. Four <see cref="int"/> values describe
-	///  the <see cref="Left"/>, <see cref="Top"/>, <see cref="Right"/>, and <see cref="Bottom"/> sides
-	///  of the rectangle, respectively.
-	/// </summary>
-	/// <remarks>
-	/// <para>
-	/// Use the helper API (<see cref="GetInside(Rect)"/> to get the rectangle describing the insides of the frame,
-	/// with the thickness widths subtracted.
-	/// </para>
-	/// <para>
-	/// Use the helper API (<see cref="Draw(Rect, string)"/> to draw the frame with the specified thickness.
-	/// </para>
-	/// </remarks>
-	public class Thickness : IEquatable<Thickness> {
-		private int validate (int width)
-		{
-			if (width < 0) {
-				throw new ArgumentException ("Thickness widths cannot be negative.");
-			}
-			return width;
-		}
-
-		/// <summary>
-		/// Gets or sets the width of the left side of the rectangle.
-		/// </summary>
-		[JsonInclude]
-		public int Left;
-
-		/// <summary>
-		/// Gets or sets the width of the upper side of the rectangle.
-		/// </summary>
-		[JsonInclude]
-		public int Top;
-
-		/// <summary>
-		/// Gets or sets the width of the right side of the rectangle.
-		/// </summary>
-		[JsonInclude]
-		public int Right;
-
-		/// <summary>
-		/// Gets or sets the width of the lower side of the rectangle.
-		/// </summary>
-		[JsonInclude]
-		public int Bottom;
-
-		/// <summary>
-		/// Initializes a new instance of the <see cref="Thickness"/> class with all widths
-		/// set to 0.
-		/// </summary>
-		public Thickness () { }
-
-		/// <summary>
-		/// Initializes a new instance of the <see cref="Thickness"/> class with a uniform width to each side.
-		/// </summary>
-		/// <param name="width"></param>
-		public Thickness (int width) : this (width, width, width, width) { }
-
-		/// <summary>
-		/// Initializes a new instance of the <see cref="Thickness"/> class that has specific
-		///  widths applied to each side of the rectangle.
-		/// </summary>
-		/// <param name="left"></param>
-		/// <param name="top"></param>
-		/// <param name="right"></param>
-		/// <param name="bottom"></param>
-		public Thickness (int left, int top, int right, int bottom)
-		{
-			Left = left;
-			Top = top;
-			Right = right;
-			Bottom = bottom;
-		}
-
-		/// <summary>
-		/// Gets the total height of the top and bottom sides of the rectangle. Sets the height of the top and bottom sides of the rectangle to half the specified value.
-		/// </summary>
-		public int Vertical {
-			get {
-				return Top + Bottom;
-			}
-			set {
-				Top = Bottom = value / 2;
-			}
-		}
-
-		/// <summary>
-		/// Gets the total width of the left and right sides of the rectangle. Sets the width of the left and rigth sides of the rectangle to half the specified value.
-		/// </summary>
-		public int Horizontal {
-			get {
-				return Left + Right;
-			}
-			set {
-				Left = Right = value / 2;
-			}
-		}
-
-		/// <summary>
-		/// Gets whether the specified coordinates lie within the thickness (inside the bounding rectangle but outside of
-		/// the rectangle described by <see cref="GetInside(Rect)"/>.
-		/// </summary>
-		/// <param name="outside">Describes the location and size of the rectangle that contains the thickness.</param>
-		/// <param name="x">The x coord to check.</param>
-		/// <param name="y">The y coord to check.</param>
-		/// <returns><see langword="true"/> if the specified coordinate is within the thickness; <see langword="false"/> otherwise.</returns>
-		public bool Contains (Rect outside, int x, int y)
-		{
-			var inside = GetInside (outside);
-			return outside.Contains (x, y) && !inside.Contains (x, y);
-		}
-
-		/// <summary>
-		/// Returns a rectangle describing the location and size of the inside area of <paramref name="rect"/>
-		/// with the thickness widths subtracted. The height and width of the returned rectangle will
-		/// never be less than 0.
-		/// </summary>
-		/// <remarks>If a thickness width is negative, the inside rectangle will be larger than <paramref name="rect"/>. e.g.
-		/// a <c>Thickness (-1, -1, -1, -1) will result in a rectangle skewed -1 in the X and Y directions and 
-		/// with a Size increased by 1.</c></remarks>
-		/// <param name="rect">The source rectangle</param>
-		/// <returns></returns>
-		public Rect GetInside (Rect rect)
-		{
-			var x = rect.X + Left;
-			var y = rect.Y + Top;
-			var width = Math.Max (0, rect.Size.Width - Horizontal);
-			var height = Math.Max (0, rect.Size.Height - Vertical);
-			return new Rect (new Point (x, y), new Size (width, height));
-		}
-
-		/// <summary>
-		/// Draws the <see cref="Thickness"/> rectangle with an optional diagnostics label.
-		/// </summary>
-		/// <remarks>
-		/// If <see cref="ConsoleDriver.DiagnosticFlags"/> is set to <see cref="ConsoleDriver.DiagnosticFlags.FramePadding"/> then
-		/// 'T', 'L', 'R', and 'B' glyphs will be used instead of space. If <see cref="ConsoleDriver.DiagnosticFlags"/>
-		/// is set to <see cref="ConsoleDriver.DiagnosticFlags.FrameRuler"/> then a ruler will be drawn on the outer edge of the
-		/// Thickness.
-		/// </remarks>
-		/// <param name="rect">The location and size of the rectangle that bounds the thickness rectangle, in 
-		/// screen coordinates.</param>
-		/// <param name="label">The diagnostics label to draw on the bottom of the <see cref="Bottom"/>.</param>
-		/// <returns>The inner rectangle remaining to be drawn.</returns>
-		public Rect Draw (Rect rect, string label = null)
-		{
-			if (rect.Size.Width < 1 || rect.Size.Height < 1) {
-				return Rect.Empty;
-			}
-
-			Rune clearChar = (Rune)' ';
-			Rune leftChar = clearChar;
-			Rune rightChar = clearChar;
-			Rune topChar = clearChar;
-			Rune bottomChar = clearChar;
-
-			if ((ConsoleDriver.Diagnostics & ConsoleDriver.DiagnosticFlags.FramePadding) == ConsoleDriver.DiagnosticFlags.FramePadding) {
-				leftChar = (Rune)'L';
-				rightChar = (Rune)'R';
-				topChar = (Rune)'T';
-				bottomChar = (Rune)'B';
-				if (!string.IsNullOrEmpty (label)) {
-					leftChar = rightChar = bottomChar = topChar = (Rune)label [0];
-				}
-			}
-
-			// Draw the Top side
-			if (Top > 0) {
-				Application.Driver.FillRect (new Rect (rect.X, rect.Y, rect.Width, Math.Min (rect.Height, Top)), topChar);
-			}
-
-			// Draw the Left side
-			if (Left > 0) {
-				Application.Driver.FillRect (new Rect (rect.X, rect.Y, Math.Min (rect.Width, Left), rect.Height), leftChar);
-			}
-
-			// Draw the Right side			
-			if (Right > 0) {
-				Application.Driver.FillRect (new Rect (Math.Max (0, rect.X + rect.Width - Right), rect.Y, Math.Min (rect.Width, Right), rect.Height), rightChar);
-			}
-
-			// Draw the Bottom side
-			if (Bottom > 0) {
-				Application.Driver.FillRect (new Rect (rect.X, rect.Y + Math.Max (0, rect.Height - Bottom), rect.Width, Bottom), bottomChar);
-			}
-
-			// TODO: This should be moved to LineCanvas as a new LineStyle.Ruler
-			if ((ConsoleDriver.Diagnostics & ConsoleDriver.DiagnosticFlags.FrameRuler) == ConsoleDriver.DiagnosticFlags.FrameRuler) {
-				// Top
-				var hruler = new Ruler () { Length = rect.Width, Orientation = Orientation.Horizontal };
-				if (Top > 0) {
-					hruler.Draw (new Point (rect.X, rect.Y));
-				}
-
-				//Left
-				var vruler = new Ruler () { Length = rect.Height - 2, Orientation = Orientation.Vertical };
-				if (Left > 0) {
-					vruler.Draw (new Point (rect.X, rect.Y + 1), 1);
-				}
-
-				// Bottom
-				if (Bottom > 0) {
-					hruler.Draw (new Point (rect.X, rect.Y + rect.Height - 1));
-				}
-
-				// Right
-				if (Right > 0) {
-					vruler.Draw (new Point (rect.X + rect.Width - 1, rect.Y + 1), 1);
-				}
-			}
-
-			if ((ConsoleDriver.Diagnostics & ConsoleDriver.DiagnosticFlags.FramePadding) == ConsoleDriver.DiagnosticFlags.FramePadding) {
-				// Draw the diagnostics label on the bottom
-				var tf = new TextFormatter () {
-					Text = label == null ? string.Empty : $"{label} {this}",
-					Alignment = TextAlignment.Centered,
-					VerticalAlignment = VerticalTextAlignment.Bottom
-				};
-				tf.Draw (rect, Application.Driver.CurrentAttribute, Application.Driver.CurrentAttribute, rect, false);
-			}
-
-			return GetInside (rect);
-
-		}
-
-		// TODO: add operator overloads
-		/// <summary>
-		/// Gets an empty thickness.
-		/// </summary>
-		public static Thickness Empty => new Thickness (0);
-
-		/// <summary>Determines whether the specified object is equal to the current object.</summary>
-		/// <param name="obj">The object to compare with the current object.</param>
-		/// <returns><c>true</c> if the specified object is equal to the current object; otherwise, <c>false</c>.</returns>
-		public override bool Equals (object obj)
-		{
-			//Check for null and compare run-time types.
-			if ((obj == null) || !this.GetType ().Equals (obj.GetType ())) {
-				return false;
-			} else {
-				return Equals ((Thickness)obj);
-			}
-		}
-
-		/// <summary>Returns the thickness widths of the Thickness formatted as a string.</summary>
-		/// <returns>The thickness widths as a string.</returns>
-		public override string ToString ()
-		{
-			return $"(Left={Left},Top={Top},Right={Right},Bottom={Bottom})";
-		}
-
-		// IEquitable
-		/// <summary>
-		/// Indicates whether the current object is equal to another object of the same type.
-		/// </summary>
-		/// <param name="other"></param>
-		/// <returns>true if the current object is equal to the other parameter; otherwise, false.</returns>
-		public bool Equals (Thickness other)
-		{
-			return other is not null &&
-			       Left == other.Left &&
-			       Right == other.Right &&
-			       Top == other.Top &&
-			       Bottom == other.Bottom;
-		}
-
-		/// <inheritdoc/>
-		public override int GetHashCode ()
-		{
-			int hashCode = 1380952125;
-			hashCode = hashCode * -1521134295 + Left.GetHashCode ();
-			hashCode = hashCode * -1521134295 + Right.GetHashCode ();
-			hashCode = hashCode * -1521134295 + Top.GetHashCode ();
-			hashCode = hashCode * -1521134295 + Bottom.GetHashCode ();
-			return hashCode;
-		}
-
-		/// <inheritdoc/>
-		public static bool operator == (Thickness left, Thickness right)
-		{
-			return EqualityComparer<Thickness>.Default.Equals (left, right);
-		}
-
-		/// <inheritdoc/>
-		public static bool operator != (Thickness left, Thickness right)
-		{
-			return !(left == right);
-		}
-	}
-}
+using System.Text.Json.Serialization;
+
+namespace Terminal.Gui;
+
+/// <summary>
+///     Describes the thickness of a frame around a rectangle. Four <see cref="int"/> values describe the
+///     <see cref="Left"/>, <see cref="Top"/>, <see cref="Right"/>, and <see cref="Bottom"/> sides of the rectangle,
+///     respectively.
+/// </summary>
+/// <remarks>
+///     <para>
+///         Use the helper API (<see cref="GetInside(Rectangle)"/> to get the rectangle describing the insides of the frame,
+///         with the thickness widths subtracted.
+///     </para>
+///     <para>Use the helper API (<see cref="Draw(Rectangle, string)"/> to draw the frame with the specified thickness.</para>
+/// </remarks>
+public class Thickness : IEquatable<Thickness>
+{
+    /// <summary>Gets or sets the width of the lower side of the rectangle.</summary>
+    [JsonInclude]
+    public int Bottom;
+
+    /// <summary>Gets or sets the width of the left side of the rectangle.</summary>
+    [JsonInclude]
+    public int Left;
+
+    /// <summary>Gets or sets the width of the right side of the rectangle.</summary>
+    [JsonInclude]
+    public int Right;
+
+    /// <summary>Gets or sets the width of the upper side of the rectangle.</summary>
+    [JsonInclude]
+    public int Top;
+
+    /// <summary>Initializes a new instance of the <see cref="Thickness"/> class with all widths set to 0.</summary>
+    public Thickness () { }
+
+    /// <summary>Initializes a new instance of the <see cref="Thickness"/> class with a uniform width to each side.</summary>
+    /// <param name="width"></param>
+    public Thickness (int width) : this (width, width, width, width) { }
+
+    /// <summary>
+    ///     Initializes a new instance of the <see cref="Thickness"/> class that has specific widths applied to each side
+    ///     of the rectangle.
+    /// </summary>
+    /// <param name="left"></param>
+    /// <param name="top"></param>
+    /// <param name="right"></param>
+    /// <param name="bottom"></param>
+    public Thickness (int left, int top, int right, int bottom)
+    {
+        Left = left;
+        Top = top;
+        Right = right;
+        Bottom = bottom;
+    }
+
+    // TODO: add operator overloads
+    /// <summary>Gets an empty thickness.</summary>
+    public static Thickness Empty => new (0);
+
+    /// <summary>
+    ///     Gets the total width of the left and right sides of the rectangle. Sets the width of the left and rigth sides
+    ///     of the rectangle to half the specified value.
+    /// </summary>
+    public int Horizontal
+    {
+        get => Left + Right;
+        set => Left = Right = value / 2;
+    }
+
+    /// <summary>
+    ///     Gets the total height of the top and bottom sides of the rectangle. Sets the height of the top and bottom
+    ///     sides of the rectangle to half the specified value.
+    /// </summary>
+    public int Vertical
+    {
+        get => Top + Bottom;
+        set => Top = Bottom = value / 2;
+    }
+
+    // IEquitable
+    /// <summary>Indicates whether the current object is equal to another object of the same type.</summary>
+    /// <param name="other"></param>
+    /// <returns>true if the current object is equal to the other parameter; otherwise, false.</returns>
+    public bool Equals (Thickness other) { return other is { } && Left == other.Left && Right == other.Right && Top == other.Top && Bottom == other.Bottom; }
+
+    /// <summary>
+    ///     Gets whether the specified coordinates lie within the thickness (inside the bounding rectangle but outside of
+    ///     the rectangle described by <see cref="GetInside(Rectangle)"/>.
+    /// </summary>
+    /// <param name="outside">Describes the location and size of the rectangle that contains the thickness.</param>
+    /// <param name="x">The x coord to check.</param>
+    /// <param name="y">The y coord to check.</param>
+    /// <returns><see langword="true"/> if the specified coordinate is within the thickness; <see langword="false"/> otherwise.</returns>
+    public bool Contains (Rectangle outside, int x, int y)
+    {
+        Rectangle inside = GetInside (outside);
+
+        return outside.Contains (x, y) && !inside.Contains (x, y);
+    }
+
+    /// <summary>Draws the <see cref="Thickness"/> rectangle with an optional diagnostics label.</summary>
+    /// <remarks>
+    ///     If <see cref="ConsoleDriver.DiagnosticFlags"/> is set to
+    ///     <see cref="ConsoleDriver.DiagnosticFlags.FramePadding"/> then 'T', 'L', 'R', and 'B' glyphs will be used instead of
+    ///     space. If <see cref="ConsoleDriver.DiagnosticFlags"/> is set to
+    ///     <see cref="ConsoleDriver.DiagnosticFlags.FrameRuler"/> then a ruler will be drawn on the outer edge of the
+    ///     Thickness.
+    /// </remarks>
+    /// <param name="rect">The location and size of the rectangle that bounds the thickness rectangle, in screen coordinates.</param>
+    /// <param name="label">The diagnostics label to draw on the bottom of the <see cref="Bottom"/>.</param>
+    /// <returns>The inner rectangle remaining to be drawn.</returns>
+    public Rectangle Draw (Rectangle rect, string label = null)
+    {
+        if (rect.Size.Width < 1 || rect.Size.Height < 1)
+        {
+            return Rectangle.Empty;
+        }
+
+        var clearChar = (Rune)' ';
+        Rune leftChar = clearChar;
+        Rune rightChar = clearChar;
+        Rune topChar = clearChar;
+        Rune bottomChar = clearChar;
+
+        if ((ConsoleDriver.Diagnostics & ConsoleDriver.DiagnosticFlags.FramePadding)
+            == ConsoleDriver.DiagnosticFlags.FramePadding)
+        {
+            leftChar = (Rune)'L';
+            rightChar = (Rune)'R';
+            topChar = (Rune)'T';
+            bottomChar = (Rune)'B';
+
+            if (!string.IsNullOrEmpty (label))
+            {
+                leftChar = rightChar = bottomChar = topChar = (Rune)label [0];
+            }
+        }
+
+        // Draw the Top side
+        if (Top > 0)
+        {
+            Application.Driver.FillRect (rect with { Height = Math.Min (rect.Height, Top) }, topChar);
+        }
+
+        // Draw the Left side
+        if (Left > 0)
+        {
+            Application.Driver.FillRect (rect with { Width = Math.Min (rect.Width, Left) }, leftChar);
+        }
+
+        // Draw the Right side
+        if (Right > 0)
+        {
+            Application.Driver.FillRect (
+                                         rect with
+                                         {
+                                             X = Math.Max (0, rect.X + rect.Width - Right),
+                                             Width = Math.Min (rect.Width, Right)
+                                         },
+                                         rightChar
+                                        );
+        }
+
+        // Draw the Bottom side
+        if (Bottom > 0)
+        {
+            Application.Driver.FillRect (
+                                         rect with
+                                         {
+                                             Y = rect.Y + Math.Max (0, rect.Height - Bottom),
+                                             Height = Bottom
+                                         },
+                                         bottomChar
+                                        );
+        }
+
+        // TODO: This should be moved to LineCanvas as a new LineStyle.Ruler
+        if ((ConsoleDriver.Diagnostics & ConsoleDriver.DiagnosticFlags.FrameRuler)
+            == ConsoleDriver.DiagnosticFlags.FrameRuler)
+        {
+            // PERF: This can almost certainly be simplified down to a single point offset and fewer calls to Draw
+            // Top
+            var hruler = new Ruler { Length = rect.Width, Orientation = Orientation.Horizontal };
+
+            if (Top > 0)
+            {
+                hruler.Draw (rect.Location);
+            }
+
+            //Left
+            var vruler = new Ruler { Length = rect.Height - 2, Orientation = Orientation.Vertical };
+
+            if (Left > 0)
+            {
+                vruler.Draw (rect.Location with { Y = rect.Y + 1 }, 1);
+            }
+
+            // Bottom
+            if (Bottom > 0)
+            {
+                hruler.Draw (rect.Location with { Y = rect.Y + rect.Height - 1 });
+            }
+
+            // Right
+            if (Right > 0)
+            {
+                vruler.Draw (new (rect.X + rect.Width - 1, rect.Y + 1), 1);
+            }
+        }
+
+        if ((ConsoleDriver.Diagnostics & ConsoleDriver.DiagnosticFlags.FramePadding)
+            == ConsoleDriver.DiagnosticFlags.FramePadding)
+        {
+            // Draw the diagnostics label on the bottom
+            var tf = new TextFormatter
+            {
+                Text = label is null ? string.Empty : $"{label} {this}",
+                Alignment = TextAlignment.Centered,
+                VerticalAlignment = VerticalTextAlignment.Bottom
+            };
+            tf.Draw (rect, Application.Driver.CurrentAttribute, Application.Driver.CurrentAttribute, rect);
+        }
+
+        return GetInside (rect);
+    }
+
+    /// <summary>Determines whether the specified object is equal to the current object.</summary>
+    /// <param name="obj">The object to compare with the current object.</param>
+    /// <returns><c>true</c> if the specified object is equal to the current object; otherwise, <c>false</c>.</returns>
+    public override bool Equals (object obj)
+    {
+        //Check for null and compare run-time types.
+        if (obj is null || !GetType ().Equals (obj.GetType ()))
+        {
+            return false;
+        }
+
+        return Equals ((Thickness)obj);
+    }
+
+    /// <inheritdoc/>
+    public override int GetHashCode ()
+    {
+        var hashCode = 1380952125;
+        hashCode = hashCode * -1521134295 + Left.GetHashCode ();
+        hashCode = hashCode * -1521134295 + Right.GetHashCode ();
+        hashCode = hashCode * -1521134295 + Top.GetHashCode ();
+        hashCode = hashCode * -1521134295 + Bottom.GetHashCode ();
+
+        return hashCode;
+    }
+
+    /// <summary>
+    ///     Returns a rectangle describing the location and size of the inside area of <paramref name="rect"/> with the
+    ///     thickness widths subtracted. The height and width of the returned rectangle will never be less than 0.
+    /// </summary>
+    /// <remarks>
+    ///     If a thickness width is negative, the inside rectangle will be larger than <paramref name="rect"/>. e.g. a
+    ///     <c>
+    ///         Thickness (-1, -1, -1, -1) will result in a rectangle skewed -1 in the X and Y directions and with a Size
+    ///         increased by 1.
+    ///     </c>
+    /// </remarks>
+    /// <param name="rect">The source rectangle</param>
+    /// <returns></returns>
+    public Rectangle GetInside (Rectangle rect)
+    {
+        int x = rect.X + Left;
+        int y = rect.Y + Top;
+        int width = Math.Max (0, rect.Size.Width - Horizontal);
+        int height = Math.Max (0, rect.Size.Height - Vertical);
+
+        return new (x, y, width, height);
+    }
+
+    /// <inheritdoc/>
+    public static bool operator == (Thickness left, Thickness right) { return EqualityComparer<Thickness>.Default.Equals (left, right); }
+
+    /// <inheritdoc/>
+    public static bool operator != (Thickness left, Thickness right) { return !(left == right); }
+
+    /// <summary>Returns the thickness widths of the Thickness formatted as a string.</summary>
+    /// <returns>The thickness widths as a string.</returns>
+    public override string ToString () { return $"(Left={Left},Top={Top},Right={Right},Bottom={Bottom})"; }
+
+    private int validate (int width)
+    {
+        if (width < 0)
+        {
+            throw new ArgumentException ("Thickness widths cannot be negative.");
+        }
+
+        return width;
+    }
+}

+ 12 - 24
Terminal.Gui/Drawing/ThicknessEventArgs.cs

@@ -1,28 +1,16 @@
-using System;
+#nullable enable
 
-#nullable enable
+namespace Terminal.Gui;
 
-namespace Terminal.Gui {
-	/// <summary>
-	/// Event arguments for the <see cref="Thickness"/> events.
-	/// </summary>
-	public class ThicknessEventArgs : EventArgs {
+/// <summary>Event arguments for the <see cref="Thickness"/> events.</summary>
+public class ThicknessEventArgs : EventArgs
+{
+    /// <summary>Initializes a new instance of <see cref="ThicknessEventArgs"/></summary>
+    public ThicknessEventArgs () { }
 
-		/// <summary>
-		/// Initializes a new instance of <see cref="ThicknessEventArgs"/>
-		/// </summary>
-		public ThicknessEventArgs ()
-		{
-		}
+    /// <summary>The previous Thickness.</summary>
+    public Thickness PreviousThickness { get; set; } = Thickness.Empty;
 
-		/// <summary>
-		/// The new Thickness.
-		/// </summary>
-		public Thickness Thickness { get; set; } = Thickness.Empty;
-
-		/// <summary>
-		/// The previous Thickness.
-		/// </summary>
-		public Thickness PreviousThickness { get; set; } = Thickness.Empty;
-	}
-}
+    /// <summary>The new Thickness.</summary>
+    public Thickness Thickness { get; set; } = Thickness.Empty;
+}

+ 93 - 116
Terminal.Gui/FileServices/AllowedType.cs

@@ -1,116 +1,93 @@
-using System;
-using System.CodeDom;
-using System.Data;
-using System.IO;
-using System.Linq;
-using Terminal.Gui.Resources;
-
-namespace Terminal.Gui {
-
-	/// <summary>
-	/// Interface for <see cref="FileDialog"/> restrictions on which file type(s) the
-	/// user is allowed to select/enter.
-	/// </summary>
-	public interface IAllowedType
-	{
-		/// <summary>
-		/// Returns true if the file at <paramref name="path"/> is compatible with this
-		/// allow option.  Note that the file may not exist (e.g. in the case of saving).
-		/// </summary>
-		/// <param name="path"></param>
-		/// <returns></returns>
-		bool IsAllowed (string path);
-	}
-
-	/// <summary>
-	/// <see cref="IAllowedType"/> that allows selection of any types (*.*).
-	/// </summary>
-	public class AllowedTypeAny : IAllowedType {
-
-		/// <inheritdoc/>
-		public bool IsAllowed (string path)
-		{
-			return true;
-		}
-
-		/// <summary>
-		/// Returns a string representation of this <see cref="AllowedTypeAny"/>.
-		/// </summary>
-		/// <returns></returns>
-		public override string ToString ()
-		{
-			return Strings.fdAnyFiles + "(*.*)";
-		}
-	}
-
-	/// <summary>
-	/// Describes a requirement on what <see cref="FileInfo"/> can be selected.
-	/// This can be combined with other <see cref="IAllowedType"/> in a <see cref="FileDialog"/>
-	/// to for example show only .csv files but let user change to open any if they want.
-	/// </summary>
-	public class AllowedType : IAllowedType {
-
-		/// <summary>
-		/// Initializes a new instance of the <see cref="AllowedType"/> class.
-		/// </summary>
-		/// <param name="description">The human readable text to display.</param>
-		/// <param name="extensions">Extension(s) to match e.g. .csv.</param>
-		public AllowedType (string description, params string [] extensions)
-		{
-			if (extensions.Length == 0) {
-				throw new ArgumentException ("You must supply at least one extension");
-			}
-
-			this.Description = description;
-			this.Extensions = extensions;
-		}
-
-		/// <summary>
-		/// Gets or Sets the human readable description for the file type
-		/// e.g. "Comma Separated Values".
-		/// </summary>
-		public string Description { get; set; }
-
-		/// <summary>
-		/// Gets or Sets the permitted file extension(s) (e.g. ".csv").
-		/// </summary>
-		public string [] Extensions { get; set; }
-
-		/// <summary>
-		/// Returns <see cref="Description"/> plus all <see cref="Extensions"/> separated by semicolons.
-		/// </summary>
-		public override string ToString ()
-		{
-			const int maxLength = 30;
-
-			var desc = $"{this.Description} ({string.Join (";", this.Extensions.Select (e => '*' + e).ToArray ())})";
-
-			if(desc.Length > maxLength) {
-				return desc.Substring (0, maxLength-2) + "…";
-			}
-			return desc;
-		}
-
-		/// <inheritdoc/>
-		public bool IsAllowed(string path)
-		{
-			if(string.IsNullOrWhiteSpace(path)) {
-				return false;
-			}
-
-			var extension = Path.GetExtension (path);
-
-			if(this.Extensions.Any(e=>path.EndsWith(e, StringComparison.InvariantCultureIgnoreCase))) {
-				return true;
-			}
-
-			// There is a requirement to have a particular extension and we have none
-			if (string.IsNullOrEmpty (extension)) {
-				return false;
-			}
-
-			return this.Extensions.Any (e => e.Equals (extension, StringComparison.InvariantCultureIgnoreCase));
-		}
-	}
-	
-}
+using Terminal.Gui.Resources;
+
+namespace Terminal.Gui;
+
+/// <summary>Interface for <see cref="FileDialog"/> restrictions on which file type(s) the user is allowed to select/enter.</summary>
+public interface IAllowedType
+{
+    /// <summary>
+    ///     Returns true if the file at <paramref name="path"/> is compatible with this allow option.  Note that the file
+    ///     may not exist (e.g. in the case of saving).
+    /// </summary>
+    /// <param name="path"></param>
+    /// <returns></returns>
+    bool IsAllowed (string path);
+}
+
+/// <summary><see cref="IAllowedType"/> that allows selection of any types (*.*).</summary>
+public class AllowedTypeAny : IAllowedType
+{
+    /// <inheritdoc/>
+    public bool IsAllowed (string path) { return true; }
+
+    /// <summary>Returns a string representation of this <see cref="AllowedTypeAny"/>.</summary>
+    /// <returns></returns>
+    public override string ToString () { return Strings.fdAnyFiles + "(*.*)"; }
+}
+
+/// <summary>
+///     Describes a requirement on what <see cref="FileInfo"/> can be selected. This can be combined with other
+///     <see cref="IAllowedType"/> in a <see cref="FileDialog"/> to for example show only .csv files but let user change to
+///     open any if they want.
+/// </summary>
+public class AllowedType : IAllowedType
+{
+    /// <summary>Initializes a new instance of the <see cref="AllowedType"/> class.</summary>
+    /// <param name="description">The human readable text to display.</param>
+    /// <param name="extensions">Extension(s) to match e.g. .csv.</param>
+    public AllowedType (string description, params string [] extensions)
+    {
+        if (extensions.Length == 0)
+        {
+            throw new ArgumentException ("You must supply at least one extension");
+        }
+
+        Description = description;
+        Extensions = extensions;
+    }
+
+    /// <summary>Gets or Sets the human readable description for the file type e.g. "Comma Separated Values".</summary>
+    public string Description { get; set; }
+
+    /// <summary>Gets or Sets the permitted file extension(s) (e.g. ".csv").</summary>
+    public string [] Extensions { get; set; }
+
+    /// <inheritdoc/>
+    public bool IsAllowed (string path)
+    {
+        if (string.IsNullOrWhiteSpace (path))
+        {
+            return false;
+        }
+
+        string extension = Path.GetExtension (path);
+
+        if (Extensions.Any (e => path.EndsWith (e, StringComparison.InvariantCultureIgnoreCase)))
+        {
+            return true;
+        }
+
+        // There is a requirement to have a particular extension and we have none
+        if (string.IsNullOrEmpty (extension))
+        {
+            return false;
+        }
+
+        return Extensions.Any (e => e.Equals (extension, StringComparison.InvariantCultureIgnoreCase));
+    }
+
+    /// <summary>Returns <see cref="Description"/> plus all <see cref="Extensions"/> separated by semicolons.</summary>
+    public override string ToString ()
+    {
+        const int maxLength = 30;
+
+        var desc = $"{Description} ({string.Join (";", Extensions.Select (e => '*' + e).ToArray ())})";
+
+        if (desc.Length > maxLength)
+        {
+            return desc.Substring (0, maxLength - 2) + "…";
+        }
+
+        return desc;
+    }
+}

+ 167 - 137
Terminal.Gui/FileServices/DefaultFileOperations.cs

@@ -1,140 +1,170 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
 using System.IO.Abstractions;
-using System.Linq;
 using Terminal.Gui.Resources;
 
-namespace Terminal.Gui {
-	/// <summary>
-	/// Default file operation handlers using modal dialogs.
-	/// </summary>
-	public class DefaultFileOperations : IFileOperations {
-
-		/// <inheritdoc/>
-		public bool Delete (IEnumerable<IFileSystemInfo> toDelete)
-		{
-			// Default implementation does not allow deleting multiple files
-			if (toDelete.Count () != 1) {
-				return false;
-			}
-			var d = toDelete.Single ();
-			var adjective = d.Name;
-
-			int result = MessageBox.Query (
-				string.Format (Strings.fdDeleteTitle, adjective),
-				string.Format (Strings.fdDeleteBody, adjective),
-				Strings.btnYes, Strings.btnNo);
-
-			try {
-				if (result == 0) {
-					if (d is IFileInfo) {
-						d.Delete ();
-					} else {
-						((IDirectoryInfo)d).Delete (true);
-					}
-
-					return true;
-				}
-			} catch (Exception ex) {
-				MessageBox.ErrorQuery (Strings.fdDeleteFailedTitle, ex.Message, Strings.btnOk);
-			}
-
-			return false;
-		}
-
-		private bool Prompt (string title, string defaultText, out string result)
-		{
-
-			bool confirm = false;
-			var btnOk = new Button (Strings.btnOk) {
-				IsDefault = true,
-			};
-			btnOk.Clicked += (s, e) => {
-				confirm = true;
-				Application.RequestStop ();
-			};
-			var btnCancel = new Button (Strings.btnCancel);
-			btnCancel.Clicked += (s, e) => {
-				confirm = false;
-				Application.RequestStop ();
-			};
-
-			var lbl = new Label (Strings.fdRenamePrompt);
-			var tf = new TextField (defaultText) {
-				X = Pos.Right (lbl),
-				Width = Dim.Fill (),
-			};
-			tf.SelectAll ();
-
-			var dlg = new Dialog () {
-				Title = title,
-				Width = Dim.Percent (50),
-				Height = 4
-			};
-			dlg.Add (lbl);
-			dlg.Add (tf);
-
-			// Add buttons last so tab order is friendly
-			// and TextField gets focus
-			dlg.AddButton (btnOk);
-			dlg.AddButton (btnCancel);
-
-			Application.Run (dlg);
-
-			result = tf.Text;
-
-			return confirm;
-		}
-
-		/// <inheritdoc/>
-		public IFileSystemInfo Rename (IFileSystem fileSystem, IFileSystemInfo toRename)
-		{
-			// Don't allow renaming C: or D: or / (on linux) etc
-			if (toRename is IDirectoryInfo dir && dir.Parent == null) {
-				return null;
-			}
-
-			if (Prompt (Strings.fdRenameTitle, toRename.Name, out var newName)) {
-				if (!string.IsNullOrWhiteSpace (newName)) {
-					try {
-						if (toRename is IFileInfo f) {
-
-							var newLocation = fileSystem.FileInfo.New (Path.Combine (f.Directory.FullName, newName));
-							f.MoveTo (newLocation.FullName);
-							return newLocation;
-
-						} else {
-							var d = (IDirectoryInfo)toRename;
-
-							var newLocation = fileSystem.DirectoryInfo.New (Path.Combine (d.Parent.FullName, newName));
-							d.MoveTo (newLocation.FullName);
-							return newLocation;
-						}
-					} catch (Exception ex) {
-						MessageBox.ErrorQuery (Strings.fdRenameFailedTitle, ex.Message, "Ok");
-					}
-				}
-			}
-
-			return null;
-		}
-
-		/// <inheritdoc/>
-		public IFileSystemInfo New (IFileSystem fileSystem, IDirectoryInfo inDirectory)
-		{
-			if (Prompt (Strings.fdNewTitle, "", out var named)) {
-				if (!string.IsNullOrWhiteSpace (named)) {
-					try {
-						var newDir = fileSystem.DirectoryInfo.New (Path.Combine (inDirectory.FullName, named));
-						newDir.Create ();
-						return newDir;
-					} catch (Exception ex) {
-						MessageBox.ErrorQuery (Strings.fdNewFailed, ex.Message, "Ok");
-					}
-				}
-			}
-			return null;
-		}
-	}
-}
+namespace Terminal.Gui;
+
+/// <summary>Default file operation handlers using modal dialogs.</summary>
+public class DefaultFileOperations : IFileOperations
+{
+    /// <inheritdoc/>
+    public bool Delete (IEnumerable<IFileSystemInfo> toDelete)
+    {
+        // Default implementation does not allow deleting multiple files
+        if (toDelete.Count () != 1)
+        {
+            return false;
+        }
+
+        IFileSystemInfo d = toDelete.Single ();
+        string adjective = d.Name;
+
+        int result = MessageBox.Query (
+                                       string.Format (Strings.fdDeleteTitle, adjective),
+                                       string.Format (Strings.fdDeleteBody, adjective),
+                                       Strings.btnYes,
+                                       Strings.btnNo
+                                      );
+
+        try
+        {
+            if (result == 0)
+            {
+                if (d is IFileInfo)
+                {
+                    d.Delete ();
+                }
+                else
+                {
+                    ((IDirectoryInfo)d).Delete (true);
+                }
+
+                return true;
+            }
+        }
+        catch (Exception ex)
+        {
+            MessageBox.ErrorQuery (Strings.fdDeleteFailedTitle, ex.Message, Strings.btnOk);
+        }
+
+        return false;
+    }
+
+    /// <inheritdoc/>
+    public IFileSystemInfo Rename (IFileSystem fileSystem, IFileSystemInfo toRename)
+    {
+        // Don't allow renaming C: or D: or / (on linux) etc
+        if (toRename is IDirectoryInfo dir && dir.Parent is null)
+        {
+            return null;
+        }
+
+        if (Prompt (Strings.fdRenameTitle, toRename.Name, out string newName))
+        {
+            if (!string.IsNullOrWhiteSpace (newName))
+            {
+                try
+                {
+                    if (toRename is IFileInfo f)
+                    {
+                        IFileInfo newLocation =
+                            fileSystem.FileInfo.New (
+                                                     Path.Combine (
+                                                                   f.Directory.FullName,
+                                                                   newName
+                                                                  )
+                                                    );
+                        f.MoveTo (newLocation.FullName);
+
+                        return newLocation;
+                    }
+                    else
+                    {
+                        var d = (IDirectoryInfo)toRename;
+
+                        IDirectoryInfo newLocation =
+                            fileSystem.DirectoryInfo.New (
+                                                          Path.Combine (
+                                                                        d.Parent.FullName,
+                                                                        newName
+                                                                       )
+                                                         );
+                        d.MoveTo (newLocation.FullName);
+
+                        return newLocation;
+                    }
+                }
+                catch (Exception ex)
+                {
+                    MessageBox.ErrorQuery (Strings.fdRenameFailedTitle, ex.Message, "Ok");
+                }
+            }
+        }
+
+        return null;
+    }
+
+    /// <inheritdoc/>
+    public IFileSystemInfo New (IFileSystem fileSystem, IDirectoryInfo inDirectory)
+    {
+        if (Prompt (Strings.fdNewTitle, "", out string named))
+        {
+            if (!string.IsNullOrWhiteSpace (named))
+            {
+                try
+                {
+                    IDirectoryInfo newDir =
+                        fileSystem.DirectoryInfo.New (
+                                                      Path.Combine (inDirectory.FullName, named)
+                                                     );
+                    newDir.Create ();
+
+                    return newDir;
+                }
+                catch (Exception ex)
+                {
+                    MessageBox.ErrorQuery (Strings.fdNewFailed, ex.Message, "Ok");
+                }
+            }
+        }
+
+        return null;
+    }
+
+    private bool Prompt (string title, string defaultText, out string result)
+    {
+        var confirm = false;
+        var btnOk = new Button { IsDefault = true, Text = Strings.btnOk };
+
+        btnOk.Accept += (s, e) =>
+                         {
+                             confirm = true;
+                             Application.RequestStop ();
+                         };
+        var btnCancel = new Button { Text = Strings.btnCancel };
+
+        btnCancel.Accept += (s, e) =>
+                             {
+                                 confirm = false;
+                                 Application.RequestStop ();
+                             };
+
+        var lbl = new Label { Text = Strings.fdRenamePrompt };
+        var tf = new TextField { X = Pos.Right (lbl), Width = Dim.Fill (), Text = defaultText };
+        tf.SelectAll ();
+
+        var dlg = new Dialog { Title = title, Width = Dim.Percent (50), Height = 4 };
+        dlg.Add (lbl);
+        dlg.Add (tf);
+
+        // Add buttons last so tab order is friendly
+        // and TextField gets focus
+        dlg.AddButton (btnOk);
+        dlg.AddButton (btnCancel);
+
+        Application.Run (dlg);
+
+        result = tf.Text;
+
+        return confirm;
+    }
+}

+ 19 - 24
Terminal.Gui/FileServices/DefaultSearchMatcher.cs

@@ -1,29 +1,24 @@
-using System;
-using System.IO;
-using System.IO.Abstractions;
-using System.Linq;
+using System.IO.Abstractions;
 
-namespace Terminal.Gui {
-	class DefaultSearchMatcher : ISearchMatcher {
-		string [] terms;
+namespace Terminal.Gui;
 
-		public void Initialize (string terms)
-		{
-			this.terms = terms.Split (new string [] { " " }, StringSplitOptions.RemoveEmptyEntries);
-		}
+internal class DefaultSearchMatcher : ISearchMatcher
+{
+    private string [] terms;
+    public void Initialize (string terms) { this.terms = terms.Split (new [] { " " }, StringSplitOptions.RemoveEmptyEntries); }
 
-		public bool IsMatch (IFileSystemInfo f)
-		{
-			//Contains overload with StringComparison is not available in (net472) or (netstandard2.0)
-			//return f.Name.Contains (terms, StringComparison.OrdinalIgnoreCase);
+    public bool IsMatch (IFileSystemInfo f)
+    {
+        //Contains overload with StringComparison is not available in (net472) or (netstandard2.0)
+        //return f.Name.Contains (terms, StringComparison.OrdinalIgnoreCase);
 
-			return
-				// At least one term must match the file name only e.g. "my" in "myfile.csv"
-				terms.Any (t => f.Name.IndexOf (t, StringComparison.OrdinalIgnoreCase) >= 0)
-				&&
-				// All terms must exist in full path e.g. "dos my" can match "c:\documents\myfile.csv"
-				terms.All (t => f.FullName.IndexOf (t, StringComparison.OrdinalIgnoreCase) >= 0);
-		}
-	}
+        return
 
-}
+            // At least one term must match the file name only e.g. "my" in "myfile.csv"
+            terms.Any (t => f.Name.IndexOf (t, StringComparison.OrdinalIgnoreCase) >= 0)
+            &&
+
+            // All terms must exist in full path e.g. "dos my" can match "c:\documents\myfile.csv"
+            terms.All (t => f.FullName.IndexOf (t, StringComparison.OrdinalIgnoreCase) >= 0);
+    }
+}

+ 97 - 111
Terminal.Gui/FileServices/FileDialogHistory.cs

@@ -1,111 +1,97 @@
-using System.Collections.Generic;
-using System.IO;
-using System.IO.Abstractions;
-
-namespace Terminal.Gui {
-
-	internal class FileDialogHistory {
-		private Stack<FileDialogState> back = new Stack<FileDialogState> ();
-		private Stack<FileDialogState> forward = new Stack<FileDialogState> ();
-		private FileDialog dlg;
-
-		public FileDialogHistory (FileDialog dlg)
-		{
-			this.dlg = dlg;
-		}
-
-		public bool Back ()
-		{
-
-			IDirectoryInfo goTo = null;
-			FileSystemInfoStats restoreSelection = null;
-			string restorePath = null;
-
-			if (this.CanBack ()) {
-
-				var backTo = this.back.Pop ();
-				goTo = backTo.Directory;
-				restoreSelection = backTo.Selected;
-				restorePath = backTo.Path;
-
-			} else if (this.CanUp ()) {
-				goTo = this.dlg.State?.Directory.Parent;
-			}
-
-			// nowhere to go
-			if (goTo == null) {
-				return false;
-			}
-
-			this.forward.Push (this.dlg.State);
-			this.dlg.PushState (goTo, false, true, false, restorePath);
-
-
-			if (restoreSelection != null) {
-				this.dlg.RestoreSelection (restoreSelection.FileSystemInfo);
-			}
-
-			return true;
-		}
-
-		internal bool CanBack ()
-		{
-			return this.back.Count > 0;
-		}
-
-		internal bool Forward ()
-		{
-			if (this.forward.Count > 0) {
-
-				this.dlg.PushState (this.forward.Pop ().Directory, true, true, false);
-				return true;
-			}
-
-			return false;
-		}
-
-		internal bool Up ()
-		{
-			var parent = this.dlg.State?.Directory.Parent;
-			if (parent != null) {
-
-				this.back.Push (new FileDialogState (parent, this.dlg));
-				this.dlg.PushState (parent, false);
-				return true;
-			}
-
-			return false;
-		}
-
-		internal bool CanUp ()
-		{
-			return this.dlg.State?.Directory.Parent != null;
-		}
-
-		internal void Push (FileDialogState state, bool clearForward)
-		{
-			if (state == null) {
-				return;
-			}
-
-			// if changing to a new directory push onto the Back history
-			if (this.back.Count == 0 || this.back.Peek ().Directory.FullName != state.Directory.FullName) {
-
-				this.back.Push (state);
-				if (clearForward) {
-					this.ClearForward ();
-				}
-			}
-		}
-
-		internal bool CanForward ()
-		{
-			return this.forward.Count > 0;
-		}
-
-		internal void ClearForward ()
-		{
-			this.forward.Clear ();
-		}
-	}
-}
+using System.IO.Abstractions;
+
+namespace Terminal.Gui;
+
+internal class FileDialogHistory
+{
+    private readonly Stack<FileDialogState> back = new ();
+    private readonly FileDialog dlg;
+    private readonly Stack<FileDialogState> forward = new ();
+    public FileDialogHistory (FileDialog dlg) { this.dlg = dlg; }
+
+    public bool Back ()
+    {
+        IDirectoryInfo goTo = null;
+        FileSystemInfoStats restoreSelection = null;
+        string restorePath = null;
+
+        if (CanBack ())
+        {
+            FileDialogState backTo = back.Pop ();
+            goTo = backTo.Directory;
+            restoreSelection = backTo.Selected;
+            restorePath = backTo.Path;
+        }
+        else if (CanUp ())
+        {
+            goTo = dlg.State?.Directory.Parent;
+        }
+
+        // nowhere to go
+        if (goTo is null)
+        {
+            return false;
+        }
+
+        forward.Push (dlg.State);
+        dlg.PushState (goTo, false, true, false, restorePath);
+
+        if (restoreSelection is { })
+        {
+            dlg.RestoreSelection (restoreSelection.FileSystemInfo);
+        }
+
+        return true;
+    }
+
+    internal bool CanBack () { return back.Count > 0; }
+    internal bool CanForward () { return forward.Count > 0; }
+    internal bool CanUp () { return dlg.State?.Directory.Parent != null; }
+    internal void ClearForward () { forward.Clear (); }
+
+    internal bool Forward ()
+    {
+        if (forward.Count > 0)
+        {
+            dlg.PushState (forward.Pop ().Directory, true, true, false);
+
+            return true;
+        }
+
+        return false;
+    }
+
+    internal void Push (FileDialogState state, bool clearForward)
+    {
+        if (state is null)
+        {
+            return;
+        }
+
+        // if changing to a new directory push onto the Back history
+        if (back.Count == 0 || back.Peek ().Directory.FullName != state.Directory.FullName)
+        {
+            back.Push (state);
+
+            if (clearForward)
+            {
+                ClearForward ();
+            }
+        }
+    }
+
+    internal bool Up ()
+    {
+        IDirectoryInfo parent = dlg.State?.Directory.Parent;
+
+        if (parent is { })
+        {
+            back.Push (new FileDialogState (parent, dlg));
+            dlg.PushState (parent, false);
+
+            return true;
+        }
+
+        return false;
+    }
+}

+ 90 - 84
Terminal.Gui/FileServices/FileDialogState.cs

@@ -1,84 +1,90 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.IO.Abstractions;
-using System.Linq;
-
-namespace Terminal.Gui {
-
-	internal class FileDialogState {
-
-		public FileSystemInfoStats Selected { get; set; }
-		protected readonly FileDialog Parent;
-
-		/// <summary>
-		/// Gets what was entered in the path text box of the dialog
-		/// when the state was active.
-		/// </summary>
-		public string Path { get; }
-		
-		public FileDialogState (IDirectoryInfo dir, FileDialog parent)
-		{
-			this.Directory = dir;
-			Parent = parent;
-			Path = parent.Path;
-
-			this.RefreshChildren ();
-		}
-
-		public IDirectoryInfo Directory { get; }
-
-		public FileSystemInfoStats [] Children { get; internal set; }
-
-		internal virtual void RefreshChildren ()
-		{
-			var dir = this.Directory;
-			Children = GetChildren (dir).ToArray ();
-		}
-
-		protected virtual IEnumerable<FileSystemInfoStats> GetChildren (IDirectoryInfo dir)
-		{
-			try {
-
-				List<FileSystemInfoStats> children;
-
-				// if directories only
-				if (Parent.OpenMode == OpenMode.Directory) {
-					children = dir.GetDirectories ().Select (e => new FileSystemInfoStats (e, Parent.Style.Culture)).ToList ();
-				} else {
-					children = dir.GetFileSystemInfos ().Select (e => new FileSystemInfoStats (e, Parent.Style.Culture)).ToList ();
-				}
-
-				// if only allowing specific file types
-				if (Parent.AllowedTypes.Any () && Parent.OpenMode == OpenMode.File) {
-
-					children = children.Where (
-						c => c.IsDir ||
-						(c.FileSystemInfo is IFileInfo f && Parent.IsCompatibleWithAllowedExtensions (f)))
-						.ToList ();
-				}
-
-				// if theres a UI filter in place too
-				if (Parent.CurrentFilter != null) {
-					children = children.Where (MatchesApiFilter).ToList ();
-				}
-
-				// allow navigating up as '..'
-				if (dir.Parent != null) {
-					children.Add (new FileSystemInfoStats (dir.Parent, Parent.Style.Culture) { IsParent = true });
-				}
-
-				return children;
-			} catch (Exception) {
-				// Access permissions Exceptions, Dir not exists etc
-				return Enumerable.Empty<FileSystemInfoStats> ();
-			}
-		}
-
-		protected bool MatchesApiFilter (FileSystemInfoStats arg)
-		{
-			return arg.IsDir ||
-			(arg.FileSystemInfo is IFileInfo f && Parent.CurrentFilter.IsAllowed (f.FullName));
-		}
-	}
-}
+using System.IO.Abstractions;
+
+namespace Terminal.Gui;
+
+internal class FileDialogState
+{
+    protected readonly FileDialog Parent;
+
+    public FileDialogState (IDirectoryInfo dir, FileDialog parent)
+    {
+        Directory = dir;
+        Parent = parent;
+        Path = parent.Path;
+
+        RefreshChildren ();
+    }
+
+    public FileSystemInfoStats [] Children { get; internal set; }
+    public IDirectoryInfo Directory { get; }
+
+    /// <summary>Gets what was entered in the path text box of the dialog when the state was active.</summary>
+    public string Path { get; }
+
+    public FileSystemInfoStats Selected { get; set; }
+
+    protected virtual IEnumerable<FileSystemInfoStats> GetChildren (IDirectoryInfo dir)
+    {
+        try
+        {
+            List<FileSystemInfoStats> children;
+
+            // if directories only
+            if (Parent.OpenMode == OpenMode.Directory)
+            {
+                children = dir.GetDirectories ()
+                              .Select (e => new FileSystemInfoStats (e, Parent.Style.Culture))
+                              .ToList ();
+            }
+            else
+            {
+                children = dir.GetFileSystemInfos ()
+                              .Select (e => new FileSystemInfoStats (e, Parent.Style.Culture))
+                              .ToList ();
+            }
+
+            // if only allowing specific file types
+            if (Parent.AllowedTypes.Any () && Parent.OpenMode == OpenMode.File)
+            {
+                children = children.Where (
+                                           c => c.IsDir
+                                                || (c.FileSystemInfo is IFileInfo f
+                                                    && Parent.IsCompatibleWithAllowedExtensions (f))
+                                          )
+                                   .ToList ();
+            }
+
+            // if theres a UI filter in place too
+            if (Parent.CurrentFilter is { })
+            {
+                children = children.Where (MatchesApiFilter).ToList ();
+            }
+
+            // allow navigating up as '..'
+            if (dir.Parent is { })
+            {
+                children.Add (new FileSystemInfoStats (dir.Parent, Parent.Style.Culture) { IsParent = true });
+            }
+
+            return children;
+        }
+        catch (Exception)
+        {
+            // Access permissions Exceptions, Dir not exists etc
+            return Enumerable.Empty<FileSystemInfoStats> ();
+        }
+    }
+
+    protected bool MatchesApiFilter (FileSystemInfoStats arg)
+    {
+        return arg.IsDir
+               || (arg.FileSystemInfo is IFileInfo f
+                   && Parent.CurrentFilter.IsAllowed (f.FullName));
+    }
+
+    internal virtual void RefreshChildren ()
+    {
+        IDirectoryInfo dir = Directory;
+        Children = GetChildren (dir).ToArray ();
+    }
+}

+ 198 - 228
Terminal.Gui/FileServices/FileDialogStyle.cs

@@ -1,231 +1,201 @@
-using System;
-using System.Collections.Generic;
-using System.Data;
-using System.Globalization;
-using System.IO;
+using System.Globalization;
 using System.IO.Abstractions;
-using System.Linq;
 using Terminal.Gui.Resources;
 using static System.Environment;
-using static Terminal.Gui.ConfigurationManager;
-
-namespace Terminal.Gui {
-
-	/// <summary>
-	/// Stores style settings for <see cref="FileDialog"/>.
-	/// </summary>
-	public class FileDialogStyle {
-		readonly IFileSystem _fileSystem;
-
-		/// <summary>
-		/// Gets or sets the default value to use for <see cref="UseColors"/>.
-		/// This can be populated from .tui config files via <see cref="ConfigurationManager"/>
-		/// </summary>
-		[SerializableConfigurationProperty (Scope = typeof (SettingsScope))]
-		public static bool DefaultUseColors { get; set; }
-
-		/// <summary>
-		/// Gets or sets the default value to use for <see cref="UseUnicodeCharacters"/>.
-		/// This can be populated from .tui config files via <see cref="ConfigurationManager"/>
-		/// </summary>
-		[SerializableConfigurationProperty (Scope = typeof (SettingsScope))]
-		public static bool DefaultUseUnicodeCharacters { get; set; }
-
-		/// <summary>
-		/// Gets or Sets a value indicating whether different colors
-		/// should be used for different file types/directories.  Defaults
-		/// to false.
-		/// </summary>
-		public bool UseColors { get; set; } = DefaultUseColors;
-
-		/// <summary>
-		/// Gets or sets the class responsible for determining which symbol
-		/// to use to represent files and directories.
-		/// </summary>
-		public FileSystemIconProvider IconProvider { get; set; } = new FileSystemIconProvider ();
-
-		/// <summary>
-		///	Gets or sets the class thatis responsible for determining which color
-		/// to use to represent files and directories when <see cref="UseColors"/> is
-		/// <see langword="true"/>.
-		/// </summary>
-		public FileSystemColorProvider ColorProvider { get; set; } = new FileSystemColorProvider ();
-
-		/// <summary>
-		/// Gets or sets the culture to use (e.g. for number formatting).
-		/// Defaults to <see cref="CultureInfo.CurrentUICulture"/>.
-		/// </summary>
-		public CultureInfo Culture { get; set; } = CultureInfo.CurrentUICulture;
-
-		/// <summary>
-		/// Gets or sets the header text displayed in the Filename column of the files table.
-		/// </summary>
-		public string FilenameColumnName { get; set; } = Strings.fdFilename;
-
-		/// <summary>
-		/// Gets or sets the header text displayed in the Size column of the files table.
-		/// </summary>
-		public string SizeColumnName { get; set; } = Strings.fdSize;
-
-		/// <summary>
-		/// Gets or sets the header text displayed in the Modified column of the files table.
-		/// </summary>
-		public string ModifiedColumnName { get; set; } = Strings.fdModified;
-
-		/// <summary>
-		/// Gets or sets the header text displayed in the Type column of the files table.
-		/// </summary>
-		public string TypeColumnName { get; set; } = Strings.fdType;
-
-		/// <summary>
-		/// Gets or sets the text displayed in the 'Search' text box when user has not supplied any input yet.
-		/// </summary>
-		public string SearchCaption { get; set; } = Strings.fdSearchCaption;
-
-		/// <summary>
-		/// Gets or sets the text displayed in the 'Path' text box when user has not supplied any input yet.
-		/// </summary>
-		public string PathCaption { get; set; } = Strings.fdPathCaption;
-
-		/// <summary>
-		/// Gets or sets the text on the 'Ok' button.  Typically you may want to change this to
-		/// "Open" or "Save" etc.
-		/// </summary>
-		public string OkButtonText { get; set; } = Strings.btnOk;
-
-		/// <summary>
-		/// Gets or sets the text on the 'Cancel' button.
-		/// </summary>
-		public string CancelButtonText { get; set; } = Strings.btnCancel;
-
-		/// <summary>
-		/// Gets or sets whether to flip the order of the Ok and Cancel buttons. Defaults
-		/// to false (Ok button then Cancel button). Set to true to show Cancel button on
-		/// left then Ok button instead.
-		/// </summary>
-		public bool FlipOkCancelButtonLayoutOrder { get; set; }
-
-		/// <summary>
-		/// Gets or sets error message when user attempts to select a file type that is not one of <see cref="FileDialog.AllowedTypes"/>
-		/// </summary>
-		public string WrongFileTypeFeedback { get; set; } = Strings.fdWrongFileTypeFeedback;
-
-		/// <summary>
-		/// Gets or sets error message when user selects a directory that does not exist and
-		/// <see cref="OpenMode"/> is <see cref="OpenMode.Directory"/> and <see cref="FileDialog.MustExist"/> is <see langword="true"/>.
-		/// </summary>
-		public string DirectoryMustExistFeedback { get; set; } = Strings.fdDirectoryMustExistFeedback;
-
-		/// <summary>
-		/// Gets or sets error message when user <see cref="OpenMode"/> is <see cref="OpenMode.Directory"/>
-		/// and user enters the name of an existing file (File system cannot have a folder with the same name as a file).
-		/// </summary>
-		public string FileAlreadyExistsFeedback { get; set; } = Strings.fdFileAlreadyExistsFeedback;
-
-		/// <summary>
-		/// Gets or sets error message when user selects a file that does not exist and
-		/// <see cref="OpenMode"/> is <see cref="OpenMode.File"/> and <see cref="FileDialog.MustExist"/> is <see langword="true"/>.
-		/// </summary>
-		public string FileMustExistFeedback { get; set; } = Strings.fdFileMustExistFeedback;
-
-		/// <summary>
-		/// Gets or sets error message when user <see cref="OpenMode"/> is <see cref="OpenMode.File"/>
-		/// and user enters the name of an existing directory (File system cannot have a folder with the same name as a file).
-		/// </summary>
-		public string DirectoryAlreadyExistsFeedback { get; set; } = Strings.fdDirectoryAlreadyExistsFeedback;
-
-		/// <summary>
-		/// Gets or sets error message when user selects a file/dir that does not exist and
-		/// <see cref="OpenMode"/> is <see cref="OpenMode.Mixed"/> and <see cref="FileDialog.MustExist"/> is <see langword="true"/>.
-		/// </summary>
-		public string FileOrDirectoryMustExistFeedback { get; set; } = Strings.fdFileOrDirectoryMustExistFeedback;
-
-		/// <summary>
-		/// Gets the style settings for the table of files (in currently selected directory).
-		/// </summary>
-		public TableStyle TableStyle { get; internal set; }
-
-		/// <summary>
-		/// Gets the style settings for the collapse-able directory/places tree
-		/// </summary>
-		public TreeStyle TreeStyle { get; internal set; }
-
-		/// <summary>
-		/// Gets or Sets the method for getting the root tree objects that are displayed in
-		/// the collapse-able tree in the <see cref="FileDialog"/>.  Defaults to all accessible
-		/// <see cref="System.Environment.GetLogicalDrives"/> and unique
-		/// <see cref="Environment.SpecialFolder"/>.
-		/// </summary>
-		/// <remarks>Must be configured before showing the dialog.</remarks>
-		public Func<Dictionary<IDirectoryInfo, string>> TreeRootGetter { get; set; }
-
-		/// <summary>
-		/// Gets or sets whether to use advanced unicode characters which might not be installed
-		/// on all users computers.
-		/// </summary>
-		public bool UseUnicodeCharacters { get; set; } = DefaultUseUnicodeCharacters;
-
-		/// <summary>
-		/// Gets or sets the format to use for date/times in the Modified column.
-		/// Defaults to <see cref="DateTimeFormatInfo.SortableDateTimePattern"/> 
-		/// of the <see cref="CultureInfo.CurrentCulture"/>
-		/// </summary>
-		public string DateFormat { get; set; }
-
-		/// <summary>
-		/// Creates a new instance of the <see cref="FileDialogStyle"/> class.
-		/// </summary>
-		public FileDialogStyle (IFileSystem fileSystem)
-		{
-			_fileSystem = fileSystem;
-			TreeRootGetter = DefaultTreeRootGetter;
-
-			DateFormat = CultureInfo.CurrentCulture.DateTimeFormat.SortableDateTimePattern;
-		}
-
-
-		private Dictionary<IDirectoryInfo, string> DefaultTreeRootGetter ()
-		{
-			var roots = new Dictionary<IDirectoryInfo, string> ();
-			try {
-				foreach (var d in Environment.GetLogicalDrives ()) {
-
-					var dir = _fileSystem.DirectoryInfo.New (d);
-
-					if (!roots.ContainsKey (dir)) {
-						roots.Add (dir, d);
-					}
-				}
-
-			} catch (Exception) {
-				// Cannot get the system disks thats fine
-			}
-
-			try {
-				foreach (var special in Enum.GetValues (typeof (Environment.SpecialFolder)).Cast<SpecialFolder> ()) {
-					try {
-						var path = Environment.GetFolderPath (special);
-
-						if (string.IsNullOrWhiteSpace (path)) {
-							continue;
-						}
-
-						var dir = _fileSystem.DirectoryInfo.New (path);
-
-						if (!roots.ContainsKey (dir) && dir.Exists) {
-							roots.Add (dir, special.ToString ());
-						}
-					} catch (Exception) {
-						// Special file exists but contents are unreadable (permissions?)
-						// skip it anyway
-					}
-				}
-			} catch (Exception) {
-				// Cannot get the special files for this OS oh well
-			}
-
-			return roots;
-		}
-	}
-
-}
+
+namespace Terminal.Gui;
+
+/// <summary>Stores style settings for <see cref="FileDialog"/>.</summary>
+public class FileDialogStyle
+{
+    private readonly IFileSystem _fileSystem;
+
+    /// <summary>Creates a new instance of the <see cref="FileDialogStyle"/> class.</summary>
+    public FileDialogStyle (IFileSystem fileSystem)
+    {
+        _fileSystem = fileSystem;
+        TreeRootGetter = DefaultTreeRootGetter;
+
+        DateFormat = CultureInfo.CurrentCulture.DateTimeFormat.SortableDateTimePattern;
+    }
+
+    /// <summary>Gets or sets the text on the 'Cancel' button.</summary>
+    public string CancelButtonText { get; set; } = Strings.btnCancel;
+
+    /// <summary>
+    ///     Gets or sets the class thatis responsible for determining which color to use to represent files and
+    ///     directories when <see cref="UseColors"/> is <see langword="true"/>.
+    /// </summary>
+    public FileSystemColorProvider ColorProvider { get; set; } = new ();
+
+    /// <summary>
+    ///     Gets or sets the culture to use (e.g. for number formatting). Defaults to
+    ///     <see cref="CultureInfo.CurrentUICulture"/>.
+    /// </summary>
+    public CultureInfo Culture { get; set; } = CultureInfo.CurrentUICulture;
+
+    /// <summary>
+    ///     Gets or sets the format to use for date/times in the Modified column. Defaults to
+    ///     <see cref="DateTimeFormatInfo.SortableDateTimePattern"/> of the <see cref="CultureInfo.CurrentCulture"/>
+    /// </summary>
+    public string DateFormat { get; set; }
+
+    /// <summary>
+    ///     Gets or sets the default value to use for <see cref="UseColors"/>. This can be populated from .tui config
+    ///     files via <see cref="ConfigurationManager"/>
+    /// </summary>
+    [SerializableConfigurationProperty (Scope = typeof (SettingsScope))]
+    public static bool DefaultUseColors { get; set; }
+
+    /// <summary>
+    ///     Gets or sets the default value to use for <see cref="UseUnicodeCharacters"/>. This can be populated from .tui
+    ///     config files via <see cref="ConfigurationManager"/>
+    /// </summary>
+    [SerializableConfigurationProperty (Scope = typeof (SettingsScope))]
+    public static bool DefaultUseUnicodeCharacters { get; set; }
+
+    /// <summary>
+    ///     Gets or sets error message when user <see cref="OpenMode"/> is <see cref="OpenMode.File"/> and user enters the
+    ///     name of an existing directory (File system cannot have a folder with the same name as a file).
+    /// </summary>
+    public string DirectoryAlreadyExistsFeedback { get; set; } = Strings.fdDirectoryAlreadyExistsFeedback;
+
+    /// <summary>
+    ///     Gets or sets error message when user selects a directory that does not exist and <see cref="OpenMode"/> is
+    ///     <see cref="OpenMode.Directory"/> and <see cref="FileDialog.MustExist"/> is <see langword="true"/>.
+    /// </summary>
+    public string DirectoryMustExistFeedback { get; set; } = Strings.fdDirectoryMustExistFeedback;
+
+    /// <summary>
+    ///     Gets or sets error message when user <see cref="OpenMode"/> is <see cref="OpenMode.Directory"/> and user
+    ///     enters the name of an existing file (File system cannot have a folder with the same name as a file).
+    /// </summary>
+    public string FileAlreadyExistsFeedback { get; set; } = Strings.fdFileAlreadyExistsFeedback;
+
+    /// <summary>
+    ///     Gets or sets error message when user selects a file that does not exist and <see cref="OpenMode"/> is
+    ///     <see cref="OpenMode.File"/> and <see cref="FileDialog.MustExist"/> is <see langword="true"/>.
+    /// </summary>
+    public string FileMustExistFeedback { get; set; } = Strings.fdFileMustExistFeedback;
+
+    /// <summary>Gets or sets the header text displayed in the Filename column of the files table.</summary>
+    public string FilenameColumnName { get; set; } = Strings.fdFilename;
+
+    /// <summary>
+    ///     Gets or sets error message when user selects a file/dir that does not exist and <see cref="OpenMode"/> is
+    ///     <see cref="OpenMode.Mixed"/> and <see cref="FileDialog.MustExist"/> is <see langword="true"/>.
+    /// </summary>
+    public string FileOrDirectoryMustExistFeedback { get; set; } = Strings.fdFileOrDirectoryMustExistFeedback;
+
+    /// <summary>
+    ///     Gets or sets whether to flip the order of the Ok and Cancel buttons. Defaults to false (Ok button then Cancel
+    ///     button). Set to true to show Cancel button on left then Ok button instead.
+    /// </summary>
+    public bool FlipOkCancelButtonLayoutOrder { get; set; }
+
+    /// <summary>Gets or sets the class responsible for determining which symbol to use to represent files and directories.</summary>
+    public FileSystemIconProvider IconProvider { get; set; } = new ();
+
+    /// <summary>Gets or sets the header text displayed in the Modified column of the files table.</summary>
+    public string ModifiedColumnName { get; set; } = Strings.fdModified;
+
+    /// <summary>Gets or sets the text on the 'Ok' button.  Typically you may want to change this to "Open" or "Save" etc.</summary>
+    public string OkButtonText { get; set; } = Strings.btnOk;
+
+    /// <summary>Gets or sets the text displayed in the 'Path' text box when user has not supplied any input yet.</summary>
+    public string PathCaption { get; set; } = Strings.fdPathCaption;
+
+    /// <summary>Gets or sets the text displayed in the 'Search' text box when user has not supplied any input yet.</summary>
+    public string SearchCaption { get; set; } = Strings.fdSearchCaption;
+
+    /// <summary>Gets or sets the header text displayed in the Size column of the files table.</summary>
+    public string SizeColumnName { get; set; } = Strings.fdSize;
+
+    /// <summary>Gets the style settings for the table of files (in currently selected directory).</summary>
+    public TableStyle TableStyle { get; internal set; }
+
+    /// <summary>
+    ///     Gets or Sets the method for getting the root tree objects that are displayed in the collapse-able tree in the
+    ///     <see cref="FileDialog"/>.  Defaults to all accessible <see cref="System.Environment.GetLogicalDrives"/> and unique
+    ///     <see cref="Environment.SpecialFolder"/>.
+    /// </summary>
+    /// <remarks>Must be configured before showing the dialog.</remarks>
+    public Func<Dictionary<IDirectoryInfo, string>> TreeRootGetter { get; set; }
+
+    /// <summary>Gets the style settings for the collapse-able directory/places tree</summary>
+    public TreeStyle TreeStyle { get; internal set; }
+
+    /// <summary>Gets or sets the header text displayed in the Type column of the files table.</summary>
+    public string TypeColumnName { get; set; } = Strings.fdType;
+
+    /// <summary>
+    ///     Gets or Sets a value indicating whether different colors should be used for different file types/directories.
+    ///     Defaults to false.
+    /// </summary>
+    public bool UseColors { get; set; } = DefaultUseColors;
+
+    /// <summary>Gets or sets whether to use advanced unicode characters which might not be installed on all users computers.</summary>
+    public bool UseUnicodeCharacters { get; set; } = DefaultUseUnicodeCharacters;
+
+    /// <summary>
+    ///     Gets or sets error message when user attempts to select a file type that is not one of
+    ///     <see cref="FileDialog.AllowedTypes"/>
+    /// </summary>
+    public string WrongFileTypeFeedback { get; set; } = Strings.fdWrongFileTypeFeedback;
+
+    private Dictionary<IDirectoryInfo, string> DefaultTreeRootGetter ()
+    {
+        Dictionary<IDirectoryInfo, string> roots = new ();
+
+        try
+        {
+            foreach (string d in GetLogicalDrives ())
+            {
+                IDirectoryInfo dir = _fileSystem.DirectoryInfo.New (d);
+
+                if (!roots.ContainsKey (dir))
+                {
+                    roots.Add (dir, d);
+                }
+            }
+        }
+        catch (Exception)
+        {
+            // Cannot get the system disks thats fine
+        }
+
+        try
+        {
+            foreach (SpecialFolder special in Enum.GetValues (typeof (SpecialFolder)).Cast<SpecialFolder> ())
+            {
+                try
+                {
+                    string path = GetFolderPath (special);
+
+                    if (string.IsNullOrWhiteSpace (path))
+                    {
+                        continue;
+                    }
+
+                    IDirectoryInfo dir = _fileSystem.DirectoryInfo.New (path);
+
+                    if (!roots.ContainsKey (dir) && dir.Exists)
+                    {
+                        roots.Add (dir, special.ToString ());
+                    }
+                }
+                catch (Exception)
+                {
+                    // Special file exists but contents are unreadable (permissions?)
+                    // skip it anyway
+                }
+            }
+        }
+        catch (Exception)
+        {
+            // Cannot get the special files for this OS oh well
+        }
+
+        return roots;
+    }
+}

+ 107 - 103
Terminal.Gui/FileServices/FileSystemInfoStats.cs

@@ -1,106 +1,110 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
+using System.Globalization;
 using System.IO.Abstractions;
-using System.Linq;
 using Terminal.Gui.Resources;
 
-namespace Terminal.Gui {
-
-	/// <summary>
-	/// Wrapper for <see cref="FileSystemInfo"/> that contains additional information
-	/// (e.g. <see cref="IsParent"/>) and helper methods.
-	/// </summary>
-	internal class FileSystemInfoStats {
-
-		/* ---- Colors used by the ls command line tool ----
-		 *
-		* Blue: Directory
-		* Green: Executable or recognized data file
-		* Cyan (Sky Blue): Symbolic link file
-		* Yellow with black background: Device
-		* Magenta (Pink): Graphic image file
-		* Red: Archive file
-		* Red with black background: Broken link
-		*/
-
-		private const long ByteConversion = 1024;
-
-		private static readonly string [] SizeSuffixes = { "B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB" };
-		private static readonly List<string> ImageExtensions = new List<string> { ".JPG", ".JPEG", ".JPE", ".BMP", ".GIF", ".PNG" };
-		private static readonly List<string> ExecutableExtensions = new List<string> { ".EXE", ".BAT" };
-
-		/// <summary>
-		/// Initializes a new instance of the <see cref="FileSystemInfoStats"/> class.
-		/// </summary>
-		/// <param name="fsi">The directory of path to wrap.</param>
-		/// <param name="culture"></param>
-		public FileSystemInfoStats (IFileSystemInfo fsi, CultureInfo culture)
-		{
-			this.FileSystemInfo = fsi;
-			this.LastWriteTime = fsi.LastWriteTime;
-
-			if (fsi is IFileInfo fi) {
-				this.MachineReadableLength = fi.Length;
-				this.HumanReadableLength = GetHumanReadableFileSize (this.MachineReadableLength, culture);
-				this.Type = fi.Extension;
-			} else {
-				this.HumanReadableLength = string.Empty;
-				this.Type = $"<{Strings.fdDirectory}>";
-				this.IsDir = true;
-			}
-		}
-
-		/// <summary>
-		/// Gets the wrapped <see cref="FileSystemInfo"/> (directory or file).
-		/// </summary>
-		public IFileSystemInfo FileSystemInfo { get; }
-		public string HumanReadableLength { get; }
-		public long MachineReadableLength { get; }
-		public DateTime? LastWriteTime { get; }
-		public string Type { get; }
-
-		/// <summary>
-		/// Gets or Sets a value indicating whether this instance represents
-		/// the parent of the current state (i.e. "..").
-		/// </summary>
-		public bool IsParent { get; internal set; }
-		public string Name => this.IsParent ? ".." : this.FileSystemInfo.Name;
-
-		public bool IsDir { get; }
-
-		public bool IsImage ()
-		{
-			return this.FileSystemInfo is IFileSystemInfo f &&
-				ImageExtensions.Contains (
-					f.Extension,
-					StringComparer.InvariantCultureIgnoreCase);
-		}
-
-		public bool IsExecutable ()
-		{
-			// TODO: handle linux executable status
-			return this.FileSystemInfo is IFileSystemInfo f &&
-				ExecutableExtensions.Contains (
-					f.Extension,
-					StringComparer.InvariantCultureIgnoreCase);
-		}
-
-		private static string GetHumanReadableFileSize (long value, CultureInfo culture)
-		{
-
-			if (value < 0) {
-				return "-" + GetHumanReadableFileSize (-value, culture);
-			}
-
-			if (value == 0) {
-				return "0.0 B";
-			}
-
-			int mag = (int)Math.Log (value, ByteConversion);
-			double adjustedSize = value / Math.Pow (1000, mag);
-			return string.Format (culture.NumberFormat,"{0:n2} {1}", adjustedSize, SizeSuffixes [mag]);
-		}
-	}
-}
+namespace Terminal.Gui;
+
+/// <summary>
+///     Wrapper for <see cref="FileSystemInfo"/> that contains additional information (e.g. <see cref="IsParent"/>)
+///     and helper methods.
+/// </summary>
+internal class FileSystemInfoStats
+{
+    /* ---- Colors used by the ls command line tool ----
+     *
+     * Blue: Directory
+     * Green: Executable or recognized data file
+     * Cyan (Sky Blue): Symbolic link file
+     * Yellow with black background: Device
+     * Magenta (Pink): Graphic image file
+     * Red: Archive file
+     * Red with black background: Broken link
+     */
+    private const long ByteConversion = 1024;
+    private static readonly List<string> ExecutableExtensions = new () { ".EXE", ".BAT" };
+
+    private static readonly List<string> ImageExtensions = new ()
+    {
+        ".JPG",
+        ".JPEG",
+        ".JPE",
+        ".BMP",
+        ".GIF",
+        ".PNG"
+    };
+
+    private static readonly string [] SizeSuffixes = { "B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB" };
+
+    /// <summary>Initializes a new instance of the <see cref="FileSystemInfoStats"/> class.</summary>
+    /// <param name="fsi">The directory of path to wrap.</param>
+    /// <param name="culture"></param>
+    public FileSystemInfoStats (IFileSystemInfo fsi, CultureInfo culture)
+    {
+        FileSystemInfo = fsi;
+        LastWriteTime = fsi.LastWriteTime;
+
+        if (fsi is IFileInfo fi)
+        {
+            MachineReadableLength = fi.Length;
+            HumanReadableLength = GetHumanReadableFileSize (MachineReadableLength, culture);
+            Type = fi.Extension;
+        }
+        else
+        {
+            HumanReadableLength = string.Empty;
+            Type = $"<{Strings.fdDirectory}>";
+            IsDir = true;
+        }
+    }
+
+    /// <summary>Gets the wrapped <see cref="FileSystemInfo"/> (directory or file).</summary>
+    public IFileSystemInfo FileSystemInfo { get; }
+
+    public string HumanReadableLength { get; }
+    public bool IsDir { get; }
+
+    /// <summary>Gets or Sets a value indicating whether this instance represents the parent of the current state (i.e. "..").</summary>
+    public bool IsParent { get; internal set; }
+
+    public DateTime? LastWriteTime { get; }
+    public long MachineReadableLength { get; }
+    public string Name => IsParent ? ".." : FileSystemInfo.Name;
+    public string Type { get; }
+
+    public bool IsExecutable ()
+    {
+        // TODO: handle linux executable status
+        return FileSystemInfo is IFileSystemInfo f
+               && ExecutableExtensions.Contains (
+                                                 f.Extension,
+                                                 StringComparer.InvariantCultureIgnoreCase
+                                                );
+    }
+
+    public bool IsImage ()
+    {
+        return FileSystemInfo is IFileSystemInfo f
+               && ImageExtensions.Contains (
+                                            f.Extension,
+                                            StringComparer.InvariantCultureIgnoreCase
+                                           );
+    }
+
+    private static string GetHumanReadableFileSize (long value, CultureInfo culture)
+    {
+        if (value < 0)
+        {
+            return "-" + GetHumanReadableFileSize (-value, culture);
+        }
+
+        if (value == 0)
+        {
+            return "0.0 B";
+        }
+
+        var mag = (int)Math.Log (value, ByteConversion);
+        double adjustedSize = value / Math.Pow (1000, mag);
+
+        return string.Format (culture.NumberFormat, "{0:n2} {1}", adjustedSize, SizeSuffixes [mag]);
+    }
+}

+ 60 - 80
Terminal.Gui/FileServices/FileSystemTreeBuilder.cs

@@ -1,80 +1,60 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.IO.Abstractions;
-using System.Linq;
-
-namespace Terminal.Gui {
-
-	/// <summary>
-	/// TreeView builder for creating file system based trees.
-	/// </summary>
-	public class FileSystemTreeBuilder : ITreeBuilder<IFileSystemInfo>, IComparer<IFileSystemInfo> {
-
-
-		/// <summary>
-		/// Creates a new instance of the <see cref="FileSystemTreeBuilder"/> class.
-		/// </summary>
-		public FileSystemTreeBuilder ()
-		{
-			Sorter = this;
-		}
-
-		/// <inheritdoc/>
-		public bool SupportsCanExpand => true;
-
-		/// <summary>
-		/// Gets or sets a flag indicating whether to show files as leaf elements
-		/// in the tree. Defaults to true.
-		/// </summary>
-		public bool IncludeFiles { get; } = true;
-
-		/// <summary>
-		/// Gets or sets the order of directory children.  Defaults to <see langword="this"/>.
-		/// </summary>
-		public IComparer<IFileSystemInfo> Sorter { get; set; }
-
-		/// <inheritdoc/>
-		public bool CanExpand (IFileSystemInfo toExpand)
-		{
-			return this.TryGetChildren (toExpand).Any ();
-		}
-
-		/// <inheritdoc/>
-		public IEnumerable<IFileSystemInfo> GetChildren (IFileSystemInfo forObject)
-		{
-			return this.TryGetChildren (forObject).OrderBy(k=>k,Sorter);
-		}
-
-		private IEnumerable<IFileSystemInfo> TryGetChildren (IFileSystemInfo entry)
-		{
-			if (entry is IFileInfo) {
-				return Enumerable.Empty<IFileSystemInfo> ();
-			}
-
-			var dir = (IDirectoryInfo)entry;
-
-			try {
-				return dir.GetFileSystemInfos ().Where (e => IncludeFiles || e is IDirectoryInfo);
-
-			} catch (Exception) {
-
-				return Enumerable.Empty<IFileSystemInfo> ();
-			}
-		}
-
-		/// <inheritdoc/>
-		public int Compare (IFileSystemInfo x, IFileSystemInfo y)
-		{
-			if (x is IDirectoryInfo && y is not IDirectoryInfo) {
-				return -1;
-			}
-
-			if (x is not IDirectoryInfo && y is IDirectoryInfo) {
-				return 1;
-			}
-
-			return x.Name.CompareTo (y.Name);
-		}
-	}
-}
+using System.IO.Abstractions;
+
+namespace Terminal.Gui;
+
+/// <summary>TreeView builder for creating file system based trees.</summary>
+public class FileSystemTreeBuilder : ITreeBuilder<IFileSystemInfo>, IComparer<IFileSystemInfo>
+{
+    /// <summary>Creates a new instance of the <see cref="FileSystemTreeBuilder"/> class.</summary>
+    public FileSystemTreeBuilder () { Sorter = this; }
+
+    /// <summary>Gets or sets a flag indicating whether to show files as leaf elements in the tree. Defaults to true.</summary>
+    public bool IncludeFiles { get; } = true;
+
+    /// <summary>Gets or sets the order of directory children.  Defaults to <see langword="this"/>.</summary>
+    public IComparer<IFileSystemInfo> Sorter { get; set; }
+
+    /// <inheritdoc/>
+    public int Compare (IFileSystemInfo x, IFileSystemInfo y)
+    {
+        if (x is IDirectoryInfo && y is not IDirectoryInfo)
+        {
+            return -1;
+        }
+
+        if (x is not IDirectoryInfo && y is IDirectoryInfo)
+        {
+            return 1;
+        }
+
+        return x.Name.CompareTo (y.Name);
+    }
+
+    /// <inheritdoc/>
+    public bool SupportsCanExpand => true;
+
+    /// <inheritdoc/>
+    public bool CanExpand (IFileSystemInfo toExpand) { return TryGetChildren (toExpand).Any (); }
+
+    /// <inheritdoc/>
+    public IEnumerable<IFileSystemInfo> GetChildren (IFileSystemInfo forObject) { return TryGetChildren (forObject).OrderBy (k => k, Sorter); }
+
+    private IEnumerable<IFileSystemInfo> TryGetChildren (IFileSystemInfo entry)
+    {
+        if (entry is IFileInfo)
+        {
+            return Enumerable.Empty<IFileSystemInfo> ();
+        }
+
+        var dir = (IDirectoryInfo)entry;
+
+        try
+        {
+            return dir.GetFileSystemInfos ().Where (e => IncludeFiles || e is IDirectoryInfo);
+        }
+        catch (Exception)
+        {
+            return Enumerable.Empty<IFileSystemInfo> ();
+        }
+    }
+}

+ 18 - 27
Terminal.Gui/FileServices/FilesSelectedEventArgs.cs

@@ -1,30 +1,21 @@
-using System;
+namespace Terminal.Gui;
 
-namespace Terminal.Gui {
-	/// <summary>
-	/// Event args for the <see cref="FileDialog.FilesSelected"/> event
-	/// </summary>
-	public class FilesSelectedEventArgs : EventArgs {
-		/// <summary>
-		/// Set to true if you want to prevent the selection
-		/// going ahead (this will leave the <see cref="FileDialog"/>
-		/// still showing).
-		/// </summary>
-		public bool Cancel { get; set; }
+/// <summary>Event args for the <see cref="FileDialog.FilesSelected"/> event</summary>
+public class FilesSelectedEventArgs : EventArgs
+{
+    /// <summary>Creates a new instance of the <see cref="FilesSelectedEventArgs"/></summary>
+    /// <param name="dialog"></param>
+    public FilesSelectedEventArgs (FileDialog dialog) { Dialog = dialog; }
 
-		/// <summary>
-		/// The dialog where the choice is being made.  Use <see cref="FileDialog.Path"/>
-		/// and/or <see cref="FileDialog.MultiSelected"/> to evaluate the users choice.
-		/// </summary>
-		public FileDialog Dialog { get; }
+    /// <summary>
+    ///     Set to true if you want to prevent the selection going ahead (this will leave the <see cref="FileDialog"/>
+    ///     still showing).
+    /// </summary>
+    public bool Cancel { get; set; }
 
-		/// <summary>
-		/// Creates a new instance of the <see cref="FilesSelectedEventArgs"/>
-		/// </summary>
-		/// <param name="dialog"></param>
-		public FilesSelectedEventArgs (FileDialog dialog)
-		{
-			Dialog = dialog;
-		}
-	}
-}
+    /// <summary>
+    ///     The dialog where the choice is being made.  Use <see cref="FileDialog.Path"/> and/or
+    ///     <see cref="FileDialog.MultiSelected"/> to evaluate the users choice.
+    /// </summary>
+    public FileDialog Dialog { get; }
+}

+ 32 - 41
Terminal.Gui/FileServices/IFileOperations.cs

@@ -1,45 +1,36 @@
-using System.Collections.Generic;
-using System.IO;
 using System.IO.Abstractions;
 
-namespace Terminal.Gui {
-	/// <summary>
-	/// Interface for defining how to handle file/directory 
-	/// deletion, rename and newing attempts in <see cref="FileDialog"/>.
-	/// </summary>
-	public interface IFileOperations {
-		/// <summary>
-		/// Specifies how to handle file/directory deletion attempts
-		/// in <see cref="FileDialog"/>.
-		/// </summary>
-		/// <param name="toDelete"></param>
-		/// <returns><see langword="true"/> if operation was completed or 
-		/// <see langword="false"/> if cancelled</returns>
-		/// <remarks>Ensure you use a try/catch block with appropriate
-		/// error handling (e.g. showing a <see cref="MessageBox"/></remarks>
-		bool Delete (IEnumerable<IFileSystemInfo> toDelete);
+namespace Terminal.Gui;
 
-		/// <summary>
-		/// Specifies how to handle file/directory rename attempts
-		/// in <see cref="FileDialog"/>.
-		/// </summary>
-		/// <param name="fileSystem"></param>
-		/// <param name="toRename"></param>
-		/// <returns>The new name for the file or null if cancelled</returns>
-		/// <remarks>Ensure you use a try/catch block with appropriate
-		/// error handling (e.g. showing a <see cref="MessageBox"/></remarks>
-		IFileSystemInfo Rename (IFileSystem fileSystem, IFileSystemInfo toRename);
+/// <summary>
+///     Interface for defining how to handle file/directory deletion, rename and newing attempts in
+///     <see cref="FileDialog"/>.
+/// </summary>
+public interface IFileOperations
+{
+    /// <summary>Specifies how to handle file/directory deletion attempts in <see cref="FileDialog"/>.</summary>
+    /// <param name="toDelete"></param>
+    /// <returns><see langword="true"/> if operation was completed or <see langword="false"/> if cancelled</returns>
+    /// <remarks>
+    ///     Ensure you use a try/catch block with appropriate error handling (e.g. showing a <see cref="MessageBox"/>
+    /// </remarks>
+    bool Delete (IEnumerable<IFileSystemInfo> toDelete);
 
-		/// <summary>
-		/// Specifies how to handle 'new directory' operation
-		/// in <see cref="FileDialog"/>.
-		/// </summary>
-		/// <param name="fileSystem"></param>
-		/// <param name="inDirectory">The parent directory in which the new
-		/// directory should be created</param>
-		/// <returns>The newly created directory or null if cancelled.</returns>
-		/// <remarks>Ensure you use a try/catch block with appropriate
-		/// error handling (e.g. showing a <see cref="MessageBox"/></remarks>
-		IFileSystemInfo New (IFileSystem fileSystem, IDirectoryInfo inDirectory);
-	}
-}
+    /// <summary>Specifies how to handle 'new directory' operation in <see cref="FileDialog"/>.</summary>
+    /// <param name="fileSystem"></param>
+    /// <param name="inDirectory">The parent directory in which the new directory should be created</param>
+    /// <returns>The newly created directory or null if cancelled.</returns>
+    /// <remarks>
+    ///     Ensure you use a try/catch block with appropriate error handling (e.g. showing a <see cref="MessageBox"/>
+    /// </remarks>
+    IFileSystemInfo New (IFileSystem fileSystem, IDirectoryInfo inDirectory);
+
+    /// <summary>Specifies how to handle file/directory rename attempts in <see cref="FileDialog"/>.</summary>
+    /// <param name="fileSystem"></param>
+    /// <param name="toRename"></param>
+    /// <returns>The new name for the file or null if cancelled</returns>
+    /// <remarks>
+    ///     Ensure you use a try/catch block with appropriate error handling (e.g. showing a <see cref="MessageBox"/>
+    /// </remarks>
+    IFileSystemInfo Rename (IFileSystem fileSystem, IFileSystemInfo toRename);
+}

+ 10 - 20
Terminal.Gui/FileServices/ISearchMatcher.cs

@@ -1,23 +1,13 @@
-using System.IO;
-using System.IO.Abstractions;
+using System.IO.Abstractions;
 
-namespace Terminal.Gui {
+namespace Terminal.Gui;
 
-	/// <summary>
-	/// Defines whether a given file/directory matches a set of
-	/// search terms.
-	/// </summary>
-	public interface ISearchMatcher {
-		/// <summary>
-		/// Called once for each new search. Defines the string
-		/// the user has provided as search terms.
-		/// </summary>
-		void Initialize (string terms);
+/// <summary>Defines whether a given file/directory matches a set of search terms.</summary>
+public interface ISearchMatcher
+{
+    /// <summary>Called once for each new search. Defines the string the user has provided as search terms.</summary>
+    void Initialize (string terms);
 
-		/// <summary>
-		/// Return true if <paramref name="f"/> is a match to the
-		/// last provided search terms
-		/// </summary>
-		bool IsMatch (IFileSystemInfo f);
-	}
-}
+    /// <summary>Return true if <paramref name="f"/> is a match to the last provided search terms</summary>
+    bool IsMatch (IFileSystemInfo f);
+}

+ 263 - 414
Terminal.Gui/Input/Command.cs

@@ -3,417 +3,266 @@
 
 namespace Terminal.Gui;
 
-/// <summary>
-/// Actions which can be performed by the application or bound to keys in a <see cref="View"/> control.
-/// </summary>
-public enum Command {
-	/// <summary>
-	/// The default command. For <see cref="View"/> this focuses the view.
-	/// </summary>
-	Default,
-
-	/// <summary>
-	/// Moves down one item (cell, line, etc...).
-	/// </summary>
-	LineDown,
-
-	/// <summary>
-	/// Extends the selection down one (cell, line, etc...).
-	/// </summary>
-	LineDownExtend,
-
-	/// <summary>
-	/// Moves down to the last child node of the branch that holds the current selection.
-	/// </summary>
-	LineDownToLastBranch,
-
-	/// <summary>
-	/// Scrolls down one (cell, line, etc...) (without changing the selection).
-	/// </summary>
-	ScrollDown,
-
-	// --------------------------------------------------------------------
-
-	/// <summary>
-	/// Moves up one (cell, line, etc...).
-	/// </summary>
-	LineUp,
-
-	/// <summary>
-	/// Extends the selection up one item (cell, line, etc...).
-	/// </summary>
-	LineUpExtend,
-
-	/// <summary>
-	/// Moves up to the first child node of the branch that holds the current selection.
-	/// </summary>
-	LineUpToFirstBranch,
-
-	/// <summary>
-	/// Scrolls up one item (cell, line, etc...) (without changing the selection).
-	/// </summary>
-	ScrollUp,
-
-	/// <summary>
-	/// Moves the selection left one by the minimum increment supported by the <see cref="View"/> e.g. single character, cell, item etc.
-	/// </summary>
-	Left,
-
-	/// <summary>
-	/// Scrolls one item (cell, character, etc...) to the left
-	/// </summary>
-	ScrollLeft,
-
-	/// <summary>
-	/// Extends the selection left one by the minimum increment supported by the view e.g. single character, cell, item etc.
-	/// </summary>
-	LeftExtend,
-
-	/// <summary>
-	/// Moves the selection right one by the minimum increment supported by the view e.g. single character, cell, item etc.
-	/// </summary>
-	Right,
-
-	/// <summary>
-	/// Scrolls one item (cell, character, etc...) to the right.
-	/// </summary>
-	ScrollRight,
-
-	/// <summary>
-	/// Extends the selection right one by the minimum increment supported by the view e.g. single character, cell, item etc.
-	/// </summary>
-	RightExtend,
-
-	/// <summary>
-	/// Moves the caret to the start of the previous word.
-	/// </summary>
-	WordLeft,
-
-	/// <summary>
-	/// Extends the selection to the start of the previous word.
-	/// </summary>
-	WordLeftExtend,
-
-	/// <summary>
-	/// Moves the caret to the start of the next word.
-	/// </summary>
-	WordRight,
-
-	/// <summary>
-	/// Extends the selection to the start of the next word.
-	/// </summary>
-	WordRightExtend,
-
-	/// <summary>
-	/// Cuts to the clipboard the characters from the current position to the end of the line.
-	/// </summary>
-	CutToEndLine,
-
-	/// <summary>
-	/// Cuts to the clipboard the characters from the current position to the start of the line.
-	/// </summary>
-	CutToStartLine,
-
-	/// <summary>
-	/// Deletes the characters forwards.
-	/// </summary>
-	KillWordForwards,
-
-	/// <summary>
-	/// Deletes the characters backwards.
-	/// </summary>
-	KillWordBackwards,
-
-	/// <summary>
-	/// Toggles overwrite mode such that newly typed text overwrites the text that is
-	/// already there (typically associated with the Insert key).
-	/// </summary>
-	ToggleOverwrite,
-
-	/// <summary>
-	/// Enables overwrite mode such that newly typed text overwrites the text that is
-	/// already there (typically associated with the Insert key).
-	/// </summary>
-	EnableOverwrite,
-
-	/// <summary>
-	/// Disables overwrite mode (<see cref="EnableOverwrite"/>)
-	/// </summary>
-	DisableOverwrite,
-
-	/// <summary>
-	/// Move one page down.
-	/// </summary>
-	PageDown,
-
-	/// <summary>
-	/// Move one page page extending the selection to cover revealed objects/characters.
-	/// </summary>
-	PageDownExtend,
-
-	/// <summary>
-	/// Move one page up.
-	/// </summary>
-	PageUp,
-
-	/// <summary>
-	/// Move one page up extending the selection to cover revealed objects/characters.
-	/// </summary>
-	PageUpExtend,
-
-	/// <summary>
-	/// Moves to the top/home.
-	/// </summary>
-	TopHome,
-
-	/// <summary>
-	/// Extends the selection to the top/home.
-	/// </summary>
-	TopHomeExtend,
-
-	/// <summary>
-	/// Moves to the bottom/end.
-	/// </summary>
-	BottomEnd,
-
-	/// <summary>
-	/// Extends the selection to the bottom/end.
-	/// </summary>
-	BottomEndExtend,
-
-	/// <summary>
-	/// Open the selected item.
-	/// </summary>
-	OpenSelectedItem,
-
-	/// <summary>
-	/// Toggle the checked state.
-	/// </summary>
-	ToggleChecked,
-
-	/// <summary>
-	/// Accepts the current state (e.g. selection, button press etc).
-	/// </summary>
-	Accept,
-
-	/// <summary>
-	/// Toggles the Expanded or collapsed state of a a list or item (with subitems).
-	/// </summary>
-	ToggleExpandCollapse,
-
-	/// <summary>
-	/// Expands a list or item (with subitems).
-	/// </summary>
-	Expand,
-
-	/// <summary>
-	/// Recursively Expands all child items and their child items (if any).
-	/// </summary>
-	ExpandAll,
-
-	/// <summary>
-	/// Collapses a list or item (with subitems).
-	/// </summary>
-	Collapse,
-
-	/// <summary>
-	/// Recursively collapses a list items of their children (if any).
-	/// </summary>
-	CollapseAll,
-
-	/// <summary>
-	/// Cancels an action or any temporary states on the control e.g. expanding
-	/// a combo list.
-	/// </summary>
-	Cancel,
-
-	/// <summary>
-	/// Unix emulation.
-	/// </summary>
-	UnixEmulation,
-
-	/// <summary>
-	/// Deletes the character on the right.
-	/// </summary>
-	DeleteCharRight,
-
-	/// <summary>
-	/// Deletes the character on the left.
-	/// </summary>
-	DeleteCharLeft,
-
-	/// <summary>
-	/// Selects all objects.
-	/// </summary>
-	SelectAll,
-
-	/// <summary>
-	/// Deletes all objects.
-	/// </summary>
-	DeleteAll,
-
-	/// <summary>
-	/// Moves the cursor to the start of line.
-	/// </summary>
-	StartOfLine,
-
-	/// <summary>
-	/// Extends the selection to the start of line.
-	/// </summary>
-	StartOfLineExtend,
-
-	/// <summary>
-	/// Moves the cursor to the end of line.
-	/// </summary>
-	EndOfLine,
-
-	/// <summary>
-	/// Extends the selection to the end of line.
-	/// </summary>
-	EndOfLineExtend,
-
-	/// <summary>
-	/// Moves the cursor to the top of page.
-	/// </summary>
-	StartOfPage,
-
-	/// <summary>
-	/// Moves the cursor to the bottom of page.
-	/// </summary>
-	EndOfPage,
-
-	/// <summary>
-	/// Moves to the left page.
-	/// </summary>
-	PageLeft,
-
-	/// <summary>
-	/// Moves to the right page.
-	/// </summary>
-	PageRight,
-
-	/// <summary>
-	/// Moves to the left begin.
-	/// </summary>
-	LeftHome,
-
-	/// <summary>
-	/// Extends the selection to the left begin.
-	/// </summary>
-	LeftHomeExtend,
-
-	/// <summary>
-	/// Moves to the right end.
-	/// </summary>
-	RightEnd,
-
-	/// <summary>
-	/// Extends the selection to the right end.
-	/// </summary>
-	RightEndExtend,
-
-	/// <summary>
-	/// Undo changes.
-	/// </summary>
-	Undo,
-
-	/// <summary>
-	/// Redo changes.
-	/// </summary>
-	Redo,
-
-	/// <summary>
-	/// Copies the current selection.
-	/// </summary>
-	Copy,
-
-	/// <summary>
-	/// Cuts the current selection.
-	/// </summary>
-	Cut,
-
-	/// <summary>
-	/// Pastes the current selection.
-	/// </summary>
-	Paste,
-
-	/// <summary>
-	/// Quit a <see cref="Toplevel"/>.
-	/// </summary>
-	QuitToplevel,
-
-	/// <summary>
-	/// Suspend a application (Only implemented in <see cref="CursesDriver"/>).
-	/// </summary>
-	Suspend,
-
-	/// <summary>
-	/// Moves focus to the next view.
-	/// </summary>
-	NextView,
-
-	/// <summary>
-	/// Moves focuss to the previous view.
-	/// </summary>
-	PreviousView,
-
-	/// <summary>
-	/// Moves focus to the next view or Toplevel (case of Overlapped).
-	/// </summary>
-	NextViewOrTop,
-
-	/// <summary>
-	/// Moves focus to the next previous or Toplevel (case of Overlapped).
-	/// </summary>
-	PreviousViewOrTop,
-
-	/// <summary>
-	/// Refresh.
-	/// </summary>
-	Refresh,
-
-	/// <summary>
-	/// Toggles the selection.
-	/// </summary>
-	ToggleExtend,
-
-	/// <summary>
-	/// Inserts a new item.
-	/// </summary>
-	NewLine,
-
-	/// <summary>
-	/// Tabs to the next item.
-	/// </summary>
-	Tab,
-
-	/// <summary>
-	/// Tabs back to the previous item.
-	/// </summary>
-	BackTab,
-
-	/// <summary>
-	/// Saves the current document.
-	/// </summary>
-	Save,
-
-	/// <summary>
-	/// Saves the current document with a new name.
-	/// </summary>
-	SaveAs,
-
-	/// <summary>
-	/// Creates a new document.
-	/// </summary>
-	New,
-
-	/// <summary>
-	/// Moves selection to an item (e.g. highlighting a different menu item) without necessarily accepting it.
-	/// </summary>
-	Select,
-
-	/// <summary>
-	/// Shows context about the item (e.g. a context menu).
-	/// </summary>
-	ShowContextMenu
-}
+/// <summary>Actions which can be performed by the application or bound to keys in a <see cref="View"/> control.</summary>
+public enum Command
+{
+    /// <summary>Invoked when the HotKey for the View has been pressed.</summary>
+    HotKey,
+
+    /// <summary>Accepts the current state (e.g. list selection, button press, toggle, etc).</summary>
+    Accept,
+
+    /// <summary>Selects an item (e.g. a list item or menu item) without necessarily accepting it.</summary>
+    Select,
+
+    /// <summary>Moves down one item (cell, line, etc...).</summary>
+    LineDown,
+
+    /// <summary>Extends the selection down one (cell, line, etc...).</summary>
+    LineDownExtend,
+
+    /// <summary>Moves down to the last child node of the branch that holds the current selection.</summary>
+    LineDownToLastBranch,
+
+    /// <summary>Scrolls down one (cell, line, etc...) (without changing the selection).</summary>
+    ScrollDown,
+
+    // --------------------------------------------------------------------
+
+    /// <summary>Moves up one (cell, line, etc...).</summary>
+    LineUp,
+
+    /// <summary>Extends the selection up one item (cell, line, etc...).</summary>
+    LineUpExtend,
+
+    /// <summary>Moves up to the first child node of the branch that holds the current selection.</summary>
+    LineUpToFirstBranch,
+
+    /// <summary>Scrolls up one item (cell, line, etc...) (without changing the selection).</summary>
+    ScrollUp,
+
+    /// <summary>
+    ///     Moves the selection left one by the minimum increment supported by the <see cref="View"/> e.g. single
+    ///     character, cell, item etc.
+    /// </summary>
+    Left,
+
+    /// <summary>Scrolls one item (cell, character, etc...) to the left</summary>
+    ScrollLeft,
+
+    /// <summary>
+    ///     Extends the selection left one by the minimum increment supported by the view e.g. single character, cell,
+    ///     item etc.
+    /// </summary>
+    LeftExtend,
+
+    /// <summary>
+    ///     Moves the selection right one by the minimum increment supported by the view e.g. single character, cell, item
+    ///     etc.
+    /// </summary>
+    Right,
+
+    /// <summary>Scrolls one item (cell, character, etc...) to the right.</summary>
+    ScrollRight,
+
+    /// <summary>
+    ///     Extends the selection right one by the minimum increment supported by the view e.g. single character, cell,
+    ///     item etc.
+    /// </summary>
+    RightExtend,
+
+    /// <summary>Moves the caret to the start of the previous word.</summary>
+    WordLeft,
+
+    /// <summary>Extends the selection to the start of the previous word.</summary>
+    WordLeftExtend,
+
+    /// <summary>Moves the caret to the start of the next word.</summary>
+    WordRight,
+
+    /// <summary>Extends the selection to the start of the next word.</summary>
+    WordRightExtend,
+
+    /// <summary>Cuts to the clipboard the characters from the current position to the end of the line.</summary>
+    CutToEndLine,
+
+    /// <summary>Cuts to the clipboard the characters from the current position to the start of the line.</summary>
+    CutToStartLine,
+
+    /// <summary>Deletes the characters forwards.</summary>
+    KillWordForwards,
+
+    /// <summary>Deletes the characters backwards.</summary>
+    KillWordBackwards,
+
+    /// <summary>
+    ///     Toggles overwrite mode such that newly typed text overwrites the text that is already there (typically
+    ///     associated with the Insert key).
+    /// </summary>
+    ToggleOverwrite,
+
+    /// <summary>
+    ///     Enables overwrite mode such that newly typed text overwrites the text that is already there (typically
+    ///     associated with the Insert key).
+    /// </summary>
+    EnableOverwrite,
+
+    /// <summary>Disables overwrite mode (<see cref="EnableOverwrite"/>)</summary>
+    DisableOverwrite,
+
+    /// <summary>Move one page down.</summary>
+    PageDown,
+
+    /// <summary>Move one page page extending the selection to cover revealed objects/characters.</summary>
+    PageDownExtend,
+
+    /// <summary>Move one page up.</summary>
+    PageUp,
+
+    /// <summary>Move one page up extending the selection to cover revealed objects/characters.</summary>
+    PageUpExtend,
+
+    /// <summary>Moves to the top/home.</summary>
+    TopHome,
+
+    /// <summary>Extends the selection to the top/home.</summary>
+    TopHomeExtend,
+
+    /// <summary>Moves to the bottom/end.</summary>
+    BottomEnd,
+
+    /// <summary>Extends the selection to the bottom/end.</summary>
+    BottomEndExtend,
+
+    /// <summary>Open the selected item.</summary>
+    OpenSelectedItem,
+
+    /// <summary>Toggles the Expanded or collapsed state of a a list or item (with subitems).</summary>
+    ToggleExpandCollapse,
+
+    /// <summary>Expands a list or item (with subitems).</summary>
+    Expand,
+
+    /// <summary>Recursively Expands all child items and their child items (if any).</summary>
+    ExpandAll,
+
+    /// <summary>Collapses a list or item (with subitems).</summary>
+    Collapse,
+
+    /// <summary>Recursively collapses a list items of their children (if any).</summary>
+    CollapseAll,
+
+    /// <summary>Cancels an action or any temporary states on the control e.g. expanding a combo list.</summary>
+    Cancel,
+
+    /// <summary>Unix emulation.</summary>
+    UnixEmulation,
+
+    /// <summary>Deletes the character on the right.</summary>
+    DeleteCharRight,
+
+    /// <summary>Deletes the character on the left.</summary>
+    DeleteCharLeft,
+
+    /// <summary>Selects all objects.</summary>
+    SelectAll,
+
+    /// <summary>Deletes all objects.</summary>
+    DeleteAll,
+
+    /// <summary>Moves the cursor to the start of line.</summary>
+    StartOfLine,
+
+    /// <summary>Extends the selection to the start of line.</summary>
+    StartOfLineExtend,
+
+    /// <summary>Moves the cursor to the end of line.</summary>
+    EndOfLine,
+
+    /// <summary>Extends the selection to the end of line.</summary>
+    EndOfLineExtend,
+
+    /// <summary>Moves the cursor to the top of page.</summary>
+    StartOfPage,
+
+    /// <summary>Moves the cursor to the bottom of page.</summary>
+    EndOfPage,
+
+    /// <summary>Moves to the left page.</summary>
+    PageLeft,
+
+    /// <summary>Moves to the right page.</summary>
+    PageRight,
+
+    /// <summary>Moves to the left begin.</summary>
+    LeftHome,
+
+    /// <summary>Extends the selection to the left begin.</summary>
+    LeftHomeExtend,
+
+    /// <summary>Moves to the right end.</summary>
+    RightEnd,
+
+    /// <summary>Extends the selection to the right end.</summary>
+    RightEndExtend,
+
+    /// <summary>Undo changes.</summary>
+    Undo,
+
+    /// <summary>Redo changes.</summary>
+    Redo,
+
+    /// <summary>Copies the current selection.</summary>
+    Copy,
+
+    /// <summary>Cuts the current selection.</summary>
+    Cut,
+
+    /// <summary>Pastes the current selection.</summary>
+    Paste,
+
+    /// <summary>Quit a <see cref="Toplevel"/>.</summary>
+    QuitToplevel,
+
+    /// <summary>Suspend a application (Only implemented in <see cref="CursesDriver"/>).</summary>
+    Suspend,
+
+    /// <summary>Moves focus to the next view.</summary>
+    NextView,
+
+    /// <summary>Moves focuss to the previous view.</summary>
+    PreviousView,
+
+    /// <summary>Moves focus to the next view or Toplevel (case of Overlapped).</summary>
+    NextViewOrTop,
+
+    /// <summary>Moves focus to the next previous or Toplevel (case of Overlapped).</summary>
+    PreviousViewOrTop,
+
+    /// <summary>Refresh.</summary>
+    Refresh,
+
+    /// <summary>Toggles the selection.</summary>
+    ToggleExtend,
+
+    /// <summary>Inserts a new item.</summary>
+    NewLine,
+
+    /// <summary>Tabs to the next item.</summary>
+    Tab,
+
+    /// <summary>Tabs back to the previous item.</summary>
+    BackTab,
+
+    /// <summary>Saves the current document.</summary>
+    Save,
+
+    /// <summary>Saves the current document with a new name.</summary>
+    SaveAs,
+
+    /// <summary>Creates a new document.</summary>
+    New,
+
+    /// <summary>Shows context about the item (e.g. a context menu).</summary>
+    ShowContextMenu
+}

+ 14 - 25
Terminal.Gui/Input/GrabMouseEventArgs.cs

@@ -1,29 +1,18 @@
-using System;
+namespace Terminal.Gui;
 
-namespace Terminal.Gui {
-	/// <summary>
-	/// Args <see cref="Application.GrabMouse"/> related events.
-	/// </summary>
-	public class GrabMouseEventArgs : EventArgs {
+/// <summary>Args <see cref="Application.GrabMouse"/> related events.</summary>
+public class GrabMouseEventArgs : EventArgs
+{
+    /// <summary>Creates a new instance of the <see cref="GrabMouseEventArgs"/> class.</summary>
+    /// <param name="view">The view that the event is about.</param>
+    public GrabMouseEventArgs (View view) { View = view; }
 
-		/// <summary>
-		/// Creates a new instance of the <see cref="GrabMouseEventArgs"/> class.
-		/// </summary>
-		/// <param name="view">The view that the event is about.</param>
-		public GrabMouseEventArgs (View view)
-		{
-			View = view;
-		}
+    /// <summary>
+    ///     Flag that allows the cancellation of the event. If set to <see langword="true"/> in the event handler, the
+    ///     event will be canceled.
+    /// </summary>
+    public bool Cancel { get; set; }
 
-		/// <summary>
-		/// Gets the view that grabbed the mouse.
-		/// </summary>
-		public View View { get; }
-
-		/// <summary>
-		/// Flag that allows the cancellation of the event. If set to <see langword="true"/> in the
-		/// event handler, the event will be canceled.
-		/// </summary>
-		public bool Cancel { get; set; }
-	}
+    /// <summary>Gets the view that grabbed the mouse.</summary>
+    public View View { get; }
 }

+ 966 - 1044
Terminal.Gui/Input/Key.cs

@@ -1,1052 +1,974 @@
-using System;
-using System.Collections.Generic;
 using System.Diagnostics.CodeAnalysis;
-using System.Linq;
-using System.Text;
-using System.Text.Json.Serialization;
-using static System.Runtime.CompilerServices.RuntimeHelpers;
+using System.Globalization;
 
 namespace Terminal.Gui;
 
 /// <summary>
-/// Provides an abstraction for common keyboard operations and state. Used for processing keyboard input and raising keyboard events.
+///     Provides an abstraction for common keyboard operations and state. Used for processing keyboard input and
+///     raising keyboard events.
 /// </summary>
 /// <remarks>
-/// <para>
-/// This class provides a high-level abstraction with helper methods and properties for common keyboard operations. Use this class
-/// instead of the <see cref="Terminal.Gui.KeyCode"/> enumeration for keyboard input whenever possible.
-/// </para>
-/// <para>
-/// 
-/// </para>
-/// <para>
-/// The default value for <see cref="Key"/> is <see cref="KeyCode.Null"/> and can be tested using <see cref="Key.Empty"/>.
-/// </para>
-/// <para>
-/// <list type="table">
-///	<listheader>
-///	<term>Concept</term><description>Definition</description>
-///	</listheader>
-///	<item>
-///	<term>Testing Shift State</term>
-///	<description>
-///	The <c>Is</c> properties (<see cref="IsShift"/>,<see cref="IsCtrl"/>, <see cref="IsAlt"/>) test for shift state; whether the key press was modified by a shift key.
-///	</description>
-///	</item>
-///	<item>
-/// 	<term>Adding Shift State</term>
-///	<description>
-///	The <c>With</c> properties (<see cref="WithShift"/>,<see cref="WithCtrl"/>, <see cref="WithAlt"/>) return a copy of the Key with the shift modifier applied. This
-///     is useful for specifying a key that requires a shift modifier (e.g. <c>var ControlAltDelete = new Key(Key.Delete).WithAlt.WithDel;</c>.
-///	</description>
-///	</item>
-///	<item>
-/// 	<term>Removing Shift State</term>
-///	<description>
-///	The <c>No</c> properties (<see cref="NoShift"/>,<see cref="NoCtrl"/>, <see cref="NoAlt"/>) return a copy of the Key with the shift modifier removed. This
-///     is useful for specifying a key that does not require a shift modifier (e.g. <c>var ControlDelete = ControlAltDelete.NoCtrl;</c>.
-///	</description>
-///	</item>
-///	<item>
-///	<term>Encoding of A..Z</term>
-///	<description>
-///	Lowercase alpha keys are encoded (in <see cref="Key.KeyCode"/>) as values between 65 and 90 corresponding to
-///	the un-shifted A to Z keys on a keyboard. Properties are provided for these (e.g. <see cref="Key.A"/>, <see cref="Key.B"/>, etc.).
-///	Even though the encoded values are the same as the ASCII values for uppercase characters, these enum values represent *lowercase*, un-shifted characters.
-///	</description>
-///	</item>
-///	<item>
-///     <term>Persistence as strings</term>
-///	<description>
-///     Keys are persisted as <c>"[Modifiers]+[Key]</c>. For example <c>new Key(Key.Delete).WithAlt.WithDel</c> is persisted as <c>"Ctrl+Alt+Delete"</c>. See <see cref="ToString()"/>
-///     and <see cref="TryParse(string, out Terminal.Gui.Key)"/> for more information.
-///	</description>
-///	</item>
-/// </list>
-/// </para>
+///     <para>
+///         This class provides a high-level abstraction with helper methods and properties for common keyboard
+///         operations. Use this class instead of the <see cref="Terminal.Gui.KeyCode"/> enumeration for keyboard input
+///         whenever possible.
+///     </para>
+///     <para></para>
+///     <para>
+///         The default value for <see cref="Key"/> is <see cref="KeyCode.Null"/> and can be tested using
+///         <see cref="Key.Empty"/>.
+///     </para>
+///     <para>
+///         <list type="table">
+///             <listheader>
+///                 <term>Concept</term><description>Definition</description>
+///             </listheader>
+///             <item>
+///                 <term>Testing Shift State</term>
+///                 <description>
+///                     The <c>Is</c> properties (<see cref="IsShift"/>,<see cref="IsCtrl"/>, <see cref="IsAlt"/>)
+///                     test for shift state; whether the key press was modified by a shift key.
+///                 </description>
+///             </item>
+///             <item>
+///                 <term>Adding Shift State</term>
+///                 <description>
+///                     The <c>With</c> properties (<see cref="WithShift"/>,<see cref="WithCtrl"/>,
+///                     <see cref="WithAlt"/>) return a copy of the Key with the shift modifier applied. This is useful for
+///                     specifying a key that requires a shift modifier (e.g.
+///                     <c>var ControlAltDelete = new Key(Key.Delete).WithAlt.WithDel;</c>.
+///                 </description>
+///             </item>
+///             <item>
+///                 <term>Removing Shift State</term>
+///                 <description>
+///                     The <c>No</c> properties (<see cref="NoShift"/>,<see cref="NoCtrl"/>, <see cref="NoAlt"/>)
+///                     return a copy of the Key with the shift modifier removed. This is useful for specifying a key that
+///                     does not require a shift modifier (e.g. <c>var ControlDelete = ControlAltDelete.NoCtrl;</c>.
+///                 </description>
+///             </item>
+///             <item>
+///                 <term>Encoding of A..Z</term>
+///                 <description>
+///                     Lowercase alpha keys are encoded (in <see cref="Key.KeyCode"/>) as values between 65 and
+///                     90 corresponding to the un-shifted A to Z keys on a keyboard. Properties are provided for these
+///                     (e.g. <see cref="Key.A"/>, <see cref="Key.B"/>, etc.). Even though the encoded values are the same
+///                     as the ASCII values for uppercase characters, these enum values represent *lowercase*, un-shifted
+///                     characters.
+///                 </description>
+///             </item>
+///             <item>
+///                 <term>Persistence as strings</term>
+///                 <description>
+///                     Keys are persisted as <c>"[Modifiers]+[Key]</c>. For example
+///                     <c>new Key(Key.Delete).WithAlt.WithDel</c> is persisted as <c>"Ctrl+Alt+Delete"</c>. See
+///                     <see cref="ToString()"/> and <see cref="TryParse(string, out Terminal.Gui.Key)"/> for more
+///                     information.
+///                 </description>
+///             </item>
+///         </list>
+///     </para>
 /// </remarks>
-public class Key : EventArgs, IEquatable<Key> {
-	/// <summary>
-	/// Constructs a new <see cref="Key"/>
-	/// </summary>
-	public Key () : this (KeyCode.Null) { }
-
-	/// <summary>
-	/// Constructs a new <see cref="Key"/> from the provided Key value
-	/// </summary>
-	/// <param name="k">The key</param>
-	public Key (KeyCode k) => KeyCode = k;
-
-	/// <summary>
-	/// Constructs a new <see cref="Key"/> from a char.
-	/// </summary>
-	/// <remarks>
-	/// <para>
-	/// The key codes for the A..Z keys are encoded as values between 65 and 90 (<see cref="KeyCode.A"/> through <see cref="KeyCode.Z"/>).
-	/// While these are the same as the ASCII values for uppercase characters, they represent *keys*, not characters.
-	/// Therefore, this constructor will store 'A'..'Z' as <see cref="KeyCode.A"/>..<see cref="KeyCode.Z"/> with the
-	/// <see cref="KeyCode.ShiftMask"/> set and will
-	/// store `a`..`z` as <see cref="KeyCode.A"/>..<see cref="KeyCode.Z"/>.
-	/// </para>
-	/// </remarks>
-	/// <param name="ch"></param>
-	public Key (char ch)
-	{
-		switch (ch) {
-		case >= 'A' and <= 'Z':
-			// Upper case A..Z mean "Shift-char" so we need to add Shift
-			KeyCode = (KeyCode)ch | KeyCode.ShiftMask;
-			break;
-		case >= 'a' and <= 'z':
-			// Lower case a..z mean no shift, so we need to store as Key.A...Key.Z
-			KeyCode = (KeyCode)(ch - 32);
-			return;
-		default:
-			KeyCode = (KeyCode)ch;
-			break;
-		}
-	}
-
-	/// <summary>
-	/// Constructs a new Key from a string describing the key.
-	/// See <see cref="TryParse(string, out Terminal.Gui.Key)"/> for information
-	/// on the format of the string.
-	/// </summary>
-	/// <param name="str">The string describing the key.</param>
-	public Key (string str)
-	{
-		var result = Key.TryParse (str, out Key key);
-		if (!result) {
-			throw new ArgumentException (@$"Invalid key string: {str}", nameof (str));
-		}
-		KeyCode = key.KeyCode;
-	}
-
-	/// <summary>
-	/// Indicates if the current Key event has already been processed and the driver should stop notifying any other event subscriber.
-	/// Its important to set this value to true specially when updating any View's layout from inside the subscriber method.
-	/// </summary>
-	public bool Handled { get; set; } = false;
-
-	/// <summary>
-	/// The encoded key value. 
-	/// </summary>
-	/// <para>
-	/// IMPORTANT: Lowercase alpha keys are encoded (in <see cref="Gui.KeyCode"/>) as values between 65 and
-	/// 90 corresponding to the un-shifted A to Z keys on a keyboard. Enum values
-	/// are provided for these (e.g. <see cref="KeyCode.A"/>, <see cref="KeyCode.B"/>, etc.). Even though the values are the same as the ASCII
-	/// values for uppercase characters, these enum values represent *lowercase*, un-shifted characters.
-	/// </para>
-	/// <remarks>
-	/// This property is the backing data for the <see cref="Key"/>. It is a <see cref="KeyCode"/> enum value.
-	/// </remarks>
-	public KeyCode KeyCode { get; init; }
-
-	/// <summary>
-	/// Enables passing the key binding scope with the event. Default is <see cref="KeyBindingScope.Focused"/>.
-	/// </summary>
-	public KeyBindingScope Scope { get; set; } = KeyBindingScope.Focused;
-
-	/// <summary>
-	/// The key value as a Rune. This is the actual value of the key pressed, and is independent of the modifiers. Useful
-	/// for determining if a key represents is a printable character.
-	/// </summary>
-	/// <remarks>
-	/// <para>
-	/// Keys with Ctrl or Alt modifiers will return <see langword="default"/>. 
-	/// </para>
-	/// <para>
-	/// If the key is a letter key (A-Z), the Rune will be the upper or lower case letter depending on whether
-	/// <see cref="KeyCode.ShiftMask"/> is set.
-	/// </para>
-	/// <para>
-	/// If the key is outside of the <see cref="KeyCode.CharMask"/> range, the returned Rune will be <see langword="default"/>.
-	/// </para>
-	/// </remarks>
-	public Rune AsRune => ToRune (KeyCode);
-
-	/// <summary>
-	/// Converts a <see cref="KeyCode"/> to a <see cref="Rune"/>. Useful
-	/// for determining if a key represents is a printable character.
-	/// </summary>
-	/// <remarks>
-	/// <para>
-	/// Keys with Ctrl or Alt modifiers will return <see langword="default"/>. 
-	/// </para>
-	/// <para>
-	/// If the key is a letter key (A-Z), the Rune will be the upper or lower case letter depending on whether
-	/// <see cref="KeyCode.ShiftMask"/> is set.
-	/// </para>
-	/// <para>
-	/// If the key is outside of the <see cref="KeyCode.CharMask"/> range, the returned Rune will be <see langword="default"/>.
-	/// </para>
-	/// </remarks>
-	/// <param name="key"></param>
-	/// <returns>The key converted to a Rune. <see langword="default"/> if conversion is not possible.</returns>
-	public static Rune ToRune (KeyCode key)
-	{
-		if (key is KeyCode.Null or KeyCode.SpecialMask || key.HasFlag (KeyCode.CtrlMask) || key.HasFlag (KeyCode.AltMask)) {
-			return default;
-		}
-
-		// Extract the base key code
-		var baseKey = key;
-		if (baseKey.HasFlag(KeyCode.ShiftMask)) {
-			baseKey &= ~KeyCode.ShiftMask;
-		}
-
-		switch (baseKey) {
-		case >= KeyCode.A and <= KeyCode.Z when !key.HasFlag (KeyCode.ShiftMask):
-			return new Rune ((uint)(baseKey + 32));
-		case >= KeyCode.A and <= KeyCode.Z when key.HasFlag (KeyCode.ShiftMask):
-			return new Rune ((uint)baseKey);
-		case > KeyCode.Null and < KeyCode.A:
-			return new Rune ((uint)baseKey);
-		}
-
-		if (Enum.IsDefined (typeof (KeyCode), baseKey)) {
-			return default;
-		}
-
-		return new Rune ((uint)baseKey);
-	}
-
-	/// <summary>
-	/// Gets a value indicating whether the Shift key was pressed.
-	/// </summary>
-	/// <value><see langword="true"/> if is shift; otherwise, <see langword="false"/>.</value>
-	public bool IsShift => (KeyCode & KeyCode.ShiftMask) != 0;
-
-	/// <summary>
-	/// Gets a value indicating whether the Alt key was pressed (real or synthesized)
-	/// </summary>
-	/// <value><see langword="true"/> if is alternate; otherwise, <see langword="false"/>.</value>
-	public bool IsAlt => (KeyCode & KeyCode.AltMask) != 0;
-
-	/// <summary>
-	/// Gets a value indicating whether the Ctrl key was pressed.
-	/// </summary>
-	/// <value><see langword="true"/> if is ctrl; otherwise, <see langword="false"/>.</value>
-	public bool IsCtrl => (KeyCode & KeyCode.CtrlMask) != 0;
-
-	/// <summary>
-	/// Gets a value indicating whether the key represents a key in the range of <see cref="KeyCode.A"/> to <see cref="KeyCode.Z"/>,
-	/// regardless of the <see cref="KeyCode.ShiftMask"/>. This is useful for testing if a key is based on these keys which are
-	/// special cased.
-	/// </summary>
-	/// <remarks>
-	/// IMPORTANT: Lowercase alpha keys are encoded in <see cref="Key.KeyCode"/> as values between 65 and 90 corresponding to
-	/// the un-shifted A to Z keys on a keyboard. Helper properties are provided these (e.g. <see cref="Key.A"/>, <see cref="Key.B"/>, etc.).
-	/// Even though the values are the same as the ASCII values for uppercase characters, these enum values represent *lowercase*, un-shifted characters.
-	/// </remarks>
-	public bool IsKeyCodeAtoZ => GetIsKeyCodeAtoZ (KeyCode);
-
-	/// <summary>
-	/// Tests if a KeyCode represents a key in the range of <see cref="KeyCode.A"/> to <see cref="KeyCode.Z"/>,
-	/// regardless of the <see cref="KeyCode.ShiftMask"/>. This is useful for testing if a key is based on these keys which are
-	/// special cased.
-	/// </summary>
-	/// <remarks>
-	/// IMPORTANT: Lowercase alpha keys are encoded in <see cref="Key.KeyCode"/> as values between 65 and 90 corresponding to
-	/// the un-shifted A to Z keys on a keyboard. Helper properties are provided these (e.g. <see cref="Key.A"/>, <see cref="Key.B"/>, etc.).
-	/// Even though the values are the same as the ASCII values for uppercase characters, these enum values represent *lowercase*, un-shifted characters.
-	/// </remarks>
-	public static bool GetIsKeyCodeAtoZ (KeyCode keyCode)
-	{
-		if ((keyCode & KeyCode.AltMask) != 0 || (keyCode & KeyCode.CtrlMask) != 0) {
-			return false;
-		}
-
-		if ((keyCode & ~KeyCode.Space & ~KeyCode.ShiftMask) is >= KeyCode.A and <= KeyCode.Z) {
-			return true;
-		}
-
-		return (keyCode & KeyCode.CharMask) is >= KeyCode.A and <= KeyCode.Z;
-	}
-
-	/// <summary>
-	/// Indicates whether the <see cref="Key"/> is valid or not. Invalid keys are <see cref="Key.Empty"/>,
-	/// and keys with only shift modifiers.
-	/// </summary>
-	public bool IsValid => this != Empty && (NoAlt.NoShift.NoCtrl != Empty);
-
-	/// <summary>
-	/// Helper for specifying a shifted <see cref="Key"/>.
-	/// <code>
-	/// var ControlAltDelete = new Key(Key.Delete).WithAlt.WithDel;
-	/// </code>
-	/// </summary>
-	public Key WithShift => new (KeyCode | KeyCode.ShiftMask);
-
-	/// <summary>
-	/// Helper for removing a shift modifier from a <see cref="Key"/>.
-	/// <code>
-	/// var ControlAltDelete = new Key(Key.Delete).WithAlt.WithDel;
-	/// var AltDelete = ControlAltDelete.NoCtrl;
-	/// </code>
-	/// </summary>
-	public Key NoShift => new (KeyCode & ~KeyCode.ShiftMask);
-
-	/// <summary>
-	/// Helper for specifying a shifted <see cref="Key"/>.
-	/// <code>
-	/// var ControlAltDelete = new Key(Key.Delete).WithAlt.WithDel;
-	/// </code>
-	/// </summary>
-	public Key WithCtrl => new (KeyCode | KeyCode.CtrlMask);
-
-	/// <summary>
-	/// Helper for removing a shift modifier from a <see cref="Key"/>.
-	/// <code>
-	/// var ControlAltDelete = new Key(Key.Delete).WithAlt.WithDel;
-	/// var AltDelete = ControlAltDelete.NoCtrl;
-	/// </code>
-	/// </summary>
-	public Key NoCtrl => new (KeyCode & ~KeyCode.CtrlMask);
-
-	/// <summary>
-	/// Helper for specifying a shifted <see cref="Key"/>.
-	/// <code>
-	/// var ControlAltDelete = new Key(Key.Delete).WithAlt.WithDel;
-	/// </code>
-	/// </summary>
-	public Key WithAlt => new (KeyCode | KeyCode.AltMask);
-
-	/// <summary>
-	/// Helper for removing a shift modifier from a <see cref="Key"/>.
-	/// <code>
-	/// var ControlAltDelete = new Key(Key.Delete).WithAlt.WithDel;
-	/// var AltDelete = ControlAltDelete.NoCtrl;
-	/// </code>
-	/// </summary>
-	public Key NoAlt => new (KeyCode & ~KeyCode.AltMask);
-
-	#region Operators
-	/// <summary>
-	/// Explicitly cast a <see cref="Key"/> to a <see cref="Rune"/>. The conversion is lossy because properties
-	/// such as <see cref="Handled"/> are not encoded in <see cref="KeyCode"/>.
-	/// </summary>
-	/// <remarks>
-	/// Uses <see cref="AsRune"/>.
-	/// </remarks>
-	/// <param name="kea"></param>
-	public static explicit operator Rune (Key kea) => kea.AsRune;
-
-	// BUGBUG: (Tig) I do not think this cast operator is really needed. 
-	/// <summary>
-	/// Explicitly cast <see cref="Key"/> to a <see langword="uint"/>. The conversion is lossy because properties
-	/// such as <see cref="Handled"/> are not encoded in <see cref="KeyCode"/>.
-	/// </summary>
-	/// <param name="kea"></param>
-	public static explicit operator uint (Key kea) => (uint)kea.KeyCode;
-
-	/// <summary>
-	/// Explicitly cast <see cref="Key"/> to a <see cref="KeyCode"/>. The conversion is lossy because properties
-	/// such as <see cref="Handled"/> are not encoded in <see cref="KeyCode"/>.
-	/// </summary>
-	/// <param name="key"></param>
-	public static explicit operator KeyCode (Key key) => key.KeyCode;
-
-	/// <summary>
-	/// Cast <see cref="KeyCode"/> to a <see cref="Key"/>. 
-	/// </summary>
-	/// <param name="keyCode"></param>
-	public static implicit operator Key (KeyCode keyCode) => new Key (keyCode);
-
-	/// <summary>
-	/// Cast <see langword="char"/> to a <see cref="Key"/>. 
-	/// </summary>
-	/// <remarks>
-	/// See <see cref="Key(char)"/> for more information.
-	/// </remarks>
-	/// <param name="ch"></param>
-	public static implicit operator Key (char ch) => new Key (ch);
-
-	/// <summary>
-	/// Cast <see langword="string"/> to a <see cref="Key"/>. 
-	/// </summary>
-	/// <remarks>
-	/// See <see cref="Key(string)"/> for more information.
-	/// </remarks>
-	/// <param name="str"></param>
-	public static implicit operator Key (string str) => new Key (str);
-
-	/// <summary>
-	/// Cast a <see cref="Key"/> to a <see langword="string"/>. 
-	/// </summary>
-	/// <remarks>
-	/// See <see cref="Key(string)"/> for more information.
-	/// </remarks>
-	/// <param name="key"></param>
-	public static implicit operator string (Key key) => key.ToString ();
-
-	/// <inheritdoc/>
-	public override bool Equals (object obj) => obj is Key k && k.KeyCode == KeyCode;
-
-	bool IEquatable<Key>.Equals (Key other) => Equals ((object)other);
-
-	/// <inheritdoc/>
-	public override int GetHashCode () => (int)KeyCode;
-
-	/// <summary>
-	/// Compares two <see cref="Key"/>s for equality.
-	/// </summary>
-	/// <param name="a"></param>
-	/// <param name="b"></param>
-	/// <returns></returns>
-	public static bool operator == (Key a, Key b) => a?.KeyCode == b?.KeyCode;
-
-	/// <summary>
-	/// Compares two <see cref="Key"/>s for not equality.
-	/// </summary>
-	/// <param name="a"></param>
-	/// <param name="b"></param>
-	/// <returns></returns>
-	public static bool operator != (Key a, Key b) => a?.KeyCode != b?.KeyCode;
-
-	/// <summary>
-	/// Compares two <see cref="Key"/>s for less-than.
-	/// </summary>
-	/// <param name="a"></param>
-	/// <param name="b"></param>
-	/// <returns></returns>
-	public static bool operator < (Key a, Key b) => a?.KeyCode < b?.KeyCode;
-
-	/// <summary>
-	/// Compares two <see cref="Key"/>s for greater-than.
-	/// </summary>
-	/// <param name="a"></param>
-	/// <param name="b"></param>
-	/// <returns></returns>
-	public static bool operator > (Key a, Key b) => a?.KeyCode > b?.KeyCode;
-
-	/// <summary>
-	/// Compares two <see cref="Key"/>s for greater-than-or-equal-to.
-	/// </summary>
-	/// <param name="a"></param>
-	/// <param name="b"></param>
-	/// <returns></returns>
-	public static bool operator <= (Key a, Key b) => a?.KeyCode <= b?.KeyCode;
-
-	/// <summary>
-	/// Compares two <see cref="Key"/>s for greater-than-or-equal-to.
-	/// </summary>
-	/// <param name="a"></param>
-	/// <param name="b"></param>
-	/// <returns></returns>
-	public static bool operator >= (Key a, Key b) => a?.KeyCode >= b?.KeyCode;
-	#endregion Operators
-
-	#region String conversion
-	/// <summary>
-	/// Pretty prints the KeyEvent
-	/// </summary>
-	/// <returns></returns>
-	public override string ToString () => ToString (KeyCode, (Rune)'+');
-
-	static string GetKeyString (KeyCode key)
-	{
-		if (key is KeyCode.Null or KeyCode.SpecialMask) {
-			return string.Empty;
-		}
-		// Extract the base key (removing modifier flags)
-		var baseKey = key & ~KeyCode.CtrlMask & ~KeyCode.AltMask & ~KeyCode.ShiftMask;
-
-		if (!key.HasFlag (KeyCode.ShiftMask) && baseKey is >= KeyCode.A and <= KeyCode.Z) {
-			return ((Rune)(uint)(key + 32)).ToString ();
-		}
-
-		if (key is > KeyCode.Space and < KeyCode.A) {
-			return ((Rune)(uint)key).ToString ();
-		}
-
-		string keyName = Enum.GetName (typeof (KeyCode), key);
-		return !string.IsNullOrEmpty (keyName) ? keyName : ((Rune)(uint)key).ToString ();
-	}
-
-
-	/// <summary>
-	/// Formats a <see cref="KeyCode"/> as a string using the default separator of '+'
-	/// </summary>
-	/// <param name="key">The key to format.</param>
-	/// <returns>The formatted string. If the key is a printable character, it will be returned as a string. Otherwise, the key name will be returned.</returns>
-	public static string ToString (KeyCode key) => ToString (key, (Rune)'+');
-
-	/// <summary>
-	/// Formats a <see cref="KeyCode"/> as a string.
-	/// </summary>
-	/// <param name="key">The key to format.</param>
-	/// <param name="separator">The character to use as a separator between modifier keys and and the key itself.</param>
-	/// <returns>The formatted string. If the key is a printable character, it will be returned as a string. Otherwise, the key name will be returned.</returns>
-	public static string ToString (KeyCode key, Rune separator)
-	{
-		if (key is KeyCode.Null) {
-			// Same as Key.IsValid
-			return @"Null";
-		}
-
-		var sb = new StringBuilder ();
-
-		// Extract the base key (removing modifier flags)
-		var baseKey = key & ~KeyCode.CtrlMask & ~KeyCode.AltMask & ~KeyCode.ShiftMask;
-
-		// Extract and handle modifiers
-		bool hasModifiers = false;
-		if ((key & KeyCode.CtrlMask) != 0) {
-			sb.Append ($"Ctrl{separator}");
-			hasModifiers = true;
-		}
-		if ((key & KeyCode.AltMask) != 0) {
-			sb.Append ($"Alt{separator}");
-			hasModifiers = true;
-		}
-		if ((key & KeyCode.ShiftMask) != 0 && !GetIsKeyCodeAtoZ (key)) {
-			sb.Append ($"Shift{separator}");
-			hasModifiers = true;
-		}
-
-		// Handle special cases and modifiers on their own
-		if (key != KeyCode.SpecialMask && (baseKey != KeyCode.Null || hasModifiers)) {
-			if ((key & KeyCode.SpecialMask) != 0 && (baseKey & ~KeyCode.Space) is >= KeyCode.A and <= KeyCode.Z) {
-				sb.Append (baseKey & ~KeyCode.Space);
-			} else {
-				// Append the actual key name
-				sb.Append (GetKeyString (baseKey));
-			}
-		}
-
-		return TrimEndSeparator (sb.ToString (), separator);
-	}
-
-	static string TrimEndSeparator (string input, Rune separator)
-	{
-		// Trim the trailing separator (+). Unless there are two separators at the end.
-		// "+" (don't trim)
-		// "Ctrl+" (trim)
-		// "Ctrl++" (trim)
-
-		if (input.Length > 1 && new Rune(input [^1]) == separator && new Rune(input [^2]) != separator) {
-			return input [..^1];
-		}
-		return input;
-	}
-
-	static readonly Dictionary<string, KeyCode> _modifierDict = new (comparer: StringComparer.InvariantCultureIgnoreCase) {
-		{ "Shift", KeyCode.ShiftMask },
-		{ "Ctrl", KeyCode.CtrlMask },
-		{ "Alt", KeyCode.AltMask }
-	};
-
-	/// <summary>
-	/// Converts the provided string to a new <see cref="Key"/> instance.
-	/// </summary>
-	/// <param name="text">The text to analyze. Formats supported are
-	/// "Ctrl+X", "Alt+X", "Shift+X", "Ctrl+Alt+X", "Ctrl+Shift+X", "Alt+Shift+X", "Ctrl+Alt+Shift+X", and "X".
-	/// </param>
-	/// <param name="key">The parsed value.</param>
-	/// <returns>A boolean value indicating whether parsing was successful.</returns>
-	/// <remarks>
-	/// </remarks>
-	public static bool TryParse (string text, [NotNullWhen (true)] out Key key)
-	{
-		if (string.IsNullOrEmpty (text)) {
-			key = new Key (KeyCode.Null);
-			return true;
-		}
-
-		key = null;
-
-		// Split the string into parts
-		string [] parts = text.Split ('+', '-');
-
-		if (parts.Length is 0 or > 4 || parts.Any (string.IsNullOrEmpty)) {
-			return false;
-		}
-
-		// if it's just a shift key
-		if (parts.Length == 1) {
-			switch (parts [0]) {
-			case "Ctrl":
-				key = new Key (KeyCode.CtrlMask);
-				return true;
-			case "Alt":
-				key = new Key (KeyCode.AltMask);
-				return true;
-			case "Shift":
-				key = new Key (KeyCode.ShiftMask);
-				return true;
-			}
-		}
-
-		var modifiers = KeyCode.Null;
-		for (int index = 0; index < parts.Length; index++) {
-			if (_modifierDict.TryGetValue (parts [index].ToLowerInvariant (), out var modifier)) {
-				modifiers |= modifier;
-				parts [index] = string.Empty; // eat it
-			}
-		}
-
-		// we now have the modifiers
-
-		string partNotFound = parts.FirstOrDefault (p => !string.IsNullOrEmpty (p), string.Empty);
-		var parsedKeyCode = KeyCode.Null;
-		int parsedInt = 0;
-		if (partNotFound.Length == 1) {
-			var keyCode = (KeyCode)partNotFound [0];
-			// if it's a single digit int, treat it as such
-			if (int.TryParse (partNotFound,
-				System.Globalization.NumberStyles.Integer,
-				System.Globalization.CultureInfo.InvariantCulture,
-				out parsedInt)) {
-				keyCode = (KeyCode)((int)KeyCode.D0 + parsedInt);
-			} else if (Enum.TryParse (partNotFound, false, out parsedKeyCode)) {
-				if (parsedKeyCode != KeyCode.Null) {
-					if (parsedKeyCode is >= KeyCode.A and <= KeyCode.Z && modifiers == 0) {
-						key = new Key (parsedKeyCode | KeyCode.ShiftMask);
-						return true;
-					}
-					key = new Key ((KeyCode)parsedKeyCode | modifiers);
-					return true;
-				}
-			}
-			key = new Key (keyCode | modifiers);
-			return true;
-		}
-
-		if (Enum.TryParse (partNotFound, true, out parsedKeyCode)) {
-			if (parsedKeyCode != KeyCode.Null) {
-				if (parsedKeyCode is >= KeyCode.A and <= KeyCode.Z && modifiers == 0) {
-					key = new Key (parsedKeyCode | KeyCode.ShiftMask);
-					return true;
-				}
-				key = new Key (parsedKeyCode | modifiers);
-				return true;
-			}
-		}
-
-		// if it's a number int, treat it as a unicode value
-		if (int.TryParse (partNotFound,
-			System.Globalization.NumberStyles.Number,
-			System.Globalization.CultureInfo.InvariantCulture, out parsedInt)) {
-			if (!Rune.IsValid (parsedInt)) {
-				return false;
-			}
-			if ((KeyCode)parsedInt is >= KeyCode.A and <= KeyCode.Z && modifiers == 0) {
-				key = new Key ((KeyCode)parsedInt | KeyCode.ShiftMask);
-				return true;
-			}
-			key = new Key ((KeyCode)parsedInt);
-			return true;
-		}
-
-		if (!Enum.TryParse (partNotFound, true, out parsedKeyCode)) {
-			return false;
-		}
-
-		if (GetIsKeyCodeAtoZ (parsedKeyCode)) {
-			key = new Key (parsedKeyCode | modifiers & ~KeyCode.Space);
-			return true;
-		}
-
-		return false;
-	}
-	#endregion
-
-
-	#region Standard Key Definitions
-	/// <summary>
-	/// An uninitialized The <see cref="Key"/> object.
-	/// </summary>
-	public new static Key Empty => new ();
-
-	/// <summary>
-	/// The <see cref="Key"/> object for the Backspace key.
-	/// </summary>
-	public static Key Backspace => new (KeyCode.Backspace);
-
-	/// <summary>
-	/// The <see cref="Key"/> object for the tab key (forwards tab key).
-	/// </summary>
-	public static Key Tab => new (KeyCode.Tab);
-
-	/// <summary>
-	/// The <see cref="Key"/> object for the return key.
-	/// </summary>
-	public static Key Enter => new (KeyCode.Enter);
-
-	/// <summary>
-	/// The <see cref="Key"/> object for the clear key.
-	/// </summary>
-	public static Key Clear => new (KeyCode.Clear);
-
-	/// <summary>
-	/// The <see cref="Key"/> object for the Escape key.
-	/// </summary>
-	public static Key Esc => new (KeyCode.Esc);
-
-	/// <summary>
-	/// The <see cref="Key"/> object for the Space bar key.
-	/// </summary>
-	public static Key Space => new (KeyCode.Space);
-
-	/// <summary>
-	/// The <see cref="Key"/> object for 0 key.
-	/// </summary>
-	public static Key D0 => new (KeyCode.D0);
-
-	/// <summary>
-	/// The <see cref="Key"/> object for 1 key.
-	/// </summary>
-	public static Key D1 => new (KeyCode.D1);
-
-	/// <summary>
-	/// The <see cref="Key"/> object for 2 key.
-	/// </summary>
-	public static Key D2 => new (KeyCode.D2);
-
-	/// <summary>
-	/// The <see cref="Key"/> object for 3 key.
-	/// </summary>
-	public static Key D3 => new (KeyCode.D3);
-
-	/// <summary>
-	/// The <see cref="Key"/> object for 4 key.
-	/// </summary>
-	public static Key D4 => new (KeyCode.D4);
-
-	/// <summary>
-	/// The <see cref="Key"/> object for 5 key.
-	/// </summary>
-	public static Key D5 => new (KeyCode.D5);
-
-	/// <summary>
-	/// The <see cref="Key"/> object for 6 key.
-	/// </summary>
-	public static Key D6 => new (KeyCode.D6);
-
-	/// <summary>
-	/// The <see cref="Key"/> object for 7 key.
-	/// </summary>
-	public static Key D7 => new (KeyCode.D7);
-
-	/// <summary>
-	/// The <see cref="Key"/> object for 8 key.
-	/// </summary>
-	public static Key D8 => new (KeyCode.D8);
-
-	/// <summary>
-	/// The <see cref="Key"/> object for 9 key.
-	/// </summary>
-	public static Key D9 => new (KeyCode.D9);
-
-	/// <summary>
-	/// The <see cref="Key"/> object for the A key (un-shifted). Use <c>Key.A.WithShift</c> for uppercase 'A'.
-	/// </summary>
-	public static Key A => new (KeyCode.A);
-
-	/// <summary>
-	/// The <see cref="Key"/> object for the B key (un-shifted). Use <c>Key.B.WithShift</c> for uppercase 'B'.
-	/// </summary>
-	public static Key B => new (KeyCode.B);
-
-	/// <summary>
-	/// The <see cref="Key"/> object for the C key (un-shifted). Use <c>Key.C.WithShift</c> for uppercase 'C'.
-	/// </summary>
-	public static Key C => new (KeyCode.C);
-
-	/// <summary>
-	/// The <see cref="Key"/> object for the D key (un-shifted). Use <c>Key.D.WithShift</c> for uppercase 'D'.
-	/// </summary>
-	public static Key D => new (KeyCode.D);
-
-	/// <summary>
-	/// The <see cref="Key"/> object for the E key (un-shifted). Use <c>Key.E.WithShift</c> for uppercase 'E'.
-	/// </summary>
-	public static Key E => new (KeyCode.E);
-
-	/// <summary>
-	/// The <see cref="Key"/> object for the F key (un-shifted). Use <c>Key.F.WithShift</c> for uppercase 'F'.
-	/// </summary>
-	public static Key F => new (KeyCode.F);
-
-	/// <summary>
-	/// The <see cref="Key"/> object for the G key (un-shifted). Use <c>Key.G.WithShift</c> for uppercase 'G'.
-	/// </summary>
-	public static Key G => new (KeyCode.G);
-
-	/// <summary>
-	/// The <see cref="Key"/> object for the H key (un-shifted). Use <c>Key.H.WithShift</c> for uppercase 'H'.
-	/// </summary>
-	public static Key H => new (KeyCode.H);
-
-	/// <summary>
-	/// The <see cref="Key"/> object for the I key (un-shifted). Use <c>Key.I.WithShift</c> for uppercase 'I'.
-	/// </summary>
-	public static Key I => new (KeyCode.I);
-
-	/// <summary>
-	/// The <see cref="Key"/> object for the J key (un-shifted). Use <c>Key.J.WithShift</c> for uppercase 'J'.
-	/// </summary>
-	public static Key J => new (KeyCode.J);
-
-	/// <summary>
-	/// The <see cref="Key"/> object for the K key (un-shifted). Use <c>Key.K.WithShift</c> for uppercase 'K'.
-	/// </summary>
-	public static Key K => new (KeyCode.K);
-
-	/// <summary>
-	/// The <see cref="Key"/> object for the L key (un-shifted). Use <c>Key.L.WithShift</c> for uppercase 'L'.
-	/// </summary>
-	public static Key L => new (KeyCode.L);
-
-	/// <summary>
-	/// The <see cref="Key"/> object for the M key (un-shifted). Use <c>Key.M.WithShift</c> for uppercase 'M'.
-	/// </summary>
-	public static Key M => new (KeyCode.M);
-
-	/// <summary>
-	/// The <see cref="Key"/> object for the N key (un-shifted). Use <c>Key.N.WithShift</c> for uppercase 'N'.
-	/// </summary>
-	public static Key N => new (KeyCode.N);
-
-	/// <summary>
-	/// The <see cref="Key"/> object for the O key (un-shifted). Use <c>Key.O.WithShift</c> for uppercase 'O'.
-	/// </summary>
-	public static Key O => new (KeyCode.O);
-
-	/// <summary>
-	/// The <see cref="Key"/> object for the P key (un-shifted). Use <c>Key.P.WithShift</c> for uppercase 'P'.
-	/// </summary>
-	public static Key P => new (KeyCode.P);
-
-	/// <summary>
-	/// The <see cref="Key"/> object for the Q key (un-shifted). Use <c>Key.Q.WithShift</c> for uppercase 'Q'.
-	/// </summary>
-	public static Key Q => new (KeyCode.Q);
-
-	/// <summary>
-	/// The <see cref="Key"/> object for the R key (un-shifted). Use <c>Key.R.WithShift</c> for uppercase 'R'.
-	/// </summary>
-	public static Key R => new (KeyCode.R);
-
-	/// <summary>
-	/// The <see cref="Key"/> object for the S key (un-shifted). Use <c>Key.S.WithShift</c> for uppercase 'S'.
-	/// </summary>
-	public static Key S => new (KeyCode.S);
-
-	/// <summary>
-	/// The <see cref="Key"/> object for the T key (un-shifted). Use <c>Key.T.WithShift</c> for uppercase 'T'.
-	/// </summary>
-	public static Key T => new (KeyCode.T);
-
-	/// <summary>
-	/// The <see cref="Key"/> object for the U key (un-shifted). Use <c>Key.U.WithShift</c> for uppercase 'U'.
-	/// </summary>
-	public static Key U => new (KeyCode.U);
-
-	/// <summary>
-	/// The <see cref="Key"/> object for the V key (un-shifted). Use <c>Key.V.WithShift</c> for uppercase 'V'.
-	/// </summary>
-	public static Key V => new (KeyCode.V);
-
-	/// <summary>
-	/// The <see cref="Key"/> object for the W key (un-shifted). Use <c>Key.W.WithShift</c> for uppercase 'W'.
-	/// </summary>
-	public static Key W => new (KeyCode.W);
-
-	/// <summary>
-	/// The <see cref="Key"/> object for the X key (un-shifted). Use <c>Key.X.WithShift</c> for uppercase 'X'.
-	/// </summary>
-	public static Key X => new (KeyCode.X);
-
-	/// <summary>
-	/// The <see cref="Key"/> object for the Y key (un-shifted). Use <c>Key.Y.WithShift</c> for uppercase 'Y'.
-	/// </summary>
-	public static Key Y => new (KeyCode.Y);
-
-	/// <summary>
-	/// The <see cref="Key"/> object for the Z key (un-shifted). Use <c>Key.Z.WithShift</c> for uppercase 'Z'.
-	/// </summary>
-	public static Key Z => new (KeyCode.Z);
-
-	/// <summary>
-	/// The <see cref="Key"/> object for the Delete key.
-	/// </summary>
-	public static Key Delete => new (KeyCode.Delete);
-
-	/// <summary>
-	/// The <see cref="Key"/> object for the Cursor up key.
-	/// </summary>
-	public static Key CursorUp => new (KeyCode.CursorUp);
-
-	/// <summary>
-	/// The <see cref="Key"/> object for Cursor down key.
-	/// </summary>
-	public static Key CursorDown => new (KeyCode.CursorDown);
-
-	/// <summary>
-	/// The <see cref="Key"/> object for Cursor left key.
-	/// </summary>
-	public static Key CursorLeft => new (KeyCode.CursorLeft);
-
-	/// <summary>
-	/// The <see cref="Key"/> object for Cursor right key.
-	/// </summary>
-	public static Key CursorRight => new (KeyCode.CursorRight);
-
-	/// <summary>
-	/// The <see cref="Key"/> object for Page Up key.
-	/// </summary>
-	public static Key PageUp => new (KeyCode.PageUp);
-
-	/// <summary>
-	/// The <see cref="Key"/> object for Page Down key.
-	/// </summary>
-	public static Key PageDown => new (KeyCode.PageDown);
-
-	/// <summary>
-	/// The <see cref="Key"/> object for Home key.
-	/// </summary>
-	public static Key Home => new (KeyCode.Home);
-
-	/// <summary>
-	/// The <see cref="Key"/> object for End key.
-	/// </summary>
-	public static Key End => new (KeyCode.End);
-
-	/// <summary>
-	/// The <see cref="Key"/> object for Insert Character key.
-	/// </summary>
-	public static Key InsertChar => new (KeyCode.Insert);
-
-	/// <summary>
-	/// The <see cref="Key"/> object for Delete Character key.
-	/// </summary>
-	public static Key DeleteChar => new (KeyCode.Delete);
-
-	/// <summary>
-	/// The <see cref="Key"/> object for Print Screen key.
-	/// </summary>
-	public static Key PrintScreen => new (KeyCode.PrintScreen);
-
-	/// <summary>
-	/// The <see cref="Key"/> object for F1 key.
-	/// </summary>
-	public static Key F1 => new (KeyCode.F1);
-
-	/// <summary>
-	/// The <see cref="Key"/> object for F2 key.
-	/// </summary>
-	public static Key F2 => new (KeyCode.F2);
-
-	/// <summary>
-	/// The <see cref="Key"/> object for F3 key.
-	/// </summary>
-	public static Key F3 => new (KeyCode.F3);
-
-	/// <summary>
-	/// The <see cref="Key"/> object for F4 key.
-	/// </summary>
-	public static Key F4 => new (KeyCode.F4);
-
-	/// <summary>
-	/// The <see cref="Key"/> object for F5 key.
-	/// </summary>
-	public static Key F5 => new (KeyCode.F5);
-
-	/// <summary>
-	/// The <see cref="Key"/> object for F6 key.
-	/// </summary>
-	public static Key F6 => new (KeyCode.F6);
-
-	/// <summary>
-	/// The <see cref="Key"/> object for F7 key.
-	/// </summary>
-	public static Key F7 => new (KeyCode.F7);
-
-	/// <summary>
-	/// The <see cref="Key"/> object for F8 key.
-	/// </summary>
-	public static Key F8 => new (KeyCode.F8);
-
-	/// <summary>
-	/// The <see cref="Key"/> object for F9 key.
-	/// </summary>
-	public static Key F9 => new (KeyCode.F9);
-
-	/// <summary>
-	/// The <see cref="Key"/> object for F10 key.
-	/// </summary>
-	public static Key F10 => new (KeyCode.F10);
-
-	/// <summary>
-	/// The <see cref="Key"/> object for F11 key.
-	/// </summary>
-	public static Key F11 => new (KeyCode.F11);
-
-	/// <summary>
-	/// The <see cref="Key"/> object for F12 key.
-	/// </summary>
-	public static Key F12 => new (KeyCode.F12);
-
-	/// <summary>
-	/// The <see cref="Key"/> object for F13 key.
-	/// </summary>
-	public static Key F13 => new (KeyCode.F13);
-
-	/// <summary>
-	/// The <see cref="Key"/> object for F14 key.
-	/// </summary>
-	public static Key F14 => new (KeyCode.F14);
-
-	/// <summary>
-	/// The <see cref="Key"/> object for F15 key.
-	/// </summary>
-	public static Key F15 => new (KeyCode.F15);
-
-	/// <summary>
-	/// The <see cref="Key"/> object for F16 key.
-	/// </summary>
-	public static Key F16 => new (KeyCode.F16);
-
-	/// <summary>
-	/// The <see cref="Key"/> object for F17 key.
-	/// </summary>
-	public static Key F17 => new (KeyCode.F17);
-
-	/// <summary>
-	/// The <see cref="Key"/> object for F18 key.
-	/// </summary>
-	public static Key F18 => new (KeyCode.F18);
-
-	/// <summary>
-	/// The <see cref="Key"/> object for F19 key.
-	/// </summary>
-	public static Key F19 => new (KeyCode.F19);
-
-	/// <summary>
-	/// The <see cref="Key"/> object for F20 key.
-	/// </summary>
-	public static Key F20 => new (KeyCode.F20);
-
-	/// <summary>
-	/// The <see cref="Key"/> object for F21 key.
-	/// </summary>
-	public static Key F21 => new (KeyCode.F21);
-
-	/// <summary>
-	/// The <see cref="Key"/> object for F22 key.
-	/// </summary>
-	public static Key F22 => new (KeyCode.F22);
-
-	/// <summary>
-	/// The <see cref="Key"/> object for F23 key.
-	/// </summary>
-	public static Key F23 => new (KeyCode.F23);
-
-	/// <summary>
-	/// The <see cref="Key"/> object for F24 key.
-	/// </summary>
-	public static Key F24 => new (KeyCode.F24);
-	#endregion
-}
+public class Key : EventArgs, IEquatable<Key>
+{
+    /// <summary>Constructs a new <see cref="Key"/></summary>
+    public Key () : this (KeyCode.Null) { }
+
+    /// <summary>Constructs a new <see cref="Key"/> from the provided Key value</summary>
+    /// <param name="k">The key</param>
+    public Key (KeyCode k) { KeyCode = k; }
+
+    /// <summary>
+    /// Copy constructor.
+    /// </summary>
+    /// <param name="key">The Key to copy</param>
+    public Key (Key key)
+    {
+        KeyCode = key.KeyCode;
+        Handled = key.Handled;
+    }
+
+    /// <summary>Constructs a new <see cref="Key"/> from a char.</summary>
+    /// <remarks>
+    ///     <para>
+    ///         The key codes for the A..Z keys are encoded as values between 65 and 90 (<see cref="KeyCode.A"/> through
+    ///         <see cref="KeyCode.Z"/>). While these are the same as the ASCII values for uppercase characters, they represent
+    ///         *keys*, not characters. Therefore, this constructor will store 'A'..'Z' as <see cref="KeyCode.A"/>..
+    ///         <see cref="KeyCode.Z"/> with the <see cref="KeyCode.ShiftMask"/> set and will store `a`..`z` as
+    ///         <see cref="KeyCode.A"/>..<see cref="KeyCode.Z"/>.
+    ///     </para>
+    /// </remarks>
+    /// <param name="ch"></param>
+    public Key (char ch)
+    {
+        switch (ch)
+        {
+            case >= 'A' and <= 'Z':
+                // Upper case A..Z mean "Shift-char" so we need to add Shift
+                KeyCode = (KeyCode)ch | KeyCode.ShiftMask;
+
+                break;
+            case >= 'a' and <= 'z':
+                // Lower case a..z mean no shift, so we need to store as Key.A...Key.Z
+                KeyCode = (KeyCode)(ch - 32);
+
+                return;
+            default:
+                KeyCode = (KeyCode)ch;
+
+                break;
+        }
+    }
+
+    /// <summary>
+    ///     Constructs a new Key from a string describing the key. See
+    ///     <see cref="TryParse(string, out Terminal.Gui.Key)"/> for information on the format of the string.
+    /// </summary>
+    /// <param name="str">The string describing the key.</param>
+    public Key (string str)
+    {
+        bool result = TryParse (str, out Key key);
+
+        if (!result)
+        {
+            throw new ArgumentException (@$"Invalid key string: {str}", nameof (str));
+        }
+
+        KeyCode = key.KeyCode;
+    }
+
+    /// <summary>
+    ///     The key value as a Rune. This is the actual value of the key pressed, and is independent of the modifiers.
+    ///     Useful for determining if a key represents is a printable character.
+    /// </summary>
+    /// <remarks>
+    ///     <para>Keys with Ctrl or Alt modifiers will return <see langword="default"/>.</para>
+    ///     <para>
+    ///         If the key is a letter key (A-Z), the Rune will be the upper or lower case letter depending on whether
+    ///         <see cref="KeyCode.ShiftMask"/> is set.
+    ///     </para>
+    ///     <para>
+    ///         If the key is outside of the <see cref="KeyCode.CharMask"/> range, the returned Rune will be
+    ///         <see langword="default"/>.
+    ///     </para>
+    /// </remarks>
+    public Rune AsRune => ToRune (KeyCode);
+
+    private bool _handled = false;
+
+    /// <summary>
+    ///     Indicates if the current Key event has already been processed and the driver should stop notifying any other
+    ///     event subscriber. It's important to set this value to true specially when updating any View's layout from inside the
+    ///     subscriber method.
+    /// </summary>
+    public bool Handled
+    {
+        get => _handled;
+        set => _handled = value;
+    }
+
+    /// <summary>Gets a value indicating whether the Alt key was pressed (real or synthesized)</summary>
+    /// <value><see langword="true"/> if is alternate; otherwise, <see langword="false"/>.</value>
+    public bool IsAlt => (KeyCode & KeyCode.AltMask) != 0;
+
+    /// <summary>Gets a value indicating whether the Ctrl key was pressed.</summary>
+    /// <value><see langword="true"/> if is ctrl; otherwise, <see langword="false"/>.</value>
+    public bool IsCtrl => (KeyCode & KeyCode.CtrlMask) != 0;
+
+    /// <summary>
+    ///     Gets a value indicating whether the key represents a key in the range of <see cref="KeyCode.A"/> to
+    ///     <see cref="KeyCode.Z"/>, regardless of the <see cref="KeyCode.ShiftMask"/>. This is useful for testing if a key is
+    ///     based on these keys which are special cased.
+    /// </summary>
+    /// <remarks>
+    ///     IMPORTANT: Lowercase alpha keys are encoded in <see cref="Key.KeyCode"/> as values between 65 and 90
+    ///     corresponding to the un-shifted A to Z keys on a keyboard. Helper properties are provided these (e.g.
+    ///     <see cref="Key.A"/>, <see cref="Key.B"/>, etc.). Even though the values are the same as the ASCII values for
+    ///     uppercase characters, these enum values represent *lowercase*, un-shifted characters.
+    /// </remarks>
+    public bool IsKeyCodeAtoZ => GetIsKeyCodeAtoZ (KeyCode);
+
+    /// <summary>Gets a value indicating whether the Shift key was pressed.</summary>
+    /// <value><see langword="true"/> if is shift; otherwise, <see langword="false"/>.</value>
+    public bool IsShift => (KeyCode & KeyCode.ShiftMask) != 0;
+
+    /// <summary>
+    ///     Indicates whether the <see cref="Key"/> is valid or not. Invalid keys are <see cref="Key.Empty"/>, and keys
+    ///     with only shift modifiers.
+    /// </summary>
+    public bool IsValid => this != Empty && NoAlt.NoShift.NoCtrl != Empty;
+
+    private readonly KeyCode _keyCode;
+
+    /// <summary>The encoded key value.</summary>
+    /// <para>
+    ///     IMPORTANT: Lowercase alpha keys are encoded (in <see cref="Gui.KeyCode"/>) as values between 65 and 90
+    ///     corresponding to the un-shifted A to Z keys on a keyboard. Enum values are provided for these (e.g.
+    ///     <see cref="KeyCode.A"/>, <see cref="KeyCode.B"/>, etc.). Even though the values are the same as the ASCII values
+    ///     for uppercase characters, these enum values represent *lowercase*, un-shifted characters.
+    /// </para>
+    /// <remarks>This property is the backing data for the <see cref="Key"/>. It is a <see cref="KeyCode"/> enum value.</remarks>
+    public KeyCode KeyCode
+    {
+        get => _keyCode;
+        init
+        {
+#if DEBUG
+            if (GetIsKeyCodeAtoZ (value) && (value & KeyCode.Space) != 0)
+            {
+                throw new ArgumentException ($"Invalid KeyCode: {value} is invalid.", nameof (value));
+            }
+
+#endif
+            _keyCode = value;
+        }
+    }
+
+    /// <summary>
+    ///     Helper for removing a shift modifier from a <see cref="Key"/>.
+    ///     <code>
+    /// var ControlAltDelete = new Key(Key.Delete).WithAlt.WithDel;
+    /// var AltDelete = ControlAltDelete.NoCtrl;
+    /// </code>
+    /// </summary>
+    public Key NoAlt => new (this) { KeyCode = KeyCode & ~KeyCode.AltMask };
+
+    /// <summary>
+    ///     Helper for removing a shift modifier from a <see cref="Key"/>.
+    ///     <code>
+    /// var ControlAltDelete = new Key(Key.Delete).WithAlt.WithDel;
+    /// var AltDelete = ControlAltDelete.NoCtrl;
+    /// </code>
+    /// </summary>
+    public Key NoCtrl => new (this) { KeyCode = KeyCode & ~KeyCode.CtrlMask };
+
+    /// <summary>
+    ///     Helper for removing a shift modifier from a <see cref="Key"/>.
+    ///     <code>
+    /// var ControlAltDelete = new Key(Key.Delete).WithAlt.WithDel;
+    /// var AltDelete = ControlAltDelete.NoCtrl;
+    /// </code>
+    /// </summary>
+    public Key NoShift => new (this) { KeyCode = KeyCode & ~KeyCode.ShiftMask };
+
+    /// <summary>
+    ///     Helper for specifying a shifted <see cref="Key"/>.
+    ///     <code>
+    /// var ControlAltDelete = new Key(Key.Delete).WithAlt.WithDel;
+    /// </code>
+    /// </summary>
+    public Key WithAlt => new (this) { KeyCode = KeyCode | KeyCode.AltMask };
+
+    /// <summary>
+    ///     Helper for specifying a shifted <see cref="Key"/>.
+    ///     <code>
+    /// var ControlAltDelete = new Key(Key.Delete).WithAlt.WithDel;
+    /// </code>
+    /// </summary>
+    public Key WithCtrl => new (this) { KeyCode = KeyCode | KeyCode.CtrlMask };
+
+    /// <summary>
+    ///     Helper for specifying a shifted <see cref="Key"/>.
+    ///     <code>
+    /// var ControlAltDelete = new Key(Key.Delete).WithAlt.WithDel;
+    /// </code>
+    /// </summary>
+    public Key WithShift => new (this) { KeyCode = KeyCode | KeyCode.ShiftMask };
+
+    /// <summary>
+    ///     Tests if a KeyCode represents a key in the range of <see cref="KeyCode.A"/> to <see cref="KeyCode.Z"/>,
+    ///     regardless of the <see cref="KeyCode.ShiftMask"/>. This is useful for testing if a key is based on these keys which
+    ///     are special cased.
+    /// </summary>
+    /// <remarks>
+    ///     IMPORTANT: Lowercase alpha keys are encoded in <see cref="Key.KeyCode"/> as values between 65 and 90
+    ///     corresponding to the un-shifted A to Z keys on a keyboard. Helper properties are provided these (e.g.
+    ///     <see cref="Key.A"/>, <see cref="Key.B"/>, etc.). Even though the values are the same as the ASCII values for
+    ///     uppercase characters, these enum values represent *lowercase*, un-shifted characters.
+    /// </remarks>
+    public static bool GetIsKeyCodeAtoZ (KeyCode keyCode)
+    {
+        if ((keyCode & KeyCode.AltMask) != 0 || (keyCode & KeyCode.CtrlMask) != 0)
+        {
+            return false;
+        }
+
+        if ((keyCode & ~KeyCode.Space & ~KeyCode.ShiftMask) is >= KeyCode.A and <= KeyCode.Z)
+        {
+            return true;
+        }
+
+        return (keyCode & KeyCode.CharMask) is >= KeyCode.A and <= KeyCode.Z;
+    }
+
+    /// <summary>
+    ///     Converts a <see cref="KeyCode"/> to a <see cref="Rune"/>. Useful for determining if a key represents is a
+    ///     printable character.
+    /// </summary>
+    /// <remarks>
+    ///     <para>Keys with Ctrl or Alt modifiers will return <see langword="default"/>.</para>
+    ///     <para>
+    ///         If the key is a letter key (A-Z), the Rune will be the upper or lower case letter depending on whether
+    ///         <see cref="KeyCode.ShiftMask"/> is set.
+    ///     </para>
+    ///     <para>
+    ///         If the key is outside of the <see cref="KeyCode.CharMask"/> range, the returned Rune will be
+    ///         <see langword="default"/>.
+    ///     </para>
+    /// </remarks>
+    /// <param name="key"></param>
+    /// <returns>The key converted to a Rune. <see langword="default"/> if conversion is not possible.</returns>
+    public static Rune ToRune (KeyCode key)
+    {
+        if (key is KeyCode.Null or KeyCode.SpecialMask
+            || key.HasFlag (KeyCode.CtrlMask)
+            || key.HasFlag (KeyCode.AltMask))
+        {
+            return default (Rune);
+        }
+
+        // Extract the base key code
+        KeyCode baseKey = key;
+
+        if (baseKey.HasFlag (KeyCode.ShiftMask))
+        {
+            baseKey &= ~KeyCode.ShiftMask;
+        }
+
+        switch (baseKey)
+        {
+            case >= KeyCode.A and <= KeyCode.Z when !key.HasFlag (KeyCode.ShiftMask):
+                return new Rune ((uint)(baseKey + 32));
+            case >= KeyCode.A and <= KeyCode.Z when key.HasFlag (KeyCode.ShiftMask):
+                return new Rune ((uint)baseKey);
+            case > KeyCode.Null and < KeyCode.A:
+                return new Rune ((uint)baseKey);
+        }
+
+        if (Enum.IsDefined (typeof (KeyCode), baseKey))
+        {
+            return default (Rune);
+        }
+
+        return new Rune ((uint)baseKey);
+    }
+
+    #region Operators
+
+    /// <summary>
+    ///     Explicitly cast a <see cref="Key"/> to a <see cref="Rune"/>. The conversion is lossy because properties such
+    ///     as <see cref="Handled"/> are not encoded in <see cref="KeyCode"/>.
+    /// </summary>
+    /// <remarks>Uses <see cref="AsRune"/>.</remarks>
+    /// <param name="kea"></param>
+    public static explicit operator Rune (Key kea) { return kea.AsRune; }
+
+    // BUGBUG: (Tig) I do not think this cast operator is really needed. 
+    /// <summary>
+    ///     Explicitly cast <see cref="Key"/> to a <see langword="uint"/>. The conversion is lossy because properties such
+    ///     as <see cref="Handled"/> are not encoded in <see cref="KeyCode"/>.
+    /// </summary>
+    /// <param name="kea"></param>
+    public static explicit operator uint (Key kea) { return (uint)kea.KeyCode; }
+
+    /// <summary>
+    ///     Explicitly cast <see cref="Key"/> to a <see cref="KeyCode"/>. The conversion is lossy because properties such
+    ///     as <see cref="Handled"/> are not encoded in <see cref="KeyCode"/>.
+    /// </summary>
+    /// <param name="key"></param>
+    public static explicit operator KeyCode (Key key) { return key.KeyCode; }
+
+    /// <summary>Cast <see cref="KeyCode"/> to a <see cref="Key"/>.</summary>
+    /// <param name="keyCode"></param>
+    public static implicit operator Key (KeyCode keyCode) { return new Key (keyCode); }
+
+    /// <summary>Cast <see langword="char"/> to a <see cref="Key"/>.</summary>
+    /// <remarks>See <see cref="Key(char)"/> for more information.</remarks>
+    /// <param name="ch"></param>
+    public static implicit operator Key (char ch) { return new Key (ch); }
+
+    /// <summary>Cast <see langword="string"/> to a <see cref="Key"/>.</summary>
+    /// <remarks>See <see cref="Key(string)"/> for more information.</remarks>
+    /// <param name="str"></param>
+    public static implicit operator Key (string str) { return new Key (str); }
+
+    /// <summary>Cast a <see cref="Key"/> to a <see langword="string"/>.</summary>
+    /// <remarks>See <see cref="Key(string)"/> for more information.</remarks>
+    /// <param name="key"></param>
+    public static implicit operator string (Key key) { return key.ToString (); }
+
+    /// <inheritdoc/>
+    public override bool Equals (object obj)
+    {
+        return obj is Key k && k.KeyCode == KeyCode && k.Handled == Handled;
+    }
+
+    bool IEquatable<Key>.Equals (Key other) { return Equals (other); }
+
+    /// <inheritdoc/>
+    public override int GetHashCode () { return (int)KeyCode; }
+
+    /// <summary>Compares two <see cref="Key"/>s for equality.</summary>
+    /// <param name="a"></param>
+    /// <param name="b"></param>
+    /// <returns></returns>
+    public static bool operator == (Key a, Key b) { return a?.KeyCode == b?.KeyCode; }
+
+    /// <summary>Compares two <see cref="Key"/>s for not equality.</summary>
+    /// <param name="a"></param>
+    /// <param name="b"></param>
+    /// <returns></returns>
+    public static bool operator != (Key a, Key b) { return a?.KeyCode != b?.KeyCode; }
+
+    /// <summary>Compares two <see cref="Key"/>s for less-than.</summary>
+    /// <param name="a"></param>
+    /// <param name="b"></param>
+    /// <returns></returns>
+    public static bool operator < (Key a, Key b) { return a?.KeyCode < b?.KeyCode; }
+
+    /// <summary>Compares two <see cref="Key"/>s for greater-than.</summary>
+    /// <param name="a"></param>
+    /// <param name="b"></param>
+    /// <returns></returns>
+    public static bool operator > (Key a, Key b) { return a?.KeyCode > b?.KeyCode; }
+
+    /// <summary>Compares two <see cref="Key"/>s for greater-than-or-equal-to.</summary>
+    /// <param name="a"></param>
+    /// <param name="b"></param>
+    /// <returns></returns>
+    public static bool operator <= (Key a, Key b) { return a?.KeyCode <= b?.KeyCode; }
+
+    /// <summary>Compares two <see cref="Key"/>s for greater-than-or-equal-to.</summary>
+    /// <param name="a"></param>
+    /// <param name="b"></param>
+    /// <returns></returns>
+    public static bool operator >= (Key a, Key b) { return a?.KeyCode >= b?.KeyCode; }
+
+    #endregion Operators
+
+    #region String conversion
+
+    /// <summary>Pretty prints the KeyEvent</summary>
+    /// <returns></returns>
+    public override string ToString () { return ToString (KeyCode, (Rune)'+'); }
+
+    private static string GetKeyString (KeyCode key)
+    {
+        if (key is KeyCode.Null or KeyCode.SpecialMask)
+        {
+            return string.Empty;
+        }
+
+        // Extract the base key (removing modifier flags)
+        KeyCode baseKey = key & ~KeyCode.CtrlMask & ~KeyCode.AltMask & ~KeyCode.ShiftMask;
+
+        if (!key.HasFlag (KeyCode.ShiftMask) && baseKey is >= KeyCode.A and <= KeyCode.Z)
+        {
+            return ((Rune)(uint)(key + 32)).ToString ();
+        }
+
+        if (key is > KeyCode.Space and < KeyCode.A)
+        {
+            return ((Rune)(uint)key).ToString ();
+        }
+
+        string keyName = Enum.GetName (typeof (KeyCode), key);
+
+        return !string.IsNullOrEmpty (keyName) ? keyName : ((Rune)(uint)key).ToString ();
+    }
+
+    /// <summary>Formats a <see cref="KeyCode"/> as a string using the default separator of '+'</summary>
+    /// <param name="key">The key to format.</param>
+    /// <returns>
+    ///     The formatted string. If the key is a printable character, it will be returned as a string. Otherwise, the key
+    ///     name will be returned.
+    /// </returns>
+    public static string ToString (KeyCode key) { return ToString (key, (Rune)'+'); }
+
+    /// <summary>Formats a <see cref="KeyCode"/> as a string.</summary>
+    /// <param name="key">The key to format.</param>
+    /// <param name="separator">The character to use as a separator between modifier keys and and the key itself.</param>
+    /// <returns>
+    ///     The formatted string. If the key is a printable character, it will be returned as a string. Otherwise, the key
+    ///     name will be returned.
+    /// </returns>
+    public static string ToString (KeyCode key, Rune separator)
+    {
+        if (key is KeyCode.Null)
+        {
+            // Same as Key.IsValid
+            return @"Null";
+        }
+
+        var sb = new StringBuilder ();
+
+        // Extract the base key (removing modifier flags)
+        KeyCode baseKey = key & ~KeyCode.CtrlMask & ~KeyCode.AltMask & ~KeyCode.ShiftMask;
+
+        // Extract and handle modifiers
+        var hasModifiers = false;
+
+        if ((key & KeyCode.CtrlMask) != 0)
+        {
+            sb.Append ($"Ctrl{separator}");
+            hasModifiers = true;
+        }
+
+        if ((key & KeyCode.AltMask) != 0)
+        {
+            sb.Append ($"Alt{separator}");
+            hasModifiers = true;
+        }
+
+        if ((key & KeyCode.ShiftMask) != 0 && !GetIsKeyCodeAtoZ (key))
+        {
+            sb.Append ($"Shift{separator}");
+            hasModifiers = true;
+        }
+
+        // Handle special cases and modifiers on their own
+        if (key != KeyCode.SpecialMask && (baseKey != KeyCode.Null || hasModifiers))
+        {
+            if ((key & KeyCode.SpecialMask) != 0 && (baseKey & ~KeyCode.Space) is >= KeyCode.A and <= KeyCode.Z)
+            {
+                sb.Append (baseKey & ~KeyCode.Space);
+            }
+            else
+            {
+                // Append the actual key name
+                sb.Append (GetKeyString (baseKey));
+            }
+        }
+
+        return TrimEndSeparator (sb.ToString (), separator);
+    }
+
+    private static string TrimEndSeparator (string input, Rune separator)
+    {
+        // Trim the trailing separator (+). Unless there are two separators at the end.
+        // "+" (don't trim)
+        // "Ctrl+" (trim)
+        // "Ctrl++" (trim)
+
+        if (input.Length > 1 && new Rune (input [^1]) == separator && new Rune (input [^2]) != separator)
+        {
+            return input [..^1];
+        }
+
+        return input;
+    }
+
+    private static readonly Dictionary<string, KeyCode> _modifierDict =
+        new (StringComparer.InvariantCultureIgnoreCase)
+        {
+            { "Shift", KeyCode.ShiftMask }, { "Ctrl", KeyCode.CtrlMask }, { "Alt", KeyCode.AltMask }
+        };
+
+    /// <summary>Converts the provided string to a new <see cref="Key"/> instance.</summary>
+    /// <param name="text">
+    ///     The text to analyze. Formats supported are "Ctrl+X", "Alt+X", "Shift+X", "Ctrl+Alt+X",
+    ///     "Ctrl+Shift+X", "Alt+Shift+X", "Ctrl+Alt+Shift+X", and "X".
+    /// </param>
+    /// <param name="key">The parsed value.</param>
+    /// <returns>A boolean value indicating whether parsing was successful.</returns>
+    /// <remarks></remarks>
+    public static bool TryParse (string text, [NotNullWhen (true)] out Key key)
+    {
+        if (string.IsNullOrEmpty (text))
+        {
+            key = Key.Empty;
+
+            return true;
+        }
+
+        key = null;
+
+        // Split the string into parts
+        string [] parts = text.Split ('+', '-');
+
+        if (parts.Length is 0 or > 4 || parts.Any (string.IsNullOrEmpty))
+        {
+            return false;
+        }
+
+        // if it's just a shift key
+        if (parts.Length == 1)
+        {
+            switch (parts [0])
+            {
+                case "Ctrl":
+                    key = KeyCode.CtrlMask;
+
+                    return true;
+                case "Alt":
+                    key = KeyCode.AltMask;
+
+                    return true;
+                case "Shift":
+                    key = KeyCode.ShiftMask;
+
+                    return true;
+            }
+        }
+
+        var modifiers = KeyCode.Null;
+
+        for (var index = 0; index < parts.Length; index++)
+        {
+            if (_modifierDict.TryGetValue (parts [index].ToLowerInvariant (), out KeyCode modifier))
+            {
+                modifiers |= modifier;
+                parts [index] = string.Empty; // eat it
+            }
+        }
+
+        // we now have the modifiers
+
+        string partNotFound = parts.FirstOrDefault (p => !string.IsNullOrEmpty (p), string.Empty);
+        var parsedKeyCode = KeyCode.Null;
+        var parsedInt = 0;
+
+        if (partNotFound.Length == 1)
+        {
+            var keyCode = (KeyCode)partNotFound [0];
+
+            // if it's a single digit int, treat it as such
+            if (int.TryParse (
+                              partNotFound,
+                              NumberStyles.Integer,
+                              CultureInfo.InvariantCulture,
+                              out parsedInt
+                             ))
+            {
+                keyCode = (KeyCode)((int)KeyCode.D0 + parsedInt);
+            }
+            else if (Enum.TryParse (partNotFound, false, out parsedKeyCode))
+            {
+                if (parsedKeyCode != KeyCode.Null)
+                {
+                    if (parsedKeyCode is >= KeyCode.A and <= KeyCode.Z && modifiers == 0)
+                    {
+                        key = new Key (parsedKeyCode | KeyCode.ShiftMask);
+
+                        return true;
+                    }
+
+                    key = new Key (parsedKeyCode | modifiers);
+
+                    return true;
+                }
+            }
+
+            if (GetIsKeyCodeAtoZ (keyCode) && (keyCode & KeyCode.Space) != 0)
+            {
+                keyCode = keyCode & ~KeyCode.Space;
+            }
+            key = new Key (keyCode | modifiers);
+
+            return true;
+        }
+
+        if (Enum.TryParse (partNotFound, true, out parsedKeyCode))
+        {
+            if (parsedKeyCode != KeyCode.Null)
+            {
+                if (parsedKeyCode is >= KeyCode.A and <= KeyCode.Z && modifiers == 0)
+                {
+                    key = new Key (parsedKeyCode | KeyCode.ShiftMask);
+
+                    return true;
+                }
+
+                if (GetIsKeyCodeAtoZ (parsedKeyCode) && (parsedKeyCode & KeyCode.Space) != 0)
+                {
+                    parsedKeyCode = parsedKeyCode & ~KeyCode.Space;
+                }
+                key = new Key (parsedKeyCode | modifiers);
+
+                return true;
+            }
+        }
+
+        // if it's a number int, treat it as a unicode value
+        if (int.TryParse (
+                          partNotFound,
+                          NumberStyles.Number,
+                          CultureInfo.InvariantCulture,
+                          out parsedInt
+                         ))
+        {
+            if (!Rune.IsValid (parsedInt))
+            {
+                return false;
+            }
+
+            if ((KeyCode)parsedInt is >= KeyCode.A and <= KeyCode.Z && modifiers == 0)
+            {
+                key = new Key ((KeyCode)parsedInt | KeyCode.ShiftMask);
+
+                return true;
+            }
+
+            key = new Key ((KeyCode)parsedInt);
+
+            return true;
+        }
+
+        if (!Enum.TryParse (partNotFound, true, out parsedKeyCode))
+        {
+            return false;
+        }
+
+        if (GetIsKeyCodeAtoZ (parsedKeyCode))
+        {
+            key = new Key (parsedKeyCode | (modifiers & ~KeyCode.Space));
+
+            return true;
+        }
+
+        return false;
+    }
+
+    #endregion
+
+    #region Standard Key Definitions
+
+    /// <summary>An uninitialized The <see cref="Key"/> object.</summary>
+    public new static Key Empty => new ();
+
+    /// <summary>The <see cref="Key"/> object for the Backspace key.</summary>
+    public static Key Backspace => new (KeyCode.Backspace);
+
+    /// <summary>The <see cref="Key"/> object for the tab key (forwards tab key).</summary>
+    public static Key Tab => new (KeyCode.Tab);
+
+    /// <summary>The <see cref="Key"/> object for the return key.</summary>
+    public static Key Enter => new (KeyCode.Enter);
+
+    /// <summary>The <see cref="Key"/> object for the clear key.</summary>
+    public static Key Clear => new (KeyCode.Clear);
+
+    /// <summary>The <see cref="Key"/> object for the Escape key.</summary>
+    public static Key Esc => new (KeyCode.Esc);
+
+    /// <summary>The <see cref="Key"/> object for the Space bar key.</summary>
+    public static Key Space => new (KeyCode.Space);
+
+    /// <summary>The <see cref="Key"/> object for 0 key.</summary>
+    public static Key D0 => new (KeyCode.D0);
+
+    /// <summary>The <see cref="Key"/> object for 1 key.</summary>
+    public static Key D1 => new (KeyCode.D1);
+
+    /// <summary>The <see cref="Key"/> object for 2 key.</summary>
+    public static Key D2 => new (KeyCode.D2);
+
+    /// <summary>The <see cref="Key"/> object for 3 key.</summary>
+    public static Key D3 => new (KeyCode.D3);
+
+    /// <summary>The <see cref="Key"/> object for 4 key.</summary>
+    public static Key D4 => new (KeyCode.D4);
+
+    /// <summary>The <see cref="Key"/> object for 5 key.</summary>
+    public static Key D5 => new (KeyCode.D5);
+
+    /// <summary>The <see cref="Key"/> object for 6 key.</summary>
+    public static Key D6 => new (KeyCode.D6);
+
+    /// <summary>The <see cref="Key"/> object for 7 key.</summary>
+    public static Key D7 => new (KeyCode.D7);
+
+    /// <summary>The <see cref="Key"/> object for 8 key.</summary>
+    public static Key D8 => new (KeyCode.D8);
+
+    /// <summary>The <see cref="Key"/> object for 9 key.</summary>
+    public static Key D9 => new (KeyCode.D9);
+
+    /// <summary>The <see cref="Key"/> object for the A key (un-shifted). Use <c>Key.A.WithShift</c> for uppercase 'A'.</summary>
+    public static Key A => new (KeyCode.A);
+
+    /// <summary>The <see cref="Key"/> object for the B key (un-shifted). Use <c>Key.B.WithShift</c> for uppercase 'B'.</summary>
+    public static Key B => new (KeyCode.B);
+
+    /// <summary>The <see cref="Key"/> object for the C key (un-shifted). Use <c>Key.C.WithShift</c> for uppercase 'C'.</summary>
+    public static Key C => new (KeyCode.C);
+
+    /// <summary>The <see cref="Key"/> object for the D key (un-shifted). Use <c>Key.D.WithShift</c> for uppercase 'D'.</summary>
+    public static Key D => new (KeyCode.D);
+
+    /// <summary>The <see cref="Key"/> object for the E key (un-shifted). Use <c>Key.E.WithShift</c> for uppercase 'E'.</summary>
+    public static Key E => new (KeyCode.E);
+
+    /// <summary>The <see cref="Key"/> object for the F key (un-shifted). Use <c>Key.F.WithShift</c> for uppercase 'F'.</summary>
+    public static Key F => new (KeyCode.F);
+
+    /// <summary>The <see cref="Key"/> object for the G key (un-shifted). Use <c>Key.G.WithShift</c> for uppercase 'G'.</summary>
+    public static Key G => new (KeyCode.G);
+
+    /// <summary>The <see cref="Key"/> object for the H key (un-shifted). Use <c>Key.H.WithShift</c> for uppercase 'H'.</summary>
+    public static Key H => new (KeyCode.H);
+
+    /// <summary>The <see cref="Key"/> object for the I key (un-shifted). Use <c>Key.I.WithShift</c> for uppercase 'I'.</summary>
+    public static Key I => new (KeyCode.I);
+
+    /// <summary>The <see cref="Key"/> object for the J key (un-shifted). Use <c>Key.J.WithShift</c> for uppercase 'J'.</summary>
+    public static Key J => new (KeyCode.J);
+
+    /// <summary>The <see cref="Key"/> object for the K key (un-shifted). Use <c>Key.K.WithShift</c> for uppercase 'K'.</summary>
+    public static Key K => new (KeyCode.K);
+
+    /// <summary>The <see cref="Key"/> object for the L key (un-shifted). Use <c>Key.L.WithShift</c> for uppercase 'L'.</summary>
+    public static Key L => new (KeyCode.L);
+
+    /// <summary>The <see cref="Key"/> object for the M key (un-shifted). Use <c>Key.M.WithShift</c> for uppercase 'M'.</summary>
+    public static Key M => new (KeyCode.M);
+
+    /// <summary>The <see cref="Key"/> object for the N key (un-shifted). Use <c>Key.N.WithShift</c> for uppercase 'N'.</summary>
+    public static Key N => new (KeyCode.N);
+
+    /// <summary>The <see cref="Key"/> object for the O key (un-shifted). Use <c>Key.O.WithShift</c> for uppercase 'O'.</summary>
+    public static Key O => new (KeyCode.O);
+
+    /// <summary>The <see cref="Key"/> object for the P key (un-shifted). Use <c>Key.P.WithShift</c> for uppercase 'P'.</summary>
+    public static Key P => new (KeyCode.P);
+
+    /// <summary>The <see cref="Key"/> object for the Q key (un-shifted). Use <c>Key.Q.WithShift</c> for uppercase 'Q'.</summary>
+    public static Key Q => new (KeyCode.Q);
+
+    /// <summary>The <see cref="Key"/> object for the R key (un-shifted). Use <c>Key.R.WithShift</c> for uppercase 'R'.</summary>
+    public static Key R => new (KeyCode.R);
+
+    /// <summary>The <see cref="Key"/> object for the S key (un-shifted). Use <c>Key.S.WithShift</c> for uppercase 'S'.</summary>
+    public static Key S => new (KeyCode.S);
+
+    /// <summary>The <see cref="Key"/> object for the T key (un-shifted). Use <c>Key.T.WithShift</c> for uppercase 'T'.</summary>
+    public static Key T => new (KeyCode.T);
+
+    /// <summary>The <see cref="Key"/> object for the U key (un-shifted). Use <c>Key.U.WithShift</c> for uppercase 'U'.</summary>
+    public static Key U => new (KeyCode.U);
+
+    /// <summary>The <see cref="Key"/> object for the V key (un-shifted). Use <c>Key.V.WithShift</c> for uppercase 'V'.</summary>
+    public static Key V => new (KeyCode.V);
+
+    /// <summary>The <see cref="Key"/> object for the W key (un-shifted). Use <c>Key.W.WithShift</c> for uppercase 'W'.</summary>
+    public static Key W => new (KeyCode.W);
+
+    /// <summary>The <see cref="Key"/> object for the X key (un-shifted). Use <c>Key.X.WithShift</c> for uppercase 'X'.</summary>
+    public static Key X => new (KeyCode.X);
+
+    /// <summary>The <see cref="Key"/> object for the Y key (un-shifted). Use <c>Key.Y.WithShift</c> for uppercase 'Y'.</summary>
+    public static Key Y => new (KeyCode.Y);
+
+    /// <summary>The <see cref="Key"/> object for the Z key (un-shifted). Use <c>Key.Z.WithShift</c> for uppercase 'Z'.</summary>
+    public static Key Z => new (KeyCode.Z);
+
+    /// <summary>The <see cref="Key"/> object for the Delete key.</summary>
+    public static Key Delete => new (KeyCode.Delete);
+
+    /// <summary>The <see cref="Key"/> object for the Cursor up key.</summary>
+    public static Key CursorUp => new (KeyCode.CursorUp);
+
+    /// <summary>The <see cref="Key"/> object for Cursor down key.</summary>
+    public static Key CursorDown => new (KeyCode.CursorDown);
+
+    /// <summary>The <see cref="Key"/> object for Cursor left key.</summary>
+    public static Key CursorLeft => new (KeyCode.CursorLeft);
+
+    /// <summary>The <see cref="Key"/> object for Cursor right key.</summary>
+    public static Key CursorRight => new (KeyCode.CursorRight);
+
+    /// <summary>The <see cref="Key"/> object for Page Up key.</summary>
+    public static Key PageUp => new (KeyCode.PageUp);
+
+    /// <summary>The <see cref="Key"/> object for Page Down key.</summary>
+    public static Key PageDown => new (KeyCode.PageDown);
+
+    /// <summary>The <see cref="Key"/> object for Home key.</summary>
+    public static Key Home => new (KeyCode.Home);
+
+    /// <summary>The <see cref="Key"/> object for End key.</summary>
+    public static Key End => new (KeyCode.End);
+
+    /// <summary>The <see cref="Key"/> object for Insert Character key.</summary>
+    public static Key InsertChar => new (KeyCode.Insert);
+
+    /// <summary>The <see cref="Key"/> object for Delete Character key.</summary>
+    public static Key DeleteChar => new (KeyCode.Delete);
+
+    /// <summary>The <see cref="Key"/> object for Print Screen key.</summary>
+    public static Key PrintScreen => new (KeyCode.PrintScreen);
+
+    /// <summary>The <see cref="Key"/> object for F1 key.</summary>
+    public static Key F1 => new (KeyCode.F1);
+
+    /// <summary>The <see cref="Key"/> object for F2 key.</summary>
+    public static Key F2 => new (KeyCode.F2);
+
+    /// <summary>The <see cref="Key"/> object for F3 key.</summary>
+    public static Key F3 => new (KeyCode.F3);
+
+    /// <summary>The <see cref="Key"/> object for F4 key.</summary>
+    public static Key F4 => new (KeyCode.F4);
+
+    /// <summary>The <see cref="Key"/> object for F5 key.</summary>
+    public static Key F5 => new (KeyCode.F5);
+
+    /// <summary>The <see cref="Key"/> object for F6 key.</summary>
+    public static Key F6 => new (KeyCode.F6);
+
+    /// <summary>The <see cref="Key"/> object for F7 key.</summary>
+    public static Key F7 => new (KeyCode.F7);
+
+    /// <summary>The <see cref="Key"/> object for F8 key.</summary>
+    public static Key F8 => new (KeyCode.F8);
+
+    /// <summary>The <see cref="Key"/> object for F9 key.</summary>
+    public static Key F9 => new (KeyCode.F9);
+
+    /// <summary>The <see cref="Key"/> object for F10 key.</summary>
+    public static Key F10 => new (KeyCode.F10);
+
+    /// <summary>The <see cref="Key"/> object for F11 key.</summary>
+    public static Key F11 => new (KeyCode.F11);
+
+    /// <summary>The <see cref="Key"/> object for F12 key.</summary>
+    public static Key F12 => new (KeyCode.F12);
+
+    /// <summary>The <see cref="Key"/> object for F13 key.</summary>
+    public static Key F13 => new (KeyCode.F13);
+
+    /// <summary>The <see cref="Key"/> object for F14 key.</summary>
+    public static Key F14 => new (KeyCode.F14);
+
+    /// <summary>The <see cref="Key"/> object for F15 key.</summary>
+    public static Key F15 => new (KeyCode.F15);
+
+    /// <summary>The <see cref="Key"/> object for F16 key.</summary>
+    public static Key F16 => new (KeyCode.F16);
+
+    /// <summary>The <see cref="Key"/> object for F17 key.</summary>
+    public static Key F17 => new (KeyCode.F17);
+
+    /// <summary>The <see cref="Key"/> object for F18 key.</summary>
+    public static Key F18 => new (KeyCode.F18);
+
+    /// <summary>The <see cref="Key"/> object for F19 key.</summary>
+    public static Key F19 => new (KeyCode.F19);
+
+    /// <summary>The <see cref="Key"/> object for F20 key.</summary>
+    public static Key F20 => new (KeyCode.F20);
+
+    /// <summary>The <see cref="Key"/> object for F21 key.</summary>
+    public static Key F21 => new (KeyCode.F21);
+
+    /// <summary>The <see cref="Key"/> object for F22 key.</summary>
+    public static Key F22 => new (KeyCode.F22);
+
+    /// <summary>The <see cref="Key"/> object for F23 key.</summary>
+    public static Key F23 => new (KeyCode.F23);
+
+    /// <summary>The <see cref="Key"/> object for F24 key.</summary>
+    public static Key F24 => new (KeyCode.F24);
+
+    #endregion
+}

+ 227 - 253
Terminal.Gui/Input/KeyBinding.cs

@@ -1,286 +1,260 @@
 // These classes use a key binding system based on the design implemented in Scintilla.Net which is an
 // MIT licensed open source project https://github.com/jacobslusser/ScintillaNET/blob/master/src/ScintillaNET/Command.cs
 
-using System;
-using System.Collections.Generic;
-using System.Linq;
-
 namespace Terminal.Gui;
 
 /// <summary>
-/// Defines the scope of a <see cref="Command"/> that has been bound to a key with <see cref="KeyBindings.Add(Key, Terminal.Gui.Command[])"/>.
+///     Defines the scope of a <see cref="Command"/> that has been bound to a key with
+///     <see cref="KeyBindings.Add(Key, Terminal.Gui.Command[])"/>.
 /// </summary>
 /// <remarks>
-/// <para>
-/// Key bindings are scoped to the most-focused view (<see cref="Focused"/>) by default.
-/// </para>
+///     <para>Key bindings are scoped to the most-focused view (<see cref="Focused"/>) by default.</para>
 /// </remarks>
-public enum KeyBindingScope {
-	/// <summary>
-	/// The key binding is scoped to just the view that has focus.
-	/// </summary>
-	Focused = 0,
+[Flags]
+public enum KeyBindingScope
+{
+    /// <summary>The key binding is scoped to just the view that has focus.</summary>
+    Focused = 1,
 
-	/// <summary>
-	/// The key binding is scoped to the View's SuperView and will be triggered even when the View does not have focus, as long as the
-	/// SuperView does have focus. This is typically used for <see cref="View.HotKey"/>s.
-	/// <remarks>
-	/// <para>
-	/// Use for Views such as MenuBar and StatusBar which provide commands (shortcuts etc...) that trigger even when not focused.
-	/// </para>
-	/// <para>
-	/// HotKey-scoped key bindings are only invoked if the key down event was not handled by the focused view or any of its subviews.
-	/// </para>
-	/// </remarks>
-	/// </summary>
-	HotKey,
+    /// <summary>
+    ///     The key binding is scoped to the View's SuperView and will be triggered even when the View does not have focus, as
+    ///     long as the SuperView does have focus. This is typically used for <see cref="View.HotKey"/>s.
+    ///     <remarks>
+    ///         <para>
+    ///             Use for Views such as MenuBar and StatusBar which provide commands (shortcuts etc...) that trigger even
+    ///             when not focused.
+    ///         </para>
+    ///         <para>
+    ///             HotKey-scoped key bindings are only invoked if the key down event was not handled by the focused view or
+    ///             any of its subviews.
+    ///         </para>
+    ///     </remarks>
+    /// </summary>
+    HotKey = 2,
 
-	/// <summary>
-	/// The key binding will be triggered regardless of which view has focus. This is typically used for global commands.
-	/// </summary>
-	/// <remarks>
-	/// Application-scoped key bindings are only invoked if the key down event was not handled by the focused view or any of its subviews,
-	/// and if the key down event was not bound to a <see cref="View.HotKey"/>.
-	/// </remarks>
-	Application
+    /// <summary>
+    ///     The key binding will be triggered regardless of which view has focus. This is typically used for global
+    ///     commands.
+    /// </summary>
+    /// <remarks>
+    ///     Application-scoped key bindings are only invoked if the key down event was not handled by the focused view or
+    ///     any of its subviews, and if the key down event was not bound to a <see cref="View.HotKey"/>.
+    /// </remarks>
+    Application = 4
 }
 
-/// <summary>
-/// Provides a collection of <see cref="Command"/> objects that are scoped to <see cref="KeyBindingScope"/>.
-/// </summary>
-public class KeyBinding {
-	/// <summary>
-	/// Initializes a new instance.
-	/// </summary>
-	/// <param name="commands"></param>
-	/// <param name="scope"></param>
-	public KeyBinding (Command [] commands, KeyBindingScope scope)
-	{
-		Commands = commands;
-		Scope = scope;
-	}
+/// <summary>Provides a collection of <see cref="Command"/> objects that are scoped to <see cref="KeyBindingScope"/>.</summary>
+public class KeyBinding
+{
+    /// <summary>Initializes a new instance.</summary>
+    /// <param name="commands"></param>
+    /// <param name="scope"></param>
+    public KeyBinding (Command [] commands, KeyBindingScope scope)
+    {
+        Commands = commands;
+        Scope = scope;
+    }
 
-	/// <summary>
-	/// The actions which can be performed by the application or bound to keys in a <see cref="View"/> control.
-	/// </summary>
-	public Command [] Commands { get; set; }
+    /// <summary>The actions which can be performed by the application or bound to keys in a <see cref="View"/> control.</summary>
+    public Command [] Commands { get; set; }
 
-	/// <summary>
-	/// The scope of the <see cref="Commands"/> bound to a key.
-	/// </summary>
-	public KeyBindingScope Scope { get; set; }
+    /// <summary>The scope of the <see cref="Commands"/> bound to a key.</summary>
+    public KeyBindingScope Scope { get; set; }
 }
 
-/// <summary>
-/// A class that provides a collection of <see cref="KeyBinding"/> objects bound to a <see cref="Key"/>.
-/// </summary>
-public class KeyBindings {
-	/// <summary>
-	/// The collection of <see cref="KeyBinding"/> objects.
-	/// </summary>
-	public Dictionary<Key, KeyBinding> Bindings { get; } = new ();
+/// <summary>A class that provides a collection of <see cref="KeyBinding"/> objects bound to a <see cref="Key"/>.</summary>
+public class KeyBindings
+{
+    // TODO: Add a dictionary comparer that ignores Scope
+    /// <summary>The collection of <see cref="KeyBinding"/> objects.</summary>
+    public Dictionary<Key, KeyBinding> Bindings { get; } = new ();
 
-	/// <summary>
-	/// Adds a <see cref="KeyBinding"/> to the collection.
-	/// </summary>
-	/// <param name="key"></param>
-	/// <param name="binding"></param>
-	public void Add (Key key, KeyBinding binding) => Bindings.Add (key, binding);
+    /// <summary>Adds a <see cref="KeyBinding"/> to the collection.</summary>
+    /// <param name="key"></param>
+    /// <param name="binding"></param>
+    public void Add (Key key, KeyBinding binding) { Bindings.Add (key, binding); }
 
-	/// <summary>
-	/// Removes a <see cref="KeyBinding"/> from the collection.
-	/// </summary>
-	/// <param name="key"></param>
-	public void Remove (Key key) => Bindings.Remove (key);
+    /// <summary>
+    ///     <para>Adds a new key combination that will trigger the commands in <paramref name="commands"/>.</para>
+    ///     <para>
+    ///         If the key is already bound to a different array of <see cref="Command"/>s it will be rebound
+    ///         <paramref name="commands"/>.
+    ///     </para>
+    /// </summary>
+    /// <remarks>
+    ///     Commands are only ever applied to the current <see cref="View"/> (i.e. this feature cannot be used to switch
+    ///     focus to another view and perform multiple commands there).
+    /// </remarks>
+    /// <param name="key">The key to check.</param>
+    /// <param name="scope">The scope for the command.</param>
+    /// <param name="commands">
+    ///     The command to invoked on the <see cref="View"/> when <paramref name="key"/> is pressed. When
+    ///     multiple commands are provided,they will be applied in sequence. The bound <paramref name="key"/> strike will be
+    ///     consumed if any took effect.
+    /// </param>
+    public void Add (Key key, KeyBindingScope scope, params Command [] commands)
+    {
+        if (key is null || !key.IsValid)
+        {
+            //throw new ArgumentException ("Invalid Key", nameof (commands));
+            return;
+        }
+        
+        if (commands.Length == 0)
+        {
+            throw new ArgumentException (@"At least one command must be specified", nameof (commands));
+        }
 
-	/// <summary>
-	/// Removes all <see cref="KeyBinding"/> objects from the collection.
-	/// </summary>
-	public void Clear () => Bindings.Clear ();
+        if (TryGet (key, out KeyBinding _))
+        {
+            Bindings [key] = new KeyBinding (commands, scope);
+        }
+        else
+        {
+            Bindings.Add (key, new KeyBinding (commands, scope));
+        }
+    }
 
-	/// <summary>
-	/// <para>
-	/// Adds a new key combination that will trigger the commands in <paramref name="commands"/>.
-	/// </para>
-	/// <para>
-	/// If the key is already bound to a different array of <see cref="Command"/>s it will be
-	/// rebound <paramref name="commands"/>.</para>
-	/// </summary>
-	/// <remarks>
-	/// Commands are only ever applied to the current <see cref="View"/> (i.e. this feature
-	/// cannot be used to switch focus to another view and perform multiple commands there).
-	/// </remarks>
-	/// <param name="key">
-	/// The key to check.
-	/// </param>
-	/// <param name="scope">The scope for the command.</param>
-	/// <param name="commands">The command to invoked on the <see cref="View"/> when <paramref name="key"/> is pressed.
-	/// When multiple commands are provided,they will be applied in sequence. The bound <paramref name="key"/> strike
-	/// will be consumed if any took effect.</param>
-	public void Add (Key key, KeyBindingScope scope, params Command [] commands)
-	{
-		if (commands.Length == 0) {
-			throw new ArgumentException (@"At least one command must be specified", nameof (commands));
-		}
+    /// <summary>
+    ///     <para>
+    ///         Adds a new key combination that will trigger the commands in <paramref name="commands"/> (if supported by the
+    ///         View - see <see cref="View.GetSupportedCommands"/>).
+    ///     </para>
+    ///     <para>
+    ///         This is a helper function for <see cref="Add(Key,KeyBindingScope,Terminal.Gui.Command[])"/> for
+    ///         <see cref="KeyBindingScope.Focused"/> scoped commands.
+    ///     </para>
+    ///     <para>
+    ///         If the key is already bound to a different array of <see cref="Command"/>s it will be rebound
+    ///         <paramref name="commands"/>.
+    ///     </para>
+    /// </summary>
+    /// <remarks>
+    ///     Commands are only ever applied to the current <see cref="View"/> (i.e. this feature cannot be used to switch
+    ///     focus to another view and perform multiple commands there).
+    /// </remarks>
+    /// <param name="key">The key to check.</param>
+    /// <param name="commands">
+    ///     The command to invoked on the <see cref="View"/> when <paramref name="key"/> is pressed. When
+    ///     multiple commands are provided,they will be applied in sequence. The bound <paramref name="key"/> strike will be
+    ///     consumed if any took effect.
+    /// </param>
+    public void Add (Key key, params Command [] commands) { Add (key, KeyBindingScope.Focused, commands); }
 
-		if (key == null || !key.IsValid) {
-			//throw new ArgumentException ("Invalid Key", nameof (commands));
-			return;
-		}
+    /// <summary>Removes all <see cref="KeyBinding"/> objects from the collection.</summary>
+    public void Clear () { Bindings.Clear (); }
 
-		if (TryGet (key, out var _)) {
-			Bindings [key] = new KeyBinding (commands, scope);
-		} else {
-			Bindings.Add (key, new KeyBinding (commands, scope));
-		}
-	}
+    /// <summary>
+    ///     Removes all key bindings that trigger the given command set. Views can have multiple different keys bound to
+    ///     the same command sets and this method will clear all of them.
+    /// </summary>
+    /// <param name="command"></param>
+    public void Clear (params Command [] command)
+    {
+        var kvps = Bindings
+                   .Where (kvp => kvp.Value.Commands.SequenceEqual (command))
+                   .ToArray ();
+        foreach (KeyValuePair<Key, KeyBinding> kvp in kvps)
+        {
+            Bindings.Remove (kvp.Key);
+        }
+    }
 
-	/// <summary>
-	/// <para>
-	/// Adds a new key combination that will trigger the commands in <paramref name="commands"/>
-	/// (if supported by the View - see <see cref="View.GetSupportedCommands"/>).
-	/// </para>
-	/// <para>
-	/// This is a helper function for <see cref="Add(Key,KeyBindingScope,Terminal.Gui.Command[])"/>
-	/// for <see cref="KeyBindingScope.Focused"/> scoped commands.
-	/// </para>
-	/// <para>
-	/// If the key is already bound to a different array of <see cref="Command"/>s it will be
-	/// rebound <paramref name="commands"/>.</para>
-	/// </summary>
-	/// <remarks>
-	/// Commands are only ever applied to the current <see cref="View"/> (i.e. this feature
-	/// cannot be used to switch focus to another view and perform multiple commands there).
-	/// </remarks>
-	/// <param name="key">
-	/// The key to check.
-	/// </param>
-	/// <param name="commands">The command to invoked on the <see cref="View"/> when <paramref name="key"/> is pressed.
-	/// When multiple commands are provided,they will be applied in sequence. The bound <paramref name="key"/> strike
-	/// will be consumed if any took effect.</param>
-	public void Add (Key key, params Command [] commands) => Add (key, KeyBindingScope.Focused, commands);
+    /// <summary>Gets the <see cref="KeyBinding"/> for the specified <see cref="Key"/>.</summary>
+    /// <param name="key"></param>
+    /// <returns></returns>
+    public KeyBinding Get (Key key) { return TryGet (key, out KeyBinding binding) ? binding : null; }
 
-	/// <summary>
-	/// Replaces a key combination already bound to a set of <see cref="Command"/>s.
-	/// </summary>
-	/// <remarks>
-	/// </remarks>
-	/// <param name="fromKey">The key to be replaced.</param>
-	/// <param name="toKey">The new key to be used.</param>
-	public void Replace (Key fromKey, Key toKey)
-	{
-		if (!TryGet (fromKey, out var _)) {
-			return;
-		}
-		var value = Bindings [fromKey];
-		Bindings.Remove (fromKey);
-		Bindings [toKey] = value;
-	}
+    /// <summary>Gets the <see cref="KeyBinding"/> for the specified <see cref="Key"/>.</summary>
+    /// <param name="key"></param>
+    /// <param name="scope"></param>
+    /// <returns></returns>
+    public KeyBinding Get (Key key, KeyBindingScope scope) { return TryGet (key, scope, out KeyBinding binding) ? binding : null; }
 
-	/// <summary>
-	/// Gets the commands bound with the specified Key.
-	/// </summary>
-	/// <remarks>
-	/// </remarks>
-	/// <param name="key">
-	/// The key to check.
-	/// </param>
-	/// <param name="binding">
-	/// When this method returns, contains the commands bound with the specified Key, if the Key is found;
-	/// otherwise, null. This parameter is passed uninitialized.
-	/// </param>
-	/// <returns>
-	/// <see langword="true"/> if the Key is bound; otherwise <see langword="false"/>.
-	/// </returns>
-	public bool TryGet (Key key, out KeyBinding binding)
-	{
-		if (key.IsValid) {
-			return Bindings.TryGetValue (key, out binding);
-		}
-		binding = new KeyBinding (Array.Empty<Command> (), KeyBindingScope.Focused);
-		return false;
-	}
+    /// <summary>Gets the array of <see cref="Command"/>s bound to <paramref name="key"/> if it exists.</summary>
+    /// <param name="key">The key to check.</param>
+    /// <returns>
+    ///     The array of <see cref="Command"/>s if <paramref name="key"/> is bound. An empty <see cref="Command"/> array
+    ///     if not.
+    /// </returns>
+    public Command [] GetCommands (Key key)
+    {
+        if (TryGet (key, out KeyBinding bindings))
+        {
+            return bindings.Commands;
+        }
 
-	/// <summary>
-	/// Gets the <see cref="KeyBinding"/> for the specified <see cref="Key"/>.
-	/// </summary>
-	/// <param name="key"></param>
-	/// <returns></returns>
-	public KeyBinding Get (Key key) => TryGet (key, out var binding) ? binding : null;
+        return Array.Empty<Command> ();
+    }
 
-	/// <summary>
-	/// Gets the commands bound with the specified Key that are scoped to a particular scope.
-	/// </summary>
-	/// <remarks>
-	/// </remarks>
-	/// <param name="key">
-	/// The key to check.
-	/// </param>
-	/// <param name="scope">the scope to filter on</param>
-	/// <param name="binding">
-	/// When this method returns, contains the commands bound with the specified Key, if the Key is found;
-	/// otherwise, null. This parameter is passed uninitialized.
-	/// </param>
-	/// <returns>
-	/// <see langword="true"/> if the Key is bound; otherwise <see langword="false"/>.
-	/// </returns>
-	public bool TryGet (Key key, KeyBindingScope scope, out KeyBinding binding)
-	{
-		if (key.IsValid && Bindings.TryGetValue (key, out binding)) {
-			if (binding.Scope == scope) {
-				return true;
-			}
-		}
-		binding = new KeyBinding (Array.Empty<Command> (), KeyBindingScope.Focused);
-		return false;
-	}
+    /// <summary>Gets the Key used by a set of commands.</summary>
+    /// <remarks></remarks>
+    /// <param name="commands">The set of commands to search.</param>
+    /// <returns>The <see cref="Key"/> used by a <see cref="Command"/></returns>
+    /// <exception cref="InvalidOperationException">If no matching set of commands was found.</exception>
+    public Key GetKeyFromCommands (params Command [] commands) { return Bindings.First (a => a.Value.Commands.SequenceEqual (commands)).Key; }
 
-	/// <summary>
-	/// Gets the <see cref="KeyBinding"/> for the specified <see cref="Key"/>.
-	/// </summary>
-	/// <param name="key"></param>
-	/// <param name="scope"></param>
-	/// <returns></returns>
-	public KeyBinding Get (Key key, KeyBindingScope scope) => TryGet (key, scope, out var binding) ? binding : null;
+    /// <summary>Removes a <see cref="KeyBinding"/> from the collection.</summary>
+    /// <param name="key"></param>
+    public void Remove (Key key) { Bindings.Remove (key); }
 
-	/// <summary>
-	/// Gets the array of <see cref="Command"/>s bound to <paramref name="key"/> if it exists.
-	/// </summary>
-	/// <param name="key">
-	/// The key to check.
-	/// </param>
-	/// <returns>The array of <see cref="Command"/>s if <paramref name="key"/> is bound. An empty <see cref="Command"/> array if not.</returns>
-	public Command [] GetCommands (Key key)
-	{
-		if (TryGet (key, out var bindings)) {
-			return bindings.Commands;
-		}
-		return Array.Empty<Command> ();
-	}
+    /// <summary>Replaces a key combination already bound to a set of <see cref="Command"/>s.</summary>
+    /// <remarks></remarks>
+    /// <param name="fromKey">The key to be replaced.</param>
+    /// <param name="toKey">The new key to be used.</param>
+    public void Replace (Key fromKey, Key toKey)
+    {
+        if (!TryGet (fromKey, out KeyBinding _))
+        {
+            return;
+        }
 
-	/// <summary>
-	/// Removes all key bindings that trigger the given command set. Views can have multiple different
-	/// keys bound to the same command sets and this method will clear all of them.
-	/// </summary>
-	/// <param name="command"></param>
-	public void Clear (params Command [] command)
-	{
-		foreach (var kvp in Bindings.Where (kvp => kvp.Value.Commands.SequenceEqual (command)).ToArray ()) {
-			Bindings.Remove (kvp.Key);
-		}
-	}
+        KeyBinding value = Bindings [fromKey];
+        Bindings.Remove (fromKey);
+        Bindings [toKey] = value;
+    }
 
-	/// <summary>
-	/// Gets the Key used by a set of commands.
-	/// </summary>
-	/// <remarks>
-	/// </remarks>
-	/// <param name="commands">The set of commands to search.</param>
-	/// <returns>The <see cref="Key"/> used by a <see cref="Command"/></returns>
-	/// <exception cref="InvalidOperationException">If no matching set of commands was found.</exception>
-	public Key GetKeyFromCommands (params Command [] commands)
-	{
-		return Bindings.First (a => a.Value.Commands.SequenceEqual (commands)).Key;
-	}
-}
+    /// <summary>Gets the commands bound with the specified Key.</summary>
+    /// <remarks></remarks>
+    /// <param name="key">The key to check.</param>
+    /// <param name="binding">
+    ///     When this method returns, contains the commands bound with the specified Key, if the Key is
+    ///     found; otherwise, null. This parameter is passed uninitialized.
+    /// </param>
+    /// <returns><see langword="true"/> if the Key is bound; otherwise <see langword="false"/>.</returns>
+    public bool TryGet (Key key, out KeyBinding binding)
+    {
+        if (key.IsValid)
+        {
+            return Bindings.TryGetValue (key, out binding);
+        }
 
+        binding = new KeyBinding (Array.Empty<Command> (), KeyBindingScope.Focused);
+
+        return false;
+    }
+
+    /// <summary>Gets the commands bound with the specified Key that are scoped to a particular scope.</summary>
+    /// <remarks></remarks>
+    /// <param name="key">The key to check.</param>
+    /// <param name="scope">the scope to filter on</param>
+    /// <param name="binding">
+    ///     When this method returns, contains the commands bound with the specified Key, if the Key is
+    ///     found; otherwise, null. This parameter is passed uninitialized.
+    /// </param>
+    /// <returns><see langword="true"/> if the Key is bound; otherwise <see langword="false"/>.</returns>
+    public bool TryGet (Key key, KeyBindingScope scope, out KeyBinding binding)
+    {
+        if (key.IsValid && Bindings.TryGetValue (key, out binding))
+        {
+            if (scope.HasFlag (binding.Scope))
+            {
+                return true;
+            }
+        }
+
+        binding = new KeyBinding (Array.Empty<Command> (), KeyBindingScope.Focused);
+
+        return false;
+    }
+}

+ 17 - 27
Terminal.Gui/Input/KeyChangedEventArgs.cs

@@ -1,33 +1,23 @@
-using System;
-
-namespace Terminal.Gui;
+namespace Terminal.Gui;
 
 /// <summary>
-/// Event args for when a <see cref="Key"/> is changed from
-/// one value to a new value (e.g. in <see cref="View.HotKeyChanged"/>)
+///     Event args for when a <see cref="Key"/> is changed from one value to a new value (e.g. in
+///     <see cref="View.HotKeyChanged"/>)
 /// </summary>
-public class KeyChangedEventArgs : EventArgs {
-
-	/// <summary>
-	/// Gets the old <see cref="Key"/> that was set before the event.
-	/// Use <see cref="Key.Empty"/> to check for empty.
-	/// </summary>
-	public Key OldKey { get; }
+public class KeyChangedEventArgs : EventArgs
+{
+    /// <summary>Creates a new instance of the <see cref="KeyChangedEventArgs"/> class</summary>
+    /// <param name="oldKey"></param>
+    /// <param name="newKey"></param>
+    public KeyChangedEventArgs (Key oldKey, Key newKey)
+    {
+        OldKey = oldKey;
+        NewKey = newKey;
+    }
 
-	/// <summary>
-	/// Gets the new <see cref="Key"/> that is being used.
-	/// Use <see cref="Key.Empty"/> to check for empty.
-	/// </summary>
-	public Key NewKey { get; }
+    /// <summary>Gets the new <see cref="Key"/> that is being used. Use <see cref="Key.Empty"/> to check for empty.</summary>
+    public Key NewKey { get; }
 
-	/// <summary>
-	/// Creates a new instance of the <see cref="KeyChangedEventArgs"/> class
-	/// </summary>
-	/// <param name="oldKey"></param>
-	/// <param name="newKey"></param>
-	public KeyChangedEventArgs (Key oldKey, Key newKey)
-	{
-		this.OldKey = oldKey;
-		this.NewKey = newKey;
-	}
+    /// <summary>Gets the old <see cref="Key"/> that was set before the event. Use <see cref="Key.Empty"/> to check for empty.</summary>
+    public Key OldKey { get; }
 }

+ 9 - 19
Terminal.Gui/Input/KeystrokeNavigatorEventArgs.cs

@@ -1,22 +1,12 @@
-using System;
+namespace Terminal.Gui;
 
-namespace Terminal.Gui {
-	/// <summary>
-	/// Event arguments for the <see cref="CollectionNavigatorBase.SearchStringChanged"/> event.
-	/// </summary>
-	public class KeystrokeNavigatorEventArgs : EventArgs {
-		/// <summary>
-		/// he current <see cref="SearchString"/>.
-		/// </summary>
-		public string SearchString { get; }
+/// <summary>Event arguments for the <see cref="CollectionNavigatorBase.SearchStringChanged"/> event.</summary>
+public class KeystrokeNavigatorEventArgs : EventArgs
+{
+    /// <summary>Initializes a new instance of <see cref="KeystrokeNavigatorEventArgs"/></summary>
+    /// <param name="searchString">The current <see cref="SearchString"/>.</param>
+    public KeystrokeNavigatorEventArgs (string searchString) { SearchString = searchString; }
 
-		/// <summary>
-		/// Initializes a new instance of <see cref="KeystrokeNavigatorEventArgs"/>
-		/// </summary>
-		/// <param name="searchString">The current <see cref="SearchString"/>.</param>
-		public KeystrokeNavigatorEventArgs (string searchString)
-		{
-			SearchString = searchString;
-		}
-	}
+    /// <summary>he current <see cref="SearchString"/>.</summary>
+    public string SearchString { get; }
 }

+ 126 - 175
Terminal.Gui/Input/Mouse.cs

@@ -1,185 +1,136 @@
-using System;
-
 namespace Terminal.Gui;
 
-/// <summary>
-/// Mouse flags reported in <see cref="MouseEvent"/>.
-/// </summary>
-/// <remarks>
-/// They just happen to map to the ncurses ones.
-/// </remarks>
+/// <summary>Mouse flags reported in <see cref="MouseEvent"/>.</summary>
+/// <remarks>They just happen to map to the ncurses ones.</remarks>
 [Flags]
-public enum MouseFlags {
-	/// <summary>
-	/// The first mouse button was pressed.
-	/// </summary>
-	Button1Pressed = unchecked((int)0x2),
-	/// <summary>
-	/// The first mouse button was released.
-	/// </summary>
-	Button1Released = unchecked((int)0x1),
-	/// <summary>
-	/// The first mouse button was clicked (press+release).
-	/// </summary>
-	Button1Clicked = unchecked((int)0x4),
-	/// <summary>
-	/// The first mouse button was double-clicked.
-	/// </summary>
-	Button1DoubleClicked = unchecked((int)0x8),
-	/// <summary>
-	/// The first mouse button was triple-clicked.
-	/// </summary>
-	Button1TripleClicked = unchecked((int)0x10),
-	/// <summary>
-	/// The second mouse button was pressed.
-	/// </summary>
-	Button2Pressed = unchecked((int)0x80),
-	/// <summary>
-	/// The second mouse button was released.
-	/// </summary>
-	Button2Released = unchecked((int)0x40),
-	/// <summary>
-	/// The second mouse button was clicked (press+release).
-	/// </summary>
-	Button2Clicked = unchecked((int)0x100),
-	/// <summary>
-	/// The second mouse button was double-clicked.
-	/// </summary>
-	Button2DoubleClicked = unchecked((int)0x200),
-	/// <summary>
-	/// The second mouse button was triple-clicked.
-	/// </summary>
-	Button2TripleClicked = unchecked((int)0x400),
-	/// <summary>
-	/// The third mouse button was pressed.
-	/// </summary>
-	Button3Pressed = unchecked((int)0x2000),
-	/// <summary>
-	/// The third mouse button was released.
-	/// </summary>
-	Button3Released = unchecked((int)0x1000),
-	/// <summary>
-	/// The third mouse button was clicked (press+release).
-	/// </summary>
-	Button3Clicked = unchecked((int)0x4000),
-	/// <summary>
-	/// The third mouse button was double-clicked.
-	/// </summary>
-	Button3DoubleClicked = unchecked((int)0x8000),
-	/// <summary>
-	/// The third mouse button was triple-clicked.
-	/// </summary>
-	Button3TripleClicked = unchecked((int)0x10000),
-	/// <summary>
-	/// The fourth mouse button was pressed.
-	/// </summary>
-	Button4Pressed = unchecked((int)0x80000),
-	/// <summary>
-	/// The fourth mouse button was released.
-	/// </summary>
-	Button4Released = unchecked((int)0x40000),
-	/// <summary>
-	/// The fourth button was clicked (press+release).
-	/// </summary>
-	Button4Clicked = unchecked((int)0x100000),
-	/// <summary>
-	/// The fourth button was double-clicked.
-	/// </summary>
-	Button4DoubleClicked = unchecked((int)0x200000),
-	/// <summary>
-	/// The fourth button was triple-clicked.
-	/// </summary>
-	Button4TripleClicked = unchecked((int)0x400000),
-	/// <summary>
-	/// Flag: the shift key was pressed when the mouse button took place.
-	/// </summary>
-	ButtonShift = unchecked((int)0x2000000),
-	/// <summary>
-	/// Flag: the ctrl key was pressed when the mouse button took place.
-	/// </summary>
-	ButtonCtrl = unchecked((int)0x1000000),
-	/// <summary>
-	/// Flag: the alt key was pressed when the mouse button took place.
-	/// </summary>
-	ButtonAlt = unchecked((int)0x4000000),
-	/// <summary>
-	/// The mouse position is being reported in this event.
-	/// </summary>
-	ReportMousePosition = unchecked((int)0x8000000),
-	/// <summary>
-	/// Vertical button wheeled up.
-	/// </summary>
-	WheeledUp = unchecked((int)0x10000000),
-	/// <summary>
-	/// Vertical button wheeled down.
-	/// </summary>
-	WheeledDown = unchecked((int)0x20000000),
-	/// <summary>
-	/// Vertical button wheeled up while pressing ButtonShift.
-	/// </summary>
-	WheeledLeft = ButtonShift | WheeledUp,
-	/// <summary>
-	/// Vertical button wheeled down while pressing ButtonShift.
-	/// </summary>
-	WheeledRight = ButtonShift | WheeledDown,
-	/// <summary>
-	/// Mask that captures all the events.
-	/// </summary>
-	AllEvents = unchecked((int)0x7ffffff),
+public enum MouseFlags
+{
+    /// <summary>The first mouse button was pressed.</summary>
+    Button1Pressed = 0x2,
+
+    /// <summary>The first mouse button was released.</summary>
+    Button1Released = 0x1,
+
+    /// <summary>The first mouse button was clicked (press+release).</summary>
+    Button1Clicked = 0x4,
+
+    /// <summary>The first mouse button was double-clicked.</summary>
+    Button1DoubleClicked = 0x8,
+
+    /// <summary>The first mouse button was triple-clicked.</summary>
+    Button1TripleClicked = 0x10,
+
+    /// <summary>The second mouse button was pressed.</summary>
+    Button2Pressed = 0x80,
+
+    /// <summary>The second mouse button was released.</summary>
+    Button2Released = 0x40,
+
+    /// <summary>The second mouse button was clicked (press+release).</summary>
+    Button2Clicked = 0x100,
+
+    /// <summary>The second mouse button was double-clicked.</summary>
+    Button2DoubleClicked = 0x200,
+
+    /// <summary>The second mouse button was triple-clicked.</summary>
+    Button2TripleClicked = 0x400,
+
+    /// <summary>The third mouse button was pressed.</summary>
+    Button3Pressed = 0x2000,
+
+    /// <summary>The third mouse button was released.</summary>
+    Button3Released = 0x1000,
+
+    /// <summary>The third mouse button was clicked (press+release).</summary>
+    Button3Clicked = 0x4000,
+
+    /// <summary>The third mouse button was double-clicked.</summary>
+    Button3DoubleClicked = 0x8000,
+
+    /// <summary>The third mouse button was triple-clicked.</summary>
+    Button3TripleClicked = 0x10000,
+
+    /// <summary>The fourth mouse button was pressed.</summary>
+    Button4Pressed = 0x80000,
+
+    /// <summary>The fourth mouse button was released.</summary>
+    Button4Released = 0x40000,
+
+    /// <summary>The fourth button was clicked (press+release).</summary>
+    Button4Clicked = 0x100000,
+
+    /// <summary>The fourth button was double-clicked.</summary>
+    Button4DoubleClicked = 0x200000,
+
+    /// <summary>The fourth button was triple-clicked.</summary>
+    Button4TripleClicked = 0x400000,
+
+    /// <summary>Flag: the shift key was pressed when the mouse button took place.</summary>
+    ButtonShift = 0x2000000,
+
+    /// <summary>Flag: the ctrl key was pressed when the mouse button took place.</summary>
+    ButtonCtrl = 0x1000000,
+
+    /// <summary>Flag: the alt key was pressed when the mouse button took place.</summary>
+    ButtonAlt = 0x4000000,
+
+    /// <summary>The mouse position is being reported in this event.</summary>
+    ReportMousePosition = 0x8000000,
+
+    /// <summary>Vertical button wheeled up.</summary>
+    WheeledUp = 0x10000000,
+
+    /// <summary>Vertical button wheeled down.</summary>
+    WheeledDown = 0x20000000,
+
+    /// <summary>Vertical button wheeled up while pressing ButtonShift.</summary>
+    WheeledLeft = ButtonShift | WheeledUp,
+
+    /// <summary>Vertical button wheeled down while pressing ButtonShift.</summary>
+    WheeledRight = ButtonShift | WheeledDown,
+
+    /// <summary>Mask that captures all the events.</summary>
+    AllEvents = 0x7ffffff
 }
 
 // TODO: Merge MouseEvent and MouseEventEventArgs into a single class.
 
 /// <summary>
-/// Low-level construct that conveys the details of mouse events, such
-/// as coordinates and button state, from ConsoleDrivers up to <see cref="Application"/> and
-/// Views.
+///     Low-level construct that conveys the details of mouse events, such as coordinates and button state, from
+///     ConsoleDrivers up to <see cref="Application"/> and Views.
 /// </summary>
-/// <remarks>The <see cref="Application"/> class includes the <see cref="Application.MouseEvent"/>
-/// Action which takes a MouseEvent argument.</remarks>
-public class MouseEvent {
-	/// <summary>
-	/// The X (column) location for the mouse event.
-	/// </summary>
-	public int X { get; set; }
-
-	/// <summary>
-	/// The Y (column) location for the mouse event.
-	/// </summary>
-	public int Y { get; set; }
-
-	/// <summary>
-	/// Flags indicating the kind of mouse event that is being posted.
-	/// </summary>
-	public MouseFlags Flags { get; set; }
-
-	/// <summary>
-	/// The offset X (column) location for the mouse event.
-	/// </summary>
-	public int OfX { get; set; }
-
-	/// <summary>
-	/// The offset Y (column) location for the mouse event.
-	/// </summary>
-	public int OfY { get; set; }
-
-	/// <summary>
-	/// The current view at the location for the mouse event.
-	/// </summary>
-	public View View { get; set; }
-
-	/// <summary>
-	/// Indicates if the current mouse event has already been processed and the driver should stop notifying any other event subscriber.
-	/// Its important to set this value to true specially when updating any View's layout from inside the subscriber method.
-	/// </summary>
-	public bool Handled { get; set; }
-
-	/// <summary>
-	/// Returns a <see cref="T:System.String"/> that represents the current <see cref="MouseEvent"/>.
-	/// </summary>
-	/// <returns>A <see cref="T:System.String"/> that represents the current <see cref="MouseEvent"/>.</returns>
-	public override string ToString ()
-	{
-		return $"({X},{Y}):{Flags}";
-	}
+/// <remarks>
+///     The <see cref="Application"/> class includes the <see cref="Application.MouseEvent"/> Action which takes a
+///     MouseEvent argument.
+/// </remarks>
+public class MouseEvent
+{
+    /// <summary>Flags indicating the kind of mouse event that is being posted.</summary>
+    public MouseFlags Flags { get; set; }
+
+    /// <summary>
+    ///     Indicates if the current mouse event has already been processed and the driver should stop notifying any other
+    ///     event subscriber. Its important to set this value to true specially when updating any View's layout from inside the
+    ///     subscriber method.
+    /// </summary>
+    public bool Handled { get; set; }
+
+    /// <summary>The offset X (column) location for the mouse event.</summary>
+    public int OfX { get; set; }
+
+    /// <summary>The offset Y (column) location for the mouse event.</summary>
+    public int OfY { get; set; }
+
+    /// <summary>The current view at the location for the mouse event.</summary>
+    public View View { get; set; }
+
+    /// <summary>The X (column) location for the mouse event.</summary>
+    public int X { get; set; }
+
+    /// <summary>The Y (column) location for the mouse event.</summary>
+    public int Y { get; set; }
+
+    /// <summary>Returns a <see cref="T:System.String"/> that represents the current <see cref="MouseEvent"/>.</summary>
+    /// <returns>A <see cref="T:System.String"/> that represents the current <see cref="MouseEvent"/>.</returns>
+    public override string ToString () { return $"({X},{Y}):{Flags}"; }
 }

+ 28 - 29
Terminal.Gui/Input/MouseEventEventArgs.cs

@@ -1,33 +1,32 @@
-using System;
+namespace Terminal.Gui;
 
-namespace Terminal.Gui {
-	/// <summary>
-	/// Specifies the event arguments for <see cref="Terminal.Gui.MouseEvent"/>. This is a higher-level construct
-	/// than the wrapped <see cref="MouseEvent"/> class and is used for the events defined on <see cref="View"/>
-	/// and subclasses of View (e.g. <see cref="View.MouseEnter"/> and <see cref="View.MouseClick"/>).
-	/// </summary>
-	public class MouseEventEventArgs : EventArgs {
-		/// <summary>
-		/// Constructs.
-		/// </summary>
-		/// <param name="me">The mouse event.</param>
-		public MouseEventEventArgs (MouseEvent me) => MouseEvent = me;
+/// <summary>
+///     Specifies the event arguments for <see cref="Terminal.Gui.MouseEvent"/>. This is a higher-level construct than
+///     the wrapped <see cref="MouseEvent"/> class and is used for the events defined on <see cref="View"/> and subclasses
+///     of View (e.g. <see cref="View.MouseEnter"/> and <see cref="View.MouseClick"/>).
+/// </summary>
+public class MouseEventEventArgs : EventArgs
+{
+    /// <summary>Constructs.</summary>
+    /// <param name="me">The mouse event.</param>
+    public MouseEventEventArgs (MouseEvent me) { MouseEvent = me; }
 
-		// TODO: Merge MouseEvent and MouseEventEventArgs into a single class.
-		/// <summary>
-		/// The <see cref="Terminal.Gui.MouseEvent"/> for the event.
-		/// </summary>
-		public MouseEvent MouseEvent { get; set; }
+    /// <summary>
+    ///     Indicates if the current mouse event has already been processed and the driver should stop notifying any other
+    ///     event subscriber. Its important to set this value to true specially when updating any View's layout from inside the
+    ///     subscriber method.
+    /// </summary>
+    /// <remarks>
+    ///     This property forwards to the <see cref="MouseEvent.Handled"/> property and is provided as a convenience and
+    ///     for backwards compatibility
+    /// </remarks>
+    public bool Handled
+    {
+        get => MouseEvent.Handled;
+        set => MouseEvent.Handled = value;
+    }
 
-		/// <summary>
-		/// Indicates if the current mouse event has already been processed and the driver should stop notifying any other event subscriber.
-		/// Its important to set this value to true specially when updating any View's layout from inside the subscriber method.
-		/// </summary>
-		/// <remarks>This property forwards to the <see cref="MouseEvent.Handled"/> property and is provided as a convenience and for
-		/// backwards compatibility</remarks>
-		public bool Handled {
-			get => MouseEvent.Handled;
-			set => MouseEvent.Handled = value;
-		}
-	}
+    // TODO: Merge MouseEvent and MouseEventEventArgs into a single class.
+    /// <summary>The <see cref="Terminal.Gui.MouseEvent"/> for the event.</summary>
+    public MouseEvent MouseEvent { get; set; }
 }

+ 16 - 26
Terminal.Gui/Input/MouseFlagsChangedEventArgs.cs

@@ -1,30 +1,20 @@
-using System;
+namespace Terminal.Gui;
 
-namespace Terminal.Gui {
-	/// <summary>
-	/// Args for events that describe a change in <see cref="MouseFlags"/>
-	/// </summary>
-	public class MouseFlagsChangedEventArgs : EventArgs {
+/// <summary>Args for events that describe a change in <see cref="MouseFlags"/></summary>
+public class MouseFlagsChangedEventArgs : EventArgs
+{
+    /// <summary>Creates a new instance of the <see cref="MouseFlagsChangedEventArgs"/> class.</summary>
+    /// <param name="oldValue"></param>
+    /// <param name="newValue"></param>
+    public MouseFlagsChangedEventArgs (MouseFlags oldValue, MouseFlags newValue)
+    {
+        OldValue = oldValue;
+        NewValue = newValue;
+    }
 
-		/// <summary>
-		/// Creates a new instance of the <see cref="MouseFlagsChangedEventArgs"/> class.
-		/// </summary>
-		/// <param name="oldValue"></param>
-		/// <param name="newValue"></param>
-		public MouseFlagsChangedEventArgs (MouseFlags oldValue, MouseFlags newValue)
-		{
-			OldValue = oldValue;
-			NewValue = newValue;
-		}
+    /// <summary>The new value</summary>
+    public MouseFlags NewValue { get; }
 
-		/// <summary>
-		/// The old value before event
-		/// </summary>
-		public MouseFlags OldValue { get; }
-
-		/// <summary>
-		/// The new value
-		/// </summary>
-		public MouseFlags NewValue { get; }
-	}
+    /// <summary>The old value before event</summary>
+    public MouseFlags OldValue { get; }
 }

+ 9 - 20
Terminal.Gui/Input/PointEventArgs.cs

@@ -1,23 +1,12 @@
-using System;
+namespace Terminal.Gui;
 
-namespace Terminal.Gui {
-	/// <summary>
-	/// Event args for events which relate to a single <see cref="Point"/>
-	/// </summary>
-	public class PointEventArgs : EventArgs {
+/// <summary>Event args for events which relate to a single <see cref="Point"/></summary>
+public class PointEventArgs : EventArgs
+{
+    /// <summary>Creates a new instance of the <see cref="PointEventArgs"/> class</summary>
+    /// <param name="p"></param>
+    public PointEventArgs (Point p) { Point = p; }
 
-		/// <summary>
-		/// Creates a new instance of the <see cref="PointEventArgs"/> class
-		/// </summary>
-		/// <param name="p"></param>
-		public PointEventArgs (Point p)
-		{
-			this.Point = p;
-		}
-
-		/// <summary>
-		/// The point the event happened at
-		/// </summary>
-		public Point Point { get; }
-	}
+    /// <summary>The point the event happened at</summary>
+    public Point Point { get; }
 }

+ 139 - 179
Terminal.Gui/Input/Responder.cs

@@ -13,193 +13,153 @@
 // Optimziations
 //   - Add rendering limitation to the exposed area
 
-using System;
-using System.Collections.Generic;
-using System.Linq;
 using System.Reflection;
 
 namespace Terminal.Gui;
-/// <summary>
-/// Responder base class implemented by objects that want to participate on keyboard and mouse input.
-/// </summary>
-public class Responder : IDisposable {
-	bool disposedValue;
 
+/// <summary>Responder base class implemented by objects that want to participate on keyboard and mouse input.</summary>
+public class Responder : IDisposable
+{
+    private bool disposedValue;
+
+    /// <summary>Gets or sets a value indicating whether this <see cref="Responder"/> can focus.</summary>
+    /// <value><c>true</c> if can focus; otherwise, <c>false</c>.</value>
+    public virtual bool CanFocus { get; set; }
+
+    /// <summary>Gets or sets a value indicating whether this <see cref="Responder"/> can respond to user interaction.</summary>
+    public virtual bool Enabled { get; set; } = true;
+
+    /// <summary>Gets or sets a value indicating whether this <see cref="Responder"/> has focus.</summary>
+    /// <value><c>true</c> if has focus; otherwise, <c>false</c>.</value>
+    public virtual bool HasFocus { get; }
+
+    /// <summary>Gets or sets a value indicating whether this <see cref="Responder"/> and all its child controls are displayed.</summary>
+    public virtual bool Visible { get; set; } = true;
+
+    /// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resource.</summary>
+    public void Dispose ()
+    {
+        // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
+        Disposing?.Invoke (this, EventArgs.Empty);
+        Dispose (true);
+        GC.SuppressFinalize (this);
 #if DEBUG_IDISPOSABLE
-	/// <summary>
-	/// For debug purposes to verify objects are being disposed properly
-	/// </summary>
-	public bool WasDisposed = false;
-	/// <summary>
-	/// For debug purposes to verify objects are being disposed properly
-	/// </summary>
-	public int DisposedCount = 0;
-	/// <summary>
-	/// For debug purposes
-	/// </summary>
-	public static List<Responder> Instances = new List<Responder> ();
-	/// <summary>
-	/// For debug purposes
-	/// </summary>
-	public Responder ()
-	{
-		Instances.Add (this);
-	}
+        WasDisposed = true;
+
+        foreach (Responder instance in Instances.Where (x => x.WasDisposed).ToList ())
+        {
+            Instances.Remove (instance);
+        }
 #endif
+    }
+
+    /// <summary>Event raised when <see cref="Dispose()"/> has been called to signal that this object is being disposed.</summary>
+    public event EventHandler Disposing;
+
+    /// <summary>Method invoked when a mouse event is generated</summary>
+    /// <remarks>
+    /// The coordinates are relative to <see cref="View.Bounds"/>.
+    /// </remarks>
+    /// <returns><c>true</c>, if the event was handled, <c>false</c> otherwise.</returns>
+    /// <param name="mouseEvent">Contains the details about the mouse event.</param>
+    public virtual bool MouseEvent (MouseEvent mouseEvent) { return false; }
+
+    /// <summary>Method invoked when the <see cref="CanFocus"/> property from a view is changed.</summary>
+    public virtual void OnCanFocusChanged () { }
+
+    /// <summary>Method invoked when the <see cref="Enabled"/> property from a view is changed.</summary>
+    public virtual void OnEnabledChanged () { }
+
+    /// <summary>Method invoked when a view gets focus.</summary>
+    /// <param name="view">The view that is losing focus.</param>
+    /// <returns><c>true</c>, if the event was handled, <c>false</c> otherwise.</returns>
+    public virtual bool OnEnter (View view) { return false; }
+
+    /// <summary>Method invoked when a view loses focus.</summary>
+    /// <param name="view">The view that is getting focus.</param>
+    /// <returns><c>true</c>, if the event was handled, <c>false</c> otherwise.</returns>
+    public virtual bool OnLeave (View view) { return false; }
+
+    /// <summary>
+    ///     Called when the mouse first enters the view; the view will now receives mouse events until the mouse leaves
+    ///     the view. At which time, <see cref="OnMouseLeave(Gui.MouseEvent)"/> will be called.
+    /// </summary>
+    /// <remarks>
+    /// The coordinates are relative to <see cref="View.Bounds"/>.
+    /// </remarks>
+    /// <param name="mouseEvent"></param>
+    /// <returns><c>true</c>, if the event was handled, <c>false</c> otherwise.</returns>
+    public virtual bool OnMouseEnter (MouseEvent mouseEvent) { return false; }
+
+    /// <summary>
+    ///     Called when the mouse has moved outside of the view; the view will no longer receive mouse events (until the
+    ///     mouse moves within the view again and <see cref="OnMouseEnter(Gui.MouseEvent)"/> is called).
+    /// </summary>
+    /// <remarks>
+    /// The coordinates are relative to <see cref="View.Bounds"/>.
+    /// </remarks>
+    /// <param name="mouseEvent"></param>
+    /// <returns><c>true</c>, if the event was handled, <c>false</c> otherwise.</returns>
+    public virtual bool OnMouseLeave (MouseEvent mouseEvent) { return false; }
+
+    /// <summary>Method invoked when the <see cref="Visible"/> property from a view is changed.</summary>
+    public virtual void OnVisibleChanged () { }
+
+    /// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
+    /// <remarks>
+    ///     If disposing equals true, the method has been called directly or indirectly by a user's code. Managed and
+    ///     unmanaged resources can be disposed. If disposing equals false, the method has been called by the runtime from
+    ///     inside the finalizer and you should not reference other objects. Only unmanaged resources can be disposed.
+    /// </remarks>
+    /// <param name="disposing"></param>
+    protected virtual void Dispose (bool disposing)
+    {
+        if (!disposedValue)
+        {
+            if (disposing)
+            {
+                // TODO: dispose managed state (managed objects)
+            }
+
+            disposedValue = true;
+        }
+    }
+
+    // TODO: v2 - nuke this
+    /// <summary>Utilty function to determine <paramref name="method"/> is overridden in the <paramref name="subclass"/>.</summary>
+    /// <param name="subclass">The view.</param>
+    /// <param name="method">The method name.</param>
+    /// <returns><see langword="true"/> if it's overridden, <see langword="false"/> otherwise.</returns>
+    internal static bool IsOverridden (Responder subclass, string method)
+    {
+        MethodInfo m = subclass.GetType ()
+                               .GetMethod (
+                                           method,
+                                           BindingFlags.Instance
+                                           | BindingFlags.Public
+                                           | BindingFlags.NonPublic
+                                           | BindingFlags.DeclaredOnly
+                                          );
+
+        if (m is null)
+        {
+            return false;
+        }
+
+        return m.GetBaseDefinition ().DeclaringType != m.DeclaringType;
+    }
 
-	/// <summary>
-	/// Event raised when <see cref="Dispose()"/> has been called to signal that this object is being disposed.
-	/// </summary>
-	public event EventHandler Disposing;
-
-	/// <summary>
-	/// Gets or sets a value indicating whether this <see cref="Responder"/> can focus.
-	/// </summary>
-	/// <value><c>true</c> if can focus; otherwise, <c>false</c>.</value>
-	public virtual bool CanFocus { get; set; }
-
-	/// <summary>
-	/// Gets or sets a value indicating whether this <see cref="Responder"/> has focus.
-	/// </summary>
-	/// <value><c>true</c> if has focus; otherwise, <c>false</c>.</value>
-	public virtual bool HasFocus { get; }
-
-	/// <summary>
-	/// Gets or sets a value indicating whether this <see cref="Responder"/> can respond to user interaction.
-	/// </summary>
-	public virtual bool Enabled { get; set; } = true;
-
-	/// <summary>
-	/// Gets or sets a value indicating whether this <see cref="Responder"/> and all its child controls are displayed.
-	/// </summary>
-	public virtual bool Visible { get; set; } = true;
-
-	/// <summary>
-	/// Method invoked when a mouse event is generated
-	/// </summary>
-	/// <returns><c>true</c>, if the event was handled, <c>false</c> otherwise.</returns>
-	/// <param name="mouseEvent">Contains the details about the mouse event.</param>
-	public virtual bool MouseEvent (MouseEvent mouseEvent)
-	{
-		return false;
-	}
-
-	/// <summary>
-	/// Called when the mouse first enters the view; the view will now
-	/// receives mouse events until the mouse leaves the view. At which time, <see cref="OnMouseLeave(Gui.MouseEvent)"/>
-	/// will be called.
-	/// </summary>
-	/// <param name="mouseEvent"></param>
-	/// <returns><c>true</c>, if the event was handled, <c>false</c> otherwise.</returns>
-	public virtual bool OnMouseEnter (MouseEvent mouseEvent)
-	{
-		return false;
-	}
-
-	/// <summary>
-	/// Called when the mouse has moved outside of the view; the view will no longer receive mouse events (until
-	/// the mouse moves within the view again and <see cref="OnMouseEnter(Gui.MouseEvent)"/> is called).
-	/// </summary>
-	/// <param name="mouseEvent"></param>
-	/// <returns><c>true</c>, if the event was handled, <c>false</c> otherwise.</returns>
-	public virtual bool OnMouseLeave (MouseEvent mouseEvent)
-	{
-		return false;
-	}
-
-	/// <summary>
-	/// Method invoked when a view gets focus.
-	/// </summary>
-	/// <param name="view">The view that is losing focus.</param>
-	/// <returns><c>true</c>, if the event was handled, <c>false</c> otherwise.</returns>
-	public virtual bool OnEnter (View view)
-	{
-		return false;
-	}
-
-	/// <summary>
-	/// Method invoked when a view loses focus.
-	/// </summary>
-	/// <param name="view">The view that is getting focus.</param>
-	/// <returns><c>true</c>, if the event was handled, <c>false</c> otherwise.</returns>
-	public virtual bool OnLeave (View view)
-	{
-		return false;
-	}
-
-	/// <summary>
-	/// Method invoked when the <see cref="CanFocus"/> property from a view is changed.
-	/// </summary>
-	public virtual void OnCanFocusChanged () { }
-
-	/// <summary>
-	/// Method invoked when the <see cref="Enabled"/> property from a view is changed.
-	/// </summary>
-	public virtual void OnEnabledChanged () { }
-
-	/// <summary>
-	/// Method invoked when the <see cref="Visible"/> property from a view is changed.
-	/// </summary>
-	public virtual void OnVisibleChanged () { }
-
-	// TODO: v2 - nuke this
-	/// <summary>
-	/// Utilty function to determine <paramref name="method"/> is overridden in the <paramref name="subclass"/>.
-	/// </summary>
-	/// <param name="subclass">The view.</param>
-	/// <param name="method">The method name.</param>
-	/// <returns><see langword="true"/> if it's overridden, <see langword="false"/> otherwise.</returns>
-	internal static bool IsOverridden (Responder subclass, string method)
-	{
-		MethodInfo m = subclass.GetType ().GetMethod (method,
-			BindingFlags.Instance
-			| BindingFlags.Public
-			| BindingFlags.NonPublic
-			| BindingFlags.DeclaredOnly);
-		if (m == null) {
-			return false;
-		}
-		return m.GetBaseDefinition ().DeclaringType != m.DeclaringType;
-	}
-
-	/// <summary>
-	/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
-	/// </summary>
-	/// <remarks>
-	/// If disposing equals true, the method has been called directly
-	/// or indirectly by a user's code. Managed and unmanaged resources
-	/// can be disposed.
-	/// If disposing equals false, the method has been called by the
-	/// runtime from inside the finalizer and you should not reference
-	/// other objects. Only unmanaged resources can be disposed.
-	/// </remarks>
-	/// <param name="disposing"></param>
-	protected virtual void Dispose (bool disposing)
-	{
-		if (!disposedValue) {
-			if (disposing) {
-				// TODO: dispose managed state (managed objects)
-			}
-
-			disposedValue = true;
-		}
-	}
-
-	/// <summary>
-	/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resource.
-	/// </summary>
-	public void Dispose ()
-	{
-		// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
-		Disposing?.Invoke (this, EventArgs.Empty);
-		Dispose (disposing: true);
-		GC.SuppressFinalize (this);
 #if DEBUG_IDISPOSABLE
-		WasDisposed = true;
+    /// <summary>For debug purposes to verify objects are being disposed properly</summary>
+    public bool WasDisposed;
+
+    /// <summary>For debug purposes to verify objects are being disposed properly</summary>
+    public int DisposedCount = 0;
+
+    /// <summary>For debug purposes</summary>
+    public static List<Responder> Instances = new ();
 
-		foreach (var instance in Instances.Where (x => x.WasDisposed).ToList ()) {
-			Instances.Remove (instance);
-		}
+    /// <summary>For debug purposes</summary>
+    public Responder () { Instances.Add (this); }
 #endif
-	}
 }

+ 171 - 149
Terminal.Gui/Input/ShortcutHelper.cs

@@ -1,152 +1,174 @@
-using System;
-using System.Diagnostics;
-using System.Linq;
-using System.Text;
+using System.Diagnostics;
 
 namespace Terminal.Gui;
-/// <summary>
-/// Represents a helper to manipulate shortcut keys used on views.
-/// </summary>
-public class ShortcutHelper {
-	// TODO: Update this to use Key, not KeyCode
-	private KeyCode shortcut;
-
-	/// <summary>
-	/// This is the global setting that can be used as a global shortcut to invoke the action on the view.
-	/// </summary>
-	public virtual KeyCode Shortcut {
-		get => shortcut;
-		set {
-			if (shortcut != value && (PostShortcutValidation (value) || value is KeyCode.Null)) {
-				shortcut = value;
-			}
-		}
-	}
-
-	/// <summary>
-	/// The keystroke combination used in the <see cref="Shortcut"/> as string.
-	/// </summary>
-	public virtual string ShortcutTag => Key.ToString (shortcut, MenuBar.ShortcutDelimiter);
-	
-	/// <summary>
-	/// Return key as string.
-	/// </summary>
-	/// <param name="key">The key to extract.</param>
-	/// <param name="knm">Correspond to the non modifier key.</param>
-	static string GetKeyToString (KeyCode key, out KeyCode knm)
-	{
-		if (key == KeyCode.Null) {
-			knm = KeyCode.Null;
-			return "";
-		}
-
-		knm = key;
-		var mK = key & (KeyCode.AltMask | KeyCode.CtrlMask | KeyCode.ShiftMask);
-		knm &= ~mK;
-		for (uint i = (uint)KeyCode.F1; i < (uint)KeyCode.F12; i++) {
-			if (knm == (KeyCode)i) {
-				mK |= (KeyCode)i;
-			}
-		}
-		knm &= ~mK;
-		uint.TryParse (knm.ToString (), out uint c);
-		var s = mK == KeyCode.Null ? "" : mK.ToString ();
-		if (s != "" && (knm != KeyCode.Null || c > 0)) {
-			s += ",";
-		}
-		s += c == 0 ? knm == KeyCode.Null ? "" : knm.ToString () : ((char)c).ToString ();
-		return s;
-	}
-
-	/// <summary>
-	/// Allows to retrieve a <see cref="KeyCode"/> from a <see cref="ShortcutTag"/>
-	/// </summary>
-	/// <param name="tag">The key as string.</param>
-	/// <param name="delimiter">The delimiter string.</param>
-	public static KeyCode GetShortcutFromTag (string tag, Rune delimiter = default)
-	{
-		var sCut = tag;
-		if (string.IsNullOrEmpty (sCut)) {
-			return default;
-		}
-
-		KeyCode key = KeyCode.Null;
-		//var hasCtrl = false;
-		if (delimiter == default) {
-			delimiter = MenuBar.ShortcutDelimiter;
-		}
-
-		string [] keys = sCut.Split (delimiter.ToString());
-		for (int i = 0; i < keys.Length; i++) {
-			var k = keys [i];
-			if (k == "Ctrl") {
-				//hasCtrl = true;
-				key |= KeyCode.CtrlMask;
-			} else if (k == "Shift") {
-				key |= KeyCode.ShiftMask;
-			} else if (k == "Alt") {
-				key |= KeyCode.AltMask;
-			} else if (k.StartsWith ("F") && k.Length > 1) {
-				int.TryParse (k.Substring (1).ToString (), out int n);
-				for (uint j = (uint)KeyCode.F1; j <= (uint)KeyCode.F12; j++) {
-					int.TryParse (((KeyCode)j).ToString ().Substring (1), out int f);
-					if (f == n) {
-						key |= (KeyCode)j;
-					}
-				}
-			} else {
-				key |= (KeyCode)Enum.Parse (typeof (KeyCode), k.ToString ());
-			}
-		}
-
-		return key;
-	}
-
-	/// <summary>
-	/// Lookup for a <see cref="KeyCode"/> on range of keys.
-	/// </summary>
-	/// <param name="key">The source key.</param>
-	/// <param name="first">First key in range.</param>
-	/// <param name="last">Last key in range.</param>
-	public static bool CheckKeysFlagRange (KeyCode key, KeyCode first, KeyCode last)
-	{
-		for (uint i = (uint)first; i < (uint)last; i++) {
-			if ((key | (KeyCode)i) == key) {
-				return true;
-			}
-		}
-		return false;
-	}
-
-	/// <summary>
-	/// Used at key down or key press validation.
-	/// </summary>
-	/// <param name="key">The key to validate.</param>
-	/// <returns><c>true</c> if is valid.<c>false</c>otherwise.</returns>
-	public static bool PreShortcutValidation (KeyCode key)
-	{
-		if ((key & (KeyCode.CtrlMask | KeyCode.ShiftMask | KeyCode.AltMask)) == 0 && !CheckKeysFlagRange (key, KeyCode.F1, KeyCode.F12)) {
-			return false;
-		}
-
-		return true;
-	}
-
-	/// <summary>
-	/// Used at key up validation.
-	/// </summary>
-	/// <param name="key">The key to validate.</param>
-	/// <returns><c>true</c> if is valid.<c>false</c>otherwise.</returns>
-	public static bool PostShortcutValidation (KeyCode key)
-	{
-		GetKeyToString (key, out KeyCode knm);
-
-		if (CheckKeysFlagRange (key, KeyCode.F1, KeyCode.F12) ||
-			((key & (KeyCode.CtrlMask | KeyCode.ShiftMask | KeyCode.AltMask)) != 0 && knm != KeyCode.Null)) {
-			return true;
-		}
-		Debug.WriteLine ($"WARNING: {Key.ToString (key)} is not a valid shortcut key.");
-		return false;
-	}
-}
 
+/// <summary>Represents a helper to manipulate shortcut keys used on views.</summary>
+public class ShortcutHelper
+{
+    // TODO: Update this to use Key, not KeyCode
+    private KeyCode shortcut;
+
+    /// <summary>This is the global setting that can be used as a global shortcut to invoke the action on the view.</summary>
+    public virtual KeyCode Shortcut
+    {
+        get => shortcut;
+        set
+        {
+            if (shortcut != value && (PostShortcutValidation (value) || value is KeyCode.Null))
+            {
+                shortcut = value;
+            }
+        }
+    }
+
+    /// <summary>The keystroke combination used in the <see cref="Shortcut"/> as string.</summary>
+    public virtual string ShortcutTag => Key.ToString (shortcut, MenuBar.ShortcutDelimiter);
+
+    /// <summary>Lookup for a <see cref="KeyCode"/> on range of keys.</summary>
+    /// <param name="key">The source key.</param>
+    /// <param name="first">First key in range.</param>
+    /// <param name="last">Last key in range.</param>
+    public static bool CheckKeysFlagRange (KeyCode key, KeyCode first, KeyCode last)
+    {
+        for (var i = (uint)first; i < (uint)last; i++)
+        {
+            if ((key | (KeyCode)i) == key)
+            {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /// <summary>Allows to retrieve a <see cref="KeyCode"/> from a <see cref="ShortcutTag"/></summary>
+    /// <param name="tag">The key as string.</param>
+    /// <param name="delimiter">The delimiter string.</param>
+    public static KeyCode GetShortcutFromTag (string tag, Rune delimiter = default)
+    {
+        string sCut = tag;
+
+        if (string.IsNullOrEmpty (sCut))
+        {
+            return default (KeyCode);
+        }
+
+        var key = KeyCode.Null;
+
+        //var hasCtrl = false;
+        if (delimiter == default (Rune))
+        {
+            delimiter = MenuBar.ShortcutDelimiter;
+        }
+
+        string [] keys = sCut.Split (delimiter.ToString ());
+
+        for (var i = 0; i < keys.Length; i++)
+        {
+            string k = keys [i];
+
+            if (k == "Ctrl")
+            {
+                //hasCtrl = true;
+                key |= KeyCode.CtrlMask;
+            }
+            else if (k == "Shift")
+            {
+                key |= KeyCode.ShiftMask;
+            }
+            else if (k == "Alt")
+            {
+                key |= KeyCode.AltMask;
+            }
+            else if (k.StartsWith ("F") && k.Length > 1)
+            {
+                int.TryParse (k.Substring (1), out int n);
+
+                for (var j = (uint)KeyCode.F1; j <= (uint)KeyCode.F12; j++)
+                {
+                    int.TryParse (((KeyCode)j).ToString ().Substring (1), out int f);
+
+                    if (f == n)
+                    {
+                        key |= (KeyCode)j;
+                    }
+                }
+            }
+            else
+            {
+                key |= (KeyCode)Enum.Parse (typeof (KeyCode), k);
+            }
+        }
+
+        return key;
+    }
+
+    /// <summary>Used at key up validation.</summary>
+    /// <param name="key">The key to validate.</param>
+    /// <returns><c>true</c> if is valid.<c>false</c>otherwise.</returns>
+    public static bool PostShortcutValidation (KeyCode key)
+    {
+        GetKeyToString (key, out KeyCode knm);
+
+        if (CheckKeysFlagRange (key, KeyCode.F1, KeyCode.F12) || ((key & (KeyCode.CtrlMask | KeyCode.ShiftMask | KeyCode.AltMask)) != 0 && knm != KeyCode.Null))
+        {
+            return true;
+        }
+
+        Debug.WriteLine ($"WARNING: {Key.ToString (key)} is not a valid shortcut key.");
+
+        return false;
+    }
+
+    /// <summary>Used at key down or key press validation.</summary>
+    /// <param name="key">The key to validate.</param>
+    /// <returns><c>true</c> if is valid.<c>false</c>otherwise.</returns>
+    public static bool PreShortcutValidation (KeyCode key)
+    {
+        if ((key & (KeyCode.CtrlMask | KeyCode.ShiftMask | KeyCode.AltMask)) == 0
+            && !CheckKeysFlagRange (key, KeyCode.F1, KeyCode.F12))
+        {
+            return false;
+        }
+
+        return true;
+    }
+
+    /// <summary>Return key as string.</summary>
+    /// <param name="key">The key to extract.</param>
+    /// <param name="knm">Correspond to the non modifier key.</param>
+    private static string GetKeyToString (KeyCode key, out KeyCode knm)
+    {
+        if (key == KeyCode.Null)
+        {
+            knm = KeyCode.Null;
+
+            return "";
+        }
+
+        knm = key;
+        KeyCode mK = key & (KeyCode.AltMask | KeyCode.CtrlMask | KeyCode.ShiftMask);
+        knm &= ~mK;
+
+        for (var i = (uint)KeyCode.F1; i < (uint)KeyCode.F12; i++)
+        {
+            if (knm == (KeyCode)i)
+            {
+                mK |= (KeyCode)i;
+            }
+        }
+
+        knm &= ~mK;
+        uint.TryParse (knm.ToString (), out uint c);
+        string s = mK == KeyCode.Null ? "" : mK.ToString ();
+
+        if (s != "" && (knm != KeyCode.Null || c > 0))
+        {
+            s += ",";
+        }
+
+        s += c == 0 ? knm == KeyCode.Null ? "" : knm.ToString () : ((char)c).ToString ();
+
+        return s;
+    }
+}

+ 5 - 0
Terminal.Gui/IterationEventArgs.cs

@@ -0,0 +1,5 @@
+namespace Terminal.Gui;
+
+/// <summary>Event arguments for the <see cref="Application.Iteration"/> event.</summary>
+public class IterationEventArgs : EventArgs
+{ }

+ 383 - 372
Terminal.Gui/MainLoop.cs

@@ -4,378 +4,389 @@
 // Authors:
 //   Miguel de Icaza ([email protected])
 //
-using System;
-using System.Collections.Generic;
+
 using System.Collections.ObjectModel;
 
-namespace Terminal.Gui {
-	/// <summary>
-	/// Interface to create a platform specific <see cref="MainLoop"/> driver.
-	/// </summary>
-	internal interface IMainLoopDriver {
-		/// <summary>
-		/// Initializes the <see cref="MainLoop"/>, gets the calling main loop for the initialization.
-		/// </summary>
-		/// <remarks>
-		/// Call <see cref="TearDown"/> to release resources.
-		/// </remarks>
-		/// <param name="mainLoop">Main loop.</param>
-		void Setup (MainLoop mainLoop);
-
-		/// <summary>
-		/// Wakes up the <see cref="MainLoop"/> that might be waiting on input, must be thread safe.
-		/// </summary>
-		void Wakeup ();
-
-		/// <summary>
-		/// Must report whether there are any events pending, or even block waiting for events.
-		/// </summary>
-		/// <returns><c>true</c>, if there were pending events, <c>false</c> otherwise.</returns>
-		bool EventsPending ();
-
-		/// <summary>
-		/// The iteration function.
-		/// </summary>
-		void Iteration ();
-
-		/// <summary>
-		/// Tears down the <see cref="MainLoop"/> driver. Releases resources created in <see cref="Setup"/>.
-		/// </summary>
-		void TearDown ();
-	}
-
-
-	/// <summary>
-	///   The MainLoop monitors timers and idle handlers.
-	/// </summary>
-	/// <remarks>
-	///   Monitoring of file descriptors is only available on Unix, there
-	///   does not seem to be a way of supporting this on Windows.
-	/// </remarks>
-	internal class MainLoop : IDisposable {
-
-		internal SortedList<long, Timeout> _timeouts = new SortedList<long, Timeout> ();
-		readonly object _timeoutsLockToken = new object ();
-
-		/// <summary>
-		/// The idle handlers and lock that must be held while manipulating them
-		/// </summary>
-		readonly object _idleHandlersLock = new object ();
-		internal List<Func<bool>> _idleHandlers = new List<Func<bool>> ();
-
-		/// <summary>
-		/// Gets the list of all timeouts sorted by the <see cref="TimeSpan"/> time ticks.
-		/// A shorter limit time can be added at the end, but it will be called before an
-		///  earlier addition that has a longer limit time.
-		/// </summary>
-		internal SortedList<long, Timeout> Timeouts => _timeouts;
-
-		/// <summary>
-		/// Gets a copy of the list of all idle handlers.
-		/// </summary>
-		internal ReadOnlyCollection<Func<bool>> IdleHandlers {
-			get {
-				lock (_idleHandlersLock) {
-					return new List<Func<bool>> (_idleHandlers).AsReadOnly ();
-				}
-			}
-		}
-
-		/// <summary>
-		/// The current <see cref="IMainLoopDriver"/> in use.
-		/// </summary>
-		/// <value>The main loop driver.</value>
-		internal IMainLoopDriver MainLoopDriver { get; private set; }
-
-		/// <summary>
-		/// Invoked when a new timeout is added. To be used in the case
-		/// when <see cref="Application.EndAfterFirstIteration"/> is <see langword="true"/>.
-		/// </summary>
-		internal event EventHandler<TimeoutEventArgs> TimeoutAdded;
-
-		/// <summary>
-		///  Creates a new MainLoop. 
-		/// </summary>
-		/// <remarks>
-		/// Use <see cref="Dispose"/> to release resources.
-		/// </remarks>
-		/// <param name="driver">The <see cref="ConsoleDriver"/> instance
-		/// (one of the implementations FakeMainLoop, UnixMainLoop, NetMainLoop or WindowsMainLoop).</param>
-		internal MainLoop (IMainLoopDriver driver)
-		{
-			MainLoopDriver = driver;
-			driver.Setup (this);
-		}
-
-		/// <summary>
-		///   Adds specified idle handler function to <see cref="MainLoop"/> processing. 
-		///   The handler function will be called once per iteration of the main loop after other events have been handled.
-		/// </summary>
-		/// <remarks>
-		/// <para>
-		///   Remove an idle handler by calling <see cref="RemoveIdle(Func{bool})"/> with the token this method returns.
-		/// </para>
-		/// <para>
-		///   If the <paramref name="idleHandler"/> returns  <see langword="false"/> it will be removed and not called subsequently.
-		/// </para>
-		/// </remarks>
-		/// <param name="idleHandler">Token that can be used to remove the idle handler with <see cref="RemoveIdle(Func{bool})"/> .</param>
-		internal Func<bool> AddIdle (Func<bool> idleHandler)
-		{
-			lock (_idleHandlersLock) {
-				_idleHandlers.Add (idleHandler);
-			}
-
-			MainLoopDriver.Wakeup ();
-			return idleHandler;
-		}
-
-		/// <summary>
-		///   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>
-		/// 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.
-		internal bool RemoveIdle (Func<bool> token)
-		{
-			lock (_idleHandlersLock) {
-				return _idleHandlers.Remove (token);
-			}
-		}
-
-		void AddTimeout (TimeSpan time, Timeout timeout)
-		{
-			lock (_timeoutsLockToken) {
-				var k = (DateTime.UtcNow + time).Ticks;
-				_timeouts.Add (NudgeToUniqueKey (k), timeout);
-				TimeoutAdded?.Invoke (this, new TimeoutEventArgs (timeout, k));
-			}
-		}
-
-		/// <summary>
-		///   Adds a timeout to the <see cref="MainLoop"/>.
-		/// </summary>
-		/// <remarks>
-		///   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.
-		///
-		///   The returned value is a token that can be used to stop the timeout
-		///   by calling <see cref="RemoveTimeout(object)"/>.
-		/// </remarks>
-		internal object AddTimeout (TimeSpan time, Func<bool> callback)
-		{
-			if (callback == null) {
-				throw new ArgumentNullException (nameof (callback));
-			}
-			var timeout = new Timeout () {
-				Span = time,
-				Callback = callback
-			};
-			AddTimeout (time, timeout);
-			return timeout;
-		}
-
-		/// <summary>
-		///   Removes a previously scheduled timeout
-		/// </summary>
-		/// <remarks>
-		///   The token parameter is the value returned by AddTimeout.
-		/// </remarks>
-		/// 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.
-		internal bool RemoveTimeout (object token)
-		{
-			lock (_timeoutsLockToken) {
-				var idx = _timeouts.IndexOfValue (token as Timeout);
-				if (idx == -1) {
-					return false;
-				}
-				_timeouts.RemoveAt (idx);
-			}
-			return true;
-		}
-
-		void RunTimers ()
-		{
-			var now = DateTime.UtcNow.Ticks;
-			SortedList<long, Timeout> copy;
-
-			// lock prevents new timeouts being added
-			// after we have taken the copy but before
-			// we have allocated a new list (which would
-			// result in lost timeouts or errors during enumeration)
-			lock (_timeoutsLockToken) {
-				copy = _timeouts;
-				_timeouts = new SortedList<long, Timeout> ();
-			}
-
-			foreach ((var k, var timeout) in copy) {
-				if (k < now) {
-					if (timeout.Callback ()) {
-						AddTimeout (timeout.Span, timeout);
-					}
-				} else {
-					lock (_timeoutsLockToken) {
-						_timeouts.Add (NudgeToUniqueKey (k), timeout);
-					}
-				}
-			}
-		}
-
-		/// <summary>
-		/// Called from <see cref="IMainLoopDriver.EventsPending"/> to check if there are any outstanding timers or idle handlers.
-		/// </summary>
-		/// <param name="waitTimeout">Returns the number of milliseconds remaining in the current timer (if any). Will be -1 if
-		/// there are no active timers.</param>
-		/// <returns><see langword="true"/> if there is a timer or idle handler active.</returns>
-		internal bool CheckTimersAndIdleHandlers (out int waitTimeout)
-		{
-			var now = DateTime.UtcNow.Ticks;
-
-			waitTimeout = 0;
-
-			lock (_timeouts) {
-				if (_timeouts.Count > 0) {
-					waitTimeout = (int)((_timeouts.Keys [0] - now) / TimeSpan.TicksPerMillisecond);
-					if (waitTimeout < 0) {
-						// This avoids 'poll' waiting infinitely if 'waitTimeout < 0' until some action is detected
-						// This can occur after IMainLoopDriver.Wakeup is executed where the pollTimeout is less than 0
-						// and no event occurred in elapsed time when the 'poll' is start running again.
-						waitTimeout = 0;
-					}
-					return true;
-				}
-				// ManualResetEventSlim.Wait, which is called by IMainLoopDriver.EventsPending, will wait indefinitely if
-				// the timeout is -1.
-				waitTimeout = -1;
-			}
-
-			// There are no timers set, check if there are any idle handlers
-
-			lock (_idleHandlers) {
-				return _idleHandlers.Count > 0;
-			}
-		}
-
-		/// <summary>
-		/// Finds the closest number to <paramref name="k"/> that is not
-		/// present in <see cref="_timeouts"/> (incrementally).
-		/// </summary>
-		/// <param name="k"></param>
-		/// <returns></returns>
-		long NudgeToUniqueKey (long k)
-		{
-			lock (_timeoutsLockToken) {
-				while (_timeouts.ContainsKey (k)) {
-					k++;
-				}
-			}
-
-			return k;
-		}
-
-		void RunIdle ()
-		{
-			List<Func<bool>> iterate;
-			lock (_idleHandlersLock) {
-				iterate = _idleHandlers;
-				_idleHandlers = new List<Func<bool>> ();
-			}
-
-			foreach (var idle in iterate) {
-				if (idle ()) {
-					lock (_idleHandlersLock) {
-						_idleHandlers.Add (idle);
-					}
-				}
-			}
-		}
-
-		/// <summary>
-		/// Used for unit tests.
-		/// </summary>
-		internal bool Running { get; set; }
-
-		/// <summary>
-		///   Determines whether there are pending events to be processed.
-		/// </summary>
-		/// <remarks>
-		///   You can use this method if you want to probe if events are pending.
-		///   Typically used if you need to flush the input queue while still
-		///   running some of your own code in your main thread.
-		/// </remarks>
-		internal bool EventsPending ()
-		{
-			return MainLoopDriver.EventsPending ();
-		}
-
-		/// <summary>
-		///   Runs one iteration of timers and file watches
-		/// </summary>
-		/// <remarks>
-		///   Use this to process all pending events (timers, idle handlers and file watches).
-		///
-		///   <code>
-		///     while (main.EventsPending ()) RunIteration ();
-		///   </code>
-		/// </remarks>
-		internal void RunIteration ()
-		{
-			lock (_timeouts) {
-				if (_timeouts.Count > 0) {
-					RunTimers ();
-				}
-			}
-
-			MainLoopDriver.Iteration ();
-
-			var runIdle = false;
-			lock (_idleHandlersLock) {
-				runIdle = _idleHandlers.Count > 0;
-			}
-
-			if (runIdle) {
-				RunIdle ();
-			}
-		}
-
-		/// <summary>
-		///   Runs the <see cref="MainLoop"/>. Used only for unit tests.
-		/// </summary>
-		internal void Run ()
-		{
-			var prev = Running;
-			Running = true;
-			while (Running) {
-				EventsPending ();
-				RunIteration ();
-			}
-			Running = prev;
-		}
-
-		/// <summary>
-		/// Wakes up the <see cref="MainLoop"/> that might be waiting on input.
-		/// </summary>
-		internal void Wakeup () => MainLoopDriver?.Wakeup ();
-
-		/// <summary>
-		/// Stops the main loop driver and calls <see cref="IMainLoopDriver.Wakeup"/>. Used only for unit tests.
-		/// </summary>
-		internal void Stop ()
-		{
-			Running = false;
-			Wakeup ();
-		}
-
-		/// <inheritdoc/>
-		public void Dispose ()
-		{
-			GC.SuppressFinalize (this);
-			Stop ();
-			Running = false;
-			MainLoopDriver?.TearDown ();
-			MainLoopDriver = null;
-		}
-	}
+namespace Terminal.Gui;
+
+/// <summary>Interface to create a platform specific <see cref="MainLoop"/> driver.</summary>
+internal interface IMainLoopDriver
+{
+    /// <summary>Must report whether there are any events pending, or even block waiting for events.</summary>
+    /// <returns><c>true</c>, if there were pending events, <c>false</c> otherwise.</returns>
+    bool EventsPending ();
+
+    /// <summary>The iteration function.</summary>
+    void Iteration ();
+
+    /// <summary>Initializes the <see cref="MainLoop"/>, gets the calling main loop for the initialization.</summary>
+    /// <remarks>Call <see cref="TearDown"/> to release resources.</remarks>
+    /// <param name="mainLoop">Main loop.</param>
+    void Setup (MainLoop mainLoop);
+
+    /// <summary>Tears down the <see cref="MainLoop"/> driver. Releases resources created in <see cref="Setup"/>.</summary>
+    void TearDown ();
+
+    /// <summary>Wakes up the <see cref="MainLoop"/> that might be waiting on input, must be thread safe.</summary>
+    void Wakeup ();
+}
+
+/// <summary>The MainLoop monitors timers and idle handlers.</summary>
+/// <remarks>
+///     Monitoring of file descriptors is only available on Unix, there does not seem to be a way of supporting this
+///     on Windows.
+/// </remarks>
+internal class MainLoop : IDisposable
+{
+    internal List<Func<bool>> _idleHandlers = new ();
+    internal SortedList<long, Timeout> _timeouts = new ();
+
+    /// <summary>The idle handlers and lock that must be held while manipulating them</summary>
+    private readonly object _idleHandlersLock = new ();
+
+    private readonly object _timeoutsLockToken = new ();
+
+    /// <summary>Creates a new MainLoop.</summary>
+    /// <remarks>Use <see cref="Dispose"/> to release resources.</remarks>
+    /// <param name="driver">
+    ///     The <see cref="ConsoleDriver"/> instance (one of the implementations FakeMainLoop, UnixMainLoop,
+    ///     NetMainLoop or WindowsMainLoop).
+    /// </param>
+    internal MainLoop (IMainLoopDriver driver)
+    {
+        MainLoopDriver = driver;
+        driver.Setup (this);
+    }
+
+    /// <summary>Gets a copy of the list of all idle handlers.</summary>
+    internal ReadOnlyCollection<Func<bool>> IdleHandlers
+    {
+        get
+        {
+            lock (_idleHandlersLock)
+            {
+                return new List<Func<bool>> (_idleHandlers).AsReadOnly ();
+            }
+        }
+    }
+
+    /// <summary>The current <see cref="IMainLoopDriver"/> in use.</summary>
+    /// <value>The main loop driver.</value>
+    internal IMainLoopDriver MainLoopDriver { get; private set; }
+
+    /// <summary>Used for unit tests.</summary>
+    internal bool Running { get; set; }
+
+    /// <summary>
+    ///     Gets the list of all timeouts sorted by the <see cref="TimeSpan"/> time ticks. A shorter limit time can be
+    ///     added at the end, but it will be called before an earlier addition that has a longer limit time.
+    /// </summary>
+    internal SortedList<long, Timeout> Timeouts => _timeouts;
+
+    /// <inheritdoc/>
+    public void Dispose ()
+    {
+        GC.SuppressFinalize (this);
+        Stop ();
+        Running = false;
+        MainLoopDriver?.TearDown ();
+        MainLoopDriver = null;
+    }
+
+    /// <summary>
+    ///     Adds specified idle handler function to <see cref="MainLoop"/> processing. The handler function will be called
+    ///     once per iteration of the main loop after other events have been handled.
+    /// </summary>
+    /// <remarks>
+    ///     <para>Remove an idle handler by calling <see cref="RemoveIdle(Func{bool})"/> with the token this method returns.</para>
+    ///     <para>
+    ///         If the <paramref name="idleHandler"/> returns  <see langword="false"/> it will be removed and not called
+    ///         subsequently.
+    ///     </para>
+    /// </remarks>
+    /// <param name="idleHandler">Token that can be used to remove the idle handler with <see cref="RemoveIdle(Func{bool})"/> .</param>
+    // QUESTION: Why are we re-inventing the event wheel here?
+    // PERF: This is heavy.
+    // CONCURRENCY: Race conditions exist here.
+    // CONCURRENCY: null delegates will hose this.
+    // 
+    internal Func<bool> AddIdle (Func<bool> idleHandler)
+    {
+        lock (_idleHandlersLock)
+        {
+            _idleHandlers.Add (idleHandler);
+        }
+
+        MainLoopDriver.Wakeup ();
+
+        return idleHandler;
+    }
+
+    /// <summary>Adds a timeout to the <see cref="MainLoop"/>.</summary>
+    /// <remarks>
+    ///     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. The returned value is a
+    ///     token that can be used to stop the timeout by calling <see cref="RemoveTimeout(object)"/>.
+    /// </remarks>
+    internal object AddTimeout (TimeSpan time, Func<bool> callback)
+    {
+        if (callback is null)
+        {
+            throw new ArgumentNullException (nameof (callback));
+        }
+
+        var timeout = new Timeout { Span = time, Callback = callback };
+        AddTimeout (time, timeout);
+
+        return timeout;
+    }
+
+    /// <summary>
+    ///     Called from <see cref="IMainLoopDriver.EventsPending"/> to check if there are any outstanding timers or idle
+    ///     handlers.
+    /// </summary>
+    /// <param name="waitTimeout">
+    ///     Returns the number of milliseconds remaining in the current timer (if any). Will be -1 if
+    ///     there are no active timers.
+    /// </param>
+    /// <returns><see langword="true"/> if there is a timer or idle handler active.</returns>
+    internal bool CheckTimersAndIdleHandlers (out int waitTimeout)
+    {
+        long now = DateTime.UtcNow.Ticks;
+
+        waitTimeout = 0;
+
+        lock (_timeouts)
+        {
+            if (_timeouts.Count > 0)
+            {
+                waitTimeout = (int)((_timeouts.Keys [0] - now) / TimeSpan.TicksPerMillisecond);
+
+                if (waitTimeout < 0)
+                {
+                    // This avoids 'poll' waiting infinitely if 'waitTimeout < 0' until some action is detected
+                    // This can occur after IMainLoopDriver.Wakeup is executed where the pollTimeout is less than 0
+                    // and no event occurred in elapsed time when the 'poll' is start running again.
+                    waitTimeout = 0;
+                }
+
+                return true;
+            }
+
+            // ManualResetEventSlim.Wait, which is called by IMainLoopDriver.EventsPending, will wait indefinitely if
+            // the timeout is -1.
+            waitTimeout = -1;
+        }
+
+        // There are no timers set, check if there are any idle handlers
+
+        lock (_idleHandlers)
+        {
+            return _idleHandlers.Count > 0;
+        }
+    }
+
+    /// <summary>Determines whether there are pending events to be processed.</summary>
+    /// <remarks>
+    ///     You can use this method if you want to probe if events are pending. Typically used if you need to flush the
+    ///     input queue while still running some of your own code in your main thread.
+    /// </remarks>
+    internal bool EventsPending () { return MainLoopDriver.EventsPending (); }
+
+    /// <summary>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>
+    /// 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.
+    internal bool RemoveIdle (Func<bool> token)
+    {
+        lock (_idleHandlersLock)
+        {
+            return _idleHandlers.Remove (token);
+        }
+    }
+
+    /// <summary>Removes a previously scheduled timeout</summary>
+    /// <remarks>The token parameter is the value returned by AddTimeout.</remarks>
+    /// 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.
+    internal bool RemoveTimeout (object token)
+    {
+        lock (_timeoutsLockToken)
+        {
+            int idx = _timeouts.IndexOfValue (token as Timeout);
+
+            if (idx == -1)
+            {
+                return false;
+            }
+
+            _timeouts.RemoveAt (idx);
+        }
+
+        return true;
+    }
+
+    /// <summary>Runs the <see cref="MainLoop"/>. Used only for unit tests.</summary>
+    internal void Run ()
+    {
+        bool prev = Running;
+        Running = true;
+
+        while (Running)
+        {
+            EventsPending ();
+            RunIteration ();
+        }
+
+        Running = prev;
+    }
+
+    /// <summary>Runs one iteration of timers and file watches</summary>
+    /// <remarks>
+    ///     Use this to process all pending events (timers, idle handlers and file watches).
+    ///     <code>
+    ///     while (main.EventsPending ()) RunIteration ();
+    ///   </code>
+    /// </remarks>
+    internal void RunIteration ()
+    {
+        lock (_timeouts)
+        {
+            if (_timeouts.Count > 0)
+            {
+                RunTimers ();
+            }
+        }
+
+        MainLoopDriver.Iteration ();
+
+        var runIdle = false;
+
+        lock (_idleHandlersLock)
+        {
+            runIdle = _idleHandlers.Count > 0;
+        }
+
+        if (runIdle)
+        {
+            RunIdle ();
+        }
+    }
+
+    /// <summary>Stops the main loop driver and calls <see cref="IMainLoopDriver.Wakeup"/>. Used only for unit tests.</summary>
+    internal void Stop ()
+    {
+        Running = false;
+        Wakeup ();
+    }
+
+    /// <summary>
+    ///     Invoked when a new timeout is added. To be used in the case when
+    ///     <see cref="Application.EndAfterFirstIteration"/> is <see langword="true"/>.
+    /// </summary>
+    internal event EventHandler<TimeoutEventArgs> TimeoutAdded;
+
+    /// <summary>Wakes up the <see cref="MainLoop"/> that might be waiting on input.</summary>
+    internal void Wakeup () { MainLoopDriver?.Wakeup (); }
+
+    private void AddTimeout (TimeSpan time, Timeout timeout)
+    {
+        lock (_timeoutsLockToken)
+        {
+            long k = (DateTime.UtcNow + time).Ticks;
+            _timeouts.Add (NudgeToUniqueKey (k), timeout);
+            TimeoutAdded?.Invoke (this, new TimeoutEventArgs (timeout, k));
+        }
+    }
+
+    /// <summary>
+    ///     Finds the closest number to <paramref name="k"/> that is not present in <see cref="_timeouts"/>
+    ///     (incrementally).
+    /// </summary>
+    /// <param name="k"></param>
+    /// <returns></returns>
+    private long NudgeToUniqueKey (long k)
+    {
+        lock (_timeoutsLockToken)
+        {
+            while (_timeouts.ContainsKey (k))
+            {
+                k++;
+            }
+        }
+
+        return k;
+    }
+
+    // PERF: This is heavier than it looks.
+    // CONCURRENCY: Potential deadlock city here.
+    // CONCURRENCY: Multiple concurrency pitfalls on the delegates themselves.
+    // INTENT: It looks like the general architecture here is trying to be a form of publisher/consumer pattern.
+    private void RunIdle ()
+    {
+        List<Func<bool>> iterate;
+
+        lock (_idleHandlersLock)
+        {
+            iterate = _idleHandlers;
+            _idleHandlers = new List<Func<bool>> ();
+        }
+
+        foreach (Func<bool> idle in iterate)
+        {
+            if (idle ())
+            {
+                lock (_idleHandlersLock)
+                {
+                    _idleHandlers.Add (idle);
+                }
+            }
+        }
+    }
+
+    private void RunTimers ()
+    {
+        long now = DateTime.UtcNow.Ticks;
+        SortedList<long, Timeout> copy;
+
+        // lock prevents new timeouts being added
+        // after we have taken the copy but before
+        // we have allocated a new list (which would
+        // result in lost timeouts or errors during enumeration)
+        lock (_timeoutsLockToken)
+        {
+            copy = _timeouts;
+            _timeouts = new SortedList<long, Timeout> ();
+        }
+
+        foreach ((long k, Timeout timeout) in copy)
+        {
+            if (k < now)
+            {
+                if (timeout.Callback ())
+                {
+                    AddTimeout (timeout.Span, timeout);
+                }
+            }
+            else
+            {
+                lock (_timeoutsLockToken)
+                {
+                    _timeouts.Add (NudgeToUniqueKey (k), timeout);
+                }
+            }
+        }
+    }
 }

+ 47 - 72
Terminal.Gui/RunState.cs

@@ -1,79 +1,54 @@
-using System;
-using System.Collections.Generic;
-
-namespace Terminal.Gui;
-
-/// <summary>
-/// The execution state for a <see cref="Toplevel"/> view.
-/// </summary>
-public class RunState : IDisposable {
-	/// <summary>
-	/// Initializes a new <see cref="RunState"/> class.
-	/// </summary>
-	/// <param name="view"></param>
-	public RunState (Toplevel view)
-	{
-		Toplevel = view;
-	}
-	/// <summary>
-	/// The <see cref="Toplevel"/> belonging to this <see cref="RunState"/>.
-	/// </summary>
-	public Toplevel Toplevel { get; internal set; }
-
+namespace Terminal.Gui;
+
+/// <summary>The execution state for a <see cref="Toplevel"/> view.</summary>
+public class RunState : IDisposable
+{
+    /// <summary>Initializes a new <see cref="RunState"/> class.</summary>
+    /// <param name="view"></param>
+    public RunState (Toplevel view) { Toplevel = view; }
+
+    /// <summary>The <see cref="Toplevel"/> belonging to this <see cref="RunState"/>.</summary>
+    public Toplevel Toplevel { get; internal set; }
+
+    /// <summary>Releases all resource used by the <see cref="RunState"/> object.</summary>
+    /// <remarks>Call <see cref="Dispose()"/> when you are finished using the <see cref="RunState"/>.</remarks>
+    /// <remarks>
+    ///     <see cref="Dispose()"/> method leaves the <see cref="RunState"/> in an unusable state. After calling
+    ///     <see cref="Dispose()"/>, you must release all references to the <see cref="RunState"/> so the garbage collector can
+    ///     reclaim the memory that the <see cref="RunState"/> was occupying.
+    /// </remarks>
+    public void Dispose ()
+    {
+        Dispose (true);
+        GC.SuppressFinalize (this);
 #if DEBUG_IDISPOSABLE
-	/// <summary>
-	/// For debug (see DEBUG_IDISPOSABLE define) purposes to verify objects are being disposed properly
-	/// </summary>
-	public bool WasDisposed = false;
+        WasDisposed = true;
+#endif
+    }
+
+    /// <summary>Releases all resource used by the <see cref="RunState"/> object.</summary>
+    /// <param name="disposing">If set to <see langword="true"/> we are disposing and should dispose held objects.</param>
+    protected virtual void Dispose (bool disposing)
+    {
+        if (Toplevel is { } && disposing)
+        {
+            throw new InvalidOperationException (
+                                                 "You must clean up (Dispose) the Toplevel before calling Application.RunState.Dispose"
+                                                );
+        }
+    }
 
-	/// <summary>
-	/// For debug (see DEBUG_IDISPOSABLE define) purposes to verify objects are being disposed properly
-	/// </summary>
-	public int DisposedCount = 0;
+#if DEBUG_IDISPOSABLE
+    /// <summary>For debug (see DEBUG_IDISPOSABLE define) purposes to verify objects are being disposed properly</summary>
+    public bool WasDisposed;
 
-	/// <summary>
-	/// For debug (see DEBUG_IDISPOSABLE define) purposes; the runstate instances that have been created
-	/// </summary>
-	public static List<RunState> Instances = new List<RunState> ();
+    /// <summary>For debug (see DEBUG_IDISPOSABLE define) purposes to verify objects are being disposed properly</summary>
+    public int DisposedCount = 0;
 
-	/// <summary>
-	/// Creates a new RunState object.
-	/// </summary>
-	public RunState ()
-	{
-		Instances.Add (this);
-	}
-#endif
+    /// <summary>For debug (see DEBUG_IDISPOSABLE define) purposes; the runstate instances that have been created</summary>
+    public static List<RunState> Instances = new ();
 
-	/// <summary>
-	/// Releases all resource used by the <see cref="RunState"/> object.
-	/// </summary>
-	/// <remarks>
-	/// Call <see cref="Dispose()"/> when you are finished using the <see cref="RunState"/>. 
-	/// </remarks>
-	/// <remarks>
-	/// <see cref="Dispose()"/> method leaves the <see cref="RunState"/> in an unusable state. After
-	/// calling <see cref="Dispose()"/>, you must release all references to the
-	/// <see cref="RunState"/> so the garbage collector can reclaim the memory that the
-	/// <see cref="RunState"/> was occupying.
-	/// </remarks>
-	public void Dispose ()
-	{
-		Dispose (true);
-		GC.SuppressFinalize (this);
-#if DEBUG_IDISPOSABLE
-		WasDisposed = true;
+    /// <summary>Creates a new RunState object.</summary>
+    public RunState () { Instances.Add (this); }
 #endif
-	}
-
-	/// <summary>
-	/// Releases all resource used by the <see cref="RunState"/> object.
-	/// </summary>
-	/// <param name="disposing">If set to <see langword="true"/> we are disposing and should dispose held objects.</param>
-	protected virtual void Dispose (bool disposing)
-	{
-		if (Toplevel != null && disposing) {
-			throw new InvalidOperationException ("You must clean up (Dispose) the Toplevel before calling Application.RunState.Dispose");
-		}
-	}
 }

+ 9 - 21
Terminal.Gui/RunStateEventArgs.cs

@@ -1,24 +1,12 @@
-using System;
-using static Terminal.Gui.Application;
+namespace Terminal.Gui;
 
-namespace Terminal.Gui {
-	/// <summary>
-	/// Event arguments for events about <see cref="RunState"/>
-	/// </summary>
-	public class RunStateEventArgs : EventArgs {
+/// <summary>Event arguments for events about <see cref="RunState"/></summary>
+public class RunStateEventArgs : EventArgs
+{
+    /// <summary>Creates a new instance of the <see cref="RunStateEventArgs"/> class</summary>
+    /// <param name="state"></param>
+    public RunStateEventArgs (RunState state) { State = state; }
 
-		/// <summary>
-		/// Creates a new instance of the <see cref="RunStateEventArgs"/> class
-		/// </summary>
-		/// <param name="state"></param>
-		public RunStateEventArgs (RunState state)
-		{
-			State = state;
-		}
-
-		/// <summary>
-		/// The state being reported on by the event
-		/// </summary>
-		public RunState State { get; }
-	}
+    /// <summary>The state being reported on by the event</summary>
+    public RunState State { get; }
 }

+ 242 - 195
Terminal.Gui/StackExtensions.cs

@@ -1,196 +1,243 @@
-using System;
-using System.Collections.Generic;
-
-namespace Terminal.Gui {
-	/// <summary>
-	/// Extension of <see cref="Stack{T}"/> helper to work with specific <see cref="IEqualityComparer{T}"/>
-	/// </summary>
-	public static class StackExtensions {
-		/// <summary>
-		/// Replaces an stack object values that match with the value to replace.
-		/// </summary>
-		/// <typeparam name="T">The stack object type.</typeparam>
-		/// <param name="stack">The stack object.</param>
-		/// <param name="valueToReplace">Value to replace.</param>
-		/// <param name="valueToReplaceWith">Value to replace with to what matches the value to replace.</param>
-		/// <param name="comparer">The comparison object.</param>
-		public static void Replace<T> (this Stack<T> stack, T valueToReplace,
-			T valueToReplaceWith, IEqualityComparer<T> comparer = null)
-		{
-			comparer = comparer ?? EqualityComparer<T>.Default;
-
-			var temp = new Stack<T> ();
-			while (stack.Count > 0) {
-				var value = stack.Pop ();
-				if (comparer.Equals (value, valueToReplace)) {
-					stack.Push (valueToReplaceWith);
-					break;
-				}
-				temp.Push (value);
-			}
-
-			while (temp.Count > 0)
-				stack.Push (temp.Pop ());
-		}
-
-		/// <summary>
-		/// Swap two stack objects values that matches with the both values.
-		/// </summary>
-		/// <typeparam name="T">The stack object type.</typeparam>
-		/// <param name="stack">The stack object.</param>
-		/// <param name="valueToSwapFrom">Value to swap from.</param>
-		/// <param name="valueToSwapTo">Value to swap to.</param>
-		/// <param name="comparer">The comparison object.</param>
-		public static void Swap<T> (this Stack<T> stack, T valueToSwapFrom,
-			T valueToSwapTo, IEqualityComparer<T> comparer = null)
-		{
-			comparer = comparer ?? EqualityComparer<T>.Default;
-
-			int index = stack.Count - 1;
-			T [] stackArr = new T [stack.Count];
-			while (stack.Count > 0) {
-				var value = stack.Pop ();
-				if (comparer.Equals (value, valueToSwapFrom)) {
-					stackArr [index] = valueToSwapTo;
-				} else if (comparer.Equals (value, valueToSwapTo)) {
-					stackArr [index] = valueToSwapFrom;
-				} else {
-					stackArr [index] = value;
-				}
-				index--;
-			}
-
-			for (int i = 0; i < stackArr.Length; i++)
-				stack.Push (stackArr [i]);
-		}
-
-		/// <summary>
-		/// Move the first stack object value to the end.
-		/// </summary>
-		/// <typeparam name="T">The stack object type.</typeparam>
-		/// <param name="stack">The stack object.</param>
-		public static void MoveNext<T> (this Stack<T> stack)
-		{
-			var temp = new Stack<T> ();
-			var last = stack.Pop ();
-			while (stack.Count > 0) {
-				var value = stack.Pop ();
-				temp.Push (value);
-			}
-			temp.Push (last);
-
-			while (temp.Count > 0)
-				stack.Push (temp.Pop ());
-		}
-
-		/// <summary>
-		/// Move the last stack object value to the top.
-		/// </summary>
-		/// <typeparam name="T">The stack object type.</typeparam>
-		/// <param name="stack">The stack object.</param>
-		public static void MovePrevious<T> (this Stack<T> stack)
-		{
-			var temp = new Stack<T> ();
-			T first = default;
-			while (stack.Count > 0) {
-				var value = stack.Pop ();
-				temp.Push (value);
-				if (stack.Count == 1) {
-					first = stack.Pop ();
-				}
-			}
-
-			while (temp.Count > 0)
-				stack.Push (temp.Pop ());
-			stack.Push (first);
-		}
-
-		/// <summary>
-		/// Find all duplicates stack objects values.
-		/// </summary>
-		/// <typeparam name="T">The stack object type.</typeparam>
-		/// <param name="stack">The stack object.</param>
-		/// <param name="comparer">The comparison object.</param>
-		/// <returns>The duplicates stack object.</returns>
-		public static Stack<T> FindDuplicates<T> (this Stack<T> stack, IEqualityComparer<T> comparer = null)
-		{
-			comparer = comparer ?? EqualityComparer<T>.Default;
-
-			var dup = new Stack<T> ();
-			T [] stackArr = stack.ToArray ();
-			for (int i = 0; i < stackArr.Length; i++) {
-				var value = stackArr [i];
-				for (int j = i + 1; j < stackArr.Length; j++) {
-					var valueToFind = stackArr [j];
-					if (comparer.Equals (value, valueToFind) && !Contains (dup, valueToFind)) {
-						dup.Push (value);
-					}
-				}
-			}
-
-			return dup;
-		}
-
-		/// <summary>
-		/// Check if the stack object contains the value to find.
-		/// </summary>
-		/// <typeparam name="T">The stack object type.</typeparam>
-		/// <param name="stack">The stack object.</param>
-		/// <param name="valueToFind">Value to find.</param>
-		/// <param name="comparer">The comparison object.</param>
-		/// <returns><c>true</c> If the value was found.<c>false</c> otherwise.</returns>
-		public static bool Contains<T> (this Stack<T> stack, T valueToFind, IEqualityComparer<T> comparer = null)
-		{
-			comparer = comparer ?? EqualityComparer<T>.Default;
-
-			foreach (T obj in stack) {
-				if (comparer.Equals (obj, valueToFind)) {
-					return true;
-				}
-			}
-			return false;
-		}
-
-		/// <summary>
-		/// Move the stack object value to the index.
-		/// </summary>
-		/// <typeparam name="T">The stack object type.</typeparam>
-		/// <param name="stack">The stack object.</param>
-		/// <param name="valueToMove">Value to move.</param>
-		/// <param name="index">The index where to move.</param>
-		/// <param name="comparer">The comparison object.</param>
-		public static void MoveTo<T> (this Stack<T> stack, T valueToMove, int index = 0,
-			IEqualityComparer<T> comparer = null)
-		{
-			if (index < 0) {
-				return;
-			}
-
-			comparer = comparer ?? EqualityComparer<T>.Default;
-
-			var temp = new Stack<T> ();
-			var toMove = default (T);
-			var stackCount = stack.Count;
-			var count = 0;
-			while (stack.Count > 0) {
-				var value = stack.Pop ();
-				if (comparer.Equals (value, valueToMove)) {
-					toMove = value;
-					break;
-				}
-				temp.Push (value);
-				count++;
-			}
-
-			int idx = 0;
-			while (stack.Count < stackCount) {
-				if (count - idx == index) {
-					stack.Push (toMove);
-				} else {
-					stack.Push (temp.Pop ());
-				}
-				idx++;
-			}
-		}
-	}
+namespace Terminal.Gui;
+
+/// <summary>Extension of <see cref="Stack{T}"/> helper to work with specific <see cref="IEqualityComparer{T}"/></summary>
+public static class StackExtensions
+{
+    /// <summary>Check if the stack object contains the value to find.</summary>
+    /// <typeparam name="T">The stack object type.</typeparam>
+    /// <param name="stack">The stack object.</param>
+    /// <param name="valueToFind">Value to find.</param>
+    /// <param name="comparer">The comparison object.</param>
+    /// <returns><c>true</c> If the value was found.<c>false</c> otherwise.</returns>
+    public static bool Contains<T> (this Stack<T> stack, T valueToFind, IEqualityComparer<T> comparer = null)
+    {
+        comparer = comparer ?? EqualityComparer<T>.Default;
+
+        foreach (T obj in stack)
+        {
+            if (comparer.Equals (obj, valueToFind))
+            {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /// <summary>Find all duplicates stack objects values.</summary>
+    /// <typeparam name="T">The stack object type.</typeparam>
+    /// <param name="stack">The stack object.</param>
+    /// <param name="comparer">The comparison object.</param>
+    /// <returns>The duplicates stack object.</returns>
+    public static Stack<T> FindDuplicates<T> (this Stack<T> stack, IEqualityComparer<T> comparer = null)
+    {
+        comparer = comparer ?? EqualityComparer<T>.Default;
+
+        Stack<T> dup = new ();
+        T [] stackArr = stack.ToArray ();
+
+        for (var i = 0; i < stackArr.Length; i++)
+        {
+            T value = stackArr [i];
+
+            for (int j = i + 1; j < stackArr.Length; j++)
+            {
+                T valueToFind = stackArr [j];
+
+                if (comparer.Equals (value, valueToFind) && !Contains (dup, valueToFind))
+                {
+                    dup.Push (value);
+                }
+            }
+        }
+
+        return dup;
+    }
+
+    /// <summary>Move the first stack object value to the end.</summary>
+    /// <typeparam name="T">The stack object type.</typeparam>
+    /// <param name="stack">The stack object.</param>
+    public static void MoveNext<T> (this Stack<T> stack)
+    {
+        Stack<T> temp = new ();
+        T last = stack.Pop ();
+
+        while (stack.Count > 0)
+        {
+            T value = stack.Pop ();
+            temp.Push (value);
+        }
+
+        temp.Push (last);
+
+        while (temp.Count > 0)
+        {
+            stack.Push (temp.Pop ());
+        }
+    }
+
+    /// <summary>Move the last stack object value to the top.</summary>
+    /// <typeparam name="T">The stack object type.</typeparam>
+    /// <param name="stack">The stack object.</param>
+    public static void MovePrevious<T> (this Stack<T> stack)
+    {
+        Stack<T> temp = new ();
+        T first = default;
+
+        while (stack.Count > 0)
+        {
+            T value = stack.Pop ();
+            temp.Push (value);
+
+            if (stack.Count == 1)
+            {
+                first = stack.Pop ();
+            }
+        }
+
+        while (temp.Count > 0)
+        {
+            stack.Push (temp.Pop ());
+        }
+
+        stack.Push (first);
+    }
+
+    /// <summary>Move the stack object value to the index.</summary>
+    /// <typeparam name="T">The stack object type.</typeparam>
+    /// <param name="stack">The stack object.</param>
+    /// <param name="valueToMove">Value to move.</param>
+    /// <param name="index">The index where to move.</param>
+    /// <param name="comparer">The comparison object.</param>
+    public static void MoveTo<T> (
+        this Stack<T> stack,
+        T valueToMove,
+        int index = 0,
+        IEqualityComparer<T> comparer = null
+    )
+    {
+        if (index < 0)
+        {
+            return;
+        }
+
+        comparer = comparer ?? EqualityComparer<T>.Default;
+
+        Stack<T> temp = new ();
+        var toMove = default (T);
+        int stackCount = stack.Count;
+        var count = 0;
+
+        while (stack.Count > 0)
+        {
+            T value = stack.Pop ();
+
+            if (comparer.Equals (value, valueToMove))
+            {
+                toMove = value;
+
+                break;
+            }
+
+            temp.Push (value);
+            count++;
+        }
+
+        var idx = 0;
+
+        while (stack.Count < stackCount)
+        {
+            if (count - idx == index)
+            {
+                stack.Push (toMove);
+            }
+            else
+            {
+                stack.Push (temp.Pop ());
+            }
+
+            idx++;
+        }
+    }
+
+    /// <summary>Replaces an stack object values that match with the value to replace.</summary>
+    /// <typeparam name="T">The stack object type.</typeparam>
+    /// <param name="stack">The stack object.</param>
+    /// <param name="valueToReplace">Value to replace.</param>
+    /// <param name="valueToReplaceWith">Value to replace with to what matches the value to replace.</param>
+    /// <param name="comparer">The comparison object.</param>
+    public static void Replace<T> (
+        this Stack<T> stack,
+        T valueToReplace,
+        T valueToReplaceWith,
+        IEqualityComparer<T> comparer = null
+    )
+    {
+        comparer = comparer ?? EqualityComparer<T>.Default;
+
+        Stack<T> temp = new ();
+
+        while (stack.Count > 0)
+        {
+            T value = stack.Pop ();
+
+            if (comparer.Equals (value, valueToReplace))
+            {
+                stack.Push (valueToReplaceWith);
+
+                break;
+            }
+
+            temp.Push (value);
+        }
+
+        while (temp.Count > 0)
+        {
+            stack.Push (temp.Pop ());
+        }
+    }
+
+    /// <summary>Swap two stack objects values that matches with the both values.</summary>
+    /// <typeparam name="T">The stack object type.</typeparam>
+    /// <param name="stack">The stack object.</param>
+    /// <param name="valueToSwapFrom">Value to swap from.</param>
+    /// <param name="valueToSwapTo">Value to swap to.</param>
+    /// <param name="comparer">The comparison object.</param>
+    public static void Swap<T> (
+        this Stack<T> stack,
+        T valueToSwapFrom,
+        T valueToSwapTo,
+        IEqualityComparer<T> comparer = null
+    )
+    {
+        comparer = comparer ?? EqualityComparer<T>.Default;
+
+        int index = stack.Count - 1;
+        T [] stackArr = new T [stack.Count];
+
+        while (stack.Count > 0)
+        {
+            T value = stack.Pop ();
+
+            if (comparer.Equals (value, valueToSwapFrom))
+            {
+                stackArr [index] = valueToSwapTo;
+            }
+            else if (comparer.Equals (value, valueToSwapTo))
+            {
+                stackArr [index] = valueToSwapFrom;
+            }
+            else
+            {
+                stackArr [index] = value;
+            }
+
+            index--;
+        }
+
+        for (var i = 0; i < stackArr.Length; i++)
+        {
+            stack.Push (stackArr [i]);
+        }
+    }
 }

+ 25 - 11
Terminal.Gui/Terminal.Gui.csproj

@@ -10,6 +10,14 @@
   <!-- =================================================================== -->
   <!-- .NET Build Settings -->
   <!-- =================================================================== -->
+  <PropertyGroup>
+    <TargetFrameworks>net8.0</TargetFrameworks>
+    <LangVersion>12</LangVersion>
+    <RootNamespace>Terminal.Gui</RootNamespace>
+    <AssemblyName>Terminal.Gui</AssemblyName>
+    <SuppressNETCoreSdkPreviewMessage>true</SuppressNETCoreSdkPreviewMessage>
+    <DefineConstants>$(DefineConstants);JETBRAINS_ANNOTATIONS;CONTRACTS_FULL</DefineConstants>
+  </PropertyGroup>
   <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
     <DebugType>portable</DebugType>
     <VersionSuffix></VersionSuffix>
@@ -18,13 +26,6 @@
     <DefineConstants>TRACE;DEBUG_IDISPOSABLE</DefineConstants>
     <DebugType>portable</DebugType>
   </PropertyGroup>
-  <PropertyGroup>
-    <TargetFrameworks>net8.0</TargetFrameworks>
-    <!--<LangVersion>11.0</LangVersion>-->
-    <RootNamespace>Terminal.Gui</RootNamespace>
-    <AssemblyName>Terminal.Gui</AssemblyName>
-    <SuppressNETCoreSdkPreviewMessage>true</SuppressNETCoreSdkPreviewMessage>
-  </PropertyGroup>
   <!-- =================================================================== -->
   <!-- Configuration Manager -->
   <!-- =================================================================== -->
@@ -38,13 +39,18 @@
   <!-- Dependencies -->
   <!-- =================================================================== -->
   <ItemGroup>
+    <PackageReference Include="JetBrains.Annotations" Version="2023.3.0" />
     <PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3" PrivateAssets="all" />
-    <PackageReference Include="System.IO.Abstractions" Version="20.0.4" />
-    <PackageReference Include="System.Text.Json" Version="8.0.1" />
-    <PackageReference Include="System.Management" Version="8.0.0" />
-    <PackageReference Include="Wcwidth" Version="2.0.0" />
     <!-- Enable Nuget Source Link for github -->
     <PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="All" />
+    <PackageReference Include="System.IO.Abstractions" Version="20.0.15" />
+    <PackageReference Include="System.Text.Json" Version="8.0.2" />
+    <PackageReference Include="Wcwidth" Version="2.0.0" />
+  </ItemGroup>
+  <!-- =================================================================== -->
+  <!-- Namespaces for which internal items are visible -->
+  <!-- =================================================================== -->
+  <ItemGroup>
     <InternalsVisibleTo Include="UnitTests" />
   </ItemGroup>
   <PropertyGroup>
@@ -81,6 +87,13 @@
       <LastGenOutput>Strings.Designer.cs</LastGenOutput>
     </EmbeddedResource>
   </ItemGroup>
+  <ItemGroup>
+    <Using Include="JetBrains.Annotations" />
+    <Using Include="System.Diagnostics.Contracts.PureAttribute" Alias="PureAttribute" />
+    <Using Include="System.Drawing" />
+    <Using Include="System.Text" />
+    <Using Include="JetBrains.Annotations" />
+  </ItemGroup>
   <!-- =================================================================== -->
   <!-- Nuget  -->
   <!-- =================================================================== -->
@@ -114,5 +127,6 @@
     <!--<DebugType>Embedded</DebugType>-->
     <Authors>Miguel de Icaza, Tig Kindel (@tig), @BDisp</Authors>
     <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+    <ImplicitUsings>enable</ImplicitUsings>
   </PropertyGroup>
 </Project>

+ 5 - 0
Terminal.Gui/Terminal.Gui.csproj.DotSettings

@@ -0,0 +1,5 @@
+<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
+	<s:String x:Key="/Default/CodeEditing/Localization/Localizable/@EntryValue">Yes</s:String>
+	<s:String x:Key="/Default/CodeInspection/CSharpLanguageProject/LanguageLevel/@EntryValue">CSharp120</s:String>
+	<s:String x:Key="/Default/CodeInspection/Daemon/ConfigureAwaitAnalysisMode/@EntryValue">Library</s:String>
+</wpf:ResourceDictionary>

部分文件因为文件数量过多而无法显示