diff --git a/.github/workflows/build.yml b/.github/workflows/build-validation.yml similarity index 77% rename from .github/workflows/build.yml rename to .github/workflows/build-validation.yml index d5f71b92d..9f49a7375 100644 --- a/.github/workflows/build.yml +++ b/.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 diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index ae2da454b..ba315db00 100644 --- a/.github/workflows/integration-tests.yml +++ b/.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: Checkout code - uses: actions/checkout@v4 + - name: Setup .NET Core + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.x + dotnet-quality: ga - - 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: Download build artifacts - uses: actions/download-artifact@v4 - with: - name: build-artifacts - path: . + - name: Restore NuGet packages + run: dotnet restore - - name: Restore NuGet packages - run: dotnet restore + - 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/${{ runner.os }}/}" >> $GITHUB_ENV + - name: Set VSTEST_DUMP_PATH + shell: bash + run: echo "VSTEST_DUMP_PATH=logs/IntegrationTests/${{ runner.os }}/" >> $GITHUB_ENV - - 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: 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: Upload Integration Test Logs + if: always() + uses: actions/upload-artifact@v4 + with: + name: integration_tests-logs-${{ runner.os }} + path: | + logs/IntegrationTests/ + TestResults/ + + - 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 diff --git a/.github/workflows/quick-build.yml b/.github/workflows/quick-build.yml new file mode 100644 index 000000000..01695d8c3 --- /dev/null +++ b/.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 diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 07a7e00d5..4488bb645 100644 --- a/.github/workflows/unit-tests.yml +++ b/.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 diff --git a/.gitignore b/.gitignore index e2b23118e..cdec09ec2 100644 --- a/.gitignore +++ b/.gitignore @@ -68,3 +68,8 @@ BenchmarkDotNet.Artifacts/ *.log.* log.* + +/Tests/coverage/ +!/Tests/coverage/.gitkeep # keep folder in repo +/Tests/report/ +*.cobertura.xml diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d6686b758..2e985e560 100644 --- a/CONTRIBUTING.md +++ b/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(); ``` -- **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 diff --git a/Examples/NativeAot/Program.cs b/Examples/NativeAot/Program.cs index bfc566c6d..3de9bfeec 100644 --- a/Examples/NativeAot/Program.cs +++ b/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); diff --git a/Examples/SelfContained/Program.cs b/Examples/SelfContained/Program.cs index 2cfdb8cc9..02109bf3a 100644 --- a/Examples/SelfContained/Program.cs +++ b/Examples/SelfContained/Program.cs @@ -12,7 +12,7 @@ namespace SelfContained; public static class Program { - [RequiresUnreferencedCode ("Calls Terminal.Gui.Application.Run(Func, IConsoleDriver)")] + [RequiresUnreferencedCode ("Calls Terminal.Gui.Application.Run(Func, IDriver)")] private static void Main (string [] args) { ConfigurationManager.Enable (ConfigLocations.All); diff --git a/Examples/UICatalog/Scenario.cs b/Examples/UICatalog/Scenario.cs index b0a6e5f36..76fc5dc2a 100644 --- a/Examples/UICatalog/Scenario.cs +++ b/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!); diff --git a/Examples/UICatalog/Scenarios/GraphViewExample.cs b/Examples/UICatalog/Scenarios/GraphViewExample.cs index 507a0c475..5dcd5df40 100644 --- a/Examples/UICatalog/Scenarios/GraphViewExample.cs +++ b/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; diff --git a/Examples/UICatalog/Scenarios/Keys.cs b/Examples/UICatalog/Scenarios/Keys.cs index 8f0b0094d..0c17a15f8 100644 --- a/Examples/UICatalog/Scenarios/Keys.cs +++ b/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"); diff --git a/Examples/UICatalog/Scenarios/Mazing.cs b/Examples/UICatalog/Scenarios/Mazing.cs index 0cf2e1bee..01bbc41a4 100644 --- a/Examples/UICatalog/Scenarios/Mazing.cs +++ b/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; diff --git a/Examples/UICatalog/Scenarios/Navigation.cs b/Examples/UICatalog/Scenarios/Navigation.cs index 7a3390c6f..7ce1d3317 100644 --- a/Examples/UICatalog/Scenarios/Navigation.cs +++ b/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 e) { testFrame.SetScheme (testFrame.GetScheme () with { Normal = new (testFrame.GetAttributeForRole (VisualRole.Normal).Foreground, e.Result) }); diff --git a/Examples/UICatalog/Scenarios/Notepad.cs b/Examples/UICatalog/Scenarios/Notepad.cs index fd3259807..996702298 100644 --- a/Examples/UICatalog/Scenarios/Notepad.cs +++ b/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 /// Creates a new tab with initial text /// File that was read or null if a new blank document + /// 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}"; }; } /// The text of the tab the last time it was saved /// - 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)) { diff --git a/Examples/UICatalog/Scenarios/NumericUpDownDemo.cs b/Examples/UICatalog/Scenarios/NumericUpDownDemo.cs index 5694e4be9..c75c5bdb5 100644 --- a/Examples/UICatalog/Scenarios/NumericUpDownDemo.cs +++ b/Examples/UICatalog/Scenarios/NumericUpDownDemo.cs @@ -266,14 +266,14 @@ internal class NumericUpDownEditor : View where T : notnull void NumericUpDownOnIncrementChanged (object? o, EventArgs 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 (); } } diff --git a/Examples/UICatalog/Scenarios/TableEditor.cs b/Examples/UICatalog/Scenarios/TableEditor.cs index 29b03b064..84d7644b9 100644 --- a/Examples/UICatalog/Scenarios/TableEditor.cs +++ b/Examples/UICatalog/Scenarios/TableEditor.cs @@ -1,4 +1,5 @@ -using System.Data; +#nullable enable +using System.Data; using System.Globalization; using System.Text; @@ -11,333 +12,471 @@ namespace UICatalog.Scenarios; [ScenarioCategory ("Text and Formatting")] public class TableEditor : Scenario { - private readonly HashSet _checkedFileSystemInfos = new (); - private readonly List _toDispose = new (); + private readonly HashSet? _checkedFileSystemInfos = []; + private readonly List? _toDispose = []; - private readonly List Ranges = new () - { + private readonly List? _ranges = + [ new ( 0x0000, 0x001F, "ASCII Control Characters" ), + new (0x0080, 0x009F, "C0 Control Characters"), + new ( 0x1100, 0x11ff, "Hangul Jamo" ), // This is where wide chars tend to start + new (0x20A0, 0x20CF, "Currency Symbols"), + new (0x2100, 0x214F, "Letterlike Symbols"), + new (0x2190, 0x21ff, "Arrows"), + new (0x2200, 0x22ff, "Mathematical symbols"), + new ( 0x2300, 0x23ff, "Miscellaneous Technical" ), + new ( 0x2500, 0x25ff, "Box Drawing & Geometric Shapes" ), + new (0x2600, 0x26ff, "Miscellaneous Symbols"), + new (0x2700, 0x27ff, "Dingbats"), + new (0x2800, 0x28ff, "Braille"), + new ( 0x2b00, 0x2bff, "Miscellaneous Symbols and Arrows" ), + new ( 0xFB00, 0xFb4f, "Alphabetic Presentation Forms" ), + new ( 0x12400, 0x1240f, "Cuneiform Numbers and Punctuation" ), + new ( (uint)(Terminal.Gui.Views.UnicodeRange.Ranges.Max (r => r.End) - 16), (uint)Terminal.Gui.Views.UnicodeRange.Ranges.Max (r => r.End), "End" ), + new (0x0020, 0x007F, "Basic Latin"), + new (0x00A0, 0x00FF, "Latin-1 Supplement"), + new (0x0100, 0x017F, "Latin Extended-A"), + new (0x0180, 0x024F, "Latin Extended-B"), + new (0x0250, 0x02AF, "IPA Extensions"), + new ( 0x02B0, 0x02FF, "Spacing Modifier Letters" ), + new ( 0x0300, 0x036F, "Combining Diacritical Marks" ), + new (0x0370, 0x03FF, "Greek and Coptic"), + new (0x0400, 0x04FF, "Cyrillic"), + new (0x0500, 0x052F, "Cyrillic Supplementary"), + new (0x0530, 0x058F, "Armenian"), + new (0x0590, 0x05FF, "Hebrew"), + new (0x0600, 0x06FF, "Arabic"), + new (0x0700, 0x074F, "Syriac"), + new (0x0780, 0x07BF, "Thaana"), + new (0x0900, 0x097F, "Devanagari"), + new (0x0980, 0x09FF, "Bengali"), + new (0x0A00, 0x0A7F, "Gurmukhi"), + new (0x0A80, 0x0AFF, "Gujarati"), + new (0x0B00, 0x0B7F, "Oriya"), + new (0x0B80, 0x0BFF, "Tamil"), + new (0x0C00, 0x0C7F, "Telugu"), + new (0x0C80, 0x0CFF, "Kannada"), + new (0x0D00, 0x0D7F, "Malayalam"), + new (0x0D80, 0x0DFF, "Sinhala"), + new (0x0E00, 0x0E7F, "Thai"), + new (0x0E80, 0x0EFF, "Lao"), + new (0x0F00, 0x0FFF, "Tibetan"), + new (0x1000, 0x109F, "Myanmar"), + new (0x10A0, 0x10FF, "Georgian"), + new (0x1100, 0x11FF, "Hangul Jamo"), + new (0x1200, 0x137F, "Ethiopic"), + new (0x13A0, 0x13FF, "Cherokee"), + new ( 0x1400, 0x167F, "Unified Canadian Aboriginal Syllabics" ), + new (0x1680, 0x169F, "Ogham"), + new (0x16A0, 0x16FF, "Runic"), + new (0x1700, 0x171F, "Tagalog"), + new (0x1720, 0x173F, "Hanunoo"), + new (0x1740, 0x175F, "Buhid"), + new (0x1760, 0x177F, "Tagbanwa"), + new (0x1780, 0x17FF, "Khmer"), + new (0x1800, 0x18AF, "Mongolian"), + new (0x1900, 0x194F, "Limbu"), + new (0x1950, 0x197F, "Tai Le"), + new (0x19E0, 0x19FF, "Khmer Symbols"), + new (0x1D00, 0x1D7F, "Phonetic Extensions"), + new ( 0x1E00, 0x1EFF, "Latin Extended Additional" ), + new (0x1F00, 0x1FFF, "Greek Extended"), + new (0x2000, 0x206F, "General Punctuation"), + new ( 0x2070, 0x209F, "Superscripts and Subscripts" ), + new (0x20A0, 0x20CF, "Currency Symbols"), + new ( 0x20D0, 0x20FF, "Combining Diacritical Marks for Symbols" ), + new (0x2100, 0x214F, "Letterlike Symbols"), + new (0x2150, 0x218F, "Number Forms"), + new (0x2190, 0x21FF, "Arrows"), + new (0x2200, 0x22FF, "Mathematical Operators"), + new ( 0x2300, 0x23FF, "Miscellaneous Technical" ), + new (0x2400, 0x243F, "Control Pictures"), + new ( 0x2440, 0x245F, "Optical Character Recognition" ), + new (0x2460, 0x24FF, "Enclosed Alphanumerics"), + new (0x2500, 0x257F, "Box Drawing"), + new (0x2580, 0x259F, "Block Elements"), + new (0x25A0, 0x25FF, "Geometric Shapes"), + new (0x2600, 0x26FF, "Miscellaneous Symbols"), + new (0x2700, 0x27BF, "Dingbats"), + new ( 0x27C0, 0x27EF, "Miscellaneous Mathematical Symbols-A" ), + new (0x27F0, 0x27FF, "Supplemental Arrows-A"), + new (0x2800, 0x28FF, "Braille Patterns"), + new (0x2900, 0x297F, "Supplemental Arrows-B"), + new ( 0x2980, 0x29FF, "Miscellaneous Mathematical Symbols-B" ), + new ( 0x2A00, 0x2AFF, "Supplemental Mathematical Operators" ), + new ( 0x2B00, 0x2BFF, "Miscellaneous Symbols and Arrows" ), + new ( 0x2E80, 0x2EFF, "CJK Radicals Supplement" ), + new (0x2F00, 0x2FDF, "Kangxi Radicals"), + new ( 0x2FF0, 0x2FFF, "Ideographic Description Characters" ), + new ( 0x3000, 0x303F, "CJK Symbols and Punctuation" ), + new (0x3040, 0x309F, "Hiragana"), + new (0x30A0, 0x30FF, "Katakana"), + new (0x3100, 0x312F, "Bopomofo"), + new ( 0x3130, 0x318F, "Hangul Compatibility Jamo" ), + new (0x3190, 0x319F, "Kanbun"), + new (0x31A0, 0x31BF, "Bopomofo Extended"), + new ( 0x31F0, 0x31FF, "Katakana Phonetic Extensions" ), + new ( 0x3200, 0x32FF, "Enclosed CJK Letters and Months" ), + new (0x3300, 0x33FF, "CJK Compatibility"), + new ( 0x3400, 0x4DBF, "CJK Unified Ideographs Extension A" ), + new ( 0x4DC0, 0x4DFF, "Yijing Hexagram Symbols" ), + new (0x4E00, 0x9FFF, "CJK Unified Ideographs"), + new (0xA000, 0xA48F, "Yi Syllables"), + new (0xA490, 0xA4CF, "Yi Radicals"), + new (0xAC00, 0xD7AF, "Hangul Syllables"), + new (0xD800, 0xDB7F, "High Surrogates"), + new ( 0xDB80, 0xDBFF, "High Private Use Surrogates" ), + new (0xDC00, 0xDFFF, "Low Surrogates"), + new (0xE000, 0xF8FF, "Private Use Area"), + new ( 0xF900, 0xFAFF, "CJK Compatibility Ideographs" ), + new ( 0xFB00, 0xFB4F, "Alphabetic Presentation Forms" ), + new ( 0xFB50, 0xFDFF, "Arabic Presentation Forms-A" ), + new (0xFE00, 0xFE0F, "Variation Selectors"), + new (0xFE20, 0xFE2F, "Combining Half Marks"), + new ( 0xFE30, 0xFE4F, "CJK Compatibility Forms" ), + new (0xFE50, 0xFE6F, "Small Form Variants"), + new ( 0xFE70, 0xFEFF, "Arabic Presentation Forms-B" ), + new ( 0xFF00, 0xFFEF, "Halfwidth and Fullwidth Forms" ), + new (0xFFF0, 0xFFFF, "Specials"), + new (0x10000, 0x1007F, "Linear B Syllabary"), + new (0x10080, 0x100FF, "Linear B Ideograms"), + new (0x10100, 0x1013F, "Aegean Numbers"), + new (0x10300, 0x1032F, "Old Italic"), + new (0x10330, 0x1034F, "Gothic"), + new (0x10380, 0x1039F, "Ugaritic"), + new (0x10400, 0x1044F, "Deseret"), + new (0x10450, 0x1047F, "Shavian"), + new (0x10480, 0x104AF, "Osmanya"), + new (0x10800, 0x1083F, "Cypriot Syllabary"), + new ( 0x1D000, 0x1D0FF, "Byzantine Musical Symbols" ), + new (0x1D100, 0x1D1FF, "Musical Symbols"), + new ( 0x1D300, 0x1D35F, "Tai Xuan Jing Symbols" ), + new ( 0x1D400, 0x1D7FF, "Mathematical Alphanumeric Symbols" ), + new (0x1F600, 0x1F532, "Emojis Symbols"), + new ( 0x20000, 0x2A6DF, "CJK Unified Ideographs Extension B" ), + new ( 0x2F800, 0x2FA1F, "CJK Compatibility Ideographs Supplement" ), - new (0xE0000, 0xE007F, "Tags") - }; - private Scheme _alternatingScheme; - private DataTable _currentTable; - private MenuItem _miAlternatingColors; - private MenuItem _miAlwaysShowHeaders; - private MenuItem _miAlwaysUseNormalColorForVerticalCellLines; - private MenuItem _miBottomline; - private MenuItem _miCellLines; - private MenuItem _miCheckboxes; - private MenuItem _miCursor; - private MenuItem _miExpandLastColumn; - private MenuItem _miFullRowSelect; - private MenuItem _miHeaderMidline; - private MenuItem _miHeaderOverline; - private MenuItem _miHeaderUnderline; - private MenuItem _miRadioboxes; - private MenuItem _miShowHeaders; - private MenuItem _miShowHorizontalScrollIndicators; - private MenuItem _miSmoothScrolling; - private Scheme _redScheme; - private Scheme _redSchemeAlt; - private TableView _tableView; + new (0xE0000, 0xE007F, "Tags") + ]; + + private Scheme? _alternatingScheme; + private DataTable? _currentTable; + private MenuItem? _miAlternatingColors; + private MenuItem? _miAlwaysShowHeaders; + private MenuItem? _miAlwaysUseNormalColorForVerticalCellLines; + private MenuItem? _miBottomline; + private MenuItem? _miCellLines; + private MenuItem? _miCheckboxes; + private MenuItem? _miCursor; + private MenuItem? _miExpandLastColumn; + private MenuItem? _miFullRowSelect; + private MenuItem? _miHeaderMidline; + private MenuItem? _miHeaderOverline; + private MenuItem? _miHeaderUnderline; + private MenuItem? _miRadioboxes; + private MenuItem? _miShowHeaders; + private MenuItem? _miShowHorizontalScrollIndicators; + private MenuItem? _miSmoothScrolling; + private Scheme? _redScheme; + private Scheme? _redSchemeAlt; + private TableView? _tableView; /// /// Builds a simple table in which cell values contents are the index of the cell. This helps testing that @@ -427,7 +566,7 @@ public class TableEditor : Scenario () => ToggleShowHeaders () ) { - Checked = _tableView.Style.ShowHeaders, + Checked = _tableView!.Style.ShowHeaders, CheckType = MenuItemCheckStyle.Checked }, _miAlwaysShowHeaders = @@ -437,7 +576,7 @@ public class TableEditor : Scenario () => ToggleAlwaysShowHeaders () ) { - Checked = _tableView.Style.AlwaysShowHeaders, + Checked = _tableView!.Style.AlwaysShowHeaders, CheckType = MenuItemCheckStyle.Checked }, _miHeaderOverline = @@ -447,7 +586,7 @@ public class TableEditor : Scenario () => ToggleOverline () ) { - Checked = _tableView.Style + Checked = _tableView!.Style .ShowHorizontalHeaderOverline, CheckType = MenuItemCheckStyle.Checked }, @@ -457,7 +596,7 @@ public class TableEditor : Scenario () => ToggleHeaderMidline () ) { - Checked = _tableView.Style + Checked = _tableView!.Style .ShowVerticalHeaderLines, CheckType = MenuItemCheckStyle.Checked }, @@ -467,7 +606,7 @@ public class TableEditor : Scenario () => ToggleUnderline () ) { - Checked = _tableView.Style + Checked = _tableView!.Style .ShowHorizontalHeaderUnderline, CheckType = MenuItemCheckStyle.Checked }, @@ -477,7 +616,7 @@ public class TableEditor : Scenario () => ToggleBottomline () ) { - Checked = _tableView.Style + Checked = _tableView!.Style .ShowHorizontalBottomline, CheckType = MenuItemCheckStyle .Checked @@ -490,7 +629,7 @@ public class TableEditor : Scenario ToggleHorizontalScrollIndicators () ) { - Checked = _tableView.Style + Checked = _tableView!.Style .ShowHorizontalScrollIndicators, CheckType = MenuItemCheckStyle.Checked }, @@ -500,7 +639,7 @@ public class TableEditor : Scenario () => ToggleFullRowSelect () ) { - Checked = _tableView.FullRowSelect, + Checked = _tableView!.FullRowSelect, CheckType = MenuItemCheckStyle.Checked }, _miCellLines = new ( @@ -509,7 +648,7 @@ public class TableEditor : Scenario () => ToggleCellLines () ) { - Checked = _tableView.Style + Checked = _tableView!.Style .ShowVerticalCellLines, CheckType = MenuItemCheckStyle .Checked @@ -521,7 +660,7 @@ public class TableEditor : Scenario () => ToggleExpandLastColumn () ) { - Checked = _tableView.Style.ExpandLastColumn, + Checked = _tableView!.Style.ExpandLastColumn, CheckType = MenuItemCheckStyle.Checked }, _miAlwaysUseNormalColorForVerticalCellLines = @@ -532,7 +671,7 @@ public class TableEditor : Scenario ToggleAlwaysUseNormalColorForVerticalCellLines () ) { - Checked = _tableView.Style + Checked = _tableView!.Style .AlwaysUseNormalColorForVerticalCellLines, CheckType = MenuItemCheckStyle.Checked }, @@ -543,7 +682,7 @@ public class TableEditor : Scenario () => ToggleSmoothScrolling () ) { - Checked = _tableView.Style + Checked = _tableView!.Style .SmoothHorizontalScrolling, CheckType = MenuItemCheckStyle.Checked }, @@ -581,7 +720,7 @@ public class TableEditor : Scenario ToggleInvertSelectedCellFirstCharacter () ) { - Checked = _tableView.Style + Checked = _tableView!.Style .InvertSelectedCellFirstCharacter, CheckType = MenuItemCheckStyle.Checked }, @@ -657,45 +796,45 @@ public class TableEditor : Scenario appWindow.Add (_tableView); - _tableView.SelectedCellChanged += (s, e) => { selectedCellLabel.Text = $"{_tableView.SelectedRow},{_tableView.SelectedColumn}"; }; - _tableView.CellActivated += EditCurrentCell; - _tableView.KeyDown += TableViewKeyPress; + _tableView!.SelectedCellChanged += (s, e) => { selectedCellLabel.Text = $"{_tableView!.SelectedRow},{_tableView!.SelectedColumn}"; }; + _tableView!.CellActivated += EditCurrentCell; + _tableView!.KeyDown += TableViewKeyPress; //SetupScrollBar (); _redScheme = new () { - Disabled = appWindow.GetAttributeForRole(VisualRole.Disabled), - HotFocus = appWindow.GetAttributeForRole(VisualRole.HotFocus), - Focus = appWindow.GetAttributeForRole(VisualRole.Focus), - Normal = new (Color.Red, appWindow.GetAttributeForRole(VisualRole.Normal).Background) + Disabled = appWindow.GetAttributeForRole (VisualRole.Disabled), + HotFocus = appWindow.GetAttributeForRole (VisualRole.HotFocus), + Focus = appWindow.GetAttributeForRole (VisualRole.Focus), + Normal = new (Color.Red, appWindow.GetAttributeForRole (VisualRole.Normal).Background) }; _alternatingScheme = new () { - Disabled = appWindow.GetAttributeForRole(VisualRole.Disabled), - HotFocus = appWindow.GetAttributeForRole(VisualRole.HotFocus), - Focus = appWindow.GetAttributeForRole(VisualRole.Focus), + Disabled = appWindow.GetAttributeForRole (VisualRole.Disabled), + HotFocus = appWindow.GetAttributeForRole (VisualRole.HotFocus), + Focus = appWindow.GetAttributeForRole (VisualRole.Focus), Normal = new (Color.White, Color.BrightBlue) }; _redSchemeAlt = new () { - Disabled = appWindow.GetAttributeForRole(VisualRole.Disabled), - HotFocus = appWindow.GetAttributeForRole(VisualRole.HotFocus), - Focus = appWindow.GetAttributeForRole(VisualRole.Focus), + Disabled = appWindow.GetAttributeForRole (VisualRole.Disabled), + HotFocus = appWindow.GetAttributeForRole (VisualRole.HotFocus), + Focus = appWindow.GetAttributeForRole (VisualRole.Focus), Normal = new (Color.Red, Color.BrightBlue) }; // if user clicks the mouse in TableView - _tableView.MouseClick += (s, e) => + _tableView!.MouseClick += (s, e) => { if (_currentTable == null) { return; } - _tableView.ScreenToCell (e.Position, out int? clickedCol); + _tableView!.ScreenToCell (e.Position, out int? clickedCol); if (clickedCol != null) { @@ -712,7 +851,7 @@ public class TableEditor : Scenario } }; - _tableView.KeyBindings.ReplaceCommands (Key.Space, Command.Accept); + _tableView!.KeyBindings.ReplaceCommands (Key.Space, Command.Accept); // Run - Start the application. Application.Run (appWindow); @@ -726,7 +865,7 @@ public class TableEditor : Scenario { base.Dispose (disposing); - foreach (IDisposable d in _toDispose) + foreach (IDisposable d in _toDispose!) { d.Dispose (); } @@ -740,7 +879,7 @@ public class TableEditor : Scenario for (var i = 0; i < 10; i++) { DataColumn col = dt.Columns.Add (i.ToString (), typeof (uint)); - ColumnStyle style = _tableView.Style.GetOrCreateColumnStyle (col.Ordinal); + ColumnStyle style = _tableView!.Style.GetOrCreateColumnStyle (col.Ordinal); style.RepresentationGetter = o => new Rune ((uint)o).ToString (); } @@ -748,14 +887,14 @@ public class TableEditor : Scenario for (int i = 'a'; i < 'a' + 26; i++) { DataColumn col = dt.Columns.Add (((char)i).ToString (), typeof (uint)); - ColumnStyle style = _tableView.Style.GetOrCreateColumnStyle (col.Ordinal); + ColumnStyle style = _tableView!.Style.GetOrCreateColumnStyle (col.Ordinal); style.RepresentationGetter = o => new Rune ((uint)o).ToString (); } // now add table contents List runes = new (); - foreach (UnicodeRange range in Ranges) + foreach (UnicodeRange range in _ranges!) { for (uint i = range.Start; i <= range.End; i++) { @@ -763,7 +902,7 @@ public class TableEditor : Scenario } } - DataRow dr = null; + DataRow? dr = null; for (var i = 0; i < runes.Count; i++) { @@ -782,23 +921,23 @@ public class TableEditor : Scenario { if (check) { - _checkedFileSystemInfos.Add (info); + _checkedFileSystemInfos!.Add (info); } else { - _checkedFileSystemInfos.Remove (info); + _checkedFileSystemInfos!.Remove (info); } } private void ClearColumnStyles () { - _tableView.Style.ColumnStyles.Clear (); - _tableView.Update (); + _tableView!.Style.ColumnStyles.Clear (); + _tableView!.Update (); } - private void CloseExample () { _tableView.Table = null; } + private void CloseExample () { _tableView!.Table = null; } - private void EditCurrentCell (object sender, CellActivatedEventArgs e) + private void EditCurrentCell (object? sender, CellActivatedEventArgs e) { if (e.Table is not DataTableSource || _currentTable == null) { @@ -830,7 +969,7 @@ public class TableEditor : Scenario cancel.Accepting += (s, e) => { Application.RequestStop (); }; var d = new Dialog { Title = title, Buttons = [ok, cancel] }; - var lbl = new Label { X = 0, Y = 1, Text = _tableView.Table.ColumnNames [e.Col] }; + var lbl = new Label { X = 0, Y = 1, Text = _tableView!.Table.ColumnNames [e.Col] }; var tf = new TextField { Text = oldValue, X = 0, Y = 2, Width = Dim.Fill () }; @@ -852,7 +991,7 @@ public class TableEditor : Scenario MessageBox.ErrorQuery (60, 20, "Failed to set text", ex.Message, "Ok"); } - _tableView.Update (); + _tableView!.Update (); } } @@ -865,30 +1004,30 @@ public class TableEditor : Scenario catch (Exception) { // Permission denied etc - return Enumerable.Empty (); + return []; } } private int? GetColumn () { - if (_tableView.Table == null) + if (_tableView!.Table == null) { return null; } - if (_tableView.SelectedColumn < 0 || _tableView.SelectedColumn > _tableView.Table.Columns) + if (_tableView!.SelectedColumn < 0 || _tableView!.SelectedColumn > _tableView!.Table.Columns) { return null; } - return _tableView.SelectedColumn; + return _tableView!.SelectedColumn; } private string GetHumanReadableFileSize (FileSystemInfo fsi) { if (fsi is not FileInfo fi) { - return null; + return string.Empty; } long value = fi.Length; @@ -921,8 +1060,8 @@ public class TableEditor : Scenario private string GetProposedNewSortOrder (int clickedCol, out bool isAsc) { // work out new sort order - string sort = _currentTable.DefaultView.Sort; - string colName = _tableView.Table.ColumnNames [clickedCol]; + string sort = _currentTable!.DefaultView.Sort; + string colName = _tableView!.Table.ColumnNames [clickedCol]; if (sort?.EndsWith ("ASC") ?? false) { @@ -938,14 +1077,14 @@ public class TableEditor : Scenario return sort; } - private string GetUnicodeCategory (uint u) { return Ranges.FirstOrDefault (r => u >= r.Start && u <= r.End)?.Category ?? "Unknown"; } - private bool HasCheckboxes () { return _tableView.Table is CheckBoxTableSourceWrapperBase; } + private string GetUnicodeCategory (uint u) { return _ranges!.FirstOrDefault (r => u >= r.Start && u <= r.End)?.Category ?? "Unknown"; } + private bool HasCheckboxes () { return _tableView!.Table is CheckBoxTableSourceWrapperBase; } private void HideColumn (int clickedCol) { - ColumnStyle style = _tableView.Style.GetOrCreateColumnStyle (clickedCol); + ColumnStyle style = _tableView!.Style.GetOrCreateColumnStyle (clickedCol); style.Visible = false; - _tableView.Update (); + _tableView!.Update (); } private void OpenExample (bool big) @@ -958,7 +1097,7 @@ public class TableEditor : Scenario private void OpenTreeExample () { - _tableView.Style.ColumnStyles.Clear (); + _tableView!.Style.ColumnStyles.Clear (); TreeView tree = new () { @@ -991,15 +1130,15 @@ public class TableEditor : Scenario MessageBox.ErrorQuery ("Could not find local drives", e.Message, "Ok"); } - _tableView.Table = source; + _tableView!.Table = source; - _toDispose.Add (tree); + _toDispose?.Add (tree); } private void OpenUnicodeMap () { SetTable (BuildUnicodeMap ()); - _tableView.Update (); + _tableView?.Update (); } private void Quit () { Application.RequestStop (); } @@ -1033,9 +1172,9 @@ public class TableEditor : Scenario Buttons = [ok, cancel] }; - ColumnStyle style = _tableView.Style.GetOrCreateColumnStyle (col.Value); + ColumnStyle style = _tableView!.Style.GetOrCreateColumnStyle (col.Value); - var lbl = new Label { X = 0, Y = 0, Text = $"{_tableView.Table.ColumnNames [col.Value]}: " }; + var lbl = new Label { X = 0, Y = 0, Text = $"{_tableView!.Table.ColumnNames [col.Value]}: " }; var tf = new TextField { Text = getter (style).ToString (), X = Pos.Right (lbl), Y = 0, Width = 20 }; d.Add (lbl, tf); @@ -1055,13 +1194,13 @@ public class TableEditor : Scenario MessageBox.ErrorQuery (60, 20, "Failed to set", ex.Message, "Ok"); } - _tableView.Update (); + _tableView!.Update (); } } private void SetDemoTableStyles () { - _tableView.Style.ColumnStyles.Clear (); + _tableView!.Style.ColumnStyles.Clear (); var alignMid = new ColumnStyle { Alignment = Alignment.Center }; var alignRight = new ColumnStyle { Alignment = Alignment.End }; @@ -1097,7 +1236,7 @@ public class TableEditor : Scenario // color 0 and negative values red d <= 0.0000001 ? a.RowIndex % 2 == 0 - && _miAlternatingColors.Checked == true + && _miAlternatingColors!.Checked == true ? _redSchemeAlt : _redScheme : @@ -1110,12 +1249,12 @@ public class TableEditor : Scenario null }; - _tableView.Style.ColumnStyles.Add (_currentTable.Columns ["DateCol"].Ordinal, dateFormatStyle); - _tableView.Style.ColumnStyles.Add (_currentTable.Columns ["DoubleCol"].Ordinal, negativeRight); - _tableView.Style.ColumnStyles.Add (_currentTable.Columns ["NullsCol"].Ordinal, alignMid); - _tableView.Style.ColumnStyles.Add (_currentTable.Columns ["IntCol"].Ordinal, alignRight); + _tableView!.Style.ColumnStyles.Add (_currentTable!.Columns ["DateCol"]!.Ordinal, dateFormatStyle); + _tableView!.Style.ColumnStyles.Add (_currentTable!.Columns ["DoubleCol"]!.Ordinal, negativeRight); + _tableView!.Style.ColumnStyles.Add (_currentTable!.Columns ["NullsCol"]!.Ordinal, alignMid); + _tableView!.Style.ColumnStyles.Add (_currentTable!.Columns ["IntCol"]!.Ordinal, alignRight); - _tableView.Update (); + _tableView!.Update (); } private void SetMaxWidth () @@ -1138,9 +1277,9 @@ public class TableEditor : Scenario private void SetMinAcceptableWidthToOne () { - for (var i = 0; i < _tableView.Table.Columns; i++) + for (var i = 0; i < _tableView!.Table.Columns; i++) { - ColumnStyle style = _tableView.Style.GetOrCreateColumnStyle (i); + ColumnStyle style = _tableView!.Style.GetOrCreateColumnStyle (i); style.MinAcceptableWidth = 1; } } @@ -1151,7 +1290,7 @@ public class TableEditor : Scenario RunColumnWidthDialog (col, "MinWidth", (s, v) => s.MinWidth = v, s => s.MinWidth); } - private void SetTable (DataTable dataTable) { _tableView.Table = new DataTableSource (_currentTable = dataTable); } + private void SetTable (DataTable dataTable) { _tableView!.Table = new DataTableSource (_currentTable = dataTable); } //private void SetupScrollBar () //{ @@ -1159,14 +1298,14 @@ public class TableEditor : Scenario // scrollBar.ChangedPosition += (s, e) => // { - // _tableView.RowOffset = scrollBar.Position; + // _tableView!.RowOffset = scrollBar.Position; - // if (_tableView.RowOffset != scrollBar.Position) + // if (_tableView!.RowOffset != scrollBar.Position) // { - // scrollBar.Position = _tableView.RowOffset; + // scrollBar.Position = _tableView!.RowOffset; // } - // _tableView.SetNeedsDraw (); + // _tableView!.SetNeedsDraw (); // }; // /* // scrollBar.OtherScrollBarView.ChangedPosition += (s,e) => { @@ -1177,10 +1316,10 @@ public class TableEditor : Scenario // tableView.SetNeedsDraw (); // };*/ - // _tableView.DrawingContent += (s, e) => + // _tableView!.DrawingContent += (s, e) => // { - // scrollBar.Size = _tableView.Table?.Rows ?? 0; - // scrollBar.Position = _tableView.RowOffset; + // scrollBar.Size = _tableView!.Table?.Rows ?? 0; + // scrollBar.Position = _tableView!.RowOffset; // //scrollBar.OtherScrollBarView.Size = tableView.Maxlength - 1; // //scrollBar.OtherScrollBarView.Position = tableView.LeftItem; @@ -1190,12 +1329,12 @@ public class TableEditor : Scenario private void ShowAllColumns () { - foreach (KeyValuePair colStyle in _tableView.Style.ColumnStyles) + foreach (KeyValuePair colStyle in _tableView!.Style.ColumnStyles) { colStyle.Value.Visible = true; } - _tableView.Update (); + _tableView!.Update (); } private void ShowHeaderContextMenu (int clickedCol, MouseEventArgs e) @@ -1206,7 +1345,7 @@ public class TableEditor : Scenario } string sort = GetProposedNewSortOrder (clickedCol, out bool isAsc); - string colName = _tableView.Table.ColumnNames [clickedCol]; + string colName = _tableView!.Table.ColumnNames [clickedCol]; PopoverMenu? contextMenu = new ( [ @@ -1272,12 +1411,12 @@ public class TableEditor : Scenario } } - _tableView.Update (); + _tableView!.Update (); } private string StripArrows (string columnName) { return columnName.Replace ($"{Glyphs.DownArrow}", "").Replace ($"{Glyphs.UpArrow}", ""); } - private void TableViewKeyPress (object sender, Key e) + private void TableViewKeyPress (object? sender, Key e) { if (_currentTable == null) { @@ -1286,10 +1425,10 @@ public class TableEditor : Scenario if (e.KeyCode == KeyCode.Delete) { - if (_tableView.FullRowSelect) + if (_tableView!.FullRowSelect) { // Delete button deletes all rows when in full row mode - foreach (int toRemove in _tableView.GetAllSelectedCells () + foreach (int toRemove in _tableView!.GetAllSelectedCells () .Select (p => p.Y) .Distinct () .OrderByDescending (i => i)) @@ -1300,90 +1439,90 @@ public class TableEditor : Scenario else { // otherwise set all selected cells to null - foreach (Point pt in _tableView.GetAllSelectedCells ()) + foreach (Point pt in _tableView!.GetAllSelectedCells ()) { _currentTable.Rows [pt.Y] [pt.X] = DBNull.Value; } } - _tableView.Update (); + _tableView!.Update (); e.Handled = true; } } private void ToggleAllCellLines () { - _tableView.Style.ShowHorizontalHeaderOverline = true; - _tableView.Style.ShowVerticalHeaderLines = true; - _tableView.Style.ShowHorizontalHeaderUnderline = true; - _tableView.Style.ShowVerticalCellLines = true; + _tableView!.Style.ShowHorizontalHeaderOverline = true; + _tableView!.Style.ShowVerticalHeaderLines = true; + _tableView!.Style.ShowHorizontalHeaderUnderline = true; + _tableView!.Style.ShowVerticalCellLines = true; - _miHeaderOverline.Checked = true; - _miHeaderMidline.Checked = true; - _miHeaderUnderline.Checked = true; - _miCellLines.Checked = true; + _miHeaderOverline!.Checked = true; + _miHeaderMidline!.Checked = true; + _miHeaderUnderline!.Checked = true; + _miCellLines!.Checked = true; - _tableView.Update (); + _tableView!.Update (); } private void ToggleAlternatingColors () { //toggle menu item - _miAlternatingColors.Checked = !_miAlternatingColors.Checked; + _miAlternatingColors!.Checked = !_miAlternatingColors.Checked; if (_miAlternatingColors.Checked == true) { - _tableView.Style.RowColorGetter = a => { return a.RowIndex % 2 == 0 ? _alternatingScheme : null; }; + _tableView!.Style.RowColorGetter = a => { return a.RowIndex % 2 == 0 ? _alternatingScheme : null; }; } else { - _tableView.Style.RowColorGetter = null; + _tableView!.Style.RowColorGetter = null; } - _tableView.SetNeedsDraw (); + _tableView!.SetNeedsDraw (); } private void ToggleAlwaysShowHeaders () { - _miAlwaysShowHeaders.Checked = !_miAlwaysShowHeaders.Checked; - _tableView.Style.AlwaysShowHeaders = (bool)_miAlwaysShowHeaders.Checked; - _tableView.Update (); + _miAlwaysShowHeaders!.Checked = !_miAlwaysShowHeaders.Checked; + _tableView!.Style.AlwaysShowHeaders = (bool)_miAlwaysShowHeaders.Checked!; + _tableView!.Update (); } private void ToggleAlwaysUseNormalColorForVerticalCellLines () { - _miAlwaysUseNormalColorForVerticalCellLines.Checked = + _miAlwaysUseNormalColorForVerticalCellLines!.Checked = !_miAlwaysUseNormalColorForVerticalCellLines.Checked; - _tableView.Style.AlwaysUseNormalColorForVerticalCellLines = - (bool)_miAlwaysUseNormalColorForVerticalCellLines.Checked; + _tableView!.Style.AlwaysUseNormalColorForVerticalCellLines = + (bool)_miAlwaysUseNormalColorForVerticalCellLines.Checked!; - _tableView.Update (); + _tableView!.Update (); } private void ToggleBottomline () { - _miBottomline.Checked = !_miBottomline.Checked; - _tableView.Style.ShowHorizontalBottomline = (bool)_miBottomline.Checked; - _tableView.Update (); + _miBottomline!.Checked = !_miBottomline.Checked; + _tableView!.Style.ShowHorizontalBottomline = (bool)_miBottomline.Checked!; + _tableView!.Update (); } private void ToggleCellLines () { - _miCellLines.Checked = !_miCellLines.Checked; - _tableView.Style.ShowVerticalCellLines = (bool)_miCellLines.Checked; - _tableView.Update (); + _miCellLines!.Checked = !_miCellLines.Checked; + _tableView!.Style.ShowVerticalCellLines = (bool)_miCellLines.Checked!; + _tableView!.Update (); } private void ToggleCheckboxes (bool radio) { - if (_tableView.Table is CheckBoxTableSourceWrapperBase wrapper) + if (_tableView!.Table is CheckBoxTableSourceWrapperBase wrapper) { // unwrap it to remove check boxes - _tableView.Table = wrapper.Wrapping; + _tableView!.Table = wrapper.Wrapping; - _miCheckboxes.Checked = false; - _miRadioboxes.Checked = false; + _miCheckboxes!.Checked = false; + _miRadioboxes!.Checked = false; // if toggling off checkboxes/radio if (wrapper.UseRadioButtons == radio) @@ -1395,114 +1534,114 @@ public class TableEditor : Scenario ITableSource source; // Either toggling on checkboxes/radio or switching from radio to checkboxes (or vice versa) - if (_tableView.Table is TreeTableSource treeSource) + if (_tableView!.Table is TreeTableSource treeSource) { source = new CheckBoxTableSourceWrapperByObject ( _tableView, treeSource, - _checkedFileSystemInfos.Contains, + _checkedFileSystemInfos!.Contains, CheckOrUncheckFile ) - { UseRadioButtons = radio }; + { UseRadioButtons = radio }; } else { - source = new CheckBoxTableSourceWrapperByIndex (_tableView, _tableView.Table) { UseRadioButtons = radio }; + source = new CheckBoxTableSourceWrapperByIndex (_tableView, _tableView!.Table) { UseRadioButtons = radio }; } - _tableView.Table = source; + _tableView!.Table = source; if (radio) { - _miRadioboxes.Checked = true; - _miCheckboxes.Checked = false; + _miRadioboxes!.Checked = true; + _miCheckboxes!.Checked = false; } else { - _miRadioboxes.Checked = false; - _miCheckboxes.Checked = true; + _miRadioboxes!.Checked = false; + _miCheckboxes!.Checked = true; } } private void ToggleExpandLastColumn () { - _miExpandLastColumn.Checked = !_miExpandLastColumn.Checked; - _tableView.Style.ExpandLastColumn = (bool)_miExpandLastColumn.Checked; + _miExpandLastColumn!.Checked = !_miExpandLastColumn.Checked; + _tableView!.Style.ExpandLastColumn = (bool)_miExpandLastColumn.Checked!; - _tableView.Update (); + _tableView!.Update (); } private void ToggleFullRowSelect () { - _miFullRowSelect.Checked = !_miFullRowSelect.Checked; - _tableView.FullRowSelect = (bool)_miFullRowSelect.Checked; - _tableView.Update (); + _miFullRowSelect!.Checked = !_miFullRowSelect.Checked; + _tableView!.FullRowSelect = (bool)_miFullRowSelect.Checked!; + _tableView!.Update (); } private void ToggleHeaderMidline () { - _miHeaderMidline.Checked = !_miHeaderMidline.Checked; - _tableView.Style.ShowVerticalHeaderLines = (bool)_miHeaderMidline.Checked; - _tableView.Update (); + _miHeaderMidline!.Checked = !_miHeaderMidline.Checked; + _tableView!.Style.ShowVerticalHeaderLines = (bool)_miHeaderMidline.Checked!; + _tableView!.Update (); } private void ToggleHorizontalScrollIndicators () { - _miShowHorizontalScrollIndicators.Checked = !_miShowHorizontalScrollIndicators.Checked; - _tableView.Style.ShowHorizontalScrollIndicators = (bool)_miShowHorizontalScrollIndicators.Checked; - _tableView.Update (); + _miShowHorizontalScrollIndicators!.Checked = !_miShowHorizontalScrollIndicators.Checked; + _tableView!.Style.ShowHorizontalScrollIndicators = (bool)_miShowHorizontalScrollIndicators.Checked!; + _tableView!.Update (); } private void ToggleInvertSelectedCellFirstCharacter () { //toggle menu item - _miCursor.Checked = !_miCursor.Checked; - _tableView.Style.InvertSelectedCellFirstCharacter = (bool)_miCursor.Checked; - _tableView.SetNeedsDraw (); + _miCursor!.Checked = !_miCursor.Checked; + _tableView!.Style.InvertSelectedCellFirstCharacter = (bool)_miCursor.Checked!; + _tableView!.SetNeedsDraw (); } private void ToggleNoCellLines () { - _tableView.Style.ShowHorizontalHeaderOverline = false; - _tableView.Style.ShowVerticalHeaderLines = false; - _tableView.Style.ShowHorizontalHeaderUnderline = false; - _tableView.Style.ShowVerticalCellLines = false; + _tableView!.Style.ShowHorizontalHeaderOverline = false; + _tableView!.Style.ShowVerticalHeaderLines = false; + _tableView!.Style.ShowHorizontalHeaderUnderline = false; + _tableView!.Style.ShowVerticalCellLines = false; - _miHeaderOverline.Checked = false; - _miHeaderMidline.Checked = false; - _miHeaderUnderline.Checked = false; - _miCellLines.Checked = false; + _miHeaderOverline!.Checked = false; + _miHeaderMidline!.Checked = false; + _miHeaderUnderline!.Checked = false; + _miCellLines!.Checked = false; - _tableView.Update (); + _tableView!.Update (); } private void ToggleOverline () { - _miHeaderOverline.Checked = !_miHeaderOverline.Checked; - _tableView.Style.ShowHorizontalHeaderOverline = (bool)_miHeaderOverline.Checked; - _tableView.Update (); + _miHeaderOverline!.Checked = !_miHeaderOverline.Checked; + _tableView!.Style.ShowHorizontalHeaderOverline = (bool)_miHeaderOverline.Checked!; + _tableView!.Update (); } private void ToggleShowHeaders () { - _miShowHeaders.Checked = !_miShowHeaders.Checked; - _tableView.Style.ShowHeaders = (bool)_miShowHeaders.Checked; - _tableView.Update (); + _miShowHeaders!.Checked = !_miShowHeaders.Checked; + _tableView!.Style.ShowHeaders = (bool)_miShowHeaders.Checked!; + _tableView!.Update (); } private void ToggleSmoothScrolling () { - _miSmoothScrolling.Checked = !_miSmoothScrolling.Checked; - _tableView.Style.SmoothHorizontalScrolling = (bool)_miSmoothScrolling.Checked; + _miSmoothScrolling!.Checked = !_miSmoothScrolling.Checked; + _tableView!.Style.SmoothHorizontalScrolling = (bool)_miSmoothScrolling.Checked!; - _tableView.Update (); + _tableView!.Update (); } private void ToggleUnderline () { - _miHeaderUnderline.Checked = !_miHeaderUnderline.Checked; - _tableView.Style.ShowHorizontalHeaderUnderline = (bool)_miHeaderUnderline.Checked; - _tableView.Update (); + _miHeaderUnderline!.Checked = !_miHeaderUnderline.Checked; + _tableView!.Style.ShowHorizontalHeaderUnderline = (bool)_miHeaderUnderline.Checked!; + _tableView!.Update (); } private int ToTableCol (int col) diff --git a/Examples/UICatalog/Scenarios/TextInputControls.cs b/Examples/UICatalog/Scenarios/TextInputControls.cs index 19e71a427..182e47d49 100644 --- a/Examples/UICatalog/Scenarios/TextInputControls.cs +++ b/Examples/UICatalog/Scenarios/TextInputControls.cs @@ -498,5 +498,5 @@ public class TextInputControls : Scenario - private void TimeChanged (object sender, DateTimeEventArgs e) { _labelMirroringTimeField.Text = _timeField.Text; } + private void TimeChanged (object sender, EventArgs e) { _labelMirroringTimeField.Text = _timeField.Text; } } diff --git a/Examples/UICatalog/Scenarios/TimeAndDate.cs b/Examples/UICatalog/Scenarios/TimeAndDate.cs index b29be8896..11be627bd 100644 --- a/Examples/UICatalog/Scenarios/TimeAndDate.cs +++ b/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 e) + private void DateChanged (object? sender, EventArgs 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 e) + private void TimeChanged (object? sender, EventArgs 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}"; } } diff --git a/Examples/UICatalog/Scenarios/TreeViewFileSystem.cs b/Examples/UICatalog/Scenarios/TreeViewFileSystem.cs index cdd013fbc..34540d5cc 100644 --- a/Examples/UICatalog/Scenarios/TreeViewFileSystem.cs +++ b/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. diff --git a/Examples/UICatalog/Scenarios/Unicode.cs b/Examples/UICatalog/Scenarios/Unicode.cs index 813e1cb49..0f58b1e6d 100644 --- a/Examples/UICatalog/Scenarios/Unicode.cs +++ b/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 { 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 { 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); diff --git a/Examples/UICatalog/UICatalog.cs b/Examples/UICatalog/UICatalog.cs index e7b6d2340..6f7a37139 100644 --- a/Examples/UICatalog/UICatalog.cs +++ b/Examples/UICatalog/UICatalog.cs @@ -80,7 +80,7 @@ public class UICatalog // Get allowed driver names string? [] allowedDrivers = Application.GetDriverTypes ().Item2.ToArray (); - Option driverOption = new Option ("--driver", "The IConsoleDriver to use.") + Option driverOption = new Option ("--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 } } diff --git a/README.md b/README.md index fd5baafb4..187aa1634 100644 --- a/README.md +++ b/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) diff --git a/Scripts/Run-LocalCoverage.ps1 b/Scripts/Run-LocalCoverage.ps1 new file mode 100644 index 000000000..312b229a9 --- /dev/null +++ b/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" \ No newline at end of file diff --git a/Terminal.Gui.Analyzers.Tests/ProjectBuilder.cs b/Terminal.Gui.Analyzers.Tests/ProjectBuilder.cs index e5fec924b..2611fb532 100644 --- a/Terminal.Gui.Analyzers.Tests/ProjectBuilder.cs +++ b/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 diagnostics = compilation!.GetDiagnostics (); + IEnumerable errors = diagnostics.Where (d => d.Severity == DiagnosticSeverity.Error); - if (errors.Any ()) + IEnumerable 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 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}"); + 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}"); - var workspace = new AdhocWorkspace (); - var projectId = ProjectId.CreateNewId (); - var documentId = DocumentId.CreateNewId (projectId); + AdhocWorkspace workspace = new AdhocWorkspace (); + ProjectId projectId = ProjectId.CreateNewId (); + DocumentId documentId = DocumentId.CreateNewId (projectId); - var references = new List () - { - 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).Assembly.Location), + List 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).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> 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 ApplyCodeFixAsync (Document document, Diagnostic diagnostic, CodeFixProvider codeFix) + private static async Task 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 ().First ().ChangedSolution; + ImmutableArray operations = await codeAction.GetOperationsAsync (CancellationToken.None); + Solution solution = operations.OfType ().First ().ChangedSolution; return solution.GetDocument (document.Id); } } diff --git a/Terminal.Gui/App/Application.Driver.cs b/Terminal.Gui/App/Application.Driver.cs index aa2ee80fe..c08fb879f 100644 --- a/Terminal.Gui/App/Application.Driver.cs +++ b/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; - - /// Gets the that has been selected. See also . - public static IConsoleDriver? Driver + /// + public static IDriver? Driver { get => ApplicationImpl.Instance.Driver; internal set => ApplicationImpl.Instance.Driver = value; } - /// - /// Gets or sets whether will be forced to output only the 16 colors defined in - /// . The default is , meaning 24-bit (TrueColor) colors will be output - /// as long as the selected supports TrueColor. - /// + /// [ConfigurationProperty (Scope = typeof (SettingsScope))] public static bool Force16Colors { @@ -25,14 +21,7 @@ public static partial class Application // Driver abstractions set => ApplicationImpl.Instance.Force16Colors = value; } - /// - /// 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. - /// - /// - /// Note, will override this configuration setting if called - /// with either `driver` or `driverName` specified. - /// + /// [ConfigurationProperty (Scope = typeof (SettingsScope))] public static string ForceDriver { @@ -40,9 +29,34 @@ public static partial class Application // Driver abstractions set => ApplicationImpl.Instance.ForceDriver = value; } - /// - /// Collection of sixel images to write out to screen when updating. - /// Only add to this collection if you are sure terminal supports sixel format. - /// + /// public static List Sixel => ApplicationImpl.Instance.Sixel; -} + + /// Gets a list of types and type names that are available. + /// + [RequiresUnreferencedCode ("AOT")] + public static (List, List) GetDriverTypes () + { + // use reflection to get the list of drivers + List 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 driverTypeNames = driverTypes + .Where (d => !typeof (IDriver).IsAssignableFrom (d)) + .Select (d => d!.Name) + .Union (["dotnet", "windows", "unix", "fake"]) + .ToList ()!; + + return (driverTypes, driverTypeNames); + } +} \ No newline at end of file diff --git a/Terminal.Gui/App/Application.Keyboard.cs b/Terminal.Gui/App/Application.Keyboard.cs index 4ec75af85..1fa176a7d 100644 --- a/Terminal.Gui/App/Application.Keyboard.cs +++ b/Terminal.Gui/App/Application.Keyboard.cs @@ -4,9 +4,7 @@ namespace Terminal.Gui.App; public static partial class Application // Keyboard handling { - /// - /// Static reference to the current . - /// + /// public static IKeyboard Keyboard { get => ApplicationImpl.Instance.Keyboard; @@ -14,40 +12,14 @@ public static partial class Application // Keyboard handling throw new ArgumentNullException(nameof(value)); } - /// - /// Called when the user presses a key (by the ). Raises the cancelable - /// event, then calls on all top level views, and finally - /// if the key was not handled, invokes any Application-scoped . - /// - /// Can be used to simulate key press events. - /// - /// if the key was handled. - public static bool RaiseKeyDownEvent (Key key) => Keyboard.RaiseKeyDownEvent (key); + /// + public static bool RaiseKeyDownEvent (Key key) => ApplicationImpl.Instance.Keyboard.RaiseKeyDownEvent (key); - /// - /// Invokes any commands bound at the Application-level to . - /// - /// - /// - /// if no command was found; input processing should continue. - /// if the command was invoked and was not handled (or cancelled); input processing should continue. - /// if the command was invoked the command was handled (or cancelled); input processing should stop. - /// - public static bool? InvokeCommandsBoundToKey (Key key) => Keyboard.InvokeCommandsBoundToKey (key); + /// + public static bool? InvokeCommandsBoundToKey (Key key) => ApplicationImpl.Instance.Keyboard.InvokeCommandsBoundToKey (key); - /// - /// Invokes an Application-bound command. - /// - /// The Command to invoke - /// The Application-bound Key that was pressed. - /// Describes the binding. - /// - /// if no command was found; input processing should continue. - /// if the command was invoked and was not handled (or cancelled); input processing should continue. - /// if the command was invoked the command was handled (or cancelled); input processing should stop. - /// - /// - public static bool? InvokeCommand (Command command, Key key, KeyBinding binding) => Keyboard.InvokeCommand (command, key, binding); + /// + public static bool? InvokeCommand (Command command, Key key, KeyBinding binding) => ApplicationImpl.Instance.Keyboard.InvokeCommand (command, key, binding); /// /// Raised when the user presses a key. @@ -63,29 +35,13 @@ public static partial class Application // Keyboard handling /// public static event EventHandler? KeyDown { - add => Keyboard.KeyDown += value; - remove => Keyboard.KeyDown -= value; + add => ApplicationImpl.Instance.Keyboard.KeyDown += value; + remove => ApplicationImpl.Instance.Keyboard.KeyDown -= value; } - /// - /// Called when the user releases a key (by the ). Raises the cancelable - /// - /// event - /// then calls on all top level views. Called after . - /// - /// Can be used to simulate key release events. - /// - /// if the key was handled. - public static bool RaiseKeyUpEvent (Key key) => Keyboard.RaiseKeyUpEvent (key); + /// + public static bool RaiseKeyUpEvent (Key key) => ApplicationImpl.Instance.Keyboard.RaiseKeyUpEvent (key); - /// Gets the Application-scoped key bindings. - public static KeyBindings KeyBindings => Keyboard.KeyBindings; - - internal static void AddKeyBindings () - { - if (Keyboard is KeyboardImpl keyboard) - { - keyboard.AddKeyBindings (); - } - } + /// + public static KeyBindings KeyBindings => ApplicationImpl.Instance.Keyboard.KeyBindings; } diff --git a/Terminal.Gui/App/Application.Lifecycle.cs b/Terminal.Gui/App/Application.Lifecycle.cs index 9cd2b7996..051d6b5c8 100644 --- a/Terminal.Gui/App/Application.Lifecycle.cs +++ b/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) /// Initializes a new instance of a Terminal.Gui Application. must be called when the application is closing. /// Call this method once per instance (or after has been called). /// - /// This function loads the right for the platform, Creates a . and + /// This function loads the right for the platform, Creates a . and /// assigns it to /// /// @@ -22,90 +26,36 @@ public static partial class Application // Lifecycle (Init/Shutdown) /// /// /// The function combines - /// and + /// and /// into a single /// call. An application can use without explicitly calling - /// . + /// . /// /// - /// The to use. If neither or + /// The to use. If neither or /// are specified the default driver for the platform will be used. /// /// /// The short name (e.g. "dotnet", "windows", "unix", or "fake") of the - /// to use. If neither or are + /// to use. If neither or are /// specified the default driver for the platform will be used. /// [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 + /// + /// Gets or sets the main thread ID for the application. + /// + 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); } - - /// Gets a list of types and type names that are available. - /// - [RequiresUnreferencedCode ("AOT")] - public static (List, List) GetDriverTypes () - { - // use reflection to get the list of drivers - List 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 driverTypeNames = driverTypes - .Where (d => !typeof (IConsoleDriverFacade).IsAssignableFrom (d)) - .Select (d => d!.Name) - .Union (["dotnet", "windows", "unix", "fake"]) - .ToList ()!; - - return (driverTypes, driverTypeNames); - } - /// Shutdown an application initialized with . /// /// Shutdown must be called for every call to or @@ -129,19 +79,17 @@ public static partial class Application // Lifecycle (Init/Shutdown) internal set => ApplicationImpl.Instance.Initialized = value; } - /// - /// This event is raised after the and methods have been called. - /// - /// - /// Intended to support unit tests that need to know when the application has been initialized. - /// - public static event EventHandler>? InitializedChanged; - - /// - /// Raises the event. - /// - internal static void OnInitializedChanged (object sender, EventArgs e) + /// + public static event EventHandler>? 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); } diff --git a/Terminal.Gui/App/Application.Mouse.cs b/Terminal.Gui/App/Application.Mouse.cs index 287cb4e78..659e07b1c 100644 --- a/Terminal.Gui/App/Application.Mouse.cs +++ b/Terminal.Gui/App/Application.Mouse.cs @@ -92,10 +92,8 @@ public static partial class Application // Mouse handling /// /// This method can be used to simulate a mouse event, e.g. in unit tests. /// The mouse event with coordinates relative to the screen. - internal static void RaiseMouseEvent (MouseEventArgs mouseEvent) { Mouse.RaiseMouseEvent (mouseEvent); } - - /// - /// INTERNAL: Clears mouse state during application reset. - /// - internal static void ResetMouseState () { Mouse.ResetState (); } + internal static void RaiseMouseEvent (MouseEventArgs mouseEvent) + { + Mouse.RaiseMouseEvent (mouseEvent); + } } diff --git a/Terminal.Gui/App/Application.Navigation.cs b/Terminal.Gui/App/Application.Navigation.cs index 28e86d2eb..b822a6027 100644 --- a/Terminal.Gui/App/Application.Navigation.cs +++ b/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; } /// Alternative key to navigate forwards through views. Tab is the primary key. [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; } /// @@ -43,23 +43,23 @@ public static partial class Application // Navigation stuff /// public static event EventHandler? KeyUp { - add => Keyboard.KeyUp += value; - remove => Keyboard.KeyUp -= value; + add => ApplicationImpl.Instance.Keyboard.KeyUp += value; + remove => ApplicationImpl.Instance.Keyboard.KeyUp -= value; } /// Alternative key to navigate backwards through views. Shift+Ctrl+Tab is the primary key. [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; } /// Alternative key to navigate backwards through views. Shift+Tab is the primary key. [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; } } diff --git a/Terminal.Gui/App/Application.Run.cs b/Terminal.Gui/App/Application.Run.cs index 7477325aa..56206a997 100644 --- a/Terminal.Gui/App/Application.Run.cs +++ b/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; } /// Gets or sets the key to activate arranging views using the keyboard. [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; } - /// - /// Notify that a new was created ( was called). The token is - /// created in and this event will be fired before that function exits. - /// - /// - /// If is callers to - /// must also subscribe to and manually dispose of the token - /// when the application is done. - /// - public static event EventHandler? NotifyNewRunState; + /// + public static SessionToken Begin (Toplevel toplevel) => ApplicationImpl.Instance.Begin (toplevel); - /// Notify that an existent is stopping ( was called). - /// - /// If is callers to - /// must also subscribe to and manually dispose of the token - /// when the application is done. - /// -#pragma warning disable CS0067 // Event is never used -#pragma warning disable CS0414 // Event is never used - public static event EventHandler? NotifyStopRunState; -#pragma warning restore CS0414 // Event is never used -#pragma warning restore CS0067 // Event is never used + /// + public static bool PositionCursor () => ApplicationImpl.Instance.PositionCursor (); - /// Building block API: Prepares the provided for execution. - /// - /// The handle that needs to be passed to the method upon - /// completion. - /// - /// The to prepare execution for. - /// - /// This method prepares the provided for running with the focus, it adds this to the list - /// of s, lays out the SubViews, focuses the first element, and draws the - /// in the screen. This is usually followed by executing the method, and then the - /// method upon termination which will undo these changes. - /// - 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; - } - - /// - /// Calls on the most focused view. - /// - /// - /// Does nothing if there is no most focused view. - /// - /// If the most focused view is not visible within it's superview, the cursor will be hidden. - /// - /// - /// if a view positioned the cursor and the position is visible. - 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; - } - - /// - /// Runs the application by creating a object and calling - /// . - /// - /// - /// Calling first is not needed as this function will initialize the application. - /// - /// must be called when the application is closing (typically after Run> has returned) to - /// ensure resources are cleaned up and terminal settings restored. - /// - /// - /// The caller is responsible for disposing the object returned by this method. - /// - /// - /// The created object. The caller is responsible for disposing this object. + /// [RequiresUnreferencedCode ("AOT")] [RequiresDynamicCode ("AOT")] - public static Toplevel Run (Func? errorHandler = null, IConsoleDriver? driver = null) - { - return ApplicationImpl.Instance.Run (errorHandler, driver); - } + public static Toplevel Run (Func? errorHandler = null, string? driver = null) => ApplicationImpl.Instance.Run (errorHandler, driver); - /// - /// Runs the application by creating a -derived object of type T and calling - /// . - /// - /// - /// Calling first is not needed as this function will initialize the application. - /// - /// must be called when the application is closing (typically after Run> has returned) to - /// ensure resources are cleaned up and terminal settings restored. - /// - /// - /// The caller is responsible for disposing the object returned by this method. - /// - /// - /// - /// - /// The to use. If not specified the default driver for the platform will - /// be used. Must be if has already been called. - /// - /// The created T object. The caller is responsible for disposing this object. + /// [RequiresUnreferencedCode ("AOT")] [RequiresDynamicCode ("AOT")] - public static T Run (Func? errorHandler = null, IConsoleDriver? driver = null) - where T : Toplevel, new() + public static TView Run (Func? errorHandler = null, string? driver = null) + where TView : Toplevel, new() => ApplicationImpl.Instance.Run (errorHandler, driver); + + /// + public static void Run (Toplevel view, Func? errorHandler = null) => ApplicationImpl.Instance.Run (view, errorHandler); + + /// + public static object? AddTimeout (TimeSpan time, Func callback) => ApplicationImpl.Instance.AddTimeout (time, callback); + + /// + public static bool RemoveTimeout (object token) => ApplicationImpl.Instance.RemoveTimeout (token); + + /// + /// + public static ITimedEvents? TimedEvents => ApplicationImpl.Instance?.TimedEvents; + /// + public static void Invoke (Action action) => ApplicationImpl.Instance.Invoke (action); + + /// + public static void LayoutAndDraw (bool forceRedraw = false) => ApplicationImpl.Instance.LayoutAndDraw (forceRedraw); + + /// + public static bool StopAfterFirstIteration { - return ApplicationImpl.Instance.Run (errorHandler, driver); + get => ApplicationImpl.Instance.StopAfterFirstIteration; + set => ApplicationImpl.Instance.StopAfterFirstIteration = value; } - /// Runs the Application using the provided view. - /// - /// - /// This method is used to start processing events for the main application, but it is also used to run other - /// modal s such as boxes. - /// - /// - /// To make a stop execution, call - /// . - /// - /// - /// Calling is equivalent to calling - /// , followed by , and then calling - /// . - /// - /// - /// Alternatively, to have a program control the main loop and process events manually, call - /// to set things up manually and then repeatedly call - /// with the wait parameter set to false. By doing this the - /// method will only process any pending events, timers handlers and then - /// return control immediately. - /// - /// - /// When using or - /// - /// will be called automatically. - /// - /// - /// RELEASE builds only: When is any exceptions will be - /// rethrown. Otherwise, if will be called. If - /// returns the will resume; otherwise this method will - /// exit. - /// - /// - /// The to run as a modal. - /// - /// RELEASE builds only: Handler for any unhandled exceptions (resumes when returns true, - /// rethrows when null). - /// - public static void Run (Toplevel view, Func? errorHandler = null) { ApplicationImpl.Instance.Run (view, errorHandler); } + /// + public static void RequestStop (Toplevel? top = null) => ApplicationImpl.Instance.RequestStop (top); - /// Adds a timeout to the application. - /// - /// 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 . - /// - public static object? AddTimeout (TimeSpan time, Func callback) { return ApplicationImpl.Instance.AddTimeout (time, callback); } + /// + public static void End (SessionToken sessionToken) => ApplicationImpl.Instance.End (sessionToken); - /// Removes a previously scheduled timeout - /// The token parameter is the value returned by . - /// Returns - /// - /// if the timeout is successfully removed; otherwise, - /// - /// . - /// This method also returns - /// - /// if the timeout is not found. - public static bool RemoveTimeout (object token) { return ApplicationImpl.Instance.RemoveTimeout (token); } + /// + internal static void RaiseIteration () => ApplicationImpl.Instance.RaiseIteration (); - /// Runs on the thread that is processing events - /// the action to be invoked on the main processing thread. - public static void Invoke (Action action) { ApplicationImpl.Instance.Invoke (action); } - - /// - /// 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 ) will be laid out. - /// Only Views that need to be drawn (see ) will be drawn. - /// - /// - /// If the entire View hierarchy will be redrawn. The default is and - /// should only be overriden for testing. - /// - public static void LayoutAndDraw (bool forceRedraw = false) + /// + public static event EventHandler? Iteration { - ApplicationImpl.Instance.LayoutAndDraw (forceRedraw); + add => ApplicationImpl.Instance.Iteration += value; + remove => ApplicationImpl.Instance.Iteration -= value; } - /// This event is raised on each iteration of the main loop. - /// See also - public static event EventHandler? Iteration; - - /// - /// Set to true to cause to be called after the first iteration. Set to false (the default) to - /// cause the application to continue running until Application.RequestStop () is called. - /// - public static bool EndAfterFirstIteration { get; set; } - - /// Building block API: Runs the main loop for the created . - /// The state returned by the method. - public static void RunLoop (RunState state) + /// + public static event EventHandler? SessionBegun { - ArgumentNullException.ThrowIfNull (state); - ObjectDisposedException.ThrowIf (state.Toplevel is null, "state"); - - var firstIteration = true; - - for (state.Toplevel.Running = true; state.Toplevel?.Running == true;) - { - if (EndAfterFirstIteration && !firstIteration) - { - return; - } - - firstIteration = RunIteration (ref state, firstIteration); - } - - // Run one last iteration to consume any outstanding input events from Driver - // This is important for remaining OnKeyUp events. - RunIteration (ref state, firstIteration); + add => ApplicationImpl.Instance.SessionBegun += value; + remove => ApplicationImpl.Instance.SessionBegun -= value; } - /// Run one application iteration. - /// The state returned by . - /// - /// Set to if this is the first run loop iteration. - /// - /// if at least one iteration happened. - public static bool RunIteration (ref RunState state, bool firstIteration = false) + /// + public static event EventHandler? SessionEnded { - ApplicationImpl appImpl = (ApplicationImpl)ApplicationImpl.Instance; - appImpl.Coordinator?.RunIteration (); - - return false; - } - - /// Stops the provided , causing or the if provided. - /// The to stop. - /// - /// This will cause to return. - /// - /// Calling is equivalent to setting the - /// - /// property on the currently running to false. - /// - /// - public static void RequestStop (Toplevel? top = null) { ApplicationImpl.Instance.RequestStop (top); } - - /// - /// Building block API: completes the execution of a that was started with - /// . - /// - /// The returned by the method. - public static void End (RunState runState) - { - 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); - } - internal static void RaiseIteration () - { - Iteration?.Invoke (null, new ()); + add => ApplicationImpl.Instance.SessionEnded += value; + remove => ApplicationImpl.Instance.SessionEnded -= value; } } diff --git a/Terminal.Gui/App/Application.Screen.cs b/Terminal.Gui/App/Application.Screen.cs index 92522c235..2cc223cdb 100644 --- a/Terminal.Gui/App/Application.Screen.cs +++ b/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 { - /// - /// Gets or sets the size of the screen. By default, this is the size of the screen as reported by the . - /// - /// - /// - /// If the has not been initialized, this will return a default size of 2048x2048; useful for unit tests. - /// - /// + /// + public static Rectangle Screen { get => ApplicationImpl.Instance.Screen; set => ApplicationImpl.Instance.Screen = value; } - /// Invoked when the terminal's size changed. The new size of the terminal is provided. - public static event EventHandler>? ScreenChanged; - - /// - /// Called when the application's size has changed. Sets the size of all s and fires the - /// event. - /// - /// The new screen size and position. - public static void RaiseScreenChangedEvent (Rectangle screen) + /// + public static event EventHandler>? 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; } - /// - /// Gets or sets whether the screen will be cleared, and all Views redrawn, during the next Application iteration. - /// - /// - /// This is typical set to true when a View's changes and that view has no - /// SuperView (e.g. when is moved or resized. - /// + /// + internal static bool ClearScreenNextIteration { get => ApplicationImpl.Instance.ClearScreenNextIteration; diff --git a/Terminal.Gui/App/Application.Toplevel.cs b/Terminal.Gui/App/Application.Toplevel.cs index eeb70522a..e7c05e437 100644 --- a/Terminal.Gui/App/Application.Toplevel.cs +++ b/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 - - /// Holds the stack of TopLevel views. - internal static ConcurrentStack TopLevels => ApplicationImpl.Instance.TopLevels; + /// + public static ConcurrentStack TopLevels => ApplicationImpl.Instance.TopLevels; /// The that is currently active. /// The top. @@ -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; - } - - } diff --git a/Terminal.Gui/App/Application.cd b/Terminal.Gui/App/Application.cd index 294a90411..49b0c85cb 100644 --- a/Terminal.Gui/App/Application.cd +++ b/Terminal.Gui/App/Application.cd @@ -36,19 +36,19 @@ App\MainLoopSyncContext.cs - + AAAAAAAAACACAgAAAAAAAAAAAAAAAAACQAAAAAAAAAA= - App\RunState.cs + App\SessionToken.cs - + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAA= - App\RunStateEventArgs.cs + App\SessionTokenEventArgs.cs diff --git a/Terminal.Gui/App/Application.cs b/Terminal.Gui/App/Application.cs index 99991bded..bd3ccf7a3 100644 --- a/Terminal.Gui/App/Application.cs +++ b/Terminal.Gui/App/Application.cs @@ -39,18 +39,6 @@ namespace Terminal.Gui.App; /// public static partial class Application { - /// Gets all cultures supported by the application without the invariant language. - public static List? SupportedCultures { get; private set; } = GetSupportedCultures (); - - - /// - /// - /// Handles recurring events. These are invoked on the main UI thread - allowing for - /// safe updates to instances. - /// - /// - public static ITimedEvents? TimedEvents => ApplicationImpl.Instance?.TimedEvents; - /// /// Maximum number of iterations of the main loop (and hence draws) /// to allow to occur per second. Defaults to > which is a 40ms sleep @@ -71,7 +59,7 @@ public static partial class Application /// A string representation of the Application public new static string ToString () { - IConsoleDriver? driver = Driver; + IDriver? driver = Driver; if (driver is null) { @@ -82,11 +70,11 @@ public static partial class Application } /// - /// Gets a string representation of the Application rendered by the provided . + /// Gets a string representation of the Application rendered by the provided . /// /// The driver to use to render the contents. /// A string representation of the Application - 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 (); } + /// Gets all cultures supported by the application without the invariant language. + public static List? SupportedCultures { get; private set; } = GetSupportedCultures (); + + internal static List 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); - } } diff --git a/Terminal.Gui/App/ApplicationImpl.Driver.cs b/Terminal.Gui/App/ApplicationImpl.Driver.cs new file mode 100644 index 000000000..36679b2b0 --- /dev/null +++ b/Terminal.Gui/App/ApplicationImpl.Driver.cs @@ -0,0 +1,176 @@ +#nullable enable +using System.Collections.Concurrent; + +namespace Terminal.Gui.App; + +public partial class ApplicationImpl +{ + /// + public IDriver? Driver { get; set; } + + /// + public bool Force16Colors { get; set; } + + /// + public string ForceDriver { get; set; } = string.Empty; + + /// + public List Sixel { get; } = new (); + + /// + /// Creates the appropriate based on platform and driverName. + /// + /// + /// + /// + /// + 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; + bool factoryIsDotNet = _componentFactory is IComponentFactory; + bool factoryIsUnix = _componentFactory is IComponentFactory; + bool factoryIsFake = _componentFactory is IComponentFactory; + + // 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; + + /// + /// INTERNAL: Gets or sets the main loop coordinator that orchestrates the application's event processing, + /// input handling, and rendering pipeline. + /// + /// + /// + /// The is the central component responsible for: + /// + /// Managing the platform-specific input thread that reads from the console + /// Coordinating the main application loop via + /// Processing queued input events and translating them to Terminal.Gui events + /// Managing the that handles rendering + /// Executing scheduled timeouts and callbacks via + /// + /// + /// + /// The coordinator is created in based on the selected driver + /// (Windows, Unix, .NET, or Fake) and is started by calling + /// . + /// + /// + internal IMainLoopCoordinator? Coordinator { get; private set; } + + /// + /// INTERNAL: Creates a with the appropriate component factory + /// for the specified input record type. + /// + /// + /// Platform-specific input type: (.NET/Fake), + /// (Windows), or (Unix). + /// + /// + /// Factory function to create the component factory if + /// is not of type . + /// + /// + /// A configured with the input queue, + /// main loop, timed events, and selected component factory. + /// + private IMainLoopCoordinator CreateSubcomponents (Func> fallbackFactory) where TInputRecord : struct + { + ConcurrentQueue inputQueue = new (); + ApplicationMainLoop loop = new (); + + IComponentFactory cf; + + if (_componentFactory is IComponentFactory typedFactory) + { + cf = typedFactory; + } + else + { + cf = fallbackFactory (); + } + + return new MainLoopCoordinator (_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); } +} diff --git a/Terminal.Gui/App/ApplicationImpl.Lifecycle.cs b/Terminal.Gui/App/ApplicationImpl.Lifecycle.cs new file mode 100644 index 000000000..0a7795833 --- /dev/null +++ b/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 +{ + /// + public bool Initialized { get; set; } + + /// + public event EventHandler>? InitializedChanged; + + /// + [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; + } + + /// Shutdown an application initialized with . + 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 + /// + /// DEBUG ONLY: Asserts that an event has no remaining subscribers. + /// + /// The name of the event for diagnostic purposes. + /// The event delegate to check. + 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 + + /// + 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 + } + + /// + /// Raises the event. + /// + internal void RaiseInitializedChanged (object sender, EventArgs e) { InitializedChanged?.Invoke (sender, e); } +} diff --git a/Terminal.Gui/App/ApplicationImpl.Run.cs b/Terminal.Gui/App/ApplicationImpl.Run.cs new file mode 100644 index 000000000..fd0564e05 --- /dev/null +++ b/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 +{ + /// + /// INTERNAL: Gets or sets the managed thread ID of the application's main UI thread, which is set during + /// and used to determine if code is executing on the main thread. + /// + /// + /// The managed thread ID of the main UI thread, or if the application is not initialized. + /// + internal int? MainThreadId { get; set; } + + #region Begin->Run->Stop->End + + /// + public event EventHandler? SessionBegun; + + /// + public event EventHandler? SessionEnded; + + /// + 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; + } + + /// + public bool StopAfterFirstIteration { get; set; } + + /// + public event EventHandler? Iteration; + + /// + [RequiresUnreferencedCode ("AOT")] + [RequiresDynamicCode ("AOT")] + public Toplevel Run (Func? errorHandler = null, string? driver = null) { return Run (errorHandler, driver); } + + /// + [RequiresUnreferencedCode ("AOT")] + [RequiresDynamicCode ("AOT")] + public TView Run (Func? 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; + } + + + /// + public void Run (Toplevel view, Func? 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); + } + + /// + 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)); + } + + /// + public void RequestStop () { RequestStop (null); } + + /// + 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; + } + + /// + public void RaiseIteration () { Iteration?.Invoke (null, new ()); } + + #endregion Begin->Run->Stop->End + + #region Timeouts and Invoke + + private readonly ITimedEvents _timedEvents = new TimedEvents (); + + /// + public ITimedEvents? TimedEvents => _timedEvents; + + /// + public object AddTimeout (TimeSpan time, Func callback) { return _timedEvents.Add (time, callback); } + + /// + public bool RemoveTimeout (object token) { return _timedEvents.Remove (token); } + + /// + 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 +} diff --git a/Terminal.Gui/App/ApplicationImpl.Screen.cs b/Terminal.Gui/App/ApplicationImpl.Screen.cs new file mode 100644 index 000000000..3c2f32cb6 --- /dev/null +++ b/Terminal.Gui/App/ApplicationImpl.Screen.cs @@ -0,0 +1,177 @@ +#nullable enable + +namespace Terminal.Gui.App; + +public partial class ApplicationImpl +{ + /// + public event EventHandler>? ScreenChanged; + + private readonly object _lockScreen = new (); + private Rectangle? _screen; + + /// + 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; + } + } + } + + /// + public bool ClearScreenNextIteration { get; set; } + + /// + 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; + } + + /// + /// INTERNAL: Resets the Screen field to null so it will be recalculated on next access. + /// + private void ResetScreen () + { + lock (_lockScreen) + { + _screen = null; + } + } + + /// + /// INTERNAL: Called when the application's size has changed. Sets the size of all s and fires + /// the + /// event. + /// + /// The new screen size and position. + 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)); } + + /// + public void LayoutAndDraw (bool forceRedraw = false) + { + List 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 (); + } +} diff --git a/Terminal.Gui/App/ApplicationImpl.cs b/Terminal.Gui/App/ApplicationImpl.cs index e167876dd..916cff31f 100644 --- a/Terminal.Gui/App/ApplicationImpl.cs +++ b/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; /// -/// Implementation of core methods using the modern -/// main loop architecture with component factories for different platforms. +/// Implementation of core methods using the modern +/// main loop architecture with component factories for different platforms. /// -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 _topLevels = new (); - private int _mainThreadId = -1; - private bool _force16Colors; - private string _forceDriver = string.Empty; - private readonly List _sixel = new (); - private readonly object _lockScreen = new (); - private Rectangle? _screen; - private bool _clearScreenNextIteration; + /// + /// Creates a new instance of the Application backend. + /// + public ApplicationImpl () { } + + /// + /// INTERNAL: Creates a new instance of the Application backend. + /// + /// + internal ApplicationImpl (IComponentFactory componentFactory) { _componentFactory = componentFactory; } + + #region Singleton // Private static readonly Lazy instance of Application private static Lazy _lazyInstance = new (() => new ApplicationImpl ()); /// - /// Gets the currently configured backend implementation of gateway methods. - /// Change to your own implementation by using (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 . + /// + /// + public static void ChangeInstance (IApplication? newApplication) { _lazyInstance = new (newApplication!); } + + /// + /// Gets the currently configured backend implementation of gateway methods. + /// Change to your own implementation by using (before init). /// public static IApplication Instance => _lazyInstance.Value; - /// - public ITimedEvents? TimedEvents => _timedEvents; + #endregion Singleton - internal IMainLoopCoordinator? Coordinator => _coordinator; + private string? _driverName; + + + #region Input private IMouse? _mouse; /// - /// Handles mouse event state and processing. + /// Handles mouse event state and processing. /// 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; /// - /// Handles keyboard input and key bindings at the Application level + /// Handles keyboard input and key bindings at the Application level /// 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)); } - /// - public IConsoleDriver? Driver - { - get => _driver; - set => _driver = value; - } + #endregion Input + + #region View Management /// - public bool Initialized - { - get => _initialized; - set => _initialized = value; - } + public ApplicationPopover? Popover { get; set; } /// - public bool Force16Colors - { - get => _force16Colors; - set => _force16Colors = value; - } + public ApplicationNavigation? Navigation { get; set; } /// - public string ForceDriver - { - get => _forceDriver; - set => _forceDriver = value; - } + public Toplevel? Top { get; set; } + + // BUGBUG: Technically, this is not the full lst of TopLevels. There be dragons here, e.g. see how Toplevel.Id is used. What /// - public List Sixel => _sixel; + public ConcurrentStack TopLevels { get; } = new (); /// - public Rectangle Screen - { - get - { - lock (_lockScreen) - { - if (_screen == null) - { - _screen = Driver?.Screen ?? new (new (0, 0), new (2048, 2048)); - } + public Toplevel? CachedSessionTokenToplevel { get; set; } - 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; - } - } - } - - /// - public bool ClearScreenNextIteration - { - get => _clearScreenNextIteration; - set => _clearScreenNextIteration = value; - } - - /// - public ApplicationPopover? Popover - { - get => _popover; - set => _popover = value; - } - - /// - public ApplicationNavigation? Navigation - { - get => _navigation; - set => _navigation = value; - } - - /// - public Toplevel? Top - { - get => _top; - set => _top = value; - } - - /// - public ConcurrentStack 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`. - /// - public Toplevel? CachedRunStateToplevel { get; set; } - - /// - /// Gets or sets the main thread ID for the application. - /// - internal int MainThreadId - { - get => _mainThreadId; - set => _mainThreadId = value; - } - - /// - public void RequestStop () => RequestStop (null); - - /// - /// Creates a new instance of the Application backend. - /// - public ApplicationImpl () - { - } - - internal ApplicationImpl (IComponentFactory componentFactory) - { - _componentFactory = componentFactory; - } - - /// - /// 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 . - /// - /// - public static void ChangeInstance (IApplication newApplication) - { - _lazyInstance = new Lazy (newApplication); - } - - /// - [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; - bool factoryIsDotNet = _componentFactory is IComponentFactory; - bool factoryIsUnix = _componentFactory is IComponentFactory; - bool factoryIsFake = _componentFactory is IComponentFactory; - - // 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 (Func> fallbackFactory) - { - ConcurrentQueue inputBuffer = new (); - ApplicationMainLoop loop = new (); - - IComponentFactory cf; - - if (_componentFactory is IComponentFactory typedFactory) - { - cf = typedFactory; - } - else - { - cf = fallbackFactory (); - } - - return new MainLoopCoordinator (_timedEvents, inputBuffer, loop, cf); - } - - /// - /// Runs the application by creating a object and calling - /// . - /// - /// The created object. The caller is responsible for disposing this object. - [RequiresUnreferencedCode ("AOT")] - [RequiresDynamicCode ("AOT")] - public Toplevel Run (Func? errorHandler = null, IConsoleDriver? driver = null) { return Run (errorHandler, driver); } - - /// - /// Runs the application by creating a -derived object of type T and calling - /// . - /// - /// - /// - /// The to use. If not specified the default driver for the platform will - /// be used. Must be if has already been called. - /// - /// The created T object. The caller is responsible for disposing this object. - [RequiresUnreferencedCode ("AOT")] - [RequiresDynamicCode ("AOT")] - public T Run (Func? 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; - } - - /// Runs the Application using the provided view. - /// The to run as a modal. - /// Handler for any unhandled exceptions. - public void Run (Toplevel view, Func? 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); - } - - /// Shutdown an application initialized with . - 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 ()); - } - - /// - 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; - } - - /// - 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; - } - ); - } - - /// - public bool IsLegacy => false; - - /// - public object AddTimeout (TimeSpan time, Func callback) { return _timedEvents.Add (time, callback); } - - /// - public bool RemoveTimeout (object token) { return _timedEvents.Remove (token); } - - /// - public void LayoutAndDraw (bool forceRedraw = false) - { - List 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 (); - } - - /// - /// Resets the Screen field to null so it will be recalculated on next access. - /// - internal void ResetScreen () - { - lock (_lockScreen) - { - _screen = null; - } - } + #endregion View Management } diff --git a/Terminal.Gui/App/Clipboard/ClipboardBase.cs b/Terminal.Gui/App/Clipboard/ClipboardBase.cs index 908035f91..97cfec61e 100644 --- a/Terminal.Gui/App/Clipboard/ClipboardBase.cs +++ b/Terminal.Gui/App/Clipboard/ClipboardBase.cs @@ -103,7 +103,7 @@ public abstract class ClipboardBase : IClipboard } /// - /// Returns the contents of the OS clipboard if possible. Implemented by -specific + /// Returns the contents of the OS clipboard if possible. Implemented by -specific /// subclasses. /// /// The contents of the OS clipboard if successful. @@ -111,7 +111,7 @@ public abstract class ClipboardBase : IClipboard protected abstract string GetClipboardDataImpl (); /// - /// Pastes the to the OS clipboard if possible. Implemented by + /// Pastes the to the OS clipboard if possible. Implemented by /// -specific subclasses. /// /// The text to paste to the OS clipboard. diff --git a/Terminal.Gui/App/Clipboard/ClipboardProcessRunner.cs b/Terminal.Gui/App/Clipboard/ClipboardProcessRunner.cs index ba0b68ad5..214b5337d 100644 --- a/Terminal.Gui/App/Clipboard/ClipboardProcessRunner.cs +++ b/Terminal.Gui/App/Clipboard/ClipboardProcessRunner.cs @@ -5,7 +5,7 @@ namespace Terminal.Gui.App; /// /// 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. /// internal static class ClipboardProcessRunner { diff --git a/Terminal.Gui/App/IApplication.cs b/Terminal.Gui/App/IApplication.cs index 1fb9e84f0..64620f09e 100644 --- a/Terminal.Gui/App/IApplication.cs +++ b/Terminal.Gui/App/IApplication.cs @@ -1,4 +1,5 @@ #nullable enable +using System.Collections.Concurrent; using System.Diagnostics.CodeAnalysis; namespace Terminal.Gui.App; @@ -9,34 +10,388 @@ namespace Terminal.Gui.App; /// public interface IApplication { - /// Adds a timeout to the application. - /// - /// 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 . - /// - object AddTimeout (TimeSpan time, Func callback); + #region Keyboard /// - /// Handles keyboard input and key bindings at the Application level. + /// Handles keyboard input and key bindings at the Application level. /// + /// + /// + /// Provides access to keyboard state, key bindings, and keyboard event handling. Set during . + /// + /// IKeyboard Keyboard { get; set; } + #endregion Keyboard + + #region Mouse + /// /// Handles mouse event state and processing. /// + /// + /// + /// Provides access to mouse state, mouse grabbing, and mouse event handling. Set during . + /// + /// IMouse Mouse { get; set; } - /// Gets or sets the console driver being used. - IConsoleDriver? Driver { get; set; } + #endregion Mouse + + #region Initialization and Shutdown + + /// Initializes a new instance of Application. + /// + /// The to use. If neither or + /// are specified the default driver for the platform will be used. + /// + /// + /// The short name (e.g. "dotnet", "windows", "unix", or "fake") of the + /// to use. If neither or are + /// specified the default driver for the platform will be used. + /// + /// + /// Call this method once per instance (or after has been called). + /// + /// This function loads the right for the platform, creates a main loop coordinator, + /// initializes keyboard and mouse handlers, and subscribes to driver events. + /// + /// + /// must be called when the application is closing (typically after + /// has returned) to ensure resources are cleaned up and terminal settings restored. + /// + /// + /// The function combines and + /// into a single call. An application can use + /// without explicitly calling . + /// + /// + [RequiresUnreferencedCode ("AOT")] + [RequiresDynamicCode ("AOT")] + public void Init (IDriver? driver = null, string? driverName = null); + + /// + /// This event is raised after the and methods have been called. + /// + /// + /// Intended to support unit tests that need to know when the application has been initialized. + /// + public event EventHandler>? InitializedChanged; /// Gets or sets whether the application has been initialized. bool Initialized { get; set; } + /// Shutdown an application initialized with . + /// + /// Shutdown must be called for every call to or + /// to ensure all resources are cleaned + /// up (Disposed) and terminal settings are restored. + /// + public void Shutdown (); + + /// + /// Resets the state of this instance. + /// + /// If true, ignores disposed state checks during reset. + /// + /// + /// 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 + /// starts running and after returns. + /// + /// + /// IMPORTANT: Ensure all property/fields are reset here. See Init_ResetState_Resets_Properties unit test. + /// + /// + public void ResetState (bool ignoreDisposed = false); + + #endregion Initialization and Shutdown + + #region Begin->Run->Iteration->Stop->End + + /// + /// Building block API: Creates a and prepares the provided for + /// execution. Not usually called directly by applications. Use + /// instead. + /// + /// + /// The that needs to be passed to the method upon + /// completion. + /// + /// The to prepare execution for. + /// + /// + /// This method prepares the provided for running. It adds this to the + /// list of s, lays out the SubViews, focuses the first element, and draws the + /// on the screen. This is usually followed by starting the main loop, and then the + /// method upon termination which will undo these changes. + /// + /// + /// Raises the event before returning. + /// + /// + public SessionToken Begin (Toplevel toplevel); + + /// + /// Runs a new Session creating a and calling . When the session is + /// stopped, will be called. + /// + /// Handler for any unhandled exceptions (resumes when returns true, rethrows when null). + /// + /// The driver name. If not specified the default driver for the platform will be used. Must be + /// if has already been called. + /// + /// The created . The caller is responsible for disposing this object. + /// + /// Calling first is not needed as this function will initialize the application. + /// + /// must be called when the application is closing (typically after Run has returned) to + /// ensure resources are cleaned up and terminal settings restored. + /// + /// + /// The caller is responsible for disposing the object returned by this method. + /// + /// + [RequiresUnreferencedCode ("AOT")] + [RequiresDynamicCode ("AOT")] + public Toplevel Run (Func? errorHandler = null, string? driver = null); + + /// + /// Runs a new Session creating a -derived object of type + /// and calling . When the session is stopped, + /// will be called. + /// + /// The type of to create and run. + /// Handler for any unhandled exceptions (resumes when returns true, rethrows when null). + /// + /// The driver name. If not specified the default driver for the platform will be used. Must be + /// if has already been called. + /// + /// The created object. The caller is responsible for disposing this object. + /// + /// + /// This method is used to start processing events for the main application, but it is also used to run other + /// modal s such as boxes. + /// + /// + /// To make stop execution, call + /// or . + /// + /// + /// Calling is equivalent to calling + /// , followed by starting the main loop, and then calling + /// . + /// + /// + /// When using or , + /// will be called automatically. + /// + /// + /// In RELEASE builds: When is any exceptions will be + /// rethrown. Otherwise, will be called. If + /// returns the main loop will resume; otherwise this method will exit. + /// + /// + /// must be called when the application is closing (typically after Run has returned) to + /// ensure resources are cleaned up and terminal settings restored. + /// + /// + /// In RELEASE builds: When is any exceptions will be + /// rethrown. Otherwise, will be called. If + /// returns the main loop will resume; otherwise this method will exit. + /// + /// + /// The caller is responsible for disposing the object returned by this method. + /// + /// + [RequiresUnreferencedCode ("AOT")] + [RequiresDynamicCode ("AOT")] + public TView Run (Func? errorHandler = null, string? driver = null) + where TView : Toplevel, new (); + + /// + /// Runs a new Session using the provided view and calling + /// . + /// When the session is stopped, will be called.. + /// + /// The to run as a modal. + /// Handler for any unhandled exceptions (resumes when returns true, rethrows when null). + /// + /// + /// This method is used to start processing events for the main application, but it is also used to run other + /// modal s such as boxes. + /// + /// + /// To make stop execution, call + /// or . + /// + /// + /// Calling is equivalent to calling + /// , followed by starting the main loop, and then calling + /// . + /// + /// + /// When using or , + /// will be called automatically. + /// + /// + /// must be called when the application is closing (typically after Run has returned) to + /// ensure resources are cleaned up and terminal settings restored. + /// + /// + /// In RELEASE builds: When is any exceptions will be + /// rethrown. Otherwise, will be called. If + /// returns the main loop will resume; otherwise this method will exit. + /// + /// + /// The caller is responsible for disposing the object returned by this method. + /// + /// + public void Run (Toplevel view, Func? errorHandler = null); + + /// + /// Raises the event. + /// + /// + /// This is called once per main loop iteration, before processing input, timeouts, or rendering. + /// + public void RaiseIteration (); + + /// This event is raised on each iteration of the main loop. + /// + /// + /// This event is raised before input processing, timeout callbacks, and rendering occur each iteration. + /// + /// See also and . + /// + public event EventHandler? Iteration; + + /// Runs on the main UI loop thread. + /// The action to be invoked on the main processing thread. + /// + /// + /// If called from the main thread, the action is executed immediately. Otherwise, it is queued via + /// with and will be executed on the next main loop + /// iteration. + /// + /// + void Invoke (Action action); + + /// + /// Building block API: Ends a Session and completes the execution of a that was started with + /// . Not usually called directly by applications. + /// + /// will automatically call this method when the session is stopped. + /// + /// The returned by the method. + /// + /// + /// This method removes the from the stack, raises the + /// event, and disposes the . + /// + /// + public void End (SessionToken sessionToken); + + /// Requests that the currently running Session stop. The Session will stop after the current iteration completes. + /// + /// This will cause to return. + /// + /// This is equivalent to calling with as the parameter. + /// + /// + void RequestStop (); + + /// Requests that the currently running Session stop. The Session will stop after the current iteration completes. + /// + /// The to stop. If , stops the currently running . + /// + /// + /// This will cause to return. + /// + /// Calling is equivalent to setting the + /// property on the specified to . + /// + /// + void RequestStop (Toplevel? top); + + /// + /// Set to to cause the session to stop running after first iteration. + /// + /// + /// + /// Used primarily for unit testing. When , will be called + /// automatically after the first main loop iteration. + /// + /// + bool StopAfterFirstIteration { get; set; } + + /// + /// Raised when has been called and has created a new . + /// + /// + /// If is , callers to + /// must also subscribe to and manually dispose of the token + /// when the application is done. + /// + public event EventHandler? SessionBegun; + + /// + /// Raised when was called and the session is stopping. The event args contain a + /// reference to the + /// that was active during the session. This can be used to ensure the Toplevel is disposed of properly. + /// + /// + /// If is , callers to + /// must also subscribe to and manually dispose of the token + /// when the application is done. + /// + public event EventHandler? SessionEnded; + + #endregion Begin->Run->Iteration->Stop->End + + #region Toplevel Management + + /// Gets or sets the current Toplevel. + /// + /// + /// This is set by and cleared by . + /// + /// + Toplevel? Top { get; set; } + + /// Gets the stack of all Toplevels. + /// + /// + /// Toplevels are added to this stack by and removed by + /// . + /// + /// + ConcurrentStack TopLevels { get; } + + /// + /// Caches the Toplevel associated with the current Session. + /// + /// + /// Used internally to optimize Toplevel state transitions. + /// + Toplevel? CachedSessionTokenToplevel { get; set; } + + #endregion Toplevel Management + + #region Screen and Driver + + /// Gets or sets the console driver being used. + /// + /// + /// Set by based on the driver parameter or platform default. + /// + /// + IDriver? Driver { get; set; } + /// /// Gets or sets whether will be forced to output only the 16 colors defined in - /// . The default is , meaning 24-bit (TrueColor) colors will be output - /// as long as the selected supports TrueColor. + /// . The default is , meaning 24-bit (TrueColor) colors will be + /// output as long as the selected supports TrueColor. /// bool Force16Colors { get; set; } @@ -47,219 +402,141 @@ public interface IApplication string ForceDriver { get; set; } /// - /// Collection of sixel images to write out to screen when updating. - /// Only add to this collection if you are sure terminal supports sixel format. - /// - List Sixel { get; } - - /// - /// Gets or sets the size of the screen. By default, this is the size of the screen as reported by the . + /// Gets or sets the size of the screen. By default, this is the size of the screen as reported by the + /// . /// + /// + /// + /// If the has not been initialized, this will return a default size of 2048x2048; useful + /// for unit tests. + /// + /// Rectangle Screen { get; set; } + /// Raised when the terminal's size changed. The new size of the terminal is provided. + /// + /// + /// This event is raised when the driver detects a screen size change. The event provides the new screen + /// rectangle. + /// + /// + public event EventHandler>? ScreenChanged; + /// /// Gets or sets whether the screen will be cleared, and all Views redrawn, during the next Application iteration. /// + /// + /// + /// This is typically set to when a View's changes and that view + /// has no SuperView (e.g. when is moved or resized). + /// + /// + /// Automatically reset to after processes it. + /// + /// bool ClearScreenNextIteration { get; set; } - /// Gets or sets the popover manager. - ApplicationPopover? Popover { get; set; } - - /// Gets or sets the navigation manager. - ApplicationNavigation? Navigation { get; set; } - - /// Gets the currently active Toplevel. - Toplevel? Top { get; set; } - - /// Gets the stack of all Toplevels. - System.Collections.Concurrent.ConcurrentStack TopLevels { get; } - /// - /// Caches the Toplevel associated with the current RunState. + /// Collection of sixel images to write out to screen when updating. + /// Only add to this collection if you are sure terminal supports sixel format. /// - Toplevel? CachedRunStateToplevel { get; set; } + List Sixel { get; } - /// Requests that the application stop running. - void RequestStop (); + #endregion Screen and Driver + + #region Layout and Drawing /// - /// 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 ) will be laid out. - /// Only Views that need to be drawn (see ) will be drawn. + /// 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 ) will be laid out. Only Views that need to be drawn + /// (see ) will be drawn. /// /// /// If the entire View hierarchy will be redrawn. The default is and - /// should only be overriden for testing. + /// should only be overridden for testing. /// + /// + /// + /// This method is called automatically each main loop iteration when any views need layout or drawing. + /// + /// + /// If is , the screen will be cleared before + /// drawing and the flag will be reset to . + /// + /// public void LayoutAndDraw (bool forceRedraw = false); - /// Initializes a new instance of Application. - /// Call this method once per instance (or after has been called). - /// - /// This function loads the right for the platform, Creates a . and - /// assigns it to - /// - /// - /// must be called when the application is closing (typically after - /// has returned) to ensure resources are cleaned up and - /// terminal settings - /// restored. - /// - /// - /// The function combines - /// and - /// into a single - /// call. An application cam use without explicitly calling - /// . - /// - /// - /// The to use. If neither or - /// are specified the default driver for the platform will be used. - /// - /// - /// The driver name (e.g. "dotnet", "windows", "fake", or "unix") of the - /// to use. If neither or are - /// specified the default driver for the platform will be used. - /// - [RequiresUnreferencedCode ("AOT")] - [RequiresDynamicCode ("AOT")] - public void Init (IConsoleDriver? driver = null, string? driverName = null); - - /// Runs on the main UI loop thread - /// the action to be invoked on the main processing thread. - void Invoke (Action action); - /// - /// if implementation is 'old'. if implementation - /// is cutting edge. + /// Calls on the most focused view. /// - bool IsLegacy { get; } + /// + /// Does nothing if there is no most focused view. + /// + /// If the most focused view is not visible within its superview, the cursor will be hidden. + /// + /// + /// if a view positioned the cursor and the position is visible. + public bool PositionCursor (); - /// Removes a previously scheduled timeout - /// The token parameter is the value returned by . + #endregion Layout and Drawing + + #region Navigation and Popover + + /// Gets or sets the popover manager. + /// + /// + /// Manages application-level popover views. Initialized during . + /// + /// + ApplicationPopover? Popover { get; set; } + + /// Gets or sets the navigation manager. + /// + /// + /// Manages focus navigation and tracking of the most focused view. Initialized during . + /// + /// + ApplicationNavigation? Navigation { get; set; } + + #endregion Navigation and Popover + + #region Timeouts + + /// Adds a timeout to the application. + /// The time span to wait before invoking the callback. + /// + /// The callback to invoke. If it returns , the timeout will be reset and repeat. If it + /// returns , the timeout will stop and be removed. + /// /// - /// - /// if the timeout is successfully removed; otherwise, - /// - /// . - /// This method also returns - /// - /// if the timeout is not found. + /// A token that can be used to stop the timeout by calling . + /// + /// + /// + /// When the time specified passes, the callback will be invoked on the main UI thread. + /// + /// + object AddTimeout (TimeSpan time, Func callback); + + /// Removes a previously scheduled timeout. + /// The token returned by . + /// + /// if the timeout is successfully removed; otherwise, . + /// This method also returns if the timeout is not found. /// bool RemoveTimeout (object token); - /// Stops the provided , causing or the if provided. - /// The to stop. - /// - /// This will cause to return. - /// - /// Calling is equivalent to setting the - /// property on the currently running to false. - /// - /// - void RequestStop (Toplevel? top); - - /// - /// Runs the application by creating a object and calling - /// . - /// - /// - /// Calling first is not needed as this function will initialize the application. - /// - /// must be called when the application is closing (typically after Run> has returned) to - /// ensure resources are cleaned up and terminal settings restored. - /// - /// - /// The caller is responsible for disposing the object returned by this method. - /// - /// - /// The created object. The caller is responsible for disposing this object. - [RequiresUnreferencedCode ("AOT")] - [RequiresDynamicCode ("AOT")] - public Toplevel Run (Func? errorHandler = null, IConsoleDriver? driver = null); - - /// - /// Runs the application by creating a -derived object of type T and calling - /// . - /// - /// - /// Calling first is not needed as this function will initialize the application. - /// - /// must be called when the application is closing (typically after Run> has returned) to - /// ensure resources are cleaned up and terminal settings restored. - /// - /// - /// The caller is responsible for disposing the object returned by this method. - /// - /// - /// - /// - /// The to use. If not specified the default driver for the platform will - /// be used. Must be - /// if has already been called. - /// - /// The created T object. The caller is responsible for disposing this object. - [RequiresUnreferencedCode ("AOT")] - [RequiresDynamicCode ("AOT")] - public T Run (Func? errorHandler = null, IConsoleDriver? driver = null) - where T : Toplevel, new(); - - /// Runs the Application using the provided view. - /// - /// - /// This method is used to start processing events for the main application, but it is also used to run other - /// modal s such as boxes. - /// - /// - /// To make a stop execution, call - /// . - /// - /// - /// Calling is equivalent to calling - /// , followed by , and then - /// calling - /// . - /// - /// - /// Alternatively, to have a program control the main loop and process events manually, call - /// to set things up manually and then repeatedly call - /// with the wait parameter set to false. By doing this the - /// method will only process any pending events, timers handlers and - /// then - /// return control immediately. - /// - /// - /// When using or - /// - /// will be called automatically. - /// - /// - /// RELEASE builds only: When is any exceptions will be - /// rethrown. Otherwise, if will be called. If - /// returns the will resume; otherwise this - /// method will - /// exit. - /// - /// - /// The to run as a modal. - /// - /// RELEASE builds only: Handler for any unhandled exceptions (resumes when returns true, - /// rethrows when null). - /// - public void Run (Toplevel view, Func? errorHandler = null); - - /// Shutdown an application initialized with . - /// - /// Shutdown must be called for every call to or - /// to ensure all resources are cleaned - /// up (Disposed) - /// and terminal settings are restored. - /// - public void Shutdown (); - /// /// Handles recurring events. These are invoked on the main UI thread - allowing for /// safe updates to instances. /// + /// + /// + /// Provides low-level access to the timeout management system. Most applications should use + /// and instead. + /// + /// ITimedEvents? TimedEvents { get; } + + #endregion Timeouts } diff --git a/Terminal.Gui/App/IterationEventArgs.cs b/Terminal.Gui/App/IterationEventArgs.cs index 417346eee..e0c98d2ab 100644 --- a/Terminal.Gui/App/IterationEventArgs.cs +++ b/Terminal.Gui/App/IterationEventArgs.cs @@ -1,5 +1,5 @@ namespace Terminal.Gui.App; -/// Event arguments for the event. +/// Event arguments for the event. public class IterationEventArgs : EventArgs { } diff --git a/Terminal.Gui/App/Keyboard/IKeyboard.cs b/Terminal.Gui/App/Keyboard/IKeyboard.cs index 9377db02b..d0fbd023e 100644 --- a/Terminal.Gui/App/Keyboard/IKeyboard.cs +++ b/Terminal.Gui/App/Keyboard/IKeyboard.cs @@ -17,7 +17,7 @@ public interface IKeyboard IApplication? Application { get; set; } /// - /// Called when the user presses a key (by the ). Raises the cancelable + /// Called when the user presses a key (by the ). Raises the cancelable /// event, then calls on all top level views, and finally /// if the key was not handled, invokes any Application-scoped . /// @@ -27,7 +27,7 @@ public interface IKeyboard bool RaiseKeyDownEvent (Key key); /// - /// Called when the user releases a key (by the ). Raises the cancelable + /// Called when the user releases a key (by the ). Raises the cancelable /// /// event /// then calls on all top level views. Called after . diff --git a/Terminal.Gui/App/Keyboard/KeyboardImpl.cs b/Terminal.Gui/App/Keyboard/KeyboardImpl.cs index 05abbb066..0741f4c53 100644 --- a/Terminal.Gui/App/Keyboard/KeyboardImpl.cs +++ b/Terminal.Gui/App/Keyboard/KeyboardImpl.cs @@ -1,4 +1,6 @@ #nullable enable +using System.Diagnostics; + namespace Terminal.Gui.App; /// @@ -114,7 +116,8 @@ internal class KeyboardImpl : IKeyboard /// 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 diff --git a/Terminal.Gui/App/MainLoop/ApplicationMainLoop.cs b/Terminal.Gui/App/MainLoop/ApplicationMainLoop.cs index c64ff95b4..69a16bd49 100644 --- a/Terminal.Gui/App/MainLoop/ApplicationMainLoop.cs +++ b/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; /// Throttling iterations to respect /// /// -/// Type of raw input events, e.g. for .NET driver -public class ApplicationMainLoop : IApplicationMainLoop +/// Type of raw input events, e.g. for .NET driver +public class ApplicationMainLoop : IApplicationMainLoop where TInputRecord : struct { private ITimedEvents? _timedEvents; - private ConcurrentQueue? _inputBuffer; + private ConcurrentQueue? _inputQueue; private IInputProcessor? _inputProcessor; - private IConsoleOutput? _out; + private IOutput? _output; private AnsiRequestScheduler? _ansiRequestScheduler; - private IConsoleSizeMonitor? _consoleSizeMonitor; + private ISizeMonitor? _sizeMonitor; /// public ITimedEvents TimedEvents @@ -40,13 +41,13 @@ public class ApplicationMainLoop : IApplicationMainLoop /// /// The input events thread-safe collection. This is populated on separate - /// thread by a . Is drained as part of each - /// + /// thread by a . Is drained as part of each + /// on the main loop thread. /// - public ConcurrentQueue InputBuffer + public ConcurrentQueue InputQueue { - get => _inputBuffer ?? throw new NotInitializedException (nameof (InputBuffer)); - private set => _inputBuffer = value; + get => _inputQueue ?? throw new NotInitializedException (nameof (InputQueue)); + private set => _inputQueue = value; } /// @@ -57,13 +58,13 @@ public class ApplicationMainLoop : IApplicationMainLoop } /// - public IOutputBuffer OutputBuffer { get; } = new OutputBuffer (); + public IOutputBuffer OutputBuffer { get; } = new OutputBufferImpl (); /// - 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; } /// @@ -74,10 +75,10 @@ public class ApplicationMainLoop : IApplicationMainLoop } /// - 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; } /// @@ -85,12 +86,6 @@ public class ApplicationMainLoop : IApplicationMainLoop /// public IToplevelTransitionManager ToplevelTransitionManager = new ToplevelTransitionManager (); - /// - /// Determines how to get the current system type, adjust - /// in unit tests to simulate specific timings. - /// - public Func Now { get; set; } = () => DateTime.Now; - /// /// Initializes the class with the provided subcomponents /// @@ -101,34 +96,34 @@ public class ApplicationMainLoop : IApplicationMainLoop /// public void Initialize ( ITimedEvents timedEvents, - ConcurrentQueue inputBuffer, + ConcurrentQueue inputBuffer, IInputProcessor inputProcessor, - IConsoleOutput consoleOutput, - IComponentFactory componentFactory + IOutput consoleOutput, + IComponentFactory 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); } /// 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 : IApplicationMainLoop 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 : IApplicationMainLoop || 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 : IApplicationMainLoop 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 : IApplicationMainLoop // 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); } } diff --git a/Terminal.Gui/App/MainLoop/IApplicationMainLoop.cs b/Terminal.Gui/App/MainLoop/IApplicationMainLoop.cs index f4069315e..014e002ba 100644 --- a/Terminal.Gui/App/MainLoop/IApplicationMainLoop.cs +++ b/Terminal.Gui/App/MainLoop/IApplicationMainLoop.cs @@ -15,27 +15,27 @@ namespace Terminal.Gui.App; /// Rendering UI updates to the console /// /// -/// Type of raw input events processed by the loop, e.g. for cross-platform .NET driver -public interface IApplicationMainLoop : IDisposable +/// Type of raw input events processed by the loop, e.g. for cross-platform .NET driver +public interface IApplicationMainLoop : IDisposable where TInputRecord : struct { /// - /// Gets the class responsible for servicing user timeouts + /// Gets the implementation that manages user-defined timeouts and periodic events. /// public ITimedEvents TimedEvents { get; } /// - /// Gets the class responsible for writing final rendered output to the console + /// Gets the representing the desired screen state for console rendering. /// public IOutputBuffer OutputBuffer { get; } /// - /// Class for writing output to the console. + /// Gets the implementation responsible for rendering the to the console using platform specific methods. /// - public IConsoleOutput Out { get; } + public IOutput Output { get; } /// - /// Gets the class responsible for processing buffered console input and translating - /// it into events on the UI thread. + /// Gets implementation that processes the mouse and keyboard input populated by + /// implementations on the input thread and translating to events on the UI thread. /// public IInputProcessor InputProcessor { get; } @@ -46,24 +46,59 @@ public interface IApplicationMainLoop : IDisposable public AnsiRequestScheduler AnsiRequestScheduler { get; } /// - /// Gets the class responsible for determining the current console size + /// Gets the implementation that tracks terminal size changes. /// - public IConsoleSizeMonitor ConsoleSizeMonitor { get; } + public ISizeMonitor SizeMonitor { get; } /// - /// Initializes the loop with a buffer from which data can be read + /// Initializes the main loop with its required dependencies. /// - /// - /// - /// - /// - /// + /// + /// The implementation for managing user-defined timeouts and periodic callbacks + /// (e.g., ). + /// + /// + /// The thread-safe queue containing raw input events populated by on + /// the input thread. This queue is drained by during each . + /// + /// + /// The that translates raw input records (e.g., ) + /// into Terminal.Gui events (, ) and raises them on the main UI thread. + /// + /// + /// The implementation responsible for rendering the to the + /// console using platform-specific methods (e.g., Win32 APIs, ANSI escape sequences). + /// + /// + /// The factory for creating driver-specific components. Used here to create the + /// that tracks terminal size changes. + /// + /// + /// + /// This method is called by during application startup + /// to wire up all the components needed for the main loop to function. It must be called before + /// can be invoked. + /// + /// + /// Initialization order: + /// + /// + /// Store references to , , + /// , and + /// Create for managing ANSI requests/responses + /// Initialize size to match current console dimensions + /// Create using the + /// + /// + /// After initialization, the main loop is ready to process events via . + /// + /// void Initialize ( ITimedEvents timedEvents, - ConcurrentQueue inputBuffer, + ConcurrentQueue inputQueue, IInputProcessor inputProcessor, - IConsoleOutput consoleOutput, - IComponentFactory componentFactory + IOutput output, + IComponentFactory componentFactory ); /// diff --git a/Terminal.Gui/App/MainLoop/IMainLoopCoordinator.cs b/Terminal.Gui/App/MainLoop/IMainLoopCoordinator.cs index fcffa8c83..e08b2a742 100644 --- a/Terminal.Gui/App/MainLoop/IMainLoopCoordinator.cs +++ b/Terminal.Gui/App/MainLoop/IMainLoopCoordinator.cs @@ -8,7 +8,6 @@ /// /// Starting the asynchronous input reading thread /// Initializing the main UI loop on the application thread -/// Building the facade /// Coordinating clean shutdown of both threads /// /// @@ -26,7 +25,7 @@ public interface IMainLoopCoordinator /// /// /// A task that completes when initialization is done - public Task StartAsync (); + public Task StartInputTaskAsync (); /// /// Stops the input thread and performs cleanup. diff --git a/Terminal.Gui/App/MainLoop/MainLoopCoordinator.cs b/Terminal.Gui/App/MainLoop/MainLoopCoordinator.cs index 60f6ced3e..45fd2a7d3 100644 --- a/Terminal.Gui/App/MainLoop/MainLoopCoordinator.cs +++ b/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; /// /// This class is designed to be managed by /// -/// Type of raw input events, e.g. for .NET driver -internal class MainLoopCoordinator : IMainLoopCoordinator +/// Type of raw input events, e.g. for .NET driver +internal class MainLoopCoordinator : IMainLoopCoordinator where TInputRecord : struct { - private readonly ConcurrentQueue _inputBuffer; - private readonly IInputProcessor _inputProcessor; - private readonly IApplicationMainLoop _loop; - private readonly IComponentFactory _componentFactory; - private readonly CancellationTokenSource _tokenSource = new (); - private IConsoleInput _input; - private IConsoleOutput _output; - private readonly object _oLockInitialization = new (); - private ConsoleDriverFacade _facade; - private Task _inputTask; - private readonly ITimedEvents _timedEvents; - - private readonly SemaphoreSlim _startupSemaphore = new (0, 1); - /// /// Creates a new coordinator that will manage the main UI loop and input thread. /// /// Handles scheduling and execution of user timeout callbacks - /// Thread-safe queue for buffering raw console input + /// Thread-safe queue for buffering raw console input /// The main application loop instance /// Factory for creating driver-specific components (input, output, etc.) public MainLoopCoordinator ( ITimedEvents timedEvents, - ConcurrentQueue inputBuffer, - IApplicationMainLoop loop, - IComponentFactory componentFactory + ConcurrentQueue inputQueue, + IApplicationMainLoop loop, + IComponentFactory componentFactory ) { _timedEvents = timedEvents; - _inputBuffer = inputBuffer; - _inputProcessor = componentFactory.CreateInputProcessor (_inputBuffer); + _inputQueue = inputQueue; + _inputProcessor = componentFactory.CreateInputProcessor (_inputQueue); _loop = loop; _componentFactory = componentFactory; } + private readonly IApplicationMainLoop _loop; + private readonly IComponentFactory _componentFactory; + private readonly CancellationTokenSource _runCancellationTokenSource = new (); + private readonly ConcurrentQueue _inputQueue; + private readonly IInputProcessor _inputProcessor; + private readonly object _oLockInitialization = new (); + private readonly ITimedEvents _timedEvents; + + private readonly SemaphoreSlim _startupSemaphore = new (0, 1); + private IInput _input; + private Task _inputTask; + private IOutput _output; + private DriverImpl _driver; + + private bool _stopCalled; + /// /// Starts the input loop thread in separate task (returning immediately). /// - public async Task StartAsync () + public async Task StartInputTaskAsync () { - Logging.Logger.LogInformation ("Main Loop Coordinator booting..."); + Logging.Trace ("Booting... ()"); _inputTask = Task.Run (RunInput); @@ -80,85 +80,21 @@ internal class MainLoopCoordinator : 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"); - } - - private void RunInput () - { - 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) - { - Logging.Logger.LogCritical (e, "Input loop crashed"); - - throw; - } - - if (_stopCalled) - { - Logging.Logger.LogInformation ("Input loop exited cleanly"); - } - else - { - Logging.Logger.LogCritical ("Input loop exited early (stop not called)"); - } + Logging.Trace ("Booting complete"); } /// - public void RunIteration () { _loop.Iteration (); } - - private void BootMainLoop () + public void RunIteration () { lock (_oLockInitialization) { - // Instance must be constructed on the thread in which it is used. - _output = _componentFactory.CreateOutput (); - _loop.Initialize (_timedEvents, _inputBuffer, _inputProcessor, _output,_componentFactory); - - BuildFacadeIfPossible (); + _loop.Iteration (); } } - private void BuildFacadeIfPossible () - { - if (_input != null && _output != null) - { - _facade = new ( - _inputProcessor, - _loop.OutputBuffer, - _output, - _loop.AnsiRequestScheduler, - _loop.ConsoleSizeMonitor); - - Application.Driver = _facade; - - _startupSemaphore.Release (); - } - } - - private bool _stopCalled; - /// public void Stop () { @@ -170,10 +106,91 @@ internal class MainLoopCoordinator : IMainLoopCoordinator _stopCalled = true; - _tokenSource.Cancel (); + _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, _inputQueue, _inputProcessor, _output, _componentFactory); + + BuildDriverIfPossible (); + } + } + + private void BuildDriverIfPossible () + { + + if (_input != null && _output != null) + { + _driver = new ( + _inputProcessor, + _loop.OutputBuffer, + _output, + _loop.AnsiRequestScheduler, + _loop.SizeMonitor); + + Application.Driver = _driver; + + _startupSemaphore.Release (); + Logging.Trace ($"Driver: _input: {_input}, _output: {_output}"); + } + } + + /// + /// INTERNAL: Runs the IInput read loop on a new thread called the "Input Thread". + /// + private void RunInput () + { + try + { + lock (_oLockInitialization) + { + // Instance must be constructed on the thread in which it is used. + _input = _componentFactory.CreateInput (); + _input.Initialize (_inputQueue); + + // Wire up InputImpl reference for ITestableInput support + if (_inputProcessor is InputProcessorImpl impl) + { + impl.InputImpl = _input; + } + + BuildDriverIfPossible (); + } + + 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)"); + } + } } diff --git a/Terminal.Gui/App/Mouse/MouseImpl.cs b/Terminal.Gui/App/Mouse/MouseImpl.cs index e30266a5c..78d662dde 100644 --- a/Terminal.Gui/App/Mouse/MouseImpl.cs +++ b/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 /// 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. diff --git a/Terminal.Gui/App/RunStateEventArgs.cs b/Terminal.Gui/App/RunStateEventArgs.cs deleted file mode 100644 index 95e11eb03..000000000 --- a/Terminal.Gui/App/RunStateEventArgs.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Terminal.Gui.App; - -/// Event arguments for events about -public class RunStateEventArgs : EventArgs -{ - /// Creates a new instance of the class - /// - public RunStateEventArgs (RunState state) { State = state; } - - /// The state being reported on by the event - public RunState State { get; } -} diff --git a/Terminal.Gui/App/RunState.cs b/Terminal.Gui/App/SessionToken.cs similarity index 64% rename from Terminal.Gui/App/RunState.cs rename to Terminal.Gui/App/SessionToken.cs index a9c3017d3..d6466ed3f 100644 --- a/Terminal.Gui/App/RunState.cs +++ b/Terminal.Gui/App/SessionToken.cs @@ -2,22 +2,22 @@ namespace Terminal.Gui.App; -/// The execution state for a view. -public class RunState : IDisposable +/// Defines a session token for a running . +public class SessionToken : IDisposable { - /// Initializes a new class. + /// Initializes a new class. /// - public RunState (Toplevel view) { Toplevel = view; } + public SessionToken (Toplevel view) { Toplevel = view; } - /// The belonging to this . + /// The belonging to this . public Toplevel Toplevel { get; internal set; } - /// Releases all resource used by the object. - /// Call when you are finished using the . + /// Releases all resource used by the object. + /// Call when you are finished using the . /// - /// method leaves the in an unusable state. After calling - /// , you must release all references to the so the garbage collector can - /// reclaim the memory that the was occupying. + /// method leaves the in an unusable state. After calling + /// , you must release all references to the so the garbage collector can + /// reclaim the memory that the was occupying. /// public void Dispose () { @@ -28,7 +28,7 @@ public class RunState : IDisposable #endif } - /// Releases all resource used by the object. + /// Releases all resource used by the object. /// If set to we are disposing and should dispose held objects. 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 /// - /// Gets whether was called on this RunState or not. + /// Gets whether was called on this SessionToken or not. /// For debug purposes to verify objects are being disposed properly. /// Only valid when DEBUG_IDISPOSABLE is defined. /// public bool WasDisposed { get; private set; } /// - /// Gets the number of times was called on this object. + /// Gets the number of times was called on this object. /// For debug purposes to verify objects are being disposed properly. /// Only valid when DEBUG_IDISPOSABLE is defined. /// public int DisposedCount { get; private set; } = 0; /// - /// 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. /// - public static ConcurrentBag Instances { get; private set; } = []; + public static ConcurrentBag Instances { get; private set; } = []; - /// Creates a new RunState object. - public RunState () + /// Creates a new SessionToken object. + public SessionToken () { Instances.Add (this); } diff --git a/Terminal.Gui/App/SessionTokenEventArgs.cs b/Terminal.Gui/App/SessionTokenEventArgs.cs new file mode 100644 index 000000000..ef8a8b126 --- /dev/null +++ b/Terminal.Gui/App/SessionTokenEventArgs.cs @@ -0,0 +1,12 @@ +namespace Terminal.Gui.App; + +/// Event arguments for events about +public class SessionTokenEventArgs : EventArgs +{ + /// Creates a new instance of the class + /// + public SessionTokenEventArgs (SessionToken state) { State = state; } + + /// The state being reported on by the event + public SessionToken State { get; } +} diff --git a/Terminal.Gui/App/Timeout/ITimedEvents.cs b/Terminal.Gui/App/Timeout/ITimedEvents.cs index 225301ffe..aa9499520 100644 --- a/Terminal.Gui/App/Timeout/ITimedEvents.cs +++ b/Terminal.Gui/App/Timeout/ITimedEvents.cs @@ -23,7 +23,7 @@ public interface ITimedEvents /// /// Invoked when a new timeout is added. To be used in the case when - /// is . + /// is . /// event EventHandler? Added; diff --git a/Terminal.Gui/Drawing/Cell.cs b/Terminal.Gui/Drawing/Cell.cs index 0a76f8713..e72a7837e 100644 --- a/Terminal.Gui/Drawing/Cell.cs +++ b/Terminal.Gui/Drawing/Cell.cs @@ -5,7 +5,7 @@ namespace Terminal.Gui.Drawing; /// /// Represents a single row/column in a Terminal.Gui rendering surface (e.g. and -/// ). +/// ). /// public record struct Cell (Attribute? Attribute = null, bool IsDirty = false, Rune Rune = default) { diff --git a/Terminal.Gui/Drawing/LineCanvas/LineCanvas.cs b/Terminal.Gui/Drawing/LineCanvas/LineCanvas.cs index 672c24830..637b61861 100644 --- a/Terminal.Gui/Drawing/LineCanvas/LineCanvas.cs +++ b/Terminal.Gui/Drawing/LineCanvas/LineCanvas.cs @@ -402,7 +402,7 @@ public class LineCanvas : IDisposable // TODO: Add other resolvers }; - private Cell? GetCellForIntersects (IConsoleDriver? driver, ReadOnlySpan intersects) + private Cell? GetCellForIntersects (IDriver? driver, ReadOnlySpan intersects) { if (intersects.IsEmpty) { @@ -422,7 +422,7 @@ public class LineCanvas : IDisposable return cell; } - private Rune? GetRuneForIntersects (IConsoleDriver? driver, ReadOnlySpan intersects) + private Rune? GetRuneForIntersects (IDriver? driver, ReadOnlySpan intersects) { if (intersects.IsEmpty) { @@ -769,7 +769,7 @@ public class LineCanvas : IDisposable internal Rune _thickV; protected IntersectionRuneResolver () { SetGlyphs (); } - public Rune? GetRuneForIntersects (IConsoleDriver? driver, ReadOnlySpan intersects) + public Rune? GetRuneForIntersects (IDriver? driver, ReadOnlySpan intersects) { // Note that there aren't any glyphs for intersections of double lines with heavy lines diff --git a/Terminal.Gui/Drawing/Ruler.cs b/Terminal.Gui/Drawing/Ruler.cs index b7e91485d..89ef6b6d1 100644 --- a/Terminal.Gui/Drawing/Ruler.cs +++ b/Terminal.Gui/Drawing/Ruler.cs @@ -24,7 +24,7 @@ internal class Ruler /// The location to start drawing the ruler, in screen-relative coordinates. /// The start value of the ruler. /// Optional Driver. If not provided, driver will be used. - 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) { diff --git a/Terminal.Gui/Drawing/Sixel/SixelToRender.cs b/Terminal.Gui/Drawing/Sixel/SixelToRender.cs index a2a3e9bb7..c66d4bdaf 100644 --- a/Terminal.Gui/Drawing/Sixel/SixelToRender.cs +++ b/Terminal.Gui/Drawing/Sixel/SixelToRender.cs @@ -2,7 +2,7 @@ /// /// Describes a request to render a given at a given . -/// Requires that the terminal and both support sixel. +/// Requires that the terminal and both support sixel. /// public class SixelToRender { diff --git a/Terminal.Gui/Drawing/Thickness.cs b/Terminal.Gui/Drawing/Thickness.cs index 72b0f6ff8..c89773f1c 100644 --- a/Terminal.Gui/Drawing/Thickness.cs +++ b/Terminal.Gui/Drawing/Thickness.cs @@ -90,7 +90,7 @@ public record struct Thickness /// The diagnostics label to draw on the bottom of the . /// Optional driver. If not specified, will be used. /// The inner rectangle remaining to be drawn. - 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) { diff --git a/Terminal.Gui/Drivers/AnsiHandling/AnsiEscapeSequenceRequest.cs b/Terminal.Gui/Drivers/AnsiHandling/AnsiEscapeSequenceRequest.cs index 0bdc82ca9..de61ae920 100644 --- a/Terminal.Gui/Drivers/AnsiHandling/AnsiEscapeSequenceRequest.cs +++ b/Terminal.Gui/Drivers/AnsiHandling/AnsiEscapeSequenceRequest.cs @@ -22,7 +22,7 @@ public class AnsiEscapeSequenceRequest : AnsiEscapeSequence /// - /// Sends the to the raw output stream of the current . + /// Sends the to the raw output stream of the current . /// Only call this method from the main UI thread. You should use if /// sending many requests. /// diff --git a/Terminal.Gui/Drivers/AnsiHandling/AnsiResponseParser.cs b/Terminal.Gui/Drivers/AnsiHandling/AnsiResponseParser.cs index 0c4d5d3ba..5bafbfe55 100644 --- a/Terminal.Gui/Drivers/AnsiHandling/AnsiResponseParser.cs +++ b/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 : AnsiResponseParserBase +internal class AnsiResponseParser () : AnsiResponseParserBase (new GenericHeld ()) { - public AnsiResponseParser () : base (new GenericHeld ()) { } - /// - public Func>, bool> UnexpectedResponseHandler { get; set; } = _ => false; + public Func>, bool> UnexpectedResponseHandler { get; set; } = _ => false; - public IEnumerable> ProcessInput (params Tuple [] input) + public IEnumerable> ProcessInput (params Tuple [] input) { - List> output = new (); + List> output = []; ProcessInputBase ( i => input [i].Item1, @@ -499,22 +498,22 @@ internal class AnsiResponseParser : AnsiResponseParserBase return output; } - private void AppendOutput (List> output, object c) + private void AppendOutput (List> output, object c) { - Tuple tuple = (Tuple)c; + Tuple tuple = (Tuple)c; - Logging.Trace ($"AnsiResponseParser releasing '{tuple.Item1}'"); + //Logging.Trace ($"AnsiResponseParser releasing '{tuple.Item1}'"); output.Add (tuple); } - public Tuple [] Release () + public Tuple [] Release () { // Lock in case Release is called from different Thread from parse lock (_lockState) { TryLastMinuteSequences (); - Tuple [] result = HeldToEnumerable ().ToArray (); + Tuple [] result = HeldToEnumerable ().ToArray (); ResetState (); @@ -522,7 +521,7 @@ internal class AnsiResponseParser : AnsiResponseParserBase } } - private IEnumerable> HeldToEnumerable () { return (IEnumerable>)_heldContent.HeldToObjects (); } + private IEnumerable> HeldToEnumerable () { return (IEnumerable>)_heldContent.HeldToObjects (); } /// /// 'Overload' for specifying an expectation that requires the metadata as well as characters. Has @@ -532,7 +531,7 @@ internal class AnsiResponseParser : AnsiResponseParserBase /// /// /// - public void ExpectResponseT (string? terminator, Action>> response, Action? abandoned, bool persistent) + public void ExpectResponseT (string? terminator, Action>> 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); } diff --git a/Terminal.Gui/Drivers/AnsiHandling/EscSeqUtils/EscSeqUtils.cs b/Terminal.Gui/Drivers/AnsiHandling/EscSeqUtils/EscSeqUtils.cs index 8e4c5e9e8..6257d1d66 100644 --- a/Terminal.Gui/Drivers/AnsiHandling/EscSeqUtils/EscSeqUtils.cs +++ b/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 ( diff --git a/Terminal.Gui/Drivers/AnsiHandling/GenericHeld.cs b/Terminal.Gui/Drivers/AnsiHandling/GenericHeld.cs index 79933472e..6373003fa 100644 --- a/Terminal.Gui/Drivers/AnsiHandling/GenericHeld.cs +++ b/Terminal.Gui/Drivers/AnsiHandling/GenericHeld.cs @@ -2,12 +2,12 @@ namespace Terminal.Gui.Drivers; /// -/// Implementation of for +/// Implementation of for /// -/// -internal class GenericHeld : IHeld +/// +internal class GenericHeld : IHeld { - private readonly List> held = new (); + private readonly List> held = []; public void ClearHeld () { held.Clear (); } @@ -15,7 +15,7 @@ internal class GenericHeld : IHeld public IEnumerable HeldToObjects () { return held; } - public void AddToHeld (object o) { held.Add ((Tuple)o); } + public void AddToHeld (object o) { held.Add ((Tuple)o); } /// public int Length => held.Count; diff --git a/Terminal.Gui/Drivers/ComponentFactory.cs b/Terminal.Gui/Drivers/ComponentFactory.cs deleted file mode 100644 index 3a80fd2e6..000000000 --- a/Terminal.Gui/Drivers/ComponentFactory.cs +++ /dev/null @@ -1,26 +0,0 @@ -#nullable enable -using System.Collections.Concurrent; - -namespace Terminal.Gui.Drivers; - -/// -/// Abstract base class implementation of -/// -/// -public abstract class ComponentFactory : IComponentFactory -{ - /// - public abstract IConsoleInput CreateInput (); - - /// - public abstract IInputProcessor CreateInputProcessor (ConcurrentQueue inputBuffer); - - /// - public virtual IConsoleSizeMonitor CreateConsoleSizeMonitor (IConsoleOutput consoleOutput, IOutputBuffer outputBuffer) - { - return new ConsoleSizeMonitor (consoleOutput, outputBuffer); - } - - /// - public abstract IConsoleOutput CreateOutput (); -} diff --git a/Terminal.Gui/Drivers/ComponentFactoryImpl.cs b/Terminal.Gui/Drivers/ComponentFactoryImpl.cs new file mode 100644 index 000000000..d09099954 --- /dev/null +++ b/Terminal.Gui/Drivers/ComponentFactoryImpl.cs @@ -0,0 +1,25 @@ +#nullable enable +using System.Collections.Concurrent; + +namespace Terminal.Gui.Drivers; + +/// +/// Abstract base class implementation of that provides a default implementation of . +/// The platform specific keyboard input type (e.g. or +public abstract class ComponentFactoryImpl : IComponentFactory where TInputRecord : struct +{ + /// + public abstract IInput CreateInput (); + + /// + public abstract IInputProcessor CreateInputProcessor (ConcurrentQueue inputBuffer); + + /// + public virtual ISizeMonitor CreateSizeMonitor (IOutput consoleOutput, IOutputBuffer outputBuffer) + { + return new SizeMonitorImpl (consoleOutput); + } + + /// + public abstract IOutput CreateOutput (); +} diff --git a/Terminal.Gui/Drivers/ConsoleDriver.cs b/Terminal.Gui/Drivers/ConsoleDriver.cs deleted file mode 100644 index a4c668f74..000000000 --- a/Terminal.Gui/Drivers/ConsoleDriver.cs +++ /dev/null @@ -1,753 +0,0 @@ -#nullable enable - -using System.Diagnostics; - -namespace Terminal.Gui.Drivers; - -/// Base class for Terminal.Gui IConsoleDriver implementations. -/// -/// 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. -/// -public abstract class ConsoleDriver : IConsoleDriver -{ - /// - /// Set this to true in any unit tests that attempt to test drivers other than FakeDriver. - /// - /// public ColorTests () - /// { - /// ConsoleDriver.RunningUnitTests = true; - /// } - /// - /// - internal static bool RunningUnitTests { get; set; } - - /// Get the operating system clipboard. - public IClipboard? Clipboard { get; internal set; } - - /// Returns the name of the driver and relevant library version information. - /// - 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? - /// - /// Provide proper writing to send escape sequence recognized by the . - /// - /// - public abstract void WriteRaw (string ansi); - - #endregion ANSI Esc Sequence Handling - - #region Screen and Contents - - - /// - /// How long after Esc has been pressed before we give up on getting an Ansi escape sequence - /// - 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? - /// Gets the location and size of the terminal screen. - public Rectangle Screen => new (0, 0, Cols, Rows); - - /// - /// Sets the screen size for testing purposes. Only supported by FakeDriver. - /// is the source of truth for screen dimensions. - /// and are read-only and derived from . - /// - /// The new width in columns. - /// The new height in rows. - /// Thrown when called on non-FakeDriver instances. - public virtual void SetScreenSize (int width, int height) - { - throw new NotSupportedException ("SetScreenSize is only supported by FakeDriver for test scenarios."); - } - - private Region? _clip; - - /// - /// Gets or sets the clip rectangle that and are subject - /// to. - /// - /// The rectangle describing the of region. - 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); - } - } - } - - /// - /// Gets the column last set by . and are used by - /// and to determine where to add content. - /// - public int Col { get; private set; } - - /// The number of columns visible in the terminal. - public virtual int Cols - { - get => _cols; - set - { - _cols = value; - ClearContents (); - } - } - - /// - /// The contents of the application output. The driver outputs this buffer to the terminal when - /// is called. - /// The format of the array is rows, columns. The first index is the row, the second index is the column. - /// - public Cell [,]? Contents { get; set; } - - /// The leftmost column in the terminal. - public virtual int Left { get; set; } = 0; - - /// Tests if the specified rune is supported by the driver. - /// - /// - /// if the rune can be properly presented; if the driver does not - /// support displaying this rune. - /// - public virtual bool IsRuneSupported (Rune rune) { return Rune.IsValid (rune.Value); } - - /// Tests whether the specified coordinate are valid for drawing. - /// The column. - /// The row. - /// - /// if the coordinate is outside the screen bounds or outside of . - /// otherwise. - /// - public bool IsValidLocation (int col, int row) { return col >= 0 && row >= 0 && col < Cols && row < Rows && Clip!.Contains (col, row); } - - /// - /// Updates and to the specified column and row in . - /// Used by and to determine where to add content. - /// - /// - /// This does not move the cursor on the screen, it only updates the internal state of the driver. - /// - /// If or are negative or beyond and - /// , the method still sets those properties. - /// - /// - /// Column to move to. - /// Row to move to. - 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; - } - - /// - /// Gets the row last set by . and are used by - /// and to determine where to add content. - /// - public int Row { get; private set; } - - /// The number of rows visible in the terminal. - public virtual int Rows - { - get => _rows; - set - { - _rows = value; - ClearContents (); - } - } - - /// The topmost row in the terminal. - public virtual int Top { get; set; } = 0; - - /// Adds the specified rune to the display at the current cursor position. - /// - /// - /// When the method returns, will be incremented by the number of columns - /// required, even if the new column value is outside of the or screen - /// dimensions defined by . - /// - /// - /// If requires more than one column, and plus the number of columns - /// needed exceeds the or screen dimensions, the default Unicode replacement character (U+FFFD) - /// will be added instead. - /// - /// - /// Rune to add. - 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++; - } - } - - /// - /// Adds the specified to the display at the current cursor position. This method is a - /// convenience method that calls with the constructor. - /// - /// Character to add. - public void AddRune (char c) { AddRune (new Rune (c)); } - - /// Adds the to the display at the cursor position. - /// - /// - /// When the method returns, will be incremented by the number of columns - /// required, unless the new column value is outside of the or screen - /// dimensions defined by . - /// - /// If requires more columns than are available, the output will be clipped. - /// - /// String. - public void AddStr (string str) - { - List runes = str.EnumerateRunes ().ToList (); - - for (var i = 0; i < runes.Count; i++) - { - AddRune (runes [i]); - } - } - - /// Fills the specified rectangle with the specified rune, using - /// - /// The value of is honored. Any parts of the rectangle not in the clip will not be drawn. - /// - /// The Screen-relative rectangle. - /// The Rune used to fill the rectangle - 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; - } - } - } - } - - /// Clears the of the driver. - 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); - } - - /// - /// Raised each time is called. For benchmarking. - /// - public event EventHandler? ClearedContents; - - /// - /// Sets as dirty for situations where views - /// don't need layout and redrawing, but just refresh the screen. - /// - 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; - } - } - } - - /// - /// Fills the specified rectangle with the specified . This method is a convenience method - /// that calls . - /// - /// - /// - public void FillRect (Rectangle rect, char c) { FillRect (rect, new Rune (c)); } - - #endregion Screen and Contents - - #region Cursor Handling - - /// Gets the terminal cursor visibility. - /// The current - /// upon success - public abstract bool GetCursorVisibility (out CursorVisibility visibility); - - /// Tests whether the specified coordinate are valid for drawing the specified Rune. - /// Used to determine if one or two columns are required. - /// The column. - /// The row. - /// - /// if the coordinate is outside the screen bounds or outside of . - /// otherwise. - /// - 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); - } - } - - /// - /// Called when the terminal screen changes (size, position, etc.). Fires the event. - /// reflects the source of truth for screen dimensions. - /// and are derived from and are read-only. - /// - /// Event arguments containing the new screen size. - public void OnSizeChanged (SizeChangedEventArgs args) - { - SizeChanged?.Invoke (this, args); - } - - - /// Updates the screen to reflect all the changes that have been done to the display buffer - public void Refresh () - { - bool updated = UpdateScreen (); - UpdateCursor (); - - Refreshed?.Invoke (this, new EventArgs (in updated)); - } - - /// - /// Raised each time is called. For benchmarking. - /// - public event EventHandler>? Refreshed; - - /// Sets the terminal cursor visibility. - /// The wished - /// upon success - public abstract bool SetCursorVisibility (CursorVisibility visibility); - - /// - /// The event fired when the screen changes (size, position, etc.). - /// is the source of truth for screen dimensions. - /// and are read-only and derived from . - /// - public event EventHandler? SizeChanged; - - #endregion Cursor Handling - - /// Suspends the application (e.g. on Linux via SIGTSTP) and upon resume, resets the console driver. - /// This is only implemented in the Unix driver. - public abstract void Suspend (); - - /// Sets the position of the terminal cursor to and . - public abstract void UpdateCursor (); - - /// Redraws the physical screen with the contents that have been queued up via any of the printing commands. - /// if any updates to the screen were made. - public abstract bool UpdateScreen (); - - #region Setup & Teardown - - /// Initializes the driver - public abstract void Init (); - - /// Ends the execution of the console driver. - public abstract void End (); - - #endregion - - #region Color Handling - - /// Gets whether the supports TrueColor output. - 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 - /// - /// Gets or sets whether the should use 16 colors instead of the default TrueColors. - /// See to change this setting via . - /// - /// - /// - /// Will be forced to if is - /// , indicating that the cannot support TrueColor. - /// - /// - public virtual bool Force16Colors - { - get => Application.Force16Colors || !SupportsTrueColor; - set => Application.Force16Colors = value || !SupportsTrueColor; - } - - private int _cols; - private int _rows; - - /// - /// The that will be used for the next or - /// call. - /// - public Attribute CurrentAttribute { get; set; } - - /// Selects the specified attribute as the attribute to use for future calls to AddRune and AddString. - /// Implementations should call base.SetAttribute(c). - /// C. - public Attribute SetAttribute (Attribute c) - { - Attribute prevAttribute = CurrentAttribute; - CurrentAttribute = c; - - return prevAttribute; - } - - /// Gets the current . - /// The current attribute. - public Attribute GetAttribute () { return CurrentAttribute; } - - #endregion Color Handling - - #region Mouse Handling - - /// Event fired when a mouse event occurs. - public event EventHandler? MouseEvent; - - /// Called when a mouse event occurs. Fires the event. - /// - public void OnMouseEvent (MouseEventArgs a) - { - // Ensure ScreenPosition is set - a.ScreenPosition = a.Position; - - MouseEvent?.Invoke (this, a); - } - - #endregion Mouse Handling - - #region Keyboard Handling - - /// Event fired when a key is pressed down. This is a precursor to . - public event EventHandler? KeyDown; - - /// - /// Called when a key is pressed down. Fires the event. This is a precursor to - /// . - /// - /// - public void OnKeyDown (Key a) { KeyDown?.Invoke (this, a); } - - /// Event fired when a key is released. - /// - /// Drivers that do not support key release events will fire this event after processing is - /// complete. - /// - public event EventHandler? KeyUp; - - /// Called when a key is released. Fires the event. - /// - /// Drivers that do not support key release events will call this method after processing - /// is complete. - /// - /// - 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; - - /// - /// Queues the given for execution - /// - /// - public void QueueAnsiRequest (AnsiEscapeSequenceRequest request) - { - GetRequestScheduler ().SendOrSchedule (request); - } - - internal abstract IAnsiResponseParser GetParser (); - - /// - /// Gets the for this . - /// - /// - public AnsiRequestScheduler GetRequestScheduler () - { - // Lazy initialization because GetParser is virtual - return _scheduler ??= new (GetParser ()); - } - -} \ No newline at end of file diff --git a/Terminal.Gui/Drivers/ConsoleInput.cs b/Terminal.Gui/Drivers/ConsoleInput.cs deleted file mode 100644 index 981f2de9e..000000000 --- a/Terminal.Gui/Drivers/ConsoleInput.cs +++ /dev/null @@ -1,79 +0,0 @@ -#nullable enable -using System.Collections.Concurrent; - -namespace Terminal.Gui.Drivers; - -/// -/// Base class for reading console input in perpetual loop -/// -/// -public abstract class ConsoleInput : IConsoleInput -{ - private ConcurrentQueue? _inputBuffer; - - /// - /// Determines how to get the current system type, adjust - /// in unit tests to simulate specific timings. - /// - public Func Now { get; set; } = () => DateTime.Now; - - /// - public virtual void Dispose () { } - - /// - public void Initialize (ConcurrentQueue inputBuffer) { _inputBuffer = inputBuffer; } - - /// - 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) - { } - } - - /// - /// When implemented in a derived class, returns true if there is data available - /// to read from console. - /// - /// - protected abstract bool Peek (); - - /// - /// Returns the available data without blocking, called when - /// returns . - /// - /// - protected abstract IEnumerable Read (); -} diff --git a/Terminal.Gui/Drivers/ConsoleKeyInfoExtensions.cs b/Terminal.Gui/Drivers/ConsoleKeyInfoExtensions.cs new file mode 100644 index 000000000..2ff535618 --- /dev/null +++ b/Terminal.Gui/Drivers/ConsoleKeyInfoExtensions.cs @@ -0,0 +1,95 @@ +namespace Terminal.Gui.Drivers; + +/// +/// Extension methods for . +/// +public static class ConsoleKeyInfoExtensions +{ + /// + /// Returns a string representation of the suitable for debugging and logging. + /// + /// The ConsoleKeyInfo to convert to string. + /// A formatted string showing the key, character, and modifiers. + /// + /// + /// Examples: + /// + /// Key: A ('a') - lowercase 'a' pressed + /// Key: A ('A'), Modifiers: Shift - uppercase 'A' pressed + /// Key: A (\0), Modifiers: Control - Ctrl+A (no printable char) + /// Key: Enter (0x000D) - Enter key (carriage return) + /// Key: F5 (\0) - F5 function key + /// Key: D2 ('@'), Modifiers: Shift - Shift+2 on US keyboard + /// Key: None ('') - Accented character + /// Key: CursorUp (\0), Modifiers: Shift | Control - Ctrl+Shift+Up Arrow + /// + /// + /// + 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 (); + } +} diff --git a/Terminal.Gui/Drivers/ConsoleKeyMapping.cs b/Terminal.Gui/Drivers/ConsoleKeyMapping.cs index ca8fd6d6f..6a1c4b5ba 100644 --- a/Terminal.Gui/Drivers/ConsoleKeyMapping.cs +++ b/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? -/// Helper class to handle the scan code and virtual key from a . +/// Helper class to handle mapping between and . public static class ConsoleKeyMapping { -#if !WT_ISSUE_8871_FIXED // https://github.com/microsoft/terminal/issues/8871 /// - /// 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 from a . /// - /// - /// - /// 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. - /// - /// - /// - /// 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. - /// - [DllImport ("user32.dll", EntryPoint = "MapVirtualKeyExW", CharSet = CharSet.Unicode)] - private static extern uint MapVirtualKeyEx (VK vk, uint uMapType, nint dwhkl); - - /// Retrieves the active input locale identifier (formerly called the keyboard layout). - /// 0 for current thread - /// - /// 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. - /// - [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); - - /// - /// Translates the specified virtual-key code and keyboard state to the corresponding Unicode character or - /// characters using the Win32 API MapVirtualKey. - /// - /// - /// - /// 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. - /// - 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 - /// - /// Translates (maps) a virtual-key code into a scan code or character value, or translates a scan code into a virtual-key code. - /// - /// - /// - /// 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. - /// - /// 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. - [DllImport ("user32.dll", EntryPoint = "MapVirtualKeyW", CharSet = CharSet.Unicode)] - extern static uint MapVirtualKey (VK vk, uint uMapType = 2); - - uint MapVKtoChar (VK vk) => MapVirtualKeyToCharEx (vk); -#endif - /// - /// Retrieves the name of the active input locale identifier (formerly called the keyboard layout) for the calling - /// thread. - /// - /// - /// - [DllImport ("user32.dll")] - private static extern bool GetKeyboardLayoutName ([Out] StringBuilder pwszKLID); - - /// - /// Retrieves the name of the active input locale identifier (formerly called the keyboard layout) for the calling - /// thread. - /// - /// - 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 - { - 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 - /// Get the scan code from a . - /// The console key info. - /// The value if apply. - 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 - /// Gets the from the provided . - /// The key code. - /// The console key info. + /// The key code to convert. + /// A ConsoleKeyInfo representing the key. + /// + /// This method is primarily used for test simulation via . + /// 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. + /// public static ConsoleKeyInfo GetConsoleKeyInfoFromKeyCode (KeyCode key) { ConsoleModifiers modifiers = MapToConsoleModifiers (key); - uint keyValue = MapKeyCodeToConsoleKey (key, out bool isConsoleKey); + KeyCode keyWithoutModifiers = key & ~KeyCode.CtrlMask & ~KeyCode.ShiftMask & ~KeyCode.AltMask; - if (isConsoleKey) - { - ConsoleModifiers mod = GetModifiers (modifiers); - ScanCodeMapping scode = GetScanCode ("VirtualKey", keyValue, mod); + // Map to ConsoleKey enum + (ConsoleKey consoleKey, char keyChar) = MapToConsoleKeyAndChar (keyWithoutModifiers, modifiers); - 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) - ); - } - - /// Map existing modifiers to . - /// The key code. - /// The console modifiers. - 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; + return new ( + keyChar, + consoleKey, + modifiers.HasFlag (ConsoleModifiers.Shift), + modifiers.HasFlag (ConsoleModifiers.Alt), + modifiers.HasFlag (ConsoleModifiers.Control) + ); } /// Gets from modifiers. @@ -295,450 +58,6 @@ public static class ConsoleKeyMapping return modifiers; } - /// - /// Get the from a unicode character and modifiers (e.g. (Key)'a' and - /// (Key)Key.CtrlMask). - /// - /// The key as a unicode codepoint. - /// The modifier keys. - /// The resulting scan code. - /// The . - 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; - } - - /// - /// Get the output character from the , with the correct - /// and the scan code used on Windows. - /// - /// The unicode character. - /// The modifiers keys. - /// The resulting console key. - /// The resulting scan code. - /// Indicates if the is a . - /// The output character or the . - /// This is only used by the and by unit tests. - 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; - } - - /// Maps a unicode character (e.g. (Key)'a') to a uint representing a . - /// The key value. - /// - /// Indicates if the is a . - /// means the return value is in the ConsoleKey enum. means the return - /// value can be mapped to a valid unicode character. - /// - /// The or the . - /// This is only used by the and by unit tests. - 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; - } - /// Maps a to a . /// The console key. /// The or the . @@ -922,6 +241,34 @@ public static class ConsoleKeyMapping return keyCode; } + /// Map existing modifiers to . + /// The key code. + /// The console modifiers. + 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; + } + /// Maps a to a . /// The console modifiers. /// The key code. @@ -948,1636 +295,92 @@ public static class ConsoleKeyMapping return keyMod != KeyCode.Null ? keyMod | key : key; } - /// Generated from winuser.h. See https://learn.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes - public enum VK : ushort - { - /// Left mouse button. - LBUTTON = 0x01, - - /// Right mouse button. - RBUTTON = 0x02, - - /// Control-break processing. - CANCEL = 0x03, - - /// Middle mouse button (three-button mouse). - MBUTTON = 0x04, - - /// X1 mouse button. - XBUTTON1 = 0x05, - - /// X2 mouse button. - XBUTTON2 = 0x06, - - /// BACKSPACE key. - BACK = 0x08, - - /// TAB key. - TAB = 0x09, - - /// CLEAR key. - CLEAR = 0x0C, - - /// ENTER key. - RETURN = 0x0D, - - /// SHIFT key. - SHIFT = 0x10, - - /// CTRL key. - CONTROL = 0x11, - - /// ALT key. - MENU = 0x12, - - /// PAUSE key. - PAUSE = 0x13, - - /// CAPS LOCK key. - CAPITAL = 0x14, - - /// IME Kana mode. - KANA = 0x15, - - /// IME Hangul mode. - HANGUL = 0x15, - - /// IME Junja mode. - JUNJA = 0x17, - - /// IME final mode. - FINAL = 0x18, - - /// IME Hanja mode. - HANJA = 0x19, - - /// IME Kanji mode. - KANJI = 0x19, - - /// ESC key. - ESCAPE = 0x1B, - - /// IME convert. - CONVERT = 0x1C, - - /// IME nonconvert. - NONCONVERT = 0x1D, - - /// IME accept. - ACCEPT = 0x1E, - - /// IME mode change request. - MODECHANGE = 0x1F, - - /// SPACEBAR. - SPACE = 0x20, - - /// PAGE UP key. - PRIOR = 0x21, - - /// PAGE DOWN key. - NEXT = 0x22, - - /// END key. - END = 0x23, - - /// HOME key. - HOME = 0x24, - - /// LEFT ARROW key. - LEFT = 0x25, - - /// UP ARROW key. - UP = 0x26, - - /// RIGHT ARROW key. - RIGHT = 0x27, - - /// DOWN ARROW key. - DOWN = 0x28, - - /// SELECT key. - SELECT = 0x29, - - /// PRINT key. - PRINT = 0x2A, - - /// EXECUTE key - EXECUTE = 0x2B, - - /// PRINT SCREEN key - SNAPSHOT = 0x2C, - - /// INS key - INSERT = 0x2D, - - /// DEL key - DELETE = 0x2E, - - /// HELP key - HELP = 0x2F, - - /// Left Windows key (Natural keyboard) - LWIN = 0x5B, - - /// Right Windows key (Natural keyboard) - RWIN = 0x5C, - - /// Applications key (Natural keyboard) - APPS = 0x5D, - - /// Computer Sleep key - SLEEP = 0x5F, - - /// Numeric keypad 0 key - NUMPAD0 = 0x60, - - /// Numeric keypad 1 key - NUMPAD1 = 0x61, - - /// Numeric keypad 2 key - NUMPAD2 = 0x62, - - /// Numeric keypad 3 key - NUMPAD3 = 0x63, - - /// Numeric keypad 4 key - NUMPAD4 = 0x64, - - /// Numeric keypad 5 key - NUMPAD5 = 0x65, - - /// Numeric keypad 6 key - NUMPAD6 = 0x66, - - /// Numeric keypad 7 key - NUMPAD7 = 0x67, - - /// Numeric keypad 8 key - NUMPAD8 = 0x68, - - /// Numeric keypad 9 key - NUMPAD9 = 0x69, - - /// Multiply key - MULTIPLY = 0x6A, - - /// Add key - ADD = 0x6B, - - /// Separator key - SEPARATOR = 0x6C, - - /// Subtract key - SUBTRACT = 0x6D, - - /// Decimal key - DECIMAL = 0x6E, - - /// Divide key - DIVIDE = 0x6F, - - /// F1 key - F1 = 0x70, - - /// F2 key - F2 = 0x71, - - /// F3 key - F3 = 0x72, - - /// F4 key - F4 = 0x73, - - /// F5 key - F5 = 0x74, - - /// F6 key - F6 = 0x75, - - /// F7 key - F7 = 0x76, - - /// F8 key - F8 = 0x77, - - /// F9 key - F9 = 0x78, - - /// F10 key - F10 = 0x79, - - /// F11 key - F11 = 0x7A, - - /// F12 key - F12 = 0x7B, - - /// F13 key - F13 = 0x7C, - - /// F14 key - F14 = 0x7D, - - /// F15 key - F15 = 0x7E, - - /// F16 key - F16 = 0x7F, - - /// F17 key - F17 = 0x80, - - /// F18 key - F18 = 0x81, - - /// F19 key - F19 = 0x82, - - /// F20 key - F20 = 0x83, - - /// F21 key - F21 = 0x84, - - /// F22 key - F22 = 0x85, - - /// F23 key - F23 = 0x86, - - /// F24 key - F24 = 0x87, - - /// NUM LOCK key - NUMLOCK = 0x90, - - /// SCROLL LOCK key - SCROLL = 0x91, - - /// NEC PC-9800 kbd definition: '=' key on numpad - OEM_NEC_EQUAL = 0x92, - - /// Fujitsu/OASYS kbd definition: 'Dictionary' key - OEM_FJ_JISHO = 0x92, - - /// Fujitsu/OASYS kbd definition: 'Unregister word' key - OEM_FJ_MASSHOU = 0x93, - - /// Fujitsu/OASYS kbd definition: 'Register word' key - OEM_FJ_TOUROKU = 0x94, - - /// Fujitsu/OASYS kbd definition: 'Left OYAYUBI' key - OEM_FJ_LOYA = 0x95, - - /// Fujitsu/OASYS kbd definition: 'Right OYAYUBI' key - OEM_FJ_ROYA = 0x96, - - /// Left SHIFT key - LSHIFT = 0xA0, - - /// Right SHIFT key - RSHIFT = 0xA1, - - /// Left CONTROL key - LCONTROL = 0xA2, - - /// Right CONTROL key - RCONTROL = 0xA3, - - /// Left MENU key (Left Alt key) - LMENU = 0xA4, - - /// Right MENU key (Right Alt key) - RMENU = 0xA5, - - /// Browser Back key - BROWSER_BACK = 0xA6, - - /// Browser Forward key - BROWSER_FORWARD = 0xA7, - - /// Browser Refresh key - BROWSER_REFRESH = 0xA8, - - /// Browser Stop key - BROWSER_STOP = 0xA9, - - /// Browser Search key - BROWSER_SEARCH = 0xAA, - - /// Browser Favorites key - BROWSER_FAVORITES = 0xAB, - - /// Browser Home key - BROWSER_HOME = 0xAC, - - /// Volume Mute key - VOLUME_MUTE = 0xAD, - - /// Volume Down key - VOLUME_DOWN = 0xAE, - - /// Volume Up key - VOLUME_UP = 0xAF, - - /// Next Track key - MEDIA_NEXT_TRACK = 0xB0, - - /// Previous Track key - MEDIA_PREV_TRACK = 0xB1, - - /// Stop Media key - MEDIA_STOP = 0xB2, - - /// Play/Pause Media key - MEDIA_PLAY_PAUSE = 0xB3, - - /// Start Mail key - LAUNCH_MAIL = 0xB4, - - /// Select Media key - LAUNCH_MEDIA_SELECT = 0xB5, - - /// Start Application 1 key - LAUNCH_APP1 = 0xB6, - - /// Start Application 2 key - LAUNCH_APP2 = 0xB7, - - /// Used for miscellaneous characters; it can vary by keyboard. For the US standard keyboard, the ';:' key - OEM_1 = 0xBA, - - /// For any country/region, the '+' key - OEM_PLUS = 0xBB, - - /// For any country/region, the ',' key - OEM_COMMA = 0xBC, - - /// For any country/region, the '-' key - OEM_MINUS = 0xBD, - - /// For any country/region, the '.' key - OEM_PERIOD = 0xBE, - - /// Used for miscellaneous characters; it can vary by keyboard. For the US standard keyboard, the '/?' key - OEM_2 = 0xBF, - - /// Used for miscellaneous characters; it can vary by keyboard. For the US standard keyboard, the '`~' key - OEM_3 = 0xC0, - - /// Used for miscellaneous characters; it can vary by keyboard. For the US standard keyboard, the '[{' key - OEM_4 = 0xDB, - - /// Used for miscellaneous characters; it can vary by keyboard. For the US standard keyboard, the '\|' key - OEM_5 = 0xDC, - - /// Used for miscellaneous characters; it can vary by keyboard. For the US standard keyboard, the ']}' key - OEM_6 = 0xDD, - - /// - /// Used for miscellaneous characters; it can vary by keyboard. For the US standard keyboard, the - /// 'single-quote/double-quote' key - /// - OEM_7 = 0xDE, - - /// Used for miscellaneous characters; it can vary by keyboard. - OEM_8 = 0xDF, - - /// 'AX' key on Japanese AX kbd - OEM_AX = 0xE1, - - /// Either the angle bracket key or the backslash key on the RT 102-key keyboard - OEM_102 = 0xE2, - - /// Help key on ICO - ICO_HELP = 0xE3, - - /// 00 key on ICO - ICO_00 = 0xE4, - - /// Process key - PROCESSKEY = 0xE5, - - /// Clear key on ICO - ICO_CLEAR = 0xE6, - - /// Packet key to be used to pass Unicode characters as if they were keystrokes - PACKET = 0xE7, - - /// Reset key - OEM_RESET = 0xE9, - - /// Jump key - OEM_JUMP = 0xEA, - - /// PA1 key - OEM_PA1 = 0xEB, - - /// PA2 key - OEM_PA2 = 0xEC, - - /// PA3 key - OEM_PA3 = 0xED, - - /// WsCtrl key - OEM_WSCTRL = 0xEE, - - /// CuSel key - OEM_CUSEL = 0xEF, - - /// Attn key - OEM_ATTN = 0xF0, - - /// Finish key - OEM_FINISH = 0xF1, - - /// Copy key - OEM_COPY = 0xF2, - - /// Auto key - OEM_AUTO = 0xF3, - - /// Enlw key - OEM_ENLW = 0xF4, - - /// BackTab key - OEM_BACKTAB = 0xF5, - - /// Attn key - ATTN = 0xF6, - - /// CrSel key - CRSEL = 0xF7, - - /// ExSel key - EXSEL = 0xF8, - - /// Erase EOF key - EREOF = 0xF9, - - /// Play key - PLAY = 0xFA, - - /// Zoom key - ZOOM = 0xFB, - - /// Reserved - NONAME = 0xFC, - - /// PA1 key - PA1 = 0xFD, - - /// Clear key - OEM_CLEAR = 0xFE - } - - // 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 _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' - ) - }; - - /// Decode a that is using . - /// The console key info. - /// The decoded or the . - /// - /// If it's a the may be a - /// or a value. - /// - public static ConsoleKeyInfo DecodeVKPacketToKConsoleKeyInfo (ConsoleKeyInfo consoleKeyInfo) - { - if (consoleKeyInfo.Key != ConsoleKey.Packet) - { - return consoleKeyInfo; - } - - return GetConsoleKeyInfoFromKeyChar (consoleKeyInfo.KeyChar, consoleKeyInfo.Modifiers, out _); - } - /// - /// Encode the with the if the first a byte - /// length, otherwise only the KeyChar is considered and searched on the database. + /// Maps a KeyCode to its corresponding ConsoleKey and character representation. /// - /// The console key info. - /// The encoded KeyChar with the Key if both can be shifted, otherwise only the KeyChar. - /// This is useful to use with the . - public static char EncodeKeyCharForVKPacket (ConsoleKeyInfo consoleKeyInfo) + private static (ConsoleKey consoleKey, char keyChar) MapToConsoleKeyAndChar (KeyCode key, ConsoleModifiers modifiers) { - char keyChar = consoleKeyInfo.KeyChar; - ConsoleKey consoleKey = consoleKeyInfo.Key; + var keyValue = (uint)key; - if (keyChar != 0 && consoleKeyInfo.KeyChar < byte.MaxValue && consoleKey == ConsoleKey.None) + // Check if this is a special key (value > MaxCodePoint means it's offset by MaxCodePoint) + if (keyValue > (uint)KeyCode.MaxCodePoint) { - // try to get the ConsoleKey - ScanCodeMapping scode = _scanCodes.FirstOrDefault (e => e.UnicodeChar == keyChar); + var specialKey = (ConsoleKey)(keyValue - (uint)KeyCode.MaxCodePoint); - if (scode is { }) + // 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 + }; + + return (specialKey, specialChar); + } + + // 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)); + + if (modifiers.HasFlag (ConsoleModifiers.Shift)) { - consoleKey = (ConsoleKey)scode.VirtualKey; + letterChar = char.ToUpper (letterChar); } + + return (letterKey, letterChar); } - if (keyChar < byte.MaxValue && consoleKey != ConsoleKey.None) + // Handle number keys (D0-D9) with US keyboard layout + if (keyValue >= (uint)KeyCode.D0 && keyValue <= (uint)KeyCode.D9) { - keyChar = (char)((consoleKeyInfo.KeyChar << 8) | (byte)consoleKey); + var numberKey = (ConsoleKey)keyValue; + char numberChar; + + 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)); + } + + return (numberKey, numberChar); } - return keyChar; + // Handle other standard keys + var standardKey = (ConsoleKey)keyValue; + + if (Enum.IsDefined (typeof (ConsoleKey), (int)keyValue)) + { + 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 + }; + + return (standardKey, standardChar); + } + + // For printable Unicode characters, return character with ConsoleKey.None + if (keyValue <= 0x10FFFF && !char.IsControl ((char)keyValue)) + { + return (ConsoleKey.None, (char)keyValue); + } + + // Fallback + return (ConsoleKey.None, (char)keyValue); } } diff --git a/Terminal.Gui/Drivers/DotNetDriver/INetInput.cs b/Terminal.Gui/Drivers/DotNetDriver/INetInput.cs index 74f7e495b..672a32209 100644 --- a/Terminal.Gui/Drivers/DotNetDriver/INetInput.cs +++ b/Terminal.Gui/Drivers/DotNetDriver/INetInput.cs @@ -1,4 +1,7 @@ namespace Terminal.Gui.Drivers; -internal interface INetInput : IConsoleInput +/// +/// Wraps IConsoleInput for .NET console input events (ConsoleKeyInfo). Needed to support Mocking in tests. +/// +internal interface INetInput : IInput { } diff --git a/Terminal.Gui/Drivers/DotNetDriver/NetComponentFactory.cs b/Terminal.Gui/Drivers/DotNetDriver/NetComponentFactory.cs index 024169f61..669c6efce 100644 --- a/Terminal.Gui/Drivers/DotNetDriver/NetComponentFactory.cs +++ b/Terminal.Gui/Drivers/DotNetDriver/NetComponentFactory.cs @@ -4,26 +4,17 @@ using System.Collections.Concurrent; namespace Terminal.Gui.Drivers; /// -/// implementation for native csharp console I/O i.e. dotnet. -/// This factory creates instances of internal classes , etc. +/// implementation for native csharp console I/O i.e. dotnet. +/// This factory creates instances of internal classes , etc. /// -public class NetComponentFactory : ComponentFactory +public class NetComponentFactory : ComponentFactoryImpl { /// - public override IConsoleInput CreateInput () - { - return new NetInput (); - } + public override IInput CreateInput () { return new NetInput (); } - /// - public override IConsoleOutput CreateOutput () - { - return new NetOutput (); - } + /// + public override IInputProcessor CreateInputProcessor (ConcurrentQueue inputBuffer) { return new NetInputProcessor (inputBuffer); } - /// - public override IInputProcessor CreateInputProcessor (ConcurrentQueue inputBuffer) - { - return new NetInputProcessor (inputBuffer); - } + /// + public override IOutput CreateOutput () { return new NetOutput (); } } diff --git a/Terminal.Gui/Drivers/DotNetDriver/NetInput.cs b/Terminal.Gui/Drivers/DotNetDriver/NetInput.cs index c9a0e5b27..b44c59522 100644 --- a/Terminal.Gui/Drivers/DotNetDriver/NetInput.cs +++ b/Terminal.Gui/Drivers/DotNetDriver/NetInput.cs @@ -3,12 +3,12 @@ namespace Terminal.Gui.Drivers; /// -/// Console input implementation that uses native dotnet methods e.g. . +/// implementation that uses native dotnet methods e.g. . +/// The and methods are executed +/// on the input thread created by . /// -public class NetInput : ConsoleInput, INetInput +public class NetInput : InputImpl, ITestableInput, IDisposable { - private readonly NetWinVTConsole _adjustConsole; - /// /// 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, INetInput /// 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, 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); - - //Set cursor key to application. - Console.Out.Write (EscSeqUtils.CSI_HideCursor); - - Console.Out.Write (EscSeqUtils.CSI_EnableMouseEvents); - Console.TreatControlCAsInput = true; - } - - /// - protected override bool Peek () - { - if (ConsoleDriver.RunningUnitTests) + try { - return false; + //Enable alternative screen buffer. + Console.Out.Write (EscSeqUtils.CSI_SaveCursorAndActivateAltBufferNoBackscroll); + + //Set cursor key to application. + Console.Out.Write (EscSeqUtils.CSI_HideCursor); + + Console.Out.Write (EscSeqUtils.CSI_EnableMouseEvents); + Console.TreatControlCAsInput = true; } - - return Console.KeyAvailable; - } - - /// - protected override IEnumerable Read () - { - while (Console.KeyAvailable) + catch { - yield return Console.ReadKey (true); + // Swallow any exceptions during initialization for unit tests } } - private void FlushConsoleInput () - { - if (!ConsoleDriver.RunningUnitTests) - { - while (Console.KeyAvailable) - { - Console.ReadKey (intercept: true); - } - } - } + private readonly NetWinVTConsole _adjustConsole; /// public override void Dispose () { base.Dispose (); - if (ConsoleDriver.RunningUnitTests) + try { - return; + // 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 (); } + catch + { + // Swallow any exceptions during Dispose for unit tests + } + } - // Disable mouse events first - Console.Out.Write (EscSeqUtils.CSI_DisableMouseEvents); + /// + public void AddInput (ConsoleKeyInfo input) { throw new NotImplementedException (); } - //Disable alternative screen buffer. - Console.Out.Write (EscSeqUtils.CSI_RestoreCursorAndRestoreAltBufferWithBackscroll); + /// + public override bool Peek () + { + try + { + return Console.KeyAvailable; + } + catch + { + return false; + } + } - //Set cursor key to cursor. - Console.Out.Write (EscSeqUtils.CSI_ShowCursor); + /// + public override IEnumerable Read () + { + while (true) + { + ConsoleKeyInfo keyInfo = default; - _adjustConsole?.Cleanup (); + try + { + if (!Console.KeyAvailable) + { + break; + } - // Flush any pending input so no stray events appear - FlushConsoleInput (); + keyInfo = Console.ReadKey (true); + } + catch (InvalidOperationException) + { + // Not connected to a terminal (GitHub Actions, redirected input, etc.) + yield break; + } + catch (IOException) + { + // I/O error reading from console + yield break; + } + + yield return keyInfo; + } + } + + private void FlushConsoleInput () + { + while (Console.KeyAvailable) + { + Console.ReadKey (true); + } } } diff --git a/Terminal.Gui/Drivers/DotNetDriver/NetInputProcessor.cs b/Terminal.Gui/Drivers/DotNetDriver/NetInputProcessor.cs index 5f8a2971b..a64952b2c 100644 --- a/Terminal.Gui/Drivers/DotNetDriver/NetInputProcessor.cs +++ b/Terminal.Gui/Drivers/DotNetDriver/NetInputProcessor.cs @@ -5,20 +5,8 @@ namespace Terminal.Gui.Drivers; /// /// Input processor for , deals in stream /// -public class NetInputProcessor : InputProcessor +public class NetInputProcessor : InputProcessorImpl { -#pragma warning disable CA2211 - /// - /// Set to true to generate code in (verbose only) for test cases in NetInputProcessorTests. - /// - /// 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. - /// - /// - public static bool GenerateTestCasesForKeyPresses = false; -#pragma warning restore CA2211 - /// public NetInputProcessor (ConcurrentQueue inputBuffer) : base (inputBuffer, new NetKeyConverter ()) { @@ -26,41 +14,11 @@ public class NetInputProcessor : InputProcessor } /// - protected override void Process (ConsoleKeyInfo consoleKeyInfo) + protected override void Process (ConsoleKeyInfo input) { - // For building test cases - if (GenerateTestCasesForKeyPresses) - { - Logging.Trace (FormatConsoleKeyInfoForTestCase (consoleKeyInfo)); - } - - foreach (Tuple released in Parser.ProcessInput (Tuple.Create (consoleKeyInfo.KeyChar, consoleKeyInfo))) + foreach (Tuple released in Parser.ProcessInput (Tuple.Create (input.KeyChar, input))) { ProcessAfterParsing (released.Item2); } } - - /// - 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 ()}),"; - } } diff --git a/Terminal.Gui/Drivers/DotNetDriver/NetKeyConverter.cs b/Terminal.Gui/Drivers/DotNetDriver/NetKeyConverter.cs index 198875463..e43ad3ecc 100644 --- a/Terminal.Gui/Drivers/DotNetDriver/NetKeyConverter.cs +++ b/Terminal.Gui/Drivers/DotNetDriver/NetKeyConverter.cs @@ -1,5 +1,4 @@ - -namespace Terminal.Gui.Drivers; +namespace Terminal.Gui.Drivers; /// /// capable of converting the @@ -23,4 +22,7 @@ internal class NetKeyConverter : IKeyConverter return EscSeqUtils.MapKey (adjustedInput); } + + /// + public ConsoleKeyInfo ToKeyInfo (Key key) { return ConsoleKeyMapping.GetConsoleKeyInfoFromKeyCode (key.KeyCode); } } diff --git a/Terminal.Gui/Drivers/DotNetDriver/NetOutput.cs b/Terminal.Gui/Drivers/DotNetDriver/NetOutput.cs index a97ec407b..8fbd11ba1 100644 --- a/Terminal.Gui/Drivers/DotNetDriver/NetOutput.cs +++ b/Terminal.Gui/Drivers/DotNetDriver/NetOutput.cs @@ -3,10 +3,10 @@ namespace Terminal.Gui.Drivers; /// -/// Implementation of that uses native dotnet +/// Implementation of that uses native dotnet /// methods e.g. /// -public class NetOutput : OutputBase, IConsoleOutput +public class NetOutput : OutputBase, IOutput { private readonly bool _isWinPlatform; @@ -15,9 +15,16 @@ public class NetOutput : OutputBase, IConsoleOutput /// 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 /// public void Write (ReadOnlySpan text) { - Console.Out.Write (text); + try + { + Console.Out.Write (text); + } + catch (IOException) + { + // Not connected to a terminal; do nothing + } } /// 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); + /// + public Point GetCursorPosition () + { + return _lastCursorPosition ?? Point.Empty; } /// @@ -88,7 +111,14 @@ public class NetOutput : OutputBase, IConsoleOutput /// protected override void Write (StringBuilder output) { - Console.Out.Write (output); + try + { + Console.Out.Write (output); + } + catch (IOException) + { + // Not connected to a terminal; do nothing + } } /// @@ -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; - /// + /// 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); + } + else + { + Write (EscSeqUtils.CSI_HideCursor); } - - Write (EscSeqUtils.CSI_ShowCursor); } - else + catch { - Write (EscSeqUtils.CSI_HideCursor); + // Ignore any exceptions } } } diff --git a/Terminal.Gui/Drivers/DotNetDriver/NetWinVTConsole.cs b/Terminal.Gui/Drivers/DotNetDriver/NetWinVTConsole.cs index 873fba47d..d6730e044 100644 --- a/Terminal.Gui/Drivers/DotNetDriver/NetWinVTConsole.cs +++ b/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)) diff --git a/Terminal.Gui/Drivers/ConsoleDriverFacade.cs b/Terminal.Gui/Drivers/DriverImpl.cs similarity index 58% rename from Terminal.Gui/Drivers/ConsoleDriverFacade.cs rename to Terminal.Gui/Drivers/DriverImpl.cs index cc7a1c118..db1918459 100644 --- a/Terminal.Gui/Drivers/ConsoleDriverFacade.cs +++ b/Terminal.Gui/Drivers/DriverImpl.cs @@ -3,69 +3,108 @@ using System.Runtime.InteropServices; namespace Terminal.Gui.Drivers; -internal class ConsoleDriverFacade : IConsoleDriver, IConsoleDriverFacade +/// +/// Provides the main implementation of the driver abstraction layer for Terminal.Gui. +/// This implementation of coordinates the interaction between input processing, output +/// rendering, +/// screen size monitoring, and ANSI escape sequence handling. +/// +/// +/// +/// implements , +/// serving as the central coordination point for console I/O operations. It delegates functionality +/// to specialized components: +/// +/// +/// - Processes keyboard and mouse input +/// - Manages the screen buffer state +/// - Handles actual console output rendering +/// - Manages ANSI escape sequence requests +/// - Monitors terminal size changes +/// +/// +/// This class is internal and should not be used directly by application code. +/// Applications interact with drivers through the class. +/// +/// +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; /// - /// The event fired when the screen changes (size, position, etc.). + /// Initializes a new instance of the class. /// - public event EventHandler? SizeChanged; - - public IInputProcessor InputProcessor { get; } - public IOutputBuffer OutputBuffer => _outputBuffer; - - public IConsoleSizeMonitor ConsoleSizeMonitor { get; } - - - public ConsoleDriverFacade ( + /// The input processor for handling keyboard and mouse events. + /// The output buffer for managing screen state. + /// The output interface for rendering to the console. + /// The scheduler for managing ANSI escape sequence requests. + /// The monitor for tracking terminal size changes. + 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 (); } + /// + /// The event fired when the screen changes (size, position, etc.). + /// + public event EventHandler? SizeChanged; + + /// + public IInputProcessor InputProcessor { get; } + + /// + public IOutputBuffer OutputBuffer { get; } + + /// + 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 : IConsoleDriver, IConsoleDriverFacade { Clipboard = new WSLClipboard (); } - else - { - Clipboard = new FakeClipboard (); - } + + // Clipboard is set to FakeClipboard at initialization } /// Gets the location and size of the terminal screen. @@ -88,27 +125,27 @@ internal class ConsoleDriverFacade : 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); } } /// - /// Sets the screen size for testing purposes. Only supported by FakeDriver. + /// Sets the screen size for testing purposes. Only supported by FakeDriver. /// /// The new width in columns. /// The new height in rows. /// Thrown when called on non-FakeDriver instances. 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))); } /// @@ -118,24 +155,24 @@ internal class ConsoleDriverFacade : IConsoleDriver, IConsoleDriverFacade /// The rectangle describing the of region. public Region? Clip { - get => _outputBuffer.Clip; - set => _outputBuffer.Clip = value; + get => OutputBuffer.Clip; + set => OutputBuffer.Clip = value; } /// Get the operating system clipboard. - public IClipboard Clipboard { get; private set; } = new FakeClipboard (); + public IClipboard? Clipboard { get; private set; } = new FakeClipboard (); /// /// Gets the column last set by . and are used by /// and to determine where to add content. /// - public int Col => _outputBuffer.Col; + public int Col => OutputBuffer.Col; /// The number of columns visible in the terminal. public int Cols { - get => _outputBuffer.Cols; - set => _outputBuffer.Cols = value; + get => OutputBuffer.Cols; + set => OutputBuffer.Cols = value; } /// @@ -144,51 +181,51 @@ internal class ConsoleDriverFacade : IConsoleDriver, IConsoleDriverFacade /// public Cell [,]? Contents { - get => _outputBuffer.Contents; - set => _outputBuffer.Contents = value; + get => OutputBuffer.Contents; + set => OutputBuffer.Contents = value; } /// The leftmost column in the terminal. public int Left { - get => _outputBuffer.Left; - set => _outputBuffer.Left = value; + get => OutputBuffer.Left; + set => OutputBuffer.Left = value; } /// /// Gets the row last set by . and are used by /// and to determine where to add content. /// - public int Row => _outputBuffer.Row; + public int Row => OutputBuffer.Row; /// The number of rows visible in the terminal. public int Rows { - get => _outputBuffer.Rows; - set => _outputBuffer.Rows = value; + get => OutputBuffer.Rows; + set => OutputBuffer.Rows = value; } /// The topmost row in the terminal. public int Top { - get => _outputBuffer.Top; - set => _outputBuffer.Top = value; + get => OutputBuffer.Top; + set => OutputBuffer.Top = value; } // TODO: Probably not everyone right? - /// Gets whether the supports TrueColor output. + /// Gets whether the supports TrueColor output. public bool SupportsTrueColor => true; // TODO: Currently ignored /// - /// Gets or sets whether the should use 16 colors instead of the default TrueColors. + /// Gets or sets whether the should use 16 colors instead of the default TrueColors. /// See to change this setting via . /// /// /// - /// Will be forced to if is - /// , indicating that the cannot support TrueColor. + /// Will be forced to if is + /// , indicating that the cannot support TrueColor. /// /// public bool Force16Colors @@ -198,85 +235,86 @@ internal class ConsoleDriverFacade : IConsoleDriver, IConsoleDriverFacade } /// - /// The that will be used for the next or + /// The that will be used for the next or + /// /// call. /// public Attribute CurrentAttribute { - get => _outputBuffer.CurrentAttribute; - set => _outputBuffer.CurrentAttribute = value; + get => OutputBuffer.CurrentAttribute; + set => OutputBuffer.CurrentAttribute = value; } /// Adds the specified rune to the display at the current cursor position. /// /// - /// When the method returns, will be incremented by the number of columns + /// When the method returns, will be incremented by the number of columns /// required, even if the new column value is outside of the - /// or screen - /// dimensions defined by . + /// or screen + /// dimensions defined by . /// /// - /// If requires more than one column, and plus the number + /// If requires more than one column, and plus the number /// of columns - /// needed exceeds the or screen dimensions, the default Unicode replacement + /// needed exceeds the or screen dimensions, the default Unicode replacement /// character (U+FFFD) /// will be added instead. /// /// /// Rune to add. - public void AddRune (Rune rune) { _outputBuffer.AddRune (rune); } + public void AddRune (Rune rune) { OutputBuffer.AddRune (rune); } /// /// Adds the specified to the display at the current cursor position. This method is a - /// convenience method that calls with the + /// convenience method that calls with the /// constructor. /// /// Character to add. - public void AddRune (char c) { _outputBuffer.AddRune (c); } + public void AddRune (char c) { OutputBuffer.AddRune (c); } /// Adds the to the display at the cursor position. /// /// - /// When the method returns, will be incremented by the number of columns - /// required, unless the new column value is outside of the + /// When the method returns, will be incremented by the number of columns + /// required, unless the new column value is outside of the /// or screen - /// dimensions defined by . + /// dimensions defined by . /// /// If requires more columns than are available, the output will be clipped. /// /// String. - public void AddStr (string str) { _outputBuffer.AddStr (str); } + public void AddStr (string str) { OutputBuffer.AddStr (str); } - /// Clears the of the driver. + /// Clears the of the driver. public void ClearContents () { - _outputBuffer.ClearContents (); + OutputBuffer.ClearContents (); ClearedContents?.Invoke (this, new MouseEventArgs ()); } /// - /// Raised each time is called. For benchmarking. + /// Raised each time is called. For benchmarking. /// public event EventHandler? ClearedContents; /// - /// Fills the specified rectangle with the specified rune, using + /// Fills the specified rectangle with the specified rune, using /// /// - /// The value of is honored. Any parts of the rectangle not in the clip will not be + /// The value of is honored. Any parts of the rectangle not in the clip will not be /// drawn. /// /// The Screen-relative rectangle. /// The Rune used to fill the rectangle - public void FillRect (Rectangle rect, Rune rune = default) { _outputBuffer.FillRect (rect, rune); } + public void FillRect (Rectangle rect, Rune rune = default) { OutputBuffer.FillRect (rect, rune); } /// /// Fills the specified rectangle with the specified . This method is a convenience method - /// that calls . + /// that calls . /// /// /// - public void FillRect (Rectangle rect, char c) { _outputBuffer.FillRect (rect, c); } + public void FillRect (Rectangle rect, char c) { OutputBuffer.FillRect (rect, c); } /// public virtual string GetVersionInfo () @@ -300,28 +338,28 @@ internal class ConsoleDriverFacade : IConsoleDriver, IConsoleDriverFacade /// The row. /// /// if the coordinate is outside the screen bounds or outside of - /// . + /// . /// otherwise. /// - 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); } /// - /// Updates and to the specified column and row in - /// . - /// Used by and to determine + /// Updates and to the specified column and row in + /// . + /// Used by and to determine /// where to add content. /// /// /// This does not move the cursor on the screen, it only updates the internal state of the driver. /// - /// If or are negative or beyond + /// If or are negative or beyond /// and - /// , the method still sets those properties. + /// , the method still sets those properties. /// /// /// Column to move to. /// Row to move to. - 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 : IConsoleDriver, IConsoleDriverFacade /// 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 : IConsoleDriver, IConsoleDriverFacade Console.Out.Write (EscSeqUtils.CSI_DisableMouseEvents); - if (!ConsoleDriver.RunningUnitTests) + try { Console.ResetColor (); Console.Clear (); @@ -369,16 +409,21 @@ internal class ConsoleDriverFacade : 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); } /// - /// Sets the position of the terminal cursor to and - /// . + /// Sets the position of the terminal cursor to and + /// . /// public void UpdateCursor () { _output.SetCursorPosition (Col, Row); } @@ -397,22 +442,22 @@ internal class ConsoleDriverFacade : IConsoleDriver, IConsoleDriverFacade /// The previously set Attribute. public Attribute SetAttribute (Attribute newAttribute) { - Attribute currentAttribute = _outputBuffer.CurrentAttribute; - _outputBuffer.CurrentAttribute = newAttribute; + Attribute currentAttribute = OutputBuffer.CurrentAttribute; + OutputBuffer.CurrentAttribute = newAttribute; return currentAttribute; } /// Gets the current . /// The current attribute. - public Attribute GetAttribute () { return _outputBuffer.CurrentAttribute; } + public Attribute GetAttribute () { return OutputBuffer.CurrentAttribute; } - /// Event fired when a key is pressed down. This is a precursor to . + /// Event fired when a key is pressed down. This is a precursor to . public event EventHandler? KeyDown; /// Event fired when a key is released. /// - /// Drivers that do not support key release events will fire this event after + /// Drivers that do not support key release events will fire this event after /// processing is /// complete. /// @@ -422,17 +467,31 @@ internal class ConsoleDriverFacade : IConsoleDriver, IConsoleDriverFacade public event EventHandler? MouseEvent; /// - /// Provide proper writing to send escape sequence recognized by the . + /// Provide proper writing to send escape sequence recognized by the . /// /// public void WriteRaw (string ansi) { _output.Write (ansi); } + /// + public void EnqueueKeyEvent (Key key) + { + InputProcessor.EnqueueKeyDownEvent (key); + } + /// - /// Queues the given for execution + /// Queues the specified ANSI escape sequence request for execution. /// - /// + /// The ANSI request to queue. + /// + /// The request is sent immediately if possible, or queued for later execution + /// by the to prevent overwhelming the console. + /// public void QueueAnsiRequest (AnsiEscapeSequenceRequest request) { _ansiRequestScheduler.SendOrSchedule (request); } + /// + /// Gets the instance used by this driver. + /// + /// The ANSI request scheduler. public AnsiRequestScheduler GetRequestScheduler () { return _ansiRequestScheduler; } /// @@ -440,4 +499,9 @@ internal class ConsoleDriverFacade : IConsoleDriver, IConsoleDriverFacade { // No need we will always draw when dirty } + + public string? GetName () + { + return InputProcessor.DriverName?.ToLowerInvariant (); + } } diff --git a/Terminal.Gui/Drivers/FakeDriver/FakeComponentFactory.cs b/Terminal.Gui/Drivers/FakeDriver/FakeComponentFactory.cs index 2314649bc..e7ce72f3d 100644 --- a/Terminal.Gui/Drivers/FakeDriver/FakeComponentFactory.cs +++ b/Terminal.Gui/Drivers/FakeDriver/FakeComponentFactory.cs @@ -4,47 +4,47 @@ using System.Collections.Concurrent; namespace Terminal.Gui.Drivers; /// -/// implementation for fake/mock console I/O used in unit tests. -/// This factory creates instances that simulate console behavior without requiring a real terminal. +/// implementation for fake/mock console I/O used in unit tests. +/// This factory creates instances that simulate console behavior without requiring a real terminal. /// -public class FakeComponentFactory : ComponentFactory +public class FakeComponentFactory : ComponentFactoryImpl { - private readonly ConcurrentQueue? _predefinedInput; - private readonly FakeConsoleOutput? _output; + private readonly FakeInput? _input; + private readonly IOutput? _output; + private readonly ISizeMonitor? _sizeMonitor; /// - /// Creates a new FakeComponentFactory with optional predefined input and output capture. + /// Creates a new FakeComponentFactory with optional output capture. /// - /// Optional queue of predefined input events to simulate. + /// /// Optional fake output to capture what would be written to console. - public FakeComponentFactory (ConcurrentQueue? predefinedInput = null, FakeConsoleOutput? output = null) + /// + public FakeComponentFactory (FakeInput? input = null, IOutput? output = null, ISizeMonitor? sizeMonitor = null) { - _predefinedInput = predefinedInput; + _input = input; _output = output; + _sizeMonitor = sizeMonitor; + } + + + /// + public override ISizeMonitor CreateSizeMonitor (IOutput consoleOutput, IOutputBuffer outputBuffer) + { + return _sizeMonitor ?? new SizeMonitorImpl (consoleOutput); } /// - public override IConsoleInput CreateInput () + public override IInput CreateInput () { - return new FakeConsoleInput (_predefinedInput); + return _input ?? new FakeInput (); } - /// - public override IConsoleOutput CreateOutput () - { - return _output ?? new FakeConsoleOutput (); - } + /// + public override IInputProcessor CreateInputProcessor (ConcurrentQueue inputBuffer) { return new FakeInputProcessor (inputBuffer); } - /// - public override IInputProcessor CreateInputProcessor (ConcurrentQueue inputBuffer) + /// + public override IOutput CreateOutput () { - return new NetInputProcessor (inputBuffer); - } - - /// - public override IConsoleSizeMonitor CreateConsoleSizeMonitor (IConsoleOutput consoleOutput, IOutputBuffer outputBuffer) - { - outputBuffer.SetSize(consoleOutput.GetSize().Width, consoleOutput.GetSize().Height); - return new ConsoleSizeMonitor (consoleOutput, outputBuffer); + return _output ?? new FakeOutput (); } } diff --git a/Terminal.Gui/Drivers/FakeDriver/FakeConsole.cs b/Terminal.Gui/Drivers/FakeDriver/FakeConsole.cs deleted file mode 100644 index 1b43a6b39..000000000 --- a/Terminal.Gui/Drivers/FakeDriver/FakeConsole.cs +++ /dev/null @@ -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. -/// -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. - - /// Specifies the initial console width. - public const int WIDTH = 0; - - /// Specifies the initial console height. - public const int HEIGHT = 0; - - /// - 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. - /// - 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. - /// - public static bool IsErrorRedirected { get; } - - // - // Summary: - // Gets the standard input stream. - // - // Returns: - // A System.IO.TextReader that represents the standard input stream. - /// - public static TextReader In { get; } - - // - // Summary: - // Gets the standard output stream. - // - // Returns: - // A System.IO.TextWriter that represents the standard output stream. - /// - public static TextWriter Out { get; } - - // - // Summary: - // Gets the standard error output stream. - // - // Returns: - // A System.IO.TextWriter that represents the standard error output stream. - /// - 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. - /// - 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. - /// - 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; - - /// - 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; - - /// - 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. - /// - 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. - /// - 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. - /// - 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. - /// - 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. - /// - 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. - /// - 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. - /// - 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. - /// - 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. - /// - 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. - /// - 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. - /// - 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. - /// - 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. - /// - 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. - /// - 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. - /// - 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. - /// - 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. - /// - 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]; - - /// - 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. - /// - 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] - /// - 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. - /// - 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. - /// - 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. - /// - public static Stream OpenStandardInput (int bufferSize) { throw new NotImplementedException (); } - - // - // Summary: - // Acquires the standard input stream. - // - // Returns: - // The standard input stream. - /// - 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. - /// - public static Stream OpenStandardOutput (int bufferSize) { throw new NotImplementedException (); } - - // - // Summary: - // Acquires the standard output stream. - // - // Returns: - // The standard output stream. - /// - 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. - /// - 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] - - /// A stack of keypresses to return when ReadKey is called. - public static Stack MockKeyPresses = new (); - - /// Helper to push a onto . - /// - 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. - /// - 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. - /// - 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] - /// - 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] - /// - 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] - /// - 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] - /// - 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] - /// - 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] - /// - /// - 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] - /// - /// - /// - 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] - /// - /// - /// - 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. - /// - /// - 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. - /// - /// - 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)] - /// - /// - 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. - /// - /// - 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. - /// - /// - /// - /// - 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. - /// - /// - 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. - /// - /// - /// - 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)] - /// - /// - public static void Write (uint value) { throw new NotImplementedException (); } - - //[CLSCompliant (false)] - /// - /// - /// - /// - /// - /// - 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. - /// - /// - /// - 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. - /// - /// - 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. - /// - /// - 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. - /// - /// - 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. - /// - /// - /// - /// - 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. - /// - /// - /// - /// - /// - 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. - /// - /// - 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. - /// - /// - 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. - /// - /// - 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. - /// - 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. - /// - /// - 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. - /// - /// - 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)] - /// - /// - 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. - /// - /// - 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)] - /// - /// - 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. - /// - /// - 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. - /// - /// - 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. - /// - /// - /// - 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. - /// - /// - /// - /// - /// - public static void WriteLine (string format, object arg0, object arg1, object arg2) { throw new NotImplementedException (); } - - //[CLSCompliant (false)] - /// - /// - /// - /// - /// - /// - 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. - /// - /// - /// - 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. - /// - /// - /// - /// - 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. - /// - /// - 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. - /// - /// - 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. - /// - /// - 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. - /// - /// - 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. - /// - /// - /// - /// - 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. - /// - /// - public static void WriteLine (double value) { throw new NotImplementedException (); } -} diff --git a/Terminal.Gui/Drivers/FakeDriver/FakeConsoleInput.cs b/Terminal.Gui/Drivers/FakeDriver/FakeConsoleInput.cs deleted file mode 100644 index 949d56117..000000000 --- a/Terminal.Gui/Drivers/FakeDriver/FakeConsoleInput.cs +++ /dev/null @@ -1,42 +0,0 @@ -#nullable enable -using System.Collections.Concurrent; - -namespace Terminal.Gui.Drivers; - -/// -/// Fake console input for testing that can return predefined input or wait indefinitely. -/// -public class FakeConsoleInput : ConsoleInput -{ - private readonly ConcurrentQueue? _predefinedInput; - - /// - /// Creates a new FakeConsoleInput with optional predefined input. - /// - /// Optional queue of predefined input to return. - public FakeConsoleInput (ConcurrentQueue? predefinedInput = null) - { - _predefinedInput = predefinedInput; - } - - /// - protected override bool Peek () - { - if (_predefinedInput != null && !_predefinedInput.IsEmpty) - { - return true; - } - - // No input available - return false; - } - - /// - protected override IEnumerable Read () - { - if (_predefinedInput != null && _predefinedInput.TryDequeue (out ConsoleKeyInfo key)) - { - yield return key; - } - } -} diff --git a/Terminal.Gui/Drivers/FakeDriver/FakeDriver.cs b/Terminal.Gui/Drivers/FakeDriver/FakeDriver.cs deleted file mode 100644 index 2c1f2d474..000000000 --- a/Terminal.Gui/Drivers/FakeDriver/FakeDriver.cs +++ /dev/null @@ -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; - -/// Implements a mock IConsoleDriver for unit testing -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; - - /// - 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; - - /// - public override bool GetCursorVisibility (out CursorVisibility visibility) - { - visibility = FakeConsole.CursorVisible - ? CursorVisibility.Default - : CursorVisibility.Invisible; - - return FakeConsole.CursorVisible; - } - - /// - 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 (); - - /// - internal override IAnsiResponseParser GetParser () { return _parser; } - - /// - /// Sets the screen size for testing purposes. Only available in FakeDriver. - /// This method updates the driver's dimensions and triggers the ScreenChanged event. - /// - /// The new width in columns. - /// The new height in rows. - 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 -} \ No newline at end of file diff --git a/Terminal.Gui/Drivers/FakeDriver/FakeInput.cs b/Terminal.Gui/Drivers/FakeDriver/FakeInput.cs new file mode 100644 index 000000000..76093e785 --- /dev/null +++ b/Terminal.Gui/Drivers/FakeDriver/FakeInput.cs @@ -0,0 +1,47 @@ +#nullable enable +using System.Collections.Concurrent; + +namespace Terminal.Gui.Drivers; + +/// +/// implementation that uses a fake input source for testing. +/// The and methods are executed +/// on the input thread created by . +/// +public class FakeInput : InputImpl, ITestableInput +{ + // Queue for storing injected input that will be returned by Peek/Read + private readonly ConcurrentQueue _testInput = new (); + + /// + /// Creates a new FakeInput. + /// + public FakeInput () + { } + + /// + public override bool Peek () + { + // Will be called on the input thread. + return !_testInput.IsEmpty; + } + + /// + public override IEnumerable Read () + { + // Will be called on the input thread. + while (_testInput.TryDequeue (out ConsoleKeyInfo input)) + { + yield return input; + } + } + + /// + public void AddInput (ConsoleKeyInfo input) + { + //Logging.Trace ($"Enqueuing input: {input.Key}"); + + // Will be called on the main loop thread. + _testInput.Enqueue (input); + } +} \ No newline at end of file diff --git a/Terminal.Gui/Drivers/FakeDriver/FakeInputProcessor.cs b/Terminal.Gui/Drivers/FakeDriver/FakeInputProcessor.cs new file mode 100644 index 000000000..7c07f0a16 --- /dev/null +++ b/Terminal.Gui/Drivers/FakeDriver/FakeInputProcessor.cs @@ -0,0 +1,47 @@ +using System.Collections.Concurrent; + +namespace Terminal.Gui.Drivers; + +/// +/// Input processor for , deals in stream +/// +public class FakeInputProcessor : InputProcessorImpl +{ + /// + public FakeInputProcessor (ConcurrentQueue inputBuffer) : base (inputBuffer, new NetKeyConverter ()) + { + DriverName = "fake"; + } + + /// + protected override void Process (ConsoleKeyInfo input) + { + Logging.Trace ($"input: {input.KeyChar}"); + + foreach (Tuple released in Parser.ProcessInput (Tuple.Create (input.KeyChar, input))) + { + Logging.Trace($"released: {released.Item1}"); + ProcessAfterParsing (released.Item2); + } + } + + /// + 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); + } + } +} diff --git a/Terminal.Gui/Drivers/FakeDriver/FakeConsoleOutput.cs b/Terminal.Gui/Drivers/FakeDriver/FakeOutput.cs similarity index 66% rename from Terminal.Gui/Drivers/FakeDriver/FakeConsoleOutput.cs rename to Terminal.Gui/Drivers/FakeDriver/FakeOutput.cs index 07c59e224..0722079fe 100644 --- a/Terminal.Gui/Drivers/FakeDriver/FakeConsoleOutput.cs +++ b/Terminal.Gui/Drivers/FakeDriver/FakeOutput.cs @@ -1,21 +1,43 @@ #nullable enable +using System; + namespace Terminal.Gui.Drivers; /// /// Fake console output for testing that captures what would be written to the console. /// -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); + /// + /// + /// + public FakeOutput () + { + LastBuffer = new OutputBufferImpl (); + LastBuffer.SetSize (80, 25); + } + + /// + /// Gets or sets the last output buffer written. + /// + public IOutputBuffer? LastBuffer { get; set; } + /// /// Gets the captured output as a string. /// public string Output => _output.ToString (); + /// + public Point GetCursorPosition () + { + return new (_cursorLeft, _cursorTop); + } + /// public void SetCursorPosition (int col, int row) { SetCursorPositionImpl (col, row); } @@ -34,23 +56,23 @@ public class FakeConsoleOutput : OutputBase, IConsoleOutput return true; } - /// - /// Sets the fake window size. - /// - public void SetConsoleSize (int width, int height) { _consoleSize = new (width, height); } - - /// - /// Gets the current cursor position. - /// - public (int left, int top) GetCursorPosition () { return (_cursorLeft, _cursorTop); } - /// public Size GetSize () { return _consoleSize; } /// - public void Write (ReadOnlySpan text) { _output.Append (text); } + public void Write (ReadOnlySpan text) + { + _output.Append (text); + } - /// + /// + public override void Write (IOutputBuffer buffer) + { + LastBuffer = buffer; + base.Write (buffer); + } + + /// 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 } /// - protected override void Write (StringBuilder output) { _output.Append (output); } + protected override void Write (StringBuilder output) + { + _output.Append (output); + } } diff --git a/Terminal.Gui/Drivers/IComponentFactory.cs b/Terminal.Gui/Drivers/IComponentFactory.cs index f7120c274..7122c4af3 100644 --- a/Terminal.Gui/Drivers/IComponentFactory.cs +++ b/Terminal.Gui/Drivers/IComponentFactory.cs @@ -1,51 +1,60 @@ #nullable enable using System.Collections.Concurrent; -using Terminal.Gui.App; namespace Terminal.Gui.Drivers; /// -/// Base untyped interface for for methods that are not templated on low level -/// console input type. +/// Base untyped interface for for methods that are not templated on low level +/// console input type. /// public interface IComponentFactory { /// - /// Create the class for the current driver implementation i.e. the class responsible for - /// rendering into the console. + /// Create the class for the current driver implementation i.e. the class responsible for + /// rendering into the console. /// /// - IConsoleOutput CreateOutput (); + IOutput CreateOutput (); } /// -/// Creates driver specific subcomponent classes (, etc) for a -/// . +/// Creates driver specific subcomponent classes (, +/// etc) for a +/// . /// -/// -public interface IComponentFactory : IComponentFactory +/// +/// The platform specific console input type. Must be a value type (struct). +/// Valid types are , , and . +/// +public interface IComponentFactory : IComponentFactory + where TInputRecord : struct { /// - /// Create class for the current driver implementation i.e. the class responsible for reading - /// user input from the console. + /// Create class for the current driver implementation i.e. the class responsible for reading + /// user input from the console. /// /// - IConsoleInput CreateInput (); + IInput CreateInput (); /// - /// Creates the class for the current driver implementation i.e. the class responsible for - /// translating raw console input into Terminal.Gui common event and . + /// Creates the class for the current driver implementation i.e. the class + /// responsible for + /// translating raw console input into Terminal.Gui common event and . /// - /// + /// + /// The input queue containing raw console input events, populated by + /// implementations on the input thread and + /// read by on the main loop thread. + /// /// - IInputProcessor CreateInputProcessor (ConcurrentQueue inputBuffer); + IInputProcessor CreateInputProcessor (ConcurrentQueue inputQueue); /// - /// Creates class for the current driver implementation i.e. the class responsible for - /// reporting the current size of the terminal. + /// Creates class for the current driver implementation i.e. the class responsible for + /// reporting the current size of the terminal. /// /// /// /// - IConsoleSizeMonitor CreateConsoleSizeMonitor (IConsoleOutput consoleOutput, IOutputBuffer outputBuffer); + ISizeMonitor CreateSizeMonitor (IOutput consoleOutput, IOutputBuffer outputBuffer); } diff --git a/Terminal.Gui/Drivers/IConsoleDriverFacade.cs b/Terminal.Gui/Drivers/IConsoleDriverFacade.cs deleted file mode 100644 index bf150af74..000000000 --- a/Terminal.Gui/Drivers/IConsoleDriverFacade.cs +++ /dev/null @@ -1,26 +0,0 @@ -#nullable enable -namespace Terminal.Gui.Drivers; - -/// -/// Interface for v2 driver abstraction layer -/// -public interface IConsoleDriverFacade -{ - /// - /// Class responsible for processing native driver input objects - /// e.g. into events - /// and detecting and processing ansi escape sequences. - /// - IInputProcessor InputProcessor { get; } - - /// - /// Describes the desired screen state. Data source for . - /// - IOutputBuffer OutputBuffer { get; } - - /// - /// Interface for classes responsible for reporting the current - /// size of the terminal window. - /// - IConsoleSizeMonitor ConsoleSizeMonitor { get; } -} diff --git a/Terminal.Gui/Drivers/IConsoleInput.cs b/Terminal.Gui/Drivers/IConsoleInput.cs deleted file mode 100644 index a79b9159b..000000000 --- a/Terminal.Gui/Drivers/IConsoleInput.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System.Collections.Concurrent; - -namespace Terminal.Gui.Drivers; - -/// -/// 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. -/// -/// -public interface IConsoleInput : IDisposable -{ - /// - /// Initializes the input with a buffer into which to put data read - /// - /// - void Initialize (ConcurrentQueue inputBuffer); - - /// - /// Runs in an infinite input loop. - /// - /// - /// - /// Raised when token is - /// cancelled. This is the only means of exiting the input. - /// - void Run (CancellationToken token); -} diff --git a/Terminal.Gui/Drivers/IConsoleDriver.cs b/Terminal.Gui/Drivers/IDriver.cs similarity index 75% rename from Terminal.Gui/Drivers/IConsoleDriver.cs rename to Terminal.Gui/Drivers/IDriver.cs index b65fca43f..32af99b05 100644 --- a/Terminal.Gui/Drivers/IConsoleDriver.cs +++ b/Terminal.Gui/Drivers/IDriver.cs @@ -2,13 +2,37 @@ namespace Terminal.Gui.Drivers; -/// Base interface for Terminal.Gui ConsoleDriver implementations. +/// Base interface for Terminal.Gui Driver implementations. /// /// There are currently four implementations: UnixDriver, WindowsDriver, DotNetDriver, and FakeDriver /// -public interface IConsoleDriver +public interface IDriver { + /// + /// Gets the name of the driver implementation. + /// + string? GetName (); + + /// + /// Class responsible for processing native driver input objects + /// e.g. into events + /// and detecting and processing ansi escape sequences. + /// + IInputProcessor InputProcessor { get; } + + /// + /// Describes the desired screen state. Data source for . + /// + IOutputBuffer OutputBuffer { get; } + + /// + /// Interface for classes responsible for reporting the current + /// size of the terminal window. + /// + ISizeMonitor SizeMonitor { get; } + /// Get the operating system clipboard. + /// IClipboard? Clipboard { get; } /// Gets the location and size of the terminal screen. @@ -62,17 +86,17 @@ public interface IConsoleDriver /// The topmost row in the terminal. int Top { get; set; } - /// Gets whether the supports TrueColor output. + /// Gets whether the supports TrueColor output. bool SupportsTrueColor { get; } /// - /// Gets or sets whether the should use 16 colors instead of the default TrueColors. + /// Gets or sets whether the should use 16 colors instead of the default TrueColors. /// See to change this setting via . /// /// /// - /// Will be forced to if is - /// , indicating that the cannot support TrueColor. + /// Will be forced to if is + /// , indicating that the cannot support TrueColor. /// /// bool Force16Colors { get; set; } @@ -88,7 +112,7 @@ public interface IConsoleDriver string GetVersionInfo (); /// - /// Provide proper writing to send escape sequence recognized by the . + /// Provide proper writing to send escape sequence recognized by the . /// /// void WriteRaw (string ansi); @@ -107,23 +131,23 @@ public interface IConsoleDriver /// The row. /// /// if the coordinate is outside the screen bounds or outside of - /// . + /// . /// otherwise. /// bool IsValidLocation (Rune rune, int col, int row); /// - /// Updates and to the specified column and row in - /// . - /// Used by and to determine + /// Updates and to the specified column and row in + /// . + /// Used by and to determine /// where to add content. /// /// /// This does not move the cursor on the screen, it only updates the internal state of the driver. /// - /// If or are negative or beyond + /// If or are negative or beyond /// and - /// , the method still sets those properties. + /// , the method still sets those properties. /// /// /// Column to move to. @@ -133,15 +157,15 @@ public interface IConsoleDriver /// Adds the specified rune to the display at the current cursor position. /// /// - /// When the method returns, will be incremented by the number of columns + /// When the method returns, will be incremented by the number of columns /// required, even if the new column value is outside of the - /// or screen - /// dimensions defined by . + /// or screen + /// dimensions defined by . /// /// - /// If requires more than one column, and plus the number + /// If requires more than one column, and plus the number /// of columns - /// needed exceeds the or screen dimensions, the default Unicode replacement + /// needed exceeds the or screen dimensions, the default Unicode replacement /// character (U+FFFD) /// will be added instead. /// @@ -151,7 +175,7 @@ public interface IConsoleDriver /// /// Adds the specified to the display at the current cursor position. This method is a - /// convenience method that calls with the + /// convenience method that calls with the /// constructor. /// /// Character to add. @@ -160,27 +184,27 @@ public interface IConsoleDriver /// Adds the to the display at the cursor position. /// /// - /// When the method returns, will be incremented by the number of columns - /// required, unless the new column value is outside of the + /// When the method returns, will be incremented by the number of columns + /// required, unless the new column value is outside of the /// or screen - /// dimensions defined by . + /// dimensions defined by . /// /// If requires more columns than are available, the output will be clipped. /// /// String. void AddStr (string str); - /// Clears the of the driver. + /// Clears the of the driver. void ClearContents (); /// - /// Fills the specified rectangle with the specified rune, using + /// Fills the specified rectangle with the specified rune, using /// event EventHandler ClearedContents; - /// Fills the specified rectangle with the specified rune, using + /// Fills the specified rectangle with the specified rune, using /// - /// The value of is honored. Any parts of the rectangle not in the clip will not be + /// The value of is honored. Any parts of the rectangle not in the clip will not be /// drawn. /// /// The Screen-relative rectangle. @@ -189,7 +213,7 @@ public interface IConsoleDriver /// /// Fills the specified rectangle with the specified . This method is a convenience method - /// that calls . + /// that calls . /// /// /// @@ -220,8 +244,8 @@ public interface IConsoleDriver void Suspend (); /// - /// Sets the position of the terminal cursor to and - /// . + /// Sets the position of the terminal cursor to and + /// . /// void UpdateCursor (); @@ -243,17 +267,23 @@ public interface IConsoleDriver /// Event fired when a mouse event occurs. event EventHandler? MouseEvent; - /// Event fired when a key is pressed down. This is a precursor to . + /// Event fired when a key is pressed down. This is a precursor to . event EventHandler? KeyDown; /// Event fired when a key is released. /// - /// Drivers that do not support key release events will fire this event after + /// Drivers that do not support key release events will fire this event after /// processing is /// complete. /// event EventHandler? KeyUp; + /// + /// Enqueues a key input event to the driver. For unit tests. + /// + /// + void EnqueueKeyEvent (Key key); + /// /// Queues the given for execution /// diff --git a/Terminal.Gui/Drivers/IInput.cs b/Terminal.Gui/Drivers/IInput.cs new file mode 100644 index 000000000..0b2ec7d41 --- /dev/null +++ b/Terminal.Gui/Drivers/IInput.cs @@ -0,0 +1,172 @@ +#nullable enable +using System.Collections.Concurrent; + +namespace Terminal.Gui.Drivers; + +/// +/// Interface for reading console input in a perpetual loop on a dedicated input thread. +/// +/// +/// +/// Implementations run on a separate thread (started by +/// ) +/// and continuously read platform-specific input from the console, placing it into a thread-safe queue +/// for processing by on the main UI thread. +/// +/// +/// Architecture: +/// +/// +/// Input Thread: Main UI Thread: +/// ┌─────────────────┐ ┌──────────────────────┐ +/// │ IInput.Run() │ │ IInputProcessor │ +/// │ ├─ Peek() │ │ ├─ ProcessQueue() │ +/// │ ├─ Read() │──Enqueue──→ │ ├─ Process() │ +/// │ └─ Enqueue │ │ ├─ ToKey() │ +/// └─────────────────┘ │ └─ Raise Events │ +/// └──────────────────────┘ +/// +/// +/// Lifecycle: +/// +/// +/// - Set the shared input queue +/// - Start the perpetual read loop (blocks until cancelled) +/// +/// Loop calls and +/// +/// Cancellation via `runCancellationToken` or +/// +/// +/// Implementations: +/// +/// +/// - Uses Windows Console API (ReadConsoleInput) +/// - Uses .NET API +/// - Uses Unix terminal APIs +/// - For testing, implements +/// +/// +/// Testing Support: See for programmatic input injection +/// in test scenarios. +/// +/// +/// +/// The platform-specific input record type: +/// +/// - for .NET and Fake drivers +/// - for Windows driver +/// - for Unix driver +/// +/// +public interface IInput : IDisposable +{ + /// + /// Gets or sets an external cancellation token source that can stop the loop + /// in addition to the `runCancellationToken` passed to . + /// + /// + /// + /// This property allows external code (e.g., test harnesses like GuiTestContext) to + /// provide additional cancellation signals such as timeouts or hard-stop conditions. + /// + /// + /// Ownership: The setter does NOT transfer ownership of the . + /// The creator is responsible for disposal. implementations + /// should NOT dispose this token source. + /// + /// + /// How it works: creates a linked token that + /// responds to BOTH the `runCancellationToken` AND this external token: + /// + /// + /// var linkedToken = CancellationTokenSource.CreateLinkedTokenSource( + /// runCancellationToken, + /// ExternalCancellationTokenSource.Token); + /// + /// + /// + /// Test scenario with timeout: + /// + /// 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); + /// + /// + CancellationTokenSource? ExternalCancellationTokenSource { get; set; } + + /// + /// Initializes the input reader with the thread-safe queue where read input will be stored. + /// + /// + /// The shared that both (producer) + /// and (consumer) use for passing input records between threads. + /// + /// + /// + /// This queue is created by + /// and shared between the input thread and main UI thread. + /// + /// + /// Must be called before . Calling without + /// initialization will throw an exception. + /// + /// + void Initialize (ConcurrentQueue inputQueue); + + /// + /// Runs the input loop, continuously reading input and placing it into the queue + /// provided by . + /// + /// + /// The primary cancellation token that stops the input loop. Provided by + /// and triggered + /// during application shutdown. + /// + /// + /// + /// Threading: This method runs on a dedicated input thread created by + /// . and blocks until + /// cancellation is requested. It should never be called from the main UI thread. + /// + /// + /// Cancellation: The loop stops when either + /// or (if set) is cancelled. + /// + /// + /// Base Implementation: provides the + /// standard loop logic: + /// + /// + /// 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 + /// } + /// + /// + /// Testing: For implementations, + /// test input injected via + /// flows through the same Peek/Read pipeline. + /// + /// + /// + /// Thrown when or + /// is cancelled. This is the normal/expected means of exiting the input loop. + /// + /// + /// Thrown if was not called before . + /// + void Run (CancellationToken runCancellationToken); +} diff --git a/Terminal.Gui/Drivers/IInputProcessor.cs b/Terminal.Gui/Drivers/IInputProcessor.cs index 007825f61..d0c0284b8 100644 --- a/Terminal.Gui/Drivers/IInputProcessor.cs +++ b/Terminal.Gui/Drivers/IInputProcessor.cs @@ -3,58 +3,22 @@ namespace Terminal.Gui.Drivers; /// -/// 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 and translating into common Terminal.Gui /// events and data models. /// public interface IInputProcessor { - /// Event fired when a key is pressed down. This is a precursor to . - event EventHandler? KeyDown; - - /// Event fired when a key is released. - /// - /// Drivers that do not support key release events will fire this event after processing is - /// complete. - /// - event EventHandler? KeyUp; - - /// Event fired when a terminal sequence read from input is not recognized and therefore ignored. + /// Event raised when a terminal sequence read from input is not recognized and therefore ignored. public event EventHandler? AnsiSequenceSwallowed; - /// Event fired when a mouse event occurs. - event EventHandler? MouseEvent; - /// /// Gets the name of the driver associated with this input processor. /// string? DriverName { get; init; } /// - /// Called when a key is pressed down. Fires the event. This is a precursor to - /// . - /// - /// The key event data. - void OnKeyDown (Key key); - - /// - /// Called when a key is released. Fires the event. - /// - /// - /// Drivers that do not support key release events will call this method after processing - /// is complete. - /// - /// The key event data. - void OnKeyUp (Key key); - - /// - /// Called when a mouse event occurs. Fires the event. - /// - /// The mouse event data. - void OnMouseEvent (MouseEventArgs mouseEventArgs); - - /// - /// Drains the input buffer, processing all available keystrokes + /// Drains the input queue, processing all available keystrokes. To be called on the main loop thread. /// void ProcessQueue (); @@ -74,4 +38,59 @@ public interface IInputProcessor /// . /// bool IsValidInput (Key key, out Key result); + + /// + /// Called when a key down event has been dequeued. Raises the event. This is a precursor to + /// . + /// + /// The key event data. + void RaiseKeyDownEvent (Key key); + + /// Event raised when a key down event has been dequeued. This is a precursor to . + event EventHandler? KeyDown; + + /// + /// Adds a key up event to the input queue. For unit tests. + /// + /// + void EnqueueKeyDownEvent (Key key); + + /// + /// Called when a key up event has been dequeued. Raises the event. + /// + /// + /// Drivers that do not support key release events will call this method after processing + /// is complete. + /// + /// The key event data. + void RaiseKeyUpEvent (Key key); + + /// Event raised when a key up event has been dequeued. + /// + /// Drivers that do not support key release events will fire this event after processing is + /// complete. + /// + event EventHandler? KeyUp; + + /// + /// Adds a key up event to the input queue. For unit tests. + /// + /// + void EnqueueKeyUpEvent (Key key); + + /// + /// Called when a mouse event has been dequeued. Raises the event. + /// + /// The mouse event data. + void RaiseMouseEvent (MouseEventArgs mouseEventArgs); + + /// Event raised when a mouse event has been dequeued. + event EventHandler? MouseEvent; + + /// + /// Adds a mouse input event to the input queue. For unit tests. + /// + /// + void EnqueueMouseEvent (MouseEventArgs mouseEvent); + } diff --git a/Terminal.Gui/Drivers/IKeyConverter.cs b/Terminal.Gui/Drivers/IKeyConverter.cs index c915e933c..dcd5e0487 100644 --- a/Terminal.Gui/Drivers/IKeyConverter.cs +++ b/Terminal.Gui/Drivers/IKeyConverter.cs @@ -2,18 +2,26 @@ namespace Terminal.Gui.Drivers; /// -/// Interface for subcomponent of a which +/// Interface for subcomponent of a which /// can translate the raw console input type T (which typically varies by /// driver) to the shared Terminal.Gui class. /// -/// -public interface IKeyConverter +/// +public interface IKeyConverter { /// - /// Converts the native keyboard class read from console into - /// the shared class used by Terminal.Gui views. + /// Converts the native keyboard info type into + /// the class used by Terminal.Gui views. /// - /// + /// /// - Key ToKey (T value); + Key ToKey (TInputRecord keyInfo); + + /// + /// Converts a into the native keyboard info type. Should be used for simulating + /// key presses in unit tests. + /// + /// + /// + TInputRecord ToKeyInfo (Key key); } diff --git a/Terminal.Gui/Drivers/IConsoleOutput.cs b/Terminal.Gui/Drivers/IOutput.cs similarity index 78% rename from Terminal.Gui/Drivers/IConsoleOutput.cs rename to Terminal.Gui/Drivers/IOutput.cs index 4bb768c86..bc04dfaa6 100644 --- a/Terminal.Gui/Drivers/IConsoleOutput.cs +++ b/Terminal.Gui/Drivers/IOutput.cs @@ -1,10 +1,45 @@ namespace Terminal.Gui.Drivers; /// -/// Interface for writing console output +/// The low-level interface drivers implement to provide output capabilities; encapsulates platform-specific +/// output functionality. /// -public interface IConsoleOutput : IDisposable +public interface IOutput : IDisposable { + /// + /// Gets the current position of the console cursor. + /// + /// + Point GetCursorPosition (); + + /// + /// Returns the current size of the console in rows/columns (i.e. + /// of characters not pixels). + /// + /// + public Size GetSize (); + + /// + /// Moves the console cursor to the given location. + /// + /// + /// + void SetCursorPosition (int col, int row); + + /// + /// Updates the console cursor (the blinking underscore) to be hidden, + /// visible etc. + /// + /// + void SetCursorVisibility (CursorVisibility visibility); + + /// + /// Sets the size of the console. + /// + /// + /// + void SetSize (int width, int height); + /// /// Writes the given text directly to the console. Use to send /// ansi escape codes etc. Regular screen output should use the @@ -18,32 +53,4 @@ public interface IConsoleOutput : IDisposable /// /// void Write (IOutputBuffer buffer); - - /// - /// Returns the current size of the console in rows/columns (i.e. - /// of characters not pixels). - /// - /// - public Size GetSize (); - - /// - /// Updates the console cursor (the blinking underscore) to be hidden, - /// visible etc. - /// - /// - void SetCursorVisibility (CursorVisibility visibility); - - /// - /// Moves the console cursor to the given location. - /// - /// - /// - void SetCursorPosition (int col, int row); - - /// - /// Sets the size of the console.. - /// - /// - /// - void SetSize (int width, int height); } diff --git a/Terminal.Gui/Drivers/IOutputBuffer.cs b/Terminal.Gui/Drivers/IOutputBuffer.cs index 66498f4f6..2b8991593 100644 --- a/Terminal.Gui/Drivers/IOutputBuffer.cs +++ b/Terminal.Gui/Drivers/IOutputBuffer.cs @@ -3,66 +3,27 @@ namespace Terminal.Gui.Drivers; /// -/// 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 . /// +/// +/// +/// The 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 +/// after all drawing is complete, minimizing flicker and improving performance. +/// +/// +/// The buffer maintains a 2D array of objects in , where each cell +/// represents a single character position on screen with its associated character, attributes, and dirty state. +/// Drawing operations like and modify cells at the +/// current cursor position (tracked by and ), respecting any active +/// region. +/// +/// public interface IOutputBuffer { - /// - /// The contents of the application output. The driver outputs this buffer to the terminal when UpdateScreen is called. - /// - Cell [,]? Contents { get; set; } - - /// - /// Gets or sets the clip rectangle that and are subject - /// to. - /// - /// The rectangle describing the of region. - public Region? Clip { get; set; } - - /// - /// The that will be used for the next AddRune or AddStr call. - /// - Attribute CurrentAttribute { get; set; } - - /// The number of rows visible in the terminal. - int Rows { get; set; } - - /// The number of columns visible in the terminal. - int Cols { get; set; } - - /// - /// Gets the row last set by . and are used by - /// and to determine where to add content. - /// - public int Row { get; } - - /// - /// Gets the column last set by . and are used by - /// and to determine where to add content. - /// - public int Col { get; } - - /// - /// The first cell index on left of screen - basically always 0. - /// Changing this may have unexpected consequences. - /// - int Left { get; set; } - - /// - /// The first cell index on top of screen - basically always 0. - /// Changing this may have unexpected consequences. - /// - int Top { get; set; } - - /// - /// Updates the column and row to the specified location in the buffer. - /// - /// The column to move to. - /// The row to move to. - void Move (int col, int row); - /// Adds the specified rune to the display at the current cursor position. /// Rune to add. void AddRune (Rune rune); @@ -82,22 +43,30 @@ public interface IOutputBuffer void ClearContents (); /// - /// Tests whether the specified coordinate is valid for drawing the specified Rune. + /// Gets or sets the clip rectangle that and are subject + /// to. /// - /// Used to determine if one or two columns are required. - /// The column. - /// The row. - /// - /// True if the coordinate is valid for the Rune; false otherwise. - /// - bool IsValidLocation (Rune rune, int col, int row); + /// The rectangle describing the of region. + public Region? Clip { get; set; } /// - /// Changes the size of the buffer to the given size + /// Gets the column last set by . and are used by + /// and to determine where to add content. /// - /// - /// - void SetSize (int cols, int rows); + public int Col { get; } + + /// The number of columns visible in the terminal. + int Cols { get; set; } + + /// + /// The contents of the application output. The driver outputs this buffer to the terminal when UpdateScreen is called. + /// + Cell [,]? Contents { get; set; } + + /// + /// The that will be used for the next AddRune or AddStr call. + /// + Attribute CurrentAttribute { get; set; } /// /// Fills the given with the given @@ -114,4 +83,50 @@ public interface IOutputBuffer /// /// void FillRect (Rectangle rect, char rune); + + /// + /// Tests whether the specified coordinate is valid for drawing the specified Rune. + /// + /// Used to determine if one or two columns are required. + /// The column. + /// The row. + /// + /// True if the coordinate is valid for the Rune; false otherwise. + /// + bool IsValidLocation (Rune rune, int col, int row); + + /// + /// The first cell index on left of screen - basically always 0. + /// Changing this may have unexpected consequences. + /// + int Left { get; set; } + + /// + /// Updates the column and row to the specified location in the buffer. + /// + /// The column to move to. + /// The row to move to. + void Move (int col, int row); + + /// + /// Gets the row last set by . and are used by + /// and to determine where to add content. + /// + public int Row { get; } + + /// The number of rows visible in the terminal. + int Rows { get; set; } + + /// + /// Changes the size of the buffer to the given size + /// + /// + /// + void SetSize (int cols, int rows); + + /// + /// The first cell index on top of screen - basically always 0. + /// Changing this may have unexpected consequences. + /// + int Top { get; set; } } diff --git a/Terminal.Gui/Drivers/IConsoleSizeMonitor.cs b/Terminal.Gui/Drivers/ISizeMonitor.cs similarity index 94% rename from Terminal.Gui/Drivers/IConsoleSizeMonitor.cs rename to Terminal.Gui/Drivers/ISizeMonitor.cs index b34503f17..602d5e4b8 100644 --- a/Terminal.Gui/Drivers/IConsoleSizeMonitor.cs +++ b/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. /// -public interface IConsoleSizeMonitor +public interface ISizeMonitor { /// Invoked when the terminal's size changed. The new size of the terminal is provided. event EventHandler? SizeChanged; diff --git a/Terminal.Gui/Drivers/ITestableInput.cs b/Terminal.Gui/Drivers/ITestableInput.cs new file mode 100644 index 000000000..a5b96d265 --- /dev/null +++ b/Terminal.Gui/Drivers/ITestableInput.cs @@ -0,0 +1,15 @@ +namespace Terminal.Gui.Drivers; + +/// +/// Marker interface for IInput implementations that support test input injection. +/// +/// The input record type +public interface ITestableInput : IInput + where TInputRecord : struct +{ + /// + /// Adds an input record that will be returned by Peek/Read for testing. + /// + void AddInput (TInputRecord input); +} + diff --git a/Terminal.Gui/Drivers/InputImpl.cs b/Terminal.Gui/Drivers/InputImpl.cs new file mode 100644 index 000000000..d340b3d85 --- /dev/null +++ b/Terminal.Gui/Drivers/InputImpl.cs @@ -0,0 +1,89 @@ +#nullable enable +using System.Collections.Concurrent; + +namespace Terminal.Gui.Drivers; + +/// +/// Base class for reading console input in perpetual loop. +/// The and methods are executed +/// on the input thread created by . +/// +/// +public abstract class InputImpl : IInput +{ + private ConcurrentQueue? _inputQueue; + + /// + /// Determines how to get the current system type, adjust + /// in unit tests to simulate specific timings. + /// + public Func Now { get; set; } = () => DateTime.Now; + + /// + public CancellationTokenSource? ExternalCancellationTokenSource { get; set; } + + /// + public void Initialize (ConcurrentQueue inputQueue) { _inputQueue = inputQueue; } + + /// + 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 (); + } + } + + /// + /// When implemented in a derived class, returns true if there is data available + /// to read from console. + /// + /// + public abstract bool Peek (); + + /// + /// Returns the available data without blocking, called when + /// returns . + /// + /// + public abstract IEnumerable Read (); + + /// + public virtual void Dispose () { } +} \ No newline at end of file diff --git a/Terminal.Gui/Drivers/InputProcessor.cs b/Terminal.Gui/Drivers/InputProcessorImpl.cs similarity index 53% rename from Terminal.Gui/Drivers/InputProcessor.cs rename to Terminal.Gui/Drivers/InputProcessorImpl.cs index 15d320ba2..f79a96364 100644 --- a/Terminal.Gui/Drivers/InputProcessor.cs +++ b/Terminal.Gui/Drivers/InputProcessorImpl.cs @@ -5,30 +5,67 @@ using Microsoft.Extensions.Logging; namespace Terminal.Gui.Drivers; /// -/// Processes the queued input buffer contents - which must be of Type . +/// Processes the queued input queue contents - which must be of Type . /// Is responsible for and translating into common Terminal.Gui -/// events and data models. +/// events and data models. Runs on the main loop thread. /// -public abstract class InputProcessor : IInputProcessor +public abstract class InputProcessorImpl : IInputProcessor, IDisposable where TInputRecord : struct { + /// + /// Constructs base instance including wiring all relevant + /// parser events and setting to + /// the provided thread safe input collection. + /// + /// The collection that will be populated with new input (see ) + /// + /// Key converter for translating driver specific + /// class into Terminal.Gui . + /// + protected InputProcessorImpl (ConcurrentQueue inputBuffer, IKeyConverter 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)} ignored unrecognized response '{cur}'"); + AnsiSequenceSwallowed?.Invoke (this, cur); + + return true; + }; + KeyConverter = keyConverter; + } + /// /// How long after Esc has been pressed before we give up on getting an Ansi escape sequence /// private readonly TimeSpan _escTimeout = TimeSpan.FromMilliseconds (50); - internal AnsiResponseParser Parser { get; } = new (); + internal AnsiResponseParser Parser { get; } = new (); /// - /// Class responsible for translating the driver specific native input class e.g. + /// Class responsible for translating the driver specific native input class e.g. /// into the Terminal.Gui class (used for all /// internal library representations of Keys). /// - public IKeyConverter KeyConverter { get; } + public IKeyConverter KeyConverter { get; } /// - /// Input buffer which will be drained from by this class. + /// The input queue which is filled by implementations running on the input thread. + /// Implementations of this class should dequeue from this queue in on the main loop thread. /// - public ConcurrentQueue InputBuffer { get; } + public ConcurrentQueue InputQueue { get; } /// public string? DriverName { get; init; } @@ -38,110 +75,95 @@ public abstract class InputProcessor : IInputProcessor private readonly MouseInterpreter _mouseInterpreter = new (); - /// Event fired when a key is pressed down. This is a precursor to . + /// public event EventHandler? KeyDown; - /// Event fired when a terminal sequence read from input is not recognized and therefore ignored. + /// public event EventHandler? AnsiSequenceSwallowed; - /// - /// Called when a key is pressed down. Fires the event. This is a precursor to - /// . - /// - /// - public void OnKeyDown (Key a) + /// + public void RaiseKeyDownEvent (Key a) { - Logging.Trace ($"{nameof (InputProcessor)} raised {a}"); KeyDown?.Invoke (this, a); } - /// Event fired when a key is released. - /// - /// Drivers that do not support key release events will fire this event after processing is - /// complete. - /// + /// public event EventHandler? KeyUp; - /// Called when a key is released. Fires the event. - /// - /// Drivers that do not support key release events will call this method after processing - /// is complete. - /// - /// - public void OnKeyUp (Key a) { KeyUp?.Invoke (this, a); } + /// + public void RaiseKeyUpEvent (Key a) { KeyUp?.Invoke (this, a); } - /// Event fired when a mouse event occurs. + /// + /// + /// + public IInput? InputImpl { get; set; } // Set by MainLoopCoordinator + + /// + public void EnqueueKeyDownEvent (Key key) + { + // Convert Key → TInputRecord + TInputRecord inputRecord = KeyConverter.ToKeyInfo (key); + + // If input supports testing, use InputImplPeek/Read pipeline + // which runs on the input thread. + if (InputImpl is ITestableInput testableInput) + { + testableInput.AddInput (inputRecord); + } + } + + /// + public void EnqueueKeyUpEvent (Key key) + { + // TODO: Determine if we can still support this on Windows + throw new NotImplementedException (); + } + + /// public event EventHandler? MouseEvent; - /// Called when a mouse event occurs. Fires the event. - /// - public void OnMouseEvent (MouseEventArgs a) + /// + 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."); + } + + /// + public void RaiseMouseEvent (MouseEventArgs a) { // Ensure ScreenPosition is set a.ScreenPosition = a.Position; foreach (MouseEventArgs e in _mouseInterpreter.Process (a)) { - // Logging.Trace ($"Mouse Interpreter raising {e.Flags}"); + // Logging.Trace ($"Mouse Interpreter raising {e.Flags}"); // Pass on MouseEvent?.Invoke (this, e); } } - /// - /// Constructs base instance including wiring all relevant - /// parser events and setting to - /// the provided thread safe input collection. - /// - /// The collection that will be populated with new input (see ) - /// - /// Key converter for translating driver specific - /// class into Terminal.Gui . - /// - protected InputProcessor (ConcurrentQueue inputBuffer, IKeyConverter keyConverter) - { - InputBuffer = inputBuffer; - Parser.HandleMouse = true; - Parser.Mouse += (s, e) => OnMouseEvent (e); - - Parser.HandleKeyboard = true; - - Parser.Keyboard += (s, k) => - { - OnKeyDown (k); - OnKeyUp (k); - }; - - // TODO: For now handle all other escape codes with ignore - Parser.UnexpectedResponseHandler = str => - { - var cur = new string (str.Select (k => k.Item1).ToArray ()); - Logging.Logger.LogInformation ($"{nameof (InputProcessor)} ignored unrecognized response '{cur}'"); - AnsiSequenceSwallowed?.Invoke (this, cur); - - return true; - }; - KeyConverter = keyConverter; - } - - /// - /// Drains the buffer, processing all available keystrokes - /// + /// 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 ReleaseParserHeldKeysIfStale () + private IEnumerable ReleaseParserHeldKeysIfStale () { if (Parser.State is AnsiResponseParserState.ExpectingEscapeSequence or AnsiResponseParserState.InResponse && DateTime.Now - Parser.StateChangedAt > _escTimeout) @@ -154,17 +176,27 @@ public abstract class InputProcessor : IInputProcessor /// /// Process the provided single input element . This method - /// is called sequentially for each value read from . + /// is called sequentially for each value read from . /// /// - protected abstract void Process (T input); + protected abstract void Process (TInputRecord input); /// /// Process the provided single input element - short-circuiting the /// stage of the processing. /// /// - 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 : IInputProcessor return true; } + + /// + public CancellationTokenSource? ExternalCancellationTokenSource { get; set; } + + /// + public void Dispose () { ExternalCancellationTokenSource?.Dispose (); } } diff --git a/Terminal.Gui/Drivers/KeyCode.cs b/Terminal.Gui/Drivers/KeyCode.cs index 85924cb5f..eb87a7e92 100644 --- a/Terminal.Gui/Drivers/KeyCode.cs +++ b/Terminal.Gui/Drivers/KeyCode.cs @@ -2,7 +2,7 @@ namespace Terminal.Gui.Drivers; /// -/// The enumeration encodes key information from s and provides a +/// The enumeration encodes key information from s and provides a /// consistent way for application code to specify keys and receive key events. /// /// The class provides a higher-level abstraction, with helper methods and properties for diff --git a/Terminal.Gui/Drivers/OutputBase.cs b/Terminal.Gui/Drivers/OutputBase.cs index 4222ec6e1..ac43dc93d 100644 --- a/Terminal.Gui/Drivers/OutputBase.cs +++ b/Terminal.Gui/Drivers/OutputBase.cs @@ -2,7 +2,7 @@ namespace Terminal.Gui.Drivers; /// -/// Abstract base class to assist with implementing . +/// Abstract base class to assist with implementing . /// public abstract class OutputBase { @@ -18,14 +18,9 @@ public abstract class OutputBase /// public abstract void SetCursorVisibility (CursorVisibility visibility); - /// + /// public virtual void Write (IOutputBuffer buffer) { - if (ConsoleDriver.RunningUnitTests) - { - return; - } - var top = 0; var left = 0; int rows = buffer.Rows; diff --git a/Terminal.Gui/Drivers/OutputBuffer.cs b/Terminal.Gui/Drivers/OutputBufferImpl.cs similarity index 99% rename from Terminal.Gui/Drivers/OutputBuffer.cs rename to Terminal.Gui/Drivers/OutputBufferImpl.cs index 0d3efcfac..ee2493d60 100644 --- a/Terminal.Gui/Drivers/OutputBuffer.cs +++ b/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 /// -public class OutputBuffer : IOutputBuffer +public class OutputBufferImpl : IOutputBuffer { /// /// The contents of the application output. The driver outputs this buffer to the terminal when diff --git a/Terminal.Gui/Drivers/ConsoleSizeMonitor.cs b/Terminal.Gui/Drivers/SizeMonitorImpl.cs similarity index 68% rename from Terminal.Gui/Drivers/ConsoleSizeMonitor.cs rename to Terminal.Gui/Drivers/SizeMonitorImpl.cs index e5999dd39..409f27617 100644 --- a/Terminal.Gui/Drivers/ConsoleSizeMonitor.cs +++ b/Terminal.Gui/Drivers/SizeMonitorImpl.cs @@ -4,7 +4,7 @@ using Microsoft.Extensions.Logging; namespace Terminal.Gui.Drivers; /// -internal class ConsoleSizeMonitor (IConsoleOutput consoleOut, IOutputBuffer _) : IConsoleSizeMonitor +internal class SizeMonitorImpl (IOutput consoleOut) : ISizeMonitor { private Size _lastSize = Size.Empty; @@ -14,16 +14,11 @@ internal class ConsoleSizeMonitor (IConsoleOutput consoleOut, IOutputBuffer _) : /// public bool Poll () { - if (ConsoleDriver.RunningUnitTests) - { - return false; - } - Size size = consoleOut.GetSize (); if (size != _lastSize) { - Logging.Logger.LogInformation ($"Console size changes from '{_lastSize}' to {size}"); + //Logging.Trace ($"Size changed from '{_lastSize}' to {size}"); _lastSize = size; SizeChanged?.Invoke (this, new (size)); diff --git a/Terminal.Gui/Drivers/UnixDriver/IUnixInput.cs b/Terminal.Gui/Drivers/UnixDriver/IUnixInput.cs index 23755f0c2..0fba2873a 100644 --- a/Terminal.Gui/Drivers/UnixDriver/IUnixInput.cs +++ b/Terminal.Gui/Drivers/UnixDriver/IUnixInput.cs @@ -1,3 +1,6 @@ namespace Terminal.Gui.Drivers; -internal interface IUnixInput : IConsoleInput; +/// +/// Wraps IConsoleInput for Unix console input events (char). Needed to support Mocking in tests. +/// +internal interface IUnixInput : IInput; diff --git a/Terminal.Gui/Drivers/UnixDriver/UnixComponentFactory.cs b/Terminal.Gui/Drivers/UnixDriver/UnixComponentFactory.cs index 9df727f36..f53b88f85 100644 --- a/Terminal.Gui/Drivers/UnixDriver/UnixComponentFactory.cs +++ b/Terminal.Gui/Drivers/UnixDriver/UnixComponentFactory.cs @@ -7,10 +7,10 @@ namespace Terminal.Gui.Drivers; /// implementation for native unix console I/O. /// This factory creates instances of internal classes , etc. /// -public class UnixComponentFactory : ComponentFactory +public class UnixComponentFactory : ComponentFactoryImpl { /// - public override IConsoleInput CreateInput () + public override IInput CreateInput () { return new UnixInput (); } @@ -22,7 +22,7 @@ public class UnixComponentFactory : ComponentFactory } /// - public override IConsoleOutput CreateOutput () + public override IOutput CreateOutput () { return new UnixOutput (); } diff --git a/Terminal.Gui/Drivers/UnixDriver/UnixInput.cs b/Terminal.Gui/Drivers/UnixDriver/UnixInput.cs index 793553e33..0337e0aff 100644 --- a/Terminal.Gui/Drivers/UnixDriver/UnixInput.cs +++ b/Terminal.Gui/Drivers/UnixDriver/UnixInput.cs @@ -1,9 +1,14 @@ -using System.Runtime.InteropServices; -using Microsoft.Extensions.Logging; +#nullable enable +using System.Runtime.InteropServices; + +// ReSharper disable IdentifierTypo +// ReSharper disable InconsistentNaming +// ReSharper disable StringLiteralTypo +// ReSharper disable CommentTypo namespace Terminal.Gui.Drivers; -internal class UnixInput : ConsoleInput, IUnixInput +internal class UnixInput : InputImpl, IUnixInput { private const int STDIN_FILENO = 0; @@ -53,45 +58,33 @@ internal class UnixInput : ConsoleInput, IUnixInput private const ulong CS8 = 0x00000030; private Termios _original; + private bool _terminalInitialized; [StructLayout (LayoutKind.Sequential)] private struct Pollfd { public int fd; public short events; - public readonly short revents; // readonly signals "don't touch this in managed code" + public readonly short revents; } - /// Condition on which to wake up from file descriptor activity. These match the Linux/BSD poll definitions. [Flags] private enum Condition : short { - /// There is data to read PollIn = 1, - - /// There is urgent data to read PollPri = 2, - - /// Writing to the specified descriptor will not block PollOut = 4, - - /// Error condition on output PollErr = 8, - - /// Hang-up on output PollHup = 16, - - /// File descriptor is not open. PollNval = 32 } [DllImport ("libc", SetLastError = true)] - private static extern int poll ([In][Out] Pollfd [] ufds, uint nfds, int timeout); + private static extern int poll ([In] [Out] Pollfd [] ufds, uint nfds, int timeout); [DllImport ("libc", SetLastError = true)] private static extern int read (int fd, byte [] buf, int count); - // File descriptor for stdout private const int STDOUT_FILENO = 1; [DllImport ("libc", SetLastError = true)] @@ -100,118 +93,150 @@ internal class UnixInput : ConsoleInput, IUnixInput [DllImport ("libc", SetLastError = true)] private static extern int tcflush (int fd, int queueSelector); - private const int TCIFLUSH = 0; // flush data received but not read + private const int TCIFLUSH = 0; - private Pollfd [] _pollMap; + private Pollfd []? _pollMap; public UnixInput () { - Logging.Logger.LogInformation ($"Creating {nameof (UnixInput)}"); + Logging.Information ($"Creating {nameof (UnixInput)}"); - if (ConsoleDriver.RunningUnitTests) + try { - return; + _pollMap = new Pollfd [1]; + _pollMap [0].fd = STDIN_FILENO; + _pollMap [0].events = (short)Condition.PollIn; + + EnableRawModeAndTreatControlCAsInput (); + + if (_terminalInitialized) + { + WriteRaw (EscSeqUtils.CSI_SaveCursorAndActivateAltBufferNoBackscroll); + WriteRaw (EscSeqUtils.CSI_HideCursor); + WriteRaw (EscSeqUtils.CSI_EnableMouseEvents); + } + } + catch (DllNotFoundException ex) + { + Logging.Warning ($"UnixInput: libc not available: {ex.Message}. Running in degraded mode."); + _terminalInitialized = false; + } + catch (Exception ex) + { + Logging.Warning ($"UnixInput: Failed to initialize terminal: {ex.Message}. Running in degraded mode."); + _terminalInitialized = false; } - - _pollMap = new Pollfd [1]; - _pollMap [0].fd = STDIN_FILENO; // stdin - _pollMap [0].events = (short)Condition.PollIn; - - EnableRawModeAndTreatControlCAsInput (); - - //Enable alternative screen buffer. - WriteRaw (EscSeqUtils.CSI_SaveCursorAndActivateAltBufferNoBackscroll); - - //Set cursor key to application. - WriteRaw (EscSeqUtils.CSI_HideCursor); - - WriteRaw (EscSeqUtils.CSI_EnableMouseEvents); } private void EnableRawModeAndTreatControlCAsInput () { - if (tcgetattr (STDIN_FILENO, out _original) != 0) - { - var e = Marshal.GetLastWin32Error (); - throw new InvalidOperationException ($"tcgetattr failed errno={e} ({StrError (e)})"); - } - - var raw = _original; - - // Prefer cfmakeraw if available try { - cfmakeraw_ref (ref raw); - } - catch (EntryPointNotFoundException) - { - // fallback: roughly cfmakeraw equivalent - raw.c_iflag &= ~((uint)BRKINT | (uint)ICRNL | (uint)INPCK | (uint)ISTRIP | (uint)IXON); - raw.c_oflag &= ~(uint)OPOST; - raw.c_cflag |= (uint)CS8; - raw.c_lflag &= ~((uint)ECHO | (uint)ICANON | (uint)IEXTEN | (uint)ISIG); - } + int result = tcgetattr (STDIN_FILENO, out _original); - if (tcsetattr (STDIN_FILENO, TCSANOW, ref raw) != 0) + if (result != 0) + { + int e = Marshal.GetLastWin32Error (); + Logging.Warning ($"tcgetattr failed errno={e} ({StrError (e)}). Running without TTY support."); + return; + } + + Termios raw = _original; + + try + { + cfmakeraw_ref (ref raw); + } + catch (EntryPointNotFoundException) + { + raw.c_iflag &= ~((uint)BRKINT | (uint)ICRNL | (uint)INPCK | (uint)ISTRIP | (uint)IXON); + raw.c_oflag &= ~(uint)OPOST; + raw.c_cflag |= (uint)CS8; + raw.c_lflag &= ~((uint)ECHO | (uint)ICANON | (uint)IEXTEN | (uint)ISIG); + } + + result = tcsetattr (STDIN_FILENO, TCSANOW, ref raw); + + if (result != 0) + { + int e = Marshal.GetLastWin32Error (); + Logging.Warning ($"tcsetattr failed errno={e} ({StrError (e)}). Running without TTY support."); + return; + } + + _terminalInitialized = true; + } + catch (DllNotFoundException) { - var e = Marshal.GetLastWin32Error (); - throw new InvalidOperationException ($"tcsetattr failed errno={e} ({StrError (e)})"); + throw; // Re-throw to be caught by constructor } } private string StrError (int err) - { - var p = strerror (err); - return p == nint.Zero ? $"errno={err}" : Marshal.PtrToStringAnsi (p) ?? $"errno={err}"; - } - - /// - protected override bool Peek () { try { - if (ConsoleDriver.RunningUnitTests) - { - return false; - } - - int n = poll (_pollMap!, (uint)_pollMap!.Length, 0); - - if (n != 0) - { - return true; - } - - return false; + nint p = strerror (err); + return p == nint.Zero ? $"errno={err}" : Marshal.PtrToStringAnsi (p) ?? $"errno={err}"; } - catch (Exception ex) + catch { - // Optionally log the exception - Logging.Logger.LogError ($"Error in Peek: {ex.Message}"); - - return false; - } - } - private void WriteRaw (string text) - { - if (!ConsoleDriver.RunningUnitTests) - { - byte [] utf8 = Encoding.UTF8.GetBytes (text); - // Write to stdout (fd 1) - write (STDOUT_FILENO, utf8, utf8.Length); + return $"errno={err}"; } } /// - protected override IEnumerable Read () + public override bool Peek () { - while (poll (_pollMap!, (uint)_pollMap!.Length, 0) != 0) + if (!_terminalInitialized || _pollMap is null) + { + return false; + } + + try + { + int n = poll (_pollMap, (uint)_pollMap.Length, 0); + return n != 0; + } + catch (Exception ex) + { + Logging.Error ($"Error in Peek: {ex.Message}"); + return false; + } + } + + private void WriteRaw (string text) + { + if (!_terminalInitialized) + { + return; + } + + try + { + byte [] utf8 = Encoding.UTF8.GetBytes (text); + write (STDOUT_FILENO, utf8, utf8.Length); + } + catch + { + // ignore exceptions during write + } + } + + /// + public override IEnumerable Read () + { + if (!_terminalInitialized || _pollMap is null) + { + yield break; + } + + while (poll (_pollMap, (uint)_pollMap.Length, 0) != 0) { - // Check if stdin has data if ((_pollMap [0].revents & (int)Condition.PollIn) != 0) { var buf = new byte [256]; - int bytesRead = read (0, buf, buf.Length); // Read from stdin + int bytesRead = read (0, buf, buf.Length); string input = Encoding.UTF8.GetString (buf, 0, bytesRead); foreach (char ch in input) @@ -224,43 +249,51 @@ internal class UnixInput : ConsoleInput, IUnixInput private void FlushConsoleInput () { - if (!ConsoleDriver.RunningUnitTests) + if (!_terminalInitialized) { - var fds = new Pollfd [1]; + return; + } + + try + { + Pollfd [] fds = new Pollfd [1]; fds [0].fd = STDIN_FILENO; fds [0].events = (short)Condition.PollIn; var buf = new byte [256]; + while (poll (fds, 1, 0) > 0) { read (STDIN_FILENO, buf, buf.Length); } } + catch + { + // ignore + } } - /// + /// public override void Dispose () { base.Dispose (); - if (!ConsoleDriver.RunningUnitTests) + if (!_terminalInitialized) + { + return; + } + + try { - // Disable mouse events first WriteRaw (EscSeqUtils.CSI_DisableMouseEvents); - - // Drain any pending input already queued by the terminal FlushConsoleInput (); - - // Flush kernel input buffer tcflush (STDIN_FILENO, TCIFLUSH); - - //Disable alternative screen buffer. WriteRaw (EscSeqUtils.CSI_RestoreCursorAndRestoreAltBufferWithBackscroll); - - //Set cursor key to cursor. WriteRaw (EscSeqUtils.CSI_ShowCursor); - - // Restore terminal to original state tcsetattr (STDIN_FILENO, TCSANOW, ref _original); } + catch + { + // ignore exceptions during disposal + } } } diff --git a/Terminal.Gui/Drivers/UnixDriver/UnixInputProcessor.cs b/Terminal.Gui/Drivers/UnixDriver/UnixInputProcessor.cs index d8b45971d..f9179fc24 100644 --- a/Terminal.Gui/Drivers/UnixDriver/UnixInputProcessor.cs +++ b/Terminal.Gui/Drivers/UnixDriver/UnixInputProcessor.cs @@ -5,12 +5,12 @@ namespace Terminal.Gui.Drivers; /// /// Input processor for , deals in stream. /// -internal class UnixInputProcessor : InputProcessor +internal class UnixInputProcessor : InputProcessorImpl { /// public UnixInputProcessor (ConcurrentQueue inputBuffer) : base (inputBuffer, new UnixKeyConverter ()) { - DriverName = "Unix"; + DriverName = "unix"; } /// @@ -22,17 +22,4 @@ internal class UnixInputProcessor : InputProcessor } } - - /// - protected override void ProcessAfterParsing (char 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); - } - } } diff --git a/Terminal.Gui/Drivers/UnixDriver/UnixKeyConverter.cs b/Terminal.Gui/Drivers/UnixDriver/UnixKeyConverter.cs index cdaa38537..62c05a0ed 100644 --- a/Terminal.Gui/Drivers/UnixDriver/UnixKeyConverter.cs +++ b/Terminal.Gui/Drivers/UnixDriver/UnixKeyConverter.cs @@ -10,11 +10,22 @@ namespace Terminal.Gui.Drivers; /// internal class UnixKeyConverter : IKeyConverter { - /// + /// public Key ToKey (char value) { ConsoleKeyInfo adjustedInput = EscSeqUtils.MapChar (value); return EscSeqUtils.MapKey (adjustedInput); } + + /// + public char ToKeyInfo (Key key) + { + // Convert Key to ConsoleKeyInfo using the cross-platform mapping utility + ConsoleKeyInfo consoleKeyInfo = ConsoleKeyMapping.GetConsoleKeyInfoFromKeyCode (key.KeyCode); + + // Return the character representation + // For Unix, we primarily care about the KeyChar as Unix deals with character input + return consoleKeyInfo.KeyChar; + } } diff --git a/Terminal.Gui/Drivers/UnixDriver/UnixOutput.cs b/Terminal.Gui/Drivers/UnixDriver/UnixOutput.cs index 9313e2bfd..17dbe2bb4 100644 --- a/Terminal.Gui/Drivers/UnixDriver/UnixOutput.cs +++ b/Terminal.Gui/Drivers/UnixDriver/UnixOutput.cs @@ -1,9 +1,13 @@ -using System.Runtime.InteropServices; +#nullable enable +using System.Runtime.InteropServices; using Microsoft.Win32.SafeHandles; +// ReSharper disable IdentifierTypo +// ReSharper disable InconsistentNaming + namespace Terminal.Gui.Drivers; -internal class UnixOutput : OutputBase, IConsoleOutput +internal class UnixOutput : OutputBase, IOutput { [StructLayout (LayoutKind.Sequential)] private struct WinSize @@ -62,9 +66,17 @@ internal class UnixOutput : OutputBase, IConsoleOutput /// protected override void Write (StringBuilder output) { - byte [] utf8 = Encoding.UTF8.GetBytes (output.ToString ()); - // Write to stdout (fd 1) - write (STDOUT_FILENO, utf8, utf8.Length); + try + { + byte [] utf8 = Encoding.UTF8.GetBytes (output.ToString ()); + + // Write to stdout (fd 1) + write (STDOUT_FILENO, utf8, utf8.Length); + } + catch + { + // ignore for unit tests + } } private Point? _lastCursorPosition; @@ -79,89 +91,126 @@ internal class UnixOutput : OutputBase, IConsoleOutput _lastCursorPosition = new (screenPositionX, screenPositionY); - using var writer = CreateUnixStdoutWriter (); + try + { + using TextWriter? writer = CreateUnixStdoutWriter (); - // + 1 is needed because Unix is based on 1 instead of 0 and - EscSeqUtils.CSI_WriteCursorPosition (writer, screenPositionY + 1, screenPositionX + 1); + // + 1 is needed because Unix is based on 1 instead of 0 and + EscSeqUtils.CSI_WriteCursorPosition (writer!, screenPositionY + 1, screenPositionX + 1); + } + catch + { + // ignore + } return true; } - private TextWriter CreateUnixStdoutWriter () + private TextWriter? CreateUnixStdoutWriter () { - // duplicate stdout so we don’t mess with Console.Out’s FD + // duplicate stdout so we don't mess with Console.Out's FD int fdCopy = dup (STDOUT_FILENO); if (fdCopy == -1) { - throw new IOException ("Failed to dup STDOUT_FILENO"); + // Log but don't throw - we're likely running without a TTY (CI/CD, tests, etc.) + var errno = Marshal.GetLastWin32Error (); + Logging.Warning ($"Failed to dup STDOUT_FILENO, errno={errno}. Running without TTY support."); + return null; // Return null instead of throwing } - // wrap the raw fd into a SafeFileHandle - var handle = new SafeFileHandle (fdCopy, ownsHandle: true); - - // create FileStream from the safe handle - var stream = new FileStream (handle, FileAccess.Write); - - return new StreamWriter (stream) + try { - AutoFlush = true - }; + // wrap the raw fd into a SafeFileHandle + SafeFileHandle handle = new SafeFileHandle (fdCopy, ownsHandle: true); + + // create FileStream from the safe handle + FileStream stream = new FileStream (handle, FileAccess.Write); + + return new StreamWriter (stream) + { + AutoFlush = true + }; + } + catch (Exception ex) + { + Logging.Warning ($"Failed to create TextWriter from dup'd STDOUT: {ex.Message}"); + return null; + } } /// public void Write (ReadOnlySpan text) { - if (!ConsoleDriver.RunningUnitTests) + try { byte [] utf8 = Encoding.UTF8.GetBytes (text.ToArray ()); + // Write to stdout (fd 1) write (STDOUT_FILENO, utf8, utf8.Length); } + catch + { + // ignore for unit tests + } } /// public Size GetSize () { - if (ConsoleDriver.RunningUnitTests) + try { - // For unit tests, we return a default size. - return Size.Empty; - } - - if (ioctl (1, TIOCGWINSZ, out WinSize ws) == 0) - { - if (ws.ws_col > 0 && ws.ws_row > 0) + if (ioctl (1, TIOCGWINSZ, out WinSize ws) == 0) { - return new (ws.ws_col, ws.ws_row); + if (ws.ws_col > 0 && ws.ws_row > 0) + { + return new (ws.ws_col, ws.ws_row); + } } } + catch + { + // ignore + } - return Size.Empty; // fallback + return new (80, 25); // fallback } private EscSeqUtils.DECSCUSR_Style? _currentDecscusrStyle; - /// + /// 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); + } + else + { + Write (EscSeqUtils.CSI_HideCursor); } - - Write (EscSeqUtils.CSI_ShowCursor); } - else + catch { - Write (EscSeqUtils.CSI_HideCursor); + // ignore } } + /// + public Point GetCursorPosition () + { + return _lastCursorPosition ?? Point.Empty; + } + /// public void SetCursorPosition (int col, int row) { diff --git a/Terminal.Gui/Drivers/VK.cs b/Terminal.Gui/Drivers/VK.cs new file mode 100644 index 000000000..a9df8cc54 --- /dev/null +++ b/Terminal.Gui/Drivers/VK.cs @@ -0,0 +1,488 @@ +// ReSharper disable InconsistentNaming +// ReSharper disable UnusedMember.Global +// ReSharper disable IdentifierTypo +namespace Terminal.Gui.Drivers; + +/// Generated from winuser.h. See https://learn.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes +public enum VK : ushort +{ + /// Left mouse button. + LBUTTON = 0x01, + + /// Right mouse button. + RBUTTON = 0x02, + + /// Control-break processing. + CANCEL = 0x03, + + /// Middle mouse button (three-button mouse). + MBUTTON = 0x04, + + /// X1 mouse button. + XBUTTON1 = 0x05, + + /// X2 mouse button. + XBUTTON2 = 0x06, + + /// BACKSPACE key. + BACK = 0x08, + + /// TAB key. + TAB = 0x09, + + /// CLEAR key. + CLEAR = 0x0C, + + /// ENTER key. + RETURN = 0x0D, + + /// SHIFT key. + SHIFT = 0x10, + + /// CTRL key. + CONTROL = 0x11, + + /// ALT key. + MENU = 0x12, + + /// PAUSE key. + PAUSE = 0x13, + + /// CAPS LOCK key. + CAPITAL = 0x14, + + /// IME Kana mode. + KANA = 0x15, + + /// IME Hangul mode. + HANGUL = 0x15, + + /// IME Junja mode. + JUNJA = 0x17, + + /// IME final mode. + FINAL = 0x18, + + /// IME Hanja mode. + HANJA = 0x19, + + /// IME Kanji mode. + KANJI = 0x19, + + /// ESC key. + ESCAPE = 0x1B, + + /// IME convert. + CONVERT = 0x1C, + + /// IME nonconvert. + NONCONVERT = 0x1D, + + /// IME accept. + ACCEPT = 0x1E, + + /// IME mode change request. + MODECHANGE = 0x1F, + + /// SPACEBAR. + SPACE = 0x20, + + /// PAGE UP key. + PRIOR = 0x21, + + /// PAGE DOWN key. + NEXT = 0x22, + + /// END key. + END = 0x23, + + /// HOME key. + HOME = 0x24, + + /// LEFT ARROW key. + LEFT = 0x25, + + /// UP ARROW key. + UP = 0x26, + + /// RIGHT ARROW key. + RIGHT = 0x27, + + /// DOWN ARROW key. + DOWN = 0x28, + + /// SELECT key. + SELECT = 0x29, + + /// PRINT key. + PRINT = 0x2A, + + /// EXECUTE key + EXECUTE = 0x2B, + + /// PRINT SCREEN key + SNAPSHOT = 0x2C, + + /// INS key + INSERT = 0x2D, + + /// DEL key + DELETE = 0x2E, + + /// HELP key + HELP = 0x2F, + + /// Left Windows key (Natural keyboard) + LWIN = 0x5B, + + /// Right Windows key (Natural keyboard) + RWIN = 0x5C, + + /// Applications key (Natural keyboard) + APPS = 0x5D, + + /// Computer Sleep key + SLEEP = 0x5F, + + /// Numeric keypad 0 key + NUMPAD0 = 0x60, + + /// Numeric keypad 1 key + NUMPAD1 = 0x61, + + /// Numeric keypad 2 key + NUMPAD2 = 0x62, + + /// Numeric keypad 3 key + NUMPAD3 = 0x63, + + /// Numeric keypad 4 key + NUMPAD4 = 0x64, + + /// Numeric keypad 5 key + NUMPAD5 = 0x65, + + /// Numeric keypad 6 key + NUMPAD6 = 0x66, + + /// Numeric keypad 7 key + NUMPAD7 = 0x67, + + /// Numeric keypad 8 key + NUMPAD8 = 0x68, + + /// Numeric keypad 9 key + NUMPAD9 = 0x69, + + /// Multiply key + MULTIPLY = 0x6A, + + /// Add key + ADD = 0x6B, + + /// Separator key + SEPARATOR = 0x6C, + + /// Subtract key + SUBTRACT = 0x6D, + + /// Decimal key + DECIMAL = 0x6E, + + /// Divide key + DIVIDE = 0x6F, + + /// F1 key + F1 = 0x70, + + /// F2 key + F2 = 0x71, + + /// F3 key + F3 = 0x72, + + /// F4 key + F4 = 0x73, + + /// F5 key + F5 = 0x74, + + /// F6 key + F6 = 0x75, + + /// F7 key + F7 = 0x76, + + /// F8 key + F8 = 0x77, + + /// F9 key + F9 = 0x78, + + /// F10 key + F10 = 0x79, + + /// F11 key + F11 = 0x7A, + + /// F12 key + F12 = 0x7B, + + /// F13 key + F13 = 0x7C, + + /// F14 key + F14 = 0x7D, + + /// F15 key + F15 = 0x7E, + + /// F16 key + F16 = 0x7F, + + /// F17 key + F17 = 0x80, + + /// F18 key + F18 = 0x81, + + /// F19 key + F19 = 0x82, + + /// F20 key + F20 = 0x83, + + /// F21 key + F21 = 0x84, + + /// F22 key + F22 = 0x85, + + /// F23 key + F23 = 0x86, + + /// F24 key + F24 = 0x87, + + /// NUM LOCK key + NUMLOCK = 0x90, + + /// SCROLL LOCK key + SCROLL = 0x91, + + /// NEC PC-9800 kbd definition: '=' key on numpad + OEM_NEC_EQUAL = 0x92, + + /// Fujitsu/OASYS kbd definition: 'Dictionary' key + OEM_FJ_JISHO = 0x92, + + /// Fujitsu/OASYS kbd definition: 'Unregister word' key + OEM_FJ_MASSHOU = 0x93, + + /// Fujitsu/OASYS kbd definition: 'Register word' key + OEM_FJ_TOUROKU = 0x94, + + /// Fujitsu/OASYS kbd definition: 'Left OYAYUBI' key + OEM_FJ_LOYA = 0x95, + + /// Fujitsu/OASYS kbd definition: 'Right OYAYUBI' key + OEM_FJ_ROYA = 0x96, + + /// Left SHIFT key + LSHIFT = 0xA0, + + /// Right SHIFT key + RSHIFT = 0xA1, + + /// Left CONTROL key + LCONTROL = 0xA2, + + /// Right CONTROL key + RCONTROL = 0xA3, + + /// Left MENU key (Left Alt key) + LMENU = 0xA4, + + /// Right MENU key (Right Alt key) + RMENU = 0xA5, + + /// Browser Back key + BROWSER_BACK = 0xA6, + + /// Browser Forward key + BROWSER_FORWARD = 0xA7, + + /// Browser Refresh key + BROWSER_REFRESH = 0xA8, + + /// Browser Stop key + BROWSER_STOP = 0xA9, + + /// Browser Search key + BROWSER_SEARCH = 0xAA, + + /// Browser Favorites key + BROWSER_FAVORITES = 0xAB, + + /// Browser Home key + BROWSER_HOME = 0xAC, + + /// Volume Mute key + VOLUME_MUTE = 0xAD, + + /// Volume Down key + VOLUME_DOWN = 0xAE, + + /// Volume Up key + VOLUME_UP = 0xAF, + + /// Next Track key + MEDIA_NEXT_TRACK = 0xB0, + + /// Previous Track key + MEDIA_PREV_TRACK = 0xB1, + + /// Stop Media key + MEDIA_STOP = 0xB2, + + /// Play/Pause Media key + MEDIA_PLAY_PAUSE = 0xB3, + + /// Start Mail key + LAUNCH_MAIL = 0xB4, + + /// Select Media key + LAUNCH_MEDIA_SELECT = 0xB5, + + /// Start Application 1 key + LAUNCH_APP1 = 0xB6, + + /// Start Application 2 key + LAUNCH_APP2 = 0xB7, + + /// Used for miscellaneous characters; it can vary by keyboard. For the US standard keyboard, the ';:' key + OEM_1 = 0xBA, + + /// For any country/region, the '+' key + OEM_PLUS = 0xBB, + + /// For any country/region, the ',' key + OEM_COMMA = 0xBC, + + /// For any country/region, the '-' key + OEM_MINUS = 0xBD, + + /// For any country/region, the '.' key + OEM_PERIOD = 0xBE, + + /// Used for miscellaneous characters; it can vary by keyboard. For the US standard keyboard, the '/?' key + OEM_2 = 0xBF, + + /// Used for miscellaneous characters; it can vary by keyboard. For the US standard keyboard, the '`~' key + OEM_3 = 0xC0, + + /// Used for miscellaneous characters; it can vary by keyboard. For the US standard keyboard, the '[{' key + OEM_4 = 0xDB, + + /// Used for miscellaneous characters; it can vary by keyboard. For the US standard keyboard, the '\|' key + OEM_5 = 0xDC, + + /// Used for miscellaneous characters; it can vary by keyboard. For the US standard keyboard, the ']}' key + OEM_6 = 0xDD, + + /// + /// Used for miscellaneous characters; it can vary by keyboard. For the US standard keyboard, the + /// 'single-quote/double-quote' key + /// + OEM_7 = 0xDE, + + /// Used for miscellaneous characters; it can vary by keyboard. + OEM_8 = 0xDF, + + /// 'AX' key on Japanese AX kbd + OEM_AX = 0xE1, + + /// Either the angle bracket key or the backslash key on the RT 102-key keyboard + OEM_102 = 0xE2, + + /// Help key on ICO + ICO_HELP = 0xE3, + + /// 00 key on ICO + ICO_00 = 0xE4, + + /// Process key + PROCESSKEY = 0xE5, + + /// Clear key on ICO + ICO_CLEAR = 0xE6, + + /// Packet key to be used to pass Unicode characters as if they were keystrokes + PACKET = 0xE7, + + /// Reset key + OEM_RESET = 0xE9, + + /// Jump key + OEM_JUMP = 0xEA, + + /// PA1 key + OEM_PA1 = 0xEB, + + /// PA2 key + OEM_PA2 = 0xEC, + + /// PA3 key + OEM_PA3 = 0xED, + + /// WsCtrl key + OEM_WSCTRL = 0xEE, + + /// CuSel key + OEM_CUSEL = 0xEF, + + /// Attn key + OEM_ATTN = 0xF0, + + /// Finish key + OEM_FINISH = 0xF1, + + /// Copy key + OEM_COPY = 0xF2, + + /// Auto key + OEM_AUTO = 0xF3, + + /// Enlw key + OEM_ENLW = 0xF4, + + /// BackTab key + OEM_BACKTAB = 0xF5, + + /// Attn key + ATTN = 0xF6, + + /// CrSel key + CRSEL = 0xF7, + + /// ExSel key + EXSEL = 0xF8, + + /// Erase EOF key + EREOF = 0xF9, + + /// Play key + PLAY = 0xFA, + + /// Zoom key + ZOOM = 0xFB, + + /// Reserved + NONAME = 0xFC, + + /// PA1 key + PA1 = 0xFD, + + /// Clear key + OEM_CLEAR = 0xFE +} diff --git a/Terminal.Gui/Drivers/WindowsDriver/IWindowsInput.cs b/Terminal.Gui/Drivers/WindowsDriver/IWindowsInput.cs index e46bb56e6..980e505ab 100644 --- a/Terminal.Gui/Drivers/WindowsDriver/IWindowsInput.cs +++ b/Terminal.Gui/Drivers/WindowsDriver/IWindowsInput.cs @@ -1,7 +1,7 @@ namespace Terminal.Gui.Drivers; /// -/// Interface for windows only input which uses low level win32 apis +/// Wraps IConsoleInput for Windows console input events (WindowsConsole.InputRecord). Needed to support Mocking in tests. /// -public interface IWindowsInput : IConsoleInput +public interface IWindowsInput : IInput { } diff --git a/Terminal.Gui/Drivers/WindowsDriver/WindowsComponentFactory.cs b/Terminal.Gui/Drivers/WindowsDriver/WindowsComponentFactory.cs index 69cc31bd9..1f16fd66a 100644 --- a/Terminal.Gui/Drivers/WindowsDriver/WindowsComponentFactory.cs +++ b/Terminal.Gui/Drivers/WindowsDriver/WindowsComponentFactory.cs @@ -7,10 +7,10 @@ namespace Terminal.Gui.Drivers; /// implementation for win32 only I/O. /// This factory creates instances of internal classes , etc. /// -public class WindowsComponentFactory : ComponentFactory +public class WindowsComponentFactory : ComponentFactoryImpl { /// - public override IConsoleInput CreateInput () + public override IInput CreateInput () { return new WindowsInput (); } @@ -22,7 +22,7 @@ public class WindowsComponentFactory : ComponentFactory - public override IConsoleOutput CreateOutput () + public override IOutput CreateOutput () { return new WindowsOutput (); } diff --git a/Terminal.Gui/Drivers/WindowsDriver/WindowsConsole.cs b/Terminal.Gui/Drivers/WindowsDriver/WindowsConsole.cs index abff1ca0c..744c32588 100644 --- a/Terminal.Gui/Drivers/WindowsDriver/WindowsConsole.cs +++ b/Terminal.Gui/Drivers/WindowsDriver/WindowsConsole.cs @@ -12,10 +12,16 @@ namespace Terminal.Gui.Drivers; /// public class WindowsConsole { - /// - /// Standard input handle constant. - /// - public const int STD_INPUT_HANDLE = -10; + [Flags] + public enum ButtonState + { + NoButtonPressed = 0, + Button1Pressed = 1, + Button2Pressed = 4, + Button3Pressed = 8, + Button4Pressed = 16, + RightmostButtonPressed = 2 + } /// /// Windows Console mode flags. @@ -30,53 +36,6 @@ public class WindowsConsole EnableExtendedFlags = 128 } - /// - /// Key event record structure. - /// - [StructLayout (LayoutKind.Explicit, CharSet = CharSet.Unicode)] - public struct KeyEventRecord - { - [FieldOffset (0)] - [MarshalAs (UnmanagedType.Bool)] - public bool bKeyDown; - - [FieldOffset (4)] - [MarshalAs (UnmanagedType.U2)] - public ushort wRepeatCount; - - [FieldOffset (6)] - [MarshalAs (UnmanagedType.U2)] - public ConsoleKeyMapping.VK wVirtualKeyCode; - - [FieldOffset (8)] - [MarshalAs (UnmanagedType.U2)] - public ushort wVirtualScanCode; - - [FieldOffset (10)] - public char UnicodeChar; - - [FieldOffset (12)] - [MarshalAs (UnmanagedType.U4)] - public ControlKeyState dwControlKeyState; - - public readonly override string ToString () - { - return - $"[KeyEventRecord({(bKeyDown ? "down" : "up")},{wRepeatCount},{wVirtualKeyCode},{wVirtualScanCode},{new Rune (UnicodeChar).MakePrintable ()},{dwControlKeyState})]"; - } - } - - [Flags] - public enum ButtonState - { - NoButtonPressed = 0, - Button1Pressed = 1, - Button2Pressed = 4, - Button3Pressed = 8, - Button4Pressed = 16, - RightmostButtonPressed = 2 - } - [Flags] public enum ControlKeyState { @@ -102,43 +61,6 @@ public class WindowsConsole MouseHorizontalWheeled = 8 } - [StructLayout (LayoutKind.Explicit)] - public struct MouseEventRecord - { - [FieldOffset (0)] - public Coord MousePosition; - - [FieldOffset (4)] - public ButtonState ButtonState; - - [FieldOffset (8)] - public ControlKeyState ControlKeyState; - - [FieldOffset (12)] - public EventFlags EventFlags; - - public readonly override string ToString () { return $"[Mouse{MousePosition},{ButtonState},{ControlKeyState},{EventFlags}]"; } - } - - public struct WindowBufferSizeRecord (short x, short y) - { - public Coord _size = new (x, y); - - public readonly override string ToString () { return $"[WindowBufferSize{_size}"; } - } - - [StructLayout (LayoutKind.Sequential)] - public struct MenuEventRecord - { - public uint dwCommandId; - } - - [StructLayout (LayoutKind.Sequential)] - public struct FocusEventRecord - { - public uint bSetFocus; - } - public enum EventType : ushort { Focus = 0x10, @@ -148,6 +70,170 @@ public class WindowsConsole WindowBufferSize = 4 } + /// + /// Standard input handle constant. + /// + public const int STD_INPUT_HANDLE = -10; + + public static ConsoleKeyInfoEx ToConsoleKeyInfoEx (KeyEventRecord keyEvent) + { + ControlKeyState state = keyEvent.dwControlKeyState; + + bool shift = (state & ControlKeyState.ShiftPressed) != 0; + bool alt = (state & (ControlKeyState.LeftAltPressed | ControlKeyState.RightAltPressed)) != 0; + bool control = (state & (ControlKeyState.LeftControlPressed | ControlKeyState.RightControlPressed)) != 0; + bool capslock = (state & ControlKeyState.CapslockOn) != 0; + bool numlock = (state & ControlKeyState.NumlockOn) != 0; + bool scrolllock = (state & ControlKeyState.ScrolllockOn) != 0; + + var cki = new ConsoleKeyInfo (keyEvent.UnicodeChar, (ConsoleKey)keyEvent.wVirtualKeyCode, shift, alt, control); + + return new (cki, capslock, numlock, scrolllock); + } + + [StructLayout (LayoutKind.Explicit, CharSet = CharSet.Unicode)] + public struct CharInfo + { + [FieldOffset (0)] + public CharUnion Char; + + [FieldOffset (2)] + public ushort Attributes; + } + + [StructLayout (LayoutKind.Explicit, CharSet = CharSet.Unicode)] + public struct CharUnion + { + [FieldOffset (0)] + public char UnicodeChar; + + [FieldOffset (0)] + public byte AsciiChar; + } + + [StructLayout (LayoutKind.Explicit, Size = 4)] + public struct COLORREF + { + public COLORREF (byte r, byte g, byte b) + { + Value = 0; + R = r; + G = g; + B = b; + } + + public COLORREF (uint value) + { + R = 0; + G = 0; + B = 0; + Value = value & 0x00FFFFFF; + } + + [FieldOffset (0)] + public byte R; + + [FieldOffset (1)] + public byte G; + + [FieldOffset (2)] + public byte B; + + [FieldOffset (0)] + public uint Value; + } + + // See: https://github.com/gui-cs/Terminal.Gui/issues/357 + + [StructLayout (LayoutKind.Sequential)] + public struct CONSOLE_SCREEN_BUFFER_INFOEX + { + public uint cbSize; + public Coord dwSize; + public Coord dwCursorPosition; + public ushort wAttributes; + public SmallRect srWindow; + public Coord dwMaximumWindowSize; + public ushort wPopupAttributes; + public bool bFullscreenSupported; + + [MarshalAs (UnmanagedType.ByValArray, SizeConst = 16)] + public COLORREF [] ColorTable; + } + + [StructLayout (LayoutKind.Sequential)] + public struct ConsoleCursorInfo + { + /// + /// The percentage of the character cell that is filled by the cursor.This value is between 1 and 100. + /// The cursor appearance varies, ranging from completely filling the cell to showing up as a horizontal + /// line at the bottom of the cell. + /// + public uint dwSize; + + public bool bVisible; + } + + [StructLayout (LayoutKind.Sequential)] + public struct ConsoleKeyInfoEx + { + public ConsoleKeyInfoEx (ConsoleKeyInfo consoleKeyInfo, bool capslock, bool numlock, bool scrolllock) + { + ConsoleKeyInfo = consoleKeyInfo; + CapsLock = capslock; + NumLock = numlock; + ScrollLock = scrolllock; + } + + public ConsoleKeyInfo ConsoleKeyInfo; + public bool CapsLock; + public bool NumLock; + public bool ScrollLock; + + /// + /// Prints a ConsoleKeyInfoEx structure + /// + /// + /// + public readonly string ToString (ConsoleKeyInfoEx ex) + { + var ke = new Key ((KeyCode)ex.ConsoleKeyInfo.KeyChar); + var sb = new StringBuilder (); + sb.Append ($"Key: {(KeyCode)ex.ConsoleKeyInfo.Key} ({ex.ConsoleKeyInfo.Key})"); + sb.Append ((ex.ConsoleKeyInfo.Modifiers & ConsoleModifiers.Shift) != 0 ? " | Shift" : string.Empty); + sb.Append ((ex.ConsoleKeyInfo.Modifiers & ConsoleModifiers.Control) != 0 ? " | Control" : string.Empty); + sb.Append ((ex.ConsoleKeyInfo.Modifiers & ConsoleModifiers.Alt) != 0 ? " | Alt" : string.Empty); + sb.Append ($", KeyChar: {ke.AsRune.MakePrintable ()} ({(uint)ex.ConsoleKeyInfo.KeyChar}) "); + sb.Append (ex.CapsLock ? "caps," : string.Empty); + sb.Append (ex.NumLock ? "num," : string.Empty); + sb.Append (ex.ScrollLock ? "scroll," : string.Empty); + string s = sb.ToString ().TrimEnd (',').TrimEnd (' '); + + return $"[ConsoleKeyInfoEx({s})]"; + } + } + + [StructLayout (LayoutKind.Sequential)] + public struct Coord + { + public Coord (short x, short y) + { + X = x; + Y = y; + } + + public short X; + public short Y; + + public readonly override string ToString () { return $"({X},{Y})"; } + } + + [StructLayout (LayoutKind.Sequential)] + public struct FocusEventRecord + { + public uint bSetFocus; + } + [StructLayout (LayoutKind.Explicit)] public struct InputRecord { @@ -183,49 +269,69 @@ public class WindowsConsole } } - [StructLayout (LayoutKind.Sequential)] - public struct Coord - { - public short X; - public short Y; - - public Coord (short x, short y) - { - X = x; - Y = y; - } - - public readonly override string ToString () { return $"({X},{Y})"; } - } - + /// + /// Key event record structure. + /// [StructLayout (LayoutKind.Explicit, CharSet = CharSet.Unicode)] - public struct CharUnion + public struct KeyEventRecord { [FieldOffset (0)] + [MarshalAs (UnmanagedType.Bool)] + public bool bKeyDown; + + [FieldOffset (4)] + [MarshalAs (UnmanagedType.U2)] + public ushort wRepeatCount; + + [FieldOffset (6)] + [MarshalAs (UnmanagedType.U2)] + public VK wVirtualKeyCode; + + [FieldOffset (8)] + [MarshalAs (UnmanagedType.U2)] + public ushort wVirtualScanCode; + + [FieldOffset (10)] public char UnicodeChar; - [FieldOffset (0)] - public byte AsciiChar; + [FieldOffset (12)] + [MarshalAs (UnmanagedType.U4)] + public ControlKeyState dwControlKeyState; + + public readonly override string ToString () + { + return + $"[KeyEventRecord({(bKeyDown ? "down" : "up")},{wRepeatCount},{wVirtualKeyCode},{wVirtualScanCode},{new Rune (UnicodeChar).MakePrintable ()},{dwControlKeyState})]"; + } } - [StructLayout (LayoutKind.Explicit, CharSet = CharSet.Unicode)] - public struct CharInfo + [StructLayout (LayoutKind.Sequential)] + public struct MenuEventRecord + { + public uint dwCommandId; + } + + [StructLayout (LayoutKind.Explicit)] + public struct MouseEventRecord { [FieldOffset (0)] - public CharUnion Char; + public Coord MousePosition; - [FieldOffset (2)] - public ushort Attributes; + [FieldOffset (4)] + public ButtonState ButtonState; + + [FieldOffset (8)] + public ControlKeyState ControlKeyState; + + [FieldOffset (12)] + public EventFlags EventFlags; + + public readonly override string ToString () { return $"[Mouse{MousePosition},{ButtonState},{ControlKeyState},{EventFlags}]"; } } [StructLayout (LayoutKind.Sequential)] public struct SmallRect { - public short Left; - public short Top; - public short Right; - public short Bottom; - public SmallRect (short left, short top, short right, short bottom) { Left = left; @@ -234,8 +340,15 @@ public class WindowsConsole Bottom = bottom; } + public short Left; + public short Top; + public short Right; + public short Bottom; + public static void MakeEmpty (ref SmallRect rect) { rect.Left = -1; } + public readonly override string ToString () { return $"Left={Left},Top={Top},Right={Right},Bottom={Bottom}"; } + public static void Update (ref SmallRect rect, short col, short row) { if (rect.Left == -1) @@ -271,109 +384,12 @@ public class WindowsConsole rect.Bottom = row; } } - - public readonly override string ToString () { return $"Left={Left},Top={Top},Right={Right},Bottom={Bottom}"; } } - [StructLayout (LayoutKind.Sequential)] - public struct ConsoleKeyInfoEx + public struct WindowBufferSizeRecord (short x, short y) { - public ConsoleKeyInfo ConsoleKeyInfo; - public bool CapsLock; - public bool NumLock; - public bool ScrollLock; + public Coord _size = new (x, y); - public ConsoleKeyInfoEx (ConsoleKeyInfo consoleKeyInfo, bool capslock, bool numlock, bool scrolllock) - { - ConsoleKeyInfo = consoleKeyInfo; - CapsLock = capslock; - NumLock = numlock; - ScrollLock = scrolllock; - } - - /// - /// Prints a ConsoleKeyInfoEx structure - /// - /// - /// - public readonly string ToString (ConsoleKeyInfoEx ex) - { - var ke = new Key ((KeyCode)ex.ConsoleKeyInfo.KeyChar); - var sb = new StringBuilder (); - sb.Append ($"Key: {(KeyCode)ex.ConsoleKeyInfo.Key} ({ex.ConsoleKeyInfo.Key})"); - sb.Append ((ex.ConsoleKeyInfo.Modifiers & ConsoleModifiers.Shift) != 0 ? " | Shift" : string.Empty); - sb.Append ((ex.ConsoleKeyInfo.Modifiers & ConsoleModifiers.Control) != 0 ? " | Control" : string.Empty); - sb.Append ((ex.ConsoleKeyInfo.Modifiers & ConsoleModifiers.Alt) != 0 ? " | Alt" : string.Empty); - sb.Append ($", KeyChar: {ke.AsRune.MakePrintable ()} ({(uint)ex.ConsoleKeyInfo.KeyChar}) "); - sb.Append (ex.CapsLock ? "caps," : string.Empty); - sb.Append (ex.NumLock ? "num," : string.Empty); - sb.Append (ex.ScrollLock ? "scroll," : string.Empty); - string s = sb.ToString ().TrimEnd (',').TrimEnd (' '); - - return $"[ConsoleKeyInfoEx({s})]"; - } - } - - [StructLayout (LayoutKind.Sequential)] - public struct ConsoleCursorInfo - { - /// - /// The percentage of the character cell that is filled by the cursor.This value is between 1 and 100. - /// The cursor appearance varies, ranging from completely filling the cell to showing up as a horizontal - /// line at the bottom of the cell. - /// - public uint dwSize; - - public bool bVisible; - } - - // See: https://github.com/gui-cs/Terminal.Gui/issues/357 - - [StructLayout (LayoutKind.Sequential)] - public struct CONSOLE_SCREEN_BUFFER_INFOEX - { - public uint cbSize; - public Coord dwSize; - public Coord dwCursorPosition; - public ushort wAttributes; - public SmallRect srWindow; - public Coord dwMaximumWindowSize; - public ushort wPopupAttributes; - public bool bFullscreenSupported; - - [MarshalAs (UnmanagedType.ByValArray, SizeConst = 16)] - public COLORREF [] ColorTable; - } - - [StructLayout (LayoutKind.Explicit, Size = 4)] - public struct COLORREF - { - public COLORREF (byte r, byte g, byte b) - { - Value = 0; - R = r; - G = g; - B = b; - } - - public COLORREF (uint value) - { - R = 0; - G = 0; - B = 0; - Value = value & 0x00FFFFFF; - } - - [FieldOffset (0)] - public byte R; - - [FieldOffset (1)] - public byte G; - - [FieldOffset (2)] - public byte B; - - [FieldOffset (0)] - public uint Value; + public readonly override string ToString () { return $"[WindowBufferSize{_size}"; } } } diff --git a/Terminal.Gui/Drivers/WindowsDriver/WindowsInput.cs b/Terminal.Gui/Drivers/WindowsDriver/WindowsInput.cs index 12ef0c15d..026e1f61b 100644 --- a/Terminal.Gui/Drivers/WindowsDriver/WindowsInput.cs +++ b/Terminal.Gui/Drivers/WindowsDriver/WindowsInput.cs @@ -5,7 +5,7 @@ using static Terminal.Gui.Drivers.WindowsConsole; namespace Terminal.Gui.Drivers; -internal class WindowsInput : ConsoleInput, IWindowsInput +internal class WindowsInput : InputImpl, IWindowsInput { private readonly nint _inputHandle; @@ -43,30 +43,27 @@ internal class WindowsInput : ConsoleInput, IWindowsInput { Logging.Logger.LogInformation ($"Creating {nameof (WindowsInput)}"); - if (ConsoleDriver.RunningUnitTests) + try { - return; + _inputHandle = GetStdHandle (STD_INPUT_HANDLE); + + GetConsoleMode (_inputHandle, out uint v); + _originalConsoleMode = v; + + uint newConsoleMode = _originalConsoleMode; + newConsoleMode |= (uint)(ConsoleModes.EnableMouseInput | ConsoleModes.EnableExtendedFlags); + newConsoleMode &= ~(uint)ConsoleModes.EnableQuickEditMode; + newConsoleMode &= ~(uint)ConsoleModes.EnableProcessedInput; + SetConsoleMode (_inputHandle, newConsoleMode); + } + catch + { + // ignore errors during unit tests } - - _inputHandle = GetStdHandle (STD_INPUT_HANDLE); - - GetConsoleMode (_inputHandle, out uint v); - _originalConsoleMode = v; - - uint newConsoleMode = _originalConsoleMode; - newConsoleMode |= (uint)(ConsoleModes.EnableMouseInput | ConsoleModes.EnableExtendedFlags); - newConsoleMode &= ~(uint)ConsoleModes.EnableQuickEditMode; - newConsoleMode &= ~(uint)ConsoleModes.EnableProcessedInput; - SetConsoleMode (_inputHandle, newConsoleMode); } - protected override bool Peek () + public override bool Peek () { - if (ConsoleDriver.RunningUnitTests) - { - return false; - } - const int BUFFER_SIZE = 1; // We only need to check if there's at least one event nint pRecord = Marshal.AllocHGlobal (Marshal.SizeOf () * BUFFER_SIZE); @@ -87,7 +84,7 @@ internal class WindowsInput : ConsoleInput, IWindowsInput catch (Exception ex) { // Optionally log the exception - Console.WriteLine (@$"Error in Peek: {ex.Message}"); + Logging.Error (@$"Error in Peek: {ex.Message}"); return false; } @@ -98,7 +95,7 @@ internal class WindowsInput : ConsoleInput, IWindowsInput } } - protected override IEnumerable Read () + public override IEnumerable Read () { const int BUFFER_SIZE = 1; nint pRecord = Marshal.AllocHGlobal (Marshal.SizeOf () * BUFFER_SIZE); @@ -127,16 +124,18 @@ internal class WindowsInput : ConsoleInput, IWindowsInput public override void Dispose () { - if (ConsoleDriver.RunningUnitTests) + try { - return; - } + if (!FlushConsoleInputBuffer (_inputHandle)) + { + throw new ApplicationException ($"Failed to flush input buffer, error code: {Marshal.GetLastWin32Error ()}."); + } - if (!FlushConsoleInputBuffer (_inputHandle)) + SetConsoleMode (_inputHandle, _originalConsoleMode); + } + catch { - throw new ApplicationException ($"Failed to flush input buffer, error code: {Marshal.GetLastWin32Error ()}."); + // ignore errors during unit tests } - - SetConsoleMode (_inputHandle, _originalConsoleMode); } } diff --git a/Terminal.Gui/Drivers/WindowsDriver/WindowsInputProcessor.cs b/Terminal.Gui/Drivers/WindowsDriver/WindowsInputProcessor.cs index 4d40146d2..028eb4dc1 100644 --- a/Terminal.Gui/Drivers/WindowsDriver/WindowsInputProcessor.cs +++ b/Terminal.Gui/Drivers/WindowsDriver/WindowsInputProcessor.cs @@ -8,14 +8,24 @@ using InputRecord = WindowsConsole.InputRecord; /// /// Input processor for , deals in stream. /// -internal class WindowsInputProcessor : InputProcessor +internal class WindowsInputProcessor : InputProcessorImpl { - private readonly bool [] _lastWasPressed = new bool[4]; + private readonly bool [] _lastWasPressed = new bool [4]; /// public WindowsInputProcessor (ConcurrentQueue inputBuffer) : base (inputBuffer, new WindowsKeyConverter ()) { - DriverName = "Windows"; + DriverName = "windows"; + } + + /// + public override void EnqueueMouseEvent (MouseEventArgs mouseEvent) + { + InputQueue.Enqueue (new () + { + EventType = WindowsConsole.EventType.Mouse, + MouseEvent = ToMouseEventRecord (mouseEvent) + }); } /// @@ -25,6 +35,7 @@ internal class WindowsInputProcessor : InputProcessor { case WindowsConsole.EventType.Key: + // TODO: v1 supported distinct key up/down events on Windows. // TODO: For now ignore keyup because ANSI comes in as down+up which is confusing to try and parse/pair these things up if (!inputEvent.KeyEvent.bKeyDown) { @@ -61,28 +72,20 @@ internal class WindowsInputProcessor : InputProcessor break; case WindowsConsole.EventType.Mouse: - MouseEventArgs me = ToDriverMouse (inputEvent.MouseEvent); + MouseEventArgs me = ToMouseEvent (inputEvent.MouseEvent); - OnMouseEvent (me); + RaiseMouseEvent (me); break; } } - /// - protected override void ProcessAfterParsing (InputRecord 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!); - } - } - - public MouseEventArgs ToDriverMouse (WindowsConsole.MouseEventRecord e) + /// + /// Converts a Windows-specific mouse event to a . + /// + /// + /// + public MouseEventArgs ToMouseEvent (WindowsConsole.MouseEventRecord e) { var mouseFlags = MouseFlags.None; @@ -209,4 +212,79 @@ internal class WindowsInputProcessor : InputProcessor return current; } + + /// + /// Converts a to a Windows-specific . + /// + /// + /// + public WindowsConsole.MouseEventRecord ToMouseEventRecord (MouseEventArgs mouseEvent) + { + var buttonState = WindowsConsole.ButtonState.NoButtonPressed; + var eventFlags = WindowsConsole.EventFlags.NoEvent; + var controlKeyState = WindowsConsole.ControlKeyState.NoControlKeyPressed; + + // Convert button states + if (mouseEvent.Flags.HasFlag (MouseFlags.Button1Pressed)) + { + buttonState |= WindowsConsole.ButtonState.Button1Pressed; + } + + if (mouseEvent.Flags.HasFlag (MouseFlags.Button2Pressed)) + { + buttonState |= WindowsConsole.ButtonState.Button2Pressed; + } + + if (mouseEvent.Flags.HasFlag (MouseFlags.Button3Pressed)) + { + buttonState |= WindowsConsole.ButtonState.Button3Pressed; + } + + if (mouseEvent.Flags.HasFlag (MouseFlags.Button4Pressed)) + { + buttonState |= WindowsConsole.ButtonState.Button4Pressed; + } + + // Convert mouse wheel events + if (mouseEvent.Flags.HasFlag (MouseFlags.WheeledUp)) + { + eventFlags = WindowsConsole.EventFlags.MouseWheeled; + buttonState = (WindowsConsole.ButtonState)0x00780000; // Positive value for wheel up + } + else if (mouseEvent.Flags.HasFlag (MouseFlags.WheeledDown)) + { + eventFlags = WindowsConsole.EventFlags.MouseWheeled; + buttonState = (WindowsConsole.ButtonState)unchecked((int)0xFF880000); // Negative value for wheel down + } + + // Convert movement flag + if (mouseEvent.Flags.HasFlag (MouseFlags.ReportMousePosition)) + { + eventFlags |= WindowsConsole.EventFlags.MouseMoved; + } + + // Convert modifier keys + if (mouseEvent.Flags.HasFlag (MouseFlags.ButtonAlt)) + { + controlKeyState |= WindowsConsole.ControlKeyState.LeftAltPressed; + } + + if (mouseEvent.Flags.HasFlag (MouseFlags.ButtonCtrl)) + { + controlKeyState |= WindowsConsole.ControlKeyState.LeftControlPressed; + } + + if (mouseEvent.Flags.HasFlag (MouseFlags.ButtonShift)) + { + controlKeyState |= WindowsConsole.ControlKeyState.ShiftPressed; + } + + return new WindowsConsole.MouseEventRecord + { + MousePosition = new WindowsConsole.Coord ((short)mouseEvent.Position.X, (short)mouseEvent.Position.Y), + ButtonState = buttonState, + ControlKeyState = controlKeyState, + EventFlags = eventFlags + }; + } } diff --git a/Terminal.Gui/Drivers/WindowsDriver/WindowsKeyConverter.cs b/Terminal.Gui/Drivers/WindowsDriver/WindowsKeyConverter.cs index 3b447333e..4458ad6ff 100644 --- a/Terminal.Gui/Drivers/WindowsDriver/WindowsKeyConverter.cs +++ b/Terminal.Gui/Drivers/WindowsDriver/WindowsKeyConverter.cs @@ -1,6 +1,5 @@ #nullable enable - namespace Terminal.Gui.Drivers; /// @@ -14,7 +13,7 @@ internal class WindowsKeyConverter : IKeyConverter /// public Key ToKey (WindowsConsole.InputRecord inputEvent) { - if (inputEvent.KeyEvent.wVirtualKeyCode == (ConsoleKeyMapping.VK)ConsoleKey.Packet) + if (inputEvent.KeyEvent.wVirtualKeyCode == (VK)ConsoleKey.Packet) { // Used to pass Unicode characters as if they were keystrokes. // The VK_PACKET key is the low word of a 32-bit @@ -22,7 +21,7 @@ internal class WindowsKeyConverter : IKeyConverter inputEvent.KeyEvent = WindowsKeyHelper.FromVKPacketToKeyEventRecord (inputEvent.KeyEvent); } - var keyInfo = WindowsKeyHelper.ToConsoleKeyInfoEx (inputEvent.KeyEvent); + var keyInfo = WindowsConsole.ToConsoleKeyInfoEx (inputEvent.KeyEvent); //Debug.WriteLine ($"event: KBD: {GetKeyboardLayoutName()} {inputEvent.ToString ()} {keyInfo.ToString (keyInfo)}"); @@ -30,9 +29,156 @@ internal class WindowsKeyConverter : IKeyConverter if (map == KeyCode.Null) { - return (Key)0; + return 0; } return new (map); } + + /// + public WindowsConsole.InputRecord ToKeyInfo (Key key) + { + // Convert Key to ConsoleKeyInfo using the cross-platform mapping utility + ConsoleKeyInfo consoleKeyInfo = ConsoleKeyMapping.GetConsoleKeyInfoFromKeyCode (key.KeyCode); + + // Build the ControlKeyState from the ConsoleKeyInfo modifiers + var controlKeyState = WindowsConsole.ControlKeyState.NoControlKeyPressed; + + if (consoleKeyInfo.Modifiers.HasFlag (ConsoleModifiers.Shift)) + { + controlKeyState |= WindowsConsole.ControlKeyState.ShiftPressed; + } + + if (consoleKeyInfo.Modifiers.HasFlag (ConsoleModifiers.Alt)) + { + controlKeyState |= WindowsConsole.ControlKeyState.LeftAltPressed; + } + + if (consoleKeyInfo.Modifiers.HasFlag (ConsoleModifiers.Control)) + { + controlKeyState |= WindowsConsole.ControlKeyState.LeftControlPressed; + } + + // Get the scan code using Windows API if available, otherwise use a simple heuristic + ushort scanCode = GetScanCodeForKey (consoleKeyInfo.Key); + + // Create a KeyEventRecord with the converted values + var keyEvent = new WindowsConsole.KeyEventRecord + { + bKeyDown = true, // Assume key down for conversion + wRepeatCount = 1, + wVirtualKeyCode = (VK)consoleKeyInfo.Key, + wVirtualScanCode = scanCode, + UnicodeChar = consoleKeyInfo.KeyChar, + dwControlKeyState = controlKeyState + }; + + // Create and return an InputRecord with the keyboard event + return new() + { + EventType = WindowsConsole.EventType.Key, + KeyEvent = keyEvent + }; + } + + /// + /// Gets the hardware scan code for a given ConsoleKey. + /// + /// The ConsoleKey to get the scan code for. + /// The scan code, or 0 if not available. + /// + /// On Windows, uses MapVirtualKey to get the actual scan code from the OS. + /// This respects the current keyboard layout and is more accurate than a static lookup table. + /// For test simulation purposes, returning 0 is acceptable as Windows doesn't strictly require it. + /// + private static ushort GetScanCodeForKey (ConsoleKey key) + { + //// For test simulation, scan codes aren't critical. However, we can use the Windows API + //// to get the correct scan code if we're running on Windows. + //if (Environment.OSVersion.Platform == PlatformID.Win32NT) + //{ + // try + // { + // // MapVirtualKey with MAPVK_VK_TO_VSC (0) converts VK to scan code + // // This uses the current keyboard layout, so it's more accurate than a static table + // uint scanCodeExtended = WindowsKeyboardLayout.MapVirtualKey ((VK)key, 0); + + // // The scan code is in the low byte + // return (ushort)(scanCodeExtended & 0xFF); + // } + // catch + // { + // // If MapVirtualKey fails, fall back to simple heuristic + // } + //} + + // Fallback: Use a simple heuristic for common keys + // For most test scenarios, these values work fine + return key switch + { + ConsoleKey.Escape => 1, + ConsoleKey.D1 => 2, + ConsoleKey.D2 => 3, + ConsoleKey.D3 => 4, + ConsoleKey.D4 => 5, + ConsoleKey.D5 => 6, + ConsoleKey.D6 => 7, + ConsoleKey.D7 => 8, + ConsoleKey.D8 => 9, + ConsoleKey.D9 => 10, + ConsoleKey.D0 => 11, + ConsoleKey.Tab => 15, + ConsoleKey.Q => 16, + ConsoleKey.W => 17, + ConsoleKey.E => 18, + ConsoleKey.R => 19, + ConsoleKey.T => 20, + ConsoleKey.Y => 21, + ConsoleKey.U => 22, + ConsoleKey.I => 23, + ConsoleKey.O => 24, + ConsoleKey.P => 25, + ConsoleKey.Enter => 28, + ConsoleKey.A => 30, + ConsoleKey.S => 31, + ConsoleKey.D => 32, + ConsoleKey.F => 33, + ConsoleKey.G => 34, + ConsoleKey.H => 35, + ConsoleKey.J => 36, + ConsoleKey.K => 37, + ConsoleKey.L => 38, + ConsoleKey.Z => 44, + ConsoleKey.X => 45, + ConsoleKey.C => 46, + ConsoleKey.V => 47, + ConsoleKey.B => 48, + ConsoleKey.N => 49, + ConsoleKey.M => 50, + ConsoleKey.Spacebar => 57, + ConsoleKey.F1 => 59, + ConsoleKey.F2 => 60, + ConsoleKey.F3 => 61, + ConsoleKey.F4 => 62, + ConsoleKey.F5 => 63, + ConsoleKey.F6 => 64, + ConsoleKey.F7 => 65, + ConsoleKey.F8 => 66, + ConsoleKey.F9 => 67, + ConsoleKey.F10 => 68, + ConsoleKey.Home => 71, + ConsoleKey.UpArrow => 72, + ConsoleKey.PageUp => 73, + ConsoleKey.LeftArrow => 75, + ConsoleKey.RightArrow => 77, + ConsoleKey.End => 79, + ConsoleKey.DownArrow => 80, + ConsoleKey.PageDown => 81, + ConsoleKey.Insert => 82, + ConsoleKey.Delete => 83, + ConsoleKey.F11 => 87, + ConsoleKey.F12 => 88, + _ => 0 // Unknown or not needed for test simulation + }; + } } diff --git a/Terminal.Gui/Drivers/WindowsDriver/WindowsKeyHelper.cs b/Terminal.Gui/Drivers/WindowsDriver/WindowsKeyHelper.cs index 29d7d4ff9..56193db21 100644 --- a/Terminal.Gui/Drivers/WindowsDriver/WindowsKeyHelper.cs +++ b/Terminal.Gui/Drivers/WindowsDriver/WindowsKeyHelper.cs @@ -1,78 +1,66 @@ #nullable enable using System.Diagnostics; +// ReSharper disable InconsistentNaming namespace Terminal.Gui.Drivers; /// -/// Helper class for Windows key conversion utilities. -/// Contains static methods extracted from the legacy WindowsDriver for key processing. +/// Helper class for Windows key conversion utilities. +/// Contains static methods extracted from the legacy WindowsDriver for key processing. /// internal static class WindowsKeyHelper { + /// + /// Converts a key event record with a virtual key code of Packet to a corresponding key event record with updated + /// key information. + /// + /// + /// This method is typically used to interpret Packet key events, which may represent input from + /// IMEs or other sources that generate Unicode characters not directly mapped to standard virtual key codes. The + /// returned record will have its key and scan code fields updated to reflect the decoded character and + /// modifiers. + /// + /// + /// The key event record to convert. If the virtual key code is not Packet, the original record is returned + /// unchanged. + /// + /// + /// A new key event record with updated key, scan code, and character information if the input represents a Packet + /// key; otherwise, the original key event record. + /// public static WindowsConsole.KeyEventRecord FromVKPacketToKeyEventRecord (WindowsConsole.KeyEventRecord keyEvent) { - if (keyEvent.wVirtualKeyCode != (ConsoleKeyMapping.VK)ConsoleKey.Packet) + if (keyEvent.wVirtualKeyCode != (VK)ConsoleKey.Packet) { return keyEvent; } - var mod = new ConsoleModifiers (); - - if (keyEvent.dwControlKeyState.HasFlag (WindowsConsole.ControlKeyState.ShiftPressed)) + // VK_PACKET means Windows is giving us a Unicode character without a virtual key. + // The character is already in UnicodeChar - we don't need to decode anything. + // We set VK to None and scan code to 0 since they're meaningless for VK_PACKET. + return new () { - mod |= ConsoleModifiers.Shift; - } - - if (keyEvent.dwControlKeyState.HasFlag (WindowsConsole.ControlKeyState.RightAltPressed) - || keyEvent.dwControlKeyState.HasFlag (WindowsConsole.ControlKeyState.LeftAltPressed)) - { - mod |= ConsoleModifiers.Alt; - } - - if (keyEvent.dwControlKeyState.HasFlag (WindowsConsole.ControlKeyState.LeftControlPressed) - || keyEvent.dwControlKeyState.HasFlag (WindowsConsole.ControlKeyState.RightControlPressed)) - { - mod |= ConsoleModifiers.Control; - } - - var cKeyInfo = new ConsoleKeyInfo ( - keyEvent.UnicodeChar, - (ConsoleKey)keyEvent.wVirtualKeyCode, - mod.HasFlag (ConsoleModifiers.Shift), - mod.HasFlag (ConsoleModifiers.Alt), - mod.HasFlag (ConsoleModifiers.Control)); - cKeyInfo = ConsoleKeyMapping.DecodeVKPacketToKConsoleKeyInfo (cKeyInfo); - uint scanCode = ConsoleKeyMapping.GetScanCodeFromConsoleKeyInfo (cKeyInfo); - - return new WindowsConsole.KeyEventRecord - { - UnicodeChar = cKeyInfo.KeyChar, + UnicodeChar = keyEvent.UnicodeChar, // Keep the character - this is the key info! bKeyDown = keyEvent.bKeyDown, - dwControlKeyState = keyEvent.dwControlKeyState, + dwControlKeyState = keyEvent.dwControlKeyState, // Keep modifiers wRepeatCount = keyEvent.wRepeatCount, - wVirtualKeyCode = (ConsoleKeyMapping.VK)cKeyInfo.Key, - wVirtualScanCode = (ushort)scanCode + wVirtualKeyCode = (VK)ConsoleKey.None, // No virtual key for VK_PACKET + wVirtualScanCode = 0 // No scan code for VK_PACKET }; } - public static WindowsConsole.ConsoleKeyInfoEx ToConsoleKeyInfoEx (WindowsConsole.KeyEventRecord keyEvent) - { - WindowsConsole.ControlKeyState state = keyEvent.dwControlKeyState; - bool shift = (state & WindowsConsole.ControlKeyState.ShiftPressed) != 0; - bool alt = (state & (WindowsConsole.ControlKeyState.LeftAltPressed | WindowsConsole.ControlKeyState.RightAltPressed)) != 0; - bool control = (state & (WindowsConsole.ControlKeyState.LeftControlPressed | WindowsConsole.ControlKeyState.RightControlPressed)) != 0; - bool capslock = (state & WindowsConsole.ControlKeyState.CapslockOn) != 0; - bool numlock = (state & WindowsConsole.ControlKeyState.NumlockOn) != 0; - bool scrolllock = (state & WindowsConsole.ControlKeyState.ScrolllockOn) != 0; - - var cki = new ConsoleKeyInfo (keyEvent.UnicodeChar, (ConsoleKey)keyEvent.wVirtualKeyCode, shift, alt, control); - - return new WindowsConsole.ConsoleKeyInfoEx (cki, capslock, numlock, scrolllock); - } public static KeyCode MapKey (WindowsConsole.ConsoleKeyInfoEx keyInfoEx) { ConsoleKeyInfo keyInfo = keyInfoEx.ConsoleKeyInfo; + // Handle VK_PACKET / None - character-only input (IME, emoji, etc.) + if (keyInfo.Key == ConsoleKey.None && keyInfo.KeyChar != 0) + { + // This is a character from VK_PACKET (IME, emoji picker, etc.) + // Just return the character as-is with modifiers + return ConsoleKeyMapping.MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)keyInfo.KeyChar); + } + switch (keyInfo.Key) { case ConsoleKey.D0: @@ -116,7 +104,7 @@ internal static class WindowsKeyHelper case ConsoleKey.OemMinus: // These virtual key codes are mapped differently depending on the keyboard layout in use. // We use the Win32 API to map them to the correct character. - uint mapResult = ConsoleKeyMapping.MapVKtoChar ((ConsoleKeyMapping.VK)keyInfo.Key); + uint mapResult = WindowsKeyboardLayout.MapVKtoChar ((VK)keyInfo.Key); if (mapResult == 0) { @@ -162,13 +150,13 @@ internal static class WindowsKeyHelper // returned (e.g. on ENG OemPlus un-shifted is =, not +). This is important // for key persistence ("Ctrl++" vs. "Ctrl+="). mappedChar = keyInfo.Key switch - { - ConsoleKey.OemPeriod => '.', - ConsoleKey.OemComma => ',', - ConsoleKey.OemPlus => '+', - ConsoleKey.OemMinus => '-', - _ => mappedChar - }; + { + ConsoleKey.OemPeriod => '.', + ConsoleKey.OemComma => ',', + ConsoleKey.OemPlus => '+', + ConsoleKey.OemMinus => '-', + _ => mappedChar + }; } // Return the mappedChar with modifiers. Because mappedChar is un-shifted, if Shift was down @@ -245,17 +233,17 @@ internal static class WindowsKeyHelper if (Enum.IsDefined (typeof (KeyCode), (uint)keyInfo.Key)) { // If the key is JUST a modifier, return it as just that key - if (keyInfo.Key == (ConsoleKey)ConsoleKeyMapping.VK.SHIFT) + if (keyInfo.Key == (ConsoleKey)VK.SHIFT) { // Shift 16 return KeyCode.ShiftMask; } - if (keyInfo.Key == (ConsoleKey)ConsoleKeyMapping.VK.CONTROL) + if (keyInfo.Key == (ConsoleKey)VK.CONTROL) { // Ctrl 17 return KeyCode.CtrlMask; } - if (keyInfo.Key == (ConsoleKey)ConsoleKeyMapping.VK.MENU) + if (keyInfo.Key == (ConsoleKey)VK.MENU) { // Alt 18 return KeyCode.AltMask; } diff --git a/Terminal.Gui/Drivers/WindowsDriver/WindowsKeyboardLayout.cs b/Terminal.Gui/Drivers/WindowsDriver/WindowsKeyboardLayout.cs new file mode 100644 index 000000000..e368dfa8d --- /dev/null +++ b/Terminal.Gui/Drivers/WindowsDriver/WindowsKeyboardLayout.cs @@ -0,0 +1,109 @@ +using System.Runtime.InteropServices; + +namespace Terminal.Gui.Drivers; + +/// +/// Windows-specific keyboard layout information using P/Invoke. +/// This class encapsulates all Windows API calls for keyboard layout operations. +/// +internal static class WindowsKeyboardLayout +{ +#if !WT_ISSUE_8871_FIXED // https://github.com/microsoft/terminal/issues/8871 + /// + /// Translates (maps) a virtual-key code into a scan code or character value, or translates a scan code into a + /// virtual-key code. + /// + /// + /// + /// 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. + /// + /// + /// + /// 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. + /// + [DllImport ("user32.dll", EntryPoint = "MapVirtualKeyExW", CharSet = CharSet.Unicode)] + private static extern uint MapVirtualKeyEx (VK vk, uint uMapType, nint dwhkl); + + /// Retrieves the active input locale identifier (formerly called the keyboard layout). + /// 0 for current thread + /// + /// 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. + /// + [DllImport ("user32.dll", EntryPoint = "GetKeyboardLayout", CharSet = CharSet.Unicode)] + private static extern nint GetKeyboardLayout (nint idThread); + + [DllImport ("user32.dll")] + private static extern nint GetForegroundWindow (); + + [DllImport ("user32.dll")] + private static extern nint GetWindowThreadProcessId (nint hWnd, nint ProcessId); + + /// + /// Translates the specified virtual-key code and keyboard state to the corresponding Unicode character or + /// characters using the Win32 API MapVirtualKey. + /// + /// + /// + /// 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. + /// + 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 + /// + /// Translates (maps) a virtual-key code into a scan code or character value, or translates a scan code into a virtual-key code. + /// + /// + /// + /// 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. + /// + /// 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. + [DllImport ("user32.dll", EntryPoint = "MapVirtualKeyW", CharSet = CharSet.Unicode)] + private static extern uint MapVirtualKey (VK vk, uint uMapType = 2); + + public static uint MapVKtoChar (VK vk) => MapVirtualKey (vk, 2); +#endif + + /// + /// Retrieves the name of the active input locale identifier (formerly called the keyboard layout) for the calling + /// thread. + /// + /// + /// + [DllImport ("user32.dll")] + private static extern bool GetKeyboardLayoutName ([Out] StringBuilder pwszKLID); + + /// + /// Retrieves the name of the active input locale identifier (formerly called the keyboard layout) for the calling + /// thread. + /// + /// + public static string GetKeyboardLayoutName () + { + if (Environment.OSVersion.Platform != PlatformID.Win32NT) + { + return "none"; + } + + var klidSB = new StringBuilder (); + GetKeyboardLayoutName (klidSB); + + return klidSB.ToString (); + } +} diff --git a/Terminal.Gui/Drivers/WindowsDriver/WindowsOutput.cs b/Terminal.Gui/Drivers/WindowsDriver/WindowsOutput.cs index b04b9faa1..363090d7e 100644 --- a/Terminal.Gui/Drivers/WindowsDriver/WindowsOutput.cs +++ b/Terminal.Gui/Drivers/WindowsDriver/WindowsOutput.cs @@ -1,19 +1,18 @@ #nullable enable using System.ComponentModel; using System.Runtime.InteropServices; -using System.Text; using Microsoft.Extensions.Logging; namespace Terminal.Gui.Drivers; -internal partial class WindowsOutput : OutputBase, IConsoleOutput +internal partial class WindowsOutput : OutputBase, IOutput { [LibraryImport ("kernel32.dll", EntryPoint = "WriteConsoleW", SetLastError = true, StringMarshalling = StringMarshalling.Utf16)] [return: MarshalAs (UnmanagedType.Bool)] private static partial bool WriteConsole ( nint hConsoleOutput, - ReadOnlySpan lpbufer, - uint numberOfCharsToWriten, + ReadOnlySpan lpBuffer, + uint numberOfCharsToWrite, out uint lpNumberOfCharsWritten, nint lpReserved ); @@ -29,7 +28,7 @@ internal partial class WindowsOutput : OutputBase, IConsoleOutput private static partial nint CreateConsoleScreenBuffer ( DesiredAccess dwDesiredAccess, ShareMode dwShareMode, - nint secutiryAttributes, + nint securityAttributes, uint flags, nint screenBufferData ); @@ -107,7 +106,7 @@ internal partial class WindowsOutput : OutputBase, IConsoleOutput { Logging.Logger.LogInformation ($"Creating {nameof (WindowsOutput)}"); - if (ConsoleDriver.RunningUnitTests) + if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) { return; } @@ -151,17 +150,19 @@ internal partial class WindowsOutput : OutputBase, IConsoleOutput // Force 16 colors if not in virtual terminal mode. Application.Force16Colors = true; } + + GetSize (); } private void CreateScreenBuffer () { _screenBuffer = CreateConsoleScreenBuffer ( - DesiredAccess.GenericRead | DesiredAccess.GenericWrite, - ShareMode.FileShareRead | ShareMode.FileShareWrite, - nint.Zero, - 1, - nint.Zero - ); + DesiredAccess.GenericRead | DesiredAccess.GenericWrite, + ShareMode.FileShareRead | ShareMode.FileShareWrite, + nint.Zero, + 1, + nint.Zero + ); if (_screenBuffer == INVALID_HANDLE_VALUE) { @@ -181,7 +182,7 @@ internal partial class WindowsOutput : OutputBase, IConsoleOutput public void Write (ReadOnlySpan str) { - if (ConsoleDriver.RunningUnitTests) + if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) { return; } @@ -194,15 +195,26 @@ internal partial class WindowsOutput : OutputBase, IConsoleOutput public Size ResizeBuffer (Size size) { - Size newSize = SetConsoleWindow ( - (short)Math.Max (size.Width, 0), - (short)Math.Max (size.Height, 0)); + Size newSize = size; + try + { + newSize = SetConsoleWindow ((short)Math.Max (size.Width, 0), (short)Math.Max (size.Height, 0)); + } + catch + { + // Do nothing; unit tests + } return newSize; } internal Size SetConsoleWindow (short cols, short rows) { + if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) + { + return new (cols, rows); + } + var csbi = new WindowsConsole.CONSOLE_SCREEN_BUFFER_INFOEX (); csbi.cbSize = (uint)Marshal.SizeOf (csbi); @@ -240,7 +252,9 @@ internal partial class WindowsOutput : OutputBase, IConsoleOutput { if ((_isVirtualTerminal ? _outputHandle - : _screenBuffer) != nint.Zero && !SetConsoleScreenBufferInfoEx (_isVirtualTerminal ? _outputHandle : _screenBuffer, ref csbi)) + : _screenBuffer) + != nint.Zero + && !SetConsoleScreenBufferInfoEx (_isVirtualTerminal ? _outputHandle : _screenBuffer, ref csbi)) { throw new Win32Exception (Marshal.GetLastWin32Error ()); } @@ -253,6 +267,7 @@ internal partial class WindowsOutput : OutputBase, IConsoleOutput // for 16 color mode we will write to a backing buffer then flip it to the active one at the end to avoid jitter. _consoleBuffer = 0; + if (_force16Colors) { if (_isVirtualTerminal) @@ -279,9 +294,10 @@ internal partial class WindowsOutput : OutputBase, IConsoleOutput } else { - var span = _everythingStringBuilder.ToString ().AsSpan (); // still allocates the string + ReadOnlySpan span = _everythingStringBuilder.ToString ().AsSpan (); // still allocates the string + + bool result = WriteConsole (_consoleBuffer, span, (uint)span.Length, out _, nint.Zero); - var result = WriteConsole (_consoleBuffer, span, (uint)span.Length, out _, nint.Zero); if (!result) { int err = Marshal.GetLastWin32Error (); @@ -297,13 +313,14 @@ internal partial class WindowsOutput : OutputBase, IConsoleOutput { Logging.Logger.LogError ($"Error: {e.Message} in {nameof (WindowsOutput)}"); - if (!ConsoleDriver.RunningUnitTests) + if (RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) { throw; } } } - /// + + /// protected override void Write (StringBuilder output) { if (output.Length == 0) @@ -315,8 +332,8 @@ internal partial class WindowsOutput : OutputBase, IConsoleOutput if (_force16Colors && !_isVirtualTerminal) { - var a = str.ToCharArray (); - WriteConsole (_screenBuffer,a ,(uint)a.Length, out _, nint.Zero); + char [] a = str.ToCharArray (); + WriteConsole (_screenBuffer, a, (uint)a.Length, out _, nint.Zero); } else { @@ -324,10 +341,10 @@ internal partial class WindowsOutput : OutputBase, IConsoleOutput } } - /// + /// protected override void AppendOrWriteAttribute (StringBuilder output, Attribute attr, TextStyle redrawTextStyle) { - var force16Colors = Application.Force16Colors; + bool force16Colors = Application.Force16Colors; if (force16Colors) { @@ -351,7 +368,6 @@ internal partial class WindowsOutput : OutputBase, IConsoleOutput } } - private Size? _lastSize; private Size? _lastWindowSizeBeforeMaximized; private bool _lockResize; @@ -363,7 +379,7 @@ internal partial class WindowsOutput : OutputBase, IConsoleOutput return _lastSize!.Value; } - var newSize = GetWindowSize (out _); + Size newSize = GetWindowSize (out _); Size largestWindowSize = GetLargestConsoleWindowSize (); if (_lastWindowSizeBeforeMaximized is null && newSize == largestWindowSize) @@ -387,6 +403,7 @@ internal partial class WindowsOutput : OutputBase, IConsoleOutput // buffer will be wrong size, recreate it to ensure it doesn't result in // differing active and back buffer sizes (which causes flickering of window size) Size? bufSize = null; + while (bufSize != newSize) { _lockResize = true; @@ -402,32 +419,52 @@ internal partial class WindowsOutput : OutputBase, IConsoleOutput public Size GetWindowSize (out WindowsConsole.Coord cursorPosition) { - var csbi = new WindowsConsole.CONSOLE_SCREEN_BUFFER_INFOEX (); - csbi.cbSize = (uint)Marshal.SizeOf (csbi); - - if (!GetConsoleScreenBufferInfoEx (_isVirtualTerminal ? _outputHandle : _screenBuffer, ref csbi)) + try { - //throw new System.ComponentModel.Win32Exception (Marshal.GetLastWin32Error ()); - cursorPosition = default; - return Size.Empty; + var csbi = new WindowsConsole.CONSOLE_SCREEN_BUFFER_INFOEX (); + csbi.cbSize = (uint)Marshal.SizeOf (csbi); + + if (!GetConsoleScreenBufferInfoEx (_isVirtualTerminal ? _outputHandle : _screenBuffer, ref csbi)) + { + //throw new System.ComponentModel.Win32Exception (Marshal.GetLastWin32Error ()); + cursorPosition = default (WindowsConsole.Coord); + + return Size.Empty; + } + + Size sz = new ( + csbi.srWindow.Right - csbi.srWindow.Left + 1, + csbi.srWindow.Bottom - csbi.srWindow.Top + 1); + + cursorPosition = csbi.dwCursorPosition; + + return sz; + } + catch + { + cursorPosition = default (WindowsConsole.Coord); } - Size sz = new ( - csbi.srWindow.Right - csbi.srWindow.Left + 1, - csbi.srWindow.Bottom - csbi.srWindow.Top + 1); - - cursorPosition = csbi.dwCursorPosition; - return sz; + return new (80, 25); } private Size GetLargestConsoleWindowSize () { - WindowsConsole.Coord maxWinSize = GetLargestConsoleWindowSize (_isVirtualTerminal ? _outputHandle : _screenBuffer); + WindowsConsole.Coord maxWinSize; + + try + { + maxWinSize = GetLargestConsoleWindowSize (_isVirtualTerminal ? _outputHandle : _screenBuffer); + } + catch + { + maxWinSize = new (80, 25); + } return new (maxWinSize.X, maxWinSize.Y); } - /// + /// protected override bool SetCursorPositionImpl (int screenPositionX, int screenPositionY) { if (_force16Colors && !_isVirtualTerminal) @@ -446,10 +483,10 @@ internal partial class WindowsOutput : OutputBase, IConsoleOutput return true; } - /// + /// public override void SetCursorVisibility (CursorVisibility visibility) { - if (ConsoleDriver.RunningUnitTests) + if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) { return; } @@ -473,6 +510,9 @@ internal partial class WindowsOutput : OutputBase, IConsoleOutput } } + /// + public Point GetCursorPosition () { return _lastCursorPosition ?? Point.Empty; } + private Point? _lastCursorPosition; /// @@ -497,7 +537,7 @@ internal partial class WindowsOutput : OutputBase, IConsoleOutput } } - /// + /// public void SetSize (int width, int height) { // Do Nothing. @@ -506,7 +546,7 @@ internal partial class WindowsOutput : OutputBase, IConsoleOutput private bool _isDisposed; private bool _force16Colors; private nint _consoleBuffer; - private StringBuilder _everythingStringBuilder = new (); + private readonly StringBuilder _everythingStringBuilder = new (); /// public void Dispose () diff --git a/Terminal.Gui/FileServices/FileSystemIconProvider.cs b/Terminal.Gui/FileServices/FileSystemIconProvider.cs index d81c4e6b3..607582576 100644 --- a/Terminal.Gui/FileServices/FileSystemIconProvider.cs +++ b/Terminal.Gui/FileServices/FileSystemIconProvider.cs @@ -1,3 +1,4 @@ +#nullable enable using System.IO.Abstractions; namespace Terminal.Gui.FileServices; @@ -9,6 +10,43 @@ public class FileSystemIconProvider private bool _useNerdIcons = NerdFonts.Enable; private bool _useUnicodeCharacters; + /// + /// Returns the character to use to represent or an empty space if no icon + /// should be used. + /// + /// The file or directory requiring an icon. + /// + public Rune GetIcon (IFileSystemInfo? fileSystemInfo) + { + if (UseNerdIcons) + { + return new ( + _nerd.GetNerdIcon ( + fileSystemInfo, + fileSystemInfo is IDirectoryInfo dir && IsOpenGetter (dir) + ) + ); + } + + if (fileSystemInfo is IDirectoryInfo) + { + return UseUnicodeCharacters ? Glyphs.Folder : new (Path.DirectorySeparatorChar); + } + + return UseUnicodeCharacters ? Glyphs.File : new (' '); + } + + /// + /// Returns with an extra space on the end if icon is likely to overlap + /// adjacent cells. + /// + public string GetIconWithOptionalSpace (IFileSystemInfo? fileSystemInfo) + { + string space = UseNerdIcons ? " " : ""; + + return GetIcon (fileSystemInfo!) + space; + } + /// /// Gets or sets the delegate to be used to determine opened state of directories when resolving /// . Defaults to always false. @@ -51,41 +89,4 @@ public class FileSystemIconProvider } } } - - /// - /// Returns the character to use to represent or an empty space if no icon - /// should be used. - /// - /// The file or directory requiring an icon. - /// - public Rune GetIcon (IFileSystemInfo fileSystemInfo) - { - if (UseNerdIcons) - { - return new Rune ( - _nerd.GetNerdIcon ( - fileSystemInfo, - fileSystemInfo is IDirectoryInfo dir ? IsOpenGetter (dir) : false - ) - ); - } - - if (fileSystemInfo is IDirectoryInfo) - { - return UseUnicodeCharacters ? Glyphs.Folder : new Rune (Path.DirectorySeparatorChar); - } - - return UseUnicodeCharacters ? Glyphs.File : new Rune (' '); - } - - /// - /// Returns with an extra space on the end if icon is likely to overlap - /// adjacent cells. - /// - public string GetIconWithOptionalSpace (IFileSystemInfo fileSystemInfo) - { - string space = UseNerdIcons ? " " : ""; - - return GetIcon (fileSystemInfo) + space; - } } diff --git a/Terminal.Gui/FileServices/FileSystemInfoStats.cs b/Terminal.Gui/FileServices/FileSystemInfoStats.cs index 11bac6e2f..72f8ded2d 100644 --- a/Terminal.Gui/FileServices/FileSystemInfoStats.cs +++ b/Terminal.Gui/FileServices/FileSystemInfoStats.cs @@ -1,4 +1,5 @@ -using System.Globalization; +#nullable enable +using System.Globalization; using System.IO.Abstractions; namespace Terminal.Gui.FileServices; @@ -19,28 +20,28 @@ internal class FileSystemInfoStats * Red: Archive file * Red with black background: Broken link */ - private const long ByteConversion = 1024; - private static readonly List ExecutableExtensions = new () { ".EXE", ".BAT" }; + private const long BYTE_CONVERSION = 1024; + private static readonly List _executableExtensions = [".EXE", ".BAT"]; - private static readonly List ImageExtensions = new () - { + private static readonly List _imageExtensions = + [ ".JPG", ".JPEG", ".JPE", ".BMP", ".GIF", ".PNG" - }; + ]; - private static readonly string [] SizeSuffixes = { "B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB" }; + private static readonly string [] _sizeSuffixes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; /// Initializes a new instance of the class. /// The directory of path to wrap. /// - public FileSystemInfoStats (IFileSystemInfo fsi, CultureInfo culture) + public FileSystemInfoStats (IFileSystemInfo? fsi, CultureInfo culture) { FileSystemInfo = fsi; - LastWriteTime = fsi.LastWriteTime; + LastWriteTime = fsi?.LastWriteTime; if (fsi is IFileInfo fi) { @@ -57,38 +58,38 @@ internal class FileSystemInfoStats } /// Gets the wrapped (directory or file). - public IFileSystemInfo FileSystemInfo { get; } + public IFileSystemInfo? FileSystemInfo { get; } public string HumanReadableLength { get; } public bool IsDir { get; } + public bool IsExecutable () + { + // TODO: handle linux executable status + return FileSystemInfo is { } + && _executableExtensions.Contains ( + FileSystemInfo.Extension, + StringComparer.InvariantCultureIgnoreCase + ); + } + + public bool IsImage () + { + return FileSystemInfo is { } + && _imageExtensions.Contains ( + FileSystemInfo.Extension, + StringComparer.InvariantCultureIgnoreCase + ); + } + /// Gets or Sets a value indicating whether this instance represents the parent of the current state (i.e. ".."). public bool IsParent { get; internal set; } public DateTime? LastWriteTime { get; } public long MachineReadableLength { get; } - public string Name => IsParent ? ".." : FileSystemInfo.Name; + public string Name => IsParent ? ".." : FileSystemInfo?.Name ?? string.Empty; public string Type { get; } - public bool IsExecutable () - { - // TODO: handle linux executable status - return FileSystemInfo is IFileSystemInfo f - && ExecutableExtensions.Contains ( - f.Extension, - StringComparer.InvariantCultureIgnoreCase - ); - } - - public bool IsImage () - { - return FileSystemInfo is IFileSystemInfo f - && ImageExtensions.Contains ( - f.Extension, - StringComparer.InvariantCultureIgnoreCase - ); - } - private static string GetHumanReadableFileSize (long value, CultureInfo culture) { if (value < 0) @@ -101,9 +102,9 @@ internal class FileSystemInfoStats return "0.0 B"; } - var mag = (int)Math.Log (value, ByteConversion); + var mag = (int)Math.Log (value, BYTE_CONVERSION); double adjustedSize = value / Math.Pow (1000, mag); - return string.Format (culture.NumberFormat, "{0:n2} {1}", adjustedSize, SizeSuffixes [mag]); + return string.Format (culture.NumberFormat, "{0:n2} {1}", adjustedSize, _sizeSuffixes [mag]); } } diff --git a/Terminal.Gui/Text/TextFormatter.cs b/Terminal.Gui/Text/TextFormatter.cs index 6b1b5256e..70636c018 100644 --- a/Terminal.Gui/Text/TextFormatter.cs +++ b/Terminal.Gui/Text/TextFormatter.cs @@ -47,7 +47,7 @@ public class TextFormatter set => _textDirection = EnableNeedsFormat (value); } - /// Draws the text held by to using the colors specified. + /// Draws the text held by to using the colors specified. /// /// Causes the text to be formatted (references ). Sets to /// false. @@ -63,7 +63,7 @@ public class TextFormatter Attribute normalColor, Attribute hotColor, Rectangle maximum = default, - IConsoleDriver? driver = null + IDriver? driver = null ) { // With this check, we protect against subclasses with overrides of Text (like Button) @@ -874,7 +874,7 @@ public class TextFormatter /// /// /// Uses the same formatting logic as , including alignment, direction, word wrap, and constraints, - /// but does not perform actual drawing to . + /// but does not perform actual drawing to . /// /// Specifies the screen-relative location and maximum size for drawing the text. /// Specifies the screen-relative location and maximum container size. diff --git a/Terminal.Gui/ViewBase/View.Layout.cs b/Terminal.Gui/ViewBase/View.Layout.cs index 2f5a4660c..0c73aacbb 100644 --- a/Terminal.Gui/ViewBase/View.Layout.cs +++ b/Terminal.Gui/ViewBase/View.Layout.cs @@ -844,7 +844,7 @@ public partial class View // Layout APIs /// /// /// The next iteration will cause to be called on the next - /// so there is normally no reason to call see . + /// so there is normally no reason to call see . /// /// public void SetNeedsLayout () diff --git a/Terminal.Gui/ViewBase/View.Navigation.cs b/Terminal.Gui/ViewBase/View.Navigation.cs index 7f6a66177..516aef3c0 100644 --- a/Terminal.Gui/ViewBase/View.Navigation.cs +++ b/Terminal.Gui/ViewBase/View.Navigation.cs @@ -30,6 +30,7 @@ public partial class View // Focus and cross-view navigation management (TabStop /// public bool AdvanceFocus (NavigationDirection direction, TabBehavior? behavior) { + //Logging.Trace ($"{Id} - {direction} {behavior}"); if (!CanBeVisible (this)) // TODO: is this check needed? { return false; @@ -582,6 +583,8 @@ public partial class View // Focus and cross-view navigation management (TabStop { Debug.Assert (SuperView is null || IsInHierarchy (SuperView, this)); + //Logging.Trace ($"{Id} - {currentFocusedView?.Id} -> {Id}"); + // Pre-conditions if (_hasFocus) { @@ -604,7 +607,7 @@ public partial class View // Focus and cross-view navigation management (TabStop if (CanFocus && superViewOrParent is { CanFocus: false }) { - Debug.WriteLine ($@"WARNING: Attempt to FocusChanging where SuperView.CanFocus == false. {this}"); + Logging.Warning ($@"Attempt to FocusChanging where SuperView.CanFocus == false. {this}"); return (false, false); } diff --git a/Terminal.Gui/ViewBase/View.cs b/Terminal.Gui/ViewBase/View.cs index 0a26aa37b..5aa8a6e1d 100644 --- a/Terminal.Gui/ViewBase/View.cs +++ b/Terminal.Gui/ViewBase/View.cs @@ -109,14 +109,14 @@ public partial class View : IDisposable, ISupportInitializeNotification /// The id should be unique across all Views that share a SuperView. public string Id { get; set; } = ""; - private IConsoleDriver? _driver; + private IDriver? _driver; /// /// INTERNAL: Use instead. Points to the current driver in use by the view, it is a /// convenience property for simplifying the development /// of new views. /// - internal IConsoleDriver? Driver + internal IDriver? Driver { get { diff --git a/Terminal.Gui/Views/Color/ColorPicker.Prompt.cs b/Terminal.Gui/Views/Color/ColorPicker.Prompt.cs index ee519f12a..e6c29172c 100644 --- a/Terminal.Gui/Views/Color/ColorPicker.Prompt.cs +++ b/Terminal.Gui/Views/Color/ColorPicker.Prompt.cs @@ -5,7 +5,7 @@ public partial class ColorPicker { /// /// Open a with two or , based on the - /// is false or true, respectively, for + /// is false or true, respectively, for /// and colors. /// /// The title to show in the dialog. diff --git a/Terminal.Gui/Views/DatePicker.cs b/Terminal.Gui/Views/DatePicker.cs index a3253f122..a4f2791af 100644 --- a/Terminal.Gui/Views/DatePicker.cs +++ b/Terminal.Gui/Views/DatePicker.cs @@ -1,4 +1,6 @@ -// +#nullable enable + +// // DatePicker.cs: DatePicker control // // Author: Maciej Winnik @@ -12,13 +14,13 @@ namespace Terminal.Gui.Views; /// Lets the user pick a date from a visual calendar. public class DatePicker : View { - private TableView _calendar; + private TableView? _calendar; private DateTime _date; - private DateField _dateField; - private Label _dateLabel; - private Button _nextMonthButton; - private Button _previousMonthButton; - private DataTable _table; + private DateField? _dateField; + private Label? _dateLabel; + private Button? _nextMonthButton; + private Button? _previousMonthButton; + private DataTable? _table; /// Initializes a new instance of . public DatePicker () { SetInitialProperties (DateTime.Now); } @@ -27,7 +29,7 @@ public class DatePicker : View public DatePicker (DateTime date) { SetInitialProperties (date); } /// CultureInfo for date. The default is CultureInfo.CurrentCulture. - public CultureInfo Culture + public CultureInfo? Culture { get => CultureInfo.CurrentCulture; set @@ -51,35 +53,35 @@ public class DatePicker : View } } - private string Format => StandardizeDateFormat (Culture.DateTimeFormat.ShortDatePattern); + private string Format => StandardizeDateFormat (Culture?.DateTimeFormat.ShortDatePattern); /// protected override void Dispose (bool disposing) { - _dateLabel.Dispose (); - _calendar.Dispose (); - _dateField.Dispose (); - _table.Dispose (); - _previousMonthButton.Dispose (); - _nextMonthButton.Dispose (); + _dateLabel?.Dispose (); + _calendar?.Dispose (); + _dateField?.Dispose (); + _table?.Dispose (); + _previousMonthButton?.Dispose (); + _nextMonthButton?.Dispose (); base.Dispose (disposing); } private void ChangeDayDate (int day) { - _date = new DateTime (_date.Year, _date.Month, day); - _dateField.Date = _date; + _date = new (_date.Year, _date.Month, day); + _dateField!.Date = _date; CreateCalendar (); } - private void CreateCalendar () { _calendar.Table = new DataTableSource (_table = CreateDataTable (_date.Month, _date.Year)); } + private void CreateCalendar () { _calendar!.Table = new DataTableSource (_table = CreateDataTable (_date.Month, _date.Year)); } private DataTable CreateDataTable (int month, int year) { - _table = new DataTable (); + _table = new (); GenerateCalendarLabels (); int amountOfDaysInMonth = DateTime.DaysInMonth (year, month); - var dateValue = new DateTime (year, month, 1); + DateTime dateValue = new DateTime (year, month, 1); DayOfWeek dayOfWeek = dateValue.DayOfWeek; _table.Rows.Add (new object [6]); @@ -106,31 +108,31 @@ public class DatePicker : View return _table; } - private void DateField_DateChanged (object sender, DateTimeEventArgs e) + private void DateField_DateChanged (object? sender, EventArgs e) { - Date = e.NewValue; + Date = e.Value; - if (e.NewValue.Date.Day != _date.Day) + if (e.Value.Date.Day != _date.Day) { - SelectDayOnCalendar (e.NewValue.Day); + SelectDayOnCalendar (e.Value.Day); } if (_date.Month == DateTime.MinValue.Month && _date.Year == DateTime.MinValue.Year) { - _previousMonthButton.Enabled = false; + _previousMonthButton!.Enabled = false; } else { - _previousMonthButton.Enabled = true; + _previousMonthButton!.Enabled = true; } if (_date.Month == DateTime.MaxValue.Month && _date.Year == DateTime.MaxValue.Year) { - _nextMonthButton.Enabled = false; + _nextMonthButton!.Enabled = false; } else { - _nextMonthButton.Enabled = true; + _nextMonthButton!.Enabled = true; } CreateCalendar (); @@ -139,7 +141,7 @@ public class DatePicker : View private void GenerateCalendarLabels () { - _calendar.Style.ColumnStyles.Clear (); + _calendar!.Style.ColumnStyles.Clear (); for (var i = 0; i < 7; i++) { @@ -148,32 +150,32 @@ public class DatePicker : View _calendar.Style.ColumnStyles.Add ( i, - new ColumnStyle + new() { MaxWidth = abbreviatedDayName.Length, MinWidth = abbreviatedDayName.Length, MinAcceptableWidth = abbreviatedDayName.Length } ); - _table.Columns.Add (abbreviatedDayName); + _table!.Columns.Add (abbreviatedDayName); } // TODO: Get rid of the +7 which is hackish _calendar.Width = _calendar.Style.ColumnStyles.Sum (c => c.Value.MinWidth) + 7; } - private string GetBackButtonText () { return Glyphs.LeftArrow + Glyphs.LeftArrow.ToString (); } - private string GetForwardButtonText () { return Glyphs.RightArrow + Glyphs.RightArrow.ToString (); } + private static string GetBackButtonText () { return Glyphs.LeftArrow + Glyphs.LeftArrow.ToString (); } + private static string GetForwardButtonText () { return Glyphs.RightArrow + Glyphs.RightArrow.ToString (); } private void SelectDayOnCalendar (int day) { - for (var i = 0; i < _table.Rows.Count; i++) + for (var i = 0; i < _table!.Rows.Count; i++) { for (var j = 0; j < _table.Columns.Count; j++) { if (_table.Rows [i] [j].ToString () == day.ToString ()) { - _calendar.SetSelection (j, i, false); + _calendar!.SetSelection (j, i, false); return; } @@ -186,26 +188,26 @@ public class DatePicker : View _date = date; BorderStyle = LineStyle.Single; Date = date; - _dateLabel = new Label { X = 0, Y = 0, Text = "Date: " }; + _dateLabel = new() { X = 0, Y = 0, Text = "Date: " }; CanFocus = true; - _calendar = new TableView + _calendar = new() { Id = "_calendar", X = 0, Y = Pos.Bottom (_dateLabel), Height = 11, - Style = new TableStyle + Style = new() { ShowHeaders = true, ShowHorizontalBottomline = true, ShowVerticalCellLines = true, - ExpandLastColumn = true, + ExpandLastColumn = true }, MultiSelect = false }; - _dateField = new DateField (DateTime.Now) + _dateField = new (DateTime.Now) { Id = "_dateField", X = Pos.Right (_dateLabel), @@ -215,7 +217,7 @@ public class DatePicker : View Culture = Culture }; - _previousMonthButton = new Button + _previousMonthButton = new() { Id = "_previousMonthButton", X = Pos.Center () - 2, @@ -229,7 +231,7 @@ public class DatePicker : View }; _previousMonthButton.Accepting += (_, _) => AdjustMonth (-1); - _nextMonthButton = new Button + _nextMonthButton = new() { Id = "_nextMonthButton", X = Pos.Right (_previousMonthButton) + 2, @@ -247,14 +249,9 @@ public class DatePicker : View CreateCalendar (); SelectDayOnCalendar (_date.Day); - _calendar.CellActivated += (sender, e) => + _calendar.CellActivated += (_, e) => { - object dayValue = _table.Rows [e.Row] [e.Col]; - - if (dayValue is null) - { - return; - } + object dayValue = _table!.Rows [e.Row] [e.Col]; bool isDay = int.TryParse (dayValue.ToString (), out int day); @@ -280,44 +277,44 @@ public class DatePicker : View { Date = _date.AddMonths (offset); CreateCalendar (); - _dateField.Date = Date; + _dateField!.Date = Date; } - /// + /// protected override bool OnDrawingText () { return true; } - private static string StandardizeDateFormat (string format) + private static string StandardizeDateFormat (string? format) { return format switch - { - "MM/dd/yyyy" => "MM/dd/yyyy", - "yyyy-MM-dd" => "yyyy-MM-dd", - "yyyy/MM/dd" => "yyyy/MM/dd", - "dd/MM/yyyy" => "dd/MM/yyyy", - "d?/M?/yyyy" => "dd/MM/yyyy", - "dd.MM.yyyy" => "dd.MM.yyyy", - "dd-MM-yyyy" => "dd-MM-yyyy", - "dd/MM yyyy" => "dd/MM/yyyy", - "d. M. yyyy" => "dd.MM.yyyy", - "yyyy.MM.dd" => "yyyy.MM.dd", - "g yyyy/M/d" => "yyyy/MM/dd", - "d/M/yyyy" => "dd/MM/yyyy", - "d?/M?/yyyy g" => "dd/MM/yyyy", - "d-M-yyyy" => "dd-MM-yyyy", - "d.MM.yyyy" => "dd.MM.yyyy", - "d.MM.yyyy '?'." => "dd.MM.yyyy", - "M/d/yyyy" => "MM/dd/yyyy", - "d. M. yyyy." => "dd.MM.yyyy", - "d.M.yyyy." => "dd.MM.yyyy", - "g yyyy-MM-dd" => "yyyy-MM-dd", - "d.M.yyyy" => "dd.MM.yyyy", - "d/MM/yyyy" => "dd/MM/yyyy", - "yyyy/M/d" => "yyyy/MM/dd", - "dd. MM. yyyy." => "dd.MM.yyyy", - "yyyy. MM. dd." => "yyyy.MM.dd", - "yyyy. M. d." => "yyyy.MM.dd", - "d. MM. yyyy" => "dd.MM.yyyy", - _ => "dd/MM/yyyy" - }; + { + "MM/dd/yyyy" => "MM/dd/yyyy", + "yyyy-MM-dd" => "yyyy-MM-dd", + "yyyy/MM/dd" => "yyyy/MM/dd", + "dd/MM/yyyy" => "dd/MM/yyyy", + "d?/M?/yyyy" => "dd/MM/yyyy", + "dd.MM.yyyy" => "dd.MM.yyyy", + "dd-MM-yyyy" => "dd-MM-yyyy", + "dd/MM yyyy" => "dd/MM/yyyy", + "d. M. yyyy" => "dd.MM.yyyy", + "yyyy.MM.dd" => "yyyy.MM.dd", + "g yyyy/M/d" => "yyyy/MM/dd", + "d/M/yyyy" => "dd/MM/yyyy", + "d?/M?/yyyy g" => "dd/MM/yyyy", + "d-M-yyyy" => "dd-MM-yyyy", + "d.MM.yyyy" => "dd.MM.yyyy", + "d.MM.yyyy '?'." => "dd.MM.yyyy", + "M/d/yyyy" => "MM/dd/yyyy", + "d. M. yyyy." => "dd.MM.yyyy", + "d.M.yyyy." => "dd.MM.yyyy", + "g yyyy-MM-dd" => "yyyy-MM-dd", + "d.M.yyyy" => "dd.MM.yyyy", + "d/MM/yyyy" => "dd/MM/yyyy", + "yyyy/M/d" => "yyyy/MM/dd", + "dd. MM. yyyy." => "dd.MM.yyyy", + "yyyy. MM. dd." => "yyyy.MM.dd", + "yyyy. M. d." => "yyyy.MM.dd", + "d. MM. yyyy" => "dd.MM.yyyy", + _ => "dd/MM/yyyy" + }; } } diff --git a/Terminal.Gui/Views/DateTimeEventArgs.cs b/Terminal.Gui/Views/DateTimeEventArgs.cs deleted file mode 100644 index 8dfa4d78d..000000000 --- a/Terminal.Gui/Views/DateTimeEventArgs.cs +++ /dev/null @@ -1,36 +0,0 @@ -// -// DateField.cs: text entry for date -// -// Author: Barry Nolte -// -// Licensed under the MIT license -// - -namespace Terminal.Gui.Views; - -/// -/// Defines the event arguments for and -/// events. -/// -public class DateTimeEventArgs : EventArgs -{ - /// Initializes a new instance of - /// The old or value. - /// The new or value. - /// The or format string. - public DateTimeEventArgs (T oldValue, T newValue, string format) - { - OldValue = oldValue; - NewValue = newValue; - Format = format; - } - - /// The or format. - public string Format { get; } - - /// The new or value. - public T NewValue { get; } - - /// The old or value. - public T OldValue { get; } -} diff --git a/Terminal.Gui/Views/FileDialogs/FileDialog.cs b/Terminal.Gui/Views/FileDialogs/FileDialog.cs index 90982e96f..1d2a3533f 100644 --- a/Terminal.Gui/Views/FileDialogs/FileDialog.cs +++ b/Terminal.Gui/Views/FileDialogs/FileDialog.cs @@ -601,7 +601,10 @@ public class FileDialog : Dialog, IDesignable FileDialogTableSource.GetRawColumnValue (_currentSortColumn, f) ); - State!.Children = ordered.ToArray (); + if (State is { }) + { + State.Children = ordered.ToArray (); + } _tableView.Update (); } @@ -667,7 +670,7 @@ public class FileDialog : Dialog, IDesignable // Don't include ".." (IsParent) in multi-selections MultiSelected = toMultiAccept .Where (s => !s.IsParent) - .Select (s => s.FileSystemInfo.FullName) + .Select (s => s.FileSystemInfo!.FullName) .ToList () .AsReadOnly (); @@ -743,7 +746,7 @@ public class FileDialog : Dialog, IDesignable _tbPath.ClearAllSelection (); _tbPath.Autocomplete.ClearSuggestions (); - State!.RefreshChildren (); + State?.RefreshChildren (); WriteStateToTableView (); } @@ -877,7 +880,7 @@ public class FileDialog : Dialog, IDesignable private string GetBackButtonText () { return Glyphs.LeftArrow + "-"; } - private IFileSystemInfo []? GetFocusedFiles () + private IFileSystemInfo? []? GetFocusedFiles () { if (!_tableView.HasFocus || !_tableView.CanFocus) { @@ -1346,9 +1349,9 @@ public class FileDialog : Dialog, IDesignable return; } - FileSystemInfoStats stats = RowToStats (obj.NewRow); + FileSystemInfoStats? stats = RowToStats (obj.NewRow); - IFileSystemInfo dest; + IFileSystemInfo? dest; if (stats.IsParent) { @@ -1416,10 +1419,10 @@ public class FileDialog : Dialog, IDesignable } if (fileSystemInfoStatsEnumerable.All ( - m => IsCompatibleWithOpenMode ( - m.FileSystemInfo.FullName, - out reason - ) + m => m.FileSystemInfo is { } && IsCompatibleWithOpenMode ( + m.FileSystemInfo.FullName, + out reason + ) )) { Accept (fileSystemInfoStatsEnumerable); diff --git a/Terminal.Gui/Views/FileDialogs/FileDialogTableSource.cs b/Terminal.Gui/Views/FileDialogs/FileDialogTableSource.cs index 00ef33d6f..14d37f097 100644 --- a/Terminal.Gui/Views/FileDialogs/FileDialogTableSource.cs +++ b/Terminal.Gui/Views/FileDialogs/FileDialogTableSource.cs @@ -1,55 +1,53 @@ - +#nullable enable namespace Terminal.Gui.Views; -internal class FileDialogTableSource : ITableSource +internal class FileDialogTableSource ( + FileDialog? dlg, + FileDialogState? state, + FileDialogStyle? style, + int currentSortColumn, + bool currentSortIsAsc) + : ITableSource { - private readonly int currentSortColumn; - private readonly bool currentSortIsAsc; - private readonly FileDialog dlg; - private readonly FileDialogState state; - private readonly FileDialogStyle style; - - public FileDialogTableSource ( - FileDialog dlg, - FileDialogState state, - FileDialogStyle style, - int currentSortColumn, - bool currentSortIsAsc - ) + public object this [int row, int col] { - this.style = style; - this.currentSortColumn = currentSortColumn; - this.currentSortIsAsc = currentSortIsAsc; - this.dlg = dlg; - this.state = state; + get + { + if (state is { }) + { + return GetColumnValue (col, state.Children [row]); + } + + return string.Empty; + } } - public object this [int row, int col] => GetColumnValue (col, state.Children [row]); - public int Rows => state.Children.Count (); + public int Rows => state is { } ? state.Children.Count () : 0; + public int Columns => 4; - public string [] ColumnNames => new [] - { - MaybeAddSortArrows (style.FilenameColumnName, 0), + public string [] ColumnNames => + [ + MaybeAddSortArrows (style!.FilenameColumnName, 0), MaybeAddSortArrows (style.SizeColumnName, 1), MaybeAddSortArrows (style.ModifiedColumnName, 2), MaybeAddSortArrows (style.TypeColumnName, 3) - }; + ]; - internal static object GetRawColumnValue (int col, FileSystemInfoStats stats) + internal static object GetRawColumnValue (int col, FileSystemInfoStats? stats) { switch (col) { - case 0: return stats.FileSystemInfo.Name; - case 1: return stats.MachineReadableLength; - case 2: return stats.LastWriteTime; - case 3: return stats.Type; + case 0: return stats!.FileSystemInfo!.Name; + case 1: return stats!.MachineReadableLength; + case 2: return stats!.LastWriteTime ?? default (DateTime); + case 3: return stats!.Type; } throw new ArgumentOutOfRangeException (nameof (col)); } - private object GetColumnValue (int col, FileSystemInfoStats stats) + private object GetColumnValue (int col, FileSystemInfoStats? stats) { switch (col) { @@ -60,7 +58,7 @@ internal class FileDialogTableSource : ITableSource return stats.Name; } - string icon = dlg.Style.IconProvider.GetIconWithOptionalSpace (stats.FileSystemInfo); + string icon = dlg!.Style.IconProvider.GetIconWithOptionalSpace (stats!.FileSystemInfo); return (icon + (stats?.Name ?? string.Empty)).Trim (); case 1: @@ -71,7 +69,7 @@ internal class FileDialogTableSource : ITableSource return string.Empty; } - return stats.LastWriteTime.Value.ToString (style.DateFormat); + return stats.LastWriteTime.Value.ToString (style!.DateFormat); case 3: return stats?.Type ?? string.Empty; default: diff --git a/Terminal.Gui/Views/GraphView/Axis.cs b/Terminal.Gui/Views/GraphView/Axis.cs index 3e5d6acf2..3160f3ee3 100644 --- a/Terminal.Gui/Views/GraphView/Axis.cs +++ b/Terminal.Gui/Views/GraphView/Axis.cs @@ -98,7 +98,7 @@ public class HorizontalAxis : Axis /// Text to render under the axis tick public override void DrawAxisLabel (GraphView graph, int screenPosition, string text) { - IConsoleDriver driver = Application.Driver; + IDriver driver = Application.Driver; int y = GetAxisYPosition (graph); graph.Move (screenPosition, y); diff --git a/Terminal.Gui/Views/Menuv1/Menu.cs b/Terminal.Gui/Views/Menuv1/Menu.cs index ceb63d412..7833e4196 100644 --- a/Terminal.Gui/Views/Menuv1/Menu.cs +++ b/Terminal.Gui/Views/Menuv1/Menu.cs @@ -1,3 +1,4 @@ +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable. #nullable enable diff --git a/Terminal.Gui/Views/Menuv1/MenuBar.cs b/Terminal.Gui/Views/Menuv1/MenuBar.cs index 8f56ed76b..d2ab03dc5 100644 --- a/Terminal.Gui/Views/Menuv1/MenuBar.cs +++ b/Terminal.Gui/Views/Menuv1/MenuBar.cs @@ -692,7 +692,7 @@ public class MenuBar : View, IDesignable return true; } - /// Gets the superview location offset relative to the location. + /// Gets the superview location offset relative to the location. /// The location offset. internal Point GetScreenOffset () { diff --git a/Terminal.Gui/Views/Menuv1/MenuItem.cs b/Terminal.Gui/Views/Menuv1/MenuItem.cs index cc9b345df..002ea0a38 100644 --- a/Terminal.Gui/Views/Menuv1/MenuItem.cs +++ b/Terminal.Gui/Views/Menuv1/MenuItem.cs @@ -1,3 +1,4 @@ +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable. #nullable enable diff --git a/Terminal.Gui/Views/TabView/TabMouseEventArgs.cs b/Terminal.Gui/Views/TabView/TabMouseEventArgs.cs index df572140a..f52792549 100644 --- a/Terminal.Gui/Views/TabView/TabMouseEventArgs.cs +++ b/Terminal.Gui/Views/TabView/TabMouseEventArgs.cs @@ -9,7 +9,7 @@ public class TabMouseEventArgs : HandledEventArgs /// Creates a new instance of the class. /// that the mouse was over when the event occurred. /// The mouse activity being reported - public TabMouseEventArgs (Tab tab, MouseEventArgs mouseEvent) + public TabMouseEventArgs (Tab? tab, MouseEventArgs mouseEvent) { Tab = tab; MouseEvent = mouseEvent; @@ -23,5 +23,5 @@ public class TabMouseEventArgs : HandledEventArgs /// Gets the (if any) that the mouse was over when the occurred. /// This will be null if the click is after last tab or before first. - public Tab Tab { get; } + public Tab? Tab { get; } } diff --git a/Terminal.Gui/Views/TabView/TabView.cs b/Terminal.Gui/Views/TabView/TabView.cs index 8d0533cff..6c222d9f3 100644 --- a/Terminal.Gui/Views/TabView/TabView.cs +++ b/Terminal.Gui/Views/TabView/TabView.cs @@ -244,7 +244,9 @@ public class TabView : View private bool TabCanSetFocus () { +#pragma warning disable CS8629 // Nullable value type may be null. return IsInitialized && SelectedTab is { } && (HasFocus || (bool)_containerView?.HasFocus) && (_selectedTabHasFocus || !_containerView.CanFocus); +#pragma warning restore CS8629 // Nullable value type may be null. } private void ContainerViewCanFocus (object sender, EventArgs eventArgs) diff --git a/Terminal.Gui/Views/TableView/TableStyle.cs b/Terminal.Gui/Views/TableView/TableStyle.cs index 77f380147..e52cddf1f 100644 --- a/Terminal.Gui/Views/TableView/TableStyle.cs +++ b/Terminal.Gui/Views/TableView/TableStyle.cs @@ -29,7 +29,7 @@ public class TableStyle /// /// True to invert the colors of the first symbol of the selected cell in the . This gives - /// the appearance of a cursor for when the doesn't otherwise show this + /// the appearance of a cursor for when the doesn't otherwise show this /// public bool InvertSelectedCellFirstCharacter { get; set; } = false; diff --git a/Terminal.Gui/Views/TextInput/DateField.cs b/Terminal.Gui/Views/TextInput/DateField.cs index ac156724d..2da26f9a0 100644 --- a/Terminal.Gui/Views/TextInput/DateField.cs +++ b/Terminal.Gui/Views/TextInput/DateField.cs @@ -1,4 +1,6 @@ -// +#nullable enable + +// // DateField.cs: text entry for date // // Author: Barry Nolte @@ -13,12 +15,12 @@ namespace Terminal.Gui.Views; /// Provides date editing functionality with mouse support. public class DateField : TextField { - private const string RightToLeftMark = "\u200f"; + private const string RIGHT_TO_LEFT_MARK = "\u200f"; private readonly int _dateFieldLength = 12; - private DateTime _date; - private string _format; - private string _separator; + private DateTime? _date; + private string? _format; + private string? _separator; /// Initializes a new instance of . public DateField () : this (DateTime.MinValue) { } @@ -31,19 +33,18 @@ public class DateField : TextField SetInitialProperties (date); } + private CultureInfo _culture = CultureInfo.CurrentCulture; + /// CultureInfo for date. The default is CultureInfo.CurrentCulture. - public CultureInfo Culture + public CultureInfo? Culture { - get => CultureInfo.CurrentCulture; + get => _culture; set { - if (value is { }) - { - CultureInfo.CurrentCulture = value; - _separator = GetDataSeparator (value.DateTimeFormat.DateSeparator); - _format = " " + StandardizeDateFormat (value.DateTimeFormat.ShortDatePattern); - Text = Date.ToString (_format).Replace (RightToLeftMark, ""); - } + _culture = value ?? CultureInfo.CurrentCulture; + _separator = GetDataSeparator (_culture.DateTimeFormat.DateSeparator); + _format = " " + StandardizeDateFormat (_culture.DateTimeFormat.ShortDatePattern); + Text = Date?.ToString (_format).Replace (RIGHT_TO_LEFT_MARK, ""); } } @@ -56,7 +57,7 @@ public class DateField : TextField /// Gets or sets the date of the . /// - public DateTime Date + public DateTime? Date { get => _date; set @@ -66,16 +67,20 @@ public class DateField : TextField return; } - DateTime oldData = _date; + DateTime? oldData = _date; _date = value; - Text = value.ToString (" " + StandardizeDateFormat (_format.Trim ())) - .Replace (RightToLeftMark, ""); - DateTimeEventArgs args = new (oldData, value, _format); - - if (oldData != value) + if (_format is { }) { - OnDateChanged (args); + Text = value?.ToString (" " + StandardizeDateFormat (_format.Trim ())) + .Replace (RIGHT_TO_LEFT_MARK, ""); + EventArgs args = new (value!.Value); + + if (oldData != value) + { + OnDateChanged (args); + DateChanged?.Invoke (this, args); + } } } } @@ -85,7 +90,7 @@ public class DateField : TextField /// DateChanged event, raised when the property has changed. /// This event is raised when the property changes. /// The passed event arguments containing the old value, new value, and format string. - public event EventHandler> DateChanged; + public event EventHandler>? DateChanged; /// public override void DeleteCharLeft (bool useOldCursorPos = true) @@ -130,7 +135,7 @@ public class DateField : TextField /// Event firing method for the event. /// Event arguments - public virtual void OnDateChanged (DateTimeEventArgs args) { DateChanged?.Invoke (this, args); } + protected virtual void OnDateChanged (EventArgs args) { } /// protected override bool OnKeyDownNotHandled (Key a) @@ -184,8 +189,13 @@ public class DateField : TextField } } - private void OnTextChanging (object sender, ResultEventArgs e) + private void OnTextChanging (object? sender, ResultEventArgs e) { + if (e.Result is null) + { + return; + } + try { var spaces = 0; @@ -205,13 +215,13 @@ public class DateField : TextField spaces += FormatLength; string trimmedText = e.Result [..spaces]; spaces -= FormatLength; - trimmedText = trimmedText.Replace (new string (' ', spaces), " "); - var date = Convert.ToDateTime (trimmedText).ToString (_format.Trim ()); + trimmedText = trimmedText.Replace (new (' ', spaces), " "); + var date = Convert.ToDateTime (trimmedText).ToString (_format!.Trim ()); if ($" {date}" != e.Result) { // Change the date format to match the current culture - e.Result = $" {date}".Replace (RightToLeftMark, ""); + e.Result = $" {date}".Replace (RIGHT_TO_LEFT_MARK, ""); } AdjCursorPosition (CursorPosition); @@ -239,9 +249,9 @@ public class DateField : TextField { string sepChar = separator.Trim (); - if (sepChar.Length > 1 && sepChar.Contains (RightToLeftMark)) + if (sepChar.Length > 1 && sepChar.Contains (RIGHT_TO_LEFT_MARK)) { - sepChar = sepChar.Replace (RightToLeftMark, ""); + sepChar = sepChar.Replace (RIGHT_TO_LEFT_MARK, ""); } return sepChar; @@ -338,7 +348,7 @@ public class DateField : TextField return true; } - private string NormalizeFormat (string text, string fmt = null, string sepChar = null) + private string NormalizeFormat (string text, string? fmt = null, string? sepChar = null) { if (string.IsNullOrEmpty (fmt)) { @@ -350,7 +360,7 @@ public class DateField : TextField sepChar = _separator; } - if (fmt.Length != text.Length) + if (fmt is null || fmt.Length != text.Length) { return text; } @@ -367,12 +377,12 @@ public class DateField : TextField } } - return new string (fmtText); + return new (fmtText); } private void SetInitialProperties (DateTime date) { - _format = $" {StandardizeDateFormat (Culture.DateTimeFormat.ShortDatePattern)}"; + _format = $" {StandardizeDateFormat (Culture!.DateTimeFormat.ShortDatePattern)}"; _separator = GetDataSeparator (Culture.DateTimeFormat.DateSeparator); Date = date; CursorPosition = 1; @@ -424,7 +434,6 @@ public class DateField : TextField #if UNIX_KEY_BINDINGS KeyBindings.ReplaceCommands (Key.D.WithAlt, Command.DeleteCharLeft); #endif - } private bool SetText (Rune key) @@ -471,13 +480,13 @@ public class DateField : TextField for (var i = 0; i < vals.Length; i++) { - if (vals [i].Contains (RightToLeftMark)) + if (vals [i].Contains (RIGHT_TO_LEFT_MARK)) { - vals [i] = vals [i].Replace (RightToLeftMark, ""); + vals [i] = vals [i].Replace (RIGHT_TO_LEFT_MARK, ""); } } - string [] frm = _format.Split (_separator); + string [] frm = _format!.Split (_separator); int year; int month; int day; @@ -548,38 +557,38 @@ public class DateField : TextField // Converts various date formats to a uniform 10-character format. // This aids in simplifying the handling of single-digit months and days, // and reduces the number of distinct date formats to maintain. - private static string StandardizeDateFormat (string format) + private static string StandardizeDateFormat (string? format) { return format switch - { - "MM/dd/yyyy" => "MM/dd/yyyy", - "yyyy-MM-dd" => "yyyy-MM-dd", - "yyyy/MM/dd" => "yyyy/MM/dd", - "dd/MM/yyyy" => "dd/MM/yyyy", - "d?/M?/yyyy" => "dd/MM/yyyy", - "dd.MM.yyyy" => "dd.MM.yyyy", - "dd-MM-yyyy" => "dd-MM-yyyy", - "dd/MM yyyy" => "dd/MM/yyyy", - "d. M. yyyy" => "dd.MM.yyyy", - "yyyy.MM.dd" => "yyyy.MM.dd", - "g yyyy/M/d" => "yyyy/MM/dd", - "d/M/yyyy" => "dd/MM/yyyy", - "d?/M?/yyyy g" => "dd/MM/yyyy", - "d-M-yyyy" => "dd-MM-yyyy", - "d.MM.yyyy" => "dd.MM.yyyy", - "d.MM.yyyy '?'." => "dd.MM.yyyy", - "M/d/yyyy" => "MM/dd/yyyy", - "d. M. yyyy." => "dd.MM.yyyy", - "d.M.yyyy." => "dd.MM.yyyy", - "g yyyy-MM-dd" => "yyyy-MM-dd", - "d.M.yyyy" => "dd.MM.yyyy", - "d/MM/yyyy" => "dd/MM/yyyy", - "yyyy/M/d" => "yyyy/MM/dd", - "dd. MM. yyyy." => "dd.MM.yyyy", - "yyyy. MM. dd." => "yyyy.MM.dd", - "yyyy. M. d." => "yyyy.MM.dd", - "d. MM. yyyy" => "dd.MM.yyyy", - _ => "dd/MM/yyyy" - }; + { + "MM/dd/yyyy" => "MM/dd/yyyy", + "yyyy-MM-dd" => "yyyy-MM-dd", + "yyyy/MM/dd" => "yyyy/MM/dd", + "dd/MM/yyyy" => "dd/MM/yyyy", + "d?/M?/yyyy" => "dd/MM/yyyy", + "dd.MM.yyyy" => "dd.MM.yyyy", + "dd-MM-yyyy" => "dd-MM-yyyy", + "dd/MM yyyy" => "dd/MM/yyyy", + "d. M. yyyy" => "dd.MM.yyyy", + "yyyy.MM.dd" => "yyyy.MM.dd", + "g yyyy/M/d" => "yyyy/MM/dd", + "d/M/yyyy" => "dd/MM/yyyy", + "d?/M?/yyyy g" => "dd/MM/yyyy", + "d-M-yyyy" => "dd-MM-yyyy", + "d.MM.yyyy" => "dd.MM.yyyy", + "d.MM.yyyy '?'." => "dd.MM.yyyy", + "M/d/yyyy" => "MM/dd/yyyy", + "d. M. yyyy." => "dd.MM.yyyy", + "d.M.yyyy." => "dd.MM.yyyy", + "g yyyy-MM-dd" => "yyyy-MM-dd", + "d.M.yyyy" => "dd.MM.yyyy", + "d/MM/yyyy" => "dd/MM/yyyy", + "yyyy/M/d" => "yyyy/MM/dd", + "dd. MM. yyyy." => "dd.MM.yyyy", + "yyyy. MM. dd." => "yyyy.MM.dd", + "yyyy. M. d." => "yyyy.MM.dd", + "d. MM. yyyy" => "dd.MM.yyyy", + _ => "dd/MM/yyyy" + }; } } diff --git a/Terminal.Gui/Views/TextInput/TextView.cs b/Terminal.Gui/Views/TextInput/TextView.cs index 4d12a5ed1..f9e2031d9 100644 --- a/Terminal.Gui/Views/TextInput/TextView.cs +++ b/Terminal.Gui/Views/TextInput/TextView.cs @@ -1968,7 +1968,7 @@ public class TextView : View, IDesignable SetWrapModel (); string? contents = Clipboard.Contents; - if (_copyWithoutSelection && contents.FirstOrDefault (x => x is '\n' or '\r') == 0) + if (_copyWithoutSelection && contents!.FirstOrDefault (x => x is '\n' or '\r') == 0) { List runeList = contents is null ? [] : Cell.ToCellList (contents); List currentLine = GetCurrentLine (); @@ -2003,7 +2003,7 @@ public class TextView : View, IDesignable } _copyWithoutSelection = false; - InsertAllText (contents, true); + InsertAllText (contents!, true); if (IsSelecting) { diff --git a/Terminal.Gui/Views/TextInput/TimeField.cs b/Terminal.Gui/Views/TextInput/TimeField.cs index 083ccf0e1..59a906f63 100644 --- a/Terminal.Gui/Views/TextInput/TimeField.cs +++ b/Terminal.Gui/Views/TextInput/TimeField.cs @@ -124,7 +124,7 @@ public class TimeField : TextField TimeSpan oldTime = _time; _time = value; Text = " " + value.ToString (Format.Trim ()); - DateTimeEventArgs args = new (oldTime, value, Format); + EventArgs args = new (value); if (oldTime != value) { @@ -200,15 +200,11 @@ public class TimeField : TextField /// Event firing method that invokes the event. /// The event arguments - public virtual void OnTimeChanged (DateTimeEventArgs args) { TimeChanged?.Invoke (this, args); } + public virtual void OnTimeChanged (EventArgs args) { TimeChanged?.Invoke (this, args); } /// TimeChanged event, raised when the Date has changed. /// This event is raised when the changes. - /// - /// The passed is a containing the old value, new - /// value, and format string. - /// - public event EventHandler> TimeChanged; + public event EventHandler> TimeChanged; private void AdjCursorPosition (int point, bool increment = true) { diff --git a/Terminal.Gui/Views/Toplevel.cs b/Terminal.Gui/Views/Toplevel.cs index a62252f57..926eb6aef 100644 --- a/Terminal.Gui/Views/Toplevel.cs +++ b/Terminal.Gui/Views/Toplevel.cs @@ -95,25 +95,25 @@ public partial class Toplevel : View public bool IsLoaded { get; private set; } // TODO: IRunnable: Re-implement as an event on IRunnable; IRunnable.Activating/Activate - /// Invoked when the Toplevel active. + /// Invoked when the Toplevel active. public event EventHandler? Activate; // TODO: IRunnable: Re-implement as an event on IRunnable; IRunnable.Deactivating/Deactivate? - /// Invoked when the Toplevel ceases to be active. + /// Invoked when the Toplevel ceases to be active. public event EventHandler? Deactivate; - /// Invoked when the Toplevel's is closed by . + /// Invoked when the Toplevel's is closed by . public event EventHandler? Closed; /// - /// Invoked when the Toplevel's is being closed by + /// Invoked when the Toplevel's is being closed by /// . /// public event EventHandler? Closing; /// - /// Invoked when the has begun to be loaded. A Loaded event handler - /// is a good place to finalize initialization before calling . + /// Invoked when the has begun to be loaded. A Loaded event handler + /// is a good place to finalize initialization before calling Run. /// public event EventHandler? Loaded; @@ -158,8 +158,8 @@ public partial class Toplevel : View } /// - /// Invoked when the Toplevel has been unloaded. A Unloaded event handler is a good place - /// to dispose objects after calling . + /// Invoked when the Toplevel has been unloaded. A Unloaded event handler is a good place + /// to dispose objects after calling . /// public event EventHandler? Unloaded; @@ -177,7 +177,7 @@ public partial class Toplevel : View internal virtual void OnDeactivate (Toplevel activated) { Deactivate?.Invoke (this, new (activated)); } /// - /// Called from after the has entered the first iteration + /// Called from run loop after the has entered the first iteration /// of the loop. /// internal virtual void OnReady () @@ -191,7 +191,7 @@ public partial class Toplevel : View Ready?.Invoke (this, EventArgs.Empty); } - /// Called from before the is disposed. + /// Called from before the is disposed. internal virtual void OnUnloaded () { foreach (var view in SubViews.Where (v => v is Toplevel)) diff --git a/Terminal.sln b/Terminal.sln index df75ec4c3..b5335d39a 100644 --- a/Terminal.sln +++ b/Terminal.sln @@ -32,12 +32,13 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "GitHub", "GitHub", "{13BB2C46-B324-4B9C-92EB-CE6184D4736E}" ProjectSection(SolutionItems) = preProject .github\workflows\api-docs.yml = .github\workflows\api-docs.yml - .github\workflows\build.yml = .github\workflows\build.yml + .github\workflows\build-validation.yml = .github\workflows\build-validation.yml .github\workflows\check-duplicates.yml = .github\workflows\check-duplicates.yml copilot-instructions.md = copilot-instructions.md GitVersion.yml = GitVersion.yml .github\workflows\integration-tests.yml = .github\workflows\integration-tests.yml .github\workflows\publish.yml = .github\workflows\publish.yml + .github\workflows\quick-build.yml = .github\workflows\quick-build.yml .github\workflows\stress-tests.yml = .github\workflows\stress-tests.yml .github\workflows\unit-tests.yml = .github\workflows\unit-tests.yml EndProjectSection @@ -81,6 +82,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Terminal.Gui.Analyzers", "T EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Terminal.Gui.Analyzers.Tests", "Terminal.Gui.Analyzers.Tests\Terminal.Gui.Analyzers.Tests.csproj", "{8C643A64-2A77-4432-987A-2E72BD9708E3}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Testing", "Testing", "{1A3CBA89-EDAB-4F75-811E-FE81B13A4836}" + ProjectSection(SolutionItems) = preProject + Scripts\Run-LocalCoverage.ps1 = Scripts\Run-LocalCoverage.ps1 + Tests\UnitTests\runsettings.xml = Tests\UnitTests\runsettings.xml + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -163,6 +170,7 @@ Global {B8F48EE8-34A6-43B7-B00E-CD91CDD93962} = {E143FB1F-0B88-48CB-9086-72CDCECFCD22} {13BB2C46-B324-4B9C-92EB-CE6184D4736E} = {E143FB1F-0B88-48CB-9086-72CDCECFCD22} {C7A51224-5E0F-4986-AB37-A6BF89966C12} = {E143FB1F-0B88-48CB-9086-72CDCECFCD22} + {1A3CBA89-EDAB-4F75-811E-FE81B13A4836} = {E143FB1F-0B88-48CB-9086-72CDCECFCD22} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {9F8F8A4D-7B8D-4C2A-AC5E-CD7117F74C03} diff --git a/Terminal.sln.DotSettings b/Terminal.sln.DotSettings index 4e6d0e05b..4161569b1 100644 --- a/Terminal.sln.DotSettings +++ b/Terminal.sln.DotSettings @@ -6,6 +6,7 @@ 1000 3000 2000 + False SUGGESTION ERROR WARNING diff --git a/Tests/Benchmarks/ConsoleDrivers/EscSeqUtils/CSI_SetVsAppend.cs b/Tests/Benchmarks/ConsoleDrivers/EscSeqUtils/CSI_SetVsAppend.cs index 7f1f9397e..d5fe3da73 100644 --- a/Tests/Benchmarks/ConsoleDrivers/EscSeqUtils/CSI_SetVsAppend.cs +++ b/Tests/Benchmarks/ConsoleDrivers/EscSeqUtils/CSI_SetVsAppend.cs @@ -2,7 +2,7 @@ using BenchmarkDotNet.Attributes; using Tui = Terminal.Gui.Drivers; -namespace Terminal.Gui.Benchmarks.ConsoleDrivers.EscSeqUtils; +namespace Terminal.Gui.Benchmarks.Drivers.EscSeqUtils; /// /// Compares the Set and Append implementations in combination. diff --git a/Tests/Benchmarks/ConsoleDrivers/EscSeqUtils/CSI_SetVsWrite.cs b/Tests/Benchmarks/ConsoleDrivers/EscSeqUtils/CSI_SetVsWrite.cs index 5577907b0..a7476efb1 100644 --- a/Tests/Benchmarks/ConsoleDrivers/EscSeqUtils/CSI_SetVsWrite.cs +++ b/Tests/Benchmarks/ConsoleDrivers/EscSeqUtils/CSI_SetVsWrite.cs @@ -1,7 +1,7 @@ using BenchmarkDotNet.Attributes; using Tui = Terminal.Gui.Drivers; -namespace Terminal.Gui.Benchmarks.ConsoleDrivers.EscSeqUtils; +namespace Terminal.Gui.Benchmarks.Drivers.EscSeqUtils; [MemoryDiagnoser] // Hide useless column from results. diff --git a/Tests/IntegrationTests/FluentTests/BasicFluentAssertionTests.cs b/Tests/IntegrationTests/FluentTests/BasicFluentAssertionTests.cs deleted file mode 100644 index 35586cd9f..000000000 --- a/Tests/IntegrationTests/FluentTests/BasicFluentAssertionTests.cs +++ /dev/null @@ -1,224 +0,0 @@ -using TerminalGuiFluentTesting; -using TerminalGuiFluentTestingXunit; -using Xunit.Abstractions; - -namespace IntegrationTests.FluentTests; - -public class BasicFluentAssertionTests -{ - private readonly TextWriter _out; - - public BasicFluentAssertionTests (ITestOutputHelper outputHelper) { _out = new TestOutputWriter (outputHelper); } - - [Theory] - [ClassData (typeof (TestDrivers))] - public void GuiTestContext_NewInstance_Runs (TestDriver d) - { - using GuiTestContext context = With.A (40, 10, d, _out); - Assert.True (Application.Top!.Running); - - context.WriteOutLogs (_out); - context.Stop (); - } - - [Theory] - [ClassData (typeof (TestDrivers))] - public void GuiTestContext_QuitKey_Stops (TestDriver d) - { - using GuiTestContext context = With.A (40, 10, d); - Assert.True (Application.Top!.Running); - - Toplevel top = Application.Top; - context.RaiseKeyDownEvent (Application.QuitKey); - Assert.False (top!.Running); - - context.WriteOutLogs (_out); - context.Stop (); - } - - [Theory] - [ClassData (typeof (TestDrivers))] - public void GuiTestContext_StartsAndStopsWithoutError (TestDriver d) - { - using GuiTestContext context = With.A (40, 10, d); - - // No actual assertions are needed — if no exceptions are thrown, it's working - context.Stop (); - } - - [Theory] - [ClassData (typeof (TestDrivers))] - public void GuiTestContext_ForgotToStop (TestDriver d) - { - using GuiTestContext context = With.A (40, 10, d); - } - - [Theory] - [ClassData (typeof (TestDrivers))] - public void TestWindowsResize (TestDriver d) - { - var lbl = new Label - { - Width = Dim.Fill () - }; - - using GuiTestContext c = With.A (40, 10, d) - .Add (lbl) - .AssertEqual (38, lbl.Frame.Width) // Window has 2 border - .ResizeConsole (20, 20) - .WaitIteration () - .AssertEqual (18, lbl.Frame.Width) - .WriteOutLogs (_out) - .Stop (); - } - - [Theory] - [ClassData (typeof (TestDrivers))] - public void ContextMenu_CrashesOnRight (TestDriver d) - { - var clicked = false; - - MenuItemv2 [] menuItems = [new ("_New File", string.Empty, () => { clicked = true; })]; - - using GuiTestContext c = With.A (40, 10, d) - .WithContextMenu (new (menuItems)) - .ScreenShot ("Before open menu", _out) - - // Click in main area inside border - .RightClick (1, 1) - .Then ( - () => - { - // Test depends on menu having a border - IPopover? popover = Application.Popover!.GetActivePopover (); - Assert.NotNull (popover); - var popoverMenu = popover as PopoverMenu; - popoverMenu!.Root!.BorderStyle = LineStyle.Single; - }) - .WaitIteration () - .ScreenShot ("After open menu", _out) - .LeftClick (2, 2) - .Stop () - .WriteOutLogs (_out); - Assert.True (clicked); - } - - [Theory] - [ClassData (typeof (TestDrivers))] - public void ContextMenu_OpenSubmenu (TestDriver d) - { - var clicked = false; - - MenuItemv2 [] menuItems = - [ - new ("One", "", null), - new ("Two", "", null), - new ("Three", "", null), - new ( - "Four", - "", - new ( - [ - new ("SubMenu1", "", null), - new ("SubMenu2", "", () => clicked = true), - new ("SubMenu3", "", null), - new ("SubMenu4", "", null), - new ("SubMenu5", "", null), - new ("SubMenu6", "", null), - new ("SubMenu7", "", null) - ])), - new ("Five", "", null), - new ("Six", "", null) - ]; - - using GuiTestContext c = With.A (40, 10, d) - .WithContextMenu (new (menuItems)) - .ScreenShot ("Before open menu", _out) - - // Click in main area inside border - .RightClick (1, 1) - .ScreenShot ("After open menu", _out) - .Down () - .Down () - .Down () - .Right () - .ScreenShot ("After open submenu", _out) - .Down () - .Enter () - .ScreenShot ("Menu should be closed after selecting", _out) - .Stop () - .WriteOutLogs (_out); - Assert.True (clicked); - } - - [Theory] - [ClassData (typeof (TestDrivers))] - public void Toplevel_TabGroup_Forward_Backward (TestDriver d) - { - var v1 = new View { Id = "v1", CanFocus = true }; - var v2 = new View { Id = "v2", CanFocus = true }; - var v3 = new View { Id = "v3", CanFocus = true }; - var v4 = new View { Id = "v4", CanFocus = true }; - var v5 = new View { Id = "v5", CanFocus = true }; - var v6 = new View { Id = "v6", CanFocus = true }; - - using GuiTestContext c = With.A (50, 20, d) - .Then ( - () => - { - var w1 = new Window { Id = "w1" }; - w1.Add (v1, v2); - var w2 = new Window { Id = "w2" }; - w2.Add (v3, v4); - var w3 = new Window { Id = "w3" }; - w3.Add (v5, v6); - Toplevel top = Application.Top!; - Application.Top!.Add (w1, w2, w3); - }) - .WaitIteration () - .AssertTrue (v5.HasFocus) - .RaiseKeyDownEvent (Key.F6) - .AssertTrue (v1.HasFocus) - .RaiseKeyDownEvent (Key.F6) - .AssertTrue (v3.HasFocus) - .RaiseKeyDownEvent (Key.F6.WithShift) - .AssertTrue (v1.HasFocus) - .RaiseKeyDownEvent (Key.F6.WithShift) - .AssertTrue (v5.HasFocus) - .RaiseKeyDownEvent (Key.F6.WithShift) - .AssertTrue (v3.HasFocus) - .RaiseKeyDownEvent (Key.F6) - .AssertTrue (v5.HasFocus) - .RaiseKeyDownEvent (Key.F6) - .AssertTrue (v1.HasFocus) - .RaiseKeyDownEvent (Key.F6) - .AssertTrue (v3.HasFocus) - .RaiseKeyDownEvent (Key.F6.WithShift) - .AssertTrue (v1.HasFocus) - .RaiseKeyDownEvent (Key.F6.WithShift) - .AssertTrue (v5.HasFocus) - .RaiseKeyDownEvent (Key.F6.WithShift) - .AssertTrue (v3.HasFocus) - .RaiseKeyDownEvent (Key.Tab) - .AssertTrue (v4.HasFocus) - .RaiseKeyDownEvent (Key.F6) - .AssertTrue (v5.HasFocus) - .RaiseKeyDownEvent (Key.F6) - .AssertTrue (v1.HasFocus) - .RaiseKeyDownEvent (Key.F6.WithShift) - .AssertTrue (v5.HasFocus) - .RaiseKeyDownEvent (Key.Tab) - .AssertTrue (v6.HasFocus) - .RaiseKeyDownEvent (Key.F6.WithShift) - .AssertTrue (v4.HasFocus) - .RaiseKeyDownEvent (Key.F6) - .AssertTrue (v6.HasFocus) - .WriteOutLogs (_out) - .Stop (); - Assert.False (v1.HasFocus); - Assert.False (v2.HasFocus); - Assert.False (v3.HasFocus); - Assert.False (v4.HasFocus); - Assert.False (v5.HasFocus); - } -} diff --git a/Tests/IntegrationTests/FluentTests/FileDialogFluentTests.cs b/Tests/IntegrationTests/FluentTests/FileDialogFluentTests.cs index 3dd21f3ad..770078bb3 100644 --- a/Tests/IntegrationTests/FluentTests/FileDialogFluentTests.cs +++ b/Tests/IntegrationTests/FluentTests/FileDialogFluentTests.cs @@ -7,6 +7,7 @@ using TerminalGuiFluentTestingXunit; using Xunit.Abstractions; namespace IntegrationTests.FluentTests; + public class FileDialogFluentTests { private readonly TextWriter _out; @@ -46,7 +47,7 @@ public class FileDialogFluentTests return NewSaveDialog (out sd, out _, modal); } - private Toplevel NewSaveDialog (out SaveDialog sd, out MockFileSystem fs,bool modal = true) + private Toplevel NewSaveDialog (out SaveDialog sd, out MockFileSystem fs, bool modal = true) { fs = CreateExampleFileSystem (); sd = new SaveDialog (fs) { Modal = modal }; @@ -56,14 +57,13 @@ public class FileDialogFluentTests [Theory] [ClassData (typeof (TestDrivers))] - public void CancelFileDialog_UsingEscape (TestDriver d) + public void CancelFileDialog_QuitKey_Quits (TestDriver d) { SaveDialog? sd = null; - using var c = With.A (()=>NewSaveDialog(out sd), 100, 20, d) + using var c = With.A (() => NewSaveDialog (out sd), 100, 20, d) .ScreenShot ("Save dialog", _out) - .Escape () - .AssertTrue (sd!.Canceled) - .Stop (); + .EnqueueKeyEvent (Application.QuitKey) + .AssertTrue (sd!.Canceled); } [Theory] @@ -71,12 +71,11 @@ public class FileDialogFluentTests public void CancelFileDialog_UsingCancelButton_TabThenEnter (TestDriver d) { SaveDialog? sd = null; - using var c = With.A (() => NewSaveDialog (out sd,modal:false), 100, 20, d) + using var c = With.A (() => NewSaveDialog (out sd, modal: false), 100, 20, d) .ScreenShot ("Save dialog", _out) .Focus