diff --git a/Terminal.Gui/Application/Application.Initialization.cs b/Terminal.Gui/Application/Application.Initialization.cs
index 56e774a14d..a74f1545d1 100644
--- a/Terminal.Gui/Application/Application.Initialization.cs
+++ b/Terminal.Gui/Application/Application.Initialization.cs
@@ -37,7 +37,15 @@ public static partial class Application // Initialization (Init/Shutdown)
     /// </param>
     [RequiresUnreferencedCode ("AOT")]
     [RequiresDynamicCode ("AOT")]
-    public static void Init (IConsoleDriver? driver = null, string? driverName = null) { InternalInit (driver, driverName); }
+    public static void Init (IConsoleDriver? driver = null, string? driverName = null)
+    {
+        if (driverName?.StartsWith ("v2") ?? false)
+        {
+            ApplicationImpl.ChangeInstance (new ApplicationV2 ());
+        }
+
+        ApplicationImpl.Instance.Init (driver, driverName);
+    }
 
     internal static int MainThreadId { get; set; } = -1;
 
@@ -94,19 +102,7 @@ internal static void InternalInit (
 
         AddKeyBindings ();
 
-        // Start the process of configuration management.
-        // Note that we end up calling LoadConfigurationFromAllSources
-        // multiple times. We need to do this because some settings are only
-        // valid after a Driver is loaded. In this case we need just
-        // `Settings` so we can determine which driver to use.
-        // Don't reset, so we can inherit the theme from the previous run.
-        string previousTheme = Themes?.Theme ?? string.Empty;
-        Load ();
-        if (Themes is { } && !string.IsNullOrEmpty (previousTheme) && previousTheme != "Default")
-        {
-            ThemeManager.SelectedTheme = previousTheme;
-        }
-        Apply ();
+        InitializeConfigurationManagement ();
 
         // Ignore Configuration for ForceDriver if driverName is specified
         if (!string.IsNullOrEmpty (driverName))
@@ -166,12 +162,28 @@ internal static void InternalInit (
 
         SynchronizationContext.SetSynchronizationContext (new MainLoopSyncContext ());
 
-        SupportedCultures = GetSupportedCultures ();
         MainThreadId = Thread.CurrentThread.ManagedThreadId;
         bool init = Initialized = true;
         InitializedChanged?.Invoke (null, new (init));
     }
 
+    internal static void InitializeConfigurationManagement ()
+    {
+        // Start the process of configuration management.
+        // Note that we end up calling LoadConfigurationFromAllSources
+        // multiple times. We need to do this because some settings are only
+        // valid after a Driver is loaded. In this case we need just
+        // `Settings` so we can determine which driver to use.
+        // Don't reset, so we can inherit the theme from the previous run.
+        string previousTheme = Themes?.Theme ?? string.Empty;
+        Load ();
+        if (Themes is { } && !string.IsNullOrEmpty (previousTheme) && previousTheme != "Default")
+        {
+            ThemeManager.SelectedTheme = previousTheme;
+        }
+        Apply ();
+    }
+
     internal static void SubscribeDriverEvents ()
     {
         ArgumentNullException.ThrowIfNull (Driver);
@@ -226,20 +238,7 @@ internal static void UnsubscribeDriverEvents ()
     ///     up (Disposed)
     ///     and terminal settings are restored.
     /// </remarks>
-    public static void Shutdown ()
-    {
-        // TODO: Throw an exception if Init hasn't been called.
-
-        bool wasInitialized = Initialized;
-        ResetState ();
-        PrintJsonErrors ();
-
-        if (wasInitialized)
-        {
-            bool init = Initialized;
-            InitializedChanged?.Invoke (null, new (in init));
-        }
-    }
+    public static void Shutdown () => ApplicationImpl.Instance.Shutdown ();
 
     /// <summary>
     ///     Gets whether the application has been initialized with <see cref="Init"/> and not yet shutdown with <see cref="Shutdown"/>.
@@ -258,4 +257,12 @@ public static void Shutdown ()
     ///     Intended to support unit tests that need to know when the application has been initialized.
     /// </remarks>
     public static event EventHandler<EventArgs<bool>>? InitializedChanged;
+
+    /// <summary>
+    ///  Raises the <see cref="InitializedChanged"/> event.
+    /// </summary>
+    internal static void OnInitializedChanged (object sender, EventArgs<bool> e)
+    {
+        Application.InitializedChanged?.Invoke (sender,e);
+    }
 }
diff --git a/Terminal.Gui/Application/Application.Keyboard.cs b/Terminal.Gui/Application/Application.Keyboard.cs
index 625b5b15f2..873dc0af0f 100644
--- a/Terminal.Gui/Application/Application.Keyboard.cs
+++ b/Terminal.Gui/Application/Application.Keyboard.cs
@@ -177,7 +177,6 @@ internal static void AddKeyBindings ()
                         return true;
                     }
                    );
-
         AddCommand (
                     Command.Suspend,
                     static () =>
@@ -187,7 +186,6 @@ internal static void AddKeyBindings ()
                         return true;
                     }
                    );
-
         AddCommand (
                     Command.NextTabStop,
                     static () => Navigation?.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop));
diff --git a/Terminal.Gui/Application/Application.Run.cs b/Terminal.Gui/Application/Application.Run.cs
index 9e4d43b705..fc6819aed1 100644
--- a/Terminal.Gui/Application/Application.Run.cs
+++ b/Terminal.Gui/Application/Application.Run.cs
@@ -305,7 +305,8 @@ internal static bool PositionCursor ()
     /// <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, IConsoleDriver? driver = null) { return Run<Toplevel> (errorHandler, driver); }
+    public static Toplevel Run (Func<Exception, bool>? errorHandler = null, IConsoleDriver? driver = null) =>
+        ApplicationImpl.Instance.Run (errorHandler, driver);
 
     /// <summary>
     ///     Runs the application by creating a <see cref="Toplevel"/>-derived object of type <c>T</c> and calling
@@ -331,20 +332,7 @@ internal static bool PositionCursor ()
     [RequiresUnreferencedCode ("AOT")]
     [RequiresDynamicCode ("AOT")]
     public static T Run<T> (Func<Exception, bool>? errorHandler = null, IConsoleDriver? driver = null)
-        where T : Toplevel, new()
-    {
-        if (!Initialized)
-        {
-            // Init() has NOT been called.
-            InternalInit (driver, null, true);
-        }
-
-        var top = new T ();
-
-        Run (top, errorHandler);
-
-        return top;
-    }
+        where T : Toplevel, new() => ApplicationImpl.Instance.Run<T> (errorHandler, driver);
 
     /// <summary>Runs the Application using the provided <see cref="Toplevel"/> view.</summary>
     /// <remarks>
@@ -385,73 +373,7 @@ public static T Run<T> (Func<Exception, bool>? errorHandler = null, IConsoleDriv
     ///     rethrows when null).
     /// </param>
     public static void Run (Toplevel view, Func<Exception, bool>? errorHandler = null)
-    {
-        ArgumentNullException.ThrowIfNull (view);
-
-        if (Initialized)
-        {
-            if (Driver is null)
-            {
-                // Disposing before throwing
-                view.Dispose ();
-
-                // This code path should be impossible because Init(null, null) will select the platform default driver
-                throw new InvalidOperationException (
-                                                     "Init() completed without a driver being set (this should be impossible); Run<T>() cannot be called."
-                                                    );
-            }
-        }
-        else
-        {
-            // Init() has NOT been called.
-            throw new InvalidOperationException (
-                                                 "Init() has not been called. Only Run() or Run<T>() can be used without calling Init()."
-                                                );
-        }
-
-        var resume = true;
-
-        while (resume)
-        {
-#if !DEBUG
-            try
-            {
-#endif
-            resume = false;
-            RunState runState = Begin (view);
-
-            // If EndAfterFirstIteration is true then the user must dispose of the runToken
-            // by using NotifyStopRunState event.
-            RunLoop (runState);
-
-            if (runState.Toplevel is null)
-            {
-#if DEBUG_IDISPOSABLE
-                Debug.Assert (TopLevels.Count == 0);
-#endif
-                runState.Dispose ();
-
-                return;
-            }
-
-            if (!EndAfterFirstIteration)
-            {
-                End (runState);
-            }
-#if !DEBUG
-            }
-            catch (Exception error)
-            {
-                if (errorHandler is null)
-                {
-                    throw;
-                }
-
-                resume = errorHandler (error);
-            }
-#endif
-        }
-    }
+        => ApplicationImpl.Instance.Run (view, errorHandler);
 
     /// <summary>Adds a timeout to the application.</summary>
     /// <remarks>
@@ -459,36 +381,23 @@ public static void Run (Toplevel view, Func<Exception, bool>? errorHandler = nul
     ///     reset, repeating the invocation. If it returns false, the timeout will stop and be removed. The returned value is a
     ///     token that can be used to stop the timeout by calling <see cref="RemoveTimeout(object)"/>.
     /// </remarks>
-    public static object? AddTimeout (TimeSpan time, Func<bool> callback)
-    {
-        return MainLoop?.AddTimeout (time, callback) ?? null;
-    }
+    public static object? AddTimeout (TimeSpan time, Func<bool> callback) => ApplicationImpl.Instance.AddTimeout (time, callback);
 
     /// <summary>Removes a previously scheduled timeout</summary>
     /// <remarks>The token parameter is the value returned by <see cref="AddTimeout"/>.</remarks>
     /// Returns
-    /// <c>true</c>
+    /// <see langword="true"/>
     /// if the timeout is successfully removed; otherwise,
-    /// <c>false</c>
+    /// <see langword="false"/>
     /// .
     /// This method also returns
-    /// <c>false</c>
+    /// <see langword="false"/>
     /// if the timeout is not found.
-    public static bool RemoveTimeout (object token) { return MainLoop?.RemoveTimeout (token) ?? false; }
+    public static bool RemoveTimeout (object token) => ApplicationImpl.Instance.RemoveTimeout (token);
 
     /// <summary>Runs <paramref name="action"/> on the thread that is processing events</summary>
     /// <param name="action">the action to be invoked on the main processing thread.</param>
-    public static void Invoke (Action action)
-    {
-        MainLoop?.AddIdle (
-                           () =>
-                           {
-                               action ();
-
-                               return false;
-                           }
-                          );
-    }
+    public static void Invoke (Action action) => ApplicationImpl.Instance.Invoke (action);
 
     // TODO: Determine if this is really needed. The only code that calls WakeUp I can find
     // is ProgressBarStyles, and it's not clear it needs to.
@@ -517,8 +426,7 @@ public static void LayoutAndDraw (bool forceDraw = false)
 
         View.SetClipToScreen ();
         View.Draw (TopLevels, neededLayout || forceDraw);
-        View.SetClipToScreen ();
-
+        View.SetClipToScreen (); 
         Driver?.Refresh ();
     }
 
@@ -528,7 +436,7 @@ public static void LayoutAndDraw (bool forceDraw = false)
 
     /// <summary>The <see cref="MainLoop"/> driver for the application</summary>
     /// <value>The main loop.</value>
-    internal static MainLoop? MainLoop { get; private set; }
+    internal static MainLoop? MainLoop { get; set; }
 
     /// <summary>
     ///     Set to true to cause <see cref="End"/> to be called after the first iteration. Set to false (the default) to
@@ -612,31 +520,8 @@ public static bool RunIteration (ref RunState state, bool firstIteration = false
     ///         property on the currently running <see cref="Toplevel"/> to false.
     ///     </para>
     /// </remarks>
-    public static void RequestStop (Toplevel? top = null)
-    {
-        if (top is null)
-        {
-            top = Top;
-        }
-
-        if (!top!.Running)
-        {
-            return;
-        }
-
-        var ev = new ToplevelClosingEventArgs (top);
-        top.OnClosing (ev);
-
-        if (ev.Cancel)
-        {
-            return;
-        }
-
-        top.Running = false;
-        OnNotifyStopRunState (top);
-    }
-
-    private static void OnNotifyStopRunState (Toplevel top)
+    public static void RequestStop (Toplevel? top = null) => ApplicationImpl.Instance.RequestStop (top);
+    internal static void OnNotifyStopRunState (Toplevel top)
     {
         if (EndAfterFirstIteration)
         {
diff --git a/Terminal.Gui/Application/Application.cd b/Terminal.Gui/Application/Application.cd
new file mode 100644
index 0000000000..9c22dd77ba
--- /dev/null
+++ b/Terminal.Gui/Application/Application.cd
@@ -0,0 +1,91 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ClassDiagram MajorVersion="1" MinorVersion="1">
+  <Class Name="Terminal.Gui.Application">
+    <Position X="2.25" Y="1.5" Width="1.5" />
+    <TypeIdentifier>
+      <HashCode>hEI4FAgAqARIspQfBQo0gTGiACNL0AICESJKoggBSg8=</HashCode>
+      <FileName>Application\Application.cs</FileName>
+    </TypeIdentifier>
+  </Class>
+  <Class Name="Terminal.Gui.ApplicationNavigation" Collapsed="true">
+    <Position X="13.75" Y="1.75" Width="2" />
+    <TypeIdentifier>
+      <HashCode>AABAAAAAAABCAAAAAAAAAAAAAAAAIgIAAAAAAAAAAAA=</HashCode>
+      <FileName>Application\ApplicationNavigation.cs</FileName>
+    </TypeIdentifier>
+  </Class>
+  <Class Name="Terminal.Gui.IterationEventArgs" Collapsed="true">
+    <Position X="16" Y="2" Width="2" />
+    <TypeIdentifier>
+      <HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</HashCode>
+      <FileName>Application\IterationEventArgs.cs</FileName>
+    </TypeIdentifier>
+  </Class>
+  <Class Name="Terminal.Gui.MainLoop" Collapsed="true" BaseTypeListCollapsed="true">
+    <Position X="10.25" Y="2.75" Width="1.5" />
+    <TypeIdentifier>
+      <HashCode>CAAAIAAAASAAAQAQAAAAAIBADQAAEAAYIgIIwAAAAAI=</HashCode>
+      <FileName>Application\MainLoop.cs</FileName>
+    </TypeIdentifier>
+    <Lollipop Position="0.2" Collapsed="true" />
+  </Class>
+  <Class Name="Terminal.Gui.MainLoopSyncContext" Collapsed="true">
+    <Position X="12" Y="2.75" Width="2" />
+    <TypeIdentifier>
+      <HashCode>AAAAAgAAAAAAAAAAAEAAAAAACAAAAAAAAAAAAAAAAAA=</HashCode>
+      <FileName>Application\MainLoopSyncContext.cs</FileName>
+    </TypeIdentifier>
+  </Class>
+  <Class Name="Terminal.Gui.RunState" Collapsed="true" BaseTypeListCollapsed="true">
+    <Position X="14.25" Y="3" Width="1.5" />
+    <TypeIdentifier>
+      <HashCode>AAAAAAAAACACAgAAAAAAAAAAAAAAAAACQAAAAAAAAAA=</HashCode>
+      <FileName>Application\RunState.cs</FileName>
+    </TypeIdentifier>
+    <Lollipop Position="0.2" Collapsed="true" />
+  </Class>
+  <Class Name="Terminal.Gui.RunStateEventArgs" Collapsed="true">
+    <Position X="16" Y="3" Width="2" />
+    <TypeIdentifier>
+      <HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAA=</HashCode>
+      <FileName>Application\RunStateEventArgs.cs</FileName>
+    </TypeIdentifier>
+  </Class>
+  <Class Name="Terminal.Gui.Timeout" Collapsed="true">
+    <Position X="10.25" Y="3.75" Width="1.5" />
+    <TypeIdentifier>
+      <HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAQAA=</HashCode>
+      <FileName>Application\Timeout.cs</FileName>
+    </TypeIdentifier>
+  </Class>
+  <Class Name="Terminal.Gui.TimeoutEventArgs" Collapsed="true">
+    <Position X="12" Y="3.75" Width="2" />
+    <TypeIdentifier>
+      <HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAAACAIAAAAAAAAAAAA=</HashCode>
+      <FileName>Application\TimeoutEventArgs.cs</FileName>
+    </TypeIdentifier>
+  </Class>
+  <Class Name="Terminal.Gui.ApplicationImpl" BaseTypeListCollapsed="true">
+    <Position X="5.75" Y="1.75" Width="1.5" />
+    <TypeIdentifier>
+      <HashCode>AAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAQAACAACAAAI=</HashCode>
+      <FileName>Application\ApplicationImpl.cs</FileName>
+    </TypeIdentifier>
+    <Lollipop Position="0.2" />
+  </Class>
+  <Interface Name="Terminal.Gui.IMainLoopDriver" Collapsed="true">
+    <Position X="12" Y="5" Width="1.5" />
+    <TypeIdentifier>
+      <HashCode>AAAAAAAACAAAAAQAAAAABAAAAAAAEAAAAAAAAAAAAAA=</HashCode>
+      <FileName>Application\MainLoop.cs</FileName>
+    </TypeIdentifier>
+  </Interface>
+  <Interface Name="Terminal.Gui.IApplication">
+    <Position X="4" Y="1.75" Width="1.5" />
+    <TypeIdentifier>
+      <HashCode>AAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAACAAAAAAI=</HashCode>
+      <FileName>Application\IApplication.cs</FileName>
+    </TypeIdentifier>
+  </Interface>
+  <Font Name="Segoe UI" Size="9" />
+</ClassDiagram>
\ No newline at end of file
diff --git a/Terminal.Gui/Application/Application.cs b/Terminal.Gui/Application/Application.cs
index 79198349f1..a493f30569 100644
--- a/Terminal.Gui/Application/Application.cs
+++ b/Terminal.Gui/Application/Application.cs
@@ -24,7 +24,7 @@ namespace Terminal.Gui;
 public static partial class Application
 {
     /// <summary>Gets all cultures supported by the application without the invariant language.</summary>
-    public static List<CultureInfo>? SupportedCultures { get; private set; }
+    public static List<CultureInfo>? SupportedCultures { get; private set; } = GetSupportedCultures ();
 
     /// <summary>
     ///     Gets a string representation of the Application as rendered by <see cref="Driver"/>.
@@ -224,5 +224,10 @@ internal static void ResetState (bool ignoreDisposed = false)
         SynchronizationContext.SetSynchronizationContext (null);
     }
 
-    // Only return true if the Current has changed.
+
+    /// <summary>
+    ///     Adds specified idle handler function to main iteration processing. The handler function will be called
+    ///     once per iteration of the main loop after other events have been handled.
+    /// </summary>
+    public static void AddIdle (Func<bool> func) => ApplicationImpl.Instance.AddIdle (func);
 }
diff --git a/Terminal.Gui/Application/ApplicationImpl.cs b/Terminal.Gui/Application/ApplicationImpl.cs
new file mode 100644
index 0000000000..b6b8f91c42
--- /dev/null
+++ b/Terminal.Gui/Application/ApplicationImpl.cs
@@ -0,0 +1,296 @@
+#nullable enable
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+
+namespace Terminal.Gui;
+
+/// <summary>
+/// Original Terminal.Gui implementation of core <see cref="Application"/> methods.
+/// </summary>
+public class ApplicationImpl : IApplication
+{
+    // Private static readonly Lazy instance of Application
+    private static Lazy<IApplication> _lazyInstance = new (() => new ApplicationImpl ());
+
+    /// <summary>
+    /// Gets the currently configured backend implementation of <see cref="Application"/> gateway methods.
+    /// Change to your own implementation by using <see cref="ChangeInstance"/> (before init).
+    /// </summary>
+    public static IApplication Instance => _lazyInstance.Value;
+
+    /// <summary>
+    /// Change the singleton implementation, should not be called except before application
+    /// startup. This method lets you provide alternative implementations of core static gateway
+    /// methods of <see cref="Application"/>.
+    /// </summary>
+    /// <param name="newApplication"></param>
+    public static void ChangeInstance (IApplication newApplication)
+    {
+        _lazyInstance = new Lazy<IApplication> (newApplication);
+    }
+
+    /// <inheritdoc/>
+    [RequiresUnreferencedCode ("AOT")]
+    [RequiresDynamicCode ("AOT")]
+    public virtual void Init (IConsoleDriver? driver = null, string? driverName = null)
+    {
+            Application.InternalInit (driver, driverName);
+    }
+
+    /// <summary>
+    ///     Runs the application by creating a <see cref="Toplevel"/> object and calling
+    ///     <see cref="Run(Toplevel, Func{Exception, bool})"/>.
+    /// </summary>
+    /// <remarks>
+    ///     <para>Calling <see cref="Init"/> first is not needed as this function will initialize the application.</para>
+    ///     <para>
+    ///         <see cref="Shutdown"/> must be called when the application is closing (typically after Run> has returned) to
+    ///         ensure resources are cleaned up and terminal settings restored.
+    ///     </para>
+    ///     <para>
+    ///         The caller is responsible for disposing the object returned by this method.
+    ///     </para>
+    /// </remarks>
+    /// <returns>The created <see cref="Toplevel"/> object. The caller is responsible for disposing this object.</returns>
+    [RequiresUnreferencedCode ("AOT")]
+    [RequiresDynamicCode ("AOT")]
+    public 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
+    ///     <see cref="Run(Toplevel, Func{Exception, bool})"/>.
+    /// </summary>
+    /// <remarks>
+    ///     <para>Calling <see cref="Init"/> first is not needed as this function will initialize the application.</para>
+    ///     <para>
+    ///         <see cref="Shutdown"/> must be called when the application is closing (typically after Run> has returned) to
+    ///         ensure resources are cleaned up and terminal settings restored.
+    ///     </para>
+    ///     <para>
+    ///         The caller is responsible for disposing the object returned by this method.
+    ///     </para>
+    /// </remarks>
+    /// <param name="errorHandler"></param>
+    /// <param name="driver">
+    ///     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 virtual T Run<T> (Func<Exception, bool>? errorHandler = null, IConsoleDriver? driver = null)
+        where T : Toplevel, new()
+    {
+        if (!Application.Initialized)
+        {
+            // Init() has NOT been called.
+            Application.InternalInit (driver, null, true);
+        }
+
+        var top = new T ();
+
+        Run (top, errorHandler);
+
+        return top;
+    }
+
+    /// <summary>Runs the Application using the provided <see cref="Toplevel"/> view.</summary>
+    /// <remarks>
+    ///     <para>
+    ///         This method is used to start processing events for the main application, but it is also used to run other
+    ///         modal <see cref="View"/>s such as <see cref="Dialog"/> boxes.
+    ///     </para>
+    ///     <para>
+    ///         To make a <see cref="Run(Terminal.Gui.Toplevel,System.Func{System.Exception,bool})"/> stop execution, call
+    ///         <see cref="Application.RequestStop"/>.
+    ///     </para>
+    ///     <para>
+    ///         Calling <see cref="Run(Terminal.Gui.Toplevel,System.Func{System.Exception,bool})"/> is equivalent to calling
+    ///         <see cref="Application.Begin(Toplevel)"/>, followed by <see cref="Application.RunLoop(RunState)"/>, and then calling
+    ///         <see cref="Application.End(RunState)"/>.
+    ///     </para>
+    ///     <para>
+    ///         Alternatively, to have a program control the main loop and process events manually, call
+    ///         <see cref="Application.Begin(Toplevel)"/> to set things up manually and then repeatedly call
+    ///         <see cref="Application.RunLoop(RunState)"/> with the wait parameter set to false. By doing this the
+    ///         <see cref="Application.RunLoop(RunState)"/> method will only process any pending events, timers, idle handlers and then
+    ///         return control immediately.
+    ///     </para>
+    ///     <para>When using <see cref="Run{T}"/> or
+    ///         <see cref="Run(System.Func{System.Exception,bool},Terminal.Gui.IConsoleDriver)"/>
+    ///         <see cref="Init"/> will be called automatically.
+    ///     </para>
+    ///     <para>
+    ///         RELEASE builds only: When <paramref name="errorHandler"/> is <see langword="null"/> any exceptions will be
+    ///         rethrown. Otherwise, if <paramref name="errorHandler"/> will be called. If <paramref name="errorHandler"/>
+    ///         returns <see langword="true"/> the <see cref="Application.RunLoop(RunState)"/> will resume; otherwise this method will
+    ///         exit.
+    ///     </para>
+    /// </remarks>
+    /// <param name="view">The <see cref="Toplevel"/> to run as a modal.</param>
+    /// <param name="errorHandler">
+    ///     RELEASE builds only: Handler for any unhandled exceptions (resumes when returns true,
+    ///     rethrows when null).
+    /// </param>
+    public virtual void Run (Toplevel view, Func<Exception, bool>? errorHandler = null)
+    {
+        ArgumentNullException.ThrowIfNull (view);
+
+        if (Application.Initialized)
+        {
+            if (Application.Driver is null)
+            {
+                // Disposing before throwing
+                view.Dispose ();
+
+                // This code path should be impossible because Init(null, null) will select the platform default driver
+                throw new InvalidOperationException (
+                                                     "Init() completed without a driver being set (this should be impossible); Run<T>() cannot be called."
+                                                    );
+            }
+        }
+        else
+        {
+            // Init() has NOT been called.
+            throw new InvalidOperationException (
+                                                 "Init() has not been called. Only Run() or Run<T>() can be used without calling Init()."
+                                                );
+        }
+
+        var resume = true;
+
+        while (resume)
+        {
+#if !DEBUG
+            try
+            {
+#endif
+            resume = false;
+            RunState runState = Application.Begin (view);
+
+            // If EndAfterFirstIteration is true then the user must dispose of the runToken
+            // by using NotifyStopRunState event.
+            Application.RunLoop (runState);
+
+            if (runState.Toplevel is null)
+            {
+#if DEBUG_IDISPOSABLE
+                Debug.Assert (Application.TopLevels.Count == 0);
+#endif
+                runState.Dispose ();
+
+                return;
+            }
+
+            if (!Application.EndAfterFirstIteration)
+            {
+                Application.End (runState);
+            }
+#if !DEBUG
+            }
+            catch (Exception error)
+            {
+                if (errorHandler is null)
+                {
+                    throw;
+                }
+
+                resume = errorHandler (error);
+            }
+#endif
+        }
+    }
+
+    /// <summary>Shutdown an application initialized with <see cref="Init"/>.</summary>
+    /// <remarks>
+    ///     Shutdown must be called for every call to <see cref="Init"/> or
+    ///     <see cref="Application.Run(Toplevel, Func{Exception, bool})"/> to ensure all resources are cleaned
+    ///     up (Disposed)
+    ///     and terminal settings are restored.
+    /// </remarks>
+    public virtual void Shutdown ()
+    {
+        // TODO: Throw an exception if Init hasn't been called.
+
+        bool wasInitialized = Application.Initialized;
+        Application.ResetState ();
+        PrintJsonErrors ();
+
+        if (wasInitialized)
+        {
+            bool init = Application.Initialized;
+
+            Application.OnInitializedChanged(this, new (in init));
+        }
+    }
+
+    /// <inheritdoc />
+    public virtual void RequestStop (Toplevel? top)
+    {
+        top ??= Application.Top;
+
+        if (!top!.Running)
+        {
+            return;
+        }
+
+        var ev = new ToplevelClosingEventArgs (top);
+        top.OnClosing (ev);
+
+        if (ev.Cancel)
+        {
+            return;
+        }
+
+        top.Running = false;
+        Application.OnNotifyStopRunState (top);
+    }
+
+    /// <inheritdoc />
+    public virtual void Invoke (Action action)
+    {
+        Application.MainLoop?.AddIdle (
+                           () =>
+                           {
+                               action ();
+
+                               return false;
+                           }
+                          );
+    }
+
+    /// <inheritdoc />
+    public bool IsLegacy { get; protected set; } = true;
+
+    /// <inheritdoc />
+    public virtual void AddIdle (Func<bool> func)
+    {
+        if(Application.MainLoop is null)
+        {
+            throw new NotInitializedException ("Cannot add idle before main loop is initialized");
+        }
+
+        // Yes in this case we cannot go direct via TimedEvents because legacy main loop
+        // has established behaviour to do other stuff too e.g. 'wake up'.
+        Application.MainLoop.AddIdle (func);
+
+    }
+
+    /// <inheritdoc />
+    public virtual object AddTimeout (TimeSpan time, Func<bool> callback)
+    {
+        if (Application.MainLoop is null)
+        {
+            throw new NotInitializedException ("Cannot add timeout before main loop is initialized", null);
+        }
+
+        return Application.MainLoop.TimedEvents.AddTimeout (time, callback);
+    }
+
+    /// <inheritdoc />
+    public virtual bool RemoveTimeout (object token)
+    { 
+        return Application.MainLoop?.TimedEvents.RemoveTimeout (token) ?? false;
+    }
+}
diff --git a/Terminal.Gui/Application/IApplication.cs b/Terminal.Gui/Application/IApplication.cs
new file mode 100644
index 0000000000..bdd51046fe
--- /dev/null
+++ b/Terminal.Gui/Application/IApplication.cs
@@ -0,0 +1,185 @@
+#nullable enable
+using System.Diagnostics.CodeAnalysis;
+
+namespace Terminal.Gui;
+
+/// <summary>
+/// Interface for instances that provide backing functionality to static
+/// gateway class <see cref="Application"/>.
+/// </summary>
+public interface IApplication
+{
+    /// <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="IConsoleDriver"/> for the platform, Creates a <see cref="Toplevel"/>. and
+    ///     assigns it to <see cref="Application.Top"/>
+    /// </para>
+    /// <para>
+    ///     <see cref="Shutdown"/> must be called when the application is closing (typically after
+    ///     <see cref="Run{T}"/> has returned) to ensure resources are cleaned up and
+    ///     terminal settings
+    ///     restored.
+    /// </para>
+    /// <para>
+    ///     The <see cref="Run{T}"/> function combines
+    ///     <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.IConsoleDriver,string)"/>.
+    /// </para>
+    /// <param name="driver">
+    ///     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="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 void Init (IConsoleDriver? driver = null, string? driverName = null);
+
+
+    /// <summary>
+    ///     Runs the application by creating a <see cref="Toplevel"/> object and calling
+    ///     <see cref="Run(Toplevel, Func{Exception, bool})"/>.
+    /// </summary>
+    /// <remarks>
+    ///     <para>Calling <see cref="Init"/> first is not needed as this function will initialize the application.</para>
+    ///     <para>
+    ///         <see cref="Shutdown"/> must be called when the application is closing (typically after Run> has returned) to
+    ///         ensure resources are cleaned up and terminal settings restored.
+    ///     </para>
+    ///     <para>
+    ///         The caller is responsible for disposing the object returned by this method.
+    ///     </para>
+    /// </remarks>
+    /// <returns>The created <see cref="Toplevel"/> object. The caller is responsible for disposing this object.</returns>
+    [RequiresUnreferencedCode ("AOT")]
+    [RequiresDynamicCode ("AOT")]
+    public Toplevel Run (Func<Exception, bool>? errorHandler = null, IConsoleDriver? driver = null);
+
+    /// <summary>
+    ///     Runs the application by creating a <see cref="Toplevel"/>-derived object of type <c>T</c> and calling
+    ///     <see cref="Run(Toplevel, Func{Exception, bool})"/>.
+    /// </summary>
+    /// <remarks>
+    ///     <para>Calling <see cref="Init"/> first is not needed as this function will initialize the application.</para>
+    ///     <para>
+    ///         <see cref="Shutdown"/> must be called when the application is closing (typically after Run> has returned) to
+    ///         ensure resources are cleaned up and terminal settings restored.
+    ///     </para>
+    ///     <para>
+    ///         The caller is responsible for disposing the object returned by this method.
+    ///     </para>
+    /// </remarks>
+    /// <param name="errorHandler"></param>
+    /// <param name="driver">
+    ///     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 T Run<T> (Func<Exception, bool>? errorHandler = null, IConsoleDriver? driver = null)
+        where T : Toplevel, new ();
+
+    /// <summary>Runs the Application using the provided <see cref="Toplevel"/> view.</summary>
+    /// <remarks>
+    ///     <para>
+    ///         This method is used to start processing events for the main application, but it is also used to run other
+    ///         modal <see cref="View"/>s such as <see cref="Dialog"/> boxes.
+    ///     </para>
+    ///     <para>
+    ///         To make a <see cref="Run(Terminal.Gui.Toplevel,System.Func{System.Exception,bool})"/> stop execution, call
+    ///         <see cref="Application.RequestStop"/>.
+    ///     </para>
+    ///     <para>
+    ///         Calling <see cref="Run(Terminal.Gui.Toplevel,System.Func{System.Exception,bool})"/> is equivalent to calling
+    ///         <see cref="Application.Begin(Toplevel)"/>, followed by <see cref="Application.RunLoop(RunState)"/>, and then calling
+    ///         <see cref="Application.End(RunState)"/>.
+    ///     </para>
+    ///     <para>
+    ///         Alternatively, to have a program control the main loop and process events manually, call
+    ///         <see cref="Application.Begin(Toplevel)"/> to set things up manually and then repeatedly call
+    ///         <see cref="Application.RunLoop(RunState)"/> with the wait parameter set to false. By doing this the
+    ///         <see cref="Application.RunLoop(RunState)"/> method will only process any pending events, timers, idle handlers and then
+    ///         return control immediately.
+    ///     </para>
+    ///     <para>When using <see cref="Run{T}"/> or
+    ///         <see cref="Run(System.Func{System.Exception,bool},Terminal.Gui.IConsoleDriver)"/>
+    ///         <see cref="Init"/> will be called automatically.
+    ///     </para>
+    ///     <para>
+    ///         RELEASE builds only: When <paramref name="errorHandler"/> is <see langword="null"/> any exceptions will be
+    ///         rethrown. Otherwise, if <paramref name="errorHandler"/> will be called. If <paramref name="errorHandler"/>
+    ///         returns <see langword="true"/> the <see cref="Application.RunLoop(RunState)"/> will resume; otherwise this method will
+    ///         exit.
+    ///     </para>
+    /// </remarks>
+    /// <param name="view">The <see cref="Toplevel"/> to run as a modal.</param>
+    /// <param name="errorHandler">
+    ///     RELEASE builds only: Handler for any unhandled exceptions (resumes when returns true,
+    ///     rethrows when null).
+    /// </param>
+    public void Run (Toplevel view, Func<Exception, bool>? errorHandler = null);
+
+    /// <summary>Shutdown an application initialized with <see cref="Init"/>.</summary>
+    /// <remarks>
+    ///     Shutdown must be called for every call to <see cref="Init"/> or
+    ///     <see cref="Application.Run(Toplevel, Func{Exception, bool})"/> to ensure all resources are cleaned
+    ///     up (Disposed)
+    ///     and terminal settings are restored.
+    /// </remarks>
+    public void Shutdown ();
+
+    /// <summary>Stops the provided <see cref="Toplevel"/>, causing or the <paramref name="top"/> if provided.</summary>
+    /// <param name="top">The <see cref="Toplevel"/> to stop.</param>
+    /// <remarks>
+    ///     <para>This will cause <see cref="Application.Run(Toplevel, Func{Exception, bool})"/> to return.</para>
+    ///     <para>
+    ///         Calling <see cref="RequestStop(Terminal.Gui.Toplevel)"/> is equivalent to setting the <see cref="Toplevel.Running"/>
+    ///         property on the currently running <see cref="Toplevel"/> to false.
+    ///     </para>
+    /// </remarks>
+    void RequestStop (Toplevel? top);
+
+    /// <summary>Runs <paramref name="action"/> on the main UI loop thread</summary>
+    /// <param name="action">the action to be invoked on the main processing thread.</param>
+    void Invoke (Action action);
+
+    /// <summary>
+    /// <see langword="true"/> if implementation is 'old'. <see langword="false"/> if implementation
+    /// is cutting edge.
+    /// </summary>
+    bool IsLegacy { get; }
+
+    /// <summary>
+    ///     Adds specified idle handler function to main iteration processing. The handler function will be called
+    ///     once per iteration of the main loop after other events have been handled.
+    /// </summary>
+    void AddIdle (Func<bool> func);
+
+    /// <summary>Adds a timeout to the application.</summary>
+    /// <remarks>
+    ///     When time specified passes, the callback will be invoked. If the callback returns true, the timeout will be
+    ///     reset, repeating the invocation. If it returns false, the timeout will stop and be removed. The returned value is a
+    ///     token that can be used to stop the timeout by calling <see cref="RemoveTimeout(object)"/>.
+    /// </remarks>
+    object AddTimeout (TimeSpan time, Func<bool> callback);
+
+    /// <summary>Removes a previously scheduled timeout</summary>
+    /// <remarks>The token parameter is the value returned by <see cref="AddTimeout"/>.</remarks>
+    /// <returns>
+    /// <see langword="true"/>
+    /// if the timeout is successfully removed; otherwise,
+    /// <see langword="false"/>
+    /// .
+    /// This method also returns
+    /// <see langword="false"/>
+    /// if the timeout is not found.</returns>
+    bool RemoveTimeout (object token);
+}
\ No newline at end of file
diff --git a/Terminal.Gui/Application/ITimedEvents.cs b/Terminal.Gui/Application/ITimedEvents.cs
new file mode 100644
index 0000000000..80a2769336
--- /dev/null
+++ b/Terminal.Gui/Application/ITimedEvents.cs
@@ -0,0 +1,90 @@
+#nullable enable
+using System.Collections.ObjectModel;
+
+namespace Terminal.Gui;
+
+/// <summary>
+/// Manages timers and idles
+/// </summary>
+public interface ITimedEvents
+{
+    /// <summary>
+    ///     Adds specified idle handler function to main iteration processing. The handler function will be called
+    ///     once per iteration of the main loop after other events have been handled.
+    /// </summary>
+    /// <param name="idleHandler"></param>
+    void AddIdle (Func<bool> idleHandler);
+
+    /// <summary>
+    /// Runs all idle hooks
+    /// </summary>
+    void LockAndRunIdles ();
+
+    /// <summary>
+    /// Runs all timeouts that are due
+    /// </summary>
+    void LockAndRunTimers ();
+
+    /// <summary>
+    ///     Called from <see cref="IMainLoopDriver.EventsPending"/> to check if there are any outstanding timers or idle
+    ///     handlers.
+    /// </summary>
+    /// <param name="waitTimeout">
+    ///     Returns the number of milliseconds remaining in the current timer (if any). Will be -1 if
+    ///     there are no active timers.
+    /// </param>
+    /// <returns><see langword="true"/> if there is a timer or idle handler active.</returns>
+    bool CheckTimersAndIdleHandlers (out int waitTimeout);
+
+    /// <summary>Adds a timeout to the application.</summary>
+    /// <remarks>
+    ///     When time specified passes, the callback will be invoked. If the callback returns true, the timeout will be
+    ///     reset, repeating the invocation. If it returns false, the timeout will stop and be removed. The returned value is a
+    ///     token that can be used to stop the timeout by calling <see cref="RemoveTimeout(object)"/>.
+    /// </remarks>
+    object AddTimeout (TimeSpan time, Func<bool> callback);
+
+    /// <summary>Removes a previously scheduled timeout</summary>
+    /// <remarks>The token parameter is the value returned by AddTimeout.</remarks>
+    /// <returns>
+    /// Returns
+    /// <see langword="true"/>
+    /// if the timeout is successfully removed; otherwise,
+    /// <see langword="false"/>
+    /// .
+    /// This method also returns
+    /// <see langword="false"/>
+    /// if the timeout is not found.
+    /// </returns>
+    bool RemoveTimeout (object token);
+
+    /// <summary>
+    /// Returns all currently registered idles. May not include
+    /// actively executing idles.
+    /// </summary>
+    ReadOnlyCollection<Func<bool>> IdleHandlers { get;}
+
+    /// <summary>
+    /// Returns the next planned execution time (key - UTC ticks)
+    /// for each timeout that is not actively executing.
+    /// </summary>
+    SortedList<long, Timeout> Timeouts { get; }
+
+
+    /// <summary>Removes an idle handler added with <see cref="AddIdle(Func{bool})"/> from processing.</summary>
+    /// <returns>
+    /// <see langword="true"/>
+    /// if the idle handler is successfully removed; otherwise,
+    /// <see langword="false"/>
+    /// .
+    /// This method also returns
+    /// <see langword="false"/>
+    /// if the idle handler is not found.</returns>
+    bool RemoveIdle (Func<bool> fnTrue);
+
+    /// <summary>
+    ///     Invoked when a new timeout is added. To be used in the case when
+    ///     <see cref="Application.EndAfterFirstIteration"/> is <see langword="true"/>.
+    /// </summary>
+    event EventHandler<TimeoutEventArgs>? TimeoutAdded;
+}
diff --git a/Terminal.Gui/Application/MainLoop.cs b/Terminal.Gui/Application/MainLoop.cs
index 1e6006923d..3b6cf3b9d3 100644
--- a/Terminal.Gui/Application/MainLoop.cs
+++ b/Terminal.Gui/Application/MainLoop.cs
@@ -14,7 +14,7 @@ namespace Terminal.Gui;
 internal interface IMainLoopDriver
 {
     /// <summary>Must report whether there are any events pending, or even block waiting for events.</summary>
-    /// <returns><c>true</c>, if there were pending events, <c>false</c> otherwise.</returns>
+    /// <returns><see langword="true"/>, if there were pending events, <see langword="false"/> otherwise.</returns>
     bool EventsPending ();
 
     /// <summary>The iteration function.</summary>
@@ -39,13 +39,10 @@ internal interface IMainLoopDriver
 /// </remarks>
 public class MainLoop : IDisposable
 {
-    internal List<Func<bool>> _idleHandlers = new ();
-    internal SortedList<long, Timeout> _timeouts = new ();
-
-    /// <summary>The idle handlers and lock that must be held while manipulating them</summary>
-    private readonly object _idleHandlersLock = new ();
-
-    private readonly object _timeoutsLockToken = new ();
+    /// <summary>
+    /// Gets the class responsible for handling idles and timeouts
+    /// </summary>
+    public ITimedEvents TimedEvents { get; } = new TimedEvents();
 
     /// <summary>Creates a new MainLoop.</summary>
     /// <remarks>Use <see cref="Dispose"/> to release resources.</remarks>
@@ -59,17 +56,6 @@ internal MainLoop (IMainLoopDriver driver)
         driver.Setup (this);
     }
 
-    /// <summary>Gets a copy of the list of all idle handlers.</summary>
-    internal ReadOnlyCollection<Func<bool>> IdleHandlers
-    {
-        get
-        {
-            lock (_idleHandlersLock)
-            {
-                return new List<Func<bool>> (_idleHandlers).AsReadOnly ();
-            }
-        }
-    }
 
     /// <summary>The current <see cref="IMainLoopDriver"/> in use.</summary>
     /// <value>The main loop driver.</value>
@@ -78,11 +64,6 @@ internal ReadOnlyCollection<Func<bool>> IdleHandlers
     /// <summary>Used for unit tests.</summary>
     internal bool Running { get; set; }
 
-    /// <summary>
-    ///     Gets the list of all timeouts sorted by the <see cref="TimeSpan"/> time ticks. A shorter limit time can be
-    ///     added at the end, but it will be called before an earlier addition that has a longer limit time.
-    /// </summary>
-    internal SortedList<long, Timeout> Timeouts => _timeouts;
 
     /// <inheritdoc/>
     public void Dispose ()
@@ -99,13 +80,13 @@ public void Dispose ()
     ///     once per iteration of the main loop after other events have been handled.
     /// </summary>
     /// <remarks>
-    ///     <para>Remove an idle handler by calling <see cref="RemoveIdle(Func{bool})"/> with the token this method returns.</para>
+    ///     <para>Remove an idle handler by calling <see cref="TimedEvents.RemoveIdle(Func{bool})"/> with the token this method returns.</para>
     ///     <para>
     ///         If the <paramref name="idleHandler"/> returns  <see langword="false"/> it will be removed and not called
     ///         subsequently.
     ///     </para>
     /// </remarks>
-    /// <param name="idleHandler">Token that can be used to remove the idle handler with <see cref="RemoveIdle(Func{bool})"/> .</param>
+    /// <param name="idleHandler">Token that can be used to remove the idle handler with <see cref="TimedEvents.RemoveIdle(Func{bool})"/> .</param>
     // QUESTION: Why are we re-inventing the event wheel here?
     // PERF: This is heavy.
     // CONCURRENCY: Race conditions exist here.
@@ -113,76 +94,13 @@ public void Dispose ()
     // 
     internal Func<bool> AddIdle (Func<bool> idleHandler)
     {
-        lock (_idleHandlersLock)
-        {
-            _idleHandlers.Add (idleHandler);
-        }
+        TimedEvents.AddIdle (idleHandler);
 
         MainLoopDriver?.Wakeup ();
 
         return idleHandler;
     }
 
-    /// <summary>Adds a timeout to the <see cref="MainLoop"/>.</summary>
-    /// <remarks>
-    ///     When time specified passes, the callback will be invoked. If the callback returns true, the timeout will be
-    ///     reset, repeating the invocation. If it returns false, the timeout will stop and be removed. The returned value is a
-    ///     token that can be used to stop the timeout by calling <see cref="RemoveTimeout(object)"/>.
-    /// </remarks>
-    internal object AddTimeout (TimeSpan time, Func<bool> callback)
-    {
-        ArgumentNullException.ThrowIfNull (callback);
-
-        var timeout = new Timeout { Span = time, Callback = callback };
-        AddTimeout (time, timeout);
-
-        return timeout;
-    }
-
-    /// <summary>
-    ///     Called from <see cref="IMainLoopDriver.EventsPending"/> to check if there are any outstanding timers or idle
-    ///     handlers.
-    /// </summary>
-    /// <param name="waitTimeout">
-    ///     Returns the number of milliseconds remaining in the current timer (if any). Will be -1 if
-    ///     there are no active timers.
-    /// </param>
-    /// <returns><see langword="true"/> if there is a timer or idle handler active.</returns>
-    internal bool CheckTimersAndIdleHandlers (out int waitTimeout)
-    {
-        long now = DateTime.UtcNow.Ticks;
-
-        waitTimeout = 0;
-
-        lock (_timeoutsLockToken)
-        {
-            if (_timeouts.Count > 0)
-            {
-                waitTimeout = (int)((_timeouts.Keys [0] - now) / TimeSpan.TicksPerMillisecond);
-
-                if (waitTimeout < 0)
-                {
-                    // This avoids 'poll' waiting infinitely if 'waitTimeout < 0' until some action is detected
-                    // This can occur after IMainLoopDriver.Wakeup is executed where the pollTimeout is less than 0
-                    // and no event occurred in elapsed time when the 'poll' is start running again.
-                    waitTimeout = 0;
-                }
-
-                return true;
-            }
-
-            // ManualResetEventSlim.Wait, which is called by IMainLoopDriver.EventsPending, will wait indefinitely if
-            // the timeout is -1.
-            waitTimeout = -1;
-        }
-
-        // There are no timers set, check if there are any idle handlers
-
-        lock (_idleHandlers)
-        {
-            return _idleHandlers.Count > 0;
-        }
-    }
 
     /// <summary>Determines whether there are pending events to be processed.</summary>
     /// <remarks>
@@ -191,50 +109,6 @@ internal bool CheckTimersAndIdleHandlers (out int waitTimeout)
     /// </remarks>
     internal bool EventsPending () { return MainLoopDriver!.EventsPending (); }
 
-    /// <summary>Removes an idle handler added with <see cref="AddIdle(Func{bool})"/> from processing.</summary>
-    /// <param name="token">A token returned by <see cref="AddIdle(Func{bool})"/></param>
-    /// Returns
-    /// <c>true</c>
-    /// if the idle handler is successfully removed; otherwise,
-    /// <c>false</c>
-    /// .
-    /// This method also returns
-    /// <c>false</c>
-    /// if the idle handler is not found.
-    internal bool RemoveIdle (Func<bool> token)
-    {
-        lock (_idleHandlersLock)
-        {
-            return _idleHandlers.Remove (token);
-        }
-    }
-
-    /// <summary>Removes a previously scheduled timeout</summary>
-    /// <remarks>The token parameter is the value returned by AddTimeout.</remarks>
-    /// Returns
-    /// <c>true</c>
-    /// if the timeout is successfully removed; otherwise,
-    /// <c>false</c>
-    /// .
-    /// This method also returns
-    /// <c>false</c>
-    /// if the timeout is not found.
-    internal bool RemoveTimeout (object token)
-    {
-        lock (_timeoutsLockToken)
-        {
-            int idx = _timeouts.IndexOfValue ((token as Timeout)!);
-
-            if (idx == -1)
-            {
-                return false;
-            }
-
-            _timeouts.RemoveAt (idx);
-        }
-
-        return true;
-    }
 
     /// <summary>Runs the <see cref="MainLoop"/>. Used only for unit tests.</summary>
     internal void Run ()
@@ -260,29 +134,13 @@ internal void Run ()
     /// </remarks>
     internal void RunIteration ()
     {
-        lock (_timeoutsLockToken)
-        {
-            if (_timeouts.Count > 0)
-            {
-                RunTimers ();
-            }
-        }
-
         RunAnsiScheduler ();
 
         MainLoopDriver?.Iteration ();
 
-        bool runIdle;
+        TimedEvents.LockAndRunTimers ();
 
-        lock (_idleHandlersLock)
-        {
-            runIdle = _idleHandlers.Count > 0;
-        }
-
-        if (runIdle)
-        {
-            RunIdle ();
-        }
+        TimedEvents.LockAndRunIdles ();
     }
 
     private void RunAnsiScheduler ()
@@ -297,101 +155,9 @@ internal void Stop ()
         Wakeup ();
     }
 
-    /// <summary>
-    ///     Invoked when a new timeout is added. To be used in the case when
-    ///     <see cref="Application.EndAfterFirstIteration"/> is <see langword="true"/>.
-    /// </summary>
-    internal event EventHandler<TimeoutEventArgs>? TimeoutAdded;
 
     /// <summary>Wakes up the <see cref="MainLoop"/> that might be waiting on input.</summary>
     internal void Wakeup () { MainLoopDriver?.Wakeup (); }
 
-    private void AddTimeout (TimeSpan time, Timeout timeout)
-    {
-        lock (_timeoutsLockToken)
-        {
-            long k = (DateTime.UtcNow + time).Ticks;
-            _timeouts.Add (NudgeToUniqueKey (k), timeout);
-            TimeoutAdded?.Invoke (this, new TimeoutEventArgs (timeout, k));
-        }
-    }
 
-    /// <summary>
-    ///     Finds the closest number to <paramref name="k"/> that is not present in <see cref="_timeouts"/>
-    ///     (incrementally).
-    /// </summary>
-    /// <param name="k"></param>
-    /// <returns></returns>
-    private long NudgeToUniqueKey (long k)
-    {
-        lock (_timeoutsLockToken)
-        {
-            while (_timeouts.ContainsKey (k))
-            {
-                k++;
-            }
-        }
-
-        return k;
-    }
-
-    // PERF: This is heavier than it looks.
-    // CONCURRENCY: Potential deadlock city here.
-    // CONCURRENCY: Multiple concurrency pitfalls on the delegates themselves.
-    // INTENT: It looks like the general architecture here is trying to be a form of publisher/consumer pattern.
-    private void RunIdle ()
-    {
-        List<Func<bool>> iterate;
-
-        lock (_idleHandlersLock)
-        {
-            iterate = _idleHandlers;
-            _idleHandlers = new List<Func<bool>> ();
-        }
-
-        foreach (Func<bool> idle in iterate)
-        {
-            if (idle ())
-            {
-                lock (_idleHandlersLock)
-                {
-                    _idleHandlers.Add (idle);
-                }
-            }
-        }
-    }
-
-    private void RunTimers ()
-    {
-        long now = DateTime.UtcNow.Ticks;
-        SortedList<long, Timeout> copy;
-
-        // lock prevents new timeouts being added
-        // after we have taken the copy but before
-        // we have allocated a new list (which would
-        // result in lost timeouts or errors during enumeration)
-        lock (_timeoutsLockToken)
-        {
-            copy = _timeouts;
-            _timeouts = new SortedList<long, Timeout> ();
-        }
-
-        foreach ((long k, Timeout timeout) in copy)
-        {
-            if (k < now)
-            {
-                if (timeout.Callback ())
-                {
-                    AddTimeout (timeout.Span, timeout);
-                }
-            }
-            else
-            {
-                lock (_timeoutsLockToken)
-                {
-                    _timeouts.Add (NudgeToUniqueKey (k), timeout);
-                }
-            }
-        }
-    }
 }
diff --git a/Terminal.Gui/Application/TimedEvents.cs b/Terminal.Gui/Application/TimedEvents.cs
new file mode 100644
index 0000000000..8325e6ed6e
--- /dev/null
+++ b/Terminal.Gui/Application/TimedEvents.cs
@@ -0,0 +1,257 @@
+#nullable enable
+using System.Collections.ObjectModel;
+
+namespace Terminal.Gui;
+
+/// <summary>
+/// Handles timeouts and idles
+/// </summary>
+public class TimedEvents : ITimedEvents
+{
+    internal List<Func<bool>> _idleHandlers = new ();
+    internal SortedList<long, Timeout> _timeouts = new ();
+
+    /// <summary>The idle handlers and lock that must be held while manipulating them</summary>
+    private readonly object _idleHandlersLock = new ();
+
+    private readonly object _timeoutsLockToken = new ();
+
+
+    /// <summary>Gets a copy of the list of all idle handlers.</summary>
+    public ReadOnlyCollection<Func<bool>> IdleHandlers
+    {
+        get
+        {
+            lock (_idleHandlersLock)
+            {
+                return new List<Func<bool>> (_idleHandlers).AsReadOnly ();
+            }
+        }
+    }
+
+    /// <summary>
+    ///     Gets the list of all timeouts sorted by the <see cref="TimeSpan"/> time ticks. A shorter limit time can be
+    ///     added at the end, but it will be called before an earlier addition that has a longer limit time.
+    /// </summary>
+    public SortedList<long, Timeout> Timeouts => _timeouts;
+
+    /// <inheritdoc />
+    public void AddIdle (Func<bool> idleHandler)
+    {
+        lock (_idleHandlersLock)
+        {
+            _idleHandlers.Add (idleHandler);
+        }
+    }
+
+    /// <inheritdoc/>
+    public event EventHandler<TimeoutEventArgs>? TimeoutAdded;
+
+
+    private void AddTimeout (TimeSpan time, Timeout timeout)
+    {
+        lock (_timeoutsLockToken)
+        {
+            long k = (DateTime.UtcNow + time).Ticks;
+            _timeouts.Add (NudgeToUniqueKey (k), timeout);
+            TimeoutAdded?.Invoke (this, new TimeoutEventArgs (timeout, k));
+        }
+    }
+
+    /// <summary>
+    ///     Finds the closest number to <paramref name="k"/> that is not present in <see cref="_timeouts"/>
+    ///     (incrementally).
+    /// </summary>
+    /// <param name="k"></param>
+    /// <returns></returns>
+    private long NudgeToUniqueKey (long k)
+    {
+        lock (_timeoutsLockToken)
+        {
+            while (_timeouts.ContainsKey (k))
+            {
+                k++;
+            }
+        }
+
+        return k;
+    }
+
+
+    // PERF: This is heavier than it looks.
+    // CONCURRENCY: Potential deadlock city here.
+    // CONCURRENCY: Multiple concurrency pitfalls on the delegates themselves.
+    // INTENT: It looks like the general architecture here is trying to be a form of publisher/consumer pattern.
+    private void RunIdle ()
+    {
+        Func<bool> [] iterate;
+        lock (_idleHandlersLock)
+        {
+            iterate = _idleHandlers.ToArray ();
+            _idleHandlers = new List<Func<bool>> ();
+        }
+
+        foreach (Func<bool> idle in iterate)
+        {
+            if (idle ())
+            {
+                lock (_idleHandlersLock)
+                {
+                    _idleHandlers.Add (idle);
+                }
+            }
+        }
+    }
+
+    /// <inheritdoc/>
+    public void LockAndRunTimers ()
+    {
+        lock (_timeoutsLockToken)
+        {
+            if (_timeouts.Count > 0)
+            {
+                RunTimers ();
+            }
+        }
+
+    }
+
+    /// <inheritdoc/>
+    public void LockAndRunIdles ()
+    {
+        bool runIdle;
+
+        lock (_idleHandlersLock)
+        {
+            runIdle = _idleHandlers.Count > 0;
+        }
+
+        if (runIdle)
+        {
+            RunIdle ();
+        }
+    }
+    private void RunTimers ()
+    {
+        long now = DateTime.UtcNow.Ticks;
+        SortedList<long, Timeout> copy;
+
+        // lock prevents new timeouts being added
+        // after we have taken the copy but before
+        // we have allocated a new list (which would
+        // result in lost timeouts or errors during enumeration)
+        lock (_timeoutsLockToken)
+        {
+            copy = _timeouts;
+            _timeouts = new SortedList<long, Timeout> ();
+        }
+
+        foreach ((long k, Timeout timeout) in copy)
+        {
+            if (k < now)
+            {
+                if (timeout.Callback ())
+                {
+                    AddTimeout (timeout.Span, timeout);
+                }
+            }
+            else
+            {
+                lock (_timeoutsLockToken)
+                {
+                    _timeouts.Add (NudgeToUniqueKey (k), timeout);
+                }
+            }
+        }
+    }
+
+    /// <inheritdoc/>
+    public bool RemoveIdle (Func<bool> token)
+    {
+        lock (_idleHandlersLock)
+        {
+            return _idleHandlers.Remove (token);
+        }
+    }
+
+    /// <summary>Removes a previously scheduled timeout</summary>
+    /// <remarks>The token parameter is the value returned by AddTimeout.</remarks>
+    /// Returns
+    /// <see langword="true"/>
+    /// if the timeout is successfully removed; otherwise,
+    /// <see langword="false"/>
+    /// .
+    /// This method also returns
+    /// <see langword="false"/>
+    /// if the timeout is not found.
+    public bool RemoveTimeout (object token)
+    {
+        lock (_timeoutsLockToken)
+        {
+            int idx = _timeouts.IndexOfValue ((token as Timeout)!);
+
+            if (idx == -1)
+            {
+                return false;
+            }
+
+            _timeouts.RemoveAt (idx);
+        }
+
+        return true;
+    }
+
+
+    /// <summary>Adds a timeout to the <see cref="MainLoop"/>.</summary>
+    /// <remarks>
+    ///     When time specified passes, the callback will be invoked. If the callback returns true, the timeout will be
+    ///     reset, repeating the invocation. If it returns false, the timeout will stop and be removed. The returned value is a
+    ///     token that can be used to stop the timeout by calling <see cref="RemoveTimeout(object)"/>.
+    /// </remarks>
+    public object AddTimeout (TimeSpan time, Func<bool> callback)
+    {
+        ArgumentNullException.ThrowIfNull (callback);
+
+        var timeout = new Timeout { Span = time, Callback = callback };
+        AddTimeout (time, timeout);
+
+        return timeout;
+    }
+
+    /// <inheritdoc/>
+    public bool CheckTimersAndIdleHandlers (out int waitTimeout)
+    {
+        long now = DateTime.UtcNow.Ticks;
+
+        waitTimeout = 0;
+
+        lock (_timeoutsLockToken)
+        {
+            if (_timeouts.Count > 0)
+            {
+                waitTimeout = (int)((_timeouts.Keys [0] - now) / TimeSpan.TicksPerMillisecond);
+
+                if (waitTimeout < 0)
+                {
+                    // This avoids 'poll' waiting infinitely if 'waitTimeout < 0' until some action is detected
+                    // This can occur after IMainLoopDriver.Wakeup is executed where the pollTimeout is less than 0
+                    // and no event occurred in elapsed time when the 'poll' is start running again.
+                    waitTimeout = 0;
+                }
+
+                return true;
+            }
+
+            // ManualResetEventSlim.Wait, which is called by IMainLoopDriver.EventsPending, will wait indefinitely if
+            // the timeout is -1.
+            waitTimeout = -1;
+        }
+
+        // There are no timers set, check if there are any idle handlers
+
+        lock (_idleHandlersLock)
+        {
+            return _idleHandlers.Count > 0;
+        }
+    }
+}
\ No newline at end of file
diff --git a/Terminal.Gui/Application/TimeoutEventArgs.cs b/Terminal.Gui/Application/TimeoutEventArgs.cs
index 19346e57f1..2e01228c19 100644
--- a/Terminal.Gui/Application/TimeoutEventArgs.cs
+++ b/Terminal.Gui/Application/TimeoutEventArgs.cs
@@ -1,7 +1,7 @@
 namespace Terminal.Gui;
 
-/// <summary><see cref="EventArgs"/> for timeout events (e.g. <see cref="MainLoop.TimeoutAdded"/>)</summary>
-internal class TimeoutEventArgs : EventArgs
+/// <summary><see cref="EventArgs"/> for timeout events (e.g. <see cref="TimedEvents.TimeoutAdded"/>)</summary>
+public class TimeoutEventArgs : EventArgs
 {
     /// <summary>Creates a new instance of the <see cref="TimeoutEventArgs"/> class.</summary>
     /// <param name="timeout"></param>
diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiMouseParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiMouseParser.cs
new file mode 100644
index 0000000000..5ae29060bf
--- /dev/null
+++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiMouseParser.cs
@@ -0,0 +1,267 @@
+#nullable enable
+using System.Text.RegularExpressions;
+
+namespace Terminal.Gui;
+
+/// <summary>
+///     Parses mouse ansi escape sequences into <see cref="MouseEventArgs"/>
+///     including support for pressed, released and mouse wheel.
+/// </summary>
+public class AnsiMouseParser
+{
+    // Regex patterns for button press/release, wheel scroll, and mouse position reporting
+    private readonly Regex _mouseEventPattern = new (@"\u001b\[<(\d+);(\d+);(\d+)(M|m)", RegexOptions.Compiled);
+
+    /// <summary>
+    ///     Returns true if it is a mouse event
+    /// </summary>
+    /// <param name="cur"></param>
+    /// <returns></returns>
+    public bool IsMouse (string cur)
+    {
+        // Typically in this format
+        // ESC [ < {button_code};{x_pos};{y_pos}{final_byte}
+        return cur.EndsWith ('M') || cur.EndsWith ('m');
+    }
+
+    /// <summary>
+    ///     Parses a mouse ansi escape sequence into a mouse event. Returns null if input
+    ///     is not a mouse event or its syntax is not understood.
+    /// </summary>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    public MouseEventArgs? ProcessMouseInput (string input)
+    {
+        // Match mouse wheel events first
+        Match match = _mouseEventPattern.Match (input);
+
+        if (match.Success)
+        {
+            int buttonCode = int.Parse (match.Groups [1].Value);
+
+            // The top-left corner of the terminal corresponds to (1, 1) for both X (column) and Y (row) coordinates.
+            // ANSI standards and terminal conventions historically treat screen positions as 1 - based.
+
+            int x = int.Parse (match.Groups [2].Value) - 1;
+            int y = int.Parse (match.Groups [3].Value) - 1;
+            char terminator = match.Groups [4].Value.Single ();
+
+            var m = new MouseEventArgs
+            {
+                Position = new (x, y),
+                Flags = GetFlags (buttonCode, terminator)
+            };
+
+            Logging.Trace ($"{nameof (AnsiMouseParser)} handled as {input} mouse {m.Flags} at {m.Position}");
+
+            return m;
+        }
+
+        // its some kind of odd mouse event that doesn't follow expected format?
+        return null;
+    }
+
+    private static MouseFlags GetFlags (int buttonCode, char terminator)
+    {
+        MouseFlags buttonState = 0;
+
+        switch (buttonCode)
+        {
+            case 0:
+            case 8:
+            case 16:
+            case 24:
+            case 32:
+            case 36:
+            case 40:
+            case 48:
+            case 56:
+                buttonState = terminator == 'M'
+                                  ? MouseFlags.Button1Pressed
+                                  : MouseFlags.Button1Released;
+
+                break;
+            case 1:
+            case 9:
+            case 17:
+            case 25:
+            case 33:
+            case 37:
+            case 41:
+            case 45:
+            case 49:
+            case 53:
+            case 57:
+            case 61:
+                buttonState = terminator == 'M'
+                                  ? MouseFlags.Button2Pressed
+                                  : MouseFlags.Button2Released;
+
+                break;
+            case 2:
+            case 10:
+            case 14:
+            case 18:
+            case 22:
+            case 26:
+            case 30:
+            case 34:
+            case 42:
+            case 46:
+            case 50:
+            case 54:
+            case 58:
+            case 62:
+                buttonState = terminator == 'M'
+                                  ? MouseFlags.Button3Pressed
+                                  : MouseFlags.Button3Released;
+
+                break;
+            case 35:
+            //// Needed for Windows OS
+            //if (isButtonPressed && c == 'm'
+            //	&& (lastMouseEvent.ButtonState == MouseFlags.Button1Pressed
+            //	|| lastMouseEvent.ButtonState == MouseFlags.Button2Pressed
+            //	|| lastMouseEvent.ButtonState == MouseFlags.Button3Pressed)) {
+
+            //	switch (lastMouseEvent.ButtonState) {
+            //	case MouseFlags.Button1Pressed:
+            //		buttonState = MouseFlags.Button1Released;
+            //		break;
+            //	case MouseFlags.Button2Pressed:
+            //		buttonState = MouseFlags.Button2Released;
+            //		break;
+            //	case MouseFlags.Button3Pressed:
+            //		buttonState = MouseFlags.Button3Released;
+            //		break;
+            //	}
+            //} else {
+            //	buttonState = MouseFlags.ReportMousePosition;
+            //}
+            //break;
+            case 39:
+            case 43:
+            case 47:
+            case 51:
+            case 55:
+            case 59:
+            case 63:
+                buttonState = MouseFlags.ReportMousePosition;
+
+                break;
+            case 64:
+                buttonState = MouseFlags.WheeledUp;
+
+                break;
+            case 65:
+                buttonState = MouseFlags.WheeledDown;
+
+                break;
+            case 68:
+            case 72:
+            case 80:
+                buttonState = MouseFlags.WheeledLeft; // Shift/Ctrl+WheeledUp
+
+                break;
+            case 69:
+            case 73:
+            case 81:
+                buttonState = MouseFlags.WheeledRight; // Shift/Ctrl+WheeledDown
+
+                break;
+        }
+
+        // Modifiers.
+        switch (buttonCode)
+        {
+            case 8:
+            case 9:
+            case 10:
+            case 43:
+                buttonState |= MouseFlags.ButtonAlt;
+
+                break;
+            case 14:
+            case 47:
+                buttonState |= MouseFlags.ButtonAlt | MouseFlags.ButtonShift;
+
+                break;
+            case 16:
+            case 17:
+            case 18:
+            case 51:
+                buttonState |= MouseFlags.ButtonCtrl;
+
+                break;
+            case 22:
+            case 55:
+                buttonState |= MouseFlags.ButtonCtrl | MouseFlags.ButtonShift;
+
+                break;
+            case 24:
+            case 25:
+            case 26:
+            case 59:
+                buttonState |= MouseFlags.ButtonCtrl | MouseFlags.ButtonAlt;
+
+                break;
+            case 30:
+            case 63:
+                buttonState |= MouseFlags.ButtonCtrl | MouseFlags.ButtonShift | MouseFlags.ButtonAlt;
+
+                break;
+            case 32:
+            case 33:
+            case 34:
+                buttonState |= MouseFlags.ReportMousePosition;
+
+                break;
+            case 36:
+            case 37:
+                buttonState |= MouseFlags.ReportMousePosition | MouseFlags.ButtonShift;
+
+                break;
+            case 39:
+            case 68:
+            case 69:
+                buttonState |= MouseFlags.ButtonShift;
+
+                break;
+            case 40:
+            case 41:
+            case 42:
+                buttonState |= MouseFlags.ReportMousePosition | MouseFlags.ButtonAlt;
+
+                break;
+            case 45:
+            case 46:
+                buttonState |= MouseFlags.ReportMousePosition | MouseFlags.ButtonAlt | MouseFlags.ButtonShift;
+
+                break;
+            case 48:
+            case 49:
+            case 50:
+                buttonState |= MouseFlags.ReportMousePosition | MouseFlags.ButtonCtrl;
+
+                break;
+            case 53:
+            case 54:
+                buttonState |= MouseFlags.ReportMousePosition | MouseFlags.ButtonCtrl | MouseFlags.ButtonShift;
+
+                break;
+            case 56:
+            case 57:
+            case 58:
+                buttonState |= MouseFlags.ReportMousePosition | MouseFlags.ButtonCtrl | MouseFlags.ButtonAlt;
+
+                break;
+            case 61:
+            case 62:
+                buttonState |= MouseFlags.ReportMousePosition | MouseFlags.ButtonCtrl | MouseFlags.ButtonShift | MouseFlags.ButtonAlt;
+
+                break;
+        }
+
+        return buttonState;
+    }
+}
diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiRequestScheduler.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiRequestScheduler.cs
index d2a7841c44..aec693de55 100644
--- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiRequestScheduler.cs
+++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiRequestScheduler.cs
@@ -68,7 +68,7 @@ public AnsiRequestScheduler (IAnsiResponseParser parser, Func<DateTime>? now = n
 
     /// <summary>
     ///     Sends the <paramref name="request"/> immediately or queues it if there is already
-    ///     an outstanding request for the given <see cref="AnsiEscapeSequenceRequest.Terminator"/>.
+    ///     an outstanding request for the given <see cref="AnsiEscapeSequence.Terminator"/>.
     /// </summary>
     /// <param name="request"></param>
     /// <returns><see langword="true"/> if request was sent immediately. <see langword="false"/> if it was queued.</returns>
@@ -213,4 +213,4 @@ private bool ShouldThrottle (AnsiEscapeSequenceRequest r)
 
         return false;
     }
-}
\ No newline at end of file
+}
diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs
index 503a637695..93d2bbeb9a 100644
--- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs
+++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs
@@ -1,13 +1,40 @@
 #nullable enable
 
+using Microsoft.Extensions.Logging;
+
 namespace Terminal.Gui;
 
 internal abstract class AnsiResponseParserBase : IAnsiResponseParser
 {
+    private const char Escape = '\x1B';
+    private readonly AnsiMouseParser _mouseParser = new ();
+    protected readonly AnsiKeyboardParser _keyboardParser = new ();
     protected object _lockExpectedResponses = new ();
 
     protected object _lockState = new ();
 
+    /// <summary>
+    ///     Event raised when mouse events are detected - requires setting <see cref="HandleMouse"/> to true
+    /// </summary>
+    public event EventHandler<MouseEventArgs>? Mouse;
+
+    /// <summary>
+    ///     Event raised when keyboard event is detected (e.g. cursors) - requires setting <see cref="HandleKeyboard"/>
+    /// </summary>
+    public event EventHandler<Key>? Keyboard;
+
+    /// <summary>
+    ///     True to explicitly handle mouse escape sequences by passing them to <see cref="Mouse"/> event.
+    ///     Defaults to <see langword="false"/>
+    /// </summary>
+    public bool HandleMouse { get; set; } = false;
+
+    /// <summary>
+    ///     True to explicitly handle keyboard escape sequences (such as cursor keys) by passing them to <see cref="Keyboard"/>
+    ///     event
+    /// </summary>
+    public bool HandleKeyboard { get; set; } = false;
+
     /// <summary>
     ///     Responses we are expecting to come in.
     /// </summary>
@@ -110,7 +137,7 @@ int inputLength
             char currentChar = getCharAtIndex (index);
             object currentObj = getObjectAtIndex (index);
 
-            bool isEscape = currentChar == '\x1B';
+            bool isEscape = currentChar == Escape;
 
             switch (State)
             {
@@ -118,7 +145,7 @@ int inputLength
                     if (isEscape)
                     {
                         // Escape character detected, move to ExpectingBracket state
-                        State = AnsiResponseParserState.ExpectingBracket;
+                        State = AnsiResponseParserState.ExpectingEscapeSequence;
                         _heldContent.AddToHeld (currentObj); // Hold the escape character
                     }
                     else
@@ -129,18 +156,22 @@ int inputLength
 
                     break;
 
-                case AnsiResponseParserState.ExpectingBracket:
+                case AnsiResponseParserState.ExpectingEscapeSequence:
                     if (isEscape)
                     {
                         // Second escape so we must release first
-                        ReleaseHeld (appendOutput, AnsiResponseParserState.ExpectingBracket);
+                        ReleaseHeld (appendOutput, AnsiResponseParserState.ExpectingEscapeSequence);
                         _heldContent.AddToHeld (currentObj); // Hold the new escape
                     }
-                    else if (currentChar == '[')
+                    else if (_heldContent.Length == 1)
                     {
-                        // Detected '[', transition to InResponse state
+                        //We need O for SS3 mode F1-F4 e.g. "<esc>OP" => F1
+                        //We need any letter or digit for Alt+Letter (see EscAsAltPattern)
+                        //In fact lets just always see what comes after esc
+
+                        // Detected '[' or 'O', transition to InResponse state
                         State = AnsiResponseParserState.InResponse;
-                        _heldContent.AddToHeld (currentObj); // Hold the '['
+                        _heldContent.AddToHeld (currentObj); // Hold the letter
                     }
                     else
                     {
@@ -152,12 +183,24 @@ int inputLength
                     break;
 
                 case AnsiResponseParserState.InResponse:
-                    _heldContent.AddToHeld (currentObj);
 
-                    // Check if the held content should be released
-                    if (ShouldReleaseHeldContent ())
+                    // if seeing another esc, we must resolve the current one first
+                    if (isEscape)
                     {
                         ReleaseHeld (appendOutput);
+                        State = AnsiResponseParserState.ExpectingEscapeSequence;
+                        _heldContent.AddToHeld (currentObj);
+                    }
+                    else
+                    {
+                        // Non esc, so continue to build sequence
+                        _heldContent.AddToHeld (currentObj);
+
+                        // Check if the held content should be released
+                        if (ShouldReleaseHeldContent ())
+                        {
+                            ReleaseHeld (appendOutput);
+                        }
                     }
 
                     break;
@@ -169,6 +212,8 @@ int inputLength
 
     private void ReleaseHeld (Action<object> appendOutput, AnsiResponseParserState newState = AnsiResponseParserState.Normal)
     {
+        TryLastMinuteSequences ();
+
         foreach (object o in _heldContent.HeldToObjects ())
         {
             appendOutput (o);
@@ -178,6 +223,48 @@ private void ReleaseHeld (Action<object> appendOutput, AnsiResponseParserState n
         _heldContent.ClearHeld ();
     }
 
+    /// <summary>
+    ///     Checks current held chars against any sequences that have
+    ///     conflicts with longer sequences e.g. Esc as Alt sequences
+    ///     which can conflict if resolved earlier e.g. with EscOP ss3
+    ///     sequences.
+    /// </summary>
+    protected void TryLastMinuteSequences ()
+    {
+        lock (_lockState)
+        {
+            string cur = _heldContent.HeldToString ();
+
+            if (HandleKeyboard)
+            {
+                AnsiKeyboardParserPattern? pattern = _keyboardParser.IsKeyboard (cur, true);
+
+                if (pattern != null)
+                {
+                    RaiseKeyboardEvent (pattern, cur);
+                    _heldContent.ClearHeld ();
+
+                    return;
+                }
+            }
+
+            // We have something totally unexpected, not a CSI and
+            // still Esc+<something>. So give last minute swallow chance
+            if (cur.Length >= 2 && cur [0] == Escape)
+            {
+                // Maybe swallow anyway if user has custom delegate
+                bool swallow = ShouldSwallowUnexpectedResponse ();
+
+                if (swallow)
+                {
+                    _heldContent.ClearHeld ();
+
+                    Logging.Trace ($"AnsiResponseParser last minute swallowed '{cur}'");
+                }
+            }
+        }
+    }
+
     // Common response handler logic
     protected bool ShouldReleaseHeldContent ()
     {
@@ -185,6 +272,27 @@ protected bool ShouldReleaseHeldContent ()
         {
             string cur = _heldContent.HeldToString ();
 
+            if (HandleMouse && IsMouse (cur))
+            {
+                RaiseMouseEvent (cur);
+                ResetState ();
+
+                return false;
+            }
+
+            if (HandleKeyboard)
+            {
+                AnsiKeyboardParserPattern? pattern = _keyboardParser.IsKeyboard (cur);
+
+                if (pattern != null)
+                {
+                    RaiseKeyboardEvent (pattern, cur);
+                    ResetState ();
+
+                    return false;
+                }
+            }
+
             lock (_lockExpectedResponses)
             {
                 // Look for an expected response for what is accumulated so far (since Esc)
@@ -232,6 +340,8 @@ protected bool ShouldReleaseHeldContent ()
                 {
                     _heldContent.ClearHeld ();
 
+                    Logging.Trace ($"AnsiResponseParser swallowed '{cur}'");
+
                     // Do not send back to input stream
                     return false;
                 }
@@ -244,6 +354,32 @@ protected bool ShouldReleaseHeldContent ()
         return false; // Continue accumulating
     }
 
+    private void RaiseMouseEvent (string cur)
+    {
+        MouseEventArgs? ev = _mouseParser.ProcessMouseInput (cur);
+
+        if (ev != null)
+        {
+            Mouse?.Invoke (this, ev);
+        }
+    }
+
+    private bool IsMouse (string cur) { return _mouseParser.IsMouse (cur); }
+
+    protected void RaiseKeyboardEvent (AnsiKeyboardParserPattern pattern, string cur)
+    {
+        Key? k = pattern.GetKey (cur);
+
+        if (k is null)
+        {
+            Logging.Logger.LogError ($"Failed to determine a Key for given Keyboard escape sequence '{cur}'");
+        }
+        else
+        {
+            Keyboard?.Invoke (this, k);
+        }
+    }
+
     /// <summary>
     ///     <para>
     ///         When overriden in a derived class, indicates whether the unexpected response
@@ -265,6 +401,8 @@ private bool MatchResponse (string cur, List<AnsiResponseExpectation> collection
 
         if (matchingResponse?.Response != null)
         {
+            Logging.Trace ($"AnsiResponseParser processed '{cur}'");
+
             if (invokeCallback)
             {
                 matchingResponse.Response.Invoke (_heldContent);
@@ -339,8 +477,10 @@ public void StopExpecting (string terminator, bool persistent)
     }
 }
 
-internal class AnsiResponseParser<T> () : AnsiResponseParserBase (new GenericHeld<T> ())
+internal class AnsiResponseParser<T> : AnsiResponseParserBase
 {
+    public AnsiResponseParser () : base (new GenericHeld<T> ()) { }
+
     /// <inheritdoc cref="AnsiResponseParser.UnknownResponseHandler"/>
     public Func<IEnumerable<Tuple<char, T>>, bool> UnexpectedResponseHandler { get; set; } = _ => false;
 
@@ -351,17 +491,27 @@ public IEnumerable<Tuple<char, T>> ProcessInput (params Tuple<char, T> [] input)
         ProcessInputBase (
                           i => input [i].Item1,
                           i => input [i],
-                          c => output.Add ((Tuple<char, T>)c),
+                          c => AppendOutput (output, c),
                           input.Length);
 
         return output;
     }
 
+    private void AppendOutput (List<Tuple<char, T>> output, object c)
+    {
+        Tuple<char, T> tuple = (Tuple<char, T>)c;
+
+        Logging.Trace ($"AnsiResponseParser releasing '{tuple.Item1}'");
+        output.Add (tuple);
+    }
+
     public Tuple<char, T> [] Release ()
     {
         // Lock in case Release is called from different Thread from parse
         lock (_lockState)
         {
+            TryLastMinuteSequences ();
+
             Tuple<char, T> [] result = HeldToEnumerable ().ToArray ();
 
             ResetState ();
@@ -421,16 +571,24 @@ public string ProcessInput (string input)
         ProcessInputBase (
                           i => input [i],
                           i => input [i], // For string there is no T so object is same as char
-                          c => output.Append ((char)c),
+                          c => AppendOutput (output, (char)c),
                           input.Length);
 
         return output.ToString ();
     }
 
+    private void AppendOutput (StringBuilder output, char c)
+    {
+        Logging.Trace ($"AnsiResponseParser releasing '{c}'");
+        output.Append (c);
+    }
+
     public string Release ()
     {
         lock (_lockState)
         {
+            TryLastMinuteSequences ();
+
             string output = _heldContent.HeldToString ();
             ResetState ();
 
diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParserState.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParserState.cs
index 934b6eb3eb..a28050f643 100644
--- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParserState.cs
+++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParserState.cs
@@ -12,9 +12,10 @@ public enum AnsiResponseParserState
 
     /// <summary>
     ///     Parser has encountered an Esc and is waiting to see if next
-    ///     key(s) continue to form an Ansi escape sequence
+    ///     key(s) continue to form an Ansi escape sequence (typically '[' but
+    ///     also other characters e.g. O for SS3).
     /// </summary>
-    ExpectingBracket,
+    ExpectingEscapeSequence,
 
     /// <summary>
     ///     Parser has encountered Esc[ and considers that it is in the process
diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/GenericHeld.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/GenericHeld.cs
index ffd8d46891..8a5955da40 100644
--- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/GenericHeld.cs
+++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/GenericHeld.cs
@@ -16,4 +16,7 @@ internal class GenericHeld<T> : IHeld
     public IEnumerable<object> HeldToObjects () { return held; }
 
     public void AddToHeld (object o) { held.Add ((Tuple<char, T>)o); }
+
+    /// <inheritdoc/>
+    public int Length => held.Count;
 }
diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/IAnsiResponseParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/IAnsiResponseParser.cs
index dbcd16b954..f0424711b2 100644
--- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/IAnsiResponseParser.cs
+++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/IAnsiResponseParser.cs
@@ -3,7 +3,7 @@ namespace Terminal.Gui;
 
 /// <summary>
 ///     When implemented in a derived class, allows watching an input stream of characters
-///     (i.e. console input) for ANSI response sequences.
+///     (i.e. console input) for ANSI response sequences (mouse input, cursor, query responses etc.).
 /// </summary>
 public interface IAnsiResponseParser
 {
diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/IHeld.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/IHeld.cs
index ab23f477fd..1f0079dbed 100644
--- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/IHeld.cs
+++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/IHeld.cs
@@ -30,4 +30,6 @@ internal interface IHeld
     /// </summary>
     /// <param name="o"></param>
     void AddToHeld (object o);
+
+    int Length { get; }
 }
diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/AnsiKeyboardParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/AnsiKeyboardParser.cs
new file mode 100644
index 0000000000..c0da8e023a
--- /dev/null
+++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/AnsiKeyboardParser.cs
@@ -0,0 +1,27 @@
+#nullable enable
+namespace Terminal.Gui;
+
+/// <summary>
+///     Parses ANSI escape sequence strings that describe keyboard activity into <see cref="Key"/>.
+/// </summary>
+public class AnsiKeyboardParser
+{
+    private readonly List<AnsiKeyboardParserPattern> _patterns = new ()
+    {
+        new Ss3Pattern (),
+        new CsiKeyPattern (),
+        new EscAsAltPattern { IsLastMinute = true }
+    };
+
+    /// <summary>
+    ///     Looks for any pattern that matches the <paramref name="input"/> and returns
+    ///     the matching pattern or <see langword="null"/> if no matches.
+    /// </summary>
+    /// <param name="input"></param>
+    /// <param name="isLastMinute"></param>
+    /// <returns></returns>
+    public AnsiKeyboardParserPattern? IsKeyboard (string input, bool isLastMinute = false)
+    {
+        return _patterns.FirstOrDefault (pattern => pattern.IsLastMinute == isLastMinute && pattern.IsMatch (input));
+    }
+}
diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/AnsiKeyboardParserPattern.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/AnsiKeyboardParserPattern.cs
new file mode 100644
index 0000000000..52b6526853
--- /dev/null
+++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/AnsiKeyboardParserPattern.cs
@@ -0,0 +1,55 @@
+#nullable enable
+namespace Terminal.Gui;
+
+/// <summary>
+///     Base class for ANSI keyboard parsing patterns.
+/// </summary>
+public abstract class AnsiKeyboardParserPattern
+{
+    /// <summary>
+    ///     Does this pattern dangerously overlap with other sequences
+    ///     such that it should only be applied at the lsat second after
+    ///     all other sequences have been tried.
+    ///     <remarks>
+    ///         When <see langword="true"/> this pattern will only be used
+    ///         at <see cref="AnsiResponseParser.Release"/> time.
+    ///     </remarks>
+    /// </summary>
+    public bool IsLastMinute { get; set; }
+
+    /// <summary>
+    ///     Returns <see langword="true"/> if <paramref name="input"/> is one
+    ///     of the terminal sequences recognised by this class.
+    /// </summary>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    public abstract bool IsMatch (string input);
+
+    private readonly string _name;
+
+    /// <summary>
+    ///     Creates a new instance of the class.
+    /// </summary>
+    protected AnsiKeyboardParserPattern () { _name = GetType ().Name; }
+
+    /// <summary>
+    ///     Returns the <see cref="Key"/> described by the escape sequence.
+    /// </summary>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    public Key? GetKey (string input)
+    {
+        Key? key = GetKeyImpl (input);
+        Logging.Trace ($"{nameof (AnsiKeyboardParser)} interpreted {input} as {key} using {_name}");
+
+        return key;
+    }
+
+    /// <summary>
+    ///     When overriden in a derived class, returns the <see cref="Key"/>
+    ///     that matches the input ansi escape sequence.
+    /// </summary>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    protected abstract Key? GetKeyImpl (string input);
+}
diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/CsiKeyPattern.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/CsiKeyPattern.cs
new file mode 100644
index 0000000000..9c442e5e21
--- /dev/null
+++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/CsiKeyPattern.cs
@@ -0,0 +1,86 @@
+#nullable enable
+using System.Text.RegularExpressions;
+
+namespace Terminal.Gui;
+
+/// <summary>
+///     Detects ansi escape sequences in strings that have been read from
+///     the terminal (see <see cref="IAnsiResponseParser"/>). This pattern
+///     handles keys that begin <c>Esc[</c> e.g. <c>Esc[A</c> - cursor up
+/// </summary>
+public class CsiKeyPattern : AnsiKeyboardParserPattern
+{
+    private readonly Dictionary<string, Key> _terminators = new()
+    {
+        { "A", Key.CursorUp },
+        { "B", Key.CursorDown },
+        { "C", Key.CursorRight },
+        { "D", Key.CursorLeft },
+        { "H", Key.Home }, // Home (older variant)
+        { "F", Key.End }, // End (older variant)
+        { "1~", Key.Home }, // Home (modern variant)
+        { "4~", Key.End }, // End (modern variant)
+        { "5~", Key.PageUp },
+        { "6~", Key.PageDown },
+        { "2~", Key.InsertChar },
+        { "3~", Key.Delete },
+        { "11~", Key.F1 },
+        { "12~", Key.F2 },
+        { "13~", Key.F3 },
+        { "14~", Key.F4 },
+        { "15~", Key.F5 },
+        { "17~", Key.F6 },
+        { "18~", Key.F7 },
+        { "19~", Key.F8 },
+        { "20~", Key.F9 },
+        { "21~", Key.F10 },
+        { "23~", Key.F11 },
+        { "24~", Key.F12 }
+    };
+
+    private readonly Regex _pattern;
+
+    /// <inheritdoc/>
+    public override bool IsMatch (string input) { return _pattern.IsMatch (input); }
+
+    /// <summary>
+    ///     Creates a new instance of the <see cref="CsiKeyPattern"/> class.
+    /// </summary>
+    public CsiKeyPattern ()
+    {
+        var terms = new string (_terminators.Select (k => k.Key [0]).Where (k => !char.IsDigit (k)).ToArray ());
+        _pattern = new (@$"^\u001b\[(1;(\d+))?([{terms}]|\d+~)$");
+    }
+
+    protected override Key? GetKeyImpl (string input)
+    {
+        Match match = _pattern.Match (input);
+
+        if (!match.Success)
+        {
+            return null;
+        }
+
+        string terminator = match.Groups [3].Value;
+        string modifierGroup = match.Groups [2].Value;
+
+        Key? key = _terminators.GetValueOrDefault (terminator);
+
+        if (key != null && int.TryParse (modifierGroup, out int modifier))
+        {
+            key = modifier switch
+                  {
+                      2 => key.WithShift,
+                      3 => key.WithAlt,
+                      4 => key.WithAlt.WithShift,
+                      5 => key.WithCtrl,
+                      6 => key.WithCtrl.WithShift,
+                      7 => key.WithCtrl.WithAlt,
+                      8 => key.WithCtrl.WithAlt.WithShift,
+                      _ => key
+                  };
+        }
+
+        return key;
+    }
+}
diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/EscAsAltPattern.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/EscAsAltPattern.cs
new file mode 100644
index 0000000000..05fc5ebe48
--- /dev/null
+++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/EscAsAltPattern.cs
@@ -0,0 +1,27 @@
+#nullable enable
+using System.Text.RegularExpressions;
+
+namespace Terminal.Gui;
+
+internal class EscAsAltPattern : AnsiKeyboardParserPattern
+{
+    public EscAsAltPattern () { IsLastMinute = true; }
+
+    private static readonly Regex _pattern = new (@"^\u001b([a-zA-Z0-9_])$");
+
+    public override bool IsMatch (string input) { return _pattern.IsMatch (input); }
+
+    protected override Key? GetKeyImpl (string input)
+    {
+        Match match = _pattern.Match (input);
+
+        if (!match.Success)
+        {
+            return null;
+        }
+
+        char key = match.Groups [1].Value [0];
+
+        return new Key (key).WithAlt;
+    }
+}
diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/Ss3Pattern.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/Ss3Pattern.cs
new file mode 100644
index 0000000000..2e5847e0d1
--- /dev/null
+++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/Ss3Pattern.cs
@@ -0,0 +1,45 @@
+#nullable enable
+using System.Text.RegularExpressions;
+
+namespace Terminal.Gui;
+
+/// <summary>
+///     Parser for SS3 terminal escape sequences. These describe specific keys e.g.
+///     <c>EscOP</c> is F1.
+/// </summary>
+public class Ss3Pattern : AnsiKeyboardParserPattern
+{
+    private static readonly Regex _pattern = new (@"^\u001bO([PQRStDCAB])$");
+
+    /// <inheritdoc/>
+    public override bool IsMatch (string input) { return _pattern.IsMatch (input); }
+
+    /// <summary>
+    ///     Returns the ss3 key that corresponds to the provided input escape sequence
+    /// </summary>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    protected override Key? GetKeyImpl (string input)
+    {
+        Match match = _pattern.Match (input);
+
+        if (!match.Success)
+        {
+            return null;
+        }
+
+        return match.Groups [1].Value.Single () switch
+               {
+                   'P' => Key.F1,
+                   'Q' => Key.F2,
+                   'R' => Key.F3,
+                   'S' => Key.F4,
+                   't' => Key.F5,
+                   'D' => Key.CursorLeft,
+                   'C' => Key.CursorRight,
+                   'A' => Key.CursorUp,
+                   'B' => Key.CursorDown,
+                   _ => null
+               };
+    }
+}
diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/StringHeld.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/StringHeld.cs
index f8b7f4e0a0..376781e702 100644
--- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/StringHeld.cs
+++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/StringHeld.cs
@@ -15,4 +15,7 @@ internal class StringHeld : IHeld
     public IEnumerable<object> HeldToObjects () { return _held.ToString ().Select (c => (object)c); }
 
     public void AddToHeld (object o) { _held.Append ((char)o); }
+
+    /// <inheritdoc/>
+    public int Length => _held.Length;
 }
diff --git a/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs b/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs
index 2ba8ba6018..46b7f50332 100644
--- a/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs
+++ b/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs
@@ -4,7 +4,7 @@
 
 namespace Terminal.Gui;
 
-/// <summary>Base class for Terminal.Gui ConsoleDriver implementations.</summary>
+/// <summary>Base class for Terminal.Gui IConsoleDriver 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"/>
@@ -558,19 +558,19 @@ public void Refresh ()
 
     #region Color Handling
 
-    /// <summary>Gets whether the <see cref="ConsoleDriver"/> supports TrueColor output.</summary>
+    /// <summary>Gets whether the <see cref="IConsoleDriver"/> supports TrueColor output.</summary>
     public virtual bool SupportsTrueColor => true;
 
-    // TODO: This makes ConsoleDriver dependent on Application, which is not ideal. This should be moved to Application.
+    // TODO: This makes IConsoleDriver dependent on Application, which is not ideal. This should be moved to Application.
     // BUGBUG: Application.Force16Colors should be bool? so if SupportsTrueColor and Application.Force16Colors == false, this doesn't override
     /// <summary>
-    ///     Gets or sets whether the <see cref="ConsoleDriver"/> should use 16 colors instead of the default TrueColors.
+    ///     Gets or sets whether the <see cref="IConsoleDriver"/> 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.
+    ///         Will be forced to <see langword="true"/> if <see cref="IConsoleDriver.SupportsTrueColor"/> is
+    ///         <see langword="false"/>, indicating that the <see cref="IConsoleDriver"/> cannot support TrueColor.
     ///     </para>
     /// </remarks>
     public virtual bool Force16Colors
@@ -592,7 +592,7 @@ public Attribute CurrentAttribute
         get => _currentAttribute;
         set
         {
-            // TODO: This makes ConsoleDriver dependent on Application, which is not ideal. Once Attribute.PlatformColor is removed, this can be fixed.
+            // TODO: This makes IConsoleDriver dependent on Application, which is not ideal. Once Attribute.PlatformColor is removed, this can be fixed.
             if (Application.Driver is { })
             {
                 _currentAttribute = new (value.Foreground, value.Background);
diff --git a/Terminal.Gui/ConsoleDrivers/CursesDriver/UnixMainLoop.cs b/Terminal.Gui/ConsoleDrivers/CursesDriver/UnixMainLoop.cs
index 4b2daea409..cb6a0f5d12 100644
--- a/Terminal.Gui/ConsoleDrivers/CursesDriver/UnixMainLoop.cs
+++ b/Terminal.Gui/ConsoleDrivers/CursesDriver/UnixMainLoop.cs
@@ -102,7 +102,7 @@ bool IMainLoopDriver.EventsPending ()
 
         UpdatePollMap ();
 
-        bool checkTimersResult = _mainLoop!.CheckTimersAndIdleHandlers (out int pollTimeout);
+        bool checkTimersResult = _mainLoop!.TimedEvents.CheckTimersAndIdleHandlers (out int pollTimeout);
 
         int n = poll (_pollMap!, (uint)_pollMap!.Length, pollTimeout);
 
diff --git a/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs b/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs
index 70c4ba4478..5e9a883d72 100644
--- a/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs
+++ b/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs
@@ -1,5 +1,5 @@
 //
-// FakeDriver.cs: A fake ConsoleDriver for unit tests. 
+// FakeDriver.cs: A fake IConsoleDriver for unit tests. 
 //
 
 using System.Diagnostics;
@@ -10,7 +10,7 @@
 
 namespace Terminal.Gui;
 
-/// <summary>Implements a mock ConsoleDriver for unit testing</summary>
+/// <summary>Implements a mock IConsoleDriver for unit testing</summary>
 public class FakeDriver : ConsoleDriver
 {
 #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
diff --git a/Terminal.Gui/ConsoleDrivers/IConsoleDriver.cs b/Terminal.Gui/ConsoleDrivers/IConsoleDriver.cs
index af41abe667..a661b84393 100644
--- a/Terminal.Gui/ConsoleDrivers/IConsoleDriver.cs
+++ b/Terminal.Gui/ConsoleDrivers/IConsoleDriver.cs
@@ -22,6 +22,7 @@ public interface IConsoleDriver
     /// <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.
@@ -33,8 +34,7 @@ public interface IConsoleDriver
 
     // BUGBUG: This should not be publicly settable.
     /// <summary>
-    ///     Gets or sets the contents of the application output. The driver outputs this buffer to the terminal when
-    ///     <see cref="UpdateScreen"/> is called.
+    ///     Gets or sets the contents of the application output. The driver outputs this buffer to the terminal.
     ///     <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; }
@@ -93,17 +93,6 @@ public interface IConsoleDriver
     /// </returns>
     bool IsRuneSupported (Rune rune);
 
-    // BUGBUG: This is not referenced. Can it be removed?
-    /// <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>
@@ -173,9 +162,15 @@ public interface IConsoleDriver
     /// <param name="str">String.</param>
     void AddStr (string str);
 
+    /// <summary>Clears the <see cref="ConsoleDriver.Contents"/> of the driver.</summary>
+    void ClearContents ();
+
     /// <summary>
     ///     Fills the specified rectangle with the specified rune, using <see cref="ConsoleDriver.CurrentAttribute"/>
     /// </summary>
+    event EventHandler<EventArgs> ClearedContents;
+
+    /// <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.
@@ -192,31 +187,15 @@ public interface IConsoleDriver
     /// <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>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>
@@ -235,10 +214,6 @@ public interface IConsoleDriver
     /// </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 ();
@@ -264,21 +239,9 @@ public interface IConsoleDriver
     /// <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;
 
-    // BUGBUG: This is not referenced. Can it be removed?
-    /// <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"/>
@@ -287,16 +250,6 @@ public interface IConsoleDriver
     /// </remarks>
     event EventHandler<Key>? KeyUp;
 
-    // BUGBUG: This is not referenced. Can it be removed?
-    /// <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>
@@ -305,11 +258,6 @@ public interface IConsoleDriver
     /// <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);
 
-    /// <summary>
-    ///     How long after Esc has been pressed before we give up on getting an Ansi escape sequence
-    /// </summary>
-    public TimeSpan EscTimeout { get; }
-
     /// <summary>
     ///     Queues the given <paramref name="request"/> for execution
     /// </summary>
diff --git a/Terminal.Gui/ConsoleDrivers/NetDriver/NetEvents.cs b/Terminal.Gui/ConsoleDrivers/NetDriver/NetEvents.cs
index 391eb51961..a83b84921c 100644
--- a/Terminal.Gui/ConsoleDrivers/NetDriver/NetEvents.cs
+++ b/Terminal.Gui/ConsoleDrivers/NetDriver/NetEvents.cs
@@ -96,8 +96,8 @@ private ConsoleKeyInfo ReadConsoleKeyInfo (bool intercept = true)
 
     public IEnumerable<ConsoleKeyInfo> ShouldReleaseParserHeldKeys ()
     {
-        if (Parser.State == AnsiResponseParserState.ExpectingBracket &&
-            DateTime.Now - Parser.StateChangedAt > _consoleDriver.EscTimeout)
+        if (Parser.State == AnsiResponseParserState.ExpectingEscapeSequence &&
+            DateTime.Now - Parser.StateChangedAt > ((NetDriver)_consoleDriver).EscTimeout)
         {
             return Parser.Release ().Select (o => o.Item2);
         }
diff --git a/Terminal.Gui/ConsoleDrivers/NetDriver/NetMainLoop.cs b/Terminal.Gui/ConsoleDrivers/NetDriver/NetMainLoop.cs
index 37be0766f6..cf5ee0e767 100644
--- a/Terminal.Gui/ConsoleDrivers/NetDriver/NetMainLoop.cs
+++ b/Terminal.Gui/ConsoleDrivers/NetDriver/NetMainLoop.cs
@@ -55,7 +55,7 @@ bool IMainLoopDriver.EventsPending ()
 
         _waitForProbe.Set ();
 
-        if (_resultQueue.Count > 0 || _mainLoop!.CheckTimersAndIdleHandlers (out int waitTimeout))
+        if (_resultQueue.Count > 0 || _mainLoop!.TimedEvents.CheckTimersAndIdleHandlers (out int waitTimeout))
         {
             return true;
         }
@@ -82,7 +82,7 @@ bool IMainLoopDriver.EventsPending ()
 
         if (!_eventReadyTokenSource.IsCancellationRequested)
         {
-            return _resultQueue.Count > 0 || _mainLoop.CheckTimersAndIdleHandlers (out _);
+            return _resultQueue.Count > 0 || _mainLoop.TimedEvents.CheckTimersAndIdleHandlers (out _);
         }
 
         // If cancellation was requested then always return true
diff --git a/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs b/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs
new file mode 100644
index 0000000000..5679f19136
--- /dev/null
+++ b/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs
@@ -0,0 +1,235 @@
+#nullable enable
+using System.Collections.Concurrent;
+using System.Diagnostics.CodeAnalysis;
+using Microsoft.Extensions.Logging;
+
+namespace Terminal.Gui;
+
+/// <summary>
+///     Implementation of <see cref="IApplication"/> that boots the new 'v2'
+///     main loop architecture.
+/// </summary>
+public class ApplicationV2 : ApplicationImpl
+{
+    private readonly Func<INetInput> _netInputFactory;
+    private readonly Func<IConsoleOutput> _netOutputFactory;
+    private readonly Func<IWindowsInput> _winInputFactory;
+    private readonly Func<IConsoleOutput> _winOutputFactory;
+    private IMainLoopCoordinator? _coordinator;
+    private string? _driverName;
+
+    private readonly ITimedEvents _timedEvents = new TimedEvents ();
+
+    /// <summary>
+    ///     Creates anew instance of the Application backend. The provided
+    ///     factory methods will be used on Init calls to get things booted.
+    /// </summary>
+    public ApplicationV2 () : this (
+                                    () => new NetInput (),
+                                    () => new NetOutput (),
+                                    () => new WindowsInput (),
+                                    () => new WindowsOutput ()
+                                   )
+    { }
+
+    internal ApplicationV2 (
+        Func<INetInput> netInputFactory,
+        Func<IConsoleOutput> netOutputFactory,
+        Func<IWindowsInput> winInputFactory,
+        Func<IConsoleOutput> winOutputFactory
+    )
+    {
+        _netInputFactory = netInputFactory;
+        _netOutputFactory = netOutputFactory;
+        _winInputFactory = winInputFactory;
+        _winOutputFactory = winOutputFactory;
+        IsLegacy = false;
+    }
+
+    /// <inheritdoc/>
+    [RequiresUnreferencedCode ("AOT")]
+    [RequiresDynamicCode ("AOT")]
+    public override void Init (IConsoleDriver? driver = null, string? driverName = null)
+    {
+        if (Application.Initialized)
+        {
+            Logging.Logger.LogError ("Init called multiple times without shutdown, ignoring.");
+
+            return;
+        }
+
+        if (!string.IsNullOrWhiteSpace (driverName))
+        {
+            _driverName = driverName;
+        }
+
+        Application.Navigation = new ();
+
+        Application.AddKeyBindings ();
+
+        // This is consistent with Application.ForceDriver which magnetically picks up driverName
+        // making it use custom driver in future shutdown/init calls where no driver is specified
+        CreateDriver (driverName ?? _driverName);
+
+        Application.InitializeConfigurationManagement ();
+
+        Application.Initialized = true;
+
+        Application.OnInitializedChanged (this, new (true));
+        Application.SubscribeDriverEvents ();
+    }
+
+    private void CreateDriver (string? driverName)
+    {
+        PlatformID p = Environment.OSVersion.Platform;
+
+        bool definetlyWin = driverName?.Contains ("win") ?? false;
+        bool definetlyNet = driverName?.Contains ("net") ?? false;
+
+        if (definetlyWin)
+        {
+            _coordinator = CreateWindowsSubcomponents ();
+        }
+        else if (definetlyNet)
+        {
+            _coordinator = CreateNetSubcomponents ();
+        }
+        else if (p == PlatformID.Win32NT || p == PlatformID.Win32S || p == PlatformID.Win32Windows)
+        {
+            _coordinator = CreateWindowsSubcomponents ();
+        }
+        else
+        {
+            _coordinator = CreateNetSubcomponents ();
+        }
+
+        _coordinator.StartAsync ().Wait ();
+
+        if (Application.Driver == null)
+        {
+            throw new ("Application.Driver was null even after booting MainLoopCoordinator");
+        }
+    }
+
+    private IMainLoopCoordinator CreateWindowsSubcomponents ()
+    {
+        ConcurrentQueue<WindowsConsole.InputRecord> inputBuffer = new ();
+        MainLoop<WindowsConsole.InputRecord> loop = new ();
+
+        return new MainLoopCoordinator<WindowsConsole.InputRecord> (
+                                                                    _timedEvents,
+                                                                    _winInputFactory,
+                                                                    inputBuffer,
+                                                                    new WindowsInputProcessor (inputBuffer),
+                                                                    _winOutputFactory,
+                                                                    loop);
+    }
+
+    private IMainLoopCoordinator CreateNetSubcomponents ()
+    {
+        ConcurrentQueue<ConsoleKeyInfo> inputBuffer = new ();
+        MainLoop<ConsoleKeyInfo> loop = new ();
+
+        return new MainLoopCoordinator<ConsoleKeyInfo> (
+                                                        _timedEvents,
+                                                        _netInputFactory,
+                                                        inputBuffer,
+                                                        new NetInputProcessor (inputBuffer),
+                                                        _netOutputFactory,
+                                                        loop);
+    }
+
+    /// <inheritdoc/>
+    [RequiresUnreferencedCode ("AOT")]
+    [RequiresDynamicCode ("AOT")]
+    public override T Run<T> (Func<Exception, bool>? errorHandler = null, IConsoleDriver? driver = null)
+    {
+        var top = new T ();
+
+        Run (top, errorHandler);
+
+        return top;
+    }
+
+    /// <inheritdoc/>
+    public override void Run (Toplevel view, Func<Exception, bool>? errorHandler = null)
+    {
+        Logging.Logger.LogInformation ($"Run '{view}'");
+        ArgumentNullException.ThrowIfNull (view);
+
+        if (!Application.Initialized)
+        {
+            throw new NotInitializedException (nameof (Run));
+        }
+
+        Application.Top = view;
+
+        Application.Begin (view);
+
+        // TODO : how to know when we are done?
+        while (Application.TopLevels.TryPeek (out Toplevel? found) && found == view)
+        {
+            if (_coordinator is null)
+            {
+                throw new ($"{nameof (IMainLoopCoordinator)}inexplicably became null during Run");
+            }
+
+            _coordinator.RunIteration ();
+        }
+    }
+
+    /// <inheritdoc/>
+    public override void Shutdown ()
+    {
+        _coordinator?.Stop ();
+        base.Shutdown ();
+        Application.Driver = null;
+    }
+
+    /// <inheritdoc/>
+    public override void RequestStop (Toplevel? top)
+    {
+        Logging.Logger.LogInformation ($"RequestStop '{top}'");
+
+        // TODO: This definition of stop seems sketchy
+        Application.TopLevels.TryPop (out _);
+
+        if (Application.TopLevels.Count > 0)
+        {
+            Application.Top = Application.TopLevels.Peek ();
+        }
+        else
+        {
+            Application.Top = null;
+        }
+    }
+
+    /// <inheritdoc/>
+    public override void Invoke (Action action)
+    {
+        _timedEvents.AddIdle (
+                              () =>
+                              {
+                                  action ();
+
+                                  return false;
+                              }
+                             );
+    }
+
+    /// <inheritdoc/>
+    public override void AddIdle (Func<bool> func) { _timedEvents.AddIdle (func); }
+
+    /// <summary>
+    ///     Removes an idle function added by <see cref="AddIdle"/>
+    /// </summary>
+    /// <param name="fnTrue">Function to remove</param>
+    /// <returns>True if it was found and removed</returns>
+    public bool RemoveIdle (Func<bool> fnTrue) { return _timedEvents.RemoveIdle (fnTrue); }
+
+    /// <inheritdoc/>
+    public override object AddTimeout (TimeSpan time, Func<bool> callback) { return _timedEvents.AddTimeout (time, callback); }
+
+    /// <inheritdoc/>
+    public override bool RemoveTimeout (object token) { return _timedEvents.RemoveTimeout (token); }
+}
diff --git a/Terminal.Gui/ConsoleDrivers/V2/ConsoleDriverFacade.cs b/Terminal.Gui/ConsoleDrivers/V2/ConsoleDriverFacade.cs
new file mode 100644
index 0000000000..aae6c855d8
--- /dev/null
+++ b/Terminal.Gui/ConsoleDrivers/V2/ConsoleDriverFacade.cs
@@ -0,0 +1,388 @@
+using System.Runtime.InteropServices;
+
+namespace Terminal.Gui;
+
+internal class ConsoleDriverFacade<T> : IConsoleDriver, IConsoleDriverFacade
+{
+    private readonly IConsoleOutput _output;
+    private readonly IOutputBuffer _outputBuffer;
+    private readonly AnsiRequestScheduler _ansiRequestScheduler;
+    private CursorVisibility _lastCursor = CursorVisibility.Default;
+
+    /// <summary>The event fired when the terminal is resized.</summary>
+    public event EventHandler<SizeChangedEventArgs> SizeChanged;
+
+    public IInputProcessor InputProcessor { get; }
+
+    public ConsoleDriverFacade (
+        IInputProcessor inputProcessor,
+        IOutputBuffer outputBuffer,
+        IConsoleOutput output,
+        AnsiRequestScheduler ansiRequestScheduler,
+        IWindowSizeMonitor windowSizeMonitor
+    )
+    {
+        InputProcessor = inputProcessor;
+        _output = output;
+        _outputBuffer = outputBuffer;
+        _ansiRequestScheduler = ansiRequestScheduler;
+
+        InputProcessor.KeyDown += (s, e) => KeyDown?.Invoke (s, e);
+        InputProcessor.KeyUp += (s, e) => KeyUp?.Invoke (s, e);
+        InputProcessor.MouseEvent += (s, e) => MouseEvent?.Invoke (s, e);
+
+        windowSizeMonitor.SizeChanging += (_, e) => SizeChanged?.Invoke (this, e);
+
+        CreateClipboard ();
+    }
+
+    private void CreateClipboard ()
+    {
+        PlatformID p = Environment.OSVersion.Platform;
+
+        if (p == PlatformID.Win32NT || p == PlatformID.Win32S || p == PlatformID.Win32Windows)
+        {
+            Clipboard = new WindowsClipboard ();
+        }
+        else if (RuntimeInformation.IsOSPlatform (OSPlatform.OSX))
+        {
+            Clipboard = new MacOSXClipboard ();
+        }
+        else if (CursesDriver.Is_WSL_Platform ())
+        {
+            Clipboard = new WSLClipboard ();
+        }
+        else
+        {
+            Clipboard = new FakeDriver.FakeClipboard ();
+        }
+    }
+
+    /// <summary>Gets the location and size of the terminal screen.</summary>
+    public Rectangle Screen => new (new (0, 0), _output.GetWindowSize ());
+
+    /// <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>
+    public Region Clip
+    {
+        get => _outputBuffer.Clip;
+        set => _outputBuffer.Clip = value;
+    }
+
+    /// <summary>Get the operating system clipboard.</summary>
+    public IClipboard Clipboard { get; private set; } = new FakeDriver.FakeClipboard ();
+
+    /// <summary>
+    ///     Gets the column last set by <see cref="Move"/>. <see cref="Col"/> and <see cref="Row"/> are used by
+    ///     <see cref="AddRune(Rune)"/> and <see cref="AddStr"/> to determine where to add content.
+    /// </summary>
+    public int Col => _outputBuffer.Col;
+
+    /// <summary>The number of columns visible in the terminal.</summary>
+    public int Cols
+    {
+        get => _outputBuffer.Cols;
+        set => _outputBuffer.Cols = value;
+    }
+
+    /// <summary>
+    ///     The contents of the application output. The driver outputs this buffer to the terminal.
+    ///     <remarks>The format of the array is rows, columns. The first index is the row, the second index is the column.</remarks>
+    /// </summary>
+    public Cell [,] Contents
+    {
+        get => _outputBuffer.Contents;
+        set => _outputBuffer.Contents = value;
+    }
+
+    /// <summary>The leftmost column in the terminal.</summary>
+    public int Left
+    {
+        get => _outputBuffer.Left;
+        set => _outputBuffer.Left = value;
+    }
+
+    /// <summary>
+    ///     Gets the row last set by <see cref="Move"/>. <see cref="Col"/> and <see cref="Row"/> are used by
+    ///     <see cref="AddRune(Rune)"/> and <see cref="AddStr"/> to determine where to add content.
+    /// </summary>
+    public int Row => _outputBuffer.Row;
+
+    /// <summary>The number of rows visible in the terminal.</summary>
+    public int Rows
+    {
+        get => _outputBuffer.Rows;
+        set => _outputBuffer.Rows = value;
+    }
+
+    /// <summary>The topmost row in the terminal.</summary>
+    public int Top
+    {
+        get => _outputBuffer.Top;
+        set => _outputBuffer.Top = value;
+    }
+
+    // TODO: Probably not everyone right?
+
+    /// <summary>Gets whether the <see cref="ConsoleDriver"/> supports TrueColor output.</summary>
+    public bool SupportsTrueColor => true;
+
+    // TODO: Currently ignored
+    /// <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>
+    public 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>
+    public Attribute CurrentAttribute
+    {
+        get => _outputBuffer.CurrentAttribute;
+        set => _outputBuffer.CurrentAttribute = value;
+    }
+
+    /// <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>
+    public void AddRune (Rune rune) { _outputBuffer.AddRune (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>
+    public void AddRune (char c) { _outputBuffer.AddRune (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>
+    public void AddStr (string str) { _outputBuffer.AddStr (str); }
+
+    /// <summary>Clears the <see cref="ConsoleDriver.Contents"/> of the driver.</summary>
+    public void ClearContents ()
+    {
+        _outputBuffer.ClearContents ();
+        ClearedContents?.Invoke (this, new MouseEventArgs ());
+    }
+
+    /// <summary>
+    ///     Raised each time <see cref="ConsoleDriver.ClearContents"/> is called. For benchmarking.
+    /// </summary>
+    public event EventHandler<EventArgs> ClearedContents;
+
+    /// <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>
+    public void FillRect (Rectangle rect, Rune rune = default) { _outputBuffer.FillRect (rect, rune); }
+
+    /// <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>
+    public void FillRect (Rectangle rect, char c) { _outputBuffer.FillRect (rect, c); }
+
+    /// <inheritdoc/>
+    public virtual string GetVersionInfo ()
+    {
+        var type = "";
+
+        if (InputProcessor is WindowsInputProcessor)
+        {
+            type = "(win)";
+        }
+        else if (InputProcessor is NetInputProcessor)
+        {
+            type = "(net)";
+        }
+
+        return GetType ().Name.TrimEnd ('`', '1') + type;
+    }
+
+    /// <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 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>
+    /// <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>
+    public bool IsValidLocation (Rune rune, int col, int row) { return _outputBuffer.IsValidLocation (rune, col, 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>
+    public void Move (int col, int row) { _outputBuffer.Move (col, row); }
+
+    // TODO: Probably part of output
+
+    /// <summary>Sets the terminal cursor visibility.</summary>
+    /// <param name="visibility">The wished <see cref="CursorVisibility"/></param>
+    /// <returns><see langword="true"/> upon success</returns>
+    public bool SetCursorVisibility (CursorVisibility visibility)
+    {
+        _lastCursor = visibility;
+        _output.SetCursorVisibility (visibility);
+
+        return true;
+    }
+
+    /// <inheritdoc/>
+    public bool GetCursorVisibility (out CursorVisibility current)
+    {
+        current = _lastCursor;
+
+        return true;
+    }
+
+    /// <inheritdoc/>
+    public void Suspend () { }
+
+    /// <summary>
+    ///     Sets the position of the terminal cursor to <see cref="ConsoleDriver.Col"/> and
+    ///     <see cref="ConsoleDriver.Row"/>.
+    /// </summary>
+    public void UpdateCursor () { _output.SetCursorPosition (Col, Row); }
+
+    /// <summary>Initializes the driver</summary>
+    /// <returns>Returns an instance of <see cref="MainLoop"/> using the <see cref="IMainLoopDriver"/> for the driver.</returns>
+    public MainLoop Init () { throw new NotSupportedException (); }
+
+    /// <summary>Ends the execution of the console driver.</summary>
+    public void End ()
+    {
+        // TODO: Nope
+    }
+
+    /// <summary>Selects the specified attribute as the attribute to use for future calls to AddRune and AddString.</summary>
+    /// <remarks>Implementations should call <c>base.SetAttribute(c)</c>.</remarks>
+    /// <param name="c">C.</param>
+    public Attribute SetAttribute (Attribute c) { return _outputBuffer.CurrentAttribute = c; }
+
+    /// <summary>Gets the current <see cref="Attribute"/>.</summary>
+    /// <returns>The current attribute.</returns>
+    public Attribute GetAttribute () { return _outputBuffer.CurrentAttribute; }
+
+    /// <summary>Makes an <see cref="Attribute"/>.</summary>
+    /// <param name="foreground">The foreground color.</param>
+    /// <param name="background">The background color.</param>
+    /// <returns>The attribute for the foreground and background colors.</returns>
+    public Attribute MakeColor (in Color foreground, in Color background)
+    {
+        // TODO: what even is this? why Attribute constructor wants to call Driver method which must return an instance of Attribute? ?!?!?!
+        return new (
+                    -1, // only used by cursesdriver!
+                    foreground,
+                    background
+                   );
+    }
+
+    /// <summary>Event fired when a key is pressed down. This is a precursor to <see cref="ConsoleDriver.KeyUp"/>.</summary>
+    public event EventHandler<Key> KeyDown;
+
+    /// <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>
+    public event EventHandler<Key> KeyUp;
+
+    /// <summary>Event fired when a mouse event occurs.</summary>
+    public event EventHandler<MouseEventArgs> MouseEvent;
+
+    /// <summary>Simulates a key press.</summary>
+    /// <param name="keyChar">The key character.</param>
+    /// <param name="key">The key.</param>
+    /// <param name="shift">If <see langword="true"/> simulates the Shift key being pressed.</param>
+    /// <param name="alt">If <see langword="true"/> simulates the Alt key being pressed.</param>
+    /// <param name="ctrl">If <see langword="true"/> simulates the Ctrl key being pressed.</param>
+    public void SendKeys (char keyChar, ConsoleKey key, bool shift, bool alt, bool ctrl)
+    {
+        // TODO: implement
+    }
+
+    /// <summary>
+    ///     Provide proper writing to send escape sequence recognized by the <see cref="ConsoleDriver"/>.
+    /// </summary>
+    /// <param name="ansi"></param>
+    public void WriteRaw (string ansi) { _output.Write (ansi); }
+
+    /// <summary>
+    ///     Queues the given <paramref name="request"/> for execution
+    /// </summary>
+    /// <param name="request"></param>
+    public void QueueAnsiRequest (AnsiEscapeSequenceRequest request) { _ansiRequestScheduler.SendOrSchedule (request); }
+
+    public AnsiRequestScheduler GetRequestScheduler () { return _ansiRequestScheduler; }
+
+    /// <inheritdoc/>
+    public void Refresh ()
+    {
+        // No need we will always draw when dirty
+    }
+}
diff --git a/Terminal.Gui/ConsoleDrivers/V2/ConsoleInput.cs b/Terminal.Gui/ConsoleDrivers/V2/ConsoleInput.cs
new file mode 100644
index 0000000000..d3ddbbd02d
--- /dev/null
+++ b/Terminal.Gui/ConsoleDrivers/V2/ConsoleInput.cs
@@ -0,0 +1,79 @@
+#nullable enable
+using System.Collections.Concurrent;
+
+namespace Terminal.Gui;
+
+/// <summary>
+///     Base class for reading console input in perpetual loop
+/// </summary>
+/// <typeparam name="T"></typeparam>
+public abstract class ConsoleInput<T> : IConsoleInput<T>
+{
+    private ConcurrentQueue<T>? _inputBuffer;
+
+    /// <summary>
+    ///     Determines how to get the current system type, adjust
+    ///     in unit tests to simulate specific timings.
+    /// </summary>
+    public Func<DateTime> Now { get; set; } = () => DateTime.Now;
+
+    /// <inheritdoc/>
+    public virtual void Dispose () { }
+
+    /// <inheritdoc/>
+    public void Initialize (ConcurrentQueue<T> inputBuffer) { _inputBuffer = inputBuffer; }
+
+    /// <inheritdoc/>
+    public void Run (CancellationToken token)
+    {
+        try
+        {
+            if (_inputBuffer == null)
+            {
+                throw new ("Cannot run input before Initialization");
+            }
+
+            do
+            {
+                DateTime dt = Now ();
+
+                while (Peek ())
+                {
+                    foreach (T r in Read ())
+                    {
+                        _inputBuffer.Enqueue (r);
+                    }
+                }
+
+                TimeSpan took = Now () - dt;
+                TimeSpan sleepFor = TimeSpan.FromMilliseconds (20) - took;
+
+                Logging.DrainInputStream.Record (took.Milliseconds);
+
+                if (sleepFor.Milliseconds > 0)
+                {
+                    Task.Delay (sleepFor, token).Wait (token);
+                }
+
+                token.ThrowIfCancellationRequested ();
+            }
+            while (!token.IsCancellationRequested);
+        }
+        catch (OperationCanceledException)
+        { }
+    }
+
+    /// <summary>
+    ///     When implemented in a derived class, returns true if there is data available
+    ///     to read from console.
+    /// </summary>
+    /// <returns></returns>
+    protected abstract bool Peek ();
+
+    /// <summary>
+    ///     Returns the available data without blocking, called when <see cref="Peek"/>
+    ///     returns <see langword="true"/>.
+    /// </summary>
+    /// <returns></returns>
+    protected abstract IEnumerable<T> Read ();
+}
diff --git a/Terminal.Gui/ConsoleDrivers/V2/IConsoleDriverFacade.cs b/Terminal.Gui/ConsoleDrivers/V2/IConsoleDriverFacade.cs
new file mode 100644
index 0000000000..6d316e6f20
--- /dev/null
+++ b/Terminal.Gui/ConsoleDrivers/V2/IConsoleDriverFacade.cs
@@ -0,0 +1,14 @@
+namespace Terminal.Gui;
+
+/// <summary>
+///     Interface for v2 driver abstraction layer
+/// </summary>
+public interface IConsoleDriverFacade
+{
+    /// <summary>
+    ///     Class responsible for processing native driver input objects
+    ///     e.g. <see cref="ConsoleKeyInfo"/> into <see cref="Key"/> events
+    ///     and detecting and processing ansi escape sequences.
+    /// </summary>
+    public IInputProcessor InputProcessor { get; }
+}
diff --git a/Terminal.Gui/ConsoleDrivers/V2/IConsoleInput.cs b/Terminal.Gui/ConsoleDrivers/V2/IConsoleInput.cs
new file mode 100644
index 0000000000..03f7128614
--- /dev/null
+++ b/Terminal.Gui/ConsoleDrivers/V2/IConsoleInput.cs
@@ -0,0 +1,29 @@
+using System.Collections.Concurrent;
+
+namespace Terminal.Gui;
+
+/// <summary>
+///     Interface for reading console input indefinitely -
+///     i.e. in an infinite loop. The class is responsible only
+///     for reading and storing the input in a thread safe input buffer
+///     which is then processed downstream e.g. on main UI thread.
+/// </summary>
+/// <typeparam name="T"></typeparam>
+public interface IConsoleInput<T> : IDisposable
+{
+    /// <summary>
+    ///     Initializes the input with a buffer into which to put data read
+    /// </summary>
+    /// <param name="inputBuffer"></param>
+    void Initialize (ConcurrentQueue<T> inputBuffer);
+
+    /// <summary>
+    ///     Runs in an infinite input loop.
+    /// </summary>
+    /// <param name="token"></param>
+    /// <exception cref="OperationCanceledException">
+    ///     Raised when token is
+    ///     cancelled. This is the only means of exiting the input.
+    /// </exception>
+    void Run (CancellationToken token);
+}
diff --git a/Terminal.Gui/ConsoleDrivers/V2/IConsoleOutput.cs b/Terminal.Gui/ConsoleDrivers/V2/IConsoleOutput.cs
new file mode 100644
index 0000000000..6004bf5e1b
--- /dev/null
+++ b/Terminal.Gui/ConsoleDrivers/V2/IConsoleOutput.cs
@@ -0,0 +1,42 @@
+namespace Terminal.Gui;
+
+/// <summary>
+///     Interface for writing console output
+/// </summary>
+public interface IConsoleOutput : IDisposable
+{
+    /// <summary>
+    ///     Writes the given text directly to the console. Use to send
+    ///     ansi escape codes etc. Regular screen output should use the
+    ///     <see cref="IOutputBuffer"/> overload.
+    /// </summary>
+    /// <param name="text"></param>
+    void Write (string text);
+
+    /// <summary>
+    ///     Write the contents of the <paramref name="buffer"/> to the console
+    /// </summary>
+    /// <param name="buffer"></param>
+    void Write (IOutputBuffer buffer);
+
+    /// <summary>
+    ///     Returns the current size of the console window in rows/columns (i.e.
+    ///     of characters not pixels).
+    /// </summary>
+    /// <returns></returns>
+    public Size GetWindowSize ();
+
+    /// <summary>
+    ///     Updates the console cursor (the blinking underscore) to be hidden,
+    ///     visible etc.
+    /// </summary>
+    /// <param name="visibility"></param>
+    void SetCursorVisibility (CursorVisibility visibility);
+
+    /// <summary>
+    ///     Moves the console cursor to the given location.
+    /// </summary>
+    /// <param name="col"></param>
+    /// <param name="row"></param>
+    void SetCursorPosition (int col, int row);
+}
diff --git a/Terminal.Gui/ConsoleDrivers/V2/IInputProcessor.cs b/Terminal.Gui/ConsoleDrivers/V2/IInputProcessor.cs
new file mode 100644
index 0000000000..bb79ec83f4
--- /dev/null
+++ b/Terminal.Gui/ConsoleDrivers/V2/IInputProcessor.cs
@@ -0,0 +1,60 @@
+#nullable enable
+namespace Terminal.Gui;
+
+/// <summary>
+///     Interface for main loop class that will process the queued input buffer contents.
+///     Is responsible for <see cref="ProcessQueue"/> and translating into common Terminal.Gui
+///     events and data models.
+/// </summary>
+public interface IInputProcessor
+{
+    /// <summary>Event fired when a key is pressed down. This is a precursor to <see cref="KeyUp"/>.</summary>
+    event EventHandler<Key>? KeyDown;
+
+    /// <summary>Event fired when a key is released.</summary>
+    /// <remarks>
+    ///     Drivers that do not support key release events will fire this event after <see cref="KeyDown"/> processing is
+    ///     complete.
+    /// </remarks>
+    event EventHandler<Key>? KeyUp;
+
+    /// <summary>Event fired when a terminal sequence read from input is not recognized and therefore ignored.</summary>
+    public event EventHandler<string>? AnsiSequenceSwallowed;
+
+    /// <summary>Event fired when a mouse event occurs.</summary>
+    event EventHandler<MouseEventArgs>? MouseEvent;
+
+    /// <summary>
+    ///     Called when a key is pressed down. Fires the <see cref="KeyDown"/> event. This is a precursor to
+    ///     <see cref="OnKeyUp"/>.
+    /// </summary>
+    /// <param name="key">The key event data.</param>
+    void OnKeyDown (Key key);
+
+    /// <summary>
+    ///     Called when a key is released. Fires the <see cref="KeyUp"/> event.
+    /// </summary>
+    /// <remarks>
+    ///     Drivers that do not support key release events will call this method after <see cref="OnKeyDown"/> processing
+    ///     is complete.
+    /// </remarks>
+    /// <param name="key">The key event data.</param>
+    void OnKeyUp (Key key);
+
+    /// <summary>
+    ///     Called when a mouse event occurs. Fires the <see cref="MouseEvent"/> event.
+    /// </summary>
+    /// <param name="mouseEventArgs">The mouse event data.</param>
+    void OnMouseEvent (MouseEventArgs mouseEventArgs);
+
+    /// <summary>
+    ///     Drains the input buffer, processing all available keystrokes
+    /// </summary>
+    void ProcessQueue ();
+
+    /// <summary>
+    ///     Gets the response parser currently configured on this input processor.
+    /// </summary>
+    /// <returns></returns>
+    public IAnsiResponseParser GetParser ();
+}
diff --git a/Terminal.Gui/ConsoleDrivers/V2/IKeyConverter.cs b/Terminal.Gui/ConsoleDrivers/V2/IKeyConverter.cs
new file mode 100644
index 0000000000..12cc379fab
--- /dev/null
+++ b/Terminal.Gui/ConsoleDrivers/V2/IKeyConverter.cs
@@ -0,0 +1,18 @@
+namespace Terminal.Gui;
+
+/// <summary>
+///     Interface for subcomponent of a <see cref="InputProcessor{T}"/> which
+///     can translate the raw console input type T (which typically varies by
+///     driver) to the shared Terminal.Gui <see cref="Key"/> class.
+/// </summary>
+/// <typeparam name="T"></typeparam>
+public interface IKeyConverter<in T>
+{
+    /// <summary>
+    ///     Converts the native keyboard class read from console into
+    ///     the shared <see cref="Key"/> class used by Terminal.Gui views.
+    /// </summary>
+    /// <param name="value"></param>
+    /// <returns></returns>
+    Key ToKey (T value);
+}
diff --git a/Terminal.Gui/ConsoleDrivers/V2/IMainLoop.cs b/Terminal.Gui/ConsoleDrivers/V2/IMainLoop.cs
new file mode 100644
index 0000000000..73493eb7ea
--- /dev/null
+++ b/Terminal.Gui/ConsoleDrivers/V2/IMainLoop.cs
@@ -0,0 +1,58 @@
+#nullable enable
+using System.Collections.Concurrent;
+
+namespace Terminal.Gui;
+
+/// <summary>
+///     Interface for main loop that runs the core Terminal.Gui UI loop.
+/// </summary>
+/// <typeparam name="T"></typeparam>
+public interface IMainLoop<T> : IDisposable
+{
+    /// <summary>
+    ///     Gets the class responsible for servicing user timeouts and idles
+    /// </summary>
+    public ITimedEvents TimedEvents { get; }
+
+    /// <summary>
+    ///     Gets the class responsible for writing final rendered output to the console
+    /// </summary>
+    public IOutputBuffer OutputBuffer { get; }
+
+    /// <summary>
+    ///     Class for writing output to the console.
+    /// </summary>
+    public IConsoleOutput Out { get; }
+
+    /// <summary>
+    ///     Gets the class responsible for processing buffered console input and translating
+    ///     it into events on the UI thread.
+    /// </summary>
+    public IInputProcessor InputProcessor { get; }
+
+    /// <summary>
+    ///     Gets the class responsible for sending ANSI escape requests which expect a response
+    ///     from the remote terminal e.g. Device Attribute Request
+    /// </summary>
+    public AnsiRequestScheduler AnsiRequestScheduler { get; }
+
+    /// <summary>
+    ///     Gets the class responsible for determining the current console size
+    /// </summary>
+    public IWindowSizeMonitor WindowSizeMonitor { get; }
+
+    /// <summary>
+    ///     Initializes the loop with a buffer from which data can be read
+    /// </summary>
+    /// <param name="timedEvents"></param>
+    /// <param name="inputBuffer"></param>
+    /// <param name="inputProcessor"></param>
+    /// <param name="consoleOutput"></param>
+    void Initialize (ITimedEvents timedEvents, ConcurrentQueue<T> inputBuffer, IInputProcessor inputProcessor, IConsoleOutput consoleOutput);
+
+    /// <summary>
+    ///     Perform a single iteration of the main loop then blocks for a fixed length
+    ///     of time, this method is designed to be run in a loop.
+    /// </summary>
+    public void Iteration ();
+}
diff --git a/Terminal.Gui/ConsoleDrivers/V2/IMainLoopCoordinator.cs b/Terminal.Gui/ConsoleDrivers/V2/IMainLoopCoordinator.cs
new file mode 100644
index 0000000000..7a1ccf35bb
--- /dev/null
+++ b/Terminal.Gui/ConsoleDrivers/V2/IMainLoopCoordinator.cs
@@ -0,0 +1,24 @@
+namespace Terminal.Gui;
+
+/// <summary>
+///     Interface for main Terminal.Gui loop manager in v2.
+/// </summary>
+public interface IMainLoopCoordinator
+{
+    /// <summary>
+    ///     Create all required subcomponents and boot strap.
+    /// </summary>
+    /// <returns></returns>
+    public Task StartAsync ();
+
+    /// <summary>
+    ///     Stops the input thread, blocking till it exits.
+    ///     Call this method only from the main UI loop.
+    /// </summary>
+    public void Stop ();
+
+    /// <summary>
+    ///     Run a single iteration of the main UI loop
+    /// </summary>
+    void RunIteration ();
+}
diff --git a/Terminal.Gui/ConsoleDrivers/V2/INetInput.cs b/Terminal.Gui/ConsoleDrivers/V2/INetInput.cs
new file mode 100644
index 0000000000..610eee236b
--- /dev/null
+++ b/Terminal.Gui/ConsoleDrivers/V2/INetInput.cs
@@ -0,0 +1,4 @@
+namespace Terminal.Gui;
+
+internal interface INetInput : IConsoleInput<ConsoleKeyInfo>
+{ }
diff --git a/Terminal.Gui/ConsoleDrivers/V2/IOutputBuffer.cs b/Terminal.Gui/ConsoleDrivers/V2/IOutputBuffer.cs
new file mode 100644
index 0000000000..29c3475efe
--- /dev/null
+++ b/Terminal.Gui/ConsoleDrivers/V2/IOutputBuffer.cs
@@ -0,0 +1,122 @@
+#nullable enable
+namespace Terminal.Gui;
+
+/// <summary>
+///     Describes the screen state that you want the console to be in.
+///     Is designed to be drawn to repeatedly then manifest into the console
+///     once at the end of iteration after all drawing is finalized.
+/// </summary>
+public interface IOutputBuffer
+{
+    /// <summary>
+    ///     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.
+    /// </summary>
+    public bool [] DirtyLines { get; }
+
+    /// <summary>
+    ///     The contents of the application output. The driver outputs this buffer to the terminal when UpdateScreen is called.
+    /// </summary>
+    Cell [,] Contents { get; set; }
+
+    /// <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>
+    public Region? Clip { get; set; }
+
+    /// <summary>
+    ///     The <see cref="Attribute"/> that will be used for the next AddRune or AddStr call.
+    /// </summary>
+    Attribute CurrentAttribute { get; set; }
+
+    /// <summary>The number of rows visible in the terminal.</summary>
+    int Rows { get; set; }
+
+    /// <summary>The number of columns visible in the terminal.</summary>
+    int Cols { 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>
+    public int Row { get; }
+
+    /// <summary>
+    ///     Gets the column last set by <see cref="Move"/>. <see cref="Col"/> and <see cref="Row"/> are used by
+    ///     <see cref="AddRune(Rune)"/> and <see cref="AddStr"/> to determine where to add content.
+    /// </summary>
+    public int Col { get; }
+
+    /// <summary>
+    ///     The first cell index on left of screen - basically always 0.
+    ///     Changing this may have unexpected consequences.
+    /// </summary>
+    int Left { get; set; }
+
+    /// <summary>
+    ///     The first cell index on top of screen - basically always 0.
+    ///     Changing this may have unexpected consequences.
+    /// </summary>
+    int Top { get; set; }
+
+    /// <summary>
+    ///     Updates the column and row to the specified location in the buffer.
+    /// </summary>
+    /// <param name="col">The column to move to.</param>
+    /// <param name="row">The 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>
+    /// <param name="rune">Rune to add.</param>
+    void AddRune (Rune rune);
+
+    /// <summary>
+    ///     Adds the specified character to the display at the current cursor position. This is a convenience method for
+    ///     AddRune.
+    /// </summary>
+    /// <param name="c">Character to add.</param>
+    void AddRune (char c);
+
+    /// <summary>Adds the string to the display at the current cursor position.</summary>
+    /// <param name="str">String to add.</param>
+    void AddStr (string str);
+
+    /// <summary>Clears the contents of the buffer.</summary>
+    void ClearContents ();
+
+    /// <summary>
+    ///     Tests whether the specified coordinate is 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>
+    ///     True if the coordinate is valid for the Rune; false otherwise.
+    /// </returns>
+    bool IsValidLocation (Rune rune, int col, int row);
+
+    /// <summary>
+    ///     Changes the size of the buffer to the given size
+    /// </summary>
+    /// <param name="cols"></param>
+    /// <param name="rows"></param>
+    void SetWindowSize (int cols, int rows);
+
+    /// <summary>
+    ///     Fills the given <paramref name="rect"/> with the given
+    ///     symbol using the currently selected attribute.
+    /// </summary>
+    /// <param name="rect"></param>
+    /// <param name="rune"></param>
+    void FillRect (Rectangle rect, Rune rune);
+
+    /// <summary>
+    ///     Fills the given <paramref name="rect"/> with the given
+    ///     symbol using the currently selected attribute.
+    /// </summary>
+    /// <param name="rect"></param>
+    /// <param name="rune"></param>
+    void FillRect (Rectangle rect, char rune);
+}
diff --git a/Terminal.Gui/ConsoleDrivers/V2/IToplevelTransitionManager.cs b/Terminal.Gui/ConsoleDrivers/V2/IToplevelTransitionManager.cs
new file mode 100644
index 0000000000..984c54490d
--- /dev/null
+++ b/Terminal.Gui/ConsoleDrivers/V2/IToplevelTransitionManager.cs
@@ -0,0 +1,20 @@
+namespace Terminal.Gui;
+
+/// <summary>
+///     Interface for class that handles bespoke behaviours that occur when application
+///     top level changes.
+/// </summary>
+public interface IToplevelTransitionManager
+{
+    /// <summary>
+    ///     Raises the <see cref="Toplevel.Ready"/> event on the current top level
+    ///     if it has not been raised before now.
+    /// </summary>
+    void RaiseReadyEventIfNeeded ();
+
+    /// <summary>
+    ///     Handles any state change needed when the application top changes e.g.
+    ///     setting redraw flags
+    /// </summary>
+    void HandleTopMaybeChanging ();
+}
diff --git a/Terminal.Gui/ConsoleDrivers/V2/IWindowSizeMonitor.cs b/Terminal.Gui/ConsoleDrivers/V2/IWindowSizeMonitor.cs
new file mode 100644
index 0000000000..5bfedfaf58
--- /dev/null
+++ b/Terminal.Gui/ConsoleDrivers/V2/IWindowSizeMonitor.cs
@@ -0,0 +1,19 @@
+#nullable enable
+namespace Terminal.Gui;
+
+/// <summary>
+///     Interface for classes responsible for reporting the current
+///     size of the terminal window.
+/// </summary>
+public interface IWindowSizeMonitor
+{
+    /// <summary>Invoked when the terminal's size changed. The new size of the terminal is provided.</summary>
+    event EventHandler<SizeChangedEventArgs>? SizeChanging;
+
+    /// <summary>
+    ///     Examines the current size of the terminal and raises <see cref="SizeChanging"/> if it is different
+    ///     from last inspection.
+    /// </summary>
+    /// <returns></returns>
+    bool Poll ();
+}
diff --git a/Terminal.Gui/ConsoleDrivers/V2/IWindowsInput.cs b/Terminal.Gui/ConsoleDrivers/V2/IWindowsInput.cs
new file mode 100644
index 0000000000..63bf8e2933
--- /dev/null
+++ b/Terminal.Gui/ConsoleDrivers/V2/IWindowsInput.cs
@@ -0,0 +1,4 @@
+namespace Terminal.Gui;
+
+internal interface IWindowsInput : IConsoleInput<WindowsConsole.InputRecord>
+{ }
diff --git a/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs b/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs
new file mode 100644
index 0000000000..e7b7b8d2cc
--- /dev/null
+++ b/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs
@@ -0,0 +1,165 @@
+#nullable enable
+using System.Collections.Concurrent;
+using Microsoft.Extensions.Logging;
+
+namespace Terminal.Gui;
+
+/// <summary>
+///     Processes the queued input buffer contents - which must be of Type <typeparamref name="T"/>.
+///     Is responsible for <see cref="ProcessQueue"/> and translating into common Terminal.Gui
+///     events and data models.
+/// </summary>
+public abstract class InputProcessor<T> : IInputProcessor
+{
+    /// <summary>
+    ///     How long after Esc has been pressed before we give up on getting an Ansi escape sequence
+    /// </summary>
+    private readonly TimeSpan _escTimeout = TimeSpan.FromMilliseconds (50);
+
+    internal AnsiResponseParser<T> Parser { get; } = new ();
+
+    /// <summary>
+    ///     Class responsible for translating the driver specific native input class <typeparamref name="T"/> e.g.
+    ///     <see cref="ConsoleKeyInfo"/> into the Terminal.Gui <see cref="Key"/> class (used for all
+    ///     internal library representations of Keys).
+    /// </summary>
+    public IKeyConverter<T> KeyConverter { get; }
+
+    /// <summary>
+    ///     Input buffer which will be drained from by this class.
+    /// </summary>
+    public ConcurrentQueue<T> InputBuffer { get; }
+
+    /// <inheritdoc/>
+    public IAnsiResponseParser GetParser () { return Parser; }
+
+    private readonly MouseInterpreter _mouseInterpreter = new ();
+
+    /// <summary>Event fired when a key is pressed down. This is a precursor to <see cref="KeyUp"/>.</summary>
+    public event EventHandler<Key>? KeyDown;
+
+    /// <summary>Event fired when a terminal sequence read from input is not recognized and therefore ignored.</summary>
+    public event EventHandler<string>? AnsiSequenceSwallowed;
+
+    /// <summary>
+    ///     Called when a key is pressed down. Fires the <see cref="KeyDown"/> event. This is a precursor to
+    ///     <see cref="OnKeyUp"/>.
+    /// </summary>
+    /// <param name="a"></param>
+    public void OnKeyDown (Key a)
+    {
+        Logging.Trace ($"{nameof (InputProcessor<T>)} raised {a}");
+        KeyDown?.Invoke (this, a);
+    }
+
+    /// <summary>Event fired when a key is released.</summary>
+    /// <remarks>
+    ///     Drivers that do not support key release events will fire this event after <see cref="KeyDown"/> processing is
+    ///     complete.
+    /// </remarks>
+    public event EventHandler<Key>? KeyUp;
+
+    /// <summary>Called when a key is released. Fires the <see cref="KeyUp"/> event.</summary>
+    /// <remarks>
+    ///     Drivers that do not support key release events will call this method after <see cref="OnKeyDown"/> processing
+    ///     is complete.
+    /// </remarks>
+    /// <param name="a"></param>
+    public void OnKeyUp (Key a) { KeyUp?.Invoke (this, a); }
+
+    /// <summary>Event fired when a mouse event occurs.</summary>
+    public event EventHandler<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;
+
+        foreach (MouseEventArgs e in _mouseInterpreter.Process (a))
+        {
+            Logging.Trace ($"Mouse Interpreter raising {e.Flags}");
+
+            // Pass on
+            MouseEvent?.Invoke (this, e);
+        }
+    }
+
+    /// <summary>
+    ///     Constructs base instance including wiring all relevant
+    ///     parser events and setting <see cref="InputBuffer"/> to
+    ///     the provided thread safe input collection.
+    /// </summary>
+    /// <param name="inputBuffer">The collection that will be populated with new input (see <see cref="IConsoleInput{T}"/>)</param>
+    /// <param name="keyConverter">
+    ///     Key converter for translating driver specific
+    ///     <typeparamref name="T"/> class into Terminal.Gui <see cref="Key"/>.
+    /// </param>
+    protected InputProcessor (ConcurrentQueue<T> inputBuffer, IKeyConverter<T> keyConverter)
+    {
+        InputBuffer = inputBuffer;
+        Parser.HandleMouse = true;
+        Parser.Mouse += (s, e) => OnMouseEvent (e);
+
+        Parser.HandleKeyboard = true;
+
+        Parser.Keyboard += (s, k) =>
+                           {
+                               OnKeyDown (k);
+                               OnKeyUp (k);
+                           };
+
+        // TODO: For now handle all other escape codes with ignore
+        Parser.UnexpectedResponseHandler = str =>
+                                           {
+                                               var cur = new string (str.Select (k => k.Item1).ToArray ());
+                                               Logging.Logger.LogInformation ($"{nameof (InputProcessor<T>)} ignored unrecognized response '{cur}'");
+                                               AnsiSequenceSwallowed?.Invoke (this, cur);
+
+                                               return true;
+                                           };
+        KeyConverter = keyConverter;
+    }
+
+    /// <summary>
+    ///     Drains the <see cref="InputBuffer"/> buffer, processing all available keystrokes
+    /// </summary>
+    public void ProcessQueue ()
+    {
+        while (InputBuffer.TryDequeue (out T? input))
+        {
+            Process (input);
+        }
+
+        foreach (T input in ReleaseParserHeldKeysIfStale ())
+        {
+            ProcessAfterParsing (input);
+        }
+    }
+
+    private IEnumerable<T> ReleaseParserHeldKeysIfStale ()
+    {
+        if (Parser.State is AnsiResponseParserState.ExpectingEscapeSequence or AnsiResponseParserState.InResponse
+            && DateTime.Now - Parser.StateChangedAt > _escTimeout)
+        {
+            return Parser.Release ().Select (o => o.Item2);
+        }
+
+        return [];
+    }
+
+    /// <summary>
+    ///     Process the provided single input element <paramref name="input"/>. This method
+    ///     is called sequentially for each value read from <see cref="InputBuffer"/>.
+    /// </summary>
+    /// <param name="input"></param>
+    protected abstract void Process (T input);
+
+    /// <summary>
+    ///     Process the provided single input element - short-circuiting the <see cref="Parser"/>
+    ///     stage of the processing.
+    /// </summary>
+    /// <param name="input"></param>
+    protected abstract void ProcessAfterParsing (T input);
+}
diff --git a/Terminal.Gui/ConsoleDrivers/V2/Logging.cs b/Terminal.Gui/ConsoleDrivers/V2/Logging.cs
new file mode 100644
index 0000000000..0d367f4a40
--- /dev/null
+++ b/Terminal.Gui/ConsoleDrivers/V2/Logging.cs
@@ -0,0 +1,68 @@
+using System.Diagnostics.Metrics;
+using System.Runtime.CompilerServices;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+
+namespace Terminal.Gui;
+
+/// <summary>
+///     Singleton logging instance class. Do not use console loggers
+///     with this class as it will interfere with Terminal.Gui
+///     screen output (i.e. use a file logger).
+/// </summary>
+/// <remarks>
+///     Also contains the
+///     <see cref="Meter"/> instance that should be used for internal metrics
+///     (iteration timing etc).
+/// </remarks>
+public static class Logging
+{
+    /// <summary>
+    ///     Logger, defaults to NullLogger (i.e. no logging).  Set this to a
+    ///     file logger to enable logging of Terminal.Gui internals.
+    /// </summary>
+    public static ILogger Logger { get; set; } = NullLogger.Instance;
+
+    /// <summary>
+    ///     Metrics reporting meter for internal Terminal.Gui processes. To use
+    ///     create your own static instrument e.g. CreateCounter, CreateHistogram etc
+    /// </summary>
+    internal static readonly Meter Meter = new ("Terminal.Gui");
+
+    /// <summary>
+    ///     Metric for how long it takes each full iteration of the main loop to occur
+    /// </summary>
+    public static readonly Histogram<int> TotalIterationMetric = Meter.CreateHistogram<int> ("Iteration (ms)");
+
+    /// <summary>
+    ///     Metric for how long it took to do the 'timeouts and invokes' section of main loop.
+    /// </summary>
+    public static readonly Histogram<int> IterationInvokesAndTimeouts = Meter.CreateHistogram<int> ("Invokes & Timers (ms)");
+
+    /// <summary>
+    ///     Counter for when we redraw, helps detect situations e.g. where we are repainting entire UI every loop
+    /// </summary>
+    public static readonly Counter<int> Redraws = Meter.CreateCounter<int> ("Redraws");
+
+    /// <summary>
+    ///     Metric for how long it takes to read all available input from the input stream - at which
+    ///     point input loop will sleep.
+    /// </summary>
+    public static readonly Histogram<int> DrainInputStream = Meter.CreateHistogram<int> ("Drain Input (ms)");
+
+    /// <summary>
+    ///     Logs a trace message including the
+    /// </summary>
+    /// <param name="message"></param>
+    /// <param name="caller"></param>
+    /// <param name="filePath"></param>
+    public static void Trace (
+        string message,
+        [CallerMemberName] string caller = "",
+        [CallerFilePath] string filePath = ""
+    )
+    {
+        string className = Path.GetFileNameWithoutExtension (filePath);
+        Logger.LogTrace ($"[{className}] [{caller}] {message}");
+    }
+}
diff --git a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs
new file mode 100644
index 0000000000..8c830aaa36
--- /dev/null
+++ b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs
@@ -0,0 +1,203 @@
+#nullable enable
+using System.Collections.Concurrent;
+using System.Diagnostics;
+
+namespace Terminal.Gui;
+
+/// <inheritdoc/>
+public class MainLoop<T> : IMainLoop<T>
+{
+    private ITimedEvents? _timedEvents;
+    private ConcurrentQueue<T>? _inputBuffer;
+    private IInputProcessor? _inputProcessor;
+    private IConsoleOutput? _out;
+    private AnsiRequestScheduler? _ansiRequestScheduler;
+    private IWindowSizeMonitor? _windowSizeMonitor;
+
+    /// <inheritdoc/>
+    public ITimedEvents TimedEvents
+    {
+        get => _timedEvents ?? throw new NotInitializedException (nameof (TimedEvents));
+        private set => _timedEvents = value;
+    }
+
+    // TODO: follow above pattern for others too
+
+    /// <summary>
+    ///     The input events thread-safe collection. This is populated on separate
+    ///     thread by a <see cref="IConsoleInput{T}"/>. Is drained as part of each
+    ///     <see cref="Iteration"/>
+    /// </summary>
+    public ConcurrentQueue<T> InputBuffer
+    {
+        get => _inputBuffer ?? throw new NotInitializedException (nameof (InputBuffer));
+        private set => _inputBuffer = value;
+    }
+
+    /// <inheritdoc/>
+    public IInputProcessor InputProcessor
+    {
+        get => _inputProcessor ?? throw new NotInitializedException (nameof (InputProcessor));
+        private set => _inputProcessor = value;
+    }
+
+    /// <inheritdoc/>
+    public IOutputBuffer OutputBuffer { get; } = new OutputBuffer ();
+
+    /// <inheritdoc/>
+    public IConsoleOutput Out
+    {
+        get => _out ?? throw new NotInitializedException (nameof (Out));
+        private set => _out = value;
+    }
+
+    /// <inheritdoc/>
+    public AnsiRequestScheduler AnsiRequestScheduler
+    {
+        get => _ansiRequestScheduler ?? throw new NotInitializedException (nameof (AnsiRequestScheduler));
+        private set => _ansiRequestScheduler = value;
+    }
+
+    /// <inheritdoc/>
+    public IWindowSizeMonitor WindowSizeMonitor
+    {
+        get => _windowSizeMonitor ?? throw new NotInitializedException (nameof (WindowSizeMonitor));
+        private set => _windowSizeMonitor = value;
+    }
+
+    /// <summary>
+    ///     Handles raising events and setting required draw status etc when <see cref="Application.Top"/> changes
+    /// </summary>
+    public IToplevelTransitionManager ToplevelTransitionManager = new ToplevelTransitionManager ();
+
+    /// <summary>
+    ///     Determines how to get the current system type, adjust
+    ///     in unit tests to simulate specific timings.
+    /// </summary>
+    public Func<DateTime> Now { get; set; } = () => DateTime.Now;
+
+    /// <summary>
+    ///     Initializes the class with the provided subcomponents
+    /// </summary>
+    /// <param name="timedEvents"></param>
+    /// <param name="inputBuffer"></param>
+    /// <param name="inputProcessor"></param>
+    /// <param name="consoleOutput"></param>
+    public void Initialize (ITimedEvents timedEvents, ConcurrentQueue<T> inputBuffer, IInputProcessor inputProcessor, IConsoleOutput consoleOutput)
+    {
+        InputBuffer = inputBuffer;
+        Out = consoleOutput;
+        InputProcessor = inputProcessor;
+
+        TimedEvents = timedEvents;
+        AnsiRequestScheduler = new (InputProcessor.GetParser ());
+
+        WindowSizeMonitor = new WindowSizeMonitor (Out, OutputBuffer);
+    }
+
+    /// <inheritdoc/>
+    public void Iteration ()
+    {
+        DateTime dt = Now ();
+
+        IterationImpl ();
+
+        TimeSpan took = Now () - dt;
+        TimeSpan sleepFor = TimeSpan.FromMilliseconds (50) - took;
+
+        Logging.TotalIterationMetric.Record (took.Milliseconds);
+
+        if (sleepFor.Milliseconds > 0)
+        {
+            Task.Delay (sleepFor).Wait ();
+        }
+    }
+
+    internal void IterationImpl ()
+    {
+        InputProcessor.ProcessQueue ();
+
+        ToplevelTransitionManager.RaiseReadyEventIfNeeded ();
+        ToplevelTransitionManager.HandleTopMaybeChanging ();
+
+        if (Application.Top != null)
+        {
+            bool needsDrawOrLayout = AnySubviewsNeedDrawn (Application.Top);
+
+            bool sizeChanged = WindowSizeMonitor.Poll ();
+
+            if (needsDrawOrLayout || sizeChanged)
+            {
+                Logging.Redraws.Add (1);
+
+                // TODO: Test only
+                Application.LayoutAndDraw (true);
+
+                Out.Write (OutputBuffer);
+
+                Out.SetCursorVisibility (CursorVisibility.Default);
+            }
+
+            SetCursor ();
+        }
+
+        var swCallbacks = Stopwatch.StartNew ();
+
+        TimedEvents.LockAndRunTimers ();
+
+        TimedEvents.LockAndRunIdles ();
+
+        Logging.IterationInvokesAndTimeouts.Record (swCallbacks.Elapsed.Milliseconds);
+    }
+
+    private void SetCursor ()
+    {
+        View? mostFocused = Application.Top.MostFocused;
+
+        if (mostFocused == null)
+        {
+            return;
+        }
+
+        Point? to = mostFocused.PositionCursor ();
+
+        if (to.HasValue)
+        {
+            // Translate to screen coordinates
+            to = mostFocused.ViewportToScreen (to.Value);
+
+            Out.SetCursorPosition (to.Value.X, to.Value.Y);
+            Out.SetCursorVisibility (mostFocused.CursorVisibility);
+        }
+        else
+        {
+            Out.SetCursorVisibility (CursorVisibility.Invisible);
+        }
+    }
+
+    private bool AnySubviewsNeedDrawn (View v)
+    {
+        if (v.NeedsDraw || v.NeedsLayout)
+        {
+            Logging.Trace ($"{v.GetType ().Name} triggered redraw (NeedsDraw={v.NeedsDraw} NeedsLayout={v.NeedsLayout}) ");
+
+            return true;
+        }
+
+        foreach (View subview in v.Subviews)
+        {
+            if (AnySubviewsNeedDrawn (subview))
+            {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /// <inheritdoc/>
+    public void Dispose ()
+    {
+        // TODO release managed resources here
+    }
+}
diff --git a/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs b/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs
new file mode 100644
index 0000000000..28bad67ab4
--- /dev/null
+++ b/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs
@@ -0,0 +1,186 @@
+using System.Collections.Concurrent;
+using Microsoft.Extensions.Logging;
+
+namespace Terminal.Gui;
+
+/// <summary>
+///     <para>
+///         Handles creating the input loop thread and bootstrapping the
+///         <see cref="MainLoop{T}"/> that handles layout/drawing/events etc.
+///     </para>
+///     <para>This class is designed to be managed by <see cref="ApplicationV2"/></para>
+/// </summary>
+/// <typeparam name="T"></typeparam>
+internal class MainLoopCoordinator<T> : IMainLoopCoordinator
+{
+    private readonly Func<IConsoleInput<T>> _inputFactory;
+    private readonly ConcurrentQueue<T> _inputBuffer;
+    private readonly IInputProcessor _inputProcessor;
+    private readonly IMainLoop<T> _loop;
+    private readonly CancellationTokenSource _tokenSource = new ();
+    private readonly Func<IConsoleOutput> _outputFactory;
+    private IConsoleInput<T> _input;
+    private IConsoleOutput _output;
+    private readonly object _oLockInitialization = new ();
+    private ConsoleDriverFacade<T> _facade;
+    private Task _inputTask;
+    private readonly ITimedEvents _timedEvents;
+
+    private readonly SemaphoreSlim _startupSemaphore = new (0, 1);
+
+    /// <summary>
+    ///     Creates a new coordinator
+    /// </summary>
+    /// <param name="timedEvents"></param>
+    /// <param name="inputFactory">
+    ///     Function to create a new input. This must call <see langword="new"/>
+    ///     explicitly and cannot return an existing instance. This requirement arises because Windows
+    ///     console screen buffer APIs are thread-specific for certain operations.
+    /// </param>
+    /// <param name="inputBuffer"></param>
+    /// <param name="inputProcessor"></param>
+    /// <param name="outputFactory">
+    ///     Function to create a new output. This must call <see langword="new"/>
+    ///     explicitly and cannot return an existing instance. This requirement arises because Windows
+    ///     console screen buffer APIs are thread-specific for certain operations.
+    /// </param>
+    /// <param name="loop"></param>
+    public MainLoopCoordinator (
+        ITimedEvents timedEvents,
+        Func<IConsoleInput<T>> inputFactory,
+        ConcurrentQueue<T> inputBuffer,
+        IInputProcessor inputProcessor,
+        Func<IConsoleOutput> outputFactory,
+        IMainLoop<T> loop
+    )
+    {
+        _timedEvents = timedEvents;
+        _inputFactory = inputFactory;
+        _inputBuffer = inputBuffer;
+        _inputProcessor = inputProcessor;
+        _outputFactory = outputFactory;
+        _loop = loop;
+    }
+
+    /// <summary>
+    ///     Starts the input loop thread in separate task (returning immediately).
+    /// </summary>
+    public async Task StartAsync ()
+    {
+        Logging.Logger.LogInformation ("Main Loop Coordinator booting...");
+
+        _inputTask = Task.Run (RunInput);
+
+        // Main loop is now booted on same thread as rest of users application
+        BootMainLoop ();
+
+        // Wait asynchronously for the semaphore or task failure.
+        Task waitForSemaphore = _startupSemaphore.WaitAsync ();
+
+        // Wait for either the semaphore to be released or the input task to crash.
+        Task completedTask = await Task.WhenAny (waitForSemaphore, _inputTask).ConfigureAwait (false);
+
+        // Check if the task was the input task and if it has failed.
+        if (completedTask == _inputTask)
+        {
+            if (_inputTask.IsFaulted)
+            {
+                throw _inputTask.Exception;
+            }
+
+            throw new ("Input loop exited during startup instead of entering read loop properly (i.e. and blocking)");
+        }
+
+        Logging.Logger.LogInformation ("Main Loop Coordinator booting complete");
+    }
+
+    private void RunInput ()
+    {
+        try
+        {
+            lock (_oLockInitialization)
+            {
+                // Instance must be constructed on the thread in which it is used.
+                _input = _inputFactory.Invoke ();
+                _input.Initialize (_inputBuffer);
+
+                BuildFacadeIfPossible ();
+            }
+
+            try
+            {
+                _input.Run (_tokenSource.Token);
+            }
+            catch (OperationCanceledException)
+            { }
+
+            _input.Dispose ();
+        }
+        catch (Exception e)
+        {
+            Logging.Logger.LogCritical (e, "Input loop crashed");
+
+            throw;
+        }
+
+        if (_stopCalled)
+        {
+            Logging.Logger.LogInformation ("Input loop exited cleanly");
+        }
+        else
+        {
+            Logging.Logger.LogCritical ("Input loop exited early (stop not called)");
+        }
+    }
+
+    /// <inheritdoc/>
+    public void RunIteration () { _loop.Iteration (); }
+
+    private void BootMainLoop ()
+    {
+        lock (_oLockInitialization)
+        {
+            // Instance must be constructed on the thread in which it is used.
+            _output = _outputFactory.Invoke ();
+            _loop.Initialize (_timedEvents, _inputBuffer, _inputProcessor, _output);
+
+            BuildFacadeIfPossible ();
+        }
+    }
+
+    private void BuildFacadeIfPossible ()
+    {
+        if (_input != null && _output != null)
+        {
+            _facade = new (
+                           _inputProcessor,
+                           _loop.OutputBuffer,
+                           _output,
+                           _loop.AnsiRequestScheduler,
+                           _loop.WindowSizeMonitor);
+            Application.Driver = _facade;
+
+            _startupSemaphore.Release ();
+        }
+    }
+
+    private bool _stopCalled;
+
+    /// <inheritdoc/>
+    public void Stop ()
+    {
+        // Ignore repeated calls to Stop - happens if user spams Application.Shutdown().
+        if (_stopCalled)
+        {
+            return;
+        }
+
+        _stopCalled = true;
+
+        _tokenSource.Cancel ();
+        _output.Dispose ();
+
+        // Wait for input infinite loop to exit
+        _inputTask.Wait ();
+    }
+}
diff --git a/Terminal.Gui/ConsoleDrivers/V2/MouseButtonStateEx.cs b/Terminal.Gui/ConsoleDrivers/V2/MouseButtonStateEx.cs
new file mode 100644
index 0000000000..fb1aa27fee
--- /dev/null
+++ b/Terminal.Gui/ConsoleDrivers/V2/MouseButtonStateEx.cs
@@ -0,0 +1,89 @@
+#nullable enable
+namespace Terminal.Gui;
+
+/// <summary>
+///     Not to be confused with <see cref="NetEvents.MouseButtonState"/>
+/// </summary>
+internal class MouseButtonStateEx
+{
+    private readonly Func<DateTime> _now;
+    private readonly TimeSpan _repeatClickThreshold;
+    private readonly int _buttonIdx;
+    private int _consecutiveClicks;
+    private Point _lastPosition;
+
+    /// <summary>
+    ///     When the button entered its current state.
+    /// </summary>
+    public DateTime At { get; set; }
+
+    /// <summary>
+    ///     <see langword="true"/> if the button is currently down
+    /// </summary>
+    public bool Pressed { get; set; }
+
+    public MouseButtonStateEx (Func<DateTime> now, TimeSpan repeatClickThreshold, int buttonIdx)
+    {
+        _now = now;
+        _repeatClickThreshold = repeatClickThreshold;
+        _buttonIdx = buttonIdx;
+    }
+
+    public void UpdateState (MouseEventArgs e, out int? numClicks)
+    {
+        bool isPressedNow = IsPressed (_buttonIdx, e.Flags);
+        bool isSamePosition = _lastPosition == e.Position;
+
+        TimeSpan elapsed = _now () - At;
+
+        if (elapsed > _repeatClickThreshold || !isSamePosition)
+        {
+            // Expired
+            OverwriteState (e);
+            _consecutiveClicks = 0;
+            numClicks = null;
+        }
+        else
+        {
+            if (isPressedNow == Pressed)
+            {
+                // No change in button state so do nothing
+                numClicks = null;
+
+                return;
+            }
+
+            if (Pressed)
+            {
+                // Click released
+                numClicks = ++_consecutiveClicks;
+            }
+            else
+            {
+                numClicks = null;
+            }
+
+            // Record new state
+            OverwriteState (e);
+        }
+    }
+
+    private void OverwriteState (MouseEventArgs e)
+    {
+        Pressed = IsPressed (_buttonIdx, e.Flags);
+        At = _now ();
+        _lastPosition = e.Position;
+    }
+
+    private bool IsPressed (int btn, MouseFlags eFlags)
+    {
+        return btn switch
+               {
+                   0 => eFlags.HasFlag (MouseFlags.Button1Pressed),
+                   1 => eFlags.HasFlag (MouseFlags.Button2Pressed),
+                   2 => eFlags.HasFlag (MouseFlags.Button3Pressed),
+                   3 => eFlags.HasFlag (MouseFlags.Button4Pressed),
+                   _ => throw new ArgumentOutOfRangeException (nameof (btn))
+               };
+    }
+}
diff --git a/Terminal.Gui/ConsoleDrivers/V2/MouseInterpreter.cs b/Terminal.Gui/ConsoleDrivers/V2/MouseInterpreter.cs
new file mode 100644
index 0000000000..f6546302e1
--- /dev/null
+++ b/Terminal.Gui/ConsoleDrivers/V2/MouseInterpreter.cs
@@ -0,0 +1,105 @@
+#nullable enable
+
+namespace Terminal.Gui;
+
+internal class MouseInterpreter
+{
+    /// <summary>
+    ///     Function for returning the current time. Use in unit tests to
+    ///     ensure repeatable tests.
+    /// </summary>
+    public Func<DateTime> Now { get; set; }
+
+    /// <summary>
+    ///     How long to wait for a second, third, fourth click after the first before giving up and
+    ///     releasing event as a 'click'
+    /// </summary>
+    public TimeSpan RepeatedClickThreshold { get; set; }
+
+    private readonly MouseButtonStateEx [] _buttonStates;
+
+    public MouseInterpreter (
+        Func<DateTime>? now = null,
+        TimeSpan? doubleClickThreshold = null
+    )
+    {
+        Now = now ?? (() => DateTime.Now);
+        RepeatedClickThreshold = doubleClickThreshold ?? TimeSpan.FromMilliseconds (500);
+
+        _buttonStates = new []
+        {
+            new MouseButtonStateEx (Now, RepeatedClickThreshold, 0),
+            new MouseButtonStateEx (Now, RepeatedClickThreshold, 1),
+            new MouseButtonStateEx (Now, RepeatedClickThreshold, 2),
+            new MouseButtonStateEx (Now, RepeatedClickThreshold, 3)
+        };
+    }
+
+    public IEnumerable<MouseEventArgs> Process (MouseEventArgs e)
+    {
+        yield return e;
+
+        // For each mouse button
+        for (var i = 0; i < 4; i++)
+        {
+            _buttonStates [i].UpdateState (e, out int? numClicks);
+
+            if (numClicks.HasValue)
+            {
+                yield return RaiseClick (i, numClicks.Value, e);
+            }
+        }
+    }
+
+    private MouseEventArgs RaiseClick (int button, int numberOfClicks, MouseEventArgs mouseEventArgs)
+    {
+        var newClick = new MouseEventArgs
+        {
+            Handled = false,
+            Flags = ToClicks (button, numberOfClicks),
+            ScreenPosition = mouseEventArgs.ScreenPosition,
+            View = mouseEventArgs.View,
+            Position = mouseEventArgs.Position
+        };
+        Logging.Trace ($"Raising click event:{newClick.Flags} at screen {newClick.ScreenPosition}");
+
+        return newClick;
+    }
+
+    private MouseFlags ToClicks (int buttonIdx, int numberOfClicks)
+    {
+        if (numberOfClicks == 0)
+        {
+            throw new ArgumentOutOfRangeException (nameof (numberOfClicks), "Zero clicks are not valid.");
+        }
+
+        return buttonIdx switch
+               {
+                   0 => numberOfClicks switch
+                        {
+                            1 => MouseFlags.Button1Clicked,
+                            2 => MouseFlags.Button1DoubleClicked,
+                            _ => MouseFlags.Button1TripleClicked
+                        },
+                   1 => numberOfClicks switch
+                        {
+                            1 => MouseFlags.Button2Clicked,
+                            2 => MouseFlags.Button2DoubleClicked,
+                            _ => MouseFlags.Button2TripleClicked
+                        },
+                   2 => numberOfClicks switch
+                        {
+                            1 => MouseFlags.Button3Clicked,
+                            2 => MouseFlags.Button3DoubleClicked,
+                            _ => MouseFlags.Button3TripleClicked
+                        },
+                   3 => numberOfClicks switch
+                        {
+                            1 => MouseFlags.Button4Clicked,
+                            2 => MouseFlags.Button4DoubleClicked,
+                            _ => MouseFlags.Button4TripleClicked
+                        },
+                   _ => throw new ArgumentOutOfRangeException (nameof (buttonIdx), "Unsupported button index")
+               };
+    }
+}
diff --git a/Terminal.Gui/ConsoleDrivers/V2/NetInput.cs b/Terminal.Gui/ConsoleDrivers/V2/NetInput.cs
new file mode 100644
index 0000000000..d263c57493
--- /dev/null
+++ b/Terminal.Gui/ConsoleDrivers/V2/NetInput.cs
@@ -0,0 +1,61 @@
+using Microsoft.Extensions.Logging;
+
+namespace Terminal.Gui;
+
+/// <summary>
+///     Console input implementation that uses native dotnet methods e.g. <see cref="System.Console"/>.
+/// </summary>
+public class NetInput : ConsoleInput<ConsoleKeyInfo>, INetInput
+{
+    private readonly NetWinVTConsole _adjustConsole;
+
+    /// <summary>
+    ///     Creates a new instance of the class. Implicitly sends
+    ///     console mode settings that enable virtual input (mouse
+    ///     reporting etc).
+    /// </summary>
+    public NetInput ()
+    {
+        Logging.Logger.LogInformation ($"Creating {nameof (NetInput)}");
+        PlatformID p = Environment.OSVersion.Platform;
+
+        if (p == PlatformID.Win32NT || p == PlatformID.Win32S || p == PlatformID.Win32Windows)
+        {
+            try
+            {
+                _adjustConsole = new ();
+            }
+            catch (ApplicationException ex)
+            {
+                // Likely running as a unit test, or in a non-interactive session.
+                Logging.Logger.LogCritical (
+                                            ex,
+                                            "NetWinVTConsole could not be constructed i.e. could not configure terminal modes. May indicate running in non-interactive session e.g. unit testing CI");
+            }
+        }
+
+        Console.Out.Write (EscSeqUtils.CSI_EnableMouseEvents);
+        Console.TreatControlCAsInput = true;
+    }
+
+    /// <inheritdoc/>
+    protected override bool Peek () { return Console.KeyAvailable; }
+
+    /// <inheritdoc/>
+    protected override IEnumerable<ConsoleKeyInfo> Read ()
+    {
+        while (Console.KeyAvailable)
+        {
+            yield return Console.ReadKey (true);
+        }
+    }
+
+    /// <inheritdoc/>
+    public override void Dispose ()
+    {
+        base.Dispose ();
+        _adjustConsole?.Cleanup ();
+
+        Console.Out.Write (EscSeqUtils.CSI_DisableMouseEvents);
+    }
+}
diff --git a/Terminal.Gui/ConsoleDrivers/V2/NetInputProcessor.cs b/Terminal.Gui/ConsoleDrivers/V2/NetInputProcessor.cs
new file mode 100644
index 0000000000..d08daca5f4
--- /dev/null
+++ b/Terminal.Gui/ConsoleDrivers/V2/NetInputProcessor.cs
@@ -0,0 +1,59 @@
+using System.Collections.Concurrent;
+
+namespace Terminal.Gui;
+
+/// <summary>
+///     Input processor for <see cref="NetInput"/>, deals in <see cref="ConsoleKeyInfo"/> stream
+/// </summary>
+public class NetInputProcessor : InputProcessor<ConsoleKeyInfo>
+{
+#pragma warning disable CA2211
+    /// <summary>
+    ///     Set to true to generate code in <see cref="Logging"/> (verbose only) for test cases in NetInputProcessorTests.
+    ///     <remarks>
+    ///         This makes the task of capturing user/language/terminal specific keyboard issues easier to
+    ///         diagnose. By turning this on and searching logs user can send us exactly the input codes that are released
+    ///         to input stream.
+    ///     </remarks>
+    /// </summary>
+    public static bool GenerateTestCasesForKeyPresses = false;
+#pragma warning enable CA2211
+
+    /// <inheritdoc/>
+    public NetInputProcessor (ConcurrentQueue<ConsoleKeyInfo> inputBuffer) : base (inputBuffer, new NetKeyConverter ()) { }
+
+    /// <inheritdoc/>
+    protected override void Process (ConsoleKeyInfo consoleKeyInfo)
+    {
+        // For building test cases
+        if (GenerateTestCasesForKeyPresses)
+        {
+            Logging.Trace (FormatConsoleKeyInfoForTestCase (consoleKeyInfo));
+        }
+
+        foreach (Tuple<char, ConsoleKeyInfo> released in Parser.ProcessInput (Tuple.Create (consoleKeyInfo.KeyChar, consoleKeyInfo)))
+        {
+            ProcessAfterParsing (released.Item2);
+        }
+    }
+
+    /// <inheritdoc/>
+    protected override void ProcessAfterParsing (ConsoleKeyInfo input)
+    {
+        var key = KeyConverter.ToKey (input);
+        OnKeyDown (key);
+        OnKeyUp (key);
+    }
+
+    /* For building test cases */
+    private static string FormatConsoleKeyInfoForTestCase (ConsoleKeyInfo input)
+    {
+        string charLiteral = input.KeyChar == '\0' ? @"'\0'" : $"'{input.KeyChar}'";
+        var expectedLiteral = "new Rune('todo')";
+
+        return $"new ConsoleKeyInfo({charLiteral}, ConsoleKey.{input.Key}, "
+               + $"{input.Modifiers.HasFlag (ConsoleModifiers.Shift).ToString ().ToLower ()}, "
+               + $"{input.Modifiers.HasFlag (ConsoleModifiers.Alt).ToString ().ToLower ()}, "
+               + $"{input.Modifiers.HasFlag (ConsoleModifiers.Control).ToString ().ToLower ()}), {expectedLiteral}";
+    }
+}
diff --git a/Terminal.Gui/ConsoleDrivers/V2/NetKeyConverter.cs b/Terminal.Gui/ConsoleDrivers/V2/NetKeyConverter.cs
new file mode 100644
index 0000000000..b7c17e6153
--- /dev/null
+++ b/Terminal.Gui/ConsoleDrivers/V2/NetKeyConverter.cs
@@ -0,0 +1,25 @@
+namespace Terminal.Gui;
+
+/// <summary>
+///     <see cref="IKeyConverter{T}"/> capable of converting the
+///     dotnet <see cref="ConsoleKeyInfo"/> class into Terminal.Gui
+///     shared <see cref="Key"/> representation (used by <see cref="View"/>
+///     etc).
+/// </summary>
+internal class NetKeyConverter : IKeyConverter<ConsoleKeyInfo>
+{
+    /// <inheritdoc/>
+    public Key ToKey (ConsoleKeyInfo input)
+    {
+        ConsoleKeyInfo adjustedInput = EscSeqUtils.MapConsoleKeyInfo (input);
+
+        // TODO : EscSeqUtils.MapConsoleKeyInfo is wrong for e.g. '{' - it winds up clearing the Key
+        //        So if the method nuked it then we should just work with the original.
+        if (adjustedInput.Key == ConsoleKey.None && input.Key != ConsoleKey.None)
+        {
+            return EscSeqUtils.MapKey (input);
+        }
+
+        return EscSeqUtils.MapKey (adjustedInput);
+    }
+}
diff --git a/Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs b/Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs
new file mode 100644
index 0000000000..b7a9ea2806
--- /dev/null
+++ b/Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs
@@ -0,0 +1,249 @@
+using Microsoft.Extensions.Logging;
+
+namespace Terminal.Gui;
+
+/// <summary>
+///     Implementation of <see cref="IConsoleOutput"/> that uses native dotnet
+///     methods e.g. <see cref="System.Console"/>
+/// </summary>
+public class NetOutput : IConsoleOutput
+{
+    private readonly bool _isWinPlatform;
+
+    private CursorVisibility? _cachedCursorVisibility;
+
+    /// <summary>
+    ///     Creates a new instance of the <see cref="NetOutput"/> class.
+    /// </summary>
+    public NetOutput ()
+    {
+        Logging.Logger.LogInformation ($"Creating {nameof (NetOutput)}");
+
+        PlatformID p = Environment.OSVersion.Platform;
+
+        if (p == PlatformID.Win32NT || p == PlatformID.Win32S || p == PlatformID.Win32Windows)
+        {
+            _isWinPlatform = true;
+        }
+
+        //Enable alternative screen buffer.
+        Console.Out.Write (EscSeqUtils.CSI_SaveCursorAndActivateAltBufferNoBackscroll);
+
+        //Set cursor key to application.
+        Console.Out.Write (EscSeqUtils.CSI_HideCursor);
+    }
+
+    /// <inheritdoc/>
+    public void Write (string text) { Console.Write (text); }
+
+    /// <inheritdoc/>
+    public void Write (IOutputBuffer buffer)
+    {
+        if (Console.WindowHeight < 1
+            || buffer.Contents.Length != buffer.Rows * buffer.Cols
+            || buffer.Rows != Console.WindowHeight)
+        {
+            //     return;
+        }
+
+        var top = 0;
+        var left = 0;
+        int rows = buffer.Rows;
+        int cols = buffer.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;
+            }
+
+            if (!buffer.DirtyLines [row])
+            {
+                continue;
+            }
+
+            if (!SetCursorPositionImpl (0, row))
+            {
+                return;
+            }
+
+            buffer.DirtyLines [row] = false;
+            output.Clear ();
+
+            for (int col = left; col < cols; col++)
+            {
+                lastCol = -1;
+                var outputWidth = 0;
+
+                for (; col < cols; col++)
+                {
+                    if (!buffer.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 = buffer.Contents [row, col].Attribute.Value;
+
+                    // Performance: Only send the escape sequence if the attribute has changed.
+                    if (attr != redrawAttr)
+                    {
+                        redrawAttr = attr;
+
+                        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 = buffer.Contents [row, col].Rune;
+                    output.Append (rune);
+
+                    if (buffer.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);
+                        SetCursorPositionImpl (col - 1, row);
+                    }
+
+                    buffer.Contents [row, col].IsDirty = false;
+                }
+            }
+
+            if (output.Length > 0)
+            {
+                SetCursorPositionImpl (lastCol, row);
+                Console.Write (output);
+            }
+        }
+
+        foreach (SixelToRender s in Application.Sixel)
+        {
+            if (!string.IsNullOrWhiteSpace (s.SixelData))
+            {
+                SetCursorPositionImpl (s.ScreenPosition.X, s.ScreenPosition.Y);
+                Console.Write (s.SixelData);
+            }
+        }
+
+        SetCursorVisibility (savedVisibility ?? CursorVisibility.Default);
+        _cachedCursorVisibility = savedVisibility;
+    }
+
+    /// <inheritdoc/>
+    public Size GetWindowSize () { return new (Console.WindowWidth, Console.WindowHeight); }
+
+    private void WriteToConsole (StringBuilder output, ref int lastCol, int row, ref int outputWidth)
+    {
+        SetCursorPositionImpl (lastCol, row);
+        Console.Write (output);
+        output.Clear ();
+        lastCol += outputWidth;
+        outputWidth = 0;
+    }
+
+    /// <inheritdoc/>
+    public void SetCursorPosition (int col, int row) { SetCursorPositionImpl (col, row); }
+
+    private Point _lastCursorPosition;
+
+    private bool SetCursorPositionImpl (int col, int row)
+    {
+        if (_lastCursorPosition.X == col && _lastCursorPosition.Y == row)
+        {
+            return true;
+        }
+
+        _lastCursorPosition = new (col, 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;
+    }
+
+    /// <inheritdoc/>
+    public void Dispose ()
+    {
+        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 ();
+    }
+
+    /// <inheritdoc/>
+    public void SetCursorVisibility (CursorVisibility visibility)
+    {
+        Console.Out.Write (visibility == CursorVisibility.Default ? EscSeqUtils.CSI_ShowCursor : EscSeqUtils.CSI_HideCursor);
+    }
+}
diff --git a/Terminal.Gui/ConsoleDrivers/V2/NotInitializedException.cs b/Terminal.Gui/ConsoleDrivers/V2/NotInitializedException.cs
new file mode 100644
index 0000000000..5c63b4071e
--- /dev/null
+++ b/Terminal.Gui/ConsoleDrivers/V2/NotInitializedException.cs
@@ -0,0 +1,22 @@
+namespace Terminal.Gui;
+
+/// <summary>
+///     Thrown when user code attempts to access a property or perform a method
+///     that is only supported after Initialization e.g. of an <see cref="IMainLoop{T}"/>
+/// </summary>
+public class NotInitializedException : Exception
+{
+    /// <summary>
+    ///     Creates a new instance of the exception indicating that the class
+    ///     <paramref name="memberName"/> cannot be used until owner is initialized.
+    /// </summary>
+    /// <param name="memberName">Property or method name</param>
+    public NotInitializedException (string memberName) : base ($"{memberName} cannot be accessed before Initialization") { }
+
+    /// <summary>
+    ///     Creates a new instance of the exception with the full message/inner exception.
+    /// </summary>
+    /// <param name="msg"></param>
+    /// <param name="innerException"></param>
+    public NotInitializedException (string msg, Exception innerException) : base (msg, innerException) { }
+}
diff --git a/Terminal.Gui/ConsoleDrivers/V2/OutputBuffer.cs b/Terminal.Gui/ConsoleDrivers/V2/OutputBuffer.cs
new file mode 100644
index 0000000000..44d137ff32
--- /dev/null
+++ b/Terminal.Gui/ConsoleDrivers/V2/OutputBuffer.cs
@@ -0,0 +1,449 @@
+#nullable enable
+using System.Diagnostics;
+
+namespace Terminal.Gui;
+
+/// <summary>
+///     Stores the desired output state for the whole application. This is updated during
+///     draw operations before being flushed to the console as part of <see cref="MainLoop{T}"/>
+///     operation
+/// </summary>
+public class OutputBuffer : IOutputBuffer
+{
+    /// <summary>
+    ///     The contents of the application output. The driver outputs this buffer to the terminal when
+    ///     UpdateScreen is called.
+    ///     <remarks>The format of the array is rows, columns. The first index is the row, the second index is the column.</remarks>
+    /// </summary>
+    public Cell [,] Contents { get; set; } = new Cell[0, 0];
+
+    private Attribute _currentAttribute;
+    private int _cols;
+    private int _rows;
+
+    /// <summary>
+    ///     The <see cref="Attribute"/> that will be used for the next <see cref="AddRune(Rune)"/> or <see cref="AddStr"/>
+    ///     call.
+    /// </summary>
+    public Attribute CurrentAttribute
+    {
+        get => _currentAttribute;
+        set
+        {
+            // TODO: This makes IConsoleDriver dependent on Application, which is not ideal. Once Attribute.PlatformColor is removed, this can be fixed.
+            if (Application.Driver is { })
+            {
+                _currentAttribute = new (value.Foreground, value.Background);
+
+                return;
+            }
+
+            _currentAttribute = value;
+        }
+    }
+
+    /// <summary>The leftmost column in the terminal.</summary>
+    public virtual int Left { get; set; } = 0;
+
+    /// <summary>
+    ///     Gets the row last set by <see cref="Move"/>. <see cref="Col"/> and <see cref="Row"/> are used by
+    ///     <see cref="AddRune(Rune)"/> and <see cref="AddStr"/> to determine where to add content.
+    /// </summary>
+    public int Row { get; private set; }
+
+    /// <summary>
+    ///     Gets the column last set by <see cref="Move"/>. <see cref="Col"/> and <see cref="Row"/> are used by
+    ///     <see cref="AddRune(Rune)"/> and <see cref="AddStr"/> to determine where to add content.
+    /// </summary>
+    public int Col { get; private set; }
+
+    /// <summary>The number of rows visible in the terminal.</summary>
+    public int Rows
+    {
+        get => _rows;
+        set
+        {
+            _rows = value;
+            ClearContents ();
+        }
+    }
+
+    /// <summary>The number of columns visible in the terminal.</summary>
+    public int Cols
+    {
+        get => _cols;
+        set
+        {
+            _cols = value;
+            ClearContents ();
+        }
+    }
+
+    /// <summary>The topmost row in the terminal.</summary>
+    public virtual int Top { get; set; } = 0;
+
+    /// <inheritdoc/>
+    public bool [] DirtyLines { get; set; } = [];
+
+    // 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);
+
+    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>
+    public Region? Clip
+    {
+        get => _clip;
+        set
+        {
+            if (_clip == value)
+            {
+                return;
+            }
+
+            _clip = value;
+
+            // Don't ever let Clip be bigger than Screen
+            if (_clip is { })
+            {
+                _clip.Intersect (Screen);
+            }
+        }
+    }
+
+    /// <summary>Adds the specified rune to the display at the current cursor position.</summary>
+    /// <remarks>
+    ///     <para>
+    ///         When the method returns, <see cref="Col"/> will be incremented by the number of columns
+    ///         <paramref name="rune"/> required, even if the new column value is outside of the <see cref="Clip"/> or screen
+    ///         dimensions defined by <see cref="Cols"/>.
+    ///     </para>
+    ///     <para>
+    ///         If <paramref name="rune"/> requires more than one column, and <see cref="Col"/> plus the number of columns
+    ///         needed exceeds the <see cref="Clip"/> or screen dimensions, the default Unicode replacement character (U+FFFD)
+    ///         will be added instead.
+    ///     </para>
+    /// </remarks>
+    /// <param name="rune">Rune to add.</param>
+    public void AddRune (Rune rune)
+    {
+        int runeWidth = -1;
+        bool validLocation = IsValidLocation (rune, Col, Row);
+
+        if (Contents is null)
+        {
+            return;
+        }
+
+        Rectangle clipRect = Clip!.GetBounds ();
+
+        if (validLocation)
+        {
+            rune = rune.MakePrintable ();
+            runeWidth = rune.GetColumns ();
+
+            lock (Contents)
+            {
+                if (runeWidth == 0 && rune.IsCombiningMark ())
+                {
+                    // AtlasEngine does not support NON-NORMALIZED combining marks in a way
+                    // compatible with the driver architecture. Any CMs (except in the first col)
+                    // are correctly combined with the base char, but are ALSO treated as 1 column
+                    // width codepoints E.g. `echo "[e`u{0301}`u{0301}]"` will output `[é  ]`.
+                    // 
+                    // Until this is addressed (see Issue #), we do our best by 
+                    // a) Attempting to normalize any CM with the base char to it's left
+                    // b) Ignoring any CMs that don't normalize
+                    if (Col > 0)
+                    {
+                        if (Contents [Row, Col - 1].CombiningMarks.Count > 0)
+                        {
+                            // Just add this mark to the list
+                            Contents [Row, Col - 1].CombiningMarks.Add (rune);
+
+                            // Ignore. Don't move to next column (let the driver figure out what to do).
+                        }
+                        else
+                        {
+                            // Attempt to normalize the cell to our left combined with this mark
+                            string combined = Contents [Row, Col - 1].Rune + rune.ToString ();
+
+                            // Normalize to Form C (Canonical Composition)
+                            string normalized = combined.Normalize (NormalizationForm.FormC);
+
+                            if (normalized.Length == 1)
+                            {
+                                // It normalized! We can just set the Cell to the left with the
+                                // normalized codepoint 
+                                Contents [Row, Col - 1].Rune = (Rune)normalized [0];
+
+                                // Ignore. Don't move to next column because we're already there
+                            }
+                            else
+                            {
+                                // It didn't normalize. Add it to the Cell to left's CM list
+                                Contents [Row, Col - 1].CombiningMarks.Add (rune);
+
+                                // Ignore. Don't move to next column (let the driver figure out what to do).
+                            }
+                        }
+
+                        Contents [Row, Col - 1].Attribute = CurrentAttribute;
+                        Contents [Row, Col - 1].IsDirty = true;
+                    }
+                    else
+                    {
+                        // Most drivers will render a combining mark at col 0 as the mark
+                        Contents [Row, Col].Rune = rune;
+                        Contents [Row, Col].Attribute = CurrentAttribute;
+                        Contents [Row, Col].IsDirty = true;
+                        Col++;
+                    }
+                }
+                else
+                {
+                    Contents [Row, Col].Attribute = CurrentAttribute;
+                    Contents [Row, Col].IsDirty = true;
+
+                    if (Col > 0)
+                    {
+                        // Check if cell to left has a wide glyph
+                        if (Contents [Row, Col - 1].Rune.GetColumns () > 1)
+                        {
+                            // Invalidate cell to left
+                            Contents [Row, Col - 1].Rune = Rune.ReplacementChar;
+                            Contents [Row, Col - 1].IsDirty = true;
+                        }
+                    }
+
+                    if (runeWidth < 1)
+                    {
+                        Contents [Row, Col].Rune = Rune.ReplacementChar;
+                    }
+                    else if (runeWidth == 1)
+                    {
+                        Contents [Row, Col].Rune = rune;
+
+                        if (Col < clipRect.Right - 1)
+                        {
+                            Contents [Row, Col + 1].IsDirty = true;
+                        }
+                    }
+                    else if (runeWidth == 2)
+                    {
+                        if (!Clip.Contains (Col + 1, Row))
+                        {
+                            // We're at the right edge of the clip, so we can't display a wide character.
+                            // TODO: Figure out if it is better to show a replacement character or ' '
+                            Contents [Row, Col].Rune = Rune.ReplacementChar;
+                        }
+                        else if (!Clip.Contains (Col, Row))
+                        {
+                            // Our 1st column is outside the clip, so we can't display a wide character.
+                            Contents [Row, Col + 1].Rune = Rune.ReplacementChar;
+                        }
+                        else
+                        {
+                            Contents [Row, Col].Rune = rune;
+
+                            if (Col < clipRect.Right - 1)
+                            {
+                                // Invalidate cell to right so that it doesn't get drawn
+                                // TODO: Figure out if it is better to show a replacement character or ' '
+                                Contents [Row, Col + 1].Rune = Rune.ReplacementChar;
+                                Contents [Row, Col + 1].IsDirty = true;
+                            }
+                        }
+                    }
+                    else
+                    {
+                        // This is a non-spacing character, so we don't need to do anything
+                        Contents [Row, Col].Rune = (Rune)' ';
+                        Contents [Row, Col].IsDirty = false;
+                    }
+
+                    DirtyLines [Row] = true;
+                }
+            }
+        }
+
+        if (runeWidth is < 0 or > 0)
+        {
+            Col++;
+        }
+
+        if (runeWidth > 1)
+        {
+            Debug.Assert (runeWidth <= 2);
+
+            if (validLocation && Col < clipRect.Right)
+            {
+                lock (Contents!)
+                {
+                    // This is a double-width character, and we are not at the end of the line.
+                    // Col now points to the second column of the character. Ensure it doesn't
+                    // Get rendered.
+                    Contents [Row, Col].IsDirty = false;
+                    Contents [Row, Col].Attribute = CurrentAttribute;
+
+                    // TODO: Determine if we should wipe this out (for now now)
+                    //Contents [Row, Col].Rune = (Rune)' ';
+                }
+            }
+
+            Col++;
+        }
+    }
+
+    /// <summary>
+    ///     Adds the specified <see langword="char"/> to the display at the current cursor position. This method is a
+    ///     convenience method that calls <see cref="AddRune(Rune)"/> with the <see cref="Rune"/> constructor.
+    /// </summary>
+    /// <param name="c">Character to add.</param>
+    public void AddRune (char c) { AddRune (new Rune (c)); }
+
+    /// <summary>Adds the <paramref name="str"/> to the display at the cursor position.</summary>
+    /// <remarks>
+    ///     <para>
+    ///         When the method returns, <see cref="Col"/> will be incremented by the number of columns
+    ///         <paramref name="str"/> required, unless the new column value is outside of the <see cref="Clip"/> or screen
+    ///         dimensions defined by <see cref="Cols"/>.
+    ///     </para>
+    ///     <para>If <paramref name="str"/> requires more columns than are available, the output will be clipped.</para>
+    /// </remarks>
+    /// <param name="str">String.</param>
+    public void AddStr (string str)
+    {
+        List<Rune> runes = str.EnumerateRunes ().ToList ();
+
+        for (var i = 0; i < runes.Count; i++)
+        {
+            AddRune (runes [i]);
+        }
+    }
+
+    /// <summary>Clears the <see cref="Contents"/> of the driver.</summary>
+    public void ClearContents ()
+    {
+        Contents = new Cell [Rows, Cols];
+
+        //CONCURRENCY: Unsynchronized access to Clip isn't safe.
+        // TODO: ClearContents should not clear the clip; it should only clear the contents. Move clearing it elsewhere.
+        Clip = new (Screen);
+
+        DirtyLines = new bool [Rows];
+
+        lock (Contents)
+        {
+            for (var row = 0; row < Rows; row++)
+            {
+                for (var c = 0; c < Cols; c++)
+                {
+                    Contents [row, c] = new ()
+                    {
+                        Rune = (Rune)' ',
+                        Attribute = new Attribute (Color.White, Color.Black),
+                        IsDirty = true
+                    };
+                }
+
+                DirtyLines [row] = true;
+            }
+        }
+
+        // TODO: Who uses this and why? I am removing for now - this class is a state class not an events class
+        //ClearedContents?.Invoke (this, EventArgs.Empty);
+    }
+
+    /// <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="Clip"/>.
+    ///     <see langword="true"/> otherwise.
+    /// </returns>
+    public bool IsValidLocation (Rune rune, int col, int row)
+    {
+        if (rune.GetColumns () < 2)
+        {
+            return col >= 0 && row >= 0 && col < Cols && row < Rows && Clip!.Contains (col, row);
+        }
+
+        return Clip!.Contains (col, row) || Clip!.Contains (col + 1, row);
+    }
+
+    /// <inheritdoc/>
+    public void SetWindowSize (int cols, int rows)
+    {
+        Cols = cols;
+        Rows = rows;
+        ClearContents ();
+    }
+
+    /// <inheritdoc/>
+    public void FillRect (Rectangle rect, Rune rune)
+    {
+        // 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 ()
+                    {
+                        Rune = rune != default (Rune) ? rune : (Rune)' ',
+                        Attribute = CurrentAttribute, IsDirty = true
+                    };
+                }
+            }
+        }
+    }
+
+    /// <inheritdoc/>
+    public void FillRect (Rectangle rect, char rune)
+    {
+        for (int y = rect.Top; y < rect.Top + rect.Height; y++)
+        {
+            for (int x = rect.Left; x < rect.Left + rect.Width; x++)
+            {
+                Move (x, y);
+                AddRune (rune);
+            }
+        }
+    }
+
+    // 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;
+    }
+}
diff --git a/Terminal.Gui/ConsoleDrivers/V2/ToplevelTransitionManager.cs b/Terminal.Gui/ConsoleDrivers/V2/ToplevelTransitionManager.cs
new file mode 100644
index 0000000000..49bd86e4b6
--- /dev/null
+++ b/Terminal.Gui/ConsoleDrivers/V2/ToplevelTransitionManager.cs
@@ -0,0 +1,37 @@
+#nullable enable
+namespace Terminal.Gui;
+
+/// <summary>
+///     Handles bespoke behaviours that occur when application top level changes.
+/// </summary>
+public class ToplevelTransitionManager : IToplevelTransitionManager
+{
+    private readonly HashSet<Toplevel> _readiedTopLevels = new ();
+
+    private View? _lastTop;
+
+    /// <inheritdoc/>
+    public void RaiseReadyEventIfNeeded ()
+    {
+        Toplevel? top = Application.Top;
+
+        if (top != null && !_readiedTopLevels.Contains (top))
+        {
+            top.OnReady ();
+            _readiedTopLevels.Add (top);
+        }
+    }
+
+    /// <inheritdoc/>
+    public void HandleTopMaybeChanging ()
+    {
+        Toplevel? newTop = Application.Top;
+
+        if (_lastTop != null && _lastTop != newTop && newTop != null)
+        {
+            newTop.SetNeedsDraw ();
+        }
+
+        _lastTop = Application.Top;
+    }
+}
diff --git a/Terminal.Gui/ConsoleDrivers/V2/V2.cd b/Terminal.Gui/ConsoleDrivers/V2/V2.cd
new file mode 100644
index 0000000000..f5004db3ce
--- /dev/null
+++ b/Terminal.Gui/ConsoleDrivers/V2/V2.cd
@@ -0,0 +1,569 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ClassDiagram MajorVersion="1" MinorVersion="1">
+  <Comment CommentText="Thread 1 - Input thread, populates input buffer.  This thread is hidden, nobody gets to interact directly with these classes)">
+    <Position X="11" Y="0.5" Height="0.5" Width="5.325" />
+  </Comment>
+  <Comment CommentText="Thread 2 - Main Loop which does everything else including output.  Deals with input exclusively through the input buffer. Is accessible externally e.g. to Application">
+    <Position X="11.083" Y="3.813" Height="0.479" Width="5.325" />
+  </Comment>
+  <Comment CommentText="Orchestrates the 2 main threads in Terminal.Gui">
+    <Position X="6.5" Y="1.25" Height="0.291" Width="2.929" />
+  </Comment>
+  <Comment CommentText="Allows Views to work with new architecture without having to be rewritten.">
+    <Position X="4.666" Y="7.834" Height="0.75" Width="1.7" />
+  </Comment>
+  <Comment CommentText="Ansi Escape Sequence - Request / Response">
+    <Position X="19.208" Y="3.562" Height="0.396" Width="2.825" />
+  </Comment>
+  <Comment CommentText="Mouse interpretation subsystem">
+    <Position X="13.271" Y="9.896" Height="0.396" Width="2.075" />
+  </Comment>
+  <Comment CommentText="In Terminal.Gui views get things done almost exclusively by calling static methods on Application e.g. RequestStop, Run, Refresh etc">
+    <Position X="0.5" Y="3.75" Height="1.146" Width="1.7" />
+  </Comment>
+  <Comment CommentText="Static record of system state and static gateway API for everything you ever need.">
+    <Position X="0.5" Y="1.417" Height="0.875" Width="1.7" />
+  </Comment>
+  <Comment CommentText="Forwarded subset of gateway functionality. These exist to allow ''subclassing' Application.  Note that most methods 'ping pong' a lot back to main gateway submethods e.g. to manipulate TopLevel etc">
+    <Position X="2.895" Y="5.417" Height="1.063" Width="2.992" />
+  </Comment>
+  <Class Name="Terminal.Gui.WindowsInput" Collapsed="true">
+    <Position X="11.5" Y="3" Width="1.75" />
+    <TypeIdentifier>
+      <HashCode>QIAACAAAACAEAAAAAAAAAAAkAAAAAAAAAwAAAAAAABA=</HashCode>
+      <FileName>ConsoleDrivers\V2\WindowsInput.cs</FileName>
+    </TypeIdentifier>
+    <Lollipop Position="0.2" />
+  </Class>
+  <Class Name="Terminal.Gui.NetInput" Collapsed="true">
+    <Position X="13.25" Y="3" Width="2" />
+    <TypeIdentifier>
+      <HashCode>AAAAAAAAACAEAAAAQAAAAAAgAAAAAAAAAAAAAAAAAAA=</HashCode>
+      <FileName>ConsoleDrivers\V2\NetInput.cs</FileName>
+    </TypeIdentifier>
+    <Lollipop Position="0.2" />
+  </Class>
+  <Class Name="Terminal.Gui.ConsoleInput&lt;T&gt;" Collapsed="true">
+    <Position X="12.5" Y="2" Width="1.5" />
+    <TypeIdentifier>
+      <HashCode>AAAAAAAAACAEAQAAAAAAAAAgACAAAAAAAAAAAAAAAAo=</HashCode>
+      <FileName>ConsoleDrivers\V2\ConsoleInput.cs</FileName>
+    </TypeIdentifier>
+    <Lollipop Position="0.2" />
+  </Class>
+  <Class Name="Terminal.Gui.MainLoop&lt;T&gt;" Collapsed="true" BaseTypeListCollapsed="true">
+    <Position X="11" Y="4.75" Width="1.5" />
+    <AssociationLine Name="TimedEvents" Type="Terminal.Gui.ITimedEvents" ManuallyRouted="true">
+      <Path>
+        <Point X="11.312" Y="5.312" />
+        <Point X="11.312" Y="6.292" />
+        <Point X="10" Y="6.292" />
+        <Point X="10" Y="7.25" />
+      </Path>
+      <MemberNameLabel ManuallyPlaced="true">
+        <Position X="-1.015" Y="1.019" />
+      </MemberNameLabel>
+    </AssociationLine>
+    <AssociationLine Name="OutputBuffer" Type="Terminal.Gui.IOutputBuffer" ManuallyRouted="true">
+      <Path>
+        <Point X="11.718" Y="5.312" />
+        <Point X="11.718" Y="7.25" />
+      </Path>
+      <MemberNameLabel ManuallyPlaced="true">
+        <Position X="0.027" Y="0.102" />
+      </MemberNameLabel>
+    </AssociationLine>
+    <AssociationLine Name="Out" Type="Terminal.Gui.IConsoleOutput" ManuallyRouted="true">
+      <Path>
+        <Point X="12.5" Y="5.125" />
+        <Point X="12.5" Y="5.792" />
+        <Point X="13.031" Y="5.792" />
+        <Point X="13.031" Y="7.846" />
+        <Point X="14" Y="7.846" />
+      </Path>
+    </AssociationLine>
+    <AssociationLine Name="AnsiRequestScheduler" Type="Terminal.Gui.AnsiRequestScheduler" ManuallyRouted="true">
+      <Path>
+        <Point X="11.75" Y="4.75" />
+        <Point X="11.75" Y="4.39" />
+        <Point X="20.375" Y="4.39" />
+        <Point X="20.375" Y="4.5" />
+      </Path>
+      <MemberNameLabel ManuallyPlaced="true">
+        <Position X="0.11" Y="0.143" />
+      </MemberNameLabel>
+    </AssociationLine>
+    <AssociationLine Name="WindowSizeMonitor" Type="Terminal.Gui.IWindowSizeMonitor" ManuallyRouted="true">
+      <Path>
+        <Point X="12.125" Y="5.312" />
+        <Point X="12.125" Y="7" />
+        <Point X="12.844" Y="7" />
+        <Point X="12.844" Y="13.281" />
+        <Point X="13.25" Y="13.281" />
+      </Path>
+      <MemberNameLabel ManuallyPlaced="true">
+        <Position X="0.047" Y="-0.336" />
+      </MemberNameLabel>
+    </AssociationLine>
+    <AssociationLine Name="ToplevelTransitionManager" Type="Terminal.Gui.IToplevelTransitionManager" ManuallyRouted="true">
+      <Path>
+        <Point X="11" Y="5.031" />
+        <Point X="11" Y="5.406" />
+        <Point X="9.021" Y="5.406" />
+        <Point X="9.021" Y="11.5" />
+        <Point X="10.375" Y="11.5" />
+        <Point X="10.375" Y="12" />
+      </Path>
+      <MemberNameLabel ManuallyPlaced="true">
+        <Position X="-0.671" Y="0.529" />
+      </MemberNameLabel>
+    </AssociationLine>
+    <TypeIdentifier>
+      <HashCode>QQQAAAAQACABJQQAABAAAQAAACAAAAACAAEAAACAEgg=</HashCode>
+      <FileName>ConsoleDrivers\V2\MainLoop.cs</FileName>
+    </TypeIdentifier>
+    <ShowAsAssociation>
+      <Field Name="ToplevelTransitionManager" />
+      <Property Name="TimedEvents" />
+      <Property Name="InputProcessor" />
+      <Property Name="OutputBuffer" />
+      <Property Name="Out" />
+      <Property Name="AnsiRequestScheduler" />
+      <Property Name="WindowSizeMonitor" />
+    </ShowAsAssociation>
+    <Lollipop Position="0.2" />
+  </Class>
+  <Class Name="Terminal.Gui.MainLoopCoordinator&lt;T&gt;">
+    <Position X="6.5" Y="2" Width="2" />
+    <TypeIdentifier>
+      <HashCode>IAAAIAEiCAIABAAAABQAAAAAABAAAQQAIQIABAAACgg=</HashCode>
+      <FileName>ConsoleDrivers\V2\MainLoopCoordinator.cs</FileName>
+    </TypeIdentifier>
+    <ShowAsAssociation>
+      <Field Name="_loop" />
+    </ShowAsAssociation>
+    <Lollipop Position="0.2" />
+  </Class>
+  <Class Name="Terminal.Gui.AnsiResponseParser&lt;T&gt;" Collapsed="true">
+    <Position X="19.5" Y="10" Width="2" />
+    <TypeIdentifier>
+      <HashCode>AAQAAAAAAAAACIAAAAAAAAAAAAAgAABAAAAACBAAAAA=</HashCode>
+      <FileName>ConsoleDrivers\AnsiResponseParser\AnsiResponseParser.cs</FileName>
+    </TypeIdentifier>
+  </Class>
+  <Class Name="Terminal.Gui.OutputBuffer">
+    <Position X="11" Y="8.25" Width="1.5" />
+    <Compartments>
+      <Compartment Name="Fields" Collapsed="true" />
+      <Compartment Name="Methods" Collapsed="true" />
+    </Compartments>
+    <TypeIdentifier>
+      <HashCode>AwAAAAAAAIAAAECIBgAEQIAAAAEMRgAACAAAKABAgAA=</HashCode>
+      <FileName>ConsoleDrivers\V2\OutputBuffer.cs</FileName>
+    </TypeIdentifier>
+    <Lollipop Position="0.2" />
+  </Class>
+  <Class Name="Terminal.Gui.NetOutput" Collapsed="true">
+    <Position X="14.75" Y="8.5" Width="1.5" />
+    <TypeIdentifier>
+      <HashCode>AEAAAAAAACAAAAAAAAAAAAAAAAAAQAAAMACAAAEAgAk=</HashCode>
+      <FileName>ConsoleDrivers\V2\NetOutput.cs</FileName>
+    </TypeIdentifier>
+    <Lollipop Position="0.2" />
+  </Class>
+  <Class Name="Terminal.Gui.WindowsOutput" Collapsed="true">
+    <Position X="13.25" Y="8.5" Width="1.5" />
+    <TypeIdentifier>
+      <HashCode>AEAAABACACAAhAAAAAAAACCAAAgAQAAIMAAAAAEAgAQ=</HashCode>
+      <FileName>ConsoleDrivers\V2\WindowsOutput.cs</FileName>
+    </TypeIdentifier>
+    <Lollipop Position="0.2" />
+  </Class>
+  <Class Name="Terminal.Gui.InputProcessor&lt;T&gt;" Collapsed="true">
+    <Position X="16.5" Y="4.75" Width="2" />
+    <AssociationLine Name="_mouseInterpreter" Type="Terminal.Gui.MouseInterpreter" ManuallyRouted="true">
+      <Path>
+        <Point X="17.75" Y="5.312" />
+        <Point X="17.75" Y="10.031" />
+        <Point X="15.99" Y="10.031" />
+        <Point X="15.99" Y="10.605" />
+        <Point X="15" Y="10.605" />
+      </Path>
+    </AssociationLine>
+    <TypeIdentifier>
+      <HashCode>AQAkEAAAAASAiAAEAgwgAAAABAIAAAAAAAAAAAAEAAA=</HashCode>
+      <FileName>ConsoleDrivers\V2\InputProcessor.cs</FileName>
+    </TypeIdentifier>
+    <ShowAsAssociation>
+      <Field Name="_mouseInterpreter" />
+      <Property Name="Parser" />
+      <Property Name="KeyConverter" />
+    </ShowAsAssociation>
+    <Lollipop Position="0.1" />
+  </Class>
+  <Class Name="Terminal.Gui.NetInputProcessor" Collapsed="true">
+    <Position X="17.75" Y="5.75" Width="2" />
+    <TypeIdentifier>
+      <HashCode>AAAAAAAAAAAACBAAAgAAAEAAAAAAAAAAAAAAAAAAAAA=</HashCode>
+      <FileName>ConsoleDrivers\V2\NetInputProcessor.cs</FileName>
+    </TypeIdentifier>
+  </Class>
+  <Class Name="Terminal.Gui.WindowsInputProcessor" Collapsed="true">
+    <Position X="15.75" Y="5.75" Width="2" />
+    <TypeIdentifier>
+      <HashCode>AQAAAAAAAAAACAAAAgAAAAAAAgAEAAAAAAAAAAAAAAA=</HashCode>
+      <FileName>ConsoleDrivers\V2\WindowsInputProcessor.cs</FileName>
+    </TypeIdentifier>
+  </Class>
+  <Class Name="Terminal.Gui.AnsiMouseParser">
+    <Position X="23.5" Y="9.75" Width="1.75" />
+    <TypeIdentifier>
+      <HashCode>BAAAAAAAAAgAAAAAAAAAAAAAIAAAAAAAQAAAAAAAAAA=</HashCode>
+      <FileName>ConsoleDrivers\AnsiResponseParser\AnsiMouseParser.cs</FileName>
+    </TypeIdentifier>
+  </Class>
+  <Class Name="Terminal.Gui.ConsoleDriverFacade&lt;T&gt;">
+    <Position X="6.5" Y="7.75" Width="2" />
+    <Compartments>
+      <Compartment Name="Methods" Collapsed="true" />
+      <Compartment Name="Fields" Collapsed="true" />
+    </Compartments>
+    <TypeIdentifier>
+      <HashCode>AQcgAAAAAKBAgFEIBBgAQJEAAjkaQiIAGQADKABDgAQ=</HashCode>
+      <FileName>ConsoleDrivers\V2\ConsoleDriverFacade.cs</FileName>
+    </TypeIdentifier>
+    <ShowAsAssociation>
+      <Property Name="InputProcessor" />
+    </ShowAsAssociation>
+    <Lollipop Position="0.2" />
+  </Class>
+  <Class Name="Terminal.Gui.AnsiRequestScheduler" Collapsed="true">
+    <Position X="19.5" Y="4.5" Width="2" />
+    <TypeIdentifier>
+      <HashCode>AAQAACAAIAAAIAACAESQAAQAACGAAAAAAAAAAAAAQQA=</HashCode>
+      <FileName>ConsoleDrivers\AnsiResponseParser\AnsiRequestScheduler.cs</FileName>
+    </TypeIdentifier>
+    <ShowAsCollectionAssociation>
+      <Property Name="QueuedRequests" />
+    </ShowAsCollectionAssociation>
+  </Class>
+  <Class Name="Terminal.Gui.AnsiResponseParserBase" Collapsed="true">
+    <Position X="20.25" Y="9" Width="2" />
+    <AssociationLine Name="_mouseParser" Type="Terminal.Gui.AnsiMouseParser" FixedFromPoint="true" FixedToPoint="true">
+      <Path>
+        <Point X="22.25" Y="9.438" />
+        <Point X="24.375" Y="9.438" />
+        <Point X="24.375" Y="9.75" />
+      </Path>
+    </AssociationLine>
+    <AssociationLine Name="_keyboardParser" Type="Terminal.Gui.AnsiKeyboardParser" FixedFromPoint="true">
+      <Path>
+        <Point X="22.25" Y="9.375" />
+        <Point X="25.5" Y="9.375" />
+      </Path>
+    </AssociationLine>
+    <TypeIdentifier>
+      <HashCode>UAiASAAAEICQALAAQAAAKAAAoAIAAABAAQIAJiAQASQ=</HashCode>
+      <FileName>ConsoleDrivers\AnsiResponseParser\AnsiResponseParser.cs</FileName>
+    </TypeIdentifier>
+    <ShowAsAssociation>
+      <Field Name="_mouseParser" />
+      <Field Name="_keyboardParser" />
+      <Field Name="_heldContent" />
+    </ShowAsAssociation>
+    <Lollipop Position="0.2" />
+  </Class>
+  <Class Name="Terminal.Gui.MouseInterpreter">
+    <Position X="13.25" Y="10.5" Width="1.75" />
+    <TypeIdentifier>
+      <HashCode>AAAABAAAAAAAAAAAAgAAAAAAACAAAAAAAAUAAAAIAAA=</HashCode>
+      <FileName>ConsoleDrivers\V2\MouseInterpreter.cs</FileName>
+    </TypeIdentifier>
+    <ShowAsCollectionAssociation>
+      <Field Name="_buttonStates" />
+    </ShowAsCollectionAssociation>
+  </Class>
+  <Class Name="Terminal.Gui.MouseButtonStateEx">
+    <Position X="16.5" Y="10.25" Width="2" />
+    <TypeIdentifier>
+      <HashCode>AAAAAAAAAMwAIAAAAAAAAAAAABCAAAAAAAAABAAEAAg=</HashCode>
+      <FileName>ConsoleDrivers\V2\MouseButtonStateEx.cs</FileName>
+    </TypeIdentifier>
+  </Class>
+  <Class Name="Terminal.Gui.StringHeld" Collapsed="true">
+    <Position X="21.5" Y="11" Width="1.75" />
+    <TypeIdentifier>
+      <HashCode>AAAAAAAAAAIAACAAAAAAAIBAAAAAAACAAAAAAAgAAAA=</HashCode>
+      <FileName>ConsoleDrivers\AnsiResponseParser\StringHeld.cs</FileName>
+    </TypeIdentifier>
+    <Lollipop Position="0.2" />
+  </Class>
+  <Class Name="Terminal.Gui.GenericHeld&lt;T&gt;" Collapsed="true">
+    <Position X="19.75" Y="11" Width="1.75" />
+    <TypeIdentifier>
+      <HashCode>AAAAAAAAgAIAACAAAAAAAIBAAAAAAACAAAAAAAAAAAA=</HashCode>
+      <FileName>ConsoleDrivers\AnsiResponseParser\GenericHeld.cs</FileName>
+    </TypeIdentifier>
+    <Lollipop Position="0.2" />
+  </Class>
+  <Class Name="Terminal.Gui.AnsiEscapeSequenceRequest">
+    <Position X="23" Y="4.5" Width="2.5" />
+    <TypeIdentifier>
+      <HashCode>AAAAAAAAAEAAAAAAAEAAAAACAAAAAAAAAAAAAAAAAAA=</HashCode>
+      <FileName>ConsoleDrivers\AnsiEscapeSequenceRequest.cs</FileName>
+    </TypeIdentifier>
+  </Class>
+  <Class Name="Terminal.Gui.AnsiEscapeSequence" Collapsed="true">
+    <Position X="23" Y="3.75" Width="2.5" />
+    <TypeIdentifier>
+      <HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAgAAEAAAA=</HashCode>
+      <FileName>ConsoleDrivers\AnsiEscapeSequence.cs</FileName>
+    </TypeIdentifier>
+  </Class>
+  <Class Name="Terminal.Gui.AnsiResponseParser" Collapsed="true">
+    <Position X="21.5" Y="10" Width="1.75" />
+    <TypeIdentifier>
+      <HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAAgACBAAAAACBAAAAA=</HashCode>
+      <FileName>ConsoleDrivers\AnsiResponseParser\AnsiResponseParser.cs</FileName>
+    </TypeIdentifier>
+  </Class>
+  <Class Name="Terminal.Gui.Application" Collapsed="true">
+    <Position X="0.5" Y="0.5" Width="1.5" />
+    <TypeIdentifier>
+      <HashCode>hEK4FAgAqARIspQeBwoUgTGgACNL0AIAESLKoggBSw8=</HashCode>
+      <FileName>Application\Application.cs</FileName>
+    </TypeIdentifier>
+  </Class>
+  <Class Name="Terminal.Gui.ApplicationImpl" Collapsed="true">
+    <Position X="2.75" Y="4.5" Width="1.5" />
+    <TypeIdentifier>
+      <HashCode>AABAAAAAIAAIAgQQAAAAAQAAAAAAAAAAQAAKgAAAAAI=</HashCode>
+      <FileName>Application\ApplicationImpl.cs</FileName>
+    </TypeIdentifier>
+    <ShowAsAssociation>
+      <Property Name="Instance" />
+    </ShowAsAssociation>
+    <Lollipop Position="0.2" />
+  </Class>
+  <Class Name="Terminal.Gui.ApplicationV2" Collapsed="true">
+    <Position X="4.75" Y="4.5" Width="1.5" />
+    <TypeIdentifier>
+      <HashCode>QAAAAAgABAEIBgAQAAAAAQBAAAAAgAEAAAAKgIAAAgI=</HashCode>
+      <FileName>ConsoleDrivers\V2\ApplicationV2.cs</FileName>
+    </TypeIdentifier>
+    <ShowAsAssociation>
+      <Field Name="_coordinator" />
+    </ShowAsAssociation>
+  </Class>
+  <Class Name="Terminal.Gui.View" Collapsed="true">
+    <Position X="0.5" Y="3" Width="1.5" />
+    <TypeIdentifier>
+      <HashCode>3/v2dzPLvbb/5+LOHuv1x0dem3Y57v/8c6afz2/e/Y8=</HashCode>
+      <FileName>View\View.Adornments.cs</FileName>
+    </TypeIdentifier>
+    <Lollipop Position="0.2" />
+  </Class>
+  <Class Name="Terminal.Gui.WindowsKeyConverter" Collapsed="true">
+    <Position X="16" Y="7.5" Width="1.75" />
+    <TypeIdentifier>
+      <HashCode>AAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</HashCode>
+      <FileName>ConsoleDrivers\V2\WindowsKeyConverter.cs</FileName>
+    </TypeIdentifier>
+    <Lollipop Position="0.2" />
+  </Class>
+  <Class Name="Terminal.Gui.NetKeyConverter" Collapsed="true">
+    <Position X="17.75" Y="7.5" Width="1.5" />
+    <TypeIdentifier>
+      <HashCode>AAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</HashCode>
+      <FileName>ConsoleDrivers\V2\NetKeyConverter.cs</FileName>
+    </TypeIdentifier>
+    <Lollipop Position="0.2" />
+  </Class>
+  <Class Name="Terminal.Gui.AnsiKeyboardParser">
+    <Position X="25.5" Y="9.25" Width="1.75" />
+    <TypeIdentifier>
+      <HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAE=</HashCode>
+      <FileName>ConsoleDrivers\AnsiResponseParser\Keyboard\AnsiKeyboardParser.cs</FileName>
+    </TypeIdentifier>
+    <ShowAsCollectionAssociation>
+      <Field Name="_patterns" />
+    </ShowAsCollectionAssociation>
+  </Class>
+  <Class Name="Terminal.Gui.ToplevelTransitionManager" Collapsed="true">
+    <Position X="9.25" Y="13.75" Width="2.25" />
+    <TypeIdentifier>
+      <HashCode>AIAAAAAAAAAAAAEAAAAAAAAAAEIAAAAAAAAAAAAAAAA=</HashCode>
+      <FileName>ConsoleDrivers\V2\ToplevelTransitionManager.cs</FileName>
+    </TypeIdentifier>
+    <Lollipop Position="0.2" />
+  </Class>
+  <Class Name="Terminal.Gui.Logging" Collapsed="true">
+    <Position X="0.5" Y="5.25" Width="1.5" />
+    <TypeIdentifier>
+      <HashCode>AAAAAAAAAAIgAAAAAAEQAAAAAAAAABAAgAAAAAAAEAA=</HashCode>
+      <FileName>ConsoleDrivers\V2\Logging.cs</FileName>
+    </TypeIdentifier>
+  </Class>
+  <Class Name="Terminal.Gui.WindowSizeMonitor" Collapsed="true" BaseTypeListCollapsed="true">
+    <Position X="13.25" Y="14" Width="1.75" />
+    <TypeIdentifier>
+      <HashCode>AAAAgAAAAAAAAAAEAAAAABAAAAAACAAAAAAAAAAAACA=</HashCode>
+      <FileName>ConsoleDrivers\V2\WindowSizeMonitor.cs</FileName>
+    </TypeIdentifier>
+    <Lollipop Position="0.2" />
+  </Class>
+  <Class Name="Terminal.Gui.AnsiKeyboardParserPattern" Collapsed="true">
+    <Position X="28.5" Y="9.5" Width="2" />
+    <TypeIdentifier>
+      <HashCode>AAACIAAAAAAAAAAAAAAAAAQQAAAAAAAAAAAAAAAACAA=</HashCode>
+      <FileName>ConsoleDrivers\AnsiResponseParser\Keyboard\AnsiKeyboardParserPattern.cs</FileName>
+    </TypeIdentifier>
+  </Class>
+  <Class Name="Terminal.Gui.CsiKeyPattern" Collapsed="true">
+    <Position X="25.5" Y="10.75" Width="1.5" />
+    <TypeIdentifier>
+      <HashCode>AAACAAAAAAAAABAAAAAAAAAQAACAAAAAAAAAAAAAAAA=</HashCode>
+      <FileName>ConsoleDrivers\AnsiResponseParser\Keyboard\CsiKeyPattern.cs</FileName>
+    </TypeIdentifier>
+  </Class>
+  <Class Name="Terminal.Gui.EscAsAltPattern" Collapsed="true">
+    <Position X="27.75" Y="10.75" Width="1.5" />
+    <TypeIdentifier>
+      <HashCode>AAACAAAAAAAAAAAAAAAAAAAQAACAAAAAAAAAAAAAAAA=</HashCode>
+      <FileName>ConsoleDrivers\AnsiResponseParser\Keyboard\EscAsAltPattern.cs</FileName>
+    </TypeIdentifier>
+  </Class>
+  <Class Name="Terminal.Gui.Ss3Pattern" Collapsed="true">
+    <Position X="29.5" Y="10.75" Width="1.5" />
+    <TypeIdentifier>
+      <HashCode>AAACAAAAAAAAAAAAAAAAAAAQAACAAAAAAAAAAAAAAAA=</HashCode>
+      <FileName>ConsoleDrivers\AnsiResponseParser\Keyboard\Ss3Pattern.cs</FileName>
+    </TypeIdentifier>
+  </Class>
+  <Interface Name="Terminal.Gui.IConsoleInput&lt;T&gt;" Collapsed="true">
+    <Position X="12.5" Y="1" Width="1.5" />
+    <TypeIdentifier>
+      <HashCode>AAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAI=</HashCode>
+      <FileName>ConsoleDrivers\V2\IConsoleInput.cs</FileName>
+    </TypeIdentifier>
+  </Interface>
+  <Interface Name="Terminal.Gui.IMainLoop&lt;T&gt;" Collapsed="true">
+    <Position X="9.25" Y="4.5" Width="1.5" />
+    <TypeIdentifier>
+      <HashCode>QAQAAAAAAAABIQQAAAAAAAAAAAAAAAACAAAAAAAAEAA=</HashCode>
+      <FileName>ConsoleDrivers\V2\IMainLoop.cs</FileName>
+    </TypeIdentifier>
+  </Interface>
+  <Interface Name="Terminal.Gui.IConsoleOutput" Collapsed="true">
+    <Position X="14" Y="7.5" Width="1.5" />
+    <TypeIdentifier>
+      <HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAMAAAAAEAAAA=</HashCode>
+      <FileName>ConsoleDrivers\V2\IConsoleOutput.cs</FileName>
+    </TypeIdentifier>
+  </Interface>
+  <Interface Name="Terminal.Gui.IOutputBuffer" Collapsed="true">
+    <Position X="11" Y="7.25" Width="1.5" />
+    <TypeIdentifier>
+      <HashCode>AQAAAAAAAIAAAEAIAAAAQIAAAAEMRgAACAAAKABAgAA=</HashCode>
+      <FileName>ConsoleDrivers\V2\IOutputBuffer.cs</FileName>
+    </TypeIdentifier>
+  </Interface>
+  <Interface Name="Terminal.Gui.IInputProcessor">
+    <Position X="14" Y="4.5" Width="1.5" />
+    <TypeIdentifier>
+      <HashCode>AAAkAAAAAACAgAAAAAggAAAABAIAAAAAAAAAAAAEAAA=</HashCode>
+      <FileName>ConsoleDrivers\V2\IInputProcessor.cs</FileName>
+    </TypeIdentifier>
+  </Interface>
+  <Interface Name="Terminal.Gui.IHeld">
+    <Position X="23.75" Y="6.5" Width="1.75" />
+    <TypeIdentifier>
+      <HashCode>AAAAAAAAAAIAACAAAAAAAIBAAAAAAACAAAAAAAAAAAA=</HashCode>
+      <FileName>ConsoleDrivers\AnsiResponseParser\IHeld.cs</FileName>
+    </TypeIdentifier>
+  </Interface>
+  <Interface Name="Terminal.Gui.IAnsiResponseParser">
+    <Position X="20.25" Y="5.25" Width="2" />
+    <TypeIdentifier>
+      <HashCode>AAAAQAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAJAAAAAA=</HashCode>
+      <FileName>ConsoleDrivers\AnsiResponseParser\IAnsiResponseParser.cs</FileName>
+    </TypeIdentifier>
+    <ShowAsAssociation>
+      <Property Name="State" />
+    </ShowAsAssociation>
+  </Interface>
+  <Interface Name="Terminal.Gui.IApplication">
+    <Position X="3" Y="1" Width="1.5" />
+    <TypeIdentifier>
+      <HashCode>AAAAAAAAAAAIAgQQAAAAAQAAAAAAAAAAAAAKgAAAAAI=</HashCode>
+      <FileName>Application\IApplication.cs</FileName>
+    </TypeIdentifier>
+  </Interface>
+  <Interface Name="Terminal.Gui.IMainLoopCoordinator" Collapsed="true">
+    <Position X="6.5" Y="0.5" Width="2" />
+    <TypeIdentifier>
+      <HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIQIAAAAAAAA=</HashCode>
+      <FileName>ConsoleDrivers\V2\IMainLoopCoordinator.cs</FileName>
+    </TypeIdentifier>
+  </Interface>
+  <Interface Name="Terminal.Gui.IWindowSizeMonitor" Collapsed="true">
+    <Position X="13.25" Y="13" Width="1.75" />
+    <TypeIdentifier>
+      <HashCode>AAAAAAAAAAAAAAAEAAAAAAAAAAAACAAAAAAAAAAAAAA=</HashCode>
+      <FileName>ConsoleDrivers\V2\IWindowSizeMonitor.cs</FileName>
+    </TypeIdentifier>
+  </Interface>
+  <Interface Name="Terminal.Gui.ITimedEvents">
+    <Position X="9.25" Y="7.25" Width="1.5" />
+    <Compartments>
+      <Compartment Name="Methods" Collapsed="true" />
+    </Compartments>
+    <TypeIdentifier>
+      <HashCode>BAAAIAAAAQAAAAAQACAAAIBAAQAAAAAAAAAIgAAAAAA=</HashCode>
+      <FileName>Application\ITimedEvents.cs</FileName>
+    </TypeIdentifier>
+  </Interface>
+  <Interface Name="Terminal.Gui.IKeyConverter&lt;T&gt;" Collapsed="true">
+    <Position X="17" Y="6.5" Width="1.75" />
+    <TypeIdentifier>
+      <HashCode>AAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</HashCode>
+      <FileName>ConsoleDrivers\V2\IKeyConverter.cs</FileName>
+    </TypeIdentifier>
+  </Interface>
+  <Interface Name="Terminal.Gui.IToplevelTransitionManager">
+    <Position X="9.25" Y="12" Width="2.25" />
+    <TypeIdentifier>
+      <HashCode>AIAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</HashCode>
+      <FileName>ConsoleDrivers\V2\IToplevelTransitionManager.cs</FileName>
+    </TypeIdentifier>
+  </Interface>
+  <Interface Name="Terminal.Gui.IConsoleDriverFacade">
+    <Position X="4.5" Y="8.75" Width="1.75" />
+    <TypeIdentifier>
+      <HashCode>AAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</HashCode>
+      <FileName>ConsoleDrivers\V2\IConsoleDriverFacade.cs</FileName>
+    </TypeIdentifier>
+  </Interface>
+  <Interface Name="Terminal.Gui.INetInput" Collapsed="true">
+    <Position X="14.25" Y="2" Width="1.75" />
+    <TypeIdentifier>
+      <HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</HashCode>
+      <FileName>ConsoleDrivers\V2\INetInput.cs</FileName>
+    </TypeIdentifier>
+  </Interface>
+  <Interface Name="Terminal.Gui.IWindowsInput" Collapsed="true">
+    <Position X="10.75" Y="2" Width="1.5" />
+    <TypeIdentifier>
+      <HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</HashCode>
+      <FileName>ConsoleDrivers\V2\IWindowsInput.cs</FileName>
+    </TypeIdentifier>
+  </Interface>
+  <Enum Name="Terminal.Gui.AnsiResponseParserState">
+    <Position X="20.25" Y="7.25" Width="2" />
+    <TypeIdentifier>
+      <HashCode>AAAAAAAAAAAAAAAAAAAACAAAAAAIAAIAAAAAAAAAAAA=</HashCode>
+      <FileName>ConsoleDrivers\AnsiResponseParser\AnsiResponseParserState.cs</FileName>
+    </TypeIdentifier>
+  </Enum>
+  <Font Name="Segoe UI" Size="9" />
+</ClassDiagram>
\ No newline at end of file
diff --git a/Terminal.Gui/ConsoleDrivers/V2/WindowSizeMonitor.cs b/Terminal.Gui/ConsoleDrivers/V2/WindowSizeMonitor.cs
new file mode 100644
index 0000000000..18707fa83a
--- /dev/null
+++ b/Terminal.Gui/ConsoleDrivers/V2/WindowSizeMonitor.cs
@@ -0,0 +1,37 @@
+using Microsoft.Extensions.Logging;
+
+namespace Terminal.Gui;
+
+internal class WindowSizeMonitor : IWindowSizeMonitor
+{
+    private readonly IConsoleOutput _consoleOut;
+    private readonly IOutputBuffer _outputBuffer;
+    private Size _lastSize = new (0, 0);
+
+    /// <summary>Invoked when the terminal's size changed. The new size of the terminal is provided.</summary>
+    public event EventHandler<SizeChangedEventArgs> SizeChanging;
+
+    public WindowSizeMonitor (IConsoleOutput consoleOut, IOutputBuffer outputBuffer)
+    {
+        _consoleOut = consoleOut;
+        _outputBuffer = outputBuffer;
+    }
+
+    /// <inheritdoc/>
+    public bool Poll ()
+    {
+        Size size = _consoleOut.GetWindowSize ();
+
+        if (size != _lastSize)
+        {
+            Logging.Logger.LogInformation ($"Console size changes from '{_lastSize}' to {size}");
+            _outputBuffer.SetWindowSize (size.Width, size.Height);
+            _lastSize = size;
+            SizeChanging?.Invoke (this, new (size));
+
+            return true;
+        }
+
+        return false;
+    }
+}
diff --git a/Terminal.Gui/ConsoleDrivers/V2/WindowsInput.cs b/Terminal.Gui/ConsoleDrivers/V2/WindowsInput.cs
new file mode 100644
index 0000000000..423c6f06a0
--- /dev/null
+++ b/Terminal.Gui/ConsoleDrivers/V2/WindowsInput.cs
@@ -0,0 +1,114 @@
+using System.Runtime.InteropServices;
+using Microsoft.Extensions.Logging;
+using static Terminal.Gui.WindowsConsole;
+
+namespace Terminal.Gui;
+
+internal class WindowsInput : ConsoleInput<InputRecord>, IWindowsInput
+{
+    private readonly nint _inputHandle;
+
+    [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", EntryPoint = "PeekConsoleInputW", CharSet = CharSet.Unicode)]
+    public static extern bool PeekConsoleInput (
+        nint hConsoleInput,
+        nint lpBuffer,
+        uint nLength,
+        out uint lpNumberOfEventsRead
+    );
+
+    [DllImport ("kernel32.dll", SetLastError = true)]
+    private static extern nint GetStdHandle (int nStdHandle);
+
+    [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);
+
+    private readonly uint _originalConsoleMode;
+
+    public WindowsInput ()
+    {
+        Logging.Logger.LogInformation ($"Creating {nameof (WindowsInput)}");
+        _inputHandle = GetStdHandle (STD_INPUT_HANDLE);
+
+        GetConsoleMode (_inputHandle, out uint v);
+        _originalConsoleMode = v;
+
+        uint newConsoleMode = _originalConsoleMode;
+        newConsoleMode |= (uint)(ConsoleModes.EnableMouseInput | ConsoleModes.EnableExtendedFlags);
+        newConsoleMode &= ~(uint)ConsoleModes.EnableQuickEditMode;
+        newConsoleMode &= ~(uint)ConsoleModes.EnableProcessedInput;
+        SetConsoleMode (_inputHandle, newConsoleMode);
+    }
+
+    protected override bool Peek ()
+    {
+        const int bufferSize = 1; // We only need to check if there's at least one event
+        nint pRecord = Marshal.AllocHGlobal (Marshal.SizeOf<InputRecord> () * bufferSize);
+
+        try
+        {
+            // Use PeekConsoleInput to inspect the input buffer without removing events
+            if (PeekConsoleInput (_inputHandle, pRecord, bufferSize, out uint numberOfEventsRead))
+            {
+                // Return true if there's at least one event in the buffer
+                return numberOfEventsRead > 0;
+            }
+            else
+            {
+                // Handle the failure of PeekConsoleInput
+                throw new InvalidOperationException ("Failed to peek console input.");
+            }
+        }
+        catch (Exception ex)
+        {
+            // Optionally log the exception
+            Console.WriteLine ($"Error in Peek: {ex.Message}");
+
+            return false;
+        }
+        finally
+        {
+            // Free the allocated memory
+            Marshal.FreeHGlobal (pRecord);
+        }
+    }
+
+    protected override IEnumerable<InputRecord> Read ()
+    {
+        const int bufferSize = 1;
+        nint pRecord = Marshal.AllocHGlobal (Marshal.SizeOf<InputRecord> () * bufferSize);
+
+        try
+        {
+            ReadConsoleInput (
+                              _inputHandle,
+                              pRecord,
+                              bufferSize,
+                              out uint numberEventsRead);
+
+            return numberEventsRead == 0
+                       ? []
+                       : new [] { Marshal.PtrToStructure<InputRecord> (pRecord) };
+        }
+        catch (Exception)
+        {
+            return [];
+        }
+        finally
+        {
+            Marshal.FreeHGlobal (pRecord);
+        }
+    }
+
+    public override void Dispose () { SetConsoleMode (_inputHandle, _originalConsoleMode); }
+}
diff --git a/Terminal.Gui/ConsoleDrivers/V2/WindowsInputProcessor.cs b/Terminal.Gui/ConsoleDrivers/V2/WindowsInputProcessor.cs
new file mode 100644
index 0000000000..da1e81887a
--- /dev/null
+++ b/Terminal.Gui/ConsoleDrivers/V2/WindowsInputProcessor.cs
@@ -0,0 +1,157 @@
+#nullable enable
+using System.Collections.Concurrent;
+using static Terminal.Gui.WindowsConsole;
+
+namespace Terminal.Gui;
+
+using InputRecord = InputRecord;
+
+/// <summary>
+///     Input processor for <see cref="WindowsInput"/>, deals in <see cref="WindowsConsole.InputRecord"/> stream.
+/// </summary>
+internal class WindowsInputProcessor : InputProcessor<InputRecord>
+{
+    private readonly bool [] _lastWasPressed = new bool[4];
+
+    /// <inheritdoc/>
+    public WindowsInputProcessor (ConcurrentQueue<InputRecord> inputBuffer) : base (inputBuffer, new WindowsKeyConverter ()) { }
+
+    /// <inheritdoc/>
+    protected override void Process (InputRecord inputEvent)
+    {
+        switch (inputEvent.EventType)
+        {
+            case EventType.Key:
+
+                // TODO: For now ignore keyup because ANSI comes in as down+up which is confusing to try and parse/pair these things up
+                if (!inputEvent.KeyEvent.bKeyDown)
+                {
+                    return;
+                }
+
+                foreach (Tuple<char, InputRecord> released in Parser.ProcessInput (Tuple.Create (inputEvent.KeyEvent.UnicodeChar, inputEvent)))
+                {
+                    ProcessAfterParsing (released.Item2);
+                }
+
+                /*
+                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;
+                }
+                */
+                // This follows convention in NetDriver
+
+                break;
+
+            case EventType.Mouse:
+                MouseEventArgs me = ToDriverMouse (inputEvent.MouseEvent);
+
+                OnMouseEvent (me);
+
+                break;
+        }
+    }
+
+    /// <inheritdoc/>
+    protected override void ProcessAfterParsing (InputRecord input)
+    {
+        var key = KeyConverter.ToKey (input);
+
+        if (key != (Key)0)
+        {
+            OnKeyDown (key!);
+            OnKeyUp (key!);
+        }
+    }
+
+    public MouseEventArgs ToDriverMouse (MouseEventRecord e)
+    {
+        var mouseFlags = MouseFlags.ReportMousePosition;
+
+        mouseFlags = UpdateMouseFlags (mouseFlags, e.ButtonState, ButtonState.Button1Pressed, MouseFlags.Button1Pressed, MouseFlags.Button1Released, 0);
+        mouseFlags = UpdateMouseFlags (mouseFlags, e.ButtonState, ButtonState.Button2Pressed, MouseFlags.Button2Pressed, MouseFlags.Button2Released, 1);
+        mouseFlags = UpdateMouseFlags (mouseFlags, e.ButtonState, ButtonState.Button4Pressed, MouseFlags.Button4Pressed, MouseFlags.Button4Released, 3);
+
+        // Deal with button 3 separately because it is considered same as 'rightmost button'
+        if (e.ButtonState.HasFlag (ButtonState.Button3Pressed) || e.ButtonState.HasFlag (ButtonState.RightmostButtonPressed))
+        {
+            mouseFlags |= MouseFlags.Button3Pressed;
+            _lastWasPressed [2] = true;
+        }
+        else
+        {
+            if (_lastWasPressed [2])
+            {
+                mouseFlags |= MouseFlags.Button3Released;
+                _lastWasPressed [2] = false;
+            }
+        }
+
+        if (e.EventFlags == EventFlags.MouseWheeled)
+        {
+            switch ((int)e.ButtonState)
+            {
+                case > 0:
+                    mouseFlags = MouseFlags.WheeledUp;
+
+                    break;
+
+                case < 0:
+                    mouseFlags = MouseFlags.WheeledDown;
+
+                    break;
+            }
+        }
+
+        var result = new MouseEventArgs
+        {
+            Position = new (e.MousePosition.X, e.MousePosition.Y),
+            Flags = mouseFlags
+        };
+
+        // TODO: Return keys too
+
+        return result;
+    }
+
+    private MouseFlags UpdateMouseFlags (
+        MouseFlags current,
+        ButtonState newState,
+        ButtonState pressedState,
+        MouseFlags pressedFlag,
+        MouseFlags releasedFlag,
+        int buttonIndex
+    )
+    {
+        if (newState.HasFlag (pressedState))
+        {
+            current |= pressedFlag;
+            _lastWasPressed [buttonIndex] = true;
+        }
+        else
+        {
+            if (_lastWasPressed [buttonIndex])
+            {
+                current |= releasedFlag;
+                _lastWasPressed [buttonIndex] = false;
+            }
+        }
+
+        return current;
+    }
+}
diff --git a/Terminal.Gui/ConsoleDrivers/V2/WindowsKeyConverter.cs b/Terminal.Gui/ConsoleDrivers/V2/WindowsKeyConverter.cs
new file mode 100644
index 0000000000..fd092db67c
--- /dev/null
+++ b/Terminal.Gui/ConsoleDrivers/V2/WindowsKeyConverter.cs
@@ -0,0 +1,38 @@
+#nullable enable
+using Terminal.Gui.ConsoleDrivers;
+
+namespace Terminal.Gui;
+
+/// <summary>
+///     <see cref="IKeyConverter{T}"/> capable of converting the
+///     windows native <see cref="WindowsConsole.InputRecord"/> class
+///     into Terminal.Gui shared <see cref="Key"/> representation
+///     (used by <see cref="View"/> etc).
+/// </summary>
+internal class WindowsKeyConverter : IKeyConverter<WindowsConsole.InputRecord>
+{
+    /// <inheritdoc/>
+    public Key ToKey (WindowsConsole.InputRecord inputEvent)
+    {
+        if (inputEvent.KeyEvent.wVirtualKeyCode == (ConsoleKeyMapping.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 = WindowsDriver.FromVKPacketToKeyEventRecord (inputEvent.KeyEvent);
+        }
+
+        var keyInfo = WindowsDriver.ToConsoleKeyInfoEx (inputEvent.KeyEvent);
+
+        //Debug.WriteLine ($"event: KBD: {GetKeyboardLayoutName()} {inputEvent.ToString ()} {keyInfo.ToString (keyInfo)}");
+
+        KeyCode map = WindowsDriver.MapKey (keyInfo);
+
+        if (map == KeyCode.Null)
+        {
+            return (Key)0;
+        }
+
+        return new (map);
+    }
+}
diff --git a/Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs b/Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs
new file mode 100644
index 0000000000..382b01aa87
--- /dev/null
+++ b/Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs
@@ -0,0 +1,344 @@
+#nullable enable
+using System.ComponentModel;
+using System.Runtime.InteropServices;
+using Microsoft.Extensions.Logging;
+using static Terminal.Gui.WindowsConsole;
+
+namespace Terminal.Gui;
+
+internal class WindowsOutput : IConsoleOutput
+{
+    [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)]
+    private static extern bool CloseHandle (nint handle);
+
+    [DllImport ("kernel32.dll", SetLastError = true)]
+    private static extern nint CreateConsoleScreenBuffer (
+        DesiredAccess dwDesiredAccess,
+        ShareMode dwShareMode,
+        nint secutiryAttributes,
+        uint flags,
+        nint screenBufferData
+    );
+
+    [DllImport ("kernel32.dll", SetLastError = true)]
+    private static extern bool GetConsoleScreenBufferInfoEx (nint hConsoleOutput, ref CONSOLE_SCREEN_BUFFER_INFOEX csbi);
+
+    [Flags]
+    private enum ShareMode : uint
+    {
+        FileShareRead = 1,
+        FileShareWrite = 2
+    }
+
+    [Flags]
+    private enum DesiredAccess : uint
+    {
+        GenericRead = 2147483648,
+        GenericWrite = 1073741824
+    }
+
+    internal static nint INVALID_HANDLE_VALUE = new (-1);
+
+    [DllImport ("kernel32.dll", SetLastError = true)]
+    private static extern bool SetConsoleActiveScreenBuffer (nint handle);
+
+    [DllImport ("kernel32.dll")]
+    private static extern bool SetConsoleCursorPosition (nint hConsoleOutput, Coord dwCursorPosition);
+
+    private readonly nint _screenBuffer;
+
+    public WindowsOutput ()
+    {
+        Logging.Logger.LogInformation ($"Creating {nameof (WindowsOutput)}");
+
+        _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);
+            }
+        }
+
+        if (!SetConsoleActiveScreenBuffer (_screenBuffer))
+        {
+            throw new Win32Exception (Marshal.GetLastWin32Error ());
+        }
+    }
+
+    public void Write (string str)
+    {
+        if (!WriteConsole (_screenBuffer, str, (uint)str.Length, out uint _, nint.Zero))
+        {
+            throw new Win32Exception (Marshal.GetLastWin32Error (), "Failed to write to console screen buffer.");
+        }
+    }
+
+    public void Write (IOutputBuffer buffer)
+    {
+        ExtendedCharInfo [] outputBuffer = new ExtendedCharInfo [buffer.Rows * buffer.Cols];
+
+        // TODO: probably do need this right?
+        /*
+        if (!windowSize.IsEmpty && (windowSize.Width != buffer.Cols || windowSize.Height != buffer.Rows))
+        {
+            return;
+        }*/
+
+        var bufferCoords = new Coord
+        {
+            X = (short)buffer.Cols, //Clip.Width,
+            Y = (short)buffer.Rows //Clip.Height
+        };
+
+        for (var row = 0; row < buffer.Rows; row++)
+        {
+            if (!buffer.DirtyLines [row])
+            {
+                continue;
+            }
+
+            buffer.DirtyLines [row] = false;
+
+            for (var col = 0; col < buffer.Cols; col++)
+            {
+                int position = row * buffer.Cols + col;
+                outputBuffer [position].Attribute = buffer.Contents [row, col].Attribute.GetValueOrDefault ();
+
+                if (buffer.Contents [row, col].IsDirty == false)
+                {
+                    outputBuffer [position].Empty = true;
+                    outputBuffer [position].Char = (char)Rune.ReplacementChar.Value;
+
+                    continue;
+                }
+
+                outputBuffer [position].Empty = false;
+
+                if (buffer.Contents [row, col].Rune.IsBmp)
+                {
+                    outputBuffer [position].Char = (char)buffer.Contents [row, col].Rune.Value;
+                }
+                else
+                {
+                    //outputBuffer [position].Empty = true;
+                    outputBuffer [position].Char = (char)Rune.ReplacementChar.Value;
+
+                    if (buffer.Contents [row, col].Rune.GetColumns () > 1 && col + 1 < buffer.Cols)
+                    {
+                        // TODO: This is a hack to deal with non-BMP and wide characters.
+                        col++;
+                        position = row * buffer.Cols + col;
+                        outputBuffer [position].Empty = false;
+                        outputBuffer [position].Char = ' ';
+                    }
+                }
+            }
+        }
+
+        var damageRegion = new SmallRect
+        {
+            Top = 0,
+            Left = 0,
+            Bottom = (short)buffer.Rows,
+            Right = (short)buffer.Cols
+        };
+
+        //size, ExtendedCharInfo [] charInfoBuffer, Coord , SmallRect window,
+        if (!WriteToConsole (
+                             new (buffer.Cols, buffer.Rows),
+                             outputBuffer,
+                             bufferCoords,
+                             damageRegion,
+                             false))
+        {
+            int err = Marshal.GetLastWin32Error ();
+
+            if (err != 0)
+            {
+                throw new Win32Exception (err);
+            }
+        }
+
+        SmallRect.MakeEmpty (ref damageRegion);
+    }
+
+    public bool WriteToConsole (Size size, ExtendedCharInfo [] charInfoBuffer, Coord bufferSize, SmallRect window, bool force16Colors)
+    {
+        var stringBuilder = new StringBuilder ();
+
+        //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 ()
+                {
+                    Char = new () { UnicodeChar = info.Char },
+                    Attributes =
+                        (ushort)((int)info.Attribute.Foreground.GetClosestNamedColor16 () | ((int)info.Attribute.Background.GetClosestNamedColor16 () << 4))
+                };
+            }
+
+            result = WriteConsoleOutput (_screenBuffer, ci, bufferSize, new () { 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);
+
+            foreach (SixelToRender sixel in Application.Sixel)
+            {
+                SetCursorPosition ((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 Size GetWindowSize ()
+    {
+        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 ());
+            return Size.Empty;
+        }
+
+        Size sz = new (
+                       csbi.srWindow.Right - csbi.srWindow.Left + 1,
+                       csbi.srWindow.Bottom - csbi.srWindow.Top + 1);
+
+        return sz;
+    }
+
+    /// <inheritdoc/>
+    public void SetCursorVisibility (CursorVisibility visibility)
+    {
+        var sb = new StringBuilder ();
+        sb.Append (visibility != CursorVisibility.Invisible ? EscSeqUtils.CSI_ShowCursor : EscSeqUtils.CSI_HideCursor);
+        Write (sb.ToString ());
+    }
+
+    private Point _lastCursorPosition;
+
+    /// <inheritdoc/>
+    public void SetCursorPosition (int col, int row)
+    {
+        if (_lastCursorPosition.X == col && _lastCursorPosition.Y == row)
+        {
+            return;
+        }
+
+        _lastCursorPosition = new (col, row);
+
+        SetConsoleCursorPosition (_screenBuffer, new ((short)col, (short)row));
+    }
+
+    private bool _isDisposed;
+
+    /// <inheritdoc/>
+    public void Dispose ()
+    {
+        if (_isDisposed)
+        {
+            return;
+        }
+
+        if (_screenBuffer != nint.Zero)
+        {
+            try
+            {
+                CloseHandle (_screenBuffer);
+            }
+            catch (Exception e)
+            {
+                Logging.Logger.LogError (e, "Error trying to close screen buffer handle in WindowsOutput via interop method");
+            }
+        }
+
+        _isDisposed = true;
+    }
+}
diff --git a/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsConsole.cs b/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsConsole.cs
index 0d9a562543..8266790511 100644
--- a/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsConsole.cs
+++ b/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsConsole.cs
@@ -36,7 +36,7 @@ public WindowsConsole ()
         newConsoleMode &= ~(uint)ConsoleModes.EnableProcessedInput;
         ConsoleMode = newConsoleMode;
 
-        _inputReadyCancellationTokenSource = new ();
+        _inputReadyCancellationTokenSource = new (); 
         Task.Run (ProcessInputQueue, _inputReadyCancellationTokenSource.Token);
     }
 
@@ -918,7 +918,7 @@ 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 (
+    public static extern bool WriteConsoleOutput (
         nint hConsoleOutput,
         CharInfo [] lpBuffer,
         Coord dwBufferSize,
diff --git a/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsDriver.cs b/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsDriver.cs
index db680d5c73..26ca3023b4 100644
--- a/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsDriver.cs
+++ b/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsDriver.cs
@@ -70,7 +70,7 @@ public WindowsDriver ()
 
     public WindowsConsole? WinConsole { get; private set; }
 
-    public WindowsConsole.KeyEventRecord FromVKPacketToKeyEventRecord (WindowsConsole.KeyEventRecord keyEvent)
+    public static WindowsConsole.KeyEventRecord FromVKPacketToKeyEventRecord (WindowsConsole.KeyEventRecord keyEvent)
     {
         if (keyEvent.wVirtualKeyCode != (VK)ConsoleKey.Packet)
         {
@@ -203,7 +203,7 @@ public override void WriteRaw (string str)
 
     #endregion
 
-    public WindowsConsole.ConsoleKeyInfoEx ToConsoleKeyInfoEx (WindowsConsole.KeyEventRecord keyEvent)
+    public static WindowsConsole.ConsoleKeyInfoEx ToConsoleKeyInfoEx (WindowsConsole.KeyEventRecord keyEvent)
     {
         WindowsConsole.ControlKeyState state = keyEvent.dwControlKeyState;
 
@@ -582,7 +582,7 @@ internal void ProcessInputAfterParsing (WindowsConsole.InputRecord inputEvent)
 
     public IEnumerable<WindowsConsole.InputRecord> ShouldReleaseParserHeldKeys ()
     {
-        if (_parser.State == AnsiResponseParserState.ExpectingBracket &&
+        if (_parser.State == AnsiResponseParserState.ExpectingEscapeSequence &&
             DateTime.Now - _parser.StateChangedAt > EscTimeout)
         {
             return _parser.Release ().Select (o => o.Item2);
@@ -627,7 +627,7 @@ private void ChangeWin (object s, SizeChangedEventArgs e)
     }
 #endif
 
-    private KeyCode MapKey (WindowsConsole.ConsoleKeyInfoEx keyInfoEx)
+    public static KeyCode MapKey (WindowsConsole.ConsoleKeyInfoEx keyInfoEx)
     {
         ConsoleKeyInfo keyInfo = keyInfoEx.ConsoleKeyInfo;
 
diff --git a/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsMainLoop.cs b/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsMainLoop.cs
index fedcf6f732..13fcafbd9a 100644
--- a/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsMainLoop.cs
+++ b/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsMainLoop.cs
@@ -68,7 +68,7 @@ bool IMainLoopDriver.EventsPending ()
 #if HACK_CHECK_WINCHANGED
         _winChange.Set ();
 #endif
-        if (_resultQueue.Count > 0 || _mainLoop!.CheckTimersAndIdleHandlers (out int waitTimeout))
+        if (_resultQueue.Count > 0 || _mainLoop!.TimedEvents.CheckTimersAndIdleHandlers (out int waitTimeout))
         {
             return true;
         }
@@ -97,9 +97,9 @@ bool IMainLoopDriver.EventsPending ()
         if (!_eventReadyTokenSource.IsCancellationRequested)
         {
 #if HACK_CHECK_WINCHANGED
-            return _resultQueue.Count > 0 || _mainLoop.CheckTimersAndIdleHandlers (out _) || _winChanged;
+            return _resultQueue.Count > 0 || _mainLoop.TimedEvents.CheckTimersAndIdleHandlers (out _) || _winChanged;
 #else
-            return _resultQueue.Count > 0 || _mainLoop.CheckTimersAndIdleHandlers (out _);
+            return _resultQueue.Count > 0 || _mainLoop.TimedEvents.CheckTimersAndIdleHandlers (out _);
 #endif
         }
 
diff --git a/Terminal.Gui/Input/Keyboard/Key.cs b/Terminal.Gui/Input/Keyboard/Key.cs
index cf524b6e5b..53d6292756 100644
--- a/Terminal.Gui/Input/Keyboard/Key.cs
+++ b/Terminal.Gui/Input/Keyboard/Key.cs
@@ -418,7 +418,7 @@ public override bool Equals (object? obj)
     /// <param name="a"></param>
     /// <param name="b"></param>
     /// <returns></returns>
-    public static bool operator != (Key a, Key b) { return !a!.Equals (b); }
+    public static bool operator != (Key a, Key? b) { return !a!.Equals (b); }
 
     /// <summary>Compares two <see cref="Key"/>s for less-than.</summary>
     /// <param name="a"></param>
diff --git a/Terminal.Gui/Terminal.Gui.csproj b/Terminal.Gui/Terminal.Gui.csproj
index bd2d56f292..aaadfee756 100644
--- a/Terminal.Gui/Terminal.Gui.csproj
+++ b/Terminal.Gui/Terminal.Gui.csproj
@@ -58,6 +58,7 @@
     <PackageReference Include="Microsoft.CodeAnalysis" Version="[4.10,5)" PrivateAssets="all" />
     <PackageReference Include="Microsoft.CodeAnalysis.Common" Version="[4.10,5)" PrivateAssets="all" />
     <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="[4.10,5)" PrivateAssets="all" />
+    <PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
     <!-- Enable Nuget Source Link for github -->
     <PackageReference Include="Microsoft.SourceLink.GitHub" Version="[8,9)" PrivateAssets="all" />
     <PackageReference Include="System.IO.Abstractions" Version="[21.0.22,22)" />
@@ -80,6 +81,7 @@
   <ItemGroup>
     <InternalsVisibleTo Include="UnitTests" />
     <InternalsVisibleTo Include="TerminalGuiDesigner" />
+    <InternalsVisibleTo Include="DynamicProxyGenAssembly2" />
   </ItemGroup>
   <!-- =================================================================== -->
   <!-- API Documentation -->
diff --git a/Terminal.Gui/Text/CollectionNavigatorBase.cs b/Terminal.Gui/Text/CollectionNavigatorBase.cs
index 4caa219cd9..3ffe411d54 100644
--- a/Terminal.Gui/Text/CollectionNavigatorBase.cs
+++ b/Terminal.Gui/Text/CollectionNavigatorBase.cs
@@ -1,4 +1,6 @@
-namespace Terminal.Gui;
+using Microsoft.Extensions.Logging;
+
+namespace Terminal.Gui;
 
 /// <summary>
 ///     Navigates a collection of items using keystrokes. The keystrokes are used to build a search string. The
@@ -57,18 +59,23 @@ public int GetNextMatchingItem (int currentIndex, char keyStruck)
             // but if we find none then we must fallback on cycling
             // d instead and discard the candidate state
             var candidateState = "";
+            var elapsedTime = DateTime.Now - _lastKeystroke;
+
+            Logging.Trace($"CollectionNavigator began processing '{keyStruck}', it has been {elapsedTime} since last keystroke");
 
             // is it a second or third (etc) keystroke within a short time
-            if (SearchString.Length > 0 && DateTime.Now - _lastKeystroke < TimeSpan.FromMilliseconds (TypingDelay))
+            if (SearchString.Length > 0 && elapsedTime < TimeSpan.FromMilliseconds (TypingDelay))
             {
                 // "dd" is a candidate
                 candidateState = SearchString + keyStruck;
+                Logging.Trace($"Appending, search is now for '{candidateState}'");
             }
             else
             {
                 // its a fresh keystroke after some time
                 // or its first ever key press
                 SearchString = new string (keyStruck, 1);
+                Logging.Trace($"It has been too long since last key press so beginning new search");
             }
 
             int idxCandidate = GetNextMatchingItem (
@@ -79,12 +86,14 @@ public int GetNextMatchingItem (int currentIndex, char keyStruck)
                                                     candidateState.Length > 1
                                                    );
 
+            Logging.Trace($"CollectionNavigator searching (preferring minimum movement) matched:{idxCandidate}");
             if (idxCandidate != -1)
             {
                 // found "dd" so candidate search string is accepted
                 _lastKeystroke = DateTime.Now;
                 SearchString = candidateState;
 
+                Logging.Trace($"Found collection item that matched search:{idxCandidate}");
                 return idxCandidate;
             }
 
@@ -93,10 +102,13 @@ public int GetNextMatchingItem (int currentIndex, char keyStruck)
             _lastKeystroke = DateTime.Now;
             idxCandidate = GetNextMatchingItem (currentIndex, candidateState);
 
+            Logging.Trace($"CollectionNavigator searching (any match) matched:{idxCandidate}");
+
             // if a match wasn't found, the user typed a 'wrong' key in their search ("can" + 'z'
             // instead of "can" + 'd').
             if (SearchString.Length > 1 && idxCandidate == -1)
             {
+                Logging.Trace("CollectionNavigator ignored key and returned existing index");
                 // ignore it since we're still within the typing delay
                 // don't add it to SearchString either
                 return currentIndex;
@@ -105,6 +117,8 @@ public int GetNextMatchingItem (int currentIndex, char keyStruck)
             // if no changes to current state manifested
             if (idxCandidate == currentIndex || idxCandidate == -1)
             {
+                Logging.Trace("CollectionNavigator found no changes to current index, so clearing search");
+
                 // clear history and treat as a fresh letter
                 ClearSearchString ();
 
@@ -112,13 +126,18 @@ public int GetNextMatchingItem (int currentIndex, char keyStruck)
                 SearchString = new string (keyStruck, 1);
                 idxCandidate = GetNextMatchingItem (currentIndex, SearchString);
 
+                Logging.Trace($"CollectionNavigator new SearchString {SearchString} matched index:{idxCandidate}" );
+
                 return idxCandidate == -1 ? currentIndex : idxCandidate;
             }
 
+            Logging.Trace($"CollectionNavigator final answer was:{idxCandidate}" );
             // Found another "d" or just leave index as it was
             return idxCandidate;
         }
 
+        Logging.Trace("CollectionNavigator found key press was not actionable so clearing search and returning -1");
+
         // clear state because keypress was a control char
         ClearSearchString ();
 
diff --git a/Terminal.Gui/View/View.Drawing.cs b/Terminal.Gui/View/View.Drawing.cs
index e15339bb45..2e33706e14 100644
--- a/Terminal.Gui/View/View.Drawing.cs
+++ b/Terminal.Gui/View/View.Drawing.cs
@@ -738,7 +738,8 @@ public void SetNeedsDraw (Rectangle viewPortRelativeRegion)
             adornment.Parent?.SetSubViewNeedsDraw ();
         }
 
-        foreach (View subview in Subviews)
+        // There was multiple enumeration error here, so calling ToArray - probably a stop gap
+        foreach (View subview in Subviews.ToArray ())
         {
             if (subview.Frame.IntersectsWith (viewPortRelativeRegion))
             {
diff --git a/Terminal.Gui/View/View.cs b/Terminal.Gui/View/View.cs
index f0bf0daa04..8ffff38c86 100644
--- a/Terminal.Gui/View/View.cs
+++ b/Terminal.Gui/View/View.cs
@@ -163,8 +163,11 @@ public virtual void EndInit ()
             }
         }
 
-        // TODO: Figure out how to move this out of here and just depend on LayoutNeeded in Mainloop
-        Layout (); // the EventLog in AllViewsTester fails to layout correctly if this is not here (convoluted Dim.Fill(Func)).
+        if (ApplicationImpl.Instance.IsLegacy)
+        {
+            // TODO: Figure out how to move this out of here and just depend on LayoutNeeded in Mainloop
+            Layout (); // the EventLog in AllViewsTester fails to layout correctly if this is not here (convoluted Dim.Fill(Func)).
+        }
         SetNeedsLayout ();
 
         Initialized?.Invoke (this, EventArgs.Empty);
diff --git a/Terminal.Gui/Views/Menu/MenuBar.cs b/Terminal.Gui/Views/Menu/MenuBar.cs
index 749ec1a5a2..a9b0676dd8 100644
--- a/Terminal.Gui/Views/Menu/MenuBar.cs
+++ b/Terminal.Gui/Views/Menu/MenuBar.cs
@@ -1031,7 +1031,7 @@ internal bool Run (Action? action)
             return false;
         }
 
-        Application.MainLoop!.AddIdle (
+        Application.AddIdle (
                                        () =>
                                        {
                                            action ();
diff --git a/UICatalog/Properties/launchSettings.json b/UICatalog/Properties/launchSettings.json
index 1c37ced48c..0ddda42a4b 100644
--- a/UICatalog/Properties/launchSettings.json
+++ b/UICatalog/Properties/launchSettings.json
@@ -11,6 +11,14 @@
       "commandName": "Project",
       "commandLineArgs": "--driver WindowsDriver"
     },
+    "UICatalog --driver v2win": {
+      "commandName": "Project",
+      "commandLineArgs": "--driver v2win"
+    },
+    "UICatalog --driver v2net": {
+      "commandName": "Project",
+      "commandLineArgs": "--driver v2net"
+    },
     "WSL: UICatalog": {
       "commandName": "Executable",
       "executablePath": "wsl",
diff --git a/UICatalog/Scenario.cs b/UICatalog/Scenario.cs
index 02d9c98df1..c460675c25 100644
--- a/UICatalog/Scenario.cs
+++ b/UICatalog/Scenario.cs
@@ -193,15 +193,19 @@ private void OnApplicationOnInitializedChanged (object? s, EventArgs<bool> a)
 
             Application.Iteration += OnApplicationOnIteration;
             Application.Driver!.ClearedContents += (sender, args) => BenchmarkResults.ClearedContentCount++;
-            Application.Driver!.Refreshed += (sender, args) =>
+
+            if (Application.Driver is ConsoleDriver cd)
             {
-                BenchmarkResults.RefreshedCount++;
+                cd.Refreshed += (sender, args) =>
+                                                 {
+                                                     BenchmarkResults.RefreshedCount++;
+                                                     if (args.CurrentValue)
+                                                     {
+                                                         BenchmarkResults.UpdatedCount++;
+                                                     }
+                                                 };
 
-                if (args.CurrentValue)
-                {
-                    BenchmarkResults.UpdatedCount++;
-                }
-            };
+            }
             Application.NotifyNewRunState += OnApplicationNotifyNewRunState;
 
 
diff --git a/UICatalog/Scenarios/Keys.cs b/UICatalog/Scenarios/Keys.cs
index f2796a344d..b4988a3c93 100644
--- a/UICatalog/Scenarios/Keys.cs
+++ b/UICatalog/Scenarios/Keys.cs
@@ -12,6 +12,7 @@ public override void Main ()
         Application.Init ();
         ObservableCollection<string> keyDownList = [];
         ObservableCollection<string> keyDownNotHandledList = new ();
+        ObservableCollection<string> swallowedList = new ();
 
         var win = new Window { Title = GetQuitKeyAndName () };
 
@@ -137,6 +138,32 @@ public override void Main ()
         onKeyDownNotHandledListView.ColorScheme = Colors.ColorSchemes ["TopLevel"];
         win.Add (onKeyDownNotHandledListView);
 
+
+        // Swallowed
+        label = new Label
+        {
+            X = Pos.Right (onKeyDownNotHandledListView) + 1,
+            Y = Pos.Top (label),
+            Text = "Swallowed:"
+        };
+        win.Add (label);
+
+        var onSwallowedListView = new ListView
+        {
+            X = Pos.Left (label),
+            Y = Pos.Bottom (label),
+            Width = maxKeyString,
+            Height = Dim.Fill (),
+            Source = new ListWrapper<string> (swallowedList)
+        };
+        onSwallowedListView.ColorScheme = Colors.ColorSchemes ["TopLevel"];
+        win.Add (onSwallowedListView);
+
+        if (Application.Driver is IConsoleDriverFacade fac)
+        {
+            fac.InputProcessor.AnsiSequenceSwallowed += (s, e) => { swallowedList.Add (e.Replace ("\x1b","Esc")); };
+        }
+
         Application.KeyDown += (s, a) => KeyDownPressUp (a, "Down");
         Application.KeyUp += (s, a) => KeyDownPressUp (a, "Up");
 
diff --git a/UICatalog/UICatalog.cs b/UICatalog/UICatalog.cs
index b9ce42d927..8b2ca2b684 100644
--- a/UICatalog/UICatalog.cs
+++ b/UICatalog/UICatalog.cs
@@ -16,9 +16,14 @@
 using System.Text;
 using System.Text.Json;
 using System.Text.Json.Serialization;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Serilog;
 using Terminal.Gui;
 using static Terminal.Gui.ConfigurationManager;
 using Command = Terminal.Gui.Command;
+using ILogger = Microsoft.Extensions.Logging.ILogger;
 using RuntimeEnvironment = Microsoft.DotNet.PlatformAbstractions.RuntimeEnvironment;
 
 #nullable enable
@@ -118,6 +123,8 @@ private static void ConfigFileChanged (object sender, FileSystemEventArgs e)
 
     private static int Main (string [] args)
     {
+        Logging.Logger = CreateLogger ();
+
         Console.OutputEncoding = Encoding.Default;
 
         if (Debugger.IsAttached)
@@ -204,6 +211,28 @@ private static int Main (string [] args)
         return 0;
     }
 
+    private static ILogger CreateLogger ()
+    {
+        // Configure Serilog to write logs to a file
+        Log.Logger = new LoggerConfiguration ()
+                     .MinimumLevel.Verbose () // Verbose includes Trace and Debug
+                     .Enrich.FromLogContext () // Enables dynamic enrichment
+                     .WriteTo.File ("logs/logfile.txt", rollingInterval: RollingInterval.Day,
+                                    outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}")
+                     .CreateLogger ();
+
+        // Create a logger factory compatible with Microsoft.Extensions.Logging
+        using var loggerFactory = LoggerFactory.Create (builder =>
+                                                        {
+                                                            builder
+                                                                .AddSerilog (dispose: true) // Integrate Serilog with ILogger
+                                                                .SetMinimumLevel (LogLevel.Trace); // Set minimum log level
+                                                        });
+
+        // Get an ILogger instance
+        return loggerFactory.CreateLogger ("Global Logger");
+    }
+
     private static void OpenUrl (string url)
     {
         if (RuntimeInformation.IsOSPlatform (OSPlatform.Windows))
@@ -896,6 +925,12 @@ public UICatalogTopLevel ()
 
         public void ConfigChanged ()
         {
+            if (MenuBar == null)
+            {
+                // View is probably disposed
+                return;
+            }
+
             if (_topLevelColorScheme == null || !Colors.ColorSchemes.ContainsKey (_topLevelColorScheme))
             {
                 _topLevelColorScheme = "Base";
diff --git a/UICatalog/UICatalog.csproj b/UICatalog/UICatalog.csproj
index fb12478862..c854ef8e68 100644
--- a/UICatalog/UICatalog.csproj
+++ b/UICatalog/UICatalog.csproj
@@ -31,6 +31,9 @@
   <ItemGroup>
     <PackageReference Include="JetBrains.Annotations" Version="[2024.2.0,)" PrivateAssets="all" />
     <PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="[1.21,2)" />
+    <PackageReference Include="Serilog" Version="4.2.0" />
+    <PackageReference Include="Serilog.Extensions.Logging" Version="9.0.0" />
+    <PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
     <PackageReference Include="SixLabors.ImageSharp" Version="[3.1.5,4)" />
     <PackageReference Include="CsvHelper" Version="[33.0.1,34)" />
     <PackageReference Include="Microsoft.DotNet.PlatformAbstractions" Version="[3.1.6,4)" />
diff --git a/UnitTests/Application/MainLoopTests.cs b/UnitTests/Application/MainLoopTests.cs
index 47812926d2..8204142746 100644
--- a/UnitTests/Application/MainLoopTests.cs
+++ b/UnitTests/Application/MainLoopTests.cs
@@ -58,47 +58,47 @@ public void AddIdle_Adds_And_Removes ()
         ml.AddIdle (fnTrue);
         ml.AddIdle (fnFalse);
 
-        Assert.Equal (2, ml.IdleHandlers.Count);
-        Assert.Equal (fnTrue, ml.IdleHandlers [0]);
-        Assert.NotEqual (fnFalse, ml.IdleHandlers [0]);
+        Assert.Equal (2, ml.TimedEvents.IdleHandlers.Count);
+        Assert.Equal (fnTrue, ml.TimedEvents.IdleHandlers [0]);
+        Assert.NotEqual (fnFalse, ml.TimedEvents.IdleHandlers[0]);
 
-        Assert.True (ml.RemoveIdle (fnTrue));
-        Assert.Single (ml.IdleHandlers);
+        Assert.True (ml.TimedEvents.RemoveIdle (fnTrue));
+        Assert.Single (ml.TimedEvents.IdleHandlers);
 
         // BUGBUG: This doesn't throw or indicate an error. Ideally RemoveIdle would either 
         // throw an exception in this case, or return an error.
         // No. Only need to return a boolean.
-        Assert.False (ml.RemoveIdle (fnTrue));
+        Assert.False (ml.TimedEvents.RemoveIdle (fnTrue));
 
-        Assert.True (ml.RemoveIdle (fnFalse));
+        Assert.True (ml.TimedEvents.RemoveIdle (fnFalse));
 
         // BUGBUG: This doesn't throw an exception or indicate an error. Ideally RemoveIdle would either 
         // throw an exception in this case, or return an error.
         // No. Only need to return a boolean.
-        Assert.False (ml.RemoveIdle (fnFalse));
+        Assert.False (ml.TimedEvents.RemoveIdle (fnFalse));
 
         // Add again, but with dupe
         ml.AddIdle (fnTrue);
         ml.AddIdle (fnTrue);
 
-        Assert.Equal (2, ml.IdleHandlers.Count);
-        Assert.Equal (fnTrue, ml.IdleHandlers [0]);
-        Assert.True (ml.IdleHandlers [0] ());
-        Assert.Equal (fnTrue, ml.IdleHandlers [1]);
-        Assert.True (ml.IdleHandlers [1] ());
+        Assert.Equal (2, ml.TimedEvents.IdleHandlers.Count);
+        Assert.Equal (fnTrue, ml.TimedEvents.IdleHandlers[0]);
+        Assert.True (ml.TimedEvents.IdleHandlers[0] ());
+        Assert.Equal (fnTrue, ml.TimedEvents.IdleHandlers[1]);
+        Assert.True (ml.TimedEvents.IdleHandlers[1] ());
 
-        Assert.True (ml.RemoveIdle (fnTrue));
-        Assert.Single (ml.IdleHandlers);
-        Assert.Equal (fnTrue, ml.IdleHandlers [0]);
-        Assert.NotEqual (fnFalse, ml.IdleHandlers [0]);
+        Assert.True (ml.TimedEvents.RemoveIdle (fnTrue));
+        Assert.Single (ml.TimedEvents.IdleHandlers);
+        Assert.Equal (fnTrue, ml.TimedEvents.IdleHandlers[0]);
+        Assert.NotEqual (fnFalse, ml.TimedEvents.IdleHandlers[0]);
 
-        Assert.True (ml.RemoveIdle (fnTrue));
-        Assert.Empty (ml.IdleHandlers);
+        Assert.True (ml.TimedEvents.RemoveIdle (fnTrue));
+        Assert.Empty (ml.TimedEvents.IdleHandlers);
 
         // BUGBUG: This doesn't throw an exception or indicate an error. Ideally RemoveIdle would either 
         // throw an exception in this case, or return an error.
         // No. Only need to return a boolean.
-        Assert.False (ml.RemoveIdle (fnTrue));
+        Assert.False (ml.TimedEvents.RemoveIdle (fnTrue));
     }
 
     [Fact]
@@ -153,9 +153,9 @@ public void AddIdle_Twice_Returns_False_Called_Twice ()
         ml.AddIdle (fn1);
         ml.AddIdle (fn1);
         ml.Run ();
-        Assert.True (ml.RemoveIdle (fnStop));
-        Assert.False (ml.RemoveIdle (fn1));
-        Assert.False (ml.RemoveIdle (fn1));
+        Assert.True (ml.TimedEvents.RemoveIdle (fnStop));
+        Assert.False (ml.TimedEvents.RemoveIdle (fn1));
+        Assert.False (ml.TimedEvents.RemoveIdle (fn1));
 
         Assert.Equal (2, functionCalled);
     }
@@ -178,20 +178,20 @@ public void AddIdleTwice_Function_CalledTwice ()
         ml.AddIdle (fn);
         ml.RunIteration ();
         Assert.Equal (2, functionCalled);
-        Assert.Equal (2, ml.IdleHandlers.Count);
+        Assert.Equal (2, ml.TimedEvents.IdleHandlers.Count);
 
         functionCalled = 0;
-        Assert.True (ml.RemoveIdle (fn));
-        Assert.Single (ml.IdleHandlers);
+        Assert.True (ml.TimedEvents.RemoveIdle (fn));
+        Assert.Single (ml.TimedEvents.IdleHandlers);
         ml.RunIteration ();
         Assert.Equal (1, functionCalled);
 
         functionCalled = 0;
-        Assert.True (ml.RemoveIdle (fn));
-        Assert.Empty (ml.IdleHandlers);
+        Assert.True (ml.TimedEvents.RemoveIdle (fn));
+        Assert.Empty (ml.TimedEvents.IdleHandlers);
         ml.RunIteration ();
         Assert.Equal (0, functionCalled);
-        Assert.False (ml.RemoveIdle (fn));
+        Assert.False (ml.TimedEvents.RemoveIdle (fn));
     }
 
     [Fact]
@@ -209,7 +209,7 @@ public void AddThenRemoveIdle_Function_NotCalled ()
                         };
 
         ml.AddIdle (fn);
-        Assert.True (ml.RemoveIdle (fn));
+        Assert.True (ml.TimedEvents.RemoveIdle (fn));
         ml.RunIteration ();
         Assert.Equal (0, functionCalled);
     }
@@ -230,13 +230,13 @@ public void AddTimer_Adds_Removes_NoFaults ()
                                   return true;
                               };
 
-        object token = ml.AddTimeout (TimeSpan.FromMilliseconds (ms), callback);
+        object token = ml.TimedEvents.AddTimeout (TimeSpan.FromMilliseconds (ms), callback);
 
-        Assert.True (ml.RemoveTimeout (token));
+        Assert.True (ml.TimedEvents.RemoveTimeout (token));
 
         // BUGBUG: This should probably fault?
         // Must return a boolean.
-        Assert.False (ml.RemoveTimeout (token));
+        Assert.False (ml.TimedEvents.RemoveTimeout (token));
     }
 
     [Fact]
@@ -260,8 +260,8 @@ public async Task AddTimer_Duplicate_Keys_Not_Allowed ()
                                   return true;
                               };
 
-        var task1 = new Task (() => token1 = ml.AddTimeout (TimeSpan.FromMilliseconds (ms), callback));
-        var task2 = new Task (() => token2 = ml.AddTimeout (TimeSpan.FromMilliseconds (ms), callback));
+        var task1 = new Task (() => token1 = ml.TimedEvents.AddTimeout (TimeSpan.FromMilliseconds (ms), callback));
+        var task2 = new Task (() => token2 = ml.TimedEvents.AddTimeout (TimeSpan.FromMilliseconds (ms), callback));
         Assert.Null (token1);
         Assert.Null (token2);
         task1.Start ();
@@ -270,8 +270,8 @@ public async Task AddTimer_Duplicate_Keys_Not_Allowed ()
         Assert.NotNull (token1);
         Assert.NotNull (token2);
         await Task.WhenAll (task1, task2);
-        Assert.True (ml.RemoveTimeout (token1));
-        Assert.True (ml.RemoveTimeout (token2));
+        Assert.True (ml.TimedEvents.RemoveTimeout (token1));
+        Assert.True (ml.TimedEvents.RemoveTimeout (token2));
 
         Assert.Equal (2, callbackCount);
     }
@@ -297,15 +297,15 @@ public void AddTimer_EventFired ()
         object sender = null;
         TimeoutEventArgs args = null;
 
-        ml.TimeoutAdded += (s, e) =>
+        ml.TimedEvents.TimeoutAdded += (s, e) =>
                            {
                                sender = s;
                                args = e;
                            };
 
-        object token = ml.AddTimeout (TimeSpan.FromMilliseconds (ms), callback);
+        object token = ml.TimedEvents.AddTimeout (TimeSpan.FromMilliseconds (ms), callback);
 
-        Assert.Same (ml, sender);
+        Assert.Same (ml.TimedEvents, sender);
         Assert.NotNull (args.Timeout);
         Assert.True (args.Ticks - originTicks >= 100 * TimeSpan.TicksPerMillisecond);
     }
@@ -332,14 +332,14 @@ public void AddTimer_In_Parallel_Wont_Throw ()
                               };
 
         Parallel.Invoke (
-                         () => token1 = ml.AddTimeout (TimeSpan.FromMilliseconds (ms), callback),
-                         () => token2 = ml.AddTimeout (TimeSpan.FromMilliseconds (ms), callback)
+                         () => token1 = ml.TimedEvents.AddTimeout (TimeSpan.FromMilliseconds (ms), callback),
+                         () => token2 = ml.TimedEvents.AddTimeout (TimeSpan.FromMilliseconds (ms), callback)
                         );
         ml.Run ();
         Assert.NotNull (token1);
         Assert.NotNull (token2);
-        Assert.True (ml.RemoveTimeout (token1));
-        Assert.True (ml.RemoveTimeout (token2));
+        Assert.True (ml.TimedEvents.RemoveTimeout (token1));
+        Assert.True (ml.TimedEvents.RemoveTimeout (token2));
 
         Assert.Equal (2, callbackCount);
     }
@@ -375,8 +375,8 @@ public void AddTimer_Remove_NotCalled ()
                                   return true;
                               };
 
-        object token = ml.AddTimeout (ms, callback);
-        Assert.True (ml.RemoveTimeout (token));
+        object token = ml.TimedEvents.AddTimeout (ms, callback);
+        Assert.True (ml.TimedEvents.RemoveTimeout (token));
         ml.Run ();
         Assert.Equal (0, callbackCount);
     }
@@ -413,11 +413,11 @@ public void AddTimer_ReturnFalse_StopsBeingCalled ()
                                   return false;
                               };
 
-        object token = ml.AddTimeout (ms, callback);
+        object token = ml.TimedEvents.AddTimeout (ms, callback);
         ml.Run ();
         Assert.Equal (1, callbackCount);
         Assert.Equal (10, stopCount);
-        Assert.False (ml.RemoveTimeout (token));
+        Assert.False (ml.TimedEvents.RemoveTimeout (token));
     }
 
     [Fact]
@@ -436,9 +436,9 @@ public void AddTimer_Run_Called ()
                                   return true;
                               };
 
-        object token = ml.AddTimeout (TimeSpan.FromMilliseconds (ms), callback);
+        object token = ml.TimedEvents.AddTimeout (TimeSpan.FromMilliseconds (ms), callback);
         ml.Run ();
-        Assert.True (ml.RemoveTimeout (token));
+        Assert.True (ml.TimedEvents.RemoveTimeout (token));
 
         Assert.Equal (1, callbackCount);
     }
@@ -461,7 +461,7 @@ public void AddTimer_Run_CalledAtApproximatelyRightTime ()
                                   return true;
                               };
 
-        object token = ml.AddTimeout (ms, callback);
+        object token = ml.TimedEvents.AddTimeout (ms, callback);
         watch.Start ();
         ml.Run ();
 
@@ -469,7 +469,7 @@ public void AddTimer_Run_CalledAtApproximatelyRightTime ()
         // https://github.com/xunit/assert.xunit/pull/25
         Assert.Equal (ms * callbackCount, watch.Elapsed, new MillisecondTolerance (100));
 
-        Assert.True (ml.RemoveTimeout (token));
+        Assert.True (ml.TimedEvents.RemoveTimeout (token));
         Assert.Equal (1, callbackCount);
     }
 
@@ -495,7 +495,7 @@ public void AddTimer_Run_CalledTwiceApproximatelyRightTime ()
                                   return true;
                               };
 
-        object token = ml.AddTimeout (ms, callback);
+        object token = ml.TimedEvents.AddTimeout (ms, callback);
         watch.Start ();
         ml.Run ();
 
@@ -503,7 +503,7 @@ public void AddTimer_Run_CalledTwiceApproximatelyRightTime ()
         // https://github.com/xunit/assert.xunit/pull/25
         Assert.Equal (ms * callbackCount, watch.Elapsed, new MillisecondTolerance (100));
 
-        Assert.True (ml.RemoveTimeout (token));
+        Assert.True (ml.TimedEvents.RemoveTimeout (token));
         Assert.Equal (2, callbackCount);
     }
 
@@ -511,7 +511,7 @@ public void AddTimer_Run_CalledTwiceApproximatelyRightTime ()
     public void CheckTimersAndIdleHandlers_NoTimers_Returns_False ()
     {
         var ml = new MainLoop (new FakeMainLoop ());
-        bool retVal = ml.CheckTimersAndIdleHandlers (out int waitTimeOut);
+        bool retVal = ml.TimedEvents.CheckTimersAndIdleHandlers (out int waitTimeOut);
         Assert.False (retVal);
         Assert.Equal (-1, waitTimeOut);
     }
@@ -523,7 +523,7 @@ public void CheckTimersAndIdleHandlers_NoTimers_WithIdle_Returns_True ()
         Func<bool> fnTrue = () => true;
 
         ml.AddIdle (fnTrue);
-        bool retVal = ml.CheckTimersAndIdleHandlers (out int waitTimeOut);
+        bool retVal = ml.TimedEvents.CheckTimersAndIdleHandlers (out int waitTimeOut);
         Assert.True (retVal);
         Assert.Equal (-1, waitTimeOut);
     }
@@ -536,8 +536,8 @@ public void CheckTimersAndIdleHandlers_With1Timer_Returns_Timer ()
 
         static bool Callback () { return false; }
 
-        _ = ml.AddTimeout (ms, Callback);
-        bool retVal = ml.CheckTimersAndIdleHandlers (out int waitTimeOut);
+        _ = ml.TimedEvents.AddTimeout (ms, Callback);
+        bool retVal = ml.TimedEvents.CheckTimersAndIdleHandlers (out int waitTimeOut);
 
         Assert.True (retVal);
 
@@ -553,9 +553,9 @@ public void CheckTimersAndIdleHandlers_With2Timers_Returns_Timer ()
 
         static bool Callback () { return false; }
 
-        _ = ml.AddTimeout (ms, Callback);
-        _ = ml.AddTimeout (ms, Callback);
-        bool retVal = ml.CheckTimersAndIdleHandlers (out int waitTimeOut);
+        _ = ml.TimedEvents.AddTimeout (ms, Callback);
+        _ = ml.TimedEvents.AddTimeout (ms, Callback);
+        bool retVal = ml.TimedEvents.CheckTimersAndIdleHandlers (out int waitTimeOut);
 
         Assert.True (retVal);
 
@@ -600,8 +600,8 @@ public void False_Idle_Stops_It_Being_Called_Again ()
         ml.AddIdle (fnStop);
         ml.AddIdle (fn1);
         ml.Run ();
-        Assert.True (ml.RemoveIdle (fnStop));
-        Assert.False (ml.RemoveIdle (fn1));
+        Assert.True (ml.TimedEvents.RemoveIdle (fnStop));
+        Assert.False (ml.TimedEvents.RemoveIdle (fn1));
 
         Assert.Equal (10, functionCalled);
         Assert.Equal (20, stopCount);
@@ -612,8 +612,8 @@ public void Internal_Tests ()
     {
         var testMainloop = new TestMainloop ();
         var mainloop = new MainLoop (testMainloop);
-        Assert.Empty (mainloop._timeouts);
-        Assert.Empty (mainloop._idleHandlers);
+        Assert.Empty (mainloop.TimedEvents.Timeouts);
+        Assert.Empty (mainloop.TimedEvents.IdleHandlers);
 
         Assert.NotNull (
                         new Timeout { Span = new TimeSpan (), Callback = () => true }
@@ -748,7 +748,7 @@ public void RemoveIdle_Function_NotCalled ()
                             return true;
                         };
 
-        Assert.False (ml.RemoveIdle (fn));
+        Assert.False (ml.TimedEvents.RemoveIdle (fn));
         ml.RunIteration ();
         Assert.Equal (0, functionCalled);
     }
@@ -774,7 +774,7 @@ public void Run_Runs_Idle_Stop_Stops_Idle ()
 
         ml.AddIdle (fn);
         ml.Run ();
-        Assert.True (ml.RemoveIdle (fn));
+        Assert.True (ml.TimedEvents.RemoveIdle (fn));
 
         Assert.Equal (10, functionCalled);
     }
diff --git a/UnitTests/ConsoleDrivers/AnsiKeyboardParserTests.cs b/UnitTests/ConsoleDrivers/AnsiKeyboardParserTests.cs
new file mode 100644
index 0000000000..1554ac65e3
--- /dev/null
+++ b/UnitTests/ConsoleDrivers/AnsiKeyboardParserTests.cs
@@ -0,0 +1,73 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace UnitTests.ConsoleDrivers;
+public class AnsiKeyboardParserTests
+{
+    private readonly AnsiKeyboardParser _parser = new ();
+
+    public static IEnumerable<object []> GetKeyboardTestData ()
+    {
+        // Test data for various ANSI escape sequences and their expected Key values
+        yield return new object [] { "\u001b[A", Key.CursorUp };
+        yield return new object [] { "\u001b[B", Key.CursorDown };
+        yield return new object [] { "\u001b[C", Key.CursorRight };
+        yield return new object [] { "\u001b[D", Key.CursorLeft };
+
+        // Valid inputs with modifiers
+        yield return new object [] { "\u001b[1;2A", Key.CursorUp.WithShift };
+        yield return new object [] { "\u001b[1;3A", Key.CursorUp.WithAlt };
+        yield return new object [] { "\u001b[1;4A", Key.CursorUp.WithAlt.WithShift };
+        yield return new object [] { "\u001b[1;5A", Key.CursorUp.WithCtrl };
+        yield return new object [] { "\u001b[1;6A", Key.CursorUp.WithCtrl.WithShift };
+        yield return new object [] { "\u001b[1;7A", Key.CursorUp.WithCtrl.WithAlt };
+        yield return new object [] { "\u001b[1;8A", Key.CursorUp.WithCtrl.WithAlt.WithShift };
+
+        yield return new object [] { "\u001b[1;2B", Key.CursorDown.WithShift };
+        yield return new object [] { "\u001b[1;3B", Key.CursorDown.WithAlt };
+        yield return new object [] { "\u001b[1;4B", Key.CursorDown.WithAlt.WithShift };
+        yield return new object [] { "\u001b[1;5B", Key.CursorDown.WithCtrl };
+        yield return new object [] { "\u001b[1;6B", Key.CursorDown.WithCtrl.WithShift };
+        yield return new object [] { "\u001b[1;7B", Key.CursorDown.WithCtrl.WithAlt };
+        yield return new object [] { "\u001b[1;8B", Key.CursorDown.WithCtrl.WithAlt.WithShift };
+
+        yield return new object [] { "\u001b[1;2C", Key.CursorRight.WithShift };
+        yield return new object [] { "\u001b[1;3C", Key.CursorRight.WithAlt };
+        yield return new object [] { "\u001b[1;4C", Key.CursorRight.WithAlt.WithShift };
+        yield return new object [] { "\u001b[1;5C", Key.CursorRight.WithCtrl };
+        yield return new object [] { "\u001b[1;6C", Key.CursorRight.WithCtrl.WithShift };
+        yield return new object [] { "\u001b[1;7C", Key.CursorRight.WithCtrl.WithAlt };
+        yield return new object [] { "\u001b[1;8C", Key.CursorRight.WithCtrl.WithAlt.WithShift };
+
+        yield return new object [] { "\u001b[1;2D", Key.CursorLeft.WithShift };
+        yield return new object [] { "\u001b[1;3D", Key.CursorLeft.WithAlt };
+        yield return new object [] { "\u001b[1;4D", Key.CursorLeft.WithAlt.WithShift };
+        yield return new object [] { "\u001b[1;5D", Key.CursorLeft.WithCtrl };
+        yield return new object [] { "\u001b[1;6D", Key.CursorLeft.WithCtrl.WithShift };
+        yield return new object [] { "\u001b[1;7D", Key.CursorLeft.WithCtrl.WithAlt };
+        yield return new object [] { "\u001b[1;8D", Key.CursorLeft.WithCtrl.WithAlt.WithShift };
+
+
+        // Invalid inputs
+        yield return new object [] { "\u001b[Z", null };
+        yield return new object [] { "\u001b[invalid", null };
+        yield return new object [] { "\u001b[1", null };
+        yield return new object [] { "\u001b[AB", null };
+        yield return new object [] { "\u001b[;A", null };
+    }
+
+    // Consolidated test for all keyboard events (e.g., arrow keys)
+    [Theory]
+    [MemberData (nameof (GetKeyboardTestData))]
+    public void ProcessKeyboardInput_ReturnsCorrectKey (string input, Key? expectedKey)
+    {
+        // Act
+        Key? result = _parser.IsKeyboard (input)?.GetKey (input);
+
+        // Assert
+        Assert.Equal (expectedKey, result); // Verify the returned key matches the expected one
+    }
+}
diff --git a/UnitTests/ConsoleDrivers/AnsiMouseParserTests.cs b/UnitTests/ConsoleDrivers/AnsiMouseParserTests.cs
new file mode 100644
index 0000000000..c7bb046210
--- /dev/null
+++ b/UnitTests/ConsoleDrivers/AnsiMouseParserTests.cs
@@ -0,0 +1,42 @@
+namespace UnitTests.ConsoleDrivers;
+
+public class AnsiMouseParserTests
+{
+    private readonly AnsiMouseParser _parser;
+
+    public AnsiMouseParserTests () { _parser = new (); }
+
+    // Consolidated test for all mouse events: button press/release, wheel scroll, position, modifiers
+    [Theory]
+    [InlineData ("\u001b[<0;100;200M", 99, 199, MouseFlags.Button1Pressed)] // Button 1 Pressed
+    [InlineData ("\u001b[<0;150;250m", 149, 249, MouseFlags.Button1Released)] // Button 1 Released
+    [InlineData ("\u001b[<1;120;220M", 119, 219, MouseFlags.Button2Pressed)] // Button 2 Pressed
+    [InlineData ("\u001b[<1;180;280m", 179, 279, MouseFlags.Button2Released)] // Button 2 Released
+    [InlineData ("\u001b[<2;200;300M", 199, 299, MouseFlags.Button3Pressed)] // Button 3 Pressed
+    [InlineData ("\u001b[<2;250;350m", 249, 349, MouseFlags.Button3Released)] // Button 3 Released
+    [InlineData ("\u001b[<64;100;200M", 99, 199, MouseFlags.WheeledUp)] // Wheel Scroll Up
+    [InlineData ("\u001b[<65;150;250m", 149, 249, MouseFlags.WheeledDown)] // Wheel Scroll Down
+    [InlineData ("\u001b[<39;100;200m", 99, 199, MouseFlags.ButtonShift | MouseFlags.ReportMousePosition)] // Mouse Position (No Button)
+    [InlineData ("\u001b[<43;120;240m", 119, 239, MouseFlags.ButtonAlt | MouseFlags.ReportMousePosition)] // Mouse Position (No Button)
+    [InlineData ("\u001b[<8;100;200M", 99, 199, MouseFlags.Button1Pressed | MouseFlags.ButtonAlt)] // Button 1 Pressed + Alt
+    [InlineData ("\u001b[<invalid;100;200M", 0, 0, MouseFlags.None)] // Invalid Input (Expecting null)
+    [InlineData ("\u001b[<100;200;300Z", 0, 0, MouseFlags.None)] // Invalid Input (Expecting null)
+    [InlineData ("\u001b[<invalidInput>", 0, 0, MouseFlags.None)] // Invalid Input (Expecting null)
+    public void ProcessMouseInput_ReturnsCorrectFlags (string input, int expectedX, int expectedY, MouseFlags expectedFlags)
+    {
+        // Act
+        MouseEventArgs result = _parser.ProcessMouseInput (input);
+
+        // Assert
+        if (expectedFlags == MouseFlags.None)
+        {
+            Assert.Null (result); // Expect null for invalid inputs
+        }
+        else
+        {
+            Assert.NotNull (result); // Expect non-null result for valid inputs
+            Assert.Equal (new (expectedX, expectedY), result!.Position); // Verify position
+            Assert.Equal (expectedFlags, result.Flags); // Verify flags
+        }
+    }
+}
diff --git a/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs b/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs
index fbc87761f5..fcc2b6127d 100644
--- a/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs
+++ b/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs
@@ -153,7 +153,7 @@ public static IEnumerable<object []> TestInputSequencesExact_Cases ()
             null,
             new []
             {
-                new StepExpectation ('\u001b',AnsiResponseParserState.ExpectingBracket,string.Empty)
+                new StepExpectation ('\u001b',AnsiResponseParserState.ExpectingEscapeSequence,string.Empty)
             }
         ];
 
@@ -163,13 +163,13 @@ public static IEnumerable<object []> TestInputSequencesExact_Cases ()
             'c',
             new []
             {
-                new StepExpectation ('\u001b',AnsiResponseParserState.ExpectingBracket,string.Empty),
-                new StepExpectation ('H',AnsiResponseParserState.Normal,"\u001bH"), // H is known terminator and not expected one so here we release both chars
-                new StepExpectation ('\u001b',AnsiResponseParserState.ExpectingBracket,string.Empty),
+                new StepExpectation ('\u001b',AnsiResponseParserState.ExpectingEscapeSequence,string.Empty),
+                new StepExpectation ('H',AnsiResponseParserState.InResponse,string.Empty), // H is known terminator and not expected one so here we release both chars
+                new StepExpectation ('\u001b',AnsiResponseParserState.ExpectingEscapeSequence,"\u001bH"),
                 new StepExpectation ('[',AnsiResponseParserState.InResponse,string.Empty),
                 new StepExpectation ('0',AnsiResponseParserState.InResponse,string.Empty),
                 new StepExpectation ('c',AnsiResponseParserState.Normal,string.Empty,"\u001b[0c"), // c is expected terminator so here we swallow input and populate expected response
-                new StepExpectation ('\u001b',AnsiResponseParserState.ExpectingBracket,string.Empty),
+                new StepExpectation ('\u001b',AnsiResponseParserState.ExpectingEscapeSequence,string.Empty),
             }
         ];
     }
@@ -227,8 +227,10 @@ public void TestInputSequencesExact (string caseName, char? terminator, IEnumera
         {
             parser.ExpectResponse (terminator.Value.ToString (),(s)=> response = s,null, false);
         }
+        int step= 0;
         foreach (var state in expectedStates)
         {
+            step++;
             // If we expect the response to be detected at this step
             if (!string.IsNullOrWhiteSpace (state.ExpectedAnsiResponse))
             {
@@ -247,6 +249,8 @@ public void TestInputSequencesExact (string caseName, char? terminator, IEnumera
                 // And after passing input it shuld be the expected value
                 Assert.Equal (state.ExpectedAnsiResponse, response);
             }
+
+            output.WriteLine ($"Step {step} passed");
         }
     }
 
@@ -260,8 +264,8 @@ public void ReleasesEscapeAfterTimeout ()
         AssertConsumed (input,ref i);
 
         // We should know when the state changed
-        Assert.Equal (AnsiResponseParserState.ExpectingBracket, _parser1.State);
-        Assert.Equal (AnsiResponseParserState.ExpectingBracket, _parser2.State);
+        Assert.Equal (AnsiResponseParserState.ExpectingEscapeSequence, _parser1.State);
+        Assert.Equal (AnsiResponseParserState.ExpectingEscapeSequence, _parser2.State);
 
         Assert.Equal (DateTime.Now.Date, _parser1.StateChangedAt.Date);
         Assert.Equal (DateTime.Now.Date, _parser2.StateChangedAt.Date);
@@ -289,35 +293,6 @@ public void TwoExcapesInARow ()
         AssertManualReleaseIs ("\u001b",1);
     }
 
-    [Fact]
-    public void TwoExcapesInARowWithTextBetween ()
-    {
-        // Example user presses Esc key and types at the speed of light (normally the consumer should be handling Esc timeout)
-        // then a DAR comes in.
-        string input = "\u001bfish\u001b";
-        int i = 0;
-
-        // First Esc gets grabbed
-        AssertConsumed (input, ref i); // Esc
-        Assert.Equal (AnsiResponseParserState.ExpectingBracket,_parser1.State);
-        Assert.Equal (AnsiResponseParserState.ExpectingBracket, _parser2.State);
-
-        // Because next char is 'f' we do not see a bracket so release both
-        AssertReleased (input, ref i, "\u001bf", 0,1); // f
-
-        Assert.Equal (AnsiResponseParserState.Normal, _parser1.State);
-        Assert.Equal (AnsiResponseParserState.Normal, _parser2.State);
-
-        AssertReleased (input, ref i,"i",2);
-        AssertReleased (input, ref i, "s", 3);
-        AssertReleased (input, ref i, "h", 4);
-
-        AssertConsumed (input, ref i); // Second Esc
-
-        // Assume 50ms or something has passed, lets force release as no new content
-        AssertManualReleaseIs ("\u001b", 5);
-    }
-
     [Fact]
     public void TestLateResponses ()
     {
@@ -474,6 +449,191 @@ public void UnknownResponses_ParameterShouldMatch ()
         Assert.Equal (expectedUnknownResponses, unknownResponses);
     }
 
+    [Fact]
+    public void ParserDetectsMouse ()
+    {
+        // ANSI escape sequence for mouse down (using a generic format example)
+        const string MOUSE_DOWN = "\u001B[<0;12;32M";
+
+        // ANSI escape sequence for Device Attribute Response (e.g., Terminal identifying itself)
+        const string DEVICE_ATTRIBUTE_RESPONSE = "\u001B[?1;2c";
+
+        // ANSI escape sequence for mouse up (using a generic format example)
+        const string MOUSE_UP = "\u001B[<0;25;50m";
+
+        var parser = new AnsiResponseParser ();
+
+        parser.HandleMouse = true;
+        string? foundDar = null;
+        List<MouseEventArgs> mouseEventArgs = new ();
+
+        parser.Mouse += (s, e) => mouseEventArgs.Add (e);
+        parser.ExpectResponse ("c", (dar) => foundDar = dar, null, false);
+        var released = parser.ProcessInput ("a" + MOUSE_DOWN + "asdf" + DEVICE_ATTRIBUTE_RESPONSE + "bbcc" + MOUSE_UP + "sss");
+
+        Assert.Equal ("aasdfbbccsss", released);
+
+        Assert.Equal (2, mouseEventArgs.Count);
+
+        Assert.NotNull (foundDar);
+        Assert.Equal (DEVICE_ATTRIBUTE_RESPONSE,foundDar);
+
+        Assert.True (mouseEventArgs [0].IsPressed);
+        // Mouse positions in ANSI are 1 based so actual Terminal.Gui Screen positions are x-1,y-1
+        Assert.Equal (11,mouseEventArgs [0].Position.X);
+        Assert.Equal (31, mouseEventArgs [0].Position.Y);
+
+        Assert.True (mouseEventArgs [1].IsReleased);
+        Assert.Equal (24, mouseEventArgs [1].Position.X);
+        Assert.Equal (49, mouseEventArgs [1].Position.Y);
+    }
+
+
+    [Fact]
+    public void ParserDetectsKeyboard ()
+    {
+
+        // ANSI escape sequence for cursor left
+        const string LEFT = "\u001b[D";
+
+        // ANSI escape sequence for Device Attribute Response (e.g., Terminal identifying itself)
+        const string DEVICE_ATTRIBUTE_RESPONSE = "\u001B[?1;2c";
+
+        // ANSI escape sequence for cursor up (while shift held down)
+        const string SHIFT_UP = "\u001b[1;2A";
+
+        var parser = new AnsiResponseParser ();
+
+        parser.HandleKeyboard = true;
+        string? foundDar = null;
+        List<Key> keys = new ();
+
+        parser.Keyboard += (s, e) => keys.Add (e);
+        parser.ExpectResponse ("c", (dar) => foundDar = dar, null, false);
+        var released = parser.ProcessInput ("a" + LEFT + "asdf" + DEVICE_ATTRIBUTE_RESPONSE + "bbcc" + SHIFT_UP + "sss");
+
+        Assert.Equal ("aasdfbbccsss", released);
+
+        Assert.Equal (2, keys.Count);
+
+        Assert.NotNull (foundDar);
+        Assert.Equal (DEVICE_ATTRIBUTE_RESPONSE, foundDar);
+
+        Assert.Equal (Key.CursorLeft,keys [0]);
+        Assert.Equal (Key.CursorUp.WithShift, keys [1]);
+    }
+
+    public static IEnumerable<object []> ParserDetects_FunctionKeys_Cases ()
+    {
+        // These are VT100 escape codes for F1-4
+        yield return
+        [
+            "\u001bOP",
+            Key.F1
+        ];
+
+        yield return
+        [
+            "\u001bOQ",
+            Key.F2
+        ];
+
+        yield return
+        [
+            "\u001bOR",
+            Key.F3
+        ];
+
+        yield return
+        [
+            "\u001bOS",
+            Key.F4
+        ];
+
+
+        // These are also F keys
+        yield return [
+                         "\u001b[11~",
+                         Key.F1
+                     ];
+
+        yield return [
+                         "\u001b[12~",
+                         Key.F2
+                     ];
+
+        yield return [
+                         "\u001b[13~",
+                         Key.F3
+                     ];
+
+        yield return [
+                         "\u001b[14~",
+                         Key.F4
+                     ];
+
+        yield return [
+                         "\u001b[15~",
+                         Key.F5
+                     ];
+
+        yield return [
+                         "\u001b[17~",
+                         Key.F6
+                     ];
+
+        yield return [
+                         "\u001b[18~",
+                         Key.F7
+                     ];
+
+        yield return [
+                         "\u001b[19~",
+                         Key.F8
+                     ];
+
+        yield return [
+                         "\u001b[20~",
+                         Key.F9
+                     ];
+
+        yield return [
+                         "\u001b[21~",
+                         Key.F10
+                     ];
+
+        yield return [
+                         "\u001b[23~",
+                         Key.F11
+                     ];
+
+        yield return [
+                         "\u001b[24~",
+                         Key.F12
+                     ];
+    }
+
+    [MemberData (nameof (ParserDetects_FunctionKeys_Cases))]
+
+    [Theory]
+    public void ParserDetects_FunctionKeys (string input, Key expectedKey)
+    {
+        var parser = new AnsiResponseParser ();
+
+        parser.HandleKeyboard = true;
+        List<Key> keys = new ();
+
+        parser.Keyboard += (s, e) => keys.Add (e);
+
+        foreach (var ch in input.ToCharArray ())
+        {
+            parser.ProcessInput (new (ch,1));
+        }
+        var k = Assert.Single (keys);
+
+        Assert.Equal (k,expectedKey);
+    }
+
     private Tuple<char, int> [] StringToBatch (string batch)
     {
         return batch.Select ((k) => Tuple.Create (k, tIndex++)).ToArray ();
diff --git a/UnitTests/ConsoleDrivers/ConsoleDriverTests.cs b/UnitTests/ConsoleDrivers/ConsoleDriverTests.cs
index d75a56639d..cd957af483 100644
--- a/UnitTests/ConsoleDrivers/ConsoleDriverTests.cs
+++ b/UnitTests/ConsoleDrivers/ConsoleDriverTests.cs
@@ -221,7 +221,8 @@ public void TerminalResized_Simulation (Type driverType)
 
         driver.Cols = 120;
         driver.Rows = 40;
-        driver.OnSizeChanged (new SizeChangedEventArgs (new (driver.Cols, driver.Rows)));
+
+        ((ConsoleDriver)driver).OnSizeChanged (new SizeChangedEventArgs (new (driver.Cols, driver.Rows)));
         Assert.Equal (120, driver.Cols);
         Assert.Equal (40, driver.Rows);
         Assert.True (wasTerminalResized);
diff --git a/UnitTests/ConsoleDrivers/MainLoopDriverTests.cs b/UnitTests/ConsoleDrivers/MainLoopDriverTests.cs
index 9720d51b5e..228d9e5726 100644
--- a/UnitTests/ConsoleDrivers/MainLoopDriverTests.cs
+++ b/UnitTests/ConsoleDrivers/MainLoopDriverTests.cs
@@ -52,7 +52,7 @@ public void MainLoop_AddTimeout_ValidParameters_ReturnsToken (Type driverType, T
         var mainLoop = new MainLoop (mainLoopDriver);
         var callbackInvoked = false;
 
-        object token = mainLoop.AddTimeout (
+        object token = mainLoop.TimedEvents.AddTimeout (
                                             TimeSpan.FromMilliseconds (100),
                                             () =>
                                             {
@@ -88,7 +88,7 @@ Type mainLoopDriverType
         var mainLoop = new MainLoop (mainLoopDriver);
 
         mainLoop.AddIdle (() => false);
-        bool result = mainLoop.CheckTimersAndIdleHandlers (out int waitTimeout);
+        bool result = mainLoop.TimedEvents.CheckTimersAndIdleHandlers (out int waitTimeout);
 
         Assert.True (result);
         Assert.Equal (-1, waitTimeout);
@@ -111,7 +111,7 @@ Type mainLoopDriverType
         var mainLoopDriver = (IMainLoopDriver)Activator.CreateInstance (mainLoopDriverType, driver);
         var mainLoop = new MainLoop (mainLoopDriver);
 
-        bool result = mainLoop.CheckTimersAndIdleHandlers (out int waitTimeout);
+        bool result = mainLoop.TimedEvents.CheckTimersAndIdleHandlers (out int waitTimeout);
 
         Assert.False (result);
         Assert.Equal (-1, waitTimeout);
@@ -134,8 +134,8 @@ Type mainLoopDriverType
         var mainLoopDriver = (IMainLoopDriver)Activator.CreateInstance (mainLoopDriverType, driver);
         var mainLoop = new MainLoop (mainLoopDriver);
 
-        mainLoop.AddTimeout (TimeSpan.FromMilliseconds (100), () => false);
-        bool result = mainLoop.CheckTimersAndIdleHandlers (out int waitTimeout);
+        mainLoop.TimedEvents.AddTimeout (TimeSpan.FromMilliseconds (100), () => false);
+        bool result = mainLoop.TimedEvents.CheckTimersAndIdleHandlers (out int waitTimeout);
 
         Assert.True (result);
         Assert.True (waitTimeout >= 0);
@@ -158,8 +158,8 @@ public void MainLoop_Constructs_Disposes (Type driverType, Type mainLoopDriverTy
         // Check default values
         Assert.NotNull (mainLoop);
         Assert.Equal (mainLoopDriver, mainLoop.MainLoopDriver);
-        Assert.Empty (mainLoop.IdleHandlers);
-        Assert.Empty (mainLoop.Timeouts);
+        Assert.Empty (mainLoop.TimedEvents.IdleHandlers);
+        Assert.Empty (mainLoop.TimedEvents.Timeouts);
         Assert.False (mainLoop.Running);
 
         // Clean up
@@ -168,8 +168,8 @@ public void MainLoop_Constructs_Disposes (Type driverType, Type mainLoopDriverTy
         // TODO: It'd be nice if we could really verify IMainLoopDriver.TearDown was called
         // and that it was actually cleaned up.
         Assert.Null (mainLoop.MainLoopDriver);
-        Assert.Empty (mainLoop.IdleHandlers);
-        Assert.Empty (mainLoop.Timeouts);
+        Assert.Empty (mainLoop.TimedEvents.IdleHandlers);
+        Assert.Empty (mainLoop.TimedEvents.Timeouts);
         Assert.False (mainLoop.Running);
     }
 
@@ -186,7 +186,7 @@ public void MainLoop_RemoveIdle_InvalidToken_ReturnsFalse (Type driverType, Type
         var mainLoopDriver = (IMainLoopDriver)Activator.CreateInstance (mainLoopDriverType, driver);
         var mainLoop = new MainLoop (mainLoopDriver);
 
-        bool result = mainLoop.RemoveIdle (() => false);
+        bool result = mainLoop.TimedEvents.RemoveIdle (() => false);
 
         Assert.False (result);
         mainLoop.Dispose ();
@@ -208,7 +208,7 @@ public void MainLoop_RemoveIdle_ValidToken_ReturnsTrue (Type driverType, Type ma
         bool IdleHandler () { return false; }
 
         Func<bool> token = mainLoop.AddIdle (IdleHandler);
-        bool result = mainLoop.RemoveIdle (token);
+        bool result = mainLoop.TimedEvents.RemoveIdle (token);
 
         Assert.True (result);
         mainLoop.Dispose ();
@@ -227,7 +227,7 @@ public void MainLoop_RemoveTimeout_InvalidToken_ReturnsFalse (Type driverType, T
         var mainLoopDriver = (IMainLoopDriver)Activator.CreateInstance (mainLoopDriverType, driver);
         var mainLoop = new MainLoop (mainLoopDriver);
 
-        bool result = mainLoop.RemoveTimeout (new object ());
+        bool result = mainLoop.TimedEvents.RemoveTimeout (new object ());
 
         Assert.False (result);
     }
@@ -245,8 +245,8 @@ public void MainLoop_RemoveTimeout_ValidToken_ReturnsTrue (Type driverType, Type
         var mainLoopDriver = (IMainLoopDriver)Activator.CreateInstance (mainLoopDriverType, driver);
         var mainLoop = new MainLoop (mainLoopDriver);
 
-        object token = mainLoop.AddTimeout (TimeSpan.FromMilliseconds (100), () => false);
-        bool result = mainLoop.RemoveTimeout (token);
+        object token = mainLoop.TimedEvents.AddTimeout (TimeSpan.FromMilliseconds (100), () => false);
+        bool result = mainLoop.TimedEvents.RemoveTimeout (token);
 
         Assert.True (result);
         mainLoop.Dispose ();
diff --git a/UnitTests/ConsoleDrivers/V2/ApplicationV2Tests.cs b/UnitTests/ConsoleDrivers/V2/ApplicationV2Tests.cs
new file mode 100644
index 0000000000..1afd367847
--- /dev/null
+++ b/UnitTests/ConsoleDrivers/V2/ApplicationV2Tests.cs
@@ -0,0 +1,287 @@
+using System.Collections.Concurrent;
+using Microsoft.Extensions.Logging;
+using Moq;
+
+namespace UnitTests.ConsoleDrivers.V2;
+public class ApplicationV2Tests
+{
+
+    private ApplicationV2 NewApplicationV2 ()
+    {
+        var netInput = new Mock<INetInput> ();
+        SetupRunInputMockMethodToBlock (netInput);
+        var winInput = new Mock<IWindowsInput> ();
+        SetupRunInputMockMethodToBlock (winInput);
+
+        return new (
+                    ()=>netInput.Object,
+                    Mock.Of<IConsoleOutput>,
+                    () => winInput.Object,
+                    Mock.Of<IConsoleOutput>);
+    }
+
+    [Fact]
+    public void TestInit_CreatesKeybindings ()
+    {
+        var v2 = NewApplicationV2();
+
+        Application.KeyBindings.Clear();
+
+        Assert.Empty(Application.KeyBindings.GetBindings ());
+
+        v2.Init ();
+
+        Assert.NotEmpty (Application.KeyBindings.GetBindings ());
+
+        v2.Shutdown ();
+    }
+
+    [Fact]
+    public void TestInit_DriverIsFacade ()
+    {
+        var v2 = NewApplicationV2();
+
+        Assert.Null (Application.Driver);
+        v2.Init ();
+        Assert.NotNull (Application.Driver);
+
+        var type = Application.Driver.GetType ();
+        Assert.True(type.IsGenericType); 
+        Assert.True (type.GetGenericTypeDefinition () == typeof (ConsoleDriverFacade<>));
+        v2.Shutdown ();
+
+        Assert.Null (Application.Driver);
+    }
+
+    [Fact]
+    public void TestInit_ExplicitlyRequestWin ()
+    {
+        var netInput = new Mock<INetInput> (MockBehavior.Strict);
+        var netOutput = new Mock<IConsoleOutput> (MockBehavior.Strict);
+        var winInput = new Mock<IWindowsInput> (MockBehavior.Strict);
+        var winOutput = new Mock<IConsoleOutput> (MockBehavior.Strict);
+
+        winInput.Setup (i => i.Initialize (It.IsAny<ConcurrentQueue<WindowsConsole.InputRecord>> ()))
+                .Verifiable(Times.Once);
+        SetupRunInputMockMethodToBlock (winInput);
+        winInput.Setup (i=>i.Dispose ())
+                .Verifiable(Times.Once);
+        winOutput.Setup (i => i.Dispose ())
+                 .Verifiable (Times.Once);
+
+        var v2 = new ApplicationV2 (
+                                    ()=> netInput.Object,
+                                    () => netOutput.Object,
+                                    () => winInput.Object,
+                                    () => winOutput.Object);
+
+        Assert.Null (Application.Driver);
+        v2.Init (null,"v2win");
+        Assert.NotNull (Application.Driver);
+
+        var type = Application.Driver.GetType ();
+        Assert.True (type.IsGenericType);
+        Assert.True (type.GetGenericTypeDefinition () == typeof (ConsoleDriverFacade<>));
+        v2.Shutdown ();
+
+        Assert.Null (Application.Driver);
+
+        winInput.VerifyAll();
+    }
+
+    [Fact]
+    public void TestInit_ExplicitlyRequestNet ()
+    {
+        var netInput = new Mock<INetInput> (MockBehavior.Strict);
+        var netOutput = new Mock<IConsoleOutput> (MockBehavior.Strict);
+        var winInput = new Mock<IWindowsInput> (MockBehavior.Strict);
+        var winOutput = new Mock<IConsoleOutput> (MockBehavior.Strict);
+
+        netInput.Setup (i => i.Initialize (It.IsAny<ConcurrentQueue<ConsoleKeyInfo>> ()))
+                .Verifiable (Times.Once);
+        SetupRunInputMockMethodToBlock (netInput);
+        netInput.Setup (i => i.Dispose ())
+                .Verifiable (Times.Once);
+        netOutput.Setup (i => i.Dispose ())
+                 .Verifiable (Times.Once);
+        var v2 = new ApplicationV2 (
+                                    () => netInput.Object,
+                                    () => netOutput.Object,
+                                    () => winInput.Object,
+                                    () => winOutput.Object);
+
+        Assert.Null (Application.Driver);
+        v2.Init (null, "v2net");
+        Assert.NotNull (Application.Driver);
+
+        var type = Application.Driver.GetType ();
+        Assert.True (type.IsGenericType);
+        Assert.True (type.GetGenericTypeDefinition () == typeof (ConsoleDriverFacade<>));
+        v2.Shutdown ();
+
+        Assert.Null (Application.Driver);
+
+        netInput.VerifyAll ();
+    }
+
+    private void SetupRunInputMockMethodToBlock (Mock<IWindowsInput> winInput)
+    {
+        winInput.Setup (r => r.Run (It.IsAny<CancellationToken> ()))
+                .Callback<CancellationToken> (token =>
+                                              {
+                                                  // Simulate an infinite loop that checks for cancellation
+                                                  while (!token.IsCancellationRequested)
+                                                  {
+                                                      // Perform the action that should repeat in the loop
+                                                      // This could be some mock behavior or just an empty loop depending on the context
+                                                  }
+                                              })
+                .Verifiable (Times.Once);
+    }
+    private void SetupRunInputMockMethodToBlock (Mock<INetInput> netInput)
+    {
+        netInput.Setup (r => r.Run (It.IsAny<CancellationToken> ()))
+                .Callback<CancellationToken> (token =>
+                                              {
+                                                  // Simulate an infinite loop that checks for cancellation
+                                                  while (!token.IsCancellationRequested)
+                                                  {
+                                                      // Perform the action that should repeat in the loop
+                                                      // This could be some mock behavior or just an empty loop depending on the context
+                                                  }
+                                              })
+                .Verifiable (Times.Once);
+    }
+
+    [Fact]
+    public void Test_NoInitThrowOnRun ()
+    {
+        var app = NewApplicationV2();
+
+        var ex = Assert.Throws<NotInitializedException> (() => app.Run (new Window ()));
+        Assert.Equal ("Run cannot be accessed before Initialization", ex.Message);
+    }
+
+    [Fact]
+    public void Test_InitRunShutdown ()
+    {
+        var orig = ApplicationImpl.Instance;
+
+        var v2 = NewApplicationV2();
+        ApplicationImpl.ChangeInstance (v2);
+
+        v2.Init ();
+
+        var timeoutToken = v2.AddTimeout (TimeSpan.FromMilliseconds (150),
+                       () =>
+                       {
+                           if (Application.Top != null)
+                           {
+                               Application.RequestStop ();
+                               return true;
+                           }
+
+                           return true;
+                       }
+                       );
+        Assert.Null (Application.Top);
+
+        // Blocks until the timeout call is hit
+
+        v2.Run (new Window ());
+
+        Assert.True(v2.RemoveTimeout (timeoutToken));
+
+        Assert.Null (Application.Top);
+        v2.Shutdown ();
+
+        ApplicationImpl.ChangeInstance (orig);
+    }
+
+
+    [Fact]
+    public void Test_InitRunShutdown_Generic_IdleForExit ()
+    {
+        var orig = ApplicationImpl.Instance;
+
+        var v2 = NewApplicationV2 ();
+        ApplicationImpl.ChangeInstance (v2);
+
+        v2.Init ();
+
+        v2.AddIdle (IdleExit);
+        Assert.Null (Application.Top);
+
+        // Blocks until the timeout call is hit
+
+        v2.Run<Window> ();
+
+        Assert.Null (Application.Top);
+        v2.Shutdown ();
+
+        ApplicationImpl.ChangeInstance (orig);
+    }
+    private bool IdleExit ()
+    {
+        if (Application.Top != null)
+        {
+            Application.RequestStop ();
+            return true;
+        }
+
+        return true;
+    }
+
+    [Fact]
+    public void TestRepeatedShutdownCalls_DoNotDuplicateDisposeOutput ()
+    {
+        var netInput = new Mock<INetInput> ();
+        SetupRunInputMockMethodToBlock (netInput);
+        Mock<IConsoleOutput>? outputMock = null;
+
+
+        var v2 = new ApplicationV2(
+                                   () => netInput.Object,
+                                   ()=> (outputMock = new Mock<IConsoleOutput>()).Object,
+                                   Mock.Of<IWindowsInput>,
+                                   Mock.Of<IConsoleOutput>);
+
+        v2.Init (null,"v2net");
+
+
+        v2.Shutdown ();
+        v2.Shutdown ();
+        outputMock.Verify(o=>o.Dispose (),Times.Once);
+    }
+    [Fact]
+    public void TestRepeatedInitCalls_WarnsAndIgnores ()
+    {
+        var v2 = NewApplicationV2 ();
+
+        Assert.Null (Application.Driver);
+        v2.Init ();
+        Assert.NotNull (Application.Driver);
+
+        var mockLogger = new Mock<ILogger> ();
+
+        var beforeLogger = Logging.Logger;
+        Logging.Logger = mockLogger.Object;
+
+        v2.Init ();
+        v2.Init ();
+
+        mockLogger.Verify(
+                          l=>l.Log (LogLevel.Error,
+                                    It.IsAny<EventId> (),
+                                    It.Is<It.IsAnyType> ((v, t) => v.ToString () == "Init called multiple times without shutdown, ignoring."),
+                                    It.IsAny<Exception> (),
+                                    It.IsAny<Func<It.IsAnyType, Exception, string>> ())
+                          ,Times.Exactly (2));
+
+        v2.Shutdown ();
+
+        // Restore the original null logger to be polite to other tests
+        Logging.Logger = beforeLogger;
+    }
+
+}
diff --git a/UnitTests/ConsoleDrivers/V2/ConsoleInputTests.cs b/UnitTests/ConsoleDrivers/V2/ConsoleInputTests.cs
new file mode 100644
index 0000000000..888cd5943b
--- /dev/null
+++ b/UnitTests/ConsoleDrivers/V2/ConsoleInputTests.cs
@@ -0,0 +1,57 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace UnitTests.ConsoleDrivers.V2;
+public class ConsoleInputTests
+{
+    class FakeInput : ConsoleInput<char>
+    {
+        private readonly string [] _reads;
+
+        public FakeInput (params string [] reads ) { _reads = reads; }
+
+        int iteration = 0;
+        /// <inheritdoc />
+        protected override bool Peek ()
+        {
+            return iteration < _reads.Length;
+        }
+
+        /// <inheritdoc />
+        protected override IEnumerable<char> Read ()
+        {
+            return _reads [iteration++];
+        }
+    }
+
+    [Fact]
+    public void Test_ThrowsIfNotInitialized ()
+    {
+        var input = new FakeInput ("Fish");
+
+        var ex = Assert.Throws<Exception>(()=>input.Run (new (canceled: true)));
+        Assert.Equal ("Cannot run input before Initialization", ex.Message);
+    }
+
+
+    [Fact]
+    public void Test_Simple ()
+    {
+        var input = new FakeInput ("Fish");
+        var queue = new ConcurrentQueue<char> ();
+
+        input.Initialize (queue);
+
+        var cts = new CancellationTokenSource ();
+        cts.CancelAfter (25); // Cancel after 25 milliseconds
+        CancellationToken token = cts.Token;
+        Assert.Empty (queue);
+        input.Run (token);
+
+        Assert.Equal ("Fish",new string (queue.ToArray ()));
+    }
+}
diff --git a/UnitTests/ConsoleDrivers/V2/MainLoopCoordinatorTests.cs b/UnitTests/ConsoleDrivers/V2/MainLoopCoordinatorTests.cs
new file mode 100644
index 0000000000..d678c93e97
--- /dev/null
+++ b/UnitTests/ConsoleDrivers/V2/MainLoopCoordinatorTests.cs
@@ -0,0 +1,91 @@
+using System.Collections.Concurrent;
+using Microsoft.Extensions.Logging;
+using Moq;
+
+namespace UnitTests.ConsoleDrivers.V2;
+public class MainLoopCoordinatorTests
+{
+    [Fact]
+    public void TestMainLoopCoordinator_InputCrashes_ExceptionSurfacesMainThread ()
+    {
+
+        var mockLogger = new Mock<ILogger> ();
+
+        var beforeLogger = Logging.Logger;
+        Logging.Logger = mockLogger.Object;
+
+        var c = new MainLoopCoordinator<char> (new TimedEvents (),
+                                               // Runs on a separate thread (input thread)
+                                               () => throw new Exception ("Crash on boot"),
+
+                                               // Rest runs on main thread
+                                               new ConcurrentQueue<char> (),
+                                               Mock.Of <IInputProcessor>(),
+                                               ()=>Mock.Of<IConsoleOutput>(),
+                                               Mock.Of<IMainLoop<char>>());
+
+        // StartAsync boots the main loop and the input thread. But if the input class bombs
+        // on startup it is important that the exception surface at the call site and not lost
+        var ex = Assert.ThrowsAsync<AggregateException>(c.StartAsync).Result;
+        Assert.Equal ("Crash on boot", ex.InnerExceptions [0].Message);
+
+
+        // Restore the original null logger to be polite to other tests
+        Logging.Logger = beforeLogger;
+
+
+        // Logs should explicitly call out that input loop crashed.
+        mockLogger.Verify (
+                           l => l.Log (LogLevel.Critical,
+                                       It.IsAny<EventId> (),
+                                       It.Is<It.IsAnyType> ((v, t) => v.ToString () == "Input loop crashed"),
+                                       It.IsAny<Exception> (),
+                                       It.IsAny<Func<It.IsAnyType, Exception, string>> ())
+                         , Times.Once);
+    }
+    /*
+    [Fact]
+    public void TestMainLoopCoordinator_InputExitsImmediately_ExceptionRaisedInMainThread ()
+    {
+
+        // Runs on a separate thread (input thread)
+        // But because it's just a mock it immediately exists
+        var mockInputFactoryMethod = () => Mock.Of<IConsoleInput<char>> ();
+
+
+        var mockOutput = Mock.Of<IConsoleOutput> ();
+        var mockInputProcessor = Mock.Of<IInputProcessor> ();
+        var inputQueue = new ConcurrentQueue<char> ();
+        var timedEvents = new TimedEvents ();
+
+        var mainLoop = new MainLoop<char> ();
+        mainLoop.Initialize (timedEvents,
+                      inputQueue,
+                      mockInputProcessor,
+                      mockOutput
+                      );
+
+        var c = new MainLoopCoordinator<char> (timedEvents,
+                                               mockInputFactoryMethod,
+                                               inputQueue,
+                                               mockInputProcessor,
+                                               ()=>mockOutput,
+                                               mainLoop
+                                               );
+
+        // TODO: This test has race condition
+        //
+        // * When the input loop exits it can happen
+        // * - During boot
+        // * - After boot
+        // *
+        // * If it happens in boot you get input exited
+        // * If it happens after you get "Input loop exited early (stop not called)"
+        //
+
+        // Because the console input class does not block - i.e. breaks contract
+        // We need to let the user know input has silently exited and all has gone bad.
+        var ex = Assert.ThrowsAsync<Exception> (c.StartAsync).Result;
+        Assert.Equal ("Input loop exited during startup instead of entering read loop properly (i.e. and blocking)", ex.Message);
+    }*/
+}
diff --git a/UnitTests/ConsoleDrivers/V2/MainLoopTTests.cs b/UnitTests/ConsoleDrivers/V2/MainLoopTTests.cs
new file mode 100644
index 0000000000..727c105536
--- /dev/null
+++ b/UnitTests/ConsoleDrivers/V2/MainLoopTTests.cs
@@ -0,0 +1,33 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using Moq;
+
+namespace UnitTests.ConsoleDrivers.V2;
+public class MainLoopTTests
+{
+    [Fact]
+    public void MainLoopT_NotInitialized_Throws()
+    {
+        var m = new MainLoop<int> ();
+
+        Assert.Throws<NotInitializedException> (() => m.TimedEvents);
+        Assert.Throws<NotInitializedException> (() => m.InputBuffer);
+        Assert.Throws<NotInitializedException> (() => m.InputProcessor);
+        Assert.Throws<NotInitializedException> (() => m.Out);
+        Assert.Throws<NotInitializedException> (() => m.AnsiRequestScheduler);
+        Assert.Throws<NotInitializedException> (() => m.WindowSizeMonitor);
+
+        m.Initialize (new TimedEvents (),
+                      new ConcurrentQueue<int> (),
+                      Mock.Of <IInputProcessor>(),
+                      Mock.Of<IConsoleOutput>());
+
+        Assert.NotNull (m.TimedEvents);
+        Assert.NotNull (m.InputBuffer);
+        Assert.NotNull (m.InputProcessor);
+        Assert.NotNull (m.Out);
+        Assert.NotNull (m.AnsiRequestScheduler);
+        Assert.NotNull (m.WindowSizeMonitor);
+    }
+}
diff --git a/UnitTests/ConsoleDrivers/V2/MouseInterpreterTests.cs b/UnitTests/ConsoleDrivers/V2/MouseInterpreterTests.cs
new file mode 100644
index 0000000000..50e2ac4c29
--- /dev/null
+++ b/UnitTests/ConsoleDrivers/V2/MouseInterpreterTests.cs
@@ -0,0 +1,155 @@
+using Moq;
+
+namespace UnitTests.ConsoleDrivers.V2;
+public class MouseInterpreterTests
+{
+    [Theory]
+    [MemberData (nameof (SequenceTests))]
+    public void TestMouseEventSequences_InterpretedOnlyAsFlag (List<MouseEventArgs> events, params MouseFlags?[] expected)
+    {
+        // Arrange: Mock dependencies and set up the interpreter
+        var interpreter = new MouseInterpreter (null);
+
+        // Act and Assert
+        for (int i = 0; i < events.Count; i++)
+        {
+            var results = interpreter.Process (events [i]).ToArray();
+
+            // Raw input event should be there
+            Assert.Equal (events [i].Flags, results [0].Flags);
+
+            // also any expected should be there
+            if (expected [i] != null)
+            {
+                Assert.Equal (expected [i], results [1].Flags);
+            }
+            else
+            {
+                Assert.Single (results);
+            }
+        }
+    }
+
+    public static IEnumerable<object []> SequenceTests ()
+    {
+        yield return new object []
+        {
+            new List<MouseEventArgs>
+            {
+                new() { Flags = MouseFlags.Button1Pressed },
+                new()
+            },
+            null,
+            MouseFlags.Button1Clicked
+        };
+
+        yield return new object []
+        {
+            new List<MouseEventArgs>
+            {
+                new() { Flags = MouseFlags.Button1Pressed },
+                new(),
+                new() { Flags = MouseFlags.Button1Pressed },
+                new()
+            },
+            null,
+            MouseFlags.Button1Clicked,
+            null,
+            MouseFlags.Button1DoubleClicked
+        };
+
+        yield return new object []
+        {
+            new List<MouseEventArgs>
+            {
+                new() { Flags = MouseFlags.Button1Pressed },
+                new(),
+                new() { Flags = MouseFlags.Button1Pressed },
+                new(),
+                new() { Flags = MouseFlags.Button1Pressed },
+                new()
+            },
+            null,
+            MouseFlags.Button1Clicked,
+            null,
+            MouseFlags.Button1DoubleClicked,
+            null,
+            MouseFlags.Button1TripleClicked
+        };
+
+        yield return new object []
+        {
+            new List<MouseEventArgs>
+            {
+                new() { Flags = MouseFlags.Button2Pressed },
+                new(),
+                new() { Flags = MouseFlags.Button2Pressed },
+                new(),
+                new() { Flags = MouseFlags.Button2Pressed },
+                new()
+            },
+            null,
+            MouseFlags.Button2Clicked,
+            null,
+            MouseFlags.Button2DoubleClicked,
+            null,
+            MouseFlags.Button2TripleClicked
+        };
+
+        yield return new object []
+        {
+            new List<MouseEventArgs>
+            {
+                new() { Flags = MouseFlags.Button3Pressed },
+                new(),
+                new() { Flags = MouseFlags.Button3Pressed },
+                new(),
+                new() { Flags = MouseFlags.Button3Pressed },
+                new()
+            },
+            null,
+            MouseFlags.Button3Clicked,
+            null,
+            MouseFlags.Button3DoubleClicked,
+            null,
+            MouseFlags.Button3TripleClicked
+        };
+
+        yield return new object []
+        {
+            new List<MouseEventArgs>
+            {
+                new() { Flags = MouseFlags.Button4Pressed },
+                new(),
+                new() { Flags = MouseFlags.Button4Pressed },
+                new(),
+                new() { Flags = MouseFlags.Button4Pressed },
+                new()
+            },
+            null,
+            MouseFlags.Button4Clicked,
+            null,
+            MouseFlags.Button4DoubleClicked,
+            null,
+            MouseFlags.Button4TripleClicked
+        };
+
+        yield return new object []
+        {
+            new List<MouseEventArgs>
+            {
+                new() { Flags = MouseFlags.Button1Pressed ,Position = new Point (10,11)},
+                new(){Position = new Point (10,11)},
+
+                // Clicking the line below means no double click because it's a different location
+                new() { Flags = MouseFlags.Button1Pressed,Position = new Point (10,12) },
+                new(){Position = new Point (10,12)}
+            },
+            null,
+            MouseFlags.Button1Clicked,
+            null,
+            MouseFlags.Button1Clicked //release is click because new position
+        };
+    }
+
+}
diff --git a/UnitTests/ConsoleDrivers/V2/NetInputProcessorTests.cs b/UnitTests/ConsoleDrivers/V2/NetInputProcessorTests.cs
new file mode 100644
index 0000000000..ec7a4fff60
--- /dev/null
+++ b/UnitTests/ConsoleDrivers/V2/NetInputProcessorTests.cs
@@ -0,0 +1,120 @@
+using System.Collections.Concurrent;
+using System.Text;
+
+namespace UnitTests.ConsoleDrivers.V2;
+public class NetInputProcessorTests
+{
+    public static IEnumerable<object []> GetConsoleKeyInfoToKeyTestCases_Rune ()
+    {
+        yield return new object [] { new ConsoleKeyInfo ('C', ConsoleKey.None, false, false, false), new Rune('C') };
+        yield return new object [] { new ConsoleKeyInfo ('\\', ConsoleKey.Oem5, false, false, false), new Rune ('\\') };
+        yield return new object [] { new ConsoleKeyInfo ('+', ConsoleKey.OemPlus, true, false, false), new Rune ('+') };
+        yield return new object [] { new ConsoleKeyInfo ('=', ConsoleKey.OemPlus, false, false, false), new Rune ('=') };
+        yield return new object [] { new ConsoleKeyInfo ('_', ConsoleKey.OemMinus, true, false, false), new Rune ('_') };
+        yield return new object [] { new ConsoleKeyInfo ('-', ConsoleKey.OemMinus, false, false, false), new Rune ('-') };
+        yield return new object [] { new ConsoleKeyInfo (')', ConsoleKey.None, false, false, false), new Rune (')') };
+        yield return new object [] { new ConsoleKeyInfo ('0', ConsoleKey.None, false, false, false), new Rune ('0') };
+        yield return new object [] { new ConsoleKeyInfo ('(', ConsoleKey.None, false, false, false), new Rune ('(') };
+        yield return new object [] { new ConsoleKeyInfo ('9', ConsoleKey.None, false, false, false), new Rune ('9') };
+        yield return new object [] { new ConsoleKeyInfo ('*', ConsoleKey.None, false, false, false), new Rune ('*') };
+        yield return new object [] { new ConsoleKeyInfo ('8', ConsoleKey.None, false, false, false), new Rune ('8') };
+        yield return new object [] { new ConsoleKeyInfo ('&', ConsoleKey.None, false, false, false), new Rune ('&') };
+        yield return new object [] { new ConsoleKeyInfo ('7', ConsoleKey.None, false, false, false), new Rune ('7') };
+        yield return new object [] { new ConsoleKeyInfo ('^', ConsoleKey.None, false, false, false), new Rune ('^') };
+        yield return new object [] { new ConsoleKeyInfo ('6', ConsoleKey.None, false, false, false), new Rune ('6') };
+        yield return new object [] { new ConsoleKeyInfo ('%', ConsoleKey.None, false, false, false), new Rune ('%') };
+        yield return new object [] { new ConsoleKeyInfo ('5', ConsoleKey.None, false, false, false), new Rune ('5') };
+        yield return new object [] { new ConsoleKeyInfo ('$', ConsoleKey.None, false, false, false), new Rune ('$') };
+        yield return new object [] { new ConsoleKeyInfo ('4', ConsoleKey.None, false, false, false), new Rune ('4') };
+        yield return new object [] { new ConsoleKeyInfo ('#', ConsoleKey.None, false, false, false), new Rune ('#') };
+        yield return new object [] { new ConsoleKeyInfo ('@', ConsoleKey.None, false, false, false), new Rune ('@') };
+        yield return new object [] { new ConsoleKeyInfo ('2', ConsoleKey.None, false, false, false), new Rune ('2') };
+        yield return new object [] { new ConsoleKeyInfo ('!', ConsoleKey.None, false, false, false), new Rune ('!') };
+        yield return new object [] { new ConsoleKeyInfo ('1', ConsoleKey.None, false, false, false), new Rune ('1') };
+        yield return new object [] { new ConsoleKeyInfo ('\t', ConsoleKey.None, false, false, false), new Rune ('\t') };
+        yield return new object [] { new ConsoleKeyInfo ('}', ConsoleKey.Oem6, true, false, false), new Rune ('}') };
+        yield return new object [] { new ConsoleKeyInfo (']', ConsoleKey.Oem6, false, false, false), new Rune (']') };
+        yield return new object [] { new ConsoleKeyInfo ('{', ConsoleKey.Oem4, true, false, false), new Rune ('{') };
+        yield return new object [] { new ConsoleKeyInfo ('[', ConsoleKey.Oem4, false, false, false), new Rune ('[') };
+        yield return new object [] { new ConsoleKeyInfo ('\"', ConsoleKey.Oem7, true, false, false), new Rune ('\"') };
+        yield return new object [] { new ConsoleKeyInfo ('\'', ConsoleKey.Oem7, false, false, false), new Rune ('\'') };
+        yield return new object [] { new ConsoleKeyInfo (':', ConsoleKey.Oem1, true, false, false), new Rune (':') };
+        yield return new object [] { new ConsoleKeyInfo (';', ConsoleKey.Oem1, false, false, false), new Rune (';') };
+        yield return new object [] { new ConsoleKeyInfo ('?', ConsoleKey.Oem2, true, false, false), new Rune ('?') };
+        yield return new object [] { new ConsoleKeyInfo ('/', ConsoleKey.Oem2, false, false, false), new Rune ('/') };
+        yield return new object [] { new ConsoleKeyInfo ('>', ConsoleKey.OemPeriod, true, false, false), new Rune ('>') };
+        yield return new object [] { new ConsoleKeyInfo ('.', ConsoleKey.OemPeriod, false, false, false), new Rune ('.') };
+        yield return new object [] { new ConsoleKeyInfo ('<', ConsoleKey.OemComma, true, false, false), new Rune ('<') };
+        yield return new object [] { new ConsoleKeyInfo (',', ConsoleKey.OemComma, false, false, false), new Rune (',') };
+        yield return new object [] { new ConsoleKeyInfo ('w', ConsoleKey.None, false, false, false), new Rune ('w') };
+        yield return new object [] { new ConsoleKeyInfo ('e', ConsoleKey.None, false, false, false), new Rune ('e') };
+        yield return new object [] { new ConsoleKeyInfo ('a', ConsoleKey.None, false, false, false), new Rune ('a') };
+        yield return new object [] { new ConsoleKeyInfo ('s', ConsoleKey.None, false, false, false), new Rune ('s') };
+    }
+
+    [Theory]
+    [MemberData (nameof (GetConsoleKeyInfoToKeyTestCases_Rune))]
+    public void ConsoleKeyInfoToKey_ValidInput_AsRune (ConsoleKeyInfo input, Rune expected)
+    {
+        var converter = new NetKeyConverter ();
+
+        // Act
+        var result = converter.ToKey (input);
+
+        // Assert
+        Assert.Equal (expected, result.AsRune);
+    }
+
+    public static IEnumerable<object []> GetConsoleKeyInfoToKeyTestCases_Key ()
+    {
+        yield return new object [] { new ConsoleKeyInfo ('\t', ConsoleKey.None, false, false, false), Key.Tab};
+        yield return new object [] { new ConsoleKeyInfo ('\u001B', ConsoleKey.None, false, false, false), Key.Esc };
+        yield return new object [] { new ConsoleKeyInfo ('\u007f', ConsoleKey.None, false, false, false), Key.Backspace };
+
+        // TODO: Terminal.Gui does not have a Key for this mapped
+        // TODO: null and default(Key) are both not same as Null.  Why user has to do (Key)0 to get a null key?!
+        yield return new object [] { new ConsoleKeyInfo ('\0', ConsoleKey.LeftWindows, false, false, false), (Key)0 };
+
+    }
+
+
+
+    [Theory]
+    [MemberData (nameof (GetConsoleKeyInfoToKeyTestCases_Key))]
+    public void ConsoleKeyInfoToKey_ValidInput_AsKey (ConsoleKeyInfo input, Key expected)
+    {
+        var converter = new NetKeyConverter ();
+        // Act
+        var result = converter.ToKey (input);
+
+        // Assert
+        Assert.Equal (expected, result);
+    }
+
+    [Fact]
+    public void Test_ProcessQueue_CapitalHLowerE ()
+    {
+        var queue = new ConcurrentQueue<ConsoleKeyInfo> ();
+
+        queue.Enqueue (new ConsoleKeyInfo ('H', ConsoleKey.None, true, false, false));
+        queue.Enqueue (new ConsoleKeyInfo ('e', ConsoleKey.None, false, false, false));
+
+        var processor = new NetInputProcessor (queue);
+
+        List<Key> ups = new List<Key> ();
+        List<Key> downs = new List<Key> ();
+
+        processor.KeyUp += (s, e) => { ups.Add (e); };
+        processor.KeyDown += (s, e) => { downs.Add (e); };
+
+        Assert.Empty (ups);
+        Assert.Empty (downs);
+
+        processor.ProcessQueue ();
+
+        Assert.Equal (Key.H.WithShift, ups [0]);
+        Assert.Equal (Key.H.WithShift, downs [0]);
+        Assert.Equal (Key.E, ups [1]);
+        Assert.Equal (Key.E, downs [1]);
+    }
+}
diff --git a/UnitTests/ConsoleDrivers/V2/WindowSizeMonitorTests.cs b/UnitTests/ConsoleDrivers/V2/WindowSizeMonitorTests.cs
new file mode 100644
index 0000000000..8b7c7a7b64
--- /dev/null
+++ b/UnitTests/ConsoleDrivers/V2/WindowSizeMonitorTests.cs
@@ -0,0 +1,76 @@
+using Moq;
+
+namespace UnitTests.ConsoleDrivers.V2;
+public class WindowSizeMonitorTests
+{
+    [Fact]
+    public void TestWindowSizeMonitor_RaisesEventWhenChanges ()
+    {
+        var consoleOutput = new Mock<IConsoleOutput> ();
+
+        var queue = new Queue<Size>(new []{
+            new Size (30, 20),
+            new Size (20, 20)
+
+        });
+
+        consoleOutput.Setup (m => m.GetWindowSize ())
+                     .Returns (queue.Dequeue);
+
+        var outputBuffer = Mock.Of<IOutputBuffer> ();
+
+        var monitor = new WindowSizeMonitor (consoleOutput.Object, outputBuffer);
+
+        var result = new List<SizeChangedEventArgs> ();
+        monitor.SizeChanging += (s, e) => { result.Add (e);};
+
+        Assert.Empty (result);
+        monitor.Poll ();
+
+        Assert.Single (result);
+        Assert.Equal (new Size (30,20),result [0].Size);
+
+        monitor.Poll ();
+
+        Assert.Equal (2,result.Count);
+        Assert.Equal (new Size (30, 20), result [0].Size);
+        Assert.Equal (new Size (20, 20), result [1].Size);
+    }
+
+    [Fact]
+    public void TestWindowSizeMonitor_DoesNotRaiseEventWhen_NoChanges ()
+    {
+        var consoleOutput = new Mock<IConsoleOutput> ();
+
+        var queue = new Queue<Size> (new []{
+            new Size (30, 20),
+            new Size (30, 20),
+        });
+
+        consoleOutput.Setup (m => m.GetWindowSize ())
+                     .Returns (queue.Dequeue);
+
+        var outputBuffer = Mock.Of<IOutputBuffer> ();
+
+        var monitor = new WindowSizeMonitor (consoleOutput.Object, outputBuffer);
+
+        var result = new List<SizeChangedEventArgs> ();
+        monitor.SizeChanging += (s, e) => { result.Add (e); };
+
+        // First poll always raises event because going from unknown size i.e. 0,0
+        Assert.Empty (result);
+        monitor.Poll ();
+
+        Assert.Single (result);
+        Assert.Equal (new Size (30, 20), result [0].Size);
+
+        // No change 
+        monitor.Poll ();
+
+        Assert.Single (result);
+        Assert.Equal (new Size (30, 20), result [0].Size);
+    }
+
+
+
+}
diff --git a/UnitTests/ConsoleDrivers/V2/WindowsInputProcessorTests.cs b/UnitTests/ConsoleDrivers/V2/WindowsInputProcessorTests.cs
new file mode 100644
index 0000000000..7c9fc14429
--- /dev/null
+++ b/UnitTests/ConsoleDrivers/V2/WindowsInputProcessorTests.cs
@@ -0,0 +1,305 @@
+using System.Collections.Concurrent;
+using Terminal.Gui.ConsoleDrivers;
+using InputRecord = Terminal.Gui.WindowsConsole.InputRecord;
+using ButtonState = Terminal.Gui.WindowsConsole.ButtonState;
+using MouseEventRecord = Terminal.Gui.WindowsConsole.MouseEventRecord;
+
+namespace UnitTests.ConsoleDrivers.V2;
+public class WindowsInputProcessorTests
+{
+
+    [Fact]
+    public void Test_ProcessQueue_CapitalHLowerE ()
+    {
+        var queue = new ConcurrentQueue<InputRecord> ();
+
+        queue.Enqueue (new  InputRecord()
+        {
+            EventType = WindowsConsole.EventType.Key,
+            KeyEvent = new WindowsConsole.KeyEventRecord ()
+            {
+                bKeyDown = true,
+                UnicodeChar = 'H',
+                dwControlKeyState = WindowsConsole.ControlKeyState.CapslockOn,
+                wVirtualKeyCode = (ConsoleKeyMapping.VK)72,
+                wVirtualScanCode = 35
+            }
+        });
+        queue.Enqueue (new InputRecord ()
+        {
+            EventType = WindowsConsole.EventType.Key,
+            KeyEvent = new WindowsConsole.KeyEventRecord ()
+            {
+                bKeyDown = false,
+                UnicodeChar = 'H',
+                dwControlKeyState = WindowsConsole.ControlKeyState.CapslockOn,
+                wVirtualKeyCode = (ConsoleKeyMapping.VK)72,
+                wVirtualScanCode = 35
+            }
+        });
+        queue.Enqueue (new InputRecord ()
+        {
+            EventType = WindowsConsole.EventType.Key,
+            KeyEvent = new WindowsConsole.KeyEventRecord ()
+            {
+                bKeyDown = true,
+                UnicodeChar = 'i',
+                dwControlKeyState = WindowsConsole.ControlKeyState.NoControlKeyPressed,
+                wVirtualKeyCode = (ConsoleKeyMapping.VK)73,
+                wVirtualScanCode = 23
+            }
+        });
+        queue.Enqueue (new InputRecord ()
+        {
+            EventType = WindowsConsole.EventType.Key,
+            KeyEvent = new WindowsConsole.KeyEventRecord ()
+            {
+                bKeyDown = false,
+                UnicodeChar = 'i',
+                dwControlKeyState = WindowsConsole.ControlKeyState.NoControlKeyPressed,
+                wVirtualKeyCode = (ConsoleKeyMapping.VK)73,
+                wVirtualScanCode = 23
+            }
+        });
+
+        var processor = new WindowsInputProcessor (queue);
+
+        List<Key> ups = new List<Key> ();
+        List<Key> downs = new List<Key> ();
+
+        processor.KeyUp += (s, e) => { ups.Add (e); };
+        processor.KeyDown += (s, e) => { downs.Add (e); };
+
+        Assert.Empty (ups);
+        Assert.Empty (downs);
+
+        processor.ProcessQueue ();
+
+        Assert.Equal (Key.H.WithShift, ups [0]);
+        Assert.Equal (Key.H.WithShift, downs [0]);
+        Assert.Equal (Key.I, ups [1]);
+        Assert.Equal (Key.I, downs [1]);
+    }
+
+
+    [Fact]
+    public void Test_ProcessQueue_Mouse_Move ()
+    {
+        var queue = new ConcurrentQueue<InputRecord> ();
+
+        queue.Enqueue (new InputRecord ()
+        {
+            EventType = WindowsConsole.EventType.Mouse,
+            MouseEvent = new WindowsConsole.MouseEventRecord
+            {
+                MousePosition = new WindowsConsole.Coord(32,31),
+                ButtonState = WindowsConsole.ButtonState.NoButtonPressed,
+                ControlKeyState = WindowsConsole.ControlKeyState.NoControlKeyPressed,
+                EventFlags = WindowsConsole.EventFlags.MouseMoved
+            }
+        });
+
+        var processor = new WindowsInputProcessor (queue);
+
+        List<MouseEventArgs> mouseEvents = new List<MouseEventArgs> ();
+
+        processor.MouseEvent += (s, e) => { mouseEvents.Add (e); };
+
+        Assert.Empty (mouseEvents);
+
+        processor.ProcessQueue ();
+
+        var s = Assert.Single (mouseEvents);
+        Assert.Equal (s.Flags,MouseFlags.ReportMousePosition);
+        Assert.Equal (s.ScreenPosition,new Point (32,31));
+    }
+
+    [Theory]
+    [InlineData(WindowsConsole.ButtonState.Button1Pressed,MouseFlags.Button1Pressed)]
+    [InlineData (WindowsConsole.ButtonState.Button2Pressed, MouseFlags.Button2Pressed)]
+    [InlineData (WindowsConsole.ButtonState.Button3Pressed, MouseFlags.Button3Pressed)]
+    [InlineData (WindowsConsole.ButtonState.Button4Pressed, MouseFlags.Button4Pressed)]
+    internal void Test_ProcessQueue_Mouse_Pressed (WindowsConsole.ButtonState state,MouseFlags expectedFlag )
+    {
+        var queue = new ConcurrentQueue<InputRecord> ();
+
+        queue.Enqueue (new InputRecord ()
+        {
+            EventType = WindowsConsole.EventType.Mouse,
+            MouseEvent = new WindowsConsole.MouseEventRecord
+            {
+                MousePosition = new WindowsConsole.Coord (32, 31),
+                ButtonState = state,
+                ControlKeyState = WindowsConsole.ControlKeyState.NoControlKeyPressed,
+                EventFlags = WindowsConsole.EventFlags.MouseMoved
+            }
+        });
+
+        var processor = new WindowsInputProcessor (queue);
+
+        List<MouseEventArgs> mouseEvents = new List<MouseEventArgs> ();
+
+        processor.MouseEvent += (s, e) => { mouseEvents.Add (e); };
+
+        Assert.Empty (mouseEvents);
+
+        processor.ProcessQueue ();
+
+        var s = Assert.Single (mouseEvents);
+        Assert.Equal (s.Flags, MouseFlags.ReportMousePosition | expectedFlag);
+        Assert.Equal (s.ScreenPosition, new Point (32, 31));
+    }
+
+
+    [Theory]
+    [InlineData (100, MouseFlags.WheeledUp)]
+    [InlineData ( -100, MouseFlags.WheeledDown)]
+    internal void Test_ProcessQueue_Mouse_Wheel (int wheelValue, MouseFlags expectedFlag)
+    {
+        var queue = new ConcurrentQueue<InputRecord> ();
+
+        queue.Enqueue (new InputRecord ()
+        {
+            EventType = WindowsConsole.EventType.Mouse,
+            MouseEvent = new WindowsConsole.MouseEventRecord
+            {
+                MousePosition = new WindowsConsole.Coord (32, 31),
+                ButtonState = (WindowsConsole.ButtonState)wheelValue,
+                ControlKeyState = WindowsConsole.ControlKeyState.NoControlKeyPressed,
+                EventFlags = WindowsConsole.EventFlags.MouseWheeled
+            }
+        });
+
+        var processor = new WindowsInputProcessor (queue);
+
+        List<MouseEventArgs> mouseEvents = new List<MouseEventArgs> ();
+
+        processor.MouseEvent += (s, e) => { mouseEvents.Add (e); };
+
+        Assert.Empty (mouseEvents);
+
+        processor.ProcessQueue ();
+
+        var s = Assert.Single (mouseEvents);
+        Assert.Equal (s.Flags,expectedFlag);
+        Assert.Equal (s.ScreenPosition, new Point (32, 31));
+    }
+
+    public static IEnumerable<object []> MouseFlagTestData ()
+    {
+        yield return new object []
+        {
+            new Tuple<ButtonState, MouseFlags>[]
+            {
+                Tuple.Create(ButtonState.Button1Pressed, MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition),
+                Tuple.Create(ButtonState.NoButtonPressed, MouseFlags.Button1Released | MouseFlags.ReportMousePosition),
+                Tuple.Create(ButtonState.NoButtonPressed, MouseFlags.ReportMousePosition | MouseFlags.ReportMousePosition)
+            }
+        };
+
+        yield return new object []
+        {
+            new Tuple<ButtonState, MouseFlags>[]
+            {
+                Tuple.Create ( ButtonState.Button2Pressed, MouseFlags.Button2Pressed  | MouseFlags.ReportMousePosition),
+                Tuple.Create(ButtonState.NoButtonPressed, MouseFlags.Button2Released | MouseFlags.ReportMousePosition),
+                Tuple.Create(ButtonState.NoButtonPressed, MouseFlags.ReportMousePosition | MouseFlags.ReportMousePosition)
+            }
+        }; 
+        yield return new object []
+        {
+            new Tuple<ButtonState, MouseFlags>[]
+            {
+                Tuple.Create(ButtonState.Button3Pressed, MouseFlags.Button3Pressed | MouseFlags.ReportMousePosition),
+                Tuple.Create(ButtonState.NoButtonPressed, MouseFlags.Button3Released | MouseFlags.ReportMousePosition),
+                Tuple.Create(ButtonState.NoButtonPressed, MouseFlags.ReportMousePosition | MouseFlags.ReportMousePosition)
+            }
+        };
+
+        yield return new object []
+        {
+            new Tuple<ButtonState, MouseFlags>[]
+            {
+                Tuple.Create(ButtonState.Button4Pressed, MouseFlags.Button4Pressed | MouseFlags.ReportMousePosition),
+                Tuple.Create(ButtonState.NoButtonPressed, MouseFlags.Button4Released | MouseFlags.ReportMousePosition),
+                Tuple.Create(ButtonState.NoButtonPressed, MouseFlags.ReportMousePosition | MouseFlags.ReportMousePosition)
+            }
+        };
+
+        yield return new object []
+        {
+            new Tuple<ButtonState, MouseFlags>[]
+            {
+                Tuple.Create(ButtonState.RightmostButtonPressed, MouseFlags.Button3Pressed | MouseFlags.ReportMousePosition),
+                Tuple.Create(ButtonState.NoButtonPressed, MouseFlags.Button3Released | MouseFlags.ReportMousePosition),
+                Tuple.Create(ButtonState.NoButtonPressed, MouseFlags.ReportMousePosition | MouseFlags.ReportMousePosition)
+            }
+        };
+
+        // Tests for holding down 2 buttons at once and releasing them one after the other
+        yield return new object []
+        {
+            new Tuple<ButtonState, MouseFlags>[]
+            {
+                Tuple.Create(ButtonState.Button1Pressed | ButtonState.Button2Pressed, MouseFlags.Button1Pressed | MouseFlags.Button2Pressed | MouseFlags.ReportMousePosition),
+                Tuple.Create(ButtonState.Button1Pressed, MouseFlags.Button1Pressed | MouseFlags.Button2Released | MouseFlags.ReportMousePosition),
+                Tuple.Create(ButtonState.NoButtonPressed, MouseFlags.Button1Released | MouseFlags.ReportMousePosition),
+                Tuple.Create(ButtonState.NoButtonPressed,  MouseFlags.ReportMousePosition)
+            }
+        };
+
+        yield return new object []
+        {
+            new Tuple<ButtonState, MouseFlags>[]
+            {
+                Tuple.Create(ButtonState.Button3Pressed | ButtonState.Button4Pressed, MouseFlags.Button3Pressed | MouseFlags.Button4Pressed | MouseFlags.ReportMousePosition),
+                Tuple.Create(ButtonState.Button3Pressed, MouseFlags.Button3Pressed | MouseFlags.Button4Released | MouseFlags.ReportMousePosition),
+                Tuple.Create(ButtonState.NoButtonPressed, MouseFlags.Button3Released | MouseFlags.ReportMousePosition),
+                Tuple.Create(ButtonState.NoButtonPressed,  MouseFlags.ReportMousePosition)
+            }
+        };
+
+        // Test for holding down 2 buttons at once and releasing them simultaneously
+        yield return new object []
+        {
+            new Tuple<ButtonState, MouseFlags>[]
+            {
+                Tuple.Create(ButtonState.Button1Pressed | ButtonState.Button2Pressed, MouseFlags.Button1Pressed | MouseFlags.Button2Pressed | MouseFlags.ReportMousePosition),
+                Tuple.Create(ButtonState.NoButtonPressed, MouseFlags.Button1Released | MouseFlags.Button2Released | MouseFlags.ReportMousePosition),
+                Tuple.Create(ButtonState.NoButtonPressed, MouseFlags.ReportMousePosition)
+            }
+        };
+
+        // Test that rightmost and button 3 are the same button so 2 states is still only 1 flag
+        yield return new object []
+        {
+            new Tuple<ButtonState, MouseFlags>[]
+            {
+                Tuple.Create(ButtonState.Button3Pressed | ButtonState.RightmostButtonPressed, MouseFlags.Button3Pressed | MouseFlags.ReportMousePosition),
+                // Can swap between without raising the released
+                Tuple.Create(ButtonState.Button3Pressed, MouseFlags.Button3Pressed | MouseFlags.ReportMousePosition),
+                Tuple.Create(ButtonState.RightmostButtonPressed, MouseFlags.Button3Pressed | MouseFlags.ReportMousePosition),
+
+                // Now with neither we get released
+                Tuple.Create(ButtonState.NoButtonPressed, MouseFlags.Button3Released | MouseFlags.ReportMousePosition),
+                Tuple.Create(ButtonState.NoButtonPressed, MouseFlags.ReportMousePosition)
+            }
+        };
+    }
+
+    [Theory]
+    [MemberData (nameof (MouseFlagTestData))]
+    internal void MouseFlags_Should_Map_Correctly (Tuple<ButtonState, MouseFlags>[] inputOutputPairs)
+    {
+        var processor = new WindowsInputProcessor (new ());
+
+        foreach (var pair in inputOutputPairs)
+        {
+            var mockEvent = new MouseEventRecord { ButtonState = pair.Item1 };
+            var result = processor.ToDriverMouse (mockEvent);
+
+            Assert.Equal (pair.Item2, result.Flags);
+        }
+    }
+}
+
diff --git a/UnitTests/UICatalog/ScenarioTests.cs b/UnitTests/UICatalog/ScenarioTests.cs
index aeac22357a..09ba8b7543 100644
--- a/UnitTests/UICatalog/ScenarioTests.cs
+++ b/UnitTests/UICatalog/ScenarioTests.cs
@@ -222,15 +222,20 @@ void OnApplicationOnInitializedChanged (object s, EventArgs<bool> a)
                 initialized = true;
                 Application.Iteration += OnApplicationOnIteration;
                 Application.Driver!.ClearedContents += (sender, args) => clearedContentCount++;
-                Application.Driver!.Refreshed += (sender, args) =>
-                                                 {
-                                                     refreshedCount++;
-
-                                                     if (args.CurrentValue)
-                                                     {
-                                                         updatedCount++;
-                                                     }
-                                                 };
+
+                if (Application.Driver is ConsoleDriver cd)
+                {
+                    cd!.Refreshed += (sender, args) =>
+                                     {
+                                         refreshedCount++;
+
+                                         if (args.CurrentValue)
+                                         {
+                                             updatedCount++;
+                                         }
+                                     };
+                }
+
                 Application.NotifyNewRunState += OnApplicationNotifyNewRunState;
 
                 stopwatch = Stopwatch.StartNew ();
@@ -800,7 +805,7 @@ public void Run_Generic ()
             if (token == null)
             {
                 // Timeout only must start at first iteration
-                token = Application.MainLoop.AddTimeout (TimeSpan.FromMilliseconds (ms), abortCallback);
+                token = Application.AddTimeout (TimeSpan.FromMilliseconds (ms), abortCallback);
             }
 
             iterations++;
diff --git a/UnitTests/View/Draw/AllViewsDrawTests.cs b/UnitTests/View/Draw/AllViewsDrawTests.cs
index fa865068bf..64d6c543eb 100644
--- a/UnitTests/View/Draw/AllViewsDrawTests.cs
+++ b/UnitTests/View/Draw/AllViewsDrawTests.cs
@@ -5,10 +5,13 @@ namespace Terminal.Gui.LayoutTests;
 public class AllViewsDrawTests (ITestOutputHelper _output) : TestsAllViews
 {
     [Theory]
+    [SetupFakeDriver] // Required for spinner view that wants to register timeouts
     [MemberData (nameof (AllViewTypes))]
     public void AllViews_Draw_Does_Not_Layout (Type viewType)
     {
         Application.ResetState (true);
+        // Required for spinner view that wants to register timeouts
+        Application.MainLoop = new MainLoop (new FakeMainLoop (Application.Driver));
 
         var view = (View)CreateInstanceIfNotGeneric (viewType);
 
diff --git a/UnitTests/View/Keyboard/KeyboardEventTests.cs b/UnitTests/View/Keyboard/KeyboardEventTests.cs
index fa842d50fc..7cbbcaa8fa 100644
--- a/UnitTests/View/Keyboard/KeyboardEventTests.cs
+++ b/UnitTests/View/Keyboard/KeyboardEventTests.cs
@@ -11,6 +11,7 @@ public class KeyboardEventTests (ITestOutputHelper output) : TestsAllViews
     ///     events: KeyDown and KeyDownNotHandled. Note that KeyUp is independent.
     /// </summary>
     [Theory]
+    [SetupFakeDriver] // Required for spinner view that wants to register timeouts
     [MemberData (nameof (AllViewTypes))]
     public void AllViews_NewKeyDownEvent_All_EventsFire (Type viewType)
     {
@@ -53,6 +54,7 @@ public void AllViews_NewKeyDownEvent_All_EventsFire (Type viewType)
     ///     KeyUp
     /// </summary>
     [Theory]
+    [SetupFakeDriver] // Required for spinner view that wants to register timeouts
     [MemberData (nameof (AllViewTypes))]
     public void AllViews_NewKeyUpEvent_All_EventsFire (Type viewType)
     {
diff --git a/UnitTests/View/Layout/LayoutTests.cs b/UnitTests/View/Layout/LayoutTests.cs
index d1cde695d7..1b41105532 100644
--- a/UnitTests/View/Layout/LayoutTests.cs
+++ b/UnitTests/View/Layout/LayoutTests.cs
@@ -5,9 +5,14 @@ namespace Terminal.Gui.LayoutTests;
 public class LayoutTests (ITestOutputHelper _output) : TestsAllViews
 {
     [Theory]
+    [SetupFakeDriver] // Required for spinner view that wants to register timeouts
     [MemberData (nameof (AllViewTypes))]
     public void AllViews_Layout_Does_Not_Draw (Type viewType)
     {
+
+        // Required for spinner view that wants to register timeouts
+        Application.MainLoop = new MainLoop (new FakeMainLoop (Application.Driver));
+
         var view = (View)CreateInstanceIfNotGeneric (viewType);
 
         if (view == null)
diff --git a/UnitTests/View/Mouse/MouseTests.cs b/UnitTests/View/Mouse/MouseTests.cs
index ab23aead12..9c91735ac4 100644
--- a/UnitTests/View/Mouse/MouseTests.cs
+++ b/UnitTests/View/Mouse/MouseTests.cs
@@ -154,6 +154,7 @@ public void NewMouseEvent_Invokes_MouseEvent_Properly ()
     }
 
     [Theory]
+    [SetupFakeDriver] // Required for spinner view that wants to register timeouts
     [MemberData (nameof (AllViewTypes))]
     public void AllViews_NewMouseEvent_Enabled_False_Does_Not_Set_Handled (Type viewType)
     {
@@ -173,6 +174,7 @@ public void AllViews_NewMouseEvent_Enabled_False_Does_Not_Set_Handled (Type view
     }
 
     [Theory]
+    [SetupFakeDriver] // Required for spinner view that wants to register timeouts
     [MemberData (nameof (AllViewTypes))]
     public void AllViews_NewMouseEvent_Clicked_Enabled_False_Does_Not_Set_Handled (Type viewType)
     {
diff --git a/UnitTests/Views/AllViewsTests.cs b/UnitTests/Views/AllViewsTests.cs
index bc20443618..456c2be0bd 100644
--- a/UnitTests/Views/AllViewsTests.cs
+++ b/UnitTests/Views/AllViewsTests.cs
@@ -12,8 +12,12 @@ public class AllViewsTests (ITestOutputHelper output) : TestsAllViews
 
     [Theory]
     [MemberData (nameof (AllViewTypes))]
+    [SetupFakeDriver] // Required for spinner view that wants to register timeouts
     public void AllViews_Center_Properly (Type viewType)
     {
+        // Required for spinner view that wants to register timeouts
+        Application.MainLoop = new MainLoop (new FakeMainLoop (Application.Driver));
+
         var view = (View)CreateInstanceIfNotGeneric (viewType);
         // See https://github.com/gui-cs/Terminal.Gui/issues/3156
 
@@ -62,6 +66,7 @@ public void AllViews_Center_Properly (Type viewType)
 
     [Theory]
     [MemberData (nameof (AllViewTypes))]
+    [SetupFakeDriver] // Required for spinner view that wants to register timeouts
     public void AllViews_Tests_All_Constructors (Type viewType)
     {
         Assert.True (Test_All_Constructors_Of_Type (viewType));
@@ -97,8 +102,12 @@ public bool Test_All_Constructors_Of_Type (Type type)
 
     [Theory]
     [MemberData (nameof (AllViewTypes))]
+    [SetupFakeDriver] // Required for spinner view that wants to register timeouts
     public void AllViews_Command_Select_Raises_Selecting (Type viewType)
     {
+        // Required for spinner view that wants to register timeouts
+        Application.MainLoop = new MainLoop (new FakeMainLoop (Application.Driver));
+
         var view = (View)CreateInstanceIfNotGeneric (viewType);
 
         if (view == null)
@@ -131,9 +140,13 @@ public void AllViews_Command_Select_Raises_Selecting (Type viewType)
     }
 
     [Theory]
+    [SetupFakeDriver] // Required for spinner view that wants to register timeouts
     [MemberData (nameof (AllViewTypes))]
     public void AllViews_Command_Accept_Raises_Accepted (Type viewType)
     {
+        // Required for spinner view that wants to register timeouts
+        Application.MainLoop = new MainLoop (new FakeMainLoop (Application.Driver));
+
         var view = (View)CreateInstanceIfNotGeneric (viewType);
 
         if (view == null)
@@ -168,8 +181,12 @@ public void AllViews_Command_Accept_Raises_Accepted (Type viewType)
 
     [Theory]
     [MemberData (nameof (AllViewTypes))]
+    [SetupFakeDriver] // Required for spinner view that wants to register timeouts
     public void AllViews_Command_HotKey_Raises_HandlingHotKey (Type viewType)
     {
+        // Required for spinner view that wants to register timeouts
+        Application.MainLoop = new MainLoop (new FakeMainLoop (Application.Driver));
+
         var view = (View)CreateInstanceIfNotGeneric (viewType);
 
         if (view == null)
diff --git a/UnitTests/Views/SpinnerViewTests.cs b/UnitTests/Views/SpinnerViewTests.cs
index b8e62f8281..0af6f0b50a 100644
--- a/UnitTests/Views/SpinnerViewTests.cs
+++ b/UnitTests/Views/SpinnerViewTests.cs
@@ -15,33 +15,33 @@ public void TestSpinnerView_AutoSpin (bool callStop)
     {
         SpinnerView view = GetSpinnerView ();
 
-        Assert.Empty (Application.MainLoop._timeouts);
+        Assert.Empty (Application.MainLoop.TimedEvents.Timeouts);
         view.AutoSpin = true;
-        Assert.NotEmpty (Application.MainLoop._timeouts);
+        Assert.NotEmpty (Application.MainLoop.TimedEvents.Timeouts);
         Assert.True (view.AutoSpin);
 
         //More calls to AutoSpin do not add more timeouts
-        Assert.Single (Application.MainLoop._timeouts);
+        Assert.Single (Application.MainLoop.TimedEvents.Timeouts);
         view.AutoSpin = true;
         view.AutoSpin = true;
         view.AutoSpin = true;
         Assert.True (view.AutoSpin);
-        Assert.Single (Application.MainLoop._timeouts);
+        Assert.Single (Application.MainLoop.TimedEvents.Timeouts);
 
         if (callStop)
         {
             view.AutoSpin = false;
-            Assert.Empty (Application.MainLoop._timeouts);
+            Assert.Empty (Application.MainLoop.TimedEvents.Timeouts);
             Assert.False (view.AutoSpin);
         }
         else
         {
-            Assert.NotEmpty (Application.MainLoop._timeouts);
+            Assert.NotEmpty (Application.MainLoop.TimedEvents.Timeouts);
         }
 
         // Dispose clears timeout
         view.Dispose ();
-        Assert.Empty (Application.MainLoop._timeouts);
+        Assert.Empty (Application.MainLoop.TimedEvents.Timeouts);
         Application.Top.Dispose ();
     }
 
diff --git a/docfx/docs/ansiparser.md b/docfx/docs/ansiparser.md
new file mode 100644
index 0000000000..d18bbe8882
--- /dev/null
+++ b/docfx/docs/ansiparser.md
@@ -0,0 +1,50 @@
+# AnsiResponseParser
+
+## Background
+Terminals send input to the running process as a stream of characters. In addition to regular characters ('a','b','c' etc), the terminal needs to be able to communicate more advanced concepts ('Alt+x', 'Mouse Moved', 'Terminal resized' etc). This is done through the use of 'terminal sequences'.
+
+All terminal sequences start with Esc (`'\x1B'`) and are then followed by specific characters per the event to which they correspond. For example:
+
+| Input Sequence  | Meaning                               |
+|----------------|-------------------------------------------|
+| `<Esc>[A`       | Up Arrow Key                              |
+| `<Esc>[B`       | Down Arrow Key                            |
+| `<Esc>[5~`      | Page Up Key                               |
+| `<Esc>[6~`      | Page Down Key                             |
+
+Most sequences begin with what is called a `CSI` which is just `\x1B[` (`<Esc>[`). But some terminals send older sequences such as:
+
+| Input Sequence  | Meaning                               |
+|----------------|-------------------------------------------|
+| `<Esc>OR`     | F3 (SS3 Pattern)                         |
+| `<Esc>OD`       | Left Arrow Key (SS3 Pattern)             |
+| `<Esc>g`      | Alt+G (Escape as alt)                      |
+| `<Esc>O`      |  Alt+Shift+O  (Escape as alt)|
+
+When using the windows driver, this is mostly already dealt with automatically by the relevant native APIs (e.g. `ReadConsoleInputW` from kernel32).  In contrast the net driver operates in 'raw mode' and processes input almost exclusively using these sequences.
+
+Regardless of the driver used, some escape codes will always be processed e.g. responses to Device Attributes Requests etc.
+
+## Role of AnsiResponseParser
+
+This class is responsible for filtering the input stream to distinguish between regular user key presses and terminal sequences. 
+
+## Timing 
+Timing is a critical component of interpreting the input stream. For example if the stream serves the escape (Esc), the parser must decide whether it's a standalone keypress or the start of a sequence. Similarly seeing the sequence `<Esc>O` could be Alt+Upper Case O, or the beginning of an SS3 pattern for F3 (were R to follow).
+
+Because it is such a critical component, it is abstracted away from the parser itself. Instead the host class (e.g. InputProcessor) must decide when a suitable time has elapsed, after which the `Release` method should be invoked which will forcibly resolve any waiting state.
+
+This can be controlled through the `_escTimeout` field. This approach is consistent with other terminal interfaces e.g. bash.
+
+## State and Held
+
+The parser has 3 states:
+
+| State  | Meaning                               |
+|----------------|-------------------------------------------|
+| `Normal`     | We are encountering regular keys and letting them pass through unprocessed|
+| `ExpectingEscapeSequence`       | We encountered an `Esc` and are holding it to see if a sequence follows |
+| `InResponse`      | The `Esc` was followed by more keys that look like they will make a full terminal sequence (hold them all) |
+
+Extensive trace logging is built into the implementation, to allow for reproducing corner cases and/or rare environments. See the logging article for more details on how to set this up.
+
diff --git a/docfx/docs/logging.md b/docfx/docs/logging.md
new file mode 100644
index 0000000000..6df97a0feb
--- /dev/null
+++ b/docfx/docs/logging.md
@@ -0,0 +1,123 @@
+# Logging
+
+Logging has come to Terminal.Gui! You can now enable comprehensive logging of the internals of the libray. This can help diagnose issues with specific terminals, keyboard cultures and/or operating system specific issues.
+
+To enable file logging you should set the static property `Logging.Logger` to an instance of `Microsoft.Extensions.Logging.ILogger`.  If your program already uses logging you can provide a shared instance or instance from Dependency Injection (DI).
+
+Alternatively you can create a new log to ensure only Terminal.Gui logs appear.
+
+Any logging framework will work  (Serilog, NLog, Log4Net etc) but you should ensure you only log to File or UDP etc (i.e. not to console!).
+
+## Worked example with Serilog to file
+
+Here is an example of how to add logging of Terminal.Gui internals to your program using Serilog file log.
+
+Add the Serilog dependencies:
+
+```
+ dotnet add package Serilog
+ dotnet add package Serilog.Sinks.File
+ dotnet add package Serilog.Extensions.Logging 
+```
+
+Create a static helper function to create the logger and store in `Logging.Logger`:
+
+```csharp
+Logging.Logger = CreateLogger();
+
+
+ static ILogger CreateLogger()
+{
+    // Configure Serilog to write logs to a file
+    Log.Logger = new LoggerConfiguration()
+        .MinimumLevel.Verbose() // Verbose includes Trace and Debug
+        .WriteTo.File("logs/logfile.txt", rollingInterval: RollingInterval.Day)
+        .CreateLogger();
+
+    // Create a logger factory compatible with Microsoft.Extensions.Logging
+    using var loggerFactory = LoggerFactory.Create(builder =>
+    {
+        builder
+            .AddSerilog(dispose: true) // Integrate Serilog with ILogger
+            .SetMinimumLevel(LogLevel.Trace); // Set minimum log level
+    });
+
+    // Get an ILogger instance
+    return loggerFactory.CreateLogger("Global Logger");
+}
+```
+
+This will create logs in your executables directory (e.g.`\bin\Debug\net8.0`).
+
+Example logs:
+```
+2025-02-15 13:36:48.635 +00:00 [INF] Main Loop Coordinator booting...
+2025-02-15 13:36:48.663 +00:00 [INF] Creating NetOutput
+2025-02-15 13:36:48.668 +00:00 [INF] Creating NetInput
+2025-02-15 13:36:48.671 +00:00 [INF] Main Loop Coordinator booting complete
+2025-02-15 13:36:49.145 +00:00 [INF] Run 'MainWindow(){X=0,Y=0,Width=0,Height=0}'
+2025-02-15 13:36:49.163 +00:00 [VRB] Mouse Interpreter raising ReportMousePosition
+2025-02-15 13:36:49.165 +00:00 [VRB] AnsiResponseParser handled as mouse '[<35;50;23m'
+2025-02-15 13:36:49.166 +00:00 [VRB] MainWindow triggered redraw (NeedsDraw=True NeedsLayout=True) 
+2025-02-15 13:36:49.167 +00:00 [INF] Console size changes from '{Width=0, Height=0}' to {Width=120, Height=30}
+2025-02-15 13:36:49.859 +00:00 [VRB] AnsiResponseParser releasing '
+'
+2025-02-15 13:36:49.867 +00:00 [VRB] MainWindow triggered redraw (NeedsDraw=True NeedsLayout=True) 
+2025-02-15 13:36:50.857 +00:00 [VRB] MainWindow triggered redraw (NeedsDraw=True NeedsLayout=True) 
+2025-02-15 13:36:51.417 +00:00 [VRB] MainWindow triggered redraw (NeedsDraw=True NeedsLayout=True) 
+2025-02-15 13:36:52.224 +00:00 [VRB] Mouse Interpreter raising ReportMousePosition
+2025-02-15 13:36:52.226 +00:00 [VRB] AnsiResponseParser handled as mouse '[<35;51;23m'
+2025-02-15 13:36:52.226 +00:00 [VRB] Mouse Interpreter raising ReportMousePosition
+2025-02-15 13:36:52.226 +00:00 [VRB] AnsiResponseParser handled as mouse '[<35;52;23m'
+2025-02-15 13:36:52.226 +00:00 [VRB] Mouse Interpreter raising ReportMousePosition
+2025-02-15 13:36:52.226 +00:00 [VRB] AnsiResponseParser handled as mouse '[<35;53;23m'
+...
+2025-02-15 13:36:52.846 +00:00 [VRB] Mouse Interpreter raising ReportMousePosition
+2025-02-15 13:36:52.846 +00:00 [VRB] AnsiResponseParser handled as mouse '[<35;112;4m'
+2025-02-15 13:36:54.151 +00:00 [INF] RequestStop ''
+2025-02-15 13:36:54.151 +00:00 [VRB] AnsiResponseParser handled as keyboard '[21~'
+2025-02-15 13:36:54.225 +00:00 [INF] Input loop exited cleanly
+```
+
+## Metrics
+
+If you are finding that the UI is slow or unresponsive - or are just interested in performance metrics.  You can see these by instaling the `dotnet-counter` tool and running it for your process.
+
+```
+dotnet tool install dotnet-counters --global
+```
+
+```
+ dotnet-counters monitor -n YourProcessName --counters Terminal.Gui
+```
+
+Example output:
+
+```
+Press p to pause, r to resume, q to quit.
+    Status: Running
+
+Name                                                      Current Value
+[Terminal.Gui]
+    Drain Input (ms)
+        Percentile
+        50                                                      0
+        95                                                      0
+        99                                                      0
+    Invokes & Timers (ms)
+        Percentile
+        50                                                      0
+        95                                                      0
+        99                                                      0
+    Iteration (ms)
+        Percentile
+        50                                                      0
+        95                                                      1
+        99                                                      1
+        Redraws (Count)                                         9
+```
+
+Metrics figures issues such as:
+
+- Your console constantly being refreshed (Redraws)
+- You are blocking main thread with long running Invokes / Timeout callbacks (Invokes & Timers)