Просмотр исходного кода

Fixes #4374 - Nukes all (?) legacy Driver and Application stuff; revamps tests (#4376)

Tig 1 месяц назад
Родитель
Сommit
d53fcd7485
100 измененных файлов с 4039 добавлено и 7959 удалено
  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. 1 1
      Examples/UICatalog/Scenarios/GraphViewExample.cs
  11. 1 4
      Examples/UICatalog/Scenarios/Keys.cs
  12. 1 1
      Examples/UICatalog/Scenarios/Mazing.cs
  13. 14 12
      Examples/UICatalog/Scenarios/Navigation.cs
  14. 34 33
      Examples/UICatalog/Scenarios/Notepad.cs
  15. 2 2
      Examples/UICatalog/Scenarios/NumericUpDownDemo.cs
  16. 246 107
      Examples/UICatalog/Scenarios/TableEditor.cs
  17. 1 1
      Examples/UICatalog/Scenarios/TextInputControls.cs
  18. 12 15
      Examples/UICatalog/Scenarios/TimeAndDate.cs
  19. 1 1
      Examples/UICatalog/Scenarios/TreeViewFileSystem.cs
  20. 9 10
      Examples/UICatalog/Scenarios/Unicode.cs
  21. 5 5
      Examples/UICatalog/UICatalog.cs
  22. 1 1
      README.md
  23. 109 0
      Scripts/Run-LocalCoverage.ps1
  24. 62 56
      Terminal.Gui.Analyzers.Tests/ProjectBuilder.cs
  25. 36 22
      Terminal.Gui/App/Application.Driver.cs
  26. 13 57
      Terminal.Gui/App/Application.Keyboard.cs
  27. 25 77
      Terminal.Gui/App/Application.Lifecycle.cs
  28. 4 6
      Terminal.Gui/App/Application.Mouse.cs
  29. 10 10
      Terminal.Gui/App/Application.Navigation.cs
  30. 50 462
      Terminal.Gui/App/Application.Run.cs
  31. 8 35
      Terminal.Gui/App/Application.Screen.cs
  32. 2 12
      Terminal.Gui/App/Application.Toplevel.cs
  33. 4 4
      Terminal.Gui/App/Application.cd
  34. 7 111
      Terminal.Gui/App/Application.cs
  35. 176 0
      Terminal.Gui/App/ApplicationImpl.Driver.cs
  36. 251 0
      Terminal.Gui/App/ApplicationImpl.Lifecycle.cs
  37. 347 0
      Terminal.Gui/App/ApplicationImpl.Run.cs
  38. 177 0
      Terminal.Gui/App/ApplicationImpl.Screen.cs
  39. 43 488
      Terminal.Gui/App/ApplicationImpl.cs
  40. 2 2
      Terminal.Gui/App/Clipboard/ClipboardBase.cs
  41. 1 1
      Terminal.Gui/App/Clipboard/ClipboardProcessRunner.cs
  42. 440 163
      Terminal.Gui/App/IApplication.cs
  43. 1 1
      Terminal.Gui/App/IterationEventArgs.cs
  44. 2 2
      Terminal.Gui/App/Keyboard/IKeyboard.cs
  45. 4 1
      Terminal.Gui/App/Keyboard/KeyboardImpl.cs
  46. 35 39
      Terminal.Gui/App/MainLoop/ApplicationMainLoop.cs
  47. 54 19
      Terminal.Gui/App/MainLoop/IApplicationMainLoop.cs
  48. 1 2
      Terminal.Gui/App/MainLoop/IMainLoopCoordinator.cs
  49. 97 80
      Terminal.Gui/App/MainLoop/MainLoopCoordinator.cs
  50. 2 0
      Terminal.Gui/App/Mouse/MouseImpl.cs
  51. 0 12
      Terminal.Gui/App/RunStateEventArgs.cs
  52. 19 19
      Terminal.Gui/App/SessionToken.cs
  53. 12 0
      Terminal.Gui/App/SessionTokenEventArgs.cs
  54. 1 1
      Terminal.Gui/App/Timeout/ITimedEvents.cs
  55. 1 1
      Terminal.Gui/Drawing/Cell.cs
  56. 3 3
      Terminal.Gui/Drawing/LineCanvas/LineCanvas.cs
  57. 1 1
      Terminal.Gui/Drawing/Ruler.cs
  58. 1 1
      Terminal.Gui/Drawing/Sixel/SixelToRender.cs
  59. 1 1
      Terminal.Gui/Drawing/Thickness.cs
  60. 1 1
      Terminal.Gui/Drivers/AnsiHandling/AnsiEscapeSequenceRequest.cs
  61. 16 17
      Terminal.Gui/Drivers/AnsiHandling/AnsiResponseParser.cs
  62. 4 4
      Terminal.Gui/Drivers/AnsiHandling/EscSeqUtils/EscSeqUtils.cs
  63. 5 5
      Terminal.Gui/Drivers/AnsiHandling/GenericHeld.cs
  64. 0 26
      Terminal.Gui/Drivers/ComponentFactory.cs
  65. 25 0
      Terminal.Gui/Drivers/ComponentFactoryImpl.cs
  66. 0 753
      Terminal.Gui/Drivers/ConsoleDriver.cs
  67. 0 79
      Terminal.Gui/Drivers/ConsoleInput.cs
  68. 95 0
      Terminal.Gui/Drivers/ConsoleKeyInfoExtensions.cs
  69. 118 2315
      Terminal.Gui/Drivers/ConsoleKeyMapping.cs
  70. 4 1
      Terminal.Gui/Drivers/DotNetDriver/INetInput.cs
  71. 8 17
      Terminal.Gui/Drivers/DotNetDriver/NetComponentFactory.cs
  72. 81 51
      Terminal.Gui/Drivers/DotNetDriver/NetInput.cs
  73. 3 45
      Terminal.Gui/Drivers/DotNetDriver/NetInputProcessor.cs
  74. 4 2
      Terminal.Gui/Drivers/DotNetDriver/NetKeyConverter.cs
  75. 57 20
      Terminal.Gui/Drivers/DotNetDriver/NetOutput.cs
  76. 1 1
      Terminal.Gui/Drivers/DotNetDriver/NetWinVTConsole.cs
  77. 166 102
      Terminal.Gui/Drivers/DriverImpl.cs
  78. 23 23
      Terminal.Gui/Drivers/FakeDriver/FakeComponentFactory.cs
  79. 0 1708
      Terminal.Gui/Drivers/FakeDriver/FakeConsole.cs
  80. 0 42
      Terminal.Gui/Drivers/FakeDriver/FakeConsoleInput.cs
  81. 0 379
      Terminal.Gui/Drivers/FakeDriver/FakeDriver.cs
  82. 47 0
      Terminal.Gui/Drivers/FakeDriver/FakeInput.cs
  83. 47 0
      Terminal.Gui/Drivers/FakeDriver/FakeInputProcessor.cs
  84. 39 14
      Terminal.Gui/Drivers/FakeDriver/FakeOutput.cs
  85. 29 20
      Terminal.Gui/Drivers/IComponentFactory.cs
  86. 0 26
      Terminal.Gui/Drivers/IConsoleDriverFacade.cs
  87. 0 29
      Terminal.Gui/Drivers/IConsoleInput.cs
  88. 61 31
      Terminal.Gui/Drivers/IDriver.cs
  89. 172 0
      Terminal.Gui/Drivers/IInput.cs
  90. 58 39
      Terminal.Gui/Drivers/IInputProcessor.cs
  91. 15 7
      Terminal.Gui/Drivers/IKeyConverter.cs
  92. 28 21
      Terminal.Gui/Drivers/IOutput.cs
  93. 74 59
      Terminal.Gui/Drivers/IOutputBuffer.cs
  94. 1 1
      Terminal.Gui/Drivers/ISizeMonitor.cs
  95. 15 0
      Terminal.Gui/Drivers/ITestableInput.cs
  96. 89 0
      Terminal.Gui/Drivers/InputImpl.cs
  97. 117 79
      Terminal.Gui/Drivers/InputProcessorImpl.cs
  98. 1 1
      Terminal.Gui/Drivers/KeyCode.cs
  99. 2 7
      Terminal.Gui/Drivers/OutputBase.cs
  100. 1 1
      Terminal.Gui/Drivers/OutputBufferImpl.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!);
 

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

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

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

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

Разница между файлами не показана из-за своего большого размера
+ 246 - 107
Examples/UICatalog/Scenarios/TableEditor.cs


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

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

+ 9 - 10
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);
@@ -185,7 +184,7 @@ public class UnicodeInMenu : Scenario
             X = 20,
             Y = Pos.Y (label),
             Width = Dim.Percent (60),
-            RadioLabels = new [] { "item #1", gitString, "Со_хранить", "𝔽𝕆𝕆𝔹𝔸ℝ" }
+            RadioLabels = ["item #1", gitString, "Со_хранить", "𝔽𝕆𝕆𝔹𝔸ℝ"]
         };
         appWindow.Add (radioGroup);
 

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

+ 1 - 1
README.md

@@ -1,6 +1,6 @@
 ![.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/branch/v2_develop/graph/badge.svg)](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 ();
+    }
+}

+ 43 - 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,6 +59,7 @@ public class ApplicationImpl : IApplication
             {
                 _mouse = new MouseImpl { Application = this };
             }
+
             return _mouse;
         }
         set => _mouse = value ?? throw new ArgumentNullException (nameof (value));
@@ -66,7 +68,7 @@ public class ApplicationImpl : IApplication
     private IKeyboard? _keyboard;
 
     /// <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 +78,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
 }

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

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

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

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

+ 8 - 17
Terminal.Gui/Drivers/DotNetDriver/NetComponentFactory.cs

@@ -4,26 +4,17 @@ using System.Collections.Concurrent;
 namespace Terminal.Gui.Drivers;
 
 /// <summary>
-/// <see cref="IComponentFactory{T}"/> implementation for native csharp console I/O i.e. dotnet.
-/// This factory creates instances of internal classes <see cref="NetInput"/>, <see cref="NetOutput"/> etc.
+///     <see cref="IComponentFactory{T}"/> implementation for native csharp console I/O i.e. dotnet.
+///     This factory creates instances of internal classes <see cref="NetInput"/>, <see cref="NetOutput"/> etc.
 /// </summary>
-public class NetComponentFactory : ComponentFactory<ConsoleKeyInfo>
+public class NetComponentFactory : ComponentFactoryImpl<ConsoleKeyInfo>
 {
     /// <inheritdoc/>
-    public override IConsoleInput<ConsoleKeyInfo> CreateInput ()
-    {
-        return new NetInput ();
-    }
+    public override IInput<ConsoleKeyInfo> CreateInput () { return new NetInput (); }
 
-    /// <inheritdoc />
-    public override IConsoleOutput CreateOutput ()
-    {
-        return new NetOutput ();
-    }
+    /// <inheritdoc/>
+    public override IInputProcessor CreateInputProcessor (ConcurrentQueue<ConsoleKeyInfo> inputBuffer) { return new NetInputProcessor (inputBuffer); }
 
-    /// <inheritdoc />
-    public override IInputProcessor CreateInputProcessor (ConcurrentQueue<ConsoleKeyInfo> inputBuffer)
-    {
-        return new NetInputProcessor (inputBuffer);
-    }
+    /// <inheritdoc/>
+    public override IOutput CreateOutput () { return new NetOutput (); }
 }

+ 81 - 51
Terminal.Gui/Drivers/DotNetDriver/NetInput.cs

@@ -3,12 +3,12 @@
 namespace Terminal.Gui.Drivers;
 
 /// <summary>
-///     Console input implementation that uses native dotnet methods e.g. <see cref="System.Console"/>.
+///     <see cref="IInput{TInputRecord}"/> implementation that uses native dotnet methods e.g. <see cref="System.Console"/>.
+///     The <see cref="Peek"/> and <see cref="Read"/> methods are executed
+///     on the input thread created by <see cref="MainLoopCoordinator{TInputRecord}.StartInputTaskAsync"/>.
 /// </summary>
-public class NetInput : ConsoleInput<ConsoleKeyInfo>, INetInput
+public class NetInput : InputImpl<ConsoleKeyInfo>, ITestableInput<ConsoleKeyInfo>, IDisposable
 {
-    private readonly NetWinVTConsole _adjustConsole;
-
     /// <summary>
     ///     Creates a new instance of the class. Implicitly sends
     ///     console mode settings that enable virtual input (mouse
@@ -16,12 +16,7 @@ public class NetInput : ConsoleInput<ConsoleKeyInfo>, INetInput
     /// </summary>
     public NetInput ()
     {
-        Logging.Logger.LogInformation ($"Creating {nameof (NetInput)}");
-
-        if (ConsoleDriver.RunningUnitTests)
-        {
-            return;
-        }
+        Logging.Information ($"Creating {nameof (NetInput)}");
 
         PlatformID p = Environment.OSVersion.Platform;
 
@@ -34,75 +29,110 @@ public class NetInput : ConsoleInput<ConsoleKeyInfo>, INetInput
             catch (ApplicationException ex)
             {
                 // Likely running as a unit test, or in a non-interactive session.
-                Logging.Logger.LogCritical (
-                                            ex,
-                                            "NetWinVTConsole could not be constructed i.e. could not configure terminal modes. May indicate running in non-interactive session e.g. unit testing CI");
+                Logging.Critical ($"NetWinVTConsole could not configure terminal modes. May indicate running in non-interactive session: {ex}");
+
+                return;
             }
         }
 
-        //Enable alternative screen buffer.
-        Console.Out.Write (EscSeqUtils.CSI_SaveCursorAndActivateAltBufferNoBackscroll);
+        try
+        {
+            //Enable alternative screen buffer.
+            Console.Out.Write (EscSeqUtils.CSI_SaveCursorAndActivateAltBufferNoBackscroll);
 
-        //Set cursor key to application.
-        Console.Out.Write (EscSeqUtils.CSI_HideCursor);
+            //Set cursor key to application.
+            Console.Out.Write (EscSeqUtils.CSI_HideCursor);
 
-        Console.Out.Write (EscSeqUtils.CSI_EnableMouseEvents);
-        Console.TreatControlCAsInput = true;
+            Console.Out.Write (EscSeqUtils.CSI_EnableMouseEvents);
+            Console.TreatControlCAsInput = true;
+        }
+        catch
+        {
+            // Swallow any exceptions during initialization for unit tests
+        }
     }
 
+    private readonly NetWinVTConsole _adjustConsole;
+
     /// <inheritdoc/>
-    protected override bool Peek ()
+    public override void Dispose ()
     {
-        if (ConsoleDriver.RunningUnitTests)
+        base.Dispose ();
+
+        try
         {
-            return false;
-        }
+            // Disable mouse events first
+            Console.Out.Write (EscSeqUtils.CSI_DisableMouseEvents);
+
+            //Disable alternative screen buffer.
+            Console.Out.Write (EscSeqUtils.CSI_RestoreCursorAndRestoreAltBufferWithBackscroll);
+
+            //Set cursor key to cursor.
+            Console.Out.Write (EscSeqUtils.CSI_ShowCursor);
 
-        return Console.KeyAvailable;
+            _adjustConsole?.Cleanup ();
+
+            // Flush any pending input so no stray events appear
+            FlushConsoleInput ();
+        }
+        catch
+        {
+            // Swallow any exceptions during Dispose for unit tests
+        }
     }
 
+    /// <inheritdoc />
+    public void AddInput (ConsoleKeyInfo input) { throw new NotImplementedException (); }
+
     /// <inheritdoc/>
-    protected override IEnumerable<ConsoleKeyInfo> Read ()
+    public override bool Peek ()
     {
-        while (Console.KeyAvailable)
+        try
         {
-            yield return Console.ReadKey (true);
+            return Console.KeyAvailable;
+        }
+        catch
+        {
+            return false;
         }
     }
 
-    private void FlushConsoleInput ()
+    /// <inheritdoc/>
+    public override IEnumerable<ConsoleKeyInfo> Read ()
     {
-        if (!ConsoleDriver.RunningUnitTests)
+        while (true)
         {
-            while (Console.KeyAvailable)
+            ConsoleKeyInfo keyInfo = default;
+
+            try
+            {
+                if (!Console.KeyAvailable)
+                {
+                    break;
+                }
+
+                keyInfo = Console.ReadKey (true);
+            }
+            catch (InvalidOperationException)
+            {
+                // Not connected to a terminal (GitHub Actions, redirected input, etc.)
+                yield break;
+            }
+            catch (IOException)
             {
-                Console.ReadKey (intercept: true);
+                // I/O error reading from console
+                yield break;
             }
+
+            yield return keyInfo;
         }
     }
 
-    /// <inheritdoc/>
-    public override void Dispose ()
+    private void FlushConsoleInput ()
     {
-        base.Dispose ();
-
-        if (ConsoleDriver.RunningUnitTests)
+        while (Console.KeyAvailable)
         {
-            return;
+            Console.ReadKey (true);
         }
-
-        // Disable mouse events first
-        Console.Out.Write (EscSeqUtils.CSI_DisableMouseEvents);
-
-        //Disable alternative screen buffer.
-        Console.Out.Write (EscSeqUtils.CSI_RestoreCursorAndRestoreAltBufferWithBackscroll);
-
-        //Set cursor key to cursor.
-        Console.Out.Write (EscSeqUtils.CSI_ShowCursor);
-
-        _adjustConsole?.Cleanup ();
-
-        // Flush any pending input so no stray events appear
-        FlushConsoleInput ();
     }
 }

+ 3 - 45
Terminal.Gui/Drivers/DotNetDriver/NetInputProcessor.cs

@@ -5,20 +5,8 @@ namespace Terminal.Gui.Drivers;
 /// <summary>
 ///     Input processor for <see cref="NetInput"/>, deals in <see cref="ConsoleKeyInfo"/> stream
 /// </summary>
-public class NetInputProcessor : InputProcessor<ConsoleKeyInfo>
+public class NetInputProcessor : InputProcessorImpl<ConsoleKeyInfo>
 {
-#pragma warning disable CA2211
-    /// <summary>
-    ///     Set to true to generate code in <see cref="Logging"/> (verbose only) for test cases in NetInputProcessorTests.
-    ///     <remarks>
-    ///         This makes the task of capturing user/language/terminal specific keyboard issues easier to
-    ///         diagnose. By turning this on and searching logs user can send us exactly the input codes that are released
-    ///         to input stream.
-    ///     </remarks>
-    /// </summary>
-    public static bool GenerateTestCasesForKeyPresses = false;
-#pragma warning restore CA2211
-
     /// <inheritdoc/>
     public NetInputProcessor (ConcurrentQueue<ConsoleKeyInfo> inputBuffer) : base (inputBuffer, new NetKeyConverter ())
     {
@@ -26,41 +14,11 @@ public class NetInputProcessor : InputProcessor<ConsoleKeyInfo>
     }
 
     /// <inheritdoc/>
-    protected override void Process (ConsoleKeyInfo consoleKeyInfo)
+    protected override void Process (ConsoleKeyInfo input)
     {
-        // For building test cases
-        if (GenerateTestCasesForKeyPresses)
-        {
-            Logging.Trace (FormatConsoleKeyInfoForTestCase (consoleKeyInfo));
-        }
-
-        foreach (Tuple<char, ConsoleKeyInfo> released in Parser.ProcessInput (Tuple.Create (consoleKeyInfo.KeyChar, consoleKeyInfo)))
+        foreach (Tuple<char, ConsoleKeyInfo> released in Parser.ProcessInput (Tuple.Create (input.KeyChar, input)))
         {
             ProcessAfterParsing (released.Item2);
         }
     }
-
-    /// <inheritdoc/>
-    protected override void ProcessAfterParsing (ConsoleKeyInfo input)
-    {
-        var key = KeyConverter.ToKey (input);
-
-        // If the key is not valid, we don't want to raise any events.
-        if (IsValidInput (key, out key))
-        {
-            OnKeyDown (key);
-            OnKeyUp (key);
-        }
-    }
-
-    /* For building test cases */
-    private static string FormatConsoleKeyInfoForTestCase (ConsoleKeyInfo input)
-    {
-        string charLiteral = input.KeyChar == '\0' ? @"'\0'" : $"'{input.KeyChar}'";
-
-        return $"new ConsoleKeyInfo({charLiteral}, ConsoleKey.{input.Key}, "
-               + $"{input.Modifiers.HasFlag (ConsoleModifiers.Shift).ToString ().ToLower ()}, "
-               + $"{input.Modifiers.HasFlag (ConsoleModifiers.Alt).ToString ().ToLower ()}, "
-               + $"{input.Modifiers.HasFlag (ConsoleModifiers.Control).ToString ().ToLower ()}),";
-    }
 }

+ 4 - 2
Terminal.Gui/Drivers/DotNetDriver/NetKeyConverter.cs

@@ -1,5 +1,4 @@
-
-namespace Terminal.Gui.Drivers;
+namespace Terminal.Gui.Drivers;
 
 /// <summary>
 ///     <see cref="IKeyConverter{T}"/> capable of converting the
@@ -23,4 +22,7 @@ internal class NetKeyConverter : IKeyConverter<ConsoleKeyInfo>
 
         return EscSeqUtils.MapKey (adjustedInput);
     }
+
+    /// <inheritdoc/>
+    public ConsoleKeyInfo ToKeyInfo (Key key) { return ConsoleKeyMapping.GetConsoleKeyInfoFromKeyCode (key.KeyCode); }
 }

+ 57 - 20
Terminal.Gui/Drivers/DotNetDriver/NetOutput.cs

@@ -3,10 +3,10 @@
 namespace Terminal.Gui.Drivers;
 
 /// <summary>
-///     Implementation of <see cref="IConsoleOutput"/> that uses native dotnet
+///     Implementation of <see cref="IOutput"/> that uses native dotnet
 ///     methods e.g. <see cref="System.Console"/>
 /// </summary>
-public class NetOutput : OutputBase, IConsoleOutput
+public class NetOutput : OutputBase, IOutput
 {
     private readonly bool _isWinPlatform;
 
@@ -15,9 +15,16 @@ public class NetOutput : OutputBase, IConsoleOutput
     /// </summary>
     public NetOutput ()
     {
-        Logging.Logger.LogInformation ($"Creating {nameof (NetOutput)}");
+        Logging.Information ($"Creating {nameof (NetOutput)}");
 
-        Console.OutputEncoding = Encoding.UTF8;
+        try
+        {
+            Console.OutputEncoding = Encoding.UTF8;
+        }
+        catch
+        {
+            // ignore for unit tests
+        }
 
         PlatformID p = Environment.OSVersion.Platform;
 
@@ -30,20 +37,36 @@ public class NetOutput : OutputBase, IConsoleOutput
     /// <inheritdoc/>
     public void Write (ReadOnlySpan<char> text)
     {
-        Console.Out.Write (text);
+        try
+        {
+            Console.Out.Write (text);
+        }
+        catch (IOException)
+        {
+            // Not connected to a terminal; do nothing
+        }
     }
 
 
     /// <inheritdoc/>
     public Size GetSize ()
     {
-        if (ConsoleDriver.RunningUnitTests)
+        try
         {
-            // For unit tests, we return a default size.
-            return Size.Empty;
+            Size size = new (Console.WindowWidth, Console.WindowHeight);
+            return size.IsEmpty ? new (80, 25) : size;
         }
+        catch (IOException)
+        {
+            // Not connected to a terminal; return a default size
+            return new (80, 25);
+        }
+    }
 
-        return new (Console.WindowWidth, Console.WindowHeight);
+    /// <inheritdoc />
+    public Point GetCursorPosition ()
+    {
+        return _lastCursorPosition ?? Point.Empty;
     }
 
     /// <inheritdoc/>
@@ -88,7 +111,14 @@ public class NetOutput : OutputBase, IConsoleOutput
     /// <inheritdoc />
     protected override void Write (StringBuilder output)
     {
-        Console.Out.Write (output);
+        try
+        {
+            Console.Out.Write (output);
+        }
+        catch (IOException)
+        {
+            // Not connected to a terminal; do nothing
+        }
     }
 
     /// <inheritdoc />
@@ -103,7 +133,7 @@ public class NetOutput : OutputBase, IConsoleOutput
 
         if (_isWinPlatform)
         {
-            // Could happens that the windows is still resizing and the col is bigger than Console.WindowWidth.
+            // Could happen that the windows is still resizing and the col is bigger than Console.WindowWidth.
             try
             {
                 Console.SetCursorPosition (col, row);
@@ -131,23 +161,30 @@ public class NetOutput : OutputBase, IConsoleOutput
 
     private EscSeqUtils.DECSCUSR_Style? _currentDecscusrStyle;
 
-    /// <inheritdoc cref="IConsoleOutput.SetCursorVisibility"/>
+    /// <inheritdoc cref="IOutput.SetCursorVisibility"/>
     public override void SetCursorVisibility (CursorVisibility visibility)
     {
-        if (visibility != CursorVisibility.Invisible)
+        try
         {
-            if (_currentDecscusrStyle is null || _currentDecscusrStyle != (EscSeqUtils.DECSCUSR_Style)(((int)visibility >> 24) & 0xFF))
+            if (visibility != CursorVisibility.Invisible)
             {
-                _currentDecscusrStyle = (EscSeqUtils.DECSCUSR_Style)(((int)visibility >> 24) & 0xFF);
+                if (_currentDecscusrStyle is null || _currentDecscusrStyle != (EscSeqUtils.DECSCUSR_Style)(((int)visibility >> 24) & 0xFF))
+                {
+                    _currentDecscusrStyle = (EscSeqUtils.DECSCUSR_Style)(((int)visibility >> 24) & 0xFF);
 
-                Write (EscSeqUtils.CSI_SetCursorStyle ((EscSeqUtils.DECSCUSR_Style)_currentDecscusrStyle));
-            }
+                    Write (EscSeqUtils.CSI_SetCursorStyle ((EscSeqUtils.DECSCUSR_Style)_currentDecscusrStyle));
+                }
 
-            Write (EscSeqUtils.CSI_ShowCursor);
+                Write (EscSeqUtils.CSI_ShowCursor);
+            }
+            else
+            {
+                Write (EscSeqUtils.CSI_HideCursor);
+            }
         }
-        else
+        catch
         {
-            Write (EscSeqUtils.CSI_HideCursor);
+            // Ignore any exceptions
         }
     }
 }

+ 1 - 1
Terminal.Gui/Drivers/DotNetDriver/NetWinVTConsole.cs

@@ -79,7 +79,7 @@ internal class NetWinVTConsole
     {
         if (!FlushConsoleInputBuffer (_inputHandle))
         {
-            throw new ApplicationException ($"Failed to flush input buffer, error code: {GetLastError ()}.");
+            throw new ApplicationException ($"Failed to flush input queue, error code: {GetLastError ()}.");
         }
 
         if (!SetConsoleMode (_inputHandle, _originalInputConsoleMode))

+ 166 - 102
Terminal.Gui/Drivers/ConsoleDriverFacade.cs → Terminal.Gui/Drivers/DriverImpl.cs

@@ -3,69 +3,108 @@ using System.Runtime.InteropServices;
 
 namespace Terminal.Gui.Drivers;
 
-internal class ConsoleDriverFacade<T> : IConsoleDriver, IConsoleDriverFacade
+/// <summary>
+///     Provides the main implementation of the driver abstraction layer for Terminal.Gui.
+///     This implementation of <see cref="IDriver"/> coordinates the interaction between input processing, output
+///     rendering,
+///     screen size monitoring, and ANSI escape sequence handling.
+/// </summary>
+/// <remarks>
+///     <para>
+///         <see cref="DriverImpl"/> implements <see cref="IDriver"/>,
+///         serving as the central coordination point for console I/O operations. It delegates functionality
+///         to specialized components:
+///     </para>
+///     <list type="bullet">
+///         <item><see cref="IInputProcessor"/> - Processes keyboard and mouse input</item>
+///         <item><see cref="IOutputBuffer"/> - Manages the screen buffer state</item>
+///         <item><see cref="IOutput"/> - Handles actual console output rendering</item>
+///         <item><see cref="AnsiRequestScheduler"/> - Manages ANSI escape sequence requests</item>
+///         <item><see cref="ISizeMonitor"/> - Monitors terminal size changes</item>
+///     </list>
+///     <para>
+///         This class is internal and should not be used directly by application code.
+///         Applications interact with drivers through the <see cref="Application"/> class.
+///     </para>
+/// </remarks>
+internal class DriverImpl : IDriver
 {
-    private readonly IConsoleOutput _output;
-    private readonly IOutputBuffer _outputBuffer;
+    private readonly IOutput _output;
     private readonly AnsiRequestScheduler _ansiRequestScheduler;
     private CursorVisibility _lastCursor = CursorVisibility.Default;
 
     /// <summary>
-    /// The event fired when the screen changes (size, position, etc.).
+    ///     Initializes a new instance of the <see cref="DriverImpl"/> class.
     /// </summary>
-    public event EventHandler<SizeChangedEventArgs>? SizeChanged;
-
-    public IInputProcessor InputProcessor { get; }
-    public IOutputBuffer OutputBuffer => _outputBuffer;
-
-    public IConsoleSizeMonitor ConsoleSizeMonitor { get; }
-
-
-    public ConsoleDriverFacade (
+    /// <param name="inputProcessor">The input processor for handling keyboard and mouse events.</param>
+    /// <param name="outputBuffer">The output buffer for managing screen state.</param>
+    /// <param name="output">The output interface for rendering to the console.</param>
+    /// <param name="ansiRequestScheduler">The scheduler for managing ANSI escape sequence requests.</param>
+    /// <param name="sizeMonitor">The monitor for tracking terminal size changes.</param>
+    public DriverImpl (
         IInputProcessor inputProcessor,
         IOutputBuffer outputBuffer,
-        IConsoleOutput output,
+        IOutput output,
         AnsiRequestScheduler ansiRequestScheduler,
-        IConsoleSizeMonitor sizeMonitor
+        ISizeMonitor sizeMonitor
     )
     {
         InputProcessor = inputProcessor;
         _output = output;
-        _outputBuffer = outputBuffer;
+        OutputBuffer = outputBuffer;
         _ansiRequestScheduler = ansiRequestScheduler;
 
         InputProcessor.KeyDown += (s, e) => KeyDown?.Invoke (s, e);
         InputProcessor.KeyUp += (s, e) => KeyUp?.Invoke (s, e);
+
         InputProcessor.MouseEvent += (s, e) =>
                                      {
                                          //Logging.Logger.LogTrace ($"Mouse {e.Flags} at x={e.ScreenPosition.X} y={e.ScreenPosition.Y}");
                                          MouseEvent?.Invoke (s, e);
                                      };
 
-        ConsoleSizeMonitor = sizeMonitor;
+        SizeMonitor = sizeMonitor;
+
         sizeMonitor.SizeChanged += (_, e) =>
-        {
-            SetScreenSize(e.Size!.Value.Width, e.Size.Value.Height);
-            //SizeChanged?.Invoke (this, e);
-        };
+                                   {
+                                       SetScreenSize (e.Size!.Value.Width, e.Size.Value.Height);
+
+                                       //SizeChanged?.Invoke (this, e);
+                                   };
 
         CreateClipboard ();
     }
 
+    /// <summary>
+    ///     The event fired when the screen changes (size, position, etc.).
+    /// </summary>
+    public event EventHandler<SizeChangedEventArgs>? SizeChanged;
+
+    /// <inheritdoc/>
+    public IInputProcessor InputProcessor { get; }
+
+    /// <inheritdoc/>
+    public IOutputBuffer OutputBuffer { get; }
+
+    /// <inheritdoc/>
+    public ISizeMonitor SizeMonitor { get; }
+
+
     private void CreateClipboard ()
     {
-        if (FakeDriver.FakeBehaviors.UseFakeClipboard)
+        if (InputProcessor.DriverName is { } && InputProcessor.DriverName.Contains ("fake"))
         {
-            Clipboard = new FakeClipboard (
-                FakeDriver.FakeBehaviors.FakeClipboardAlwaysThrowsNotSupportedException,
-                FakeDriver.FakeBehaviors.FakeClipboardIsSupportedAlwaysFalse);
+            if (Clipboard is null)
+            {
+                Clipboard = new FakeClipboard ();
+            }
 
             return;
         }
 
         PlatformID p = Environment.OSVersion.Platform;
 
-        if (p == PlatformID.Win32NT || p == PlatformID.Win32S || p == PlatformID.Win32Windows)
+        if (p is PlatformID.Win32NT or PlatformID.Win32S or PlatformID.Win32Windows)
         {
             Clipboard = new WindowsClipboard ();
         }
@@ -77,10 +116,8 @@ internal class ConsoleDriverFacade<T> : IConsoleDriver, IConsoleDriverFacade
         {
             Clipboard = new WSLClipboard ();
         }
-        else
-        {
-            Clipboard = new FakeClipboard ();
-        }
+
+        // Clipboard is set to FakeClipboard at initialization
     }
 
     /// <summary>Gets the location and size of the terminal screen.</summary>
@@ -88,27 +125,27 @@ internal class ConsoleDriverFacade<T> : IConsoleDriver, IConsoleDriverFacade
     {
         get
         {
-            if (ConsoleDriver.RunningUnitTests && _output is WindowsOutput or NetOutput)
-            {
-                // In unit tests, we don't have a real output, so we return an empty rectangle.
-                return Rectangle.Empty;
-            }
+            //if (Application.RunningUnitTests && _output is WindowsConsoleOutput or NetOutput)
+            //{
+            //    // In unit tests, we don't have a real output, so we return an empty rectangle.
+            //    return Rectangle.Empty;
+            //}
 
-            return new (0, 0, _outputBuffer.Cols, _outputBuffer.Rows);
+            return new (0, 0, OutputBuffer.Cols, OutputBuffer.Rows);
         }
     }
 
     /// <summary>
-    /// Sets the screen size for testing purposes. Only supported by FakeDriver.
+    ///     Sets the screen size for testing purposes. Only supported by FakeDriver.
     /// </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)
     {
-        _outputBuffer.SetSize (width, height);
+        OutputBuffer.SetSize (width, height);
         _output.SetSize (width, height);
-        SizeChanged?.Invoke(this, new (new (width, height)));
+        SizeChanged?.Invoke (this, new (new (width, height)));
     }
 
     /// <summary>
@@ -118,24 +155,24 @@ internal class ConsoleDriverFacade<T> : IConsoleDriver, IConsoleDriverFacade
     /// <value>The rectangle describing the of <see cref="Clip"/> region.</value>
     public Region? Clip
     {
-        get => _outputBuffer.Clip;
-        set => _outputBuffer.Clip = value;
+        get => OutputBuffer.Clip;
+        set => OutputBuffer.Clip = value;
     }
 
     /// <summary>Get the operating system clipboard.</summary>
-    public IClipboard Clipboard { get; private set; } = new FakeClipboard ();
+    public IClipboard? Clipboard { get; private set; } = new FakeClipboard ();
 
     /// <summary>
     ///     Gets the column last set by <see cref="Move"/>. <see cref="Col"/> and <see cref="Row"/> are used by
     ///     <see cref="AddRune(Rune)"/> and <see cref="AddStr"/> to determine where to add content.
     /// </summary>
-    public int Col => _outputBuffer.Col;
+    public int Col => OutputBuffer.Col;
 
     /// <summary>The number of columns visible in the terminal.</summary>
     public int Cols
     {
-        get => _outputBuffer.Cols;
-        set => _outputBuffer.Cols = value;
+        get => OutputBuffer.Cols;
+        set => OutputBuffer.Cols = value;
     }
 
     /// <summary>
@@ -144,51 +181,51 @@ internal class ConsoleDriverFacade<T> : IConsoleDriver, IConsoleDriverFacade
     /// </summary>
     public Cell [,]? Contents
     {
-        get => _outputBuffer.Contents;
-        set => _outputBuffer.Contents = value;
+        get => OutputBuffer.Contents;
+        set => OutputBuffer.Contents = value;
     }
 
     /// <summary>The leftmost column in the terminal.</summary>
     public int Left
     {
-        get => _outputBuffer.Left;
-        set => _outputBuffer.Left = value;
+        get => OutputBuffer.Left;
+        set => OutputBuffer.Left = value;
     }
 
     /// <summary>
     ///     Gets the row last set by <see cref="Move"/>. <see cref="Col"/> and <see cref="Row"/> are used by
     ///     <see cref="AddRune(Rune)"/> and <see cref="AddStr"/> to determine where to add content.
     /// </summary>
-    public int Row => _outputBuffer.Row;
+    public int Row => OutputBuffer.Row;
 
     /// <summary>The number of rows visible in the terminal.</summary>
     public int Rows
     {
-        get => _outputBuffer.Rows;
-        set => _outputBuffer.Rows = value;
+        get => OutputBuffer.Rows;
+        set => OutputBuffer.Rows = value;
     }
 
     /// <summary>The topmost row in the terminal.</summary>
     public int Top
     {
-        get => _outputBuffer.Top;
-        set => _outputBuffer.Top = value;
+        get => OutputBuffer.Top;
+        set => OutputBuffer.Top = value;
     }
 
     // TODO: Probably not everyone right?
 
-    /// <summary>Gets whether the <see cref="ConsoleDriver"/> supports TrueColor output.</summary>
+    /// <summary>Gets whether the <see cref="IDriver"/> supports TrueColor output.</summary>
     public bool SupportsTrueColor => true;
 
     // TODO: Currently ignored
     /// <summary>
-    ///     Gets or sets whether the <see cref="ConsoleDriver"/> should use 16 colors instead of the default TrueColors.
+    ///     Gets or sets whether the <see cref="IDriver"/> should use 16 colors instead of the default TrueColors.
     ///     See <see cref="Application.Force16Colors"/> to change this setting via <see cref="ConfigurationManager"/>.
     /// </summary>
     /// <remarks>
     ///     <para>
-    ///         Will be forced to <see langword="true"/> if <see cref="ConsoleDriver.SupportsTrueColor"/> is
-    ///         <see langword="false"/>, indicating that the <see cref="ConsoleDriver"/> cannot support TrueColor.
+    ///         Will be forced to <see langword="true"/> if <see cref="IDriver.SupportsTrueColor"/> is
+    ///         <see langword="false"/>, indicating that the <see cref="IDriver"/> cannot support TrueColor.
     ///     </para>
     /// </remarks>
     public bool Force16Colors
@@ -198,85 +235,86 @@ internal class ConsoleDriverFacade<T> : IConsoleDriver, IConsoleDriverFacade
     }
 
     /// <summary>
-    ///     The <see cref="System.Attribute"/> that will be used for the next <see cref="AddRune(Rune)"/> or <see cref="AddStr"/>
+    ///     The <see cref="System.Attribute"/> that will be used for the next <see cref="AddRune(Rune)"/> or
+    ///     <see cref="AddStr"/>
     ///     call.
     /// </summary>
     public Attribute CurrentAttribute
     {
-        get => _outputBuffer.CurrentAttribute;
-        set => _outputBuffer.CurrentAttribute = value;
+        get => OutputBuffer.CurrentAttribute;
+        set => OutputBuffer.CurrentAttribute = value;
     }
 
     /// <summary>Adds the specified rune to the display at the current cursor position.</summary>
     /// <remarks>
     ///     <para>
-    ///         When the method returns, <see cref="ConsoleDriver.Col"/> will be incremented by the number of columns
+    ///         When the method returns, <see cref="IDriver.Col"/> will be incremented by the number of columns
     ///         <paramref name="rune"/> required, even if the new column value is outside of the
-    ///         <see cref="ConsoleDriver.Clip"/> or screen
-    ///         dimensions defined by <see cref="ConsoleDriver.Cols"/>.
+    ///         <see cref="IDriver.Clip"/> or screen
+    ///         dimensions defined by <see cref="IDriver.Cols"/>.
     ///     </para>
     ///     <para>
-    ///         If <paramref name="rune"/> requires more than one column, and <see cref="ConsoleDriver.Col"/> plus the number
+    ///         If <paramref name="rune"/> requires more than one column, and <see cref="IDriver.Col"/> plus the number
     ///         of columns
-    ///         needed exceeds the <see cref="ConsoleDriver.Clip"/> or screen dimensions, the default Unicode replacement
+    ///         needed exceeds the <see cref="IDriver.Clip"/> or screen dimensions, the default Unicode replacement
     ///         character (U+FFFD)
     ///         will be added instead.
     ///     </para>
     /// </remarks>
     /// <param name="rune">Rune to add.</param>
-    public void AddRune (Rune rune) { _outputBuffer.AddRune (rune); }
+    public void AddRune (Rune rune) { OutputBuffer.AddRune (rune); }
 
     /// <summary>
     ///     Adds the specified <see langword="char"/> to the display at the current cursor position. This method is a
-    ///     convenience method that calls <see cref="ConsoleDriver.AddRune(System.Text.Rune)"/> with the <see cref="Rune"/>
+    ///     convenience method that calls <see cref="IDriver.AddRune(System.Text.Rune)"/> with the <see cref="Rune"/>
     ///     constructor.
     /// </summary>
     /// <param name="c">Character to add.</param>
-    public void AddRune (char c) { _outputBuffer.AddRune (c); }
+    public void AddRune (char c) { OutputBuffer.AddRune (c); }
 
     /// <summary>Adds the <paramref name="str"/> to the display at the cursor position.</summary>
     /// <remarks>
     ///     <para>
-    ///         When the method returns, <see cref="ConsoleDriver.Col"/> will be incremented by the number of columns
-    ///         <paramref name="str"/> required, unless the new column value is outside of the <see cref="ConsoleDriver.Clip"/>
+    ///         When the method returns, <see cref="IDriver.Col"/> will be incremented by the number of columns
+    ///         <paramref name="str"/> required, unless the new column value is outside of the <see cref="IDriver.Clip"/>
     ///         or screen
-    ///         dimensions defined by <see cref="ConsoleDriver.Cols"/>.
+    ///         dimensions defined by <see cref="IDriver.Cols"/>.
     ///     </para>
     ///     <para>If <paramref name="str"/> requires more columns than are available, the output will be clipped.</para>
     /// </remarks>
     /// <param name="str">String.</param>
-    public void AddStr (string str) { _outputBuffer.AddStr (str); }
+    public void AddStr (string str) { OutputBuffer.AddStr (str); }
 
-    /// <summary>Clears the <see cref="ConsoleDriver.Contents"/> of the driver.</summary>
+    /// <summary>Clears the <see cref="IDriver.Contents"/> of the driver.</summary>
     public void ClearContents ()
     {
-        _outputBuffer.ClearContents ();
+        OutputBuffer.ClearContents ();
         ClearedContents?.Invoke (this, new MouseEventArgs ());
     }
 
     /// <summary>
-    ///     Raised each time <see cref="ConsoleDriver.ClearContents"/> is called. For benchmarking.
+    ///     Raised each time <see cref="IDriver.ClearContents"/> is called. For benchmarking.
     /// </summary>
     public event EventHandler<EventArgs>? ClearedContents;
 
     /// <summary>
-    ///     Fills the specified rectangle with the specified rune, using <see cref="ConsoleDriver.CurrentAttribute"/>
+    ///     Fills the specified rectangle with the specified rune, using <see cref="IDriver.CurrentAttribute"/>
     /// </summary>
     /// <remarks>
-    ///     The value of <see cref="ConsoleDriver.Clip"/> is honored. Any parts of the rectangle not in the clip will not be
+    ///     The value of <see cref="IDriver.Clip"/> is honored. Any parts of the rectangle not in the clip will not be
     ///     drawn.
     /// </remarks>
     /// <param name="rect">The Screen-relative rectangle.</param>
     /// <param name="rune">The Rune used to fill the rectangle</param>
-    public void FillRect (Rectangle rect, Rune rune = default) { _outputBuffer.FillRect (rect, rune); }
+    public void FillRect (Rectangle rect, Rune rune = default) { OutputBuffer.FillRect (rect, rune); }
 
     /// <summary>
     ///     Fills the specified rectangle with the specified <see langword="char"/>. This method is a convenience method
-    ///     that calls <see cref="ConsoleDriver.FillRect(System.Drawing.Rectangle,System.Text.Rune)"/>.
+    ///     that calls <see cref="IDriver.FillRect(System.Drawing.Rectangle,System.Text.Rune)"/>.
     /// </summary>
     /// <param name="rect"></param>
     /// <param name="c"></param>
-    public void FillRect (Rectangle rect, char c) { _outputBuffer.FillRect (rect, c); }
+    public void FillRect (Rectangle rect, char c) { OutputBuffer.FillRect (rect, c); }
 
     /// <inheritdoc/>
     public virtual string GetVersionInfo ()
@@ -300,28 +338,28 @@ internal class ConsoleDriverFacade<T> : IConsoleDriver, IConsoleDriverFacade
     /// <param name="row">The row.</param>
     /// <returns>
     ///     <see langword="false"/> if the coordinate is outside the screen bounds or outside of
-    ///     <see cref="ConsoleDriver.Clip"/>.
+    ///     <see cref="IDriver.Clip"/>.
     ///     <see langword="true"/> otherwise.
     /// </returns>
-    public bool IsValidLocation (Rune rune, int col, int row) { return _outputBuffer.IsValidLocation (rune, col, row); }
+    public bool IsValidLocation (Rune rune, int col, int row) { return OutputBuffer.IsValidLocation (rune, col, row); }
 
     /// <summary>
-    ///     Updates <see cref="ConsoleDriver.Col"/> and <see cref="ConsoleDriver.Row"/> to the specified column and row in
-    ///     <see cref="ConsoleDriver.Contents"/>.
-    ///     Used by <see cref="ConsoleDriver.AddRune(System.Text.Rune)"/> and <see cref="ConsoleDriver.AddStr"/> to determine
+    ///     Updates <see cref="IDriver.Col"/> and <see cref="IDriver.Row"/> to the specified column and row in
+    ///     <see cref="IDriver.Contents"/>.
+    ///     Used by <see cref="IDriver.AddRune(System.Text.Rune)"/> and <see cref="IDriver.AddStr"/> to determine
     ///     where to add content.
     /// </summary>
     /// <remarks>
     ///     <para>This does not move the cursor on the screen, it only updates the internal state of the driver.</para>
     ///     <para>
-    ///         If <paramref name="col"/> or <paramref name="row"/> are negative or beyond  <see cref="ConsoleDriver.Cols"/>
+    ///         If <paramref name="col"/> or <paramref name="row"/> are negative or beyond  <see cref="IDriver.Cols"/>
     ///         and
-    ///         <see cref="ConsoleDriver.Rows"/>, the method still sets those properties.
+    ///         <see cref="IDriver.Rows"/>, the method still sets those properties.
     ///     </para>
     /// </remarks>
     /// <param name="col">Column to move to.</param>
     /// <param name="row">Row to move to.</param>
-    public void Move (int col, int row) { _outputBuffer.Move (col, row); }
+    public void Move (int col, int row) { OutputBuffer.Move (col, row); }
 
     // TODO: Probably part of output
 
@@ -347,6 +385,8 @@ internal class ConsoleDriverFacade<T> : IConsoleDriver, IConsoleDriverFacade
     /// <inheritdoc/>
     public void Suspend ()
     {
+        // BUGBUG: This is all platform-specific and should not be implemented here.
+        // BUGBUG: This needs to be in each platform's driver implementation.
         if (Environment.OSVersion.Platform != PlatformID.Unix)
         {
             return;
@@ -354,7 +394,7 @@ internal class ConsoleDriverFacade<T> : IConsoleDriver, IConsoleDriverFacade
 
         Console.Out.Write (EscSeqUtils.CSI_DisableMouseEvents);
 
-        if (!ConsoleDriver.RunningUnitTests)
+        try
         {
             Console.ResetColor ();
             Console.Clear ();
@@ -369,16 +409,21 @@ internal class ConsoleDriverFacade<T> : IConsoleDriver, IConsoleDriverFacade
 
             //Enable alternative screen buffer.
             Console.Out.Write (EscSeqUtils.CSI_SaveCursorAndActivateAltBufferNoBackscroll);
-
-            Application.LayoutAndDraw ();
         }
+        catch (Exception ex)
+        {
+            Logging.Error ($"Error suspending terminal: {ex.Message}");
+        }
+
+        Application.LayoutAndDraw ();
+
 
         Console.Out.Write (EscSeqUtils.CSI_EnableMouseEvents);
     }
 
     /// <summary>
-    ///     Sets the position of the terminal cursor to <see cref="ConsoleDriver.Col"/> and
-    ///     <see cref="ConsoleDriver.Row"/>.
+    ///     Sets the position of the terminal cursor to <see cref="IDriver.Col"/> and
+    ///     <see cref="IDriver.Row"/>.
     /// </summary>
     public void UpdateCursor () { _output.SetCursorPosition (Col, Row); }
 
@@ -397,22 +442,22 @@ internal class ConsoleDriverFacade<T> : IConsoleDriver, IConsoleDriverFacade
     /// <returns>The previously set Attribute.</returns>
     public Attribute SetAttribute (Attribute newAttribute)
     {
-        Attribute currentAttribute = _outputBuffer.CurrentAttribute;
-        _outputBuffer.CurrentAttribute = newAttribute;
+        Attribute currentAttribute = OutputBuffer.CurrentAttribute;
+        OutputBuffer.CurrentAttribute = newAttribute;
 
         return currentAttribute;
     }
 
     /// <summary>Gets the current <see cref="Attribute"/>.</summary>
     /// <returns>The current attribute.</returns>
-    public Attribute GetAttribute () { return _outputBuffer.CurrentAttribute; }
+    public Attribute GetAttribute () { return OutputBuffer.CurrentAttribute; }
 
-    /// <summary>Event fired when a key is pressed down. This is a precursor to <see cref="ConsoleDriver.KeyUp"/>.</summary>
+    /// <summary>Event fired when a key is pressed down. This is a precursor to <see cref="IDriver.KeyUp"/>.</summary>
     public event EventHandler<Key>? KeyDown;
 
     /// <summary>Event fired when a key is released.</summary>
     /// <remarks>
-    ///     Drivers that do not support key release events will fire this event after <see cref="ConsoleDriver.KeyDown"/>
+    ///     Drivers that do not support key release events will fire this event after <see cref="IDriver.KeyDown"/>
     ///     processing is
     ///     complete.
     /// </remarks>
@@ -422,17 +467,31 @@ internal class ConsoleDriverFacade<T> : IConsoleDriver, IConsoleDriverFacade
     public event EventHandler<MouseEventArgs>? MouseEvent;
 
     /// <summary>
-    ///     Provide proper writing to send escape sequence recognized by the <see cref="ConsoleDriver"/>.
+    ///     Provide proper writing to send escape sequence recognized by the <see cref="IDriver"/>.
     /// </summary>
     /// <param name="ansi"></param>
     public void WriteRaw (string ansi) { _output.Write (ansi); }
 
+    /// <inheritdoc/>
+    public void EnqueueKeyEvent (Key key)
+    {
+        InputProcessor.EnqueueKeyDownEvent (key);
+    }
+
     /// <summary>
-    ///     Queues the given <paramref name="request"/> for execution
+    ///     Queues the specified ANSI escape sequence request for execution.
     /// </summary>
-    /// <param name="request"></param>
+    /// <param name="request">The ANSI request to queue.</param>
+    /// <remarks>
+    ///     The request is sent immediately if possible, or queued for later execution
+    ///     by the <see cref="AnsiRequestScheduler"/> to prevent overwhelming the console.
+    /// </remarks>
     public void QueueAnsiRequest (AnsiEscapeSequenceRequest request) { _ansiRequestScheduler.SendOrSchedule (request); }
 
+    /// <summary>
+    ///     Gets the <see cref="AnsiRequestScheduler"/> instance used by this driver.
+    /// </summary>
+    /// <returns>The ANSI request scheduler.</returns>
     public AnsiRequestScheduler GetRequestScheduler () { return _ansiRequestScheduler; }
 
     /// <inheritdoc/>
@@ -440,4 +499,9 @@ internal class ConsoleDriverFacade<T> : IConsoleDriver, IConsoleDriverFacade
     {
         // No need we will always draw when dirty
     }
+
+    public string? GetName ()
+    {
+        return InputProcessor.DriverName?.ToLowerInvariant ();
+    }
 }

+ 23 - 23
Terminal.Gui/Drivers/FakeDriver/FakeComponentFactory.cs

@@ -4,47 +4,47 @@ using System.Collections.Concurrent;
 namespace Terminal.Gui.Drivers;
 
 /// <summary>
-/// <see cref="IComponentFactory{T}"/> implementation for fake/mock console I/O used in unit tests.
-/// This factory creates instances that simulate console behavior without requiring a real terminal.
+///     <see cref="IComponentFactory{T}"/> implementation for fake/mock console I/O used in unit tests.
+///     This factory creates instances that simulate console behavior without requiring a real terminal.
 /// </summary>
-public class FakeComponentFactory : ComponentFactory<ConsoleKeyInfo>
+public class FakeComponentFactory : ComponentFactoryImpl<ConsoleKeyInfo>
 {
-    private readonly ConcurrentQueue<ConsoleKeyInfo>? _predefinedInput;
-    private readonly FakeConsoleOutput? _output;
+    private readonly FakeInput? _input;
+    private readonly IOutput? _output;
+    private readonly ISizeMonitor? _sizeMonitor;
 
     /// <summary>
-    /// Creates a new FakeComponentFactory with optional predefined input and output capture.
+    ///     Creates a new FakeComponentFactory with optional output capture.
     /// </summary>
-    /// <param name="predefinedInput">Optional queue of predefined input events to simulate.</param>
+    /// <param name="input"></param>
     /// <param name="output">Optional fake output to capture what would be written to console.</param>
-    public FakeComponentFactory (ConcurrentQueue<ConsoleKeyInfo>? predefinedInput = null, FakeConsoleOutput? output = null)
+    /// <param name="sizeMonitor"></param>
+    public FakeComponentFactory (FakeInput? input = null, IOutput? output = null, ISizeMonitor? sizeMonitor = null)
     {
-        _predefinedInput = predefinedInput;
+        _input = input;
         _output = output;
+        _sizeMonitor = sizeMonitor;
     }
 
+
     /// <inheritdoc/>
-    public override IConsoleInput<ConsoleKeyInfo> CreateInput ()
+    public override ISizeMonitor CreateSizeMonitor (IOutput consoleOutput, IOutputBuffer outputBuffer)
     {
-        return new FakeConsoleInput (_predefinedInput);
+        return _sizeMonitor ?? new SizeMonitorImpl (consoleOutput);
     }
 
-    /// <inheritdoc />
-    public override IConsoleOutput CreateOutput ()
+    /// <inheritdoc/>
+    public override IInput<ConsoleKeyInfo> CreateInput ()
     {
-        return _output ?? new FakeConsoleOutput ();
+        return _input ?? new FakeInput ();
     }
 
-    /// <inheritdoc />
-    public override IInputProcessor CreateInputProcessor (ConcurrentQueue<ConsoleKeyInfo> inputBuffer)
-    {
-        return new NetInputProcessor (inputBuffer);
-    }
+    /// <inheritdoc/>
+    public override IInputProcessor CreateInputProcessor (ConcurrentQueue<ConsoleKeyInfo> inputBuffer) { return new FakeInputProcessor (inputBuffer); }
 
-    /// <inheritdoc />
-    public override IConsoleSizeMonitor CreateConsoleSizeMonitor (IConsoleOutput consoleOutput, IOutputBuffer outputBuffer)
+    /// <inheritdoc/>
+    public override IOutput CreateOutput ()
     {
-        outputBuffer.SetSize(consoleOutput.GetSize().Width, consoleOutput.GetSize().Height);
-        return new ConsoleSizeMonitor (consoleOutput, outputBuffer);
+        return _output ?? new FakeOutput ();
     }
 }

+ 0 - 1708
Terminal.Gui/Drivers/FakeDriver/FakeConsole.cs

@@ -1,1708 +0,0 @@
-//
-// FakeConsole.cs: A fake .NET Windows Console API implementation for unit tests.
-//
-
-namespace Terminal.Gui.Drivers;
-
-#pragma warning disable RCS1138 // Add summary to documentation comment.
-/// <summary></summary>
-public static class FakeConsole
-{
-#pragma warning restore RCS1138 // Add summary to documentation comment.
-
-    //
-    // Summary:
-    //	Gets or sets the width of the console window.
-    //
-    // Returns:
-    //	The width of the console window measured in columns.
-    //
-    // Exceptions:
-    //	T:System.ArgumentOutOfRangeException:
-    //	The value of the System.Console.WindowWidth property or the value of the System.Console.WindowHeight
-    //	property is less than or equal to 0.-or-The value of the System.Console.WindowHeight
-    //	property plus the value of the System.Console.WindowTop property is greater than
-    //	or equal to System.Int16.MaxValue.-or-The value of the System.Console.WindowWidth
-    //	property or the value of the System.Console.WindowHeight property is greater
-    //	than the largest possible window width or height for the current screen resolution
-    //	and console font.
-    //
-    //	T:System.IO.IOException:
-    //	Error reading or writing information.
-#pragma warning disable RCS1138 // Add summary to documentation comment.
-
-    /// <summary>Specifies the initial console width.</summary>
-    public const int WIDTH = 0;
-
-    /// <summary>Specifies the initial console height.</summary>
-    public const int HEIGHT = 0;
-
-    /// <summary></summary>
-    public static int WindowWidth { get; set; } = WIDTH;
-
-    //
-    // Summary:
-    //	Gets a value that indicates whether output has been redirected from the standard
-    //	output stream.
-    //
-    // Returns:
-    //	true if output is redirected; otherwise, false.
-    /// <summary></summary>
-    public static bool IsOutputRedirected { get; }
-
-    //
-    // Summary:
-    //	Gets a value that indicates whether the error output stream has been redirected
-    //	from the standard error stream.
-    //
-    // Returns:
-    //	true if error output is redirected; otherwise, false.
-    /// <summary></summary>
-    public static bool IsErrorRedirected { get; }
-
-    //
-    // Summary:
-    //	Gets the standard input stream.
-    //
-    // Returns:
-    //	A System.IO.TextReader that represents the standard input stream.
-    /// <summary></summary>
-    public static TextReader In { get; }
-
-    //
-    // Summary:
-    //	Gets the standard output stream.
-    //
-    // Returns:
-    //	A System.IO.TextWriter that represents the standard output stream.
-    /// <summary></summary>
-    public static TextWriter Out { get; }
-
-    //
-    // Summary:
-    //	Gets the standard error output stream.
-    //
-    // Returns:
-    //	A System.IO.TextWriter that represents the standard error output stream.
-    /// <summary></summary>
-    public static TextWriter Error { get; }
-
-    //
-    // Summary:
-    //	Gets or sets the encoding the console uses to read input.
-    //
-    // Returns:
-    //	The encoding used to read console input.
-    //
-    // Exceptions:
-    //	T:System.ArgumentNullException:
-    //	The property value in a set operation is null.
-    //
-    //	T:System.IO.IOException:
-    //	An error occurred during the execution of this operation.
-    //
-    //	T:System.Security.SecurityException:
-    //	Your application does not have permission to perform this operation.
-    /// <summary></summary>
-    public static Encoding InputEncoding { get; set; }
-
-    //
-    // Summary:
-    //	Gets or sets the encoding the console uses to write output.
-    //
-    // Returns:
-    //	The encoding used to write console output.
-    //
-    // Exceptions:
-    //	T:System.ArgumentNullException:
-    //	The property value in a set operation is null.
-    //
-    //	T:System.IO.IOException:
-    //	An error occurred during the execution of this operation.
-    //
-    //	T:System.Security.SecurityException:
-    //	Your application does not have permission to perform this operation.
-    /// <summary></summary>
-    public static Encoding OutputEncoding { get; set; }
-
-    //
-    // Summary:
-    //	Gets or sets the background color of the console.
-    //
-    // Returns:
-    //	A value that specifies the background color of the console; that is, the color
-    //	that appears behind each character. The default is black.
-    //
-    // Exceptions:
-    //	T:System.ArgumentException:
-    //	The color specified in a set operation is not a valid member of System.ConsoleColor.
-    //
-    //	T:System.Security.SecurityException:
-    //	The user does not have permission to perform this action.
-    //
-    //	T:System.IO.IOException:
-    //	An I/O error occurred.
-    private static readonly ConsoleColor _defaultBackgroundColor = ConsoleColor.Black;
-
-    /// <summary></summary>
-    public static ConsoleColor BackgroundColor { get; set; } = _defaultBackgroundColor;
-
-    //
-    // Summary:
-    //	Gets or sets the foreground color of the console.
-    //
-    // Returns:
-    //	A System.ConsoleColor that specifies the foreground color of the console; that
-    //	is, the color of each character that is displayed. The default is gray.
-    //
-    // Exceptions:
-    //	T:System.ArgumentException:
-    //	The color specified in a set operation is not a valid member of System.ConsoleColor.
-    //
-    //	T:System.Security.SecurityException:
-    //	The user does not have permission to perform this action.
-    //
-    //	T:System.IO.IOException:
-    //	An I/O error occurred.
-    private static readonly ConsoleColor _defaultForegroundColor = ConsoleColor.Gray;
-
-    /// <summary></summary>
-    public static ConsoleColor ForegroundColor { get; set; } = _defaultForegroundColor;
-
-    //
-    // Summary:
-    //	Gets or sets the height of the buffer area.
-    //
-    // Returns:
-    //	The current height, in rows, of the buffer area.
-    //
-    // Exceptions:
-    //	T:System.ArgumentOutOfRangeException:
-    //	The value in a set operation is less than or equal to zero.-or- The value in
-    //	a set operation is greater than or equal to System.Int16.MaxValue.-or- The value
-    //	in a set operation is less than System.Console.WindowTop + System.Console.WindowHeight.
-    //
-    //	T:System.Security.SecurityException:
-    //	The user does not have permission to perform this action.
-    //
-    //	T:System.IO.IOException:
-    //	An I/O error occurred.
-    /// <summary></summary>
-    public static int BufferHeight { get; set; } = HEIGHT;
-
-    //
-    // Summary:
-    //	Gets or sets the width of the buffer area.
-    //
-    // Returns:
-    //	The current width, in columns, of the buffer area.
-    //
-    // Exceptions:
-    //	T:System.ArgumentOutOfRangeException:
-    //	The value in a set operation is less than or equal to zero.-or- The value in
-    //	a set operation is greater than or equal to System.Int16.MaxValue.-or- The value
-    //	in a set operation is less than System.Console.WindowLeft + System.Console.WindowWidth.
-    //
-    //	T:System.Security.SecurityException:
-    //	The user does not have permission to perform this action.
-    //
-    //	T:System.IO.IOException:
-    //	An I/O error occurred.
-    /// <summary></summary>
-    public static int BufferWidth { get; set; } = WIDTH;
-
-    //
-    // Summary:
-    //	Gets or sets the height of the console window area.
-    //
-    // Returns:
-    //	The height of the console window measured in rows.
-    //
-    // Exceptions:
-    //	T:System.ArgumentOutOfRangeException:
-    //	The value of the System.Console.WindowWidth property or the value of the System.Console.WindowHeight
-    //	property is less than or equal to 0.-or-The value of the System.Console.WindowHeight
-    //	property plus the value of the System.Console.WindowTop property is greater than
-    //	or equal to System.Int16.MaxValue.-or-The value of the System.Console.WindowWidth
-    //	property or the value of the System.Console.WindowHeight property is greater
-    //	than the largest possible window width or height for the current screen resolution
-    //	and console font.
-    //
-    //	T:System.IO.IOException:
-    //	Error reading or writing information.
-    /// <summary></summary>
-    public static int WindowHeight { get; set; } = HEIGHT;
-
-    //
-    // Summary:
-    //	Gets or sets a value indicating whether the combination of the System.ConsoleModifiers.Control
-    //	modifier key and System.ConsoleKey.C console key (Ctrl+C) is treated as ordinary
-    //	input or as an interruption that is handled by the operating system.
-    //
-    // Returns:
-    //	true if Ctrl+C is treated as ordinary input; otherwise, false.
-    //
-    // Exceptions:
-    //	T:System.IO.IOException:
-    //	Unable to get or set the input mode of the console input buffer.
-    /// <summary></summary>
-    public static bool TreatControlCAsInput { get; set; }
-
-    //
-    // Summary:
-    //	Gets the largest possible number of console window columns, based on the current
-    //	font and screen resolution.
-    //
-    // Returns:
-    //	The width of the largest possible console window measured in columns.
-    /// <summary></summary>
-    public static int LargestWindowWidth { get; }
-
-    //
-    // Summary:
-    //	Gets the largest possible number of console window rows, based on the current
-    //	font and screen resolution.
-    //
-    // Returns:
-    //	The height of the largest possible console window measured in rows.
-    /// <summary></summary>
-    public static int LargestWindowHeight { get; }
-
-    //
-    // Summary:
-    //	Gets or sets the leftmost position of the console window area relative to the
-    //	screen buffer.
-    //
-    // Returns:
-    //	The leftmost console window position measured in columns.
-    //
-    // Exceptions:
-    //	T:System.ArgumentOutOfRangeException:
-    //	In a set operation, the value to be assigned is less than zero.-or-As a result
-    //	of the assignment, System.Console.WindowLeft plus System.Console.WindowWidth
-    //	would exceed System.Console.BufferWidth.
-    //
-    //	T:System.IO.IOException:
-    //	Error reading or writing information.
-    /// <summary></summary>
-    public static int WindowLeft { get; set; }
-
-    //
-    // Summary:
-    //	Gets or sets the top position of the console window area relative to the screen
-    //	buffer.
-    //
-    // Returns:
-    //	The uppermost console window position measured in rows.
-    //
-    // Exceptions:
-    //	T:System.ArgumentOutOfRangeException:
-    //	In a set operation, the value to be assigned is less than zero.-or-As a result
-    //	of the assignment, System.Console.WindowTop plus System.Console.WindowHeight
-    //	would exceed System.Console.BufferHeight.
-    //
-    //	T:System.IO.IOException:
-    //	Error reading or writing information.
-    /// <summary></summary>
-    public static int WindowTop { get; set; }
-
-    //
-    // Summary:
-    //	Gets or sets the column position of the cursor within the buffer area.
-    //
-    // Returns:
-    //	The current position, in columns, of the cursor.
-    //
-    // Exceptions:
-    //	T:System.ArgumentOutOfRangeException:
-    //	The value in a set operation is less than zero.-or- The value in a set operation
-    //	is greater than or equal to System.Console.BufferWidth.
-    //
-    //	T:System.Security.SecurityException:
-    //	The user does not have permission to perform this action.
-    //
-    //	T:System.IO.IOException:
-    //	An I/O error occurred.
-    /// <summary></summary>
-    public static int CursorLeft { get; set; }
-
-    //
-    // Summary:
-    //	Gets or sets the row position of the cursor within the buffer area.
-    //
-    // Returns:
-    //	The current position, in rows, of the cursor.
-    //
-    // Exceptions:
-    //	T:System.ArgumentOutOfRangeException:
-    //	The value in a set operation is less than zero.-or- The value in a set operation
-    //	is greater than or equal to System.Console.BufferHeight.
-    //
-    //	T:System.Security.SecurityException:
-    //	The user does not have permission to perform this action.
-    //
-    //	T:System.IO.IOException:
-    //	An I/O error occurred.
-    /// <summary></summary>
-    public static int CursorTop { get; set; }
-
-    //
-    // Summary:
-    //	Gets or sets the height of the cursor within a character cell.
-    //
-    // Returns:
-    //	The size of the cursor expressed as a percentage of the height of a character
-    //	cell. The property value ranges from 1 to 100.
-    //
-    // Exceptions:
-    //	T:System.ArgumentOutOfRangeException:
-    //	The value specified in a set operation is less than 1 or greater than 100.
-    //
-    //	T:System.Security.SecurityException:
-    //	The user does not have permission to perform this action.
-    //
-    //	T:System.IO.IOException:
-    //	An I/O error occurred.
-    /// <summary></summary>
-    public static int CursorSize { get; set; }
-
-    //
-    // Summary:
-    //	Gets or sets a value indicating whether the cursor is visible.
-    //
-    // Returns:
-    //	true if the cursor is visible; otherwise, false.
-    //
-    // Exceptions:
-    //	T:System.Security.SecurityException:
-    //	The user does not have permission to perform this action.
-    //
-    //	T:System.IO.IOException:
-    //	An I/O error occurred.
-    /// <summary></summary>
-    public static bool CursorVisible { get; set; }
-
-    //
-    // Summary:
-    //	Gets or sets the title to display in the console title bar.
-    //
-    // Returns:
-    //	The string to be displayed in the title bar of the console. The maximum length
-    //	of the title string is 24500 characters.
-    //
-    // Exceptions:
-    //	T:System.InvalidOperationException:
-    //	In a get operation, the retrieved title is longer than 24500 characters.
-    //
-    //	T:System.ArgumentOutOfRangeException:
-    //	In a set operation, the specified title is longer than 24500 characters.
-    //
-    //	T:System.ArgumentNullException:
-    //	In a set operation, the specified title is null.
-    //
-    //	T:System.IO.IOException:
-    //	An I/O error occurred.
-    /// <summary></summary>
-    public static string Title { get; set; }
-
-    //
-    // Summary:
-    //	Gets a value indicating whether a key press is available in the input stream.
-    //
-    // Returns:
-    //	true if a key press is available; otherwise, false.
-    //
-    // Exceptions:
-    //	T:System.IO.IOException:
-    //	An I/O error occurred.
-    //
-    //	T:System.InvalidOperationException:
-    //	Standard input is redirected to a file instead of the keyboard.
-    /// <summary></summary>
-    public static bool KeyAvailable { get; }
-
-    //
-    // Summary:
-    //	Gets a value that indicates whether input has been redirected from the standard
-    //	input stream.
-    //
-    // Returns:
-    //	true if input is redirected; otherwise, false.
-    /// <summary></summary>
-    public static bool IsInputRedirected { get; }
-
-    //
-    // Summary:
-    //	Plays the sound of a beep through the console speaker.
-    //
-    // Exceptions:
-    //	T:System.Security.HostProtectionException:
-    //	This method was executed on a server, such as SQL Server, that does not permit
-    //	access to a user interface.
-    /// <summary></summary>
-    public static void Beep () { throw new NotImplementedException (); }
-
-    //
-    // Summary:
-    //	Plays the sound of a beep of a specified frequency and duration through the console
-    //	speaker.
-    //
-    // Parameters:
-    //	frequency:
-    //	The frequency of the beep, ranging from 37 to 32767 hertz.
-    //
-    //	duration:
-    //	The duration of the beep measured in milliseconds.
-    //
-    // Exceptions:
-    //	T:System.ArgumentOutOfRangeException:
-    //	frequency is less than 37 or more than 32767 hertz.-or- duration is less than
-    //	or equal to zero.
-    //
-    //	T:System.Security.HostProtectionException:
-    //	This method was executed on a server, such as SQL Server, that does not permit
-    //	access to the console.
-    /// <summary></summary>
-    public static void Beep (int frequency, int duration) { throw new NotImplementedException (); }
-
-    //
-    // Summary:
-    //	Clears the console buffer and corresponding console window of display information.
-    //
-    // Exceptions:
-    //	T:System.IO.IOException:
-    //	An I/O error occurred.
-    private static char [,] _buffer = new char [WindowWidth, WindowHeight];
-
-    /// <summary></summary>
-    public static void Clear ()
-    {
-        _buffer = new char [BufferWidth, BufferHeight];
-        SetCursorPosition (0, 0);
-    }
-
-    //
-    // Summary:
-    //	Copies a specified source area of the screen buffer to a specified destination
-    //	area.
-    //
-    // Parameters:
-    //	sourceLeft:
-    //	The leftmost column of the source area.
-    //
-    //	sourceTop:
-    //	The topmost row of the source area.
-    //
-    //	sourceWidth:
-    //	The number of columns in the source area.
-    //
-    //	sourceHeight:
-    //	The number of rows in the source area.
-    //
-    //	targetLeft:
-    //	The leftmost column of the destination area.
-    //
-    //	targetTop:
-    //	The topmost row of the destination area.
-    //
-    // Exceptions:
-    //	T:System.ArgumentOutOfRangeException:
-    //	One or more of the parameters is less than zero.-or- sourceLeft or targetLeft
-    //	is greater than or equal to System.Console.BufferWidth.-or- sourceTop or targetTop
-    //	is greater than or equal to System.Console.BufferHeight.-or- sourceTop + sourceHeight
-    //	is greater than or equal to System.Console.BufferHeight.-or- sourceLeft + sourceWidth
-    //	is greater than or equal to System.Console.BufferWidth.
-    //
-    //	T:System.Security.SecurityException:
-    //	The user does not have permission to perform this action.
-    //
-    //	T:System.IO.IOException:
-    //	An I/O error occurred.
-    /// <summary></summary>
-    public static void MoveBufferArea (
-        int sourceLeft,
-        int sourceTop,
-        int sourceWidth,
-        int sourceHeight,
-        int targetLeft,
-        int targetTop
-    )
-    {
-        throw new NotImplementedException ();
-    }
-
-    //
-    // Summary:
-    //	Copies a specified source area of the screen buffer to a specified destination
-    //	area.
-    //
-    // Parameters:
-    //	sourceLeft:
-    //	The leftmost column of the source area.
-    //
-    //	sourceTop:
-    //	The topmost row of the source area.
-    //
-    //	sourceWidth:
-    //	The number of columns in the source area.
-    //
-    //	sourceHeight:
-    //	The number of rows in the source area.
-    //
-    //	targetLeft:
-    //	The leftmost column of the destination area.
-    //
-    //	targetTop:
-    //	The topmost row of the destination area.
-    //
-    //	sourceChar:
-    //	The character used to fill the source area.
-    //
-    //	sourceForeColor:
-    //	The foreground color used to fill the source area.
-    //
-    //	sourceBackColor:
-    //	The background color used to fill the source area.
-    //
-    // Exceptions:
-    //	T:System.ArgumentOutOfRangeException:
-    //	One or more of the parameters is less than zero.-or- sourceLeft or targetLeft
-    //	is greater than or equal to System.Console.BufferWidth.-or- sourceTop or targetTop
-    //	is greater than or equal to System.Console.BufferHeight.-or- sourceTop + sourceHeight
-    //	is greater than or equal to System.Console.BufferHeight.-or- sourceLeft + sourceWidth
-    //	is greater than or equal to System.Console.BufferWidth.
-    //
-    //	T:System.ArgumentException:
-    //	One or both of the color parameters is not a member of the System.ConsoleColor
-    //	enumeration.
-    //
-    //	T:System.Security.SecurityException:
-    //	The user does not have permission to perform this action.
-    //
-    //	T:System.IO.IOException:
-    //	An I/O error occurred.
-    //[SecuritySafeCritical]
-    /// <summary></summary>
-    public static void MoveBufferArea (
-        int sourceLeft,
-        int sourceTop,
-        int sourceWidth,
-        int sourceHeight,
-        int targetLeft,
-        int targetTop,
-        char sourceChar,
-        ConsoleColor sourceForeColor,
-        ConsoleColor sourceBackColor
-    )
-    {
-        throw new NotImplementedException ();
-    }
-
-    //
-    // Summary:
-    //	Acquires the standard error stream.
-    //
-    // Returns:
-    //	The standard error stream.
-    /// <summary></summary>
-    public static Stream OpenStandardError () { throw new NotImplementedException (); }
-
-    //
-    // Summary:
-    //	Acquires the standard error stream, which is set to a specified buffer size.
-    //
-    // Parameters:
-    //	bufferSize:
-    //	The internal stream buffer size.
-    //
-    // Returns:
-    //	The standard error stream.
-    //
-    // Exceptions:
-    //	T:System.ArgumentOutOfRangeException:
-    //	bufferSize is less than or equal to zero.
-    /// <summary></summary>
-    public static Stream OpenStandardError (int bufferSize) { throw new NotImplementedException (); }
-
-    //
-    // Summary:
-    //	Acquires the standard input stream, which is set to a specified buffer size.
-    //
-    // Parameters:
-    //	bufferSize:
-    //	The internal stream buffer size.
-    //
-    // Returns:
-    //	The standard input stream.
-    //
-    // Exceptions:
-    //	T:System.ArgumentOutOfRangeException:
-    //	bufferSize is less than or equal to zero.
-    /// <summary></summary>
-    public static Stream OpenStandardInput (int bufferSize) { throw new NotImplementedException (); }
-
-    //
-    // Summary:
-    //	Acquires the standard input stream.
-    //
-    // Returns:
-    //	The standard input stream.
-    /// <summary></summary>
-    public static Stream OpenStandardInput () { throw new NotImplementedException (); }
-
-    //
-    // Summary:
-    //	Acquires the standard output stream, which is set to a specified buffer size.
-    //
-    // Parameters:
-    //	bufferSize:
-    //	The internal stream buffer size.
-    //
-    // Returns:
-    //	The standard output stream.
-    //
-    // Exceptions:
-    //	T:System.ArgumentOutOfRangeException:
-    //	bufferSize is less than or equal to zero.
-    /// <summary></summary>
-    public static Stream OpenStandardOutput (int bufferSize) { throw new NotImplementedException (); }
-
-    //
-    // Summary:
-    //	Acquires the standard output stream.
-    //
-    // Returns:
-    //	The standard output stream.
-    /// <summary></summary>
-    public static Stream OpenStandardOutput () { throw new NotImplementedException (); }
-
-    //
-    // Summary:
-    //	Reads the next character from the standard input stream.
-    //
-    // Returns:
-    //	The next character from the input stream, or negative one (-1) if there are currently
-    //	no more characters to be read.
-    //
-    // Exceptions:
-    //	T:System.IO.IOException:
-    //	An I/O error occurred.
-    /// <summary></summary>
-    public static int Read () { throw new NotImplementedException (); }
-
-    //
-    // Summary:
-    //	Obtains the next character or function key pressed by the user. The pressed key
-    //	is optionally displayed in the console window.
-    //
-    // Parameters:
-    //	intercept:
-    //	Determines whether to display the pressed key in the console window. true to
-    //	not display the pressed key; otherwise, false.
-    //
-    // Returns:
-    //	An object that describes the System.ConsoleKey constant and Unicode character,
-    //	if any, that correspond to the pressed console key. The System.ConsoleKeyInfo
-    //	object also describes, in a bitwise combination of System.ConsoleModifiers values,
-    //	whether one or more Shift, Alt, or Ctrl modifier keys was pressed simultaneously
-    //	with the console key.
-    //
-    // Exceptions:
-    //	T:System.InvalidOperationException:
-    //	The System.Console.In property is redirected from some stream other than the
-    //	console.
-    //[SecuritySafeCritical]
-
-    /// <summary>A stack of keypresses to return when ReadKey is called.</summary>
-    public static Stack<ConsoleKeyInfo> MockKeyPresses = new ();
-
-    /// <summary>Helper to push a <see cref="KeyCode"/> onto <see cref="MockKeyPresses"/>.</summary>
-    /// <param name="key"></param>
-    public static void PushMockKeyPress (KeyCode key)
-    {
-        MockKeyPresses.Push (
-                             new ConsoleKeyInfo (
-                                                 (char)(key
-                                                        & ~KeyCode.CtrlMask
-                                                        & ~KeyCode.ShiftMask
-                                                        & ~KeyCode.AltMask),
-                                                 ConsoleKeyMapping.GetConsoleKeyInfoFromKeyCode (key).Key,
-                                                 key.HasFlag (KeyCode.ShiftMask),
-                                                 key.HasFlag (KeyCode.AltMask),
-                                                 key.HasFlag (KeyCode.CtrlMask)
-                                                )
-                            );
-    }
-
-    //
-    // Summary:
-    //	Obtains the next character or function key pressed by the user. The pressed key
-    //	is displayed in the console window.
-    //
-    // Returns:
-    //	An object that describes the System.ConsoleKey constant and Unicode character,
-    //	if any, that correspond to the pressed console key. The System.ConsoleKeyInfo
-    //	object also describes, in a bitwise combination of System.ConsoleModifiers values,
-    //	whether one or more Shift, Alt, or Ctrl modifier keys was pressed simultaneously
-    //	with the console key.
-    //
-    // Exceptions:
-    //	T:System.InvalidOperationException:
-    //	The System.Console.In property is redirected from some stream other than the
-    //	console.
-    /// <summary></summary>
-    public static ConsoleKeyInfo ReadKey () { throw new NotImplementedException (); }
-
-    //
-    // Summary:
-    //	Reads the next line of characters from the standard input stream.
-    //
-    // Returns:
-    //	The next line of characters from the input stream, or null if no more lines are
-    //	available.
-    //
-    // Exceptions:
-    //	T:System.IO.IOException:
-    //	An I/O error occurred.
-    //
-    //	T:System.OutOfMemoryException:
-    //	There is insufficient memory to allocate a buffer for the returned string.
-    //
-    //	T:System.ArgumentOutOfRangeException:
-    //	The number of characters in the next line of characters is greater than System.Int32.MaxValue.
-    /// <summary></summary>
-    public static string ReadLine () { throw new NotImplementedException (); }
-
-    //
-    // Summary:
-    //	Sets the foreground and background console colors to their defaults.
-    //
-    // Exceptions:
-    //	T:System.Security.SecurityException:
-    //	The user does not have permission to perform this action.
-    //
-    //	T:System.IO.IOException:
-    //	An I/O error occurred.
-    //[SecuritySafeCritical]
-    /// <summary></summary>
-    public static void ResetColor ()
-    {
-        BackgroundColor = _defaultBackgroundColor;
-        ForegroundColor = _defaultForegroundColor;
-    }
-
-    //
-    // Summary:
-    //	Sets the height and width of the screen buffer area to the specified values.
-    //
-    // Parameters:
-    //	width:
-    //	The width of the buffer area measured in columns.
-    //
-    //	height:
-    //	The height of the buffer area measured in rows.
-    //
-    // Exceptions:
-    //	T:System.ArgumentOutOfRangeException:
-    //	height or width is less than or equal to zero.-or- height or width is greater
-    //	than or equal to System.Int16.MaxValue.-or- width is less than System.Console.WindowLeft
-    //	+ System.Console.WindowWidth.-or- height is less than System.Console.WindowTop
-    //	+ System.Console.WindowHeight.
-    //
-    //	T:System.Security.SecurityException:
-    //	The user does not have permission to perform this action.
-    //
-    //	T:System.IO.IOException:
-    //	An I/O error occurred.
-    //[SecuritySafeCritical]
-    /// <summary></summary>
-    public static void SetBufferSize (int width, int height)
-    {
-        BufferWidth = width;
-        BufferHeight = height;
-        _buffer = new char [BufferWidth, BufferHeight];
-    }
-
-    //
-    // Summary:
-    //	Sets the position of the cursor.
-    //
-    // Parameters:
-    //	left:
-    //	The column position of the cursor. Columns are numbered from left to right starting
-    //	at 0.
-    //
-    //	top:
-    //	The row position of the cursor. Rows are numbered from top to bottom starting
-    //	at 0.
-    //
-    // Exceptions:
-    //	T:System.ArgumentOutOfRangeException:
-    //	left or top is less than zero.-or- left is greater than or equal to System.Console.BufferWidth.-or-
-    //	top is greater than or equal to System.Console.BufferHeight.
-    //
-    //	T:System.Security.SecurityException:
-    //	The user does not have permission to perform this action.
-    //
-    //	T:System.IO.IOException:
-    //	An I/O error occurred.
-    //[SecuritySafeCritical]
-    /// <summary></summary>
-    public static void SetCursorPosition (int left, int top)
-    {
-        CursorLeft = left;
-        CursorTop = top;
-        WindowLeft = Math.Max (Math.Min (left, BufferWidth - WindowWidth), 0);
-        WindowTop = Math.Max (Math.Min (top, BufferHeight - WindowHeight), 0);
-    }
-
-    //
-    // Summary:
-    //	Sets the System.Console.Error property to the specified System.IO.TextWriter
-    //	object.
-    //
-    // Parameters:
-    //	newError:
-    //	A stream that is the new standard error output.
-    //
-    // Exceptions:
-    //	T:System.ArgumentNullException:
-    //	newError is null.
-    //
-    //	T:System.Security.SecurityException:
-    //	The caller does not have the required permission.
-    //[SecuritySafeCritical]
-    /// <summary></summary>
-    public static void SetError (TextWriter newError) { throw new NotImplementedException (); }
-
-    //
-    // Summary:
-    //	Sets the System.Console.In property to the specified System.IO.TextReader object.
-    //
-    // Parameters:
-    //	newIn:
-    //	A stream that is the new standard input.
-    //
-    // Exceptions:
-    //	T:System.ArgumentNullException:
-    //	newIn is null.
-    //
-    //	T:System.Security.SecurityException:
-    //	The caller does not have the required permission.
-    //[SecuritySafeCritical]
-    /// <summary></summary>
-    public static void SetIn (TextReader newIn) { throw new NotImplementedException (); }
-
-    //
-    // Summary:
-    //	Sets the System.Console.Out property to the specified System.IO.TextWriter object.
-    //
-    // Parameters:
-    //	newOut:
-    //	A stream that is the new standard output.
-    //
-    // Exceptions:
-    //	T:System.ArgumentNullException:
-    //	newOut is null.
-    //
-    //	T:System.Security.SecurityException:
-    //	The caller does not have the required permission.
-    //[SecuritySafeCritical]
-    /// <summary></summary>
-    /// <param name="newOut"></param>
-    public static void SetOut (TextWriter newOut) { throw new NotImplementedException (); }
-
-    //
-    // Summary:
-    //	Sets the position of the console window relative to the screen buffer.
-    //
-    // Parameters:
-    //	left:
-    //	The column position of the upper left corner of the console window.
-    //
-    //	top:
-    //	The row position of the upper left corner of the console window.
-    //
-    // Exceptions:
-    //	T:System.ArgumentOutOfRangeException:
-    //	left or top is less than zero.-or- left + System.Console.WindowWidth is greater
-    //	than System.Console.BufferWidth.-or- top + System.Console.WindowHeight is greater
-    //	than System.Console.BufferHeight.
-    //
-    //	T:System.Security.SecurityException:
-    //	The user does not have permission to perform this action.
-    //
-    //	T:System.IO.IOException:
-    //	An I/O error occurred.
-    //[SecuritySafeCritical]
-    /// <summary></summary>
-    /// <param name="left"></param>
-    /// <param name="top"></param>
-    public static void SetWindowPosition (int left, int top)
-    {
-        WindowLeft = left;
-        WindowTop = top;
-    }
-
-    //
-    // Summary:
-    //	Sets the height and width of the console window to the specified values.
-    //
-    // Parameters:
-    //	width:
-    //	The width of the console window measured in columns.
-    //
-    //	height:
-    //	The height of the console window measured in rows.
-    //
-    // Exceptions:
-    //	T:System.ArgumentOutOfRangeException:
-    //	width or height is less than or equal to zero.-or- width plus System.Console.WindowLeft
-    //	or height plus System.Console.WindowTop is greater than or equal to System.Int16.MaxValue.
-    //	-or- width or height is greater than the largest possible window width or height
-    //	for the current screen resolution and console font.
-    //
-    //	T:System.Security.SecurityException:
-    //	The user does not have permission to perform this action.
-    //
-    //	T:System.IO.IOException:
-    //	An I/O error occurred.
-    //[SecuritySafeCritical]
-    /// <summary></summary>
-    /// <param name="width"></param>
-    /// <param name="height"></param>
-    public static void SetConsoleSize (int width, int height)
-    {
-        WindowWidth = width;
-        WindowHeight = height;
-    }
-
-    //
-    // Summary:
-    //	Writes the specified string value to the standard output stream.
-    //
-    // Parameters:
-    //	value:
-    //	The value to write.
-    //
-    // Exceptions:
-    //	T:System.IO.IOException:
-    //	An I/O error occurred.
-    /// <summary></summary>
-    /// <param name="value"></param>
-    public static void Write (string value) { throw new NotImplementedException (); }
-
-    //
-    // Summary:
-    //	Writes the text representation of the specified object to the standard output
-    //	stream.
-    //
-    // Parameters:
-    //	value:
-    //	The value to write, or null.
-    //
-    // Exceptions:
-    //	T:System.IO.IOException:
-    //	An I/O error occurred.
-    /// <summary></summary>
-    /// <param name="value"></param>
-    public static void Write (object value)
-    {
-        if (value is Rune rune)
-        {
-            Write ((char)rune.Value);
-        }
-        else
-        {
-            throw new NotImplementedException ();
-        }
-    }
-
-    //
-    // Summary:
-    //	Writes the text representation of the specified 64-bit unsigned integer value
-    //	to the standard output stream.
-    //
-    // Parameters:
-    //	value:
-    //	The value to write.
-    //
-    // Exceptions:
-    //	T:System.IO.IOException:
-    //	An I/O error occurred.
-    //[CLSCompliant (false)]
-    /// <summary></summary>
-    /// <param name="value"></param>
-    public static void Write (ulong value) { throw new NotImplementedException (); }
-
-    //
-    // Summary:
-    //	Writes the text representation of the specified 64-bit signed integer value to
-    //	the standard output stream.
-    //
-    // Parameters:
-    //	value:
-    //	The value to write.
-    //
-    // Exceptions:
-    //	T:System.IO.IOException:
-    //	An I/O error occurred.
-    /// <summary></summary>
-    /// <param name="value"></param>
-    public static void Write (long value) { throw new NotImplementedException (); }
-
-    //
-    // Summary:
-    //	Writes the text representation of the specified objects to the standard output
-    //	stream using the specified format information.
-    //
-    // Parameters:
-    //	format:
-    //	A composite format string (see Remarks).
-    //
-    //	arg0:
-    //	The first object to write using format.
-    //
-    //	arg1:
-    //	The second object to write using format.
-    //
-    // Exceptions:
-    //	T:System.IO.IOException:
-    //	An I/O error occurred.
-    //
-    //	T:System.ArgumentNullException:
-    //	format is null.
-    //
-    //	T:System.FormatException:
-    //	The format specification in format is invalid.
-    /// <summary></summary>
-    /// <param name="format"></param>
-    /// <param name="arg0"></param>
-    /// <param name="arg1"></param>
-    public static void Write (string format, object arg0, object arg1) { throw new NotImplementedException (); }
-
-    //
-    // Summary:
-    //	Writes the text representation of the specified 32-bit signed integer value to
-    //	the standard output stream.
-    //
-    // Parameters:
-    //	value:
-    //	The value to write.
-    //
-    // Exceptions:
-    //	T:System.IO.IOException:
-    //	An I/O error occurred.
-    /// <summary></summary>
-    /// <param name="value"></param>
-    public static void Write (int value) { throw new NotImplementedException (); }
-
-    //
-    // Summary:
-    //	Writes the text representation of the specified object to the standard output
-    //	stream using the specified format information.
-    //
-    // Parameters:
-    //	format:
-    //	A composite format string (see Remarks).
-    //
-    //	arg0:
-    //	An object to write using format.
-    //
-    // Exceptions:
-    //	T:System.IO.IOException:
-    //	An I/O error occurred.
-    //
-    //	T:System.ArgumentNullException:
-    //	format is null.
-    //
-    //	T:System.FormatException:
-    //	The format specification in format is invalid.
-    /// <summary></summary>
-    /// <param name="format"></param>
-    /// <param name="arg0"></param>
-    public static void Write (string format, object arg0) { throw new NotImplementedException (); }
-
-    //
-    // Summary:
-    //	Writes the text representation of the specified 32-bit unsigned integer value
-    //	to the standard output stream.
-    //
-    // Parameters:
-    //	value:
-    //	The value to write.
-    //
-    // Exceptions:
-    //	T:System.IO.IOException:
-    //	An I/O error occurred.
-    //[CLSCompliant (false)]
-    /// <summary></summary>
-    /// <param name="value"></param>
-    public static void Write (uint value) { throw new NotImplementedException (); }
-
-    //[CLSCompliant (false)]
-    /// <summary></summary>
-    /// <param name="format"></param>
-    /// <param name="arg0"></param>
-    /// <param name="arg1"></param>
-    /// <param name="arg2"></param>
-    /// <param name="arg3"></param>
-    public static void Write (string format, object arg0, object arg1, object arg2, object arg3) { throw new NotImplementedException (); }
-
-    //
-    // Summary:
-    //	Writes the text representation of the specified array of objects to the standard
-    //	output stream using the specified format information.
-    //
-    // Parameters:
-    //	format:
-    //	A composite format string (see Remarks).
-    //
-    //	arg:
-    //	An array of objects to write using format.
-    //
-    // Exceptions:
-    //	T:System.IO.IOException:
-    //	An I/O error occurred.
-    //
-    //	T:System.ArgumentNullException:
-    //	format or arg is null.
-    //
-    //	T:System.FormatException:
-    //	The format specification in format is invalid.
-    /// <summary></summary>
-    /// <param name="format"></param>
-    /// <param name="arg"></param>
-    public static void Write (string format, params object [] arg) { throw new NotImplementedException (); }
-
-    //
-    // Summary:
-    //	Writes the text representation of the specified Boolean value to the standard
-    //	output stream.
-    //
-    // Parameters:
-    //	value:
-    //	The value to write.
-    //
-    // Exceptions:
-    //	T:System.IO.IOException:
-    //	An I/O error occurred.
-    /// <summary></summary>
-    /// <param name="value"></param>
-    public static void Write (bool value) { throw new NotImplementedException (); }
-
-    //
-    // Summary:
-    //	Writes the specified Unicode character value to the standard output stream.
-    //
-    // Parameters:
-    //	value:
-    //	The value to write.
-    //
-    // Exceptions:
-    //	T:System.IO.IOException:
-    //	An I/O error occurred.
-    /// <summary></summary>
-    /// <param name="value"></param>
-    public static void Write (char value) { _buffer [CursorLeft, CursorTop] = value; }
-
-    //
-    // Summary:
-    //	Writes the specified array of Unicode characters to the standard output stream.
-    //
-    // Parameters:
-    //	buffer:
-    //	A Unicode character array.
-    //
-    // Exceptions:
-    //	T:System.IO.IOException:
-    //	An I/O error occurred.
-    /// <summary></summary>
-    /// <param name="buffer"></param>
-    public static void Write (char [] buffer)
-    {
-        _buffer [CursorLeft, CursorTop] = (char)0;
-
-        foreach (char ch in buffer)
-        {
-            _buffer [CursorLeft, CursorTop] += ch;
-        }
-    }
-
-    //
-    // Summary:
-    //	Writes the specified subarray of Unicode characters to the standard output stream.
-    //
-    // Parameters:
-    //	buffer:
-    //	An array of Unicode characters.
-    //
-    //	index:
-    //	The starting position in buffer.
-    //
-    //	count:
-    //	The number of characters to write.
-    //
-    // Exceptions:
-    //	T:System.ArgumentNullException:
-    //	buffer is null.
-    //
-    //	T:System.ArgumentOutOfRangeException:
-    //	index or count is less than zero.
-    //
-    //	T:System.ArgumentException:
-    //	index plus count specify a position that is not within buffer.
-    //
-    //	T:System.IO.IOException:
-    //	An I/O error occurred.
-    /// <summary></summary>
-    /// <param name="buffer"></param>
-    /// <param name="index"></param>
-    /// <param name="count"></param>
-    public static void Write (char [] buffer, int index, int count) { throw new NotImplementedException (); }
-
-    //
-    // Summary:
-    //	Writes the text representation of the specified objects to the standard output
-    //	stream using the specified format information.
-    //
-    // Parameters:
-    //	format:
-    //	A composite format string (see Remarks).
-    //
-    //	arg0:
-    //	The first object to write using format.
-    //
-    //	arg1:
-    //	The second object to write using format.
-    //
-    //	arg2:
-    //	The third object to write using format.
-    //
-    // Exceptions:
-    //	T:System.IO.IOException:
-    //	An I/O error occurred.
-    //
-    //	T:System.ArgumentNullException:
-    //	format is null.
-    //
-    //	T:System.FormatException:
-    //	The format specification in format is invalid.
-    /// <summary></summary>
-    /// <param name="format"></param>
-    /// <param name="arg0"></param>
-    /// <param name="arg1"></param>
-    /// <param name="arg2"></param>
-    public static void Write (string format, object arg0, object arg1, object arg2) { throw new NotImplementedException (); }
-
-    //
-    // Summary:
-    //	Writes the text representation of the specified System.Decimal value to the standard
-    //	output stream.
-    //
-    // Parameters:
-    //	value:
-    //	The value to write.
-    //
-    // Exceptions:
-    //	T:System.IO.IOException:
-    //	An I/O error occurred.
-    /// <summary></summary>
-    /// <param name="value"></param>
-    public static void Write (decimal value) { throw new NotImplementedException (); }
-
-    //
-    // Summary:
-    //	Writes the text representation of the specified single-precision floating-point
-    //	value to the standard output stream.
-    //
-    // Parameters:
-    //	value:
-    //	The value to write.
-    //
-    // Exceptions:
-    //	T:System.IO.IOException:
-    //	An I/O error occurred.
-    /// <summary></summary>
-    /// <param name="value"></param>
-    public static void Write (float value) { throw new NotImplementedException (); }
-
-    //
-    // Summary:
-    //	Writes the text representation of the specified double-precision floating-point
-    //	value to the standard output stream.
-    //
-    // Parameters:
-    //	value:
-    //	The value to write.
-    //
-    // Exceptions:
-    //	T:System.IO.IOException:
-    //	An I/O error occurred.
-    /// <summary></summary>
-    /// <param name="value"></param>
-    public static void Write (double value) { throw new NotImplementedException (); }
-
-    //
-    // Summary:
-    //	Writes the current line terminator to the standard output stream.
-    //
-    // Exceptions:
-    //	T:System.IO.IOException:
-    //	An I/O error occurred.
-    /// <summary></summary>
-    public static void WriteLine () { throw new NotImplementedException (); }
-
-    //
-    // Summary:
-    //	Writes the text representation of the specified single-precision floating-point
-    //	value, followed by the current line terminator, to the standard output stream.
-    //
-    // Parameters:
-    //	value:
-    //	The value to write.
-    //
-    // Exceptions:
-    //	T:System.IO.IOException:
-    //	An I/O error occurred.
-    /// <summary></summary>
-    /// <param name="value"></param>
-    public static void WriteLine (float value) { throw new NotImplementedException (); }
-
-    //
-    // Summary:
-    //	Writes the text representation of the specified 32-bit signed integer value,
-    //	followed by the current line terminator, to the standard output stream.
-    //
-    // Parameters:
-    //	value:
-    //	The value to write.
-    //
-    // Exceptions:
-    //	T:System.IO.IOException:
-    //	An I/O error occurred.
-    /// <summary></summary>
-    /// <param name="value"></param>
-    public static void WriteLine (int value) { throw new NotImplementedException (); }
-
-    //
-    // Summary:
-    //	Writes the text representation of the specified 32-bit unsigned integer value,
-    //	followed by the current line terminator, to the standard output stream.
-    //
-    // Parameters:
-    //	value:
-    //	The value to write.
-    //
-    // Exceptions:
-    //	T:System.IO.IOException:
-    //	An I/O error occurred.
-    //[CLSCompliant (false)]
-    /// <summary></summary>
-    /// <param name="value"></param>
-    public static void WriteLine (uint value) { throw new NotImplementedException (); }
-
-    //
-    // Summary:
-    //	Writes the text representation of the specified 64-bit signed integer value,
-    //	followed by the current line terminator, to the standard output stream.
-    //
-    // Parameters:
-    //	value:
-    //	The value to write.
-    //
-    // Exceptions:
-    //	T:System.IO.IOException:
-    //	An I/O error occurred.
-    /// <summary></summary>
-    /// <param name="value"></param>
-    public static void WriteLine (long value) { throw new NotImplementedException (); }
-
-    //
-    // Summary:
-    //	Writes the text representation of the specified 64-bit unsigned integer value,
-    //	followed by the current line terminator, to the standard output stream.
-    //
-    // Parameters:
-    //	value:
-    //	The value to write.
-    //
-    // Exceptions:
-    //	T:System.IO.IOException:
-    //	An I/O error occurred.
-    //[CLSCompliant (false)]
-    /// <summary></summary>
-    /// <param name="value"></param>
-    public static void WriteLine (ulong value) { throw new NotImplementedException (); }
-
-    //
-    // Summary:
-    //	Writes the text representation of the specified object, followed by the current
-    //	line terminator, to the standard output stream.
-    //
-    // Parameters:
-    //	value:
-    //	The value to write.
-    //
-    // Exceptions:
-    //	T:System.IO.IOException:
-    //	An I/O error occurred.
-    /// <summary></summary>
-    /// <param name="value"></param>
-    public static void WriteLine (object value) { throw new NotImplementedException (); }
-
-    //
-    // Summary:
-    //	Writes the specified string value, followed by the current line terminator, to
-    //	the standard output stream.
-    //
-    // Parameters:
-    //	value:
-    //	The value to write.
-    //
-    // Exceptions:
-    //	T:System.IO.IOException:
-    //	An I/O error occurred.
-    /// <summary></summary>
-    /// <param name="value"></param>
-    public static void WriteLine (string value) { throw new NotImplementedException (); }
-
-    //
-    // Summary:
-    //	Writes the text representation of the specified object, followed by the current
-    //	line terminator, to the standard output stream using the specified format information.
-    //
-    // Parameters:
-    //	format:
-    //	A composite format string (see Remarks).
-    //
-    //	arg0:
-    //	An object to write using format.
-    //
-    // Exceptions:
-    //	T:System.IO.IOException:
-    //	An I/O error occurred.
-    //
-    //	T:System.ArgumentNullException:
-    //	format is null.
-    //
-    //	T:System.FormatException:
-    //	The format specification in format is invalid.
-    /// <summary></summary>
-    /// <param name="format"></param>
-    /// <param name="arg0"></param>
-    public static void WriteLine (string format, object arg0) { throw new NotImplementedException (); }
-
-    //
-    // Summary:
-    //	Writes the text representation of the specified objects, followed by the current
-    //	line terminator, to the standard output stream using the specified format information.
-    //
-    // Parameters:
-    //	format:
-    //	A composite format string (see Remarks).
-    //
-    //	arg0:
-    //	The first object to write using format.
-    //
-    //	arg1:
-    //	The second object to write using format.
-    //
-    //	arg2:
-    //	The third object to write using format.
-    //
-    // Exceptions:
-    //	T:System.IO.IOException:
-    //	An I/O error occurred.
-    //
-    //	T:System.ArgumentNullException:
-    //	format is null.
-    //
-    //	T:System.FormatException:
-    //	The format specification in format is invalid.
-    /// <summary></summary>
-    /// <param name="format"></param>
-    /// <param name="arg0"></param>
-    /// <param name="arg1"></param>
-    /// <param name="arg2"></param>
-    public static void WriteLine (string format, object arg0, object arg1, object arg2) { throw new NotImplementedException (); }
-
-    //[CLSCompliant (false)]
-    /// <summary></summary>
-    /// <param name="format"></param>
-    /// <param name="arg0"></param>
-    /// <param name="arg1"></param>
-    /// <param name="arg2"></param>
-    /// <param name="arg3"></param>
-    public static void WriteLine (string format, object arg0, object arg1, object arg2, object arg3) { throw new NotImplementedException (); }
-
-    //
-    // Summary:
-    //	Writes the text representation of the specified array of objects, followed by
-    //	the current line terminator, to the standard output stream using the specified
-    //	format information.
-    //
-    // Parameters:
-    //	format:
-    //	A composite format string (see Remarks).
-    //
-    //	arg:
-    //	An array of objects to write using format.
-    //
-    // Exceptions:
-    //	T:System.IO.IOException:
-    //	An I/O error occurred.
-    //
-    //	T:System.ArgumentNullException:
-    //	format or arg is null.
-    //
-    //	T:System.FormatException:
-    //	The format specification in format is invalid.
-    /// <summary></summary>
-    /// <param name="format"></param>
-    /// <param name="arg"></param>
-    public static void WriteLine (string format, params object [] arg) { throw new NotImplementedException (); }
-
-    //
-    // Summary:
-    //	Writes the specified subarray of Unicode characters, followed by the current
-    //	line terminator, to the standard output stream.
-    //
-    // Parameters:
-    //	buffer:
-    //	An array of Unicode characters.
-    //
-    //	index:
-    //	The starting position in buffer.
-    //
-    //	count:
-    //	The number of characters to write.
-    //
-    // Exceptions:
-    //	T:System.ArgumentNullException:
-    //	buffer is null.
-    //
-    //	T:System.ArgumentOutOfRangeException:
-    //	index or count is less than zero.
-    //
-    //	T:System.ArgumentException:
-    //	index plus count specify a position that is not within buffer.
-    //
-    //	T:System.IO.IOException:
-    //	An I/O error occurred.
-    /// <summary></summary>
-    /// <param name="buffer"></param>
-    /// <param name="index"></param>
-    /// <param name="count"></param>
-    public static void WriteLine (char [] buffer, int index, int count) { throw new NotImplementedException (); }
-
-    //
-    // Summary:
-    //	Writes the text representation of the specified System.Decimal value, followed
-    //	by the current line terminator, to the standard output stream.
-    //
-    // Parameters:
-    //	value:
-    //	The value to write.
-    //
-    // Exceptions:
-    //	T:System.IO.IOException:
-    //	An I/O error occurred.
-    /// <summary></summary>
-    /// <param name="value"></param>
-    public static void WriteLine (decimal value) { throw new NotImplementedException (); }
-
-    //
-    // Summary:
-    //	Writes the specified array of Unicode characters, followed by the current line
-    //	terminator, to the standard output stream.
-    //
-    // Parameters:
-    //	buffer:
-    //	A Unicode character array.
-    //
-    // Exceptions:
-    //	T:System.IO.IOException:
-    //	An I/O error occurred.
-    /// <summary></summary>
-    /// <param name="buffer"></param>
-    public static void WriteLine (char [] buffer) { throw new NotImplementedException (); }
-
-    //
-    // Summary:
-    //	Writes the specified Unicode character, followed by the current line terminator,
-    //	value to the standard output stream.
-    //
-    // Parameters:
-    //	value:
-    //	The value to write.
-    //
-    // Exceptions:
-    //	T:System.IO.IOException:
-    //	An I/O error occurred.
-    /// <summary></summary>
-    /// <param name="value"></param>
-    public static void WriteLine (char value) { throw new NotImplementedException (); }
-
-    //
-    // Summary:
-    //	Writes the text representation of the specified Boolean value, followed by the
-    //	current line terminator, to the standard output stream.
-    //
-    // Parameters:
-    //	value:
-    //	The value to write.
-    //
-    // Exceptions:
-    //	T:System.IO.IOException:
-    //	An I/O error occurred.
-    /// <summary></summary>
-    /// <param name="value"></param>
-    public static void WriteLine (bool value) { throw new NotImplementedException (); }
-
-    //
-    // Summary:
-    //	Writes the text representation of the specified objects, followed by the current
-    //	line terminator, to the standard output stream using the specified format information.
-    //
-    // Parameters:
-    //	format:
-    //	A composite format string (see Remarks).
-    //
-    //	arg0:
-    //	The first object to write using format.
-    //
-    //	arg1:
-    //	The second object to write using format.
-    //
-    // Exceptions:
-    //	T:System.IO.IOException:
-    //	An I/O error occurred.
-    //
-    //	T:System.ArgumentNullException:
-    //	format is null.
-    //
-    //	T:System.FormatException:
-    //	The format specification in format is invalid.
-    /// <summary></summary>
-    /// <param name="format"></param>
-    /// <param name="arg0"></param>
-    /// <param name="arg1"></param>
-    public static void WriteLine (string format, object arg0, object arg1) { throw new NotImplementedException (); }
-
-    //
-    // Summary:
-    //	Writes the text representation of the specified double-precision floating-point
-    //	value, followed by the current line terminator, to the standard output stream.
-    //
-    // Parameters:
-    //	value:
-    //	The value to write.
-    //
-    // Exceptions:
-    //	T:System.IO.IOException:
-    //	An I/O error occurred.
-    /// <summary></summary>
-    /// <param name="value"></param>
-    public static void WriteLine (double value) { throw new NotImplementedException (); }
-}

+ 0 - 42
Terminal.Gui/Drivers/FakeDriver/FakeConsoleInput.cs

@@ -1,42 +0,0 @@
-#nullable enable
-using System.Collections.Concurrent;
-
-namespace Terminal.Gui.Drivers;
-
-/// <summary>
-/// Fake console input for testing that can return predefined input or wait indefinitely.
-/// </summary>
-public class FakeConsoleInput : ConsoleInput<ConsoleKeyInfo>
-{
-    private readonly ConcurrentQueue<ConsoleKeyInfo>? _predefinedInput;
-
-    /// <summary>
-    /// Creates a new FakeConsoleInput with optional predefined input.
-    /// </summary>
-    /// <param name="predefinedInput">Optional queue of predefined input to return.</param>
-    public FakeConsoleInput (ConcurrentQueue<ConsoleKeyInfo>? predefinedInput = null)
-    {
-        _predefinedInput = predefinedInput;
-    }
-
-    /// <inheritdoc/>
-    protected override bool Peek ()
-    {
-        if (_predefinedInput != null && !_predefinedInput.IsEmpty)
-        {
-            return true;
-        }
-
-        // No input available
-        return false;
-    }
-
-    /// <inheritdoc/>
-    protected override IEnumerable<ConsoleKeyInfo> Read ()
-    {
-        if (_predefinedInput != null && _predefinedInput.TryDequeue (out ConsoleKeyInfo key))
-        {
-            yield return key;
-        }
-    }
-}

+ 0 - 379
Terminal.Gui/Drivers/FakeDriver/FakeDriver.cs

@@ -1,379 +0,0 @@
-#nullable enable
-
-//
-// FakeDriver.cs: A fake IConsoleDriver for unit tests. 
-//
-
-using System.Diagnostics;
-using System.Runtime.InteropServices;
-
-namespace Terminal.Gui.Drivers;
-
-/// <summary>Implements a mock IConsoleDriver for unit testing</summary>
-public class FakeDriver : ConsoleDriver
-{
-#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
-
-    public class Behaviors
-    {
-        public Behaviors (
-            bool useFakeClipboard = false,
-            bool fakeClipboardAlwaysThrowsNotSupportedException = false,
-            bool fakeClipboardIsSupportedAlwaysTrue = false
-        )
-        {
-            UseFakeClipboard = useFakeClipboard;
-            FakeClipboardAlwaysThrowsNotSupportedException = fakeClipboardAlwaysThrowsNotSupportedException;
-            FakeClipboardIsSupportedAlwaysFalse = fakeClipboardIsSupportedAlwaysTrue;
-
-            // double check usage is correct
-            Debug.Assert (!useFakeClipboard && !fakeClipboardAlwaysThrowsNotSupportedException);
-            Debug.Assert (!useFakeClipboard && !fakeClipboardIsSupportedAlwaysTrue);
-        }
-
-        public bool FakeClipboardAlwaysThrowsNotSupportedException { get; internal set; }
-        public bool FakeClipboardIsSupportedAlwaysFalse { get; internal set; }
-        public bool UseFakeClipboard { get; internal set; }
-    }
-
-    public static Behaviors FakeBehaviors { get; } = new ();
-    public override bool SupportsTrueColor => false;
-
-    /// <inheritdoc/>
-    public override void WriteRaw (string ansi) { }
-
-    public FakeDriver ()
-    {
-        // FakeDriver implies UnitTests
-        RunningUnitTests = true;
-
-        //base.Cols = FakeConsole.WindowWidth = FakeConsole.BufferWidth = FakeConsole.WIDTH;
-        //base.Rows = FakeConsole.WindowHeight = FakeConsole.BufferHeight = FakeConsole.HEIGHT;
-
-        if (FakeBehaviors.UseFakeClipboard)
-        {
-            Clipboard = new FakeClipboard (
-                                           FakeBehaviors.FakeClipboardAlwaysThrowsNotSupportedException,
-                                           FakeBehaviors.FakeClipboardIsSupportedAlwaysFalse
-                                          );
-        }
-        else
-        {
-            if (RuntimeInformation.IsOSPlatform (OSPlatform.Windows))
-            {
-                Clipboard = new WindowsClipboard ();
-            }
-            else if (RuntimeInformation.IsOSPlatform (OSPlatform.OSX))
-            {
-                Clipboard = new MacOSXClipboard ();
-            }
-            else
-            {
-                if (PlatformDetection.IsWSLPlatform ())
-                {
-                    Clipboard = new WSLClipboard ();
-                }
-                else
-                {
-                    Clipboard = new UnixClipboard ();
-                }
-            }
-        }
-    }
-
-    public override void End ()
-    {
-        FakeConsole.ResetColor ();
-        FakeConsole.Clear ();
-    }
-
-    public override void Init ()
-    {
-        FakeConsole.MockKeyPresses.Clear ();
-
-        //Cols = FakeConsole.WindowWidth = FakeConsole.BufferWidth = FakeConsole.WIDTH;
-        //Rows = FakeConsole.WindowHeight = FakeConsole.BufferHeight = FakeConsole.HEIGHT;
-        FakeConsole.Clear ();
-
-        SetScreenSize (80,25);
-        ResizeScreen ();
-        ClearContents ();
-        CurrentAttribute = new (Color.White, Color.Black);
-    }
-
-    public override bool UpdateScreen ()
-    {
-        var updated = false;
-
-        int savedRow = FakeConsole.CursorTop;
-        int savedCol = FakeConsole.CursorLeft;
-        bool savedCursorVisible = FakeConsole.CursorVisible;
-
-        var top = 0;
-        var left = 0;
-        int rows = Rows;
-        int cols = Cols;
-        var output = new StringBuilder ();
-        var redrawAttr = new Attribute ();
-        int lastCol = -1;
-
-        for (int row = top; row < rows; row++)
-        {
-            if (!_dirtyLines! [row])
-            {
-                continue;
-            }
-
-            updated = true;
-
-            FakeConsole.CursorTop = row;
-            FakeConsole.CursorLeft = 0;
-
-            _dirtyLines [row] = false;
-            output.Clear ();
-
-            for (int col = left; col < cols; col++)
-            {
-                lastCol = -1;
-                var outputWidth = 0;
-
-                for (; col < cols; col++)
-                {
-                    if (!Contents! [row, col].IsDirty)
-                    {
-                        if (output.Length > 0)
-                        {
-                            WriteToConsole (output, ref lastCol, row, ref outputWidth);
-                        }
-                        else if (lastCol == -1)
-                        {
-                            lastCol = col;
-                        }
-
-                        if (lastCol + 1 < cols)
-                        {
-                            lastCol++;
-                        }
-
-                        continue;
-                    }
-
-                    if (lastCol == -1)
-                    {
-                        lastCol = col;
-                    }
-
-                    Attribute attr = Contents [row, col].Attribute!.Value;
-
-                    // Performance: Only send the escape sequence if the attribute has changed.
-                    if (attr != redrawAttr)
-                    {
-                        redrawAttr = attr;
-                        FakeConsole.ForegroundColor = (ConsoleColor)attr.Foreground.GetClosestNamedColor16 ();
-                        FakeConsole.BackgroundColor = (ConsoleColor)attr.Background.GetClosestNamedColor16 ();
-                    }
-
-                    outputWidth++;
-                    Rune rune = Contents [row, col].Rune;
-                    output.Append (rune.ToString ());
-
-                    if (rune.IsSurrogatePair () && rune.GetColumns () < 2)
-                    {
-                        WriteToConsole (output, ref lastCol, row, ref outputWidth);
-                        FakeConsole.CursorLeft--;
-                    }
-
-                    Contents [row, col].IsDirty = false;
-                }
-            }
-
-            if (output.Length > 0)
-            {
-                FakeConsole.CursorTop = row;
-                FakeConsole.CursorLeft = lastCol;
-
-                foreach (char c in output.ToString ())
-                {
-                    FakeConsole.Write (c);
-                }
-            }
-        }
-
-        FakeConsole.CursorTop = 0;
-        FakeConsole.CursorLeft = 0;
-
-        //SetCursorVisibility (savedVisibility);
-
-        void WriteToConsole (StringBuilder outputSb, ref int lastColumn, int row, ref int outputWidth)
-        {
-            FakeConsole.CursorTop = row;
-            FakeConsole.CursorLeft = lastColumn;
-
-            foreach (char c in outputSb.ToString ())
-            {
-                FakeConsole.Write (c);
-            }
-
-            outputSb.Clear ();
-            lastColumn += outputWidth;
-            outputWidth = 0;
-        }
-
-        FakeConsole.CursorTop = savedRow;
-        FakeConsole.CursorLeft = savedCol;
-        FakeConsole.CursorVisible = savedCursorVisible;
-
-        return updated;
-    }
-
-    private CursorVisibility _savedCursorVisibility;
-
-    /// <inheritdoc/>
-    public override bool GetCursorVisibility (out CursorVisibility visibility)
-    {
-        visibility = FakeConsole.CursorVisible
-                         ? CursorVisibility.Default
-                         : CursorVisibility.Invisible;
-
-        return FakeConsole.CursorVisible;
-    }
-
-    /// <inheritdoc/>
-    public override bool SetCursorVisibility (CursorVisibility visibility)
-    {
-        _savedCursorVisibility = visibility;
-
-        return FakeConsole.CursorVisible = visibility == CursorVisibility.Default;
-    }
-
-    private bool EnsureCursorVisibility ()
-    {
-        if (!(Col >= 0 && Row >= 0 && Col < Cols && Row < Rows))
-        {
-            GetCursorVisibility (out CursorVisibility cursorVisibility);
-            _savedCursorVisibility = cursorVisibility;
-            SetCursorVisibility (CursorVisibility.Invisible);
-
-            return false;
-        }
-
-        SetCursorVisibility (_savedCursorVisibility);
-
-        return FakeConsole.CursorVisible;
-    }
-
-    private readonly AnsiResponseParser _parser = new ();
-
-    /// <inheritdoc/>
-    internal override IAnsiResponseParser GetParser () { return _parser; }
-
-    /// <summary>
-    ///     Sets the screen size for testing purposes. Only available in FakeDriver.
-    ///     This method updates the driver's dimensions and triggers the ScreenChanged event.
-    /// </summary>
-    /// <param name="width">The new width in columns.</param>
-    /// <param name="height">The new height in rows.</param>
-    public override void SetScreenSize (int width, int height) { SetBufferSize (width, height); }
-
-    public void SetBufferSize (int width, int height)
-    {
-        FakeConsole.SetBufferSize (width, height);
-        Cols = width;
-        Rows = height;
-        SetConsoleSize (width, height);
-        ProcessResize ();
-    }
-
-    public void SetConsoleSize (int width, int height)
-    {
-        FakeConsole.SetConsoleSize (width, height);
-        FakeConsole.SetBufferSize (width, height);
-
-        if (width != Cols || height != Rows)
-        {
-            SetBufferSize (width, height);
-            Cols = width;
-            Rows = height;
-        }
-
-        ProcessResize ();
-    }
-
-    public void SetWindowPosition (int left, int top)
-    {
-        if (Left > 0 || Top > 0)
-        {
-            Left = 0;
-            Top = 0;
-        }
-
-        FakeConsole.SetWindowPosition (Left, Top);
-    }
-
-    private void ProcessResize ()
-    {
-        ResizeScreen ();
-        ClearContents ();
-        OnSizeChanged (new (new (Cols, Rows)));
-    }
-
-    public virtual void ResizeScreen ()
-    {
-        if (FakeConsole.WindowHeight > 0)
-        {
-            // Can raise an exception while it is still resizing.
-            try
-            {
-                FakeConsole.CursorTop = 0;
-                FakeConsole.CursorLeft = 0;
-                FakeConsole.WindowTop = 0;
-                FakeConsole.WindowLeft = 0;
-            }
-            catch (IOException)
-            {
-                return;
-            }
-            catch (ArgumentOutOfRangeException)
-            {
-                return;
-            }
-        }
-
-        // CONCURRENCY: Unsynchronized access to Clip is not safe.
-        Clip = new (Screen);
-    }
-
-    public override void UpdateCursor ()
-    {
-        if (!EnsureCursorVisibility ())
-        {
-            return;
-        }
-
-        // Prevents the exception to size changing during resizing.
-        try
-        {
-            // BUGBUG: Why is this using BufferWidth/Height and now Cols/Rows?
-            if (Col >= 0 && Col < FakeConsole.BufferWidth && Row >= 0 && Row < FakeConsole.BufferHeight)
-            {
-                FakeConsole.SetCursorPosition (Col, Row);
-            }
-        }
-        catch (IOException)
-        { }
-        catch (ArgumentOutOfRangeException)
-        { }
-    }
-
-    #region Not Implemented
-
-    public override void Suspend ()
-    {
-        //throw new NotImplementedException ();
-    }
-
-    #endregion
-
-
-#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member
-}

+ 47 - 0
Terminal.Gui/Drivers/FakeDriver/FakeInput.cs

@@ -0,0 +1,47 @@
+#nullable enable
+using System.Collections.Concurrent;
+
+namespace Terminal.Gui.Drivers;
+
+/// <summary>
+///     <see cref="IInput{TInputRecord}"/> implementation that uses a fake input source for testing.
+///     The <see cref="Peek"/> and <see cref="Read"/> methods are executed
+///     on the input thread created by <see cref="MainLoopCoordinator{TInputRecord}.StartInputTaskAsync"/>.
+/// </summary>
+public class FakeInput : InputImpl<ConsoleKeyInfo>, ITestableInput<ConsoleKeyInfo>
+{
+    // Queue for storing injected input that will be returned by Peek/Read
+    private readonly ConcurrentQueue<ConsoleKeyInfo> _testInput = new ();
+
+    /// <summary>
+    ///     Creates a new FakeInput.
+    /// </summary>
+    public FakeInput ()
+    { }
+
+    /// <inheritdoc/>
+    public override bool Peek ()
+    {
+        // Will be called on the input thread.
+        return !_testInput.IsEmpty;
+    }
+
+    /// <inheritdoc/>
+    public override IEnumerable<ConsoleKeyInfo> Read ()
+    {
+        // Will be called on the input thread.
+        while (_testInput.TryDequeue (out ConsoleKeyInfo input))
+        {
+            yield return input;
+        }
+    }
+
+    /// <inheritdoc />
+    public void AddInput (ConsoleKeyInfo input)
+    {
+        //Logging.Trace ($"Enqueuing input: {input.Key}");
+
+        // Will be called on the main loop thread.
+        _testInput.Enqueue (input);
+    }
+}

+ 47 - 0
Terminal.Gui/Drivers/FakeDriver/FakeInputProcessor.cs

@@ -0,0 +1,47 @@
+using System.Collections.Concurrent;
+
+namespace Terminal.Gui.Drivers;
+
+/// <summary>
+///     Input processor for <see cref="FakeInput"/>, deals in <see cref="ConsoleKeyInfo"/> stream
+/// </summary>
+public class FakeInputProcessor : InputProcessorImpl<ConsoleKeyInfo>
+{
+    /// <inheritdoc/>
+    public FakeInputProcessor (ConcurrentQueue<ConsoleKeyInfo> inputBuffer) : base (inputBuffer, new NetKeyConverter ())
+    {
+        DriverName = "fake";
+    }
+
+    /// <inheritdoc/>
+    protected override void Process (ConsoleKeyInfo input)
+    {
+        Logging.Trace ($"input: {input.KeyChar}");
+
+        foreach (Tuple<char, ConsoleKeyInfo> released in Parser.ProcessInput (Tuple.Create (input.KeyChar, input)))
+        {
+            Logging.Trace($"released: {released.Item1}");
+            ProcessAfterParsing (released.Item2);
+        }
+    }
+
+    /// <inheritdoc />
+    public override void EnqueueMouseEvent (MouseEventArgs mouseEvent)
+    {
+        // FakeDriver uses ConsoleKeyInfo as its input record type, which cannot represent mouse events.
+
+        // If Application.Invoke is available (running in Application context), defer to next iteration
+        // to ensure proper timing - the event is raised after views are laid out.
+        // Otherwise (unit tests), raise immediately so tests can verify synchronously.
+        if (Application.MainThreadId is { })
+        {
+            // Application is running - use Invoke to defer to next iteration
+            Application.Invoke (() => RaiseMouseEvent (mouseEvent));
+        }
+        else
+        {
+            // Not in Application context (unit tests) - raise immediately
+            RaiseMouseEvent (mouseEvent);
+        }
+    }
+}

+ 39 - 14
Terminal.Gui/Drivers/FakeDriver/FakeConsoleOutput.cs → Terminal.Gui/Drivers/FakeDriver/FakeOutput.cs

@@ -1,21 +1,43 @@
 #nullable enable
+using System;
+
 namespace Terminal.Gui.Drivers;
 
 /// <summary>
 ///     Fake console output for testing that captures what would be written to the console.
 /// </summary>
-public class FakeConsoleOutput : OutputBase, IConsoleOutput
+public class FakeOutput : OutputBase, IOutput
 {
     private readonly StringBuilder _output = new ();
     private int _cursorLeft;
     private int _cursorTop;
     private Size _consoleSize = new (80, 25);
 
+    /// <summary>
+    /// 
+    /// </summary>
+    public FakeOutput ()
+    {
+        LastBuffer = new OutputBufferImpl ();
+        LastBuffer.SetSize (80, 25);
+    }
+
+    /// <summary>
+    ///     Gets or sets the last output buffer written.
+    /// </summary>
+    public IOutputBuffer? LastBuffer { get; set; }
+
     /// <summary>
     ///     Gets the captured output as a string.
     /// </summary>
     public string Output => _output.ToString ();
 
+    /// <inheritdoc />
+    public Point GetCursorPosition ()
+    {
+        return new (_cursorLeft, _cursorTop);
+    }
+
     /// <inheritdoc/>
     public void SetCursorPosition (int col, int row) { SetCursorPositionImpl (col, row); }
 
@@ -34,23 +56,23 @@ public class FakeConsoleOutput : OutputBase, IConsoleOutput
         return true;
     }
 
-    /// <summary>
-    ///     Sets the fake window size.
-    /// </summary>
-    public void SetConsoleSize (int width, int height) { _consoleSize = new (width, height); }
-
-    /// <summary>
-    ///     Gets the current cursor position.
-    /// </summary>
-    public (int left, int top) GetCursorPosition () { return (_cursorLeft, _cursorTop); }
-
     /// <inheritdoc/>
     public Size GetSize () { return _consoleSize; }
 
     /// <inheritdoc/>
-    public void Write (ReadOnlySpan<char> text) { _output.Append (text); }
+    public void Write (ReadOnlySpan<char> text)
+    {
+        _output.Append (text);
+    }
 
-    /// <inheritdoc/>
+    /// <inheritdoc cref="IDriver"/>
+    public override void Write (IOutputBuffer buffer)
+    {
+        LastBuffer = buffer;
+        base.Write (buffer);
+    }
+
+    /// <inheritdoc cref="IDriver"/>
     public override void SetCursorVisibility (CursorVisibility visibility)
     {
         // Capture but don't act on it in fake output
@@ -70,5 +92,8 @@ public class FakeConsoleOutput : OutputBase, IConsoleOutput
     }
 
     /// <inheritdoc/>
-    protected override void Write (StringBuilder output) { _output.Append (output); }
+    protected override void Write (StringBuilder output)
+    {
+        _output.Append (output);
+    }
 }

+ 29 - 20
Terminal.Gui/Drivers/IComponentFactory.cs

@@ -1,51 +1,60 @@
 #nullable enable
 using System.Collections.Concurrent;
-using Terminal.Gui.App;
 
 namespace Terminal.Gui.Drivers;
 
 /// <summary>
-/// Base untyped interface for <see cref="IComponentFactory{T}"/> for methods that are not templated on low level
-/// console input type.
+///     Base untyped interface for <see cref="IComponentFactory{T}"/> for methods that are not templated on low level
+///     console input type.
 /// </summary>
 public interface IComponentFactory
 {
     /// <summary>
-    /// Create the <see cref="IConsoleOutput"/> class for the current driver implementation i.e. the class responsible for
-    /// rendering <see cref="IOutputBuffer"/> into the console.
+    ///     Create the <see cref="IOutput"/> class for the current driver implementation i.e. the class responsible for
+    ///     rendering <see cref="IOutputBuffer"/> into the console.
     /// </summary>
     /// <returns></returns>
-    IConsoleOutput CreateOutput ();
+    IOutput CreateOutput ();
 }
 
 /// <summary>
-/// Creates driver specific subcomponent classes (<see cref="IConsoleInput{T}"/>, <see cref="IInputProcessor"/> etc) for a
-/// <see cref="IMainLoopCoordinator"/>.
+///     Creates driver specific subcomponent classes (<see cref="IInput{TInputRecord}"/>, <see cref="IInputProcessor"/>
+///     etc) for a
+///     <see cref="IMainLoopCoordinator"/>.
 /// </summary>
-/// <typeparam name="T"></typeparam>
-public interface IComponentFactory<T> : IComponentFactory
+/// <typeparam name="TInputRecord">
+///     The platform specific console input type. Must be a value type (struct).
+///     Valid types are <see cref="ConsoleKeyInfo"/>, <see cref="WindowsConsole.InputRecord"/>, and <see cref="char"/>.
+/// </typeparam>
+public interface IComponentFactory<TInputRecord> : IComponentFactory
+    where TInputRecord : struct
 {
     /// <summary>
-    /// Create <see cref="IConsoleInput{T}"/> class for the current driver implementation i.e. the class responsible for reading
-    /// user input from the console.
+    ///     Create <see cref="IInput{T}"/> class for the current driver implementation i.e. the class responsible for reading
+    ///     user input from the console.
     /// </summary>
     /// <returns></returns>
-    IConsoleInput<T> CreateInput ();
+    IInput<TInputRecord> CreateInput ();
 
     /// <summary>
-    /// Creates the <see cref="InputProcessor{T}"/> class for the current driver implementation i.e. the class responsible for
-    /// translating raw console input into Terminal.Gui common event <see cref="Key"/> and <see cref="MouseEventArgs"/>.
+    ///     Creates the <see cref="InputProcessorImpl{T}"/> class for the current driver implementation i.e. the class
+    ///     responsible for
+    ///     translating raw console input into Terminal.Gui common event <see cref="Key"/> and <see cref="MouseEventArgs"/>.
     /// </summary>
-    /// <param name="inputBuffer"></param>
+    /// <param name="inputQueue">
+    ///     The input queue containing raw console input events, populated by <see cref="IInput{TInputRecord}"/>
+    ///     implementations on the input thread and
+    ///     read by <see cref="IInputProcessor"/> on the main loop thread.
+    /// </param>
     /// <returns></returns>
-    IInputProcessor CreateInputProcessor (ConcurrentQueue<T> inputBuffer);
+    IInputProcessor CreateInputProcessor (ConcurrentQueue<TInputRecord> inputQueue);
 
     /// <summary>
-    /// Creates <see cref="IConsoleSizeMonitor"/> class for the current driver implementation i.e. the class responsible for
-    /// reporting the current size of the terminal.
+    ///     Creates <see cref="ISizeMonitor"/> class for the current driver implementation i.e. the class responsible for
+    ///     reporting the current size of the terminal.
     /// </summary>
     /// <param name="consoleOutput"></param>
     /// <param name="outputBuffer"></param>
     /// <returns></returns>
-    IConsoleSizeMonitor CreateConsoleSizeMonitor (IConsoleOutput consoleOutput, IOutputBuffer outputBuffer);
+    ISizeMonitor CreateSizeMonitor (IOutput consoleOutput, IOutputBuffer outputBuffer);
 }

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

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

+ 0 - 29
Terminal.Gui/Drivers/IConsoleInput.cs

@@ -1,29 +0,0 @@
-using System.Collections.Concurrent;
-
-namespace Terminal.Gui.Drivers;
-
-/// <summary>
-///     Interface for reading console input indefinitely -
-///     i.e. in an infinite loop. The class is responsible only
-///     for reading and storing the input in a thread safe input buffer
-///     which is then processed downstream e.g. on main UI thread.
-/// </summary>
-/// <typeparam name="T"></typeparam>
-public interface IConsoleInput<T> : IDisposable
-{
-    /// <summary>
-    ///     Initializes the input with a buffer into which to put data read
-    /// </summary>
-    /// <param name="inputBuffer"></param>
-    void Initialize (ConcurrentQueue<T> inputBuffer);
-
-    /// <summary>
-    ///     Runs in an infinite input loop.
-    /// </summary>
-    /// <param name="token"></param>
-    /// <exception cref="OperationCanceledException">
-    ///     Raised when token is
-    ///     cancelled. This is the only means of exiting the input.
-    /// </exception>
-    void Run (CancellationToken token);
-}

+ 61 - 31
Terminal.Gui/Drivers/IConsoleDriver.cs → Terminal.Gui/Drivers/IDriver.cs

@@ -2,13 +2,37 @@
 
 namespace Terminal.Gui.Drivers;
 
-/// <summary>Base interface for Terminal.Gui ConsoleDriver implementations.</summary>
+/// <summary>Base interface for Terminal.Gui Driver implementations.</summary>
 /// <remarks>
 ///     There are currently four implementations: UnixDriver, WindowsDriver, DotNetDriver, and FakeDriver
 /// </remarks>
-public interface IConsoleDriver
+public interface IDriver
 {
+    /// <summary>
+    ///     Gets the name of the driver implementation.
+    /// </summary>
+    string? GetName ();
+
+    /// <summary>
+    ///     Class responsible for processing native driver input objects
+    ///     e.g. <see cref="ConsoleKeyInfo"/> into <see cref="Key"/> events
+    ///     and detecting and processing ansi escape sequences.
+    /// </summary>
+    IInputProcessor InputProcessor { get; }
+
+    /// <summary>
+    ///     Describes the desired screen state. Data source for <see cref="IOutput"/>.
+    /// </summary>
+    IOutputBuffer OutputBuffer { get; }
+
+    /// <summary>
+    ///     Interface for classes responsible for reporting the current
+    ///     size of the terminal window.
+    /// </summary>
+    ISizeMonitor SizeMonitor { get; }
+
     /// <summary>Get the operating system clipboard.</summary>
+    /// 
     IClipboard? Clipboard { get; }
 
     /// <summary>Gets the location and size of the terminal screen.</summary>
@@ -62,17 +86,17 @@ public interface IConsoleDriver
     /// <summary>The topmost row in the terminal.</summary>
     int Top { get; set; }
 
-    /// <summary>Gets whether the <see cref="ConsoleDriver"/> supports TrueColor output.</summary>
+    /// <summary>Gets whether the <see cref="IDriver"/> supports TrueColor output.</summary>
     bool SupportsTrueColor { get; }
 
     /// <summary>
-    ///     Gets or sets whether the <see cref="ConsoleDriver"/> should use 16 colors instead of the default TrueColors.
+    ///     Gets or sets whether the <see cref="IDriver"/> should use 16 colors instead of the default TrueColors.
     ///     See <see cref="Application.Force16Colors"/> to change this setting via <see cref="ConfigurationManager"/>.
     /// </summary>
     /// <remarks>
     ///     <para>
-    ///         Will be forced to <see langword="true"/> if <see cref="ConsoleDriver.SupportsTrueColor"/> is
-    ///         <see langword="false"/>, indicating that the <see cref="ConsoleDriver"/> cannot support TrueColor.
+    ///         Will be forced to <see langword="true"/> if <see cref="IDriver.SupportsTrueColor"/> is
+    ///         <see langword="false"/>, indicating that the <see cref="IDriver"/> cannot support TrueColor.
     ///     </para>
     /// </remarks>
     bool Force16Colors { get; set; }
@@ -88,7 +112,7 @@ public interface IConsoleDriver
     string GetVersionInfo ();
 
     /// <summary>
-    ///     Provide proper writing to send escape sequence recognized by the <see cref="ConsoleDriver"/>.
+    ///     Provide proper writing to send escape sequence recognized by the <see cref="IDriver"/>.
     /// </summary>
     /// <param name="ansi"></param>
     void WriteRaw (string ansi);
@@ -107,23 +131,23 @@ public interface IConsoleDriver
     /// <param name="row">The row.</param>
     /// <returns>
     ///     <see langword="false"/> if the coordinate is outside the screen bounds or outside of
-    ///     <see cref="ConsoleDriver.Clip"/>.
+    ///     <see cref="IDriver.Clip"/>.
     ///     <see langword="true"/> otherwise.
     /// </returns>
     bool IsValidLocation (Rune rune, int col, int row);
 
     /// <summary>
-    ///     Updates <see cref="ConsoleDriver.Col"/> and <see cref="ConsoleDriver.Row"/> to the specified column and row in
-    ///     <see cref="ConsoleDriver.Contents"/>.
-    ///     Used by <see cref="ConsoleDriver.AddRune(System.Text.Rune)"/> and <see cref="ConsoleDriver.AddStr"/> to determine
+    ///     Updates <see cref="IDriver.Col"/> and <see cref="IDriver.Row"/> to the specified column and row in
+    ///     <see cref="IDriver.Contents"/>.
+    ///     Used by <see cref="IDriver.AddRune(System.Text.Rune)"/> and <see cref="IDriver.AddStr"/> to determine
     ///     where to add content.
     /// </summary>
     /// <remarks>
     ///     <para>This does not move the cursor on the screen, it only updates the internal state of the driver.</para>
     ///     <para>
-    ///         If <paramref name="col"/> or <paramref name="row"/> are negative or beyond  <see cref="ConsoleDriver.Cols"/>
+    ///         If <paramref name="col"/> or <paramref name="row"/> are negative or beyond  <see cref="IDriver.Cols"/>
     ///         and
-    ///         <see cref="ConsoleDriver.Rows"/>, the method still sets those properties.
+    ///         <see cref="IDriver.Rows"/>, the method still sets those properties.
     ///     </para>
     /// </remarks>
     /// <param name="col">Column to move to.</param>
@@ -133,15 +157,15 @@ public interface IConsoleDriver
     /// <summary>Adds the specified rune to the display at the current cursor position.</summary>
     /// <remarks>
     ///     <para>
-    ///         When the method returns, <see cref="ConsoleDriver.Col"/> will be incremented by the number of columns
+    ///         When the method returns, <see cref="IDriver.Col"/> will be incremented by the number of columns
     ///         <paramref name="rune"/> required, even if the new column value is outside of the
-    ///         <see cref="ConsoleDriver.Clip"/> or screen
-    ///         dimensions defined by <see cref="ConsoleDriver.Cols"/>.
+    ///         <see cref="IDriver.Clip"/> or screen
+    ///         dimensions defined by <see cref="IDriver.Cols"/>.
     ///     </para>
     ///     <para>
-    ///         If <paramref name="rune"/> requires more than one column, and <see cref="ConsoleDriver.Col"/> plus the number
+    ///         If <paramref name="rune"/> requires more than one column, and <see cref="IDriver.Col"/> plus the number
     ///         of columns
-    ///         needed exceeds the <see cref="ConsoleDriver.Clip"/> or screen dimensions, the default Unicode replacement
+    ///         needed exceeds the <see cref="IDriver.Clip"/> or screen dimensions, the default Unicode replacement
     ///         character (U+FFFD)
     ///         will be added instead.
     ///     </para>
@@ -151,7 +175,7 @@ public interface IConsoleDriver
 
     /// <summary>
     ///     Adds the specified <see langword="char"/> to the display at the current cursor position. This method is a
-    ///     convenience method that calls <see cref="ConsoleDriver.AddRune(System.Text.Rune)"/> with the <see cref="Rune"/>
+    ///     convenience method that calls <see cref="IDriver.AddRune(System.Text.Rune)"/> with the <see cref="Rune"/>
     ///     constructor.
     /// </summary>
     /// <param name="c">Character to add.</param>
@@ -160,27 +184,27 @@ public interface IConsoleDriver
     /// <summary>Adds the <paramref name="str"/> to the display at the cursor position.</summary>
     /// <remarks>
     ///     <para>
-    ///         When the method returns, <see cref="ConsoleDriver.Col"/> will be incremented by the number of columns
-    ///         <paramref name="str"/> required, unless the new column value is outside of the <see cref="ConsoleDriver.Clip"/>
+    ///         When the method returns, <see cref="IDriver.Col"/> will be incremented by the number of columns
+    ///         <paramref name="str"/> required, unless the new column value is outside of the <see cref="IDriver.Clip"/>
     ///         or screen
-    ///         dimensions defined by <see cref="ConsoleDriver.Cols"/>.
+    ///         dimensions defined by <see cref="IDriver.Cols"/>.
     ///     </para>
     ///     <para>If <paramref name="str"/> requires more columns than are available, the output will be clipped.</para>
     /// </remarks>
     /// <param name="str">String.</param>
     void AddStr (string str);
 
-    /// <summary>Clears the <see cref="ConsoleDriver.Contents"/> of the driver.</summary>
+    /// <summary>Clears the <see cref="IDriver.Contents"/> of the driver.</summary>
     void ClearContents ();
 
     /// <summary>
-    ///     Fills the specified rectangle with the specified rune, using <see cref="ConsoleDriver.CurrentAttribute"/>
+    ///     Fills the specified rectangle with the specified rune, using <see cref="IDriver.CurrentAttribute"/>
     /// </summary>
     event EventHandler<EventArgs> ClearedContents;
 
-    /// <summary>Fills the specified rectangle with the specified rune, using <see cref="ConsoleDriver.CurrentAttribute"/></summary>
+    /// <summary>Fills the specified rectangle with the specified rune, using <see cref="IDriver.CurrentAttribute"/></summary>
     /// <remarks>
-    ///     The value of <see cref="ConsoleDriver.Clip"/> is honored. Any parts of the rectangle not in the clip will not be
+    ///     The value of <see cref="IDriver.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>
@@ -189,7 +213,7 @@ public interface IConsoleDriver
 
     /// <summary>
     ///     Fills the specified rectangle with the specified <see langword="char"/>. This method is a convenience method
-    ///     that calls <see cref="ConsoleDriver.FillRect(System.Drawing.Rectangle,System.Text.Rune)"/>.
+    ///     that calls <see cref="IDriver.FillRect(System.Drawing.Rectangle,System.Text.Rune)"/>.
     /// </summary>
     /// <param name="rect"></param>
     /// <param name="c"></param>
@@ -220,8 +244,8 @@ public interface IConsoleDriver
     void Suspend ();
 
     /// <summary>
-    ///     Sets the position of the terminal cursor to <see cref="ConsoleDriver.Col"/> and
-    ///     <see cref="ConsoleDriver.Row"/>.
+    ///     Sets the position of the terminal cursor to <see cref="IDriver.Col"/> and
+    ///     <see cref="IDriver.Row"/>.
     /// </summary>
     void UpdateCursor ();
 
@@ -243,17 +267,23 @@ public interface IConsoleDriver
     /// <summary>Event fired when a mouse event occurs.</summary>
     event EventHandler<MouseEventArgs>? MouseEvent;
 
-    /// <summary>Event fired when a key is pressed down. This is a precursor to <see cref="ConsoleDriver.KeyUp"/>.</summary>
+    /// <summary>Event fired when a key is pressed down. This is a precursor to <see cref="IDriver.KeyUp"/>.</summary>
     event EventHandler<Key>? KeyDown;
 
     /// <summary>Event fired when a key is released.</summary>
     /// <remarks>
-    ///     Drivers that do not support key release events will fire this event after <see cref="ConsoleDriver.KeyDown"/>
+    ///     Drivers that do not support key release events will fire this event after <see cref="IDriver.KeyDown"/>
     ///     processing is
     ///     complete.
     /// </remarks>
     event EventHandler<Key>? KeyUp;
 
+    /// <summary>
+    ///     Enqueues a key input event to the driver. For unit tests.
+    /// </summary>
+    /// <param name="key"></param>
+    void EnqueueKeyEvent (Key key);
+
     /// <summary>
     ///     Queues the given <paramref name="request"/> for execution
     /// </summary>

+ 172 - 0
Terminal.Gui/Drivers/IInput.cs

@@ -0,0 +1,172 @@
+#nullable enable
+using System.Collections.Concurrent;
+
+namespace Terminal.Gui.Drivers;
+
+/// <summary>
+///     Interface for reading console input in a perpetual loop on a dedicated input thread.
+/// </summary>
+/// <remarks>
+///     <para>
+///         Implementations run on a separate thread (started by
+///         <see cref="MainLoopCoordinator{TInputRecord}.StartInputTaskAsync"/>)
+///         and continuously read platform-specific input from the console, placing it into a thread-safe queue
+///         for processing by <see cref="IInputProcessor"/> on the main UI thread.
+///     </para>
+///     <para>
+///         <b>Architecture:</b>
+///     </para>
+///     <code>
+///         Input Thread:                    Main UI Thread:
+///         ┌─────────────────┐             ┌──────────────────────┐
+///         │ IInput.Run()    │             │ IInputProcessor      │
+///         │  ├─ Peek()      │             │  ├─ ProcessQueue()   │
+///         │  ├─ Read()      │──Enqueue──→ │  ├─ Process()        │
+///         │  └─ Enqueue     │             │  ├─ ToKey()          │
+///         └─────────────────┘             │  └─ Raise Events     │
+///                                         └──────────────────────┘
+///     </code>
+///     <para>
+///         <b>Lifecycle:</b>
+///     </para>
+///     <list type="number">
+///         <item><see cref="Initialize"/> - Set the shared input queue</item>
+///         <item><see cref="Run"/> - Start the perpetual read loop (blocks until cancelled)</item>
+///         <item>
+///             Loop calls <see cref="InputImpl{TInputRecord}.Peek"/> and <see cref="InputImpl{TInputRecord}.Read"/>
+///         </item>
+///         <item>Cancellation via `runCancellationToken` or <see cref="ExternalCancellationTokenSource"/></item>
+///     </list>
+///     <para>
+///         <b>Implementations:</b>
+///     </para>
+///     <list type="bullet">
+///         <item><see cref="WindowsInput"/> - Uses Windows Console API (<c>ReadConsoleInput</c>)</item>
+///         <item><see cref="NetInput"/> - Uses .NET <see cref="System.Console"/> API</item>
+///         <item><see cref="UnixInput"/> - Uses Unix terminal APIs</item>
+///         <item><see cref="FakeInput"/> - For testing, implements <see cref="ITestableInput{TInputRecord}"/></item>
+///     </list>
+///     <para>
+///         <b>Testing Support:</b> See <see cref="ITestableInput{TInputRecord}"/> for programmatic input injection
+///         in test scenarios.
+///     </para>
+/// </remarks>
+/// <typeparam name="TInputRecord">
+///     The platform-specific input record type:
+///     <list type="bullet">
+///         <item><see cref="ConsoleKeyInfo"/> - for .NET and Fake drivers</item>
+///         <item><see cref="WindowsConsole.InputRecord"/> - for Windows driver</item>
+///         <item><see cref="char"/> - for Unix driver</item>
+///     </list>
+/// </typeparam>
+public interface IInput<TInputRecord> : IDisposable
+{
+    /// <summary>
+    ///     Gets or sets an external cancellation token source that can stop the <see cref="Run"/> loop
+    ///     in addition to the `runCancellationToken` passed to <see cref="Run"/>.
+    /// </summary>
+    /// <remarks>
+    ///     <para>
+    ///         This property allows external code (e.g., test harnesses like <c>GuiTestContext</c>) to
+    ///         provide additional cancellation signals such as timeouts or hard-stop conditions.
+    ///     </para>
+    ///     <para>
+    ///         <b>Ownership:</b> The setter does NOT transfer ownership of the <see cref="CancellationTokenSource"/>.
+    ///         The creator is responsible for disposal. <see cref="IInput{TInputRecord}"/> implementations
+    ///         should NOT dispose this token source.
+    ///     </para>
+    ///     <para>
+    ///         <b>How it works:</b> <see cref="InputImpl{TInputRecord}.Run"/> creates a linked token that
+    ///         responds to BOTH the `runCancellationToken` AND this external token:
+    ///     </para>
+    ///     <code>
+    ///         var linkedToken = CancellationTokenSource.CreateLinkedTokenSource(
+    ///             runCancellationToken,
+    ///             ExternalCancellationTokenSource.Token);
+    ///     </code>
+    /// </remarks>
+    /// <example>
+    ///     Test scenario with timeout:
+    ///     <code>
+    ///         var input = new FakeInput();
+    ///         input.ExternalCancellationTokenSource = new CancellationTokenSource(
+    ///             TimeSpan.FromSeconds(30)); // 30-second timeout
+    ///         
+    ///         // Run will stop if either:
+    ///         // 1. runCancellationToken is cancelled (normal shutdown)
+    ///         // 2. 30 seconds elapse (timeout)
+    ///         input.Run(normalCancellationToken);
+    ///     </code>
+    /// </example>
+    CancellationTokenSource? ExternalCancellationTokenSource { get; set; }
+
+    /// <summary>
+    ///     Initializes the input reader with the thread-safe queue where read input will be stored.
+    /// </summary>
+    /// <param name="inputQueue">
+    ///     The shared <see cref="ConcurrentQueue{T}"/> that both <see cref="Run"/> (producer)
+    ///     and <see cref="IInputProcessor"/> (consumer) use for passing input records between threads.
+    /// </param>
+    /// <remarks>
+    ///     <para>
+    ///         This queue is created by <see cref="Terminal.Gui.App.MainLoopCoordinator{TInputRecord}"/>
+    ///         and shared between the input thread and main UI thread.
+    ///     </para>
+    ///     <para>
+    ///         <b>Must be called before <see cref="Run"/>.</b> Calling <see cref="Run"/> without
+    ///         initialization will throw an exception.
+    ///     </para>
+    /// </remarks>
+    void Initialize (ConcurrentQueue<TInputRecord> inputQueue);
+
+    /// <summary>
+    ///     Runs the input loop, continuously reading input and placing it into the queue
+    ///     provided by <see cref="Initialize"/>.
+    /// </summary>
+    /// <param name="runCancellationToken">
+    ///     The primary cancellation token that stops the input loop. Provided by
+    ///     <see cref="Terminal.Gui.App.MainLoopCoordinator{TInputRecord}"/> and triggered
+    ///     during application shutdown.
+    /// </param>
+    /// <remarks>
+    ///     <para>
+    ///         <b>Threading:</b> This method runs on a dedicated input thread created by
+    ///         <see cref="MainLoopCoordinator{TInputRecord}.StartInputTaskAsync"/>. and blocks until
+    ///         cancellation is requested. It should never be called from the main UI thread.
+    ///     </para>
+    ///     <para>
+    ///         <b>Cancellation:</b> The loop stops when either <paramref name="runCancellationToken"/>
+    ///         or <see cref="ExternalCancellationTokenSource"/> (if set) is cancelled.
+    ///     </para>
+    ///     <para>
+    ///         <b>Base Implementation:</b> <see cref="InputImpl{TInputRecord}.Run"/> provides the
+    ///         standard loop logic:
+    ///     </para>
+    ///     <code>
+    ///         while (!cancelled)
+    ///         {
+    ///             while (Peek())  // Check for available input
+    ///             {
+    ///                 foreach (var input in Read())  // Read all available
+    ///                 {
+    ///                     inputQueue.Enqueue(input);  // Store for processing
+    ///                 }
+    ///             }
+    ///             Task.Delay(20ms);  // Throttle to ~50 polls/second
+    ///         }
+    ///     </code>
+    ///     <para>
+    ///         <b>Testing:</b> For <see cref="ITestableInput{TInputRecord}"/> implementations,
+    ///         test input injected via <see cref="ITestableInput{TInputRecord}.AddInput"/>
+    ///         flows through the same <c>Peek/Read</c> pipeline.
+    ///     </para>
+    /// </remarks>
+    /// <exception cref="OperationCanceledException">
+    ///     Thrown when <paramref name="runCancellationToken"/> or <see cref="ExternalCancellationTokenSource"/>
+    ///     is cancelled. This is the normal/expected means of exiting the input loop.
+    /// </exception>
+    /// <exception cref="InvalidOperationException">
+    ///     Thrown if <see cref="Initialize"/> was not called before <see cref="Run"/>.
+    /// </exception>
+    void Run (CancellationToken runCancellationToken);
+}

+ 58 - 39
Terminal.Gui/Drivers/IInputProcessor.cs

@@ -3,58 +3,22 @@
 namespace Terminal.Gui.Drivers;
 
 /// <summary>
-///     Interface for main loop class that will process the queued input buffer contents.
+///     Interface for main loop class that will process the queued input.
 ///     Is responsible for <see cref="ProcessQueue"/> and translating into common Terminal.Gui
 ///     events and data models.
 /// </summary>
 public interface IInputProcessor
 {
-    /// <summary>Event fired when a key is pressed down. This is a precursor to <see cref="KeyUp"/>.</summary>
-    event EventHandler<Key>? KeyDown;
-
-    /// <summary>Event fired when a key is released.</summary>
-    /// <remarks>
-    ///     Drivers that do not support key release events will fire this event after <see cref="KeyDown"/> processing is
-    ///     complete.
-    /// </remarks>
-    event EventHandler<Key>? KeyUp;
-
-    /// <summary>Event fired when a terminal sequence read from input is not recognized and therefore ignored.</summary>
+    /// <summary>Event raised when a terminal sequence read from input is not recognized and therefore ignored.</summary>
     public event EventHandler<string>? AnsiSequenceSwallowed;
 
-    /// <summary>Event fired when a mouse event occurs.</summary>
-    event EventHandler<MouseEventArgs>? MouseEvent;
-
     /// <summary>
     /// Gets the name of the driver associated with this input processor.
     /// </summary>
     string? DriverName { get; init; }
 
     /// <summary>
-    ///     Called when a key is pressed down. Fires the <see cref="KeyDown"/> event. This is a precursor to
-    ///     <see cref="OnKeyUp"/>.
-    /// </summary>
-    /// <param name="key">The key event data.</param>
-    void OnKeyDown (Key key);
-
-    /// <summary>
-    ///     Called when a key is released. Fires the <see cref="KeyUp"/> event.
-    /// </summary>
-    /// <remarks>
-    ///     Drivers that do not support key release events will call this method after <see cref="OnKeyDown"/> processing
-    ///     is complete.
-    /// </remarks>
-    /// <param name="key">The key event data.</param>
-    void OnKeyUp (Key key);
-
-    /// <summary>
-    ///     Called when a mouse event occurs. Fires the <see cref="MouseEvent"/> event.
-    /// </summary>
-    /// <param name="mouseEventArgs">The mouse event data.</param>
-    void OnMouseEvent (MouseEventArgs mouseEventArgs);
-
-    /// <summary>
-    ///     Drains the input buffer, processing all available keystrokes
+    ///     Drains the input queue, processing all available keystrokes. To be called on the main loop thread.
     /// </summary>
     void ProcessQueue ();
 
@@ -74,4 +38,59 @@ public interface IInputProcessor
     ///     <see langword="false"/>.
     /// </returns>
     bool IsValidInput (Key key, out Key result);
+
+    /// <summary>
+    ///     Called when a key down event has been dequeued. Raises the <see cref="KeyDown"/> event. This is a precursor to
+    ///     <see cref="RaiseKeyUpEvent"/>.
+    /// </summary>
+    /// <param name="key">The key event data.</param>
+    void RaiseKeyDownEvent (Key key);
+
+    /// <summary>Event raised when a key down event has been dequeued. This is a precursor to <see cref="KeyUp"/>.</summary>
+    event EventHandler<Key>? KeyDown;
+
+    /// <summary>
+    ///     Adds a key up event to the input queue. For unit tests.
+    /// </summary>
+    /// <param name="key"></param>
+    void EnqueueKeyDownEvent (Key key);
+
+    /// <summary>
+    ///     Called when a key up event has been dequeued. Raises the <see cref="KeyUp"/> event.
+    /// </summary>
+    /// <remarks>
+    ///     Drivers that do not support key release events will call this method after <see cref="RaiseKeyDownEvent"/> processing
+    ///     is complete.
+    /// </remarks>
+    /// <param name="key">The key event data.</param>
+    void RaiseKeyUpEvent (Key key);
+
+    /// <summary>Event raised when a key up event has been dequeued.</summary>
+    /// <remarks>
+    ///     Drivers that do not support key release events will fire this event after <see cref="KeyDown"/> processing is
+    ///     complete.
+    /// </remarks>
+    event EventHandler<Key>? KeyUp;
+
+    /// <summary>
+    ///     Adds a key up event to the input queue. For unit tests.
+    /// </summary>
+    /// <param name="key"></param>
+    void EnqueueKeyUpEvent (Key key);
+
+    /// <summary>
+    ///     Called when a mouse event has been dequeued. Raises the <see cref="MouseEvent"/> event.
+    /// </summary>
+    /// <param name="mouseEventArgs">The mouse event data.</param>
+    void RaiseMouseEvent (MouseEventArgs mouseEventArgs);
+
+    /// <summary>Event raised when a mouse event has been dequeued.</summary>
+    event EventHandler<MouseEventArgs>? MouseEvent;
+
+    /// <summary>
+    ///     Adds a mouse input event to the input queue. For unit tests.
+    /// </summary>
+    /// <param name="mouseEvent"></param>
+    void EnqueueMouseEvent (MouseEventArgs mouseEvent);
+
 }

+ 15 - 7
Terminal.Gui/Drivers/IKeyConverter.cs

@@ -2,18 +2,26 @@
 namespace Terminal.Gui.Drivers;
 
 /// <summary>
-///     Interface for subcomponent of a <see cref="InputProcessor{T}"/> which
+///     Interface for subcomponent of a <see cref="InputProcessorImpl{T}"/> which
 ///     can translate the raw console input type T (which typically varies by
 ///     driver) to the shared Terminal.Gui <see cref="Key"/> class.
 /// </summary>
-/// <typeparam name="T"></typeparam>
-public interface IKeyConverter<in T>
+/// <typeparam name="TInputRecord"></typeparam>
+public interface IKeyConverter<TInputRecord>
 {
     /// <summary>
-    ///     Converts the native keyboard class read from console into
-    ///     the shared <see cref="Key"/> class used by Terminal.Gui views.
+    ///     Converts the native keyboard info type into
+    ///     the <see cref="Key"/> class used by Terminal.Gui views.
     /// </summary>
-    /// <param name="value"></param>
+    /// <param name="keyInfo"></param>
     /// <returns></returns>
-    Key ToKey (T value);
+    Key ToKey (TInputRecord keyInfo);
+
+    /// <summary>
+    ///     Converts a <see cref="Key"/> into the native keyboard info type. Should be used for simulating
+    ///     key presses in unit tests.
+    /// </summary>
+    /// <param name="key"></param>
+    /// <returns></returns>
+    TInputRecord ToKeyInfo (Key key);
 }

+ 28 - 21
Terminal.Gui/Drivers/IConsoleOutput.cs → Terminal.Gui/Drivers/IOutput.cs

@@ -1,23 +1,16 @@
 namespace Terminal.Gui.Drivers;
 
 /// <summary>
-///     Interface for writing console output
+///     The low-level interface drivers implement to provide output capabilities; encapsulates platform-specific
+///     output functionality.
 /// </summary>
-public interface IConsoleOutput : IDisposable
+public interface IOutput : IDisposable
 {
     /// <summary>
-    ///     Writes the given text directly to the console. Use to send
-    ///     ansi escape codes etc. Regular screen output should use the
-    ///     <see cref="IOutputBuffer"/> overload.
-    /// </summary>
-    /// <param name="text"></param>
-    void Write (ReadOnlySpan<char> text);
-
-    /// <summary>
-    ///     Write the contents of the <paramref name="buffer"/> to the console
+    ///     Gets the current position of the console cursor.
     /// </summary>
-    /// <param name="buffer"></param>
-    void Write (IOutputBuffer buffer);
+    /// <returns></returns>
+    Point GetCursorPosition ();
 
     /// <summary>
     ///     Returns the current size of the console in rows/columns (i.e.
@@ -26,13 +19,6 @@ public interface IConsoleOutput : IDisposable
     /// <returns></returns>
     public Size GetSize ();
 
-    /// <summary>
-    ///     Updates the console cursor (the blinking underscore) to be hidden,
-    ///     visible etc.
-    /// </summary>
-    /// <param name="visibility"></param>
-    void SetCursorVisibility (CursorVisibility visibility);
-
     /// <summary>
     ///     Moves the console cursor to the given location.
     /// </summary>
@@ -41,9 +27,30 @@ public interface IConsoleOutput : IDisposable
     void SetCursorPosition (int col, int row);
 
     /// <summary>
-    ///     Sets the size of the console..
+    ///     Updates the console cursor (the blinking underscore) to be hidden,
+    ///     visible etc.
+    /// </summary>
+    /// <param name="visibility"></param>
+    void SetCursorVisibility (CursorVisibility visibility);
+
+    /// <summary>
+    ///     Sets the size of the console.
     /// </summary>
     /// <param name="width"></param>
     /// <param name="height"></param>
     void SetSize (int width, int height);
+
+    /// <summary>
+    ///     Writes the given text directly to the console. Use to send
+    ///     ansi escape codes etc. Regular screen output should use the
+    ///     <see cref="IOutputBuffer"/> overload.
+    /// </summary>
+    /// <param name="text"></param>
+    void Write (ReadOnlySpan<char> text);
+
+    /// <summary>
+    ///     Write the contents of the <paramref name="buffer"/> to the console
+    /// </summary>
+    /// <param name="buffer"></param>
+    void Write (IOutputBuffer buffer);
 }

+ 74 - 59
Terminal.Gui/Drivers/IOutputBuffer.cs

@@ -3,16 +3,44 @@
 namespace Terminal.Gui.Drivers;
 
 /// <summary>
-///     Describes the screen state that you want the console to be in.
-///     Is designed to be drawn to repeatedly then manifest into the console
-///     once at the end of iteration after all drawing is finalized.
+///     Represents the desired screen state for console rendering. This interface provides methods for building up
+///     visual content (text, attributes, fills) in a buffer that can be efficiently written to the terminal
+///     in a single operation at the end of each iteration. Final output is handled by <see cref="IOutput"/>.
 /// </summary>
+/// <remarks>
+///     <para>
+///         The <see cref="IOutputBuffer"/> acts as an intermediary between Terminal.Gui's high-level drawing operations
+///         and the low-level console output. Rather than writing directly to the console for each operation, views
+///         draw to this buffer during layout and rendering. The buffer is then flushed to the terminal by
+///         <see cref="IOutput"/> after all drawing is complete, minimizing flicker and improving performance.
+///     </para>
+///     <para>
+///         The buffer maintains a 2D array of <see cref="Cell"/> objects in <see cref="Contents"/>, where each cell
+///         represents a single character position on screen with its associated character, attributes, and dirty state.
+///         Drawing operations like <see cref="AddRune(Rune)"/> and <see cref="AddStr(string)"/> modify cells at the
+///         current cursor position (tracked by <see cref="Col"/> and <see cref="Row"/>), respecting any active
+///         <see cref="Clip"/> region.
+///     </para>
+/// </remarks>
 public interface IOutputBuffer
 {
+    /// <summary>Adds the specified rune to the display at the current cursor position.</summary>
+    /// <param name="rune">Rune to add.</param>
+    void AddRune (Rune rune);
+
     /// <summary>
-    ///     The contents of the application output. The driver outputs this buffer to the terminal when UpdateScreen is called.
+    ///     Adds the specified character to the display at the current cursor position. This is a convenience method for
+    ///     AddRune.
     /// </summary>
-    Cell [,]? Contents { get; set; }
+    /// <param name="c">Character to add.</param>
+    void AddRune (char c);
+
+    /// <summary>Adds the string to the display at the current cursor position.</summary>
+    /// <param name="str">String to add.</param>
+    void AddStr (string str);
+
+    /// <summary>Clears the contents of the buffer.</summary>
+    void ClearContents ();
 
     /// <summary>
     ///     Gets or sets the clip rectangle that <see cref="AddRune(Rune)"/> and <see cref="AddStr(string)"/> are subject
@@ -22,39 +50,56 @@ public interface IOutputBuffer
     public Region? Clip { get; set; }
 
     /// <summary>
-    ///     The <see cref="Attribute"/> that will be used for the next AddRune or AddStr call.
+    ///     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>
-    Attribute CurrentAttribute { get; set; }
-
-    /// <summary>The number of rows visible in the terminal.</summary>
-    int Rows { get; set; }
+    public int Col { get; }
 
     /// <summary>The number of columns visible in the terminal.</summary>
     int Cols { get; set; }
 
     /// <summary>
-    ///     Gets the row last set by <see cref="Move"/>. <see cref="Col"/> and <see cref="Row"/> are used by
-    ///     <see cref="AddRune(Rune)"/> and <see cref="AddStr"/> to determine where to add content.
+    ///     The contents of the application output. The driver outputs this buffer to the terminal when UpdateScreen is called.
     /// </summary>
-    public int Row { get; }
+    Cell [,]? Contents { get; set; }
 
     /// <summary>
-    ///     Gets the column last set by <see cref="Move"/>. <see cref="Col"/> and <see cref="Row"/> are used by
-    ///     <see cref="AddRune(Rune)"/> and <see cref="AddStr"/> to determine where to add content.
+    ///     The <see cref="Attribute"/> that will be used for the next AddRune or AddStr call.
     /// </summary>
-    public int Col { get; }
+    Attribute CurrentAttribute { get; set; }
 
     /// <summary>
-    ///     The first cell index on left of screen - basically always 0.
-    ///     Changing this may have unexpected consequences.
+    ///     Fills the given <paramref name="rect"/> with the given
+    ///     symbol using the currently selected attribute.
     /// </summary>
-    int Left { get; set; }
+    /// <param name="rect"></param>
+    /// <param name="rune"></param>
+    void FillRect (Rectangle rect, Rune rune);
 
     /// <summary>
-    ///     The first cell index on top of screen - basically always 0.
+    ///     Fills the given <paramref name="rect"/> with the given
+    ///     symbol using the currently selected attribute.
+    /// </summary>
+    /// <param name="rect"></param>
+    /// <param name="rune"></param>
+    void FillRect (Rectangle rect, char rune);
+
+    /// <summary>
+    ///     Tests whether the specified coordinate is valid for drawing the specified Rune.
+    /// </summary>
+    /// <param name="rune">Used to determine if one or two columns are required.</param>
+    /// <param name="col">The column.</param>
+    /// <param name="row">The row.</param>
+    /// <returns>
+    ///     True if the coordinate is valid for the Rune; false otherwise.
+    /// </returns>
+    bool IsValidLocation (Rune rune, int col, int row);
+
+    /// <summary>
+    ///     The first cell index on left of screen - basically always 0.
     ///     Changing this may have unexpected consequences.
     /// </summary>
-    int Top { get; set; }
+    int Left { get; set; }
 
     /// <summary>
     ///     Updates the column and row to the specified location in the buffer.
@@ -63,34 +108,14 @@ public interface IOutputBuffer
     /// <param name="row">The row to move to.</param>
     void Move (int col, int row);
 
-    /// <summary>Adds the specified rune to the display at the current cursor position.</summary>
-    /// <param name="rune">Rune to add.</param>
-    void AddRune (Rune rune);
-
     /// <summary>
-    ///     Adds the specified character to the display at the current cursor position. This is a convenience method for
-    ///     AddRune.
+    ///     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>
-    /// <param name="c">Character to add.</param>
-    void AddRune (char c);
-
-    /// <summary>Adds the string to the display at the current cursor position.</summary>
-    /// <param name="str">String to add.</param>
-    void AddStr (string str);
-
-    /// <summary>Clears the contents of the buffer.</summary>
-    void ClearContents ();
+    public int Row { get; }
 
-    /// <summary>
-    ///     Tests whether the specified coordinate is valid for drawing the specified Rune.
-    /// </summary>
-    /// <param name="rune">Used to determine if one or two columns are required.</param>
-    /// <param name="col">The column.</param>
-    /// <param name="row">The row.</param>
-    /// <returns>
-    ///     True if the coordinate is valid for the Rune; false otherwise.
-    /// </returns>
-    bool IsValidLocation (Rune rune, int col, int row);
+    /// <summary>The number of rows visible in the terminal.</summary>
+    int Rows { get; set; }
 
     /// <summary>
     ///     Changes the size of the buffer to the given size
@@ -100,18 +125,8 @@ public interface IOutputBuffer
     void SetSize (int cols, int rows);
 
     /// <summary>
-    ///     Fills the given <paramref name="rect"/> with the given
-    ///     symbol using the currently selected attribute.
-    /// </summary>
-    /// <param name="rect"></param>
-    /// <param name="rune"></param>
-    void FillRect (Rectangle rect, Rune rune);
-
-    /// <summary>
-    ///     Fills the given <paramref name="rect"/> with the given
-    ///     symbol using the currently selected attribute.
+    ///     The first cell index on top of screen - basically always 0.
+    ///     Changing this may have unexpected consequences.
     /// </summary>
-    /// <param name="rect"></param>
-    /// <param name="rune"></param>
-    void FillRect (Rectangle rect, char rune);
+    int Top { get; set; }
 }

+ 1 - 1
Terminal.Gui/Drivers/IConsoleSizeMonitor.cs → Terminal.Gui/Drivers/ISizeMonitor.cs

@@ -6,7 +6,7 @@ namespace Terminal.Gui.Drivers;
 ///     Interface for classes responsible for reporting the current
 ///     size of the terminal window.
 /// </summary>
-public interface IConsoleSizeMonitor
+public interface ISizeMonitor
 {
     /// <summary>Invoked when the terminal's size changed. The new size of the terminal is provided.</summary>
     event EventHandler<SizeChangedEventArgs>? SizeChanged;

+ 15 - 0
Terminal.Gui/Drivers/ITestableInput.cs

@@ -0,0 +1,15 @@
+namespace Terminal.Gui.Drivers;
+
+/// <summary>
+///     Marker interface for IInput implementations that support test input injection.
+/// </summary>
+/// <typeparam name="TInputRecord">The input record type</typeparam>
+public interface ITestableInput<TInputRecord> : IInput<TInputRecord>
+    where TInputRecord : struct
+{
+    /// <summary>
+    ///     Adds an input record that will be returned by Peek/Read for testing.
+    /// </summary>
+    void AddInput (TInputRecord input);
+}
+

+ 89 - 0
Terminal.Gui/Drivers/InputImpl.cs

@@ -0,0 +1,89 @@
+#nullable enable
+using System.Collections.Concurrent;
+
+namespace Terminal.Gui.Drivers;
+
+/// <summary>
+///     Base class for reading console input in perpetual loop.
+///     The <see cref="Peek"/> and <see cref="Read"/> methods are executed
+///     on the input thread created by <see cref="MainLoopCoordinator{TInputRecord}.StartInputTaskAsync"/>.
+/// </summary>
+/// <typeparam name="TInputRecord"></typeparam>
+public abstract class InputImpl<TInputRecord> : IInput<TInputRecord>
+{
+    private ConcurrentQueue<TInputRecord>? _inputQueue;
+
+    /// <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 CancellationTokenSource? ExternalCancellationTokenSource { get; set; }
+
+    /// <inheritdoc/>
+    public void Initialize (ConcurrentQueue<TInputRecord> inputQueue) { _inputQueue = inputQueue; }
+
+    /// <inheritdoc/>
+    public void Run (CancellationToken runCancellationToken)
+    {
+        // Create a linked token source if we have an external one
+        CancellationTokenSource? linkedCts = null;
+        CancellationToken effectiveToken = runCancellationToken;
+
+        if (ExternalCancellationTokenSource != null)
+        {
+            linkedCts = CancellationTokenSource.CreateLinkedTokenSource (runCancellationToken, ExternalCancellationTokenSource.Token);
+            effectiveToken = linkedCts.Token;
+        }
+
+        try
+        {
+            if (_inputQueue == null)
+            {
+                throw new ("Cannot run input before Initialization");
+            }
+
+            do
+            {
+                DateTime dt = Now ();
+
+                while (Peek ())
+                {
+                    foreach (TInputRecord r in Read ())
+                    {
+                        _inputQueue.Enqueue (r);
+                    }
+                }
+
+                effectiveToken.ThrowIfCancellationRequested ();
+            }
+            while (!effectiveToken.IsCancellationRequested);
+        }
+        catch (OperationCanceledException)
+        { }
+        finally
+        {
+            Logging.Trace($"Stopping input processing");
+            linkedCts?.Dispose ();
+        }
+    }
+
+    /// <summary>
+    ///     When implemented in a derived class, returns true if there is data available
+    ///     to read from console.
+    /// </summary>
+    /// <returns></returns>
+    public abstract bool Peek ();
+
+    /// <summary>
+    ///     Returns the available data without blocking, called when <see cref="Peek"/>
+    ///     returns <see langword="true"/>.
+    /// </summary>
+    /// <returns></returns>
+    public abstract IEnumerable<TInputRecord> Read ();
+
+    /// <inheritdoc/>
+    public virtual void Dispose () { }
+}

+ 117 - 79
Terminal.Gui/Drivers/InputProcessor.cs → Terminal.Gui/Drivers/InputProcessorImpl.cs

@@ -5,30 +5,67 @@ using Microsoft.Extensions.Logging;
 namespace Terminal.Gui.Drivers;
 
 /// <summary>
-///     Processes the queued input buffer contents - which must be of Type <typeparamref name="T"/>.
+///     Processes the queued input queue contents - which must be of Type <typeparamref name="TInputRecord"/>.
 ///     Is responsible for <see cref="ProcessQueue"/> and translating into common Terminal.Gui
-///     events and data models.
+///     events and data models. Runs on the main loop thread.
 /// </summary>
-public abstract class InputProcessor<T> : IInputProcessor
+public abstract class InputProcessorImpl<TInputRecord> : IInputProcessor, IDisposable where TInputRecord : struct
 {
+    /// <summary>
+    ///     Constructs base instance including wiring all relevant
+    ///     parser events and setting <see cref="InputQueue"/> to
+    ///     the provided thread safe input collection.
+    /// </summary>
+    /// <param name="inputBuffer">The collection that will be populated with new input (see <see cref="IInput{T}"/>)</param>
+    /// <param name="keyConverter">
+    ///     Key converter for translating driver specific
+    ///     <typeparamref name="TInputRecord"/> class into Terminal.Gui <see cref="Key"/>.
+    /// </param>
+    protected InputProcessorImpl (ConcurrentQueue<TInputRecord> inputBuffer, IKeyConverter<TInputRecord> keyConverter)
+    {
+        InputQueue = inputBuffer;
+        Parser.HandleMouse = true;
+        Parser.Mouse += (s, e) => RaiseMouseEvent (e);
+
+        Parser.HandleKeyboard = true;
+
+        Parser.Keyboard += (s, k) =>
+                           {
+                               RaiseKeyDownEvent (k);
+                               RaiseKeyUpEvent (k);
+                           };
+
+        // TODO: For now handle all other escape codes with ignore
+        Parser.UnexpectedResponseHandler = str =>
+                                           {
+                                               var cur = new string (str.Select (k => k.Item1).ToArray ());
+                                               Logging.Logger.LogInformation ($"{nameof (InputProcessorImpl<TInputRecord>)} ignored unrecognized response '{cur}'");
+                                               AnsiSequenceSwallowed?.Invoke (this, cur);
+
+                                               return true;
+                                           };
+        KeyConverter = keyConverter;
+    }
+
     /// <summary>
     ///     How long after Esc has been pressed before we give up on getting an Ansi escape sequence
     /// </summary>
     private readonly TimeSpan _escTimeout = TimeSpan.FromMilliseconds (50);
 
-    internal AnsiResponseParser<T> Parser { get; } = new ();
+    internal AnsiResponseParser<TInputRecord> Parser { get; } = new ();
 
     /// <summary>
-    ///     Class responsible for translating the driver specific native input class <typeparamref name="T"/> e.g.
+    ///     Class responsible for translating the driver specific native input class <typeparamref name="TInputRecord"/> e.g.
     ///     <see cref="ConsoleKeyInfo"/> into the Terminal.Gui <see cref="Key"/> class (used for all
     ///     internal library representations of Keys).
     /// </summary>
-    public IKeyConverter<T> KeyConverter { get; }
+    public IKeyConverter<TInputRecord> KeyConverter { get; }
 
     /// <summary>
-    ///     Input buffer which will be drained from by this class.
+    ///     The input queue which is filled by <see cref="IInput{TInputRecord}"/> implementations running on the input thread.
+    ///     Implementations of this class should dequeue from this queue in <see cref="ProcessQueue"/> on the main loop thread.
     /// </summary>
-    public ConcurrentQueue<T> InputBuffer { get; }
+    public ConcurrentQueue<TInputRecord> InputQueue { get; }
 
     /// <inheritdoc />
     public string? DriverName { get; init; }
@@ -38,110 +75,95 @@ public abstract class InputProcessor<T> : IInputProcessor
 
     private readonly MouseInterpreter _mouseInterpreter = new ();
 
-    /// <summary>Event fired when a key is pressed down. This is a precursor to <see cref="KeyUp"/>.</summary>
+    /// <inheritdoc />
     public event EventHandler<Key>? KeyDown;
 
-    /// <summary>Event fired when a terminal sequence read from input is not recognized and therefore ignored.</summary>
+    /// <inheritdoc />
     public event EventHandler<string>? AnsiSequenceSwallowed;
 
-    /// <summary>
-    ///     Called when a key is pressed down. Fires the <see cref="KeyDown"/> event. This is a precursor to
-    ///     <see cref="OnKeyUp"/>.
-    /// </summary>
-    /// <param name="a"></param>
-    public void OnKeyDown (Key a)
+    /// <inheritdoc />
+    public void RaiseKeyDownEvent (Key a)
     {
-        Logging.Trace ($"{nameof (InputProcessor<T>)} raised {a}");
         KeyDown?.Invoke (this, a);
     }
 
-    /// <summary>Event fired when a key is released.</summary>
-    /// <remarks>
-    ///     Drivers that do not support key release events will fire this event after <see cref="KeyDown"/> processing is
-    ///     complete.
-    /// </remarks>
+    /// <inheritdoc />
     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); }
+    /// <inheritdoc />
+    public void RaiseKeyUpEvent (Key a) { KeyUp?.Invoke (this, a); }
 
-    /// <summary>Event fired when a mouse event occurs.</summary>
-    public event EventHandler<MouseEventArgs>? MouseEvent;
+    /// <summary>
+    /// 
+    /// </summary>
+    public IInput<TInputRecord>? InputImpl { get; set; }  // Set by MainLoopCoordinator
 
-    /// <summary>Called when a mouse event occurs. Fires the <see cref="MouseEvent"/> event.</summary>
-    /// <param name="a"></param>
-    public void OnMouseEvent (MouseEventArgs a)
+    /// <inheritdoc />
+    public void EnqueueKeyDownEvent (Key key)
     {
-        // Ensure ScreenPosition is set
-        a.ScreenPosition = a.Position;
+        // Convert Key → TInputRecord
+        TInputRecord inputRecord = KeyConverter.ToKeyInfo (key);
 
-        foreach (MouseEventArgs e in _mouseInterpreter.Process (a))
+        // If input supports testing, use InputImplPeek/Read pipeline
+        // which runs on the input thread.
+        if (InputImpl is ITestableInput<TInputRecord> testableInput)
         {
-           // Logging.Trace ($"Mouse Interpreter raising {e.Flags}");
-
-            // Pass on
-            MouseEvent?.Invoke (this, e);
+            testableInput.AddInput (inputRecord);
         }
     }
 
-    /// <summary>
-    ///     Constructs base instance including wiring all relevant
-    ///     parser events and setting <see cref="InputBuffer"/> to
-    ///     the provided thread safe input collection.
-    /// </summary>
-    /// <param name="inputBuffer">The collection that will be populated with new input (see <see cref="IConsoleInput{T}"/>)</param>
-    /// <param name="keyConverter">
-    ///     Key converter for translating driver specific
-    ///     <typeparamref name="T"/> class into Terminal.Gui <see cref="Key"/>.
-    /// </param>
-    protected InputProcessor (ConcurrentQueue<T> inputBuffer, IKeyConverter<T> keyConverter)
+    /// <inheritdoc />
+    public void EnqueueKeyUpEvent (Key key)
     {
-        InputBuffer = inputBuffer;
-        Parser.HandleMouse = true;
-        Parser.Mouse += (s, e) => OnMouseEvent (e);
+        // TODO: Determine if we can still support this on Windows
+        throw new NotImplementedException ();
+    }
 
-        Parser.HandleKeyboard = true;
+    /// <inheritdoc />
+    public event EventHandler<MouseEventArgs>? MouseEvent;
 
-        Parser.Keyboard += (s, k) =>
-                           {
-                               OnKeyDown (k);
-                               OnKeyUp (k);
-                           };
+    /// <inheritdoc />
+    public virtual void EnqueueMouseEvent (MouseEventArgs mouseEvent)
+    {
+        // Base implementation: For drivers where TInputRecord cannot represent mouse events
+        // (e.g., ConsoleKeyInfo), derived classes should override this method.
+        // See WindowsInputProcessor for an example implementation that converts MouseEventArgs
+        // to InputRecord and enqueues it.
+        Logging.Logger.LogWarning (
+            $"{DriverName ?? "Unknown"} driver's InputProcessor does not support EnqueueMouseEvent. " +
+            "Override this method to enable mouse event enqueueing for testing.");
+    }
 
-        // TODO: For now handle all other escape codes with ignore
-        Parser.UnexpectedResponseHandler = str =>
-                                           {
-                                               var cur = new string (str.Select (k => k.Item1).ToArray ());
-                                               Logging.Logger.LogInformation ($"{nameof (InputProcessor<T>)} ignored unrecognized response '{cur}'");
-                                               AnsiSequenceSwallowed?.Invoke (this, cur);
+    /// <inheritdoc />
+    public void RaiseMouseEvent (MouseEventArgs a)
+    {
+        // Ensure ScreenPosition is set
+        a.ScreenPosition = a.Position;
 
-                                               return true;
-                                           };
-        KeyConverter = keyConverter;
+        foreach (MouseEventArgs e in _mouseInterpreter.Process (a))
+        {
+            // Logging.Trace ($"Mouse Interpreter raising {e.Flags}");
+
+            // Pass on
+            MouseEvent?.Invoke (this, e);
+        }
     }
 
-    /// <summary>
-    ///     Drains the <see cref="InputBuffer"/> buffer, processing all available keystrokes
-    /// </summary>
+    /// <inheritdoc />
     public void ProcessQueue ()
     {
-        while (InputBuffer.TryDequeue (out T? input))
+        while (InputQueue.TryDequeue (out TInputRecord input))
         {
             Process (input);
         }
 
-        foreach (T input in ReleaseParserHeldKeysIfStale ())
+        foreach (TInputRecord input in ReleaseParserHeldKeysIfStale ())
         {
             ProcessAfterParsing (input);
         }
     }
 
-    private IEnumerable<T> ReleaseParserHeldKeysIfStale ()
+    private IEnumerable<TInputRecord> ReleaseParserHeldKeysIfStale ()
     {
         if (Parser.State is AnsiResponseParserState.ExpectingEscapeSequence or AnsiResponseParserState.InResponse
             && DateTime.Now - Parser.StateChangedAt > _escTimeout)
@@ -154,17 +176,27 @@ public abstract class InputProcessor<T> : IInputProcessor
 
     /// <summary>
     ///     Process the provided single input element <paramref name="input"/>. This method
-    ///     is called sequentially for each value read from <see cref="InputBuffer"/>.
+    ///     is called sequentially for each value read from <see cref="InputQueue"/>.
     /// </summary>
     /// <param name="input"></param>
-    protected abstract void Process (T input);
+    protected abstract void Process (TInputRecord input);
 
     /// <summary>
     ///     Process the provided single input element - short-circuiting the <see cref="Parser"/>
     ///     stage of the processing.
     /// </summary>
     /// <param name="input"></param>
-    protected abstract void ProcessAfterParsing (T input);
+    protected virtual void ProcessAfterParsing (TInputRecord input)
+    {
+        var key = KeyConverter.ToKey (input);
+
+        // If the key is not valid, we don't want to raise any events.
+        if (IsValidInput (key, out key))
+        {
+            RaiseKeyDownEvent (key);
+            RaiseKeyUpEvent (key);
+        }
+    }
 
     private char _highSurrogate = '\0';
 
@@ -221,4 +253,10 @@ public abstract class InputProcessor<T> : IInputProcessor
 
         return true;
     }
+
+    /// <inheritdoc/>
+    public CancellationTokenSource? ExternalCancellationTokenSource { get; set; }
+
+    /// <inheritdoc />
+    public void Dispose () { ExternalCancellationTokenSource?.Dispose (); }
 }

+ 1 - 1
Terminal.Gui/Drivers/KeyCode.cs

@@ -2,7 +2,7 @@
 namespace Terminal.Gui.Drivers;
 
 /// <summary>
-///     The <see cref="KeyCode"/> enumeration encodes key information from <see cref="IConsoleDriver"/>s and provides a
+///     The <see cref="KeyCode"/> enumeration encodes key information from <see cref="IDriver"/>s and provides a
 ///     consistent way for application code to specify keys and receive key events.
 ///     <para>
 ///         The <see cref="Key"/> class provides a higher-level abstraction, with helper methods and properties for

+ 2 - 7
Terminal.Gui/Drivers/OutputBase.cs

@@ -2,7 +2,7 @@
 namespace Terminal.Gui.Drivers;
 
 /// <summary>
-///     Abstract base class to assist with implementing <see cref="IConsoleOutput"/>.
+///     Abstract base class to assist with implementing <see cref="IOutput"/>.
 /// </summary>
 public abstract class OutputBase
 {
@@ -18,14 +18,9 @@ public abstract class OutputBase
     /// <param name="visibility"></param>
     public abstract void SetCursorVisibility (CursorVisibility visibility);
 
-    /// <inheritdoc cref="IConsoleOutput.Write(IOutputBuffer)"/>
+    /// <inheritdoc cref="IOutput.Write(IOutputBuffer)"/>
     public virtual void Write (IOutputBuffer buffer)
     {
-        if (ConsoleDriver.RunningUnitTests)
-        {
-            return;
-        }
-
         var top = 0;
         var left = 0;
         int rows = buffer.Rows;

+ 1 - 1
Terminal.Gui/Drivers/OutputBuffer.cs → Terminal.Gui/Drivers/OutputBufferImpl.cs

@@ -8,7 +8,7 @@ namespace Terminal.Gui.Drivers;
 ///     draw operations before being flushed to the console as part of the main loop.
 ///     operation
 /// </summary>
-public class OutputBuffer : IOutputBuffer
+public class OutputBufferImpl : IOutputBuffer
 {
     /// <summary>
     ///     The contents of the application output. The driver outputs this buffer to the terminal when

Некоторые файлы не были показаны из-за большого количества измененных файлов