Browse Source

Merge branch 'v2_develop' into v2_3847_tabview-focus-fix

Tig 8 months ago
parent
commit
37bb43abc2
70 changed files with 7394 additions and 5999 deletions
  1. 1 0
      CommunityToolkitExample/LoginView.cs
  2. 2 2
      NativeAot/Program.cs
  3. 1 1
      SelfContained/Program.cs
  4. 4 4
      Terminal.Gui/Application/Application.Driver.cs
  5. 10 10
      Terminal.Gui/Application/Application.Initialization.cs
  6. 3 3
      Terminal.Gui/Application/Application.Keyboard.cs
  7. 10 5
      Terminal.Gui/Application/Application.Run.cs
  8. 12 3
      Terminal.Gui/Application/Application.Screen.cs
  9. 5 3
      Terminal.Gui/Application/Application.cs
  10. 14 17
      Terminal.Gui/Application/MainLoop.cs
  11. 1 1
      Terminal.Gui/Clipboard/Clipboard.cs
  12. 2 2
      Terminal.Gui/Clipboard/ClipboardBase.cs
  13. 168 454
      Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs
  14. 38 2
      Terminal.Gui/ConsoleDrivers/ConsoleKeyMapping.cs
  15. 279 293
      Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs
  16. 46 27
      Terminal.Gui/ConsoleDrivers/CursesDriver/UnixMainLoop.cs
  17. 0 125
      Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqReq.cs
  18. 28 0
      Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqReqStatus.cs
  19. 98 0
      Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqRequests.cs
  20. 613 132
      Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqUtils.cs
  21. 5 2
      Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs
  22. 1 1
      Terminal.Gui/ConsoleDrivers/FakeDriver/FakeMainLoop.cs
  23. 295 0
      Terminal.Gui/ConsoleDrivers/IConsoleDriver.cs
  24. 321 0
      Terminal.Gui/ConsoleDrivers/KeyCode.cs
  25. 0 1827
      Terminal.Gui/ConsoleDrivers/NetDriver.cs
  26. 790 0
      Terminal.Gui/ConsoleDrivers/NetDriver/NetDriver.cs
  27. 695 0
      Terminal.Gui/ConsoleDrivers/NetDriver/NetEvents.cs
  28. 168 0
      Terminal.Gui/ConsoleDrivers/NetDriver/NetMainLoop.cs
  29. 126 0
      Terminal.Gui/ConsoleDrivers/NetDriver/NetWinVTConsole.cs
  30. 0 2545
      Terminal.Gui/ConsoleDrivers/WindowsDriver.cs
  31. 179 0
      Terminal.Gui/ConsoleDrivers/WindowsDriver/ClipboardImpl.cs
  32. 1094 0
      Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsConsole.cs
  33. 1190 0
      Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsDriver.cs
  34. 240 0
      Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsMainLoop.cs
  35. 1 1
      Terminal.Gui/Drawing/Attribute.cs
  36. 1 1
      Terminal.Gui/Drawing/Cell.cs
  37. 3 3
      Terminal.Gui/Drawing/LineCanvas.cs
  38. 1 1
      Terminal.Gui/Drawing/SixelToRender.cs
  39. 1 1
      Terminal.Gui/Input/Key.cs
  40. 2 2
      Terminal.Gui/README.md
  41. 2 2
      Terminal.Gui/Text/TextFormatter.cs
  42. 8 1
      Terminal.Gui/View/View.Layout.cs
  43. 9 2
      Terminal.Gui/View/View.cs
  44. 1 1
      Terminal.Gui/Views/ColorPicker.Prompt.cs
  45. 1 1
      Terminal.Gui/Views/GraphView/Axis.cs
  46. 1 2
      Terminal.Gui/Views/Menu/Menu.cs
  47. 2 2
      Terminal.Gui/Views/Menu/MenuBar.cs
  48. 1 1
      Terminal.Gui/Views/TableView/TableStyle.cs
  49. 2 2
      Terminal.Gui/Views/TableView/TableView.cs
  50. 5 5
      Terminal.Gui/Views/TreeView/Branch.cs
  51. 1 1
      UICatalog/Scenarios/GraphViewExample.cs
  52. 1 1
      UICatalog/UICatalog.cs
  53. 68 0
      UnitTests/Application/ApplicationScreenTests.cs
  54. 22 1
      UnitTests/Application/ApplicationTests.cs
  55. 6 3
      UnitTests/Application/SynchronizatonContextTests.cs
  56. 2 0
      UnitTests/Configuration/ConfigPropertyTests.cs
  57. 1 1
      UnitTests/ConsoleDrivers/AddRuneTests.cs
  58. 3 3
      UnitTests/ConsoleDrivers/ClipRegionTests.cs
  59. 6 6
      UnitTests/ConsoleDrivers/ConsoleDriverTests.cs
  60. 3 3
      UnitTests/ConsoleDrivers/ContentsTests.cs
  61. 3 3
      UnitTests/ConsoleDrivers/DriverColorTests.cs
  62. 12 12
      UnitTests/ConsoleDrivers/MainLoopDriverTests.cs
  63. 1 1
      UnitTests/Drawing/SixelEncoderTests.cs
  64. 0 72
      UnitTests/Input/EscSeqReqTests.cs
  65. 188 0
      UnitTests/Input/EscSeqRequestsTests.cs
  66. 579 336
      UnitTests/Input/EscSeqUtilsTests.cs
  67. 2 4
      UnitTests/Input/KeyTests.cs
  68. 2 1
      UnitTests/LocalPackagesTests.cs
  69. 14 12
      UnitTests/TestHelpers.cs
  70. 0 52
      bench.json

+ 1 - 0
CommunityToolkitExample/LoginView.cs

@@ -59,6 +59,7 @@ internal partial class LoginView : IRecipient<Message<LoginActions>>
                 }
         }
         SetText();
+        // BUGBUG: This should not be needed:
         Application.LayoutAndDraw ();
     }
 

+ 2 - 2
NativeAot/Program.cs

@@ -9,8 +9,8 @@ namespace NativeAot;
 
 public static class Program
 {
-    [RequiresUnreferencedCode ("Calls Terminal.Gui.Application.Init(ConsoleDriver, String)")]
-    [RequiresDynamicCode ("Calls Terminal.Gui.Application.Init(ConsoleDriver, String)")]
+    [RequiresUnreferencedCode ("Calls Terminal.Gui.Application.Init(IConsoleDriver, String)")]
+    [RequiresDynamicCode ("Calls Terminal.Gui.Application.Init(IConsoleDriver, String)")]
     private static void Main (string [] args)
     {
         Application.Init ();

+ 1 - 1
SelfContained/Program.cs

@@ -9,7 +9,7 @@ namespace SelfContained;
 
 public static class Program
 {
-    [RequiresUnreferencedCode ("Calls Terminal.Gui.Application.Run<T>(Func<Exception, Boolean>, ConsoleDriver)")]
+    [RequiresUnreferencedCode ("Calls Terminal.Gui.Application.Run<T>(Func<Exception, Boolean>, IConsoleDriver)")]
     private static void Main (string [] args)
     {
         Application.Init ();

+ 4 - 4
Terminal.Gui/Application/Application.Driver.cs

@@ -5,14 +5,14 @@ public static partial class Application // Driver abstractions
 {
     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 the <see cref="IConsoleDriver"/> that has been selected. See also <see cref="ForceDriver"/>.</summary>
+    public static IConsoleDriver? Driver { get; internal set; }
 
     // BUGBUG: Force16Colors should be nullable.
     /// <summary>
     ///     Gets or sets whether <see cref="Application.Driver"/> will be forced to output only the 16 colors defined in
     ///     <see cref="ColorName16"/>. The default is <see langword="false"/>, meaning 24-bit (TrueColor) colors will be output
-    ///     as long as the selected <see cref="ConsoleDriver"/> supports TrueColor.
+    ///     as long as the selected <see cref="IConsoleDriver"/> supports TrueColor.
     /// </summary>
     [SerializableConfigurationProperty (Scope = typeof (SettingsScope))]
     public static bool Force16Colors { get; set; }
@@ -23,7 +23,7 @@ public static partial class Application // Driver abstractions
     ///     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
+    ///     Note, <see cref="Application.Init(IConsoleDriver, string)"/> will override this configuration setting if called
     ///     with either `driver` or `driverName` specified.
     /// </remarks>
     [SerializableConfigurationProperty (Scope = typeof (SettingsScope))]

+ 10 - 10
Terminal.Gui/Application/Application.Initialization.cs

@@ -10,7 +10,7 @@ public static partial class Application // 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
+    ///     This function loads the right <see cref="IConsoleDriver"/> for the platform, Creates a <see cref="Toplevel"/>. and
     ///     assigns it to <see cref="Top"/>
     /// </para>
     /// <para>
@@ -21,23 +21,23 @@ public static partial class Application // Initialization (Init/Shutdown)
     /// </para>
     /// <para>
     ///     The <see cref="Run{T}"/> function combines
-    ///     <see cref="Init(Terminal.Gui.ConsoleDriver,string)"/> and <see cref="Run(Toplevel, Func{Exception, bool})"/>
+    ///     <see cref="Init(Terminal.Gui.IConsoleDriver,string)"/> and <see cref="Run(Toplevel, Func{Exception, bool})"/>
     ///     into a single
     ///     call. An application cam use <see cref="Run{T}"/> without explicitly calling
-    ///     <see cref="Init(Terminal.Gui.ConsoleDriver,string)"/>.
+    ///     <see cref="Init(Terminal.Gui.IConsoleDriver,string)"/>.
     /// </para>
     /// <param name="driver">
-    ///     The <see cref="ConsoleDriver"/> to use. If neither <paramref name="driver"/> or
+    ///     The <see cref="IConsoleDriver"/> 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
+    ///     <see cref="IConsoleDriver"/> to use. If neither <paramref name="driver"/> or <paramref name="driverName"/> are
     ///     specified the default driver for the platform will be used.
     /// </param>
     [RequiresUnreferencedCode ("AOT")]
     [RequiresDynamicCode ("AOT")]
-    public static void Init (ConsoleDriver? driver = null, string? driverName = null) { InternalInit (driver, driverName); }
+    public static void Init (IConsoleDriver? driver = null, string? driverName = null) { InternalInit (driver, driverName); }
 
     internal static int MainThreadId { get; set; } = -1;
 
@@ -53,7 +53,7 @@ public static partial class Application // Initialization (Init/Shutdown)
     [RequiresUnreferencedCode ("AOT")]
     [RequiresDynamicCode ("AOT")]
     internal static void InternalInit (
-        ConsoleDriver? driver = null,
+        IConsoleDriver? driver = null,
         string? driverName = null,
         bool calledViaRunT = false
     )
@@ -136,7 +136,7 @@ public static partial class Application // Initialization (Init/Shutdown)
 
                 if (driverType is { })
                 {
-                    Driver = (ConsoleDriver)Activator.CreateInstance (driverType)!;
+                    Driver = (IConsoleDriver)Activator.CreateInstance (driverType)!;
                 }
                 else
                 {
@@ -181,7 +181,7 @@ public static partial class Application // Initialization (Init/Shutdown)
     private static void Driver_KeyUp (object? sender, Key e) { RaiseKeyUpEvent (e); }
     private static void Driver_MouseEvent (object? sender, MouseEventArgs e) { RaiseMouseEvent (e); }
 
-    /// <summary>Gets of list of <see cref="ConsoleDriver"/> types that are available.</summary>
+    /// <summary>Gets of list of <see cref="IConsoleDriver"/> types that are available.</summary>
     /// <returns></returns>
     [RequiresUnreferencedCode ("AOT")]
     public static List<Type?> GetDriverTypes ()
@@ -193,7 +193,7 @@ public static partial class Application // Initialization (Init/Shutdown)
         {
             foreach (Type? type in asm.GetTypes ())
             {
-                if (type.IsSubclassOf (typeof (ConsoleDriver)) && !type.IsAbstract)
+                if (typeof (IConsoleDriver).IsAssignableFrom (type) && !type.IsAbstract && type.IsClass)
                 {
                     driverTypes.Add (type);
                 }

+ 3 - 3
Terminal.Gui/Application/Application.Keyboard.cs

@@ -4,7 +4,7 @@ namespace Terminal.Gui;
 public static partial class Application // Keyboard handling
 {
     /// <summary>
-    ///     Called when the user presses a key (by the <see cref="ConsoleDriver"/>). Raises the cancelable
+    ///     Called when the user presses a key (by the <see cref="IConsoleDriver"/>). Raises the cancelable
     ///     <see cref="KeyDown"/> event, then calls <see cref="View.NewKeyDownEvent"/> on all top level views, and finally
     ///     if the key was not handled, invokes any Application-scoped <see cref="KeyBindings"/>.
     /// </summary>
@@ -116,7 +116,7 @@ public static partial class Application // Keyboard handling
     public static event EventHandler<Key>? KeyDown;
 
     /// <summary>
-    ///     Called when the user releases a key (by the <see cref="ConsoleDriver"/>). Raises the cancelable <see cref="KeyUp"/>
+    ///     Called when the user releases a key (by the <see cref="IConsoleDriver"/>). Raises the cancelable <see cref="KeyUp"/>
     ///     event
     ///     then calls <see cref="View.NewKeyUpEvent"/> on all top level views. Called after <see cref="RaiseKeyDownEvent"/>.
     /// </summary>
@@ -205,7 +205,7 @@ public static partial class Application // Keyboard handling
                     Command.Refresh,
                     static () =>
                     {
-                        LayoutAndDraw ();
+                        LayoutAndDraw (true);
 
                         return true;
                     }

+ 10 - 5
Terminal.Gui/Application/Application.Run.cs

@@ -305,7 +305,7 @@ public static partial class Application // Run (Begin, Run, End, Stop)
     /// <returns>The created <see cref="Toplevel"/> object. The caller is responsible for disposing this object.</returns>
     [RequiresUnreferencedCode ("AOT")]
     [RequiresDynamicCode ("AOT")]
-    public static Toplevel Run (Func<Exception, bool>? errorHandler = null, ConsoleDriver? driver = null) { return Run<Toplevel> (errorHandler, driver); }
+    public static Toplevel Run (Func<Exception, bool>? errorHandler = null, IConsoleDriver? driver = null) { return Run<Toplevel> (errorHandler, driver); }
 
     /// <summary>
     ///     Runs the application by creating a <see cref="Toplevel"/>-derived object of type <c>T</c> and calling
@@ -323,14 +323,14 @@ public static partial class Application // Run (Begin, Run, End, Stop)
     /// </remarks>
     /// <param name="errorHandler"></param>
     /// <param name="driver">
-    ///     The <see cref="ConsoleDriver"/> to use. If not specified the default driver for the platform will
+    ///     The <see cref="IConsoleDriver"/> 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>
     /// <returns>The created T object. The caller is responsible for disposing this object.</returns>
     [RequiresUnreferencedCode ("AOT")]
     [RequiresDynamicCode ("AOT")]
-    public static T Run<T> (Func<Exception, bool>? errorHandler = null, ConsoleDriver? driver = null)
+    public static T Run<T> (Func<Exception, bool>? errorHandler = null, IConsoleDriver? driver = null)
         where T : Toplevel, new()
     {
         if (!Initialized)
@@ -369,7 +369,7 @@ public static partial class Application // Run (Begin, Run, End, Stop)
     ///         return control immediately.
     ///     </para>
     ///     <para>When using <see cref="Run{T}"/> or
-    ///         <see cref="Run(System.Func{System.Exception,bool},Terminal.Gui.ConsoleDriver)"/>
+    ///         <see cref="Run(System.Func{System.Exception,bool},Terminal.Gui.IConsoleDriver)"/>
     ///         <see cref="Init"/> will be called automatically.
     ///     </para>
     ///     <para>
@@ -505,6 +505,11 @@ public static partial class Application // Run (Begin, Run, End, Stop)
     {
         bool neededLayout = View.Layout (TopLevels.Reverse (), Screen.Size);
 
+        if (ClearScreenNextIteration)
+        {
+            forceDraw = true;
+            ClearScreenNextIteration = false;
+        }
         if (forceDraw)
         {
             Driver?.ClearContents ();
@@ -688,6 +693,6 @@ public static partial class Application // Run (Begin, Run, End, Stop)
         runState.Toplevel = null;
         runState.Dispose ();
 
-        LayoutAndDraw ();
+        LayoutAndDraw (true);
     }
 }

+ 12 - 3
Terminal.Gui/Application/Application.Screen.cs

@@ -6,11 +6,11 @@ public static partial class Application // Screen related stuff
     private static Rectangle? _screen;
 
     /// <summary>
-    ///     Gets or sets the size of the screen. By default, this is the size of the screen as reported by the <see cref="ConsoleDriver"/>.
+    ///     Gets or sets the size of the screen. By default, this is the size of the screen as reported by the <see cref="IConsoleDriver"/>.
     /// </summary>
     /// <remarks>
     /// <para>
-    ///     If the <see cref="ConsoleDriver"/> has not been initialized, this will return a default size of 2048x2048; useful for unit tests.
+    ///     If the <see cref="IConsoleDriver"/> has not been initialized, this will return a default size of 2048x2048; useful for unit tests.
     /// </para>
     /// </remarks>
     public static Rectangle Screen
@@ -63,8 +63,17 @@ public static partial class Application // Screen related stuff
             t.SetNeedsLayout ();
         }
 
-        LayoutAndDraw ();
+        LayoutAndDraw (true);
 
         return true;
     }
+
+    /// <summary>
+    ///     Gets or sets whether the screen will be cleared, and all Views redrawn, during the next Application iteration.
+    /// </summary>
+    /// <remarks>
+    ///     This is typicall set to true when a View's <see cref="View.Frame"/> changes and that view has no
+    ///     SuperView (e.g. when <see cref="Application.Top"/> is moved or resized.
+    /// </remarks>
+    public static bool ClearScreenNextIteration { get; set; }
 }

+ 5 - 3
Terminal.Gui/Application/Application.cs

@@ -32,7 +32,7 @@ public static partial class Application
     /// <returns>A string representation of the Application </returns>
     public new static string ToString ()
     {
-        ConsoleDriver? driver = Driver;
+        IConsoleDriver? driver = Driver;
 
         if (driver is null)
         {
@@ -43,11 +43,11 @@ public static partial class Application
     }
 
     /// <summary>
-    ///     Gets a string representation of the Application rendered by the provided <see cref="ConsoleDriver"/>.
+    ///     Gets a string representation of the Application rendered by the provided <see cref="IConsoleDriver"/>.
     /// </summary>
     /// <param name="driver">The driver to use to render the contents.</param>
     /// <returns>A string representation of the Application </returns>
-    public static string ToString (ConsoleDriver? driver)
+    public static string ToString (IConsoleDriver? driver)
     {
         if (driver is null)
         {
@@ -215,6 +215,8 @@ public static partial class Application
 
         Navigation = null;
 
+        ClearScreenNextIteration = false;
+
         AddApplicationKeyBindings ();
 
         // Reset synchronization context to allow the user to run async/await,

+ 14 - 17
Terminal.Gui/Application/MainLoop.cs

@@ -1,4 +1,5 @@
-//
+#nullable enable
+//
 // MainLoop.cs: IMainLoopDriver and MainLoop for Terminal.Gui
 //
 // Authors:
@@ -36,7 +37,7 @@ internal interface IMainLoopDriver
 ///     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
+public class MainLoop : IDisposable
 {
     internal List<Func<bool>> _idleHandlers = new ();
     internal SortedList<long, Timeout> _timeouts = new ();
@@ -49,7 +50,7 @@ internal class MainLoop : IDisposable
     /// <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,
+    ///     The <see cref="IConsoleDriver"/> instance (one of the implementations FakeMainLoop, UnixMainLoop,
     ///     NetMainLoop or WindowsMainLoop).
     /// </param>
     internal MainLoop (IMainLoopDriver driver)
@@ -72,7 +73,7 @@ internal class MainLoop : IDisposable
 
     /// <summary>The current <see cref="IMainLoopDriver"/> in use.</summary>
     /// <value>The main loop driver.</value>
-    internal IMainLoopDriver MainLoopDriver { get; private set; }
+    internal IMainLoopDriver? MainLoopDriver { get; private set; }
 
     /// <summary>Used for unit tests.</summary>
     internal bool Running { get; set; }
@@ -117,7 +118,7 @@ internal class MainLoop : IDisposable
             _idleHandlers.Add (idleHandler);
         }
 
-        MainLoopDriver.Wakeup ();
+        MainLoopDriver?.Wakeup ();
 
         return idleHandler;
     }
@@ -130,10 +131,7 @@ internal class MainLoop : IDisposable
     /// </remarks>
     internal object AddTimeout (TimeSpan time, Func<bool> callback)
     {
-        if (callback is null)
-        {
-            throw new ArgumentNullException (nameof (callback));
-        }
+        ArgumentNullException.ThrowIfNull (callback);
 
         var timeout = new Timeout { Span = time, Callback = callback };
         AddTimeout (time, timeout);
@@ -156,7 +154,7 @@ internal class MainLoop : IDisposable
 
         waitTimeout = 0;
 
-        lock (_timeouts)
+        lock (_timeoutsLockToken)
         {
             if (_timeouts.Count > 0)
             {
@@ -191,7 +189,7 @@ internal class MainLoop : IDisposable
     ///     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 (); }
+    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>
@@ -225,7 +223,7 @@ internal class MainLoop : IDisposable
     {
         lock (_timeoutsLockToken)
         {
-            int idx = _timeouts.IndexOfValue (token as Timeout);
+            int idx = _timeouts.IndexOfValue ((token as Timeout)!);
 
             if (idx == -1)
             {
@@ -262,7 +260,7 @@ internal class MainLoop : IDisposable
     /// </remarks>
     internal void RunIteration ()
     {
-        lock (_timeouts)
+        lock (_timeoutsLockToken)
         {
             if (_timeouts.Count > 0)
             {
@@ -270,9 +268,9 @@ internal class MainLoop : IDisposable
             }
         }
 
-        MainLoopDriver.Iteration ();
+        MainLoopDriver?.Iteration ();
 
-        var runIdle = false;
+        bool runIdle;
 
         lock (_idleHandlersLock)
         {
@@ -296,8 +294,7 @@ internal class MainLoop : IDisposable
     ///     Invoked when a new timeout is added. To be used in the case when
     ///     <see cref="Application.EndAfterFirstIteration"/> is <see langword="true"/>.
     /// </summary>
-    [CanBeNull]
-    internal event EventHandler<TimeoutEventArgs> TimeoutAdded;
+    internal event EventHandler<TimeoutEventArgs>? TimeoutAdded;
 
     /// <summary>Wakes up the <see cref="MainLoop"/> that might be waiting on input.</summary>
     internal void Wakeup () { MainLoopDriver?.Wakeup (); }

+ 1 - 1
Terminal.Gui/Clipboard/Clipboard.cs

@@ -111,7 +111,7 @@ public static class Clipboard
 
 /// <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.
+///     CursesDriver, but also used in Unit tests which is why it is in IConsoleDriver.cs.
 /// </summary>
 internal static class ClipboardProcessRunner
 {

+ 2 - 2
Terminal.Gui/Clipboard/ClipboardBase.cs

@@ -103,7 +103,7 @@ public abstract class ClipboardBase : IClipboard
     }
 
     /// <summary>
-    ///     Returns the contents of the OS clipboard if possible. Implemented by <see cref="ConsoleDriver"/>-specific
+    ///     Returns the contents of the OS clipboard if possible. Implemented by <see cref="IConsoleDriver"/>-specific
     ///     subclasses.
     /// </summary>
     /// <returns>The contents of the OS clipboard if successful.</returns>
@@ -111,7 +111,7 @@ public abstract class ClipboardBase : IClipboard
     protected abstract string GetClipboardDataImpl ();
 
     /// <summary>
-    ///     Pastes the <paramref name="text"/> to the OS clipboard if possible. Implemented by <see cref="ConsoleDriver"/>
+    ///     Pastes the <paramref name="text"/> to the OS clipboard if possible. Implemented by <see cref="IConsoleDriver"/>
     ///     -specific subclasses.
     /// </summary>
     /// <param name="text">The text to paste to the OS clipboard.</param>

+ 168 - 454
Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs

@@ -1,7 +1,4 @@
 #nullable enable
-//
-// ConsoleDriver.cs: Base class for Terminal.Gui ConsoleDriver implementations.
-//
 
 using System.Diagnostics;
 
@@ -13,24 +10,56 @@ namespace Terminal.Gui;
 ///     <see cref="WindowsDriver"/> - <see cref="NetDriver"/> that uses the .NET Console API - <see cref="FakeConsole"/>
 ///     for unit testing.
 /// </remarks>
-public abstract class ConsoleDriver
+public abstract class ConsoleDriver : IConsoleDriver
 {
+    /// <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>Get the operating system clipboard.</summary>
+    public IClipboard? Clipboard { get; internal set; }
+
+    /// <summary>Returns the name of the driver and relevant library version information.</summary>
+    /// <returns></returns>
+    public virtual string GetVersionInfo () { return GetType ().Name; }
+
+    #region ANSI Esc Sequence Handling
+
+    // QUESTION: This appears to be an API to help in debugging. It's only implemented in CursesDriver and WindowsDriver.
+    // QUESTION: Can it be factored such that it does not contaminate the ConsoleDriver API?
+    /// <summary>
+    ///     Provide proper writing to send escape sequence recognized by the <see cref="ConsoleDriver"/>.
+    /// </summary>
+    /// <param name="ansi"></param>
+    public abstract void WriteRaw (string ansi);
+
+    #endregion ANSI Esc Sequence Handling
+
+    #region Screen and Contents
+
     // 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;
 
     // QUESTION: When non-full screen apps are supported, will this represent the app size, or will that be in Application?
     /// <summary>Gets the location and size of the terminal screen.</summary>
-    internal Rectangle Screen => new (0, 0, Cols, Rows);
+    public Rectangle Screen => new (0, 0, Cols, Rows);
 
-    private Region? _clip = null;
+    private Region? _clip;
 
     /// <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 of <see cref="Clip"/> region.</value>
-    internal Region? Clip
+    public Region? Clip
     {
         get => _clip;
         set
@@ -50,17 +79,14 @@ public abstract class ConsoleDriver
         }
     }
 
-    /// <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>
-    internal int Col { get; private set; }
+    public int Col { get; private set; }
 
     /// <summary>The number of columns visible in the terminal.</summary>
-    internal virtual int Cols
+    public virtual int Cols
     {
         get => _cols;
         set
@@ -75,19 +101,56 @@ public abstract class ConsoleDriver
     ///     <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>
-    internal Cell [,]? Contents { get; set; }
+    public Cell [,]? Contents { get; set; }
 
     /// <summary>The leftmost column in the terminal.</summary>
-    internal virtual int Left { get; set; } = 0;
+    public virtual int Left { get; set; } = 0;
+
+    /// <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 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)
+    {
+        //Debug.Assert (col >= 0 && row >= 0 && col < Contents.GetLength(1) && row < Contents.GetLength(0));
+        Col = col;
+        Row = row;
+    }
 
     /// <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>
-    internal int Row { get; private set; }
+    public int Row { get; private set; }
 
     /// <summary>The number of rows visible in the terminal.</summary>
-    internal virtual int Rows
+    public virtual int Rows
     {
         get => _rows;
         set
@@ -98,18 +161,7 @@ public abstract class ConsoleDriver
     }
 
     /// <summary>The topmost row in the terminal.</summary>
-    internal virtual int Top { get; 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; }
+    public virtual int Top { get; set; } = 0;
 
     /// <summary>Adds the specified rune to the display at the current cursor position.</summary>
     /// <remarks>
@@ -125,7 +177,7 @@ public abstract class ConsoleDriver
     ///     </para>
     /// </remarks>
     /// <param name="rune">Rune to add.</param>
-    internal void AddRune (Rune rune)
+    public void AddRune (Rune rune)
     {
         int runeWidth = -1;
         bool validLocation = IsValidLocation (rune, Col, Row);
@@ -151,7 +203,7 @@ public abstract class ConsoleDriver
                     // 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 
+                    // 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)
@@ -174,7 +226,7 @@ public abstract class ConsoleDriver
                             if (normalized.Length == 1)
                             {
                                 // It normalized! We can just set the Cell to the left with the
-                                // normalized codepoint 
+                                // normalized codepoint
                                 Contents [Row, Col - 1].Rune = (Rune)normalized [0];
 
                                 // Ignore. Don't move to next column because we're already there
@@ -300,7 +352,7 @@ public abstract class ConsoleDriver
     ///     convenience method that calls <see cref="AddRune(Rune)"/> with the <see cref="Rune"/> constructor.
     /// </summary>
     /// <param name="c">Character to add.</param>
-    internal void AddRune (char c) { AddRune (new Rune (c)); }
+    public void AddRune (char c) { AddRune (new Rune (c)); }
 
     /// <summary>Adds the <paramref name="str"/> to the display at the cursor position.</summary>
     /// <remarks>
@@ -312,7 +364,7 @@ public abstract class ConsoleDriver
     ///     <para>If <paramref name="str"/> requires more columns than are available, the output will be clipped.</para>
     /// </remarks>
     /// <param name="str">String.</param>
-    internal void AddStr (string str)
+    public void AddStr (string str)
     {
         List<Rune> runes = str.EnumerateRunes ().ToList ();
 
@@ -322,8 +374,39 @@ public abstract class ConsoleDriver
         }
     }
 
+    /// <summary>Fills the specified rectangle with the specified rune, using <see cref="CurrentAttribute"/></summary>
+    /// <remarks>
+    ///     The value of <see cref="Clip"/> is honored. Any parts of the rectangle not in the clip will not be drawn.
+    /// </remarks>
+    /// <param name="rect">The Screen-relative rectangle.</param>
+    /// <param name="rune">The Rune used to fill the rectangle</param>
+    public void FillRect (Rectangle rect, Rune rune = default)
+    {
+        // BUGBUG: This should be a method on Region
+        rect = Rectangle.Intersect (rect, Clip?.GetBounds () ?? Screen);
+        lock (Contents!)
+        {
+            for (int r = rect.Y; r < rect.Y + rect.Height; r++)
+            {
+                for (int c = rect.X; c < rect.X + rect.Width; c++)
+                {
+                    if (!IsValidLocation (rune, c, r))
+                    {
+                        continue;
+                    }
+                    Contents [r, c] = new Cell
+                    {
+                        Rune = rune != default ? rune : (Rune)' ',
+                        Attribute = CurrentAttribute, IsDirty = true
+                    };
+                    _dirtyLines! [r] = true;
+                }
+            }
+        }
+    }
+
     /// <summary>Clears the <see cref="Contents"/> of the driver.</summary>
-    internal void ClearContents ()
+    public void ClearContents ()
     {
         Contents = new Cell [Rows, Cols];
 
@@ -339,13 +422,14 @@ public abstract class ConsoleDriver
             {
                 for (var c = 0; c < Cols; c++)
                 {
-                    Contents [row, c] = new Cell
+                    Contents [row, c] = new ()
                     {
                         Rune = (Rune)' ',
                         Attribute = new Attribute (Color.White, Color.Black),
                         IsDirty = true
                     };
                 }
+
                 _dirtyLines [row] = true;
             }
         }
@@ -362,7 +446,7 @@ public abstract class ConsoleDriver
     /// Sets <see cref="Contents"/> as dirty for situations where views
     /// don't need layout and redrawing, but just refresh the screen.
     /// </summary>
-    internal void SetContentsAsDirty ()
+    public void SetContentsAsDirty ()
     {
         lock (Contents!)
         {
@@ -372,42 +456,8 @@ public abstract class ConsoleDriver
                 {
                     Contents [row, c].IsDirty = true;
                 }
-                _dirtyLines! [row] = true;
-            }
-        }
-    }
-
-    /// <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 ();
 
-    /// <summary>Fills the specified rectangle with the specified rune, using <see cref="CurrentAttribute"/></summary>
-    /// <remarks>
-    /// The value of <see cref="Clip"/> is honored. Any parts of the rectangle not in the clip will not be drawn.
-    /// </remarks>
-    /// <param name="rect">The Screen-relative rectangle.</param>
-    /// <param name="rune">The Rune used to fill the rectangle</param>
-    internal void FillRect (Rectangle rect, Rune rune = default)
-    {
-        // BUGBUG: This should be a method on Region
-        rect = Rectangle.Intersect (rect, Clip?.GetBounds () ?? Screen);
-        lock (Contents!)
-        {
-            for (int r = rect.Y; r < rect.Y + rect.Height; r++)
-            {
-                for (int c = rect.X; c < rect.X + rect.Width; c++)
-                {
-                    if (!IsValidLocation (rune, c, r))
-                    {
-                        continue;
-                    }
-                    Contents [r, c] = new Cell
-                    {
-                        Rune = (rune != default ? rune : (Rune)' '),
-                        Attribute = CurrentAttribute, IsDirty = true
-                    };
-                    _dirtyLines! [r] = true;
-                }
+                _dirtyLines! [row] = true;
             }
         }
     }
@@ -418,25 +468,21 @@ public abstract class ConsoleDriver
     /// </summary>
     /// <param name="rect"></param>
     /// <param name="c"></param>
-    internal void FillRect (Rectangle rect, char c) { FillRect (rect, new Rune (c)); }
+    public void FillRect (Rectangle rect, char c) { FillRect (rect, new Rune (c)); }
+
+    #endregion Screen and Contents
+
+    #region Cursor Handling
+
+    /// <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 ();
 
     /// <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 the specified Rune.</summary>
     /// <param name="rune">Used to determine if one or two columns are required.</param>
     /// <param name="col">The column.</param>
@@ -445,7 +491,7 @@ public abstract class ConsoleDriver
     ///     <see langword="false"/> if the coordinate is outside the screen bounds or outside of <see cref="Clip"/>.
     ///     <see langword="true"/> otherwise.
     /// </returns>
-    internal bool IsValidLocation (Rune rune, int col, int row)
+    public bool IsValidLocation (Rune rune, int col, int row)
     {
         if (rune.GetColumns () < 2)
         {
@@ -458,33 +504,12 @@ public abstract class ConsoleDriver
         }
     }
 
-    // TODO: Make internal once Menu is upgraded
-    /// <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)
-    {
-        //Debug.Assert (col >= 0 && row >= 0 && col < Contents.GetLength(1) && row < Contents.GetLength(0));
-        Col = col;
-        Row = row;
-    }
-
     /// <summary>Called when the terminal size changes. Fires the <see cref="SizeChanged"/> event.</summary>
     /// <param name="args"></param>
-    internal void OnSizeChanged (SizeChangedEventArgs args) { SizeChanged?.Invoke (this, args); }
+    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>
-    internal void Refresh ()
+    public void Refresh ()
     {
         bool updated = UpdateScreen ();
         UpdateCursor ();
@@ -505,6 +530,8 @@ public abstract class ConsoleDriver
     /// <summary>The event fired when the terminal is resized.</summary>
     public event EventHandler<SizeChangedEventArgs>? SizeChanged;
 
+    #endregion Cursor Handling
+
     /// <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 ();
@@ -520,10 +547,10 @@ public abstract class ConsoleDriver
 
     /// <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 ();
+    public abstract MainLoop Init ();
 
     /// <summary>Ends the execution of the console driver.</summary>
-    internal abstract void End ();
+    public abstract void End ();
 
     #endregion
 
@@ -544,7 +571,7 @@ public abstract class ConsoleDriver
     ///         <see langword="false"/>, indicating that the <see cref="ConsoleDriver"/> cannot support TrueColor.
     ///     </para>
     /// </remarks>
-    internal virtual bool Force16Colors
+    public virtual bool Force16Colors
     {
         get => Application.Force16Colors || !SupportsTrueColor;
         set => Application.Force16Colors = value || !SupportsTrueColor;
@@ -566,7 +593,7 @@ public abstract class ConsoleDriver
             // TODO: This makes ConsoleDriver dependent on Application, which is not ideal. Once Attribute.PlatformColor is removed, this can be fixed.
             if (Application.Driver is { })
             {
-                _currentAttribute = new Attribute (value.Foreground, value.Background);
+                _currentAttribute = new (value.Foreground, value.Background);
 
                 return;
             }
@@ -578,7 +605,7 @@ public abstract class ConsoleDriver
     /// <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>
-    internal Attribute SetAttribute (Attribute c)
+    public Attribute SetAttribute (Attribute c)
     {
         Attribute prevAttribute = CurrentAttribute;
         CurrentAttribute = c;
@@ -588,7 +615,7 @@ public abstract class ConsoleDriver
 
     /// <summary>Gets the current <see cref="Attribute"/>.</summary>
     /// <returns>The current attribute.</returns>
-    internal Attribute GetAttribute () { return CurrentAttribute; }
+    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).
@@ -599,16 +626,33 @@ public abstract class ConsoleDriver
     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
-                             );
+        return new (
+                    -1, // only used by cursesdriver!
+                    foreground,
+                    background
+                   );
     }
 
-    #endregion
+    #endregion Color Handling
+
+    #region Mouse Handling
+
+    /// <summary>Event fired when a mouse event occurs.</summary>
+    public event EventHandler<MouseEventArgs>? MouseEvent;
+
+    /// <summary>Called when a mouse event occurs. Fires the <see cref="MouseEvent"/> event.</summary>
+    /// <param name="a"></param>
+    public void OnMouseEvent (MouseEventArgs a)
+    {
+        // Ensure ScreenPosition is set
+        a.ScreenPosition = a.Position;
+
+        MouseEvent?.Invoke (this, a);
+    }
+
+    #endregion Mouse Handling
 
-    #region Mouse and Keyboard
+    #region Keyboard Handling
 
     /// <summary>Event fired when a key is pressed down. This is a precursor to <see cref="KeyUp"/>.</summary>
     public event EventHandler<Key>? KeyDown;
@@ -635,19 +679,8 @@ public abstract class ConsoleDriver
     /// <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<MouseEventArgs>? MouseEvent;
-
-    /// <summary>Called when a mouse event occurs. Fires the <see cref="MouseEvent"/> event.</summary>
-    /// <param name="a"></param>
-    public void OnMouseEvent (MouseEventArgs a)
-    {
-        // Ensure ScreenPosition is set
-        a.ScreenPosition = a.Position;
-
-        MouseEvent?.Invoke (this, a);
-    }
-
+    // TODO: Remove this API - it was needed when we didn't have a reliable way to simulate key presses.
+    // TODO: We now do: Applicaiton.RaiseKeyDown and Application.RaiseKeyUp
     /// <summary>Simulates a key press.</summary>
     /// <param name="keyChar">The key character.</param>
     /// <param name="key">The key.</param>
@@ -656,324 +689,5 @@ public abstract class ConsoleDriver
     /// <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>
-///     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>
-/// </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 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
+    #endregion Keyboard Handling
 }

+ 38 - 2
Terminal.Gui/ConsoleDrivers/ConsoleKeyMapping.cs

@@ -3,6 +3,7 @@ using System.Runtime.InteropServices;
 
 namespace Terminal.Gui.ConsoleDrivers;
 
+// QUESTION: This class combines Windows specific code with cross-platform code. Should this be split into two classes?
 /// <summary>Helper class to handle the scan code and virtual key from a <see cref="ConsoleKey"/>.</summary>
 public static class ConsoleKeyMapping
 {
@@ -249,7 +250,7 @@ public static class ConsoleKeyMapping
     {
         var modifiers = new ConsoleModifiers ();
 
-        if (key.HasFlag (KeyCode.ShiftMask))
+        if (key.HasFlag (KeyCode.ShiftMask) || char.IsUpper ((char)key))
         {
             modifiers |= ConsoleModifiers.Shift;
         }
@@ -590,7 +591,8 @@ public static class ConsoleKeyMapping
 
                 if (uc != UnicodeCategory.NonSpacingMark && uc != UnicodeCategory.OtherLetter)
                 {
-                    consoleKey = char.ToUpper (stFormD [i]);
+                    char ck = char.ToUpper (stFormD [i]);
+                    consoleKey = (uint)(ck > 0 && ck <= 255 ? char.ToUpper (stFormD [i]) : 0);
                     scode = GetScanCode ("VirtualKey", char.ToUpper (stFormD [i]), 0);
 
                     if (scode is { })
@@ -704,6 +706,32 @@ public static class ConsoleKeyMapping
                 return (uint)ConsoleKey.F24;
             case KeyCode.Tab | KeyCode.ShiftMask:
                 return (uint)ConsoleKey.Tab;
+            case KeyCode.Space:
+                return (uint)ConsoleKey.Spacebar;
+            default:
+                uint c = (char)keyValue;
+
+                if (c is >= (char)ConsoleKey.A and <= (char)ConsoleKey.Z)
+                {
+                    return c;
+                }
+
+                if ((c - 32) is >= (char)ConsoleKey.A and <= (char)ConsoleKey.Z)
+                {
+                    return (c - 32);
+                }
+
+                if (Enum.IsDefined (typeof (ConsoleKey), keyValue.ToString ()))
+                {
+                    return (uint)keyValue;
+                }
+
+                // DEL
+                if ((uint)keyValue == 127)
+                {
+                    return (uint)ConsoleKey.Backspace;
+                }
+                break;
         }
 
         isConsoleKey = false;
@@ -867,6 +895,14 @@ public static class ConsoleKeyMapping
             case ConsoleKey.Tab:
                 keyCode = KeyCode.Tab;
 
+                break;
+            case ConsoleKey.Spacebar:
+                keyCode = KeyCode.Space;
+
+                break;
+            case ConsoleKey.Backspace:
+                keyCode = KeyCode.Backspace;
+
                 break;
             default:
                 if ((int)consoleKeyInfo.KeyChar is >= 1 and <= 26)

+ 279 - 293
Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs

@@ -1,25 +1,20 @@
-//
+#nullable enable
+//
 // Driver.cs: Curses-based Driver
 //
 
-using System.Diagnostics;
 using System.Runtime.InteropServices;
 using Terminal.Gui.ConsoleDrivers;
 using Unix.Terminal;
 
 namespace Terminal.Gui;
 
-/// <summary>This is the Curses driver for the gui.cs/Terminal framework.</summary>
+/// <summary>A Linux/Mac driver based on the Curses library.</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 string GetVersionInfo () { return $"{Curses.curses_version ()}"; }
 
-    internal override int Cols
+    public override int Cols
     {
         get => Curses.Cols;
         set
@@ -29,7 +24,7 @@ internal class CursesDriver : ConsoleDriver
         }
     }
 
-    internal override int Rows
+    public override int Rows
     {
         get => Curses.Lines;
         set
@@ -39,45 +34,6 @@ internal class CursesDriver : ConsoleDriver
         }
     }
 
-    public override bool SupportsTrueColor => true;
-
-    /// <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
@@ -101,34 +57,33 @@ internal class CursesDriver : ConsoleDriver
         {
             // Not a valid location (outside screen or clip region)
             // Move within the clip region, then AddRune will actually move to Col, Row
-            Rectangle clipRect = Clip.GetBounds ();
+            Rectangle clipRect = Clip!.GetBounds ();
             Curses.move (clipRect.Y, clipRect.X);
         }
     }
 
-    
     public override void SendKeys (char keyChar, ConsoleKey consoleKey, bool shift, bool alt, bool control)
     {
         KeyCode key;
 
         if (consoleKey == ConsoleKey.Packet)
         {
-            var mod = new ConsoleModifiers ();
+            //var mod = new ConsoleModifiers ();
 
-            if (shift)
-            {
-                mod |= ConsoleModifiers.Shift;
-            }
+            //if (shift)
+            //{
+            //    mod |= ConsoleModifiers.Shift;
+            //}
 
-            if (alt)
-            {
-                mod |= ConsoleModifiers.Alt;
-            }
+            //if (alt)
+            //{
+            //    mod |= ConsoleModifiers.Alt;
+            //}
 
-            if (control)
-            {
-                mod |= ConsoleModifiers.Control;
-            }
+            //if (control)
+            //{
+            //    mod |= ConsoleModifiers.Control;
+            //}
 
             var cKeyInfo = new ConsoleKeyInfo (keyChar, consoleKey, shift, alt, control);
             cKeyInfo = ConsoleKeyMapping.DecodeVKPacketToKConsoleKeyInfo (cKeyInfo);
@@ -139,40 +94,12 @@ internal class CursesDriver : ConsoleDriver
             key = (KeyCode)keyChar;
         }
 
-        OnKeyDown (new Key (key));
-        OnKeyUp (new Key (key));
+        OnKeyDown (new (key));
+        OnKeyUp (new (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)
@@ -213,14 +140,18 @@ internal class CursesDriver : ConsoleDriver
 
         if (!RunningUnitTests && Col >= 0 && Col < Cols && Row >= 0 && Row < Rows)
         {
-            Curses.move (Row, Col);
-
             if (Force16Colors)
             {
+                Curses.move (Row, Col);
+
                 Curses.raw ();
                 Curses.noecho ();
                 Curses.refresh ();
             }
+            else
+            {
+                _mainLoopDriver?.WriteRaw (EscSeqUtils.CSI_SetCursorPosition (Row + 1, Col + 1));
+            }
         }
     }
 
@@ -231,7 +162,7 @@ internal class CursesDriver : ConsoleDriver
         {
             for (var row = 0; row < Rows; row++)
             {
-                if (!_dirtyLines [row])
+                if (!_dirtyLines! [row])
                 {
                     continue;
                 }
@@ -240,7 +171,7 @@ internal class CursesDriver : ConsoleDriver
 
                 for (var col = 0; col < Cols; col++)
                 {
-                    if (Contents [row, col].IsDirty == false)
+                    if (Contents! [row, col].IsDirty == false)
                     {
                         continue;
                     }
@@ -284,14 +215,14 @@ internal class CursesDriver : ConsoleDriver
             if (!RunningUnitTests)
             {
                 Curses.move (Row, Col);
-                _window.wrefresh ();
+                _window?.wrefresh ();
             }
         }
         else
         {
             if (RunningUnitTests
                 || Console.WindowHeight < 1
-                || Contents.Length != Rows * Cols
+                || Contents!.Length != Rows * Cols
                 || Rows != Console.WindowHeight)
             {
                 return updated;
@@ -315,7 +246,7 @@ internal class CursesDriver : ConsoleDriver
                     return updated;
                 }
 
-                if (!_dirtyLines [row])
+                if (!_dirtyLines! [row])
                 {
                     continue;
                 }
@@ -360,7 +291,7 @@ internal class CursesDriver : ConsoleDriver
                             lastCol = col;
                         }
 
-                        Attribute attr = Contents [row, col].Attribute.Value;
+                        Attribute attr = Contents [row, col].Attribute!.Value;
 
                         // Performance: Only send the escape sequence if the attribute has changed.
                         if (attr != redrawAttr)
@@ -419,10 +350,10 @@ internal class CursesDriver : ConsoleDriver
             }
 
             // SIXELS
-            foreach (var s in Application.Sixel)
+            foreach (SixelToRender s in Application.Sixel)
             {
                 SetCursorPosition (s.ScreenPosition.X, s.ScreenPosition.Y);
-                Console.Write(s.SixelData);
+                Console.Write (s.SixelData);
             }
 
             SetCursorPosition (0, 0);
@@ -442,40 +373,218 @@ internal class CursesDriver : ConsoleDriver
         return updated;
     }
 
-    private bool SetCursorPosition (int col, int row)
+    #region Color Handling
+
+    public override bool SupportsTrueColor => true;
+
+    /// <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)
     {
-        // + 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));
+        //var v = (short)((ushort)foreground | (background << 4));
+        var v = (short)(((ushort)(foreground & 0xffff) << 16) | (background & 0xffff));
+
+        // TODO: for TrueColor - Use InitExtendedPair
+        Curses.InitColorPair (v, foreground, background);
+
+        return new (
+                    Curses.ColorPair (v),
+                    CursesColorNumberToColorName16 (foreground),
+                    CursesColorNumberToColorName16 (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 && Force16Colors)
+        {
+            return MakeColor (
+                              ColorNameToCursesColorNumber (foreground.GetClosestNamedColor16 ()),
+                              ColorNameToCursesColorNumber (background.GetClosestNamedColor16 ())
+                             );
+        }
+
+        return new (
+                    0,
+                    foreground,
+                    background
+                   );
+    }
+
+    private static short ColorNameToCursesColorNumber (ColorName16 color)
+    {
+        switch (color)
+        {
+            case ColorName16.Black:
+                return Curses.COLOR_BLACK;
+            case ColorName16.Blue:
+                return Curses.COLOR_BLUE;
+            case ColorName16.Green:
+                return Curses.COLOR_GREEN;
+            case ColorName16.Cyan:
+                return Curses.COLOR_CYAN;
+            case ColorName16.Red:
+                return Curses.COLOR_RED;
+            case ColorName16.Magenta:
+                return Curses.COLOR_MAGENTA;
+            case ColorName16.Yellow:
+                return Curses.COLOR_YELLOW;
+            case ColorName16.Gray:
+                return Curses.COLOR_WHITE;
+            case ColorName16.DarkGray:
+                return Curses.COLOR_GRAY;
+            case ColorName16.BrightBlue:
+                return Curses.COLOR_BLUE | Curses.COLOR_GRAY;
+            case ColorName16.BrightGreen:
+                return Curses.COLOR_GREEN | Curses.COLOR_GRAY;
+            case ColorName16.BrightCyan:
+                return Curses.COLOR_CYAN | Curses.COLOR_GRAY;
+            case ColorName16.BrightRed:
+                return Curses.COLOR_RED | Curses.COLOR_GRAY;
+            case ColorName16.BrightMagenta:
+                return Curses.COLOR_MAGENTA | Curses.COLOR_GRAY;
+            case ColorName16.BrightYellow:
+                return Curses.COLOR_YELLOW | Curses.COLOR_GRAY;
+            case ColorName16.White:
+                return Curses.COLOR_WHITE | Curses.COLOR_GRAY;
+        }
+
+        throw new ArgumentException ("Invalid color code");
+    }
+
+    private static ColorName16 CursesColorNumberToColorName16 (short color)
+    {
+        switch (color)
+        {
+            case Curses.COLOR_BLACK:
+                return ColorName16.Black;
+            case Curses.COLOR_BLUE:
+                return ColorName16.Blue;
+            case Curses.COLOR_GREEN:
+                return ColorName16.Green;
+            case Curses.COLOR_CYAN:
+                return ColorName16.Cyan;
+            case Curses.COLOR_RED:
+                return ColorName16.Red;
+            case Curses.COLOR_MAGENTA:
+                return ColorName16.Magenta;
+            case Curses.COLOR_YELLOW:
+                return ColorName16.Yellow;
+            case Curses.COLOR_WHITE:
+                return ColorName16.Gray;
+            case Curses.COLOR_GRAY:
+                return ColorName16.DarkGray;
+            case Curses.COLOR_BLUE | Curses.COLOR_GRAY:
+                return ColorName16.BrightBlue;
+            case Curses.COLOR_GREEN | Curses.COLOR_GRAY:
+                return ColorName16.BrightGreen;
+            case Curses.COLOR_CYAN | Curses.COLOR_GRAY:
+                return ColorName16.BrightCyan;
+            case Curses.COLOR_RED | Curses.COLOR_GRAY:
+                return ColorName16.BrightRed;
+            case Curses.COLOR_MAGENTA | Curses.COLOR_GRAY:
+                return ColorName16.BrightMagenta;
+            case Curses.COLOR_YELLOW | Curses.COLOR_GRAY:
+                return ColorName16.BrightYellow;
+            case Curses.COLOR_WHITE | Curses.COLOR_GRAY:
+                return ColorName16.White;
+        }
+
+        throw new ArgumentException ("Invalid curses color code");
+    }
+
+    #endregion
+
+    private CursorVisibility? _currentCursorVisibility;
+    private CursorVisibility? _initialCursorVisibility;
+
+    /// <inheritdoc/>
+    public override bool EnsureCursorVisibility ()
+    {
+        if (!(Col >= 0 && Row >= 0 && Col < Cols && Row < Rows))
+        {
+            GetCursorVisibility (out CursorVisibility cursorVisibility);
+            _currentCursorVisibility = cursorVisibility;
+            SetCursorVisibility (CursorVisibility.Invisible);
+
+            return false;
+        }
+
+        SetCursorVisibility (_currentCursorVisibility ?? CursorVisibility.Default);
+
+        return _currentCursorVisibility == CursorVisibility.Default;
+    }
+
+    /// <inheritdoc/>
+    public override bool GetCursorVisibility (out CursorVisibility visibility)
+    {
+        visibility = CursorVisibility.Invisible;
+
+        if (!_currentCursorVisibility.HasValue)
+        {
+            return false;
+        }
+
+        visibility = _currentCursorVisibility.Value;
 
         return true;
     }
 
-    internal override void End ()
+    /// <inheritdoc/>
+    public override bool SetCursorVisibility (CursorVisibility visibility)
     {
-        StopReportingMouseMoves ();
-        SetCursorVisibility (CursorVisibility.Default);
+        if (_initialCursorVisibility.HasValue == false)
+        {
+            return false;
+        }
 
-        if (_mainLoopDriver is { })
+        if (!RunningUnitTests)
         {
-            _mainLoopDriver.RemoveWatch (_processInputToken);
+            Curses.curs_set (((int)visibility >> 16) & 0x000000FF);
         }
 
-        if (RunningUnitTests)
+        if (visibility != CursorVisibility.Invisible)
         {
-            return;
+            _mainLoopDriver?.WriteRaw (
+                                       EscSeqUtils.CSI_SetCursorStyle (
+                                                                       (EscSeqUtils.DECSCUSR_Style)
+                                                                       (((int)visibility >> 24)
+                                                                        & 0xFF)
+                                                                      )
+                                      );
         }
 
-        // throws away any typeahead that has been typed by
-        // the user and has not yet been read by the program.
-        Curses.flushinp ();
+        _currentCursorVisibility = visibility;
 
-        Curses.endwin ();
+        return true;
     }
 
-    internal override MainLoop Init ()
+    private bool SetCursorPosition (int col, int row)
     {
-        _mainLoopDriver = new UnixMainLoop (this);
+        // + 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;
+    }
+
+    #region Init/End/MainLoop
+
+    private Curses.Window? _window;
+    private UnixMainLoop? _mainLoopDriver;
+    private object _processInputToken;
+
+    public override MainLoop Init ()
+    {
+        _mainLoopDriver = new (this);
 
         if (!RunningUnitTests)
         {
@@ -531,19 +640,19 @@ internal class CursesDriver : ConsoleDriver
                 Curses.timeout (0);
             }
 
-            _processInputToken = _mainLoopDriver?.AddWatch (
-                                                            0,
-                                                            UnixMainLoop.Condition.PollIn,
-                                                            x =>
-                                                            {
-                                                                ProcessInput ();
+            _processInputToken = _mainLoopDriver.AddWatch (
+                                                           0,
+                                                           UnixMainLoop.Condition.PollIn,
+                                                           x =>
+                                                           {
+                                                               ProcessInput ();
 
-                                                                return true;
-                                                            }
-                                                           );
+                                                               return true;
+                                                           }
+                                                          );
         }
 
-        CurrentAttribute = new Attribute (ColorName16.White, ColorName16.Black);
+        CurrentAttribute = new (ColorName16.White, ColorName16.Black);
 
         if (Environment.OSVersion.Platform == PlatformID.Win32NT)
         {
@@ -575,13 +684,13 @@ internal class CursesDriver : ConsoleDriver
         {
             Curses.CheckWinChange ();
 
-            if (Force16Colors)
-            {
-                Curses.refresh ();
-            }
+            // On Init this call is needed no mater Force16Colors or not
+            Curses.refresh ();
+
+            EscSeqUtils.ContinuousButtonPressed += EscSeqUtils_ContinuousButtonPressed;
         }
 
-        return new MainLoop (_mainLoopDriver);
+        return new (_mainLoopDriver);
     }
 
     internal void ProcessInput ()
@@ -841,7 +950,6 @@ internal class CursesDriver : ConsoleDriver
             if (wch2 == 0 || wch2 == 27 || wch2 == Curses.KeyMouse)
             {
                 EscSeqUtils.DecodeEscSeq (
-                                          null,
                                           ref consoleKeyInfo,
                                           ref ck,
                                           cki,
@@ -854,14 +962,14 @@ internal class CursesDriver : ConsoleDriver
                                           out List<MouseFlags> mouseFlags,
                                           out Point pos,
                                           out _,
-                                          ProcessMouseEvent
+                                          EscSeqUtils.ProcessMouseEvent
                                          );
 
                 if (isKeyMouse)
                 {
                     foreach (MouseFlags mf in mouseFlags)
                     {
-                        ProcessMouseEvent (mf, pos);
+                        OnMouseEvent (new () { Flags = mf, Position = pos });
                     }
 
                     cki = null;
@@ -894,6 +1002,11 @@ internal class CursesDriver : ConsoleDriver
         }
     }
 
+    private void EscSeqUtils_ContinuousButtonPressed (object? sender, MouseEventArgs e)
+    {
+        OnMouseEvent (e);
+    }
+
     private static KeyCode MapCursesKey (int cursesKey)
     {
         switch (cursesKey)
@@ -971,181 +1084,54 @@ internal class CursesDriver : ConsoleDriver
         }
     }
 
-    private void ProcessMouseEvent (MouseFlags mouseFlag, Point pos)
+    public override void End ()
     {
-        bool WasButtonReleased (MouseFlags flag)
-        {
-            return flag.HasFlag (MouseFlags.Button1Released)
-                   || flag.HasFlag (MouseFlags.Button2Released)
-                   || flag.HasFlag (MouseFlags.Button3Released)
-                   || flag.HasFlag (MouseFlags.Button4Released);
-        }
+        EscSeqUtils.ContinuousButtonPressed -= EscSeqUtils_ContinuousButtonPressed;
 
-        bool IsButtonNotPressed (MouseFlags flag)
-        {
-            return !flag.HasFlag (MouseFlags.Button1Pressed)
-                   && !flag.HasFlag (MouseFlags.Button2Pressed)
-                   && !flag.HasFlag (MouseFlags.Button3Pressed)
-                   && !flag.HasFlag (MouseFlags.Button4Pressed);
-        }
+        StopReportingMouseMoves ();
+        SetCursorVisibility (CursorVisibility.Default);
 
-        bool IsButtonClickedOrDoubleClicked (MouseFlags flag)
+        if (_mainLoopDriver is { })
         {
-            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);
+            _mainLoopDriver.RemoveWatch (_processInputToken);
         }
 
-        Debug.WriteLine ($"CursesDriver: ({pos.X},{pos.Y}) - {mouseFlag}");
-
-
-        if ((WasButtonReleased (mouseFlag) && IsButtonNotPressed (_lastMouseFlags)) || (IsButtonClickedOrDoubleClicked (mouseFlag) && _lastMouseFlags == 0))
+        if (RunningUnitTests)
         {
             return;
         }
 
-        _lastMouseFlags = mouseFlag;
-
-        var me = new MouseEventArgs { Flags = mouseFlag, Position = pos };
-        //Debug.WriteLine ($"CursesDriver: ({me.Position}) - {me.Flags}");
-
-        OnMouseEvent (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)((ushort)foreground | (background << 4));
-        var v = (short)(((ushort)(foreground & 0xffff) << 16) | (background & 0xffff));
-
-        // TODO: for TrueColor - Use InitExtendedPair
-        Curses.InitColorPair (v, foreground, background);
+        // throws away any typeahead that has been typed by
+        // the user and has not yet been read by the program.
+        Curses.flushinp ();
 
-        return new Attribute (
-                              Curses.ColorPair (v),
-                              CursesColorNumberToColorName16 (foreground),
-                              CursesColorNumberToColorName16 (background)
-                             );
+        Curses.endwin ();
     }
 
-    /// <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 && Force16Colors)
-        {
-            return MakeColor (
-                              ColorNameToCursesColorNumber (foreground.GetClosestNamedColor16 ()),
-                              ColorNameToCursesColorNumber (background.GetClosestNamedColor16 ())
-                             );
-        }
-
-        return new Attribute (
-                              0,
-                              foreground,
-                              background
-                             );
-    }
+    #endregion Init/End/MainLoop
 
-    private static short ColorNameToCursesColorNumber (ColorName16 color)
+    public static bool Is_WSL_Platform ()
     {
-        switch (color)
-        {
-            case ColorName16.Black:
-                return Curses.COLOR_BLACK;
-            case ColorName16.Blue:
-                return Curses.COLOR_BLUE;
-            case ColorName16.Green:
-                return Curses.COLOR_GREEN;
-            case ColorName16.Cyan:
-                return Curses.COLOR_CYAN;
-            case ColorName16.Red:
-                return Curses.COLOR_RED;
-            case ColorName16.Magenta:
-                return Curses.COLOR_MAGENTA;
-            case ColorName16.Yellow:
-                return Curses.COLOR_YELLOW;
-            case ColorName16.Gray:
-                return Curses.COLOR_WHITE;
-            case ColorName16.DarkGray:
-                return Curses.COLOR_GRAY;
-            case ColorName16.BrightBlue:
-                return Curses.COLOR_BLUE | Curses.COLOR_GRAY;
-            case ColorName16.BrightGreen:
-                return Curses.COLOR_GREEN | Curses.COLOR_GRAY;
-            case ColorName16.BrightCyan:
-                return Curses.COLOR_CYAN | Curses.COLOR_GRAY;
-            case ColorName16.BrightRed:
-                return Curses.COLOR_RED | Curses.COLOR_GRAY;
-            case ColorName16.BrightMagenta:
-                return Curses.COLOR_MAGENTA | Curses.COLOR_GRAY;
-            case ColorName16.BrightYellow:
-                return Curses.COLOR_YELLOW | Curses.COLOR_GRAY;
-            case ColorName16.White:
-                return Curses.COLOR_WHITE | Curses.COLOR_GRAY;
-        }
-
-        throw new ArgumentException ("Invalid color code");
-    }
+        // 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);
 
-    private static ColorName16 CursesColorNumberToColorName16 (short color)
-    {
-        switch (color)
+        if (exitCode == 0 && result.Contains ("microsoft") && result.Contains ("WSL"))
         {
-            case Curses.COLOR_BLACK:
-                return ColorName16.Black;
-            case Curses.COLOR_BLUE:
-                return ColorName16.Blue;
-            case Curses.COLOR_GREEN:
-                return ColorName16.Green;
-            case Curses.COLOR_CYAN:
-                return ColorName16.Cyan;
-            case Curses.COLOR_RED:
-                return ColorName16.Red;
-            case Curses.COLOR_MAGENTA:
-                return ColorName16.Magenta;
-            case Curses.COLOR_YELLOW:
-                return ColorName16.Yellow;
-            case Curses.COLOR_WHITE:
-                return ColorName16.Gray;
-            case Curses.COLOR_GRAY:
-                return ColorName16.DarkGray;
-            case Curses.COLOR_BLUE | Curses.COLOR_GRAY:
-                return ColorName16.BrightBlue;
-            case Curses.COLOR_GREEN | Curses.COLOR_GRAY:
-                return ColorName16.BrightGreen;
-            case Curses.COLOR_CYAN | Curses.COLOR_GRAY:
-                return ColorName16.BrightCyan;
-            case Curses.COLOR_RED | Curses.COLOR_GRAY:
-                return ColorName16.BrightRed;
-            case Curses.COLOR_MAGENTA | Curses.COLOR_GRAY:
-                return ColorName16.BrightMagenta;
-            case Curses.COLOR_YELLOW | Curses.COLOR_GRAY:
-                return ColorName16.BrightYellow;
-            case Curses.COLOR_WHITE | Curses.COLOR_GRAY:
-                return ColorName16.White;
+            return true;
         }
 
-        throw new ArgumentException ("Invalid curses color code");
+        return false;
     }
 
-    #endregion
+    /// <inheritdoc/>
+    public override void WriteRaw (string ansi) { _mainLoopDriver?.WriteRaw (ansi); }
 }
 
+// TODO: One type per file - move to another file
 internal static class Platform
 {
     private static int _suspendSignal;
@@ -1224,4 +1210,4 @@ internal static class Platform
 
     [DllImport ("libc")]
     private static extern int uname (nint buf);
-}
+}

+ 46 - 27
Terminal.Gui/ConsoleDrivers/CursesDriver/UnixMainLoop.cs

@@ -1,3 +1,4 @@
+#nullable enable
 //
 // mainloop.cs: Linux/Curses MainLoop implementation.
 //
@@ -36,21 +37,22 @@ internal class UnixMainLoop : IMainLoopDriver
         PollNval = 32
     }
 
-    public const int KEY_RESIZE = unchecked ((int)0xffffffffffffffff);
+    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 MainLoop? _mainLoop;
     private bool _pollDirty = true;
-    private Pollfd [] _pollMap;
+    private Pollfd []? _pollMap;
     private bool _winChanged;
 
-    public UnixMainLoop (ConsoleDriver consoleDriver = null)
+    public UnixMainLoop (IConsoleDriver IConsoleDriver)
     {
-        // UnixDriver doesn't use the consoleDriver parameter, but the WindowsDriver does.
-        _cursesDriver = (CursesDriver)Application.Driver;
+        ArgumentNullException.ThrowIfNull (IConsoleDriver);
+
+        _cursesDriver = (CursesDriver)IConsoleDriver;
     }
 
     void IMainLoopDriver.Wakeup ()
@@ -77,7 +79,7 @@ internal class UnixMainLoop : IMainLoopDriver
             AddWatch (
                       _wakeUpPipes [0],
                       Condition.PollIn,
-                      ml =>
+                      _ =>
                       {
                           read (_wakeUpPipes [0], _ignore, 1);
 
@@ -93,11 +95,16 @@ internal class UnixMainLoop : IMainLoopDriver
 
     bool IMainLoopDriver.EventsPending ()
     {
+        if (ConsoleDriver.RunningUnitTests)
+        {
+            return true;
+        }
+
         UpdatePollMap ();
 
-        bool checkTimersResult = _mainLoop.CheckTimersAndIdleHandlers (out int pollTimeout);
+        bool checkTimersResult = _mainLoop!.CheckTimersAndIdleHandlers (out int pollTimeout);
 
-        int n = poll (_pollMap, (uint)_pollMap.Length, pollTimeout);
+        int n = poll (_pollMap!, (uint)_pollMap!.Length, pollTimeout);
 
         if (n == KEY_RESIZE)
         {
@@ -109,6 +116,11 @@ internal class UnixMainLoop : IMainLoopDriver
 
     void IMainLoopDriver.Iteration ()
     {
+        if (ConsoleDriver.RunningUnitTests)
+        {
+            return;
+        }
+
         if (_winChanged)
         {
             _winChanged = false;
@@ -125,19 +137,17 @@ internal class UnixMainLoop : IMainLoopDriver
 
         foreach (Pollfd p in _pollMap)
         {
-            Watch watch;
-
             if (p.revents == 0)
             {
                 continue;
             }
 
-            if (!_descriptorWatchers.TryGetValue (p.fd, out watch))
+            if (!_descriptorWatchers.TryGetValue (p.fd, out Watch? watch))
             {
                 continue;
             }
 
-            if (!watch.Callback (_mainLoop))
+            if (!watch.Callback (_mainLoop!))
             {
                 _descriptorWatchers.Remove (p.fd);
             }
@@ -146,7 +156,7 @@ internal class UnixMainLoop : IMainLoopDriver
 
     void IMainLoopDriver.TearDown ()
     {
-        _descriptorWatchers?.Clear ();
+        _descriptorWatchers.Clear ();
 
         _mainLoop = null;
     }
@@ -159,10 +169,7 @@ internal class UnixMainLoop : IMainLoopDriver
     /// </remarks>
     internal object AddWatch (int fileDescriptor, Condition condition, Func<MainLoop, bool> callback)
     {
-        if (callback is null)
-        {
-            throw new ArgumentNullException (nameof (callback));
-        }
+        ArgumentNullException.ThrowIfNull (callback);
 
         var watch = new Watch { Condition = condition, Callback = callback, File = fileDescriptor };
         _descriptorWatchers [fileDescriptor] = watch;
@@ -186,15 +193,6 @@ internal class UnixMainLoop : IMainLoopDriver
         }
     }
 
-    [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)
@@ -215,9 +213,30 @@ internal class UnixMainLoop : IMainLoopDriver
         }
     }
 
+    internal void WriteRaw (string ansiRequest)
+    {
+        // Write to stdout (fd 1)
+        write (STDOUT_FILENO, ansiRequest, ansiRequest.Length);
+    }
+
+    [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);
+
     [DllImport ("libc")]
     private static extern int write (int fd, nint buf, nint n);
 
+    // File descriptor for stdout
+    private const int STDOUT_FILENO = 1;
+
+    [DllImport ("libc")]
+    private static extern int write (int fd, string buf, int n);
+
     [StructLayout (LayoutKind.Sequential)]
     private struct Pollfd
     {

+ 0 - 125
Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqReq.cs

@@ -1,125 +0,0 @@
-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 Terminator (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 { 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);
-                }
-            }
-        }
-    }
-}

+ 28 - 0
Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqReqStatus.cs

@@ -0,0 +1,28 @@
+#nullable enable
+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; internal set; }
+
+    /// <summary>Gets the number of requests.</summary>
+    public int NumRequests { get; internal set; }
+
+    /// <summary>Gets the Escape Sequence Terminator (e.g. ESC[8t ... t is the terminator).</summary>
+    public string Terminator { get; }
+}

+ 98 - 0
Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqRequests.cs

@@ -0,0 +1,98 @@
+#nullable enable
+
+namespace Terminal.Gui;
+
+/// <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 static class EscSeqRequests
+{
+    /// <summary>Gets the <see cref="EscSeqReqStatus"/> list.</summary>
+    public static List<EscSeqReqStatus> Statuses { get; } = [];
+
+    /// <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="numRequests">The number of requests.</param>
+    public static void Add (string terminator, int numRequests = 1)
+    {
+        ArgumentException.ThrowIfNullOrEmpty (terminator);
+
+        int numReq = Math.Max (numRequests, 1);
+
+        lock (Statuses)
+        {
+            EscSeqReqStatus? found = Statuses.Find (x => x.Terminator == terminator);
+
+            if (found is null)
+            {
+                Statuses.Add (new (terminator, numReq));
+            }
+            else
+            {
+                found.NumRequests += numReq;
+                found.NumOutstanding += numReq;
+            }
+        }
+    }
+
+    /// <summary>
+    ///     Clear the <see cref="Statuses"/> property.
+    /// </summary>
+    public static void Clear ()
+    {
+        lock (Statuses)
+        {
+            Statuses.Clear ();
+        }
+    }
+
+    /// <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 static bool HasResponse (string terminator)
+    {
+        lock (Statuses)
+        {
+            EscSeqReqStatus? found = Statuses.Find (x => x.Terminator == terminator);
+
+            return found is { };
+        }
+    }
+
+    /// <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 static void Remove (string terminator)
+    {
+        ArgumentException.ThrowIfNullOrEmpty (terminator);
+
+        lock (Statuses)
+        {
+            EscSeqReqStatus? found = Statuses.Find (x => x.Terminator == terminator);
+
+            if (found is null)
+            {
+                return;
+            }
+
+            found.NumOutstanding--;
+
+            if (found.NumOutstanding == 0)
+            {
+                Statuses.Remove (found);
+            }
+        }
+    }
+}

File diff suppressed because it is too large
+ 613 - 132
Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqUtils.cs


+ 5 - 2
Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs

@@ -76,7 +76,7 @@ public class FakeDriver : ConsoleDriver
         }
     }
 
-    internal override void End ()
+    public override void End ()
     {
         FakeConsole.ResetColor ();
         FakeConsole.Clear ();
@@ -84,7 +84,7 @@ public class FakeDriver : ConsoleDriver
 
     private FakeMainLoop _mainLoopDriver;
 
-    internal override MainLoop Init ()
+    public override MainLoop Init ()
     {
         FakeConsole.MockKeyPresses.Clear ();
 
@@ -392,6 +392,9 @@ public class FakeDriver : ConsoleDriver
         MockKeyPressedHandler (new ConsoleKeyInfo (keyChar, key, shift, alt, control));
     }
 
+    /// <inheritdoc />
+    public override void WriteRaw (string ansi) { throw new NotImplementedException (); }
+
     public void SetBufferSize (int width, int height)
     {
         FakeConsole.SetBufferSize (width, height);

+ 1 - 1
Terminal.Gui/ConsoleDrivers/FakeDriver/FakeMainLoop.cs

@@ -4,7 +4,7 @@ internal class FakeMainLoop : IMainLoopDriver
 {
     public Action<ConsoleKeyInfo> MockKeyPressed;
 
-    public FakeMainLoop (ConsoleDriver consoleDriver = null)
+    public FakeMainLoop (IConsoleDriver consoleDriver = null)
     {
         // No implementation needed for FakeMainLoop
     }

+ 295 - 0
Terminal.Gui/ConsoleDrivers/IConsoleDriver.cs

@@ -0,0 +1,295 @@
+#nullable enable
+namespace Terminal.Gui;
+
+/// <summary>Base interface 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.
+/// </remarks>
+public interface IConsoleDriver
+{
+    /// <summary>Get the operating system clipboard.</summary>
+    IClipboard? Clipboard { get; }
+
+    /// <summary>Gets the location and size of the terminal screen.</summary>
+    Rectangle Screen { get; }
+
+    /// <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 of <see cref="Clip"/> region.</value>
+    Region? Clip { get; 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>
+    int Col { get; }
+
+    /// <summary>The number of columns visible in the terminal.</summary>
+    int Cols { get; 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>
+    Cell [,]? Contents { get; set; }
+
+    /// <summary>The leftmost column in the terminal.</summary>
+    int Left { get; 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>
+    int Row { get; }
+
+    /// <summary>The number of rows visible in the terminal.</summary>
+    int Rows { get; set; }
+
+    /// <summary>The topmost row in the terminal.</summary>
+    int Top { get; set; }
+
+    /// <summary>Gets whether the <see cref="ConsoleDriver"/> supports TrueColor output.</summary>
+    bool SupportsTrueColor { get; }
+
+    /// <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>
+    bool Force16Colors { get; set; }
+
+    /// <summary>
+    ///     The <see cref="Attribute"/> that will be used for the next <see cref="AddRune(Rune)"/> or <see cref="AddStr"/>
+    ///     call.
+    /// </summary>
+    Attribute CurrentAttribute { get; set; }
+
+    /// <summary>Returns the name of the driver and relevant library version information.</summary>
+    /// <returns></returns>
+    string GetVersionInfo ();
+
+    /// <summary>
+    ///     Provide proper writing to send escape sequence recognized by the <see cref="ConsoleDriver"/>.
+    /// </summary>
+    /// <param name="ansi"></param>
+    void WriteRaw (string ansi);
+
+    /// <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>
+    bool IsRuneSupported (Rune rune);
+
+    /// <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 the screen bounds or outside of <see cref="ConsoleDriver.Clip"/>.
+    ///     <see langword="true"/> otherwise.
+    /// </returns>
+    bool IsValidLocation (int col, int row);
+
+    /// <summary>Tests whether the specified coordinate are valid for drawing the specified Rune.</summary>
+    /// <param name="rune">Used to determine if one or two columns are required.</param>
+    /// <param name="col">The column.</param>
+    /// <param name="row">The row.</param>
+    /// <returns>
+    ///     <see langword="false"/> if the coordinate is outside the screen bounds or outside of <see cref="ConsoleDriver.Clip"/>.
+    ///     <see langword="true"/> otherwise.
+    /// </returns>
+    bool IsValidLocation (Rune rune, int col, int row);
+
+    /// <summary>
+    ///     Updates <see cref="ConsoleDriver.Col"/> and <see cref="ConsoleDriver.Row"/> to the specified column and row in <see cref="ConsoleDriver.Contents"/>.
+    ///     Used by <see cref="ConsoleDriver.AddRune(System.Text.Rune)"/> and <see cref="ConsoleDriver.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="ConsoleDriver.Cols"/> and
+    ///         <see cref="ConsoleDriver.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>
+    void Move (int col, int row);
+
+    /// <summary>Adds the specified rune to the display at the current cursor position.</summary>
+    /// <remarks>
+    ///     <para>
+    ///         When the method returns, <see cref="ConsoleDriver.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="ConsoleDriver.Clip"/> or screen
+    ///         dimensions defined by <see cref="ConsoleDriver.Cols"/>.
+    ///     </para>
+    ///     <para>
+    ///         If <paramref name="rune"/> requires more than one column, and <see cref="ConsoleDriver.Col"/> plus the number of columns
+    ///         needed exceeds the <see cref="ConsoleDriver.Clip"/> or screen dimensions, the default Unicode replacement character (U+FFFD)
+    ///         will be added instead.
+    ///     </para>
+    /// </remarks>
+    /// <param name="rune">Rune to add.</param>
+    void AddRune (Rune rune);
+
+    /// <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="ConsoleDriver.AddRune(System.Text.Rune)"/> with the <see cref="Rune"/> constructor.
+    /// </summary>
+    /// <param name="c">Character to add.</param>
+    void AddRune (char c);
+
+    /// <summary>Adds the <paramref name="str"/> to the display at the cursor position.</summary>
+    /// <remarks>
+    ///     <para>
+    ///         When the method returns, <see cref="ConsoleDriver.Col"/> will be incremented by the number of columns
+    ///         <paramref name="str"/> required, unless the new column value is outside of the <see cref="ConsoleDriver.Clip"/> or screen
+    ///         dimensions defined by <see cref="ConsoleDriver.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>
+    void AddStr (string str);
+
+    /// <summary>Fills the specified rectangle with the specified rune, using <see cref="ConsoleDriver.CurrentAttribute"/></summary>
+    /// <remarks>
+    ///     The value of <see cref="ConsoleDriver.Clip"/> is honored. Any parts of the rectangle not in the clip will not be drawn.
+    /// </remarks>
+    /// <param name="rect">The Screen-relative rectangle.</param>
+    /// <param name="rune">The Rune used to fill the rectangle</param>
+    void FillRect (Rectangle rect, Rune rune = default);
+
+    /// <summary>
+    ///     Fills the specified rectangle with the specified <see langword="char"/>. This method is a convenience method
+    ///     that calls <see cref="ConsoleDriver.FillRect(System.Drawing.Rectangle,System.Text.Rune)"/>.
+    /// </summary>
+    /// <param name="rect"></param>
+    /// <param name="c"></param>
+    void FillRect (Rectangle rect, char c);
+
+    /// <summary>Clears the <see cref="ConsoleDriver.Contents"/> of the driver.</summary>
+    void ClearContents ();
+
+    /// <summary>
+    ///     Raised each time <see cref="ConsoleDriver.ClearContents"/> is called. For benchmarking.
+    /// </summary>
+    event EventHandler<EventArgs>? ClearedContents;
+
+    /// <summary>
+    /// Sets <see cref="ConsoleDriver.Contents"/> as dirty for situations where views
+    /// don't need layout and redrawing, but just refresh the screen.
+    /// </summary>
+    void SetContentsAsDirty ();
+
+    /// <summary>Determines if the terminal cursor should be visible or not and sets it accordingly.</summary>
+    /// <returns><see langword="true"/> upon success</returns>
+    bool EnsureCursorVisibility ();
+
+    /// <summary>Gets the terminal cursor visibility.</summary>
+    /// <param name="visibility">The current <see cref="CursorVisibility"/></param>
+    /// <returns><see langword="true"/> upon success</returns>
+    bool GetCursorVisibility (out CursorVisibility visibility);
+
+    /// <summary>Called when the terminal size changes. Fires the <see cref="ConsoleDriver.SizeChanged"/> event.</summary>
+    /// <param name="args"></param>
+    void OnSizeChanged (SizeChangedEventArgs args);
+
+    /// <summary>Updates the screen to reflect all the changes that have been done to the display buffer</summary>
+    void Refresh ();
+
+    /// <summary>
+    ///     Raised each time <see cref="ConsoleDriver.Refresh"/> is called. For benchmarking.
+    /// </summary>
+    event EventHandler<EventArgs<bool>>? Refreshed;
+
+    /// <summary>Sets the terminal cursor visibility.</summary>
+    /// <param name="visibility">The wished <see cref="CursorVisibility"/></param>
+    /// <returns><see langword="true"/> upon success</returns>
+    bool SetCursorVisibility (CursorVisibility visibility);
+
+    /// <summary>The event fired when the terminal is resized.</summary>
+    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>
+    void Suspend ();
+
+    /// <summary>Sets the position of the terminal cursor to <see cref="ConsoleDriver.Col"/> and <see cref="ConsoleDriver.Row"/>.</summary>
+    void UpdateCursor ();
+
+    /// <summary>Redraws the physical screen with the contents that have been queued up via any of the printing commands.</summary>
+    /// <returns><see langword="true"/> if any updates to the screen were made.</returns>
+    bool UpdateScreen ();
+
+    /// <summary>Initializes the driver</summary>
+    /// <returns>Returns an instance of <see cref="MainLoop"/> using the <see cref="IMainLoopDriver"/> for the driver.</returns>
+    MainLoop Init ();
+
+    /// <summary>Ends the execution of the console driver.</summary>
+    void End ();
+
+    /// <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>
+    Attribute SetAttribute (Attribute c);
+
+    /// <summary>Gets the current <see cref="Attribute"/>.</summary>
+    /// <returns>The current attribute.</returns>
+    Attribute GetAttribute ();
+
+    /// <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>
+    Attribute MakeColor (in Color foreground, in Color background);
+
+    /// <summary>Event fired when a mouse event occurs.</summary>
+    event EventHandler<MouseEventArgs>? MouseEvent;
+
+    /// <summary>Called when a mouse event occurs. Fires the <see cref="ConsoleDriver.MouseEvent"/> event.</summary>
+    /// <param name="a"></param>
+    void OnMouseEvent (MouseEventArgs a);
+
+    /// <summary>Event fired when a key is pressed down. This is a precursor to <see cref="ConsoleDriver.KeyUp"/>.</summary>
+    event EventHandler<Key>? KeyDown;
+
+    /// <summary>
+    ///     Called when a key is pressed down. Fires the <see cref="ConsoleDriver.KeyDown"/> event. This is a precursor to
+    ///     <see cref="ConsoleDriver.OnKeyUp"/>.
+    /// </summary>
+    /// <param name="a"></param>
+    void OnKeyDown (Key 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="ConsoleDriver.KeyDown"/> processing is
+    ///     complete.
+    /// </remarks>
+    event EventHandler<Key>? KeyUp;
+
+    /// <summary>Called when a key is released. Fires the <see cref="ConsoleDriver.KeyUp"/> event.</summary>
+    /// <remarks>
+    ///     Drivers that do not support key release events will call this method after <see cref="ConsoleDriver.OnKeyDown"/> processing
+    ///     is complete.
+    /// </remarks>
+    /// <param name="a"></param>
+    void OnKeyUp (Key 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>
+    void SendKeys (char keyChar, ConsoleKey key, bool shift, bool alt, bool ctrl);
+}

+ 321 - 0
Terminal.Gui/ConsoleDrivers/KeyCode.cs

@@ -0,0 +1,321 @@
+#nullable enable
+namespace Terminal.Gui;
+
+/// <summary>
+///     The <see cref="KeyCode"/> enumeration encodes key information from <see cref="IConsoleDriver"/>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>
+/// </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 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
+}

+ 0 - 1827
Terminal.Gui/ConsoleDrivers/NetDriver.cs

@@ -1,1827 +0,0 @@
-//
-// NetDriver.cs: The System.Console-based .NET driver, works on Windows and Unix, but is not particularly efficient.
-//
-
-using System.Diagnostics;
-using System.Diagnostics.CodeAnalysis;
-using System.Runtime.InteropServices;
-using static Terminal.Gui.ConsoleDrivers.ConsoleKeyMapping;
-using static Terminal.Gui.NetEvents;
-
-namespace Terminal.Gui;
-
-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);
-}
-
-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;
-#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 ();
-            }
-
-#if PROCESS_REQUEST
-            _neededProcessRequest = false;
-#endif
-            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, cancellationToken).Wait (cancellationToken);
-
-            if (Console.KeyAvailable)
-            {
-                return Console.ReadKey (intercept);
-            }
-        }
-
-        cancellationToken.ThrowIfCancellationRequested ();
-
-        return default (ConsoleKeyInfo);
-    }
-
-    private void ProcessInputQueue ()
-    {
-        while (_inputReadyCancellationTokenSource is { IsCancellationRequested: false })
-        {
-            try
-            {
-                _waitForStart.Wait (_inputReadyCancellationTokenSource.Token);
-            }
-            catch (OperationCanceledException)
-            {
-                return;
-            }
-
-            _waitForStart.Reset ();
-
-            if (_inputQueue.Count == 0)
-            {
-                ConsoleKey key = 0;
-                ConsoleModifiers mod = 0;
-                ConsoleKeyInfo newConsoleKeyInfo = default;
-
-                while (_inputReadyCancellationTokenSource is { IsCancellationRequested: false })
-                {
-                    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).Wait (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 (_inputReadyCancellationTokenSource is { IsCancellationRequested: false })
-        {
-            try
-            {
-                _winChange.Wait (_inputReadyCancellationTokenSource.Token);
-                _winChange.Reset ();
-
-                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);
-    }
-
-    [UnconditionalSuppressMessage ("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "<Pending>")]
-    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
-        }
-    }
-}
-
-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 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)
-        { }
-    }
-
-    public override void Suspend ()
-    {
-        if (Environment.OSVersion.Platform != PlatformID.Unix)
-        {
-            return;
-        }
-
-        StopReportingMouseMoves ();
-
-        if (!RunningUnitTests)
-        {
-            Console.ResetColor ();
-            Console.Clear ();
-
-            //Disable alternative screen buffer.
-            Console.Out.Write (EscSeqUtils.CSI_RestoreCursorAndRestoreAltBufferWithBackscroll);
-
-            //Set cursor key to cursor.
-            Console.Out.Write (EscSeqUtils.CSI_ShowCursor);
-
-            Platform.Suspend ();
-
-            //Enable alternative screen buffer.
-            Console.Out.Write (EscSeqUtils.CSI_SaveCursorAndActivateAltBufferNoBackscroll);
-
-            SetContentsAsDirty ();
-            Refresh ();
-        }
-
-        StartReportingMouseMoves ();
-    }
-
-    public override bool UpdateScreen ()
-    {
-        bool updated = false;
-        if (RunningUnitTests
-            || _winSizeChanging
-            || Console.WindowHeight < 1
-            || Contents.Length != Rows * Cols
-            || Rows != Console.WindowHeight)
-        {
-            return updated;
-        }
-
-        var top = 0;
-        var left = 0;
-        int rows = Rows;
-        int cols = Cols;
-        var output = new StringBuilder ();
-        Attribute? redrawAttr = null;
-        int lastCol = -1;
-
-        CursorVisibility? savedVisibility = _cachedCursorVisibility;
-        SetCursorVisibility (CursorVisibility.Invisible);
-
-        for (int row = top; row < rows; row++)
-        {
-            if (Console.WindowHeight < 1)
-            {
-                return updated;
-            }
-
-            if (!_dirtyLines [row])
-            {
-                continue;
-            }
-
-            if (!SetCursorPosition (0, row))
-            {
-                return updated;
-            }
-
-            updated = true;
-            _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.GetClosestNamedColor16 (),
-                                                                                            false
-                                                                                           ),
-                                                                                 MapColors ((ConsoleColor)attr.Foreground.GetClosestNamedColor16 ())
-                                                                                )
-                                          );
-                        }
-                        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);
-            }
-
-            foreach (var s in Application.Sixel)
-            {
-                if (!string.IsNullOrWhiteSpace (s.SixelData))
-                {
-                    SetCursorPosition (s.ScreenPosition.X, s.ScreenPosition.Y);
-                    Console.Write (s.SixelData);
-                }
-            }
-        }
-
-        SetCursorPosition (0, 0);
-
-        _cachedCursorVisibility = savedVisibility;
-
-        void WriteToConsole (StringBuilder output, ref int lastCol, int row, ref int outputWidth)
-        {
-            SetCursorPosition (lastCol, row);
-            Console.Write (output);
-            output.Clear ();
-            lastCol += outputWidth;
-            outputWidth = 0;
-        }
-
-        return updated;
-    }
-
-    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:
-                MouseEventArgs me = ToDriverMouse (inputEvent.MouseEvent);
-                //Debug.WriteLine ($"NetDriver: ({me.X},{me.Y}) - {me.Flags}");
-                OnMouseEvent (me);
-
-                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);
-                }
-#pragma warning restore CA1416
-            }
-            // INTENT: Why are these eating the exceptions?
-            // Comments would be good here.
-            catch (IOException)
-            {
-                // CONCURRENCY: Unsynchronized access to Clip is not safe.
-                Clip = new (Screen);
-            }
-            catch (ArgumentOutOfRangeException)
-            {
-                // CONCURRENCY: Unsynchronized access to Clip is not safe.
-                Clip = new (Screen);
-            }
-        }
-        else
-        {
-            Console.Out.Write (EscSeqUtils.CSI_SetTerminalWindowSize (Rows, Cols));
-        }
-
-        // CONCURRENCY: Unsynchronized access to Clip is not safe.
-        Clip = new (Screen);
-    }
-
-    #endregion
-
-    #region Color Handling
-
-    // Cache the list of ConsoleColor values.
-    [UnconditionalSuppressMessage ("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "<Pending>")]
-    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;
-
-        Console.Out.Write (visibility == CursorVisibility.Default ? EscSeqUtils.CSI_ShowCursor : EscSeqUtils.CSI_HideCursor);
-
-        return visibility == CursorVisibility.Default;
-    }
-
-    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 MouseEventArgs 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 MouseEventArgs { Position = me.Position, 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);
-        }
-
-        // 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))
-        {
-            if (keyInfo.Modifiers.HasFlag(ConsoleModifiers.Control) && keyInfo.Key == ConsoleKey.I)
-            {
-                return KeyCode.Tab;
-            }
-
-            return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)((uint)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));
-        }
-
-        if (((ConsoleKey)keyInfo.KeyChar) is >= ConsoleKey.A and <= ConsoleKey.Z)
-        {
-            // Shifted
-            keyInfo = new ConsoleKeyInfo (
-                                          keyInfo.KeyChar,
-                                          (ConsoleKey)keyInfo.KeyChar,
-                                          true,
-                                          keyInfo.Modifiers.HasFlag (ConsoleModifiers.Alt),
-                                          keyInfo.Modifiers.HasFlag (ConsoleModifiers.Control));
-        }
-
-        if ((ConsoleKey)keyInfo.KeyChar - 32 is >= ConsoleKey.A and <= ConsoleKey.Z)
-        {
-            // Unshifted
-            keyInfo = new ConsoleKeyInfo (
-                                          keyInfo.KeyChar,
-                                          (ConsoleKey)(keyInfo.KeyChar - 32),
-                                          false,
-                                          keyInfo.Modifiers.HasFlag (ConsoleModifiers.Alt),
-                                          keyInfo.Modifiers.HasFlag (ConsoleModifiers.Control));
-        }
-
-        if (keyInfo.Key is >= ConsoleKey.A and <= ConsoleKey.Z )
-        {
-            if (keyInfo.Modifiers.HasFlag (ConsoleModifiers.Alt)
-                || keyInfo.Modifiers.HasFlag (ConsoleModifiers.Control))
-            {
-                // NetDriver doesn't support Shift-Ctrl/Shift-Alt combos
-                return MapToKeyCodeModifiers (keyInfo.Modifiers & ~ConsoleModifiers.Shift, (KeyCode)keyInfo.Key);
-            }
-
-            if (keyInfo.Modifiers == ConsoleModifiers.Shift)
-            {
-                // If ShiftMask is on  add the ShiftMask
-                if (char.IsUpper (keyInfo.KeyChar))
-                {
-                    return (KeyCode)keyInfo.Key | KeyCode.ShiftMask;
-                }
-            }
-
-            return (KeyCode)keyInfo.Key;
-        }
-
-
-        return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)((uint)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.
-/// </summary>
-/// <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 readonly 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 ();
-        }
-
-        _eventReadyTokenSource.Token.ThrowIfCancellationRequested ();
-
-        if (!_eventReadyTokenSource.IsCancellationRequested)
-        {
-            return _resultQueue.Count > 0 || _mainLoop.CheckTimersAndIdleHandlers (out _);
-        }
-
-        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;
-            }
-
-            _inputHandlerTokenSource.Token.ThrowIfCancellationRequested ();
-
-            if (_resultQueue.Count == 0)
-            {
-                _resultQueue.Enqueue (_netEvents.DequeueInput ());
-            }
-
-            while (_resultQueue.Count > 0 && _resultQueue.Peek () is null)
-            {
-                _resultQueue.Dequeue ();
-            }
-
-            if (_resultQueue.Count > 0)
-            {
-                _eventReady.Set ();
-            }
-        }
-    }
-}

+ 790 - 0
Terminal.Gui/ConsoleDrivers/NetDriver/NetDriver.cs

@@ -0,0 +1,790 @@
+#nullable enable
+//
+// NetDriver.cs: The System.Console-based .NET driver, works on Windows and Unix, but is not particularly efficient.
+//
+
+using System.Diagnostics;
+using System.Runtime.InteropServices;
+using static Terminal.Gui.NetEvents;
+
+namespace Terminal.Gui;
+
+internal class NetDriver : ConsoleDriver
+{
+    public bool IsWinPlatform { get; private set; }
+    public NetWinVTConsole? NetWinConsole { get; private set; }
+
+    public override void Suspend ()
+    {
+        if (Environment.OSVersion.Platform != PlatformID.Unix)
+        {
+            return;
+        }
+
+        StopReportingMouseMoves ();
+
+        if (!RunningUnitTests)
+        {
+            Console.ResetColor ();
+            Console.Clear ();
+
+            //Disable alternative screen buffer.
+            Console.Out.Write (EscSeqUtils.CSI_RestoreCursorAndRestoreAltBufferWithBackscroll);
+
+            //Set cursor key to cursor.
+            Console.Out.Write (EscSeqUtils.CSI_ShowCursor);
+
+            Platform.Suspend ();
+
+            //Enable alternative screen buffer.
+            Console.Out.Write (EscSeqUtils.CSI_SaveCursorAndActivateAltBufferNoBackscroll);
+
+            SetContentsAsDirty ();
+            Refresh ();
+        }
+
+        StartReportingMouseMoves ();
+    }
+
+    public override bool UpdateScreen ()
+    {
+        bool updated = false;
+        if (RunningUnitTests
+            || _winSizeChanging
+            || Console.WindowHeight < 1
+            || Contents?.Length != Rows * Cols
+            || Rows != Console.WindowHeight)
+        {
+            return updated;
+        }
+
+        var top = 0;
+        var left = 0;
+        int rows = Rows;
+        int cols = Cols;
+        var output = new StringBuilder ();
+        Attribute? redrawAttr = null;
+        int lastCol = -1;
+
+        CursorVisibility? savedVisibility = _cachedCursorVisibility;
+        SetCursorVisibility (CursorVisibility.Invisible);
+
+        for (int row = top; row < rows; row++)
+        {
+            if (Console.WindowHeight < 1)
+            {
+                return updated;
+            }
+
+            if (!_dirtyLines! [row])
+            {
+                continue;
+            }
+
+            if (!SetCursorPosition (0, row))
+            {
+                return updated;
+            }
+
+            updated = true;
+            _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
+                                                                                                             .GetClosestNamedColor16 (),
+                                                                                                         false
+                                                                                                        ),
+                                                                                                    MapColors (
+                                                                                                     (ConsoleColor)attr.Foreground
+                                                                                                         .GetClosestNamedColor16 ())
+                                                                                                   )
+                                          );
+                        }
+                        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);
+            }
+
+            foreach (var s in Application.Sixel)
+            {
+                if (!string.IsNullOrWhiteSpace (s.SixelData))
+                {
+                    SetCursorPosition (s.ScreenPosition.X, s.ScreenPosition.Y);
+                    Console.Write (s.SixelData);
+                }
+            }
+        }
+
+        SetCursorPosition (0, 0);
+
+        _cachedCursorVisibility = savedVisibility;
+
+        void WriteToConsole (StringBuilder output, ref int lastCol, int row, ref int outputWidth)
+        {
+            SetCursorPosition (lastCol, row);
+            Console.Write (output);
+            output.Clear ();
+            lastCol += outputWidth;
+            outputWidth = 0;
+        }
+
+        return updated;
+    }
+
+    #region Init/End/MainLoop
+
+    internal NetMainLoop? _mainLoopDriver;
+
+    public override MainLoop Init ()
+    {
+        PlatformID p = Environment.OSVersion.Platform;
+
+        if (p == PlatformID.Win32NT || p == PlatformID.Win32S || p == PlatformID.Win32Windows)
+        {
+            IsWinPlatform = true;
+
+            try
+            {
+                NetWinConsole = new ();
+            }
+            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 (Color.White, Color.Black);
+
+        StartReportingMouseMoves ();
+
+        _mainLoopDriver = new (this);
+        _mainLoopDriver.ProcessInput = ProcessInput;
+
+        return new (_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 = EscSeqUtils.MapKey (consoleKeyInfo);
+
+                if (map == KeyCode.Null)
+                {
+                    break;
+                }
+
+                OnKeyDown (new (map));
+                OnKeyUp (new (map));
+
+                break;
+            case EventType.Mouse:
+                MouseEventArgs me = ToDriverMouse (inputEvent.MouseEvent);
+
+                //Debug.WriteLine ($"NetDriver: ({me.X},{me.Y}) - {me.Flags}");
+                OnMouseEvent (me);
+
+                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 (new (Cols, Rows)));
+
+                break;
+            case EventType.RequestResponse:
+                break;
+            case EventType.WindowPosition:
+                break;
+            default:
+                throw new ArgumentOutOfRangeException ();
+        }
+    }
+
+    public 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 ();
+        }
+    }
+
+    #endregion Init/End/MainLoop
+
+    #region Color Handling
+
+    public override bool SupportsTrueColor => Environment.OSVersion.Platform == PlatformID.Unix
+                                              || (IsWinPlatform && Environment.OSVersion.Version.Build >= 14931);
+
+    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;
+
+    //// Cache the list of ConsoleColor values.
+    //[UnconditionalSuppressMessage (
+    //                                  "AOT",
+    //                                  "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.",
+    //                                  Justification = "<Pending>")]
+    //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;
+    }
+
+    #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;
+
+        Console.Out.Write (visibility == CursorVisibility.Default ? EscSeqUtils.CSI_ShowCursor : EscSeqUtils.CSI_HideCursor);
+
+        return visibility == CursorVisibility.Default;
+    }
+
+    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 MouseEventArgs ToDriverMouse (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() { Position = me.Position, Flags = mouseFlag };
+    }
+
+    #endregion Mouse Handling
+
+    #region Keyboard Handling
+
+    public override void SendKeys (char keyChar, ConsoleKey key, bool shift, bool alt, bool control)
+    {
+        var input = new InputResult
+        {
+            EventType = EventType.Key, ConsoleKeyInfo = new (keyChar, key, shift, alt, control)
+        };
+
+        try
+        {
+            ProcessInput (input);
+        }
+        catch (OverflowException)
+        { }
+    }
+
+    //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 (cKeyInfo.KeyChar, cKeyInfo.Key, shift, alt, control);
+    //}
+
+    #endregion Keyboard Handling
+
+    #region Low-Level DotNet tuff
+
+    /// <inheritdoc/>
+    public override void WriteRaw (string ansi)
+    {
+        Console.Out.Write (ansi);
+        Console.Out.Flush ();
+    }
+
+    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);
+                }
+#pragma warning restore CA1416
+            }
+            // INTENT: Why are these eating the exceptions?
+            // Comments would be good here.
+            catch (IOException)
+            {
+                // CONCURRENCY: Unsynchronized access to Clip is not safe.
+                Clip = new (Screen);
+            }
+            catch (ArgumentOutOfRangeException)
+            {
+                // CONCURRENCY: Unsynchronized access to Clip is not safe.
+                Clip = new (Screen);
+            }
+        }
+        else
+        {
+            Console.Out.Write (EscSeqUtils.CSI_SetTerminalWindowSize (Rows, Cols));
+        }
+
+        // CONCURRENCY: Unsynchronized access to Clip is not safe.
+        Clip = new (Screen);
+    }
+
+    #endregion Low-Level DotNet tuff
+}

+ 695 - 0
Terminal.Gui/ConsoleDrivers/NetDriver/NetEvents.cs

@@ -0,0 +1,695 @@
+#nullable enable
+using System.Diagnostics.CodeAnalysis;
+
+namespace Terminal.Gui;
+
+internal class NetEvents : IDisposable
+{
+    private readonly ManualResetEventSlim _inputReady = new (false);
+    private CancellationTokenSource? _inputReadyCancellationTokenSource;
+    private readonly Queue<InputResult> _inputQueue = new ();
+    private readonly IConsoleDriver _consoleDriver;
+    private ConsoleKeyInfo []? _cki;
+    private bool _isEscSeq;
+#if PROCESS_REQUEST
+    bool _neededProcessRequest;
+#endif
+    public NetEvents (IConsoleDriver consoleDriver)
+    {
+        _consoleDriver = consoleDriver ?? throw new ArgumentNullException (nameof (consoleDriver));
+        _inputReadyCancellationTokenSource = new ();
+
+        Task.Run (ProcessInputQueue, _inputReadyCancellationTokenSource.Token);
+
+        Task.Run (CheckWindowSizeChange, _inputReadyCancellationTokenSource.Token);
+    }
+
+    public InputResult? DequeueInput ()
+    {
+        while (_inputReadyCancellationTokenSource is { Token.IsCancellationRequested: false })
+        {
+            try
+            {
+                if (!_inputReadyCancellationTokenSource.Token.IsCancellationRequested)
+                {
+                    if (_inputQueue.Count == 0)
+                    {
+                        _inputReady.Wait (_inputReadyCancellationTokenSource.Token);
+                    }
+                }
+
+                if (_inputQueue.Count > 0)
+                {
+                    return _inputQueue.Dequeue ();
+                }
+            }
+            catch (OperationCanceledException)
+            {
+                return null;
+            }
+            finally
+            {
+                if (_inputReadyCancellationTokenSource is { IsCancellationRequested: false })
+                {
+                    _inputReady.Reset ();
+                }
+            }
+
+#if PROCESS_REQUEST
+            _neededProcessRequest = false;
+#endif
+        }
+
+        return null;
+    }
+
+    private ConsoleKeyInfo ReadConsoleKeyInfo (CancellationToken cancellationToken, bool intercept = true)
+    {
+        while (!cancellationToken.IsCancellationRequested)
+        {
+            // if there is a key available, return it without waiting
+            //  (or dispatching work to the thread queue)
+            if (Console.KeyAvailable)
+            {
+                return Console.ReadKey (intercept);
+            }
+
+            // The delay must be here because it may have a request response after a while
+            // In WSL it takes longer for keys to be available.
+            Task.Delay (100, cancellationToken).Wait (cancellationToken);
+        }
+
+        cancellationToken.ThrowIfCancellationRequested ();
+
+        return default (ConsoleKeyInfo);
+    }
+
+    private void ProcessInputQueue ()
+    {
+        while (_inputReadyCancellationTokenSource is { IsCancellationRequested: false })
+        {
+            try
+            {
+                ConsoleKey key = 0;
+                ConsoleModifiers mod = 0;
+                ConsoleKeyInfo newConsoleKeyInfo = default;
+
+                while (_inputReadyCancellationTokenSource is { IsCancellationRequested: false })
+                {
+                    ConsoleKeyInfo consoleKeyInfo;
+
+                    try
+                    {
+                        consoleKeyInfo = ReadConsoleKeyInfo (_inputReadyCancellationTokenSource.Token);
+                    }
+                    catch (OperationCanceledException)
+                    {
+                        return;
+                    }
+
+                    var ckiAlreadyResized = false;
+
+                    if (EscSeqUtils.IncompleteCkInfos is { })
+                    {
+                        ckiAlreadyResized = true;
+
+                        _cki = EscSeqUtils.ResizeArray (consoleKeyInfo, _cki);
+                        _cki = EscSeqUtils.InsertArray (EscSeqUtils.IncompleteCkInfos, _cki);
+                        EscSeqUtils.IncompleteCkInfos = null;
+
+                        if (_cki.Length > 1 && _cki [0].KeyChar == '\u001B')
+                        {
+                            _isEscSeq = true;
+                        }
+                    }
+
+                    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 (
+                                                                                    (char)KeyCode.Esc,
+                                                                                    0,
+                                                                                    false,
+                                                                                    false,
+                                                                                    false
+                                                                                   ),
+                                                                               _cki
+                                                                              );
+                        }
+
+                        _isEscSeq = true;
+
+                        if ((_cki is { } && _cki [^1].KeyChar != Key.Esc && consoleKeyInfo.KeyChar != Key.Esc && consoleKeyInfo.KeyChar <= Key.Space)
+                            || (_cki is { } && _cki [^1].KeyChar != '\u001B' && consoleKeyInfo.KeyChar == 127)
+                            || (_cki is { }
+                                && char.IsLetter (_cki [^1].KeyChar)
+                                && char.IsLower (consoleKeyInfo.KeyChar)
+                                && char.IsLetter (consoleKeyInfo.KeyChar))
+                            || (_cki is { Length: > 2 } && char.IsLetter (_cki [^1].KeyChar) && char.IsLetterOrDigit (consoleKeyInfo.KeyChar))
+                            || (_cki is { Length: > 2 } && char.IsLetter (_cki [^1].KeyChar) && char.IsPunctuation (consoleKeyInfo.KeyChar))
+                            || (_cki is { Length: > 2 } && char.IsLetter (_cki [^1].KeyChar) && char.IsSymbol (consoleKeyInfo.KeyChar)))
+                        {
+                            ProcessRequestResponse (ref newConsoleKeyInfo, ref key, _cki, ref mod);
+                            _cki = null;
+                            _isEscSeq = false;
+
+                            ProcessMapConsoleKeyInfo (consoleKeyInfo);
+                        }
+                        else
+                        {
+                            newConsoleKeyInfo = consoleKeyInfo;
+
+                            if (!ckiAlreadyResized)
+                            {
+                                _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;
+                }
+
+                if (_inputQueue.Count > 0)
+                {
+                    _inputReady.Set ();
+                }
+            }
+            catch (OperationCanceledException)
+            {
+                return;
+            }
+        }
+
+        void ProcessMapConsoleKeyInfo (ConsoleKeyInfo consoleKeyInfo)
+        {
+            _inputQueue.Enqueue (
+                                 new ()
+                                 {
+                                     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).Wait (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 (_inputReadyCancellationTokenSource is { IsCancellationRequested: false })
+        {
+            try
+            {
+                RequestWindowSize (_inputReadyCancellationTokenSource.Token);
+
+                if (_inputQueue.Count > 0)
+                {
+                    _inputReady.Set ();
+                }
+            }
+            catch (OperationCanceledException)
+            {
+                return;
+            }
+        }
+    }
+
+    /// <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 ()
+                             {
+                                 EventType = EventType.WindowSize, WindowSizeEvent = new () { 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 (
+                                                     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;
+        }
+
+        if (newConsoleKeyInfo != default)
+        {
+            HandleKeyboardEvent (newConsoleKeyInfo);
+        }
+    }
+
+    [UnconditionalSuppressMessage ("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "<Pending>")]
+    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)
+    {
+        if (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.
+            // The observation is correct because the response isn't immediate and this is useless
+            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;
+            }
+        }
+        else if (terminating == EscSeqUtils.CSI_ReportTerminalSizeInChars_Terminator)
+        {
+            if (values [0] == 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)
+                                       );
+            }
+            else
+            {
+                EnqueueRequestResponseEvent (c1Control, code, values, terminating);
+            }
+        }
+        else
+        {
+            EnqueueRequestResponseEvent (c1Control, code, values, terminating);
+        }
+
+        _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 () { EventType = EventType.Mouse, MouseEvent = mouseEvent }
+                            );
+    }
+
+    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;
+
+        _inputReady.Dispose ();
+
+        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
+        }
+    }
+}

+ 168 - 0
Terminal.Gui/ConsoleDrivers/NetDriver/NetMainLoop.cs

@@ -0,0 +1,168 @@
+#nullable enable
+
+using System.Collections.Concurrent;
+
+namespace Terminal.Gui;
+
+/// <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.
+/// </summary>
+/// <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<NetEvents.InputResult>? ProcessInput;
+
+    private readonly ManualResetEventSlim _eventReady = new (false);
+    private readonly CancellationTokenSource _eventReadyTokenSource = new ();
+    private readonly CancellationTokenSource _inputHandlerTokenSource = new ();
+    private readonly ManualResetEventSlim _waitForProbe = new (false);
+    private readonly ConcurrentQueue<NetEvents.InputResult> _resultQueue = new ();
+    private MainLoop? _mainLoop;
+
+    /// <summary>Initializes the class with the console driver.</summary>
+    /// <remarks>Passing a IConsoleDriver 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 (IConsoleDriver consoleDriver)
+    {
+        ArgumentNullException.ThrowIfNull (consoleDriver);
+
+        if (!ConsoleDriver.RunningUnitTests)
+        {
+            _netEvents = new (consoleDriver);
+        }
+    }
+
+    void IMainLoopDriver.Setup (MainLoop mainLoop)
+    {
+        _mainLoop = mainLoop;
+
+        if (!ConsoleDriver.RunningUnitTests)
+        {
+            Task.Run (NetInputHandler, _inputHandlerTokenSource.Token);
+        }
+    }
+
+    void IMainLoopDriver.Wakeup () { _eventReady.Set (); }
+
+    bool IMainLoopDriver.EventsPending ()
+    {
+        if (ConsoleDriver.RunningUnitTests)
+        {
+            return true;
+        }
+
+        _waitForProbe.Set ();
+
+        if (_resultQueue.Count > 0 || _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 ();
+        }
+
+        _eventReadyTokenSource.Token.ThrowIfCancellationRequested ();
+
+        if (!_eventReadyTokenSource.IsCancellationRequested)
+        {
+            return _resultQueue.Count > 0 || _mainLoop.CheckTimersAndIdleHandlers (out _);
+        }
+
+        // If cancellation was requested then always return true
+        return true;
+    }
+
+    void IMainLoopDriver.Iteration ()
+    {
+        while (!ConsoleDriver.RunningUnitTests && _resultQueue.TryDequeue (out NetEvents.InputResult inputRecords))
+        {
+            ProcessInput?.Invoke (inputRecords);
+        }
+    }
+
+    void IMainLoopDriver.TearDown ()
+    {
+        _inputHandlerTokenSource.Cancel ();
+        _inputHandlerTokenSource.Dispose ();
+        _eventReadyTokenSource.Cancel ();
+        _eventReadyTokenSource.Dispose ();
+
+        _eventReady.Dispose ();
+        _waitForProbe.Dispose ();
+
+        _resultQueue.Clear ();
+        _netEvents?.Dispose ();
+        _netEvents = null;
+
+        _mainLoop = null;
+    }
+
+    private void NetInputHandler ()
+    {
+        while (_mainLoop is { })
+        {
+            try
+            {
+                if (!_inputHandlerTokenSource.IsCancellationRequested)
+                {
+                    try
+                    {
+                        _waitForProbe.Wait (_inputHandlerTokenSource.Token);
+                    }
+                    catch (Exception ex)
+                    {
+                        if (ex is OperationCanceledException or ObjectDisposedException)
+                        {
+                            return;
+                        }
+
+                        throw;
+                    }
+
+                    _waitForProbe.Reset ();
+                }
+
+                ProcessInputQueue ();
+            }
+            catch (OperationCanceledException)
+            {
+                return;
+            }
+        }
+    }
+
+    private void ProcessInputQueue ()
+    {
+        if (_resultQueue.Count == 0)
+        {
+            NetEvents.InputResult? result = _netEvents!.DequeueInput ();
+
+            if (result.HasValue)
+            {
+                _resultQueue.Enqueue (result.Value);
+
+                _eventReady.Set ();
+            }
+        }
+    }
+}

+ 126 - 0
Terminal.Gui/ConsoleDrivers/NetDriver/NetWinVTConsole.cs

@@ -0,0 +1,126 @@
+#nullable enable
+using System.Runtime.InteropServices;
+
+namespace Terminal.Gui;
+
+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);
+}

+ 0 - 2545
Terminal.Gui/ConsoleDrivers/WindowsDriver.cs

@@ -1,2545 +0,0 @@
-//
-// WindowsDriver.cs: Windows specific driver
-//
-
-// HACK:
-// WindowsConsole/Terminal has two issues:
-// 1) Tearing can occur when the console is resized.
-// 2) The values provided during Init (and the first WindowsConsole.EventType.WindowBufferSize) are not correct.
-//
-// If HACK_CHECK_WINCHANGED is defined then we ignore WindowsConsole.EventType.WindowBufferSize events
-// and instead check the console size every every 500ms in a thread in WidowsMainLoop. 
-// As of Windows 11 23H2 25947.1000 and/or WT 1.19.2682 tearing no longer occurs when using 
-// the WindowsConsole.EventType.WindowBufferSize event. However, on Init the window size is
-// still incorrect so we still need this hack.
-
-#define HACK_CHECK_WINCHANGED
-
-using System.ComponentModel;
-using System.Diagnostics;
-using System.Runtime.InteropServices;
-using System.Text;
-using static Terminal.Gui.ConsoleDrivers.ConsoleKeyMapping;
-using static Terminal.Gui.SpinnerStyle;
-
-namespace Terminal.Gui;
-
-internal class WindowsConsole
-{
-    public const int STD_OUTPUT_HANDLE = -11;
-    public const int STD_INPUT_HANDLE = -10;
-
-    private readonly nint _inputHandle;
-    private readonly nint _outputHandle;
-    private nint _screenBuffer;
-    private readonly uint _originalConsoleMode;
-    private CursorVisibility? _initialCursorVisibility;
-    private CursorVisibility? _currentCursorVisibility;
-    private CursorVisibility? _pendingCursorVisibility;
-    private readonly StringBuilder _stringBuilder = new (256 * 1024);
-    private string _lastWrite = string.Empty;
-
-    public WindowsConsole ()
-    {
-        _inputHandle = GetStdHandle (STD_INPUT_HANDLE);
-        _outputHandle = GetStdHandle (STD_OUTPUT_HANDLE);
-        _originalConsoleMode = ConsoleMode;
-        uint newConsoleMode = _originalConsoleMode;
-        newConsoleMode |= (uint)(ConsoleModes.EnableMouseInput | ConsoleModes.EnableExtendedFlags);
-        newConsoleMode &= ~(uint)ConsoleModes.EnableQuickEditMode;
-        newConsoleMode &= ~(uint)ConsoleModes.EnableProcessedInput;
-        ConsoleMode = newConsoleMode;
-    }
-
-    private CharInfo [] _originalStdOutChars;
-
-    public bool WriteToConsole (Size size, ExtendedCharInfo [] charInfoBuffer, Coord bufferSize, SmallRect window, bool force16Colors)
-    {
-        //Debug.WriteLine ("WriteToConsole");
-
-        if (_screenBuffer == nint.Zero)
-        {
-            ReadFromConsoleOutput (size, bufferSize, ref window);
-        }
-
-        var result = false;
-
-        if (force16Colors)
-        {
-            var i = 0;
-            CharInfo [] ci = new CharInfo [charInfoBuffer.Length];
-
-            foreach (ExtendedCharInfo info in charInfoBuffer)
-            {
-                ci [i++] = new CharInfo
-                {
-                    Char = new CharUnion { UnicodeChar = info.Char },
-                    Attributes =
-                        (ushort)((int)info.Attribute.Foreground.GetClosestNamedColor16 () | ((int)info.Attribute.Background.GetClosestNamedColor16 () << 4))
-                };
-            }
-
-            result = WriteConsoleOutput (_screenBuffer, ci, bufferSize, new Coord { X = window.Left, Y = window.Top }, ref window);
-        }
-        else
-        {
-            _stringBuilder.Clear ();
-
-            _stringBuilder.Append (EscSeqUtils.CSI_SaveCursorPosition);
-            _stringBuilder.Append (EscSeqUtils.CSI_SetCursorPosition (0, 0));
-
-            Attribute? prev = null;
-
-            foreach (ExtendedCharInfo info in charInfoBuffer)
-            {
-                Attribute attr = info.Attribute;
-
-                if (attr != prev)
-                {
-                    prev = attr;
-                    _stringBuilder.Append (EscSeqUtils.CSI_SetForegroundColorRGB (attr.Foreground.R, attr.Foreground.G, attr.Foreground.B));
-                    _stringBuilder.Append (EscSeqUtils.CSI_SetBackgroundColorRGB (attr.Background.R, attr.Background.G, attr.Background.B));
-                }
-
-                if (info.Char != '\x1b')
-                {
-                    if (!info.Empty)
-                    {
-                        _stringBuilder.Append (info.Char);
-                    }
-                }
-                else
-                {
-                    _stringBuilder.Append (' ');
-                }
-            }
-
-            _stringBuilder.Append (EscSeqUtils.CSI_RestoreCursorPosition);
-            _stringBuilder.Append (EscSeqUtils.CSI_HideCursor);
-
-            var s = _stringBuilder.ToString ();
-
-            // TODO: requires extensive testing if we go down this route
-            // If console output has changed
-            if (s != _lastWrite)
-            {
-                // supply console with the new content
-                result = WriteConsole (_screenBuffer, s, (uint)s.Length, out uint _, nint.Zero);
-            }
-
-            _lastWrite = s;
-
-            foreach (var sixel in Application.Sixel)
-            {
-                SetCursorPosition (new Coord ((short)sixel.ScreenPosition.X, (short)sixel.ScreenPosition.Y));
-                WriteConsole (_screenBuffer, sixel.SixelData, (uint)sixel.SixelData.Length, out uint _, nint.Zero);
-            }
-        }
-
-        if (!result)
-        {
-            int err = Marshal.GetLastWin32Error ();
-
-            if (err != 0)
-            {
-                throw new Win32Exception (err);
-            }
-        }
-
-        return result;
-    }
-
-    public bool WriteANSI (string ansi)
-    {
-        return WriteConsole (_screenBuffer, ansi, (uint)ansi.Length, out uint _, nint.Zero);
-    }
-
-    public void ReadFromConsoleOutput (Size size, Coord coords, ref SmallRect window)
-    {
-        _screenBuffer = CreateConsoleScreenBuffer (
-                                                   DesiredAccess.GenericRead | DesiredAccess.GenericWrite,
-                                                   ShareMode.FileShareRead | ShareMode.FileShareWrite,
-                                                   nint.Zero,
-                                                   1,
-                                                   nint.Zero
-                                                  );
-
-        if (_screenBuffer == INVALID_HANDLE_VALUE)
-        {
-            int err = Marshal.GetLastWin32Error ();
-
-            if (err != 0)
-            {
-                throw new Win32Exception (err);
-            }
-        }
-
-        SetInitialCursorVisibility ();
-
-        if (!SetConsoleActiveScreenBuffer (_screenBuffer))
-        {
-            throw new Win32Exception (Marshal.GetLastWin32Error ());
-        }
-
-        _originalStdOutChars = new CharInfo [size.Height * size.Width];
-
-        if (!ReadConsoleOutput (_screenBuffer, _originalStdOutChars, coords, new Coord { X = 0, Y = 0 }, ref window))
-        {
-            throw new Win32Exception (Marshal.GetLastWin32Error ());
-        }
-    }
-
-    public bool SetCursorPosition (Coord position)
-    {
-        return SetConsoleCursorPosition (_screenBuffer, position);
-    }
-
-    public void SetInitialCursorVisibility ()
-    {
-        if (_initialCursorVisibility.HasValue == false && GetCursorVisibility (out CursorVisibility visibility))
-        {
-            _initialCursorVisibility = visibility;
-        }
-    }
-
-    public bool GetCursorVisibility (out CursorVisibility visibility)
-    {
-        if (_screenBuffer == nint.Zero)
-        {
-            visibility = CursorVisibility.Invisible;
-
-            return false;
-        }
-
-        if (!GetConsoleCursorInfo (_screenBuffer, out ConsoleCursorInfo info))
-        {
-            int err = Marshal.GetLastWin32Error ();
-
-            if (err != 0)
-            {
-                throw new Win32Exception (err);
-            }
-
-            visibility = CursorVisibility.Default;
-
-            return false;
-        }
-
-        if (!info.bVisible)
-        {
-            visibility = CursorVisibility.Invisible;
-        }
-        else if (info.dwSize > 50)
-        {
-            visibility = CursorVisibility.Default;
-        }
-        else
-        {
-            visibility = CursorVisibility.Default;
-        }
-
-        return true;
-    }
-
-    public bool EnsureCursorVisibility ()
-    {
-        if (_initialCursorVisibility.HasValue && _pendingCursorVisibility.HasValue && SetCursorVisibility (_pendingCursorVisibility.Value))
-        {
-            _pendingCursorVisibility = null;
-
-            return true;
-        }
-
-        return false;
-    }
-
-    public void ForceRefreshCursorVisibility ()
-    {
-        if (_currentCursorVisibility.HasValue)
-        {
-            _pendingCursorVisibility = _currentCursorVisibility;
-            _currentCursorVisibility = null;
-        }
-    }
-
-    public bool SetCursorVisibility (CursorVisibility visibility)
-    {
-        if (_initialCursorVisibility.HasValue == false)
-        {
-            _pendingCursorVisibility = visibility;
-
-            return false;
-        }
-
-        if (_currentCursorVisibility.HasValue == false || _currentCursorVisibility.Value != visibility)
-        {
-            var info = new ConsoleCursorInfo
-            {
-                dwSize = (uint)visibility & 0x00FF,
-                bVisible = ((uint)visibility & 0xFF00) != 0
-            };
-
-            if (!SetConsoleCursorInfo (_screenBuffer, ref info))
-            {
-                return false;
-            }
-
-            _currentCursorVisibility = visibility;
-        }
-
-        return true;
-    }
-
-    public void Cleanup ()
-    {
-        if (_initialCursorVisibility.HasValue)
-        {
-            SetCursorVisibility (_initialCursorVisibility.Value);
-        }
-
-        SetConsoleOutputWindow (out _);
-
-        ConsoleMode = _originalConsoleMode;
-
-        if (!SetConsoleActiveScreenBuffer (_outputHandle))
-        {
-            int err = Marshal.GetLastWin32Error ();
-            Console.WriteLine ("Error: {0}", err);
-        }
-
-        if (_screenBuffer != nint.Zero)
-        {
-            CloseHandle (_screenBuffer);
-        }
-
-        _screenBuffer = nint.Zero;
-    }
-
-    internal Size GetConsoleBufferWindow (out Point position)
-    {
-        if (_screenBuffer == nint.Zero)
-        {
-            position = Point.Empty;
-
-            return Size.Empty;
-        }
-
-        var csbi = new CONSOLE_SCREEN_BUFFER_INFOEX ();
-        csbi.cbSize = (uint)Marshal.SizeOf (csbi);
-
-        if (!GetConsoleScreenBufferInfoEx (_screenBuffer, ref csbi))
-        {
-            //throw new System.ComponentModel.Win32Exception (Marshal.GetLastWin32Error ());
-            position = Point.Empty;
-
-            return Size.Empty;
-        }
-
-        Size sz = new (
-                       csbi.srWindow.Right - csbi.srWindow.Left + 1,
-                       csbi.srWindow.Bottom - csbi.srWindow.Top + 1);
-        position = new (csbi.srWindow.Left, csbi.srWindow.Top);
-
-        return sz;
-    }
-
-    internal Size GetConsoleOutputWindow (out Point position)
-    {
-        var csbi = new CONSOLE_SCREEN_BUFFER_INFOEX ();
-        csbi.cbSize = (uint)Marshal.SizeOf (csbi);
-
-        if (!GetConsoleScreenBufferInfoEx (_outputHandle, ref csbi))
-        {
-            throw new Win32Exception (Marshal.GetLastWin32Error ());
-        }
-
-        Size sz = new (
-                       csbi.srWindow.Right - csbi.srWindow.Left + 1,
-                       csbi.srWindow.Bottom - csbi.srWindow.Top + 1);
-        position = new (csbi.srWindow.Left, csbi.srWindow.Top);
-
-        return sz;
-    }
-
-    internal Size SetConsoleWindow (short cols, short rows)
-    {
-        var csbi = new CONSOLE_SCREEN_BUFFER_INFOEX ();
-        csbi.cbSize = (uint)Marshal.SizeOf (csbi);
-
-        if (!GetConsoleScreenBufferInfoEx (_screenBuffer, ref csbi))
-        {
-            throw new Win32Exception (Marshal.GetLastWin32Error ());
-        }
-
-        Coord maxWinSize = GetLargestConsoleWindowSize (_screenBuffer);
-        short newCols = Math.Min (cols, maxWinSize.X);
-        short newRows = Math.Min (rows, maxWinSize.Y);
-        csbi.dwSize = new Coord (newCols, Math.Max (newRows, (short)1));
-        csbi.srWindow = new SmallRect (0, 0, newCols, newRows);
-        csbi.dwMaximumWindowSize = new Coord (newCols, newRows);
-
-        if (!SetConsoleScreenBufferInfoEx (_screenBuffer, ref csbi))
-        {
-            throw new Win32Exception (Marshal.GetLastWin32Error ());
-        }
-
-        var winRect = new SmallRect (0, 0, (short)(newCols - 1), (short)Math.Max (newRows - 1, 0));
-
-        if (!SetConsoleWindowInfo (_outputHandle, true, ref winRect))
-        {
-            //throw new System.ComponentModel.Win32Exception (Marshal.GetLastWin32Error ());
-            return new (cols, rows);
-        }
-
-        SetConsoleOutputWindow (csbi);
-
-        return new (winRect.Right + 1, newRows - 1 < 0 ? 0 : winRect.Bottom + 1);
-    }
-
-    private void SetConsoleOutputWindow (CONSOLE_SCREEN_BUFFER_INFOEX csbi)
-    {
-        if (_screenBuffer != nint.Zero && !SetConsoleScreenBufferInfoEx (_screenBuffer, ref csbi))
-        {
-            throw new Win32Exception (Marshal.GetLastWin32Error ());
-        }
-    }
-
-    internal Size SetConsoleOutputWindow (out Point position)
-    {
-        if (_screenBuffer == nint.Zero)
-        {
-            position = Point.Empty;
-
-            return Size.Empty;
-        }
-
-        var csbi = new CONSOLE_SCREEN_BUFFER_INFOEX ();
-        csbi.cbSize = (uint)Marshal.SizeOf (csbi);
-
-        if (!GetConsoleScreenBufferInfoEx (_screenBuffer, ref csbi))
-        {
-            throw new Win32Exception (Marshal.GetLastWin32Error ());
-        }
-
-        Size sz = new (
-                           csbi.srWindow.Right - csbi.srWindow.Left + 1,
-                           Math.Max (csbi.srWindow.Bottom - csbi.srWindow.Top + 1, 0));
-        position = new (csbi.srWindow.Left, csbi.srWindow.Top);
-        SetConsoleOutputWindow (csbi);
-        var winRect = new SmallRect (0, 0, (short)(sz.Width - 1), (short)Math.Max (sz.Height - 1, 0));
-
-        if (!SetConsoleScreenBufferInfoEx (_outputHandle, ref csbi))
-        {
-            throw new Win32Exception (Marshal.GetLastWin32Error ());
-        }
-
-        if (!SetConsoleWindowInfo (_outputHandle, true, ref winRect))
-        {
-            throw new Win32Exception (Marshal.GetLastWin32Error ());
-        }
-
-        return sz;
-    }
-
-    private uint ConsoleMode
-    {
-        get
-        {
-            GetConsoleMode (_inputHandle, out uint v);
-
-            return v;
-        }
-        set => SetConsoleMode (_inputHandle, value);
-    }
-
-    [Flags]
-    public enum ConsoleModes : uint
-    {
-        EnableProcessedInput = 1,
-        EnableMouseInput = 16,
-        EnableQuickEditMode = 64,
-        EnableExtendedFlags = 128
-    }
-
-    [StructLayout (LayoutKind.Explicit, CharSet = CharSet.Unicode)]
-    public struct KeyEventRecord
-    {
-        [FieldOffset (0)]
-        [MarshalAs (UnmanagedType.Bool)]
-        public bool bKeyDown;
-
-        [FieldOffset (4)]
-        [MarshalAs (UnmanagedType.U2)]
-        public ushort wRepeatCount;
-
-        [FieldOffset (6)]
-        [MarshalAs (UnmanagedType.U2)]
-        public VK wVirtualKeyCode;
-
-        [FieldOffset (8)]
-        [MarshalAs (UnmanagedType.U2)]
-        public ushort wVirtualScanCode;
-
-        [FieldOffset (10)]
-        public char UnicodeChar;
-
-        [FieldOffset (12)]
-        [MarshalAs (UnmanagedType.U4)]
-        public ControlKeyState dwControlKeyState;
-
-        public readonly override string ToString ()
-        {
-            return
-                $"[KeyEventRecord({(bKeyDown ? "down" : "up")},{wRepeatCount},{wVirtualKeyCode},{wVirtualScanCode},{new Rune (UnicodeChar).MakePrintable ()},{dwControlKeyState})]";
-        }
-    }
-
-    [Flags]
-    public enum ButtonState
-    {
-        NoButtonPressed = 0,
-        Button1Pressed = 1,
-        Button2Pressed = 4,
-        Button3Pressed = 8,
-        Button4Pressed = 16,
-        RightmostButtonPressed = 2
-    }
-
-    [Flags]
-    public enum ControlKeyState
-    {
-        NoControlKeyPressed = 0,
-        RightAltPressed = 1,
-        LeftAltPressed = 2,
-        RightControlPressed = 4,
-        LeftControlPressed = 8,
-        ShiftPressed = 16,
-        NumlockOn = 32,
-        ScrolllockOn = 64,
-        CapslockOn = 128,
-        EnhancedKey = 256
-    }
-
-    [Flags]
-    public enum EventFlags
-    {
-        NoEvent = 0,
-        MouseMoved = 1,
-        DoubleClick = 2,
-        MouseWheeled = 4,
-        MouseHorizontalWheeled = 8
-    }
-
-    [StructLayout (LayoutKind.Explicit)]
-    public struct MouseEventRecord
-    {
-        [FieldOffset (0)]
-        public Coord MousePosition;
-
-        [FieldOffset (4)]
-        public ButtonState ButtonState;
-
-        [FieldOffset (8)]
-        public ControlKeyState ControlKeyState;
-
-        [FieldOffset (12)]
-        public EventFlags EventFlags;
-
-        public readonly override string ToString () { return $"[Mouse{MousePosition},{ButtonState},{ControlKeyState},{EventFlags}]"; }
-    }
-
-    public struct WindowBufferSizeRecord
-    {
-        public Coord _size;
-
-        public WindowBufferSizeRecord (short x, short y) { _size = new Coord (x, y); }
-
-        public readonly override string ToString () { return $"[WindowBufferSize{_size}"; }
-    }
-
-    [StructLayout (LayoutKind.Sequential)]
-    public struct MenuEventRecord
-    {
-        public uint dwCommandId;
-    }
-
-    [StructLayout (LayoutKind.Sequential)]
-    public struct FocusEventRecord
-    {
-        public uint bSetFocus;
-    }
-
-    public enum EventType : ushort
-    {
-        Focus = 0x10,
-        Key = 0x1,
-        Menu = 0x8,
-        Mouse = 2,
-        WindowBufferSize = 4
-    }
-
-    [StructLayout (LayoutKind.Explicit)]
-    public struct InputRecord
-    {
-        [FieldOffset (0)]
-        public EventType EventType;
-
-        [FieldOffset (4)]
-        public KeyEventRecord KeyEvent;
-
-        [FieldOffset (4)]
-        public MouseEventRecord MouseEvent;
-
-        [FieldOffset (4)]
-        public WindowBufferSizeRecord WindowBufferSizeEvent;
-
-        [FieldOffset (4)]
-        public MenuEventRecord MenuEvent;
-
-        [FieldOffset (4)]
-        public FocusEventRecord FocusEvent;
-
-        public readonly override string ToString ()
-        {
-            return EventType switch
-            {
-                EventType.Focus => FocusEvent.ToString (),
-                EventType.Key => KeyEvent.ToString (),
-                EventType.Menu => MenuEvent.ToString (),
-                EventType.Mouse => MouseEvent.ToString (),
-                EventType.WindowBufferSize => WindowBufferSizeEvent.ToString (),
-                _ => "Unknown event type: " + EventType
-            };
-        }
-    }
-
-    [Flags]
-    private enum ShareMode : uint
-    {
-        FileShareRead = 1,
-        FileShareWrite = 2
-    }
-
-    [Flags]
-    private enum DesiredAccess : uint
-    {
-        GenericRead = 2147483648,
-        GenericWrite = 1073741824
-    }
-
-    [StructLayout (LayoutKind.Sequential)]
-    public struct ConsoleScreenBufferInfo
-    {
-        public Coord dwSize;
-        public Coord dwCursorPosition;
-        public ushort wAttributes;
-        public SmallRect srWindow;
-        public Coord dwMaximumWindowSize;
-    }
-
-    [StructLayout (LayoutKind.Sequential)]
-    public struct Coord
-    {
-        public short X;
-        public short Y;
-
-        public Coord (short x, short y)
-        {
-            X = x;
-            Y = y;
-        }
-
-        public readonly override string ToString () { return $"({X},{Y})"; }
-    }
-
-    [StructLayout (LayoutKind.Explicit, CharSet = CharSet.Unicode)]
-    public struct CharUnion
-    {
-        [FieldOffset (0)]
-        public char UnicodeChar;
-
-        [FieldOffset (0)]
-        public byte AsciiChar;
-    }
-
-    [StructLayout (LayoutKind.Explicit, CharSet = CharSet.Unicode)]
-    public struct CharInfo
-    {
-        [FieldOffset (0)]
-        public CharUnion Char;
-
-        [FieldOffset (2)]
-        public ushort Attributes;
-    }
-
-    public struct ExtendedCharInfo
-    {
-        public char Char { get; set; }
-        public Attribute Attribute { get; set; }
-        public bool Empty { get; set; } // TODO: Temp hack until virtual terminal sequences
-
-        public ExtendedCharInfo (char character, Attribute attribute)
-        {
-            Char = character;
-            Attribute = attribute;
-            Empty = false;
-        }
-    }
-
-    [StructLayout (LayoutKind.Sequential)]
-    public struct SmallRect
-    {
-        public short Left;
-        public short Top;
-        public short Right;
-        public short Bottom;
-
-        public SmallRect (short left, short top, short right, short bottom)
-        {
-            Left = left;
-            Top = top;
-            Right = right;
-            Bottom = bottom;
-        }
-
-        public static void MakeEmpty (ref SmallRect rect) { rect.Left = -1; }
-
-        public static void Update (ref SmallRect rect, short col, short row)
-        {
-            if (rect.Left == -1)
-            {
-                rect.Left = rect.Right = col;
-                rect.Bottom = rect.Top = row;
-
-                return;
-            }
-
-            if (col >= rect.Left && col <= rect.Right && row >= rect.Top && row <= rect.Bottom)
-            {
-                return;
-            }
-
-            if (col < rect.Left)
-            {
-                rect.Left = col;
-            }
-
-            if (col > rect.Right)
-            {
-                rect.Right = col;
-            }
-
-            if (row < rect.Top)
-            {
-                rect.Top = row;
-            }
-
-            if (row > rect.Bottom)
-            {
-                rect.Bottom = row;
-            }
-        }
-
-        public readonly override string ToString () { return $"Left={Left},Top={Top},Right={Right},Bottom={Bottom}"; }
-    }
-
-    [StructLayout (LayoutKind.Sequential)]
-    public struct ConsoleKeyInfoEx
-    {
-        public ConsoleKeyInfo ConsoleKeyInfo;
-        public bool CapsLock;
-        public bool NumLock;
-        public bool ScrollLock;
-
-        public ConsoleKeyInfoEx (ConsoleKeyInfo consoleKeyInfo, bool capslock, bool numlock, bool scrolllock)
-        {
-            ConsoleKeyInfo = consoleKeyInfo;
-            CapsLock = capslock;
-            NumLock = numlock;
-            ScrollLock = scrolllock;
-        }
-
-        /// <summary>
-        ///     Prints a ConsoleKeyInfoEx structure
-        /// </summary>
-        /// <param name="ex"></param>
-        /// <returns></returns>
-        public readonly string ToString (ConsoleKeyInfoEx ex)
-        {
-            var ke = new Key ((KeyCode)ex.ConsoleKeyInfo.KeyChar);
-            var sb = new StringBuilder ();
-            sb.Append ($"Key: {(KeyCode)ex.ConsoleKeyInfo.Key} ({ex.ConsoleKeyInfo.Key})");
-            sb.Append ((ex.ConsoleKeyInfo.Modifiers & ConsoleModifiers.Shift) != 0 ? " | Shift" : string.Empty);
-            sb.Append ((ex.ConsoleKeyInfo.Modifiers & ConsoleModifiers.Control) != 0 ? " | Control" : string.Empty);
-            sb.Append ((ex.ConsoleKeyInfo.Modifiers & ConsoleModifiers.Alt) != 0 ? " | Alt" : string.Empty);
-            sb.Append ($", KeyChar: {ke.AsRune.MakePrintable ()} ({(uint)ex.ConsoleKeyInfo.KeyChar}) ");
-            sb.Append (ex.CapsLock ? "caps," : string.Empty);
-            sb.Append (ex.NumLock ? "num," : string.Empty);
-            sb.Append (ex.ScrollLock ? "scroll," : string.Empty);
-            string s = sb.ToString ().TrimEnd (',').TrimEnd (' ');
-
-            return $"[ConsoleKeyInfoEx({s})]";
-        }
-    }
-
-    [DllImport ("kernel32.dll", SetLastError = true)]
-    private static extern nint GetStdHandle (int nStdHandle);
-
-    [DllImport ("kernel32.dll", SetLastError = true)]
-    private static extern bool CloseHandle (nint handle);
-
-    [DllImport ("kernel32.dll", EntryPoint = "ReadConsoleInputW", CharSet = CharSet.Unicode)]
-    public static extern bool ReadConsoleInput (
-        nint hConsoleInput,
-        nint lpBuffer,
-        uint nLength,
-        out uint lpNumberOfEventsRead
-    );
-
-    [DllImport ("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
-    private static extern bool ReadConsoleOutput (
-        nint hConsoleOutput,
-        [Out] CharInfo [] lpBuffer,
-        Coord dwBufferSize,
-        Coord dwBufferCoord,
-        ref SmallRect lpReadRegion
-    );
-
-    // TODO: This API is obsolete. See https://learn.microsoft.com/en-us/windows/console/writeconsoleoutput
-    [DllImport ("kernel32.dll", EntryPoint = "WriteConsoleOutputW", SetLastError = true, CharSet = CharSet.Unicode)]
-    private static extern bool WriteConsoleOutput (
-        nint hConsoleOutput,
-        CharInfo [] lpBuffer,
-        Coord dwBufferSize,
-        Coord dwBufferCoord,
-        ref SmallRect lpWriteRegion
-    );
-
-    [DllImport ("kernel32.dll", EntryPoint = "WriteConsole", SetLastError = true, CharSet = CharSet.Unicode)]
-    private static extern bool WriteConsole (
-        nint hConsoleOutput,
-        string lpbufer,
-        uint NumberOfCharsToWriten,
-        out uint lpNumberOfCharsWritten,
-        nint lpReserved
-    );
-
-    [DllImport ("kernel32.dll")]
-    private static extern bool SetConsoleCursorPosition (nint hConsoleOutput, Coord dwCursorPosition);
-
-    [StructLayout (LayoutKind.Sequential)]
-    public struct ConsoleCursorInfo
-    {
-        /// <summary>
-        /// The percentage of the character cell that is filled by the cursor.This value is between 1 and 100.
-        /// The cursor appearance varies, ranging from completely filling the cell to showing up as a horizontal
-        /// line at the bottom of the cell.
-        /// </summary>
-        public uint dwSize;
-        public bool bVisible;
-    }
-
-    [DllImport ("kernel32.dll", SetLastError = true)]
-    private static extern bool SetConsoleCursorInfo (nint hConsoleOutput, [In] ref ConsoleCursorInfo lpConsoleCursorInfo);
-
-    [DllImport ("kernel32.dll", SetLastError = true)]
-    private static extern bool GetConsoleCursorInfo (nint hConsoleOutput, out ConsoleCursorInfo lpConsoleCursorInfo);
-
-    [DllImport ("kernel32.dll")]
-    private static extern bool GetConsoleMode (nint hConsoleHandle, out uint lpMode);
-
-    [DllImport ("kernel32.dll")]
-    private static extern bool SetConsoleMode (nint hConsoleHandle, uint dwMode);
-
-    [DllImport ("kernel32.dll", SetLastError = true)]
-    private static extern nint CreateConsoleScreenBuffer (
-        DesiredAccess dwDesiredAccess,
-        ShareMode dwShareMode,
-        nint secutiryAttributes,
-        uint flags,
-        nint screenBufferData
-    );
-
-    internal static nint INVALID_HANDLE_VALUE = new (-1);
-
-    [DllImport ("kernel32.dll", SetLastError = true)]
-    private static extern bool SetConsoleActiveScreenBuffer (nint Handle);
-
-    [DllImport ("kernel32.dll", SetLastError = true)]
-    private static extern bool GetNumberOfConsoleInputEvents (nint handle, out uint lpcNumberOfEvents);
-
-    internal uint GetNumberOfConsoleInputEvents ()
-    {
-        if (!GetNumberOfConsoleInputEvents (_inputHandle, out uint numOfEvents))
-        {
-            Console.WriteLine ($"Error: {Marshal.GetLastWin32Error ()}");
-
-            return 0;
-        }
-
-        return numOfEvents;
-    }
-
-    [DllImport ("kernel32.dll", SetLastError = true)]
-    private static extern bool FlushConsoleInputBuffer (nint handle);
-
-    internal void FlushConsoleInputBuffer ()
-    {
-        if (!FlushConsoleInputBuffer (_inputHandle))
-        {
-            Console.WriteLine ($"Error: {Marshal.GetLastWin32Error ()}");
-        }
-    }
-
-    public InputRecord [] ReadConsoleInput ()
-    {
-        const int bufferSize = 1;
-        nint pRecord = Marshal.AllocHGlobal (Marshal.SizeOf<InputRecord> () * bufferSize);
-
-        try
-        {
-            ReadConsoleInput (
-                              _inputHandle,
-                              pRecord,
-                              bufferSize,
-                              out uint numberEventsRead);
-
-            return numberEventsRead == 0
-                       ? null
-                       : new [] { Marshal.PtrToStructure<InputRecord> (pRecord) };
-        }
-        catch (Exception)
-        {
-            return null;
-        }
-        finally
-        {
-            Marshal.FreeHGlobal (pRecord);
-        }
-    }
-
-#if false // Not needed on the constructor. Perhaps could be used on resizing. To study.
-		[DllImport ("kernel32.dll", ExactSpelling = true)]
-		static extern IntPtr GetConsoleWindow ();
-
-		[DllImport ("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
-		static extern bool ShowWindow (IntPtr hWnd, int nCmdShow);
-
-		public const int HIDE = 0;
-		public const int MAXIMIZE = 3;
-		public const int MINIMIZE = 6;
-		public const int RESTORE = 9;
-
-		internal void ShowWindow (int state)
-		{
-			IntPtr thisConsole = GetConsoleWindow ();
-			ShowWindow (thisConsole, state);
-		}
-#endif
-
-    // See: https://github.com/gui-cs/Terminal.Gui/issues/357
-
-    [StructLayout (LayoutKind.Sequential)]
-    public struct CONSOLE_SCREEN_BUFFER_INFOEX
-    {
-        public uint cbSize;
-        public Coord dwSize;
-        public Coord dwCursorPosition;
-        public ushort wAttributes;
-        public SmallRect srWindow;
-        public Coord dwMaximumWindowSize;
-        public ushort wPopupAttributes;
-        public bool bFullscreenSupported;
-
-        [MarshalAs (UnmanagedType.ByValArray, SizeConst = 16)]
-        public COLORREF [] ColorTable;
-    }
-
-    [StructLayout (LayoutKind.Explicit, Size = 4)]
-    public struct COLORREF
-    {
-        public COLORREF (byte r, byte g, byte b)
-        {
-            Value = 0;
-            R = r;
-            G = g;
-            B = b;
-        }
-
-        public COLORREF (uint value)
-        {
-            R = 0;
-            G = 0;
-            B = 0;
-            Value = value & 0x00FFFFFF;
-        }
-
-        [FieldOffset (0)]
-        public byte R;
-
-        [FieldOffset (1)]
-        public byte G;
-
-        [FieldOffset (2)]
-        public byte B;
-
-        [FieldOffset (0)]
-        public uint Value;
-    }
-
-    [DllImport ("kernel32.dll", SetLastError = true)]
-    private static extern bool GetConsoleScreenBufferInfoEx (nint hConsoleOutput, ref CONSOLE_SCREEN_BUFFER_INFOEX csbi);
-
-    [DllImport ("kernel32.dll", SetLastError = true)]
-    private static extern bool SetConsoleScreenBufferInfoEx (nint hConsoleOutput, ref CONSOLE_SCREEN_BUFFER_INFOEX ConsoleScreenBufferInfo);
-
-    [DllImport ("kernel32.dll", SetLastError = true)]
-    private static extern bool SetConsoleWindowInfo (
-        nint hConsoleOutput,
-        bool bAbsolute,
-        [In] ref SmallRect lpConsoleWindow
-    );
-
-    [DllImport ("kernel32.dll", SetLastError = true)]
-    private static extern Coord GetLargestConsoleWindowSize (
-        nint hConsoleOutput
-    );
-}
-
-internal class WindowsDriver : ConsoleDriver
-{
-    private readonly bool _isWindowsTerminal;
-
-    private WindowsConsole.SmallRect _damageRegion;
-    private bool _isButtonDoubleClicked;
-    private bool _isButtonPressed;
-    private bool _isButtonReleased;
-    private bool _isOneFingerDoubleClicked;
-
-    private WindowsConsole.ButtonState? _lastMouseButtonPressed;
-    private WindowsMainLoop _mainLoopDriver;
-    private WindowsConsole.ExtendedCharInfo [] _outputBuffer;
-    private Point? _point;
-    private Point _pointMove;
-    private bool _processButtonClick;
-
-    public WindowsDriver ()
-    {
-        if (Environment.OSVersion.Platform == PlatformID.Win32NT)
-        {
-            WinConsole = new WindowsConsole ();
-
-            // otherwise we're probably running in unit tests
-            Clipboard = new WindowsClipboard ();
-        }
-        else
-        {
-            Clipboard = new FakeDriver.FakeClipboard ();
-        }
-
-        // TODO: if some other Windows-based terminal supports true color, update this logic to not
-        // force 16color mode (.e.g ConEmu which really doesn't work well at all).
-        _isWindowsTerminal = _isWindowsTerminal =
-                                 Environment.GetEnvironmentVariable ("WT_SESSION") is { } || Environment.GetEnvironmentVariable ("VSAPPIDNAME") != null;
-
-        if (!_isWindowsTerminal)
-        {
-            Force16Colors = true;
-        }
-    }
-
-    public override bool SupportsTrueColor => RunningUnitTests || (Environment.OSVersion.Version.Build >= 14931 && _isWindowsTerminal);
-
-    public WindowsConsole WinConsole { get; private set; }
-
-    public WindowsConsole.KeyEventRecord FromVKPacketToKeyEventRecord (WindowsConsole.KeyEventRecord keyEvent)
-    {
-        if (keyEvent.wVirtualKeyCode != (VK)ConsoleKey.Packet)
-        {
-            return keyEvent;
-        }
-
-        var mod = new ConsoleModifiers ();
-
-        if (keyEvent.dwControlKeyState.HasFlag (WindowsConsole.ControlKeyState.ShiftPressed))
-        {
-            mod |= ConsoleModifiers.Shift;
-        }
-
-        if (keyEvent.dwControlKeyState.HasFlag (WindowsConsole.ControlKeyState.RightAltPressed)
-            || keyEvent.dwControlKeyState.HasFlag (WindowsConsole.ControlKeyState.LeftAltPressed))
-        {
-            mod |= ConsoleModifiers.Alt;
-        }
-
-        if (keyEvent.dwControlKeyState.HasFlag (WindowsConsole.ControlKeyState.LeftControlPressed)
-            || keyEvent.dwControlKeyState.HasFlag (WindowsConsole.ControlKeyState.RightControlPressed))
-        {
-            mod |= ConsoleModifiers.Control;
-        }
-
-        var cKeyInfo = new ConsoleKeyInfo (
-                                           keyEvent.UnicodeChar,
-                                           (ConsoleKey)keyEvent.wVirtualKeyCode,
-                                           mod.HasFlag (ConsoleModifiers.Shift),
-                                           mod.HasFlag (ConsoleModifiers.Alt),
-                                           mod.HasFlag (ConsoleModifiers.Control));
-        cKeyInfo = DecodeVKPacketToKConsoleKeyInfo (cKeyInfo);
-        uint scanCode = GetScanCodeFromConsoleKeyInfo (cKeyInfo);
-
-        return new WindowsConsole.KeyEventRecord
-        {
-            UnicodeChar = cKeyInfo.KeyChar,
-            bKeyDown = keyEvent.bKeyDown,
-            dwControlKeyState = keyEvent.dwControlKeyState,
-            wRepeatCount = keyEvent.wRepeatCount,
-            wVirtualKeyCode = (VK)cKeyInfo.Key,
-            wVirtualScanCode = (ushort)scanCode
-        };
-    }
-
-    public override bool IsRuneSupported (Rune rune) { return base.IsRuneSupported (rune) && rune.IsBmp; }
-
-    public override void SendKeys (char keyChar, ConsoleKey key, bool shift, bool alt, bool control)
-    {
-        var input = new WindowsConsole.InputRecord
-        {
-            EventType = WindowsConsole.EventType.Key
-        };
-
-        var keyEvent = new WindowsConsole.KeyEventRecord
-        {
-            bKeyDown = true
-        };
-        var controlKey = new WindowsConsole.ControlKeyState ();
-
-        if (shift)
-        {
-            controlKey |= WindowsConsole.ControlKeyState.ShiftPressed;
-            keyEvent.UnicodeChar = '\0';
-            keyEvent.wVirtualKeyCode = VK.SHIFT;
-        }
-
-        if (alt)
-        {
-            controlKey |= WindowsConsole.ControlKeyState.LeftAltPressed;
-            controlKey |= WindowsConsole.ControlKeyState.RightAltPressed;
-            keyEvent.UnicodeChar = '\0';
-            keyEvent.wVirtualKeyCode = VK.MENU;
-        }
-
-        if (control)
-        {
-            controlKey |= WindowsConsole.ControlKeyState.LeftControlPressed;
-            controlKey |= WindowsConsole.ControlKeyState.RightControlPressed;
-            keyEvent.UnicodeChar = '\0';
-            keyEvent.wVirtualKeyCode = VK.CONTROL;
-        }
-
-        keyEvent.dwControlKeyState = controlKey;
-
-        input.KeyEvent = keyEvent;
-
-        if (shift || alt || control)
-        {
-            ProcessInput (input);
-        }
-
-        keyEvent.UnicodeChar = keyChar;
-
-        //if ((uint)key < 255) {
-        //	keyEvent.wVirtualKeyCode = (ushort)key;
-        //} else {
-        //	keyEvent.wVirtualKeyCode = '\0';
-        //}
-        keyEvent.wVirtualKeyCode = (VK)key;
-
-        input.KeyEvent = keyEvent;
-
-        try
-        {
-            ProcessInput (input);
-        }
-        catch (OverflowException)
-        { }
-        finally
-        {
-            keyEvent.bKeyDown = false;
-            input.KeyEvent = keyEvent;
-            ProcessInput (input);
-        }
-    }
-
-
-    #region Not Implemented
-
-    public override void Suspend () { throw new NotImplementedException (); }
-
-    #endregion
-
-    public WindowsConsole.ConsoleKeyInfoEx ToConsoleKeyInfoEx (WindowsConsole.KeyEventRecord keyEvent)
-    {
-        WindowsConsole.ControlKeyState state = keyEvent.dwControlKeyState;
-
-        bool shift = (state & WindowsConsole.ControlKeyState.ShiftPressed) != 0;
-        bool alt = (state & (WindowsConsole.ControlKeyState.LeftAltPressed | WindowsConsole.ControlKeyState.RightAltPressed)) != 0;
-        bool control = (state & (WindowsConsole.ControlKeyState.LeftControlPressed | WindowsConsole.ControlKeyState.RightControlPressed)) != 0;
-        bool capslock = (state & WindowsConsole.ControlKeyState.CapslockOn) != 0;
-        bool numlock = (state & WindowsConsole.ControlKeyState.NumlockOn) != 0;
-        bool scrolllock = (state & WindowsConsole.ControlKeyState.ScrolllockOn) != 0;
-
-        var cki = new ConsoleKeyInfo (keyEvent.UnicodeChar, (ConsoleKey)keyEvent.wVirtualKeyCode, shift, alt, control);
-
-        return new WindowsConsole.ConsoleKeyInfoEx (cki, capslock, numlock, scrolllock);
-    }
-
-    #region Cursor Handling
-
-    private CursorVisibility? _cachedCursorVisibility;
-
-    public override void UpdateCursor ()
-    {
-        if (Col < 0 || Row < 0 || Col >= Cols || Row >= Rows)
-        {
-            GetCursorVisibility (out CursorVisibility cursorVisibility);
-            _cachedCursorVisibility = cursorVisibility;
-            SetCursorVisibility (CursorVisibility.Invisible);
-
-            return;
-        }
-
-        var position = new WindowsConsole.Coord
-        {
-            X = (short)Col,
-            Y = (short)Row
-        };
-
-        if (Force16Colors)
-        {
-            WinConsole?.SetCursorPosition (position);
-        }
-        else
-        {
-            var sb = new StringBuilder ();
-            sb.Append (EscSeqUtils.CSI_SetCursorPosition (position.Y + 1, position.X + 1));
-            WinConsole?.WriteANSI (sb.ToString ());
-        }
-
-        if (_cachedCursorVisibility is { })
-        {
-            SetCursorVisibility (_cachedCursorVisibility.Value);
-        }
-        //EnsureCursorVisibility ();
-    }
-
-    /// <inheritdoc/>
-    public override bool GetCursorVisibility (out CursorVisibility visibility)
-    {
-        if (WinConsole is { })
-        {
-            return WinConsole.GetCursorVisibility (out visibility);
-        }
-
-        visibility = _cachedCursorVisibility ?? CursorVisibility.Default;
-
-        return true;
-    }
-
-    /// <inheritdoc/>
-    public override bool SetCursorVisibility (CursorVisibility visibility)
-    {
-        _cachedCursorVisibility = visibility;
-
-        if (Force16Colors)
-        {
-            return WinConsole is null || WinConsole.SetCursorVisibility (visibility);
-        }
-        else
-        {
-            var sb = new StringBuilder ();
-            sb.Append (visibility != CursorVisibility.Invisible ? EscSeqUtils.CSI_ShowCursor : EscSeqUtils.CSI_HideCursor);
-            return WinConsole?.WriteANSI (sb.ToString ()) ?? false;
-        }
-    }
-
-    /// <inheritdoc/>
-    public override bool EnsureCursorVisibility ()
-    {
-        if (Force16Colors)
-        {
-            return WinConsole is null || WinConsole.EnsureCursorVisibility ();
-        }
-        else
-        {
-            var sb = new StringBuilder ();
-            sb.Append (_cachedCursorVisibility != CursorVisibility.Invisible ? EscSeqUtils.CSI_ShowCursor : EscSeqUtils.CSI_HideCursor);
-            return WinConsole?.WriteANSI (sb.ToString ()) ?? false;
-        }
-
-        //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 Cursor Handling
-
-    public override bool UpdateScreen ()
-    {
-        bool updated = false;
-        Size windowSize = WinConsole?.GetConsoleBufferWindow (out Point _) ?? new Size (Cols, Rows);
-
-        if (!windowSize.IsEmpty && (windowSize.Width != Cols || windowSize.Height != Rows))
-        {
-            return updated;
-        }
-
-        var bufferCoords = new WindowsConsole.Coord
-        {
-            X = (short)Cols, //Clip.Width,
-            Y = (short)Rows, //Clip.Height
-        };
-
-        for (var row = 0; row < Rows; row++)
-        {
-            if (!_dirtyLines [row])
-            {
-                continue;
-            }
-
-            _dirtyLines [row] = false;
-            updated = true;
-
-            for (var col = 0; col < Cols; col++)
-            {
-                int position = row * Cols + col;
-                _outputBuffer [position].Attribute = Contents [row, col].Attribute.GetValueOrDefault ();
-
-                if (Contents [row, col].IsDirty == false)
-                {
-                    _outputBuffer [position].Empty = true;
-                    _outputBuffer [position].Char = (char)Rune.ReplacementChar.Value;
-
-                    continue;
-                }
-
-                _outputBuffer [position].Empty = false;
-
-                if (Contents [row, col].Rune.IsBmp)
-                {
-                    _outputBuffer [position].Char = (char)Contents [row, col].Rune.Value;
-                }
-                else
-                {
-                    //_outputBuffer [position].Empty = true;
-                    _outputBuffer [position].Char = (char)Rune.ReplacementChar.Value;
-
-                    if (Contents [row, col].Rune.GetColumns () > 1 && col + 1 < Cols)
-                    {
-                        // TODO: This is a hack to deal with non-BMP and wide characters.
-                        col++;
-                        position = row * Cols + col;
-                        _outputBuffer [position].Empty = false;
-                        _outputBuffer [position].Char = ' ';
-                    }
-                }
-            }
-        }
-
-        _damageRegion = new WindowsConsole.SmallRect
-        {
-            Top = 0,
-            Left = 0,
-            Bottom = (short)Rows,
-            Right = (short)Cols
-        };
-
-        if (!RunningUnitTests
-            && WinConsole != null
-            && !WinConsole.WriteToConsole (new (Cols, Rows), _outputBuffer, bufferCoords, _damageRegion, Force16Colors))
-        {
-            int err = Marshal.GetLastWin32Error ();
-
-            if (err != 0)
-            {
-                throw new Win32Exception (err);
-            }
-        }
-
-        WindowsConsole.SmallRect.MakeEmpty (ref _damageRegion);
-
-        return updated;
-    }
-
-    internal override void End ()
-    {
-        if (_mainLoopDriver is { })
-        {
-#if HACK_CHECK_WINCHANGED
-
-            _mainLoopDriver.WinChanged -= ChangeWin;
-#endif
-        }
-
-        _mainLoopDriver = null;
-
-        WinConsole?.Cleanup ();
-        WinConsole = null;
-
-        if (!RunningUnitTests && _isWindowsTerminal)
-        {
-            // Disable alternative screen buffer.
-            Console.Out.Write (EscSeqUtils.CSI_RestoreCursorAndRestoreAltBufferWithBackscroll);
-        }
-    }
-
-    internal override MainLoop Init ()
-    {
-        _mainLoopDriver = new WindowsMainLoop (this);
-
-        if (!RunningUnitTests)
-        {
-            try
-            {
-                if (WinConsole is { })
-                {
-                    // BUGBUG: The results from GetConsoleOutputWindow are incorrect when called from Init.
-                    // Our thread in WindowsMainLoop.CheckWin will get the correct results. See #if HACK_CHECK_WINCHANGED
-                    Size winSize = WinConsole.GetConsoleOutputWindow (out Point pos);
-                    Cols = winSize.Width;
-                    Rows = winSize.Height;
-                    OnSizeChanged (new SizeChangedEventArgs (new (Cols, Rows)));
-                }
-
-                WindowsConsole.SmallRect.MakeEmpty (ref _damageRegion);
-
-                if (_isWindowsTerminal)
-                {
-                    Console.Out.Write (EscSeqUtils.CSI_SaveCursorAndActivateAltBufferNoBackscroll);
-                }
-            }
-            catch (Win32Exception e)
-            {
-                // We are being run in an environment that does not support a console
-                // such as a unit test, or a pipe.
-                Debug.WriteLine ($"Likely running unit tests. Setting WinConsole to null so we can test it elsewhere. Exception: {e}");
-                WinConsole = null;
-            }
-        }
-
-        CurrentAttribute = new Attribute (Color.White, Color.Black);
-
-        _outputBuffer = new WindowsConsole.ExtendedCharInfo [Rows * Cols];
-        // CONCURRENCY: Unsynchronized access to Clip is not safe.
-        Clip = new (Screen);
-
-        _damageRegion = new WindowsConsole.SmallRect
-        {
-            Top = 0,
-            Left = 0,
-            Bottom = (short)Rows,
-            Right = (short)Cols
-        };
-
-        ClearContents ();
-
-#if HACK_CHECK_WINCHANGED
-        _mainLoopDriver.WinChanged = ChangeWin;
-#endif
-
-        WinConsole?.SetInitialCursorVisibility ();
-        return new MainLoop (_mainLoopDriver);
-    }
-
-    internal void ProcessInput (WindowsConsole.InputRecord inputEvent)
-    {
-        switch (inputEvent.EventType)
-        {
-            case WindowsConsole.EventType.Key:
-                if (inputEvent.KeyEvent.wVirtualKeyCode == (VK)ConsoleKey.Packet)
-                {
-                    // Used to pass Unicode characters as if they were keystrokes.
-                    // The VK_PACKET key is the low word of a 32-bit
-                    // Virtual Key value used for non-keyboard input methods.
-                    inputEvent.KeyEvent = FromVKPacketToKeyEventRecord (inputEvent.KeyEvent);
-                }
-
-                WindowsConsole.ConsoleKeyInfoEx keyInfo = ToConsoleKeyInfoEx (inputEvent.KeyEvent);
-
-                //Debug.WriteLine ($"event: KBD: {GetKeyboardLayoutName()} {inputEvent.ToString ()} {keyInfo.ToString (keyInfo)}");
-
-                KeyCode map = MapKey (keyInfo);
-
-                if (map == KeyCode.Null)
-                {
-                    break;
-                }
-
-                if (inputEvent.KeyEvent.bKeyDown)
-                {
-                    // Avoid sending repeat key down events
-                    OnKeyDown (new Key (map));
-                }
-                else
-                {
-                    OnKeyUp (new Key (map));
-                }
-
-                break;
-
-            case WindowsConsole.EventType.Mouse:
-                MouseEventArgs me = ToDriverMouse (inputEvent.MouseEvent);
-
-                if (me is null || me.Flags == MouseFlags.None)
-                {
-                    break;
-                }
-
-                OnMouseEvent (me);
-
-                if (_processButtonClick)
-                {
-                    OnMouseEvent (new ()
-                    {
-                        Position = me.Position,
-                        Flags = ProcessButtonClick (inputEvent.MouseEvent)
-                    });
-                }
-
-                break;
-
-            case WindowsConsole.EventType.Focus:
-                break;
-
-#if !HACK_CHECK_WINCHANGED
-		case WindowsConsole.EventType.WindowBufferSize:
-			
-			Cols = inputEvent.WindowBufferSizeEvent._size.X;
-			Rows = inputEvent.WindowBufferSizeEvent._size.Y;
-
-			ResizeScreen ();
-			ClearContents ();
-			TerminalResized.Invoke ();
-			break;
-#endif
-        }
-    }
-
-#if HACK_CHECK_WINCHANGED
-    private void ChangeWin (object s, SizeChangedEventArgs e)
-    {
-        if (e.Size is null)
-        {
-            return;
-        }
-
-        int w = e.Size.Value.Width;
-
-        if (w == Cols - 3 && e.Size.Value.Height < Rows)
-        {
-            w += 3;
-        }
-
-        Left = 0;
-        Top = 0;
-        Cols = e.Size.Value.Width;
-        Rows = e.Size.Value.Height;
-
-        if (!RunningUnitTests)
-        {
-            Size newSize = WinConsole.SetConsoleWindow (
-                                                        (short)Math.Max (w, 16),
-                                                        (short)Math.Max (e.Size.Value.Height, 0));
-
-            Cols = newSize.Width;
-            Rows = newSize.Height;
-        }
-
-        ResizeScreen ();
-        ClearContents ();
-        OnSizeChanged (new SizeChangedEventArgs (new (Cols, Rows)));
-    }
-#endif
-
-    private KeyCode MapKey (WindowsConsole.ConsoleKeyInfoEx keyInfoEx)
-    {
-        ConsoleKeyInfo keyInfo = keyInfoEx.ConsoleKeyInfo;
-
-        switch (keyInfo.Key)
-        {
-            case ConsoleKey.D0:
-            case ConsoleKey.D1:
-            case ConsoleKey.D2:
-            case ConsoleKey.D3:
-            case ConsoleKey.D4:
-            case ConsoleKey.D5:
-            case ConsoleKey.D6:
-            case ConsoleKey.D7:
-            case ConsoleKey.D8:
-            case ConsoleKey.D9:
-            case ConsoleKey.NumPad0:
-            case ConsoleKey.NumPad1:
-            case ConsoleKey.NumPad2:
-            case ConsoleKey.NumPad3:
-            case ConsoleKey.NumPad4:
-            case ConsoleKey.NumPad5:
-            case ConsoleKey.NumPad6:
-            case ConsoleKey.NumPad7:
-            case ConsoleKey.NumPad8:
-            case ConsoleKey.NumPad9:
-            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.Multiply:
-            case ConsoleKey.Add:
-            case ConsoleKey.Separator:
-            case ConsoleKey.Subtract:
-            case ConsoleKey.Decimal:
-            case ConsoleKey.Divide:
-            case ConsoleKey.OemPeriod:
-            case ConsoleKey.OemComma:
-            case ConsoleKey.OemPlus:
-            case ConsoleKey.OemMinus:
-                // These virtual key codes are mapped differently depending on the keyboard layout in use.
-                // We use the Win32 API to map them to the correct character.
-                uint mapResult = MapVKtoChar ((VK)keyInfo.Key);
-
-                if (mapResult == 0)
-                {
-                    // There is no mapping - this should not happen
-                    Debug.Assert (mapResult != 0, $@"Unable to map the virtual key code {keyInfo.Key}.");
-
-                    return KeyCode.Null;
-                }
-
-                // An un-shifted character value is in the low order word of the return value.
-                var mappedChar = (char)(mapResult & 0x0000FFFF);
-
-                if (keyInfo.KeyChar == 0)
-                {
-                    // If the keyChar is 0, keyInfo.Key value is not a printable character. 
-
-                    // Dead keys (diacritics) are indicated by setting the top bit of the return value. 
-                    if ((mapResult & 0x80000000) != 0)
-                    {
-                        // Dead key (e.g. Oem2 '~'/'^' on POR keyboard)
-                        // Option 1: Throw it out. 
-                        //    - Apps will never see the dead keys
-                        //    - If user presses a key that can be combined with the dead key ('a'), the right thing happens (app will see '�').
-                        //      - NOTE: With Dead Keys, KeyDown != KeyUp. The KeyUp event will have just the base char ('a').
-                        //    - If user presses dead key again, the right thing happens (app will see `~~`)
-                        //    - This is what Notepad etc... appear to do
-                        // Option 2: Expand the API to indicate the KeyCode is a dead key
-                        //    - Enables apps to do their own dead key processing
-                        //    - Adds complexity; no dev has asked for this (yet).
-                        // We choose Option 1 for now.
-                        return KeyCode.Null;
-
-                        // Note: Ctrl-Deadkey (like Oem3 '`'/'~` on ENG) can't be supported.
-                        // Sadly, the charVal is just the deadkey and subsequent key events do not contain
-                        // any info that the previous event was a deadkey.
-                        // Note WT does not support Ctrl-Deadkey either.
-                    }
-
-                    if (keyInfo.Modifiers != 0)
-                    {
-                        // These Oem keys have well-defined chars. We ensure the representative char is used.
-                        // If we don't do this, then on some keyboard layouts the wrong char is 
-                        // returned (e.g. on ENG OemPlus un-shifted is =, not +). This is important
-                        // for key persistence ("Ctrl++" vs. "Ctrl+=").
-                        mappedChar = keyInfo.Key switch
-                        {
-                            ConsoleKey.OemPeriod => '.',
-                            ConsoleKey.OemComma => ',',
-                            ConsoleKey.OemPlus => '+',
-                            ConsoleKey.OemMinus => '-',
-                            _ => mappedChar
-                        };
-                    }
-
-                    // Return the mappedChar with modifiers. Because mappedChar is un-shifted, if Shift was down
-                    // we should keep it
-                    return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)mappedChar);
-                }
-
-                // KeyChar is printable
-                if (keyInfo.Modifiers.HasFlag (ConsoleModifiers.Alt) && keyInfo.Modifiers.HasFlag (ConsoleModifiers.Control))
-                {
-                    // AltGr support - AltGr is equivalent to Ctrl+Alt - the correct char is in KeyChar
-                    return (KeyCode)keyInfo.KeyChar;
-                }
-
-                if (keyInfo.Modifiers != ConsoleModifiers.Shift)
-                {
-                    // If Shift wasn't down we don't need to do anything but return the mappedChar
-                    return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)mappedChar);
-                }
-
-                // 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);
-        }
-
-        // 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.KeyChar == 0)
-            {
-                // KeyChar is not printable - possibly an AltGr key?
-                // AltGr support - AltGr is equivalent to Ctrl+Alt
-                if (keyInfo.Modifiers.HasFlag (ConsoleModifiers.Alt) && keyInfo.Modifiers.HasFlag (ConsoleModifiers.Control))
-                {
-                    return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)(uint)keyInfo.Key);
-                }
-            }
-
-            if (keyInfo.Modifiers.HasFlag (ConsoleModifiers.Alt) || keyInfo.Modifiers.HasFlag (ConsoleModifiers.Control))
-            {
-                return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)(uint)keyInfo.Key);
-            }
-
-            if ((keyInfo.Modifiers == ConsoleModifiers.Shift) ^ keyInfoEx.CapsLock)
-            {
-                // If (ShiftMask is on and CapsLock is off) or (ShiftMask is off and CapsLock is on) add the ShiftMask
-                if (char.IsUpper (keyInfo.KeyChar))
-                {
-                    return (KeyCode)(uint)keyInfo.Key | KeyCode.ShiftMask;
-                }
-            }
-
-            // If KeyInfo.Key is A...Z and KeyInfo.KeyChar is not a...z then we have an accent.
-            // See https://github.com/gui-cs/Terminal.Gui/issues/3807#issuecomment-2455997595
-            if (keyInfo.KeyChar <= (char)'z')
-            {
-                return (KeyCode)keyInfo.Key;
-            }
-            return (KeyCode)keyInfo.KeyChar;
-        }
-
-        // Handle control keys whose VK codes match the related ASCII value (those below ASCII 33) like ESC
-        if (Enum.IsDefined (typeof (KeyCode), (uint)keyInfo.Key))
-        {
-            // If the key is JUST a modifier, return it as just that key
-            if (keyInfo.Key == (ConsoleKey)VK.SHIFT)
-            { // Shift 16
-                return KeyCode.ShiftMask;
-            }
-
-            if (keyInfo.Key == (ConsoleKey)VK.CONTROL)
-            { // Ctrl 17
-                return KeyCode.CtrlMask;
-            }
-
-            if (keyInfo.Key == (ConsoleKey)VK.MENU)
-            { // Alt 18
-                return KeyCode.AltMask;
-            }
-
-            if (keyInfo.KeyChar == 0)
-            {
-                return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)keyInfo.KeyChar);
-            }
-
-            if (keyInfo.Key != ConsoleKey.None)
-            {
-                return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)keyInfo.KeyChar);
-            }
-
-            return MapToKeyCodeModifiers (keyInfo.Modifiers & ~ConsoleModifiers.Shift, (KeyCode)keyInfo.KeyChar);
-        }
-
-        // Handle control keys (e.g. CursorUp)
-        if (Enum.IsDefined (typeof (KeyCode), (uint)keyInfo.Key + (uint)KeyCode.MaxCodePoint))
-        {
-            return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)((uint)keyInfo.Key + (uint)KeyCode.MaxCodePoint));
-        }
-
-        return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)keyInfo.KeyChar);
-    }
-
-    private MouseFlags ProcessButtonClick (WindowsConsole.MouseEventRecord mouseEvent)
-    {
-        MouseFlags mouseFlag = 0;
-
-        switch (_lastMouseButtonPressed)
-        {
-            case WindowsConsole.ButtonState.Button1Pressed:
-                mouseFlag = MouseFlags.Button1Clicked;
-
-                break;
-
-            case WindowsConsole.ButtonState.Button2Pressed:
-                mouseFlag = MouseFlags.Button2Clicked;
-
-                break;
-
-            case WindowsConsole.ButtonState.RightmostButtonPressed:
-                mouseFlag = MouseFlags.Button3Clicked;
-
-                break;
-        }
-
-        _point = new Point
-        {
-            X = mouseEvent.MousePosition.X,
-            Y = mouseEvent.MousePosition.Y
-        };
-        _lastMouseButtonPressed = null;
-        _isButtonReleased = false;
-        _processButtonClick = false;
-        _point = null;
-
-        return mouseFlag;
-    }
-
-    private async Task ProcessButtonDoubleClickedAsync ()
-    {
-        await Task.Delay (200);
-        _isButtonDoubleClicked = false;
-        _isOneFingerDoubleClicked = false;
-
-        //buttonPressedCount = 0;
-    }
-
-    private async Task ProcessContinuousButtonPressedAsync (MouseFlags mouseFlag)
-    {
-        // When a user presses-and-holds, start generating pressed events every `startDelay`
-        // After `iterationsUntilFast` iterations, speed them up to `fastDelay` ms
-        const int startDelay = 500;
-        const int iterationsUntilFast = 4;
-        const int fastDelay = 50;
-
-        int iterations = 0;
-        int delay = startDelay;
-        while (_isButtonPressed)
-        {
-            // TODO: This makes ConsoleDriver dependent on Application, which is not ideal. This should be moved to Application.
-            View view = Application.WantContinuousButtonPressedView;
-
-            if (view is null)
-            {
-                break;
-            }
-
-            if (iterations++ >= iterationsUntilFast)
-            {
-                delay = fastDelay;
-            }
-            await Task.Delay (delay);
-
-            var me = new MouseEventArgs
-            {
-                ScreenPosition = _pointMove,
-                Flags = mouseFlag
-            };
-
-            //Debug.WriteLine($"ProcessContinuousButtonPressedAsync: {view}");
-            if (_isButtonPressed && (mouseFlag & MouseFlags.ReportMousePosition) == 0)
-            {
-                // TODO: This makes ConsoleDriver dependent on Application, which is not ideal. This should be moved to Application.
-                Application.Invoke (() => OnMouseEvent (me));
-            }
-        }
-    }
-
-    private void ResizeScreen ()
-    {
-        _outputBuffer = new WindowsConsole.ExtendedCharInfo [Rows * Cols];
-        // CONCURRENCY: Unsynchronized access to Clip is not safe.
-        Clip = new (Screen);
-
-        _damageRegion = new WindowsConsole.SmallRect
-        {
-            Top = 0,
-            Left = 0,
-            Bottom = (short)Rows,
-            Right = (short)Cols
-        };
-        _dirtyLines = new bool [Rows];
-
-        WinConsole?.ForceRefreshCursorVisibility ();
-    }
-
-    private static MouseFlags SetControlKeyStates (WindowsConsole.MouseEventRecord mouseEvent, MouseFlags mouseFlag)
-    {
-        if (mouseEvent.ControlKeyState.HasFlag (WindowsConsole.ControlKeyState.RightControlPressed)
-            || mouseEvent.ControlKeyState.HasFlag (WindowsConsole.ControlKeyState.LeftControlPressed))
-        {
-            mouseFlag |= MouseFlags.ButtonCtrl;
-        }
-
-        if (mouseEvent.ControlKeyState.HasFlag (WindowsConsole.ControlKeyState.ShiftPressed))
-        {
-            mouseFlag |= MouseFlags.ButtonShift;
-        }
-
-        if (mouseEvent.ControlKeyState.HasFlag (WindowsConsole.ControlKeyState.RightAltPressed)
-            || mouseEvent.ControlKeyState.HasFlag (WindowsConsole.ControlKeyState.LeftAltPressed))
-        {
-            mouseFlag |= MouseFlags.ButtonAlt;
-        }
-
-        return mouseFlag;
-    }
-
-    [CanBeNull]
-    private MouseEventArgs ToDriverMouse (WindowsConsole.MouseEventRecord mouseEvent)
-    {
-        var mouseFlag = MouseFlags.AllEvents;
-
-        //Debug.WriteLine ($"ToDriverMouse: {mouseEvent}");
-
-        if (_isButtonDoubleClicked || _isOneFingerDoubleClicked)
-        {
-            // TODO: This makes ConsoleDriver dependent on Application, which is not ideal. This should be moved to Application.
-            Application.MainLoop.AddIdle (
-                                          () =>
-                                          {
-                                              Task.Run (async () => await ProcessButtonDoubleClickedAsync ());
-
-                                              return false;
-                                          });
-        }
-
-        // The ButtonState member of the MouseEvent structure has bit corresponding to each mouse button.
-        // This will tell when a mouse button is pressed. When the button is released this event will
-        // be fired with its bit set to 0. So when the button is up ButtonState will be 0.
-        // To map to the correct driver events we save the last pressed mouse button, so we can
-        // map to the correct clicked event.
-        if ((_lastMouseButtonPressed is { } || _isButtonReleased) && mouseEvent.ButtonState != 0)
-        {
-            _lastMouseButtonPressed = null;
-
-            //isButtonPressed = false;
-            _isButtonReleased = false;
-        }
-
-        var p = new Point
-        {
-            X = mouseEvent.MousePosition.X,
-            Y = mouseEvent.MousePosition.Y
-        };
-
-        if ((mouseEvent.ButtonState != 0 && mouseEvent.EventFlags == 0 && _lastMouseButtonPressed is null && !_isButtonDoubleClicked)
-            || (_lastMouseButtonPressed == null
-                && mouseEvent.EventFlags.HasFlag (WindowsConsole.EventFlags.MouseMoved)
-                && mouseEvent.ButtonState != 0
-                && !_isButtonReleased
-                && !_isButtonDoubleClicked))
-        {
-            switch (mouseEvent.ButtonState)
-            {
-                case WindowsConsole.ButtonState.Button1Pressed:
-                    mouseFlag = MouseFlags.Button1Pressed;
-
-                    break;
-
-                case WindowsConsole.ButtonState.Button2Pressed:
-                    mouseFlag = MouseFlags.Button2Pressed;
-
-                    break;
-
-                case WindowsConsole.ButtonState.RightmostButtonPressed:
-                    mouseFlag = MouseFlags.Button3Pressed;
-
-                    break;
-            }
-
-            if (_point is null)
-            {
-                _point = p;
-            }
-
-            if (mouseEvent.EventFlags.HasFlag (WindowsConsole.EventFlags.MouseMoved))
-            {
-                _pointMove = p;
-                mouseFlag |= MouseFlags.ReportMousePosition;
-                _isButtonReleased = false;
-                _processButtonClick = false;
-            }
-
-            _lastMouseButtonPressed = mouseEvent.ButtonState;
-            _isButtonPressed = true;
-
-            if ((mouseFlag & MouseFlags.ReportMousePosition) == 0)
-            {
-                // TODO: This makes ConsoleDriver dependent on Application, which is not ideal. This should be moved to Application.
-                Application.MainLoop.AddIdle (
-                                              () =>
-                                              {
-                                                  Task.Run (async () => await ProcessContinuousButtonPressedAsync (mouseFlag));
-
-                                                  return false;
-                                              });
-            }
-        }
-        else if (_lastMouseButtonPressed != null
-                 && mouseEvent.EventFlags == 0
-                 && !_isButtonReleased
-                 && !_isButtonDoubleClicked
-                 && !_isOneFingerDoubleClicked)
-        {
-            switch (_lastMouseButtonPressed)
-            {
-                case WindowsConsole.ButtonState.Button1Pressed:
-                    mouseFlag = MouseFlags.Button1Released;
-
-                    break;
-
-                case WindowsConsole.ButtonState.Button2Pressed:
-                    mouseFlag = MouseFlags.Button2Released;
-
-                    break;
-
-                case WindowsConsole.ButtonState.RightmostButtonPressed:
-                    mouseFlag = MouseFlags.Button3Released;
-
-                    break;
-            }
-
-            _isButtonPressed = false;
-            _isButtonReleased = true;
-
-            if (_point is { } && ((Point)_point).X == mouseEvent.MousePosition.X && ((Point)_point).Y == mouseEvent.MousePosition.Y)
-            {
-                _processButtonClick = true;
-            }
-            else
-            {
-                _point = null;
-            }
-            _processButtonClick = true;
-
-        }
-        else if (mouseEvent.EventFlags == WindowsConsole.EventFlags.MouseMoved
-                 && !_isOneFingerDoubleClicked
-                 && _isButtonReleased
-                 && p == _point)
-        {
-            mouseFlag = ProcessButtonClick (mouseEvent);
-        }
-        else if (mouseEvent.EventFlags.HasFlag (WindowsConsole.EventFlags.DoubleClick))
-        {
-            switch (mouseEvent.ButtonState)
-            {
-                case WindowsConsole.ButtonState.Button1Pressed:
-                    mouseFlag = MouseFlags.Button1DoubleClicked;
-
-                    break;
-
-                case WindowsConsole.ButtonState.Button2Pressed:
-                    mouseFlag = MouseFlags.Button2DoubleClicked;
-
-                    break;
-
-                case WindowsConsole.ButtonState.RightmostButtonPressed:
-                    mouseFlag = MouseFlags.Button3DoubleClicked;
-
-                    break;
-            }
-
-            _isButtonDoubleClicked = true;
-        }
-        else if (mouseEvent.EventFlags == 0 && mouseEvent.ButtonState != 0 && _isButtonDoubleClicked)
-        {
-            switch (mouseEvent.ButtonState)
-            {
-                case WindowsConsole.ButtonState.Button1Pressed:
-                    mouseFlag = MouseFlags.Button1TripleClicked;
-
-                    break;
-
-                case WindowsConsole.ButtonState.Button2Pressed:
-                    mouseFlag = MouseFlags.Button2TripleClicked;
-
-                    break;
-
-                case WindowsConsole.ButtonState.RightmostButtonPressed:
-                    mouseFlag = MouseFlags.Button3TripleClicked;
-
-                    break;
-            }
-
-            _isButtonDoubleClicked = false;
-        }
-        else if (mouseEvent.EventFlags == WindowsConsole.EventFlags.MouseWheeled)
-        {
-            switch ((int)mouseEvent.ButtonState)
-            {
-                case int v when v > 0:
-                    mouseFlag = MouseFlags.WheeledUp;
-
-                    break;
-
-                case int v when v < 0:
-                    mouseFlag = MouseFlags.WheeledDown;
-
-                    break;
-            }
-        }
-        else if (mouseEvent.EventFlags == WindowsConsole.EventFlags.MouseWheeled && mouseEvent.ControlKeyState == WindowsConsole.ControlKeyState.ShiftPressed)
-        {
-            switch ((int)mouseEvent.ButtonState)
-            {
-                case int v when v > 0:
-                    mouseFlag = MouseFlags.WheeledLeft;
-
-                    break;
-
-                case int v when v < 0:
-                    mouseFlag = MouseFlags.WheeledRight;
-
-                    break;
-            }
-        }
-        else if (mouseEvent.EventFlags == WindowsConsole.EventFlags.MouseHorizontalWheeled)
-        {
-            switch ((int)mouseEvent.ButtonState)
-            {
-                case int v when v < 0:
-                    mouseFlag = MouseFlags.WheeledLeft;
-
-                    break;
-
-                case int v when v > 0:
-                    mouseFlag = MouseFlags.WheeledRight;
-
-                    break;
-            }
-        }
-        else if (mouseEvent.EventFlags == WindowsConsole.EventFlags.MouseMoved)
-        {
-            mouseFlag = MouseFlags.ReportMousePosition;
-
-            if (mouseEvent.MousePosition.X != _pointMove.X || mouseEvent.MousePosition.Y != _pointMove.Y)
-            {
-                _pointMove = new Point (mouseEvent.MousePosition.X, mouseEvent.MousePosition.Y);
-            }
-        }
-        else if (mouseEvent is { ButtonState: 0, EventFlags: 0 })
-        {
-            // This happens on a double or triple click event.
-            mouseFlag = MouseFlags.None;
-        }
-
-        mouseFlag = SetControlKeyStates (mouseEvent, mouseFlag);
-
-        //System.Diagnostics.Debug.WriteLine (
-        //	$"point.X:{(point is { } ? ((Point)point).X : -1)};point.Y:{(point is { } ? ((Point)point).Y : -1)}");
-
-        return new MouseEventArgs
-        {
-            Position = new (mouseEvent.MousePosition.X, mouseEvent.MousePosition.Y),
-            Flags = mouseFlag
-        };
-    }
-}
-
-/// <summary>
-///     Mainloop intended to be used with the <see cref="WindowsDriver"/>, and can
-///     only be used on Windows.
-/// </summary>
-/// <remarks>
-///     This implementation is used for WindowsDriver.
-/// </remarks>
-internal class WindowsMainLoop : IMainLoopDriver
-{
-    /// <summary>
-    ///     Invoked when the window is changed.
-    /// </summary>
-    public EventHandler<SizeChangedEventArgs> WinChanged;
-
-    private readonly ConsoleDriver _consoleDriver;
-    private readonly ManualResetEventSlim _eventReady = new (false);
-
-    // The records that we keep fetching
-    private readonly Queue<WindowsConsole.InputRecord []> _resultQueue = new ();
-    private readonly ManualResetEventSlim _waitForProbe = new (false);
-    private readonly WindowsConsole _winConsole;
-    private CancellationTokenSource _eventReadyTokenSource = new ();
-    private readonly CancellationTokenSource _inputHandlerTokenSource = new ();
-    private MainLoop _mainLoop;
-
-    public WindowsMainLoop (ConsoleDriver consoleDriver = null)
-    {
-        _consoleDriver = consoleDriver ?? throw new ArgumentNullException (nameof (consoleDriver));
-        _winConsole = ((WindowsDriver)consoleDriver).WinConsole;
-    }
-
-    void IMainLoopDriver.Setup (MainLoop mainLoop)
-    {
-        _mainLoop = mainLoop;
-        Task.Run (WindowsInputHandler, _inputHandlerTokenSource.Token);
-#if HACK_CHECK_WINCHANGED
-        Task.Run (CheckWinChange);
-#endif
-    }
-
-    void IMainLoopDriver.Wakeup () { _eventReady.Set (); }
-
-    bool IMainLoopDriver.EventsPending ()
-    {
-        _waitForProbe.Set ();
-#if HACK_CHECK_WINCHANGED
-        _winChange.Set ();
-#endif
-        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);
-                //
-            }
-            _eventReady.Reset ();
-        }
-        catch (OperationCanceledException)
-        {
-            _eventReady.Reset ();
-            return true;
-        }
-        finally
-        {
-            //_eventReady.Reset ();
-        }
-
-        if (!_eventReadyTokenSource.IsCancellationRequested)
-        {
-#if HACK_CHECK_WINCHANGED
-            return _resultQueue.Count > 0 || _mainLoop.CheckTimersAndIdleHandlers (out _) || _winChanged;
-#else
-			return _resultQueue.Count > 0 || _mainLoop.CheckTimersAndIdleHandlers (out _);
-#endif
-        }
-
-        _eventReadyTokenSource.Dispose ();
-        _eventReadyTokenSource = new CancellationTokenSource ();
-
-        return true;
-    }
-
-    void IMainLoopDriver.Iteration ()
-    {
-        while (_resultQueue.Count > 0)
-        {
-            WindowsConsole.InputRecord [] inputRecords = _resultQueue.Dequeue ();
-
-            if (inputRecords is { Length: > 0 })
-            {
-                ((WindowsDriver)_consoleDriver).ProcessInput (inputRecords [0]);
-            }
-        }
-#if HACK_CHECK_WINCHANGED
-        if (_winChanged)
-        {
-            _winChanged = false;
-            WinChanged?.Invoke (this, new SizeChangedEventArgs (_windowSize));
-        }
-#endif
-    }
-
-    void IMainLoopDriver.TearDown ()
-    {
-        _inputHandlerTokenSource?.Cancel ();
-        _inputHandlerTokenSource?.Dispose ();
-
-        if (_winConsole is { })
-        {
-            var numOfEvents = _winConsole.GetNumberOfConsoleInputEvents ();
-
-            if (numOfEvents > 0)
-            {
-                _winConsole.FlushConsoleInputBuffer ();
-                //Debug.WriteLine ($"Flushed {numOfEvents} events.");
-            }
-        }
-
-        _waitForProbe?.Dispose ();
-
-        _resultQueue?.Clear ();
-
-        _eventReadyTokenSource?.Cancel ();
-        _eventReadyTokenSource?.Dispose ();
-        _eventReady?.Dispose ();
-
-#if HACK_CHECK_WINCHANGED
-        _winChange?.Dispose ();
-#endif
-
-        _mainLoop = null;
-    }
-
-    private void WindowsInputHandler ()
-    {
-        while (_mainLoop is { })
-        {
-            try
-            {
-                if (!_inputHandlerTokenSource.IsCancellationRequested)
-                {
-                    _waitForProbe.Wait (_inputHandlerTokenSource.Token);
-                }
-            }
-            catch (OperationCanceledException)
-            {
-                // Wakes the _waitForProbe if it's waiting
-                _waitForProbe.Set ();
-                return;
-            }
-            finally
-            {
-                // If IsCancellationRequested is true the code after
-                // the `finally` block will not be executed.
-                if (!_inputHandlerTokenSource.IsCancellationRequested)
-                {
-                    _waitForProbe.Reset ();
-                }
-            }
-
-            if (_resultQueue?.Count == 0)
-            {
-                var input = _winConsole.ReadConsoleInput ();
-
-                //if (input [0].EventType != WindowsConsole.EventType.Focus)
-                {
-                    _resultQueue.Enqueue (input);
-                }
-            }
-
-            if (_resultQueue?.Count > 0)
-            {
-                _eventReady.Set ();
-            }
-        }
-    }
-
-#if HACK_CHECK_WINCHANGED
-    private readonly ManualResetEventSlim _winChange = new (false);
-    private bool _winChanged;
-    private Size _windowSize;
-    private void CheckWinChange ()
-    {
-        while (_mainLoop is { })
-        {
-            _winChange.Wait ();
-            _winChange.Reset ();
-
-            // Check if the window size changed every half second. 
-            // We do this to minimize the weird tearing seen on Windows when resizing the console
-            while (_mainLoop is { })
-            {
-                Task.Delay (500).Wait ();
-                _windowSize = _winConsole.GetConsoleBufferWindow (out _);
-
-                if (_windowSize != Size.Empty
-                    && (_windowSize.Width != _consoleDriver.Cols
-                        || _windowSize.Height != _consoleDriver.Rows))
-                {
-                    break;
-                }
-            }
-
-            _winChanged = true;
-            _eventReady.Set ();
-        }
-    }
-#endif
-}
-
-internal class WindowsClipboard : ClipboardBase
-{
-    private const uint CF_UNICODE_TEXT = 13;
-
-    public override bool IsSupported { get; } = CheckClipboardIsAvailable ();
-
-    private static bool CheckClipboardIsAvailable ()
-    {
-        // Attempt to open the clipboard
-        if (OpenClipboard (nint.Zero))
-        {
-            // Clipboard is available
-            // Close the clipboard after use
-            CloseClipboard ();
-
-            return true;
-        }
-        // Clipboard is not available
-        return false;
-    }
-
-    protected override string GetClipboardDataImpl ()
-    {
-        try
-        {
-            if (!OpenClipboard (nint.Zero))
-            {
-                return string.Empty;
-            }
-
-            nint handle = GetClipboardData (CF_UNICODE_TEXT);
-
-            if (handle == nint.Zero)
-            {
-                return string.Empty;
-            }
-
-            nint pointer = nint.Zero;
-
-            try
-            {
-                pointer = GlobalLock (handle);
-
-                if (pointer == nint.Zero)
-                {
-                    return string.Empty;
-                }
-
-                int size = GlobalSize (handle);
-                var buff = new byte [size];
-
-                Marshal.Copy (pointer, buff, 0, size);
-
-                return Encoding.Unicode.GetString (buff).TrimEnd ('\0');
-            }
-            finally
-            {
-                if (pointer != nint.Zero)
-                {
-                    GlobalUnlock (handle);
-                }
-            }
-        }
-        finally
-        {
-            CloseClipboard ();
-        }
-    }
-
-    protected override void SetClipboardDataImpl (string text)
-    {
-        OpenClipboard ();
-
-        EmptyClipboard ();
-        nint hGlobal = default;
-
-        try
-        {
-            int bytes = (text.Length + 1) * 2;
-            hGlobal = Marshal.AllocHGlobal (bytes);
-
-            if (hGlobal == default (nint))
-            {
-                ThrowWin32 ();
-            }
-
-            nint target = GlobalLock (hGlobal);
-
-            if (target == default (nint))
-            {
-                ThrowWin32 ();
-            }
-
-            try
-            {
-                Marshal.Copy (text.ToCharArray (), 0, target, text.Length);
-            }
-            finally
-            {
-                GlobalUnlock (target);
-            }
-
-            if (SetClipboardData (CF_UNICODE_TEXT, hGlobal) == default (nint))
-            {
-                ThrowWin32 ();
-            }
-
-            hGlobal = default (nint);
-        }
-        finally
-        {
-            if (hGlobal != default (nint))
-            {
-                Marshal.FreeHGlobal (hGlobal);
-            }
-
-            CloseClipboard ();
-        }
-    }
-
-    [DllImport ("user32.dll", SetLastError = true)]
-    [return: MarshalAs (UnmanagedType.Bool)]
-    private static extern bool CloseClipboard ();
-
-    [DllImport ("user32.dll")]
-    private static extern bool EmptyClipboard ();
-
-    [DllImport ("user32.dll", SetLastError = true)]
-    private static extern nint GetClipboardData (uint uFormat);
-
-    [DllImport ("kernel32.dll", SetLastError = true)]
-    private static extern nint GlobalLock (nint hMem);
-
-    [DllImport ("kernel32.dll", SetLastError = true)]
-    private static extern int GlobalSize (nint handle);
-
-    [DllImport ("kernel32.dll", SetLastError = true)]
-    [return: MarshalAs (UnmanagedType.Bool)]
-    private static extern bool GlobalUnlock (nint hMem);
-
-    [DllImport ("User32.dll", SetLastError = true)]
-    [return: MarshalAs (UnmanagedType.Bool)]
-    private static extern bool IsClipboardFormatAvailable (uint format);
-
-    private void OpenClipboard ()
-    {
-        var num = 10;
-
-        while (true)
-        {
-            if (OpenClipboard (default (nint)))
-            {
-                break;
-            }
-
-            if (--num == 0)
-            {
-                ThrowWin32 ();
-            }
-
-            Thread.Sleep (100);
-        }
-    }
-
-    [DllImport ("user32.dll", SetLastError = true)]
-    [return: MarshalAs (UnmanagedType.Bool)]
-    private static extern bool OpenClipboard (nint hWndNewOwner);
-
-    [DllImport ("user32.dll", SetLastError = true)]
-    private static extern nint SetClipboardData (uint uFormat, nint data);
-
-    private void ThrowWin32 () { throw new Win32Exception (Marshal.GetLastWin32Error ()); }
-}

+ 179 - 0
Terminal.Gui/ConsoleDrivers/WindowsDriver/ClipboardImpl.cs

@@ -0,0 +1,179 @@
+#nullable enable
+using System.ComponentModel;
+using System.Runtime.InteropServices;
+
+namespace Terminal.Gui;
+
+internal class WindowsClipboard : ClipboardBase
+{
+    private const uint CF_UNICODE_TEXT = 13;
+
+    public override bool IsSupported { get; } = CheckClipboardIsAvailable ();
+
+    private static bool CheckClipboardIsAvailable ()
+    {
+        // Attempt to open the clipboard
+        if (OpenClipboard (nint.Zero))
+        {
+            // Clipboard is available
+            // Close the clipboard after use
+            CloseClipboard ();
+
+            return true;
+        }
+        // Clipboard is not available
+        return false;
+    }
+
+    protected override string GetClipboardDataImpl ()
+    {
+        try
+        {
+            if (!OpenClipboard (nint.Zero))
+            {
+                return string.Empty;
+            }
+
+            nint handle = GetClipboardData (CF_UNICODE_TEXT);
+
+            if (handle == nint.Zero)
+            {
+                return string.Empty;
+            }
+
+            nint pointer = nint.Zero;
+
+            try
+            {
+                pointer = GlobalLock (handle);
+
+                if (pointer == nint.Zero)
+                {
+                    return string.Empty;
+                }
+
+                int size = GlobalSize (handle);
+                var buff = new byte [size];
+
+                Marshal.Copy (pointer, buff, 0, size);
+
+                return Encoding.Unicode.GetString (buff).TrimEnd ('\0');
+            }
+            finally
+            {
+                if (pointer != nint.Zero)
+                {
+                    GlobalUnlock (handle);
+                }
+            }
+        }
+        finally
+        {
+            CloseClipboard ();
+        }
+    }
+
+    protected override void SetClipboardDataImpl (string text)
+    {
+        OpenClipboard ();
+
+        EmptyClipboard ();
+        nint hGlobal = default;
+
+        try
+        {
+            int bytes = (text.Length + 1) * 2;
+            hGlobal = Marshal.AllocHGlobal (bytes);
+
+            if (hGlobal == default (nint))
+            {
+                ThrowWin32 ();
+            }
+
+            nint target = GlobalLock (hGlobal);
+
+            if (target == default (nint))
+            {
+                ThrowWin32 ();
+            }
+
+            try
+            {
+                Marshal.Copy (text.ToCharArray (), 0, target, text.Length);
+            }
+            finally
+            {
+                GlobalUnlock (target);
+            }
+
+            if (SetClipboardData (CF_UNICODE_TEXT, hGlobal) == default (nint))
+            {
+                ThrowWin32 ();
+            }
+
+            hGlobal = default (nint);
+        }
+        finally
+        {
+            if (hGlobal != default (nint))
+            {
+                Marshal.FreeHGlobal (hGlobal);
+            }
+
+            CloseClipboard ();
+        }
+    }
+
+    [DllImport ("user32.dll", SetLastError = true)]
+    [return: MarshalAs (UnmanagedType.Bool)]
+    private static extern bool CloseClipboard ();
+
+    [DllImport ("user32.dll")]
+    private static extern bool EmptyClipboard ();
+
+    [DllImport ("user32.dll", SetLastError = true)]
+    private static extern nint GetClipboardData (uint uFormat);
+
+    [DllImport ("kernel32.dll", SetLastError = true)]
+    private static extern nint GlobalLock (nint hMem);
+
+    [DllImport ("kernel32.dll", SetLastError = true)]
+    private static extern int GlobalSize (nint handle);
+
+    [DllImport ("kernel32.dll", SetLastError = true)]
+    [return: MarshalAs (UnmanagedType.Bool)]
+    private static extern bool GlobalUnlock (nint hMem);
+
+    [DllImport ("User32.dll", SetLastError = true)]
+    [return: MarshalAs (UnmanagedType.Bool)]
+    private static extern bool IsClipboardFormatAvailable (uint format);
+
+    private void OpenClipboard ()
+    {
+        var num = 10;
+
+        while (true)
+        {
+            if (OpenClipboard (default (nint)))
+            {
+                break;
+            }
+
+            if (--num == 0)
+            {
+                ThrowWin32 ();
+            }
+
+            Thread.Sleep (100);
+        }
+    }
+
+    [DllImport ("user32.dll", SetLastError = true)]
+    [return: MarshalAs (UnmanagedType.Bool)]
+    private static extern bool OpenClipboard (nint hWndNewOwner);
+
+    [DllImport ("user32.dll", SetLastError = true)]
+    private static extern nint SetClipboardData (uint uFormat, nint data);
+
+    private void ThrowWin32 () { throw new Win32Exception (Marshal.GetLastWin32Error ()); }
+}

+ 1094 - 0
Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsConsole.cs

@@ -0,0 +1,1094 @@
+#nullable enable
+using System.Collections.Concurrent;
+using System.ComponentModel;
+using System.Runtime.InteropServices;
+using Terminal.Gui.ConsoleDrivers;
+
+namespace Terminal.Gui;
+
+internal class WindowsConsole
+{
+    private CancellationTokenSource? _inputReadyCancellationTokenSource;
+    private readonly BlockingCollection<InputRecord> _inputQueue = new (new ConcurrentQueue<InputRecord> ());
+    internal WindowsMainLoop? _mainLoop;
+
+    public const int STD_OUTPUT_HANDLE = -11;
+    public const int STD_INPUT_HANDLE = -10;
+
+    private readonly nint _inputHandle;
+    private nint _outputHandle;
+    //private nint _screenBuffer;
+    private readonly uint _originalConsoleMode;
+    private CursorVisibility? _initialCursorVisibility;
+    private CursorVisibility? _currentCursorVisibility;
+    private CursorVisibility? _pendingCursorVisibility;
+    private readonly StringBuilder _stringBuilder = new (256 * 1024);
+    private string _lastWrite = string.Empty;
+
+    public WindowsConsole ()
+    {
+        _inputHandle = GetStdHandle (STD_INPUT_HANDLE);
+        _outputHandle = GetStdHandle (STD_OUTPUT_HANDLE);
+        _originalConsoleMode = ConsoleMode;
+        uint newConsoleMode = _originalConsoleMode;
+        newConsoleMode |= (uint)(ConsoleModes.EnableMouseInput | ConsoleModes.EnableExtendedFlags);
+        newConsoleMode &= ~(uint)ConsoleModes.EnableQuickEditMode;
+        newConsoleMode &= ~(uint)ConsoleModes.EnableProcessedInput;
+        ConsoleMode = newConsoleMode;
+
+        _inputReadyCancellationTokenSource = new ();
+        Task.Run (ProcessInputQueue, _inputReadyCancellationTokenSource.Token);
+    }
+
+    public InputRecord? DequeueInput ()
+    {
+        while (_inputReadyCancellationTokenSource is { })
+        {
+            try
+            {
+                return _inputQueue.Take (_inputReadyCancellationTokenSource.Token);
+            }
+            catch (OperationCanceledException)
+            {
+                return null;
+            }
+        }
+
+        return null;
+    }
+
+    public InputRecord? ReadConsoleInput ()
+    {
+        const int BUFFER_SIZE = 1;
+        InputRecord inputRecord = default;
+        uint numberEventsRead = 0;
+
+        while (!_inputReadyCancellationTokenSource!.IsCancellationRequested)
+        {
+            try
+            {
+                // Peek to check if there is any input available
+                if (PeekConsoleInput (_inputHandle, out _, BUFFER_SIZE, out uint eventsRead) && eventsRead > 0)
+                {
+                    // Read the input since it is available
+                    ReadConsoleInput (
+                                      _inputHandle,
+                                      out inputRecord,
+                                      BUFFER_SIZE,
+                                      out numberEventsRead);
+                }
+
+                if (numberEventsRead > 0)
+                {
+                    return inputRecord;
+                }
+
+                try
+                {
+                    Task.Delay (100, _inputReadyCancellationTokenSource.Token).Wait (_inputReadyCancellationTokenSource.Token);
+                }
+                catch (OperationCanceledException)
+                {
+                    return null;
+                }
+            }
+            catch (Exception ex)
+            {
+                if (ex is OperationCanceledException or ObjectDisposedException)
+                {
+                    return null;
+                }
+
+                throw;
+            }
+        }
+
+        return null;
+    }
+
+    private void ProcessInputQueue ()
+    {
+        while (_inputReadyCancellationTokenSource is { IsCancellationRequested: false })
+        {
+            try
+            {
+                if (_inputQueue.Count == 0)
+                {
+                    while (_inputReadyCancellationTokenSource is { IsCancellationRequested: false })
+                    {
+                        try
+                        {
+                            InputRecord? inpRec = ReadConsoleInput ();
+
+                            if (inpRec is { })
+                            {
+                                _inputQueue.Add (inpRec.Value);
+
+                                break;
+                            }
+                        }
+                        catch (OperationCanceledException)
+                        {
+                            return;
+                        }
+                    }
+                }
+            }
+            catch (OperationCanceledException)
+            {
+                return;
+            }
+        }
+    }
+
+    private CharInfo []? _originalStdOutChars;
+
+    public bool WriteToConsole (Size size, ExtendedCharInfo [] charInfoBuffer, Coord bufferSize, SmallRect window, bool force16Colors)
+    {
+        //Debug.WriteLine ("WriteToConsole");
+
+        //if (_screenBuffer == nint.Zero)
+        //{
+        //    ReadFromConsoleOutput (size, bufferSize, ref window);
+        //}
+
+        var result = false;
+
+        if (force16Colors)
+        {
+            var i = 0;
+            CharInfo [] ci = new CharInfo [charInfoBuffer.Length];
+
+            foreach (ExtendedCharInfo info in charInfoBuffer)
+            {
+                ci [i++] = new CharInfo
+                {
+                    Char = new CharUnion { UnicodeChar = info.Char },
+                    Attributes =
+                        (ushort)((int)info.Attribute.Foreground.GetClosestNamedColor16 () | ((int)info.Attribute.Background.GetClosestNamedColor16 () << 4))
+                };
+            }
+
+            result = WriteConsoleOutput (_outputHandle, ci, bufferSize, new Coord { X = window.Left, Y = window.Top }, ref window);
+        }
+        else
+        {
+            _stringBuilder.Clear ();
+
+            _stringBuilder.Append (EscSeqUtils.CSI_SaveCursorPosition);
+            _stringBuilder.Append (EscSeqUtils.CSI_SetCursorPosition (0, 0));
+
+            Attribute? prev = null;
+
+            foreach (ExtendedCharInfo info in charInfoBuffer)
+            {
+                Attribute attr = info.Attribute;
+
+                if (attr != prev)
+                {
+                    prev = attr;
+                    _stringBuilder.Append (EscSeqUtils.CSI_SetForegroundColorRGB (attr.Foreground.R, attr.Foreground.G, attr.Foreground.B));
+                    _stringBuilder.Append (EscSeqUtils.CSI_SetBackgroundColorRGB (attr.Background.R, attr.Background.G, attr.Background.B));
+                }
+
+                if (info.Char != '\x1b')
+                {
+                    if (!info.Empty)
+                    {
+                        _stringBuilder.Append (info.Char);
+                    }
+                }
+                else
+                {
+                    _stringBuilder.Append (' ');
+                }
+            }
+
+            _stringBuilder.Append (EscSeqUtils.CSI_RestoreCursorPosition);
+            _stringBuilder.Append (EscSeqUtils.CSI_HideCursor);
+
+            var s = _stringBuilder.ToString ();
+
+            // TODO: requires extensive testing if we go down this route
+            // If console output has changed
+            if (s != _lastWrite)
+            {
+                // supply console with the new content
+                result = WriteConsole (_outputHandle, s, (uint)s.Length, out uint _, nint.Zero);
+            }
+
+            _lastWrite = s;
+
+            foreach (var sixel in Application.Sixel)
+            {
+                SetCursorPosition (new Coord ((short)sixel.ScreenPosition.X, (short)sixel.ScreenPosition.Y));
+                WriteConsole (_outputHandle, sixel.SixelData, (uint)sixel.SixelData.Length, out uint _, nint.Zero);
+            }
+        }
+
+        if (!result)
+        {
+            int err = Marshal.GetLastWin32Error ();
+
+            if (err != 0)
+            {
+                throw new Win32Exception (err);
+            }
+        }
+
+        return result;
+    }
+
+    internal bool WriteANSI (string ansi)
+    {
+        if (WriteConsole (_outputHandle, ansi, (uint)ansi.Length, out uint _, nint.Zero))
+        {
+            // Flush the output to make sure it's sent immediately
+            return FlushFileBuffers (_outputHandle);
+        }
+
+        return false;
+    }
+
+    public void ReadFromConsoleOutput (Size size, Coord coords, ref SmallRect window)
+    {
+        //_screenBuffer = CreateConsoleScreenBuffer (
+        //                                           DesiredAccess.GenericRead | DesiredAccess.GenericWrite,
+        //                                           ShareMode.FileShareRead | ShareMode.FileShareWrite,
+        //                                           nint.Zero,
+        //                                           1,
+        //                                           nint.Zero
+        //                                          );
+
+        //if (_screenBuffer == INVALID_HANDLE_VALUE)
+        //{
+        //    int err = Marshal.GetLastWin32Error ();
+
+        //    if (err != 0)
+        //    {
+        //        throw new Win32Exception (err);
+        //    }
+        //}
+
+        SetInitialCursorVisibility ();
+
+        //if (!SetConsoleActiveScreenBuffer (_screenBuffer))
+        //{
+        //    throw new Win32Exception (Marshal.GetLastWin32Error ());
+        //}
+
+        _originalStdOutChars = new CharInfo [size.Height * size.Width];
+
+        if (!ReadConsoleOutput (_outputHandle, _originalStdOutChars, coords, new Coord { X = 0, Y = 0 }, ref window))
+        {
+            throw new Win32Exception (Marshal.GetLastWin32Error ());
+        }
+    }
+
+    public bool SetCursorPosition (Coord position)
+    {
+        return SetConsoleCursorPosition (_outputHandle, position);
+    }
+
+    public void SetInitialCursorVisibility ()
+    {
+        if (_initialCursorVisibility.HasValue == false && GetCursorVisibility (out CursorVisibility visibility))
+        {
+            _initialCursorVisibility = visibility;
+        }
+    }
+
+    public bool GetCursorVisibility (out CursorVisibility visibility)
+    {
+        if (_outputHandle == nint.Zero)
+        {
+            visibility = CursorVisibility.Invisible;
+
+            return false;
+        }
+
+        if (!GetConsoleCursorInfo (_outputHandle, out ConsoleCursorInfo info))
+        {
+            int err = Marshal.GetLastWin32Error ();
+
+            if (err != 0)
+            {
+                throw new Win32Exception (err);
+            }
+
+            visibility = CursorVisibility.Default;
+
+            return false;
+        }
+
+        if (!info.bVisible)
+        {
+            visibility = CursorVisibility.Invisible;
+        }
+        else if (info.dwSize > 50)
+        {
+            visibility = CursorVisibility.Default;
+        }
+        else
+        {
+            visibility = CursorVisibility.Default;
+        }
+
+        return true;
+    }
+
+    public bool EnsureCursorVisibility ()
+    {
+        if (_initialCursorVisibility.HasValue && _pendingCursorVisibility.HasValue && SetCursorVisibility (_pendingCursorVisibility.Value))
+        {
+            _pendingCursorVisibility = null;
+
+            return true;
+        }
+
+        return false;
+    }
+
+    public void ForceRefreshCursorVisibility ()
+    {
+        if (_currentCursorVisibility.HasValue)
+        {
+            _pendingCursorVisibility = _currentCursorVisibility;
+            _currentCursorVisibility = null;
+        }
+    }
+
+    public bool SetCursorVisibility (CursorVisibility visibility)
+    {
+        if (_initialCursorVisibility.HasValue == false)
+        {
+            _pendingCursorVisibility = visibility;
+
+            return false;
+        }
+
+        if (_currentCursorVisibility.HasValue == false || _currentCursorVisibility.Value != visibility)
+        {
+            var info = new ConsoleCursorInfo
+            {
+                dwSize = (uint)visibility & 0x00FF,
+                bVisible = ((uint)visibility & 0xFF00) != 0
+            };
+
+            if (!SetConsoleCursorInfo (_outputHandle, ref info))
+            {
+                return false;
+            }
+
+            _currentCursorVisibility = visibility;
+        }
+
+        return true;
+    }
+
+    public void Cleanup ()
+    {
+        if (_initialCursorVisibility.HasValue)
+        {
+            SetCursorVisibility (_initialCursorVisibility.Value);
+        }
+
+        //SetConsoleOutputWindow (out _);
+
+        ConsoleMode = _originalConsoleMode;
+
+        _outputHandle = CreateConsoleScreenBuffer (
+                                                   DesiredAccess.GenericRead | DesiredAccess.GenericWrite,
+                                                   ShareMode.FileShareRead | ShareMode.FileShareWrite,
+                                                   nint.Zero,
+                                                   1,
+                                                   nint.Zero
+                                                  );
+
+        if (!SetConsoleActiveScreenBuffer (_outputHandle))
+        {
+            int err = Marshal.GetLastWin32Error ();
+            Console.WriteLine ("Error: {0}", err);
+        }
+
+        //if (_screenBuffer != nint.Zero)
+        //{
+        //    CloseHandle (_screenBuffer);
+        //}
+
+        //_screenBuffer = nint.Zero;
+
+        _inputReadyCancellationTokenSource?.Cancel ();
+        _inputReadyCancellationTokenSource?.Dispose ();
+        _inputReadyCancellationTokenSource = null;
+    }
+
+    internal Size GetConsoleBufferWindow (out Point position)
+    {
+        if (_outputHandle == nint.Zero)
+        {
+            position = Point.Empty;
+
+            return Size.Empty;
+        }
+
+        var csbi = new CONSOLE_SCREEN_BUFFER_INFOEX ();
+        csbi.cbSize = (uint)Marshal.SizeOf (csbi);
+
+        if (!GetConsoleScreenBufferInfoEx (_outputHandle, ref csbi))
+        {
+            //throw new System.ComponentModel.Win32Exception (Marshal.GetLastWin32Error ());
+            position = Point.Empty;
+
+            return Size.Empty;
+        }
+
+        Size sz = new (
+                       csbi.srWindow.Right - csbi.srWindow.Left + 1,
+                       csbi.srWindow.Bottom - csbi.srWindow.Top + 1);
+        position = new (csbi.srWindow.Left, csbi.srWindow.Top);
+
+        return sz;
+    }
+
+    internal Size GetConsoleOutputWindow (out Point position)
+    {
+        var csbi = new CONSOLE_SCREEN_BUFFER_INFOEX ();
+        csbi.cbSize = (uint)Marshal.SizeOf (csbi);
+
+        if (!GetConsoleScreenBufferInfoEx (_outputHandle, ref csbi))
+        {
+            throw new Win32Exception (Marshal.GetLastWin32Error ());
+        }
+
+        Size sz = new (
+                       csbi.srWindow.Right - csbi.srWindow.Left + 1,
+                       csbi.srWindow.Bottom - csbi.srWindow.Top + 1);
+        position = new (csbi.srWindow.Left, csbi.srWindow.Top);
+
+        return sz;
+    }
+
+    //internal Size SetConsoleWindow (short cols, short rows)
+    //{
+    //    var csbi = new CONSOLE_SCREEN_BUFFER_INFOEX ();
+    //    csbi.cbSize = (uint)Marshal.SizeOf (csbi);
+
+    //    if (!GetConsoleScreenBufferInfoEx (_screenBuffer, ref csbi))
+    //    {
+    //        throw new Win32Exception (Marshal.GetLastWin32Error ());
+    //    }
+
+    //    Coord maxWinSize = GetLargestConsoleWindowSize (_screenBuffer);
+    //    short newCols = Math.Min (cols, maxWinSize.X);
+    //    short newRows = Math.Min (rows, maxWinSize.Y);
+    //    csbi.dwSize = new Coord (newCols, Math.Max (newRows, (short)1));
+    //    csbi.srWindow = new SmallRect (0, 0, newCols, newRows);
+    //    csbi.dwMaximumWindowSize = new Coord (newCols, newRows);
+
+    //    if (!SetConsoleScreenBufferInfoEx (_screenBuffer, ref csbi))
+    //    {
+    //        throw new Win32Exception (Marshal.GetLastWin32Error ());
+    //    }
+
+    //    var winRect = new SmallRect (0, 0, (short)(newCols - 1), (short)Math.Max (newRows - 1, 0));
+
+    //    if (!SetConsoleWindowInfo (_outputHandle, true, ref winRect))
+    //    {
+    //        //throw new System.ComponentModel.Win32Exception (Marshal.GetLastWin32Error ());
+    //        return new (cols, rows);
+    //    }
+
+    //    SetConsoleOutputWindow (csbi);
+
+    //    return new (winRect.Right + 1, newRows - 1 < 0 ? 0 : winRect.Bottom + 1);
+    //}
+
+    //private void SetConsoleOutputWindow (CONSOLE_SCREEN_BUFFER_INFOEX csbi)
+    //{
+    //    if (_screenBuffer != nint.Zero && !SetConsoleScreenBufferInfoEx (_screenBuffer, ref csbi))
+    //    {
+    //        throw new Win32Exception (Marshal.GetLastWin32Error ());
+    //    }
+    //}
+
+    //internal Size SetConsoleOutputWindow (out Point position)
+    //{
+    //    if (_screenBuffer == nint.Zero)
+    //    {
+    //        position = Point.Empty;
+
+    //        return Size.Empty;
+    //    }
+
+    //    var csbi = new CONSOLE_SCREEN_BUFFER_INFOEX ();
+    //    csbi.cbSize = (uint)Marshal.SizeOf (csbi);
+
+    //    if (!GetConsoleScreenBufferInfoEx (_screenBuffer, ref csbi))
+    //    {
+    //        throw new Win32Exception (Marshal.GetLastWin32Error ());
+    //    }
+
+    //    Size sz = new (
+    //                       csbi.srWindow.Right - csbi.srWindow.Left + 1,
+    //                       Math.Max (csbi.srWindow.Bottom - csbi.srWindow.Top + 1, 0));
+    //    position = new (csbi.srWindow.Left, csbi.srWindow.Top);
+    //    SetConsoleOutputWindow (csbi);
+    //    var winRect = new SmallRect (0, 0, (short)(sz.Width - 1), (short)Math.Max (sz.Height - 1, 0));
+
+    //    if (!SetConsoleScreenBufferInfoEx (_outputHandle, ref csbi))
+    //    {
+    //        throw new Win32Exception (Marshal.GetLastWin32Error ());
+    //    }
+
+    //    if (!SetConsoleWindowInfo (_outputHandle, true, ref winRect))
+    //    {
+    //        throw new Win32Exception (Marshal.GetLastWin32Error ());
+    //    }
+
+    //    return sz;
+    //}
+
+    private uint ConsoleMode
+    {
+        get
+        {
+            GetConsoleMode (_inputHandle, out uint v);
+
+            return v;
+        }
+        set => SetConsoleMode (_inputHandle, value);
+    }
+
+    [Flags]
+    public enum ConsoleModes : uint
+    {
+        EnableProcessedInput = 1,
+        EnableMouseInput = 16,
+        EnableQuickEditMode = 64,
+        EnableExtendedFlags = 128
+    }
+
+    [StructLayout (LayoutKind.Explicit, CharSet = CharSet.Unicode)]
+    public struct KeyEventRecord
+    {
+        [FieldOffset (0)]
+        [MarshalAs (UnmanagedType.Bool)]
+        public bool bKeyDown;
+
+        [FieldOffset (4)]
+        [MarshalAs (UnmanagedType.U2)]
+        public ushort wRepeatCount;
+
+        [FieldOffset (6)]
+        [MarshalAs (UnmanagedType.U2)]
+        public ConsoleKeyMapping.VK wVirtualKeyCode;
+
+        [FieldOffset (8)]
+        [MarshalAs (UnmanagedType.U2)]
+        public ushort wVirtualScanCode;
+
+        [FieldOffset (10)]
+        public char UnicodeChar;
+
+        [FieldOffset (12)]
+        [MarshalAs (UnmanagedType.U4)]
+        public ControlKeyState dwControlKeyState;
+
+        public readonly override string ToString ()
+        {
+            return
+                $"[KeyEventRecord({(bKeyDown ? "down" : "up")},{wRepeatCount},{wVirtualKeyCode},{wVirtualScanCode},{new Rune (UnicodeChar).MakePrintable ()},{dwControlKeyState})]";
+        }
+    }
+
+    [Flags]
+    public enum ButtonState
+    {
+        NoButtonPressed = 0,
+        Button1Pressed = 1,
+        Button2Pressed = 4,
+        Button3Pressed = 8,
+        Button4Pressed = 16,
+        RightmostButtonPressed = 2
+    }
+
+    [Flags]
+    public enum ControlKeyState
+    {
+        NoControlKeyPressed = 0,
+        RightAltPressed = 1,
+        LeftAltPressed = 2,
+        RightControlPressed = 4,
+        LeftControlPressed = 8,
+        ShiftPressed = 16,
+        NumlockOn = 32,
+        ScrolllockOn = 64,
+        CapslockOn = 128,
+        EnhancedKey = 256
+    }
+
+    [Flags]
+    public enum EventFlags
+    {
+        NoEvent = 0,
+        MouseMoved = 1,
+        DoubleClick = 2,
+        MouseWheeled = 4,
+        MouseHorizontalWheeled = 8
+    }
+
+    [StructLayout (LayoutKind.Explicit)]
+    public struct MouseEventRecord
+    {
+        [FieldOffset (0)]
+        public Coord MousePosition;
+
+        [FieldOffset (4)]
+        public ButtonState ButtonState;
+
+        [FieldOffset (8)]
+        public ControlKeyState ControlKeyState;
+
+        [FieldOffset (12)]
+        public EventFlags EventFlags;
+
+        public readonly override string ToString () { return $"[Mouse{MousePosition},{ButtonState},{ControlKeyState},{EventFlags}]"; }
+    }
+
+    public struct WindowBufferSizeRecord
+    {
+        public Coord _size;
+
+        public WindowBufferSizeRecord (short x, short y) { _size = new Coord (x, y); }
+
+        public readonly override string ToString () { return $"[WindowBufferSize{_size}"; }
+    }
+
+    [StructLayout (LayoutKind.Sequential)]
+    public struct MenuEventRecord
+    {
+        public uint dwCommandId;
+    }
+
+    [StructLayout (LayoutKind.Sequential)]
+    public struct FocusEventRecord
+    {
+        public uint bSetFocus;
+    }
+
+    public enum EventType : ushort
+    {
+        Focus = 0x10,
+        Key = 0x1,
+        Menu = 0x8,
+        Mouse = 2,
+        WindowBufferSize = 4
+    }
+
+    [StructLayout (LayoutKind.Explicit)]
+    public struct InputRecord
+    {
+        [FieldOffset (0)]
+        public EventType EventType;
+
+        [FieldOffset (4)]
+        public KeyEventRecord KeyEvent;
+
+        [FieldOffset (4)]
+        public MouseEventRecord MouseEvent;
+
+        [FieldOffset (4)]
+        public WindowBufferSizeRecord WindowBufferSizeEvent;
+
+        [FieldOffset (4)]
+        public MenuEventRecord MenuEvent;
+
+        [FieldOffset (4)]
+        public FocusEventRecord FocusEvent;
+
+        public readonly override string ToString ()
+        {
+            return (EventType switch
+                    {
+                        EventType.Focus => FocusEvent.ToString (),
+                        EventType.Key => KeyEvent.ToString (),
+                        EventType.Menu => MenuEvent.ToString (),
+                        EventType.Mouse => MouseEvent.ToString (),
+                        EventType.WindowBufferSize => WindowBufferSizeEvent.ToString (),
+                        _ => "Unknown event type: " + EventType
+                    })!;
+        }
+    }
+
+    [Flags]
+    private enum ShareMode : uint
+    {
+        FileShareRead = 1,
+        FileShareWrite = 2
+    }
+
+    [Flags]
+    private enum DesiredAccess : uint
+    {
+        GenericRead = 2147483648,
+        GenericWrite = 1073741824
+    }
+
+    [StructLayout (LayoutKind.Sequential)]
+    public struct ConsoleScreenBufferInfo
+    {
+        public Coord dwSize;
+        public Coord dwCursorPosition;
+        public ushort wAttributes;
+        public SmallRect srWindow;
+        public Coord dwMaximumWindowSize;
+    }
+
+    [StructLayout (LayoutKind.Sequential)]
+    public struct Coord
+    {
+        public short X;
+        public short Y;
+
+        public Coord (short x, short y)
+        {
+            X = x;
+            Y = y;
+        }
+
+        public readonly override string ToString () { return $"({X},{Y})"; }
+    }
+
+    [StructLayout (LayoutKind.Explicit, CharSet = CharSet.Unicode)]
+    public struct CharUnion
+    {
+        [FieldOffset (0)]
+        public char UnicodeChar;
+
+        [FieldOffset (0)]
+        public byte AsciiChar;
+    }
+
+    [StructLayout (LayoutKind.Explicit, CharSet = CharSet.Unicode)]
+    public struct CharInfo
+    {
+        [FieldOffset (0)]
+        public CharUnion Char;
+
+        [FieldOffset (2)]
+        public ushort Attributes;
+    }
+
+    public struct ExtendedCharInfo
+    {
+        public char Char { get; set; }
+        public Attribute Attribute { get; set; }
+        public bool Empty { get; set; } // TODO: Temp hack until virtual terminal sequences
+
+        public ExtendedCharInfo (char character, Attribute attribute)
+        {
+            Char = character;
+            Attribute = attribute;
+            Empty = false;
+        }
+    }
+
+    [StructLayout (LayoutKind.Sequential)]
+    public struct SmallRect
+    {
+        public short Left;
+        public short Top;
+        public short Right;
+        public short Bottom;
+
+        public SmallRect (short left, short top, short right, short bottom)
+        {
+            Left = left;
+            Top = top;
+            Right = right;
+            Bottom = bottom;
+        }
+
+        public static void MakeEmpty (ref SmallRect rect) { rect.Left = -1; }
+
+        public static void Update (ref SmallRect rect, short col, short row)
+        {
+            if (rect.Left == -1)
+            {
+                rect.Left = rect.Right = col;
+                rect.Bottom = rect.Top = row;
+
+                return;
+            }
+
+            if (col >= rect.Left && col <= rect.Right && row >= rect.Top && row <= rect.Bottom)
+            {
+                return;
+            }
+
+            if (col < rect.Left)
+            {
+                rect.Left = col;
+            }
+
+            if (col > rect.Right)
+            {
+                rect.Right = col;
+            }
+
+            if (row < rect.Top)
+            {
+                rect.Top = row;
+            }
+
+            if (row > rect.Bottom)
+            {
+                rect.Bottom = row;
+            }
+        }
+
+        public readonly override string ToString () { return $"Left={Left},Top={Top},Right={Right},Bottom={Bottom}"; }
+    }
+
+    [StructLayout (LayoutKind.Sequential)]
+    public struct ConsoleKeyInfoEx
+    {
+        public ConsoleKeyInfo ConsoleKeyInfo;
+        public bool CapsLock;
+        public bool NumLock;
+        public bool ScrollLock;
+
+        public ConsoleKeyInfoEx (ConsoleKeyInfo consoleKeyInfo, bool capslock, bool numlock, bool scrolllock)
+        {
+            ConsoleKeyInfo = consoleKeyInfo;
+            CapsLock = capslock;
+            NumLock = numlock;
+            ScrollLock = scrolllock;
+        }
+
+        /// <summary>
+        ///     Prints a ConsoleKeyInfoEx structure
+        /// </summary>
+        /// <param name="ex"></param>
+        /// <returns></returns>
+        public readonly string ToString (ConsoleKeyInfoEx ex)
+        {
+            var ke = new Key ((KeyCode)ex.ConsoleKeyInfo.KeyChar);
+            var sb = new StringBuilder ();
+            sb.Append ($"Key: {(KeyCode)ex.ConsoleKeyInfo.Key} ({ex.ConsoleKeyInfo.Key})");
+            sb.Append ((ex.ConsoleKeyInfo.Modifiers & ConsoleModifiers.Shift) != 0 ? " | Shift" : string.Empty);
+            sb.Append ((ex.ConsoleKeyInfo.Modifiers & ConsoleModifiers.Control) != 0 ? " | Control" : string.Empty);
+            sb.Append ((ex.ConsoleKeyInfo.Modifiers & ConsoleModifiers.Alt) != 0 ? " | Alt" : string.Empty);
+            sb.Append ($", KeyChar: {ke.AsRune.MakePrintable ()} ({(uint)ex.ConsoleKeyInfo.KeyChar}) ");
+            sb.Append (ex.CapsLock ? "caps," : string.Empty);
+            sb.Append (ex.NumLock ? "num," : string.Empty);
+            sb.Append (ex.ScrollLock ? "scroll," : string.Empty);
+            string s = sb.ToString ().TrimEnd (',').TrimEnd (' ');
+
+            return $"[ConsoleKeyInfoEx({s})]";
+        }
+    }
+
+    [DllImport ("kernel32.dll", SetLastError = true)]
+    private static extern nint GetStdHandle (int nStdHandle);
+
+    [DllImport ("kernel32.dll", SetLastError = true)]
+    private static extern bool CloseHandle (nint handle);
+
+    [DllImport ("kernel32.dll", SetLastError = true)]
+    public static extern bool PeekConsoleInput (nint hConsoleInput, out InputRecord lpBuffer, uint nLength, out uint lpNumberOfEventsRead);
+
+    [DllImport ("kernel32.dll", EntryPoint = "ReadConsoleInputW", CharSet = CharSet.Unicode)]
+    public static extern bool ReadConsoleInput (
+        nint hConsoleInput,
+        out InputRecord lpBuffer,
+        uint nLength,
+        out uint lpNumberOfEventsRead
+    );
+
+    [DllImport ("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
+    private static extern bool ReadConsoleOutput (
+        nint hConsoleOutput,
+        [Out] CharInfo [] lpBuffer,
+        Coord dwBufferSize,
+        Coord dwBufferCoord,
+        ref SmallRect lpReadRegion
+    );
+
+    // TODO: This API is obsolete. See https://learn.microsoft.com/en-us/windows/console/writeconsoleoutput
+    [DllImport ("kernel32.dll", EntryPoint = "WriteConsoleOutputW", SetLastError = true, CharSet = CharSet.Unicode)]
+    private static extern bool WriteConsoleOutput (
+        nint hConsoleOutput,
+        CharInfo [] lpBuffer,
+        Coord dwBufferSize,
+        Coord dwBufferCoord,
+        ref SmallRect lpWriteRegion
+    );
+
+    [DllImport ("kernel32.dll", EntryPoint = "WriteConsole", SetLastError = true, CharSet = CharSet.Unicode)]
+    private static extern bool WriteConsole (
+        nint hConsoleOutput,
+        string lpbufer,
+        uint NumberOfCharsToWriten,
+        out uint lpNumberOfCharsWritten,
+        nint lpReserved
+    );
+
+    [DllImport ("kernel32.dll", SetLastError = true)]
+    static extern bool FlushFileBuffers (nint hFile);
+
+    [DllImport ("kernel32.dll")]
+    private static extern bool SetConsoleCursorPosition (nint hConsoleOutput, Coord dwCursorPosition);
+
+    [StructLayout (LayoutKind.Sequential)]
+    public struct ConsoleCursorInfo
+    {
+        /// <summary>
+        /// The percentage of the character cell that is filled by the cursor.This value is between 1 and 100.
+        /// The cursor appearance varies, ranging from completely filling the cell to showing up as a horizontal
+        /// line at the bottom of the cell.
+        /// </summary>
+        public uint dwSize;
+        public bool bVisible;
+    }
+
+    [DllImport ("kernel32.dll", SetLastError = true)]
+    private static extern bool SetConsoleCursorInfo (nint hConsoleOutput, [In] ref ConsoleCursorInfo lpConsoleCursorInfo);
+
+    [DllImport ("kernel32.dll", SetLastError = true)]
+    private static extern bool GetConsoleCursorInfo (nint hConsoleOutput, out ConsoleCursorInfo lpConsoleCursorInfo);
+
+    [DllImport ("kernel32.dll")]
+    private static extern bool GetConsoleMode (nint hConsoleHandle, out uint lpMode);
+
+    [DllImport ("kernel32.dll")]
+    private static extern bool SetConsoleMode (nint hConsoleHandle, uint dwMode);
+
+    [DllImport ("kernel32.dll", SetLastError = true)]
+    private static extern nint CreateConsoleScreenBuffer (
+        DesiredAccess dwDesiredAccess,
+        ShareMode dwShareMode,
+        nint secutiryAttributes,
+        uint flags,
+        nint screenBufferData
+    );
+
+    internal static nint INVALID_HANDLE_VALUE = new (-1);
+
+    [DllImport ("kernel32.dll", SetLastError = true)]
+    private static extern bool SetConsoleActiveScreenBuffer (nint handle);
+
+    [DllImport ("kernel32.dll", SetLastError = true)]
+    private static extern bool GetNumberOfConsoleInputEvents (nint handle, out uint lpcNumberOfEvents);
+
+    internal uint GetNumberOfConsoleInputEvents ()
+    {
+        if (!GetNumberOfConsoleInputEvents (_inputHandle, out uint numOfEvents))
+        {
+            Console.WriteLine ($"Error: {Marshal.GetLastWin32Error ()}");
+
+            return 0;
+        }
+
+        return numOfEvents;
+    }
+
+    [DllImport ("kernel32.dll", SetLastError = true)]
+    private static extern bool FlushConsoleInputBuffer (nint handle);
+
+    internal void FlushConsoleInputBuffer ()
+    {
+        if (!FlushConsoleInputBuffer (_inputHandle))
+        {
+            Console.WriteLine ($"Error: {Marshal.GetLastWin32Error ()}");
+        }
+    }
+
+#if false // Not needed on the constructor. Perhaps could be used on resizing. To study.
+		[DllImport ("kernel32.dll", ExactSpelling = true)]
+		static extern IntPtr GetConsoleWindow ();
+
+		[DllImport ("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
+		static extern bool ShowWindow (IntPtr hWnd, int nCmdShow);
+
+		public const int HIDE = 0;
+		public const int MAXIMIZE = 3;
+		public const int MINIMIZE = 6;
+		public const int RESTORE = 9;
+
+		internal void ShowWindow (int state)
+		{
+			IntPtr thisConsole = GetConsoleWindow ();
+			ShowWindow (thisConsole, state);
+		}
+#endif
+
+    // See: https://github.com/gui-cs/Terminal.Gui/issues/357
+
+    [StructLayout (LayoutKind.Sequential)]
+    public struct CONSOLE_SCREEN_BUFFER_INFOEX
+    {
+        public uint cbSize;
+        public Coord dwSize;
+        public Coord dwCursorPosition;
+        public ushort wAttributes;
+        public SmallRect srWindow;
+        public Coord dwMaximumWindowSize;
+        public ushort wPopupAttributes;
+        public bool bFullscreenSupported;
+
+        [MarshalAs (UnmanagedType.ByValArray, SizeConst = 16)]
+        public COLORREF [] ColorTable;
+    }
+
+    [StructLayout (LayoutKind.Explicit, Size = 4)]
+    public struct COLORREF
+    {
+        public COLORREF (byte r, byte g, byte b)
+        {
+            Value = 0;
+            R = r;
+            G = g;
+            B = b;
+        }
+
+        public COLORREF (uint value)
+        {
+            R = 0;
+            G = 0;
+            B = 0;
+            Value = value & 0x00FFFFFF;
+        }
+
+        [FieldOffset (0)]
+        public byte R;
+
+        [FieldOffset (1)]
+        public byte G;
+
+        [FieldOffset (2)]
+        public byte B;
+
+        [FieldOffset (0)]
+        public uint Value;
+    }
+
+    [DllImport ("kernel32.dll", SetLastError = true)]
+    private static extern bool GetConsoleScreenBufferInfoEx (nint hConsoleOutput, ref CONSOLE_SCREEN_BUFFER_INFOEX csbi);
+
+    [DllImport ("kernel32.dll", SetLastError = true)]
+    private static extern bool SetConsoleScreenBufferInfoEx (nint hConsoleOutput, ref CONSOLE_SCREEN_BUFFER_INFOEX consoleScreenBufferInfo);
+
+    [DllImport ("kernel32.dll", SetLastError = true)]
+    private static extern bool SetConsoleWindowInfo (
+        nint hConsoleOutput,
+        bool bAbsolute,
+        [In] ref SmallRect lpConsoleWindow
+    );
+
+    [DllImport ("kernel32.dll", SetLastError = true)]
+    private static extern Coord GetLargestConsoleWindowSize (
+        nint hConsoleOutput
+    );
+}

+ 1190 - 0
Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsDriver.cs

@@ -0,0 +1,1190 @@
+#nullable enable
+// 
+// WindowsDriver.cs: Windows specific driver
+//
+
+// HACK:
+// WindowsConsole/Terminal has two issues:
+// 1) Tearing can occur when the console is resized.
+// 2) The values provided during Init (and the first WindowsConsole.EventType.WindowBufferSize) are not correct.
+//
+// If HACK_CHECK_WINCHANGED is defined then we ignore WindowsConsole.EventType.WindowBufferSize events
+// and instead check the console size every 500ms in a thread in WidowsMainLoop.
+// As of Windows 11 23H2 25947.1000 and/or WT 1.19.2682 tearing no longer occurs when using
+// the WindowsConsole.EventType.WindowBufferSize event. However, on Init the window size is
+// still incorrect so we still need this hack.
+
+//#define HACK_CHECK_WINCHANGED
+
+using System.ComponentModel;
+using System.Diagnostics;
+using System.Runtime.InteropServices;
+using static Terminal.Gui.ConsoleDrivers.ConsoleKeyMapping;
+
+namespace Terminal.Gui;
+
+internal class WindowsDriver : ConsoleDriver
+{
+    private readonly bool _isWindowsTerminal;
+
+    private WindowsConsole.SmallRect _damageRegion;
+    private bool _isButtonDoubleClicked;
+    private bool _isButtonPressed;
+    private bool _isButtonReleased;
+    private bool _isOneFingerDoubleClicked;
+
+    private WindowsConsole.ButtonState? _lastMouseButtonPressed;
+    private WindowsMainLoop? _mainLoopDriver;
+    private WindowsConsole.ExtendedCharInfo [] _outputBuffer;
+    private Point? _point;
+    private Point _pointMove;
+    private bool _processButtonClick;
+
+    public WindowsDriver ()
+    {
+        if (Environment.OSVersion.Platform == PlatformID.Win32NT)
+        {
+            WinConsole = new ();
+
+            // otherwise we're probably running in unit tests
+            Clipboard = new WindowsClipboard ();
+        }
+        else
+        {
+            Clipboard = new FakeDriver.FakeClipboard ();
+        }
+
+        // TODO: if some other Windows-based terminal supports true color, update this logic to not
+        // force 16color mode (.e.g ConEmu which really doesn't work well at all).
+        _isWindowsTerminal = _isWindowsTerminal =
+                                 Environment.GetEnvironmentVariable ("WT_SESSION") is { } || Environment.GetEnvironmentVariable ("VSAPPIDNAME") != null;
+
+        if (!_isWindowsTerminal)
+        {
+            Force16Colors = true;
+        }
+    }
+
+    public override bool SupportsTrueColor => RunningUnitTests || (Environment.OSVersion.Version.Build >= 14931 && _isWindowsTerminal);
+
+    public WindowsConsole? WinConsole { get; private set; }
+
+    public WindowsConsole.KeyEventRecord FromVKPacketToKeyEventRecord (WindowsConsole.KeyEventRecord keyEvent)
+    {
+        if (keyEvent.wVirtualKeyCode != (VK)ConsoleKey.Packet)
+        {
+            return keyEvent;
+        }
+
+        var mod = new ConsoleModifiers ();
+
+        if (keyEvent.dwControlKeyState.HasFlag (WindowsConsole.ControlKeyState.ShiftPressed))
+        {
+            mod |= ConsoleModifiers.Shift;
+        }
+
+        if (keyEvent.dwControlKeyState.HasFlag (WindowsConsole.ControlKeyState.RightAltPressed)
+            || keyEvent.dwControlKeyState.HasFlag (WindowsConsole.ControlKeyState.LeftAltPressed))
+        {
+            mod |= ConsoleModifiers.Alt;
+        }
+
+        if (keyEvent.dwControlKeyState.HasFlag (WindowsConsole.ControlKeyState.LeftControlPressed)
+            || keyEvent.dwControlKeyState.HasFlag (WindowsConsole.ControlKeyState.RightControlPressed))
+        {
+            mod |= ConsoleModifiers.Control;
+        }
+
+        var cKeyInfo = new ConsoleKeyInfo (
+                                           keyEvent.UnicodeChar,
+                                           (ConsoleKey)keyEvent.wVirtualKeyCode,
+                                           mod.HasFlag (ConsoleModifiers.Shift),
+                                           mod.HasFlag (ConsoleModifiers.Alt),
+                                           mod.HasFlag (ConsoleModifiers.Control));
+        cKeyInfo = DecodeVKPacketToKConsoleKeyInfo (cKeyInfo);
+        uint scanCode = GetScanCodeFromConsoleKeyInfo (cKeyInfo);
+
+        return new WindowsConsole.KeyEventRecord
+        {
+            UnicodeChar = cKeyInfo.KeyChar,
+            bKeyDown = keyEvent.bKeyDown,
+            dwControlKeyState = keyEvent.dwControlKeyState,
+            wRepeatCount = keyEvent.wRepeatCount,
+            wVirtualKeyCode = (VK)cKeyInfo.Key,
+            wVirtualScanCode = (ushort)scanCode
+        };
+    }
+
+    public override bool IsRuneSupported (Rune rune) { return base.IsRuneSupported (rune) && rune.IsBmp; }
+
+    public override void SendKeys (char keyChar, ConsoleKey key, bool shift, bool alt, bool control)
+    {
+        var input = new WindowsConsole.InputRecord
+        {
+            EventType = WindowsConsole.EventType.Key
+        };
+
+        var keyEvent = new WindowsConsole.KeyEventRecord
+        {
+            bKeyDown = true
+        };
+        var controlKey = new WindowsConsole.ControlKeyState ();
+
+        if (shift)
+        {
+            controlKey |= WindowsConsole.ControlKeyState.ShiftPressed;
+            keyEvent.UnicodeChar = '\0';
+            keyEvent.wVirtualKeyCode = VK.SHIFT;
+        }
+
+        if (alt)
+        {
+            controlKey |= WindowsConsole.ControlKeyState.LeftAltPressed;
+            controlKey |= WindowsConsole.ControlKeyState.RightAltPressed;
+            keyEvent.UnicodeChar = '\0';
+            keyEvent.wVirtualKeyCode = VK.MENU;
+        }
+
+        if (control)
+        {
+            controlKey |= WindowsConsole.ControlKeyState.LeftControlPressed;
+            controlKey |= WindowsConsole.ControlKeyState.RightControlPressed;
+            keyEvent.UnicodeChar = '\0';
+            keyEvent.wVirtualKeyCode = VK.CONTROL;
+        }
+
+        keyEvent.dwControlKeyState = controlKey;
+
+        input.KeyEvent = keyEvent;
+
+        if (shift || alt || control)
+        {
+            ProcessInput (input);
+        }
+
+        keyEvent.UnicodeChar = keyChar;
+
+        //if ((uint)key < 255) {
+        //	keyEvent.wVirtualKeyCode = (ushort)key;
+        //} else {
+        //	keyEvent.wVirtualKeyCode = '\0';
+        //}
+        keyEvent.wVirtualKeyCode = (VK)key;
+
+        input.KeyEvent = keyEvent;
+
+        try
+        {
+            ProcessInput (input);
+        }
+        catch (OverflowException)
+        { }
+        finally
+        {
+            keyEvent.bKeyDown = false;
+            input.KeyEvent = keyEvent;
+            ProcessInput (input);
+        }
+    }
+
+    public override void WriteRaw (string ansi) { WinConsole?.WriteANSI (ansi); }
+
+    #region Not Implemented
+
+    public override void Suspend () { throw new NotImplementedException (); }
+
+    #endregion
+
+    public WindowsConsole.ConsoleKeyInfoEx ToConsoleKeyInfoEx (WindowsConsole.KeyEventRecord keyEvent)
+    {
+        WindowsConsole.ControlKeyState state = keyEvent.dwControlKeyState;
+
+        bool shift = (state & WindowsConsole.ControlKeyState.ShiftPressed) != 0;
+        bool alt = (state & (WindowsConsole.ControlKeyState.LeftAltPressed | WindowsConsole.ControlKeyState.RightAltPressed)) != 0;
+        bool control = (state & (WindowsConsole.ControlKeyState.LeftControlPressed | WindowsConsole.ControlKeyState.RightControlPressed)) != 0;
+        bool capslock = (state & WindowsConsole.ControlKeyState.CapslockOn) != 0;
+        bool numlock = (state & WindowsConsole.ControlKeyState.NumlockOn) != 0;
+        bool scrolllock = (state & WindowsConsole.ControlKeyState.ScrolllockOn) != 0;
+
+        var cki = new ConsoleKeyInfo (keyEvent.UnicodeChar, (ConsoleKey)keyEvent.wVirtualKeyCode, shift, alt, control);
+
+        return new WindowsConsole.ConsoleKeyInfoEx (cki, capslock, numlock, scrolllock);
+    }
+
+    #region Cursor Handling
+
+    private CursorVisibility? _cachedCursorVisibility;
+
+    public override void UpdateCursor ()
+    {
+        if (RunningUnitTests)
+        {
+            return;
+        }
+
+        if (Col < 0 || Row < 0 || Col >= Cols || Row >= Rows)
+        {
+            GetCursorVisibility (out CursorVisibility cursorVisibility);
+            _cachedCursorVisibility = cursorVisibility;
+            SetCursorVisibility (CursorVisibility.Invisible);
+
+            return;
+        }
+
+        var position = new WindowsConsole.Coord
+        {
+            X = (short)Col,
+            Y = (short)Row
+        };
+
+        if (Force16Colors)
+        {
+            WinConsole?.SetCursorPosition (position);
+        }
+        else
+        {
+            var sb = new StringBuilder ();
+            sb.Append (EscSeqUtils.CSI_SetCursorPosition (position.Y + 1, position.X + 1));
+            WinConsole?.WriteANSI (sb.ToString ());
+        }
+
+        if (_cachedCursorVisibility is { })
+        {
+            SetCursorVisibility (_cachedCursorVisibility.Value);
+        }
+        //EnsureCursorVisibility ();
+    }
+
+    /// <inheritdoc/>
+    public override bool GetCursorVisibility (out CursorVisibility visibility)
+    {
+        if (WinConsole is { })
+        {
+            return WinConsole.GetCursorVisibility (out visibility);
+        }
+
+        visibility = _cachedCursorVisibility ?? CursorVisibility.Default;
+
+        return true;
+    }
+
+    /// <inheritdoc/>
+    public override bool SetCursorVisibility (CursorVisibility visibility)
+    {
+        _cachedCursorVisibility = visibility;
+
+        if (Force16Colors)
+        {
+            return WinConsole is null || WinConsole.SetCursorVisibility (visibility);
+        }
+        else
+        {
+            var sb = new StringBuilder ();
+            sb.Append (visibility != CursorVisibility.Invisible ? EscSeqUtils.CSI_ShowCursor : EscSeqUtils.CSI_HideCursor);
+            return WinConsole?.WriteANSI (sb.ToString ()) ?? false;
+        }
+    }
+
+    /// <inheritdoc/>
+    public override bool EnsureCursorVisibility ()
+    {
+        if (Force16Colors)
+        {
+            return WinConsole is null || WinConsole.EnsureCursorVisibility ();
+        }
+        else
+        {
+            var sb = new StringBuilder ();
+            sb.Append (_cachedCursorVisibility != CursorVisibility.Invisible ? EscSeqUtils.CSI_ShowCursor : EscSeqUtils.CSI_HideCursor);
+            return WinConsole?.WriteANSI (sb.ToString ()) ?? false;
+        }
+
+        //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 Cursor Handling
+
+    public override bool UpdateScreen ()
+    {
+        bool updated = false;
+        Size windowSize = WinConsole?.GetConsoleBufferWindow (out Point _) ?? new Size (Cols, Rows);
+
+        if (!windowSize.IsEmpty && (windowSize.Width != Cols || windowSize.Height != Rows))
+        {
+            return updated;
+        }
+
+        var bufferCoords = new WindowsConsole.Coord
+        {
+            X = (short)Cols, //Clip.Width,
+            Y = (short)Rows, //Clip.Height
+        };
+
+        for (var row = 0; row < Rows; row++)
+        {
+            if (!_dirtyLines! [row])
+            {
+                continue;
+            }
+
+            _dirtyLines [row] = false;
+            updated = true;
+
+            for (var col = 0; col < Cols; col++)
+            {
+                int position = row * Cols + col;
+                _outputBuffer [position].Attribute = Contents! [row, col].Attribute.GetValueOrDefault ();
+
+                if (Contents [row, col].IsDirty == false)
+                {
+                    _outputBuffer [position].Empty = true;
+                    _outputBuffer [position].Char = (char)Rune.ReplacementChar.Value;
+
+                    continue;
+                }
+
+                _outputBuffer [position].Empty = false;
+
+                if (Contents [row, col].Rune.IsBmp)
+                {
+                    _outputBuffer [position].Char = (char)Contents [row, col].Rune.Value;
+                }
+                else
+                {
+                    //_outputBuffer [position].Empty = true;
+                    _outputBuffer [position].Char = (char)Rune.ReplacementChar.Value;
+
+                    if (Contents [row, col].Rune.GetColumns () > 1 && col + 1 < Cols)
+                    {
+                        // TODO: This is a hack to deal with non-BMP and wide characters.
+                        col++;
+                        position = row * Cols + col;
+                        _outputBuffer [position].Empty = false;
+                        _outputBuffer [position].Char = ' ';
+                    }
+                }
+            }
+        }
+
+        _damageRegion = new WindowsConsole.SmallRect
+        {
+            Top = 0,
+            Left = 0,
+            Bottom = (short)Rows,
+            Right = (short)Cols
+        };
+
+        if (!RunningUnitTests
+            && WinConsole != null
+            && !WinConsole.WriteToConsole (new (Cols, Rows), _outputBuffer, bufferCoords, _damageRegion, Force16Colors))
+        {
+            int err = Marshal.GetLastWin32Error ();
+
+            if (err != 0)
+            {
+                throw new Win32Exception (err);
+            }
+        }
+
+        WindowsConsole.SmallRect.MakeEmpty (ref _damageRegion);
+
+        return updated;
+    }
+
+    public override void End ()
+    {
+        if (_mainLoopDriver is { })
+        {
+#if HACK_CHECK_WINCHANGED
+
+            _mainLoopDriver.WinChanged -= ChangeWin;
+#endif
+        }
+
+        _mainLoopDriver = null;
+
+        WinConsole?.Cleanup ();
+        WinConsole = null;
+
+        if (!RunningUnitTests && _isWindowsTerminal)
+        {
+            // Disable alternative screen buffer.
+            Console.Out.Write (EscSeqUtils.CSI_RestoreCursorAndRestoreAltBufferWithBackscroll);
+        }
+    }
+
+    public override MainLoop Init ()
+    {
+        _mainLoopDriver = new WindowsMainLoop (this);
+
+        if (!RunningUnitTests)
+        {
+            try
+            {
+                if (WinConsole is { })
+                {
+                    // BUGBUG: The results from GetConsoleOutputWindow are incorrect when called from Init.
+                    // Our thread in WindowsMainLoop.CheckWin will get the correct results. See #if HACK_CHECK_WINCHANGED
+                    Size winSize = WinConsole.GetConsoleOutputWindow (out Point _);
+                    Cols = winSize.Width;
+                    Rows = winSize.Height;
+                    OnSizeChanged (new SizeChangedEventArgs (new (Cols, Rows)));
+                }
+
+                WindowsConsole.SmallRect.MakeEmpty (ref _damageRegion);
+
+                if (_isWindowsTerminal)
+                {
+                    Console.Out.Write (EscSeqUtils.CSI_SaveCursorAndActivateAltBufferNoBackscroll);
+                }
+            }
+            catch (Win32Exception e)
+            {
+                // We are being run in an environment that does not support a console
+                // such as a unit test, or a pipe.
+                Debug.WriteLine ($"Likely running unit tests. Setting WinConsole to null so we can test it elsewhere. Exception: {e}");
+                WinConsole = null;
+            }
+        }
+
+        CurrentAttribute = new Attribute (Color.White, Color.Black);
+
+        _outputBuffer = new WindowsConsole.ExtendedCharInfo [Rows * Cols];
+        // CONCURRENCY: Unsynchronized access to Clip is not safe.
+        Clip = new (Screen);
+
+        _damageRegion = new WindowsConsole.SmallRect
+        {
+            Top = 0,
+            Left = 0,
+            Bottom = (short)Rows,
+            Right = (short)Cols
+        };
+
+        ClearContents ();
+
+#if HACK_CHECK_WINCHANGED
+        _mainLoopDriver.WinChanged = ChangeWin;
+#endif
+
+        if (!RunningUnitTests)
+        {
+            WinConsole?.SetInitialCursorVisibility ();
+        }
+
+        return new MainLoop (_mainLoopDriver);
+    }
+
+    internal void ProcessInput (WindowsConsole.InputRecord inputEvent)
+    {
+        switch (inputEvent.EventType)
+        {
+            case WindowsConsole.EventType.Key:
+                if (inputEvent.KeyEvent.wVirtualKeyCode == (VK)ConsoleKey.Packet)
+                {
+                    // Used to pass Unicode characters as if they were keystrokes.
+                    // The VK_PACKET key is the low word of a 32-bit
+                    // Virtual Key value used for non-keyboard input methods.
+                    inputEvent.KeyEvent = FromVKPacketToKeyEventRecord (inputEvent.KeyEvent);
+                }
+
+                WindowsConsole.ConsoleKeyInfoEx keyInfo = ToConsoleKeyInfoEx (inputEvent.KeyEvent);
+
+                //Debug.WriteLine ($"event: KBD: {GetKeyboardLayoutName()} {inputEvent.ToString ()} {keyInfo.ToString (keyInfo)}");
+
+                KeyCode map = MapKey (keyInfo);
+
+                if (map == KeyCode.Null)
+                {
+                    break;
+                }
+
+                if (inputEvent.KeyEvent.bKeyDown)
+                {
+                    // Avoid sending repeat key down events
+                    OnKeyDown (new Key (map));
+                }
+                else
+                {
+                    OnKeyUp (new Key (map));
+                }
+
+                break;
+
+            case WindowsConsole.EventType.Mouse:
+                MouseEventArgs me = ToDriverMouse (inputEvent.MouseEvent);
+
+                if (me.Flags == MouseFlags.None)
+                {
+                    break;
+                }
+
+                OnMouseEvent (me);
+
+                if (_processButtonClick)
+                {
+                    OnMouseEvent (new ()
+                    {
+                        Position = me.Position,
+                        Flags = ProcessButtonClick (inputEvent.MouseEvent)
+                    });
+                }
+
+                break;
+
+            case WindowsConsole.EventType.Focus:
+                break;
+
+#if !HACK_CHECK_WINCHANGED
+            case WindowsConsole.EventType.WindowBufferSize:
+
+                Cols = inputEvent.WindowBufferSizeEvent._size.X;
+                Rows = inputEvent.WindowBufferSizeEvent._size.Y;
+                Application.Screen = new (0, 0, Cols, Rows);
+
+                ResizeScreen ();
+                ClearContents ();
+                Application.Top?.SetNeedsLayout ();
+                Application.LayoutAndDraw ();
+
+                break;
+#endif
+        }
+    }
+
+#if HACK_CHECK_WINCHANGED
+    private void ChangeWin (object s, SizeChangedEventArgs e)
+    {
+        if (e.Size is null)
+        {
+            return;
+        }
+
+        int w = e.Size.Value.Width;
+
+        if (w == Cols - 3 && e.Size.Value.Height < Rows)
+        {
+            w += 3;
+        }
+
+        Left = 0;
+        Top = 0;
+        Cols = e.Size.Value.Width;
+        Rows = e.Size.Value.Height;
+
+        if (!RunningUnitTests)
+        {
+            Size newSize = WinConsole.SetConsoleWindow (
+                                                        (short)Math.Max (w, 16),
+                                                        (short)Math.Max (e.Size.Value.Height, 0));
+
+            Cols = newSize.Width;
+            Rows = newSize.Height;
+        }
+
+        ResizeScreen ();
+        ClearContents ();
+        OnSizeChanged (new SizeChangedEventArgs (new (Cols, Rows)));
+    }
+#endif
+
+    private KeyCode MapKey (WindowsConsole.ConsoleKeyInfoEx keyInfoEx)
+    {
+        ConsoleKeyInfo keyInfo = keyInfoEx.ConsoleKeyInfo;
+
+        switch (keyInfo.Key)
+        {
+            case ConsoleKey.D0:
+            case ConsoleKey.D1:
+            case ConsoleKey.D2:
+            case ConsoleKey.D3:
+            case ConsoleKey.D4:
+            case ConsoleKey.D5:
+            case ConsoleKey.D6:
+            case ConsoleKey.D7:
+            case ConsoleKey.D8:
+            case ConsoleKey.D9:
+            case ConsoleKey.NumPad0:
+            case ConsoleKey.NumPad1:
+            case ConsoleKey.NumPad2:
+            case ConsoleKey.NumPad3:
+            case ConsoleKey.NumPad4:
+            case ConsoleKey.NumPad5:
+            case ConsoleKey.NumPad6:
+            case ConsoleKey.NumPad7:
+            case ConsoleKey.NumPad8:
+            case ConsoleKey.NumPad9:
+            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.Multiply:
+            case ConsoleKey.Add:
+            case ConsoleKey.Separator:
+            case ConsoleKey.Subtract:
+            case ConsoleKey.Decimal:
+            case ConsoleKey.Divide:
+            case ConsoleKey.OemPeriod:
+            case ConsoleKey.OemComma:
+            case ConsoleKey.OemPlus:
+            case ConsoleKey.OemMinus:
+                // These virtual key codes are mapped differently depending on the keyboard layout in use.
+                // We use the Win32 API to map them to the correct character.
+                uint mapResult = MapVKtoChar ((VK)keyInfo.Key);
+
+                if (mapResult == 0)
+                {
+                    // There is no mapping - this should not happen
+                    Debug.Assert (true, $@"Unable to map the virtual key code {keyInfo.Key}.");
+
+                    return KeyCode.Null;
+                }
+
+                // An un-shifted character value is in the low order word of the return value.
+                var mappedChar = (char)(mapResult & 0x0000FFFF);
+
+                if (keyInfo.KeyChar == 0)
+                {
+                    // If the keyChar is 0, keyInfo.Key value is not a printable character.
+
+                    // Dead keys (diacritics) are indicated by setting the top bit of the return value.
+                    if ((mapResult & 0x80000000) != 0)
+                    {
+                        // Dead key (e.g. Oem2 '~'/'^' on POR keyboard)
+                        // Option 1: Throw it out.
+                        //    - Apps will never see the dead keys
+                        //    - If user presses a key that can be combined with the dead key ('a'), the right thing happens (app will see '�').
+                        //      - NOTE: With Dead Keys, KeyDown != KeyUp. The KeyUp event will have just the base char ('a').
+                        //    - If user presses dead key again, the right thing happens (app will see `~~`)
+                        //    - This is what Notepad etc... appear to do
+                        // Option 2: Expand the API to indicate the KeyCode is a dead key
+                        //    - Enables apps to do their own dead key processing
+                        //    - Adds complexity; no dev has asked for this (yet).
+                        // We choose Option 1 for now.
+                        return KeyCode.Null;
+
+                        // Note: Ctrl-Deadkey (like Oem3 '`'/'~` on ENG) can't be supported.
+                        // Sadly, the charVal is just the deadkey and subsequent key events do not contain
+                        // any info that the previous event was a deadkey.
+                        // Note WT does not support Ctrl-Deadkey either.
+                    }
+
+                    if (keyInfo.Modifiers != 0)
+                    {
+                        // These Oem keys have well-defined chars. We ensure the representative char is used.
+                        // If we don't do this, then on some keyboard layouts the wrong char is
+                        // returned (e.g. on ENG OemPlus un-shifted is =, not +). This is important
+                        // for key persistence ("Ctrl++" vs. "Ctrl+=").
+                        mappedChar = keyInfo.Key switch
+                        {
+                            ConsoleKey.OemPeriod => '.',
+                            ConsoleKey.OemComma => ',',
+                            ConsoleKey.OemPlus => '+',
+                            ConsoleKey.OemMinus => '-',
+                            _ => mappedChar
+                        };
+                    }
+
+                    // Return the mappedChar with modifiers. Because mappedChar is un-shifted, if Shift was down
+                    // we should keep it
+                    return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)mappedChar);
+                }
+
+                // KeyChar is printable
+                if (keyInfo.Modifiers.HasFlag (ConsoleModifiers.Alt) && keyInfo.Modifiers.HasFlag (ConsoleModifiers.Control))
+                {
+                    // AltGr support - AltGr is equivalent to Ctrl+Alt - the correct char is in KeyChar
+                    return (KeyCode)keyInfo.KeyChar;
+                }
+
+                if (keyInfo.Modifiers != ConsoleModifiers.Shift)
+                {
+                    // If Shift wasn't down we don't need to do anything but return the mappedChar
+                    return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)mappedChar);
+                }
+
+                // 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);
+        }
+
+        // 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.KeyChar == 0)
+            {
+                // KeyChar is not printable - possibly an AltGr key?
+                // AltGr support - AltGr is equivalent to Ctrl+Alt
+                if (keyInfo.Modifiers.HasFlag (ConsoleModifiers.Alt) && keyInfo.Modifiers.HasFlag (ConsoleModifiers.Control))
+                {
+                    return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)(uint)keyInfo.Key);
+                }
+            }
+
+            if (keyInfo.Modifiers.HasFlag (ConsoleModifiers.Alt) || keyInfo.Modifiers.HasFlag (ConsoleModifiers.Control))
+            {
+                return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)(uint)keyInfo.Key);
+            }
+
+            if ((keyInfo.Modifiers == ConsoleModifiers.Shift) ^ keyInfoEx.CapsLock)
+            {
+                // If (ShiftMask is on and CapsLock is off) or (ShiftMask is off and CapsLock is on) add the ShiftMask
+                if (char.IsUpper (keyInfo.KeyChar))
+                {
+                    if (keyInfo.KeyChar <= 'Z')
+                    {
+                        return (KeyCode)keyInfo.Key | KeyCode.ShiftMask;
+                    }
+
+                    // Always return the KeyChar because it may be an Á, À with Oem1, etc
+                    return (KeyCode)keyInfo.KeyChar;
+                }
+            }
+
+            if (keyInfo.KeyChar <= 'z')
+            {
+                return (KeyCode)keyInfo.Key;
+            }
+
+            // Always return the KeyChar because it may be an á, à with Oem1, etc
+            return (KeyCode)keyInfo.KeyChar;
+        }
+
+        // Handle control keys whose VK codes match the related ASCII value (those below ASCII 33) like ESC
+        if (Enum.IsDefined (typeof (KeyCode), (uint)keyInfo.Key))
+        {
+            // If the key is JUST a modifier, return it as just that key
+            if (keyInfo.Key == (ConsoleKey)VK.SHIFT)
+            { // Shift 16
+                return KeyCode.ShiftMask;
+            }
+
+            if (keyInfo.Key == (ConsoleKey)VK.CONTROL)
+            { // Ctrl 17
+                return KeyCode.CtrlMask;
+            }
+
+            if (keyInfo.Key == (ConsoleKey)VK.MENU)
+            { // Alt 18
+                return KeyCode.AltMask;
+            }
+
+            if (keyInfo.KeyChar == 0)
+            {
+                return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)keyInfo.KeyChar);
+            }
+
+            if (keyInfo.Key != ConsoleKey.None)
+            {
+                return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)keyInfo.KeyChar);
+            }
+
+            return MapToKeyCodeModifiers (keyInfo.Modifiers & ~ConsoleModifiers.Shift, (KeyCode)keyInfo.KeyChar);
+        }
+
+        // Handle control keys (e.g. CursorUp)
+        if (Enum.IsDefined (typeof (KeyCode), (uint)keyInfo.Key + (uint)KeyCode.MaxCodePoint))
+        {
+            return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)((uint)keyInfo.Key + (uint)KeyCode.MaxCodePoint));
+        }
+
+        return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)keyInfo.KeyChar);
+    }
+
+    private MouseFlags ProcessButtonClick (WindowsConsole.MouseEventRecord mouseEvent)
+    {
+        MouseFlags mouseFlag = 0;
+
+        switch (_lastMouseButtonPressed)
+        {
+            case WindowsConsole.ButtonState.Button1Pressed:
+                mouseFlag = MouseFlags.Button1Clicked;
+
+                break;
+
+            case WindowsConsole.ButtonState.Button2Pressed:
+                mouseFlag = MouseFlags.Button2Clicked;
+
+                break;
+
+            case WindowsConsole.ButtonState.RightmostButtonPressed:
+                mouseFlag = MouseFlags.Button3Clicked;
+
+                break;
+        }
+
+        _point = new Point
+        {
+            X = mouseEvent.MousePosition.X,
+            Y = mouseEvent.MousePosition.Y
+        };
+        _lastMouseButtonPressed = null;
+        _isButtonReleased = false;
+        _processButtonClick = false;
+        _point = null;
+
+        return mouseFlag;
+    }
+
+    private async Task ProcessButtonDoubleClickedAsync ()
+    {
+        await Task.Delay (200);
+        _isButtonDoubleClicked = false;
+        _isOneFingerDoubleClicked = false;
+
+        //buttonPressedCount = 0;
+    }
+
+    private async Task ProcessContinuousButtonPressedAsync (MouseFlags mouseFlag)
+    {
+        // When a user presses-and-holds, start generating pressed events every `startDelay`
+        // After `iterationsUntilFast` iterations, speed them up to `fastDelay` ms
+        const int START_DELAY = 500;
+        const int ITERATIONS_UNTIL_FAST = 4;
+        const int FAST_DELAY = 50;
+
+        int iterations = 0;
+        int delay = START_DELAY;
+        while (_isButtonPressed)
+        {
+            // TODO: This makes IConsoleDriver dependent on Application, which is not ideal. This should be moved to Application.
+            View? view = Application.WantContinuousButtonPressedView;
+
+            if (view is null)
+            {
+                break;
+            }
+
+            if (iterations++ >= ITERATIONS_UNTIL_FAST)
+            {
+                delay = FAST_DELAY;
+            }
+            await Task.Delay (delay);
+
+            var me = new MouseEventArgs
+            {
+                ScreenPosition = _pointMove,
+                Flags = mouseFlag
+            };
+
+            //Debug.WriteLine($"ProcessContinuousButtonPressedAsync: {view}");
+            if (_isButtonPressed && (mouseFlag & MouseFlags.ReportMousePosition) == 0)
+            {
+                // TODO: This makes IConsoleDriver dependent on Application, which is not ideal. This should be moved to Application.
+                Application.Invoke (() => OnMouseEvent (me));
+            }
+        }
+    }
+
+    private void ResizeScreen ()
+    {
+        _outputBuffer = new WindowsConsole.ExtendedCharInfo [Rows * Cols];
+        // CONCURRENCY: Unsynchronized access to Clip is not safe.
+        Clip = new (Screen);
+
+        _damageRegion = new WindowsConsole.SmallRect
+        {
+            Top = 0,
+            Left = 0,
+            Bottom = (short)Rows,
+            Right = (short)Cols
+        };
+        _dirtyLines = new bool [Rows];
+
+        WinConsole?.ForceRefreshCursorVisibility ();
+    }
+
+    private static MouseFlags SetControlKeyStates (WindowsConsole.MouseEventRecord mouseEvent, MouseFlags mouseFlag)
+    {
+        if (mouseEvent.ControlKeyState.HasFlag (WindowsConsole.ControlKeyState.RightControlPressed)
+            || mouseEvent.ControlKeyState.HasFlag (WindowsConsole.ControlKeyState.LeftControlPressed))
+        {
+            mouseFlag |= MouseFlags.ButtonCtrl;
+        }
+
+        if (mouseEvent.ControlKeyState.HasFlag (WindowsConsole.ControlKeyState.ShiftPressed))
+        {
+            mouseFlag |= MouseFlags.ButtonShift;
+        }
+
+        if (mouseEvent.ControlKeyState.HasFlag (WindowsConsole.ControlKeyState.RightAltPressed)
+            || mouseEvent.ControlKeyState.HasFlag (WindowsConsole.ControlKeyState.LeftAltPressed))
+        {
+            mouseFlag |= MouseFlags.ButtonAlt;
+        }
+
+        return mouseFlag;
+    }
+
+    [CanBeNull]
+    private MouseEventArgs ToDriverMouse (WindowsConsole.MouseEventRecord mouseEvent)
+    {
+        var mouseFlag = MouseFlags.AllEvents;
+
+        //Debug.WriteLine ($"ToDriverMouse: {mouseEvent}");
+
+        if (_isButtonDoubleClicked || _isOneFingerDoubleClicked)
+        {
+            // TODO: This makes IConsoleDriver dependent on Application, which is not ideal. This should be moved to Application.
+            Application.MainLoop!.AddIdle (
+                                           () =>
+                                           {
+                                               Task.Run (async () => await ProcessButtonDoubleClickedAsync ());
+
+                                               return false;
+                                           });
+        }
+
+        // The ButtonState member of the MouseEvent structure has bit corresponding to each mouse button.
+        // This will tell when a mouse button is pressed. When the button is released this event will
+        // be fired with its bit set to 0. So when the button is up ButtonState will be 0.
+        // To map to the correct driver events we save the last pressed mouse button, so we can
+        // map to the correct clicked event.
+        if ((_lastMouseButtonPressed is { } || _isButtonReleased) && mouseEvent.ButtonState != 0)
+        {
+            _lastMouseButtonPressed = null;
+
+            //isButtonPressed = false;
+            _isButtonReleased = false;
+        }
+
+        var p = new Point
+        {
+            X = mouseEvent.MousePosition.X,
+            Y = mouseEvent.MousePosition.Y
+        };
+
+        if ((mouseEvent.ButtonState != 0 && mouseEvent.EventFlags == 0 && _lastMouseButtonPressed is null && !_isButtonDoubleClicked)
+            || (_lastMouseButtonPressed == null
+                && mouseEvent.EventFlags.HasFlag (WindowsConsole.EventFlags.MouseMoved)
+                && mouseEvent.ButtonState != 0
+                && !_isButtonReleased
+                && !_isButtonDoubleClicked))
+        {
+            switch (mouseEvent.ButtonState)
+            {
+                case WindowsConsole.ButtonState.Button1Pressed:
+                    mouseFlag = MouseFlags.Button1Pressed;
+
+                    break;
+
+                case WindowsConsole.ButtonState.Button2Pressed:
+                    mouseFlag = MouseFlags.Button2Pressed;
+
+                    break;
+
+                case WindowsConsole.ButtonState.RightmostButtonPressed:
+                    mouseFlag = MouseFlags.Button3Pressed;
+
+                    break;
+            }
+
+            if (_point is null)
+            {
+                _point = p;
+            }
+
+            if (mouseEvent.EventFlags.HasFlag (WindowsConsole.EventFlags.MouseMoved))
+            {
+                _pointMove = p;
+                mouseFlag |= MouseFlags.ReportMousePosition;
+                _isButtonReleased = false;
+                _processButtonClick = false;
+            }
+
+            _lastMouseButtonPressed = mouseEvent.ButtonState;
+            _isButtonPressed = true;
+
+            if ((mouseFlag & MouseFlags.ReportMousePosition) == 0)
+            {
+                // TODO: This makes IConsoleDriver dependent on Application, which is not ideal. This should be moved to Application.
+                Application.MainLoop!.AddIdle (
+                                               () =>
+                                               {
+                                                   Task.Run (async () => await ProcessContinuousButtonPressedAsync (mouseFlag));
+
+                                                   return false;
+                                               });
+            }
+        }
+        else if (_lastMouseButtonPressed != null
+                 && mouseEvent.EventFlags == 0
+                 && !_isButtonReleased
+                 && !_isButtonDoubleClicked
+                 && !_isOneFingerDoubleClicked)
+        {
+            switch (_lastMouseButtonPressed)
+            {
+                case WindowsConsole.ButtonState.Button1Pressed:
+                    mouseFlag = MouseFlags.Button1Released;
+
+                    break;
+
+                case WindowsConsole.ButtonState.Button2Pressed:
+                    mouseFlag = MouseFlags.Button2Released;
+
+                    break;
+
+                case WindowsConsole.ButtonState.RightmostButtonPressed:
+                    mouseFlag = MouseFlags.Button3Released;
+
+                    break;
+            }
+
+            _isButtonPressed = false;
+            _isButtonReleased = true;
+
+            if (_point is { } && ((Point)_point).X == mouseEvent.MousePosition.X && ((Point)_point).Y == mouseEvent.MousePosition.Y)
+            {
+                _processButtonClick = true;
+            }
+            else
+            {
+                _point = null;
+            }
+            _processButtonClick = true;
+
+        }
+        else if (mouseEvent.EventFlags == WindowsConsole.EventFlags.MouseMoved
+                 && !_isOneFingerDoubleClicked
+                 && _isButtonReleased
+                 && p == _point)
+        {
+            mouseFlag = ProcessButtonClick (mouseEvent);
+        }
+        else if (mouseEvent.EventFlags.HasFlag (WindowsConsole.EventFlags.DoubleClick))
+        {
+            switch (mouseEvent.ButtonState)
+            {
+                case WindowsConsole.ButtonState.Button1Pressed:
+                    mouseFlag = MouseFlags.Button1DoubleClicked;
+
+                    break;
+
+                case WindowsConsole.ButtonState.Button2Pressed:
+                    mouseFlag = MouseFlags.Button2DoubleClicked;
+
+                    break;
+
+                case WindowsConsole.ButtonState.RightmostButtonPressed:
+                    mouseFlag = MouseFlags.Button3DoubleClicked;
+
+                    break;
+            }
+
+            _isButtonDoubleClicked = true;
+        }
+        else if (mouseEvent.EventFlags == 0 && mouseEvent.ButtonState != 0 && _isButtonDoubleClicked)
+        {
+            switch (mouseEvent.ButtonState)
+            {
+                case WindowsConsole.ButtonState.Button1Pressed:
+                    mouseFlag = MouseFlags.Button1TripleClicked;
+
+                    break;
+
+                case WindowsConsole.ButtonState.Button2Pressed:
+                    mouseFlag = MouseFlags.Button2TripleClicked;
+
+                    break;
+
+                case WindowsConsole.ButtonState.RightmostButtonPressed:
+                    mouseFlag = MouseFlags.Button3TripleClicked;
+
+                    break;
+            }
+
+            _isButtonDoubleClicked = false;
+        }
+        else if (mouseEvent.EventFlags == WindowsConsole.EventFlags.MouseWheeled)
+        {
+            switch ((int)mouseEvent.ButtonState)
+            {
+                case int v when v > 0:
+                    mouseFlag = MouseFlags.WheeledUp;
+
+                    break;
+
+                case int v when v < 0:
+                    mouseFlag = MouseFlags.WheeledDown;
+
+                    break;
+            }
+        }
+        else if (mouseEvent.EventFlags == WindowsConsole.EventFlags.MouseWheeled && mouseEvent.ControlKeyState == WindowsConsole.ControlKeyState.ShiftPressed)
+        {
+            switch ((int)mouseEvent.ButtonState)
+            {
+                case int v when v > 0:
+                    mouseFlag = MouseFlags.WheeledLeft;
+
+                    break;
+
+                case int v when v < 0:
+                    mouseFlag = MouseFlags.WheeledRight;
+
+                    break;
+            }
+        }
+        else if (mouseEvent.EventFlags == WindowsConsole.EventFlags.MouseHorizontalWheeled)
+        {
+            switch ((int)mouseEvent.ButtonState)
+            {
+                case int v when v < 0:
+                    mouseFlag = MouseFlags.WheeledLeft;
+
+                    break;
+
+                case int v when v > 0:
+                    mouseFlag = MouseFlags.WheeledRight;
+
+                    break;
+            }
+        }
+        else if (mouseEvent.EventFlags == WindowsConsole.EventFlags.MouseMoved)
+        {
+            mouseFlag = MouseFlags.ReportMousePosition;
+
+            if (mouseEvent.MousePosition.X != _pointMove.X || mouseEvent.MousePosition.Y != _pointMove.Y)
+            {
+                _pointMove = new Point (mouseEvent.MousePosition.X, mouseEvent.MousePosition.Y);
+            }
+        }
+        else if (mouseEvent is { ButtonState: 0, EventFlags: 0 })
+        {
+            // This happens on a double or triple click event.
+            mouseFlag = MouseFlags.None;
+        }
+
+        mouseFlag = SetControlKeyStates (mouseEvent, mouseFlag);
+
+        //System.Diagnostics.Debug.WriteLine (
+        //	$"point.X:{(point is { } ? ((Point)point).X : -1)};point.Y:{(point is { } ? ((Point)point).Y : -1)}");
+
+        return new MouseEventArgs
+        {
+            Position = new (mouseEvent.MousePosition.X, mouseEvent.MousePosition.Y),
+            Flags = mouseFlag
+        };
+    }
+}

+ 240 - 0
Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsMainLoop.cs

@@ -0,0 +1,240 @@
+#nullable enable
+
+using System.Collections.Concurrent;
+
+namespace Terminal.Gui;
+
+/// <summary>
+///     Mainloop intended to be used with the <see cref="WindowsDriver"/>, and can
+///     only be used on Windows.
+/// </summary>
+/// <remarks>
+///     This implementation is used for WindowsDriver.
+/// </remarks>
+internal class WindowsMainLoop : IMainLoopDriver
+{
+    /// <summary>
+    ///     Invoked when the window is changed.
+    /// </summary>
+    public EventHandler<SizeChangedEventArgs>? WinChanged;
+
+    private readonly IConsoleDriver _consoleDriver;
+    private readonly ManualResetEventSlim _eventReady = new (false);
+
+    // The records that we keep fetching
+    private readonly ConcurrentQueue<WindowsConsole.InputRecord> _resultQueue = new ();
+    private readonly ManualResetEventSlim _waitForProbe = new (false);
+    private readonly WindowsConsole? _winConsole;
+    private CancellationTokenSource _eventReadyTokenSource = new ();
+    private readonly CancellationTokenSource _inputHandlerTokenSource = new ();
+    private MainLoop? _mainLoop;
+
+    public WindowsMainLoop (IConsoleDriver consoleDriver)
+    {
+        _consoleDriver = consoleDriver ?? throw new ArgumentNullException (nameof (consoleDriver));
+
+        if (!ConsoleDriver.RunningUnitTests)
+        {
+            _winConsole = ((WindowsDriver)consoleDriver).WinConsole;
+            _winConsole!._mainLoop = this;
+        }
+    }
+
+    void IMainLoopDriver.Setup (MainLoop mainLoop)
+    {
+        _mainLoop = mainLoop;
+
+        if (ConsoleDriver.RunningUnitTests)
+        {
+            return;
+        }
+
+        Task.Run (WindowsInputHandler, _inputHandlerTokenSource.Token);
+#if HACK_CHECK_WINCHANGED
+        Task.Run (CheckWinChange);
+#endif
+    }
+
+    void IMainLoopDriver.Wakeup () { _eventReady.Set (); }
+
+    bool IMainLoopDriver.EventsPending ()
+    {
+        if (ConsoleDriver.RunningUnitTests)
+        {
+            return true;
+        }
+
+        _waitForProbe.Set ();
+#if HACK_CHECK_WINCHANGED
+        _winChange.Set ();
+#endif
+        if (_resultQueue.Count > 0 || _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
+        {
+            if (!_eventReadyTokenSource.IsCancellationRequested)
+            {
+                _eventReady.Reset ();
+            }
+        }
+
+        if (!_eventReadyTokenSource.IsCancellationRequested)
+        {
+#if HACK_CHECK_WINCHANGED
+            return _resultQueue.Count > 0 || _mainLoop.CheckTimersAndIdleHandlers (out _) || _winChanged;
+#else
+            return _resultQueue.Count > 0 || _mainLoop.CheckTimersAndIdleHandlers (out _);
+#endif
+        }
+
+        _eventReadyTokenSource.Dispose ();
+        _eventReadyTokenSource = new CancellationTokenSource ();
+
+        // If cancellation was requested then always return true
+        return true;
+    }
+
+    void IMainLoopDriver.Iteration ()
+    {
+        while (!ConsoleDriver.RunningUnitTests && _resultQueue.TryDequeue (out WindowsConsole.InputRecord inputRecords))
+        {
+            ((WindowsDriver)_consoleDriver).ProcessInput (inputRecords);
+        }
+#if HACK_CHECK_WINCHANGED
+        if (_winChanged)
+        {
+            _winChanged = false;
+            WinChanged?.Invoke (this, new SizeChangedEventArgs (_windowSize));
+        }
+#endif
+    }
+
+    void IMainLoopDriver.TearDown ()
+    {
+        _inputHandlerTokenSource.Cancel ();
+        _inputHandlerTokenSource.Dispose ();
+
+        if (_winConsole is { })
+        {
+            var numOfEvents = _winConsole.GetNumberOfConsoleInputEvents ();
+
+            if (numOfEvents > 0)
+            {
+                _winConsole.FlushConsoleInputBuffer ();
+                //Debug.WriteLine ($"Flushed {numOfEvents} events.");
+            }
+        }
+
+        _waitForProbe.Dispose ();
+
+        _resultQueue.Clear ();
+
+        _eventReadyTokenSource.Cancel ();
+        _eventReadyTokenSource.Dispose ();
+        _eventReady.Dispose ();
+
+#if HACK_CHECK_WINCHANGED
+        _winChange?.Dispose ();
+#endif
+
+        _mainLoop = null;
+    }
+
+    private void WindowsInputHandler ()
+    {
+        while (_mainLoop is { })
+        {
+            try
+            {
+                if (_inputHandlerTokenSource.IsCancellationRequested)
+                {
+                    try
+                    {
+                        _waitForProbe.Wait (_inputHandlerTokenSource.Token);
+                    }
+                    catch (Exception ex)
+                    {
+                        if (ex is OperationCanceledException or ObjectDisposedException)
+                        {
+                            return;
+                        }
+
+                        throw;
+                    }
+
+                    _waitForProbe.Reset ();
+                }
+
+                ProcessInputQueue ();
+            }
+            catch (OperationCanceledException)
+            {
+                return;
+            }
+
+        }
+    }
+
+    private void ProcessInputQueue ()
+    {
+        if (_resultQueue?.Count == 0)
+        {
+            WindowsConsole.InputRecord? result = _winConsole!.DequeueInput ();
+
+            if (result.HasValue)
+            {
+                _resultQueue!.Enqueue (result.Value);
+
+                _eventReady.Set ();
+            }
+        }
+    }
+
+#if HACK_CHECK_WINCHANGED
+    private readonly ManualResetEventSlim _winChange = new (false);
+    private bool _winChanged;
+    private Size _windowSize;
+    private void CheckWinChange ()
+    {
+        while (_mainLoop is { })
+        {
+            _winChange.Wait ();
+            _winChange.Reset ();
+
+            // Check if the window size changed every half second.
+            // We do this to minimize the weird tearing seen on Windows when resizing the console
+            while (_mainLoop is { })
+            {
+                Task.Delay (500).Wait ();
+                _windowSize = _winConsole.GetConsoleBufferWindow (out _);
+
+                if (_windowSize != Size.Empty
+                    && (_windowSize.Width != _consoleDriver.Cols
+                        || _windowSize.Height != _consoleDriver.Rows))
+                {
+                    break;
+                }
+            }
+
+            _winChanged = true;
+            _eventReady.Set ();
+        }
+    }
+#endif
+}

+ 1 - 1
Terminal.Gui/Drawing/Attribute.cs

@@ -17,7 +17,7 @@ public readonly record struct Attribute : IEqualityOperators<Attribute, Attribut
     [JsonIgnore]
     public static Attribute Default => new (Color.White, Color.Black);
 
-    /// <summary>The <see cref="ConsoleDriver"/>-specific color value.</summary>
+    /// <summary>The <see cref="IConsoleDriver"/>-specific color value.</summary>
     [JsonIgnore (Condition = JsonIgnoreCondition.Always)]
     internal int PlatformColor { get; init; }
 

+ 1 - 1
Terminal.Gui/Drawing/Cell.cs

@@ -2,7 +2,7 @@
 
 /// <summary>
 ///     Represents a single row/column in a Terminal.Gui rendering surface (e.g. <see cref="LineCanvas"/> and
-///     <see cref="ConsoleDriver"/>).
+///     <see cref="IConsoleDriver"/>).
 /// </summary>
 public record struct Cell (Attribute? Attribute = null, bool IsDirty = false, Rune Rune = default)
 {

+ 3 - 3
Terminal.Gui/Drawing/LineCanvas.cs

@@ -384,7 +384,7 @@ public class LineCanvas : IDisposable
         // TODO: Add other resolvers
     };
 
-    private Cell? GetCellForIntersects (ConsoleDriver? driver, IntersectionDefinition? [] intersects)
+    private Cell? GetCellForIntersects (IConsoleDriver? driver, IntersectionDefinition? [] intersects)
     {
         if (!intersects.Any ())
         {
@@ -404,7 +404,7 @@ public class LineCanvas : IDisposable
         return cell;
     }
 
-    private Rune? GetRuneForIntersects (ConsoleDriver? driver, IntersectionDefinition? [] intersects)
+    private Rune? GetRuneForIntersects (IConsoleDriver? driver, IntersectionDefinition? [] intersects)
     {
         if (!intersects.Any ())
         {
@@ -727,7 +727,7 @@ public class LineCanvas : IDisposable
         internal Rune _thickV;
         protected IntersectionRuneResolver () { SetGlyphs (); }
 
-        public Rune? GetRuneForIntersects (ConsoleDriver? driver, IntersectionDefinition? [] intersects)
+        public Rune? GetRuneForIntersects (IConsoleDriver? driver, IntersectionDefinition? [] intersects)
         {
             bool useRounded = intersects.Any (
                                               i => i?.Line.Length != 0

+ 1 - 1
Terminal.Gui/Drawing/SixelToRender.cs

@@ -2,7 +2,7 @@
 
 /// <summary>
 ///     Describes a request to render a given <see cref="SixelData"/> at a given <see cref="ScreenPosition"/>.
-///     Requires that the terminal and <see cref="ConsoleDriver"/> both support sixel.
+///     Requires that the terminal and <see cref="IConsoleDriver"/> both support sixel.
 /// </summary>
 public class SixelToRender
 {

+ 1 - 1
Terminal.Gui/Input/Key.cs

@@ -713,7 +713,7 @@ public class Key : EventArgs, IEquatable<Key>
 
             if (GetIsKeyCodeAtoZ (keyCode) && (keyCode & KeyCode.Space) != 0)
             {
-                keyCode = keyCode & ~KeyCode.Space;
+                keyCode &= ~KeyCode.Space;
             }
 
             key = new (keyCode | modifiers);

+ 2 - 2
Terminal.Gui/README.md

@@ -9,8 +9,8 @@ All files required to build the **Terminal.Gui** library (and NuGet package).
 	- `Application\` - The core `Application` logic, including `Application.cs`, which is is a `static` class that provides the base 'application engine', `RunState`, and `MainLoop`.
 
 - `ConsoleDrivers\`
-	- `ConsoleDriver.cs` - Definition for the Console Driver API.
-	- Source files for the three `ConsoleDriver`-based drivers: .NET: `NetDriver`, Unix & Mac: `UnixDriver`, and Windows: `WindowsDriver`.
+	- `IConsoleDriver.cs` - Definition for the Console Driver API.
+	- Source files for the three `IConsoleDriver`-based drivers: .NET: `NetDriver`, Unix & Mac: `UnixDriver`, and Windows: `WindowsDriver`.
 
 - `Configuration\` - Classes related the `ConfigurationManager`.
 

+ 2 - 2
Terminal.Gui/Text/TextFormatter.cs

@@ -43,7 +43,7 @@ public class TextFormatter
         set => _textDirection = EnableNeedsFormat (value);
     }
 
-    /// <summary>Draws the text held by <see cref="TextFormatter"/> to <see cref="ConsoleDriver"/> using the colors specified.</summary>
+    /// <summary>Draws the text held by <see cref="TextFormatter"/> to <see cref="IConsoleDriver"/> using the colors specified.</summary>
     /// <remarks>
     ///     Causes the text to be formatted (references <see cref="GetLines"/>). Sets <see cref="NeedsFormat"/> to
     ///     <c>false</c>.
@@ -59,7 +59,7 @@ public class TextFormatter
         Attribute normalColor,
         Attribute hotColor,
         Rectangle maximum = default,
-        ConsoleDriver? driver = null
+        IConsoleDriver? driver = null
     )
     {
         // With this check, we protect against subclasses with overrides of Text (like Button)

+ 8 - 1
Terminal.Gui/View/View.Layout.cs

@@ -557,7 +557,14 @@ public partial class View // Layout APIs
                 SetTitleTextFormatterSize ();
             }
 
-            SuperView?.SetNeedsDraw ();
+            if (SuperView is { })
+            {
+                SuperView?.SetNeedsDraw ();
+            }
+            else
+            {
+                Application.ClearScreenNextIteration = true;
+            }
         }
 
         if (TextFormatter.ConstrainToWidth is null)

+ 9 - 2
Terminal.Gui/View/View.cs

@@ -126,7 +126,7 @@ public partial class View : IDisposable, ISupportInitializeNotification
     ///     Points to the current driver in use by the view, it is a convenience property for simplifying the development
     ///     of new views.
     /// </summary>
-    public static ConsoleDriver? Driver => Application.Driver;
+    public static IConsoleDriver? Driver => Application.Driver;
 
     /// <summary>Initializes a new instance of <see cref="View"/>.</summary>
     /// <remarks>
@@ -369,7 +369,14 @@ public partial class View : IDisposable, ISupportInitializeNotification
             SetNeedsLayout ();
             SuperView?.SetNeedsLayout ();
             SetNeedsDraw ();
-            SuperView?.SetNeedsDraw ();
+            if (SuperView is { })
+            {
+                SuperView?.SetNeedsDraw ();
+            }
+            else
+            {
+                Application.ClearScreenNextIteration = true;
+            }
         }
     }
 

+ 1 - 1
Terminal.Gui/Views/ColorPicker.Prompt.cs

@@ -4,7 +4,7 @@ public partial class ColorPicker
 {
     /// <summary>
     ///     Open a <see cref="Dialog"/> with two <see cref="ColorPicker"/> or <see cref="ColorPicker16"/>, based on the
-    ///     <see cref="ConsoleDriver.Force16Colors"/> is false or true, respectively, for <see cref="Attribute.Foreground"/>
+    ///     <see cref="IConsoleDriver.Force16Colors"/> is false or true, respectively, for <see cref="Attribute.Foreground"/>
     ///     and <see cref="Attribute.Background"/> colors.
     /// </summary>
     /// <param name="title">The title to show in the dialog.</param>

+ 1 - 1
Terminal.Gui/Views/GraphView/Axis.cs

@@ -97,7 +97,7 @@ public class HorizontalAxis : Axis
     /// <param name="text">Text to render under the axis tick</param>
     public override void DrawAxisLabel (GraphView graph, int screenPosition, string text)
     {
-        ConsoleDriver driver = Application.Driver;
+        IConsoleDriver driver = Application.Driver;
         int y = GetAxisYPosition (graph);
 
         graph.Move (screenPosition, y);

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

@@ -608,8 +608,7 @@ internal sealed class Menu : View
 
         Application.UngrabMouse ();
         _host.CloseAllMenus ();
-        Application.Driver!.ClearContents ();
-        Application.LayoutAndDraw ();
+        Application.LayoutAndDraw (true);
 
         _host.Run (action);
     }

+ 2 - 2
Terminal.Gui/Views/Menu/MenuBar.cs

@@ -667,7 +667,7 @@ public class MenuBar : View, IDesignable
         return true;
     }
 
-    /// <summary>Gets the superview location offset relative to the <see cref="ConsoleDriver"/> location.</summary>
+    /// <summary>Gets the superview location offset relative to the <see cref="IConsoleDriver"/> location.</summary>
     /// <returns>The location offset.</returns>
     internal Point GetScreenOffset ()
     {
@@ -1117,7 +1117,7 @@ public class MenuBar : View, IDesignable
 
         Application.UngrabMouse ();
         CloseAllMenus ();
-        Application.LayoutAndDraw ();
+        Application.LayoutAndDraw (true);
         _openedByAltKey = true;
 
         return Run (item.Action);

+ 1 - 1
Terminal.Gui/Views/TableView/TableStyle.cs

@@ -29,7 +29,7 @@ public class TableStyle
 
     /// <summary>
     ///     True to invert the colors of the first symbol of the selected cell in the <see cref="TableView"/>. This gives
-    ///     the appearance of a cursor for when the <see cref="ConsoleDriver"/> doesn't otherwise show this
+    ///     the appearance of a cursor for when the <see cref="IConsoleDriver"/> doesn't otherwise show this
     /// </summary>
     public bool InvertSelectedCellFirstCharacter { get; set; } = false;
 

+ 2 - 2
Terminal.Gui/Views/TableView/TableView.cs

@@ -1277,7 +1277,7 @@ public class TableView : View, IDesignable
 
     /// <summary>
     ///     Override to provide custom multi colouring to cells.  Use <see cref="View.Driver"/> to with
-    ///     <see cref="ConsoleDriver.AddStr(string)"/>.  The driver will already be in the correct place when rendering and you
+    ///     <see cref="IConsoleDriver.AddStr(string)"/>.  The driver will already be in the correct place when rendering and you
     ///     must render the full <paramref name="render"/> or the view will not look right.  For simpler provision of color use
     ///     <see cref="ColumnStyle.ColorGetter"/> For changing the content that is rendered use
     ///     <see cref="ColumnStyle.RepresentationGetter"/>
@@ -1335,7 +1335,7 @@ public class TableView : View, IDesignable
     /// <returns></returns>
     internal int GetHeaderHeightIfAny () { return ShouldRenderHeaders () ? GetHeaderHeight () : 0; }
 
-    private void AddRuneAt (ConsoleDriver d, int col, int row, Rune ch)
+    private void AddRuneAt (IConsoleDriver d, int col, int row, Rune ch)
     {
         Move (col, row);
         d?.AddRune (ch);

+ 5 - 5
Terminal.Gui/Views/TreeView/Branch.cs

@@ -73,7 +73,7 @@ internal class Branch<T> where T : class
     /// <param name="colorScheme"></param>
     /// <param name="y"></param>
     /// <param name="availableWidth"></param>
-    public virtual void Draw (ConsoleDriver driver, ColorScheme colorScheme, int y, int availableWidth)
+    public virtual void Draw (IConsoleDriver driver, ColorScheme colorScheme, int y, int availableWidth)
     {
         List<Cell> cells = new ();
         int? indexOfExpandCollapseSymbol = null;
@@ -291,7 +291,7 @@ internal class Branch<T> where T : class
     /// </summary>
     /// <param name="driver"></param>
     /// <returns></returns>
-    public Rune GetExpandableSymbol (ConsoleDriver driver)
+    public Rune GetExpandableSymbol (IConsoleDriver driver)
     {
         Rune leafSymbol = tree.Style.ShowBranchLines ? Glyphs.HLine : (Rune)' ';
 
@@ -313,7 +313,7 @@ internal class Branch<T> where T : class
     ///     line body).
     /// </summary>
     /// <returns></returns>
-    public virtual int GetWidth (ConsoleDriver driver)
+    public virtual int GetWidth (IConsoleDriver driver)
     {
         return
             GetLinePrefix (driver).Sum (r => r.GetColumns ()) + GetExpandableSymbol (driver).GetColumns () + (tree.AspectGetter (Model) ?? "").Length;
@@ -408,7 +408,7 @@ internal class Branch<T> where T : class
     /// </summary>
     /// <param name="driver"></param>
     /// <returns></returns>
-    internal IEnumerable<Rune> GetLinePrefix (ConsoleDriver driver)
+    internal IEnumerable<Rune> GetLinePrefix (IConsoleDriver driver)
     {
         // If not showing line branches or this is a root object.
         if (!tree.Style.ShowBranchLines)
@@ -453,7 +453,7 @@ internal class Branch<T> where T : class
     /// <param name="driver"></param>
     /// <param name="x"></param>
     /// <returns></returns>
-    internal bool IsHitOnExpandableSymbol (ConsoleDriver driver, int x)
+    internal bool IsHitOnExpandableSymbol (IConsoleDriver driver, int x)
     {
         // if leaf node then we cannot expand
         if (!CanExpand ())

+ 1 - 1
UICatalog/Scenarios/GraphViewExample.cs

@@ -1000,7 +1000,7 @@ public class GraphViewExample : Scenario
 
         protected override void DrawBarLine (GraphView graph, Point start, Point end, BarSeriesBar beingDrawn)
         {
-            ConsoleDriver driver = Application.Driver;
+            IConsoleDriver driver = Application.Driver;
 
             int x = start.X;
 

+ 1 - 1
UICatalog/UICatalog.cs

@@ -136,7 +136,7 @@ public class UICatalogApp
         // Process command line args
         // "UICatalog [--driver <driver>] [--benchmark] [scenario name]"
         // If no driver is provided, the default driver is used.
-        Option<string> driverOption = new Option<string> ("--driver", "The ConsoleDriver to use.").FromAmong (
+        Option<string> driverOption = new Option<string> ("--driver", "The IConsoleDriver to use.").FromAmong (
              Application.GetDriverTypes ()
                         .Select (d => d!.Name)
                         .ToArray ()

+ 68 - 0
UnitTests/Application/ApplicationScreenTests.cs

@@ -0,0 +1,68 @@
+using Xunit.Abstractions;
+
+namespace Terminal.Gui.ApplicationTests;
+
+public class ApplicationScreenTests (ITestOutputHelper output)
+{
+    [Fact]
+    public void ClearScreenNextIteration_Resets_To_False_After_LayoutAndDraw ()
+    {
+        // Arrange
+        Application.Init ();
+
+        // Act
+        Application.ClearScreenNextIteration = true;
+        Application.LayoutAndDraw ();
+
+        // Assert
+        Assert.False (Application.ClearScreenNextIteration);
+
+        // Cleanup
+        Application.ResetState (true);
+    }
+
+    [Fact]
+    public void ClearContents_Called_When_Top_Frame_Changes ()
+    {
+        // Arrange
+        Application.Init (new FakeDriver ());
+        Application.Top = new Toplevel ();
+        Application.TopLevels.Push (Application.Top);
+
+        int clearedContentsRaised = 0;
+
+        Application.Driver!.ClearedContents += (e, a) => clearedContentsRaised++;
+
+        // Act
+        Application.LayoutAndDraw ();
+
+        // Assert
+        Assert.Equal (1, clearedContentsRaised);
+
+        // Act
+        Application.Top.SetNeedsLayout ();
+        Application.LayoutAndDraw ();
+
+        // Assert
+        Assert.Equal (1, clearedContentsRaised);
+
+        // Act
+        Application.Top.X = 1;
+        Application.LayoutAndDraw ();
+
+        // Assert
+        Assert.Equal (2, clearedContentsRaised);
+
+        // Act
+        Application.Top.Width = 10;
+        Application.LayoutAndDraw ();
+
+        // Assert
+        Assert.Equal (3, clearedContentsRaised);
+
+        // Cleanup
+        Application.Top.Dispose ();
+        Application.Top = null;
+        Application.Shutdown ();
+    }
+}

+ 22 - 1
UnitTests/Application/ApplicationTests.cs

@@ -248,7 +248,7 @@ public class ApplicationTests
     [InlineData (typeof (CursesDriver))]
     public void Init_DriverName_Should_Pick_Correct_Driver (Type driverType)
     {
-        var driver = (ConsoleDriver)Activator.CreateInstance (driverType);
+        var driver = (IConsoleDriver)Activator.CreateInstance (driverType);
         Application.Init (driverName: driverType.Name);
         Assert.NotNull (Application.Driver);
         Assert.NotEqual (driver, Application.Driver);
@@ -620,6 +620,27 @@ public class ApplicationTests
         }
     }
 
+    [Fact]
+    public void Screen_Size_Changes ()
+    {
+        var driver = new FakeDriver ();
+        Application.Init (driver);
+        Assert.Equal (new (0, 0, 80, 25), driver.Screen);
+        Assert.Equal (new (0, 0, 80, 25), Application.Screen);
+
+        driver.Cols = 100;
+        driver.Rows = 30;
+        // IConsoleDriver.Screen isn't assignable
+        //driver.Screen = new (0, 0, driver.Cols, Rows);
+        Assert.Equal (new (0, 0, 100, 30), driver.Screen);
+        Assert.NotEqual (new (0, 0, 100, 30), Application.Screen);
+        Assert.Equal (new (0, 0, 80, 25), Application.Screen);
+        Application.Screen = new (0, 0, driver.Cols, driver.Rows);
+        Assert.Equal (new (0, 0, 100, 30), driver.Screen);
+
+        Application.Shutdown ();
+    }
+
     private void Init ()
     {
         Application.Init (new FakeDriver ());

+ 6 - 3
UnitTests/Application/SynchronizatonContextTests.cs

@@ -4,9 +4,10 @@ namespace Terminal.Gui.ApplicationTests;
 
 public class SyncrhonizationContextTests
 {
-    [Fact(Skip = "Causes ubuntu to crash in github action.")]
+    [Fact]
     public void SynchronizationContext_CreateCopy ()
     {
+        ConsoleDriver.RunningUnitTests = true;
         Application.Init ();
         SynchronizationContext context = SynchronizationContext.Current;
         Assert.NotNull (context);
@@ -20,11 +21,12 @@ public class SyncrhonizationContextTests
 
     [Theory]
     [InlineData (typeof (FakeDriver))]
-    //[InlineData (typeof (NetDriver))]
+    [InlineData (typeof (NetDriver))]
     [InlineData (typeof (WindowsDriver))]
-    //[InlineData (typeof (CursesDriver))]
+    [InlineData (typeof (CursesDriver))]
     public void SynchronizationContext_Post (Type driverType)
     {
+        ConsoleDriver.RunningUnitTests = true;
         Application.Init (driverName: driverType.Name);
         SynchronizationContext context = SynchronizationContext.Current;
 
@@ -60,6 +62,7 @@ public class SyncrhonizationContextTests
     [AutoInitShutdown]
     public void SynchronizationContext_Send ()
     {
+        ConsoleDriver.RunningUnitTests = true;
         Application.Init ();
         SynchronizationContext context = SynchronizationContext.Current;
 

+ 2 - 0
UnitTests/Configuration/ConfigPropertyTests.cs

@@ -5,6 +5,8 @@ using System.Text.Json.Serialization;
 using Terminal.Gui;
 using Xunit;
 
+namespace Terminal.Gui.ConfigurationTests;
+
 public class ConfigPropertyTests
 {
     [Fact]

+ 1 - 1
UnitTests/ConsoleDrivers/AddRuneTests.cs

@@ -25,7 +25,7 @@ public class AddRuneTests
     [InlineData (typeof (CursesDriver))]
     public void AddRune (Type driverType)
     {
-        var driver = (ConsoleDriver)Activator.CreateInstance (driverType);
+        var driver = (IConsoleDriver)Activator.CreateInstance (driverType);
         driver.Init ();
 
         driver.Rows = 25;

+ 3 - 3
UnitTests/ConsoleDrivers/ClipRegionTests.cs

@@ -24,7 +24,7 @@ public class ClipRegionTests
     [InlineData (typeof (CursesDriver))]
     public void AddRune_Is_Clipped (Type driverType)
     {
-        var driver = (ConsoleDriver)Activator.CreateInstance (driverType);
+        var driver = (IConsoleDriver)Activator.CreateInstance (driverType);
         Application.Init (driver);
         Application.Driver!.Rows = 25;
         Application.Driver!.Cols = 80;
@@ -62,7 +62,7 @@ public class ClipRegionTests
     [InlineData (typeof (CursesDriver))]
     public void Clip_Set_To_Empty_AllInvalid (Type driverType)
     {
-        var driver = (ConsoleDriver)Activator.CreateInstance (driverType);
+        var driver = (IConsoleDriver)Activator.CreateInstance (driverType);
         Application.Init (driver);
 
         // Define a clip rectangle
@@ -92,7 +92,7 @@ public class ClipRegionTests
     [InlineData (typeof (CursesDriver))]
     public void IsValidLocation (Type driverType)
     {
-        var driver = (ConsoleDriver)Activator.CreateInstance (driverType);
+        var driver = (IConsoleDriver)Activator.CreateInstance (driverType);
         Application.Init (driver);
         Application.Driver!.Rows = 10;
         Application.Driver!.Cols = 10;

+ 6 - 6
UnitTests/ConsoleDrivers/ConsoleDriverTests.cs

@@ -25,7 +25,7 @@ public class ConsoleDriverTests
     [InlineData (typeof (CursesDriver))]
     public void End_Cleans_Up (Type driverType)
     {
-        var driver = (ConsoleDriver)Activator.CreateInstance (driverType);
+        var driver = (IConsoleDriver)Activator.CreateInstance (driverType);
         driver.Init ();
         driver.End ();
     }
@@ -34,7 +34,7 @@ public class ConsoleDriverTests
     [InlineData (typeof (FakeDriver))]
     public void FakeDriver_MockKeyPresses (Type driverType)
     {
-        var driver = (ConsoleDriver)Activator.CreateInstance (driverType);
+        var driver = (IConsoleDriver)Activator.CreateInstance (driverType);
         Application.Init (driver);
 
         var text = "MockKeyPresses";
@@ -85,7 +85,7 @@ public class ConsoleDriverTests
     [InlineData (typeof (FakeDriver))]
     public void FakeDriver_Only_Sends_Keystrokes_Through_MockKeyPresses (Type driverType)
     {
-        var driver = (ConsoleDriver)Activator.CreateInstance (driverType);
+        var driver = (IConsoleDriver)Activator.CreateInstance (driverType);
         Application.Init (driver);
 
         Toplevel top = new ();
@@ -124,7 +124,7 @@ public class ConsoleDriverTests
     [InlineData (typeof (CursesDriver))]
     public void Init_Inits (Type driverType)
     {
-        var driver = (ConsoleDriver)Activator.CreateInstance (driverType);
+        var driver = (IConsoleDriver)Activator.CreateInstance (driverType);
         MainLoop ml = driver.Init ();
         Assert.NotNull (ml);
         Assert.NotNull (driver.Clipboard);
@@ -140,7 +140,7 @@ public class ConsoleDriverTests
     //[InlineData (typeof (FakeDriver))]
     //public void FakeDriver_MockKeyPresses_Press_AfterTimeOut (Type driverType)
     //{
-    //	var driver = (ConsoleDriver)Activator.CreateInstance (driverType);
+    //	var driver = (IConsoleDriver)Activator.CreateInstance (driverType);
     //	Application.Init (driver);
 
     //	// Simulating pressing of QuitKey after a short period of time
@@ -201,7 +201,7 @@ public class ConsoleDriverTests
     [InlineData (typeof (CursesDriver))]
     public void TerminalResized_Simulation (Type driverType)
     {
-        var driver = (ConsoleDriver)Activator.CreateInstance (driverType);
+        var driver = (IConsoleDriver)Activator.CreateInstance (driverType);
         driver?.Init ();
         driver.Cols = 80;
         driver.Rows = 25;

+ 3 - 3
UnitTests/ConsoleDrivers/ContentsTests.cs

@@ -24,7 +24,7 @@ public class ContentsTests
     //[InlineData (typeof (WindowsDriver))] // TODO: Uncomment when #2610 is fixed
     public void AddStr_Combining_Character_1st_Column (Type driverType)
     {
-        var driver = (ConsoleDriver)Activator.CreateInstance (driverType);
+        var driver = (IConsoleDriver)Activator.CreateInstance (driverType);
         driver.Init ();
         var expected = "\u0301!";
         driver.AddStr ("\u0301!"); // acute accent + exclamation mark
@@ -42,7 +42,7 @@ public class ContentsTests
     //[InlineData (typeof (WindowsDriver))] // TODO: Uncomment when #2610 is fixed
     public void AddStr_With_Combining_Characters (Type driverType)
     {
-        var driver = (ConsoleDriver)Activator.CreateInstance (driverType);
+        var driver = (IConsoleDriver)Activator.CreateInstance (driverType);
         driver.Init ();
 
         var acuteaccent = new Rune (0x0301); // Combining acute accent (é)
@@ -98,7 +98,7 @@ public class ContentsTests
     [InlineData (typeof (CursesDriver))]
     public void Move_Bad_Coordinates (Type driverType)
     {
-        var driver = (ConsoleDriver)Activator.CreateInstance (driverType);
+        var driver = (IConsoleDriver)Activator.CreateInstance (driverType);
         driver.Init ();
 
         Assert.Equal (0, driver.Col);

+ 3 - 3
UnitTests/ConsoleDrivers/DriverColorTests.cs

@@ -17,7 +17,7 @@ public class DriverColorTests
     [InlineData (typeof (CursesDriver))]
     public void Force16Colors_Sets (Type driverType)
     {
-        var driver = (ConsoleDriver)Activator.CreateInstance (driverType);
+        var driver = (IConsoleDriver)Activator.CreateInstance (driverType);
         driver.Init ();
 
         driver.Force16Colors = true;
@@ -35,7 +35,7 @@ public class DriverColorTests
     [InlineData (typeof (CursesDriver))]
     public void SetColors_Changes_Colors (Type driverType)
     {
-        var driver = (ConsoleDriver)Activator.CreateInstance (driverType);
+        var driver = (IConsoleDriver)Activator.CreateInstance (driverType);
         driver.Init ();
 
         Assert.Equal (ConsoleColor.Gray, Console.ForegroundColor);
@@ -63,7 +63,7 @@ public class DriverColorTests
     [InlineData (typeof (CursesDriver), true)]
     public void SupportsTrueColor_Defaults (Type driverType, bool expectedSetting)
     {
-        var driver = (ConsoleDriver)Activator.CreateInstance (driverType);
+        var driver = (IConsoleDriver)Activator.CreateInstance (driverType);
         driver.Init ();
 
         Assert.Equal (expectedSetting, driver.SupportsTrueColor);

+ 12 - 12
UnitTests/ConsoleDrivers/MainLoopDriverTests.cs

@@ -17,7 +17,7 @@ public class MainLoopDriverTests
     //[InlineData (typeof (ANSIDriver), typeof (AnsiMainLoopDriver))]
     public void MainLoop_AddIdle_ValidIdleHandler_ReturnsToken (Type driverType, Type mainLoopDriverType)
     {
-        var driver = (ConsoleDriver)Activator.CreateInstance (driverType);
+        var driver = (IConsoleDriver)Activator.CreateInstance (driverType);
         var mainLoopDriver = (IMainLoopDriver)Activator.CreateInstance (mainLoopDriverType, driver);
         var mainLoop = new MainLoop (mainLoopDriver);
         var idleHandlerInvoked = false;
@@ -47,7 +47,7 @@ public class MainLoopDriverTests
     //[InlineData (typeof (ANSIDriver), typeof (AnsiMainLoopDriver))]
     public void MainLoop_AddTimeout_ValidParameters_ReturnsToken (Type driverType, Type mainLoopDriverType)
     {
-        var driver = (ConsoleDriver)Activator.CreateInstance (driverType);
+        var driver = (IConsoleDriver)Activator.CreateInstance (driverType);
         var mainLoopDriver = (IMainLoopDriver)Activator.CreateInstance (mainLoopDriverType, driver);
         var mainLoop = new MainLoop (mainLoopDriver);
         var callbackInvoked = false;
@@ -83,7 +83,7 @@ public class MainLoopDriverTests
         Type mainLoopDriverType
     )
     {
-        var driver = (ConsoleDriver)Activator.CreateInstance (driverType);
+        var driver = (IConsoleDriver)Activator.CreateInstance (driverType);
         var mainLoopDriver = (IMainLoopDriver)Activator.CreateInstance (mainLoopDriverType, driver);
         var mainLoop = new MainLoop (mainLoopDriver);
 
@@ -107,7 +107,7 @@ public class MainLoopDriverTests
         Type mainLoopDriverType
     )
     {
-        var driver = (ConsoleDriver)Activator.CreateInstance (driverType);
+        var driver = (IConsoleDriver)Activator.CreateInstance (driverType);
         var mainLoopDriver = (IMainLoopDriver)Activator.CreateInstance (mainLoopDriverType, driver);
         var mainLoop = new MainLoop (mainLoopDriver);
 
@@ -130,7 +130,7 @@ public class MainLoopDriverTests
         Type mainLoopDriverType
     )
     {
-        var driver = (ConsoleDriver)Activator.CreateInstance (driverType);
+        var driver = (IConsoleDriver)Activator.CreateInstance (driverType);
         var mainLoopDriver = (IMainLoopDriver)Activator.CreateInstance (mainLoopDriverType, driver);
         var mainLoop = new MainLoop (mainLoopDriver);
 
@@ -151,7 +151,7 @@ public class MainLoopDriverTests
     //[InlineData (typeof (ANSIDriver), typeof (AnsiMainLoopDriver))]
     public void MainLoop_Constructs_Disposes (Type driverType, Type mainLoopDriverType)
     {
-        var driver = (ConsoleDriver)Activator.CreateInstance (driverType);
+        var driver = (IConsoleDriver)Activator.CreateInstance (driverType);
         var mainLoopDriver = (IMainLoopDriver)Activator.CreateInstance (mainLoopDriverType, driver);
         var mainLoop = new MainLoop (mainLoopDriver);
 
@@ -182,7 +182,7 @@ public class MainLoopDriverTests
     //[InlineData (typeof (ANSIDriver), typeof (AnsiMainLoopDriver))]
     public void MainLoop_RemoveIdle_InvalidToken_ReturnsFalse (Type driverType, Type mainLoopDriverType)
     {
-        var driver = (ConsoleDriver)Activator.CreateInstance (driverType);
+        var driver = (IConsoleDriver)Activator.CreateInstance (driverType);
         var mainLoopDriver = (IMainLoopDriver)Activator.CreateInstance (mainLoopDriverType, driver);
         var mainLoop = new MainLoop (mainLoopDriver);
 
@@ -201,7 +201,7 @@ public class MainLoopDriverTests
     //[InlineData (typeof (ANSIDriver), typeof (AnsiMainLoopDriver))]
     public void MainLoop_RemoveIdle_ValidToken_ReturnsTrue (Type driverType, Type mainLoopDriverType)
     {
-        var driver = (ConsoleDriver)Activator.CreateInstance (driverType);
+        var driver = (IConsoleDriver)Activator.CreateInstance (driverType);
         var mainLoopDriver = (IMainLoopDriver)Activator.CreateInstance (mainLoopDriverType, driver);
         var mainLoop = new MainLoop (mainLoopDriver);
 
@@ -223,7 +223,7 @@ public class MainLoopDriverTests
     //[InlineData (typeof (ANSIDriver), typeof (AnsiMainLoopDriver))]
     public void MainLoop_RemoveTimeout_InvalidToken_ReturnsFalse (Type driverType, Type mainLoopDriverType)
     {
-        var driver = (ConsoleDriver)Activator.CreateInstance (driverType);
+        var driver = (IConsoleDriver)Activator.CreateInstance (driverType);
         var mainLoopDriver = (IMainLoopDriver)Activator.CreateInstance (mainLoopDriverType, driver);
         var mainLoop = new MainLoop (mainLoopDriver);
 
@@ -241,7 +241,7 @@ public class MainLoopDriverTests
     //[InlineData (typeof (ANSIDriver), typeof (AnsiMainLoopDriver))]
     public void MainLoop_RemoveTimeout_ValidToken_ReturnsTrue (Type driverType, Type mainLoopDriverType)
     {
-        var driver = (ConsoleDriver)Activator.CreateInstance (driverType);
+        var driver = (IConsoleDriver)Activator.CreateInstance (driverType);
         var mainLoopDriver = (IMainLoopDriver)Activator.CreateInstance (mainLoopDriverType, driver);
         var mainLoop = new MainLoop (mainLoopDriver);
 
@@ -261,7 +261,7 @@ public class MainLoopDriverTests
     //[InlineData (typeof (ANSIDriver), typeof (AnsiMainLoopDriver))]
     public void MainLoop_RunIteration_ValidIdleHandler_CallsIdleHandler (Type driverType, Type mainLoopDriverType)
     {
-        var driver = (ConsoleDriver)Activator.CreateInstance (driverType);
+        var driver = (IConsoleDriver)Activator.CreateInstance (driverType);
         var mainLoopDriver = (IMainLoopDriver)Activator.CreateInstance (mainLoopDriverType, driver);
         var mainLoop = new MainLoop (mainLoopDriver);
         var idleHandlerInvoked = false;
@@ -287,7 +287,7 @@ public class MainLoopDriverTests
     //[InlineData (typeof (WindowsDriver), typeof (WindowsMainLoop))]
     //public void MainLoop_Invoke_ValidAction_RunsAction (Type driverType, Type mainLoopDriverType)
     //{
-    //	var driver = (ConsoleDriver)Activator.CreateInstance (driverType);
+    //	var driver = (IConsoleDriver)Activator.CreateInstance (driverType);
     //	var mainLoopDriver = (IMainLoopDriver)Activator.CreateInstance (mainLoopDriverType, new object [] { driver });
     //	var mainLoop = new MainLoop (mainLoopDriver);
     //	var actionInvoked = false;

+ 1 - 1
UnitTests/Drawing/SixelEncoderTests.cs

@@ -1,6 +1,6 @@
 using Color = Terminal.Gui.Color;
 
-namespace UnitTests.Drawing;
+namespace Terminal.Gui.DrawingTests;
 
 public class SixelEncoderTests
 {

+ 0 - 72
UnitTests/Input/EscSeqReqTests.cs

@@ -1,72 +0,0 @@
-namespace Terminal.Gui.InputTests;
-
-public class EscSeqReqTests
-{
-    [Fact]
-    public void Add_Tests ()
-    {
-        var escSeqReq = new EscSeqRequests ();
-        escSeqReq.Add ("t");
-        Assert.Single (escSeqReq.Statuses);
-        Assert.Equal ("t", escSeqReq.Statuses [^1].Terminator);
-        Assert.Equal (1, escSeqReq.Statuses [^1].NumRequests);
-        Assert.Equal (1, escSeqReq.Statuses [^1].NumOutstanding);
-
-        escSeqReq.Add ("t", 2);
-        Assert.Single (escSeqReq.Statuses);
-        Assert.Equal ("t", escSeqReq.Statuses [^1].Terminator);
-        Assert.Equal (1, escSeqReq.Statuses [^1].NumRequests);
-        Assert.Equal (1, escSeqReq.Statuses [^1].NumOutstanding);
-
-        escSeqReq = new EscSeqRequests ();
-        escSeqReq.Add ("t", 2);
-        Assert.Single (escSeqReq.Statuses);
-        Assert.Equal ("t", escSeqReq.Statuses [^1].Terminator);
-        Assert.Equal (2, escSeqReq.Statuses [^1].NumRequests);
-        Assert.Equal (2, escSeqReq.Statuses [^1].NumOutstanding);
-
-        escSeqReq.Add ("t", 3);
-        Assert.Single (escSeqReq.Statuses);
-        Assert.Equal ("t", escSeqReq.Statuses [^1].Terminator);
-        Assert.Equal (2, escSeqReq.Statuses [^1].NumRequests);
-        Assert.Equal (2, escSeqReq.Statuses [^1].NumOutstanding);
-    }
-
-    [Fact]
-    public void Constructor_Defaults ()
-    {
-        var escSeqReq = new EscSeqRequests ();
-        Assert.NotNull (escSeqReq.Statuses);
-        Assert.Empty (escSeqReq.Statuses);
-    }
-
-    [Fact]
-    public void Remove_Tests ()
-    {
-        var escSeqReq = new EscSeqRequests ();
-        escSeqReq.Add ("t");
-        escSeqReq.Remove ("t");
-        Assert.Empty (escSeqReq.Statuses);
-
-        escSeqReq.Add ("t", 2);
-        escSeqReq.Remove ("t");
-        Assert.Single (escSeqReq.Statuses);
-        Assert.Equal ("t", escSeqReq.Statuses [^1].Terminator);
-        Assert.Equal (2, escSeqReq.Statuses [^1].NumRequests);
-        Assert.Equal (1, escSeqReq.Statuses [^1].NumOutstanding);
-
-        escSeqReq.Remove ("t");
-        Assert.Empty (escSeqReq.Statuses);
-    }
-
-    [Fact]
-    public void Requested_Tests ()
-    {
-        var escSeqReq = new EscSeqRequests ();
-        Assert.False (escSeqReq.HasResponse ("t"));
-
-        escSeqReq.Add ("t");
-        Assert.False (escSeqReq.HasResponse ("r"));
-        Assert.True (escSeqReq.HasResponse ("t"));
-    }
-}

+ 188 - 0
UnitTests/Input/EscSeqRequestsTests.cs

@@ -0,0 +1,188 @@
+namespace Terminal.Gui.InputTests;
+
+public class EscSeqRequestsTests
+{
+    [Fact]
+    public void Add_Tests ()
+    {
+        EscSeqRequests.Add ("t");
+        Assert.Single (EscSeqRequests.Statuses);
+        Assert.Equal ("t", EscSeqRequests.Statuses [^1].Terminator);
+        Assert.Equal (1, EscSeqRequests.Statuses [^1].NumRequests);
+        Assert.Equal (1, EscSeqRequests.Statuses [^1].NumOutstanding);
+
+        EscSeqRequests.Add ("t", 2);
+        Assert.Single (EscSeqRequests.Statuses);
+        Assert.Equal ("t", EscSeqRequests.Statuses [^1].Terminator);
+        Assert.Equal (3, EscSeqRequests.Statuses [^1].NumRequests);
+        Assert.Equal (3, EscSeqRequests.Statuses [^1].NumOutstanding);
+
+        EscSeqRequests.Clear ();
+        EscSeqRequests.Add ("t", 2);
+        Assert.Single (EscSeqRequests.Statuses);
+        Assert.Equal ("t", EscSeqRequests.Statuses [^1].Terminator);
+        Assert.Equal (2, EscSeqRequests.Statuses [^1].NumRequests);
+        Assert.Equal (2, EscSeqRequests.Statuses [^1].NumOutstanding);
+
+        EscSeqRequests.Add ("t", 3);
+        Assert.Single (EscSeqRequests.Statuses);
+        Assert.Equal ("t", EscSeqRequests.Statuses [^1].Terminator);
+        Assert.Equal (5, EscSeqRequests.Statuses [^1].NumRequests);
+        Assert.Equal (5, EscSeqRequests.Statuses [^1].NumOutstanding);
+
+        EscSeqRequests.Clear ();
+    }
+
+    [Fact]
+    public void Constructor_Defaults ()
+    {
+        Assert.NotNull (EscSeqRequests.Statuses);
+        Assert.Empty (EscSeqRequests.Statuses);
+    }
+
+    [Fact]
+    public void Remove_Tests ()
+    {
+        EscSeqRequests.Add ("t");
+        EscSeqRequests.Remove ("t");
+        Assert.Empty (EscSeqRequests.Statuses);
+
+        EscSeqRequests.Add ("t", 2);
+        EscSeqRequests.Remove ("t");
+        Assert.Single (EscSeqRequests.Statuses);
+        Assert.Equal ("t", EscSeqRequests.Statuses [^1].Terminator);
+        Assert.Equal (2, EscSeqRequests.Statuses [^1].NumRequests);
+        Assert.Equal (1, EscSeqRequests.Statuses [^1].NumOutstanding);
+
+        EscSeqRequests.Remove ("t");
+        Assert.Empty (EscSeqRequests.Statuses);
+
+        EscSeqRequests.Clear ();
+    }
+
+    [Fact]
+    public void HasResponse_Tests ()
+    {
+        Assert.False (EscSeqRequests.HasResponse ("t"));
+
+        EscSeqRequests.Add ("t");
+        Assert.False (EscSeqRequests.HasResponse ("r"));
+        Assert.True (EscSeqRequests.HasResponse ("t"));
+        Assert.Single (EscSeqRequests.Statuses);
+        Assert.Equal ("t", EscSeqRequests.Statuses [^1].Terminator);
+        Assert.Equal (1, EscSeqRequests.Statuses [^1].NumRequests);
+        Assert.Equal (1, EscSeqRequests.Statuses [^1].NumOutstanding);
+
+        EscSeqRequests.Remove ("t");
+        Assert.Empty (EscSeqRequests.Statuses);
+    }
+
+    [Theory]
+    [InlineData (null)]
+    [InlineData ("")]
+    public void Add_Null_Or_Empty_Terminator_Throws (string terminator)
+    {
+        if (terminator is null)
+        {
+            Assert.Throws<ArgumentNullException> (() => EscSeqRequests.Add (terminator));
+        }
+        else
+        {
+            Assert.Throws<ArgumentException> (() => EscSeqRequests.Add (terminator));
+        }
+    }
+
+    [Theory]
+    [InlineData (null)]
+    [InlineData ("")]
+    public void HasResponse_Null_Or_Empty_Terminator_Does_Not_Throws (string terminator)
+    {
+        EscSeqRequests.Add ("t");
+
+        Assert.False (EscSeqRequests.HasResponse (terminator));
+
+        EscSeqRequests.Clear ();
+    }
+
+    [Theory]
+    [InlineData (null)]
+    [InlineData ("")]
+    public void Remove_Null_Or_Empty_Terminator_Throws (string terminator)
+    {
+        EscSeqRequests.Add ("t");
+
+        if (terminator is null)
+        {
+            Assert.Throws<ArgumentNullException> (() => EscSeqRequests.Remove (terminator));
+        }
+        else
+        {
+            Assert.Throws<ArgumentException> (() => EscSeqRequests.Remove (terminator));
+        }
+
+        EscSeqRequests.Clear ();
+    }
+
+    [Fact]
+    public void Requests_Responses_Tests ()
+    {
+        // This is simulated response from a CSI_ReportTerminalSizeInChars
+        ConsoleKeyInfo [] cki =
+        [
+            new ('\u001b', 0, false, false, false),
+            new ('[', 0, false, false, false),
+            new ('8', 0, false, false, false),
+            new (';', 0, false, false, false),
+            new ('1', 0, false, false, false),
+            new ('0', 0, false, false, false),
+            new (';', 0, false, false, false),
+            new ('2', 0, false, false, false),
+            new ('0', 0, false, false, false),
+            new ('t', 0, false, false, false)
+        ];
+        ConsoleKeyInfo newConsoleKeyInfo = default;
+        ConsoleKey key = default;
+        ConsoleModifiers mod = default;
+
+        Assert.Empty (EscSeqRequests.Statuses);
+
+        EscSeqRequests.Add ("t");
+        Assert.Single (EscSeqRequests.Statuses);
+        Assert.Equal ("t", EscSeqRequests.Statuses [^1].Terminator);
+        Assert.Equal (1, EscSeqRequests.Statuses [^1].NumRequests);
+        Assert.Equal (1, EscSeqRequests.Statuses [^1].NumOutstanding);
+
+        EscSeqUtils.DecodeEscSeq (
+                                  ref newConsoleKeyInfo,
+                                  ref key,
+                                  cki,
+                                  ref mod,
+                                  out string c1Control,
+                                  out string code,
+                                  out string [] values,
+                                  out string terminating,
+                                  out bool isKeyMouse,
+                                  out List<MouseFlags> mouseFlags,
+                                  out Point pos,
+                                  out bool isResponse,
+                                  null
+                                 );
+
+        Assert.Empty (EscSeqRequests.Statuses);
+        Assert.Equal (default, newConsoleKeyInfo);
+        Assert.Equal (default, key);
+        Assert.Equal (10, cki.Length);
+        Assert.Equal (default, mod);
+        Assert.Equal ("CSI", c1Control);
+        Assert.Null (code);
+        // ReSharper disable once HeuristicUnreachableCode
+        Assert.Equal (3, values.Length);
+        Assert.Equal ("8", values [0]);
+        Assert.Equal ("t", terminating);
+        Assert.False (isKeyMouse);
+        Assert.Single (mouseFlags);
+        Assert.Equal (default, mouseFlags [^1]);
+        Assert.Equal (Point.Empty, pos);
+        Assert.True (isResponse);
+    }
+}

File diff suppressed because it is too large
+ 579 - 336
UnitTests/Input/EscSeqUtilsTests.cs


+ 2 - 4
UnitTests/Input/KeyTests.cs

@@ -342,10 +342,8 @@ public class KeyTests
     [InlineData ((KeyCode)'{', "{")]
     [InlineData ((KeyCode)'\'', "\'")]
     [InlineData ((KeyCode)'ó', "ó")]
-    [InlineData (
-                    (KeyCode)'Ó' | KeyCode.ShiftMask,
-                    "Shift+Ó"
-                )] // TODO: This is not correct, it should be Shift+ó or just Ó
+    [InlineData ((KeyCode)'Ó' | KeyCode.ShiftMask, "Shift+Ó")]
+    [InlineData ((KeyCode)'ó' | KeyCode.ShiftMask, "Shift+ó")]
     [InlineData ((KeyCode)'Ó', "Ó")]
     [InlineData ((KeyCode)'ç' | KeyCode.ShiftMask | KeyCode.AltMask | KeyCode.CtrlMask, "Ctrl+Alt+Shift+ç")]
     [InlineData ((KeyCode)'a', "a")] // 97 or Key.Space | Key.A

+ 2 - 1
UnitTests/LocalPackagesTests.cs

@@ -1,4 +1,5 @@
-namespace Terminal.Gui;
+
+namespace Terminal.Gui.BuildAndDeployTests;
 
 public class LocalPackagesTests
 {

+ 14 - 12
UnitTests/TestHelpers.cs

@@ -25,21 +25,21 @@ public class AutoInitShutdownAttribute : BeforeAfterTestAttribute
     /// </summary>
     /// <param name="autoInit">If true, Application.Init will be called Before the test runs.</param>
     /// <param name="consoleDriverType">
-    ///     Determines which ConsoleDriver (FakeDriver, WindowsDriver, CursesDriver, NetDriver)
+    ///     Determines which IConsoleDriver (FakeDriver, WindowsDriver, CursesDriver, NetDriver)
     ///     will be used when Application.Init is called. If null FakeDriver will be used. Only valid if
     ///     <paramref name="autoInit"/> is true.
     /// </param>
     /// <param name="useFakeClipboard">
     ///     If true, will force the use of <see cref="FakeDriver.FakeClipboard"/>. Only valid if
-    ///     <see cref="ConsoleDriver"/> == <see cref="FakeDriver"/> and <paramref name="autoInit"/> is true.
+    ///     <see cref="IConsoleDriver"/> == <see cref="FakeDriver"/> and <paramref name="autoInit"/> is true.
     /// </param>
     /// <param name="fakeClipboardAlwaysThrowsNotSupportedException">
     ///     Only valid if <paramref name="autoInit"/> is true. Only
-    ///     valid if <see cref="ConsoleDriver"/> == <see cref="FakeDriver"/> and <paramref name="autoInit"/> is true.
+    ///     valid if <see cref="IConsoleDriver"/> == <see cref="FakeDriver"/> and <paramref name="autoInit"/> is true.
     /// </param>
     /// <param name="fakeClipboardIsSupportedAlwaysTrue">
     ///     Only valid if <paramref name="autoInit"/> is true. Only valid if
-    ///     <see cref="ConsoleDriver"/> == <see cref="FakeDriver"/> and <paramref name="autoInit"/> is true.
+    ///     <see cref="IConsoleDriver"/> == <see cref="FakeDriver"/> and <paramref name="autoInit"/> is true.
     /// </param>
     /// <param name="configLocation">Determines what config file locations <see cref="ConfigurationManager"/> will load from.</param>
     /// <param name="verifyShutdown">If true and <see cref="Application.Initialized"/> is true, the test will fail.</param>
@@ -135,7 +135,7 @@ public class AutoInitShutdownAttribute : BeforeAfterTestAttribute
                 View.Instances.Clear ();
             }
 #endif
-            Application.Init ((ConsoleDriver)Activator.CreateInstance (_driverType));
+            Application.Init ((IConsoleDriver)Activator.CreateInstance (_driverType));
         }
     }
 
@@ -199,6 +199,8 @@ public class SetupFakeDriverAttribute : BeforeAfterTestAttribute
 
         if (Application.Driver is { })
         {
+            ((FakeDriver)Application.Driver).Rows = 25;
+            ((FakeDriver)Application.Driver).Cols = 25;
             ((FakeDriver)Application.Driver).End ();
         }
 
@@ -249,12 +251,12 @@ internal partial class TestHelpers
     ///     <paramref name="expectedAttributes"/>.
     /// </param>
     /// <param name="output"></param>
-    /// <param name="driver">The ConsoleDriver to use. If null <see cref="Application.Driver"/> will be used.</param>
+    /// <param name="driver">The IConsoleDriver to use. If null <see cref="Application.Driver"/> will be used.</param>
     /// <param name="expectedAttributes"></param>
     public static void AssertDriverAttributesAre (
         string expectedLook,
         ITestOutputHelper output,
-        ConsoleDriver driver = null,
+        IConsoleDriver driver = null,
         params Attribute [] expectedAttributes
     )
     {
@@ -319,12 +321,12 @@ internal partial class TestHelpers
     /// <summary>Asserts that the driver contents match the expected contents, optionally ignoring any trailing whitespace.</summary>
     /// <param name="expectedLook"></param>
     /// <param name="output"></param>
-    /// <param name="driver">The ConsoleDriver to use. If null <see cref="Application.Driver"/> will be used.</param>
+    /// <param name="driver">The IConsoleDriver to use. If null <see cref="Application.Driver"/> will be used.</param>
     /// <param name="ignoreLeadingWhitespace"></param>
     public static void AssertDriverContentsAre (
         string expectedLook,
         ITestOutputHelper output,
-        ConsoleDriver driver = null,
+        IConsoleDriver driver = null,
         bool ignoreLeadingWhitespace = false
     )
     {
@@ -365,12 +367,12 @@ internal partial class TestHelpers
     /// </summary>
     /// <param name="expectedLook"></param>
     /// <param name="output"></param>
-    /// <param name="driver">The ConsoleDriver to use. If null <see cref="Application.Driver"/> will be used.</param>
+    /// <param name="driver">The IConsoleDriver to use. If null <see cref="Application.Driver"/> will be used.</param>
     /// <returns></returns>
     public static Rectangle AssertDriverContentsWithFrameAre (
         string expectedLook,
         ITestOutputHelper output,
-        ConsoleDriver driver = null
+        IConsoleDriver driver = null
     )
     {
         List<List<Rune>> lines = new ();
@@ -623,7 +625,7 @@ internal partial class TestHelpers
     /// </summary>
     /// <param name="driver">if null uses <see cref="Application.Driver"/></param>
     /// <param name="expectedColors"></param>
-    internal static void AssertDriverUsedColors (ConsoleDriver driver = null, params Attribute [] expectedColors)
+    internal static void AssertDriverUsedColors (IConsoleDriver driver = null, params Attribute [] expectedColors)
     {
         driver ??= Application.Driver;
         Cell [,] contents = driver.Contents;

+ 0 - 52
bench.json

@@ -1,52 +0,0 @@
-[
-  {
-    "Scenario": "Adornments Demo",
-    "Duration": "00:00:00.1805368",
-    "IterationCount": 501,
-    "ClearedContentCount": 0,
-    "RefreshedCount": 503,
-    "UpdatedCount": 1,
-    "DrawCompleteCount": 82,
-    "LaidOutCount": 82
-  },
-  {
-    "Scenario": "All Views Tester",
-    "Duration": "00:00:00.1070009",
-    "IterationCount": 501,
-    "ClearedContentCount": 0,
-    "RefreshedCount": 503,
-    "UpdatedCount": 1,
-    "DrawCompleteCount": 103,
-    "LaidOutCount": 182
-  },
-  {
-    "Scenario": "Animation",
-    "Duration": "00:00:00.0675802",
-    "IterationCount": 501,
-    "ClearedContentCount": 0,
-    "RefreshedCount": 503,
-    "UpdatedCount": 1,
-    "DrawCompleteCount": 4,
-    "LaidOutCount": 4
-  },
-  {
-    "Scenario": "Arrangement",
-    "Duration": "00:00:00.1284709",
-    "IterationCount": 501,
-    "ClearedContentCount": 0,
-    "RefreshedCount": 503,
-    "UpdatedCount": 1,
-    "DrawCompleteCount": 123,
-    "LaidOutCount": 123
-  },
-  {
-    "Scenario": "ASCIICustomButtonTest",
-    "Duration": "00:00:01.0613372",
-    "IterationCount": 30,
-    "ClearedContentCount": 0,
-    "RefreshedCount": 32,
-    "UpdatedCount": 31,
-    "DrawCompleteCount": 4185,
-    "LaidOutCount": 2852
-  }
-]

Some files were not shown because too many files changed in this diff