Explorar o código

Merge branch 'v2_develop' into v2_release

Tig hai 2 meses
pai
achega
c262a4b065
Modificáronse 100 ficheiros con 2589 adicións e 1971 borrados
  1. 1 1
      CONTRIBUTING.md
  2. 9 5
      Directory.Packages.props
  3. 2 2
      Examples/NativeAot/Publish_linux-x64_Debug.sh
  4. 2 2
      Examples/NativeAot/Publish_linux-x64_Release.sh
  5. 2 2
      Examples/NativeAot/Publish_osx-x64_Debug.sh
  6. 2 2
      Examples/NativeAot/Publish_osx-x64_Release.sh
  7. 54 0
      Examples/UICatalog/Properties/launchSettings.json
  8. 1 1
      Examples/UICatalog/Resources/config.json
  9. 1 1
      Examples/UICatalog/Scenarios/Adornments.cs
  10. 4 4
      Examples/UICatalog/Scenarios/AllViewsTester.cs
  11. 1 1
      Examples/UICatalog/Scenarios/Arrangement.cs
  12. 81 32
      Examples/UICatalog/Scenarios/CharacterMap/CharacterMap.cs
  13. 72 16
      Examples/UICatalog/Scenarios/CombiningMarks.cs
  14. 1 1
      Examples/UICatalog/Scenarios/ConfigurationEditor.cs
  15. 1 1
      Examples/UICatalog/Scenarios/DynamicMenuBar.cs
  16. 1 1
      Examples/UICatalog/Scenarios/EditorsAndHelpers/AdornmentEditor.cs
  17. 1 1
      Examples/UICatalog/Scenarios/EditorsAndHelpers/AdornmentsEditor.cs
  18. 3 3
      Examples/UICatalog/Scenarios/EditorsAndHelpers/DimEditor.cs
  19. 1 1
      Examples/UICatalog/Scenarios/EditorsAndHelpers/EventLog.cs
  20. 3 3
      Examples/UICatalog/Scenarios/EditorsAndHelpers/PosEditor.cs
  21. 1 1
      Examples/UICatalog/Scenarios/ListColumns.cs
  22. 1 1
      Examples/UICatalog/Scenarios/ListViewWithSelection.cs
  23. 1 1
      Examples/UICatalog/Scenarios/Mouse.cs
  24. 1 1
      Examples/UICatalog/Scenarios/NumericUpDownDemo.cs
  25. 8 8
      Examples/UICatalog/Scenarios/ScrollBarDemo.cs
  26. 1 4
      Examples/UICatalog/Scenarios/Scrolling.cs
  27. 0 117
      Examples/UICatalog/Scenarios/SendKeys.cs
  28. 7 7
      Examples/UICatalog/Scenarios/Shortcuts.cs
  29. 1 1
      Examples/UICatalog/Scenarios/SingleBackgroundWorker.cs
  30. 16 13
      Examples/UICatalog/Scenarios/TextAlignmentAndDirection.cs
  31. 88 0
      Examples/UICatalog/Scenarios/Threading.cs
  32. 3 3
      Examples/UICatalog/Scenarios/ViewportSettings.cs
  33. 0 294
      Examples/UICatalog/Scenarios/VkeyPacketSimulator.cs
  34. 5 5
      Examples/UICatalog/Scenarios/Wizards.cs
  35. 0 5
      Examples/UICatalog/UICatalog.cs
  36. 36 40
      Examples/UICatalog/UICatalogTop.cs
  37. 4 4
      README.md
  38. 61 0
      Terminal.Gui.Analyzers.Tests/HandledEventArgsAnalyzerTests.cs
  39. 165 0
      Terminal.Gui.Analyzers.Tests/ProjectBuilder.cs
  40. 35 0
      Terminal.Gui.Analyzers.Tests/Terminal.Gui.Analyzers.Tests.csproj
  41. 6 0
      Terminal.Gui.Analyzers/AnalyzerReleases.Shipped.md
  42. 5 0
      Terminal.Gui.Analyzers/AnalyzerReleases.Unshipped.md
  43. 67 0
      Terminal.Gui.Analyzers/DiagnosticCategory.cs
  44. 269 0
      Terminal.Gui.Analyzers/HandledEventArgsAnalyzer.cs
  45. 34 0
      Terminal.Gui.Analyzers/TGUI001.md
  46. 18 0
      Terminal.Gui.Analyzers/Terminal.Gui.Analyzers.csproj
  47. 8 17
      Terminal.Gui/App/Application.Initialization.cs
  48. 13 133
      Terminal.Gui/App/Application.Mouse.cs
  49. 11 9
      Terminal.Gui/App/Application.Run.cs
  50. 13 4
      Terminal.Gui/App/Application.Screen.cs
  51. 65 39
      Terminal.Gui/App/Application.cd
  52. 24 12
      Terminal.Gui/App/Application.cs
  53. 38 17
      Terminal.Gui/App/ApplicationImpl.cs
  54. 9 139
      Terminal.Gui/App/Clipboard/Clipboard.cs
  55. 79 0
      Terminal.Gui/App/Clipboard/ClipboardProcessRunner.cs
  56. 13 8
      Terminal.Gui/App/IApplication.cs
  57. 0 90
      Terminal.Gui/App/ITimedEvents.cs
  58. 4 32
      Terminal.Gui/App/MainLoop.cs
  59. 16 7
      Terminal.Gui/App/MainLoopSyncContext.cs
  60. 87 0
      Terminal.Gui/App/Mouse/IMouseGrabHandler.cs
  61. 118 0
      Terminal.Gui/App/Mouse/MouseGrabHandler.cs
  62. 4 2
      Terminal.Gui/App/RunState.cs
  63. 0 18
      Terminal.Gui/App/Timeout.cs
  64. 64 0
      Terminal.Gui/App/Timeout/ITimedEvents.cs
  65. 38 0
      Terminal.Gui/App/Timeout/LogarithmicTimeout.cs
  66. 53 0
      Terminal.Gui/App/Timeout/SmoothAcceleratingTimeout.cs
  67. 105 169
      Terminal.Gui/App/Timeout/TimedEvents.cs
  68. 33 0
      Terminal.Gui/App/Timeout/Timeout.cs
  69. 1 1
      Terminal.Gui/App/Timeout/TimeoutEventArgs.cs
  70. 2 2
      Terminal.Gui/Configuration/ConfigurationManager.cs
  71. 27 5
      Terminal.Gui/Configuration/DeepCloner.cs
  72. 2 2
      Terminal.Gui/Configuration/SettingsScope.cs
  73. 1 1
      Terminal.Gui/Drawing/Scheme.cs
  74. 2 2
      Terminal.Gui/Drawing/Sixel/SixelSupportDetector.cs
  75. 9 3
      Terminal.Gui/Drivers/AnsiResponseParser/Keyboard/CsiCursorPattern.cs
  76. 9 2
      Terminal.Gui/Drivers/AnsiResponseParser/Keyboard/EscAsAltPattern.cs
  77. 8 1
      Terminal.Gui/Drivers/AnsiResponseParser/Keyboard/Ss3Pattern.cs
  78. 18 12
      Terminal.Gui/Drivers/ConsoleDriver.cs
  79. 122 336
      Terminal.Gui/Drivers/CursesDriver/CursesDriver.cs
  80. 83 0
      Terminal.Gui/Drivers/CursesDriver/Platform.cs
  81. 2 3
      Terminal.Gui/Drivers/CursesDriver/UnixMainLoop.cs
  82. 57 44
      Terminal.Gui/Drivers/EscSeqUtils/EscSeqUtils.cs
  83. 17 26
      Terminal.Gui/Drivers/FakeDriver/FakeDriver.cs
  84. 0 8
      Terminal.Gui/Drivers/IConsoleDriver.cs
  85. 6 62
      Terminal.Gui/Drivers/NetDriver/NetDriver.cs
  86. 14 18
      Terminal.Gui/Drivers/NetDriver/NetEvents.cs
  87. 2 2
      Terminal.Gui/Drivers/NetDriver/NetMainLoop.cs
  88. 25 41
      Terminal.Gui/Drivers/NetDriver/NetWinVTConsole.cs
  89. 57 66
      Terminal.Gui/Drivers/V2/ApplicationV2.cs
  90. 26 0
      Terminal.Gui/Drivers/V2/ComponentFactory.cs
  91. 54 33
      Terminal.Gui/Drivers/V2/ConsoleDriverFacade.cs
  92. 50 0
      Terminal.Gui/Drivers/V2/IComponentFactory.cs
  93. 14 2
      Terminal.Gui/Drivers/V2/IConsoleDriverFacade.cs
  94. 16 0
      Terminal.Gui/Drivers/V2/IInputProcessor.cs
  95. 10 3
      Terminal.Gui/Drivers/V2/IMainLoop.cs
  96. 1 7
      Terminal.Gui/Drivers/V2/IOutputBuffer.cs
  97. 3 0
      Terminal.Gui/Drivers/V2/IUnixInput.cs
  98. 4 1
      Terminal.Gui/Drivers/V2/IWindowsInput.cs
  99. 59 0
      Terminal.Gui/Drivers/V2/InputProcessor.cs
  100. 17 7
      Terminal.Gui/Drivers/V2/MainLoop.cs

+ 1 - 1
CONTRIBUTING.md

@@ -141,7 +141,7 @@ Great care has been provided thus far in ensuring **Terminal.Gui** has great [AP
 
 ### Defining Events
 
-See https://gui-cs.github.io/Terminal.GuiV2Docs/docs/events.html
+See https://gui-cs.github.io/Terminal.Gui/docs/events.html
 
 
 ### Defining new `View` classes

+ 9 - 5
Directory.Packages.props

@@ -5,22 +5,26 @@
   <ItemGroup>
     <!-- Enable Nuget Source Link for github -->
     <PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" />
+    <PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.11.0" />
+    <PackageVersion Include="Microsoft.CodeAnalysis.VisualBasic.Workspaces" Version="4.11.0" />
+    <PackageVersion Include="Microsoft.CodeAnalysis.Workspaces.MSBuild" Version="4.11.0" />
+    <PackageVersion Include="Microsoft.Net.Compilers.Toolset" Version="4.11.0" />
     <PackageVersion Include="Microsoft.SourceLink.GitHub" Version="[8,9)" />
     <PackageVersion Include="ColorHelper" Version="[1.8.1,2)" />
     <PackageVersion Include="JetBrains.Annotations" Version="[2024.3.0,)" />
-    <PackageVersion Include="Microsoft.CodeAnalysis" Version="[4.11,4.12)" />
-    <PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="[4.11,4.12)" />
+    <PackageVersion Include="Microsoft.CodeAnalysis" Version="4.11.0" />
+    <PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="4.11.0" />
     <PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.11.0" />
-    <PackageVersion Include="Microsoft.Extensions.Logging" Version="[9.0.2,10)" />
+    <PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="[9.0.2,10)" />
+    <PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.6" />
     <PackageVersion Include="System.IO.Abstractions" Version="[22.0.11,23)" />
-    <PackageVersion Include="System.Text.Json" Version="[8.0.5,9)" />
     <PackageVersion Include="Wcwidth" Version="[2,3)" />
     <PackageVersion Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="[1.21.2,2)" />
     <PackageVersion Include="Serilog" Version="4.2.0" />
     <PackageVersion Include="Serilog.Extensions.Logging" Version="9.0.0" />
     <PackageVersion Include="Serilog.Sinks.Debug" Version="3.0.0" />
     <PackageVersion Include="Serilog.Sinks.File" Version="6.0.0" />
-    <PackageVersion Include="SixLabors.ImageSharp" Version="[3.1.7,4)" />
+    <PackageVersion Include="SixLabors.ImageSharp" Version="[3.1.11,4)" />
     <PackageVersion Include="CsvHelper" Version="[33.0.1,34)" />
     <PackageVersion Include="Microsoft.DotNet.PlatformAbstractions" Version="[3.1.6,4)" />
     <PackageVersion Include="System.CommandLine" Version="[2.0.0-beta4.22272.1,3)" />

+ 2 - 2
Examples/NativeAot/Publish_linux-x64_Debug.sh

@@ -1,5 +1,5 @@
 #!/bin/bash
 
-dotnet clean
-dotnet build
+dotnet clean -c Debug
+dotnet build -c Debug
 dotnet publish -c Debug -r linux-x64 --self-contained

+ 2 - 2
Examples/NativeAot/Publish_linux-x64_Release.sh

@@ -1,5 +1,5 @@
 #!/bin/bash
 
-dotnet clean
-dotnet build
+dotnet clean -c Release
+dotnet build -c Release
 dotnet publish -c Release -r linux-x64 --self-contained

+ 2 - 2
Examples/NativeAot/Publish_osx-x64_Debug.sh

@@ -1,5 +1,5 @@
 #!/bin/bash
 
-dotnet clean
-dotnet build
+dotnet clean -c Debug
+dotnet build -c Debug
 dotnet publish -c Debug -r osx-x64 --self-contained

+ 2 - 2
Examples/NativeAot/Publish_osx-x64_Release.sh

@@ -1,5 +1,5 @@
 #!/bin/bash
 
-dotnet clean
-dotnet build
+dotnet clean -c Release
+dotnet build -c Release
 dotnet publish -c Release -r osx-x64 --self-contained

+ 54 - 0
Examples/UICatalog/Properties/launchSettings.json

@@ -42,12 +42,48 @@
       "commandLineArgs": "dotnet UICatalog.dll --driver v2",
       "distributionName": ""
     },
+    "WSL: UICatalog --driver v2unix": {
+      "commandName": "Executable",
+      "executablePath": "wsl",
+      "commandLineArgs": "dotnet UICatalog.dll --driver v2unix",
+      "distributionName": ""
+    },
     "WSL: UICatalog --driver v2net": {
       "commandName": "Executable",
       "executablePath": "wsl",
       "commandLineArgs": "dotnet UICatalog.dll --driver v2net",
       "distributionName": ""
     },
+    "WSL-Gnome: UICatalog": {
+      "commandName": "Executable",
+      "executablePath": "wsl",
+      "commandLineArgs": "bash -c 'while [ ! -e \"$XDG_RUNTIME_DIR/bus\" ]; do sleep 0.1; done; gnome-terminal --wait -- bash -l -c \"dotnet UICatalog.dll; exec bash\"'",
+      "distributionName": ""
+    },
+    "WSL-Gnome: UICatalog --driver NetDriver": {
+      "commandName": "Executable",
+      "executablePath": "wsl",
+      "commandLineArgs": "bash -c 'while [ ! -e \"$XDG_RUNTIME_DIR/bus\" ]; do sleep 0.1; done; gnome-terminal --wait -- bash -l -c \"dotnet UICatalog.dll --driver NetDriver; exec bash\"'",
+      "distributionName": ""
+    },
+    "WSL-Gnome: UICatalog --driver v2": {
+      "commandName": "Executable",
+      "executablePath": "wsl",
+      "commandLineArgs": "bash -c 'dbus-run-session -- gnome-terminal --wait -- bash -l -c \"dotnet UICatalog.dll --driver v2; exec bash\"'",
+      "distributionName": ""
+    },
+    "WSL-Gnome: UICatalog --driver v2unix": {
+      "commandName": "Executable",
+      "executablePath": "wsl",
+      "commandLineArgs": "bash -c 'dbus-run-session -- gnome-terminal --wait -- bash -l -c \"dotnet UICatalog.dll --driver v2unix; exec bash\"'",
+      "distributionName": ""
+    },
+    "WSL-Gnome: UICatalog --driver v2net": {
+      "commandName": "Executable",
+      "executablePath": "wsl",
+      "commandLineArgs": "bash -c 'dbus-run-session -- gnome-terminal --wait -- bash -l -c \"dotnet UICatalog.dll --driver v2net; exec bash\"'",
+      "distributionName": ""
+    },
     "Benchmark All": {
       "commandName": "Project",
       "commandLineArgs": "--benchmark"
@@ -70,6 +106,24 @@
       "commandLineArgs": "dotnet UICatalog.dll --benchmark",
       "distributionName": ""
     },
+    "WSL: Benchmark All --driver v2": {
+      "commandName": "Executable",
+      "executablePath": "wsl",
+      "commandLineArgs": "dotnet UICatalog.dll --driver v2 --benchmark",
+      "distributionName": ""
+    },
+    "WSL: Benchmark All --driver v2unix": {
+      "commandName": "Executable",
+      "executablePath": "wsl",
+      "commandLineArgs": "dotnet UICatalog.dll --driver v2unix --benchmark",
+      "distributionName": ""
+    },
+    "WSL: Benchmark All --driver v2net": {
+      "commandName": "Executable",
+      "executablePath": "wsl",
+      "commandLineArgs": "dotnet UICatalog.dll --driver v2net --benchmark",
+      "distributionName": ""
+    },
     "Docker": {
       "commandName": "Docker"
     },

+ 1 - 1
Examples/UICatalog/Resources/config.json

@@ -1,5 +1,5 @@
 {
-  "$schema": "https://gui-cs.github.io/Terminal.GuiV2Docs/schemas/tui-config-schema.json",
+  "$schema": "https://gui-cs.github.io/Terminal.Gui/schemas/tui-config-schema.json",
   "FileDialog.MaxSearchResults": 10000,
   "FileDialogStyle.DefaultUseColors": false,
   "FileDialogStyle.DefaultUseUnicodeCharacters": false,

+ 1 - 1
Examples/UICatalog/Scenarios/Adornments.cs

@@ -35,7 +35,7 @@ public class Adornments : Scenario
             Title = "The _Window",
             Arrangement = ViewArrangement.Overlapped | ViewArrangement.Movable,
 
-            Width = Dim.Fill (Dim.Func (() => editor.Frame.Width)),
+            Width = Dim.Fill (Dim.Func (_ => editor.Frame.Width)),
             Height = Dim.Fill ()
         };
         app.Add (window);

+ 4 - 4
Examples/UICatalog/Scenarios/AllViewsTester.cs

@@ -101,7 +101,7 @@ public class AllViewsTester : Scenario
         {
             Title = "Arrangement [_3]",
             X = Pos.Right (_classListView) - 1,
-            Y = Pos.Bottom (_adornmentsEditor) - Pos.Func (() => _adornmentsEditor.Frame.Height == 1 ? 0 : 1),
+            Y = Pos.Bottom (_adornmentsEditor) - Pos.Func (_ => _adornmentsEditor.Frame.Height == 1 ? 0 : 1),
             Width = Dim.Width (_adornmentsEditor),
             Height = Dim.Fill (),
             AutoSelectViewToEdit = false,
@@ -134,7 +134,7 @@ public class AllViewsTester : Scenario
         {
             Title = "ViewportSettings [_5]",
             X = Pos.Right (_arrangementEditor) - 1,
-            Y = Pos.Bottom (_layoutEditor) - Pos.Func (() => _layoutEditor.Frame.Height == 1 ? 0 : 1),
+            Y = Pos.Bottom (_layoutEditor) - Pos.Func (_ => _layoutEditor.Frame.Height == 1 ? 0 : 1),
             Width = Dim.Width (_layoutEditor),
             Height = Dim.Auto (),
             CanFocus = true,
@@ -148,7 +148,7 @@ public class AllViewsTester : Scenario
         {
             Title = "View Properties [_6]",
             X = Pos.Right (_adornmentsEditor) - 1,
-            Y = Pos.Bottom (_viewportSettingsEditor) - Pos.Func (() => _viewportSettingsEditor.Frame.Height == 1 ? 0 : 1),
+            Y = Pos.Bottom (_viewportSettingsEditor) - Pos.Func (_ => _viewportSettingsEditor.Frame.Height == 1 ? 0 : 1),
             Width = Dim.Width (_layoutEditor),
             Height = Dim.Auto (),
             CanFocus = true,
@@ -171,7 +171,7 @@ public class AllViewsTester : Scenario
 
         _layoutEditor.Width = Dim.Fill (
                                         Dim.Func (
-                                                  () =>
+                                                  _ =>
                                                   {
                                                       if (_eventLog.NeedsLayout)
                                                       {

+ 1 - 1
Examples/UICatalog/Scenarios/Arrangement.cs

@@ -66,7 +66,7 @@ public class Arrangement : Scenario
         View tiledView3 = CreateTiledView (2, Pos.Right (tiledView2) - 1, Pos.Top (tiledView2));
         tiledView3.Height = Dim.Height (tiledView1);
         View tiledView4 = CreateTiledView (3, Pos.Left (tiledView1), Pos.Bottom (tiledView1) - 1);
-        tiledView4.Width = Dim.Func (() => tiledView3.Frame.Width + tiledView2.Frame.Width + tiledView1.Frame.Width - 2);
+        tiledView4.Width = Dim.Func (_ => tiledView3.Frame.Width + tiledView2.Frame.Width + tiledView1.Frame.Width - 2);
 
         View movableSizeableWithProgress = CreateOverlappedView (2, 10, 8);
         movableSizeableWithProgress.Title = "Movable _& Sizable";

+ 81 - 32
Examples/UICatalog/Scenarios/CharacterMap/CharacterMap.cs

@@ -1,9 +1,6 @@
 #nullable enable
 
-using System;
-using System.Collections.Generic;
 using System.Globalization;
-using System.Linq;
 using System.Text;
 
 namespace UICatalog.Scenarios;
@@ -24,6 +21,33 @@ public class CharacterMap : Scenario
     private Label? _errorLabel;
     private TableView? _categoryList;
     private CharMap? _charMap;
+    private OptionSelector? _unicodeCategorySelector;
+
+    public override List<Key> GetDemoKeyStrokes ()
+    {
+        List<Key> keys = new ();
+
+        for (var i = 0; i < 200; i++)
+        {
+            keys.Add (Key.CursorDown);
+        }
+
+        // Category table
+        keys.Add (Key.Tab.WithShift);
+
+        // Block elements
+        keys.Add (Key.B);
+        keys.Add (Key.L);
+
+        keys.Add (Key.Tab);
+
+        for (var i = 0; i < 200; i++)
+        {
+            keys.Add (Key.CursorLeft);
+        }
+
+        return keys;
+    }
 
     // Don't create a Window, just return the top-level view
     public override void Main ()
@@ -39,10 +63,9 @@ public class CharacterMap : Scenario
         {
             X = 0,
             Y = 1,
-            Width = Dim.Fill (Dim.Func (() => _categoryList!.Frame.Width)),
-            Height = Dim.Fill (),
-           // SchemeName = "Base"
+            Height = Dim.Fill ()
 
+            // SchemeName = "Base"
         };
         top.Add (_charMap);
 
@@ -51,7 +74,8 @@ public class CharacterMap : Scenario
             X = Pos.Right (_charMap) + 1,
             Y = Pos.Y (_charMap),
             HotKeySpecifier = (Rune)'_',
-            Text = "_Jump To:",
+            Text = "_Jump To:"
+
             //SchemeName = "Dialog"
         };
         top.Add (jumpLabel);
@@ -61,7 +85,8 @@ public class CharacterMap : Scenario
             X = Pos.Right (jumpLabel) + 1,
             Y = Pos.Y (_charMap),
             Width = 17,
-            Caption = "e.g. 01BE3 or ✈",
+            Caption = "e.g. 01BE3 or ✈"
+
             //SchemeName = "Dialog"
         };
         top.Add (jumpEdit);
@@ -90,10 +115,12 @@ public class CharacterMap : Scenario
 
         jumpEdit.Accepting += JumpEditOnAccept;
 
-        _categoryList = new () { 
-            X = Pos.Right (_charMap), 
-            Y = Pos.Bottom (jumpLabel), 
-            Height = Dim.Fill (),
+        _categoryList = new ()
+        {
+            X = Pos.Right (_charMap),
+            Y = Pos.Bottom (jumpLabel),
+            Height = Dim.Fill ()
+
             //SchemeName = "Dialog"
         };
         _categoryList.FullRowSelect = true;
@@ -166,12 +193,14 @@ public class CharacterMap : Scenario
                     ),
                 new (
                      "_Options",
-                     new MenuItemv2 [] { CreateMenuShowWidth () }
+                     [CreateMenuShowWidth (), CreateMenuUnicodeCategorySelector ()]
                     )
             ]
         };
         top.Add (menu);
 
+        _charMap.Width = Dim.Fill (Dim.Func (v => v!.Frame.Width, _categoryList));
+
         _charMap.SelectedCodePoint = 0;
         _charMap.SetFocus ();
 
@@ -316,6 +345,7 @@ public class CharacterMap : Scenario
             CheckedState = _charMap!.ShowGlyphWidths ? CheckState.Checked : CheckState.None
         };
         var item = new MenuItemv2 { CommandView = cb };
+
         item.Action += () =>
                        {
                            if (_charMap is { })
@@ -327,29 +357,48 @@ public class CharacterMap : Scenario
         return item;
     }
 
-    public override List<Key> GetDemoKeyStrokes ()
+    private MenuItemv2 CreateMenuUnicodeCategorySelector ()
     {
-        List<Key> keys = new ();
-
-        for (var i = 0; i < 200; i++)
+        // First option is "All" (no filter), followed by all UnicodeCategory names
+        string [] allCategoryNames = Enum.GetNames<UnicodeCategory> ();
+        var options = new string [allCategoryNames.Length + 1];
+        options [0] = "All";
+        Array.Copy (allCategoryNames, 0, options, 1, allCategoryNames.Length);
+
+        // TODO: When #4126 is merged update this to use OptionSelector<UnicodeCategory?>
+        var selector = new OptionSelector
         {
-            keys.Add (Key.CursorDown);
-        }
-
-        // Category table
-        keys.Add (Key.Tab.WithShift);
+            AssignHotKeysToCheckBoxes = true,
+            Options = options
+        };
 
-        // Block elements
-        keys.Add (Key.B);
-        keys.Add (Key.L);
+        _unicodeCategorySelector = selector;
 
-        keys.Add (Key.Tab);
+        // Default to "All"
+        selector.SelectedItem = 0;
+        _charMap!.ShowUnicodeCategory = null;
 
-        for (var i = 0; i < 200; i++)
-        {
-            keys.Add (Key.CursorLeft);
-        }
-
-        return keys;
+        selector.SelectedItemChanged += (s, e) =>
+                                        {
+                                            int? idx = selector.SelectedItem;
+
+                                            if (idx is null)
+                                            {
+                                                return;
+                                            }
+
+                                            if (idx.Value == 0)
+                                            {
+                                                _charMap.ShowUnicodeCategory = null;
+                                            }
+                                            else
+                                            {
+                                                // Map index to UnicodeCategory (offset by 1 because 0 is "All")
+                                                UnicodeCategory cat = Enum.GetValues<UnicodeCategory> () [idx.Value - 1];
+                                                _charMap.ShowUnicodeCategory = cat;
+                                            }
+                                        };
+
+        return new() { CommandView = selector };
     }
 }

+ 72 - 16
Examples/UICatalog/Scenarios/CombiningMarks.cs

@@ -11,22 +11,78 @@ public class CombiningMarks : Scenario
         var top = new Toplevel ();
 
         top.DrawComplete += (s, e) =>
-                                   {
-                                       top.Move (0, 0);
-                                       top.AddStr ("Terminal.Gui only supports combining marks that normalize. See Issue #2616.");
-                                       top.Move (0, 2);
-                                       top.AddStr ("\u0301\u0301\u0328<- \"\\u301\\u301\\u328]\" using AddStr.");
-                                       top.Move (0, 3);
-                                       top.AddStr ("[a\u0301\u0301\u0328]<- \"[a\\u301\\u301\\u328]\" using AddStr.");
-                                       top.Move (0, 4);
-                                       top.AddRune ('[');
-                                       top.AddRune ('a');
-                                       top.AddRune ('\u0301');
-                                       top.AddRune ('\u0301');
-                                       top.AddRune ('\u0328');
-                                       top.AddRune (']');
-                                       top.AddStr ("<- \"[a\\u301\\u301\\u328]\" using AddRune for each.");
-                                   };
+        {
+            // Forces reset _lineColsOffset because we're dealing with direct draw
+            Application.ClearScreenNextIteration = true;
+
+            var i = -1;
+            top.AddStr ("Terminal.Gui only supports combining marks that normalize. See Issue #2616.");
+            top.Move (0, ++i);
+            top.AddStr ("\u0301<- \"\\u0301\" using AddStr.");
+            top.Move (0, ++i);
+            top.AddStr ("[\u0301]<- \"[\\u0301]\" using AddStr.");
+            top.Move (0, ++i);
+            top.AddStr ("[ \u0301]<- \"[ \\u0301]\" using AddStr.");
+            top.Move (0, ++i);
+            top.AddStr ("[\u0301 ]<- \"[\\u0301 ]\" using AddStr.");
+            top.Move (0, ++i);
+            top.AddStr ("\u0301\u0301\u0328<- \"\\u0301\\u0301\\u0328\" using AddStr.");
+            top.Move (0, ++i);
+            top.AddStr ("[\u0301\u0301\u0328]<- \"[\\u0301\\u0301\\u0328]\" using AddStr.");
+            top.Move (0, ++i);
+            top.AddStr ("[a\u0301\u0301\u0328]<- \"[a\\u0301\\u0301\\u0328]\" using AddStr.");
+            top.Move (0, ++i);
+            top.AddRune ('[');
+            top.AddRune ('a');
+            top.AddRune ('\u0301');
+            top.AddRune ('\u0301');
+            top.AddRune ('\u0328');
+            top.AddRune (']');
+            top.AddStr ("<- \"[a\\u0301\\u0301\\u0328]\" using AddRune for each.");
+            top.Move (0, ++i);
+            top.AddStr ("[a\u0301\u0301\u0328]<- \"[a\\u0301\\u0301\\u0328]\" using AddStr.");
+            top.Move (0, ++i);
+            top.AddStr ("[e\u0301\u0301\u0328]<- \"[e\\u0301\\u0301\\u0328]\" using AddStr.");
+            top.Move (0, ++i);
+            top.AddStr ("[e\u0328\u0301]<- \"[e\\u0328\\u0301]\" using AddStr.");
+            top.Move (0, ++i);
+            top.AddStr ("\u00ad<- \"\\u00ad\" using AddStr.");
+            top.Move (0, ++i);
+            top.AddStr ("[\u00ad]<- \"[\\u00ad]\" using AddStr.");
+            top.Move (0, ++i);
+            top.AddRune ('[');
+            top.AddRune ('\u00ad');
+            top.AddRune (']');
+            top.AddStr ("<- \"[\\u00ad]\" using AddRune for each.");
+            i++;
+            top.Move (0, ++i);
+            top.AddStr ("From now on we are using TextFormatter");
+            TextFormatter tf = new () { Text = "[e\u0301\u0301\u0328]<- \"[e\\u0301\\u0301\\u0328]\" using TextFormatter." };
+            tf.Draw (new (0, ++i, tf.Text.Length, 1), top.GetAttributeForRole (VisualRole.Normal), top.GetAttributeForRole (VisualRole.Normal));
+            tf.Text = "[e\u0328\u0301]<- \"[e\\u0328\\u0301]\" using TextFormatter.";
+            tf.Draw (new (0, ++i, tf.Text.Length, 1), top.GetAttributeForRole (VisualRole.Normal), top.GetAttributeForRole (VisualRole.Normal));
+            i++;
+            top.Move (0, ++i);
+            top.AddStr ("From now on we are using Surrogate pairs with combining diacritics");
+            top.Move (0, ++i);
+            top.AddStr ("[\ud835\udc4b\u0302]<- \"[\\ud835\\udc4b\\u0302]\" using AddStr.");
+            top.Move (0, ++i);
+            top.AddStr ("[\ud83d\udc68\ud83e\uddd2]<- \"[\\ud83d\\udc68\\ud83e\\uddd2]\" using AddStr.");
+            top.Move (0, ++i);
+            top.AddStr ("\u200d<- \"\\u200d\" using AddStr.");
+            top.Move (0, ++i);
+            top.AddStr ("[\u200d]<- \"[\\u200d]\" using AddStr.");
+            top.Move (0, ++i);
+            top.AddStr ("[\ud83d\udc68\u200d\ud83e\uddd2]<- \"[\\ud83d\\udc68\\u200d\\ud83e\\uddd2]\" using AddStr.");
+            top.Move (0, ++i);
+            top.AddStr ("[\U0001F469\U0001F9D2]<- \"[\\U0001F469\\U0001F9D2]\" using AddStr.");
+            top.Move (0, ++i);
+            top.AddStr ("[\U0001F469\u200D\U0001F9D2]<- \"[\\U0001F469\\u200D\\U0001F9D2]\" using AddStr.");
+            top.Move (0, ++i);
+            top.AddStr ("[\U0001F468\U0001F469\U0001F9D2]<- \"[\\U0001F468\\U0001F469\\U0001F9D2]\" using AddStr.");
+            top.Move (0, ++i);
+            top.AddStr ("[\U0001F468\u200D\U0001F469\u200D\U0001F9D2]<- \"[\\U0001F468\\u200D\\U0001F469\\u200D\\U0001F9D2]\" using AddStr.");
+        };
 
         Application.Run (top);
         top.Dispose ();

+ 1 - 1
Examples/UICatalog/Scenarios/ConfigurationEditor.cs

@@ -55,7 +55,7 @@ public class ConfigurationEditor : Scenario
         _tabView = new ()
         {
             Width = Dim.Fill (),
-            Height = Dim.Fill (Dim.Func (() => statusBar.Frame.Height))
+            Height = Dim.Fill (Dim.Func (_ => statusBar.Frame.Height))
         };
 
         win.Add (_tabView, statusBar);

+ 1 - 1
Examples/UICatalog/Scenarios/DynamicMenuBar.cs

@@ -598,7 +598,7 @@ public class DynamicMenuBar : Scenario
                 X = Pos.Right (btnPrevious) + 1,
                 Y = Pos.Top (btnPrevious),
 
-                Width = Dim.Fill () - Dim.Func (() => btnAdd.Frame.Width + 1),
+                Width = Dim.Fill () - Dim.Func (_ => btnAdd.Frame.Width + 1),
                 Height = 1
             };
 

+ 1 - 1
Examples/UICatalog/Scenarios/EditorsAndHelpers/AdornmentEditor.cs

@@ -100,7 +100,7 @@ public class AdornmentEditor : EditorBase
 
         _leftEdit = new ()
         {
-            X = Pos.Left (_topEdit) - Pos.Func (() => _topEdit.Text.Length) - 2, Y = Pos.Bottom (_topEdit),
+            X = Pos.Left (_topEdit) - Pos.Func (_ => _topEdit.Text.Length) - 2, Y = Pos.Bottom (_topEdit),
             Format = _topEdit.Format
         };
 

+ 1 - 1
Examples/UICatalog/Scenarios/EditorsAndHelpers/AdornmentsEditor.cs

@@ -122,7 +122,7 @@ public class AdornmentsEditor : EditorBase
         PaddingEditor.Border!.Thickness = PaddingEditor.Border.Thickness with { Bottom = 0 };
         Add (PaddingEditor);
 
-        Width = Dim.Auto (maximumContentDim: Dim.Func (() => MarginEditor.Frame.Width - 2));
+        Width = Dim.Auto (maximumContentDim: Dim.Func (_ => MarginEditor.Frame.Width - 2));
 
         MarginEditor.ExpanderButton!.Collapsed = true;
         BorderEditor.ExpanderButton!.Collapsed = true;

+ 3 - 3
Examples/UICatalog/Scenarios/EditorsAndHelpers/DimEditor.cs

@@ -68,7 +68,7 @@ public class DimEditor : EditorBase
                 break;
             case DimFunc func:
                 _valueEdit.Enabled = true;
-                _value = func.Fn ();
+                _value = func.Fn (null);
                 _valueEdit!.Text = _value.ToString ();
                 break;
             case DimPercent percent:
@@ -98,7 +98,7 @@ public class DimEditor : EditorBase
         {
             X = Pos.Right (label) + 1,
             Y = 0,
-            Width = Dim.Func (() => _radioItems.Max (i => i.GetColumns ()) - label.Frame.Width + 1),
+            Width = Dim.Func (_ => _radioItems.Max (i => i.GetColumns ()) - label.Frame.Width + 1),
             Text = $"{_value}"
         };
 
@@ -141,7 +141,7 @@ public class DimEditor : EditorBase
                 0 => Dim.Absolute (_value),
                 1 => Dim.Auto (),
                 2 => Dim.Fill (_value),
-                3 => Dim.Func (() => _value),
+                3 => Dim.Func (_ => _value),
                 4 => Dim.Percent (_value),
                 _ => Dimension == Dimension.Width ? ViewToEdit.Width : ViewToEdit.Height
             };

+ 1 - 1
Examples/UICatalog/Scenarios/EditorsAndHelpers/EventLog.cs

@@ -20,7 +20,7 @@ public class EventLog : ListView
         Y = 0;
 
         Width = Dim.Func (
-                          () =>
+                          _ =>
                           {
                               if (!IsInitialized)
                               {

+ 3 - 3
Examples/UICatalog/Scenarios/EditorsAndHelpers/PosEditor.cs

@@ -70,7 +70,7 @@ public class PosEditor : EditorBase
                 break;
             case PosFunc func:
                 _valueEdit.Enabled = true;
-                _value = func.Fn ();
+                _value = func.Fn (null);
                 _valueEdit!.Text = _value.ToString ();
 
                 break;
@@ -98,7 +98,7 @@ public class PosEditor : EditorBase
         {
             X = Pos.Right (label) + 1,
             Y = 0,
-            Width = Dim.Func (() => _radioItems.Max (i => i.GetColumns ()) - label.Frame.Width + 1),
+            Width = Dim.Func (_ => _radioItems.Max (i => i.GetColumns ()) - label.Frame.Width + 1),
             Text = $"{_value}"
         };
 
@@ -142,7 +142,7 @@ public class PosEditor : EditorBase
                            1 => Pos.Align (Alignment.Start),
                            2 => new PosAnchorEnd (),
                            3 => Pos.Center (),
-                           4 => Pos.Func (() => _value),
+                           4 => Pos.Func (_ => _value),
                            5 => Pos.Percent (_value),
                            _ => Dimension == Dimension.Width ? ViewToEdit.X : ViewToEdit.Y
                        };

+ 1 - 1
Examples/UICatalog/Scenarios/ListColumns.cs

@@ -252,7 +252,7 @@ public class ListColumns : Scenario
 
         top.Add (menu, appWindow, statusBar);
         appWindow.Y = 1;
-        appWindow.Height = Dim.Fill(Dim.Func (() => statusBar.Frame.Height));
+        appWindow.Height = Dim.Fill(Dim.Func (_ => statusBar.Frame.Height));
 
         // Run - Start the application.
         Application.Run (top);

+ 1 - 1
Examples/UICatalog/Scenarios/ListViewWithSelection.cs

@@ -76,7 +76,7 @@ public class ListViewWithSelection : Scenario
             Title = "_ListView",
             X = 0,
             Y = Pos.Bottom (_allowMarkingCb),
-            Width = Dim.Func (() => _listView?.MaxLength ?? 10),
+            Width = Dim.Func (_ => _listView?.MaxLength ?? 10),
             Height = Dim.Fill (),
             AllowsMarking = false,
             AllowsMultipleSelection = false,

+ 1 - 1
Examples/UICatalog/Scenarios/Mouse.cs

@@ -109,7 +109,7 @@ public class Mouse : Scenario
                                    X = 0,
                                    Y = 0,
                                    Width = Dim.Fill (),
-                                   Height = Dim.Func (() => demo.Padding.Thickness.Top),
+                                   Height = Dim.Func (_ => demo.Padding.Thickness.Top),
                                    Title = "inPadding",
                                    Id = "inPadding"
                                });

+ 1 - 1
Examples/UICatalog/Scenarios/NumericUpDownDemo.cs

@@ -252,7 +252,7 @@ internal class NumericUpDownEditor<T> : View where T : notnull
             {
                 X = Pos.Center (),
                 Y = Pos.Bottom (_increment) + 1,
-                Increment = (dynamic)1,
+                Increment = NumericUpDown<int>.TryConvert (1, out T? increment) ? increment : default (T?),
             };
 
             _numericUpDown.ValueChanged += NumericUpDownOnValueChanged;

+ 8 - 8
Examples/UICatalog/Scenarios/ScrollBarDemo.cs

@@ -72,7 +72,7 @@ public class ScrollBarDemo : Scenario
             Text = "_Width/Height:",
             TextAlignment = Alignment.End,
             Y = Pos.Align (Alignment.Start, AlignmentModes.StartToEnd, groupId: 1),
-            Width = Dim.Func (() => GetMaxLabelWidth (1))
+            Width = Dim.Func (_ => GetMaxLabelWidth (1))
         };
         demoFrame.Add (lblWidthHeight);
 
@@ -114,7 +114,7 @@ public class ScrollBarDemo : Scenario
             Text = "_Orientation:",
             TextAlignment = Alignment.End,
             Y = Pos.Align (Alignment.Start, groupId: 1),
-            Width = Dim.Func (() => GetMaxLabelWidth (1))
+            Width = Dim.Func (_ => GetMaxLabelWidth (1))
         };
         demoFrame.Add (lblOrientationLabel);
 
@@ -160,7 +160,7 @@ public class ScrollBarDemo : Scenario
             Text = "Scrollable_ContentSize:",
             TextAlignment = Alignment.End,
             Y = Pos.Align (Alignment.Start, groupId: 1),
-            Width = Dim.Func (() => GetMaxLabelWidth (1))
+            Width = Dim.Func (_ => GetMaxLabelWidth (1))
         };
         demoFrame.Add (lblSize);
 
@@ -193,7 +193,7 @@ public class ScrollBarDemo : Scenario
             Text = "_VisibleContentSize:",
             TextAlignment = Alignment.End,
             Y = Pos.Align (Alignment.Start, groupId: 1),
-            Width = Dim.Func (() => GetMaxLabelWidth (1))
+            Width = Dim.Func (_ => GetMaxLabelWidth (1))
         };
         demoFrame.Add (lblVisibleContentSize);
 
@@ -226,7 +226,7 @@ public class ScrollBarDemo : Scenario
             Text = "_Position:",
             TextAlignment = Alignment.End,
             Y = Pos.Align (Alignment.Start, groupId: 1),
-            Width = Dim.Func (() => GetMaxLabelWidth (1))
+            Width = Dim.Func (_ => GetMaxLabelWidth (1))
 
         };
         demoFrame.Add (lblPosition);
@@ -264,7 +264,7 @@ public class ScrollBarDemo : Scenario
             Text = "Options:",
             TextAlignment = Alignment.End,
             Y = Pos.Align (Alignment.Start, groupId: 1),
-            Width = Dim.Func (() => GetMaxLabelWidth (1))
+            Width = Dim.Func (_ => GetMaxLabelWidth (1))
         };
         demoFrame.Add (lblOptions);
         var autoShow = new CheckBox
@@ -282,7 +282,7 @@ public class ScrollBarDemo : Scenario
             Text = "SliderPosition:",
             TextAlignment = Alignment.End,
             Y = Pos.Align (Alignment.Start, groupId: 1),
-            Width = Dim.Func (() => GetMaxLabelWidth (1))
+            Width = Dim.Func (_ => GetMaxLabelWidth (1))
         };
         demoFrame.Add (lblSliderPosition);
 
@@ -299,7 +299,7 @@ public class ScrollBarDemo : Scenario
             Text = "Scrolled:",
             TextAlignment = Alignment.End,
             Y = Pos.Align (Alignment.Start, groupId: 1),
-            Width = Dim.Func (() => GetMaxLabelWidth (1))
+            Width = Dim.Func (_ => GetMaxLabelWidth (1))
 
         };
         demoFrame.Add (lblScrolled);

+ 1 - 4
Examples/UICatalog/Scenarios/Scrolling.cs

@@ -1,7 +1,4 @@
-using System;
-using Microsoft.CodeAnalysis.CSharp.Syntax;
-
-namespace UICatalog.Scenarios;
+namespace UICatalog.Scenarios;
 
 [ScenarioMetadata ("Scrolling", "Content scrolling, IScrollBars, etc...")]
 [ScenarioCategory ("Controls")]

+ 0 - 117
Examples/UICatalog/Scenarios/SendKeys.cs

@@ -1,117 +0,0 @@
-using System;
-
-namespace UICatalog.Scenarios;
-
-[ScenarioMetadata ("SendKeys", "SendKeys sample - Send key combinations.")]
-[ScenarioCategory ("Mouse and Keyboard")]
-public class SendKeys : Scenario
-{
-    public override void Main ()
-    {
-        Application.Init ();
-        var win = new Window { Title = GetQuitKeyAndName () };
-        var label = new Label { X = Pos.Center (), Y = Pos.Center () - 6, Text = "Insert the text to send:" };
-        win.Add (label);
-
-        var txtInput = new TextField { X = Pos.Center (), Y = Pos.Center () - 5, Width = 20, Text = "MockKeyPresses" };
-        win.Add (txtInput);
-
-        var ckbShift = new CheckBox { X = Pos.Center (), Y = Pos.Center () - 4, Text = "Shift" };
-        win.Add (ckbShift);
-
-        var ckbAlt = new CheckBox { X = Pos.Center (), Y = Pos.Center () - 3, Text = "Alt" };
-        win.Add (ckbAlt);
-
-        var ckbControl = new CheckBox { X = Pos.Center (), Y = Pos.Center () - 2, Text = "Control" };
-        win.Add (ckbControl);
-
-        label = new Label { X = Pos.Center (), Y = Pos.Center () + 1, Text = "Result keys:" };
-        win.Add (label);
-
-        var txtResult = new TextField { X = Pos.Center (), Y = Pos.Center () + 2, Width = 20 };
-        win.Add (txtResult);
-
-        var rKeys = "";
-        var rControlKeys = "";
-        var IsShift = false;
-        var IsAlt = false;
-        var IsCtrl = false;
-
-        txtResult.KeyDown += (s, e) =>
-                             {
-                                 rKeys += (char)e.KeyCode;
-
-                                 if (!IsShift && e.IsShift)
-                                 {
-                                     rControlKeys += " Shift ";
-                                     IsShift = true;
-                                 }
-
-                                 if (!IsAlt && e.IsAlt)
-                                 {
-                                     rControlKeys += " Alt ";
-                                     IsAlt = true;
-                                 }
-
-                                 if (!IsCtrl && e.IsCtrl)
-                                 {
-                                     rControlKeys += " Ctrl ";
-                                     IsCtrl = true;
-                                 }
-                             };
-
-        var lblShippedKeys = new Label { X = Pos.Center (), Y = Pos.Center () + 3 };
-        win.Add (lblShippedKeys);
-
-        var lblShippedControlKeys = new Label { X = Pos.Center (), Y = Pos.Center () + 5 };
-        win.Add (lblShippedControlKeys);
-
-        var button = new Button { X = Pos.Center (), Y = Pos.Center () + 7, IsDefault = true, Text = "Process keys" };
-        win.Add (button);
-
-        void ProcessInput ()
-        {
-            rKeys = "";
-            rControlKeys = "";
-            txtResult.Text = "";
-            IsShift = false;
-            IsAlt = false;
-            IsCtrl = false;
-            txtResult.SetFocus ();
-
-            foreach (char r in txtInput.Text)
-            {
-                ConsoleKey ck = char.IsLetter (r)
-                                    ? (ConsoleKey)char.ToUpper (r)
-                                    : (ConsoleKey)r;
-
-                Application.Driver?.SendKeys (
-                                             r,
-                                             ck,
-                                             ckbShift.CheckedState == CheckState.Checked,
-                                             ckbAlt.CheckedState == CheckState.Checked,
-                                             ckbControl.CheckedState == CheckState.Checked
-                                            );
-            }
-
-            lblShippedKeys.Text = rKeys;
-            lblShippedControlKeys.Text = rControlKeys;
-            txtInput.SetFocus ();
-        }
-
-        button.Accepting += (s, e) => ProcessInput ();
-
-        win.KeyDown += (s, e) =>
-                       {
-                           if (e.KeyCode == KeyCode.Enter)
-                           {
-                               ProcessInput ();
-                               e.Handled = true;
-                           }
-                       };
-
-        Application.Run (win);
-        win.Dispose ();
-        Application.Shutdown ();
-    }
-}

+ 7 - 7
Examples/UICatalog/Scenarios/Shortcuts.cs

@@ -43,14 +43,14 @@ public class Shortcuts : Scenario
         };
 
         eventLog.Width = Dim.Func (
-                                   () => Math.Min (
-                                                   Application.Top.Viewport.Width / 2,
-                                                   eventLog?.MaxLength + eventLog!.GetAdornmentsThickness ().Horizontal ?? 0));
+                                   _ => Math.Min (
+                                                  Application.Top.Viewport.Width / 2,
+                                                  eventLog?.MaxLength + eventLog!.GetAdornmentsThickness ().Horizontal ?? 0));
 
         eventLog.Width = Dim.Func (
-                                   () => Math.Min (
-                                                   eventLog.SuperView!.Viewport.Width / 2,
-                                                   eventLog?.MaxLength + eventLog!.GetAdornmentsThickness ().Horizontal ?? 0));
+                                   _ => Math.Min (
+                                                  eventLog.SuperView!.Viewport.Width / 2,
+                                                  eventLog?.MaxLength + eventLog!.GetAdornmentsThickness ().Horizontal ?? 0));
         Application.Top.Add (eventLog);
 
         var alignKeysShortcut = new Shortcut
@@ -193,7 +193,7 @@ public class Shortcuts : Scenario
             Id = "appShortcut",
             X = 0,
             Y = Pos.Bottom (canFocusShortcut),
-            Width = Dim.Fill (Dim.Func (() => eventLog.Frame.Width)),
+            Width = Dim.Fill (Dim.Func (_ => eventLog.Frame.Width)),
             Title = "A_pp Shortcut",
             Key = Key.F1,
             Text = "Width is DimFill",

+ 1 - 1
Examples/UICatalog/Scenarios/SingleBackgroundWorker.cs

@@ -83,7 +83,7 @@ public class SingleBackgroundWorker : Scenario
             workerLogTop.Add (_listLog);
 
             workerLogTop.Y = 1;
-            workerLogTop.Height = Dim.Fill (Dim.Func (() => statusBar.Frame.Height));
+            workerLogTop.Height = Dim.Fill (Dim.Func (_ => statusBar.Frame.Height));
 
             Add (menu, workerLogTop, statusBar);
             Title = "MainApp";

+ 16 - 13
Examples/UICatalog/Scenarios/TextAlignmentAndDirection.cs

@@ -410,7 +410,7 @@ public class TextAlignmentAndDirection : Scenario
         // Save Alignment in Data
         foreach (View t in multiLineLabels)
         {
-            t.Data = new { h = t.TextAlignment, v = t.VerticalTextAlignment };
+            t.Data = new TextAlignmentData (t.TextAlignment, t.VerticalTextAlignment);
         }
 
         container.Add (txtLabelTL);
@@ -594,8 +594,9 @@ public class TextAlignmentAndDirection : Scenario
 
                 foreach (View t in multiLineLabels)
                 {
-                    t.TextAlignment = (Alignment)((dynamic)t.Data).h;
-                    t.VerticalTextAlignment = (Alignment)((dynamic)t.Data).v;
+                    var data = (TextAlignmentData)t.Data;
+                    t.TextAlignment = data!.h;
+                    t.VerticalTextAlignment = data.v;
                 }
             }
             else
@@ -607,24 +608,23 @@ public class TextAlignmentAndDirection : Scenario
                         justifyOptions.Enabled = true;
                     }
 
+                    var data = (TextAlignmentData)t.Data;
+
                     if (TextFormatter.IsVerticalDirection (t.TextDirection))
                     {
                         switch (justifyOptions.SelectedItem)
                         {
                             case 0:
                                 t.VerticalTextAlignment = Alignment.Fill;
-                                t.TextAlignment = ((dynamic)t.Data).h;
-
+                                t.TextAlignment = data!.h;
                                 break;
                             case 1:
-                                t.VerticalTextAlignment = (Alignment)((dynamic)t.Data).v;
+                                t.VerticalTextAlignment = data!.v;
                                 t.TextAlignment = Alignment.Fill;
-
                                 break;
                             case 2:
                                 t.VerticalTextAlignment = Alignment.Fill;
                                 t.TextAlignment = Alignment.Fill;
-
                                 break;
                         }
                     }
@@ -634,18 +634,15 @@ public class TextAlignmentAndDirection : Scenario
                         {
                             case 0:
                                 t.TextAlignment = Alignment.Fill;
-                                t.VerticalTextAlignment = ((dynamic)t.Data).v;
-
+                                t.VerticalTextAlignment = data!.v;
                                 break;
                             case 1:
-                                t.TextAlignment = (Alignment)((dynamic)t.Data).h;
+                                t.TextAlignment = data!.h;
                                 t.VerticalTextAlignment = Alignment.Fill;
-
                                 break;
                             case 2:
                                 t.TextAlignment = Alignment.Fill;
                                 t.VerticalTextAlignment = Alignment.Fill;
-
                                 break;
                         }
                     }
@@ -653,4 +650,10 @@ public class TextAlignmentAndDirection : Scenario
             }
         }
     }
+
+    private class TextAlignmentData (Alignment h, Alignment v)
+    {
+        public Alignment h { get; } = h;
+        public Alignment v { get; } = v;
+    }
 }

+ 88 - 0
Examples/UICatalog/Scenarios/Threading.cs

@@ -19,6 +19,16 @@ public class Threading : Scenario
     private ListView _logJob;
     private Action _sync;
 
+    private LogarithmicTimeout _logarithmicTimeout;
+    private NumericUpDown _numberLog;
+    private Button _btnLogarithmic;
+    private object _timeoutObj;
+
+    private SmoothAcceleratingTimeout _smoothTimeout;
+    private NumericUpDown _numberSmooth;
+    private Button _btnSmooth;
+    private object _timeoutObjSmooth;
+
     public override void Main ()
     {
         Application.Init ();
@@ -82,6 +92,35 @@ public class Threading : Scenario
 
         var text = new TextField { X = 1, Y = 3, Width = 100, Text = "Type anything after press the button" };
 
+        _btnLogarithmic = new Button ()
+        {
+            X = 50,
+            Y = 4,
+            Text = "Start Log Counter"
+        };
+        _btnLogarithmic.Accepting += StartStopLogTimeout;
+
+        _numberLog = new NumericUpDown ()
+        {
+            X = Pos.Right (_btnLogarithmic),
+            Y = 4,
+        };
+
+        _btnSmooth = new Button ()
+        {
+            X = Pos.Right (_numberLog),
+            Y = 4,
+            Text = "Start Smooth Counter"
+        };
+        _btnSmooth.Accepting += StartStopSmoothTimeout;
+
+        _numberSmooth = new NumericUpDown ()
+        {
+            X = Pos.Right (_btnSmooth),
+            Y = 4,
+        };
+
+
         var btnAction = new Button { X = 80, Y = 10, Text = "Load Data Action" };
         btnAction.Accepting += (s, e) => _action.Invoke ();
         var btnLambda = new Button { X = 80, Y = 12, Text = "Load Data Lambda" };
@@ -107,6 +146,10 @@ public class Threading : Scenario
                  _btnActionCancel,
                  _logJob,
                  text,
+                 _btnLogarithmic,
+                 _numberLog,
+                 _btnSmooth,
+                 _numberSmooth,
                  btnAction,
                  btnLambda,
                  btnHandler,
@@ -129,6 +172,51 @@ public class Threading : Scenario
         Application.Shutdown ();
     }
 
+    private bool LogTimeout ()
+    {
+        _numberLog.Value++;
+        _logarithmicTimeout.AdvanceStage ();
+        return true;
+    }
+    private bool SmoothTimeout ()
+    {
+        _numberSmooth.Value++;
+        _smoothTimeout.AdvanceStage ();
+        return true;
+    }
+
+    private void StartStopLogTimeout (object sender, CommandEventArgs e)
+    {
+        if (_timeoutObj != null)
+        {
+            _btnLogarithmic.Text = "Start Log Counter";
+            Application.TimedEvents.Remove (_timeoutObj);
+            _timeoutObj = null;
+        }
+        else
+        {
+            _btnLogarithmic.Text = "Stop Log Counter";
+            _logarithmicTimeout = new LogarithmicTimeout (TimeSpan.FromMilliseconds (500), LogTimeout);
+            _timeoutObj = Application.TimedEvents.Add (_logarithmicTimeout);
+        }
+    }
+
+    private void StartStopSmoothTimeout (object sender, CommandEventArgs e)
+    {
+        if (_timeoutObjSmooth != null)
+        {
+            _btnSmooth.Text = "Start Smooth Counter";
+            Application.TimedEvents.Remove (_timeoutObjSmooth);
+            _timeoutObjSmooth = null;
+        }
+        else
+        {
+            _btnSmooth.Text = "Stop Smooth Counter";
+            _smoothTimeout = new SmoothAcceleratingTimeout (TimeSpan.FromMilliseconds (500), TimeSpan.FromMilliseconds (50), 0.5, SmoothTimeout);
+            _timeoutObjSmooth = Application.TimedEvents.Add (_smoothTimeout);
+        }
+    }
+
     private async void CallLoadItemsAsync ()
     {
         _cancellationTokenSource = new CancellationTokenSource ();

+ 3 - 3
Examples/UICatalog/Scenarios/ViewportSettings.cs

@@ -124,8 +124,8 @@ public class ViewportSettings : Scenario
         var view = new ViewportSettingsDemoView
         {
             Title = "ViewportSettings Demo View",
-            Width = Dim.Fill (Dim.Func (() => app.IsInitialized ? adornmentsEditor.Frame.Width + 1 : 1)),
-            Height = Dim.Fill (Dim.Func (() => app.IsInitialized ? viewportSettingsEditor.Frame.Height : 1))
+            Width = Dim.Fill (Dim.Func (_ => app.IsInitialized ? adornmentsEditor.Frame.Width + 1 : 1)),
+            Height = Dim.Fill (Dim.Func (_ => app.IsInitialized ? viewportSettingsEditor.Frame.Height : 1))
         };
 
         app.Add (view);
@@ -164,7 +164,7 @@ public class ViewportSettings : Scenario
         {
             X = Pos.Center (),
             Y = Pos.Bottom (textView) + 1,
-            Width = Dim.Auto (DimAutoStyle.Content, maximumContentDim: Dim.Func (() => view.GetContentSize ().Width)),
+            Width = Dim.Auto (DimAutoStyle.Content, maximumContentDim: Dim.Func (_ => view.GetContentSize ().Width)),
             Height = Dim.Auto (DimAutoStyle.Content, maximumContentDim: Dim.Percent (20)),
         };
 

+ 0 - 294
Examples/UICatalog/Scenarios/VkeyPacketSimulator.cs

@@ -1,294 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace UICatalog.Scenarios;
-
-[ScenarioMetadata ("VkeyPacketSimulator", "Simulates the Virtual Key Packet")]
-[ScenarioCategory ("Mouse and Keyboard")]
-public class VkeyPacketSimulator : Scenario
-{
-    private static readonly ManualResetEventSlim _stopOutput = new (false);
-    private readonly List<KeyCode> _keyboardStrokes = new ();
-    private bool _outputStarted;
-    private bool _wasUnknown;
-
-    public override void Main ()
-    {
-        Application.Init ();
-        var win = new Window { Title = GetQuitKeyAndName () };
-
-        var label = new Label { X = Pos.Center (), Text = "Input" };
-        win.Add (label);
-
-        var btnInput = new Button { X = Pos.AnchorEnd (16), Text = "Select Input" };
-        win.Add (btnInput);
-
-        const string ruler = "|123456789";
-
-        var inputHorizontalRuler = new Label
-        {
-            Y = Pos.Bottom (btnInput), Width = Dim.Fill (), SchemeName = "Error"
-        };
-        win.Add (inputHorizontalRuler);
-
-        var inputVerticalRuler = new Label
-        {
-            Y = Pos.Bottom (btnInput),
-
-            Width = 1,
-            Height = Dim.Percent (50),
-            SchemeName = "Error",
-            TextDirection = TextDirection.TopBottom_LeftRight
-        };
-        win.Add (inputVerticalRuler);
-
-        var tvInput = new TextView
-        {
-            Title = "Input",
-            X = 1,
-            Y = Pos.Bottom (inputHorizontalRuler),
-            Width = Dim.Fill (),
-            Height = Dim.Percent (50) - 1
-        };
-        win.Add (tvInput);
-
-        label = new() { X = Pos.Center (), Y = Pos.Bottom (tvInput), Text = "Output" };
-        win.Add (label);
-
-        var btnOutput = new Button { X = Pos.AnchorEnd (17), Y = Pos.Top (label), Text = "Select Output" };
-        win.Add (btnOutput);
-
-        var outputHorizontalRuler = new Label
-        {
-            Y = Pos.Bottom (btnOutput),
-
-            Width = Dim.Fill (),
-            SchemeName = "Error"
-        };
-        win.Add (outputHorizontalRuler);
-
-        var outputVerticalRuler = new Label
-        {
-            Y = Pos.Bottom(btnOutput),
-
-            Width = 1,
-            Height = Dim.Fill (),
-            SchemeName = "Error",
-            TextDirection = TextDirection.TopBottom_LeftRight
-        };
-        win.Add (outputVerticalRuler);
-
-        var tvOutput = new TextView
-        {
-            Title = "Output",
-            X = 1,
-            Y = Pos.Bottom (outputHorizontalRuler),
-            Width = Dim.Fill (),
-            Height = Dim.Fill (),
-            ReadOnly = true
-        };
-
-        // Detect unknown keys and reject them.
-        tvOutput.KeyDown += (s, e) =>
-                            {
-                                //System.Diagnostics.Debug.WriteLine ($"Output - KeyDown: {e.KeyCode}");
-                                if (e.NoAlt.NoCtrl.NoShift == KeyCode.Null)
-                                {
-                                    _wasUnknown = true;
-                                    e.Handled = true;
-
-                                    return;
-                                }
-
-                                //System.Diagnostics.Debug.WriteLine ($"Output - KeyPress - _keyboardStrokes: {_keyboardStrokes.Count}");
-                                if (_outputStarted)
-                                {
-                                    // If the key wasn't handled by the TextView will popup a Dialog with the keys pressed.
-                                    bool? handled = tvOutput.NewKeyDownEvent (e);
-
-                                    if (handled == null || handled == false)
-                                    {
-                                        if (!tvOutput.NewKeyDownEvent (e))
-                                        {
-                                            Application.Invoke (
-                                                                () => MessageBox.Query (
-                                                                                        "Keys",
-                                                                                        $"'{Key.ToString (
-                                                                                                          e.KeyCode,
-                                                                                                          Key.Separator
-                                                                                                         )}' pressed!",
-                                                                                        "Ok"
-                                                                                       )
-                                                               );
-                                        }
-                                    }
-                                }
-
-                                e.Handled = true;
-                                _stopOutput.Set ();
-                            };
-
-        win.Add (tvOutput);
-
-        tvInput.KeyDown += (s, e) =>
-                           {
-                               //System.Diagnostics.Debug.WriteLine ($"Input - KeyDown: {e.KeyCode.Key}");
-                               if (e.KeyCode == Key.Empty)
-                               {
-                                   _wasUnknown = true;
-                                   e.Handled = true;
-                               }
-                               else
-                               {
-                                   _wasUnknown = false;
-                               }
-                           };
-
-        tvInput.KeyDownNotHandled += (s, e) =>
-                                       {
-                                           Key ev = e;
-
-                                           //System.Diagnostics.Debug.WriteLine ($"Input - KeyPress: {ev}");
-                                           //System.Diagnostics.Debug.WriteLine ($"Input - KeyPress - _keyboardStrokes: {_keyboardStrokes.Count}");
-
-                                           if (!e.IsValid)
-                                           {
-                                               _wasUnknown = true;
-                                               e.Handled = true;
-
-                                               return;
-                                           }
-
-                                           _keyboardStrokes.Add (e.KeyCode);
-                                       };
-
-        tvInput.KeyUp += (s, e) =>
-                         {
-                             //System.Diagnostics.Debug.WriteLine ($"Input - KeyUp: {e.Key}");
-                             e.Handled = true;
-
-                             if (!_wasUnknown && _keyboardStrokes.Count > 0)
-                             {
-                                 _outputStarted = true;
-                                 tvOutput.ReadOnly = false;
-                                 tvOutput.SetFocus ();
-                                 tvOutput.SetNeedsDraw ();
-
-                                 Task.Run (
-                                           () =>
-                                           {
-                                               while (_outputStarted)
-                                               {
-                                                   try
-                                                   {
-                                                       while (_keyboardStrokes.Count > 0)
-                                                       {
-                                                           if (_keyboardStrokes [0] == KeyCode.Null)
-                                                           {
-                                                               continue;
-                                                           }
-
-                                                           ConsoleKeyInfo consoleKeyInfo =
-                                                               ConsoleKeyMapping.GetConsoleKeyInfoFromKeyCode (_keyboardStrokes [0]);
-
-                                                           char keyChar =
-                                                               ConsoleKeyMapping.EncodeKeyCharForVKPacket (consoleKeyInfo);
-
-                                                           Application.Driver?.SendKeys (
-                                                                                        keyChar,
-                                                                                        ConsoleKey.Packet,
-                                                                                        consoleKeyInfo.Modifiers.HasFlag (ConsoleModifiers.Shift),
-                                                                                        consoleKeyInfo.Modifiers.HasFlag (ConsoleModifiers.Alt),
-                                                                                        consoleKeyInfo.Modifiers
-                                                                                                      .HasFlag (ConsoleModifiers.Control)
-                                                                                       );
-
-                                                           _stopOutput.Wait ();
-                                                           _stopOutput.Reset ();
-                                                           _keyboardStrokes.RemoveAt (0);
-
-                                                           Application.Invoke (
-                                                                               () =>
-                                                                               {
-                                                                                   tvOutput.ReadOnly = true;
-                                                                                   tvInput.SetFocus ();
-                                                                               }
-                                                                              );
-                                                       }
-
-                                                       _outputStarted = false;
-                                                   }
-                                                   catch (Exception)
-                                                   {
-                                                       Application.Invoke (
-                                                                           () =>
-                                                                           {
-                                                                               MessageBox.ErrorQuery (
-                                                                                                      "Error",
-                                                                                                      "Couldn't send the keystrokes!",
-                                                                                                      "Ok"
-                                                                                                     );
-                                                                               Application.RequestStop ();
-                                                                           }
-                                                                          );
-                                                   }
-                                               }
-
-                                               //System.Diagnostics.Debug.WriteLine ($"_outputStarted: {_outputStarted}");
-                                           }
-                                          );
-                             }
-                         };
-
-        btnInput.Accepting += (s, e) =>
-                           {
-                               if (!tvInput.HasFocus && _keyboardStrokes.Count == 0)
-                               {
-                                   tvInput.SetFocus ();
-                               }
-                           };
-
-        btnOutput.Accepting += (s, e) =>
-                            {
-                                if (!tvOutput.HasFocus && _keyboardStrokes.Count == 0)
-                                {
-                                    tvOutput.SetFocus ();
-                                }
-                            };
-
-        tvInput.SetFocus ();
-
-        void Win_LayoutComplete (object sender, LayoutEventArgs obj)
-        {
-            if (inputHorizontalRuler.Viewport.Width == 0 || inputVerticalRuler.Viewport.Height == 0)
-            {
-                return;
-            }
-            inputHorizontalRuler.Text = outputHorizontalRuler.Text =
-                                            ruler.Repeat (
-                                                          (int)Math.Ceiling (
-                                                                             inputHorizontalRuler.Viewport.Width
-                                                                             / (double)ruler.Length
-                                                                            )
-                                                         ) [
-                                                            ..inputHorizontalRuler.Viewport.Width];
-            inputVerticalRuler.Height = tvInput.Frame.Height + 1;
-
-            inputVerticalRuler.Text =
-                ruler.Repeat ((int)Math.Ceiling (inputVerticalRuler.Viewport.Height / (double)ruler.Length)) [
-                     ..inputVerticalRuler.Viewport.Height];
-
-            outputVerticalRuler.Text =
-                ruler.Repeat ((int)Math.Ceiling (outputVerticalRuler.Viewport.Height / (double)ruler.Length)) [
-                     ..outputVerticalRuler.Viewport.Height];
-        }
-
-        win.SubViewsLaidOut += Win_LayoutComplete;
-
-        Application.Run (win);
-        win.Dispose ();
-        Application.Shutdown ();
-    }
-}

+ 5 - 5
Examples/UICatalog/Scenarios/Wizards.cs

@@ -282,11 +282,11 @@ public class Wizards : Scenario
 
                                            someText.Height = Dim.Fill (
                                                                        Dim.Func (
-                                                                                 () => someText.SuperView is { IsInitialized: true }
-                                                                                           ? someText.SuperView.SubViews
-                                                                                                     .First (view => view.Y.Has<PosAnchorEnd> (out _))
-                                                                                                     .Frame.Height
-                                                                                           : 1));
+                                                                                 v => someText.SuperView is { IsInitialized: true }
+                                                                                          ? someText.SuperView.SubViews
+                                                                                                    .First (view => view.Y.Has<PosAnchorEnd> (out _))
+                                                                                                    .Frame.Height
+                                                                                          : 1));
                                            var help = "This is helpful.";
                                            fourthStep.Add (someText);
 

+ 0 - 5
Examples/UICatalog/UICatalog.cs

@@ -428,11 +428,6 @@ public class UICatalog
 
         Application.Init (driverName: _forceDriver);
 
-        if (benchmark)
-        {
-            Application.Screen = new (0, 0, 120, 40);
-        }
-
         scenario.Main ();
 
         BenchmarkResults? results = null;

+ 36 - 40
Examples/UICatalog/UICatalogTop.cs

@@ -127,7 +127,7 @@ public class UICatalogTop : Toplevel
                                               new MenuItemv2 (
                                                               "_Documentation",
                                                               "",
-                                                              () => OpenUrl ("https://gui-cs.github.io/Terminal.GuiV2Docs"),
+                                                              () => OpenUrl ("https://gui-cs.github.io/Terminal.Gui"),
                                                               Key.F1
                                                              ),
                                               new MenuItemv2 (
@@ -166,13 +166,23 @@ public class UICatalogTop : Toplevel
                 CheckedState = Application.Force16Colors ? CheckState.Checked : CheckState.UnChecked
             };
 
+            _force16ColorsMenuItemCb.CheckedStateChanging += (sender, args) =>
+                                                             {
+                                                                 if (Application.Force16Colors
+                                                                     && args.Result == CheckState.UnChecked
+                                                                     && !Application.Driver!.SupportsTrueColor)
+                                                                 {
+                                                                     args.Handled = true;
+                                                                 }
+                                                             };
+
             _force16ColorsMenuItemCb.CheckedStateChanged += (sender, args) =>
-            {
-                Application.Force16Colors = args.Value == CheckState.Checked;
+                                                            {
+                                                                Application.Force16Colors = args.Value == CheckState.Checked;
 
-                _force16ColorsShortcutCb!.CheckedState = args.Value;
-                Application.LayoutAndDraw ();
-            };
+                                                                _force16ColorsShortcutCb!.CheckedState = args.Value;
+                                                                Application.LayoutAndDraw ();
+                                                            };
 
             menuItems.Add (
                            new MenuItemv2
@@ -404,20 +414,7 @@ public class UICatalogTop : Toplevel
             X = Pos.Right (_categoryList!) - 1,
             Y = Pos.Bottom (_menuBar!),
             Width = Dim.Fill (),
-            Height = Dim.Fill (
-                               Dim.Func (
-                                         () =>
-                                         {
-                                             if (_statusBar!.NeedsLayout)
-                                             {
-                                                 throw new LayoutException ("DimFunc.Fn aborted because dependent View needs layout.");
-
-                                                 //_statusBar.Layout ();
-                                             }
-
-                                             return _statusBar.Frame.Height;
-                                         })),
-
+            Height = Dim.Fill (Dim.Func (v => v!.Frame.Height, _statusBar)),
             //AllowsMarking = false,
             CanFocus = true,
             Title = "_Scenarios",
@@ -515,19 +512,7 @@ public class UICatalogTop : Toplevel
             X = 0,
             Y = Pos.Bottom (_menuBar!),
             Width = Dim.Auto (),
-            Height = Dim.Fill (
-                               Dim.Func (
-                                         () =>
-                                         {
-                                             if (_statusBar!.NeedsLayout)
-                                             {
-                                                 throw new LayoutException ("DimFunc.Fn aborted because dependent View needs layout.");
-
-                                                 //_statusBar.Layout ();
-                                             }
-
-                                             return _statusBar.Frame.Height;
-                                         })),
+            Height = Dim.Fill (Dim.Func (v => v!.Frame.Height, _statusBar)),
             AllowsMarking = false,
             CanFocus = true,
             Title = "_Categories",
@@ -595,8 +580,8 @@ public class UICatalogTop : Toplevel
         // ReSharper disable All
         statusBar.Height = Dim.Auto (
                                      DimAutoStyle.Auto,
-                                     minimumContentDim: Dim.Func (() => statusBar.Visible ? 1 : 0),
-                                     maximumContentDim: Dim.Func (() => statusBar.Visible ? 1 : 0));
+                                     minimumContentDim: Dim.Func (_ => statusBar.Visible ? 1 : 0),
+                                     maximumContentDim: Dim.Func (_ => statusBar.Visible ? 1 : 0));
         // ReSharper restore All
 
         _shQuit = new ()
@@ -633,11 +618,22 @@ public class UICatalogTop : Toplevel
         };
 
         _force16ColorsShortcutCb.CheckedStateChanging += (sender, args) =>
-        {
-            Application.Force16Colors = args.Result == CheckState.Checked;
-            _force16ColorsMenuItemCb!.CheckedState = args.Result;
-            Application.LayoutAndDraw ();
-        };
+                                                         {
+                                                             if (Application.Force16Colors
+                                                                 && args.Result == CheckState.UnChecked
+                                                                 && !Application.Driver!.SupportsTrueColor)
+                                                             {
+                                                                 // If the driver does not support TrueColor, we cannot disable 16 colors
+                                                                 args.Handled = true;
+                                                             }
+                                                         };
+
+        _force16ColorsShortcutCb.CheckedStateChanged += (sender, args) =>
+                                                         {
+                                                             Application.Force16Colors = args.Value == CheckState.Checked;
+                                                             _force16ColorsMenuItemCb!.CheckedState = args.Value;
+                                                             Application.LayoutAndDraw ();
+                                                         };
 
         statusBar.Add (
                        _shQuit,

+ 4 - 4
README.md

@@ -5,7 +5,7 @@
 [![License](https://img.shields.io/github/license/gui-cs/gui.cs.svg)](LICENSE)
 ![Bugs](https://img.shields.io/github/issues/gui-cs/gui.cs/bug)
 
-# Terminal.Gui
+# Terminal.Gui v2
 
 The premier toolkit for building rich console apps for Windows, the Mac, and Linux/Unix.
 
@@ -50,9 +50,9 @@ The full developer documentation for Terminal.Gui is available at [gui-cs.github
 
 ## Getting Started
 
-- [Getting Started](https://gui-cs.github.io/Terminal.Gui/docs/getting-started.md) - Quick start guide to create your first Terminal.Gui application
-- [Migrating from v1 to v2](https://gui-cs.github.io/Terminal.Gui/docs/migratingfromv1.md) - Complete guide for upgrading existing applications
-- [What's New in v2](https://gui-cs.github.io/Terminal.Gui/docs/newinv2.md) - Overview of new features and improvements
+- [Getting Started](https://gui-cs.github.io/Terminal.Gui/docs/getting-started) - Quick start guide to create your first Terminal.Gui application
+- [Migrating from v1 to v2](https://gui-cs.github.io/Terminal.Gui/docs/migratingfromv1) - Complete guide for upgrading existing applications
+- [What's New in v2](https://gui-cs.github.io/Terminal.Gui/docs/newinv2) - Overview of new features and improvements
 
 ## API Reference
 

+ 61 - 0
Terminal.Gui.Analyzers.Tests/HandledEventArgsAnalyzerTests.cs

@@ -0,0 +1,61 @@
+using Terminal.Gui.Input;
+using Terminal.Gui.Views;
+
+namespace Terminal.Gui.Analyzers.Tests;
+
+public class HandledEventArgsAnalyzerTests
+{
+    [Theory]
+    [InlineData("e")]
+    [InlineData ("args")]
+    public async Task Should_ReportDiagnostic_When_EHandledNotSet_Lambda (string paramName)
+    {
+        var originalCode = $$"""
+                            using Terminal.Gui.Views;
+
+                            class TestClass
+                            {
+                                void Setup()
+                                {
+                                    var b = new Button();
+                                    b.Accepting += (s, {{paramName}}) =>
+                                    {
+                                        // Forgot {{paramName}}.Handled = true;
+                                    };
+                                }
+                            }
+                            """;
+        await new ProjectBuilder ()
+              .WithSourceCode (originalCode)
+              .WithAnalyzer (new HandledEventArgsAnalyzer ())
+              .ValidateAsync ();
+    }
+
+    [Theory]
+    [InlineData ("e")]
+    [InlineData ("args")]
+    public async Task Should_ReportDiagnostic_When_EHandledNotSet_Method (string paramName)
+    {
+        var originalCode = $$"""
+                            using Terminal.Gui.Views;
+                            using Terminal.Gui.Input;
+
+                            class TestClass
+                            {
+                                void Setup()
+                                {
+                                    var b = new Button();
+                                    b.Accepting += BOnAccepting;
+                                }
+                                private void BOnAccepting (object? sender, CommandEventArgs {{paramName}})
+                                {
+
+                                }
+                            }
+                            """;
+        await new ProjectBuilder ()
+              .WithSourceCode (originalCode)
+              .WithAnalyzer (new HandledEventArgsAnalyzer ())
+              .ValidateAsync ();
+    }
+}

+ 165 - 0
Terminal.Gui.Analyzers.Tests/ProjectBuilder.cs

@@ -0,0 +1,165 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.Text;
+using Microsoft.CodeAnalysis.CodeFixes;
+using Microsoft.CodeAnalysis.Diagnostics;
+using System.Collections.Immutable;
+using System.Collections.ObjectModel;
+using System.ComponentModel;
+using System.Drawing;
+using Microsoft.CodeAnalysis.CodeActions;
+using Terminal.Gui.ViewBase;
+using Terminal.Gui.Views;
+using Document = Microsoft.CodeAnalysis.Document;
+using Formatter = Microsoft.CodeAnalysis.Formatting.Formatter;
+using System.Reflection;
+using JetBrains.Annotations;
+
+public sealed class ProjectBuilder
+{
+    private string _sourceCode;
+    private string _expectedFixedCode;
+    private DiagnosticAnalyzer _analyzer;
+    private CodeFixProvider _codeFix;
+
+    public ProjectBuilder WithSourceCode (string source)
+    {
+        _sourceCode = source;
+        return this;
+    }
+
+    public ProjectBuilder ShouldFixCodeWith (string expected)
+    {
+        _expectedFixedCode = expected;
+        return this;
+    }
+
+    public ProjectBuilder WithAnalyzer (DiagnosticAnalyzer analyzer)
+    {
+        _analyzer = analyzer;
+        return this;
+    }
+
+    public ProjectBuilder WithCodeFix (CodeFixProvider codeFix)
+    {
+        _codeFix = codeFix;
+        return this;
+    }
+
+    public async Task ValidateAsync ()
+    {
+        if (_sourceCode == null)
+        {
+            throw new InvalidOperationException ("Source code not set.");
+        }
+
+        if (_analyzer == null)
+        {
+            throw new InvalidOperationException ("Analyzer not set.");
+        }
+
+        // Parse original document
+        var document = CreateDocument (_sourceCode);
+        var compilation = await document.Project.GetCompilationAsync ();
+
+        var diagnostics = compilation.GetDiagnostics ();
+        var errors = diagnostics.Where (d => d.Severity == DiagnosticSeverity.Error);
+
+        if (errors.Any ())
+        {
+            var errorMessages = string.Join (Environment.NewLine, errors.Select (e => e.ToString ()));
+            throw new Exception ("Compilation failed with errors:" + Environment.NewLine + errorMessages);
+        }
+
+        // Run analyzer
+        var analyzerDiagnostics = await GetAnalyzerDiagnosticsAsync (compilation, _analyzer);
+
+        Assert.NotEmpty (analyzerDiagnostics);
+
+        if (_expectedFixedCode != null)
+        {
+            if (_codeFix == null)
+            {
+                throw new InvalidOperationException ("Expected code fix but none was set.");
+            }
+
+            var fixedDocument = await ApplyCodeFixAsync (document, analyzerDiagnostics.First (), _codeFix);
+
+            var formattedDocument = await Formatter.FormatAsync (fixedDocument);
+            var fixedSource = (await formattedDocument.GetTextAsync ()).ToString ();
+
+            Assert.Equal (_expectedFixedCode, fixedSource);
+        }
+    }
+
+    private static Document CreateDocument (string source)
+    {
+        var dd = typeof (Enumerable).GetTypeInfo ().Assembly.Location;
+        var coreDir = Directory.GetParent (dd) ?? throw new Exception ($"Could not find parent directory of dotnet sdk.  Sdk directory was {dd}");
+
+        var workspace = new AdhocWorkspace ();
+        var projectId = ProjectId.CreateNewId ();
+        var documentId = DocumentId.CreateNewId (projectId);
+
+        var references = new List<MetadataReference> ()
+        {
+            MetadataReference.CreateFromFile(typeof(Button).Assembly.Location),
+            MetadataReference.CreateFromFile(typeof(View).Assembly.Location),
+            MetadataReference.CreateFromFile(typeof(System.IO.FileSystemInfo).Assembly.Location),
+            MetadataReference.CreateFromFile(typeof(System.Linq.Enumerable).Assembly.Location),
+            MetadataReference.CreateFromFile(typeof(object).Assembly.Location),
+            MetadataReference.CreateFromFile(typeof(MarshalByValueComponent).Assembly.Location),
+            MetadataReference.CreateFromFile(typeof(ObservableCollection<string>).Assembly.Location),
+
+            // New assemblies required by Terminal.Gui version 2
+            MetadataReference.CreateFromFile(typeof(Size).Assembly.Location),
+            MetadataReference.CreateFromFile(typeof(CanBeNullAttribute).Assembly.Location),
+
+
+            MetadataReference.CreateFromFile(Path.Combine(coreDir.FullName, "mscorlib.dll")),
+            MetadataReference.CreateFromFile(Path.Combine(coreDir.FullName, "System.Runtime.dll")),
+            MetadataReference.CreateFromFile(Path.Combine(coreDir.FullName, "System.Collections.dll")),
+            MetadataReference.CreateFromFile(Path.Combine(coreDir.FullName, "System.Data.Common.dll")),
+            // Add more as necessary
+        };
+
+
+        var projectInfo = ProjectInfo.Create (
+                                              projectId,
+                                              VersionStamp.Create (),
+                                              "TestProject",
+                                              "TestAssembly",
+                                              LanguageNames.CSharp,
+                                              compilationOptions: new CSharpCompilationOptions (OutputKind.DynamicallyLinkedLibrary),
+                                              metadataReferences: references);
+
+        var solution = workspace.CurrentSolution
+                                .AddProject (projectInfo)
+                                .AddDocument (documentId, "Test.cs", SourceText.From (source));
+
+        return solution.GetDocument (documentId)!;
+    }
+
+    private static async Task<ImmutableArray<Diagnostic>> GetAnalyzerDiagnosticsAsync (Compilation compilation, DiagnosticAnalyzer analyzer)
+    {
+        var compilationWithAnalyzers = compilation.WithAnalyzers (ImmutableArray.Create (analyzer));
+        return await compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync ();
+    }
+
+    private static async Task<Document> ApplyCodeFixAsync (Document document, Diagnostic diagnostic, CodeFixProvider codeFix)
+    {
+        CodeAction _codeAction = null;
+        var context = new CodeFixContext ((TextDocument)document, diagnostic, (action, _) => _codeAction = action, CancellationToken.None);
+
+        await codeFix.RegisterCodeFixesAsync (context);
+
+        if (_codeAction == null)
+        {
+            throw new InvalidOperationException ("Code fix did not register a fix.");
+        }
+
+        var operations = await _codeAction.GetOperationsAsync (CancellationToken.None);
+        var solution = operations.OfType<ApplyChangesOperation> ().First ().ChangedSolution;
+        return solution.GetDocument (document.Id);
+    }
+}

+ 35 - 0
Terminal.Gui.Analyzers.Tests/Terminal.Gui.Analyzers.Tests.csproj

@@ -0,0 +1,35 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+	<PropertyGroup>
+		<Nullable>enable</Nullable>
+		<IsPackable>false</IsPackable>
+		<IsTestProject>true</IsTestProject>
+		<DefineConstants>$(DefineConstants);JETBRAINS_ANNOTATIONS;CONTRACTS_FULL</DefineConstants>
+		<DebugType>portable</DebugType>
+		<ImplicitUsings>enable</ImplicitUsings>
+		<NoLogo>true</NoLogo>
+		<SuppressNETCoreSdkPreviewMessage>true</SuppressNETCoreSdkPreviewMessage>
+		<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
+	</PropertyGroup>
+	<ItemGroup>
+		<PackageReference Include="coverlet.collector" />
+		<PackageReference Include="Microsoft.CodeAnalysis.CSharp" />
+		<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" />
+		<PackageReference Include="Microsoft.CodeAnalysis.VisualBasic.Workspaces" />
+		<PackageReference Include="Microsoft.NET.Test.Sdk" />
+		<PackageReference Include="xunit" />
+		<PackageReference Include="xunit.runner.visualstudio" />
+  </ItemGroup>
+	<ItemGroup>
+	  <ProjectReference Include="..\Terminal.Gui.Analyzers\Terminal.Gui.Analyzers.csproj" />
+	  <ProjectReference Include="..\Terminal.Gui\Terminal.Gui.csproj" />
+	</ItemGroup>
+	<ItemGroup>
+		<Using Include="Xunit" />
+	</ItemGroup>
+	<ItemGroup>
+		<None Update="xunit.runner.json">
+			<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+		</None>
+	</ItemGroup>
+</Project>

+ 6 - 0
Terminal.Gui.Analyzers/AnalyzerReleases.Shipped.md

@@ -0,0 +1,6 @@
+## Release 1.0.0
+
+### New Rules
+
+Rule ID | Category | Severity | Notes
+--------|----------|----------|--------------------

+ 5 - 0
Terminal.Gui.Analyzers/AnalyzerReleases.Unshipped.md

@@ -0,0 +1,5 @@
+### New Rules
+
+Rule ID | Category | Severity | Notes
+--------|----------|----------|--------------------
+TGUI001  | Reliability |  Warning | HandledEventArgsAnalyzer, [Documentation](./TGUI001.md)

+ 67 - 0
Terminal.Gui.Analyzers/DiagnosticCategory.cs

@@ -0,0 +1,67 @@
+namespace Terminal.Gui.Analyzers;
+
+/// <summary>
+/// Categories commonly used for diagnostic analyzers, inspired by FxCop and .NET analyzers conventions.
+/// </summary>
+internal enum DiagnosticCategory
+{
+    /// <summary>
+    /// Issues related to naming conventions and identifiers.
+    /// </summary>
+    Naming,
+
+    /// <summary>
+    /// API design, class structure, inheritance, etc.
+    /// </summary>
+    Design,
+
+    /// <summary>
+    /// How code uses APIs or language features incorrectly or suboptimally.
+    /// </summary>
+    Usage,
+
+    /// <summary>
+    /// Patterns that cause poor runtime performance.
+    /// </summary>
+    Performance,
+
+    /// <summary>
+    /// Vulnerabilities or insecure coding patterns.
+    /// </summary>
+    Security,
+
+    /// <summary>
+    /// Code patterns that can cause bugs, crashes, or unpredictable behavior.
+    /// </summary>
+    Reliability,
+
+    /// <summary>
+    /// Code readability, complexity, or future-proofing concerns.
+    /// </summary>
+    Maintainability,
+
+    /// <summary>
+    /// Code patterns that may not work on all platforms or frameworks.
+    /// </summary>
+    Portability,
+
+    /// <summary>
+    /// Issues with culture, localization, or globalization support.
+    /// </summary>
+    Globalization,
+
+    /// <summary>
+    /// Problems when working with COM, P/Invoke, or other interop scenarios.
+    /// </summary>
+    Interoperability,
+
+    /// <summary>
+    /// Issues with missing or incorrect XML doc comments.
+    /// </summary>
+    Documentation,
+
+    /// <summary>
+    /// Purely stylistic issues not affecting semantics (e.g., whitespace, order).
+    /// </summary>
+    Style
+}

+ 269 - 0
Terminal.Gui.Analyzers/HandledEventArgsAnalyzer.cs

@@ -0,0 +1,269 @@
+using System.Collections.Immutable;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Diagnostics;
+
+namespace Terminal.Gui.Analyzers;
+
+[DiagnosticAnalyzer (LanguageNames.CSharp)]
+public class HandledEventArgsAnalyzer : DiagnosticAnalyzer
+{
+    public const string DiagnosticId = "TGUI001";
+    private static readonly LocalizableString Title = "Accepting event handler should set Handled = true";
+    private static readonly LocalizableString MessageFormat = "Accepting event handler does not set Handled = true";
+    private static readonly LocalizableString Description = "Handlers for Accepting should mark the CommandEventArgs as handled by setting Handled = true otherwise subsequent Accepting event handlers may also fire (e.g. default buttons).";
+    private static readonly string Url = "https://github.com/tznind/gui.cs/blob/analyzer-no-handled/Terminal.Gui.Analyzers/TGUI001.md";
+    private const string Category = nameof(DiagnosticCategory.Reliability);
+
+    private static readonly DiagnosticDescriptor _rule = new (
+                                                              DiagnosticId,
+                                                              Title,
+                                                              MessageFormat,
+                                                              Category,
+                                                              DiagnosticSeverity.Warning,
+                                                              true,
+                                                              Description,
+                                                              helpLinkUri: Url);
+
+    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => [_rule];
+
+    public override void Initialize (AnalysisContext context)
+    {
+        context.EnableConcurrentExecution ();
+
+        // Only analyze non-generated code
+        context.ConfigureGeneratedCodeAnalysis (GeneratedCodeAnalysisFlags.None);
+
+        // Register for b.Accepting += (s,e)=>{...};
+        context.RegisterSyntaxNodeAction (
+                                          AnalyzeLambdaOrAnonymous,
+                                          SyntaxKind.ParenthesizedLambdaExpression,
+                                          SyntaxKind.SimpleLambdaExpression,
+                                          SyntaxKind.AnonymousMethodExpression);
+
+        // Register for b.Accepting += MyMethod;
+        context.RegisterSyntaxNodeAction (
+                                          AnalyzeEventSubscriptionWithMethodGroup,
+                                          SyntaxKind.AddAssignmentExpression);
+    }
+
+    private static void AnalyzeLambdaOrAnonymous (SyntaxNodeAnalysisContext context)
+    {
+        var lambda = (AnonymousFunctionExpressionSyntax)context.Node;
+
+        // Check if this lambda is assigned to the Accepting event
+        if (!IsAssignedToAcceptingEvent (lambda.Parent, context))
+        {
+            return;
+        }
+
+        // Look for any parameter of type CommandEventArgs (regardless of name)
+        IParameterSymbol? eParam = GetCommandEventArgsParameter (lambda, context.SemanticModel);
+
+        if (eParam == null)
+        {
+            return;
+        }
+
+        // Analyze lambda body for e.Handled = true assignment
+        if (lambda.Body is BlockSyntax block)
+        {
+            bool setsHandled = block.Statements
+                                    .SelectMany (s => s.DescendantNodes ().OfType<AssignmentExpressionSyntax> ())
+                                    .Any (a => IsHandledAssignment (a, eParam, context));
+
+            if (!setsHandled)
+            {
+                var diag = Diagnostic.Create (_rule, lambda.GetLocation ());
+                context.ReportDiagnostic (diag);
+            }
+        }
+        else if (lambda.Body is ExpressionSyntax)
+        {
+            // Expression-bodied lambdas unlikely for event handlers — skip
+        }
+    }
+
+    /// <summary>
+    ///     Finds the first parameter of type CommandEventArgs in any parameter list (method or lambda).
+    /// </summary>
+    /// <param name="paramOwner"></param>
+    /// <param name="semanticModel"></param>
+    /// <returns></returns>
+    private static IParameterSymbol? GetCommandEventArgsParameter (SyntaxNode paramOwner, SemanticModel semanticModel)
+    {
+        SeparatedSyntaxList<ParameterSyntax>? parameters = paramOwner switch
+                                                           {
+                                                               AnonymousFunctionExpressionSyntax lambda => GetParameters (lambda),
+                                                               MethodDeclarationSyntax method => method.ParameterList.Parameters,
+                                                               _ => null
+                                                           };
+
+        if (parameters == null || parameters.Value.Count == 0)
+        {
+            return null;
+        }
+
+        foreach (ParameterSyntax param in parameters.Value)
+        {
+            IParameterSymbol? symbol = semanticModel.GetDeclaredSymbol (param);
+
+            if (symbol != null && IsCommandEventArgsType (symbol.Type))
+            {
+                return symbol;
+            }
+        }
+
+        return null;
+    }
+
+    private static bool IsAssignedToAcceptingEvent (SyntaxNode? node, SyntaxNodeAnalysisContext context)
+    {
+        if (node is AssignmentExpressionSyntax assignment && IsAcceptingEvent (assignment.Left, context))
+        {
+            return true;
+        }
+
+        if (node?.Parent is AssignmentExpressionSyntax parentAssignment && IsAcceptingEvent (parentAssignment.Left, context))
+        {
+            return true;
+        }
+
+        return false;
+    }
+
+    private static bool IsCommandEventArgsType (ITypeSymbol? type) { return type != null && type.Name == "CommandEventArgs"; }
+
+    private static void AnalyzeEventSubscriptionWithMethodGroup (SyntaxNodeAnalysisContext context)
+    {
+        var assignment = (AssignmentExpressionSyntax)context.Node;
+
+        // Check event name: b.Accepting += ...
+        if (!IsAcceptingEvent (assignment.Left, context))
+        {
+            return;
+        }
+
+        // Right side: should be method group (IdentifierNameSyntax)
+        if (assignment.Right is IdentifierNameSyntax methodGroup)
+        {
+            // Resolve symbol of method group
+            SymbolInfo symbolInfo = context.SemanticModel.GetSymbolInfo (methodGroup);
+
+            if (symbolInfo.Symbol is IMethodSymbol methodSymbol)
+            {
+                // Find method declaration in syntax tree
+                ImmutableArray<SyntaxReference> declRefs = methodSymbol.DeclaringSyntaxReferences;
+
+                foreach (SyntaxReference declRef in declRefs)
+                {
+                    var methodDecl = declRef.GetSyntax () as MethodDeclarationSyntax;
+
+                    if (methodDecl != null)
+                    {
+                        AnalyzeHandlerMethodBody (context, methodDecl, methodSymbol);
+                    }
+                }
+            }
+        }
+    }
+
+    private static void AnalyzeHandlerMethodBody (SyntaxNodeAnalysisContext context, MethodDeclarationSyntax methodDecl, IMethodSymbol methodSymbol)
+    {
+        // Look for any parameter of type CommandEventArgs
+        IParameterSymbol? eParam = GetCommandEventArgsParameter (methodDecl, context.SemanticModel);
+
+        if (eParam == null)
+        {
+            return;
+        }
+
+        // Analyze method body
+        if (methodDecl.Body != null)
+        {
+            bool setsHandled = methodDecl.Body.Statements
+                                         .SelectMany (s => s.DescendantNodes ().OfType<AssignmentExpressionSyntax> ())
+                                         .Any (a => IsHandledAssignment (a, eParam, context));
+
+            if (!setsHandled)
+            {
+                var diag = Diagnostic.Create (_rule, methodDecl.Identifier.GetLocation ());
+                context.ReportDiagnostic (diag);
+            }
+        }
+    }
+
+    private static SeparatedSyntaxList<ParameterSyntax> GetParameters (AnonymousFunctionExpressionSyntax lambda)
+    {
+        switch (lambda)
+        {
+            case ParenthesizedLambdaExpressionSyntax p:
+                return p.ParameterList.Parameters;
+            case SimpleLambdaExpressionSyntax s:
+                // Simple lambda has a single parameter, wrap it in a list
+                return SyntaxFactory.SeparatedList (new [] { s.Parameter });
+            case AnonymousMethodExpressionSyntax a:
+                return a.ParameterList?.Parameters ?? default (SeparatedSyntaxList<ParameterSyntax>);
+            default:
+                return default (SeparatedSyntaxList<ParameterSyntax>);
+        }
+    }
+
+    private static bool IsAcceptingEvent (ExpressionSyntax expr, SyntaxNodeAnalysisContext context)
+    {
+        // Check if expr is b.Accepting or similar
+
+        // Get symbol info
+        SymbolInfo symbolInfo = context.SemanticModel.GetSymbolInfo (expr);
+        ISymbol? symbol = symbolInfo.Symbol;
+
+        if (symbol == null)
+        {
+            return false;
+        }
+
+        // Accepting event symbol should be an event named "Accepting"
+        if (symbol.Kind == SymbolKind.Event && symbol.Name == "Accepting")
+        {
+            return true;
+        }
+
+        return false;
+    }
+
+    private static bool IsHandledAssignment (AssignmentExpressionSyntax assignment, IParameterSymbol eParamSymbol, SyntaxNodeAnalysisContext context)
+    {
+        // Check if left side is "e.Handled" and right side is "true"
+        // Left side should be MemberAccessExpression: e.Handled
+
+        if (assignment.Left is MemberAccessExpressionSyntax memberAccess)
+        {
+            // Check that member access expression is "e.Handled"
+            ISymbol? exprSymbol = context.SemanticModel.GetSymbolInfo (memberAccess.Expression).Symbol;
+
+            if (exprSymbol == null)
+            {
+                return false;
+            }
+
+            if (!SymbolEqualityComparer.Default.Equals (exprSymbol, eParamSymbol))
+            {
+                return false;
+            }
+
+            if (memberAccess.Name.Identifier.Text != "Handled")
+            {
+                return false;
+            }
+
+            // Check right side is true literal
+            if (assignment.Right is LiteralExpressionSyntax literal && literal.IsKind (SyntaxKind.TrueLiteralExpression))
+            {
+                return true;
+            }
+        }
+
+        return false;
+    }
+}

+ 34 - 0
Terminal.Gui.Analyzers/TGUI001.md

@@ -0,0 +1,34 @@
+# TGUI001: Describe what your rule checks
+
+**Category:** Reliability  
+**Severity:** Warning  
+**Enabled by default:** Yes
+
+## Cause
+
+When registering an event handler for `Accepting`, you should set Handled to true, this prevents other subsequent Views from responding to the same input event.
+
+## Reason for rule
+
+If you do not do this then you may see unpredictable behaviour such as clicking a Button resulting in another `IsDefault` button in the View also firing.
+
+See:
+
+- https://github.com/gui-cs/Terminal.Gui/issues/3913
+- https://github.com/gui-cs/Terminal.Gui/issues/4170
+
+## How to fix violations
+
+Set Handled to `true` in your event handler
+
+### Examples
+
+```diff
+var b = new Button();
+b.Accepting += (s, e) =>
+{
+    // Do something
+
++    e.Handled = true;
+};
+```

+ 18 - 0
Terminal.Gui.Analyzers/Terminal.Gui.Analyzers.csproj

@@ -0,0 +1,18 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>netstandard2.0</TargetFramework>
+    <IncludeBuildOutput>false</IncludeBuildOutput>
+    <EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
+    <!-- Analyzer only, no executable -->
+    <Nullable>enable</Nullable>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" PrivateAssets="all" />
+    <PackageReference Include="Microsoft.CodeAnalysis.CSharp" PrivateAssets="all" />
+
+  </ItemGroup>
+
+
+</Project>

+ 8 - 17
Terminal.Gui/App/Application.Initialization.cs

@@ -82,16 +82,6 @@ public static partial class Application // Initialization (Init/Shutdown)
         if (driver is { })
         {
             Driver = driver;
-
-            if (driver is FakeDriver)
-            {
-                //// We're running unit tests. Disable loading config files other than default
-                //if (Locations == ConfigLocations.All)
-                //{
-                //    Locations = ConfigLocations.Default;
-                //    ResetAllSettings ();
-                //}
-            }
         }
 
         // Ignore Configuration for ForceDriver if driverName is specified
@@ -212,21 +202,22 @@ public static partial class Application // Initialization (Init/Shutdown)
         // use reflection to get the list of drivers
         List<Type?> driverTypes = new ();
 
-        foreach (Assembly asm in AppDomain.CurrentDomain.GetAssemblies ())
+        // Only inspect the IConsoleDriver assembly
+        var asm = typeof (IConsoleDriver).Assembly;
+
+        foreach (Type? type in asm.GetTypes ())
         {
-            foreach (Type? type in asm.GetTypes ())
+            if (typeof (IConsoleDriver).IsAssignableFrom (type) &&
+                type is { IsAbstract: false, IsClass: true })
             {
-                if (typeof (IConsoleDriver).IsAssignableFrom (type) && !type.IsAbstract && type.IsClass)
-                {
-                    driverTypes.Add (type);
-                }
+                driverTypes.Add (type);
             }
         }
 
         List<string?> driverTypeNames = driverTypes
                                         .Where (d => !typeof (IConsoleDriverFacade).IsAssignableFrom (d))
                                         .Select (d => d!.Name)
-                                        .Union (["v2", "v2win", "v2net"])
+                                        .Union (["v2", "v2win", "v2net", "v2unix"])
                                         .ToList ()!;
 
         return (driverTypes, driverTypeNames);

+ 13 - 133
Terminal.Gui/App/Application.Mouse.cs

@@ -19,122 +19,16 @@ public static partial class Application // Mouse handling
     [ConfigurationProperty (Scope = typeof (SettingsScope))]
     public static bool IsMouseDisabled { get; set; }
 
-    /// <summary>Gets <see cref="View"/> that has registered to get continuous mouse button pressed events.</summary>
-    public static View? WantContinuousButtonPressedView { get; internal set; }
-
     /// <summary>
-    ///     Gets the view that grabbed the mouse (e.g. for dragging). When this is set, all mouse events will be routed to
-    ///     this view until the view calls <see cref="UngrabMouse"/> or the mouse is released.
+    /// Static reference to the current <see cref="IApplication"/> <see cref="IMouseGrabHandler"/>.
     /// </summary>
-    public static View? MouseGrabView { get; private set; }
-
-    /// <summary>Invoked when a view wants to grab the mouse; can be canceled.</summary>
-    public static event EventHandler<GrabMouseEventArgs>? GrabbingMouse;
-
-    /// <summary>Invoked when a view wants un-grab the mouse; can be canceled.</summary>
-    public static event EventHandler<GrabMouseEventArgs>? UnGrabbingMouse;
-
-    /// <summary>Invoked after a view has grabbed the mouse.</summary>
-    public static event EventHandler<ViewEventArgs>? GrabbedMouse;
-
-    /// <summary>Invoked after a view has un-grabbed the mouse.</summary>
-    public static event EventHandler<ViewEventArgs>? UnGrabbedMouse;
-
-    /// <summary>
-    ///     Grabs the mouse, forcing all mouse events to be routed to the specified view until <see cref="UngrabMouse"/>
-    ///     is called.
-    /// </summary>
-    /// <param name="view">View that will receive all mouse events until <see cref="UngrabMouse"/> is invoked.</param>
-    public static void GrabMouse (View? view)
+    public static IMouseGrabHandler MouseGrabHandler
     {
-        if (view is null || RaiseGrabbingMouseEvent (view))
-        {
-            return;
-        }
-
-        RaiseGrabbedMouseEvent (view);
-
-        if (Initialized)
-        {
-            // MouseGrabView is a static; only set if the application is initialized.
-            MouseGrabView = view;
-        }
+        get => ApplicationImpl.Instance.MouseGrabHandler;
+        set => ApplicationImpl.Instance.MouseGrabHandler = value ??
+                                                           throw new ArgumentNullException(nameof(value));
     }
 
-    /// <summary>Releases the mouse grab, so mouse events will be routed to the view on which the mouse is.</summary>
-    public static void UngrabMouse ()
-    {
-        if (MouseGrabView is null)
-        {
-            return;
-        }
-
-#if DEBUG_IDISPOSABLE
-        if (View.EnableDebugIDisposableAsserts)
-        {
-            ObjectDisposedException.ThrowIf (MouseGrabView.WasDisposed, MouseGrabView);
-        }
-#endif
-
-        if (!RaiseUnGrabbingMouseEvent (MouseGrabView))
-        {
-            View view = MouseGrabView;
-            MouseGrabView = null;
-            RaiseUnGrabbedMouseEvent (view);
-        }
-    }
-
-    /// <exception cref="Exception">A delegate callback throws an exception.</exception>
-    private static bool RaiseGrabbingMouseEvent (View? view)
-    {
-        if (view is null)
-        {
-            return false;
-        }
-
-        var evArgs = new GrabMouseEventArgs (view);
-        GrabbingMouse?.Invoke (view, evArgs);
-
-        return evArgs.Cancel;
-    }
-
-    /// <exception cref="Exception">A delegate callback throws an exception.</exception>
-    private static bool RaiseUnGrabbingMouseEvent (View? view)
-    {
-        if (view is null)
-        {
-            return false;
-        }
-
-        var evArgs = new GrabMouseEventArgs (view);
-        UnGrabbingMouse?.Invoke (view, evArgs);
-
-        return evArgs.Cancel;
-    }
-
-    /// <exception cref="Exception">A delegate callback throws an exception.</exception>
-    private static void RaiseGrabbedMouseEvent (View? view)
-    {
-        if (view is null)
-        {
-            return;
-        }
-
-        GrabbedMouse?.Invoke (view, new (view));
-    }
-
-    /// <exception cref="Exception">A delegate callback throws an exception.</exception>
-    private static void RaiseUnGrabbedMouseEvent (View? view)
-    {
-        if (view is null)
-        {
-            return;
-        }
-
-        UnGrabbedMouse?.Invoke (view, new (view));
-    }
-
-
     /// <summary>
     ///     INTERNAL API: Called when a mouse event is raised by the driver. Determines the view under the mouse and
     ///     calls the appropriate View mouse event handlers.
@@ -198,15 +92,6 @@ public static partial class Application // Mouse handling
             return;
         }
 
-        if (Initialized)
-        {
-            WantContinuousButtonPressedView = deepestViewUnderMouse switch
-            {
-                { WantContinuousButtonPressed: true } => deepestViewUnderMouse,
-                _ => null
-            };
-        }
-
         // May be null before the prior condition or the condition may set it as null.
         // So, the checking must be outside the prior condition.
         if (deepestViewUnderMouse is null)
@@ -258,12 +143,7 @@ public static partial class Application // Mouse handling
 
         RaiseMouseEnterLeaveEvents (viewMouseEvent.ScreenPosition, currentViewsUnderMouse);
 
-        if (Initialized)
-        {
-            WantContinuousButtonPressedView = deepestViewUnderMouse.WantContinuousButtonPressed ? deepestViewUnderMouse : null;
-        }
-
-        while (deepestViewUnderMouse.NewMouseEvent (viewMouseEvent) is not true && MouseGrabView is not { })
+        while (deepestViewUnderMouse.NewMouseEvent (viewMouseEvent) is not true && MouseGrabHandler.MouseGrabView is not { })
         {
             if (deepestViewUnderMouse is Adornment adornmentView)
             {
@@ -315,35 +195,35 @@ public static partial class Application // Mouse handling
 
     internal static bool HandleMouseGrab (View? deepestViewUnderMouse, MouseEventArgs mouseEvent)
     {
-        if (MouseGrabView is { })
+        if (MouseGrabHandler.MouseGrabView is { })
         {
 #if DEBUG_IDISPOSABLE
-            if (View.EnableDebugIDisposableAsserts && MouseGrabView.WasDisposed)
+            if (View.EnableDebugIDisposableAsserts && MouseGrabHandler.MouseGrabView.WasDisposed)
             {
-                throw new ObjectDisposedException (MouseGrabView.GetType ().FullName);
+                throw new ObjectDisposedException (MouseGrabHandler.MouseGrabView.GetType ().FullName);
             }
 #endif
 
             // If the mouse is grabbed, send the event to the view that grabbed it.
             // The coordinates are relative to the Bounds of the view that grabbed the mouse.
-            Point frameLoc = MouseGrabView.ScreenToViewport (mouseEvent.ScreenPosition);
+            Point frameLoc = MouseGrabHandler.MouseGrabView.ScreenToViewport (mouseEvent.ScreenPosition);
 
             var viewRelativeMouseEvent = new MouseEventArgs
             {
                 Position = frameLoc,
                 Flags = mouseEvent.Flags,
                 ScreenPosition = mouseEvent.ScreenPosition,
-                View = deepestViewUnderMouse ?? MouseGrabView
+                View = deepestViewUnderMouse ?? MouseGrabHandler.MouseGrabView
             };
 
             //System.Diagnostics.Debug.WriteLine ($"{nme.Flags};{nme.X};{nme.Y};{mouseGrabView}");
-            if (MouseGrabView?.NewMouseEvent (viewRelativeMouseEvent) is true)
+            if (MouseGrabHandler.MouseGrabView?.NewMouseEvent (viewRelativeMouseEvent) is true)
             {
                 return true;
             }
 
             // ReSharper disable once ConditionIsAlwaysTrueOrFalse
-            if (MouseGrabView is null && deepestViewUnderMouse is Adornment)
+            if (MouseGrabHandler.MouseGrabView is null && deepestViewUnderMouse is Adornment)
             {
                 // The view that grabbed the mouse has been disposed
                 return true;

+ 11 - 9
Terminal.Gui/App/Application.Run.cs

@@ -89,10 +89,9 @@ public static partial class Application // Run (Begin, Run, End, Stop)
         //#endif
 
         // Ensure the mouse is ungrabbed.
-        if (MouseGrabView is { })
+        if (MouseGrabHandler.MouseGrabView is { })
         {
-            UngrabMouse ();
-            MouseGrabView = null;
+            MouseGrabHandler.UngrabMouse ();
         }
 
         var rs = new RunState (toplevel);
@@ -206,6 +205,8 @@ public static partial class Application // Run (Begin, Run, End, Stop)
 
         toplevel.OnLoaded ();
 
+        LayoutAndDraw (true);
+
         if (PositionCursor ())
         {
             Driver?.UpdateCursor ();
@@ -213,9 +214,6 @@ public static partial class Application // Run (Begin, Run, End, Stop)
 
         NotifyNewRunState?.Invoke (toplevel, new (rs));
 
-        // Force an Idle event so that an Iteration (and Refresh) happen.
-        Invoke (() => { });
-
         return rs;
     }
 
@@ -366,7 +364,7 @@ public static partial class Application // Run (Begin, Run, End, Stop)
     ///         Alternatively, to have a program control the main loop and process events manually, call
     ///         <see cref="Begin(Toplevel)"/> to set things up manually and then repeatedly call
     ///         <see cref="RunLoop(RunState)"/> with the wait parameter set to false. By doing this the
-    ///         <see cref="RunLoop(RunState)"/> method will only process any pending events, timers, idle handlers and then
+    ///         <see cref="RunLoop(RunState)"/> method will only process any pending events, timers handlers and then
     ///         return control immediately.
     ///     </para>
     ///     <para>
@@ -462,7 +460,7 @@ public static partial class Application // Run (Begin, Run, End, Stop)
     /// <summary>This event is raised on each iteration of the main loop.</summary>
     /// <remarks>See also <see cref="Timeout"/></remarks>
     public static event EventHandler<IterationEventArgs>? Iteration;
-
+    
     /// <summary>The <see cref="MainLoop"/> driver for the application</summary>
     /// <value>The main loop.</value>
     internal static MainLoop? MainLoop { get; set; }
@@ -536,7 +534,7 @@ public static partial class Application // Run (Begin, Run, End, Stop)
             return firstIteration;
         }
 
-        LayoutAndDraw ();
+        LayoutAndDraw (TopLevels.Any (v => v.NeedsLayout || v.NeedsDraw));
 
         if (PositionCursor ())
         {
@@ -620,4 +618,8 @@ public static partial class Application // Run (Begin, Run, End, Stop)
 
         LayoutAndDraw (true);
     }
+    internal static void RaiseIteration ()
+    {
+        Iteration?.Invoke (null, new ());
+    }
 }

+ 13 - 4
Terminal.Gui/App/Application.Screen.cs

@@ -4,6 +4,7 @@ namespace Terminal.Gui.App;
 
 public static partial class Application // Screen related stuff
 {
+    private static readonly object _lockScreen = new ();
     private static Rectangle? _screen;
 
     /// <summary>
@@ -18,11 +19,15 @@ public static partial class Application // Screen related stuff
     {
         get
         {
-            if (_screen == null)
+            lock (_lockScreen)
             {
-                _screen = Driver?.Screen ?? new (new (0, 0), new (2048, 2048));
+                if (_screen == null)
+                {
+                    _screen = Driver?.Screen ?? new (new (0, 0), new (2048, 2048));
+                }
+
+                return _screen.Value;
             }
-            return _screen.Value;
         }
         set
         {
@@ -30,7 +35,11 @@ public static partial class Application // Screen related stuff
             {
                 throw new NotImplementedException ($"Screen locations other than 0, 0 are not yet supported");
             }
-            _screen = value;
+
+            lock (_lockScreen)
+            {
+                _screen = value;
+            }
         }
     }
 

+ 65 - 39
Terminal.Gui/App/Application.cd

@@ -1,90 +1,116 @@
 <?xml version="1.0" encoding="utf-8"?>
 <ClassDiagram MajorVersion="1" MinorVersion="1">
-  <Class Name="Terminal.Gui.Application">
+  <Class Name="Terminal.Gui.App.Application">
     <Position X="2.25" Y="1.5" Width="1.5" />
     <TypeIdentifier>
-      <HashCode>hEI4FAgAqARIspQfBQo0gTGiACNL0AICESJKoggBSg8=</HashCode>
-      <FileName>Application\Application.cs</FileName>
+      <HashCode>gEK4FIgQOAQIuhQeBwoUgSCgAAJL0AACESIKoAiBWw8=</HashCode>
+      <FileName>App\Application.cs</FileName>
     </TypeIdentifier>
   </Class>
-  <Class Name="Terminal.Gui.ApplicationNavigation" Collapsed="true">
-    <Position X="13.75" Y="1.75" Width="2" />
+  <Class Name="Terminal.Gui.App.ApplicationNavigation" Collapsed="true">
+    <Position X="14.75" Y="2.75" Width="2" />
     <TypeIdentifier>
       <HashCode>AABAAAAAAABCAAAAAAAAAAAAAAAAIgIAAAAAAAAAAAA=</HashCode>
-      <FileName>Application\ApplicationNavigation.cs</FileName>
+      <FileName>App\ApplicationNavigation.cs</FileName>
     </TypeIdentifier>
   </Class>
-  <Class Name="Terminal.Gui.IterationEventArgs" Collapsed="true">
-    <Position X="16" Y="2" Width="2" />
+  <Class Name="Terminal.Gui.App.IterationEventArgs" Collapsed="true">
+    <Position X="17" Y="3" Width="2" />
     <TypeIdentifier>
       <HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</HashCode>
-      <FileName>Application\IterationEventArgs.cs</FileName>
+      <FileName>App\IterationEventArgs.cs</FileName>
     </TypeIdentifier>
   </Class>
-  <Class Name="Terminal.Gui.MainLoop" Collapsed="true" BaseTypeListCollapsed="true">
-    <Position X="10.25" Y="2.75" Width="1.5" />
+  <Class Name="Terminal.Gui.App.MainLoop" Collapsed="true" BaseTypeListCollapsed="true">
+    <Position X="11.25" Y="3.75" Width="1.5" />
     <TypeIdentifier>
-      <HashCode>CAAAIAAAASAAAQAQAAAAAIBADQAAEAAYIgIIwAAAAAI=</HashCode>
-      <FileName>Application\MainLoop.cs</FileName>
+      <HashCode>AAAAAAAAACAAAAAAAAAAAAAACBAAEAAIIAIAgAAAEAI=</HashCode>
+      <FileName>App\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" />
+  <Class Name="Terminal.Gui.App.MainLoopSyncContext" Collapsed="true">
+    <Position X="13" Y="3.75" Width="2" />
     <TypeIdentifier>
       <HashCode>AAAAAgAAAAAAAAAAAEAAAAAACAAAAAAAAAAAAAAAAAA=</HashCode>
-      <FileName>Application\MainLoopSyncContext.cs</FileName>
+      <FileName>App\MainLoopSyncContext.cs</FileName>
     </TypeIdentifier>
   </Class>
-  <Class Name="Terminal.Gui.RunState" Collapsed="true" BaseTypeListCollapsed="true">
-    <Position X="14.25" Y="3" Width="1.5" />
+  <Class Name="Terminal.Gui.App.RunState" Collapsed="true" BaseTypeListCollapsed="true">
+    <Position X="15.25" Y="4" Width="1.5" />
     <TypeIdentifier>
       <HashCode>AAAAAAAAACACAgAAAAAAAAAAAAAAAAACQAAAAAAAAAA=</HashCode>
-      <FileName>Application\RunState.cs</FileName>
+      <FileName>App\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" />
+  <Class Name="Terminal.Gui.App.RunStateEventArgs" Collapsed="true">
+    <Position X="17" Y="4" Width="2" />
     <TypeIdentifier>
       <HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAA=</HashCode>
-      <FileName>Application\RunStateEventArgs.cs</FileName>
+      <FileName>App\RunStateEventArgs.cs</FileName>
     </TypeIdentifier>
   </Class>
-  <Class Name="Terminal.Gui.Timeout" Collapsed="true">
-    <Position X="10.25" Y="3.75" Width="1.5" />
+  <Class Name="Terminal.Gui.App.Timeout" Collapsed="true">
+    <Position X="11.25" Y="4.75" Width="1.5" />
     <TypeIdentifier>
       <HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAQAA=</HashCode>
-      <FileName>Application\Timeout.cs</FileName>
+      <FileName>App\Timeout.cs</FileName>
     </TypeIdentifier>
   </Class>
-  <Class Name="Terminal.Gui.TimeoutEventArgs" Collapsed="true">
-    <Position X="12" Y="3.75" Width="2" />
+  <Class Name="Terminal.Gui.App.TimeoutEventArgs" Collapsed="true">
+    <Position X="13" Y="4.75" Width="2" />
     <TypeIdentifier>
       <HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAAACAIAAAAAAAAAAAA=</HashCode>
-      <FileName>Application\TimeoutEventArgs.cs</FileName>
+      <FileName>App\TimeoutEventArgs.cs</FileName>
     </TypeIdentifier>
   </Class>
-  <Class Name="Terminal.Gui.ApplicationImpl" BaseTypeListCollapsed="true">
-    <Position X="5.75" Y="1.75" Width="1.5" />
+  <Class Name="Terminal.Gui.App.ApplicationImpl" BaseTypeListCollapsed="true">
+    <Position X="4" Y="5" Width="2" />
     <TypeIdentifier>
-      <HashCode>AAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAQAACAACAAAI=</HashCode>
-      <FileName>Application\ApplicationImpl.cs</FileName>
+      <HashCode>AABgAAAAIAAIAgQUAAAAAQAAAAAAAAAAQAAKgAAAEAI=</HashCode>
+      <FileName>App\ApplicationImpl.cs</FileName>
     </TypeIdentifier>
     <Lollipop Position="0.2" />
   </Class>
-  <Interface Name="Terminal.Gui.IMainLoopDriver" Collapsed="true">
-    <Position X="12" Y="5" Width="1.5" />
+  <Class Name="Terminal.Gui.App.MouseGrabHandler" Collapsed="true">
+    <Position X="6.25" Y="9.25" Width="2" />
+    <TypeIdentifier>
+      <HashCode>BAAgAAAAgABAAoAAAAAAABAAACEAAAAAAABAAgAAAAA=</HashCode>
+      <FileName>App\MouseGrabHandler.cs</FileName>
+    </TypeIdentifier>
+    <Lollipop Position="0.2" />
+  </Class>
+  <Interface Name="Terminal.Gui.App.IMainLoopDriver">
+    <Position X="11.25" Y="1.5" Width="1.5" />
     <TypeIdentifier>
       <HashCode>AAAAAAAACAAAAAQAAAAABAAAAAAAEAAAAAAAAAAAAAA=</HashCode>
-      <FileName>Application\MainLoop.cs</FileName>
+      <FileName>App\MainLoop.cs</FileName>
+    </TypeIdentifier>
+  </Interface>
+  <Interface Name="Terminal.Gui.App.IApplication">
+    <Position X="4" Y="1.5" Width="1.5" />
+    <TypeIdentifier>
+      <HashCode>AAAgAAAAAAAIAgQUAAAAAQAAAAAAAAAAAAAKgAAAEAI=</HashCode>
+      <FileName>App\IApplication.cs</FileName>
+    </TypeIdentifier>
+    <ShowAsAssociation>
+      <Property Name="MouseGrabHandler" />
+      <Property Name="TimedEvents" />
+    </ShowAsAssociation>
+  </Interface>
+  <Interface Name="Terminal.Gui.App.IMouseGrabHandler">
+    <Position X="7" Y="1.5" Width="2" />
+    <TypeIdentifier>
+      <HashCode>BAAgAAAAAAAAAgAAAAAAABAAACEAAAAAAAAAAgAAAAA=</HashCode>
+      <FileName>App\IMouseGrabHandler.cs</FileName>
     </TypeIdentifier>
   </Interface>
-  <Interface Name="Terminal.Gui.IApplication">
-    <Position X="4" Y="1.75" Width="1.5" />
+  <Interface Name="Terminal.Gui.App.ITimedEvents">
+    <Position X="7" Y="4.5" Width="2" />
     <TypeIdentifier>
-      <HashCode>AAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAACAAAAAAI=</HashCode>
-      <FileName>Application\IApplication.cs</FileName>
+      <HashCode>BAAAIAAAAQAAAAAQACAAAIBAAQAAAAAAAAAIgAAAAAA=</HashCode>
+      <FileName>App\ITimedEvents.cs</FileName>
     </TypeIdentifier>
   </Interface>
   <Font Name="Segoe UI" Size="9" />

+ 24 - 12
Terminal.Gui/App/Application.cs

@@ -42,6 +42,29 @@ 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; } = GetSupportedCultures ();
 
+
+    /// <summary>
+    /// <para>
+    /// Handles recurring events. These are invoked on the main UI thread - allowing for
+    /// safe updates to <see cref="View"/> instances.
+    /// </para>
+    /// </summary>
+    public static ITimedEvents? TimedEvents => ApplicationImpl.Instance?.TimedEvents;
+
+    /// <summary>
+    /// Maximum number of iterations of the main loop (and hence draws)
+    /// to allow to occur per second. Defaults to <see cref="DefaultMaximumIterationsPerSecond"/>> which is a 40ms sleep
+    /// after iteration (factoring in how long iteration took to run).
+    /// <remarks>Note that not every iteration draws (see <see cref="View.NeedsDraw"/>).
+    /// Only affects v2 drivers.</remarks>
+    /// </summary>
+    public static ushort MaximumIterationsPerSecond = DefaultMaximumIterationsPerSecond;
+
+    /// <summary>
+    /// Default value for <see cref="MaximumIterationsPerSecond"/>
+    /// </summary>
+    public const ushort DefaultMaximumIterationsPerSecond = 25;
+
     /// <summary>
     ///     Gets a string representation of the Application as rendered by <see cref="Driver"/>.
     /// </summary>
@@ -221,7 +244,7 @@ public static partial class Application
         // Run State stuff
         NotifyNewRunState = null;
         NotifyStopRunState = null;
-        MouseGrabView = null;
+        MouseGrabHandler = new MouseGrabHandler ();
         Initialized = false;
 
         // Mouse
@@ -229,12 +252,7 @@ public static partial class Application
         // last mouse pos.
         //_lastMousePosition = null;
         CachedViewsUnderMouse.Clear ();
-        WantContinuousButtonPressedView = null;
         MouseEvent = null;
-        GrabbedMouse = null;
-        UnGrabbingMouse = null;
-        GrabbedMouse = null;
-        UnGrabbedMouse = null;
 
         // Keyboard
         KeyDown = null;
@@ -252,10 +270,4 @@ public static partial class Application
         // (https://github.com/gui-cs/Terminal.Gui/issues/1084).
         SynchronizationContext.SetSynchronizationContext (null);
     }
-
-    /// <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); }
 }

+ 38 - 17
Terminal.Gui/App/ApplicationImpl.cs

@@ -18,6 +18,15 @@ public class ApplicationImpl : IApplication
     /// </summary>
     public static IApplication Instance => _lazyInstance.Value;
 
+
+    /// <inheritdoc/>
+    public virtual ITimedEvents? TimedEvents => Application.MainLoop?.TimedEvents;
+
+    /// <summary>
+    /// Handles which <see cref="View"/> (if any) has captured the mouse
+    /// </summary>
+    public IMouseGrabHandler MouseGrabHandler { get; set; } = new MouseGrabHandler ();
+
     /// <summary>
     /// Change the singleton implementation, should not be called except before application
     /// startup. This method lets you provide alternative implementations of core static gateway
@@ -119,7 +128,7 @@ public class ApplicationImpl : IApplication
     ///         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
+    ///         <see cref="Application.RunLoop(RunState)"/> method will only process any pending events, timers handlers and then
     ///         return control immediately.
     ///     </para>
     ///     <para>When using <see cref="Run{T}"/> or
@@ -261,7 +270,24 @@ public class ApplicationImpl : IApplication
     /// <inheritdoc />
     public virtual void Invoke (Action action)
     {
-        Application.MainLoop?.AddIdle (
+
+        // If we are already on the main UI thread
+        if (Application.MainThreadId == Thread.CurrentThread.ManagedThreadId)
+        {
+            action ();
+            WakeupMainLoop ();
+
+            return;
+        }
+
+        if (Application.MainLoop == null)
+        {
+            Logging.Warning ("Ignored Invoke because MainLoop is not initialized yet");
+            return;
+        }
+
+
+        Application.AddTimeout (TimeSpan.Zero,
                            () =>
                            {
                                action ();
@@ -269,25 +295,20 @@ public class ApplicationImpl : IApplication
                                return false;
                            }
                           );
-    }
 
-    /// <inheritdoc />
-    public bool IsLegacy { get; protected set; } = true;
+        WakeupMainLoop ();
 
-    /// <inheritdoc />
-    public virtual void AddIdle (Func<bool> func)
-    {
-        if (Application.MainLoop is null)
+        void WakeupMainLoop ()
         {
-            throw new NotInitializedException ("Cannot add idle before main loop is initialized");
+            // Ensure the action is executed in the main loop
+            // Wakeup mainloop if it's waiting for events
+            Application.MainLoop?.Wakeup ();
         }
-
-        // 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 bool IsLegacy { get; protected set; } = true;
+
     /// <inheritdoc />
     public virtual object AddTimeout (TimeSpan time, Func<bool> callback)
     {
@@ -296,13 +317,13 @@ public class ApplicationImpl : IApplication
             throw new NotInitializedException ("Cannot add timeout before main loop is initialized", null);
         }
 
-        return Application.MainLoop.TimedEvents.AddTimeout (time, callback);
+        return Application.MainLoop.TimedEvents.Add (time, callback);
     }
 
     /// <inheritdoc />
     public virtual bool RemoveTimeout (object token)
     {
-        return Application.MainLoop?.TimedEvents.RemoveTimeout (token) ?? false;
+        return Application.MainLoop?.TimedEvents.Remove (token) ?? false;
     }
 
     /// <inheritdoc />

+ 9 - 139
Terminal.Gui/App/Clipboard/Clipboard.cs

@@ -1,5 +1,4 @@
-using System.Diagnostics;
-
+#nullable enable
 namespace Terminal.Gui.App;
 
 /// <summary>Provides cut, copy, and paste support for the OS clipboard.</summary>
@@ -20,10 +19,10 @@ namespace Terminal.Gui.App;
 /// </remarks>
 public static class Clipboard
 {
-    private static string _contents = string.Empty;
+    private static string? _contents = string.Empty;
 
     /// <summary>Gets (copies from) or sets (pastes to) the contents of the OS clipboard.</summary>
-    public static string Contents
+    public static string? Contents
     {
         get
         {
@@ -31,13 +30,8 @@ public static class Clipboard
             {
                 if (IsSupported)
                 {
-                    string clipData = Application.Driver?.Clipboard.GetClipboardData ();
-
-                    if (clipData is null)
-                    {
-                        // throw new InvalidOperationException ($"{Application.Driver?.GetType ().Name}.GetClipboardData returned null instead of string.Empty");
-                        clipData = string.Empty;
-                    }
+                    // throw new InvalidOperationException ($"{Application.Driver?.GetType ().Name}.GetClipboardData returned null instead of string.Empty");
+                    string? clipData = Application.Driver?.Clipboard?.GetClipboardData () ?? string.Empty;
 
                     _contents = clipData;
                 }
@@ -55,12 +49,9 @@ public static class Clipboard
             {
                 if (IsSupported)
                 {
-                    if (value is null)
-                    {
-                        value = string.Empty;
-                    }
+                    value ??= string.Empty;
 
-                    Application.Driver?.Clipboard.SetClipboardData (value);
+                    Application.Driver?.Clipboard?.SetClipboardData (value);
                 }
 
                 _contents = value;
@@ -74,126 +65,5 @@ public static class Clipboard
 
     /// <summary>Returns true if the environmental dependencies are in place to interact with the OS clipboard.</summary>
     /// <remarks></remarks>
-    public static bool IsSupported => Application.Driver?.Clipboard.IsSupported ?? false;
-
-    /// <summary>Copies the _contents of the OS clipboard to <paramref name="result"/> if possible.</summary>
-    /// <param name="result">The _contents of the OS clipboard if successful, <see cref="string.Empty"/> if not.</param>
-    /// <returns><see langword="true"/> the OS clipboard was retrieved, <see langword="false"/> otherwise.</returns>
-    public static bool TryGetClipboardData (out string result)
-    {
-        if (IsSupported && Application.Driver!.Clipboard.TryGetClipboardData (out result))
-        {
-            _contents = result;
-
-            return true;
-        }
-
-        result = string.Empty;
-
-        return false;
-    }
-
-    /// <summary>Pastes the <paramref name="text"/> to the OS clipboard if possible.</summary>
-    /// <param name="text">The text to paste to the OS clipboard.</param>
-    /// <returns><see langword="true"/> the OS clipboard was set, <see langword="false"/> otherwise.</returns>
-    public static bool TrySetClipboardData (string text)
-    {
-        if (IsSupported && Application.Driver!.Clipboard.TrySetClipboardData (text))
-        {
-            _contents = text;
-
-            return true;
-        }
-
-        return false;
-    }
-}
-
-/// <summary>
-///     Helper class for console drivers to invoke shell commands to interact with the clipboard. Used primarily by
-///     CursesDriver, but also used in Unit tests which is why it is in IConsoleDriver.cs.
-/// </summary>
-internal static class ClipboardProcessRunner
-{
-    public static (int exitCode, string result) Bash (
-        string commandLine,
-        string inputText = "",
-        bool waitForOutput = false
-    )
-    {
-        var arguments = $"-c \"{commandLine}\"";
-        (int exitCode, string result) = Process ("bash", arguments, inputText, waitForOutput);
-
-        return (exitCode, result.TrimEnd ());
-    }
-
-    public static bool DoubleWaitForExit (this Process process)
-    {
-        bool result = process.WaitForExit (500);
-
-        if (result)
-        {
-            process.WaitForExit ();
-        }
-
-        return result;
-    }
-
-    public static bool FileExists (this string value) { return !string.IsNullOrEmpty (value) && !value.Contains ("not found"); }
-
-    public static (int exitCode, string result) Process (
-        string cmd,
-        string arguments,
-        string input = null,
-        bool waitForOutput = true
-    )
-    {
-            var output = string.Empty;
-
-        using (var process = new Process
-               {
-                   StartInfo = new()
-                   {
-                       FileName = cmd,
-                       Arguments = arguments,
-                       RedirectStandardOutput = true,
-                       RedirectStandardError = true,
-                       RedirectStandardInput = true,
-                       UseShellExecute = false,
-                       CreateNoWindow = true
-                   }
-               })
-        {
-            TaskCompletionSource<bool> eventHandled = new ();
-            process.Start ();
-
-            if (!string.IsNullOrEmpty (input))
-            {
-                process.StandardInput.Write (input);
-                process.StandardInput.Close ();
-            }
-
-            if (!process.WaitForExit (5000))
-            {
-                var timeoutError =
-                    $@"Process timed out. Command line: {process.StartInfo.FileName} {process.StartInfo.Arguments}.";
-
-                throw new TimeoutException (timeoutError);
-            }
-
-            if (waitForOutput && process.StandardOutput.Peek () != -1)
-            {
-                output = process.StandardOutput.ReadToEnd ();
-            }
-
-            if (process.ExitCode > 0)
-            {
-                output = $@"Process failed to run. Command line: {cmd} {arguments}.
-										Output: {output}
-										Error: {process.StandardError.ReadToEnd ()}";
-            }
-
-            return (process.ExitCode, output);
-        }
-    }
-}
+    public static bool IsSupported => Application.Driver?.Clipboard?.IsSupported ?? false;
+}

+ 79 - 0
Terminal.Gui/App/Clipboard/ClipboardProcessRunner.cs

@@ -0,0 +1,79 @@
+#nullable enable
+using System.Diagnostics;
+
+namespace Terminal.Gui.App;
+
+/// <summary>
+///     Helper class for console drivers to invoke shell commands to interact with the clipboard. Used primarily by
+///     CursesDriver, but also used in Unit tests which is why it is in IConsoleDriver.cs.
+/// </summary>
+internal static class ClipboardProcessRunner
+{
+    public static (int exitCode, string result) Bash (
+        string commandLine,
+        string inputText = "",
+        bool waitForOutput = false
+    )
+    {
+        var arguments = $"-c \"{commandLine}\"";
+        (int exitCode, string result) = Process ("bash", arguments, inputText, waitForOutput);
+
+        return (exitCode, result.TrimEnd ());
+    }
+
+    public static bool FileExists (this string value) { return !string.IsNullOrEmpty (value) && !value.Contains ("not found"); }
+
+    public static (int exitCode, string result) Process (
+        string cmd,
+        string arguments,
+        string? input = null,
+        bool waitForOutput = true
+    )
+    {
+        var output = string.Empty;
+
+        using var process = new Process ();
+
+        process.StartInfo = new()
+        {
+            FileName = cmd,
+            Arguments = arguments,
+            RedirectStandardOutput = true,
+            RedirectStandardError = true,
+            RedirectStandardInput = true,
+            UseShellExecute = false,
+            CreateNoWindow = true
+        };
+
+        TaskCompletionSource<bool> eventHandled = new ();
+        process.Start ();
+
+        if (!string.IsNullOrEmpty (input))
+        {
+            process.StandardInput.Write (input);
+            process.StandardInput.Close ();
+        }
+
+        if (!process.WaitForExit (5000))
+        {
+            var timeoutError =
+                $@"Process timed out. Command line: {process.StartInfo.FileName} {process.StartInfo.Arguments}.";
+
+            throw new TimeoutException (timeoutError);
+        }
+
+        if (waitForOutput && process.StandardOutput.Peek () != -1)
+        {
+            output = process.StandardOutput.ReadToEnd ();
+        }
+
+        if (process.ExitCode > 0)
+        {
+            output = $@"Process failed to run. Command line: {cmd} {arguments}.
+										Output: {output}
+										Error: {process.StandardError.ReadToEnd ()}";
+        }
+
+        return (process.ExitCode, output);
+    }
+}

+ 13 - 8
Terminal.Gui/App/IApplication.cs

@@ -9,6 +9,17 @@ namespace Terminal.Gui.App;
 /// </summary>
 public interface IApplication
 {
+    /// <summary>
+    /// Handles recurring events. These are invoked on the main UI thread - allowing for
+    /// safe updates to <see cref="View"/> instances.
+    /// </summary>
+    ITimedEvents? TimedEvents { get; }
+
+    /// <summary>
+    /// Handles grabbing the mouse (only a single <see cref="View"/> can grab the mouse at once).
+    /// </summary>
+    IMouseGrabHandler MouseGrabHandler { get; set; }
+
     /// <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>
@@ -106,7 +117,7 @@ public interface IApplication
     ///         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
+    ///         <see cref="Application.RunLoop(RunState)"/> method will only process any pending events, timers handlers and then
     ///         return control immediately.
     ///     </para>
     ///     <para>When using <see cref="Run{T}"/> or
@@ -156,13 +167,7 @@ public interface IApplication
     /// 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

+ 0 - 90
Terminal.Gui/App/ITimedEvents.cs

@@ -1,90 +0,0 @@
-#nullable enable
-using System.Collections.ObjectModel;
-
-namespace Terminal.Gui.App;
-
-/// <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;
-}

+ 4 - 32
Terminal.Gui/App/MainLoop.cs

@@ -32,7 +32,7 @@ internal interface IMainLoopDriver
     void Wakeup ();
 }
 
-/// <summary>The MainLoop monitors timers and idle handlers.</summary>
+/// <summary>The main event loop of v1 driver based applications.</summary>
 /// <remarks>
 ///     Monitoring of file descriptors is only available on Unix, there does not seem to be a way of supporting this
 ///     on Windows.
@@ -40,7 +40,7 @@ internal interface IMainLoopDriver
 public class MainLoop : IDisposable
 {
     /// <summary>
-    /// Gets the class responsible for handling idles and timeouts
+    /// Gets the class responsible for handling timeouts
     /// </summary>
     public ITimedEvents TimedEvents { get; } = new TimedEvents();
 
@@ -75,32 +75,6 @@ public class MainLoop : IDisposable
         MainLoopDriver = null;
     }
 
-    /// <summary>
-    ///     Adds specified idle handler function to <see cref="MainLoop"/> processing. The handler function will be called
-    ///     once per iteration of the main loop after other events have been handled.
-    /// </summary>
-    /// <remarks>
-    ///     <para>Remove an idle handler by calling <see cref="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="TimedEvents.RemoveIdle(Func{bool})"/> .</param>
-    // QUESTION: Why are we re-inventing the event wheel here?
-    // PERF: This is heavy.
-    // CONCURRENCY: Race conditions exist here.
-    // CONCURRENCY: null delegates will hose this.
-    // 
-    internal Func<bool> AddIdle (Func<bool> idleHandler)
-    {
-        TimedEvents.AddIdle (idleHandler);
-
-        MainLoopDriver?.Wakeup ();
-
-        return idleHandler;
-    }
-
 
     /// <summary>Determines whether there are pending events to be processed.</summary>
     /// <remarks>
@@ -127,7 +101,7 @@ public class MainLoop : IDisposable
 
     /// <summary>Runs one iteration of timers and file watches</summary>
     /// <remarks>
-    ///     Use this to process all pending events (timers, idle handlers and file watches).
+    ///     Use this to process all pending events (timers handlers and file watches).
     ///     <code>
     ///     while (main.EventsPending ()) RunIteration ();
     ///   </code>
@@ -138,9 +112,7 @@ public class MainLoop : IDisposable
 
         MainLoopDriver?.Iteration ();
 
-        TimedEvents.LockAndRunTimers ();
-
-        TimedEvents.LockAndRunIdles ();
+        TimedEvents.RunTimers ();
     }
 
     private void RunAnsiScheduler ()

+ 16 - 7
Terminal.Gui/App/MainLoopSyncContext.cs

@@ -10,14 +10,23 @@ internal sealed class MainLoopSyncContext : SynchronizationContext
 
     public override void Post (SendOrPostCallback d, object state)
     {
-        Application.MainLoop?.AddIdle (
-                                       () =>
-                                       {
-                                           d (state);
+        // Queue the task
+        if (ApplicationImpl.Instance.IsLegacy)
+        {
+            Application.MainLoop?.TimedEvents.Add (TimeSpan.Zero,
+                                                   () =>
+                                                   {
+                                                       d (state);
 
-                                           return false;
-                                       }
-                                      );
+                                                       return false;
+                                                   }
+                                                  );
+            Application.MainLoop?.Wakeup ();
+        }
+        else
+        {
+            ApplicationImpl.Instance.Invoke (() => { d (state); });
+        }
     }
 
     //_mainLoop.Driver.Wakeup ();

+ 87 - 0
Terminal.Gui/App/Mouse/IMouseGrabHandler.cs

@@ -0,0 +1,87 @@
+#nullable enable
+namespace Terminal.Gui.App;
+
+/// <summary>
+///     Defines a contract for tracking which <see cref="View"/> (if any) has 'grabbed' the mouse,
+///     giving it exclusive priority for mouse events such as movement, button presses, and release.
+///     <para>
+///         This is typically used for scenarios like dragging, scrolling, or any interaction where a view
+///         needs to receive all mouse events until the operation completes (e.g., a scrollbar thumb being dragged).
+///     </para>
+///     <para>
+///         Usage pattern:
+///         <list type="number">
+///             <item>
+///                 <description>Call <see cref="GrabMouse"/> to route all mouse events to a specific view.</description>
+///             </item>
+///             <item>
+///                 <description>Call <see cref="UngrabMouse"/> to release the grab and restore normal mouse routing.</description>
+///             </item>
+///             <item>
+///                 <description>
+///                     Listen to <see cref="GrabbingMouse"/>, <see cref="GrabbedMouse"/>, <see cref="UnGrabbingMouse"/>,
+///                     and <see cref="UnGrabbedMouse"/> for grab lifecycle events.
+///                 </description>
+///             </item>
+///         </list>
+///     </para>
+/// </summary>
+public interface IMouseGrabHandler
+{
+    /// <summary>
+    ///     Occurs after a view has grabbed the mouse.
+    ///     <para>
+    ///         This event is raised after the mouse grab operation is complete and the specified view will receive all mouse
+    ///         events.
+    ///     </para>
+    /// </summary>
+    public event EventHandler<ViewEventArgs>? GrabbedMouse;
+
+    /// <summary>
+    ///     Occurs when a view requests to grab the mouse; can be canceled.
+    ///     <para>
+    ///         Handlers can set <c>e.Cancel</c> to <see langword="true"/> to prevent the grab.
+    ///     </para>
+    /// </summary>
+    public event EventHandler<GrabMouseEventArgs>? GrabbingMouse;
+
+    /// <summary>
+    ///     Grabs the mouse, forcing all mouse events to be routed to the specified view until <see cref="UngrabMouse"/> is
+    ///     called.
+    /// </summary>
+    /// <param name="view">
+    ///     The <see cref="View"/> that will receive all mouse events until <see cref="UngrabMouse"/> is invoked.
+    ///     If <see langword="null"/>, the grab is released.
+    /// </param>
+    public void GrabMouse (View? view);
+
+    /// <summary>
+    ///     Gets the view that currently has grabbed the mouse (e.g., for dragging).
+    ///     <para>
+    ///         When this property is not <see langword="null"/>, all mouse events are routed to this view until
+    ///         <see cref="UngrabMouse"/> is called or the mouse is released.
+    ///     </para>
+    /// </summary>
+    public View? MouseGrabView { get; }
+
+    /// <summary>
+    ///     Occurs after a view has released the mouse grab.
+    ///     <para>
+    ///         This event is raised after the mouse grab has been released and normal mouse routing resumes.
+    ///     </para>
+    /// </summary>
+    public event EventHandler<ViewEventArgs>? UnGrabbedMouse;
+
+    /// <summary>
+    ///     Occurs when a view requests to release the mouse grab; can be canceled.
+    ///     <para>
+    ///         Handlers can set <c>e.Cancel</c> to <see langword="true"/> to prevent the ungrab.
+    ///     </para>
+    /// </summary>
+    public event EventHandler<GrabMouseEventArgs>? UnGrabbingMouse;
+
+    /// <summary>
+    ///     Releases the mouse grab, so mouse events will be routed to the view under the mouse pointer.
+    /// </summary>
+    public void UngrabMouse ();
+}

+ 118 - 0
Terminal.Gui/App/Mouse/MouseGrabHandler.cs

@@ -0,0 +1,118 @@
+#nullable enable
+namespace Terminal.Gui.App;
+
+/// <summary>
+///     INTERNAL: Implements <see cref="IMouseGrabHandler"/> to manage which <see cref="View"/> (if any) has 'grabbed' the mouse,
+///     giving it exclusive priority for mouse events such as movement, button presses, and release.
+///     <para>
+///         Used for scenarios like dragging, scrolling, or any interaction where a view needs to receive all mouse events
+///         until the operation completes (e.g., a scrollbar thumb being dragged).
+///     </para>
+///     <para>
+///         See <see cref="IMouseGrabHandler"/> for usage details.
+///     </para>
+/// </summary>
+internal class MouseGrabHandler : IMouseGrabHandler
+{
+    /// <inheritdoc/>
+    public View? MouseGrabView { get; private set; }
+
+    /// <inheritdoc/>
+    public event EventHandler<GrabMouseEventArgs>? GrabbingMouse;
+
+    /// <inheritdoc/>
+    public event EventHandler<GrabMouseEventArgs>? UnGrabbingMouse;
+
+    /// <inheritdoc/>
+    public event EventHandler<ViewEventArgs>? GrabbedMouse;
+
+    /// <inheritdoc/>
+    public event EventHandler<ViewEventArgs>? UnGrabbedMouse;
+
+    /// <inheritdoc/>
+    public void GrabMouse (View? view)
+    {
+        if (view is null || RaiseGrabbingMouseEvent (view))
+        {
+            return;
+        }
+
+        RaiseGrabbedMouseEvent (view);
+
+        // MouseGrabView is a static; only set if the application is initialized.
+        MouseGrabView = view;
+    }
+
+    /// <inheritdoc/>
+    public void UngrabMouse ()
+    {
+        if (MouseGrabView is null)
+        {
+            return;
+        }
+
+#if DEBUG_IDISPOSABLE
+        if (View.EnableDebugIDisposableAsserts)
+        {
+            ObjectDisposedException.ThrowIf (MouseGrabView.WasDisposed, MouseGrabView);
+        }
+#endif
+
+        if (!RaiseUnGrabbingMouseEvent (MouseGrabView))
+        {
+            View view = MouseGrabView;
+            MouseGrabView = null;
+            RaiseUnGrabbedMouseEvent (view);
+        }
+    }
+
+    /// <exception cref="Exception">A delegate callback throws an exception.</exception>
+    private bool RaiseGrabbingMouseEvent (View? view)
+    {
+        if (view is null)
+        {
+            return false;
+        }
+
+        var evArgs = new GrabMouseEventArgs (view);
+        GrabbingMouse?.Invoke (view, evArgs);
+
+        return evArgs.Cancel;
+    }
+
+    /// <exception cref="Exception">A delegate callback throws an exception.</exception>
+    private bool RaiseUnGrabbingMouseEvent (View? view)
+    {
+        if (view is null)
+        {
+            return false;
+        }
+
+        var evArgs = new GrabMouseEventArgs (view);
+        UnGrabbingMouse?.Invoke (view, evArgs);
+
+        return evArgs.Cancel;
+    }
+
+    /// <exception cref="Exception">A delegate callback throws an exception.</exception>
+    private void RaiseGrabbedMouseEvent (View? view)
+    {
+        if (view is null)
+        {
+            return;
+        }
+
+        GrabbedMouse?.Invoke (view, new (view));
+    }
+
+    /// <exception cref="Exception">A delegate callback throws an exception.</exception>
+    private void RaiseUnGrabbedMouseEvent (View? view)
+    {
+        if (view is null)
+        {
+            return;
+        }
+
+        UnGrabbedMouse?.Invoke (view, new (view));
+    }
+}

+ 4 - 2
Terminal.Gui/App/RunState.cs

@@ -44,15 +44,16 @@ public class RunState : IDisposable
     }
 
 #if DEBUG_IDISPOSABLE
+#pragma warning disable CS0419 // Ambiguous reference in cref attribute
     /// <summary>
-    ///     Gets whether <see cref="Dispose"/> was called on this RunState or not.
+    ///     Gets whether <see cref="RunState.Dispose"/> was called on this RunState or not.
     ///     For debug purposes to verify objects are being disposed properly.
     ///     Only valid when DEBUG_IDISPOSABLE is defined.
     /// </summary>
     public bool WasDisposed { get; private set; }
 
     /// <summary>
-    ///     Gets the number of times <see cref="Dispose"/> was called on this object.
+    ///     Gets the number of times <see cref="RunState.Dispose"/> was called on this object.
     ///     For debug purposes to verify objects are being disposed properly.
     ///     Only valid when DEBUG_IDISPOSABLE is defined.
     /// </summary>
@@ -71,5 +72,6 @@ public class RunState : IDisposable
     {
         Instances.Add (this);
     }
+#pragma warning restore CS0419 // Ambiguous reference in cref attribute
 #endif
 }

+ 0 - 18
Terminal.Gui/App/Timeout.cs

@@ -1,18 +0,0 @@
-//
-// MainLoop.cs: IMainLoopDriver and MainLoop for Terminal.Gui
-//
-// Authors:
-//   Miguel de Icaza ([email protected])
-//
-
-namespace Terminal.Gui.App;
-
-/// <summary>Provides data for timers running manipulation.</summary>
-public sealed class Timeout
-{
-    /// <summary>The function that will be invoked.</summary>
-    public Func<bool> Callback;
-
-    /// <summary>Time to wait before invoke the callback.</summary>
-    public TimeSpan Span;
-}

+ 64 - 0
Terminal.Gui/App/Timeout/ITimedEvents.cs

@@ -0,0 +1,64 @@
+#nullable enable
+namespace Terminal.Gui.App;
+
+/// <summary>
+///     Manages timers.
+/// </summary>
+public interface ITimedEvents
+{
+    /// <summary>
+    ///     Adds a timeout to the application.
+    /// </summary>
+    /// <remarks>
+    ///     When the specified time passes, the callback will be invoked. If the callback returns <see langword="true"/>, the
+    ///     timeout will be
+    ///     reset, repeating the invocation. If it returns <see langword="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="Remove"/>.
+    /// </remarks>
+    object Add (TimeSpan time, Func<bool> callback);
+
+    /// <inheritdoc cref="Add(System.TimeSpan,System.Func{bool})"/>
+    object Add (Timeout timeout);
+
+    /// <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>? Added;
+
+    /// <summary>
+    ///     Called from <see cref="IMainLoopDriver.EventsPending"/> to check if there are any outstanding timer 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 active; otherwise, <see langword="false"/>.
+    /// </returns>
+    bool CheckTimers (out int waitTimeout);
+
+    /// <summary>
+    ///     Removes a previously scheduled timeout.
+    /// </summary>
+    /// <remarks>
+    ///     The token parameter is the value returned by <see cref="Add(TimeSpan, Func{bool})"/> or <see cref="Add(Timeout)"/>.
+    /// </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 Remove (object token);
+
+    /// <summary>
+    ///     Runs all timeouts that are due.
+    /// </summary>
+    void RunTimers ();
+
+    /// <summary>
+    ///     Returns the next planned execution time (key - UTC ticks)
+    ///     for each timeout that is not actively executing.
+    /// </summary>
+    SortedList<long, Timeout> Timeouts { get; }
+}

+ 38 - 0
Terminal.Gui/App/Timeout/LogarithmicTimeout.cs

@@ -0,0 +1,38 @@
+namespace Terminal.Gui.App;
+
+/// <summary>Implements a logarithmic increasing timeout.</summary>
+public class LogarithmicTimeout : Timeout
+{
+    /// <summary>
+    ///     Creates a new instance where stages are the logarithm multiplied by the
+    ///     <paramref name="baseDelay"/> (starts fast then slows).
+    /// </summary>
+    /// <param name="baseDelay">Multiple for the logarithm</param>
+    /// <param name="callback">Method to invoke</param>
+    public LogarithmicTimeout (TimeSpan baseDelay, Func<bool> callback)
+    {
+        _baseDelay = baseDelay;
+        Callback = callback;
+    }
+
+    private readonly TimeSpan _baseDelay;
+    private int _stage;
+
+    /// <summary>Increments the stage to increase the timeout.</summary>
+    public void AdvanceStage () { _stage++; }
+
+    /// <summary>Resets the stage back to zero.</summary>
+    public void Reset () { _stage = 0; }
+
+    /// <summary>Gets the current calculated Span based on the stage.</summary>
+    public override TimeSpan Span
+    {
+        get
+        {
+            // Calculate logarithmic increase
+            double multiplier = Math.Log (_stage + 1); // ln(stage + 1)
+
+            return TimeSpan.FromMilliseconds (_baseDelay.TotalMilliseconds * multiplier);
+        }
+    }
+}

+ 53 - 0
Terminal.Gui/App/Timeout/SmoothAcceleratingTimeout.cs

@@ -0,0 +1,53 @@
+namespace Terminal.Gui.App;
+
+/// <summary>
+///     Timeout which accelerates slowly at first then fast up to a maximum speed.
+///     Use <see cref="AdvanceStage"/> to increment the stage of the timer (e.g. in
+///     your timer callback code).
+/// </summary>
+public class SmoothAcceleratingTimeout : Timeout
+{
+    /// <summary>
+    ///     Creates a new instance of the smooth acceleration timeout.
+    /// </summary>
+    /// <param name="initialDelay">Delay before first tick, the longest it will ever take</param>
+    /// <param name="minDelay">The fastest the timer can get no matter how long it runs</param>
+    /// <param name="decayFactor">Controls how fast the timer accelerates</param>
+    /// <param name="callback">Method to call when timer ticks</param>
+    public SmoothAcceleratingTimeout (TimeSpan initialDelay, TimeSpan minDelay, double decayFactor, Func<bool> callback)
+    {
+        _initialDelay = initialDelay;
+        _minDelay = minDelay;
+        _decayFactor = decayFactor;
+        Callback = callback;
+    }
+
+    private readonly TimeSpan _initialDelay;
+    private readonly TimeSpan _minDelay;
+    private readonly double _decayFactor;
+    private int _stage;
+
+    /// <summary>
+    ///     Advances the timer stage, this should be called from your timer callback or whenever
+    ///     you want to advance the speed.
+    /// </summary>
+    public void AdvanceStage () { _stage++; }
+
+    /// <summary>
+    ///     Resets the timer to original speed.
+    /// </summary>
+    public void Reset () { _stage = 0; }
+
+    /// <inheritdoc/>
+    public override TimeSpan Span
+    {
+        get
+        {
+            double initialMs = _initialDelay.TotalMilliseconds;
+            double minMs = _minDelay.TotalMilliseconds;
+            double delayMs = minMs + (initialMs - minMs) * Math.Pow (_decayFactor, _stage);
+
+            return TimeSpan.FromMilliseconds (delayMs);
+        }
+    }
+}

+ 105 - 169
Terminal.Gui/App/TimedEvents.cs → Terminal.Gui/App/Timeout/TimedEvents.cs

@@ -1,137 +1,163 @@
 #nullable enable
-using System.Collections.ObjectModel;
-
 namespace Terminal.Gui.App;
 
 /// <summary>
-/// Handles timeouts and idles
+///     Manages scheduled timeouts (timed callbacks) for the application.
+///     <para>
+///         Allows scheduling of callbacks to be invoked after a specified delay, with optional repetition.
+///         Timeouts are stored in a sorted list by their scheduled execution time (UTC ticks).
+///         Thread-safe for concurrent access.
+///     </para>
+///     <para>
+///         Typical usage:
+///         <list type="number">
+///             <item>
+///                 <description>Call <see cref="Add(TimeSpan, Func{bool})"/> to schedule a callback.</description>
+///             </item>
+///             <item>
+///                 <description>
+///                     Call <see cref="RunTimers"/> periodically (e.g., from the main loop) to execute due
+///                     callbacks.
+///                 </description>
+///             </item>
+///             <item>
+///                 <description>Call <see cref="Remove"/> to cancel a scheduled timeout.</description>
+///             </item>
+///         </list>
+///     </para>
 /// </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;
-
+    public event EventHandler<TimeoutEventArgs>? Added;
 
-    private void AddTimeout (TimeSpan time, Timeout timeout)
+    /// <inheritdoc/>
+    public void RunTimers ()
     {
         lock (_timeoutsLockToken)
         {
-            long k = (DateTime.UtcNow + time).Ticks;
-            _timeouts.Add (NudgeToUniqueKey (k), timeout);
-            TimeoutAdded?.Invoke (this, new TimeoutEventArgs (timeout, k));
+            if (_timeouts.Count > 0)
+            {
+                RunTimersImpl ();
+            }
         }
     }
 
-    /// <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)
+    /// <inheritdoc/>
+    public bool Remove (object token)
     {
         lock (_timeoutsLockToken)
         {
-            while (_timeouts.ContainsKey (k))
+            int idx = _timeouts.IndexOfValue ((token as Timeout)!);
+
+            if (idx == -1)
             {
-                k++;
+                return false;
             }
+
+            _timeouts.RemoveAt (idx);
         }
 
-        return k;
+        return true;
     }
 
+    /// <inheritdoc/>
+    public object Add (TimeSpan time, Func<bool> callback)
+    {
+        ArgumentNullException.ThrowIfNull (callback);
+
+        var timeout = new Timeout { Span = time, Callback = callback };
+        AddTimeout (time, timeout);
 
-    // 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 ()
+        return timeout;
+    }
+
+    /// <inheritdoc/>
+    public object Add (Timeout timeout)
     {
-        Func<bool> [] iterate;
-        lock (_idleHandlersLock)
-        {
-            iterate = _idleHandlers.ToArray ();
-            _idleHandlers = new List<Func<bool>> ();
-        }
+        AddTimeout (timeout.Span, timeout);
 
-        foreach (Func<bool> idle in iterate)
+        return timeout;
+    }
+
+    /// <inheritdoc/>
+    public bool CheckTimers (out int waitTimeout)
+    {
+        long now = DateTime.UtcNow.Ticks;
+
+        waitTimeout = 0;
+
+        lock (_timeoutsLockToken)
         {
-            if (idle ())
+            if (_timeouts.Count > 0)
             {
-                lock (_idleHandlersLock)
+                waitTimeout = (int)((_timeouts.Keys [0] - now) / TimeSpan.TicksPerMillisecond);
+
+                if (waitTimeout < 0)
                 {
-                    _idleHandlers.Add (idle);
+                    // 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;
         }
+
+        return false;
     }
 
-    /// <inheritdoc/>
-    public void LockAndRunTimers ()
+    private void AddTimeout (TimeSpan time, Timeout timeout)
     {
         lock (_timeoutsLockToken)
         {
-            if (_timeouts.Count > 0)
+            long k = (DateTime.UtcNow + time).Ticks;
+
+            // if user wants to run as soon as possible set timer such that it expires right away (no race conditions)
+            if (time == TimeSpan.Zero)
             {
-                RunTimers ();
+                k -= 100;
             }
-        }
 
+            _timeouts.Add (NudgeToUniqueKey (k), timeout);
+            Added?.Invoke (this, new (timeout, k));
+        }
     }
 
-    /// <inheritdoc/>
-    public void LockAndRunIdles ()
+    /// <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)
     {
-        bool runIdle;
-
-        lock (_idleHandlersLock)
+        lock (_timeoutsLockToken)
         {
-            runIdle = _idleHandlers.Count > 0;
+            while (_timeouts.ContainsKey (k))
+            {
+                k++;
+            }
         }
 
-        if (runIdle)
-        {
-            RunIdle ();
-        }
+        return k;
     }
-    private void RunTimers ()
+
+    private void RunTimersImpl ()
     {
         long now = DateTime.UtcNow.Ticks;
         SortedList<long, Timeout> copy;
@@ -143,7 +169,7 @@ public class TimedEvents : ITimedEvents
         lock (_timeoutsLockToken)
         {
             copy = _timeouts;
-            _timeouts = new SortedList<long, Timeout> ();
+            _timeouts = new ();
         }
 
         foreach ((long k, Timeout timeout) in copy)
@@ -164,94 +190,4 @@ public class TimedEvents : ITimedEvents
             }
         }
     }
-
-    /// <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;
-        }
-    }
-}
+}

+ 33 - 0
Terminal.Gui/App/Timeout/Timeout.cs

@@ -0,0 +1,33 @@
+namespace Terminal.Gui.App;
+
+/// <summary>
+///     Represents a scheduled timeout for use with timer management APIs.
+///     <para>
+///         Encapsulates a callback function to be invoked after a specified time interval. The callback can optionally
+///         indicate whether the timeout should repeat.
+///     </para>
+///     <para>
+///         Used by <see cref="ITimedEvents"/> and related timer systems to manage timed operations in the application.
+///     </para>
+/// </summary>
+public class Timeout
+{
+    /// <summary>
+    ///     Gets or sets the function to invoke when the timeout expires.
+    /// </summary>
+    /// <value>
+    ///     A <see cref="Func{Boolean}"/> delegate. If the callback returns <see langword="true"/>, the timeout will be
+    ///     rescheduled and invoked again after the same interval.
+    ///     If the callback returns <see langword="false"/>, the timeout will be removed and not invoked again.
+    /// </value>
+    public Func<bool> Callback { get; set; }
+
+    /// <summary>
+    ///     Gets or sets the time interval to wait before invoking the <see cref="Callback"/>.
+    /// </summary>
+    /// <value>
+    ///     A <see cref="TimeSpan"/> representing the delay before the callback is invoked. If the timeout is rescheduled
+    ///     (i.e., <see cref="Callback"/> returns <see langword="true"/>), this interval is used again.
+    /// </value>
+    public virtual TimeSpan Span { get; set; }
+}

+ 1 - 1
Terminal.Gui/App/TimeoutEventArgs.cs → Terminal.Gui/App/Timeout/TimeoutEventArgs.cs

@@ -1,6 +1,6 @@
 namespace Terminal.Gui.App;
 
-/// <summary><see cref="EventArgs"/> for timeout events (e.g. <see cref="TimedEvents.TimeoutAdded"/>)</summary>
+/// <summary><see cref="EventArgs"/> for timeout events (e.g. <see cref="TimedEvents.Added"/>)</summary>
 public class TimeoutEventArgs : EventArgs
 {
     /// <summary>Creates a new instance of the <see cref="TimeoutEventArgs"/> class.</summary>

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

@@ -13,7 +13,7 @@ namespace Terminal.Gui.Configuration;
 
 /// <summary>
 ///     Provides settings and configuration management for Terminal.Gui applications. See the Configuration Deep Dive for
-///     more information: <see href="https://gui-cs.github.io/Terminal.GuiV2Docs/docs/config.html"/>.
+///     more information: <see href="https://gui-cs.github.io/Terminal.Gui/docs/config.html"/>.
 ///     <para>
 ///         Users can set Terminal.Gui settings on a global or per-application basis by providing JSON formatted
 ///         configuration files. The configuration files can be placed in at <c>.tui</c> folder in the user's home
@@ -23,7 +23,7 @@ namespace Terminal.Gui.Configuration;
 ///     </para>
 ///     <para>
 ///         Settings are defined in JSON format, according to this schema:
-///         https://gui-cs.github.io/Terminal.GuiV2Docs/schemas/tui-config-schema.json
+///         https://gui-cs.github.io/Terminal.Gui/schemas/tui-config-schema.json
 ///     </para>
 ///     <para>
 ///         Settings that will apply to all applications (global settings) reside in files named <c>config.json</c>.

+ 27 - 5
Terminal.Gui/Configuration/DeepCloner.cs

@@ -278,17 +278,37 @@ public static class DeepCloner
 
         // Determine dictionary type and comparer
         Type [] genericArgs = type.GetGenericArguments ();
-        Type dictType = genericArgs.Length == 2
-            ? typeof (Dictionary<,>).MakeGenericType (genericArgs)
-            : typeof (Dictionary<object, object>);
+        Type dictType;
+
+        if (genericArgs.Length == 2)
+        {
+            if (type.GetGenericTypeDefinition () == typeof (Dictionary<,>))
+            {
+                dictType = typeof (Dictionary<,>).MakeGenericType (genericArgs);
+            }
+            else if (type.GetGenericTypeDefinition () == typeof (ConcurrentDictionary<,>))
+            {
+                dictType = typeof (ConcurrentDictionary<,>).MakeGenericType (genericArgs);
+            }
+            else
+            {
+                throw new InvalidOperationException (
+                                                     $"Unsupported dictionary type: {type}. Only Dictionary<,> and ConcurrentDictionary<,> are supported.");
+            }
+        }
+        else
+        {
+            dictType = typeof (Dictionary<object, object>);
+        }
+
         object? comparer = type.GetProperty ("Comparer")?.GetValue (source);
 
         // Create a temporary dictionary to hold cloned key-value pairs
         IDictionary tempDict = CreateDictionaryInstance (dictType, comparer);
         visited.TryAdd (source, tempDict);
 
-
         object? lastKey = null;
+
         try
         {
             // Clone all key-value pairs
@@ -311,7 +331,9 @@ public static class DeepCloner
         catch (InvalidOperationException ex)
         {
             // Handle cases where the dictionary is modified during enumeration
-            throw new InvalidOperationException ($"Error cloning dictionary ({source}) (last key was \"{lastKey}\"). Ensure the source dictionary is not modified during cloning.", ex);
+            throw new InvalidOperationException (
+                                                 $"Error cloning dictionary ({source}) (last key was \"{lastKey}\"). Ensure the source dictionary is not modified during cloning.",
+                                                 ex);
         }
 
         // If the original dictionary type has a parameterless constructor, create a new instance

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

@@ -12,7 +12,7 @@ namespace Terminal.Gui.Configuration;
 /// <example>
 ///     <code>
 ///  {
-///    "$schema" : "https://gui-cs.github.io/Terminal.GuiV2Docs/schemas/tui-config-schema.json",
+///    "$schema" : "https://gui-cs.github.io/Terminal.Gui/schemas/tui-config-schema.json",
 ///    "Application.UseSystemConsole" : true,
 ///    "Theme" : "Default",
 ///    "Themes": {
@@ -42,5 +42,5 @@ public class SettingsScope : Scope<SettingsScope>
     /// <summary>Points to our JSON schema.</summary>
     [JsonInclude]
     [JsonPropertyName ("$schema")]
-    public string Schema { get; set; } = "https://gui-cs.github.io/Terminal.GuiV2Docs/schemas/tui-config-schema.json";
+    public string Schema { get; set; } = "https://gui-cs.github.io/Terminal.Gui/schemas/tui-config-schema.json";
 }

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

@@ -22,7 +22,7 @@ namespace Terminal.Gui.Drawing;
 ///         Use <see cref="SchemeManager"/> to manage available schemes and apply them to views.
 ///     </para>
 ///     <para>
-///         See <see href="https://gui-cs.github.io/Terminal.GuiV2Docs/docs/drawing.html"/> for more information.
+///         See <see href="https://gui-cs.github.io/Terminal.Gui/docs/drawing.html"/> for more information.
 ///     </para>
 /// </summary>
 /// <remarks>

+ 2 - 2
Terminal.Gui/Drawing/Sixel/SixelSupportDetector.cs

@@ -20,7 +20,7 @@ public class SixelSupportDetector
     public void Detect (Action<SixelSupportResult> resultCallback)
     {
         var result = new SixelSupportResult ();
-        result.SupportsTransparency = IsWindowsTerminal () || IsXtermWithTransparency ();
+        result.SupportsTransparency = IsVirtualTerminal () || IsXtermWithTransparency ();
         IsSixelSupportedByDar (result, resultCallback);
     }
 
@@ -142,7 +142,7 @@ public class SixelSupportDetector
 
     private static bool ResponseIndicatesSupport (string response) { return response.Split (';').Contains ("4"); }
 
-    private static bool IsWindowsTerminal ()
+    private static bool IsVirtualTerminal ()
     {
         return !string.IsNullOrWhiteSpace (Environment.GetEnvironmentVariable ("WT_SESSION"));
 

+ 9 - 3
Terminal.Gui/Drivers/AnsiResponseParser/Keyboard/CsiCursorPattern.cs

@@ -7,11 +7,11 @@ namespace Terminal.Gui.Drivers;
 ///     Detects ansi escape sequences in strings that have been read from
 ///     the terminal (see <see cref="IAnsiResponseParser"/>).
 ///     Handles navigation CSI key parsing such as <c>\x1b[A</c> (Cursor up)
-///     and <c>\x1b[1;5A</c> (Cursor up with Ctrl)
+///     and <c>\x1b[1;5A</c> (Cursor/Function with modifier(s))
 /// </summary>
 public class CsiCursorPattern : AnsiKeyboardParserPattern
 {
-    private readonly Regex _pattern = new (@"^\u001b\[(?:1;(\d+))?([A-DHF])$");
+    private readonly Regex _pattern = new (@"^\u001b\[(?:1;(\d+))?([A-DFHPQRS])$");
 
     private readonly Dictionary<char, Key> _cursorMap = new ()
     {
@@ -20,7 +20,13 @@ public class CsiCursorPattern : AnsiKeyboardParserPattern
         { 'C', Key.CursorRight },
         { 'D', Key.CursorLeft },
         { 'H', Key.Home },
-        { 'F', Key.End }
+        { 'F', Key.End },
+
+        // F1–F4 as per xterm VT100-style CSI sequences
+        { 'P', Key.F1 },
+        { 'Q', Key.F2 },
+        { 'R', Key.F3 },
+        { 'S', Key.F4 }
     };
 
     /// <inheritdoc/>

+ 9 - 2
Terminal.Gui/Drivers/AnsiResponseParser/Keyboard/EscAsAltPattern.cs

@@ -8,7 +8,7 @@ internal class EscAsAltPattern : AnsiKeyboardParserPattern
     public EscAsAltPattern () { IsLastMinute = true; }
 
 #pragma warning disable IDE1006 // Naming Styles
-    private static readonly Regex _pattern = new (@"^\u001b([a-zA-Z0-9_])$");
+    private static readonly Regex _pattern = new (@"^\u001b([\u0001-\u001a\u001fa-zA-Z0-9_])$");
 #pragma warning restore IDE1006 // Naming Styles
 
     public override bool IsMatch (string? input) { return _pattern.IsMatch (input!); }
@@ -22,7 +22,14 @@ internal class EscAsAltPattern : AnsiKeyboardParserPattern
             return null;
         }
 
-        char key = match.Groups [1].Value [0];
+        char ch = match.Groups [1].Value [0];
+
+        Key key = ch switch
+                  {
+                      >= '\u0001' and <= '\u001a' => ((Key)(ch + 96)).WithCtrl,
+                      '\u001f' => Key.D7.WithCtrl.WithShift,
+                      _ => ch
+                  };
 
         return new Key (key).WithAlt;
     }

+ 8 - 1
Terminal.Gui/Drivers/AnsiResponseParser/Keyboard/Ss3Pattern.cs

@@ -10,7 +10,7 @@ namespace Terminal.Gui.Drivers;
 public class Ss3Pattern : AnsiKeyboardParserPattern
 {
 #pragma warning disable IDE1006 // Naming Styles
-    private static readonly Regex _pattern = new (@"^\u001bO([PQRStDCAB])$");
+    private static readonly Regex _pattern = new (@"^\u001bO([PQRStDCABOHFwqysu])$");
 #pragma warning restore IDE1006 // Naming Styles
 
     /// <inheritdoc/>
@@ -41,6 +41,13 @@ public class Ss3Pattern : AnsiKeyboardParserPattern
                    'C' => Key.CursorRight,
                    'A' => Key.CursorUp,
                    'B' => Key.CursorDown,
+                   'H' => Key.Home,
+                   'F' => Key.End,
+                   'w' => Key.Home,
+                   'q' => Key.End,
+                   'y' => Key.PageUp,
+                   's' => Key.PageDown,
+                   'u' => Key.Clear,
                    _ => null
                };
     }

+ 18 - 12
Terminal.Gui/Drivers/ConsoleDriver.cs

@@ -269,7 +269,7 @@ public abstract class ConsoleDriver : IConsoleDriver
                         if (Contents [Row, Col - 1].Rune.GetColumns () > 1)
                         {
                             // Invalidate cell to left
-                            Contents [Row, Col - 1].Rune = Rune.ReplacementChar;
+                            Contents [Row, Col - 1].Rune = (Rune)'\0';
                             Contents [Row, Col - 1].IsDirty = true;
                         }
                     }
@@ -308,7 +308,7 @@ public abstract class ConsoleDriver : IConsoleDriver
                             {
                                 // 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].Rune = (Rune)'\0';
                                 Contents [Row, Col + 1].IsDirty = true;
                             }
                         }
@@ -681,16 +681,6 @@ public abstract class ConsoleDriver : IConsoleDriver
     /// <param name="a"></param>
     public void OnKeyUp (Key a) { KeyUp?.Invoke (this, a); }
 
-    // TODO: Remove this API - it was needed when we didn't have a reliable way to simulate key presses.
-    // TODO: We now do: Application.RaiseKeyDown and Application.RaiseKeyUp
-    /// <summary>Simulates a key press.</summary>
-    /// <param name="keyChar">The key character.</param>
-    /// <param name="key">The key.</param>
-    /// <param name="shift">If <see langword="true"/> simulates the Shift key being pressed.</param>
-    /// <param name="alt">If <see langword="true"/> simulates the Alt key being pressed.</param>
-    /// <param name="ctrl">If <see langword="true"/> simulates the Ctrl key being pressed.</param>
-    public abstract void SendKeys (char keyChar, ConsoleKey key, bool shift, bool alt, bool ctrl);
-
     internal char _highSurrogate = '\0';
 
     internal bool IsValidInput (KeyCode keyCode, out KeyCode result)
@@ -707,6 +697,22 @@ public abstract class ConsoleDriver : IConsoleDriver
         if (_highSurrogate > 0 && char.IsLowSurrogate ((char)keyCode))
         {
             result = (KeyCode)new Rune (_highSurrogate, (char)keyCode).Value;
+
+            if ((keyCode & KeyCode.AltMask) != 0)
+            {
+                result |= KeyCode.AltMask;
+            }
+
+            if ((keyCode & KeyCode.CtrlMask) != 0)
+            {
+                result |= KeyCode.CtrlMask;
+            }
+
+            if ((keyCode & KeyCode.ShiftMask) != 0)
+            {
+                result |= KeyCode.ShiftMask;
+            }
+
             _highSurrogate = '\0';
 
             return true;

+ 122 - 336
Terminal.Gui/Drivers/CursesDriver/CursesDriver.cs

@@ -61,44 +61,6 @@ internal class CursesDriver : ConsoleDriver
         }
     }
 
-    public override void SendKeys (char keyChar, ConsoleKey consoleKey, bool shift, bool alt, bool control)
-    {
-        KeyCode key;
-
-        if (consoleKey == ConsoleKey.Packet)
-        {
-            //var mod = new ConsoleModifiers ();
-
-            //if (shift)
-            //{
-            //    mod |= ConsoleModifiers.Shift;
-            //}
-
-            //if (alt)
-            //{
-            //    mod |= ConsoleModifiers.Alt;
-            //}
-
-            //if (control)
-            //{
-            //    mod |= ConsoleModifiers.Control;
-            //}
-
-            var cKeyInfo = new ConsoleKeyInfo (keyChar, consoleKey, shift, alt, control);
-            cKeyInfo = ConsoleKeyMapping.DecodeVKPacketToKConsoleKeyInfo (cKeyInfo);
-            key = ConsoleKeyMapping.MapConsoleKeyInfoToKeyCode (cKeyInfo);
-        }
-        else
-        {
-            key = (KeyCode)keyChar;
-        }
-
-        OnKeyDown (new (key));
-        OnKeyUp (new (key));
-
-        //OnKeyPressed (new KeyEventArgsEventArgs (key));
-    }
-
     public void StartReportingMouseMoves ()
     {
         if (!RunningUnitTests)
@@ -123,12 +85,6 @@ internal class CursesDriver : ConsoleDriver
         if (!RunningUnitTests)
         {
             Platform.Suspend ();
-
-            if (Force16Colors)
-            {
-                Curses.Window.Standard.redrawwin ();
-                Curses.refresh ();
-            }
         }
 
         StartReportingMouseMoves ();
@@ -140,164 +96,98 @@ internal class CursesDriver : ConsoleDriver
 
         if (!RunningUnitTests && Col >= 0 && Col < Cols && Row >= 0 && Row < Rows)
         {
-            if (Force16Colors)
-            {
-                Curses.move (Row, Col);
-
-                Curses.raw ();
-                Curses.noecho ();
-                Curses.refresh ();
-            }
-            else
-            {
-                _mainLoopDriver?.WriteRaw (EscSeqUtils.CSI_SetCursorPosition (Row + 1, Col + 1));
-            }
+            _mainLoopDriver?.WriteRaw (EscSeqUtils.CSI_SetCursorPosition (Row + 1, Col + 1));
         }
     }
 
     public override bool UpdateScreen ()
     {
         bool updated = false;
-        if (Force16Colors)
+        if (RunningUnitTests
+            || Console.WindowHeight < 1
+            || Contents?.Length != Rows * Cols
+            || Rows != Console.WindowHeight)
         {
-            for (var row = 0; row < Rows; row++)
-            {
-                if (!_dirtyLines! [row])
-                {
-                    continue;
-                }
-
-                _dirtyLines [row] = false;
-
-                for (var col = 0; col < Cols; col++)
-                {
-                    if (Contents! [row, col].IsDirty == false)
-                    {
-                        continue;
-                    }
-
-                    if (RunningUnitTests)
-                    {
-                        // In unit tests, we don't want to actually write to the screen.
-                        continue;
-                    }
-
-                    Curses.attrset (Contents [row, col].Attribute.GetValueOrDefault ().PlatformColor);
+            return updated;
+        }
 
-                    Rune rune = Contents [row, col].Rune;
+        var top = 0;
+        var left = 0;
+        int rows = Rows;
+        int cols = Cols;
+        var output = new StringBuilder ();
+        Attribute? redrawAttr = null;
+        int lastCol = -1;
 
-                    if (rune.IsBmp)
-                    {
-                        // BUGBUG: CursesDriver doesn't render CharMap correctly for wide chars (and other Unicode) - Curses is doing something funky with glyphs that report GetColums() of 1 yet are rendered wide. E.g. 0x2064 (invisible times) is reported as 1 column but is rendered as 2. WindowsDriver & NetDriver correctly render this as 1 column, overlapping the next cell.
-                        if (rune.GetColumns () < 2)
-                        {
-                            Curses.mvaddch (row, col, rune.Value);
-                        }
-                        else /*if (col + 1 < Cols)*/
-                        {
-                            Curses.mvaddwstr (row, col, rune.ToString ());
-                        }
-                    }
-                    else
-                    {
-                        Curses.mvaddwstr (row, col, rune.ToString ());
+        CursorVisibility? savedVisibility = _currentCursorVisibility;
+        SetCursorVisibility (CursorVisibility.Invisible);
 
-                        if (rune.GetColumns () > 1 && col + 1 < Cols)
-                        {
-                            // TODO: This is a hack to deal with non-BMP and wide characters.
-                            //col++;
-                            Curses.mvaddch (row, ++col, '*');
-                        }
-                    }
-                }
+        for (int row = top; row < rows; row++)
+        {
+            if (Console.WindowHeight < 1)
+            {
+                return updated;
             }
 
-            if (!RunningUnitTests)
+            if (!_dirtyLines! [row])
             {
-                Curses.move (Row, Col);
-                _window?.wrefresh ();
+                continue;
             }
-        }
-        else
-        {
-            if (RunningUnitTests
-                || Console.WindowHeight < 1
-                || Contents!.Length != Rows * Cols
-                || Rows != Console.WindowHeight)
+
+            if (!SetCursorPosition (0, row))
             {
                 return updated;
             }
 
-            var top = 0;
-            var left = 0;
-            int rows = Rows;
-            int cols = Cols;
-            var output = new StringBuilder ();
-            Attribute? redrawAttr = null;
-            int lastCol = -1;
-
-            CursorVisibility? savedVisibility = _currentCursorVisibility;
-            SetCursorVisibility (CursorVisibility.Invisible);
+            updated = true;
+            _dirtyLines [row] = false;
+            output.Clear ();
 
-            for (int row = top; row < rows; row++)
+            for (int col = left; col < cols; col++)
             {
-                if (Console.WindowHeight < 1)
-                {
-                    return updated;
-                }
-
-                if (!_dirtyLines! [row])
-                {
-                    continue;
-                }
+                lastCol = -1;
+                var outputWidth = 0;
 
-                if (!SetCursorPosition (0, row))
+                for (; col < cols; col++)
                 {
-                    return updated;
-                }
-
-                _dirtyLines [row] = false;
-                output.Clear ();
-
-                for (int col = left; col < cols; col++)
-                {
-                    lastCol = -1;
-                    var outputWidth = 0;
-
-                    for (; col < cols; col++)
+                    if (!Contents [row, col].IsDirty)
                     {
-                        updated = true;
-                        if (!Contents [row, col].IsDirty)
+                        if (output.Length > 0)
                         {
-                            if (output.Length > 0)
-                            {
-                                WriteToConsole (output, ref lastCol, row, ref outputWidth);
-                            }
-                            else if (lastCol == -1)
-                            {
-                                lastCol = col;
-                            }
-
-                            if (lastCol + 1 < cols)
-                            {
-                                lastCol++;
-                            }
-
-                            continue;
+                            WriteToConsole (output, ref lastCol, row, ref outputWidth);
                         }
-
-                        if (lastCol == -1)
+                        else if (lastCol == -1)
                         {
                             lastCol = col;
                         }
 
-                        Attribute attr = Contents [row, col].Attribute!.Value;
-
-                        // Performance: Only send the escape sequence if the attribute has changed.
-                        if (attr != redrawAttr)
+                        if (lastCol + 1 < cols)
                         {
-                            redrawAttr = attr;
+                            lastCol++;
+                        }
+
+                        continue;
+                    }
+
+                    if (lastCol == -1)
+                    {
+                        lastCol = col;
+                    }
 
+                    Attribute attr = Contents [row, col].Attribute!.Value;
+
+                    // Performance: Only send the escape sequence if the attribute has changed.
+                    if (attr != redrawAttr)
+                    {
+                        redrawAttr = attr;
+
+                        if (Force16Colors)
+                        {
+                            output.Append (EscSeqUtils.CSI_SetForegroundColor (attr.Foreground.GetAnsiColorCode ()));
+                            output.Append (EscSeqUtils.CSI_SetBackgroundColor (attr.Background.GetAnsiColorCode ()));
+                        }
+                        else
+                        {
                             output.Append (
                                            EscSeqUtils.CSI_SetForegroundColorRGB (
                                                                                   attr.Foreground.R,
@@ -314,62 +204,64 @@ internal class CursesDriver : ConsoleDriver
                                                                                  )
                                           );
                         }
+                    }
 
-                        outputWidth++;
-                        Rune rune = Contents [row, col].Rune;
-                        output.Append (rune);
-
-                        if (Contents [row, col].CombiningMarks.Count > 0)
-                        {
-                            // AtlasEngine does not support NON-NORMALIZED combining marks in a way
-                            // compatible with the driver architecture. Any CMs (except in the first col)
-                            // are correctly combined with the base char, but are ALSO treated as 1 column
-                            // width codepoints E.g. `echo "[e`u{0301}`u{0301}]"` will output `[é  ]`.
-                            // 
-                            // For now, we just ignore the list of CMs.
-                            //foreach (var combMark in Contents [row, col].CombiningMarks) {
-                            //	output.Append (combMark);
-                            //}
-                            // WriteToConsole (output, ref lastCol, row, ref outputWidth);
-                        }
-                        else if (rune.IsSurrogatePair () && rune.GetColumns () < 2)
-                        {
-                            WriteToConsole (output, ref lastCol, row, ref outputWidth);
-                            SetCursorPosition (col - 1, row);
-                        }
+                    outputWidth++;
+                    Rune rune = Contents [row, col].Rune;
+                    output.Append (rune);
 
-                        Contents [row, col].IsDirty = false;
+                    if (Contents [row, col].CombiningMarks.Count > 0)
+                    {
+                        // AtlasEngine does not support NON-NORMALIZED combining marks in a way
+                        // compatible with the driver architecture. Any CMs (except in the first col)
+                        // are correctly combined with the base char, but are ALSO treated as 1 column
+                        // width codepoints E.g. `echo "[e`u{0301}`u{0301}]"` will output `[é  ]`.
+                        // 
+                        // For now, we just ignore the list of CMs.
+                        //foreach (var combMark in Contents [row, col].CombiningMarks) {
+                        //	output.Append (combMark);
+                        //}
+                        // WriteToConsole (output, ref lastCol, row, ref outputWidth);
+                    }
+                    else if (rune.IsSurrogatePair () && rune.GetColumns () < 2)
+                    {
+                        WriteToConsole (output, ref lastCol, row, ref outputWidth);
+                        SetCursorPosition (col - 1, row);
                     }
-                }
 
-                if (output.Length > 0)
-                {
-                    SetCursorPosition (lastCol, row);
-                    Console.Write (output);
+                    Contents [row, col].IsDirty = false;
                 }
             }
 
-            // SIXELS
-            foreach (SixelToRender s in Application.Sixel)
+            if (output.Length > 0)
             {
-                SetCursorPosition (s.ScreenPosition.X, s.ScreenPosition.Y);
-                Console.Write (s.SixelData);
+                SetCursorPosition (lastCol, row);
+                Console.Write (output);
             }
 
-            SetCursorPosition (0, 0);
-
-            _currentCursorVisibility = savedVisibility;
-
-            void WriteToConsole (StringBuilder output, ref int lastCol, int row, ref int outputWidth)
+            foreach (var s in Application.Sixel)
             {
-                SetCursorPosition (lastCol, row);
-                Console.Write (output);
-                output.Clear ();
-                lastCol += outputWidth;
-                outputWidth = 0;
+                if (!string.IsNullOrWhiteSpace (s.SixelData))
+                {
+                    SetCursorPosition (s.ScreenPosition.X, s.ScreenPosition.Y);
+                    Console.Write (s.SixelData);
+                }
             }
         }
 
+        SetCursorPosition (0, 0);
+
+        _currentCursorVisibility = savedVisibility;
+
+        void WriteToConsole (StringBuilder output, ref int lastCol, int row, ref int outputWidth)
+        {
+            SetCursorPosition (lastCol, row);
+            Console.Write (output);
+            output.Clear ();
+            lastCol += outputWidth;
+            outputWidth = 0;
+        }
+
         return updated;
     }
 
@@ -396,29 +288,6 @@ internal class CursesDriver : ConsoleDriver
                    );
     }
 
-    /// <inheritdoc/>
-    /// <remarks>
-    ///     In the CursesDriver, colors are encoded as an int. The foreground color is stored in the most significant 4
-    ///     bits, and the background color is stored in the least significant 4 bits. The Terminal.GUi Color values are
-    ///     converted to curses color encoding before being encoded.
-    /// </remarks>
-    public override Attribute MakeColor (in Color foreground, in Color background)
-    {
-        if (!RunningUnitTests && Force16Colors)
-        {
-            return MakeColor (
-                              ColorNameToCursesColorNumber (foreground.GetClosestNamedColor16 ()),
-                              ColorNameToCursesColorNumber (background.GetClosestNamedColor16 ())
-                             );
-        }
-
-        return new (
-                    0,
-                    foreground,
-                    background
-                   );
-    }
-
     private static short ColorNameToCursesColorNumber (ColorName16 color)
     {
         switch (color)
@@ -536,6 +405,8 @@ internal class CursesDriver : ConsoleDriver
         return true;
     }
 
+    private EscSeqUtils.DECSCUSR_Style? _currentDecscusrStyle;
+
     /// <inheritdoc/>
     public override bool SetCursorVisibility (CursorVisibility visibility)
     {
@@ -547,17 +418,19 @@ internal class CursesDriver : ConsoleDriver
         if (!RunningUnitTests)
         {
             Curses.curs_set (((int)visibility >> 16) & 0x000000FF);
+            Curses.leaveok (_window!.Handle, !Force16Colors);
         }
 
         if (visibility != CursorVisibility.Invisible)
         {
-            _mainLoopDriver?.WriteRaw (
-                                       EscSeqUtils.CSI_SetCursorStyle (
-                                                                       (EscSeqUtils.DECSCUSR_Style)
-                                                                       (((int)visibility >> 24)
-                                                                        & 0xFF)
-                                                                      )
-                                      );
+            if (_currentDecscusrStyle is null || _currentDecscusrStyle != (EscSeqUtils.DECSCUSR_Style)(((int)visibility >> 24) & 0xFF))
+            {
+                _currentDecscusrStyle = (EscSeqUtils.DECSCUSR_Style)(((int)visibility >> 24) & 0xFF);
+
+                _mainLoopDriver?.WriteRaw (
+                                           EscSeqUtils.CSI_SetCursorStyle ((EscSeqUtils.DECSCUSR_Style)_currentDecscusrStyle)
+                                          );
+            }
         }
 
         _currentCursorVisibility = visibility;
@@ -727,8 +600,7 @@ internal class CursesDriver : ConsoleDriver
 
                 while (wch2 == Curses.KeyMouse)
                 {
-                    // BUGBUG: Fix this nullable issue.
-                    Key kea = null;
+                    Key? kea = null;
 
                     ConsoleKeyInfo [] cki =
                     {
@@ -737,8 +609,7 @@ internal class CursesDriver : ConsoleDriver
                         new ('<', 0, false, false, false)
                     };
                     code = 0;
-                    // BUGBUG: Fix this nullable issue.
-                    HandleEscSeqResponse (ref code, ref k, ref wch2, ref kea, ref cki);
+                    HandleEscSeqResponse (ref code, ref k, ref wch2, ref kea!, ref cki!);
                 }
 
                 return;
@@ -799,8 +670,7 @@ internal class CursesDriver : ConsoleDriver
                 k = KeyCode.AltMask | MapCursesKey (wch);
             }
 
-            // BUGBUG: Fix this nullable issue.
-            Key key = null;
+            Key? key = null;
 
             if (code == 0)
             {
@@ -830,8 +700,7 @@ internal class CursesDriver : ConsoleDriver
                     [
                         new ((char)KeyCode.Esc, 0, false, false, false), new ((char)wch2, 0, false, false, false)
                     ];
-                    // BUGBUG: Fix this nullable issue.
-                    HandleEscSeqResponse (ref code, ref k, ref wch2, ref key, ref cki);
+                    HandleEscSeqResponse (ref code, ref k, ref wch2, ref key!, ref cki!);
 
                     return;
                 }
@@ -964,7 +833,7 @@ internal class CursesDriver : ConsoleDriver
         ref KeyCode k,
         ref int wch2,
         ref Key keyEventArgs,
-        ref ConsoleKeyInfo [] cki
+        ref ConsoleKeyInfo []? cki
     )
     {
         ConsoleKey ck = 0;
@@ -988,11 +857,10 @@ internal class CursesDriver : ConsoleDriver
                 // the given terminator (e.g. mouse) or did not understand format somehow.
                 // Carry on with the older code for processing curses escape codes
 
-                // BUGBUG: Fix this nullable issue.
                 EscSeqUtils.DecodeEscSeq (
                                           ref consoleKeyInfo,
                                           ref ck,
-                                          cki,
+                                          cki!,
                                           ref mod,
                                           out _,
                                           out _,
@@ -1012,7 +880,6 @@ internal class CursesDriver : ConsoleDriver
                         OnMouseEvent (new () { Flags = mf, Position = pos });
                     }
 
-                    // BUGBUG: Fix this nullable issue.
                     cki = null;
 
                     if (wch2 == 27)
@@ -1171,84 +1038,3 @@ internal class CursesDriver : ConsoleDriver
     /// <inheritdoc/>
     public override void WriteRaw (string ansi) { _mainLoopDriver?.WriteRaw (ansi); }
 }
-
-// TODO: One type per file - move to another file
-internal static class Platform
-{
-    private static int _suspendSignal;
-
-    /// <summary>Suspends the process by sending SIGTSTP to itself</summary>
-    /// <returns>True if the suspension was successful.</returns>
-    public static bool Suspend ()
-    {
-        int signal = GetSuspendSignal ();
-
-        if (signal == -1)
-        {
-            return false;
-        }
-
-        killpg (0, signal);
-
-        return true;
-    }
-
-    private static int GetSuspendSignal ()
-    {
-        if (_suspendSignal != 0)
-        {
-            return _suspendSignal;
-        }
-
-        nint buf = Marshal.AllocHGlobal (8192);
-
-        if (uname (buf) != 0)
-        {
-            Marshal.FreeHGlobal (buf);
-            _suspendSignal = -1;
-
-            return _suspendSignal;
-        }
-
-        try
-        {
-            switch (Marshal.PtrToStringAnsi (buf))
-            {
-                case "Darwin":
-                case "DragonFly":
-                case "FreeBSD":
-                case "NetBSD":
-                case "OpenBSD":
-                    _suspendSignal = 18;
-
-                    break;
-                case "Linux":
-                    // TODO: should fetch the machine name and
-                    // if it is MIPS return 24
-                    _suspendSignal = 20;
-
-                    break;
-                case "Solaris":
-                    _suspendSignal = 24;
-
-                    break;
-                default:
-                    _suspendSignal = -1;
-
-                    break;
-            }
-
-            return _suspendSignal;
-        }
-        finally
-        {
-            Marshal.FreeHGlobal (buf);
-        }
-    }
-
-    [DllImport ("libc")]
-    private static extern int killpg (int pgrp, int pid);
-
-    [DllImport ("libc")]
-    private static extern int uname (nint buf);
-}

+ 83 - 0
Terminal.Gui/Drivers/CursesDriver/Platform.cs

@@ -0,0 +1,83 @@
+using System.Runtime.InteropServices;
+
+namespace Terminal.Gui.Drivers;
+
+internal static class Platform
+{
+    private static int _suspendSignal;
+
+    /// <summary>Suspends the process by sending SIGTSTP to itself</summary>
+    /// <returns>True if the suspension was successful.</returns>
+    public static bool Suspend ()
+    {
+        int signal = GetSuspendSignal ();
+
+        if (signal == -1)
+        {
+            return false;
+        }
+
+        killpg (0, signal);
+
+        return true;
+    }
+
+    private static int GetSuspendSignal ()
+    {
+        if (_suspendSignal != 0)
+        {
+            return _suspendSignal;
+        }
+
+        nint buf = Marshal.AllocHGlobal (8192);
+
+        if (uname (buf) != 0)
+        {
+            Marshal.FreeHGlobal (buf);
+            _suspendSignal = -1;
+
+            return _suspendSignal;
+        }
+
+        try
+        {
+            switch (Marshal.PtrToStringAnsi (buf))
+            {
+                case "Darwin":
+                case "DragonFly":
+                case "FreeBSD":
+                case "NetBSD":
+                case "OpenBSD":
+                    _suspendSignal = 18;
+
+                    break;
+                case "Linux":
+                    // TODO: should fetch the machine name and
+                    // if it is MIPS return 24
+                    _suspendSignal = 20;
+
+                    break;
+                case "Solaris":
+                    _suspendSignal = 24;
+
+                    break;
+                default:
+                    _suspendSignal = -1;
+
+                    break;
+            }
+
+            return _suspendSignal;
+        }
+        finally
+        {
+            Marshal.FreeHGlobal (buf);
+        }
+    }
+
+    [DllImport ("libc")]
+    private static extern int killpg (int pgrp, int pid);
+
+    [DllImport ("libc")]
+    private static extern int uname (nint buf);
+}

+ 2 - 3
Terminal.Gui/Drivers/CursesDriver/UnixMainLoop.cs

@@ -104,7 +104,7 @@ internal class UnixMainLoop : IMainLoopDriver
 
         UpdatePollMap ();
 
-        bool checkTimersResult = _mainLoop!.TimedEvents.CheckTimersAndIdleHandlers (out int pollTimeout);
+        bool checkTimersResult = _mainLoop!.TimedEvents.CheckTimers (out int pollTimeout);
 
         int n = poll (_pollMap!, (uint)_pollMap!.Length, pollTimeout);
 
@@ -249,8 +249,7 @@ internal class UnixMainLoop : IMainLoopDriver
 
     private class Watch
     {
-        // BUGBUG: Fix this nullable issue.
-        public Func<MainLoop, bool> Callback;
+        public Func<MainLoop, bool>? Callback;
         public Condition Condition;
         public int File;
     }

+ 57 - 44
Terminal.Gui/Drivers/EscSeqUtils/EscSeqUtils.cs

@@ -900,22 +900,8 @@ public static class EscSeqUtils
 
             _point = pos;
 
-            if ((mouseFlags [0] & MouseFlags.ReportMousePosition) == 0)
-            {
-                Application.MainLoop?.AddIdle (
-                                              () =>
-                                              {
-                                                  // INTENT: What's this trying to do?
-                                                  // The task itself is not awaited.
-                                                  Task.Run (
-                                                            async () => await ProcessContinuousButtonPressedAsync (
-                                                                         buttonState,
-                                                                         continuousButtonPressedHandler));
 
-                                                  return false;
-                                              });
-            }
-            else if (mouseFlags [0].HasFlag (MouseFlags.ReportMousePosition))
+            if (mouseFlags [0].HasFlag (MouseFlags.ReportMousePosition))
             {
                 _point = pos;
 
@@ -945,7 +931,7 @@ public static class EscSeqUtils
             _isButtonClicked = false;
             _isButtonDoubleClicked = true;
 
-            Application.MainLoop?.AddIdle (
+            Application.MainLoop?.TimedEvents.Add (TimeSpan.Zero,
                                           () =>
                                           {
                                               Task.Run (async () => await ProcessButtonDoubleClickedAsync ());
@@ -959,7 +945,7 @@ public static class EscSeqUtils
         //	lastMouseButtonReleased = null;
         //	isButtonReleased = false;
         //	isButtonClicked = true;
-        //	Application.MainLoop.AddIdle (() => {
+        //	Application.MainLoop.AddTimeout (() => {
         //		Task.Run (async () => await ProcessButtonClickedAsync ());
         //		return false;
         //	});
@@ -984,7 +970,7 @@ public static class EscSeqUtils
                 mouseFlags.Add (GetButtonClicked (buttonState));
                 _isButtonClicked = true;
 
-                Application.MainLoop?.AddIdle (
+                Application.MainLoop?.TimedEvents.Add (TimeSpan.Zero,
                                               () =>
                                               {
                                                   Task.Run (async () => await ProcessButtonClickedAsync ());
@@ -1043,6 +1029,16 @@ public static class EscSeqUtils
         //}
     }
 
+    /// <summary>
+    /// Helper to set the Control key states based on the char.
+    /// </summary>
+    /// <param name="ch">The char value.</param>
+    /// <returns></returns>
+    public static ConsoleKeyInfo MapChar (char ch)
+    {
+        return MapConsoleKeyInfo (new (ch, ConsoleKey.None, false, false, false));
+    }
+
     /// <summary>
     ///     Ensures a console key is mapped to one that works correctly with ANSI escape sequences.
     /// </summary>
@@ -1145,6 +1141,17 @@ public static class EscSeqUtils
                                              true);
                 }
 
+                break;
+            case uint n when n is >= '\u001c'  and <= '\u001f':
+                key = (ConsoleKey)(char)(consoleKeyInfo.KeyChar + 24);
+
+                newConsoleKeyInfo = new (
+                                         (char)key,
+                                         key,
+                                         (consoleKeyInfo.Modifiers & ConsoleModifiers.Shift) != 0,
+                                         (consoleKeyInfo.Modifiers & ConsoleModifiers.Alt) != 0,
+                                         true);
+
                 break;
             case 127: // DEL
                 key = ConsoleKey.Backspace;
@@ -1389,6 +1396,12 @@ public static class EscSeqUtils
     {
         switch (keyInfo.Key)
         {
+            case ConsoleKey.Multiply:
+            case ConsoleKey.Add:
+            case ConsoleKey.Separator:
+            case ConsoleKey.Subtract:
+            case ConsoleKey.Decimal:
+            case ConsoleKey.Divide:
             case ConsoleKey.OemPeriod:
             case ConsoleKey.OemComma:
             case ConsoleKey.OemPlus:
@@ -1405,8 +1418,31 @@ public static class EscSeqUtils
             case ConsoleKey.Oem102:
                 if (keyInfo.KeyChar == 0)
                 {
-                    // If the keyChar is 0, keyInfo.Key value is not a printable character.
-                    System.Diagnostics.Debug.Assert (keyInfo.Key == 0);
+                    // All Oem* produce a valid KeyChar and is not guaranteed to be printable ASCII, but it’s never just '\0' (null).
+                    // If that happens it's because Console.ReadKey is misreporting for AltGr + non-character keys
+                    // or if it's a combine key waiting for the next input which will determine the respective KeyChar.
+                    // This behavior only happens on Windows and not on Unix-like systems.
+                    if (keyInfo.Key != ConsoleKey.Multiply
+                        && keyInfo.Key != ConsoleKey.Add
+                        && keyInfo.Key != ConsoleKey.Decimal
+                        && keyInfo.Key != ConsoleKey.Subtract
+                        && keyInfo.Key != ConsoleKey.Divide
+                        && keyInfo.Key != ConsoleKey.OemPeriod
+                        && keyInfo.Key != ConsoleKey.OemComma
+                        && keyInfo.Key != ConsoleKey.OemPlus
+                        && keyInfo.Key != ConsoleKey.OemMinus
+                        && keyInfo.Key != ConsoleKey.Oem1
+                        && keyInfo.Key != ConsoleKey.Oem2
+                        && keyInfo.Key != ConsoleKey.Oem3
+                        && keyInfo.Key != ConsoleKey.Oem4
+                        && keyInfo.Key != ConsoleKey.Oem5
+                        && keyInfo.Key != ConsoleKey.Oem6
+                        && keyInfo.Key != ConsoleKey.Oem7
+                        && keyInfo.Key != ConsoleKey.Oem102)
+                    {
+                        // If the keyChar is 0, keyInfo.Key value is not a printable character.
+                        System.Diagnostics.Debug.Assert (keyInfo.Key == 0);
+                    }
 
                     return KeyCode.Null; // MapToKeyCodeModifiers (keyInfo.Modifiers, KeyCode)keyInfo.Key);
                 }
@@ -1425,7 +1461,7 @@ public static class EscSeqUtils
         // Handle control keys whose VK codes match the related ASCII value (those below ASCII 33) like ESC
         if (keyInfo.Key != ConsoleKey.None && Enum.IsDefined (typeof (KeyCode), (uint)keyInfo.Key))
         {
-            if (keyInfo.Modifiers.HasFlag (ConsoleModifiers.Control) && keyInfo.Key == ConsoleKey.I)
+            if (keyInfo is { Modifiers: ConsoleModifiers.Control, Key: ConsoleKey.I })
             {
                 return KeyCode.Tab;
             }
@@ -1498,29 +1534,6 @@ public static class EscSeqUtils
         _isButtonDoubleClicked = false;
     }
 
-    private static async Task ProcessContinuousButtonPressedAsync (MouseFlags mouseFlag, Action<MouseFlags, Point> continuousButtonPressedHandler)
-    {
-        // PERF: Pause and poll in a hot loop.
-        // This should be replaced with event dispatch and a synchronization primitive such as AutoResetEvent.
-        // Will make a massive difference in responsiveness.
-        while (_isButtonPressed)
-        {
-            await Task.Delay (100);
-
-            View view = Application.WantContinuousButtonPressedView;
-
-            if (view is null)
-            {
-                break;
-            }
-
-            if (_isButtonPressed && _lastMouseButtonPressed is { } && (mouseFlag & MouseFlags.ReportMousePosition) == 0)
-            {
-                Application.Invoke (() => continuousButtonPressedHandler (mouseFlag, _point ?? Point.Empty));
-            }
-        }
-    }
-
     private static MouseFlags SetControlKeyStates (MouseFlags buttonState, MouseFlags mouseFlag)
     {
         if ((buttonState & MouseFlags.ButtonCtrl) != 0 && (mouseFlag & MouseFlags.ButtonCtrl) == 0)

+ 17 - 26
Terminal.Gui/Drivers/FakeDriver/FakeDriver.cs

@@ -1,4 +1,5 @@
-//
+#nullable enable
+//
 // FakeDriver.cs: A fake IConsoleDriver for unit tests. 
 //
 
@@ -36,7 +37,7 @@ public class FakeDriver : ConsoleDriver
         public bool UseFakeClipboard { get; internal set; }
     }
 
-    public static Behaviors FakeBehaviors = new ();
+    public static Behaviors FakeBehaviors { get; } = new ();
     public override bool SupportsTrueColor => false;
 
     /// <inheritdoc />
@@ -47,8 +48,8 @@ public class FakeDriver : ConsoleDriver
 
     public FakeDriver ()
     {
-        Cols = FakeConsole.WindowWidth = FakeConsole.BufferWidth = FakeConsole.WIDTH;
-        Rows = FakeConsole.WindowHeight = FakeConsole.BufferHeight = FakeConsole.HEIGHT;
+        base.Cols = FakeConsole.WindowWidth = FakeConsole.BufferWidth = FakeConsole.WIDTH;
+        base.Rows = FakeConsole.WindowHeight = FakeConsole.BufferHeight = FakeConsole.HEIGHT;
 
         if (FakeBehaviors.UseFakeClipboard)
         {
@@ -87,7 +88,7 @@ public class FakeDriver : ConsoleDriver
         FakeConsole.Clear ();
     }
 
-    private FakeMainLoop _mainLoopDriver;
+    private FakeMainLoop? _mainLoopDriver;
 
     public override MainLoop Init ()
     {
@@ -124,7 +125,7 @@ public class FakeDriver : ConsoleDriver
 
         for (int row = top; row < rows; row++)
         {
-            if (!_dirtyLines [row])
+            if (!_dirtyLines! [row])
             {
                 continue;
             }
@@ -144,7 +145,7 @@ public class FakeDriver : ConsoleDriver
 
                 for (; col < cols; col++)
                 {
-                    if (!Contents [row, col].IsDirty)
+                    if (!Contents! [row, col].IsDirty)
                     {
                         if (output.Length > 0)
                         {
@@ -168,7 +169,7 @@ public class FakeDriver : ConsoleDriver
                         lastCol = col;
                     }
 
-                    Attribute attr = Contents [row, col].Attribute.Value;
+                    Attribute attr = Contents [row, col].Attribute!.Value;
 
                     // Performance: Only send the escape sequence if the attribute has changed.
                     if (attr != redrawAttr)
@@ -209,18 +210,18 @@ public class FakeDriver : ConsoleDriver
 
         //SetCursorVisibility (savedVisibility);
 
-        void WriteToConsole (StringBuilder output, ref int lastCol, int row, ref int outputWidth)
+        void WriteToConsole (StringBuilder outputSb, ref int lastColumn, int row, ref int outputWidth)
         {
             FakeConsole.CursorTop = row;
-            FakeConsole.CursorLeft = lastCol;
+            FakeConsole.CursorLeft = lastColumn;
 
-            foreach (char c in output.ToString ())
+            foreach (char c in outputSb.ToString ())
             {
                 FakeConsole.Write (c);
             }
 
-            output.Clear ();
-            lastCol += outputWidth;
+            outputSb.Clear ();
+            lastColumn += outputWidth;
             outputWidth = 0;
         }
 
@@ -396,11 +397,6 @@ public class FakeDriver : ConsoleDriver
         return FakeConsole.CursorVisible;
     }
 
-    public override void SendKeys (char keyChar, ConsoleKey key, bool shift, bool alt, bool control)
-    {
-        MockKeyPressedHandler (new ConsoleKeyInfo (keyChar, key, shift, alt, control));
-    }
-
     private AnsiResponseParser _parser = new ();
 
     /// <inheritdoc />
@@ -506,7 +502,7 @@ public class FakeDriver : ConsoleDriver
 
     public class FakeClipboard : ClipboardBase
     {
-        public Exception FakeException;
+        public Exception? FakeException { get; set; }
 
         private readonly bool _isSupportedAlwaysFalse;
         private string _contents = string.Empty;
@@ -536,19 +532,14 @@ public class FakeDriver : ConsoleDriver
             return _contents;
         }
 
-        protected override void SetClipboardDataImpl (string text)
+        protected override void SetClipboardDataImpl (string? text)
         {
-            if (text is null)
-            {
-                throw new ArgumentNullException (nameof (text));
-            }
-
             if (FakeException is { })
             {
                 throw FakeException;
             }
 
-            _contents = text;
+            _contents = text ?? throw new ArgumentNullException (nameof (text));
         }
     }
 

+ 0 - 8
Terminal.Gui/Drivers/IConsoleDriver.cs

@@ -251,14 +251,6 @@ public interface IConsoleDriver
     /// </remarks>
     event EventHandler<Key>? KeyUp;
 
-    /// <summary>Simulates a key press.</summary>
-    /// <param name="keyChar">The key character.</param>
-    /// <param name="key">The key.</param>
-    /// <param name="shift">If <see langword="true"/> simulates the Shift key being pressed.</param>
-    /// <param name="alt">If <see langword="true"/> simulates the Alt key being pressed.</param>
-    /// <param name="ctrl">If <see langword="true"/> simulates the Ctrl key being pressed.</param>
-    void SendKeys (char keyChar, ConsoleKey key, bool shift, bool alt, bool ctrl);
-
     /// <summary>
     ///     Queues the given <paramref name="request"/> for execution
     /// </summary>

+ 6 - 62
Terminal.Gui/Drivers/NetDriver/NetDriver.cs

@@ -223,7 +223,7 @@ internal class NetDriver : ConsoleDriver
 
     // BUGBUG: Fix this nullable issue.
     /// <inheritdoc />
-    internal override IAnsiResponseParser GetParser () => _mainLoopDriver._netEvents.Parser;
+    internal override IAnsiResponseParser GetParser () => _mainLoopDriver!._netEvents!.Parser;
     internal NetMainLoop? _mainLoopDriver;
 
     /// <inheritdoc />
@@ -356,11 +356,6 @@ internal class NetDriver : ConsoleDriver
     }
     public override void End ()
     {
-        if (IsWinPlatform)
-        {
-            NetWinConsole?.Cleanup ();
-        }
-
         StopReportingMouseMoves ();
 
         if (!RunningUnitTests)
@@ -373,6 +368,11 @@ internal class NetDriver : ConsoleDriver
             //Set cursor key to cursor.
             Console.Out.Write (EscSeqUtils.CSI_ShowCursor);
             Console.Out.Close ();
+
+            // Reset the console to its original state
+            // after sending the escape sequences to restore
+            // alternative buffer and cursor visibility.
+            NetWinConsole?.Cleanup ();
         }
     }
 
@@ -685,21 +685,6 @@ internal class NetDriver : ConsoleDriver
 
     #region Keyboard Handling
 
-    public override void SendKeys (char keyChar, ConsoleKey key, bool shift, bool alt, bool control)
-    {
-        var input = new InputResult
-        {
-            EventType = EventType.Key, ConsoleKeyInfo = new (keyChar, key, shift, alt, control)
-        };
-
-        try
-        {
-            ProcessInput (input);
-        }
-        catch (OverflowException)
-        { }
-    }
-
     //private ConsoleKeyInfo FromVKPacketToKConsoleKeyInfo (ConsoleKeyInfo consoleKeyInfo)
     //{
     //    if (consoleKeyInfo.Key != ConsoleKey.Packet)
@@ -746,47 +731,6 @@ internal class NetDriver : ConsoleDriver
 
     public virtual void ResizeScreen ()
     {
-        // Not supported on Unix.
-        if (IsWinPlatform)
-        {
-            // Can raise an exception while is still resizing.
-            try
-            {
-#pragma warning disable CA1416
-                if (Console.WindowHeight > 0)
-                {
-                    Console.CursorTop = 0;
-                    Console.CursorLeft = 0;
-                    Console.WindowTop = 0;
-                    Console.WindowLeft = 0;
-
-                    if (Console.WindowHeight > Rows)
-                    {
-                        Console.SetWindowSize (Cols, Rows);
-                    }
-
-                    Console.SetBufferSize (Cols, Rows);
-                }
-#pragma warning restore CA1416
-            }
-            // INTENT: Why are these eating the exceptions?
-            // Comments would be good here.
-            catch (IOException)
-            {
-                // CONCURRENCY: Unsynchronized access to Clip is not safe.
-                Clip = new (Screen);
-            }
-            catch (ArgumentOutOfRangeException)
-            {
-                // CONCURRENCY: Unsynchronized access to Clip is not safe.
-                Clip = new (Screen);
-            }
-        }
-        else
-        {
-            Console.Out.Write (EscSeqUtils.CSI_SetTerminalWindowSize (Rows, Cols));
-        }
-
         // CONCURRENCY: Unsynchronized access to Clip is not safe.
         Clip = new (Screen);
     }

+ 14 - 18
Terminal.Gui/Drivers/NetDriver/NetEvents.cs

@@ -50,7 +50,7 @@ internal class NetEvents : IDisposable
 
     public InputResult? DequeueInput ()
     {
-        while (!_netEventsDisposed.Token.IsCancellationRequested)
+        while (_netEventsDisposed is { Token.IsCancellationRequested: false })
         {
             _winChange.Set ();
 
@@ -80,7 +80,7 @@ internal class NetEvents : IDisposable
             return Console.ReadKey (intercept);
         }
 
-        while (!_netEventsDisposed.IsCancellationRequested)
+        while (!_netEventsDisposed!.IsCancellationRequested)
         {
             Task.Delay (100, _netEventsDisposed.Token).Wait (_netEventsDisposed.Token);
 
@@ -113,11 +113,11 @@ internal class NetEvents : IDisposable
 
     private void ProcessInputQueue ()
     {
-        while (!_netEventsDisposed.IsCancellationRequested)
+        while (_netEventsDisposed is { IsCancellationRequested: false })
         {
             if (_inputQueue.Count == 0)
             {
-                while (!_netEventsDisposed.IsCancellationRequested)
+                while (_netEventsDisposed is { IsCancellationRequested: false })
                 {
                     ConsoleKeyInfo consoleKeyInfo;
 
@@ -147,7 +147,7 @@ internal class NetEvents : IDisposable
     {
         void RequestWindowSize ()
         {
-            while (!_netEventsDisposed.IsCancellationRequested)
+            while (_netEventsDisposed is { IsCancellationRequested: false })
             {
                 // Wait for a while then check if screen has changed sizes
                 Task.Delay (500, _netEventsDisposed.Token).Wait (_netEventsDisposed.Token);
@@ -179,7 +179,7 @@ internal class NetEvents : IDisposable
             _netEventsDisposed.Token.ThrowIfCancellationRequested ();
         }
 
-        while (!_netEventsDisposed.IsCancellationRequested)
+        while (!_netEventsDisposed!.IsCancellationRequested)
         {
             try
             {
@@ -434,10 +434,6 @@ internal class NetEvents : IDisposable
                                          new InputResult { EventType = eventType, WindowPositionEvent = winPositionEv }
                                         );
                 }
-                else
-                {
-                    return;
-                }
 
                 break;
 
@@ -563,15 +559,15 @@ internal class NetEvents : IDisposable
 
         public readonly override string ToString ()
         {
-            return EventType switch
-            {
-                EventType.Key => ToString (ConsoleKeyInfo),
-                EventType.Mouse => MouseEvent.ToString (),
+            return (EventType switch
+                    {
+                        EventType.Key => ToString (ConsoleKeyInfo),
+                        EventType.Mouse => MouseEvent.ToString (),
 
-                //EventType.WindowSize => WindowSize.ToString (),
-                //EventType.RequestResponse => RequestResponse.ToString (),
-                _ => "Unknown event type: " + EventType
-            };
+                        //EventType.WindowSize => WindowSize.ToString (),
+                        //EventType.RequestResponse => RequestResponse.ToString (),
+                        _ => "Unknown event type: " + EventType
+                    })!;
         }
 
         /// <summary>Prints a ConsoleKeyInfoEx structure</summary>

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

@@ -57,7 +57,7 @@ internal class NetMainLoop : IMainLoopDriver
 
         _waitForProbe.Set ();
 
-        if (_resultQueue.Count > 0 || _mainLoop!.TimedEvents.CheckTimersAndIdleHandlers (out int waitTimeout))
+        if (_resultQueue.Count > 0 || _mainLoop!.TimedEvents.CheckTimers (out int waitTimeout))
         {
             return true;
         }
@@ -84,7 +84,7 @@ internal class NetMainLoop : IMainLoopDriver
 
         if (!_eventReadyTokenSource.IsCancellationRequested)
         {
-            return _resultQueue.Count > 0 || _mainLoop.TimedEvents.CheckTimersAndIdleHandlers (out _);
+            return _resultQueue.Count > 0 || _mainLoop.TimedEvents.CheckTimers (out _);
         }
 
         // If cancellation was requested then always return true

+ 25 - 41
Terminal.Gui/Drivers/NetDriver/NetWinVTConsole.cs

@@ -5,31 +5,31 @@ namespace Terminal.Gui.Drivers;
 
 internal class NetWinVTConsole
 {
-    private const uint DISABLE_NEWLINE_AUTO_RETURN = 8;
-    private const uint ENABLE_ECHO_INPUT = 4;
-    private const uint ENABLE_EXTENDED_FLAGS = 128;
-    private const uint ENABLE_INSERT_MODE = 32;
-    private const uint ENABLE_LINE_INPUT = 2;
-    private const uint ENABLE_LVB_GRID_WORLDWIDE = 10;
-    private const uint ENABLE_MOUSE_INPUT = 16;
-
     // Input modes.
     private const uint ENABLE_PROCESSED_INPUT = 1;
+    private const uint ENABLE_LINE_INPUT = 2;
+    private const uint ENABLE_ECHO_INPUT = 4;
+    private const uint ENABLE_WINDOW_INPUT = 8;
+    private const uint ENABLE_MOUSE_INPUT = 16;
+    private const uint ENABLE_INSERT_MODE = 32;
+    private const uint ENABLE_QUICK_EDIT_MODE = 64;
+    private const uint ENABLE_EXTENDED_FLAGS = 128;
+    private const uint ENABLE_VIRTUAL_TERMINAL_INPUT = 512;
 
     // Output modes.
     private const uint ENABLE_PROCESSED_OUTPUT = 1;
-    private const uint ENABLE_QUICK_EDIT_MODE = 64;
-    private const uint ENABLE_VIRTUAL_TERMINAL_INPUT = 512;
-    private const uint ENABLE_VIRTUAL_TERMINAL_PROCESSING = 4;
-    private const uint ENABLE_WINDOW_INPUT = 8;
     private const uint ENABLE_WRAP_AT_EOL_OUTPUT = 2;
+    private const uint ENABLE_VIRTUAL_TERMINAL_PROCESSING = 4;
+    private const uint DISABLE_NEWLINE_AUTO_RETURN = 8;
+    private const uint ENABLE_LVB_GRID_WORLDWIDE = 10;
+
+    // Standard handles.
     private const int STD_ERROR_HANDLE = -12;
     private const int STD_INPUT_HANDLE = -10;
     private const int STD_OUTPUT_HANDLE = -11;
 
-    private readonly nint _errorHandle;
+    // Handles and original console modes.
     private readonly nint _inputHandle;
-    private readonly uint _originalErrorConsoleMode;
     private readonly uint _originalInputConsoleMode;
     private readonly uint _originalOutputConsoleMode;
     private readonly nint _outputHandle;
@@ -45,7 +45,7 @@ internal class NetWinVTConsole
 
         _originalInputConsoleMode = mode;
 
-        if ((mode & ENABLE_VIRTUAL_TERMINAL_INPUT) < ENABLE_VIRTUAL_TERMINAL_INPUT)
+        if ((mode & ENABLE_VIRTUAL_TERMINAL_INPUT) == 0)
         {
             mode |= ENABLE_VIRTUAL_TERMINAL_INPUT;
 
@@ -64,38 +64,24 @@ internal class NetWinVTConsole
 
         _originalOutputConsoleMode = mode;
 
-        if ((mode & (ENABLE_VIRTUAL_TERMINAL_PROCESSING | DISABLE_NEWLINE_AUTO_RETURN)) < DISABLE_NEWLINE_AUTO_RETURN)
+        if ((mode & ENABLE_VIRTUAL_TERMINAL_PROCESSING) == 0)
         {
-            mode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING | DISABLE_NEWLINE_AUTO_RETURN;
+            mode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING;
 
             if (!SetConsoleMode (_outputHandle, mode))
             {
                 throw new ApplicationException ($"Failed to set output console mode, error code: {GetLastError ()}.");
             }
         }
-
-        _errorHandle = GetStdHandle (STD_ERROR_HANDLE);
-
-        if (!GetConsoleMode (_errorHandle, out mode))
-        {
-            throw new ApplicationException ($"Failed to get error console mode, error code: {GetLastError ()}.");
-        }
-
-        _originalErrorConsoleMode = mode;
-
-        if ((mode & DISABLE_NEWLINE_AUTO_RETURN) < DISABLE_NEWLINE_AUTO_RETURN)
-        {
-            mode |= DISABLE_NEWLINE_AUTO_RETURN;
-
-            if (!SetConsoleMode (_errorHandle, mode))
-            {
-                throw new ApplicationException ($"Failed to set error console mode, error code: {GetLastError ()}.");
-            }
-        }
     }
 
     public void Cleanup ()
     {
+        if (!FlushConsoleInputBuffer (_inputHandle))
+        {
+            throw new ApplicationException ($"Failed to flush input buffer, error code: {GetLastError ()}.");
+        }
+
         if (!SetConsoleMode (_inputHandle, _originalInputConsoleMode))
         {
             throw new ApplicationException ($"Failed to restore input console mode, error code: {GetLastError ()}.");
@@ -105,11 +91,6 @@ internal class NetWinVTConsole
         {
             throw new ApplicationException ($"Failed to restore output console mode, error code: {GetLastError ()}.");
         }
-
-        if (!SetConsoleMode (_errorHandle, _originalErrorConsoleMode))
-        {
-            throw new ApplicationException ($"Failed to restore error console mode, error code: {GetLastError ()}.");
-        }
     }
 
     [DllImport ("kernel32.dll")]
@@ -123,4 +104,7 @@ internal class NetWinVTConsole
 
     [DllImport ("kernel32.dll")]
     private static extern bool SetConsoleMode (nint hConsoleHandle, uint dwMode);
+
+    [DllImport ("kernel32.dll", SetLastError = true)]
+    private static extern bool FlushConsoleInputBuffer (nint hConsoleInput);
 }

+ 57 - 66
Terminal.Gui/Drivers/V2/ApplicationV2.cs

@@ -1,5 +1,6 @@
 #nullable enable
 using System.Collections.Concurrent;
+using System.ComponentModel;
 using System.Diagnostics;
 using System.Diagnostics.CodeAnalysis;
 using Microsoft.Extensions.Logging;
@@ -12,38 +13,29 @@ namespace Terminal.Gui.Drivers;
 /// </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 readonly IComponentFactory? _componentFactory;
     private IMainLoopCoordinator? _coordinator;
     private string? _driverName;
 
     private readonly ITimedEvents _timedEvents = new TimedEvents ();
 
+    /// <inheritdoc/>
+    public override ITimedEvents TimedEvents => _timedEvents;
+
+    internal IMainLoopCoordinator? Coordinator => _coordinator;
+
     /// <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
-    )
+    public ApplicationV2 ()
+    {
+        IsLegacy = false;
+    }
+
+    internal ApplicationV2 (IComponentFactory componentFactory)
     {
-        _netInputFactory = netInputFactory;
-        _netOutputFactory = netOutputFactory;
-        _winInputFactory = winInputFactory;
-        _winOutputFactory = winOutputFactory;
+        _componentFactory = componentFactory;
         IsLegacy = false;
     }
 
@@ -89,24 +81,29 @@ public class ApplicationV2 : ApplicationImpl
     {
         PlatformID p = Environment.OSVersion.Platform;
 
-        bool definetlyWin = driverName?.Contains ("win") ?? false;
-        bool definetlyNet = driverName?.Contains ("net") ?? false;
+        bool definetlyWin = (driverName?.Contains ("win") ?? false) || _componentFactory is IComponentFactory<WindowsConsole.InputRecord>;
+        bool definetlyNet = (driverName?.Contains ("net") ?? false) || _componentFactory is IComponentFactory<ConsoleKeyInfo>;
+        bool definetlyUnix = (driverName?.Contains ("unix") ?? false) || _componentFactory is IComponentFactory<char>;
 
         if (definetlyWin)
         {
-            _coordinator = CreateWindowsSubcomponents ();
+            _coordinator = CreateSubcomponents (() => new WindowsComponentFactory ());
         }
         else if (definetlyNet)
         {
-            _coordinator = CreateNetSubcomponents ();
+            _coordinator = CreateSubcomponents (() => new NetComponentFactory ());
+        }
+        else if (definetlyUnix)
+        {
+            _coordinator = CreateSubcomponents (() => new UnixComponentFactory ());
         }
         else if (p == PlatformID.Win32NT || p == PlatformID.Win32S || p == PlatformID.Win32Windows)
         {
-            _coordinator = CreateWindowsSubcomponents ();
+            _coordinator = CreateSubcomponents (() => new WindowsComponentFactory ());
         }
         else
         {
-            _coordinator = CreateNetSubcomponents ();
+            _coordinator = CreateSubcomponents (() => new UnixComponentFactory ());
         }
 
         _coordinator.StartAsync ().Wait ();
@@ -117,32 +114,23 @@ public class ApplicationV2 : ApplicationImpl
         }
     }
 
-    private IMainLoopCoordinator CreateWindowsSubcomponents ()
+    private IMainLoopCoordinator CreateSubcomponents<T> (Func<IComponentFactory<T>> fallbackFactory)
     {
-        ConcurrentQueue<WindowsConsole.InputRecord> inputBuffer = new ();
-        MainLoop<WindowsConsole.InputRecord> loop = new ();
-
-        return new MainLoopCoordinator<WindowsConsole.InputRecord> (
-                                                                    _timedEvents,
-                                                                    _winInputFactory,
-                                                                    inputBuffer,
-                                                                    new WindowsInputProcessor (inputBuffer),
-                                                                    _winOutputFactory,
-                                                                    loop);
-    }
+        ConcurrentQueue<T> inputBuffer = new ();
+        MainLoop<T> loop = new ();
 
-    private IMainLoopCoordinator CreateNetSubcomponents ()
-    {
-        ConcurrentQueue<ConsoleKeyInfo> inputBuffer = new ();
-        MainLoop<ConsoleKeyInfo> loop = new ();
-
-        return new MainLoopCoordinator<ConsoleKeyInfo> (
-                                                        _timedEvents,
-                                                        _netInputFactory,
-                                                        inputBuffer,
-                                                        new NetInputProcessor (inputBuffer),
-                                                        _netOutputFactory,
-                                                        loop);
+        IComponentFactory<T> cf;
+
+        if (_componentFactory is IComponentFactory<T> typedFactory)
+        {
+            cf = typedFactory;
+        }
+        else
+        {
+            cf = fallbackFactory ();
+        }
+
+        return new MainLoopCoordinator<T> (_timedEvents, inputBuffer, loop, cf);
     }
 
     /// <inheritdoc/>
@@ -168,6 +156,12 @@ public class ApplicationV2 : ApplicationImpl
             throw new NotInitializedException (nameof (Run));
         }
 
+        if (Application.Driver == null)
+        {
+            // See Run_T_Init_Driver_Cleared_with_TestTopLevel_Throws
+            throw new  InvalidOperationException ("Driver was inexplicably null when trying to Run view");
+        }
+
         Application.Top = view;
 
         RunState rs = Application.Begin (view);
@@ -225,7 +219,14 @@ public class ApplicationV2 : ApplicationImpl
     /// <inheritdoc/>
     public override void Invoke (Action action)
     {
-        _timedEvents.AddIdle (
+        // If we are already on the main UI thread
+        if (Application.MainThreadId == Thread.CurrentThread.ManagedThreadId)
+        {
+            action ();
+            return;
+        }
+
+        _timedEvents.Add (TimeSpan.Zero,
                               () =>
                               {
                                   action ();
@@ -236,20 +237,10 @@ public class ApplicationV2 : ApplicationImpl
     }
 
     /// <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); }
+    public override object AddTimeout (TimeSpan time, Func<bool> callback) { return _timedEvents.Add (time, callback); }
 
     /// <inheritdoc/>
-    public override bool RemoveTimeout (object token) { return _timedEvents.RemoveTimeout (token); }
+    public override bool RemoveTimeout (object token) { return _timedEvents.Remove (token); }
 
     /// <inheritdoc />
     public override void LayoutAndDraw (bool forceDraw)
@@ -258,4 +249,4 @@ public class ApplicationV2 : ApplicationImpl
         Application.Top?.SetNeedsDraw();
         Application.Top?.SetNeedsLayout ();
     }
-}
+}

+ 26 - 0
Terminal.Gui/Drivers/V2/ComponentFactory.cs

@@ -0,0 +1,26 @@
+#nullable enable
+using System.Collections.Concurrent;
+
+namespace Terminal.Gui.Drivers;
+
+/// <summary>
+/// Abstract base class implementation of <see cref="IComponentFactory{T}"/>
+/// </summary>
+/// <typeparam name="T"></typeparam>
+public abstract class ComponentFactory<T> : IComponentFactory<T>
+{
+    /// <inheritdoc />
+    public abstract IConsoleInput<T> CreateInput ();
+
+    /// <inheritdoc />
+    public abstract IInputProcessor CreateInputProcessor (ConcurrentQueue<T> inputBuffer);
+
+    /// <inheritdoc />
+    public virtual IWindowSizeMonitor CreateWindowSizeMonitor (IConsoleOutput consoleOutput, IOutputBuffer outputBuffer)
+    {
+        return new WindowSizeMonitor (consoleOutput, outputBuffer);
+    }
+
+    /// <inheritdoc />
+    public abstract IConsoleOutput CreateOutput ();
+}

+ 54 - 33
Terminal.Gui/Drivers/V2/ConsoleDriverFacade.cs

@@ -1,5 +1,5 @@
-using System.Runtime.InteropServices;
-using Microsoft.Extensions.Logging;
+#nullable enable
+using System.Runtime.InteropServices;
 
 namespace Terminal.Gui.Drivers;
 
@@ -11,9 +11,13 @@ internal class ConsoleDriverFacade<T> : IConsoleDriver, IConsoleDriverFacade
     private CursorVisibility _lastCursor = CursorVisibility.Default;
 
     /// <summary>The event fired when the terminal is resized.</summary>
-    public event EventHandler<SizeChangedEventArgs> SizeChanged;
+    public event EventHandler<SizeChangedEventArgs>? SizeChanged;
 
     public IInputProcessor InputProcessor { get; }
+    public IOutputBuffer OutputBuffer => _outputBuffer;
+
+    public IWindowSizeMonitor WindowSizeMonitor { get; }
+
 
     public ConsoleDriverFacade (
         IInputProcessor inputProcessor,
@@ -36,13 +40,21 @@ internal class ConsoleDriverFacade<T> : IConsoleDriver, IConsoleDriverFacade
                                          MouseEvent?.Invoke (s, e);
                                      };
 
-        windowSizeMonitor.SizeChanging += (_, e) => SizeChanged?.Invoke (this, e);
+        WindowSizeMonitor = windowSizeMonitor;
+        windowSizeMonitor.SizeChanging += (_,e) => SizeChanged?.Invoke (this, e);
 
         CreateClipboard ();
     }
 
     private void CreateClipboard ()
     {
+        if (FakeDriver.FakeBehaviors.UseFakeClipboard)
+        {
+            Clipboard = new FakeDriver.FakeClipboard ();
+
+            return;
+        }
+
         PlatformID p = Environment.OSVersion.Platform;
 
         if (p == PlatformID.Win32NT || p == PlatformID.Win32S || p == PlatformID.Win32Windows)
@@ -68,7 +80,7 @@ internal class ConsoleDriverFacade<T> : IConsoleDriver, IConsoleDriverFacade
     {
         get
         {
-            if (ConsoleDriver.RunningUnitTests)
+            if (ConsoleDriver.RunningUnitTests && _output is WindowsOutput or NetOutput)
             {
                 // In unit tests, we don't have a real output, so we return an empty rectangle.
                 return Rectangle.Empty;
@@ -83,7 +95,7 @@ internal class ConsoleDriverFacade<T> : IConsoleDriver, IConsoleDriverFacade
     ///     to.
     /// </summary>
     /// <value>The rectangle describing the of <see cref="Clip"/> region.</value>
-    public Region Clip
+    public Region? Clip
     {
         get => _outputBuffer.Clip;
         set => _outputBuffer.Clip = value;
@@ -109,7 +121,7 @@ internal class ConsoleDriverFacade<T> : IConsoleDriver, IConsoleDriverFacade
     ///     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
+    public Cell [,]? Contents
     {
         get => _outputBuffer.Contents;
         set => _outputBuffer.Contents = value;
@@ -224,7 +236,7 @@ internal class ConsoleDriverFacade<T> : IConsoleDriver, IConsoleDriverFacade
     /// <summary>
     ///     Raised each time <see cref="ConsoleDriver.ClearContents"/> is called. For benchmarking.
     /// </summary>
-    public event EventHandler<EventArgs> ClearedContents;
+    public event EventHandler<EventArgs>? ClearedContents;
 
     /// <summary>
     ///     Fills the specified rectangle with the specified rune, using <see cref="ConsoleDriver.CurrentAttribute"/>
@@ -248,16 +260,7 @@ internal class ConsoleDriverFacade<T> : IConsoleDriver, IConsoleDriverFacade
     /// <inheritdoc/>
     public virtual string GetVersionInfo ()
     {
-        var type = "";
-
-        if (InputProcessor is WindowsInputProcessor)
-        {
-            type = "win";
-        }
-        else if (InputProcessor is NetInputProcessor)
-        {
-            type = "net";
-        }
+        string type = InputProcessor.DriverName ?? throw new ArgumentNullException (nameof (InputProcessor.DriverName));
 
         return "v2" + type;
     }
@@ -321,7 +324,36 @@ internal class ConsoleDriverFacade<T> : IConsoleDriver, IConsoleDriverFacade
     }
 
     /// <inheritdoc/>
-    public void Suspend () { }
+    public void Suspend ()
+    {
+        if (Environment.OSVersion.Platform != PlatformID.Unix)
+        {
+            return;
+        }
+
+        Console.Out.Write (EscSeqUtils.CSI_DisableMouseEvents);
+
+        if (!ConsoleDriver.RunningUnitTests)
+        {
+            Console.ResetColor ();
+            Console.Clear ();
+
+            //Disable alternative screen buffer.
+            Console.Out.Write (EscSeqUtils.CSI_RestoreCursorAndRestoreAltBufferWithBackscroll);
+
+            //Set cursor key to cursor.
+            Console.Out.Write (EscSeqUtils.CSI_ShowCursor);
+
+            Platform.Suspend ();
+
+            //Enable alternative screen buffer.
+            Console.Out.Write (EscSeqUtils.CSI_SaveCursorAndActivateAltBufferNoBackscroll);
+
+            Application.LayoutAndDraw ();
+        }
+
+        Console.Out.Write (EscSeqUtils.CSI_EnableMouseEvents);
+    }
 
     /// <summary>
     ///     Sets the position of the terminal cursor to <see cref="ConsoleDriver.Col"/> and
@@ -363,7 +395,7 @@ internal class ConsoleDriverFacade<T> : IConsoleDriver, IConsoleDriverFacade
     }
 
     /// <summary>Event fired when a key is pressed down. This is a precursor to <see cref="ConsoleDriver.KeyUp"/>.</summary>
-    public event EventHandler<Key> KeyDown;
+    public event EventHandler<Key>? KeyDown;
 
     /// <summary>Event fired when a key is released.</summary>
     /// <remarks>
@@ -371,21 +403,10 @@ internal class ConsoleDriverFacade<T> : IConsoleDriver, IConsoleDriverFacade
     ///     processing is
     ///     complete.
     /// </remarks>
-    public event EventHandler<Key> KeyUp;
+    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
-    }
+    public event EventHandler<MouseEventArgs>? MouseEvent;
 
     /// <summary>
     ///     Provide proper writing to send escape sequence recognized by the <see cref="ConsoleDriver"/>.

+ 50 - 0
Terminal.Gui/Drivers/V2/IComponentFactory.cs

@@ -0,0 +1,50 @@
+#nullable enable
+using System.Collections.Concurrent;
+
+namespace Terminal.Gui.Drivers;
+
+/// <summary>
+/// Base untyped interface for <see cref="IComponentFactory{T}"/> for methods that are not templated on low level
+/// console input type.
+/// </summary>
+public interface IComponentFactory
+{
+    /// <summary>
+    /// Create the <see cref="IConsoleOutput"/> class for the current driver implementation i.e. the class responsible for
+    /// rendering <see cref="IOutputBuffer"/> into the console.
+    /// </summary>
+    /// <returns></returns>
+    IConsoleOutput CreateOutput ();
+}
+
+/// <summary>
+/// Creates driver specific subcomponent classes (<see cref="IConsoleInput{T}"/>, <see cref="IInputProcessor"/> etc) for a
+/// <see cref="IMainLoopCoordinator"/>.
+/// </summary>
+/// <typeparam name="T"></typeparam>
+public interface IComponentFactory<T> : IComponentFactory
+{
+    /// <summary>
+    /// Create <see cref="IConsoleInput{T}"/> class for the current driver implementation i.e. the class responsible for reading
+    /// user input from the console.
+    /// </summary>
+    /// <returns></returns>
+    IConsoleInput<T> CreateInput ();
+
+    /// <summary>
+    /// Creates the <see cref="InputProcessor{T}"/> class for the current driver implementation i.e. the class responsible for
+    /// translating raw console input into Terminal.Gui common event <see cref="Key"/> and <see cref="MouseEventArgs"/>.
+    /// </summary>
+    /// <param name="inputBuffer"></param>
+    /// <returns></returns>
+    IInputProcessor CreateInputProcessor (ConcurrentQueue<T> inputBuffer);
+
+    /// <summary>
+    /// Creates <see cref="IWindowSizeMonitor"/> class for the current driver implementation i.e. the class responsible for
+    /// reporting the current size of the terminal window.
+    /// </summary>
+    /// <param name="consoleOutput"></param>
+    /// <param name="outputBuffer"></param>
+    /// <returns></returns>
+    IWindowSizeMonitor CreateWindowSizeMonitor (IConsoleOutput consoleOutput, IOutputBuffer outputBuffer);
+}

+ 14 - 2
Terminal.Gui/Drivers/V2/IConsoleDriverFacade.cs

@@ -1,4 +1,5 @@
-namespace Terminal.Gui.Drivers;
+#nullable enable
+namespace Terminal.Gui.Drivers;
 
 /// <summary>
 ///     Interface for v2 driver abstraction layer
@@ -10,5 +11,16 @@ public interface IConsoleDriverFacade
     ///     e.g. <see cref="ConsoleKeyInfo"/> into <see cref="Key"/> events
     ///     and detecting and processing ansi escape sequences.
     /// </summary>
-    public IInputProcessor InputProcessor { get; }
+    IInputProcessor InputProcessor { get; }
+
+    /// <summary>
+    ///     Describes the desired screen state. Data source for <see cref="IConsoleOutput"/>.
+    /// </summary>
+    IOutputBuffer OutputBuffer { get; }
+
+    /// <summary>
+    ///     Interface for classes responsible for reporting the current
+    ///     size of the terminal window.
+    /// </summary>
+    IWindowSizeMonitor WindowSizeMonitor { get; }
 }

+ 16 - 0
Terminal.Gui/Drivers/V2/IInputProcessor.cs

@@ -25,6 +25,11 @@ public interface IInputProcessor
     /// <summary>Event fired when a mouse event occurs.</summary>
     event EventHandler<MouseEventArgs>? MouseEvent;
 
+    /// <summary>
+    /// Gets the name of the driver associated with this input processor.
+    /// </summary>
+    string DriverName { get; init; }
+
     /// <summary>
     ///     Called when a key is pressed down. Fires the <see cref="KeyDown"/> event. This is a precursor to
     ///     <see cref="OnKeyUp"/>.
@@ -58,4 +63,15 @@ public interface IInputProcessor
     /// </summary>
     /// <returns></returns>
     public IAnsiResponseParser GetParser ();
+
+    /// <summary>
+    ///     Handles surrogate pairs in the input stream.
+    /// </summary>
+    /// <param name="key">The key from input.</param>
+    /// <param name="result">Get the surrogate pair or the key.</param>
+    /// <returns>
+    ///     <see langword="true"/> if the result is a valid surrogate pair or a valid key, otherwise
+    ///     <see langword="false"/>.
+    /// </returns>
+    bool IsValidInput (Key key, out Key result);
 }

+ 10 - 3
Terminal.Gui/Drivers/V2/IMainLoop.cs

@@ -6,11 +6,11 @@ namespace Terminal.Gui.Drivers;
 /// <summary>
 ///     Interface for main loop that runs the core Terminal.Gui UI loop.
 /// </summary>
-/// <typeparam name="T"></typeparam>
+/// <typeparam name="T">Type of raw input events processed by the loop e.g. <see cref="ConsoleKeyInfo"/></typeparam>
 public interface IMainLoop<T> : IDisposable
 {
     /// <summary>
-    ///     Gets the class responsible for servicing user timeouts and idles
+    ///     Gets the class responsible for servicing user timeouts
     /// </summary>
     public ITimedEvents TimedEvents { get; }
 
@@ -48,7 +48,14 @@ public interface IMainLoop<T> : IDisposable
     /// <param name="inputBuffer"></param>
     /// <param name="inputProcessor"></param>
     /// <param name="consoleOutput"></param>
-    void Initialize (ITimedEvents timedEvents, ConcurrentQueue<T> inputBuffer, IInputProcessor inputProcessor, IConsoleOutput consoleOutput);
+    /// <param name="componentFactory"></param>
+    void Initialize (
+        ITimedEvents timedEvents,
+        ConcurrentQueue<T> inputBuffer,
+        IInputProcessor inputProcessor,
+        IConsoleOutput consoleOutput,
+        IComponentFactory<T> componentFactory
+    );
 
     /// <summary>
     ///     Perform a single iteration of the main loop then blocks for a fixed length

+ 1 - 7
Terminal.Gui/Drivers/V2/IOutputBuffer.cs

@@ -9,16 +9,10 @@ namespace Terminal.Gui.Drivers;
 /// </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; }
+    Cell [,]? Contents { get; set; }
 
     /// <summary>
     ///     Gets or sets the clip rectangle that <see cref="AddRune(Rune)"/> and <see cref="AddStr(string)"/> are subject

+ 3 - 0
Terminal.Gui/Drivers/V2/IUnixInput.cs

@@ -0,0 +1,3 @@
+namespace Terminal.Gui.Drivers;
+
+internal interface IUnixInput : IConsoleInput<char>;

+ 4 - 1
Terminal.Gui/Drivers/V2/IWindowsInput.cs

@@ -1,4 +1,7 @@
 namespace Terminal.Gui.Drivers;
 
-internal interface IWindowsInput : IConsoleInput<WindowsConsole.InputRecord>
+/// <summary>
+/// Interface for windows only input which uses low level win32 apis (v2win)
+/// </summary>
+public interface IWindowsInput : IConsoleInput<WindowsConsole.InputRecord>
 { }

+ 59 - 0
Terminal.Gui/Drivers/V2/InputProcessor.cs

@@ -30,6 +30,9 @@ public abstract class InputProcessor<T> : IInputProcessor
     /// </summary>
     public ConcurrentQueue<T> InputBuffer { get; }
 
+    /// <inheritdoc />
+    public string DriverName { get; init; }
+
     /// <inheritdoc/>
     public IAnsiResponseParser GetParser () { return Parser; }
 
@@ -162,4 +165,60 @@ public abstract class InputProcessor<T> : IInputProcessor
     /// </summary>
     /// <param name="input"></param>
     protected abstract void ProcessAfterParsing (T input);
+
+    internal char _highSurrogate = '\0';
+
+    /// <inheritdoc />
+    public bool IsValidInput (Key key, out Key result)
+    {
+        result = key;
+
+        if (char.IsHighSurrogate ((char)key))
+        {
+            _highSurrogate = (char)key;
+
+            return false;
+        }
+
+        if (_highSurrogate > 0 && char.IsLowSurrogate ((char)key))
+        {
+            result = (KeyCode)new Rune (_highSurrogate, (char)key).Value;
+
+            if (key.IsAlt)
+            {
+                result = result.WithAlt;
+            }
+
+            if (key.IsCtrl)
+            {
+                result = result.WithCtrl;
+            }
+
+            if (key.IsShift)
+            {
+                result = result.WithShift;
+            }
+
+            _highSurrogate = '\0';
+
+            return true;
+        }
+
+        if (char.IsSurrogate ((char)key))
+        {
+            return false;
+        }
+
+        if (_highSurrogate > 0)
+        {
+            _highSurrogate = '\0';
+        }
+
+        if (key.KeyCode == 0)
+        {
+            return false;
+        }
+
+        return true;
+    }
 }

+ 17 - 7
Terminal.Gui/Drivers/V2/MainLoop.cs

@@ -83,7 +83,14 @@ public class MainLoop<T> : IMainLoop<T>
     /// <param name="inputBuffer"></param>
     /// <param name="inputProcessor"></param>
     /// <param name="consoleOutput"></param>
-    public void Initialize (ITimedEvents timedEvents, ConcurrentQueue<T> inputBuffer, IInputProcessor inputProcessor, IConsoleOutput consoleOutput)
+    /// <param name="componentFactory"></param>
+    public void Initialize (
+        ITimedEvents timedEvents,
+        ConcurrentQueue<T> inputBuffer,
+        IInputProcessor inputProcessor,
+        IConsoleOutput consoleOutput,
+        IComponentFactory<T> componentFactory
+    )
     {
         InputBuffer = inputBuffer;
         Out = consoleOutput;
@@ -92,18 +99,22 @@ public class MainLoop<T> : IMainLoop<T>
         TimedEvents = timedEvents;
         AnsiRequestScheduler = new (InputProcessor.GetParser ());
 
-        WindowSizeMonitor = new WindowSizeMonitor (Out, OutputBuffer);
+        WindowSizeMonitor = componentFactory.CreateWindowSizeMonitor (Out, OutputBuffer);
     }
 
     /// <inheritdoc/>
     public void Iteration ()
     {
+
+        Application.RaiseIteration ();
+
         DateTime dt = Now ();
+        int timeAllowed = 1000 / Math.Max(1,(int)Application.MaximumIterationsPerSecond);
 
         IterationImpl ();
 
         TimeSpan took = Now () - dt;
-        TimeSpan sleepFor = TimeSpan.FromMilliseconds (50) - took;
+        TimeSpan sleepFor = TimeSpan.FromMilliseconds (timeAllowed) - took;
 
         Logging.TotalIterationMetric.Record (took.Milliseconds);
 
@@ -123,7 +134,8 @@ public class MainLoop<T> : IMainLoop<T>
         if (Application.Top != null)
         {
             bool needsDrawOrLayout = AnySubViewsNeedDrawn (Application.Popover?.GetActivePopover () as View)
-                                     || AnySubViewsNeedDrawn (Application.Top);
+                                     || AnySubViewsNeedDrawn (Application.Top)
+                                     || (Application.MouseGrabHandler.MouseGrabView != null && AnySubViewsNeedDrawn (Application.MouseGrabHandler.MouseGrabView));
 
             bool sizeChanged = WindowSizeMonitor.Poll ();
 
@@ -143,9 +155,7 @@ public class MainLoop<T> : IMainLoop<T>
 
         var swCallbacks = Stopwatch.StartNew ();
 
-        TimedEvents.LockAndRunTimers ();
-
-        TimedEvents.LockAndRunIdles ();
+        TimedEvents.RunTimers ();
 
         Logging.IterationInvokesAndTimeouts.Record (swCallbacks.Elapsed.Milliseconds);
     }

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio