2
0
Эх сурвалжийг харах

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

Tig 4 сар өмнө
parent
commit
cc749bca1b
100 өөрчлөгдсөн 7977 нэмэгдсэн , 1034 устгасан
  1. 9 8
      .gitattributes
  2. 2 0
      .gitignore
  3. 20 0
      Benchmarks/Benchmarks.csproj
  4. 48 0
      Benchmarks/ConsoleDrivers/EscSeqUtils/CSI_SetVsAppend.cs
  5. 20 0
      Benchmarks/Program.cs
  6. 66 0
      Benchmarks/Text/RuneExtensions/DecodeSurrogatePair.cs
  7. 72 0
      Benchmarks/Text/RuneExtensions/Encode.cs
  8. 36 0
      Benchmarks/Text/RuneExtensions/EncodeSurrogatePair.cs
  9. 74 0
      Benchmarks/Text/RuneExtensions/GetEncodingLength.cs
  10. 50 0
      Benchmarks/Text/RuneExtensions/IsSurrogatePair.cs
  11. 6 2
      CommunityToolkitExample/LoginView.cs
  12. 2 0
      Example/Example.cs
  13. 2 0
      NativeAot/Program.cs
  14. 2 0
      SelfContained/Program.cs
  15. 36 29
      Terminal.Gui/Application/Application.Initialization.cs
  16. 0 2
      Terminal.Gui/Application/Application.Keyboard.cs
  17. 3 3
      Terminal.Gui/Application/Application.Mouse.cs
  18. 19 129
      Terminal.Gui/Application/Application.Run.cs
  19. 91 0
      Terminal.Gui/Application/Application.cd
  20. 7 2
      Terminal.Gui/Application/Application.cs
  21. 303 0
      Terminal.Gui/Application/ApplicationImpl.cs
  22. 192 0
      Terminal.Gui/Application/IApplication.cs
  23. 90 0
      Terminal.Gui/Application/ITimedEvents.cs
  24. 10 244
      Terminal.Gui/Application/MainLoop.cs
  25. 257 0
      Terminal.Gui/Application/TimedEvents.cs
  26. 2 2
      Terminal.Gui/Application/TimeoutEventArgs.cs
  27. 2 2
      Terminal.Gui/Configuration/AttributeJsonConverter.cs
  28. 1 1
      Terminal.Gui/Configuration/ColorSchemeJsonConverter.cs
  29. 29 23
      Terminal.Gui/Configuration/ConfigurationManager.cs
  30. 2 2
      Terminal.Gui/Configuration/DictionaryJsonConverter.cs
  31. 1 0
      Terminal.Gui/Configuration/Scope.cs
  32. 5 5
      Terminal.Gui/Configuration/ScopeJsonConverter.cs
  33. 4 4
      Terminal.Gui/Configuration/SettingsScope.cs
  34. 1 1
      Terminal.Gui/Configuration/SourceGenerationContext.cs
  35. 3 3
      Terminal.Gui/Configuration/ThemeManager.cs
  36. 267 0
      Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiMouseParser.cs
  37. 2 2
      Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiRequestScheduler.cs
  38. 171 13
      Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs
  39. 3 2
      Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParserState.cs
  40. 3 0
      Terminal.Gui/ConsoleDrivers/AnsiResponseParser/GenericHeld.cs
  41. 1 1
      Terminal.Gui/ConsoleDrivers/AnsiResponseParser/IAnsiResponseParser.cs
  42. 2 0
      Terminal.Gui/ConsoleDrivers/AnsiResponseParser/IHeld.cs
  43. 27 0
      Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/AnsiKeyboardParser.cs
  44. 55 0
      Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/AnsiKeyboardParserPattern.cs
  45. 86 0
      Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/CsiKeyPattern.cs
  46. 27 0
      Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/EscAsAltPattern.cs
  47. 45 0
      Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/Ss3Pattern.cs
  48. 3 0
      Terminal.Gui/ConsoleDrivers/AnsiResponseParser/StringHeld.cs
  49. 7 7
      Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs
  50. 1 1
      Terminal.Gui/ConsoleDrivers/CursesDriver/UnixMainLoop.cs
  51. 101 70
      Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqUtils.cs
  52. 2 2
      Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs
  53. 8 60
      Terminal.Gui/ConsoleDrivers/IConsoleDriver.cs
  54. 2 2
      Terminal.Gui/ConsoleDrivers/NetDriver/NetEvents.cs
  55. 2 2
      Terminal.Gui/ConsoleDrivers/NetDriver/NetMainLoop.cs
  56. 243 0
      Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs
  57. 388 0
      Terminal.Gui/ConsoleDrivers/V2/ConsoleDriverFacade.cs
  58. 79 0
      Terminal.Gui/ConsoleDrivers/V2/ConsoleInput.cs
  59. 14 0
      Terminal.Gui/ConsoleDrivers/V2/IConsoleDriverFacade.cs
  60. 29 0
      Terminal.Gui/ConsoleDrivers/V2/IConsoleInput.cs
  61. 42 0
      Terminal.Gui/ConsoleDrivers/V2/IConsoleOutput.cs
  62. 60 0
      Terminal.Gui/ConsoleDrivers/V2/IInputProcessor.cs
  63. 18 0
      Terminal.Gui/ConsoleDrivers/V2/IKeyConverter.cs
  64. 58 0
      Terminal.Gui/ConsoleDrivers/V2/IMainLoop.cs
  65. 24 0
      Terminal.Gui/ConsoleDrivers/V2/IMainLoopCoordinator.cs
  66. 4 0
      Terminal.Gui/ConsoleDrivers/V2/INetInput.cs
  67. 122 0
      Terminal.Gui/ConsoleDrivers/V2/IOutputBuffer.cs
  68. 20 0
      Terminal.Gui/ConsoleDrivers/V2/IToplevelTransitionManager.cs
  69. 19 0
      Terminal.Gui/ConsoleDrivers/V2/IWindowSizeMonitor.cs
  70. 4 0
      Terminal.Gui/ConsoleDrivers/V2/IWindowsInput.cs
  71. 165 0
      Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs
  72. 148 0
      Terminal.Gui/ConsoleDrivers/V2/Logging.cs
  73. 202 0
      Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs
  74. 186 0
      Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs
  75. 89 0
      Terminal.Gui/ConsoleDrivers/V2/MouseButtonStateEx.cs
  76. 105 0
      Terminal.Gui/ConsoleDrivers/V2/MouseInterpreter.cs
  77. 61 0
      Terminal.Gui/ConsoleDrivers/V2/NetInput.cs
  78. 59 0
      Terminal.Gui/ConsoleDrivers/V2/NetInputProcessor.cs
  79. 25 0
      Terminal.Gui/ConsoleDrivers/V2/NetKeyConverter.cs
  80. 249 0
      Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs
  81. 22 0
      Terminal.Gui/ConsoleDrivers/V2/NotInitializedException.cs
  82. 449 0
      Terminal.Gui/ConsoleDrivers/V2/OutputBuffer.cs
  83. 37 0
      Terminal.Gui/ConsoleDrivers/V2/ToplevelTransitionManager.cs
  84. 569 0
      Terminal.Gui/ConsoleDrivers/V2/V2.cd
  85. 37 0
      Terminal.Gui/ConsoleDrivers/V2/WindowSizeMonitor.cs
  86. 114 0
      Terminal.Gui/ConsoleDrivers/V2/WindowsInput.cs
  87. 157 0
      Terminal.Gui/ConsoleDrivers/V2/WindowsInputProcessor.cs
  88. 38 0
      Terminal.Gui/ConsoleDrivers/V2/WindowsKeyConverter.cs
  89. 344 0
      Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs
  90. 14 14
      Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsConsole.cs
  91. 14 7
      Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsDriver.cs
  92. 3 3
      Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsMainLoop.cs
  93. 190 151
      Terminal.Gui/Drawing/Glyphs.cs
  94. 175 117
      Terminal.Gui/Drawing/LineCanvas/LineCanvas.cs
  95. 3 3
      Terminal.Gui/Drawing/LineCanvas/LineStyle.cs
  96. 970 114
      Terminal.Gui/Drawing/Region.cs
  97. 128 0
      Terminal.Gui/Drawing/RegionOp.cs
  98. 13 0
      Terminal.Gui/Drawing/Thickness.cs
  99. 4 0
      Terminal.Gui/FileServices/DefaultFileOperations.cs
  100. 1 1
      Terminal.Gui/Input/CommandContext.cs

+ 9 - 8
.gitattributes

@@ -1,13 +1,14 @@
-# Set the default behavior for all files.
+# Set default behavior to automatically normalize line endings.
 * text=auto
 
-# Normalized and converts to 
-# native line endings on checkout.
-*.cs text
-
-# Convert to LF line endings on checkout.
+# Explicitly declare text files you want to always be normalized and converted to native line endings on checkout.
+*.cs text eol=lf
+*.txt text eol=lf
+*.md text eol=lf
 *.sh text eol=lf
+*.ps1 text eol=lf
 
-# Binary files.
+# Denote all files that are truly binary and should not be modified.
 *.png binary
-*.jpg binary
+*.jpg binary
+*.gif binary

+ 2 - 0
.gitignore

@@ -58,3 +58,5 @@ demo.*
 *.tui/
 
 *.dotCover
+
+logs/

+ 20 - 0
Benchmarks/Benchmarks.csproj

@@ -0,0 +1,20 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <OutputType>Exe</OutputType>
+    <TargetFramework>net8.0</TargetFramework>
+    <IsPackable>false</IsPackable>
+    <ImplicitUsings>enable</ImplicitUsings>
+    <Nullable>enable</Nullable>
+    <RootNamespace>Terminal.Gui.$(MSBuildProjectName.Replace(" ", "_"))</RootNamespace>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="BenchmarkDotNet" Version="0.14.0" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\Terminal.Gui\Terminal.Gui.csproj" />
+  </ItemGroup>
+
+</Project>

+ 48 - 0
Benchmarks/ConsoleDrivers/EscSeqUtils/CSI_SetVsAppend.cs

@@ -0,0 +1,48 @@
+using System.Text;
+using BenchmarkDotNet.Attributes;
+using Tui = Terminal.Gui;
+
+namespace Terminal.Gui.Benchmarks.ConsoleDrivers.EscSeqUtils;
+
+/// <summary>
+/// Compares the Set and Append implementations in combination.
+/// </summary>
+/// <remarks>
+/// A bit misleading because *CursorPosition is called very seldom compared to the other operations
+/// but they are very similar in performance because they do very similar things.
+/// </remarks>
+[MemoryDiagnoser]
+[BenchmarkCategory (nameof (Tui.EscSeqUtils))]
+// Hide useless empty column from results.
+[HideColumns ("stringBuilder")]
+public class CSI_SetVsAppend
+{
+    [Benchmark (Baseline = true)]
+    [ArgumentsSource (nameof (StringBuilderSource))]
+    public StringBuilder Set (StringBuilder stringBuilder)
+    {
+        stringBuilder.Append (Tui.EscSeqUtils.CSI_SetBackgroundColorRGB (1, 2, 3));
+        stringBuilder.Append (Tui.EscSeqUtils.CSI_SetForegroundColorRGB (3, 2, 1));
+        stringBuilder.Append (Tui.EscSeqUtils.CSI_SetCursorPosition (4, 2));
+        // Clear to prevent out of memory exception from consecutive iterations.
+        stringBuilder.Clear ();
+        return stringBuilder;
+    }
+
+    [Benchmark]
+    [ArgumentsSource (nameof (StringBuilderSource))]
+    public StringBuilder Append (StringBuilder stringBuilder)
+    {
+        Tui.EscSeqUtils.CSI_AppendBackgroundColorRGB (stringBuilder, 1, 2, 3);
+        Tui.EscSeqUtils.CSI_AppendForegroundColorRGB (stringBuilder, 3, 2, 1);
+        Tui.EscSeqUtils.CSI_AppendCursorPosition (stringBuilder, 4, 2);
+        // Clear to prevent out of memory exception from consecutive iterations.
+        stringBuilder.Clear ();
+        return stringBuilder;
+    }
+
+    public static IEnumerable<object> StringBuilderSource ()
+    {
+        return [new StringBuilder ()];
+    }
+}

+ 20 - 0
Benchmarks/Program.cs

@@ -0,0 +1,20 @@
+using BenchmarkDotNet.Configs;
+using BenchmarkDotNet.Running;
+
+namespace Terminal.Gui.Benchmarks;
+
+class Program
+{
+    static void Main (string [] args)
+    {
+        var config = DefaultConfig.Instance;
+
+        // Uncomment for faster but less accurate intermediate iteration.
+        // Final benchmarks should be run with at least the default run length.
+        //config = config.AddJob (BenchmarkDotNet.Jobs.Job.ShortRun);
+
+        BenchmarkSwitcher
+            .FromAssembly (typeof (Program).Assembly)
+            .Run(args, config);
+    }
+}

+ 66 - 0
Benchmarks/Text/RuneExtensions/DecodeSurrogatePair.cs

@@ -0,0 +1,66 @@
+using System.Text;
+using BenchmarkDotNet.Attributes;
+using Tui = Terminal.Gui;
+
+namespace Terminal.Gui.Benchmarks.Text.RuneExtensions;
+
+/// <summary>
+/// Benchmarks for <see cref="Tui.RuneExtensions.DecodeSurrogatePair"/> performance fine-tuning.
+/// </summary>
+[MemoryDiagnoser]
+[BenchmarkCategory (nameof (Tui.RuneExtensions))]
+public class DecodeSurrogatePair
+{
+    /// <summary>
+    /// Benchmark for previous implementation.
+    /// </summary>
+    /// <param name="rune"></param>
+    /// <returns></returns>
+    [Benchmark]
+    [ArgumentsSource (nameof (DataSource))]
+    public char []? Previous (Rune rune)
+    {
+        _ = RuneToStringToCharArray (rune, out char []? chars);
+        return chars;
+    }
+
+    /// <summary>
+    /// Benchmark for current implementation.
+    /// 
+    /// Utilizes Rune methods that take Span argument avoiding intermediate heap array allocation when combined with stack allocated intermediate buffer.
+    /// When rune is not surrogate pair there will be no heap allocation.
+    /// 
+    /// Final surrogate pair array allocation cannot be avoided due to the current method signature design.
+    /// Changing the method signature, or providing an alternative method, to take a destination Span would allow further optimizations by allowing caller to reuse buffer for consecutive calls.
+    /// </summary>
+    [Benchmark (Baseline = true)]
+    [ArgumentsSource (nameof (DataSource))]
+    public char []? Current (Rune rune)
+    {
+        _ = Tui.RuneExtensions.DecodeSurrogatePair (rune, out char []? chars);
+        return chars;
+    }
+
+    /// <summary>
+    /// Previous implementation with intermediate string allocation.
+    /// 
+    /// The IsSurrogatePair implementation at the time had hidden extra string allocation so there were intermediate heap allocations even if rune is not surrogate pair.
+    /// </summary>
+    private static bool RuneToStringToCharArray (Rune rune, out char []? chars)
+    {
+        if (rune.IsSurrogatePair ())
+        {
+            chars = rune.ToString ().ToCharArray ();
+            return true;
+        }
+
+        chars = null;
+        return false;
+    }
+
+    public static IEnumerable<object> DataSource ()
+    {
+        yield return new Rune ('a');
+        yield return "𝔹".EnumerateRunes ().Single ();
+    }
+}

+ 72 - 0
Benchmarks/Text/RuneExtensions/Encode.cs

@@ -0,0 +1,72 @@
+using System.Text;
+using BenchmarkDotNet.Attributes;
+using Tui = Terminal.Gui;
+
+namespace Terminal.Gui.Benchmarks.Text.RuneExtensions;
+
+/// <summary>
+/// Benchmarks for <see cref="Tui.RuneExtensions.Encode"/> performance fine-tuning.
+/// </summary>
+[MemoryDiagnoser]
+[BenchmarkCategory (nameof (Tui.RuneExtensions))]
+public class Encode
+{
+    /// <summary>
+    /// Benchmark for previous implementation.
+    /// </summary>
+    [Benchmark]
+    [ArgumentsSource (nameof (DataSource))]
+    public byte [] Previous (Rune rune, byte [] destination, int start, int count)
+    {
+        _ = StringEncodingGetBytes (rune, destination, start, count);
+        return destination;
+    }
+
+    /// <summary>
+    /// Benchmark for current implementation.
+    /// 
+    /// Avoids intermediate heap allocations with stack allocated intermediate buffer.
+    /// </summary>
+    [Benchmark (Baseline = true)]
+    [ArgumentsSource (nameof (DataSource))]
+    public byte [] Current (Rune rune, byte [] destination, int start, int count)
+    {
+        _ = Tui.RuneExtensions.Encode (rune, destination, start, count);
+        return destination;
+    }
+
+    /// <summary>
+    /// Previous implementation with intermediate byte array and string allocation.
+    /// </summary>
+    private static int StringEncodingGetBytes (Rune rune, byte [] dest, int start = 0, int count = -1)
+    {
+        byte [] bytes = Encoding.UTF8.GetBytes (rune.ToString ());
+        var length = 0;
+
+        for (var i = 0; i < (count == -1 ? bytes.Length : count); i++)
+        {
+            if (bytes [i] == 0)
+            {
+                break;
+            }
+
+            dest [start + i] = bytes [i];
+            length++;
+        }
+
+        return length;
+    }
+
+    public static IEnumerable<object []> DataSource ()
+    {
+        Rune[] runes = [ new Rune ('a'),"𝔞".EnumerateRunes().Single() ];
+
+        foreach (var rune in runes)
+        {
+            yield return new object [] { rune, new byte [16], 0, -1 };
+            yield return new object [] { rune, new byte [16], 8, -1 };
+            // Does not work in original implementation
+            //yield return new object [] { rune, new byte [16], 8, 8 };
+        }
+    }
+}

+ 36 - 0
Benchmarks/Text/RuneExtensions/EncodeSurrogatePair.cs

@@ -0,0 +1,36 @@
+using System.Text;
+using BenchmarkDotNet.Attributes;
+using Tui = Terminal.Gui;
+
+namespace Terminal.Gui.Benchmarks.Text.RuneExtensions;
+
+/// <summary>
+/// Benchmarks for <see cref="Tui.RuneExtensions.EncodeSurrogatePair"/> performance fine-tuning.
+/// </summary>
+[MemoryDiagnoser]
+[BenchmarkCategory (nameof (Tui.RuneExtensions))]
+public class EncodeSurrogatePair
+{
+    /// <summary>
+    /// Benchmark for current implementation.
+    /// </summary>
+    [Benchmark (Baseline = true)]
+    [ArgumentsSource (nameof (DataSource))]
+    public Rune Current (char highSurrogate, char lowSurrogate)
+    {
+        _ = Tui.RuneExtensions.EncodeSurrogatePair (highSurrogate, lowSurrogate, out Rune rune);
+        return rune;
+    }
+
+    public static IEnumerable<object []> DataSource ()
+    {
+        string[] runeStrings = ["🍕", "🧠", "🌹"];
+        foreach (string symbol in runeStrings)
+        {
+            if (symbol is [char high, char low])
+            {
+                yield return [high, low];
+            }
+        }
+    }
+}

+ 74 - 0
Benchmarks/Text/RuneExtensions/GetEncodingLength.cs

@@ -0,0 +1,74 @@
+using System.Text;
+using BenchmarkDotNet.Attributes;
+using Tui = Terminal.Gui;
+
+namespace Terminal.Gui.Benchmarks.Text.RuneExtensions;
+
+/// <summary>
+/// Benchmarks for <see cref="Tui.RuneExtensions.GetEncodingLength"/> performance fine-tuning.
+/// </summary>
+[MemoryDiagnoser]
+[BenchmarkCategory (nameof (Tui.RuneExtensions))]
+public class GetEncodingLength
+{
+    /// <summary>
+    /// Benchmark for previous implementation.
+    /// </summary>
+    [Benchmark]
+    [ArgumentsSource (nameof (DataSource))]
+    public int Previous (Rune rune, PrettyPrintedEncoding encoding)
+    {
+        return WithEncodingGetBytesArray (rune, encoding);
+    }
+
+    /// <summary>
+    /// Benchmark for current implementation.
+    /// </summary>
+    [Benchmark (Baseline = true)]
+    [ArgumentsSource (nameof (DataSource))]
+    public int Current (Rune rune, PrettyPrintedEncoding encoding)
+    {
+        return Tui.RuneExtensions.GetEncodingLength (rune, encoding);
+    }
+
+    /// <summary>
+    /// Previous implementation with intermediate byte array, string, and char array allocation.
+    /// </summary>
+    private static int WithEncodingGetBytesArray (Rune rune, Encoding? encoding = null)
+    {
+        encoding ??= Encoding.UTF8;
+        byte [] bytes = encoding.GetBytes (rune.ToString ().ToCharArray ());
+        var offset = 0;
+
+        if (bytes [^1] == 0)
+        {
+            offset++;
+        }
+
+        return bytes.Length - offset;
+    }
+
+    public static IEnumerable<object []> DataSource ()
+    {
+        PrettyPrintedEncoding[] encodings = [ new(Encoding.UTF8), new(Encoding.Unicode), new(Encoding.UTF32) ];
+        Rune[] runes = [ new Rune ('a'), "𝔹".EnumerateRunes ().Single () ];
+
+        foreach (var encoding in encodings)
+        {
+            foreach (Rune rune in runes)
+            {
+                yield return [rune, encoding];
+            }
+        }
+    }
+
+    /// <summary>
+    /// <see cref="System.Text.Encoding"/> wrapper to display proper encoding name in benchmark results.
+    /// </summary>
+    public record PrettyPrintedEncoding (Encoding Encoding)
+    {
+        public static implicit operator Encoding (PrettyPrintedEncoding ppe) => ppe.Encoding;
+
+        public override string ToString () => Encoding.HeaderName;
+    }
+}

+ 50 - 0
Benchmarks/Text/RuneExtensions/IsSurrogatePair.cs

@@ -0,0 +1,50 @@
+using System.Text;
+using BenchmarkDotNet.Attributes;
+using Tui = Terminal.Gui;
+
+namespace Terminal.Gui.Benchmarks.Text.RuneExtensions;
+
+/// <summary>
+/// Benchmarks for <see cref="Tui.RuneExtensions.IsSurrogatePair"/> performance fine-tuning.
+/// </summary>
+[MemoryDiagnoser]
+[BenchmarkCategory (nameof (Tui.RuneExtensions))]
+public class IsSurrogatePair
+{
+    /// <summary>
+    /// Benchmark for previous implementation.
+    /// </summary>
+    /// <param name="rune"></param>
+    [Benchmark]
+    [ArgumentsSource (nameof (DataSource))]
+    public bool Previous (Rune rune)
+    {
+        return WithToString (rune);
+    }
+
+    /// <summary>
+    /// Benchmark for current implementation.
+    /// 
+    /// Avoids intermediate heap allocations by using stack allocated buffer.
+    /// </summary>
+    [Benchmark (Baseline = true)]
+    [ArgumentsSource (nameof (DataSource))]
+    public bool Current (Rune rune)
+    {
+        return Tui.RuneExtensions.IsSurrogatePair (rune);
+    }
+
+    /// <summary>
+    /// Previous implementation with intermediate string allocation.
+    /// </summary>
+    private static bool WithToString (Rune rune)
+    {
+        return char.IsSurrogatePair (rune.ToString (), 0);
+    }
+
+    public static IEnumerable<object> DataSource ()
+    {
+        yield return new Rune ('a');
+        yield return "𝔹".EnumerateRunes ().Single ();
+    }
+}

+ 6 - 2
CommunityToolkitExample/LoginView.cs

@@ -19,15 +19,19 @@ internal partial class LoginView : IRecipient<Message<LoginActions>>
                                      {
                                          ViewModel.Password = passwordInput.Text;
                                      };
-        loginButton.Accepting += (_, _) =>
+        loginButton.Accepting += (_, e) =>
                               {
                                   if (!ViewModel.CanLogin) { return; }
                                   ViewModel.LoginCommand.Execute (null);
+                                  // Anytime Accepting is handled, make sure to set e.Cancel to false.
+                                  e.Cancel = false;
                               };
 
-        clearButton.Accepting += (_, _) =>
+        clearButton.Accepting += (_, e) =>
                               {
                                   ViewModel.ClearCommand.Execute (null);
+                                  // Anytime Accepting is handled, make sure to set e.Cancel to false.
+                                  e.Cancel = false;
                               };
 
         Initialized += (_, _) => { ViewModel.Initialized (); };

+ 2 - 0
Example/Example.cs

@@ -78,6 +78,8 @@ public class ExampleWindow : Window
                                {
                                    MessageBox.ErrorQuery ("Logging In", "Incorrect username or password", "Ok");
                                }
+                               // Anytime Accepting is handled, make sure to set e.Cancel to false.
+                               e.Cancel = false;
                            };
 
         // Add the views to the Window

+ 2 - 0
NativeAot/Program.cs

@@ -105,6 +105,8 @@ public class ExampleWindow : Window
             {
                 MessageBox.ErrorQuery ("Logging In", "Incorrect username or password", "Ok");
             }
+            // Anytime Accepting is handled, make sure to set e.Cancel to false.
+            e.Cancel = false;
         };
 
         // Add the views to the Window

+ 2 - 0
SelfContained/Program.cs

@@ -104,6 +104,8 @@ public class ExampleWindow : Window
                                {
                                    MessageBox.ErrorQuery ("Logging In", "Incorrect username or password", "Ok");
                                }
+                               // Anytime Accepting is handled, make sure to set e.Cancel to false.
+                               e.Cancel = false;
                            };
 
         // Add the views to the Window

+ 36 - 29
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 @@ public static partial class Application // Initialization (Init/Shutdown)
 
         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 @@ public static partial class Application // Initialization (Init/Shutdown)
 
         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 @@ public static partial class Application // Initialization (Init/Shutdown)
     ///     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 partial class Application // Initialization (Init/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);
+    }
 }

+ 0 - 2
Terminal.Gui/Application/Application.Keyboard.cs

@@ -177,7 +177,6 @@ public static partial class Application // Keyboard handling
                         return true;
                     }
                    );
-
         AddCommand (
                     Command.Suspend,
                     static () =>
@@ -187,7 +186,6 @@ public static partial class Application // Keyboard handling
                         return true;
                     }
                    );
-
         AddCommand (
                     Command.NextTabStop,
                     static () => Navigation?.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop));

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

@@ -254,20 +254,20 @@ public static partial class Application // Mouse handling
 
 #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved
     /// <summary>
-    /// Raised when a mouse event occurs. Can be cancelled by setting <see cref="MouseEventArgs.Handled"/> to <see langword="true"/>.
+    /// Raised when a mouse event occurs. Can be cancelled by setting <see cref="HandledEventArgs.Handled"/> to <see langword="true"/>.
     /// </summary>
     /// <remarks>
     ///     <para>
     ///         <see cref="MouseEventArgs.ScreenPosition"/> coordinates are screen-relative.
     ///     </para>
     ///     <para>
-    ///         <see cref="MouseEventArgs.View"/> will be the deepest view under the under the mouse.
+    ///         <see cref="MouseEventArgs.View"/> will be the deepest view under the mouse.
     ///     </para>
     ///     <para>
     ///         <see cref="MouseEventArgs.Position"/> coordinates are view-relative. Only valid if <see cref="MouseEventArgs.View"/> is set.
     ///     </para>
     ///     <para>
-    ///         Use this evento to handle mouse events at the application level, before View-specific handling.
+    ///         Use this even to handle mouse events at the application level, before View-specific handling.
     ///     </para>
     /// </remarks>
     public static event EventHandler<MouseEventArgs>? MouseEvent;

+ 19 - 129
Terminal.Gui/Application/Application.Run.cs

@@ -305,7 +305,8 @@ public static partial class Application // Run (Begin, Run, End, Stop)
     /// <returns>The created <see cref="Toplevel"/> object. The caller is responsible for disposing this object.</returns>
     [RequiresUnreferencedCode ("AOT")]
     [RequiresDynamicCode ("AOT")]
-    public static Toplevel Run (Func<Exception, bool>? errorHandler = null, 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 @@ public static partial class Application // Run (Begin, Run, End, Stop)
     [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 partial class Application // Run (Begin, Run, End, Stop)
     ///     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 partial class Application // Run (Begin, Run, End, Stop)
     ///     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.
@@ -502,6 +411,11 @@ public static partial class Application // Run (Begin, Run, End, Stop)
     /// </summary>
     /// <param name="forceDraw">If <see langword="true"/> the entire View hierarchy will be redrawn. The default is <see langword="false"/> and should only be overriden for testing.</param>
     public static void LayoutAndDraw (bool forceDraw = false)
+    {
+        ApplicationImpl.Instance.LayoutAndDraw (forceDraw);
+    }
+
+    internal static void LayoutAndDrawImpl (bool forceDraw = false)
     {
         bool neededLayout = View.Layout (TopLevels.Reverse (), Screen.Size);
 
@@ -517,8 +431,7 @@ public static partial class Application // Run (Begin, Run, End, Stop)
 
         View.SetClipToScreen ();
         View.Draw (TopLevels, neededLayout || forceDraw);
-        View.SetClipToScreen ();
-
+        View.SetClipToScreen (); 
         Driver?.Refresh ();
     }
 
@@ -528,7 +441,7 @@ public static partial class Application // Run (Begin, Run, End, Stop)
 
     /// <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 +525,8 @@ public static partial class Application // Run (Begin, Run, End, Stop)
     ///         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)
         {

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

+ 7 - 2
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 @@ public static partial class Application
         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);
 }

+ 303 - 0
Terminal.Gui/Application/ApplicationImpl.cs

@@ -0,0 +1,303 @@
+#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 ();
+        LogJsonErrors ();
+        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;
+    }
+
+    /// <inheritdoc />
+    public virtual void LayoutAndDraw (bool forceDraw)
+    {
+        Application.LayoutAndDrawImpl (forceDraw);
+    }
+}

+ 192 - 0
Terminal.Gui/Application/IApplication.cs

@@ -0,0 +1,192 @@
+#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);
+
+    /// <summary>
+    /// Causes any Toplevels that need layout to be laid out. Then draws any Toplevels that need display. Only Views that need to be laid out (see <see cref="View.NeedsLayout"/>) will be laid out.
+    /// Only Views that need to be drawn (see <see cref="View.NeedsDraw"/>) will be drawn.
+    /// </summary>
+    /// <param name="forceDraw">If <see langword="true"/> the entire View hierarchy will be redrawn. The default is <see langword="false"/> and should only be overriden for testing.</param>
+    void LayoutAndDraw (bool forceDraw);
+}

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

+ 10 - 244
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 @@ public class MainLoop : IDisposable
         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 @@ public class MainLoop : IDisposable
     /// <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 class MainLoop : IDisposable
     ///     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 class MainLoop : IDisposable
     // 
     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 @@ public class MainLoop : IDisposable
     /// </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 @@ public class MainLoop : IDisposable
     /// </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 @@ public class MainLoop : IDisposable
         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);
-                }
-            }
-        }
-    }
 }

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

+ 2 - 2
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>

+ 2 - 2
Terminal.Gui/Configuration/AttributeJsonConverter.cs

@@ -57,11 +57,11 @@ internal class AttributeJsonConverter : JsonConverter<Attribute>
             switch (propertyName?.ToLower ())
             {
                 case "foreground":
-                    foreground = JsonSerializer.Deserialize (color, _serializerContext.Color);
+                    foreground = JsonSerializer.Deserialize (color, SerializerContext.Color);
 
                     break;
                 case "background":
-                    background = JsonSerializer.Deserialize (color, _serializerContext.Color);
+                    background = JsonSerializer.Deserialize (color, SerializerContext.Color);
 
                     break;
 

+ 1 - 1
Terminal.Gui/Configuration/ColorSchemeJsonConverter.cs

@@ -59,7 +59,7 @@ internal class ColorSchemeJsonConverter : JsonConverter<ColorScheme>
 
             string propertyName = reader.GetString ();
             reader.Read ();
-            var attribute = JsonSerializer.Deserialize (ref reader, _serializerContext.Attribute);
+            var attribute = JsonSerializer.Deserialize (ref reader, SerializerContext.Attribute);
 
             switch (propertyName.ToLower ())
             {

+ 29 - 23
Terminal.Gui/Configuration/ConfigurationManager.cs

@@ -8,6 +8,7 @@ using System.Runtime.Versioning;
 using System.Text.Encodings.Web;
 using System.Text.Json;
 using System.Text.Json.Serialization;
+using Microsoft.Extensions.Logging;
 
 #nullable enable
 
@@ -65,7 +66,7 @@ public static class ConfigurationManager
     internal static Dictionary<string, ConfigProperty>? _allConfigProperties;
 
     [SuppressMessage ("Style", "IDE1006:Naming Styles", Justification = "<Pending>")]
-    internal static readonly JsonSerializerOptions _serializerOptions = new ()
+    internal static readonly JsonSerializerOptions SerializerOptions = new ()
     {
         ReadCommentHandling = JsonCommentHandling.Skip,
         PropertyNameCaseInsensitive = true,
@@ -87,7 +88,7 @@ public static class ConfigurationManager
     };
 
     [SuppressMessage ("Style", "IDE1006:Naming Styles", Justification = "<Pending>")]
-    internal static readonly SourceGenerationContext _serializerContext = new (_serializerOptions);
+    internal static readonly SourceGenerationContext SerializerContext = new (SerializerOptions);
 
     [SuppressMessage ("Style", "IDE1006:Naming Styles", Justification = "<Pending>")]
     internal static StringBuilder _jsonErrors = new ();
@@ -110,14 +111,6 @@ public static class ConfigurationManager
     [JsonPropertyName ("AppSettings")]
     public static AppScope? AppSettings { get; set; }
 
-    /// <summary>
-    ///     The set of glyphs used to draw checkboxes, lines, borders, etc...See also
-    ///     <seealso cref="Terminal.Gui.GlyphDefinitions"/>.
-    /// </summary>
-    [SerializableConfigurationProperty (Scope = typeof (SettingsScope), OmitClassName = true)]
-    [JsonPropertyName ("Glyphs")]
-    public static GlyphDefinitions Glyphs { get; set; } = new ();
-
     /// <summary>
     ///     Gets and sets the locations where <see cref="ConfigurationManager"/> will look for config files. The value is
     ///     <see cref="ConfigLocations.All"/>.
@@ -217,7 +210,7 @@ public static class ConfigurationManager
         var emptyScope = new SettingsScope ();
         emptyScope.Clear ();
 
-        return JsonSerializer.Serialize (emptyScope, typeof (SettingsScope), _serializerContext);
+        return JsonSerializer.Serialize (emptyScope, typeof (SettingsScope), SerializerContext);
     }
 
     /// <summary>
@@ -243,7 +236,7 @@ public static class ConfigurationManager
     [RequiresDynamicCode ("AOT")]
     public static void Load (bool reset = false)
     {
-        Debug.WriteLine ("ConfigurationManager.Load()");
+        Logging.Trace ($"reset = {reset}");
 
         if (reset)
         {
@@ -300,7 +293,8 @@ public static class ConfigurationManager
     /// </summary>
     public static void OnApplied ()
     {
-        Debug.WriteLine ("ConfigurationManager.OnApplied()");
+        //Logging.Trace ("");
+
         Applied?.Invoke (null, new ());
 
         // TODO: Refactor ConfigurationManager to not use an event handler for this.
@@ -315,7 +309,7 @@ public static class ConfigurationManager
     /// </summary>
     public static void OnUpdated ()
     {
-        Debug.WriteLine (@"ConfigurationManager.OnUpdated()");
+        //Logging.Trace (@"");
         Updated?.Invoke (null, new ());
     }
 
@@ -331,6 +325,18 @@ public static class ConfigurationManager
         }
     }
 
+
+    public static void LogJsonErrors ()
+    {
+        if (_jsonErrors.Length > 0)
+        {
+            Logging.Warning (
+                               @"Encountered the following errors while deserializing configuration files:"
+                              );
+            Logging.Warning (_jsonErrors.ToString ());
+        }
+    }
+
     /// <summary>
     ///     Resets the state of <see cref="ConfigurationManager"/>. Should be called whenever a new app session (e.g. in
     ///     <see cref="Application.Init"/> starts. Called by <see cref="Load"/> if the <c>reset</c> parameter is
@@ -341,7 +347,7 @@ public static class ConfigurationManager
     [RequiresDynamicCode ("AOT")]
     public static void Reset ()
     {
-        Debug.WriteLine (@"ConfigurationManager.Reset()");
+        Logging.Trace ($"_allConfigProperties = {_allConfigProperties}");
 
         if (_allConfigProperties is null)
         {
@@ -376,7 +382,7 @@ public static class ConfigurationManager
 
     internal static void AddJsonError (string error)
     {
-        Debug.WriteLine ($"ConfigurationManager: {error}");
+        Logging.Trace ($"error = {error}");
         _jsonErrors.AppendLine (error);
     }
 
@@ -548,8 +554,8 @@ public static class ConfigurationManager
             classesWithConfigProps.Add (classWithConfig.Name, classWithConfig);
         }
 
-        //Debug.WriteLine ($"ConfigManager.getConfigProperties found {classesWithConfigProps.Count} classes:");
-        classesWithConfigProps.ToList ().ForEach (x => Debug.WriteLine ($"  Class: {x.Key}"));
+        //Logging.Trace ($"ConfigManager.getConfigProperties found {classesWithConfigProps.Count} classes:");
+        classesWithConfigProps.ToList ().ForEach (x => Logging.Trace ($"  Class: {x.Key}"));
 
         foreach (PropertyInfo? p in from c in classesWithConfigProps
                                     let props = c.Value
@@ -602,9 +608,9 @@ public static class ConfigurationManager
                                                                    StringComparer.InvariantCultureIgnoreCase
                                                                   );
 
-        //Debug.WriteLine ($"ConfigManager.Initialize found {_allConfigProperties.Count} properties:");
+        //Logging.Trace ($"Found {_allConfigProperties.Count} properties:");
 
-        //_allConfigProperties.ToList ().ForEach (x => Debug.WriteLine ($"  Property: {x.Key}"));
+        //_allConfigProperties.ToList ().ForEach (x => Logging.Trace ($"  Property: {x.Key}"));
 
         AppSettings = new ();
     }
@@ -615,16 +621,16 @@ public static class ConfigurationManager
     [RequiresDynamicCode ("AOT")]
     internal static string ToJson ()
     {
-        //Debug.WriteLine ("ConfigurationManager.ToJson()");
+        //Logging.Trace ("ConfigurationManager.ToJson()");
 
-        return JsonSerializer.Serialize (Settings!, typeof (SettingsScope), _serializerContext);
+        return JsonSerializer.Serialize (Settings!, typeof (SettingsScope), SerializerContext);
     }
 
     [RequiresUnreferencedCode ("AOT")]
     [RequiresDynamicCode ("AOT")]
     internal static Stream ToStream ()
     {
-        string json = JsonSerializer.Serialize (Settings!, typeof (SettingsScope), _serializerContext);
+        string json = JsonSerializer.Serialize (Settings!, typeof (SettingsScope), SerializerContext);
 
         // turn it into a stream
         var stream = new MemoryStream ();

+ 2 - 2
Terminal.Gui/Configuration/DictionaryJsonConverter.cs

@@ -28,7 +28,7 @@ internal class DictionaryJsonConverter<T> : JsonConverter<Dictionary<string, T>>
                 {
                     string key = reader.GetString ();
                     reader.Read ();
-                    var value = JsonSerializer.Deserialize (ref reader, typeof (T), _serializerContext);
+                    var value = JsonSerializer.Deserialize (ref reader, typeof (T), SerializerContext);
                     dictionary.Add (key, (T)value);
                 }
             }
@@ -51,7 +51,7 @@ internal class DictionaryJsonConverter<T> : JsonConverter<Dictionary<string, T>>
 
             //writer.WriteString (item.Key, item.Key);
             writer.WritePropertyName (item.Key);
-            JsonSerializer.Serialize (writer, item.Value, typeof (T), _serializerContext);
+            JsonSerializer.Serialize (writer, item.Value, typeof (T), SerializerContext);
             writer.WriteEndObject ();
         }
 

+ 1 - 0
Terminal.Gui/Configuration/Scope.cs

@@ -1,4 +1,5 @@
 #nullable enable
+using System.Diagnostics;
 using System.Reflection;
 
 namespace Terminal.Gui;

+ 5 - 5
Terminal.Gui/Configuration/ScopeJsonConverter.cs

@@ -89,11 +89,11 @@ internal class ScopeJsonConverter<[DynamicallyAccessedMembers (DynamicallyAccess
                     try
                     {
                         scope! [propertyName].PropertyValue =
-                            JsonSerializer.Deserialize (ref reader, propertyType!, _serializerContext);
+                            JsonSerializer.Deserialize (ref reader, propertyType!, SerializerContext);
                     }
                     catch (Exception ex)
                     {
-                        Debug.WriteLine ($"scopeT Read: {ex}");
+                       // Logging.Trace ($"scopeT Read: {ex}");
                     }
                 }
             }
@@ -137,7 +137,7 @@ internal class ScopeJsonConverter<[DynamicallyAccessedMembers (DynamicallyAccess
                 if (property is { })
                 {
                     PropertyInfo prop = scope.GetType ().GetProperty (propertyName!)!;
-                    prop.SetValue (scope, JsonSerializer.Deserialize (ref reader, prop.PropertyType, _serializerContext));
+                    prop.SetValue (scope, JsonSerializer.Deserialize (ref reader, prop.PropertyType, SerializerContext));
                 }
                 else
                 {
@@ -165,7 +165,7 @@ internal class ScopeJsonConverter<[DynamicallyAccessedMembers (DynamicallyAccess
         {
             writer.WritePropertyName (ConfigProperty.GetJsonPropertyName (p));
             object? prop = scope.GetType ().GetProperty (p.Name)?.GetValue (scope);
-            JsonSerializer.Serialize (writer, prop, prop!.GetType (), _serializerContext);
+            JsonSerializer.Serialize (writer, prop, prop!.GetType (), SerializerContext);
         }
 
         foreach (KeyValuePair<string, ConfigProperty> p in from p in scope
@@ -211,7 +211,7 @@ internal class ScopeJsonConverter<[DynamicallyAccessedMembers (DynamicallyAccess
             else
             {
                 object? prop = p.Value.PropertyValue;
-                JsonSerializer.Serialize (writer, prop, prop!.GetType (), _serializerContext);
+                JsonSerializer.Serialize (writer, prop, prop!.GetType (), SerializerContext);
             }
         }
 

+ 4 - 4
Terminal.Gui/Configuration/SettingsScope.cs

@@ -45,9 +45,9 @@ public class SettingsScope : Scope<SettingsScope>
         // Update the existing settings with the new settings.
         try
         {
-            Update ((SettingsScope)JsonSerializer.Deserialize (stream, typeof (SettingsScope), _serializerOptions)!);
+            Update ((SettingsScope)JsonSerializer.Deserialize (stream, typeof (SettingsScope), SerializerOptions)!);
             OnUpdated ();
-            Debug.WriteLine ($"ConfigurationManager: Read configuration from \"{source}\"");
+            Logging.Trace ($"Read from \"{source}\"");
             if (!Sources.ContainsValue (source))
             {
                 Sources.Add (location, source);
@@ -79,7 +79,7 @@ public class SettingsScope : Scope<SettingsScope>
 
         if (!File.Exists (realPath))
         {
-            Debug.WriteLine ($"ConfigurationManager: Configuration file \"{realPath}\" does not exist.");
+            Logging.Warning ($"\"{realPath}\" does not exist.");
             if (!Sources.ContainsValue (filePath))
             {
                 Sources.Add (location, filePath);
@@ -105,7 +105,7 @@ public class SettingsScope : Scope<SettingsScope>
             }
             catch (IOException ioe)
             {
-                Debug.WriteLine ($"Couldn't open {filePath}. Retrying...: {ioe}");
+                Logging.Warning ($"Couldn't open {filePath}. Retrying...: {ioe}");
                 Task.Delay (100);
                 retryCount++;
             }

+ 1 - 1
Terminal.Gui/Configuration/SourceGenerationContext.cs

@@ -10,7 +10,7 @@ namespace Terminal.Gui;
 [JsonSerializable (typeof (AppScope))]
 [JsonSerializable (typeof (SettingsScope))]
 [JsonSerializable (typeof (Key))]
-[JsonSerializable (typeof (GlyphDefinitions))]
+[JsonSerializable (typeof (Glyphs))]
 [JsonSerializable (typeof (Alignment))]
 [JsonSerializable (typeof (AlignmentModes))]
 [JsonSerializable (typeof (LineStyle))]

+ 3 - 3
Terminal.Gui/Configuration/ThemeManager.cs

@@ -127,7 +127,7 @@ public class ThemeManager : IDictionary<string, ThemeScope>
     [RequiresDynamicCode ("Calls Terminal.Gui.ThemeManager.Themes")]
     internal static void GetHardCodedDefaults ()
     {
-        //Debug.WriteLine ("Themes.GetHardCodedDefaults()");
+        //Logging.Trace ("Themes.GetHardCodedDefaults()");
         var theme = new ThemeScope ();
         theme.RetrieveValues ();
 
@@ -141,7 +141,7 @@ public class ThemeManager : IDictionary<string, ThemeScope>
     /// <summary>Called when the selected theme has changed. Fires the <see cref="ThemeChanged"/> event.</summary>
     internal void OnThemeChanged (string theme)
     {
-        //Debug.WriteLine ($"Themes.OnThemeChanged({theme}) -> {Theme}");
+        //Logging.Trace ($"Themes.OnThemeChanged({theme}) -> {Theme}");
         ThemeChanged?.Invoke (this, new ThemeManagerEventArgs (theme));
     }
 
@@ -149,7 +149,7 @@ public class ThemeManager : IDictionary<string, ThemeScope>
     [RequiresDynamicCode ("Calls Terminal.Gui.ThemeManager.Themes")]
     internal static void Reset ()
     {
-        Debug.WriteLine ("Themes.Reset()");
+        //Logging.Trace ("Themes.Reset()");
         Colors.Reset ();
         Themes?.Clear ();
         SelectedTheme = string.Empty;

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

+ 2 - 2
Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiRequestScheduler.cs

@@ -68,7 +68,7 @@ public class AnsiRequestScheduler
 
     /// <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 @@ public class AnsiRequestScheduler
 
         return false;
     }
-}
+}

+ 171 - 13
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 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser
             char currentChar = getCharAtIndex (index);
             object currentObj = getObjectAtIndex (index);
 
-            bool isEscape = currentChar == '\x1B';
+            bool isEscape = currentChar == Escape;
 
             switch (State)
             {
@@ -118,7 +145,7 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser
                     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 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser
 
                     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 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser
                     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 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser
 
     private void ReleaseHeld (Action<object> appendOutput, AnsiResponseParserState newState = AnsiResponseParserState.Normal)
     {
+        TryLastMinuteSequences ();
+
         foreach (object o in _heldContent.HeldToObjects ())
         {
             appendOutput (o);
@@ -178,6 +223,48 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser
         _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 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser
         {
             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 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser
                 {
                     _heldContent.ClearHeld ();
 
+                    Logging.Trace ($"AnsiResponseParser swallowed '{cur}'");
+
                     // Do not send back to input stream
                     return false;
                 }
@@ -244,6 +354,32 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser
         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 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser
 
         if (matchingResponse?.Response != null)
         {
+            Logging.Trace ($"AnsiResponseParser processed '{cur}'");
+
             if (invokeCallback)
             {
                 matchingResponse.Response.Invoke (_heldContent);
@@ -339,8 +477,10 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser
     }
 }
 
-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 @@ internal class AnsiResponseParser<T> () : AnsiResponseParserBase (new GenericHel
         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 @@ internal class AnsiResponseParser () : AnsiResponseParserBase (new StringHeld ()
         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 ();
 

+ 3 - 2
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

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

+ 1 - 1
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
 {

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

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

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

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

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

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

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

+ 7 - 7
Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs

@@ -4,7 +4,7 @@ using System.Diagnostics;
 
 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 abstract class ConsoleDriver : IConsoleDriver
 
     #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 abstract class ConsoleDriver : IConsoleDriver
         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);

+ 1 - 1
Terminal.Gui/ConsoleDrivers/CursesDriver/UnixMainLoop.cs

@@ -102,7 +102,7 @@ internal class UnixMainLoop : IMainLoopDriver
 
         UpdatePollMap ();
 
-        bool checkTimersResult = _mainLoop!.CheckTimersAndIdleHandlers (out int pollTimeout);
+        bool checkTimersResult = _mainLoop!.TimedEvents.CheckTimersAndIdleHandlers (out int pollTimeout);
 
         int n = poll (_pollMap!, (uint)_pollMap!.Length, pollTimeout);
 

+ 101 - 70
Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqUtils.cs

@@ -411,25 +411,25 @@ public static class EscSeqUtils
     {
         // These control characters are used in the vtXXX emulation.
         return c switch
-               {
-                   'D' => "IND", // Index
-                   'E' => "NEL", // Next Line
-                   'H' => "HTS", // Tab Set
-                   'M' => "RI", // Reverse Index
-                   'N' => "SS2", // Single Shift Select of G2 Character Set: affects next character only
-                   'O' => "SS3", // Single Shift Select of G3 Character Set: affects next character only
-                   'P' => "DCS", // Device Control String
-                   'V' => "SPA", // Start of Guarded Area
-                   'W' => "EPA", // End of Guarded Area
-                   'X' => "SOS", // Start of String
-                   'Z' => "DECID", // Return Terminal ID Obsolete form of CSI c (DA)
-                   '[' => "CSI", // Control Sequence Introducer
-                   '\\' => "ST", // String Terminator
-                   ']' => "OSC", // Operating System Command
-                   '^' => "PM", // Privacy Message
-                   '_' => "APC", // Application Program Command
-                   _ => string.Empty
-               };
+        {
+            'D' => "IND", // Index
+            'E' => "NEL", // Next Line
+            'H' => "HTS", // Tab Set
+            'M' => "RI", // Reverse Index
+            'N' => "SS2", // Single Shift Select of G2 Character Set: affects next character only
+            'O' => "SS3", // Single Shift Select of G3 Character Set: affects next character only
+            'P' => "DCS", // Device Control String
+            'V' => "SPA", // Start of Guarded Area
+            'W' => "EPA", // End of Guarded Area
+            'X' => "SOS", // Start of String
+            'Z' => "DECID", // Return Terminal ID Obsolete form of CSI c (DA)
+            '[' => "CSI", // Control Sequence Introducer
+            '\\' => "ST", // String Terminator
+            ']' => "OSC", // Operating System Command
+            '^' => "PM", // Privacy Message
+            '_' => "APC", // Application Program Command
+            _ => string.Empty
+        };
     }
 
 
@@ -462,46 +462,46 @@ public static class EscSeqUtils
         }
 
         return (terminator, value) switch
-               {
-                   ('A', _) => ConsoleKey.UpArrow,
-                   ('B', _) => ConsoleKey.DownArrow,
-                   ('C', _) => ConsoleKey.RightArrow,
-                   ('D', _) => ConsoleKey.LeftArrow,
-                   ('E', _) => ConsoleKey.Clear,
-                   ('F', _) => ConsoleKey.End,
-                   ('H', _) => ConsoleKey.Home,
-                   ('P', _) => ConsoleKey.F1,
-                   ('Q', _) => ConsoleKey.F2,
-                   ('R', _) => ConsoleKey.F3,
-                   ('S', _) => ConsoleKey.F4,
-                   ('Z', _) => ConsoleKey.Tab,
-                   ('~', "2") => ConsoleKey.Insert,
-                   ('~', "3") => ConsoleKey.Delete,
-                   ('~', "5") => ConsoleKey.PageUp,
-                   ('~', "6") => ConsoleKey.PageDown,
-                   ('~', "15") => ConsoleKey.F5,
-                   ('~', "17") => ConsoleKey.F6,
-                   ('~', "18") => ConsoleKey.F7,
-                   ('~', "19") => ConsoleKey.F8,
-                   ('~', "20") => ConsoleKey.F9,
-                   ('~', "21") => ConsoleKey.F10,
-                   ('~', "23") => ConsoleKey.F11,
-                   ('~', "24") => ConsoleKey.F12,
-                   // These terminators are used by macOS on a numeric keypad without keys modifiers
-                   ('l', null) => ConsoleKey.Add,
-                   ('m', null) => ConsoleKey.Subtract,
-                   ('p', null) => ConsoleKey.Insert,
-                   ('q', null) => ConsoleKey.End,
-                   ('r', null) => ConsoleKey.DownArrow,
-                   ('s', null) => ConsoleKey.PageDown,
-                   ('t', null) => ConsoleKey.LeftArrow,
-                   ('u', null) => ConsoleKey.Clear,
-                   ('v', null) => ConsoleKey.RightArrow,
-                   ('w', null) => ConsoleKey.Home,
-                   ('x', null) => ConsoleKey.UpArrow,
-                   ('y', null) => ConsoleKey.PageUp,
-                   (_, _) => 0
-               };
+        {
+            ('A', _) => ConsoleKey.UpArrow,
+            ('B', _) => ConsoleKey.DownArrow,
+            ('C', _) => ConsoleKey.RightArrow,
+            ('D', _) => ConsoleKey.LeftArrow,
+            ('E', _) => ConsoleKey.Clear,
+            ('F', _) => ConsoleKey.End,
+            ('H', _) => ConsoleKey.Home,
+            ('P', _) => ConsoleKey.F1,
+            ('Q', _) => ConsoleKey.F2,
+            ('R', _) => ConsoleKey.F3,
+            ('S', _) => ConsoleKey.F4,
+            ('Z', _) => ConsoleKey.Tab,
+            ('~', "2") => ConsoleKey.Insert,
+            ('~', "3") => ConsoleKey.Delete,
+            ('~', "5") => ConsoleKey.PageUp,
+            ('~', "6") => ConsoleKey.PageDown,
+            ('~', "15") => ConsoleKey.F5,
+            ('~', "17") => ConsoleKey.F6,
+            ('~', "18") => ConsoleKey.F7,
+            ('~', "19") => ConsoleKey.F8,
+            ('~', "20") => ConsoleKey.F9,
+            ('~', "21") => ConsoleKey.F10,
+            ('~', "23") => ConsoleKey.F11,
+            ('~', "24") => ConsoleKey.F12,
+            // These terminators are used by macOS on a numeric keypad without keys modifiers
+            ('l', null) => ConsoleKey.Add,
+            ('m', null) => ConsoleKey.Subtract,
+            ('p', null) => ConsoleKey.Insert,
+            ('q', null) => ConsoleKey.End,
+            ('r', null) => ConsoleKey.DownArrow,
+            ('s', null) => ConsoleKey.PageDown,
+            ('t', null) => ConsoleKey.LeftArrow,
+            ('u', null) => ConsoleKey.Clear,
+            ('v', null) => ConsoleKey.RightArrow,
+            ('w', null) => ConsoleKey.Home,
+            ('x', null) => ConsoleKey.UpArrow,
+            ('y', null) => ConsoleKey.PageUp,
+            (_, _) => 0
+        };
     }
 
     /// <summary>
@@ -512,18 +512,18 @@ public static class EscSeqUtils
     public static ConsoleModifiers GetConsoleModifiers (string? value)
     {
         return value switch
-               {
-                   "2" => ConsoleModifiers.Shift,
-                   "3" => ConsoleModifiers.Alt,
-                   "4" => ConsoleModifiers.Shift | ConsoleModifiers.Alt,
-                   "5" => ConsoleModifiers.Control,
-                   "6" => ConsoleModifiers.Shift | ConsoleModifiers.Control,
-                   "7" => ConsoleModifiers.Alt | ConsoleModifiers.Control,
-                   "8" => ConsoleModifiers.Shift | ConsoleModifiers.Alt | ConsoleModifiers.Control,
-                   _ => 0
-               };
+        {
+            "2" => ConsoleModifiers.Shift,
+            "3" => ConsoleModifiers.Alt,
+            "4" => ConsoleModifiers.Shift | ConsoleModifiers.Alt,
+            "5" => ConsoleModifiers.Control,
+            "6" => ConsoleModifiers.Shift | ConsoleModifiers.Control,
+            "7" => ConsoleModifiers.Alt | ConsoleModifiers.Control,
+            "8" => ConsoleModifiers.Shift | ConsoleModifiers.Alt | ConsoleModifiers.Control,
+            _ => 0
+        };
     }
-    #nullable restore
+#nullable restore
 
     /// <summary>
     ///     Gets all the needed information about an escape sequence.
@@ -1675,6 +1675,19 @@ public static class EscSeqUtils
     /// <returns></returns>
     public static string CSI_SetCursorPosition (int row, int col) { return $"{CSI}{row};{col}H"; }
 
+    /// <summary>
+    ///     ESC [ y ; x H - CUP Cursor Position - Cursor moves to x ; y coordinate within the viewport, where x is the column
+    ///     of the y line
+    /// </summary>
+    /// <param name="builder">StringBuilder where to append the cursor position sequence.</param>
+    /// <param name="row">Origin is (1,1).</param>
+    /// <param name="col">Origin is (1,1).</param>
+    public static void CSI_AppendCursorPosition (StringBuilder builder, int row, int col)
+    {
+        // InterpolatedStringHandler is composed in stack, skipping the string allocation.
+        builder.Append ($"{CSI}{row};{col}H");
+    }
+
     //ESC [ <y> ; <x> f - HVP     Horizontal Vertical Position* Cursor moves to<x>; <y> coordinate within the viewport, where <x> is the column of the<y> line
     //ESC [ s - ANSISYSSC       Save Cursor – Ansi.sys emulation	**With no parameters, performs a save cursor operation like DECSC
     //ESC [ u - ANSISYSRC       Restore Cursor – Ansi.sys emulation	**With no parameters, performs a restore cursor operation like DECRC
@@ -1785,11 +1798,29 @@ public static class EscSeqUtils
     /// </summary>
     public static string CSI_SetForegroundColorRGB (int r, int g, int b) { return $"{CSI}38;2;{r};{g};{b}m"; }
 
+    /// <summary>
+    ///     ESC[38;2;{r};{g};{b}m	Append foreground color as RGB to StringBuilder.
+    /// </summary>
+    public static void CSI_AppendForegroundColorRGB (StringBuilder builder, int r, int g, int b)
+    {
+        // InterpolatedStringHandler is composed in stack, skipping the string allocation.
+        builder.Append ($"{CSI}38;2;{r};{g};{b}m");
+    }
+
     /// <summary>
     ///     ESC[48;2;{r};{g};{b}m	Set background color as RGB.
     /// </summary>
     public static string CSI_SetBackgroundColorRGB (int r, int g, int b) { return $"{CSI}48;2;{r};{g};{b}m"; }
 
+    /// <summary>
+    ///     ESC[48;2;{r};{g};{b}m	Append background color as RGB to StringBuilder.
+    /// </summary>
+    public static void CSI_AppendBackgroundColorRGB (StringBuilder builder, int r, int g, int b)
+    {
+        // InterpolatedStringHandler is composed in stack, skipping the string allocation.
+        builder.Append ($"{CSI}48;2;{r};{g};{b}m");
+    }
+
     #endregion
 
     #region Requests

+ 2 - 2
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 @@ using Terminal.Gui.ConsoleDrivers;
 
 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

+ 8 - 60
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>

+ 2 - 2
Terminal.Gui/ConsoleDrivers/NetDriver/NetEvents.cs

@@ -96,8 +96,8 @@ internal class NetEvents : IDisposable
 
     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);
         }

+ 2 - 2
Terminal.Gui/ConsoleDrivers/NetDriver/NetMainLoop.cs

@@ -55,7 +55,7 @@ internal class NetMainLoop : IMainLoopDriver
 
         _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 @@ internal class NetMainLoop : IMainLoopDriver
 
         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

+ 243 - 0
Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs

@@ -0,0 +1,243 @@
+#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); }
+
+    /// <inheritdoc />
+    public override void LayoutAndDraw (bool forceDraw)
+    {
+        // No more ad-hoc drawing, you must wait for iteration to do it
+        Application.Top?.SetNeedsDraw();
+        Application.Top?.SetNeedsLayout ();
+    }
+}

+ 388 - 0
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
+    }
+}

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

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

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

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

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

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

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

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

+ 4 - 0
Terminal.Gui/ConsoleDrivers/V2/INetInput.cs

@@ -0,0 +1,4 @@
+namespace Terminal.Gui;
+
+internal interface INetInput : IConsoleInput<ConsoleKeyInfo>
+{ }

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

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

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

+ 4 - 0
Terminal.Gui/ConsoleDrivers/V2/IWindowsInput.cs

@@ -0,0 +1,4 @@
+namespace Terminal.Gui;
+
+internal interface IWindowsInput : IConsoleInput<WindowsConsole.InputRecord>
+{ }

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

+ 148 - 0
Terminal.Gui/ConsoleDrivers/V2/Logging.cs

@@ -0,0 +1,148 @@
+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 an error message including the class and method name.
+    /// </summary>
+    /// <param name="message"></param>
+    /// <param name="caller"></param>
+    /// <param name="filePath"></param>
+    public static void Error (
+        string message,
+        [CallerMemberName] string caller = "",
+        [CallerFilePath] string filePath = ""
+    )
+    {
+        string className = Path.GetFileNameWithoutExtension (filePath);
+        Logger.LogError ($"[{className}] [{caller}] {message}");
+    }
+
+    /// <summary>
+    ///     Logs a fatal/critical message including the class and method name.
+    /// </summary>
+    /// <param name="message"></param>
+    /// <param name="caller"></param>
+    /// <param name="filePath"></param>
+    public static void Critical (
+        string message,
+        [CallerMemberName] string caller = "",
+        [CallerFilePath] string filePath = ""
+    )
+    {
+        string className = Path.GetFileNameWithoutExtension (filePath);
+        Logger.LogCritical ($"[{className}] [{caller}] {message}");
+    }
+
+    /// <summary>
+    ///     Logs a debug message including the class and method name.
+    /// </summary>
+    /// <param name="message"></param>
+    /// <param name="caller"></param>
+    /// <param name="filePath"></param>
+    public static void Debug (
+        string message,
+        [CallerMemberName] string caller = "",
+        [CallerFilePath] string filePath = ""
+    )
+    {
+        string className = Path.GetFileNameWithoutExtension (filePath);
+        Logger.LogDebug ($"[{className}] [{caller}] {message}");
+    }
+
+    /// <summary>
+    ///     Logs an informational message including the class and method name.
+    /// </summary>
+    /// <param name="message"></param>
+    /// <param name="caller"></param>
+    /// <param name="filePath"></param>
+    public static void Information (
+        string message,
+        [CallerMemberName] string caller = "",
+        [CallerFilePath] string filePath = ""
+    )
+    {
+        string className = Path.GetFileNameWithoutExtension (filePath);
+        Logger.LogInformation ($"[{className}] [{caller}] {message}");
+    }
+
+    /// <summary>
+    ///     Logs a trace/verbose message including the class and method name.
+    /// </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}");
+    }
+
+    /// <summary>
+    ///     Logs a warning message including the class and method name.
+    /// </summary>
+    /// <param name="message"></param>
+    /// <param name="caller"></param>
+    /// <param name="filePath"></param>
+    public static void Warning (
+        string message,
+        [CallerMemberName] string caller = "",
+        [CallerFilePath] string filePath = ""
+    )
+    {
+        string className = Path.GetFileNameWithoutExtension (filePath);
+        Logger.LogWarning ($"[{className}] [{caller}] {message}");
+    }
+}

+ 202 - 0
Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs

@@ -0,0 +1,202 @@
+#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);
+
+                Application.LayoutAndDrawImpl (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
+    }
+}

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

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

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

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

+ 59 - 0
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}";
+    }
+}

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

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

+ 22 - 0
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) { }
+}

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

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

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

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

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

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

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

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

+ 14 - 14
Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsConsole.cs

@@ -36,7 +36,7 @@ internal class WindowsConsole
         newConsoleMode &= ~(uint)ConsoleModes.EnableProcessedInput;
         ConsoleMode = newConsoleMode;
 
-        _inputReadyCancellationTokenSource = new ();
+        _inputReadyCancellationTokenSource = new (); 
         Task.Run (ProcessInputQueue, _inputReadyCancellationTokenSource.Token);
     }
 
@@ -176,7 +176,7 @@ internal class WindowsConsole
             _stringBuilder.Clear ();
 
             _stringBuilder.Append (EscSeqUtils.CSI_SaveCursorPosition);
-            _stringBuilder.Append (EscSeqUtils.CSI_SetCursorPosition (0, 0));
+            EscSeqUtils.CSI_AppendCursorPosition (_stringBuilder, 0, 0);
 
             Attribute? prev = null;
 
@@ -187,8 +187,8 @@ internal class WindowsConsole
                 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));
+                    EscSeqUtils.CSI_AppendForegroundColorRGB (_stringBuilder, attr.Foreground.R, attr.Foreground.G, attr.Foreground.B);
+                    EscSeqUtils.CSI_AppendBackgroundColorRGB (_stringBuilder, attr.Background.R, attr.Background.G, attr.Background.B);
                 }
 
                 if (info.Char != '\x1b')
@@ -334,7 +334,7 @@ internal class WindowsConsole
             visibility = CursorVisibility.Default;
         }
 
-        return true;
+        return visibility != CursorVisibility.Invisible;
     }
 
     public bool EnsureCursorVisibility ()
@@ -710,14 +710,14 @@ internal class WindowsConsole
         public readonly override string ToString ()
         {
             return (EventType switch
-                    {
-                        EventType.Focus => FocusEvent.ToString (),
-                        EventType.Key => KeyEvent.ToString (),
-                        EventType.Menu => MenuEvent.ToString (),
-                        EventType.Mouse => MouseEvent.ToString (),
-                        EventType.WindowBufferSize => WindowBufferSizeEvent.ToString (),
-                        _ => "Unknown event type: " + EventType
-                    })!;
+            {
+                EventType.Focus => FocusEvent.ToString (),
+                EventType.Key => KeyEvent.ToString (),
+                EventType.Menu => MenuEvent.ToString (),
+                EventType.Mouse => MouseEvent.ToString (),
+                EventType.WindowBufferSize => WindowBufferSizeEvent.ToString (),
+                _ => "Unknown event type: " + EventType
+            })!;
         }
     }
 
@@ -918,7 +918,7 @@ internal class WindowsConsole
 
     // 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,

+ 14 - 7
Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsDriver.cs

@@ -70,7 +70,7 @@ internal class WindowsDriver : ConsoleDriver
 
     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 @@ internal class WindowsDriver : ConsoleDriver
 
     #endregion
 
-    public WindowsConsole.ConsoleKeyInfoEx ToConsoleKeyInfoEx (WindowsConsole.KeyEventRecord keyEvent)
+    public static WindowsConsole.ConsoleKeyInfoEx ToConsoleKeyInfoEx (WindowsConsole.KeyEventRecord keyEvent)
     {
         WindowsConsole.ControlKeyState state = keyEvent.dwControlKeyState;
 
@@ -252,7 +252,7 @@ internal class WindowsDriver : ConsoleDriver
         else
         {
             var sb = new StringBuilder ();
-            sb.Append (EscSeqUtils.CSI_SetCursorPosition (position.Y + 1, position.X + 1));
+            EscSeqUtils.CSI_AppendCursorPosition (sb, position.Y + 1, position.X + 1);
             WinConsole?.WriteANSI (sb.ToString ());
         }
 
@@ -268,12 +268,19 @@ internal class WindowsDriver : ConsoleDriver
     {
         if (WinConsole is { })
         {
-            return WinConsole.GetCursorVisibility (out visibility);
+            bool result = WinConsole.GetCursorVisibility (out visibility);
+
+            if (_cachedCursorVisibility is { } && visibility != _cachedCursorVisibility)
+            {
+                _cachedCursorVisibility = visibility;
+            }
+
+            return result;
         }
 
         visibility = _cachedCursorVisibility ?? CursorVisibility.Default;
 
-        return true;
+        return visibility != CursorVisibility.Invisible;
     }
 
     /// <inheritdoc/>
@@ -575,7 +582,7 @@ internal class WindowsDriver : ConsoleDriver
 
     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);
@@ -620,7 +627,7 @@ internal class WindowsDriver : ConsoleDriver
     }
 #endif
 
-    private KeyCode MapKey (WindowsConsole.ConsoleKeyInfoEx keyInfoEx)
+    public static KeyCode MapKey (WindowsConsole.ConsoleKeyInfoEx keyInfoEx)
     {
         ConsoleKeyInfo keyInfo = keyInfoEx.ConsoleKeyInfo;
 

+ 3 - 3
Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsMainLoop.cs

@@ -68,7 +68,7 @@ internal class WindowsMainLoop : IMainLoopDriver
 #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 @@ internal class WindowsMainLoop : IMainLoopDriver
         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
         }
 

+ 190 - 151
Terminal.Gui/Drawing/Glyphs.cs

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

+ 175 - 117
Terminal.Gui/Drawing/LineCanvas/LineCanvas.cs

@@ -1,4 +1,7 @@
 #nullable enable
+using System.Buffers;
+using System.Runtime.InteropServices;
+
 namespace Terminal.Gui;
 
 /// <summary>Facilitates box drawing and line intersection detection and rendering. Does not support diagonal lines.</summary>
@@ -161,18 +164,25 @@ public class LineCanvas : IDisposable
     {
         Dictionary<Point, Cell?> map = new ();
 
+        List<IntersectionDefinition> intersectionsBufferList = [];
+
         // walk through each pixel of the bitmap
         for (int y = Bounds.Y; y < Bounds.Y + Bounds.Height; y++)
         {
             for (int x = Bounds.X; x < Bounds.X + Bounds.Width; x++)
             {
-                IntersectionDefinition? [] intersects = _lines
-                                                        .Select (l => l.Intersects (x, y))
-                                                        .Where (i => i is { })
-                                                        .ToArray ();
-
+                intersectionsBufferList.Clear ();
+                foreach (var line in _lines)
+                {
+                    if (line.Intersects (x, y) is IntersectionDefinition intersect)
+                    {
+                        intersectionsBufferList.Add (intersect);
+                    }
+                }
+                // Safe as long as the list is not modified while the span is in use.
+                ReadOnlySpan<IntersectionDefinition> intersects = CollectionsMarshal.AsSpan(intersectionsBufferList);
                 Cell? cell = GetCellForIntersects (Application.Driver, intersects);
-
+                // TODO: Can we skip the whole nested looping if _exclusionRegion is null?
                 if (cell is { } && _exclusionRegion?.Contains (x, y) is null or false)
                 {
                     map.Add (new (x, y), cell);
@@ -207,10 +217,11 @@ public class LineCanvas : IDisposable
         {
             for (int x = inArea.X; x < inArea.X + inArea.Width; x++)
             {
-                IntersectionDefinition? [] intersects = _lines
-                                                        .Select (l => l.Intersects (x, y))
-                                                        .Where (i => i is { })
-                                                        .ToArray ();
+                IntersectionDefinition [] intersects = _lines
+                    // ! nulls are filtered out by the next Where filter
+                    .Select (l => l.Intersects (x, y)!)
+                    .Where (i => i is not null)
+                    .ToArray ();
 
                 Rune? rune = GetRuneForIntersects (Application.Driver, intersects);
 
@@ -315,9 +326,16 @@ public class LineCanvas : IDisposable
         return sb.ToString ();
     }
 
-    private static bool All (IntersectionDefinition? [] intersects, Orientation orientation)
+    private static bool All (ReadOnlySpan<IntersectionDefinition> intersects, Orientation orientation)
     {
-        return intersects.All (i => i!.Line.Orientation == orientation);
+        foreach (var intersect in intersects)
+        {
+            if (intersect.Line.Orientation != orientation)
+            {
+                return false;
+            }
+        }
+        return true;
     }
 
     private void ConfigurationManager_Applied (object? sender, ConfigurationManagerEventArgs e)
@@ -337,9 +355,9 @@ public class LineCanvas : IDisposable
     /// <returns></returns>
     private static bool Exactly (HashSet<IntersectionType> intersects, params IntersectionType [] types) { return intersects.SetEquals (types); }
 
-    private Attribute? GetAttributeForIntersects (IntersectionDefinition? [] intersects)
+    private Attribute? GetAttributeForIntersects (ReadOnlySpan<IntersectionDefinition> intersects)
     {
-        return Fill?.GetAttribute (intersects [0]!.Point) ?? intersects [0]!.Line.Attribute;
+        return Fill?.GetAttribute (intersects [0].Point) ?? intersects [0].Line.Attribute;
     }
 
     private readonly Dictionary<IntersectionRuneType, IntersectionRuneResolver> _runeResolvers = new ()
@@ -384,9 +402,9 @@ public class LineCanvas : IDisposable
         // TODO: Add other resolvers
     };
 
-    private Cell? GetCellForIntersects (IConsoleDriver? driver, IntersectionDefinition? [] intersects)
+    private Cell? GetCellForIntersects (IConsoleDriver? driver, ReadOnlySpan<IntersectionDefinition> intersects)
     {
-        if (!intersects.Any ())
+        if (intersects.IsEmpty)
         {
             return null;
         }
@@ -404,37 +422,28 @@ public class LineCanvas : IDisposable
         return cell;
     }
 
-    private Rune? GetRuneForIntersects (IConsoleDriver? driver, IntersectionDefinition? [] intersects)
+    private Rune? GetRuneForIntersects (IConsoleDriver? driver, ReadOnlySpan<IntersectionDefinition> intersects)
     {
-        if (!intersects.Any ())
+        if (intersects.IsEmpty)
         {
             return null;
         }
 
         IntersectionRuneType runeType = GetRuneTypeForIntersects (intersects);
-
         if (_runeResolvers.TryGetValue (runeType, out IntersectionRuneResolver? resolver))
         {
             return resolver.GetRuneForIntersects (driver, intersects);
         }
 
         // TODO: Remove these once we have all of the below ported to IntersectionRuneResolvers
-        bool useDouble = intersects.Any (i => i?.Line.Style == LineStyle.Double);
-
-        bool useDashed = intersects.Any (
-                                         i => i?.Line.Style == LineStyle.Dashed
-                                              || i?.Line.Style == LineStyle.RoundedDashed
-                                        );
-
-        bool useDotted = intersects.Any (
-                                         i => i?.Line.Style == LineStyle.Dotted
-                                              || i?.Line.Style == LineStyle.RoundedDotted
-                                        );
+        bool useDouble = AnyLineStyles(intersects, [LineStyle.Double]);
+        bool useDashed = AnyLineStyles(intersects, [LineStyle.Dashed, LineStyle.RoundedDashed]);
+        bool useDotted = AnyLineStyles(intersects, [LineStyle.Dotted, LineStyle.RoundedDotted]);
 
         // horiz and vert lines same as Single for Rounded
-        bool useThick = intersects.Any (i => i?.Line.Style == LineStyle.Heavy);
-        bool useThickDashed = intersects.Any (i => i?.Line.Style == LineStyle.HeavyDashed);
-        bool useThickDotted = intersects.Any (i => i?.Line.Style == LineStyle.HeavyDotted);
+        bool useThick = AnyLineStyles(intersects, [LineStyle.Heavy]);
+        bool useThickDashed = AnyLineStyles(intersects, [LineStyle.HeavyDashed]);
+        bool useThickDotted = AnyLineStyles(intersects, [LineStyle.HeavyDotted]);
 
         // TODO: Support ruler
         //var useRuler = intersects.Any (i => i.Line.Style == LineStyle.Ruler && i.Line.Length != 0);
@@ -493,18 +502,38 @@ public class LineCanvas : IDisposable
                            + runeType
                           );
         }
+
+
+        static bool AnyLineStyles (ReadOnlySpan<IntersectionDefinition> intersects, ReadOnlySpan<LineStyle> lineStyles)
+        {
+            foreach (IntersectionDefinition intersect in intersects)
+            {
+                foreach (LineStyle style in lineStyles)
+                {
+                    if (intersect.Line.Style == style)
+                    {
+                        return true;
+                    }
+                }
+            }
+            return false;
+        }
     }
 
-    private IntersectionRuneType GetRuneTypeForIntersects (IntersectionDefinition? [] intersects)
+    private IntersectionRuneType GetRuneTypeForIntersects (ReadOnlySpan<IntersectionDefinition> intersects)
     {
-        HashSet<IntersectionType> set = new (intersects.Select (i => i!.Type));
+        HashSet<IntersectionType> set = new (capacity: intersects.Length);
+        foreach (var intersect in intersects)
+        {
+            set.Add (intersect.Type);
+        }
 
         #region Cross Conditions
 
         if (Has (
                  set,
-                 IntersectionType.PassOverHorizontal,
-                 IntersectionType.PassOverVertical
+                 [IntersectionType.PassOverHorizontal,
+                 IntersectionType.PassOverVertical]
                 ))
         {
             return IntersectionRuneType.Cross;
@@ -512,9 +541,9 @@ public class LineCanvas : IDisposable
 
         if (Has (
                  set,
-                 IntersectionType.PassOverVertical,
+                 [IntersectionType.PassOverVertical,
                  IntersectionType.StartLeft,
-                 IntersectionType.StartRight
+                 IntersectionType.StartRight]
                 ))
         {
             return IntersectionRuneType.Cross;
@@ -522,9 +551,9 @@ public class LineCanvas : IDisposable
 
         if (Has (
                  set,
-                 IntersectionType.PassOverHorizontal,
+                 [IntersectionType.PassOverHorizontal,
                  IntersectionType.StartUp,
-                 IntersectionType.StartDown
+                 IntersectionType.StartDown]
                 ))
         {
             return IntersectionRuneType.Cross;
@@ -532,10 +561,10 @@ public class LineCanvas : IDisposable
 
         if (Has (
                  set,
-                 IntersectionType.StartLeft,
+                 [IntersectionType.StartLeft,
                  IntersectionType.StartRight,
                  IntersectionType.StartUp,
-                 IntersectionType.StartDown
+                 IntersectionType.StartDown]
                 ))
         {
             return IntersectionRuneType.Cross;
@@ -545,38 +574,22 @@ public class LineCanvas : IDisposable
 
         #region Corner Conditions
 
-        if (Exactly (
-                     set,
-                     IntersectionType.StartRight,
-                     IntersectionType.StartDown
-                    ))
+        if (Exactly (set, CornerIntersections.UpperLeft))
         {
             return IntersectionRuneType.ULCorner;
         }
 
-        if (Exactly (
-                     set,
-                     IntersectionType.StartLeft,
-                     IntersectionType.StartDown
-                    ))
+        if (Exactly (set, CornerIntersections.UpperRight))
         {
             return IntersectionRuneType.URCorner;
         }
 
-        if (Exactly (
-                     set,
-                     IntersectionType.StartUp,
-                     IntersectionType.StartLeft
-                    ))
+        if (Exactly (set, CornerIntersections.LowerRight))
         {
             return IntersectionRuneType.LRCorner;
         }
 
-        if (Exactly (
-                     set,
-                     IntersectionType.StartUp,
-                     IntersectionType.StartRight
-                    ))
+        if (Exactly (set, CornerIntersections.LowerLeft))
         {
             return IntersectionRuneType.LLCorner;
         }
@@ -587,8 +600,8 @@ public class LineCanvas : IDisposable
 
         if (Has (
                  set,
-                 IntersectionType.PassOverHorizontal,
-                 IntersectionType.StartDown
+                 [IntersectionType.PassOverHorizontal,
+                 IntersectionType.StartDown]
                 ))
         {
             return IntersectionRuneType.TopTee;
@@ -596,9 +609,9 @@ public class LineCanvas : IDisposable
 
         if (Has (
                  set,
-                 IntersectionType.StartRight,
+                 [IntersectionType.StartRight,
                  IntersectionType.StartLeft,
-                 IntersectionType.StartDown
+                 IntersectionType.StartDown]
                 ))
         {
             return IntersectionRuneType.TopTee;
@@ -606,8 +619,8 @@ public class LineCanvas : IDisposable
 
         if (Has (
                  set,
-                 IntersectionType.PassOverHorizontal,
-                 IntersectionType.StartUp
+                 [IntersectionType.PassOverHorizontal,
+                 IntersectionType.StartUp]
                 ))
         {
             return IntersectionRuneType.BottomTee;
@@ -615,9 +628,9 @@ public class LineCanvas : IDisposable
 
         if (Has (
                  set,
-                 IntersectionType.StartRight,
+                 [IntersectionType.StartRight,
                  IntersectionType.StartLeft,
-                 IntersectionType.StartUp
+                 IntersectionType.StartUp]
                 ))
         {
             return IntersectionRuneType.BottomTee;
@@ -625,8 +638,8 @@ public class LineCanvas : IDisposable
 
         if (Has (
                  set,
-                 IntersectionType.PassOverVertical,
-                 IntersectionType.StartRight
+                 [IntersectionType.PassOverVertical,
+                 IntersectionType.StartRight]
                 ))
         {
             return IntersectionRuneType.LeftTee;
@@ -634,9 +647,9 @@ public class LineCanvas : IDisposable
 
         if (Has (
                  set,
-                 IntersectionType.StartRight,
+                 [IntersectionType.StartRight,
                  IntersectionType.StartDown,
-                 IntersectionType.StartUp
+                 IntersectionType.StartUp]
                 ))
         {
             return IntersectionRuneType.LeftTee;
@@ -644,8 +657,8 @@ public class LineCanvas : IDisposable
 
         if (Has (
                  set,
-                 IntersectionType.PassOverVertical,
-                 IntersectionType.StartLeft
+                 [IntersectionType.PassOverVertical,
+                 IntersectionType.StartLeft]
                 ))
         {
             return IntersectionRuneType.RightTee;
@@ -653,9 +666,9 @@ public class LineCanvas : IDisposable
 
         if (Has (
                  set,
-                 IntersectionType.StartLeft,
+                 [IntersectionType.StartLeft,
                  IntersectionType.StartDown,
-                 IntersectionType.StartUp
+                 IntersectionType.StartUp]
                 ))
         {
             return IntersectionRuneType.RightTee;
@@ -683,7 +696,36 @@ public class LineCanvas : IDisposable
     /// <param name="intersects"></param>
     /// <param name="types"></param>
     /// <returns></returns>
-    private bool Has (HashSet<IntersectionType> intersects, params IntersectionType [] types) { return types.All (t => intersects.Contains (t)); }
+    private bool Has (HashSet<IntersectionType> intersects, ReadOnlySpan<IntersectionType> types)
+    {
+        foreach (var type in types)
+        {
+            if (!intersects.Contains (type))
+            {
+                return false;
+            }
+        }
+        return true;
+    }
+
+
+    /// <summary>
+    /// Preallocated arrays for <see cref="GetRuneTypeForIntersects"/> calls to <see cref="Exactly"/>.
+    /// </summary>
+    /// <remarks>
+    /// Optimization to avoid array allocation for each call from array params. Please do not edit the arrays at runtime. :)
+    /// 
+    /// More ideal solution would be to change <see cref="Exactly"/> to take ReadOnlySpan instead of an array
+    /// but that would require replacing the HashSet.SetEquals call.
+    /// </remarks>
+    private static class CornerIntersections
+    {
+        // Names matching #region "Corner Conditions" IntersectionRuneType
+        internal static readonly IntersectionType[] UpperLeft = [IntersectionType.StartRight, IntersectionType.StartDown];
+        internal static readonly IntersectionType[] UpperRight = [IntersectionType.StartLeft, IntersectionType.StartDown];
+        internal static readonly IntersectionType[] LowerRight = [IntersectionType.StartUp, IntersectionType.StartLeft];
+        internal static readonly IntersectionType[] LowerLeft = [IntersectionType.StartUp, IntersectionType.StartRight];
+    }
 
     private class BottomTeeIntersectionRuneResolver : IntersectionRuneResolver
     {
@@ -727,45 +769,12 @@ public class LineCanvas : IDisposable
         internal Rune _thickV;
         protected IntersectionRuneResolver () { SetGlyphs (); }
 
-        public Rune? GetRuneForIntersects (IConsoleDriver? driver, IntersectionDefinition? [] intersects)
+        public Rune? GetRuneForIntersects (IConsoleDriver? driver, ReadOnlySpan<IntersectionDefinition> intersects)
         {
-            bool useRounded = intersects.Any (
-                                              i => i?.Line.Length != 0
-                                                   && (
-                                                          i?.Line.Style == LineStyle.Rounded
-                                                          || i?.Line.Style
-                                                          == LineStyle.RoundedDashed
-                                                          || i?.Line.Style
-                                                          == LineStyle.RoundedDotted)
-                                             );
-
             // Note that there aren't any glyphs for intersections of double lines with heavy lines
 
-            bool doubleHorizontal = intersects.Any (
-                                                    l => l?.Line.Orientation == Orientation.Horizontal
-                                                         && l.Line.Style == LineStyle.Double
-                                                   );
-
-            bool doubleVertical = intersects.Any (
-                                                  l => l?.Line.Orientation == Orientation.Vertical
-                                                       && l.Line.Style == LineStyle.Double
-                                                 );
-
-            bool thickHorizontal = intersects.Any (
-                                                   l => l?.Line.Orientation == Orientation.Horizontal
-                                                        && (
-                                                               l.Line.Style == LineStyle.Heavy
-                                                               || l.Line.Style == LineStyle.HeavyDashed
-                                                               || l.Line.Style == LineStyle.HeavyDotted)
-                                                  );
-
-            bool thickVertical = intersects.Any (
-                                                 l => l?.Line.Orientation == Orientation.Vertical
-                                                      && (
-                                                             l.Line.Style == LineStyle.Heavy
-                                                             || l.Line.Style == LineStyle.HeavyDashed
-                                                             || l.Line.Style == LineStyle.HeavyDotted)
-                                                );
+            bool doubleHorizontal = AnyWithOrientationAndAnyLineStyle(intersects, Orientation.Horizontal, [LineStyle.Double]);
+            bool doubleVertical = AnyWithOrientationAndAnyLineStyle(intersects, Orientation.Vertical, [LineStyle.Double]);
 
             if (doubleHorizontal)
             {
@@ -777,6 +786,11 @@ public class LineCanvas : IDisposable
                 return _doubleV;
             }
 
+            bool thickHorizontal = AnyWithOrientationAndAnyLineStyle(intersects, Orientation.Horizontal,
+                [LineStyle.Heavy, LineStyle.HeavyDashed, LineStyle.HeavyDotted]);
+            bool thickVertical = AnyWithOrientationAndAnyLineStyle(intersects, Orientation.Vertical,
+                [LineStyle.Heavy, LineStyle.HeavyDashed, LineStyle.HeavyDotted]);
+
             if (thickHorizontal)
             {
                 return thickVertical ? _thickBoth : _thickH;
@@ -787,7 +801,51 @@ public class LineCanvas : IDisposable
                 return _thickV;
             }
 
-            return useRounded ? _round : _normal;
+            return UseRounded (intersects) ? _round : _normal;
+
+            static bool UseRounded (ReadOnlySpan<IntersectionDefinition> intersects)
+            {
+                foreach (var intersect in intersects)
+                {
+                    if (intersect.Line.Length == 0)
+                    {
+                        continue;
+                    }
+
+                    if (intersect.Line.Style is
+                        LineStyle.Rounded or
+                        LineStyle.RoundedDashed or
+                        LineStyle.RoundedDotted)
+                    {
+                        return true;
+                    }
+                }
+                return false;
+            }
+
+            static bool AnyWithOrientationAndAnyLineStyle (
+                ReadOnlySpan<IntersectionDefinition> intersects,
+                Orientation orientation,
+                ReadOnlySpan<LineStyle> lineStyles)
+            {
+                foreach (var i in intersects)
+                {
+                    if (i.Line.Orientation != orientation)
+                    {
+                        continue;
+                    }
+
+                    // Any line style
+                    foreach (var style in lineStyles)
+                    {
+                        if (i.Line.Style == style)
+                        {
+                            return true;
+                        }
+                    }
+                }
+                return false;
+            }
         }
 
         /// <summary>

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

@@ -10,7 +10,7 @@ public enum LineStyle
     /// <summary>No border is drawn.</summary>
     None,
 
-    /// <summary>The border is drawn using thin line CM.Glyphs.</summary>
+    /// <summary>The border is drawn using thin line Glyphs.</summary>
     Single,
 
     /// <summary>The border is drawn using thin line glyphs with dashed (double and triple) straight lines.</summary>
@@ -19,10 +19,10 @@ public enum LineStyle
     /// <summary>The border is drawn using thin line glyphs with short dashed (triple and quadruple) straight lines.</summary>
     Dotted,
 
-    /// <summary>The border is drawn using thin double line CM.Glyphs.</summary>
+    /// <summary>The border is drawn using thin double line Glyphs.</summary>
     Double,
 
-    /// <summary>The border is drawn using heavy line CM.Glyphs.</summary>
+    /// <summary>The border is drawn using heavy line Glyphs.</summary>
     Heavy,
 
     /// <summary>The border is drawn using heavy line glyphs with dashed (double and triple) straight lines.</summary>

+ 970 - 114
Terminal.Gui/Drawing/Region.cs

@@ -1,90 +1,245 @@
-/// <summary>
-///     Represents a region composed of one or more rectangles, providing methods for union, intersection, exclusion, and
-///     complement operations.
+#nullable enable
+
+namespace Terminal.Gui;
+
+/// <summary>
+///     Represents a region composed of one or more rectangles, providing methods for geometric set operations such as
+///     union,
+///     intersection, exclusion, and complement. This class is designed for use in graphical or terminal-based user
+///     interfaces
+///     where regions need to be manipulated to manage screen areas, clipping, or drawing boundaries.
 /// </summary>
-public class Region : IDisposable
+/// <remarks>
+///     <para>
+///         This class is thread-safe. All operations are synchronized to ensure consistent state when accessed concurrently.
+///     </para>
+///     <para>
+///         The <see cref="Region"/> class adopts a philosophy of efficiency and flexibility, balancing performance with
+///         usability for GUI applications. It maintains a list of <see cref="Rectangle"/> objects, representing disjoint
+///         (non-overlapping) rectangular areas, and supports operations inspired by set theory. These operations allow
+///         combining regions in various ways, such as merging areas (<see cref="RegionOp.Union"/> or
+///         <see cref="RegionOp.MinimalUnion"/>),
+///         finding common areas (<see cref="RegionOp.Intersect"/>), or removing portions (
+///         <see cref="RegionOp.Difference"/> or
+///         <see cref="Exclude(Rectangle)"/>).
+///     </para>
+///     <para>
+///         To achieve high performance, the class employs a sweep-line algorithm for merging rectangles, which efficiently
+///         processes large sets of rectangles in O(n log n) time by scanning along the x-axis and tracking active vertical
+///         intervals. This approach ensures scalability for typical GUI scenarios with moderate numbers of rectangles. For
+///         operations like <see cref="RegionOp.Union"/> and <see cref="RegionOp.MinimalUnion"/>, an optional minimization
+///         step (
+///         <see
+///             cref="MinimizeRectangles"/>
+///         ) is used to reduce the number of rectangles to a minimal set, producing the smallest
+///         possible collection of non-overlapping rectangles that cover the same area. This minimization, while O(n²) in
+///         worst-case complexity, is optimized for small-to-medium collections and provides a compact representation ideal
+///         for drawing or logical operations.
+///     </para>
+///     <para>
+///         The class is immutable in its operations (returning new regions or modifying in-place via methods like
+///         <see cref="Combine(Rectangle,RegionOp)"/>), supports nullability for robustness, and implements
+///         <see cref="IDisposable"/> to manage
+///         resources by clearing internal state. Developers can choose between granular (detailed) or minimal (compact)
+///         outputs for union operations via <see cref="RegionOp.Union"/> and <see cref="RegionOp.MinimalUnion"/>, catering
+///         to diverse use cases such as rendering optimization, event handling, or visualization.
+///     </para>
+/// </remarks>
+public class Region
 {
-    private List<Rectangle> _rectangles;
+    private readonly List<Rectangle> _rectangles = [];
+
+    // Add a single reusable list for temp operations
+    private readonly List<Rectangle> _tempRectangles = new();
+
+    // Object used for synchronization
+    private readonly object _syncLock = new object();
 
     /// <summary>
     ///     Initializes a new instance of the <see cref="Region"/> class.
     /// </summary>
-    public Region () { _rectangles = new (); }
+    public Region () { }
 
     /// <summary>
     ///     Initializes a new instance of the <see cref="Region"/> class with the specified rectangle.
     /// </summary>
     /// <param name="rectangle">The initial rectangle for the region.</param>
-    public Region (Rectangle rectangle) { _rectangles = new () { rectangle }; }
-
-    /// <summary>
-    ///     Adds the specified rectangle to the region.
-    /// </summary>
-    /// <param name="rectangle">The rectangle to add to the region.</param>
-    public void Union (Rectangle rectangle)
+    public Region (Rectangle rectangle)
     {
-        _rectangles.Add (rectangle);
-        _rectangles = MergeRectangles (_rectangles);
+        lock (_syncLock)
+        {
+            _rectangles.Add (rectangle);
+        }
     }
 
     /// <summary>
-    ///     Adds the specified region to this region.
+    ///     Creates an exact copy of the region.
     /// </summary>
-    /// <param name="region">The region to add to this region.</param>
-    public void Union (Region region)
+    /// <returns>A new <see cref="Region"/> that is a copy of this instance.</returns>
+    public Region Clone ()
     {
-        _rectangles.AddRange (region._rectangles);
-        _rectangles = MergeRectangles (_rectangles);
-    }
+        lock (_syncLock)
+        {
+            var clone = new Region ();
+            clone._rectangles.Capacity = _rectangles.Count; // Pre-allocate capacity
+            clone._rectangles.AddRange (_rectangles);
 
-    /// <summary>
-    ///     Updates the region to be the intersection of itself with the specified rectangle.
-    /// </summary>
-    /// <param name="rectangle">The rectangle to intersect with the region.</param>
-    public void Intersect (Rectangle rectangle)
-    {
-        _rectangles = _rectangles.Select (r => Rectangle.Intersect (r, rectangle)).Where (r => !r.IsEmpty).ToList ();
+            return clone;
+        }
     }
 
     /// <summary>
-    ///     Updates the region to be the intersection of itself with the specified region.
+    ///     Combines <paramref name="rectangle"/> with the region using the specified operation.
     /// </summary>
-    /// <param name="region">The region to intersect with this region.</param>
-    public void Intersect (Region region)
+    /// <param name="rectangle">The rectangle to combine.</param>
+    /// <param name="operation">The operation to perform.</param>
+    public void Combine (Rectangle rectangle, RegionOp operation)
     {
-        List<Rectangle> intersections = new List<Rectangle> ();
-
-        foreach (Rectangle rect1 in _rectangles)
+        lock (_syncLock)
         {
-            foreach (Rectangle rect2 in region._rectangles)
+            if (rectangle.IsEmpty && operation != RegionOp.Replace)
             {
-                Rectangle intersected = Rectangle.Intersect (rect1, rect2);
-
-                if (!intersected.IsEmpty)
+                if (operation == RegionOp.Intersect)
                 {
-                    intersections.Add (intersected);
+                    _rectangles.Clear ();
                 }
+
+                return;
             }
-        }
 
-        _rectangles = intersections;
+            Combine (new Region (rectangle), operation);
+        }
     }
 
     /// <summary>
-    ///     Removes the specified rectangle from the region.
+    ///     Combines <paramref name="region"/> with the region using the specified operation.
     /// </summary>
-    /// <param name="rectangle">The rectangle to exclude from the region.</param>
-    public void Exclude (Rectangle rectangle) { _rectangles = _rectangles.SelectMany (r => SubtractRectangle (r, rectangle)).ToList (); }
+    /// <param name="region">The region to combine.</param>
+    /// <param name="operation">The operation to perform.</param>
+    public void Combine (Region? region, RegionOp operation)
+    {
+        lock (_syncLock)
+        {
+            CombineInternal(region, operation);
+        }
+    }
 
-    /// <summary>
-    ///     Removes the portion of the specified region from this region.
-    /// </summary>
-    /// <param name="region">The region to exclude from this region.</param>
-    public void Exclude (Region region)
+    // Private method to implement the combine logic within a lock
+    private void CombineInternal(Region? region, RegionOp operation)
     {
-        foreach (Rectangle rect in region._rectangles)
+        if (region is null || region._rectangles.Count == 0)
         {
-            _rectangles = _rectangles.SelectMany (r => SubtractRectangle (r, rect)).ToList ();
+            if (operation is RegionOp.Intersect or RegionOp.Replace)
+            {
+                _rectangles.Clear ();
+            }
+
+            return;
+        }
+
+        switch (operation)
+        {
+            case RegionOp.Difference:
+
+                // region is regionB
+                // We'll chain the difference: (regionA - rect1) - rect2 - rect3 ...
+                List<Rectangle> newRectangles = new (_rectangles);
+
+                foreach (Rectangle rect in region._rectangles)
+                {
+                    List<Rectangle> temp = new ();
+
+                    foreach (Rectangle r in newRectangles)
+                    {
+                        temp.AddRange (SubtractRectangle (r, rect));
+                    }
+
+                    newRectangles = temp;
+                }
+
+                _rectangles.Clear ();
+                _rectangles.AddRange (newRectangles);
+
+                break;
+
+            case RegionOp.Intersect:
+                List<Rectangle> intersections = new (_rectangles.Count); // Pre-allocate
+
+                // Null is same as empty region
+                region ??= new ();
+
+                foreach (Rectangle rect1 in _rectangles)
+                {
+                    foreach (Rectangle rect2 in region!._rectangles)
+                    {
+                        Rectangle intersected = Rectangle.Intersect (rect1, rect2);
+
+                        if (!intersected.IsEmpty)
+                        {
+                            intersections.Add (intersected);
+                        }
+                    }
+                }
+
+                _rectangles.Clear ();
+                _rectangles.AddRange (intersections);
+
+                break;
+
+            case RegionOp.Union:
+                // Avoid collection initialization with spread operator
+                _tempRectangles.Clear();
+                _tempRectangles.AddRange(_rectangles);
+                if (region != null)
+                {
+                    // Get the region's rectangles safely
+                    lock (region._syncLock)
+                    {
+                        _tempRectangles.AddRange(region._rectangles);
+                    }
+                }
+                List<Rectangle> mergedUnion = MergeRectangles(_tempRectangles, false);
+                _rectangles.Clear();
+                _rectangles.AddRange(mergedUnion);
+                break;
+
+            case RegionOp.MinimalUnion:
+                // Avoid collection initialization with spread operator
+                _tempRectangles.Clear();
+                _tempRectangles.AddRange(_rectangles);
+                if (region != null)
+                {
+                    // Get the region's rectangles safely
+                    lock (region._syncLock)
+                    {
+                        _tempRectangles.AddRange(region._rectangles);
+                    }
+                }
+                List<Rectangle> mergedMinimalUnion = MergeRectangles(_tempRectangles, true);
+                _rectangles.Clear();
+                _rectangles.AddRange(mergedMinimalUnion);
+                break;
+
+            case RegionOp.XOR:
+                Exclude (region);
+                region.Combine (this, RegionOp.Difference);
+                _rectangles.AddRange (region._rectangles);
+
+                break;
+
+            case RegionOp.ReverseDifference:
+                region.Combine (this, RegionOp.Difference);
+                _rectangles.Clear ();
+                _rectangles.AddRange (region._rectangles);
+
+                break;
+
+            case RegionOp.Replace:
+                _rectangles.Clear ();
+                _rectangles.Capacity = region._rectangles.Count; // Pre-allocate
+                _rectangles.AddRange (region._rectangles);
+
+                break;
         }
     }
 
@@ -101,28 +256,154 @@ public class Region : IDisposable
             return;
         }
 
-        List<Rectangle> complementRectangles = new List<Rectangle> { bounds };
+        List<Rectangle> complementRectangles = new (4) { bounds }; // Typical max initial capacity
 
         foreach (Rectangle rect in _rectangles)
         {
             complementRectangles = complementRectangles.SelectMany (r => SubtractRectangle (r, rect)).ToList ();
         }
 
-        _rectangles = complementRectangles;
+        _rectangles.Clear ();
+        _rectangles.AddRange (complementRectangles);
     }
 
     /// <summary>
-    ///     Creates an exact copy of the region.
+    ///     Determines whether the specified point is contained within the region.
     /// </summary>
-    /// <returns>A new <see cref="Region"/> that is a copy of this instance.</returns>
-    public Region Clone ()
+    /// <param name="x">The x-coordinate of the point.</param>
+    /// <param name="y">The y-coordinate of the point.</param>
+    /// <returns><c>true</c> if the point is contained within the region; otherwise, <c>false</c>.</returns>
+    public bool Contains (int x, int y)
+    {
+        lock (_syncLock)
+        {
+            foreach (Rectangle r in _rectangles)
+            {
+                if (r.Contains (x, y))
+                {
+                    return true;
+                }
+            }
+
+            return false;
+        }
+    }
+
+    /// <summary>
+    ///     Determines whether the specified rectangle is contained within the region.
+    /// </summary>
+    /// <param name="rectangle">The rectangle to check for containment.</param>
+    /// <returns><c>true</c> if the rectangle is contained within the region; otherwise, <c>false</c>.</returns>
+    public bool Contains (Rectangle rectangle)
+    {
+        lock (_syncLock)
+        {
+            foreach (Rectangle r in _rectangles)
+            {
+                if (r.Contains (rectangle))
+                {
+                    return true;
+                }
+            }
+
+            return false;
+        }
+    }
+
+    /// <summary>
+    ///     Determines whether the specified object is equal to this region.
+    /// </summary>
+    /// <param name="obj">The object to compare with this region.</param>
+    /// <returns><c>true</c> if the objects are equal; otherwise, <c>false</c>.</returns>
+    public override bool Equals (object? obj) { return obj is Region other && Equals (other); }
+
+    private static bool IsRegionEmpty (List<Rectangle> rectangles)
     {
-        var clone = new Region ();
-        clone._rectangles = new (_rectangles);
+        if (rectangles.Count == 0)
+        {
+            return true;
+        }
 
-        return clone;
+        foreach (Rectangle r in rectangles)
+        {
+            if (r is { IsEmpty: false, Width: > 0, Height: > 0 })
+            {
+                return false;
+            }
+        }
+
+        return true;
     }
 
+    /// <summary>
+    ///     Determines whether the specified region is equal to this region.
+    /// </summary>
+    /// <param name="other">The region to compare with this region.</param>
+    /// <returns><c>true</c> if the regions are equal; otherwise, <c>false</c>.</returns>
+    public bool Equals (Region? other)
+    {
+        if (other is null)
+        {
+            return false;
+        }
+
+        if (ReferenceEquals (this, other))
+        {
+            return true;
+        }
+
+        // Check if either region is empty
+        bool thisEmpty = IsRegionEmpty (_rectangles);
+        bool otherEmpty = IsRegionEmpty (other._rectangles);
+
+        // If either is empty, they're equal only if both are empty
+        if (thisEmpty || otherEmpty)
+        {
+            return thisEmpty == otherEmpty;
+        }
+
+        // For non-empty regions, compare rectangle counts
+        if (_rectangles.Count != other._rectangles.Count)
+        {
+            return false;
+        }
+
+        // Compare all rectangles - order matters since we maintain canonical form
+        for (var i = 0; i < _rectangles.Count; i++)
+        {
+            if (!_rectangles [i].Equals (other._rectangles [i]))
+            {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    /// <summary>
+    ///     Removes the specified rectangle from the region.
+    /// </summary>
+    /// <remarks>
+    ///     <para>
+    ///         This is a helper method that is equivalent to calling <see cref="Combine(Rectangle,RegionOp)"/> with
+    ///         <see cref="RegionOp.Difference"/>.
+    ///     </para>
+    /// </remarks>
+    /// <param name="rectangle">The rectangle to exclude from the region.</param>
+    public void Exclude (Rectangle rectangle) { Combine (rectangle, RegionOp.Difference); }
+
+    /// <summary>
+    ///     Removes the portion of the specified region from this region.
+    /// </summary>
+    /// <remarks>
+    ///     <para>
+    ///         This is a helper method that is equivalent to calling <see cref="Combine(Region,RegionOp)"/> with
+    ///         <see cref="RegionOp.Difference"/>.
+    ///     </para>
+    /// </remarks>
+    /// <param name="region">The region to exclude from this region.</param>
+    public void Exclude (Region? region) { Combine (region, RegionOp.Difference); }
+
     /// <summary>
     ///     Gets a bounding rectangle for the entire region.
     /// </summary>
@@ -134,94 +415,346 @@ public class Region : IDisposable
             return Rectangle.Empty;
         }
 
-        int left = _rectangles.Min (r => r.Left);
-        int top = _rectangles.Min (r => r.Top);
-        int right = _rectangles.Max (r => r.Right);
-        int bottom = _rectangles.Max (r => r.Bottom);
+        Rectangle first = _rectangles [0];
+        int left = first.Left;
+        int top = first.Top;
+        int right = first.Right;
+        int bottom = first.Bottom;
+
+        for (var i = 1; i < _rectangles.Count; i++)
+        {
+            Rectangle r = _rectangles [i];
+            left = Math.Min (left, r.Left);
+            top = Math.Min (top, r.Top);
+            right = Math.Max (right, r.Right);
+            bottom = Math.Max (bottom, r.Bottom);
+        }
 
         return new (left, top, right - left, bottom - top);
     }
 
     /// <summary>
-    ///     Determines whether the region is empty.
+    ///     Returns a hash code for this region.
     /// </summary>
-    /// <returns><c>true</c> if the region is empty; otherwise, <c>false</c>.</returns>
-    public bool IsEmpty () { return !_rectangles.Any (); }
+    /// <returns>A hash code for this region.</returns>
+    public override int GetHashCode ()
+    {
+        var hash = new HashCode ();
+
+        foreach (Rectangle rect in _rectangles)
+        {
+            hash.Add (rect);
+        }
+
+        return hash.ToHashCode ();
+    }
 
     /// <summary>
-    ///     Determines whether the specified point is contained within the region.
+    ///     Returns an array of rectangles that represent the region.
     /// </summary>
-    /// <param name="x">The x-coordinate of the point.</param>
-    /// <param name="y">The y-coordinate of the point.</param>
-    /// <returns><c>true</c> if the point is contained within the region; otherwise, <c>false</c>.</returns>
-    public bool Contains (int x, int y) { return _rectangles.Any (r => r.Contains (x, y)); }
+    /// <returns>An array of <see cref="Rectangle"/> objects that make up the region.</returns>
+    public Rectangle [] GetRectangles () { return _rectangles.ToArray (); }
 
     /// <summary>
-    ///     Determines whether the specified rectangle is contained within the region.
+    ///     Updates the region to be the intersection of itself with the specified rectangle.
     /// </summary>
-    /// <param name="rectangle">The rectangle to check for containment.</param>
-    /// <returns><c>true</c> if the rectangle is contained within the region; otherwise, <c>false</c>.</returns>
-    public bool Contains (Rectangle rectangle) { return _rectangles.Any (r => r.Contains (rectangle)); }
+    /// <remarks>
+    ///     <para>
+    ///         This is a helper method that is equivalent to calling <see cref="Combine(Rectangle,RegionOp)"/> with
+    ///         <see cref="RegionOp.Intersect"/>.
+    ///     </para>
+    /// </remarks>
+    /// <param name="rectangle">The rectangle to intersect with the region.</param>
+    public void Intersect (Rectangle rectangle) { Combine (rectangle, RegionOp.Intersect); }
 
     /// <summary>
-    ///     Returns an array of rectangles that represent the region.
+    ///     Updates the region to be the intersection of itself with the specified region.
     /// </summary>
-    /// <returns>An array of <see cref="Rectangle"/> objects that make up the region.</returns>
-    public Rectangle [] GetRegionScans () { return _rectangles.ToArray (); }
+    /// <remarks>
+    ///     <para>
+    ///         This is a helper method that is equivalent to calling <see cref="Combine(Region,RegionOp)"/> with
+    ///         <see cref="RegionOp.Intersect"/>.
+    ///     </para>
+    /// </remarks>
+    /// <param name="region">The region to intersect with this region.</param>
+    public void Intersect (Region? region) { Combine (region, RegionOp.Intersect); }
 
     /// <summary>
-    ///     Offsets all rectangles in the region by the specified amounts.
+    ///     Determines whether the region is empty.
+    /// </summary>
+    /// <returns><c>true</c> if the region is empty; otherwise, <c>false</c>.</returns>
+    public bool IsEmpty ()
+    {
+        if (_rectangles.Count == 0)
+        {
+            return true;
+        }
+
+        foreach (Rectangle r in _rectangles)
+        {
+            if (r is { IsEmpty: false, Width: > 0, Height: > 0 })
+            {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    /// <summary>
+    ///     Translates all rectangles in the region by the specified offsets.
     /// </summary>
     /// <param name="offsetX">The amount to offset along the x-axis.</param>
     /// <param name="offsetY">The amount to offset along the y-axis.</param>
-    public void Offset (int offsetX, int offsetY)
+    public void Translate (int offsetX, int offsetY)
     {
-        for (int i = 0; i < _rectangles.Count; i++)
+        if (offsetX == 0 && offsetY == 0)
+        {
+            return;
+        }
+
+        for (var i = 0; i < _rectangles.Count; i++)
         {
-            var rect = _rectangles [i];
-            _rectangles [i] = new Rectangle (rect.Left + offsetX, rect.Top + offsetY, rect.Width, rect.Height);
+            Rectangle rect = _rectangles [i];
+            _rectangles [i] = rect with { X = rect.Left + offsetX, Y = rect.Top + offsetY };
         }
     }
 
     /// <summary>
-    ///     Merges overlapping rectangles into a minimal set of non-overlapping rectangles.
+    ///     Adds the specified rectangle to the region. Merges all rectangles into a minimal or granular bounding shape.
+    /// </summary>
+    /// <param name="rectangle">The rectangle to add to the region.</param>
+    public void Union (Rectangle rectangle) { Combine (rectangle, RegionOp.Union); }
+
+    /// <summary>
+    ///     Adds the specified region to this region. Merges all rectangles into a minimal or granular bounding shape.
+    /// </summary>
+    /// <param name="region">The region to add to this region.</param>
+    public void Union (Region? region) { Combine (region, RegionOp.Union); }
+
+    /// <summary>
+    ///     Adds the specified rectangle to the region. Merges all rectangles into the smallest possible bounding shape.
+    /// </summary>
+    /// <param name="rectangle">The rectangle to add to the region.</param>
+    public void MinimalUnion (Rectangle rectangle) { Combine (rectangle, RegionOp.MinimalUnion); }
+
+    /// <summary>
+    ///     Adds the specified region to this region. Merges all rectangles into the smallest possible bounding shape.
+    /// </summary>
+    /// <param name="region">The region to add to this region.</param>
+    public void MinimalUnion (Region? region) { Combine (region, RegionOp.MinimalUnion); }
+
+    /// <summary>
+    ///     Merges overlapping rectangles into a minimal or granular set of non-overlapping rectangles with a minimal bounding
+    ///     shape.
     /// </summary>
     /// <param name="rectangles">The list of rectangles to merge.</param>
+    /// <param name="minimize">
+    ///     If <c>true</c>, minimizes the set to the smallest possible number of rectangles; otherwise,
+    ///     returns a granular set.
+    /// </param>
     /// <returns>A list of merged rectangles.</returns>
-    private List<Rectangle> MergeRectangles (List<Rectangle> rectangles)
+    internal static List<Rectangle> MergeRectangles (List<Rectangle> rectangles, bool minimize)
     {
-        // Simplified merging logic: this does not handle all edge cases for merging overlapping rectangles.
-        // For a full implementation, a plane sweep algorithm or similar would be needed.
-        List<Rectangle> merged = new List<Rectangle> (rectangles);
-        bool mergedAny;
+        if (rectangles.Count == 0)
+        {
+            return [];
+        }
+
+        // Sweep-line algorithm to merge rectangles
+        List<(int x, bool isStart, int yTop, int yBottom)> events = new (rectangles.Count * 2); // Pre-allocate
+
+        foreach (Rectangle r in rectangles)
+        {
+            if (!r.IsEmpty)
+            {
+                events.Add ((r.Left, true, r.Top, r.Bottom)); // Start event
+                events.Add ((r.Right, false, r.Top, r.Bottom)); // End event
+            }
+        }
+
+        if (events.Count == 0)
+        {
+            return []; // Return empty list if no non-empty rectangles exist
+        }
+
+        events.Sort (
+                     (a, b) =>
+                     {
+                         int cmp = a.x.CompareTo (b.x);
+
+                         if (cmp != 0)
+                         {
+                             return cmp;
+                         }
+
+                         return a.isStart.CompareTo (b.isStart); // Start events before end events at same x
+                     });
+
+        List<Rectangle> merged = [];
+
+        SortedSet<(int yTop, int yBottom)> active = new (
+                                                         Comparer<(int yTop, int yBottom)>.Create (
+                                                                                                   (a, b) =>
+                                                                                                   {
+                                                                                                       int cmp = a.yTop.CompareTo (b.yTop);
+
+                                                                                                       return cmp != 0 ? cmp : a.yBottom.CompareTo (b.yBottom);
+                                                                                                   }));
+        int lastX = events [0].x;
+
+        foreach ((int x, bool isStart, int yTop, int yBottom) evt in events)
+        {
+            // Output rectangles for the previous segment if there are active rectangles
+            if (active.Count > 0 && evt.x > lastX)
+            {
+                merged.AddRange (MergeVerticalIntervals (active, lastX, evt.x));
+            }
+
+            // Process the event
+            if (evt.isStart)
+            {
+                active.Add ((evt.yTop, evt.yBottom));
+            }
+            else
+            {
+                active.Remove ((evt.yTop, evt.yBottom));
+            }
+
+            lastX = evt.x;
+        }
+
+        return minimize ? MinimizeRectangles (merged) : merged;
+    }
+
+    /// <summary>
+    ///     Merges overlapping vertical intervals into a minimal set of non-overlapping rectangles.
+    /// </summary>
+    /// <param name="active">The set of active vertical intervals.</param>
+    /// <param name="startX">The starting x-coordinate for the rectangles.</param>
+    /// <param name="endX">The ending x-coordinate for the rectangles.</param>
+    /// <returns>A list of merged rectangles.</returns>
+    internal static List<Rectangle> MergeVerticalIntervals (SortedSet<(int yTop, int yBottom)> active, int startX, int endX)
+    {
+        if (active.Count == 0)
+        {
+            return [];
+        }
+
+        List<Rectangle> result = new (active.Count); // Pre-allocate
+        int? currentTop = null;
+        int? currentBottom = null;
+
+        foreach ((int yTop, int yBottom) in active)
+        {
+            if (currentTop == null)
+            {
+                currentTop = yTop;
+                currentBottom = yBottom;
+            }
+            else if (yTop <= currentBottom)
+            {
+                currentBottom = Math.Max (currentBottom.Value, yBottom);
+            }
+            else
+            {
+                result.Add (new (startX, currentTop.Value, endX - startX, currentBottom.Value - currentTop.Value));
+                currentTop = yTop;
+                currentBottom = yBottom;
+            }
+        }
+
+        if (currentTop != null)
+        {
+            result.Add (new (startX, currentTop.Value, endX - startX, currentBottom!.Value - currentTop.Value));
+        }
+
+        return result;
+    }
+
+    /// <summary>
+    ///     Minimizes a list of rectangles into the smallest possible set of non-overlapping rectangles
+    ///     by merging adjacent rectangles where possible.
+    /// </summary>
+    /// <param name="rectangles">The list of rectangles to minimize.</param>
+    /// <returns>A list of minimized rectangles.</returns>
+    internal static List<Rectangle> MinimizeRectangles (List<Rectangle> rectangles)
+    {
+        if (rectangles.Count <= 1)
+        {
+            return rectangles.ToList ();
+        }
+
+        List<Rectangle> minimized = new (rectangles.Count); // Pre-allocate
+        List<Rectangle> current = new (rectangles); // Work with a copy
+
+        bool changed;
 
         do
         {
-            mergedAny = false;
+            changed = false;
+            minimized.Clear ();
+
+            // Sort by Y then X for consistent processing
+            current.Sort (
+                          (a, b) =>
+                          {
+                              int cmp = a.Top.CompareTo (b.Top);
+
+                              return cmp != 0 ? cmp : a.Left.CompareTo (b.Left);
+                          });
+
+            var i = 0;
 
-            for (var i = 0; i < merged.Count; i++)
+            while (i < current.Count)
             {
-                for (int j = i + 1; j < merged.Count; j++)
+                Rectangle r = current [i];
+                int j = i + 1;
+
+                while (j < current.Count)
                 {
-                    if (merged [i].IntersectsWith (merged [j]))
+                    Rectangle next = current [j];
+
+                    // Check if rectangles can be merged horizontally (same Y range, adjacent X)
+                    if (r.Top == next.Top && r.Bottom == next.Bottom && (r.Right == next.Left || next.Right == r.Left || r.IntersectsWith (next)))
                     {
-                        merged [i] = Rectangle.Union (merged [i], merged [j]);
-                        merged.RemoveAt (j);
-                        mergedAny = true;
+                        r = new (
+                                 Math.Min (r.Left, next.Left),
+                                 r.Top,
+                                 Math.Max (r.Right, next.Right) - Math.Min (r.Left, next.Left),
+                                 r.Height
+                                );
+                        current.RemoveAt (j);
+                        changed = true;
+                    }
 
-                        break;
+                    // Check if rectangles can be merged vertically (same X range, adjacent Y)
+                    else if (r.Left == next.Left && r.Right == next.Right && (r.Bottom == next.Top || next.Bottom == r.Top || r.IntersectsWith (next)))
+                    {
+                        r = new (
+                                 r.Left,
+                                 Math.Min (r.Top, next.Top),
+                                 r.Width,
+                                 Math.Max (r.Bottom, next.Bottom) - Math.Min (r.Top, next.Top)
+                                );
+                        current.RemoveAt (j);
+                        changed = true;
+                    }
+                    else
+                    {
+                        j++;
                     }
                 }
 
-                if (mergedAny)
-                {
-                    break;
-                }
+                minimized.Add (r);
+                i++;
             }
+
+            current = minimized.ToList (); // Prepare for next iteration
         }
-        while (mergedAny);
+        while (changed);
 
-        return merged;
+        return minimized;
     }
 
     /// <summary>
@@ -230,8 +763,28 @@ public class Region : IDisposable
     /// <param name="original">The original rectangle.</param>
     /// <param name="subtract">The rectangle to subtract from the original.</param>
     /// <returns>An enumerable collection of resulting rectangles after subtraction.</returns>
-    private IEnumerable<Rectangle> SubtractRectangle (Rectangle original, Rectangle subtract)
+    internal static IEnumerable<Rectangle> SubtractRectangle (Rectangle original, Rectangle subtract)
     {
+        // Handle empty or invalid rectangles
+        if (original.IsEmpty || original.Width <= 0 || original.Height <= 0)
+        {
+            yield break; // Return empty enumeration for empty or invalid original
+        }
+
+        if (subtract.IsEmpty || subtract.Width <= 0 || subtract.Height <= 0)
+        {
+            yield return original;
+
+            yield break;
+        }
+
+        // Check for complete overlap (subtract fully contains or equals original)
+        if (subtract.Left <= original.Left && subtract.Top <= original.Top && subtract.Right >= original.Right && subtract.Bottom >= original.Bottom)
+        {
+            yield break; // Return empty if subtract completely overlaps original
+        }
+
+        // Check for no overlap
         if (!original.IntersectsWith (subtract))
         {
             yield return original;
@@ -239,19 +792,29 @@ public class Region : IDisposable
             yield break;
         }
 
-        // Top segment
+        // Fragment the original rectangle into segments excluding the subtract rectangle
+
+        // Top segment (above subtract)
         if (original.Top < subtract.Top)
         {
-            yield return new (original.Left, original.Top, original.Width, subtract.Top - original.Top);
+            yield return new (
+                              original.Left,
+                              original.Top,
+                              original.Width,
+                              subtract.Top - original.Top);
         }
 
-        // Bottom segment
+        // Bottom segment (below subtract)
         if (original.Bottom > subtract.Bottom)
         {
-            yield return new (original.Left, subtract.Bottom, original.Width, original.Bottom - subtract.Bottom);
+            yield return new (
+                              original.Left,
+                              subtract.Bottom,
+                              original.Width,
+                              original.Bottom - subtract.Bottom);
         }
 
-        // Left segment
+        // Left segment (to the left of subtract)
         if (original.Left < subtract.Left)
         {
             int top = Math.Max (original.Top, subtract.Top);
@@ -259,11 +822,15 @@ public class Region : IDisposable
 
             if (bottom > top)
             {
-                yield return new (original.Left, top, subtract.Left - original.Left, bottom - top);
+                yield return new (
+                                  original.Left,
+                                  top,
+                                  subtract.Left - original.Left,
+                                  bottom - top);
             }
         }
 
-        // Right segment
+        // Right segment (to the right of subtract)
         if (original.Right > subtract.Right)
         {
             int top = Math.Max (original.Top, subtract.Top);
@@ -271,13 +838,302 @@ public class Region : IDisposable
 
             if (bottom > top)
             {
-                yield return new (subtract.Right, top, original.Right - subtract.Right, bottom - top);
+                yield return new (
+                                  subtract.Right,
+                                  top,
+                                  original.Right - subtract.Right,
+                                  bottom - top);
+            }
+        }
+    }
+
+    /// <summary>
+    ///     Fills the interior of all rectangles in the region with the specified attribute and fill rune.
+    /// </summary>
+    /// <param name="attribute">The attribute (color/style) to use.</param>
+    /// <param name="fillRune">
+    ///     The rune to fill the interior of the rectangles with. If <cref langword="null"/> space will be
+    ///     used.
+    /// </param>
+    public void FillRectangles (Attribute attribute, Rune? fillRune = null)
+    {
+        if (_rectangles.Count == 0)
+        {
+            return;
+        }
+
+        foreach (Rectangle rect in _rectangles)
+        {
+            if (rect.IsEmpty || rect.Width <= 0 || rect.Height <= 0)
+            {
+                continue;
+            }
+
+            Application.Driver?.SetAttribute (attribute);
+
+            for (int y = rect.Top; y < rect.Bottom; y++)
+            {
+                for (int x = rect.Left; x < rect.Right; x++)
+                {
+                    Application.Driver?.Move (x, y);
+                    Application.Driver?.AddRune (fillRune ?? (Rune)' ');
+                }
             }
         }
     }
 
+
     /// <summary>
-    ///     Releases all resources used by the <see cref="Region"/>.
+    ///     Draws the boundaries of all rectangles in the region using the specified attributes, only if the rectangle is big
+    ///     enough.
     /// </summary>
-    public void Dispose () { _rectangles.Clear (); }
+    /// <param name="canvas">The canvas to draw on.</param>
+    /// <param name="style">The line style to use for drawing.</param>
+    /// <param name="attribute">The attribute (color/style) to use for the lines. If <c>null</c>.</param>
+    public void DrawBoundaries (LineCanvas canvas, LineStyle style, Attribute? attribute = null)
+    {
+        if (_rectangles.Count == 0)
+        {
+            return;
+        }
+
+        foreach (Rectangle rect in _rectangles)
+        {
+            if (rect.IsEmpty || rect.Width <= 0 || rect.Height <= 0)
+            {
+                continue;
+            }
+
+            // Only draw boundaries if the rectangle is "big enough" (e.g., width and height > 1)
+            //if (rect.Width > 2 && rect.Height > 2)
+            {
+                if (rect.Width > 1)
+                {
+                    // Add horizontal lines
+                    canvas.AddLine (new (rect.Left, rect.Top), rect.Width, Orientation.Horizontal, style, attribute);
+                    canvas.AddLine (new (rect.Left, rect.Bottom - 1), rect.Width, Orientation.Horizontal, style, attribute);
+                }
+
+                if (rect.Height > 1)
+                {
+                    // Add vertical lines 
+                    canvas.AddLine (new (rect.Left, rect.Top), rect.Height, Orientation.Vertical, style, attribute);
+                    canvas.AddLine (new (rect.Right - 1, rect.Top), rect.Height, Orientation.Vertical, style, attribute);
+                }
+            }
+        }
+    }
+
+
+    // BUGBUG: DrawOuterBoundary does not work right. it draws all regions +1 too tall/wide. It should draw single width/height regions as just a line.
+    //
+    // Example: There are 3 regions here. the first is a rect (0,0,1,4). Second is (10, 0, 2, 4). 
+    // This is how they should draw:
+    //
+    // |123456789|123456789|123456789
+    // 1 │        ┌┐        ┌─┐ 
+    // 2 │        ││        │ │ 
+    // 3 │        ││        │ │ 
+    // 4 │        └┘        └─┘
+    // 
+    // But this is what it draws:
+    // |123456789|123456789|123456789
+    // 1┌┐        ┌─┐       ┌──┐     
+    // 2││        │ │       │  │     
+    // 3││        │ │       │  │     
+    // 4││        │ │       │  │     
+    // 5└┘        └─┘       └──┘         
+    //
+    // Example: There are two rectangles in this region. (0,0,3,3) and (3, 3, 3, 3).
+    // This is fill - correct:
+    // |123456789
+    // 1░░░      
+    // 2░░░      
+    // 3░░░░░    
+    // 4  ░░░    
+    // 5  ░░░    
+    // 6         
+    //
+    // This is what DrawOuterBoundary should draw
+    // |123456789|123456789
+    // 1┌─┐               
+    // 2│ │             
+    // 3└─┼─┐             
+    // 4  │ │             
+    // 5  └─┘             
+    // 6
+    //
+    // This is what DrawOuterBoundary actually draws
+    // |123456789|123456789
+    // 1┌──┐               
+    // 2│  │               
+    // 3│  └─┐             
+    // 4└─┐  │             
+    // 5  │  │             
+    // 6  └──┘             
+
+    /// <summary>
+    ///     Draws the outer perimeter of the region to <paramref name="lineCanvas"/> using <paramref name="style"/> and
+    ///     <paramref name="attribute"/>.
+    ///     The outer perimeter follows the shape of the rectangles in the region, even if non-rectangular, by drawing
+    ///     boundaries and excluding internal lines.
+    /// </summary>
+    /// <param name="lineCanvas">The LineCanvas to draw on.</param>
+    /// <param name="style">The line style to use for drawing.</param>
+    /// <param name="attribute">The attribute (color/style) to use for the lines. If <c>null</c>.</param>
+    public void DrawOuterBoundary (LineCanvas lineCanvas, LineStyle style, Attribute? attribute = null)
+    {
+        if (_rectangles.Count == 0)
+        {
+            return;
+        }
+
+        // Get the bounds of the region
+        Rectangle bounds = GetBounds ();
+
+        // Add protection against extremely large allocations
+        if (bounds.Width > 1000 || bounds.Height > 1000)
+        {
+            // Fall back to drawing each rectangle's boundary
+            DrawBoundaries(lineCanvas, style, attribute);
+            return;
+        }
+
+        // Create a grid to track which cells are inside the region
+        var insideRegion = new bool [bounds.Width + 1, bounds.Height + 1];
+
+        // Fill the grid based on rectangles
+        foreach (Rectangle rect in _rectangles)
+        {
+            if (rect.IsEmpty || rect.Width <= 0 || rect.Height <= 0)
+            {
+                continue;
+            }
+
+            for (int x = rect.Left; x < rect.Right; x++)
+            {
+                for (int y = rect.Top; y < rect.Bottom; y++)
+                {
+                    // Adjust coordinates to grid space
+                    int gridX = x - bounds.Left;
+                    int gridY = y - bounds.Top;
+
+                    if (gridX >= 0 && gridX < bounds.Width && gridY >= 0 && gridY < bounds.Height)
+                    {
+                        insideRegion [gridX, gridY] = true;
+                    }
+                }
+            }
+        }
+
+        // Find horizontal boundary lines
+        for (var y = 0; y <= bounds.Height; y++)
+        {
+            int startX = -1;
+
+            for (var x = 0; x <= bounds.Width; x++)
+            {
+                bool above = y > 0 && insideRegion [x, y - 1];
+                bool below = y < bounds.Height && insideRegion [x, y];
+
+                // A boundary exists where one side is inside and the other is outside
+                bool isBoundary = above != below;
+
+                if (isBoundary)
+                {
+                    // Start a new segment or continue the current one
+                    if (startX == -1)
+                    {
+                        startX = x;
+                    }
+                }
+                else
+                {
+                    // End the current segment if one exists
+                    if (startX != -1)
+                    {
+                        int length = x - startX + 1; // Add 1 to make sure lines connect
+
+                        lineCanvas.AddLine (
+                                            new (startX + bounds.Left, y + bounds.Top),
+                                            length,
+                                            Orientation.Horizontal,
+                                            style,
+                                            attribute
+                                           );
+                        startX = -1;
+                    }
+                }
+            }
+
+            // End any segment that reaches the right edge
+            if (startX != -1)
+            {
+                int length = bounds.Width + 1 - startX + 1; // Add 1 to make sure lines connect
+
+                lineCanvas.AddLine (
+                                    new (startX + bounds.Left, y + bounds.Top),
+                                    length,
+                                    Orientation.Horizontal,
+                                    style,
+                                    attribute
+                                   );
+            }
+        }
+
+        // Find vertical boundary lines
+        for (var x = 0; x <= bounds.Width; x++)
+        {
+            int startY = -1;
+
+            for (var y = 0; y <= bounds.Height; y++)
+            {
+                bool left = x > 0 && insideRegion [x - 1, y];
+                bool right = x < bounds.Width && insideRegion [x, y];
+
+                // A boundary exists where one side is inside and the other is outside
+                bool isBoundary = left != right;
+
+                if (isBoundary)
+                {
+                    // Start a new segment or continue the current one
+                    if (startY == -1)
+                    {
+                        startY = y;
+                    }
+                }
+                else
+                {
+                    // End the current segment if one exists
+                    if (startY != -1)
+                    {
+                        int length = y - startY + 1; // Add 1 to make sure lines connect
+
+                        lineCanvas.AddLine (
+                                            new (x + bounds.Left, startY + bounds.Top),
+                                            length,
+                                            Orientation.Vertical,
+                                            style,
+                                            attribute
+                                           );
+                        startY = -1;
+                    }
+                }
+            }
+
+            // End any segment that reaches the bottom edge
+            if (startY != -1)
+            {
+                int length = bounds.Height + 1 - startY + 1; // Add 1 to make sure lines connect
+
+                lineCanvas.AddLine (
+                                    new (x + bounds.Left, startY + bounds.Top),
+                                    length,
+                                    Orientation.Vertical,
+                                    style,
+                                    attribute
+                                   );
+            }
+        }
+    }
 }

+ 128 - 0
Terminal.Gui/Drawing/RegionOp.cs

@@ -0,0 +1,128 @@
+#nullable enable
+
+namespace Terminal.Gui;
+
+/// <summary>
+///     Specifies the operation to perform when combining two regions or a <see cref="Region"/> with a
+///     <see cref="Rectangle"/>>, defining how their
+///     rectangular areas are merged, intersected, or subtracted.
+/// </summary>
+/// <remarks>
+///     <para>
+///         Each operation modifies the first region's set of rectangles based on the second (op) region or rectangle,
+///         producing a new set of non-overlapping rectangles. The operations align with set theory, enabling flexible
+///         manipulation for TUI layout, clipping, or drawing. Developers can choose between granular outputs (e.g.,
+///         <see cref="Union"/>) that preserve detailed rectangles or minimal outputs (e.g., <see cref="MinimalUnion"/>)
+///         that reduce the rectangle count for compactness.
+///     </para>
+/// </remarks>
+public enum RegionOp
+{
+    /// <summary>
+    ///     Subtracts the second (op) region or rectangle from the first region, removing any areas where the op overlaps
+    ///     the first region. The result includes only the portions of the first region that do not intersect with the op.
+    ///     <para>
+    ///         For example, if the first region contains rectangle A = (0,0,10,10) and the op is B = (5,5,5,5), the result
+    ///         would include rectangles covering A minus the overlapping part of B, such as (0,0,10,5), (0,5,5,5), and
+    ///         (5,10,5,5).
+    ///     </para>
+    ///     <para>
+    ///         If the op region is empty or null, the operation has no effect unless the first region is also empty, in
+    ///         which case it clears the first region.
+    ///     </para>
+    /// </summary>
+    Difference = 0,
+
+    /// <summary>
+    ///     Intersects the first region with the second (op) region or rectangle, retaining only the areas where both
+    ///     regions overlap. The result includes rectangles covering the common areas, excluding any parts unique to either
+    ///     region.
+    ///     <para>
+    ///         For example, if the first region contains A = (0,0,10,10) and the op is B = (5,5,5,5), the result would be
+    ///         a single rectangle (5,5,5,5), representing the intersection.
+    ///     </para>
+    ///     <para>
+    ///         If either region is empty or null, the result clears the first region, as there’s no intersection possible.
+    ///     </para>
+    /// </summary>
+    Intersect = 1,
+
+    /// <summary>
+    ///     Performs a union (inclusive-or) of the first region and the second (op) region or rectangle, combining all
+    ///     areas covered by either region into a single contiguous region without holes (unless explicitly subtracted).
+    ///     <para>
+    ///         The formal union (∪) includes all points in at least one rectangle, producing a granular set of
+    ///         non-overlapping rectangles that cover the combined area. For example, if the first region contains A =
+    ///         (0,0,5,5) and the op is B = (5,0,5,5), the result might include (0,0,5,5) and (5,0,5,5) unless minimized.
+    ///     </para>
+    ///     <para>
+    ///         This operation uses granular output (preserving detailed rectangles). To minimize the result use
+    ///         <see cref="MinimalUnion"/> instead.
+    ///     </para>
+    ///     <para>
+    ///         If the op region is empty or null, the first region remains unchanged.
+    ///     </para>
+    /// </summary>
+    Union = 2,
+
+    /// <summary>
+    ///     Performs a minimal union (inclusive-or) of the first region and the second (op) region or rectangle, merging adjacent or
+    ///     overlapping rectangles into the smallest possible set of non-overlapping rectangles that cover the combined
+    ///     area.
+    ///     <para>
+    ///         This operation minimizes the number of rectangles, producing a more compact representation compared to
+    ///         <see cref="Union"/>. For example, if the first region contains A = (0,0,5,5) and the op is B = (5,0,5,5),
+    ///         the result would be a single rectangle (0,0,10,5), reducing redundancy.
+    ///     </para>
+    ///     <para>
+    ///         This operation always minimizes the output and has lower performance than <see cref="Union"/>.
+    ///     </para>
+    ///     <para>
+    ///         If the op region is empty or null, the first region remains unchanged.
+    ///     </para>
+    /// </summary>
+    MinimalUnion = 3,
+
+    /// <summary>
+    ///     Performs an exclusive-or (XOR) of the first region and the second (op) region or rectangle, retaining only the
+    ///     areas that are unique to each region—i.e., areas present in one region but not both.
+    ///     <para>
+    ///         For example, if the first region contains A = (0,0,10,10) and the op is B = (5,5,5,5), the result would
+    ///         include rectangles covering (0,0,10,5), (0,5,5,5), (5,10,5,5), and (5,5,5,5), excluding the intersection
+    ///         (5,5,5,5).
+    ///     </para>
+    ///     <para>
+    ///         If the op region is empty or null, this operation excludes the first region from itself (clearing it) or
+    ///         adds the first region to the op (if op is empty), depending on the logic.
+    ///     </para>
+    /// </summary>
+    XOR = 4,
+
+    /// <summary>
+    ///     Subtracts the first region from the second (op) region or rectangle, retaining only the areas of the op that do
+    ///     not overlap with the first region. The result replaces the first region with these areas.
+    ///     <para>
+    ///         For example, if the first region contains A = (5,5,5,5) and the op is B = (0,0,10,10), the result would
+    ///         include rectangles covering B minus A, such as (0,0,10,5), (0,5,5,5), and (5,10,5,5).
+    ///     </para>
+    ///     <para>
+    ///         If the first region is empty or null, the op region replaces the first region. If the op region is empty,
+    ///         the first region is cleared.
+    ///     </para>
+    /// </summary>
+    ReverseDifference = 5,
+
+    /// <summary>
+    ///     Replaces the first region entirely with the second (op) region or rectangle, discarding the first region's
+    ///     current rectangles and adopting the op's rectangles.
+    ///     <para>
+    ///         For example, if the first region contains (0,0,5,5) and the op is (10,10,5,5), the first region will be
+    ///         cleared and replaced with (10,10,5,5).
+    ///     </para>
+    ///     <para>
+    ///         If the op region is empty or null, the first region is cleared. This operation is useful for resetting or
+    ///         overwriting region state.
+    ///     </para>
+    /// </summary>
+    Replace = 6
+}

+ 13 - 0
Terminal.Gui/Drawing/Thickness.cs

@@ -235,6 +235,19 @@ public record struct Thickness
         return new (x, y, width, height);
     }
 
+    /// <summary>
+    ///     Returns a region describing the thickness.
+    /// </summary>
+    /// <param name="rect">The source rectangle</param>
+    /// <returns></returns>
+    public Region AsRegion (Rectangle rect)
+    {
+        Region region = new Region (rect);
+        region.Exclude (GetInside (rect));
+
+        return region;
+    }
+
     /// <summary>
     ///     Gets the total width of the left and right sides of the rectangle. Sets the width of the left and right sides
     ///     of the rectangle to half the specified value.

+ 4 - 0
Terminal.Gui/FileServices/DefaultFileOperations.cs

@@ -139,6 +139,8 @@ public class DefaultFileOperations : IFileOperations
                          {
                              confirm = true;
                              Application.RequestStop ();
+                             // Anytime Accepting is handled, make sure to set e.Cancel to false.
+                             e.Cancel = false;
                          };
         var btnCancel = new Button { Text = Strings.btnCancel };
 
@@ -146,6 +148,8 @@ public class DefaultFileOperations : IFileOperations
                              {
                                  confirm = false;
                                  Application.RequestStop ();
+                                 // Anytime Accepting is handled, make sure to set e.Cancel to false.
+                                 e.Cancel = false;
                              };
 
         var lbl = new Label { Text = Strings.fdRenamePrompt };

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

@@ -5,7 +5,7 @@ namespace Terminal.Gui;
 /// <summary>
 ///     Provides context for a <see cref="Command"/> invocation.
 /// </summary>
-/// <seealso cref="View.Invoke(Command)"/>.
+/// <seealso cref="View.InvokeCommand"/>.
 #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved
 public record struct CommandContext<TBinding> : ICommandContext
 {

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно