Browse Source

Merge branch 'v2_develop' into copilot/rename-application-top-property

copilot-swe-agent[bot] 1 month ago
parent
commit
deb09cca4d
100 changed files with 3879 additions and 6013 deletions
  1. 3 21
      .github/workflows/build-validation.yml
  2. 77 36
      .github/workflows/integration-tests.yml
  3. 43 0
      .github/workflows/quick-build.yml
  4. 104 26
      .github/workflows/unit-tests.yml
  5. 5 0
      .gitignore
  6. 58 49
      CONTRIBUTING.md
  7. 2 2
      Examples/NativeAot/Program.cs
  8. 1 1
      Examples/SelfContained/Program.cs
  9. 9 16
      Examples/UICatalog/Scenario.cs
  10. 5 9
      Examples/UICatalog/Scenarios/AllViewsTester.cs
  11. 2 2
      Examples/UICatalog/Scenarios/Bars.cs
  12. 22 49
      Examples/UICatalog/Scenarios/Buttons.cs
  13. 6 30
      Examples/UICatalog/Scenarios/CharacterMap/CharacterMap.cs
  14. 19 15
      Examples/UICatalog/Scenarios/ColorPicker.cs
  15. 7 10
      Examples/UICatalog/Scenarios/Dialogs.cs
  16. 4 4
      Examples/UICatalog/Scenarios/DimAutoDemo.cs
  17. 10 10
      Examples/UICatalog/Scenarios/DynamicMenuBar.cs
  18. 13 79
      Examples/UICatalog/Scenarios/EditorsAndHelpers/ArrangementEditor.cs
  19. 14 13
      Examples/UICatalog/Scenarios/EditorsAndHelpers/BorderEditor.cs
  20. 9 9
      Examples/UICatalog/Scenarios/EditorsAndHelpers/DimEditor.cs
  21. 14 22
      Examples/UICatalog/Scenarios/EditorsAndHelpers/MarginEditor.cs
  22. 9 9
      Examples/UICatalog/Scenarios/EditorsAndHelpers/PosEditor.cs
  23. 9 10
      Examples/UICatalog/Scenarios/EditorsAndHelpers/ViewPropertiesEditor.cs
  24. 133 133
      Examples/UICatalog/Scenarios/FileDialogExamples.cs
  25. 1 1
      Examples/UICatalog/Scenarios/GraphViewExample.cs
  26. 16 16
      Examples/UICatalog/Scenarios/Images.cs
  27. 1 4
      Examples/UICatalog/Scenarios/Keys.cs
  28. 10 4
      Examples/UICatalog/Scenarios/LineDrawing.cs
  29. 1 1
      Examples/UICatalog/Scenarios/Mazing.cs
  30. 9 8
      Examples/UICatalog/Scenarios/MessageBoxes.cs
  31. 14 12
      Examples/UICatalog/Scenarios/Navigation.cs
  32. 34 33
      Examples/UICatalog/Scenarios/Notepad.cs
  33. 2 2
      Examples/UICatalog/Scenarios/NumericUpDownDemo.cs
  34. 141 135
      Examples/UICatalog/Scenarios/PosAlignDemo.cs
  35. 17 11
      Examples/UICatalog/Scenarios/ProgressBarStyles.cs
  36. 18 45
      Examples/UICatalog/Scenarios/RegionScenario.cs
  37. 5 9
      Examples/UICatalog/Scenarios/ScrollBarDemo.cs
  38. 287 0
      Examples/UICatalog/Scenarios/Selectors.cs
  39. 102 65
      Examples/UICatalog/Scenarios/Shortcuts.cs
  40. 246 107
      Examples/UICatalog/Scenarios/TableEditor.cs
  41. 10 10
      Examples/UICatalog/Scenarios/TextAlignmentAndDirection.cs
  42. 1 1
      Examples/UICatalog/Scenarios/TextInputControls.cs
  43. 6 6
      Examples/UICatalog/Scenarios/Themes.cs
  44. 12 15
      Examples/UICatalog/Scenarios/TimeAndDate.cs
  45. 1 1
      Examples/UICatalog/Scenarios/TreeViewFileSystem.cs
  46. 13 14
      Examples/UICatalog/Scenarios/Unicode.cs
  47. 6 6
      Examples/UICatalog/Scenarios/Wizards.cs
  48. 5 5
      Examples/UICatalog/UICatalog.cs
  49. 52 44
      Examples/UICatalog/UICatalogTop.cs
  50. 1 2
      README.md
  51. 109 0
      Scripts/Run-LocalCoverage.ps1
  52. 62 56
      Terminal.Gui.Analyzers.Tests/ProjectBuilder.cs
  53. 36 22
      Terminal.Gui/App/Application.Driver.cs
  54. 13 57
      Terminal.Gui/App/Application.Keyboard.cs
  55. 25 77
      Terminal.Gui/App/Application.Lifecycle.cs
  56. 4 6
      Terminal.Gui/App/Application.Mouse.cs
  57. 10 10
      Terminal.Gui/App/Application.Navigation.cs
  58. 50 462
      Terminal.Gui/App/Application.Run.cs
  59. 8 35
      Terminal.Gui/App/Application.Screen.cs
  60. 2 12
      Terminal.Gui/App/Application.Toplevel.cs
  61. 4 4
      Terminal.Gui/App/Application.cd
  62. 7 111
      Terminal.Gui/App/Application.cs
  63. 176 0
      Terminal.Gui/App/ApplicationImpl.Driver.cs
  64. 251 0
      Terminal.Gui/App/ApplicationImpl.Lifecycle.cs
  65. 347 0
      Terminal.Gui/App/ApplicationImpl.Run.cs
  66. 177 0
      Terminal.Gui/App/ApplicationImpl.Screen.cs
  67. 44 488
      Terminal.Gui/App/ApplicationImpl.cs
  68. 15 7
      Terminal.Gui/App/CWP/CWPPropertyHelper.cs
  69. 2 2
      Terminal.Gui/App/Clipboard/ClipboardBase.cs
  70. 1 1
      Terminal.Gui/App/Clipboard/ClipboardProcessRunner.cs
  71. 440 163
      Terminal.Gui/App/IApplication.cs
  72. 1 1
      Terminal.Gui/App/IterationEventArgs.cs
  73. 2 2
      Terminal.Gui/App/Keyboard/IKeyboard.cs
  74. 4 1
      Terminal.Gui/App/Keyboard/KeyboardImpl.cs
  75. 35 39
      Terminal.Gui/App/MainLoop/ApplicationMainLoop.cs
  76. 54 19
      Terminal.Gui/App/MainLoop/IApplicationMainLoop.cs
  77. 1 2
      Terminal.Gui/App/MainLoop/IMainLoopCoordinator.cs
  78. 97 80
      Terminal.Gui/App/MainLoop/MainLoopCoordinator.cs
  79. 4 2
      Terminal.Gui/App/Mouse/MouseImpl.cs
  80. 0 12
      Terminal.Gui/App/RunStateEventArgs.cs
  81. 19 19
      Terminal.Gui/App/SessionToken.cs
  82. 12 0
      Terminal.Gui/App/SessionTokenEventArgs.cs
  83. 1 1
      Terminal.Gui/App/Timeout/ITimedEvents.cs
  84. 1 1
      Terminal.Gui/Drawing/Cell.cs
  85. 2 2
      Terminal.Gui/Drawing/Glyphs.cs
  86. 3 3
      Terminal.Gui/Drawing/LineCanvas/LineCanvas.cs
  87. 1 1
      Terminal.Gui/Drawing/Ruler.cs
  88. 1 1
      Terminal.Gui/Drawing/Sixel/SixelToRender.cs
  89. 1 1
      Terminal.Gui/Drawing/Thickness.cs
  90. 1 1
      Terminal.Gui/Drivers/AnsiHandling/AnsiEscapeSequenceRequest.cs
  91. 16 17
      Terminal.Gui/Drivers/AnsiHandling/AnsiResponseParser.cs
  92. 4 4
      Terminal.Gui/Drivers/AnsiHandling/EscSeqUtils/EscSeqUtils.cs
  93. 5 5
      Terminal.Gui/Drivers/AnsiHandling/GenericHeld.cs
  94. 0 26
      Terminal.Gui/Drivers/ComponentFactory.cs
  95. 25 0
      Terminal.Gui/Drivers/ComponentFactoryImpl.cs
  96. 0 753
      Terminal.Gui/Drivers/ConsoleDriver.cs
  97. 0 79
      Terminal.Gui/Drivers/ConsoleInput.cs
  98. 95 0
      Terminal.Gui/Drivers/ConsoleKeyInfoExtensions.cs
  99. 118 2315
      Terminal.Gui/Drivers/ConsoleKeyMapping.cs
  100. 4 1
      Terminal.Gui/Drivers/DotNetDriver/INetInput.cs

+ 3 - 21
.github/workflows/build.yml → .github/workflows/build-validation.yml

@@ -1,4 +1,4 @@
-name: Build Solution
+name: Build Validation
 
 on:
   push:
@@ -9,18 +9,11 @@ on:
     branches: [ v2_release, v2_develop ]
     paths-ignore:
       - '**.md'
-  workflow_call:
-    outputs:
-      artifact-name:
-        description: "Name of the build artifacts"
-        value: ${{ jobs.build.outputs.artifact-name }}
       
 jobs:
-  build:
-    name: Build Debug & Release
+  build-validation:
+    name: Build All Configurations
     runs-on: ubuntu-latest
-    outputs:
-      artifact-name: build-artifacts
     
     timeout-minutes: 10
     steps:
@@ -63,14 +56,3 @@ jobs:
 
     - name: Build Release Solution
       run: dotnet build --configuration Release --no-restore -property:NoWarn=0618%3B0612
-
-    - name: Upload build artifacts
-      uses: actions/upload-artifact@v4
-      with:
-        name: build-artifacts
-        path: |
-          **/bin/Debug/**
-          **/obj/Debug/**
-          **/bin/Release/**
-          **/obj/Release/**
-        retention-days: 1

+ 77 - 36
.github/workflows/integration-tests.yml

@@ -1,5 +1,4 @@
 name: Build & Run Integration Tests
-
 on:
   push:
     branches: [ v2_release, v2_develop ]
@@ -9,57 +8,99 @@ on:
     branches: [ v2_release, v2_develop ]
     paths-ignore:
       - '**.md'
-      
+
 jobs:
-  # Call the build workflow to build the solution once
   build:
-    uses: ./.github/workflows/build.yml
+    uses: ./.github/workflows/quick-build.yml
 
   integration_tests:
     name: Integration Tests
     runs-on: ${{ matrix.os }}
     needs: build
     strategy:
-      # Turn off fail-fast to let all runners run even if there are errors
-      fail-fast: true
+      fail-fast: false  # Let all OSes finish even if one fails
       matrix:
         os: [ ubuntu-latest, windows-latest, macos-latest ]
+    timeout-minutes: 15
 
-    timeout-minutes: 10
     steps:
+      - name: Checkout code
+        uses: actions/checkout@v4
+
+      - name: Setup .NET Core
+        uses: actions/setup-dotnet@v4
+        with:
+          dotnet-version: 8.x
+          dotnet-quality: ga
+
+      - name: Download build artifacts
+        uses: actions/download-artifact@v4
+        with:
+          name: test-build-artifacts
+          path: .
 
-    - name: Checkout code
-      uses: actions/checkout@v4
+      - name: Restore NuGet packages
+        run: dotnet restore
 
-    - name: Setup .NET Core
-      uses: actions/setup-dotnet@v4
-      with:
-        dotnet-version: 8.x
-        dotnet-quality: 'ga'
+      - name: Disable Windows Defender (Windows only)
+        if: runner.os == 'Windows'
+        shell: powershell
+        run: |
+          Add-MpPreference -ExclusionPath "${{ github.workspace }}"
+          Add-MpPreference -ExclusionProcess "dotnet.exe"
+          Add-MpPreference -ExclusionProcess "testhost.exe"
+          Add-MpPreference -ExclusionProcess "VSTest.Console.exe"
 
-    - name: Download build artifacts
-      uses: actions/download-artifact@v4
-      with:
-        name: build-artifacts
-        path: .
+      - name: Set VSTEST_DUMP_PATH
+        shell: bash
+        run: echo "VSTEST_DUMP_PATH=logs/IntegrationTests/${{ runner.os }}/" >> $GITHUB_ENV
 
-    - name: Restore NuGet packages
-      run: dotnet restore
+      - name: Run IntegrationTests
+        shell: bash
+        run: |
+          if [ "${{ runner.os }}" == "Linux" ]; then
+            # Run with coverage on Linux only
+            dotnet test Tests/IntegrationTests \
+              --no-build \
+              --verbosity minimal \
+              --collect:"XPlat Code Coverage" \
+              --settings Tests/IntegrationTests/runsettings.coverage.xml \
+              --diag:logs/IntegrationTests/${{ runner.os }}/logs.txt \
+              --blame \
+              --blame-crash \
+              --blame-hang \
+              --blame-hang-timeout 60s \
+              --blame-crash-collect-always
+          else
+            # Run without coverage on Windows/macOS for speed
+            dotnet test Tests/IntegrationTests \
+              --no-build \
+              --verbosity minimal \
+              --settings Tests/IntegrationTests/runsettings.xml \
+              --diag:logs/IntegrationTests/${{ runner.os }}/logs.txt \
+              --blame \
+              --blame-crash \
+              --blame-hang \
+              --blame-hang-timeout 60s \
+              --blame-crash-collect-always
+          fi
 
-    - name: Set VSTEST_DUMP_PATH
-      shell: bash
-      run: echo "{VSTEST_DUMP_PATH}={logs/${{ runner.os }}/}" >> $GITHUB_ENV
+      - name: Upload Integration Test Logs
+        if: always()
+        uses: actions/upload-artifact@v4
+        with:
+          name: integration_tests-logs-${{ runner.os }}
+          path: |
+            logs/IntegrationTests/
+            TestResults/
 
-    - name: Run IntegrationTests
-      run: |
-       dotnet test Tests/IntegrationTests --no-build --verbosity normal --diag:logs/${{ runner.os }}/logs.txt --blame --blame-crash --blame-hang --blame-hang-timeout 60s --blame-crash-collect-always -- xunit.stopOnFail=true
-     
-    - name: Upload Test Logs
-      if: always()
-      uses: actions/upload-artifact@v4
-      with:
-        name: integration-test-logs-${{ runner.os }}
-        path: |
-          logs/    
-          TestResults/IntegrationTests/
+      - name: Upload Integration Tests Coverage to Codecov
+        if: matrix.os == 'ubuntu-latest' && always()
+        uses: codecov/codecov-action@v4
+        with:
+          files: TestResults/**/coverage.cobertura.xml
+          flags: integrationtests
+          name: IntegrationTests-${{ runner.os }}
+          token: ${{ secrets.CODECOV_TOKEN }}
+          fail_ci_if_error: false
 

+ 43 - 0
.github/workflows/quick-build.yml

@@ -0,0 +1,43 @@
+name: Quick Build for Tests
+
+on:
+  workflow_call:
+    outputs:
+      artifact-name:
+        description: "Name of the build artifacts"
+        value: ${{ jobs.quick-build.outputs.artifact-name }}
+      
+jobs:
+  quick-build:
+    name: Build Debug Only
+    runs-on: ubuntu-latest
+    outputs:
+      artifact-name: test-build-artifacts
+    
+    timeout-minutes: 5
+    steps:
+
+    - name: Checkout code
+      uses: actions/checkout@v4
+
+    - name: Setup .NET Core
+      uses: actions/setup-dotnet@v4
+      with:
+        dotnet-version: 8.x
+        dotnet-quality: 'ga'
+
+    - name: Restore dependencies
+      run: dotnet restore
+
+    # Suppress CS0618 (member is obsolete) and CS0612 (member is obsolete without message)
+    - name: Build Debug
+      run: dotnet build --configuration Debug --no-restore -property:NoWarn=0618%3B0612
+
+    - name: Upload build artifacts
+      uses: actions/upload-artifact@v4
+      with:
+        name: test-build-artifacts
+        path: |
+          **/bin/Debug/**
+          **/obj/Debug/**
+        retention-days: 1

+ 104 - 26
.github/workflows/unit-tests.yml

@@ -11,9 +11,9 @@ on:
       - '**.md'
       
 jobs:
-  # Call the build workflow to build the solution once
+  # Call the quick-build workflow to build Debug configuration only
   build:
-    uses: ./.github/workflows/build.yml
+    uses: ./.github/workflows/quick-build.yml
 
   non_parallel_unittests:
     name: Non-Parallel Unit Tests  
@@ -21,11 +21,11 @@ jobs:
     needs: build
     strategy:
       # Turn off fail-fast to let all runners run even if there are errors
-      fail-fast: true
+      fail-fast: false
       matrix:
         os: [ ubuntu-latest, windows-latest, macos-latest ]
 
-    timeout-minutes: 10
+    timeout-minutes: 15  # Increased from 10 for Windows
     steps:
 
     - name: Checkout code
@@ -40,26 +40,56 @@ jobs:
     - name: Download build artifacts
       uses: actions/download-artifact@v4
       with:
-        name: build-artifacts
+        name: test-build-artifacts
         path: .
 
+    # KEEP THIS - It's needed for --no-build to work
     - name: Restore NuGet packages
       run: dotnet restore
 
-# Test
-    # Note: The --blame and VSTEST_DUMP_PATH stuff is needed to diagnose the test runner crashing on ubuntu/mac
-    # See https://github.com/microsoft/vstest/issues/2952 for why the --blame stuff below is needed.
-    # Without it, the test runner crashes on ubuntu (but not Windows or mac)
+    # Optimize Windows performance
+    - name: Disable Windows Defender (Windows only)
+      if: runner.os == 'Windows'
+      shell: powershell
+      run: |
+        Add-MpPreference -ExclusionPath "${{ github.workspace }}"
+        Add-MpPreference -ExclusionProcess "dotnet.exe"
+        Add-MpPreference -ExclusionProcess "testhost.exe"
+        Add-MpPreference -ExclusionProcess "VSTest.Console.exe"
 
     - name: Set VSTEST_DUMP_PATH
       shell: bash
-      run: echo "{VSTEST_DUMP_PATH}={logs/UnitTests/${{ runner.os }}/}" >> $GITHUB_ENV
+      run: echo "VSTEST_DUMP_PATH=logs/UnitTests/${{ runner.os }}/" >> $GITHUB_ENV
 
     - name: Run UnitTests
+      shell: bash
       run: |
-       dotnet test Tests/UnitTests --no-build --verbosity normal --collect:"XPlat Code Coverage" --settings Tests/UnitTests/coverlet.runsettings --diag:logs/UnitTests/${{ runner.os }}/logs.txt --blame --blame-crash --blame-hang --blame-hang-timeout 60s --blame-crash-collect-always -- xunit.stopOnFail=false
-     
-       # mv -v Tests/UnitTests/TestResults/*/*.* TestResults/UnitTests/
+        if [ "${{ runner.os }}" == "Linux" ]; then
+          # Run with coverage on Linux only
+          dotnet test Tests/UnitTests \
+            --no-build \
+            --verbosity normal \
+            --collect:"XPlat Code Coverage" \
+            --settings Tests/UnitTests/runsettings.xml \
+            --diag:logs/UnitTests/${{ runner.os }}/logs.txt \
+            --blame \
+            --blame-crash \
+            --blame-hang \
+            --blame-hang-timeout 60s \
+            --blame-crash-collect-always
+        else
+          # Run without coverage on Windows/macOS for speed
+          dotnet test Tests/UnitTests \
+            --no-build \
+            --verbosity normal \
+            --settings Tests/UnitTests/runsettings.xml \
+            --diag:logs/UnitTests/${{ runner.os }}/logs.txt \
+            --blame \
+            --blame-crash \
+            --blame-hang \
+            --blame-hang-timeout 120s \
+            --blame-crash-collect-always
+        fi
 
     - name: Upload Test Logs
       if: always()
@@ -68,19 +98,29 @@ jobs:
         name: non_parallel_unittests-logs-${{ runner.os }}
         path: |
           logs/UnitTests    
-          TestResults/UnitTests/
+          TestResults/
   
+    - name: Upload Non-Parallel UnitTests Coverage to Codecov
+      if: matrix.os == 'ubuntu-latest' && always()
+      uses: codecov/codecov-action@v4
+      with:
+        files: TestResults/**/coverage.cobertura.xml
+        flags: unittests-nonparallel
+        name: UnitTests-${{ runner.os }}
+        token: ${{ secrets.CODECOV_TOKEN }}
+        fail_ci_if_error: false
+
   parallel_unittests:
     name: Parallel Unit Tests  
     runs-on: ${{ matrix.os }}
     needs: build
     strategy:
       # Turn off fail-fast to let all runners run even if there are errors
-      fail-fast: true
+      fail-fast: false
       matrix:
         os: [ ubuntu-latest, windows-latest, macos-latest ]
 
-    timeout-minutes: 10
+    timeout-minutes: 15
     steps:
 
     - name: Checkout code
@@ -95,26 +135,54 @@ jobs:
     - name: Download build artifacts
       uses: actions/download-artifact@v4
       with:
-        name: build-artifacts
+        name: test-build-artifacts
         path: .
 
     - name: Restore NuGet packages
       run: dotnet restore
 
-# Test
-    # Note: The --blame and VSTEST_DUMP_PATH stuff is needed to diagnose the test runner crashing on ubuntu/mac
-    # See https://github.com/microsoft/vstest/issues/2952 for why the --blame stuff below is needed.
-    # Without it, the test runner crashes on ubuntu (but not Windows or mac)
+    - name: Disable Windows Defender (Windows only)
+      if: runner.os == 'Windows'
+      shell: powershell
+      run: |
+        Add-MpPreference -ExclusionPath "${{ github.workspace }}"
+        Add-MpPreference -ExclusionProcess "dotnet.exe"
+        Add-MpPreference -ExclusionProcess "testhost.exe"
+        Add-MpPreference -ExclusionProcess "VSTest.Console.exe"
 
     - name: Set VSTEST_DUMP_PATH
       shell: bash
-      run: echo "{VSTEST_DUMP_PATH}={logs/UnitTestsParallelizable/${{ runner.os }}/}" >> $GITHUB_ENV
+      run: echo "VSTEST_DUMP_PATH=logs/UnitTestsParallelizable/${{ runner.os }}/" >> $GITHUB_ENV
 
     - name: Run UnitTestsParallelizable
+      shell: bash
       run: |
-       dotnet test Tests/UnitTestsParallelizable --no-build --verbosity normal --collect:"XPlat Code Coverage" --settings Tests/UnitTestsParallelizable/coverlet.runsettings --diag:logs/UnitTestsParallelizable/${{ runner.os }}/logs.txt --blame --blame-crash --blame-hang --blame-hang-timeout 60s --blame-crash-collect-always -- xunit.stopOnFail=false
-     
-       # mv -v Tests/UnitTestsParallelizable/TestResults/*/*.* TestResults/UnitTestsParallelizable/
+        if [ "${{ runner.os }}" == "Linux" ]; then
+          # Run with coverage on Linux only
+          dotnet test Tests/UnitTestsParallelizable \
+            --no-build \
+            --verbosity normal \
+            --collect:"XPlat Code Coverage" \
+            --settings Tests/UnitTests/runsettings.coverage.xml \
+            --diag:logs/UnitTestsParallelizable/${{ runner.os }}/logs.txt \
+            --blame \
+            --blame-crash \
+            --blame-hang \
+            --blame-hang-timeout 60s \
+            --blame-crash-collect-always
+        else
+          # Run without coverage on Windows/macOS for speed
+          dotnet test Tests/UnitTestsParallelizable \
+            --no-build \
+            --verbosity normal \
+            --settings Tests/UnitTestsParallelizable/runsettings.xml \
+            --diag:logs/UnitTestsParallelizable/${{ runner.os }}/logs.txt \
+            --blame \
+            --blame-crash \
+            --blame-hang \
+            --blame-hang-timeout 60s \
+            --blame-crash-collect-always
+        fi
 
     - name: Upload UnitTestsParallelizable Logs
       if: always()
@@ -123,4 +191,14 @@ jobs:
         name: parallel_unittests-logs-${{ runner.os }}
         path: |
           logs/UnitTestsParallelizable/
-          TestResults/UnitTestsParallelizable/
+          TestResults/
+  
+    - name: Upload Parallelizable UnitTests Coverage to Codecov
+      if: matrix.os == 'ubuntu-latest' && always()
+      uses: codecov/codecov-action@v4
+      with:
+        files: TestResults/**/coverage.cobertura.xml
+        flags: unittests-parallel
+        name: UnitTestsParallelizable-${{ runner.os }}
+        token: ${{ secrets.CODECOV_TOKEN }}
+        fail_ci_if_error: false

+ 5 - 0
.gitignore

@@ -68,3 +68,8 @@ BenchmarkDotNet.Artifacts/
 *.log.*
 
 log.*
+
+/Tests/coverage/
+!/Tests/coverage/.gitkeep   # keep folder in repo
+/Tests/report/
+*.cobertura.xml

+ 58 - 49
CONTRIBUTING.md

@@ -23,11 +23,10 @@ Welcome! This guide provides everything you need to know to contribute effective
 
 ## Project Overview
 
-**Terminal.Gui** is a cross-platform UI toolkit for creating console-based graphical user interfaces in .NET. It's a large codebase (~1,050 C# files, 333MB) providing a comprehensive framework for building interactive console applications with support for keyboard and mouse input, customizable views, and a robust event system.
+**Terminal.Gui** is a cross-platform UI toolkit for creating console-based graphical user interfaces in .NET. It's a large codebase (~1,050 C# files) providing a comprehensive framework for building interactive console applications with support for keyboard and mouse input, customizable views, and a robust event system.
 
 **Key characteristics:**
 - **Language**: C# (net8.0)
-- **Size**: ~496 source files in core library, ~1,050 total C# files
 - **Platform**: Cross-platform (Windows, macOS, Linux)
 - **Architecture**: Console UI toolkit with driver-based architecture
 - **Version**: v2 (Alpha), v1 (maintenance mode)
@@ -88,25 +87,12 @@ Welcome! This guide provides everything you need to know to contribute effective
    dotnet test Tests/IntegrationTests --no-build --verbosity normal
    ```
 
-**Important**: Tests may take significant time. CI uses blame flags for crash detection:
-```bash
---diag:logs/UnitTests/logs.txt --blame --blame-crash --blame-hang --blame-hang-timeout 60s --blame-crash-collect-always
-```
-
 ### Common Build Issues
 
 #### Issue: Build Warnings
-- **Expected**: ~326 warnings (nullable refs, unused vars, xUnit suggestions)
+- **Expected**: None warnings (~100 currently).
 - **Action**: Don't add new warnings; fix warnings in code you modify
 
-#### Issue: Test Timeouts
-- **Expected**: Tests can take 5-10 minutes
-- **Action**: Use appropriate timeout values (60-120 seconds for test commands)
-
-#### Issue: Restore Failures
-- **Solution**: Ensure `dotnet restore` completes before building
-- **Note**: Takes 15-20 seconds on first run
-
 #### Issue: NativeAot/SelfContained Build
 - **Solution**: Restore these projects explicitly:
   ```bash
@@ -135,19 +121,18 @@ dotnet run --project Examples/UICatalog/UICatalog.csproj
 
 ### Code Formatting
 
+**⚠️ CRITICAL - These rules MUST be followed in ALL new or modified code:**
+
 - **Do NOT add formatting tools** - Use existing `.editorconfig` and `Terminal.sln.DotSettings`
 - Format code with:
   1. ReSharper/Rider (`Ctrl-E-C`)
   2. JetBrains CleanupCode CLI tool (free)
   3. Visual Studio (`Ctrl-K-D`) as fallback
 - **Only format files you modify**
-
-### Critical Coding Rules
-
-**⚠️ CRITICAL - These rules MUST be followed in ALL new or modified code:**
-
-#### Type Declarations and Object Creation
-
+- Follow `.editorconfig` settings (e.g., braces on new lines, spaces after keywords)
+- 4-space indentation
+- No trailing whitespace
+- File-scoped namespaces
 - **ALWAYS use explicit types** - Never use `var` except for built-in simple types (`int`, `string`, `bool`, `double`, `float`, `decimal`, `char`, `byte`)
   ```csharp
   // ✅ CORRECT - Explicit types
@@ -163,7 +148,7 @@ dotnet run --project Examples/UICatalog/UICatalog.csproj
   var views = new List<View?>();
   ```
 
-- **ALWAYS use target-typed `new()`** - Use `new ()` instead of `new TypeName()` when the type is already declared
+- **ALWAYS use target-typed `new ()`** - Use `new ()` instead of `new TypeName()` when the type is already declared
   ```csharp
   // ✅ CORRECT - Target-typed new
   View view = new () { Width = 10 };
@@ -174,13 +159,6 @@ dotnet run --project Examples/UICatalog/UICatalog.csproj
   MouseEventArgs args = new MouseEventArgs();
   ```
 
-#### Other Conventions
-
-- Follow `.editorconfig` settings (e.g., braces on new lines, spaces after keywords)
-- 4-space indentation
-- No trailing whitespace
-- File-scoped namespaces
-
 **⚠️ CRITICAL - These conventions apply to ALL code - production code, test code, examples, and samples.**
 
 ---
@@ -191,6 +169,11 @@ dotnet run --project Examples/UICatalog/UICatalog.csproj
 
 - **Never decrease code coverage** - PRs must maintain or increase coverage
 - Target: 70%+ coverage for new code
+- **Coverage collection**:
+- Centralized in `TestResults/` directory at repository root
+- Collected only on Linux (ubuntu-latest) runners in CI for performance
+- Windows and macOS runners skip coverage collection to reduce execution time
+- Coverage reports uploaded to Codecov automatically from Linux runner
 - CI monitors coverage on each PR
 
 ### Test Patterns
@@ -258,33 +241,58 @@ The repository uses multiple GitHub Actions workflows. What runs and when:
 - **Triggers**: push and pull_request to `v2_release`, `v2_develop` (ignores `**.md`); supports `workflow_call`
 - **Runner/timeout**: `ubuntu-latest`, 10 minutes
 - **Steps**:
-  - Checkout and setup .NET 8.x GA
-  - `dotnet restore`
-  - Build Debug: `dotnet build --configuration Debug --no-restore -property:NoWarn=0618%3B0612`
-  - Build Release (library): `dotnet build Terminal.Gui/Terminal.Gui.csproj --configuration Release --no-incremental --force -property:NoWarn=0618%3B0612`
-  - Pack Release: `dotnet pack Terminal.Gui/Terminal.Gui.csproj --configuration Release --output ./local_packages -property:NoWarn=0618%3B0612`
-  - Restore NativeAot/SelfContained examples, then restore solution again
-  - Build Release for `Examples/NativeAot` and `Examples/SelfContained`
-  - Build Release solution
-  - Upload artifacts named `build-artifacts`, retention 1 day
+- Checkout and setup .NET 8.x GA
+- `dotnet restore`
+- Build Debug: `dotnet build --configuration Debug --no-restore -property:NoWarn=0618%3B0612`
+- Build Release (library): `dotnet build Terminal.Gui/Terminal.Gui.csproj --configuration Release --no-incremental --force -property:NoWarn=0618%3B0612`
+- Pack Release: `dotnet pack Terminal.Gui/Terminal.Gui.csproj --configuration Release --output ./local_packages -property:NoWarn=0618%3B0612`
+- Restore NativeAot/SelfContained examples, then restore solution again
+- Build Release for `Examples/NativeAot` and `Examples/SelfContained`
+- Build Release solution
+- Upload artifacts named `build-artifacts`, retention 1 day
 
 ### 2) Build & Run Unit Tests (`.github/workflows/unit-tests.yml`)
 
 - **Triggers**: push and pull_request to `v2_release`, `v2_develop` (ignores `**.md`)
-- **Process**: Calls build workflow, then runs:
-  - Non-parallel UnitTests on Ubuntu/Windows/macOS matrix with coverage and blame/diag flags; `xunit.stopOnFail=false`
-  - Parallel UnitTestsParallelizable similarly with coverage; `xunit.stopOnFail=false`
-  - Uploads logs per-OS
+- **Matrix**: Ubuntu/Windows/macOS
+- **Timeout**: 15 minutes per job
+- **Process**:
+1. Calls build workflow to build solution once
+2. Downloads build artifacts
+3. Runs `dotnet restore` (required for `--no-build` to work)
+4. **Performance optimizations**:
+   - Disables Windows Defender on Windows runners (significant speedup)
+   - Collects code coverage **only on Linux** (ubuntu-latest) for performance
+   - Windows and macOS skip coverage collection to reduce test time
+   - Increased blame-hang-timeout to 120s for Windows/macOS (60s for Linux)
+5. Runs two test jobs:
+   - **Non-parallel UnitTests**: `Tests/UnitTests` with blame/diag flags; `xunit.stopOnFail=false`
+   - **Parallel UnitTestsParallelizable**: `Tests/UnitTestsParallelizable` with blame/diag flags; `xunit.stopOnFail=false`
+6. Uploads test logs and diagnostic data from all runners
+7. **Uploads code coverage to Codecov only from Linux runner**
+
+**Test results**: All tests output to unified `TestResults/` directory at repository root
 
 ### 3) Build & Run Integration Tests (`.github/workflows/integration-tests.yml`)
 
 - **Triggers**: push and pull_request to `v2_release`, `v2_develop` (ignores `**.md`)
-- **Process**: Calls build workflow, then runs IntegrationTests on matrix with blame/diag; `xunit.stopOnFail=true`
-- Uploads logs per-OS
+- **Matrix**: Ubuntu/Windows/macOS
+- **Timeout**: 15 minutes
+- **Process**:
+1. Calls build workflow
+2. Downloads build artifacts
+3. Runs `dotnet restore`
+4. **Performance optimizations** (same as unit tests):
+   - Disables Windows Defender on Windows runners
+   - Collects code coverage **only on Linux**
+   - Increased blame-hang-timeout to 120s for Windows/macOS
+5. Runs IntegrationTests with blame/diag flags; `xunit.stopOnFail=true`
+6. Uploads logs per-OS
+7. **Uploads coverage to Codecov only from Linux runner**
 
 ### 4) Publish to NuGet (`.github/workflows/publish.yml`)
 
-- **Triggers**: push to `v2_release`, `v2_develop`, and tags `v*` (ignores `**.md`)
+- **Triggers**: push to `v2_release`, `v2_develop`, and tags `v*`(ignores `**.md`)
 - Uses GitVersion to compute SemVer, builds Release, packs with symbols, and pushes to NuGet.org using `NUGET_API_KEY`
 
 ### 5) Build and publish API docs (`.github/workflows/api-docs.yml`)
@@ -292,6 +300,7 @@ The repository uses multiple GitHub Actions workflows. What runs and when:
 - **Triggers**: push to `v1_release` and `v2_develop`
 - Builds DocFX site on Windows and deploys to GitHub Pages when `ref_name` is `v2_release` or `v2_develop`
 
+
 ### Replicating CI Locally
 
 ```bash
@@ -323,9 +332,9 @@ dotnet build --configuration Release --no-restore
 ### Main Directories
 
 **`/Terminal.Gui/`** - Core library (496 C# files):
-- `App/` - Application lifecycle (`Application.cs` static class, `RunState`, `MainLoop`)
+- `App/` - Application lifecycle (`Application.cs` static class, `SessionToken`, `MainLoop`)
 - `Configuration/` - `ConfigurationManager` for settings
-- `Drivers/` - Console driver implementations (`IConsoleDriver`, `NetDriver`, `UnixDriver`, `WindowsDriver`)
+- `Drivers/` - Console driver implementations (`Dotnet`, `Windows`, `Unix`, `Fake`)
 - `Drawing/` - Rendering system (attributes, colors, glyphs)
 - `Input/` - Keyboard and mouse input handling
 - `ViewBase/` - Core `View` class hierarchy and layout

+ 2 - 2
Examples/NativeAot/Program.cs

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

+ 1 - 1
Examples/SelfContained/Program.cs

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

+ 9 - 16
Examples/UICatalog/Scenario.cs

@@ -189,32 +189,25 @@ public class Scenario : IDisposable
             }
 
             Application.Iteration += OnApplicationOnIteration;
-            Application.Driver!.ClearedContents += (sender, args) => BenchmarkResults.ClearedContentCount++;
 
-            if (Application.Driver is ConsoleDriver cd)
-            {
-                cd.Refreshed += (sender, args) =>
-                                                 {
-                                                     BenchmarkResults.RefreshedCount++;
-                                                     if (args.Value)
-                                                     {
-                                                         BenchmarkResults.UpdatedCount++;
-                                                     }
-                                                 };
-
-            }
-            Application.NotifyNewRunState += OnApplicationNotifyNewRunState;
+            Application.Driver!.ClearedContents += OnClearedContents;
+            Application.SessionBegun += OnApplicationSessionBegun;
 
 
             _stopwatch = Stopwatch.StartNew ();
         }
         else
         {
-            Application.NotifyNewRunState -= OnApplicationNotifyNewRunState;
+            Application.Driver!.ClearedContents -= OnClearedContents;
+            Application.SessionBegun -= OnApplicationSessionBegun;
             Application.Iteration -= OnApplicationOnIteration;
             BenchmarkResults.Duration = _stopwatch!.Elapsed;
             _stopwatch?.Stop ();
         }
+
+        return;
+
+        void OnClearedContents (object? sender, EventArgs args) => BenchmarkResults.ClearedContentCount++;
     }
 
     private void OnApplicationOnIteration (object? s, IterationEventArgs a)
@@ -226,7 +219,7 @@ public class Scenario : IDisposable
         }
     }
 
-    private void OnApplicationNotifyNewRunState (object? sender, RunStateEventArgs e)
+    private void OnApplicationSessionBegun (object? sender, SessionTokenEventArgs e)
     {
         SubscribeAllSubViews (Application.Top!);
 

+ 5 - 9
Examples/UICatalog/Scenarios/AllViewsTester.cs

@@ -158,16 +158,13 @@ public class AllViewsTester : Scenario
 
         _eventLog = new ()
         {
-            // X = Pos.Right(_layoutEditor),
+            X = Pos.AnchorEnd () - 1,
+            Y = 0,
+            Width = 30,
+            Height = Dim.Fill (),
             SuperViewRendersLineCanvas = true
         };
         _eventLog.Border!.Thickness = new (1);
-        _eventLog.X = Pos.AnchorEnd () - 1;
-        _eventLog.Y = 0;
-
-        _eventLog.Height = Dim.Height (_classListView);
-
-        //_eventLog.Width = 30;
 
         _layoutEditor.Width = Dim.Fill (
                                         Dim.Func (
@@ -194,7 +191,6 @@ public class AllViewsTester : Scenario
             Height = Dim.Fill (),
             CanFocus = true,
             TabStop = TabBehavior.TabStop,
-            //SchemeName = SchemeManager.SchemesToSchemeName (Schemes.Base),
             Arrangement = ViewArrangement.LeftResizable | ViewArrangement.BottomResizable | ViewArrangement.RightResizable,
             BorderStyle = LineStyle.Double,
             SuperViewRendersLineCanvas = true
@@ -228,7 +224,7 @@ public class AllViewsTester : Scenario
         if (type.IsGenericType)
         {
             // For each of the <T> arguments
-            List<Type> typeArguments = new ();
+            List<Type> typeArguments = [];
 
             // use <object> or the original type if applicable
             foreach (Type arg in type.GetGenericArguments ())

+ 2 - 2
Examples/UICatalog/Scenarios/Bars.cs

@@ -185,9 +185,9 @@ public class Bars : Scenario
 
         menuLikeExamples.Add (popOverMenu);
 
-        menuLikeExamples.MouseClick += MenuLikeExamplesMouseClick;
+        menuLikeExamples.MouseEvent += MenuLikeExamplesMouseEvent;
 
-        void MenuLikeExamplesMouseClick (object sender, MouseEventArgs e)
+        void MenuLikeExamplesMouseEvent (object _, MouseEventArgs e)
         {
             if (e.Flags.HasFlag (MouseFlags.Button3Clicked))
             {

+ 22 - 49
Examples/UICatalog/Scenarios/Buttons.cs

@@ -243,17 +243,17 @@ public class Buttons : Scenario
         };
         main.Add (label);
 
-        var radioGroup = new RadioGroup
+        OptionSelector<Alignment> osAlignment = new ()
         {
             X = 4,
             Y = Pos.Bottom (label) + 1,
-            SelectedItem = 2,
-            RadioLabels = new [] { "_Start", "_End", "_Center", "_Fill" },
-            Title = "_9 RadioGroup",
+            Value = Alignment.Center,
+            AssignHotKeys = true,
+            Title = "_9 OptionSelector",
             BorderStyle = LineStyle.Dotted,
             // CanFocus = false
         };
-        main.Add (radioGroup);
+        main.Add (osAlignment);
 
         // Demo changing hotkey
         string MoveHotkey (string txt)
@@ -292,7 +292,7 @@ public class Buttons : Scenario
         var moveHotKeyBtn = new Button
         {
             X = 2,
-            Y = Pos.Bottom (radioGroup) + 1,
+            Y = Pos.Bottom (osAlignment) + 1,
             Width = Dim.Width (computedFrame) - 2,
             SchemeName = "TopLevel",
             Text = mhkb
@@ -309,7 +309,7 @@ public class Buttons : Scenario
         var moveUnicodeHotKeyBtn = new Button
         {
             X = Pos.Left (absoluteFrame) + 1,
-            Y = Pos.Bottom (radioGroup) + 1,
+            Y = Pos.Bottom (osAlignment) + 1,
             Width = Dim.Width (absoluteFrame) - 2,
             SchemeName = "TopLevel",
             Text = muhkb
@@ -321,48 +321,21 @@ public class Buttons : Scenario
                                        };
         main.Add (moveUnicodeHotKeyBtn);
 
-        radioGroup.SelectedItemChanged += (s, args) =>
-                                          {
-                                              switch (args.SelectedItem)
-                                              {
-                                                  case 0:
-                                                      moveBtn.TextAlignment = Alignment.Start;
-                                                      sizeBtn.TextAlignment = Alignment.Start;
-                                                      moveBtnA.TextAlignment = Alignment.Start;
-                                                      sizeBtnA.TextAlignment = Alignment.Start;
-                                                      moveHotKeyBtn.TextAlignment = Alignment.Start;
-                                                      moveUnicodeHotKeyBtn.TextAlignment = Alignment.Start;
-
-                                                      break;
-                                                  case 1:
-                                                      moveBtn.TextAlignment = Alignment.End;
-                                                      sizeBtn.TextAlignment = Alignment.End;
-                                                      moveBtnA.TextAlignment = Alignment.End;
-                                                      sizeBtnA.TextAlignment = Alignment.End;
-                                                      moveHotKeyBtn.TextAlignment = Alignment.End;
-                                                      moveUnicodeHotKeyBtn.TextAlignment = Alignment.End;
-
-                                                      break;
-                                                  case 2:
-                                                      moveBtn.TextAlignment = Alignment.Center;
-                                                      sizeBtn.TextAlignment = Alignment.Center;
-                                                      moveBtnA.TextAlignment = Alignment.Center;
-                                                      sizeBtnA.TextAlignment = Alignment.Center;
-                                                      moveHotKeyBtn.TextAlignment = Alignment.Center;
-                                                      moveUnicodeHotKeyBtn.TextAlignment = Alignment.Center;
-
-                                                      break;
-                                                  case 3:
-                                                      moveBtn.TextAlignment = Alignment.Fill;
-                                                      sizeBtn.TextAlignment = Alignment.Fill;
-                                                      moveBtnA.TextAlignment = Alignment.Fill;
-                                                      sizeBtnA.TextAlignment = Alignment.Fill;
-                                                      moveHotKeyBtn.TextAlignment = Alignment.Fill;
-                                                      moveUnicodeHotKeyBtn.TextAlignment = Alignment.Fill;
-
-                                                      break;
-                                              }
-                                          };
+        osAlignment.ValueChanged += (s, args) =>
+                                    {
+                                        if (args.Value is null)
+                                        {
+                                            return;
+                                        }
+
+                                        Alignment newValue = args.Value.Value;
+                                        moveBtn.TextAlignment = newValue;
+                                        sizeBtn.TextAlignment = newValue;
+                                        moveBtnA.TextAlignment = newValue;
+                                        sizeBtnA.TextAlignment = newValue;
+                                        moveHotKeyBtn.TextAlignment = newValue;
+                                        moveUnicodeHotKeyBtn.TextAlignment = newValue;
+                                    };
 
         label = new ()
         {

+ 6 - 30
Examples/UICatalog/Scenarios/CharacterMap/CharacterMap.cs

@@ -365,40 +365,16 @@ public class CharacterMap : Scenario
         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
-        {
-            AssignHotKeysToCheckBoxes = true,
-            Options = options
-        };
+        // TODO: Add a "None" option
+        OptionSelector<UnicodeCategory> selector = new ();
 
         _unicodeCategorySelector = selector;
 
-        // Default to "All"
-        selector.SelectedItem = 0;
+        selector.Value = null;
         _charMap!.ShowUnicodeCategory = null;
 
-        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 };
+        selector.ValueChanged += (_, e) => _charMap.ShowUnicodeCategory = e.Value;
+
+        return new () { CommandView = selector };
     }
 }

+ 19 - 15
Examples/UICatalog/Scenarios/ColorPicker.cs

@@ -125,26 +125,25 @@ public class ColorPickers : Scenario
         app.Add (_demoView);
 
 
-        // Radio for switching color models
-        var rgColorModel = new RadioGroup ()
+        var osColorModel = new OptionSelector ()
         {
             Y = Pos.Bottom (_demoView),
             Width = Dim.Auto (),
             Height = Dim.Auto (),
-            RadioLabels = new []
-            {
+            Labels =
+            [
                 "_RGB",
                 "_HSV",
                 "H_SL",
                 "_16 Colors"
-            },
-            SelectedItem = (int)foregroundColorPicker.Style.ColorModel,
+            ],
+            Value = (int)foregroundColorPicker.Style.ColorModel,
         };
 
-        rgColorModel.SelectedItemChanged += (_, e) =>
+        osColorModel.ValueChanged += (_, e) =>
                                             {
                                                 // 16 colors
-                                                if (e.SelectedItem == 3)
+                                                if (e.Value == 3)
                                                 {
 
                                                     foregroundColorPicker16.Visible = true;
@@ -161,12 +160,17 @@ public class ColorPickers : Scenario
                                                 {
                                                     foregroundColorPicker16.Visible = false;
                                                     foregroundColorPicker.Visible = true;
-                                                    foregroundColorPicker.Style.ColorModel = (ColorModel)e.SelectedItem;
-                                                    foregroundColorPicker.ApplyStyleChanges ();
 
-                                                    backgroundColorPicker16.Visible = false;
-                                                    backgroundColorPicker.Visible = true;
-                                                    backgroundColorPicker.Style.ColorModel = (ColorModel)e.SelectedItem;
+                                                    if (e.Value is { })
+                                                    {
+                                                        foregroundColorPicker.Style.ColorModel = (ColorModel)e.Value;
+                                                        foregroundColorPicker.ApplyStyleChanges ();
+
+                                                        backgroundColorPicker16.Visible = false;
+                                                        backgroundColorPicker.Visible = true;
+                                                        backgroundColorPicker.Style.ColorModel = (ColorModel)e.Value;
+                                                    }
+
                                                     backgroundColorPicker.ApplyStyleChanges ();
 
 
@@ -176,13 +180,13 @@ public class ColorPickers : Scenario
                                                 }
                                             };
 
-        app.Add (rgColorModel);
+        app.Add (osColorModel);
 
         // Checkbox for switching show text fields on and off
         var cbShowTextFields = new CheckBox ()
         {
             Text = "Show _Text Fields",
-            Y = Pos.Bottom (rgColorModel) + 1,
+            Y = Pos.Bottom (osColorModel) + 1,
             Width = Dim.Auto (),
             Height = Dim.Auto (),
             CheckedState = foregroundColorPicker.Style.ShowTextFields ? CheckState.Checked : CheckState.UnChecked,

+ 7 - 10
Examples/UICatalog/Scenarios/Dialogs.cs

@@ -151,18 +151,15 @@ public class Dialogs : Scenario
         };
         frame.Add (label);
 
-        // Add hotkeys
-        var labels = Enum.GetNames<Alignment> ().Select (n => n = "_" + n);
-        var alignmentGroup = new RadioGroup
+        OptionSelector<Alignment> alignmentOptionSelector = new ()
         {
             X = Pos.Right (label) + 1,
             Y = Pos.Top (label),
-            RadioLabels = labels.ToArray (),
             Title = "Ali_gn",
-            BorderStyle = LineStyle.Dashed
+            AssignHotKeys = true
         };
-        frame.Add (alignmentGroup);
-        alignmentGroup.SelectedItem = labels.ToList ().IndexOf ("_" + Dialog.DefaultButtonAlignment.ToString ());
+        frame.Add (alignmentOptionSelector);
+        alignmentOptionSelector.Value = Dialog.DefaultButtonAlignment;
 
         frame.ValidatePosDim = true;
 
@@ -192,7 +189,7 @@ public class Dialogs : Scenario
                                                                       titleEdit,
                                                                       numButtonsEdit,
                                                                       glyphsNotWords,
-                                                                      alignmentGroup,
+                                                                      alignmentOptionSelector,
                                                                       buttonPressedLabel
                                                                      );
                                        Application.Run (dlg);
@@ -216,7 +213,7 @@ public class Dialogs : Scenario
         TextField titleEdit,
         TextField numButtonsEdit,
         CheckBox glyphsNotWords,
-        RadioGroup alignmentRadioGroup,
+        OptionSelector alignmentGroup,
         Label buttonPressedLabel
     )
     {
@@ -269,7 +266,7 @@ public class Dialogs : Scenario
             {
                 Title = titleEdit.Text,
                 Text = "Dialog Text",
-                ButtonAlignment = (Alignment)Enum.Parse (typeof (Alignment), alignmentRadioGroup.RadioLabels [alignmentRadioGroup.SelectedItem].Substring (1)),
+                ButtonAlignment = (Alignment)Enum.Parse (typeof (Alignment), alignmentGroup.Labels! [(int)alignmentGroup.Value!.Value] [1..]),
 
                 Buttons = buttons.ToArray ()
             };

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

@@ -163,15 +163,15 @@ public class DimAutoDemo : Scenario
         dimAutoFrameView.Add (resetButton);
 
 
-        var radioGroup = new RadioGroup ()
+        var optionSelector = new OptionSelector ()
         {
-            RadioLabels = ["One", "Two", "Three"],
+            Labels = ["One", "Two", "Three"],
             X = 0,
             Y = Pos.AnchorEnd (),
-            Title = "Radios",
+            Title = "Options",
             BorderStyle = LineStyle.Dotted
         };
-        dimAutoFrameView.Add (radioGroup);
+        dimAutoFrameView.Add (optionSelector);
         return dimAutoFrameView;
     }
 

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

@@ -174,11 +174,11 @@ public class DynamicMenuBar : Scenario
 
             var rChkLabels = new [] { "NoCheck", "Checked", "Radio" };
 
-            RbChkStyle = new ()
+            OsChkStyle = new ()
             {
-                X = Pos.Left (lblTitle), Y = Pos.Bottom (CkbSubMenu) + 1, RadioLabels = rChkLabels
+                X = Pos.Left (lblTitle), Y = Pos.Bottom (CkbSubMenu) + 1, Labels = rChkLabels
             };
-            Add (RbChkStyle);
+            Add (OsChkStyle);
 
             var lblShortcut = new Label
             {
@@ -294,7 +294,7 @@ public class DynamicMenuBar : Scenario
         public CheckBox CkbIsTopLevel { get; }
         public CheckBox CkbNullCheck { get; }
         public CheckBox CkbSubMenu { get; }
-        public RadioGroup RbChkStyle { get; }
+        public OptionSelector OsChkStyle { get; }
         public TextView TextAction { get; }
         public TextField TextHelp { get; }
         public TextField TextHotKey { get; }
@@ -361,7 +361,7 @@ public class DynamicMenuBar : Scenario
             CkbNullCheck.CheckedState = menuItem.AllowNullChecked ? CheckState.Checked : CheckState.UnChecked;
             TextHelp.Enabled = CkbSubMenu.CheckedState == CheckState.UnChecked;
             TextAction.Enabled = CkbSubMenu.CheckedState == CheckState.UnChecked;
-            RbChkStyle.SelectedItem = (int)(menuItem?.CheckType ?? MenuItemCheckStyle.NoCheck);
+            OsChkStyle.Value = (int)(menuItem?.CheckType ?? MenuItemCheckStyle.NoCheck);
             TextShortcutKey.Text = menuItem?.ShortcutTag ?? "";
 
             TextShortcutKey.Enabled = CkbIsTopLevel.CheckedState == CheckState.Checked && CkbSubMenu.CheckedState == CheckState.UnChecked
@@ -434,8 +434,8 @@ public class DynamicMenuBar : Scenario
                     HotKey = TextHotKey.Text,
                     IsTopLevel = CkbIsTopLevel?.CheckedState == CheckState.Checked,
                     HasSubMenu = CkbSubMenu?.CheckedState == CheckState.Checked,
-                    CheckStyle = RbChkStyle.SelectedItem == 0 ? MenuItemCheckStyle.NoCheck :
-                                 RbChkStyle.SelectedItem == 1 ? MenuItemCheckStyle.Checked :
+                    CheckStyle = OsChkStyle.Value == 0 ? MenuItemCheckStyle.NoCheck :
+                                 OsChkStyle.Value == 1 ? MenuItemCheckStyle.Checked :
                                  MenuItemCheckStyle.Radio,
                     ShortcutKey = TextShortcutKey.Text,
                     AllowNullChecked = CkbNullCheck?.CheckedState == CheckState.Checked,
@@ -484,7 +484,7 @@ public class DynamicMenuBar : Scenario
             TextHotKey.Text = "";
             CkbIsTopLevel.CheckedState = CheckState.UnChecked;
             CkbSubMenu.CheckedState = CheckState.UnChecked;
-            RbChkStyle.SelectedItem = (int)MenuItemCheckStyle.NoCheck;
+            OsChkStyle.Value = (int)MenuItemCheckStyle.NoCheck;
             TextShortcutKey.Text = "";
         }
 
@@ -829,9 +829,9 @@ public class DynamicMenuBar : Scenario
                                          HotKey = frmMenuDetails.TextHotKey.Text,
                                          IsTopLevel = frmMenuDetails.CkbIsTopLevel?.CheckedState == CheckState.Checked,
                                          HasSubMenu = frmMenuDetails.CkbSubMenu?.CheckedState == CheckState.Checked,
-                                         CheckStyle = frmMenuDetails.RbChkStyle.SelectedItem == 0
+                                         CheckStyle = frmMenuDetails.OsChkStyle.Value == 0
                                                           ? MenuItemCheckStyle.NoCheck
-                                                          : frmMenuDetails.RbChkStyle.SelectedItem == 1
+                                                          : frmMenuDetails.OsChkStyle.Value == 1
                                                               ? MenuItemCheckStyle.Checked
                                                               : MenuItemCheckStyle.Radio,
                                          ShortcutKey = frmMenuDetails.TextShortcutKey.Text

+ 13 - 79
Examples/UICatalog/Scenarios/EditorsAndHelpers/ArrangementEditor.cs

@@ -1,7 +1,4 @@
 #nullable enable
-using System;
-using System.Collections.Generic;
-
 namespace UICatalog.Scenarios;
 
 /// <summary>
@@ -16,92 +13,34 @@ public sealed class ArrangementEditor : EditorBase
 
         Initialized += ArrangementEditor_Initialized;
 
-        _arrangementSlider.Options =
-        [
-            new SliderOption<ViewArrangement>
-            {
-                Legend = ViewArrangement.Movable.ToString (),
-                Data = ViewArrangement.Movable
-            },
-
-            new SliderOption<ViewArrangement>
-            {
-                Legend = ViewArrangement.LeftResizable.ToString (),
-                Data = ViewArrangement.LeftResizable
-            },
-
-            new SliderOption<ViewArrangement>
-            {
-                Legend = ViewArrangement.RightResizable.ToString (),
-                Data = ViewArrangement.RightResizable
-            },
-
-            new SliderOption<ViewArrangement>
-            {
-                Legend = ViewArrangement.TopResizable.ToString (),
-                Data = ViewArrangement.TopResizable
-            },
-
-            new SliderOption<ViewArrangement>
-            {
-                Legend = ViewArrangement.BottomResizable.ToString (),
-                Data = ViewArrangement.BottomResizable
-            },
-
-            new SliderOption<ViewArrangement>
-            {
-                Legend = ViewArrangement.Overlapped.ToString (),
-                Data = ViewArrangement.Overlapped
-            }
-        ];
-
-        Add (_arrangementSlider);
+        Add (_arrangementSelector);
     }
 
-    private readonly Slider<ViewArrangement> _arrangementSlider = new()
+    private readonly FlagSelector<ViewArrangement> _arrangementSelector = new ()
     {
-        Orientation = Orientation.Vertical,
-        UseMinimumSize = true,
-        Type = SliderType.Multiple,
-        AllowEmpty = true,
+        Orientation = Orientation.Vertical
     };
 
     protected override void OnViewToEditChanged ()
     {
-        _arrangementSlider.Enabled = ViewToEdit is not Adornment;
+        _arrangementSelector.Enabled = ViewToEdit is not Adornment;
 
-        _arrangementSlider.OptionsChanged -= ArrangementSliderOnOptionsChanged;
+        _arrangementSelector.ValueChanged -= ArrangementFlagsOnValueChanged;
 
         // Set the appropriate options in the slider based on _viewToEdit.Arrangement
         if (ViewToEdit is { })
         {
-            _arrangementSlider.Options.ForEach (
-                                                option =>
-                                                {
-                                                    _arrangementSlider.ChangeOption (
-                                                                                     _arrangementSlider.Options.IndexOf (option),
-                                                                                     (ViewToEdit.Arrangement & option.Data) == option.Data);
-                                                });
+            _arrangementSelector.Value = ViewToEdit.Arrangement;
         }
 
-        _arrangementSlider.OptionsChanged += ArrangementSliderOnOptionsChanged;
+        _arrangementSelector.ValueChanged += ArrangementFlagsOnValueChanged;
     }
 
-    private void ArrangementEditor_Initialized (object? sender, EventArgs e) { _arrangementSlider.OptionsChanged += ArrangementSliderOnOptionsChanged; }
-
-    private void ArrangementSliderOnOptionsChanged (object? sender, SliderEventArgs<ViewArrangement> e)
+    private void ArrangementFlagsOnValueChanged (object? sender, EventArgs<ViewArrangement?> e)
     {
-        if (ViewToEdit is { })
+        if (ViewToEdit is { } && e.Value is { })
         {
-            // Set the arrangement based on the selected options
-            var arrangement = ViewArrangement.Fixed;
-
-            foreach (KeyValuePair<int, SliderOption<ViewArrangement>> option in e.Options)
-            {
-                arrangement |= option.Value.Data;
-            }
-
-            ViewToEdit.Arrangement = arrangement;
+            ViewToEdit.Arrangement = (ViewArrangement)e.Value;
 
             if (ViewToEdit.Arrangement.HasFlag (ViewArrangement.Overlapped))
             {
@@ -114,14 +53,9 @@ public sealed class ArrangementEditor : EditorBase
                 ViewToEdit.SchemeName = ViewToEdit!.SuperView!.SchemeName;
             }
 
-            if (ViewToEdit.Arrangement.HasFlag (ViewArrangement.Movable))
-            {
-                ViewToEdit.BorderStyle = LineStyle.Double;
-            }
-            else
-            {
-                ViewToEdit.BorderStyle = LineStyle.Single;
-            }
+            ViewToEdit.BorderStyle = ViewToEdit.Arrangement.HasFlag (ViewArrangement.Movable) ? LineStyle.Double : LineStyle.Single;
         }
     }
+
+    private void ArrangementEditor_Initialized (object? sender, EventArgs e) { _arrangementSelector.ValueChanged += ArrangementFlagsOnValueChanged; }
 }

+ 14 - 13
Examples/UICatalog/Scenarios/EditorsAndHelpers/BorderEditor.cs

@@ -8,7 +8,7 @@ namespace UICatalog.Scenarios;
 public class BorderEditor : AdornmentEditor
 {
     private CheckBox? _ckbTitle;
-    private RadioGroup? _rbBorderStyle;
+    private OptionSelector<LineStyle>? _osBorderStyle;
     private CheckBox? _ckbGradient;
 
     public BorderEditor ()
@@ -21,34 +21,31 @@ public class BorderEditor : AdornmentEditor
     private void BorderEditor_AdornmentChanged (object? sender, EventArgs e)
     {
         _ckbTitle!.CheckedState = ((Border)AdornmentToEdit!).Settings.FastHasFlags (BorderSettings.Title) ? CheckState.Checked : CheckState.UnChecked;
-        _rbBorderStyle!.SelectedItem = (int)((Border)AdornmentToEdit).LineStyle;
+        _osBorderStyle!.Value = ((Border)AdornmentToEdit).LineStyle;
         _ckbGradient!.CheckedState = ((Border)AdornmentToEdit).Settings.FastHasFlags (BorderSettings.Gradient) ? CheckState.Checked : CheckState.UnChecked;
     }
 
     private void BorderEditor_Initialized (object? sender, EventArgs e)
     {
-        List<LineStyle> borderStyleEnum = Enum.GetValues (typeof (LineStyle)).Cast<LineStyle> ().ToList ();
-
-        _rbBorderStyle = new ()
+        _osBorderStyle = new ()
         {
             X = 0,
 
-            Y = Pos.Bottom (SubViews.ToArray() [^1]),
+            Y = Pos.Bottom (SubViews.ToArray () [^1]),
             Width = Dim.Fill (),
-            SelectedItem = (int)(((Border)AdornmentToEdit!)?.LineStyle ?? LineStyle.None),
+            Value = ((Border)AdornmentToEdit!)?.LineStyle ?? LineStyle.None,
             BorderStyle = LineStyle.Single,
             Title = "Border St_yle",
             SuperViewRendersLineCanvas = true,
-            RadioLabels = borderStyleEnum.Select (style => style.ToString ()).ToArray ()
         };
-        Add (_rbBorderStyle);
+        Add (_osBorderStyle);
 
-        _rbBorderStyle.SelectedItemChanged += OnRbBorderStyleOnSelectedItemChanged;
+        _osBorderStyle.ValueChanged += OnRbBorderStyleOnValueChanged;
 
         _ckbTitle = new ()
         {
             X = 0,
-            Y = Pos.Bottom (_rbBorderStyle),
+            Y = Pos.Bottom (_osBorderStyle),
 
             CheckedState = CheckState.Checked,
             SuperViewRendersLineCanvas = true,
@@ -73,10 +70,14 @@ public class BorderEditor : AdornmentEditor
 
         return;
 
-        void OnRbBorderStyleOnSelectedItemChanged (object? s, SelectedItemChangedArgs args)
+        void OnRbBorderStyleOnValueChanged (object? s, EventArgs<LineStyle?> args)
         {
             LineStyle prevBorderStyle = AdornmentToEdit!.BorderStyle;
-            ((Border)AdornmentToEdit).LineStyle = (LineStyle)args.SelectedItem!;
+
+            if (args.Value is { })
+            {
+                ((Border)AdornmentToEdit).LineStyle = (LineStyle)args.Value;
+            }
 
             if (((Border)AdornmentToEdit).LineStyle == LineStyle.None)
             {

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

@@ -18,7 +18,7 @@ public class DimEditor : EditorBase
     }
 
     private int _value;
-    private RadioGroup? _dimRadioGroup;
+    private OptionSelector? _dimOptionSelector;
     private TextField? _valueEdit;
 
     /// <inheritdoc />
@@ -44,7 +44,7 @@ public class DimEditor : EditorBase
 
         try
         {
-            _dimRadioGroup!.SelectedItem = _dimNames.IndexOf (_dimNames.First (s => dim!.ToString ().StartsWith (s)));
+            _dimOptionSelector!.Value = _dimNames.IndexOf (_dimNames.First (s => dim!.ToString ().StartsWith (s)));
         }
         catch (InvalidOperationException e)
         {
@@ -92,13 +92,13 @@ public class DimEditor : EditorBase
             Text = $"{Title}:"
         };
         Add (label);
-        _dimRadioGroup = new () { X = 0, Y = Pos.Bottom (label), RadioLabels = _radioItems };
-        _dimRadioGroup.SelectedItemChanged += OnRadioGroupOnSelectedItemChanged;
+        _dimOptionSelector = new () { X = 0, Y = Pos.Bottom (label), Labels = _optionLabels };
+        _dimOptionSelector.ValueChanged += OnOptionSelectorOnValueChanged;
         _valueEdit = new ()
         {
             X = Pos.Right (label) + 1,
             Y = 0,
-            Width = Dim.Func (_ => _radioItems.Max (i => i.GetColumns ()) - label.Frame.Width + 1),
+            Width = Dim.Func (_ => _optionLabels.Max (i => i.GetColumns ()) - label.Frame.Width + 1),
             Text = $"{_value}"
         };
 
@@ -117,15 +117,15 @@ public class DimEditor : EditorBase
         };
         Add (_valueEdit);
 
-        Add (_dimRadioGroup);
+        Add (_dimOptionSelector);
 
     }
 
-    private void OnRadioGroupOnSelectedItemChanged (object? s, SelectedItemChangedArgs selected) { DimChanged (); }
+    private void OnOptionSelectorOnValueChanged (object? s, EventArgs<int?> selected) { DimChanged (); }
 
     // These need to have same order 
     private readonly List<string> _dimNames = ["Absolute", "Auto", "Fill", "Func", "Percent",];
-    private readonly string [] _radioItems = ["Absolute(n)", "Auto", "Fill(n)", "Func(()=>n)", "Percent(n)",];
+    private readonly string [] _optionLabels = ["Absolute(n)", "Auto", "Fill(n)", "Func(()=>n)", "Percent(n)",];
 
     private void DimChanged ()
     {
@@ -136,7 +136,7 @@ public class DimEditor : EditorBase
 
         try
         {
-            Dim? dim = _dimRadioGroup!.SelectedItem switch
+            Dim? dim = _dimOptionSelector!.Value switch
             {
                 0 => Dim.Absolute (_value),
                 1 => Dim.Auto (),

+ 14 - 22
Examples/UICatalog/Scenarios/EditorsAndHelpers/MarginEditor.cs

@@ -12,7 +12,7 @@ public class MarginEditor : AdornmentEditor
         AdornmentChanged += MarginEditor_AdornmentChanged;
     }
 
-    private RadioGroup? _rgShadow;
+    private OptionSelector<ShadowStyle>? _optionsShadow;
 
     private FlagSelector? _flagSelectorTransparent;
 
@@ -20,18 +20,18 @@ public class MarginEditor : AdornmentEditor
     {
         if (AdornmentToEdit is { })
         {
-            _rgShadow!.SelectedItem = (int)((Margin)AdornmentToEdit).ShadowStyle;
+            _optionsShadow!.Value = ((Margin)AdornmentToEdit).ShadowStyle;
         }
 
         if (AdornmentToEdit is { })
         {
-            _flagSelectorTransparent!.Value = (uint)((Margin)AdornmentToEdit).ViewportSettings;
+            _flagSelectorTransparent!.Value = (int)((Margin)AdornmentToEdit).ViewportSettings;
         }
     }
 
     private void MarginEditor_Initialized (object? sender, EventArgs e)
     {
-        _rgShadow = new RadioGroup
+        _optionsShadow = new ()
         {
             X = 0,
             Y = Pos.Bottom (SubViews.ElementAt(SubViews.Count-1)),
@@ -39,44 +39,36 @@ public class MarginEditor : AdornmentEditor
             SuperViewRendersLineCanvas = true,
             Title = "_Shadow",
             BorderStyle = LineStyle.Single,
-            RadioLabels = Enum.GetNames (typeof (ShadowStyle)),
+            AssignHotKeys = true
         };
 
         if (AdornmentToEdit is { })
         {
-            _rgShadow.SelectedItem = (int)((Margin)AdornmentToEdit).ShadowStyle;
+            _optionsShadow.Value = ((Margin)AdornmentToEdit).ShadowStyle;
         }
 
-        _rgShadow.SelectedItemChanged += (_, args) =>
-                                        {
-                                            ((Margin)AdornmentToEdit!).ShadowStyle = (ShadowStyle)args.SelectedItem!;
-                                        };
+        _optionsShadow.ValueChanged += (_, args) => ((Margin)AdornmentToEdit!).ShadowStyle = args.Value!.Value;
 
-        Add (_rgShadow);
+        Add (_optionsShadow);
 
-        var flags = new Dictionary<uint, string> ()
-        {
-            { (uint)Terminal.Gui.ViewBase.ViewportSettingsFlags.Transparent, "Transparent" },
-            { (uint)Terminal.Gui.ViewBase.ViewportSettingsFlags.TransparentMouse, "TransparentMouse" }
-        };
-
-        _flagSelectorTransparent = new FlagSelector ()
+        _flagSelectorTransparent = new FlagSelector<ViewportSettingsFlags> ()
         {
             X = 0,
-            Y = Pos.Bottom (_rgShadow),
+            Y = Pos.Bottom (_optionsShadow),
 
             SuperViewRendersLineCanvas = true,
             Title = "_ViewportSettings",
             BorderStyle = LineStyle.Single,
         };
-        _flagSelectorTransparent.SetFlags(flags.AsReadOnly ());
-
+        _flagSelectorTransparent.Values = [(int)ViewportSettingsFlags.Transparent, (int)ViewportSettingsFlags.TransparentMouse];
+        _flagSelectorTransparent.Labels = ["Transparent", "TransparentMouse"];
+        _flagSelectorTransparent.AssignHotKeys = true;
 
         Add (_flagSelectorTransparent);
 
         if (AdornmentToEdit is { })
         {
-            _flagSelectorTransparent.Value = (uint)((Margin)AdornmentToEdit).ViewportSettings;
+            _flagSelectorTransparent.Value = (int)((Margin)AdornmentToEdit).ViewportSettings;
         }
 
         _flagSelectorTransparent.ValueChanged += (_, args) =>

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

@@ -19,7 +19,7 @@ public class PosEditor : EditorBase
     }
 
     private int _value;
-    private RadioGroup? _posRadioGroup;
+    private OptionSelector? _posOptionSelector;
     private TextField? _valueEdit;
 
     protected override void OnUpdateLayoutSettings ()
@@ -44,7 +44,7 @@ public class PosEditor : EditorBase
 
         try
         {
-            _posRadioGroup!.SelectedItem = _posNames.IndexOf (_posNames.First (s => pos.ToString ().Contains (s)));
+            _posOptionSelector!.Value = _posNames.IndexOf (_posNames.First (s => pos.ToString ().Contains (s)));
         }
         catch (InvalidOperationException e)
         {
@@ -91,14 +91,14 @@ public class PosEditor : EditorBase
             Text = $"{Title}:"
         };
         Add (label);
-        _posRadioGroup = new () { X = 0, Y = Pos.Bottom (label), RadioLabels = _radioItems };
-        _posRadioGroup.SelectedItemChanged += OnRadioGroupOnSelectedItemChanged;
+        _posOptionSelector = new () { X = 0, Y = Pos.Bottom (label), Labels = _optionLabels };
+        _posOptionSelector.ValueChanged += OnOptionSelectorOnValueChanged;
 
         _valueEdit = new ()
         {
             X = Pos.Right (label) + 1,
             Y = 0,
-            Width = Dim.Func (_ => _radioItems.Max (i => i.GetColumns ()) - label.Frame.Width + 1),
+            Width = Dim.Func (_ => _optionLabels.Max (i => i.GetColumns ()) - label.Frame.Width + 1),
             Text = $"{_value}"
         };
 
@@ -118,14 +118,14 @@ public class PosEditor : EditorBase
                                 };
         Add (_valueEdit);
 
-        Add (_posRadioGroup);
+        Add (_posOptionSelector);
     }
 
-    private void OnRadioGroupOnSelectedItemChanged (object? s, SelectedItemChangedArgs selected) { PosChanged (); }
+    private void OnOptionSelectorOnValueChanged (object? s, EventArgs<int?> selected) { PosChanged (); }
 
     // These need to have same order 
     private readonly List<string> _posNames = ["Absolute", "Align", "AnchorEnd", "Center", "Func", "Percent"];
-    private readonly string [] _radioItems = ["Absolute(n)", "Align", "AnchorEnd", "Center", "Func(()=>n)", "Percent(n)"];
+    private readonly string [] _optionLabels = ["Absolute(n)", "Align", "AnchorEnd", "Center", "Func(()=>n)", "Percent(n)"];
 
     private void PosChanged ()
     {
@@ -136,7 +136,7 @@ public class PosEditor : EditorBase
 
         try
         {
-            Pos? pos = _posRadioGroup!.SelectedItem switch
+            Pos? pos = _posOptionSelector!.Value switch
                        {
                            0 => Pos.Absolute (_value),
                            1 => Pos.Align (Alignment.Start),

+ 9 - 10
Examples/UICatalog/Scenarios/EditorsAndHelpers/ViewPropertiesEditor.cs

@@ -6,7 +6,7 @@ public class ViewPropertiesEditor : EditorBase
 {
     private CheckBox? _canFocusCheckBox;
     private CheckBox? _enabledCheckBox;
-    private RadioGroup? _orientation;
+    private OptionSelector<Orientation>? _orientationOptionSelector;
     private TextView? _text;
 
     /// <inheritdoc/>
@@ -48,24 +48,23 @@ public class ViewPropertiesEditor : EditorBase
 
         Label label = new () { X = Pos.Right (_enabledCheckBox) + 1, Y = Pos.Top (_enabledCheckBox), Text = "Orientation:" };
 
-        _orientation = new ()
+        _orientationOptionSelector = new ()
         {
             X = Pos.Right (label) + 1,
             Y = Pos.Top (label),
-            RadioLabels = new [] { "Horizontal", "Vertical" },
             Orientation = Orientation.Horizontal
         };
 
-        _orientation.SelectedItemChanged += (s, selected) =>
+        _orientationOptionSelector.ValueChanged += (s, selected) =>
                                             {
                                                 if (ViewToEdit is IOrientation orientatedView)
                                                 {
-                                                    orientatedView.Orientation = (Orientation)_orientation.SelectedItem!;
+                                                    orientatedView.Orientation = _orientationOptionSelector.Value!.Value;
                                                 }
                                             };
-        Add (label, _orientation);
+        Add (label, _orientationOptionSelector);
 
-        label = new () { X = 0, Y = Pos.Bottom (_orientation), Text = "Text:" };
+        label = new () { X = 0, Y = Pos.Bottom (_orientationOptionSelector), Text = "Text:" };
 
         _text = new ()
         {
@@ -114,12 +113,12 @@ public class ViewPropertiesEditor : EditorBase
 
             if (ViewToEdit is IOrientation orientatedView)
             {
-                _orientation!.SelectedItem = (int)orientatedView.Orientation;
-                _orientation.Enabled = true;
+                _orientationOptionSelector!.Value = orientatedView.Orientation;
+                _orientationOptionSelector.Enabled = true;
             }
             else
             {
-                _orientation!.Enabled = false;
+                _orientationOptionSelector!.Enabled = false;
             }
         }
     }

+ 133 - 133
Examples/UICatalog/Scenarios/FileDialogExamples.cs

@@ -1,8 +1,4 @@
-using System;
-using System.IO;
-using System.IO.Abstractions;
-using System.Linq;
-using System.Text;
+using System.IO.Abstractions;
 
 namespace UICatalog.Scenarios;
 
@@ -20,10 +16,10 @@ public class FileDialogExamples : Scenario
     private CheckBox _cbMustExist;
     private CheckBox _cbShowTreeBranchLines;
     private CheckBox _cbUseColors;
-    private RadioGroup _rgAllowedTypes;
-    private RadioGroup _rgCaption;
-    private RadioGroup _rgIcons;
-    private RadioGroup _rgOpenMode;
+    private OptionSelector _osAllowedTypes;
+    private OptionSelector _osCaption;
+    private OptionSelector _osIcons;
+    private OptionSelector _osOpenMode;
     private TextField _tbCancelButton;
     private TextField _tbOkButton;
 
@@ -34,28 +30,29 @@ public class FileDialogExamples : Scenario
         var x = 1;
         var win = new Window { Title = GetQuitKeyAndName () };
 
-        _cbMustExist = new CheckBox { CheckedState = CheckState.Checked, Y = y++, X = x, Text = "Must E_xist" };
+        _cbMustExist = new () { CheckedState = CheckState.Checked, Y = y++, X = x, Text = "Must E_xist" };
         win.Add (_cbMustExist);
 
-        _cbUseColors = new CheckBox { CheckedState = FileDialogStyle.DefaultUseColors ? CheckState.Checked : CheckState.UnChecked, Y = y++, X = x, Text = "_Use Colors" };
+        _cbUseColors = new ()
+            { CheckedState = FileDialogStyle.DefaultUseColors ? CheckState.Checked : CheckState.UnChecked, Y = y++, X = x, Text = "_Use Colors" };
         win.Add (_cbUseColors);
 
-        _cbCaseSensitive = new CheckBox { CheckedState = CheckState.UnChecked, Y = y++, X = x, Text = "_Case Sensitive Search" };
+        _cbCaseSensitive = new () { CheckedState = CheckState.UnChecked, Y = y++, X = x, Text = "_Case Sensitive Search" };
         win.Add (_cbCaseSensitive);
 
-        _cbAllowMultipleSelection = new CheckBox { CheckedState = CheckState.UnChecked, Y = y++, X = x, Text = "_Multiple" };
+        _cbAllowMultipleSelection = new () { CheckedState = CheckState.UnChecked, Y = y++, X = x, Text = "_Multiple" };
         win.Add (_cbAllowMultipleSelection);
 
-        _cbShowTreeBranchLines = new CheckBox { CheckedState = CheckState.Checked, Y = y++, X = x, Text = "Tree Branch _Lines" };
+        _cbShowTreeBranchLines = new () { CheckedState = CheckState.Checked, Y = y++, X = x, Text = "Tree Branch _Lines" };
         win.Add (_cbShowTreeBranchLines);
 
-        _cbAlwaysTableShowHeaders = new CheckBox { CheckedState = CheckState.Checked, Y = y++, X = x, Text = "Always Show _Headers" };
+        _cbAlwaysTableShowHeaders = new () { CheckedState = CheckState.Checked, Y = y++, X = x, Text = "Always Show _Headers" };
         win.Add (_cbAlwaysTableShowHeaders);
 
-        _cbDrivesOnlyInTree = new CheckBox { CheckedState = CheckState.UnChecked, Y = y++, X = x, Text = "Only Show _Drives" };
+        _cbDrivesOnlyInTree = new () { CheckedState = CheckState.UnChecked, Y = y++, X = x, Text = "Only Show _Drives" };
         win.Add (_cbDrivesOnlyInTree);
 
-        _cbPreserveFilenameOnDirectoryChanges = new CheckBox { CheckedState = CheckState.UnChecked, Y = y++, X = x, Text = "Preserve Filename" };
+        _cbPreserveFilenameOnDirectoryChanges = new () { CheckedState = CheckState.UnChecked, Y = y++, X = x, Text = "Preserve Filename" };
         win.Add (_cbPreserveFilenameOnDirectoryChanges);
 
         y = 0;
@@ -66,9 +63,9 @@ public class FileDialogExamples : Scenario
                 );
         win.Add (new Label { X = x++, Y = y++, Text = "Caption" });
 
-        _rgCaption = new RadioGroup { X = x, Y = y };
-        _rgCaption.RadioLabels = new [] { "_Ok", "O_pen", "_Save" };
-        win.Add (_rgCaption);
+        _osCaption = new () { X = x, Y = y };
+        _osCaption.Labels = ["_Ok", "O_pen", "_Save"];
+        win.Add (_osCaption);
 
         y = 0;
         x = 34;
@@ -78,9 +75,9 @@ public class FileDialogExamples : Scenario
                 );
         win.Add (new Label { X = x++, Y = y++, Text = "OpenMode" });
 
-        _rgOpenMode = new RadioGroup { X = x, Y = y };
-        _rgOpenMode.RadioLabels = new [] { "_File", "D_irectory", "_Mixed" };
-        win.Add (_rgOpenMode);
+        _osOpenMode = new () { X = x, Y = y };
+        _osOpenMode.Labels = ["_File", "D_irectory", "_Mixed"];
+        win.Add (_osOpenMode);
 
         y = 0;
         x = 48;
@@ -90,9 +87,9 @@ public class FileDialogExamples : Scenario
                 );
         win.Add (new Label { X = x++, Y = y++, Text = "Icons" });
 
-        _rgIcons = new RadioGroup { X = x, Y = y };
-        _rgIcons.RadioLabels = new [] { "_None", "_Unicode", "Nerd_*" };
-        win.Add (_rgIcons);
+        _osIcons = new () { X = x, Y = y };
+        _osIcons.Labels = ["_None", "_Unicode", "Nerd_*"];
+        win.Add (_osIcons);
 
         win.Add (new Label { Y = Pos.AnchorEnd (2), Text = "* Requires installing Nerd fonts" });
         win.Add (new Label { Y = Pos.AnchorEnd (1), Text = "  (see: https://github.com/devblackops/Terminal-Icons)" });
@@ -105,9 +102,9 @@ public class FileDialogExamples : Scenario
                 );
         win.Add (new Label { X = x++, Y = y++, Text = "Allowed" });
 
-        _rgAllowedTypes = new RadioGroup { X = x, Y = y };
-        _rgAllowedTypes.RadioLabels = new [] { "An_y", "Cs_v (Recommended)", "Csv (S_trict)" };
-        win.Add (_rgAllowedTypes);
+        _osAllowedTypes = new () { X = x, Y = y };
+        _osAllowedTypes.Labels = ["An_y", "Cs_v (Recommended)", "Csv (S_trict)"];
+        win.Add (_osAllowedTypes);
 
         y = 5;
         x = 45;
@@ -118,31 +115,31 @@ public class FileDialogExamples : Scenario
         win.Add (new Label { X = x++, Y = y++, Text = "Buttons" });
 
         win.Add (new Label { X = x, Y = y++, Text = "O_k Text:" });
-        _tbOkButton = new TextField { X = x, Y = y++, Width = 12 };
+        _tbOkButton = new () { X = x, Y = y++, Width = 12 };
         win.Add (_tbOkButton);
         win.Add (new Label { X = x, Y = y++, Text = "_Cancel Text:" });
-        _tbCancelButton = new TextField { X = x, Y = y++, Width = 12 };
+        _tbCancelButton = new () { X = x, Y = y++, Width = 12 };
         win.Add (_tbCancelButton);
-        _cbFlipButtonOrder = new CheckBox { X = x, Y = y++, Text = "Flip Ord_er" };
+        _cbFlipButtonOrder = new () { X = x, Y = y++, Text = "Flip Ord_er" };
         win.Add (_cbFlipButtonOrder);
 
         var btn = new Button { X = 1, Y = 9, IsDefault = true, Text = "Run Dialog" };
 
         win.Accepting += (s, e) =>
-                        {
-                            try
-                            {
-                                CreateDialog ();
-                            }
-                            catch (Exception ex)
-                            {
-                                MessageBox.ErrorQuery ("Error", ex.ToString (), "_Ok");
-                            }
-                            finally
-                            {
-                                e.Handled = true;
-                            }
-                        };
+                         {
+                             try
+                             {
+                                 CreateDialog ();
+                             }
+                             catch (Exception ex)
+                             {
+                                 MessageBox.ErrorQuery ("Error", ex.ToString (), "_Ok");
+                             }
+                             finally
+                             {
+                                 e.Handled = true;
+                             }
+                         };
         win.Add (btn);
 
         Application.Run (win);
@@ -164,112 +161,115 @@ public class FileDialogExamples : Scenario
 
     private void CreateDialog ()
     {
-        var fd = new FileDialog
+        if (_osOpenMode.Value is { })
         {
-            OpenMode = Enum.Parse<OpenMode> (
-                                             _rgOpenMode.RadioLabels
-                                                        .Select (l => TextFormatter.FindHotKey (l, _rgOpenMode.HotKeySpecifier, out int hotPos, out Key _)
-
-                                                                          // Remove the hotkey specifier at the found position
-                                                                          ? TextFormatter.RemoveHotKeySpecifier (l, hotPos, _rgOpenMode.HotKeySpecifier)
-
-                                                                          // No hotkey found, return the label as is
-                                                                          : l)
-                                                        .ToArray () [_rgOpenMode.SelectedItem]
-                                            ),
-            MustExist = _cbMustExist.CheckedState == CheckState.Checked,
-            AllowsMultipleSelection = _cbAllowMultipleSelection.CheckedState == CheckState.Checked
-        };
+            var fd = new FileDialog
+            {
+                OpenMode = Enum.Parse<OpenMode> (
+                                                 _osOpenMode.Labels
+                                                            .Select (l => TextFormatter.FindHotKey (l, _osOpenMode.HotKeySpecifier, out int hotPos, out Key _)
+
+                                                                              // Remove the hotkey specifier at the found position
+                                                                              ? TextFormatter.RemoveHotKeySpecifier (l, hotPos, _osOpenMode.HotKeySpecifier)
+
+                                                                              // No hotkey found, return the label as is
+                                                                              : l)
+                                                            .ToArray () [_osOpenMode.Value.Value]
+                                                ),
+                MustExist = _cbMustExist.CheckedState == CheckState.Checked,
+                AllowsMultipleSelection = _cbAllowMultipleSelection.CheckedState == CheckState.Checked
+            };
+
+            fd.Style.OkButtonText =
+                _osCaption.Labels.Select (l => TextFormatter.RemoveHotKeySpecifier (l, 0, _osCaption.HotKeySpecifier)).ToArray ()
+                    [_osCaption.Value!.Value];
+
+            // If Save style dialog then give them an overwrite prompt
+            if (_osCaption.Value == 2)
+            {
+                fd.FilesSelected += ConfirmOverwrite;
+            }
 
-        fd.Style.OkButtonText = _rgCaption.RadioLabels.Select (l => TextFormatter.RemoveHotKeySpecifier(l, 0, _rgCaption.HotKeySpecifier)).ToArray() [_rgCaption.SelectedItem];
+            fd.Style.IconProvider.UseUnicodeCharacters = _osIcons.Value == 1;
+            fd.Style.IconProvider.UseNerdIcons = _osIcons.Value == 2;
 
-        // If Save style dialog then give them an overwrite prompt
-        if (_rgCaption.SelectedItem == 2)
-        {
-            fd.FilesSelected += ConfirmOverwrite;
-        }
-
-        fd.Style.IconProvider.UseUnicodeCharacters = _rgIcons.SelectedItem == 1;
-        fd.Style.IconProvider.UseNerdIcons = _rgIcons.SelectedItem == 2;
+            if (_cbCaseSensitive.CheckedState == CheckState.Checked)
+            {
+                fd.SearchMatcher = new CaseSensitiveSearchMatcher ();
+            }
 
-        if (_cbCaseSensitive.CheckedState == CheckState.Checked)
-        {
-            fd.SearchMatcher = new CaseSensitiveSearchMatcher ();
-        }
+            fd.Style.UseColors = _cbUseColors.CheckedState == CheckState.Checked;
 
-        fd.Style.UseColors = _cbUseColors.CheckedState == CheckState.Checked;
+            fd.Style.TreeStyle.ShowBranchLines = _cbShowTreeBranchLines.CheckedState == CheckState.Checked;
+            fd.Style.TableStyle.AlwaysShowHeaders = _cbAlwaysTableShowHeaders.CheckedState == CheckState.Checked;
 
-        fd.Style.TreeStyle.ShowBranchLines = _cbShowTreeBranchLines.CheckedState == CheckState.Checked;
-        fd.Style.TableStyle.AlwaysShowHeaders = _cbAlwaysTableShowHeaders.CheckedState == CheckState.Checked;
+            IDirectoryInfoFactory dirInfoFactory = new FileSystem ().DirectoryInfo;
 
-        IDirectoryInfoFactory dirInfoFactory = new FileSystem ().DirectoryInfo;
+            if (_cbDrivesOnlyInTree.CheckedState == CheckState.Checked)
+            {
+                fd.Style.TreeRootGetter = () => { return Environment.GetLogicalDrives ().ToDictionary (dirInfoFactory.New, k => k); };
+            }
 
-        if (_cbDrivesOnlyInTree.CheckedState == CheckState.Checked)
-        {
-            fd.Style.TreeRootGetter = () => { return Environment.GetLogicalDrives ().ToDictionary (dirInfoFactory.New, k => k); };
-        }
+            fd.Style.PreserveFilenameOnDirectoryChanges = _cbPreserveFilenameOnDirectoryChanges.CheckedState == CheckState.Checked;
 
-        fd.Style.PreserveFilenameOnDirectoryChanges = _cbPreserveFilenameOnDirectoryChanges.CheckedState == CheckState.Checked;
-        
+            if (_osAllowedTypes.Value > 0)
+            {
+                fd.AllowedTypes.Add (new AllowedType ("Data File", ".csv", ".tsv"));
 
-        if (_rgAllowedTypes.SelectedItem > 0)
-        {
-            fd.AllowedTypes.Add (new AllowedType ("Data File", ".csv", ".tsv"));
+                if (_osAllowedTypes.Value == 1)
+                {
+                    fd.AllowedTypes.Insert (1, new AllowedTypeAny ());
+                }
+            }
 
-            if (_rgAllowedTypes.SelectedItem == 1)
+            if (!string.IsNullOrWhiteSpace (_tbOkButton.Text))
             {
-                fd.AllowedTypes.Insert (1, new AllowedTypeAny ());
+                fd.Style.OkButtonText = _tbOkButton.Text;
             }
-        }
 
-        if (!string.IsNullOrWhiteSpace (_tbOkButton.Text))
-        {
-            fd.Style.OkButtonText = _tbOkButton.Text;
-        }
-
-        if (!string.IsNullOrWhiteSpace (_tbCancelButton.Text))
-        {
-            fd.Style.CancelButtonText = _tbCancelButton.Text;
-        }
-
-        if (_cbFlipButtonOrder.CheckedState == CheckState.Checked)
-        {
-            fd.Style.FlipOkCancelButtonLayoutOrder = true;
-        }
+            if (!string.IsNullOrWhiteSpace (_tbCancelButton.Text))
+            {
+                fd.Style.CancelButtonText = _tbCancelButton.Text;
+            }
 
-        Application.Run (fd);
+            if (_cbFlipButtonOrder.CheckedState == CheckState.Checked)
+            {
+                fd.Style.FlipOkCancelButtonLayoutOrder = true;
+            }
 
-        var canceled = fd.Canceled;
-        var multiSelected = fd.MultiSelected;
-        var path = fd.Path;
+            Application.Run (fd);
 
-        // This needs to be disposed before opening other toplevel
-        fd.Dispose ();
+            bool canceled = fd.Canceled;
+            IReadOnlyList<string> multiSelected = fd.MultiSelected;
+            string path = fd.Path;
 
-        if (canceled)
-        {
-            MessageBox.Query (
-                              "Canceled",
-                              "You canceled navigation and did not pick anything",
-                              "Ok"
-                             );
+            // This needs to be disposed before opening other toplevel
+            fd.Dispose ();
 
-        }
-        else if (_cbAllowMultipleSelection.CheckedState == CheckState.Checked)
-        {
-            MessageBox.Query (
-                              "Chosen!",
-                              "You chose:" + Environment.NewLine + string.Join (Environment.NewLine, multiSelected.Select (m => m)),
-                              "Ok"
-                             );
-        }
-        else
-        {
-            MessageBox.Query (
-                              "Chosen!",
-                              "You chose:" + Environment.NewLine + path,
-                              "Ok"
-                             );
+            if (canceled)
+            {
+                MessageBox.Query (
+                                  "Canceled",
+                                  "You canceled navigation and did not pick anything",
+                                  "Ok"
+                                 );
+            }
+            else if (_cbAllowMultipleSelection.CheckedState == CheckState.Checked)
+            {
+                MessageBox.Query (
+                                  "Chosen!",
+                                  "You chose:" + Environment.NewLine + string.Join (Environment.NewLine, multiSelected.Select (m => m)),
+                                  "Ok"
+                                 );
+            }
+            else
+            {
+                MessageBox.Query (
+                                  "Chosen!",
+                                  "You chose:" + Environment.NewLine + path,
+                                  "Ok"
+                                 );
+            }
         }
     }
 

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

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

+ 16 - 16
Examples/UICatalog/Scenarios/Images.cs

@@ -50,8 +50,8 @@ public class Images : Scenario
     private SixelToRender _fireSixel;
     private int _fireFrameCounter;
     private bool _isDisposed;
-    private RadioGroup _rgPaletteBuilder;
-    private RadioGroup _rgDistanceAlgorithm;
+    private OptionSelector _osPaletteBuilder;
+    private OptionSelector _osDistanceAlgorithm;
     private NumericUpDown _popularityThreshold;
     private SixelToRender _sixelImage;
 
@@ -408,22 +408,22 @@ public class Images : Scenario
             Y = Pos.Bottom (_pxY) + 1
         };
 
-        _rgPaletteBuilder = new ()
+        _osPaletteBuilder = new ()
         {
-            RadioLabels = new []
-            {
+            Labels =
+            [
                 "Popularity",
                 "Median Cut"
-            },
+            ],
             X = Pos.Right (_sixelView) + 2,
             Y = Pos.Bottom (l1),
-            SelectedItem = 1
+            Value = 1
         };
 
         _popularityThreshold = new ()
         {
-            X = Pos.Right (_rgPaletteBuilder) + 1,
-            Y = Pos.Top (_rgPaletteBuilder),
+            X = Pos.Right (_osPaletteBuilder) + 1,
+            Y = Pos.Top (_osPaletteBuilder),
             Value = 8
         };
 
@@ -439,12 +439,12 @@ public class Images : Scenario
             Text = "Color Distance Algorithm",
             Width = Dim.Auto (),
             X = Pos.Right (_sixelView),
-            Y = Pos.Bottom (_rgPaletteBuilder) + 1
+            Y = Pos.Bottom (_osPaletteBuilder) + 1
         };
 
-        _rgDistanceAlgorithm = new ()
+        _osDistanceAlgorithm = new ()
         {
-            RadioLabels = new []
+            Labels = new []
             {
                 "Euclidian",
                 "CIE76"
@@ -458,10 +458,10 @@ public class Images : Scenario
         _sixelSupported.Add (lblPxY);
         _sixelSupported.Add (_pxY);
         _sixelSupported.Add (l1);
-        _sixelSupported.Add (_rgPaletteBuilder);
+        _sixelSupported.Add (_osPaletteBuilder);
 
         _sixelSupported.Add (l2);
-        _sixelSupported.Add (_rgDistanceAlgorithm);
+        _sixelSupported.Add (_osDistanceAlgorithm);
         _sixelSupported.Add (_popularityThreshold);
         _sixelSupported.Add (lblPopThreshold);
 
@@ -470,7 +470,7 @@ public class Images : Scenario
 
     private IPaletteBuilder GetPaletteBuilder ()
     {
-        switch (_rgPaletteBuilder.SelectedItem)
+        switch (_osPaletteBuilder.Value)
         {
             case 0: return new PopularityPaletteWithThreshold (GetDistanceAlgorithm (), _popularityThreshold.Value);
             case 1: return new MedianCutPaletteBuilder (GetDistanceAlgorithm ());
@@ -480,7 +480,7 @@ public class Images : Scenario
 
     private IColorDistance GetDistanceAlgorithm ()
     {
-        switch (_rgDistanceAlgorithm.SelectedItem)
+        switch (_osDistanceAlgorithm.Value)
         {
             case 0: return new EuclideanColorDistance ();
             case 1: return new CIE76ColorDistance ();

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

@@ -158,10 +158,7 @@ public class Keys : Scenario
         appKeyListView.SchemeName = "TopLevel";
         win.Add (onSwallowedListView);
 
-        if (Application.Driver is IConsoleDriverFacade fac)
-        {
-            fac.InputProcessor.AnsiSequenceSwallowed += (s, e) => { swallowedList.Add (e.Replace ("\x1b","Esc")); };
-        }
+        Application.Driver!.InputProcessor.AnsiSequenceSwallowed += (s, e) => { swallowedList.Add (e.Replace ("\x1b", "Esc")); };
 
         Application.KeyDown += (s, a) => KeyDownPressUp (a, "Down");
         Application.KeyUp += (s, a) => KeyDownPressUp (a, "Up");

+ 10 - 4
Examples/UICatalog/Scenarios/LineDrawing.cs

@@ -210,7 +210,7 @@ public class ToolsView : Window
 {
     private Button _addLayerBtn;
     private readonly AttributeView _colors;
-    private RadioGroup _stylePicker;
+    private OptionSelector<LineStyle> _stylePicker;
 
     public Attribute CurrentColor
     {
@@ -236,10 +236,16 @@ public class ToolsView : Window
 
         _stylePicker = new ()
         {
-            X = 0, Y = Pos.Bottom (_colors), RadioLabels = Enum.GetNames (typeof (LineStyle)).ToArray ()
+            X = 0, Y = Pos.Bottom (_colors), AssignHotKeys = true
         };
-        _stylePicker.SelectedItemChanged += (s, a) => { SetStyle?.Invoke ((LineStyle)a.SelectedItem); };
-        _stylePicker.SelectedItem = 1;
+        _stylePicker.ValueChanged += (s, a) =>
+                                     {
+                                         if (a.Value is { })
+                                         {
+                                             SetStyle?.Invoke ((LineStyle)a.Value);
+                                         }
+                                     };
+        _stylePicker.Value = LineStyle.Single;
 
         _addLayerBtn = new () { Text = "New Layer", X = Pos.Center (), Y = Pos.Bottom (_stylePicker) };
 

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

@@ -129,7 +129,7 @@ public class Mazing : Scenario
             return;
         }
 
-        Point newPos = _m.Player;
+        Point newPos = _m!.Player;
 
         Command? command = e.Context?.Command;
 

+ 9 - 8
Examples/UICatalog/Scenarios/MessageBoxes.cs

@@ -182,18 +182,19 @@ public class MessageBoxes : Scenario
         };
         frame.Add (label);
 
-        var styleRadioGroup = new RadioGroup
+        var styleOptionSelector = new OptionSelector ()
         {
-            X = Pos.Right (label) + 1, 
-            Y = Pos.Top (label), 
-            RadioLabels = ["_Query", "_Error"],
+            X = Pos.Right (label) + 1,
+            Y = Pos.Top (label),
+            Labels = ["_Query", "_Error"],
+            Title = "Sty_le"
         };
-        frame.Add (styleRadioGroup);
+        frame.Add (styleOptionSelector);
 
         label = new ()
         {
             X = 0,
-            Y = Pos.Bottom (styleRadioGroup),
+            Y = Pos.Bottom (styleOptionSelector),
 
             Width = Dim.Width (label),
             Height = 1,
@@ -202,7 +203,7 @@ public class MessageBoxes : Scenario
         };
         var ckbWrapMessage = new CheckBox
         {
-            X = Pos.Right (label) + 1, Y = Pos.Bottom (styleRadioGroup),
+            X = Pos.Right (label) + 1, Y = Pos.Bottom (styleOptionSelector),
             CheckedState = CheckState.Checked,
             Text = "_Wrap Message",
         };
@@ -246,7 +247,7 @@ public class MessageBoxes : Scenario
                                                    btns.Add ($"_{NumberToWords.Convert (i)}");
                                                }
 
-                                               if (styleRadioGroup.SelectedItem == 0)
+                                               if (styleOptionSelector.Value == 0)
                                                {
                                                    buttonPressedLabel.Text =
                                                        $"{MessageBox.Query (

+ 14 - 12
Examples/UICatalog/Scenarios/Navigation.cs

@@ -107,18 +107,7 @@ public class Navigation : Scenario
         //                 };
         //timer.Start ();
 
-        Application.Iteration += (sender, args) =>
-                                 {
-                                     if (progressBar.Fraction == 1.0)
-                                     {
-                                         progressBar.Fraction = 0;
-                                     }
-
-                                     progressBar.Fraction += 0.01f;
-
-                                     Application.Invoke (() => { });
-
-                                 };
+        Application.Iteration += OnApplicationIteration;
 
         View overlappedView2 = CreateOverlappedView (3, 8, 10);
 
@@ -214,12 +203,25 @@ public class Navigation : Scenario
 
         testFrame.SetFocus ();
         Application.Run (app);
+        Application.Iteration -= OnApplicationIteration;
         // timer.Close ();
         app.Dispose ();
         Application.Shutdown ();
 
         return;
 
+        void OnApplicationIteration (object sender, IterationEventArgs args)
+        {
+            if (progressBar.Fraction == 1.0)
+            {
+                progressBar.Fraction = 0;
+            }
+
+            progressBar.Fraction += 0.01f;
+
+            Application.Invoke (() => { });
+        }
+
         void ColorPicker_ColorChanged (object sender, ResultEventArgs<Color> e)
         {
             testFrame.SetScheme (testFrame.GetScheme () with { Normal = new (testFrame.GetAttributeForRole (VisualRole.Normal).Foreground, e.Result) });

+ 34 - 33
Examples/UICatalog/Scenarios/Notepad.cs

@@ -1,6 +1,4 @@
-using System.IO;
-using System.Linq;
-
+#nullable enable
 namespace UICatalog.Scenarios;
 
 [ScenarioMetadata ("Notepad", "Multi-tab text editor using the TabView control.")]
@@ -9,10 +7,10 @@ namespace UICatalog.Scenarios;
 [ScenarioCategory ("TextView")]
 public class Notepad : Scenario
 {
-    private TabView _focusedTabView;
-    public Shortcut LenShortcut { get; private set; }
+    private TabView? _focusedTabView;
     private int _numNewTabs = 1;
-    private TabView _tabView;
+    private TabView? _tabView;
+    public Shortcut? LenShortcut { get; private set; }
 
     public override void Main ()
     {
@@ -67,14 +65,15 @@ public class Notepad : Scenario
         top.Add (_tabView);
         LenShortcut = new (Key.Empty, "Len: ", null);
 
-        var statusBar = new StatusBar (new [] {
-                                           new (Application.QuitKey, $"Quit", Quit),
-                                           new Shortcut(Key.F2, "Open", Open),
-                                           new Shortcut(Key.F1, "New", New),
+        var statusBar = new StatusBar (
+                                       [
+                                           new (Application.QuitKey, "Quit", Quit),
+                                           new (Key.F2, "Open", Open),
+                                           new (Key.F1, "New", New),
                                            new (Key.F3, "Save", Save),
                                            new (Key.F6, "Close", Close),
                                            LenShortcut
-                                       }
+                                       ]
                                       )
         {
             AlignmentModes = AlignmentModes.IgnoreFirstOrLast
@@ -97,7 +96,7 @@ public class Notepad : Scenario
         Application.Shutdown ();
     }
 
-    public void Save () { Save (_focusedTabView, _focusedTabView.SelectedTab); }
+    public void Save () { Save (_focusedTabView!, _focusedTabView!.SelectedTab!); }
 
     public void Save (TabView tabViewToSave, Tab tabToSave)
     {
@@ -119,7 +118,7 @@ public class Notepad : Scenario
 
     public bool SaveAs ()
     {
-        var tab = _focusedTabView.SelectedTab as OpenedFile;
+        var tab = _focusedTabView!.SelectedTab as OpenedFile;
 
         if (tab == null)
         {
@@ -152,7 +151,7 @@ public class Notepad : Scenario
         return true;
     }
 
-    private void Close () { Close (_focusedTabView, _focusedTabView.SelectedTab); }
+    private void Close () { Close (_focusedTabView!, _focusedTabView!.SelectedTab!); }
 
     private void Close (TabView tv, Tab tabToClose)
     {
@@ -196,7 +195,7 @@ public class Notepad : Scenario
 
         // close and dispose the tab
         tv.RemoveTab (tab);
-        tab.View.Dispose ();
+        tab.View?.Dispose ();
         _focusedTabView = tv;
 
         // If last tab is closed, open a new one
@@ -217,7 +216,7 @@ public class Notepad : Scenario
         return tv;
     }
 
-    private void New () { Open (null, $"new {_numNewTabs++}"); }
+    private void New () { Open (null!, $"new {_numNewTabs++}"); }
 
     private void Open ()
     {
@@ -246,26 +245,27 @@ public class Notepad : Scenario
 
     /// <summary>Creates a new tab with initial text</summary>
     /// <param name="fileInfo">File that was read or null if a new blank document</param>
+    /// <param name="tabName"></param>
     private void Open (FileInfo fileInfo, string tabName)
     {
         var tab = new OpenedFile (this) { DisplayText = tabName, File = fileInfo };
         tab.View = tab.CreateTextView (fileInfo);
         tab.SavedText = tab.View.Text;
-        tab.RegisterTextViewEvents (_focusedTabView);
+        tab.RegisterTextViewEvents (_focusedTabView!);
 
-        _focusedTabView.AddTab (tab, true);
+        _focusedTabView!.AddTab (tab, true);
     }
 
     private void Quit () { Application.RequestStop (); }
 
-    private void TabView_SelectedTabChanged (object sender, TabChangedEventArgs e)
+    private void TabView_SelectedTabChanged (object? sender, TabChangedEventArgs e)
     {
-        LenShortcut.Title = $"Len:{e.NewTab?.View?.Text?.Length ?? 0}";
+        LenShortcut!.Title = $"Len:{e.NewTab?.View?.Text?.Length ?? 0}";
 
         e.NewTab?.View?.SetFocus ();
     }
 
-    private void TabView_TabClicked (object sender, TabMouseEventArgs e)
+    private void TabView_TabClicked (object? sender, TabMouseEventArgs e)
     {
         // we are only interested in right clicks
         if (!e.MouseEvent.Flags.HasFlag (MouseFlags.Button3Clicked))
@@ -281,12 +281,12 @@ public class Notepad : Scenario
         }
         else
         {
-            var tv = (TabView)sender;
+            var tv = (TabView)sender!;
             var t = (OpenedFile)e.Tab;
 
             items =
             [
-                new MenuItemv2 ("Save", "", () => Save (_focusedTabView, e.Tab)),
+                new MenuItemv2 ("Save", "", () => Save (_focusedTabView!, e.Tab)),
                 new MenuItemv2 ("Close", "", () => Close (tv, e.Tab))
             ];
 
@@ -303,12 +303,12 @@ public class Notepad : Scenario
 
     private class OpenedFile (Notepad notepad) : Tab
     {
-        private Notepad _notepad = notepad;
+        private readonly Notepad _notepad = notepad;
 
         public OpenedFile CloneTo (TabView other)
         {
             var newTab = new OpenedFile (_notepad) { DisplayText = base.Text, File = File };
-            newTab.View = newTab.CreateTextView (newTab.File);
+            newTab.View = newTab.CreateTextView (newTab.File!);
             newTab.SavedText = newTab.View.Text;
             newTab.RegisterTextViewEvents (other);
             other.AddTab (newTab, true);
@@ -316,11 +316,11 @@ public class Notepad : Scenario
             return newTab;
         }
 
-        public View CreateTextView (FileInfo file)
+        public View CreateTextView (FileInfo? file)
         {
             var initialText = string.Empty;
 
-            if (file != null && file.Exists)
+            if (file is { Exists: true })
             {
                 initialText = System.IO.File.ReadAllText (file.FullName);
             }
@@ -336,11 +336,11 @@ public class Notepad : Scenario
             };
         }
 
-        public FileInfo File { get; set; }
+        public FileInfo? File { get; set; }
 
         public void RegisterTextViewEvents (TabView parent)
         {
-            var textView = (TextView)View;
+            var textView = (TextView)View!;
 
             // when user makes changes rename tab to indicate unsaved
             textView.ContentsChanged += (s, k) =>
@@ -362,19 +362,20 @@ public class Notepad : Scenario
                                                     DisplayText = Text.TrimEnd ('*');
                                                 }
                                             }
-                                            _notepad.LenShortcut.Title = $"Len:{textView.Text.Length}";
+
+                                            _notepad.LenShortcut!.Title = $"Len:{textView.Text.Length}";
                                         };
         }
 
         /// <summary>The text of the tab the last time it was saved</summary>
         /// <value></value>
-        public string SavedText { get; set; }
+        public string? SavedText { get; set; }
 
-        public bool UnsavedChanges => !string.Equals (SavedText, View.Text);
+        public bool UnsavedChanges => !string.Equals (SavedText, View!.Text);
 
         internal void Save ()
         {
-            string newText = View.Text;
+            string newText = View!.Text;
 
             if (File is null || string.IsNullOrWhiteSpace (File.FullName))
             {

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

@@ -266,14 +266,14 @@ internal class NumericUpDownEditor<T> : View where T : notnull
 
             void NumericUpDownOnIncrementChanged (object? o, EventArgs<T> eventArgs)
             {
-                _increment.Text = _numericUpDown.Increment.ToString ();
+                _increment.Text = _numericUpDown!.Increment?.ToString ();
             }
 
             Add (_numericUpDown);
 
             _value.Text = _numericUpDown.Text;
             _format.Text = _numericUpDown.Format;
-            _increment.Text = _numericUpDown.Increment.ToString ();
+            _increment.Text = _numericUpDown!.Increment?.ToString ();
         }
     }
 

+ 141 - 135
Examples/UICatalog/Scenarios/PosAlignDemo.cs

@@ -1,14 +1,10 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-
 namespace UICatalog.Scenarios;
 
 [ScenarioMetadata ("Pos.Align", "Demonstrates Pos.Align")]
 [ScenarioCategory ("Layout")]
 public sealed class PosAlignDemo : Scenario
 {
-    private readonly Aligner _horizAligner = new () { AlignmentModes = AlignmentModes.StartToEnd | AlignmentModes.AddSpaceBetweenItems};
+    private readonly Aligner _horizAligner = new () { AlignmentModes = AlignmentModes.StartToEnd | AlignmentModes.AddSpaceBetweenItems };
     private int _leftMargin;
     private readonly Aligner _vertAligner = new () { AlignmentModes = AlignmentModes.StartToEnd | AlignmentModes.AddSpaceBetweenItems };
     private int _topMargin;
@@ -40,43 +36,42 @@ public sealed class PosAlignDemo : Scenario
 
     private void SetupControls (Window appWindow, Dimension dimension, Schemes scheme)
     {
-        RadioGroup alignRadioGroup = new ()
+        OptionSelector<Alignment> alignOptionSelector = new ()
         {
-            RadioLabels = Enum.GetNames<Alignment> (),
-            SchemeName = SchemeManager.SchemesToSchemeName (scheme),
+            SchemeName = SchemeManager.SchemesToSchemeName (scheme)
         };
 
         if (dimension == Dimension.Width)
         {
-            alignRadioGroup.X = Pos.Align (_horizAligner.Alignment);
-            alignRadioGroup.Y = Pos.Center ();
+            alignOptionSelector.X = Pos.Align (_horizAligner.Alignment);
+            alignOptionSelector.Y = Pos.Center ();
         }
         else
         {
-            alignRadioGroup.X = Pos.Center ();
-            alignRadioGroup.Y = Pos.Align (_vertAligner.Alignment);
+            alignOptionSelector.X = Pos.Center ();
+            alignOptionSelector.Y = Pos.Align (_vertAligner.Alignment);
         }
 
-        alignRadioGroup.SelectedItemChanged += (s, e) =>
-                                               {
-                                                   if (dimension == Dimension.Width)
-                                                   {
-                                                       _horizAligner.Alignment =
-                                                           (Alignment)Enum.Parse (
-                                                                                  typeof (Alignment),
-                                                                                  alignRadioGroup.RadioLabels [alignRadioGroup.SelectedItem]);
-                                                       UpdatePosAlignObjects (appWindow, dimension, _horizAligner);
-                                                   }
-                                                   else
-                                                   {
-                                                       _vertAligner.Alignment =
-                                                           (Alignment)Enum.Parse (
-                                                                                  typeof (Alignment),
-                                                                                  alignRadioGroup.RadioLabels [alignRadioGroup.SelectedItem]);
-                                                       UpdatePosAlignObjects (appWindow, dimension, _vertAligner);
-                                                   }
-                                               };
-        appWindow.Add (alignRadioGroup);
+        alignOptionSelector.ValueChanged += (s, e) =>
+                                            {
+                                                if (alignOptionSelector.Value is null)
+                                                {
+                                                    return;
+                                                }
+
+                                                if (dimension == Dimension.Width)
+                                                {
+                                                    _horizAligner.Alignment = alignOptionSelector.Value.Value;
+
+                                                    UpdatePosAlignObjects (appWindow, dimension, _horizAligner);
+                                                }
+                                                else
+                                                {
+                                                    _vertAligner.Alignment = alignOptionSelector.Value.Value;
+                                                    UpdatePosAlignObjects (appWindow, dimension, _vertAligner);
+                                                }
+                                            };
+        appWindow.Add (alignOptionSelector);
 
         CheckBox endToStartCheckBox = new ()
         {
@@ -88,32 +83,32 @@ public sealed class PosAlignDemo : Scenario
         {
             endToStartCheckBox.CheckedState = _horizAligner.AlignmentModes.HasFlag (AlignmentModes.EndToStart) ? CheckState.Checked : CheckState.UnChecked;
             endToStartCheckBox.X = Pos.Align (_horizAligner.Alignment);
-            endToStartCheckBox.Y = Pos.Top (alignRadioGroup);
+            endToStartCheckBox.Y = Pos.Top (alignOptionSelector);
         }
         else
         {
             endToStartCheckBox.CheckedState = _vertAligner.AlignmentModes.HasFlag (AlignmentModes.EndToStart) ? CheckState.Checked : CheckState.UnChecked;
-            endToStartCheckBox.X = Pos.Left (alignRadioGroup);
+            endToStartCheckBox.X = Pos.Left (alignOptionSelector);
             endToStartCheckBox.Y = Pos.Align (_vertAligner.Alignment);
         }
 
         endToStartCheckBox.CheckedStateChanging += (s, e) =>
-                                      {
-                                          if (dimension == Dimension.Width)
-                                          {
-                                              _horizAligner.AlignmentModes = e.Result == CheckState.Checked
-                                                                                 ? _horizAligner.AlignmentModes | AlignmentModes.EndToStart
-                                                                                 : _horizAligner.AlignmentModes & ~AlignmentModes.EndToStart;
-                                              UpdatePosAlignObjects (appWindow, dimension, _horizAligner);
-                                          }
-                                          else
-                                          {
-                                              _vertAligner.AlignmentModes = e.Result == CheckState.Checked
-                                                                                ? _vertAligner.AlignmentModes | AlignmentModes.EndToStart
-                                                                                : _vertAligner.AlignmentModes & ~AlignmentModes.EndToStart;
-                                              UpdatePosAlignObjects (appWindow, dimension, _vertAligner);
-                                          }
-                                      };
+                                                   {
+                                                       if (dimension == Dimension.Width)
+                                                       {
+                                                           _horizAligner.AlignmentModes = e.Result == CheckState.Checked
+                                                                                              ? _horizAligner.AlignmentModes | AlignmentModes.EndToStart
+                                                                                              : _horizAligner.AlignmentModes & ~AlignmentModes.EndToStart;
+                                                           UpdatePosAlignObjects (appWindow, dimension, _horizAligner);
+                                                       }
+                                                       else
+                                                       {
+                                                           _vertAligner.AlignmentModes = e.Result == CheckState.Checked
+                                                                                             ? _vertAligner.AlignmentModes | AlignmentModes.EndToStart
+                                                                                             : _vertAligner.AlignmentModes & ~AlignmentModes.EndToStart;
+                                                           UpdatePosAlignObjects (appWindow, dimension, _vertAligner);
+                                                       }
+                                                   };
         appWindow.Add (endToStartCheckBox);
 
         CheckBox ignoreFirstOrLast = new ()
@@ -124,34 +119,35 @@ public sealed class PosAlignDemo : Scenario
 
         if (dimension == Dimension.Width)
         {
-            ignoreFirstOrLast.CheckedState = _horizAligner.AlignmentModes.HasFlag (AlignmentModes.IgnoreFirstOrLast) ? CheckState.Checked : CheckState.UnChecked;
+            ignoreFirstOrLast.CheckedState =
+                _horizAligner.AlignmentModes.HasFlag (AlignmentModes.IgnoreFirstOrLast) ? CheckState.Checked : CheckState.UnChecked;
             ignoreFirstOrLast.X = Pos.Align (_horizAligner.Alignment);
-            ignoreFirstOrLast.Y = Pos.Top (alignRadioGroup);
+            ignoreFirstOrLast.Y = Pos.Top (alignOptionSelector);
         }
         else
         {
             ignoreFirstOrLast.CheckedState = _vertAligner.AlignmentModes.HasFlag (AlignmentModes.IgnoreFirstOrLast) ? CheckState.Checked : CheckState.UnChecked;
-            ignoreFirstOrLast.X = Pos.Left (alignRadioGroup);
+            ignoreFirstOrLast.X = Pos.Left (alignOptionSelector);
             ignoreFirstOrLast.Y = Pos.Align (_vertAligner.Alignment);
         }
 
         ignoreFirstOrLast.CheckedStateChanging += (s, e) =>
-                                     {
-                                         if (dimension == Dimension.Width)
-                                         {
-                                             _horizAligner.AlignmentModes = e.Result == CheckState.Checked
-                                                     ? _horizAligner.AlignmentModes | AlignmentModes.IgnoreFirstOrLast
-                                                     : _horizAligner.AlignmentModes & ~AlignmentModes.IgnoreFirstOrLast;
-                                             UpdatePosAlignObjects (appWindow, dimension, _horizAligner);
-                                         }
-                                         else
-                                         {
-                                             _vertAligner.AlignmentModes = e.Result == CheckState.Checked
-                                                                               ? _vertAligner.AlignmentModes | AlignmentModes.IgnoreFirstOrLast
-                                                                               : _vertAligner.AlignmentModes & ~AlignmentModes.IgnoreFirstOrLast;
-                                             UpdatePosAlignObjects (appWindow, dimension, _vertAligner);
-                                         }
-                                     };
+                                                  {
+                                                      if (dimension == Dimension.Width)
+                                                      {
+                                                          _horizAligner.AlignmentModes = e.Result == CheckState.Checked
+                                                                                             ? _horizAligner.AlignmentModes | AlignmentModes.IgnoreFirstOrLast
+                                                                                             : _horizAligner.AlignmentModes & ~AlignmentModes.IgnoreFirstOrLast;
+                                                          UpdatePosAlignObjects (appWindow, dimension, _horizAligner);
+                                                      }
+                                                      else
+                                                      {
+                                                          _vertAligner.AlignmentModes = e.Result == CheckState.Checked
+                                                                                            ? _vertAligner.AlignmentModes | AlignmentModes.IgnoreFirstOrLast
+                                                                                            : _vertAligner.AlignmentModes & ~AlignmentModes.IgnoreFirstOrLast;
+                                                          UpdatePosAlignObjects (appWindow, dimension, _vertAligner);
+                                                      }
+                                                  };
         appWindow.Add (ignoreFirstOrLast);
 
         CheckBox addSpacesBetweenItems = new ()
@@ -162,34 +158,40 @@ public sealed class PosAlignDemo : Scenario
 
         if (dimension == Dimension.Width)
         {
-            addSpacesBetweenItems.CheckedState = _horizAligner.AlignmentModes.HasFlag (AlignmentModes.AddSpaceBetweenItems) ? CheckState.Checked : CheckState.UnChecked;
+            addSpacesBetweenItems.CheckedState =
+                _horizAligner.AlignmentModes.HasFlag (AlignmentModes.AddSpaceBetweenItems) ? CheckState.Checked : CheckState.UnChecked;
             addSpacesBetweenItems.X = Pos.Align (_horizAligner.Alignment);
-            addSpacesBetweenItems.Y = Pos.Top (alignRadioGroup);
+            addSpacesBetweenItems.Y = Pos.Top (alignOptionSelector);
         }
         else
         {
-            addSpacesBetweenItems.CheckedState = _vertAligner.AlignmentModes.HasFlag (AlignmentModes.AddSpaceBetweenItems) ? CheckState.Checked : CheckState.UnChecked;
-            addSpacesBetweenItems.X = Pos.Left (alignRadioGroup);
+            addSpacesBetweenItems.CheckedState =
+                _vertAligner.AlignmentModes.HasFlag (AlignmentModes.AddSpaceBetweenItems) ? CheckState.Checked : CheckState.UnChecked;
+            addSpacesBetweenItems.X = Pos.Left (alignOptionSelector);
             addSpacesBetweenItems.Y = Pos.Align (_vertAligner.Alignment);
         }
 
         addSpacesBetweenItems.CheckedStateChanging += (s, e) =>
-                                         {
-                                             if (dimension == Dimension.Width)
-                                             {
-                                                 _horizAligner.AlignmentModes = e.Result == CheckState.Checked
-                                                                                    ? _horizAligner.AlignmentModes | AlignmentModes.AddSpaceBetweenItems
-                                                                                    : _horizAligner.AlignmentModes & ~AlignmentModes.AddSpaceBetweenItems;
-                                                 UpdatePosAlignObjects (appWindow, dimension, _horizAligner);
-                                             }
-                                             else
-                                             {
-                                                 _vertAligner.AlignmentModes = e.Result == CheckState.Checked
-                                                                                   ? _vertAligner.AlignmentModes | AlignmentModes.AddSpaceBetweenItems
-                                                                                   : _vertAligner.AlignmentModes & ~AlignmentModes.AddSpaceBetweenItems;
-                                                 UpdatePosAlignObjects (appWindow, dimension, _vertAligner);
-                                             }
-                                         };
+                                                      {
+                                                          if (dimension == Dimension.Width)
+                                                          {
+                                                              _horizAligner.AlignmentModes = e.Result == CheckState.Checked
+                                                                                                 ? _horizAligner.AlignmentModes
+                                                                                                   | AlignmentModes.AddSpaceBetweenItems
+                                                                                                 : _horizAligner.AlignmentModes
+                                                                                                   & ~AlignmentModes.AddSpaceBetweenItems;
+                                                              UpdatePosAlignObjects (appWindow, dimension, _horizAligner);
+                                                          }
+                                                          else
+                                                          {
+                                                              _vertAligner.AlignmentModes = e.Result == CheckState.Checked
+                                                                                                ? _vertAligner.AlignmentModes
+                                                                                                  | AlignmentModes.AddSpaceBetweenItems
+                                                                                                : _vertAligner.AlignmentModes
+                                                                                                  & ~AlignmentModes.AddSpaceBetweenItems;
+                                                              UpdatePosAlignObjects (appWindow, dimension, _vertAligner);
+                                                          }
+                                                      };
 
         appWindow.Add (addSpacesBetweenItems);
 
@@ -202,7 +204,7 @@ public sealed class PosAlignDemo : Scenario
         if (dimension == Dimension.Width)
         {
             margin.X = Pos.Align (_horizAligner.Alignment);
-            margin.Y = Pos.Top (alignRadioGroup);
+            margin.Y = Pos.Top (alignOptionSelector);
         }
         else
         {
@@ -211,31 +213,31 @@ public sealed class PosAlignDemo : Scenario
         }
 
         margin.CheckedStateChanging += (s, e) =>
-                          {
-                              if (dimension == Dimension.Width)
-                              {
-                                  _leftMargin = e.Result == CheckState.Checked ? 1 : 0;
-                                  UpdatePosAlignObjects (appWindow, dimension, _horizAligner);
-                              }
-                              else
-                              {
-                                  _topMargin = e.Result == CheckState.Checked ? 1 : 0;
-                                  UpdatePosAlignObjects (appWindow, dimension, _vertAligner);
-                              }
-                          };
+                                       {
+                                           if (dimension == Dimension.Width)
+                                           {
+                                               _leftMargin = e.Result == CheckState.Checked ? 1 : 0;
+                                               UpdatePosAlignObjects (appWindow, dimension, _horizAligner);
+                                           }
+                                           else
+                                           {
+                                               _topMargin = e.Result == CheckState.Checked ? 1 : 0;
+                                               UpdatePosAlignObjects (appWindow, dimension, _vertAligner);
+                                           }
+                                       };
         appWindow.Add (margin);
 
         List<Button> addedViews =
         [
             new ()
             {
-                X = dimension == Dimension.Width ? Pos.Align (_horizAligner.Alignment) : Pos.Left (alignRadioGroup),
-                Y = dimension == Dimension.Width ? Pos.Top (alignRadioGroup) : Pos.Align (_vertAligner.Alignment),
+                X = dimension == Dimension.Width ? Pos.Align (_horizAligner.Alignment) : Pos.Left (alignOptionSelector),
+                Y = dimension == Dimension.Width ? Pos.Top (alignOptionSelector) : Pos.Align (_vertAligner.Alignment),
                 Text = NumberToWords.Convert (0)
             }
         ];
 
-        NumericUpDown<int> addedViewsUpDown = new()
+        NumericUpDown<int> addedViewsUpDown = new ()
         {
             Width = 9,
             Title = "Added",
@@ -247,12 +249,12 @@ public sealed class PosAlignDemo : Scenario
         if (dimension == Dimension.Width)
         {
             addedViewsUpDown.X = Pos.Align (_horizAligner.Alignment);
-            addedViewsUpDown.Y = Pos.Top (alignRadioGroup);
+            addedViewsUpDown.Y = Pos.Top (alignOptionSelector);
             addedViewsUpDown.Border!.Thickness = new (0, 1, 0, 0);
         }
         else
         {
-            addedViewsUpDown.X = Pos.Left (alignRadioGroup);
+            addedViewsUpDown.X = Pos.Left (alignOptionSelector);
             addedViewsUpDown.Y = Pos.Align (_vertAligner.Alignment);
             addedViewsUpDown.Border!.Thickness = new (1, 0, 0, 0);
         }
@@ -286,8 +288,10 @@ public sealed class PosAlignDemo : Scenario
                                                   {
                                                       var button = new Button
                                                       {
-                                                          X = dimension == Dimension.Width ? Pos.Align (_horizAligner.Alignment) : Pos.Left (alignRadioGroup),
-                                                          Y = dimension == Dimension.Width ? Pos.Top (alignRadioGroup) : Pos.Align (_vertAligner.Alignment),
+                                                          X = dimension == Dimension.Width
+                                                                  ? Pos.Align (_horizAligner.Alignment)
+                                                                  : Pos.Left (alignOptionSelector),
+                                                          Y = dimension == Dimension.Width ? Pos.Top (alignOptionSelector) : Pos.Align (_vertAligner.Alignment),
                                                           Text = NumberToWords.Convert (i + 1)
                                                       };
                                                       appWindow.Add (button);
@@ -351,46 +355,48 @@ public sealed class PosAlignDemo : Scenario
             Width = Dim.Percent (40),
             Height = Dim.Percent (40)
         };
-        container.Padding.Thickness = new (8, 1, 0, 0);
+        container.Padding!.Thickness = new (8, 1, 0, 0);
         container.Padding.SchemeName = "Error";
 
         Aligner widthAligner = new () { AlignmentModes = AlignmentModes.StartToEnd };
 
-        RadioGroup widthAlignRadioGroup = new ()
+        OptionSelector widthAlignOptionSelector = new ()
         {
-            RadioLabels = Enum.GetNames<Alignment> (),
+            Labels = Enum.GetNames<Alignment> (),
             Orientation = Orientation.Horizontal,
             X = Pos.Center ()
         };
-        container.Padding.Add (widthAlignRadioGroup);
-
-        widthAlignRadioGroup.SelectedItemChanged += (sender, e) =>
-                                                    {
-                                                        widthAligner.Alignment =
-                                                            (Alignment)Enum.Parse (
-                                                                                   typeof (Alignment),
-                                                                                   widthAlignRadioGroup.RadioLabels [widthAlignRadioGroup.SelectedItem]);
-                                                        UpdatePosAlignObjects (container, Dimension.Width, widthAligner);
-                                                    };
+        container.Padding.Add (widthAlignOptionSelector);
+
+        widthAlignOptionSelector.ValueChanged += (_, _) =>
+                                                        {
+                                                            widthAligner.Alignment =
+                                                                (Alignment)Enum.Parse (
+                                                                                       typeof (Alignment),
+                                                                                       widthAlignOptionSelector.Labels [widthAlignOptionSelector
+                                                                                           .Value!.Value]);
+                                                            UpdatePosAlignObjects (container, Dimension.Width, widthAligner);
+                                                        };
 
         Aligner heightAligner = new () { AlignmentModes = AlignmentModes.StartToEnd };
 
-        RadioGroup heightAlignRadioGroup = new ()
+        OptionSelector heightAlignOptionSelector = new ()
         {
-            RadioLabels = Enum.GetNames<Alignment> (),
+            Labels = Enum.GetNames<Alignment> (),
             Orientation = Orientation.Vertical,
             Y = Pos.Center ()
         };
-        container.Padding.Add (heightAlignRadioGroup);
-
-        heightAlignRadioGroup.SelectedItemChanged += (sender, e) =>
-                                                     {
-                                                         heightAligner.Alignment =
-                                                             (Alignment)Enum.Parse (
-                                                                                    typeof (Alignment),
-                                                                                    heightAlignRadioGroup.RadioLabels [heightAlignRadioGroup.SelectedItem]);
-                                                         UpdatePosAlignObjects (container, Dimension.Height, heightAligner);
-                                                     };
+        container.Padding.Add (heightAlignOptionSelector);
+
+        heightAlignOptionSelector.ValueChanged += (sender, e) =>
+                                                         {
+                                                             heightAligner.Alignment =
+                                                                 (Alignment)Enum.Parse (
+                                                                                        typeof (Alignment),
+                                                                                        heightAlignOptionSelector.Labels [heightAlignOptionSelector
+                                                                                            .Value!.Value]);
+                                                             UpdatePosAlignObjects (container, Dimension.Height, heightAligner);
+                                                         };
 
         for (var i = 0; i < 9; i++)
 
@@ -401,7 +407,7 @@ public sealed class PosAlignDemo : Scenario
                 Text = $"{i}",
                 BorderStyle = LineStyle.Dashed,
                 Height = Dim.Auto (),
-                Width = Dim.Auto() + 2
+                Width = Dim.Auto () + 2
             };
 
             v.X = Pos.Align (widthAligner.Alignment, widthAligner.AlignmentModes, i / 3);

+ 17 - 11
Examples/UICatalog/Scenarios/ProgressBarStyles.cs

@@ -132,15 +132,15 @@ public class ProgressBarStyles : Scenario
         List<ProgressBarFormat> pbFormatEnum =
             Enum.GetValues (typeof (ProgressBarFormat)).Cast<ProgressBarFormat> ().ToList ();
 
-        var rbPBFormat = new RadioGroup
+        OptionSelector<ProgressBarFormat> osPbFormat = new ()
         {
             BorderStyle = LineStyle.Single,
             Title = "ProgressBarFormat",
             X = Pos.Center (),
             Y = Pos.Align (Alignment.Start),
-            RadioLabels = pbFormatEnum.Select (e => e.ToString ()).ToArray ()
+            AssignHotKeys = true
         };
-        container.Add (rbPBFormat);
+        container.Add (osPbFormat);
 
         var button = new Button
         {
@@ -161,7 +161,7 @@ public class ProgressBarStyles : Scenario
         };
         container.Add (blocksPB);
 
-        rbPBFormat.SelectedItem = (int)blocksPB.ProgressBarFormat;
+        osPbFormat.Value = blocksPB.ProgressBarFormat;
 
         var continuousPB = new ProgressBar
         {
@@ -256,13 +256,19 @@ public class ProgressBarStyles : Scenario
                                       };
 
 
-        rbPBFormat.SelectedItemChanged += (s, e) =>
-                                          {
-                                              blocksPB.ProgressBarFormat = (ProgressBarFormat)e.SelectedItem;
-                                              continuousPB.ProgressBarFormat = (ProgressBarFormat)e.SelectedItem;
-                                              marqueesBlocksPB.ProgressBarFormat = (ProgressBarFormat)e.SelectedItem;
-                                              marqueesContinuousPB.ProgressBarFormat = (ProgressBarFormat)e.SelectedItem;
-                                          };
+        osPbFormat.ValueChanged += (s, e) =>
+                                   {
+                                       if (e.Value is null)
+                                       {
+                                           return;
+                                       }
+
+                                       blocksPB.ProgressBarFormat = e.Value.Value;
+                                       continuousPB.ProgressBarFormat = e.Value.Value;
+                                       marqueesBlocksPB.ProgressBarFormat = e.Value.Value;
+                                       marqueesContinuousPB.ProgressBarFormat = e.Value.Value;
+
+                                   };
 
         ckbBidirectional.CheckedStateChanging += (s, e) =>
                                    {

+ 18 - 45
Examples/UICatalog/Scenarios/RegionScenario.cs

@@ -1,7 +1,4 @@
 #nullable enable
-using System;
-using System.Collections.Generic;
-using System.Linq;
 using System.Text;
 using UICatalog;
 using UICatalog.Scenarios;
@@ -40,7 +37,7 @@ public class RegionScenario : Scenario
 
         tools.SetStyle += b =>
                           {
-                              _drawStyle = (RegionDrawStyles)b;
+                              _drawStyle = b;
                               app.SetNeedsDraw ();
                           };
 
@@ -169,8 +166,8 @@ public class ToolsView : Window
 {
     //private Button _addLayerBtn;
     private readonly AttributeView _attributeView = new ();
-    private RadioGroup? _stylePicker;
-    private RegionOpSelector? _regionOpSelector;
+    private OptionSelector<RegionDrawStyles>? _stylePicker;
+    private OptionSelector<RegionOp>? _regionOpSelector;
 
     public Attribute? CurrentAttribute
     {
@@ -197,22 +194,30 @@ public class ToolsView : Window
         _stylePicker = new ()
         {
             Width = Dim.Fill (),
-            X = 0, Y = Pos.Bottom (_attributeView) + 1, RadioLabels = Enum.GetNames<RegionDrawStyles> ().Select (n => n = "_" + n).ToArray ()
+            X = 0, Y = Pos.Bottom (_attributeView) + 1,
+            AssignHotKeys = true
         };
         _stylePicker.BorderStyle = LineStyle.Single;
         _stylePicker.Border!.Thickness = new (0, 1, 0, 0);
         _stylePicker.Title = "Draw Style";
 
-        _stylePicker.SelectedItemChanged += (s, a) => { SetStyle?.Invoke ((LineStyle)a.SelectedItem!); };
-        _stylePicker.SelectedItem = (int)RegionDrawStyles.FillOnly;
+        _stylePicker.ValueChanged += (s, a) => { SetStyle?.Invoke ((RegionDrawStyles)a.Value!); };
+        _stylePicker.Value = RegionDrawStyles.FillOnly;
 
         _regionOpSelector = new ()
         {
             X = 0,
-            Y = Pos.Bottom (_stylePicker) + 1
+            Y = Pos.Bottom (_stylePicker) + 1,
+            AssignHotKeys = true
         };
-        _regionOpSelector.SelectedItemChanged += (s, a) => { RegionOpChanged?.Invoke (this, a); };
-        _regionOpSelector.SelectedItem = RegionOp.MinimalUnion;
+        _regionOpSelector.ValueChanged += (s, a) =>
+                                          {
+                                              if (a.Value is { })
+                                              {
+                                                  RegionOpChanged?.Invoke (this, (RegionOp)a.Value);
+                                              }
+                                          };
+        _regionOpSelector.Value = RegionOp.MinimalUnion;
 
         //_addLayerBtn = new () { Text = "New Layer", X = Pos.Center (), Y = Pos.Bottom (_stylePicker) };
 
@@ -222,39 +227,7 @@ public class ToolsView : Window
 
     public event EventHandler<Attribute?>? AttributeChanged;
     public event EventHandler<RegionOp>? RegionOpChanged;
-    public event Action<LineStyle>? SetStyle;
-}
-
-public class RegionOpSelector : View
-{
-    private readonly RadioGroup _radioGroup;
-
-    public RegionOpSelector ()
-    {
-        Width = Dim.Auto ();
-        Height = Dim.Auto ();
-
-        BorderStyle = LineStyle.Single;
-        Border!.Thickness = new (0, 1, 0, 0);
-        Title = "RegionOp";
-
-        _radioGroup = new ()
-        {
-            X = 0,
-            Y = 0,
-            RadioLabels = Enum.GetNames<RegionOp> ().Select (n => n = "_" + n).ToArray ()
-        };
-        _radioGroup.SelectedItemChanged += (s, a) => { SelectedItemChanged?.Invoke (this, (RegionOp)a.SelectedItem!); };
-        Add (_radioGroup);
-    }
-
-    public event EventHandler<RegionOp>? SelectedItemChanged;
-
-    public RegionOp SelectedItem
-    {
-        get => (RegionOp)_radioGroup.SelectedItem;
-        set => _radioGroup.SelectedItem = (int)value;
-    }
+    public event Action<RegionDrawStyles>? SetStyle;
 }
 
 public class AttributeView : View

+ 5 - 9
Examples/UICatalog/Scenarios/ScrollBarDemo.cs

@@ -118,23 +118,19 @@ public class ScrollBarDemo : Scenario
         };
         demoFrame.Add (lblOrientationLabel);
 
-        var rgOrientation = new RadioGroup
+        OptionSelector<Orientation> osOrientation = new ()
         {
             X = Pos.Right (lblOrientationLabel) + 1,
             Y = Pos.Top (lblOrientationLabel),
-            RadioLabels = ["Vertical", "Horizontal"],
+            AssignHotKeys = true,
             Orientation = Orientation.Horizontal
         };
-        demoFrame.Add (rgOrientation);
+        demoFrame.Add (osOrientation);
 
-        rgOrientation.SelectedItemChanged += (s, e) =>
+        osOrientation.ValueChanged += (s, e) =>
                                              {
-                                                 if (e.SelectedItem == e.PreviousSelectedItem)
-                                                 {
-                                                     return;
-                                                 }
 
-                                                 if (rgOrientation.SelectedItem == 0)
+                                                 if (osOrientation.Value == Orientation.Horizontal)
                                                  {
                                                      scrollBar.Orientation = Orientation.Vertical;
                                                      scrollBar.X = Pos.AnchorEnd () - 5;

+ 287 - 0
Examples/UICatalog/Scenarios/Selectors.cs

@@ -0,0 +1,287 @@
+#nullable enable
+
+namespace UICatalog.Scenarios;
+
+[ScenarioMetadata ("Selectors", "Demonstrates OptionSelector and FlagSelector.")]
+[ScenarioCategory ("Controls")]
+public sealed class Selectors : Scenario
+{
+    public override void Main ()
+    {
+        // Init
+        Application.Init ();
+
+        // Setup - Create a top-level application window and configure it.
+        Window appWindow = new ()
+        {
+            Title = GetQuitKeyAndName (),
+            BorderStyle = LineStyle.None
+        };
+
+        FrameView? optionSelectorsFrame = null;
+        FrameView? flagSelectorsFrame = null;
+
+        OptionSelector<Orientation> orientationSelector = new ()
+        {
+            Orientation = Orientation.Horizontal,
+            BorderStyle = LineStyle.Dotted,
+            Title = "Selector Or_ientation",
+            Value = Orientation.Vertical
+        };
+        orientationSelector.ValueChanged += OrientationSelectorOnSelectedItemChanged;
+
+        FlagSelector<SelectorStyles> stylesSelector = new ()
+        {
+            X = Pos.Right (orientationSelector) + 1,
+            Orientation = Orientation.Horizontal,
+            BorderStyle = LineStyle.Dotted,
+            Title = "Selector St_yles",
+        };
+        stylesSelector.ValueChanged += StylesSelectorOnValueChanged;
+
+        NumericUpDown<int> horizontalSpace = new ()
+        {
+            X = 0,
+            Y = Pos.Bottom (orientationSelector),
+            Width = 11,
+            Title = "H_. Space",
+            Value = stylesSelector.HorizontalSpace,
+            BorderStyle = LineStyle.Dotted,
+        };
+        horizontalSpace.ValueChanging += HorizontalSpaceOnValueChanging;
+
+        CheckBox showBorderAndTitle = new ()
+        {
+            X = Pos.Right (horizontalSpace) + 1,
+            Y = Pos.Top (horizontalSpace),
+            Title = "Border _& Title",
+            CheckedState = CheckState.Checked,
+            BorderStyle = LineStyle.Dotted,
+        };
+        showBorderAndTitle.CheckedStateChanged += ShowBorderAndTitleOnCheckedStateChanged;
+
+        CheckBox canFocus = new ()
+        {
+            X = Pos.Right (showBorderAndTitle) + 1,
+            Y = Pos.Top (horizontalSpace),
+            Title = "_CanFocus",
+            CheckedState = CheckState.Checked,
+            BorderStyle = LineStyle.Dotted,
+        };
+        canFocus.CheckedStateChanged += CanFocusOnCheckedStateChanged;
+
+        optionSelectorsFrame = new ()
+        {
+            Y = Pos.Bottom (canFocus),
+            Width = Dim.Percent (50),
+            Height = Dim.Fill (),
+            Title = "O_ptionSelectors",
+            TabStop = TabBehavior.TabStop,
+            //InvertFocusAttribute = true
+        };
+        optionSelectorsFrame.ClearingViewport += (sender, args) =>
+                                                 {
+                                                     //               optionSelectorsFrame.SetAttributeForRole (optionSelectorsFrame.HasFocus ? VisualRole.Focus : VisualRole.Normal);
+                                                 };
+
+
+        Label label = new ()
+        {
+            Title = "Fo_ur Options:"
+        };
+
+        OptionSelector optionSelector = new ()
+        {
+            X = Pos.Right (label) + 1,
+            Title = "Fou_r Options",
+            BorderStyle = LineStyle.Dotted,
+            UsedHotKeys = { label.HotKey },
+            AssignHotKeys = true,
+            Labels = ["Option _1 (0)", "Option _2 (1)", "Option _3 (5) 你", "Option _Quattro (4) 你"],
+            Values = [0, 1, 5, 4],
+            Arrangement = ViewArrangement.Resizable,
+        };
+        optionSelectorsFrame.Add (label, optionSelector);
+
+        label = new ()
+        {
+            Y = Pos.Bottom (optionSelector),
+            Title = "<VisualRole_>:"
+        };
+
+        OptionSelector<VisualRole> optionSelectorT = new ()
+        {
+            X = Pos.Right (label) + 1,
+            Y = Pos.Bottom (optionSelector),
+            Title = "<Vi_sualRole>",
+            BorderStyle = LineStyle.Dotted,
+            UsedHotKeys = optionSelector.UsedHotKeys,
+            AssignHotKeys = true,
+        };
+
+        optionSelectorsFrame.Add (label, optionSelectorT);
+
+        flagSelectorsFrame = new ()
+        {
+            Y = Pos.Top (optionSelectorsFrame),
+            X = Pos.Right (optionSelectorsFrame),
+            Width = Dim.Fill (),
+            Height = Dim.Fill (),
+            Title = "_FlagSelectors",
+            TabStop = TabBehavior.TabStop
+        };
+
+        label = new ()
+        {
+            Title = "FlagSelector _(uint):"
+        };
+
+        FlagSelector flagSelector = new ()
+        {
+            X = Pos.Right (label) + 1,
+            UsedHotKeys = optionSelectorT.UsedHotKeys,
+            BorderStyle = LineStyle.Dotted,
+            Title = "FlagSe_lector (uint)",
+            AssignHotKeys = true,
+            Values =
+            [
+                0b_0001,
+                0b_0010,
+                0b_0100,
+                0b_1000,
+                0b_1111
+            ],
+            Labels =
+            [
+                "0x0001 One",
+                "0x0010 Two",
+                "0x0100 Quattro",
+                "0x1000 8",
+                "0x1111 Fifteen"
+            ]
+        };
+        flagSelectorsFrame.Add (label, flagSelector);
+
+        label = new ()
+        {
+            Y = Pos.Bottom (flagSelector),
+            Title = "_<ViewDiagnosticFlags>:"
+        };
+
+        FlagSelector<ViewDiagnosticFlags> flagSelectorT = new ()
+        {
+            X = Pos.Right (label) + 1,
+            BorderStyle = LineStyle.Dotted,
+            Title = "<ViewD_iagnosticFlags>",
+            Y = Pos.Bottom (flagSelector),
+            UsedHotKeys = flagSelector.UsedHotKeys,
+            AssignHotKeys = true,
+            Value = View.Diagnostics
+        };
+        flagSelectorsFrame.Add (label, flagSelectorT);
+        flagSelectorT.ValueChanged += (s, a) =>
+                                      {
+                                          View.Diagnostics = (ViewDiagnosticFlags)a.Value!;
+                                      };
+
+        appWindow.Add (orientationSelector, stylesSelector, horizontalSpace, showBorderAndTitle, canFocus, optionSelectorsFrame, flagSelectorsFrame);
+
+        // Run - Start the application.
+        Application.Run (appWindow);
+        appWindow.Dispose ();
+
+        // Shutdown - Calling Application.Shutdown is required.
+        Application.Shutdown ();
+
+        return;
+
+        void OrientationSelectorOnSelectedItemChanged (object? sender, EventArgs<Orientation?> e)
+        {
+            if (sender is not OptionSelector<Orientation> s)
+            {
+                return;
+            }
+
+            List<SelectorBase> selectors = GetAllSelectors ();
+            foreach (SelectorBase selector in selectors)
+            {
+                selector.Orientation = s.Value!.Value;
+            }
+        }
+
+        void StylesSelectorOnValueChanged (object? sender, EventArgs<SelectorStyles?> e)
+        {
+            if (sender is not FlagSelector<SelectorStyles> s)
+            {
+                return;
+            }
+
+            List<SelectorBase> selectors = GetAllSelectors ();
+
+            foreach (SelectorBase selector in selectors)
+            {
+                selector.Styles = s.Value!.Value;
+            }
+        }
+
+        void HorizontalSpaceOnValueChanging (object? sender, CancelEventArgs<int> e)
+        {
+            if (sender is not NumericUpDown<int> upDown || e.NewValue < 0)
+            {
+                e.Cancel = true;
+                return;
+            }
+
+            List<SelectorBase> selectors = GetAllSelectors ();
+
+            foreach (SelectorBase selector in selectors)
+            {
+                selector.HorizontalSpace = e.NewValue;
+            }
+        }
+
+
+        void ShowBorderAndTitleOnCheckedStateChanged (object? sender, EventArgs<CheckState> e)
+        {
+            if (sender is not CheckBox cb)
+            {
+                return;
+            }
+
+            List<SelectorBase> selectors = GetAllSelectors ();
+
+            foreach (SelectorBase selector in selectors)
+            {
+                selector.Border!.Thickness = cb.CheckedState == CheckState.Checked ? new (1) : new Thickness (0);
+            }
+        }
+
+        void CanFocusOnCheckedStateChanged (object? sender, EventArgs<CheckState> e)
+        {
+            if (sender is not CheckBox cb)
+            {
+                return;
+            }
+
+            List<SelectorBase> selectors = GetAllSelectors ();
+
+            foreach (SelectorBase selector in selectors)
+            {
+                selector.CanFocus = cb.CheckedState == CheckState.Checked;
+            }
+        }
+
+        List<SelectorBase> GetAllSelectors ()
+        {
+            List<SelectorBase> optionSelectors = [];
+            // ReSharper disable once AccessToModifiedClosure
+            optionSelectors.AddRange (optionSelectorsFrame!.SubViews.OfType<SelectorBase> ());
+            // ReSharper disable once AccessToModifiedClosure
+            optionSelectors.AddRange (flagSelectorsFrame!.SubViews.OfType<FlagSelector> ());
+
+            return optionSelectors;
+        }
+    }
+
+
+}

+ 102 - 65
Examples/UICatalog/Scenarios/Shortcuts.cs

@@ -66,47 +66,26 @@ public class Shortcuts : Scenario
             {
                 Text = "_Align Keys",
                 CanFocus = false,
-                HighlightStates = MouseState.None
+                HighlightStates = MouseState.None,
+                CheckedState = CheckState.Checked
             },
             Key = Key.F5.WithCtrl.WithAlt.WithShift
         };
 
-        // ((CheckBox)vShortcut3.CommandView).CheckedStateChanging += (_, args) =>
         ((CheckBox)alignKeysShortcut.CommandView).CheckedStateChanging += (s, e) =>
                                                                           {
                                                                               if (alignKeysShortcut.CommandView is CheckBox cb)
                                                                               {
+                                                                                  bool align = e.Result == CheckState.Checked;
                                                                                   eventSource.Add (
                                                                                                    $"{alignKeysShortcut.Id}.CommandView.CheckedStateChanging: {cb.Text}");
                                                                                   eventLog.MoveDown ();
 
-                                                                                  var max = 0;
-
-                                                                                  IEnumerable<View> toAlign =
-                                                                                      Application.Top.SubViews.Where (
-                                                                                       v => v is Shortcut { Width: not DimAbsolute });
-                                                                                  IEnumerable<View> enumerable = toAlign as View [] ?? toAlign.ToArray ();
-
-                                                                                  if (e.Result == CheckState.Checked)
-                                                                                  {
-                                                                                      max = (from Shortcut? peer in enumerable
-                                                                                             select peer.Key.ToString ().GetColumns ()).Prepend (max)
-                                                                                          .Max ();
-
-                                                                                      foreach (View view in enumerable)
-                                                                                      {
-                                                                                          var peer = (Shortcut)view;
-                                                                                          max = Math.Max (max, peer.KeyView.Text.GetColumns ());
-                                                                                      }
-                                                                                  }
-
-                                                                                  foreach (View view in enumerable)
-                                                                                  {
-                                                                                      var peer = (Shortcut)view;
-                                                                                      peer.MinimumKeyTextSize = max;
-                                                                                  }
+                                                                                  AlignKeys (align);
                                                                               }
                                                                           };
+
+
         Application.Top.Add (alignKeysShortcut);
 
         var commandFirstShortcut = new Shortcut
@@ -136,9 +115,7 @@ public class Shortcuts : Scenario
                                                                                                       $"{commandFirstShortcut.Id}.CommandView.CheckedStateChanging: {cb.Text}");
                                                                                      eventLog.MoveDown ();
 
-                                                                                     IEnumerable<View> toAlign =
-                                                                                         Application.Top.SubViews.Where (
-                                                                                          v => v is Shortcut { Width: not DimAbsolute });
+                                                                                     IEnumerable<View> toAlign = Application.Top.SubViews.OfType<Shortcut> ();
                                                                                      IEnumerable<View> enumerable = toAlign as View [] ?? toAlign.ToArray ();
 
                                                                                      foreach (View view in enumerable)
@@ -166,8 +143,8 @@ public class Shortcuts : Scenario
             Y = Pos.Bottom (commandFirstShortcut),
             Width = Dim.Fill ()! - Dim.Width (eventLog),
             Key = Key.F4,
-            HelpText = "Changes all Command.CanFocus",
-            CommandView = new CheckBox { Text = "_CanFocus" }
+            HelpText = "Changes all CommandView.CanFocus",
+            CommandView = new CheckBox { Text = "_CommandView.CanFocus" },
         };
 
         ((CheckBox)canFocusShortcut.CommandView).CheckedStateChanging += (s, e) =>
@@ -179,13 +156,7 @@ public class Shortcuts : Scenario
 
                                                                                  //cb.CanFocus = e.NewValue == CheckState.Checked;
 
-                                                                                 foreach (Shortcut peer in Application.Top.SubViews.Where (v => v is Shortcut)!)
-                                                                                 {
-                                                                                     if (peer.CanFocus)
-                                                                                     {
-                                                                                         peer.CommandView.CanFocus = e.Result == CheckState.Checked;
-                                                                                     }
-                                                                                 }
+                                                                                 SetCanFocus (e.Result == CheckState.Checked);
                                                                              }
                                                                          };
         Application.Top.Add (canFocusShortcut);
@@ -224,37 +195,39 @@ public class Shortcuts : Scenario
 
         Application.Top.Add (buttonShortcut);
 
-        var radioGroupShortcut = new Shortcut
+        var optionSelectorShortcut = new Shortcut
         {
-            Id = "radioGroupShortcut",
+            Id = "optionSelectorShortcut",
+            HelpText = "Option Selector",
             X = 0,
             Y = Pos.Bottom (buttonShortcut),
             Key = Key.F2,
             Width = Dim.Fill ()! - Dim.Width (eventLog),
-            CommandView = new RadioGroup
+            CommandView = new OptionSelector ()
             {
                 Orientation = Orientation.Vertical,
-                RadioLabels = ["O_ne", "T_wo", "Th_ree", "Fo_ur"]
-            }
+                Labels = ["O_ne", "T_wo", "Th_ree", "Fo_ur"],
+                HighlightStates = MouseState.None,
+            },
         };
 
-        ((RadioGroup)radioGroupShortcut.CommandView).SelectedItemChanged += (o, args) =>
-                                                                            {
-                                                                                if (o is { })
+        ((OptionSelector)optionSelectorShortcut.CommandView).ValueChanged += (o, args) =>
                                                                                 {
-                                                                                    eventSource.Add (
-                                                                                                     $"SelectedItemChanged: {o.GetType ().Name} - {args.SelectedItem}");
-                                                                                    eventLog.MoveDown ();
-                                                                                }
-                                                                            };
+                                                                                    if (o is { })
+                                                                                    {
+                                                                                        eventSource.Add (
+                                                                                                         $"ValueChanged: {o.GetType ().Name} - {args.Value}");
+                                                                                        eventLog.MoveDown ();
+                                                                                    }
+                                                                                };
 
-        Application.Top.Add (radioGroupShortcut);
+        Application.Top.Add (optionSelectorShortcut);
 
         var sliderShortcut = new Shortcut
         {
             Id = "sliderShortcut",
             X = 0,
-            Y = Pos.Bottom (radioGroupShortcut),
+            Y = Pos.Bottom (optionSelectorShortcut),
             Width = Dim.Fill ()! - Dim.Width (eventLog),
             HelpText = "Sliders work!",
             CommandView = new Slider<string>
@@ -277,12 +250,34 @@ public class Shortcuts : Scenario
 
         Application.Top.Add (sliderShortcut);
 
+        ListView listView = new ListView ()
+        {
+            Height = Dim.Auto (),
+            Width = Dim.Auto (),
+            Title = "ListView",
+            BorderStyle = LineStyle.Single
+        };
+        listView.EnableForDesign ();
+
+        var listViewShortcut = new Shortcut ()
+        {
+            Id = "listViewShortcut",
+            X = 0,
+            Y = Pos.Bottom (sliderShortcut),
+            Width = Dim.Fill ()! - Dim.Width (eventLog),
+            HelpText = "A ListView with Border",
+            CommandView = listView,
+            Key = Key.F5.WithCtrl,
+        };
+
+        Application.Top.Add (listViewShortcut);
+
         var noCommandShortcut = new Shortcut
         {
             Id = "noCommandShortcut",
             X = 0,
-            Y = Pos.Bottom (sliderShortcut),
-            Width = Dim.Width (sliderShortcut),
+            Y = Pos.Bottom (listViewShortcut),
+            Width = Dim.Width (listViewShortcut),
             HelpText = "No Command",
             Key = Key.D0
         };
@@ -321,13 +316,15 @@ public class Shortcuts : Scenario
             Id = "framedShortcut",
             X = 0,
             Y = Pos.Bottom (noHelpShortcut) + 1,
-            Title = "Framed",
+            Width = Dim.Width (noHelpShortcut),
+            Title = "Framed Shortcut",
             Key = Key.K.WithCtrl,
-            Text = "Resize frame",
+            Text = "Help: You can resize this",
             BorderStyle = LineStyle.Dotted,
             Arrangement = ViewArrangement.RightResizable | ViewArrangement.BottomResizable
         };
-        framedShortcut.Orientation = Orientation.Horizontal;
+        framedShortcut.Border!.Settings = BorderSettings.Title;
+        //framedShortcut.Orientation = Orientation.Horizontal;
 
         if (framedShortcut.Padding is { })
         {
@@ -337,12 +334,12 @@ public class Shortcuts : Scenario
 
         if (framedShortcut.CommandView.Margin is { })
         {
-            framedShortcut.CommandView.Margin!.SchemeName = framedShortcut.CommandView.SchemeName = "Error";
-            framedShortcut.HelpView.Margin!.SchemeName = framedShortcut.HelpView.SchemeName = "Dialog";
-            framedShortcut.KeyView.Margin!.SchemeName = framedShortcut.KeyView.SchemeName = "Menu";
+            framedShortcut.CommandView.SchemeName = framedShortcut.CommandView.SchemeName = SchemeManager.SchemesToSchemeName (Schemes.Dialog);
+            framedShortcut.HelpView.SchemeName = framedShortcut.HelpView.SchemeName = SchemeManager.SchemesToSchemeName (Schemes.Error);
+            framedShortcut.KeyView.SchemeName = framedShortcut.KeyView.SchemeName = SchemeManager.SchemesToSchemeName (Schemes.Base);
         }
 
-        framedShortcut.SchemeName = "TopLevel";
+        framedShortcut.SchemeName = SchemeManager.SchemesToSchemeName (Schemes.Toplevel);
         Application.Top.Add (framedShortcut);
 
         // Horizontal
@@ -504,7 +501,7 @@ public class Shortcuts : Scenario
                                                   eventSource.Add (
                                                                    $"{shortcut!.Id}.CommandView.Selecting: {shortcut!.CommandView.Text} {shortcut!.CommandView.GetType ().Name}");
                                                   eventLog.MoveDown ();
-                                                  args.Handled = true;
+                                                  //args.Handled = true;
                                               };
 
             shortcut.Accepting += (o, args) =>
@@ -513,7 +510,7 @@ public class Shortcuts : Scenario
                                       eventLog.MoveDown ();
 
                                       // We don't want this to exit the Scenario
-                                      args.Handled = true;
+                                      //args.Handled = true;
                                   };
 
             shortcut.CommandView.Accepting += (o, args) =>
@@ -523,6 +520,46 @@ public class Shortcuts : Scenario
                                                   eventLog.MoveDown ();
                                               };
         }
+
+        SetCanFocus (false);
+
+        AlignKeys (true);
+
+        return;
+
+        void SetCanFocus (bool canFocus)
+        {
+            foreach (Shortcut peer in Application.Top!.SubViews.OfType<Shortcut> ())
+            {
+                if (peer.CanFocus)
+                {
+                    peer.CommandView.CanFocus = canFocus;
+                }
+            }
+        }
+
+        void AlignKeys (bool align)
+        {
+            var max = 0;
+
+            IEnumerable<Shortcut> toAlign = Application.Top!.SubViews.OfType<Shortcut> ().Where(s => !s.Y.Has<PosAnchorEnd>(out _)).Cast<Shortcut>();
+            IEnumerable<Shortcut> enumerable = toAlign as Shortcut [] ?? toAlign.ToArray ();
+
+            if (align)
+            {
+                max = (from Shortcut? peer in enumerable
+                       select peer!.Key.ToString ().GetColumns ()).Prepend (max)
+                                                                  .Max ();
+
+                max = enumerable.Select (peer => peer.KeyView.Text.GetColumns ()).Prepend (max).Max ();
+            }
+
+            foreach (Shortcut shortcut in enumerable)
+            {
+                Shortcut peer = shortcut;
+                peer.MinimumKeyTextSize = max;
+            }
+        }
     }
 
     private void Button_Clicked (object? sender, CommandEventArgs e)

File diff suppressed because it is too large
+ 246 - 107
Examples/UICatalog/Scenarios/TableEditor.cs


+ 10 - 10
Examples/UICatalog/Scenarios/TextAlignmentAndDirection.cs

@@ -492,18 +492,18 @@ public class TextAlignmentAndDirection : Scenario
 
         // JUSTIFY OPTIONS
 
-        var justifyOptions = new RadioGroup
+        var justifyOptions = new OptionSelector
         {
             X = Pos.Left (justifyCheckbox) + 1,
             Y = Pos.Y (justifyCheckbox) + 1,
             Width = Dim.Fill (9),
-            RadioLabels = ["Current direction", "Opposite direction", "FIll Both"],
+            Labels = ["Current direction", "Opposite direction", "FIll Both"],
             Enabled = false
         };
 
         justifyCheckbox.CheckedStateChanging += (s, e) => ToggleJustify (e.Result != CheckState.Checked);
 
-        justifyOptions.SelectedItemChanged += (s, e) => { ToggleJustify (false, true); };
+        justifyOptions.ValueChanged += (_, _) => { ToggleJustify (false, true); };
 
         app.Add (justifyOptions);
 
@@ -541,17 +541,17 @@ public class TextAlignmentAndDirection : Scenario
 
         List<TextDirection> directionsEnum = Enum.GetValues (typeof (TextDirection)).Cast<TextDirection> ().ToList ();
 
-        var directionOptions = new RadioGroup
+        var directionOptions = new OptionSelector
         {
             X = Pos.Right (container) + 1,
             Y = Pos.Bottom (wrapCheckbox) + 1,
             Width = Dim.Fill (10),
             Height = Dim.Fill (1),
             HotKeySpecifier = (Rune)'\xffff',
-            RadioLabels = directionsEnum.Select (e => e.ToString ()).ToArray ()
+            Labels = directionsEnum.Select (e => e.ToString ()).ToArray ()
         };
 
-        directionOptions.SelectedItemChanged += (s, ev) =>
+        directionOptions.ValueChanged += (s, ev) =>
                                                 {
                                                     bool justChecked = justifyCheckbox.CheckedState == CheckState.Checked;
 
@@ -560,9 +560,9 @@ public class TextAlignmentAndDirection : Scenario
                                                         ToggleJustify (true);
                                                     }
 
-                                                    foreach (View v in multiLineLabels)
+                                                    foreach (View v in multiLineLabels.Where (v => ev.Value is { }))
                                                     {
-                                                        v.TextDirection = (TextDirection)ev.SelectedItem;
+                                                        v.TextDirection = (TextDirection)ev.Value!.Value;
                                                     }
 
                                                     if (justChecked)
@@ -612,7 +612,7 @@ public class TextAlignmentAndDirection : Scenario
 
                     if (TextFormatter.IsVerticalDirection (t.TextDirection))
                     {
-                        switch (justifyOptions.SelectedItem)
+                        switch (justifyOptions.Value)
                         {
                             case 0:
                                 t.VerticalTextAlignment = Alignment.Fill;
@@ -630,7 +630,7 @@ public class TextAlignmentAndDirection : Scenario
                     }
                     else
                     {
-                        switch (justifyOptions.SelectedItem)
+                        switch (justifyOptions.Value)
                         {
                             case 0:
                                 t.TextAlignment = Alignment.Fill;

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

@@ -498,5 +498,5 @@ public class TextInputControls : Scenario
 
 
 
-    private void TimeChanged (object sender, DateTimeEventArgs<TimeSpan> e) { _labelMirroringTimeField.Text = _timeField.Text; }
+    private void TimeChanged (object sender, EventArgs<TimeSpan> e) { _labelMirroringTimeField.Text = _timeField.Text; }
 }

+ 6 - 6
Examples/UICatalog/Scenarios/Themes.cs

@@ -23,26 +23,26 @@ public sealed class Themes : Scenario
         };
 
         string[]  options = ThemeManager.GetThemeNames ().Select (option => option = "_" + option).ToArray ();
-        RadioGroup themeOptionSelector = new ()
+        OptionSelector themeOptionSelector = new ()
         {
             Title = "_Themes",
             BorderStyle = LineStyle.Rounded,
             Width = Dim.Auto (),
             Height = Dim.Auto (),
-            RadioLabels= options,
-            SelectedItem = ThemeManager.GetThemeNames ().IndexOf (ThemeManager.Theme)
+            Labels= options,
+            Value = ThemeManager.GetThemeNames ().IndexOf (ThemeManager.Theme)
         };
         themeOptionSelector.Border!.Thickness = new (0, 1, 0, 0);
         themeOptionSelector.Margin!.Thickness = new (0, 0, 1, 0);
 
-        themeOptionSelector.SelectedItemChanged += (sender, args) =>
+        themeOptionSelector.ValueChanged += (sender, args) =>
                                              {
-                                                 RadioGroup? optionSelector = sender as RadioGroup;
+                                                 OptionSelector? optionSelector = sender as OptionSelector;
                                                  if (optionSelector is null)
                                                  {
                                                      return;
                                                  }
-                                                 var newTheme = optionSelector!.RadioLabels! [(int)args.SelectedItem!] as string;
+                                                 var newTheme = optionSelector!.Labels! [(int)args.Value!] as string;
                                                  // strip off the leading underscore
                                                  ThemeManager.Theme = newTheme!.Substring (1);
                                                  ConfigurationManager.Apply ();

+ 12 - 15
Examples/UICatalog/Scenarios/TimeAndDate.cs

@@ -1,4 +1,5 @@
-using System;
+#nullable enable
+using System;
 
 namespace UICatalog.Scenarios;
 
@@ -7,12 +8,12 @@ namespace UICatalog.Scenarios;
 [ScenarioCategory ("DateTime")]
 public class TimeAndDate : Scenario
 {
-    private Label _lblDateFmt;
-    private Label _lblNewDate;
-    private Label _lblNewTime;
-    private Label _lblOldDate;
-    private Label _lblOldTime;
-    private Label _lblTimeFmt;
+    private Label? _lblDateFmt;
+    private Label? _lblNewDate;
+    private Label? _lblNewTime;
+    private Label? _lblOldDate;
+    private Label? _lblOldTime;
+    private Label? _lblTimeFmt;
 
     public override void Main ()
     {
@@ -143,17 +144,13 @@ public class TimeAndDate : Scenario
         Application.Shutdown ();
     }
 
-    private void DateChanged (object sender, DateTimeEventArgs<DateTime> e)
+    private void DateChanged (object? sender, EventArgs<DateTime> e)
     {
-        _lblOldDate.Text = $"Old Date: {e.OldValue}";
-        _lblNewDate.Text = $"New Date: {e.NewValue}";
-        _lblDateFmt.Text = $"Date Format: {e.Format}";
+        _lblNewDate!.Text = $"New Date: {e.Value}";
     }
 
-    private void TimeChanged (object sender, DateTimeEventArgs<TimeSpan> e)
+    private void TimeChanged (object? sender, EventArgs<TimeSpan> e)
     {
-        _lblOldTime.Text = $"Old Time: {e.OldValue}";
-        _lblNewTime.Text = $"New Time: {e.NewValue}";
-        _lblTimeFmt.Text = $"Time Format: {e.Format}";
+        _lblNewTime!.Text = $"New Time: {e.Value}";
     }
 }

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

@@ -414,7 +414,7 @@ public class TreeViewFileSystem : Scenario
 
     private void ShowContextMenu (Point screenPoint, IFileSystemInfo forObject)
     {
-        PopoverMenu? contextMenu = new ([new ("Properties", $"Show {forObject.Name} properties", () => ShowPropertiesOf (forObject))]);
+        PopoverMenu contextMenu = new ([new ("Properties", $"Show {forObject.Name} properties", () => ShowPropertiesOf (forObject))]);
 
         // Registering with the PopoverManager will ensure that the context menu is closed when the view is no longer focused
         // and the context menu is disposed when it is closed.

+ 13 - 14
Examples/UICatalog/Scenarios/Unicode.cs

@@ -71,8 +71,7 @@ public class UnicodeInMenu : Scenario
         appWindow.Add (menu);
 
         var statusBar = new StatusBar (
-                                       new Shortcut []
-                                       {
+                                       [
                                            new (
                                                 Application.QuitKey,
                                                 "Выход",
@@ -80,7 +79,7 @@ public class UnicodeInMenu : Scenario
                                                ),
                                            new (Key.F2, "Создать", null),
                                            new (Key.F3, "Со_хранить", null)
-                                       }
+                                       ]
                                       );
         appWindow.Add (statusBar);
 
@@ -145,13 +144,13 @@ public class UnicodeInMenu : Scenario
         };
         appWindow.Add (checkBox, checkBoxRight);
 
-        label = new () { X = Pos.X (label), Y = Pos.Bottom (checkBoxRight) + 1, Text = "ComboBox:" };
-        appWindow.Add (label);
-        var comboBox = new ComboBox { X = 20, Y = Pos.Y (label), Width = Dim.Percent (50) };
-        comboBox.SetSource (new ObservableCollection<string> { gitString, "Со_хранить" });
+        //label = new () { X = Pos.X (label), Y = Pos.Bottom (checkBoxRight) + 1, Text = "ComboBox:" };
+        //appWindow.Add (label);
+        //var comboBox = new ComboBox { X = 20, Y = Pos.Y (label), Width = Dim.Percent (50) };
+        //comboBox.SetSource (new ObservableCollection<string> { gitString, "Со_хранить" });
 
-        appWindow.Add (comboBox);
-        comboBox.Text = gitString;
+        //appWindow.Add (comboBox);
+        //comboBox.Text = gitString;
 
         label = new () { X = Pos.X (label), Y = Pos.Bottom (label) + 2, Text = "HexView:" };
         appWindow.Add (label);
@@ -177,19 +176,19 @@ public class UnicodeInMenu : Scenario
         };
         appWindow.Add (listView);
 
-        label = new () { X = Pos.X (label), Y = Pos.Bottom (listView) + 1, Text = "RadioGroup:" };
+        label = new () { X = Pos.X (label), Y = Pos.Bottom (listView) + 1, Text = "OptionSelector:" };
         appWindow.Add (label);
 
-        var radioGroup = new RadioGroup
+        var optionSelector = new OptionSelector
         {
             X = 20,
             Y = Pos.Y (label),
             Width = Dim.Percent (60),
-            RadioLabels = new [] { "item #1", gitString, "Со_хранить", "𝔽𝕆𝕆𝔹𝔸ℝ" }
+            Labels = new [] { "item #1", gitString, "Со_хранить", "𝔽𝕆𝕆𝔹𝔸ℝ" }
         };
-        appWindow.Add (radioGroup);
+        appWindow.Add (optionSelector);
 
-        label = new () { X = Pos.X (label), Y = Pos.Bottom (radioGroup) + 1, Text = "TextField:" };
+        label = new () { X = Pos.X (label), Y = Pos.Bottom (optionSelector) + 1, Text = "TextField:" };
         appWindow.Add (label);
 
         var textField = new TextField

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

@@ -162,11 +162,11 @@ public class Wizards : Scenario
                                            firstStep.HelpText =
                                                "This is the End User License Agreement.\n\n\n\n\n\nThis is a test of the emergency broadcast system. This is a test of the emergency broadcast system.\nThis is a test of the emergency broadcast system.\n\n\nThis is a test of the emergency broadcast system.\n\nThis is a test of the emergency broadcast system.\n\n\n\nThe end of the EULA.";
 
-                                           RadioGroup radioGroup = new ()
+                                           OptionSelector optionSelector = new ()
                                            {
-                                               RadioLabels = ["_One", "_Two", "_3"]
+                                               Labels = ["_One", "_Two", "_3"]
                                            };
-                                           firstStep.Add (radioGroup);
+                                           firstStep.Add (optionSelector);
 
                                            wizard.AddStep (firstStep);
 
@@ -184,12 +184,12 @@ public class Wizards : Scenario
                                                Text = "Press Me to Rename Step", X = Pos.Right (buttonLbl), Y = Pos.Top (buttonLbl)
                                            };
 
-                                           RadioGroup radioGroup2 = new ()
+                                           OptionSelector optionSelecor2 = new ()
                                            {
-                                               RadioLabels = ["_A", "_B", "_C"],
+                                               Labels = ["_A", "_B", "_C"],
                                                Orientation = Orientation.Horizontal
                                            };
-                                           secondStep.Add (radioGroup2);
+                                           secondStep.Add (optionSelecor2);
 
                                            button.Accepting += (s, e) =>
                                                             {

+ 5 - 5
Examples/UICatalog/UICatalog.cs

@@ -80,7 +80,7 @@ public class UICatalog
         // Get allowed driver names
         string? [] allowedDrivers = Application.GetDriverTypes ().Item2.ToArray ();
 
-        Option<string> driverOption = new Option<string> ("--driver", "The IConsoleDriver to use.")
+        Option<string> driverOption = new Option<string> ("--driver", "The IDriver to use.")
             .FromAmong (allowedDrivers!);
         driverOption.SetDefaultValue (string.Empty);
         driverOption.AddAlias ("-d");
@@ -635,7 +635,7 @@ public class UICatalog
         if (!View.EnableDebugIDisposableAsserts)
         {
             View.Instances.Clear ();
-            RunState.Instances.Clear ();
+            SessionToken.Instances.Clear ();
 
             return;
         }
@@ -650,15 +650,15 @@ public class UICatalog
 
         View.Instances.Clear ();
 
-        // Validate there are no outstanding Application.RunState-based instances 
+        // Validate there are no outstanding Application sessions
         // after a scenario was selected to run. This proves the main UI Catalog
         // 'app' closed cleanly.
-        foreach (RunState? inst in RunState.Instances)
+        foreach (SessionToken? inst in SessionToken.Instances)
         {
             Debug.Assert (inst.WasDisposed);
         }
 
-        RunState.Instances.Clear ();
+        SessionToken.Instances.Clear ();
 #endif
     }
 }

+ 52 - 44
Examples/UICatalog/UICatalogTop.cs

@@ -96,9 +96,9 @@ public class UICatalogTop : Toplevel
 
     private readonly MenuBarv2? _menuBar;
     private CheckBox? _force16ColorsMenuItemCb;
-    private OptionSelector? _themesRg;
-    private OptionSelector? _topSchemeRg;
-    private OptionSelector? _logLevelRg;
+    private OptionSelector? _themesSelector;
+    private OptionSelector? _topSchemesSelector;
+    private OptionSelector? _logLevelSelector;
     private FlagSelector<ViewDiagnosticFlags>? _diagnosticFlagsSelector;
     private CheckBox? _disableMouseCb;
 
@@ -126,13 +126,13 @@ public class UICatalogTop : Toplevel
                                           [
                                               new MenuItemv2 (
                                                               "_Documentation",
-                                                              "",
+                                                              "API docs",
                                                               () => OpenUrl ("https://gui-cs.github.io/Terminal.Gui"),
                                                               Key.F1
                                                              ),
                                               new MenuItemv2 (
                                                               "_README",
-                                                              "",
+                                                              "Project readme",
                                                               () => OpenUrl ("https://github.com/gui-cs/Terminal.Gui"),
                                                               Key.F2
                                                              ),
@@ -163,7 +163,10 @@ public class UICatalogTop : Toplevel
             _force16ColorsMenuItemCb = new ()
             {
                 Title = "Force _16 Colors",
-                CheckedState = Application.Force16Colors ? CheckState.Checked : CheckState.UnChecked
+                CheckedState = Application.Force16Colors ? CheckState.Checked : CheckState.UnChecked,
+                // Best practice for CheckBoxes in menus is to disable focus and highlight states
+                CanFocus = false,
+                HighlightStates = MouseState.None
             };
 
             _force16ColorsMenuItemCb.CheckedStateChanging += (sender, args) =>
@@ -194,23 +197,25 @@ public class UICatalogTop : Toplevel
 
             if (ConfigurationManager.IsEnabled)
             {
-                _themesRg = new ()
+                _themesSelector = new ()
                 {
-                    HighlightStates = Terminal.Gui.ViewBase.MouseState.None,
+                    // HighlightStates = MouseState.In,
+                    CanFocus = true,
+                    // InvertFocusAttribute = true
                 };
 
-                _themesRg.SelectedItemChanged += (_, args) =>
+                _themesSelector.ValueChanged += (_, args) =>
                                                  {
-                                                     if (args.SelectedItem is null)
+                                                     if (args.Value is null)
                                                      {
                                                          return;
                                                      }
-                                                     ThemeManager.Theme = ThemeManager.GetThemeNames () [args.SelectedItem!.Value];
+                                                     ThemeManager.Theme = ThemeManager.GetThemeNames () [(int)args.Value];
                                                  };
 
                 var menuItem = new MenuItemv2
                 {
-                    CommandView = _themesRg,
+                    CommandView = _themesSelector,
                     HelpText = "Cycle Through Themes",
                     Key = Key.T.WithCtrl
                 };
@@ -218,18 +223,18 @@ public class UICatalogTop : Toplevel
 
                 menuItems.Add (new Line ());
 
-                _topSchemeRg = new ()
+                _topSchemesSelector = new ()
                 {
-                    HighlightStates = Terminal.Gui.ViewBase.MouseState.None,
+                    //  HighlightStates = MouseState.In,
                 };
 
-                _topSchemeRg.SelectedItemChanged += (_, args) =>
+                _topSchemesSelector.ValueChanged += (_, args) =>
                                                     {
-                                                        if (args.SelectedItem is null)
+                                                        if (args.Value is null)
                                                         {
                                                             return;
                                                         }
-                                                        CachedTopLevelScheme = SchemeManager.GetSchemesForCurrentTheme ()!.Keys.ToArray () [args.SelectedItem!.Value];
+                                                        CachedTopLevelScheme = SchemeManager.GetSchemesForCurrentTheme ()!.Keys.ToArray () [(int)args.Value];
                                                         SchemeName = CachedTopLevelScheme;
                                                         SetNeedsDraw ();
                                                     };
@@ -241,7 +246,7 @@ public class UICatalogTop : Toplevel
                                    [
                                        new ()
                                        {
-                                           CommandView = _topSchemeRg,
+                                           CommandView = _topSchemesSelector,
                                            HelpText = "Cycle Through schemes",
                                            Key = Key.S.WithCtrl
                                        }
@@ -269,12 +274,12 @@ public class UICatalogTop : Toplevel
 
             _diagnosticFlagsSelector = new ()
             {
-                CanFocus = true,
-                Styles = FlagSelectorStyles.ShowNone,
-                HighlightStates = Terminal.Gui.ViewBase.MouseState.None,
+                Styles = SelectorStyles.ShowNoneFlag,
+                CanFocus = true
+
             };
             _diagnosticFlagsSelector.UsedHotKeys.Add (Key.D);
-            _diagnosticFlagsSelector.AssignHotKeysToCheckBoxes = true;
+            _diagnosticFlagsSelector.AssignHotKeys = true;
             _diagnosticFlagsSelector.Value = Diagnostics;
             _diagnosticFlagsSelector.ValueChanged += (sender, args) =>
                                                      {
@@ -294,7 +299,10 @@ public class UICatalogTop : Toplevel
             _disableMouseCb = new ()
             {
                 Title = "_Disable Mouse",
-                CheckedState = Application.IsMouseDisabled ? CheckState.Checked : CheckState.UnChecked
+                CheckedState = Application.IsMouseDisabled ? CheckState.Checked : CheckState.UnChecked,
+                // Best practice for CheckBoxes in menus is to disable focus and highlight states
+                CanFocus = false,
+                HighlightStates = MouseState.None
             };
 
             _disableMouseCb.CheckedStateChanged += (_, args) => { Application.IsMouseDisabled = args.Value == CheckState.Checked; };
@@ -315,17 +323,17 @@ public class UICatalogTop : Toplevel
 
             LogLevel [] logLevels = Enum.GetValues<LogLevel> ();
 
-            _logLevelRg = new ()
+            _logLevelSelector = new ()
             {
-                AssignHotKeysToCheckBoxes = true,
-                Options = Enum.GetNames<LogLevel> (),
-                SelectedItem = logLevels.ToList ().IndexOf (Enum.Parse<LogLevel> (UICatalog.Options.DebugLogLevel)),
-                HighlightStates = Terminal.Gui.ViewBase.MouseState.In
+                AssignHotKeys = true,
+                Labels = Enum.GetNames<LogLevel> (),
+                Value = logLevels.ToList ().IndexOf (Enum.Parse<LogLevel> (UICatalog.Options.DebugLogLevel)),
+                // HighlightStates = MouseState.In,
             };
 
-            _logLevelRg.SelectedItemChanged += (_, args) =>
+            _logLevelSelector.ValueChanged += (_, args) =>
             {
-                UICatalog.Options = UICatalog.Options with { DebugLogLevel = Enum.GetName (logLevels [args.SelectedItem!.Value])! };
+                UICatalog.Options = UICatalog.Options with { DebugLogLevel = Enum.GetName (logLevels [args.Value!.Value])! };
 
                 UICatalog.LogLevelSwitch.MinimumLevel =
                     UICatalog.LogLevelToLogEventLevel (Enum.Parse<LogLevel> (UICatalog.Options.DebugLogLevel));
@@ -334,7 +342,7 @@ public class UICatalogTop : Toplevel
             menuItems.Add (
                            new MenuItemv2
                            {
-                               CommandView = _logLevelRg,
+                               CommandView = _logLevelSelector,
                                HelpText = "Cycle Through Log Levels",
                                Key = Key.L.WithCtrl
                            });
@@ -356,27 +364,27 @@ public class UICatalogTop : Toplevel
 
     private void UpdateThemesMenu ()
     {
-        if (_themesRg is null)
+        if (_themesSelector is null)
         {
             return;
         }
 
-        _themesRg.SelectedItem = null;
-        _themesRg.AssignHotKeysToCheckBoxes = true;
-        _themesRg.UsedHotKeys.Clear ();
-        _themesRg.Options = ThemeManager.GetThemeNames ();
-        _themesRg.SelectedItem =ThemeManager.GetThemeNames ().IndexOf (ThemeManager.GetCurrentThemeName ());
+        _themesSelector.Value = null;
+        _themesSelector.AssignHotKeys = true;
+        _themesSelector.UsedHotKeys.Clear ();
+        _themesSelector.Labels = ThemeManager.GetThemeNames ().ToArray ();
+        _themesSelector.Value = ThemeManager.GetThemeNames ().IndexOf (ThemeManager.GetCurrentThemeName ());
 
-        if (_topSchemeRg is null)
+        if (_topSchemesSelector is null)
         {
             return;
         }
 
-        _topSchemeRg.AssignHotKeysToCheckBoxes = true;
-        _topSchemeRg.UsedHotKeys.Clear ();
-        int? selectedScheme = _topSchemeRg.SelectedItem;
-        _topSchemeRg.Options = SchemeManager.GetSchemeNames ();
-        _topSchemeRg.SelectedItem = selectedScheme;
+        _topSchemesSelector.AssignHotKeys = true;
+        _topSchemesSelector.UsedHotKeys.Clear ();
+        int? selectedScheme = _topSchemesSelector.Value;
+        _topSchemesSelector.Labels = SchemeManager.GetSchemeNames ().ToArray ();
+        _topSchemesSelector.Value = selectedScheme;
 
         if (CachedTopLevelScheme is null || !SchemeManager.GetSchemeNames ().Contains (CachedTopLevelScheme))
         {
@@ -387,7 +395,7 @@ public class UICatalogTop : Toplevel
         // if the item is in bounds then select it
         if (newSelectedItem >= 0 && newSelectedItem < SchemeManager.GetSchemeNames ().Count)
         {
-            _topSchemeRg.SelectedItem = newSelectedItem;
+            _topSchemesSelector.Value = newSelectedItem;
         }
     }
 

+ 1 - 2
README.md

@@ -1,6 +1,5 @@
-![.NET Core](https://github.com/gui-cs/Terminal.Gui/workflows/.NET%20Core/badge.svg?branch=develop)
 [![Version](https://img.shields.io/nuget/v/Terminal.Gui.svg)](https://www.nuget.org/packages/Terminal.Gui)
-![Code Coverage](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/migueldeicaza/90ef67a684cb71db1817921a970f8d27/raw/code-coverage.json)
+[![codecov](https://codecov.io/gh/gui-cs/Terminal.Gui/graph/badge.svg?token=1Ac9gyGtrj)](https://codecov.io/gh/gui-cs/Terminal.Gui)
 [![Downloads](https://img.shields.io/nuget/dt/Terminal.Gui)](https://www.nuget.org/packages/Terminal.Gui)
 [![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)

+ 109 - 0
Scripts/Run-LocalCoverage.ps1

@@ -0,0 +1,109 @@
+# ------------------------------------------------------------
+# Run-LocalCoverage.ps1
+# Local-only: Unit + Parallel + Integration tests
+# Quiet, merged coverage in /Tests
+# ------------------------------------------------------------
+
+# 1. Define paths
+$testDir    = Join-Path $PWD "Tests"
+$covDir     = Join-Path $testDir "coverage"
+$reportDir  = Join-Path $testDir "report"
+$resultsDir = Join-Path $testDir "TestResults"
+$mergedFile = Join-Path $covDir "coverage.merged.cobertura.xml"
+
+# 2. Clean old results - INCLUDING TestResults directory
+Write-Host "Cleaning old coverage files and test results..."
+Remove-Item -Recurse -Force $covDir, $reportDir, $resultsDir -ErrorAction SilentlyContinue
+New-Item -ItemType Directory -Path $covDir, $reportDir -Force | Out-Null
+
+dotnet build --configuration Debug --no-restore
+
+# ------------------------------------------------------------
+# 3. Run UNIT TESTS (non-parallel)
+# ------------------------------------------------------------
+Write-Host "`nRunning UnitTests (quiet)..."
+dotnet test Tests/UnitTests `
+  --no-build `
+  --verbosity minimal `
+  --collect:"XPlat Code Coverage" `
+  --settings Tests/UnitTests/runsettings.coverage.xml `
+  --blame-hang-timeout 10s
+
+# ------------------------------------------------------------
+# 4. Run UNIT TESTS (parallel)
+# ------------------------------------------------------------
+Write-Host "`nRunning UnitTestsParallelizable (quiet)..."
+dotnet test Tests/UnitTestsParallelizable `
+  --no-build `
+  --verbosity minimal `
+  --collect:"XPlat Code Coverage" `
+  --settings Tests/UnitTestsParallelizable/runsettings.coverage.xml
+
+# ------------------------------------------------------------
+# 5. Run INTEGRATION TESTS
+# ------------------------------------------------------------
+Write-Host "`nRunning IntegrationTests (quiet)..."
+dotnet test Tests/IntegrationTests `
+  --no-build `
+  --verbosity minimal `
+  --collect:"XPlat Code Coverage" `
+  --settings Tests/IntegrationTests/runsettings.coverage.xml
+
+# ------------------------------------------------------------
+# 6. Find ALL coverage files (from all 3 projects) - NOW SCOPED TO Tests/TestResults
+# ------------------------------------------------------------
+Write-Host "`nCollecting coverage files..."
+$covFiles = Get-ChildItem -Path $resultsDir -Recurse -Filter coverage.cobertura.xml -File -ErrorAction SilentlyContinue
+
+if (-not $covFiles) {
+    Write-Error "No coverage files found in $resultsDir. Did all tests run successfully?"
+    exit 1
+}
+
+# ------------------------------------------------------------
+# 7. Move to Tests/coverage
+# ------------------------------------------------------------
+Write-Host "Moving $($covFiles.Count) coverage file(s) to $covDir..."
+$fileIndex = 1
+foreach ($f in $covFiles) {
+    $destFile = Join-Path $covDir "coverage.$fileIndex.cobertura.xml"
+    Copy-Item $f.FullName -Destination $destFile -Force
+    $fileIndex++
+}
+
+# ------------------------------------------------------------
+# 8. Merge into one file
+# ------------------------------------------------------------
+Write-Host "Merging coverage from all test projects..."
+dotnet-coverage merge `
+    -o $mergedFile `
+    -f cobertura `
+    "$covDir\*.cobertura.xml"
+
+# ------------------------------------------------------------
+# 9. Generate HTML + text report
+# ------------------------------------------------------------
+Write-Host "Generating final HTML report..."
+reportgenerator `
+    -reports:$mergedFile `
+    -targetdir:$reportDir `
+    -reporttypes:"Html;TextSummary"
+
+# ------------------------------------------------------------
+# 10. Show summary + open report
+# ------------------------------------------------------------
+Write-Host "`n=== Final Coverage Summary (Unit + Integration) ==="
+Get-Content "$reportDir\Summary.txt"
+
+$indexHtml = Join-Path $reportDir "index.html"
+if (Test-Path $indexHtml) {
+    Write-Host "Opening report in browser..."
+    Start-Process $indexHtml
+} else {
+    Write-Warning "HTML report not found at $indexHtml"
+}
+
+Write-Host "`nCoverage artifacts:"
+Write-Host "  - Merged coverage: $mergedFile"
+Write-Host "  - HTML report:     $reportDir\index.html"
+Write-Host "  - Test results:    $resultsDir"

+ 62 - 56
Terminal.Gui.Analyzers.Tests/ProjectBuilder.cs

@@ -17,10 +17,10 @@ using JetBrains.Annotations;
 
 public sealed class ProjectBuilder
 {
-    private string _sourceCode;
-    private string _expectedFixedCode;
-    private DiagnosticAnalyzer _analyzer;
-    private CodeFixProvider _codeFix;
+    private string? _sourceCode;
+    private string? _expectedFixedCode;
+    private DiagnosticAnalyzer? _analyzer;
+    private CodeFixProvider? _codeFix;
 
     public ProjectBuilder WithSourceCode (string source)
     {
@@ -59,20 +59,22 @@ public sealed class ProjectBuilder
         }
 
         // Parse original document
-        var document = CreateDocument (_sourceCode);
-        var compilation = await document.Project.GetCompilationAsync ();
+        Document document = CreateDocument (_sourceCode);
+        Compilation? compilation = await document.Project.GetCompilationAsync ();
 
-        var diagnostics = compilation.GetDiagnostics ();
-        var errors = diagnostics.Where (d => d.Severity == DiagnosticSeverity.Error);
+        ImmutableArray<Diagnostic> diagnostics = compilation!.GetDiagnostics ();
+        IEnumerable<Diagnostic> errors = diagnostics.Where (d => d.Severity == DiagnosticSeverity.Error);
 
-        if (errors.Any ())
+        IEnumerable<Diagnostic> enumerable = errors as Diagnostic [] ?? errors.ToArray ();
+
+        if (enumerable.Any ())
         {
-            var errorMessages = string.Join (Environment.NewLine, errors.Select (e => e.ToString ()));
+            string errorMessages = string.Join (Environment.NewLine, enumerable.Select (e => e.ToString ()));
             throw new Exception ("Compilation failed with errors:" + Environment.NewLine + errorMessages);
         }
 
         // Run analyzer
-        var analyzerDiagnostics = await GetAnalyzerDiagnosticsAsync (compilation, _analyzer);
+        ImmutableArray<Diagnostic> analyzerDiagnostics = await GetAnalyzerDiagnosticsAsync (compilation, _analyzer);
 
         Assert.NotEmpty (analyzerDiagnostics);
 
@@ -83,83 +85,87 @@ public sealed class ProjectBuilder
                 throw new InvalidOperationException ("Expected code fix but none was set.");
             }
 
-            var fixedDocument = await ApplyCodeFixAsync (document, analyzerDiagnostics.First (), _codeFix);
+            Document? fixedDocument = await ApplyCodeFixAsync (document, analyzerDiagnostics.First (), _codeFix);
 
-            var formattedDocument = await Formatter.FormatAsync (fixedDocument);
-            var fixedSource = (await formattedDocument.GetTextAsync ()).ToString ();
+            if (fixedDocument is { })
+            {
+                Document formattedDocument = await Formatter.FormatAsync (fixedDocument);
+                string fixedSource = (await formattedDocument.GetTextAsync ()).ToString ();
 
-            Assert.Equal (_expectedFixedCode, fixedSource);
+                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),
+        string dd = typeof (Enumerable).GetTypeInfo ().Assembly.Location;
+        DirectoryInfo coreDir = Directory.GetParent (dd) ?? throw new Exception ($"Could not find parent directory of dotnet sdk.  Sdk directory was {dd}");
+
+        AdhocWorkspace workspace = new AdhocWorkspace ();
+        ProjectId projectId = ProjectId.CreateNewId ();
+        DocumentId documentId = DocumentId.CreateNewId (projectId);
+
+        List<MetadataReference> references =
+        [
+            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 (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"))
 
-            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);
+        ProjectInfo 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));
+        Solution 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));
+        CompilationWithAnalyzers compilationWithAnalyzers = compilation.WithAnalyzers ([analyzer]);
         return await compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync ();
     }
 
-    private static async Task<Document> ApplyCodeFixAsync (Document document, Diagnostic diagnostic, CodeFixProvider codeFix)
+    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);
+        CodeAction? codeAction = null;
+        var context = new CodeFixContext ((TextDocument)document, diagnostic, (action, _) => codeAction = action, CancellationToken.None);
 
         await codeFix.RegisterCodeFixesAsync (context);
 
-        if (_codeAction == null)
+        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;
+        ImmutableArray<CodeActionOperation> operations = await codeAction.GetOperationsAsync (CancellationToken.None);
+        Solution solution = operations.OfType<ApplyChangesOperation> ().First ().ChangedSolution;
         return solution.GetDocument (document.Id);
     }
 }

+ 36 - 22
Terminal.Gui/App/Application.Driver.cs

@@ -1,23 +1,19 @@
 #nullable enable
 
+using System.Diagnostics.CodeAnalysis;
+
 namespace Terminal.Gui.App;
 
 public static partial class Application // Driver abstractions
 {
-    internal static bool _forceFakeConsole;
-
-    /// <summary>Gets the <see cref="IConsoleDriver"/> that has been selected. See also <see cref="ForceDriver"/>.</summary>
-    public static IConsoleDriver? Driver
+    /// <inheritdoc cref="IApplication.Driver"/>
+    public static IDriver? Driver
     {
         get => ApplicationImpl.Instance.Driver;
         internal set => ApplicationImpl.Instance.Driver = value;
     }
 
-    /// <summary>
-    ///     Gets or sets whether <see cref="Application.Driver"/> will be forced to output only the 16 colors defined in
-    ///     <see cref="ColorName16"/>. The default is <see langword="false"/>, meaning 24-bit (TrueColor) colors will be output
-    ///     as long as the selected <see cref="IConsoleDriver"/> supports TrueColor.
-    /// </summary>
+    /// <inheritdoc cref="IApplication.Force16Colors"/>
     [ConfigurationProperty (Scope = typeof (SettingsScope))]
     public static bool Force16Colors
     {
@@ -25,14 +21,7 @@ public static partial class Application // Driver abstractions
         set => ApplicationImpl.Instance.Force16Colors = value;
     }
 
-    /// <summary>
-    ///     Forces the use of the specified driver (one of "fake", "dotnet", "windows", or "unix"). If not
-    ///     specified, the driver is selected based on the platform.
-    /// </summary>
-    /// <remarks>
-    ///     Note, <see cref="Application.Init(IConsoleDriver, string)"/> will override this configuration setting if called
-    ///     with either `driver` or `driverName` specified.
-    /// </remarks>
+    /// <inheritdoc cref="IApplication.ForceDriver"/>
     [ConfigurationProperty (Scope = typeof (SettingsScope))]
     public static string ForceDriver
     {
@@ -40,9 +29,34 @@ public static partial class Application // Driver abstractions
         set => ApplicationImpl.Instance.ForceDriver = value;
     }
 
-    /// <summary>
-    /// Collection of sixel images to write out to screen when updating.
-    /// Only add to this collection if you are sure terminal supports sixel format.
-    /// </summary>
+    /// <inheritdoc cref="IApplication.Sixel"/>
     public static List<SixelToRender> Sixel => ApplicationImpl.Instance.Sixel;
-}
+
+    /// <summary>Gets a list of <see cref="IDriver"/> types and type names that are available.</summary>
+    /// <returns></returns>
+    [RequiresUnreferencedCode ("AOT")]
+    public static (List<Type?>, List<string?>) GetDriverTypes ()
+    {
+        // use reflection to get the list of drivers
+        List<Type?> driverTypes = new ();
+
+        // Only inspect the IDriver assembly
+        var asm = typeof (IDriver).Assembly;
+
+        foreach (Type? type in asm.GetTypes ())
+        {
+            if (typeof (IDriver).IsAssignableFrom (type) && type is { IsAbstract: false, IsClass: true })
+            {
+                driverTypes.Add (type);
+            }
+        }
+
+        List<string?> driverTypeNames = driverTypes
+                                        .Where (d => !typeof (IDriver).IsAssignableFrom (d))
+                                        .Select (d => d!.Name)
+                                        .Union (["dotnet", "windows", "unix", "fake"])
+                                        .ToList ()!;
+
+        return (driverTypes, driverTypeNames);
+    }
+}

+ 13 - 57
Terminal.Gui/App/Application.Keyboard.cs

@@ -4,9 +4,7 @@ namespace Terminal.Gui.App;
 
 public static partial class Application // Keyboard handling
 {
-    /// <summary>
-    /// Static reference to the current <see cref="IApplication"/> <see cref="IKeyboard"/>.
-    /// </summary>
+    /// <inheritdoc cref="IApplication.Keyboard"/>
     public static IKeyboard Keyboard
     {
         get => ApplicationImpl.Instance.Keyboard;
@@ -14,40 +12,14 @@ public static partial class Application // Keyboard handling
                                                            throw new ArgumentNullException(nameof(value));
     }
 
-    /// <summary>
-    ///     Called when the user presses a key (by the <see cref="IConsoleDriver"/>). Raises the cancelable
-    ///     <see cref="KeyDown"/> event, then calls <see cref="View.NewKeyDownEvent"/> on all top level views, and finally
-    ///     if the key was not handled, invokes any Application-scoped <see cref="KeyBindings"/>.
-    /// </summary>
-    /// <remarks>Can be used to simulate key press events.</remarks>
-    /// <param name="key"></param>
-    /// <returns><see langword="true"/> if the key was handled.</returns>
-    public static bool RaiseKeyDownEvent (Key key) => Keyboard.RaiseKeyDownEvent (key);
+    /// <inheritdoc cref="IKeyboard.RaiseKeyDownEvent"/>
+    public static bool RaiseKeyDownEvent (Key key) => ApplicationImpl.Instance.Keyboard.RaiseKeyDownEvent (key);
 
-    /// <summary>
-    ///     Invokes any commands bound at the Application-level to <paramref name="key"/>.
-    /// </summary>
-    /// <param name="key"></param>
-    /// <returns>
-    ///     <see langword="null"/> if no command was found; input processing should continue.
-    ///     <see langword="false"/> if the command was invoked and was not handled (or cancelled); input processing should continue.
-    ///     <see langword="true"/> if the command was invoked the command was handled (or cancelled); input processing should stop.
-    /// </returns>
-    public static bool? InvokeCommandsBoundToKey (Key key) => Keyboard.InvokeCommandsBoundToKey (key);
+    /// <inheritdoc cref="IKeyboard.InvokeCommandsBoundToKey"/>
+    public static bool? InvokeCommandsBoundToKey (Key key) => ApplicationImpl.Instance.Keyboard.InvokeCommandsBoundToKey (key);
 
-    /// <summary>
-    ///     Invokes an Application-bound command.
-    /// </summary>
-    /// <param name="command">The Command to invoke</param>
-    /// <param name="key">The Application-bound Key that was pressed.</param>
-    /// <param name="binding">Describes the binding.</param>
-    /// <returns>
-    ///     <see langword="null"/> if no command was found; input processing should continue.
-    ///     <see langword="false"/> if the command was invoked and was not handled (or cancelled); input processing should continue.
-    ///     <see langword="true"/> if the command was invoked the command was handled (or cancelled); input processing should stop.
-    /// </returns>
-    /// <exception cref="NotSupportedException"></exception>
-    public static bool? InvokeCommand (Command command, Key key, KeyBinding binding) => Keyboard.InvokeCommand (command, key, binding);
+    /// <inheritdoc cref="IKeyboard.InvokeCommand"/>
+    public static bool? InvokeCommand (Command command, Key key, KeyBinding binding) => ApplicationImpl.Instance.Keyboard.InvokeCommand (command, key, binding);
 
     /// <summary>
     ///     Raised when the user presses a key.
@@ -63,29 +35,13 @@ public static partial class Application // Keyboard handling
     /// </remarks>
     public static event EventHandler<Key>? KeyDown
     {
-        add => Keyboard.KeyDown += value;
-        remove => Keyboard.KeyDown -= value;
+        add => ApplicationImpl.Instance.Keyboard.KeyDown += value;
+        remove => ApplicationImpl.Instance.Keyboard.KeyDown -= value;
     }
 
-    /// <summary>
-    ///     Called when the user releases a key (by the <see cref="IConsoleDriver"/>). Raises the cancelable
-    ///     <see cref="KeyUp"/>
-    ///     event
-    ///     then calls <see cref="View.NewKeyUpEvent"/> on all top level views. Called after <see cref="RaiseKeyDownEvent"/>.
-    /// </summary>
-    /// <remarks>Can be used to simulate key release events.</remarks>
-    /// <param name="key"></param>
-    /// <returns><see langword="true"/> if the key was handled.</returns>
-    public static bool RaiseKeyUpEvent (Key key) => Keyboard.RaiseKeyUpEvent (key);
-
-    /// <summary>Gets the Application-scoped key bindings.</summary>
-    public static KeyBindings KeyBindings => Keyboard.KeyBindings;
+    /// <inheritdoc cref="IKeyboard.RaiseKeyUpEvent"/>
+    public static bool RaiseKeyUpEvent (Key key) => ApplicationImpl.Instance.Keyboard.RaiseKeyUpEvent (key);
 
-    internal static void AddKeyBindings ()
-    {
-        if (Keyboard is KeyboardImpl keyboard)
-        {
-            keyboard.AddKeyBindings ();
-        }
-    }
+    /// <inheritdoc cref="IKeyboard.KeyBindings"/>
+    public static KeyBindings KeyBindings => ApplicationImpl.Instance.Keyboard.KeyBindings;
 }

+ 25 - 77
Terminal.Gui/App/Application.Lifecycle.cs

@@ -2,6 +2,10 @@
 using System.Diagnostics;
 using System.Diagnostics.CodeAnalysis;
 using System.Reflection;
+using Microsoft.VisualBasic;
+using Terminal.Gui.App;
+using Terminal.Gui.Drivers;
+using Terminal.Gui.Views;
 
 namespace Terminal.Gui.App;
 
@@ -11,7 +15,7 @@ public static partial class Application // Lifecycle (Init/Shutdown)
     /// <summary>Initializes a new instance of a Terminal.Gui Application. <see cref="Shutdown"/> must be called when the application is closing.</summary>
     /// <para>Call this method once per instance (or after <see cref="Shutdown"/> has been called).</para>
     /// <para>
-    ///     This function loads the right <see cref="IConsoleDriver"/> for the platform, Creates a <see cref="Toplevel"/>. and
+    ///     This function loads the right <see cref="IDriver"/> for the platform, Creates a <see cref="Toplevel"/>. and
     ///     assigns it to <see cref="Top"/>
     /// </para>
     /// <para>
@@ -22,90 +26,36 @@ public static partial class Application // Lifecycle (Init/Shutdown)
     /// </para>
     /// <para>
     ///     The <see cref="Run{T}"/> function combines
-    ///     <see cref="Init(IConsoleDriver,string)"/> and <see cref="Run(Toplevel, Func{Exception, bool})"/>
+    ///     <see cref="Init(IDriver,string)"/> and <see cref="Run(Toplevel, Func{Exception, bool})"/>
     ///     into a single
     ///     call. An application can use <see cref="Run{T}"/> without explicitly calling
-    ///     <see cref="Init(IConsoleDriver,string)"/>.
+    ///     <see cref="Init(IDriver,string)"/>.
     /// </para>
     /// <param name="driver">
-    ///     The <see cref="IConsoleDriver"/> to use. If neither <paramref name="driver"/> or
+    ///     The <see cref="IDriver"/> to use. If neither <paramref name="driver"/> or
     ///     <paramref name="driverName"/> are specified the default driver for the platform will be used.
     /// </param>
     /// <param name="driverName">
     ///     The short name (e.g. "dotnet", "windows", "unix", or "fake") of the
-    ///     <see cref="IConsoleDriver"/> to use. If neither <paramref name="driver"/> or <paramref name="driverName"/> are
+    ///     <see cref="IDriver"/> to use. If neither <paramref name="driver"/> or <paramref name="driverName"/> are
     ///     specified the default driver for the platform will be used.
     /// </param>
     [RequiresUnreferencedCode ("AOT")]
     [RequiresDynamicCode ("AOT")]
-    public static void Init (IConsoleDriver? driver = null, string? driverName = null)
+    public static void Init (IDriver? driver = null, string? driverName = null)
     {
         ApplicationImpl.Instance.Init (driver, driverName ?? ForceDriver);
     }
 
-    internal static int MainThreadId
+    /// <summary>
+    ///     Gets or sets the main thread ID for the application.
+    /// </summary>
+    public static int? MainThreadId
     {
         get => ((ApplicationImpl)ApplicationImpl.Instance).MainThreadId;
         set => ((ApplicationImpl)ApplicationImpl.Instance).MainThreadId = value;
     }
 
-    internal static void SubscribeDriverEvents ()
-    {
-        ArgumentNullException.ThrowIfNull (Driver);
-
-        Driver.SizeChanged += Driver_SizeChanged;
-        Driver.KeyDown += Driver_KeyDown;
-        Driver.KeyUp += Driver_KeyUp;
-        Driver.MouseEvent += Driver_MouseEvent;
-    }
-
-    internal static void UnsubscribeDriverEvents ()
-    {
-        ArgumentNullException.ThrowIfNull (Driver);
-
-        Driver.SizeChanged -= Driver_SizeChanged;
-        Driver.KeyDown -= Driver_KeyDown;
-        Driver.KeyUp -= Driver_KeyUp;
-        Driver.MouseEvent -= Driver_MouseEvent;
-    }
-
-    private static void Driver_SizeChanged (object? sender, SizeChangedEventArgs e)
-    {
-        RaiseScreenChangedEvent (new Rectangle (new (0, 0), e.Size!.Value));
-    }
-    private static void Driver_KeyDown (object? sender, Key e) { RaiseKeyDownEvent (e); }
-    private static void Driver_KeyUp (object? sender, Key e) { RaiseKeyUpEvent (e); }
-    private static void Driver_MouseEvent (object? sender, MouseEventArgs e) { RaiseMouseEvent (e); }
-
-    /// <summary>Gets a list of <see cref="IConsoleDriver"/> types and type names that are available.</summary>
-    /// <returns></returns>
-    [RequiresUnreferencedCode ("AOT")]
-    public static (List<Type?>, List<string?>) GetDriverTypes ()
-    {
-        // use reflection to get the list of drivers
-        List<Type?> driverTypes = new ();
-
-        // Only inspect the IConsoleDriver assembly
-        var asm = typeof (IConsoleDriver).Assembly;
-
-        foreach (Type? type in asm.GetTypes ())
-        {
-            if (typeof (IConsoleDriver).IsAssignableFrom (type) &&
-                type is { IsAbstract: false, IsClass: true })
-            {
-                driverTypes.Add (type);
-            }
-        }
-
-        List<string?> driverTypeNames = driverTypes
-                                        .Where (d => !typeof (IConsoleDriverFacade).IsAssignableFrom (d))
-                                        .Select (d => d!.Name)
-                                        .Union (["dotnet", "windows", "unix", "fake"])
-                                        .ToList ()!;
-
-        return (driverTypes, driverTypeNames);
-    }
-
     /// <summary>Shutdown an application initialized with <see cref="Init"/>.</summary>
     /// <remarks>
     ///     Shutdown must be called for every call to <see cref="Init"/> or
@@ -129,19 +79,17 @@ public static partial class Application // Lifecycle (Init/Shutdown)
         internal set => ApplicationImpl.Instance.Initialized = value;
     }
 
-    /// <summary>
-    ///     This event is raised after the <see cref="Init"/> and <see cref="Shutdown"/> methods have been called.
-    /// </summary>
-    /// <remarks>
-    ///     Intended to support unit tests that need to know when the application has been initialized.
-    /// </remarks>
-    public static event EventHandler<EventArgs<bool>>? InitializedChanged;
-
-    /// <summary>
-    ///  Raises the <see cref="InitializedChanged"/> event.
-    /// </summary>
-    internal static void OnInitializedChanged (object sender, EventArgs<bool> e)
+    /// <inheritdoc cref="IApplication.InitializedChanged"/>
+    public static event EventHandler<EventArgs<bool>>? InitializedChanged
     {
-        Application.InitializedChanged?.Invoke (sender, e);
+        add => ApplicationImpl.Instance.InitializedChanged += value;
+        remove => ApplicationImpl.Instance.InitializedChanged -= value;
     }
+
+    // IMPORTANT: Ensure all property/fields are reset here. See Init_ResetState_Resets_Properties unit test.
+    // Encapsulate all setting of initial state for Application; Having
+    // this in a function like this ensures we don't make mistakes in
+    // guaranteeing that the state of this singleton is deterministic when Init
+    // starts running and after Shutdown returns.
+    internal static void ResetState (bool ignoreDisposed = false) => ApplicationImpl.Instance?.ResetState (ignoreDisposed);
 }

+ 4 - 6
Terminal.Gui/App/Application.Mouse.cs

@@ -92,10 +92,8 @@ public static partial class Application // Mouse handling
     /// </summary>
     /// <remarks>This method can be used to simulate a mouse event, e.g. in unit tests.</remarks>
     /// <param name="mouseEvent">The mouse event with coordinates relative to the screen.</param>
-    internal static void RaiseMouseEvent (MouseEventArgs mouseEvent) { Mouse.RaiseMouseEvent (mouseEvent); }
-
-    /// <summary>
-    ///     INTERNAL: Clears mouse state during application reset.
-    /// </summary>
-    internal static void ResetMouseState () { Mouse.ResetState (); }
+    internal static void RaiseMouseEvent (MouseEventArgs mouseEvent)
+    {
+        Mouse.RaiseMouseEvent (mouseEvent);
+    }
 }

+ 10 - 10
Terminal.Gui/App/Application.Navigation.cs

@@ -17,16 +17,16 @@ public static partial class Application // Navigation stuff
     [ConfigurationProperty (Scope = typeof (SettingsScope))]
     public static Key NextTabGroupKey
     {
-        get => Keyboard.NextTabGroupKey;
-        set => Keyboard.NextTabGroupKey = value;
+        get => ApplicationImpl.Instance.Keyboard.NextTabGroupKey;
+        set => ApplicationImpl.Instance.Keyboard.NextTabGroupKey = value;
     }
 
     /// <summary>Alternative key to navigate forwards through views. Tab is the primary key.</summary>
     [ConfigurationProperty (Scope = typeof (SettingsScope))]
     public static Key NextTabKey
     {
-        get => Keyboard.NextTabKey;
-        set => Keyboard.NextTabKey = value;
+        get => ApplicationImpl.Instance.Keyboard.NextTabKey;
+        set => ApplicationImpl.Instance.Keyboard.NextTabKey = value;
     }
 
     /// <summary>
@@ -43,23 +43,23 @@ public static partial class Application // Navigation stuff
     /// </remarks>
     public static event EventHandler<Key>? KeyUp
     {
-        add => Keyboard.KeyUp += value;
-        remove => Keyboard.KeyUp -= value;
+        add => ApplicationImpl.Instance.Keyboard.KeyUp += value;
+        remove => ApplicationImpl.Instance.Keyboard.KeyUp -= value;
     }
 
     /// <summary>Alternative key to navigate backwards through views. Shift+Ctrl+Tab is the primary key.</summary>
     [ConfigurationProperty (Scope = typeof (SettingsScope))]
     public static Key PrevTabGroupKey
     {
-        get => Keyboard.PrevTabGroupKey;
-        set => Keyboard.PrevTabGroupKey = value;
+        get => ApplicationImpl.Instance.Keyboard.PrevTabGroupKey;
+        set => ApplicationImpl.Instance.Keyboard.PrevTabGroupKey = value;
     }
 
     /// <summary>Alternative key to navigate backwards through views. Shift+Tab is the primary key.</summary>
     [ConfigurationProperty (Scope = typeof (SettingsScope))]
     public static Key PrevTabKey
     {
-        get => Keyboard.PrevTabKey;
-        set => Keyboard.PrevTabKey = value;
+        get => ApplicationImpl.Instance.Keyboard.PrevTabKey;
+        set => ApplicationImpl.Instance.Keyboard.PrevTabKey = value;
     }
 }

+ 50 - 462
Terminal.Gui/App/Application.Run.cs

@@ -1,5 +1,4 @@
 #nullable enable
-using System.Diagnostics;
 using System.Diagnostics.CodeAnalysis;
 
 namespace Terminal.Gui.App;
@@ -10,498 +9,87 @@ public static partial class Application // Run (Begin -> Run -> Layout/Draw -> E
     [ConfigurationProperty (Scope = typeof (SettingsScope))]
     public static Key QuitKey
     {
-        get => Keyboard.QuitKey;
-        set => Keyboard.QuitKey = value;
+        get => ApplicationImpl.Instance.Keyboard.QuitKey;
+        set => ApplicationImpl.Instance.Keyboard.QuitKey = value;
     }
 
     /// <summary>Gets or sets the key to activate arranging views using the keyboard.</summary>
     [ConfigurationProperty (Scope = typeof (SettingsScope))]
     public static Key ArrangeKey
     {
-        get => Keyboard.ArrangeKey;
-        set => Keyboard.ArrangeKey = value;
+        get => ApplicationImpl.Instance.Keyboard.ArrangeKey;
+        set => ApplicationImpl.Instance.Keyboard.ArrangeKey = value;
     }
 
-    /// <summary>
-    ///     Notify that a new <see cref="RunState"/> was created (<see cref="Begin(Toplevel)"/> was called). The token is
-    ///     created in <see cref="Begin(Toplevel)"/> and this event will be fired before that function exits.
-    /// </summary>
-    /// <remarks>
-    ///     If <see cref="EndAfterFirstIteration"/> is <see langword="true"/> callers to <see cref="Begin(Toplevel)"/>
-    ///     must also subscribe to <see cref="NotifyStopRunState"/> and manually dispose of the <see cref="RunState"/> token
-    ///     when the application is done.
-    /// </remarks>
-    public static event EventHandler<RunStateEventArgs>? NotifyNewRunState;
+    /// <inheritdoc cref="IApplication.Begin"/>
+    public static SessionToken Begin (Toplevel toplevel) => ApplicationImpl.Instance.Begin (toplevel);
 
-    /// <summary>Notify that an existent <see cref="RunState"/> is stopping (<see cref="End(RunState)"/> was called).</summary>
-    /// <remarks>
-    ///     If <see cref="EndAfterFirstIteration"/> is <see langword="true"/> callers to <see cref="Begin(Toplevel)"/>
-    ///     must also subscribe to <see cref="NotifyStopRunState"/> and manually dispose of the <see cref="RunState"/> token
-    ///     when the application is done.
-    /// </remarks>
-#pragma warning disable CS0067 // Event is never used
-#pragma warning disable CS0414 // Event is never used
-    public static event EventHandler<ToplevelEventArgs>? NotifyStopRunState;
-#pragma warning restore CS0414 // Event is never used
-#pragma warning restore CS0067 // Event is never used
+    /// <inheritdoc cref="IApplication.PositionCursor"/>
+    public static bool PositionCursor () => ApplicationImpl.Instance.PositionCursor ();
 
-    /// <summary>Building block API: Prepares the provided <see cref="Toplevel"/> for execution.</summary>
-    /// <returns>
-    ///     The <see cref="RunState"/> handle that needs to be passed to the <see cref="End(RunState)"/> method upon
-    ///     completion.
-    /// </returns>
-    /// <param name="toplevel">The <see cref="Toplevel"/> to prepare execution for.</param>
-    /// <remarks>
-    ///     This method prepares the provided <see cref="Toplevel"/> for running with the focus, it adds this to the list
-    ///     of <see cref="Toplevel"/>s, lays out the SubViews, focuses the first element, and draws the <see cref="Toplevel"/>
-    ///     in the screen. This is usually followed by executing the <see cref="RunLoop"/> method, and then the
-    ///     <see cref="End(RunState)"/> method upon termination which will undo these changes.
-    /// </remarks>
-    public static RunState Begin (Toplevel toplevel)
-    {
-        ArgumentNullException.ThrowIfNull (toplevel);
-
-        // Ensure the mouse is ungrabbed.
-        if (Mouse.MouseGrabView is { })
-        {
-            Mouse.UngrabMouse ();
-        }
-
-        var rs = new RunState (toplevel);
-
-#if DEBUG_IDISPOSABLE
-        if (View.EnableDebugIDisposableAsserts && Top is { } && toplevel != Top && !TopLevels.Contains (Top))
-        {
-            // This assertion confirm if the Top was already disposed
-            Debug.Assert (Top.WasDisposed);
-            Debug.Assert (Top == CachedRunStateToplevel);
-        }
-#endif
-
-        lock (TopLevels)
-        {
-            if (Top is { } && toplevel != Top && !TopLevels.Contains (Top))
-            {
-                // If Top was already disposed and isn't on the Toplevels Stack,
-                // clean it up here if is the same as _cachedRunStateToplevel
-                if (Top == CachedRunStateToplevel)
-                {
-                    Top = null;
-                }
-                else
-                {
-                    // Probably this will never hit
-                    throw new ObjectDisposedException (Top.GetType ().FullName);
-                }
-            }
-
-            // BUGBUG: We should not depend on `Id` internally.
-            // BUGBUG: It is super unclear what this code does anyway.
-            if (string.IsNullOrEmpty (toplevel.Id))
-            {
-                var count = 1;
-                var id = (TopLevels.Count + count).ToString ();
-
-                while (TopLevels.Count > 0 && TopLevels.FirstOrDefault (x => x.Id == id) is { })
-                {
-                    count++;
-                    id = (TopLevels.Count + count).ToString ();
-                }
-
-                toplevel.Id = (TopLevels.Count + count).ToString ();
-
-                TopLevels.Push (toplevel);
-            }
-            else
-            {
-                Toplevel? dup = TopLevels.FirstOrDefault (x => x.Id == toplevel.Id);
-
-                if (dup is null)
-                {
-                    TopLevels.Push (toplevel);
-                }
-            }
-        }
-
-        if (Top is null)
-        {
-            Top = toplevel;
-        }
-
-        if ((Top?.Modal == false && toplevel.Modal)
-            || (Top?.Modal == false && !toplevel.Modal)
-            || (Top?.Modal == true && toplevel.Modal))
-        {
-            if (toplevel.Visible)
-            {
-                if (Top is { HasFocus: true })
-                {
-                    Top.HasFocus = false;
-                }
-
-                // Force leave events for any entered views in the old Top
-                if (GetLastMousePosition () is { })
-                {
-                    RaiseMouseEnterLeaveEvents (GetLastMousePosition ()!.Value, new ());
-                }
-
-                Top?.OnDeactivate (toplevel);
-                Toplevel previousTop = Top!;
-
-                Top = toplevel;
-                Top.OnActivate (previousTop);
-            }
-        }
-
-        // View implements ISupportInitializeNotification which is derived from ISupportInitialize
-        if (!toplevel.IsInitialized)
-        {
-            toplevel.BeginInit ();
-            toplevel.EndInit (); // Calls Layout
-        }
-
-        // Try to set initial focus to any TabStop
-        if (!toplevel.HasFocus)
-        {
-            toplevel.SetFocus ();
-        }
-
-        toplevel.OnLoaded ();
-
-        ApplicationImpl.Instance.LayoutAndDraw (true);
-
-        if (PositionCursor ())
-        {
-            Driver?.UpdateCursor ();
-        }
-
-        NotifyNewRunState?.Invoke (toplevel, new (rs));
-
-        return rs;
-    }
-
-    /// <summary>
-    ///     Calls <see cref="View.PositionCursor"/> on the most focused view.
-    /// </summary>
-    /// <remarks>
-    ///     Does nothing if there is no most focused view.
-    ///     <para>
-    ///         If the most focused view is not visible within it's superview, the cursor will be hidden.
-    ///     </para>
-    /// </remarks>
-    /// <returns><see langword="true"/> if a view positioned the cursor and the position is visible.</returns>
-    internal static bool PositionCursor ()
-    {
-        // Find the most focused view and position the cursor there.
-        View? mostFocused = Navigation?.GetFocused ();
-
-        // If the view is not visible or enabled, don't position the cursor
-        if (mostFocused is null || !mostFocused.Visible || !mostFocused.Enabled)
-        {
-            var current = CursorVisibility.Invisible;
-            Driver?.GetCursorVisibility (out current);
-
-            if (current != CursorVisibility.Invisible)
-            {
-                Driver?.SetCursorVisibility (CursorVisibility.Invisible);
-            }
-
-            return false;
-        }
-
-        // If the view is not visible within it's superview, don't position the cursor
-        Rectangle mostFocusedViewport = mostFocused.ViewportToScreen (mostFocused.Viewport with { Location = Point.Empty });
-
-        Rectangle superViewViewport =
-            mostFocused.SuperView?.ViewportToScreen (mostFocused.SuperView.Viewport with { Location = Point.Empty }) ?? Driver!.Screen;
-
-        if (!superViewViewport.IntersectsWith (mostFocusedViewport))
-        {
-            return false;
-        }
-
-        Point? cursor = mostFocused.PositionCursor ();
-
-        Driver!.GetCursorVisibility (out CursorVisibility currentCursorVisibility);
-
-        if (cursor is { })
-        {
-            // Convert cursor to screen coords
-            cursor = mostFocused.ViewportToScreen (mostFocused.Viewport with { Location = cursor.Value }).Location;
-
-            // If the cursor is not in a visible location in the SuperView, hide it
-            if (!superViewViewport.Contains (cursor.Value))
-            {
-                if (currentCursorVisibility != CursorVisibility.Invisible)
-                {
-                    Driver.SetCursorVisibility (CursorVisibility.Invisible);
-                }
-
-                return false;
-            }
-
-            // Show it
-            if (currentCursorVisibility == CursorVisibility.Invisible)
-            {
-                Driver.SetCursorVisibility (mostFocused.CursorVisibility);
-            }
-
-            return true;
-        }
-
-        if (currentCursorVisibility != CursorVisibility.Invisible)
-        {
-            Driver.SetCursorVisibility (CursorVisibility.Invisible);
-        }
-
-        return false;
-    }
-
-    /// <summary>
-    ///     Runs the application by creating a <see cref="Toplevel"/> object and calling
-    ///     <see cref="Run(Toplevel, Func{Exception, bool})"/>.
-    /// </summary>
-    /// <remarks>
-    ///     <para>Calling <see cref="Init"/> first is not needed as this function will initialize the application.</para>
-    ///     <para>
-    ///         <see cref="Shutdown"/> must be called when the application is closing (typically after Run> has returned) to
-    ///         ensure resources are cleaned up and terminal settings restored.
-    ///     </para>
-    ///     <para>
-    ///         The caller is responsible for disposing the object returned by this method.
-    ///     </para>
-    /// </remarks>
-    /// <returns>The created <see cref="Toplevel"/> object. The caller is responsible for disposing this object.</returns>
+    /// <inheritdoc cref="IApplication.Run(Func{Exception, bool}, string)"/>
     [RequiresUnreferencedCode ("AOT")]
     [RequiresDynamicCode ("AOT")]
-    public static Toplevel Run (Func<Exception, bool>? errorHandler = null, IConsoleDriver? driver = null)
-    {
-        return ApplicationImpl.Instance.Run (errorHandler, driver);
-    }
+    public static Toplevel Run (Func<Exception, bool>? errorHandler = null, string? driver = null) => ApplicationImpl.Instance.Run (errorHandler, driver);
 
-    /// <summary>
-    ///     Runs the application by creating a <see cref="Toplevel"/>-derived object of type <c>T</c> and calling
-    ///     <see cref="Run(Toplevel, Func{Exception, bool})"/>.
-    /// </summary>
-    /// <remarks>
-    ///     <para>Calling <see cref="Init"/> first is not needed as this function will initialize the application.</para>
-    ///     <para>
-    ///         <see cref="Shutdown"/> must be called when the application is closing (typically after Run> has returned) to
-    ///         ensure resources are cleaned up and terminal settings restored.
-    ///     </para>
-    ///     <para>
-    ///         The caller is responsible for disposing the object returned by this method.
-    ///     </para>
-    /// </remarks>
-    /// <param name="errorHandler"></param>
-    /// <param name="driver">
-    ///     The <see cref="IConsoleDriver"/> to use. If not specified the default driver for the platform will
-    ///     be used. Must be <see langword="null"/> if <see cref="Init"/> has already been called.
-    /// </param>
-    /// <returns>The created T object. The caller is responsible for disposing this object.</returns>
+    /// <inheritdoc cref="IApplication.Run{TView}(Func{Exception, bool}, string)"/>
     [RequiresUnreferencedCode ("AOT")]
     [RequiresDynamicCode ("AOT")]
-    public static T Run<T> (Func<Exception, bool>? errorHandler = null, IConsoleDriver? driver = null)
-        where T : Toplevel, new()
-    {
-        return ApplicationImpl.Instance.Run<T> (errorHandler, driver);
-    }
+    public static TView Run<TView> (Func<Exception, bool>? errorHandler = null, string? driver = null)
+        where TView : Toplevel, new() => ApplicationImpl.Instance.Run<TView> (errorHandler, driver);
 
-    /// <summary>Runs the Application using the provided <see cref="Toplevel"/> view.</summary>
-    /// <remarks>
-    ///     <para>
-    ///         This method is used to start processing events for the main application, but it is also used to run other
-    ///         modal <see cref="View"/>s such as <see cref="Dialog"/> boxes.
-    ///     </para>
-    ///     <para>
-    ///         To make a <see cref="Run(Toplevel,System.Func{System.Exception,bool})"/> stop execution, call
-    ///         <see cref="Application.RequestStop"/>.
-    ///     </para>
-    ///     <para>
-    ///         Calling <see cref="Run(Toplevel,System.Func{System.Exception,bool})"/> is equivalent to calling
-    ///         <see cref="Begin(Toplevel)"/>, followed by <see cref="RunLoop(RunState)"/>, and then calling
-    ///         <see cref="End(RunState)"/>.
-    ///     </para>
-    ///     <para>
-    ///         Alternatively, to have a program control the main loop and process events manually, call
-    ///         <see cref="Begin(Toplevel)"/> to set things up manually and then repeatedly call
-    ///         <see cref="RunLoop(RunState)"/> with the wait parameter set to false. By doing this the
-    ///         <see cref="RunLoop(RunState)"/> method will only process any pending events, timers handlers and then
-    ///         return control immediately.
-    ///     </para>
-    ///     <para>
-    ///         When using <see cref="Run{T}"/> or
-    ///         <see cref="Run(System.Func{System.Exception,bool},IConsoleDriver)"/>
-    ///         <see cref="Init"/> will be called automatically.
-    ///     </para>
-    ///     <para>
-    ///         RELEASE builds only: When <paramref name="errorHandler"/> is <see langword="null"/> any exceptions will be
-    ///         rethrown. Otherwise, if <paramref name="errorHandler"/> will be called. If <paramref name="errorHandler"/>
-    ///         returns <see langword="true"/> the <see cref="RunLoop(RunState)"/> will resume; otherwise this method will
-    ///         exit.
-    ///     </para>
-    /// </remarks>
-    /// <param name="view">The <see cref="Toplevel"/> to run as a modal.</param>
-    /// <param name="errorHandler">
-    ///     RELEASE builds only: Handler for any unhandled exceptions (resumes when returns true,
-    ///     rethrows when null).
-    /// </param>
-    public static void Run (Toplevel view, Func<Exception, bool>? errorHandler = null) { ApplicationImpl.Instance.Run (view, errorHandler); }
+    /// <inheritdoc cref="IApplication.Run(Toplevel, Func{Exception, bool})"/>
+    public static void Run (Toplevel view, Func<Exception, bool>? errorHandler = null) => ApplicationImpl.Instance.Run (view, errorHandler);
 
-    /// <summary>Adds a timeout to the application.</summary>
-    /// <remarks>
-    ///     When time specified passes, the callback will be invoked. If the callback returns true, the timeout will be
-    ///     reset, repeating the invocation. If it returns false, the timeout will stop and be removed. The returned value is a
-    ///     token that can be used to stop the timeout by calling <see cref="RemoveTimeout(object)"/>.
-    /// </remarks>
-    public static object? AddTimeout (TimeSpan time, Func<bool> callback) { return ApplicationImpl.Instance.AddTimeout (time, callback); }
+    /// <inheritdoc cref="IApplication.AddTimeout"/>
+    public static object? AddTimeout (TimeSpan time, Func<bool> callback) => ApplicationImpl.Instance.AddTimeout (time, callback);
 
-    /// <summary>Removes a previously scheduled timeout</summary>
-    /// <remarks>The token parameter is the value returned by <see cref="AddTimeout"/>.</remarks>
-    /// Returns
-    /// <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 static bool RemoveTimeout (object token) { return ApplicationImpl.Instance.RemoveTimeout (token); }
-
-    /// <summary>Runs <paramref name="action"/> on the thread that is processing events</summary>
-    /// <param name="action">the action to be invoked on the main processing thread.</param>
-    public static void Invoke (Action action) { ApplicationImpl.Instance.Invoke (action); }
-
-    /// <summary>
-    ///     Causes any Toplevels that need layout to be laid out. Then draws any Toplevels that need display. Only Views that
-    ///     need to be laid out (see <see cref="View.NeedsLayout"/>) will be laid out.
-    ///     Only Views that need to be drawn (see <see cref="View.NeedsDraw"/>) will be drawn.
-    /// </summary>
-    /// <param name="forceRedraw">
-    ///     If <see langword="true"/> the entire View hierarchy will be redrawn. The default is <see langword="false"/> and
-    ///     should only be overriden for testing.
-    /// </param>
-    public static void LayoutAndDraw (bool forceRedraw = false)
-    {
-        ApplicationImpl.Instance.LayoutAndDraw (forceRedraw);
-    }
+    /// <inheritdoc cref="IApplication.RemoveTimeout"/>
+    public static bool RemoveTimeout (object token) => ApplicationImpl.Instance.RemoveTimeout (token);
 
-    /// <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;
+    /// <inheritdoc cref="IApplication.TimedEvents"/>
+    /// 
+    public static ITimedEvents? TimedEvents => ApplicationImpl.Instance?.TimedEvents;
+    /// <inheritdoc cref="IApplication.Invoke"/>
+    public static void Invoke (Action action) => ApplicationImpl.Instance.Invoke (action);
 
-    /// <summary>
-    ///     Set to true to cause <see cref="End"/> to be called after the first iteration. Set to false (the default) to
-    ///     cause the application to continue running until Application.RequestStop () is called.
-    /// </summary>
-    public static bool EndAfterFirstIteration { get; set; }
+    /// <inheritdoc cref="IApplication.LayoutAndDraw"/>
+    public static void LayoutAndDraw (bool forceRedraw = false) => ApplicationImpl.Instance.LayoutAndDraw (forceRedraw);
 
-    /// <summary>Building block API: Runs the main loop for the created <see cref="Toplevel"/>.</summary>
-    /// <param name="state">The state returned by the <see cref="Begin(Toplevel)"/> method.</param>
-    public static void RunLoop (RunState state)
+    /// <inheritdoc cref="IApplication.StopAfterFirstIteration"/>
+    public static bool StopAfterFirstIteration
     {
-        ArgumentNullException.ThrowIfNull (state);
-        ObjectDisposedException.ThrowIf (state.Toplevel is null, "state");
-
-        var firstIteration = true;
+        get => ApplicationImpl.Instance.StopAfterFirstIteration;
+        set => ApplicationImpl.Instance.StopAfterFirstIteration = value;
+    }
 
-        for (state.Toplevel.Running = true; state.Toplevel?.Running == true;)
-        {
-            if (EndAfterFirstIteration && !firstIteration)
-            {
-                return;
-            }
+    /// <inheritdoc cref="IApplication.RequestStop(Toplevel)"/>
+    public static void RequestStop (Toplevel? top = null) => ApplicationImpl.Instance.RequestStop (top);
 
-            firstIteration = RunIteration (ref state, firstIteration);
-        }
+    /// <inheritdoc cref="IApplication.End"/>
+    public static void End (SessionToken sessionToken) => ApplicationImpl.Instance.End (sessionToken);
 
-        // Run one last iteration to consume any outstanding input events from Driver
-        // This is important for remaining OnKeyUp events.
-        RunIteration (ref state, firstIteration);
-    }
+    /// <inheritdoc cref="IApplication.RaiseIteration"/>
+    internal static void RaiseIteration () => ApplicationImpl.Instance.RaiseIteration ();
 
-    /// <summary>Run one application iteration.</summary>
-    /// <param name="state">The state returned by <see cref="Begin(Toplevel)"/>.</param>
-    /// <param name="firstIteration">
-    ///     Set to <see langword="true"/> if this is the first run loop iteration.
-    /// </param>
-    /// <returns><see langword="false"/> if at least one iteration happened.</returns>
-    public static bool RunIteration (ref RunState state, bool firstIteration = false)
+    /// <inheritdoc cref="IApplication.Iteration"/>
+    public static event EventHandler<IterationEventArgs>? Iteration
     {
-        ApplicationImpl appImpl = (ApplicationImpl)ApplicationImpl.Instance;
-        appImpl.Coordinator?.RunIteration ();
-
-        return false;
+        add => ApplicationImpl.Instance.Iteration += value;
+        remove => ApplicationImpl.Instance.Iteration -= value;
     }
 
-    /// <summary>Stops the provided <see cref="Toplevel"/>, causing or the <paramref name="top"/> if provided.</summary>
-    /// <param name="top">The <see cref="Toplevel"/> to stop.</param>
-    /// <remarks>
-    ///     <para>This will cause <see cref="Application.Run(Toplevel, Func{Exception, bool})"/> to return.</para>
-    ///     <para>
-    ///         Calling <see cref="RequestStop(Toplevel)"/> is equivalent to setting the
-    ///         <see cref="Toplevel.Running"/>
-    ///         property on the currently running <see cref="Toplevel"/> to false.
-    ///     </para>
-    /// </remarks>
-    public static void RequestStop (Toplevel? top = null) { ApplicationImpl.Instance.RequestStop (top); }
-
-    /// <summary>
-    ///     Building block API: completes the execution of a <see cref="Toplevel"/> that was started with
-    ///     <see cref="Begin(Toplevel)"/> .
-    /// </summary>
-    /// <param name="runState">The <see cref="RunState"/> returned by the <see cref="Begin(Toplevel)"/> method.</param>
-    public static void End (RunState runState)
+    /// <inheritdoc cref="IApplication.SessionBegun"/>
+    public static event EventHandler<SessionTokenEventArgs>? SessionBegun
     {
-        ArgumentNullException.ThrowIfNull (runState);
-
-        if (Popover?.GetActivePopover () as View is { Visible: true } visiblePopover)
-        {
-            ApplicationPopover.HideWithQuitCommand (visiblePopover);
-        }
-
-        runState.Toplevel.OnUnloaded ();
-
-        // End the RunState.Toplevel
-        // First, take it off the Toplevel Stack
-        if (TopLevels.TryPop (out Toplevel? topOfStack))
-        {
-            if (topOfStack != runState.Toplevel)
-            {
-                // If the top of the stack is not the RunState.Toplevel then
-                // this call to End is not balanced with the call to Begin that started the RunState
-                throw new ArgumentException ("End must be balanced with calls to Begin");
-            }
-        }
-
-        // Notify that it is closing
-        runState.Toplevel?.OnClosed (runState.Toplevel);
-
-        if (TopLevels.TryPeek (out Toplevel? newTop))
-        {
-            Top = newTop;
-            Top?.SetNeedsDraw ();
-        }
-
-        if (runState.Toplevel is { HasFocus: true })
-        {
-            runState.Toplevel.HasFocus = false;
-        }
-
-        if (Top is { HasFocus: false })
-        {
-            Top.SetFocus ();
-        }
-
-        CachedRunStateToplevel = runState.Toplevel;
-
-        runState.Toplevel = null;
-        runState.Dispose ();
-
-        LayoutAndDraw (true);
+        add => ApplicationImpl.Instance.SessionBegun += value;
+        remove => ApplicationImpl.Instance.SessionBegun -= value;
     }
-    internal static void RaiseIteration ()
+
+    /// <inheritdoc cref="IApplication.SessionEnded"/>
+    public static event EventHandler<ToplevelEventArgs>? SessionEnded
     {
-        Iteration?.Invoke (null, new ());
+        add => ApplicationImpl.Instance.SessionEnded += value;
+        remove => ApplicationImpl.Instance.SessionEnded -= value;
     }
 }

+ 8 - 35
Terminal.Gui/App/Application.Screen.cs

@@ -4,50 +4,23 @@ namespace Terminal.Gui.App;
 
 public static partial class Application // Screen related stuff; intended to hide Driver details
 {
-    /// <summary>
-    ///     Gets or sets the size of the screen. By default, this is the size of the screen as reported by the <see cref="IConsoleDriver"/>.
-    /// </summary>
-    /// <remarks>
-    /// <para>
-    ///     If the <see cref="IConsoleDriver"/> has not been initialized, this will return a default size of 2048x2048; useful for unit tests.
-    /// </para>
-    /// </remarks>
+    /// <inheritdoc cref="IApplication.Screen"/>
+
     public static Rectangle Screen
     {
         get => ApplicationImpl.Instance.Screen;
         set => ApplicationImpl.Instance.Screen = value;
     }
 
-    /// <summary>Invoked when the terminal's size changed. The new size of the terminal is provided.</summary>
-    public static event EventHandler<EventArgs<Rectangle>>? ScreenChanged;
-
-    /// <summary>
-    ///     Called when the application's size has changed. Sets the size of all <see cref="Toplevel"/>s and fires the
-    ///     <see cref="ScreenChanged"/> event.
-    /// </summary>
-    /// <param name="screen">The new screen size and position.</param>
-    public static void RaiseScreenChangedEvent (Rectangle screen)
+    /// <inheritdoc cref="IApplication.ScreenChanged"/>
+    public static event EventHandler<EventArgs<Rectangle>>? ScreenChanged
     {
-        Screen = new (Point.Empty, screen.Size);
-
-        ScreenChanged?.Invoke (ApplicationImpl.Instance, new (screen));
-
-        foreach (Toplevel t in TopLevels)
-        {
-            t.OnSizeChanging (new (screen.Size));
-            t.SetNeedsLayout ();
-        }
-
-        LayoutAndDraw (true);
+        add => ApplicationImpl.Instance.ScreenChanged += value;
+        remove => ApplicationImpl.Instance.ScreenChanged -= value;
     }
 
-    /// <summary>
-    ///     Gets or sets whether the screen will be cleared, and all Views redrawn, during the next Application iteration.
-    /// </summary>
-    /// <remarks>
-    ///     This is typical set to true when a View's <see cref="View.Frame"/> changes and that view has no
-    ///     SuperView (e.g. when <see cref="Application.Top"/> is moved or resized.
-    /// </remarks>
+    /// <inheritdoc cref="IApplication.ClearScreenNextIteration"/>
+
     internal static bool ClearScreenNextIteration
     {
         get => ApplicationImpl.Instance.ClearScreenNextIteration;

+ 2 - 12
Terminal.Gui/App/Application.Toplevel.cs

@@ -5,10 +5,8 @@ namespace Terminal.Gui.App;
 
 public static partial class Application // Toplevel handling
 {
-    // BUGBUG: Technically, this is not the full lst of TopLevels. There be dragons here, e.g. see how Toplevel.Id is used. What
-
-    /// <summary>Holds the stack of TopLevel views.</summary>
-    internal static ConcurrentStack<Toplevel> TopLevels => ApplicationImpl.Instance.TopLevels;
+    /// <inheritdoc cref="IApplication.TopLevels"/>
+    public static ConcurrentStack<Toplevel> TopLevels => ApplicationImpl.Instance.TopLevels;
 
     /// <summary>The <see cref="Toplevel"/> that is currently active.</summary>
     /// <value>The top.</value>
@@ -17,12 +15,4 @@ public static partial class Application // Toplevel handling
         get => ApplicationImpl.Instance.Top;
         internal set => ApplicationImpl.Instance.Top = value;
     }
-
-    internal static Toplevel? CachedRunStateToplevel
-    {
-        get => ApplicationImpl.Instance.CachedRunStateToplevel;
-        private set => ApplicationImpl.Instance.CachedRunStateToplevel = value;
-    }
-
-
 }

+ 4 - 4
Terminal.Gui/App/Application.cd

@@ -36,19 +36,19 @@
       <FileName>App\MainLoopSyncContext.cs</FileName>
     </TypeIdentifier>
   </Class>
-  <Class Name="Terminal.Gui.App.RunState" Collapsed="true" BaseTypeListCollapsed="true">
+  <Class Name="Terminal.Gui.App.SessionToken" Collapsed="true" BaseTypeListCollapsed="true">
     <Position X="15.25" Y="4" Width="1.5" />
     <TypeIdentifier>
       <HashCode>AAAAAAAAACACAgAAAAAAAAAAAAAAAAACQAAAAAAAAAA=</HashCode>
-      <FileName>App\RunState.cs</FileName>
+      <FileName>App\SessionToken.cs</FileName>
     </TypeIdentifier>
     <Lollipop Position="0.2" Collapsed="true" />
   </Class>
-  <Class Name="Terminal.Gui.App.RunStateEventArgs" Collapsed="true">
+  <Class Name="Terminal.Gui.App.SessionTokenEventArgs" Collapsed="true">
     <Position X="17" Y="4" Width="2" />
     <TypeIdentifier>
       <HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAA=</HashCode>
-      <FileName>App\RunStateEventArgs.cs</FileName>
+      <FileName>App\SessionTokenEventArgs.cs</FileName>
     </TypeIdentifier>
   </Class>
   <Class Name="Terminal.Gui.App.Timeout" Collapsed="true">

+ 7 - 111
Terminal.Gui/App/Application.cs

@@ -39,18 +39,6 @@ namespace Terminal.Gui.App;
 /// <remarks></remarks>
 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
@@ -71,7 +59,7 @@ public static partial class Application
     /// <returns>A string representation of the Application </returns>
     public new static string ToString ()
     {
-        IConsoleDriver? driver = Driver;
+        IDriver? driver = Driver;
 
         if (driver is null)
         {
@@ -82,11 +70,11 @@ public static partial class Application
     }
 
     /// <summary>
-    ///     Gets a string representation of the Application rendered by the provided <see cref="IConsoleDriver"/>.
+    ///     Gets a string representation of the Application rendered by the provided <see cref="IDriver"/>.
     /// </summary>
     /// <param name="driver">The driver to use to render the contents.</param>
     /// <returns>A string representation of the Application </returns>
-    public static string ToString (IConsoleDriver? driver)
+    public static string ToString (IDriver? driver)
     {
         if (driver is null)
         {
@@ -129,6 +117,10 @@ public static partial class Application
         return sb.ToString ();
     }
 
+    /// <summary>Gets all cultures supported by the application without the invariant language.</summary>
+    public static List<CultureInfo>? SupportedCultures { get; private set; } = GetSupportedCultures ();
+
+
     internal static List<CultureInfo> GetAvailableCulturesFromEmbeddedResources ()
     {
         ResourceManager rm = new (typeof (Strings));
@@ -171,100 +163,4 @@ public static partial class Application
         // It's called from a self-contained single-file and get available cultures from the embedded resources strings.
         return GetAvailableCulturesFromEmbeddedResources ();
     }
-
-    // IMPORTANT: Ensure all property/fields are reset here. See Init_ResetState_Resets_Properties unit test.
-    // Encapsulate all setting of initial state for Application; Having
-    // this in a function like this ensures we don't make mistakes in
-    // guaranteeing that the state of this singleton is deterministic when Init
-    // starts running and after Shutdown returns.
-    internal static void ResetState (bool ignoreDisposed = false)
-    {
-        // Shutdown is the bookend for Init. As such it needs to clean up all resources
-        // Init created. Apps that do any threading will need to code defensively for this.
-        // e.g. see Issue #537
-        foreach (Toplevel? t in TopLevels)
-        {
-            t!.Running = false;
-        }
-
-        if (Popover?.GetActivePopover () is View popover)
-        {
-            // This forcefully closes the popover; invoking Command.Quit would be more graceful
-            // but since this is shutdown, doing this is ok.
-            popover.Visible = false;
-        }
-
-        Popover?.Dispose ();
-        Popover = null;
-
-        TopLevels.Clear ();
-#if DEBUG_IDISPOSABLE
-
-        // Don't dispose the Top. It's up to caller dispose it
-        if (View.EnableDebugIDisposableAsserts && !ignoreDisposed && Top is { })
-        {
-            Debug.Assert (Top.WasDisposed, $"Title = {Top.Title}, Id = {Top.Id}");
-
-            // If End wasn't called _cachedRunStateToplevel may be null
-            if (CachedRunStateToplevel is { })
-            {
-                Debug.Assert (CachedRunStateToplevel.WasDisposed);
-                Debug.Assert (CachedRunStateToplevel == Top);
-            }
-        }
-#endif
-        Top = null;
-        CachedRunStateToplevel = null;
-
-        MainThreadId = -1;
-        Iteration = null;
-        EndAfterFirstIteration = false;
-        ClearScreenNextIteration = false;
-
-        // Driver stuff
-        if (Driver is { })
-        {
-            UnsubscribeDriverEvents ();
-            Driver?.End ();
-            Driver = null;
-        }
-
-        // Reset Screen to null so it will be recalculated on next access
-        // Note: ApplicationImpl.Shutdown() also calls ResetScreen() before calling this method
-        // to avoid potential circular reference issues. Calling it twice is harmless.
-        if (ApplicationImpl.Instance is ApplicationImpl impl)
-        {
-            impl.ResetScreen ();
-        }
-
-        // Don't reset ForceDriver; it needs to be set before Init is called.
-        //ForceDriver = string.Empty;
-        //Force16Colors = false;
-        _forceFakeConsole = false;
-
-        // Run State stuff
-        NotifyNewRunState = null;
-        NotifyStopRunState = null;
-        // Mouse and Keyboard will be lazy-initialized in ApplicationImpl on next access
-        Initialized = false;
-
-        // Mouse
-        // Do not clear _lastMousePosition; Popovers require it to stay set with
-        // last mouse pos.
-        //_lastMousePosition = null;
-        CachedViewsUnderMouse.Clear ();
-        ResetMouseState ();
-
-        // Keyboard events and bindings are now managed by the Keyboard instance
-
-        ScreenChanged = null;
-
-        Navigation = null;
-
-        // Reset synchronization context to allow the user to run async/await,
-        // as the main loop has been ended, the synchronization context from
-        // gui.cs does no longer process any callbacks. See #1084 for more details:
-        // (https://github.com/gui-cs/Terminal.Gui/issues/1084).
-        SynchronizationContext.SetSynchronizationContext (null);
-    }
 }

+ 176 - 0
Terminal.Gui/App/ApplicationImpl.Driver.cs

@@ -0,0 +1,176 @@
+#nullable enable
+using System.Collections.Concurrent;
+
+namespace Terminal.Gui.App;
+
+public partial class ApplicationImpl
+{
+    /// <inheritdoc/>
+    public IDriver? Driver { get; set; }
+
+    /// <inheritdoc/>
+    public bool Force16Colors { get; set; }
+
+    /// <inheritdoc/>
+    public string ForceDriver { get; set; } = string.Empty;
+
+    /// <inheritdoc/>
+    public List<SixelToRender> Sixel { get; } = new ();
+
+    /// <summary>
+    ///     Creates the appropriate <see cref="IDriver"/> based on platform and driverName.
+    /// </summary>
+    /// <param name="driverName"></param>
+    /// <returns></returns>
+    /// <exception cref="Exception"></exception>
+    /// <exception cref="InvalidOperationException"></exception>
+    private void CreateDriver (string? driverName)
+    {
+        PlatformID p = Environment.OSVersion.Platform;
+
+        // Check component factory type first - this takes precedence over driverName
+        bool factoryIsWindows = _componentFactory is IComponentFactory<WindowsConsole.InputRecord>;
+        bool factoryIsDotNet = _componentFactory is IComponentFactory<ConsoleKeyInfo>;
+        bool factoryIsUnix = _componentFactory is IComponentFactory<char>;
+        bool factoryIsFake = _componentFactory is IComponentFactory<ConsoleKeyInfo>;
+
+        // Then check driverName
+        bool nameIsWindows = driverName?.Contains ("win", StringComparison.OrdinalIgnoreCase) ?? false;
+        bool nameIsDotNet = driverName?.Contains ("dotnet", StringComparison.OrdinalIgnoreCase) ?? false;
+        bool nameIsUnix = driverName?.Contains ("unix", StringComparison.OrdinalIgnoreCase) ?? false;
+        bool nameIsFake = driverName?.Contains ("fake", StringComparison.OrdinalIgnoreCase) ?? false;
+
+        // Decide which driver to use - component factory type takes priority
+        if (factoryIsFake || (!factoryIsWindows && !factoryIsDotNet && !factoryIsUnix && nameIsFake))
+        {
+            Coordinator = CreateSubcomponents (() => new FakeComponentFactory ());
+            _driverName = "fake";
+        }
+        else if (factoryIsWindows || (!factoryIsDotNet && !factoryIsUnix && nameIsWindows))
+        {
+            Coordinator = CreateSubcomponents (() => new WindowsComponentFactory ());
+            _driverName = "windows";
+        }
+        else if (factoryIsDotNet || (!factoryIsWindows && !factoryIsUnix && nameIsDotNet))
+        {
+            Coordinator = CreateSubcomponents (() => new NetComponentFactory ());
+            _driverName = "dotnet";
+        }
+        else if (factoryIsUnix || (!factoryIsWindows && !factoryIsDotNet && nameIsUnix))
+        {
+            Coordinator = CreateSubcomponents (() => new UnixComponentFactory ());
+            _driverName = "unix";
+        }
+        else if (p == PlatformID.Win32NT || p == PlatformID.Win32S || p == PlatformID.Win32Windows)
+        {
+            Coordinator = CreateSubcomponents (() => new WindowsComponentFactory ());
+            _driverName = "windows";
+        }
+        else if (p == PlatformID.Unix)
+        {
+            Coordinator = CreateSubcomponents (() => new UnixComponentFactory ());
+            _driverName = "unix";
+        }
+        else
+        {
+            Logging.Information($"Falling back to dotnet driver.");
+            Coordinator = CreateSubcomponents (() => new NetComponentFactory ());
+            _driverName = "dotnet";
+        }
+
+        Logging.Trace ($"Created Subcomponents: {Coordinator}");
+
+        Coordinator.StartInputTaskAsync ().Wait ();
+
+        if (Driver == null)
+        {
+            throw new ("Driver was null even after booting MainLoopCoordinator");
+        }
+    }
+
+    private readonly IComponentFactory? _componentFactory;
+
+    /// <summary>
+    ///     INTERNAL: Gets or sets the main loop coordinator that orchestrates the application's event processing,
+    ///     input handling, and rendering pipeline.
+    /// </summary>
+    /// <remarks>
+    ///     <para>
+    ///         The <see cref="IMainLoopCoordinator"/> is the central component responsible for:
+    ///         <list type="bullet">
+    ///             <item>Managing the platform-specific input thread that reads from the console</item>
+    ///             <item>Coordinating the main application loop via <see cref="IMainLoopCoordinator.RunIteration"/></item>
+    ///             <item>Processing queued input events and translating them to Terminal.Gui events</item>
+    ///             <item>Managing the <see cref="ApplicationMainLoop{TInputRecord}"/> that handles rendering</item>
+    ///             <item>Executing scheduled timeouts and callbacks via <see cref="ITimedEvents"/></item>
+    ///         </list>
+    ///     </para>
+    ///     <para>
+    ///         The coordinator is created in <see cref="CreateDriver"/> based on the selected driver
+    ///         (Windows, Unix, .NET, or Fake) and is started by calling
+    ///         <see cref="IMainLoopCoordinator.StartInputTaskAsync"/>.
+    ///     </para>
+    /// </remarks>
+    internal IMainLoopCoordinator? Coordinator { get; private set; }
+
+    /// <summary>
+    ///     INTERNAL: Creates a <see cref="MainLoopCoordinator{TInputRecord}"/> with the appropriate component factory
+    ///     for the specified input record type.
+    /// </summary>
+    /// <typeparam name="TInputRecord">
+    ///     Platform-specific input type: <see cref="ConsoleKeyInfo"/> (.NET/Fake),
+    ///     <see cref="WindowsConsole.InputRecord"/> (Windows), or <see cref="char"/> (Unix).
+    /// </typeparam>
+    /// <param name="fallbackFactory">
+    ///     Factory function to create the component factory if <see cref="_componentFactory"/>
+    ///     is not of type <see cref="IComponentFactory{TInputRecord}"/>.
+    /// </param>
+    /// <returns>
+    ///     A <see cref="MainLoopCoordinator{TInputRecord}"/> configured with the input queue,
+    ///     main loop, timed events, and selected component factory.
+    /// </returns>
+    private IMainLoopCoordinator CreateSubcomponents<TInputRecord> (Func<IComponentFactory<TInputRecord>> fallbackFactory) where TInputRecord : struct
+    {
+        ConcurrentQueue<TInputRecord> inputQueue = new ();
+        ApplicationMainLoop<TInputRecord> loop = new ();
+
+        IComponentFactory<TInputRecord> cf;
+
+        if (_componentFactory is IComponentFactory<TInputRecord> typedFactory)
+        {
+            cf = typedFactory;
+        }
+        else
+        {
+            cf = fallbackFactory ();
+        }
+
+        return new MainLoopCoordinator<TInputRecord> (_timedEvents, inputQueue, loop, cf);
+    }
+
+    internal void SubscribeDriverEvents ()
+    {
+        ArgumentNullException.ThrowIfNull (Driver);
+
+        Driver.SizeChanged += Driver_SizeChanged;
+        Driver.KeyDown += Driver_KeyDown;
+        Driver.KeyUp += Driver_KeyUp;
+        Driver.MouseEvent += Driver_MouseEvent;
+    }
+
+    internal void UnsubscribeDriverEvents ()
+    {
+        ArgumentNullException.ThrowIfNull (Driver);
+
+        Driver.SizeChanged -= Driver_SizeChanged;
+        Driver.KeyDown -= Driver_KeyDown;
+        Driver.KeyUp -= Driver_KeyUp;
+        Driver.MouseEvent -= Driver_MouseEvent;
+    }
+
+    private void Driver_KeyDown (object? sender, Key e) { Keyboard?.RaiseKeyDownEvent (e); }
+
+    private void Driver_KeyUp (object? sender, Key e) { Keyboard?.RaiseKeyUpEvent (e); }
+
+    private void Driver_MouseEvent (object? sender, MouseEventArgs e) { Mouse?.RaiseMouseEvent (e); }
+}

+ 251 - 0
Terminal.Gui/App/ApplicationImpl.Lifecycle.cs

@@ -0,0 +1,251 @@
+#nullable enable
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+
+namespace Terminal.Gui.App;
+
+public partial class ApplicationImpl
+{
+    /// <inheritdoc/>
+    public bool Initialized { get; set; }
+
+    /// <inheritdoc/>
+    public event EventHandler<EventArgs<bool>>? InitializedChanged;
+
+    /// <inheritdoc/>
+    [RequiresUnreferencedCode ("AOT")]
+    [RequiresDynamicCode ("AOT")]
+    public void Init (IDriver? driver = null, string? driverName = null)
+    {
+        if (Initialized)
+        {
+            Logging.Error ("Init called multiple times without shutdown, aborting.");
+
+            throw new InvalidOperationException ("Init called multiple times without Shutdown");
+        }
+
+        if (!string.IsNullOrWhiteSpace (driverName))
+        {
+            _driverName = driverName;
+        }
+
+        if (string.IsNullOrWhiteSpace (_driverName))
+        {
+            _driverName = ForceDriver;
+        }
+
+        Debug.Assert (Navigation is null);
+        Navigation = new ();
+
+        Debug.Assert (Popover is null);
+        Popover = new ();
+
+        // Preserve existing keyboard settings if they exist
+        bool hasExistingKeyboard = _keyboard is { };
+        Key existingQuitKey = _keyboard?.QuitKey ?? Key.Esc;
+        Key existingArrangeKey = _keyboard?.ArrangeKey ?? Key.F5.WithCtrl;
+        Key existingNextTabKey = _keyboard?.NextTabKey ?? Key.Tab;
+        Key existingPrevTabKey = _keyboard?.PrevTabKey ?? Key.Tab.WithShift;
+        Key existingNextTabGroupKey = _keyboard?.NextTabGroupKey ?? Key.F6;
+        Key existingPrevTabGroupKey = _keyboard?.PrevTabGroupKey ?? Key.F6.WithShift;
+
+        // Reset keyboard to ensure fresh state with default bindings
+        _keyboard = new KeyboardImpl { Application = this };
+
+        // Restore previously set keys if they existed and were different from defaults
+        if (hasExistingKeyboard)
+        {
+            _keyboard.QuitKey = existingQuitKey;
+            _keyboard.ArrangeKey = existingArrangeKey;
+            _keyboard.NextTabKey = existingNextTabKey;
+            _keyboard.PrevTabKey = existingPrevTabKey;
+            _keyboard.NextTabGroupKey = existingNextTabGroupKey;
+            _keyboard.PrevTabGroupKey = existingPrevTabGroupKey;
+        }
+
+        CreateDriver (driverName ?? _driverName);
+        Screen = Driver!.Screen;
+        Initialized = true;
+
+        RaiseInitializedChanged (this, new (true));
+        SubscribeDriverEvents ();
+
+        SynchronizationContext.SetSynchronizationContext (new ());
+        MainThreadId = Thread.CurrentThread.ManagedThreadId;
+    }
+
+    /// <summary>Shutdown an application initialized with <see cref="Init"/>.</summary>
+    public void Shutdown ()
+    {
+        // Stop the coordinator if running
+        Coordinator?.Stop ();
+
+        // Capture state before cleanup
+        bool wasInitialized = Initialized;
+
+#if DEBUG
+
+        // Check that all Application events have no remaining subscribers BEFORE clearing them
+        // Only check if we were actually initialized
+        if (wasInitialized)
+        {
+            AssertNoEventSubscribers (nameof (Iteration), Iteration);
+            AssertNoEventSubscribers (nameof (SessionBegun), SessionBegun);
+            AssertNoEventSubscribers (nameof (SessionEnded), SessionEnded);
+            AssertNoEventSubscribers (nameof (ScreenChanged), ScreenChanged);
+
+            //AssertNoEventSubscribers (nameof (InitializedChanged), InitializedChanged);
+        }
+#endif
+
+        // Clean up all application state (including sync context)
+        // ResetState handles the case where Initialized is false
+        ResetState ();
+
+        // Configuration manager diagnostics
+        ConfigurationManager.PrintJsonErrors ();
+
+        // Raise the initialized changed event to notify shutdown
+        if (wasInitialized)
+        {
+            bool init = Initialized; // Will be false after ResetState
+            RaiseInitializedChanged (this, new (in init));
+        }
+
+        // Clear the event to prevent memory leaks
+        InitializedChanged = null;
+
+        // Create a new lazy instance for potential future Init
+        _lazyInstance = new (() => new ApplicationImpl ());
+    }
+
+#if DEBUG
+    /// <summary>
+    ///     DEBUG ONLY: Asserts that an event has no remaining subscribers.
+    /// </summary>
+    /// <param name="eventName">The name of the event for diagnostic purposes.</param>
+    /// <param name="eventDelegate">The event delegate to check.</param>
+    private static void AssertNoEventSubscribers (string eventName, Delegate? eventDelegate)
+    {
+        if (eventDelegate is null)
+        {
+            return;
+        }
+
+        Delegate [] subscribers = eventDelegate.GetInvocationList ();
+
+        if (subscribers.Length > 0)
+        {
+            string subscriberInfo = string.Join (
+                                                 ", ",
+                                                 subscribers.Select (d => $"{d.Method.DeclaringType?.Name}.{d.Method.Name}"
+                                                                    )
+                                                );
+
+            Debug.Fail (
+                        $"Application.{eventName} has {subscribers.Length} remaining subscriber(s) after Shutdown: {subscriberInfo}"
+                       );
+        }
+    }
+#endif
+
+    /// <inheritdoc/>
+    public void ResetState (bool ignoreDisposed = false)
+    {
+        // Shutdown is the bookend for Init. As such it needs to clean up all resources
+        // Init created. Apps that do any threading will need to code defensively for this.
+        // e.g. see Issue #537
+
+        // === 1. Stop all running toplevels ===
+        foreach (Toplevel? t in TopLevels)
+        {
+            t!.Running = false;
+        }
+
+        // === 2. Close and dispose popover ===
+        if (Popover?.GetActivePopover () is View popover)
+        {
+            // This forcefully closes the popover; invoking Command.Quit would be more graceful
+            // but since this is shutdown, doing this is ok.
+            popover.Visible = false;
+        }
+
+        Popover?.Dispose ();
+        Popover = null;
+
+        // === 3. Clean up toplevels ===
+        TopLevels.Clear ();
+
+#if DEBUG_IDISPOSABLE
+
+        // Don't dispose the Top. It's up to caller dispose it
+        if (View.EnableDebugIDisposableAsserts && !ignoreDisposed && Top is { })
+        {
+            Debug.Assert (Top.WasDisposed, $"Title = {Top.Title}, Id = {Top.Id}");
+
+            // If End wasn't called _CachedSessionTokenToplevel may be null
+            if (CachedSessionTokenToplevel is { })
+            {
+                Debug.Assert (CachedSessionTokenToplevel.WasDisposed);
+                Debug.Assert (CachedSessionTokenToplevel == Top);
+            }
+        }
+#endif
+
+        Top = null;
+        CachedSessionTokenToplevel = null;
+
+        // === 4. Clean up driver ===
+        if (Driver is { })
+        {
+            UnsubscribeDriverEvents ();
+            Driver?.End ();
+            Driver = null;
+        }
+
+        // Reset screen
+        ResetScreen ();
+        _screen = null;
+
+        // === 5. Clear run state ===
+        Iteration = null;
+        SessionBegun = null;
+        SessionEnded = null;
+        StopAfterFirstIteration = false;
+        ClearScreenNextIteration = false;
+
+        // === 6. Reset input systems ===
+        // Mouse and Keyboard will be lazy-initialized on next access
+        _mouse = null;
+        _keyboard = null;
+        Mouse.ResetState ();
+
+        // === 7. Clear navigation and screen state ===
+        ScreenChanged = null;
+        Navigation = null;
+
+        // === 8. Reset initialization state ===
+        Initialized = false;
+        MainThreadId = null;
+
+        // === 9. Clear graphics ===
+        Sixel.Clear ();
+
+        // === 10. Reset synchronization context ===
+        // IMPORTANT: Always reset sync context, even if not initialized
+        // This ensures cleanup works correctly even if Shutdown is called without Init
+        // Reset synchronization context to allow the user to run async/await,
+        // as the main loop has been ended, the synchronization context from
+        // gui.cs does no longer process any callbacks. See #1084 for more details:
+        // (https://github.com/gui-cs/Terminal.Gui/issues/1084).
+        SynchronizationContext.SetSynchronizationContext (null);
+
+        // Note: ForceDriver and Force16Colors are NOT reset; 
+        // they need to persist across Init/Shutdown cycles
+    }
+
+    /// <summary>
+    ///     Raises the <see cref="InitializedChanged"/> event.
+    /// </summary>
+    internal void RaiseInitializedChanged (object sender, EventArgs<bool> e) { InitializedChanged?.Invoke (sender, e); }
+}

+ 347 - 0
Terminal.Gui/App/ApplicationImpl.Run.cs

@@ -0,0 +1,347 @@
+#nullable enable
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+
+namespace Terminal.Gui.App;
+
+public partial class ApplicationImpl
+{
+    /// <summary>
+    ///     INTERNAL: Gets or sets the managed thread ID of the application's main UI thread, which is set during
+    ///     <see cref="Init"/> and used to determine if code is executing on the main thread.
+    /// </summary>
+    /// <value>
+    ///     The managed thread ID of the main UI thread, or <see langword="null"/> if the application is not initialized.
+    /// </value>
+    internal int? MainThreadId { get; set; }
+
+    #region Begin->Run->Stop->End
+
+    /// <inheritdoc/>
+    public event EventHandler<SessionTokenEventArgs>? SessionBegun;
+
+    /// <inheritdoc/>
+    public event EventHandler<ToplevelEventArgs>? SessionEnded;
+
+    /// <inheritdoc/>
+    public SessionToken Begin (Toplevel toplevel)
+    {
+        ArgumentNullException.ThrowIfNull (toplevel);
+
+        // Ensure the mouse is ungrabbed.
+        if (Mouse.MouseGrabView is { })
+        {
+            Mouse.UngrabMouse ();
+        }
+
+        var rs = new SessionToken (toplevel);
+
+#if DEBUG_IDISPOSABLE
+        if (View.EnableDebugIDisposableAsserts && Top is { } && toplevel != Top && !TopLevels.Contains (Top))
+        {
+            // This assertion confirm if the Top was already disposed
+            Debug.Assert (Top.WasDisposed);
+            Debug.Assert (Top == CachedSessionTokenToplevel);
+        }
+#endif
+
+        lock (TopLevels)
+        {
+            if (Top is { } && toplevel != Top && !TopLevels.Contains (Top))
+            {
+                // If Top was already disposed and isn't on the Toplevels Stack,
+                // clean it up here if is the same as _CachedSessionTokenToplevel
+                if (Top == CachedSessionTokenToplevel)
+                {
+                    Top = null;
+                }
+                else
+                {
+                    // Probably this will never hit
+                    throw new ObjectDisposedException (Top.GetType ().FullName);
+                }
+            }
+
+            // BUGBUG: We should not depend on `Id` internally.
+            // BUGBUG: It is super unclear what this code does anyway.
+            if (string.IsNullOrEmpty (toplevel.Id))
+            {
+                var count = 1;
+                var id = (TopLevels.Count + count).ToString ();
+
+                while (TopLevels.Count > 0 && TopLevels.FirstOrDefault (x => x.Id == id) is { })
+                {
+                    count++;
+                    id = (TopLevels.Count + count).ToString ();
+                }
+
+                toplevel.Id = (TopLevels.Count + count).ToString ();
+
+                TopLevels.Push (toplevel);
+            }
+            else
+            {
+                Toplevel? dup = TopLevels.FirstOrDefault (x => x.Id == toplevel.Id);
+
+                if (dup is null)
+                {
+                    TopLevels.Push (toplevel);
+                }
+            }
+        }
+
+        if (Top is null)
+        {
+            Top = toplevel;
+        }
+
+        if ((Top?.Modal == false && toplevel.Modal)
+            || (Top?.Modal == false && !toplevel.Modal)
+            || (Top?.Modal == true && toplevel.Modal))
+        {
+            if (toplevel.Visible)
+            {
+                if (Top is { HasFocus: true })
+                {
+                    Top.HasFocus = false;
+                }
+
+                // Force leave events for any entered views in the old Top
+                if (Mouse.GetLastMousePosition () is { })
+                {
+                    Mouse.RaiseMouseEnterLeaveEvents (Mouse.GetLastMousePosition ()!.Value, new ());
+                }
+
+                Top?.OnDeactivate (toplevel);
+                Toplevel previousTop = Top!;
+
+                Top = toplevel;
+                Top.OnActivate (previousTop);
+            }
+        }
+
+        // View implements ISupportInitializeNotification which is derived from ISupportInitialize
+        if (!toplevel.IsInitialized)
+        {
+            toplevel.BeginInit ();
+            toplevel.EndInit (); // Calls Layout
+        }
+
+        // Try to set initial focus to any TabStop
+        if (!toplevel.HasFocus)
+        {
+            toplevel.SetFocus ();
+        }
+
+        toplevel.OnLoaded ();
+
+        Instance.LayoutAndDraw (true);
+
+        if (PositionCursor ())
+        {
+            Driver?.UpdateCursor ();
+        }
+
+        SessionBegun?.Invoke (this, new (rs));
+
+        return rs;
+    }
+
+    /// <inheritdoc/>
+    public bool StopAfterFirstIteration { get; set; }
+
+    /// <inheritdoc/>
+    public event EventHandler<IterationEventArgs>? Iteration;
+
+    /// <inheritdoc/>
+    [RequiresUnreferencedCode ("AOT")]
+    [RequiresDynamicCode ("AOT")]
+    public Toplevel Run (Func<Exception, bool>? errorHandler = null, string? driver = null) { return Run<Toplevel> (errorHandler, driver); }
+
+    /// <inheritdoc/>
+    [RequiresUnreferencedCode ("AOT")]
+    [RequiresDynamicCode ("AOT")]
+    public TView Run<TView> (Func<Exception, bool>? errorHandler = null, string? driver = null)
+        where TView : Toplevel, new ()
+    {
+        if (!Initialized)
+        {
+            // Init() has NOT been called. Auto-initialize as per interface contract.
+            Init (null, driver);
+        }
+
+        TView top = new ();
+        Run (top, errorHandler);
+
+        return top;
+    }
+
+
+    /// <inheritdoc/>
+    public void Run (Toplevel view, Func<Exception, bool>? errorHandler = null)
+    {
+        Logging.Information ($"Run '{view}'");
+        ArgumentNullException.ThrowIfNull (view);
+
+        if (!Initialized)
+        {
+            throw new NotInitializedException (nameof (Run));
+        }
+
+        if (Driver == null)
+        {
+            throw new InvalidOperationException ("Driver was inexplicably null when trying to Run view");
+        }
+
+        Top = view;
+
+        SessionToken rs = Application.Begin (view);
+
+        Top.Running = true;
+
+        var firstIteration = true;
+
+        while (TopLevels.TryPeek (out Toplevel? found) && found == view && view.Running)
+        {
+            if (Coordinator is null)
+            {
+                throw new ($"{nameof (IMainLoopCoordinator)} inexplicably became null during Run");
+            }
+
+            Coordinator.RunIteration ();
+
+            if (StopAfterFirstIteration && firstIteration)
+            {
+                Logging.Information ("Run - Stopping after first iteration as requested");
+                view.RequestStop ();
+            }
+
+            firstIteration = false;
+        }
+
+        Logging.Information ("Run - Calling End");
+        Application.End (rs);
+    }
+
+    /// <inheritdoc/>
+    public void End (SessionToken sessionToken)
+    {
+        ArgumentNullException.ThrowIfNull (sessionToken);
+
+        if (Popover?.GetActivePopover () as View is { Visible: true } visiblePopover)
+        {
+            ApplicationPopover.HideWithQuitCommand (visiblePopover);
+        }
+
+        sessionToken.Toplevel.OnUnloaded ();
+
+        // End the Session
+        // First, take it off the Toplevel Stack
+        if (TopLevels.TryPop (out Toplevel? topOfStack))
+        {
+            if (topOfStack != sessionToken.Toplevel)
+            {
+                // If the top of the stack is not the SessionToken.Toplevel then
+                // this call to End is not balanced with the call to Begin that started the Session
+                throw new ArgumentException ("End must be balanced with calls to Begin");
+            }
+        }
+
+        // Notify that it is closing
+        sessionToken.Toplevel?.OnClosed (sessionToken.Toplevel);
+
+        if (TopLevels.TryPeek (out Toplevel? newTop))
+        {
+            Top = newTop;
+            Top?.SetNeedsDraw ();
+        }
+
+        if (sessionToken.Toplevel is { HasFocus: true })
+        {
+            sessionToken.Toplevel.HasFocus = false;
+        }
+
+        if (Top is { HasFocus: false })
+        {
+            Top.SetFocus ();
+        }
+
+        CachedSessionTokenToplevel = sessionToken.Toplevel;
+
+        sessionToken.Toplevel = null;
+        sessionToken.Dispose ();
+
+        // BUGBUG: Why layout and draw here? This causes the screen to be cleared!
+        //LayoutAndDraw (true);
+
+        SessionEnded?.Invoke (this, new (CachedSessionTokenToplevel));
+    }
+
+    /// <inheritdoc/>
+    public void RequestStop () { RequestStop (null); }
+
+    /// <inheritdoc/>
+    public void RequestStop (Toplevel? top)
+    {
+        Logging.Trace ($"Top: '{(top is { } ? top : "null")}'");
+
+        top ??= Top;
+
+        if (top == null)
+        {
+            return;
+        }
+
+        ToplevelClosingEventArgs ev = new (top);
+        top.OnClosing (ev);
+
+        if (ev.Cancel)
+        {
+            return;
+        }
+
+        top.Running = false;
+    }
+
+    /// <inheritdoc/>
+    public void RaiseIteration () { Iteration?.Invoke (null, new ()); }
+
+    #endregion Begin->Run->Stop->End
+
+    #region Timeouts and Invoke
+
+    private readonly ITimedEvents _timedEvents = new TimedEvents ();
+
+    /// <inheritdoc/>
+    public ITimedEvents? TimedEvents => _timedEvents;
+
+    /// <inheritdoc/>
+    public object AddTimeout (TimeSpan time, Func<bool> callback) { return _timedEvents.Add (time, callback); }
+
+    /// <inheritdoc/>
+    public bool RemoveTimeout (object token) { return _timedEvents.Remove (token); }
+
+    /// <inheritdoc/>
+    public void Invoke (Action action)
+    {
+        // If we are already on the main UI thread
+        if (Top is { Running: true } && MainThreadId == Thread.CurrentThread.ManagedThreadId)
+        {
+            action ();
+
+            return;
+        }
+
+        _timedEvents.Add (
+                          TimeSpan.Zero,
+                          () =>
+                          {
+                              action ();
+
+                              return false;
+                          }
+                         );
+    }
+
+    #endregion Timeouts and Invoke
+}

+ 177 - 0
Terminal.Gui/App/ApplicationImpl.Screen.cs

@@ -0,0 +1,177 @@
+#nullable enable
+
+namespace Terminal.Gui.App;
+
+public partial class ApplicationImpl
+{
+    /// <inheritdoc/>
+    public event EventHandler<EventArgs<Rectangle>>? ScreenChanged;
+
+    private readonly object _lockScreen = new ();
+    private Rectangle? _screen;
+
+    /// <inheritdoc/>
+    public Rectangle Screen
+    {
+        get
+        {
+            lock (_lockScreen)
+            {
+                if (_screen == null)
+                {
+                    _screen = Driver?.Screen ?? new (new (0, 0), new (2048, 2048));
+                }
+
+                return _screen.Value;
+            }
+        }
+        set
+        {
+            if (value is { } && (value.X != 0 || value.Y != 0))
+            {
+                throw new NotImplementedException ("Screen locations other than 0, 0 are not yet supported");
+            }
+
+            lock (_lockScreen)
+            {
+                _screen = value;
+            }
+        }
+    }
+
+    /// <inheritdoc/>
+    public bool ClearScreenNextIteration { get; set; }
+
+    /// <inheritdoc/>
+    public bool PositionCursor ()
+    {
+        // Find the most focused view and position the cursor there.
+        View? mostFocused = Navigation?.GetFocused ();
+
+        // If the view is not visible or enabled, don't position the cursor
+        if (mostFocused is null || !mostFocused.Visible || !mostFocused.Enabled)
+        {
+            var current = CursorVisibility.Invisible;
+            Driver?.GetCursorVisibility (out current);
+
+            if (current != CursorVisibility.Invisible)
+            {
+                Driver?.SetCursorVisibility (CursorVisibility.Invisible);
+            }
+
+            return false;
+        }
+
+        // If the view is not visible within it's superview, don't position the cursor
+        Rectangle mostFocusedViewport = mostFocused.ViewportToScreen (mostFocused.Viewport with { Location = Point.Empty });
+
+        Rectangle superViewViewport =
+            mostFocused.SuperView?.ViewportToScreen (mostFocused.SuperView.Viewport with { Location = Point.Empty }) ?? Driver!.Screen;
+
+        if (!superViewViewport.IntersectsWith (mostFocusedViewport))
+        {
+            return false;
+        }
+
+        Point? cursor = mostFocused.PositionCursor ();
+
+        Driver!.GetCursorVisibility (out CursorVisibility currentCursorVisibility);
+
+        if (cursor is { })
+        {
+            // Convert cursor to screen coords
+            cursor = mostFocused.ViewportToScreen (mostFocused.Viewport with { Location = cursor.Value }).Location;
+
+            // If the cursor is not in a visible location in the SuperView, hide it
+            if (!superViewViewport.Contains (cursor.Value))
+            {
+                if (currentCursorVisibility != CursorVisibility.Invisible)
+                {
+                    Driver.SetCursorVisibility (CursorVisibility.Invisible);
+                }
+
+                return false;
+            }
+
+            // Show it
+            if (currentCursorVisibility == CursorVisibility.Invisible)
+            {
+                Driver.SetCursorVisibility (mostFocused.CursorVisibility);
+            }
+
+            return true;
+        }
+
+        if (currentCursorVisibility != CursorVisibility.Invisible)
+        {
+            Driver.SetCursorVisibility (CursorVisibility.Invisible);
+        }
+
+        return false;
+    }
+
+    /// <summary>
+    ///     INTERNAL: Resets the Screen field to null so it will be recalculated on next access.
+    /// </summary>
+    private void ResetScreen ()
+    {
+        lock (_lockScreen)
+        {
+            _screen = null;
+        }
+    }
+
+    /// <summary>
+    ///     INTERNAL: Called when the application's size has changed. Sets the size of all <see cref="Toplevel"/>s and fires
+    ///     the
+    ///     <see cref="ScreenChanged"/> event.
+    /// </summary>
+    /// <param name="screen">The new screen size and position.</param>
+    private void RaiseScreenChangedEvent (Rectangle screen)
+    {
+        Screen = new (Point.Empty, screen.Size);
+
+        ScreenChanged?.Invoke (this, new (screen));
+
+        foreach (Toplevel t in TopLevels)
+        {
+            t.OnSizeChanging (new (screen.Size));
+            t.SetNeedsLayout ();
+        }
+
+        LayoutAndDraw (true);
+    }
+
+    private void Driver_SizeChanged (object? sender, SizeChangedEventArgs e) { RaiseScreenChangedEvent (new (new (0, 0), e.Size!.Value)); }
+
+    /// <inheritdoc/>
+    public void LayoutAndDraw (bool forceRedraw = false)
+    {
+        List<View> tops = [.. TopLevels];
+
+        if (Popover?.GetActivePopover () as View is { Visible: true } visiblePopover)
+        {
+            visiblePopover.SetNeedsDraw ();
+            visiblePopover.SetNeedsLayout ();
+            tops.Insert (0, visiblePopover);
+        }
+
+        bool neededLayout = View.Layout (tops.ToArray ().Reverse (), Screen.Size);
+
+        if (ClearScreenNextIteration)
+        {
+            forceRedraw = true;
+            ClearScreenNextIteration = false;
+        }
+
+        if (forceRedraw)
+        {
+            Driver?.ClearContents ();
+        }
+
+        View.SetClipToScreen ();
+        View.Draw (tops, neededLayout || forceRedraw);
+        View.SetClipToScreen ();
+        Driver?.Refresh ();
+    }
+}

+ 44 - 488
Terminal.Gui/App/ApplicationImpl.cs

@@ -1,54 +1,55 @@
 #nullable enable
 using System.Collections.Concurrent;
-using System.Diagnostics;
-using System.Diagnostics.CodeAnalysis;
-using Microsoft.Extensions.Logging;
-using Terminal.Gui.Drivers;
 
 namespace Terminal.Gui.App;
 
 /// <summary>
-/// Implementation of core <see cref="Application"/> methods using the modern
-/// main loop architecture with component factories for different platforms.
+///     Implementation of core <see cref="Application"/> methods using the modern
+///     main loop architecture with component factories for different platforms.
 /// </summary>
-public class ApplicationImpl : IApplication
+public partial class ApplicationImpl : IApplication
 {
-    private readonly IComponentFactory? _componentFactory;
-    private IMainLoopCoordinator? _coordinator;
-    private string? _driverName;
-    private readonly ITimedEvents _timedEvents = new TimedEvents ();
-    private IConsoleDriver? _driver;
-    private bool _initialized;
-    private ApplicationPopover? _popover;
-    private ApplicationNavigation? _navigation;
-    private Toplevel? _top;
-    private readonly ConcurrentStack<Toplevel> _topLevels = new ();
-    private int _mainThreadId = -1;
-    private bool _force16Colors;
-    private string _forceDriver = string.Empty;
-    private readonly List<SixelToRender> _sixel = new ();
-    private readonly object _lockScreen = new ();
-    private Rectangle? _screen;
-    private bool _clearScreenNextIteration;
+    /// <summary>
+    ///     Creates a new instance of the Application backend.
+    /// </summary>
+    public ApplicationImpl () { }
+
+    /// <summary>
+    ///     INTERNAL: Creates a new instance of the Application backend.
+    /// </summary>
+    /// <param name="componentFactory"></param>
+    internal ApplicationImpl (IComponentFactory componentFactory) { _componentFactory = componentFactory; }
+
+    #region Singleton
 
     // Private static readonly Lazy instance of Application
     private static Lazy<IApplication> _lazyInstance = new (() => new ApplicationImpl ());
 
     /// <summary>
-    /// Gets the currently configured backend implementation of <see cref="Application"/> gateway methods.
-    /// Change to your own implementation by using <see cref="ChangeInstance"/> (before init).
+    ///     Change the singleton implementation, should not be called except before application
+    ///     startup. This method lets you provide alternative implementations of core static gateway
+    ///     methods of <see cref="Application"/>.
+    /// </summary>
+    /// <param name="newApplication"></param>
+    public static void ChangeInstance (IApplication? newApplication) { _lazyInstance = new (newApplication!); }
+
+    /// <summary>
+    ///     Gets the currently configured backend implementation of <see cref="Application"/> gateway methods.
+    ///     Change to your own implementation by using <see cref="ChangeInstance"/> (before init).
     /// </summary>
     public static IApplication Instance => _lazyInstance.Value;
 
-    /// <inheritdoc/>
-    public ITimedEvents? TimedEvents => _timedEvents;
+    #endregion Singleton
+
+    private string? _driverName;
 
-    internal IMainLoopCoordinator? Coordinator => _coordinator;
+
+    #region Input
 
     private IMouse? _mouse;
 
     /// <summary>
-    /// Handles mouse event state and processing.
+    ///     Handles mouse event state and processing.
     /// </summary>
     public IMouse Mouse
     {
@@ -58,15 +59,17 @@ public class ApplicationImpl : IApplication
             {
                 _mouse = new MouseImpl { Application = this };
             }
+
             return _mouse;
         }
         set => _mouse = value ?? throw new ArgumentNullException (nameof (value));
     }
 
     private IKeyboard? _keyboard;
+    private bool _stopAfterFirstIteration;
 
     /// <summary>
-    /// Handles keyboard input and key bindings at the Application level
+    ///     Handles keyboard input and key bindings at the Application level
     /// </summary>
     public IKeyboard Keyboard
     {
@@ -76,479 +79,32 @@ public class ApplicationImpl : IApplication
             {
                 _keyboard = new KeyboardImpl { Application = this };
             }
+
             return _keyboard;
         }
         set => _keyboard = value ?? throw new ArgumentNullException (nameof (value));
     }
 
-    /// <inheritdoc/>
-    public IConsoleDriver? Driver
-    {
-        get => _driver;
-        set => _driver = value;
-    }
+    #endregion Input
 
-    /// <inheritdoc/>
-    public bool Initialized
-    {
-        get => _initialized;
-        set => _initialized = value;
-    }
+    #region View Management
 
     /// <inheritdoc/>
-    public bool Force16Colors
-    {
-        get => _force16Colors;
-        set => _force16Colors = value;
-    }
+    public ApplicationPopover? Popover { get; set; }
 
     /// <inheritdoc/>
-    public string ForceDriver
-    {
-        get => _forceDriver;
-        set => _forceDriver = value;
-    }
+    public ApplicationNavigation? Navigation { get; set; }
 
     /// <inheritdoc/>
-    public List<SixelToRender> Sixel => _sixel;
+    public Toplevel? Top { get; set; }
 
-    /// <inheritdoc/>
-    public Rectangle Screen
-    {
-        get
-        {
-            lock (_lockScreen)
-            {
-                if (_screen == null)
-                {
-                    _screen = Driver?.Screen ?? new (new (0, 0), new (2048, 2048));
-                }
-
-                return _screen.Value;
-            }
-        }
-        set
-        {
-            if (value is { } && (value.X != 0 || value.Y != 0))
-            {
-                throw new NotImplementedException ($"Screen locations other than 0, 0 are not yet supported");
-            }
-
-            lock (_lockScreen)
-            {
-                _screen = value;
-            }
-        }
-    }
+    // BUGBUG: Technically, this is not the full lst of TopLevels. There be dragons here, e.g. see how Toplevel.Id is used. What
 
     /// <inheritdoc/>
-    public bool ClearScreenNextIteration
-    {
-        get => _clearScreenNextIteration;
-        set => _clearScreenNextIteration = value;
-    }
-
-    /// <inheritdoc/>
-    public ApplicationPopover? Popover
-    {
-        get => _popover;
-        set => _popover = value;
-    }
+    public ConcurrentStack<Toplevel> TopLevels { get; } = new ();
 
     /// <inheritdoc/>
-    public ApplicationNavigation? Navigation
-    {
-        get => _navigation;
-        set => _navigation = value;
-    }
-
-    /// <inheritdoc/>
-    public Toplevel? Top
-    {
-        get => _top;
-        set => _top = value;
-    }
+    public Toplevel? CachedSessionTokenToplevel { get; set; }
 
-    /// <inheritdoc/>
-    public ConcurrentStack<Toplevel> TopLevels => _topLevels;
-
-    // When `End ()` is called, it is possible `RunState.Toplevel` is a different object than `Top`.
-    // This variable is set in `End` in this case so that `Begin` correctly sets `Top`.
-    /// <inheritdoc />
-    public Toplevel? CachedRunStateToplevel { get; set; }
-
-    /// <summary>
-    /// Gets or sets the main thread ID for the application.
-    /// </summary>
-    internal int MainThreadId
-    {
-        get => _mainThreadId;
-        set => _mainThreadId = value;
-    }
-
-    /// <inheritdoc/>
-    public void RequestStop () => RequestStop (null);
-
-    /// <summary>
-    /// Creates a new instance of the Application backend.
-    /// </summary>
-    public ApplicationImpl ()
-    {
-    }
-
-    internal ApplicationImpl (IComponentFactory componentFactory)
-    {
-        _componentFactory = componentFactory;
-    }
-
-    /// <summary>
-    /// Change the singleton implementation, should not be called except before application
-    /// startup. This method lets you provide alternative implementations of core static gateway
-    /// methods of <see cref="Application"/>.
-    /// </summary>
-    /// <param name="newApplication"></param>
-    public static void ChangeInstance (IApplication newApplication)
-    {
-        _lazyInstance = new Lazy<IApplication> (newApplication);
-    }
-
-    /// <inheritdoc/>
-    [RequiresUnreferencedCode ("AOT")]
-    [RequiresDynamicCode ("AOT")]
-    public void Init (IConsoleDriver? driver = null, string? driverName = null)
-    {
-        if (_initialized)
-        {
-            Logging.Logger.LogError ("Init called multiple times without shutdown, aborting.");
-
-            throw new InvalidOperationException ("Init called multiple times without Shutdown");
-        }
-
-        if (!string.IsNullOrWhiteSpace (driverName))
-        {
-            _driverName = driverName;
-        }
-
-        if (string.IsNullOrWhiteSpace (_driverName))
-        {
-            _driverName = ForceDriver;
-        }
-
-        Debug.Assert (_navigation is null);
-        _navigation = new ();
-
-        Debug.Assert (_popover is null);
-        _popover = new ();
-
-        // Preserve existing keyboard settings if they exist
-        bool hasExistingKeyboard = _keyboard is not null;
-        Key existingQuitKey = _keyboard?.QuitKey ?? Key.Esc;
-        Key existingArrangeKey = _keyboard?.ArrangeKey ?? Key.F5.WithCtrl;
-        Key existingNextTabKey = _keyboard?.NextTabKey ?? Key.Tab;
-        Key existingPrevTabKey = _keyboard?.PrevTabKey ?? Key.Tab.WithShift;
-        Key existingNextTabGroupKey = _keyboard?.NextTabGroupKey ?? Key.F6;
-        Key existingPrevTabGroupKey = _keyboard?.PrevTabGroupKey ?? Key.F6.WithShift;
-
-        // Reset keyboard to ensure fresh state with default bindings
-        _keyboard = new KeyboardImpl { Application = this };
-
-        // Restore previously set keys if they existed and were different from defaults
-        if (hasExistingKeyboard)
-        {
-            _keyboard.QuitKey = existingQuitKey;
-            _keyboard.ArrangeKey = existingArrangeKey;
-            _keyboard.NextTabKey = existingNextTabKey;
-            _keyboard.PrevTabKey = existingPrevTabKey;
-            _keyboard.NextTabGroupKey = existingNextTabGroupKey;
-            _keyboard.PrevTabGroupKey = existingPrevTabGroupKey;
-        }
-
-        CreateDriver (driverName ?? _driverName);
-        Screen = Driver!.Screen;
-        _initialized = true;
-
-        Application.OnInitializedChanged (this, new (true));
-        Application.SubscribeDriverEvents ();
-
-        SynchronizationContext.SetSynchronizationContext (new ());
-        _mainThreadId = Thread.CurrentThread.ManagedThreadId;
-    }
-
-    private void CreateDriver (string? driverName)
-    {
-        PlatformID p = Environment.OSVersion.Platform;
-
-        // Check component factory type first - this takes precedence over driverName
-        bool factoryIsWindows = _componentFactory is IComponentFactory<WindowsConsole.InputRecord>;
-        bool factoryIsDotNet = _componentFactory is IComponentFactory<ConsoleKeyInfo>;
-        bool factoryIsUnix = _componentFactory is IComponentFactory<char>;
-        bool factoryIsFake = _componentFactory is IComponentFactory<ConsoleKeyInfo>;
-
-        // Then check driverName
-        bool nameIsWindows = driverName?.Contains ("win", StringComparison.OrdinalIgnoreCase) ?? false;
-        bool nameIsDotNet = (driverName?.Contains ("dotnet", StringComparison.OrdinalIgnoreCase) ?? false);
-        bool nameIsUnix = driverName?.Contains ("unix", StringComparison.OrdinalIgnoreCase) ?? false;
-        bool nameIsFake = driverName?.Contains ("fake", StringComparison.OrdinalIgnoreCase) ?? false;
-
-        // Decide which driver to use - component factory type takes priority
-        if (factoryIsFake || (!factoryIsWindows && !factoryIsDotNet && !factoryIsUnix && nameIsFake))
-        {
-            FakeConsoleOutput fakeOutput = new ();
-            fakeOutput.SetConsoleSize (80, 25);
-
-            _coordinator = CreateSubcomponents (() => new FakeComponentFactory (null, fakeOutput));
-        }
-        else if (factoryIsWindows || (!factoryIsDotNet && !factoryIsUnix && nameIsWindows))
-        {
-            _coordinator = CreateSubcomponents (() => new WindowsComponentFactory ());
-        }
-        else if (factoryIsDotNet || (!factoryIsWindows && !factoryIsUnix && nameIsDotNet))
-        {
-            _coordinator = CreateSubcomponents (() => new NetComponentFactory ());
-        }
-        else if (factoryIsUnix || (!factoryIsWindows && !factoryIsDotNet && nameIsUnix))
-        {
-            _coordinator = CreateSubcomponents (() => new UnixComponentFactory ());
-        }
-        else if (p == PlatformID.Win32NT || p == PlatformID.Win32S || p == PlatformID.Win32Windows)
-        {
-            _coordinator = CreateSubcomponents (() => new WindowsComponentFactory ());
-        }
-        else
-        {
-            _coordinator = CreateSubcomponents (() => new UnixComponentFactory ());
-        }
-
-        _coordinator.StartAsync ().Wait ();
-
-        if (_driver == null)
-        {
-            throw new ("Driver was null even after booting MainLoopCoordinator");
-        }
-    }
-
-    private IMainLoopCoordinator CreateSubcomponents<T> (Func<IComponentFactory<T>> fallbackFactory)
-    {
-        ConcurrentQueue<T> inputBuffer = new ();
-        ApplicationMainLoop<T> loop = new ();
-
-        IComponentFactory<T> cf;
-
-        if (_componentFactory is IComponentFactory<T> typedFactory)
-        {
-            cf = typedFactory;
-        }
-        else
-        {
-            cf = fallbackFactory ();
-        }
-
-        return new MainLoopCoordinator<T> (_timedEvents, inputBuffer, loop, cf);
-    }
-
-    /// <summary>
-    ///     Runs the application by creating a <see cref="Toplevel"/> object and calling
-    ///     <see cref="Run(Toplevel, Func{Exception, bool})"/>.
-    /// </summary>
-    /// <returns>The created <see cref="Toplevel"/> object. The caller is responsible for disposing this object.</returns>
-    [RequiresUnreferencedCode ("AOT")]
-    [RequiresDynamicCode ("AOT")]
-    public Toplevel Run (Func<Exception, bool>? errorHandler = null, IConsoleDriver? driver = null) { return Run<Toplevel> (errorHandler, driver); }
-
-    /// <summary>
-    ///     Runs the application by creating a <see cref="Toplevel"/>-derived object of type <c>T</c> and calling
-    ///     <see cref="Run(Toplevel, Func{Exception, bool})"/>.
-    /// </summary>
-    /// <param name="errorHandler"></param>
-    /// <param name="driver">
-    ///     The <see cref="IConsoleDriver"/> to use. If not specified the default driver for the platform will
-    ///     be used. Must be <see langword="null"/> if <see cref="Init"/> has already been called.
-    /// </param>
-    /// <returns>The created T object. The caller is responsible for disposing this object.</returns>
-    [RequiresUnreferencedCode ("AOT")]
-    [RequiresDynamicCode ("AOT")]
-    public T Run<T> (Func<Exception, bool>? errorHandler = null, IConsoleDriver? driver = null)
-        where T : Toplevel, new()
-    {
-        if (!_initialized)
-        {
-            // Init() has NOT been called. Auto-initialize as per interface contract.
-            Init (driver, null);
-        }
-
-        T top = new ();
-        Run (top, errorHandler);
-        return top;
-    }
-
-    /// <summary>Runs the Application using the provided <see cref="Toplevel"/> view.</summary>
-    /// <param name="view">The <see cref="Toplevel"/> to run as a modal.</param>
-    /// <param name="errorHandler">Handler for any unhandled exceptions.</param>
-    public void Run (Toplevel view, Func<Exception, bool>? errorHandler = null)
-    {
-        Logging.Information ($"Run '{view}'");
-        ArgumentNullException.ThrowIfNull (view);
-
-        if (!_initialized)
-        {
-            throw new NotInitializedException (nameof (Run));
-        }
-
-        if (_driver == null)
-        {
-            throw new InvalidOperationException ("Driver was inexplicably null when trying to Run view");
-        }
-
-        _top = view;
-
-        RunState rs = Application.Begin (view);
-
-        _top.Running = true;
-
-        while (_topLevels.TryPeek (out Toplevel? found) && found == view && view.Running)
-        {
-            if (_coordinator is null)
-            {
-                throw new ($"{nameof (IMainLoopCoordinator)} inexplicably became null during Run");
-            }
-
-            _coordinator.RunIteration ();
-        }
-
-        Logging.Information ($"Run - Calling End");
-        Application.End (rs);
-    }
-
-    /// <summary>Shutdown an application initialized with <see cref="Init"/>.</summary>
-    public void Shutdown ()
-    {
-        _coordinator?.Stop ();
-
-        bool wasInitialized = _initialized;
-
-        // Reset Screen before calling Application.ResetState to avoid circular reference
-        ResetScreen ();
-
-        // Call ResetState FIRST so it can properly dispose Popover and other resources
-        // that are accessed via Application.* static properties that now delegate to instance fields
-        Application.ResetState ();
-        ConfigurationManager.PrintJsonErrors ();
-
-        // Clear instance fields after ResetState has disposed everything
-        _driver = null;
-        _mouse = null;
-        _keyboard = null;
-        _initialized = false;
-        _navigation = null;
-        _popover = null;
-        CachedRunStateToplevel = null;
-        _top = null;
-        _topLevels.Clear ();
-        _mainThreadId = -1;
-        _screen = null;
-        _clearScreenNextIteration = false;
-        _sixel.Clear ();
-        // Don't reset ForceDriver and Force16Colors; they need to be set before Init is called
-
-        if (wasInitialized)
-        {
-            bool init = _initialized; // Will be false after clearing fields above
-            Application.OnInitializedChanged (this, new (in init));
-        }
-
-        _lazyInstance = new (() => new ApplicationImpl ());
-    }
-
-    /// <inheritdoc />
-    public void RequestStop (Toplevel? top)
-    {
-        Logging.Logger.LogInformation ($"RequestStop '{(top is { } ? top : "null")}'");
-
-        top ??= _top;
-
-        if (top == null)
-        {
-            return;
-        }
-
-        ToplevelClosingEventArgs ev = new (top);
-        top.OnClosing (ev);
-
-        if (ev.Cancel)
-        {
-            return;
-        }
-
-        top.Running = false;
-    }
-
-    /// <inheritdoc />
-    public void Invoke (Action action)
-    {
-        // If we are already on the main UI thread
-        if (Top is { Running: true } && _mainThreadId == Thread.CurrentThread.ManagedThreadId)
-        {
-            action ();
-            return;
-        }
-
-        _timedEvents.Add (TimeSpan.Zero,
-                              () =>
-                              {
-                                  action ();
-                                  return false;
-                              }
-                             );
-    }
-
-    /// <inheritdoc />
-    public bool IsLegacy => false;
-
-    /// <inheritdoc />
-    public object AddTimeout (TimeSpan time, Func<bool> callback) { return _timedEvents.Add (time, callback); }
-
-    /// <inheritdoc />
-    public bool RemoveTimeout (object token) { return _timedEvents.Remove (token); }
-
-    /// <inheritdoc />
-    public void LayoutAndDraw (bool forceRedraw = false)
-    {
-        List<View> tops = [.. _topLevels];
-
-        if (_popover?.GetActivePopover () as View is { Visible: true } visiblePopover)
-        {
-            visiblePopover.SetNeedsDraw ();
-            visiblePopover.SetNeedsLayout ();
-            tops.Insert (0, visiblePopover);
-        }
-
-        bool neededLayout = View.Layout (tops.ToArray ().Reverse (), Screen.Size);
-
-        if (ClearScreenNextIteration)
-        {
-            forceRedraw = true;
-            ClearScreenNextIteration = false;
-        }
-
-        if (forceRedraw)
-        {
-            _driver?.ClearContents ();
-        }
-
-        View.SetClipToScreen ();
-        View.Draw (tops, neededLayout || forceRedraw);
-        View.SetClipToScreen ();
-        _driver?.Refresh ();
-    }
-
-    /// <summary>
-    /// Resets the Screen field to null so it will be recalculated on next access.
-    /// </summary>
-    internal void ResetScreen ()
-    {
-        lock (_lockScreen)
-        {
-            _screen = null;
-        }
-    }
+    #endregion View Management
 }

+ 15 - 7
Terminal.Gui/App/CWP/CWPPropertyHelper.cs

@@ -22,7 +22,10 @@ public static class CWPPropertyHelper
     ///     The type of the property value, which may be a nullable reference type (e.g., <see cref="string"/>
     ///     ?).
     /// </typeparam>
-    /// <param name="currentValue">The current property value, which may be null for nullable types.</param>
+    /// <param name="currentValue">
+    ///     Reference to the current property value, which may be null for nullable types. If the change is not cancelled, this
+    ///     will be set to <paramref name="finalValue"/>.
+    /// </param>
     /// <param name="newValue">The proposed new property value, which may be null for nullable types.</param>
     /// <param name="onChanging">The virtual method invoked before the change, returning true to cancel.</param>
     /// <param name="changingEvent">The pre-change event raised to allow modification or cancellation.</param>
@@ -52,9 +55,9 @@ public static class CWPPropertyHelper
     ///     </code>
     /// </example>
     public static bool ChangeProperty<T> (
-        T currentValue,
+        ref T currentValue,
         T newValue,
-        Func<ValueChangingEventArgs<T>, bool> onChanging,
+        Func<ValueChangingEventArgs<T>, bool>? onChanging,
         EventHandler<ValueChangingEventArgs<T>>? changingEvent,
         Action<T> doWork,
         Action<ValueChangedEventArgs<T>>? onChanged,
@@ -70,13 +73,17 @@ public static class CWPPropertyHelper
         }
 
         ValueChangingEventArgs<T> args = new (currentValue, newValue);
-        bool cancelled = onChanging (args) || args.Handled;
 
-        if (cancelled)
+        if (onChanging is { })
         {
-            finalValue = currentValue;
+            bool cancelled = onChanging (args) || args.Handled;
 
-            return false;
+            if (cancelled)
+            {
+                finalValue = currentValue;
+
+                return false;
+            }
         }
 
         changingEvent?.Invoke (null, args);
@@ -100,6 +107,7 @@ public static class CWPPropertyHelper
         doWork (finalValue);
         
         ValueChangedEventArgs<T> changedArgs = new (currentValue, finalValue);
+        currentValue = finalValue;
         onChanged?.Invoke (changedArgs);
         changedEvent?.Invoke (null, changedArgs);
 

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

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

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

@@ -5,7 +5,7 @@ namespace Terminal.Gui.App;
 
 /// <summary>
 ///     Helper class for console drivers to invoke shell commands to interact with the clipboard. Used primarily by
-///     UnixDriver, but also used in Unit tests which is why it is in IConsoleDriver.cs.
+///     UnixDriver, but also used in Unit tests which is why it is in IDriver.cs.
 /// </summary>
 internal static class ClipboardProcessRunner
 {

+ 440 - 163
Terminal.Gui/App/IApplication.cs

@@ -1,4 +1,5 @@
 #nullable enable
+using System.Collections.Concurrent;
 using System.Diagnostics.CodeAnalysis;
 
 namespace Terminal.Gui.App;
@@ -9,257 +10,533 @@ namespace Terminal.Gui.App;
 /// </summary>
 public interface IApplication
 {
-    /// <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);
+    #region Keyboard
 
     /// <summary>
-    /// Handles keyboard input and key bindings at the Application level.
+    ///     Handles keyboard input and key bindings at the Application level.
     /// </summary>
+    /// <remarks>
+    ///     <para>
+    ///         Provides access to keyboard state, key bindings, and keyboard event handling. Set during <see cref="Init"/>.
+    ///     </para>
+    /// </remarks>
     IKeyboard Keyboard { get; set; }
 
+    #endregion Keyboard
+
+    #region Mouse
+
     /// <summary>
     ///     Handles mouse event state and processing.
     /// </summary>
+    /// <remarks>
+    ///     <para>
+    ///         Provides access to mouse state, mouse grabbing, and mouse event handling. Set during <see cref="Init"/>.
+    ///     </para>
+    /// </remarks>
     IMouse Mouse { get; set; }
 
-    /// <summary>Gets or sets the console driver being used.</summary>
-    IConsoleDriver? Driver { get; set; }
+    #endregion Mouse
+
+    #region Initialization and Shutdown
+
+    /// <summary>Initializes a new instance of <see cref="Terminal.Gui"/> Application.</summary>
+    /// <param name="driver">
+    ///     The <see cref="IDriver"/> to use. If neither <paramref name="driver"/> or
+    ///     <paramref name="driverName"/> are specified the default driver for the platform will be used.
+    /// </param>
+    /// <param name="driverName">
+    ///     The short name (e.g. "dotnet", "windows", "unix", or "fake") of the
+    ///     <see cref="IDriver"/> to use. If neither <paramref name="driver"/> or <paramref name="driverName"/> are
+    ///     specified the default driver for the platform will be used.
+    /// </param>
+    /// <remarks>
+    ///     <para>Call this method once per instance (or after <see cref="Shutdown"/> has been called).</para>
+    ///     <para>
+    ///         This function loads the right <see cref="IDriver"/> for the platform, creates a main loop coordinator,
+    ///         initializes keyboard and mouse handlers, and subscribes to driver events.
+    ///     </para>
+    ///     <para>
+    ///         <see cref="Shutdown"/> must be called when the application is closing (typically after
+    ///         <see cref="Run{T}"/> has returned) to ensure resources are cleaned up and terminal settings restored.
+    ///     </para>
+    ///     <para>
+    ///         The <see cref="Run{T}"/> function combines <see cref="Init(IDriver,string)"/> and
+    ///         <see cref="Run(Toplevel, Func{Exception, bool})"/> into a single call. An application can use
+    ///         <see cref="Run{T}"/> without explicitly calling <see cref="Init(IDriver,string)"/>.
+    ///     </para>
+    /// </remarks>
+    [RequiresUnreferencedCode ("AOT")]
+    [RequiresDynamicCode ("AOT")]
+    public void Init (IDriver? driver = null, string? driverName = null);
+
+    /// <summary>
+    ///     This event is raised after the <see cref="Init"/> and <see cref="Shutdown"/> methods have been called.
+    /// </summary>
+    /// <remarks>
+    ///     Intended to support unit tests that need to know when the application has been initialized.
+    /// </remarks>
+    public event EventHandler<EventArgs<bool>>? InitializedChanged;
 
     /// <summary>Gets or sets whether the application has been initialized.</summary>
     bool Initialized { get; set; }
 
+    /// <summary>Shutdown an application initialized with <see cref="Init"/>.</summary>
+    /// <remarks>
+    ///     Shutdown must be called for every call to <see cref="Init"/> or
+    ///     <see cref="Application.Run(Toplevel, Func{Exception, bool})"/> to ensure all resources are cleaned
+    ///     up (Disposed) and terminal settings are restored.
+    /// </remarks>
+    public void Shutdown ();
+
     /// <summary>
-    ///     Gets or sets whether <see cref="Driver"/> will be forced to output only the 16 colors defined in
-    ///     <see cref="ColorName16"/>. The default is <see langword="false"/>, meaning 24-bit (TrueColor) colors will be output
-    ///     as long as the selected <see cref="IConsoleDriver"/> supports TrueColor.
+    ///     Resets the state of this instance.
     /// </summary>
-    bool Force16Colors { get; set; }
+    /// <param name="ignoreDisposed">If true, ignores disposed state checks during reset.</param>
+    /// <remarks>
+    ///     <para>
+    ///         Encapsulates all setting of initial state for Application; having this in a function like this ensures we
+    ///         don't make mistakes in guaranteeing that the state of this singleton is deterministic when <see cref="Init"/>
+    ///         starts running and after <see cref="Shutdown"/> returns.
+    ///     </para>
+    ///     <para>
+    ///         IMPORTANT: Ensure all property/fields are reset here. See Init_ResetState_Resets_Properties unit test.
+    ///     </para>
+    /// </remarks>
+    public void ResetState (bool ignoreDisposed = false);
+
+    #endregion Initialization and Shutdown
+
+    #region Begin->Run->Iteration->Stop->End
 
     /// <summary>
-    ///     Forces the use of the specified driver (one of "fake", "dotnet", "windows", or "unix"). If not
-    ///     specified, the driver is selected based on the platform.
+    ///     Building block API: Creates a <see cref="SessionToken"/> and prepares the provided <see cref="Toplevel"/> for
+    ///     execution. Not usually called directly by applications. Use <see cref="Run(Toplevel, Func{Exception, bool})"/>
+    ///     instead.
     /// </summary>
-    string ForceDriver { get; set; }
+    /// <returns>
+    ///     The <see cref="SessionToken"/> that needs to be passed to the <see cref="End(SessionToken)"/> method upon
+    ///     completion.
+    /// </returns>
+    /// <param name="toplevel">The <see cref="Toplevel"/> to prepare execution for.</param>
+    /// <remarks>
+    ///     <para>
+    ///         This method prepares the provided <see cref="Toplevel"/> for running. It adds this to the
+    ///         list of <see cref="Toplevel"/>s, lays out the SubViews, focuses the first element, and draws the
+    ///         <see cref="Toplevel"/> on the screen. This is usually followed by starting the main loop, and then the
+    ///         <see cref="End(SessionToken)"/> method upon termination which will undo these changes.
+    ///     </para>
+    ///     <para>
+    ///         Raises the <see cref="SessionBegun"/> event before returning.
+    ///     </para>
+    /// </remarks>
+    public SessionToken Begin (Toplevel toplevel);
 
     /// <summary>
-    /// Collection of sixel images to write out to screen when updating.
-    /// Only add to this collection if you are sure terminal supports sixel format.
+    ///     Runs a new Session creating a <see cref="Toplevel"/> and calling <see cref="Begin(Toplevel)"/>. When the session is
+    ///     stopped, <see cref="End(SessionToken)"/> will be called.
     /// </summary>
-    List<SixelToRender> Sixel { get; }
+    /// <param name="errorHandler">Handler for any unhandled exceptions (resumes when returns true, rethrows when null).</param>
+    /// <param name="driver">
+    ///     The driver name. If not specified the default driver for the platform will be used. Must be
+    ///     <see langword="null"/> if <see cref="Init"/> has already been called.
+    /// </param>
+    /// <returns>The created <see cref="Toplevel"/>. The caller is responsible for disposing this object.</returns>
+    /// <remarks>
+    ///     <para>Calling <see cref="Init"/> first is not needed as this function will initialize the application.</para>
+    ///     <para>
+    ///         <see cref="Shutdown"/> must be called when the application is closing (typically after Run has returned) to
+    ///         ensure resources are cleaned up and terminal settings restored.
+    ///     </para>
+    ///     <para>
+    ///         The caller is responsible for disposing the object returned by this method.
+    ///     </para>
+    /// </remarks>
+    [RequiresUnreferencedCode ("AOT")]
+    [RequiresDynamicCode ("AOT")]
+    public Toplevel Run (Func<Exception, bool>? errorHandler = null, string? driver = null);
 
     /// <summary>
-    ///     Gets or sets the size of the screen. By default, this is the size of the screen as reported by the <see cref="IConsoleDriver"/>.
+    ///     Runs a new Session creating a <see cref="Toplevel"/>-derived object of type <typeparamref name="TView"/>
+    ///     and calling <see cref="Run(Toplevel, Func{Exception, bool})"/>. When the session is stopped,
+    ///     <see cref="End(SessionToken)"/> will be called.
     /// </summary>
-    Rectangle Screen { get; set; }
+    /// <typeparam name="TView">The type of <see cref="Toplevel"/> to create and run.</typeparam>
+    /// <param name="errorHandler">Handler for any unhandled exceptions (resumes when returns true, rethrows when null).</param>
+    /// <param name="driver">
+    ///     The driver name. If not specified the default driver for the platform will be used. Must be
+    ///     <see langword="null"/> if <see cref="Init"/> has already been called.
+    /// </param>
+    /// <returns>The created <typeparamref name="TView"/> object. The caller is responsible for disposing this object.</returns>
+    /// <remarks>
+    ///     <para>
+    ///         This method is used to start processing events for the main application, but it is also used to run other
+    ///         modal <see cref="View"/>s such as <see cref="Dialog"/> boxes.
+    ///     </para>
+    ///     <para>
+    ///         To make <see cref="Run(Toplevel, Func{Exception, bool})"/> stop execution, call
+    ///         <see cref="RequestStop()"/> or <see cref="RequestStop(Toplevel)"/>.
+    ///     </para>
+    ///     <para>
+    ///         Calling <see cref="Run(Toplevel, Func{Exception, bool})"/> is equivalent to calling
+    ///         <see cref="Begin(Toplevel)"/>, followed by starting the main loop, and then calling
+    ///         <see cref="End(SessionToken)"/>.
+    ///     </para>
+    ///     <para>
+    ///         When using <see cref="Run{T}"/> or <see cref="Run(Func{Exception, bool}, string)"/>,
+    ///         <see cref="Init"/> will be called automatically.
+    ///     </para>
+    ///     <para>
+    ///         In RELEASE builds: When <paramref name="errorHandler"/> is <see langword="null"/> any exceptions will be
+    ///         rethrown. Otherwise, <paramref name="errorHandler"/> will be called. If <paramref name="errorHandler"/>
+    ///         returns <see langword="true"/> the main loop will resume; otherwise this method will exit.
+    ///     </para>
+    ///     <para>
+    ///         <see cref="Shutdown"/> must be called when the application is closing (typically after Run has returned) to
+    ///         ensure resources are cleaned up and terminal settings restored.
+    ///     </para>
+    ///     <para>
+    ///         In RELEASE builds: When <paramref name="errorHandler"/> is <see langword="null"/> any exceptions will be
+    ///         rethrown. Otherwise, <paramref name="errorHandler"/> will be called. If <paramref name="errorHandler"/>
+    ///         returns <see langword="true"/> the main loop will resume; otherwise this method will exit.
+    ///     </para>
+    ///     <para>
+    ///         The caller is responsible for disposing the object returned by this method.
+    ///     </para>
+    /// </remarks>
+    [RequiresUnreferencedCode ("AOT")]
+    [RequiresDynamicCode ("AOT")]
+    public TView Run<TView> (Func<Exception, bool>? errorHandler = null, string? driver = null)
+        where TView : Toplevel, new ();
 
     /// <summary>
-    ///     Gets or sets whether the screen will be cleared, and all Views redrawn, during the next Application iteration.
+    ///     Runs a new Session using the provided <see cref="Toplevel"/> view and calling
+    ///     <see cref="Run(Toplevel, Func{Exception, bool})"/>.
+    ///     When the session is stopped, <see cref="End(SessionToken)"/> will be called..
     /// </summary>
-    bool ClearScreenNextIteration { get; set; }
-
-    /// <summary>Gets or sets the popover manager.</summary>
-    ApplicationPopover? Popover { get; set; }
+    /// <param name="view">The <see cref="Toplevel"/> to run as a modal.</param>
+    /// <param name="errorHandler">Handler for any unhandled exceptions (resumes when returns true, rethrows when null).</param>
+    /// <remarks>
+    ///     <para>
+    ///         This method is used to start processing events for the main application, but it is also used to run other
+    ///         modal <see cref="View"/>s such as <see cref="Dialog"/> boxes.
+    ///     </para>
+    ///     <para>
+    ///         To make <see cref="Run(Toplevel, Func{Exception, bool})"/> stop execution, call
+    ///         <see cref="RequestStop()"/> or <see cref="RequestStop(Toplevel)"/>.
+    ///     </para>
+    ///     <para>
+    ///         Calling <see cref="Run(Toplevel, Func{Exception, bool})"/> is equivalent to calling
+    ///         <see cref="Begin(Toplevel)"/>, followed by starting the main loop, and then calling
+    ///         <see cref="End(SessionToken)"/>.
+    ///     </para>
+    ///     <para>
+    ///         When using <see cref="Run{T}"/> or <see cref="Run(Func{Exception, bool}, string)"/>,
+    ///         <see cref="Init"/> will be called automatically.
+    ///     </para>
+    ///     <para>
+    ///         <see cref="Shutdown"/> must be called when the application is closing (typically after Run has returned) to
+    ///         ensure resources are cleaned up and terminal settings restored.
+    ///     </para>
+    ///     <para>
+    ///         In RELEASE builds: When <paramref name="errorHandler"/> is <see langword="null"/> any exceptions will be
+    ///         rethrown. Otherwise, <paramref name="errorHandler"/> will be called. If <paramref name="errorHandler"/>
+    ///         returns <see langword="true"/> the main loop will resume; otherwise this method will exit.
+    ///     </para>
+    ///     <para>
+    ///         The caller is responsible for disposing the object returned by this method.
+    ///     </para>
+    /// </remarks>
+    public void Run (Toplevel view, Func<Exception, bool>? errorHandler = null);
 
-    /// <summary>Gets or sets the navigation manager.</summary>
-    ApplicationNavigation? Navigation { get; set; }
+    /// <summary>
+    ///     Raises the <see cref="Iteration"/> event.
+    /// </summary>
+    /// <remarks>
+    ///     This is called once per main loop iteration, before processing input, timeouts, or rendering.
+    /// </remarks>
+    public void RaiseIteration ();
 
-    /// <summary>Gets the currently active Toplevel.</summary>
-    Toplevel? Top { get; set; }
+    /// <summary>This event is raised on each iteration of the main loop.</summary>
+    /// <remarks>
+    ///     <para>
+    ///         This event is raised before input processing, timeout callbacks, and rendering occur each iteration.
+    ///     </para>
+    ///     <para>See also <see cref="AddTimeout"/> and <see cref="TimedEvents"/>.</para>
+    /// </remarks>
+    public event EventHandler<IterationEventArgs>? Iteration;
 
-    /// <summary>Gets the stack of all Toplevels.</summary>
-    System.Collections.Concurrent.ConcurrentStack<Toplevel> TopLevels { get; }
+    /// <summary>Runs <paramref name="action"/> on the main UI loop thread.</summary>
+    /// <param name="action">The action to be invoked on the main processing thread.</param>
+    /// <remarks>
+    ///     <para>
+    ///         If called from the main thread, the action is executed immediately. Otherwise, it is queued via
+    ///         <see cref="AddTimeout"/> with <see cref="TimeSpan.Zero"/> and will be executed on the next main loop
+    ///         iteration.
+    ///     </para>
+    /// </remarks>
+    void Invoke (Action action);
 
     /// <summary>
-    /// Caches the Toplevel associated with the current RunState.
+    ///     Building block API: Ends a Session and completes the execution of a <see cref="Toplevel"/> that was started with
+    ///     <see cref="Begin(Toplevel)"/>. Not usually called directly by applications.
+    ///     <see cref="Run(Toplevel, Func{Exception, bool})"/>
+    ///     will automatically call this method when the session is stopped.
     /// </summary>
-    Toplevel? CachedRunStateToplevel { get; set; }
+    /// <param name="sessionToken">The <see cref="SessionToken"/> returned by the <see cref="Begin(Toplevel)"/> method.</param>
+    /// <remarks>
+    ///     <para>
+    ///         This method removes the <see cref="Toplevel"/> from the stack, raises the <see cref="SessionEnded"/>
+    ///         event, and disposes the <paramref name="sessionToken"/>.
+    ///     </para>
+    /// </remarks>
+    public void End (SessionToken sessionToken);
 
-    /// <summary>Requests that the application stop running.</summary>
+    /// <summary>Requests that the currently running Session stop. The Session will stop after the current iteration completes.</summary>
+    /// <remarks>
+    ///     <para>This will cause <see cref="Run(Toplevel, Func{Exception, bool})"/> to return.</para>
+    ///     <para>
+    ///         This is equivalent to calling <see cref="RequestStop(Toplevel)"/> with <see cref="Top"/> as the parameter.
+    ///     </para>
+    /// </remarks>
     void RequestStop ();
 
+    /// <summary>Requests that the currently running Session stop. The Session will stop after the current iteration completes.</summary>
+    /// <param name="top">
+    ///     The <see cref="Toplevel"/> to stop. If <see langword="null"/>, stops the currently running <see cref="Top"/>.
+    /// </param>
+    /// <remarks>
+    ///     <para>This will cause <see cref="Run(Toplevel, Func{Exception, bool})"/> to return.</para>
+    ///     <para>
+    ///         Calling <see cref="RequestStop(Toplevel)"/> is equivalent to setting the <see cref="Toplevel.Running"/>
+    ///         property on the specified <see cref="Toplevel"/> to <see langword="false"/>.
+    ///     </para>
+    /// </remarks>
+    void RequestStop (Toplevel? top);
+
     /// <summary>
-    ///     Causes any Toplevels that need layout to be laid out. Then draws any Toplevels that need display. Only Views that
-    ///     need to be laid out (see <see cref="View.NeedsLayout"/>) will be laid out.
-    ///     Only Views that need to be drawn (see <see cref="View.NeedsDraw"/>) will be drawn.
+    ///     Set to <see langword="true"/> to cause the session to stop running after first iteration.
     /// </summary>
-    /// <param name="forceRedraw">
-    ///     If <see langword="true"/> the entire View hierarchy will be redrawn. The default is <see langword="false"/> and
-    ///     should only be overriden for testing.
-    /// </param>
-    public void LayoutAndDraw (bool forceRedraw = false);
+    /// <remarks>
+    ///     <para>
+    ///         Used primarily for unit testing. When <see langword="true"/>, <see cref="End"/> will be called
+    ///         automatically after the first main loop iteration.
+    ///     </para>
+    /// </remarks>
+    bool StopAfterFirstIteration { 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>
-    ///     This function loads the right <see cref="IConsoleDriver"/> for the platform, Creates a <see cref="Toplevel"/>. and
-    ///     assigns it to <see cref="Application.Top"/>
-    /// </para>
-    /// <para>
-    ///     <see cref="Shutdown"/> must be called when the application is closing (typically after
-    ///     <see cref="Run{T}"/> has returned) to ensure resources are cleaned up and
-    ///     terminal settings
-    ///     restored.
-    /// </para>
-    /// <para>
-    ///     The <see cref="Run{T}"/> function combines
-    ///     <see cref="Init(IConsoleDriver,string)"/> and <see cref="Run(Toplevel, Func{Exception, bool})"/>
-    ///     into a single
-    ///     call. An application cam use <see cref="Run{T}"/> without explicitly calling
-    ///     <see cref="Init(IConsoleDriver,string)"/>.
-    /// </para>
-    /// <param name="driver">
-    ///     The <see cref="IConsoleDriver"/> to use. If neither <paramref name="driver"/> or
-    ///     <paramref name="driverName"/> are specified the default driver for the platform will be used.
-    /// </param>
-    /// <param name="driverName">
-    ///     The driver name (e.g. "dotnet", "windows", "fake", or "unix") of the
-    ///     <see cref="IConsoleDriver"/> to use. If neither <paramref name="driver"/> or <paramref name="driverName"/> are
-    ///     specified the default driver for the platform will be used.
-    /// </param>
-    [RequiresUnreferencedCode ("AOT")]
-    [RequiresDynamicCode ("AOT")]
-    public void Init (IConsoleDriver? driver = null, string? driverName = null);
+    /// <summary>
+    ///     Raised when <see cref="Begin(Toplevel)"/> has been called and has created a new <see cref="SessionToken"/>.
+    /// </summary>
+    /// <remarks>
+    ///     If <see cref="StopAfterFirstIteration"/> is <see langword="true"/>, callers to <see cref="Begin(Toplevel)"/>
+    ///     must also subscribe to <see cref="SessionEnded"/> and manually dispose of the <see cref="SessionToken"/> token
+    ///     when the application is done.
+    /// </remarks>
+    public event EventHandler<SessionTokenEventArgs>? SessionBegun;
 
-    /// <summary>Runs <paramref name="action"/> on the main UI loop thread</summary>
-    /// <param name="action">the action to be invoked on the main processing thread.</param>
-    void Invoke (Action action);
+    /// <summary>
+    ///     Raised when <see cref="End(SessionToken)"/> was called and the session is stopping. The event args contain a
+    ///     reference to the <see cref="Toplevel"/>
+    ///     that was active during the session. This can be used to ensure the Toplevel is disposed of properly.
+    /// </summary>
+    /// <remarks>
+    ///     If <see cref="StopAfterFirstIteration"/> is <see langword="true"/>, callers to <see cref="Begin(Toplevel)"/>
+    ///     must also subscribe to <see cref="SessionEnded"/> and manually dispose of the <see cref="SessionToken"/> token
+    ///     when the application is done.
+    /// </remarks>
+    public event EventHandler<ToplevelEventArgs>? SessionEnded;
+
+    #endregion Begin->Run->Iteration->Stop->End
+
+    #region Toplevel Management
+
+    /// <summary>Gets or sets the current Toplevel.</summary>
+    /// <remarks>
+    ///     <para>
+    ///         This is set by <see cref="Begin(Toplevel)"/> and cleared by <see cref="End(SessionToken)"/>.
+    ///     </para>
+    /// </remarks>
+    Toplevel? Top { get; set; }
+
+    /// <summary>Gets the stack of all Toplevels.</summary>
+    /// <remarks>
+    ///     <para>
+    ///         Toplevels are added to this stack by <see cref="Begin(Toplevel)"/> and removed by
+    ///         <see cref="End(SessionToken)"/>.
+    ///     </para>
+    /// </remarks>
+    ConcurrentStack<Toplevel> TopLevels { get; }
 
     /// <summary>
-    ///     <see langword="true"/> if implementation is 'old'. <see langword="false"/> if implementation
-    ///     is cutting edge.
+    ///     Caches the Toplevel associated with the current Session.
     /// </summary>
-    bool IsLegacy { get; }
+    /// <remarks>
+    ///     Used internally to optimize Toplevel state transitions.
+    /// </remarks>
+    Toplevel? CachedSessionTokenToplevel { get; set; }
 
-    /// <summary>Removes a previously scheduled timeout</summary>
-    /// <remarks>The token parameter is the value returned by <see cref="AddTimeout"/>.</remarks>
-    /// <returns>
-    ///     <see langword="true"/>
-    ///     if the timeout is successfully removed; otherwise,
-    ///     <see langword="false"/>
-    ///     .
-    ///     This method also returns
-    ///     <see langword="false"/>
-    ///     if the timeout is not found.
-    /// </returns>
-    bool RemoveTimeout (object token);
+    #endregion Toplevel Management
+
+    #region Screen and Driver
 
-    /// <summary>Stops the provided <see cref="Toplevel"/>, causing or the <paramref name="top"/> if provided.</summary>
-    /// <param name="top">The <see cref="Toplevel"/> to stop.</param>
+    /// <summary>Gets or sets the console driver being used.</summary>
     /// <remarks>
-    ///     <para>This will cause <see cref="Application.Run(Toplevel, Func{Exception, bool})"/> to return.</para>
     ///     <para>
-    ///         Calling <see cref="RequestStop(Toplevel)"/> is equivalent to setting the <see cref="Toplevel.Running"/>
-    ///         property on the currently running <see cref="Toplevel"/> to false.
+    ///         Set by <see cref="Init"/> based on the driver parameter or platform default.
     ///     </para>
     /// </remarks>
-    void RequestStop (Toplevel? top);
+    IDriver? Driver { get; set; }
 
     /// <summary>
-    ///     Runs the application by creating a <see cref="Toplevel"/> object and calling
-    ///     <see cref="Run(Toplevel, Func{Exception, bool})"/>.
+    ///     Gets or sets whether <see cref="Driver"/> will be forced to output only the 16 colors defined in
+    ///     <see cref="ColorName16"/>. The default is <see langword="false"/>, meaning 24-bit (TrueColor) colors will be
+    ///     output as long as the selected <see cref="IDriver"/> supports TrueColor.
+    /// </summary>
+    bool Force16Colors { get; set; }
+
+    /// <summary>
+    ///     Forces the use of the specified driver (one of "fake", "dotnet", "windows", or "unix"). If not
+    ///     specified, the driver is selected based on the platform.
+    /// </summary>
+    string ForceDriver { get; set; }
+
+    /// <summary>
+    ///     Gets or sets the size of the screen. By default, this is the size of the screen as reported by the
+    ///     <see cref="IDriver"/>.
     /// </summary>
     /// <remarks>
-    ///     <para>Calling <see cref="Init"/> first is not needed as this function will initialize the application.</para>
     ///     <para>
-    ///         <see cref="Shutdown"/> must be called when the application is closing (typically after Run> has returned) to
-    ///         ensure resources are cleaned up and terminal settings restored.
+    ///         If the <see cref="IDriver"/> has not been initialized, this will return a default size of 2048x2048; useful
+    ///         for unit tests.
     ///     </para>
+    /// </remarks>
+    Rectangle Screen { get; set; }
+
+    /// <summary>Raised when the terminal's size changed. The new size of the terminal is provided.</summary>
+    /// <remarks>
     ///     <para>
-    ///         The caller is responsible for disposing the object returned by this method.
+    ///         This event is raised when the driver detects a screen size change. The event provides the new screen
+    ///         rectangle.
     ///     </para>
     /// </remarks>
-    /// <returns>The created <see cref="Toplevel"/> object. The caller is responsible for disposing this object.</returns>
-    [RequiresUnreferencedCode ("AOT")]
-    [RequiresDynamicCode ("AOT")]
-    public Toplevel Run (Func<Exception, bool>? errorHandler = null, IConsoleDriver? driver = null);
+    public event EventHandler<EventArgs<Rectangle>>? ScreenChanged;
 
     /// <summary>
-    ///     Runs the application by creating a <see cref="Toplevel"/>-derived object of type <c>T</c> and calling
-    ///     <see cref="Run(Toplevel, Func{Exception, bool})"/>.
+    ///     Gets or sets whether the screen will be cleared, and all Views redrawn, during the next Application iteration.
     /// </summary>
     /// <remarks>
-    ///     <para>Calling <see cref="Init"/> first is not needed as this function will initialize the application.</para>
     ///     <para>
-    ///         <see cref="Shutdown"/> must be called when the application is closing (typically after Run> has returned) to
-    ///         ensure resources are cleaned up and terminal settings restored.
+    ///         This is typically set to <see langword="true"/> when a View's <see cref="View.Frame"/> changes and that view
+    ///         has no SuperView (e.g. when <see cref="Top"/> is moved or resized).
     ///     </para>
     ///     <para>
-    ///         The caller is responsible for disposing the object returned by this method.
+    ///         Automatically reset to <see langword="false"/> after <see cref="LayoutAndDraw"/> processes it.
     ///     </para>
     /// </remarks>
-    /// <param name="errorHandler"></param>
-    /// <param name="driver">
-    ///     The <see cref="IConsoleDriver"/> to use. If not specified the default driver for the platform will
-    ///     be used. Must be
-    ///     <see langword="null"/> if <see cref="Init"/> has already been called.
-    /// </param>
-    /// <returns>The created T object. The caller is responsible for disposing this object.</returns>
-    [RequiresUnreferencedCode ("AOT")]
-    [RequiresDynamicCode ("AOT")]
-    public T Run<T> (Func<Exception, bool>? errorHandler = null, IConsoleDriver? driver = null)
-        where T : Toplevel, new();
+    bool ClearScreenNextIteration { get; set; }
 
-    /// <summary>Runs the Application using the provided <see cref="Toplevel"/> view.</summary>
+    /// <summary>
+    ///     Collection of sixel images to write out to screen when updating.
+    ///     Only add to this collection if you are sure terminal supports sixel format.
+    /// </summary>
+    List<SixelToRender> Sixel { get; }
+
+    #endregion Screen and Driver
+
+    #region Layout and Drawing
+
+    /// <summary>
+    ///     Causes any Toplevels that need layout to be laid out, then draws any Toplevels that need display. Only Views
+    ///     that need to be laid out (see <see cref="View.NeedsLayout"/>) will be laid out. Only Views that need to be drawn
+    ///     (see <see cref="View.NeedsDraw"/>) will be drawn.
+    /// </summary>
+    /// <param name="forceRedraw">
+    ///     If <see langword="true"/> the entire View hierarchy will be redrawn. The default is <see langword="false"/> and
+    ///     should only be overridden for testing.
+    /// </param>
     /// <remarks>
     ///     <para>
-    ///         This method is used to start processing events for the main application, but it is also used to run other
-    ///         modal <see cref="View"/>s such as <see cref="Dialog"/> boxes.
+    ///         This method is called automatically each main loop iteration when any views need layout or drawing.
     ///     </para>
     ///     <para>
-    ///         To make a <see cref="Run(Toplevel,System.Func{System.Exception,bool})"/> stop execution, call
-    ///         <see cref="Application.RequestStop"/>.
-    ///     </para>
-    ///     <para>
-    ///         Calling <see cref="Run(Toplevel,System.Func{System.Exception,bool})"/> is equivalent to calling
-    ///         <see cref="Application.Begin(Toplevel)"/>, followed by <see cref="Application.RunLoop(RunState)"/>, and then
-    ///         calling
-    ///         <see cref="Application.End(RunState)"/>.
+    ///         If <see cref="ClearScreenNextIteration"/> is <see langword="true"/>, the screen will be cleared before
+    ///         drawing and the flag will be reset to <see langword="false"/>.
     ///     </para>
+    /// </remarks>
+    public void LayoutAndDraw (bool forceRedraw = false);
+
+    /// <summary>
+    ///     Calls <see cref="View.PositionCursor"/> on the most focused view.
+    /// </summary>
+    /// <remarks>
+    ///     <para>Does nothing if there is no most focused view.</para>
     ///     <para>
-    ///         Alternatively, to have a program control the main loop and process events manually, call
-    ///         <see cref="Application.Begin(Toplevel)"/> to set things up manually and then repeatedly call
-    ///         <see cref="Application.RunLoop(RunState)"/> with the wait parameter set to false. By doing this the
-    ///         <see cref="Application.RunLoop(RunState)"/> method will only process any pending events, timers handlers and
-    ///         then
-    ///         return control immediately.
+    ///         If the most focused view is not visible within its superview, the cursor will be hidden.
     ///     </para>
+    /// </remarks>
+    /// <returns><see langword="true"/> if a view positioned the cursor and the position is visible.</returns>
+    public bool PositionCursor ();
+
+    #endregion Layout and Drawing
+
+    #region Navigation and Popover
+
+    /// <summary>Gets or sets the popover manager.</summary>
+    /// <remarks>
     ///     <para>
-    ///         When using <see cref="Run{T}"/> or
-    ///         <see cref="Run(System.Func{System.Exception,bool},IConsoleDriver)"/>
-    ///         <see cref="Init"/> will be called automatically.
+    ///         Manages application-level popover views. Initialized during <see cref="Init"/>.
     ///     </para>
+    /// </remarks>
+    ApplicationPopover? Popover { get; set; }
+
+    /// <summary>Gets or sets the navigation manager.</summary>
+    /// <remarks>
     ///     <para>
-    ///         RELEASE builds only: When <paramref name="errorHandler"/> is <see langword="null"/> any exceptions will be
-    ///         rethrown. Otherwise, if <paramref name="errorHandler"/> will be called. If <paramref name="errorHandler"/>
-    ///         returns <see langword="true"/> the <see cref="Application.RunLoop(RunState)"/> will resume; otherwise this
-    ///         method will
-    ///         exit.
+    ///         Manages focus navigation and tracking of the most focused view. Initialized during <see cref="Init"/>.
     ///     </para>
     /// </remarks>
-    /// <param name="view">The <see cref="Toplevel"/> to run as a modal.</param>
-    /// <param name="errorHandler">
-    ///     RELEASE builds only: Handler for any unhandled exceptions (resumes when returns true,
-    ///     rethrows when null).
-    /// </param>
-    public void Run (Toplevel view, Func<Exception, bool>? errorHandler = null);
+    ApplicationNavigation? Navigation { get; set; }
 
-    /// <summary>Shutdown an application initialized with <see cref="Init"/>.</summary>
+    #endregion Navigation and Popover
+
+    #region Timeouts
+
+    /// <summary>Adds a timeout to the application.</summary>
+    /// <param name="time">The time span to wait before invoking the callback.</param>
+    /// <param name="callback">
+    ///     The callback to invoke. If it returns <see langword="true"/>, the timeout will be reset and repeat. If it
+    ///     returns <see langword="false"/>, the timeout will stop and be removed.
+    /// </param>
+    /// <returns>
+    ///     A token that can be used to stop the timeout by calling <see cref="RemoveTimeout(object)"/>.
+    /// </returns>
     /// <remarks>
-    ///     Shutdown must be called for every call to <see cref="Init"/> or
-    ///     <see cref="Application.Run(Toplevel, Func{Exception, bool})"/> to ensure all resources are cleaned
-    ///     up (Disposed)
-    ///     and terminal settings are restored.
+    ///     <para>
+    ///         When the time specified passes, the callback will be invoked on the main UI thread.
+    ///     </para>
     /// </remarks>
-    public void Shutdown ();
+    object AddTimeout (TimeSpan time, Func<bool> callback);
+
+    /// <summary>Removes a previously scheduled timeout.</summary>
+    /// <param name="token">The token returned by <see cref="AddTimeout"/>.</param>
+    /// <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>
     ///     Handles recurring events. These are invoked on the main UI thread - allowing for
     ///     safe updates to <see cref="View"/> instances.
     /// </summary>
+    /// <remarks>
+    ///     <para>
+    ///         Provides low-level access to the timeout management system. Most applications should use
+    ///         <see cref="AddTimeout"/> and <see cref="RemoveTimeout"/> instead.
+    ///     </para>
+    /// </remarks>
     ITimedEvents? TimedEvents { get; }
+
+    #endregion Timeouts
 }

+ 1 - 1
Terminal.Gui/App/IterationEventArgs.cs

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

+ 2 - 2
Terminal.Gui/App/Keyboard/IKeyboard.cs

@@ -17,7 +17,7 @@ public interface IKeyboard
     IApplication? Application { get; set; }
 
     /// <summary>
-    ///     Called when the user presses a key (by the <see cref="IConsoleDriver"/>). Raises the cancelable
+    ///     Called when the user presses a key (by the <see cref="IDriver"/>). Raises the cancelable
     ///     <see cref="KeyDown"/> event, then calls <see cref="View.NewKeyDownEvent"/> on all top level views, and finally
     ///     if the key was not handled, invokes any Application-scoped <see cref="KeyBindings"/>.
     /// </summary>
@@ -27,7 +27,7 @@ public interface IKeyboard
     bool RaiseKeyDownEvent (Key key);
 
     /// <summary>
-    ///     Called when the user releases a key (by the <see cref="IConsoleDriver"/>). Raises the cancelable
+    ///     Called when the user releases a key (by the <see cref="IDriver"/>). Raises the cancelable
     ///     <see cref="KeyUp"/>
     ///     event
     ///     then calls <see cref="View.NewKeyUpEvent"/> on all top level views. Called after <see cref="RaiseKeyDownEvent"/>.

+ 4 - 1
Terminal.Gui/App/Keyboard/KeyboardImpl.cs

@@ -1,4 +1,6 @@
 #nullable enable
+using System.Diagnostics;
+
 namespace Terminal.Gui.App;
 
 /// <summary>
@@ -114,7 +116,8 @@ internal class KeyboardImpl : IKeyboard
     /// <inheritdoc/>
     public bool RaiseKeyDownEvent (Key key)
     {
-        Logging.Debug ($"{key}");
+        //ebug.Assert (App.Application.MainThreadId == Thread.CurrentThread.ManagedThreadId);
+        //Logging.Debug ($"{key}");
 
         // TODO: Add a way to ignore certain keys, esp for debugging.
         //#if DEBUG

+ 35 - 39
Terminal.Gui/App/MainLoop/ApplicationMainLoop.cs

@@ -1,7 +1,8 @@
 #nullable enable
-using Terminal.Gui.Drivers;
+using System;
 using System.Collections.Concurrent;
 using System.Diagnostics;
+using Terminal.Gui.Drivers;
 
 namespace Terminal.Gui.App;
 
@@ -19,15 +20,15 @@ namespace Terminal.Gui.App;
 ///         <item>Throttling iterations to respect <see cref="Application.MaximumIterationsPerSecond"/></item>
 ///     </list>
 /// </remarks>
-/// <typeparam name="T">Type of raw input events, e.g. <see cref="ConsoleKeyInfo"/> for .NET driver</typeparam>
-public class ApplicationMainLoop<T> : IApplicationMainLoop<T>
+/// <typeparam name="TInputRecord">Type of raw input events, e.g. <see cref="ConsoleKeyInfo"/> for .NET driver</typeparam>
+public class ApplicationMainLoop<TInputRecord> : IApplicationMainLoop<TInputRecord> where TInputRecord : struct
 {
     private ITimedEvents? _timedEvents;
-    private ConcurrentQueue<T>? _inputBuffer;
+    private ConcurrentQueue<TInputRecord>? _inputQueue;
     private IInputProcessor? _inputProcessor;
-    private IConsoleOutput? _out;
+    private IOutput? _output;
     private AnsiRequestScheduler? _ansiRequestScheduler;
-    private IConsoleSizeMonitor? _consoleSizeMonitor;
+    private ISizeMonitor? _sizeMonitor;
 
     /// <inheritdoc/>
     public ITimedEvents TimedEvents
@@ -40,13 +41,13 @@ public class ApplicationMainLoop<T> : IApplicationMainLoop<T>
 
     /// <summary>
     ///     The input events thread-safe collection. This is populated on separate
-    ///     thread by a <see cref="IConsoleInput{T}"/>. Is drained as part of each
-    ///     <see cref="Iteration"/>
+    ///     thread by a <see cref="IInput{T}"/>. Is drained as part of each
+    ///     <see cref="Iteration"/> on the main loop thread.
     /// </summary>
-    public ConcurrentQueue<T> InputBuffer
+    public ConcurrentQueue<TInputRecord> InputQueue
     {
-        get => _inputBuffer ?? throw new NotInitializedException (nameof (InputBuffer));
-        private set => _inputBuffer = value;
+        get => _inputQueue ?? throw new NotInitializedException (nameof (InputQueue));
+        private set => _inputQueue = value;
     }
 
     /// <inheritdoc/>
@@ -57,13 +58,13 @@ public class ApplicationMainLoop<T> : IApplicationMainLoop<T>
     }
 
     /// <inheritdoc/>
-    public IOutputBuffer OutputBuffer { get; } = new OutputBuffer ();
+    public IOutputBuffer OutputBuffer { get; } = new OutputBufferImpl ();
 
     /// <inheritdoc/>
-    public IConsoleOutput Out
+    public IOutput Output
     {
-        get => _out ?? throw new NotInitializedException (nameof (Out));
-        private set => _out = value;
+        get => _output ?? throw new NotInitializedException (nameof (Output));
+        private set => _output = value;
     }
 
     /// <inheritdoc/>
@@ -74,10 +75,10 @@ public class ApplicationMainLoop<T> : IApplicationMainLoop<T>
     }
 
     /// <inheritdoc/>
-    public IConsoleSizeMonitor ConsoleSizeMonitor
+    public ISizeMonitor SizeMonitor
     {
-        get => _consoleSizeMonitor ?? throw new NotInitializedException (nameof (ConsoleSizeMonitor));
-        private set => _consoleSizeMonitor = value;
+        get => _sizeMonitor ?? throw new NotInitializedException (nameof (SizeMonitor));
+        private set => _sizeMonitor = value;
     }
 
     /// <summary>
@@ -85,12 +86,6 @@ public class ApplicationMainLoop<T> : IApplicationMainLoop<T>
     /// </summary>
     public IToplevelTransitionManager ToplevelTransitionManager = new ToplevelTransitionManager ();
 
-    /// <summary>
-    ///     Determines how to get the current system type, adjust
-    ///     in unit tests to simulate specific timings.
-    /// </summary>
-    public Func<DateTime> Now { get; set; } = () => DateTime.Now;
-
     /// <summary>
     ///     Initializes the class with the provided subcomponents
     /// </summary>
@@ -101,34 +96,34 @@ public class ApplicationMainLoop<T> : IApplicationMainLoop<T>
     /// <param name="componentFactory"></param>
     public void Initialize (
         ITimedEvents timedEvents,
-        ConcurrentQueue<T> inputBuffer,
+        ConcurrentQueue<TInputRecord> inputBuffer,
         IInputProcessor inputProcessor,
-        IConsoleOutput consoleOutput,
-        IComponentFactory<T> componentFactory
+        IOutput consoleOutput,
+        IComponentFactory<TInputRecord> componentFactory
     )
     {
-        InputBuffer = inputBuffer;
-        Out = consoleOutput;
+        InputQueue = inputBuffer;
+        Output = consoleOutput;
         InputProcessor = inputProcessor;
 
         TimedEvents = timedEvents;
         AnsiRequestScheduler = new (InputProcessor.GetParser ());
 
-        ConsoleSizeMonitor = componentFactory.CreateConsoleSizeMonitor (Out, OutputBuffer);
+        OutputBuffer.SetSize (consoleOutput.GetSize ().Width, consoleOutput.GetSize ().Height);
+        SizeMonitor = componentFactory.CreateSizeMonitor (Output, OutputBuffer);
     }
 
     /// <inheritdoc/>
     public void Iteration ()
     {
-
         Application.RaiseIteration ();
 
-        DateTime dt = Now ();
+        DateTime dt = DateTime.Now;
         int timeAllowed = 1000 / Math.Max(1,(int)Application.MaximumIterationsPerSecond);
 
         IterationImpl ();
 
-        TimeSpan took = Now () - dt;
+        TimeSpan took = DateTime.Now - dt;
         TimeSpan sleepFor = TimeSpan.FromMilliseconds (timeAllowed) - took;
 
         Logging.TotalIterationMetric.Record (took.Milliseconds);
@@ -141,6 +136,7 @@ public class ApplicationMainLoop<T> : IApplicationMainLoop<T>
 
     internal void IterationImpl ()
     {
+        // Pull any input events from the input queue and process them
         InputProcessor.ProcessQueue ();
 
         ToplevelTransitionManager.RaiseReadyEventIfNeeded ();
@@ -152,7 +148,7 @@ public class ApplicationMainLoop<T> : IApplicationMainLoop<T>
                                      || AnySubViewsNeedDrawn (Application.Top)
                                      || (Application.Mouse.MouseGrabView != null && AnySubViewsNeedDrawn (Application.Mouse.MouseGrabView));
 
-            bool sizeChanged = ConsoleSizeMonitor.Poll ();
+            bool sizeChanged = SizeMonitor.Poll ();
 
             if (needsDrawOrLayout || sizeChanged)
             {
@@ -160,9 +156,9 @@ public class ApplicationMainLoop<T> : IApplicationMainLoop<T>
 
                 Application.LayoutAndDraw (true);
 
-                Out.Write (OutputBuffer);
+                Output.Write (OutputBuffer);
 
-                Out.SetCursorVisibility (CursorVisibility.Default);
+                Output.SetCursorVisibility (CursorVisibility.Default);
             }
 
             SetCursor ();
@@ -191,12 +187,12 @@ public class ApplicationMainLoop<T> : IApplicationMainLoop<T>
             // Translate to screen coordinates
             to = mostFocused.ViewportToScreen (to.Value);
 
-            Out.SetCursorPosition (to.Value.X, to.Value.Y);
-            Out.SetCursorVisibility (mostFocused.CursorVisibility);
+            Output.SetCursorPosition (to.Value.X, to.Value.Y);
+            Output.SetCursorVisibility (mostFocused.CursorVisibility);
         }
         else
         {
-            Out.SetCursorVisibility (CursorVisibility.Invisible);
+            Output.SetCursorVisibility (CursorVisibility.Invisible);
         }
     }
 

+ 54 - 19
Terminal.Gui/App/MainLoop/IApplicationMainLoop.cs

@@ -15,27 +15,27 @@ namespace Terminal.Gui.App;
 ///         <item>Rendering UI updates to the console</item>
 ///     </list>
 /// </remarks>
-/// <typeparam name="T">Type of raw input events processed by the loop, e.g. <see cref="ConsoleKeyInfo"/> for cross-platform .NET driver</typeparam>
-public interface IApplicationMainLoop<T> : IDisposable
+/// <typeparam name="TInputRecord">Type of raw input events processed by the loop, e.g. <see cref="ConsoleKeyInfo"/> for cross-platform .NET driver</typeparam>
+public interface IApplicationMainLoop<TInputRecord> : IDisposable where TInputRecord : struct
 {
     /// <summary>
-    ///     Gets the class responsible for servicing user timeouts
+    ///     Gets the <see cref="ITimedEvents"/> implementation that manages user-defined timeouts and periodic events.
     /// </summary>
     public ITimedEvents TimedEvents { get; }
 
     /// <summary>
-    ///     Gets the class responsible for writing final rendered output to the console
+    ///     Gets the <see cref="IOutputBuffer"/> representing the desired screen state for console rendering.
     /// </summary>
     public IOutputBuffer OutputBuffer { get; }
 
     /// <summary>
-    ///     Class for writing output to the console.
+    ///     Gets the <see cref="IOutput"/> implementation responsible for rendering the <see cref="OutputBuffer"/> to the console using platform specific methods.
     /// </summary>
-    public IConsoleOutput Out { get; }
+    public IOutput Output { get; }
 
     /// <summary>
-    ///     Gets the class responsible for processing buffered console input and translating
-    ///     it into events on the UI thread.
+    ///     Gets <see cref="InputProcessor"/> implementation that processes the mouse and keyboard input populated by <see cref="IInput{TInputRecord}"/>
+    ///     implementations on the input thread and translating to events on the UI thread.
     /// </summary>
     public IInputProcessor InputProcessor { get; }
 
@@ -46,24 +46,59 @@ public interface IApplicationMainLoop<T> : IDisposable
     public AnsiRequestScheduler AnsiRequestScheduler { get; }
 
     /// <summary>
-    ///     Gets the class responsible for determining the current console size
+    ///     Gets the <see cref="ISizeMonitor"/> implementation that tracks terminal size changes.
     /// </summary>
-    public IConsoleSizeMonitor ConsoleSizeMonitor { get; }
+    public ISizeMonitor SizeMonitor { get; }
 
     /// <summary>
-    ///     Initializes the loop with a buffer from which data can be read
+    ///     Initializes the main loop with its required dependencies.
     /// </summary>
-    /// <param name="timedEvents"></param>
-    /// <param name="inputBuffer"></param>
-    /// <param name="inputProcessor"></param>
-    /// <param name="consoleOutput"></param>
-    /// <param name="componentFactory"></param>
+    /// <param name="timedEvents">
+    ///     The <see cref="ITimedEvents"/> implementation for managing user-defined timeouts and periodic callbacks
+    ///     (e.g., <see cref="Application.AddTimeout"/>).
+    /// </param>
+    /// <param name="inputQueue">
+    ///     The thread-safe queue containing raw input events populated by <see cref="IInput{TInputRecord}"/> on
+    ///     the input thread. This queue is drained by <see cref="InputProcessor"/> during each <see cref="Iteration"/>.
+    /// </param>
+    /// <param name="inputProcessor">
+    ///     The <see cref="IInputProcessor"/> that translates raw input records (e.g., <see cref="ConsoleKeyInfo"/>) 
+    ///     into Terminal.Gui events (<see cref="Key"/>, <see cref="MouseEventArgs"/>) and raises them on the main UI thread.
+    /// </param>
+    /// <param name="output">
+    ///     The <see cref="IOutput"/> implementation responsible for rendering the <see cref="OutputBuffer"/> to the
+    ///     console using platform-specific methods (e.g., Win32 APIs, ANSI escape sequences).
+    /// </param>
+    /// <param name="componentFactory">
+    ///     The factory for creating driver-specific components. Used here to create the <see cref="ISizeMonitor"/>
+    ///     that tracks terminal size changes.
+    /// </param>
+    /// <remarks>
+    ///     <para>
+    ///         This method is called by <see cref="MainLoopCoordinator{TInputRecord}"/> during application startup
+    ///         to wire up all the components needed for the main loop to function. It must be called before
+    ///         <see cref="Iteration"/> can be invoked.
+    ///     </para>
+    ///     <para>
+    ///         <b>Initialization order:</b>
+    ///     </para>
+    ///     <list type="number">
+    ///         <item>Store references to <paramref name="timedEvents"/>, <paramref name="inputQueue"/>, 
+    ///               <paramref name="inputProcessor"/>, and <paramref name="output"/></item>
+    ///         <item>Create <see cref="AnsiRequestScheduler"/> for managing ANSI requests/responses</item>
+    ///         <item>Initialize <see cref="OutputBuffer"/> size to match current console dimensions</item>
+    ///         <item>Create <see cref="ISizeMonitor"/> using the <paramref name="componentFactory"/></item>
+    ///     </list>
+    ///     <para>
+    ///         After initialization, the main loop is ready to process events via <see cref="Iteration"/>.
+    ///     </para>
+    /// </remarks>
     void Initialize (
         ITimedEvents timedEvents,
-        ConcurrentQueue<T> inputBuffer,
+        ConcurrentQueue<TInputRecord> inputQueue,
         IInputProcessor inputProcessor,
-        IConsoleOutput consoleOutput,
-        IComponentFactory<T> componentFactory
+        IOutput output,
+        IComponentFactory<TInputRecord> componentFactory
     );
 
     /// <summary>

+ 1 - 2
Terminal.Gui/App/MainLoop/IMainLoopCoordinator.cs

@@ -8,7 +8,6 @@
 ///     <list type="bullet">
 ///         <item>Starting the asynchronous input reading thread</item>
 ///         <item>Initializing the main UI loop on the application thread</item>
-///         <item>Building the <see cref="IConsoleDriver"/> facade</item>
 ///         <item>Coordinating clean shutdown of both threads</item>
 ///     </list>
 /// </remarks>
@@ -26,7 +25,7 @@ public interface IMainLoopCoordinator
     ///     </list>
     /// </remarks>
     /// <returns>A task that completes when initialization is done</returns>
-    public Task StartAsync ();
+    public Task StartInputTaskAsync ();
 
     /// <summary>
     ///     Stops the input thread and performs cleanup.

+ 97 - 80
Terminal.Gui/App/MainLoop/MainLoopCoordinator.cs

@@ -1,6 +1,4 @@
 using System.Collections.Concurrent;
-using Terminal.Gui.Drivers;
-using Microsoft.Extensions.Logging;
 
 namespace Terminal.Gui.App;
 
@@ -15,50 +13,52 @@ namespace Terminal.Gui.App;
 ///     </para>
 ///     <para>This class is designed to be managed by <see cref="ApplicationImpl"/></para>
 /// </summary>
-/// <typeparam name="T">Type of raw input events, e.g. <see cref="ConsoleKeyInfo"/> for .NET driver</typeparam>
-internal class MainLoopCoordinator<T> : IMainLoopCoordinator
+/// <typeparam name="TInputRecord">Type of raw input events, e.g. <see cref="ConsoleKeyInfo"/> for .NET driver</typeparam>
+internal class MainLoopCoordinator<TInputRecord> : IMainLoopCoordinator where TInputRecord : struct
 {
-    private readonly ConcurrentQueue<T> _inputBuffer;
-    private readonly IInputProcessor _inputProcessor;
-    private readonly IApplicationMainLoop<T> _loop;
-    private readonly IComponentFactory<T> _componentFactory;
-    private readonly CancellationTokenSource _tokenSource = new ();
-    private IConsoleInput<T> _input;
-    private IConsoleOutput _output;
-    private readonly object _oLockInitialization = new ();
-    private ConsoleDriverFacade<T> _facade;
-    private Task _inputTask;
-    private readonly ITimedEvents _timedEvents;
-
-    private readonly SemaphoreSlim _startupSemaphore = new (0, 1);
-
     /// <summary>
     ///     Creates a new coordinator that will manage the main UI loop and input thread.
     /// </summary>
     /// <param name="timedEvents">Handles scheduling and execution of user timeout callbacks</param>
-    /// <param name="inputBuffer">Thread-safe queue for buffering raw console input</param>
+    /// <param name="inputQueue">Thread-safe queue for buffering raw console input</param>
     /// <param name="loop">The main application loop instance</param>
     /// <param name="componentFactory">Factory for creating driver-specific components (input, output, etc.)</param>
     public MainLoopCoordinator (
         ITimedEvents timedEvents,
-        ConcurrentQueue<T> inputBuffer,
-        IApplicationMainLoop<T> loop,
-        IComponentFactory<T> componentFactory
+        ConcurrentQueue<TInputRecord> inputQueue,
+        IApplicationMainLoop<TInputRecord> loop,
+        IComponentFactory<TInputRecord> componentFactory
     )
     {
         _timedEvents = timedEvents;
-        _inputBuffer = inputBuffer;
-        _inputProcessor = componentFactory.CreateInputProcessor (_inputBuffer);
+        _inputQueue = inputQueue;
+        _inputProcessor = componentFactory.CreateInputProcessor (_inputQueue);
         _loop = loop;
         _componentFactory = componentFactory;
     }
 
+    private readonly IApplicationMainLoop<TInputRecord> _loop;
+    private readonly IComponentFactory<TInputRecord> _componentFactory;
+    private readonly CancellationTokenSource _runCancellationTokenSource = new ();
+    private readonly ConcurrentQueue<TInputRecord> _inputQueue;
+    private readonly IInputProcessor _inputProcessor;
+    private readonly object _oLockInitialization = new ();
+    private readonly ITimedEvents _timedEvents;
+
+    private readonly SemaphoreSlim _startupSemaphore = new (0, 1);
+    private IInput<TInputRecord> _input;
+    private Task _inputTask;
+    private IOutput _output;
+    private DriverImpl _driver;
+
+    private bool _stopCalled;
+
     /// <summary>
     ///     Starts the input loop thread in separate task (returning immediately).
     /// </summary>
-    public async Task StartAsync ()
+    public async Task StartInputTaskAsync ()
     {
-        Logging.Logger.LogInformation ("Main Loop Coordinator booting...");
+        Logging.Trace ("Booting... ()");
 
         _inputTask = Task.Run (RunInput);
 
@@ -80,100 +80,117 @@ internal class MainLoopCoordinator<T> : IMainLoopCoordinator
                 throw _inputTask.Exception;
             }
 
-            Logging.Logger.LogCritical("Input loop exited during startup instead of entering read loop properly (i.e. and blocking)");
+            Logging.Critical ("Input loop exited during startup instead of entering read loop properly (i.e. and blocking)");
         }
 
-        Logging.Logger.LogInformation ("Main Loop Coordinator booting complete");
+        Logging.Trace ("Booting complete");
     }
 
-    private void RunInput ()
+    /// <inheritdoc/>
+    public void RunIteration ()
     {
-        try
-        {
-            lock (_oLockInitialization)
-            {
-                // Instance must be constructed on the thread in which it is used.
-                _input = _componentFactory.CreateInput ();
-                _input.Initialize (_inputBuffer);
-
-                BuildFacadeIfPossible ();
-            }
-
-            try
-            {
-                _input.Run (_tokenSource.Token);
-            }
-            catch (OperationCanceledException)
-            { }
-
-            _input.Dispose ();
-        }
-        catch (Exception e)
+        lock (_oLockInitialization)
         {
-            Logging.Logger.LogCritical (e, "Input loop crashed");
-
-            throw;
+            _loop.Iteration ();
         }
+    }
 
+    /// <inheritdoc/>
+    public void Stop ()
+    {
+        // Ignore repeated calls to Stop - happens if user spams Application.Shutdown().
         if (_stopCalled)
         {
-            Logging.Logger.LogInformation ("Input loop exited cleanly");
-        }
-        else
-        {
-            Logging.Logger.LogCritical ("Input loop exited early (stop not called)");
+            return;
         }
-    }
 
-    /// <inheritdoc/>
-    public void RunIteration () { _loop.Iteration (); }
+        _stopCalled = true;
+
+        _runCancellationTokenSource.Cancel ();
+        _output.Dispose ();
+
+        // Wait for input infinite loop to exit
+        _inputTask.Wait ();
+    }
 
     private void BootMainLoop ()
     {
+        //Logging.Trace ($"_inputProcessor: {_inputProcessor}, _output: {_output}, _componentFactory: {_componentFactory}");
+
         lock (_oLockInitialization)
         {
             // Instance must be constructed on the thread in which it is used.
             _output = _componentFactory.CreateOutput ();
-            _loop.Initialize (_timedEvents, _inputBuffer, _inputProcessor, _output,_componentFactory);
+            _loop.Initialize (_timedEvents, _inputQueue, _inputProcessor, _output, _componentFactory);
 
-            BuildFacadeIfPossible ();
+            BuildDriverIfPossible ();
         }
     }
 
-    private void BuildFacadeIfPossible ()
+    private void BuildDriverIfPossible ()
     {
+
         if (_input != null && _output != null)
         {
-            _facade = new (
+            _driver = new (
                            _inputProcessor,
                            _loop.OutputBuffer,
                            _output,
                            _loop.AnsiRequestScheduler,
-                           _loop.ConsoleSizeMonitor);
+                           _loop.SizeMonitor);
 
-            Application.Driver = _facade;
+            Application.Driver = _driver;
 
             _startupSemaphore.Release ();
+            Logging.Trace ($"Driver: _input: {_input}, _output: {_output}");
         }
     }
 
-    private bool _stopCalled;
-
-    /// <inheritdoc/>
-    public void Stop ()
+    /// <summary>
+    ///     INTERNAL: Runs the IInput read loop on a new thread called the "Input Thread".
+    /// </summary>
+    private void RunInput ()
     {
-        // Ignore repeated calls to Stop - happens if user spams Application.Shutdown().
-        if (_stopCalled)
+        try
         {
-            return;
-        }
+            lock (_oLockInitialization)
+            {
+                // Instance must be constructed on the thread in which it is used.
+                _input = _componentFactory.CreateInput ();
+                _input.Initialize (_inputQueue);
 
-        _stopCalled = true;
+                // Wire up InputImpl reference for ITestableInput support
+                if (_inputProcessor is InputProcessorImpl<TInputRecord> impl)
+                {
+                    impl.InputImpl = _input;
+                }
 
-        _tokenSource.Cancel ();
-        _output.Dispose ();
+                BuildDriverIfPossible ();
+            }
 
-        // Wait for input infinite loop to exit
-        _inputTask.Wait ();
+            try
+            {
+                _input.Run (_runCancellationTokenSource.Token);
+            }
+            catch (OperationCanceledException)
+            { }
+
+            _input.Dispose ();
+        }
+        catch (Exception e)
+        {
+            Logging.Critical ($"Input loop crashed: {e}");
+
+            throw;
+        }
+
+        if (_stopCalled)
+        {
+            Logging.Information ("Input loop exited cleanly");
+        }
+        else
+        {
+            Logging.Critical ("Input loop exited early (stop not called)");
+        }
     }
 }

+ 4 - 2
Terminal.Gui/App/Mouse/MouseImpl.cs

@@ -1,5 +1,6 @@
 #nullable enable
 using System.ComponentModel;
+using System.Diagnostics;
 
 namespace Terminal.Gui.App;
 
@@ -55,6 +56,7 @@ internal class MouseImpl : IMouse
     /// <inheritdoc/>
     public void RaiseMouseEvent (MouseEventArgs mouseEvent)
     {
+        //Debug.Assert (App.Application.MainThreadId == Thread.CurrentThread.ManagedThreadId);
         if (Application?.Initialized is true)
         {
             // LastMousePosition is only set if the application is initialized.
@@ -371,11 +373,11 @@ internal class MouseImpl : IMouse
                 Position = frameLoc,
                 Flags = mouseEvent.Flags,
                 ScreenPosition = mouseEvent.ScreenPosition,
-                View = deepestViewUnderMouse ?? MouseGrabView
+                View = MouseGrabView // Always set to the grab view. See Issue #4370
             };
 
             //System.Diagnostics.Debug.WriteLine ($"{nme.Flags};{nme.X};{nme.Y};{mouseGrabView}");
-            if (MouseGrabView?.NewMouseEvent (viewRelativeMouseEvent) is true)
+            if (MouseGrabView?.NewMouseEvent (viewRelativeMouseEvent) is true || viewRelativeMouseEvent.IsSingleClicked)
             {
                 return true;
             }

+ 0 - 12
Terminal.Gui/App/RunStateEventArgs.cs

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

+ 19 - 19
Terminal.Gui/App/RunState.cs → Terminal.Gui/App/SessionToken.cs

@@ -2,22 +2,22 @@
 
 namespace Terminal.Gui.App;
 
-/// <summary>The execution state for a <see cref="Toplevel"/> view.</summary>
-public class RunState : IDisposable
+/// <summary>Defines a session token for a running <see cref="Toplevel"/>.</summary>
+public class SessionToken : IDisposable
 {
-    /// <summary>Initializes a new <see cref="RunState"/> class.</summary>
+    /// <summary>Initializes a new <see cref="SessionToken"/> class.</summary>
     /// <param name="view"></param>
-    public RunState (Toplevel view) { Toplevel = view; }
+    public SessionToken (Toplevel view) { Toplevel = view; }
 
-    /// <summary>The <see cref="Toplevel"/> belonging to this <see cref="RunState"/>.</summary>
+    /// <summary>The <see cref="Toplevel"/> belonging to this <see cref="SessionToken"/>.</summary>
     public Toplevel Toplevel { get; internal set; }
 
-    /// <summary>Releases all resource used by the <see cref="RunState"/> object.</summary>
-    /// <remarks>Call <see cref="Dispose()"/> when you are finished using the <see cref="RunState"/>.</remarks>
+    /// <summary>Releases all resource used by the <see cref="SessionToken"/> object.</summary>
+    /// <remarks>Call <see cref="Dispose()"/> when you are finished using the <see cref="SessionToken"/>.</remarks>
     /// <remarks>
-    ///     <see cref="Dispose()"/> method leaves the <see cref="RunState"/> in an unusable state. After calling
-    ///     <see cref="Dispose()"/>, you must release all references to the <see cref="RunState"/> so the garbage collector can
-    ///     reclaim the memory that the <see cref="RunState"/> was occupying.
+    ///     <see cref="Dispose()"/> method leaves the <see cref="SessionToken"/> in an unusable state. After calling
+    ///     <see cref="Dispose()"/>, you must release all references to the <see cref="SessionToken"/> so the garbage collector can
+    ///     reclaim the memory that the <see cref="SessionToken"/> was occupying.
     /// </remarks>
     public void Dispose ()
     {
@@ -28,7 +28,7 @@ public class RunState : IDisposable
 #endif
     }
 
-    /// <summary>Releases all resource used by the <see cref="RunState"/> object.</summary>
+    /// <summary>Releases all resource used by the <see cref="SessionToken"/> object.</summary>
     /// <param name="disposing">If set to <see langword="true"/> we are disposing and should dispose held objects.</param>
     protected virtual void Dispose (bool disposing)
     {
@@ -38,7 +38,7 @@ public class RunState : IDisposable
             // But that is not correct becaue `Begin` didn't create the TopLevel, `Init` did; thus
             // disposing should be done by `Shutdown`, not `End`.
             throw new InvalidOperationException (
-                                                 "Toplevel must be null before calling Application.RunState.Dispose"
+                                                 "Toplevel must be null before calling Application.SessionToken.Dispose"
                                                 );
         }
     }
@@ -46,29 +46,29 @@ public class RunState : IDisposable
 #if DEBUG_IDISPOSABLE
 #pragma warning disable CS0419 // Ambiguous reference in cref attribute
     /// <summary>
-    ///     Gets whether <see cref="RunState.Dispose"/> was called on this RunState or not.
+    ///     Gets whether <see cref="SessionToken.Dispose"/> was called on this SessionToken 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="RunState.Dispose"/> was called on this object.
+    ///     Gets the number of times <see cref="SessionToken.Dispose"/> was called on this object.
     ///     For debug purposes to verify objects are being disposed properly.
     ///     Only valid when DEBUG_IDISPOSABLE is defined.
     /// </summary>
     public int DisposedCount { get; private set; } = 0;
 
     /// <summary>
-    ///     Gets the list of RunState objects that have been created and not yet disposed.
-    ///     Note, this is a static property and will affect all RunState objects.
+    ///     Gets the list of SessionToken objects that have been created and not yet disposed.
+    ///     Note, this is a static property and will affect all SessionToken objects.
     ///     For debug purposes to verify objects are being disposed properly.
     ///     Only valid when DEBUG_IDISPOSABLE is defined.
     /// </summary>
-    public static ConcurrentBag<RunState> Instances { get; private set; } = [];
+    public static ConcurrentBag<SessionToken> Instances { get; private set; } = [];
 
-    /// <summary>Creates a new RunState object.</summary>
-    public RunState ()
+    /// <summary>Creates a new SessionToken object.</summary>
+    public SessionToken ()
     {
         Instances.Add (this);
     }

+ 12 - 0
Terminal.Gui/App/SessionTokenEventArgs.cs

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

+ 1 - 1
Terminal.Gui/App/Timeout/ITimedEvents.cs

@@ -23,7 +23,7 @@ public interface ITimedEvents
 
     /// <summary>
     ///     Invoked when a new timeout is added. To be used in the case when
-    ///     <see cref="Application.EndAfterFirstIteration"/> is <see langword="true"/>.
+    ///     <see cref="IApplication.StopAfterFirstIteration"/> is <see langword="true"/>.
     /// </summary>
     event EventHandler<TimeoutEventArgs>? Added;
 

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

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

+ 2 - 2
Terminal.Gui/Drawing/Glyphs.cs

@@ -57,11 +57,11 @@ public class Glyphs
     [ConfigurationProperty (Scope = typeof (ThemeScope))]
     public static Rune CheckStateNone { get; set; } = (Rune)'□'; // TODO: Verify this works as broadly as possible
 
-    /// <summary>Selected indicator  (e.g. for <see cref="ListView"/> and <see cref="RadioGroup"/>).</summary>
+    /// <summary>Selected indicator  (e.g. for <see cref="ListView"/> and <see cref="OptionSelector"/>).</summary>
     [ConfigurationProperty (Scope = typeof (ThemeScope))]
     public static Rune Selected { get; set; } = (Rune)'◉';
 
-    /// <summary>Not Selected indicator (e.g. for <see cref="ListView"/> and <see cref="RadioGroup"/>).</summary>
+    /// <summary>Not Selected indicator (e.g. for <see cref="ListView"/> and <see cref="OptionSelector"/>).</summary>
     [ConfigurationProperty (Scope = typeof (ThemeScope))]
     public static Rune UnSelected { get; set; } = (Rune)'○';
 

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

@@ -402,7 +402,7 @@ public class LineCanvas : IDisposable
         // TODO: Add other resolvers
     };
 
-    private Cell? GetCellForIntersects (IConsoleDriver? driver, ReadOnlySpan<IntersectionDefinition> intersects)
+    private Cell? GetCellForIntersects (IDriver? driver, ReadOnlySpan<IntersectionDefinition> intersects)
     {
         if (intersects.IsEmpty)
         {
@@ -422,7 +422,7 @@ public class LineCanvas : IDisposable
         return cell;
     }
 
-    private Rune? GetRuneForIntersects (IConsoleDriver? driver, ReadOnlySpan<IntersectionDefinition> intersects)
+    private Rune? GetRuneForIntersects (IDriver? driver, ReadOnlySpan<IntersectionDefinition> intersects)
     {
         if (intersects.IsEmpty)
         {
@@ -769,7 +769,7 @@ public class LineCanvas : IDisposable
         internal Rune _thickV;
         protected IntersectionRuneResolver () { SetGlyphs (); }
 
-        public Rune? GetRuneForIntersects (IConsoleDriver? driver, ReadOnlySpan<IntersectionDefinition> intersects)
+        public Rune? GetRuneForIntersects (IDriver? driver, ReadOnlySpan<IntersectionDefinition> intersects)
         {
             // Note that there aren't any glyphs for intersections of double lines with heavy lines
 

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

@@ -24,7 +24,7 @@ internal class Ruler
     /// <param name="location">The location to start drawing the ruler, in screen-relative coordinates.</param>
     /// <param name="start">The start value of the ruler.</param>
     /// <param name="driver">Optional Driver. If not provided, driver will be used.</param>
-    public void Draw (Point location, int start = 0, IConsoleDriver? driver = null)
+    public void Draw (Point location, int start = 0, IDriver? driver = null)
     {
         if (start < 0)
         {

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

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

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

@@ -90,7 +90,7 @@ public record struct Thickness
     /// <param name="label">The diagnostics label to draw on the bottom of the <see cref="Bottom"/>.</param>
     /// <param name="driver">Optional driver. If not specified, <see cref="Application.Driver"/> will be used.</param>
     /// <returns>The inner rectangle remaining to be drawn.</returns>
-    public Rectangle Draw (Rectangle rect, ViewDiagnosticFlags diagnosticFlags = ViewDiagnosticFlags.Off, string? label = null, IConsoleDriver? driver = null)
+    public Rectangle Draw (Rectangle rect, ViewDiagnosticFlags diagnosticFlags = ViewDiagnosticFlags.Off, string? label = null, IDriver? driver = null)
     {
         if (rect.Size.Width < 1 || rect.Size.Height < 1)
         {

+ 1 - 1
Terminal.Gui/Drivers/AnsiHandling/AnsiEscapeSequenceRequest.cs

@@ -22,7 +22,7 @@ public class AnsiEscapeSequenceRequest : AnsiEscapeSequence
 
 
     /// <summary>
-    ///     Sends the <see cref="AnsiEscapeSequence.Request"/> to the raw output stream of the current <see cref="ConsoleDriver"/>.
+    ///     Sends the <see cref="AnsiEscapeSequence.Request"/> to the raw output stream of the current <see cref="IDriver"/>.
     ///     Only call this method from the main UI thread. You should use <see cref="AnsiRequestScheduler"/> if
     ///     sending many requests.
     /// </summary>

+ 16 - 17
Terminal.Gui/Drivers/AnsiHandling/AnsiResponseParser.cs

@@ -141,6 +141,7 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser
 
             bool isEscape = currentChar == ESCAPE;
 
+            // Logging.Trace($"Processing character '{currentChar}' (isEscape: {isEscape})");
             switch (State)
             {
                 case AnsiResponseParserState.Normal:
@@ -261,7 +262,7 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser
                 {
                     _heldContent.ClearHeld ();
 
-                    Logging.Trace ($"AnsiResponseParser last minute swallowed '{cur}'");
+                    //Logging.Trace ($"AnsiResponseParser last minute swallowed '{cur}'");
                 }
             }
         }
@@ -342,7 +343,7 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser
                 {
                     _heldContent.ClearHeld ();
 
-                    Logging.Trace ($"AnsiResponseParser swallowed '{cur}'");
+                    //Logging.Trace ($"AnsiResponseParser swallowed '{cur}'");
 
                     // Do not send back to input stream
                     return false;
@@ -403,7 +404,7 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser
 
         if (matchingResponse?.Response != null)
         {
-            Logging.Trace ($"AnsiResponseParser processed '{cur}'");
+            //Logging.Trace ($"AnsiResponseParser processed '{cur}'");
 
             if (invokeCallback)
             {
@@ -479,16 +480,14 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser
     }
 }
 
-internal class AnsiResponseParser<T> : AnsiResponseParserBase
+internal class AnsiResponseParser<TInputRecord> () : AnsiResponseParserBase (new GenericHeld<TInputRecord> ())
 {
-    public AnsiResponseParser () : base (new GenericHeld<T> ()) { }
-
     /// <inheritdoc cref="AnsiResponseParser.UnknownResponseHandler"/>
-    public Func<IEnumerable<Tuple<char, T>>, bool> UnexpectedResponseHandler { get; set; } = _ => false;
+    public Func<IEnumerable<Tuple<char, TInputRecord>>, bool> UnexpectedResponseHandler { get; set; } = _ => false;
 
-    public IEnumerable<Tuple<char, T>> ProcessInput (params Tuple<char, T> [] input)
+    public IEnumerable<Tuple<char, TInputRecord>> ProcessInput (params Tuple<char, TInputRecord> [] input)
     {
-        List<Tuple<char, T>> output = new ();
+        List<Tuple<char, TInputRecord>> output = [];
 
         ProcessInputBase (
                           i => input [i].Item1,
@@ -499,22 +498,22 @@ internal class AnsiResponseParser<T> : AnsiResponseParserBase
         return output;
     }
 
-    private void AppendOutput (List<Tuple<char, T>> output, object c)
+    private void AppendOutput (List<Tuple<char, TInputRecord>> output, object c)
     {
-        Tuple<char, T> tuple = (Tuple<char, T>)c;
+        Tuple<char, TInputRecord> tuple = (Tuple<char, TInputRecord>)c;
 
-        Logging.Trace ($"AnsiResponseParser releasing '{tuple.Item1}'");
+        //Logging.Trace ($"AnsiResponseParser releasing '{tuple.Item1}'");
         output.Add (tuple);
     }
 
-    public Tuple<char, T> [] Release ()
+    public Tuple<char, TInputRecord> [] Release ()
     {
         // Lock in case Release is called from different Thread from parse
         lock (_lockState)
         {
             TryLastMinuteSequences ();
 
-            Tuple<char, T> [] result = HeldToEnumerable ().ToArray ();
+            Tuple<char, TInputRecord> [] result = HeldToEnumerable ().ToArray ();
 
             ResetState ();
 
@@ -522,7 +521,7 @@ internal class AnsiResponseParser<T> : AnsiResponseParserBase
         }
     }
 
-    private IEnumerable<Tuple<char, T>> HeldToEnumerable () { return (IEnumerable<Tuple<char, T>>)_heldContent.HeldToObjects (); }
+    private IEnumerable<Tuple<char, TInputRecord>> HeldToEnumerable () { return (IEnumerable<Tuple<char, TInputRecord>>)_heldContent.HeldToObjects (); }
 
     /// <summary>
     ///     'Overload' for specifying an expectation that requires the metadata as well as characters. Has
@@ -532,7 +531,7 @@ internal class AnsiResponseParser<T> : AnsiResponseParserBase
     /// <param name="response"></param>
     /// <param name="abandoned"></param>
     /// <param name="persistent"></param>
-    public void ExpectResponseT (string? terminator, Action<IEnumerable<Tuple<char, T>>> response, Action? abandoned, bool persistent)
+    public void ExpectResponseT (string? terminator, Action<IEnumerable<Tuple<char, TInputRecord>>> response, Action? abandoned, bool persistent)
     {
         lock (_lockExpectedResponses)
         {
@@ -581,7 +580,7 @@ internal class AnsiResponseParser () : AnsiResponseParserBase (new StringHeld ()
 
     private void AppendOutput (StringBuilder output, char c)
     {
-        Logging.Trace ($"AnsiResponseParser releasing '{c}'");
+       // Logging.Trace ($"AnsiResponseParser releasing '{c}'");
         output.Append (c);
     }
 

+ 4 - 4
Terminal.Gui/Drivers/AnsiHandling/EscSeqUtils/EscSeqUtils.cs

@@ -295,11 +295,11 @@ public static class EscSeqUtils
 
                 break;
             default:
-                uint ck = ConsoleKeyMapping.MapKeyCodeToConsoleKey ((KeyCode)consoleKeyInfo.KeyChar, out bool isConsoleKey);
+                //uint ck = ConsoleKeyMapping.MapKeyCodeToConsoleKey ((KeyCode)consoleKeyInfo.KeyChar, out bool isConsoleKey);
 
-                if (isConsoleKey)
-                {
-                    key = (ConsoleKey)ck;
+                //if (isConsoleKey)
+            {
+                key = consoleKeyInfo.Key;// (ConsoleKey)ck;
                 }
 
                 newConsoleKeyInfo = new (

+ 5 - 5
Terminal.Gui/Drivers/AnsiHandling/GenericHeld.cs

@@ -2,12 +2,12 @@
 namespace Terminal.Gui.Drivers;
 
 /// <summary>
-///     Implementation of <see cref="IHeld"/> for <see cref="AnsiResponseParser{T}"/>
+///     Implementation of <see cref="IHeld"/> for <see cref="AnsiResponseParser{TInputRecord}"/>
 /// </summary>
-/// <typeparam name="T"></typeparam>
-internal class GenericHeld<T> : IHeld
+/// <typeparam name="TInputRecord"></typeparam>
+internal class GenericHeld<TInputRecord> : IHeld
 {
-    private readonly List<Tuple<char, T>> held = new ();
+    private readonly List<Tuple<char, TInputRecord>> held = [];
 
     public void ClearHeld () { held.Clear (); }
 
@@ -15,7 +15,7 @@ internal class GenericHeld<T> : IHeld
 
     public IEnumerable<object> HeldToObjects () { return held; }
 
-    public void AddToHeld (object o) { held.Add ((Tuple<char, T>)o); }
+    public void AddToHeld (object o) { held.Add ((Tuple<char, TInputRecord>)o); }
 
     /// <inheritdoc/>
     public int Length => held.Count;

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

@@ -1,26 +0,0 @@
-#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 IConsoleSizeMonitor CreateConsoleSizeMonitor (IConsoleOutput consoleOutput, IOutputBuffer outputBuffer)
-    {
-        return new ConsoleSizeMonitor (consoleOutput, outputBuffer);
-    }
-
-    /// <inheritdoc />
-    public abstract IConsoleOutput CreateOutput ();
-}

+ 25 - 0
Terminal.Gui/Drivers/ComponentFactoryImpl.cs

@@ -0,0 +1,25 @@
+#nullable enable
+using System.Collections.Concurrent;
+
+namespace Terminal.Gui.Drivers;
+
+/// <summary>
+/// Abstract base class implementation of <see cref="IComponentFactory{TInputRecord}"/> that provides a default implementation of <see cref="CreateSizeMonitor"/>.</summary>
+/// <typeparam name="TInputRecord">The platform specific keyboard input type (e.g. <see cref="ConsoleKeyInfo"/> or <see cref="WindowsConsole.InputRecord"/></typeparam>
+public abstract class ComponentFactoryImpl<TInputRecord> : IComponentFactory<TInputRecord> where TInputRecord : struct
+{
+    /// <inheritdoc />
+    public abstract IInput<TInputRecord> CreateInput ();
+
+    /// <inheritdoc />
+    public abstract IInputProcessor CreateInputProcessor (ConcurrentQueue<TInputRecord> inputBuffer);
+
+    /// <inheritdoc />
+    public virtual ISizeMonitor CreateSizeMonitor (IOutput consoleOutput, IOutputBuffer outputBuffer)
+    {
+        return new SizeMonitorImpl (consoleOutput);
+    }
+
+    /// <inheritdoc />
+    public abstract IOutput CreateOutput ();
+}

+ 0 - 753
Terminal.Gui/Drivers/ConsoleDriver.cs

@@ -1,753 +0,0 @@
-#nullable enable
-
-using System.Diagnostics;
-
-namespace Terminal.Gui.Drivers;
-
-/// <summary>Base class for Terminal.Gui IConsoleDriver implementations.</summary>
-/// <remarks>
-///     There are currently four implementations:
-/// - DotNetDriver that uses the .NET Console API and works on all platforms
-/// - UnixDriver optimized for Unix and Mac.
-/// - WindowsDriver optimized for Windows.
-/// - FakeDriver for unit testing.
-/// </remarks>
-public abstract class ConsoleDriver : IConsoleDriver
-{
-    /// <summary>
-    ///     Set this to true in any unit tests that attempt to test drivers other than FakeDriver.
-    ///     <code>
-    ///  public ColorTests ()
-    ///  {
-    ///    ConsoleDriver.RunningUnitTests = true;
-    ///  }
-    /// </code>
-    /// </summary>
-    internal static bool RunningUnitTests { get; set; }
-
-    /// <summary>Get the operating system clipboard.</summary>
-    public IClipboard? Clipboard { get; internal set; }
-
-    /// <summary>Returns the name of the driver and relevant library version information.</summary>
-    /// <returns></returns>
-    public virtual string GetVersionInfo () { return GetType ().Name; }
-
-    #region ANSI Esc Sequence Handling
-
-    // QUESTION: This appears to be an API to help in debugging. It's only implemented in UnixDriver and WindowsDriver.
-    // QUESTION: Can it be factored such that it does not contaminate the ConsoleDriver API?
-    /// <summary>
-    ///     Provide proper writing to send escape sequence recognized by the <see cref="ConsoleDriver"/>.
-    /// </summary>
-    /// <param name="ansi"></param>
-    public abstract void WriteRaw (string ansi);
-
-    #endregion ANSI Esc Sequence Handling
-
-    #region Screen and Contents
-
-
-    /// <summary>
-    /// How long after Esc has been pressed before we give up on getting an Ansi escape sequence
-    /// </summary>
-    public TimeSpan EscTimeout { get; } = TimeSpan.FromMilliseconds (50);
-
-    // As performance is a concern, we keep track of the dirty lines and only refresh those.
-    // This is in addition to the dirty flag on each cell.
-    internal bool []? _dirtyLines;
-
-    // QUESTION: When non-full screen apps are supported, will this represent the app size, or will that be in Application?
-    /// <summary>Gets the location and size of the terminal screen.</summary>
-    public Rectangle Screen => new (0, 0, Cols, Rows);
-
-    /// <summary>
-    /// Sets the screen size for testing purposes. Only supported by FakeDriver.
-    /// <see cref="Screen"/> is the source of truth for screen dimensions.
-    /// <see cref="Cols"/> and <see cref="Rows"/> are read-only and derived from <see cref="Screen"/>.
-    /// </summary>
-    /// <param name="width">The new width in columns.</param>
-    /// <param name="height">The new height in rows.</param>
-    /// <exception cref="NotSupportedException">Thrown when called on non-FakeDriver instances.</exception>
-    public virtual void SetScreenSize (int width, int height)
-    {
-        throw new NotSupportedException ("SetScreenSize is only supported by FakeDriver for test scenarios.");
-    }
-
-    private Region? _clip;
-
-    /// <summary>
-    ///     Gets or sets the clip rectangle that <see cref="AddRune(Rune)"/> and <see cref="AddStr(string)"/> are subject
-    ///     to.
-    /// </summary>
-    /// <value>The rectangle describing the of <see cref="Clip"/> region.</value>
-    public Region? Clip
-    {
-        get => _clip;
-        set
-        {
-            if (_clip == value)
-            {
-                return;
-            }
-
-            _clip = value;
-
-            // Don't ever let Clip be bigger than Screen
-            if (_clip is { })
-            {
-                _clip.Intersect (Screen);
-            }
-        }
-    }
-
-    /// <summary>
-    ///     Gets the column last set by <see cref="Move"/>. <see cref="Col"/> and <see cref="Row"/> are used by
-    ///     <see cref="AddRune(Rune)"/> and <see cref="AddStr"/> to determine where to add content.
-    /// </summary>
-    public int Col { get; private set; }
-
-    /// <summary>The number of columns visible in the terminal.</summary>
-    public virtual int Cols
-    {
-        get => _cols;
-        set
-        {
-            _cols = value;
-            ClearContents ();
-        }
-    }
-
-    /// <summary>
-    ///     The contents of the application output. The driver outputs this buffer to the terminal when
-    ///     <see cref="UpdateScreen"/> is called.
-    ///     <remarks>The format of the array is rows, columns. The first index is the row, the second index is the column.</remarks>
-    /// </summary>
-    public Cell [,]? Contents { get; set; }
-
-    /// <summary>The leftmost column in the terminal.</summary>
-    public virtual int Left { get; set; } = 0;
-
-    /// <summary>Tests if the specified rune is supported by the driver.</summary>
-    /// <param name="rune"></param>
-    /// <returns>
-    ///     <see langword="true"/> if the rune can be properly presented; <see langword="false"/> if the driver does not
-    ///     support displaying this rune.
-    /// </returns>
-    public virtual bool IsRuneSupported (Rune rune) { return Rune.IsValid (rune.Value); }
-
-    /// <summary>Tests whether the specified coordinate are valid for drawing.</summary>
-    /// <param name="col">The column.</param>
-    /// <param name="row">The row.</param>
-    /// <returns>
-    ///     <see langword="false"/> if the coordinate is outside the screen bounds or outside of <see cref="Clip"/>.
-    ///     <see langword="true"/> otherwise.
-    /// </returns>
-    public bool IsValidLocation (int col, int row) { return col >= 0 && row >= 0 && col < Cols && row < Rows && Clip!.Contains (col, row); }
-
-    /// <summary>
-    ///     Updates <see cref="Col"/> and <see cref="Row"/> to the specified column and row in <see cref="Contents"/>.
-    ///     Used by <see cref="AddRune(Rune)"/> and <see cref="AddStr"/> to determine where to add content.
-    /// </summary>
-    /// <remarks>
-    ///     <para>This does not move the cursor on the screen, it only updates the internal state of the driver.</para>
-    ///     <para>
-    ///         If <paramref name="col"/> or <paramref name="row"/> are negative or beyond  <see cref="Cols"/> and
-    ///         <see cref="Rows"/>, the method still sets those properties.
-    ///     </para>
-    /// </remarks>
-    /// <param name="col">Column to move to.</param>
-    /// <param name="row">Row to move to.</param>
-    public virtual void Move (int col, int row)
-    {
-        //Debug.Assert (col >= 0 && row >= 0 && col < Contents.GetLength(1) && row < Contents.GetLength(0));
-        Col = col;
-        Row = row;
-    }
-
-    /// <summary>
-    ///     Gets the row last set by <see cref="Move"/>. <see cref="Col"/> and <see cref="Row"/> are used by
-    ///     <see cref="AddRune(Rune)"/> and <see cref="AddStr"/> to determine where to add content.
-    /// </summary>
-    public int Row { get; private set; }
-
-    /// <summary>The number of rows visible in the terminal.</summary>
-    public virtual int Rows
-    {
-        get => _rows;
-        set
-        {
-            _rows = value;
-            ClearContents ();
-        }
-    }
-
-    /// <summary>The topmost row in the terminal.</summary>
-    public virtual int Top { get; set; } = 0;
-
-    /// <summary>Adds the specified rune to the display at the current cursor position.</summary>
-    /// <remarks>
-    ///     <para>
-    ///         When the method returns, <see cref="Col"/> will be incremented by the number of columns
-    ///         <paramref name="rune"/> required, even if the new column value is outside of the <see cref="Clip"/> or screen
-    ///         dimensions defined by <see cref="Cols"/>.
-    ///     </para>
-    ///     <para>
-    ///         If <paramref name="rune"/> requires more than one column, and <see cref="Col"/> plus the number of columns
-    ///         needed exceeds the <see cref="Clip"/> or screen dimensions, the default Unicode replacement character (U+FFFD)
-    ///         will be added instead.
-    ///     </para>
-    /// </remarks>
-    /// <param name="rune">Rune to add.</param>
-    public void AddRune (Rune rune)
-    {
-        int runeWidth = -1;
-        bool validLocation = IsValidLocation (rune, Col, Row);
-
-        if (Contents is null)
-        {
-            return;
-        }
-
-        Rectangle clipRect = Clip!.GetBounds ();
-
-        if (validLocation)
-        {
-            rune = rune.MakePrintable ();
-            runeWidth = rune.GetColumns ();
-
-            lock (Contents)
-            {
-                if (runeWidth == 0 && rune.IsCombiningMark ())
-                {
-                    // AtlasEngine does not support NON-NORMALIZED combining marks in a way
-                    // compatible with the driver architecture. Any CMs (except in the first col)
-                    // are correctly combined with the base char, but are ALSO treated as 1 column
-                    // width codepoints E.g. `echo "[e`u{0301}`u{0301}]"` will output `[é  ]`.
-                    // 
-                    // Until this is addressed (see Issue #), we do our best by
-                    // a) Attempting to normalize any CM with the base char to it's left
-                    // b) Ignoring any CMs that don't normalize
-                    if (Col > 0)
-                    {
-                        if (Contents [Row, Col - 1].CombiningMarks.Count > 0)
-                        {
-                            // Just add this mark to the list
-                            Contents [Row, Col - 1].AddCombiningMark (rune);
-
-                            // Ignore. Don't move to next column (let the driver figure out what to do).
-                        }
-                        else
-                        {
-                            // Attempt to normalize the cell to our left combined with this mark
-                            string combined = Contents [Row, Col - 1].Rune + rune.ToString ();
-
-                            // Normalize to Form C (Canonical Composition)
-                            string normalized = combined.Normalize (NormalizationForm.FormC);
-
-                            if (normalized.Length == 1)
-                            {
-                                // It normalized! We can just set the Cell to the left with the
-                                // normalized codepoint
-                                Contents [Row, Col - 1].Rune = (Rune)normalized [0];
-
-                                // Ignore. Don't move to next column because we're already there
-                            }
-                            else
-                            {
-                                // It didn't normalize. Add it to the Cell to left's CM list
-                                Contents [Row, Col - 1].AddCombiningMark (rune);
-
-                                // Ignore. Don't move to next column (let the driver figure out what to do).
-                            }
-                        }
-
-                        Contents [Row, Col - 1].Attribute = CurrentAttribute;
-                        Contents [Row, Col - 1].IsDirty = true;
-                    }
-                    else
-                    {
-                        // Most drivers will render a combining mark at col 0 as the mark
-                        Contents [Row, Col].Rune = rune;
-                        Contents [Row, Col].Attribute = CurrentAttribute;
-                        Contents [Row, Col].IsDirty = true;
-                        Col++;
-                    }
-                }
-                else
-                {
-                    Contents [Row, Col].Attribute = CurrentAttribute;
-                    Contents [Row, Col].IsDirty = true;
-
-                    if (Col > 0)
-                    {
-                        // Check if cell to left has a wide glyph
-                        if (Contents [Row, Col - 1].Rune.GetColumns () > 1)
-                        {
-                            // Invalidate cell to left
-                            Contents [Row, Col - 1].Rune = (Rune)'\0';
-                            Contents [Row, Col - 1].IsDirty = true;
-                        }
-                    }
-
-                    if (runeWidth < 1)
-                    {
-                        Contents [Row, Col].Rune = Rune.ReplacementChar;
-                    }
-                    else if (runeWidth == 1)
-                    {
-                        Contents [Row, Col].Rune = rune;
-
-                        if (Col < clipRect.Right - 1)
-                        {
-                            Contents [Row, Col + 1].IsDirty = true;
-                        }
-                    }
-                    else if (runeWidth == 2)
-                    {
-                        if (!Clip.Contains (Col + 1, Row))
-                        {
-                            // We're at the right edge of the clip, so we can't display a wide character.
-                            // TODO: Figure out if it is better to show a replacement character or ' '
-                            Contents [Row, Col].Rune = Rune.ReplacementChar;
-                        }
-                        else if (!Clip.Contains (Col, Row))
-                        {
-                            // Our 1st column is outside the clip, so we can't display a wide character.
-                            Contents [Row, Col + 1].Rune = Rune.ReplacementChar;
-                        }
-                        else
-                        {
-                            Contents [Row, Col].Rune = rune;
-
-                            if (Col < clipRect.Right - 1)
-                            {
-                                // Invalidate cell to right so that it doesn't get drawn
-                                // TODO: Figure out if it is better to show a replacement character or ' '
-                                Contents [Row, Col + 1].Rune = (Rune)'\0';
-                                Contents [Row, Col + 1].IsDirty = true;
-                            }
-                        }
-                    }
-                    else
-                    {
-                        // This is a non-spacing character, so we don't need to do anything
-                        Contents [Row, Col].Rune = (Rune)' ';
-                        Contents [Row, Col].IsDirty = false;
-                    }
-
-                    _dirtyLines! [Row] = true;
-                }
-            }
-        }
-
-        if (runeWidth is < 0 or > 0)
-        {
-            Col++;
-        }
-
-        if (runeWidth > 1)
-        {
-            Debug.Assert (runeWidth <= 2);
-
-            if (validLocation && Col < clipRect.Right)
-            {
-                lock (Contents!)
-                {
-                    // This is a double-width character, and we are not at the end of the line.
-                    // Col now points to the second column of the character. Ensure it doesn't
-                    // Get rendered.
-                    Contents [Row, Col].IsDirty = false;
-                    Contents [Row, Col].Attribute = CurrentAttribute;
-
-                    // TODO: Determine if we should wipe this out (for now now)
-                    //Contents [Row, Col].Rune = (Rune)' ';
-                }
-            }
-
-            Col++;
-        }
-    }
-
-    /// <summary>
-    ///     Adds the specified <see langword="char"/> to the display at the current cursor position. This method is a
-    ///     convenience method that calls <see cref="AddRune(Rune)"/> with the <see cref="Rune"/> constructor.
-    /// </summary>
-    /// <param name="c">Character to add.</param>
-    public void AddRune (char c) { AddRune (new Rune (c)); }
-
-    /// <summary>Adds the <paramref name="str"/> to the display at the cursor position.</summary>
-    /// <remarks>
-    ///     <para>
-    ///         When the method returns, <see cref="Col"/> will be incremented by the number of columns
-    ///         <paramref name="str"/> required, unless the new column value is outside of the <see cref="Clip"/> or screen
-    ///         dimensions defined by <see cref="Cols"/>.
-    ///     </para>
-    ///     <para>If <paramref name="str"/> requires more columns than are available, the output will be clipped.</para>
-    /// </remarks>
-    /// <param name="str">String.</param>
-    public void AddStr (string str)
-    {
-        List<Rune> runes = str.EnumerateRunes ().ToList ();
-
-        for (var i = 0; i < runes.Count; i++)
-        {
-            AddRune (runes [i]);
-        }
-    }
-
-    /// <summary>Fills the specified rectangle with the specified rune, using <see cref="CurrentAttribute"/></summary>
-    /// <remarks>
-    ///     The value of <see cref="Clip"/> is honored. Any parts of the rectangle not in the clip will not be drawn.
-    /// </remarks>
-    /// <param name="rect">The Screen-relative rectangle.</param>
-    /// <param name="rune">The Rune used to fill the rectangle</param>
-    public void FillRect (Rectangle rect, Rune rune = default)
-    {
-        // BUGBUG: This should be a method on Region
-        rect = Rectangle.Intersect (rect, Clip?.GetBounds () ?? Screen);
-        lock (Contents!)
-        {
-            for (int r = rect.Y; r < rect.Y + rect.Height; r++)
-            {
-                for (int c = rect.X; c < rect.X + rect.Width; c++)
-                {
-                    if (!IsValidLocation (rune, c, r))
-                    {
-                        continue;
-                    }
-                    Contents [r, c] = new Cell
-                    {
-                        Rune = rune != default ? rune : (Rune)' ',
-                        Attribute = CurrentAttribute, IsDirty = true
-                    };
-                    _dirtyLines! [r] = true;
-                }
-            }
-        }
-    }
-
-    /// <summary>Clears the <see cref="Contents"/> of the driver.</summary>
-    public void ClearContents ()
-    {
-        Contents = new Cell [Rows, Cols];
-
-        //CONCURRENCY: Unsynchronized access to Clip isn't safe.
-        // TODO: ClearContents should not clear the clip; it should only clear the contents. Move clearing it elsewhere.
-        Clip = new (Screen);
-
-        _dirtyLines = new bool [Rows];
-
-        lock (Contents)
-        {
-            for (var row = 0; row < Rows; row++)
-            {
-                for (var c = 0; c < Cols; c++)
-                {
-                    Contents [row, c] = new ()
-                    {
-                        Rune = (Rune)' ',
-                        Attribute = new Attribute (Color.White, Color.Black),
-                        IsDirty = true
-                    };
-                }
-
-                _dirtyLines [row] = true;
-            }
-        }
-
-        ClearedContents?.Invoke (this, EventArgs.Empty);
-    }
-
-    /// <summary>
-    ///     Raised each time <see cref="ClearContents"/> is called. For benchmarking.
-    /// </summary>
-    public event EventHandler<EventArgs>? ClearedContents;
-
-    /// <summary>
-    /// Sets <see cref="Contents"/> as dirty for situations where views
-    /// don't need layout and redrawing, but just refresh the screen.
-    /// </summary>
-    protected void SetContentsAsDirty ()
-    {
-        lock (Contents!)
-        {
-            for (var row = 0; row < Rows; row++)
-            {
-                for (var c = 0; c < Cols; c++)
-                {
-                    Contents [row, c].IsDirty = true;
-                }
-
-                _dirtyLines! [row] = true;
-            }
-        }
-    }
-
-    /// <summary>
-    ///     Fills the specified rectangle with the specified <see langword="char"/>. This method is a convenience method
-    ///     that calls <see cref="FillRect(Rectangle, Rune)"/>.
-    /// </summary>
-    /// <param name="rect"></param>
-    /// <param name="c"></param>
-    public void FillRect (Rectangle rect, char c) { FillRect (rect, new Rune (c)); }
-
-    #endregion Screen and Contents
-
-    #region Cursor Handling
-
-    /// <summary>Gets the terminal cursor visibility.</summary>
-    /// <param name="visibility">The current <see cref="CursorVisibility"/></param>
-    /// <returns><see langword="true"/> upon success</returns>
-    public abstract bool GetCursorVisibility (out CursorVisibility visibility);
-
-    /// <summary>Tests whether the specified coordinate are valid for drawing the specified Rune.</summary>
-    /// <param name="rune">Used to determine if one or two columns are required.</param>
-    /// <param name="col">The column.</param>
-    /// <param name="row">The row.</param>
-    /// <returns>
-    ///     <see langword="false"/> if the coordinate is outside the screen bounds or outside of <see cref="Clip"/>.
-    ///     <see langword="true"/> otherwise.
-    /// </returns>
-    public bool IsValidLocation (Rune rune, int col, int row)
-    {
-        if (rune.GetColumns () < 2)
-        {
-            return col >= 0 && row >= 0 && col < Cols && row < Rows && Clip!.Contains (col, row);
-        }
-        else
-        {
-
-            return Clip!.Contains (col, row) || Clip!.Contains (col + 1, row);
-        }
-    }
-
-    /// <summary>
-    /// Called when the terminal screen changes (size, position, etc.). Fires the <see cref="SizeChanged"/> event.
-    /// <see cref="Screen"/> reflects the source of truth for screen dimensions.
-    /// <see cref="Cols"/> and <see cref="Rows"/> are derived from <see cref="Screen"/> and are read-only.
-    /// </summary>
-    /// <param name="args">Event arguments containing the new screen size.</param>
-    public void OnSizeChanged (SizeChangedEventArgs args) 
-    { 
-        SizeChanged?.Invoke (this, args);
-    }
-
-
-    /// <summary>Updates the screen to reflect all the changes that have been done to the display buffer</summary>
-    public void Refresh ()
-    {
-        bool updated = UpdateScreen ();
-        UpdateCursor ();
-
-        Refreshed?.Invoke (this, new EventArgs<bool> (in updated));
-    }
-
-    /// <summary>
-    ///     Raised each time <see cref="Refresh"/> is called. For benchmarking.
-    /// </summary>
-    public event EventHandler<EventArgs<bool>>? Refreshed;
-
-    /// <summary>Sets the terminal cursor visibility.</summary>
-    /// <param name="visibility">The wished <see cref="CursorVisibility"/></param>
-    /// <returns><see langword="true"/> upon success</returns>
-    public abstract bool SetCursorVisibility (CursorVisibility visibility);
-
-    /// <summary>
-    /// The event fired when the screen changes (size, position, etc.).
-    /// <see cref="Screen"/> is the source of truth for screen dimensions.
-    /// <see cref="Cols"/> and <see cref="Rows"/> are read-only and derived from <see cref="Screen"/>.
-    /// </summary>
-    public event EventHandler<SizeChangedEventArgs>? SizeChanged;
-
-    #endregion Cursor Handling
-
-    /// <summary>Suspends the application (e.g. on Linux via SIGTSTP) and upon resume, resets the console driver.</summary>
-    /// <remarks>This is only implemented in the Unix driver.</remarks>
-    public abstract void Suspend ();
-
-    /// <summary>Sets the position of the terminal cursor to <see cref="Col"/> and <see cref="Row"/>.</summary>
-    public abstract void UpdateCursor ();
-
-    /// <summary>Redraws the physical screen with the contents that have been queued up via any of the printing commands.</summary>
-    /// <returns><see langword="true"/> if any updates to the screen were made.</returns>
-    public abstract bool UpdateScreen ();
-
-    #region Setup & Teardown
-
-    /// <summary>Initializes the driver</summary>
-    public abstract void Init ();
-
-    /// <summary>Ends the execution of the console driver.</summary>
-    public abstract void End ();
-
-    #endregion
-
-    #region Color Handling
-
-    /// <summary>Gets whether the <see cref="IConsoleDriver"/> supports TrueColor output.</summary>
-    public virtual bool SupportsTrueColor => true;
-
-    // TODO: This makes IConsoleDriver dependent on Application, which is not ideal. This should be moved to Application.
-    // BUGBUG: Application.Force16Colors should be bool? so if SupportsTrueColor and Application.Force16Colors == false, this doesn't override
-    /// <summary>
-    ///     Gets or sets whether the <see cref="IConsoleDriver"/> should use 16 colors instead of the default TrueColors.
-    ///     See <see cref="Application.Force16Colors"/> to change this setting via <see cref="ConfigurationManager"/>.
-    /// </summary>
-    /// <remarks>
-    ///     <para>
-    ///         Will be forced to <see langword="true"/> if <see cref="IConsoleDriver.SupportsTrueColor"/> is
-    ///         <see langword="false"/>, indicating that the <see cref="IConsoleDriver"/> cannot support TrueColor.
-    ///     </para>
-    /// </remarks>
-    public virtual bool Force16Colors
-    {
-        get => Application.Force16Colors || !SupportsTrueColor;
-        set => Application.Force16Colors = value || !SupportsTrueColor;
-    }
-
-    private int _cols;
-    private int _rows;
-
-    /// <summary>
-    ///     The <see cref="Attribute"/> that will be used for the next <see cref="AddRune(Rune)"/> or <see cref="AddStr"/>
-    ///     call.
-    /// </summary>
-    public Attribute CurrentAttribute { get; set; }
-
-    /// <summary>Selects the specified attribute as the attribute to use for future calls to AddRune and AddString.</summary>
-    /// <remarks>Implementations should call <c>base.SetAttribute(c)</c>.</remarks>
-    /// <param name="c">C.</param>
-    public Attribute SetAttribute (Attribute c)
-    {
-        Attribute prevAttribute = CurrentAttribute;
-        CurrentAttribute = c;
-
-        return prevAttribute;
-    }
-
-    /// <summary>Gets the current <see cref="Attribute"/>.</summary>
-    /// <returns>The current attribute.</returns>
-    public Attribute GetAttribute () { return CurrentAttribute; }
-    
-    #endregion Color Handling
-
-    #region Mouse Handling
-
-    /// <summary>Event fired when a mouse event occurs.</summary>
-    public event EventHandler<MouseEventArgs>? MouseEvent;
-
-    /// <summary>Called when a mouse event occurs. Fires the <see cref="MouseEvent"/> event.</summary>
-    /// <param name="a"></param>
-    public void OnMouseEvent (MouseEventArgs a)
-    {
-        // Ensure ScreenPosition is set
-        a.ScreenPosition = a.Position;
-
-        MouseEvent?.Invoke (this, a);
-    }
-
-    #endregion Mouse Handling
-
-    #region Keyboard Handling
-
-    /// <summary>Event fired when a key is pressed down. This is a precursor to <see cref="KeyUp"/>.</summary>
-    public event EventHandler<Key>? KeyDown;
-
-    /// <summary>
-    ///     Called when a key is pressed down. Fires the <see cref="KeyDown"/> event. This is a precursor to
-    ///     <see cref="OnKeyUp"/>.
-    /// </summary>
-    /// <param name="a"></param>
-    public void OnKeyDown (Key a) { KeyDown?.Invoke (this, a); }
-
-    /// <summary>Event fired when a key is released.</summary>
-    /// <remarks>
-    ///     Drivers that do not support key release events will fire this event after <see cref="KeyDown"/> processing is
-    ///     complete.
-    /// </remarks>
-    public event EventHandler<Key>? KeyUp;
-
-    /// <summary>Called when a key is released. Fires the <see cref="KeyUp"/> event.</summary>
-    /// <remarks>
-    ///     Drivers that do not support key release events will call this method after <see cref="OnKeyDown"/> processing
-    ///     is complete.
-    /// </remarks>
-    /// <param name="a"></param>
-    public void OnKeyUp (Key a) { KeyUp?.Invoke (this, a); }
-
-    internal char _highSurrogate = '\0';
-
-    internal bool IsValidInput (KeyCode keyCode, out KeyCode result)
-    {
-        result = keyCode;
-
-        if (char.IsHighSurrogate ((char)keyCode))
-        {
-            _highSurrogate = (char)keyCode;
-
-            return false;
-        }
-
-        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;
-        }
-
-        if (char.IsSurrogate ((char)keyCode))
-        {
-            return false;
-        }
-
-        if (_highSurrogate > 0)
-        {
-            _highSurrogate = '\0';
-        }
-
-        return true;
-    }
-
-    #endregion
-
-    private AnsiRequestScheduler? _scheduler;
-
-    /// <summary>
-    /// Queues the given <paramref name="request"/> for execution
-    /// </summary>
-    /// <param name="request"></param>
-    public void QueueAnsiRequest (AnsiEscapeSequenceRequest request)
-    {
-        GetRequestScheduler ().SendOrSchedule (request);
-    }
-
-    internal abstract IAnsiResponseParser GetParser ();
-
-    /// <summary>
-    ///     Gets the <see cref="AnsiRequestScheduler"/> for this <see cref="ConsoleDriver"/>.
-    /// </summary>
-    /// <returns></returns>
-    public AnsiRequestScheduler GetRequestScheduler ()
-    {
-        // Lazy initialization because GetParser is virtual
-        return _scheduler ??= new (GetParser ());
-    }
-
-}

+ 0 - 79
Terminal.Gui/Drivers/ConsoleInput.cs

@@ -1,79 +0,0 @@
-#nullable enable
-using System.Collections.Concurrent;
-
-namespace Terminal.Gui.Drivers;
-
-/// <summary>
-///     Base class for reading console input in perpetual loop
-/// </summary>
-/// <typeparam name="T"></typeparam>
-public abstract class ConsoleInput<T> : IConsoleInput<T>
-{
-    private ConcurrentQueue<T>? _inputBuffer;
-
-    /// <summary>
-    ///     Determines how to get the current system type, adjust
-    ///     in unit tests to simulate specific timings.
-    /// </summary>
-    public Func<DateTime> Now { get; set; } = () => DateTime.Now;
-
-    /// <inheritdoc/>
-    public virtual void Dispose () { }
-
-    /// <inheritdoc/>
-    public void Initialize (ConcurrentQueue<T> inputBuffer) { _inputBuffer = inputBuffer; }
-
-    /// <inheritdoc/>
-    public void Run (CancellationToken token)
-    {
-        try
-        {
-            if (_inputBuffer == null)
-            {
-                throw new ("Cannot run input before Initialization");
-            }
-
-            do
-            {
-                DateTime dt = Now ();
-
-                while (Peek ())
-                {
-                    foreach (T r in Read ())
-                    {
-                        _inputBuffer.Enqueue (r);
-                    }
-                }
-
-                TimeSpan took = Now () - dt;
-                TimeSpan sleepFor = TimeSpan.FromMilliseconds (20) - took;
-
-                Logging.DrainInputStream.Record (took.Milliseconds);
-
-                if (sleepFor.Milliseconds > 0)
-                {
-                    Task.Delay (sleepFor, token).Wait (token);
-                }
-
-                token.ThrowIfCancellationRequested ();
-            }
-            while (!token.IsCancellationRequested);
-        }
-        catch (OperationCanceledException)
-        { }
-    }
-
-    /// <summary>
-    ///     When implemented in a derived class, returns true if there is data available
-    ///     to read from console.
-    /// </summary>
-    /// <returns></returns>
-    protected abstract bool Peek ();
-
-    /// <summary>
-    ///     Returns the available data without blocking, called when <see cref="Peek"/>
-    ///     returns <see langword="true"/>.
-    /// </summary>
-    /// <returns></returns>
-    protected abstract IEnumerable<T> Read ();
-}

+ 95 - 0
Terminal.Gui/Drivers/ConsoleKeyInfoExtensions.cs

@@ -0,0 +1,95 @@
+namespace Terminal.Gui.Drivers;
+
+/// <summary>
+///     Extension methods for <see cref="ConsoleKeyInfo"/>.
+/// </summary>
+public static class ConsoleKeyInfoExtensions
+{
+    /// <summary>
+    ///     Returns a string representation of the <see cref="ConsoleKeyInfo"/> suitable for debugging and logging.
+    /// </summary>
+    /// <param name="consoleKeyInfo">The ConsoleKeyInfo to convert to string.</param>
+    /// <returns>A formatted string showing the key, character, and modifiers.</returns>
+    /// <remarks>
+    ///     <para>
+    ///         Examples:
+    ///         <list type="bullet">
+    ///             <item><c>Key: A ('a')</c> - lowercase 'a' pressed</item>
+    ///             <item><c>Key: A ('A'), Modifiers: Shift</c> - uppercase 'A' pressed</item>
+    ///             <item><c>Key: A (\0), Modifiers: Control</c> - Ctrl+A (no printable char)</item>
+    ///             <item><c>Key: Enter (0x000D)</c> - Enter key (carriage return)</item>
+    ///             <item><c>Key: F5 (\0)</c> - F5 function key</item>
+    ///             <item><c>Key: D2 ('@'), Modifiers: Shift</c> - Shift+2 on US keyboard</item>
+    ///             <item><c>Key: None ('é')</c> - Accented character</item>
+    ///             <item><c>Key: CursorUp (\0), Modifiers: Shift | Control</c> - Ctrl+Shift+Up Arrow</item>
+    ///         </list>
+    ///     </para>
+    /// </remarks>
+    public static string ToString (this ConsoleKeyInfo consoleKeyInfo)
+    {
+        var sb = new StringBuilder ();
+
+        // Always show the ConsoleKey enum value
+        sb.Append ("Key: ");
+        sb.Append (consoleKeyInfo.Key);
+
+        // Show the character if it's printable, otherwise show hex representation
+        sb.Append (" (");
+
+        if (consoleKeyInfo.KeyChar >= 32 && consoleKeyInfo.KeyChar <= 126) // Printable ASCII range
+        {
+            sb.Append ('\'');
+            sb.Append (consoleKeyInfo.KeyChar);
+            sb.Append ('\'');
+        }
+        else if (consoleKeyInfo.KeyChar == 0)
+        {
+            sb.Append ("\\0");
+        }
+        else
+        {
+            // Show special characters or non-printable as hex
+            sb.Append ("0x");
+            sb.Append (((int)consoleKeyInfo.KeyChar).ToString ("X4"));
+        }
+
+        sb.Append (')');
+
+        // Show modifiers if any are set
+        if (consoleKeyInfo.Modifiers != 0)
+        {
+            sb.Append (", Modifiers: ");
+
+            var needsSeparator = false;
+
+            if ((consoleKeyInfo.Modifiers & ConsoleModifiers.Shift) != 0)
+            {
+                sb.Append ("Shift");
+                needsSeparator = true;
+            }
+
+            if ((consoleKeyInfo.Modifiers & ConsoleModifiers.Alt) != 0)
+            {
+                if (needsSeparator)
+                {
+                    sb.Append (" | ");
+                }
+
+                sb.Append ("Alt");
+                needsSeparator = true;
+            }
+
+            if ((consoleKeyInfo.Modifiers & ConsoleModifiers.Control) != 0)
+            {
+                if (needsSeparator)
+                {
+                    sb.Append (" | ");
+                }
+
+                sb.Append ("Control");
+            }
+        }
+
+        return sb.ToString ();
+    }
+}

+ 118 - 2315
Terminal.Gui/Drivers/ConsoleKeyMapping.cs

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

+ 4 - 1
Terminal.Gui/Drivers/DotNetDriver/INetInput.cs

@@ -1,4 +1,7 @@
 namespace Terminal.Gui.Drivers;
 
-internal interface INetInput : IConsoleInput<ConsoleKeyInfo>
+/// <summary>
+///     Wraps IConsoleInput for .NET console input events (ConsoleKeyInfo). Needed to support Mocking in tests.
+/// </summary>
+internal interface INetInput : IInput<ConsoleKeyInfo>
 { }

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